mirror of
https://github.com/koreader/koreader
synced 2024-11-18 03:25:46 +00:00
444 lines
20 KiB
Lua
444 lines
20 KiB
Lua
--[[--
|
|
This module shows HTML code and CSS content from crengine documents.
|
|
It it used by ReaderHighlight as an action after text selection.
|
|
--]]
|
|
|
|
local BD = require("ui/bidi")
|
|
local Device = require("device")
|
|
local InfoMessage = require("ui/widget/infomessage")
|
|
local Notification = require("ui/widget/notification")
|
|
local TextViewer = require("ui/widget/textviewer")
|
|
local UIManager = require("ui/uimanager")
|
|
local util = require("util")
|
|
local _ = require("gettext")
|
|
local T = require("ffi/util").template
|
|
|
|
local ViewHtml = {
|
|
VIEWS = {
|
|
-- For available flags, see the "#define WRITENODEEX_*" in crengine/src/lvtinydom.cpp.
|
|
-- Start with valid and classic displayed HTML (with only block nodes indented),
|
|
-- including styles found in <HEAD>, linked CSS files content, and misc info.
|
|
{ _("Switch to standard view"), 0xE830, false },
|
|
|
|
-- Each node on a line, with markers and numbers of skipped chars and siblings shown,
|
|
-- with possibly invalid HTML (text nodes not escaped)
|
|
{ _("Switch to debug view"), 0xEB5A, true },
|
|
|
|
-- Additionally show rendering methods of each node
|
|
{ _("Switch to rendering debug view"), 0xEF5A, true },
|
|
|
|
-- Or additionally show unicode codepoint of each char
|
|
{ _("Switch to unicode debug view"), 0xEB5E, true },
|
|
},
|
|
}
|
|
|
|
-- Main entry point
|
|
function ViewHtml:viewSelectionHTML(document, selected_text)
|
|
if not selected_text or not selected_text.pos0 or not selected_text.pos1 then
|
|
return
|
|
end
|
|
self:_viewSelectionHTML(document, selected_text, 1, true, false)
|
|
end
|
|
|
|
function ViewHtml:_viewSelectionHTML(document, selected_text, view, with_css_files_buttons, hide_stylesheet_elem_content)
|
|
local next_view = view < #self.VIEWS and view + 1 or 1
|
|
local next_view_text = self.VIEWS[next_view][1]
|
|
|
|
local html_flags = self.VIEWS[view][2]
|
|
local massage_html = self.VIEWS[view][3]
|
|
|
|
local html, css_files, css_selectors_offsets = document:getHTMLFromXPointers(selected_text.pos0,
|
|
selected_text.pos1, html_flags, true)
|
|
if not html then
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Failed getting HTML for selection"),
|
|
})
|
|
return
|
|
end
|
|
|
|
-- Our substitutions may mess with the offsets in css_selectors_offsets: we need to keep
|
|
-- track of shifts induced by these substitutions to correct the offsets
|
|
local offset_shifts = {}
|
|
local replace_in_html = function(pat, repl)
|
|
local new_html = ""
|
|
local is_match = false -- given the html we get and our patterns, we know the first part won't be a match
|
|
for part in util.gsplit(html, pat, true, true) do
|
|
if is_match then
|
|
local r = type(repl) == "function" and repl(part) or repl
|
|
local offset_shift = #r - #part
|
|
if offset_shift ~= 0 then
|
|
table.insert(offset_shifts, {#new_html + #part + 1, offset_shift})
|
|
end
|
|
new_html = new_html .. r
|
|
else
|
|
-- (we provided capture_empty_entity=true, to match adjacent 'pat',
|
|
-- so here we may get empty 'part', that we can just concatenate)
|
|
new_html = new_html .. part
|
|
end
|
|
is_match = not is_match
|
|
end
|
|
html = new_html
|
|
end
|
|
if massage_html then
|
|
-- Make some invisible chars visible
|
|
replace_in_html("\u{00A0}", "\u{2423}") -- no break space: open box
|
|
replace_in_html("\u{00AD}", "\u{22C5}") -- soft hyphen: dot operator (smaller than middle dot ·)
|
|
-- Prettify inlined CSS (from <HEAD>, put in an internal
|
|
-- <body><stylesheet> element by crengine (the opening tag may
|
|
-- include some href=, or end with " ~X>" with some html_flags)
|
|
-- (We do that in debug views only: as this may increase the
|
|
-- height of this section, we don't want to have to scroll many
|
|
-- pages to get to the HTML content on the initial view.)
|
|
end
|
|
if massage_html or hide_stylesheet_elem_content then
|
|
replace_in_html("<stylesheet[^>]*>.-</stylesheet>", function(s)
|
|
local pre, css_text, post = s:match("(<stylesheet[^>]*>)%s*(.-)%s*(</stylesheet>)")
|
|
if hide_stylesheet_elem_content then
|
|
return pre .. "[...]" .. post
|
|
end
|
|
return pre .. "\n" .. util.prettifyCSS(css_text) .. post
|
|
end)
|
|
end
|
|
-- Make sure we won't get wrapped just after our indentation if there is no break opportunity later
|
|
replace_in_html("\n *", function(s)
|
|
return "\n" .. ("\u{00A0}"):rep(#s - 1)
|
|
end)
|
|
|
|
local textviewer
|
|
-- Prepare bottom buttons and their actions
|
|
local buttons_hold_callback = function()
|
|
-- Allow hiding css files buttons if there are too many
|
|
-- and the available height for text is too short
|
|
UIManager:close(textviewer)
|
|
self:_viewSelectionHTML(document, selected_text, view, not with_css_files_buttons, hide_stylesheet_elem_content)
|
|
end
|
|
local buttons_table = {}
|
|
if css_files and with_css_files_buttons then
|
|
for i=1, #css_files do
|
|
local button = {
|
|
text = T(_("View %1"), BD.filepath(css_files[i])),
|
|
callback = function()
|
|
local css_text = document:getDocumentFileContent(css_files[i])
|
|
local cssviewer
|
|
cssviewer = TextViewer:new{
|
|
title = css_files[i],
|
|
text = css_text or _("Failed getting CSS content"),
|
|
text_type = "code",
|
|
para_direction_rtl = false,
|
|
auto_para_direction = false,
|
|
add_default_buttons = true,
|
|
buttons_table = {
|
|
{{
|
|
text = _("Prettify"),
|
|
enabled = css_text and true or false,
|
|
callback = function()
|
|
UIManager:close(cssviewer)
|
|
UIManager:show(TextViewer:new{
|
|
title = css_files[i],
|
|
text = util.prettifyCSS(css_text),
|
|
text_type = "code",
|
|
para_direction_rtl = false,
|
|
auto_para_direction = false,
|
|
})
|
|
end,
|
|
}},
|
|
}
|
|
}
|
|
UIManager:show(cssviewer)
|
|
end,
|
|
hold_callback = buttons_hold_callback,
|
|
}
|
|
-- One button per row, to make room for the possibly long css filename
|
|
table.insert(buttons_table, {button})
|
|
end
|
|
end
|
|
table.insert(buttons_table, {{
|
|
text = next_view_text,
|
|
callback = function()
|
|
UIManager:close(textviewer)
|
|
self:_viewSelectionHTML(document, selected_text, next_view, with_css_files_buttons, hide_stylesheet_elem_content)
|
|
end,
|
|
hold_callback = buttons_hold_callback,
|
|
}})
|
|
|
|
-- Long-press in the HTML will present a list of CSS selectors related to the element
|
|
-- we pressed on, to be copied to clipboard
|
|
local text_selection_callback = function(text, hold_duration, start_idx, end_idx, to_source_index_func)
|
|
if not css_selectors_offsets or css_selectors_offsets == "" then -- no flag provided
|
|
Device.input.setClipboardText(text)
|
|
UIManager:show(Notification:new{
|
|
text = _("Selection copied to clipboard.")
|
|
})
|
|
return
|
|
end
|
|
-- We only work with one index (let's choose start_idx), and we want the offset in the utf8 stream
|
|
local idx = to_source_index_func(start_idx)
|
|
self:_handleLongPress(document, css_selectors_offsets, offset_shifts, idx, function()
|
|
UIManager:close(textviewer)
|
|
self:_viewSelectionHTML(document, selected_text, view, with_css_files_buttons, not hide_stylesheet_elem_content)
|
|
end)
|
|
end
|
|
|
|
textviewer = TextViewer:new{
|
|
title = _("Selection HTML"),
|
|
text = html,
|
|
text_type = "code",
|
|
para_direction_rtl = false,
|
|
auto_para_direction = false,
|
|
add_default_buttons = true,
|
|
default_hold_callback = buttons_hold_callback,
|
|
buttons_table = buttons_table,
|
|
text_selection_callback = text_selection_callback,
|
|
}
|
|
UIManager:show(textviewer)
|
|
end
|
|
|
|
function ViewHtml:_handleLongPress(document, css_selectors_offsets, offset_shifts, idx, stylesheet_elem_callback)
|
|
|
|
-- We want to propose for "copy into clipboard" a few interesting selectors related to the element
|
|
-- the user long-pressed on, which can then be pasted in "Find" when viewing a stylesheet, or
|
|
-- pasted in "Book style tweaks" when willing to tweak the style for this element.
|
|
local proposed_selectors = {}
|
|
local seen_kind = {} -- only one selector of some kind proposed, to not have too many
|
|
local ancestors_classnames_selector = "" -- we will have a final one selecting the whole ancestors
|
|
|
|
-- Ignore some crengine internal attributes:
|
|
local ignore_attrs = { "StyleSheet" }
|
|
-- Some attributes have too variable values, that are not interesting when used as selectors:
|
|
local skip_value_attrs = { "href", "id", "style", "title", }
|
|
|
|
-- We will also show 2 buttons to show the individual CSS rulesets (selector + declaration)
|
|
-- that would match this elements, and this element and its ancestor.
|
|
local ancestors = {}
|
|
|
|
-- We get as css_selectors_offsets from crengine such content:
|
|
-- (Format: Offset in 'html', node level, node dataIndex, element name, class and attribute selectors
|
|
-- 0 2 33 body
|
|
-- 9 3 449 DocFragment [StyleSheet=stylesheet.css] [id=_doc_fragment_52] [lang=fr-FR]
|
|
-- 90 4 465 stylesheet [href=OPS/]
|
|
-- 163 4
|
|
-- 168 4 481 body [type=bodymatter] [lang=fr-FR] [lang=fr-FR] .calibre1
|
|
-- 251 5 545 section .chap [type=chapter] [role=doc-chapter]
|
|
-- 321 6 561 div
|
|
-- 349 7 577 p .justif1 .no-indent [type=main]
|
|
-- 395 7
|
|
-- 406 7 593 p .justif1
|
|
-- 457 7
|
|
-- 472 6
|
|
-- 489 5
|
|
-- 501 4
|
|
-- 518 3
|
|
-- 526 2
|
|
local offsets = {}
|
|
for line in css_selectors_offsets:gmatch("[^\n]+") do
|
|
local t = util.splitToArray(line, "\t")
|
|
table.insert(offsets, t)
|
|
end
|
|
-- Iterate from end until we find a smaller offset (this is the element we are in)
|
|
-- and from then on, only deal with elements with a smaller level (the parents)
|
|
local cur_level = math.huge
|
|
local stop_gathering_selectors = false
|
|
for i=#offsets, 1, -1 do
|
|
local info = offsets[i]
|
|
local offset, level = tonumber(info[1]), tonumber(info[2])
|
|
-- Correct offsets with the shifts caused by our substitutions
|
|
for _, offset_shift in ipairs(offset_shifts) do
|
|
if offset >= offset_shift[1] then
|
|
offset = offset + offset_shift[2]
|
|
end
|
|
end
|
|
if offset <= idx and level < cur_level then -- meeting element or new parent
|
|
cur_level = level
|
|
if #info > 2 then -- this is an element (and not a level we leave)
|
|
local elem = info[4]
|
|
table.insert(ancestors, { elem, info[3] })
|
|
if elem == "body" and #proposed_selectors > 0 then
|
|
-- Stop and don't include body (unless long-press on <body> itself)
|
|
stop_gathering_selectors = true
|
|
end
|
|
if not stop_gathering_selectors then
|
|
if not seen_kind.element then
|
|
-- Propose as selector the selected element tag name, ie. "p".
|
|
if elem == "stylesheet" then -- long-press on <stylesheet>
|
|
stylesheet_elem_callback()
|
|
return
|
|
end
|
|
table.insert(proposed_selectors, elem)
|
|
end
|
|
local all_classnames = ""
|
|
local all_attrs = ""
|
|
for j=5, #info do
|
|
local sel = info[j]
|
|
if sel:sub(1,1) == "." then
|
|
if not seen_kind.individual_classname then
|
|
-- Propose as selectors each of the classnames of the selected element
|
|
-- (or its neareast parent with a class), ie. ".justif1" , ".no-indent".
|
|
table.insert(proposed_selectors, sel)
|
|
end
|
|
all_classnames = all_classnames .. sel
|
|
else
|
|
local attrname = sel:match("^%[(.-)=") or ""
|
|
if elem == "DocFragment" then
|
|
if attrname == "id" then -- keep id= full, it can be useful with DocFragment
|
|
all_attrs = all_attrs .. sel
|
|
end
|
|
elseif util.arrayContains(ignore_attrs, attrname) then
|
|
do end -- luacheck: ignore 541
|
|
elseif util.arrayContains(skip_value_attrs, attrname) then
|
|
all_attrs = all_attrs .. "[" .. attrname .. "]"
|
|
else
|
|
all_attrs = all_attrs .. sel
|
|
end
|
|
end
|
|
end
|
|
if all_classnames ~= "" and not seen_kind.all_classnames then
|
|
-- Propose as selector the selected element (or its neareast parent with a class)
|
|
-- with all its classnames concatenated, ie. "p.justif1.no-indent".
|
|
table.insert(proposed_selectors, elem .. all_classnames)
|
|
seen_kind.all_classnames = true
|
|
seen_kind.individual_classname = true
|
|
end
|
|
if all_attrs ~= "" and not seen_kind.element then
|
|
-- Propose as selector the selected element with all its attributes (and classnames),
|
|
-- ie. "p.justif1.no-indent[type=main]".
|
|
table.insert(proposed_selectors, elem .. all_classnames .. all_attrs)
|
|
end
|
|
-- Accumulate into the full ancestor element & classname selector
|
|
if ancestors_classnames_selector ~= "" then
|
|
ancestors_classnames_selector = " > " .. ancestors_classnames_selector
|
|
end
|
|
ancestors_classnames_selector = elem .. all_classnames .. ancestors_classnames_selector
|
|
seen_kind.element = true -- done with selectors targetting the selected element only
|
|
end
|
|
if elem == "DocFragment" or elem == "FictionBook" then
|
|
-- Ignore the root node up these
|
|
break;
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Add a button for each proposed selector to copy it, avoiding possible duplicates
|
|
table.insert(proposed_selectors, ancestors_classnames_selector) -- ie. "section.chap > div > p.justif1.no-indent
|
|
local copy_buttons = {}
|
|
local add_copy_button = function(text)
|
|
table.insert(copy_buttons, {{
|
|
text = text,
|
|
callback = function()
|
|
Device.input.setClipboardText(text)
|
|
UIManager:show(Notification:new{
|
|
text = _("Selector copied to clipboard.")
|
|
})
|
|
end,
|
|
-- Allow "appending" with long-press, in case we want to gather a few selectors
|
|
-- at once to later work with them in a style tweak
|
|
hold_callback = function()
|
|
Device.input.setClipboardText(Device.input.getClipboardText() .. "\n" .. text)
|
|
UIManager:show(Notification:new{
|
|
text = _("Selector appended to clipboard.")
|
|
})
|
|
end,
|
|
}})
|
|
end
|
|
local already_added = {}
|
|
for _, text in ipairs(proposed_selectors) do
|
|
if text and text ~= "" and not already_added[text] then
|
|
add_copy_button(text)
|
|
already_added[text] = true
|
|
end
|
|
end
|
|
|
|
-- Add Show matched stylesheet rulesets buttons
|
|
table.insert(copy_buttons, {})
|
|
table.insert(copy_buttons, {{
|
|
text = _("Show matched stylesheets rules (element only)"),
|
|
callback = function()
|
|
self:_showMatchingSelectors(document, ancestors, false)
|
|
end,
|
|
hold_callback = function()
|
|
-- skip main stylesheet and style tweaks
|
|
self:_showMatchingSelectors(document, ancestors, false, false)
|
|
end,
|
|
}})
|
|
table.insert(copy_buttons, {{
|
|
text = _("Show matched stylesheets rules (all ancestors)"),
|
|
callback = function()
|
|
self:_showMatchingSelectors(document, ancestors, true)
|
|
end,
|
|
hold_callback = function()
|
|
-- skip main stylesheet and style tweaks
|
|
self:_showMatchingSelectors(document, ancestors, true, false)
|
|
end,
|
|
}})
|
|
|
|
local ButtonDialog = require("ui/widget/buttondialog")
|
|
local widget = ButtonDialog:new{
|
|
title = _("Copy to clipboard:"),
|
|
title_align = "center",
|
|
width_factor = 0.8,
|
|
use_info_style = false,
|
|
buttons = copy_buttons,
|
|
}
|
|
UIManager:show(widget)
|
|
end
|
|
|
|
function ViewHtml:_showMatchingSelectors(document, ancestors, show_all_ancestors, with_main_stylesheet)
|
|
local snippets
|
|
if not show_all_ancestors then
|
|
local node_dataindex = ancestors[1][2]
|
|
snippets = document:getStylesheetsMatchingRulesets(node_dataindex, with_main_stylesheet)
|
|
else
|
|
snippets = {}
|
|
local elements = {}
|
|
for _, ancestor in ipairs(ancestors) do
|
|
table.insert(elements, 1, ancestor[1])
|
|
end
|
|
for i = 1, #ancestors do
|
|
local node_dataindex = ancestors[i][2]
|
|
if #snippets > 0 then
|
|
-- Separate them with 2 blank lines
|
|
table.insert(snippets, "")
|
|
table.insert(snippets, "")
|
|
end
|
|
local desc = table.concat(elements, " > ", 1, #ancestors - i + 1)
|
|
-- We use Unicode solid black blocks to make these really visible
|
|
table.insert(snippets, "/* \u{259B}" .. ("\u{2580}"):rep(20) .. " */")
|
|
table.insert(snippets, "/* \u{258C}" .. desc .. " */")
|
|
table.insert(snippets, "/* \u{2599}" .. ("\u{2584}"):rep(20) .. " */")
|
|
util.arrayAppend(snippets, document:getStylesheetsMatchingRulesets(node_dataindex, with_main_stylesheet))
|
|
end
|
|
end
|
|
|
|
local title = show_all_ancestors and _("Matching rulesets (all ancestors)")
|
|
or _("Matching rulesets (element only)")
|
|
local css_text = table.concat(snippets, "\n")
|
|
local cssviewer
|
|
cssviewer = TextViewer:new{
|
|
title = title,
|
|
text = css_text or _("No matching rulesets"),
|
|
text_type = "code",
|
|
para_direction_rtl = false,
|
|
auto_para_direction = false,
|
|
add_default_buttons = true,
|
|
buttons_table = {
|
|
{{
|
|
text = _("Prettify"),
|
|
enabled = css_text and true or false,
|
|
callback = function()
|
|
UIManager:close(cssviewer)
|
|
UIManager:show(TextViewer:new{
|
|
title = title,
|
|
text = util.prettifyCSS(css_text),
|
|
text_type = "code",
|
|
para_direction_rtl = false,
|
|
auto_para_direction = false,
|
|
})
|
|
end,
|
|
}},
|
|
}
|
|
}
|
|
UIManager:show(cssviewer)
|
|
end
|
|
|
|
return ViewHtml
|