move wayfinder files to Flumi folder

This commit is contained in:
Face
2025-08-02 14:18:14 +03:00
parent 8fbabdd01a
commit e575974721
442 changed files with 6 additions and 188 deletions

View File

@@ -0,0 +1,828 @@
class_name CSSParser
extends RefCounted
class CSSRule:
var selector: String
var event_prefix: String = ""
var properties: Dictionary = {}
var specificity: int = 0
var selector_type: String = "simple" # simple, descendant, child, adjacent_sibling, general_sibling, attribute
var selector_parts: Array = [] # For complex selectors
func init(sel: String = ""):
selector = sel
parse_selector()
calculate_specificity()
func parse_selector():
if selector.contains(":"):
var parts = selector.split(":", false, 1)
if parts.size() == 2:
selector = parts[0]
event_prefix = parts[1]
# Parse complex selectors
if selector.contains(" > "):
selector_type = "child"
selector_parts = selector.split(" > ")
elif selector.contains(" + "):
selector_type = "adjacent_sibling"
selector_parts = selector.split(" + ")
elif selector.contains(" ~ "):
selector_type = "general_sibling"
selector_parts = selector.split(" ~ ")
elif selector.contains("["):
selector_type = "attribute"
parse_attribute_selector()
elif selector.contains(" "):
selector_type = "descendant"
selector_parts = selector.split(" ")
else:
selector_type = "simple"
selector_parts = [selector]
func parse_attribute_selector():
var bracket_start = selector.find("[")
var bracket_end = selector.find("]")
if bracket_start != -1 and bracket_end != -1:
var element_part = selector.substr(0, bracket_start)
var attribute_part = selector.substr(bracket_start + 1, bracket_end - bracket_start - 1)
selector_parts = [element_part, attribute_part]
func calculate_specificity():
specificity = 1
if selector.begins_with("."):
specificity += 10
if selector.contains("["):
specificity += 10 # Attribute selectors
match selector_type:
"child":
specificity += 8
"adjacent_sibling":
specificity += 7
"attribute":
specificity += 6
"general_sibling":
specificity += 5
"descendant":
specificity += 4
if event_prefix.length() > 0:
specificity += 10
class CSSStylesheet:
var rules: Array[CSSRule] = []
func add_rule(rule: CSSRule):
rules.append(rule)
func get_styles_for_element(tag_name: String, event: String = "", class_names: Array[String] = [], element: HTMLParser.HTMLElement = null) -> Dictionary:
var styles = {}
# Sort rules by specificity
var applicable_rules: Array[CSSRule] = []
for rule in rules:
if selector_matches(rule, tag_name, event, class_names, element):
applicable_rules.append(rule)
applicable_rules.sort_custom(func(a, b): return a.specificity < b.specificity)
# Apply styles in order of specificity
for rule in applicable_rules:
for property in rule.properties:
styles[property] = rule.properties[property]
return styles
func selector_matches(rule: CSSRule, tag_name: String, event: String = "", cls_names: Array[String] = [], element: HTMLParser.HTMLElement = null) -> bool:
if rule.event_prefix.length() > 0:
if rule.event_prefix != event:
return false
elif event.length() > 0:
return false
match rule.selector_type:
"simple":
return matches_simple_selector(rule.selector_parts[0], tag_name, cls_names)
"descendant":
return matches_descendant_selector(rule.selector_parts, element)
"child":
return matches_child_selector(rule.selector_parts, element)
"adjacent_sibling":
return matches_adjacent_sibling_selector(rule.selector_parts, element)
"general_sibling":
return matches_general_sibling_selector(rule.selector_parts, element)
"attribute":
return matches_attribute_selector(rule.selector_parts, tag_name, cls_names, element)
return false
func matches_simple_selector(selector: String, tag_name: String, cls_names: Array[String]) -> bool:
if selector.begins_with("."):
var cls = selector.substr(1)
return cls in cls_names
else:
return selector == tag_name
func matches_descendant_selector(parts: Array, element: HTMLParser.HTMLElement) -> bool:
if not element or parts.size() < 2:
return false
# Last part should match current element
var last_part = parts[-1].strip_edges()
if not matches_simple_selector(last_part, element.tag_name, get_element_class_names(element)):
return false
# Check ancestors for remaining parts
var current_element = element.parent
var part_index = parts.size() - 2
while current_element and part_index >= 0:
var part = parts[part_index].strip_edges()
if matches_simple_selector(part, current_element.tag_name, get_element_class_names(current_element)):
part_index -= 1
if part_index < 0:
return true
current_element = current_element.parent
return false
func matches_child_selector(parts: Array, element: HTMLParser.HTMLElement) -> bool:
if not element or not element.parent or parts.size() != 2:
return false
var child_part = parts[1].strip_edges()
var parent_part = parts[0].strip_edges()
# Element must match the child part
if not matches_simple_selector(child_part, element.tag_name, get_element_class_names(element)):
return false
# Parent must match the parent part
return matches_simple_selector(parent_part, element.parent.tag_name, get_element_class_names(element.parent))
func matches_adjacent_sibling_selector(parts: Array, element: HTMLParser.HTMLElement) -> bool:
if not element or not element.parent or parts.size() != 2:
return false
var second_part = parts[1].strip_edges()
var first_part = parts[0].strip_edges()
if not matches_simple_selector(second_part, element.tag_name, get_element_class_names(element)):
return false
# Find previous sibling
var siblings = element.parent.children
var element_index = siblings.find(element)
if element_index <= 0:
return false
var prev_sibling = siblings[element_index - 1]
return matches_simple_selector(first_part, prev_sibling.tag_name, get_element_class_names(prev_sibling))
func matches_general_sibling_selector(parts: Array, element: HTMLParser.HTMLElement) -> bool:
if not element or not element.parent or parts.size() != 2:
return false
var second_part = parts[1].strip_edges()
var first_part = parts[0].strip_edges()
if not matches_simple_selector(second_part, element.tag_name, get_element_class_names(element)):
return false
# Check all previous siblings
var siblings = element.parent.children
var element_index = siblings.find(element)
for i in range(element_index):
var sibling = siblings[i]
if matches_simple_selector(first_part, sibling.tag_name, get_element_class_names(sibling)):
return true
return false
func matches_attribute_selector(parts: Array, tag_name: String, cls_names: Array[String], element: HTMLParser.HTMLElement) -> bool:
if not element or parts.size() != 2:
return false
var element_part = parts[0].strip_edges()
var attribute_part = parts[1].strip_edges()
# Check if element matches
if element_part != "" and not matches_simple_selector(element_part, tag_name, cls_names):
return false
# Parse attribute condition
if attribute_part.contains("="):
var parsed = {}
var element_value = ""
if attribute_part.contains("^="):
# Starts with
parsed = parse_attribute_value(attribute_part, "^=")
element_value = element.get_attribute(parsed.name)
return element_value.begins_with(parsed.value)
elif attribute_part.contains("$="):
# Ends with
parsed = parse_attribute_value(attribute_part, "$=")
element_value = element.get_attribute(parsed.name)
return element_value.ends_with(parsed.value)
elif attribute_part.contains("*="):
# Contains
parsed = parse_attribute_value(attribute_part, "*=")
element_value = element.get_attribute(parsed.name)
return element_value.contains(parsed.value)
else:
# Exact match
parsed = parse_attribute_value(attribute_part, "=")
return element.get_attribute(parsed.name) == parsed.value
else:
# Just check if attribute exists
return element.has_attribute(attribute_part)
func parse_attribute_value(attribute_part: String, operator: String) -> Dictionary:
var attr_parts = attribute_part.split(operator)
var attr_name = attr_parts[0].strip_edges()
var attr_value = attr_parts[1].strip_edges()
# Remove quotes
if attr_value.begins_with('"') and attr_value.ends_with('"'):
attr_value = attr_value.substr(1, attr_value.length() - 2)
elif attr_value.begins_with("'") and attr_value.ends_with("'"):
attr_value = attr_value.substr(1, attr_value.length() - 2)
return {"name": attr_name, "value": attr_value}
func get_element_class_names(element: HTMLParser.HTMLElement) -> Array[String]:
var class_names: Array[String] = []
var class_attr = element.get_attribute("class")
if class_attr.length() > 0:
var classes = class_attr.split(" ")
for cls in classes:
cls = cls.strip_edges()
if cls.length() > 0:
class_names.append(cls)
return class_names
var stylesheet: CSSStylesheet
var css_text: String
func init(css_content: String = ""):
stylesheet = CSSStylesheet.new()
css_text = css_content
func parse() -> void:
if css_text.is_empty():
return
var cleaned_css = preprocess_css(css_text)
var rules = extract_rules(cleaned_css)
for rule_data in rules:
var rule = parse_rule(rule_data)
if rule:
stylesheet.add_rule(rule)
func preprocess_css(css: String) -> String:
# Remove comments
var regex = RegEx.new()
regex.compile("/\\*.*?\\*/")
css = regex.sub(css, "", true)
# Normalize whitespace
regex.compile("\\s+")
css = regex.sub(css, " ", true)
return css.strip_edges()
func extract_rules(css: String) -> Array:
var rules = []
var current_pos = 0
while current_pos < css.length():
var brace_start = css.find("{", current_pos)
if brace_start == -1:
break
var brace_end = find_matching_brace(css, brace_start)
if brace_end == -1:
break
var selector_part = css.substr(current_pos, brace_start - current_pos).strip_edges()
var properties_part = css.substr(brace_start + 1, brace_end - brace_start - 1).strip_edges()
# Handle multiple selectors separated by commas
var selectors = selector_part.split(",")
for selector in selectors:
rules.append({
"selector": selector.strip_edges(),
"properties": properties_part
})
current_pos = brace_end + 1
return rules
func find_matching_brace(css: String, start_pos: int) -> int:
var brace_count = 0
var pos = start_pos
while pos < css.length():
match css[pos]:
"{":
brace_count += 1
"}":
brace_count -= 1
if brace_count == 0:
return pos
pos += 1
return -1
func parse_rule(rule_data: Dictionary) -> CSSRule:
var rule = CSSRule.new()
rule.selector = rule_data.selector
rule.init(rule.selector)
var properties_text = rule_data.properties
var utility_classes = properties_text.split(" ")
for utility_name in utility_classes:
utility_name = utility_name.strip_edges()
if utility_name.is_empty():
continue
parse_utility_class(rule, utility_name)
return rule
func parse_utility_class(rule: CSSRule, utility_name: String) -> void:
var pseudo_classes = ["hover", "active"]
for pseudo in pseudo_classes:
var prefix = pseudo + ":"
if utility_name.begins_with(prefix):
var actual_utility = utility_name.substr(prefix.length())
var pseudo_rule = CSSRule.new()
pseudo_rule.selector = rule.selector + ":" + pseudo
pseudo_rule.init(pseudo_rule.selector)
parse_utility_class_internal(pseudo_rule, actual_utility)
stylesheet.add_rule(pseudo_rule)
return
# Fallback to normal parsing
parse_utility_class_internal(rule, utility_name)
# Parses a utility class (e.g. "text-red-500") and adds properties to the rule (e.g. "color: red")
# Used as a translation layer for Tailwind-like utility classes, as it becomes easier to manage these programmatically
static func parse_utility_class_internal(rule: CSSRule, utility_name: String) -> void:
# Handle color classes like text-[#ff0000]
if utility_name.begins_with("text-[") and utility_name.ends_with("]"):
var color_value = SizeUtils.extract_bracket_content(utility_name, 5) # after 'text-'
rule.properties["color"] = ColorUtils.parse_color(color_value)
return
# Handle standard text color classes like text-white, text-black, etc.
# But exclude text alignment classes
if utility_name.begins_with("text-") and not utility_name in ["text-left", "text-center", "text-right", "text-justify"]:
var color_name = utility_name.substr(5) # after 'text-'
var color = ColorUtils.get_color(color_name)
if color != null:
rule.properties["color"] = color
return
# Handle background color classes like bg-[#ff0000]
if utility_name.begins_with("bg-[") and utility_name.ends_with("]"):
var color_value = SizeUtils.extract_bracket_content(utility_name, 3) # after 'bg-'
var color = ColorUtils.parse_color(color_value)
rule.properties["background-color"] = color
return
# Handle standard background color classes like bg-white, bg-black, etc.
if utility_name.begins_with("bg-"):
var color_name = utility_name.substr(3) # after 'bg-'
var color = ColorUtils.get_color(color_name)
if color != null:
rule.properties["background-color"] = color
return
# e.g. max-w-[123px], w-[50%], h-[2rem]
if utility_name.match("^max-w-\\[.*\\]$"):
var val = SizeUtils.extract_bracket_content(utility_name, 6)
rule.properties["max-width"] = val
return
if utility_name.match("^max-h-\\[.*\\]$"):
var val = SizeUtils.extract_bracket_content(utility_name, 6)
rule.properties["max-height"] = val
return
if utility_name.match("^min-w-\\[.*\\]$"):
var val = SizeUtils.extract_bracket_content(utility_name, 6)
rule.properties["min-width"] = val
return
if utility_name.match("^min-h-\\[.*\\]$"):
var val = SizeUtils.extract_bracket_content(utility_name, 6)
rule.properties["min-height"] = val
return
if utility_name.match("^w-\\[.*\\]$"):
var val = SizeUtils.extract_bracket_content(utility_name, 2)
rule.properties["width"] = val
return
if utility_name.match("^h-\\[.*\\]$"):
var val = SizeUtils.extract_bracket_content(utility_name, 2)
rule.properties["height"] = val
return
# Handle font weight
if utility_name == "font-bold":
rule.properties["font-bold"] = true
return
# Handle font family
if utility_name == "font-sans":
rule.properties["font-family"] = "sans-serif"
return
if utility_name == "font-serif":
rule.properties["font-family"] = "serif"
return
if utility_name == "font-mono":
rule.properties["font-family"] = "monospace"
rule.properties["font-mono"] = true
return
var reserved_font_styles = ["font-sans", "font-serif", "font-mono", "font-bold", "font-italic"]
# Handle custom font families like font-roboto
if utility_name.begins_with("font-") and not utility_name in reserved_font_styles:
var font_name = utility_name.substr(5) # after 'font-'
rule.properties["font-family"] = font_name
return
# Handle font style italic
if utility_name == "font-italic":
rule.properties["font-italic"] = true
return
# Handle underline
if utility_name == "underline":
rule.properties["underline"] = true
return
# Handle text size classes
match utility_name:
"text-xs": rule.properties["font-size"] = 12
"text-sm": rule.properties["font-size"] = 14
"text-base": rule.properties["font-size"] = 16
"text-lg": rule.properties["font-size"] = 18
"text-xl": rule.properties["font-size"] = 20
"text-2xl": rule.properties["font-size"] = 24
"text-3xl": rule.properties["font-size"] = 30
"text-4xl": rule.properties["font-size"] = 36
"text-5xl": rule.properties["font-size"] = 48
"text-6xl": rule.properties["font-size"] = 60
# Handle text alignment classes
"text-left": rule.properties["text-align"] = "left"
"text-center": rule.properties["text-align"] = "center"
"text-right": rule.properties["text-align"] = "right"
"text-justify": rule.properties["text-align"] = "justify"
# Width
if utility_name.begins_with("w-"):
var val = utility_name.substr(2)
if val.begins_with("[") and val.ends_with("]"):
val = val.substr(1, val.length() - 2)
rule.properties["width"] = SizeUtils.parse_size(val)
return
# Height
if utility_name.begins_with("h-"):
var val = utility_name.substr(2)
if val.begins_with("[") and val.ends_with("]"):
val = val.substr(1, val.length() - 2)
rule.properties["height"] = SizeUtils.parse_size(val)
return
# Min width
if utility_name.begins_with("min-w-"):
var val = utility_name.substr(6)
rule.properties["min-width"] = SizeUtils.parse_size(val)
return
# Min height
if utility_name.begins_with("min-h-"):
var val = utility_name.substr(6)
rule.properties["min-height"] = SizeUtils.parse_size(val)
return
# Max width
if utility_name.begins_with("max-w-"):
var val = utility_name.substr(6)
rule.properties["max-width"] = SizeUtils.parse_size(val)
return
# Max height
if utility_name.begins_with("max-h-"):
var val = utility_name.substr(6)
rule.properties["max-height"] = SizeUtils.parse_size(val)
return
# Flex container
if utility_name == "flex":
rule.properties["display"] = "flex"
return
if utility_name == "inline-flex":
rule.properties["display"] = "inline-flex"
return
# Flex direction
match utility_name:
"flex-row": rule.properties["flex-direction"] = "row"; return
"flex-row-reverse": rule.properties["flex-direction"] = "row-reverse"; return
"flex-col": rule.properties["flex-direction"] = "column"; return
"flex-col-reverse": rule.properties["flex-direction"] = "column-reverse"; return
# Flex wrap
match utility_name:
"flex-nowrap": rule.properties["flex-wrap"] = "nowrap"; return
"flex-wrap": rule.properties["flex-wrap"] = "wrap"; return
"flex-wrap-reverse": rule.properties["flex-wrap"] = "wrap-reverse"; return
# Justify content
match utility_name:
"justify-start": rule.properties["justify-content"] = "flex-start"; return
"justify-end": rule.properties["justify-content"] = "flex-end"; return
"justify-center": rule.properties["justify-content"] = "center"; return
"justify-between": rule.properties["justify-content"] = "space-between"; return
"justify-around": rule.properties["justify-content"] = "space-around"; return
"justify-evenly": rule.properties["justify-content"] = "space-evenly"; return
# Align items
match utility_name:
"items-start": rule.properties["align-items"] = "flex-start"; return
"items-end": rule.properties["align-items"] = "flex-end"; return
"items-center": rule.properties["align-items"] = "center"; return
"items-baseline": rule.properties["align-items"] = "baseline"; return
"items-stretch": rule.properties["align-items"] = "stretch"; return
# Align content
match utility_name:
"content-start": rule.properties["align-content"] = "flex-start"; return
"content-end": rule.properties["align-content"] = "flex-end"; return
"content-center": rule.properties["align-content"] = "center"; return
"content-between": rule.properties["align-content"] = "space-between"; return
"content-around": rule.properties["align-content"] = "space-around"; return
"content-stretch": rule.properties["align-content"] = "stretch"; return
# Gap
if utility_name.begins_with("gap-"):
var val = utility_name.substr(4)
rule.properties["gap"] = SizeUtils.parse_size(val)
return
if utility_name.begins_with("row-gap-"):
var val = utility_name.substr(8)
rule.properties["row-gap"] = SizeUtils.parse_size(val)
return
if utility_name.begins_with("col-gap-"):
var val = utility_name.substr(8)
rule.properties["column-gap"] = SizeUtils.parse_size(val)
return
# FLEX ITEM PROPERTIES
if utility_name.begins_with("flex-grow-"):
var val = utility_name.substr(10)
rule.properties["flex-grow"] = val.to_float()
return
if utility_name.begins_with("flex-shrink-"):
var val = utility_name.substr(12)
rule.properties["flex-shrink"] = val.to_float()
return
if utility_name.begins_with("basis-"):
var val = utility_name.substr(6)
rule.properties["flex-basis"] = SizeUtils.parse_size(val)
return
# Align self
match utility_name:
"self-auto": rule.properties["align-self"] = "auto"; return
"self-start": rule.properties["align-self"] = "flex-start"; return
"self-end": rule.properties["align-self"] = "flex-end"; return
"self-center": rule.properties["align-self"] = "center"; return
"self-stretch": rule.properties["align-self"] = "stretch"; return
"self-baseline": rule.properties["align-self"] = "baseline"; return
# Order
if utility_name.begins_with("order-"):
var val = utility_name.substr(6)
rule.properties["order"] = val.to_int()
return
if utility_name == "rounded":
rule.properties["border-radius"] = "4px" # Default rounded
return
# Handle padding classes like p-8, px-4, py-2, etc.
if utility_name.begins_with("p-"):
var val = utility_name.substr(2)
var padding_value = SizeUtils.parse_size(val)
rule.properties["padding"] = padding_value
return
if utility_name.begins_with("px-"):
var val = utility_name.substr(3)
var padding_value = SizeUtils.parse_size(val)
rule.properties["padding-left"] = padding_value
rule.properties["padding-right"] = padding_value
return
if utility_name.begins_with("py-"):
var val = utility_name.substr(3)
var padding_value = SizeUtils.parse_size(val)
rule.properties["padding-top"] = padding_value
rule.properties["padding-bottom"] = padding_value
return
if utility_name.begins_with("pt-"):
var val = utility_name.substr(3)
var padding_value = SizeUtils.parse_size(val)
rule.properties["padding-top"] = padding_value
return
if utility_name.begins_with("pr-"):
var val = utility_name.substr(3)
var padding_value = SizeUtils.parse_size(val)
rule.properties["padding-right"] = padding_value
return
if utility_name.begins_with("pb-"):
var val = utility_name.substr(3)
var padding_value = SizeUtils.parse_size(val)
rule.properties["padding-bottom"] = padding_value
return
if utility_name.begins_with("pl-"):
var val = utility_name.substr(3)
var padding_value = SizeUtils.parse_size(val)
rule.properties["padding-left"] = padding_value
return
# Handle border radius classes like rounded, rounded-lg, rounded-[12px]
if utility_name == "rounded":
rule.properties["border-radius"] = "4px"
return
if utility_name == "rounded-none":
rule.properties["border-radius"] = "0px"
return
if utility_name == "rounded-sm":
rule.properties["border-radius"] = "2px"
return
if utility_name == "rounded-md":
rule.properties["border-radius"] = "6px"
return
if utility_name == "rounded-lg":
rule.properties["border-radius"] = "8px"
return
if utility_name == "rounded-xl":
rule.properties["border-radius"] = "12px"
return
if utility_name == "rounded-2xl":
rule.properties["border-radius"] = "16px"
return
if utility_name == "rounded-3xl":
rule.properties["border-radius"] = "24px"
return
if utility_name == "rounded-full":
rule.properties["border-radius"] = "9999px"
return
# Handle custom border radius like rounded-[12px]
if utility_name.begins_with("rounded-[") and utility_name.ends_with("]"):
var radius_value = SizeUtils.extract_bracket_content(utility_name, 8) # after 'rounded-'
rule.properties["border-radius"] = radius_value
return
# Handle numeric border radius classes like rounded-8, rounded-12, etc.
if utility_name.begins_with("rounded-"):
var val = utility_name.substr(8)
if val.is_valid_int():
rule.properties["border-radius"] = str(int(val)) + "px"
return
# Handle margin auto classes for centering
if utility_name == "mx-auto":
rule.properties["mx-auto"] = true
return
if utility_name == "my-auto":
rule.properties["my-auto"] = true
return
if utility_name == "m-auto":
rule.properties["mx-auto"] = true
rule.properties["my-auto"] = true
return
# Apply border properties
var apply_border = func(side: String, width: String = "", color = null, style: String = "solid"):
var prefix = "border" + ("-" + side if side != "" else "")
if width != "":
rule.properties[prefix + "-width"] = width
if color != null:
rule.properties[prefix + "-color"] = color
if style != "":
rule.properties[prefix + "-style"] = style
# Handle border utilities
if utility_name == "border":
apply_border.call("", "1px", Color.BLACK)
return
if utility_name == "border-none":
rule.properties["border-style"] = "none"
return
# Individual border sides - pattern: border-{side}-{value}
var border_sides = ["t", "r", "b", "l"]
var side_map = {"t": "top", "r": "right", "b": "bottom", "l": "left"}
for side in border_sides:
var short_side = side
var full_side = side_map[side]
# Basic side border (e.g., border-t)
if utility_name == "border-" + short_side:
apply_border.call(full_side, "1px")
return
# Side with value (e.g., border-t-2, border-t-red-500)
if utility_name.begins_with("border-" + short_side + "-"):
var val = utility_name.substr(9) # after "border-X-"
# Check for bracket notation first
if utility_name.begins_with("border-" + short_side + "-[") and utility_name.ends_with("]"):
var value = SizeUtils.extract_bracket_content(utility_name, 9)
if value.begins_with("#") or ColorUtils.parse_color(value) != null:
apply_border.call(full_side, "", ColorUtils.parse_color(value))
else:
apply_border.call(full_side, value)
return
# Check if it's a numeric width
if val.is_valid_int():
apply_border.call(full_side, str(int(val)) + "px")
return
# Check if it's a color
var color = ColorUtils.get_color(val)
if color != null:
apply_border.call(full_side, "", color)
return
# General border width (e.g., border-2)
if utility_name.begins_with("border-"):
var val = utility_name.substr(7)
# Custom border width like border-[2px]
if utility_name.begins_with("border-[") and utility_name.ends_with("]"):
var value = SizeUtils.extract_bracket_content(utility_name, 7)
if value.begins_with("#"):
apply_border.call("", "", ColorUtils.parse_color(value))
else:
apply_border.call("", value)
return
# Numeric width
if val.is_valid_int():
apply_border.call("", str(int(val)) + "px")
return
# Color name
var color = ColorUtils.get_color(val)
if color != null:
apply_border.call("", "", color)
return
# Handle cursor classes like cursor-pointer, cursor-default, cursor-text, etc.
if utility_name.begins_with("cursor-"):
var cursor_type = utility_name.substr(7) # after 'cursor-'
rule.properties["cursor"] = cursor_type
return
# Handle z-index classes like z-10, z-50, z-[999]
if utility_name.begins_with("z-"):
var val = utility_name.substr(2)
if val.begins_with("[") and val.ends_with("]"):
val = val.substr(1, val.length() - 2)
rule.properties["z-index"] = val.to_int()
return
# Handle opacity classes like opacity-50, opacity-75, opacity-[0.5]
if utility_name.begins_with("opacity-"):
var val = utility_name.substr(8) # after 'opacity-'
if val.begins_with("[") and val.ends_with("]"):
val = val.substr(1, val.length() - 2)
rule.properties["opacity"] = val.to_float()
elif val.is_valid_int():
# Convert percentage (0-100) to decimal (0.0-1.0)
rule.properties["opacity"] = val.to_int() / 100.0
return
# Handle more utility classes as needed
# Add more cases here for other utilities
static func parse_inline_style(style_string: String) -> Dictionary:
var rule = CSSRule.new()
rule.selector = ""
rule.init(rule.selector)
var utility_classes = style_string.split(" ")
for utility_name in utility_classes:
utility_name = utility_name.strip_edges()
if utility_name.is_empty():
continue
parse_utility_class_internal(rule, utility_name)
return rule.properties

View File

@@ -0,0 +1 @@
uid://cffcjsiwgyln

View File

@@ -0,0 +1,405 @@
class_name HTMLParser
extends Node
class HTMLElement:
var tag_name: String
var attributes: Dictionary = {}
var text_content: String = ""
var children: Array[HTMLElement] = []
var parent: HTMLElement = null
var is_self_closing: bool = false
func _init(tag: String = ""):
tag_name = tag
func get_attribute(name_: String, default: String = "") -> String:
return attributes.get(name_, default)
func has_attribute(name_: String) -> bool:
return attributes.has(name_)
func get_class_name() -> String:
return get_attribute("class")
func get_id() -> String:
return get_attribute("id")
func get_collapsed_text() -> String:
var collapsed = text_content.strip_edges()
# Replace multiple whitespace characters with single space
var regex = RegEx.new()
regex.compile("\\s+")
return regex.sub(collapsed, " ", true)
func get_preserved_text() -> String:
return text_content
func get_bbcode_formatted_text(parser: HTMLParser = null) -> String:
var styles = {}
if parser != null:
styles = parser.get_element_styles_with_inheritance(self, "", [])
return HTMLParser.get_bbcode_with_styles(self, styles, parser)
func is_inline_element() -> bool:
return tag_name in ["b", "i", "u", "small", "mark", "code", "span", "a", "input"]
class ParseResult:
var root: HTMLElement
var all_elements: Array[HTMLElement] = []
var errors: Array[String] = []
var css_parser: CSSParser = null
var inline_styles: Dictionary = {}
func _init():
root = HTMLElement.new("document")
# Properties
var xml_parser: XMLParser
var bitcode: PackedByteArray
var parse_result: ParseResult
func _init(data: PackedByteArray):
bitcode = data
xml_parser = XMLParser.new()
parse_result = ParseResult.new()
# Main parsing function
func parse() -> ParseResult:
xml_parser.open_buffer(bitcode)
var element_stack: Array[HTMLElement] = [parse_result.root]
while xml_parser.read() != ERR_FILE_EOF:
match xml_parser.get_node_type():
XMLParser.NODE_ELEMENT:
var element = create_element()
var current_parent = element_stack.back()
element.parent = current_parent
current_parent.children.append(element)
parse_result.all_elements.append(element)
if element.tag_name == "style":
handle_style_element(element)
if not element.is_self_closing:
element_stack.append(element)
XMLParser.NODE_ELEMENT_END:
if element_stack.size() > 1:
element_stack.pop_back()
XMLParser.NODE_TEXT:
var text = xml_parser.get_node_data().strip_edges()
if text.length() > 0 and element_stack.size() > 0:
element_stack.back().text_content += text
return parse_result
func handle_style_element(style_element: HTMLElement) -> void:
# Check if it's an external stylesheet
var src = style_element.get_attribute("src")
if src.length() > 0:
# TODO: Handle external CSS loading when Network module is available
print("External CSS not yet supported: " + src)
return
# Handle inline CSS - we'll get the text content when parsing is complete
# For now, create a parser that will be populated later
if not parse_result.css_parser:
parse_result.css_parser = CSSParser.new()
parse_result.css_parser.init()
func process_styles() -> void:
if not parse_result.css_parser:
return
# Collect all style element content
var css_content = Constants.DEFAULT_CSS
var style_elements = find_all("style")
for style_element in style_elements:
if style_element.get_attribute("src").is_empty():
css_content += style_element.text_content + "\n"
print("Processing CSS: ", css_content)
# Parse CSS if we have any
if css_content.length() > 0:
parse_result.css_parser.css_text = css_content
parse_result.css_parser.parse()
for child: CSSParser.CSSRule in parse_result.css_parser.stylesheet.rules:
print("INFO: for selector \"%s\" we have props: %s" % [child.selector, child.properties])
func get_element_styles_with_inheritance(element: HTMLElement, event: String = "", visited_elements: Array = []) -> Dictionary:
# Prevent infinite recursion
if element in visited_elements:
return {}
visited_elements.append(element)
var styles = {}
var class_names = get_css_class_names(element)
styles.merge(parse_result.css_parser.stylesheet.get_styles_for_element(element.tag_name, event, class_names, element))
# Apply inline styles (higher priority) - force override CSS rules
var inline_style = element.get_attribute("style")
if inline_style.length() > 0:
var inline_parsed = CSSParser.parse_inline_style(inline_style)
for property in inline_parsed:
styles[property] = inline_parsed[property]
# Inherit certain properties from parent elements
var inheritable_properties = ["width", "height", "font-size", "color", "font-family"]
var parent_element = element.parent
while parent_element:
var parent_styles = get_element_styles_internal(parent_element, event)
for property in inheritable_properties:
# Only inherit if child doesn't already have this property
if not styles.has(property) and parent_styles.has(property):
styles[property] = parent_styles[property]
parent_element = parent_element.parent
return styles
func get_element_styles_internal(element: HTMLElement, event: String = "") -> Dictionary:
var styles = {}
# Apply CSS rules
if parse_result.css_parser:
var class_names = get_css_class_names(element)
styles.merge(parse_result.css_parser.stylesheet.get_styles_for_element(element.tag_name, event, class_names, element))
# Apply inline styles (higher priority) - force override CSS rules
var inline_style = element.get_attribute("style")
if inline_style.length() > 0:
var inline_parsed = parse_inline_style_with_event(inline_style, event)
for property in inline_parsed:
styles[property] = inline_parsed[property] # Force override
return styles
func parse_inline_style_with_event(style_string: String, event: String = "") -> Dictionary:
var properties = {}
# Split style string into individual utility classes
var utility_classes = style_string.split(" ") # e.g. ["bg-red-500, "text-lg", "hover:bg-blue-500"]
for utility_name in utility_classes:
utility_name = utility_name.strip_edges() # e.g. "bg-red-500"
if utility_name.is_empty():
continue
# Check if this utility is for the requested event
if event.length() > 0:
if utility_name.begins_with(event + ":"): # e.g. "hover:bg-blue-500"
var actual_utility = utility_name.substr(event.length() + 1) # bg-blue-500
var rule = CSSParser.CSSRule.new()
CSSParser.parse_utility_class_internal(rule, actual_utility)
for property in rule.properties:
properties[property] = rule.properties[property]
else:
if not utility_name.contains(":"):
var rule = CSSParser.CSSRule.new()
CSSParser.parse_utility_class_internal(rule, utility_name)
for property in rule.properties:
properties[property] = rule.properties[property]
return properties
func get_css_class_names(element: HTMLElement) -> Array[String]:
var class_names: Array[String] = []
var class_attr = element.get_attribute("class")
if class_attr.length() > 0:
var classes = class_attr.split(" ")
for cls in classes:
cls = cls.strip_edges()
if cls.length() > 0:
class_names.append(cls)
return class_names
func extract_class_names_from_style(element: HTMLElement) -> Array[String]:
var class_names: Array[String] = []
var style_attr = element.get_attribute("style")
if style_attr.length() > 0:
var style_tokens = style_attr.split(" ")
for token in style_tokens:
token = token.strip_edges()
if token.length() > 0 and not UtilityClassValidator.is_utility_class(token):
class_names.append(token)
return class_names
# Creates element from CURRENT xml parser node
func create_element() -> HTMLElement:
var element = HTMLElement.new(xml_parser.get_node_name())
element.is_self_closing = xml_parser.is_empty()
# Parse attributes
for i in range(xml_parser.get_attribute_count()):
var attr_name = xml_parser.get_attribute_name(i)
var attr_value = xml_parser.get_attribute_value(i)
element.attributes[attr_name] = attr_value
return element
# Utility functions
func find_all(tag: String, attribute: String = "") -> Array[HTMLElement]:
if parse_result.all_elements.is_empty():
parse()
var results: Array[HTMLElement] = []
for element in parse_result.all_elements:
if element.tag_name == tag:
if attribute.is_empty() or element.has_attribute(attribute):
results.append(element)
return results
func find_all_by_class(tag: String, the_class_name: String) -> Array[HTMLElement]:
if parse_result.all_elements.is_empty():
parse()
var results: Array[HTMLElement] = []
for element in parse_result.all_elements:
if element.tag_name == tag and element.get_class_name() == the_class_name:
results.append(element)
return results
func find_by_id(element_id: String) -> HTMLElement:
if parse_result.all_elements.is_empty():
parse()
for element in parse_result.all_elements:
if element.get_id() == element_id:
return element
return null
func find_first(tag: String, attribute: String = "") -> HTMLElement:
var results = find_all(tag, attribute)
return results[0] if results.size() > 0 else null
# Extract attribute values
func get_attribute_values(tag: String, attribute: String) -> Array[String]:
var elements = find_all(tag, attribute)
var values: Array[String] = []
for element in elements:
var value = element.get_attribute(attribute)
if value.length() > 0:
values.append(value)
return values
func get_attribute_values_by_class(tag: String, the_class_name: String, attribute: String) -> Array[String]:
var elements = find_all_by_class(tag, the_class_name)
var values: Array[String] = []
for element in elements:
var value = element.get_attribute(attribute)
if value.length() > 0:
values.append(value)
return values
# Misc
func get_title() -> String:
var title_element = find_first("title")
return title_element.text_content if title_element != null else ""
func get_icon() -> String:
var icon_element = find_first("icon")
return icon_element.get_attribute("src") if icon_element != null else ""
func process_fonts() -> void:
var font_elements = find_all("font")
for font_element in font_elements:
var name = font_element.get_attribute("name")
var src = font_element.get_attribute("src")
var weight = font_element.get_attribute("weight", "400")
if name and src:
FontManager.register_font(name, src, weight)
func get_meta_content(name_: String) -> String:
var meta_elements = find_all("meta", "name")
for element in meta_elements:
if element.get_attribute("name") == name_:
return element.get_attribute("content")
return ""
func get_all_links() -> Array[String]:
return get_attribute_values("a", "href")
func get_all_images() -> Array[String]:
return get_attribute_values("img", "src")
func get_all_scripts() -> Array[String]:
return get_attribute_values("script", "src")
func get_all_stylesheets() -> Array[String]:
return get_attribute_values("style", "src")
func apply_element_styles(node: Control, element: HTMLElement, parser: HTMLParser) -> void:
var styles = parser.get_element_styles_with_inheritance(element, "", [])
if node.get("rich_text_label"):
var label = node.rich_text_label
var text = HTMLParser.get_bbcode_with_styles(element, styles, parser)
label.text = text
static func apply_element_bbcode_formatting(element: HTMLElement, styles: Dictionary, content: String) -> String:
match element.tag_name:
"b":
if styles.has("font-bold") and styles["font-bold"]:
return "[b]" + content + "[/b]"
"i":
if styles.has("font-italic") and styles["font-italic"]:
return "[i]" + content + "[/i]"
"u":
if styles.has("underline") and styles["underline"]:
return "[u]" + content + "[/u]"
"small":
if styles.has("font-size"):
return "[font_size=%d]%s[/font_size]" % [styles["font-size"], content]
else:
return "[font_size=20]%s[/font_size]" % content
"mark":
if styles.has("bg"):
var color = styles["bg"]
if typeof(color) == TYPE_COLOR:
color = color.to_html(false)
return "[bgcolor=#%s]%s[/bgcolor]" % [color, content]
else:
return "[bgcolor=#FFFF00]%s[/bgcolor]" % content
"code":
if styles.has("font-size"):
return "[font_size=%d][code]%s[/code][/font_size]" % [styles["font-size"], content]
else:
return "[font_size=20][code]%s[/code][/font_size]" % content
"a":
var href = element.get_attribute("href")
var color = "#1a0dab"
if styles.has("color"):
var c = styles["color"]
if typeof(c) == TYPE_COLOR:
color = "#" + c.to_html(false)
else:
color = str(c)
if href.length() > 0:
return "[color=%s][url=%s]%s[/url][/color]" % [color, href, content]
return content
static func get_bbcode_with_styles(element: HTMLElement, styles: Dictionary, parser: HTMLParser) -> String:
var text = ""
if element.text_content.length() > 0:
text += element.get_collapsed_text()
for child in element.children:
var child_styles = styles
if parser != null:
child_styles = parser.get_element_styles_with_inheritance(child, "", [])
var child_content = HTMLParser.get_bbcode_with_styles(child, child_styles, parser)
child_content = apply_element_bbcode_formatting(child, child_styles, child_content)
text += child_content
# Apply formatting to the current element itself
text = apply_element_bbcode_formatting(element, styles, text)
return text

View File

@@ -0,0 +1 @@
uid://bw7u24wgio8ij