From 3cb7c82cf7c4e050148f69be23590a7fbe587a27 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 31 Mar 2026 10:11:18 +0000 Subject: Add static site builder: SQLite-backed MD→HTML pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cmd/nebbet: CLI with build [--watch] and user add/passwd/delete/list - internal/builder: markdown→HTML, component injection via HTML comments, auto importmap from lib/, fsnotify watch with 150ms debounce - internal/db: meta.db (page index, tag queries) + search.db (FTS5) - internal/sqlitedrv: minimal CGO database/sql driver for system libsqlite3 - internal/auth: htpasswd-compatible bcrypt password file management - templates/base.html + admin.html, styles/main.css + admin.css - nginx.conf with auth_basic for /admin, clean URLs, gzip - nebbet.service systemd unit for watch daemon - Example content/index.md and components/site-greeting.js https://claude.ai/code/session_01HTc1BCBCiMTEB54XQP1Wz9 --- internal/builder/components.go | 97 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 internal/builder/components.go (limited to 'internal/builder/components.go') diff --git a/internal/builder/components.go b/internal/builder/components.go new file mode 100644 index 0000000..54a226a --- /dev/null +++ b/internal/builder/components.go @@ -0,0 +1,97 @@ +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 +} -- cgit v1.3