Skip to content

Commit 790b7e5

Browse files
committed
feat: First working version
1 parent 7358c1d commit 790b7e5

File tree

5 files changed

+405
-0
lines changed

5 files changed

+405
-0
lines changed

pkg/twmerge/class-utils.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package twmerge
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
)
7+
8+
type GetClassGroupIdfn func(string) (isTwClass bool, groupId string)
9+
10+
func MakeGetClassGroupId(conf *TwMergeConfig) GetClassGroupIdfn {
11+
var getClassGroupIdRecursive func(classParts []string, i int, classMap *ClassPart) (isTwClass bool, groupId string)
12+
getClassGroupIdRecursive = func(classParts []string, i int, classMap *ClassPart) (isTwClass bool, groupId string) {
13+
if i >= len(classParts) {
14+
if classMap.ClassGroupId != "" {
15+
return true, classMap.ClassGroupId
16+
}
17+
18+
return false, ""
19+
}
20+
21+
if classMap.NextPart != nil {
22+
nextClassMap := classMap.NextPart[classParts[i]]
23+
isTw, id := getClassGroupIdRecursive(classParts, i+1, &nextClassMap)
24+
if isTw {
25+
return isTw, id
26+
}
27+
}
28+
29+
if classMap.Validators != nil && len(classMap.Validators) > 0 {
30+
remainingClass := strings.Join(classParts[i:], string(conf.ClassSeparator))
31+
32+
for _, validator := range classMap.Validators {
33+
if validator.Fn(remainingClass) {
34+
return true, validator.ClassGroupId
35+
}
36+
}
37+
38+
}
39+
return false, ""
40+
}
41+
42+
var arbitraryPropertyRegex = regexp.MustCompile(`^\[(.+)\]$`)
43+
44+
getGroupIdForArbitraryProperty := func(class string) (bool, string) {
45+
if arbitraryPropertyRegex.MatchString(class) {
46+
arbitraryPropertyClassName := arbitraryPropertyRegex.FindStringSubmatch(class)[1]
47+
property := arbitraryPropertyClassName[:strings.Index(arbitraryPropertyClassName, ":")]
48+
49+
if property != "" {
50+
// I use two dots here because one dot is used as prefix for class groups in plugins
51+
return true, "arbitrary.." + property
52+
}
53+
}
54+
55+
return false, ""
56+
}
57+
58+
return func(baseClass string) (isTwClass bool, groupdId string) {
59+
60+
classParts := strings.Split(baseClass, string(conf.ClassSeparator))
61+
// remove first element if empty for things like -px-4
62+
if len(classParts) > 0 && classParts[0] == "" {
63+
classParts = classParts[1:]
64+
}
65+
isTwClass, groupId := getClassGroupIdRecursive(classParts, 0, &conf.ClassGroups)
66+
if isTwClass {
67+
return isTwClass, groupId
68+
}
69+
70+
return getGroupIdForArbitraryProperty(baseClass)
71+
}
72+
73+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package twmerge
2+
3+
import (
4+
"strings"
5+
6+
lru "github.com/Oudwins/tailwind-merge-go/pkg/cache"
7+
)
8+
9+
// create the config (just gets the config passed in)
10+
11+
// create the config utils
12+
// LRU cache
13+
// split modifiers
14+
// -> for things like hover:bg-x
15+
// class utils
16+
// -> for splitting classes
17+
18+
// cache get & set
19+
20+
// merge fn
21+
// 1. check cache
22+
// 2. mergeClassList
23+
// 3. set cache
24+
25+
// should this also take a cache directly?
26+
func CreateTwMerge(config *TwMergeConfig, cache lru.Cache) func(args ...string) string {
27+
if config == nil {
28+
config = MakeDefaultConfig()
29+
}
30+
if cache == nil {
31+
cache = lru.Make(config.MaxCacheSize)
32+
}
33+
34+
splitModifiers := MakeSplitModifiers(config)
35+
36+
getClassGroupId := MakeGetClassGroupId(config)
37+
38+
mergeClassList := MakeMergeClassList(config, splitModifiers, getClassGroupId)
39+
40+
return func(args ...string) string {
41+
classList := strings.Join(args, " ")
42+
cached := cache.Get(classList)
43+
if cached != "" {
44+
return cached
45+
}
46+
// check if in cache
47+
merged := mergeClassList(classList)
48+
cache.Set(classList, merged)
49+
return merged
50+
}
51+
}
52+
53+
var Merge = CreateTwMerge(nil, nil)

pkg/twmerge/merge-classlist.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package twmerge
2+
3+
import (
4+
"regexp"
5+
"slices"
6+
"strings"
7+
)
8+
9+
const SPLIT_CLASSES_REGEX = `\s+`
10+
11+
var splitPattern = regexp.MustCompile(SPLIT_CLASSES_REGEX)
12+
13+
func MakeMergeClassList(conf *TwMergeConfig, splitModifiers SplitModifiersFn, getClassGroupId GetClassGroupIdfn) func(classList string) string {
14+
return func(classList string) string {
15+
classes := splitPattern.Split(strings.TrimSpace(classList), -1)
16+
unqClasses := make(map[string]string, len(classes))
17+
resultClassList := ""
18+
19+
for _, class := range classes {
20+
baseClass, modifiers, hasImportant, maybePostfixModPosition := splitModifiers(class)
21+
22+
// there is a postfix modifier -> text-lg/8
23+
if maybePostfixModPosition != -1 {
24+
baseClass = baseClass[:maybePostfixModPosition]
25+
}
26+
isTwClass, groupId := getClassGroupId(baseClass)
27+
if !isTwClass {
28+
resultClassList += class + " "
29+
continue
30+
}
31+
// we have to sort the modifiers bc hover:focus:bg-red-500 == focus:hover:bg-red-500
32+
modifiers = SortModifiers(modifiers)
33+
if hasImportant {
34+
modifiers = append(modifiers, "!")
35+
}
36+
unqClasses[groupId+strings.Join(modifiers, string(conf.ModifierSeparator))] = class
37+
38+
conflicts := conf.ConflictingClassGroups[groupId]
39+
if conflicts == nil {
40+
continue
41+
}
42+
for _, conflict := range conflicts {
43+
// erase the conflicts with the same modifiers
44+
unqClasses[conflict+strings.Join(modifiers, string(conf.ModifierSeparator))] = ""
45+
}
46+
}
47+
48+
for _, class := range unqClasses {
49+
if class == "" {
50+
continue
51+
}
52+
resultClassList += class + " "
53+
}
54+
return strings.TrimSpace(resultClassList)
55+
}
56+
57+
}
58+
59+
/**
60+
* Sorts modifiers according to following schema:
61+
* - Predefined modifiers are sorted alphabetically
62+
* - When an arbitrary variant appears, it must be preserved which modifiers are before and after it
63+
*/
64+
func SortModifiers(modifiers []string) []string {
65+
if modifiers == nil || len(modifiers) < 2 {
66+
return modifiers
67+
}
68+
69+
unsortedModifiers := []string{}
70+
sorted := make([]string, len(modifiers))
71+
72+
for _, modifier := range modifiers {
73+
isArbitraryVariant := modifier[0] == '['
74+
if isArbitraryVariant {
75+
slices.Sort(unsortedModifiers)
76+
sorted = append(sorted, unsortedModifiers...)
77+
sorted = append(sorted, modifier)
78+
unsortedModifiers = []string{}
79+
continue
80+
}
81+
unsortedModifiers = append(unsortedModifiers, modifier)
82+
}
83+
84+
slices.Sort(unsortedModifiers)
85+
sorted = append(sorted, unsortedModifiers...)
86+
87+
return sorted
88+
}

pkg/twmerge/modifier-utils.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package twmerge
2+
3+
type SplitModifiersFn = func(string) (baseClass string, modifiers []string, hasImportant bool, maybePostfixModPosition int)
4+
5+
func MakeSplitModifiers(conf *TwMergeConfig) SplitModifiersFn {
6+
separator := conf.ModifierSeparator
7+
8+
return func(className string) (string, []string, bool, int) {
9+
modifiers := []string{}
10+
modifierStart := 0
11+
bracketDepth := 0
12+
// used for bg-red-500/50 (50% opacity)
13+
maybePostfixModPosition := -1
14+
15+
for i := 0; i < len(className); i++ {
16+
char := rune(className[i])
17+
18+
if char == '[' {
19+
bracketDepth++
20+
continue
21+
}
22+
if char == ']' {
23+
bracketDepth--
24+
continue
25+
}
26+
27+
if bracketDepth == 0 {
28+
if char == separator {
29+
modifiers = append(modifiers, className[modifierStart:i])
30+
modifierStart = i + 1
31+
continue
32+
}
33+
34+
if char == conf.PostfixModifier {
35+
maybePostfixModPosition = i
36+
}
37+
}
38+
}
39+
40+
baseClassWithImportant := className[modifierStart:]
41+
hasImportant := baseClassWithImportant[0] == byte(conf.ImportantModifier)
42+
var baseClass string
43+
if hasImportant {
44+
baseClass = baseClassWithImportant[1:]
45+
} else {
46+
baseClass = baseClassWithImportant
47+
}
48+
49+
return baseClass, modifiers, hasImportant, maybePostfixModPosition
50+
51+
}
52+
}

0 commit comments

Comments
 (0)