209 lines
4.2 KiB
Lua
209 lines
4.2 KiB
Lua
local M = {}
|
|
|
|
local function trim(str)
|
|
return str:match("^%s*(.-)%s*$")
|
|
end
|
|
|
|
|
|
local COMBINATORS = {
|
|
DESCENDANT = {},
|
|
DIRECT_DESCENDANT = {},
|
|
NEXT_SIBLING = {},
|
|
SUBSEQUENT_SIBLING = {},
|
|
}
|
|
M.COMBINATORS = COMBINATORS
|
|
|
|
local COMBINATOR_CHARS = {
|
|
[">"] = COMBINATORS.DIRECT_DESCENDANT,
|
|
["+"] = COMBINATORS.NEXT_SIBLING,
|
|
["~"] = COMBINATORS.SUBSEQUENT_SIBLING
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
local function create_tokeniser(input)
|
|
local pos = 1
|
|
local len = #input
|
|
|
|
local function peek()
|
|
if pos > len then return nil end
|
|
return input:sub(pos, pos)
|
|
end
|
|
|
|
local function next()
|
|
local char = peek()
|
|
if char then pos = pos + 1 end
|
|
return char
|
|
end
|
|
|
|
local function read_identifier()
|
|
local result = ""
|
|
while pos <= len do
|
|
local char = peek()
|
|
if char and char:match("[%w-]") then
|
|
result = result .. next()
|
|
else
|
|
break
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
return {
|
|
peek = peek,
|
|
next = next,
|
|
read_identifier = read_identifier,
|
|
pos = function() return pos end
|
|
}
|
|
end
|
|
|
|
|
|
local function parse_compound_selector( tokeniser )
|
|
local selector = {
|
|
tag_name = nil,
|
|
id = nil,
|
|
class = {},
|
|
attributes_values = {},
|
|
attributes_present = {},
|
|
}
|
|
|
|
--local selectors = {}
|
|
|
|
-- Parse first part (type or universal)
|
|
local char = tokeniser.peek()
|
|
if char == "*" then
|
|
tokeniser.next()
|
|
--table.insert(selectors, {type = "universal"})
|
|
selector.tag_name = "*"
|
|
elseif char and char:match("[%w-]") then
|
|
local name = tokeniser.read_identifier()
|
|
if name ~= "" then
|
|
--table.insert(selectors, {type = "type", value = name})
|
|
selector.tag_name = name
|
|
end
|
|
end
|
|
|
|
-- Parse additional class or ID selectors
|
|
while true do
|
|
char = tokeniser.peek()
|
|
if not char then break end
|
|
|
|
if char == "." then
|
|
tokeniser.next() -- consume '.'
|
|
local name = tokeniser.read_identifier()
|
|
if name == "" then
|
|
error("Expected class name at position " .. tokeniser.pos())
|
|
end
|
|
--table.insert(selectors, {type = "class", value = name})
|
|
table.insert( selector.class, name )
|
|
elseif char == "#" then
|
|
tokeniser.next() -- consume '#'
|
|
local name = tokeniser.read_identifier()
|
|
if name == "" then
|
|
error("Expected id at position " .. tokeniser.pos())
|
|
end
|
|
--table.insert(selectors, {type = "id", value = name})
|
|
selector.id = name
|
|
elseif char == "[" then
|
|
tokeniser.next() -- consume leading [
|
|
|
|
local name = tokeniser.read_identifier()
|
|
|
|
if tokeniser.peek() == "=" then
|
|
tokeniser.next()
|
|
|
|
if tokeniser.peek() ~= "\"" then
|
|
error("Expected opening quote \" at pos " .. tokeniser.pos() )
|
|
end
|
|
tokeniser.next() -- consume leading "
|
|
|
|
local value = ""
|
|
while tokeniser.peek() ~= "\"" do
|
|
value = value .. tokeniser.peek()
|
|
tokeniser.next()
|
|
end
|
|
|
|
tokeniser.next() -- consume trailing "
|
|
|
|
selector.attributes_values[name] = value
|
|
else
|
|
table.insert( selector.attributes_present, name )
|
|
end
|
|
|
|
if tokeniser.peek() ~= "]" then
|
|
error("Expected closing bracket (']') at " .. tokeniser.pos())
|
|
end
|
|
|
|
tokeniser.next() -- consume trailing ]
|
|
else
|
|
break
|
|
end
|
|
end
|
|
|
|
return selector
|
|
end
|
|
|
|
|
|
local function parse_combinator( tokeniser )
|
|
-- Skip leading whitespace
|
|
while tokeniser.peek() and tokeniser.peek():match("%s") do
|
|
tokeniser.next()
|
|
end
|
|
|
|
local char = tokeniser.peek()
|
|
if not char then return nil end
|
|
|
|
if char == ">" or char == "+" or char == "~" then
|
|
tokeniser.next()
|
|
-- Skip trailing whitespace
|
|
while tokeniser.peek() and tokeniser.peek():match("%s") do
|
|
tokeniser.next()
|
|
end
|
|
return COMBINATOR_CHARS[char]
|
|
else
|
|
-- Make sure next character isn't an explicit combinator
|
|
char = tokeniser.peek()
|
|
if char and not (char == ">" or char == "+" or char == "~") then
|
|
return COMBINATORS.DESCENDANT
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
|
|
|
|
|
|
function M.parse( input )
|
|
input = trim( input )
|
|
|
|
local tokeniser = create_tokeniser( input )
|
|
|
|
local output = { selector = parse_compound_selector( tokeniser ) }
|
|
local current = output
|
|
|
|
-- Parse combinations of combinators and compound selectors
|
|
while true do
|
|
local combinator = parse_combinator( tokeniser )
|
|
if not combinator then
|
|
current.combinator = nil
|
|
current.next = nil
|
|
break
|
|
end
|
|
|
|
local next_selector = parse_compound_selector( tokeniser )
|
|
current.combinator = combinator
|
|
current.next = { selector = next_selector }
|
|
current = current.next
|
|
end
|
|
|
|
return output
|
|
end
|
|
|
|
|
|
return M
|
|
|