mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
Includes: - Fix a few clang-tidy warnings - Add support for <img src="data:image/png;base64,...> - XML parsing: add more HTML5 named entities, optimize search - Text: fix standalone BR not making an empty line - Fix BR with "display: block" not making an empty line - Fix hyphens from soft-hyphens not part of highlighted segments - Use libunibreak for line breaking - Adds TextLangMan for text typography by language Tweak ReaderHyphenation to work with the new TextLangMan (even if it will be replaced soon by ReaderTypography).
312 lines
14 KiB
Lua
312 lines
14 KiB
Lua
local BD = require("ui/bidi")
|
||
local Device = require("device")
|
||
local Event = require("ui/event")
|
||
local InfoMessage = require("ui/widget/infomessage")
|
||
local InputContainer = require("ui/widget/container/inputcontainer")
|
||
local JSON = require("json")
|
||
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
|
||
local UIManager = require("ui/uimanager")
|
||
local logger = require("logger")
|
||
local util = require("util")
|
||
local _ = require("gettext")
|
||
local C_ = _.pgettext
|
||
local T = require("ffi/util").template
|
||
local Screen = Device.screen
|
||
|
||
local ReaderHyphenation = InputContainer:new{
|
||
hyph_menu_title = _("Hyphenation"),
|
||
hyph_table = nil,
|
||
}
|
||
|
||
function ReaderHyphenation:init()
|
||
self.lang_table = {}
|
||
self.hyph_table = {}
|
||
self.hyph_algs_settings = {}
|
||
self.hyph_alg = cre.getSelectedHyphDict()
|
||
|
||
table.insert(self.hyph_table, {
|
||
text_func = function()
|
||
-- Note: with our callback, we either get hyph_left_hyphen_min and
|
||
-- hyph_right_hyphen_min both nil, or both defined.
|
||
if G_reader_settings:readSetting("hyph_left_hyphen_min") or
|
||
G_reader_settings:readSetting("hyph_right_hyphen_min") then
|
||
-- @translators to RTL language translators: %1/left is the min length of the start of a hyphenated word, %2/right is the min length of the end of a hyphenated word (note that there is yet no support for hyphenation with RTL languages, so this will mostly apply to LTR documents)
|
||
return T(_("Left/right minimal sizes: %1 - %2"),
|
||
G_reader_settings:readSetting("hyph_left_hyphen_min"),
|
||
G_reader_settings:readSetting("hyph_right_hyphen_min"))
|
||
end
|
||
return _("Left/right minimal sizes: language defaults")
|
||
end,
|
||
callback = function()
|
||
local DoubleSpinWidget = require("/ui/widget/doublespinwidget")
|
||
local hyph_settings = self.hyph_algs_settings[self.hyph_alg] or {}
|
||
local alg_left_hyphen_min = hyph_settings.left_hyphen_min
|
||
local alg_right_hyphen_min = hyph_settings.right_hyphen_min
|
||
local hyph_limits_widget = DoubleSpinWidget:new{
|
||
-- Min (1) and max (10) values are enforced by crengine
|
||
-- Note that when hitting "Use language defaults", we show the default
|
||
-- values from languages.json, but we give 0 to crengine, which will
|
||
-- use its own default hardcoded values (in textlang.cpp).
|
||
-- Try to keep these values in sync.
|
||
left_value = G_reader_settings:readSetting("hyph_left_hyphen_min") or alg_left_hyphen_min or 2,
|
||
left_min = 1,
|
||
left_max = 10,
|
||
right_value = G_reader_settings:readSetting("hyph_right_hyphen_min") or alg_right_hyphen_min or 2,
|
||
right_min = 1,
|
||
right_max = 10,
|
||
left_default = alg_left_hyphen_min or 2,
|
||
right_default = alg_right_hyphen_min or 2,
|
||
-- let room on the widget sides so we can see
|
||
-- the hyphenation changes happening
|
||
width = Screen:getWidth() * 0.6,
|
||
default_values = true,
|
||
default_text = _("Use language defaults"),
|
||
title_text = _("Hyphenation limits"),
|
||
info_text = _([[
|
||
Set minimum length before hyphenation occurs.
|
||
These settings will apply to all books with any hyphenation dictionary.
|
||
'Use language defaults' resets them.]]),
|
||
callback = function(left_hyphen_min, right_hyphen_min)
|
||
G_reader_settings:saveSetting("hyph_left_hyphen_min", left_hyphen_min)
|
||
G_reader_settings:saveSetting("hyph_right_hyphen_min", right_hyphen_min)
|
||
self.ui.document:setHyphLeftHyphenMin(G_reader_settings:readSetting("hyph_left_hyphen_min") or 0)
|
||
self.ui.document:setHyphRightHyphenMin(G_reader_settings:readSetting("hyph_right_hyphen_min") or 0)
|
||
-- signal readerrolling to update pos in new height, and redraw page
|
||
self.ui:handleEvent(Event:new("UpdatePos"))
|
||
end
|
||
}
|
||
UIManager:show(hyph_limits_widget)
|
||
end,
|
||
enabled_func = function()
|
||
return self.hyph_alg ~= "@none"
|
||
end,
|
||
})
|
||
table.insert(self.hyph_table, {
|
||
text = _("Trust soft hyphens"),
|
||
callback = function()
|
||
G_reader_settings:flipNilOrFalse("hyph_trust_soft_hyphens")
|
||
self.ui.document:setTrustSoftHyphens(G_reader_settings:isTrue("hyph_trust_soft_hyphens"))
|
||
self.ui:handleEvent(Event:new("UpdatePos"))
|
||
end,
|
||
checked_func = function()
|
||
-- for @none and @softhyphens, set the checkbox to reflect how
|
||
-- these trust soft hyphens, no matter what our setting is
|
||
if self.hyph_alg == "@none" then
|
||
return false
|
||
elseif self.hyph_alg == "@softhyphens" then
|
||
return true
|
||
else
|
||
return G_reader_settings:isTrue("hyph_trust_soft_hyphens")
|
||
end
|
||
end,
|
||
enabled_func = function()
|
||
return self.hyph_alg ~= "@none" and self.hyph_alg ~= "@softhyphens"
|
||
end,
|
||
separator = true,
|
||
})
|
||
|
||
local lang_data_file = assert(io.open("./data/hyph/languages.json"), "r")
|
||
local ok, lang_data = pcall(JSON.decode, lang_data_file:read("*all"))
|
||
|
||
if ok and lang_data then
|
||
for k,v in ipairs(lang_data) do
|
||
self.hyph_algs_settings[v.filename] = v -- just store full table
|
||
table.insert(self.hyph_table, {
|
||
text_func = function()
|
||
local text = v.name
|
||
if v.filename == G_reader_settings:readSetting("hyph_alg_default") then
|
||
text = text .. " ★"
|
||
end
|
||
if v.filename == G_reader_settings:readSetting("hyph_alg_fallback") then
|
||
text = text .. " <20>"
|
||
end
|
||
return text
|
||
end,
|
||
callback = function()
|
||
self.hyph_alg = v.filename
|
||
self.ui.doc_settings:saveSetting("hyph_alg", self.hyph_alg)
|
||
UIManager:show(InfoMessage:new{
|
||
text = T(_("Changed hyphenation to %1."), BD.wrap(v.name)),
|
||
})
|
||
self.ui.document:setHyphDictionary(v.filename)
|
||
-- (No need to apply hyphenation sides limits: previous values will stick)
|
||
-- signal readerrolling to update pos in new height, and redraw page
|
||
self.ui:handleEvent(Event:new("UpdatePos"))
|
||
end,
|
||
hold_callback = function(touchmenu_instance)
|
||
UIManager:show(MultiConfirmBox:new{
|
||
-- No real need for a way to remove default one, we can just
|
||
-- toggle between setting a default OR a fallback (if a default
|
||
-- one is set, no fallback will ever be used - if a fallback one
|
||
-- is set, no default is wanted; so when we set one below, we
|
||
-- remove the other).
|
||
text = T( _("Would you like %1 to be used as the default (★) or fallback (<28>) hyphenation language?\n\nDefault will always take precedence while fallback will only be used if the language of the book can't be automatically determined."), BD.wrap(v.name)),
|
||
choice1_text = _("Default"),
|
||
choice1_callback = function()
|
||
G_reader_settings:saveSetting("hyph_alg_default", v.filename)
|
||
G_reader_settings:delSetting("hyph_alg_fallback")
|
||
if touchmenu_instance then touchmenu_instance:updateItems() end
|
||
end,
|
||
choice2_text = C_("Hyphenation", "Fallback"),
|
||
choice2_callback = function()
|
||
G_reader_settings:saveSetting("hyph_alg_fallback", v.filename)
|
||
G_reader_settings:delSetting("hyph_alg_default")
|
||
if touchmenu_instance then touchmenu_instance:updateItems() end
|
||
end,
|
||
})
|
||
end,
|
||
checked_func = function()
|
||
return v.filename == self.hyph_alg
|
||
end,
|
||
separator = v.separator,
|
||
})
|
||
|
||
self.lang_table[v.language] = v.filename
|
||
if v.aliases then
|
||
for i,alias in ipairs(v.aliases) do
|
||
self.lang_table[alias] = v.filename
|
||
end
|
||
end
|
||
end
|
||
end
|
||
self.ui.menu:registerToMainMenu(self)
|
||
end
|
||
|
||
function ReaderHyphenation:parseLanguageTag(lang_tag)
|
||
-- Parse an RFC 5646 language tag, like "en-US" or "en".
|
||
-- https://tools.ietf.org/html/rfc5646
|
||
|
||
-- We are only interested in the language and region parts.
|
||
local language = nil
|
||
local region = nil
|
||
|
||
for part in util.gsplit(lang_tag, "-", false) do
|
||
if not language then
|
||
language = string.lower(part)
|
||
elseif string.len(part) == 2 and not string.match(part, "[^%a]") then
|
||
region = string.upper(part)
|
||
end
|
||
end
|
||
return language, region
|
||
end
|
||
|
||
function ReaderHyphenation:getDictForLanguage(lang_tag)
|
||
-- EPUB language is an RFC 5646 language tag.
|
||
-- http://www.idpf.org/epub/301/spec/epub-publications.html#sec-opf-dclanguage
|
||
--
|
||
-- FB2 language is a two-letter language code
|
||
-- (which is also a valid RFC 5646 language tag).
|
||
-- http://fictionbook.org/index.php/%D0%AD%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82_lang (in Russian)
|
||
|
||
local language, region = self:parseLanguageTag(lang_tag)
|
||
if not language then
|
||
return
|
||
end
|
||
|
||
local dict
|
||
if region then
|
||
dict = self.lang_table[language .. '-' .. region]
|
||
end
|
||
if not dict then
|
||
dict = self.lang_table[language]
|
||
end
|
||
return dict
|
||
end
|
||
|
||
-- Setting the hyph algo before loading the document may save crengine
|
||
-- from re-doing some expensive work at render time (the hyph algo
|
||
-- is accounted in the nodeStyleHash, and would cause a mismatch if it is
|
||
-- different at render time from how it was at load time - "English US" by
|
||
-- default - causing a full re-init of the nodes styles.)
|
||
-- We will only re-set it on pre-render (only then, after loading, we
|
||
-- know the document language) if it's really needed: when no algo saved
|
||
-- in book settings, no default algo, and book has some language defined.
|
||
function ReaderHyphenation:onReadSettings(config)
|
||
-- Decide and set the adequate hyph algorithm according to settings
|
||
local hyph_alg
|
||
self.allow_doc_lang_hyph_alg_override = false
|
||
|
||
-- Use the one manually set for this document
|
||
hyph_alg = config:readSetting("hyph_alg")
|
||
if hyph_alg then
|
||
logger.dbg("Hyphenation: using", hyph_alg, "from doc settings")
|
||
self:setHyphAlgo(hyph_alg)
|
||
return
|
||
end
|
||
|
||
-- Use the one manually set as default (with Hold)
|
||
hyph_alg = G_reader_settings:readSetting("hyph_alg_default")
|
||
if hyph_alg then
|
||
logger.dbg("Hyphenation: using default ", hyph_alg)
|
||
self:setHyphAlgo(hyph_alg)
|
||
return
|
||
end
|
||
|
||
-- Document language will be allowed to override the one we set from now on
|
||
self.allow_doc_lang_hyph_alg_override = true
|
||
|
||
-- Use the one manually set as fallback (with Hold)
|
||
hyph_alg = G_reader_settings:readSetting("hyph_alg_fallback")
|
||
if hyph_alg then
|
||
logger.dbg("Hyphenation: using fallback ", hyph_alg, ", might be overriden by doc language")
|
||
self:setHyphAlgo(hyph_alg)
|
||
return
|
||
end
|
||
|
||
-- None decided, get back the current one set in crengine
|
||
logger.dbg("Hyphenation: no algo set")
|
||
self:setHyphAlgo()
|
||
logger.dbg("Hyphenation: keeping current crengine algo:", self.hyph_alg)
|
||
-- Note: it would be better to select English_US here, rather than
|
||
-- keeping the hyph dict possibly set from the previous book language,
|
||
-- to keep it consistent and avoid a re-rendering on the new book.
|
||
end
|
||
|
||
function ReaderHyphenation:onPreRenderDocument(config)
|
||
-- This is called after the document has been loaded
|
||
-- so we can access the document language.
|
||
local doc_language = self.ui.document:getProps().language
|
||
if not self.allow_doc_lang_hyph_alg_override then
|
||
logger.dbg("Hyphenation: not overriding", self.hyph_alg, "with doc language:",
|
||
(doc_language and doc_language or "none"))
|
||
elseif not doc_language then
|
||
logger.dbg("Hyphenation: no doc language, keeping", self.hyph_alg)
|
||
else
|
||
local hyph_alg = self:getDictForLanguage(doc_language)
|
||
if not hyph_alg then
|
||
logger.dbg("Hyphenation: no algo found for doc language:", doc_language, ", keeping", self.hyph_alg)
|
||
else
|
||
if hyph_alg == self.hyph_alg then
|
||
logger.dbg("Hyphenation: current", self.hyph_alg, "is right for doc language:", doc_language)
|
||
else
|
||
logger.dbg("Hyphenation: updating for doc language", doc_language, ":", self.hyph_alg, "=>", hyph_alg)
|
||
self:setHyphAlgo(hyph_alg)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
function ReaderHyphenation:setHyphAlgo(hyph_alg)
|
||
if hyph_alg then
|
||
self.ui.document:setHyphDictionary(hyph_alg)
|
||
end
|
||
-- If we haven't set any (nil, or invalid), hardcoded
|
||
-- English_US.pattern (in cre.cpp) will be used
|
||
self.hyph_alg = cre.getSelectedHyphDict()
|
||
-- Apply hyphenation sides limits (default to 0, to use language defaults)
|
||
self.ui.document:setHyphLeftHyphenMin(G_reader_settings:readSetting("hyph_left_hyphen_min") or 0)
|
||
self.ui.document:setHyphRightHyphenMin(G_reader_settings:readSetting("hyph_right_hyphen_min") or 0)
|
||
self.ui.document:setTrustSoftHyphens(G_reader_settings:isTrue("hyph_trust_soft_hyphens"))
|
||
end
|
||
|
||
function ReaderHyphenation:addToMainMenu(menu_items)
|
||
self.hyph_table.max_per_page = 5
|
||
-- insert table to main reader menu
|
||
menu_items.hyphenation = {
|
||
text = self.hyph_menu_title,
|
||
sub_item_table = self.hyph_table,
|
||
}
|
||
end
|
||
|
||
return ReaderHyphenation
|