restore slug.go, slug_test.go, style.css to tracking
commit 1ed81d60bd39f2bdf80daa165c0930406e3b6229 · parent 1a3eccc323
· browse this tree
Ximianth, 2026-07-24 16:05:40 UTC
The 'add moonshot-1-mem' commit accidentally recorded these files as deleted (staging mishap; the working tree copies were never harmed). A fresh clone of the previous commit would not have built. Contents here are identical to what the styling-pass commit introduced, as the working tree carried them forward the whole time.
diff --git a/slug.go b/slug.go new file mode 100644 index 0000000..ac36e22 --- /dev/null +++ b/slug.go @@ -0,0 +1,84 @@ +package main + +import ( + "strconv" + "strings" + + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/text" +) + +// slugify turns heading text into a URL fragment: +// lowercase, alphanumerics kept, every other run of characters becomes one dash. +func slugify(s string) string { + var b strings.Builder + dash := false + for _, r := range strings.ToLower(s) { + switch { + case r >= 'a' && r <= 'z' || r >= '0' && r <= '9': + if dash && b.Len() > 0 { + b.WriteByte('-') + } + dash = false + b.WriteRune(r) + default: + dash = true + } + } + return b.String() +} + +// slugIDs implements parser.IDs so goldmark assigns our slugs as heading ids. +// Duplicate headings get "-2", "-3", ... suffixes. +type slugIDs struct { + used map[string]bool +} + +func newSlugIDs() *slugIDs { + return &slugIDs{used: make(map[string]bool)} +} + +func (s *slugIDs) Generate(value []byte, kind ast.NodeKind) []byte { + slug := slugify(string(value)) + if slug == "" { + slug = "section" + } + candidate := slug + for n := 2; s.used[candidate]; n++ { + candidate = slug + "-" + strconv.Itoa(n) + } + s.used[candidate] = true + return []byte(candidate) +} + +func (s *slugIDs) Put(value []byte) { + s.used[string(value)] = true +} + +// headingAnchors is a goldmark AST transformer that appends a chainlink +// anchor to every heading that received an id, linking to its own fragment. +// Pure HTML output: works without javascript. +type headingAnchors struct{} + +func (headingAnchors) Transform(doc *ast.Document, reader text.Reader, pc parser.Context) { + _ = ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + h, ok := n.(*ast.Heading) + if !ok { + return ast.WalkContinue, nil + } + id, ok := h.AttributeString("id") + if !ok { + return ast.WalkContinue, nil + } + link := ast.NewLink() + link.Destination = append([]byte("#"), id.([]byte)...) + link.SetAttributeString("class", []byte("anchor")) + link.AppendChild(link, ast.NewString([]byte("\U0001F517"))) // chainlink emoji + h.AppendChild(h, link) + return ast.WalkContinue, nil + }) +} diff --git a/slug_test.go b/slug_test.go new file mode 100644 index 0000000..55a0050 --- /dev/null +++ b/slug_test.go @@ -0,0 +1,50 @@ +package main + +import ( + "strings" + "testing" +) + +func TestSlugify(t *testing.T) { + cases := map[string]string{ + "What the verifier catches": "what-the-verifier-catches", + "Use before definition": "use-before-definition", + "C++ / FFI (part 2)": "c-ffi-part-2", + " spaces everywhere ": "spaces-everywhere", + "???": "", + } + for in, want := range cases { + if got := slugify(in); got != want { + t.Errorf("slugify(%q) = %q, want %q", in, got, want) + } + } +} + +func TestHeadingAnchorsInRenderedPage(t *testing.T) { + srv := newTestServer(t) + rec := get(t, srv, "/demo/README.md") + body := rec.Body.String() + if !strings.Contains(body, `<h1 id="hello">`) { + t.Errorf("heading missing slug id; body:\n%s", body) + } + if !strings.Contains(body, `<a href="#hello" class="anchor">`) { + t.Errorf("heading missing chainlink anchor; body:\n%s", body) + } +} + +func TestDuplicateHeadingsGetDistinctIDs(t *testing.T) { + srv := newTestServer(t) + // dup.md is written by newTestServer with two identical headings. + rec := get(t, srv, "/demo/dup.md") + body := rec.Body.String() + if !strings.Contains(body, `id="twice"`) || !strings.Contains(body, `id="twice-2"`) { + t.Errorf("duplicate headings not disambiguated; body:\n%s", body) + } +} + +func TestStylesheetServed(t *testing.T) { + rec := get(t, newTestServer(t), "/-/style.css") + if rec.Code != 200 || !strings.HasPrefix(rec.Header().Get("Content-Type"), "text/css") { + t.Errorf("status = %d, content-type = %q", rec.Code, rec.Header().Get("Content-Type")) + } +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..34676d3 --- /dev/null +++ b/style.css @@ -0,0 +1,102 @@ +/* repoview stylesheet. Embedded in the binary; served at /-/style.css. */ + +:root { + --ink: #24292f; + --ink-soft: #57606a; + --accent: #0a5fa4; + --rule: #d0d7de; + --paper: #fdfdfc; + --well: #f3f5f7; + --well-edge: #c9d1d9; +} + +body { + font-family: sans-serif; + color: var(--ink); + background: var(--paper); + max-width: 56rem; + margin: 1rem auto; + padding: 0 1rem; + line-height: 1.5; +} + +a { color: var(--accent); } + +nav { + border-bottom: 2px solid var(--accent); + padding-bottom: .5rem; + margin-bottom: 1rem; +} +nav .raw { + float: right; + font-size: .85rem; + border: 1px solid var(--rule); + border-radius: .25rem; + padding: .1rem .5rem; + text-decoration: none; +} +nav .raw:hover { background: var(--well); } + +ul.listing { list-style: none; padding-left: 1.5rem; } +ul.listing li { padding: .15rem 0; } + +iframe.passthrough { + width: 100%; + height: 75vh; + border: 1px solid var(--well-edge); + border-radius: .25rem; + background: white; +} + +/* Headings sit flush left; all other article content is inset, + so headings read as structure at a glance. */ +article > * { margin-left: 1.5rem; } +article > h1, article > h2, article > h3, +article > h4, article > h5, article > h6 { margin-left: 0; } + +article h1, article h2 { + border-bottom: 1px solid var(--rule); + padding-bottom: .2rem; +} +article h1 { color: var(--accent); } + +/* Chainlink anchors: subtle until the heading is hovered. */ +a.anchor { + font-size: .7em; + text-decoration: none; + margin-left: .4rem; + opacity: 0; +} +h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, +h4:hover a.anchor, h5:hover a.anchor, h6:hover a.anchor, +a.anchor:focus { opacity: .6; } + +/* Code blocks: inset further than prose, bordered, in a well. */ +article pre { + margin-left: 3rem; + background: var(--well); + border: 1px solid var(--well-edge); + border-left: 3px solid var(--accent); + border-radius: .25rem; + padding: .6rem .8rem; + overflow-x: auto; +} +article code { + background: var(--well); + border-radius: .2rem; + padding: .05rem .25rem; +} +article pre code { background: none; padding: 0; } + +article blockquote { + border-left: 3px solid var(--rule); + color: var(--ink-soft); + padding-left: .8rem; +} + +article table { border-collapse: collapse; } +article th, article td { + border: 1px solid var(--rule); + padding: .2rem .5rem; +} +article th { background: var(--well); }