mirror of https://github.com/koreader/koreader
FileConverter added with local Markdown to HTML conversion (#2737)
* FileConverter added with local Markdown to HTML conversionpull/2762/head
parent
c847d628e1
commit
9b2128ecb8
@ -0,0 +1,108 @@
|
|||||||
|
--[[--
|
||||||
|
This module is responsible for converting files.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
|
||||||
|
local ConfirmBox = require("ui/widget/confirmbox")
|
||||||
|
local UIManager = require("ui/uimanager")
|
||||||
|
local lfs = require("libs/libkoreader-lfs")
|
||||||
|
local logger = require("logger")
|
||||||
|
local util = require("util")
|
||||||
|
local gettext = require("gettext")
|
||||||
|
local T = require("ffi/util").template
|
||||||
|
|
||||||
|
local FileConverter = {
|
||||||
|
formats_from = {
|
||||||
|
md = {
|
||||||
|
name = gettext("Markdown"),
|
||||||
|
from = "markdown",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
formats_to = {
|
||||||
|
epub = {
|
||||||
|
to = "epub",
|
||||||
|
},
|
||||||
|
html = {
|
||||||
|
to = "html",
|
||||||
|
},
|
||||||
|
pdf = {
|
||||||
|
to = "pdf",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
--curl --form input_files[]=@README.md --form from=markdown --form to=pdf http://c.docverter.com/convert
|
||||||
|
docverter_url = "http://c.docverter.com/convert",
|
||||||
|
}
|
||||||
|
|
||||||
|
--- Converts a markdown fragment to a full HTML document.
|
||||||
|
---- @string markdown the markdown fragment
|
||||||
|
---- @string title an optional title for the HTML document
|
||||||
|
---- @treturn string an HTML document
|
||||||
|
function FileConverter:mdToHtml(markdown, title)
|
||||||
|
local MD = require("frontend/apps/filemanager/lib/md")
|
||||||
|
local md_options = {
|
||||||
|
prependHead = "<!DOCTYPE html>\n<html>\n<head>\n",
|
||||||
|
insertHead = string.format("<title>%s</title>\n</head>\n<body>\n", title),
|
||||||
|
appendTail = "\n</body>\n</html>",
|
||||||
|
}
|
||||||
|
local html, err = MD(markdown, md_options)
|
||||||
|
if err then
|
||||||
|
logger.warn("FileManagerConverter: could not generate HTML", err)
|
||||||
|
end
|
||||||
|
return html
|
||||||
|
end
|
||||||
|
|
||||||
|
function FileConverter:_mdFileToHtml(file, title)
|
||||||
|
local f = io.open(file, "rb")
|
||||||
|
local content = f:read("*all")
|
||||||
|
f:close()
|
||||||
|
local html = self:mdToHtml(content, title)
|
||||||
|
return html
|
||||||
|
end
|
||||||
|
|
||||||
|
function FileConverter:writeStringToFile(content, file)
|
||||||
|
local f = io.open(file, "w")
|
||||||
|
f:write(content)
|
||||||
|
f:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
function FileConverter:isSupported(file)
|
||||||
|
return FileConverter.formats_from[util.getFileNameSuffix(file)] and true or false
|
||||||
|
end
|
||||||
|
|
||||||
|
function FileConverter:showConvertButtons(file, ui)
|
||||||
|
local _, filename_pure = util.splitFilePathName(file)
|
||||||
|
local filename_suffix = util.getFileNameSuffix(file)
|
||||||
|
local filetype_name = self.formats_from[filename_suffix].name
|
||||||
|
self.convert_dialog = ButtonDialogTitle:new{
|
||||||
|
title = T(gettext("Convert %1 to:"), filetype_name),
|
||||||
|
buttons = {
|
||||||
|
{
|
||||||
|
{
|
||||||
|
text = gettext("HTML"),
|
||||||
|
callback = function()
|
||||||
|
local html = FileConverter:_mdFileToHtml(file, filename_pure)
|
||||||
|
if not html then return end
|
||||||
|
local filename_html = file..".html"
|
||||||
|
if lfs.attributes(filename_html, "mode") == "file" then
|
||||||
|
UIManager:show(ConfirmBox:new{
|
||||||
|
text = gettext("Overwrite existing HTML file?"),
|
||||||
|
ok_text = gettext("Overwrite"),
|
||||||
|
ok_callback = function()
|
||||||
|
FileConverter:writeStringToFile(html, filename_html)
|
||||||
|
UIManager:close(self.convert_dialog)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
else
|
||||||
|
FileConverter:writeStringToFile(html, filename_html)
|
||||||
|
ui:refreshPath()
|
||||||
|
UIManager:close(self.convert_dialog)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
UIManager:show(self.convert_dialog)
|
||||||
|
end
|
||||||
|
|
||||||
|
return FileConverter
|
@ -0,0 +1,517 @@
|
|||||||
|
-- From https://github.com/bakpakin/luamd revision daf7cc71f5de9a2fe66a60f452172e9597724d5d
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Copyright (c) 2016 Calvin Rose <calsrose@gmail.com>
|
||||||
|
|
||||||
|
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.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local concat = table.concat
|
||||||
|
local sub = string.sub
|
||||||
|
local match = string.match
|
||||||
|
local format = string.format
|
||||||
|
local gmatch = string.gmatch
|
||||||
|
local byte = string.byte
|
||||||
|
local find = string.find
|
||||||
|
local lower = string.lower
|
||||||
|
local tonumber = tonumber -- luacheck: no unused
|
||||||
|
local type = type
|
||||||
|
local pcall = pcall
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Stream Utils
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function stringLineStream(str)
|
||||||
|
return gmatch(str, "([^\n\r]*)\r?\n?")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function tableLineStream(t)
|
||||||
|
local index = 0
|
||||||
|
return function()
|
||||||
|
index = index + 1
|
||||||
|
return t[index]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bufferStream(linestream)
|
||||||
|
local bufferedLine = linestream()
|
||||||
|
return function()
|
||||||
|
bufferedLine = linestream()
|
||||||
|
return bufferedLine
|
||||||
|
end, function()
|
||||||
|
return bufferedLine
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Line Level Operations
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local lineDelimiters = {'`', '__', '**', '_', '*'}
|
||||||
|
local function findDelim(str, start, max)
|
||||||
|
local delim = nil
|
||||||
|
local min = 1/0
|
||||||
|
local finish = 1/0
|
||||||
|
max = max or #str
|
||||||
|
for i = 1, #lineDelimiters do
|
||||||
|
local pos, fin = find(str, lineDelimiters[i], start, true)
|
||||||
|
if pos and pos < min and pos <= max then
|
||||||
|
min = pos
|
||||||
|
finish = fin
|
||||||
|
delim = lineDelimiters[i]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return delim, min, finish
|
||||||
|
end
|
||||||
|
|
||||||
|
local function externalLinkEscape(str, t)
|
||||||
|
local nomatches = true
|
||||||
|
for m1, m2, m3 in gmatch(str, '(.*)%[(.*)%](.*)') do
|
||||||
|
if nomatches then t[#t + 1] = match(m1, '^(.-)!?$'); nomatches = false end
|
||||||
|
if byte(m1, #m1) == byte '!' then
|
||||||
|
t[#t + 1] = {type = 'img', attributes = {alt = m2}}
|
||||||
|
else
|
||||||
|
t[#t + 1] = {m2, type = 'a'}
|
||||||
|
end
|
||||||
|
t[#t + 1] = m3
|
||||||
|
end
|
||||||
|
if nomatches then t[#t + 1] = str end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function linkEscape(str, t)
|
||||||
|
local nomatches = true
|
||||||
|
for m1, m2, m3, m4 in gmatch(str, '(.*)%[(.*)%]%((.*)%)(.*)') do
|
||||||
|
if nomatches then externalLinkEscape(match(m1, '^(.-)!?$'), t); nomatches = false end
|
||||||
|
if byte(m1, #m1) == byte '!' then
|
||||||
|
t[#t + 1] = {type = 'img', attributes = {
|
||||||
|
src = m3,
|
||||||
|
alt = m2
|
||||||
|
}, noclose = true}
|
||||||
|
else
|
||||||
|
t[#t + 1] = {m2, type = 'a', attributes = {href = m3}}
|
||||||
|
end
|
||||||
|
externalLinkEscape(m4, t)
|
||||||
|
end
|
||||||
|
if nomatches then externalLinkEscape(str, t) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local lineDeimiterNames = {['`'] = 'code', ['__'] = 'strong', ['**'] = 'strong', ['_'] = 'em', ['*'] = 'em' }
|
||||||
|
local function lineRead(str, start, finish)
|
||||||
|
start, finish = start or 1, finish or #str
|
||||||
|
local searchIndex = start
|
||||||
|
local tree = {}
|
||||||
|
while true do
|
||||||
|
local delim, dstart, dfinish = findDelim(str, searchIndex, finish)
|
||||||
|
if not delim then
|
||||||
|
linkEscape(sub(str, searchIndex, finish), tree)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
if dstart > searchIndex then
|
||||||
|
linkEscape(sub(str, searchIndex, dstart - 1), tree)
|
||||||
|
end
|
||||||
|
local nextdstart, nextdfinish = find(str, delim, dfinish + 1, true)
|
||||||
|
if nextdstart then
|
||||||
|
if delim == '`' then
|
||||||
|
tree[#tree + 1] = {
|
||||||
|
sub(str, dfinish + 1, nextdstart - 1),
|
||||||
|
type = 'code'
|
||||||
|
}
|
||||||
|
else
|
||||||
|
local subtree = lineRead(str, dfinish + 1, nextdstart - 1)
|
||||||
|
subtree.type = lineDeimiterNames[delim]
|
||||||
|
tree[#tree + 1] = subtree
|
||||||
|
end
|
||||||
|
searchIndex = nextdfinish + 1
|
||||||
|
else
|
||||||
|
tree[#tree + 1] = {
|
||||||
|
delim,
|
||||||
|
}
|
||||||
|
searchIndex = dfinish + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return tree
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getIndentLevel(line)
|
||||||
|
local level = 0
|
||||||
|
for i = 1, #line do
|
||||||
|
local b = byte(line, i)
|
||||||
|
if b == byte(' ') or b == byte('>') then
|
||||||
|
level = level + 1
|
||||||
|
elseif b == byte('\t') then
|
||||||
|
level = level + 4
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return level
|
||||||
|
end
|
||||||
|
|
||||||
|
local function stripIndent(line, level, ignorepattern) -- luacheck: no unused args
|
||||||
|
local currentLevel = -1
|
||||||
|
for i = 1, #line do
|
||||||
|
if byte(line, i) == byte("\t") then
|
||||||
|
currentLevel = currentLevel + 4
|
||||||
|
elseif byte(line, i) == byte(" ") or byte(line, i) == byte(">") then
|
||||||
|
currentLevel = currentLevel + 1
|
||||||
|
else
|
||||||
|
return sub(line, i, -1)
|
||||||
|
end
|
||||||
|
if currentLevel == level then
|
||||||
|
return sub(line, i, -1)
|
||||||
|
elseif currentLevel > level then
|
||||||
|
local front = ""
|
||||||
|
for j = 1, currentLevel - level do front = front .. " " end -- luacheck: no unused args
|
||||||
|
return front .. sub(line, i, -1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Patterns
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local PATTERN_EMPTY = "^%s*$"
|
||||||
|
local PATTERN_COMMENT = "^%s*<>"
|
||||||
|
local PATTERN_HEADER = "^%s*(%#+)%s*(.*)%#*$"
|
||||||
|
local PATTERN_RULE1 = "^%s*(%-+)%s*$"
|
||||||
|
local PATTERN_RULE2 = "^%s*(%*+)%s*$"
|
||||||
|
local PATTERN_RULE3 = "^%s*(%_+)%s*$"
|
||||||
|
local PATTERN_CODEBLOCK = "^%s*%`%`%`(.*)"
|
||||||
|
local PATTERN_BLOCKQUOTE = "^%s*> (.*)$"
|
||||||
|
local PATTERN_ULIST = "^%s*[%*%-] (.+)$"
|
||||||
|
local PATTERN_OLIST = "^%s*%d+%. (.+)$"
|
||||||
|
local PATTERN_LINKDEF = "^%s*%[(.*)%]%s*%:%s*(.*)"
|
||||||
|
|
||||||
|
-- List of patterns
|
||||||
|
local PATTERNS = {
|
||||||
|
PATTERN_EMPTY,
|
||||||
|
PATTERN_COMMENT,
|
||||||
|
PATTERN_HEADER,
|
||||||
|
PATTERN_RULE1,
|
||||||
|
PATTERN_RULE2,
|
||||||
|
PATTERN_RULE3,
|
||||||
|
PATTERN_CODEBLOCK,
|
||||||
|
PATTERN_BLOCKQUOTE,
|
||||||
|
PATTERN_ULIST,
|
||||||
|
PATTERN_OLIST,
|
||||||
|
PATTERN_LINKDEF
|
||||||
|
}
|
||||||
|
|
||||||
|
local function isSpecialLine(line)
|
||||||
|
for i = 1, #PATTERNS do
|
||||||
|
if match(line, PATTERNS[i]) then return PATTERNS[i] end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Simple Reading - Non Recursive
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function readSimple(pop, peek, tree, links)
|
||||||
|
|
||||||
|
local line = peek()
|
||||||
|
if not line then return end
|
||||||
|
|
||||||
|
-- Test for Empty or Comment
|
||||||
|
if match(line, PATTERN_EMPTY) or match(line, PATTERN_COMMENT) then
|
||||||
|
return pop()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test for Header
|
||||||
|
local m, rest = match(line, PATTERN_HEADER)
|
||||||
|
if m then
|
||||||
|
tree[#tree + 1] = {
|
||||||
|
lineRead(rest),
|
||||||
|
type = "h" .. #m
|
||||||
|
}
|
||||||
|
return pop()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test for Horizontal Rule
|
||||||
|
if match(line, PATTERN_RULE1) or
|
||||||
|
match(line, PATTERN_RULE2) or
|
||||||
|
match(line, PATTERN_RULE3) then
|
||||||
|
tree[#tree + 1] = { type = "hr", noclose = true }
|
||||||
|
return pop()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test for Code Block
|
||||||
|
local syntax = match(line, PATTERN_CODEBLOCK)
|
||||||
|
if syntax then
|
||||||
|
local indent = getIndentLevel(line)
|
||||||
|
local code = {
|
||||||
|
type = "code"
|
||||||
|
}
|
||||||
|
if #syntax > 0 then
|
||||||
|
code.attributes = {
|
||||||
|
class = format('language-%s', lower(syntax))
|
||||||
|
}
|
||||||
|
end
|
||||||
|
local pre = {
|
||||||
|
type = "pre",
|
||||||
|
[1] = code
|
||||||
|
}
|
||||||
|
tree[#tree + 1] = pre
|
||||||
|
while not (match(pop(), PATTERN_CODEBLOCK) and getIndentLevel(peek()) == indent) do
|
||||||
|
code[#code + 1] = peek()
|
||||||
|
code[#code + 1] = '\r\n'
|
||||||
|
end
|
||||||
|
return pop()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test for link definition
|
||||||
|
local linkname, location = match(line, PATTERN_LINKDEF)
|
||||||
|
if linkname then
|
||||||
|
links[lower(linkname)] = location
|
||||||
|
return pop()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test for header type two
|
||||||
|
local nextLine = pop()
|
||||||
|
if nextLine and match(nextLine, "^%s*%=+$") then
|
||||||
|
tree[#tree + 1] = { lineRead(line), type = "h1" }
|
||||||
|
return pop()
|
||||||
|
elseif nextLine and match(nextLine, "^%s*%-+$") then
|
||||||
|
tree[#tree + 1] = { lineRead(line), type = "h2" }
|
||||||
|
return pop()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Do Paragraph
|
||||||
|
local p = {
|
||||||
|
lineRead(line), '\r\n',
|
||||||
|
type = "p"
|
||||||
|
}
|
||||||
|
tree[#tree + 1] = p
|
||||||
|
while nextLine and not isSpecialLine(nextLine) do
|
||||||
|
p[#p + 1] = lineRead(nextLine)
|
||||||
|
p[#p + 1] = '\r\n'
|
||||||
|
nextLine = pop()
|
||||||
|
end
|
||||||
|
return peek()
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Main Reading - Potentially Recursive
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local readLineStream
|
||||||
|
|
||||||
|
local function readFragment(pop, peek, links, stop, ...)
|
||||||
|
local accum2 = {}
|
||||||
|
local line = peek()
|
||||||
|
local indent = getIndentLevel(line)
|
||||||
|
while true do
|
||||||
|
accum2[#accum2 + 1] = stripIndent(line, indent)
|
||||||
|
line = pop()
|
||||||
|
if not line then break end
|
||||||
|
if stop(line, ...) then break end
|
||||||
|
end
|
||||||
|
local tree = {}
|
||||||
|
readLineStream(tableLineStream(accum2), tree, links)
|
||||||
|
return tree
|
||||||
|
end
|
||||||
|
|
||||||
|
local function readBlockQuote(pop, peek, tree, links)
|
||||||
|
local line = peek()
|
||||||
|
if match(line, PATTERN_BLOCKQUOTE) then
|
||||||
|
local bq = readFragment(pop, peek, links, function(l)
|
||||||
|
local tp = isSpecialLine(l)
|
||||||
|
return tp and tp ~= PATTERN_BLOCKQUOTE
|
||||||
|
end)
|
||||||
|
bq.type = 'blockquote'
|
||||||
|
tree[#tree + 1] = bq
|
||||||
|
return peek()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function readList(pop, peek, tree, links, expectedIndent)
|
||||||
|
if not peek() then return end
|
||||||
|
if expectedIndent and getIndentLevel(peek()) ~= expectedIndent then return end
|
||||||
|
local listPattern = (match(peek(), PATTERN_ULIST) and PATTERN_ULIST) or
|
||||||
|
(match(peek(), PATTERN_OLIST) and PATTERN_OLIST)
|
||||||
|
if not listPattern then return end
|
||||||
|
local lineType = listPattern
|
||||||
|
local line = peek()
|
||||||
|
local indent = getIndentLevel(line)
|
||||||
|
local list = {
|
||||||
|
type = (listPattern == PATTERN_ULIST and "ul" or "ol")
|
||||||
|
}
|
||||||
|
tree[#tree + 1] = list
|
||||||
|
while lineType == listPattern do
|
||||||
|
list[#list + 1] = {
|
||||||
|
lineRead(match(line, lineType)),
|
||||||
|
type = "li"
|
||||||
|
}
|
||||||
|
line = pop()
|
||||||
|
if not line then break end
|
||||||
|
lineType = isSpecialLine(line)
|
||||||
|
if lineType ~= PATTERN_EMPTY then
|
||||||
|
local i = getIndentLevel(line)
|
||||||
|
if i < indent then break end
|
||||||
|
if i > indent then
|
||||||
|
local subtree = readFragment(pop, peek, links, function(l)
|
||||||
|
if not l then return true end
|
||||||
|
local tp = isSpecialLine(l)
|
||||||
|
return tp ~= PATTERN_EMPTY and getIndentLevel(l) < i
|
||||||
|
end)
|
||||||
|
list[#list + 1] = subtree
|
||||||
|
line = peek()
|
||||||
|
if not line then break end
|
||||||
|
lineType = isSpecialLine(line)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return peek()
|
||||||
|
end
|
||||||
|
|
||||||
|
function readLineStream(stream, tree, links)
|
||||||
|
local pop, peek = bufferStream(stream)
|
||||||
|
tree = tree or {}
|
||||||
|
links = links or {}
|
||||||
|
while peek() do
|
||||||
|
if not readBlockQuote(pop, peek, tree, links) then
|
||||||
|
if not readList(pop, peek, tree, links) then
|
||||||
|
readSimple(pop, peek, tree, links)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return tree, links
|
||||||
|
end
|
||||||
|
|
||||||
|
local function read(str) -- luacheck: no unused
|
||||||
|
return readLineStream(stringLineStream(str))
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Rendering
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function renderAttributes(attributes)
|
||||||
|
local accum = {}
|
||||||
|
for k, v in pairs(attributes) do
|
||||||
|
accum[#accum + 1] = format("%s=\"%s\"", k, v)
|
||||||
|
end
|
||||||
|
return concat(accum, ' ')
|
||||||
|
end
|
||||||
|
|
||||||
|
local function renderTree(tree, links, accum)
|
||||||
|
if tree.type then
|
||||||
|
local attribs = tree.attributes or {}
|
||||||
|
if tree.type == 'a' and not attribs.href then attribs.href = links[lower(tree[1] or '')] or '' end
|
||||||
|
if tree.type == 'img' and not attribs.src then attribs.src = links[lower(attribs.alt or '')] or '' end
|
||||||
|
local attribstr = renderAttributes(attribs)
|
||||||
|
if #attribstr > 0 then
|
||||||
|
accum[#accum + 1] = format("<%s %s>", tree.type, attribstr)
|
||||||
|
else
|
||||||
|
accum[#accum + 1] = format("<%s>", tree.type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for i = 1, #tree do
|
||||||
|
local line = tree[i]
|
||||||
|
if type(line) == "string" then
|
||||||
|
accum[#accum + 1] = line
|
||||||
|
elseif type(line) == "table" then
|
||||||
|
renderTree(line, links, accum)
|
||||||
|
else
|
||||||
|
error "Unexpected node while rendering tree."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not tree.noclose and tree.type then
|
||||||
|
accum[#accum + 1] = format("</%s>", tree.type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function renderLinesRaw(stream, options)
|
||||||
|
local tree, links = readLineStream(stream)
|
||||||
|
local accum = {}
|
||||||
|
local head, tail, insertHead, insertTail, prependHead, appendTail = nil, nil, nil, nil, nil, nil
|
||||||
|
if options then
|
||||||
|
assert(type(options) == 'table', "Options argument should be a table.")
|
||||||
|
if options.tag then
|
||||||
|
tail = format('</%s>', options.tag)
|
||||||
|
if options.attributes then
|
||||||
|
head = format('<%s %s>', options.tag, renderAttributes(options.attributes))
|
||||||
|
else
|
||||||
|
head = format('<%s>', options.tag)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
insertHead = options.insertHead
|
||||||
|
insertTail = options.insertTail
|
||||||
|
prependHead = options.prependHead
|
||||||
|
appendTail = options.appendTail
|
||||||
|
end
|
||||||
|
accum[#accum + 1] = prependHead
|
||||||
|
accum[#accum + 1] = head
|
||||||
|
accum[#accum + 1] = insertHead
|
||||||
|
renderTree(tree, links, accum)
|
||||||
|
accum[#accum + 1] = insertTail
|
||||||
|
accum[#accum + 1] = tail
|
||||||
|
accum[#accum + 1] = appendTail
|
||||||
|
return concat(accum)
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Module
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function pwrap(...)
|
||||||
|
local status, value = pcall(...)
|
||||||
|
if status then
|
||||||
|
return value
|
||||||
|
else
|
||||||
|
return nil, value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function renderLineIterator(stream, options)
|
||||||
|
return pwrap(renderLinesRaw, stream, options)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function renderTable(t, options)
|
||||||
|
return pwrap(renderLinesRaw, tableLineStream(t), options)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function renderString(str, options)
|
||||||
|
return pwrap(renderLinesRaw, stringLineStream(str), options)
|
||||||
|
end
|
||||||
|
|
||||||
|
local renderers = {
|
||||||
|
['string'] = renderString,
|
||||||
|
['table'] = renderTable,
|
||||||
|
['function'] = renderLineIterator
|
||||||
|
}
|
||||||
|
|
||||||
|
local function render(source, options)
|
||||||
|
local renderer = renderers[type(source)]
|
||||||
|
if not renderer then return nil, "Source must be a string, table, or function." end
|
||||||
|
return renderer(source, options)
|
||||||
|
end
|
||||||
|
|
||||||
|
return setmetatable({
|
||||||
|
render = render,
|
||||||
|
renderString = renderString,
|
||||||
|
renderLineIterator = renderLineIterator,
|
||||||
|
renderTable = renderTable
|
||||||
|
}, {
|
||||||
|
__call = function(self, ...) -- luacheck: no unused args
|
||||||
|
return render(...)
|
||||||
|
end
|
||||||
|
})
|
Loading…
Reference in New Issue