@react-x11/components 0.10.0 → 0.12.0

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 (155) hide show
  1. package/dist/code/index.d.ts.map +1 -1
  2. package/dist/code/index.js +25 -24
  3. package/dist/code/index.js.map +1 -1
  4. package/dist/code-language/index.d.ts +1 -1
  5. package/dist/code-language/index.d.ts.map +1 -1
  6. package/dist/code-language/index.js +1 -1
  7. package/dist/code-language/index.js.map +1 -1
  8. package/dist/code-language/runs.d.ts +31 -0
  9. package/dist/code-language/runs.d.ts.map +1 -1
  10. package/dist/code-language/runs.js +105 -0
  11. package/dist/code-language/runs.js.map +1 -1
  12. package/dist/codeblock/index.d.ts +13 -0
  13. package/dist/codeblock/index.d.ts.map +1 -1
  14. package/dist/codeblock/index.js +37 -1
  15. package/dist/codeblock/index.js.map +1 -1
  16. package/dist/flow/node.d.ts +14 -6
  17. package/dist/flow/node.d.ts.map +1 -1
  18. package/dist/flow/node.js +82 -64
  19. package/dist/flow/node.js.map +1 -1
  20. package/dist/html/controls.d.ts +26 -7
  21. package/dist/html/controls.d.ts.map +1 -1
  22. package/dist/html/controls.js +28 -3
  23. package/dist/html/controls.js.map +1 -1
  24. package/dist/html/css/cascade.d.ts +32 -10
  25. package/dist/html/css/cascade.d.ts.map +1 -1
  26. package/dist/html/css/cascade.js +89 -31
  27. package/dist/html/css/cascade.js.map +1 -1
  28. package/dist/html/css/color.d.ts +19 -0
  29. package/dist/html/css/color.d.ts.map +1 -1
  30. package/dist/html/css/color.js +86 -0
  31. package/dist/html/css/color.js.map +1 -1
  32. package/dist/html/css/parse.d.ts +11 -0
  33. package/dist/html/css/parse.d.ts.map +1 -1
  34. package/dist/html/css/parse.js +114 -4
  35. package/dist/html/css/parse.js.map +1 -1
  36. package/dist/html/css/style.d.ts +45 -5
  37. package/dist/html/css/style.d.ts.map +1 -1
  38. package/dist/html/css/style.js +363 -14
  39. package/dist/html/css/style.js.map +1 -1
  40. package/dist/html/css/ua.js +22 -5
  41. package/dist/html/css/ua.js.map +1 -1
  42. package/dist/html/css/values.d.ts +10 -1
  43. package/dist/html/css/values.d.ts.map +1 -1
  44. package/dist/html/css/values.js +27 -2
  45. package/dist/html/css/values.js.map +1 -1
  46. package/dist/html/css/vars.d.ts +5 -0
  47. package/dist/html/css/vars.d.ts.map +1 -1
  48. package/dist/html/css/vars.js +2 -2
  49. package/dist/html/css/vars.js.map +1 -1
  50. package/dist/html/dom.d.ts.map +1 -1
  51. package/dist/html/dom.js +35 -2
  52. package/dist/html/dom.js.map +1 -1
  53. package/dist/html/index.d.ts.map +1 -1
  54. package/dist/html/index.js +38 -1
  55. package/dist/html/index.js.map +1 -1
  56. package/dist/html/layout/block.d.ts +11 -2
  57. package/dist/html/layout/block.d.ts.map +1 -1
  58. package/dist/html/layout/block.js +306 -25
  59. package/dist/html/layout/block.js.map +1 -1
  60. package/dist/html/layout/boxes.d.ts +61 -6
  61. package/dist/html/layout/boxes.d.ts.map +1 -1
  62. package/dist/html/layout/boxes.js +65 -16
  63. package/dist/html/layout/boxes.js.map +1 -1
  64. package/dist/html/layout/cache.d.ts +2 -2
  65. package/dist/html/layout/cache.d.ts.map +1 -1
  66. package/dist/html/layout/cache.js +8 -3
  67. package/dist/html/layout/cache.js.map +1 -1
  68. package/dist/html/layout/floats.d.ts +4 -0
  69. package/dist/html/layout/floats.d.ts.map +1 -1
  70. package/dist/html/layout/floats.js +26 -0
  71. package/dist/html/layout/floats.js.map +1 -1
  72. package/dist/html/layout/inline.d.ts +29 -2
  73. package/dist/html/layout/inline.d.ts.map +1 -1
  74. package/dist/html/layout/inline.js +1454 -125
  75. package/dist/html/layout/inline.js.map +1 -1
  76. package/dist/html/node.d.ts +33 -0
  77. package/dist/html/node.d.ts.map +1 -1
  78. package/dist/html/node.js +608 -86
  79. package/dist/html/node.js.map +1 -1
  80. package/dist/html/paint.d.ts +53 -1
  81. package/dist/html/paint.d.ts.map +1 -1
  82. package/dist/html/paint.js +746 -214
  83. package/dist/html/paint.js.map +1 -1
  84. package/dist/html/surfaces.d.ts +7 -2
  85. package/dist/html/surfaces.d.ts.map +1 -1
  86. package/dist/html/surfaces.js +16 -0
  87. package/dist/html/surfaces.js.map +1 -1
  88. package/dist/html/svg.d.ts.map +1 -1
  89. package/dist/html/svg.js +27 -20
  90. package/dist/html/svg.js.map +1 -1
  91. package/dist/internal/codelines.d.ts +42 -0
  92. package/dist/internal/codelines.d.ts.map +1 -0
  93. package/dist/internal/codelines.js +170 -0
  94. package/dist/internal/codelines.js.map +1 -0
  95. package/dist/internal/window.d.ts +22 -0
  96. package/dist/internal/window.d.ts.map +1 -1
  97. package/dist/internal/window.js +35 -8
  98. package/dist/internal/window.js.map +1 -1
  99. package/dist/markdown/index.d.ts.map +1 -1
  100. package/dist/markdown/index.js +98 -26
  101. package/dist/markdown/index.js.map +1 -1
  102. package/dist/rich-text-editor/index.d.ts.map +1 -1
  103. package/dist/rich-text-editor/index.js +3 -8
  104. package/dist/rich-text-editor/index.js.map +1 -1
  105. package/dist/rich-text-editor/markdown.d.ts.map +1 -1
  106. package/dist/rich-text-editor/markdown.js +30 -1
  107. package/dist/rich-text-editor/markdown.js.map +1 -1
  108. package/dist/rich-text-editor/replace.d.ts +13 -0
  109. package/dist/rich-text-editor/replace.d.ts.map +1 -0
  110. package/dist/rich-text-editor/replace.js +64 -0
  111. package/dist/rich-text-editor/replace.js.map +1 -0
  112. package/dist/richtext/node.d.ts +7 -0
  113. package/dist/richtext/node.d.ts.map +1 -1
  114. package/dist/richtext/node.js.map +1 -1
  115. package/dist/terminal-output/index.d.ts.map +1 -1
  116. package/dist/terminal-output/index.js +100 -25
  117. package/dist/terminal-output/index.js.map +1 -1
  118. package/dist/terminal-output/runs.d.ts +4 -1
  119. package/dist/terminal-output/runs.d.ts.map +1 -1
  120. package/dist/terminal-output/runs.js +5 -1
  121. package/dist/terminal-output/runs.js.map +1 -1
  122. package/package.json +5 -3
  123. package/src/code/index.ts +34 -32
  124. package/src/code-language/index.ts +1 -1
  125. package/src/code-language/runs.ts +111 -1
  126. package/src/codeblock/index.ts +48 -1
  127. package/src/flow/node.ts +80 -64
  128. package/src/html/controls.ts +50 -9
  129. package/src/html/css/cascade.ts +134 -43
  130. package/src/html/css/color.ts +87 -0
  131. package/src/html/css/parse.ts +107 -4
  132. package/src/html/css/style.ts +386 -23
  133. package/src/html/css/ua.ts +22 -5
  134. package/src/html/css/values.ts +31 -3
  135. package/src/html/css/vars.ts +2 -2
  136. package/src/html/dom.ts +38 -2
  137. package/src/html/index.ts +39 -1
  138. package/src/html/layout/block.ts +360 -22
  139. package/src/html/layout/boxes.ts +98 -23
  140. package/src/html/layout/cache.ts +8 -3
  141. package/src/html/layout/floats.ts +24 -0
  142. package/src/html/layout/inline.ts +1690 -119
  143. package/src/html/node.ts +648 -101
  144. package/src/html/paint.ts +883 -283
  145. package/src/html/surfaces.ts +21 -1
  146. package/src/html/svg.ts +30 -19
  147. package/src/internal/codelines.ts +200 -0
  148. package/src/internal/window.ts +36 -8
  149. package/src/markdown/index.ts +145 -26
  150. package/src/rich-text-editor/index.ts +3 -11
  151. package/src/rich-text-editor/markdown.ts +36 -1
  152. package/src/rich-text-editor/replace.ts +85 -0
  153. package/src/richtext/node.ts +7 -0
  154. package/src/terminal-output/index.ts +127 -34
  155. package/src/terminal-output/runs.ts +16 -2
@@ -42,6 +42,7 @@ import {
42
42
  GRID_TRACKS,
43
43
  FIRST_LINE,
44
44
  INLINE_OFFSETS,
45
+ MOVED_OFF_LINES,
45
46
  LINE_BOX_RAISES,
46
47
  SHIFTED_LINES,
47
48
  TEXT_SHIFTS,
@@ -51,6 +52,7 @@ import type {
51
52
  BoxTree,
52
53
  FirstLineStyler,
53
54
  Intrinsic,
55
+ InlineDecoration,
54
56
  LineBox,
55
57
  MarginStrut,
56
58
  Marker,
@@ -58,6 +60,7 @@ import type {
58
60
  import { FloatContext } from './floats.js';
59
61
  import {
60
62
  faceExtentOf,
63
+ joinsBefore,
61
64
  layoutInline,
62
65
  lineHeightMultiplier,
63
66
  spaceBreaksOnly,
@@ -69,7 +72,15 @@ import type { TextRun } from '../../richtext/index.js';
69
72
  import { layoutFlex } from './flex.js';
70
73
  import { finishCaptions, layoutTable } from './table.js';
71
74
  import { collapseEdges } from './collapse.js';
72
- import { computePaintBounds, hoistNegative, stackLayers } from '../paint.js';
75
+ import {
76
+ clipsFor,
77
+ computePaintBounds,
78
+ FIXED_BOXES,
79
+ fixedToViewport,
80
+ hoistNegative,
81
+ OUT_OF_FLOW_REACH,
82
+ stackLayers,
83
+ } from '../paint.js';
73
84
 
74
85
  export interface LayoutContext {
75
86
  fonts: FontsLike | null;
@@ -180,7 +191,9 @@ export function layoutDocument(
180
191
  // there — or `<p>hi</p>` would stand 8px lower than the same paragraph in
181
192
  // `<body>`.
182
193
  resolveEdges(root, viewportWidth);
194
+ const body = fragmentBodyHeight(tree, viewportHeight);
183
195
  const leading = root.marginTop;
196
+ const trailing = root.marginBottom;
184
197
  root.padRight += root.marginRight;
185
198
  root.padBottom += root.marginBottom;
186
199
  root.padLeft += root.marginLeft;
@@ -188,6 +201,7 @@ export function layoutDocument(
188
201
  root.marginRight = 0;
189
202
  root.marginBottom = 0;
190
203
  root.marginLeft = 0;
204
+ if (body) handPercentBase(root, body.inner);
191
205
 
192
206
  const contentWidth = Math.max(0, viewportWidth - root.horizontalExtra);
193
207
  const floats = new FloatContext(root.contentX, root.contentX + contentWidth);
@@ -195,7 +209,7 @@ export function layoutDocument(
195
209
  // height on the root element resolves against (CSS 2.1 10.1, 10.5); a
196
210
  // fragment has no root element, and the box standing in for its body has
197
211
  // the body's `auto` height to give
198
- let readsViewportHeight = false;
212
+ let readsViewportHeight = body?.viewport === true;
199
213
  for (const child of root.children) {
200
214
  if (child.el?.name !== 'html') continue;
201
215
  child.percentHeightBase = viewportHeight;
@@ -218,7 +232,9 @@ export function layoutDocument(
218
232
  );
219
233
  const floatBottom =
220
234
  floats.bottom === -Infinity ? 0 : floats.bottom - root.contentY;
221
- root.height = Math.max(flow.height, floatBottom) + root.verticalExtra;
235
+ root.height = body
236
+ ? leading + body.outer + trailing
237
+ : Math.max(flow.height, floatBottom) + root.verticalExtra;
222
238
 
223
239
  // Positioned boxes last, and in the order they were found, so a later one
224
240
  // can be positioned against an earlier one's resolved rectangle.
@@ -231,19 +247,35 @@ export function layoutDocument(
231
247
  // against where it *was* — which is the whole of what makes it relative —
232
248
  // and the ink bounds are computed after that, so culling sees where boxes
233
249
  // ended up rather than where they were laid out.
234
- if (tree.relative) applyRelativeOffsets(root);
250
+ if (tree.relative) applyRelativeOffsets(root, viewportWidth, viewportHeight);
235
251
  const reach = computePaintBounds(root, tree.movedInline);
236
252
  if (tree.negative) hoistNegative(root);
237
253
  stackLayers(root);
238
254
 
239
255
  // The document is as tall as what overflows the root, not the root: an
240
256
  // `html, body { height: 100% }` a window tall holds a message longer than
241
- // the window, and the element sizes to all of it.
257
+ // the window, and the element sizes to all of it. Not what a box that
258
+ // clips its overflow holds: that is its own to scroll, and the root's
259
+ // scrollable overflow takes in a positioned box only where no box on the
260
+ // way to it clips it (CSS Overflow 3, 2.2): an absolute box 900px down
261
+ // an `overflow: hidden` card made a page a browser shows 400px tall run
262
+ // on to 980, blank
242
263
  let bottom = Math.max(root.height, reach);
264
+ let fixed: Box[] | null = null;
243
265
  for (const { box } of ctx.positioned) {
244
- if (box.style.position !== 'fixed')
245
- bottom = Math.max(bottom, box.y + box.height);
266
+ if (box.style.position === 'fixed') {
267
+ if (fixedToViewport(box)) (fixed ??= []).push(box);
268
+ continue;
269
+ }
270
+ if (clipsFor(box, root).length) continue;
271
+ // and what it holds, where it does not clip it: a page set in an
272
+ // absolute wrapper 497px tall runs on below it, and the document with
273
+ // it — design 094 was a window tall and no taller, its text past the
274
+ // end of the scroll
275
+ bottom = Math.max(bottom, OUT_OF_FLOW_REACH.get(box) ?? box.y + box.height);
246
276
  }
277
+ if (fixed) FIXED_BOXES.set(tree, fixed);
278
+ else FIXED_BOXES.delete(tree);
247
279
  return {
248
280
  width: viewportWidth,
249
281
  height: bottom,
@@ -251,6 +283,42 @@ export function layoutDocument(
251
283
  };
252
284
  }
253
285
 
286
+ /**
287
+ * How tall the body a fragment's root box stands in for says it is, where
288
+ * that is definite (CSS 2.1 10.5): a length, `100vh` among them, or a
289
+ * percentage of the `<html>` implied around it where that has a height of
290
+ * its own — a length, or a percentage of the viewport, as `html, body {
291
+ * height: 100% }` fills a window. Its border box, and its content box,
292
+ * which what it holds takes its percentages of; `viewport` where the
293
+ * answer read the viewport's height. Null where the document has an
294
+ * `<html>` or a `<body>`, and where the body's height is `auto`, and the
295
+ * box is as tall as what is in it. Measured before its margins are folded
296
+ * into its padding.
297
+ */
298
+ function fragmentBodyHeight(
299
+ tree: BoxTree,
300
+ viewportHeight: number,
301
+ ): { outer: number; inner: number; viewport: boolean } | null {
302
+ const html = tree.impliedHtml;
303
+ if (!html) return null;
304
+ const root = tree.root;
305
+ const style = root.style;
306
+ const base = resolveOrNull(html.height, viewportHeight);
307
+ const set = resolveOrNull(style.height, base ?? NaN);
308
+ if (set === null) return null;
309
+ root.percentHeightBase = base ?? NaN;
310
+ const extra = root.verticalExtra;
311
+ const outer = clampHeight(
312
+ root,
313
+ style.boxSizing === 'border-box' ? Math.max(set, extra) : set + extra,
314
+ );
315
+ return {
316
+ outer,
317
+ inner: Math.max(0, outer - extra),
318
+ viewport: isPct(style.height) && isPct(html.height),
319
+ };
320
+ }
321
+
254
322
  /** No margin at all, shared: most joins leave a strut as it was. */
255
323
  const NO_MARGIN: MarginStrut = Object.freeze({ pos: 0, neg: 0 });
256
324
 
@@ -725,6 +793,12 @@ function layoutChildren(
725
793
  // this box's first formatted line is its first child's in flow (CSS 2.1
726
794
  // 5.12.1), which its `::first-line` is handed to
727
795
  let firstLine = ctx.firstLine ? firstLineOf(box) : null;
796
+ // and the room a list item's marker takes on it (`MARKER_ROOM`): an
797
+ // inside marker's is handed down to the line it is on, and an outside
798
+ // one's is room above the baseline of the first child that has a line,
799
+ // which goes lower where the marker reaches higher (`MARKER_ASCENT`)
800
+ let pushed = MARKER_ASCENT.get(box) ?? null;
801
+ let marked = pushed === null ? (MARKER_ROOM.get(box) ?? null) : null;
728
802
  const clamp = ctx.clamp ?? null;
729
803
 
730
804
  for (const child of box.children) {
@@ -743,6 +817,10 @@ function layoutChildren(
743
817
  if (!FIRST_LINE.has(child)) HANDED.set(child, firstLine);
744
818
  firstLine = null;
745
819
  }
820
+ if (marked && !child.outOfFlow && !child.isFloat) {
821
+ MARKER_ROOM.set(child, marked);
822
+ marked = null;
823
+ }
746
824
  if (child.outOfFlow) {
747
825
  placeStatic(
748
826
  child,
@@ -814,6 +892,7 @@ function layoutChildren(
814
892
  }
815
893
 
816
894
  const counted = clamp?.fit?.lines ?? 0;
895
+ const floatMark = floats.count;
817
896
  if (
818
897
  !floats.isEmpty &&
819
898
  (child.kind === 'replaced' || establishesBFC(child))
@@ -822,6 +901,17 @@ function layoutChildren(
822
901
  } else {
823
902
  layoutBlockLevel(child, ctx, floats, contentLeft, childY, contentWidth);
824
903
  }
904
+ if (pushed !== null) {
905
+ const line = firstLineIn(child);
906
+ if (line) {
907
+ const push = pushed - (line.y + line.baseline - child.y);
908
+ if (push > 0) {
909
+ translate(child, 0, push);
910
+ floats.moveSince(floatMark, push);
911
+ }
912
+ pushed = null;
913
+ }
914
+ }
825
915
  // a block with none of the flow's lines in it — an image, a box of a
826
916
  // formatting context of its own — puts the clamp point of an `auto`
827
917
  // clamp before it where it ends below the room
@@ -870,12 +960,15 @@ function layoutChildren(
870
960
  // escapes, and the caller merges it into the box's own bottom margin so
871
961
  // the next sibling still sees it. Dropping it here was the bug that made
872
962
  // `<div><p>…</p></div><p>…</p>` set the two paragraphs solid.
963
+ // A percentage of a height that depends on content computes to `auto`
964
+ // (CSS 2.1 10.5), and lets it through as `auto` does: a `height: 100%`
965
+ // wrapper in an `auto` body kept its last child's margin inside it
873
966
  if (
874
967
  !first &&
875
968
  !cleared &&
876
969
  !box.borderBottom &&
877
970
  !box.padBottom &&
878
- box.style.height === AUTO &&
971
+ specifiedHeight(box) === null &&
879
972
  !establishesBFC(box)
880
973
  ) {
881
974
  const height = y - contentTop;
@@ -978,11 +1071,20 @@ function layoutBesideFloats(
978
1071
  band = over;
979
1072
  continue;
980
1073
  }
1074
+ // Where no float narrows the room on either side, the box is where
1075
+ // it would be were there none, and one too wide for its containing
1076
+ // block overflows it here: below the floats it would be the same box
1077
+ // in the same room. Blink tests a fit only against a side a float is
1078
+ // on (`can_expand_outside_opportunity`, BlockLayoutAlgorithm::
1079
+ // LayoutNewFormattingContext). Design 209's 247px heading, which
1080
+ // clips its overflow, in a 240px column beside a float that ends
1081
+ // left of the column, went under the float, 555px down.
1082
+ if (lo === -Infinity && hi === Infinity) fits = true;
981
1083
  // What is too wide overflows at the end of the line, as far as a
982
1084
  // negative margin there takes it, but no further into a float or
983
1085
  // past the room; at the start it would run into the floats, and
984
1086
  // waits below them (the suite's floats-wrap-bfc-with-margin tests)
985
- if (inRoom) {
1087
+ else if (inRoom) {
986
1088
  fits = box.marginLeft + box.width + box.marginRight <= room + 0.5;
987
1089
  } else if (rtl) {
988
1090
  const limit = Math.max(contentLeft + Math.min(0, box.marginLeft), lo);
@@ -1063,6 +1165,7 @@ function layoutInlineContent(
1063
1165
  floats,
1064
1166
  originX: contentLeft,
1065
1167
  clipText: ctx.clipText,
1168
+ firstStrut: MARKER_ROOM.get(box),
1066
1169
  floatBoxes: floated
1067
1170
  ? {
1068
1171
  size: (child: Box) => sizeFloat(child, ctx, contentWidth),
@@ -1260,7 +1363,8 @@ function makesNothing(box: Box): boolean {
1260
1363
  }
1261
1364
 
1262
1365
  /** Whether a `::first-line` style sets its line's text otherwise than its
1263
- * block's: in another face, size or line height, or spaced apart. */
1366
+ * block's: in another face, size or line height, spaced apart, or in
1367
+ * capitals. */
1264
1368
  function setsFonts(line: ComputedStyle, block: ComputedStyle): boolean {
1265
1369
  return (
1266
1370
  line.fontFamily !== block.fontFamily ||
@@ -1275,7 +1379,8 @@ function setsFonts(line: ComputedStyle, block: ComputedStyle): boolean {
1275
1379
  line.fontVariantLigatures !== block.fontVariantLigatures ||
1276
1380
  line.fontVariantPosition !== block.fontVariantPosition ||
1277
1381
  line.fontKerning !== block.fontKerning ||
1278
- line.fontFeatureSettings !== block.fontFeatureSettings
1382
+ line.fontFeatureSettings !== block.fontFeatureSettings ||
1383
+ line.textTransform !== block.textTransform
1279
1384
  );
1280
1385
  }
1281
1386
 
@@ -1685,13 +1790,28 @@ function layoutBox(
1685
1790
  : outerFloats;
1686
1791
  // the line a pass before gave a marker, which this one may not
1687
1792
  if (box.marker) box.lines = null;
1793
+ if (box.marker && ctx.fonts) {
1794
+ const room = markerRoom(box, box.marker, ctx.fonts);
1795
+ if (room) MARKER_ROOM.set(box, room);
1796
+ else MARKER_ROOM.delete(box);
1797
+ if (box.style.listStylePosition === 'inside') MARKER_ASCENT.delete(box);
1798
+ else MARKER_ASCENT.set(box, markerAscent(box, box.marker, ctx.fonts));
1799
+ }
1688
1800
  const flow = layoutChildren(box, ctx, floats, box.contentY, contentWidth);
1689
1801
  // A list item with a marker and no line holds one, the marker's, a line
1690
1802
  // of its own face tall: an empty `<li>` is a line tall in a browser, and
1691
1803
  // an inline-block around one sits on its marker's baseline.
1692
1804
  let height = flow.height;
1693
1805
  if (box.marker && ctx.fonts && !firstLineIn(box)) {
1694
- const strut = strutOf(ctx.fonts, box.style);
1806
+ const own = strutOf(ctx.fonts, box.style);
1807
+ // as tall as the marker on it, as a first line with text is
1808
+ const room = MARKER_ROOM.get(box);
1809
+ const strut = room
1810
+ ? {
1811
+ ascent: Math.max(own.ascent, room.ascent),
1812
+ descent: Math.max(own.descent, room.descent),
1813
+ }
1814
+ : own;
1695
1815
  const line = strut.ascent + strut.descent;
1696
1816
  box.lines = [
1697
1817
  {
@@ -1735,6 +1855,55 @@ function layoutBox(
1735
1855
  if (box.marker) layoutMarker(box, box.marker, ctx);
1736
1856
  }
1737
1857
 
1858
+ /**
1859
+ * The room a list item's marker takes about its first line's baseline,
1860
+ * handed to the box that line is in: an image's height above it, its
1861
+ * bottom on the baseline as an inline image's is, and a marker in a face
1862
+ * of its own — a `::marker` rule's — its face at its line height. CSS 2.1
1863
+ * (12.5.1) leaves where an outside marker goes to the user agent; Blink
1864
+ * and Gecko both set it on the item's first line and make the line as tall
1865
+ * as it, and design 032's bullets, images taller than its 10px text, set
1866
+ * every item's first line lower than ours did.
1867
+ */
1868
+ const MARKER_ROOM = new WeakMap<Box, InlineDecoration>();
1869
+
1870
+ function markerRoom(
1871
+ box: Box,
1872
+ marker: Marker,
1873
+ fonts: FontsLike,
1874
+ ): InlineDecoration | null {
1875
+ if (marker.image) {
1876
+ return marker.image.height > 0
1877
+ ? { ascent: marker.image.height, descent: 0 }
1878
+ : null;
1879
+ }
1880
+ return marker.style && marker.style !== box.style
1881
+ ? strutOf(fonts, marker.style)
1882
+ : null;
1883
+ }
1884
+
1885
+ /**
1886
+ * How far an outside marker reaches above the baseline it sits on. The
1887
+ * marker is a line of its own face, the item's or a `::marker` rule's, so a
1888
+ * bullet or a number reaches its face's ascent on that line, and an image,
1889
+ * set on the line bottom on the baseline, the higher of that and its own
1890
+ * height. Where the item's first line is in a block inside it — a link set
1891
+ * `display: block`, a paragraph — and the block's first baseline is nearer
1892
+ * its top than that, the block goes lower by the difference, and its line
1893
+ * stays the height it was: CSS 2.1 (12.5.1) leaves this to the user agent,
1894
+ * and Blink aligns the marker's baseline with the block's and pushes the
1895
+ * block down (`UnpositionedListMarker::AddToBox`). Its first line grew
1896
+ * instead, and design 196's links, under 18px bullets, were 21px tall where
1897
+ * Chrome has them 14px, 7px lower. On the item's own line the two come to
1898
+ * the same: the line grows by what the push would have moved it.
1899
+ */
1900
+ const MARKER_ASCENT = new WeakMap<Box, number>();
1901
+
1902
+ function markerAscent(box: Box, marker: Marker, fonts: FontsLike): number {
1903
+ const ascent = strutOf(fonts, marker.style ?? box.style).ascent;
1904
+ return marker.image ? Math.max(marker.image.height, ascent) : ascent;
1905
+ }
1906
+
1738
1907
  /** The first line box anywhere under a box, in layout order. */
1739
1908
  function firstLineIn(box: Box): LineBox | null {
1740
1909
  if (box.lines?.length) return box.lines[0];
@@ -2732,10 +2901,19 @@ function wordBound(box: Box, fonts: FontsLike, measured: boolean): number {
2732
2901
  let last: object | null = null;
2733
2902
  for (const line of box.lines) {
2734
2903
  if (unbroken) widest = Math.max(widest, line.width);
2904
+ // a word across inline boxes' edges is in two fragments of text, and
2905
+ // as wide as its parts in both and the edges between: bound by the
2906
+ // widest word of each, a float in a narrow room took a letter of a
2907
+ // word of letters in bordered spans for its least width, and was cut
2908
+ // to the room (`joinsBefore`)
2909
+ let chain = 0;
2735
2910
  for (const text of line.texts) {
2736
- if (text.layout === last) continue;
2911
+ const joins = joinsBefore(text);
2912
+ if (joins === undefined && text.layout === last) continue;
2737
2913
  last = text.layout;
2738
- widest = Math.max(widest, widestWord(fonts, text.layout, measured));
2914
+ const own = widestWord(fonts, text.layout, measured);
2915
+ chain = joins === undefined ? own : chain + joins + own;
2916
+ widest = Math.max(widest, chain);
2739
2917
  }
2740
2918
  for (const placed of line.atomics) {
2741
2919
  const b = placed.box;
@@ -4008,19 +4186,35 @@ export function establishesBFC(box: Box): boolean {
4008
4186
 
4009
4187
  /** What a box's `position: relative` offset moves it by, applied after
4010
4188
  * layout so it does not affect anything else's position — which is the
4011
- * whole of what makes it *relative* — and its translation with it. */
4012
- export function applyRelativeOffsets(box: Box): void {
4013
- for (const child of box.children) applyRelativeOffsets(child);
4189
+ * whole of what makes it *relative* — and its translation with it, and
4190
+ * where `position: sticky` puts one at rest (`stickyOffset`). */
4191
+ export function applyRelativeOffsets(
4192
+ box: Box,
4193
+ viewportWidth: number,
4194
+ viewportHeight: number,
4195
+ ): void {
4196
+ for (const child of box.children)
4197
+ applyRelativeOffsets(child, viewportWidth, viewportHeight);
4014
4198
  const style = box.style;
4015
4199
  if (transformed(style) && box.kind !== 'inline') {
4016
4200
  // a transform moves the box it is on, and an inline box is none
4017
4201
  const [dx, dy] = translationOf(box);
4018
- if (dx || dy) translate(box, dx, dy);
4202
+ if (dx || dy) {
4203
+ translate(box, dx, dy);
4204
+ movedOffLine(box);
4205
+ }
4019
4206
  }
4020
4207
  if (style.position !== 'relative' && style.position !== 'sticky') return;
4021
- const [dx, dy] = relativeOffset(box);
4208
+ const [dx, dy] =
4209
+ style.position === 'sticky'
4210
+ ? stickyOffset(box, viewportWidth, viewportHeight)
4211
+ : relativeOffset(box);
4212
+ if (!(dx || dy)) return;
4022
4213
  translate(box, dx, dy);
4023
- if (box.kind !== 'inline' || !(dx || dy)) return;
4214
+ if (box.kind !== 'inline') {
4215
+ movedOffLine(box);
4216
+ return;
4217
+ }
4024
4218
  offsetInline(box, dx, dy);
4025
4219
  // the blocks it was broken around move with it (9.2.1.1)
4026
4220
  const blocks = box.cut ? CUT_BLOCKS.get(box) : undefined;
@@ -4035,10 +4229,10 @@ export function transformed(style: ComputedStyle): boolean {
4035
4229
 
4036
4230
  /** How far `translate` and a `transform` move a box: a percentage is of
4037
4231
  * its own border box (CSS Transforms 1, 7). */
4038
- function translationOf(box: Box): [number, number] {
4232
+ function translationOf(box: Box, style = box.style): [number, number] {
4039
4233
  let dx = 0;
4040
4234
  let dy = 0;
4041
- for (const moved of [box.style.translate, box.style.transformTranslate]) {
4235
+ for (const moved of [style.translate, style.transformTranslate]) {
4042
4236
  if (!moved) continue;
4043
4237
  dx += resolve(moved[0], box.width, 0);
4044
4238
  dy += resolve(moved[1], box.height, 0);
@@ -4046,6 +4240,21 @@ function translationOf(box: Box): [number, number] {
4046
4240
  return [dx, dy];
4047
4241
  }
4048
4242
 
4243
+ /**
4244
+ * Move a laid-out box, and everything in it, by how far its translation
4245
+ * changed when its style went from `was` to the one it has — what
4246
+ * `applyRelativeOffsets` moves it by now, less what it moved it by then.
4247
+ * For a style changed in place, where nothing else moves (a `transform` is
4248
+ * what makes that true). Not for an inline box, whose text is what moves.
4249
+ */
4250
+ export function retranslate(box: Box, was: ComputedStyle): void {
4251
+ const [x0, y0] = translationOf(box, was);
4252
+ const [x1, y1] = translationOf(box);
4253
+ if (x1 === x0 && y1 === y0) return;
4254
+ translate(box, x1 - x0, y1 - y0);
4255
+ movedOffLine(box);
4256
+ }
4257
+
4049
4258
  /** How far `position: relative` moves a box. */
4050
4259
  function relativeOffset(box: Box): [number, number] {
4051
4260
  const style = box.style;
@@ -4064,6 +4273,111 @@ function relativeOffset(box: Box): [number, number] {
4064
4273
  return [dx, dy];
4065
4274
  }
4066
4275
 
4276
+ /**
4277
+ * Where `position: sticky` puts a box at rest (CSS Positioned Layout 3,
4278
+ * 3.4): moved only as far as keeps its border box inside the sticky view
4279
+ * rectangle — its nearest scroll container's scrollport, scrolled to its
4280
+ * start, less its insets — and no further than keeps its margin box in its
4281
+ * containing block, `top` winning over `bottom` and the start side over the
4282
+ * end. Nothing here scrolls a box the document holds, so for one of those
4283
+ * this is where the box stays. The viewport does scroll, and the document
4284
+ * with it, and a sticky box does not follow: against the viewport only its
4285
+ * `top` and its start side are kept, which at rest put a box as a browser
4286
+ * does, while a `bottom: 0` would pin a footer to the middle of the page
4287
+ * once it was scrolled. Taken as `relative`, a box stood its `top` below
4288
+ * where a browser starts it: Wikipedia's contents, 24px low.
4289
+ */
4290
+ function stickyOffset(
4291
+ box: Box,
4292
+ viewportWidth: number,
4293
+ viewportHeight: number,
4294
+ ): [number, number] {
4295
+ const style = box.style;
4296
+ // the scroll container a sticky box keeps to: the nearest box that
4297
+ // scrolls, or the viewport, whose are the root's and the body's overflow
4298
+ let port = box.parent;
4299
+ while (port && !scrolls(port.style)) port = port.parent;
4300
+ const tag = port?.el?.name;
4301
+ const inside = !!port && tag !== 'html' && tag !== 'body';
4302
+ let cb = box.parent;
4303
+ while (cb && cb.kind === 'inline') cb = cb.parent;
4304
+ if (!cb) return [0, 0];
4305
+ // the scrollport, at its start: a scroll container's padding box, or
4306
+ // the viewport, which the document's top is at
4307
+ const px = port && inside ? port.x + port.borderLeft : 0;
4308
+ const py = port && inside ? port.y + port.borderTop : 0;
4309
+ const pw =
4310
+ port && inside
4311
+ ? port.width - port.borderLeft - port.borderRight
4312
+ : viewportWidth;
4313
+ const ph =
4314
+ port && inside
4315
+ ? port.height - port.borderTop - port.borderBottom
4316
+ : viewportHeight;
4317
+ const dx = stuck(
4318
+ box.x,
4319
+ box.width,
4320
+ box.marginLeft,
4321
+ box.marginRight,
4322
+ cb.contentX,
4323
+ cb.contentWidth,
4324
+ px,
4325
+ pw,
4326
+ resolveOrNull(style.left, pw),
4327
+ inside ? resolveOrNull(style.right, pw) : null,
4328
+ // `left` wins where both hold, or `right` in a right-to-left block
4329
+ cb.style.direction !== 'rtl',
4330
+ );
4331
+ const dy = stuck(
4332
+ box.y,
4333
+ box.height,
4334
+ box.marginTop,
4335
+ box.marginBottom,
4336
+ cb.contentY,
4337
+ cb.contentHeight,
4338
+ py,
4339
+ ph,
4340
+ resolveOrNull(style.top, ph),
4341
+ inside ? resolveOrNull(style.bottom, ph) : null,
4342
+ true,
4343
+ );
4344
+ return [dx, dy];
4345
+ }
4346
+
4347
+ /**
4348
+ * How far a sticky box moves along one axis: toward the end until its
4349
+ * start edge is `start` inside the scrollport's, or toward the start until
4350
+ * its end edge is `end` inside the scrollport's, each no further than its
4351
+ * margin box stays in its containing block — and where both would move
4352
+ * it, `startWins` says which does.
4353
+ */
4354
+ function stuck(
4355
+ at: number,
4356
+ size: number,
4357
+ marginStart: number,
4358
+ marginEnd: number,
4359
+ cbAt: number,
4360
+ cbSize: number,
4361
+ portAt: number,
4362
+ portSize: number,
4363
+ start: number | null,
4364
+ end: number | null,
4365
+ startWins: boolean,
4366
+ ): number {
4367
+ let byStart = 0;
4368
+ if (start !== null && Number.isFinite(start) && at < portAt + start) {
4369
+ const room = cbAt + cbSize - (at + size + marginEnd);
4370
+ byStart = Math.max(0, Math.min(portAt + start - at, room));
4371
+ }
4372
+ let byEnd = 0;
4373
+ if (end !== null && Number.isFinite(end)) {
4374
+ const over = at + size - (portAt + portSize - end);
4375
+ const room = at - marginStart - cbAt;
4376
+ if (over > 0) byEnd = -Math.max(0, Math.min(over, room));
4377
+ }
4378
+ return startWins ? byStart || byEnd : byEnd || byStart;
4379
+ }
4380
+
4067
4381
  /**
4068
4382
  * Move an inline box's text where `position: relative` puts the box. Its
4069
4383
  * text is on its block's lines, laid out apart from the text around it
@@ -4089,10 +4403,34 @@ function offsetInline(box: Box, dx: number, dy: number): void {
4089
4403
  y: (was?.y ?? 0) + dy,
4090
4404
  });
4091
4405
  SHIFTED_LINES.add(line);
4406
+ markMovedOff(lines, line);
4407
+ }
4408
+ }
4409
+ }
4410
+
4411
+ /** Mark the line an inline-block or an image a box moved off was placed
4412
+ * on, where it was on one: the line stays, and the box is drawn where it
4413
+ * went (`MOVED_OFF_LINES`). */
4414
+ function movedOffLine(box: Box): void {
4415
+ let block = box.parent;
4416
+ while (block && block.kind === 'inline') block = block.parent;
4417
+ const lines = block?.lines;
4418
+ if (!lines) return;
4419
+ for (const line of lines) {
4420
+ for (const placed of line.atomics) {
4421
+ if (placed.box !== box) continue;
4422
+ markMovedOff(lines, line);
4423
+ return;
4092
4424
  }
4093
4425
  }
4094
4426
  }
4095
4427
 
4428
+ function markMovedOff(lines: LineBox[], line: LineBox): void {
4429
+ const moved = MOVED_OFF_LINES.get(lines);
4430
+ if (moved) moved.add(line);
4431
+ else MOVED_OFF_LINES.set(lines, new Set([line]));
4432
+ }
4433
+
4096
4434
  /** Whether `inner` is `outer` or inside it. */
4097
4435
  function holds(outer: Box, inner: Box): boolean {
4098
4436
  for (let at: Box | null = inner; at; at = at.parent) {