From 96a9bc661c4ce90b5e320cac9a272a401d2a0186 Mon Sep 17 00:00:00 2001 From: Julia Ortiz <94128293+julia-script@users.noreply.github.com> Date: Thu, 22 May 2025 12:54:36 -0300 Subject: [PATCH 1/4] docs: annotate layout building --- packages/core/src/layout/LayoutTree.zig | 52 ++++++++++++++++++++++- packages/core/src/layout/doc-from-xml.zig | 20 ++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/packages/core/src/layout/LayoutTree.zig b/packages/core/src/layout/LayoutTree.zig index 0394015..3e47f8b 100644 --- a/packages/core/src/layout/LayoutTree.zig +++ b/packages/core/src/layout/LayoutTree.zig @@ -1,3 +1,6 @@ +// The LayoutTree mirrors the DOM tree but only contains the information +// required for layout. This module provides a small tree of lightweight +// nodes which can later be used by the layout and rendering passes. const std = @import("std"); const DocNodeId = @import("../tree/Node.zig").NodeId; const DocTree = @import("../tree/Tree.zig"); @@ -5,16 +8,22 @@ const Array = std.ArrayListUnmanaged; const HashMap = std.AutoHashMapUnmanaged; const docFromXml = @import("./doc-from-xml.zig").docFromXml; +// Map of all layout nodes indexed by their id. We store nodes here so the ids +// remain stable and small. nodes: HashMap(LayoutNode.Id, LayoutNode) = .{}, +// Monotonic counter used to assign new ids. node_count: LayoutNode.Id = 0, +// Allocator used throughout the tree. allocator: std.mem.Allocator, const Self = @This(); pub fn init(allocator: std.mem.Allocator) Self { + // Initialises an empty layout tree using the provided allocator. return Self{ .allocator = allocator }; } pub fn deinit(self: *Self) void { + // Clean up all nodes stored in the tree. var it = self.nodes.iterator(); while (it.next()) |entry| { entry.value_ptr.deinit(self.allocator); @@ -29,6 +38,7 @@ pub fn createNode(self: *Self, data: LayoutNode.Data) !LayoutNode.Id { return id; } pub fn createTextNode(self: *Self, contents: []const u8) !LayoutNode.Id { + // Convenience helper for text nodes which also stores the text contents. const node_id = try self.createNode(.{ .text_node = .{} }); var node = self.getNodePtr(node_id); try node.data.text_node.contents.appendSlice(self.allocator, contents); @@ -42,20 +52,31 @@ pub fn appendNode(self: *Self, parent_id: LayoutNode.Id, child_id: LayoutNode.Id .inline_container_node => |*n| &n.children, else => return error.InvalidParent, }; + // Update the appropriate child list depending on the parent type. try list.append(self.allocator, child_id); } pub fn getNodePtr(self: *Self, id: LayoutNode.Id) *LayoutNode { + // Retrieve a node pointer by id or panic if it does not exist. return self.nodes.getPtr(id) orelse std.debug.panic("LayoutTree: Node {d} not found", .{id}); } +/// Base node stored in the LayoutTree. Each node is identified by a unique +/// numeric id and tagged union containing the specific node type. pub const LayoutNode = struct { + /// Identifier used as key in the `nodes` map. id: Id, + /// Concrete node payload. data: Data, pub const Id = u32; + /// Different kinds of nodes that can appear in the layout tree. pub const Data = union(enum) { + /// Simple text leaf. text_node: TextNode, + /// Represents an inline DOM element. inline_node: InlineNode, + /// Block formatting context participant with block children. block_container_node: BlockContainerNode, + /// Block container that contains only inline children. inline_container_node: InlineContainerNode, }; pub fn deinit(self: *LayoutNode, allocator: std.mem.Allocator) void { @@ -69,6 +90,7 @@ pub const LayoutNode = struct { }; pub const TextNode = struct { + /// Raw textual contents for this node. contents: Array(u8) = .{}, pub fn deinit(self: *TextNode, allocator: std.mem.Allocator) void { self.contents.deinit(allocator); @@ -76,8 +98,13 @@ pub const TextNode = struct { }; pub const InlineNode = struct { + /// Back reference to the originating DOM node or anonymous. ref: DocRef, + /// When true the node is treated as a single fragment. is_atomic: bool, + /// Link to the next node in the continuation chain when this inline is split. + continuation: ?LayoutNode.Id = null, + /// Layout children produced from DOM children. children: Array(LayoutNode.Id) = .{}, pub fn deinit(self: *InlineNode, allocator: std.mem.Allocator) void { self.children.deinit(allocator); @@ -85,12 +112,18 @@ pub const InlineNode = struct { }; pub const DocRef = union(enum) { + /// Layout nodes that do not correspond to a DOM node use this tag. anonymous, + /// Reference to the DOM node that created this layout node. doc_node: DocNodeId, }; pub const BlockContainerNode = struct { + /// Originating DOM node or anonymous wrapper. ref: DocRef, + /// Optional continuation pointer used when inline flows are split by blocks. + continuation: ?LayoutNode.Id = null, + /// Block children contained inside this node. children: Array(LayoutNode.Id) = .{}, pub fn deinit(self: *BlockContainerNode, allocator: std.mem.Allocator) void { self.children.deinit(allocator); @@ -103,8 +136,11 @@ pub const BlockContainerNode = struct { /// The same as a block container, but all children are inline, which enables inline formatting context. /// this node also holds the LineBoxes pub const InlineContainerNode = struct { + /// Wrapper for a block formatting context that contains only inline children. ref: DocRef, + continuation: ?LayoutNode.Id = null, children: Array(LayoutNode.Id) = .{}, + /// Lines produced during inline layout pass. line_boxes: Array(LineBox) = .{}, pub fn deinit(self: *InlineContainerNode, allocator: std.mem.Allocator) void { self.children.deinit(allocator); @@ -113,6 +149,7 @@ pub const InlineContainerNode = struct { }; pub const LineBox = struct { + /// Inline fragments laid out on this line. fragments: Array(Fragment) = .{}, pub const Fragment = struct { node: LayoutNode.Id, @@ -125,6 +162,7 @@ pub const LineBox = struct { }; pub fn fromTree(allocator: std.mem.Allocator, tree: *DocTree) !Self { + // Entry point used by tests to convert a DOM tree into a layout tree. var self = Self.init(allocator); // Start building the layout tree at the document root. _ = try self.build(tree, DocTree.ROOT_NODE_ID); @@ -132,6 +170,8 @@ pub fn fromTree(allocator: std.mem.Allocator, tree: *DocTree) !Self { } fn nodeIsInline(tree: *DocTree, node_id: DocNodeId) bool { + // Helper used during tree construction to check if a DOM node is inline + // level and therefore should live in an inline formatting context. const kind = tree.getNodeKind(node_id); if (kind == .text) return true; return tree.getStyle(node_id).display.outside == .@"inline"; @@ -202,6 +242,9 @@ fn build(self: *Self, tree: *DocTree, node_id: DocNodeId) !?LayoutNode.Id { var inline_seq: Array(LayoutNode.Id) = .{}; defer inline_seq.deinit(self.allocator); + // Iterate over the DOM children converting them into layout nodes on the + // fly. Inline children are grouped so that a single anonymous wrapper can + // be inserted before we append any block level node. for (children) |child| { const child_is_inline = nodeIsInline(tree, child); const maybe_child = try self.build(tree, child); @@ -212,7 +255,9 @@ fn build(self: *Self, tree: *DocTree, node_id: DocNodeId) !?LayoutNode.Id { // Accumulate inline children so they can be wrapped together. try inline_seq.append(self.allocator, l_id); } else { - // Flush any collected inline children before appending the block. + // A block child terminates the current inline sequence. Wrap the + // accumulated inline nodes in an anonymous inline container before + // adding the block to the container. if (inline_seq.items.len > 0) { const anon = try self.createNode(.{ .inline_container_node = .{ .ref = .anonymous } }); for (inline_seq.items) |iid| { @@ -225,7 +270,7 @@ fn build(self: *Self, tree: *DocTree, node_id: DocNodeId) !?LayoutNode.Id { } } - // Flush trailing inline children. + // Any remaining inline sequence becomes the trailing anonymous container. if (inline_seq.items.len > 0) { const anon = try self.createNode(.{ .inline_container_node = .{ .ref = .anonymous } }); for (inline_seq.items) |iid| { @@ -253,6 +298,7 @@ fn getChildren(node: *LayoutNode) []const LayoutNode.Id { }; } +// Helper used by the test printer to render the tree structure. fn printNodeInternal(self: *Self, node_id: LayoutNode.Id, writer: std.io.AnyWriter, prefix: []const u8, is_root: bool, is_last: bool) !void { const node = self.getNodePtr(node_id); @@ -321,6 +367,8 @@ pub fn printRoot(self: *Self, writer: std.io.AnyWriter) !void { } pub fn expectLayoutTree(description: []const u8, docXml: []const u8, expected: []const u8) !void { + // Utility used by the unit tests to compare the produced layout tree + // against an expected textual representation. var tree = try docFromXml(std.testing.allocator, docXml, .{}); defer tree.deinit(); diff --git a/packages/core/src/layout/doc-from-xml.zig b/packages/core/src/layout/doc-from-xml.zig index 03bcf2d..0693584 100644 --- a/packages/core/src/layout/doc-from-xml.zig +++ b/packages/core/src/layout/doc-from-xml.zig @@ -1,13 +1,23 @@ +// Helper functions to convert a small XML subset into the document tree used by +// the layout tests. The parser is intentionally simple and exists only so the +// layout code can be tested without pulling in a full HTML parser. const xml = @import("../xml.zig"); const std = @import("std"); const Tree = @import("../tree/Tree.zig"); +/// Simple configuration options used when converting XML into a test DOM tree. pub const Options = struct { + /// Discard text nodes that only contain whitespace. ignore_empty_text: bool = true, + /// Remove leading and trailing whitespace from text nodes. trim_text: bool = true, + /// Split text nodes on newline characters into multiple nodes. split_lines: bool = true, }; +/// Parse a string of XML into the document tree representation understood by +/// the layout code. The resulting DOM tree is independent of the XML parser +/// after this function returns. pub fn docFromXml(allocator: std.mem.Allocator, xml_string: []const u8, options: Options) !Tree { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); @@ -45,12 +55,16 @@ fn trimText(text: []const u8) []const u8 { return std.mem.trim(u8, text, " \n\t\r"); } +/// Recursively build the Tree representation from a parsed XML element. fn nodeFromXmlElement(tree: *Tree, element: *xml.Element, options: Options) TreeFromXmlError!Tree.Node.NodeId { + // Create a DOM node corresponding to this element. const node_id = try tree.createNode(); + // Walk all children of the element and create corresponding DOM nodes. for (element.children) |child| { switch (child) { .char_data => { + // Text nodes may be dropped or split according to the options. if (options.ignore_empty_text and isEmpty(child.char_data)) { continue; } @@ -67,9 +81,12 @@ fn nodeFromXmlElement(tree: *Tree, element: *xml.Element, options: Options) Tree } }, .comment => { - // ignore + // Comments are ignored entirely. }, .element => { + // Recursively build the subtree for the child element and + // assign inline style hints for some HTML-like tags used in the + // tests. const child_id = try nodeFromXmlElement(tree, child.element, options); var child_node = tree.getNode(child_id); if (std.mem.eql(u8, child.element.tag, "span")) { @@ -88,6 +105,7 @@ fn nodeFromXmlElement(tree: *Tree, element: *xml.Element, options: Options) Tree return node_id; } test "treeFromXml" { + // Basic sanity test to print the generated tree to stderr during testing. var tree = try docFromXml(std.testing.allocator, "
Hello, world!
", .{}); defer tree.deinit(); const stderr = std.io.getStdErr().writer().any(); From 85a3fe0c1da78bbac8c5b93e38e328fd2d7cd4d4 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Thu, 22 May 2025 15:39:54 -0300 Subject: [PATCH 2/4] cleanup --- packages/core/src/layout/LayoutTree.zig | 168 +++++----------------- packages/core/src/layout/doc-from-xml.zig | 4 +- 2 files changed, 42 insertions(+), 130 deletions(-) diff --git a/packages/core/src/layout/LayoutTree.zig b/packages/core/src/layout/LayoutTree.zig index 3e47f8b..b8e2be8 100644 --- a/packages/core/src/layout/LayoutTree.zig +++ b/packages/core/src/layout/LayoutTree.zig @@ -1,6 +1,3 @@ -// The LayoutTree mirrors the DOM tree but only contains the information -// required for layout. This module provides a small tree of lightweight -// nodes which can later be used by the layout and rendering passes. const std = @import("std"); const DocNodeId = @import("../tree/Node.zig").NodeId; const DocTree = @import("../tree/Tree.zig"); @@ -8,22 +5,16 @@ const Array = std.ArrayListUnmanaged; const HashMap = std.AutoHashMapUnmanaged; const docFromXml = @import("./doc-from-xml.zig").docFromXml; -// Map of all layout nodes indexed by their id. We store nodes here so the ids -// remain stable and small. nodes: HashMap(LayoutNode.Id, LayoutNode) = .{}, -// Monotonic counter used to assign new ids. node_count: LayoutNode.Id = 0, -// Allocator used throughout the tree. allocator: std.mem.Allocator, const Self = @This(); pub fn init(allocator: std.mem.Allocator) Self { - // Initialises an empty layout tree using the provided allocator. return Self{ .allocator = allocator }; } pub fn deinit(self: *Self) void { - // Clean up all nodes stored in the tree. var it = self.nodes.iterator(); while (it.next()) |entry| { entry.value_ptr.deinit(self.allocator); @@ -38,7 +29,6 @@ pub fn createNode(self: *Self, data: LayoutNode.Data) !LayoutNode.Id { return id; } pub fn createTextNode(self: *Self, contents: []const u8) !LayoutNode.Id { - // Convenience helper for text nodes which also stores the text contents. const node_id = try self.createNode(.{ .text_node = .{} }); var node = self.getNodePtr(node_id); try node.data.text_node.contents.appendSlice(self.allocator, contents); @@ -52,31 +42,21 @@ pub fn appendNode(self: *Self, parent_id: LayoutNode.Id, child_id: LayoutNode.Id .inline_container_node => |*n| &n.children, else => return error.InvalidParent, }; - // Update the appropriate child list depending on the parent type. try list.append(self.allocator, child_id); } pub fn getNodePtr(self: *Self, id: LayoutNode.Id) *LayoutNode { - // Retrieve a node pointer by id or panic if it does not exist. return self.nodes.getPtr(id) orelse std.debug.panic("LayoutTree: Node {d} not found", .{id}); } -/// Base node stored in the LayoutTree. Each node is identified by a unique -/// numeric id and tagged union containing the specific node type. pub const LayoutNode = struct { - /// Identifier used as key in the `nodes` map. id: Id, - /// Concrete node payload. + parent: ?Id = null, data: Data, pub const Id = u32; - /// Different kinds of nodes that can appear in the layout tree. pub const Data = union(enum) { - /// Simple text leaf. text_node: TextNode, - /// Represents an inline DOM element. inline_node: InlineNode, - /// Block formatting context participant with block children. block_container_node: BlockContainerNode, - /// Block container that contains only inline children. inline_container_node: InlineContainerNode, }; pub fn deinit(self: *LayoutNode, allocator: std.mem.Allocator) void { @@ -90,7 +70,6 @@ pub const LayoutNode = struct { }; pub const TextNode = struct { - /// Raw textual contents for this node. contents: Array(u8) = .{}, pub fn deinit(self: *TextNode, allocator: std.mem.Allocator) void { self.contents.deinit(allocator); @@ -98,32 +77,22 @@ pub const TextNode = struct { }; pub const InlineNode = struct { - /// Back reference to the originating DOM node or anonymous. ref: DocRef, - /// When true the node is treated as a single fragment. is_atomic: bool, - /// Link to the next node in the continuation chain when this inline is split. - continuation: ?LayoutNode.Id = null, - /// Layout children produced from DOM children. children: Array(LayoutNode.Id) = .{}, + continuation: ?LayoutNode.Id = null, pub fn deinit(self: *InlineNode, allocator: std.mem.Allocator) void { self.children.deinit(allocator); } }; pub const DocRef = union(enum) { - /// Layout nodes that do not correspond to a DOM node use this tag. anonymous, - /// Reference to the DOM node that created this layout node. doc_node: DocNodeId, }; pub const BlockContainerNode = struct { - /// Originating DOM node or anonymous wrapper. ref: DocRef, - /// Optional continuation pointer used when inline flows are split by blocks. - continuation: ?LayoutNode.Id = null, - /// Block children contained inside this node. children: Array(LayoutNode.Id) = .{}, pub fn deinit(self: *BlockContainerNode, allocator: std.mem.Allocator) void { self.children.deinit(allocator); @@ -136,11 +105,8 @@ pub const BlockContainerNode = struct { /// The same as a block container, but all children are inline, which enables inline formatting context. /// this node also holds the LineBoxes pub const InlineContainerNode = struct { - /// Wrapper for a block formatting context that contains only inline children. ref: DocRef, - continuation: ?LayoutNode.Id = null, children: Array(LayoutNode.Id) = .{}, - /// Lines produced during inline layout pass. line_boxes: Array(LineBox) = .{}, pub fn deinit(self: *InlineContainerNode, allocator: std.mem.Allocator) void { self.children.deinit(allocator); @@ -149,7 +115,6 @@ pub const InlineContainerNode = struct { }; pub const LineBox = struct { - /// Inline fragments laid out on this line. fragments: Array(Fragment) = .{}, pub const Fragment = struct { node: LayoutNode.Id, @@ -162,7 +127,6 @@ pub const LineBox = struct { }; pub fn fromTree(allocator: std.mem.Allocator, tree: *DocTree) !Self { - // Entry point used by tests to convert a DOM tree into a layout tree. var self = Self.init(allocator); // Start building the layout tree at the document root. _ = try self.build(tree, DocTree.ROOT_NODE_ID); @@ -170,41 +134,47 @@ pub fn fromTree(allocator: std.mem.Allocator, tree: *DocTree) !Self { } fn nodeIsInline(tree: *DocTree, node_id: DocNodeId) bool { - // Helper used during tree construction to check if a DOM node is inline - // level and therefore should live in an inline formatting context. const kind = tree.getNodeKind(node_id); if (kind == .text) return true; return tree.getStyle(node_id).display.outside == .@"inline"; } +fn isOnlyInlineSubtree(tree: *DocTree, node_id: DocNodeId) bool { + const kind = tree.getNodeKind(node_id); + if (kind == .text) return true; + if (tree.getStyle(node_id).display.outside != .@"inline") return false; + for (tree.getNodeChildren(node_id)) |child| { + if (!isOnlyInlineSubtree(tree, child)) return false; + } + return true; +} +pub fn isDisplayNone(tree: *DocTree, node_id: DocNodeId) bool { + return tree.getStyle(node_id).display.outside == .none; +} /// Recursively convert the DOM starting at `node_id` into layout nodes. /// Returns the id of the created layout node or `null` if the DOM node should /// not produce a layout representation. -fn build(self: *Self, tree: *DocTree, node_id: DocNodeId) !?LayoutNode.Id { +fn build(self: *Self, tree: *DocTree, node_id: DocNodeId) !LayoutNode.Id { const kind = tree.getNodeKind(node_id); // 1. Text DOM nodes map directly to layout text nodes. Empty text nodes are // ignored. if (kind == .text) { const text = tree.getText(node_id).bytes.items; - if (text.len == 0) return null; const id = try self.createTextNode(text); return id; } const style = tree.getStyle(node_id); - // 2. Nodes with `display: none` do not participate in layout. - if (style.display.outside == .none) return null; - // 3. Inline-level elements produce an `InlineNode` and simply convert all of // their children. if (style.display.outside == .@"inline") { const id = try self.createNode(.{ .inline_node = .{ .ref = .{ .doc_node = node_id }, .is_atomic = false } }); for (tree.getNodeChildren(node_id)) |child| { - if (try self.build(tree, child)) |child_id| { - try self.appendNode(id, child_id); - } + if (isDisplayNone(tree, child)) continue; + const child_layout_node_id = try self.build(tree, child); + try self.appendNode(id, child_layout_node_id); } return id; } @@ -215,11 +185,12 @@ fn build(self: *Self, tree: *DocTree, node_id: DocNodeId) !?LayoutNode.Id { // Determine whether every visible child is inline-level so we know what // kind of container to create. for (children) |child| { - if (!nodeIsInline(tree, child)) { - if (tree.getStyle(child).display.outside != .none) { - only_inline = false; - break; - } + if (tree.getStyle(child).display.outside == .none) { + continue; + } + if (!isOnlyInlineSubtree(tree, child)) { + only_inline = false; + break; } } @@ -228,9 +199,9 @@ fn build(self: *Self, tree: *DocTree, node_id: DocNodeId) !?LayoutNode.Id { // so they participate in the inline formatting context. const id = try self.createNode(.{ .inline_container_node = .{ .ref = .{ .doc_node = node_id } } }); for (children) |child| { - if (try self.build(tree, child)) |child_id| { - try self.appendNode(id, child_id); - } + if (isDisplayNone(tree, child)) continue; + const child_layout_node_id = try self.build(tree, child); + try self.appendNode(id, child_layout_node_id); } return id; } @@ -239,45 +210,7 @@ fn build(self: *Self, tree: *DocTree, node_id: DocNodeId) !?LayoutNode.Id { // containers around contiguous inline children to preserve block model // invariants. const container_id = try self.createNode(.{ .block_container_node = .{ .ref = .{ .doc_node = node_id } } }); - var inline_seq: Array(LayoutNode.Id) = .{}; - defer inline_seq.deinit(self.allocator); - - // Iterate over the DOM children converting them into layout nodes on the - // fly. Inline children are grouped so that a single anonymous wrapper can - // be inserted before we append any block level node. - for (children) |child| { - const child_is_inline = nodeIsInline(tree, child); - const maybe_child = try self.build(tree, child); - if (maybe_child == null) continue; - const l_id = maybe_child.?; - - if (child_is_inline) { - // Accumulate inline children so they can be wrapped together. - try inline_seq.append(self.allocator, l_id); - } else { - // A block child terminates the current inline sequence. Wrap the - // accumulated inline nodes in an anonymous inline container before - // adding the block to the container. - if (inline_seq.items.len > 0) { - const anon = try self.createNode(.{ .inline_container_node = .{ .ref = .anonymous } }); - for (inline_seq.items) |iid| { - try self.appendNode(anon, iid); - } - try self.appendNode(container_id, anon); - inline_seq.clearRetainingCapacity(); - } - try self.appendNode(container_id, l_id); - } - } - - // Any remaining inline sequence becomes the trailing anonymous container. - if (inline_seq.items.len > 0) { - const anon = try self.createNode(.{ .inline_container_node = .{ .ref = .anonymous } }); - for (inline_seq.items) |iid| { - try self.appendNode(anon, iid); - } - try self.appendNode(container_id, anon); - } + // TODO return container_id; } @@ -298,7 +231,6 @@ fn getChildren(node: *LayoutNode) []const LayoutNode.Id { }; } -// Helper used by the test printer to render the tree structure. fn printNodeInternal(self: *Self, node_id: LayoutNode.Id, writer: std.io.AnyWriter, prefix: []const u8, is_root: bool, is_last: bool) !void { const node = self.getNodePtr(node_id); @@ -322,6 +254,9 @@ fn printNodeInternal(self: *Self, node_id: LayoutNode.Id, writer: std.io.AnyWrit if (inline_node.is_atomic) { try writer.print(" atomic", .{}); } + if (inline_node.continuation) |continuation| { + try writer.print(" continuation={{#{d}}}", .{continuation}); + } try writer.print(" ref=", .{}); try writeDocRef(writer, inline_node.ref); try writer.print(" children={{{d}}}]", .{inline_node.children.items.len}); @@ -367,8 +302,6 @@ pub fn printRoot(self: *Self, writer: std.io.AnyWriter) !void { } pub fn expectLayoutTree(description: []const u8, docXml: []const u8, expected: []const u8) !void { - // Utility used by the unit tests to compare the produced layout tree - // against an expected textual representation. var tree = try docFromXml(std.testing.allocator, docXml, .{}); defer tree.deinit(); @@ -397,36 +330,13 @@ test "LayoutTree" { \\ zzz \\ , - \\[block_container_node #0 ref={anon} children={2}] - \\├── [inline_container_node #1 ref={anon} children={1} lines={0}] - \\│ └── [inline_node #2 atomic={false} ref={anon} children={2}] - \\│ ├── [text_node #3] "abc" - \\│ └── [text_node #4] "def" - \\└── [text_node #5] "zzz" - ); -} - -test "fromTree inline only" { - const allocator = std.testing.allocator; - var doc = try docFromXml(allocator, "
abcdef
", .{}); - defer doc.deinit(); - - var lt = try fromTree(allocator, &doc); - defer lt.deinit(); - - var buf = std.ArrayList(u8).init(allocator); - defer buf.deinit(); - try lt.printRoot(buf.writer().any()); - - const expected = \\[inline_container_node #0 ref={doc#0} children={2} lines={0}] - \\├── [inline_node #1 ref={doc#1} children={1}] - \\│ └── [text_node #2] "abc" - \\└── [inline_node #3 ref={doc#3} children={1}] - \\ └── [text_node #4] "def" + \\├── [inline_node #1 ref={doc#1} children={2}] + \\│ ├── [text_node #2] "abc" + \\│ └── [text_node #3] "def" + \\└── [text_node #4] "zzz" \\ - ; - try std.testing.expectEqualStrings(buf.items, expected); + ); } test "deep formatting context break" { @@ -450,9 +360,9 @@ test "deep formatting context break" { try expectLayoutTree("deep formatting context break", \\Italic only italic and bold
Wow, a block!
Wow, another block!
More italic and bold text
More italic text
, - \\[block_container_node #0 ref={anon} children={2}] - \\├── [inline_container_node #1 ref={doc#0} children={1} lines={0}] - \\│ ├── [text_node #2] "Italic only " + \\[block_container_node #0 ref={doc#0} children={3}] + \\├── [inline_container_node #1 ref={anon} children={1} lines={0}] + \\│ ├── [text_node #2] "Italic only" \\│ └── [inline_node #3 ref={doc#1} children={2}] \\│ └── [text_node #4] "italic and bold" \\├── [block_container_node #5 ref={anon} children={2}] diff --git a/packages/core/src/layout/doc-from-xml.zig b/packages/core/src/layout/doc-from-xml.zig index 0693584..6a1ba9c 100644 --- a/packages/core/src/layout/doc-from-xml.zig +++ b/packages/core/src/layout/doc-from-xml.zig @@ -72,7 +72,9 @@ fn nodeFromXmlElement(tree: *Tree, element: *xml.Element, options: Options) Tree var iter = std.mem.splitScalar(u8, child.char_data, '\n'); while (iter.next()) |line| { - const text_node_id = try tree.createTextNode(if (options.trim_text) trimText(line) else line); + const text = if (options.trim_text) trimText(line) else line; + if (text.len == 0) continue; + const text_node_id = try tree.createTextNode(text); _ = try tree.appendChild(node_id, text_node_id); } } else { From ee8dba425b2ee231ecb6e141f0f33a3119aa1191 Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Thu, 22 May 2025 15:43:07 -0300 Subject: [PATCH 3/4] comment test --- packages/core/src/layout/LayoutTree.zig | 76 ++++++++++++------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/packages/core/src/layout/LayoutTree.zig b/packages/core/src/layout/LayoutTree.zig index b8e2be8..d1fd96a 100644 --- a/packages/core/src/layout/LayoutTree.zig +++ b/packages/core/src/layout/LayoutTree.zig @@ -339,41 +339,41 @@ test "LayoutTree" { ); } -test "deep formatting context break" { - // FIXME: - // example from https://webkit.org/blog/115/webcore-rendering-ii-blocks-and-inlines/ - // should output this structure - // - // Italic only italic and bold - // - // - //
- // Wow, a block! - //
- //
- // Wow, another block! - //
- //
- // - // More italic and bold text More italic text - // - try expectLayoutTree("deep formatting context break", - \\Italic only italic and bold
Wow, a block!
Wow, another block!
More italic and bold text
More italic text
- , - \\[block_container_node #0 ref={doc#0} children={3}] - \\├── [inline_container_node #1 ref={anon} children={1} lines={0}] - \\│ ├── [text_node #2] "Italic only" - \\│ └── [inline_node #3 ref={doc#1} children={2}] - \\│ └── [text_node #4] "italic and bold" - \\├── [block_container_node #5 ref={anon} children={2}] - \\│ ├── [block_container_node #6 ref={anon} children={1}] - \\│ │ └── [text_node #7] "Wow, a block!" - \\│ └── [block_container_node #8 ref={anon} children={1}] - \\│ └── [text_node #9] "Wow, another block!" - \\└── [inline_container_node #9 ref={doc#0} children={1} lines={0}] - \\ └── [inline_node #10 ref={doc#2} children={2}] - \\ ├── [text_node #11] "More italic and bold text" - \\ └── [text_node #12] "More italic text" - \\ - ); -} +// test "deep formatting context break" { +// // FIXME: +// // example from https://webkit.org/blog/115/webcore-rendering-ii-blocks-and-inlines/ +// // should output this structure +// // +// // Italic only italic and bold +// // +// // +// //
+// // Wow, a block! +// //
+// //
+// // Wow, another block! +// //
+// //
+// // +// // More italic and bold text More italic text +// // +// try expectLayoutTree("deep formatting context break", +// \\Italic only italic and bold
Wow, a block!
Wow, another block!
More italic and bold text
More italic text
+// , +// \\[block_container_node #0 ref={doc#0} children={3}] +// \\├── [inline_container_node #1 ref={anon} children={1} lines={0}] +// \\│ ├── [text_node #2] "Italic only" +// \\│ └── [inline_node #3 ref={doc#1} children={2}] +// \\│ └── [text_node #4] "italic and bold" +// \\├── [block_container_node #5 ref={anon} children={2}] +// \\│ ├── [block_container_node #6 ref={anon} children={1}] +// \\│ │ └── [text_node #7] "Wow, a block!" +// \\│ └── [block_container_node #8 ref={anon} children={1}] +// \\│ └── [text_node #9] "Wow, another block!" +// \\└── [inline_container_node #9 ref={doc#0} children={1} lines={0}] +// \\ └── [inline_node #10 ref={doc#2} children={2}] +// \\ ├── [text_node #11] "More italic and bold text" +// \\ └── [text_node #12] "More italic text" +// \\ +// ); +// } From fc4b595d248409ce2557a51572f2bb809e76d9ca Mon Sep 17 00:00:00 2001 From: Julia Ortiz Date: Thu, 22 May 2025 19:37:39 -0300 Subject: [PATCH 4/4] implement builder --- packages/core/src/layout/LayoutTree.zig | 328 +++++++++++++++++++----- 1 file changed, 257 insertions(+), 71 deletions(-) diff --git a/packages/core/src/layout/LayoutTree.zig b/packages/core/src/layout/LayoutTree.zig index d1fd96a..f64263a 100644 --- a/packages/core/src/layout/LayoutTree.zig +++ b/packages/core/src/layout/LayoutTree.zig @@ -78,9 +78,10 @@ pub const TextNode = struct { pub const InlineNode = struct { ref: DocRef, - is_atomic: bool, + is_atomic: bool = false, children: Array(LayoutNode.Id) = .{}, continuation: ?LayoutNode.Id = null, + continuationOf: ?LayoutNode.Id = null, pub fn deinit(self: *InlineNode, allocator: std.mem.Allocator) void { self.children.deinit(allocator); } @@ -108,6 +109,8 @@ pub const InlineContainerNode = struct { ref: DocRef, children: Array(LayoutNode.Id) = .{}, line_boxes: Array(LineBox) = .{}, + continuation: ?LayoutNode.Id = null, + continuationOf: ?LayoutNode.Id = null, pub fn deinit(self: *InlineContainerNode, allocator: std.mem.Allocator) void { self.children.deinit(allocator); self.line_boxes.deinit(allocator); @@ -141,7 +144,9 @@ fn nodeIsInline(tree: *DocTree, node_id: DocNodeId) bool { fn isOnlyInlineSubtree(tree: *DocTree, node_id: DocNodeId) bool { const kind = tree.getNodeKind(node_id); if (kind == .text) return true; - if (tree.getStyle(node_id).display.outside != .@"inline") return false; + const style = tree.getStyle(node_id); + if (style.display.outside != .@"inline") return false; + if (isAtomicInline(tree, node_id)) return true; for (tree.getNodeChildren(node_id)) |child| { if (!isOnlyInlineSubtree(tree, child)) return false; } @@ -150,27 +155,161 @@ fn isOnlyInlineSubtree(tree: *DocTree, node_id: DocNodeId) bool { pub fn isDisplayNone(tree: *DocTree, node_id: DocNodeId) bool { return tree.getStyle(node_id).display.outside == .none; } +pub fn isInlineFlow(tree: *DocTree, node_id: DocNodeId) bool { + const style = tree.getStyle(node_id); + return style.display.outside == .@"inline" and style.display.inside == .flow; +} +pub fn isAtomicInline(tree: *DocTree, node_id: DocNodeId) bool { + const style = tree.getStyle(node_id); + return style.display.outside == .@"inline" and style.display.inside != .flow; +} + +const BuildError = error{ + OutOfMemory, + InvalidParent, +}; +const MixedContextBuilder = struct { + layout_tree: *Self, + doc_tree: *DocTree, + root_container_id: LayoutNode.Id, + current_container_id: LayoutNode.Id, + allocator: std.mem.Allocator, + stack: Array(LayoutNode.Id) = .{}, + pub fn isCurrentContainerInline(self: *MixedContextBuilder) bool { + const current_container = self.layout_tree.getNodePtr(self.current_container_id); + return switch (current_container.data) { + .inline_container_node => true, + .block_container_node => false, + else => unreachable, + }; + } + pub fn init(allocator: std.mem.Allocator, layout_tree: *Self, doc_tree: *DocTree, root_container_id: LayoutNode.Id) !MixedContextBuilder { + return MixedContextBuilder{ + .allocator = allocator, + .layout_tree = layout_tree, + .doc_tree = doc_tree, + .root_container_id = root_container_id, + .current_container_id = root_container_id, + }; + } + pub fn getCurrentParent(self: *MixedContextBuilder) LayoutNode.Id { + return if (self.stack.items.len > 0) self.stack.items[self.stack.items.len - 1] else self.current_container_id; + } + pub fn createBlockContainer(self: *MixedContextBuilder) !LayoutNode.Id { + const id = try self.layout_tree.createNode(.{ .block_container_node = .{ .ref = .anonymous } }); + try self.layout_tree.appendNode(self.root_container_id, id); + self.current_container_id = id; + return id; + } + pub fn createInlineContainer(self: *MixedContextBuilder, parent_id: LayoutNode.Id) !LayoutNode.Id { + const id = try self.layout_tree.createNode(.{ .inline_container_node = .{ .ref = .anonymous } }); + try self.layout_tree.appendNode(parent_id, id); + self.current_container_id = id; + return id; + } + pub fn appendNode(self: *MixedContextBuilder, child_id: LayoutNode.Id) !void { + if (!self.isCurrentContainerInline()) { + try self.splitStack(); + } + try self.layout_tree.appendNode(self.getCurrentParent(), child_id); + } + pub fn splitStack(self: *MixedContextBuilder) !void { + var parent = try self.createInlineContainer(self.root_container_id); + for (0..self.stack.items.len) |i| { + const id = self.stack.items[i]; + var node = self.layout_tree.getNodePtr(id); + switch (node.data) { + .inline_container_node => { + const clone_inline_container_node_id = try self.layout_tree.createNode(.{ .inline_container_node = .{ .ref = .anonymous } }); + node = self.layout_tree.getNodePtr(id); + node.data.inline_container_node.continuation = clone_inline_container_node_id; + const clone_node = self.layout_tree.getNodePtr(clone_inline_container_node_id); + clone_node.data.inline_container_node.continuationOf = id; + + try self.layout_tree.appendNode(parent, clone_inline_container_node_id); + parent = clone_inline_container_node_id; + self.stack.items[i] = clone_inline_container_node_id; + }, + .inline_node => { + const clone_inline_node_id = try self.layout_tree.createNode(.{ .inline_node = .{ .ref = .{ .doc_node = node.data.inline_node.ref.doc_node } } }); + node = self.layout_tree.getNodePtr(id); + node.data.inline_node.continuation = clone_inline_node_id; + const clone_node = self.layout_tree.getNodePtr(clone_inline_node_id); + clone_node.data.inline_node.continuationOf = id; + try self.layout_tree.appendNode(parent, clone_inline_node_id); + parent = clone_inline_node_id; + self.stack.items[i] = clone_inline_node_id; + }, + else => unreachable, + } + } + } + pub fn build(self: *MixedContextBuilder) BuildError!void { + const children = self.doc_tree.getNodeChildren(self.root_container_id); + for (children) |child| { + if (isDisplayNone(self.doc_tree, child)) continue; + try self.buildFromNode(child); + } + } + pub fn buildFromNode(self: *MixedContextBuilder, node_id: DocNodeId) BuildError!void { + const kind = self.doc_tree.getNodeKind(node_id); + if (kind == .text) { + const text = self.doc_tree.getText(node_id).bytes.items; + const id = try self.layout_tree.createTextNode(text); + try self.appendNode(id); + + // return id; + return; + } + if (isAtomicInline(self.doc_tree, node_id)) { + const id = try self.layout_tree.buildInsideBlock(self.doc_tree, node_id); + try self.appendNode(id); + return; + } + if (isInlineFlow(self.doc_tree, node_id)) { + const id = try self.layout_tree.createNode(.{ .inline_node = .{ .ref = .{ .doc_node = node_id } } }); + try self.layout_tree.appendNode(self.getCurrentParent(), id); + // push to the stack + try self.stack.append(self.allocator, id); + defer _ = self.stack.pop(); + const children = self.doc_tree.getNodeChildren(node_id); + for (children) |child| { + if (isDisplayNone(self.doc_tree, child)) continue; + try self.buildFromNode(child); + } + return; + } + + // otherwise it's a block + const block_container_id = if (self.isCurrentContainerInline()) try self.createBlockContainer() else self.current_container_id; + const block_node = try self.layout_tree.buildInsideBlock(self.doc_tree, node_id); + try self.layout_tree.appendNode(block_container_id, block_node); + } + pub fn deinit(self: *MixedContextBuilder) void { + self.stack.deinit(self.allocator); + } +}; /// Recursively convert the DOM starting at `node_id` into layout nodes. /// Returns the id of the created layout node or `null` if the DOM node should /// not produce a layout representation. -fn build(self: *Self, tree: *DocTree, node_id: DocNodeId) !LayoutNode.Id { +fn build(self: *Self, tree: *DocTree, node_id: DocNodeId) BuildError!LayoutNode.Id { const kind = tree.getNodeKind(node_id); - // 1. Text DOM nodes map directly to layout text nodes. Empty text nodes are - // ignored. + // 1. Text DOM nodes map directly to layout text nodes. if (kind == .text) { const text = tree.getText(node_id).bytes.items; const id = try self.createTextNode(text); return id; } - const style = tree.getStyle(node_id); + if (style.display.inside != .flow) { + return self.buildInsideBlock(tree, node_id); + } - // 3. Inline-level elements produce an `InlineNode` and simply convert all of - // their children. - if (style.display.outside == .@"inline") { - const id = try self.createNode(.{ .inline_node = .{ .ref = .{ .doc_node = node_id }, .is_atomic = false } }); + // 2. Atomic inline elements produce an `InlineNode`, right now we dont have other types of atomic inline elements besides inline-block or inline-flex + if (isInlineFlow(tree, node_id)) { + const id = try self.createNode(.{ .inline_node = .{ .ref = .{ .doc_node = node_id } } }); for (tree.getNodeChildren(node_id)) |child| { if (isDisplayNone(tree, child)) continue; const child_layout_node_id = try self.build(tree, child); @@ -178,43 +317,35 @@ fn build(self: *Self, tree: *DocTree, node_id: DocNodeId) !LayoutNode.Id { } return id; } - + unreachable; +} +pub fn buildInsideBlock(self: *Self, tree: *DocTree, node_id: DocNodeId) !LayoutNode.Id { const children = tree.getNodeChildren(node_id); - var only_inline = true; - - // Determine whether every visible child is inline-level so we know what - // kind of container to create. + var only_inline_children = true; for (children) |child| { - if (tree.getStyle(child).display.outside == .none) { - continue; - } + if (isDisplayNone(tree, child)) continue; if (!isOnlyInlineSubtree(tree, child)) { - only_inline = false; - break; + only_inline_children = false; } } - - if (only_inline) { - // 4. If all children are inline, wrap them in an `InlineContainerNode` - // so they participate in the inline formatting context. - const id = try self.createNode(.{ .inline_container_node = .{ .ref = .{ .doc_node = node_id } } }); + if (only_inline_children) { + const inline_container_id = try self.createNode(.{ .inline_container_node = .{ .ref = .{ .doc_node = node_id } } }); for (children) |child| { if (isDisplayNone(tree, child)) continue; const child_layout_node_id = try self.build(tree, child); - try self.appendNode(id, child_layout_node_id); + try self.appendNode(inline_container_id, child_layout_node_id); } - return id; + return inline_container_id; } - // 5. Otherwise we create a `BlockContainerNode` and insert anonymous inline - // containers around contiguous inline children to preserve block model - // invariants. const container_id = try self.createNode(.{ .block_container_node = .{ .ref = .{ .doc_node = node_id } } }); - // TODO - + var mixed_context_builder = try MixedContextBuilder.init(self.allocator, self, tree, container_id); + try mixed_context_builder.build(); + mixed_context_builder.deinit(); + // try self.build(node_id); + // const container_id = try self.appendNode(parent_id: LayoutNode.Id, child_id: LayoutNode.Id) return container_id; } - fn writeDocRef(writer: std.io.AnyWriter, ref: DocRef) !void { switch (ref) { .anonymous => try writer.writeAll("{anon}"), @@ -254,9 +385,13 @@ fn printNodeInternal(self: *Self, node_id: LayoutNode.Id, writer: std.io.AnyWrit if (inline_node.is_atomic) { try writer.print(" atomic", .{}); } + if (inline_node.continuationOf) |continuation_of| { + try writer.print(" continuationOf={{#{d}}}", .{continuation_of}); + } if (inline_node.continuation) |continuation| { try writer.print(" continuation={{#{d}}}", .{continuation}); } + try writer.print(" ref=", .{}); try writeDocRef(writer, inline_node.ref); try writer.print(" children={{{d}}}]", .{inline_node.children.items.len}); @@ -264,11 +399,18 @@ fn printNodeInternal(self: *Self, node_id: LayoutNode.Id, writer: std.io.AnyWrit .block_container_node => |block| { try writer.print("[{s} #{d} ref=", .{ @tagName(node.data), node.id }); try writeDocRef(writer, block.ref); + try writer.print(" children={{{d}}}]", .{block.children.items.len}); }, .inline_container_node => |container| { try writer.print("[{s} #{d} ref=", .{ @tagName(node.data), node.id }); try writeDocRef(writer, container.ref); + if (container.continuationOf) |continuation_of| { + try writer.print(" continuationOf={{#{d}}}", .{continuation_of}); + } + if (container.continuation) |continuation| { + try writer.print(" continuation={{#{d}}}", .{continuation}); + } try writer.print(" children={{{d}}} lines={{{d}}}]", .{ container.children.items.len, container.line_boxes.items.len }); }, } @@ -339,41 +481,85 @@ test "LayoutTree" { ); } -// test "deep formatting context break" { -// // FIXME: -// // example from https://webkit.org/blog/115/webcore-rendering-ii-blocks-and-inlines/ -// // should output this structure -// // -// // Italic only italic and bold -// // -// // -// //
-// // Wow, a block! -// //
-// //
-// // Wow, another block! -// //
-// //
-// // -// // More italic and bold text More italic text -// // -// try expectLayoutTree("deep formatting context break", -// \\Italic only italic and bold
Wow, a block!
Wow, another block!
More italic and bold text
More italic text
-// , -// \\[block_container_node #0 ref={doc#0} children={3}] -// \\├── [inline_container_node #1 ref={anon} children={1} lines={0}] -// \\│ ├── [text_node #2] "Italic only" -// \\│ └── [inline_node #3 ref={doc#1} children={2}] -// \\│ └── [text_node #4] "italic and bold" -// \\├── [block_container_node #5 ref={anon} children={2}] -// \\│ ├── [block_container_node #6 ref={anon} children={1}] -// \\│ │ └── [text_node #7] "Wow, a block!" -// \\│ └── [block_container_node #8 ref={anon} children={1}] -// \\│ └── [text_node #9] "Wow, another block!" -// \\└── [inline_container_node #9 ref={doc#0} children={1} lines={0}] -// \\ └── [inline_node #10 ref={doc#2} children={2}] -// \\ ├── [text_node #11] "More italic and bold text" -// \\ └── [text_node #12] "More italic text" -// \\ -// ); -// } +test "deep formatting context break" { + // FIXME: + // example from https://webkit.org/blog/115/webcore-rendering-ii-blocks-and-inlines/ + // should output this structure + // + // Italic only italic and bold + // + // + //
+ // Wow, a block! + //
+ //
+ // Wow, another block! + //
+ //
+ // + // More italic and bold text More italic text + // + try expectLayoutTree("deep formatting context break", + \\ + \\ Italic only + \\ + \\ italic and bold + \\
Wow, a block!
+ \\
Wow, another block!
+ \\ More italic and bold text + \\
+ \\ More italic text + \\
+ \\ + , + \\[block_container_node #0 ref={doc#0} children={3}] + \\├── [inline_container_node #2 ref={anon} children={2} lines={0}] + \\│ ├── [text_node #1] "Italic only" + \\│ └── [inline_node #3 continuation={#12} ref={doc#2} children={1}] + \\│ └── [text_node #4] "italic and bold" + \\├── [block_container_node #5 ref={anon} children={2}] + \\│ ├── [inline_container_node #6 ref={doc#4} children={1} lines={0}] + \\│ │ └── [text_node #7] "Wow, a block!" + \\│ └── [inline_container_node #8 ref={doc#6} children={1} lines={0}] + \\│ └── [text_node #9] "Wow, another block!" + \\└── [inline_container_node #11 ref={anon} children={2} lines={0}] + \\ ├── [inline_node #12 continuationOf={#3} ref={doc#2} children={1}] + \\ │ └── [text_node #10] "More italic and bold text" + \\ └── [text_node #13] "More italic text" + \\ + ); + + try expectLayoutTree("deep formatting context break 2", + \\ + \\ Italic only + \\ + \\ italic and bold + \\
Wow, a block!
+ \\ + \\
Wow, another block!
+ \\ More italic and bold text + \\
+ \\
+ \\ More italic text + \\
+ \\ + , + \\[block_container_node #0 ref={doc#0} children={3}] + \\├── [inline_container_node #2 ref={anon} children={2} lines={0}] + \\│ ├── [text_node #1] "Italic only" + \\│ └── [inline_node #3 continuation={#13} ref={doc#2} children={2}] + \\│ ├── [text_node #4] "italic and bold" + \\│ └── [inline_node #8 continuation={#14} ref={doc#6} children={0}] + \\├── [block_container_node #5 ref={anon} children={2}] + \\│ ├── [inline_container_node #6 ref={doc#4} children={1} lines={0}] + \\│ │ └── [text_node #7] "Wow, a block!" + \\│ └── [inline_container_node #9 ref={doc#7} children={1} lines={0}] + \\│ └── [text_node #10] "Wow, another block!" + \\└── [inline_container_node #12 ref={anon} children={2} lines={0}] + \\ ├── [inline_node #13 continuationOf={#3} ref={doc#2} children={1}] + \\ │ └── [inline_node #14 continuationOf={#8} ref={doc#6} children={1}] + \\ │ └── [text_node #11] "More italic and bold text" + \\ └── [text_node #15] "More italic text" + \\ + ); +}