feat: CSS selectors
							parent
							
								
									86f8f67562
								
							
						
					
					
						commit
						4bab98ef0d
					
				| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue