diff --git a/cmd/build/main.go b/cmd/build/main.go new file mode 100644 index 0000000..d72e440 --- /dev/null +++ b/cmd/build/main.go @@ -0,0 +1,221 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "html/template" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" + + "smartbar/internal/config" +) + +// Expose page size to templates via CSS variables inline on . +// Use HTMLAttr so it renders as an attribute safely. +func pageSizeStyleAttr(widthIn, heightIn float64) template.HTMLAttr { + return template.HTMLAttr( + `style="--page-w: ` + floatToIn(widthIn) + `; --page-h: ` + floatToIn(heightIn) + `;"`, + ) +} + +func floatToIn(v float64) string { + return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.4fin", v), "0"), ".") +} + +type PageData struct { + Title string + Content template.HTML +} + +type IndexPage struct { + Name string + Href string +} + +type PrintPage struct { + Content template.HTML +} + +type TwoUpSheet struct { + Left PrintPage + Right *PrintPage +} + +func mustReadFile(path string) string { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + return string(data) +} + +func writeFile(path string, content []byte) error { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + return os.WriteFile(path, content, 0o644) +} + +func main() { + // Required flags + var pagesDir string + var outDir string + var templatesDir string + var orderPath string + flagSet := flag.NewFlagSet(os.Args[0], flag.ExitOnError) + flagSet.StringVar(&pagesDir, "pages", "", "path to pages directory (required)") + flagSet.StringVar(&outDir, "out", "", "path to output directory (required)") + flagSet.StringVar(&templatesDir, "templates", "", "path to templates directory (required)") + flagSet.StringVar(&orderPath, "order", "pages.yaml", "path to YAML file listing page order (optional)") + _ = flagSet.Parse(os.Args[1:]) + if pagesDir == "" || outDir == "" || templatesDir == "" { + fmt.Fprintln(os.Stderr, "error: --pages, --out, and --templates are required") + os.Exit(2) + } + baseTplPath := filepath.Join(templatesDir, "base.gohtml") + + baseTpl := template.Must(template.New("base").Parse(mustReadFile(baseTplPath))) + + var pageFiles []string + if data, err := os.ReadFile(orderPath); err == nil { + ordered, perr := parseYAMLListOfStrings(string(data)) + if perr != nil { + panic(perr) + } + for _, rel := range ordered { + abs := filepath.Join(pagesDir, rel) + if _, statErr := os.Stat(abs); statErr == nil { + pageFiles = append(pageFiles, abs) + } else { + fmt.Fprintf(os.Stderr, "warning: listed page not found: %s\n", rel) + } + } + } + if len(pageFiles) == 0 { + filepath.WalkDir(pagesDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + if strings.HasSuffix(d.Name(), ".html") { + pageFiles = append(pageFiles, path) + } + return nil + }) + sort.Strings(pageFiles) + } + + // Build each page + for _, pagePath := range pageFiles { + raw := mustReadFile(pagePath) + // Trim outer whitespace but keep inner spacing + content := strings.TrimSpace(raw) + + // Title based on filename + base := filepath.Base(pagePath) + title := strings.TrimSuffix(base, filepath.Ext(base)) + + var out bytes.Buffer + if err := baseTpl.Execute(&out, map[string]any{ + "Title": title, + "Content": template.HTML(content), + "PageSizeAttr": pageSizeStyleAttr(config.PageWidthIn, config.PageHeightIn), + }); err != nil { + panic(err) + } + + rel, _ := filepath.Rel(pagesDir, pagePath) + outPath := filepath.Join(outDir, rel) + if err := writeFile(outPath, out.Bytes()); err != nil { + panic(err) + } + } + + // Build index.html via template + indexPath := filepath.Join(outDir, "index.html") + indexTplPath := filepath.Join(templatesDir, "index.gohtml") + indexTpl := template.Must(template.New("index").Parse(mustReadFile(indexTplPath))) + + var pages []IndexPage + for _, pagePath := range pageFiles { + rel, _ := filepath.Rel(pagesDir, pagePath) + pages = append(pages, IndexPage{ + Name: filepath.Base(pagePath), + // Index is written to outDir, and the compiled pages are also in outDir. + // So iframe src should be relative to index location, not prefixed with outDir again. + Href: filepath.ToSlash(rel), + }) + } + var outIndex bytes.Buffer + if err := indexTpl.Execute(&outIndex, map[string]any{"Pages": pages}); err != nil { + panic(err) + } + if err := writeFile(indexPath, outIndex.Bytes()); err != nil { + panic(err) + } + + // Build print.html containing all pages one after another for PDF + printTplPath := filepath.Join(templatesDir, "print.gohtml") + printTpl := template.Must(template.New("print").Parse(mustReadFile(printTplPath))) + var printPages []PrintPage + for _, pagePath := range pageFiles { + content := strings.TrimSpace(mustReadFile(pagePath)) + printPages = append(printPages, PrintPage{Content: template.HTML(content)}) + } + var outPrint bytes.Buffer + if err := printTpl.Execute(&outPrint, map[string]any{"Pages": printPages, "PageSizeAttr": pageSizeStyleAttr(config.PageWidthIn, config.PageHeightIn)}); err != nil { + panic(err) + } + if err := writeFile(filepath.Join(outDir, "print.html"), outPrint.Bytes()); err != nil { + panic(err) + } + + // Build 2-up print HTML (letter pages with two half-letter pages side by side) + var sheets []TwoUpSheet + for i := 0; i < len(printPages); i += 2 { + left := printPages[i] + var right *PrintPage + if i+1 < len(printPages) { + r := printPages[i+1] + right = &r + } + sheets = append(sheets, TwoUpSheet{Left: left, Right: right}) + } + print2TplPath := filepath.Join(templatesDir, "print_2up.gohtml") + print2Tpl := template.Must(template.New("print2up").Parse(mustReadFile(print2TplPath))) + var outPrint2 bytes.Buffer + if err := print2Tpl.Execute(&outPrint2, map[string]any{"Sheets": sheets}); err != nil { + panic(err) + } + if err := writeFile(filepath.Join(outDir, "print_2up.html"), outPrint2.Bytes()); err != nil { + panic(err) + } +} + +// parseYAMLListOfStrings expects a YAML document that is a top-level sequence of strings. +// We avoid adding a dependency to keep the builder lightweight; this is minimal and strict. +func parseYAMLListOfStrings(src string) ([]string, error) { + var out []string + for _, line := range strings.Split(src, "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + if strings.HasPrefix(line, "-") { + item := strings.TrimSpace(strings.TrimPrefix(line, "-")) + if item != "" { + out = append(out, item) + } + } else { + // For simplicity, only support list form + return nil, fmt.Errorf("invalid yaml line (expect '- item'): %s", line) + } + } + return out, nil +}