diff options
Diffstat (limited to 'Master/texmf-dist/tex/luatex/luaxml/luaxml-mod-html.lua')
-rw-r--r-- | Master/texmf-dist/tex/luatex/luaxml/luaxml-mod-html.lua | 221 |
1 files changed, 197 insertions, 24 deletions
diff --git a/Master/texmf-dist/tex/luatex/luaxml/luaxml-mod-html.lua b/Master/texmf-dist/tex/luatex/luaxml/luaxml-mod-html.lua index 69e32b16e22..d8c2b7e9327 100644 --- a/Master/texmf-dist/tex/luatex/luaxml/luaxml-mod-html.lua +++ b/Master/texmf-dist/tex/luatex/luaxml/luaxml-mod-html.lua @@ -1,3 +1,6 @@ +--- HTML parsing module for LuaXML +-- @module luaxml-mod-html +-- @author Michal Hoftich <michal.h21@gmail.com -- Copyright Michal Hoftich, 2022 -- HTML parser inspired by https://browser.engineering/html.html -- but then redone using https://html.spec.whatwg.org/multipage/parsing.html @@ -1540,9 +1543,14 @@ local function is_formatting_element(name) return formatting_element_names[name] end -local special_elements = {} +local function hash_from_array(tbl) + local t = {} + for _, v in ipairs(tbl) do t[v] = true end + return t +end + -local special_elements_list = {"address", "applet", "area", "article", "aside", +local special_elements_list = hash_from_array {"address", "applet", "area", "article", "aside", "base", "basefont", "bgsound", "blockquote", "body", "br", "button", "caption", "center", "col", "colgroup", "dd", "details", "dir", "div", "dl", "dt", "embed", "fieldset", "figcaption", "figure", "footer", "form", "frame", @@ -1555,13 +1563,9 @@ local special_elements_list = {"address", "applet", "area", "article", "aside", "mi","mo","mn","ms","mtext", "annotation-xml","foreignObject","desc", "title" } -for k,v in ipairs(special_elements_list) do - special_elements[v] = true -end - local function is_special(name) - return special_elements[name] + return special_elements_list[name] end -- these lists are used in HtmlParser:generate_implied_endtags() @@ -1649,8 +1653,12 @@ local HtmlTreeStates = {} +--- @type HtmlParser local HtmlParser = {} +--- Initialize the HTML Object +---@param body string HTML to be parsed +---@return table initialized object function HtmlParser:init(body) local o ={} setmetatable(o, self) @@ -1686,10 +1694,11 @@ for _,v in ipairs(self_closing_tags_list) do self_closing_tags[v] = true end - +--- Execute the HTML parser +--- @return table Root node of the HTML DOM function HtmlParser:parse() -- we assume utf8 input, you must convert it yourself if the source is - -- in a different encoding + -- in a different encoding. for example using luaxml-encodings library self.text = {} self.state = self.default_state -- this should enable us to pass over some characters that we want to ignore @@ -1791,6 +1800,7 @@ function HtmlParser:emit(token) elseif token_type == "start_tag" then self:add_text() -- self:start_attribute() + self:reset_insertion_mode() self:start_tag() -- print("Emit start tag", table.concat(token.name)) -- save last attribute @@ -1882,8 +1892,139 @@ function HtmlParser:set_xmlns(node, parent) end end +function HtmlParser:pop_element() + -- close the current element and add it to the DOM + local el = self:close_element() + local parent = self:get_parent() + parent:add_child(el) + return el +end + +local close_p_at_start = hash_from_array {"address", "article", "aside", "blockquote", "center", "details", "dialog", "dir", "div", "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "main", "menu", "nav", "ol", "p", "search", "section", "summary", "ul", "pre", "listing", "form", "table", "xmp", "hr"} + +local close_headers = hash_from_array {"h1", "h2", "h3", "h4", "h5", "h6"} + +local body_modes = hash_from_array {"in_body", "in_cell", "in_row", "in_select", "in_table", "in_table_body", "in_frameset"} + +local list_items = hash_from_array {"li", "dt", "dd"} + +local close_address_at_end = hash_from_array{"address", "article", "aside", "blockquote", "button", "center", "details", "dialog", "dir", "div", "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "listing", "main", "menu", "nav", "ol", "pre", "search", "section", "summary", "ul", "form"} + + +function HtmlParser:close_unfinished(name) + -- close all unfinished elements until the element with the given name is found + for i = #self.unfinished, 1, -1 do + local el = self:pop_element() + if el.tag == name then + break + end + end +end + + +function HtmlParser:close_paragraph() + -- close currently open <p> elements + self:close_unfinished("p") +end + +function HtmlParser:current_element_name() + -- return name of the current element + return self:get_parent().tag +end + +local not_specials = hash_from_array { "address", "div", "p"} + +local function handle_list_item(self, name) + -- we handle li, dt and dd. dt and dd should close each other, li closes only itself + local names = {dt = true, dd = true} + if name == "li" then names = {li=true} end + for i = #self.unfinished, 1, -1 do + local current = self.unfinished[i] + local current_tag = current.tag + if names[current_tag] then + self:generate_implied_endtags(nil, {current.tag}) + for j = #self.unfinished, i, -1 do + self:pop_element() + end + break + elseif is_special(current_tag) and not not_specials[name] then + break + end + end +end + +local close_paragraph = function(self) + if is_in_button_scope(self, "p") then + self:close_paragraph() + end +end + +function HtmlParser:handle_insertion_mode(token) + -- simple handling of https://html.spec.whatwg.org/multipage/parsing.html#tree-construction + -- we don't support most rules, just the most important for avoiding mismatched tags + + if body_modes[self.insertion_mode] then + if token.type == "start_tag" then + local name = table.concat(token.name) + if close_p_at_start[name] then close_paragraph(self) end + if close_headers[name] then + close_paragraph(self) + -- close current element if it is already header + if close_headers[self:current_element_name()] then + self:pop_element() + end + elseif name == "pre" or name == "listing" then + -- we should ignore next "\n" char token + elseif name == "image" then + -- image tag is an error, change to <img> + token.name = {"img"} + elseif list_items[name] then + handle_list_item(self, name) + close_paragraph(self) + end + elseif token.type == "end_tag" then + local name = table.concat(token.name) + if close_address_at_end[name] then + if is_in_scope(self, name, {}) then + self:close_unfinished(name) + else + token.type = "ignore" + end + elseif name == "p" then + if not is_in_button_scope(self, "p") then + local parent = self:get_parent() + local node = Element:init("p", parent) + table.insert(self.unfinished, node) + end + -- use self:close_paragraph() instead of close_paragraph() because we don't need to check scope at this point + self:close_paragraph() + elseif name == "br" then + token.type = "start_tag" + elseif close_headers[name] then + local header_in_scope = false + -- detect, if there are any open h1-h6 tag and close it + for el, _ in pairs(close_headers) do + if is_in_scope(self, el, {}) then + header_in_scope = el + break + end + end + if not header_in_scope then + token.type = "ignore" + else + self:close_unfinished(header_in_scope) + end + end + end + end +end + +local rawtext_elements = hash_from_array {"style", "textarea", "xmp"} + + function HtmlParser:start_tag() local token = self.current_token + self:handle_insertion_mode(token) if token.type == "start_tag" then -- close all currently opened attributes self:start_attribute() @@ -1894,18 +2035,18 @@ function HtmlParser:start_tag() node.attr = token.attr node.self_closing = token.self_closing self:set_xmlns(node) - -- + -- in this handler we should close <p> or <li> elements without explicit closing tags if token.self_closing -- <img /> or self_closing_tags[name] -- void elements then - parent:add_child(node) + parent:add_child(node, node.tag) else -- add to the unfinished list table.insert(self.unfinished, node) end if name == "title" then self.element_state = "rcdata" - elseif name == "style" then + elseif rawtext_elements[name] then self.element_state = "rawtext" elseif name == "script" then self.element_state = "script_data" @@ -1916,11 +2057,13 @@ end function HtmlParser:end_tag() -- close current opened element local token = self.current_token + self:handle_insertion_mode(token) if token.type == "end_tag" then if #self.unfinished==0 then return nil end - local node = self:close_element() - local parent = self:get_parent() - parent:add_child(node) + -- close the current element only if the token is in the current scope + if is_in_scope(self, table.concat(token.name), {}) then + self:pop_element() + end end end @@ -1964,6 +2107,21 @@ function HtmlParser:adjusted_current_node() end +local simple_modes = { + body = "in_body", + td = "in_cell", + th = "in_cell", + tr = "in_row", + tbody = "in_table_body", + thead = "in_table_body", + tfoot = "in_table_body", + caption = "in_caption", + colgroup = "in_column_group", + table = "in_table", + template = "current_template_insertion_mode", + frameset = "in_frameset" +} + function HtmlParser:reset_insertion_mode() -- https://html.spec.whatwg.org/multipage/parsing.html#reset-the-insertion-mode-appropriately local last = false @@ -1977,16 +2135,33 @@ function HtmlParser:reset_insertion_mode() if name == "head" and last == true then self:switch_insertion("in_head") return - elseif name == "body" then - self:switch_insertion("in_body") - return elseif name == "html" then - if self.head_pointer then + if not self.head_pointer then self:switch_insertion("before_head") return else self:switch_insertion("after_head") + return + end + elseif name == "select" then + if not last then + for x = position -1, 1, -1 do + if x == 1 then break end + local ancestor = self.unfinished[x] + local ancestor_name = ancestor.tag + if ancestor_name == "template" then + break + elseif ancestor_name == "table" then + self:switch_insertion("in_select_in_table") + return + end + end end + self:switch_insertion("in_select") + return + elseif simple_modes[name] then + self:switch_insertion(simple_modes[name]) + return elseif last == true then self:switch_insertion("in_body") return @@ -2006,8 +2181,8 @@ function HtmlParser:generate_implied_endtags(included, ignored) local current = self:current_node() or {} -- keep removing elements while they are in the "included" list if included[current.tag] then - table.remove(self.unfinished) - self:generate_implied_endtags(ignored) + self:pop_element() + self:generate_implied_endtags(included, ignored) end end @@ -2023,9 +2198,7 @@ function HtmlParser:finish() self:start_tag("html") end while #self.unfinished > 0 do - local node = self:close_element() - local parent = self:get_parent() - parent:add_child(node) + self:pop_element() end -- return root element return self.Document -- self:close_element() |