summaryrefslogtreecommitdiffstats
path: root/internal/builder/components.go
diff options
context:
space:
mode:
authorIvar Løvlie <38570165+ivarlovlie@users.noreply.github.com>2026-03-31 12:27:46 +0200
committerGitHub <noreply@github.com>2026-03-31 12:27:46 +0200
commit8d7cda6e578e684483c0b5c7391c48e5b9ac5192 (patch)
treed2b6506db2de72b3a6982cfbe69925b88936de90 /internal/builder/components.go
parent33f214f6cd9729473bb55fd7b3b923d5d960bb98 (diff)
parent3cb7c82cf7c4e050148f69be23590a7fbe587a27 (diff)
downloadnebbet.no-8d7cda6e578e684483c0b5c7391c48e5b9ac5192.tar.xz
nebbet.no-8d7cda6e578e684483c0b5c7391c48e5b9ac5192.zip
Merge pull request #1 from ivarlovlie/claude/static-site-sqlite-setup-mrcAr
Diffstat (limited to 'internal/builder/components.go')
-rw-r--r--internal/builder/components.go97
1 files changed, 97 insertions, 0 deletions
diff --git a/internal/builder/components.go b/internal/builder/components.go
new file mode 100644
index 0000000..54a226a
--- /dev/null
+++ b/internal/builder/components.go
@@ -0,0 +1,97 @@
+package builder
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+)
+
+// componentRe matches <!-- component:tag-name { ...json... } -->
+// Props JSON is optional.
+var componentRe = regexp.MustCompile(
+ `<!--\s*component:([a-z][a-z0-9-]*)\s*(\{[^}]*\})?\s*-->`)
+
+// customElementRe matches opening tags for custom elements (name must contain a hyphen).
+var customElementRe = regexp.MustCompile(`<([a-z][a-z0-9]*(?:-[a-z0-9]+)+)[\s/>]`)
+
+// ProcessComponents replaces HTML comment component directives with custom element tags.
+//
+// <!-- component:my-counter {"start": 5, "label": "Count"} -->
+// → <my-counter start="5" label="Count"></my-counter>
+func ProcessComponents(html string) string {
+ return componentRe.ReplaceAllStringFunc(html, func(match string) string {
+ subs := componentRe.FindStringSubmatch(match)
+ if len(subs) < 2 {
+ return match
+ }
+ tagName := subs[1]
+ attrs := ""
+ if len(subs) > 2 && subs[2] != "" {
+ var props map[string]any
+ if err := json.Unmarshal([]byte(subs[2]), &props); err == nil {
+ attrs = propsToAttrs(props)
+ }
+ }
+ if attrs != "" {
+ return fmt.Sprintf(`<%s %s></%s>`, tagName, attrs, tagName)
+ }
+ return fmt.Sprintf(`<%s></%s>`, tagName, tagName)
+ })
+}
+
+// propsToAttrs converts a JSON props map to an HTML attribute string.
+// Keys are emitted in sorted order for deterministic output.
+func propsToAttrs(props map[string]any) string {
+ keys := make([]string, 0, len(props))
+ for k := range props {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ var parts []string
+ for _, k := range keys {
+ v := props[k]
+ switch val := v.(type) {
+ case string:
+ parts = append(parts, fmt.Sprintf(`%s="%s"`, k, strings.ReplaceAll(val, `"`, `&quot;`)))
+ case bool:
+ if val {
+ parts = append(parts, k) // boolean attribute, no value
+ }
+ case float64:
+ if val == float64(int64(val)) {
+ parts = append(parts, fmt.Sprintf(`%s="%d"`, k, int64(val)))
+ } else {
+ parts = append(parts, fmt.Sprintf(`%s="%g"`, k, val))
+ }
+ default:
+ // Complex value → JSON-encode into single-quoted attribute.
+ b, _ := json.Marshal(v)
+ parts = append(parts, fmt.Sprintf(`%s='%s'`, k, string(b)))
+ }
+ }
+ return strings.Join(parts, " ")
+}
+
+// FindComponentScripts scans HTML for used custom elements and returns
+// /components/<name>.js paths for any that exist on disk.
+func FindComponentScripts(html, componentsDir string) []string {
+ matches := customElementRe.FindAllStringSubmatch(html, -1)
+ seen := make(map[string]bool)
+ var scripts []string
+ for _, m := range matches {
+ if len(m) < 2 || seen[m[1]] {
+ continue
+ }
+ seen[m[1]] = true
+ jsPath := filepath.Join(componentsDir, m[1]+".js")
+ if _, err := os.Stat(jsPath); err == nil {
+ scripts = append(scripts, "/components/"+m[1]+".js")
+ }
+ }
+ return scripts
+}