diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-24 11:39:36 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-07-30 15:18:55 -0300 |
| commit | cb05c363045bf0e7af5858f6d7bc26f026fa9d70 (patch) | |
| tree | c3e89426ff044ffe339dd3a77e7f3b7115cba636 /vrsite/static.go | |
| download | notevi-cb05c363045bf0e7af5858f6d7bc26f026fa9d70.tar.gz notevi-cb05c363045bf0e7af5858f6d7bc26f026fa9d70.zip | |
Diffstat (limited to 'vrsite/static.go')
| -rw-r--r-- | vrsite/static.go | 181 |
1 files changed, 181 insertions, 0 deletions
diff --git a/vrsite/static.go b/vrsite/static.go new file mode 100644 index 0000000..e4970c6 --- /dev/null +++ b/vrsite/static.go @@ -0,0 +1,181 @@ +// static.go — the offline export: the same pages written out as files. +// +// Everything that needs a server (comments, live activity, jj queries) is +// omitted; the trace/notes toggle, the file finder and the note filters all +// keep working, since they run in the browser. +package main + +import ( + "bytes" + "fmt" + "html/template" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "sync" + "time" +) + +func run(dir string, name string, args ...string) []byte { + cmd := exec.Command(name, args...) + cmd.Dir = dir + out, err := cmd.Output() + if err != nil { + if ee, ok := err.(*exec.ExitError); ok { + os.Stderr.Write(ee.Stderr) + } + fatal("%s %s: %v", name, strings.Join(args, " "), err) + } + return out +} + +func buildSite(logPath, repoAbs, out, title string, theme, light *Theme) { + start := time.Now() + site := buildModel(title, readLog(logPath)) + site.Theme, site.Light, site.Repo = theme, light, repoAbs + if site.Change == "" { + fatal("log has no jj change ids") + } + + // every file in the repo at the site change; content comes from one + // git archive of the commit instead of a jj spawn per file + list, err := jjRun(repoAbs, "file", "list", "-r", site.Change) + if err != nil { + fmt.Fprintln(os.Stderr, err) + fatal("change %s is not in %s — logs are per repository; point -repo at the one the trace was recorded in", short(site.Change), repoAbs) + } + site.All = site.filesAt(strings.Split(strings.TrimRight(string(list), "\n"), "\n"), site.Change) + + commit := strings.TrimSpace(string(mustJJ(repoAbs, "log", "-r", site.Change, "--no-graph", "-T", "commit_id"))) + tmp, err := os.MkdirTemp("", "vrsite-") + if err != nil { + fatal("%v", err) + } + defer os.RemoveAll(tmp) + archive := exec.Command("git", "-C", repoAbs, "archive", commit) + untar := exec.Command("tar", "-x", "-C", tmp) + untar.Stdin, _ = archive.StdoutPipe() + untar.Stderr = os.Stderr + if err := untar.Start(); err != nil { + fatal("tar: %v", err) + } + if err := archive.Run(); err != nil { + fatal("git archive %s: %v", short(commit), err) + } + if err := untar.Wait(); err != nil { + fatal("tar: %v", err) + } + + writeSite(site, out, tmp) + stubs := 0 + for _, f := range site.All { + if f.Stub != "" { + stubs++ + } + } + fmt.Printf("vrsite: %d files (%d traced, %d not rendered), %d notes, %d agents in %s -> %s\n", + len(site.All), len(site.Order), stubs, len(site.Notes), len(site.Agents), + time.Since(start).Round(time.Millisecond), out) +} + +func writeSite(site *Site, out, srcDir string) { + tpl := newTemplates() + if err := os.MkdirAll(out, 0o755); err != nil { + fatal("%v", err) + } + + each := func(fn func(f *File)) { + sem := make(chan struct{}, runtime.NumCPU()) + var wg sync.WaitGroup + for _, f := range site.All { + wg.Add(1) + sem <- struct{}{} + go func(f *File) { + defer func() { <-sem; wg.Done() }() + fn(f) + }(f) + } + wg.Wait() + } + // phase A: content — coverage percentages must exist before any tree + // or index below is written + hl := newHighlighter(site.Theme, site.Light) + each(func(f *File) { + src, err := os.ReadFile(filepath.Join(srcDir, f.Path)) + switch { + case err != nil: + f.Stub = "not present in the archived commit" + case bytes.IndexByte(src, 0) >= 0: + f.Stub = "binary file — not rendered" + case len(src) > 2<<20: + f.Stub = fmt.Sprintf("file too large to render (%d KB)", len(src)/1024) + default: + f.render(src, hl) + } + }) + + cf := mustCreate(filepath.Join(out, "style.css")) + if err := renderCSS(cf, site); err != nil { + fatal("css: %v", err) + } + cf.Close() + for _, name := range []string{"app.js"} { + b, err := tfs.ReadFile("assets/" + name) + if err != nil { + fatal("%v", err) + } + if err := os.WriteFile(filepath.Join(out, name), b, 0o644); err != nil { + fatal("%v", err) + } + } + + // shared tree fragment: the only nav tree, fetched by every page; "@@" is + // replaced with the page root by app.js, since the fragment is fetched + // from any page depth + treeFrag := renderTree(site.All, func(p string) string { return "@@code/" + p + ".html" }) + if err := os.WriteFile(filepath.Join(out, "tree.html"), []byte(treeFrag), 0o644); err != nil { + fatal("%v", err) + } + + facets := site.Facets() + render := func(path string, p *page) { + p.Site, p.Facets, p.ChangeID = site, facets, site.Change + f := mustCreate(path) + if err := tpl.Execute(f, p); err != nil { + fatal("render %s: %v", path, err) + } + f.Close() + } + + latest := site.FilterNotes(NoteFilter{}) + if len(latest) > 24 { + latest = latest[:24] + } + render(filepath.Join(out, "index.html"), &page{Kind: "index", Title: site.Title, Notes: latest}) + + fullTree := renderTree(site.All, func(p string) string { return "code/" + p + ".html" }) + render(filepath.Join(out, "files.html"), &page{Kind: "files", Title: "files — " + site.Title, + Tree: template.HTML(fullTree)}) + + render(filepath.Join(out, "notes.html"), &page{Kind: "notes", Title: "notes — " + site.Title, + Notes: site.FilterNotes(NoteFilter{})}) + + // phase B: every file page, in parallel + each(func(f *File) { + dst := filepath.Join(out, "code", f.Path+".html") + os.MkdirAll(filepath.Dir(dst), 0o755) + depth := strings.Count(f.Path, "/") + 1 + render(dst, &page{Kind: "file", Title: f.Path + " — " + site.Title, + Root: strings.Repeat("../", depth), Current: f.Path, File: f}) + }) +} + +func mustCreate(path string) *os.File { + f, err := os.Create(path) + if err != nil { + fatal("%v", err) + } + return f +} |
