// SPDX-Licence-Identifier: EUPL-1.2 package help import ( "encoding/json" "os" "path/filepath" ) // searchIndexEntry represents a single topic in the client-side search index. type searchIndexEntry struct { ID string `json:"id"` Title string `json:"title"` Tags []string `json:"tags"` Content string `json:"content"` } // Generate writes a complete static help site to outputDir. // It creates: // - index.html -- topic listing grouped by tags // - topics/{id}.html -- one page per topic // - search.html -- client-side search with inline JS // - search-index.json -- JSON index for client-side search // - 404.html -- not found page // // All CSS is inlined; no external stylesheets are needed. func Generate(catalog *Catalog, outputDir string) error { topics := catalog.List() // Ensure output directories exist. topicsDir := filepath.Join(outputDir, "topics") if err := os.MkdirAll(topicsDir, 0o755); err != nil { return err } // 1. index.html if err := writeFile(outputDir, "index.html", RenderIndexPage(topics)); err != nil { return err } // 2. topics/{id}.html -- one per topic for _, t := range topics { if err := writeFile(topicsDir, t.ID+".html", RenderTopicPage(t, topics)); err != nil { return err } } // 3. search.html -- client-side search page if err := writeFile(outputDir, "search.html", RenderSearchPage("", nil)+clientSearchScript); err != nil { return err } // 4. search-index.json if err := writeSearchIndex(outputDir, topics); err != nil { return err } // 5. 404.html if err := writeFile(outputDir, "404.html", Render404Page()); err != nil { return err } return nil } // writeFile writes content to a file in the given directory. func writeFile(dir, filename, content string) error { path := filepath.Join(dir, filename) return os.WriteFile(path, []byte(content), 0o644) } // writeSearchIndex writes the JSON search index for client-side search. func writeSearchIndex(outputDir string, topics []*Topic) error { entries := make([]searchIndexEntry, 0, len(topics)) for _, t := range topics { // Truncate content for the index to keep file size reasonable. content := t.Content runes := []rune(content) if len(runes) > 500 { content = string(runes[:500]) } entries = append(entries, searchIndexEntry{ ID: t.ID, Title: t.Title, Tags: t.Tags, Content: content, }) } path := filepath.Join(outputDir, "search-index.json") f, err := os.Create(path) if err != nil { return err } defer f.Close() enc := json.NewEncoder(f) enc.SetIndent("", " ") return enc.Encode(entries) } // clientSearchScript is the inline JS for static-site client-side search. // All values are escaped via escapeHTML() before DOM insertion. // The search index is generated from our own catalog data, not user input. const clientSearchScript = ` `