blitz-pdf 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +6 -6
  2. package/THIRD_PARTY_NOTICES.md +5 -2
  3. package/docs/layout-regressions.md +65 -0
  4. package/docs/pagination-regressions.md +7 -6
  5. package/package.json +1 -1
  6. package/vendor/README.md +35 -0
  7. package/vendor/blitz-dom/Cargo.toml +113 -0
  8. package/vendor/blitz-dom/LICENSE-APACHE +176 -0
  9. package/vendor/blitz-dom/LICENSE-MIT +23 -0
  10. package/vendor/blitz-dom/assets/default.css +1142 -0
  11. package/vendor/blitz-dom/assets/moz-bullet-font.otf +0 -0
  12. package/vendor/blitz-dom/src/accessibility.rs +255 -0
  13. package/vendor/blitz-dom/src/config.rs +97 -0
  14. package/vendor/blitz-dom/src/cssom.rs +971 -0
  15. package/vendor/blitz-dom/src/debug.rs +206 -0
  16. package/vendor/blitz-dom/src/document.rs +3395 -0
  17. package/vendor/blitz-dom/src/events/driver.rs +427 -0
  18. package/vendor/blitz-dom/src/events/focus.rs +40 -0
  19. package/vendor/blitz-dom/src/events/ime.rs +31 -0
  20. package/vendor/blitz-dom/src/events/keyboard.rs +174 -0
  21. package/vendor/blitz-dom/src/events/mod.rs +315 -0
  22. package/vendor/blitz-dom/src/events/pointer.rs +849 -0
  23. package/vendor/blitz-dom/src/font_metrics.rs +177 -0
  24. package/vendor/blitz-dom/src/form.rs +463 -0
  25. package/vendor/blitz-dom/src/html.rs +38 -0
  26. package/vendor/blitz-dom/src/iframe.rs +186 -0
  27. package/vendor/blitz-dom/src/layout/construct.rs +1328 -0
  28. package/vendor/blitz-dom/src/layout/damage.rs +642 -0
  29. package/vendor/blitz-dom/src/layout/inline.rs +1160 -0
  30. package/vendor/blitz-dom/src/layout/list.rs +237 -0
  31. package/vendor/blitz-dom/src/layout/mod.rs +969 -0
  32. package/vendor/blitz-dom/src/layout/paint_tree.rs +462 -0
  33. package/vendor/blitz-dom/src/layout/replaced.rs +439 -0
  34. package/vendor/blitz-dom/src/layout/table.rs +967 -0
  35. package/vendor/blitz-dom/src/layout/text_transform.rs +979 -0
  36. package/vendor/blitz-dom/src/layout/writing_mode.rs +437 -0
  37. package/vendor/blitz-dom/src/lib.rs +160 -0
  38. package/vendor/blitz-dom/src/mutator.rs +1818 -0
  39. package/vendor/blitz-dom/src/net.rs +649 -0
  40. package/vendor/blitz-dom/src/node/attributes.rs +60 -0
  41. package/vendor/blitz-dom/src/node/custom_widget.rs +178 -0
  42. package/vendor/blitz-dom/src/node/element.rs +1106 -0
  43. package/vendor/blitz-dom/src/node/mod.rs +30 -0
  44. package/vendor/blitz-dom/src/node/node.rs +2036 -0
  45. package/vendor/blitz-dom/src/node/scrollbar.rs +237 -0
  46. package/vendor/blitz-dom/src/node/serialize.rs +225 -0
  47. package/vendor/blitz-dom/src/node/stylo_data.rs +142 -0
  48. package/vendor/blitz-dom/src/node/svg.rs +265 -0
  49. package/vendor/blitz-dom/src/node/text.rs +804 -0
  50. package/vendor/blitz-dom/src/query_selector.rs +275 -0
  51. package/vendor/blitz-dom/src/resolve.rs +474 -0
  52. package/vendor/blitz-dom/src/resolved_style.rs +797 -0
  53. package/vendor/blitz-dom/src/scrolling.rs +780 -0
  54. package/vendor/blitz-dom/src/selection.rs +122 -0
  55. package/vendor/blitz-dom/src/stylo.rs +1518 -0
  56. package/vendor/blitz-dom/src/stylo_device.rs +88 -0
  57. package/vendor/blitz-dom/src/stylo_to_cursor_icon.rs +49 -0
  58. package/vendor/blitz-dom/src/stylo_to_kurbo.rs +129 -0
  59. package/vendor/blitz-dom/src/stylo_to_parley.rs +655 -0
  60. package/vendor/blitz-dom/src/traversal.rs +600 -0
  61. package/vendor/blitz-dom/src/tree.rs +136 -0
  62. package/vendor/blitz-dom/src/url.rs +63 -0
  63. package/vendor/blitz-dom/src/util.rs +256 -0
  64. package/wasm/blitz_pdf_bg.wasm +0 -0
@@ -0,0 +1,1328 @@
1
+ use blitz_traits::node_id::NodeId;
2
+ use core::str;
3
+ use std::sync::Arc;
4
+
5
+ use markup5ever::{QualName, local_name};
6
+ use parley::{
7
+ FontContext, InlineBox, InlineBoxKind, LayoutContext, StyleProperty, TreeBuilder,
8
+ WhiteSpaceCollapse,
9
+ };
10
+ use style::{
11
+ computed_values::position::T as PositionProperty,
12
+ data::ElementData as StyloElementData,
13
+ shared_lock::StylesheetGuards,
14
+ values::{
15
+ computed::{Content, ContentItem, Display, Float},
16
+ specified::box_::{DisplayInside, DisplayOutside},
17
+ },
18
+ };
19
+ use thin_vec::ThinVec;
20
+
21
+ use crate::{
22
+ BaseDocument, ElementData, Node, NodeData,
23
+ layout::damage::{
24
+ CONSTRUCT_BOX, CONSTRUCT_DESCENDENT, CONSTRUCT_FC, sort_layout_children_by_order,
25
+ },
26
+ node::{
27
+ ListItemLayout, ListItemLayoutPosition, Marker, NodeFlags, NodeKind, SpecialElementData,
28
+ TextBrush, TextInputData, TextLayout,
29
+ },
30
+ qual_name, stylo_to_parley,
31
+ traversal::{iter_children, iter_children_and_pseudos},
32
+ };
33
+
34
+ use super::{
35
+ damage::ALL_DAMAGE,
36
+ list::{BULLET_FONT_FAMILY, collect_list_item_children},
37
+ replaced::is_inline_box_element,
38
+ table::build_table_context,
39
+ text_transform::{CaseTransform, TextTransformer},
40
+ };
41
+
42
+ const DUMMY_NAME: QualName = qual_name!("div", html);
43
+
44
+ #[derive(Clone)]
45
+ pub(crate) struct ConstructionTask {
46
+ pub(crate) node_id: NodeId,
47
+ pub(crate) data: ConstructionTaskData,
48
+ }
49
+
50
+ pub(crate) struct ConstructionTaskResult {
51
+ pub(crate) node_id: NodeId,
52
+ pub(crate) data: ConstructionTaskResultData,
53
+ }
54
+
55
+ #[derive(Clone)]
56
+ pub(crate) enum ConstructionTaskData {
57
+ InlineLayout(Box<TextLayout>),
58
+ }
59
+
60
+ pub(crate) enum ConstructionTaskResultData {
61
+ InlineLayout(Box<TextLayout>),
62
+ }
63
+
64
+ /// Accumulator threaded through layout-child collection.
65
+ ///
66
+ /// `children` is the list of layout children being built up, and
67
+ /// `anonymous_block_id` is the currently-open anonymous block container (if
68
+ /// any) that wrapping children are being appended to.
69
+ #[derive(Default)]
70
+ pub(crate) struct LayoutChildren {
71
+ pub(crate) children: ThinVec<NodeId>,
72
+ pub(crate) anonymous_block_id: Option<NodeId>,
73
+ /// All anonymous blocks created while collecting these layout children.
74
+ ///
75
+ /// These are recorded on the container node so they can be deallocated the
76
+ /// next time it is reconstructed.
77
+ pub(crate) anonymous_blocks: ThinVec<NodeId>,
78
+ }
79
+
80
+ impl LayoutChildren {
81
+ /// Append a single layout child.
82
+ fn push(&mut self, child_id: NodeId, doc: &mut BaseDocument) {
83
+ self.maybe_push_anon_block(doc);
84
+ self.children.push(child_id);
85
+ }
86
+
87
+ /// Append all layout children in `slice`.
88
+ fn extend(&mut self, slice: &[NodeId], doc: &mut BaseDocument) {
89
+ self.maybe_push_anon_block(doc);
90
+ self.children.extend_from_slice(slice);
91
+ }
92
+
93
+ fn maybe_push_anon_block(&mut self, doc: &mut BaseDocument) {
94
+ fn block_is_only_whitespace(doc: &BaseDocument, node_id: NodeId) -> bool {
95
+ for child_id in doc.nodes[node_id].children.iter().copied() {
96
+ let child = &doc.nodes[child_id];
97
+ if !child.is_collapsible_whitespace_node() {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ true
103
+ }
104
+
105
+ // If anonymous block node only contains whitespace then delete it
106
+ if let Some(anon_id) = self.anonymous_block_id {
107
+ if block_is_only_whitespace(doc, anon_id) {
108
+ // Remove by identity, not pop(): hoisted display:contents
109
+ // children may have been pushed after the anon block.
110
+ if let Some(pos) = self.children.iter().rposition(|id| *id == anon_id) {
111
+ self.children.remove(pos);
112
+ }
113
+ self.anonymous_blocks.retain(|id| *id != anon_id);
114
+ doc.remove_node_from_tree(anon_id);
115
+ }
116
+ }
117
+
118
+ self.anonymous_block_id = None;
119
+ }
120
+
121
+ fn push_wrapped(
122
+ &mut self,
123
+ container_node_id: NodeId,
124
+ child_id: NodeId,
125
+ doc: &mut BaseDocument,
126
+ ) {
127
+ if self.anonymous_block_id.is_none() {
128
+ self.create_anonymous_block(container_node_id, doc);
129
+ }
130
+ doc.nodes[self.anonymous_block_id.unwrap()]
131
+ .children
132
+ .push(child_id);
133
+ }
134
+
135
+ fn create_anonymous_block(&mut self, container_node_id: NodeId, doc: &mut BaseDocument) {
136
+ let node_id = create_anonymous_box(doc, container_node_id, Display::Block);
137
+ self.children.push(node_id);
138
+ self.anonymous_block_id = Some(node_id);
139
+ self.anonymous_blocks.push(node_id);
140
+ }
141
+ }
142
+
143
+ pub(super) fn anonymous_box_style(
144
+ doc: &BaseDocument,
145
+ parent_id: NodeId,
146
+ display: Display,
147
+ ) -> style::servo_arc::Arc<style::properties::ComputedValues> {
148
+ let parent_style = doc.nodes[parent_id].primary_styles().unwrap();
149
+ let read_guard = doc.guard.read();
150
+ let guards = StylesheetGuards::same(&read_guard);
151
+ let mut style = doc.stylist.style_for_anonymous::<&Node>(
152
+ &guards,
153
+ &style::selector_parser::PseudoElement::ServoAnonymousBox,
154
+ &parent_style,
155
+ );
156
+ let box_style = style::servo_arc::Arc::make_mut(&mut style).mutate_box();
157
+ box_style.set_display(display);
158
+ box_style.original_display = display;
159
+ style
160
+ }
161
+
162
+ /// Create a layout-only wrapper without changing the DOM or selector matching.
163
+ /// Its owner must retain the ID in `anonymous_blocks` for reconstruction cleanup.
164
+ pub(super) fn create_anonymous_box(
165
+ doc: &mut BaseDocument,
166
+ parent_id: NodeId,
167
+ display: Display,
168
+ ) -> NodeId {
169
+ let style = anonymous_box_style(doc, parent_id, display);
170
+ let node_id = doc.create_node(NodeData::AnonymousBlock(Box::new(ElementData::new(
171
+ DUMMY_NAME.clone(),
172
+ Vec::new(),
173
+ ))));
174
+ let mut data = StyloElementData {
175
+ damage: ALL_DAMAGE,
176
+ ..Default::default()
177
+ };
178
+ data.styles.primary = Some(style);
179
+ data.set_restyled();
180
+ *doc.nodes[node_id]
181
+ .stylo_element_data_mut()
182
+ .ensure_init_mut() = data;
183
+ if doc.nodes[parent_id]
184
+ .flags
185
+ .contains(NodeFlags::IS_IN_DOCUMENT)
186
+ {
187
+ doc.nodes[node_id].flags.insert(NodeFlags::IS_IN_DOCUMENT);
188
+ }
189
+ doc.nodes[node_id].parent = Some(parent_id);
190
+ doc.nodes[node_id].layout_parent.set(Some(parent_id));
191
+ node_id
192
+ }
193
+
194
+ fn push_children_and_pseudos(layout_children: &mut ThinVec<NodeId>, node: &Node) {
195
+ if let Some(before) = node.before() {
196
+ layout_children.push(before);
197
+ }
198
+ layout_children.extend(node.children.iter().copied().filter(|child_id| {
199
+ let child_node = node.with(*child_id);
200
+ child_node.data.kind() != NodeKind::Comment
201
+ }));
202
+ if let Some(after) = node.after() {
203
+ layout_children.push(after);
204
+ }
205
+ }
206
+
207
+ /// Wrapping policy of the nearest non-contents ancestor container. Children
208
+ /// hoisted out of display:contents nodes must be wrapped in anonymous blocks
209
+ /// exactly as if they were direct children of that container: without this,
210
+ /// a bare text node could end up as the layout child of a block/flex/grid
211
+ /// container, which cannot be laid out (text nodes carry no style).
212
+ #[derive(Copy, Clone)]
213
+ struct WrapContext {
214
+ /// The container whose style anonymous blocks inherit from.
215
+ container_node_id: NodeId,
216
+ needs_wrap: fn(NodeKind, DisplayOutside) -> bool,
217
+ }
218
+
219
+ fn block_item_needs_wrap(child_node_kind: NodeKind, display_outside: DisplayOutside) -> bool {
220
+ child_node_kind == NodeKind::Text || display_outside == DisplayOutside::Inline
221
+ }
222
+
223
+ /// Used for flex/grid containers, and for flow containers whose in-flow
224
+ /// children are all out-of-flow (where inline elements are pushed raw, but
225
+ /// bare text still cannot be laid out directly).
226
+ fn text_item_needs_wrap(child_node_kind: NodeKind, _display_outside: DisplayOutside) -> bool {
227
+ child_node_kind == NodeKind::Text
228
+ }
229
+
230
+ /// Push a single hoisted child, recursing through display:contents nodes and
231
+ /// wrapping text/inline children per the ancestor container's `WrapContext`.
232
+ fn push_hoisted_child(
233
+ doc: &mut BaseDocument,
234
+ child_id: NodeId,
235
+ out: &mut LayoutChildren,
236
+ wrap: Option<WrapContext>,
237
+ ) {
238
+ let child = &doc.nodes[child_id];
239
+ let child_display = child.display_style().unwrap_or(Display::inline());
240
+ if matches!(child_display.inside(), DisplayInside::Contents) {
241
+ collect_layout_children_with_wrap(doc, child_id, out, wrap);
242
+ return;
243
+ }
244
+ if let Some(wrap_ctx) = wrap {
245
+ let child_node_kind = child.data.kind();
246
+ let display_outside = if child.is_or_contains_block() {
247
+ DisplayOutside::Block
248
+ } else {
249
+ child_display.outside()
250
+ };
251
+ if (wrap_ctx.needs_wrap)(child_node_kind, display_outside) {
252
+ out.push_wrapped(wrap_ctx.container_node_id, child_id, doc);
253
+ return;
254
+ }
255
+ }
256
+ out.push(child_id, doc);
257
+ }
258
+
259
+ /// Push the container's children (and ::before/::after pseudos) as layout
260
+ /// children, hoisting transparently through display:contents nodes and
261
+ /// filtering out comments and whitespace.
262
+ fn push_hoisted_children_and_pseudos(
263
+ doc: &mut BaseDocument,
264
+ container_node_id: NodeId,
265
+ out: &mut LayoutChildren,
266
+ wrap: Option<WrapContext>,
267
+ ) {
268
+ if let Some(before) = doc.nodes[container_node_id].before() {
269
+ push_hoisted_child(doc, before, out, wrap);
270
+ }
271
+ // Take children array from node to avoid borrow checker issues.
272
+ let children = std::mem::take(&mut doc.nodes[container_node_id].children);
273
+ for child_id in children.iter().copied() {
274
+ let child = &doc.nodes[child_id];
275
+ if child.data.kind() == NodeKind::Comment || child.is_collapsible_whitespace_node() {
276
+ continue;
277
+ }
278
+ push_hoisted_child(doc, child_id, out, wrap);
279
+ }
280
+ doc.nodes[container_node_id].children = children;
281
+ if let Some(after) = doc.nodes[container_node_id].after() {
282
+ push_hoisted_child(doc, after, out, wrap);
283
+ }
284
+ }
285
+
286
+ fn push_non_whitespace_children_and_pseudos(layout_children: &mut ThinVec<NodeId>, node: &Node) {
287
+ if let Some(before) = node.before() {
288
+ layout_children.push(before);
289
+ }
290
+ layout_children.extend(node.children.iter().copied().filter(|child_id| {
291
+ let child_node = node.with(*child_id);
292
+ !child_node.is_whitespace_node() && child_node.data.kind() != NodeKind::Comment
293
+ }));
294
+ if let Some(after) = node.after() {
295
+ layout_children.push(after);
296
+ }
297
+ }
298
+
299
+ /// Result of classifying the in-flow children of a flow container as
300
+ /// all-block, all-inline and/or all-out-of-flow.
301
+ struct FlowClassification {
302
+ all_block: bool,
303
+ all_inline: bool,
304
+ all_out_of_flow: bool,
305
+ has_contents: bool,
306
+ }
307
+
308
+ impl Default for FlowClassification {
309
+ fn default() -> Self {
310
+ Self {
311
+ all_block: true,
312
+ all_inline: true,
313
+ all_out_of_flow: true,
314
+ has_contents: false,
315
+ }
316
+ }
317
+ }
318
+
319
+ /// Classify a flow container's children (including its ::before/::after
320
+ /// pseudo-elements) for inline-vs-block layout, recursing transparently
321
+ /// through display:contents nodes (whose children participate in the
322
+ /// container's formatting context).
323
+ fn classify_flow_children(
324
+ doc: &BaseDocument,
325
+ container_node_id: NodeId,
326
+ classification: &mut FlowClassification,
327
+ ) {
328
+ let node = &doc.nodes[container_node_id];
329
+ // ::before/::after pseudos with display:contents are transparent for box
330
+ // generation, so their children (e.g. generated text) participate in the
331
+ // container's formatting context and vote in the classification. Pseudos
332
+ // with any other display value generate their own box, which every
333
+ // construction arm already pushes explicitly, so they cast no vote.
334
+ let pseudo_ids = node
335
+ .before()
336
+ .into_iter()
337
+ .chain(node.after())
338
+ .filter(|pe_id| {
339
+ let display = doc.nodes[*pe_id]
340
+ .display_style()
341
+ .unwrap_or(Display::inline());
342
+ matches!(display.inside(), DisplayInside::Contents)
343
+ });
344
+ let child_ids = node.children.iter().copied().chain(pseudo_ids);
345
+ for child_id in child_ids {
346
+ let child = &doc.nodes[child_id];
347
+
348
+ // Comment nodes generate no boxes and must not affect the
349
+ // inline-vs-block classification: an unstyled comment would
350
+ // default to display:inline below and force an inline
351
+ // formatting context on the container, swallowing element
352
+ // siblings into the inline layout (zero-sizing any
353
+ // out-of-flow ones).
354
+ if child.data.kind() == NodeKind::Comment {
355
+ continue;
356
+ }
357
+
358
+ // Unwraps on Text and SVG nodes
359
+ let style = child.primary_styles();
360
+ let style = style.as_ref();
361
+ let display = child.display_style().unwrap_or(Display::inline());
362
+ if matches!(display.inside(), DisplayInside::Contents) {
363
+ // Transparent for box generation: the contents node casts
364
+ // no vote itself — its children decide.
365
+ classification.has_contents = true;
366
+ classify_flow_children(doc, child_id, classification);
367
+ } else if matches!(display.inside(), DisplayInside::None) {
368
+ // display:none children generate no boxes and cast no vote.
369
+ continue;
370
+ } else {
371
+ let position = style
372
+ .map(|s| s.clone_position())
373
+ .unwrap_or(PositionProperty::Static);
374
+ let float = style.map(|s| s.clone_float()).unwrap_or(Float::None);
375
+
376
+ // Ignore nodes whose whitespace is entirely collapsed away
377
+ if child.is_collapsible_whitespace_node() {
378
+ continue;
379
+ }
380
+
381
+ // display:none children generate no boxes and cast no vote
382
+ if matches!(display.outside(), DisplayOutside::None) {
383
+ continue;
384
+ }
385
+
386
+ let is_in_flow = matches!(
387
+ position,
388
+ PositionProperty::Static | PositionProperty::Relative | PositionProperty::Sticky
389
+ ) && matches!(float, Float::None);
390
+
391
+ if !is_in_flow {
392
+ continue;
393
+ }
394
+
395
+ classification.all_out_of_flow = false;
396
+ match display.outside() {
397
+ DisplayOutside::None => {}
398
+ DisplayOutside::Block
399
+ | DisplayOutside::TableCaption
400
+ | DisplayOutside::InternalTable => classification.all_inline = false,
401
+ DisplayOutside::Inline => {
402
+ classification.all_block = false;
403
+
404
+ // We need the "complex" tree fixing when an inline contains a block
405
+ if child.is_or_contains_block() {
406
+ classification.all_inline = false;
407
+ }
408
+ }
409
+ }
410
+ }
411
+ }
412
+ }
413
+
414
+ pub(crate) fn collect_layout_children(
415
+ doc: &mut BaseDocument,
416
+ container_node_id: NodeId,
417
+ out: &mut LayoutChildren,
418
+ ) {
419
+ collect_layout_children_with_wrap(doc, container_node_id, out, None)
420
+ }
421
+
422
+ fn collect_layout_children_with_wrap(
423
+ doc: &mut BaseDocument,
424
+ container_node_id: NodeId,
425
+ out: &mut LayoutChildren,
426
+ wrap: Option<WrapContext>,
427
+ ) {
428
+ // Record the `display` the box is being constructed with
429
+ if let Some(display) = doc.nodes[container_node_id].display_style() {
430
+ *doc.nodes[container_node_id].display_constructed_as_mut() = display;
431
+ }
432
+
433
+ // Reset construction flags
434
+ // TODO: make incremental and only remove this if the element is no longer an inline root
435
+ doc.nodes[container_node_id]
436
+ .flags
437
+ .reset_construction_flags();
438
+ if let Some(element_data) = doc.nodes[container_node_id].element_data_mut() {
439
+ element_data.take_inline_layout();
440
+ }
441
+
442
+ flush_pseudo_elements(doc, container_node_id);
443
+
444
+ if let Some(el) = doc.nodes[container_node_id].data.downcast_element() {
445
+ // Handle text inputs
446
+ let tag_name = el.name.local.as_ref();
447
+ if matches!(tag_name, "input" | "textarea") {
448
+ let type_attr: Option<&str> = doc.nodes[container_node_id]
449
+ .data
450
+ .downcast_element()
451
+ .and_then(|el| el.attr(local_name!("type")));
452
+ if tag_name == "textarea" {
453
+ create_text_editor(doc, container_node_id, true);
454
+ return;
455
+ } else if matches!(
456
+ type_attr,
457
+ None | Some("text" | "password" | "email" | "number" | "search" | "tel" | "url")
458
+ ) {
459
+ create_text_editor(doc, container_node_id, false);
460
+ return;
461
+ } else if matches!(type_attr, Some("checkbox" | "radio")) {
462
+ create_checkbox_input(doc, container_node_id);
463
+ return;
464
+ }
465
+ }
466
+
467
+ #[cfg(feature = "svg")]
468
+ if matches!(tag_name, "svg") {
469
+ let outer_html = doc.get_node(container_node_id).unwrap().svg_source();
470
+
471
+ // Remove contruction damage from subtree
472
+ doc.iter_subtree_mut(container_node_id, |id: NodeId, doc: &mut BaseDocument| {
473
+ doc.nodes[id].remove_damage(CONSTRUCT_BOX | CONSTRUCT_DESCENDENT | CONSTRUCT_FC);
474
+ });
475
+
476
+ match crate::util::parse_svg_image(outer_html.as_bytes()) {
477
+ Ok(svg) => {
478
+ doc.get_node_mut(container_node_id)
479
+ .unwrap()
480
+ .element_data_mut()
481
+ .unwrap()
482
+ .special_data =
483
+ SpecialElementData::Image(Box::new(crate::node::ImageData::Svg(svg)));
484
+ }
485
+ Err(err) => {
486
+ #[cfg(feature = "tracing")]
487
+ tracing::warn!(
488
+ node_id = ?container_node_id,
489
+ html = outer_html,
490
+ error = ?err,
491
+ "SVG parse failed",
492
+ );
493
+ #[cfg(not(feature = "tracing"))]
494
+ let _ = err;
495
+ }
496
+ };
497
+ return;
498
+ }
499
+
500
+ //Only ol tags have start and reversed attributes
501
+ let (mut index, reversed) = if tag_name == "ol" {
502
+ (
503
+ el.attr_parsed(local_name!("start"))
504
+ .map(|start: usize| start - 1)
505
+ .unwrap_or(0),
506
+ el.attr_parsed(local_name!("reversed")).unwrap_or(false),
507
+ )
508
+ } else {
509
+ (0, false)
510
+ };
511
+ collect_list_item_children(doc, &mut index, reversed, container_node_id);
512
+ }
513
+
514
+ // Skip further construction if the node has no children or psuedo-children
515
+ {
516
+ let node = &doc.nodes[container_node_id];
517
+ if node.children.is_empty() && node.before().is_none() && node.after().is_none() {
518
+ return;
519
+ }
520
+ }
521
+
522
+ let container_display = doc.nodes[container_node_id].display_style().unwrap_or(
523
+ match doc.nodes[container_node_id].data.kind() {
524
+ NodeKind::AnonymousBlock => Display::Block,
525
+ _ => Display::Inline,
526
+ },
527
+ );
528
+
529
+ match container_display.inside() {
530
+ DisplayInside::None => {}
531
+ DisplayInside::Contents => {
532
+ doc.nodes[container_node_id]
533
+ .remove_damage(CONSTRUCT_BOX | CONSTRUCT_DESCENDENT | CONSTRUCT_FC);
534
+ // display:contents is transparent for box generation: hoist the
535
+ // children THEMSELVES (not their layout children) into the
536
+ // parent, recursing only through nested contents nodes.
537
+ push_hoisted_children_and_pseudos(doc, container_node_id, out, wrap);
538
+ }
539
+ DisplayInside::Flex | DisplayInside::Grid => {
540
+ // ::before/::after pseudos must be checked too: a pseudo with
541
+ // display:contents hoists its text content into the container.
542
+ let container = &doc.nodes[container_node_id];
543
+ let has_text_node_or_contents = container
544
+ .children
545
+ .iter()
546
+ .copied()
547
+ .chain(container.before())
548
+ .chain(container.after())
549
+ .map(|child_id| &doc.nodes[child_id])
550
+ .any(|child| {
551
+ let display = child.display_style().unwrap_or(Display::inline());
552
+ let node_kind = child.data.kind();
553
+ display.inside() == DisplayInside::Contents || node_kind == NodeKind::Text
554
+ });
555
+
556
+ if !has_text_node_or_contents {
557
+ push_non_whitespace_children_and_pseudos(
558
+ &mut out.children,
559
+ &doc.nodes[container_node_id],
560
+ );
561
+ } else {
562
+ collect_complex_layout_children(
563
+ doc,
564
+ container_node_id,
565
+ out,
566
+ true,
567
+ text_item_needs_wrap,
568
+ );
569
+ }
570
+
571
+ // Flex/grid items are laid out in order-modified document order.
572
+ sort_layout_children_by_order(&doc.nodes, &mut out.children);
573
+ }
574
+
575
+ DisplayInside::Table => {
576
+ let (table_context, tlayout_children) =
577
+ build_table_context(doc, container_node_id, &mut out.anonymous_blocks);
578
+ #[allow(clippy::arc_with_non_send_sync)]
579
+ let data = SpecialElementData::TableRoot(Arc::new(table_context));
580
+ doc.nodes[container_node_id]
581
+ .flags
582
+ .insert(NodeFlags::IS_TABLE_ROOT);
583
+ doc.nodes[container_node_id]
584
+ .data
585
+ .downcast_element_mut()
586
+ .unwrap()
587
+ .special_data = data;
588
+ if let Some(before) = doc.nodes[container_node_id].before() {
589
+ out.push(before, doc);
590
+ }
591
+ out.extend(&tlayout_children, doc);
592
+ if let Some(after) = doc.nodes[container_node_id].after() {
593
+ out.push(after, doc);
594
+ }
595
+ }
596
+
597
+ // Flow, FlowRoot and TableCell, plus internal table displays (row,
598
+ // row group, column, ...) occurring outside of a table. Blitz does
599
+ // not yet generate anonymous table wrapper boxes for the latter, so
600
+ // they are laid out as flow containers, which crucially ensures
601
+ // their text children get wrapped rather than being pushed as bare
602
+ // layout children (text nodes carry no style and cannot be laid out
603
+ // as block/flex/grid items).
604
+ _ => {
605
+ let mut classification = FlowClassification::default();
606
+ classify_flow_children(doc, container_node_id, &mut classification);
607
+
608
+ if classification.all_out_of_flow {
609
+ // Contents-transparent: a display:contents child may be
610
+ // holding the out-of-flow elements (otherwise the contents
611
+ // node itself would be pushed as a layout box).
612
+ let wrap = Some(WrapContext {
613
+ container_node_id,
614
+ needs_wrap: text_item_needs_wrap,
615
+ });
616
+ return push_hoisted_children_and_pseudos(doc, container_node_id, out, wrap);
617
+ }
618
+
619
+ // TODO: fix display:contents
620
+ if classification.all_inline {
621
+ let existing_layout = doc.nodes[container_node_id]
622
+ .element_data_mut()
623
+ .and_then(|el| el.inline_layout_data.take());
624
+ let layout = existing_layout.unwrap_or_else(|| Box::new(TextLayout::new()));
625
+
626
+ // Queue node for inline layout construction. Deferring construction of inline layouts to a
627
+ // dedicated phase allows us to multithread the expensive text shaping step.
628
+ doc.deferred_construction_nodes.push(ConstructionTask {
629
+ node_id: container_node_id,
630
+ data: ConstructionTaskData::InlineLayout(layout),
631
+ });
632
+ doc.nodes[container_node_id]
633
+ .flags
634
+ .insert(NodeFlags::IS_INLINE_ROOT);
635
+
636
+ find_inline_layout_embedded_boxes(doc, container_node_id, &mut out.children);
637
+ return;
638
+ }
639
+
640
+ // If the children are either all inline or all block then simply return the regular children
641
+ // as the layout children
642
+ if classification.all_block & !classification.has_contents {
643
+ return push_non_whitespace_children_and_pseudos(
644
+ &mut out.children,
645
+ &doc.nodes[container_node_id],
646
+ );
647
+ } else if classification.all_inline & !classification.has_contents {
648
+ return push_children_and_pseudos(&mut out.children, &doc.nodes[container_node_id]);
649
+ }
650
+
651
+ collect_complex_layout_children(
652
+ doc,
653
+ container_node_id,
654
+ out,
655
+ false,
656
+ block_item_needs_wrap,
657
+ );
658
+ }
659
+ }
660
+ }
661
+
662
+ /// Extract the text generated by a pseudo-element's `content` property
663
+ /// (only string content items are currently supported; other item types
664
+ /// are skipped).
665
+ fn pe_content_text(style: &style::properties::ComputedValues) -> Option<String> {
666
+ match &style.get_counters().content {
667
+ Content::Items(item_data) => {
668
+ let items = &item_data.items[0..item_data.alt_start];
669
+ let mut text = String::new();
670
+ let mut has_string = false;
671
+ for item in items {
672
+ // TODO: other types of content
673
+ if let ContentItem::String(owned_str) = item {
674
+ text.push_str(owned_str);
675
+ has_string = true;
676
+ }
677
+ }
678
+ has_string.then_some(text)
679
+ }
680
+ _ => None,
681
+ }
682
+ }
683
+
684
+ fn flush_pseudo_elements(doc: &mut BaseDocument, node_id: NodeId) {
685
+ let (before_style, after_style, before_node_id, after_node_id) = {
686
+ let node = &doc.nodes[node_id];
687
+
688
+ let before_node_id = node.before();
689
+ let after_node_id = node.after();
690
+
691
+ // Note: yes these are kinda backwards
692
+ let style_data = node.try_stylo_element_data().and_then(|s| s.get());
693
+ let before_style = style_data
694
+ .as_ref()
695
+ .and_then(|d| d.styles.pseudos.as_array()[1].clone());
696
+ let after_style = style_data
697
+ .as_ref()
698
+ .and_then(|d| d.styles.pseudos.as_array()[0].clone());
699
+
700
+ (before_style, after_style, before_node_id, after_node_id)
701
+ };
702
+
703
+ // Sync pseudo element
704
+ // TODO: Make incremental
705
+ for (idx, pe_style, pe_node_id) in [
706
+ (1, before_style, before_node_id),
707
+ (0, after_style, after_node_id),
708
+ ] {
709
+ // Delete psuedo element if it exists but shouldn't
710
+ if let (Some(pe_node_id), None) = (pe_node_id, &pe_style) {
711
+ doc.remove_and_drop_pe(pe_node_id);
712
+ let node = &mut doc.nodes[node_id];
713
+ node.set_pe_by_index(idx, None);
714
+ node.insert_damage(ALL_DAMAGE);
715
+ }
716
+
717
+ // Create pseudo element if it should exist but doesn't
718
+ if let (None, Some(pe_style)) = (pe_node_id, &pe_style) {
719
+ let new_node_id = doc.create_node(NodeData::AnonymousBlock(Box::new(
720
+ ElementData::new(DUMMY_NAME, Vec::new()),
721
+ )));
722
+ doc.nodes[new_node_id].parent = Some(node_id);
723
+ doc.nodes[new_node_id].layout_parent.set(Some(node_id));
724
+ if doc.nodes[node_id].flags.contains(NodeFlags::IS_IN_DOCUMENT) {
725
+ doc.nodes[new_node_id]
726
+ .flags
727
+ .insert(NodeFlags::IS_IN_DOCUMENT);
728
+ }
729
+
730
+ if let Some(text) = pe_content_text(pe_style) {
731
+ let text_node_id = doc.create_text_node(&text);
732
+ doc.nodes[text_node_id].parent = Some(new_node_id);
733
+ doc.nodes[new_node_id].children.push(text_node_id);
734
+ }
735
+
736
+ let mut element_data = StyloElementData::default();
737
+ element_data.styles.primary = Some(pe_style.clone());
738
+ element_data.set_restyled();
739
+ element_data.damage = ALL_DAMAGE;
740
+ *doc.nodes[new_node_id]
741
+ .stylo_element_data_mut()
742
+ .ensure_init_mut() = element_data;
743
+
744
+ let node = &mut doc.nodes[node_id];
745
+ node.set_pe_by_index(idx, Some(new_node_id));
746
+ node.insert_damage(ALL_DAMAGE);
747
+
748
+ doc.pending_style_image_nodes.push(new_node_id);
749
+ }
750
+
751
+ // Else: Update psuedo element
752
+ if let (Some(pe_node_id), Some(pe_style)) = (pe_node_id, pe_style) {
753
+ // Sync the pseudo-element's text node with its `content` style, which
754
+ // may have changed (e.g. `details[open] summary::after { content: ... }`).
755
+ //
756
+ // Note: this deliberately compares the text itself rather than relying on
757
+ // the style-pointer comparison below, as the pseudo-element's style may
758
+ // already have been updated by `sync_pseudo_element_styles` during the
759
+ // style traversal without the text having been updated.
760
+ let new_text = pe_content_text(&pe_style);
761
+ let existing_text_node_id = doc.nodes[pe_node_id]
762
+ .children
763
+ .first()
764
+ .copied()
765
+ .filter(|&child_id| doc.nodes[child_id].is_text_node());
766
+ match (existing_text_node_id, new_text) {
767
+ (Some(text_node_id), Some(new_text)) => {
768
+ let text_data = doc.nodes[text_node_id].text_data_mut().unwrap();
769
+ if text_data.content != new_text {
770
+ text_data.content = new_text;
771
+ doc.nodes[node_id].insert_damage(ALL_DAMAGE);
772
+ }
773
+ }
774
+ (None, Some(new_text)) => {
775
+ let text_node_id = doc.create_text_node(&new_text);
776
+ doc.nodes[text_node_id].parent = Some(pe_node_id);
777
+ doc.nodes[pe_node_id].children.push(text_node_id);
778
+ doc.nodes[node_id].insert_damage(ALL_DAMAGE);
779
+ }
780
+ (Some(text_node_id), None) => {
781
+ doc.nodes[pe_node_id]
782
+ .children
783
+ .retain(|&child_id| child_id != text_node_id);
784
+ doc.remove_node_from_tree(text_node_id);
785
+ doc.nodes[node_id].insert_damage(ALL_DAMAGE);
786
+ }
787
+ (None, None) => {}
788
+ }
789
+
790
+ let mut node_styles = doc.nodes[pe_node_id]
791
+ .try_stylo_element_data_mut()
792
+ .and_then(|s| s.get_mut());
793
+ let node_styles = &mut node_styles.as_mut().unwrap();
794
+ node_styles.damage.insert(ALL_DAMAGE);
795
+ let primary_styles = &mut node_styles.styles.primary;
796
+
797
+ if !std::ptr::eq(&**primary_styles.as_ref().unwrap(), &*pe_style) {
798
+ *primary_styles = Some(pe_style);
799
+ node_styles.set_restyled();
800
+ doc.pending_style_image_nodes.push(pe_node_id);
801
+ }
802
+ }
803
+ }
804
+ }
805
+
806
+ /// Handles the cases where there are text nodes or inline nodes that need to be wrapped in an anonymous block node
807
+ fn collect_complex_layout_children(
808
+ doc: &mut BaseDocument,
809
+ container_node_id: NodeId,
810
+ out: &mut LayoutChildren,
811
+ hide_whitespace: bool,
812
+ needs_wrap: fn(NodeKind, DisplayOutside) -> bool,
813
+ ) {
814
+ doc.iter_children_and_pseudos_mut(container_node_id, |child_id, doc| {
815
+ // Get node kind (text, element, comment, etc)
816
+ let child_node_kind = doc.nodes[child_id].data.kind();
817
+
818
+ // Get Display style. Default to inline because nodes without styles are probably text nodes
819
+ let contains_block = doc.nodes[child_id].is_or_contains_block();
820
+ let child_display = &doc.nodes[child_id]
821
+ .display_style()
822
+ .unwrap_or(Display::inline());
823
+ let display_inside = child_display.inside();
824
+ let display_outside = if contains_block {
825
+ DisplayOutside::Block
826
+ } else {
827
+ child_display.outside()
828
+ };
829
+
830
+ let is_whitespace_node = doc.nodes[child_id].is_whitespace_node();
831
+
832
+ // Skip comment nodes. Note that we do *not* skip `Display::None` nodes as they may need to be hidden.
833
+ // Taffy knows how to deal with `Display::None` children.
834
+ //
835
+ // Also hide all-whitespace flexbox children as these should be ignored
836
+ if child_node_kind == NodeKind::Comment || (hide_whitespace && is_whitespace_node) {
837
+ // return;
838
+ }
839
+ // Recurse into `Display::Contents` nodes, wrapping hoisted text and
840
+ // inline children as if they were direct children of this container
841
+ else if display_inside == DisplayInside::Contents {
842
+ let wrap = Some(WrapContext {
843
+ container_node_id,
844
+ needs_wrap,
845
+ });
846
+ collect_layout_children_with_wrap(doc, child_id, out, wrap)
847
+ }
848
+ // Push nodes that need wrapping into the current "anonymous block container".
849
+ // If there is not an open one then we create one.
850
+ else if needs_wrap(child_node_kind, display_outside) {
851
+ out.push_wrapped(container_node_id, child_id, doc);
852
+ }
853
+ // Else push the child directly (and close any open "anonymous block container")
854
+ else {
855
+ out.push(child_id, doc);
856
+ }
857
+ });
858
+
859
+ // If anonymous block node only contains whitespace then delete it, else push it
860
+ out.maybe_push_anon_block(doc);
861
+ }
862
+
863
+ fn create_text_editor(doc: &mut BaseDocument, input_element_id: NodeId, is_multiline: bool) {
864
+ let node = &mut doc.nodes[input_element_id];
865
+ let parley_style = node
866
+ .primary_styles()
867
+ .as_ref()
868
+ .map(|s| stylo_to_parley::style(node.id, s))
869
+ .unwrap_or_default();
870
+ let alignment = node
871
+ .primary_styles()
872
+ .map(|s| stylo_to_parley::text_align(s.clone_text_align()))
873
+ .unwrap_or(parley::layout::Alignment::Start);
874
+ let base_direction = node
875
+ .primary_styles()
876
+ .map(|s| stylo_to_parley::base_direction(s.clone_direction(), s.clone_unicode_bidi()))
877
+ .unwrap_or(parley::BaseDirection::Auto);
878
+
879
+ let initial_text = if is_multiline {
880
+ node.text_content()
881
+ } else {
882
+ node.attr(local_name!("value")).unwrap_or("").to_string()
883
+ };
884
+ let element = &mut node.data.downcast_element_mut().unwrap();
885
+ if !matches!(element.special_data, SpecialElementData::TextInput(_)) {
886
+ let mut text_input_data = TextInputData::new(is_multiline);
887
+ let editor = &mut text_input_data.editor;
888
+ editor.set_text(&initial_text);
889
+ element.special_data = SpecialElementData::TextInput(text_input_data);
890
+ }
891
+
892
+ let SpecialElementData::TextInput(text_input_data) = &mut element.special_data else {
893
+ unreachable!();
894
+ };
895
+
896
+ let editor = &mut text_input_data.editor;
897
+ editor.set_scale(doc.viewport.scale_f64() as f32);
898
+ editor.set_width(None);
899
+
900
+ let styles = editor.edit_styles();
901
+ styles.retain(|_| false);
902
+ styles.insert(StyleProperty::FontFamily(parley_style.font_family));
903
+ styles.insert(StyleProperty::FontSize(parley_style.font_size));
904
+ styles.insert(StyleProperty::FontWidth(parley_style.font_width));
905
+ styles.insert(StyleProperty::FontStyle(parley_style.font_style));
906
+ styles.insert(StyleProperty::FontWeight(parley_style.font_weight));
907
+ styles.insert(StyleProperty::FontVariations(parley_style.font_variations));
908
+ styles.insert(StyleProperty::FontFeatures(parley_style.font_features));
909
+ styles.insert(StyleProperty::LineHeight(parley_style.line_height));
910
+ styles.insert(StyleProperty::WordSpacing(parley_style.word_spacing));
911
+ styles.insert(StyleProperty::LetterSpacing(parley_style.letter_spacing));
912
+ styles.insert(StyleProperty::WordBreak(parley_style.word_break));
913
+ styles.insert(StyleProperty::OverflowWrap(parley_style.overflow_wrap));
914
+ styles.insert(StyleProperty::TextWrapMode(parley_style.text_wrap_mode));
915
+ styles.insert(StyleProperty::WhiteSpaceCollapse(
916
+ parley_style.white_space_collapse,
917
+ ));
918
+ styles.insert(StyleProperty::Brush(parley_style.brush));
919
+ editor.set_alignment(alignment);
920
+ editor.set_base_direction(base_direction);
921
+
922
+ editor.refresh_layout(&mut doc.font_ctx.lock().unwrap(), &mut doc.layout_ctx);
923
+ }
924
+
925
+ fn create_checkbox_input(doc: &mut BaseDocument, input_element_id: NodeId) {
926
+ let node = &mut doc.nodes[input_element_id];
927
+
928
+ let element = &mut node.data.downcast_element_mut().unwrap();
929
+ if !matches!(element.special_data, SpecialElementData::CheckboxInput(_)) {
930
+ let checked = element.has_attr(local_name!("checked"));
931
+ element.special_data = SpecialElementData::CheckboxInput(checked);
932
+ element.set_checkbox_input_checked(checked);
933
+ }
934
+ }
935
+
936
+ /// Find and return the "layout_children" (inline boxes) for an inline layout
937
+ /// without actually constructing the layout. This allows us to defer the expensive
938
+ /// construction of the Parley layout (which invokes text shaping) to a paralell phase.
939
+ pub(crate) fn find_inline_layout_embedded_boxes(
940
+ doc: &mut BaseDocument,
941
+ inline_context_root_node_id: NodeId,
942
+ layout_children: &mut ThinVec<NodeId>,
943
+ ) {
944
+ flush_inline_pseudos_recursive(doc, inline_context_root_node_id);
945
+
946
+ iter_children_and_pseudos!(doc.nodes[inline_context_root_node_id], |child_id| {
947
+ find_inline_layout_embedded_boxes_recursive(
948
+ &mut doc.nodes,
949
+ inline_context_root_node_id,
950
+ child_id,
951
+ layout_children,
952
+ );
953
+ });
954
+
955
+ fn flush_inline_pseudos_recursive(doc: &mut BaseDocument, node_id: NodeId) {
956
+ doc.iter_children_mut(node_id, |child_id, doc| {
957
+ flush_pseudo_elements(doc, child_id);
958
+ let display = doc.nodes[node_id]
959
+ .display_style()
960
+ .unwrap_or(Display::inline());
961
+ let do_recurse = match (display.outside(), display.inside()) {
962
+ (DisplayOutside::None, DisplayInside::Contents) => true,
963
+ (DisplayOutside::Inline, DisplayInside::Flow) => true,
964
+ (_, _) => false,
965
+ };
966
+ if do_recurse {
967
+ flush_inline_pseudos_recursive(doc, child_id);
968
+ }
969
+ });
970
+ }
971
+
972
+ fn find_inline_layout_embedded_boxes_recursive(
973
+ nodes: &mut crate::NodeTree,
974
+ parent_id: NodeId,
975
+ node_id: NodeId,
976
+ layout_children: &mut ThinVec<NodeId>,
977
+ ) {
978
+ let node = &mut nodes[node_id];
979
+
980
+ // Set layout_parent for node.
981
+ node.layout_parent.set(Some(parent_id));
982
+
983
+ match &node.data {
984
+ NodeData::Element(element_data) | NodeData::AnonymousBlock(element_data) => {
985
+ // if the input type is hidden, hide it
986
+ if *element_data.name.local == *"input" {
987
+ if let Some("hidden") = element_data.attr(local_name!("type")) {
988
+ return;
989
+ }
990
+ }
991
+
992
+ let display = node.display_style().unwrap_or(Display::inline());
993
+
994
+ match (display.outside(), display.inside()) {
995
+ (DisplayOutside::None, DisplayInside::None) => {
996
+ node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
997
+ }
998
+ (DisplayOutside::None, DisplayInside::Contents) => {
999
+ node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
1000
+ iter_children!(nodes[node_id], |child_id| {
1001
+ find_inline_layout_embedded_boxes_recursive(
1002
+ nodes,
1003
+ parent_id,
1004
+ child_id,
1005
+ layout_children,
1006
+ );
1007
+ });
1008
+ }
1009
+ (DisplayOutside::Inline, DisplayInside::Flow) => {
1010
+ let tag_name = &element_data.name.local;
1011
+
1012
+ if is_inline_box_element(tag_name) {
1013
+ layout_children.push(node_id);
1014
+ } else if *tag_name == local_name!("br") {
1015
+ node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
1016
+ } else {
1017
+ node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
1018
+ iter_children_and_pseudos!(nodes[node_id], |child_id| {
1019
+ find_inline_layout_embedded_boxes_recursive(
1020
+ nodes,
1021
+ node_id,
1022
+ child_id,
1023
+ layout_children,
1024
+ );
1025
+ });
1026
+ }
1027
+ }
1028
+ // Inline box
1029
+ (_, _) => {
1030
+ layout_children.push(node_id);
1031
+ }
1032
+ };
1033
+ }
1034
+ NodeData::Comment { .. } | NodeData::Text(_) => {
1035
+ node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
1036
+ }
1037
+ NodeData::Document(_) => unreachable!(),
1038
+ }
1039
+ }
1040
+ }
1041
+
1042
+ pub(crate) fn build_inline_layout_into(
1043
+ nodes: &crate::NodeTree,
1044
+ layout_ctx: &mut LayoutContext<TextBrush>,
1045
+ font_ctx: &mut FontContext,
1046
+ text_layout: &mut TextLayout,
1047
+ scale: f32,
1048
+ inline_context_root_node_id: NodeId,
1049
+ ) {
1050
+ // Get the inline context's root node's text styles
1051
+ let root_node = &nodes[inline_context_root_node_id];
1052
+ let root_node_style = root_node.primary_styles().or_else(|| {
1053
+ root_node
1054
+ .parent
1055
+ .and_then(|parent_id| nodes[parent_id].primary_styles())
1056
+ });
1057
+
1058
+ let mut parley_style = root_node_style
1059
+ .as_ref()
1060
+ .map(|s| stylo_to_parley::style(inline_context_root_node_id, s))
1061
+ .unwrap_or_else(|| parley::TextStyle {
1062
+ white_space_collapse: WhiteSpaceCollapse::Collapse,
1063
+ ..Default::default()
1064
+ });
1065
+
1066
+ // The formatting-context root's vertical-align positions its box in its
1067
+ // parent (for example a middle-aligned table cell), not its own text runs.
1068
+ // Passing it to Parley shifts the root against its own strut and inflates
1069
+ // every line. Descendant inline spans still use their own vertical-align.
1070
+ parley_style.vertical_align = parley::VerticalAlign::BASELINE;
1071
+
1072
+ // Create a parley tree builder
1073
+ let mut builder = layout_ctx.tree_builder(font_ctx, scale, true, &parley_style);
1074
+ if let Some(style) = root_node_style.as_deref() {
1075
+ builder.set_base_direction(stylo_to_parley::base_direction(
1076
+ style.clone_direction(),
1077
+ style.clone_unicode_bidi(),
1078
+ ));
1079
+ }
1080
+
1081
+ let text_transform = root_node_style
1082
+ .as_deref()
1083
+ .map(|s| CaseTransform::from_style(s))
1084
+ .unwrap_or(CaseTransform::NONE);
1085
+
1086
+ // Render position-inside list items
1087
+ if let Some(ListItemLayout {
1088
+ marker,
1089
+ position: ListItemLayoutPosition::Inside,
1090
+ }) = root_node
1091
+ .element_data()
1092
+ .and_then(|el| el.list_item_data.as_deref())
1093
+ {
1094
+ match marker {
1095
+ // Bullet glyphs live in the bundled bullet font. The position-outside
1096
+ // path already asks for it; without the same span here a marker like
1097
+ // disclosure-closed (U+25B8) falls back to the element's own font and
1098
+ // renders as a missing glyph.
1099
+ Marker::Char(char) => {
1100
+ let mut marker_style = parley_style.clone();
1101
+ marker_style.font_family = BULLET_FONT_FAMILY.into();
1102
+ builder.push_style_span(marker_style);
1103
+ builder.push_text(&format!("{char} "));
1104
+ builder.pop_style_span();
1105
+ }
1106
+ Marker::String(str) => builder.push_text(str),
1107
+ }
1108
+ };
1109
+ // The marker is a separate box, so words in the content don't continue from it.
1110
+ let mut text_transformer = TextTransformer::default();
1111
+ text_transformer.word_break(&builder);
1112
+
1113
+ if let Some(before_id) = root_node.before() {
1114
+ build_inline_layout_recursive(
1115
+ &mut builder,
1116
+ &mut text_transformer,
1117
+ nodes,
1118
+ before_id,
1119
+ &text_transform,
1120
+ );
1121
+ }
1122
+ for child_id in root_node.children.iter().copied() {
1123
+ build_inline_layout_recursive(
1124
+ &mut builder,
1125
+ &mut text_transformer,
1126
+ nodes,
1127
+ child_id,
1128
+ &text_transform,
1129
+ );
1130
+ }
1131
+ if let Some(after_id) = root_node.after() {
1132
+ build_inline_layout_recursive(
1133
+ &mut builder,
1134
+ &mut text_transformer,
1135
+ nodes,
1136
+ after_id,
1137
+ &text_transform,
1138
+ );
1139
+ }
1140
+
1141
+ text_layout.text = builder.build_into(&mut text_layout.layout);
1142
+ return;
1143
+
1144
+ fn build_inline_layout_recursive(
1145
+ builder: &mut TreeBuilder<TextBrush>,
1146
+ text_transformer: &mut TextTransformer,
1147
+ nodes: &crate::NodeTree,
1148
+ node_id: NodeId,
1149
+ parent_text_transform: &CaseTransform,
1150
+ ) {
1151
+ let node = &nodes[node_id];
1152
+
1153
+ let style = node.primary_styles();
1154
+ let style = style.as_ref();
1155
+
1156
+ let text_transform = style
1157
+ .map(|s| CaseTransform::from_style(s))
1158
+ .unwrap_or(CaseTransform::NONE);
1159
+
1160
+ match &node.data {
1161
+ NodeData::Element(element_data) | NodeData::AnonymousBlock(element_data) => {
1162
+ // if the input type is hidden, hide it
1163
+ if *element_data.name.local == *"input" {
1164
+ if let Some("hidden") = element_data.attr(local_name!("type")) {
1165
+ return;
1166
+ }
1167
+ }
1168
+
1169
+ let display = node.display_style().unwrap_or(Display::inline());
1170
+ let position = style
1171
+ .map(|s| s.clone_position())
1172
+ .unwrap_or(PositionProperty::Static);
1173
+ let float = style.map(|s| s.clone_float()).unwrap_or(Float::None);
1174
+ let box_kind = if position.is_absolutely_positioned() {
1175
+ InlineBoxKind::OutOfFlow
1176
+ } else if float.is_floating() {
1177
+ InlineBoxKind::CustomOutOfFlow
1178
+ } else {
1179
+ InlineBoxKind::InFlow
1180
+ };
1181
+
1182
+ match (display.outside(), display.inside()) {
1183
+ (DisplayOutside::None, DisplayInside::None) => {
1184
+ // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
1185
+ }
1186
+ (DisplayOutside::None, DisplayInside::Contents) => {
1187
+ let whitespace = style.map(|s| {
1188
+ [
1189
+ parley::StyleProperty::WhiteSpaceCollapse(
1190
+ stylo_to_parley::white_space_collapse(
1191
+ s.clone_white_space_collapse(),
1192
+ ),
1193
+ ),
1194
+ parley::StyleProperty::TextWrapMode(
1195
+ stylo_to_parley::text_wrap_mode(s.clone_text_wrap_mode()),
1196
+ ),
1197
+ ]
1198
+ });
1199
+ builder.push_style_modification_span(
1200
+ whitespace
1201
+ .as_ref()
1202
+ .map_or(&[][..], |styles| styles.as_slice()),
1203
+ );
1204
+ for child_id in node.children.iter().copied() {
1205
+ // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
1206
+ build_inline_layout_recursive(
1207
+ builder,
1208
+ text_transformer,
1209
+ nodes,
1210
+ child_id,
1211
+ &text_transform,
1212
+ );
1213
+ }
1214
+ builder.pop_style_span();
1215
+ }
1216
+ (DisplayOutside::Inline, DisplayInside::Flow) => {
1217
+ let tag_name = &element_data.name.local;
1218
+
1219
+ if is_inline_box_element(tag_name) {
1220
+ if box_kind == InlineBoxKind::InFlow {
1221
+ text_transformer.word_break(builder);
1222
+ }
1223
+ builder.push_inline_box(InlineBox {
1224
+ id: node_id.as_u64(),
1225
+ kind: box_kind,
1226
+ // Overridden by push_inline_box method
1227
+ index: 0,
1228
+ // Width and height are set during layout
1229
+ width: 0.0,
1230
+ height: 0.0,
1231
+ baseline: None,
1232
+ vertical_align: node
1233
+ .primary_styles()
1234
+ .map(|s| stylo_to_parley::vertical_align(&s))
1235
+ .unwrap_or_default(),
1236
+ });
1237
+ } else if *tag_name == local_name!("br") {
1238
+ // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
1239
+ // TODO: update span id for br spans
1240
+ builder.push_style_modification_span(&[
1241
+ parley::StyleProperty::WhiteSpaceCollapse(
1242
+ WhiteSpaceCollapse::Preserve,
1243
+ ),
1244
+ ]);
1245
+ builder.push_text("\n");
1246
+ builder.pop_style_span();
1247
+ text_transformer.word_break(builder);
1248
+ } else {
1249
+ // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
1250
+ let style = node
1251
+ .primary_styles()
1252
+ .map(|s| stylo_to_parley::style(node.id, &s))
1253
+ .unwrap_or_else(|| parley::TextStyle {
1254
+ white_space_collapse: WhiteSpaceCollapse::Collapse,
1255
+ ..Default::default()
1256
+ });
1257
+
1258
+ builder.push_style_span(style);
1259
+
1260
+ if let Some(before_id) = node.before() {
1261
+ build_inline_layout_recursive(
1262
+ builder,
1263
+ text_transformer,
1264
+ nodes,
1265
+ before_id,
1266
+ &text_transform,
1267
+ );
1268
+ }
1269
+
1270
+ for child_id in node.children.iter().copied() {
1271
+ build_inline_layout_recursive(
1272
+ builder,
1273
+ text_transformer,
1274
+ nodes,
1275
+ child_id,
1276
+ &text_transform,
1277
+ );
1278
+ }
1279
+ if let Some(after_id) = node.after() {
1280
+ build_inline_layout_recursive(
1281
+ builder,
1282
+ text_transformer,
1283
+ nodes,
1284
+ after_id,
1285
+ &text_transform,
1286
+ );
1287
+ }
1288
+
1289
+ builder.pop_style_span();
1290
+ }
1291
+ }
1292
+ // Inline box
1293
+ (_, _) => {
1294
+ if box_kind == InlineBoxKind::InFlow {
1295
+ text_transformer.word_break(builder);
1296
+ }
1297
+ builder.push_inline_box(InlineBox {
1298
+ id: node_id.as_u64(),
1299
+ kind: box_kind,
1300
+ // Overridden by push_inline_box method
1301
+ index: 0,
1302
+ // Width and height are set during layout
1303
+ width: 0.0,
1304
+ height: 0.0,
1305
+ baseline: None,
1306
+ vertical_align: node
1307
+ .primary_styles()
1308
+ .map(|s| stylo_to_parley::vertical_align(&s))
1309
+ .unwrap_or_default(),
1310
+ });
1311
+ }
1312
+ };
1313
+ }
1314
+ NodeData::Text(data) => {
1315
+ // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
1316
+ // dbg!(&data.content);
1317
+
1318
+ let text =
1319
+ text_transformer.transform(&data.content, parent_text_transform, builder);
1320
+ builder.push_text(text);
1321
+ }
1322
+ NodeData::Comment { .. } => {
1323
+ // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
1324
+ }
1325
+ NodeData::Document(_) => unreachable!(),
1326
+ }
1327
+ }
1328
+ }