@react-x11/components 0.10.0 → 0.11.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 (90) 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/html/css/cascade.d.ts +32 -10
  17. package/dist/html/css/cascade.d.ts.map +1 -1
  18. package/dist/html/css/cascade.js +72 -27
  19. package/dist/html/css/cascade.js.map +1 -1
  20. package/dist/html/css/style.d.ts +7 -4
  21. package/dist/html/css/style.d.ts.map +1 -1
  22. package/dist/html/css/style.js +37 -6
  23. package/dist/html/css/style.js.map +1 -1
  24. package/dist/html/css/ua.js +8 -5
  25. package/dist/html/css/ua.js.map +1 -1
  26. package/dist/html/layout/block.d.ts.map +1 -1
  27. package/dist/html/layout/block.js +127 -9
  28. package/dist/html/layout/block.js.map +1 -1
  29. package/dist/html/layout/boxes.d.ts +27 -0
  30. package/dist/html/layout/boxes.d.ts.map +1 -1
  31. package/dist/html/layout/boxes.js +30 -2
  32. package/dist/html/layout/boxes.js.map +1 -1
  33. package/dist/html/layout/inline.d.ts +3 -0
  34. package/dist/html/layout/inline.d.ts.map +1 -1
  35. package/dist/html/layout/inline.js +236 -14
  36. package/dist/html/layout/inline.js.map +1 -1
  37. package/dist/html/node.d.ts.map +1 -1
  38. package/dist/html/node.js +118 -57
  39. package/dist/html/node.js.map +1 -1
  40. package/dist/html/paint.d.ts.map +1 -1
  41. package/dist/html/paint.js +203 -37
  42. package/dist/html/paint.js.map +1 -1
  43. package/dist/html/svg.d.ts.map +1 -1
  44. package/dist/html/svg.js +27 -20
  45. package/dist/html/svg.js.map +1 -1
  46. package/dist/internal/codelines.d.ts +42 -0
  47. package/dist/internal/codelines.d.ts.map +1 -0
  48. package/dist/internal/codelines.js +170 -0
  49. package/dist/internal/codelines.js.map +1 -0
  50. package/dist/markdown/index.d.ts.map +1 -1
  51. package/dist/markdown/index.js +98 -26
  52. package/dist/markdown/index.js.map +1 -1
  53. package/dist/rich-text-editor/index.d.ts.map +1 -1
  54. package/dist/rich-text-editor/index.js +3 -8
  55. package/dist/rich-text-editor/index.js.map +1 -1
  56. package/dist/rich-text-editor/markdown.d.ts.map +1 -1
  57. package/dist/rich-text-editor/markdown.js +30 -1
  58. package/dist/rich-text-editor/markdown.js.map +1 -1
  59. package/dist/rich-text-editor/replace.d.ts +13 -0
  60. package/dist/rich-text-editor/replace.d.ts.map +1 -0
  61. package/dist/rich-text-editor/replace.js +64 -0
  62. package/dist/rich-text-editor/replace.js.map +1 -0
  63. package/dist/terminal-output/index.d.ts.map +1 -1
  64. package/dist/terminal-output/index.js +100 -25
  65. package/dist/terminal-output/index.js.map +1 -1
  66. package/dist/terminal-output/runs.d.ts +4 -1
  67. package/dist/terminal-output/runs.d.ts.map +1 -1
  68. package/dist/terminal-output/runs.js +5 -1
  69. package/dist/terminal-output/runs.js.map +1 -1
  70. package/package.json +1 -1
  71. package/src/code/index.ts +34 -32
  72. package/src/code-language/index.ts +1 -1
  73. package/src/code-language/runs.ts +111 -1
  74. package/src/codeblock/index.ts +48 -1
  75. package/src/html/css/cascade.ts +117 -39
  76. package/src/html/css/style.ts +43 -15
  77. package/src/html/css/ua.ts +8 -5
  78. package/src/html/layout/block.ts +134 -8
  79. package/src/html/layout/boxes.ts +43 -2
  80. package/src/html/layout/inline.ts +257 -13
  81. package/src/html/node.ts +145 -79
  82. package/src/html/paint.ts +236 -45
  83. package/src/html/svg.ts +30 -19
  84. package/src/internal/codelines.ts +200 -0
  85. package/src/markdown/index.ts +145 -26
  86. package/src/rich-text-editor/index.ts +3 -11
  87. package/src/rich-text-editor/markdown.ts +36 -1
  88. package/src/rich-text-editor/replace.ts +85 -0
  89. package/src/terminal-output/index.ts +127 -34
  90. package/src/terminal-output/runs.ts +16 -2
@@ -514,6 +514,46 @@ export interface PointerState {
514
514
 
515
515
  const NO_POINTER: PointerState = { hovered: new Set(), active: new Set() };
516
516
 
517
+ /**
518
+ * The face a style's font-relative units are measured in: its family list,
519
+ * size, weight and slant, the four that pick one. A family's faces can be
520
+ * different fonts altogether — a page's `@font-face` rules may set its
521
+ * bold in another file — so an `ex` in bold text is the bold face's.
522
+ */
523
+ export interface MetricFace {
524
+ family: string;
525
+ size: number;
526
+ weight: number;
527
+ style: ComputedStyle['fontStyle'];
528
+ }
529
+
530
+ /** What a face measures, where the fonts can say. */
531
+ export type FaceMetric = (face: MetricFace) => number | null;
532
+
533
+ /** What a style's face is read from: a computed style, or the part of one
534
+ * a declaration's font-relative units are measured against. */
535
+ type FaceSource = Pick<
536
+ ComputedStyle,
537
+ 'fontFamily' | 'fontSize' | 'fontWeight' | 'fontStyle'
538
+ >;
539
+
540
+ /** …and its line height, for `lh`. */
541
+ type LineSource = FaceSource &
542
+ Pick<ComputedStyle, 'lineHeight' | 'lineHeightIsLength'>;
543
+
544
+ function faceOf(style: FaceSource): MetricFace {
545
+ return {
546
+ family: style.fontFamily,
547
+ size: style.fontSize,
548
+ weight: style.fontWeight,
549
+ style: style.fontStyle,
550
+ };
551
+ }
552
+
553
+ function faceKey(style: FaceSource): string {
554
+ return `${style.fontFamily}\u0001${style.fontSize}\u0001${style.fontWeight}\u0001${style.fontStyle}`;
555
+ }
556
+
517
557
  /**
518
558
  * A cascade over a fixed set of stylesheets. Rebuilt when the sheets change;
519
559
  * re-run when the DOM, the viewport band or the pointer state does.
@@ -564,17 +604,15 @@ export class Cascade {
564
604
  /** The counter styles the sheets define, over the predefined ones. */
565
605
  readonly counterStyles: CounterStyles;
566
606
 
567
- /** A font's x-height at a size, for `ex`, where the fonts can say. */
568
- private _xHeightOf: ((family: string, size: number) => number | null) | null;
607
+ /** A face's x-height, for `ex`, where the fonts can say. */
608
+ private _xHeightOf: FaceMetric | null;
569
609
  private _xHeights = new Map<string, number>();
570
- /** The advance of a font's "0" at a size, for `ch`, likewise. */
571
- private _zeroWidthOf:
572
- ((family: string, size: number) => number | null) | null;
610
+ /** The advance of a face's "0", for `ch`, likewise. */
611
+ private _zeroWidthOf: FaceMetric | null;
573
612
  private _zeroWidths = new Map<string, number>();
574
- /** A font's own line height at a size, for `lh` where `line-height` is
575
- * `normal`, likewise. */
576
- private _normalLineOf:
577
- ((family: string, size: number) => number | null) | null;
613
+ /** A face's own line height, for `lh` where `line-height` is `normal`,
614
+ * likewise. */
615
+ private _normalLineOf: FaceMetric | null;
578
616
  private _normalLines = new Map<string, number>();
579
617
  /** Whether any declaration sets a custom property or reads one: without
580
618
  * one, no element's style asks about them. */
@@ -599,9 +637,9 @@ export class Cascade {
599
637
  viewportWidth: number,
600
638
  viewportHeight: number,
601
639
  scale = 1,
602
- xHeight: ((family: string, size: number) => number | null) | null = null,
603
- zeroWidth: ((family: string, size: number) => number | null) | null = null,
604
- normalLine: ((family: string, size: number) => number | null) | null = null,
640
+ xHeight: FaceMetric | null = null,
641
+ zeroWidth: FaceMetric | null = null,
642
+ normalLine: FaceMetric | null = null,
605
643
  families: FontFamilies | null = null,
606
644
  ) {
607
645
  this._xHeightOf = xHeight;
@@ -1082,13 +1120,13 @@ export class Cascade {
1082
1120
  return style;
1083
1121
  }
1084
1122
 
1085
- /** The x-height of a style's font: the font's own, asked once per face
1086
- * and size, or half an em. */
1087
- private _exOf(style: ComputedStyle): number {
1088
- const key = `${style.fontFamily}\u0001${style.fontSize}`;
1123
+ /** The x-height of a style's font: the font's own, asked once per face,
1124
+ * or half an em. */
1125
+ private _exOf(style: FaceSource): number {
1126
+ const key = faceKey(style);
1089
1127
  let ex = this._xHeights.get(key);
1090
1128
  if (ex === undefined) {
1091
- ex = this._xHeightOf?.(style.fontFamily, style.fontSize) ?? NaN;
1129
+ ex = this._xHeightOf?.(faceOf(style)) ?? NaN;
1092
1130
  if (!(ex > 0)) ex = style.fontSize * 0.5;
1093
1131
  this._xHeights.set(key, ex);
1094
1132
  }
@@ -1096,12 +1134,12 @@ export class Cascade {
1096
1134
  }
1097
1135
 
1098
1136
  /** The advance of a style's font's "0": the font's own, asked once per
1099
- * face and size, or half an em. */
1100
- private _chOf(style: ComputedStyle): number {
1101
- const key = `${style.fontFamily}\u0001${style.fontSize}`;
1137
+ * face, or half an em. */
1138
+ private _chOf(style: FaceSource): number {
1139
+ const key = faceKey(style);
1102
1140
  let ch = this._zeroWidths.get(key);
1103
1141
  if (ch === undefined) {
1104
- ch = this._zeroWidthOf?.(style.fontFamily, style.fontSize) ?? NaN;
1142
+ ch = this._zeroWidthOf?.(faceOf(style)) ?? NaN;
1105
1143
  if (!(ch > 0)) ch = style.fontSize * 0.5;
1106
1144
  this._zeroWidths.set(key, ch);
1107
1145
  }
@@ -1109,16 +1147,16 @@ export class Cascade {
1109
1147
  }
1110
1148
 
1111
1149
  /** A style's computed line height as a length, for `lh`: `normal` as
1112
- * its font's own, asked once per face and size, or 1.2em. */
1113
- private _lineHeightOf(style: ComputedStyle): number {
1150
+ * its font's own, asked once per face, or 1.2em. */
1151
+ private _lineHeightOf(style: LineSource): number {
1114
1152
  const set = style.lineHeight;
1115
1153
  if (set !== 'normal') {
1116
1154
  return style.lineHeightIsLength ? set : set * style.fontSize;
1117
1155
  }
1118
- const key = `${style.fontFamily}\u0001${style.fontSize}`;
1156
+ const key = faceKey(style);
1119
1157
  let line = this._normalLines.get(key);
1120
1158
  if (line === undefined) {
1121
- line = this._normalLineOf?.(style.fontFamily, style.fontSize) ?? NaN;
1159
+ line = this._normalLineOf?.(faceOf(style)) ?? NaN;
1122
1160
  if (!(line > 0)) line = style.fontSize * 1.2;
1123
1161
  this._normalLines.set(key, line);
1124
1162
  }
@@ -1154,12 +1192,20 @@ export class Cascade {
1154
1192
  lh: () => this._lineHeightOf(parentStyle),
1155
1193
  rlh: () => this._lineHeightOf(this.initial),
1156
1194
  };
1157
- // the family goes with the size, so an `ex` after it is its font's
1195
+ // The family, the weight and the slant go with the size: together they
1196
+ // pick the face an `ex`, a `ch` or an `lh` in any declaration is
1197
+ // measured in, however the declarations are ordered. A `width: 10ex`
1198
+ // in a sheet under an inline `font-weight: 900` was measured in the
1199
+ // family's regular face.
1158
1200
  let sized = false;
1159
1201
  let keyword = false;
1160
1202
  for (const c of candidates) {
1161
1203
  for (const d of pick(c)) {
1162
- if (d.prop === 'font-family') {
1204
+ if (
1205
+ d.prop === 'font-family' ||
1206
+ d.prop === 'font-weight' ||
1207
+ d.prop === 'font-style'
1208
+ ) {
1163
1209
  this._apply(style, parentStyle, d, ctxParent);
1164
1210
  } else if (d.prop === 'font-size' || d.prop === 'font') {
1165
1211
  // NaN until a size is set, so one the declaration did not take
@@ -1188,21 +1234,43 @@ export class Cascade {
1188
1234
  ) {
1189
1235
  style.fontSize *= mono ? FIXED_SIZE : 1 / FIXED_SIZE;
1190
1236
  }
1237
+ // The face as the first pass settled it. The second applies every
1238
+ // declaration again in cascade order, the font's among them, so the
1239
+ // style passes through the fonts of every rule on the way: read from
1240
+ // it, the `10ex` in `div { font-family: foo; width: 10ex }` was `foo`'s
1241
+ // under an inline `font-family: Ahem` that came after it.
1242
+ const face: FaceSource = {
1243
+ fontFamily: style.fontFamily,
1244
+ fontSize: style.fontSize,
1245
+ fontWeight: style.fontWeight,
1246
+ fontStyle: style.fontStyle,
1247
+ };
1248
+ let line: LineSource | null = null;
1191
1249
  const ctx: UnitContext = {
1192
1250
  ...ctxParent,
1193
1251
  em: style.fontSize,
1194
- ex: () => this._exOf(style),
1195
- ch: () => this._chOf(style),
1196
- lh: () => this._lineHeightOf(style),
1252
+ ex: () => this._exOf(face),
1253
+ ch: () => this._chOf(face),
1254
+ lh: () => this._lineHeightOf(line ?? style),
1197
1255
  };
1198
1256
  // the line height, which an `lh` in any other declaration reads, ahead
1199
- // of them: set again with the rest, in its place among them
1257
+ // of them — a `font` sets one too — and read as it is then, for the
1258
+ // same reason as the face: set again with the rest, in its place among
1259
+ // them. A `font` sets the size as well, which the first pass settled.
1200
1260
  if (this._lh) {
1201
1261
  for (const c of candidates) {
1202
1262
  for (const d of pick(c)) {
1203
- if (d.prop === 'line-height') this._apply(style, parentStyle, d, ctx);
1263
+ if (d.prop === 'line-height' || d.prop === 'font') {
1264
+ this._apply(style, parentStyle, d, ctx);
1265
+ }
1204
1266
  }
1205
1267
  }
1268
+ style.fontSize = face.fontSize;
1269
+ line = {
1270
+ ...face,
1271
+ lineHeight: style.lineHeight,
1272
+ lineHeightIsLength: style.lineHeightIsLength,
1273
+ };
1206
1274
  }
1207
1275
  const settled = style.fontSize;
1208
1276
  for (const c of candidates) {
@@ -1283,22 +1351,32 @@ export class Cascade {
1283
1351
  * that implied `<html>` instead, so an `html { font-size }` still reaches
1284
1352
  * the body. Taken as the initial style, the rule matched nothing and the
1285
1353
  * whole page came out at the theme's size.
1354
+ *
1355
+ * `html` is the implied `<html>`'s own style where the root box stands in
1356
+ * for the body inside it, and null otherwise: what an `html { … }` or a
1357
+ * `:root { … }` rule gives it that is not inherited has no box to be
1358
+ * drawn on, and its background is the canvas's (`paintDocument`).
1286
1359
  */
1287
- rootStyle(hasBody: boolean, hasHtml = true): ComputedStyle {
1360
+ rootStyle(
1361
+ hasBody: boolean,
1362
+ hasHtml = true,
1363
+ ): { style: ComputedStyle; html: ComputedStyle | null } {
1288
1364
  const style = copyStyle(this.initial);
1289
1365
  style.display = 'block';
1290
- if (hasBody && hasHtml) return style;
1366
+ if (hasBody && hasHtml) return { style, html: null };
1291
1367
  const synthetic = new DomElement('body', {}, []);
1292
1368
  let parent = style;
1369
+ let html: ComputedStyle | null = null;
1293
1370
  if (!hasHtml) {
1294
- const html = new DomElement('html', {}, hasBody ? [] : [synthetic]);
1295
- synthetic.parent = html;
1296
- parent = this.styleFor(html, style, false);
1297
- if (hasBody) return asRoot(parent);
1371
+ const element = new DomElement('html', {}, hasBody ? [] : [synthetic]);
1372
+ synthetic.parent = element;
1373
+ parent = this.styleFor(element, style, false);
1374
+ if (hasBody) return { style: asRoot(parent), html: null };
1375
+ html = parent;
1298
1376
  }
1299
1377
  // Only the box the body would have drawn is taken, not its layout role:
1300
1378
  // the root is still the initial containing block.
1301
- return asRoot(this.styleFor(synthetic, parent, false));
1379
+ return { style: asRoot(this.styleFor(synthetic, parent, false)), html };
1302
1380
  }
1303
1381
 
1304
1382
  /** The rules of `index` that match `el`, pushed onto `out` as candidates,
@@ -497,9 +497,9 @@ export interface ComputedStyle {
497
497
  /** `background-clip` and `background-origin` (CSS Backgrounds 3, 3.7 and
498
498
  * 3.8): the box a layer is painted in and the box it is placed in, the
499
499
  * colour painted in the bottom layer's; each a list as the four above. */
500
- backgroundClip: VisualBox;
500
+ backgroundClip: BackgroundClip;
501
501
  backgroundOrigin: VisualBox;
502
- backgroundClips: VisualBox[] | null;
502
+ backgroundClips: BackgroundClip[] | null;
503
503
  backgroundOrigins: VisualBox[] | null;
504
504
  /** `border-image` and its longhands (CSS Backgrounds 3, 6): one object,
505
505
  * replaced whole when any of them changes, and `NO_BORDER_IMAGE` where
@@ -1799,14 +1799,29 @@ export function applyDeclaration(
1799
1799
  }
1800
1800
  case 'background-clip':
1801
1801
  case '-webkit-background-clip': {
1802
- // `text` the first layer's alone, which paints through the text
1802
+ // `text` the first layer's alone, which paints through the text —
1803
+ // and with `border-area` through its border as well, in either order
1804
+ // (CSS Backgrounds 4, 2.1)
1803
1805
  const clips = layerValues(value, (part) => {
1804
1806
  const v = part.toLowerCase();
1805
- return v === 'text' || isVisualBox(v) ? v : null;
1807
+ if (v === 'text' || isBackgroundClip(v)) return v;
1808
+ const [a, b, more] = splitValue(v);
1809
+ return !more &&
1810
+ ((a === 'border-area' && b === 'text') ||
1811
+ (a === 'text' && b === 'border-area'))
1812
+ ? 'border-area text'
1813
+ : null;
1806
1814
  });
1807
1815
  if (!clips) return;
1808
- style.backgroundClipText = clips[0] === 'text';
1809
- const boxes = clips.map((c) => (c === 'text' ? 'border-box' : c));
1816
+ style.backgroundClipText =
1817
+ clips[0] === 'text' || clips[0] === 'border-area text';
1818
+ const boxes = clips.map((c): BackgroundClip =>
1819
+ c === 'text'
1820
+ ? 'border-box'
1821
+ : c === 'border-area text'
1822
+ ? 'border-area'
1823
+ : (c as BackgroundClip),
1824
+ );
1810
1825
  style.backgroundClip = boxes[0];
1811
1826
  style.backgroundClips = boxes.length > 1 ? boxes : null;
1812
1827
  return;
@@ -2877,12 +2892,6 @@ export function settleOverflow(style: ComputedStyle): void {
2877
2892
  else style.overflowX = x === 'visible' ? 'auto' : 'hidden';
2878
2893
  }
2879
2894
 
2880
- /**
2881
- * Whether a box is a scroll container: its `overflow` is neither `visible`
2882
- * nor `clip`. `clip` cuts what overflows as `hidden` does, but makes no
2883
- * formatting context and keeps a flex or grid item's automatic minimum
2884
- * (CSS Overflow 3, 3.1) — so only this asks it apart from `hidden`.
2885
- */
2886
2895
  /** A `<visual-box>` (CSS Box 4): the edge of a box a property names. */
2887
2896
  export type VisualBox = 'border-box' | 'padding-box' | 'content-box';
2888
2897
 
@@ -2890,6 +2899,20 @@ function isVisualBox(v: string): v is VisualBox {
2890
2899
  return v === 'border-box' || v === 'padding-box' || v === 'content-box';
2891
2900
  }
2892
2901
 
2902
+ /** Where a background layer is painted: inside one of the box's edges, or
2903
+ * `border-area`, in what its border paints (CSS Backgrounds 4, 2.1). */
2904
+ export type BackgroundClip = VisualBox | 'border-area';
2905
+
2906
+ function isBackgroundClip(v: string): v is BackgroundClip {
2907
+ return v === 'border-area' || isVisualBox(v);
2908
+ }
2909
+
2910
+ /**
2911
+ * Whether a box is a scroll container: its `overflow` is neither `visible`
2912
+ * nor `clip`. `clip` cuts what overflows as `hidden` does, but makes no
2913
+ * formatting context and keeps a flex or grid item's automatic minimum
2914
+ * (CSS Overflow 3, 3.1) — so only this asks it apart from `hidden`.
2915
+ */
2893
2916
  export function scrolls(style: ComputedStyle): boolean {
2894
2917
  const x = style.overflowX;
2895
2918
  const y = style.overflowY;
@@ -3485,7 +3508,7 @@ interface BackgroundLayer {
3485
3508
  size: ComputedStyle['backgroundSize'];
3486
3509
  attachment: ComputedStyle['backgroundAttachment'];
3487
3510
  origin: VisualBox;
3488
- clip: VisualBox;
3511
+ clip: BackgroundClip;
3489
3512
  position: [Len, Len] | null;
3490
3513
  }
3491
3514
 
@@ -3545,9 +3568,14 @@ function readBackgroundLayer(
3545
3568
  layer.attachment = v;
3546
3569
  i += 1;
3547
3570
  } else if (isVisualBox(v)) {
3548
- // one box is both the origin and the clip, and a second the clip
3549
- if ((boxes += 1) > 2) return null;
3571
+ // one box is both the origin and the clip, and a second the clip —
3572
+ // which `border-area` is instead of, and the one box the origin
3573
+ if ((boxes += 1) > (seen & 32 ? 1 : 2)) return null;
3550
3574
  if (boxes === 1) layer.origin = v;
3575
+ if (!(seen & 32)) layer.clip = v;
3576
+ i += 1;
3577
+ } else if (v === 'border-area') {
3578
+ if (boxes > 1 || !once(32)) return null;
3551
3579
  layer.clip = v;
3552
3580
  i += 1;
3553
3581
  } else if (isPositionPart(v, ctx)) {
@@ -44,8 +44,11 @@ export function uaStylesheet(look: RootLook): Stylesheet {
44
44
  }
45
45
 
46
46
  /**
47
- * Margins are in `em` throughout, so a document that sets `font-size` on
48
- * `body` scales its whitespace with its text the way a browser's does.
47
+ * Margins above and below are in `em`, so a document that sets `font-size`
48
+ * on `body` scales its whitespace with its text the way a browser's does.
49
+ * The indents of lists, definitions and figures are the HTML standard's
50
+ * 40px (15.3.3, 15.3.8), whatever the size: a list in a 10px sidebar is
51
+ * indented as far as one in the body.
49
52
  */
50
53
  function uaText(look: RootLook): string {
51
54
  const mono = look.monoFamily;
@@ -81,7 +84,7 @@ h4 { font-size: 1em; font-weight: bold; margin: 1.33em 0; }
81
84
  h5 { font-size: 0.83em; font-weight: bold; margin: 1.67em 0; }
82
85
  h6 { font-size: 0.67em; font-weight: bold; margin: 2.33em 0; }
83
86
 
84
- ul, ol { margin: 1em 0; padding-left: 2.5em; }
87
+ ul, ol { margin: 1em 0; padding-left: 40px; }
85
88
  ul { list-style-type: disc; }
86
89
  ol { list-style-type: decimal; }
87
90
  ol, ul, menu, dir { counter-reset: list-item; }
@@ -92,7 +95,7 @@ ul ul ul, ol ol ul, ul ol ul, ol ul ul { list-style-type: square; }
92
95
  ul ul, ul ol, ol ul, ol ol { margin: 0; }
93
96
 
94
97
  dl { margin: 1em 0; }
95
- dd { margin-left: 2.5em; }
98
+ dd { margin-left: 40px; }
96
99
  dt { font-weight: bold; }
97
100
 
98
101
  blockquote {
@@ -150,7 +153,7 @@ img { display: inline-block; }
150
153
  iframe { border: 2px inset; }
151
154
  /* a poster is drawn within the video's box at its own ratio (HTML 15.4.1) */
152
155
  video { object-fit: contain; }
153
- figure { margin: 1em 2.5em; }
156
+ figure { margin: 1em 40px; }
154
157
 
155
158
  table { display: table; border-collapse: separate; border-spacing: 2px; box-sizing: border-box; }
156
159
  caption { display: table-caption; text-align: center; }
@@ -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,
@@ -180,7 +182,9 @@ export function layoutDocument(
180
182
  // there — or `<p>hi</p>` would stand 8px lower than the same paragraph in
181
183
  // `<body>`.
182
184
  resolveEdges(root, viewportWidth);
185
+ const body = fragmentBodyHeight(tree, viewportHeight);
183
186
  const leading = root.marginTop;
187
+ const trailing = root.marginBottom;
184
188
  root.padRight += root.marginRight;
185
189
  root.padBottom += root.marginBottom;
186
190
  root.padLeft += root.marginLeft;
@@ -188,6 +192,7 @@ export function layoutDocument(
188
192
  root.marginRight = 0;
189
193
  root.marginBottom = 0;
190
194
  root.marginLeft = 0;
195
+ if (body) handPercentBase(root, body.inner);
191
196
 
192
197
  const contentWidth = Math.max(0, viewportWidth - root.horizontalExtra);
193
198
  const floats = new FloatContext(root.contentX, root.contentX + contentWidth);
@@ -195,7 +200,7 @@ export function layoutDocument(
195
200
  // height on the root element resolves against (CSS 2.1 10.1, 10.5); a
196
201
  // fragment has no root element, and the box standing in for its body has
197
202
  // the body's `auto` height to give
198
- let readsViewportHeight = false;
203
+ let readsViewportHeight = body?.viewport === true;
199
204
  for (const child of root.children) {
200
205
  if (child.el?.name !== 'html') continue;
201
206
  child.percentHeightBase = viewportHeight;
@@ -218,7 +223,9 @@ export function layoutDocument(
218
223
  );
219
224
  const floatBottom =
220
225
  floats.bottom === -Infinity ? 0 : floats.bottom - root.contentY;
221
- root.height = Math.max(flow.height, floatBottom) + root.verticalExtra;
226
+ root.height = body
227
+ ? leading + body.outer + trailing
228
+ : Math.max(flow.height, floatBottom) + root.verticalExtra;
222
229
 
223
230
  // Positioned boxes last, and in the order they were found, so a later one
224
231
  // can be positioned against an earlier one's resolved rectangle.
@@ -251,6 +258,42 @@ export function layoutDocument(
251
258
  };
252
259
  }
253
260
 
261
+ /**
262
+ * How tall the body a fragment's root box stands in for says it is, where
263
+ * that is definite (CSS 2.1 10.5): a length, `100vh` among them, or a
264
+ * percentage of the `<html>` implied around it where that has a height of
265
+ * its own — a length, or a percentage of the viewport, as `html, body {
266
+ * height: 100% }` fills a window. Its border box, and its content box,
267
+ * which what it holds takes its percentages of; `viewport` where the
268
+ * answer read the viewport's height. Null where the document has an
269
+ * `<html>` or a `<body>`, and where the body's height is `auto`, and the
270
+ * box is as tall as what is in it. Measured before its margins are folded
271
+ * into its padding.
272
+ */
273
+ function fragmentBodyHeight(
274
+ tree: BoxTree,
275
+ viewportHeight: number,
276
+ ): { outer: number; inner: number; viewport: boolean } | null {
277
+ const html = tree.impliedHtml;
278
+ if (!html) return null;
279
+ const root = tree.root;
280
+ const style = root.style;
281
+ const base = resolveOrNull(html.height, viewportHeight);
282
+ const set = resolveOrNull(style.height, base ?? NaN);
283
+ if (set === null) return null;
284
+ root.percentHeightBase = base ?? NaN;
285
+ const extra = root.verticalExtra;
286
+ const outer = clampHeight(
287
+ root,
288
+ style.boxSizing === 'border-box' ? Math.max(set, extra) : set + extra,
289
+ );
290
+ return {
291
+ outer,
292
+ inner: Math.max(0, outer - extra),
293
+ viewport: isPct(style.height) && isPct(html.height),
294
+ };
295
+ }
296
+
254
297
  /** No margin at all, shared: most joins leave a strut as it was. */
255
298
  const NO_MARGIN: MarginStrut = Object.freeze({ pos: 0, neg: 0 });
256
299
 
@@ -725,6 +768,8 @@ function layoutChildren(
725
768
  // this box's first formatted line is its first child's in flow (CSS 2.1
726
769
  // 5.12.1), which its `::first-line` is handed to
727
770
  let firstLine = ctx.firstLine ? firstLineOf(box) : null;
771
+ // and the room a list item's marker takes on it (`MARKER_ROOM`)
772
+ let marked = MARKER_ROOM.get(box) ?? null;
728
773
  const clamp = ctx.clamp ?? null;
729
774
 
730
775
  for (const child of box.children) {
@@ -743,6 +788,10 @@ function layoutChildren(
743
788
  if (!FIRST_LINE.has(child)) HANDED.set(child, firstLine);
744
789
  firstLine = null;
745
790
  }
791
+ if (marked && !child.outOfFlow && !child.isFloat) {
792
+ MARKER_ROOM.set(child, marked);
793
+ marked = null;
794
+ }
746
795
  if (child.outOfFlow) {
747
796
  placeStatic(
748
797
  child,
@@ -870,12 +919,15 @@ function layoutChildren(
870
919
  // escapes, and the caller merges it into the box's own bottom margin so
871
920
  // the next sibling still sees it. Dropping it here was the bug that made
872
921
  // `<div><p>…</p></div><p>…</p>` set the two paragraphs solid.
922
+ // A percentage of a height that depends on content computes to `auto`
923
+ // (CSS 2.1 10.5), and lets it through as `auto` does: a `height: 100%`
924
+ // wrapper in an `auto` body kept its last child's margin inside it
873
925
  if (
874
926
  !first &&
875
927
  !cleared &&
876
928
  !box.borderBottom &&
877
929
  !box.padBottom &&
878
- box.style.height === AUTO &&
930
+ specifiedHeight(box) === null &&
879
931
  !establishesBFC(box)
880
932
  ) {
881
933
  const height = y - contentTop;
@@ -1063,6 +1115,7 @@ function layoutInlineContent(
1063
1115
  floats,
1064
1116
  originX: contentLeft,
1065
1117
  clipText: ctx.clipText,
1118
+ firstStrut: MARKER_ROOM.get(box),
1066
1119
  floatBoxes: floated
1067
1120
  ? {
1068
1121
  size: (child: Box) => sizeFloat(child, ctx, contentWidth),
@@ -1260,7 +1313,8 @@ function makesNothing(box: Box): boolean {
1260
1313
  }
1261
1314
 
1262
1315
  /** 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. */
1316
+ * block's: in another face, size or line height, spaced apart, or in
1317
+ * capitals. */
1264
1318
  function setsFonts(line: ComputedStyle, block: ComputedStyle): boolean {
1265
1319
  return (
1266
1320
  line.fontFamily !== block.fontFamily ||
@@ -1275,7 +1329,8 @@ function setsFonts(line: ComputedStyle, block: ComputedStyle): boolean {
1275
1329
  line.fontVariantLigatures !== block.fontVariantLigatures ||
1276
1330
  line.fontVariantPosition !== block.fontVariantPosition ||
1277
1331
  line.fontKerning !== block.fontKerning ||
1278
- line.fontFeatureSettings !== block.fontFeatureSettings
1332
+ line.fontFeatureSettings !== block.fontFeatureSettings ||
1333
+ line.textTransform !== block.textTransform
1279
1334
  );
1280
1335
  }
1281
1336
 
@@ -1685,13 +1740,26 @@ function layoutBox(
1685
1740
  : outerFloats;
1686
1741
  // the line a pass before gave a marker, which this one may not
1687
1742
  if (box.marker) box.lines = null;
1743
+ if (box.marker && ctx.fonts) {
1744
+ const room = markerRoom(box, box.marker, ctx.fonts);
1745
+ if (room) MARKER_ROOM.set(box, room);
1746
+ else MARKER_ROOM.delete(box);
1747
+ }
1688
1748
  const flow = layoutChildren(box, ctx, floats, box.contentY, contentWidth);
1689
1749
  // A list item with a marker and no line holds one, the marker's, a line
1690
1750
  // of its own face tall: an empty `<li>` is a line tall in a browser, and
1691
1751
  // an inline-block around one sits on its marker's baseline.
1692
1752
  let height = flow.height;
1693
1753
  if (box.marker && ctx.fonts && !firstLineIn(box)) {
1694
- const strut = strutOf(ctx.fonts, box.style);
1754
+ const own = strutOf(ctx.fonts, box.style);
1755
+ // as tall as the marker on it, as a first line with text is
1756
+ const room = MARKER_ROOM.get(box);
1757
+ const strut = room
1758
+ ? {
1759
+ ascent: Math.max(own.ascent, room.ascent),
1760
+ descent: Math.max(own.descent, room.descent),
1761
+ }
1762
+ : own;
1695
1763
  const line = strut.ascent + strut.descent;
1696
1764
  box.lines = [
1697
1765
  {
@@ -1735,6 +1803,33 @@ function layoutBox(
1735
1803
  if (box.marker) layoutMarker(box, box.marker, ctx);
1736
1804
  }
1737
1805
 
1806
+ /**
1807
+ * The room a list item's marker takes about its first line's baseline,
1808
+ * handed to the box that line is in: an image's height above it, its
1809
+ * bottom on the baseline as an inline image's is, and a marker in a face
1810
+ * of its own — a `::marker` rule's — its face at its line height. CSS 2.1
1811
+ * (12.5.1) leaves where an outside marker goes to the user agent; Blink
1812
+ * and Gecko both set it on the item's first line and make the line as tall
1813
+ * as it, and design 032's bullets, images taller than its 10px text, set
1814
+ * every item's first line lower than ours did.
1815
+ */
1816
+ const MARKER_ROOM = new WeakMap<Box, InlineDecoration>();
1817
+
1818
+ function markerRoom(
1819
+ box: Box,
1820
+ marker: Marker,
1821
+ fonts: FontsLike,
1822
+ ): InlineDecoration | null {
1823
+ if (marker.image) {
1824
+ return marker.image.height > 0
1825
+ ? { ascent: marker.image.height, descent: 0 }
1826
+ : null;
1827
+ }
1828
+ return marker.style && marker.style !== box.style
1829
+ ? strutOf(fonts, marker.style)
1830
+ : null;
1831
+ }
1832
+
1738
1833
  /** The first line box anywhere under a box, in layout order. */
1739
1834
  function firstLineIn(box: Box): LineBox | null {
1740
1835
  if (box.lines?.length) return box.lines[0];
@@ -4015,12 +4110,19 @@ export function applyRelativeOffsets(box: Box): void {
4015
4110
  if (transformed(style) && box.kind !== 'inline') {
4016
4111
  // a transform moves the box it is on, and an inline box is none
4017
4112
  const [dx, dy] = translationOf(box);
4018
- if (dx || dy) translate(box, dx, dy);
4113
+ if (dx || dy) {
4114
+ translate(box, dx, dy);
4115
+ movedOffLine(box);
4116
+ }
4019
4117
  }
4020
4118
  if (style.position !== 'relative' && style.position !== 'sticky') return;
4021
4119
  const [dx, dy] = relativeOffset(box);
4120
+ if (!(dx || dy)) return;
4022
4121
  translate(box, dx, dy);
4023
- if (box.kind !== 'inline' || !(dx || dy)) return;
4122
+ if (box.kind !== 'inline') {
4123
+ movedOffLine(box);
4124
+ return;
4125
+ }
4024
4126
  offsetInline(box, dx, dy);
4025
4127
  // the blocks it was broken around move with it (9.2.1.1)
4026
4128
  const blocks = box.cut ? CUT_BLOCKS.get(box) : undefined;
@@ -4089,10 +4191,34 @@ function offsetInline(box: Box, dx: number, dy: number): void {
4089
4191
  y: (was?.y ?? 0) + dy,
4090
4192
  });
4091
4193
  SHIFTED_LINES.add(line);
4194
+ markMovedOff(lines, line);
4092
4195
  }
4093
4196
  }
4094
4197
  }
4095
4198
 
4199
+ /** Mark the line an inline-block or an image a box moved off was placed
4200
+ * on, where it was on one: the line stays, and the box is drawn where it
4201
+ * went (`MOVED_OFF_LINES`). */
4202
+ function movedOffLine(box: Box): void {
4203
+ let block = box.parent;
4204
+ while (block && block.kind === 'inline') block = block.parent;
4205
+ const lines = block?.lines;
4206
+ if (!lines) return;
4207
+ for (const line of lines) {
4208
+ for (const placed of line.atomics) {
4209
+ if (placed.box !== box) continue;
4210
+ markMovedOff(lines, line);
4211
+ return;
4212
+ }
4213
+ }
4214
+ }
4215
+
4216
+ function markMovedOff(lines: LineBox[], line: LineBox): void {
4217
+ const moved = MOVED_OFF_LINES.get(lines);
4218
+ if (moved) moved.add(line);
4219
+ else MOVED_OFF_LINES.set(lines, new Set([line]));
4220
+ }
4221
+
4096
4222
  /** Whether `inner` is `outer` or inside it. */
4097
4223
  function holds(outer: Box, inner: Box): boolean {
4098
4224
  for (let at: Box | null = inner; at; at = at.parent) {