~bigbes/sr-ht-spec

a8bf0009098919be1f3e54163d11eb3960dc1eab — Eugene Blikh a day ago 5a91ab2
doc: say what the anchors pass costs and what it buys, and pin the alert
2 files changed, 57 insertions(+), 10 deletions(-)

M doc/archive.go
M web/web_test.go
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")