From 6804b77251431c6c5a3a57ea0e17ef25d8260dca Mon Sep 17 00:00:00 2001 From: Utsob Roy Date: Sat, 28 May 2022 14:32:36 +0600 Subject: [PATCH] Markdown export (#9076) Enables users to export markdown locally with some configuration options to allow users to format the output to a certain extent. --- plugins/exporter.koplugin/base.lua | 7 + plugins/exporter.koplugin/main.lua | 1 + plugins/exporter.koplugin/target/joplin.lua | 48 +++---- plugins/exporter.koplugin/target/markdown.lua | 135 ++++++++++++++++++ plugins/exporter.koplugin/template/md.lua | 62 ++++++++ 5 files changed, 221 insertions(+), 32 deletions(-) create mode 100644 plugins/exporter.koplugin/target/markdown.lua create mode 100644 plugins/exporter.koplugin/template/md.lua diff --git a/plugins/exporter.koplugin/base.lua b/plugins/exporter.koplugin/base.lua index 31e37a122..546b6ff81 100644 --- a/plugins/exporter.koplugin/base.lua +++ b/plugins/exporter.koplugin/base.lua @@ -24,6 +24,13 @@ function BaseExporter:_init() self.is_remote = self.is_remote or false self.version = self.version or "1.0.0" self:loadSettings() + if type(self.init_callback) == "function" then + local changed, settings = self:init_callback(self.settings) + if changed then + self.settings = settings + self:saveSettings() + end + end return self end diff --git a/plugins/exporter.koplugin/main.lua b/plugins/exporter.koplugin/main.lua index 94b08b583..ea83e2de3 100644 --- a/plugins/exporter.koplugin/main.lua +++ b/plugins/exporter.koplugin/main.lua @@ -100,6 +100,7 @@ local Exporter = InputContainer:new { html = require("target/html"), joplin = require("target/joplin"), json = require("target/json"), + markdown = require("target/markdown"), readwise = require("target/readwise"), text = require("target/text"), }, diff --git a/plugins/exporter.koplugin/target/joplin.lua b/plugins/exporter.koplugin/target/joplin.lua index 5e086e747..ac7692e47 100644 --- a/plugins/exporter.koplugin/target/joplin.lua +++ b/plugins/exporter.koplugin/target/joplin.lua @@ -1,4 +1,3 @@ -local BD = require("ui/bidi") local InfoMessage = require("ui/widget/infomessage") local InputDialog = require("ui/widget/inputdialog") local UIManager = require("ui/uimanager") @@ -6,6 +5,7 @@ local http = require("socket.http") local json = require("json") local logger = require("logger") local ltn12 = require("ltn12") +local md = require("template/md") local socketutil = require("socketutil") local T = require("ffi/util").template local _ = require("gettext") @@ -15,6 +15,7 @@ local JoplinExporter = require("base"):new { name = "joplin", is_remote = true, notebook_name = _("KOReader Notes"), + version = "1.1.0", } local function makeRequest(url, method, request_body) @@ -64,24 +65,6 @@ local function ping(ip, port) end end -local function prepareNote(booknotes) - local note = "" - for _, clipping in ipairs(booknotes) do - local entry = clipping[1] - if entry.chapter then - note = note .. "\n\t*" .. entry.chapter .. "*\n\n * * *" - end - - note = note .. os.date("%Y-%m-%d %H:%M:%S \n", entry.time) - note = note .. entry.text - if entry.note then - note = note .. "\n---\n" .. entry.note - end - note = note .. "\n * * *\n" - end - return note -end - -- If successful returns id of found note. function JoplinExporter:findNoteByTitle(title, notebook_id) local url_base = string.format("http://%s:%s/notes?token=%s&fields=id,title,parent_id&page=", @@ -148,7 +131,7 @@ function JoplinExporter:notebookExist(title) end for i, notebook in ipairs(response.items) do - if notebook.title == title then return true end + if notebook.title == title then return notebook.id end end return false end @@ -305,16 +288,10 @@ function JoplinExporter:getMenuTable() keep_menu_open = true, callback = function() UIManager:show(InfoMessage:new { - text = T(_([[You can enter your auth token on your computer by saving an empty token. Then quit KOReader, edit the exporter.joplin_token field in %1/settings.reader.lua after creating a backup, and restart KOReader once you're done. - -To export to Joplin, you must forward the IP and port used by this plugin to the localhost:port on which Joplin is listening. This can be done with socat or a similar program. For example: - -For Windows: netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=41185 connectaddress=localhost connectport=41184 + text = T(_([[For Joplin setup instructions, see %1 -For Linux: $socat tcp-listen:41185,reuseaddr,fork tcp:localhost:41184 - -For more information, please visit https://github.com/koreader/koreader/wiki/Highlight-export.]]) - , BD.dirpath("example")) +Markdown formatting can be configured in: +Export highlights > Choose formats and services > Markdown.]]), "https://github.com/koreader/koreader/wiki/Joplin") }) end } @@ -329,7 +306,7 @@ function JoplinExporter:export(t) logger.warn("Cannot reach Joplin server") return false end - + local existing_notebook = self:notebookExist(self.notebook_name) if not self:notebookExist(self.notebook_name) then local notebook = self:createNotebook(self.notebook_name) if notebook then @@ -341,12 +318,19 @@ function JoplinExporter:export(t) logger.warn("Joplin: unable to create new notebook") return false end + else + if not self.settings.notebook_guid then + self.settings.notebook_guid = existing_notebook + self:saveSettings() + end end - + local plugin_settings = G_reader_settings:readSetting("exporter") or {} + local markdown_settings = plugin_settings.markdown local notebook_id = self.settings.notebook_guid for _, booknotes in pairs(t) do - local note = prepareNote(booknotes) + local note = md.prepareBookContent(booknotes, markdown_settings.formatting_options, markdown_settings.highlight_formatting) local note_id = self:findNoteByTitle(booknotes.title, notebook_id) + local response if note_id then response = self:updateNote(note, note_id) diff --git a/plugins/exporter.koplugin/target/markdown.lua b/plugins/exporter.koplugin/target/markdown.lua new file mode 100644 index 000000000..d81a5f5d2 --- /dev/null +++ b/plugins/exporter.koplugin/target/markdown.lua @@ -0,0 +1,135 @@ +local UIManager = require("ui/uimanager") +local md = require("template/md") +local _ = require("gettext") +local T = require("ffi/util").template + +-- markdown exporter +local MarkdownExporter = require("base"):new { + name = "markdown", + extension = "md", + init_callback = function(self, settings) + local changed = false + if not settings.formatting_options or settings.highlight_formatting == nil then + settings.formatting_options = settings.formatting_options or { + lighten = "italic", + underscore = "underline_markdownit", + strikeout = "strikethrough", + invert = "bold", + } + settings.highlight_formatting = settings.highlight_formatting or true + changed = true + end + return changed, settings + end, +} + +local formatter_buttons = { + { _("None"), "none" }, + { _("Bold"), "bold" }, + { _("Bold italic"), "bold_italic" }, + { _("Italic"), "italic" }, + { _("Strikethrough"), "strikethrough" }, + { _("Underline (Markdownit style, with ++)"), "underline_markdownit" }, + { _("Underline (with tags)"), "underline_u_tag" }, +} + +function MarkdownExporter:editFormatStyle(drawer_style, label, touchmenu_instance) + local radio_buttons = {} + for _idx, v in ipairs(formatter_buttons) do + table.insert(radio_buttons, { + { + text = v[1], + checked = self.settings.formatting_options[drawer_style] == v[2], + provider = v[2], + }, + }) + end + UIManager:show(require("ui/widget/radiobuttonwidget"):new { + title_text = T(_("Formatting style for %1"), label), + width_factor = 0.8, + radio_buttons = radio_buttons, + callback = function(radio) + self.settings.formatting_options[drawer_style] = radio.provider + touchmenu_instance:updateItems() + end, + }) +end + +function MarkdownExporter:onInit() + local changed = false + if self.settings.formatting_options == nil then + self.settings.formatting_options = { + lighten = "italic", + underscore = "underline_markdownit", + strikeout = "strikethrough", + invert = "bold", + } + changed = true + end + if self.settings.highlight_formatting == nil then + self.settings.highlight_formatting = true + changed = true + end + if changed then + self:saveSettings() + end +end + +local highlight_style = { + { _("Lighten"), "lighten" }, + { _("Underline"), "underscore" }, + { _("Strikeout"), "strikeout" }, + { _("Invert"), "invert" }, +} + +function MarkdownExporter:getMenuTable() + local menu = { + text = _("Markdown"), + checked_func = function() return self:isEnabled() end, + sub_item_table = { + { + text = _("Export to Markdown"), + checked_func = function() return self:isEnabled() end, + callback = function() self:toggleEnabled() end, + }, + { + text = _("Format highlights based on style"), + checked_func = function() return self.settings.highlight_formatting end, + callback = function() self.settings.highlight_formatting = not self.settings.highlight_formatting end, + }, + } + } + + for _idx, entry in ipairs(highlight_style) do + table.insert(menu.sub_item_table, { + text_func = function() + return entry[1] .. ": " .. md.formatters[self.settings.formatting_options[entry[2]]].label + end, + enabled_func = function() + return self.settings.highlight_formatting + end, + keep_menu_open = true, + callback = function(touchmenu_instance) + self:editFormatStyle(entry[2], entry[1], touchmenu_instance) + end, + }) + end + return menu +end + +function MarkdownExporter:export(t) + local path = self:getFilePath(t) + local file = io.open(path, "w") + if not file then return false end + for idx, book in ipairs(t) do + file:write(md.prepareBookContent(book, self.settings.formatting_options, self.settings.highlight_formatting)) + if idx < #t then + file:write("\n") + end + end + file:write("\n\n_Generated at: " .. self:getTimeStamp() .. "_") + file:close() + return true +end + +return MarkdownExporter diff --git a/plugins/exporter.koplugin/template/md.lua b/plugins/exporter.koplugin/template/md.lua new file mode 100644 index 000000000..b11f67b59 --- /dev/null +++ b/plugins/exporter.koplugin/template/md.lua @@ -0,0 +1,62 @@ +local _ = require("gettext") + +local formatters = { + none = { + formatter = "%s", + label = _("None") + }, + bold = { + formatter = "**%s**", + label = _("Bold") + }, + italic = { + formatter = "*%s*", + label = _("Italic") + }, + bold_italic = { + formatter = "**_%s_**", + label = _("Bold italic") + }, + underline_markdownit = { + formatter = "++%s++", + label = _("Underline (Markdownit style, with ++)") + }, + underline_u_tag = { + formatter = "%s", + label = _("Underline (with tags)") + }, + strikethrough = { + formatter = "~~%s~~", + label = _("Strikethrough") + }, +} + +local function prepareBookContent(book, formatting_options, highlight_formatting) + local content = "" + local current_chapter = nil + content = content .. "# " .. book.title .. "\n" + content = content .. "##### " .. book.author:gsub("\n", ", ") .. "\n\n" + for _, note in ipairs(book) do + local entry = note[1] + if entry.chapter ~= current_chapter then + current_chapter = entry.chapter + content = content .. "## " .. current_chapter .. "\n" + end + content = content .. "### Page " .. entry.page .. " @ " .. os.date("%d %B %Y %I:%M %p", entry.time) .. "\n" + if highlight_formatting then + content = content .. string.format(formatters[formatting_options[entry.drawer]].formatter, entry.text) .."\n" + else + content = content .. entry.text .. "\n" + end + if entry.note then + content = content .. "\n---\n" .. entry.note .. "\n" + end + content = content .. "\n" + end + return content +end + +return { + prepareBookContent = prepareBookContent, + formatters = formatters +}