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 DataStorage = require("datastorage")
local Device = require("device") local Device = require("device")
local DictQuickLookup = require("ui/widget/dictquicklookup") local DictQuickLookup = require("ui/widget/dictquicklookup")
local Geom = require("ui/geometry")
local InfoMessage = require("ui/widget/infomessage") local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer") local InputContainer = require("ui/widget/container/inputcontainer")
local JSON = require("json") local JSON = require("json")
@ -146,6 +147,8 @@ function ReaderDictionary:init()
end end
function ReaderDictionary:updateSdcvDictNamesOptions() function ReaderDictionary:updateSdcvDictNamesOptions()
self.enabled_dict_names = nil
-- We cannot tell sdcv which dictionaries to ignore, but we -- We cannot tell sdcv which dictionaries to ignore, but we
-- can tell it which dictionaries to use, by using multiple -- can tell it which dictionaries to use, by using multiple
-- -u <dictname> options. -- -u <dictname> options.
@ -153,28 +156,16 @@ function ReaderDictionary:updateSdcvDictNamesOptions()
-- them for ordering queries and results) -- them for ordering queries and results)
local dicts_disabled = G_reader_settings:readSetting("dicts_disabled") local dicts_disabled = G_reader_settings:readSetting("dicts_disabled")
if not next(dicts_disabled) then 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 return
end 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 for _, ifo in pairs(available_ifos) do
if not dicts_disabled[ifo.file] then if not dicts_disabled[ifo.file] then
table.insert(u_options_raw, "-u") if not self.enabled_dict_names then
table.insert(u_options_raw, ifo.name) self.enabled_dict_names = {}
-- Escape chars in dictname so it's ok for the shell command end
-- local u_esc = ("-u %q"):format(ifo.name) table.insert(self.enabled_dict_names, 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)
end end
-- Note: if all dicts are disabled, we won't get any -u, and so
-- all dicts will be queried.
end end
self.sdcv_dictnames_options_raw = u_options_raw
self.sdcv_dictnames_options_escaped = table.concat(u_options_escaped, " ")
end end
function ReaderDictionary:addToMainMenu(menu_items) 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 end
function ReaderDictionary:onLookupWord(word, box, highlight, link) 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 self.highlight = highlight
-- Wrapped through Trapper, as we may be using Trapper:dismissablePopen() in it -- Wrapped through Trapper, as we may be using Trapper:dismissablePopen() in it
Trapper:wrap(function() Trapper:wrap(function()
self:stardictLookup(word, box, link) self:stardictLookup(word, self.enabled_dict_names, not self.disable_fuzzy_search, box, link)
end) end)
return true return true
end 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 --- Gets number of available, enabled, and disabled dictionaries
-- @treturn int nb_available -- @treturn int nb_available
-- @treturn int nb_enabled -- @treturn int nb_enabled
@ -460,27 +495,7 @@ function ReaderDictionary:dismissLookupInfo()
self.lookup_progress_msg = nil self.lookup_progress_msg = nil
end end
function ReaderDictionary:stardictLookup(word, box, link) function ReaderDictionary:startSdcv(word, dict_names, fuzzy_search)
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
local final_results = {} local final_results = {}
local seen_results = {} local seen_results = {}
-- Allow for two sdcv calls : one in the classic data/dict, and -- 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.]]), 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 final_results
return
end end
local lookup_cancelled = false local lookup_cancelled = false
local common_options = self.disable_fuzzy_search and "-nje" or "-nj"
for _, dict_dir in ipairs(dict_dirs) do for _, dict_dir in ipairs(dict_dirs) do
if lookup_cancelled then if lookup_cancelled then
break -- don't do any more lookup on additional dict_dirs break -- don't do any more lookup on additional dict_dirs
end 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 local results_str = nil
if Device:isAndroid() then if Device:isAndroid() then
local A = require("android") 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)) results_str = A.stdout(unpack(args))
else else
local cmd = ("./sdcv --utf8-input --utf8-output %q %q --data-dir %q"):format(common_options, word, dict_dir) local cmd = util.shell_escape(args)
if self.sdcv_dictnames_options_escaped then
cmd = cmd .. " " .. self.sdcv_dictnames_options_escaped
end
-- cmd = "sleep 7 ; " .. cmd -- uncomment to simulate long lookup time -- cmd = "sleep 7 ; " .. cmd -- uncomment to simulate long lookup time
if self.lookup_progress_msg then if self.lookup_progress_msg then
@ -584,7 +600,30 @@ function ReaderDictionary:stardictLookup(word, box, link)
} }
} }
end 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 end
function ReaderDictionary:showDict(word, results, box, link) function ReaderDictionary:showDict(word, results, box, link)
@ -613,6 +652,9 @@ function ReaderDictionary:showDict(word, results, box, link)
self.view.footer:updateFooter() self.view.footer:updateFooter()
end end
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) table.insert(self.dict_window_list, self.dict_window)
UIManager:show(self.dict_window) UIManager:show(self.dict_window)

@ -59,6 +59,7 @@ local DictQuickLookup = InputContainer:new{
button_padding = Screen:scaleBySize(14), button_padding = Screen:scaleBySize(14),
-- refresh_callback will be called before we trigger full refresh in onSwipe -- refresh_callback will be called before we trigger full refresh in onSwipe
refresh_callback = nil, refresh_callback = nil,
html_dictionary_link_tapped_callback = nil,
} }
local highlight_strings = { local highlight_strings = {
@ -273,6 +274,9 @@ function DictQuickLookup:update()
width = self.width, width = self.width,
height = self.is_fullpage and self.height*0.75 or self.height*0.7, height = self.is_fullpage and self.height*0.75 or self.height*0.7,
dialog = self, dialog = self,
html_link_tapped_callback = function(link)
self.html_dictionary_link_tapped_callback(self.dictionary, link)
end,
} }
else else
text_widget = ScrollTextWidget:new{ text_widget = ScrollTextWidget:new{

@ -2,8 +2,10 @@
HTML widget (without scroll bars). HTML widget (without scroll bars).
--]] --]]
local Device = require("device")
local DrawContext = require("ffi/drawcontext") local DrawContext = require("ffi/drawcontext")
local Geom = require("ui/geometry") local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
local InputContainer = require("ui/widget/container/inputcontainer") local InputContainer = require("ui/widget/container/inputcontainer")
local Mupdf = require("ffi/mupdf") local Mupdf = require("ffi/mupdf")
local Screen = require("device").screen local Screen = require("device").screen
@ -19,8 +21,22 @@ local HtmlBoxWidget = InputContainer:new{
page_number = 1, page_number = 1,
hold_start_pos = nil, hold_start_pos = nil,
hold_start_tv = 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) 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 -- 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 -- HTML dictionaries with different CSS, we embed the stylesheet into the HTML instead of using
@ -118,12 +134,22 @@ function HtmlBoxWidget:onCloseWidget()
self:free() self:free()
end end
function HtmlBoxWidget:onHoldStartText(_, ges) function HtmlBoxWidget:getPosFromAbsPos(abs_pos)
self.hold_start_pos = Geom:new{ local pos = Geom:new{
x = ges.pos.x - self.dimen.x, x = abs_pos.x - self.dimen.x,
y = ges.pos.y - self.dimen.y, 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() self.hold_start_tv = TimeVal.now()
return true return true
@ -167,18 +193,10 @@ function HtmlBoxWidget:onHoldReleaseText(callback, ges)
end end
local start_pos = self.hold_start_pos 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 self.hold_start_pos = nil
-- check start and end coordinates are actually inside our area local end_pos = self:getPosFromAbsPos(ges.pos)
if start_pos.x < 0 or end_pos.x < 0 or if not end_pos then
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
return false return false
end end
@ -196,4 +214,33 @@ function HtmlBoxWidget:onHoldReleaseText(callback, ges)
return true return true
end 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 return HtmlBoxWidget

@ -22,6 +22,7 @@ local ScrollHtmlWidget = InputContainer:new{
htmlbox_widget = nil, htmlbox_widget = nil,
v_scroll_bar = nil, v_scroll_bar = nil,
dialog = nil, dialog = nil,
html_link_tapped_callback = nil,
dimen = nil, dimen = nil,
width = 0, width = 0,
height = 0, height = 0,
@ -35,6 +36,7 @@ function ScrollHtmlWidget:init()
w = self.width - self.scroll_bar_width - self.text_scroll_span, w = self.width - self.scroll_bar_width - self.text_scroll_span,
h = self.height, 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) self.htmlbox_widget:setContent(self.html_body, self.css, self.default_font_size)

@ -572,7 +572,7 @@ function util.htmlToPlainTextIfHtml(text)
end end
--- Encode the HTML entities in a string --- 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 -- Taken from https://github.com/kernelsauce/turbo/blob/e4a35c2e3fb63f07464f8f8e17252bea3a029685/turbo/escape.lua#L58-L70
function util.htmlEscape(text) function util.htmlEscape(text)
return text:gsub("[}{\">/<'&]", { return text:gsub("[}{\">/<'&]", {
@ -585,4 +585,16 @@ function util.htmlEscape(text)
}) })
end 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 return util

Loading…
Cancel
Save