#!/usr/bin/env texlua -------------------------------------------------------------------------------- -- FILE: rst_context.lua -- USAGE: ./rst_context.lua -- DESCRIPTION: -- OPTIONS: --- -- REQUIREMENTS: --- -- AUTHOR: Philipp Gesang (Phg), -- VERSION: 1.0 -- CREATED: 31/08/10 19:35:15 CEST -------------------------------------------------------------------------------- -- --- TODO -- - Find an appropriate way to handle generic tables irrespective of the grid -- settings. The problem is: -- http://archive.contextgarden.net/message/20100912.112605.8a1aaf13.en.html -- Seems we'll have to choose either the grid or split tables as default. Not -- good. require "lpeg" help = require "rst_helpers" local dbg_write = help.dbg_writef local C, Cb, Cc, Cg, Cmt, Cp, Cs, Ct, P, R, S, V, match = lpeg.C, lpeg.Cb, lpeg.Cc, lpeg.Cg, lpeg.Cmt, lpeg.Cp, lpeg.Cs, lpeg.Ct, lpeg.P, lpeg.R, lpeg.S, lpeg.V, lpeg.match -- This one should ignore escaped spaces. do local stripper = P{ [1] = "stripper", stripper = V"space"^0 * C((V"space"^0 * (V"escaped" + V"nospace")^1)^0), space = S(" \t\v\n"), nospace = 1 - V"space", escaped = P"\\" * V"space" } function string.strip(str) return stripper:match(str) or "" end end local err = function(str) if str then io.write("\n*[rstctx] Error: " .. str .. "\n\n") end end local rst_context = {} rst_context.collected_adornments = {} rst_context.last_section_level = 0 rst_context.anonymous_targets = 0 rst_context.anonymous_links = {} rst_context.collected_references = {} rst_context.context_references = {} rst_context.structure_references = {} rst_context.anonymous_set = {} rst_context.substitutions = {} rst_context.lastitemnumber = 0 -- enumerations in RST allow arbitrary skips rst_context.current_footnote_number = 0 rst_context.current_symbolnote_number = 0 function rst_context.footnote_reference (label) local tf = state.footnotes if label:match("^%d+$") then -- all digits local c = tonumber(label) --print("creating footnote nr " .. c) return [[\\footnote{\\getbuffer[__footnote_number_]].. c .."]}" elseif label == "#" then --autonumber local rc = rst_context.current_footnote_number rc = rc + 1 --print("creating footnote nr " .. rc) rst_context.current_footnote_number = rc return [[\\footnote{\\getbuffer[__footnote_number_]].. rc .."]}" elseif label:match("^#.+$") then local thelabel = label:match("^#(.+)$") --print("creating labeled footnote " .. thelabel) return [[\\footnote{\\getbuffer[__footnote_label_]].. thelabel .."]}" elseif label == "*" then local rc = rst_context.current_symbolnote_number rc = rc + 1 --print("creating symbolnote nr " .. rc) rst_context.current_symbolnote_number = rc return [[\\symbolnote{\\getbuffer[__footnote_symbol_]].. rc .."]}" else -- “citation reference” for now treating them like footnotes local rc = rst_context.current_footnote_number rc = rc + 1 --rst_context.current_footnote_number = rst_context.current_footnote_number + 1 --print("creating footnote nr " .. rc) rst_context.current_footnote_number = rc return [[\\footnote{\\getbuffer[__footnote_number_]].. rc .."]}" --return [[\\cite{]] .. label .. "}" end end function rst_context.addsetups(item) state.addme[item] = state.addme[item] or true return 0 end do local w = S" \v\t\n" / "_" local wp = Cs((w + 1)^1) function rst_context.whitespace_to_underscore(str) return str and wp:match(str) or "" end end -- So we can use crefs[n][2] to refer to the place where the reference was -- created. local function get_context_reference (str) local crefs = rst_context.context_references local srefs = rst_context.structure_references srefs[str] = true refstring = "__target_" .. rst_context.whitespace_to_underscore(str) crefs[#crefs + 1] = { refstring, str } return refstring end function rst_context.emphasis (str) return [[{\\em ]] .. str .. [[}]] end function rst_context.strong_emphasis (str) return [[{\\sc ]] .. str .. [[}]] end function rst_context.literal (str) str = str:gsub([[\]], [[\\]]) -- evade escaping of backslashes return [[\\type{]] .. str .. [[}]] --return [[\\starttyping ]] .. str .. [[\\stoptyping]] end function rst_context.interpreted_text (...) local tab = { ... } local role, str role = tab[1]:match("^:(.*):$") or tab[3]:match("^:(.*):$") str = tab[2] if not role then -- implicit role role = "emphasis" end return rst_context[role](str) end function rst_context.link_standalone (str) return "\n" .. [[\\goto{\\hyphenatedurl{]] .. str .. [[}}[url(]] .. str .. [=[)]]=] end function rst_context.reference (str) rst_context.addsetups("references") str = str:match("^`?([^`]+)`?_$") -- LPEG could render this gibberish legible but not time return [[\\RSTchoosegoto{__target_]] .. rst_context.whitespace_to_underscore(str) .. "}{" .. str .. "}" end function rst_context.anon_reference (str) rst_context.addsetups("references") str = str:match("^`?([^`]+)`?__$") rst_context.anonymous_links[#rst_context.anonymous_links+1] = str link = "__target_anon_" .. #rst_context.anonymous_links return string.format([[\\RSTchoosegoto{%s}{%s}]], link, str) end local whitespace = S" \n\t\v" local nowhitespace = 1 - whitespace local removewhitespace = Cs((nowhitespace^1 + Cs(whitespace / ""))^0) function rst_context.target (tab) rst_context.addsetups("references") --local tab = { ... } local refs = rst_context.collected_references local arefs = rst_context.anonymous_set local target = tab[#tab] -- Ct + C could be clearer but who cares tab[#tab] = nil local function create_anonymous () rst_context.anonymous_targets = rst_context.anonymous_targets + 1 return { "anon_" .. rst_context.anonymous_targets, rst_context.anonymous_targets } end local insert = "" if target == "" then -- links here for _, id in next, tab do insert = insert .. "\n\\reference[__target_" .. id .. "]{}" end else for i=1,#tab do local id = tab[i] if id == "" then -- anonymous local anon = create_anonymous() id, arefs[anon[1]] = anon[1], anon[2] else id = tab[i]:gsub("\\:",":"):match("`?([^`]+)`?") -- deescaping end if id then refs[id] = refs[id] or target end end end return insert end function rst_context.inline_internal_target (str) return "\\\\reference[__target_" .. rst_context.whitespace_to_underscore(str) .."]{}" end function rst_context.substitution_reference (str, underscores) local sub = "" rst_context.addsetups("substitutions") if underscores == "_" then -- normal reference sub = sub .. [[\\reference[__target_]] .. rst_context.whitespace_to_underscore(string.strip(str)) .. "]{}" elseif underscores == "__" then -- normal reference rst_context.anonymous_targets = rst_context.anonymous_targets + 1 sub = sub .. [[\\reference[__target_anon_]] .. rst_context.anonymous_targets .. "]{}" end return sub .. [[{\\RSTsubstitution]] .. str:gsub("%s", "") .. "}" end function rst_context.escape (str) str = str:gsub("\\(.)", "%1") str = str:gsub("&", "\\letterampersand") return str end function rst_context.joinindented (tab) return table.concat (tab, "") end local corresponding = { ['"'] = '"', ["'"] = "'", ["{"] = "}", ["("] = ")", ["["] = "]", ["<"] = ">", } local inline_parser = P{ [1] = "block", block = Cs(V"inline_as_first"^-1 * (V"enclosed" + V"inline_element" + 1)^1), inline_element = V"precede_inline" * Cs(V"inline_do_elements") * V"succede_inline" + V"footnote_reference" , -- Ugly but needed in case the first element of a paragraph is inline -- formatted. inline_as_first = V"inline_do_elements" * V"succede_inline", inline_do_elements = V"strong_emphasis" + V"substitution_reference" + V"anon_reference" + V"reference" + V"emphasis" + V"inline_literal" + V"interpreted_text" + V"inline_internal_target" + V"link_standalone" , precede_inline = V"spacing" + V"eol" + -P(1) + S[['"([{<-/:]] + P"‘" + P"“" + P"’" + P"«" + P"¡" + P"¿" + V"inline_delimiter" + P"„", -- not in standard Murkin reST succede_inline = V"spacing" + V"eol" + S[['")]}>-/:.,;!?\]] + P"’" + P"”" + P"»" + V"inline_delimiter" + -P(1) + P"“", -- non-standard again but who cares enclosed = V"precede_inline"^-1 * Cg(V"quote_single" + V"quote_double" + V"leftpar", "lastgroup") * V"inline_delimiter" * Cmt(C(V"quote_single" + V"quote_double" + V"rightpar") * Cb("lastgroup"), function(s, i, char, oldchar) return corresponding[oldchar] == char end) * V"succede_inline"^-1 * -V"underscore" , space = P" ", whitespace = (P" " + Cs(P"\t") / " " + Cs(S"\v") / " "), spacing = V"whitespace"^1, eol = P"\n", --inline_delimiters = P"‐" + P"‑" + P"‒" + P"–" + V"emdash" + V"space", -- inline markup inline_delimiter = P"‐" + P"‑" + P"‒" + P"–" + V"emdash" + V"space" + V"bareia" + V"asterisk" + V"bar" + V"lsquare" + V"rsquare" , -- inline markup asterisk = P"*", quote_single = P"'", quote_double = P'"', double_asterisk = V"asterisk" * V"asterisk", bareia = P"`", backslash = P"\\", bar = P"|", double_bareia = V"bareia" * V"bareia", escaped_bareia = (Cs(V"backslash") / "" * V"bareia") + 1, colon = P":", semicolon = P";", underscore = P"_", double_underscore = V"underscore" * V"underscore", dot = P".", interpunct = P"·", comma = P",", dash = P"-", emdash = P"—", ellipsis = P"…" + P"...", exclamationmark = P"!", questionmark = P"?", interrobang = P"‽", double_dash = V"dash" * V"dash", triple_dash = V"double_dash" * V"dash", hyphen = P"‐", dashes = V"dash" + P"‒" + P"–" + V"emdash" + P"―", lparenthesis = P"(", rparenthesis = P")", lsquare = P"[", rsquare = P"]", lbrace = P"{", rbrace = P"}", less = P"<", greater = P">", leftpar = V"lparenthesis" + V"lsquare" + V"lbrace" + V"less", rightpar = V"rparenthesis" + V"rsquare" + V"rbrace" + V"greater", --groupchars = S"()[]{}", groupchars = V"leftpar" + V"rightpar", apostrophe = P"’" + P"'", guillemets = P"«" + P"»", quotationmarks= P"‘" + P"’" + P"“" + P"”", solidus= P"⁄", slash = P"/", gartenzaun = P"#", digit = R"09", letter = R"az" + R"AZ", punctuation = V"apostrophe" + V"colon" + V"comma" + V"dashes" + V"dot" + V"ellipsis" + V"exclamationmark" + V"guillemets" + V"hyphen" + V"interpunct" + V"interrobang" + V"questionmark" + V"quotationmarks" + V"semicolon" + V"slash" + V"solidus" + V"underscore" , emphasis = (V"asterisk" - V"double_asterisk") * Cs((1 - V"spacing" - V"eol" - V"asterisk") * ((1 - (1 * V"asterisk"))^0 * (1 - V"spacing" - V"eol" - V"asterisk"))^-1) * V"asterisk" / rst_context.emphasis, strong_emphasis = V"double_asterisk" * Cs((1 - V"spacing" - V"eol" - V"asterisk") * ((1 - (1 * V"double_asterisk"))^0 * (1 - V"spacing" - V"eol" - V"asterisk"))^-1) * V"double_asterisk" / rst_context.strong_emphasis, inline_literal = V"double_bareia" * C ((V"escaped_bareia" - V"spacing" - V"eol" - V"bareia") * ((V"escaped_bareia" - (1 * V"double_bareia"))^0 * (V"escaped_bareia" - V"spacing" - V"eol" - V"bareia"))^-1) * V"double_bareia" / rst_context.literal, interpreted_text = C(V"role_marker"^-1) * (V"bareia" - V"double_bareia") * C ((1 - V"spacing" - V"eol" - V"bareia") * ((1 - (1 * V"bareia"))^0 * (1 - V"spacing" - V"eol" - V"bareia"))^-1) * V"bareia" * C(V"role_marker"^-1) / rst_context.interpreted_text, role_marker = V"colon" * (V"letter" + V"dash" + V"underscore" + V"dot")^1 * V"colon", link_standalone = C(V"uri") / rst_context.link_standalone, anon_reference = Cs(V"anon_phrase_reference" + V"anon_normal_reference") / rst_context.anon_reference, anon_normal_reference = C((1 - V"underscore" - V"spacing" - V"eol" - V"punctuation" - V"groupchars")^1) * V"double_underscore", anon_phrase_reference = (V"bareia" - V"double_bareia") * C((1 - V"bareia")^1) * V"bareia" * V"double_underscore" , reference = Cs(V"normal_reference" + V"phrase_reference") / rst_context.reference, normal_reference = (1 - V"underscore" - V"spacing" - V"eol" - V"punctuation" - V"groupchars")^1 * V"underscore", phrase_reference = (V"bareia" - V"double_bareia") * C((1 - V"bareia")^1) * V"bareia" * V"underscore" , footnote_reference = V"lsquare" * Cs(V"footnote_label" + V"citation_reference_label") * V"rsquare" * V"underscore" / rst_context.footnote_reference , footnote_label = V"digit"^1 + V"gartenzaun" * V"letter"^1 + V"gartenzaun" + V"asterisk" , citation_reference_label = V"letter" * (1 - V"rsquare")^1, inline_internal_target = V"underscore" * V"bareia" * Cs((1 - V"bareia")^1) * V"bareia" / rst_context.inline_internal_target , substitution_reference = V"bar" * C((1 - V"bar")^1) * V"bar" * C((V"double_underscore" + V"underscore")^-1) / rst_context.substitution_reference , -------------------------------------------------------------------------------- -- Urls -------------------------------------------------------------------------------- uri = V"url_protocol" * V"url_domain" * (V"slash" * V"url_path")^0, url_protocol = (P"http" + P"ftp" + P"shttp" + P"sftp") * P"://", url_domain_char = 1 - V"dot" - V"spacing" - V"eol" - V"punctuation", url_domain = V"url_domain_char"^1 * (V"dot" * V"url_domain_char"^1)^0, url_path_char = R("az", "AZ", "09") + S"-_.!~*'()", url_path = V"slash" * (V"url_path_char"^1 * V"slash"^-1)^1, } function rst_context.paragraph (data) local str if not data then return "" elseif type(data) == "table" then str = #data > 1 and helpers.string.wrapat(inline_parser:match(table.concat(data, " ")), 65) or inline_parser:match(data[1]) else str = data end --print(str) return string.format([[ \\startparagraph %s \\stopparagraph ]], str) end local sectionlevels = { [1] = "chapter", [2] = "section", [3] = "subsection", [4] = "subsubsection", [5] = "subsubsubsection", } local function get_line_pattern (chr) return P(chr)^1 * (-P(1)) end function rst_context.section (...) -- TODO general cleanup; move validity local tab = { ... } -- checking to parser. local section, str = true, "" local adornchar local ulen = unicode.utf8.len if #tab == 3 then -- TODO use unicode length with ConTeXt adornchar = tab[1]:sub(1,1) section = ulen(tab[1]) >= ulen(tab[2]) --section = get_line_pattern(adornchar):match(tab[1]) ~= nil and section str = string.strip(tab[2]) else -- no overline adornchar = tab[2]:sub(1,1) section = ulen(tab[1]) <= ulen(tab[2]) --section = get_line_pattern(adornchar):match(tab[2]) ~= nil and section str = tab[1] end if section then -- determine level local level = rst_context.last_section_level local rca = rst_context.collected_adornments if rca[adornchar] then level = rca[adornchar] else level = level + 1 rca[adornchar] = level rst_context.last_section_level = level end ref = get_context_reference (str) str = string.format("\n\\\\%s[%s]{%s}\n", sectionlevels[level], ref, str) else return [[{\\bf fix your sectioning!}\\endgraf}]] end return section and str or "" end -- Prime time for the fancybreak module. function rst_context.transition (str) rst_context.addsetups("breaks") --return "\\fancybreak\n" return "\\fancybreak{$* * *$}\n" end function rst_context.bullet_marker(str) return "marker" end -- This one should ignore escaped spaces. do local stripper = P{ [1] = "stripper", stripper = V"space"^0 * C((V"space"^0 * V"nospace"^1)^0), space = S(" \t\v\n"), escaped = P"\\" * V"space", nospace = V"escaped" + (1 - V"space"), } function string.strip(str) return stripper:match(str) or "" end end local enumeration_types = { ["*"] = "*", -- unordered bulleted ["+"] = "*", ["-"] = "*", ["•"] = "*", ["‣"] = "*", ["⁃"] = "*", ["#"] = "n", -- numbered lists and conversion ["A"] = "A", ["a"] = "a", ["I"] = "R", ["i"] = "r", } -- \setupitemize[left=(, right=), margin=4em, stopper=] local stripme = S"()." local dontstrip = 1 - stripme local itemstripper = stripme^0 * C(dontstrip^1) * stripme^0 local function parse_itemstring(str) local offset = nil local setup = ",fit][itemalign=flushright," -- string.match is slightly faster than string.find if str:match("^%(") then setup = setup .. [[left=(,]] end if str:match("%)$") then setup = setup .. [[right=)]] end if str:match("%.$") then setup = setup .. [[stopper={.\\space}]] end local num = str:match("^%d") if num then -- http://thread.gmane.org/gmane.comp.tex.context/61728/focus=61729 setup = setup .. ",start=" .. num str = "n" end str = itemstripper:match(str) str = enumeration_types[str] or str return {setup = setup, str = str} end function rst_context.startitemize(str) local setup = "" local result = "" str = string.strip(str) local listtype = enumeration_types[str] or parse_itemstring(str) if type(listtype) == "table" then setup = listtype.setup listtype = listtype.str end result = [[ \\startitemize[]] .. listtype .. setup .. [[] ]] return result end local last_item = {} -- stack local current_itemdepth = 0 function rst_context.stopitemize(str) last_item[current_itemdepth] = nil current_itemdepth = current_itemdepth - 1 return str .. [[ \\stopitemize ]] end function rst_context.bullet_item (tab) local li = last_item -- The capture of the first item has the \startitemize as -- *second* element in the array. local content = #tab == 2 and tab[2] or tab[3] local startstr = #tab == 3 and tab[2] or nil local itemtype = tab[1] local result = startstr or "" if startstr then current_itemdepth = current_itemdepth + 1 li[current_itemdepth] = itemtype elseif li[current_itemdepth] then if helpers.list.successor(itemtype, li[current_itemdepth]) then -- just leave it alone elseif helpers.list.greater(itemtype, li[current_itemdepth]) then local itemnum = tonumber(string.strip(itemtype)) or helpers.list.get_decimal(itemtype) result = result .. string.format([[ \\setnumber[itemgroup:itemize]{%s} ]], itemnum) end li[current_itemdepth] = itemtype end return result .. [[ \\item ]] .. inline_parser:match(content) .. [[ ]] end -------------------------------------------------------------------------------- -- Definition lists -------------------------------------------------------------------------------- -- TODO define proper setups (probably bnf-like and some narrower for def-paragraphs) function rst_context.deflist (list) rst_context.addsetups("deflist") local deflist = [[ \\startRSTdefinitionlist ]] for nd, item in ipairs(list) do local term = item[1] local nc = 2 local tmp = [[ \\RSTdeflistterm{]] .. string.strip(term) .. "}" if #item > 2 then while nc < #item do tmp = tmp .. [[ \\RSTdeflistclassifier{]] .. string.strip(item[nc]) .. "}" nc = nc + 1 end end tmp = tmp .. [[ \\RSTdeflistdefinition{% ]] for np, par in ipairs(item[#item]) do -- definitions, multi-paragraph tmp = tmp .. [[ \\RSTdeflistparagraph{% ]] .. inline_parser:match(par) .. "}\n" end tmp = tmp .. " }" deflist = deflist .. tmp end return deflist .. [[ \\stopRSTdefinitionlist ]] end -------------------------------------------------------------------------------- -- Field lists -------------------------------------------------------------------------------- -- TODO Do something useful with field lists. For now I'm not sure what as the -- bibliography directives from the reST specification seem to make sense only -- when using docinfo and, after all, we have .bib files that are portable. function rst_context.field_list (str) rst_context.addsetups("fieldlist") return [[ \\startRSTfieldlist]] .. str .. [[\\eTABLEbody\\stopRSTfieldlist ]] end function rst_context.field_name (str) return [[\\fieldname{]] .. str .. [[}]] end function rst_context.field_body (str) return [[\\fieldbody{]] .. inline_parser:match(str) .. [[}]] end function rst_context.field (tab) local name, body = tab[1], tab[2] return string.format([[ \\RSTfieldname{%s} \\RSTfieldbody{%s} ]], name, inline_parser:match(body)) end function rst_context.line_comment (str) return "% " .. str end function rst_context.block_comment (str) return string.format([[ \iffalse %s\fi ]], str) end function rst_context.option_list (str) return [[ \\setupTABLE[c][first] [background=color, backgroundcolor=grey, style=\tt] \\setupTABLE[c][each] [frame=off] \\setupTABLE[r][each] [frame=off] \\bTABLE[split=yes,option=stretch] \\bTABLEhead \\bTR \\bTH Option \\eTH \\bTH Description \\eTH \\eTR \\eTABLEhead \\bTABLEbody ]] .. inline_parser:match(str) .. [[ \\eTABLEbody \\eTABLE ]] end function rst_context.option_item (tab) return string.format([[\\bTR\\bTC %s \\eTC\\bTC %s \\eTC\\eTR ]], tab[1], tab[2]) end function rst_context.test(str) return ":" end function rst_context.literal_block (str, included) local indent = P" "^1 --local stripme = indent:match(str) or 0 local stripme = #str for line in str:gmatch("[^\n]+") do -- setting to the lowest indend of all lines local idt = indent:match(line) if line and idt then stripme = idt < stripme and idt or stripme end end local strip = P{ [1] = "strip", strip = Cs(V"line"^1), eol = P"\n", restofline = (1 - V"eol")^0, stop = Cs(V"eol" * P" "^0) * -P(1) / "", -- remove trailing blank lines line = Cs(V"restofline" * (V"stop" + V"eol")) / function (line) return #line > stripme and line:sub(stripme) or line end, } str = strip:match(str) str = [[ \starttyping[lines=hyphenated] ]] .. str .. [[ \stoptyping ]] if included then -- escaping can ruin your day str = str:gsub("\\", "\\\\") end return str end function rst_context.included_literal_block (str) return rst_context.literal_block(str, true) end function rst_context.line_block (str) rst_context.addsetups("lines") return [[ \\startlines ]] .. inline_parser:match(str) .. [[\\stoplines ]] end function rst_context.line_block_line(str) str = str:gsub("\n", " ") return str .. "\n" end function rst_context.line_block_empty() return "\n" end function rst_context.block_quote (tab) rst_context.addsetups("blockquote") local str = [[ \\startlinecorrection \\blank[small] \\startblockquote ]] .. inline_parser:match(tab[1]) .. [[ \\stopblockquote ]] return tab[2] and str .. [[ \\blank[small] \\startattribution ]] .. inline_parser:match(tab[2]) .. [[ \\stopattribution \\blank[small] \\stoplinecorrection ]] or str .. [[ \\blank[small] \\stoplinecorrection ]] end --function rst_context.table (str) --return [[ --\\startlinecorrection --]] .. str .. [[ --\\stoplinecorrection --]] --end function rst_context.grid_table (tab) local body = "" local nr = 1 local head if tab.has_head then head = [[ \\setupTABLE[c][each] [frame=off] \\setupTABLE[r][each] [frame=off] %\\startlinecorrection \\bTABLE[split=repeat,option=stretch] \\bTABLEhead ]] while nr <= tab.head_end do local r = tab.rows[nr] --for i,r in ipairs(tab.rows) do local isempty = true for n, cell in ipairs(r) do if cell.variant == "normal" then isempty = false break end end if not isempty then local row = [[\\bTR]] for n,c in ipairs(r) do if not (c.parent or c.variant == "separator") then local celltext = inline_parser:match(c.stripped) if c.span.x or c.span.y then local span_exp = "[" if c.span.x then span_exp = span_exp .. "nc=" .. c.span.x .. "," end if c.span.y then span_exp = span_exp .. "nr=" .. c.span.y end celltext = span_exp .. "] " .. celltext end row = row .. "\n " .. [[\\bTH ]] .. celltext .. [[\\eTH]] end end head = head .. row .. "\n" .. [[\\eTR]] .. "\n" end nr = nr + 1 end head = head .. [[ \\eTABLEhead \\bTABLEbody ]] else head = [[ \\setupTABLE[c][each] [frame=off] \\setupTABLE[r][each] [frame=off] %\\startlinecorrection \\bTABLE[split=repeat,option=stretch] \\bTABLEbody ]] end while nr <= #tab.rows do local r = tab.rows[nr] --for i,r in ipairs(tab.rows) do local isempty = true for n, cell in ipairs(r) do if cell.variant == "normal" then isempty = false break end end if not isempty then local row = [[\\bTR]] for n,c in ipairs(r) do if not (c.parent or c.variant == "separator") then local celltext = inline_parser:match(c.stripped) if c.span.x or c.span.y then local span_exp = "[" if c.span.x then span_exp = span_exp .. "nc=" .. c.span.x .. "," end if c.span.y then span_exp = span_exp .. "nr=" .. c.span.y end celltext = span_exp .. "] " .. celltext end row = row .. "\n " .. [[\\bTC ]] .. celltext .. [[\\eTC]] end end body = body .. row .. "\n" .. [[\\eTR]] .. "\n" end nr = nr + 1 end local tail = [[ \\eTABLEbody \\eTABLE %\\stoplinecorrection ]] return head .. body .. tail end function rst_context.simple_table(tab) local head local nr = 1 if tab.head_end then head = [[ \\setupTABLE[c][each] [frame=off] \\setupTABLE[r][each] [frame=off] %\\startlinecorrection \\bTABLE[split=yes,option=stretch] \\bTABLEhead ]] while nr <= tab.head_end do local row = tab[nr] if not row.ignore then dbg_write(">hr>" .. #row) head = head .. [[\\bTR]] for nc,cell in ipairs(row) do dbg_write("%7s | ", cell.content) local celltext = inline_parser:match(cell.content) if cell.span then head = head .. string.format([=[\\bTH[nc=%s]%s\\eTH]=], cell.span.x, celltext) else head = head .. [[\\bTH ]] .. celltext .. [[\\eTH]] end end dbg_write("\n") head = head .. "\\\\eTR\n" end nr = nr + 1 end head = head .. [[ \\eTABLEhead \\bTABLEbody ]] else head = [[ \\setupTABLE[c][each] [frame=off] \\setupTABLE[r][each] [frame=off] %\\startlinecorrection \\bTABLE[split=yes,option=stretch] \\bTABLEbody ]] end local tail = [[ \\eTABLEbody \\eTABLE %\\stoplinecorrection ]] local body = "" while nr <= #tab do local row = tab[nr] if not row.ignore then dbg_write(">tr>" .. #row) body = body .. [[\\bTR]] for nc,cell in ipairs(row) do dbg_write("%7s | ", cell.content) local celltext = inline_parser:match(cell.content) if cell.span then body = body .. string.format([=[\\bTC[nc=%s]%s\\eTC]=], cell.span.x, celltext) else body = body .. [[\\bTC ]] .. celltext .. [[\\eTC]] end end dbg_write("\n") body = body .. "\\\\eTR\n" end nr = nr + 1 end return head .. body .. tail end function rst_context.footnote (label, content) local tf = state.footnotes rst_context.addsetups("footnotes") if label:match("^%d+$") then -- all digits tf.numbered[tonumber(label)] = rst_context.escape(inline_parser:match(content)) elseif label == "#" then --autonumber repeat -- until next unrequested number tf.autonumber = tf.autonumber + 1 until tf.numbered[tf.autonumber] == nil tf.numbered[tf.autonumber] = rst_context.escape(inline_parser:match(content)) elseif label:match("^#.+$") then local thelabel = label:match("^#(.+)$") tf.autolabel[thelabel] = rst_context.escape(inline_parser:match(content)) elseif label == "*" then rst_context.addsetups("footnote_symbol") tf.symbol[#tf.symbol+1] = rst_context.escape(inline_parser:match(content)) else -- “citation reference” treated like ordinary footnote repeat -- until next unrequested number tf.autonumber = tf.autonumber + 1 until tf.numbered[tf.autonumber] == nil tf.numbered[tf.autonumber] = rst_context.escape(inline_parser:match(content)) end return "" end function rst_context.substitution_definition (subtext, directive, data) data = table.concat(data, "\n") local rs = rst_context.substitutions rs[subtext] = { directive = directive, data = data } return "" end -- not to be confused with the directive definition table rst_context.directives function rst_context.directive(directive, ...) local rd = rst_context.directives rst_context.addsetups("directive") local data = {...} local result = "" if rd[directive] then result = rd[directive](data) end return result end optional_setups = {} function optional_setups.footnote_symbol () local setup = [[ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Footnotes with symbol conversion % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \definenote[symbolnote][footnote] \setupnote [symbolnote][way=bypage,numberconversion=set 2] ]] return setup end function optional_setups.footnotes () local tf = state.footnotes local fn = [[ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Footnotes % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ]] local buffer = [[ %% %s \startbuffer[%s] %s\stopbuffer ]] for nf, note in next, tf.numbered do fn = fn .. string.format(buffer, "Autonumbered footnote", "__footnote_number_"..nf, note) end for nf, note in next, tf.autolabel do fn = fn .. string.format(buffer, "Labeled footnote", "__footnote_label_"..nf, note) end for nf, note in next, tf.symbol do fn = fn .. string.format(buffer, "Symbol footnote", "__footnote_symbol_"..nf, note) end return fn end function optional_setups.references () local refs = rst_context.collected_references local crefs = rst_context.context_references local arefs = rst_context.anonymous_set local function resolve_indirect (r) if r and r:match(".*_$") then -- pointing elsewhere local look_me_up = r:match("^`?([^`]*)`?_$") local result = resolve_indirect (refs[look_me_up]) if result then return result else if rst_context.structure_references[look_me_up] then -- Internal link, no useURL etc. return false end end end return r end local refsection = [[ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % References % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ]] local references = {} local ref_keys = {} for ref, target in next, refs do ref_keys[#ref_keys+1] = [[__target_]] .. rst_context.whitespace_to_underscore(ref) target = resolve_indirect(target) if target ~= false then ref_text = ref if arefs[ref_text] then ref_text = rst_context.anonymous_links[tonumber(arefs[ref_text])] end references[#references+1] = string.format([[ \useURL[__target_%s] [%s] [] [%s] ]], rst_context.whitespace_to_underscore(ref), target, ref_text) end end refsection = refsection .. table.concat(references, "\n") -- this is needed in order to select the right reference command later refsection = refsection .. "\n\n" .. [[\def \RSTexternalreferences{]] .. table.concat(ref_keys, ",") .. [[} % #1 target name, #2 link text \def\RSTchoosegoto#1#2{% \rawdoifinsetelse{#1}{\RSTexternalreferences}% {\from[#1]}% {\goto{#2}[#1]}% } ]] return refsection end -------------------------------------------------------------------------------- -- Directives for use with |substitutions| -------------------------------------------------------------------------------- rst_context.directives = {} rst_context.directives.anonymous = 0 rst_context.directives.images = {} rst_context.directives.images.done = {} rst_context.directives.images.values = {} rst_context.directives.images.keys = { ["width"] = "width", ["size"] = "width", ["caption"] = "caption", ["scale"] = "scale", } rst_context.directives.images.values.scale = function (orig) -- http://wiki.contextgarden.net/Reference/en/useexternalfigure -- scale=1000 is original size; to get 72%, use scale=720. return tonumber(orig) * 1000 end rst_context.directives.images.values.width = { ["fit"] = "\\hsize", ["hsize"] = "\\hsize", ["broad"] = "\\hsize", ["normal"] = "local", ["normal"] = "local", } -- we won't allow passing arbitrary setups to context rst_context.directives.images.permitted_setups = { "width", "scale" } local function img_setup (properties) local result = "" for _, prop in next, rst_context.directives.images.permitted_setups do if properties[prop] then result = result .. prop .. "=" .. properties[prop] .. "," end end if result ~= "" then result = "[" .. result .. "]" end return result end rst_context.directives.image = function(name, data) local properties = {} local anon = false local rd = rst_context.directives if not data then -- this makes the “name” argument optional data = name rd.anonymous = rd.anonymous + 1 anon = true -- indicates a nameless picture name = "anonymous" .. rd.anonymous end properties.caption = name --properties.width = "\\local" local processed = "" -- stub; TODO do something useful with optional dimension specifications if type(data) == "table" then -- should always be true local p = helpers.patterns for _, str in ipairs(data) do local key, val key, val = p.colon_keyval:match(str) local rdi = rst_context.directives.images if key and val then key = rdi.keys[key] -- sanitize key expression if type(rdi.values[key]) == "table" then val = rdi.values[key][val] elseif type(rdi.values[key]) == "function" then val = rdi.values[key](val) end properties[key] = val else processed = processed .. (str and str ~= "" and string.strip(str)) end end end properties.setup = img_setup(properties) or "" data = processed processed = nil local img = "" local images_done = rd.images.done if not anon then if not images_done[name] then img = img .. string.format([[ \useexternalfigure[%s][%s][] ]], name, data) images_done[name] = true end img = img .. string.format([[ \def\RSTsubstitution%s{%% \placefigure[here]{%s}{\externalfigure[%s]%s} } ]], name, inline_parser:match(properties.caption), name, properties.setup) else -- image won't be referenced but used instantly img = img .. string.format([[ \placefigure[here]{%s}{\externalfigure[%s]%s} ]], inline_parser:match(properties.caption), data, properties.setup) end return img end rst_context.directives.caution = function(raw) rst_context.addsetups("dbend") rst_context.addsetups("caution") local text local first = true for _, line in ipairs(raw) do if not helpers.patterns.spacesonly:match(line) then if first then text = line first = false else text = text .. " " .. line end end end text = rst_context.escape(helpers.string.wrapat(inline_parser:match(text))) return string.format([[ \startRSTcaution %s \stopRSTcaution ]], text) end rst_context.directives.danger = function(raw) rst_context.addsetups("dbend") rst_context.addsetups("danger") local text local first = true for _, line in ipairs(raw) do if not helpers.patterns.spacesonly:match(line) then if first then text = line first = false else text = text .. " " .. line end end end text = rst_context.escape(helpers.string.wrapat(inline_parser:match(text))) return string.format([[ \startRSTdanger %s \stopRSTdanger ]], text) end -- http://docutils.sourceforge.net/docs/ref/rst/directives.html rst_context.directives.DANGER = function(addendum) local result = "" for _,str in ipairs(addendum) do result = result .. (string.strip(str)) end return string.format([[ %% The Rabbit of Caerbannog \startlinecorrection \blank[force,big] \framed[frame=on, corner=round, rulethickness=5pt, align=middle, width=\hsize, frameoffset=.5em, backgroundoffset=1em, background=color, backgroundcolor=red, foreground=color, foregroundcolor=black]{%% \language[en-gb]\tfb\bf Follow only if ye be men of valour, for the entrance to this cave is guarded by a creature so foul, so cruel that no man yet has fought with it and lived. Bones of full fifty men lie strewn about its lair. So, brave knights, if you do doubt your courage or your strength, come no further, for death awaits you all with nasty, big, pointy teeth.%% \blank[force,big] %s%% } \blank[force,big] \stoplinecorrection ]], result) end rst_context.directives.ctx = function(name, data) local ctx = string.format([[ \startbuffer[%s] %s \stopbuffer \def\RSTsubstitution%s{%% \getbuffer[%s] } ]], name, data, name, name) return ctx end rst_context.directives.lua = function(name, data) local luacode = string.format([[ \startbuffer[%s] \startluacode %s \stopluacode \stopbuffer \def\RSTsubstitution%s{%% \getbuffer[%s]%% } ]], name, data, name, name) return luacode end rst_context.directives.replace = function(name, data) return string.format([[ \def\RSTsubstitution%s{%s} ]], name, data) end function optional_setups.substitutions () local directives = rst_context.directives local substitutions = [[ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Substitutions % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ]] local rs = rst_context.substitutions for name, content in next, rs do local directive, data = content.directive, content.data name, data = name:gsub("%s", ""), string.strip(data) if directives[directive] then substitutions = substitutions .. directives[directive](name, data) else err(directive .. " does not exist.") end end return substitutions end function optional_setups.directive () --local directives = rst_context.directives --local dirstr = [[ --%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --% Directives % --%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --]] --return dirstr return "" end function optional_setups.blockquote () return [[ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Blockquotes % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \setupdelimitedtext [blockquote][style={\tfx}] % awful placeholder \definedelimitedtext[attribution][blockquote] \setupdelimitedtext [attribution][style={\tfx\it}] ]] end function optional_setups.deflist () return [[ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Definitionlist % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \def\startRSTdefinitionlist{ \bgroup \def \RSTdeflistterm##1{{\bf ##1}} \def\RSTdeflistclassifier##1{\hbox to 1em{\it ##1}} \def\RSTdeflistdefinition##1{% \startnarrower[left] ##1% \stopnarrower} \def\RSTdeflistparagraph ##1{% \startparagraph{% \noindentation ##1 \stopparagraph} } } \let\stopRSTdefinitionlist\egroup ]] end function optional_setups.lines () return [[ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Lines environment (line blocks) % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \setuplines[% space=on,% before={\startlinecorrection\blank[small]},% after={\blank[small]\stoplinecorrection},% ] ]] end function optional_setups.breaks () return [[ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Fancy transitions % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \usemodule[fancybreak] \setupfancybreak[symbol=star] ]] end function optional_setups.fieldlist () return [[ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Fieldlists % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \def\startRSTfieldlist{% \bgroup% \unexpanded\def\RSTfieldname##1{\bTR\bTC ##1\eTC} \unexpanded\def\RSTfieldbody##1{\bTC ##1\eTC\eTR} % \setupTABLE[c][first] [background=color, backgroundcolor=grey, style=\bf] \setupTABLE[c][2] [align=right] \setupTABLE[c][each] [frame=off] \setupTABLE[r][each] [frame=off] \bTABLE[split=yes,option=stretch] \bTABLEhead \bTR \bTH Field \eTH \bTH Body \eTH \eTR \eTABLEhead \bTABLEbody } \def\stopRSTfieldlist{% %\eTABLEbody % doesn't work, temporarily moved to rst_context.field_list() \eTABLE \egroup% } ]] end function optional_setups.dbend () -- There's just no reason for not providing this. optional_setups.dbend_done = true return [[ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Dangerous bend % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \loadmapfile [manfnt.map] \definefontsynonym [bends] [manfnt] \def\GetSym#1{\getglyph{bends}{\char#1}} \startsymbolset [Dangerous Bends] \definesymbol [dbend] [\GetSym{127}] \definesymbol [lhdbend] [\GetSym{126}] \definesymbol [lhdbend] [\GetSym{0}] \stopsymbolset \setupsymbolset [Dangerous Bends] ]] end function optional_setups.caution () local result = "" --if not optional_setups.dbend_done then --result = result .. optional_setups.dbend() --end return result .. [[ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Caution directive % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \usemodule[lettrine] \setbox0=\hbox{\symbol[dbend]} \newskip\RSTbendskip \RSTbendskip=\wd0 \advance\RSTbendskip by 1em % These two lines should add \advance\RSTbendskip by 1pt % 13.4pt in mkiv and 13.14983pt in mkii % to make the indent equal to the indent % of the “danger” directive. % (2*(width)dbend + (kern)1pt + 1em \def\startRSTcaution{% \startparagraph \dontleavehmode\lettrine[Lines=2,Raise=.6,Findent=\RSTbendskip,Nindent=0pt]{\symbol[dbend]}{}% } \let\stopRSTcaution\stopparagraph ]] end function optional_setups.danger () local result = "" --if not optional_setups.dbend_done then --result = result .. optional_setups.dbend() --end return result .. [[ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Danger directive % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \usemodule[lettrine] \def\startRSTdanger{% \startparagraph \lettrine[Lines=2,Raise=.6,Findent=1em,Nindent=0pt]{\symbol[dbend]\kern 1pt\symbol[dbend]}{}% } \let\stopRSTdanger\stopparagraph ]] end return rst_context