~bigbes/sr-ht-spec

ref: 01999c70f928a0963f60dc1670f68e7201576b1b sr-ht-spec/doc/render_test.go -rw-r--r-- 5.7 KiB
01999c70 — Eugene Blikh graph: keep webhook management with the owner, not with any agent 2 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
package doc

import (
	"strings"
	"testing"
)

// fakeResolver resolves every target except those starting with "missing" and
// those ending in an attachment extension, so the tests can exercise all four
// render paths without a real archive. The hrefs it hands back are deliberately
// arbitrary: what is under test is the renderer, not the resolution rules.
type fakeResolver struct{}

func (fakeResolver) Resolve(fromDir, dest string) Target {
	switch {
	case strings.HasPrefix(dest, "http"):
		return Target{Href: dest, IsExternal: true}
	case strings.HasPrefix(dest, "missing"):
		return Target{Href: "/p/" + dest, Missing: true}
	case strings.HasSuffix(dest, ".png"), strings.HasSuffix(dest, ".pdf"), strings.HasSuffix(dest, ".base"):
		return Target{Href: "/assets/" + dest}
	}
	id, frag, _ := strings.Cut(dest, "#")
	href := "/p/" + id
	if frag != "" {
		href += "#" + strings.ToLower(frag)
	}
	return Target{Href: href, PageID: id, Kind: "markdown"}
}

func renderMD(t *testing.T, src string) Result {
	t.Helper()
	return NewRenderer().Render([]byte(src), "specs", fakeResolver{})
}

func TestWikilink(t *testing.T) {
	cases := []struct {
		name       string
		src        string
		wantHTML   []string
		wantAbsent []string
		wantLinked []string
	}{
		{
			name:       "plain",
			src:        "see [[lsm-tree]] for details",
			wantHTML:   []string{`<a class="wikilink" href="/p/lsm-tree">lsm-tree</a>`},
			wantLinked: []string{"lsm-tree"},
		},
		{
			name:       "aliased",
			src:        "see [[lsm-tree|LSM trees]]",
			wantHTML:   []string{`href="/p/lsm-tree">LSM trees</a>`},
			wantLinked: []string{"lsm-tree"},
		},
		{
			name:       "path qualified shows the last segment",
			src:        "[[reports/watchlist]]",
			wantHTML:   []string{`href="/p/reports/watchlist">watchlist</a>`},
			wantLinked: []string{"reports/watchlist"},
		},
		{
			name:       "heading reference",
			src:        "[[ai-too-expensive#Zillow|AI Chernobyl]]",
			wantHTML:   []string{`href="/p/ai-too-expensive#zillow">AI Chernobyl</a>`},
			wantLinked: []string{"ai-too-expensive"},
		},
		{
			name:     "image embed renders inline",
			src:      "![[diagram.png]]",
			wantHTML: []string{`<img class="wikilink-embed" src="/assets/diagram.png"`},
		},
		{
			name:       "non-image embed degrades to a labelled link",
			src:        "![[wiki.base]]",
			wantHTML:   []string{`class="wikilink wikilink-file" href="/assets/wiki.base">wiki.base</a>`},
			wantAbsent: []string{"<img"},
		},
		{
			name:       "page embed links rather than transcluding",
			src:        "![[lsm-tree]]",
			wantHTML:   []string{`wikilink-embed-ref" href="/p/lsm-tree">lsm-tree</a>`},
			wantLinked: []string{"lsm-tree"},
		},
		{
			name:       "unresolved link is visibly broken, never dropped",
			src:        "[[missing-page]]",
			wantHTML:   []string{`<span class="wikilink-missing"`, `>missing-page</span>`},
			wantAbsent: []string{"<a "},
		},
		{
			name: "inside a code span it is literal text",
			src:  "write `[[page]]` to link",
			wantHTML: []string{
				"<code>[[page]]</code>",
			},
			wantAbsent: []string{`class="wikilink"`},
		},
		{
			name: "inside a fenced block it is literal text",
			src:  "```\n[[page]] and ![[transclusions]]\n```\n",
			wantHTML: []string{
				"[[page]] and ![[transclusions]]",
			},
			wantAbsent: []string{`class="wikilink"`, "<img"},
		},
		{
			name:       "empty target is not a link",
			src:        "[[]] and [[|x]]",
			wantAbsent: []string{`class="wikilink"`},
		},
		{
			name:       "unclosed brackets are left alone",
			src:        "an [[unclosed link",
			wantAbsent: []string{`class="wikilink"`},
		},
		{
			// Obsidian escapes the alias pipe inside table cells.
			name:       "escaped alias pipe in a table cell",
			src:        "| a | b |\n| --- | --- |\n| [[lsm-tree\\|LSM]] | x |\n",
			wantHTML:   []string{`href="/p/lsm-tree">LSM</a>`},
			wantLinked: []string{"lsm-tree"},
		},
		{
			name:       "label is HTML-escaped",
			src:        "[[lsm-tree|<script>alert(1)</script>]]",
			wantAbsent: []string{"<script>"},
			wantHTML:   []string{"&lt;script&gt;"},
			wantLinked: []string{"lsm-tree"},
		},
		{
			name:       "an ordinary markdown link still works",
			src:        "[label](lsm-tree)",
			wantHTML:   []string{`href="/p/lsm-tree"`},
			wantLinked: []string{"lsm-tree"},
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			res := renderMD(t, c.src)
			for _, want := range c.wantHTML {
				if !strings.Contains(res.HTML, want) {
					t.Errorf("HTML missing %q:\n%s", want, res.HTML)
				}
			}
			for _, absent := range c.wantAbsent {
				if strings.Contains(res.HTML, absent) {
					t.Errorf("HTML must not contain %q:\n%s", absent, res.HTML)
				}
			}
			if len(res.LinkedIDs) != len(c.wantLinked) {
				t.Fatalf("LinkedIDs = %v, want %v", res.LinkedIDs, c.wantLinked)
			}
			for i, want := range c.wantLinked {
				if res.LinkedIDs[i] != want {
					t.Errorf("LinkedIDs[%d] = %q, want %q", i, res.LinkedIDs[i], want)
				}
			}
		})
	}
}

func TestWikilinkDedupesAndKeepsFirstAppearanceOrder(t *testing.T) {
	res := renderMD(t, "[[b]] then [[a]] then [[b]] again")
	if got := strings.Join(res.LinkedIDs, ","); got != "b,a" {
		t.Fatalf("LinkedIDs = %q, want %q", got, "b,a")
	}
}

func TestWikilinkInsideEmphasisAndLists(t *testing.T) {
	res := renderMD(t, "- **[[lsm-tree]]** — a note\n- see *[[b-tree|B-trees]]*\n")
	for _, want := range []string{`<strong><a class="wikilink" href="/p/lsm-tree"`, `href="/p/b-tree">B-trees</a>`} {
		if !strings.Contains(res.HTML, want) {
			t.Errorf("HTML missing %q:\n%s", want, res.HTML)
		}
	}
}

func TestPlainTextExcludesWikilinkMarkup(t *testing.T) {
	res := renderMD(t, "see [[lsm-tree|LSM trees]] now")
	if strings.Contains(res.PlainText, "[[") {
		t.Errorf("plain text leaked wikilink syntax: %q", res.PlainText)
	}
}