diff --git a/Scenes/Tags/span.tscn b/Scenes/Tags/span.tscn index 5973bf5..0e1ace1 100644 --- a/Scenes/Tags/span.tscn +++ b/Scenes/Tags/span.tscn @@ -3,15 +3,10 @@ [ext_resource type="Script" uid="uid://4pbphta3r67k" path="res://Scripts/Tags/span.gd" id="1_span"] [ext_resource type="Theme" uid="uid://bn6rbmdy60lhr" path="res://Scenes/Styles/BrowserText.tres" id="2_theme"] -[node name="SPAN" type="VBoxContainer"] +[node name="RichTextLabel" type="RichTextLabel"] anchors_preset = 10 anchor_right = 1.0 grow_horizontal = 2 -script = ExtResource("1_span") - -[node name="RichTextLabel" type="RichTextLabel" parent="."] -layout_mode = 2 -size_flags_horizontal = 3 focus_mode = 2 mouse_default_cursor_shape = 1 theme = ExtResource("2_theme") @@ -19,4 +14,7 @@ theme_override_colors/default_color = Color(0, 0, 0, 1) bbcode_enabled = true text = "Placeholder" fit_content = true +autowrap_mode = 0 +vertical_alignment = 2 selection_enabled = true +script = ExtResource("1_span") diff --git a/Scripts/B9/CSSParser.gd b/Scripts/B9/CSSParser.gd new file mode 100644 index 0000000..f3bb459 --- /dev/null +++ b/Scripts/B9/CSSParser.gd @@ -0,0 +1,255 @@ +class_name CSSParser +extends RefCounted + +class CSSRule: + var selector: String + var event_prefix: String = "" + var properties: Dictionary = {} + var specificity: int = 0 + + 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: + event_prefix = parts[0] + selector = parts[1] + + func calculate_specificity(): + specificity = 1 + 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 = "") -> Dictionary: + var styles = {} + + # Sort rules by specificity + var applicable_rules: Array[CSSRule] = [] + for rule in rules: + if selector_matches(rule, tag_name, event): + 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 = "") -> bool: + if rule.selector != tag_name: + return false + + if rule.event_prefix.length() > 0: + return rule.event_prefix == event + + return event.length() == 0 + +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: + # Handle color classes like text-[#ff0000] + if utility_name.begins_with("text-[") and utility_name.ends_with("]"): + var color_value = extract_bracket_content(utility_name, 5) # after 'text-' + rule.properties["color"] = parse_color(color_value) + return + + # Handle background color classes like bg-[#ff0000] + if utility_name.begins_with("bg-[") and utility_name.ends_with("]"): + var color_value = extract_bracket_content(utility_name, 3) # after 'bg-' + var color = parse_color(color_value) + rule.properties["background-color"] = color + return + + # Handle font weight + if utility_name == "font-bold": + rule.properties["font-bold"] = true + return + + # Handle font mono + if utility_name == "font-mono": + rule.properties["font-mono"] = true + 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 more utility classes as needed + # Add more cases here for other utilities + +# Helper to extract content inside first matching brackets after a given index +func extract_bracket_content(str: String, start_idx: int) -> String: + var open_idx = str.find("[", start_idx) + if open_idx == -1: + return "" + var close_idx = str.find("]", open_idx) + if close_idx == -1: + return "" + return str.substr(open_idx + 1, close_idx - open_idx - 1) + +func parse_color(color_string: String) -> Color: + print("DEBUG: parsing color: ", color_string) + color_string = color_string.strip_edges() + + # Handle hex colors + if color_string.begins_with("#"): + return Color.from_string(color_string, Color.WHITE) + + # Handle rgb/rgba + if color_string.begins_with("rgb"): + var regex = RegEx.new() + regex.compile("rgba?\\(([^)]+)\\)") + var result = regex.search(color_string) + if result: + var values = result.get_string(1).split(",") + if values.size() >= 3: + var r = values[0].strip_edges().to_float() / 255.0 + var g = values[1].strip_edges().to_float() / 255.0 + var b = values[2].strip_edges().to_float() / 255.0 + var a = 1.0 + if values.size() >= 4: + a = values[3].strip_edges().to_float() + return Color(r, g, b, a) + + # Handle named colors + # TODO: map to actual Tailwind colors + match color_string.to_lower(): + "red": return Color.RED + "green": return Color.GREEN + "blue": return Color.BLUE + "white": return Color.WHITE + "black": return Color.BLACK + "yellow": return Color.YELLOW + "cyan": return Color.CYAN + "magenta": return Color.MAGENTA + _: return Color.from_string(color_string, Color.WHITE) + +static func parse_inline_style(style_string: String) -> Dictionary: + var parser = CSSParser.new() + var rule_data = { + "selector": "", + "properties": style_string + } + var rule = parser.parse_rule(rule_data) + return rule.properties if rule else {} diff --git a/Scripts/B9/CSSParser.gd.uid b/Scripts/B9/CSSParser.gd.uid new file mode 100644 index 0000000..db3a2cf --- /dev/null +++ b/Scripts/B9/CSSParser.gd.uid @@ -0,0 +1 @@ +uid://cffcjsiwgyln diff --git a/Scripts/B9/HTMLParser.gd b/Scripts/B9/HTMLParser.gd index 4c6bf8a..9976687 100644 --- a/Scripts/B9/HTMLParser.gd +++ b/Scripts/B9/HTMLParser.gd @@ -34,49 +34,8 @@ class HTMLElement: func get_preserved_text() -> String: return text_content - func get_bbcode_formatted_text() -> String: - var result = "" - var has_previous_content = false - - if text_content.length() > 0: - result += get_collapsed_text() - has_previous_content = true - - for child in children: - var child_content = "" - match child.tag_name: - "b": - child_content = "[b]" + child.get_bbcode_formatted_text() + "[/b]" - "i": - child_content = "[i]" + child.get_bbcode_formatted_text() + "[/i]" - "u": - child_content = "[u]" + child.get_bbcode_formatted_text() + "[/u]" - "small": - child_content = "[font_size=20]" + child.get_bbcode_formatted_text() + "[/font_size]" - "mark": - child_content = "[bgcolor=#FFFF00]" + child.get_bbcode_formatted_text() + "[/bgcolor]" - "code": - child_content = "[font_size=20][code]" + child.get_bbcode_formatted_text() + "[/code][/font_size]" - "span": - child_content = child.get_bbcode_formatted_text() - "a": - var href = child.get_attribute("href") - if href.length() > 0: - child_content = "[color=#1a0dab][url=%s]%s[/url][/color]" % [href, child.get_bbcode_formatted_text()] - else: - child_content = child.get_bbcode_formatted_text() - _: - child_content = child.get_bbcode_formatted_text() - - if has_previous_content and child_content.length() > 0: - result += " " - - result += child_content - - if child_content.length() > 0: - has_previous_content = true - - return result + func get_bbcode_formatted_text(parser: HTMLParser = null) -> String: + return HTMLParser.get_bbcode_with_styles(self, {}, parser) # Pass empty dict for default func is_inline_element() -> bool: return tag_name in ["b", "i", "u", "small", "mark", "code", "span", "a", "input"] @@ -85,6 +44,8 @@ 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") @@ -94,6 +55,21 @@ var xml_parser: XMLParser var bitcode: PackedByteArray var parse_result: ParseResult +var DEFAULT_CSS := """ +h1 { text-5xl font-bold } +h2 { text-4xl font-bold } +h3 { text-3xl font-bold } +h4 { text-2xl font-bold } +h5 { text-xl font-bold } +b { font-bold } +i { font-italic } +u { underline } +small { text-xl } +mark { bg-[#FFFF00] } +code { text-xl font-mono } +a { text-[#1a0dab] } +""" + func _init(data: PackedByteArray): bitcode = data xml_parser = XMLParser.new() @@ -103,7 +79,7 @@ func _init(data: PackedByteArray): 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: @@ -113,6 +89,9 @@ func parse() -> ParseResult: 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) @@ -127,6 +106,53 @@ func parse() -> ParseResult: 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 = 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(element: HTMLElement, event: String = "") -> Dictionary: + var styles = {} + + # Apply CSS rules + if parse_result.css_parser: + styles.merge(parse_result.css_parser.stylesheet.get_styles_for_element(element.tag_name, event)) + + # Apply inline styles (higher priority) + var inline_style = element.get_attribute("style") + if inline_style.length() > 0: + var inline_parsed = CSSParser.parse_inline_style(inline_style) + styles.merge(inline_parsed) + + return styles + # Creates element from CURRENT xml parser node func create_element() -> HTMLElement: var element = HTMLElement.new(xml_parser.get_node_name()) @@ -228,3 +254,64 @@ func get_all_scripts() -> Array[String]: 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(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 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(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 + text += child_content + return text diff --git a/Scripts/Tags/button.gd b/Scripts/Tags/button.gd index 051ffd8..f3e22b3 100644 --- a/Scripts/Tags/button.gd +++ b/Scripts/Tags/button.gd @@ -1,11 +1,11 @@ extends Control -func init(element: HTMLParser.HTMLElement) -> void: +func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void: var button_node: Button = $ButtonNode var button_text = element.text_content.strip_edges() if button_text.length() == 0: - button_text = element.get_bbcode_formatted_text() + button_text = element.get_bbcode_formatted_text(parser) if button_text.length() > 0: button_node.text = button_text diff --git a/Scripts/Tags/h1.gd b/Scripts/Tags/h1.gd index e46813e..ca1fb43 100644 --- a/Scripts/Tags/h1.gd +++ b/Scripts/Tags/h1.gd @@ -2,6 +2,6 @@ extends Control @onready var rich_text_label: RichTextLabel = $RichTextLabel -func init(element: HTMLParser.HTMLElement) -> void: +func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void: var label: RichTextLabel = $RichTextLabel - label.text = "[font_size=48][b]%s[/b][/font_size]" % element.get_bbcode_formatted_text() + label.text = "[font_size=48][b]%s[/b][/font_size]" % element.get_bbcode_formatted_text(parser) diff --git a/Scripts/Tags/h2.gd b/Scripts/Tags/h2.gd index 02f3a68..d58d0c7 100644 --- a/Scripts/Tags/h2.gd +++ b/Scripts/Tags/h2.gd @@ -2,6 +2,6 @@ extends Control @onready var rich_text_label: RichTextLabel = $RichTextLabel -func init(element: HTMLParser.HTMLElement) -> void: +func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void: var label: RichTextLabel = $RichTextLabel - label.text = "[font_size=36][b]%s[/b][/font_size]" % element.get_bbcode_formatted_text() + label.text = "[font_size=36][b]%s[/b][/font_size]" % element.get_bbcode_formatted_text(parser) diff --git a/Scripts/Tags/h3.gd b/Scripts/Tags/h3.gd index b16fa8f..ad7e5f6 100644 --- a/Scripts/Tags/h3.gd +++ b/Scripts/Tags/h3.gd @@ -2,6 +2,6 @@ extends Control @onready var rich_text_label: RichTextLabel = $RichTextLabel -func init(element: HTMLParser.HTMLElement) -> void: +func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void: var label: RichTextLabel = $RichTextLabel - label.text = "[font_size=28][b]%s[/b][/font_size]" % element.get_bbcode_formatted_text() + label.text = "[font_size=28][b]%s[/b][/font_size]" % element.get_bbcode_formatted_text(parser) diff --git a/Scripts/Tags/h4.gd b/Scripts/Tags/h4.gd index ed415f4..6543470 100644 --- a/Scripts/Tags/h4.gd +++ b/Scripts/Tags/h4.gd @@ -2,6 +2,6 @@ extends Control @onready var rich_text_label: RichTextLabel = $RichTextLabel -func init(element: HTMLParser.HTMLElement) -> void: +func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void: var label: RichTextLabel = $RichTextLabel - label.text = "[font_size=24][b]%s[/b][/font_size]" % element.get_bbcode_formatted_text() + label.text = "[font_size=24][b]%s[/b][/font_size]" % element.get_bbcode_formatted_text(parser) diff --git a/Scripts/Tags/h5.gd b/Scripts/Tags/h5.gd index 19a53b7..be48d0d 100644 --- a/Scripts/Tags/h5.gd +++ b/Scripts/Tags/h5.gd @@ -2,6 +2,6 @@ extends Control @onready var rich_text_label: RichTextLabel = $RichTextLabel -func init(element: HTMLParser.HTMLElement) -> void: +func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void: var label: RichTextLabel = $RichTextLabel - label.text = "[font_size=20][b]%s[/b][/font_size]" % element.get_bbcode_formatted_text() + label.text = "[font_size=20][b]%s[/b][/font_size]" % element.get_bbcode_formatted_text(parser) diff --git a/Scripts/Tags/h6.gd b/Scripts/Tags/h6.gd index d05f716..deaecca 100644 --- a/Scripts/Tags/h6.gd +++ b/Scripts/Tags/h6.gd @@ -2,6 +2,6 @@ extends Control @onready var rich_text_label: RichTextLabel = $RichTextLabel -func init(element: HTMLParser.HTMLElement) -> void: +func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void: var label: RichTextLabel = $RichTextLabel - label.text = "[font_size=16][b]%s[/b][/font_size]" % element.get_bbcode_formatted_text() + label.text = "[font_size=16][b]%s[/b][/font_size]" % element.get_bbcode_formatted_text(parser) diff --git a/Scripts/Tags/li.gd b/Scripts/Tags/li.gd index 2c87493..16ce02a 100644 --- a/Scripts/Tags/li.gd +++ b/Scripts/Tags/li.gd @@ -1,6 +1,6 @@ extends Control -func init(element: HTMLParser.HTMLElement) -> void: +func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void: # This is mainly for cases where
  • appears outside of