CSS selectors, input styling (border/bg), fix <a> styling removing link

This commit is contained in:
Face
2025-08-01 23:08:30 +03:00
parent aa4ee1b93c
commit abe03d0f46
5 changed files with 437 additions and 81 deletions

View File

@@ -6,6 +6,8 @@ class CSSRule:
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
@@ -18,11 +20,52 @@ class CSSRule:
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 # Class selectors have higher specificity than tag selectors
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
@@ -32,13 +75,13 @@ class CSSStylesheet:
func add_rule(rule: CSSRule):
rules.append(rule)
func get_styles_for_element(tag_name: String, event: String = "", class_names: Array[String] = []) -> Dictionary:
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):
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)
@@ -50,21 +93,175 @@ class CSSStylesheet:
return styles
func selector_matches(rule: CSSRule, tag_name: String, event: String = "", cls_names: Array[String] = []) -> bool:
# Handle class selectors
if rule.selector.begins_with("."):
var cls = rule.selector.substr(1) # Remove the "." prefix
if not cls in cls_names:
return false
else:
# Handle tag selectors
if rule.selector != tag_name:
return false
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:
return rule.event_prefix == event
if rule.event_prefix != event:
return false
elif event.length() > 0:
return false
return event.length() == 0
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

View File

@@ -35,7 +35,10 @@ class HTMLElement:
return text_content
func get_bbcode_formatted_text(parser: HTMLParser = null) -> String:
return HTMLParser.get_bbcode_with_styles(self, {}, parser) # Pass empty dict for default
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"]
@@ -132,8 +135,8 @@ func get_element_styles_with_inheritance(element: HTMLElement, event: String = "
var styles = {}
var class_names = extract_class_names_from_style(element)
styles.merge(parse_result.css_parser.stylesheet.get_styles_for_element(element.tag_name, event, class_names))
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:
@@ -159,8 +162,8 @@ func get_element_styles_internal(element: HTMLElement, event: String = "") -> Di
# Apply CSS rules
if parse_result.css_parser:
var class_names = extract_class_names_from_style(element)
styles.merge(parse_result.css_parser.stylesheet.get_styles_for_element(element.tag_name, event, class_names))
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")
@@ -199,6 +202,17 @@ func parse_inline_style_with_event(style_string: String, event: String = "") ->
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")
@@ -291,7 +305,7 @@ func get_title() -> String:
func get_icon() -> String:
var icon_element = find_first("icon")
return icon_element.get_attribute("src")
return icon_element.get_attribute("src") if icon_element != null else ""
func process_fonts() -> void:
var font_elements = find_all("font")
@@ -330,6 +344,48 @@ func apply_element_styles(node: Control, element: HTMLElement, parser: HTMLParse
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:
@@ -340,46 +396,10 @@ static func get_bbcode_with_styles(element: HTMLElement, styles: Dictionary, par
if parser != null:
child_styles = parser.get_element_styles_with_inheritance(child, "", [])
var child_content = HTMLParser.get_bbcode_with_styles(child, child_styles, parser)
match child.tag_name:
"b":
if child_styles.has("font-bold") and child_styles["font-bold"]:
child_content = "[b]" + child_content + "[/b]"
"i":
if child_styles.has("font-italic") and child_styles["font-italic"]:
child_content = "[i]" + child_content + "[/i]"
"u":
if child_styles.has("underline") and child_styles["underline"]:
child_content = "[u]" + child_content + "[/u]"
"small":
if child_styles.has("font-size"):
child_content = "[font_size=%d]%s[/font_size]" % [child_styles["font-size"], child_content]
else:
child_content = "[font_size=20]%s[/font_size]" % child_content
"mark":
if child_styles.has("bg"):
var color = child_styles["bg"]
if typeof(color) == TYPE_COLOR:
color = color.to_html(false)
child_content = "[bgcolor=#%s]%s[/bgcolor]" % [color, child_content]
else:
child_content = "[bgcolor=#FFFF00]%s[/bgcolor]" % child_content
"code":
if child_styles.has("font-size"):
child_content = "[font_size=%d][code]%s[/code][/font_size]" % [child_styles["font-size"], child_content]
else:
child_content = "[font_size=20][code]%s[/code][/font_size]" % child_content
"a":
var href = child.get_attribute("href")
var color = "#1a0dab"
if child_styles.has("color"):
var c = child_styles["color"]
if typeof(c) == TYPE_COLOR:
color = "#" + c.to_html(false)
else:
color = str(c)
if href.length() > 0:
child_content = "[color=%s][url=%s]%s[/url][/color]" % [color, href, child_content]
_:
pass
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