blob: aa7e6ad3e572419fd239708c209e7fb6f17d48dd (plain)
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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
-- Unit checks for custom/extensions/about-render.lua, run under a standalone
-- Lua by t0501-about-render.sh with the script path as the first argument.
-- The second argument may be nolpeg, which makes the lpeg module fail to
-- load so the escaped plain text fallback is proven even on a machine where
-- lpeg is installed. Without it the markdown and man checks run when lpeg is
-- present and are skipped with a note when it is not, the same way the
-- filter itself degrades.

local test_directory = arg[0]:match("^(.*)/") or "."
local h = dofile(test_directory .. "/harness.lua")

local script = arg[1]
local mode = arg[2] or ""

local has_lpeg = false
if mode == "nolpeg" then
	h.preload_failure("lpeg")
else
	has_lpeg = pcall(require, "lpeg")
	if not has_lpeg then
		io.write("lpeg not installed, markdown and man checks skipped\n")
	end
end

h.load(script)

local function render(filename, text)
	return h.run({ filename }, { text })
end

-- Dispatch and the plain text rendering need nothing but the script itself,
-- so they run whatever became of lpeg.

local out, ret = render("README", "plain <text> & more")
h.contains("no extension renders as plain text", out,
	"<pre class='plaintext'>plain &lt;text&gt; &amp; more</pre>")
h.equals("close answers zero", ret, 0)

out = render("notes.txt", "just text")
h.contains("an unknown extension renders as plain text", out,
	"<pre class='plaintext'>just text</pre>")

out = render(nil, "no name at all")
h.contains("a missing filename renders as plain text", out,
	"<pre class='plaintext'>no name at all</pre>")

-- cgit's C sink stops at the first NUL byte, which the script documents, so
-- the text past one is lost while the wrapper written separately survives.
out = render("README", "before\0after")
h.contains("text before a nul byte survives", out, "before")
h.excludes("text after a nul byte is dropped", out, "after")
h.contains("the wrapper written after the text survives", out, "</pre>")

out = h.run({ "README" }, { "two ", "writes" })
h.contains("writes are joined before rendering", out, "two writes")

if mode == "nolpeg" then
	out = render("README.md", "# Title")
	h.contains("markdown without lpeg falls back to plain text", out,
		"<pre class='plaintext'># Title</pre>")
	out = render("page.1", ".SH NAME")
	h.contains("man without lpeg falls back to plain text", out,
		"<pre class='plaintext'>.SH NAME</pre>")
	h.finish()
end

if not has_lpeg then
	h.finish()
end

-- Markdown.

out = render("README.md", "# Title")
h.contains("a heading renders inside the wrapper",
	out, "<div class='markdown'><h1>Title</h1></div>")

out = render("README.md", "## Sub ##")
h.contains("trailing hashes are stripped from a heading", out,
	"<h2>Sub</h2>")

out = render("README.MD", "# Upper")
h.contains("the extension matches whatever its case", out, "<h1>Upper</h1>")

out = render("readme.markdown", "# Long")
h.contains("the long markdown extension dispatches too", out,
	"<h1>Long</h1>")

out = render("README.md", "line one\nline two")
h.contains("a paragraph keeps its line break", out,
	"<p>line one<br>line two</p>")

out = render("README.md", "first para\r\nsecond line")
h.contains("crlf line endings are normalised", out,
	"<p>first para<br>second line</p>")

out = render("README.md", "a **bold** and *leaning* word")
h.contains("double stars embolden", out, "<strong>bold</strong>")
h.contains("single stars lean", out, "<em>leaning</em>")

out = render("README.md", "an __up__ and _down_ word")
h.contains("double underscores embolden", out, "<strong>up</strong>")
h.contains("single underscores lean", out, "<em>down</em>")

out = render("README.md", "run `x < y` here")
h.contains("inline code is escaped", out, "<code>x &lt; y</code>")

out = render("README.md", "raw <script>alert(1)</script> here")
h.contains("markup in text reaches the page escaped", out,
	"&lt;script&gt;alert(1)&lt;/script&gt;")
h.excludes("no live tag slips through", out, "<script>")

out = render("README.md", "[docs](https://example.com/docs)")
h.contains("an https link renders as an anchor", out,
	"<a href='https://example.com/docs'>docs</a>")

out = render("README.md", "[docs](docs/page.html)")
h.contains("a relative link renders as an anchor", out,
	"<a href='docs/page.html'>docs</a>")

out = render("README.md", "[mail](mailto:a@example.com)")
h.contains("a mailto link renders as an anchor", out,
	"<a href='mailto:a@example.com'>mail</a>")

out = render("README.md", "[bad](javascript:alert(1))")
h.excludes("a javascript link renders no anchor", out, "<a ")
h.contains("its text still reaches the page", out, "bad")

out = render("README.md", "[bad](java\nscript:alert(1))")
h.excludes("a split scheme is still caught", out, "<a ")

out = render("README.md", "[bad](//evil.example/x)")
h.excludes("a scheme relative link renders no anchor", out, "<a ")

out = render("README.md", "[q](https://example.com/a'b)")
h.contains("a quote in a link target is escaped", out,
	"href='https://example.com/a&#x27;b'")

out = render("README.md", "![a & b](https://example.com/i.png)")
h.contains("an image renders with its alt text escaped", out,
	"<img src='https://example.com/i.png' alt='a &amp; b'>")

out = render("README.md", "![alt](data:image/png;base64,x)")
h.excludes("an unsafe image renders no tag", out, "<img")
h.contains("its alt text is kept as text", out, "![alt]")

out = render("README.md", "```lua\nlocal x = 1 < 2\n```\nafter")
h.contains("a fenced block carries its language", out,
	"<pre><code data-lang='lua'>local x = 1 &lt; 2\n</code></pre>")
h.contains("text after the fence renders on", out, "<p>after</p>")

out = render("README.md", "~~~\n**not bold**\n~~~")
h.contains("no inline parsing happens inside a fence", out,
	"<code>**not bold**\n</code>")

out = render("README.md", "---")
h.contains("a rule of dashes renders a break", out, "<hr>")

out = render("README.md", "- - -")
h.contains("a spaced rule renders a break too", out, "<hr>")

out = render("README.md", "> quoted words")
h.contains("a quote renders as a blockquote", out,
	"<blockquote><p>quoted words</p></blockquote>")

out = render("README.md", "> > deep")
h.contains("quotes nest", out,
	"<blockquote><blockquote><p>deep</p></blockquote></blockquote>")

-- A hostile readme of stacked markers has to hit the depth ceiling instead
-- of the interpreter's stack.
out = render("README.md", string.rep("> ", 30) .. "x")
local _, quote_count = out:gsub("<blockquote>", "")
h.equals("quote nesting stops at the ceiling", quote_count, 24)
h.contains("the innermost text still renders", out, "x")

out = render("README.md", string.rep("*a", 200))
h.check("an emphasis bomb still renders something", #out > 0)

out = render("README.md", "| a | b |\n| --- | --- |\n| 1 | 2 |")
h.contains("a pipe table renders its head",
	out, "<thead><tr><th>a</th><th>b</th></tr></thead>")
h.contains("a pipe table renders its body",
	out, "<tbody><tr><td>1</td><td>2</td></tr></tbody>")

out = render("README.md", "a | b in prose")
h.excludes("a pipe without a delimiter row stays prose", out, "<table")

out = render("README.md", "- first\n- second")
h.contains("dashes render an unordered list", out,
	"<ul><li>first</li><li>second</li></ul>")

out = render("README.md", "1. first\n2. second")
h.contains("numbers render an ordered list", out,
	"<ol><li>first</li><li>second</li></ol>")

out = render("README.md", "- plain\n1. numbered")
h.contains("a change of marker splits the list", out, "</ul><ol>")

out = render("README.md", "- a **bold** item")
h.contains("list items parse their text", out,
	"<li>a <strong>bold</strong> item</li>")

out = render("README.md", string.rep("a", 512 * 1024 + 1))
h.contains("an oversized readme renders as plain text", out,
	"<pre class='plaintext'>")

-- Man pages.

out = render("page.1", ".SH NAME")
h.contains("a section heading renders", out,
	"<div class='markdown manpage'><h2>NAME</h2></div>")

out = render("page.man", '.SH "TWO WORDS"')
h.contains("a quoted heading argument is unquoted", out,
	"<h2>TWO WORDS</h2>")

out = render("page.3", ".SS Details")
h.contains("a subsection renders one level down", out, "<h3>Details</h3>")

out = render("page.1", ".B bold words\nplain after")
h.contains("the bold macro renders strong", out,
	"<p><strong>bold words</strong><br>plain after</p>")

out = render("page.1", "a \\fBbold\\fR c")
h.contains("font escapes carry across a run", out,
	"<p>a <strong>bold</strong> c</p>")

out = render("page.1", "x \\(em y \\- z")
h.contains("character escapes are translated", out, "<p>x - y - z</p>")

out = render("page.1", ".nf\ncode <x>\n.fi\nafter")
h.contains("a no fill block renders as code", out,
	"<pre><code>code &lt;x&gt;\n</code></pre>")
h.contains("filling resumes after it", out, "<p>after</p>")

out = render("page.1", '.\\" a comment\nvisible')
h.excludes("a roff comment is dropped", out, "comment")
h.contains("the line after it renders", out, "visible")

out = render("page.1", "first\n\nsecond")
h.contains("a blank line splits paragraphs", out,
	"<p>first</p><p>second</p>")

out = render("page.1", ".TH title 1\nbody")
h.excludes("the title macro renders nothing itself", out, "title 1")
h.contains("the body after it renders", out, "<p>body</p>")

h.finish()