DNS record management, CSS grid, Regex, location.query

This commit is contained in:
Face
2025-08-20 14:37:57 +03:00
parent 99f17dc42c
commit e8508bfe33
22 changed files with 1351 additions and 123 deletions

View File

@@ -584,6 +584,13 @@ static func parse_utility_class_internal(rule: CSSRule, utility_name: String) ->
if utility_name == "inline-flex":
rule.properties["display"] = "inline-flex"
return
if utility_name == "grid":
rule.properties["display"] = "grid"
return
if utility_name == "inline-grid":
rule.properties["display"] = "inline-grid"
return
# Flex direction
match utility_name:
@@ -638,6 +645,52 @@ static func parse_utility_class_internal(rule: CSSRule, utility_name: String) ->
rule.properties["column-gap"] = SizeUtils.parse_size(val)
return
if utility_name.begins_with("grid-cols-"):
var val = utility_name.substr(10)
if val.is_valid_int():
rule.properties["grid-template-columns"] = val.to_int()
else:
rule.properties["grid-template-columns"] = val
return
if utility_name.begins_with("grid-rows-"):
var val = utility_name.substr(10)
if val.is_valid_int():
rule.properties["grid-template-rows"] = val.to_int()
else:
rule.properties["grid-template-rows"] = val
return
if utility_name.begins_with("col-span-"):
var val = utility_name.substr(9)
if val == "full":
rule.properties["grid-column"] = "1 / -1"
elif val.is_valid_int():
rule.properties["grid-column"] = "span " + val
return
if utility_name.begins_with("row-span-"):
var val = utility_name.substr(9)
if val == "full":
rule.properties["grid-row"] = "1 / -1"
elif val.is_valid_int():
rule.properties["grid-row"] = "span " + val
return
match utility_name:
"grid-cols-1": rule.properties["grid-template-columns"] = 1; return
"grid-cols-2": rule.properties["grid-template-columns"] = 2; return
"grid-cols-3": rule.properties["grid-template-columns"] = 3; return
"grid-cols-4": rule.properties["grid-template-columns"] = 4; return
"grid-cols-5": rule.properties["grid-template-columns"] = 5; return
"grid-cols-6": rule.properties["grid-template-columns"] = 6; return
"grid-cols-12": rule.properties["grid-template-columns"] = 12; return
"col-span-1": rule.properties["grid-column"] = "span 1"; return
"col-span-2": rule.properties["grid-column"] = "span 2"; return
"col-span-3": rule.properties["grid-column"] = "span 3"; return
"col-span-4": rule.properties["grid-column"] = "span 4"; return
"col-span-5": rule.properties["grid-column"] = "span 5"; return
"col-span-6": rule.properties["grid-column"] = "span 6"; return
"col-span-full": rule.properties["grid-column"] = "1 / -1"; return
# FLEX ITEM PROPERTIES
if utility_name.begins_with("flex-grow-"):
var val = utility_name.substr(10)

View File

@@ -173,6 +173,76 @@ func _gurt_location_get_href_handler(vm: LuauVM) -> int:
vm.lua_pushstring("")
return 1
func _gurt_location_query_get_handler(vm: LuauVM) -> int:
var key: String = vm.luaL_checkstring(1)
var query_params = get_current_query_params()
if query_params.has(key):
vm.lua_pushstring(query_params[key])
else:
vm.lua_pushnil()
return 1
func _gurt_location_query_has_handler(vm: LuauVM) -> int:
var key: String = vm.luaL_checkstring(1)
var query_params = get_current_query_params()
vm.lua_pushboolean(query_params.has(key))
return 1
func _gurt_location_query_getAll_handler(vm: LuauVM) -> int:
var key: String = vm.luaL_checkstring(1)
var query_params = get_current_query_params()
vm.lua_newtable()
if query_params.has(key):
var value = query_params[key]
if value is Array:
for i in range(value.size()):
vm.lua_pushstring(str(value[i]))
vm.lua_rawseti(-2, i + 1)
else:
vm.lua_pushstring(str(value))
vm.lua_rawseti(-2, 1)
return 1
func get_current_query_params() -> Dictionary:
var main_node = Engine.get_main_loop().current_scene
var current_url = ""
if main_node and main_node.has_method("get_current_url"):
current_url = main_node.get_current_url()
elif main_node and main_node.has_property("current_domain"):
current_url = main_node.current_domain
var query_params = {}
if "?" in current_url:
var query_string = current_url.split("?")[1]
if "#" in query_string:
query_string = query_string.split("#")[0]
for param in query_string.split("&"):
if "=" in param:
var key_value = param.split("=", false, 1)
var key = key_value[0].uri_decode()
var value = key_value[1].uri_decode() if key_value.size() > 1 else ""
if query_params.has(key):
if query_params[key] is Array:
query_params[key].append(value)
else:
query_params[key] = [query_params[key], value]
else:
query_params[key] = value
else:
var key = param.uri_decode()
query_params[key] = ""
return query_params
func _reload_current_page():
var main_node = Engine.get_main_loop().current_scene
if main_node and main_node.has_method("reload_current_page"):
@@ -660,6 +730,12 @@ func _handle_text_setting(operation: Dictionary):
var element_id = get_or_assign_element_id(element)
var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null)
if dom_node:
if element.tag_name == "button":
var button_node = dom_node.get_node_or_null("ButtonNode")
if button_node and button_node is Button:
button_node.text = text
return
var text_node = get_dom_node(dom_node, "text")
if text_node:
if text_node is RichTextLabel:

View File

@@ -24,8 +24,11 @@ pre { text-xl font-mono }
button { text-[16px] bg-[#1b1b1b] rounded-md text-white hover:bg-[#2a2a2a] active:bg-[#101010] px-3 py-1.5 }
button[disabled] { bg-[#666666] text-[#999999] cursor-not-allowed }
input[type="text"] { text-[#000000] border border-[#000000] rounded-[3px] bg-transparent px-3 py-1.5 }
input[type="text"]:active { border-[3px] border-[#000000] }
input { text-[#000000] border border-[#000000] rounded-[3px] bg-transparent px-3 py-1.5 }
input:active { border-[3px] border-[#000000] }
select { text-[#000000] border border-[#000000] rounded-[3px] bg-transparent px-3 py-1.5 }
select:active { text-[#000000] border-[3px] border-[#000000] }
"""
var HTML_CONTENT = """

View File

@@ -197,16 +197,40 @@ func _on_text_changed(new_text: String, minlength: String, pattern: String) -> v
is_valid = false
if is_valid:
line_edit.remove_theme_stylebox_override("normal")
line_edit.remove_theme_stylebox_override("focus")
line_edit.modulate = Color.WHITE
if line_edit.has_focus():
apply_active_styles()
else:
apply_normal_styles()
else:
var normal_style = create_red_border_style_from_theme(line_edit, "normal")
var focus_style = create_red_border_style_from_theme(line_edit, "focus")
var normal_style = get_current_or_create_style(line_edit, "normal")
var focus_style = get_current_or_create_style(line_edit, "focus")
normal_style.border_color = Color.RED
normal_style.border_width_left = max(normal_style.border_width_left, 2)
normal_style.border_width_right = max(normal_style.border_width_right, 2)
normal_style.border_width_top = max(normal_style.border_width_top, 2)
normal_style.border_width_bottom = max(normal_style.border_width_bottom, 2)
focus_style.border_color = Color.RED
focus_style.border_width_left = max(focus_style.border_width_left, 2)
focus_style.border_width_right = max(focus_style.border_width_right, 2)
focus_style.border_width_top = max(focus_style.border_width_top, 2)
focus_style.border_width_bottom = max(focus_style.border_width_bottom, 2)
line_edit.add_theme_stylebox_override("normal", normal_style)
line_edit.add_theme_stylebox_override("focus", focus_style)
line_edit.modulate = Color.WHITE
func get_current_or_create_style(line_edit: LineEdit, style_name: String) -> StyleBoxFlat:
if line_edit.has_theme_stylebox_override(style_name):
var current_style = line_edit.get_theme_stylebox(style_name)
if current_style is StyleBoxFlat:
return (current_style as StyleBoxFlat).duplicate()
var theme_style = line_edit.get_theme_stylebox(style_name)
if theme_style is StyleBoxFlat:
return (theme_style as StyleBoxFlat).duplicate()
return StyleBoxFlat.new()
func create_red_border_style_from_theme(line_edit: LineEdit, style_name: String) -> StyleBoxFlat:
var original_style: StyleBoxFlat = line_edit.get_theme_stylebox(style_name)
@@ -393,7 +417,6 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
var styles = parser.get_element_styles_with_inheritance(element, "", [])
# Apply text color to the active input control
var active_child = null
for child in get_children():
@@ -422,21 +445,15 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
line_edit.add_theme_color_override("font_placeholder_color", placeholder_color)
# Apply stylebox for borders, background, padding, etc.
if BackgroundUtils.needs_background_wrapper(styles):
if BackgroundUtils.needs_background_wrapper(styles) or active_child is SpinBox:
apply_stylebox_to_input(active_child, styles)
var width = null
var height = null
if styles.has("width"):
if styles["width"] == "full":
var parent_styles = parser.get_element_styles_with_inheritance(element.parent, "", []) if element.parent else {}
if parent_styles.has("width"):
var parent_width = SizingUtils.parse_size_value(parent_styles["width"])
if parent_width:
width = parent_width
else:
width = SizingUtils.parse_size_value(styles["width"])
width = SizingUtils.parse_size_value(styles["width"])
if styles.has("height"):
height = SizingUtils.parse_size_value(styles["height"])
@@ -447,7 +464,11 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
var new_height = max(active_child.custom_minimum_size.y, active_child.size.y)
if width:
if SizingUtils.is_percentage(width):
if width == "100%":
active_child.size_flags_horizontal = Control.SIZE_EXPAND_FILL
size_flags_horizontal = Control.SIZE_EXPAND_FILL
new_width = 0
elif SizingUtils.is_percentage(width):
new_width = SizingUtils.calculate_percentage_size(width, SizingUtils.DEFAULT_VIEWPORT_WIDTH)
else:
new_width = width
@@ -462,7 +483,7 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
active_child.custom_minimum_size = new_child_size
if width:
if width and width != "100%":
active_child.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
if height:
active_child.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
@@ -473,7 +494,7 @@ func apply_input_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
custom_minimum_size = new_child_size
# Root Control adjusts size flags to match child
if width:
if width and width != "100%":
size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
else:
size_flags_horizontal = Control.SIZE_EXPAND_FILL
@@ -526,9 +547,10 @@ func apply_active_styles() -> void:
if not active_child:
return
# Apply merged styles
if BackgroundUtils.needs_background_wrapper(merged_styles):
apply_stylebox_to_input(active_child, merged_styles)
elif active_child is SpinBox:
apply_stylebox_to_input(active_child, merged_styles)
func apply_normal_styles() -> void:
if not _element or not _parser:
@@ -536,7 +558,6 @@ func apply_normal_styles() -> void:
var normal_styles = _parser.get_element_styles_with_inheritance(_element, "", [])
# Find the active input control
var active_child = null
for child in get_children():
if child.visible:
@@ -546,25 +567,27 @@ func apply_normal_styles() -> void:
if not active_child:
return
# Apply normal border styles
if BackgroundUtils.needs_background_wrapper(normal_styles):
apply_stylebox_to_input(active_child, normal_styles)
elif active_child is SpinBox:
apply_stylebox_to_input(active_child, normal_styles)
else:
# Remove style overrides to use default theme
if active_child is LineEdit:
active_child.remove_theme_stylebox_override("normal")
active_child.remove_theme_stylebox_override("focus")
elif active_child is SpinBox:
active_child.remove_theme_stylebox_override("normal")
active_child.remove_theme_stylebox_override("focus")
active_child.remove_theme_stylebox_override("updown")
var line_edit = active_child.get_line_edit()
if line_edit:
line_edit.remove_theme_stylebox_override("normal")
line_edit.remove_theme_stylebox_override("focus")
elif active_child is Button:
active_child.remove_theme_stylebox_override("normal")
func apply_stylebox_to_input(control: Control, styles: Dictionary) -> void:
var style_box = BackgroundUtils.create_stylebox_from_styles(styles)
# Set appropriate content margins for inputs if no padding is specified
# Check for all possible padding-related styles
var has_left_padding = styles.has("padding") or styles.has("padding-left")
var has_right_padding = styles.has("padding") or styles.has("padding-right")
var has_top_padding = styles.has("padding") or styles.has("padding-top")
@@ -580,13 +603,17 @@ func apply_stylebox_to_input(control: Control, styles: Dictionary) -> void:
if not has_bottom_padding:
style_box.content_margin_bottom = 2.0
# Apply the style to the appropriate states
if control is LineEdit:
control.add_theme_stylebox_override("normal", style_box)
control.add_theme_stylebox_override("focus", style_box)
elif control is SpinBox:
control.add_theme_stylebox_override("normal", style_box)
control.add_theme_stylebox_override("focus", style_box)
# NOTE: currently broken, it goes over the buttons, dont have time to fix
#style_box.expand_margin_right += 32.0 # More space for stepper buttons
var line_edit = control.get_line_edit()
if line_edit:
line_edit.add_theme_stylebox_override("normal", style_box)
line_edit.add_theme_stylebox_override("focus", style_box)
elif control is Button:
control.add_theme_stylebox_override("normal", style_box)

View File

@@ -57,7 +57,7 @@ func _get_font_weight_multiplier() -> float:
elif element_styles.has("font-extrabold"):
return 1.10
elif element_styles.has("font-bold"):
return 1.08
return 1.0
elif element_styles.has("font-semibold"):
return 1.06
elif element_styles.has("font-medium"):

View File

@@ -2,7 +2,12 @@ extends Control
const BROWSER_TEXT = preload("res://Scenes/Styles/BrowserText.tres")
var _element: HTMLParser.HTMLElement
var _parser: HTMLParser
func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
_element = element
_parser = parser
var option_button: OptionButton = $OptionButton
var selected_index = -1
@@ -36,4 +41,87 @@ func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
custom_minimum_size = option_button.size
apply_select_styles(element, parser)
parser.register_dom_node(element, option_button)
func apply_select_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void:
if not element or not parser:
return
StyleManager.apply_element_styles(self, element, parser)
var normal_styles = parser.get_element_styles_with_inheritance(element, "", [])
var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", [])
var active_styles = parser.get_element_styles_with_inheritance(element, "active", [])
var option_button: OptionButton = $OptionButton
apply_select_text_colors(option_button, normal_styles, hover_styles, active_styles)
apply_select_background_styles(option_button, normal_styles, hover_styles, active_styles)
if normal_styles.has("width"):
if normal_styles["width"] == "100%":
size_flags_horizontal = Control.SIZE_EXPAND_FILL
option_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
else:
var width = StyleManager.parse_size(normal_styles["width"])
if width:
custom_minimum_size.x = width
option_button.custom_minimum_size.x = width
size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
option_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
func apply_select_text_colors(option_button: OptionButton, normal_styles: Dictionary, hover_styles: Dictionary, active_styles: Dictionary) -> void:
var normal_color = normal_styles.get("color", Color.WHITE)
var hover_color = hover_styles.get("color", normal_color)
var active_color = active_styles.get("color", hover_color)
option_button.add_theme_color_override("font_color", normal_color)
option_button.add_theme_color_override("font_hover_color", hover_color)
option_button.add_theme_color_override("font_pressed_color", active_color)
option_button.add_theme_color_override("font_hover_pressed_color", active_color)
option_button.add_theme_color_override("font_focus_color", normal_color)
func apply_select_background_styles(option_button: OptionButton, normal_styles: Dictionary, hover_styles: Dictionary, active_styles: Dictionary) -> void:
var normal_merged = normal_styles.duplicate()
var hover_merged = normal_styles.duplicate()
var active_merged = normal_styles.duplicate()
for key in hover_styles:
hover_merged[key] = hover_styles[key]
for key in active_styles:
active_merged[key] = active_styles[key]
if BackgroundUtils.needs_background_wrapper(normal_merged):
var normal_stylebox = create_select_stylebox(normal_merged)
option_button.add_theme_stylebox_override("normal", normal_stylebox)
if BackgroundUtils.needs_background_wrapper(hover_merged):
var hover_stylebox = create_select_stylebox(hover_merged)
option_button.add_theme_stylebox_override("hover", hover_stylebox)
if BackgroundUtils.needs_background_wrapper(active_merged):
var active_stylebox = create_select_stylebox(active_merged)
option_button.add_theme_stylebox_override("pressed", active_stylebox)
func create_select_stylebox(styles: Dictionary) -> StyleBoxFlat:
var style_box = BackgroundUtils.create_stylebox_from_styles(styles)
var has_left_padding = styles.has("padding") or styles.has("padding-left")
var has_right_padding = styles.has("padding") or styles.has("padding-right")
var has_top_padding = styles.has("padding") or styles.has("padding-top")
var has_bottom_padding = styles.has("padding") or styles.has("padding-bottom")
if not has_left_padding:
style_box.content_margin_left = 5.0
if not has_right_padding:
style_box.content_margin_right = 20.0 # More space for dropdown arrow
if not has_top_padding:
style_box.content_margin_top = 2.0
if not has_bottom_padding:
style_box.content_margin_bottom = 2.0
return style_box

View File

@@ -0,0 +1,84 @@
class_name GridUtils
extends RefCounted
static func apply_grid_container_properties(node: GridContainer, styles: Dictionary) -> void:
if styles.has("grid-template-columns"):
var cols = styles["grid-template-columns"]
if cols is int:
node.columns = cols
elif cols is String:
var parts = cols.split(" ")
node.columns = parts.size()
if styles.has("gap"):
var gap_value = parse_grid_value(styles["gap"])
if gap_value is int or gap_value is float:
node.add_theme_constant_override("h_separation", int(gap_value))
node.add_theme_constant_override("v_separation", int(gap_value))
if styles.has("column-gap"):
var gap_value = parse_grid_value(styles["column-gap"])
if gap_value is int or gap_value is float:
node.add_theme_constant_override("h_separation", int(gap_value))
if styles.has("row-gap"):
var gap_value = parse_grid_value(styles["row-gap"])
if gap_value is int or gap_value is float:
node.add_theme_constant_override("v_separation", int(gap_value))
static func apply_grid_item_properties(node: Control, styles: Dictionary) -> void:
var grid_properties: Dictionary = node.get_meta("grid_properties", {})
var changed = false
if styles.has("grid-column"):
grid_properties["grid-column"] = styles["grid-column"]
changed = true
if styles["grid-column"].begins_with("span "):
var span_count = styles["grid-column"].substr(5).to_int()
if span_count > 1:
node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
node.set_meta("grid_column_span", span_count)
elif styles["grid-column"] == "1 / -1":
# Full span
node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
node.set_meta("grid_column_span", -1)
if styles.has("grid-row"):
grid_properties["grid-row"] = styles["grid-row"]
changed = true
if changed:
node.set_meta("grid_properties", grid_properties)
static func parse_grid_value(val):
if val is float or val is int:
return val
if val is String:
var s_val = val.strip_edges()
if s_val.is_valid_float():
return s_val.to_float()
if s_val.ends_with("px"):
return s_val.trim_suffix("px").to_float()
if s_val == "auto":
return "auto"
return null
static func get_grid_item_span(span_property: String) -> Dictionary:
var result = {"start": -1, "end": -1, "span": 1}
if span_property.begins_with("span "):
var span_count = span_property.substr(5).to_int()
result["span"] = max(1, span_count)
elif span_property == "1 / -1":
# Full span
result["span"] = -1
else:
var parts = span_property.split(" / ")
if parts.size() == 2:
result["start"] = parts[0].to_int()
result["end"] = parts[1].to_int()
return result

View File

@@ -0,0 +1 @@
uid://4p5lrhmdwgrj

View File

@@ -341,32 +341,72 @@ static func _find_input_control_with_file_info(node: Node) -> Node:
return null
static func _get_select_value(element: HTMLParser.HTMLElement, dom_node: Node) -> String:
if dom_node is OptionButton:
var option_button = dom_node as OptionButton
var selected_index = option_button.selected
if selected_index >= 0 and selected_index < option_button.get_item_count():
var metadata = option_button.get_item_metadata(selected_index)
if metadata:
return str(metadata)
else:
return option_button.get_item_text(selected_index)
return ""
static func _set_select_value(element: HTMLParser.HTMLElement, dom_node: Node, value: Variant) -> void:
if dom_node is OptionButton:
var option_button = dom_node as OptionButton
var target_value = str(value)
# Find the correct index to select
var selected_index = -1
# Find option with matching value
for i in range(option_button.get_item_count()):
var metadata = option_button.get_item_metadata(i)
var option_value = str(metadata) if metadata else option_button.get_item_text(i)
if option_value == target_value:
selected_index = i
break
# If no matching value found, try to find by text
if selected_index == -1:
for i in range(option_button.get_item_count()):
if option_button.get_item_text(i) == target_value:
selected_index = i
break
# Use call_deferred to set the property on main thread
if selected_index != -1:
option_button.call_deferred("set", "selected", selected_index)
static func _set_input_value(element: HTMLParser.HTMLElement, dom_node: Node, value: Variant) -> void:
var input_type = element.get_attribute("type").to_lower()
match input_type:
"checkbox", "radio":
if dom_node is CheckBox:
dom_node.button_pressed = bool(value)
dom_node.call_deferred("set", "button_pressed", bool(value))
"color":
if dom_node is ColorPickerButton:
var color_value = str(value)
if color_value.begins_with("#"):
dom_node.color = Color.from_string(color_value, Color.WHITE)
var color = Color.from_string(color_value, Color.WHITE)
dom_node.call_deferred("set", "color", color)
"range":
if dom_node is HSlider:
dom_node.value = float(value)
dom_node.call_deferred("set", "value", float(value))
"number":
if dom_node is SpinBox:
dom_node.value = float(value)
dom_node.call_deferred("set", "value", float(value))
"date":
if dom_node is DateButton and dom_node.has_method("set_date_from_string"):
dom_node.set_date_from_string(str(value))
dom_node.call_deferred("set_date_from_string", str(value))
_: # text, password, email, etc.
if dom_node is LineEdit:
dom_node.text = str(value)
dom_node.call_deferred("set", "text", str(value))
elif dom_node is TextEdit:
dom_node.text = str(value)
dom_node.call_deferred("set", "text", str(value))
# Helper functions
static func find_element_by_id(element_id: String, dom_parser: HTMLParser) -> HTMLParser.HTMLElement:
@@ -931,7 +971,7 @@ static func _element_index_wrapper(vm: LuauVM) -> int:
match key:
"value":
if lua_api and tag_name == "input":
if lua_api and (tag_name == "input" or tag_name == "select"):
vm.lua_getfield(1, "_element_id")
var element_id: String = vm.lua_tostring(-1)
vm.lua_pop(1)
@@ -940,8 +980,13 @@ static func _element_index_wrapper(vm: LuauVM) -> int:
var dom_node = lua_api.dom_parser.parse_result.dom_nodes.get(element_id, null)
if element and dom_node:
var input_value = _get_input_value(element, dom_node)
vm.lua_pushstring(str(input_value))
var value_result: String
if tag_name == "input":
value_result = str(_get_input_value(element, dom_node))
elif tag_name == "select":
value_result = _get_select_value(element, dom_node)
vm.lua_pushstring(value_result)
return 1
# Fallback to empty string
@@ -1225,7 +1270,7 @@ static func _element_newindex_wrapper(vm: LuauVM) -> int:
match key:
"value":
if tag_name == "input":
if tag_name == "input" or tag_name == "select":
vm.lua_getfield(1, "_element_id")
var element_id: String = vm.lua_tostring(-1)
vm.lua_pop(1)
@@ -1234,10 +1279,12 @@ static func _element_newindex_wrapper(vm: LuauVM) -> int:
var dom_node = lua_api.dom_parser.parse_result.dom_nodes.get(element_id, null)
if element and dom_node:
# Update the HTML element's value attribute
element.set_attribute("value", str(value))
_set_input_value(element, dom_node, value)
if tag_name == "input":
element.set_attribute("value", str(value))
_set_input_value(element, dom_node, value)
elif tag_name == "select":
element.set_attribute("value", str(value))
_set_select_value(element, dom_node, value)
return 0
"text":
var text: String = str(value) # Convert value to string

View File

@@ -25,7 +25,7 @@ static func connect_element_event(signal_node: Node, event_name: String, subscri
elif signal_node is Control:
var wrapper = func(event: InputEvent):
LuaAudioUtils.mark_user_event()
subscription.lua_api._on_gui_input_click(subscription, event)
subscription.lua_api._on_gui_input_click(event, subscription)
signal_node.gui_input.connect(wrapper)
subscription.connected_signal = "gui_input"
subscription.connected_node = signal_node

View File

@@ -0,0 +1,142 @@
class_name LuaRegexUtils
extends RefCounted
static func regex_new_handler(vm: LuauVM) -> int:
var pattern: String = vm.luaL_checkstring(1)
var regex = RegEx.new()
var result = regex.compile(pattern)
if result != OK:
vm.luaL_error("Invalid regex pattern: " + pattern)
return 0
vm.lua_newtable()
vm.lua_pushobject(regex)
vm.lua_setfield(-2, "_regex")
vm.lua_pushcallable(regex_match_handler, "regex:match")
vm.lua_setfield(-2, "match")
vm.lua_pushcallable(regex_test_handler, "regex:test")
vm.lua_setfield(-2, "test")
return 1
static func regex_match_handler(vm: LuauVM) -> int:
vm.luaL_checktype(1, vm.LUA_TTABLE)
var subject: String = vm.luaL_checkstring(2)
vm.lua_getfield(1, "_regex")
var regex: RegEx = vm.lua_toobject(-1) as RegEx
vm.lua_pop(1)
if not regex:
vm.luaL_error("Invalid regex object")
return 0
var result = regex.search(subject)
if not result:
vm.lua_pushnil()
return 1
vm.lua_newtable()
vm.lua_pushstring(result.get_string())
vm.lua_rawseti(-2, 1)
for i in range(1, result.get_group_count()):
var group = result.get_string(i)
vm.lua_pushstring(group)
vm.lua_rawseti(-2, i + 1)
return 1
static func regex_test_handler(vm: LuauVM) -> int:
vm.luaL_checktype(1, vm.LUA_TTABLE)
var subject: String = vm.luaL_checkstring(2)
vm.lua_getfield(1, "_regex")
var regex: RegEx = vm.lua_toobject(-1) as RegEx
vm.lua_pop(1)
if not regex:
vm.luaL_error("Invalid regex object")
return 0
var result = regex.search(subject)
vm.lua_pushboolean(result != null)
return 1
static func string_replace_handler(vm: LuauVM) -> int:
var subject: String = vm.luaL_checkstring(1)
if vm.lua_istable(2):
vm.lua_getfield(2, "_regex")
var regex: RegEx = vm.lua_toobject(-1) as RegEx
vm.lua_pop(1)
if not regex:
vm.luaL_error("Invalid regex object")
return 0
var replacement: String = vm.luaL_checkstring(3)
var result = regex.sub(subject, replacement, false)
vm.lua_pushstring(result)
else:
var search: String = vm.luaL_checkstring(2)
var replacement: String = vm.luaL_checkstring(3)
var pos = subject.find(search)
if pos >= 0:
var result = subject.substr(0, pos) + replacement + subject.substr(pos + search.length())
vm.lua_pushstring(result)
else:
vm.lua_pushstring(subject)
return 1
static func string_replace_all_handler(vm: LuauVM) -> int:
var subject: String = vm.luaL_checkstring(1)
if vm.lua_istable(2):
vm.lua_getfield(2, "_regex")
var regex: RegEx = vm.lua_toobject(-1) as RegEx
vm.lua_pop(1)
if not regex:
vm.luaL_error("Invalid regex object")
return 0
var replacement: String = vm.luaL_checkstring(3)
var result = regex.sub(subject, replacement, true)
vm.lua_pushstring(result)
else:
var search: String = vm.luaL_checkstring(2)
var replacement: String = vm.luaL_checkstring(3)
var result = subject.replace(search, replacement)
vm.lua_pushstring(result)
return 1
static func setup_regex_api(vm: LuauVM) -> void:
vm.lua_newtable()
vm.lua_pushcallable(regex_new_handler, "Regex.new")
vm.lua_setfield(-2, "new")
vm.lua_setglobal("Regex")
vm.lua_getglobal("string")
if vm.lua_isnil(-1):
vm.lua_pop(1)
vm.lua_newtable()
vm.lua_setglobal("string")
vm.lua_getglobal("string")
vm.lua_pushcallable(string_replace_handler, "string.replace")
vm.lua_setfield(-2, "replace")
vm.lua_pushcallable(string_replace_all_handler, "string.replaceAll")
vm.lua_setfield(-2, "replaceAll")
vm.lua_pop(1)

View File

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

View File

@@ -315,6 +315,15 @@ func _setup_threaded_gurt_api():
lua_vm.lua_pushstring(current_href)
lua_vm.lua_setfield(-2, "href")
lua_vm.lua_newtable()
lua_vm.lua_pushcallable(lua_api._gurt_location_query_get_handler, "gurt.location.query.get")
lua_vm.lua_setfield(-2, "get")
lua_vm.lua_pushcallable(lua_api._gurt_location_query_has_handler, "gurt.location.query.has")
lua_vm.lua_setfield(-2, "has")
lua_vm.lua_pushcallable(lua_api._gurt_location_query_getAll_handler, "gurt.location.query.getAll")
lua_vm.lua_setfield(-2, "getAll")
lua_vm.lua_setfield(-2, "query")
lua_vm.lua_setfield(-2, "location")
var body_element = dom_parser.find_first("body")
@@ -345,6 +354,7 @@ func _setup_additional_lua_apis():
LuaWebSocketUtils.setup_websocket_api(lua_vm)
LuaAudioUtils.setup_audio_api(lua_vm)
LuaCrumbsUtils.setup_crumbs_api(lua_vm)
LuaRegexUtils.setup_regex_api(lua_vm)
func _table_tostring_handler(vm: LuauVM) -> int:
vm.luaL_checktype(1, vm.LUA_TTABLE)

View File

@@ -27,7 +27,6 @@ const BUTTON = preload("res://Scenes/Tags/button.tscn")
const UL = preload("res://Scenes/Tags/ul.tscn")
const OL = preload("res://Scenes/Tags/ol.tscn")
const SELECT = preload("res://Scenes/Tags/select.tscn")
const OPTION = preload("res://Scenes/Tags/option.tscn")
const TEXTAREA = preload("res://Scenes/Tags/textarea.tscn")
const DIV = preload("res://Scenes/Tags/div.tscn")
const AUDIO = preload("res://Scenes/Tags/audio.tscn")
@@ -53,6 +52,8 @@ func _ready():
ProjectSettings.set_setting("display/window/size/min_width", MIN_SIZE.x)
ProjectSettings.set_setting("display/window/size/min_height", MIN_SIZE.y)
DisplayServer.window_set_min_size(MIN_SIZE)
call_deferred("render")
var current_domain = "" # Store current domain for display
@@ -277,13 +278,14 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
var styles = parser.get_element_styles_with_inheritance(element, "", [])
var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", [])
var is_flex_container = styles.has("display") and ("flex" in styles["display"])
var is_grid_container = styles.has("display") and ("grid" in styles["display"])
var final_node: Control
var container_for_children: Node
# If this is an inline element AND not a flex container, do NOT recursively add child nodes for its children.
# If this is an inline element AND not a flex or grid container, do NOT recursively add child nodes for its children.
# Only create a node for the outermost inline group; nested inline tags are handled by BBCode.
if element.is_inline_element() and not is_flex_container:
if element.is_inline_element() and not is_flex_container and not is_grid_container:
final_node = await create_element_node_internal(element, parser)
if not final_node:
return null
@@ -292,7 +294,24 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
FlexUtils.apply_flex_item_properties(final_node, styles)
return final_node
if is_flex_container:
if is_grid_container:
if element.tag_name == "div":
if BackgroundUtils.needs_background_wrapper(styles) or hover_styles.size() > 0:
final_node = BackgroundUtils.create_panel_container_with_background(styles, hover_styles)
var grid_container = GridContainer.new()
grid_container.name = "Grid_" + element.tag_name
var vbox = final_node.get_child(0) as VBoxContainer
vbox.add_child(grid_container)
container_for_children = grid_container
else:
final_node = GridContainer.new()
final_node.name = "Grid_" + element.tag_name
container_for_children = final_node
else:
final_node = GridContainer.new()
final_node.name = "Grid_" + element.tag_name
container_for_children = final_node
elif is_flex_container:
# The element's primary identity IS a flex container.
if element.tag_name == "div":
if BackgroundUtils.needs_background_wrapper(styles) or hover_styles.size() > 0:
@@ -369,8 +388,29 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
if flex_container_node is FlexContainer:
FlexUtils.apply_flex_container_properties(flex_container_node, styles)
# Apply flex ITEM properties
if is_grid_container:
var grid_container_node = final_node
if final_node is GridContainer:
grid_container_node = final_node
elif final_node is MarginContainer and final_node.get_child_count() > 0:
var first_child = final_node.get_child(0)
if first_child is GridContainer:
grid_container_node = first_child
elif final_node is PanelContainer and final_node.get_child_count() > 0:
var vbox = final_node.get_child(0)
if vbox is VBoxContainer and vbox.get_child_count() > 0:
var potential_grid = vbox.get_child(0)
if potential_grid is GridContainer:
grid_container_node = potential_grid
if grid_container_node is GridContainer:
GridUtils.apply_grid_container_properties(grid_container_node, styles)
FlexUtils.apply_flex_item_properties(final_node, styles)
if not is_grid_container:
GridUtils.apply_grid_item_properties(final_node, styles)
# Skip ul/ol and non-flex forms, they handle their own children
var skip_general_processing = false
@@ -378,13 +418,13 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
if element.tag_name == "ul" or element.tag_name == "ol":
skip_general_processing = true
elif element.tag_name == "form":
skip_general_processing = not is_flex_container
skip_general_processing = not is_flex_container and not is_grid_container
if not skip_general_processing:
for child_element in element.children:
# Only add child nodes if the child is NOT an inline element
# UNLESS the parent is a flex container (inline elements become flex items)
if not child_element.is_inline_element() or is_flex_container:
# UNLESS the parent is a flex or grid container (inline elements become flex/grid items)
if not child_element.is_inline_element() or is_flex_container or is_grid_container:
var child_node = await create_element_node(child_element, parser)
if child_node and is_instance_valid(container_for_children):
# Input elements register their own DOM nodes in their init() function
@@ -469,9 +509,6 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
"select":
node = SELECT.instantiate()
node.init(element, parser)
"option":
node = OPTION.instantiate()
node.init(element, parser)
"textarea":
node = TEXTAREA.instantiate()
node.init(element, parser)
@@ -482,9 +519,10 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
var styles = parser.get_element_styles_with_inheritance(element, "", [])
var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", [])
var is_flex_container = styles.has("display") and ("flex" in styles["display"])
var is_grid_container = styles.has("display") and ("grid" in styles["display"])
# For flex divs, let the general flex container logic handle them
if is_flex_container:
# For flex or grid divs, let the general flex/grid container logic handle them
if is_flex_container or is_grid_container:
return null
# Create div container