mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
HTML dictionary link support (#3603)
This commit is contained in:
@@ -2,6 +2,7 @@ local ConfirmBox = require("ui/widget/confirmbox")
|
||||
local DataStorage = require("datastorage")
|
||||
local Device = require("device")
|
||||
local DictQuickLookup = require("ui/widget/dictquicklookup")
|
||||
local Geom = require("ui/geometry")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local InputContainer = require("ui/widget/container/inputcontainer")
|
||||
local JSON = require("json")
|
||||
@@ -146,6 +147,8 @@ function ReaderDictionary:init()
|
||||
end
|
||||
|
||||
function ReaderDictionary:updateSdcvDictNamesOptions()
|
||||
self.enabled_dict_names = nil
|
||||
|
||||
-- We cannot tell sdcv which dictionaries to ignore, but we
|
||||
-- can tell it which dictionaries to use, by using multiple
|
||||
-- -u <dictname> options.
|
||||
@@ -153,28 +156,16 @@ function ReaderDictionary:updateSdcvDictNamesOptions()
|
||||
-- them for ordering queries and results)
|
||||
local dicts_disabled = G_reader_settings:readSetting("dicts_disabled")
|
||||
if not next(dicts_disabled) then
|
||||
-- no dict disabled, no need to use any -u option
|
||||
self.sdcv_dictnames_options_raw = nil
|
||||
self.sdcv_dictnames_options_escaped = nil
|
||||
return
|
||||
end
|
||||
local u_options_raw = {} -- for android call (individual unesscaped elements)
|
||||
local u_options_escaped = {} -- for other devices call via shell
|
||||
for _, ifo in pairs(available_ifos) do
|
||||
if not dicts_disabled[ifo.file] then
|
||||
table.insert(u_options_raw, "-u")
|
||||
table.insert(u_options_raw, ifo.name)
|
||||
-- Escape chars in dictname so it's ok for the shell command
|
||||
-- local u_esc = ("-u %q"):format(ifo.name)
|
||||
-- This may be safer than using lua's %q:
|
||||
local u_esc = "-u '" .. ifo.name:gsub("'", "'\\''") .. "'"
|
||||
table.insert(u_options_escaped, u_esc)
|
||||
if not self.enabled_dict_names then
|
||||
self.enabled_dict_names = {}
|
||||
end
|
||||
table.insert(self.enabled_dict_names, ifo.name)
|
||||
end
|
||||
-- Note: if all dicts are disabled, we won't get any -u, and so
|
||||
-- all dicts will be queried.
|
||||
end
|
||||
self.sdcv_dictnames_options_raw = u_options_raw
|
||||
self.sdcv_dictnames_options_escaped = table.concat(u_options_escaped, " ")
|
||||
end
|
||||
|
||||
function ReaderDictionary:addToMainMenu(menu_items)
|
||||
@@ -306,14 +297,58 @@ If you'd like to change the order in which dictionaries are queried (and their r
|
||||
end
|
||||
|
||||
function ReaderDictionary:onLookupWord(word, box, highlight, link)
|
||||
logger.dbg("dict lookup word:", word, box)
|
||||
-- escape quotes and other funny characters in word
|
||||
word = self:cleanSelection(word)
|
||||
logger.dbg("dict stripped word:", word)
|
||||
|
||||
self.highlight = highlight
|
||||
|
||||
-- Wrapped through Trapper, as we may be using Trapper:dismissablePopen() in it
|
||||
Trapper:wrap(function()
|
||||
self:stardictLookup(word, box, link)
|
||||
self:stardictLookup(word, self.enabled_dict_names, not self.disable_fuzzy_search, box, link)
|
||||
end)
|
||||
return true
|
||||
end
|
||||
|
||||
function ReaderDictionary:onHtmlDictionaryLinkTapped(dictionary, link)
|
||||
if not link.uri then
|
||||
return
|
||||
end
|
||||
|
||||
-- The protocol is either "bword" or there is no protocol, only the word.
|
||||
-- https://github.com/koreader/koreader/issues/3588#issuecomment-357088125
|
||||
local url_prefix = "bword://"
|
||||
local word
|
||||
if link.uri:sub(1,url_prefix:len()) == url_prefix then
|
||||
word = link.uri:sub(url_prefix:len() + 1)
|
||||
elseif link.uri:find("://") then
|
||||
return
|
||||
else
|
||||
word = link.uri
|
||||
end
|
||||
|
||||
if word == "" then
|
||||
return
|
||||
end
|
||||
|
||||
local link_box = Geom:new{
|
||||
x = link.x0,
|
||||
y = link.y0,
|
||||
w = math.abs(link.x1 - link.x0),
|
||||
h = math.abs(link.y1 - link.y0),
|
||||
}
|
||||
|
||||
-- Only the first dictionary window stores the highlight, this way the highlight
|
||||
-- is only removed when there are no more dictionary windows open.
|
||||
self.highlight = nil
|
||||
|
||||
-- Wrapped through Trapper, as we may be using Trapper:dismissablePopen() in it
|
||||
Trapper:wrap(function()
|
||||
self:stardictLookup(word, {dictionary}, false, link_box, nil)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Gets number of available, enabled, and disabled dictionaries
|
||||
-- @treturn int nb_available
|
||||
-- @treturn int nb_enabled
|
||||
@@ -460,27 +495,7 @@ function ReaderDictionary:dismissLookupInfo()
|
||||
self.lookup_progress_msg = nil
|
||||
end
|
||||
|
||||
function ReaderDictionary:stardictLookup(word, box, link)
|
||||
logger.dbg("lookup word:", word, box)
|
||||
-- escape quotes and other funny characters in word
|
||||
word = self:cleanSelection(word)
|
||||
logger.dbg("stripped word:", word)
|
||||
if word == "" then
|
||||
return
|
||||
end
|
||||
|
||||
if not self.disable_lookup_history then
|
||||
local book_title = self.ui.doc_settings and self.ui.doc_settings:readSetting("doc_props").title or _("Dictionary lookup")
|
||||
lookup_history:addTableItem("lookup_history", {
|
||||
book_title = book_title,
|
||||
time = os.time(),
|
||||
word = word,
|
||||
})
|
||||
end
|
||||
|
||||
if not self.disable_fuzzy_search then
|
||||
self:showLookupInfo(word)
|
||||
end
|
||||
function ReaderDictionary:startSdcv(word, dict_names, fuzzy_search)
|
||||
local final_results = {}
|
||||
local seen_results = {}
|
||||
-- Allow for two sdcv calls : one in the classic data/dict, and
|
||||
@@ -503,30 +518,31 @@ function ReaderDictionary:stardictLookup(word, box, link)
|
||||
definition = _([[No dictionaries installed. Please search for "Dictionary support" in the KOReader Wiki to get more information about installing new dictionaries.]]),
|
||||
}
|
||||
}
|
||||
self:showDict(word, final_results, box, link)
|
||||
return
|
||||
return final_results
|
||||
end
|
||||
local lookup_cancelled = false
|
||||
local common_options = self.disable_fuzzy_search and "-nje" or "-nj"
|
||||
for _, dict_dir in ipairs(dict_dirs) do
|
||||
if lookup_cancelled then
|
||||
break -- don't do any more lookup on additional dict_dirs
|
||||
end
|
||||
|
||||
local args = {"./sdcv", "--utf8-input", "--utf8-output", "--json-output", "--non-interactive", "--data-dir", dict_dir, word}
|
||||
if not fuzzy_search then
|
||||
table.insert(args, "--exact-search")
|
||||
end
|
||||
if dict_names then
|
||||
for _, opt in pairs(dict_names) do
|
||||
table.insert(args, "-u")
|
||||
table.insert(args, opt)
|
||||
end
|
||||
end
|
||||
|
||||
local results_str = nil
|
||||
if Device:isAndroid() then
|
||||
local A = require("android")
|
||||
local args = {"./sdcv", "--utf8-input", "--utf8-output", common_options, word, "--data-dir", dict_dir}
|
||||
if self.sdcv_dictnames_options_raw then
|
||||
for _, opt in pairs(self.sdcv_dictnames_options_raw) do
|
||||
table.insert(args, opt)
|
||||
end
|
||||
end
|
||||
results_str = A.stdout(unpack(args))
|
||||
else
|
||||
local cmd = ("./sdcv --utf8-input --utf8-output %q %q --data-dir %q"):format(common_options, word, dict_dir)
|
||||
if self.sdcv_dictnames_options_escaped then
|
||||
cmd = cmd .. " " .. self.sdcv_dictnames_options_escaped
|
||||
end
|
||||
local cmd = util.shell_escape(args)
|
||||
-- cmd = "sleep 7 ; " .. cmd -- uncomment to simulate long lookup time
|
||||
|
||||
if self.lookup_progress_msg then
|
||||
@@ -584,7 +600,30 @@ function ReaderDictionary:stardictLookup(word, box, link)
|
||||
}
|
||||
}
|
||||
end
|
||||
self:showDict(word, tidyMarkup(final_results), box, link)
|
||||
|
||||
return final_results
|
||||
end
|
||||
|
||||
function ReaderDictionary:stardictLookup(word, dict_names, fuzzy_search, box, link)
|
||||
if word == "" then
|
||||
return
|
||||
end
|
||||
|
||||
if not self.disable_lookup_history then
|
||||
local book_title = self.ui.doc_settings and self.ui.doc_settings:readSetting("doc_props").title or _("Dictionary lookup")
|
||||
lookup_history:addTableItem("lookup_history", {
|
||||
book_title = book_title,
|
||||
time = os.time(),
|
||||
word = word,
|
||||
})
|
||||
end
|
||||
|
||||
if fuzzy_search then
|
||||
self:showLookupInfo(word)
|
||||
end
|
||||
|
||||
local results = self:startSdcv(word, dict_names, fuzzy_search)
|
||||
self:showDict(word, tidyMarkup(results), box, link)
|
||||
end
|
||||
|
||||
function ReaderDictionary:showDict(word, results, box, link)
|
||||
@@ -613,6 +652,9 @@ function ReaderDictionary:showDict(word, results, box, link)
|
||||
self.view.footer:updateFooter()
|
||||
end
|
||||
end,
|
||||
html_dictionary_link_tapped_callback = function(dictionary, html_link)
|
||||
self:onHtmlDictionaryLinkTapped(dictionary, html_link)
|
||||
end,
|
||||
}
|
||||
table.insert(self.dict_window_list, self.dict_window)
|
||||
UIManager:show(self.dict_window)
|
||||
|
||||
@@ -59,6 +59,7 @@ local DictQuickLookup = InputContainer:new{
|
||||
button_padding = Screen:scaleBySize(14),
|
||||
-- refresh_callback will be called before we trigger full refresh in onSwipe
|
||||
refresh_callback = nil,
|
||||
html_dictionary_link_tapped_callback = nil,
|
||||
}
|
||||
|
||||
local highlight_strings = {
|
||||
@@ -273,6 +274,9 @@ function DictQuickLookup:update()
|
||||
width = self.width,
|
||||
height = self.is_fullpage and self.height*0.75 or self.height*0.7,
|
||||
dialog = self,
|
||||
html_link_tapped_callback = function(link)
|
||||
self.html_dictionary_link_tapped_callback(self.dictionary, link)
|
||||
end,
|
||||
}
|
||||
else
|
||||
text_widget = ScrollTextWidget:new{
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
HTML widget (without scroll bars).
|
||||
--]]
|
||||
|
||||
local Device = require("device")
|
||||
local DrawContext = require("ffi/drawcontext")
|
||||
local Geom = require("ui/geometry")
|
||||
local GestureRange = require("ui/gesturerange")
|
||||
local InputContainer = require("ui/widget/container/inputcontainer")
|
||||
local Mupdf = require("ffi/mupdf")
|
||||
local Screen = require("device").screen
|
||||
@@ -19,8 +21,22 @@ local HtmlBoxWidget = InputContainer:new{
|
||||
page_number = 1,
|
||||
hold_start_pos = nil,
|
||||
hold_start_tv = nil,
|
||||
html_link_tapped_callback = nil,
|
||||
}
|
||||
|
||||
function HtmlBoxWidget:init()
|
||||
if Device:isTouchDevice() then
|
||||
self.ges_events = {
|
||||
TapText = {
|
||||
GestureRange:new{
|
||||
ges = "tap",
|
||||
range = function() return self.dimen end,
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function HtmlBoxWidget:setContent(body, css, default_font_size)
|
||||
-- fz_set_user_css is tied to the context instead of the document so to easily support multiple
|
||||
-- HTML dictionaries with different CSS, we embed the stylesheet into the HTML instead of using
|
||||
@@ -118,12 +134,22 @@ function HtmlBoxWidget:onCloseWidget()
|
||||
self:free()
|
||||
end
|
||||
|
||||
function HtmlBoxWidget:onHoldStartText(_, ges)
|
||||
self.hold_start_pos = Geom:new{
|
||||
x = ges.pos.x - self.dimen.x,
|
||||
y = ges.pos.y - self.dimen.y,
|
||||
function HtmlBoxWidget:getPosFromAbsPos(abs_pos)
|
||||
local pos = Geom:new{
|
||||
x = abs_pos.x - self.dimen.x,
|
||||
y = abs_pos.y - self.dimen.y,
|
||||
}
|
||||
|
||||
-- check if the coordinates are actually inside our area
|
||||
if pos.x < 0 or pos.x >= self.dimen.w or pos.y < 0 or pos.y >= self.dimen.h then
|
||||
return nil
|
||||
end
|
||||
|
||||
return pos
|
||||
end
|
||||
|
||||
function HtmlBoxWidget:onHoldStartText(_, ges)
|
||||
self.hold_start_pos = self:getPosFromAbsPos(ges.pos)
|
||||
self.hold_start_tv = TimeVal.now()
|
||||
|
||||
return true
|
||||
@@ -167,18 +193,10 @@ function HtmlBoxWidget:onHoldReleaseText(callback, ges)
|
||||
end
|
||||
|
||||
local start_pos = self.hold_start_pos
|
||||
local end_pos = Geom:new{
|
||||
x = ges.pos.x - self.dimen.x,
|
||||
y = ges.pos.y - self.dimen.y,
|
||||
}
|
||||
|
||||
self.hold_start_pos = nil
|
||||
|
||||
-- check start and end coordinates are actually inside our area
|
||||
if start_pos.x < 0 or end_pos.x < 0 or
|
||||
start_pos.x >= self.dimen.w or end_pos.x >= self.dimen.w or
|
||||
start_pos.y < 0 or end_pos.y < 0 or
|
||||
start_pos.y >= self.dimen.h or end_pos.y >= self.dimen.h then
|
||||
local end_pos = self:getPosFromAbsPos(ges.pos)
|
||||
if not end_pos then
|
||||
return false
|
||||
end
|
||||
|
||||
@@ -196,4 +214,33 @@ function HtmlBoxWidget:onHoldReleaseText(callback, ges)
|
||||
return true
|
||||
end
|
||||
|
||||
function HtmlBoxWidget:getLinkByPosition(pos)
|
||||
local page = self.document:openPage(self.page_number)
|
||||
local links = page:getPageLinks()
|
||||
page:close()
|
||||
|
||||
for _, link in pairs(links) do
|
||||
if pos.x >= link.x0 and pos.x < link.x1 and pos.y >= link.y0 and pos.y < link.y1 then
|
||||
return link
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function HtmlBoxWidget:onTapText(arg, ges)
|
||||
if G_reader_settings:isFalse("tap_to_follow_links") then
|
||||
return
|
||||
end
|
||||
|
||||
if self.html_link_tapped_callback then
|
||||
local pos = self:getPosFromAbsPos(ges.pos)
|
||||
if pos then
|
||||
local link = self:getLinkByPosition(pos)
|
||||
if link then
|
||||
self.html_link_tapped_callback(link)
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return HtmlBoxWidget
|
||||
|
||||
@@ -22,6 +22,7 @@ local ScrollHtmlWidget = InputContainer:new{
|
||||
htmlbox_widget = nil,
|
||||
v_scroll_bar = nil,
|
||||
dialog = nil,
|
||||
html_link_tapped_callback = nil,
|
||||
dimen = nil,
|
||||
width = 0,
|
||||
height = 0,
|
||||
@@ -35,6 +36,7 @@ function ScrollHtmlWidget:init()
|
||||
w = self.width - self.scroll_bar_width - self.text_scroll_span,
|
||||
h = self.height,
|
||||
},
|
||||
html_link_tapped_callback = self.html_link_tapped_callback,
|
||||
}
|
||||
|
||||
self.htmlbox_widget:setContent(self.html_body, self.css, self.default_font_size)
|
||||
|
||||
@@ -572,7 +572,7 @@ function util.htmlToPlainTextIfHtml(text)
|
||||
end
|
||||
|
||||
--- Encode the HTML entities in a string
|
||||
-- @string text the string to escape
|
||||
--- @string text the string to escape
|
||||
-- Taken from https://github.com/kernelsauce/turbo/blob/e4a35c2e3fb63f07464f8f8e17252bea3a029685/turbo/escape.lua#L58-L70
|
||||
function util.htmlEscape(text)
|
||||
return text:gsub("[}{\">/<'&]", {
|
||||
@@ -585,4 +585,16 @@ function util.htmlEscape(text)
|
||||
})
|
||||
end
|
||||
|
||||
--- Escape list for shell usage
|
||||
--- @table args the list of arguments to escape
|
||||
--- @treturn string the escaped and concatenated arguments
|
||||
function util.shell_escape(args)
|
||||
local escaped_args = {}
|
||||
for _, arg in ipairs(args) do
|
||||
arg = "'" .. arg:gsub("'", "'\\''") .. "'"
|
||||
table.insert(escaped_args, arg)
|
||||
end
|
||||
return table.concat(escaped_args, " ")
|
||||
end
|
||||
|
||||
return util
|
||||
|
||||
Reference in New Issue
Block a user