Diff options
Diffstat (limited to 'custom/extensions/about-render.lua')
| -rw-r--r-- | custom/extensions/about-render.lua | 259 |
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 |
