go-html/codegen/codegen.go
Claude 937c08d9ed
feat(codegen): Web Component class generation from HLCRF slots
Generates Custom Element v1 classes with closed Shadow DOM from manifest
slot assignments. Includes tag validation, PascalCase conversion, and
bundle generation for full HLCRF layout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 20:49:45 +00:00

90 lines
2.4 KiB
Go

package codegen
import (
"fmt"
"strings"
"text/template"
)
// 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 "", fmt.Errorf("codegen: custom element tag %q must contain a hyphen", tag)
}
var b strings.Builder
err := wcTemplate.Execute(&b, struct {
ClassName, Tag, Slot string
}{
ClassName: TagToClassName(tag),
Tag: tag,
Slot: slot,
})
if err != nil {
return "", fmt.Errorf("codegen: template exec: %w", 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 {
parts := strings.Split(tag, "-")
var b strings.Builder
for _, p := range parts {
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) {
seen := make(map[string]bool)
var b strings.Builder
for slot, tag := range slots {
if seen[tag] {
continue
}
seen[tag] = true
cls, err := GenerateClass(tag, slot)
if err != nil {
return "", err
}
b.WriteString(cls)
b.WriteByte('\n')
b.WriteString(GenerateRegistration(tag, TagToClassName(tag)))
b.WriteByte('\n')
}
return b.String(), nil
}