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 }