diff options
Diffstat (limited to 'internal/builder/components.go')
| -rw-r--r-- | internal/builder/components.go | 97 |
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, `"`, `"`))) + 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 +} |
