mirror of https://github.com/koreader/koreader
Merge 8e8045046a
into d82815952e
commit
0a5a7b1b07
@ -0,0 +1,6 @@
|
||||
local _ = require("gettext")
|
||||
return {
|
||||
name = "downloadtoepub",
|
||||
fullname = _("Download to EPUB"),
|
||||
description = _([[Download URLs to an EPUB.]]),
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
local DataStorage = require("datastorage")
|
||||
local LuaSettings = require("frontend/luasettings")
|
||||
local logger = require("logger")
|
||||
|
||||
local History = {
|
||||
history_file = "downloadtoepub_history.lua",
|
||||
lua_settings = nil,
|
||||
}
|
||||
|
||||
History.STACK = "stack"
|
||||
History.MAX_ITEMS = 100
|
||||
|
||||
function History:new(o)
|
||||
o = o or {}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
o:init()
|
||||
return o
|
||||
end
|
||||
|
||||
function History:init()
|
||||
self.lua_settings = LuaSettings:open(("%s/%s"):format(DataStorage:getSettingsDir(), self.history_file))
|
||||
end
|
||||
|
||||
function History:add(url, download_path)
|
||||
-- Add to the history by pushing to the first element of the list.
|
||||
-- The history stack should only contain one entry of a given ID.
|
||||
local stack = self:get()
|
||||
-- Add the new entry to the stack table.
|
||||
table.insert(stack, {
|
||||
url = url,
|
||||
download_path = download_path,
|
||||
timestamp = os.time(os.date("!*t"))
|
||||
})
|
||||
-- Sort the table by the timestamp key.
|
||||
table.sort(stack, function(a,b) return a.timestamp > b.timestamp end)
|
||||
-- Delete duplicate entries, given by puzzle id, by looping through
|
||||
-- the stack and keeping the first occurance (i.e.: newest) of
|
||||
-- a URL.
|
||||
local new_stack = {}
|
||||
local duplicates = {}
|
||||
local index = 1
|
||||
for i, value in ipairs(stack) do
|
||||
if duplicates[value.url] == nil then
|
||||
duplicates[value.url] = true
|
||||
table.insert(new_stack, value)
|
||||
index = index + 1
|
||||
end
|
||||
if index > History.MAX_ITEMS then
|
||||
break;
|
||||
end
|
||||
end
|
||||
-- Save 'er.
|
||||
self.lua_settings:saveSetting(History.STACK, new_stack)
|
||||
self.lua_settings:flush()
|
||||
end
|
||||
|
||||
-- Remove all instances of the given URL from history.
|
||||
function History:remove(url)
|
||||
local stack = self:get()
|
||||
local new_stack = {}
|
||||
for i, value in ipairs(stack) do
|
||||
logger.dbg(value)
|
||||
if value.url ~= url then
|
||||
logger.dbg("lol")
|
||||
table.insert(new_stack, value)
|
||||
end
|
||||
end
|
||||
|
||||
self.lua_settings:saveSetting(History.STACK, new_stack)
|
||||
self.lua_settings:flush()
|
||||
end
|
||||
|
||||
function History:get()
|
||||
local stack = self.lua_settings:readSetting(History.STACK) or {}
|
||||
return stack
|
||||
end
|
||||
|
||||
function History:find(v)
|
||||
local stack = self:get()
|
||||
|
||||
local maybe_found = nil
|
||||
for i, value in ipairs(stack) do
|
||||
if value.url == v or
|
||||
value.download_path == v then
|
||||
maybe_found = value
|
||||
break;
|
||||
end
|
||||
end
|
||||
|
||||
return maybe_found
|
||||
end
|
||||
|
||||
function History:save()
|
||||
|
||||
end
|
||||
|
||||
function History:clear()
|
||||
self.lua_settings:saveSetting(History.STACK, {})
|
||||
self.lua_settings:flush()
|
||||
end
|
||||
|
||||
return History
|
@ -0,0 +1,68 @@
|
||||
local _ = require("gettext")
|
||||
local History = require("epubhistory")
|
||||
|
||||
local HistoryView = {}
|
||||
|
||||
function HistoryView:new(o)
|
||||
o = o or {}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function HistoryView:getLastDownloadButton(load_puzzle_cb)
|
||||
local history = History:new{}
|
||||
if #history:get() > 0 then
|
||||
local history_item = history:get()[1]
|
||||
return {
|
||||
text = _(("Last download: \"%s\""):format(history_item['url'])),
|
||||
callback = function()
|
||||
load_puzzle_cb(history_item)
|
||||
end
|
||||
}
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
--[[--
|
||||
Return a list that can be used to populate a plugin menu.
|
||||
]]
|
||||
function HistoryView:getMenuItems(open_epub_cb, clear_history_cb)
|
||||
local menu_items = {}
|
||||
local sub_menu_items = {}
|
||||
-- If the user has started a puzzle, we'll add a new option to the menu.
|
||||
local history = History:new{}
|
||||
|
||||
if #history:get() > 0 then
|
||||
local history_list = {}
|
||||
for i, item in ipairs(history:get()) do
|
||||
table.insert(sub_menu_items, {
|
||||
text = item['url'],
|
||||
callback = function()
|
||||
open_epub_cb(item)
|
||||
end
|
||||
})
|
||||
end
|
||||
-- Add a clear history button
|
||||
table.insert(sub_menu_items,
|
||||
{
|
||||
text = _("Clear history"),
|
||||
keep_menu_open = false,
|
||||
callback = function()
|
||||
history:clear()
|
||||
end
|
||||
}
|
||||
)
|
||||
table.insert(menu_items, {
|
||||
text = _("History"),
|
||||
sub_item_table = sub_menu_items
|
||||
})
|
||||
return menu_items
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
return HistoryView
|
@ -0,0 +1,129 @@
|
||||
local xml2lua = require("libs/xml2lua/xml2lua")
|
||||
local Item = require("libs/gazette/epub/package/item")
|
||||
local Manifest = require("libs/gazette/epub/package/manifest")
|
||||
local Spine = require("libs/gazette/epub/package/spine")
|
||||
|
||||
local Package = {
|
||||
title = nil,
|
||||
author = nil,
|
||||
language = "en",
|
||||
modified = nil,
|
||||
manifest = nil,
|
||||
spine = nil,
|
||||
}
|
||||
|
||||
function Package:new(o)
|
||||
o = o or {}
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
|
||||
o.manifest = Manifest:new{}
|
||||
o.spine = Spine:new{}
|
||||
o.modified = os.date("%Y-%m-%dT%H:%M:%SZ")
|
||||
o:setTitle("Default title")
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
Package.extend = Package.new
|
||||
|
||||
function Package:setTitle(title)
|
||||
self.title = title
|
||||
end
|
||||
|
||||
function Package:setAuthor(author)
|
||||
self.author = author
|
||||
end
|
||||
|
||||
function Package:addItem(item)
|
||||
local ok, err = self.manifest:addItem(item)
|
||||
if ok and
|
||||
item ~= nil
|
||||
then
|
||||
self.spine:addItem(item)
|
||||
self:addItemToNav(item)
|
||||
end
|
||||
end
|
||||
|
||||
function Package:addItemToNav(item)
|
||||
if not item or
|
||||
item.property == Item.PROPERTY.NAV or
|
||||
item.add_to_nav == false
|
||||
then
|
||||
return false
|
||||
end
|
||||
local nav = self:getNav()
|
||||
-- Nav doesn't check to see if content already contained in nav,
|
||||
-- since it's entirely possible the same content could be linked twice.
|
||||
-- Why? I dunno, but it's possible.
|
||||
table.insert(nav.items, item)
|
||||
return true
|
||||
end
|
||||
|
||||
function Package:getNav()
|
||||
local nav_index = self.manifest:findItemLocation(function(item)
|
||||
return item.properties == Item.PROPERTY.NAV
|
||||
end)
|
||||
return self.manifest.items[nav_index]
|
||||
end
|
||||
|
||||
function Package:updateNav(item)
|
||||
local nav_index = self.manifest:findItemLocation(function(item)
|
||||
return item.properties == Item.PROPERTY.NAV
|
||||
end)
|
||||
self.manifest.items[nav_index] = item
|
||||
return true
|
||||
end
|
||||
|
||||
function Package:getManifestItems()
|
||||
return self.manifest.items
|
||||
end
|
||||
|
||||
function Package:addToNav()
|
||||
|
||||
end
|
||||
|
||||
function Package:getPackageXml()
|
||||
-- TODO: Add error catching/display
|
||||
local template, err = xml2lua.loadFile("plugins/downloadtoepub.koplugin/libs/gazette/epub/templates/package.xml")
|
||||
local manifest, err = self.manifest:build()
|
||||
local spine, err = self.spine:build()
|
||||
return string.format(
|
||||
template,
|
||||
self.title,
|
||||
self.author,
|
||||
self.language,
|
||||
self.modified,
|
||||
manifest,
|
||||
spine
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
local Epub = Package:extend{
|
||||
|
||||
}
|
||||
|
||||
function Epub:new(o)
|
||||
o = Package:new()
|
||||
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function Epub:addFromList(iterator)
|
||||
while true do
|
||||
local item = iterator()
|
||||
if type(item) == "table"
|
||||
then
|
||||
self:addItem(item)
|
||||
elseif item == nil
|
||||
then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return Epub
|
@ -0,0 +1,89 @@
|
||||
local EpubError = require("libs/gazette/epuberror")
|
||||
local md5 = require("ffi/sha2").md5
|
||||
|
||||
local Item = {
|
||||
id = nil,
|
||||
path = nil,
|
||||
content = nil,
|
||||
media_type = nil,
|
||||
properties = nil,
|
||||
add_to_nav = nil
|
||||
}
|
||||
|
||||
Item.PROPERTY = {
|
||||
NAV = "nav"
|
||||
}
|
||||
|
||||
Item.TYPE = "default"
|
||||
|
||||
function Item:new(o)
|
||||
o = o or {}
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
Item.extend = Item.new
|
||||
|
||||
function Item:generateId()
|
||||
self.id = "a" .. md5(self.path) -- IDs can't start with number
|
||||
end
|
||||
|
||||
function Item:getManifestPart()
|
||||
if not self.path and
|
||||
not self.mimetype
|
||||
then
|
||||
return false, EpubError:provideFromItem(self)
|
||||
end
|
||||
|
||||
self:generateId()
|
||||
|
||||
if self.properties
|
||||
then
|
||||
return string.format(
|
||||
[[<item id="%s" href="%s" media-type="%s" properties="%s"/>]],
|
||||
self.id,
|
||||
self.path,
|
||||
self.media_type,
|
||||
self.properties
|
||||
)
|
||||
else
|
||||
return string.format(
|
||||
[[<item id="%s" href="%s" media-type="%s"/>]],
|
||||
self.id,
|
||||
self.path,
|
||||
self.media_type
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
-- located in a spine factory
|
||||
function Item:getSpinePart()
|
||||
return string.format(
|
||||
[[<itemref idref="%s" />%s]],
|
||||
self.id,
|
||||
"\n"
|
||||
)
|
||||
end
|
||||
-- C-y ??
|
||||
-- this should be located in Nav, or a NavFactorNavFactoryestuestest
|
||||
function Item:getNavPart()
|
||||
return string.format(
|
||||
[[<li><a href="%s">%s</a></li>%s]],
|
||||
self.path,
|
||||
self.title,
|
||||
"\n"
|
||||
)
|
||||
end
|
||||
|
||||
function Item:getContent()
|
||||
if type(self.content) == "string"
|
||||
then
|
||||
return self.content
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return Item
|
@ -0,0 +1,55 @@
|
||||
local Item = require("libs/gazette/epub/package/item")
|
||||
local EpubError = require("libs/gazette/epuberror")
|
||||
local util = require("util")
|
||||
|
||||
local Image = Item:extend {
|
||||
format = nil,
|
||||
add_to_nav = false,
|
||||
}
|
||||
|
||||
Image.SUPPORTED_FORMATS = {
|
||||
jpeg = "image/jpeg",
|
||||
jpg = "image/jpeg",
|
||||
png = "image/png",
|
||||
gif = "image/gif",
|
||||
svg = "image/svg+xml"
|
||||
}
|
||||
|
||||
function Image:new(o)
|
||||
o = o or {}
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
|
||||
if not o.path
|
||||
then
|
||||
return false, EpubError.ITEM_MISSING_PATH
|
||||
end
|
||||
|
||||
-- Change "format" to "fileType" or "extension"
|
||||
local format = o:isFormatSupported(o.path)
|
||||
if not format
|
||||
then
|
||||
return false, EpubError.IMAGE_UNSUPPORTED_FORMAT
|
||||
end
|
||||
|
||||
o.media_type = format
|
||||
o:generateId()
|
||||
o.path = o.path
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function Image:fetchContent(data_source)
|
||||
|
||||
end
|
||||
|
||||
function Image:isFormatSupported(path)
|
||||
-- path = path and string.lower(path) or ""
|
||||
-- local extension = string.match(path, "[^.]+$")
|
||||
local extension = util.getFileNameSuffix(path)
|
||||
return Image.SUPPORTED_FORMATS[extension] and
|
||||
Image.SUPPORTED_FORMATS[extension] or
|
||||
false
|
||||
end
|
||||
|
||||
return Image
|
@ -0,0 +1,56 @@
|
||||
local Item = require("libs/gazette/epub/package/item")
|
||||
local xml2lua = require("libs/xml2lua/xml2lua")
|
||||
local _ = require("gettext")
|
||||
|
||||
local Nav = Item:extend{
|
||||
title = nil,
|
||||
items = nil,
|
||||
}
|
||||
|
||||
function Nav:new(o)
|
||||
o = o or {}
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
|
||||
o.title = _("Table of Contents")
|
||||
o.path = "nav.xhtml"
|
||||
o.properties = Item.PROPERTY.NAV
|
||||
o.media_type = "application/xhtml+xml"
|
||||
o.items = {},
|
||||
o:generateId()
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function Nav:setTitle(title)
|
||||
self.title = title
|
||||
end
|
||||
|
||||
function Nav:addItem(item)
|
||||
-- insert item, yes, but reference it by it's id...
|
||||
table.insert(self.items, item)
|
||||
end
|
||||
|
||||
function Nav:getContent()
|
||||
-- TODO: Add error catching/display
|
||||
local template, err = xml2lua.loadFile("plugins/downloadtoepub.koplugin/libs/gazette/epub/templates/nav.xhtml")
|
||||
local items_list = "\n"
|
||||
|
||||
for _, item in ipairs(self.items) do
|
||||
local part = item:getNavPart()
|
||||
if part
|
||||
then
|
||||
items_list = items_list .. part
|
||||
end
|
||||
end
|
||||
|
||||
template = string.format(
|
||||
template,
|
||||
self.title,
|
||||
items_list
|
||||
)
|
||||
|
||||
return template
|
||||
end
|
||||
|
||||
return Nav
|
@ -0,0 +1,32 @@
|
||||
local EpubError = require("libs/gazette/epuberror")
|
||||
local Item = require("libs/gazette/epub/package/item")
|
||||
local util = require("util")
|
||||
|
||||
local XHtmlItem = Item:extend {
|
||||
title = "Untitled Document",
|
||||
add_to_nav = true
|
||||
}
|
||||
|
||||
XHtmlItem.SUPPORTED_FORMATS = {
|
||||
xhtml = true,
|
||||
html = true
|
||||
}
|
||||
|
||||
function XHtmlItem:new(o)
|
||||
o = o or {}
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
|
||||
if not o.path
|
||||
then
|
||||
return false, EpubError.ITEM_MISSING_PATH
|
||||
end
|
||||
|
||||
o.path = util.urlEncode(o.path)
|
||||
o.media_type = "application/xhtml+xml"
|
||||
o:generateId()
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
return XHtmlItem
|
@ -0,0 +1,77 @@
|
||||
local EpubError = require("libs/gazette/epuberror")
|
||||
local xml2lua = require("libs/xml2lua/xml2lua")
|
||||
local Nav = require("libs/gazette/epub/package/item/nav")
|
||||
|
||||
local Manifest = {
|
||||
items = nil,
|
||||
nav = nil,
|
||||
}
|
||||
|
||||
function Manifest:new(o)
|
||||
o = o or {}
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
|
||||
o.items = {}
|
||||
local nav = Nav:new{}
|
||||
o:addItem(nav)
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function Manifest:addItem(item)
|
||||
if item == nil
|
||||
then
|
||||
return false, EpubError.MANIFEST_ITEM_NIL
|
||||
end
|
||||
|
||||
if not self:isItemIncluded(item)
|
||||
then
|
||||
table.insert(self.items, item)
|
||||
return true
|
||||
else
|
||||
return false, EpubError.MANIFEST_ITEM_ALREADY_EXISTS
|
||||
end
|
||||
end
|
||||
|
||||
function Manifest:isItemIncluded(item)
|
||||
return self:findItemLocation(function(existing_item)
|
||||
return existing_item.id == item.id
|
||||
end)
|
||||
end
|
||||
|
||||
function Manifest:findItemLocationByProperties(properties)
|
||||
return self:findItemLocation(function(existing_item)
|
||||
if existing_item.properties and
|
||||
existing_item.properties == properties
|
||||
then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end)
|
||||
end
|
||||
|
||||
function Manifest:findItemLocation(predicate)
|
||||
for index, item in ipairs(self.items) do
|
||||
if predicate(item) == true
|
||||
then
|
||||
return index
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function Manifest:build()
|
||||
local items_xml = "\n"
|
||||
for index, item in ipairs(self.items) do
|
||||
local part, err = item:getManifestPart()
|
||||
if not part
|
||||
then
|
||||
return false, EpubError.MANIFEST_BUILD_ERROR
|
||||
end
|
||||
items_xml = items_xml .. part .. "\n"
|
||||
end
|
||||
return items_xml
|
||||
end
|
||||
|
||||
return Manifest
|
@ -0,0 +1,32 @@
|
||||
local Spine = {
|
||||
items = nil,
|
||||
}
|
||||
|
||||
function Spine:new(o)
|
||||
o = o or {}
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
|
||||
o.items = {}
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function Spine:addItem(item)
|
||||
table.insert(self.items, item)
|
||||
end
|
||||
|
||||
function Spine:build()
|
||||
local xml = ""
|
||||
for _, item in ipairs(self.items) do
|
||||
local part, err = item:getSpinePart()
|
||||
if not part
|
||||
then
|
||||
return false, EpubError.SPINE_BUILD_ERROR
|
||||
end
|
||||
xml = xml .. part
|
||||
end
|
||||
return xml
|
||||
end
|
||||
|
||||
return Spine
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
||||
<rootfiles>
|
||||
<rootfile full-path="OPS/package.opf" media-type="application/oebps-package+xml"/>
|
||||
</rootfiles>
|
||||
</container>
|
@ -0,0 +1,12 @@
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en">
|
||||
<head>
|
||||
<title>%s</title>
|
||||
</head>
|
||||
<body>
|
||||
<nav epub:type="toc">
|
||||
<ol>
|
||||
%s
|
||||
</ol>
|
||||
</nav>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" xml:lang="en" unique-identifier="q">
|
||||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<dc:title id="title">%s</dc:title>
|
||||
<dc:creator>%s</dc:creator>
|
||||
<dc:language>%s</dc:language>
|
||||
<dc:identifier id="q">NOID</dc:identifier>
|
||||
<meta property="dcterms:modified">%s</meta>
|
||||
</metadata>
|
||||
<manifest>
|
||||
%s
|
||||
</manifest>
|
||||
<spine>
|
||||
%s
|
||||
</spine>
|
||||
</package>
|
@ -0,0 +1,104 @@
|
||||
local EpubError = require("libs/gazette/epuberror")
|
||||
local ZipWriter = require("ffi/zipwriter")
|
||||
local xml2lua = require("libs/xml2lua/xml2lua")
|
||||
|
||||
local Epub32Writer = ZipWriter:new {
|
||||
path = nil,
|
||||
temp_path = nil,
|
||||
}
|
||||
|
||||
function Epub32Writer:new(o)
|
||||
o = o or {}
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function Epub32Writer:build(epub)
|
||||
local ok, err = self:openTempPath()
|
||||
if not ok
|
||||
then
|
||||
return false, EpubError.EPUBWRITER_INVALID_PATH
|
||||
end
|
||||
|
||||
self:addMimetype()
|
||||
self:addContainer()
|
||||
self:addPackage(epub:getPackageXml())
|
||||
self:addItems(epub:getManifestItems())
|
||||
|
||||
self:close()
|
||||
os.rename(self.temp_path, self.path)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function Epub32Writer:setPath(path)
|
||||
local ok, err = self:isOutputAvailable(path)
|
||||
if not ok
|
||||
then
|
||||
return false, err
|
||||
else
|
||||
self.path = path
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function Epub32Writer:addMimetype()
|
||||
self:add("mimetype", "application/epub+zip")
|
||||
end
|
||||
|
||||
function Epub32Writer:addContainer()
|
||||
local container = Epub32Writer:getPart("container.xml")
|
||||
self:add("META-INF/container.xml", container)
|
||||
end
|
||||
|
||||
function Epub32Writer:addPackage(packagio)
|
||||
self:add("OPS/package.opf", packagio)
|
||||
end
|
||||
|
||||
function Epub32Writer:addItems(items)
|
||||
for _, item in ipairs(items) do
|
||||
local content = item:getContent()
|
||||
if content
|
||||
then
|
||||
self:add("OPS/" .. item.path, content)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Epub32Writer:openTempPath()
|
||||
self.temp_path = self.path .. ".tmp"
|
||||
|
||||
if not self:open(self.temp_path)
|
||||
then
|
||||
return false, EpubError.EPUBWRITER_INVALID_PATH
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function Epub32Writer:isOutputAvailable(path)
|
||||
local test_path = path
|
||||
|
||||
if not self:open(test_path)
|
||||
then
|
||||
return false, EpubError.EPUBWRITER_INVALID_PATH
|
||||
else
|
||||
self:close()
|
||||
os.remove(test_path)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function Epub32Writer:getPart(filename)
|
||||
local file, err = xml2lua.loadFile("plugins/downloadtoepub.koplugin/libs/gazette/epub/templates/" .. filename)
|
||||
if file
|
||||
then
|
||||
return file
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
return Epub32Writer
|
@ -0,0 +1,34 @@
|
||||
local Epub32Writer = require("libs/gazette/epub32writer")
|
||||
|
||||
local EpubBuildDirector = {
|
||||
writer = nil,
|
||||
epub = nil,
|
||||
}
|
||||
|
||||
function EpubBuildDirector:new(writer)
|
||||
if not writer then
|
||||
local defaultWriter, err = Epub32Writer:new{}
|
||||
if not defaultWriter then
|
||||
return false, err
|
||||
end
|
||||
self.writer = defaultWriter
|
||||
else
|
||||
self.writer = writer
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function EpubBuildDirector:setDestination(path)
|
||||
return self.writer:setPath(path)
|
||||
end
|
||||
|
||||
function EpubBuildDirector:construct(epub)
|
||||
local ok, err = self.writer:build(epub)
|
||||
if ok then
|
||||
return self.writer.path
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
return EpubBuildDirector
|
@ -0,0 +1,37 @@
|
||||
local _ = require("gettext")
|
||||
local T = require("ffi/util").template
|
||||
|
||||
local EpubError = {
|
||||
EPUB_INVALID_CONTENTS = _("Contents invalid"),
|
||||
EPUBWRITER_INVALID_PATH = _("The path couldn't be opened."),
|
||||
ITEMFACTORY_UNSUPPORTED_TYPE = _("Item type is not supported."),
|
||||
ITEMFACTORY_NONEXISTENT_CONSTRUCTOR = _("Item type is supported but ItemFactory doesn't have a constructor for it."),
|
||||
RESOURCE_WEBPAGE_INVALID_URL = _(""),
|
||||
ITEM_MISSNG_ID = _("Item missing id"),
|
||||
ITEM_MISSING_MEDIA_TYPE = _("Item missing media type"),
|
||||
ITEM_MISSING_PATH = _("Item missing path"),
|
||||
ITEM_NONSPECIFIC_ERROR = _("Something's wrong with your item. That's all I know"),
|
||||
IMAGE_UNSUPPORTED_FORMAT = _("Image format is not supported."),
|
||||
MANIFEST_BUILD_ERROR = _("Could not build manifest part for item."),
|
||||
MANIFEST_ITEM_ALREADY_EXISTS = _("Item already exists in manifest"),
|
||||
MANIFEST_ITEM_NIL = _("Can't add a nil item to the manifest."),
|
||||
SPINE_BUILD_ERROR = _("Could not build spine part for item."),
|
||||
}
|
||||
|
||||
function EpubError:provideFromEpubWriter(epubwriter)
|
||||
|
||||
end
|
||||
|
||||
function EpubError:provideFromItem(item)
|
||||
if not item.media_type
|
||||
then
|
||||
return EpubError.ITEM_MISSING_MEDIA_TYPE
|
||||
elseif not item.path
|
||||
then
|
||||
return EpubError.ITEM_MISSING_PATH
|
||||
else
|
||||
return EpubError.ITEM_NONSPECIFIC_ERROR
|
||||
end
|
||||
end
|
||||
|
||||
return EpubError
|
@ -0,0 +1,78 @@
|
||||
local EpubError = require("libs/gazette/epuberror")
|
||||
local XHtmlItem = require("libs/gazette/epub/package/item/xhtmlitem")
|
||||
local Image = require("libs/gazette/epub/package/item/image")
|
||||
local util = require("util")
|
||||
|
||||
local ItemFactory = {
|
||||
|
||||
}
|
||||
|
||||
ItemFactory.ITEM_TYPES = {
|
||||
xhtml = XHtmlItem.SUPPORTED_FORMATS,
|
||||
image = Image.SUPPORTED_FORMATS
|
||||
}
|
||||
|
||||
ItemFactory.ITEM_CONSTRUCTORS = {
|
||||
xhtml = function(path, content)
|
||||
return XHtmlItem:new{
|
||||
path = path,
|
||||
content = content,
|
||||
}
|
||||
end,
|
||||
image = function(path, content)
|
||||
return Image:new{
|
||||
path = path,
|
||||
content = content
|
||||
}
|
||||
end
|
||||
}
|
||||
|
||||
function ItemFactory:makeItemFromResource(resource)
|
||||
local title = resource.title
|
||||
local path = resource.filename
|
||||
local content = resource:getData()
|
||||
|
||||
local item, item_type = self:makeItem(path, content)
|
||||
|
||||
if item_type == "xhtml" and
|
||||
title
|
||||
then
|
||||
item.title = title
|
||||
end
|
||||
|
||||
return item
|
||||
end
|
||||
|
||||
function ItemFactory:makeItem(path, content)
|
||||
local suffix = util.getFileNameSuffix(
|
||||
string.lower(path)
|
||||
)
|
||||
|
||||
local matched_type = ItemFactory:getItemTypeFromFileNameSuffix(suffix)
|
||||
if not matched_type
|
||||
then
|
||||
return false, EpubError.ITEMFACTORY_UNSUPPORTED_TYPE
|
||||
end
|
||||
|
||||
local item_constructor = ItemFactory.ITEM_CONSTRUCTORS[matched_type]
|
||||
if not item_constructor
|
||||
then
|
||||
return false, EpubError.ITEMFACTORY_NONEXISTENT_CONSTRUCTOR
|
||||
end
|
||||
|
||||
return item_constructor(path, content), matched_type
|
||||
end
|
||||
|
||||
function ItemFactory:getItemTypeFromFileNameSuffix(suffix)
|
||||
local matched_item_type = nil
|
||||
for item_type, supported_formats in pairs(ItemFactory.ITEM_TYPES) do
|
||||
if supported_formats[suffix]
|
||||
then
|
||||
matched_item_type = item_type
|
||||
break
|
||||
end
|
||||
end
|
||||
return matched_item_type
|
||||
end
|
||||
|
||||
return ItemFactory
|
@ -0,0 +1,80 @@
|
||||
local Resource = require("libs/gazette/resources/resource")
|
||||
local Element = require("libs/gazette/resources/htmldocument/element")
|
||||
local util = require("util")
|
||||
|
||||
local HtmlDocument = Resource:extend{
|
||||
url = nil,
|
||||
html = nil,
|
||||
filename = nil,
|
||||
title = nil,
|
||||
}
|
||||
|
||||
function HtmlDocument:new(o)
|
||||
o = o or {}
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
|
||||
if not o.url
|
||||
and not o.html
|
||||
then
|
||||
return false
|
||||
end
|
||||
|
||||
if not o.html
|
||||
then
|
||||
local content, err = o:fetchUrlContent(o.url)
|
||||
if err
|
||||
then
|
||||
return false, err
|
||||
else
|
||||
o.html = content
|
||||
end
|
||||
end
|
||||
|
||||
o.title = o.title or o:findTitle()
|
||||
|
||||
if not o.filename
|
||||
then
|
||||
local _, filename = util.splitFilePathName(o.url or o.title)
|
||||
-- Some URLs will have a suffix (".html"), some won't.
|
||||
-- So the URL gets split to its pure filename and the suffix
|
||||
-- is manually appended.
|
||||
local pure_filename, suffix = util.splitFileNameSuffix(filename)
|
||||
local safe_filename = util.getSafeFilename(pure_filename)
|
||||
o.filename = safe_filename .. ".html"
|
||||
end
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function HtmlDocument:getData()
|
||||
return self.html
|
||||
end
|
||||
|
||||
function HtmlDocument:findImageElements()
|
||||
return self:extractElements("img")
|
||||
end
|
||||
|
||||
function HtmlDocument:findTitle()
|
||||
return string.match(self.html,"<title>(.+)</title>")
|
||||
end
|
||||
|
||||
function HtmlDocument:extractElements(tag)
|
||||
local elements = {}
|
||||
-- Build the element in two parts because the second part
|
||||
-- is generated based on the supplied tag. And it frigs with
|
||||
-- the first part because of the %s thing
|
||||
local element_to_match = "(<%s" .. string.format("*%s [^>]*>)", tag)
|
||||
for element_html in string.gmatch(self.html, element_to_match) do
|
||||
local element = Element:new(element_html)
|
||||
table.insert(elements, element)
|
||||
end
|
||||
return elements
|
||||
end
|
||||
|
||||
function HtmlDocument:modifyElements(tag, callback)
|
||||
local element_to_match = "(<%s" .. string.format("*%s [^>]*>)", tag)
|
||||
self.html = string.gsub(self.html, element_to_match, callback)
|
||||
end
|
||||
|
||||
return HtmlDocument
|
@ -0,0 +1,29 @@
|
||||
local Element = {
|
||||
html = nil
|
||||
}
|
||||
|
||||
function Element:new(html)
|
||||
o = {}
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
|
||||
o.html = html
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function Element:src()
|
||||
return self:attributeValue("src")
|
||||
end
|
||||
|
||||
function Element:attributeValue(attribute)
|
||||
local attribute_to_match = string.format([[%s="([^"]*)"]], attribute)
|
||||
local value = self.html:match(attribute_to_match)
|
||||
if not value or value == ""
|
||||
then
|
||||
return false, string.format("Error: no %s value in this element", attribute)
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
return Element
|
@ -0,0 +1,19 @@
|
||||
local Template = {}
|
||||
|
||||
Template.HTML = [[<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>%s</title>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>%s</h1>
|
||||
</header>
|
||||
<main>
|
||||
%s
|
||||
</main>
|
||||
<footer>
|
||||
</footer>
|
||||
</body>
|
||||
</html>]]
|
||||
|
||||
return Template
|
@ -0,0 +1,51 @@
|
||||
local util = require("util")
|
||||
local Resource = require("libs/gazette/resources/resource")
|
||||
|
||||
local Image = Resource:extend{
|
||||
filename = nil,
|
||||
url = nil,
|
||||
payload = nil
|
||||
}
|
||||
|
||||
function Image:new(o)
|
||||
o = o or {}
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
|
||||
if not o.url
|
||||
then
|
||||
return false
|
||||
end
|
||||
|
||||
if not o.payload
|
||||
then
|
||||
local payload, err = o:fetchUrlContent(o.url)
|
||||
if err
|
||||
then
|
||||
return false, err
|
||||
else
|
||||
o.payload = payload
|
||||
end
|
||||
end
|
||||
|
||||
if not o.filename
|
||||
then
|
||||
o.filename = o:filenameFromUrl(o.url)
|
||||
end
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function Image:getData()
|
||||
return self.payload
|
||||
end
|
||||
|
||||
function Image:filenameFromUrl(url)
|
||||
local _, filename = util.splitFilePathName(url)
|
||||
local safe_filename = util.getSafeFilename(filename)
|
||||
return safe_filename
|
||||
end
|
||||
|
||||
return Image
|
||||
|
||||
-- string.match(o.url, "((data:image/[a-z]+;base64,)(%w+))")
|
@ -0,0 +1,44 @@
|
||||
local HttpError = require("libs/http/httperror")
|
||||
local RequestFactory = require("libs/http/requestfactory")
|
||||
|
||||
local Resource = {
|
||||
data = nil,
|
||||
filename = nil
|
||||
}
|
||||
|
||||
function Resource:new(o)
|
||||
o = o or {}
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
Resource.extend = Resource.new
|
||||
|
||||
function Resource:getData()
|
||||
return self.data
|
||||
end
|
||||
|
||||
function Resource:fetchUrlContent(url)
|
||||
local request, err = RequestFactory:makeGetRequest(url, {})
|
||||
if not request
|
||||
then
|
||||
return false, err
|
||||
end
|
||||
|
||||
local response, err = request:send()
|
||||
if err or not response.content
|
||||
then
|
||||
return false, HttpError:provideFromResponse(response)
|
||||
end
|
||||
|
||||
if not response:isOk()
|
||||
then
|
||||
return false, HttpError:provideFromResponse(response)
|
||||
end
|
||||
|
||||
return response.content
|
||||
end
|
||||
|
||||
return Resource
|
@ -0,0 +1,109 @@
|
||||
local Resource = require("libs/gazette/resources/resource")
|
||||
local HtmlDocument = require("libs/gazette/resources/htmldocument")
|
||||
local Image = require("libs/gazette/resources/image")
|
||||
local ItemFactory = require("libs/gazette/factories/itemfactory")
|
||||
local RequestFactory = require("libs/http/requestfactory")
|
||||
local util = require("util")
|
||||
local socket_url = require("socket.url")
|
||||
|
||||
local WebPage = Resource:extend {
|
||||
url = nil,
|
||||
base_url = nil,
|
||||
title = nil,
|
||||
items = nil,
|
||||
resources = nil,
|
||||
}
|
||||
|
||||
function WebPage:new(o)
|
||||
o = o or {}
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
|
||||
if not o.url
|
||||
then
|
||||
return false
|
||||
end
|
||||
|
||||
if not o.html
|
||||
then
|
||||
local content, err = o:fetchUrlContent(o.url)
|
||||
if err
|
||||
then
|
||||
return false, err
|
||||
else
|
||||
o.html = content
|
||||
end
|
||||
end
|
||||
|
||||
o.base_url = socket_url.parse(o.url)
|
||||
o.resources = {}
|
||||
o.items = {}
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function WebPage:build()
|
||||
self:createResources()
|
||||
self:createItems()
|
||||
end
|
||||
|
||||
function WebPage:createResources()
|
||||
local html_document = HtmlDocument:new{
|
||||
url = self.url or nil,
|
||||
html = self.html or nil,
|
||||
title = self.title or nil
|
||||
}
|
||||
table.insert(self.resources, html_document)
|
||||
|
||||
local images = self:downloadImages(
|
||||
html_document:findImageElements()
|
||||
)
|
||||
html_document:modifyElements("img", function(element)
|
||||
local image = images[element]
|
||||
if not image
|
||||
then
|
||||
return element
|
||||
end
|
||||
-- local path = string.format("%s/%s", html_document.filename, image.filename)
|
||||
return string.format([[<img src="%s"/>]], image.filename)
|
||||
end)
|
||||
for _, image in pairs(images) do
|
||||
table.insert(self.resources, image)
|
||||
end
|
||||
end
|
||||
|
||||
function WebPage:createItems()
|
||||
for _, resource in ipairs(self.resources) do
|
||||
local item, err = ItemFactory:makeItemFromResource(resource)
|
||||
if err
|
||||
then
|
||||
goto continue
|
||||
end
|
||||
table.insert(self.items, item)
|
||||
::continue::
|
||||
end
|
||||
end
|
||||
|
||||
function WebPage:downloadImages(image_elements)
|
||||
local image_items = {}
|
||||
for _, element in ipairs(image_elements) do
|
||||
local src = element:src()
|
||||
if not src
|
||||
then
|
||||
goto continue
|
||||
end
|
||||
|
||||
local url = socket_url.absolute(self.base_url, src)
|
||||
local image, err = Image:new{
|
||||
url = url
|
||||
}
|
||||
if image
|
||||
then
|
||||
image_items[element.html] = image
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
return image_items
|
||||
end
|
||||
|
||||
return WebPage
|
@ -0,0 +1,17 @@
|
||||
local ResourceIterator = {
|
||||
|
||||
}
|
||||
|
||||
function ResourceIterator:new(webpage)
|
||||
local i = 0
|
||||
local item_count = #webpage.items
|
||||
return function()
|
||||
i = i + 1
|
||||
if i <= item_count
|
||||
then
|
||||
return webpage.items[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return ResourceIterator
|
@ -0,0 +1,34 @@
|
||||
local _ = require("gettext")
|
||||
local T = require("ffi/util").template
|
||||
|
||||
local HttpError = {
|
||||
RESPONSE_NONSPECIFIC_ERROR = _("There was an error. That's all I know."),
|
||||
REQUEST_UNSUPPORTED_SCHEME = _("Scheme not supported."),
|
||||
REQUEST_INCOMPLETE = _("Request couldn't complete. Code %1."),
|
||||
REQUEST_PAGE_NOT_FOUND = _("Page not found."),
|
||||
RESPONSE_HAS_NO_CONTENT = _("No content found in response."),
|
||||
}
|
||||
|
||||
function HttpError:extend(o)
|
||||
o = o or {}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function HttpError:provideFromResponse(response)
|
||||
if not response:hasCompleted()
|
||||
then
|
||||
return T(HttpError.REQUEST_INCOMPLETE, response.code)
|
||||
elseif response.code == 404 or not response:isHostKnown()
|
||||
then
|
||||
return HttpError.REQUEST_PAGE_NOT_FOUND
|
||||
elseif not response:hasContent()
|
||||
then
|
||||
return HttpError.RESPONSE_HAS_NO_CONTENT
|
||||
end
|
||||
return HttpError.RESPONSE_NONSPECIFIC_ERROR
|
||||
end
|
||||
|
||||
return HttpError
|
@ -0,0 +1,59 @@
|
||||
local http = require("socket.http")
|
||||
local socketutil = require("socketutil")
|
||||
local socket = require("socket")
|
||||
local ltn12 = require("ltn12")
|
||||
local logger = require("logger")
|
||||
|
||||
local ResponseFactory = require("libs/http/responsefactory")
|
||||
|
||||
local DEFAULT_TIMEOUT = 30
|
||||
local DEFAULT_MAXTIME = 30
|
||||
local DEFAULT_REDIRECTS = 5
|
||||
|
||||
local Request = {
|
||||
url = nil,
|
||||
method = nil,
|
||||
maxtime = DEFAULT_MAXTIME,
|
||||
timeout = DEFAULT_TIMEOUT,
|
||||
redirects = DEFAULT_REDIRECTS,
|
||||
sink = {},
|
||||
}
|
||||
|
||||
Request.method = {
|
||||
get = "GET",
|
||||
post = "POST",
|
||||
}
|
||||
|
||||
Request.scheme = {
|
||||
http = "HTTP",
|
||||
https = "HTTPS"
|
||||
}
|
||||
|
||||
Request.default = {
|
||||
timeout = DEFAULT_TIMEOUT,
|
||||
maxtime = DEFAULT_MAXTIME,
|
||||
redirects = DEFAULT_REDIRECTS,
|
||||
}
|
||||
|
||||
function Request:new(o)
|
||||
o = o or {}
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function Request:send()
|
||||
self.sink = {}
|
||||
socketutil:set_timeout(self.timeout, self.maxtime)
|
||||
local code, headers, status = socket.skip(1, http.request({
|
||||
url = self.url,
|
||||
method = self.method,
|
||||
sink = self.maxtime and socketutil.table_sink(self.sink) or ltn12.sink.table(self.sink)
|
||||
}))
|
||||
local content = table.concat(self.sink)
|
||||
socketutil:reset_timeout()
|
||||
return ResponseFactory:make(code, headers, status, content)
|
||||
end
|
||||
|
||||
return Request
|
@ -0,0 +1,26 @@
|
||||
local Request = require("libs/http/request")
|
||||
local HttpError = require("libs/http/httperror")
|
||||
local socket_url = require("socket.url")
|
||||
|
||||
local RequestFactory = {
|
||||
|
||||
}
|
||||
|
||||
function RequestFactory:makeGetRequest(url, config)
|
||||
|
||||
local parsed_url = socket_url.parse(url)
|
||||
|
||||
if not Request.scheme[parsed_url["scheme"]]
|
||||
then
|
||||
return false, HttpError.REQUEST_UNSUPPORTED_SCHEME
|
||||
end
|
||||
|
||||
return Request:new{
|
||||
url = url,
|
||||
timeout = config.timeout,
|
||||
maxtime = config.maxtime,
|
||||
method = Request.method.get
|
||||
}
|
||||
end
|
||||
|
||||
return RequestFactory
|
@ -0,0 +1,150 @@
|
||||
local socketutil = require("socketutil")
|
||||
local socket_url = require("socket.url")
|
||||
|
||||
local Response = {
|
||||
code = nil,
|
||||
headers = nil,
|
||||
status = nil,
|
||||
url = nil,
|
||||
content = nil,
|
||||
}
|
||||
|
||||
function Response:new(o)
|
||||
o = o or {}
|
||||
self.__index = self
|
||||
setmetatable(o, self)
|
||||
|
||||
if o:hasHeaders()
|
||||
then
|
||||
o:setUrlFromHeaders()
|
||||
end
|
||||
|
||||
if not o:isHostKnown()
|
||||
then
|
||||
o.code = 404
|
||||
end
|
||||
|
||||
if o:isXml() and
|
||||
o:hasContent()
|
||||
then
|
||||
o.content = o:decodeXml(o.content)
|
||||
end
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
Response.extend = Response.new
|
||||
|
||||
function Response:canBeConsumed()
|
||||
if self:hasCompleted() and
|
||||
self:hasHeaders()
|
||||
then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function Response:hasRedirected()
|
||||
if type(self.code) == "number" and
|
||||
self.code > 299 and
|
||||
self.code < 400
|
||||
then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function Response:isOk()
|
||||
if type(self.code) == "number" and
|
||||
self.code == 200
|
||||
then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function Response:hasCompleted()
|
||||
if not self.code or
|
||||
self.code == socketutil.TIMEOUT_CODE or
|
||||
self.code == socketutil.SSL_HANDSHAKE_CODE or
|
||||
self.code == socketutil.SINK_TIMEOUT_CODE
|
||||
then
|
||||
return false
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function Response:hasHeaders()
|
||||
if self.headers == nil or
|
||||
not self.headers["content-type"]
|
||||
then
|
||||
return false
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function Response:hasContent()
|
||||
if self.content == nil or
|
||||
not type(self.content) == "string"
|
||||
-- tonumber(self.headers["content-length"]) ~= #self.content)
|
||||
-- It would be ideal to check the content's length, but not all
|
||||
-- requests supply that value.
|
||||
then
|
||||
return false
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function Response:isHostKnown()
|
||||
if self.code == "host or service not provided, or not known"
|
||||
then
|
||||
return false
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function Response:isXml()
|
||||
if self:hasHeaders() and
|
||||
string.match(self.headers["content-type"], "(.*)xml(.*)")
|
||||
then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function Response:setUrlFromHeaders()
|
||||
local url = self.headers.location
|
||||
|
||||
if url
|
||||
then
|
||||
local parsed_url = socket_url.parse(url)
|
||||
self.url = socket_url.build(parsed_url)
|
||||
end
|
||||
end
|
||||
|
||||
function Response:decodeXml(xml_to_decode)
|
||||
local xml2lua = require("../libs/xml2lua/xml2lua")
|
||||
local handler = require("../libs/xml2lua/xmlhandler.tree"):new()
|
||||
local parser = xml2lua.parser(handler)
|
||||
|
||||
local ok, error_message = pcall(function()
|
||||
parser:parse(xml_to_decode)
|
||||
end)
|
||||
if not ok then
|
||||
-- when this method returns, the response's content attribute
|
||||
-- will be set to nil, meaning the response will be considered
|
||||
-- without content.
|
||||
return nil
|
||||
end
|
||||
return handler.root
|
||||
end
|
||||
|
||||
return Response
|
@ -0,0 +1,16 @@
|
||||
local Response = require("libs/http/response")
|
||||
|
||||
local ResponseFactory = {
|
||||
|
||||
}
|
||||
|
||||
function ResponseFactory:make(code, headers, status, content)
|
||||
return Response:new{
|
||||
code = code,
|
||||
headers = headers,
|
||||
status = status,
|
||||
content = content
|
||||
}
|
||||
end
|
||||
|
||||
return ResponseFactory
|
@ -0,0 +1,434 @@
|
||||
--- @module Class providing the actual XML parser.
|
||||
-- Available options are:
|
||||
-- * stripWS
|
||||
-- Strip non-significant whitespace (leading/trailing)
|
||||
-- and do not generate events for empty text elements
|
||||
--
|
||||
-- * expandEntities
|
||||
-- Expand entities (standard entities + single char
|
||||
-- numeric entities only currently - could be extended
|
||||
-- at runtime if suitable DTD parser added elements
|
||||
-- to table (see obj._ENTITIES). May also be possible
|
||||
-- to expand multibyre entities for UTF-8 only
|
||||
--
|
||||
-- * errorHandler
|
||||
-- Custom error handler function
|
||||
--
|
||||
-- NOTE: Boolean options must be set to 'nil' not '0'
|
||||
|
||||
---Converts the decimal code of a character to its corresponding char
|
||||
--if it's a graphical char, otherwise, returns the HTML ISO code
|
||||
--for that decimal value in the format &#code
|
||||
--@param code the decimal value to convert to its respective character
|
||||
local function decimalToHtmlChar(code)
|
||||
local num = tonumber(code)
|
||||
if num >= 0 and num < 256 then
|
||||
return string.char(num)
|
||||
end
|
||||
|
||||
return "&#"..code..";"
|
||||
end
|
||||
|
||||
---Converts the hexadecimal code of a character to its corresponding char
|
||||
--if it's a graphical char, otherwise, returns the HTML ISO code
|
||||
--for that hexadecimal value in the format ode
|
||||
--@param code the hexadecimal value to convert to its respective character
|
||||
local function hexadecimalToHtmlChar(code)
|
||||
local num = tonumber(code, 16)
|
||||
if num >= 0 and num < 256 then
|
||||
return string.char(num)
|
||||
end
|
||||
|
||||
return "&#x"..code..";"
|
||||
end
|
||||
|
||||
local XmlParser = {
|
||||
-- Private attribures/functions
|
||||
_XML = '^([^<]*)<(%/?)([^>]-)(%/?)>',
|
||||
_ATTR1 = '([%w-:_]+)%s*=%s*"(.-)"',
|
||||
_ATTR2 = '([%w-:_]+)%s*=%s*\'(.-)\'',
|
||||
_CDATA = '<%!%[CDATA%[(.-)%]%]>',
|
||||
_PI = '<%?(.-)%?>',
|
||||
_COMMENT = '<!%-%-(.-)%-%->',
|
||||
_TAG = '^(.-)%s.*',
|
||||
_LEADINGWS = '^%s+',
|
||||
_TRAILINGWS = '%s+$',
|
||||
_WS = '^%s*$',
|
||||
_DTD1 = '<!DOCTYPE%s+(.-)%s+(SYSTEM)%s+["\'](.-)["\']%s*(%b[])%s*>',
|
||||
_DTD2 = '<!DOCTYPE%s+(.-)%s+(PUBLIC)%s+["\'](.-)["\']%s+["\'](.-)["\']%s*(%b[])%s*>',
|
||||
--_DTD3 = '<!DOCTYPE%s+(.-)%s*(%b[])%s*>',
|
||||
_DTD3 = '<!DOCTYPE%s.->',
|
||||
_DTD4 = '<!DOCTYPE%s+(.-)%s+(SYSTEM)%s+["\'](.-)["\']%s*>',
|
||||
_DTD5 = '<!DOCTYPE%s+(.-)%s+(PUBLIC)%s+["\'](.-)["\']%s+["\'](.-)["\']%s*>',
|
||||
|
||||
--Matches an attribute with non-closing double quotes (The equal sign is matched non-greedly by using =+?)
|
||||
_ATTRERR1 = '=+?%s*"[^"]*$',
|
||||
--Matches an attribute with non-closing single quotes (The equal sign is matched non-greedly by using =+?)
|
||||
_ATTRERR2 = '=+?%s*\'[^\']*$',
|
||||
--Matches a closing tag such as </person> or the end of a openning tag such as <person>
|
||||
_TAGEXT = '(%/?)>',
|
||||
|
||||
_errstr = {
|
||||
xmlErr = "Error Parsing XML",
|
||||
declErr = "Error Parsing XMLDecl",
|
||||
declStartErr = "XMLDecl not at start of document",
|
||||
declAttrErr = "Invalid XMLDecl attributes",
|
||||
piErr = "Error Parsing Processing Instruction",
|
||||
commentErr = "Error Parsing Comment",
|
||||
cdataErr = "Error Parsing CDATA",
|
||||
dtdErr = "Error Parsing DTD",
|
||||
endTagErr = "End Tag Attributes Invalid",
|
||||
unmatchedTagErr = "Unbalanced Tag",
|
||||
incompleteXmlErr = "Incomplete XML Document",
|
||||
},
|
||||
|
||||
_ENTITIES = {
|
||||
["<"] = "<",
|
||||
[">"] = ">",
|
||||
["&"] = "&",
|
||||
["""] = '"',
|
||||
["'"] = "'",
|
||||
["&#(%d+);"] = decimalToHtmlChar,
|
||||
["&#x(%x+);"] = hexadecimalToHtmlChar,
|
||||
},
|
||||
}
|
||||
|
||||
--- Instantiates a XmlParser object.
|
||||
--@param _handler Handler module to be used to convert the XML string
|
||||
-- to another formats. See the available handlers at the handler directory.
|
||||
-- Usually you get an instance to a handler module using, for instance:
|
||||
-- local handler = require("xmlhandler/tree").
|
||||
--@param _options Options for this XmlParser instance.
|
||||
--@see XmlParser.options
|
||||
function XmlParser.new(_handler, _options)
|
||||
local obj = {
|
||||
handler = _handler,
|
||||
options = _options,
|
||||
_stack = {}
|
||||
}
|
||||
|
||||
setmetatable(obj, XmlParser)
|
||||
obj.__index = XmlParser
|
||||
return obj;
|
||||
end
|
||||
|
||||
---Checks if a function/field exists in a table or in its metatable
|
||||
--@param table the table to check if it has a given function
|
||||
--@param elementName the name of the function/field to check if exists
|
||||
--@return true if the function/field exists, false otherwise
|
||||
local function fexists(table, elementName)
|
||||
if table == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
if table[elementName] == nil then
|
||||
return fexists(getmetatable(table), elementName)
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function err(self, errMsg, pos)
|
||||
if self.options.errorHandler then
|
||||
self.options.errorHandler(errMsg,pos)
|
||||
end
|
||||
end
|
||||
|
||||
--- Removes whitespaces
|
||||
local function stripWS(self, s)
|
||||
if self.options.stripWS then
|
||||
s = string.gsub(s,'^%s+','')
|
||||
s = string.gsub(s,'%s+$','')
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
local function parseEntities(self, s)
|
||||
if self.options.expandEntities then
|
||||
for k,v in pairs(self._ENTITIES) do
|
||||
s = string.gsub(s,k,v)
|
||||
end
|
||||
end
|
||||
|
||||
return s
|
||||
end
|
||||
|
||||
--- Parses a string representing a tag.
|
||||
--@param s String containing tag text
|
||||
--@return a {name, attrs} table
|
||||
-- where name is the name of the tag and attrs
|
||||
-- is a table containing the atributtes of the tag
|
||||
local function parseTag(self, s)
|
||||
local tag = {
|
||||
name = string.gsub(s, self._TAG, '%1'),
|
||||
attrs = {}
|
||||
}
|
||||
|
||||
local parseFunction = function (k, v)
|
||||
tag.attrs[k] = parseEntities(self, v)
|
||||
tag.attrs._ = 1
|
||||
end
|
||||
|
||||
string.gsub(s, self._ATTR1, parseFunction)
|
||||
string.gsub(s, self._ATTR2, parseFunction)
|
||||
|
||||
if tag.attrs._ then
|
||||
tag.attrs._ = nil
|
||||
else
|
||||
tag.attrs = nil
|
||||
end
|
||||
|
||||
return tag
|
||||
end
|
||||
|
||||
local function parseXmlDeclaration(self, xml, f)
|
||||
-- XML Declaration
|
||||
f.match, f.endMatch, f.text = string.find(xml, self._PI, f.pos)
|
||||
if not f.match then
|
||||
err(self, self._errstr.declErr, f.pos)
|
||||
end
|
||||
|
||||
if f.match ~= 1 then
|
||||
-- Must be at start of doc if present
|
||||
err(self, self._errstr.declStartErr, f.pos)
|
||||
end
|
||||
|
||||
local tag = parseTag(self, f.text)
|
||||
-- TODO: Check if attributes are valid
|
||||
-- Check for version (mandatory)
|
||||
if tag.attrs and tag.attrs.version == nil then
|
||||
err(self, self._errstr.declAttrErr, f.pos)
|
||||
end
|
||||
|
||||
if fexists(self.handler, 'decl') then
|
||||
self.handler:decl(tag, f.match, f.endMatch)
|
||||
end
|
||||
|
||||
return tag
|
||||
end
|
||||
|
||||
local function parseXmlProcessingInstruction(self, xml, f)
|
||||
local tag = {}
|
||||
|
||||
-- XML Processing Instruction (PI)
|
||||
f.match, f.endMatch, f.text = string.find(xml, self._PI, f.pos)
|
||||
if not f.match then
|
||||
err(self, self._errstr.piErr, f.pos)
|
||||
end
|
||||
if fexists(self.handler, 'pi') then
|
||||
-- Parse PI attributes & text
|
||||
tag = parseTag(self, f.text)
|
||||
local pi = string.sub(f.text, string.len(tag.name)+1)
|
||||
if pi ~= "" then
|
||||
if tag.attrs then
|
||||
tag.attrs._text = pi
|
||||
else
|
||||
tag.attrs = { _text = pi }
|
||||
end
|
||||
end
|
||||
self.handler:pi(tag, f.match, f.endMatch)
|
||||
end
|
||||
|
||||
return tag
|
||||
end
|
||||
|
||||
local function parseComment(self, xml, f)
|
||||
f.match, f.endMatch, f.text = string.find(xml, self._COMMENT, f.pos)
|
||||
if not f.match then
|
||||
err(self, self._errstr.commentErr, f.pos)
|
||||
end
|
||||
|
||||
if fexists(self.handler, 'comment') then
|
||||
f.text = parseEntities(self, stripWS(self, f.text))
|
||||
self.handler:comment(f.text, next, f.match, f.endMatch)
|
||||
end
|
||||
end
|
||||
|
||||
local function _parseDtd(self, xml, pos)
|
||||
-- match,endMatch,root,type,name,uri,internal
|
||||
local dtdPatterns = {self._DTD1, self._DTD2, self._DTD3, self._DTD4, self._DTD5}
|
||||
|
||||
for _, dtd in pairs(dtdPatterns) do
|
||||
local m,e,r,t,n,u,i = string.find(xml, dtd, pos)
|
||||
if m then
|
||||
return m, e, {_root=r, _type=t, _name=n, _uri=u, _internal=i}
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function parseDtd(self, xml, f)
|
||||
f.match, f.endMatch, _ = _parseDtd(self, xml, f.pos)
|
||||
if not f.match then
|
||||
err(self, self._errstr.dtdErr, f.pos)
|
||||
end
|
||||
|
||||
if fexists(self.handler, 'dtd') then
|
||||
local tag = {name="DOCTYPE", value=string.sub(xml, f.match+10, f.endMatch-1)}
|
||||
self.handler:dtd(tag, f.match, f.endMatch)
|
||||
end
|
||||
end
|
||||
|
||||
local function parseCdata(self, xml, f)
|
||||
f.match, f.endMatch, f.text = string.find(xml, self._CDATA, f.pos)
|
||||
if not f.match then
|
||||
err(self, self._errstr.cdataErr, f.pos)
|
||||
end
|
||||
|
||||
if fexists(self.handler, 'cdata') then
|
||||
self.handler:cdata(f.text, nil, f.match, f.endMatch)
|
||||
end
|
||||
end
|
||||
|
||||
--- Parse a Normal tag
|
||||
-- Need check for embedded '>' in attribute value and extend
|
||||
-- match recursively if necessary eg. <tag attr="123>456">
|
||||
local function parseNormalTag(self, xml, f)
|
||||
--Check for errors
|
||||
while 1 do
|
||||
--If there isn't an attribute without closing quotes (single or double quotes)
|
||||
--then breaks to follow the normal processing of the tag.
|
||||
--Otherwise, try to find where the quotes close.
|
||||
f.errStart, f.errEnd = string.find(f.tagstr, self._ATTRERR1)
|
||||
|
||||
if f.errEnd == nil then
|
||||
f.errStart, f.errEnd = string.find(f.tagstr, self._ATTRERR2)
|
||||
if f.errEnd == nil then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
f.extStart, f.extEnd, f.endt2 = string.find(xml, self._TAGEXT, f.endMatch+1)
|
||||
f.tagstr = f.tagstr .. string.sub(xml, f.endMatch, f.extEnd-1)
|
||||
if not f.match then
|
||||
err(self, self._errstr.xmlErr, f.pos)
|
||||
end
|
||||
f.endMatch = f.extEnd
|
||||
end
|
||||
|
||||
-- Extract tag name and attrs
|
||||
local tag = parseTag(self, f.tagstr)
|
||||
|
||||
if (f.endt1=="/") then
|
||||
if fexists(self.handler, 'endtag') then
|
||||
if tag.attrs then
|
||||
-- Shouldn't have any attributes in endtag
|
||||
err(self, string.format("%s (/%s)", self._errstr.endTagErr, tag.name), f.pos)
|
||||
end
|
||||
if table.remove(self._stack) ~= tag.name then
|
||||
err(self, string.format("%s (/%s)", self._errstr.unmatchedTagErr, tag.name), f.pos)
|
||||
end
|
||||
self.handler:endtag(tag, f.match, f.endMatch)
|
||||
end
|
||||
else
|
||||
table.insert(self._stack, tag.name)
|
||||
|
||||
if fexists(self.handler, 'starttag') then
|
||||
self.handler:starttag(tag, f.match, f.endMatch)
|
||||
end
|
||||
|
||||
-- Self-Closing Tag
|
||||
if (f.endt2=="/") then
|
||||
table.remove(self._stack)
|
||||
if fexists(self.handler, 'endtag') then
|
||||
self.handler:endtag(tag, f.match, f.endMatch)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return tag
|
||||
end
|
||||
|
||||
local function parseTagType(self, xml, f)
|
||||
-- Test for tag type
|
||||
if string.find(string.sub(f.tagstr, 1, 5), "?xml%s") then
|
||||
parseXmlDeclaration(self, xml, f)
|
||||
elseif string.sub(f.tagstr, 1, 1) == "?" then
|
||||
parseXmlProcessingInstruction(self, xml, f)
|
||||
elseif string.sub(f.tagstr, 1, 3) == "!--" then
|
||||
parseComment(self, xml, f)
|
||||
elseif string.sub(f.tagstr, 1, 8) == "!DOCTYPE" then
|
||||
parseDtd(self, xml, f)
|
||||
elseif string.sub(f.tagstr, 1, 8) == "![CDATA[" then
|
||||
parseCdata(self, xml, f)
|
||||
else
|
||||
parseNormalTag(self, xml, f)
|
||||
end
|
||||
end
|
||||
|
||||
--- Get next tag (first pass - fix exceptions below).
|
||||
--@return true if the next tag could be got, false otherwise
|
||||
local function getNextTag(self, xml, f)
|
||||
f.match, f.endMatch, f.text, f.endt1, f.tagstr, f.endt2 = string.find(xml, self._XML, f.pos)
|
||||
if not f.match then
|
||||
if string.find(xml, self._WS, f.pos) then
|
||||
-- No more text - check document complete
|
||||
if #self._stack ~= 0 then
|
||||
err(self, self._errstr.incompleteXmlErr, f.pos)
|
||||
else
|
||||
return false
|
||||
end
|
||||
else
|
||||
-- Unparsable text
|
||||
err(self, self._errstr.xmlErr, f.pos)
|
||||
end
|
||||
end
|
||||
|
||||
f.text = f.text or ''
|
||||
f.tagstr = f.tagstr or ''
|
||||
f.match = f.match or 0
|
||||
|
||||
return f.endMatch ~= nil
|
||||
end
|
||||
|
||||
--Main function which starts the XML parsing process
|
||||
--@param xml the XML string to parse
|
||||
--@param parseAttributes indicates if tag attributes should be parsed or not.
|
||||
-- If omitted, the default value is true.
|
||||
function XmlParser:parse(xml, parseAttributes)
|
||||
if type(self) ~= "table" or getmetatable(self) ~= XmlParser then
|
||||
error("You must call xmlparser:parse(parameters) instead of xmlparser.parse(parameters)")
|
||||
end
|
||||
|
||||
if parseAttributes == nil then
|
||||
parseAttributes = true
|
||||
end
|
||||
|
||||
self.handler.parseAttributes = parseAttributes
|
||||
|
||||
--Stores string.find results and parameters
|
||||
--and other auxiliar variables
|
||||
local f = {
|
||||
--string.find return
|
||||
match = 0,
|
||||
endMatch = 0,
|
||||
-- text, end1, tagstr, end2,
|
||||
|
||||
--string.find parameters and auxiliar variables
|
||||
pos = 1,
|
||||
-- startText, endText,
|
||||
-- errStart, errEnd, extStart, extEnd,
|
||||
}
|
||||
|
||||
while f.match do
|
||||
if not getNextTag(self, xml, f) then
|
||||
break
|
||||
end
|
||||
|
||||
-- Handle leading text
|
||||
f.startText = f.match
|
||||
f.endText = f.match + string.len(f.text) - 1
|
||||
f.match = f.match + string.len(f.text)
|
||||
f.text = parseEntities(self, stripWS(self, f.text))
|
||||
if f.text ~= "" and fexists(self.handler, 'text') then
|
||||
self.handler:text(f.text, nil, f.match, f.endText)
|
||||
end
|
||||
|
||||
parseTagType(self, xml, f)
|
||||
f.pos = f.endMatch + 1
|
||||
end
|
||||
end
|
||||
|
||||
XmlParser.__index = XmlParser
|
||||
return XmlParser
|
@ -0,0 +1,248 @@
|
||||
--- @module Module providing a non-validating XML stream parser in Lua.
|
||||
--
|
||||
-- Features:
|
||||
-- =========
|
||||
--
|
||||
-- * Tokenises well-formed XML (relatively robustly)
|
||||
-- * Flexible handler based event API (see below)
|
||||
-- * Parses all XML Infoset elements - ie.
|
||||
-- - Tags
|
||||
-- - Text
|
||||
-- - Comments
|
||||
-- - CDATA
|
||||
-- - XML Decl
|
||||
-- - Processing Instructions
|
||||
-- - DOCTYPE declarations
|
||||
-- * Provides limited well-formedness checking
|
||||
-- (checks for basic syntax & balanced tags only)
|
||||
-- * Flexible whitespace handling (selectable)
|
||||
-- * Entity Handling (selectable)
|
||||
--
|
||||
-- Limitations:
|
||||
-- ============
|
||||
--
|
||||
-- * Non-validating
|
||||
-- * No charset handling
|
||||
-- * No namespace support
|
||||
-- * Shallow well-formedness checking only (fails
|
||||
-- to detect most semantic errors)
|
||||
--
|
||||
-- API:
|
||||
-- ====
|
||||
--
|
||||
-- The parser provides a partially object-oriented API with
|
||||
-- functionality split into tokeniser and handler components.
|
||||
--
|
||||
-- The handler instance is passed to the tokeniser and receives
|
||||
-- callbacks for each XML element processed (if a suitable handler
|
||||
-- function is defined). The API is conceptually similar to the
|
||||
-- SAX API but implemented differently.
|
||||
--
|
||||
-- XML data is passed to the parser instance through the 'parse'
|
||||
-- method (Note: must be passed a single string currently)
|
||||
--
|
||||
-- License:
|
||||
-- ========
|
||||
--G
|
||||
-- This code is freely distributable under the terms of the [MIT license](LICENSE).
|
||||
--
|
||||
--
|
||||
--@author Paul Chakravarti (paulc@passtheaardvark.com)
|
||||
--@author Manoel Campos da Silva Filho
|
||||
local xml2lua = {_VERSION = "1.5-2"}
|
||||
local XmlParser = require("libs/xml2lua/XmlParser")
|
||||
|
||||
---Recursivelly prints a table in an easy-to-ready format
|
||||
--@param tb The table to be printed
|
||||
--@param level the indentation level to start with
|
||||
local function printableInternal(tb, level)
|
||||
if tb == nil then
|
||||
return
|
||||
end
|
||||
|
||||
level = level or 1
|
||||
local spaces = string.rep(' ', level*2)
|
||||
for k,v in pairs(tb) do
|
||||
if type(v) == "table" then
|
||||
print(spaces .. k)
|
||||
printableInternal(v, level+1)
|
||||
else
|
||||
print(spaces .. k..'='..v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Instantiates a XmlParser object to parse a XML string
|
||||
--@param handler Handler module to be used to convert the XML string
|
||||
--to another formats. See the available handlers at the handler directory.
|
||||
-- Usually you get an instance to a handler module using, for instance:
|
||||
-- local handler = require("xmlhandler/tree").
|
||||
--@return a XmlParser object used to parse the XML
|
||||
--@see XmlParser
|
||||
function xml2lua.parser(handler)
|
||||
if handler == xml2lua then
|
||||
error("You must call xml2lua.parse(handler) instead of xml2lua:parse(handler)")
|
||||
end
|
||||
|
||||
local options = {
|
||||
--Indicates if whitespaces should be striped or not
|
||||
stripWS = 1,
|
||||
expandEntities = 1,
|
||||
errorHandler = function(errMsg, pos)
|
||||
error(string.format("%s [char=%d]\n", errMsg or "Parse Error", pos))
|
||||
end
|
||||
}
|
||||
|
||||
return XmlParser.new(handler, options)
|
||||
end
|
||||
|
||||
---Recursivelly prints a table in an easy-to-ready format
|
||||
--@param tb The table to be printed
|
||||
function xml2lua.printable(tb)
|
||||
printableInternal(tb)
|
||||
end
|
||||
|
||||
---Handler to generate a string prepresentation of a table
|
||||
--Convenience function for printHandler (Does not support recursive tables).
|
||||
--@param t Table to be parsed
|
||||
--@return a string representation of the table
|
||||
function xml2lua.toString(t)
|
||||
local sep = ''
|
||||
local res = ''
|
||||
if type(t) ~= 'table' then
|
||||
return t
|
||||
end
|
||||
|
||||
for k,v in pairs(t) do
|
||||
if type(v) == 'table' then
|
||||
v = xml2lua.toString(v)
|
||||
end
|
||||
res = res .. sep .. string.format("%s=%s", k, v)
|
||||
sep = ','
|
||||
end
|
||||
res = '{'..res..'}'
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
--- Loads an XML file from a specified path
|
||||
-- @param xmlFilePath the path for the XML file to load
|
||||
-- @return the XML loaded file content
|
||||
function xml2lua.loadFile(xmlFilePath)
|
||||
local f, e = io.open(xmlFilePath, "r")
|
||||
if f then
|
||||
--Gets the entire file content and stores into a string
|
||||
local content = f:read("*a")
|
||||
f:close()
|
||||
return content
|
||||
end
|
||||
|
||||
error(e)
|
||||
end
|
||||
|
||||
---Gets an _attr element from a table that represents the attributes of an XML tag,
|
||||
--and generates a XML String representing the attibutes to be inserted
|
||||
--into the openning tag of the XML
|
||||
--
|
||||
--@param attrTable table from where the _attr field will be got
|
||||
--@return a XML String representation of the tag attributes
|
||||
local function attrToXml(attrTable)
|
||||
local s = ""
|
||||
attrTable = attrTable or {}
|
||||
|
||||
for k, v in pairs(attrTable) do
|
||||
s = s .. " " .. k .. "=" .. '"' .. v .. '"'
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
---Gets the first key of a given table
|
||||
local function getFirstKey(tb)
|
||||
if type(tb) == "table" then
|
||||
for k, _ in pairs(tb) do
|
||||
return k
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
return tb
|
||||
end
|
||||
|
||||
--- Parses a given entry in a lua table
|
||||
-- and inserts it as a XML string into a destination table.
|
||||
-- Entries in such a destination table will be concatenated to generated
|
||||
-- the final XML string from the origin table.
|
||||
-- @param xmltb the destination table where the XML string from the parsed key will be inserted
|
||||
-- @param tagName the name of the table field that will be used as XML tag name
|
||||
-- @param fieldValue a field from the lua table to be recursively parsed to XML or a primitive value that will be enclosed in a tag name
|
||||
-- @param level a int value used to include indentation in the generated XML from the table key
|
||||
local function parseTableKeyToXml(xmltb, tagName, fieldValue, level)
|
||||
local spaces = string.rep(' ', level*2)
|
||||
|
||||
local strValue, attrsStr = "", ""
|
||||
if type(fieldValue) == "table" then
|
||||
attrsStr = attrToXml(fieldValue._attr)
|
||||
fieldValue._attr = nil
|
||||
--If after removing the _attr field there is just one element inside it,
|
||||
--the tag was enclosing a single primitive value instead of other inner tags.
|
||||
strValue = #fieldValue == 1 and spaces..tostring(fieldValue[1]) or xml2lua.toXml(fieldValue, tagName, level+1)
|
||||
strValue = '\n'..strValue..'\n'..spaces
|
||||
else
|
||||
strValue = tostring(fieldValue)
|
||||
end
|
||||
|
||||
table.insert(xmltb, spaces..'<'..tagName.. attrsStr ..'>'..strValue..'</'..tagName..'>')
|
||||
end
|
||||
|
||||
---Converts a Lua table to a XML String representation.
|
||||
--@param tb Table to be converted to XML
|
||||
--@param tableName Name of the table variable given to this function,
|
||||
-- to be used as the root tag. If a value is not provided
|
||||
-- no root tag will be created.
|
||||
--@param level Only used internally, when the function is called recursively to print indentation
|
||||
--
|
||||
--@return a String representing the table content in XML
|
||||
function xml2lua.toXml(tb, tableName, level)
|
||||
level = level or 1
|
||||
local firstLevel = level
|
||||
tableName = tableName or ''
|
||||
local xmltb = (tableName ~= '' and level == 1) and {'<'..tableName..'>'} or {}
|
||||
|
||||
for k, v in pairs(tb) do
|
||||
if type(v) == 'table' then
|
||||
-- If the key is a number, the given table is an array and the value is an element inside that array.
|
||||
-- In this case, the name of the array is used as tag name for each element.
|
||||
-- So, we are parsing an array of objects, not an array of primitives.
|
||||
if type(k) == 'number' then
|
||||
parseTableKeyToXml(xmltb, tableName, v, level)
|
||||
else
|
||||
level = level + 1
|
||||
-- If the type of the first key of the value inside the table
|
||||
-- is a number, it means we have a HashTable-like structure,
|
||||
-- in this case with keys as strings and values as arrays.
|
||||
if type(getFirstKey(v)) == 'number' then
|
||||
parseTableKeyToXml(xmltb, k, v, level)
|
||||
else
|
||||
-- Otherwise, the "HashTable" values are objects
|
||||
parseTableKeyToXml(xmltb, k, v, level)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- When values are primitives:
|
||||
-- If the type of the key is number, the value is an element from an array.
|
||||
-- In this case, uses the array name as the tag name.
|
||||
if type(k) == 'number' then
|
||||
k = tableName
|
||||
end
|
||||
parseTableKeyToXml(xmltb, k, v, level)
|
||||
end
|
||||
end
|
||||
|
||||
if tableName ~= '' and firstLevel == 1 then
|
||||
table.insert(xmltb, '</'..tableName..'>\n')
|
||||
end
|
||||
|
||||
return table.concat(xmltb, '\n')
|
||||
end
|
||||
|
||||
return xml2lua
|
@ -0,0 +1,155 @@
|
||||
local function init()
|
||||
return {
|
||||
options = {commentNode=1, piNode=1, dtdNode=1, declNode=1},
|
||||
current = { _children = {}, _type = "ROOT" },
|
||||
_stack = {}
|
||||
}
|
||||
end
|
||||
|
||||
--- @module Handler to generate a DOM-like node tree structure with
|
||||
-- a single ROOT node parent - each node is a table comprising
|
||||
-- the fields below.
|
||||
--
|
||||
-- node = { _name = <Element Name>,
|
||||
-- _type = ROOT|ELEMENT|TEXT|COMMENT|PI|DECL|DTD,
|
||||
-- _attr = { Node attributes - see callback API },
|
||||
-- _parent = <Parent Node>
|
||||
-- _children = { List of child nodes - ROOT/NODE only }
|
||||
-- }
|
||||
-- where:
|
||||
-- - PI = XML Processing Instruction tag.
|
||||
-- - DECL = XML declaration tag
|
||||
--
|
||||
-- The dom structure is capable of representing any valid XML document
|
||||
--
|
||||
-- Options
|
||||
-- =======
|
||||
-- options.(comment|pi|dtd|decl)Node = bool
|
||||
-- - Include/exclude given node types
|
||||
--
|
||||
-- License:
|
||||
-- ========
|
||||
--
|
||||
-- This code is freely distributable under the terms of the [MIT license](LICENSE).
|
||||
--
|
||||
--@author Paul Chakravarti (paulc@passtheaardvark.com)
|
||||
--@author Manoel Campos da Silva Filho
|
||||
local dom = init()
|
||||
|
||||
---Instantiates a new handler object.
|
||||
--Each instance can handle a single XML.
|
||||
--By using such a constructor, you can parse
|
||||
--multiple XML files in the same application.
|
||||
--@return the handler instance
|
||||
function dom:new()
|
||||
local obj = init()
|
||||
|
||||
obj.__index = self
|
||||
setmetatable(obj, self)
|
||||
|
||||
return obj
|
||||
end
|
||||
|
||||
---Parses a start tag.
|
||||
-- @param tag a {name, attrs} table
|
||||
-- where name is the name of the tag and attrs
|
||||
-- is a table containing the atributtes of the tag
|
||||
function dom:starttag(tag)
|
||||
local node = { _type = 'ELEMENT',
|
||||
_name = tag.name,
|
||||
_attr = tag.attrs,
|
||||
_children = {}
|
||||
}
|
||||
|
||||
if self.root == nil then
|
||||
self.root = node
|
||||
end
|
||||
|
||||
table.insert(self._stack, node)
|
||||
|
||||
table.insert(self.current._children, node)
|
||||
self.current = node
|
||||
end
|
||||
|
||||
---Parses an end tag.
|
||||
-- @param tag a {name, attrs} table
|
||||
-- where name is the name of the tag and attrs
|
||||
-- is a table containing the atributtes of the tag
|
||||
function dom:endtag(tag, s)
|
||||
--Table representing the containing tag of the current tag
|
||||
local prev = self._stack[#self._stack]
|
||||
|
||||
if tag.name ~= prev._name then
|
||||
error("XML Error - Unmatched Tag ["..s..":"..tag.name.."]\n")
|
||||
end
|
||||
|
||||
table.remove(self._stack)
|
||||
self.current = self._stack[#self._stack]
|
||||
end
|
||||
|
||||
---Parses a tag content.
|
||||
-- @param text text to process
|
||||
function dom:text(text)
|
||||
local node = { _type = "TEXT",
|
||||
_text = text
|
||||
}
|
||||
table.insert(self.current._children, node)
|
||||
end
|
||||
|
||||
---Parses a comment tag.
|
||||
-- @param text comment text
|
||||
function dom:comment(text)
|
||||
if self.options.commentNode then
|
||||
local node = { _type = "COMMENT",
|
||||
_text = text
|
||||
}
|
||||
table.insert(self.current._children, node)
|
||||
end
|
||||
end
|
||||
|
||||
--- Parses a XML processing instruction (PI) tag
|
||||
-- @param tag a {name, attrs} table
|
||||
-- where name is the name of the tag and attrs
|
||||
-- is a table containing the atributtes of the tag
|
||||
function dom:pi(tag)
|
||||
if self.options.piNode then
|
||||
local node = { _type = "PI",
|
||||
_name = tag.name,
|
||||
_attr = tag.attrs,
|
||||
}
|
||||
table.insert(self.current._children, node)
|
||||
end
|
||||
end
|
||||
|
||||
---Parse the XML declaration line (the line that indicates the XML version).
|
||||
-- @param tag a {name, attrs} table
|
||||
-- where name is the name of the tag and attrs
|
||||
-- is a table containing the atributtes of the tag
|
||||
function dom:decl(tag)
|
||||
if self.options.declNode then
|
||||
local node = { _type = "DECL",
|
||||
_name = tag.name,
|
||||
_attr = tag.attrs,
|
||||
}
|
||||
table.insert(self.current._children, node)
|
||||
end
|
||||
end
|
||||
|
||||
---Parses a DTD tag.
|
||||
-- @param tag a {name, attrs} table
|
||||
-- where name is the name of the tag and attrs
|
||||
-- is a table containing the atributtes of the tag
|
||||
function dom:dtd(tag)
|
||||
if self.options.dtdNode then
|
||||
local node = { _type = "DTD",
|
||||
_name = tag.name,
|
||||
_attr = tag.attrs,
|
||||
}
|
||||
table.insert(self.current._children, node)
|
||||
end
|
||||
end
|
||||
|
||||
---Parses CDATA tag content.
|
||||
dom.cdata = dom.text
|
||||
dom.__index = dom
|
||||
return dom
|
@ -0,0 +1,108 @@
|
||||
---@module Handler to generate a simple event trace which
|
||||
--outputs messages to the terminal during the XML
|
||||
--parsing, usually for debugging purposes.
|
||||
--
|
||||
-- License:
|
||||
-- ========
|
||||
--
|
||||
-- This code is freely distributable under the terms of the [MIT license](LICENSE).
|
||||
--
|
||||
--@author Paul Chakravarti (paulc@passtheaardvark.com)
|
||||
--@author Manoel Campos da Silva Filho
|
||||
local print = {}
|
||||
|
||||
---Parses a start tag.
|
||||
-- @param tag a {name, attrs} table
|
||||
-- where name is the name of the tag and attrs
|
||||
-- is a table containing the atributtes of the tag
|
||||
-- @param s position where the tag starts
|
||||
-- @param e position where the tag ends
|
||||
function print:starttag(tag, s, e)
|
||||
io.write("Start : "..tag.name.."\n")
|
||||
if tag.attrs then
|
||||
for k,v in pairs(tag.attrs) do
|
||||
io.write(string.format(" + %s='%s'\n", k, v))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Parses an end tag.
|
||||
-- @param tag a {name, attrs} table
|
||||
-- where name is the name of the tag and attrs
|
||||
-- is a table containing the atributtes of the tag
|
||||
-- @param s position where the tag starts
|
||||
-- @param e position where the tag ends
|
||||
function print:endtag(tag, s, e)
|
||||
io.write("End : "..tag.name.."\n")
|
||||
end
|
||||
|
||||
---Parses a tag content.
|
||||
-- @param text text to process
|
||||
-- @param s position where the tag starts
|
||||
-- @param e position where the tag ends
|
||||
function print:text(text, s, e)
|
||||
io.write("Text : "..text.."\n")
|
||||
end
|
||||
|
||||
---Parses CDATA tag content.
|
||||
-- @param text CDATA content to be processed
|
||||
-- @param s position where the tag starts
|
||||
-- @param e position where the tag ends
|
||||
function print:cdata(text, s, e)
|
||||
io.write("CDATA : "..text.."\n")
|
||||
end
|
||||
|
||||
---Parses a comment tag.
|
||||
-- @param text comment text
|
||||
-- @param s position where the tag starts
|
||||
-- @param e position where the tag ends
|
||||
function print:comment(text, s, e)
|
||||
io.write("Comment : "..text.."\n")
|
||||
end
|
||||
|
||||
---Parses a DTD tag.
|
||||
-- @param tag a {name, attrs} table
|
||||
-- where name is the name of the tag and attrs
|
||||
-- is a table containing the atributtes of the tag
|
||||
-- @param s position where the tag starts
|
||||
-- @param e position where the tag ends
|
||||
function print:dtd(tag, s, e)
|
||||
io.write("DTD : "..tag.name.."\n")
|
||||
if tag.attrs then
|
||||
for k,v in pairs(tag.attrs) do
|
||||
io.write(string.format(" + %s='%s'\n", k, v))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Parse a XML processing instructions (PI) tag.
|
||||
-- @param tag a {name, attrs} table
|
||||
-- where name is the name of the tag and attrs
|
||||
-- is a table containing the atributtes of the tag
|
||||
-- @param s position where the tag starts
|
||||
-- @param e position where the tag ends
|
||||
function print:pi(tag, s, e)
|
||||
io.write("PI : "..tag.name.."\n")
|
||||
if tag.attrs then
|
||||
for k,v in pairs(tag.attrs) do
|
||||
io. write(string.format(" + %s='%s'\n",k,v))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Parse the XML declaration line (the line that indicates the XML version).
|
||||
-- @param tag a {name, attrs} table
|
||||
-- where name is the name of the tag and attrs
|
||||
-- is a table containing the atributtes of the tag
|
||||
-- @param s position where the tag starts
|
||||
-- @param e position where the tag ends
|
||||
function print:decl(tag, s, e)
|
||||
io.write("XML Decl : "..tag.name.."\n")
|
||||
if tag.attrs then
|
||||
for k,v in pairs(tag.attrs) do
|
||||
io.write(string.format(" + %s='%s'\n", k, v))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return print
|
@ -0,0 +1,170 @@
|
||||
local function init()
|
||||
local obj = {
|
||||
root = {},
|
||||
options = {noreduce = {}}
|
||||
}
|
||||
|
||||
obj._stack = {obj.root}
|
||||
return obj
|
||||
end
|
||||
|
||||
--- @module XML Tree Handler.
|
||||
-- Generates a lua table from an XML content string.
|
||||
-- It is a simplified handler which attempts
|
||||
-- to generate a more 'natural' table based structure which
|
||||
-- supports many common XML formats.
|
||||
--
|
||||
-- The XML tree structure is mapped directly into a recursive
|
||||
-- table structure with node names as keys and child elements
|
||||
-- as either a table of values or directly as a string value
|
||||
-- for text. Where there is only a single child element this
|
||||
-- is inserted as a named key - if there are multiple
|
||||
-- elements these are inserted as a vector (in some cases it
|
||||
-- may be preferable to always insert elements as a vector
|
||||
-- which can be specified on a per element basis in the
|
||||
-- options). Attributes are inserted as a child element with
|
||||
-- a key of '_attr'.
|
||||
--
|
||||
-- Only Tag/Text & CDATA elements are processed - all others
|
||||
-- are ignored.
|
||||
--
|
||||
-- This format has some limitations - primarily
|
||||
--
|
||||
-- * Mixed-Content behaves unpredictably - the relationship
|
||||
-- between text elements and embedded tags is lost and
|
||||
-- multiple levels of mixed content does not work
|
||||
-- * If a leaf element has both a text element and attributes
|
||||
-- then the text must be accessed through a vector (to
|
||||
-- provide a container for the attribute)
|
||||
--
|
||||
-- In general however this format is relatively useful.
|
||||
--
|
||||
-- It is much easier to understand by running some test
|
||||
-- data through 'testxml.lua -simpletree' than to read this)
|
||||
--
|
||||
-- Options
|
||||
-- =======
|
||||
-- options.noreduce = { <tag> = bool,.. }
|
||||
-- - Nodes not to reduce children vector even if only
|
||||
-- one child
|
||||
--
|
||||
-- License:
|
||||
-- ========
|
||||
--
|
||||
-- This code is freely distributable under the terms of the [MIT license](LICENSE).
|
||||
--
|
||||
--@author Paul Chakravarti (paulc@passtheaardvark.com)
|
||||
--@author Manoel Campos da Silva Filho
|
||||
local tree = init()
|
||||
|
||||
---Instantiates a new handler object.
|
||||
--Each instance can handle a single XML.
|
||||
--By using such a constructor, you can parse
|
||||
--multiple XML files in the same application.
|
||||
--@return the handler instance
|
||||
function tree:new()
|
||||
local obj = init()
|
||||
|
||||
obj.__index = self
|
||||
setmetatable(obj, self)
|
||||
|
||||
return obj
|
||||
end
|
||||
|
||||
--- Recursively removes redundant vectors for nodes
|
||||
-- with single child elements
|
||||
function tree:reduce(node, key, parent)
|
||||
for k,v in pairs(node) do
|
||||
if type(v) == 'table' then
|
||||
self:reduce(v,k,node)
|
||||
end
|
||||
end
|
||||
if #node == 1 and not self.options.noreduce[key] and
|
||||
node._attr == nil then
|
||||
parent[key] = node[1]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- If an object is not an array,
|
||||
-- creates an empty array and insert that object as the 1st element.
|
||||
--
|
||||
-- It's a workaround for duplicated XML tags outside an inner tag. Check issue #55 for details.
|
||||
-- It checks if a given tag already exists on the parsing stack.
|
||||
-- In such a case, if that tag is represented as a single element,
|
||||
-- an array is created and that element is inserted on it.
|
||||
-- The existing tag is then replaced by the created array.
|
||||
-- For instance, if we have a tag x = {attr1=1, attr2=2}
|
||||
-- and another x tag is found, the previous entry will be changed to an array
|
||||
-- x = {{attr1=1, attr2=2}}. This way, the duplicated tag will be
|
||||
-- inserted into this array as x = {{attr1=1, attr2=2}, {attr1=3, attr2=4}}
|
||||
-- https://github.com/manoelcampos/xml2lua/issues/55
|
||||
--
|
||||
-- @param obj the object to try to convert to an array
|
||||
-- @return the same object if it's already an array or a new array with the object
|
||||
-- as the 1st element.
|
||||
local function convertObjectToArray(obj)
|
||||
--#obj == 0 verifies if the field is not an array
|
||||
if #obj == 0 then
|
||||
local array = {}
|
||||
table.insert(array, obj)
|
||||
return array
|
||||
end
|
||||
|
||||
return obj
|
||||
end
|
||||
|
||||
---Parses a start tag.
|
||||
-- @param tag a {name, attrs} table
|
||||
-- where name is the name of the tag and attrs
|
||||
-- is a table containing the atributtes of the tag
|
||||
function tree:starttag(tag)
|
||||
local node = {}
|
||||
if self.parseAttributes == true then
|
||||
node._attr=tag.attrs
|
||||
end
|
||||
|
||||
--Table in the stack representing the tag being processed
|
||||
local current = self._stack[#self._stack]
|
||||
|
||||
if current[tag.name] then
|
||||
local array = convertObjectToArray(current[tag.name])
|
||||
table.insert(array, node)
|
||||
current[tag.name] = array
|
||||
else
|
||||
current[tag.name] = {node}
|
||||
end
|
||||
|
||||
table.insert(self._stack, node)
|
||||
end
|
||||
|
||||
---Parses an end tag.
|
||||
-- @param tag a {name, attrs} table
|
||||
-- where name is the name of the tag and attrs
|
||||
-- is a table containing the atributtes of the tag
|
||||
function tree:endtag(tag, s)
|
||||
--Table in the stack representing the tag being processed
|
||||
--Table in the stack representing the containing tag of the current tag
|
||||
local prev = self._stack[#self._stack-1]
|
||||
if not prev[tag.name] then
|
||||
error("XML Error - Unmatched Tag ["..s..":"..tag.name.."]\n")
|
||||
end
|
||||
if prev == self.root then
|
||||
-- Once parsing complete, recursively reduce tree
|
||||
self:reduce(prev, nil, nil)
|
||||
end
|
||||
|
||||
table.remove(self._stack)
|
||||
end
|
||||
|
||||
---Parses a tag content.
|
||||
-- @param t text to process
|
||||
function tree:text(text)
|
||||
local current = self._stack[#self._stack]
|
||||
table.insert(current, text)
|
||||
end
|
||||
|
||||
---Parses CDATA tag content.
|
||||
tree.cdata = tree.text
|
||||
tree.__index = tree
|
||||
return tree
|
@ -0,0 +1,350 @@
|
||||
--[[--
|
||||
Download URLs as EPUBs
|
||||
|
||||
@module koplugin.DownloadToEPUB
|
||||
--]]--
|
||||
local BD = require("ui/bidi")
|
||||
local Blitbuffer = require("ffi/blitbuffer")
|
||||
local ConfirmBox = require("ui/widget/confirmbox")
|
||||
local DataStorage = require("datastorage")
|
||||
local Device = require("device")
|
||||
local Dispatcher = require("dispatcher")
|
||||
local Event = require("ui/event")
|
||||
local FFIUtil = require("ffi/util")
|
||||
local FileManager = require("apps/filemanager/filemanager")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local LuaSettings = require("frontend/luasettings")
|
||||
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
|
||||
local NetworkMgr = require("ui/network/manager")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
local VerticalGroup = require("ui/widget/verticalgroup")
|
||||
local Screen = Device.screen
|
||||
local Size = require("ui/size")
|
||||
local filemanagerutil = require("apps/filemanager/filemanagerutil")
|
||||
local logger = require("logger")
|
||||
local util = require("frontend/util")
|
||||
local T = FFIUtil.template
|
||||
local _ = require("gettext")
|
||||
-- Gazette Modules
|
||||
local EpubBuildDirector = require("libs/gazette/epubbuilddirector")
|
||||
local WebPage = require("libs/gazette/resources/webpage")
|
||||
local ResourceAdapter = require("libs/gazette/resources/webpageadapter")
|
||||
local Epub = require("libs/gazette/epub/epub")
|
||||
local History = require("epubhistory")
|
||||
local HistoryView = require("epubhistoryview")
|
||||
|
||||
local DownloadToEpub = WidgetContainer:new{
|
||||
name = "Download to EPUB",
|
||||
download_directory = ("%s/%s/"):format(DataStorage:getFullDataDir(), "EPUB Downloads")
|
||||
}
|
||||
|
||||
local EpubBuilder = {
|
||||
output_directory = nil,
|
||||
}
|
||||
|
||||
function DownloadToEpub:init()
|
||||
self.settings = self.readSettings()
|
||||
if self.settings.data.download_directory then
|
||||
self.download_directory = self.settings.data.download_directory
|
||||
end
|
||||
self:createDownloadDirectoryIfNotExists()
|
||||
self.ui.menu:registerToMainMenu(self)
|
||||
if self.ui and self.ui.link then
|
||||
self.ui.link:addToExternalLinkDialog("30_downloadtoepub", function(this, link_url)
|
||||
return {
|
||||
text = _("Download to EPUB"),
|
||||
callback = function()
|
||||
UIManager:close(this.external_link_dialog)
|
||||
this.ui:handleEvent(Event:new("DownloadEpubFromUrl", link_url))
|
||||
end,
|
||||
show_in_dialog_func = function()
|
||||
return true
|
||||
end
|
||||
}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function DownloadToEpub:addToMainMenu(menu_items)
|
||||
menu_items.downloadtoepub = {
|
||||
text = _("Download to EPUB"),
|
||||
sorting_hint = "tools",
|
||||
sub_item_table = {
|
||||
{
|
||||
text = _("Go to EPUB downloads"),
|
||||
callback = function()
|
||||
self:goToDownloadDirectory()
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Settings"),
|
||||
sub_item_table = {
|
||||
{
|
||||
text_func = function()
|
||||
local path = filemanagerutil.abbreviate(self.download_directory)
|
||||
return T(_("Set download directory (%1)"), BD.dirpath(path))
|
||||
end,
|
||||
keep_menu_open = true,
|
||||
callback = function() self:setDownloadDirectory() end,
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
text = _("About"),
|
||||
keep_menu_open = true,
|
||||
callback = function()
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = "DownloadToEpub lets you download external links as EPUBs to your device."
|
||||
})
|
||||
end,
|
||||
},
|
||||
}
|
||||
}
|
||||
local history_view = HistoryView:new{}
|
||||
local last_download_item = history_view:getLastDownloadButton(function(history_item)
|
||||
self:maybeOpenEpub(history_item['download_path'])
|
||||
end)
|
||||
local history_menu_items = history_view:getMenuItems(function(history_item)
|
||||
self:maybeOpenEpub(history_item['download_path'])
|
||||
end)
|
||||
if last_download_item then table.insert(menu_items.downloadtoepub.sub_item_table, 2, last_download_item) end
|
||||
if history_menu_items then table.insert(menu_items.downloadtoepub.sub_item_table, 3, history_menu_items[1]) end
|
||||
end
|
||||
|
||||
function DownloadToEpub:maybeOpenEpub(file_path)
|
||||
if util.pathExists(file_path) then
|
||||
logger.dbg("DownloadToEpub: Opening " .. file_path)
|
||||
local Event = require("ui/event")
|
||||
UIManager:broadcastEvent(Event:new("SetupShowReader"))
|
||||
local ReaderUI = require("apps/reader/readerui")
|
||||
ReaderUI:showReader(file_path)
|
||||
else
|
||||
logger.dbg("DownloadToEpub: Couldn't open " .. file_path .. ". It's been moved or deleted.")
|
||||
self:showRedownloadPrompt(file_path)
|
||||
end
|
||||
end
|
||||
|
||||
function DownloadToEpub:readSettings()
|
||||
local settings = LuaSettings:open(DataStorage:getSettingsDir() .. "downloadtoepub.lua")
|
||||
if not settings.data.downloadtoepub then
|
||||
settings.data.downloadtoepub = {}
|
||||
end
|
||||
return settings
|
||||
end
|
||||
|
||||
function DownloadToEpub:saveSettings()
|
||||
local temp_settings = {
|
||||
download_directory = self.download_directory
|
||||
}
|
||||
self.settings:saveSetting("downloadtoepub", temp_settings)
|
||||
self.settings:flush()
|
||||
end
|
||||
|
||||
function DownloadToEpub:setDownloadDirectory()
|
||||
local downloadmgr = require("ui/downloadmgr")
|
||||
downloadmgr:new{
|
||||
onConfirm = function(path)
|
||||
self.download_directory = path
|
||||
self:saveSettings()
|
||||
end
|
||||
}:chooseDir()
|
||||
end
|
||||
|
||||
function DownloadToEpub:goToDownloadDirectory()
|
||||
local FileManager = require("apps/filemanager/filemanager")
|
||||
if self.ui.document then
|
||||
self.ui:onClose()
|
||||
end
|
||||
if FileManager.instance then
|
||||
FileManager.instance:reinit(self.download_directory)
|
||||
else
|
||||
FileManager:showFiles(self.download_directory)
|
||||
end
|
||||
end
|
||||
|
||||
function DownloadToEpub:createDownloadDirectoryIfNotExists()
|
||||
if not util.pathExists(self.download_directory) then
|
||||
logger.dbg("DownloadToEpub: Creating path (" .. self.download_directory .. ")")
|
||||
lfs.mkdir(self.download_directory)
|
||||
end
|
||||
end
|
||||
|
||||
function DownloadToEpub:onDownloadEpubFromUrl(link_url)
|
||||
local prompt
|
||||
prompt = ConfirmBox:new{
|
||||
text = T(_("Download to EPUB? \n\nLink: %1"), link_url),
|
||||
ok_text = _("Yes"),
|
||||
ok_callback = function()
|
||||
UIManager:close(prompt)
|
||||
self:downloadEpubWithUi(link_url, function(file_path, err)
|
||||
if err then
|
||||
UIManager:show(InfoMessage:new{ text = T(_("Error downloading EPUB: %1", err)) })
|
||||
else
|
||||
local history = History:new{}
|
||||
history:init()
|
||||
logger.dbg("DownloadToEpub: Maybe deleting from history " .. link_url)
|
||||
history:remove(link_url) -- link might have already been downloaded. If so, remove the history item.
|
||||
logger.dbg("DownloadToEpub: Adding to history " .. link_url .. " " .. file_path)
|
||||
history:add(link_url, file_path)
|
||||
logger.dbg("DownloadToEpub: Finished downloading epub to " .. file_path)
|
||||
self:showReadPrompt(file_path)
|
||||
end
|
||||
end)
|
||||
end,
|
||||
}
|
||||
UIManager:show(prompt)
|
||||
end
|
||||
|
||||
function DownloadToEpub:downloadEpubWithUi(link_url, callback)
|
||||
local info = InfoMessage:new{ text = ("Downloading... " .. link_url) }
|
||||
UIManager:show(info)
|
||||
UIManager:forceRePaint()
|
||||
UIManager:close(info)
|
||||
|
||||
NetworkMgr:runWhenOnline(function()
|
||||
local epub_builder = EpubBuilder:new{
|
||||
output_directory = self.download_directory,
|
||||
}
|
||||
local file_path, err = epub_builder:buildFromUrl(link_url)
|
||||
callback(file_path, err)
|
||||
end)
|
||||
end
|
||||
|
||||
function DownloadToEpub:showRedownloadPrompt(file_path) -- supply this with a directory?
|
||||
local prompt
|
||||
|
||||
local history = History:new{}
|
||||
history:init()
|
||||
local history_item = history:find(file_path)
|
||||
|
||||
if history_item then
|
||||
prompt = MultiConfirmBox:new{
|
||||
text = T(_("Couldn't open EPUB! \n\nFile has been moved since download (%1)\n\nInitially downloaded from (%2)\n\nWhat would you like to do?"),
|
||||
file_path,
|
||||
history_item.url),
|
||||
choice1_text = _("Redownload EPUB"),
|
||||
choice1_callback = function()
|
||||
logger.dbg("DownloadToEpub: Redownloading " .. history_item.url)
|
||||
UIManager:close(prompt)
|
||||
self:onDownloadEpubFromUrl(history_item.url)
|
||||
end,
|
||||
choice2_text = _("Delete from history"),
|
||||
choice2_callback = function()
|
||||
logger.dbg("DownloadToEpub: Deleting from history " .. history_item.url)
|
||||
history:remove(history_item.url)
|
||||
UIManager:close(prompt)
|
||||
end,
|
||||
}
|
||||
else
|
||||
prompt = InfoMessage:new{
|
||||
text = _("Couldn't open EPUB! EPUB has been deleted or moved since being downloaded."),
|
||||
show_icon = false,
|
||||
timeout = 10,
|
||||
}
|
||||
end
|
||||
UIManager:show(prompt)
|
||||
end
|
||||
|
||||
function DownloadToEpub:showReadPrompt(file_path)
|
||||
local prompt = ConfirmBox:new{
|
||||
text = _("EPUB downloaded. Would you like to read it now?"),
|
||||
ok_text = _("Open EPUB"),
|
||||
ok_callback = function()
|
||||
logger.dbg("DownloadToEpub: Opening " .. file_path)
|
||||
local Event = require("ui/event")
|
||||
UIManager:broadcastEvent(Event:new("SetupShowReader"))
|
||||
UIManager:close(prompt)
|
||||
local ReaderUI = require("apps/reader/readerui")
|
||||
ReaderUI:showReader(file_path)
|
||||
end,
|
||||
}
|
||||
UIManager:show(prompt)
|
||||
end
|
||||
|
||||
function EpubBuilder:new(o)
|
||||
o = o or {}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function EpubBuilder:buildFromUrl(url)
|
||||
logger.dbg("DownloadToEpub: Begin download of " .. url .. " outputting to " .. self.output_directory)
|
||||
|
||||
local info = InfoMessage:new{ text = _("Getting webpage…") }
|
||||
UIManager:show(info)
|
||||
UIManager:forceRePaint()
|
||||
UIManager:close(info)
|
||||
|
||||
local webpage, err = self:createWebpage(url)
|
||||
|
||||
if not webpage then
|
||||
logger.dbg("DownloadToEpub: " .. err)
|
||||
return false, err
|
||||
end
|
||||
|
||||
info = InfoMessage:new{ text = _("Building EPUB…") }
|
||||
UIManager:show(info)
|
||||
UIManager:forceRePaint()
|
||||
UIManager:close(info)
|
||||
|
||||
local epub = Epub:new{}
|
||||
epub:addFromList(ResourceAdapter:new(webpage))
|
||||
epub:setTitle(webpage.title)
|
||||
epub:setAuthor("DownloadToEpub")
|
||||
|
||||
local epub_path = ("%s%s.epub"):format(self.output_directory, util.getSafeFilename(epub.title))
|
||||
local build_director, err = self:createBuildDirector(epub_path)
|
||||
if not build_director then
|
||||
logger.dbg("DownloadToEpub: " .. err)
|
||||
return false, err
|
||||
end
|
||||
|
||||
info = InfoMessage:new{ text = _("Writing to device…") }
|
||||
UIManager:show(info)
|
||||
UIManager:forceRePaint()
|
||||
UIManager:close(info)
|
||||
|
||||
logger.dbg("DownloadToEpub: Writing EPUB to " .. epub_path)
|
||||
local path_to_epub, err = build_director:construct(epub)
|
||||
if not path_to_epub then
|
||||
logger.dbg("DownloadToEpub: " .. err)
|
||||
return false, err
|
||||
end
|
||||
|
||||
return path_to_epub
|
||||
end
|
||||
|
||||
function EpubBuilder:createWebpage(url)
|
||||
local webpage, err = WebPage:new({
|
||||
url = url,
|
||||
})
|
||||
|
||||
if err then
|
||||
return false, err
|
||||
end
|
||||
|
||||
webpage:build()
|
||||
|
||||
return webpage
|
||||
end
|
||||
|
||||
function EpubBuilder:createBuildDirector(epub_path)
|
||||
local build_director, err = EpubBuildDirector:new()
|
||||
|
||||
if not build_director then
|
||||
return false, err
|
||||
end
|
||||
|
||||
local success, err = build_director:setDestination(epub_path)
|
||||
|
||||
if not success then
|
||||
return false, err
|
||||
end
|
||||
|
||||
return build_director
|
||||
end
|
||||
|
||||
return DownloadToEpub
|
Loading…
Reference in New Issue