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