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. // // cls, err := GenerateClass("nav-bar", "H") 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. // // js := GenerateRegistration("nav-bar", "NavBar") 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. // // 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() } // GenerateBundle produces all WC class definitions and registrations // for a set of HLCRF slot assignments. // // js, err := GenerateBundle(map[string]string{"H":"nav-bar", "C":"main-content"}) func GenerateBundle(slots map[string]string) (string, error) { 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 } // GenerateTypeDefinitions produces a TypeScript declaration file for the // generated custom elements. // // dts, err := GenerateTypeDefinitions(map[string]string{"H":"nav-bar"}) func GenerateTypeDefinitions(slots map[string]string) (string, error) { entries := orderedSlotEntries(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 } var canonicalSlotOrder = []string{"H", "L", "C", "R", "F"} func orderedSlotEntries(slots map[string]string) []slotEntry { seenSlots := make(map[string]bool, len(slots)) entries := make([]slotEntry, 0, len(slots)) for _, slot := range canonicalSlotOrder { tag, ok := slots[slot] if !ok { continue } if seenSlots[tag] { continue } seenSlots[tag] = true entries = append(entries, slotEntry{Slot: slot, Tag: tag}) } keys := slices.Collect(maps.Keys(slots)) slices.Sort(keys) for _, slot := range keys { if slot == "H" || slot == "L" || slot == "C" || slot == "R" || slot == "F" { continue } tag := slots[slot] if seenSlots[tag] { continue } seenSlots[tag] = true entries = append(entries, slotEntry{Slot: slot, Tag: tag}) } return entries }