Skip to content
quarry Artifact [467a7cfd7f]
Login

Artifact [467a7cfd7f]

Artifact 467a7cfd7fcb8887b406ac12daf97ffd6ec5d745e83b0b3f339515e9726eb8a7:


package markup

import (
	"strings"
	"testing"
)

// The token grammar is the single place chat, forum, ticket and landing
// text agree on what a reference looks like (RFC 0011 §14). Assert the
// parse, not the rendering: the rendering has its own test.
func TestParseRefGrammar(t *testing.T) {
	for _, tt := range []struct {
		in                    string
		repo, kind, id, canon string
		ok                    bool
	}{
		{in: "#412", kind: "chat", id: "412", canon: "#412", ok: true},
		{in: "sqlite:#412", repo: "sqlite", kind: "chat", id: "412", canon: "sqlite:#412", ok: true},
		{in: "[a91f3c2e91]", kind: "hash", id: "a91f3c2e91", canon: "[a91f3c2e91]", ok: true},
		{in: "sqlite:[a91f3c2e91]", repo: "sqlite", kind: "hash", id: "a91f3c2e91", canon: "sqlite:[a91f3c2e91]", ok: true},
		{in: "a91f3c2e91", kind: "hash", id: "a91f3c2e91", canon: "[a91f3c2e91]", ok: true},
		{in: "sqlite:a91f3c2e91", repo: "sqlite", kind: "hash", id: "a91f3c2e91", canon: "sqlite:[a91f3c2e91]", ok: true},
		{in: "@alice", kind: "person", id: "alice", canon: "@alice", ok: true},
		{in: "/r/sqlite/chat/412", repo: "sqlite", kind: "chat", id: "412", canon: "sqlite:#412", ok: true},
		{in: "https://q.example/r/sqlite/ticket/abc123", repo: "sqlite", kind: "ticket", id: "abc123", canon: "sqlite:ticket/abc123", ok: true},
		{in: "/r/demo/wiki/Home", repo: "demo", kind: "wiki", id: "Home", canon: "demo:wiki/Home", ok: true},
		{in: "/r/demo/checkin/deadbeef", repo: "demo", kind: "checkin", id: "deadbeef", canon: "demo:checkin/deadbeef", ok: true},
		{in: "/r/demo/landing/abc", repo: "demo", kind: "landing", id: "abc", canon: "demo:landing/abc", ok: true},
		{in: "/r/demo/forum/abc", repo: "demo", kind: "forum", id: "abc", canon: "demo:forum/abc", ok: true},
		// Not references.
		{in: "hello"},
		{in: "#"},
		{in: "#0"},
		{in: "/r/demo/timeline"},
		{in: "0xdead"},
	} {
		got, ok := ParseRef(tt.in)
		if ok != tt.ok {
			t.Fatalf("ParseRef(%q) ok = %v, want %v", tt.in, ok, tt.ok)
		}
		if !ok {
			continue
		}
		if got.Repo != tt.repo || got.Kind != tt.kind || got.ID != tt.id {
			t.Fatalf("ParseRef(%q) = %+v, want repo %q kind %q id %q", tt.in, got, tt.repo, tt.kind, tt.id)
		}
		if got.Canonical() != tt.canon {
			t.Fatalf("ParseRef(%q).Canonical() = %q, want %q", tt.in, got.Canonical(), tt.canon)
		}
	}
}

// A reference found in prose has to be found at its exact span so the
// splice can leave the surrounding sentence alone.
func TestScanRefsSpans(t *testing.T) {
	s := "see #412 and sqlite:[a91f3c2e91] plus /r/demo/ticket/xy9 done"
	got := scanRefs(s)
	if len(got) != 3 {
		t.Fatalf("scanRefs found %d refs in %q: %+v", len(got), s, got)
	}
	for i, want := range []string{"#412", "sqlite:[a91f3c2e91]", "/r/demo/ticket/xy9"} {
		if s[got[i].start:got[i].end] != want {
			t.Fatalf("ref %d spans %q, want %q", i, s[got[i].start:got[i].end], want)
		}
	}
}

// tokenRes is a Resolver that also speaks the reference grammar. It
// resolves "demo" and the current repo, and refuses "secret" — the
// cross-repo token a reader may not see.
type tokenRes struct{ fakeResolver }

func (tokenRes) ResolveToken(t RefToken) (string, bool) {
	if t.Repo == "secret" {
		return "", false
	}
	repo := t.Repo
	if repo == "" {
		repo = "demo"
	}
	switch t.Kind {
	case RefChat:
		return "/r/" + repo + "/chat/" + t.ID, true
	case RefHash:
		return "/r/" + repo + "/checkin/" + t.ID, true
	case RefTicket:
		return "/r/" + repo + "/ticket/" + t.ID, true
	}
	return "", false
}

func TestRenderMintsReferenceLinks(t *testing.T) {
	out, err := Render([]byte("see #412 and sqlite:[a91f3c2e91] here"), tokenRes{})
	if err != nil {
		t.Fatal(err)
	}
	got := string(out)
	for _, want := range []string{
		`class="ref"`,
		`data-ref="#412"`,
		`data-ref-kind="chat"`,
		`href="/r/demo/chat/412"`,
		`data-ref="sqlite:[a91f3c2e91]"`,
		`href="/r/sqlite/checkin/a91f3c2e91"`,
	} {
		if !strings.Contains(got, want) {
			t.Fatalf("rendered body missing %q:\n%s", want, got)
		}
	}
}

// A token the reader may not resolve stays a sentence, never a dead
// link (RFC 0011 §14: "resolving only when the reader can see it").
func TestRenderLeavesUnresolvableTokensAsText(t *testing.T) {
	out, err := Render([]byte("see secret:#9 and nothingatall"), tokenRes{})
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(string(out), "data-ref") {
		t.Fatalf("an unresolvable token became a link:\n%s", out)
	}
}

// A pasted quarry URL is autolinked by goldmark before the resolver
// sees it, so the resolver has to recognise the anchor it already made
// rather than only the text it did not.
func TestRenderAnnotatesPastedQuarryURLs(t *testing.T) {
	out, err := Render([]byte("look at https://q.example/r/demo/ticket/xy9 now"), tokenRes{})
	if err != nil {
		t.Fatal(err)
	}
	got := string(out)
	if !strings.Contains(got, `data-ref="demo:ticket/xy9"`) {
		t.Fatalf("pasted URL not annotated:\n%s", got)
	}
}