summaryrefslogtreecommitdiff
path: root/macros/luatex/generic/luaxml/luaxml-cssquery.lua
diff options
context:
space:
mode:
Diffstat (limited to 'macros/luatex/generic/luaxml/luaxml-cssquery.lua')
-rw-r--r--macros/luatex/generic/luaxml/luaxml-cssquery.lua184
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