Diff options
Diffstat (limited to '')
-rw-r--r--custom/extensions/about-render.lua259
1 file changed, 83 insertions, 176 deletions
diff --git a/custom/extensions/about-render.lua b/custom/extensions/about-render.lua
index 808dd08..5738b00 100644
--- a/custom/extensions/about-render.lua
+++ b/custom/extensions/about-render.lua
@@ -3,8 +3,9 @@
-- interpreter so a readme costs no extra process per request. Markdown, man
-- pages and plain text are the three formats, chosen from the readme's file
-- extension, and anything that fails to parse falls back to escaped plain
--- text. The wrappers it emits carry the classes that assets/cgit.css styles.
--- It runs on Lua 5.1 through 5.5 and LuaJIT.
+-- text. Markdown and man pages come out in div.markdown and plain text in
+-- pre.plaintext, the two wrappers assets/cgit.css styles. It runs on Lua 5.1
+-- through 5.5 and LuaJIT.
--
-- about-filter=lua:/usr/local/lib/cgit/filters/about-render.lua
@@ -190,9 +191,7 @@ end
-- first. html() only ever gets literal tag scaffolding and the character
-- references whose shape the markdown grammar checked.
-local emit_inline
-
-emit_inline = function(list)
+local function emit_inline(list)
for _, node in ipairs(list) do
local kind = node.kind
if kind == "text" then
@@ -271,9 +270,7 @@ local function emit_lines(lines)
end
end
-local emit_blocks
-
-emit_blocks = function(blocks)
+local function emit_blocks(blocks)
for _, block in ipairs(blocks) do
local kind = block.kind
if kind == "heading" then
@@ -285,13 +282,7 @@ emit_blocks = function(blocks)
elseif kind == "hr" then
html("<hr>")
elseif kind == "code" then
- html("<pre><code")
- if block.lang and block.lang ~= "" then
- html(" data-lang='")
- html_attr(block.lang)
- html("'")
- end
- html(">")
+ html("<pre><code>")
html_txt(block.text)
html("</code></pre>")
elseif kind == "blockquote" then
@@ -348,10 +339,23 @@ end
-- Both parse into a node tree before anything is emitted, so a failed parse
-- falls back to plain text without leaving half a page behind.
+local function render_blocks(text, parse)
+ local ok, blocks = pcall(parse, text)
+ if not ok then
+ return render_plaintext(text)
+ end
+ html("<div class='markdown'>")
+ -- Emitting is wrapped so a bug there can at worst truncate the page,
+ -- never end the request in an error.
+ pcall(emit_blocks, blocks)
+ html("</div>")
+end
+
local render_markdown, render_man
if has_lpeg then
- local P, R, S, B, C, Ct = lpeg.P, lpeg.R, lpeg.S, lpeg.B, lpeg.C, lpeg.Ct
+ local P, R, S, B = lpeg.P, lpeg.R, lpeg.S, lpeg.B
+ local C, Cc, Ct = lpeg.C, lpeg.Cc, lpeg.Ct
local space = S(" \t")
local whitespace = S(" \t\r\n\f\v")
@@ -447,28 +451,27 @@ if has_lpeg then
return { node_text(s) }
end
inline_depth = inline_depth + 1
- local nodes = inline_grammar:match(s) or { node_text(s) }
+ local nodes = inline_grammar:match(s)
inline_depth = inline_depth - 1
return nodes
end
- local lang_char = R("az", "AZ", "09") + S("_.+#-")
local heading_line = C(P("#") * P("#") ^ -5) * space ^ 1 * C(P(1) ^ 0)
-- A run of equals signs or dashes under a paragraph turns it into a
-- heading, as on GitHub, rather than reading as more of its text.
- local setext_line = space ^ 0 * (C(P("=") ^ 1) + C(P("-") ^ 1))
+ local setext_line = space ^ 0 * (P("=") ^ 1 * Cc(1) + P("-") ^ 1 * Cc(2))
* space ^ 0 * eol
local code_indent = P(" ") + P("\t")
local function thematic(mark)
return space ^ 0 * P(mark) * (space ^ 0 * P(mark)) ^ 2 * space ^ 0 * eol
end
local break_line = thematic("-") + thematic("*") + thematic("_")
- local fence_line = space ^ 0 * (C(P("`") ^ 3) + C(P("~") ^ 3)) * space ^ 0 * C(lang_char ^ 0)
+ local fence_line = space ^ 0 * (C(P("`") ^ 3) + C(P("~") ^ 3))
local close_backtick = space ^ 0 * P("`") ^ 3 * space ^ 0 * eol
local close_tilde = space ^ 0 * P("~") ^ 3 * space ^ 0 * eol
local quote_line = space ^ 0 * P(">")
local bullet = S("-*+")
- local number = R("09") ^ 1 * S(".)")
+ local number = digit ^ 1 * S(".)")
local item_line = space ^ 0 * C(bullet + number) * space ^ 1 * C(P(1) ^ 0)
local dash_cell = space ^ 0 * P(":") ^ -1 * P("-") ^ 1 * P(":") ^ -1 * space ^ 0
-- A delimiter row is dash-cells joined by pipes. Require at least one
@@ -480,16 +483,10 @@ if has_lpeg then
pipe * dash_cell * (pipe * dash_cell) ^ 0 * pipe ^ -1
+ dash_cell * (pipe * dash_cell) ^ 1 * pipe ^ -1
) * space ^ 0 * eol
- local block_start = space ^ 0 * ((P("#") * P("#") ^ -5 * space) + P(">")
- + P("`") ^ 3 + P("~") ^ 3 + ((bullet + number) * space))
+ local block_start = space ^ 0 * heading_line + quote_line + fence_line + item_line
local function ordered(mark) return mark:match("%d") ~= nil end
- local function follows_list(blocks)
- local last = blocks[#blocks]
- return last ~= nil and last.kind == "list"
- end
-
-- An indented block runs on through blank lines, and up to four spaces
-- or one tab come off each of its lines.
local function continues_code(line)
@@ -500,13 +497,6 @@ if has_lpeg then
return line:match("^\t(.*)$") or line:match("^ ? ? ? ?(.*)$")
end
- local function setext_level(line)
- local run = setext_line:match(line)
- if run then
- return run:sub(1, 1) == "=" and 1 or 2
- end
- end
-
local function table_starts(lines, i)
return i < #lines and lines[i]:find("|", 1, true) ~= nil
and table_delimiter:match(lines[i + 1]) ~= nil
@@ -517,7 +507,15 @@ if has_lpeg then
-- paragraph short.
local function ends_paragraph(lines, i)
return lines[i]:match("^%s*$") or block_start:match(lines[i])
- or table_starts(lines, i)
+ or break_line:match(lines[i]) or table_starts(lines, i)
+ end
+
+ local function code_block(lines)
+ local text = table.concat(lines, "\n")
+ if #lines > 0 then
+ text = text .. "\n"
+ end
+ return { kind = "code", text = text }
end
-- A paragraph's lines run together the way GitHub shows them, unless one
@@ -542,13 +540,11 @@ if has_lpeg then
return lines
end
- local parse_blocks
-
- parse_blocks = function(lines, depth)
+ local function parse_blocks(lines, depth)
local blocks, i, n = {}, 1, #lines
while i <= n do
local line = lines[i]
- local fence, lang = fence_line:match(line)
+ local fence = fence_line:match(line)
local hashes, title = heading_line:match(line)
local mark = item_line:match(line)
if line:match("^%s*$") then
@@ -562,15 +558,7 @@ if has_lpeg then
i = i + 1
end
i = i + 1
- local body = table.concat(code, "\n")
- if #code > 0 then
- body = body .. "\n"
- end
- blocks[#blocks + 1] = {
- kind = "code",
- lang = lang,
- text = body,
- }
+ blocks[#blocks + 1] = code_block(code)
elseif hashes then
blocks[#blocks + 1] = {
kind = "heading",
@@ -628,10 +616,8 @@ if has_lpeg then
end
local item = { content }
i = i + 1
- -- A wrapped line carries the item on, whatever its
- -- indent, while a rule still ends the list.
- while i <= n and not ends_paragraph(lines, i)
- and not break_line:match(lines[i]) do
+ -- A wrapped line carries the item on, whatever its indent.
+ while i <= n and not ends_paragraph(lines, i) do
item[#item + 1] = lines[i]
i = i + 1
end
@@ -645,7 +631,8 @@ if has_lpeg then
-- Four spaces or a tab start a code block, except straight
-- after a list, where an indented paragraph more likely
-- carries an item on.
- elseif code_indent:match(line) and not follows_list(blocks) then
+ elseif code_indent:match(line)
+ and (blocks[#blocks] or {}).kind ~= "list" then
local code = {}
while i <= n and continues_code(lines[i]) do
code[#code + 1] = unindent(lines[i])
@@ -654,17 +641,13 @@ if has_lpeg then
while code[#code]:match("^%s*$") do
code[#code] = nil
end
- blocks[#blocks + 1] = {
- kind = "code",
- text = table.concat(code, "\n") .. "\n",
- }
+ blocks[#blocks + 1] = code_block(code)
else
local paragraph, level = { line }, nil
i = i + 1
while i <= n do
- level = setext_level(lines[i])
- if level or ends_paragraph(lines, i)
- or break_line:match(lines[i]) then
+ level = setext_line:match(lines[i])
+ if level or ends_paragraph(lines, i) then
break
end
paragraph[#paragraph + 1] = lines[i]
@@ -693,19 +676,9 @@ if has_lpeg then
end
render_markdown = function(text)
- if #text > max_bytes then
- return render_plaintext(text)
- end
- local ok, blocks = pcall(parse_blocks, split_lines(text), 0)
- if not ok then
- return render_plaintext(text)
- end
- html("<div class='markdown'>")
- -- Emitting is wrapped so a bug there can at worst
- -- truncate the page, never raise and let the outer
- -- fallback duplicate what has already been sent.
- pcall(emit_blocks, blocks)
- html("</div>")
+ render_blocks(text, function(t)
+ return parse_blocks(split_lines(t), 0)
+ end)
end
-- Roff fonts are a current state carried across a run rather than
@@ -733,15 +706,8 @@ if has_lpeg then
}
local backslash = P("\\")
- local function text_token(text) return { kind = "text", text = text } end
- local skip_token = { kind = "skip" }
- local function skip() return skip_token end
local function font_token(name)
- return { kind = "font", font = fonts[name] or "R" }
- end
- local function char_token(name) return text_token(man_chars[name] or "") end
- local function string_token(name)
- return text_token(man_strings[name] or "")
+ return { font = fonts[name] or "R" }
end
-- \( takes a two character name and \[ a longer one, bounded so that a
@@ -754,50 +720,43 @@ if has_lpeg then
-- the escapes that only steer hyphenation or breaks.
local man_inline_grammar = Ct((
(backslash * P("f") * (named + C(P(1)))) / font_token
- + (backslash * S('"#') * P(1) ^ 0) / skip
- + (backslash * P("-")) / function() return text_token("-") end
- + (backslash * P("e")) / function() return text_token("\\") end
- + (backslash * S(" 0~")) / function() return text_token(" ") end
- + (backslash * S("&|^%:cud{}")) / skip
- + (backslash * named) / char_token
- + (backslash * P("*") * (named + C(P(1)))) / string_token
- + (backslash * P("s") * S("+-") ^ -1 * (named + R("09") * R("09") ^ -1)) / skip
- + (backslash * S("mM") * (named + P(1))) / skip
- + (backslash * C(P(1))) / text_token
- + backslash / skip
- + C((1 - backslash) ^ 1) / text_token
+ + backslash * S('"#') * P(1) ^ 0
+ + (backslash * P("-")) / "-"
+ + (backslash * P("e")) / "\\"
+ + (backslash * S(" 0~")) / " "
+ + backslash * S("&|^%:cud{}")
+ + (backslash * named) / man_chars
+ + (backslash * P("*") * (named + C(P(1)))) / man_strings
+ + (backslash * P("s") * S("+-") ^ -1 * (named + digit * digit ^ -1)) / 0
+ + (backslash * S("mM") * (named + P(1))) / 0
+ + backslash * C(P(1))
+ + backslash
+ + C((1 - backslash) ^ 1)
) ^ 0)
+ local function in_font(font, nodes)
+ if font == "B" then
+ return { { kind = "strong", kids = nodes } }
+ elseif font == "I" then
+ return { { kind = "em", kids = nodes } }
+ end
+ return nodes
+ end
+
local function man_inline(text)
- local tokens = man_inline_grammar:match(text) or {}
+ local tokens = man_inline_grammar:match(text)
local nodes, parts, font = {}, {}, "R"
local function flush()
- if #parts == 0 then
- return
- end
local run = table.concat(parts)
parts = {}
- if run == "" then
- return
- end
- if font == "B" then
- nodes[#nodes + 1] = {
- kind = "strong",
- kids = { { kind = "text", text = run } },
- }
- elseif font == "I" then
- nodes[#nodes + 1] = {
- kind = "em",
- kids = { { kind = "text", text = run } },
- }
- else
- nodes[#nodes + 1] = { kind = "text", text = run }
+ if run ~= "" then
+ nodes[#nodes + 1] = in_font(font, { node_text(run) })[1]
end
end
for _, token in ipairs(tokens) do
- if token.kind == "text" then
- parts[#parts + 1] = token.text
- elseif token.kind == "font" then
+ if type(token) == "string" then
+ parts[#parts + 1] = token
+ else
flush()
font = token.font
end
@@ -809,13 +768,7 @@ if has_lpeg then
-- A no fill block keeps its lines as they are, so only the text of
-- each one is wanted there, without its fonts.
local function man_plain(text)
- local parts = {}
- for _, token in ipairs(man_inline_grammar:match(text) or {}) do
- if token.kind == "text" then
- parts[#parts + 1] = token.text
- end
- end
- return table.concat(parts)
+ return table.concat(plain_text(man_inline(text), {}))
end
-- A macro's arguments divide at blanks, except inside double quotes.
@@ -838,15 +791,6 @@ if has_lpeg then
end
end
- local function in_font(font, nodes)
- if font == "B" then
- return { { kind = "strong", kids = nodes } }
- elseif font == "I" then
- return { { kind = "em", kids = nodes } }
- end
- return nodes
- end
-
-- Each alternating macro sets its arguments in its two fonts by turns
-- with nothing between them, as in .BR ls (1).
local single_font = { B = "B", I = "I" }
@@ -883,11 +827,7 @@ if has_lpeg then
end
local function flush_nofill()
if nofill_lines then
- local body = table.concat(nofill_lines, "\n")
- if #nofill_lines > 0 then
- body = body .. "\n"
- end
- blocks[#blocks + 1] = { kind = "code", text = body }
+ blocks[#blocks + 1] = code_block(nofill_lines)
nofill_lines = nil
end
end
@@ -1000,22 +940,8 @@ if has_lpeg then
end
render_man = function(text)
- if #text > max_bytes then
- return render_plaintext(text)
- end
- local ok, blocks = pcall(parse_man, text)
- if not ok then
- return render_plaintext(text)
- end
- html("<div class='markdown manpage'>")
- pcall(emit_blocks, blocks)
- html("</div>")
+ render_blocks(text, parse_man)
end
-else
- -- No lpeg, so markdown and man readmes read as escaped source, not
- -- nothing.
- render_markdown = render_plaintext
- render_man = render_plaintext
end
@@ -1043,32 +969,13 @@ end
-- cgit takes filter output through a C string sink that stops at the first
-- NUL byte, so each one becomes U+FFFD, as a browser would show it, and the
--- text past it survives. A plain find works on Lua 5.1, where a NUL cannot
--- sit in a pattern.
-local function replace_nul(text)
- if not text:find("\0", 1, true) then
- return text
- end
- local parts, start = {}, 1
- while true do
- local at = text:find("\0", start, true)
- if not at then
- parts[#parts + 1] = text:sub(start)
- return table.concat(parts, "\239\191\189")
- end
- parts[#parts + 1] = text:sub(start, at - 1)
- start = at + 1
- end
-end
-
+-- text past it survives. The class names NUL as the one byte outside \1 to
+-- \255, since on Lua 5.1 a zero byte in a pattern ends it.
function filter_close()
- local text = replace_nul(table.concat(chunks))
+ local text = table.concat(chunks):gsub("[^\1-\255]", "\239\191\189")
chunks = {}
local ext = (filename:match("%.([^.]+)$") or ""):lower()
- local render = handlers[ext] or render_plaintext
- local ok = pcall(render, text)
- if not ok then
- render_plaintext(text)
- end
+ local render = #text <= max_bytes and handlers[ext] or render_plaintext
+ render(text)
return 0
end