feat: add website builder using goldmark (replaces build.js)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
51
internal/builder/builder.go
Normal file
51
internal/builder/builder.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
)
|
||||
|
||||
// Build converts a Markdown file to HTML using a template.
|
||||
// The template must contain a placeholder element that will be replaced with the rendered content.
|
||||
func Build(markdownPath, templatePath, outputPath string) error {
|
||||
md, err := os.ReadFile(markdownPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read markdown: %w", err)
|
||||
}
|
||||
|
||||
tmpl, err := os.ReadFile(templatePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read template: %w", err)
|
||||
}
|
||||
|
||||
// Convert markdown to HTML
|
||||
gm := goldmark.New(
|
||||
goldmark.WithExtensions(extension.GFM),
|
||||
goldmark.WithRendererOptions(html.WithUnsafe()),
|
||||
)
|
||||
var buf bytes.Buffer
|
||||
if err := gm.Convert(md, &buf); err != nil {
|
||||
return fmt.Errorf("convert markdown: %w", err)
|
||||
}
|
||||
|
||||
// Inject into template — support both placeholder formats
|
||||
output := string(tmpl)
|
||||
replacements := []struct{ old, new string }{
|
||||
{`<div id="md"></div>`, `<div id="md">` + buf.String() + `</div>`},
|
||||
{`<section id="md" class="main-content"></section>`, `<section id="md" class="main-content">` + buf.String() + `</section>`},
|
||||
}
|
||||
for _, r := range replacements {
|
||||
output = strings.Replace(output, r.old, r.new, 1)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(outputPath, []byte(output), 0644); err != nil {
|
||||
return fmt.Errorf("write output: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
113
internal/builder/builder_test.go
Normal file
113
internal/builder/builder_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
md := "# Test List\n\n- [Example](https://example.com) - A test entry.\n"
|
||||
mdPath := filepath.Join(dir, "README.md")
|
||||
if err := os.WriteFile(mdPath, []byte(md), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tmpl := `<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div id="md"></div>
|
||||
</body>
|
||||
</html>`
|
||||
tmplPath := filepath.Join(dir, "template.html")
|
||||
if err := os.WriteFile(tmplPath, []byte(tmpl), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
outPath := filepath.Join(dir, "index.html")
|
||||
if err := Build(mdPath, tmplPath, outPath); err != nil {
|
||||
t.Fatalf("Build failed: %v", err)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(outPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
html := string(content)
|
||||
if !strings.Contains(html, "Test List") {
|
||||
t.Error("expected 'Test List' in output")
|
||||
}
|
||||
if !strings.Contains(html, "https://example.com") {
|
||||
t.Error("expected link in output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildWithSectionPlaceholder(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
md := "# Hello\n\nWorld.\n"
|
||||
mdPath := filepath.Join(dir, "README.md")
|
||||
if err := os.WriteFile(mdPath, []byte(md), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// This matches the actual template format
|
||||
tmpl := `<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<section id="md" class="main-content"></section>
|
||||
</body>
|
||||
</html>`
|
||||
tmplPath := filepath.Join(dir, "template.html")
|
||||
if err := os.WriteFile(tmplPath, []byte(tmpl), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
outPath := filepath.Join(dir, "index.html")
|
||||
if err := Build(mdPath, tmplPath, outPath); err != nil {
|
||||
t.Fatalf("Build failed: %v", err)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(outPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(content), "Hello") {
|
||||
t.Error("expected 'Hello' in output")
|
||||
}
|
||||
if !strings.Contains(string(content), `class="main-content"`) {
|
||||
t.Error("expected section class preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRealREADME(t *testing.T) {
|
||||
mdPath := "../../README.md"
|
||||
tmplPath := "../../config/website.tmpl.html"
|
||||
if _, err := os.Stat(mdPath); err != nil {
|
||||
t.Skip("README.md not found")
|
||||
}
|
||||
if _, err := os.Stat(tmplPath); err != nil {
|
||||
t.Skip("website template not found")
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
outPath := filepath.Join(dir, "index.html")
|
||||
|
||||
if err := Build(mdPath, tmplPath, outPath); err != nil {
|
||||
t.Fatalf("Build failed: %v", err)
|
||||
}
|
||||
|
||||
info, err := os.Stat(outPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info.Size() < 10000 {
|
||||
t.Errorf("output too small: %d bytes", info.Size())
|
||||
}
|
||||
t.Logf("Generated %d bytes", info.Size())
|
||||
}
|
||||
Reference in New Issue
Block a user