package codegen import ( "fmt" "slices" "strconv" "strings" "text/template" log "dappco.re/go/core/log" ) // isValidCustomElementTag reports whether tag is a safe custom element name. // The generator rejects values that would fail at customElements.define() time. func isValidCustomElementTag(tag string) bool { if tag == "" || !strings.Contains(tag, "-") { return false } if tag[0] < 'a' || tag[0] > 'z' { return false } for i := range len(tag) { ch := tag[i] switch { case ch >= 'a' && ch <= 'z': case ch >= '0' && ch <= '9': case ch == '-' || ch == '.' || ch == '_': default: return false } } return true } // wcTemplate is the Web Component class template. // Uses closed Shadow DOM for isolation. Content is set via the shadow root's // DOM API using trusted go-html codegen output (never user input). var wcTemplate = template.Must(template.New("wc").Parse(`class {{.ClassName}} extends HTMLElement { #shadow; constructor() { super(); this.#shadow = this.attachShadow({ mode: "closed" }); } connectedCallback() { this.#shadow.textContent = ""; const slot = this.getAttribute("data-slot") || "{{.Slot}}"; this.dispatchEvent(new CustomEvent("wc-ready", { detail: { tag: "{{.Tag}}", slot } })); } render(html) { const tpl = document.createElement("template"); tpl.insertAdjacentHTML("afterbegin", html); this.#shadow.textContent = ""; this.#shadow.appendChild(tpl.content.cloneNode(true)); } }`)) // codegen/codegen.go: GenerateClass produces a JS class definition for a custom element. // // Example: cls, err := GenerateClass("nav-bar", "H") func GenerateClass(tag, slot string) (string, error) { if !isValidCustomElementTag(tag) { return "", log.E("codegen.GenerateClass", "custom element tag must be a lowercase hyphenated name: "+tag, nil) } var b strings.Builder err := wcTemplate.Execute(&b, struct { ClassName, Tag, Slot string }{ ClassName: TagToClassName(tag), Tag: tag, Slot: slot, }) if err != nil { return "", log.E("codegen.GenerateClass", "template exec", err) } return b.String(), nil } // codegen/codegen.go: GenerateRegistration produces the customElements.define() call. // // Example: GenerateRegistration("nav-bar", "NavBar") func GenerateRegistration(tag, className string) string { return fmt.Sprintf(`customElements.define("%s", %s);`, tag, className) } // codegen/codegen.go: TagToClassName converts a kebab-case tag to PascalCase class name. // // Example: className := TagToClassName("nav-bar") // NavBar func TagToClassName(tag string) string { var b strings.Builder for p := range strings.SplitSeq(tag, "-") { if len(p) > 0 { b.WriteString(strings.ToUpper(p[:1])) b.WriteString(p[1:]) } } return b.String() } // codegen/codegen.go: GenerateBundle produces all WC class definitions and registrations // for a set of HLCRF slot assignments. // // Example: js, err := GenerateBundle(map[string]string{"H":"nav-bar", "C":"main-content"}) func GenerateBundle(slots map[string]string) (string, error) { if err := validateSlotKeys(slots); err != nil { return "", err } var b strings.Builder for _, entry := range orderedSlotEntries(slots) { cls, err := GenerateClass(entry.Tag, entry.Slot) if err != nil { return "", err } b.WriteString(cls) b.WriteByte('\n') b.WriteString(GenerateRegistration(entry.Tag, TagToClassName(entry.Tag))) b.WriteByte('\n') } return b.String(), nil } // codegen/codegen.go: GenerateTypeDefinitions produces a TypeScript declaration file for the // generated custom elements. // // Example: dts, err := GenerateTypeDefinitions(map[string]string{"H":"nav-bar"}) func GenerateTypeDefinitions(slots map[string]string) (string, error) { if err := validateSlotKeys(slots); err != nil { return "", err } entries := orderedSlotEntries(slots) var b strings.Builder for _, entry := range entries { if !isValidCustomElementTag(entry.Tag) { continue } className := TagToClassName(entry.Tag) b.WriteString("export declare class ") b.WriteString(className) b.WriteString(" extends HTMLElement {\n") b.WriteString(" connectedCallback(): void;\n") b.WriteString(" render(html: string): void;\n") b.WriteString("}\n") } b.WriteString("\ndeclare global {\n") b.WriteString(" interface WcReadyDetail {\n") b.WriteString(" tag: string;\n") b.WriteString(" slot: string;\n") b.WriteString(" }\n") b.WriteString(" interface HTMLElementEventMap {\n") b.WriteString(` "wc-ready": CustomEvent;` + "\n") b.WriteString(" }\n") b.WriteString(" interface HTMLElementTagNameMap {\n") for _, entry := range entries { if !isValidCustomElementTag(entry.Tag) { continue } className := TagToClassName(entry.Tag) b.WriteString(` "`) b.WriteString(entry.Tag) b.WriteString(`": `) b.WriteString(className) b.WriteString(";\n") } b.WriteString(" }\n") b.WriteString("}\n") b.WriteString("\nexport {};\n") return b.String(), nil } type slotEntry struct { Slot string Tag string } var canonicalSlotOrder = []string{"H", "L", "C", "R", "F"} func validateSlotKeys(slots map[string]string) error { if len(slots) == 0 { return nil } valid := map[string]struct{}{ "H": {}, "L": {}, "C": {}, "R": {}, "F": {}, } var invalid []string for slot := range slots { if _, ok := valid[slot]; ok { continue } invalid = append(invalid, slot) } if len(invalid) == 0 { return nil } slices.Sort(invalid) quoted := make([]string, 0, len(invalid)) for _, slot := range invalid { quoted = append(quoted, strconv.Quote(slot)) } return log.E("codegen", "invalid slot key(s): "+strings.Join(quoted, ", "), nil) } func orderedSlotEntries(slots map[string]string) []slotEntry { entries := make([]slotEntry, 0, len(slots)) seenTags := make(map[string]struct{}, len(slots)) for _, slot := range canonicalSlotOrder { tag, ok := slots[slot] if !ok { continue } if _, seen := seenTags[tag]; seen { continue } seenTags[tag] = struct{}{} entries = append(entries, slotEntry{Slot: slot, Tag: tag}) } return entries }