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 boldWow, 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 boldWow, 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 boldWow, 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 boldWow, 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"
+ \\
+ );
+}