package codegen import ( "fmt" "maps" "slices" "strings" "text/template" log "dappco.re/go/core/log" ) // 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)); } }`)) // GenerateClass produces a JS class definition for a custom element. func GenerateClass(tag, slot string) (string, error) { if !strings.Contains(tag, "-") { return "", log.E("codegen.GenerateClass", "custom element tag must contain a hyphen: "+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 } // GenerateRegistration produces the customElements.define() call. func GenerateRegistration(tag, className string) string { return fmt.Sprintf(`customElements.define("%s", %s);`, tag, className) } // TagToClassName converts a kebab-case tag to PascalCase class name. 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() } // GenerateBundle produces all WC class definitions and registrations // for a set of HLCRF slot assignments. func GenerateBundle(slots map[string]string) (string, error) { var b strings.Builder for _, entry := range sortedSlotEntries(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 } // GenerateTypeDefinitions produces a TypeScript declaration file for the // generated custom elements. func GenerateTypeDefinitions(slots map[string]string) (string, error) { entries := sortedSlotEntries(slots) var b strings.Builder for _, entry := range entries { className := TagToClassName(entry.Tag) b.WriteString("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 HTMLElementTagNameMap {\n") for _, entry := range entries { 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 } func sortedSlotEntries(slots map[string]string) []slotEntry { keys := slices.Collect(maps.Keys(slots)) slices.Sort(keys) seen := make(map[string]bool, len(keys)) entries := make([]slotEntry, 0, len(keys)) for _, slot := range keys { tag := slots[slot] if seen[tag] { continue } seen[tag] = true entries = append(entries, slotEntry{Slot: slot, Tag: tag}) } return entries }