blitz-pdf 0.1.2 → 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 +26 -0
  2. package/THIRD_PARTY_NOTICES.md +5 -2
  3. package/docs/layout-regressions.md +65 -0
  4. package/docs/pagination-regressions.md +89 -0
  5. package/package.json +2 -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,967 @@
1
+ use blitz_traits::node_id::NodeId;
2
+ use std::{ops::Range, sync::Arc};
3
+ use style::logical_geometry::WritingMode;
4
+ use style::properties::ComputedValues;
5
+
6
+ use atomic_refcell::AtomicRefCell;
7
+ use markup5ever::local_name;
8
+ use style::properties::style_structs::Border;
9
+ use style::servo_arc::Arc as ServoArc;
10
+ use style::typed_om::NumericBaseType;
11
+ use style::values::computed::calc::CalcPercentageLeaf;
12
+ use style::values::computed::length_percentage::{
13
+ CalcLengthPercentage, CalcNode, ComputedLeaf, Unpacked as UnpackedLengthPercentage,
14
+ };
15
+ use style::values::computed::{Display, Length, LengthPercentage};
16
+ use style::values::generics::Optional;
17
+ use style::values::specified::box_::{DisplayInside, DisplayOutside};
18
+ use style::{
19
+ Atom, computed_values::border_collapse::T as BorderCollapse,
20
+ computed_values::table_layout::T as TableLayout, values::computed::BorderStyle,
21
+ };
22
+ use style_traits::values::specified::AllowedNumericType;
23
+ use taffy::{
24
+ DetailedGridInfo, LayoutPartialTree as _, ResolveOrZero, TrackSizingFunction, style_helpers,
25
+ };
26
+
27
+ use crate::BaseDocument;
28
+ use crate::layout::LayoutPassState;
29
+ use thin_vec::ThinVec;
30
+
31
+ use super::construct::create_anonymous_box;
32
+ use super::damage::{CONSTRUCT_BOX, CONSTRUCT_DESCENDENT, CONSTRUCT_FC};
33
+ use super::resolve_calc_value;
34
+
35
+ /// CSS 2.1 §17.2.1: consecutive improper table children share an anonymous
36
+ /// row, and consecutive non-cell row children share an anonymous cell.
37
+ /// Keep these boxes out of the DOM so selectors still see the author's tree.
38
+ fn table_children(
39
+ doc: &mut BaseDocument,
40
+ parent_id: NodeId,
41
+ children: &[NodeId],
42
+ anonymous: &mut ThinVec<NodeId>,
43
+ ) -> Vec<NodeId> {
44
+ fn flatten(doc: &BaseDocument, children: &[NodeId], output: &mut Vec<NodeId>) {
45
+ for &id in children {
46
+ let node = &doc.nodes[id];
47
+ if node.data.kind() == crate::node::NodeKind::Comment {
48
+ continue;
49
+ }
50
+ if node
51
+ .display_style()
52
+ .is_some_and(|d| d.inside() == DisplayInside::Contents)
53
+ {
54
+ flatten(doc, &node.children, output);
55
+ } else {
56
+ output.push(id);
57
+ }
58
+ }
59
+ }
60
+ let is_row = doc.nodes[parent_id]
61
+ .display_style()
62
+ .is_some_and(|d| d.inside() == DisplayInside::TableRow);
63
+ let mut flat = Vec::new();
64
+ flatten(doc, children, &mut flat);
65
+ let mut output = Vec::new();
66
+ let mut pending = Vec::new();
67
+ let flush = |doc: &mut BaseDocument,
68
+ pending: &mut Vec<NodeId>,
69
+ output: &mut Vec<NodeId>,
70
+ anonymous: &mut ThinVec<NodeId>| {
71
+ if pending
72
+ .iter()
73
+ .any(|id| !doc.nodes[*id].is_collapsible_whitespace_node())
74
+ {
75
+ let display = if is_row {
76
+ Display::TableCell
77
+ } else {
78
+ Display::TableRow
79
+ };
80
+ let id = create_anonymous_box(doc, parent_id, display);
81
+ doc.nodes[id].children.extend(pending.drain(..));
82
+ anonymous.push(id);
83
+ output.push(id);
84
+ } else {
85
+ pending.clear();
86
+ }
87
+ };
88
+ for id in flat {
89
+ let node = &doc.nodes[id];
90
+ let display = node.display_style();
91
+ let is_oof = node
92
+ .primary_styles()
93
+ .is_some_and(|s| s.clone_position().is_absolutely_positioned());
94
+ // Hidden and out-of-flow siblings do not split a consecutive run of
95
+ // in-flow content into separate anonymous cells (and separate margins).
96
+ if is_oof || display.is_some_and(|d| d.outside() == DisplayOutside::None) {
97
+ output.push(id);
98
+ continue;
99
+ }
100
+ let proper = display.is_some_and(|d| {
101
+ if is_row {
102
+ matches!(
103
+ d.inside(),
104
+ DisplayInside::TableCell
105
+ | DisplayInside::TableColumn
106
+ | DisplayInside::TableColumnGroup
107
+ )
108
+ } else {
109
+ d.outside() == DisplayOutside::TableCaption
110
+ || matches!(
111
+ d.inside(),
112
+ DisplayInside::TableRow
113
+ | DisplayInside::TableRowGroup
114
+ | DisplayInside::TableHeaderGroup
115
+ | DisplayInside::TableFooterGroup
116
+ | DisplayInside::TableColumn
117
+ | DisplayInside::TableColumnGroup
118
+ )
119
+ }
120
+ });
121
+ if proper {
122
+ flush(doc, &mut pending, &mut output, anonymous);
123
+ output.push(id);
124
+ } else {
125
+ pending.push(id);
126
+ }
127
+ }
128
+ flush(doc, &mut pending, &mut output, anonymous);
129
+ output
130
+ }
131
+
132
+ pub struct TableTreeWrapper<'a, 'doc> {
133
+ pub(crate) doc: &'a mut LayoutPassState<'doc>,
134
+ pub(crate) ctx: Arc<TableContext>,
135
+ }
136
+
137
+ // Deliberately not `Clone`: `style` may hold raw pointers into `calc_values`.
138
+ #[derive(Debug)]
139
+ pub struct TableContext {
140
+ pub style: taffy::Style<Atom>,
141
+ pub cells: Vec<TableCell>,
142
+ /// Absolutely positioned descendants of the table's row groups and rows (and of the
143
+ /// table itself). They take no part in the table grid.
144
+ pub oof_children: Vec<(NodeId, taffy::Position)>,
145
+ pub rows: Vec<TableRow>,
146
+ pub columns: Vec<TableColumn>,
147
+ pub computed_grid_info: AtomicRefCell<Option<DetailedGridInfo<Atom>>>,
148
+ pub border_style: Option<ServoArc<Border>>,
149
+ pub border_collapse: BorderCollapse,
150
+ /// Backing storage for `calc()` track sizes synthesised by the table layout code.
151
+ /// Taffy stores calc values as raw pointers, so these must outlive the `style`.
152
+ #[allow(dead_code)]
153
+ calc_values: Vec<LengthPercentage>,
154
+ }
155
+
156
+ // #[derive(Debug, Clone, Eq, PartialEq)]
157
+ // pub enum TableItemKind {
158
+ // Row,
159
+ // Cell,
160
+ // }
161
+
162
+ #[derive(Debug, Clone)]
163
+ pub struct TableCell {
164
+ // kind: TableItemKind,
165
+ pub node_id: NodeId,
166
+ /// Index (into `TableContext::rows`) of the row the cell originates in
167
+ pub row: u16,
168
+ /// Index of the column the cell originates in
169
+ pub column: u16,
170
+ style: taffy::Style<Atom>,
171
+ }
172
+
173
+ /// Tracks the current column position while walking the table's cells, so that
174
+ /// cells are assigned the same columns Taffy's dense auto-placement will give them
175
+ /// (skipping columns still occupied by rowspan cells from earlier rows).
176
+ #[derive(Debug, Default)]
177
+ struct ColumnCursor {
178
+ /// The column the next cell in the current row will be placed in
179
+ col: u16,
180
+ /// The total number of columns seen so far
181
+ num_columns: u16,
182
+ /// For each column, the number of further rows it is occupied by a rowspan cell
183
+ rowspans: Vec<u16>,
184
+ }
185
+
186
+ impl ColumnCursor {
187
+ fn start_row(&mut self) {
188
+ self.col = 0;
189
+ for remaining in self.rowspans.iter_mut() {
190
+ *remaining = remaining.saturating_sub(1);
191
+ }
192
+ }
193
+
194
+ /// Advance past occupied columns, returning the column of the next cell
195
+ fn next_free(&mut self) -> u16 {
196
+ while self.rowspans.get(self.col as usize).is_some_and(|r| *r > 0) {
197
+ self.col += 1;
198
+ }
199
+ self.col
200
+ }
201
+
202
+ fn place(&mut self, colspan: u16, rowspan: u16) {
203
+ let end = self.col + colspan;
204
+ if self.rowspans.len() < end as usize {
205
+ self.rowspans.resize(end as usize, 0);
206
+ }
207
+ for remaining in &mut self.rowspans[self.col as usize..end as usize] {
208
+ *remaining = rowspan - 1;
209
+ }
210
+ self.col = end;
211
+ self.num_columns = self.num_columns.max(end);
212
+ }
213
+ }
214
+
215
+ #[derive(Debug, Clone)]
216
+ pub struct TableColumn {
217
+ pub node_id: NodeId,
218
+ /// The column group (if any) that contains the column
219
+ pub group: Option<NodeId>,
220
+ }
221
+
222
+ #[derive(Debug, Clone)]
223
+ pub struct TableRow {
224
+ // kind: TableItemKind,
225
+ pub node_id: NodeId,
226
+ /// The row group (if any) that contains the row
227
+ pub group: Option<NodeId>,
228
+ /// The row's specified `height`, if it is a length
229
+ pub height: Option<f32>,
230
+ /// Whether the row or any cell originating in it (and spanning only this row) has a
231
+ /// specified length `height`. Such rows do not receive excess table height when there
232
+ /// are unconstrained rows to receive it instead.
233
+ pub is_constrained: bool,
234
+ }
235
+
236
+ /// The used width of one border side: border widths are not adjusted for
237
+ /// border-style in computed styles, so a border with `none`/`hidden` style
238
+ /// must be treated as zero-width.
239
+ /// The writing mode the table's grid runs in (the table's own)
240
+ #[cfg_attr(not(feature = "writing-mode"), allow(unused_variables))]
241
+ fn table_wm(doc: &BaseDocument, table_root_node_id: NodeId) -> WritingMode {
242
+ #[cfg(feature = "writing-mode")]
243
+ {
244
+ if doc.try_root_element().map(|root| root.id) == Some(table_root_node_id) {
245
+ doc.root_layout_wm(table_root_node_id)
246
+ } else {
247
+ doc.nodes[table_root_node_id].writing_mode()
248
+ }
249
+ }
250
+ #[cfg(not(feature = "writing-mode"))]
251
+ {
252
+ WritingMode::empty()
253
+ }
254
+ }
255
+
256
+ /// Convert a table part's style into the table's writing mode
257
+ fn table_taffy_style(style: &ComputedValues, table_wm: WritingMode) -> taffy::Style<Atom> {
258
+ stylo_taffy::to_taffy_style_in(style, table_wm)
259
+ }
260
+
261
+ fn side_width(width: app_units::Au, style: BorderStyle) -> f32 {
262
+ if style.none_or_hidden() {
263
+ 0.0
264
+ } else {
265
+ width.to_f32_px()
266
+ }
267
+ }
268
+
269
+ /// Build a `calc(<percent> + <length>)` track sizing function. The calc value is
270
+ /// boxed and stored in `calc_values` so that the raw pointer Taffy holds stays valid.
271
+ fn percent_plus_length(
272
+ calc_values: &mut Vec<LengthPercentage>,
273
+ percent: f32,
274
+ length: f32,
275
+ ) -> TrackSizingFunction {
276
+ let node = CalcNode::Sum(
277
+ vec![
278
+ CalcNode::Leaf(ComputedLeaf::Percentage(CalcPercentageLeaf::new(
279
+ percent,
280
+ Optional::Some(NumericBaseType::Length),
281
+ ))),
282
+ CalcNode::Leaf(ComputedLeaf::Length(Length::new(length))),
283
+ ]
284
+ .into(),
285
+ );
286
+ let value = LengthPercentage::new_calc(node, AllowedNumericType::NonNegative);
287
+ let dim = match value.unpack() {
288
+ UnpackedLengthPercentage::Calc(calc) => {
289
+ let ptr = calc as *const CalcLengthPercentage as *const ();
290
+ // SAFETY: the pointer targets the heap allocation owned by `value`, which
291
+ // is kept alive by `calc_values` for as long as the TableContext exists.
292
+ unsafe { taffy::Dimension::from_raw(taffy::CompactLength::calc(ptr)) }
293
+ }
294
+ UnpackedLengthPercentage::Length(len) => style_helpers::length(len.px()),
295
+ UnpackedLengthPercentage::Percentage(p) => style_helpers::percent(p.0),
296
+ };
297
+ calc_values.push(value);
298
+ dim.into()
299
+ }
300
+
301
+ pub(crate) fn build_table_context(
302
+ doc: &mut BaseDocument,
303
+ table_root_node_id: NodeId,
304
+ anonymous: &mut ThinVec<NodeId>,
305
+ ) -> (TableContext, Vec<NodeId>) {
306
+ let mut cells: Vec<TableCell> = Vec::new();
307
+ let mut oof_children: Vec<(NodeId, taffy::Position)> = Vec::new();
308
+ let mut rows: Vec<TableRow> = Vec::new();
309
+ let mut row = 0u16;
310
+ let mut cursor = ColumnCursor::default();
311
+
312
+ let wm = table_wm(doc, table_root_node_id);
313
+ let root_node = &mut doc.nodes[table_root_node_id];
314
+
315
+ let dom_children = std::mem::take(&mut root_node.children);
316
+
317
+ let Some(stylo_styles) = root_node.primary_styles() else {
318
+ panic!("Ignoring table because it has no styles");
319
+ };
320
+
321
+ let mut style = table_taffy_style(&stylo_styles, wm);
322
+ style.item_is_table = true;
323
+ // Use `dense` row-flow so that each cell scans the row from its
324
+ // leftmost column for the first free track. Without `dense`,
325
+ // `place_definite_secondary_axis_item` keeps a per-item secondary
326
+ // cursor across rows, which means cells in later rows do not
327
+ // backfill columns freed up by rowspan cells from earlier rows.
328
+ style.grid_auto_flow = taffy::GridAutoFlow::RowDense;
329
+ style.grid_auto_columns = Vec::new();
330
+ style.grid_auto_rows = Vec::new();
331
+
332
+ // The fixed table layout algorithm only applies when the table has a non-auto width
333
+ let is_fixed = match stylo_styles.clone_table_layout() {
334
+ TableLayout::Fixed => !style.size.width.is_auto(),
335
+ TableLayout::Auto => false,
336
+ };
337
+
338
+ let border_collapse = stylo_styles.clone_border_collapse();
339
+ let border_spacing = stylo_styles.clone_border_spacing().0;
340
+
341
+ drop(stylo_styles);
342
+
343
+ let children = table_children(doc, table_root_node_id, &dom_children, anonymous);
344
+
345
+ let mut columns: Vec<TableColumn> = Vec::new();
346
+ let mut column_sizes: Vec<taffy::TrackSizingFunction> = Vec::new();
347
+ for child_id in children.iter().copied() {
348
+ collect_columns(doc, child_id, wm, &mut columns, &mut column_sizes);
349
+ }
350
+ // Percentage column widths only take effect in the fixed table layout algorithm
351
+ if !is_fixed {
352
+ for column in column_sizes.iter_mut() {
353
+ if column.max.into_raw().tag() == taffy::CompactLength::PERCENT_TAG {
354
+ *column = style_helpers::auto();
355
+ }
356
+ }
357
+ }
358
+ let mut first_cell_border: Option<ServoArc<Border>> = None;
359
+ // Percentage widths set on first-row cells: (column index, percentage, padding + border)
360
+ let mut percent_columns: Vec<(u16, f32, f32)> = Vec::new();
361
+ let mut calc_values: Vec<LengthPercentage> = Vec::new();
362
+ // Header row groups are laid out before other rows, and footer row groups after
363
+ let row_group_order = |doc: &BaseDocument, child_id: NodeId| -> u8 {
364
+ let display = doc.nodes[child_id]
365
+ .primary_styles()
366
+ .map(|s| s.clone_display());
367
+ match display.map(|d| d.inside()) {
368
+ Some(DisplayInside::TableHeaderGroup) => 0,
369
+ Some(DisplayInside::TableFooterGroup) => 2,
370
+ _ => 1,
371
+ }
372
+ };
373
+ for order in 0..3 {
374
+ for child_id in children.iter().copied() {
375
+ if row_group_order(doc, child_id) != order {
376
+ continue;
377
+ }
378
+ collect_table_cells(
379
+ doc,
380
+ child_id,
381
+ wm,
382
+ is_fixed,
383
+ border_collapse,
384
+ &mut row,
385
+ &mut cursor,
386
+ &mut cells,
387
+ &mut oof_children,
388
+ &mut rows,
389
+ &mut column_sizes,
390
+ &mut first_cell_border,
391
+ &mut percent_columns,
392
+ anonymous,
393
+ );
394
+ }
395
+ }
396
+ let remaining_column = if is_fixed {
397
+ style_helpers::minmax(style_helpers::length(0.0), style_helpers::fr(1.0))
398
+ } else {
399
+ style_helpers::auto()
400
+ };
401
+ // Trailing `<col>` columns which contain no cells and have no definite width
402
+ // (i.e. would be zero-width) are dropped, so they don't add border-spacing.
403
+ while column_sizes.len() > cursor.num_columns as usize
404
+ && column_sizes
405
+ .last()
406
+ .is_some_and(|c| c.min.into_raw().tag() == taffy::CompactLength::AUTO_TAG)
407
+ {
408
+ column_sizes.pop();
409
+ columns.pop();
410
+ }
411
+ let num_columns = cursor.num_columns.max(column_sizes.len() as u16);
412
+ column_sizes.resize(num_columns as usize, remaining_column);
413
+ if is_fixed {
414
+ // In the fixed table layout algorithm, percentage column widths resolve against
415
+ // the table's content width minus the horizontal border-spacing between columns,
416
+ // whereas Taffy resolves percentage tracks against the container's inner width
417
+ // (from which only the outer border-spacing has been removed via padding). Fold
418
+ // the inner border-spacing into the percentage using `calc(N% - N * spacing)`.
419
+ let spacing_x = match border_collapse {
420
+ BorderCollapse::Separate => border_spacing.width.px(),
421
+ BorderCollapse::Collapse => 0.0,
422
+ };
423
+ let inner_spacing = spacing_x * num_columns.saturating_sub(1) as f32;
424
+ let mut percent_track = |percent: f32, extra: f32| -> TrackSizingFunction {
425
+ let extra = extra - percent * inner_spacing;
426
+ if extra == 0.0 {
427
+ style_helpers::percent(percent)
428
+ } else {
429
+ percent_plus_length(&mut calc_values, percent, extra)
430
+ }
431
+ };
432
+
433
+ for column in column_sizes.iter_mut() {
434
+ if column.max.is_auto() {
435
+ *column = remaining_column;
436
+ } else if column.max.into_raw().tag() == taffy::CompactLength::PERCENT_TAG {
437
+ *column = percent_track(column.max.into_raw().value(), 0.0);
438
+ }
439
+ }
440
+
441
+ // Percentage widths on cells apply to the cell's content box, so the
442
+ // cell's horizontal padding and border must be added to the column width.
443
+ for (col_idx, percent, extra) in percent_columns {
444
+ if let Some(column) = column_sizes.get_mut(col_idx as usize) {
445
+ *column = percent_track(percent, extra);
446
+ }
447
+ }
448
+ }
449
+
450
+ style.grid_template_columns = column_sizes.into_iter().map(|dim| dim.into()).collect();
451
+ // A row's `height` is a minimum: the row grows to fit the content of its cells.
452
+ // Excess table height is distributed to unconstrained rows (via the stretching of
453
+ // `auto` tracks) if there are any, and to all rows otherwise. So constrained rows
454
+ // use a `max-content` (rather than `auto`) maximum when there are unconstrained rows.
455
+ let has_unconstrained_rows = rows.iter().any(|row| !row.is_constrained);
456
+ style.grid_template_rows = rows
457
+ .iter()
458
+ .map(|row| {
459
+ let min = match row.height {
460
+ Some(height) => style_helpers::length(height),
461
+ None => style_helpers::auto(),
462
+ };
463
+ let max = if row.is_constrained && has_unconstrained_rows {
464
+ style_helpers::max_content()
465
+ } else {
466
+ style_helpers::auto()
467
+ };
468
+ style_helpers::minmax(min, max)
469
+ })
470
+ .collect();
471
+
472
+ style.gap = match border_collapse {
473
+ BorderCollapse::Separate => {
474
+ // In the separated borders model, `border-spacing` also applies between
475
+ // the table border and the outermost cells, in addition to between cells.
476
+ let spacing_x = border_spacing.width.px();
477
+ let spacing_y = border_spacing.height.px();
478
+ let padding = style.padding.resolve_or_zero(None, resolve_calc_value);
479
+ style.padding = taffy::Rect {
480
+ left: style_helpers::length(padding.left + spacing_x),
481
+ right: style_helpers::length(padding.right + spacing_x),
482
+ top: style_helpers::length(padding.top + spacing_y),
483
+ bottom: style_helpers::length(padding.bottom + spacing_y),
484
+ };
485
+ taffy::Size {
486
+ width: style_helpers::length(spacing_x),
487
+ height: style_helpers::length(spacing_y),
488
+ }
489
+ }
490
+ BorderCollapse::Collapse => first_cell_border
491
+ .as_ref()
492
+ .map(|border| {
493
+ let x = side_width(border.border_left_width.0, border.border_left_style).max(
494
+ side_width(border.border_right_width.0, border.border_right_style),
495
+ );
496
+ let y = side_width(border.border_top_width.0, border.border_top_style).max(
497
+ side_width(border.border_bottom_width.0, border.border_bottom_style),
498
+ );
499
+ taffy::Size {
500
+ width: style_helpers::length(x),
501
+ height: style_helpers::length(y),
502
+ }
503
+ })
504
+ .unwrap_or(taffy::Size::ZERO.map(style_helpers::length)),
505
+ };
506
+
507
+ if border_collapse == BorderCollapse::Collapse {
508
+ style.border = taffy::Rect {
509
+ left: style.gap.width,
510
+ right: style.gap.width,
511
+ top: style.gap.height,
512
+ bottom: style.gap.height,
513
+ };
514
+ }
515
+
516
+ let layout_children = cells
517
+ .iter()
518
+ .map(|cell| cell.node_id)
519
+ .chain(oof_children.iter().map(|(node_id, _)| *node_id))
520
+ .collect();
521
+ let root_node = &mut doc.nodes[table_root_node_id];
522
+ root_node.children = dom_children;
523
+
524
+ (
525
+ TableContext {
526
+ style,
527
+ cells,
528
+ oof_children,
529
+ rows,
530
+ columns,
531
+ computed_grid_info: AtomicRefCell::new(None),
532
+ border_collapse,
533
+ border_style: first_cell_border,
534
+ calc_values,
535
+ },
536
+ layout_children,
537
+ )
538
+ }
539
+
540
+ /// Collect `<col>` elements (and `display: table-column` elements) along with their
541
+ /// widths, which take precedence over cell widths in the fixed table layout algorithm.
542
+ /// Columns without a specified width are recorded as `auto`.
543
+ fn collect_columns(
544
+ doc: &mut BaseDocument,
545
+ node_id: NodeId,
546
+ wm: WritingMode,
547
+ columns: &mut Vec<TableColumn>,
548
+ column_sizes: &mut Vec<TrackSizingFunction>,
549
+ ) {
550
+ let node = &doc.nodes[node_id];
551
+ if !node.is_element() {
552
+ return;
553
+ }
554
+ let Some(display) = node.display_style() else {
555
+ return;
556
+ };
557
+
558
+ match display.inside() {
559
+ DisplayInside::TableColumnGroup => {
560
+ let first_column = columns.len();
561
+ let children = std::mem::take(&mut doc.nodes[node_id].children);
562
+ for child_id in children.iter().copied() {
563
+ collect_columns(doc, child_id, wm, columns, column_sizes);
564
+ }
565
+ doc.nodes[node_id].children = children;
566
+ for column in &mut columns[first_column..] {
567
+ column.group.get_or_insert(node_id);
568
+ }
569
+ }
570
+ DisplayInside::TableColumn => {
571
+ let style = table_taffy_style(&node.primary_styles().unwrap(), wm);
572
+ let span: u16 = node
573
+ .attr(local_name!("span"))
574
+ .and_then(|val| val.parse::<u16>().ok())
575
+ .map(|v| v.max(1))
576
+ .unwrap_or(1);
577
+ let column: TrackSizingFunction = match style.size.width.tag() {
578
+ taffy::CompactLength::LENGTH_TAG => {
579
+ // A definite `max-width` clamps the column width; a zero width is treated as auto
580
+ let mut width = style.size.width.value();
581
+ if style.max_size.width.into_raw().tag() == taffy::CompactLength::LENGTH_TAG {
582
+ width = width.min(style.max_size.width.into_raw().value());
583
+ }
584
+ if width > 0.0 {
585
+ style_helpers::length(width)
586
+ } else {
587
+ style_helpers::auto()
588
+ }
589
+ }
590
+ taffy::CompactLength::PERCENT_TAG => {
591
+ style_helpers::percent(style.size.width.value())
592
+ }
593
+ // Browsers treat calc() widths on columns as auto
594
+ _ => style_helpers::auto(),
595
+ };
596
+ // A definite `min-width` on a column acts as a floor on its width
597
+ let column = match style.min_size.width.into_raw().tag() {
598
+ taffy::CompactLength::LENGTH_TAG if column.max.is_auto() => style_helpers::minmax(
599
+ style_helpers::length(style.min_size.width.into_raw().value()),
600
+ style_helpers::auto(),
601
+ ),
602
+ _ => column,
603
+ };
604
+ for _ in 0..span {
605
+ columns.push(TableColumn {
606
+ node_id,
607
+ group: None,
608
+ });
609
+ column_sizes.push(column);
610
+ }
611
+ }
612
+ _ => {}
613
+ }
614
+ }
615
+
616
+ #[allow(clippy::too_many_arguments)]
617
+ fn collect_table_cells(
618
+ doc: &mut BaseDocument,
619
+ node_id: NodeId,
620
+ wm: WritingMode,
621
+ is_fixed: bool,
622
+ border_collapse: BorderCollapse,
623
+ row: &mut u16,
624
+ cursor: &mut ColumnCursor,
625
+ cells: &mut Vec<TableCell>,
626
+ oof_children: &mut Vec<(NodeId, taffy::Position)>,
627
+ rows: &mut Vec<TableRow>,
628
+ columns: &mut Vec<TrackSizingFunction>,
629
+ first_cell_border: &mut Option<ServoArc<Border>>,
630
+ percent_columns: &mut Vec<(u16, f32, f32)>,
631
+ anonymous: &mut ThinVec<NodeId>,
632
+ ) {
633
+ let node = &mut doc.nodes[node_id];
634
+
635
+ if node.element_data().is_none() {
636
+ return;
637
+ }
638
+
639
+ let Some(display) = node.display_style() else {
640
+ #[cfg(feature = "tracing")]
641
+ tracing::info!("Ignoring table descendent because it has no styles");
642
+ return;
643
+ };
644
+
645
+ if display.outside() == DisplayOutside::None {
646
+ node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
647
+ return;
648
+ }
649
+
650
+ // Absolutely positioned boxes are out-of-flow: they are laid out by their containing
651
+ // block rather than as part of the table grid.
652
+ let position = node.primary_styles().unwrap().clone_position();
653
+ if position.is_absolutely_positioned() {
654
+ oof_children.push((node_id, stylo_taffy::convert::position(position)));
655
+ return;
656
+ }
657
+
658
+ match display.inside() {
659
+ DisplayInside::TableRowGroup
660
+ | DisplayInside::TableHeaderGroup
661
+ | DisplayInside::TableFooterGroup
662
+ | DisplayInside::Contents => {
663
+ let first_row = rows.len();
664
+ let dom_children = doc.nodes[node_id].children.clone();
665
+ let children = table_children(doc, node_id, &dom_children, anonymous);
666
+ for child_id in children.iter().copied() {
667
+ doc.nodes[child_id]
668
+ .remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
669
+ collect_table_cells(
670
+ doc,
671
+ child_id,
672
+ wm,
673
+ is_fixed,
674
+ border_collapse,
675
+ row,
676
+ cursor,
677
+ cells,
678
+ oof_children,
679
+ rows,
680
+ columns,
681
+ first_cell_border,
682
+ percent_columns,
683
+ anonymous,
684
+ );
685
+ }
686
+ if display.inside() != DisplayInside::Contents {
687
+ for row in &mut rows[first_row..] {
688
+ row.group.get_or_insert(node_id);
689
+ }
690
+ }
691
+ }
692
+ DisplayInside::TableRow => {
693
+ node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
694
+ *row += 1;
695
+ cursor.start_row();
696
+
697
+ let height = node.primary_styles().and_then(|style| {
698
+ let height = stylo_taffy::convert::dimension(&style.clone_height());
699
+ (height.tag() == taffy::CompactLength::LENGTH_TAG).then(|| height.value())
700
+ });
701
+ rows.push(TableRow {
702
+ node_id,
703
+ group: None,
704
+ height,
705
+ is_constrained: height.is_some(),
706
+ });
707
+
708
+ let dom_children = doc.nodes[node_id].children.clone();
709
+ let children = table_children(doc, node_id, &dom_children, anonymous);
710
+ for child_id in children.iter().copied() {
711
+ collect_table_cells(
712
+ doc,
713
+ child_id,
714
+ wm,
715
+ is_fixed,
716
+ border_collapse,
717
+ row,
718
+ cursor,
719
+ cells,
720
+ oof_children,
721
+ rows,
722
+ columns,
723
+ first_cell_border,
724
+ percent_columns,
725
+ anonymous,
726
+ );
727
+ }
728
+ }
729
+ DisplayInside::TableCell => {
730
+ // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
731
+ let stylo_style = &node.primary_styles().unwrap();
732
+ let colspan: u16 = node
733
+ .attr(local_name!("colspan"))
734
+ .and_then(|val| val.parse().ok())
735
+ .unwrap_or(1);
736
+ let rowspan: u16 = node
737
+ .attr(local_name!("rowspan"))
738
+ .and_then(|val| val.parse::<u16>().ok())
739
+ .map(|v| v.clamp(1, 65534))
740
+ .unwrap_or(1);
741
+ let mut style = table_taffy_style(stylo_style, wm);
742
+ let col = cursor.next_free();
743
+
744
+ // In the collapsed borders model the borders are laid out as gutters between
745
+ // the cells (see the table's `gap`) rather than as part of the cells
746
+ if border_collapse == BorderCollapse::Collapse {
747
+ style.border = taffy::Rect::ZERO.map(style_helpers::length);
748
+ }
749
+
750
+ if first_cell_border.is_none() {
751
+ *first_cell_border = Some(stylo_style.clone_border());
752
+ }
753
+
754
+ // In the fixed table layout algorithm the widths of columns are not
755
+ // affected by cell contents, so cells must not impose a min-content
756
+ // floor on their tracks.
757
+ if is_fixed {
758
+ style.min_size.width = style_helpers::length(0.0);
759
+ }
760
+
761
+ // Column widths from `<col>` elements take precedence over cell widths
762
+ let col_needs_width =
763
+ (col as usize) >= columns.len() || columns[col as usize].max.is_auto();
764
+ if *row == 1 && col_needs_width {
765
+ let column: TrackSizingFunction = match style.size.width.tag() {
766
+ taffy::CompactLength::LENGTH_TAG => {
767
+ let len = style.size.width.value();
768
+ let padding = style.padding.resolve_or_zero(None, resolve_calc_value);
769
+ let border = style.border.resolve_or_zero(None, resolve_calc_value);
770
+ match style.box_sizing {
771
+ taffy::BoxSizing::ContentBox => style_helpers::length(
772
+ len + padding.left + padding.right + border.left + border.right,
773
+ ),
774
+ taffy::BoxSizing::BorderBox => style_helpers::length(len),
775
+ }
776
+ }
777
+ taffy::CompactLength::PERCENT_TAG => {
778
+ if is_fixed {
779
+ let extra = match style.box_sizing {
780
+ taffy::BoxSizing::ContentBox => {
781
+ let padding =
782
+ style.padding.resolve_or_zero(None, resolve_calc_value);
783
+ let border =
784
+ style.border.resolve_or_zero(None, resolve_calc_value);
785
+ padding.left + padding.right + border.left + border.right
786
+ }
787
+ taffy::BoxSizing::BorderBox => 0.0,
788
+ };
789
+ // Resolved in `build_table_context` once the column count is known
790
+ percent_columns.push((col, style.size.width.value(), extra));
791
+ style_helpers::percent(style.size.width.value())
792
+ } else {
793
+ style_helpers::auto()
794
+ }
795
+ }
796
+ taffy::CompactLength::AUTO_TAG => style_helpers::auto(),
797
+ // Dimension values are always length, percentage, auto or calc(),
798
+ // so any other tag is a calc() value. Pass it through so that
799
+ // Taffy resolves it against the table's inner width.
800
+ _ => style.size.width.into(),
801
+ };
802
+ if (col as usize) < columns.len() {
803
+ if !column.max.is_auto() {
804
+ columns[col as usize] = column;
805
+ }
806
+ } else {
807
+ columns.resize(col as usize, style_helpers::auto());
808
+ columns.push(column);
809
+ }
810
+ }
811
+
812
+ // The margin properties do not apply to table-internal elements
813
+ style.margin = taffy::Rect::ZERO.map(style_helpers::length);
814
+
815
+ // Let Taffy auto-place the column. Combined with
816
+ // `grid_auto_flow: RowDense` set on the table root, each cell
817
+ // scans from the first track in its row for a free position,
818
+ // which makes cells automatically skip columns occupied by
819
+ // rowspan cells from earlier rows.
820
+ style.grid_column = taffy::Line {
821
+ start: style_helpers::auto(),
822
+ end: style_helpers::span(colspan),
823
+ };
824
+ style.grid_row = taffy::Line {
825
+ start: style_helpers::line(*row as i16),
826
+ end: style_helpers::span(rowspan),
827
+ };
828
+ if rowspan == 1 && style.size.height.tag() == taffy::CompactLength::LENGTH_TAG {
829
+ if let Some(row) = rows.last_mut() {
830
+ row.is_constrained = true;
831
+ }
832
+ }
833
+
834
+ style.size.width = style_helpers::auto();
835
+ // A cell's max-width constrains its intrinsic contribution, not the
836
+ // final column width. The cell's own style still supplies the limit
837
+ // during intrinsic measurement; the grid must be allowed to stretch it.
838
+ style.max_size.width = style_helpers::auto();
839
+ cells.push(TableCell {
840
+ node_id,
841
+ row: row.saturating_sub(1),
842
+ column: col,
843
+ style,
844
+ });
845
+
846
+ cursor.place(colspan, rowspan);
847
+ }
848
+ DisplayInside::Flow
849
+ | DisplayInside::FlowRoot
850
+ | DisplayInside::Flex
851
+ | DisplayInside::Grid => {
852
+ node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
853
+ // Probably a table caption: ignore
854
+ // println!(
855
+ // "Warning: ignoring non-table typed descendent of table ({:?})",
856
+ // display.inside()
857
+ // );
858
+ }
859
+ DisplayInside::TableColumnGroup | DisplayInside::TableColumn | DisplayInside::Table => {
860
+ node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
861
+ //Ignore
862
+ }
863
+ DisplayInside::None => {
864
+ node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
865
+ // Ignore
866
+ }
867
+ }
868
+ }
869
+
870
+ pub struct RangeIter(Range<usize>);
871
+
872
+ impl Iterator for RangeIter {
873
+ type Item = taffy::NodeId;
874
+
875
+ fn next(&mut self) -> Option<Self::Item> {
876
+ self.0.next().map(taffy::NodeId::from)
877
+ }
878
+ }
879
+
880
+ impl taffy::TraversePartialTree for TableTreeWrapper<'_, '_> {
881
+ type ChildIter<'a>
882
+ = RangeIter
883
+ where
884
+ Self: 'a;
885
+
886
+ #[inline(always)]
887
+ fn child_ids(&self, _node_id: taffy::NodeId) -> Self::ChildIter<'_> {
888
+ RangeIter(0..self.ctx.cells.len())
889
+ }
890
+
891
+ #[inline(always)]
892
+ fn child_count(&self, _node_id: taffy::NodeId) -> usize {
893
+ self.ctx.cells.len()
894
+ }
895
+
896
+ #[inline(always)]
897
+ fn get_child_id(&self, _node_id: taffy::NodeId, index: usize) -> taffy::NodeId {
898
+ index.into()
899
+ }
900
+ }
901
+ impl taffy::TraverseTree for TableTreeWrapper<'_, '_> {}
902
+
903
+ impl taffy::LayoutPartialTree for TableTreeWrapper<'_, '_> {
904
+ type CoreContainerStyle<'a>
905
+ = &'a taffy::Style<Atom>
906
+ where
907
+ Self: 'a;
908
+
909
+ type CustomIdent = Atom;
910
+
911
+ fn get_core_container_style(&self, _node_id: taffy::NodeId) -> &taffy::Style<Atom> {
912
+ &self.ctx.style
913
+ }
914
+
915
+ fn resolve_calc_value(&self, calc_ptr: *const (), parent_size: f32) -> f32 {
916
+ resolve_calc_value(calc_ptr, parent_size)
917
+ }
918
+
919
+ fn set_unrounded_layout(&mut self, node_id: taffy::NodeId, layout: &taffy::Layout) {
920
+ let node_id = crate::taffy_node_id(self.ctx.cells[usize::from(node_id)].node_id);
921
+ self.doc.set_unrounded_layout(node_id, layout)
922
+ }
923
+
924
+ fn compute_child_layout(
925
+ &mut self,
926
+ node_id: taffy::NodeId,
927
+ mut inputs: taffy::tree::LayoutInput,
928
+ ) -> taffy::LayoutOutput {
929
+ let cell = &self.ctx.cells[usize::from(node_id)];
930
+ let node_id = crate::taffy_node_id(cell.node_id);
931
+ if inputs.known_dimensions.width.is_some() {
932
+ // Column/row sizing has already applied the cell's constraints.
933
+ // Reapplying max-width here would shrink its contents back to zero
934
+ // for the common td { max-width: 0 } equal-column pattern.
935
+ inputs.sizing_mode = taffy::SizingMode::ContentSize;
936
+ }
937
+ self.doc.compute_child_layout(node_id, inputs)
938
+ }
939
+ }
940
+
941
+ impl taffy::LayoutGridContainer for TableTreeWrapper<'_, '_> {
942
+ type GridContainerStyle<'a>
943
+ = &'a taffy::Style<Atom>
944
+ where
945
+ Self: 'a;
946
+
947
+ type GridItemStyle<'a>
948
+ = &'a taffy::Style<Atom>
949
+ where
950
+ Self: 'a;
951
+
952
+ fn get_grid_container_style(&self, node_id: taffy::NodeId) -> Self::GridContainerStyle<'_> {
953
+ self.get_core_container_style(node_id)
954
+ }
955
+
956
+ fn get_grid_child_style(&self, child_node_id: taffy::NodeId) -> Self::GridItemStyle<'_> {
957
+ &self.ctx.cells[usize::from(child_node_id)].style
958
+ }
959
+
960
+ fn set_detailed_grid_info(
961
+ &mut self,
962
+ _node_id: taffy::NodeId,
963
+ detailed_grid_info: DetailedGridInfo<Atom>,
964
+ ) {
965
+ *self.ctx.computed_grid_info.borrow_mut() = Some(detailed_grid_info);
966
+ }
967
+ }