From 5c1d5c3314d2c3a9ec0636d6663ae1088f2014db Mon Sep 17 00:00:00 2001 From: chrox Date: Wed, 23 Apr 2014 22:19:29 +0800 Subject: [PATCH] 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. --- .editorconfig | 7 + .gitignore | 3 + Makefile | 2 + frontend/ui/widget/inputdialog.lua | 26 +-- frontend/ui/widget/inputtext.lua | 66 +++++-- frontend/ui/widget/logindialog.lua | 113 ++++++++++++ koreader-base | 2 +- plugins/evernote.koplugin/clip.lua | 262 ++++++++++++++++++++++++++++ plugins/evernote.koplugin/main.lua | 270 +++++++++++++++++++++++++++++ plugins/evernote.koplugin/note.tpl | 57 ++++++ plugins/evernote.koplugin/slt2.lua | 175 +++++++++++++++++++ 11 files changed, 952 insertions(+), 31 deletions(-) create mode 100644 frontend/ui/widget/logindialog.lua create mode 100644 plugins/evernote.koplugin/clip.lua create mode 100644 plugins/evernote.koplugin/main.lua create mode 100644 plugins/evernote.koplugin/note.tpl create mode 100644 plugins/evernote.koplugin/slt2.lua diff --git a/.editorconfig b/.editorconfig index 50dc06027..2395ff823 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/.gitignore b/.gitignore index 1073f850b..6aabe7d43 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Makefile b/Makefile index 72e0b7e31..2719e2ae2 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/frontend/ui/widget/inputdialog.lua b/frontend/ui/widget/inputdialog.lua index 9dd211deb..a4cdb86c8 100644 --- a/frontend/ui/widget/inputdialog.lua +++ b/frontend/ui/widget/inputdialog.lua @@ -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(), diff --git a/frontend/ui/widget/inputtext.lua b/frontend/ui/widget/inputtext.lua index 2e9e7afd0..8a9689732 100644 --- a/frontend/ui/widget/inputtext.lua +++ b/frontend/ui/widget/inputtext.lua @@ -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 diff --git a/frontend/ui/widget/logindialog.lua b/frontend/ui/widget/logindialog.lua new file mode 100644 index 000000000..f414378d9 --- /dev/null +++ b/frontend/ui/widget/logindialog.lua @@ -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 + diff --git a/koreader-base b/koreader-base index c08774440..4e15f4085 160000 --- a/koreader-base +++ b/koreader-base @@ -1 +1 @@ -Subproject commit c087744408a309b06c626825698d3e9c034a95db +Subproject commit 4e15f4085fdb1399944a7bf971d4c25acfd51743 diff --git a/plugins/evernote.koplugin/clip.lua b/plugins/evernote.koplugin/clip.lua new file mode 100644 index 000000000..aabca09f0 --- /dev/null +++ b/plugins/evernote.koplugin/clip.lua @@ -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 + diff --git a/plugins/evernote.koplugin/main.lua b/plugins/evernote.koplugin/main.lua new file mode 100644 index 000000000..b30244125 --- /dev/null +++ b/plugins/evernote.koplugin/main.lua @@ -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 + diff --git a/plugins/evernote.koplugin/note.tpl b/plugins/evernote.koplugin/note.tpl new file mode 100644 index 000000000..7ceea52fd --- /dev/null +++ b/plugins/evernote.koplugin/note.tpl @@ -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('&', '&'):gsub('<', '<'):gsub('>', '>') + return esc + end +}# +
+

#{= htmlescape(booknotes.title) }#

+
#{= htmlescape(booknotes.author) }#
+ #{ for _, chapter in ipairs(booknotes) do }# + #{ if chapter.title then }# +
#{= htmlescape(chapter.title) }#
+ #{ end }# + #{ for index, clipping in ipairs(chapter) do }# +
+
+
+ #{= os.date("%x", clipping.time) }##{= clipping.page }# +
+
+ #{= htmlescape(clipping.text) }# +
+ #{ if clipping.note then }# +
+ #{= htmlescape(notemarks) }# + #{= htmlescape(clipping.note) }# +
+ #{ end }# +
+ #{ end }# + #{ end }# +
+ diff --git a/plugins/evernote.koplugin/slt2.lua b/plugins/evernote.koplugin/slt2.lua new file mode 100644 index 000000000..0a37882e2 --- /dev/null +++ b/plugins/evernote.koplugin/slt2.lua @@ -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