mirror of https://github.com/koreader/koreader
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
parent
34fd9f3efa
commit
5c1d5c3314
@ -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('&', '&'):gsub('<', '<'):gsub('>', '>')
|
||||||
|
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…
Reference in New Issue