From 94f6914539f98975cb4f00d9d95a54ac6537ed42 Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Wed, 6 Aug 2025 16:03:09 +0300 Subject: [PATCH] DOM manipulation (insertBefore, insertAfter, replace, clone) (parent, nextSibling, previousSibling, firstChild, lastChild) --- flumi/Scripts/B9/Lua.gd | 43 ++- flumi/Scripts/Constants.gd | 73 +++++ flumi/Scripts/Network.gd | 3 + flumi/Scripts/Utils/Lua/DOM.gd | 494 +++++++++++++++++++++++++++++ flumi/Scripts/Utils/Lua/DOM.gd.uid | 1 + 5 files changed, 607 insertions(+), 7 deletions(-) create mode 100644 flumi/Scripts/Utils/Lua/DOM.gd create mode 100644 flumi/Scripts/Utils/Lua/DOM.gd.uid diff --git a/flumi/Scripts/B9/Lua.gd b/flumi/Scripts/B9/Lua.gd index 66803c7..b524032 100644 --- a/flumi/Scripts/B9/Lua.gd +++ b/flumi/Scripts/B9/Lua.gd @@ -138,21 +138,21 @@ func add_element_methods(vm: LuauVM, index: String = "element") -> void: vm.lua_pushcallable(_element_set_attribute_handler, index + ".setAttribute") vm.lua_setfield(-2, "setAttribute") - # Create metatable for property access - vm.lua_newtable() # metatable + LuaDOMUtils.add_enhanced_element_methods(vm, self, index) - # __index method for property getters - vm.lua_pushcallable(_element_index_handler, index + ".__index") + vm.lua_newtable() + + vm.lua_pushcallable(_index_handler, index + ".__index") vm.lua_setfield(-2, "__index") - # __newindex method for property setters vm.lua_pushcallable(_element_newindex_handler, index + ".__newindex") vm.lua_setfield(-2, "__newindex") - # Set metatable on element table vm.lua_setmetatable(-2) -# Property access handlers +func _index_handler(vm: LuauVM) -> int: + return LuaDOMUtils._index_handler(vm, self) + func _element_index_handler(vm: LuauVM) -> int: vm.luaL_checktype(1, vm.LUA_TTABLE) var key: String = vm.luaL_checkstring(2) @@ -406,6 +406,35 @@ func _element_classlist_remove_wrapper(vm: LuauVM) -> int: func _element_classlist_toggle_wrapper(vm: LuauVM) -> int: return LuaClassListUtils.element_classlist_toggle_handler(vm, dom_parser) +# DOM manipulation wrapper functions +func _element_insert_before_wrapper(vm: LuauVM) -> int: + return LuaDOMUtils.insert_before_handler(vm, dom_parser, self) + +func _element_insert_after_wrapper(vm: LuauVM) -> int: + return LuaDOMUtils.insert_after_handler(vm, dom_parser, self) + +func _element_replace_wrapper(vm: LuauVM) -> int: + return LuaDOMUtils.replace_handler(vm, dom_parser, self) + +func _element_clone_wrapper(vm: LuauVM) -> int: + return LuaDOMUtils.clone_handler(vm, dom_parser, self) + +# DOM traversal property wrapper functions +func _get_element_parent_wrapper(vm: LuauVM, lua_api: LuaAPI) -> int: + return LuaDOMUtils.get_element_parent_handler(vm, dom_parser, lua_api) + +func _get_element_next_sibling_wrapper(vm: LuauVM, lua_api: LuaAPI) -> int: + return LuaDOMUtils.get_element_next_sibling_handler(vm, dom_parser, lua_api) + +func _get_element_previous_sibling_wrapper(vm: LuauVM, lua_api: LuaAPI) -> int: + return LuaDOMUtils.get_element_previous_sibling_handler(vm, dom_parser, lua_api) + +func _get_element_first_child_wrapper(vm: LuauVM, lua_api: LuaAPI) -> int: + return LuaDOMUtils.get_element_first_child_handler(vm, dom_parser, lua_api) + +func _get_element_last_child_wrapper(vm: LuauVM, lua_api: LuaAPI) -> int: + return LuaDOMUtils.get_element_last_child_handler(vm, dom_parser, lua_api) + func _render_new_element(element: HTMLParser.HTMLElement, parent_node: Node) -> void: # Get reference to main scene for rendering var main_scene = get_node("/root/Main") diff --git a/flumi/Scripts/Constants.gd b/flumi/Scripts/Constants.gd index 52d1798..8484032 100644 --- a/flumi/Scripts/Constants.gd +++ b/flumi/Scripts/Constants.gd @@ -797,6 +797,79 @@ var HTML_CONTENT_ADD_REMOVE = """ """.to_utf8_buffer() +var HTML_CONTENT_DOM_MANIPULATION = """ + + DOM Utilities Test + + + + +

DOM Utilities Demo

+
+
Non-interactible
+
Child 1
+
Child 2
+
Child 3
+
+
+ + + + +
+

Test

+ +""".to_utf8_buffer() + var HTML_CONTENT = """ Signal API Demo diff --git a/flumi/Scripts/Network.gd b/flumi/Scripts/Network.gd index d0882c4..9678f9f 100644 --- a/flumi/Scripts/Network.gd +++ b/flumi/Scripts/Network.gd @@ -4,6 +4,9 @@ func fetch_image(url: String) -> ImageTexture: var http_request = HTTPRequest.new() add_child(http_request) + if url.is_empty(): + return null + var error = http_request.request(url) if error != OK: print("Error making HTTP request: ", error) diff --git a/flumi/Scripts/Utils/Lua/DOM.gd b/flumi/Scripts/Utils/Lua/DOM.gd new file mode 100644 index 0000000..741c60e --- /dev/null +++ b/flumi/Scripts/Utils/Lua/DOM.gd @@ -0,0 +1,494 @@ +class_name LuaDOMUtils +extends RefCounted + +# DOM traversal properties (read-only) +static func get_element_parent_handler(vm: LuauVM, dom_parser: HTMLParser, lua_api: LuaAPI) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var element = find_element_by_id(element_id, dom_parser) + if not element or not element.parent: + vm.lua_pushnil() + return 1 + + create_element_wrapper(vm, element.parent, lua_api) + return 1 + +static func get_element_next_sibling_handler(vm: LuauVM, dom_parser: HTMLParser, lua_api: LuaAPI) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var element = find_element_by_id(element_id, dom_parser) + if not element or not element.parent: + vm.lua_pushnil() + return 1 + + var siblings = element.parent.children + var current_index = siblings.find(element) + + if current_index >= 0 and current_index < siblings.size() - 1: + var next_sibling = siblings[current_index + 1] + create_element_wrapper(vm, next_sibling, lua_api) + else: + vm.lua_pushnil() + + return 1 + +static func get_element_previous_sibling_handler(vm: LuauVM, dom_parser: HTMLParser, lua_api: LuaAPI) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var element = find_element_by_id(element_id, dom_parser) + if not element or not element.parent: + vm.lua_pushnil() + return 1 + + var siblings = element.parent.children + var current_index = siblings.find(element) + + if current_index > 0: + var prev_sibling = siblings[current_index - 1] + create_element_wrapper(vm, prev_sibling, lua_api) + else: + vm.lua_pushnil() + + return 1 + +static func get_element_first_child_handler(vm: LuauVM, dom_parser: HTMLParser, lua_api: LuaAPI) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var element = find_element_by_id(element_id, dom_parser) + if not element or element.children.is_empty(): + vm.lua_pushnil() + return 1 + + create_element_wrapper(vm, element.children[0], lua_api) + return 1 + +static func get_element_last_child_handler(vm: LuauVM, dom_parser: HTMLParser, lua_api: LuaAPI) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var element = find_element_by_id(element_id, dom_parser) + if not element or element.children.is_empty(): + vm.lua_pushnil() + return 1 + + create_element_wrapper(vm, element.children[-1], lua_api) + return 1 + +# DOM Manipulation Methods +static func insert_before_handler(vm: LuauVM, dom_parser: HTMLParser, lua_api) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) # parent + vm.luaL_checktype(2, vm.LUA_TTABLE) # new_child + vm.luaL_checktype(3, vm.LUA_TTABLE) # reference_child + + # Get parent element info + vm.lua_getfield(1, "_element_id") + var parent_element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + # Get new child element info + vm.lua_getfield(2, "_element_id") + var new_child_element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + # Get reference child element info + vm.lua_getfield(3, "_element_id") + var reference_child_element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + # Find elements + var parent_element = find_element_by_id(parent_element_id, dom_parser) + var new_child_element = find_element_by_id(new_child_element_id, dom_parser) + var reference_child_element = find_element_by_id(reference_child_element_id, dom_parser) + + if not parent_element or not new_child_element or not reference_child_element: + vm.lua_pushnil() + return 1 + + # Find reference child index in parent's children + var reference_index = parent_element.children.find(reference_child_element) + if reference_index < 0: + vm.lua_pushnil() + return 1 + + # Remove new child from its current parent if it has one + if new_child_element.parent: + var current_parent = new_child_element.parent + var current_index = current_parent.children.find(new_child_element) + if current_index >= 0: + current_parent.children.remove_at(current_index) + + # Insert new child before reference child + new_child_element.parent = parent_element + parent_element.children.insert(reference_index, new_child_element) + + # Handle visual rendering if parent is already rendered + handle_visual_insertion_by_reference(parent_element_id, new_child_element, reference_child_element_id, true, dom_parser, lua_api) + + # Return the new child + vm.lua_pushvalue(2) + return 1 + +static func insert_after_handler(vm: LuauVM, dom_parser: HTMLParser, lua_api) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) # parent + vm.luaL_checktype(2, vm.LUA_TTABLE) # new_child + vm.luaL_checktype(3, vm.LUA_TTABLE) # reference_child + + # Get parent element info + vm.lua_getfield(1, "_element_id") + var parent_element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + # Get new child element info + vm.lua_getfield(2, "_element_id") + var new_child_element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + # Get reference child element info + vm.lua_getfield(3, "_element_id") + var reference_child_element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + # Find elements + var parent_element = find_element_by_id(parent_element_id, dom_parser) + var new_child_element = find_element_by_id(new_child_element_id, dom_parser) + var reference_child_element = find_element_by_id(reference_child_element_id, dom_parser) + + if not parent_element or not new_child_element or not reference_child_element: + vm.lua_pushnil() + return 1 + + # Find reference child index in parent's children + var reference_index = parent_element.children.find(reference_child_element) + if reference_index < 0: + vm.lua_pushnil() + return 1 + + # Remove new child from its current parent if it has one + if new_child_element.parent: + var current_parent = new_child_element.parent + var current_index = current_parent.children.find(new_child_element) + if current_index >= 0: + current_parent.children.remove_at(current_index) + + # Insert new child after reference child + new_child_element.parent = parent_element + parent_element.children.insert(reference_index + 1, new_child_element) + + # Handle visual rendering if parent is already rendered + handle_visual_insertion_by_reference(parent_element_id, new_child_element, reference_child_element_id, false, dom_parser, lua_api) + + # Return the new child + vm.lua_pushvalue(2) + return 1 + +static func replace_handler(vm: LuauVM, dom_parser: HTMLParser, lua_api) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) # parent + vm.luaL_checktype(2, vm.LUA_TTABLE) # new_child + vm.luaL_checktype(3, vm.LUA_TTABLE) # old_child + + # Get parent element info + vm.lua_getfield(1, "_element_id") + var parent_element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + # Get new child element info + vm.lua_getfield(2, "_element_id") + var new_child_element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + # Get old child element info + vm.lua_getfield(3, "_element_id") + var old_child_element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + # Find elements + var parent_element = find_element_by_id(parent_element_id, dom_parser) + var new_child_element = find_element_by_id(new_child_element_id, dom_parser) + var old_child_element = find_element_by_id(old_child_element_id, dom_parser) + + if not parent_element or not new_child_element or not old_child_element: + vm.lua_pushnil() + return 1 + + # Find old child index in parent's children + var old_index = parent_element.children.find(old_child_element) + if old_index < 0: + vm.lua_pushnil() + return 1 + + # Remove new child from its current parent if it has one + if new_child_element.parent: + var current_parent = new_child_element.parent + var current_index = current_parent.children.find(new_child_element) + if current_index >= 0: + current_parent.children.remove_at(current_index) + + # Replace old child with new child + old_child_element.parent = null + new_child_element.parent = parent_element + parent_element.children[old_index] = new_child_element + + # Handle visual updates + handle_visual_replacement(old_child_element_id, new_child_element, parent_element_id, dom_parser, lua_api) + + # Return the old child + vm.lua_pushvalue(3) + return 1 + +static func clone_handler(vm: LuauVM, dom_parser: HTMLParser, lua_api) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) # element to clone + var deep: bool = false + + if vm.lua_gettop() >= 2: + deep = vm.lua_toboolean(2) + + # Get element info + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + # Find the element + var element = find_element_by_id(element_id, dom_parser) + if not element: + vm.lua_pushnil() + return 1 + + # Clone the element + var cloned_element = clone_element(element, deep) + + # Add cloned element to parser's element collection + dom_parser.parse_result.all_elements.append(cloned_element) + + # Create Lua element wrapper with full functionality + create_element_wrapper(vm, cloned_element, lua_api) + + return 1 + +# Helper functions +static func find_element_by_id(element_id: String, dom_parser: HTMLParser) -> HTMLParser.HTMLElement: + if element_id == "body": + return dom_parser.find_first("body") + else: + return dom_parser.find_by_id(element_id) + +static func create_element_wrapper(vm: LuauVM, element: HTMLParser.HTMLElement, lua_api: LuaAPI) -> void: + vm.lua_newtable() + + var element_id: String + if element.tag_name == "body": + element_id = "body" + else: + element_id = element.get_attribute("id") + if element_id.is_empty(): + element_id = lua_api.get_or_assign_element_id(element) + element.set_attribute("id", element_id) + + vm.lua_pushstring(element_id) + vm.lua_setfield(-2, "_element_id") + vm.lua_pushstring(element.tag_name) + vm.lua_setfield(-2, "_tag_name") + + if lua_api: + lua_api.add_element_methods(vm) + +static func clone_element(element: HTMLParser.HTMLElement, deep: bool) -> HTMLParser.HTMLElement: + var cloned = HTMLParser.HTMLElement.new(element.tag_name) + + for attr_name in element.attributes: + if attr_name != "id": + cloned.attributes[attr_name] = element.attributes[attr_name] + + cloned.text_content = element.text_content + + if deep: + for child in element.children: + var cloned_child = clone_element(child, true) + cloned_child.parent = cloned + cloned.children.append(cloned_child) + + return cloned + + +static func handle_visual_insertion_by_reference(parent_element_id: String, new_child_element: HTMLParser.HTMLElement, reference_element_id: String, insert_before: bool, dom_parser: HTMLParser, lua_api) -> void: + var parent_dom_node: Node = null + if parent_element_id == "body": + var main_scene = lua_api.get_node("/root/Main") + if main_scene: + parent_dom_node = main_scene.website_container + else: + parent_dom_node = dom_parser.parse_result.dom_nodes.get(parent_element_id, null) + + if parent_dom_node: + render_new_element_by_reference.call_deferred(new_child_element, parent_dom_node, reference_element_id, insert_before, dom_parser) + +static func handle_visual_replacement(old_child_element_id: String, new_child_element: HTMLParser.HTMLElement, parent_element_id: String, dom_parser: HTMLParser, lua_api) -> void: + var old_dom_node = dom_parser.parse_result.dom_nodes.get(old_child_element_id, null) + if not old_dom_node: + return + + var parent_container = old_dom_node.get_parent() + if not parent_container: + return + + var old_position = -1 + for i in parent_container.get_child_count(): + if parent_container.get_child(i) == old_dom_node: + old_position = i + break + + old_dom_node.queue_free() + dom_parser.parse_result.dom_nodes.erase(old_child_element_id) + + if old_position >= 0: + var parent_dom_node: Node = null + if parent_element_id == "body": + var main_scene = lua_api.get_node("/root/Main") + if main_scene: + parent_dom_node = main_scene.website_container + else: + parent_dom_node = dom_parser.parse_result.dom_nodes.get(parent_element_id, null) + + if parent_dom_node: + render_new_element_at_position.call_deferred(new_child_element, parent_dom_node, old_position, dom_parser) + +static func add_enhanced_element_methods(vm: LuauVM, lua_api, index: String = "element") -> void: + vm.lua_pushcallable(lua_api._element_insert_before_wrapper, index + ".insertBefore") + vm.lua_setfield(-2, "insertBefore") + + vm.lua_pushcallable(lua_api._element_insert_after_wrapper, index + ".insertAfter") + vm.lua_setfield(-2, "insertAfter") + + vm.lua_pushcallable(lua_api._element_replace_wrapper, index + ".replace") + vm.lua_setfield(-2, "replace") + + vm.lua_pushcallable(lua_api._element_clone_wrapper, index + ".clone") + vm.lua_setfield(-2, "clone") + +static func is_same_element_visual_node(node1: Node, node2: Node) -> bool: + if node1 == node2: + return true + + var current = node1 + var node1_grandparent = node1.get_parent() + if node1_grandparent: + node1_grandparent = node1_grandparent.get_parent() + while current: + if current == node2: + return true + var parent = current.get_parent() + if not parent or (node1_grandparent and current == node1_grandparent): + break + current = parent + + current = node2 + var node2_grandparent = node2.get_parent() + if node2_grandparent: + node2_grandparent = node2_grandparent.get_parent() + while current: + if current == node1: + return true + var parent = current.get_parent() + if not parent or (node2_grandparent and current == node2_grandparent): + break + current = parent + + return false + +static func render_new_element_at_position(element: HTMLParser.HTMLElement, parent_node: Node, position: int, dom_parser: HTMLParser) -> void: + var main_scene = Engine.get_main_loop().current_scene.get_node("/root/Main") + if not main_scene: + return + + var element_node = await main_scene.create_element_node(element, dom_parser) + if not element_node: + return + + element_node.set_meta("html_element", element) + dom_parser.register_dom_node(element, element_node) + + var container_node = parent_node + if parent_node is MarginContainer and parent_node.get_child_count() > 0: + container_node = parent_node.get_child(0) + elif parent_node == main_scene.website_container: + container_node = parent_node + + container_node.add_child(element_node) + if position >= 0 and position < container_node.get_child_count(): + container_node.move_child(element_node, position) + +static func render_new_element_by_reference(element: HTMLParser.HTMLElement, parent_node: Node, reference_element_id: String, insert_before: bool, dom_parser: HTMLParser) -> void: + var main_scene = Engine.get_main_loop().current_scene.get_node("/root/Main") + if not main_scene: + return + + var reference_dom_node = dom_parser.parse_result.dom_nodes.get(reference_element_id, null) + if not reference_dom_node: + return + + var container_node = parent_node + if parent_node is MarginContainer and parent_node.get_child_count() > 0: + container_node = parent_node.get_child(0) + elif parent_node == main_scene.website_container: + container_node = parent_node + + var reference_position = -1 + for i in container_node.get_child_count(): + var child = container_node.get_child(i) + if child == reference_dom_node or is_same_element_visual_node(child, reference_dom_node): + reference_position = i + break + + if reference_position < 0: + reference_position = container_node.get_child_count() + + var insert_position = reference_position + if not insert_before: + insert_position = reference_position + 1 + + var element_node = await main_scene.create_element_node(element, dom_parser) + if not element_node: + return + + element_node.set_meta("html_element", element) + dom_parser.register_dom_node(element, element_node) + + container_node.add_child(element_node) + if insert_position >= 0 and insert_position < container_node.get_child_count() - 1: + container_node.move_child(element_node, insert_position) + +static func _index_handler(vm: LuauVM, lua_api: LuaAPI) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + var key: String = vm.luaL_checkstring(2) + + match key: + "parent": + return lua_api._get_element_parent_wrapper(vm, lua_api) + "nextSibling": + return lua_api._get_element_next_sibling_wrapper(vm, lua_api) + "previousSibling": + return lua_api._get_element_previous_sibling_wrapper(vm, lua_api) + "firstChild": + return lua_api._get_element_first_child_wrapper(vm, lua_api) + "lastChild": + return lua_api._get_element_last_child_wrapper(vm, lua_api) + _: + return lua_api._element_index_handler(vm) diff --git a/flumi/Scripts/Utils/Lua/DOM.gd.uid b/flumi/Scripts/Utils/Lua/DOM.gd.uid new file mode 100644 index 0000000..484536d --- /dev/null +++ b/flumi/Scripts/Utils/Lua/DOM.gd.uid @@ -0,0 +1 @@ +uid://odam12sq4d5d