feat(11.1-02): add the documentation theme, chroma highlighting and docs:serve
- WinterCMS-style shell: header with search and theme toggle, grouped sidebar, on-page TOC, pager, page actions, callouts, heading permalinks, footer and a 404 page - fenced code highlighted at build time by chroma/v2 into tok-* classes, with a copy button; no inline script, style or handler - vendored DM Sans/DM Mono fonts and Lucide icons with their licences - client-side search over search-index.json built with textContent only - summer docs:serve builds into a temp dir, serves on loopback by default, returns 404.html with status 404 and rebuilds on change
This commit is contained in:
@@ -3,6 +3,10 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"git.golem15.com/golem15/summercms/internal/docsite"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
@@ -79,6 +83,43 @@ func docsSyncCommand() bonfire.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func docsServeCommand() bonfire.Command {
|
||||
return bonfire.Command{
|
||||
Name: "docs:serve",
|
||||
Description: "Build the documentation site and preview it on a local address",
|
||||
Flags: []bonfire.Flag{
|
||||
{Name: "root", Description: "Repository root; src= paths and modules/ resolve against it", Default: "."},
|
||||
{Name: "src", Description: "Docs source directory (default <root>/docs)"},
|
||||
{Name: "base-url", Description: "Base URL for site links (overrides site.yaml base_url)"},
|
||||
{Name: "addr", Description: "Listen address; must be loopback unless --allow-remote", Default: docsite.DefaultServeAddr},
|
||||
{Name: "allow-remote", Description: "Allow a non-loopback --addr (serves the docs on the network)", Bare: true},
|
||||
},
|
||||
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||
addr, _ := in.Flag("addr")
|
||||
if addr == "" {
|
||||
addr = docsite.DefaultServeAddr
|
||||
}
|
||||
ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
return docsite.Serve(ctx, docsOptions(in), addr, flagTrue(in, "allow-remote"), outputWriter{mu: &sync.Mutex{}, out: out})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// outputWriter adapts bonfire.Output to io.Writer for docsite.Serve, which
|
||||
// writes from its watch goroutine too.
|
||||
type outputWriter struct {
|
||||
mu *sync.Mutex
|
||||
out bonfire.Output
|
||||
}
|
||||
|
||||
func (w outputWriter) Write(p []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
w.out.Printf("%s", p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func docsOptions(in bonfire.Input) docsite.Options {
|
||||
opts := docsite.Options{Commands: docsCommands()}
|
||||
opts.Root, _ = in.Flag("root")
|
||||
|
||||
@@ -59,6 +59,12 @@ func TestDocsBuildRealTree(t *testing.T) {
|
||||
"setup/installation.html", "setup/installation.md",
|
||||
"llms.txt", "llms-full.txt", "search-index.json",
|
||||
"assets/site.css", docsite.MarkerFile,
|
||||
"404.html", "assets/site.js", "assets/search.js", "assets/theme-init.js",
|
||||
"assets/LICENSE-lucide.txt", "assets/fonts/LICENSE-dm-sans.txt", "assets/fonts/LICENSE-dm-mono.txt",
|
||||
"assets/fonts/dm-sans-latin-400-normal.woff2", "assets/fonts/dm-sans-latin-600-normal.woff2",
|
||||
"assets/fonts/dm-sans-latin-ext-400-normal.woff2", "assets/fonts/dm-sans-latin-ext-600-normal.woff2",
|
||||
"assets/fonts/dm-sans-latin-400-italic.woff2", "assets/fonts/dm-sans-latin-ext-400-italic.woff2",
|
||||
"assets/fonts/dm-mono-latin-400-normal.woff2", "assets/fonts/dm-mono-latin-ext-400-normal.woff2",
|
||||
} {
|
||||
if _, err := os.Stat(filepath.Join(out, name)); err != nil {
|
||||
t.Errorf("missing %s: %v", name, err)
|
||||
@@ -139,6 +145,9 @@ func TestDocsAIOutputsInSync(t *testing.T) {
|
||||
if strings.HasPrefix(rel, "assets/") {
|
||||
return nil
|
||||
}
|
||||
if rel == "404.html" {
|
||||
return nil
|
||||
}
|
||||
switch filepath.Ext(rel) {
|
||||
case ".html":
|
||||
htmlFiles = append(htmlFiles, strings.TrimSuffix(rel, ".html"))
|
||||
|
||||
@@ -48,6 +48,7 @@ func toolCommands() []bonfire.Command {
|
||||
delegateCommand("queue:clear", "Clear pending queued jobs in the app binary"),
|
||||
docsBuildCommand(),
|
||||
docsSyncCommand(),
|
||||
docsServeCommand(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestToolCommandNames(t *testing.T) {
|
||||
for _, c := range toolCommands() {
|
||||
names = append(names, c.Name)
|
||||
}
|
||||
for _, want := range []string{"build", "make:plugin", "make:model", "make:migration", "make:command", "make:job", "make:admin-controller", "plugin:add", "dev", "migrate", "migrate:rollback", "migrate:status", "serve", "queue:work", "queue:clear", "schedule:run", "parity:broadcasts", "docs:build", "docs:sync"} {
|
||||
for _, want := range []string{"build", "make:plugin", "make:model", "make:migration", "make:command", "make:job", "make:admin-controller", "plugin:add", "dev", "migrate", "migrate:rollback", "migrate:status", "serve", "queue:work", "queue:clear", "schedule:run", "parity:broadcasts", "docs:build", "docs:sync", "docs:serve"} {
|
||||
if !slices.Contains(names, want) {
|
||||
t.Fatalf("missing %s in %v", want, names)
|
||||
}
|
||||
@@ -36,6 +36,7 @@ func TestToolCommandNames(t *testing.T) {
|
||||
"parity:broadcasts": {"--flow", "--step", "--ids", "127.0.0.1:8424"},
|
||||
"docs:build": {"--out", "--src", "--root", "--base-url", "--check"},
|
||||
"docs:sync": {"--src", "--root"},
|
||||
"docs:serve": {"--root", "--src", "--base-url", "--addr", "--allow-remote", "127.0.0.1:8088"},
|
||||
}
|
||||
for cmd, wants := range helpWants {
|
||||
var buf bytes.Buffer
|
||||
|
||||
Reference in New Issue
Block a user