diff options
Diffstat (limited to 'luax/libs')
| -rw-r--r-- | luax/libs/markdown.lua | 1281 |
1 files changed, 0 insertions, 1281 deletions
diff --git a/luax/libs/markdown.lua b/luax/libs/markdown.lua deleted file mode 100644 index 3fe90c1..0000000 --- a/luax/libs/markdown.lua +++ /dev/null @@ -1,1281 +0,0 @@ -#!/usr/bin/env lua - ----------------------------------------------------------------------- --- Utility functions ----------------------------------------------------------------------- - -local unpack = table.unpack or unpack - --- Returns the result of mapping the values in table t through the function f -local function map(t, f) - local out = {} - for k,v in pairs(t) do out[k] = f(v,k) end - return out -end - --- Functional style if statement. (NOTE: no short circuit evaluation) -local function iff(t, a, b) if t then return a else return b end end - --- Splits the text into an array of separate lines. -local function split(text, sep) - sep = sep or "\n" - local lines = {} - local pos = 1 - while true do - local b,e = text:find(sep, pos) - if not b then table.insert(lines, text:sub(pos)) break end - table.insert(lines, text:sub(pos, b-1)) - pos = e + 1 - end - return lines -end - --- Converts tabs to spaces -local function detab(text) - local tab_width = 4 - local function rep(match) - local spaces = -match:len() - while spaces<1 do spaces = spaces + tab_width end - return match .. string.rep(" ", spaces) - end - text = text:gsub("([^\n]-)\t", rep) - return text -end - --- Applies string.find for every pattern in the list and returns the first match -local function find_first(s, patterns, index) - local res = {} - for _,p in ipairs(patterns) do - local match = {s:find(p, index)} - if #match>0 and (#res==0 or match[1] < res[1]) then res = match end - end - return unpack(res) -end - --- If a replacement array is specified, the range [start, stop] in the array is replaced --- with the replacement array and the resulting array is returned. Without a replacement --- array the section of the array between start and stop is returned. -local function splice(array, start, stop, replacement) - if replacement then - local n = stop - start + 1 - while n > 0 do - table.remove(array, start) - n = n - 1 - end - for _,v in ipairs(replacement) do - table.insert(array, start, v) - end - return array - else - local res = {} - for i = start,stop do - table.insert(res, array[i]) - end - return res - end -end - --- Outdents the text one step. -local function outdent(text) - text = "\n" .. text - text = text:gsub("\n ? ? ?", "\n") - text = text:sub(2) - return text -end - --- Indents the text one step. -local function indent(text) - text = text:gsub("\n", "\n ") - return text -end - --- Does a simple tokenization of html data. Returns the data as a list of tokens. --- Each token is a table with a type field (which is either "tag" or "text") and --- a text field (which contains the original token data). -local function tokenize_html(html) - local tokens = {} - local pos = 1 - while true do - local start = find_first(html, {"<!%-%-", "<[a-z/!$]", "<%?"}, pos) - if not start then - table.insert(tokens, {type="text", text=html:sub(pos)}) - break - end - if start ~= pos then table.insert(tokens, {type="text", text = html:sub(pos, start-1)}) end - - local _, stop - if html:match("^<!%-%-", start) then - _,stop = html:find("%-%->", start) - elseif html:match("^<%?", start) then - _,stop = html:find("?>", start) - else - _,stop = html:find("%b<>", start) - end - if not stop then - -- error("Could not match html tag " .. html:sub(start,start+30)) - table.insert(tokens, {type="text", text=html:sub(start, start)}) - pos = start + 1 - else - table.insert(tokens, {type="tag", text=html:sub(start, stop)}) - pos = stop + 1 - end - end - return tokens -end - ----------------------------------------------------------------------- --- Hash ----------------------------------------------------------------------- - --- This is used to "hash" data into alphanumeric strings that are unique --- in the document. (Note that this is not cryptographic hash, the hash --- function is not one-way.) The hash procedure is used to protect parts --- of the document from further processing. - -local HASH = { - -- Has the hash been inited. - inited = false, - - -- The unique string prepended to all hash values. This is to ensure - -- that hash values do not accidently coincide with an actual existing - -- string in the document. - identifier = "", - - -- Counter that counts up for each new hash instance. - counter = 0, - - -- Hash table. - table = {} -} - --- Inits hashing. Creates a hash_identifier that doesn't occur anywhere --- in the text. -local function init_hash(text) - HASH.inited = true - HASH.identifier = "" - HASH.counter = 0 - HASH.table = {} - - local s = "HASH" - local counter = 0 - local id - while true do - id = s .. counter - if not text:find(id, 1, true) then break end - counter = counter + 1 - end - HASH.identifier = id -end - --- Returns the hashed value for s. -local function hash(s) - assert(HASH.inited) - if not HASH.table[s] then - HASH.counter = HASH.counter + 1 - local id = HASH.identifier .. HASH.counter .. "X" - HASH.table[s] = id - end - return HASH.table[s] -end - ----------------------------------------------------------------------- --- Protection ----------------------------------------------------------------------- - --- The protection module is used to "protect" parts of a document --- so that they are not modified by subsequent processing steps. --- Protected parts are saved in a table for later unprotection - --- Protection data -local PD = { - -- Saved blocks that have been converted - blocks = {}, - - -- Block level tags that will be protected - tags = {"p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", - "pre", "table", "dl", "ol", "ul", "script", "noscript", "form", "fieldset", - "iframe", "math", "ins", "del"} -} - --- Pattern for matching a block tag that begins and ends in the leftmost --- column and may contain indented subtags, i.e. --- <div> --- A nested block. --- <div> --- Nested data. --- </div> --- </div> -local function block_pattern(tag) - return "\n<" .. tag .. ".-\n</" .. tag .. ">[ \t]*\n" -end - --- Pattern for matching a block tag that begins and ends with a newline -local function line_pattern(tag) - return "\n<" .. tag .. ".-</" .. tag .. ">[ \t]*\n" -end - --- Protects the range of characters from start to stop in the text and --- returns the protected string. -local function protect_range(text, start, stop) - local s = text:sub(start, stop) - local h = hash(s) - PD.blocks[h] = s - text = text:sub(1,start) .. h .. text:sub(stop) - return text -end - --- Protect every part of the text that matches any of the patterns. The first --- matching pattern is protected first, etc. -local function protect_matches(text, patterns) - while true do - local start, stop = find_first(text, patterns) - if not start then break end - text = protect_range(text, start, stop) - end - return text -end - --- Protects blocklevel tags in the specified text -local function protect(text) - -- First protect potentially nested block tags - text = protect_matches(text, map(PD.tags, block_pattern)) - -- Then protect block tags at the line level. - text = protect_matches(text, map(PD.tags, line_pattern)) - -- Protect <hr> and comment tags - text = protect_matches(text, {"\n<hr[^>]->[ \t]*\n"}) - text = protect_matches(text, {"\n<!%-%-.-%-%->[ \t]*\n"}) - return text -end - --- Returns true if the string s is a hash resulting from protection -local function is_protected(s) - return PD.blocks[s] -end - --- Unprotects the specified text by expanding all the nonces -local function unprotect(text) - for k,v in pairs(PD.blocks) do - v = v:gsub("%%", "%%%%") - text = text:gsub(k, v) - end - return text -end - - -local code_block_ranges = {} - ----------------------------------------------------------------------- --- Block transform ----------------------------------------------------------------------- - --- The block transform functions transform the text on the block level. --- They work with the text as an array of lines rather than as individual --- characters. - --- Returns true if the line is a ruler of (char) characters. --- The line must contain at least three char characters and contain only spaces and --- char characters. -local function is_ruler_of(line, char) - if not line:match("^[ %" .. char .. "]*$") then return false end - if not line:match("%" .. char .. ".*%" .. char .. ".*%" .. char) then return false end - return true -end - --- Identifies the block level formatting present in the line -local function classify(line) - local info = {line = line, text = line} - - if line:match("^ ") then - info.type = "indented" - info.outdented = line:sub(5) - return info - end - - for _,c in ipairs({'*', '-', '_', '='}) do - if is_ruler_of(line, c) then - info.type = "ruler" - info.ruler_char = c - return info - end - end - - if line == "" then - info.type = "blank" - return info - end - - if line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") then - local m1, m2 = line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") - info.type = "header" - info.level = m1:len() - info.text = m2 - return info - end - - if line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") then - local number, text = line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") - info.type = "list_item" - info.list_type = "numeric" - info.number = 0 + number - info.text = text - return info - end - - if line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") then - local bullet, text = line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") - info.type = "list_item" - info.list_type = "bullet" - info.bullet = bullet - info.text= text - return info - end - - if line:match("^>[ \t]?(.*)") then - info.type = "blockquote" - info.text = line:match("^>[ \t]?(.*)") - return info - end - - if is_protected(line) then - info.type = "raw" - info.html = unprotect(line) - return info - end - - info.type = "normal" - return info -end - --- Find headers constisting of a normal line followed by a ruler and converts them to --- header entries. -local function headers(array) - local i = 1 - while i <= #array - 1 do - if array[i].type == "normal" and array[i+1].type == "ruler" and - (array[i+1].ruler_char == "-" or array[i+1].ruler_char == "=") then - local info = {line = array[i].line} - info.text = info.line - info.type = "header" - info.level = iff(array[i+1].ruler_char == "=", 1, 2) - table.remove(array, i+1) - array[i] = info - end - i = i + 1 - end - return array -end - --- Forward declarations -local block_transform, span_transform, encode_code - --- Convert lines to html code -local function blocks_to_html(lines, no_paragraphs) - local out = {} - local i = 1 - while i <= #lines do - local line = lines[i] - if line.type == "ruler" then - table.insert(out, "<hr/>") - elseif line.type == "raw" then - table.insert(out, line.html) - elseif line.type == "normal" then - local s = line.line - - while i+1 <= #lines and lines[i+1].type == "normal" do - i = i + 1 - s = s .. "\n" .. lines[i].line - end - - if no_paragraphs then - table.insert(out, span_transform(s)) - else - table.insert(out, "<p>" .. span_transform(s) .. "</p>") - end - elseif line.type == "header" then - local s = "<h" .. line.level .. ">" .. span_transform(line.text) .. "</h" .. line.level .. ">" - table.insert(out, s) - else - table.insert(out, line.line) - end - i = i + 1 - end - return out -end - --- Find list blocks and convert them to protected data blocks -local function lists(array, sublist) - local function process_list(arr) - local function any_blanks(arr) - for i = 1, #arr do - if arr[i].type == "blank" then return true end - end - return false - end - - local function split_list_items(arr) - local acc = {arr[1]} - local res = {} - for i=2,#arr do - if arr[i].type == "list_item" then - table.insert(res, acc) - acc = {arr[i]} - else - table.insert(acc, arr[i]) - end - end - table.insert(res, acc) - return res - end - - local function process_list_item(lines, block) - while lines[#lines].type == "blank" do - table.remove(lines) - end - - local itemtext = lines[1].text - for i=2,#lines do - itemtext = itemtext .. "\n" .. outdent(lines[i].line) - end - if block then - itemtext = block_transform(itemtext, true) - if not itemtext:find("<pre>") then itemtext = indent(itemtext) end - return " <li>" .. itemtext .. "</li>" - else - local lines = split(itemtext) - lines = map(lines, classify) - lines = lists(lines, true) - lines = blocks_to_html(lines, true) - itemtext = table.concat(lines, "\n") - if not itemtext:find("<pre>") then itemtext = indent(itemtext) end - return " <li>" .. itemtext .. "</li>" - end - end - - local block_list = any_blanks(arr) - local items = split_list_items(arr) - local out = "" - for _, item in ipairs(items) do - out = out .. process_list_item(item, block_list) .. "\n" - end - if arr[1].list_type == "numeric" then - return "<ol>\n" .. out .. "</ol>" - else - return "<ul>\n" .. out .. "</ul>" - end - end - - -- Finds the range of lines composing the first list in the array. A list - -- starts with (^ list_item) or (blank list_item) and ends with - -- (blank* $) or (blank normal). - -- - -- A sublist can start with just (list_item) does not need a blank... - local function find_list(array, sublist) - local function find_list_start(array, sublist) - if array[1].type == "list_item" then return 1 end - if sublist then - for i = 1,#array do - if array[i].type == "list_item" then return i end - end - else - for i = 1, #array-1 do - if array[i].type == "blank" and array[i+1].type == "list_item" then - return i+1 - end - end - end - return nil - end - local function find_list_end(array, start) - local pos = #array - for i = start, #array-1 do - if array[i].type == "blank" and array[i+1].type ~= "list_item" - and array[i+1].type ~= "indented" and array[i+1].type ~= "blank" then - pos = i-1 - break - end - end - while pos > start and array[pos].type == "blank" do - pos = pos - 1 - end - return pos - end - - local start = find_list_start(array, sublist) - if not start then return nil end - return start, find_list_end(array, start) - end - - while true do - local start, stop = find_list(array, sublist) - if not start then break end - local text = process_list(splice(array, start, stop)) - local info = { - line = text, - type = "raw", - html = text - } - array = splice(array, start, stop, {info}) - end - - -- Convert any remaining list items to normal - for _,line in ipairs(array) do - if line.type == "list_item" then line.type = "normal" end - end - - return array -end - --- Find and convert blockquote markers. -local function blockquotes(lines) - local function find_blockquote(lines) - local start - for i,line in ipairs(lines) do - if line.type == "blockquote" then - start = i - break - end - end - if not start then return nil end - - local stop = #lines - for i = start+1, #lines do - if lines[i].type == "blank" or lines[i].type == "blockquote" then - elseif lines[i].type == "normal" then - if lines[i-1].type == "blank" then stop = i-1 break end - else - stop = i-1 break - end - end - while lines[stop].type == "blank" do stop = stop - 1 end - return start, stop - end - - local function process_blockquote(lines) - local raw = lines[1].text - for i = 2,#lines do - raw = raw .. "\n" .. lines[i].text - end - local bt = block_transform(raw) - if not bt:find("<pre>") then bt = indent(bt) end - return "<blockquote>\n " .. bt .. - "\n</blockquote>" - end - - while true do - local start, stop = find_blockquote(lines) - if not start then break end - local text = process_blockquote(splice(lines, start, stop)) - local info = { - line = text, - type = "raw", - html = text - } - lines = splice(lines, start, stop, {info}) - end - return lines -end - --- Find and convert codeblocks. -local function codeblocks(lines) - local function find_codeblock(lines) - local start - for i,line in ipairs(lines) do - if line.type == "indented" then start = i break end - end - if not start then return nil end - - local stop = #lines - for i = start+1, #lines do - if lines[i].type ~= "indented" and lines[i].type ~= "blank" then - stop = i-1 - break - end - end - while lines[stop].type == "blank" do stop = stop - 1 end - return start, stop - end - - local function process_codeblock(lines) - local raw = detab(encode_code(outdent(lines[1].line))) - for i = 2,#lines do - raw = raw .. "\n" .. detab(encode_code(outdent(lines[i].line))) - end - return "<pre><code>" .. raw .. "\n</code></pre>" - end - - while true do - local start, stop = find_codeblock(lines) - if not start then break end - local text = process_codeblock(splice(lines, start, stop)) - local info = { - line = text, - type = "raw", - html = text - } - lines = splice(lines, start, stop, {info}) - end - return lines -end - --- Perform all the block level transforms -function block_transform(text, sublist) - local lines = split(text) - lines = map(lines, classify) - lines = headers(lines) - lines = lists(lines, sublist) - lines = codeblocks(lines) - lines = blockquotes(lines) - lines = blocks_to_html(lines) - local text = table.concat(lines, "\n") - return text -end - ----------------------------------------------------------------------- --- Span transform ----------------------------------------------------------------------- - --- Functions for transforming the text at the span level. - --- These characters may need to be escaped because they have a special --- meaning in markdown. -local escape_chars = "'\\`*_{}[]()>#+-.!'" -local escape_table = {} - -local function init_escape_table() - escape_table = {} - for i = 1,#escape_chars do - local c = escape_chars:sub(i,i) - escape_table[c] = hash(c) - end -end - --- Adds a new escape to the escape table. -local function add_escape(text) - if not escape_table[text] then - escape_table[text] = hash(text) - end - return escape_table[text] -end - --- Encode backspace-escaped characters in the markdown source. -local function encode_backslash_escapes(t) - for i=1,escape_chars:len() do - local c = escape_chars:sub(i,i) - t = t:gsub("\\%" .. c, escape_table[c]) - end - return t -end - --- Escape characters that should not be disturbed by markdown. -local function escape_special_chars(text) - local tokens = tokenize_html(text) - - local out = "" - for _, token in ipairs(tokens) do - local t = token.text - if token.type == "tag" then - -- In tags, encode * and _ so they don't conflict with their use in markdown. - t = t:gsub("%*", escape_table["*"]) - t = t:gsub("%_", escape_table["_"]) - else - t = encode_backslash_escapes(t) - end - out = out .. t - end - return out -end - --- Unescape characters that have been encoded. -local function unescape_special_chars(t) - local tin = t - for k,v in pairs(escape_table) do - k = k:gsub("%%", "%%%%") - t = t:gsub(v,k) - end - if t ~= tin then t = unescape_special_chars(t) end - return t -end - --- Encode/escape certain characters inside Markdown code runs. --- The point is that in code, these characters are literals, --- and lose their special Markdown meanings. -function encode_code(s) - s = s:gsub("%&", "&") - s = s:gsub("<", "<") - s = s:gsub(">", ">") - for k,v in pairs(escape_table) do - s = s:gsub("%"..k, v) - end - return s -end - --- Handle backtick blocks. -local function code_spans(s) - s = s:gsub("\\\\", escape_table["\\"]) - s = s:gsub("\\`", escape_table["`"]) - - local pos = 1 - while true do - local start, stop = s:find("`+", pos) - if not start then return s end - local count = stop - start + 1 - -- Find a matching numbert of backticks - local estart, estop = s:find(string.rep("`", count), stop+1) - local brstart = s:find("\n", stop+1) - if estart and (not brstart or estart < brstart) then - local code = s:sub(stop+1, estart-1) - code = code:gsub("^[ \t]+", "") - code = code:gsub("[ \t]+$", "") - code = code:gsub(escape_table["\\"], escape_table["\\"] .. escape_table["\\"]) - code = code:gsub(escape_table["`"], escape_table["\\"] .. escape_table["`"]) - code = "<code>" .. encode_code(code) .. "</code>" - code = add_escape(code) - s = s:sub(1, start-1) .. code .. s:sub(estop+1) - pos = start + code:len() - else - pos = stop + 1 - end - end - return s -end - --- Encode alt text... enodes &, and ". -local function encode_alt(s) - if not s then return s end - s = s:gsub('&', '&') - s = s:gsub('"', '"') - s = s:gsub('<', '<') - return s -end - --- Forward declaration for link_db as returned by strip_link_definitions. -local link_database - --- Handle image references -local function images(text) - local function reference_link(alt, id) - alt = encode_alt(alt:match("%b[]"):sub(2,-2)) - id = id:match("%[(.*)%]"):lower() - if id == "" then id = text:lower() end - link_database[id] = link_database[id] or {} - if not link_database[id].url then return nil end - local url = link_database[id].url or id - url = encode_alt(url) - local title = encode_alt(link_database[id].title) - if title then title = " title=\"" .. title .. "\"" else title = "" end - return add_escape ('<img src="' .. url .. '" alt="' .. alt .. '"' .. title .. "/>") - end - - local function inline_link(alt, link) - alt = encode_alt(alt:match("%b[]"):sub(2,-2)) - local url, title = link:match("%(<?(.-)>?[ \t]*['\"](.+)['\"]") - url = url or link:match("%(<?(.-)>?%)") - url = encode_alt(url) - title = encode_alt(title) - if title then - return add_escape('<img src="' .. url .. '" alt="' .. alt .. '" title="' .. title .. '"/>') - else - return add_escape('<img src="' .. url .. '" alt="' .. alt .. '"/>') - end - end - - text = text:gsub("!(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) - text = text:gsub("!(%b[])(%b())", inline_link) - return text -end - --- Handle anchor references -local function anchors(text) - local function reference_link(text, id) - text = text:match("%b[]"):sub(2,-2) - id = id:match("%b[]"):sub(2,-2):lower() - if id == "" then id = text:lower() end - link_database[id] = link_database[id] or {} - if not link_database[id].url then return nil end - local url = link_database[id].url or id - url = encode_alt(url) - local title = encode_alt(link_database[id].title) - if title then title = " title=\"" .. title .. "\"" else title = "" end - return add_escape("<a href=\"" .. url .. "\"" .. title .. ">") .. text .. add_escape("</a>") - end - - local function inline_link(text, link) - text = text:match("%b[]"):sub(2,-2) - local url, title = link:match("%(<?(.-)>?[ \t]*['\"](.+)['\"]") - title = encode_alt(title) - url = url or link:match("%(<?(.-)>?%)") or "" - url = encode_alt(url) - if title then - return add_escape("<a href=\"" .. url .. "\" title=\"" .. title .. "\">") .. text .. "</a>" - else - return add_escape("<a href=\"" .. url .. "\">") .. text .. add_escape("</a>") - end - end - - text = text:gsub("(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) - text = text:gsub("(%b[])(%b())", inline_link) - return text -end - --- Handle auto links, i.e. <http://www.google.com/>. -local function auto_links(text) - local function link(s) - return add_escape("<a href=\"" .. s .. "\">") .. s .. "</a>" - end - -- Encode chars as a mix of dec and hex entitites to (perhaps) fool - -- spambots. - local function encode_email_address(s) - -- Use a deterministic encoding to make unit testing possible. - -- Code 45% hex, 45% dec, 10% plain. - local hex = {code = function(c) return "&#x" .. string.format("%x", c:byte()) .. ";" end, count = 1, rate = 0.45} - local dec = {code = function(c) return "&#" .. c:byte() .. ";" end, count = 0, rate = 0.45} - local plain = {code = function(c) return c end, count = 0, rate = 0.1} - local codes = {hex, dec, plain} - local function swap(t,k1,k2) local temp = t[k2] t[k2] = t[k1] t[k1] = temp end - - local out = "" - for i = 1,s:len() do - for _,code in ipairs(codes) do code.count = code.count + code.rate end - if codes[1].count < codes[2].count then swap(codes,1,2) end - if codes[2].count < codes[3].count then swap(codes,2,3) end - if codes[1].count < codes[2].count then swap(codes,1,2) end - - local code = codes[1] - local c = s:sub(i,i) - -- Force encoding of "@" to make email address more invisible. - if c == "@" and code == plain then code = codes[2] end - out = out .. code.code(c) - code.count = code.count - 1 - end - return out - end - local function mail(s) - s = unescape_special_chars(s) - local address = encode_email_address("mailto:" .. s) - local text = encode_email_address(s) - return add_escape("<a href=\"" .. address .. "\">") .. text .. "</a>" - end - -- links - text = text:gsub("<(https?:[^'\">%s]+)>", link) - text = text:gsub("<(ftp:[^'\">%s]+)>", link) - - -- mail - text = text:gsub("<mailto:([^'\">%s]+)>", mail) - text = text:gsub("<([-.%w]+%@[-.%w]+)>", mail) - return text -end - --- Encode free standing amps (&) and angles (<)... note that this does not --- encode free >. -local function amps_and_angles(s) - -- encode amps not part of &..; expression - local pos = 1 - while true do - local amp = s:find("&", pos) - if not amp then break end - local semi = s:find(";", amp+1) - local stop = s:find("[ \t\n&]", amp+1) - if not semi or (stop and stop < semi) or (semi - amp) > 15 then - s = s:sub(1,amp-1) .. "&" .. s:sub(amp+1) - pos = amp+1 - else - pos = amp+1 - end - end - - -- encode naked <'s - s = s:gsub("<([^a-zA-Z/?$!])", "<%1") - s = s:gsub("<$", "<") - - -- what about >, nothing done in the original markdown source to handle them - return s -end - --- Handles emphasis markers (* and _) in the text. -local function emphasis(text) - for _, s in ipairs {"%*%*", "%_%_"} do - text = text:gsub(s .. "([^%s][%*%_]?)" .. s, "<strong>%1</strong>") - text = text:gsub(s .. "([^%s][^<>]-[^%s][%*%_]?)" .. s, "<strong>%1</strong>") - end - for _, s in ipairs {"%*", "%_"} do - text = text:gsub(s .. "([^%s_])" .. s, "<em>%1</em>") - text = text:gsub(s .. "(<strong>[^%s_]</strong>)" .. s, "<em>%1</em>") - text = text:gsub(s .. "([^%s_][^<>_]-[^%s_])" .. s, "<em>%1</em>") - text = text:gsub(s .. "([^<>_]-<strong>[^<>_]-</strong>[^<>_]-)" .. s, "<em>%1</em>") - end - return text -end - --- Handles line break markers in the text. -local function line_breaks(text) - return text:gsub(" +\n", " <br/>\n") -end - --- Perform all span level transforms. -function span_transform(text) - text = code_spans(text) - text = escape_special_chars(text) - text = images(text) - text = anchors(text) - text = auto_links(text) - text = amps_and_angles(text) - text = emphasis(text) - text = line_breaks(text) - return text -end - ----------------------------------------------------------------------- --- Markdown ----------------------------------------------------------------------- - --- Cleanup the text by normalizing some possible variations to make further --- processing easier. -local function cleanup(text) - -- Standardize line endings - text = text:gsub("\r\n", "\n") -- DOS to UNIX - text = text:gsub("\r", "\n") -- Mac to UNIX - - -- Convert all tabs to spaces - text = detab(text) - - -- Strip lines with only spaces and tabs - while true do - local subs - text, subs = text:gsub("\n[ \t]+\n", "\n\n") - if subs == 0 then break end - end - - return "\n" .. text .. "\n" -end - --- Strips link definitions from the text and stores the data in a lookup table. -local function strip_link_definitions(text) - local linkdb = {} - - local function link_def(id, url, title) - id = id:match("%[(.+)%]"):lower() - linkdb[id] = linkdb[id] or {} - linkdb[id].url = url or linkdb[id].url - linkdb[id].title = title or linkdb[id].title - return "" - end - - local def_no_title = "\n ? ? ?(%b[]):[ \t]*\n?[ \t]*<?([^%s>]+)>?[ \t]*" - local def_title1 = def_no_title .. "[ \t]+\n?[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" - local def_title2 = def_no_title .. "[ \t]*\n[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" - local def_title3 = def_no_title .. "[ \t]*\n?[ \t]+[\"'(]([^\n]+)[\"')][ \t]*" - - text = text:gsub(def_title1, link_def) - text = text:gsub(def_title2, link_def) - text = text:gsub(def_title3, link_def) - text = text:gsub(def_no_title, link_def) - return text, linkdb -end - -local lang_to_ext = { - rust = "rs", - lua = "lua", - python = "py", - javascript = "js", - luax = "luax", - cpp = "cpp", - c = "c", - java = "java", - go = "go", - html = "html", - css = "css", - bash = "bash", - sh = "sh", - json = "json", - toml = "toml", - c_sharp = "cs", -} - -local function highlight(lang, code) - -- create tmp file with code - local tmp_filename = os.tmpname() - local ext = lang_to_ext[lang] - if not ext then - ext = lang - end - -- add matching extension to tmp_filename (just lang for now) - tmp_filename = tmp_filename .. "." .. ext - local tmp_file = io.open(tmp_filename, "w") - if not tmp_file then - error("Could not open tmp file: " .. tmp_filename) - end - tmp_file:write(code) - tmp_file:close() - -- run tree-sitter highlight on the code with lang - local tree_sitter_output = io.popen("/usr/bin/env tree-sitter highlight -H " .. tmp_filename) - if not tree_sitter_output then - error("Could not run tree-sitter highlight") - end - - return tree_sitter_output:read("*a") -end - -local function code_block_highlight(text) - -- find ``` blocks - local pos = 1 - while true do - local start, stop = text:find("```", pos) - if not start then break end - local endstart, endstop = text:find("```", stop+1) - if not endstart then break end - - local block = text:sub(stop+2, endstart-1) - -- treat first line after ``` as language - local lang = block:match("^(.-)\n") - -- remove first line from block - block = block:gsub("^[^\n]*\n", "") - -- replace block with highlighted block - block = '<div class="code-block">' .. highlight(lang, block) .. '</div>' - text = text:sub(1, start-1) .. block .. text:sub(endstop+1) - code_block_ranges[#code_block_ranges+1] = {start, start+block:len()} - pos = start + block:len() - end - return text -end - --- Main markdown processing function -local function markdown(text) - init_hash(text) - init_escape_table() - - text = cleanup(text) - text = protect(text) - text = code_block_highlight(text) - text, link_database = strip_link_definitions(text) - text = block_transform(text) - text = unescape_special_chars(text) - return text -end - ----------------------------------------------------------------------- --- End of module ----------------------------------------------------------------------- - --- For compatibility, set markdown function as a global -_G.markdown = markdown - --- Class for parsing command-line options -local OptionParser = {} -OptionParser.__index = OptionParser - --- Creates a new option parser -function OptionParser:new() - local o = {short = {}, long = {}} - setmetatable(o, self) - return o -end - --- Calls f() whenever a flag with specified short and long name is encountered -function OptionParser:flag(short, long, f) - local info = {type = "flag", f = f} - if short then self.short[short] = info end - if long then self.long[long] = info end -end - --- Calls f(param) whenever a parameter flag with specified short and long name is encountered -function OptionParser:param(short, long, f) - local info = {type = "param", f = f} - if short then self.short[short] = info end - if long then self.long[long] = info end -end - --- Calls f(v) for each non-flag argument -function OptionParser:arg(f) - self.arg = f -end - --- Runs the option parser for the specified set of arguments. Returns true if all arguments --- where successfully parsed and false otherwise. -function OptionParser:run(args) - local pos = 1 - while pos <= #args do - local arg = args[pos] - if arg == "--" then - for i=pos+1,#args do - if self.arg then self.arg(args[i]) end - return true - end - end - if arg:match("^%-%-") then - local info = self.long[arg:sub(3)] - if not info then print("Unknown flag: " .. arg) return false end - if info.type == "flag" then - info.f() - pos = pos + 1 - else - local param = args[pos+1] - if not param then print("No parameter for flag: " .. arg) return false end - info.f(param) - pos = pos+2 - end - elseif arg:match("^%-") then - for i=2,arg:len() do - local c = arg:sub(i,i) - local info = self.short[c] - if not info then print("Unknown flag: -" .. c) return false end - if info.type == "flag" then - info.f() - else - if i == arg:len() then - local param = args[pos+1] - if not param then print("No parameter for flag: -" .. c) return false end - info.f(param) - pos = pos + 1 - else - local param = arg:sub(i+1) - info.f(param) - end - break - end - end - pos = pos + 1 - else - if self.arg then self.arg(arg) end - pos = pos + 1 - end - end - return true -end - -local function read_file(path, descr) - local file = io.open(path) or error("Could not open " .. descr .. " file: " .. path) - local contents = file:read("*a") or error("Could not read " .. descr .. " from " .. path) - file:close() - return contents -end - --- Handles the case when markdown is run from the command line -local function run_command_line(arg) - -- Generate output for input s given options - local function run(s, options) - s = markdown(s) - if not options.wrap_header then return s end - local header - if options.header then - header = read_file(options.header, "header") - else - header = [[ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -<html> -<head> - <meta http-equiv="content-type" content="text/html; charset=CHARSET" /> - <title>TITLE</title> - <link rel="stylesheet" type="text/css" href="STYLESHEET" /> -</head> -<body> -]] - local title = options.title or s:match("<h1>(.-)</h1>") or s:match("<h2>(.-)</h2>") or - s:match("<h3>(.-)</h3>") or "Untitled" - header = header:gsub("TITLE", title) - if options.inline_style then - local style = read_file(options.stylesheet, "style sheet") - header = header:gsub('<link rel="stylesheet" type="text/css" href="STYLESHEET" />', - "<style type=\"text/css\"><!--\n" .. style .. "\n--></style>") - else - header = header:gsub("STYLESHEET", options.stylesheet) - end - header = header:gsub("CHARSET", options.charset) - end - local footer = "</body></html>" - if options.footer then - footer = read_file(options.footer, "footer") - end - return header .. s .. footer - end - - -- Generate output path name from input path name given options. - local function outpath(path, options) - if options.append then return path .. ".html" end - local m = path:match("^(.+%.html)[^/\\]+$") if m then return m end - m = path:match("^(.+%.)[^/\\]*$") if m and path ~= m .. "html" then return m .. "html" end - return path .. ".html" - end - - -- Default commandline options - local options = { - wrap_header = true, - header = nil, - footer = nil, - charset = "utf-8", - title = nil, - stylesheet = "default.css", - inline_style = false - } - local help = [[ -Usage: markdown.lua [OPTION] [FILE] -Runs the markdown text markup to HTML converter on each file specified on the -command line. If no files are specified, runs on standard input. - -No header: - -n, --no-wrap Don't wrap the output in <html>... tags. -Custom header: - -e, --header FILE Use content of FILE for header. - -f, --footer FILE Use content of FILE for footer. -Generated header: - -c, --charset SET Specifies charset (default utf-8). - -i, --title TITLE Specifies title (default from first <h1> tag). - -s, --style STYLE Specifies style sheet file (default default.css). - -l, --inline-style Include the style sheet file inline in the header. -Generated files: - -a, --append Append .html extension (instead of replacing). -Other options: - -h, --help Print this help text. - -t, --test Run the unit tests. -]] - - local run_stdin = true - local op = OptionParser:new() - op:flag("n", "no-wrap", function () options.wrap_header = false end) - op:param("e", "header", function (x) options.header = x end) - op:param("f", "footer", function (x) options.footer = x end) - op:param("c", "charset", function (x) options.charset = x end) - op:param("i", "title", function(x) options.title = x end) - op:param("s", "style", function(x) options.stylesheet = x end) - op:flag("l", "inline-style", function() options.inline_style = true end) - op:flag("a", "append", function() options.append = true end) - op:flag("t", "test", function() - local n = arg[0]:gsub("markdown%.lua", "markdown-tests.lua") - local f = io.open(n) - if f then - f:close() - package.loaded.markdown = markdown - dofile(n) - else - error("Cannot find markdown-tests.lua") - end - run_stdin = false - end) - op:flag("h", "help", function() print(help) run_stdin = false end) - op:arg(function(path) - local s = read_file(path, "input") - s = run(s, options) - local file = io.open(outpath(path, options), "w") or error("Could not open output file: " .. outpath(path, options)) - file:write(s) - file:close() - run_stdin = false - end - ) - - if not op:run(arg) then - print(help) - run_stdin = false - end - - if run_stdin then - local s = io.read("*a") - s = run(s, options) - io.write(s) - end -end - --- If we are being run from the command-line, act accordingly -if arg and arg[0]:find("markdown%.lua$") then - run_command_line(arg) -else - return markdown -end |
