From 594b4c90352d4e739074c2ab260563bbaa88ae6c Mon Sep 17 00:00:00 2001 From: zwim <36999612+zwim@users.noreply.github.com> Date: Tue, 1 Jun 2021 12:06:55 +0200 Subject: [PATCH] Add option for custom hyphenation rules (#7787) This is the successor of #7746. --- base | 2 +- .../apps/reader/modules/readerhighlight.lua | 14 ++ .../apps/reader/modules/readertypography.lua | 8 +- .../apps/reader/modules/readeruserhyph.lua | 228 ++++++++++++++++++ frontend/apps/reader/readerui.lua | 7 + frontend/document/credocument.lua | 19 ++ 6 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 frontend/apps/reader/modules/readeruserhyph.lua diff --git a/base b/base index 96c002d52..502bba07e 160000 --- a/base +++ b/base @@ -1 +1 @@ -Subproject commit 96c002d5260b09148def07913b5e4363d9a58c23 +Subproject commit 502bba07e57e652dcf85f0b2a5c6b1d52fbc0f75 diff --git a/frontend/apps/reader/modules/readerhighlight.lua b/frontend/apps/reader/modules/readerhighlight.lua index e3cc9296f..9fbaf0c76 100644 --- a/frontend/apps/reader/modules/readerhighlight.lua +++ b/frontend/apps/reader/modules/readerhighlight.lua @@ -174,6 +174,20 @@ function ReaderHighlight:init() } end) + -- User hyphenation dict + self:addToHighlightDialog("11_user_dict", function(_self) + return { + text= _("Hyphenate"), + show_in_highlight_dialog_func = function() + return _self.ui.userhyph and _self.ui.userhyph:isAvailable() and not _self.selected_text.text:find("[ ,;-%.\n]") + end, + callback = function() + _self.ui.userhyph:modifyUserEntry(_self.selected_text.text) + _self:onClose() + end, + } + end) + self.ui:registerPostInitCallback(function() self.ui.menu:registerToMainMenu(self) end) diff --git a/frontend/apps/reader/modules/readertypography.lua b/frontend/apps/reader/modules/readertypography.lua index 5ad82e922..660c4d7ea 100644 --- a/frontend/apps/reader/modules/readertypography.lua +++ b/frontend/apps/reader/modules/readertypography.lua @@ -237,6 +237,7 @@ When the book's language tag is not among our presets, no specific features will }) self.text_lang_tag = lang_tag self.ui.document:setTextMainLang(lang_tag) + self.ui:handleEvent(Event:new("TypographyLanguageChanged")) self.ui:handleEvent(Event:new("UpdatePos")) end, hold_callback = function(touchmenu_instance) @@ -427,8 +428,8 @@ These settings will apply to all books with any hyphenation dictionary. enabled_func = function() return self.hyphenation and not self.hyph_soft_hyphens_only end, - separator = true, }) + table.insert(hyphenation_submenu, self.ui.userhyph:getMenuEntry()) table.insert(hyphenation_submenu, { text_func = function() -- Show the current language default hyph dict (ie: English_US for zh) @@ -488,7 +489,7 @@ These settings will apply to all books with any hyphenation dictionary. end, }) table.insert(hyphenation_submenu, { - text = _("Soft-hyphens only"), + text = _("Soft hyphens only"), callback = function() self.hyph_soft_hyphens_only = not self.hyph_soft_hyphens_only self.hyph_force_algorithmic = false @@ -760,6 +761,7 @@ function ReaderTypography:onReadSettings(config) logger.dbg("Typography lang: no lang set, using", self.text_lang_tag) end self.ui.document:setTextMainLang(self.text_lang_tag) + self.ui:handleEvent(Event:new("TypographyLanguageChanged")) end function ReaderTypography:onPreRenderDocument(config) @@ -780,6 +782,7 @@ function ReaderTypography:onPreRenderDocument(config) self.text_lang_tag = self.book_lang_tag self.ui.doc_settings:saveSetting("text_lang", self.text_lang_tag) self.ui.document:setTextMainLang(self.text_lang_tag) + self.ui:handleEvent(Event:new("TypographyLanguageChanged")) self.ui:handleEvent(Event:new("UpdatePos")) end, enabled_func = function() @@ -809,6 +812,7 @@ function ReaderTypography:onPreRenderDocument(config) end self.text_lang_tag = self.book_lang_tag self.ui.document:setTextMainLang(self.text_lang_tag) + self.ui:handleEvent(Event:new("TypographyLanguageChanged")) end end diff --git a/frontend/apps/reader/modules/readeruserhyph.lua b/frontend/apps/reader/modules/readeruserhyph.lua new file mode 100644 index 000000000..c9863b349 --- /dev/null +++ b/frontend/apps/reader/modules/readeruserhyph.lua @@ -0,0 +1,228 @@ +local DataStorage = require("datastorage") +local Event = require("ui/event") +local FFIUtil = require("ffi/util") +local InfoMessage = require("ui/widget/infomessage") +local InputDialog = require("ui/widget/inputdialog") +local UIManager = require("ui/uimanager") +local WidgetContainer = require("ui/widget/container/widgetcontainer") +local lfs = require("libs/libkoreader-lfs") +local logger = require("logger") +local _ = require("gettext") +local T = require("ffi/util").template + +local ReaderUserHyph = WidgetContainer:new{ + -- return values from setUserHyphenationDict (crengine's UserHyphDict::init()) + USER_DICT_RELOAD = 0, + USER_DICT_NOCHANGE = 1, + USER_DICT_MALFORMED = 2, + USER_DICT_ERROR_NOT_SORTED = 3, +} + +-- returns path to the user dictionary +function ReaderUserHyph:getDictionaryPath() + return FFIUtil.joinPath(DataStorage:getSettingsDir(), + "user-" .. tostring(self.ui.document:getTextMainLangDefaultHyphDictionary():gsub(".pattern$", "")) .. ".hyph") +end + +-- Load the user dictionary suitable for the actual language +-- if reload==true, force a reload +-- Unload is done automatically when a new dictionary is loaded. +function ReaderUserHyph:loadDictionary(name, reload) + if G_reader_settings:isTrue("hyph_user_dict") and lfs.attributes(name, "mode") == "file" then + local ret = self.ui.document:setUserHyphenationDict(name, reload) + -- this should only happen, if a user edits a dictionary by hand or the user messed + -- with the dictionary file by hand. -> Warning and disable. + if ret == self.USER_DICT_ERROR_NOT_SORTED then + UIManager:show(InfoMessage:new{ + text = T(_("The user dictionary\n%1\nis not alphabetically sorted.\n\nIt has been disabled."), name), + }) + logger.warn("UserHyph: Dictionary " .. name .. " is not sorted alphabetically.") + G_reader_settings:makeFalse("hyph_user_dict") + elseif ret == self.USER_DICT_MALFORMED then + UIManager:show(InfoMessage:new{ + text = T(_("The user dictionary\n%1\nhas corrupted entries.\n\nOnly valid entries will be used."), name), + }) + logger.warn("UserHyph: Dictionary " .. name .. " has corrupted entries.") + end + else + self.ui.document:setUserHyphenationDict() -- clear crengine user hyph dict + end +end + +-- Reload on change of the hyphenation language +function ReaderUserHyph:onTypographyLanguageChanged() + self:loadUserDictionary() +end + +-- Reload on "ChangedUserDictionary" event, +-- doesn't load dictionary if filesize and filename haven't changed +-- if reload==true reload +function ReaderUserHyph:loadUserDictionary(reload) + self:loadDictionary(self:isAvailable() and self:getDictionaryPath() or "", reload and true or false) + self.ui:handleEvent(Event:new("UpdatePos")) +end + +-- Functions to use with the UI + +function ReaderUserHyph:isAvailable() + return G_reader_settings:isTrue("hyph_user_dict") and self:_enabled() +end + +function ReaderUserHyph:_enabled() + return self.ui.typography.hyphenation +end + +-- add Menu entry +function ReaderUserHyph:getMenuEntry() + return { + text = _("Custom hyphenation rules"), + help_text = _("The hyphenation of a word can be changed from its default by long pressing for 3 seconds and selecting 'Hyphenate'."), + callback = function() + local hyph_user_dict = not G_reader_settings:isTrue("hyph_user_dict") + G_reader_settings:saveSetting("hyph_user_dict", hyph_user_dict) + self:loadUserDictionary() -- not needed to force a reload here + end, + checked_func = function() + return self:isAvailable() + end, + enabled_func = function() + return self:_enabled() + end, + separator = true, + } +end + +-- Helper functions for dictionary entries------------------------------------------- + +-- checks if suggestion is well formated +function ReaderUserHyph:checkHyphenation(suggestion, word) + if suggestion:find("%-%-") then + return false -- two or more consecutive '-' + end + + suggestion = suggestion:gsub("-","") + if self.ui.document:getLowercasedWord(suggestion) == self.ui.document:getLowercasedWord(word) then + return true -- characters match (case insensitive) + end + return false +end + +function ReaderUserHyph:updateDictionary(word, hyphenation) + local dict_file = self:getDictionaryPath() + local new_dict_file = dict_file .. ".new" + + local new_dict = io.open(new_dict_file, "w") + if not new_dict then + logger.err("UserHyph: could not open " .. new_dict_file) + return + end + + local word_lower = self.ui.document:getLowercasedWord(word) + local line + + local dict = io.open(dict_file, "r") + if dict then + line = dict:read() + --search entry + while line and self.ui.document:getLowercasedWord(line:sub(1, line:find(";") - 1)) < word_lower do + new_dict:write(line .. "\n") + line = dict:read() + end + + -- last word = nil if EOF, else last_word=word if found in file, else last_word is word after the new entry + if line then + local last_word = self.ui.document:getLowercasedWord(line:sub(1, line:find(";") - 1)) + if last_word == self.ui.document:getLowercasedWord(word) then + line = nil -- word found + end + else + line = nil -- EOF + end + end + + -- write new entry + if hyphenation and hyphenation ~= "" then + new_dict:write(string.format("%s;%s\n", word, hyphenation)) + end + + -- write old entry if there was one + if line then + new_dict:write(line .. "\n") + end + + if dict then + repeat + line = dict:read() + if line then + new_dict:write(line .. "\n") + end + until (not line) + dict:close() + os.remove(dict_file) + end + + new_dict:close() + os.rename(new_dict_file, dict_file) + + self:loadUserDictionary(true) -- dictionary has changed, force a reload here +end + +function ReaderUserHyph:modifyUserEntry(word) + if word:find("[ ,;-%.]") then return end -- no button if more than one word + + if not self.ui.document then return end + + local suggested_hyphenation = self.ui.document:getHyphenationForWord(word) + + local input_dialog + input_dialog = InputDialog:new{ + title = T(_("Hyphenate: %1"), word), + description = _("Add hyphenation positions with hyphens ('-') or spaces (' ')."), + input = suggested_hyphenation, + old_hyph_lowercase = self.ui.document:getLowercasedWord(suggested_hyphenation), + input_type = "string", + buttons = { + { + { + text = _("Cancel"), + callback = function() + UIManager:close(input_dialog) + end, + }, + { + text = _("Remove"), + callback = function() + UIManager:close(input_dialog) + self:updateDictionary(word) + end, + }, + { + text = _("Save"), + is_enter_default = true, + callback = function() + local new_suggestion = input_dialog:getInputText() + new_suggestion = new_suggestion:gsub(" ","-") -- replace spaces with hyphens + new_suggestion = new_suggestion:gsub("^-","") -- remove leading hypenations + new_suggestion = new_suggestion:gsub("-$","") -- remove trailing hypenations + + if self:checkHyphenation(new_suggestion, word) then + -- don't save if no changes + if self.ui.document:getLowercasedWord(new_suggestion) ~= input_dialog.old_hyph_lowercase then + self:updateDictionary(word, new_suggestion) + end + UIManager:close(input_dialog) + else + UIManager:show(InfoMessage:new{ + text = T(_("Invalid hyphenation!"), self.dict_file), + }) + end + end, + }, + }, + }, + } + UIManager:show(input_dialog) + input_dialog:onShowKeyboard() +end + +return ReaderUserHyph diff --git a/frontend/apps/reader/readerui.lua b/frontend/apps/reader/readerui.lua index 85e0c7772..03452a04a 100644 --- a/frontend/apps/reader/readerui.lua +++ b/frontend/apps/reader/readerui.lua @@ -48,6 +48,7 @@ local ReaderStyleTweak = require("apps/reader/modules/readerstyletweak") local ReaderToc = require("apps/reader/modules/readertoc") local ReaderTypeset = require("apps/reader/modules/readertypeset") local ReaderTypography = require("apps/reader/modules/readertypography") +local ReaderUserHyph = require("apps/reader/modules/readeruserhyph") local ReaderView = require("apps/reader/modules/readerview") local ReaderWikipedia = require("apps/reader/modules/readerwikipedia") local ReaderZooming = require("apps/reader/modules/readerzooming") @@ -314,6 +315,12 @@ function ReaderUI:init() view = self.view, ui = self }) + -- user hyphenation (must be registered before typography) + self:registerModule("userhyph", ReaderUserHyph:new{ + dialog = self.dialog, + view = self.view, + ui = self + }) -- typography menu (replaces previous hyphenation menu / ReaderHyphenation) self:registerModule("typography", ReaderTypography:new{ dialog = self.dialog, diff --git a/frontend/document/credocument.lua b/frontend/document/credocument.lua index a8b52029c..745908e89 100644 --- a/frontend/document/credocument.lua +++ b/frontend/document/credocument.lua @@ -980,6 +980,25 @@ function CreDocument:setTextHyphenationSoftHyphensOnly(toggle) self._document:setStringProperty("crengine.textlang.hyphenation.soft.hyphens.only", toggle and 1 or 0) end +function CreDocument:setUserHyphenationDict(dict, reload) + logger.dbg("CreDocument: set textlang hyphenation dict", dict or "none") + return self._document:setUserHyphenationDict(dict or "", reload or false) +end + +function CreDocument:getHyphenationForWord(word) + if word then + return self._document:getHyphenationForWord(word) + end + return word +end + +function CreDocument:getLowercasedWord(word) + if word then + return self._document:getLowercasedWord(word) + end + return word +end + function CreDocument:setTextHyphenationForceAlgorithmic(toggle) logger.dbg("CreDocument: set textlang hyphenation force algorithmic", toggle) self._document:setStringProperty("crengine.textlang.hyphenation.force.algorithmic", toggle and 1 or 0)