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
 | 
						|
 |