feat: CSS selectors

main
Guilian 2025-01-18 18:53:18 +01:00
parent 76d7f2e67b
commit 0f4f9030aa
Signed by: Guilian
GPG Key ID: B86CC9678982ED8C
1 changed files with 176 additions and 0 deletions

176
css.lua Normal file
View File

@ -0,0 +1,176 @@
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 = {},
}
--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
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