CSS support (font-bold, font-italic, underline, text-[size], font-mono, text-[color], bg-[color]
This commit is contained in:
@@ -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")
|
||||
|
||||
255
Scripts/B9/CSSParser.gd
Normal file
255
Scripts/B9/CSSParser.gd
Normal file
@@ -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 {}
|
||||
1
Scripts/B9/CSSParser.gd.uid
Normal file
1
Scripts/B9/CSSParser.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cffcjsiwgyln
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <li> appears outside of <ul>/<ol>
|
||||
var label: RichTextLabel = $RichTextLabel
|
||||
label.text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text()
|
||||
label.text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text(parser)
|
||||
|
||||
@@ -2,7 +2,7 @@ extends VBoxContainer
|
||||
|
||||
const BROWSER_TEXT = preload("res://Scenes/Styles/BrowserText.tres")
|
||||
|
||||
func init(element: HTMLParser.HTMLElement) -> void:
|
||||
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
|
||||
var list_type = element.get_attribute("type").to_lower()
|
||||
if list_type == "": list_type = "decimal" # Default
|
||||
|
||||
@@ -16,7 +16,7 @@ func init(element: HTMLParser.HTMLElement) -> void:
|
||||
var index = 1
|
||||
for child_element in element.children:
|
||||
if child_element.tag_name == "li":
|
||||
var li_node = create_li_node(child_element, list_type, index, marker_min_width)
|
||||
var li_node = create_li_node(child_element, list_type, index, marker_min_width, parser)
|
||||
if li_node:
|
||||
add_child(li_node)
|
||||
index += 1
|
||||
@@ -41,7 +41,7 @@ func calculate_marker_width(list_type: String, max_index: int) -> float:
|
||||
|
||||
return max(width, 30) # Minimum pixels
|
||||
|
||||
func create_li_node(element: HTMLParser.HTMLElement, list_type: String, index: int, marker_width: float = 30) -> Control:
|
||||
func create_li_node(element: HTMLParser.HTMLElement, list_type: String, index: int, marker_width: float = 30, parser: HTMLParser = null) -> Control:
|
||||
var li_container = HBoxContainer.new()
|
||||
|
||||
# Create number/letter marker
|
||||
@@ -65,7 +65,7 @@ func create_li_node(element: HTMLParser.HTMLElement, list_type: String, index: i
|
||||
content_label.scroll_active = false
|
||||
content_label.theme = BROWSER_TEXT
|
||||
|
||||
content_label.text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text()
|
||||
content_label.text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text(parser)
|
||||
|
||||
li_container.add_theme_constant_override("separation", 0)
|
||||
li_container.add_child(marker_label)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
extends Control
|
||||
|
||||
func init(element: HTMLParser.HTMLElement) -> void:
|
||||
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
|
||||
# This is mainly for cases where <option> appears outside of <select>
|
||||
var label = RichTextLabel.new()
|
||||
label.bbcode_enabled = true
|
||||
label.fit_content = true
|
||||
label.scroll_active = false
|
||||
label.text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text()
|
||||
label.text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text(parser)
|
||||
add_child(label)
|
||||
|
||||
@@ -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]%s[/font_size]" % element.get_bbcode_formatted_text()
|
||||
label.text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text(parser)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
extends Control
|
||||
extends RichTextLabel
|
||||
|
||||
func init(element: HTMLParser.HTMLElement) -> void:
|
||||
var label: RichTextLabel = $RichTextLabel
|
||||
label.text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text()
|
||||
@onready var rich_text_label: RichTextLabel = self
|
||||
|
||||
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
|
||||
text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text(parser)
|
||||
|
||||
@@ -36,7 +36,6 @@ func init(element: HTMLParser.HTMLElement) -> void:
|
||||
|
||||
# Sync Control size with TextEdit
|
||||
custom_minimum_size = text_edit.custom_minimum_size
|
||||
size = text_edit.size
|
||||
|
||||
# Set readonly state
|
||||
if readonly.length() > 0:
|
||||
|
||||
@@ -2,7 +2,7 @@ extends VBoxContainer
|
||||
|
||||
const BROWSER_TEXT = preload("res://Scenes/Styles/BrowserText.tres")
|
||||
|
||||
func init(element: HTMLParser.HTMLElement) -> void:
|
||||
func init(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> void:
|
||||
var list_type = element.get_attribute("type").to_lower()
|
||||
if list_type == "": list_type = "disc" # Default
|
||||
|
||||
@@ -10,7 +10,7 @@ func init(element: HTMLParser.HTMLElement) -> void:
|
||||
|
||||
for child_element in element.children:
|
||||
if child_element.tag_name == "li":
|
||||
var li_node = create_li_node(child_element, list_type, marker_min_width)
|
||||
var li_node = create_li_node(child_element, list_type, marker_min_width, parser)
|
||||
if li_node:
|
||||
add_child(li_node)
|
||||
|
||||
@@ -34,7 +34,7 @@ func calculate_marker_width(list_type: String) -> float:
|
||||
|
||||
return max(width, 20) # Minimum pixels
|
||||
|
||||
func create_li_node(element: HTMLParser.HTMLElement, list_type: String, marker_width: float = 20) -> Control:
|
||||
func create_li_node(element: HTMLParser.HTMLElement, list_type: String, marker_width: float = 20, parser: HTMLParser = null) -> Control:
|
||||
var li_container = HBoxContainer.new()
|
||||
|
||||
# Create bullet point
|
||||
@@ -59,7 +59,7 @@ func create_li_node(element: HTMLParser.HTMLElement, list_type: String, marker_w
|
||||
content_label.fit_content = true
|
||||
content_label.theme = BROWSER_TEXT
|
||||
content_label.scroll_active = false
|
||||
content_label.text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text()
|
||||
content_label.text = "[font_size=24]%s[/font_size]" % element.get_bbcode_formatted_text(parser)
|
||||
|
||||
li_container.add_theme_constant_override("separation", 0)
|
||||
li_container.add_child(bullet_label)
|
||||
|
||||
168
Scripts/main.gd
168
Scripts/main.gd
@@ -46,7 +46,11 @@ func render():
|
||||
<meta name=\"theme-color\" content=\"#000000\">
|
||||
<meta name=\"description\" content=\"My cool web\">
|
||||
|
||||
<style href=\"styles.css\">
|
||||
<style>
|
||||
h1 { text-[#ff0000] font-italic hover:text-[#00ff00] }
|
||||
p { text-[#333333] text-2xl }
|
||||
</style>
|
||||
<style src=\"styles.css\">
|
||||
<script src=\"script.lua\" />
|
||||
</head>
|
||||
|
||||
@@ -206,6 +210,8 @@ line breaks
|
||||
var parser: HTMLParser = HTMLParser.new(html_bytes)
|
||||
var parse_result = parser.parse()
|
||||
|
||||
parser.process_styles()
|
||||
|
||||
print("Total elements found: " + str(parse_result.all_elements.size()))
|
||||
|
||||
if parse_result.errors.size() > 0:
|
||||
@@ -225,30 +231,27 @@ line breaks
|
||||
var element: HTMLParser.HTMLElement = body.children[i]
|
||||
|
||||
if element.is_inline_element():
|
||||
# Collect consecutive inline elements and flatten nested ones
|
||||
# Create an HBoxContainer for consecutive inline elements
|
||||
var inline_elements: Array[HTMLParser.HTMLElement] = []
|
||||
var has_hyperlink = false
|
||||
|
||||
while i < body.children.size() and body.children[i].is_inline_element():
|
||||
inline_elements.append(body.children[i])
|
||||
if contains_hyperlink(body.children[i]):
|
||||
has_hyperlink = true
|
||||
i += 1
|
||||
|
||||
var inline_container = P.instantiate()
|
||||
|
||||
var temp_parent = HTMLParser.HTMLElement.new()
|
||||
temp_parent.tag_name = "p"
|
||||
temp_parent.children = inline_elements
|
||||
inline_container.init(temp_parent)
|
||||
|
||||
website_container.add_child(inline_container)
|
||||
|
||||
if has_hyperlink:
|
||||
inline_container.rich_text_label.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
|
||||
|
||||
var hbox = HBoxContainer.new()
|
||||
hbox.add_theme_constant_override("separation", 4)
|
||||
|
||||
for inline_element in inline_elements:
|
||||
var inline_node = await create_element_node(inline_element, parser)
|
||||
if inline_node:
|
||||
hbox.add_child(inline_node)
|
||||
# Handle hyperlinks for all inline elements
|
||||
if contains_hyperlink(inline_element) and inline_node.rich_text_label:
|
||||
inline_node.rich_text_label.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
|
||||
website_container.add_child(hbox)
|
||||
continue
|
||||
|
||||
var element_node = await create_element_node(element)
|
||||
var element_node = await create_element_node(element, parser)
|
||||
if element_node:
|
||||
# ul/ol handle their own adding
|
||||
if element.tag_name != "ul" and element.tag_name != "ol":
|
||||
@@ -262,6 +265,86 @@ line breaks
|
||||
|
||||
i += 1
|
||||
|
||||
func apply_element_styles(node: Control, element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
|
||||
var styles = parser.get_element_styles(element)
|
||||
var label = node if node is RichTextLabel else node.get_node_or_null("RichTextLabel")
|
||||
|
||||
if label:
|
||||
apply_styles_to_label(label, styles, element, parser)
|
||||
|
||||
func apply_styles_to_label(label: RichTextLabel, styles: Dictionary, element: HTMLParser.HTMLElement, parser) -> void:
|
||||
var text = element.get_bbcode_formatted_text(parser) # pass parser
|
||||
var font_size = 24 # default
|
||||
print("applying styles to: ", text)
|
||||
print("applying styles to label: ", label.text, " | styles: ")
|
||||
for child in styles:
|
||||
print(child)
|
||||
|
||||
# Apply font size
|
||||
if styles.has("font-size"):
|
||||
font_size = int(styles["font-size"])
|
||||
|
||||
# Apply color
|
||||
var color_tag = ""
|
||||
if styles.has("color"):
|
||||
var color = styles["color"] as Color
|
||||
color_tag = "[color=#%s]" % color.to_html(false)
|
||||
|
||||
# Apply background color
|
||||
var bg_color_tag = ""
|
||||
var bg_color_close = ""
|
||||
if styles.has("background-color"):
|
||||
var bg_color = styles["background-color"] as Color
|
||||
bg_color_tag = "[bgcolor=#%s]" % bg_color.to_html(false)
|
||||
bg_color_close = "[/bgcolor]"
|
||||
|
||||
# Apply bold
|
||||
var bold_open = ""
|
||||
var bold_close = ""
|
||||
if styles.has("font-bold") and styles["font-bold"]:
|
||||
bold_open = "[b]"
|
||||
bold_close = "[/b]"
|
||||
|
||||
# Apply italic
|
||||
var italic_open = ""
|
||||
var italic_close = ""
|
||||
if styles.has("font-italic") and styles["font-italic"]:
|
||||
italic_open = "[i]"
|
||||
italic_close = "[/i]"
|
||||
# Apply underline
|
||||
var underline_open = ""
|
||||
var underline_close = ""
|
||||
if styles.has("underline") and styles["underline"]:
|
||||
underline_open = "[u]"
|
||||
underline_close = "[/u]"
|
||||
|
||||
# Apply monospace font
|
||||
var mono_open = ""
|
||||
var mono_close = ""
|
||||
if styles.has("font-mono") and styles["font-mono"]:
|
||||
mono_open = "[code]"
|
||||
mono_close = "[/code]"
|
||||
|
||||
# Construct final text
|
||||
var styled_text = "[font_size=%d]%s%s%s%s%s%s%s%s%s%s%s%s%s[/font_size]" % [
|
||||
font_size,
|
||||
bg_color_tag,
|
||||
color_tag,
|
||||
bold_open,
|
||||
italic_open,
|
||||
underline_open,
|
||||
mono_open,
|
||||
text,
|
||||
mono_close,
|
||||
underline_close,
|
||||
italic_close,
|
||||
bold_close,
|
||||
"[/color]" if color_tag.length() > 0 else "",
|
||||
bg_color_close
|
||||
]
|
||||
|
||||
label.text = styled_text
|
||||
|
||||
func contains_hyperlink(element: HTMLParser.HTMLElement) -> bool:
|
||||
if element.tag_name == "a":
|
||||
return true
|
||||
@@ -272,25 +355,35 @@ func contains_hyperlink(element: HTMLParser.HTMLElement) -> bool:
|
||||
|
||||
return false
|
||||
|
||||
func create_element_node(element: HTMLParser.HTMLElement) -> Control:
|
||||
func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser = null) -> Control:
|
||||
var node: Control = null
|
||||
|
||||
match element.tag_name:
|
||||
"p":
|
||||
node = P.instantiate()
|
||||
node.init(element)
|
||||
if parser:
|
||||
apply_element_styles(node, element, parser)
|
||||
"h1":
|
||||
node = H1.instantiate()
|
||||
node.init(element)
|
||||
if parser:
|
||||
apply_element_styles(node, element, parser)
|
||||
"h2":
|
||||
node = H2.instantiate()
|
||||
node.init(element)
|
||||
if parser:
|
||||
apply_element_styles(node, element, parser)
|
||||
"h3":
|
||||
node = H3.instantiate()
|
||||
node.init(element)
|
||||
if parser:
|
||||
apply_element_styles(node, element, parser)
|
||||
"h4":
|
||||
node = H4.instantiate()
|
||||
node.init(element)
|
||||
if parser:
|
||||
apply_element_styles(node, element, parser)
|
||||
"h5":
|
||||
node = H5.instantiate()
|
||||
node.init(element)
|
||||
@@ -326,6 +419,43 @@ func create_element_node(element: HTMLParser.HTMLElement) -> Control:
|
||||
"span":
|
||||
node = SPAN.instantiate()
|
||||
node.init(element)
|
||||
if parser:
|
||||
apply_element_styles(node, element, parser)
|
||||
"b":
|
||||
node = SPAN.instantiate()
|
||||
node.init(element)
|
||||
if parser:
|
||||
apply_element_styles(node, element, parser)
|
||||
"i":
|
||||
node = SPAN.instantiate()
|
||||
node.init(element)
|
||||
if parser:
|
||||
apply_element_styles(node, element, parser)
|
||||
"u":
|
||||
node = SPAN.instantiate()
|
||||
node.init(element)
|
||||
if parser:
|
||||
apply_element_styles(node, element, parser)
|
||||
"small":
|
||||
node = SPAN.instantiate()
|
||||
node.init(element)
|
||||
if parser:
|
||||
apply_element_styles(node, element, parser)
|
||||
"mark":
|
||||
node = SPAN.instantiate()
|
||||
node.init(element)
|
||||
if parser:
|
||||
apply_element_styles(node, element, parser)
|
||||
"code":
|
||||
node = SPAN.instantiate()
|
||||
node.init(element)
|
||||
if parser:
|
||||
apply_element_styles(node, element, parser)
|
||||
"a":
|
||||
node = SPAN.instantiate()
|
||||
node.init(element)
|
||||
if parser:
|
||||
apply_element_styles(node, element, parser)
|
||||
"ul":
|
||||
node = UL.instantiate()
|
||||
website_container.add_child(node) # Add to scene tree first
|
||||
|
||||
Reference in New Issue
Block a user