M doc/archive.go => doc/archive.go +20 -10
@@ 203,16 203,26 @@ func (a *Archive) Roots() []*Page {
// unpinned site path. A caller rendering for display wraps the resolver to
// carry its own ?rev=; that wrapper must not be used here, or the link graph
// would depend on how the reader arrived.
-// Heading slugs are collected for every document before any document is
-// resolved, in a separate loop. Resolution now answers whether an anchor
-// reference names a heading that exists, and that answer must not depend on
-// where in the archive the two documents sit: ANALYSIS.md sorts before SPEC.md
-// and cites thirty-one of its sections, so a single interleaved pass would
-// resolve every one of them against a target whose headings were still unread.
-// The ordering is removed rather than compensated for — a false "broken anchor"
-// is worse than no check at all, because it teaches the reader to ignore the
-// marks. It costs one parse per document (Renderer.Anchors does not render), not
-// a second render.
+// Heading slugs are collected for every document in a loop of their own, before
+// any document is resolved. Resolution now answers whether an anchor reference
+// names a heading that exists, and that answer must not depend on where in the
+// archive the two documents sit — pages are in path order, so ANALYSIS.md is
+// rendered before the SPEC.md it cites.
+//
+// Two things are needed and they do different jobs. What makes a wrong answer
+// impossible is [Page.missingAnchor] refusing to answer for a document whose
+// anchors are nil: a false "broken anchor" is worse than no check at all,
+// because it teaches the reader to ignore the marks. Measured over the real
+// corpus, reading nil as "this document has no headings" reports 26 correct
+// citations as broken. What this loop adds is coverage: interleaved, 98 of that
+// corpus's 218 anchor references resolve against a target whose headings are
+// still unread, so they are silently not checked at all.
+//
+// It is a parse per document rather than a second render, but a parse is most of
+// what a render costs: measured on the 208-document benchmark corpus,
+// BenchmarkLinkPass goes from ~11ms to ~17ms and from 106k to 146k allocations.
+// At the volume this service holds — tens of documents — that is a fraction of a
+// millisecond per archive build.
func (a *Archive) LinkPass(r *Renderer, bodies map[string][]byte) error {
if r == nil {
return errors.New("doc: link pass needs a renderer")
M web/web_test.go => web/web_test.go +37 -0
@@ 625,6 625,43 @@ func TestPinnedPageKeepsThePinOnItsLinks(t *testing.T) {
}
}
+// A citation naming a section that does not exist has to be as visible as one
+// naming a document that does not exist — and told apart from it, because the
+// repair is different. The page therefore carries its own alert, and the link
+// itself stays a link: the document it points at is the right one.
+func TestDocumentPageReportsABadAnchorSeparately(t *testing.T) {
+ reader := newFakeReader()
+ reader.revs = map[string]map[string]string{headRev: {
+ "specs/0007-storage.md": "---\nid: SPEC-0007\ntitle: Proposal storage model\n---\n\n" +
+ "# Proposal storage model\n\n## Состояние\n\ntext\n",
+ "specs/notes.md": "---\nid: SPEC-0008\ntitle: Notes\n---\n\n" +
+ "# Notes\n\nSee [[SPEC-0007#Состояиие]] and [[SPEC-0007#Состояние]] and [[SPEC-0099]].\n",
+ }}
+ h, _, _ := testServerWith(t, reader)
+
+ body := get(t, h, "/~bigbes/rfcs/specs/notes", "bigbes").Body.String()
+
+ if !strings.Contains(body, "Links to a section that does not exist:") {
+ t.Fatalf("the page does not report the bad anchor:\n%s", body)
+ }
+ if !strings.Contains(body, "<code>SPEC-0007#Состояиие</code>") {
+ t.Fatalf("the bad anchor is not named:\n%s", body)
+ }
+ // The document that genuinely does not resolve keeps its own section, so the
+ // two kinds of miss are not folded into one report.
+ if !strings.Contains(body, "Unresolved wikilinks:") || !strings.Contains(body, "<code>SPEC-0099</code>") {
+ t.Fatalf("the missing document is no longer reported separately:\n%s", body)
+ }
+ // The bad anchor is still a link to the right document, and still pinned.
+ if !strings.Contains(body, `class="wikilink wikilink-anchor-missing" href="/~bigbes/rfcs/specs/0007-storage#состояиие"`) {
+ t.Fatalf("the bad anchor is not a marked, working link:\n%s", body)
+ }
+ // The correct citation is left alone.
+ if !strings.Contains(body, `class="wikilink" href="/~bigbes/rfcs/specs/0007-storage#состояние"`) {
+ t.Fatalf("the correct citation was marked too:\n%s", body)
+ }
+}
+
func TestUnknownRevIs404(t *testing.T) {
h, _ := testServer(t)
rec := get(t, h, "/~bigbes/rfcs/specs/0007-storage?rev=deadbeef", "bigbes")