HTML dictionary link support (#3603)

pull/3078/head
TnS-hun 6 years ago committed by poire-z
parent d201c04df7
commit b40bc53fc7

@ -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

Loading…
Cancel
Save