package builder import ( "encoding/json" "fmt" "os" "path/filepath" "regexp" "sort" "strings" ) // componentRe matches // Props JSON is optional. var componentRe = regexp.MustCompile( ``) // 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. // // // → 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>`, tagName, attrs, tagName) } return fmt.Sprintf(`<%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/.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 }