--[[--
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 = Device.screen
local UIManager = require("ui/uimanager")
local logger = require("logger")
local time = require("ui/time")
local util = require("util")
-- -1: right to left, 0: mixed, +1: left to right
local function getLineTextDirection(line)
local word_count = #line
if word_count <= 1 then
return 1
end
local ltr = true
local rtl = true
for i = 2, word_count do
if line[i].x0 > line[i - 1].x0 then
rtl = false
elseif line[i].x0 < line[i - 1].x0 then
ltr = false
end
end
if ltr and not rtl then
return 1
elseif rtl and not ltr then
return -1
else
return 0
end
end
local function getWordIndices(lines, pos)
local last_checked_line_index = nil
for line_index, line in ipairs(lines) do
if pos.y >= line.y0 then -- check if pos in on or below the line
if pos.y < line.y1 then -- check if pos is within the line vertically
local rtl_line = getLineTextDirection(line) < 0
if pos.x >= line.x0 and pos.x < line.x1 then -- check if pos is within the line horizontally
if #line >= 1 then -- if line is not empty then check for exact word hit
local word_start_index = 1
local word_end_index = #line
local step = 1
if rtl_line then
word_start_index, word_end_index = word_end_index, word_start_index
step = -1
end
local word_x0 = line[word_start_index].x0
for word_index = word_start_index, word_end_index, step do
local word = line[word_index]
if pos.x >= word_x0 and pos.x < word.x1 then
return line_index, word_index
end
-- join the word rectangles horizontally to avoid hit gaps
word_x0 = word.x1
end
end
elseif pos.x < line.x0 then -- check if pos is before the current line horizontally
if rtl_line then
return line_index, #line
else
return line_index, 1
end
elseif pos.x >= line.x1 then -- check if pos after the current line horizontally
if rtl_line then
-- To match TextBoxWidget's selection behavior this should be "line_index, 1"
-- but then the selection will jump between the full row and the visually
-- last word when hitting a vertical gap. If we extend the line vertically
-- till the next one then selection will be weird around new paragraphs.
-- The solution might require getPageText() to add empty lines.
return line_index, #line
else
return line_index, #line
end
end
end
last_checked_line_index = line_index
end
end
if last_checked_line_index == nil then
return 1, 1
else
return last_checked_line_index, #lines[last_checked_line_index]
end
end
local function getSelectedText(lines, start_pos, end_pos)
local start_line_index, start_word_index = getWordIndices(lines, start_pos)
local end_line_index, end_word_index = getWordIndices(lines, end_pos)
if start_line_index == nil or end_line_index == nil then
return nil, nil
elseif start_line_index > end_line_index then
start_line_index, end_line_index = end_line_index, start_line_index
start_word_index, end_word_index = end_word_index, start_word_index
elseif start_line_index == end_line_index and start_word_index > end_word_index then
start_word_index, end_word_index = end_word_index, start_word_index
end
local found_start = false
local words = {}
local rects = {}
for line_index = start_line_index, end_line_index do
local line = lines[line_index]
local line_last_rect = nil
local line_text_direction = getLineTextDirection(line)
for word_index, word in ipairs(line) do
if type(word) == 'table' then
if line_index == start_line_index and word_index == start_word_index then
found_start = true
end
if found_start then
table.insert(words, word.word)
-- do not try to join word rects in mixed direction lines
if line_last_rect == nil or line_text_direction == 0 then
local rect = Geom:new{
x = word.x0,
y = line.y0,
w = word.x1 - word.x0,
h = line.y1 - line.y0,
}
table.insert(rects, rect)
line_last_rect = rect
else
if line_text_direction > 0 then -- left to right
line_last_rect.w = word.x1 - line_last_rect.x
else -- right to left
line_last_rect.w = line_last_rect.w + (line_last_rect.x - word.x0)
line_last_rect.x = word.x0
end
end
if line_index == end_line_index and word_index == end_word_index then
break
end
end
end
end
end
if found_start then
return table.concat(words, " "), rects
else
return nil, nil
end
end
local function areTextBoxesEqual(boxes1, text1, boxes2, text2)
if text1 ~= text2 then
return false
end
if boxes1 and boxes2 then
if #boxes1 ~= #boxes2 then
return false
end
for i = 1, #boxes1, 1 do
if boxes1[i] ~= boxes2[i] then
return false
end
end
return true
else
return (boxes1 == nil) == (boxes2 == nil)
end
end
local HtmlBoxWidget = InputContainer:extend{
bb = nil,
dimen = nil,
dialog = nil, -- parent dialog that will be set dirty
document = nil,
page_count = 0,
page_number = 1,
page_boxes = nil,
hold_start_pos = nil,
hold_end_pos = nil,
hold_start_time = nil,
html_link_tapped_callback = nil,
highlight_text_selection = false, -- if true then the selected text will be highlighted
highlight_rects = nil,
highlight_text = nil,
highlight_clear_and_redraw_action = nil,
}
function HtmlBoxWidget:init()
if Device:isTouchDevice() then
self.ges_events.TapText = {
GestureRange:new{
ges = "tap",
range = function() return self.dimen end,
},
}
end
self.highlight_lighten_factor = G_reader_settings:readSetting("highlight_lighten_factor", 0.2)
end
-- These are generic "fixes" to MuPDF HTML stylesheet:
-- - MuPDF doesn't set some elements as being display:block, and would
-- consider them inline, and would badly handle
inside them.
-- Note: this is a generic issue with
inside inline elements, see:
-- https://github.com/koreader/koreader/issues/12258#issuecomment-2267629234
local mupdf_css_fixes = [[
article, aside, button, canvas, datalist, details, dialog, dir, fieldset, figcaption,
figure, footer, form, frame, frameset, header, hgroup, iframe, legend, listing,
main, map, marquee, multicol, nav, noembed, noframes, noscript, optgroup, output,
plaintext, search, select, summary, template, textarea, video, xmp {
display: block;
}
]]
function HtmlBoxWidget:setContent(body, css, default_font_size, is_xhtml, no_css_fixes, html_resource_directory)
-- 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
-- that function.
local head = ""
if css or not no_css_fixes then
head = string.format("
123
123
" as being plain text). local ok ok, self.document = pcall(Mupdf.openDocumentFromText, html, is_xhtml and "xhtml" or "html", html_resource_directory) if not ok then -- self.document contains the error logger.warn("HTML loading error:", self.document) body = util.htmlToPlainText(body) body = util.htmlEscape(body) -- Normally \n would be replaced with