diff options
Diffstat (limited to 'macros/luatex/generic/luaxml/luaxml-cssquery.lua')
-rw-r--r-- | macros/luatex/generic/luaxml/luaxml-cssquery.lua | 184 |
1 files changed, 177 insertions, 7 deletions
diff --git a/macros/luatex/generic/luaxml/luaxml-cssquery.lua b/macros/luatex/generic/luaxml/luaxml-cssquery.lua index d353a310c4..062d66d745 100644 --- a/macros/luatex/generic/luaxml/luaxml-cssquery.lua +++ b/macros/luatex/generic/luaxml/luaxml-cssquery.lua @@ -58,24 +58,37 @@ local function cssquery() local function make_nth(curr_el) local pos = 0 local el_pos = 0 + local type_pos = 0 -- get current node list local siblings = curr_el:get_siblings() if siblings then + local parent = curr_el:get_parent() + local element_types = {} for _, other_el in ipairs(siblings) do -- number the elements if other_el:is_element() then pos = pos + 1 other_el.nth = pos + -- save also element type, for nth-of-type and similar queries + local el_name = other_el:get_element_name() + local counter = (element_types[el_name] or 0) + 1 + other_el.type_nth = counter + element_types[el_name] = counter -- save the current element position if other_el == curr_el then el_pos = pos + type_pos = counter end end end + -- save counter of element types + parent.element_types = element_types + -- save count of elements + parent.child_elements = pos else return false end - return el_pos + return el_pos, type_pos end local function test_first_child(el, nth) @@ -83,6 +96,30 @@ local function cssquery() return el_pos == 1 end + local function test_first_of_type(el, val) + local type_pos = el.type_nth + if not type_pos then _, type_pos = make_nth(el) end + return type_pos == 1 + end + + local function test_last_child(el, val) + local el_pos = el.nth or make_nth(el) + -- number of child elements is saved in the parent element + -- by make_nth function + local parent = el:get_parent() + return el_pos == parent.child_elements + end + + local function test_last_of_type(el, val) + local type_pos = el.type_nth + if not type_pos then _, type_pos = make_nth(el) end + -- get table with type counts in this sibling list + local parent = el:get_parent() + local element_types = parent.element_types or {} + local type_count = element_types[el:get_element_name()] + return type_pos == type_count + end + -- test element for nth-child selector local function test_nth_child(el, nth) local el_pos = el.nth or make_nth(el) @@ -90,6 +127,50 @@ local function cssquery() return el_pos == tonumber(nth) end + -- test if element has attribute + local function test_attr(el, attr) + local result = el:get_attribute(attr) + return result~=nil + end + + local function test_any(el, value) + -- * selector + return true + end + + -- test attribute values + local function test_attr_value(el, parts) + -- parts is a table: {attr_name, modifier, search value} + local _, name, modifier, search = table.unpack(parts) + local value = el:get_attribute(name) + if value == nil then return false end + -- make sure we deal with a string + value = tostring(value) + -- make the search string safe for pattern matching + local escaped_search = search:gsub("([%(%)%.%%%+%–%*%?%[%^%$])", "%%%1") + if modifier == "" then + return value == search + elseif modifier == "|" then + -- attribute must be exactly the value or start with value + "-" + return value == search or (value:match("^" .. escaped_search .. "%-")~=nil) + elseif modifier == "~" then + -- test any word + for word in value:gmatch("(%S+)") do + if word == search then return true end + end + return false + elseif modifier == "^" then + -- value start + return value:match("^" .. escaped_search) ~= nil + elseif modifier == "$" then + -- value ends + return value:match(escaped_search .. "$") ~= nil + elseif modifier == "*" then + -- value anywhere + return value:match(escaped_search) ~= nil + end + end + --- Test prepared querylist -- @param domobj DOM element to test -- @param querylist [optional] List of queries to test @@ -118,6 +199,24 @@ local function cssquery() return test_nth_child(el, value) elseif key == "first-child" then return test_first_child(el, value) + elseif key == "first-of-type" then + return test_first_of_type(el, value) + elseif key == "last-child" then + return test_last_child(el, value) + elseif key == "last-of-type" then + return test_last_of_type(el, value) + elseif key == "attr" then + return test_attr(el, value) + elseif key == "attr_value" then + return test_attr_value(el, value) + elseif key == "any" then + return test_any(el, value) + elseif key == "combinator" then + -- ignore combinators in this function + else + if type(value) == "table" then value = table.concat(value, ":") end + print("unsupported feature", key, value) + return false end -- TODO: Add more cases -- just return true for not supported selectors @@ -128,7 +227,9 @@ local function cssquery() -- test one object in CSS selector local matched = {} for key, value in pairs(query) do - matched[#matched+1] = test_part(key, value, el) + local test = test_part(key, value, el) + if test~= true then return false end + matched[#matched+1] = test end if #matched == 0 then return false end for k, v in ipairs(matched) do @@ -137,17 +238,80 @@ local function cssquery() return true end - local function match_query(query, el) + + -- get next CSS selector + local function get_next_selector(query) local query = query or {} - local object = table.remove(query) -- get current object from the query stack + local selector = table.remove(query) + return selector + end + + local function get_next_combinator(query) + local query = query or {} + local combinator = " " -- default combinator + local selector = query[#query] -- get the last item in selector query + if not selector then return nil end + -- detect if this selector is a combinator" + if selector and selector.combinator then + -- save the combinator and select next selector from the query + combinator = selector.combinator + table.remove(query) -- remove combinator from query + end + return combinator + end + + local function get_previous_element(el) + -- try to find a previous element + local prev = el:get_prev_node() + if not prev then return nil end + if prev:is_element() then return prev end + return get_previous_element(prev) + end + + + local function match_query(query, el) + local function match_parent(query, el) + -- loop over the whole elemnt three and try to mach the css selector + if el and el:is_element() then + local query = query or {} + local object = query[#query] + local status = test_object(object, el) + return status or match_parent(query, el:get_parent()) + else + -- break processing if we reach top of the element tree + return false + end + end + local function match_sibling(query, el) + -- match potentially more distant sibling + if el and el:is_element() then + return match_query(query, el) or match_query(query, get_previous_element(el)) + else + return false + end + end + local object = get_next_selector(query) -- get current object from the query stack if not object then return true end -- if the query stack is empty, then we can be sure that it matched previous items - if not el:is_element() then return false end -- if there is object to test, but current node isn't element, test failed + if not el or not el:is_element() then return false end -- if there is object to test, but current node isn't element, test failed local result = test_object(object, el) if result then - return match_query(query, el:get_parent()) + local combinator = get_next_combinator(query) + if combinator == " " then + -- we must traverse all parent elements to find if any matches + return match_parent(query, el:get_parent()) + elseif combinator == ">" then -- simplest case, we need to match just the direct parent + return match_query(query, el:get_parent()) + elseif combinator == "+" then -- match previous element + return match_query(query, get_previous_element(el)) + elseif combinator == "~" then -- match all previous elements + return match_sibling(query, get_previous_element(el)) + elseif combinator == nil then + return result + end end return false end + for _,element in ipairs(querylist) do local query = {} for k,v in ipairs(element.query) do query[k] = v end @@ -191,7 +355,13 @@ local function cssquery() local t = {} for _, atom in ipairs(part) do local key = atom[1] - local value = atom[2] + local value + if not atom[3] then + value = atom[2] + else + -- save additional selector parts when available + value = atom + end -- support for XML namespaces in selectors -- the namespace should be added using "|" -- like namespace|element |