diff options
author | Karl Berry <karl@freefriends.org> | 2021-10-01 22:00:34 +0000 |
---|---|---|
committer | Karl Berry <karl@freefriends.org> | 2021-10-01 22:00:34 +0000 |
commit | c715cd9bf039c14787d154d495e9ef181b07bfce (patch) | |
tree | a67caf93acb953c8fd2d4c2c294200b265fa10bd /Master/texmf-dist/tex/luatex | |
parent | 070e5192bad37b3ceee5d098ea0e8c7519986734 (diff) |
markdown (2oct21)
git-svn-id: svn://tug.org/texlive/trunk@60667 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/tex/luatex')
-rw-r--r-- | Master/texmf-dist/tex/luatex/markdown/markdown-tinyyaml.lua | 838 | ||||
-rw-r--r-- | Master/texmf-dist/tex/luatex/markdown/markdown.lua | 202 |
2 files changed, 1010 insertions, 30 deletions
diff --git a/Master/texmf-dist/tex/luatex/markdown/markdown-tinyyaml.lua b/Master/texmf-dist/tex/luatex/markdown/markdown-tinyyaml.lua new file mode 100644 index 00000000000..f921ac45716 --- /dev/null +++ b/Master/texmf-dist/tex/luatex/markdown/markdown-tinyyaml.lua @@ -0,0 +1,838 @@ +-- MIT License +-- +-- Copyright (c) 2017 peposso +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy +-- of this software and associated documentation files (the "Software"), to deal +-- in the Software without restriction, including without limitation the rights +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +-- copies of the Software, and to permit persons to whom the Software is +-- furnished to do so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. + +------------------------------------------------------------------------------- +-- tinyyaml - YAML subset parser +------------------------------------------------------------------------------- + +local table = table +local string = string +local schar = string.char +local ssub, gsub = string.sub, string.gsub +local sfind, smatch = string.find, string.match +local tinsert, tconcat, tremove = table.insert, table.concat, table.remove +local setmetatable = setmetatable +local pairs = pairs +local type = type +local tonumber = tonumber +local math = math +local getmetatable = getmetatable +local error = error + +local UNESCAPES = { + ['0'] = "\x00", z = "\x00", N = "\x85", + a = "\x07", b = "\x08", t = "\x09", + n = "\x0a", v = "\x0b", f = "\x0c", + r = "\x0d", e = "\x1b", ['\\'] = '\\', +}; + +------------------------------------------------------------------------------- +-- utils +local function select(list, pred) + local selected = {} + for i = 0, #list do + local v = list[i] + if v and pred(v, i) then + tinsert(selected, v) + end + end + return selected +end + +local function startswith(haystack, needle) + return ssub(haystack, 1, #needle) == needle +end + +local function ltrim(str) + return smatch(str, "^%s*(.-)$") +end + +local function rtrim(str) + return smatch(str, "^(.-)%s*$") +end + +local function trim(str) + return smatch(str, "^%s*(.-)%s*$") +end + +------------------------------------------------------------------------------- +-- Implementation. +-- +local class = {__meta={}} +function class.__meta.__call(cls, ...) + local self = setmetatable({}, cls) + if cls.__init then + cls.__init(self, ...) + end + return self +end + +function class.def(base, typ, cls) + base = base or class + local mt = {__metatable=base, __index=base} + for k, v in pairs(base.__meta) do mt[k] = v end + cls = setmetatable(cls or {}, mt) + cls.__index = cls + cls.__metatable = cls + cls.__type = typ + cls.__meta = mt + return cls +end + + +local types = { + null = class:def('null'), + map = class:def('map'), + omap = class:def('omap'), + pairs = class:def('pairs'), + set = class:def('set'), + seq = class:def('seq'), + timestamp = class:def('timestamp'), +} + +local Null = types.null +function Null.__tostring() return 'yaml.null' end +function Null.isnull(v) + if v == nil then return true end + if type(v) == 'table' and getmetatable(v) == Null then return true end + return false +end +local null = Null() + +function types.timestamp:__init(y, m, d, h, i, s, f, z) + self.year = tonumber(y) + self.month = tonumber(m) + self.day = tonumber(d) + self.hour = tonumber(h or 0) + self.minute = tonumber(i or 0) + self.second = tonumber(s or 0) + if type(f) == 'string' and sfind(f, '^%d+$') then + self.fraction = tonumber(f) * math.pow(10, 3 - #f) + elseif f then + self.fraction = f + else + self.fraction = 0 + end + self.timezone = z +end + +function types.timestamp:__tostring() + return string.format( + '%04d-%02d-%02dT%02d:%02d:%02d.%03d%s', + self.year, self.month, self.day, + self.hour, self.minute, self.second, self.fraction, + self:gettz()) +end + +function types.timestamp:gettz() + if not self.timezone then + return '' + end + if self.timezone == 0 then + return 'Z' + end + local sign = self.timezone > 0 + local z = sign and self.timezone or -self.timezone + local zh = math.floor(z) + local zi = (z - zh) * 60 + return string.format( + '%s%02d:%02d', sign and '+' or '-', zh, zi) +end + + +local function countindent(line) + local _, j = sfind(line, '^%s+') + if not j then + return 0, line + end + return j, ssub(line, j+1) +end + +local function parsestring(line, stopper) + stopper = stopper or '' + local q = ssub(line, 1, 1) + if q == ' ' or q == '\t' then + return parsestring(ssub(line, 2)) + end + if q == "'" then + local i = sfind(line, "'", 2, true) + if not i then + return nil, line + end + return ssub(line, 2, i-1), ssub(line, i+1) + end + if q == '"' then + local i, buf = 2, '' + while i < #line do + local c = ssub(line, i, i) + if c == '\\' then + local n = ssub(line, i+1, i+1) + if UNESCAPES[n] ~= nil then + buf = buf..UNESCAPES[n] + elseif n == 'x' then + local h = ssub(i+2,i+3) + if sfind(h, '^[0-9a-fA-F]$') then + buf = buf..schar(tonumber(h, 16)) + i = i + 2 + else + buf = buf..'x' + end + else + buf = buf..n + end + i = i + 1 + elseif c == q then + break + else + buf = buf..c + end + i = i + 1 + end + return buf, ssub(line, i+1) + end + if q == '{' or q == '[' then -- flow style + return nil, line + end + if q == '|' or q == '>' then -- block + return nil, line + end + if q == '-' or q == ':' then + if ssub(line, 2, 2) == ' ' or ssub(line, 2, 2) == '\n' or #line == 1 then + return nil, line + end + end + local buf = '' + while #line > 0 do + local c = ssub(line, 1, 1) + if sfind(stopper, c, 1, true) then + break + elseif c == ':' and (ssub(line, 2, 2) == ' ' or ssub(line, 2, 2) == '\n' or #line == 1) then + break + elseif c == '#' and (ssub(buf, #buf, #buf) == ' ') then + break + else + buf = buf..c + end + line = ssub(line, 2) + end + return rtrim(buf), line +end + +local function isemptyline(line) + return line == '' or sfind(line, '^%s*$') or sfind(line, '^%s*#') +end + +local function equalsline(line, needle) + return startswith(line, needle) and isemptyline(ssub(line, #needle+1)) +end + +local function compactifyemptylines(lines) + -- Appends empty lines as "\n" to the end of the nearest preceding non-empty line + local compactified = {} + local lastline = {} + for i = 1, #lines do + local line = lines[i] + if isemptyline(line) then + if #compactified > 0 and i < #lines then + tinsert(lastline, "\n") + end + else + if #lastline > 0 then + tinsert(compactified, tconcat(lastline, "")) + end + lastline = {line} + end + end + if #lastline > 0 then + tinsert(compactified, tconcat(lastline, "")) + end + return compactified +end + +local function checkdupekey(map, key) + if map[key] ~= nil then + -- print("found a duplicate key '"..key.."' in line: "..line) + local suffix = 1 + while map[key..'_'..suffix] do + suffix = suffix + 1 + end + key = key ..'_'..suffix + end + return key +end + +local function parseflowstyle(line, lines) + local stack = {} + while true do + if #line == 0 then + if #lines == 0 then + break + else + line = tremove(lines, 1) + end + end + local c = ssub(line, 1, 1) + if c == '#' then + line = '' + elseif c == ' ' or c == '\t' or c == '\r' or c == '\n' then + line = ssub(line, 2) + elseif c == '{' or c == '[' then + tinsert(stack, {v={},t=c}) + line = ssub(line, 2) + elseif c == ':' then + local s = tremove(stack) + tinsert(stack, {v=s.v, t=':'}) + line = ssub(line, 2) + elseif c == ',' then + local value = tremove(stack) + if value.t == ':' or value.t == '{' or value.t == '[' then error() end + if stack[#stack].t == ':' then + -- map + local key = tremove(stack) + key.v = checkdupekey(stack[#stack].v, key.v) + stack[#stack].v[key.v] = value.v + elseif stack[#stack].t == '{' then + -- set + stack[#stack].v[value.v] = true + elseif stack[#stack].t == '[' then + -- seq + tinsert(stack[#stack].v, value.v) + end + line = ssub(line, 2) + elseif c == '}' then + if stack[#stack].t == '{' then + if #stack == 1 then break end + stack[#stack].t = '}' + line = ssub(line, 2) + else + line = ','..line + end + elseif c == ']' then + if stack[#stack].t == '[' then + if #stack == 1 then break end + stack[#stack].t = ']' + line = ssub(line, 2) + else + line = ','..line + end + else + local s, rest = parsestring(line, ',{}[]') + if not s then + error('invalid flowstyle line: '..line) + end + tinsert(stack, {v=s, t='s'}) + line = rest + end + end + return stack[1].v, line +end + +local function parseblockstylestring(line, lines, indent) + if #lines == 0 then + error("failed to find multi-line scalar content") + end + local s = {} + local firstindent = -1 + local endline = -1 + for i = 1, #lines do + local ln = lines[i] + local idt = countindent(ln) + if idt <= indent then + break + end + if ln == '' then + tinsert(s, '') + else + if firstindent == -1 then + firstindent = idt + elseif idt < firstindent then + break + end + tinsert(s, ssub(ln, firstindent + 1)) + end + endline = i + end + + local striptrailing = true + local sep = '\n' + local newlineatend = true + if line == '|' then + striptrailing = true + sep = '\n' + newlineatend = true + elseif line == '|+' then + striptrailing = false + sep = '\n' + newlineatend = true + elseif line == '|-' then + striptrailing = true + sep = '\n' + newlineatend = false + elseif line == '>' then + striptrailing = true + sep = ' ' + newlineatend = true + elseif line == '>+' then + striptrailing = false + sep = ' ' + newlineatend = true + elseif line == '>-' then + striptrailing = true + sep = ' ' + newlineatend = false + else + error('invalid blockstyle string:'..line) + end + local _, eonl = s[#s]:gsub('\n', '\n') + s[#s] = rtrim(s[#s]) + if striptrailing then + eonl = 0 + end + if newlineatend then + eonl = eonl + 1 + end + for i = endline, 1, -1 do + tremove(lines, i) + end + return tconcat(s, sep)..string.rep('\n', eonl) +end + +local function parsetimestamp(line) + local _, p1, y, m, d = sfind(line, '^(%d%d%d%d)%-(%d%d)%-(%d%d)') + if not p1 then + return nil, line + end + if p1 == #line then + return types.timestamp(y, m, d), '' + end + local _, p2, h, i, s = sfind(line, '^[Tt ](%d+):(%d+):(%d+)', p1+1) + if not p2 then + return types.timestamp(y, m, d), ssub(line, p1+1) + end + if p2 == #line then + return types.timestamp(y, m, d, h, i, s), '' + end + local _, p3, f = sfind(line, '^%.(%d+)', p2+1) + if not p3 then + p3 = p2 + f = 0 + end + local zc = ssub(line, p3+1, p3+1) + local _, p4, zs, z = sfind(line, '^ ?([%+%-])(%d+)', p3+1) + if p4 then + z = tonumber(z) + local _, p5, zi = sfind(line, '^:(%d+)', p4+1) + if p5 then + z = z + tonumber(zi) / 60 + end + z = zs == '-' and -tonumber(z) or tonumber(z) + elseif zc == 'Z' then + p4 = p3 + 1 + z = 0 + else + p4 = p3 + z = false + end + return types.timestamp(y, m, d, h, i, s, f, z), ssub(line, p4+1) +end + +local function parsescalar(line, lines, indent) + line = trim(line) + line = gsub(line, '^%s*#.*$', '') -- comment only -> '' + line = gsub(line, '^%s*', '') -- trim head spaces + + if line == '' or line == '~' then + return null + end + + local ts, _ = parsetimestamp(line) + if ts then + return ts + end + + local s, _ = parsestring(line) + -- startswith quote ... string + -- not startswith quote ... maybe string + if s and (startswith(line, '"') or startswith(line, "'")) then + return s + end + + if startswith('!', line) then -- unexpected tagchar + error('unsupported line: '..line) + end + + if equalsline(line, '{}') then + return {} + end + if equalsline(line, '[]') then + return {} + end + + if startswith(line, '{') or startswith(line, '[') then + return parseflowstyle(line, lines) + end + + if startswith(line, '|') or startswith(line, '>') then + return parseblockstylestring(line, lines, indent) + end + + -- Regular unquoted string + line = gsub(line, '%s*#.*$', '') -- trim tail comment + local v = line + if v == 'null' or v == 'Null' or v == 'NULL'then + return null + elseif v == 'true' or v == 'True' or v == 'TRUE' then + return true + elseif v == 'false' or v == 'False' or v == 'FALSE' then + return false + elseif v == '.inf' or v == '.Inf' or v == '.INF' then + return math.huge + elseif v == '+.inf' or v == '+.Inf' or v == '+.INF' then + return math.huge + elseif v == '-.inf' or v == '-.Inf' or v == '-.INF' then + return -math.huge + elseif v == '.nan' or v == '.NaN' or v == '.NAN' then + return 0 / 0 + elseif sfind(v, '^[%+%-]?[0-9]+$') or sfind(v, '^[%+%-]?[0-9]+%.$')then + return tonumber(v) -- : int + elseif sfind(v, '^[%+%-]?[0-9]+%.[0-9]+$') then + return tonumber(v) + end + return s or v +end + +local parsemap; -- : func + +local function parseseq(line, lines, indent) + local seq = setmetatable({}, types.seq) + if line ~= '' then + error() + end + while #lines > 0 do + -- Check for a new document + line = lines[1] + if startswith(line, '---') then + while #lines > 0 and not startswith(lines, '---') do + tremove(lines, 1) + end + return seq + end + + -- Check the indent level + local level = countindent(line) + if level < indent then + return seq + elseif level > indent then + error("found bad indenting in line: ".. line) + end + + local i, j = sfind(line, '%-%s+') + if not i then + i, j = sfind(line, '%-$') + if not i then + return seq + end + end + local rest = ssub(line, j+1) + + if sfind(rest, '^[^\'\"%s]*:%s*$') or sfind(rest, '^[^\'\"%s]*:%s+.') then + -- Inline nested hash + -- There are two patterns need to match as inline nested hash + -- first one should have no other characters except whitespace after `:` + -- and the second one should have characters besides whitespace after `:` + -- + -- value: + -- - foo: + -- bar: 1 + -- + -- and + -- + -- value: + -- - foo: bar + -- + -- And there is one pattern should not be matched, where there is no space after `:` + -- in below, `foo:bar` should be parsed into a single string + -- + -- value: + -- - foo:bar + local indent2 = j + lines[1] = string.rep(' ', indent2)..rest + tinsert(seq, parsemap('', lines, indent2)) + elseif sfind(rest, '^%-%s+') then + -- Inline nested seq + local indent2 = j + lines[1] = string.rep(' ', indent2)..rest + tinsert(seq, parseseq('', lines, indent2)) + elseif isemptyline(rest) then + tremove(lines, 1) + if #lines == 0 then + tinsert(seq, null) + return seq + end + if sfind(lines[1], '^%s*%-') then + local nextline = lines[1] + local indent2 = countindent(nextline) + if indent2 == indent then + -- Null seqay entry + tinsert(seq, null) + else + tinsert(seq, parseseq('', lines, indent2)) + end + else + -- - # comment + -- key: value + local nextline = lines[1] + local indent2 = countindent(nextline) + tinsert(seq, parsemap('', lines, indent2)) + end + elseif rest then + -- Array entry with a value + tremove(lines, 1) + tinsert(seq, parsescalar(rest, lines)) + end + end + return seq +end + +local function parseset(line, lines, indent) + if not isemptyline(line) then + error('not seq line: '..line) + end + local set = setmetatable({}, types.set) + while #lines > 0 do + -- Check for a new document + line = lines[1] + if startswith(line, '---') then + while #lines > 0 and not startswith(lines, '---') do + tremove(lines, 1) + end + return set + end + + -- Check the indent level + local level = countindent(line) + if level < indent then + return set + elseif level > indent then + error("found bad indenting in line: ".. line) + end + + local i, j = sfind(line, '%?%s+') + if not i then + i, j = sfind(line, '%?$') + if not i then + return set + end + end + local rest = ssub(line, j+1) + + if sfind(rest, '^[^\'\"%s]*:') then + -- Inline nested hash + local indent2 = j + lines[1] = string.rep(' ', indent2)..rest + set[parsemap('', lines, indent2)] = true + elseif sfind(rest, '^%s+$') then + tremove(lines, 1) + if #lines == 0 then + tinsert(set, null) + return set + end + if sfind(lines[1], '^%s*%?') then + local indent2 = countindent(lines[1]) + if indent2 == indent then + -- Null array entry + set[null] = true + else + set[parseseq('', lines, indent2)] = true + end + end + + elseif rest then + tremove(lines, 1) + set[parsescalar(rest, lines)] = true + else + error("failed to classify line: "..line) + end + end + return set +end + +function parsemap(line, lines, indent) + if not isemptyline(line) then + error('not map line: '..line) + end + local map = setmetatable({}, types.map) + while #lines > 0 do + -- Check for a new document + line = lines[1] + if startswith(line, '---') then + while #lines > 0 and not startswith(lines, '---') do + tremove(lines, 1) + end + return map + end + + -- Check the indent level + local level, _ = countindent(line) + if level < indent then + return map + elseif level > indent then + error("found bad indenting in line: ".. line) + end + + -- Find the key + local key + local s, rest = parsestring(line) + + -- Quoted keys + if s and startswith(rest, ':') then + local sc = parsescalar(s, {}, 0) + if sc and type(sc) ~= 'string' then + key = sc + else + key = s + end + line = ssub(rest, 2) + else + error("failed to classify line: "..line) + end + + key = checkdupekey(map, key) + line = ltrim(line) + + if ssub(line, 1, 1) == '!' then + -- ignore type + local rh = ltrim(ssub(line, 3)) + local typename = smatch(rh, '^!?[^%s]+') + line = ltrim(ssub(rh, #typename+1)) + end + + if not isemptyline(line) then + tremove(lines, 1) + line = ltrim(line) + map[key] = parsescalar(line, lines, indent) + else + -- An indent + tremove(lines, 1) + if #lines == 0 then + map[key] = null + return map; + end + if sfind(lines[1], '^%s*%-') then + local indent2 = countindent(lines[1]) + map[key] = parseseq('', lines, indent2) + elseif sfind(lines[1], '^%s*%?') then + local indent2 = countindent(lines[1]) + map[key] = parseset('', lines, indent2) + else + local indent2 = countindent(lines[1]) + if indent >= indent2 then + -- Null hash entry + map[key] = null + else + map[key] = parsemap('', lines, indent2) + end + end + end + end + return map +end + + +-- : (list<str>)->dict +local function parsedocuments(lines) + lines = compactifyemptylines(lines) + + if sfind(lines[1], '^%%YAML') then tremove(lines, 1) end + + local root = {} + local in_document = false + while #lines > 0 do + local line = lines[1] + -- Do we have a document header? + local docright; + if sfind(line, '^%-%-%-') then + -- Handle scalar documents + docright = ssub(line, 4) + tremove(lines, 1) + in_document = true + end + if docright then + if (not sfind(docright, '^%s+$') and + not sfind(docright, '^%s+#')) then + tinsert(root, parsescalar(docright, lines)) + end + elseif #lines == 0 or startswith(line, '---') then + -- A naked document + tinsert(root, null) + while #lines > 0 and not sfind(lines[1], '---') do + tremove(lines, 1) + end + in_document = false + -- XXX The final '-+$' is to look for -- which ends up being an + -- error later. + elseif not in_document and #root > 0 then + -- only the first document can be explicit + error('parse error: '..line) + elseif sfind(line, '^%s*%-') then + -- An array at the root + tinsert(root, parseseq('', lines, 0)) + elseif sfind(line, '^%s*[^%s]') then + -- A hash at the root + local level = countindent(line) + tinsert(root, parsemap('', lines, level)) + else + -- Shouldn't get here. @lines have whitespace-only lines + -- stripped, and previous match is a line with any + -- non-whitespace. So this clause should only be reachable via + -- a perlbug where \s is not symmetric with \S + + -- uncoverable statement + error('parse error: '..line) + end + end + if #root > 1 and Null.isnull(root[1]) then + tremove(root, 1) + return root + end + return root +end + +--- Parse yaml string into table. +local function parse(source) + local lines = {} + for line in string.gmatch(source .. '\n', '(.-)\r?\n') do + tinsert(lines, line) + end + + local docs = parsedocuments(lines) + if #docs == 1 then + return docs[1] + end + + return docs +end + +return { + version = 0.1, + parse = parse, +} diff --git a/Master/texmf-dist/tex/luatex/markdown/markdown.lua b/Master/texmf-dist/tex/luatex/markdown/markdown.lua index 85900563718..eb68cc175d5 100644 --- a/Master/texmf-dist/tex/luatex/markdown/markdown.lua +++ b/Master/texmf-dist/tex/luatex/markdown/markdown.lua @@ -58,12 +58,12 @@ -- those in the standard .ins files. -- local metadata = { - version = "2.10.1", + version = "2.11.0-0-g4505824", comment = "A module for the conversion from markdown to plain TeX", author = "John MacFarlane, Hans Hagen, Vít Novotný", copyright = {"2009-2016 John MacFarlane, Hans Hagen", "2016-2021 Vít Novotný"}, - license = "LPPL 1.3" + license = "LPPL 1.3c" } if not modules then modules = { } end @@ -97,6 +97,7 @@ defaultOptions.headerAttributes = false defaultOptions.html = false defaultOptions.hybrid = false defaultOptions.inlineFootnotes = false +defaultOptions.jekyllData = false defaultOptions.pipeTables = false defaultOptions.preserveTabs = false defaultOptions.shiftHeadings = 0 @@ -105,6 +106,7 @@ defaultOptions.smartEllipses = false defaultOptions.startNumber = true defaultOptions.stripIndent = false defaultOptions.tableCaptions = false +defaultOptions.taskLists = false defaultOptions.texComments = false defaultOptions.tightLists = true defaultOptions.underscores = true @@ -2346,19 +2348,6 @@ function M.writer.new(options) if not self.is_writing then return "" end return "\\markdownRendererHorizontalRule{}" end - local escaped_chars = { - ["{"] = "\\markdownRendererLeftBrace{}", - ["}"] = "\\markdownRendererRightBrace{}", - ["$"] = "\\markdownRendererDollarSign{}", - ["%"] = "\\markdownRendererPercentSign{}", - ["&"] = "\\markdownRendererAmpersand{}", - ["_"] = "\\markdownRendererUnderscore{}", - ["#"] = "\\markdownRendererHash{}", - ["^"] = "\\markdownRendererCircumflex{}", - ["\\"] = "\\markdownRendererBackslash{}", - ["~"] = "\\markdownRendererTilde{}", - ["|"] = "\\markdownRendererPipe{}", - } local escaped_uri_chars = { ["{"] = "\\markdownRendererLeftBrace{}", ["}"] = "\\markdownRendererRightBrace{}", @@ -2369,20 +2358,37 @@ function M.writer.new(options) ["{"] = "\\markdownRendererLeftBrace{}", ["}"] = "\\markdownRendererRightBrace{}", ["%"] = "\\markdownRendererPercentSign{}", - ["#"] = "\\markdownRendererHash{}", ["\\"] = "\\markdownRendererBackslash{}", + ["#"] = "\\markdownRendererHash{}", } local escaped_minimal_strings = { ["^^"] = "\\markdownRendererCircumflex\\markdownRendererCircumflex ", + ["☒"] = "\\markdownRendererTickedBox{}", + ["⌛"] = "\\markdownRendererHalfTickedBox{}", + ["☐"] = "\\markdownRendererUntickedBox{}", + } + local escaped_chars = { + ["{"] = "\\markdownRendererLeftBrace{}", + ["}"] = "\\markdownRendererRightBrace{}", + ["%"] = "\\markdownRendererPercentSign{}", + ["\\"] = "\\markdownRendererBackslash{}", + ["#"] = "\\markdownRendererHash{}", + ["$"] = "\\markdownRendererDollarSign{}", + ["&"] = "\\markdownRendererAmpersand{}", + ["_"] = "\\markdownRendererUnderscore{}", + ["^"] = "\\markdownRendererCircumflex{}", + ["~"] = "\\markdownRendererTilde{}", + ["|"] = "\\markdownRendererPipe{}", } - local escape = util.escaper(escaped_chars) + local escape = util.escaper(escaped_chars, escaped_minimal_strings) local escape_citation = util.escaper(escaped_citation_chars, escaped_minimal_strings) local escape_uri = util.escaper(escaped_uri_chars, escaped_minimal_strings) + local escape_minimal = util.escaper({}, escaped_minimal_strings) if options.hybrid then - self.string = function(s) return s end - self.citation = function(c) return c end - self.uri = function(u) return u end + self.string = escape_minimal + self.citation = escape_minimal + self.uri = escape_minimal else self.string = escape self.citation = escape_citation @@ -2556,6 +2562,15 @@ function M.writer.new(options) function self.emphasis(s) return {"\\markdownRendererEmphasis{",s,"}"} end + function self.tickbox(f) + if f == 1.0 then + return "☒ " + elseif f == 0.0 then + return "☐ " + else + return "⌛ " + end + end function self.strong(s) return {"\\markdownRendererStrongEmphasis{",s,"}"} end @@ -2576,6 +2591,88 @@ function M.writer.new(options) local name = util.cache(options.cacheDir, s, nil, nil, ".verbatim") return {"\\markdownRendererInputFencedCode{",name,"}{",i,"}"} end + function self.jekyllData(d, t, p) + if not self.is_writing then return "" end + + local buf = {} + + local keys = {} + for k, _ in pairs(d) do + table.insert(keys, k) + end + table.sort(keys) + + if not p then + table.insert(buf, "\\markdownRendererJekyllDataBegin") + end + + if #d > 0 then + table.insert(buf, "\\markdownRendererJekyllDataSequenceBegin{") + table.insert(buf, self.uri(p or "null")) + table.insert(buf, "}{") + table.insert(buf, #keys) + table.insert(buf, "}") + else + table.insert(buf, "\\markdownRendererJekyllDataMappingBegin{") + table.insert(buf, self.uri(p or "null")) + table.insert(buf, "}{") + table.insert(buf, #keys) + table.insert(buf, "}") + end + + for _, k in ipairs(keys) do + local v = d[k] + local typ = type(v) + k = tostring(k or "null") + if typ == "table" and next(v) ~= nil then + table.insert( + buf, + self.jekyllData(v, t, k) + ) + else + k = self.uri(k) + v = tostring(v) + if typ == "boolean" then + table.insert(buf, "\\markdownRendererJekyllDataBoolean{") + table.insert(buf, k) + table.insert(buf, "}{") + table.insert(buf, v) + table.insert(buf, "}") + elseif typ == "number" then + table.insert(buf, "\\markdownRendererJekyllDataNumber{") + table.insert(buf, k) + table.insert(buf, "}{") + table.insert(buf, v) + table.insert(buf, "}") + elseif typ == "string" then + table.insert(buf, "\\markdownRendererJekyllDataString{") + table.insert(buf, k) + table.insert(buf, "}{") + table.insert(buf, t(v)) + table.insert(buf, "}") + elseif typ == "table" then + table.insert(buf, "\\markdownRendererJekyllDataEmpty{") + table.insert(buf, k) + table.insert(buf, "}") + else + error(format("Unexpected type %s for value of " .. + "YAML key %s", typ, k)) + end + end + end + + if #d > 0 then + table.insert(buf, "\\markdownRendererJekyllDataSequenceEnd") + else + table.insert(buf, "\\markdownRendererJekyllDataMappingEnd") + end + + if not p then + table.insert(buf, "\\markdownRendererJekyllDataEnd") + end + + return buf + end self.active_headings = {} function self.heading(s,level,attributes) local active_headings = self.active_headings @@ -2660,16 +2757,16 @@ function M.writer.new(options) } end function self.set_state(s) - previous_state = self.get_state() - for key, value in pairs(state) do + local previous_state = self.get_state() + for key, value in pairs(s) do self[key] = value end return previous_state end function self.defer_call(f) - state = self.get_state() + local previous_state = self.get_state() return function(...) - state = self.set_state(state) + local state = self.set_state(previous_state) local return_value = f(...) self.set_state(state) return return_value @@ -2764,7 +2861,6 @@ parsers.spnl = parsers.optionalspace * (parsers.newline * parsers.optionalspace)^-1 parsers.line = parsers.linechar^0 * parsers.newline parsers.nonemptyline = parsers.line - parsers.blankline - parsers.commented_line_letter = parsers.linechar + parsers.newline - parsers.backslash @@ -2803,7 +2899,9 @@ parsers.commented_line = Cg(Cc(""), "backslashes") + parsers.percent -- comment * parsers.line * parsers.optionalspace) -- leading tabs and spaces - + C(parsers.newline)) + + #(parsers.newline) + * Cb("backslashes") + * C(parsers.newline)) parsers.chunk = parsers.line * (parsers.optionallyindentedline - parsers.blankline)^0 @@ -2843,6 +2941,16 @@ parsers.bullet = ( parsers.bulletchar * #parsers.spacing + parsers.space * parsers.space * parsers.space * parsers.bulletchar * #parsers.spacing ) + +local function tickbox(interior) + return parsers.optionalspace * parsers.lbracket + * interior * parsers.rbracket * parsers.spacechar^1 +end + +parsers.ticked_box = tickbox(S("xX")) * Cc(1.0) +parsers.halfticked_box = tickbox(S("./")) * Cc(0.5) +parsers.unticked_box = tickbox(parsers.spacechar^1) * Cc(0.0) + parsers.openticks = Cg(parsers.backtick^1, "ticks") local function captures_equal_length(s,i,a,b) @@ -3309,6 +3417,11 @@ parsers.BacktickFencedCode * Cs(parsers.fencedline(parsers.backtick)^0) * parsers.fencetail(parsers.backtick) +parsers.JekyllFencedCode + = parsers.fencehead(parsers.dash) + * Cs(parsers.fencedline(parsers.dash)^0) + * parsers.fencetail(parsers.dash) + parsers.lineof = function(c) return (parsers.leader * (P(c) * parsers.optionalspace)^3 * (parsers.newline * parsers.blankline^1 @@ -3797,6 +3910,19 @@ larsers.PipeTable = Ct(larsers.table_row * parsers.newline expandtabs(code)) end + larsers.JekyllData = P("---") + * parsers.blankline / 0 + * #(-parsers.blankline) -- if followed by blank, it's an hrule + * C((parsers.line - P("---") - P("..."))^0) + * (P("---") + P("...")) + / function(text) + local tinyyaml = require("markdown-tinyyaml") + data = tinyyaml.parse(text) + return writer.jekyllData(data, function(s) + return parse_blocks(s) + end, nil) + end + larsers.Blockquote = Cs(larsers.blockquote_body^1) / parse_blocks_toplevel / writer.blockquote @@ -3830,6 +3956,15 @@ larsers.PipeTable = Ct(larsers.table_row * parsers.newline / writer.plain larsers.starter = parsers.bullet + larsers.enumerator + if options.taskLists then + larsers.tickbox = ( parsers.ticked_box + + parsers.halfticked_box + + parsers.unticked_box + ) / writer.tickbox + else + larsers.tickbox = parsers.fail + end + -- we use \001 as a separator between a tight list item and a -- nested list under it. larsers.NestedList = Cs((parsers.optionallyindentedline @@ -3847,14 +3982,14 @@ larsers.PipeTable = Ct(larsers.table_row * parsers.newline larsers.TightListItem = function(starter) return -larsers.HorizontalRule - * (Cs(starter / "" * larsers.ListBlock * larsers.NestedList^-1) + * (Cs(starter / "" * larsers.tickbox^-1 * larsers.ListBlock * larsers.NestedList^-1) / parse_blocks) * -(parsers.blanklines * parsers.indent) end larsers.LooseListItem = function(starter) return -larsers.HorizontalRule - * Cs( starter / "" * larsers.ListBlock * Cc("\n") + * Cs( starter / "" * larsers.tickbox^-1 * larsers.ListBlock * Cc("\n") * (larsers.NestedList + larsers.ListContinuationBlock^0) * (parsers.blanklines / "\n\n") ) / parse_blocks @@ -3981,7 +4116,10 @@ larsers.PipeTable = Ct(larsers.table_row * parsers.newline Blank = larsers.Blank, + JekyllData = larsers.JekyllData, + Block = V("ContentBlock") + + V("JekyllData") + V("Blockquote") + V("PipeTable") + V("Verbatim") @@ -4105,8 +4243,8 @@ larsers.PipeTable = Ct(larsers.table_row * parsers.newline syntax.InlineNote = parsers.fail end - if not options.smartEllipses then - syntax.Smart = parsers.fail + if not options.jekyllData then + syntax.JekyllData = parsers.fail end if options.preserveTabs then @@ -4117,6 +4255,10 @@ larsers.PipeTable = Ct(larsers.table_row * parsers.newline syntax.PipeTable = parsers.fail end + if not options.smartEllipses then + syntax.Smart = parsers.fail + end + local blocks_toplevel_t = util.table_copy(syntax) blocks_toplevel_t.Paragraph = larsers.ToplevelParagraph larsers.blocks_toplevel = Ct(blocks_toplevel_t) |