diff --git a/frontend/apps/filemanager/filemanager.lua b/frontend/apps/filemanager/filemanager.lua index ed513f20d..d2d5e92ca 100644 --- a/frontend/apps/filemanager/filemanager.lua +++ b/frontend/apps/filemanager/filemanager.lua @@ -1,33 +1,34 @@ -local FileManagerHistory = require("apps/filemanager/filemanagerhistory") -local InputContainer = require("ui/widget/container/inputcontainer") -local FrameContainer = require("ui/widget/container/framecontainer") -local CenterContainer = require("ui/widget/container/centercontainer") -local FileManagerMenu = require("apps/filemanager/filemanagermenu") -local DocumentRegistry = require("document/documentregistry") -local VerticalGroup = require("ui/widget/verticalgroup") -local Screenshoter = require("ui/widget/screenshoter") -local ButtonDialogTitle = require("ui/widget/buttondialogtitle") -local InputDialog = require("ui/widget/inputdialog") -local VerticalSpan = require("ui/widget/verticalspan") -local FileChooser = require("ui/widget/filechooser") -local TextWidget = require("ui/widget/textwidget") local Blitbuffer = require("ffi/blitbuffer") -local lfs = require("libs/libkoreader-lfs") +local ButtonDialogTitle = require("ui/widget/buttondialogtitle") +local CenterContainer = require("ui/widget/container/centercontainer") +local Device = require("device") local DocSettings = require("docsettings") -local UIManager = require("ui/uimanager") -local Screen = require("device").screen -local Geom = require("ui/geometry") +local DocumentRegistry = require("document/documentregistry") local Event = require("ui/event") -local Device = require("device") -local util = require("ffi/util") +local FileChooser = require("ui/widget/filechooser") +local FileManagerConverter = require("apps/filemanager/filemanagerconverter") +local FileManagerHistory = require("apps/filemanager/filemanagerhistory") +local FileManagerMenu = require("apps/filemanager/filemanagermenu") local Font = require("ui/font") -local logger = require("logger") -local _ = require("gettext") -local KeyValuePage = require("ui/widget/keyvaluepage") -local ReaderUI = require("apps/reader/readerui") +local FrameContainer = require("ui/widget/container/framecontainer") +local Geom = require("ui/geometry") local InfoMessage = require("ui/widget/infomessage") +local InputContainer = require("ui/widget/container/inputcontainer") +local InputDialog = require("ui/widget/inputdialog") +local KeyValuePage = require("ui/widget/keyvaluepage") local PluginLoader = require("pluginloader") local ReaderDictionary = require("apps/reader/modules/readerdictionary") +local ReaderUI = require("apps/reader/readerui") +local Screenshoter = require("ui/widget/screenshoter") +local TextWidget = require("ui/widget/textwidget") +local VerticalGroup = require("ui/widget/verticalgroup") +local VerticalSpan = require("ui/widget/verticalspan") +local UIManager = require("ui/uimanager") +local lfs = require("libs/libkoreader-lfs") +local logger = require("logger") +local util = require("ffi/util") +local _ = require("gettext") +local Screen = Device.screen local function getDefaultDir() if Device:isKindle() then @@ -243,7 +244,18 @@ function FileManager:init() end, } }, + -- a little hack to get visual functionality grouping + {}, { + { + text = _("Convert"), + enabled = lfs.attributes(file, "mode") == "file" + and FileManagerConverter:isSupported(file), + callback = function() + UIManager:close(self.file_dialog) + FileManagerConverter:showConvertButtons(file, self) + end, + }, { text = _("Book information"), enabled = lfs.attributes(file, "mode") == "file" diff --git a/frontend/apps/filemanager/filemanagerconverter.lua b/frontend/apps/filemanager/filemanagerconverter.lua new file mode 100644 index 000000000..79761ccdb --- /dev/null +++ b/frontend/apps/filemanager/filemanagerconverter.lua @@ -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 = "\n\n\n", + insertHead = string.format("%s\n\n\n", title), + appendTail = "\n\n", + } + 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 diff --git a/frontend/apps/filemanager/lib/md.lua b/frontend/apps/filemanager/lib/md.lua new file mode 100644 index 000000000..4646c3fe3 --- /dev/null +++ b/frontend/apps/filemanager/lib/md.lua @@ -0,0 +1,517 @@ +-- From https://github.com/bakpakin/luamd revision daf7cc71f5de9a2fe66a60f452172e9597724d5d + +--[[ +Copyright (c) 2016 Calvin Rose + +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("", 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('', 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 +})