Diff options
Diffstat (limited to 'custom/extensions')
| -rw-r--r-- | custom/extensions/about-render.lua | 240 |
1 file changed, 188 insertions, 52 deletions
diff --git a/custom/extensions/about-render.lua b/custom/extensions/about-render.lua index 040768c..808dd08 100644 --- a/custom/extensions/about-render.lua +++ b/custom/extensions/about-render.lua @@ -343,9 +343,10 @@ end -- Markdown is a deliberate subset rather than CommonMark, leaving out -- reference links, raw HTML passthrough and nested lists, and inline markup --- does not span the end of a line. Man rendering covers the common macros and --- drops the rest. 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. +-- does not span the end of a line. Man rendering covers the man(7) macros and +-- drops the roff requests and definitions that generators put around them. +-- 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 render_markdown, render_man @@ -712,41 +713,58 @@ if has_lpeg then -- and folds it into the nodes markdown emits, instead of nesting. local macro_line = S(".'") * space ^ 0 * C((1 - space) ^ 1) * space ^ 0 * C(P(1) ^ 0) - local one_font = { B = "B", I = "I" } - local two_font = { CB = "B", BI = "B", CI = "I" } + -- Fonts by name and by position. Anything else, \fP for the previous + -- font among them, falls back to roman. + local fonts = { + B = "B", I = "I", BI = "B", CB = "B", CI = "I", + ["2"] = "I", ["3"] = "B", + } + + -- The named characters and strings generated pages use most. A name + -- with no entry renders as nothing rather than as its spelling. local man_chars = { - aq = "'", - cq = "'", - oq = "'", - dq = '"', - lq = '"', - rq = '"', - hy = "-", - en = "-", - em = "-", + aq = "'", dq = '"', cq = "’", oq = "‘", lq = "“", rq = "”", + hy = "-", en = "–", em = "—", bu = "•", co = "©", rg = "®", + tm = "™", ti = "~", ha = "^", rs = "\\", + } + local man_strings = { + lq = "“", rq = "”", R = "®", Tm = "™", Aq = "'", + ['L"'] = "“", ['R"'] = "”", } + local backslash = P("\\") - local function font_token(name) return { kind = "font", font = name } end 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 "") + end + + -- \( takes a two character name and \[ a longer one, bounded so that a + -- line of unclosed openers stays linear. + local named = P("(") * C(P(1) * P(1)) + + P("[") * C((1 - P("]")) ^ -64) * P("]") + -- Comments, size changes, colours, the half line moves DocBook puts + -- around a footnote mark and conditional braces draw nothing, and nor do + -- the escapes that only steer hyphenation or breaks. local man_inline_grammar = Ct(( - (backslash * P("f") * P("(") * C(P(1) * P(1))) - / function(name) return font_token(two_font[name] or "R") end - + (backslash * P("f") * P("[") * C((1 - P("]")) ^ 0) * P("]")) - / function(name) return font_token(one_font[name] or "R") end - + (backslash * P("f") * C(P(1))) - / function(name) return font_token(one_font[name] or "R") end + (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 * P(" ")) / function() return text_token(" ") end - + (backslash * S("&|^")) / function() return skip_token end - + (backslash * P("(") * C(P(1) * P(1))) - / function(name) return text_token(man_chars[name] or "") end - + (backslash * P("*") * (P("(") * P(1) * P(1) + P(1))) - / function() return skip_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 / function() return skip_token end + + backslash / skip + C((1 - backslash) ^ 1) / text_token ) ^ 0) @@ -788,10 +806,75 @@ if has_lpeg then return nodes end + -- 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) + end + + -- A macro's arguments divide at blanks, except inside double quotes. + local function man_args(rest) + local args, i = {}, 1 + while true do + i = rest:find("%S", i) + if not i then + return args + end + local stop + if rest:sub(i, i) == '"' then + stop = rest:find('"', i + 1, true) or #rest + 1 + args[#args + 1] = rest:sub(i + 1, stop - 1) + else + stop = rest:find("%s", i) or #rest + 1 + args[#args + 1] = rest:sub(i, stop - 1) + end + i = stop + 1 + 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" } + local alternating = { + BR = { "B", "R" }, RB = { "R", "B" }, IR = { "I", "R" }, + RI = { "R", "I" }, BI = { "B", "I" }, IB = { "I", "B" }, + } + + -- No fill blocks under their man(7) and groff names, and under the Vb + -- and Ve that pod2man defines for its verbatim paragraphs. + local nofill_start = { nf = true, EX = true, Vb = true } + local nofill_end = { fi = true, EE = true, Ve = true } + + -- Macros that only begin a paragraph, Sp being pod2man's, ones that only + -- set spacing or an index entry, and the requests that open a definition, + -- which runs to a line of two dots. + local breaks = { + TH = true, PP = true, LP = true, P = true, HP = true, RS = true, + RE = true, sp = true, Sp = true, + } + local silent = { PD = true, DT = true, IX = true } + local definitions = { de = true, de1 = true, am = true, ig = true } + local function parse_man(text) local lines = split_lines(text) - local blocks = {} + local blocks, defined = {}, {} local paragraph, nofill_lines, nofill = nil, nil, false + local skipping, tag_pending = false, false local function flush_paragraph() if paragraph and #paragraph > 0 then blocks[#blocks + 1] = { kind = "para", lines = paragraph } @@ -808,54 +891,107 @@ if has_lpeg then nofill_lines = nil end end + -- A tagged paragraph puts its tag on a line of its own above the + -- text it describes. local function add_line(nodes) + if tag_pending then + nodes[#nodes + 1] = { kind = "break" } + tag_pending = false + end paragraph = paragraph or {} paragraph[#paragraph + 1] = nodes end for _, line in ipairs(lines) do local macro, rest = macro_line:match(line) - if rest then - rest = (rest:gsub('^"(.*)"$', "%1")) - end - if nofill then - if macro == "fi" then + if skipping then + skipping = not line:find("^%.%.%s*$") + elseif macro and macro:sub(1, 1) == "\\" + or line:find("^[.']%s*$") then + -- A comment, the close of a conditional block or an empty + -- request. + elseif nofill then + if nofill_end[macro] then nofill = false flush_nofill() - else - nofill_lines[#nofill_lines + 1] = line + elseif not macro then + nofill_lines[#nofill_lines + 1] = man_plain(line) end + elseif definitions[macro] then + -- What a page defines for itself cannot be expanded here, + -- so the definition is skipped and so are its uses. + local name = man_args(rest)[1] + if name and macro ~= "ig" then + defined[name] = true + end + skipping = true elseif line:match("^%s*$") then flush_paragraph() - elseif macro and macro:sub(1, 1) == "\\" then - -- A roff comment, dropped. elseif macro == "SH" or macro == "SS" then flush_paragraph() + tag_pending = false blocks[#blocks + 1] = { kind = "heading", level = (macro == "SH") and 2 or 3, - kids = man_inline(rest), + kids = man_inline(table.concat(man_args(rest), " ")), } - elseif macro == "nf" then + elseif nofill_start[macro] then flush_paragraph() nofill = true nofill_lines = {} - elseif macro == "fi" then + elseif nofill_end[macro] then flush_nofill() - elseif macro == "B" or macro == "BR" or macro == "RB" or macro == "BI" or macro == "IB" then - add_line({ { kind = "strong", kids = man_inline(rest) } }) - elseif macro == "I" or macro == "IR" or macro == "RI" then - add_line({ { kind = "em", kids = man_inline(rest) } }) - elseif macro == "TH" or macro == "PP" or macro == "LP" - or macro == "P" or macro == "TP" or macro == "IP" - or macro == "HP" or macro == "RS" or macro == "RE" - or macro == "sp" or macro == "br" then + elseif single_font[macro] then + local nodes = man_inline(table.concat(man_args(rest), " ")) + add_line(in_font(single_font[macro], nodes)) + elseif alternating[macro] then + local pair, nodes = alternating[macro], {} + for k, arg in ipairs(man_args(rest)) do + local font = pair[(k - 1) % 2 + 1] + for _, node in ipairs(in_font(font, man_inline(arg))) do + nodes[#nodes + 1] = node + end + end + add_line(nodes) + elseif macro == "TP" then flush_paragraph() + tag_pending = true + elseif macro == "IP" then + -- A bullet or a number leads the text on its line, and any + -- other tag takes a line of its own. + flush_paragraph() + local tag = man_args(rest)[1] + if tag and tag ~= "" then + local nodes, plain = man_inline(tag), man_plain(tag) + if plain:find("%w") and not plain:find("^%d+[.)]?$") then + nodes[#nodes + 1] = { kind = "break" } + else + nodes[#nodes + 1] = { kind = "text", text = " " } + end + add_line(nodes) + end + elseif macro == "br" then + if paragraph then + local last = paragraph[#paragraph] + last[#last + 1] = { kind = "break" } + end + elseif breaks[macro] then + flush_paragraph() + elseif macro + and (silent[macro] or defined[macro] or macro:find("^%l")) then + -- A macro the page defined, or a roff request, which in + -- lower case only steers the layout. elseif macro then - if rest ~= "" then - add_line(man_inline(rest)) + local args = man_args(rest) + if #args > 0 then + add_line(man_inline(table.concat(args, " "))) end else - add_line(man_inline(line)) + -- A line of nothing but escapes that draw nothing, such as + -- a conditional's brace, adds no line. + local nodes = man_inline(line) + if #nodes > 0 then + add_line(nodes) + end end end flush_paragraph() |
