Skip to content
quarry Artifact [b39f835518]
Login

Artifact [b39f835518]

Artifact b39f835518f8b0a69578692cea55f131210b6c655721ef3785c61665053b938a:


package markup

import (
	"fmt"
	"regexp"
	"strings"
	"testing"
)

// refHookAttrs matches the attributes a reference anchor carries beyond
// a plain link, so a size assertion can weigh one against the other.
var refHookAttrs = regexp.MustCompile(` (?:class="ref"|data-ref="[^"]*"|data-ref-kind="[^"]*")`)

// hostResolver is a resolver that resolves the reference grammar as
// well as the older wiki and hash rules — every surface's shapes in one
// render (RFC 0011 §14).
type hostResolver struct{ fakeResolver }

func (hostResolver) ResolveToken(t RefToken) (string, bool) {
	if t.Kind == RefTicket {
		return "/r/demo/ticket/" + t.ID, true
	}
	return "", false
}

// Every reference on every surface carries the same hook — the
// unqualified [[wiki]] and bare-hash spellings included. A reader
// hovering a hash in a ticket and a repo-qualified token in chat is
// doing the same thing and must get the same card.
func TestEveryReferenceCarriesThePreviewHook(t *testing.T) {
	src := "See [[Design]], abc123def456 and /r/demo/ticket/xy9deadbeef too\n"
	out, err := Render([]byte(src), hostResolver{})
	if err != nil {
		t.Fatal(err)
	}
	s := string(out)
	for _, want := range []string{
		`data-ref="wiki/Design"`,
		`data-ref-kind="wiki"`,
		`data-ref="[abc123def456]"`,
		`data-ref-kind="hash"`,
		`data-ref="demo:ticket/xy9deadbeef"`,
	} {
		if !strings.Contains(s, want) {
			t.Fatalf("missing hook %s in:\n%s", want, s)
		}
	}
	// The token, and nothing else. The behaviour is armed once for the
	// whole page by delegated handlers on the layout's container, so an
	// anchor that carried three copies of a 380-character expression
	// now carries none.
	if strings.Contains(s, "data-on:") {
		t.Fatalf("an anchor carries its own handlers:\n%s", s)
	}
	// The hash link keeps the styling it has had since 0002: the hook
	// is added, the look is not changed.
	if strings.Contains(s, `class="ref"`) && !strings.Contains(s, `class="ref" data-ref="demo:ticket/xy9deadbeef"`) {
		t.Fatalf("hash link restyled:\n%s", s)
	}
}

// Delegation, not repetition: what one reference costs is what two
// hundred cost each. Before this, every anchor carried three copies of
// the same ~380-byte expression and a 200-reference chat page shipped
// roughly 200 KB of duplicated script — Principle 4 read backwards.
func TestReferenceCostDoesNotGrowWithTheExpression(t *testing.T) {
	render := func(n int) string {
		var src strings.Builder
		for i := 1; i <= n; i++ {
			fmt.Fprintf(&src, "line %d sees [[Design]] here\n\n", i)
		}
		out, err := Render([]byte(src.String()), fakeResolver{})
		if err != nil {
			t.Fatal(err)
		}
		return string(out)
	}
	one, many := render(1), render(200)
	if strings.Contains(many, "data-on:") {
		t.Fatalf("references still arm themselves one by one")
	}
	// Each further reference costs its two data hooks and its own
	// prose, and nothing else. A per-anchor expression would put a
	// kilobyte here.
	per := float64(len(many)-len(one)) / 199
	if per > 128 {
		t.Fatalf("each reference costs %.0f bytes; delegation was meant to make it the hooks alone", per)
	}
	// Stripping the hooks leaves the plain links the feature rides on:
	// the anchor is the whole navigation, with or without script.
	plain := refHookAttrs.ReplaceAllString(many, "")
	if n := strings.Count(plain, `<a href="/wiki/Design">Design</a>`); n != 200 {
		t.Fatalf("%d of 200 references are plain navigable links", n)
	}
}

// A resolver that resolves nothing mints no anchors, so there is
// nothing for the page's delegated handlers to reach.
func TestUnresolvedTokensCarryNoHook(t *testing.T) {
	out, err := Render([]byte("See [[Nowhere]].\n"), fakeResolver{})
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(string(out), "data-ref") {
		t.Fatalf("hooks minted for an unresolvable reference:\n%s", out)
	}
}

// dualResolver resolves the same check-in through every seam a body can
// reach it by: the older bare-hash rule, the fossil dialect's own
// [target] links, and the reference grammar.
type dualResolver struct{ fakeResolver }

func (dualResolver) ResolveHash(prefix string) string { return "/r/demo/checkin/" + prefix }

func (dualResolver) ResolveToken(t RefToken) (string, bool) {
	switch t.Kind {
	case RefHash, RefCheckin:
		return "/r/demo/checkin/" + t.ID, true
	}
	return "", false
}

// A bracketed hash long enough for the older rule is a reference like
// any other, in either dialect. The grammar claims the SHORT bracketed
// form only — claiming the long one would restyle every hash in the
// corpus and drop the comment the older rule draws beside it — so the
// consistency that matters is the hook, not the drawing: whichever rule
// mints the anchor, the page's delegated handlers must find a data-ref
// on it.
func TestLongBracketedHashesCarryTheHookInBothDialects(t *testing.T) {
	const src = "see [abc1234567] here"
	md, err := Render([]byte(src), dualResolver{})
	if err != nil {
		t.Fatal(err)
	}
	fossil, err := RenderFossilWiki([]byte(src), dualResolver{})
	if err != nil {
		t.Fatal(err)
	}
	for _, tt := range []struct{ dialect, got string }{
		{"markdown", string(md)},
		{"fossil", string(fossil)},
	} {
		if !strings.Contains(tt.got, `href="/r/demo/checkin/abc1234567"`) {
			t.Fatalf("%s: the hash did not link:\n%s", tt.dialect, tt.got)
		}
		if !refMarked.MatchString(tt.got) {
			t.Fatalf("%s: the anchor carries no data-ref, so nothing arms it:\n%s", tt.dialect, tt.got)
		}
		if strings.Contains(tt.got, "data-on:") {
			t.Fatalf("%s: the anchor arms itself:\n%s", tt.dialect, tt.got)
		}
	}
	// The short bracketed form is the one the grammar claims, and it is
	// the only one that gains class="ref". Long ones keep the look the
	// older rule has given them since 0002.
	if strings.Contains(string(md), `class="ref"`) {
		t.Fatalf("a long bracketed hash was restyled:\n%s", md)
	}
}

// refMarked finds an opening <a> tag carrying a reference hook.
var refMarked = regexp.MustCompile(`<a [^>]*\bdata-ref="[^"]*"`)