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,439 @@
1
+ use markup5ever::{LocalName, local_name};
2
+ use taffy::{
3
+ AvailableSpace, BoxSizing, CoreStyle, LayoutInput, LayoutOutput, MaybeMath, MaybeResolve,
4
+ RequestedAxis, ResolveOrZero as _, RunMode, Size, SizingMode,
5
+ };
6
+
7
+ /// Whether an element is a replaced element laid out as a leaf box with an
8
+ /// intrinsic size. Note: `<object>` is deliberately excluded as its fallback
9
+ /// children should render when no resource is loaded.
10
+ pub(crate) fn is_replaced_element(tag_name: &LocalName) -> bool {
11
+ *tag_name == local_name!("img")
12
+ || *tag_name == local_name!("svg")
13
+ || *tag_name == local_name!("canvas")
14
+ || *tag_name == local_name!("video")
15
+ || *tag_name == local_name!("embed")
16
+ || *tag_name == local_name!("iframe")
17
+ }
18
+
19
+ /// The intrinsic dimensions of a replaced element per CSS Images 3
20
+ /// (https://drafts.csswg.org/css-images/#intrinsic-dimensions): an intrinsic
21
+ /// width, an intrinsic height, and an intrinsic aspect ratio, each of which
22
+ /// may independently be absent.
23
+ #[derive(Debug, Clone, Copy, Default)]
24
+ pub struct IntrinsicSizes {
25
+ pub width: Option<f32>,
26
+ pub height: Option<f32>,
27
+ pub ratio: Option<f32>,
28
+ }
29
+
30
+ #[derive(Debug, Clone, Copy)]
31
+ pub struct ReplacedContext {
32
+ /// The element's intrinsic dimensions, each possibly absent.
33
+ pub intrinsic_sizes: IntrinsicSizes,
34
+ /// The default object size (https://drafts.csswg.org/css-images/#default-object-size)
35
+ /// used to fill in dimensions the intrinsic sizes leave unresolved. 300x150
36
+ /// for most replaced elements; zero for an image with no loaded resource.
37
+ pub default_object_size: taffy::Size<f32>,
38
+ }
39
+
40
+ /// Whether a height/width value is violating it's min- and max- constraints
41
+ /// The min- and max- constraints cannot both be violated because the max
42
+ /// constraint if floored by the min constraint (min constraint takes priority)
43
+ enum Violation {
44
+ /// Constraints are not violated
45
+ None,
46
+ /// Min constraint is violated
47
+ Min,
48
+ /// Max constraint is violated
49
+ Max,
50
+ }
51
+
52
+ pub fn compute_replaced_layout(
53
+ inputs: LayoutInput,
54
+ style: &impl CoreStyle,
55
+ resolve_calc_value: impl Fn(*const (), f32) -> f32,
56
+ context: &ReplacedContext,
57
+ ) -> LayoutOutput {
58
+ let LayoutInput {
59
+ known_dimensions,
60
+ parent_size,
61
+ available_space,
62
+ sizing_mode,
63
+ run_mode,
64
+ axis: requested_axis,
65
+ ..
66
+ } = inputs;
67
+
68
+ let padding = style
69
+ .padding()
70
+ .resolve_or_zero(parent_size.width, &resolve_calc_value);
71
+ let border = style
72
+ .border()
73
+ .resolve_or_zero(parent_size.width, &resolve_calc_value);
74
+ let padding_border = padding + border;
75
+ let pb_sum = Size {
76
+ width: padding_border.left + padding_border.right,
77
+ height: padding_border.top + padding_border.bottom,
78
+ };
79
+ let box_sizing_adjustment = if style.box_sizing() == BoxSizing::BorderBox {
80
+ pb_sum
81
+ } else {
82
+ Size::ZERO
83
+ };
84
+
85
+ // The scrollable overflow rect is measured from the scroll origin at the
86
+ // padding-box corner: the content-box size offset by the start padding. A
87
+ // scroll container's own padding at the end of the content is part of its
88
+ // scrollable overflow region, so it is included. Boxes that are not scroll
89
+ // containers do not extend their overflow region by their own padding.
90
+ let is_scroll_container = {
91
+ let overflow = style.overflow();
92
+ overflow.x.is_scroll_container() || overflow.y.is_scroll_container()
93
+ };
94
+ let is_rtl = style.direction() == taffy::Direction::Rtl;
95
+ let overflow_rect_for = |content_box_size: Size<f32>| {
96
+ let start_padding = if is_rtl { padding.right } else { padding.left };
97
+ let end_padding = if is_rtl { padding.left } else { padding.right };
98
+ taffy::Rect {
99
+ left: 0.0,
100
+ right: start_padding
101
+ + content_box_size.width
102
+ + if is_scroll_container {
103
+ end_padding
104
+ } else {
105
+ 0.0
106
+ },
107
+ top: 0.0,
108
+ bottom: padding.top
109
+ + content_box_size.height
110
+ + if is_scroll_container {
111
+ padding.bottom
112
+ } else {
113
+ 0.0
114
+ },
115
+ }
116
+ };
117
+
118
+ // Return early if both width and height are known
119
+ if run_mode == RunMode::ComputeSize {
120
+ if let Size {
121
+ width: Some(width),
122
+ height: Some(height),
123
+ } = known_dimensions
124
+ {
125
+ return LayoutOutput::from_outer_size(Size {
126
+ width: width.max(pb_sum.width),
127
+ height: height.max(pb_sum.height),
128
+ });
129
+ }
130
+ }
131
+
132
+ // Use aspect_ratio from style, fall back to inherent aspect ratio (if any).
133
+ //
134
+ // A degenerate ratio -- zero, infinite, or NaN -- is discarded. Transferring
135
+ // such a ratio between axes computes `width / ratio` or `height * ratio`,
136
+ // which yields an infinite (ratio == 0) or NaN (ratio is NaN) size in the
137
+ // other axis. CSS Sizing 4 4.1 says a degenerate ratio behaves as `auto`,
138
+ // so each source is filtered *before* the fallback: a degenerate style
139
+ // ratio falls back to the inherent one (that is what `auto` means for a
140
+ // replaced element), and only if both are degenerate or absent does the
141
+ // element get no preferred aspect ratio at all.
142
+ let is_usable_ratio = |ratio: &f32| ratio.is_finite() && *ratio > 0.0;
143
+ let intrinsic = context.intrinsic_sizes;
144
+ let aspect_ratio: Option<f32> = style
145
+ .aspect_ratio()
146
+ .filter(is_usable_ratio)
147
+ .or(intrinsic.ratio.filter(is_usable_ratio));
148
+
149
+ // Concrete object size per the CSS default sizing algorithm with no
150
+ // specified size (https://drafts.csswg.org/css-images/#default-sizing):
151
+ // missing intrinsic dimensions are derived from the aspect ratio when
152
+ // possible, otherwise taken from the default object size. An element with
153
+ // only an intrinsic aspect ratio uses the stretch-fit width in normal flow
154
+ // (CSS2 §10.3.2) when the available width is definite; shrink-to-fit
155
+ // contexts (min/max-content) contain it within the default object size.
156
+ //
157
+ // Only the intrinsic ratio participates here: the `aspect-ratio` property
158
+ // affects the sizing of the box, not the natural dimensions of its content.
159
+ let intrinsic_ratio = intrinsic.ratio.filter(is_usable_ratio);
160
+ let default_size = context.default_object_size;
161
+ let inherent_size = match (intrinsic.width, intrinsic.height) {
162
+ (Some(w), Some(h)) => Size {
163
+ width: w,
164
+ height: h,
165
+ },
166
+ (Some(w), None) => Size {
167
+ width: w,
168
+ height: intrinsic_ratio
169
+ .map(|r| w / r)
170
+ .unwrap_or(default_size.height),
171
+ },
172
+ (None, Some(h)) => Size {
173
+ width: intrinsic_ratio.map(|r| h * r).unwrap_or(default_size.width),
174
+ height: h,
175
+ },
176
+ (None, None) => match intrinsic_ratio {
177
+ Some(ratio) => {
178
+ // A ratio-only replaced element stretches to fit the width it is laid out
179
+ // in: the available width if definite, else (under a max-content constraint)
180
+ // its containing block, per CSS2 §10.3.2.
181
+ let stretch_fit_width = match available_space.width {
182
+ AvailableSpace::Definite(width) => Some(width),
183
+ AvailableSpace::MaxContent => parent_size.width,
184
+ AvailableSpace::MinContent => None,
185
+ };
186
+ if let Some(width) = stretch_fit_width {
187
+ Size {
188
+ width,
189
+ height: width / ratio,
190
+ }
191
+ } else {
192
+ // Contain within the default object size
193
+ let scale = (default_size.width / ratio).min(default_size.height);
194
+ Size {
195
+ width: scale * ratio,
196
+ height: scale,
197
+ }
198
+ }
199
+ }
200
+ None => default_size,
201
+ },
202
+ };
203
+
204
+ // Percentages are only resolved against zero in the inline axis.
205
+ // See https://www.w3.org/TR/css-sizing-3/#replaced-percentage-min-contribution
206
+ let basis_for_max_and_preferred = Size {
207
+ width: if available_space.width == AvailableSpace::MinContent {
208
+ Some(0.0)
209
+ } else {
210
+ parent_size.width
211
+ },
212
+ height: parent_size.height,
213
+ };
214
+
215
+ // `stretch` fills the containing block (less margins) when its size is definite, and
216
+ // behaves as `auto` otherwise. The content size can't be negative.
217
+ // See https://drafts.csswg.org/css-sizing-4/#stretch-fit-sizing
218
+ let margin = style
219
+ .margin()
220
+ .resolve_or_zero(parent_size.width, &resolve_calc_value);
221
+ let stretch_size = Size {
222
+ width: parent_size
223
+ .width
224
+ .filter(|_| available_space.width.is_definite())
225
+ .map(|width| (width - margin.horizontal_axis_sum() - pb_sum.width).max(0.0)),
226
+ height: parent_size
227
+ .height
228
+ .filter(|_| available_space.height.is_definite())
229
+ .map(|height| (height - margin.vertical_axis_sum() - pb_sum.height).max(0.0)),
230
+ };
231
+
232
+ // Resolve sizes
233
+ let size_style = style.size();
234
+ let style_size = size_style
235
+ .maybe_resolve(basis_for_max_and_preferred, &resolve_calc_value)
236
+ .maybe_sub(box_sizing_adjustment);
237
+ let style_size = Size {
238
+ width: if size_style.width.is_stretch() {
239
+ stretch_size.width
240
+ } else {
241
+ style_size.width
242
+ },
243
+ height: if size_style.height.is_stretch() {
244
+ stretch_size.height
245
+ } else {
246
+ style_size.height
247
+ },
248
+ };
249
+ let mut min_size = style
250
+ .min_size()
251
+ .maybe_resolve(parent_size, &resolve_calc_value)
252
+ .maybe_sub(box_sizing_adjustment);
253
+ let max_size = style
254
+ .max_size()
255
+ .maybe_resolve(basis_for_max_and_preferred, &resolve_calc_value)
256
+ .maybe_max(min_size)
257
+ .maybe_sub(box_sizing_adjustment);
258
+
259
+ // For ContentSize mode, ignore preferred/min size styles in the axis being measured: the
260
+ // parent layout algorithm applies them itself, and content-based measurement should return
261
+ // the content size (the intrinsic size for replaced elements). Constraints in the opposite
262
+ // axis are retained as they transfer through the aspect ratio (transferred size suggestion).
263
+ let mut style_size = style_size;
264
+ if sizing_mode == SizingMode::ContentSize {
265
+ match requested_axis {
266
+ RequestedAxis::Horizontal => {
267
+ style_size.width = None;
268
+ min_size.width = None;
269
+ }
270
+ RequestedAxis::Vertical => {
271
+ style_size.height = None;
272
+ min_size.height = None;
273
+ }
274
+ RequestedAxis::Both => {}
275
+ }
276
+ }
277
+
278
+ // Known dimensions are the parent's current sizing inputs. Clamp them before transferring
279
+ // an aspect ratio so provisional cross-axis sizes do not bypass replaced-element limits.
280
+ if known_dimensions.width.is_some() | known_dimensions.height.is_some() {
281
+ // Style max sizes without the available-space fallback: available space must not
282
+ // constrain the aspect-ratio transfer of parent-resolved dimensions.
283
+ let style_max_size = style
284
+ .max_size()
285
+ .maybe_resolve(basis_for_max_and_preferred, &resolve_calc_value)
286
+ .maybe_sub(box_sizing_adjustment)
287
+ .maybe_max(min_size);
288
+
289
+ let content_box_known_dimensions = known_dimensions.maybe_sub(pb_sum);
290
+ let transferred = content_box_known_dimensions
291
+ .maybe_clamp(min_size, style_max_size)
292
+ .maybe_apply_aspect_ratio(aspect_ratio)
293
+ .unwrap_or(inherent_size);
294
+
295
+ // Known axes are authoritative (already resolved by the parent); only the axis
296
+ // derived via aspect-ratio transfer (or falling back to the intrinsic size) is
297
+ // clamped by this element's min/max sizes.
298
+ let size = content_box_known_dimensions
299
+ .unwrap_or(transferred.maybe_clamp(min_size, style_max_size));
300
+
301
+ let size = size.map(|s| s.max(0.0));
302
+ return LayoutOutput::from_sizes(size + pb_sum, overflow_rect_for(size));
303
+ }
304
+
305
+ let unclamped_size = if style_size.width.is_some() | style_size.height.is_some() {
306
+ style_size
307
+ .maybe_apply_aspect_ratio(aspect_ratio)
308
+ .unwrap_or(inherent_size)
309
+ } else {
310
+ inherent_size
311
+ };
312
+
313
+ // Floor size at zero
314
+ let size = unclamped_size.map(|s| s.max(0.0));
315
+
316
+ // Violations
317
+ let width_violation = if size.width < min_size.width.unwrap_or(0.0) {
318
+ Violation::Min
319
+ } else if size.width > max_size.width.unwrap_or(f32::INFINITY) {
320
+ Violation::Max
321
+ } else {
322
+ Violation::None
323
+ };
324
+
325
+ let height_violation = if size.height < min_size.height.unwrap_or(0.0) {
326
+ Violation::Min
327
+ } else if size.height > max_size.height.unwrap_or(f32::INFINITY) {
328
+ Violation::Max
329
+ } else {
330
+ Violation::None
331
+ };
332
+
333
+ // Without an intrinsic aspect ratio, each axis is clamped independently
334
+ let Some(aspect_ratio) = aspect_ratio else {
335
+ let size = size.maybe_clamp(min_size, max_size);
336
+ return LayoutOutput::from_sizes(size + pb_sum, overflow_rect_for(size));
337
+ };
338
+ let inv_aspect_ratio = 1.0 / aspect_ratio;
339
+
340
+ // Clamp following rules in table at
341
+ // https://www.w3.org/TR/CSS22/visudet.html#min-max-widths
342
+ let size = match (width_violation, height_violation) {
343
+ // No constraint violation
344
+ (Violation::None, Violation::None) => size,
345
+ // w > max-width
346
+ (Violation::Max, Violation::None) => {
347
+ let max_width = max_size.width.unwrap();
348
+ Size {
349
+ width: max_width,
350
+ height: (max_width * inv_aspect_ratio).maybe_max(min_size.height),
351
+ }
352
+ }
353
+ // w < min-width
354
+ (Violation::Min, Violation::None) => {
355
+ let min_width = min_size.width.unwrap();
356
+ Size {
357
+ width: min_width,
358
+ height: (min_width * inv_aspect_ratio).maybe_min(max_size.height),
359
+ }
360
+ }
361
+ // h > max-height
362
+ (Violation::None, Violation::Max) => {
363
+ let max_height = max_size.height.unwrap();
364
+ Size {
365
+ width: (max_height * aspect_ratio).maybe_max(min_size.width),
366
+ height: max_height,
367
+ }
368
+ }
369
+ // h < min-height
370
+ (Violation::None, Violation::Min) => {
371
+ let min_height = min_size.height.unwrap();
372
+ Size {
373
+ width: (min_height * aspect_ratio).maybe_min(max_size.width),
374
+ height: min_height,
375
+ }
376
+ }
377
+ // (w > max-width) and (h > max-height)
378
+ (Violation::Max, Violation::Max) => {
379
+ let max_width = max_size.width.unwrap();
380
+ let max_height = max_size.height.unwrap();
381
+ if max_width / size.width <= max_height / size.height {
382
+ Size {
383
+ width: max_width,
384
+ height: (max_width * inv_aspect_ratio).maybe_max(min_size.height),
385
+ }
386
+ } else {
387
+ Size {
388
+ width: (max_height * aspect_ratio).maybe_max(min_size.width),
389
+ height: max_height,
390
+ }
391
+ }
392
+ }
393
+ // (w < min-width) and (h < min-height)
394
+ (Violation::Min, Violation::Min) => {
395
+ let min_width = min_size.width.unwrap();
396
+ let min_height = min_size.height.unwrap();
397
+ if min_width / size.width <= min_height / size.height {
398
+ Size {
399
+ width: (min_height * aspect_ratio).maybe_min(max_size.width),
400
+ height: min_height,
401
+ }
402
+ } else {
403
+ Size {
404
+ width: min_width,
405
+ height: (min_width * inv_aspect_ratio).maybe_min(max_size.height),
406
+ }
407
+ }
408
+ }
409
+ // (w < min-width) and (h > max-height)
410
+ (Violation::Min, Violation::Max) => {
411
+ let min_width = min_size.width.unwrap();
412
+ let max_height = max_size.height.unwrap();
413
+ Size {
414
+ width: min_width,
415
+ height: max_height,
416
+ }
417
+ }
418
+ // (w < min-width) and (h > max-height)
419
+ (Violation::Max, Violation::Min) => {
420
+ let max_width = max_size.width.unwrap();
421
+ let min_height = min_size.height.unwrap();
422
+ Size {
423
+ width: max_width,
424
+ height: min_height,
425
+ }
426
+ }
427
+ };
428
+
429
+ LayoutOutput::from_sizes(size + pb_sum, overflow_rect_for(size))
430
+ }
431
+
432
+ /// Inline-level elements that are laid out as atomic inline boxes (with their own
433
+ /// layout box) rather than being flattened into the surrounding text layout.
434
+ pub(crate) fn is_inline_box_element(tag_name: &LocalName) -> bool {
435
+ is_replaced_element(tag_name)
436
+ || *tag_name == local_name!("input")
437
+ || *tag_name == local_name!("textarea")
438
+ || *tag_name == local_name!("button")
439
+ }