--[[-- 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 , 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 , put in an internal -- 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("]*>.-", function(s) local pre, css_text, post = s:match("(]*>)%s*(.-)%s*()") 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 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_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