diff --git a/document.tscn b/document.tscn index 69a19a0..4b9633f 100644 --- a/document.tscn +++ b/document.tscn @@ -20,14 +20,18 @@ [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_61mkw"] -[node name="Root" type="Node2D"] +[node name="Root" type="Node2D" node_paths=PackedStringArray("document", "address_box", "enable_links")] script = ExtResource("1_i3q73") +document = NodePath("Document") +address_box = NodePath("Control/PanelContainer/VBoxContainer/TextEdit") +enable_links = NodePath("Control/PanelContainer/VBoxContainer/CheckBox2") -[node name="Document" type="Node2D" parent="." node_paths=PackedStringArray("http_request", "address_box")] +[node name="Document" type="Node2D" parent="." node_paths=PackedStringArray("http_request", "address_box", "back_button")] position = Vector2(10, 10) script = ExtResource("1_t74iv") http_request = NodePath("../HTTPRequest") address_box = NodePath("../Control/PanelContainer/VBoxContainer/TextEdit") +back_button = NodePath("../Control/PanelContainer/VBoxContainer/HBoxContainer/Button2") [node name="Floor" type="StaticBody2D" parent="."] position = Vector2(0, 1080) @@ -53,7 +57,7 @@ offset_left = -349.0 offset_top = 10.0 offset_right = -10.0 -offset_bottom = 287.0 +offset_bottom = 315.0 grow_horizontal = 0 theme_override_styles/panel = SubResource("StyleBoxFlat_55s8d") @@ -91,15 +95,31 @@ text = "Enable Gravity" script = ExtResource("3_loy23") +[node name="CheckBox2" type="CheckBox" parent="Control/PanelContainer/VBoxContainer"] +layout_mode = 2 +button_pressed = true +text = "Enable Links" + [node name="HSeparator3" type="HSeparator" parent="Control/PanelContainer/VBoxContainer"] layout_mode = 2 size_flags_vertical = 3 theme_override_styles/separator = SubResource("StyleBoxEmpty_61mkw") -[node name="Button" type="Button" parent="Control/PanelContainer/VBoxContainer"] +[node name="HBoxContainer" type="HBoxContainer" parent="Control/PanelContainer/VBoxContainer"] layout_mode = 2 -text = "Refresh" + +[node name="Button" type="Button" parent="Control/PanelContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Refesh" + +[node name="Button2" type="Button" parent="Control/PanelContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +disabled = true +text = "Back" [node name="HTTPRequest" type="HTTPRequest" parent="."] -[connection signal="pressed" from="Control/PanelContainer/VBoxContainer/Button" to="Document" method="on_refresh"] +[connection signal="pressed" from="Control/PanelContainer/VBoxContainer/HBoxContainer/Button" to="Document" method="on_refresh"] +[connection signal="pressed" from="Control/PanelContainer/VBoxContainer/HBoxContainer/Button2" to="Document" method="on_refresh"] diff --git a/scripts/document.gd b/scripts/document.gd index e89f97a..9167149 100644 --- a/scripts/document.gd +++ b/scripts/document.gd @@ -1,7 +1,12 @@ +class_name Document extends Node2D @export var http_request: HTTPRequest @export var address_box: TextEdit +@export var back_button: Button + +var current_page: String +var history: Array[String] = [] func _create_block_node(block: Layout.BlockNode) -> Node: var box = Control.new() @@ -21,6 +26,12 @@ rigid_body.position = fragment.rect.position rigid_body.input_pickable = true + if fragment.dom_node.tag_name == 'a' and 'href' in fragment.dom_node.attributes: + var link = Link.new() + link.name = 'Link' + link.href = fragment.dom_node.attributes['href'] + rigid_body.add_child(link) + var shape = CollisionShape2D.new() shape.shape = RectangleShape2D.new() shape.shape.size = fragment.rect.size @@ -53,13 +64,39 @@ var dom_tree = DOM.build_dom_tree(parser) var layout_tree = Layout.build_layout_tree(dom_tree) layout_tree.layout(1000) - _create_block(self, layout_tree) -func on_refresh() -> void: for child in get_children(): remove_child(child) - http_request.request(address_box.text) + _create_block(self, layout_tree) + +func _load_page(address: String): + if current_page != address: + print('Add to history: ', current_page) + history.append(current_page) + back_button.disabled = false + current_page = address + + print('Load ', address) + http_request.cancel_request() + http_request.request(address) + +func on_refresh() -> void: + _load_page(address_box.text) + +func _on_back_pressed(): + if len(history) == 0: + return + + var address = history.pop_back() + print('Read from history: ', address) + current_page = address + address_box.text = address + if len(history) == 0: + back_button.disabled = true + _load_page(address) func _ready(): + current_page = address_box.text http_request.request_completed.connect(_on_request_completed) + back_button.pressed.connect(_on_back_pressed) on_refresh() diff --git a/scripts/dom.gd b/scripts/dom.gd index 9027470..e88890f 100644 --- a/scripts/dom.gd +++ b/scripts/dom.gd @@ -19,12 +19,14 @@ class DomNode: var tag_name: String + var attributes: Dictionary var style: Style var text: String var children: Array[DomNode] - func _init(tag_name: String, parent_style: Style): + func _init(tag_name: String, attributes: Dictionary, parent_style: Style): self.tag_name = tag_name + self.attributes = attributes self.style = Style.apply_default_style_for_element(tag_name, parent_style) func add_child(child: DomNode): @@ -61,7 +63,7 @@ return false static func build_dom_tree(parser: Parser) -> DomNode: - var root = DomNode.new('root', Style.new()) + var root = DomNode.new('root', {}, Style.new()) var stack: Array[DomNode] = [root] while true: @@ -73,7 +75,7 @@ var tag_name = token.tag_name.to_lower() if token.type == Token.Type.Text: - var text_node = DomNode.new('TEXT', top_of_stack.style) + var text_node = DomNode.new('TEXT', {}, top_of_stack.style) text_node.text = token.text top_of_stack.add_child(text_node) continue @@ -82,8 +84,8 @@ if _should_auto_close(tag_name, top_of_stack.tag_name): stack.pop_back() top_of_stack = stack[len(stack) - 1] - - var node = DomNode.new(tag_name, top_of_stack.style) + + var node = DomNode.new(tag_name, token.attributes, top_of_stack.style) top_of_stack.add_child(node) if not tag_name in SELF_CLOSING_TAGS: stack.append(node) diff --git a/scripts/layout.gd b/scripts/layout.gd index 4e58d32..b1018d7 100644 --- a/scripts/layout.gd +++ b/scripts/layout.gd @@ -128,7 +128,7 @@ child.debug_print(indent + 1) static func _inline_children_to_block(block: BlockNode): - var inline_block = BlockNode.new(DOM.DomNode.new('INLINE', Style.new())) + var inline_block = BlockNode.new(DOM.DomNode.new('INLINE', {}, Style.new())) inline_block.inline_children = block.inline_children block.inline_children = [] block.block_children.append(inline_block) diff --git a/scripts/link.gd b/scripts/link.gd new file mode 100644 index 0000000..5ce0465 --- /dev/null +++ b/scripts/link.gd @@ -0,0 +1,4 @@ +class_name Link +extends Node + +var href = '' diff --git a/scripts/parser.gd b/scripts/parser.gd index 62cf61a..92606a9 100644 --- a/scripts/parser.gd +++ b/scripts/parser.gd @@ -5,7 +5,13 @@ enum State { Text, TagName, + Attribute, AttributeName, + AttributeEquals, + AttributeValueStart, + AttributeValueUnquoted, + AttributeValueQuoted, + DocType, } var _source: String @@ -13,6 +19,8 @@ var _index: int = 0 var _current_token: Token +var _current_attribute_name: String = '' +var _current_attribute_value: String = '' func _init(source: String) -> void: self._source = source @@ -48,9 +56,11 @@ func _on_tag_name() -> Token: match _next_char(): ' ', '\t', '\n', '\r': - _state = State.AttributeName + _state = State.Attribute '/': _current_token.is_closing = true + '!': + _state = State.DocType '>': _state = State.Text _current_token.type = Token.Type.Tag @@ -61,12 +71,104 @@ _current_token.tag_name += c return null -func _on_attibute_name() -> Token: +func _on_attibute() -> Token: match _next_char(): + ' ', '\t', '\n', '\r': + pass '>': _state = State.Text _current_token.type = Token.Type.Tag - return _current_token + return _emit_token() + EOF: + assert(false, 'TODO') + var c: + _current_attribute_name = c + _state = State.AttributeName + return null + +func _on_attibute_name() -> Token: + match _next_char(): + ' ', '\t', '\n', '\r': + _state = State.AttributeEquals + '=': + _state = State.AttributeValueStart + '>': + _state = State.Text + _current_token.type = Token.Type.Tag + return _emit_token() + EOF: + assert(false, 'TODO') + var c: + _current_attribute_name += c + return null + +func _on_attibute_equals() -> Token: + match _next_char(): + ' ', '\t', '\n', '\r': + pass + '=': + _state = State.AttributeValueStart + '>': + _state = State.Text + _current_token.type = Token.Type.Tag + return _emit_token() + _: + assert(false, 'TODO') + return null + +func _on_attibute_value_start() -> Token: + match _next_char(): + ' ', '\t', '\n', '\r': + pass + '"': + _state = State.AttributeValueQuoted + _current_attribute_value = '' + '>': + _state = State.Text + _current_token.type = Token.Type.Tag + return _emit_token() + EOF: + assert(false, 'TODO') + var c: + _current_attribute_value = c + _state = State.AttributeValueUnquoted + return null + +func _on_attibute_value_quoted() -> Token: + match _next_char(): + '"': + _current_token.attributes[_current_attribute_name.to_lower()] = _current_attribute_value + _state = State.Attribute + '>': + _state = State.Text + _current_token.type = Token.Type.Tag + return _emit_token() + EOF: + assert(false, 'TODO') + var c: + _current_attribute_value += c + return null + +func _on_attibute_value_unquoted() -> Token: + match _next_char(): + ' ', '\t', '\n', '\r': + _current_token.attributes[_current_attribute_name.to_lower()] = _current_attribute_value + _state = State.Attribute + '>': + _current_token.attributes[_current_attribute_name.to_lower()] = _current_attribute_value + _state = State.Text + _current_token.type = Token.Type.Tag + return _emit_token() + EOF: + assert(false, 'TODO') + var c: + _current_attribute_value += c + return null + +func _on_doc_type() -> Token: + match _next_char(): + '>': + _state = State.Text EOF: assert(false, 'TODO') return null @@ -75,7 +177,13 @@ match _state: State.Text: return _on_text() State.TagName: return _on_tag_name() + State.Attribute: return _on_attibute() State.AttributeName: return _on_attibute_name() + State.AttributeEquals: return _on_attibute_equals() + State.AttributeValueStart: return _on_attibute_value_start() + State.AttributeValueQuoted: return _on_attibute_value_quoted() + State.AttributeValueUnquoted: return _on_attibute_value_unquoted() + State.DocType: return _on_doc_type() return null func next() -> Token: diff --git a/scripts/physics_drag.gd b/scripts/physics_drag.gd index ad035f5..155bca8 100644 --- a/scripts/physics_drag.gd +++ b/scripts/physics_drag.gd @@ -1,5 +1,9 @@ extends Node2D +@export var document: Document +@export var address_box: TextEdit +@export var enable_links: CheckBox + var selected_body: RigidBody2D = null var drag_position: Vector2 = Vector2.ZERO @@ -8,12 +12,27 @@ var query = PhysicsPointQueryParameters2D.new() query.position = event.position var result = space_state.intersect_point(query) - + if len(result) > 0: selected_body = result[0]['collider'] drag_position = selected_body.global_transform.inverse() * event.position +func follow_link(href: String): + if not href.begins_with('http'): + var address = address_box.text.strip_edges().trim_prefix('/') + var base = '/'.join(address.split('/').slice(0, -1)) + href = base + '/' + href + + address_box.text = href + document.on_refresh() + func _on_mouse_up(event: InputEventMouseButton): + if selected_body == null: + return + + var link = selected_body.get_node('Link') + if link != null and enable_links.button_pressed: + follow_link(link.href) selected_body = null func _process(delta: float): diff --git a/scripts/token.gd b/scripts/token.gd index 3659cca..263d9c0 100644 --- a/scripts/token.gd +++ b/scripts/token.gd @@ -1,21 +1,22 @@ class_name Token enum Type { - Text, - Tag, + Text, + Tag, } var type: Type var text: String = '' var tag_name: String = '' +var attributes: Dictionary = {} var is_closing: bool = false func _to_string() -> String: - match type: - Type.Text: return 'Text(%s)' % text - Type.Tag: - if is_closing: - return 'ClosingTag(name=%s)' % tag_name - else: - return 'OpenTag(name=%s)' % tag_name - _: return 'Unknown' + match type: + Type.Text: return 'Text(%s)' % text + Type.Tag: + if is_closing: + return 'ClosingTag(name=%s)' % tag_name + else: + return 'OpenTag(name=%s)' % tag_name + _: return 'Unknown'