local export = {}

local SAMPLE_TEXTS = mw.loadData("Module:font list/data").sample_texts

-- here is a CSS parser, until it is rewritten

local function css_state_new(text)
	return {
		source = text,
		source_index = 1,
		token_class = nil,
		token_data = nil
	}
end

local CSS_TOKEN_WHITESPACE =        nil -- note: assumed to be nil by e.g. css_token_logical_next
local CSS_TOKEN_END_OF_FILE =        -1
local CSS_TOKEN_IDENTIFIER =          0
local CSS_TOKEN_STRING =              1
local CSS_TOKEN_NUMBER =              2
local CSS_TOKEN_AT =                  3

local CSS_TOKEN_SEMICOLON =         100     -- ;
local CSS_TOKEN_BRACE_LEFT =        101     -- {
local CSS_TOKEN_BRACE_RIGHT =       102     -- }
local CSS_TOKEN_COLON =             103     -- :
local CSS_TOKEN_DOT =               104     -- .
local CSS_TOKEN_HASH =              105     -- #
local CSS_TOKEN_PLUS =              106     -- +
local CSS_TOKEN_GREATER =           107     -- >
local CSS_TOKEN_COMMA =             108     -- ,
local CSS_TOKEN_STAR =              109     -- *
local CSS_TOKEN_PERCENT =           110     -- %
local CSS_TOKEN_TILDE =             111     -- ~
local CSS_TOKEN_BRACKET_LEFT =      112     -- [
local CSS_TOKEN_BRACKET_RIGHT =     113     -- ]
local CSS_TOKEN_PAREN_LEFT =        114     -- (
local CSS_TOKEN_PAREN_RIGHT =       115     -- )
local CSS_TOKEN_PIPE =              116     -- |
local CSS_TOKEN_EQUALS =            117     -- =
local CSS_TOKEN_INCLUDES =          118     -- ~=
local CSS_TOKEN_BAR_EQUALS =        119     -- |=
local CSS_TOKEN_PREFIX_HAS =        120     -- ^=
local CSS_TOKEN_SUFFIX_HAS =        121     -- $=
local CSS_TOKEN_TEXT_INCLUDES =     122     -- *=
local CSS_TOKEN_IMPORTANT =         123     -- !important
local CSS_TOKEN_MINUS =             124     -- -
local CSS_TOKEN_DIVIDES =           125     -- /


local CSS_TOKEN_LOOKUP = {
	[";"] = CSS_TOKEN_SEMICOLON,
	["{"] = CSS_TOKEN_BRACE_LEFT,
	["}"] = CSS_TOKEN_BRACE_RIGHT,
	[":"] = CSS_TOKEN_COLON,
	["."] = CSS_TOKEN_DOT,
	["#"] = CSS_TOKEN_HASH,
	["+"] = CSS_TOKEN_PLUS,
	[">"] = CSS_TOKEN_GREATER,
	[","] = CSS_TOKEN_COMMA,
	["*"] = CSS_TOKEN_STAR,
	["%"] = CSS_TOKEN_PERCENT,
	["~"] = CSS_TOKEN_TILDE,
	["["] = CSS_TOKEN_BRACKET_LEFT,
	["]"] = CSS_TOKEN_BRACKET_RIGHT,
	["("] = CSS_TOKEN_PAREN_LEFT,
	[")"] = CSS_TOKEN_PAREN_RIGHT,
	["|"] = CSS_TOKEN_PIPE,
	["="] = CSS_TOKEN_EQUALS,
	["/"] = CSS_TOKEN_DIVIDES,
}


local CSS_TOKEN_LOOKUP_EQUALS = {
	["~"] = CSS_TOKEN_INCLUDES,
	["|"] = CSS_TOKEN_BAR_EQUALS,
	["^"] = CSS_TOKEN_PREFIX_HAS,
	["$"] = CSS_TOKEN_SUFFIX_HAS,
	["*"] = CSS_TOKEN_TEXT_INCLUDES,
}


local function css_token_parse_backslash(src, idx)
	local result
	if src:match("^[\r\n]", idx) then
		local _, end_idx = src:find("^\r?\n?", idx)
		idx = end_idx + 1
		result = ""
	elseif src:match("^['\"]", idx) then
		result = src:sub(idx, idx)
		idx = idx + 1
	elseif src:match("^[0-9A-Fa-f]", idx) then
		local _, end_idx = src:find("^[0-9A-Fa-f]+", idx)
		if end_idx > idx + 5 then
			-- at most 6 hex digits
			end_idx = idx + 5
		end
		result = mw.ustring.char(tonumber(src:sub(idx, end_idx), 16))

		idx = end_idx + 1
		if src:match("^%s", idx) then
			idx = idx + 1
		end
	else
		result = src:sub(idx, idx)
		idx = idx + 1
	end
	return idx, result
end

local function css_token_read_next(state)
	local src = state.source
	local idx = state.source_index

	if idx > #src then
		-- nothing more
		return CSS_TOKEN_END_OF_FILE
	end

	while src:match("^/%*", idx) do
		-- skip comments
		idx = idx + 2
		local _, end_idx = src:find("%*/", idx)
		if end_idx == nil then
			state.source_index = #src + 1
			-- nothing more
			return CSS_TOKEN_END_OF_FILE
		end
		idx = end_idx + 2
	end

	local c1 = src:sub(idx, idx)
	local eq1 = CSS_TOKEN_LOOKUP[c1]
	if eq1 then
		if src:sub(idx + 1, idx + 1) == "=" then
			state.source_index = idx + 2
			local eq2 = CSS_TOKEN_LOOKUP_EQUALS[c1]
			if eq2 then return eq2 end
		end
	
		state.source_index = idx + 1
		return eq1

	elseif src:match("^%s", idx) then
		-- whitespace
		local _, end_idx = src:find("^%s+", idx)
		state.source_index = end_idx + 1
		return CSS_TOKEN_WHITESPACE

	elseif src:match("^['\"]", idx) then
		-- strings
		local quote = src:sub(idx, idx)
		idx = idx + 1

		local mask = "([" .. quote .. "\\])"
		local css_string = ""
		while true do
			local next_idx, _, next_chr = src:find(mask, idx)
			if next_idx == nil then
				state.source_index = #src + 1
				error("CSS error: unterminated string")
			end

			css_string = css_string .. src:sub(idx, next_idx - 1)

			idx = next_idx + 1
			if next_chr == quote then
				break
			elseif next_chr == "\\" then
				-- backslash escape
				local result
				idx, result = css_token_parse_backslash(src, idx)
				css_string = css_string .. result
			end
		end

		state.source_index = idx
		return CSS_TOKEN_STRING, css_string

	elseif src:match("^[0-9]", idx) or src:match("^%-[0-9.]", idx) or src:match("^%.[0-9]", idx) or src:match("^%+[0-9]", idx) then
		-- number
		local start_idx = idx
		if src:match("^[+-]", idx) then
			idx = idx + 1
		end

		local _, end_idx = src:find("^[0-9]*", idx)
		idx = end_idx + 1

		-- dot
		if src:match("^%.", idx) then
			idx = idx + 1
			local _, end_idx = src:find("^[0-9]*", idx)
			idx = end_idx + 1
		end

		local result = tonumber(src:sub(start_idx, idx - 1))
		state.source_index = idx
		return CSS_TOKEN_NUMBER, result

	elseif src:match("^[a-zA-Z]", idx) or src:match("^%-?%-?[a-zA-Z]", idx) or src:match("^%-%-%-", idx) then
		-- identifier
		local start_idx = idx

		local identifier = ""
		while true do
			local next_idx, _, next_chr = src:find("[^A-Za-z0-9\128-\255_-]", idx)

			if next_idx == nil then
				next_idx = #src + 1
			end
			identifier = identifier .. src:sub(idx, next_idx - 1)
			idx = next_idx
			if next_chr == "\\" then
				-- backslash escape

				local result
				idx, result = css_token_parse_backslash(src, idx + 1)
				identifier = identifier .. result
			elseif next_chr ~= "\\" then
				break
			end
		end

		state.source_index = idx
		
		if identifier == "-" then
			return CSS_TOKEN_MINUS
		end
		return CSS_TOKEN_IDENTIFIER, identifier

	elseif src:match("^@[A-Za-z-]", idx) then
		-- @ token
		local start_idx = idx + 1

		local _, end_idx = src:find("^[A-Za-z0-9-]*", idx)
		state.source_index = end_idx + 1
		return CSS_TOKEN_AT, src.sub(start_idx, end_idx)

	elseif src:match("^!important", idx) then
		state.source_index = idx + 10
		return CSS_TOKEN_IMPORTANT
	end

	-- unrecognized character.
	error("CSS error: unrecognized character '" .. src:sub(idx, idx) .. "'")
end

local function css_token_next(state)
	local token, token_data = css_token_read_next(state)
	state.token_class, state.token_data = token, token_data
	return token, token_data
end

local function css_token_logical_next(state)
	-- css_token_next but skips whitespace
	local token, token_data = css_token_read_next(state)
	while not token do
		token, token_data = css_token_read_next(state)
	end
	state.token_class, state.token_data = token, token_data
	return token, token_data
end

local CSS_COLON_SUBSELECTOR = {
	["current"] = true,
	["has"] = true,
	["is"] = true,
	["matches"] = true,
	["not"] = true,
	["where"] = true,
}

local CSS_COLON_TEXT_NUMBER = {
	["dir"] = true,
	["lang"] = true,
	["nth-child"] = true,
	["nth-of-type"] = true,
	["nth-last-child"] = true,
	["nth-last-of-type"] = true,
}

local function css_parse_selectors(state, token, token_data)
	local selectors = {}
	local leaf = {}

	table.insert(selectors, leaf)
	while true do
		if token == CSS_TOKEN_STAR then
			-- match all
			token, token_data = css_token_next(state)

		elseif token == CSS_TOKEN_IDENTIFIER then
			-- element
			if leaf.element then
				error("CSS internal error: invalid type selector")
			end
			leaf.element = token_data
			token, token_data = css_token_next(state)

		elseif token == CSS_TOKEN_DOT then
			-- .class
			token, token_data = css_token_next(state)
			if token ~= CSS_TOKEN_IDENTIFIER then
				error("CSS error: invalid class selector")
			end
			if leaf.class then
				if type(leaf.class) == "string" then
					leaf.class = { leaf.class }
				end
				table.insert(leaf.class, token_data)
			else
				leaf.class = token_data
			end
			token, token_data = css_token_next(state)

		elseif token == CSS_TOKEN_HASH then
			-- #id
			token, token_data = css_token_next(state)
			if token ~= CSS_TOKEN_IDENTIFIER then
				error("CSS error: invalid class selector")
			end
			if leaf.id then
				leaf.id = { } -- never matches
			else
				leaf.id = token_data
			end
			token, token_data = css_token_next(state)

		elseif token == CSS_TOKEN_COLON then
			-- :pseudo...(...)
			token, token_data = css_token_next(state)
			if token ~= CSS_TOKEN_IDENTIFIER then
				error("CSS error: invalid colon selector")
			end
			local colon_type = mw.ustring.lower(token_data)
			local colon = { type = colon_type }
			if not leaf.pseudo then
				leaf.pseudo = {}
			end
			table.insert(leaf.pseudo, colon)
			token, token_data = css_token_next(state)
			if token == CSS_TOKEN_PAREN_LEFT then
				-- (...)
				token, token_data = css_token_logical_next(state)

				if CSS_COLON_SUBSELECTOR[colon_type] then
					local selectors
					token, token_data, selectors = css_parse_selectors(state, token, token_data)
					colon.selectors = selectors
				elseif CSS_COLON_TEXT_NUMBER[colon_type] then
					local content = ""
					while true do
						if token == CSS_TOKEN_NUMBER then
							content = content .. tostring(token_data)
						elseif token == CSS_TOKEN_IDENTIFIER then
							content = content .. token_data
						elseif token == CSS_TOKEN_PLUS then
							content = content .. "+"
						elseif token == CSS_TOKEN_WHITESPACE then
							content = content .. " "
						elseif token == CSS_TOKEN_PAREN_RIGHT then
							token, token_data = css_token_next(state)
							break
						else
							error("CSS error: unsupported token in parameterized pseudo-class")
						end
						token, token_data = css_token_next(state)
					end
					colon.value = content
				else
					error("CSS error: unsupported parameterized pseudo-class")
				end
			elseif CSS_COLON_SUBSELECTOR[colon_type] or CSS_COLON_TEXT_NUMBER[colon_type] then
				error("CSS error: expected parameterized pseudo-class")
			end

		elseif token == CSS_TOKEN_BRACKET_LEFT then
			-- attribute
			token, token_data = css_token_logical_next(state)
			if token ~= CSS_TOKEN_IDENTIFIER then
				error("CSS error: invalid colon selector")
			end

			local attribute = { name = mw.ustring.lower(token_data) }
			if not leaf.attributes then
				leaf.attributes = {}
			end
			table.insert(leaf.attributes, attribute)

			local operator
			token, token_data = css_token_logical_next(state)
			if token == CSS_TOKEN_EQUALS then
				operator = "is"
			elseif token == CSS_TOKEN_INCLUDES then
				operator = "word"
			elseif token == CSS_TOKEN_BAR_EQUALS then
				operator = "bar_prefix"
			elseif token == CSS_TOKEN_PREFIX_HAS then
				operator = "prefix"
			elseif token == CSS_TOKEN_SUFFIX_HAS then
				operator = "suffix"
			elseif token == CSS_TOKEN_TEXT_INCLUDES then
				operator = "factor"
			else
				error("CSS error: unsupported attribute selector operator")
			end

			local attribute_value
			token, token_data = css_token_logical_next(state)
			if token == CSS_TOKEN_STRING then
				attribute_value = token_data
			elseif token == CSS_TOKEN_IDENTIFIER then
				attribute_value = token_data
			else
				error("CSS error: unsupported attribute selector value")
			end

			token, token_data = css_token_logical_next(state)
			if token == CSS_TOKEN_IDENTIFIER then
				local modifier = mw.ustring.lower(token_data)
				if modifier == "s" then
					attribute.case_insensitive = false
				elseif modifier == "i" then
					attribute.case_insensitive = true
				else
					error("CSS error: unsupported attribute selector modifier")
				end
				token, token_data = css_token_logical_next(state)
			end

			if token ~= CSS_TOKEN_BRACE_RIGHT then
				error("CSS error: expected ] to end attribute selector")
			end

			attribute[operator] = attribute_value
			token, token_data = css_token_next(state)

		else
			error("CSS internal error: invalid selector")
		end

		local had_whitespace = false
		had_whitespace = token == CSS_TOKEN_WHITESPACE
		if had_whitespace then
			token, token_data = css_token_logical_next(state)
		end
		if token == CSS_TOKEN_COMMA then
			-- next selector
			leaf = {}
			table.insert(selectors, leaf)
			token, token_data = css_token_logical_next(state)
		elseif token == CSS_TOKEN_BRACE_LEFT or token == CSS_TOKEN_PAREN_RIGHT then
			break
		elseif token == CSS_TOKEN_PIPE then
			-- a|b: namespace combinator
			for key in pairs(leaf) do
				if key ~= "element" then
					error("CSS error: invalid namespace selector")
				end
			end
			if key.namespace or not key.element then
				error("CSS error: invalid namespace selector")
			end
			key.namespace = key.element
			key.element = nil
			token, token_data = css_token_logical_next(state)

		elseif token == CSS_TOKEN_PLUS then
			-- a + b: next sibling combinator
			leaf.next = {}
			leaf = leaf.next
			token, token_data = css_token_logical_next(state)
		elseif token == CSS_TOKEN_TILDE then
			-- a ~ b: subsequent sibling combinator
			leaf.after = {}
			leaf = leaf.after
			token, token_data = css_token_logical_next(state)
		elseif token == CSS_TOKEN_GREATER then
			-- a > b: child combinator
			leaf.child = {}
			leaf = leaf.child
			token, token_data = css_token_logical_next(state)
		elseif had_whitespace then
			-- a b: descendant combinator
			leaf.descendant = {}
			leaf = leaf.descendant
		end
	end

	return token, token_data, selectors
end

local function css_parse_if_end_of_declaration(token)
	return token == CSS_TOKEN_SEMICOLON or token == CSS_TOKEN_BRACE_RIGHT or token == CSS_TOKEN_IMPORTANT
end

local CSS_FONT_FAMILY_BUILTIN = {
	["serif"] = true,
	["sans-serif"] = true,
	["monospace"] = true,
	["cursive"] = true,
	["fantasy"] = true,
	["system-ui"] = true,
	["ui-serif"] = true,
	["ui-sans-serif"] = true,
	["ui-monospace"] = true,
	["ui-rounded"] = true,
	["emoji"] = true,
	["math"] = true,
	["fangsong"] = true,
	["inherit"] = true,
	["initial"] = true,
	["revert"] = true,
	["revert-layer"] = true,
	["unset"] = true
}

local CSS_ONE_IDENTIFIER_PROPERTIES = {
	['writing-mode'] = true,
	['layout-flow'] = true,
}

local function css_parse_declaration_value(state, token, token_data, property)
	local result
	local important

	-- only parse what we need
	if property == "font-family" then
		local quoted = false
		local fonts = {}
		local font_name = nil

		while true do
			if token == CSS_TOKEN_IDENTIFIER then
				if font_name then
					font_name = font_name .. " " .. token_data
				else
					font_name = token_data
				end

			elseif token == CSS_TOKEN_STRING then
				quoted = true
				if font_name then
					font_name = font_name .. " " .. token_data
				else
					font_name = token_data
				end

			elseif token == CSS_TOKEN_COMMA then
				font_name = font_name or ""
				if not quoted and CSS_FONT_FAMILY_BUILTIN[font_name] then
					table.insert(fonts, { builtin = font_name })
				else
					table.insert(fonts, font_name)
				end
				
				quoted = false
				font_name = nil

			elseif not token then
				-- skip

			elseif css_parse_if_end_of_declaration(token) then
				break

			else
				error("Unexpected token in font-family")
			end

			token, token_data = css_token_logical_next(state)
		end

		font_name = font_name or ""
		if not quoted and CSS_FONT_FAMILY_BUILTIN[font_name] then
			table.insert(fonts, { builtin = font_name })
		else
			table.insert(fonts, font_name)
		end
	
		result = fonts
	
	elseif property == "font-size" then
		if token == CSS_TOKEN_NUMBER then
			result = tostring(token_data)
			token, token_data = css_token_logical_next(state)
			if token == CSS_TOKEN_IDENTIFIER then
				result = result .. token_data
				token, token_data = css_token_logical_next(state)
			elseif token == CSS_TOKEN_PERCENT then
				result = result .. "%"
				token, token_data = css_token_logical_next(state)
			end
		elseif token == CSS_TOKEN_IDENTIFIER then
			result = token_data
			token, token_data = css_token_logical_next(state)
		else
			error("unsupported font-weight")
		end

	elseif property == "font-weight" then
		if token == CSS_TOKEN_NUMBER then
			result = tostring(token_data)
		elseif token == CSS_TOKEN_IDENTIFIER then
			result = token_data
		else
			error("unsupported font-weight")
		end
		token, token_data = css_token_logical_next(state)

	elseif CSS_ONE_IDENTIFIER_PROPERTIES[property] then
		if token == CSS_TOKEN_IDENTIFIER then
			result = token_data
		else
			error("unsupported " .. property)
		end
		token, token_data = css_token_logical_next(state)

	else
		while not css_parse_if_end_of_declaration(token) do
			token, token_data = css_token_logical_next(state)
		end
		result = nil
	end

	if not css_parse_if_end_of_declaration(token) then
		error("CSS error: expected end of declaration")
	end
	if token ~= CSS_TOKEN_BRACE_RIGHT then
		token, token_data = css_token_logical_next(state)
		if token == CSS_TOKEN_IMPORTANT then
			important = true
			token, token_data = css_token_logical_next(state)
			if not (token == CSS_TOKEN_SEMICOLON or token == CSS_TOKEN_BRACE_RIGHT) then
				error("CSS error: expected end of declaration")
			end
		end
	end

	return token, token_data, result, important
end

local function css_parse_declaration(state, token, token_data)
	if token ~= CSS_TOKEN_IDENTIFIER then
		error("expected CSS property")
	end

	local property = mw.ustring.lower(token_data)

	token, token_data = css_token_logical_next(state)
	if token ~= CSS_TOKEN_COLON then
		error("expected colon after CSS property")
	end

	local value
	token, token_data = css_token_logical_next(state)
	token, token_data, value, important = css_parse_declaration_value(state, token, token_data, property)
	return token, token_data, { property = property, value = value, important = important }
end

local function css_parse_ruleblock(state, token, token_data)
	if token ~= CSS_TOKEN_BRACE_LEFT then
		error("CSS: expected {")
	end
	token, token_data = css_token_logical_next(state)

	local rules = { }
	local rule
	while true do
		if token == CSS_TOKEN_SEMICOLON or token == CSS_TOKEN_WHITESPACE then
			-- next token
			token, token_data = css_token_logical_next(state)
		elseif token == CSS_TOKEN_BRACE_RIGHT then
			token, token_data = css_token_logical_next(state)
			break
		else
			token, token_data, rule = css_parse_declaration(state, token, token_data)
			table.insert(rules, rule)
		end
	end

	return token, token_data, rules
end

local function css_parse_atrule(state, token, token_data, rule)
	-- skip rest of the statement, or a block of braces

	if rule == "container" or rule == "media" or rule == "supports" then
		while token ~= CSS_TOKEN_BRACE_LEFT do
			token, token_data = css_token_logical_next(state)
			if token == CSS_TOKEN_END_OF_FILE then
				error("CSS error: unexpected end of file")
			end
		end

		while token ~= CSS_TOKEN_BRACE_RIGHT do
			token, token_data = css_token_logical_next(state)
			if token == CSS_TOKEN_END_OF_FILE then
				error("CSS error: unexpected end of file")
			end

			token, token_data = css_parse_ruleset(state, token, token_data)
		end
	else
		while true do
			if token == CSS_TOKEN_SEMICOLON then
				token, token_data = css_token_logical_next(state)
				break
			elseif token == CSS_TOKEN_BRACE_LEFT then
				token, token_data = css_parse_ruleblock(state, token, token_data)
				break
			end
		end
	end

	return token, token_data, { at_rule = rule }
end

local function css_parse_ruleset_or_atrule(state, token, token_data)
	if token == CSS_TOKEN_END_OF_FILE then
		return nil
	elseif token == CSS_TOKEN_AT then
		return css_parse_atrule(state, token, token_data)
	end

	local selector, rules
	token, token_data, selectors = css_parse_selectors(state, token, token_data)
	token, token_data, rules = css_parse_ruleblock(state, token, token_data)
	return token, token_data, { selectors = selectors, rules = rules }
end

local function css_parse_document(state)
	local rulesets = {}
	local ruleset
	local token, token_data = css_token_logical_next(state)
	while true do
		token, token_data, ruleset = css_parse_ruleset_or_atrule(state, token, token_data)
		if not ruleset then
			break
		end
		table.insert(rulesets, ruleset)
	end
	return rulesets
end

-- use CSS data to get font families and sizes

local function script_language_next(state, index)
	local selectors = state.selectors

	while true do
		index = index + 1
		local selector = selectors[index]

		if not selector then
			break
		end

		if selector.class and type(selector.class) == "string" then
			local script_code = selector.class
			if state.script_codes[script_code] then
				-- is valid script code
				-- check if has language
				if selector.pseudo then
					for _, pseudo in ipairs(selector.pseudo) do
						if pseudo.type == "lang" then
							local language_code = pseudo.value
							if state.language_codes[language_code] then
								return index, script_code, language_code, script_code
							end
						end
					end
				end
				if selector.attributes then
					for _, attribute in ipairs(selector.attributes) do
						if attribute.name == "lang" and attribute.is then
							local language_code = attribute.is
							if state.language_codes[language_code] then
								return index, script_code, language_code, script_code
							end
						end
					end
				end
				-- hack to deal with hyphenated codes
				if script_code:find("%-") then
					local split_lang, split_sc = script_code:match("([a-z-]+)%-([A-Za-z]+)")
					if state.script_codes[split_sc] and state.language_codes[split_lang] then
						return index, script_code, split_lang, split_sc
					end
					return index, script_code, nil, split_sc
				end
				return index, script_code, nil, script_code
			end
		end
	end
end

local function script_language_pairs(selectors, script_codes, language_codes)
	return script_language_next, {
		selectors = selectors,
		script_codes = script_codes,
		language_codes = language_codes,
	}, 0
end

local function get_font_by_script_language(parsed)
	local script_codes = mw.loadData("Module:scripts/code to canonical name")
	local language_codes = mw.loadData("Module:languages/code to canonical name")

	local keys = {}
	local sorted_keys = {}

	function make_key(sc, lang)
		if lang then
			return sc .. "/" .. lang
		else
			return sc
		end
	end

	for ruleset_i, ruleset in ipairs(parsed) do
		if ruleset.selectors and ruleset.rules then
			local fonts = {}
			local special = {}

			for rule_i, rule in ipairs(ruleset.rules) do
				if rule.property == "font-family" then
					fonts = rule.value
				elseif false and rule.value then
					special[rule.property] = rule.value
				end
			end

			if #fonts > 1 or (#fonts == 1 and not fonts[1].builtin) then
				for pair_i, sc, lang, effective_sc in script_language_pairs(ruleset.selectors, script_codes, language_codes) do
					local key = make_key(sc, lang)
					local row = keys[key]
					if not row then
						row = { sc, lang, fonts = fonts }
						row.name = sc and script_codes[sc] or nil
						row.language_name = lang and language_codes[lang] or nil
						keys[key] = row
						table.insert(sorted_keys, key)
					end
					row.fonts = fonts
					if false then
						for special_key, special_value in pairs(special) do
							if not row.css then
								row.css = {}
							end
							row.css[special_key] = special_value
						end
					end
					row.effective_script_code = effective_sc or sc
					row.effective_script_name = script_codes[row.effective_script_code]
				end
			end
		end
	end

	table.sort(sorted_keys, function (ka, kb)
		local a = keys[ka]
		local b = keys[kb]
		return a.effective_script_name .. "/" .. a[1] .. "/" .. (a.language_name or "") < b.effective_script_name .. "/" .. b[1] .. "/" .. (b.language_name or "") 
	end)

	local sorted_rows = {}
	for _, key in ipairs(sorted_keys) do
		table.insert(sorted_rows, keys[key])
	end

	return sorted_rows
end

local function make_table(data)
	local tokens = {}

	local script_row_spans = {}
	local scripts_with_lang = {}
	for i, row in ipairs(data) do
		script_row_spans[row[1]] = (script_row_spans[row[1]] or 0) + #row.fonts + 1
		if row[2] then
			scripts_with_lang[row.effective_script_code] = true
		end
	end

	local seen_scripts = {}
	local default_example_text = SAMPLE_TEXTS[""]

	local last_script_heading

	for i, row in ipairs(data) do
		local sc, lang = unpack(row)
		local span
		local esc = row.effective_script_code
		local script_heading = row.effective_script_name
		if last_script_heading ~= script_heading then
			if last_script_heading then
				table.insert(tokens, "\n|}\n")
			end
			table.insert(tokens, "==" .. script_heading .. "==\n")
			if scripts_with_lang[esc] then
				table.insert(tokens, '{| class="wikitable font-list fonts-' .. esc .. [=[" 
! Code
! Language
! Font
! Sample
]=])
			else
				table.insert(tokens, '{| class="wikitable font-list fonts-' .. esc .. [=["
! Code
! Font
! Sample
]=])
			end
			last_script_heading = script_heading
		end

		table.insert(tokens, "|-\n")
		local example_key
		if lang then
			example_key = sc .. ":" .. lang
		else
			example_key = sc
		end
		
		local example_text
		if row.effective_script_code ~= sc then
			local example_key2
			if lang then
				example_key2 = esc .. ":" .. lang
			else
				example_key2 = esc
			end
			
			example_text = SAMPLE_TEXTS[sc] or SAMPLE_TEXTS[example_key2] or SAMPLE_TEXTS[esc] or default_example_text
		else
			example_text = SAMPLE_TEXTS[example_key] or SAMPLE_TEXTS[sc] or default_example_text
		end

		if not seen_scripts[sc] then
			local name_paren
			table.insert(tokens, '|class="codes" rowspan="' .. script_row_spans[sc] .. '"|')
			if row.name ~= script_heading then
				name_paren = "<br>(" .. row.name .. ")"
			else
				name_paren = ""
			end
			table.insert(tokens, '<code>' .. sc .. '</code>' .. name_paren .. '\n')
			seen_scripts[sc] = true
		end

		local col_start = '|class="codes" rowspan="' .. (#row.fonts + 1) .. '"| '
		if lang then
			table.insert(tokens, col_start .. row.language_name .. '<br>(<code>' .. lang .. '</code>)\n')
		elseif scripts_with_lang[sc] then
			table.insert(tokens, col_start .. "(other)\n")
		elseif scripts_with_lang[esc] then
			table.insert(tokens, col_start .. "(all)\n")
		end

		table.insert(tokens, '|class="fontauto"| &lt;current&gt;\n|')
		table.insert(tokens, tostring(mw.html.create("span"):addClass(sc):attr("lang", lang):wikitext(example_text)))
		table.insert(tokens, '\n')

		for font_i, font in ipairs(row.fonts) do
			local css_text, font_name, font_name_class
			if font.builtin then
				css_text = 'font-family:' .. font.builtin
				font_name = "&lt;" .. font.builtin .. "&gt;"
				font_name_class = 'fontauto'
			else
				css_text = font
				if mw.ustring.find(css_text, '\\') then
					css_text = mw.ustring.gsub(css_text, '\\', '\\\\')
				end
				if mw.ustring.find(css_text, '"') then
					css_text = mw.ustring.gsub(css_text, '"', '\"')
				end
				css_text = 'font-family:"' .. css_text .. '"'
				font_name = font
				font_name_class = 'fontname'
			end

			table.insert(tokens, '|-\n|class="' .. (font_name_class or '') .. '"| ' .. font_name .. '||')
			table.insert(tokens, tostring(mw.html.create("span"):attr("lang", lang):addClass(sc):cssText(css_text):wikitext(example_text)))
			table.insert(tokens, '\n')
		end
	end

	table.insert(tokens, "\n|}")
	return table.concat(tokens, "")
end

function export.show(frame)
	local css = mw.title.new("MediaWiki:Gadget-LanguagesAndScripts.css"):getContent()
	
	local state = css_state_new(css)
	local parsed = css_parse_document(state)

	local data = get_font_by_script_language(parsed)
	return make_table(data) .. require("Module:TemplateStyles")("Module:font list/style.css")
end

return export