From d8c94c354afb286b4fba9b883e49c1bd2c326bb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 19 Jul 2023 17:32:19 +0200 Subject: [PATCH] publisher: Improve class collector for dynamic classes E.g. * AlpinesJS' :class="isTrue 'class1' : 'class2'" * And dynamic classes with colon in them, e.g. `hover:bg-white` --- publisher/htmlElementsCollector.go | 56 ++++++++++++++++++++----- publisher/htmlElementsCollector_test.go | 6 ++- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/publisher/htmlElementsCollector.go b/publisher/htmlElementsCollector.go index c9d81818ce4..6c01fd8d9cb 100644 --- a/publisher/htmlElementsCollector.go +++ b/publisher/htmlElementsCollector.go @@ -32,7 +32,7 @@ const eof = -1 var ( htmlJsonFixer = strings.NewReplacer(", ", "\n") - jsonAttrRe = regexp.MustCompile(`'?(.*?)'?:.*`) + jsonAttrRe = regexp.MustCompile(`'?(.*?)'?:\s.*`) classAttrRe = regexp.MustCompile(`(?i)^class$|transition`) skipInnerElementRe = regexp.MustCompile(`(?i)^(pre|textarea|script|style)`) @@ -404,21 +404,31 @@ func (w *htmlElementsCollectorWriter) parseHTMLElement(elStr string) (el htmlEle if conf.DisableClasses { continue } + if classAttrRe.MatchString(a.Key) { el.Classes = append(el.Classes, strings.Fields(a.Val)...) } else { key := strings.ToLower(a.Key) val := strings.TrimSpace(a.Val) - if strings.Contains(key, "class") && strings.HasPrefix(val, "{") { - // This looks like a Vue or AlpineJS class binding. - val = htmlJsonFixer.Replace(strings.Trim(val, "{}")) - lines := strings.Split(val, "\n") - for i, l := range lines { - lines[i] = strings.TrimSpace(l) + + if strings.Contains(key, ":class") { + if strings.HasPrefix(val, "{") { + // This looks like a Vue or AlpineJS class binding. + val = htmlJsonFixer.Replace(strings.Trim(val, "{}")) + lines := strings.Split(val, "\n") + for i, l := range lines { + lines[i] = strings.TrimSpace(l) + } + val = strings.Join(lines, "\n") + + val = jsonAttrRe.ReplaceAllString(val, "$1") + + el.Classes = append(el.Classes, strings.Fields(val)...) } - val = strings.Join(lines, "\n") - val = jsonAttrRe.ReplaceAllString(val, "$1") - el.Classes = append(el.Classes, strings.Fields(val)...) + // Also add single quoted strings. + // This may introduce some false positives, but it covers some missing cases in the above. + // E.g. AlpinesJS' :class="isTrue 'class1' : 'class2'" + el.Classes = append(el.Classes, extractSingleQuotedStrings(val)...) } } } @@ -519,3 +529,29 @@ LOOP: func isSpace(b byte) bool { return b == ' ' || b == '\t' || b == '\n' } + +func extractSingleQuotedStrings(s string) []string { + var ( + inQuote bool + lo int + hi int + ) + + var words []string + + for i, r := range s { + switch { + case r == '\'': + if !inQuote { + inQuote = true + lo = i + 1 + } else { + inQuote = false + hi = i + words = append(words, strings.Fields(s[lo:hi])...) + } + } + } + + return words +} diff --git a/publisher/htmlElementsCollector_test.go b/publisher/htmlElementsCollector_test.go index 3047d5ca99e..3cd834acbac 100644 --- a/publisher/htmlElementsCollector_test.go +++ b/publisher/htmlElementsCollector_test.go @@ -99,6 +99,8 @@ func TestClassCollector(t *testing.T) { pl-2: b == 3, 'text-gray-600': (a > 1) }" class="block w-36 cursor-pointer pr-3 no-underline capitalize">`, f("a", "block capitalize cursor-pointer no-underline pl-2 pl-3 pr-3 text-a text-b text-gray-600 w-36", "")}, + {"AlpineJS bind 6", ``, f("button", "bg-white border-gray-500 border-t-2 border-transparent hover:bg-gray-100 pt", "")}, + {"AlpineJS bind 7", ``, f("button", "bg-white border-gray-500 border-t-2 border-transparent hover:bg-gray-100 pt", "")}, {"AlpineJS transition 1", `
`, f("div", "mobile:-translate-x-8 opacity-0 sm:-translate-y-8 transform", "")}, {"Vue bind", `
`, f("div", "active", "")}, // Issue #7746 @@ -136,7 +138,9 @@ func TestClassCollector(t *testing.T) { {minify: true}, } { - c.Run(fmt.Sprintf("%s--minify-%t", test.name, variant.minify), func(c *qt.C) { + name := fmt.Sprintf("%s--minify-%t", test.name, variant.minify) + + c.Run(name, func(c *qt.C) { w := newHTMLElementsCollectorWriter(newHTMLElementsCollector( config.BuildStats{Enable: true}, ))