add Evernote plugin to export highlights and notes

The "My Clipping" file that storing highlights and notes for Kindle
native readers could also be parsed and exported. The parser is
implemented in `evernote.koplugin/clip.lua`.

Parsed highlights and notes in one book will be packed and rendered
into html node with a slt2 template `note.tpl` that complies with
evernote markup language(ENML).

Finally the evernote client will create or update note entries and
push them to Evernote cloud.
pull/556/head
chrox 10 years ago
parent 34fd9f3efa
commit 5c1d5c3314

@ -18,3 +18,10 @@ trim_trailing_whitespace = false
indent_style = tab
indent_size = 8
[Makefile.def]
indent_style = tab
indent_size = 8
[*.{js,json,css,scss,sass,html,handlebars,tpl}]
indent_style = space
indent_size = 2

3
.gitignore vendored

@ -10,6 +10,7 @@ git-rev
*.o
tags
test/*
*.tar
emu
@ -18,4 +19,6 @@ koreader-*.zip
/.cproject
/.project
koreader-arm-linux-gnueabi
koreader-x86_64-linux-gnu

@ -39,6 +39,8 @@ endif
for f in $(INSTALL_FILES); do \
ln -sf ../../$$f $(INSTALL_DIR)/koreader/; \
done
# install plugins
cp -r plugins/* $(INSTALL_DIR)/koreader/plugins/
cp -rpL resources/fonts/* $(INSTALL_DIR)/koreader/fonts/
mkdir -p $(INSTALL_DIR)/koreader/screenshots
mkdir -p $(INSTALL_DIR)/koreader/data/dict

@ -18,13 +18,13 @@ local InputDialog = InputContainer:new{
buttons = nil,
input_type = nil,
enter_callback = nil,
width = nil,
height = nil,
title_face = Font:getFace("tfont", 22),
input_face = Font:getFace("cfont", 20),
title_padding = Screen:scaleByDPI(5),
title_margin = Screen:scaleByDPI(2),
input_padding = Screen:scaleByDPI(10),
@ -53,21 +53,21 @@ function InputDialog:init()
scroll = false,
parent = self,
}
local button_table = ButtonTable:new{
self.button_table = ButtonTable:new{
width = self.width,
button_font_face = "cfont",
button_font_size = 20,
buttons = self.buttons,
zero_sep = true,
}
local title_bar = LineWidget:new{
self.title_bar = LineWidget:new{
--background = 8,
dimen = Geom:new{
w = button_table:getSize().w + self.button_padding,
w = self.button_table:getSize().w + self.button_padding,
h = Screen:scaleByDPI(2),
}
}
self.dialog_frame = FrameContainer:new{
radius = 8,
bordersize = 3,
@ -77,11 +77,11 @@ function InputDialog:init()
VerticalGroup:new{
align = "left",
self.title,
title_bar,
self.title_bar,
-- input
CenterContainer:new{
dimen = Geom:new{
w = title_bar:getSize().w,
w = self.title_bar:getSize().w,
h = self.input:getSize().h,
},
self.input,
@ -89,14 +89,14 @@ function InputDialog:init()
-- buttons
CenterContainer:new{
dimen = Geom:new{
w = title_bar:getSize().w,
h = button_table:getSize().h,
w = self.title_bar:getSize().w,
h = self.button_table:getSize().h,
},
button_table,
self.button_table,
}
}
}
self[1] = CenterContainer:new{
dimen = Geom:new{
w = Screen:getWidth(),

@ -3,9 +3,13 @@ local ScrollTextWidget = require("ui/widget/scrolltextwidget")
local TextBoxWidget = require("ui/widget/textboxwidget")
local FrameContainer = require("ui/widget/container/framecontainer")
local VirtualKeyboard = require("ui/widget/virtualkeyboard")
local Font = require("ui/font")
local Screen = require("ui/screen")
local GestureRange = require("ui/gesturerange")
local UIManager = require("ui/uimanager")
local Geom = require("ui/geometry")
local Device = require("ui/device")
local Screen = require("ui/screen")
local Font = require("ui/font")
local DEBUG = require("dbg")
local InputText = InputContainer:new{
text = "",
@ -13,40 +17,51 @@ local InputText = InputContainer:new{
charlist = {}, -- table to store input string
charpos = 1,
input_type = nil,
text_type = nil,
width = nil,
height = nil,
face = Font:getFace("cfont", 22),
padding = 5,
margin = 5,
bordersize = 2,
parent = nil, -- parent dialog that will be set dirty
scroll = false,
focused = true,
}
function InputText:init()
self:StringToCharlist(self.text)
self:initTextBox()
self:initKeyboard()
if Device:isTouchDevice() then
self.ges_events = {
TapTextBox = {
GestureRange:new{
ges = "tap",
range = self.dimen
}
}
}
end
end
function InputText:initTextBox()
local bgcolor = nil
local fgcolor = nil
if self.text == "" then
self.text = self.hint
bgcolor = 0.0
fgcolor = 0.5
else
bgcolor = 0.0
fgcolor = 1.0
end
local bgcolor, fgcolor = 0.0, self.text == "" and 0.5 or 1.0
local text_widget = nil
local show_text = self.text
if self.text_type == "password" and show_text ~= "" then
show_text = self.text:gsub("(.-).", function() return "*" end)
show_text = show_text:gsub("(.)$", function() return self.text:sub(-1) end)
elseif show_text == "" then
show_text = self.hint
end
if self.scroll then
text_widget = ScrollTextWidget:new{
text = self.text,
text = show_text,
face = self.face,
bgcolor = bgcolor,
fgcolor = fgcolor,
@ -55,7 +70,7 @@ function InputText:initTextBox()
}
else
text_widget = TextBoxWidget:new{
text = self.text,
text = show_text,
face = self.face,
bgcolor = bgcolor,
fgcolor = fgcolor,
@ -67,6 +82,7 @@ function InputText:initTextBox()
bordersize = self.bordersize,
padding = self.padding,
margin = self.margin,
color = self.focused and 15 or 8,
text_widget,
}
self.dimen = self[1]:getSize()
@ -85,6 +101,22 @@ function InputText:initKeyboard()
}
end
function InputText:onTapTextBox()
if self.parent.onSwitchFocus then
self.parent:onSwitchFocus(self)
end
end
function InputText:unfocus()
self.focused = false
self[1].color = 8
end
function InputText:focus()
self.focused = true
self[1].color = 15
end
function InputText:onShowKeyboard()
UIManager:show(self.keyboard)
end

@ -0,0 +1,113 @@
local FrameContainer = require("ui/widget/container/framecontainer")
local CenterContainer = require("ui/widget/container/centercontainer")
local VerticalGroup = require("ui/widget/verticalgroup")
local InputDialog = require("ui/widget/inputdialog")
local InputText = require("ui/widget/inputtext")
local UIManager = require("ui/uimanager")
local Geom = require("ui/geometry")
local Screen = require("ui/screen")
local DEBUG = require("dbg")
local _ = require("gettext")
local LoginDialog = InputDialog:extend{
username = "",
username_hint = "username",
password = "",
password_hint = "password",
}
function LoginDialog:init()
-- init title and buttons in base class
InputDialog.init(self)
self.input_username = InputText:new{
text = self.username,
hint = self.username_hint,
face = self.input_face,
width = self.width * 0.9,
focused = true,
scroll = false,
parent = self,
}
self.input_password = InputText:new{
text = self.password,
hint = self.password_hint,
face = self.input_face,
width = self.width * 0.9,
text_type = "password",
focused = false,
scroll = false,
parent = self,
}
self.dialog_frame = FrameContainer:new{
radius = 8,
bordersize = 3,
padding = 0,
margin = 0,
background = 0,
VerticalGroup:new{
align = "left",
self.title,
self.title_bar,
-- username input
CenterContainer:new{
dimen = Geom:new{
w = self.title_bar:getSize().w,
h = self.input_username:getSize().h,
},
self.input_username,
},
-- password input
CenterContainer:new{
dimen = Geom:new{
w = self.title_bar:getSize().w,
h = self.input_password:getSize().h,
},
self.input_password,
},
-- buttons
CenterContainer:new{
dimen = Geom:new{
w = self.title_bar:getSize().w,
h = self.button_table:getSize().h,
},
self.button_table,
}
}
}
self.input = self.input_username
self[1] = CenterContainer:new{
dimen = Geom:new{
w = Screen:getWidth(),
h = Screen:getHeight() - self.input:getKeyboardDimen().h,
},
self.dialog_frame,
}
UIManager.repaint_all = true
UIManager.full_refresh = true
end
function LoginDialog:getCredential()
local username = self.input_username:getText()
local password = self.input_password:getText()
return username, password
end
function LoginDialog:onSwitchFocus(inputbox)
-- unfocus current inputbox
self.input:unfocus()
self.input:onCloseKeyboard()
-- focus new inputbox
self.input = inputbox
self.input:focus()
self.input:onShowKeyboard()
UIManager:show(self)
end
return LoginDialog

@ -1 +1 @@
Subproject commit c087744408a309b06c626825698d3e9c034a95db
Subproject commit 4e15f4085fdb1399944a7bf971d4c25acfd51743

@ -0,0 +1,262 @@
-- lfs
local MyClipping = {
my_clippings = "/mnt/us/documents/My Clippings.txt",
history_dir = "./history",
}
function MyClipping:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
--[[
-- clippings: main table to store parsed highlights and notes entries
-- {
-- ["Title(Author Name)"] = {
-- {
-- {
-- ["page"] = 123,
-- ["time"] = 1398127554,
-- ["text"] = "Games of all sorts were played in homes and fields."
-- },
-- {
-- ["page"] = 156,
-- ["time"] = 1398128287,
-- ["text"] = "There Spenser settled down to gentleman farming.",
-- ["note"] = "This is a sample note.",
-- },
-- ["title"] = "Chapter I"
-- },
-- }
-- }
-- ]]
function MyClipping:parseMyClippings()
-- My Clippings format:
-- Title(Author Name)
-- Your Highlight on Page 123 | Added on Monday, April 21, 2014 10:08:07 PM
--
-- This is a sample highlight.
-- ==========
local file = io.open(self.my_clippings, "r")
local clippings = {}
if file then
local index = 1
local corrupted = false
local title, author, info, text
for line in file:lines() do
line = line:match("^%s*(.-)%s*$") or ""
if index == 1 then
title, author = self:getTitle(line)
clippings[title] = clippings[title] or {
title = title,
author = author,
}
elseif index == 2 then
info = self:getInfo(line)
elseif index == 3 then
-- should be a blank line, we skip this line
elseif index == 4 then
text = self:getText(line)
end
if line == "==========" then
if index == 5 then
-- entry ends normally
local clipping = {
page = info.page or info.location,
sort = info.sort,
time = info.time,
text = text,
}
-- we cannot extract chapter info so just insert clipping
-- to a place holder chapter
table.insert(clippings[title], { clipping })
end
index = 0
end
index = index + 1
end
end
return clippings
end
local extensions = {
[".pdf"] = true,
[".djvu"] = true,
[".epub"] = true,
[".fb2"] = true,
[".mobi"] = true,
[".txt"] = true,
[".html"] = true,
[".doc"] = true,
}
-- remove file extensions added by former Koreader
-- extract author name in "Title(Author)" format
-- extract author name in "Title - Author" format
function MyClipping:getTitle(line)
line = line:match("^%s*(.-)%s*$") or ""
if extensions[line:sub(-4):lower()] then
line = line:sub(1, -5)
elseif extensions[line:sub(-5):lower()] then
line = line:sub(1, -6)
end
local _, _, title, author = line:find("(.-)%s*%((.*)%)")
if not author then
_, _, title, author = line:find("(.-)%s*-%s*(.*)")
end
if not title then title = line end
return title:match("^%s*(.-)%s*$"), author
end
local keywords = {
["highlight"] = {
"Highlight",
"标注",
},
["note"] = {
"Note",
"笔记",
},
["bookmark"] = {
"Bookmark",
"书签",
},
}
local months = {
["Jan"] = 1,
["Feb"] = 2,
["Mar"] = 3,
["Apr"] = 4,
["May"] = 5,
["Jun"] = 6,
["Jul"] = 7,
["Aug"] = 8,
["Sep"] = 9,
["Oct"] = 10,
["Nov"] = 11,
["Dec"] = 12
}
local pms = {
["PM"] = 12,
["下午"] = 12,
}
function MyClipping:getTime(line)
if not line then return end
local _, _, year, month, day = line:find("(%d+)年(%d+)月(%d+)日")
if not year or not month or not day then
_, _, year, month, day = line:find("(%d%d%d%d)-(%d%d)-(%d%d)")
end
if not year or not month or not day then
for k, v in pairs(months) do
if line:find(k) then
month = v
_, _, day = line:find(" (%d%d),")
_, _, year = line:find(" (%d%d%d%d)")
break
end
end
end
local _, _, hour, minute, second = line:find("(%d+):(%d+):(%d+)")
if year and month and day and hour and minute and second then
for k, v in pairs(pms) do
if line:find(k) then hour = hour + v end
break
end
local time = os.time({
year = year, month = month, day = day,
hour = hour, min = minute, sec = second,
})
return time
end
end
function MyClipping:getInfo(line)
local info = {}
line = line or ""
local _, _, part1, part2 = line:find("(.+)%s*|%s*(.+)")
-- find entry type and location
for sort, words in pairs(keywords) do
for _, word in ipairs(words) do
if part1 and part1:find(word) then
info.sort = sort
info.location = part1:match("(%d+-?%d+)")
break
end
end
end
-- find entry created time
info.time = self:getTime(part2 or "")
return info
end
function MyClipping:getText(line)
line = line or ""
return line:match("^%s*(.-)%s*$") or ""
end
function MyClipping:parseHighlight(highlights, book)
for page, items in pairs(highlights) do
for _, item in ipairs(items) do
local clipping = {}
clipping.page = page
clipping.sort = "highlight"
clipping.time = self:getTime(item.datetime or "")
clipping.text = self:getText(item.text)
-- TODO: store chapter info when exporting highlights
if clipping.text and clipping.text ~= "" then
table.insert(book, { clipping })
end
end
end
table.sort(book, function(v1, v2) return v1[1].page < v2[1].page end)
end
function MyClipping:parseHistory()
local clippings = {}
for f in lfs.dir(self.history_dir) do
local path = self.history_dir.."/"..f
if lfs.attributes(path, "mode") == "file" and path:find(".+%.lua$") then
local ok, stored = pcall(dofile, path)
if ok and stored.highlight then
local _, _, docname = path:find("%[.*%](.*)%.lua$")
local title, author = self:getTitle(docname)
clippings[title] = {
title = title,
author = author,
}
self:parseHighlight(stored.highlight, clippings[title])
end
end
end
return clippings
end
function MyClipping:parseCurrentDoc(view)
local clippings = {}
local path = view.document.file
local _, _, docname = path:find(".*/(.*)")
local title, author = self:getTitle(docname)
clippings[title] = {
title = title,
author = author,
}
self:parseHighlight(view.highlight.saved, clippings[title])
return clippings
end
return MyClipping

@ -0,0 +1,270 @@
local InputContainer = require("ui/widget/container/inputcontainer")
local LoginDialog = require("ui/widget/logindialog")
local InfoMessage = require("ui/widget/infomessage")
local UIManager = require("ui/uimanager")
local Screen = require("ui/screen")
local Event = require("ui/event")
local DEBUG = require("dbg")
local _ = require("gettext")
local slt2 = require('slt2')
local MyClipping = require("clip")
local EvernoteOAuth = require("EvernoteOAuth")
local EvernoteClient = require("EvernoteClient")
local EvernoteExporter = InputContainer:new{
login_title = _("Login to Evernote"),
notebook_name = _("Koreader Notes"),
--evernote_domain = "sandbox",
evernote_token,
notebook_guid,
}
function EvernoteExporter:init()
self.ui.menu:registerToMainMenu(self)
local settings = G_reader_settings:readSetting("evernote") or {}
self.evernote_username = settings.username or ""
self.evernote_token = settings.token
self.notebook_guid = settings.notebook
self.parser = MyClipping:new{
my_clippings = "/mnt/us/documents/My Clippings.txt",
history_dir = "./history",
}
self.template = slt2.loadfile(self.path.."/note.tpl")
end
function EvernoteExporter:addToMainMenu(tab_item_table)
table.insert(tab_item_table.plugins, {
text = _("Evernote"),
sub_item_table = {
{
text_func = function()
return self.evernote_token and _("Logout") or _("Login")
end,
callback = function()
if self.evernote_token then
self:logout()
else
self:login()
end
end
},
{
text = _("Export all notes in this book"),
callback = function()
UIManager:scheduleIn(0.5, function()
self:exportCurrentNotes(self.view)
end)
UIManager:show(InfoMessage:new{
text = _("This may take several seconds..."),
timeout = 3,
})
end
},
{
text = _("Export all notes in your library"),
callback = function()
UIManager:scheduleIn(0.5, function()
self:exportAllNotes()
end)
UIManager:show(InfoMessage:new{
text = _("This may take several minutes..."),
timeout = 3,
})
end
},
}
})
end
function EvernoteExporter:login()
self.login_dialog = LoginDialog:new{
title = self.login_title,
username = self.evernote_username or "",
buttons = {
{
{
text = _("Cancel"),
enabled = true,
callback = function()
self:closeDialog()
end,
},
{
text = _("Login"),
enabled = true,
callback = function()
local username, password = self:getCredential()
self:closeDialog()
UIManager:scheduleIn(0.5, function()
self:doLogin(username, password)
end)
end,
},
},
},
width = Screen:getWidth() * 0.8,
height = Screen:getHeight() * 0.4,
}
self.login_dialog:onShowKeyboard()
UIManager:show(self.login_dialog)
end
function EvernoteExporter:closeDialog()
self.login_dialog:onClose()
UIManager:close(self.login_dialog)
end
function EvernoteExporter:getCredential()
return self.login_dialog:getCredential()
end
function EvernoteExporter:doLogin(username, password)
self:closeDialog()
local oauth = EvernoteOAuth:new{
domain = self.evernote_domain,
username = username,
password = password,
}
self.evernote_username = username
local ok, token = pcall(oauth.getToken, oauth)
if not ok or not token then
UIManager:show(InfoMessage:new{
text = _("Error occurs when login:") .. "\n" .. token,
})
return
end
local client = EvernoteClient:new{
domain = self.evernote_domain,
authToken = token,
}
local ok, guid = pcall(self.getExportNotebook, self, client)
if not ok or not guid then
UIManager:show(InfoMessage:new{
text = _("Error occurs when login:") .. "\n" .. guid,
})
elseif guid then
self.evernote_token = token
self.notebook_guid = guid
UIManager:show(InfoMessage:new{
text = _("Login to Evernote successfully"),
})
end
self:saveSettings()
end
function EvernoteExporter:logout()
self.evernote_token = nil
self.notebook_guid = nil
self:saveSettings()
end
function EvernoteExporter:saveSettings()
local settings = {
username = self.evernote_username,
token = self.evernote_token,
notebook = self.notebook_guid,
}
G_reader_settings:saveSetting("evernote", settings)
end
function EvernoteExporter:getExportNotebook(client)
local name = self.notebook_name
return client:findNotebookByTitle(name) or client:createNotebook(name).guid
end
function EvernoteExporter:exportCurrentNotes(view)
local client = EvernoteClient:new{
domain = self.evernote_domain,
authToken = self.evernote_token,
}
local clippings = self.parser:parseCurrentDoc(view)
self:exportClippings(client, clippings)
end
function EvernoteExporter:exportAllNotes()
local client = EvernoteClient:new{
domain = self.evernote_domain,
authToken = self.evernote_token,
}
local clippings = self.parser:parseMyClippings()
if next(clippings) == nil then
clippings = self.parser:parseHistory()
end
-- remove blank entries
for title, booknotes in pairs(clippings) do
-- chapter number is zero
if #booknotes == 0 then
clippings[title] = nil
end
end
--DEBUG("clippings", clippings)
self:exportClippings(client, clippings)
end
function EvernoteExporter:exportClippings(client, clippings)
local export_count, error_count = 0, 0
local export_title, error_title
for title, booknotes in pairs(clippings) do
local ok, err = pcall(self.exportBooknotes, self, client, title, booknotes)
-- error reporting
if not ok then
DEBUG("Error occurs when exporting book:", title, err)
error_count = error_count + 1
error_title = title
else
DEBUG("Exported notes in book:", title)
export_count = export_count + 1
export_title = title
end
end
local msg = ""
local all_count = export_count + error_count
if export_count > 0 and error_count == 0 then
if all_count == 1 then
msg = _("Exported notes in book:") .. "\n" .. export_title
else
msg = _("Exported notes in book:") .. "\n" .. export_title
msg = msg .. "\n" .. _("and ") .. all_count-1 .. _("others.")
end
elseif error_count > 0 then
if all_count == 1 then
msg = _("Error occurs when exporting book:") .. "\n" .. error_title
else
msg = _("Errors occur when exporting book:") .. "\n" .. error_title
msg = msg .. "\n" .. _("and ") .. error_count-1 .. ("others.")
end
end
UIManager:show(InfoMessage:new{ text = msg })
end
function EvernoteExporter:exportBooknotes(client, title, booknotes)
local content = slt2.render(self.template, {
booknotes = booknotes,
notemarks = _("Note: "),
})
--DEBUG("content", content)
local note_guid = client:findNoteByTitle(title, self.notebook_guid)
if not note_guid then
client:createNote(title, content, {}, self.notebook_guid)
else
client:updateNote(note_guid, title, content, {}, self.notebook_guid)
end
end
return EvernoteExporter

@ -0,0 +1,57 @@
#{
-- helper function to map time to JET color
function timecolor(time)
local r,g,b
local year = 3600*24*30*12
local lapse = os.time() - time
if lapse <= 1*year then
r,g,b = 255, 255*(year-lapse)/year, 0
elseif lapse > 1*year and lapse < 2*year then
r,g,b = 255*(lapse-year)/year, 255, 255*(2*year-lapse)/year
elseif lapse >= 2*year then
r,g,b = 0, 255*(lapse-2*year)/year, 255
end
r = r > 255 and 255 or math.floor(r)
r = r < 0 and 0 or math.floor(r)
g = g > 255 and 255 or math.floor(g)
g = g < 0 and 0 or math.floor(g)
b = b > 255 and 255 or math.floor(b)
b = b < 0 and 0 or math.floor(b)
return r..','..g..','..b
end
function htmlescape(text)
if text == nil then return "" end
local esc, _ = text:gsub('&', '&amp;'):gsub('<', '&lt;'):gsub('>', '&gt;')
return esc
end
}#
<div style="width:90%; max-width:600px; margin:0px auto; padding:5px; font-size:12pt; font-family:Georgia">
<h2 style="font-size:18pt; text-align:right;">#{= htmlescape(booknotes.title) }#</h2>
<h5 style="font-size:12pt; text-align:right; color:gray;">#{= htmlescape(booknotes.author) }#</h5>
#{ for _, chapter in ipairs(booknotes) do }#
#{ if chapter.title then }#
<div style="font-size:14pt; font-weight:bold; text-align:center; margin:0.5em;"><span>#{= htmlescape(chapter.title) }#</span></div>
#{ end }#
#{ for index, clipping in ipairs(chapter) do }#
<div style="padding-top:0.5em; padding-bottom:0.5em;#{ if index > 1 then }# border-top:1px dotted lightgray;#{ end }#">
<div style="font-size:10pt; margin-bottom:0.2em; color:darkgray">
<div style="display:inline-block; width:0.2em; height:0.9em; margin-right:0.2em; background-color:rgb(#{= timecolor(clipping.time)}#);"></div>
<span>#{= os.date("%x", clipping.time) }#</span><span style="float:right">#{= clipping.page }#</span>
</div>
<div style="font-size:12pt">
<span>#{= htmlescape(clipping.text) }#</span>
</div>
#{ if clipping.note then }#
<div style="font-size:11pt; margin-top:0.2em;">
<span style="font-weight:bold;">#{= htmlescape(notemarks) }#</span>
<span style="color:#888888">#{= htmlescape(clipping.note) }#</span>
</div>
#{ end }#
</div>
#{ end }#
#{ end }#
</div>

@ -0,0 +1,175 @@
--[[
-- slt2 - Simple Lua Template 2
--
-- Project page: https://github.com/henix/slt2
--
-- @License
-- MIT License
--]]
local slt2 = {}
-- a tree fold on inclusion tree
-- @param init_func: must return a new value when called
local function include_fold(template, start_tag, end_tag, fold_func, init_func)
local result = init_func()
start_tag = start_tag or '#{'
end_tag = end_tag or '}#'
local start_tag_inc = start_tag..'include:'
local start1, end1 = string.find(template, start_tag_inc, 1, true)
local start2 = nil
local end2 = 0
while start1 ~= nil do
if start1 > end2 + 1 then -- for beginning part of file
result = fold_func(result, string.sub(template, end2 + 1, start1 - 1))
end
start2, end2 = string.find(template, end_tag, end1 + 1, true)
assert(start2, 'end tag "'..end_tag..'" missing')
do -- recursively include the file
local filename = assert(loadstring('return '..string.sub(template, end1 + 1, start2 - 1)))()
assert(filename)
local fin = assert(io.open(filename))
-- TODO: detect cyclic inclusion?
result = fold_func(result, include_fold(fin:read('*a'), start_tag, end_tag, fold_func, init_func), filename)
fin:close()
end
start1, end1 = string.find(template, start_tag_inc, end2 + 1, true)
end
result = fold_func(result, string.sub(template, end2 + 1))
return result
end
-- preprocess included files
-- @return string
function slt2.precompile(template, start_tag, end_tag)
return table.concat(include_fold(template, start_tag, end_tag, function(acc, v)
if type(v) == 'string' then
table.insert(acc, v)
elseif type(v) == 'table' then
table.insert(acc, table.concat(v))
else
error('Unknown type: '..type(v))
end
return acc
end, function() return {} end))
end
-- unique a list, preserve order
local function stable_uniq(t)
local existed = {}
local res = {}
for _, v in ipairs(t) do
if not existed[v] then
table.insert(res, v)
existed[v] = true
end
end
return res
end
-- @return { string }
function slt2.get_dependency(template, start_tag, end_tag)
return stable_uniq(include_fold(template, start_tag, end_tag, function(acc, v, name)
if type(v) == 'string' then
elseif type(v) == 'table' then
if name ~= nil then
table.insert(acc, name)
end
for _, subname in ipairs(v) do
table.insert(acc, subname)
end
else
error('Unknown type: '..type(v))
end
return acc
end, function() return {} end))
end
-- @return { name = string, code = string / function}
function slt2.loadstring(template, start_tag, end_tag, tmpl_name)
-- compile it to lua code
local lua_code = {}
start_tag = start_tag or '#{'
end_tag = end_tag or '}#'
local output_func = "coroutine.yield"
template = slt2.precompile(template, start_tag, end_tag)
local start1, end1 = string.find(template, start_tag, 1, true)
local start2 = nil
local end2 = 0
local cEqual = string.byte('=', 1)
while start1 ~= nil do
if start1 > end2 + 1 then
table.insert(lua_code, output_func..'('..string.format("%q", string.sub(template, end2 + 1, start1 - 1))..')')
end
start2, end2 = string.find(template, end_tag, end1 + 1, true)
assert(start2, 'end_tag "'..end_tag..'" missing')
if string.byte(template, end1 + 1) == cEqual then
table.insert(lua_code, output_func..'('..string.sub(template, end1 + 2, start2 - 1)..')')
else
table.insert(lua_code, string.sub(template, end1 + 1, start2 - 1))
end
start1, end1 = string.find(template, start_tag, end2 + 1, true)
end
table.insert(lua_code, output_func..'('..string.format("%q", string.sub(template, end2 + 1))..')')
local ret = { name = tmpl_name or '=(slt2.loadstring)' }
if setfenv == nil then -- lua 5.2
ret.code = table.concat(lua_code, '\n')
else -- lua 5.1
ret.code = assert(loadstring(table.concat(lua_code, '\n'), ret.name))
end
return ret
end
-- @return { name = string, code = string / function }
function slt2.loadfile(filename, start_tag, end_tag)
local fin = assert(io.open(filename))
local all = fin:read('*a')
fin:close()
return slt2.loadstring(all, start_tag, end_tag, filename)
end
local mt52 = { __index = _ENV }
local mt51 = { __index = _G }
-- @return a coroutine function
function slt2.render_co(t, env)
local f
if setfenv == nil then -- lua 5.2
if env ~= nil then
setmetatable(env, mt52)
end
f = assert(load(t.code, t.name, 't', env or _ENV))
else -- lua 5.1
if env ~= nil then
setmetatable(env, mt51)
end
f = setfenv(t.code, env or _G)
end
return f
end
-- @return string
function slt2.render(t, env)
local result = {}
local co = coroutine.create(slt2.render_co(t, env))
while coroutine.status(co) ~= 'dead' do
local ok, chunk = coroutine.resume(co)
if not ok then
error(chunk)
end
table.insert(result, chunk)
end
return table.concat(result)
end
return slt2
Loading…
Cancel
Save