postext 1.49.1 → 1.51.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 (87) hide show
  1. package/dist/__tests__/composed/fenceCondition.test.d.ts +2 -0
  2. package/dist/__tests__/composed/fenceCondition.test.d.ts.map +1 -0
  3. package/dist/__tests__/composed/fenceCondition.test.js +171 -0
  4. package/dist/__tests__/composed/fenceCondition.test.js.map +1 -0
  5. package/dist/__tests__/composed/fixtures/moduleNesting.d.ts +25 -0
  6. package/dist/__tests__/composed/fixtures/moduleNesting.d.ts.map +1 -0
  7. package/dist/__tests__/composed/fixtures/moduleNesting.js +36 -0
  8. package/dist/__tests__/composed/fixtures/moduleNesting.js.map +1 -0
  9. package/dist/__tests__/composed/fixtures/moduleRows.d.ts +40 -0
  10. package/dist/__tests__/composed/fixtures/moduleRows.d.ts.map +1 -0
  11. package/dist/__tests__/composed/fixtures/moduleRows.js +65 -0
  12. package/dist/__tests__/composed/fixtures/moduleRows.js.map +1 -0
  13. package/dist/__tests__/composed/frameLooks.test.d.ts +2 -0
  14. package/dist/__tests__/composed/frameLooks.test.d.ts.map +1 -0
  15. package/dist/__tests__/composed/frameLooks.test.js +151 -0
  16. package/dist/__tests__/composed/frameLooks.test.js.map +1 -0
  17. package/dist/__tests__/composed/moduleNesting.test.d.ts +2 -0
  18. package/dist/__tests__/composed/moduleNesting.test.d.ts.map +1 -0
  19. package/dist/__tests__/composed/moduleNesting.test.js +177 -0
  20. package/dist/__tests__/composed/moduleNesting.test.js.map +1 -0
  21. package/dist/__tests__/composed/moduleRows.test.d.ts +2 -0
  22. package/dist/__tests__/composed/moduleRows.test.d.ts.map +1 -0
  23. package/dist/__tests__/composed/moduleRows.test.js +239 -0
  24. package/dist/__tests__/composed/moduleRows.test.js.map +1 -0
  25. package/dist/__tests__/composed/modules.test.js +3 -2
  26. package/dist/__tests__/composed/modules.test.js.map +1 -1
  27. package/dist/__tests__/composed/pillWord.test.js +35 -0
  28. package/dist/__tests__/composed/pillWord.test.js.map +1 -1
  29. package/dist/__tests__/exports.test.js +9 -0
  30. package/dist/__tests__/exports.test.js.map +1 -1
  31. package/dist/composed/groups.d.ts.map +1 -1
  32. package/dist/composed/groups.js +9 -1
  33. package/dist/composed/groups.js.map +1 -1
  34. package/dist/composed/modules.d.ts +9 -0
  35. package/dist/composed/modules.d.ts.map +1 -1
  36. package/dist/composed/modules.js +43 -2
  37. package/dist/composed/modules.js.map +1 -1
  38. package/dist/composed/reflowLog.js +1 -1
  39. package/dist/composed/reflowLog.js.map +1 -1
  40. package/dist/composed/slotChecks.d.ts +10 -0
  41. package/dist/composed/slotChecks.d.ts.map +1 -1
  42. package/dist/composed/slotChecks.js +39 -0
  43. package/dist/composed/slotChecks.js.map +1 -1
  44. package/dist/composed/templates.d.ts +17 -1
  45. package/dist/composed/templates.d.ts.map +1 -1
  46. package/dist/composed/templates.js +82 -6
  47. package/dist/composed/templates.js.map +1 -1
  48. package/dist/composed/types.d.ts +76 -4
  49. package/dist/composed/types.d.ts.map +1 -1
  50. package/dist/composed/validate.d.ts.map +1 -1
  51. package/dist/composed/validate.js +79 -6
  52. package/dist/composed/validate.js.map +1 -1
  53. package/dist/configWarnings.d.ts +3 -1
  54. package/dist/configWarnings.d.ts.map +1 -1
  55. package/dist/configWarnings.js +32 -3
  56. package/dist/configWarnings.js.map +1 -1
  57. package/dist/defaults/headerFooter.d.ts +10 -0
  58. package/dist/defaults/headerFooter.d.ts.map +1 -1
  59. package/dist/defaults/headerFooter.js +21 -1
  60. package/dist/defaults/headerFooter.js.map +1 -1
  61. package/dist/design/fenceCondition.d.ts +45 -0
  62. package/dist/design/fenceCondition.d.ts.map +1 -0
  63. package/dist/design/fenceCondition.js +66 -0
  64. package/dist/design/fenceCondition.js.map +1 -0
  65. package/dist/design/layout.d.ts.map +1 -1
  66. package/dist/design/layout.js +33 -1
  67. package/dist/design/layout.js.map +1 -1
  68. package/dist/index.d.ts +6 -3
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +5 -2
  71. package/dist/index.js.map +1 -1
  72. package/dist/pipeline/composedLayout.d.ts +5 -1
  73. package/dist/pipeline/composedLayout.d.ts.map +1 -1
  74. package/dist/pipeline/composedLayout.js +355 -83
  75. package/dist/pipeline/composedLayout.js.map +1 -1
  76. package/dist/pipeline/config.d.ts.map +1 -1
  77. package/dist/pipeline/config.js +8 -1
  78. package/dist/pipeline/config.js.map +1 -1
  79. package/dist/pipeline/contentWarnings.d.ts.map +1 -1
  80. package/dist/pipeline/contentWarnings.js +8 -1
  81. package/dist/pipeline/contentWarnings.js.map +1 -1
  82. package/dist/types.d.ts +68 -2
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/vdt.d.ts +25 -2
  85. package/dist/vdt.d.ts.map +1 -1
  86. package/dist/vdt.js.map +1 -1
  87. package/package.json +1 -1
@@ -41,12 +41,12 @@ import { chainHead, FRAME_ATTR } from '../composed/wrap';
41
41
  import { framePlaceable, nextFrameOf, groupPlaceable, placeBox, unionRect } from '../composed/geometry';
42
42
  import { composedOrder, groupOfFrames, layoutGroup } from '../composed/groups';
43
43
  import { reflowEvents } from '../composed/reflowLog';
44
- import { anchorTarget, repeatInstance, repeatedFrames, resolveContinueIn } from '../composed/templates';
44
+ import { anchorTarget, lookOver, repeatInstance, repeatedFrames, resolveContinueIn } from '../composed/templates';
45
45
  import { collapsedOnAddedPage, collapseFrames, continuationTemplate, continuesCollapsed, continuesOffPage, trailingFrames } from '../composed/continuation';
46
46
  import { isModuleTemplate, resolvePageTemplate } from '../composed/templates';
47
47
  import { frameElementId } from '../composed/ids';
48
48
  import { expandRepeats, instanceOf, repeatItemsOf } from '../composed/repeat';
49
- import { moduleOfFrame } from '../composed/modules';
49
+ import { moduleOfFrame, nestedChain } from '../composed/modules';
50
50
  import { frameLinePx, frameRuleBlocks } from '../composed/rules';
51
51
  import { overlapChecks } from '../composed/overlap';
52
52
  import { offsetCalloutToAbsolute } from './calloutLayout';
@@ -520,6 +520,7 @@ export function layoutComposedPage(asked) {
520
520
  templateSource: templateAttr ? { sourceStart: templateAttr.sourceStart + input.bodyOffset, sourceEnd: templateAttr.sourceEnd + input.bodyOffset } : undefined,
521
521
  budgetLeft: () => runs < COMPOSE_BUDGET,
522
522
  holdsText: holdsText(frame),
523
+ dividesWord,
523
524
  });
524
525
  warnings.push({
525
526
  kind: 'frameOverflow',
@@ -830,24 +831,51 @@ function overflowProposals(c) {
830
831
  const path = (key) => frame.fieldPaths[key] ?? `${frame.configPath}/${key}`;
831
832
  const fits = (next, tpl) => c.budgetLeft() && !c.overflows(c.compose(next, tpl), name);
832
833
  // More shrink: the largest text scale, in 2.5 % steps, at which it fits.
834
+ // When that scale divides a word wider than the frame's measure, the
835
+ // largest smaller one that fits with every word whole, as the ladder
836
+ // prefers (#1084, #1090); none, and the largest that fits, saying the
837
+ // word will be divided.
833
838
  const head = c.headOf(name);
834
839
  const from = frame.fit.shrinkText ? Math.min(frame.fit.minScale, vdt.scale) : 1;
835
840
  const steps = scaleSteps(PROPOSAL_MIN_SCALE, from);
836
- if (c.holdsText && steps.length > 0 && fits(c.withText(fit, head, steps[steps.length - 1]))) {
837
- let lo = 0;
838
- let hi = steps.length - 1;
839
- while (lo < hi && c.budgetLeft()) {
840
- const mid = Math.floor((lo + hi) / 2);
841
- if (fits(c.withText(fit, head, steps[mid])))
842
- hi = mid;
841
+ const shrunk = (i) => c.withText(fit, head, steps[i]);
842
+ if (c.holdsText && steps.length > 0 && fits(shrunk(steps.length - 1))) {
843
+ const last = steps.length - 1;
844
+ /** The largest step from `lo` down at which `ok` holds, given it holds
845
+ * at the last one: by bisection. */
846
+ const largest = (lo, ok) => {
847
+ let hi = last;
848
+ while (lo < hi && c.budgetLeft()) {
849
+ const mid = Math.floor((lo + hi) / 2);
850
+ if (ok(mid))
851
+ hi = mid;
852
+ else
853
+ lo = mid + 1;
854
+ }
855
+ return hi;
856
+ };
857
+ let at = largest(0, (i) => fits(shrunk(i)));
858
+ let word;
859
+ if (c.budgetLeft() && c.dividesWord(c.compose(shrunk(at)), name)) {
860
+ const whole = (i) => {
861
+ if (!c.budgetLeft())
862
+ return false;
863
+ const comp = c.compose(shrunk(i));
864
+ return !c.overflows(comp, name) && !c.dividesWord(comp, name);
865
+ };
866
+ if (at < last && whole(last)) {
867
+ at = largest(at + 1, whole);
868
+ word = 'whole';
869
+ }
843
870
  else
844
- lo = mid + 1;
871
+ word = 'divided';
845
872
  }
846
873
  out.push({
847
874
  kind: 'allowShrink',
848
875
  path: path('fit/minScale'),
849
- value: steps[hi],
876
+ value: steps[at],
850
877
  ...(frame.fit.shrinkText ? {} : { also: [{ path: path('fit/shrinkText'), value: true }] }),
878
+ ...(word ? { word } : {}),
851
879
  });
852
880
  }
853
881
  // A frame that holds content (an empty one is short of its own box, #911:
@@ -991,9 +1019,25 @@ function composePage(pageInput, template, fit, collapsed) {
991
1019
  * numbered (`VDTDesignTextBlock.pageMark`). */
992
1020
  const toGuess = String(input.page.index + 2);
993
1021
  const fromGuess = String(Math.max(1, input.page.index));
1022
+ /** The named look (#1089) the `:::frame` fence of a frame's story names
1023
+ * (`look="<id>"`), when `frameLooks` has it: the story's, so every
1024
+ * frame of a chain and the frames it goes on in on later pages wear it. */
1025
+ const frameLooks = pass.resolved.frameLooks;
1026
+ const namedLookOf = (frame) => {
1027
+ if (!frameLooks)
1028
+ return undefined;
1029
+ const id = input.stories.get(headOf(frame.name))?.plan.attrs.look?.trim();
1030
+ return id ? frameLooks.find((l) => l.id === id) : undefined;
1031
+ };
1032
+ /** The look a frame's story names that `frameLooks` does not have
1033
+ * (#1095): the canvas marks the frame. */
1034
+ const unknownLookOf = (frame) => {
1035
+ const id = input.stories.get(headOf(frame.name))?.plan.attrs.look?.trim();
1036
+ return id && !frameLooks?.some((l) => l.id === id) ? id : undefined;
1037
+ };
994
1038
  /** The style a frame is laid out in: its box as drawn at any text scale. */
995
1039
  const styleOf = (frame) => {
996
- const style = keepChromeAt(input.styleOf(frame), textScale(frame));
1040
+ const style = keepChromeAt(input.styleOf(withNamedLook(frame, namedLookOf(frame))), textScale(frame));
997
1041
  if (!fit.goOn.has(frame.name))
998
1042
  return style;
999
1043
  // A frame cut for another page (#859) ends with its mark: its own
@@ -1125,6 +1169,9 @@ function composePage(pageInput, template, fit, collapsed) {
1125
1169
  const moduleRects = new Map();
1126
1170
  /** The foot a module cut at its group's foot is clipped to (#1062). */
1127
1171
  let clipFoot;
1172
+ /** A module a stretching row lays taller than it is (#1095): its
1173
+ * frames, its own foot (`at`) and the row's. */
1174
+ let stretchTo;
1128
1175
  const pushed = seed
1129
1176
  ? [...seed].map(([name, box]) => ({ name, box, ...(seedShapes?.get(name) ? { contour: seedShapes.get(name) } : {}) }))
1130
1177
  : [];
@@ -1722,6 +1769,7 @@ function composePage(pageInput, template, fit, collapsed) {
1722
1769
  ...(unmirroredAt(frame) !== undefined ? { unmirrorX: unmirroredAt(frame) } : {}),
1723
1770
  layer: frame.layer,
1724
1771
  ...(frame.style ? { styleId: frame.style } : {}),
1772
+ ...(holds && unknownLookOf(frame) !== undefined ? { lookUnknown: unknownLookOf(frame) } : {}),
1725
1773
  scale: s,
1726
1774
  ...(pic !== undefined && holds ? { imageScale: pic } : {}),
1727
1775
  overflowPx,
@@ -1814,6 +1862,27 @@ function composePage(pageInput, template, fit, collapsed) {
1814
1862
  rects.set(name, out);
1815
1863
  return out;
1816
1864
  };
1865
+ /** A module's `'auto'` box that ends at the module's foot, in a row
1866
+ * that stretches the module (#1095): to the row's foot, its declared
1867
+ * box by as much (the template asks for it). A frame as wide as its
1868
+ * content (a pill, a tab) keeps its height. */
1869
+ const stretchAuto = (name, r) => {
1870
+ const s = stretchTo;
1871
+ if (!s || !s.names.has(name))
1872
+ return r;
1873
+ const f = byName.get(name);
1874
+ if (!f || f.height !== 'auto' || f.width === 'auto' || f.pin || heldByFoot(f))
1875
+ return r;
1876
+ const bottom = r.y + r.height;
1877
+ if (Math.abs(bottom - s.at) > 0.5 || s.foot - bottom <= 0.01)
1878
+ return r;
1879
+ const out = { ...r, height: s.foot - r.y };
1880
+ rects.set(name, out);
1881
+ const d = declared.get(name);
1882
+ if (d)
1883
+ declared.set(name, { ...d, height: d.height + (s.foot - bottom) });
1884
+ return out;
1885
+ };
1817
1886
  /** The frames of `names` whose content goes on whole on another page
1818
1887
  * (#1061, #1062): none of it is laid here. */
1819
1888
  const spillWhole = (names, advanced, lastLaid) => {
@@ -1830,7 +1899,7 @@ function composePage(pageInput, template, fit, collapsed) {
1830
1899
  const runStep = (step, filling) => {
1831
1900
  if (step.kind === 'frame') {
1832
1901
  const p = prepare(step.frame);
1833
- const r = clipAtFoot(step.frame.name, placeFrame(p));
1902
+ const r = clipAtFoot(step.frame.name, stretchAuto(step.frame.name, placeFrame(p)));
1834
1903
  if (filling)
1835
1904
  fill(p, r);
1836
1905
  return;
@@ -1851,7 +1920,7 @@ function composePage(pageInput, template, fit, collapsed) {
1851
1920
  let lastLaid;
1852
1921
  for (const f of step.frames) {
1853
1922
  if (!grid.spilled.has(f.name)) {
1854
- fill(prepare(f), clipAtFoot(f.name, grid.rects.get(f.name)));
1923
+ fill(prepare(f), clipAtFoot(f.name, stretchAuto(f.name, grid.rects.get(f.name))));
1855
1924
  lastLaid = frameIdOf(f.name);
1856
1925
  continue;
1857
1926
  }
@@ -1863,31 +1932,136 @@ function composePage(pageInput, template, fit, collapsed) {
1863
1932
  // A frame of a chain inside the group is reached once the frame
1864
1933
  // before it was filled: each is prepared again as it is filled.
1865
1934
  for (const f of step.frames) {
1866
- const r = clipAtFoot(f.name, solved.get(f.name));
1935
+ const r = clipAtFoot(f.name, stretchAuto(f.name, solved.get(f.name)));
1867
1936
  if (filling)
1868
1937
  fill(prepare(f), r);
1869
1938
  }
1870
1939
  };
1871
- /** Lay a modules group (#1062): each module, in order, in a box as wide
1872
- * as the group, from where the module before it ends (`gap` between
1873
- * them), as tall as its frames. A module's frames are placed as its
1874
- * template draws them in that box (`'container'` is the box) and its
1875
- * groups laid in it: first in the room left, to find how tall it is
1876
- * (the frames held by the box's foot or middle, or sized by its
1877
- * height, left out), then in a box that tall. The group runs down its
1878
- * own box, else the area. A module that does not fit is cut at the
1879
- * group's foot (its frames clipped there), or, when the group goes on
1880
- * to another page (`FitState.gridsOn`), it and every module after it
1881
- * go on, whole. The modules' declared boxes follow each other at the
1882
- * heights their frames declare, so the log tells a module moved by
1883
- * the one before it. */
1940
+ /** Lay a modules group (#1062): its modules in rows down the group's
1941
+ * box (its own, else the area). A stack gives each module a row of
1942
+ * its own, as wide as the group (or its template's `width`, #1088), a
1943
+ * `'row'` puts them all in one, start to end (each its `width`, else
1944
+ * an equal share of what is left), a `'grid'` `columns` to a row in
1945
+ * equal columns; a run of modules a group inside it takes (`within`)
1946
+ * is laid as that group says where its first module comes: its rows
1947
+ * among a stack's, one cell of a row or a grid (#1095: to any depth).
1948
+ * A row's room past its modules' widths goes by `grow`, what it is
1949
+ * short by comes back by `shrink` (#1095). Rows run from the start side of the page's lines (the flow frame is
1950
+ * mirrored on a right-to-left page, so its left is the start). A
1951
+ * module is placed as its template draws its frames in its box
1952
+ * (`'container'` is the box) and its groups laid in it: first in the
1953
+ * room left, to find how tall it is (the frames held by the box's foot
1954
+ * or middle, or sized by its height, left out), then in a box that
1955
+ * tall, or (`rowAlign` `'stretch'`, a row of several) as tall as its
1956
+ * row, its `'auto'` boxes that end at its foot reaching the row's
1957
+ * (#1095). A row that does not fit is cut at the group's foot (its frames
1958
+ * clipped there), or, when the group goes on to another page
1959
+ * (`FitState.gridsOn`), it and every row after it go on, whole. The
1960
+ * modules' declared boxes follow each other at the heights their
1961
+ * frames declare, so the log tells a module moved by the one before
1962
+ * it. */
1884
1963
  const placeModules = (group, members) => {
1885
1964
  const byMember = new Map(members.map((f) => [f.name, f]));
1886
1965
  const own = groupPlaceable(group);
1887
1966
  const ownBox = own ? placeBox(own, template, pageInput, refOf, () => 0) : undefined;
1888
1967
  const box = ownBox ?? pageInput.areas.area;
1889
1968
  const foot = box.y + box.height;
1890
- const gap = dimensionToPx(group.gap, dpi, pageInput.emPx);
1969
+ const toPx = (d) => dimensionToPx(d, dpi, pageInput.emPx);
1970
+ const groupNamed = new Map(template.groups.map((g) => [g.name, g]));
1971
+ const mods = [];
1972
+ for (const item of group.items ?? []) {
1973
+ const mod = template.modules?.find((m) => m.name === item.module);
1974
+ const names = item.frames.filter((n) => byMember.has(n));
1975
+ if (mod && names.length > 0)
1976
+ mods.push({ mod, names, chain: nestedChain(template.groups, mod.nested) });
1977
+ }
1978
+ /** A module's width by its template in a group `width` wide
1979
+ * (`'fill'`: none). */
1980
+ const fixedWidth = (l, width) => {
1981
+ const w = l.mod.width;
1982
+ if (w === undefined || w === 'fill')
1983
+ return undefined;
1984
+ return 'percent' in w ? (width * w.percent) / 100 : toPx(w);
1985
+ };
1986
+ /** The cells of `list` in `g`: each module `g` lays itself, and each
1987
+ * run of the modules of a group inside `g`. */
1988
+ const cellsOf = (list, g) => {
1989
+ const out = [];
1990
+ for (const l of list) {
1991
+ const name = l.chain.find((n) => groupNamed.get(n)?.within === g.name);
1992
+ const child = name !== undefined ? groupNamed.get(name) : undefined;
1993
+ const last = out[out.length - 1];
1994
+ if (child && last?.kind === 'group' && last.group === child)
1995
+ last.list.push(l);
1996
+ else
1997
+ out.push(child ? { kind: 'group', group: child, list: [l] } : { kind: 'module', l });
1998
+ }
1999
+ return out;
2000
+ };
2001
+ const namesOf = (c) => (c.kind === 'module' ? c.l.names : c.list.flatMap((l) => l.names));
2002
+ /** `cells` side by side across `width` from `x`, `gap` between them:
2003
+ * each as wide as its template says, then the room left shared by
2004
+ * `grow` (a `'fill'` module, and a group's cell, 1 at least), or,
2005
+ * when the widths run past the end, given back by `shrink` in
2006
+ * proportion to each width (#1095). */
2007
+ const across = (cells, gap, x, width) => {
2008
+ const fixed = cells.map((c) => (c.kind === 'module' ? fixedWidth(c.l, width) : undefined));
2009
+ const taken = fixed.reduce((a, w) => a + (w ?? 0), 0) + gap * Math.max(0, cells.length - 1);
2010
+ const spare = width - taken;
2011
+ let widths;
2012
+ if (spare >= 0) {
2013
+ const grows = cells.map((c, k) => (c.kind !== 'module' ? 1 : fixed[k] === undefined ? Math.max(1, c.l.mod.grow ?? 1) : c.l.mod.grow ?? 0));
2014
+ const total = grows.reduce((a, g) => a + g, 0);
2015
+ widths = fixed.map((w, k) => Math.max(0, total > 0 ? (w ?? 0) + (spare * grows[k]) / total : w ?? 0));
2016
+ }
2017
+ else {
2018
+ const weights = cells.map((c, k) => (c.kind === 'module' && fixed[k] !== undefined ? (c.l.mod.shrink ?? 0) * fixed[k] : 0));
2019
+ const total = weights.reduce((a, g) => a + g, 0);
2020
+ widths = fixed.map((w, k) => Math.max(0, total > 0 ? (w ?? 0) + (spare * weights[k]) / total : w ?? 0));
2021
+ }
2022
+ const xs = [];
2023
+ let at = x;
2024
+ for (const w of widths) {
2025
+ xs.push(at);
2026
+ at += w + gap;
2027
+ }
2028
+ return { xs, widths };
2029
+ };
2030
+ /** The rows `list` makes in a group of `g`'s kind across `width`
2031
+ * from `x`, the first `first` after the row before it. A group
2032
+ * inside a stack adds its rows to the stack's; inside a row or a
2033
+ * grid, a run of its modules is one cell. */
2034
+ const rowsOf = (list, g, x, width, first) => {
2035
+ const gap = toPx(g.gap);
2036
+ const align = g.rowAlign ?? 'stretch';
2037
+ const cells = cellsOf(list, g);
2038
+ if (g.kind === 'row')
2039
+ return [{ cells, ...across(cells, gap, x, width), align, gapBefore: first }];
2040
+ if (g.kind === 'grid') {
2041
+ const columns = Math.max(1, g.columns ?? 1);
2042
+ const colGap = toPx(g.columnGap ?? g.gap);
2043
+ const rowGap = toPx(g.rowGap ?? g.gap);
2044
+ const colW = Math.max(1, (width - colGap * (columns - 1)) / columns);
2045
+ const out = [];
2046
+ for (let k = 0; k < cells.length; k += columns) {
2047
+ const chunk = cells.slice(k, k + columns);
2048
+ out.push({ cells: chunk, xs: chunk.map((_, c) => x + c * (colW + colGap)), widths: chunk.map(() => colW), align, gapBefore: k === 0 ? first : rowGap });
2049
+ }
2050
+ return out;
2051
+ }
2052
+ // A stack: a row of its own per module, at its width from the
2053
+ // start; a group's rows in turn.
2054
+ const out = [];
2055
+ cells.forEach((c, k) => {
2056
+ const before = k === 0 ? first : gap;
2057
+ if (c.kind === 'module')
2058
+ out.push({ cells: [c], xs: [x], widths: [fixedWidth(c.l, width) ?? width], align: 'start', gapBefore: before });
2059
+ else
2060
+ out.push(...rowsOf(c.list, c.group, x, width, before));
2061
+ });
2062
+ return out;
2063
+ };
2064
+ const rows = rowsOf(mods, group, box.x, box.width, 0);
1891
2065
  let cursor = box.y;
1892
2066
  let declCursor = box.y;
1893
2067
  let out = false;
@@ -1895,40 +2069,31 @@ function composePage(pageInput, template, fit, collapsed) {
1895
2069
  let lastLaid;
1896
2070
  const boxes = [];
1897
2071
  const declBoxes = [];
2072
+ const nestedBoxes = new Map();
1898
2073
  const restore = (to, from) => {
1899
2074
  to.clear();
1900
2075
  for (const [k, v] of from)
1901
2076
  to.set(k, v);
1902
2077
  };
1903
- for (const item of group.items ?? []) {
1904
- const mod = template.modules?.find((m) => m.name === item.module);
1905
- const names = item.frames.filter((n) => byMember.has(n));
1906
- if (!mod || names.length === 0)
1907
- continue;
1908
- if (out) {
1909
- spillWhole(names, laidAny, lastLaid);
1910
- continue;
2078
+ const lay = (l, x, y, width, height, filling) => {
2079
+ const steps = composedOrder({ frames: l.names.map((n) => byMember.get(n)), groups: template.groups.filter((g) => l.mod.groups.includes(g.name)) });
2080
+ input = { ...pageInput, areas: { ...pageInput.areas, area: { x, y, width, height: Math.max(0, height) } } };
2081
+ try {
2082
+ for (const step of steps)
2083
+ runStep(step, filling);
1911
2084
  }
1912
- const sub = { frames: names.map((n) => byMember.get(n)), groups: template.groups.filter((g) => mod.groups.includes(g.name)) };
1913
- const steps = composedOrder(sub);
1914
- const lay = (height, filling) => {
1915
- input = { ...pageInput, areas: { ...pageInput.areas, area: { x: box.x, y: cursor, width: box.width, height: Math.max(0, height) } } };
1916
- try {
1917
- for (const step of steps)
1918
- runStep(step, filling);
1919
- }
1920
- finally {
1921
- input = pageInput;
1922
- }
1923
- };
1924
- // 1. How tall the module is: its frames placed in the room left,
1925
- // then everything they set put back.
2085
+ finally {
2086
+ input = pageInput;
2087
+ }
2088
+ };
2089
+ /** How tall a module is at `(x, y)`, `width` wide: its frames placed
2090
+ * in the room left, then everything they set put back. */
2091
+ const measure = (l, x, y, width) => {
1926
2092
  const kept = { rects: new Map(rects), declared: new Map(declared), groupRects: new Map(groupRects), groupDeclared: new Map(groupDeclared), pinned: pinned.length, boxShort: new Map(boxShort), fitted: new Map(fitted) };
1927
- lay(foot - cursor, false);
1928
- const placed = names.filter((n) => rects.has(n));
2093
+ lay(l, x, y, width, foot - y, false);
2094
+ const placed = l.names.filter((n) => rects.has(n));
1929
2095
  const measured = placed.filter((n) => !heldByFoot(byMember.get(n)));
1930
2096
  const reach = unionRect((measured.length > 0 ? measured : placed).map((n) => rects.get(n)));
1931
- const height = reach ? Math.max(0, reach.y + reach.height - cursor) : 0;
1932
2097
  restore(rects, kept.rects);
1933
2098
  restore(declared, kept.declared);
1934
2099
  restore(groupRects, kept.groupRects);
@@ -1936,41 +2101,129 @@ function composePage(pageInput, template, fit, collapsed) {
1936
2101
  restore(boxShort, kept.boxShort);
1937
2102
  restore(fitted, kept.fitted);
1938
2103
  pinned.length = kept.pinned;
1939
- const fits = cursor + height <= foot + 0.5;
1940
- if (!fits && fit.gridsOn.has(group.name)) {
1941
- // This module and the ones after it go on, whole.
1942
- out = true;
2104
+ return reach ? Math.max(0, reach.y + reach.height - y) : 0;
2105
+ };
2106
+ /** How tall a cell is at `(x, y)`, `width` wide: a module's height,
2107
+ * or a group's rows one after another (#1095). */
2108
+ const measureCell = (c, x, y, width) => {
2109
+ if (c.kind === 'module')
2110
+ return measure(c.l, x, y, width);
2111
+ let at = y;
2112
+ rowsOf(c.list, c.group, x, width, 0).forEach((row, r) => {
2113
+ if (r > 0)
2114
+ at += row.gapBefore;
2115
+ at += Math.max(0, ...row.cells.map((d, k) => measureCell(d, row.xs[k], at, row.widths[k])));
2116
+ });
2117
+ return at - y;
2118
+ };
2119
+ /** Lay `row` at `rowTop` (its declared top `declTop`), its cells
2120
+ * `heights` tall: each module in a box that tall (cut at the foot
2121
+ * when the row does not fit), a shorter one placed by `rowAlign`; a
2122
+ * group's cell its rows in turn. The row's foot, laid and declared. */
2123
+ const layRow = (row, rowTop, declTop, heights, fits) => {
2124
+ const rowH = Math.max(0, ...heights);
2125
+ const several = row.cells.length > 1;
2126
+ let rowBottom = rowTop;
2127
+ let rowDeclBottom = declTop;
2128
+ row.cells.forEach((c, k) => {
2129
+ const x = row.xs[k];
2130
+ const width = row.widths[k];
2131
+ const spare = rowH - heights[k];
2132
+ const stretched = fits && several && row.align === 'stretch' && c.kind === 'module';
2133
+ const dy = fits && several && spare > 0.01 ? (row.align === 'end' ? spare : row.align === 'center' ? spare / 2 : 0) : 0;
2134
+ const y = rowTop + dy;
2135
+ if (c.kind === 'group') {
2136
+ // A run of a group inside this one (#1095): its rows, from the
2137
+ // cell's top.
2138
+ const laid = layRows(rowsOf(c.list, c.group, x, width, 0), y, declTop + dy, fits);
2139
+ rowBottom = Math.max(rowBottom, laid.bottom);
2140
+ rowDeclBottom = Math.max(rowDeclBottom, laid.declBottom);
2141
+ return;
2142
+ }
2143
+ const l = c.l;
2144
+ clipFoot = fits ? undefined : foot;
2145
+ // Its 'auto' boxes that end at its foot reach the row's (#1095).
2146
+ stretchTo = stretched && spare > 0.01 ? { at: y + heights[k], foot: y + rowH, names: new Set(l.names) } : undefined;
2147
+ try {
2148
+ lay(l, x, y, width, fits ? (stretched ? rowH : heights[k]) : foot - rowTop, true);
2149
+ }
2150
+ finally {
2151
+ clipFoot = undefined;
2152
+ stretchTo = undefined;
2153
+ }
2154
+ const u = unionRect(l.names.flatMap((n) => (rects.has(n) ? [rects.get(n)] : [])));
2155
+ const bottom = Math.max(stretched ? y + rowH : y, u ? u.y + u.height : y);
2156
+ const moduleBox = { x, y, width, height: bottom - y };
2157
+ moduleRects.set(l.mod.name, moduleBox);
2158
+ boxes.push(moduleBox);
2159
+ // Declared: after the rows before it as they declare themselves.
2160
+ const shift = declTop - rowTop;
2161
+ for (const n of l.names) {
2162
+ const d = declared.get(n);
2163
+ if (d && Math.abs(shift) > 0.001)
2164
+ declared.set(n, { ...d, y: d.y + shift });
2165
+ }
2166
+ const declY = declTop + dy;
2167
+ const du = unionRect(l.names.flatMap((n) => (declared.has(n) ? [declared.get(n)] : [])));
2168
+ const declBottom = du ? Math.max(declY, du.y + du.height) : declY;
2169
+ const declBox = { x, y: declY, width, height: declBottom - declY };
2170
+ declBoxes.push(declBox);
2171
+ for (const name of l.chain) {
2172
+ const n = nestedBoxes.get(name) ?? { laid: [], declared: [] };
2173
+ n.laid.push(moduleBox);
2174
+ n.declared.push(declBox);
2175
+ nestedBoxes.set(name, n);
2176
+ }
2177
+ rowBottom = Math.max(rowBottom, bottom);
2178
+ rowDeclBottom = Math.max(rowDeclBottom, declBottom);
2179
+ });
2180
+ return { bottom: rowBottom, declBottom: rowDeclBottom };
2181
+ };
2182
+ /** The rows of a group's cell, one after another from `top`. */
2183
+ const layRows = (list, top, declTop, fits) => {
2184
+ let at = top;
2185
+ let declAt = declTop;
2186
+ list.forEach((row, r) => {
2187
+ if (r > 0) {
2188
+ at += row.gapBefore;
2189
+ declAt += row.gapBefore;
2190
+ }
2191
+ const heights = row.cells.map((c, k) => measureCell(c, row.xs[k], at, row.widths[k]));
2192
+ const laid = layRow(row, at, declAt, heights, fits);
2193
+ at = laid.bottom;
2194
+ declAt = laid.declBottom;
2195
+ });
2196
+ return { bottom: at, declBottom: declAt };
2197
+ };
2198
+ rows.forEach((row, r) => {
2199
+ const names = row.cells.flatMap(namesOf);
2200
+ if (out) {
1943
2201
  spillWhole(names, laidAny, lastLaid);
1944
- continue;
1945
- }
1946
- // 2. The module laid in a box that tall (cut at the foot when it
1947
- // does not fit).
1948
- clipFoot = fits ? undefined : foot;
1949
- try {
1950
- lay(fits ? height : foot - cursor, true);
2202
+ return;
1951
2203
  }
1952
- finally {
1953
- clipFoot = undefined;
2204
+ if (r > 0) {
2205
+ cursor += row.gapBefore;
2206
+ declCursor += row.gapBefore;
1954
2207
  }
1955
- const u = unionRect(names.flatMap((n) => (rects.has(n) ? [rects.get(n)] : [])));
1956
- const bottom = u ? Math.max(cursor, u.y + u.height) : cursor;
1957
- const moduleBox = { x: box.x, y: cursor, width: box.width, height: bottom - cursor };
1958
- moduleRects.set(mod.name, moduleBox);
1959
- boxes.push(moduleBox);
1960
- // Declared: after the modules before it as they declare themselves.
1961
- const shift = declCursor - cursor;
1962
- for (const n of names) {
1963
- const d = declared.get(n);
1964
- if (d && Math.abs(shift) > 0.001)
1965
- declared.set(n, { ...d, y: d.y + shift });
2208
+ const heights = row.cells.map((c, k) => measureCell(c, row.xs[k], cursor, row.widths[k]));
2209
+ const rowH = Math.max(0, ...heights);
2210
+ const fits = cursor + rowH <= foot + 0.5;
2211
+ if (!fits && fit.gridsOn.has(group.name)) {
2212
+ // This row and the ones after it go on, whole.
2213
+ out = true;
2214
+ spillWhole(names, laidAny, lastLaid);
2215
+ return;
1966
2216
  }
1967
- const du = unionRect(names.flatMap((n) => (declared.has(n) ? [declared.get(n)] : [])));
1968
- const declBottom = du ? Math.max(declCursor, du.y + du.height) : declCursor;
1969
- declBoxes.push({ x: box.x, y: declCursor, width: box.width, height: declBottom - declCursor });
2217
+ const laid = layRow(row, cursor, declCursor, heights, fits);
1970
2218
  laidAny = true;
1971
2219
  lastLaid = frameIdOf(names[names.length - 1]);
1972
- cursor = bottom + gap;
1973
- declCursor = declBottom + gap;
2220
+ cursor = laid.bottom;
2221
+ declCursor = laid.declBottom;
2222
+ });
2223
+ // A group inside another (#1088, #1095) is the box of its modules.
2224
+ for (const [name, b] of nestedBoxes) {
2225
+ groupRects.set(name, unionRect(b.laid));
2226
+ groupDeclared.set(name, unionRect(b.declared));
1974
2227
  }
1975
2228
  groupRects.set(group.name, ownBox ?? unionRect(boxes) ?? { ...box, height: 0 });
1976
2229
  groupDeclared.set(group.name, ownBox ?? unionRect(declBoxes) ?? { ...box, height: 0 });
@@ -2123,6 +2376,25 @@ export function frameStyleOf(base, frame) {
2123
2376
  };
2124
2377
  }
2125
2378
  const lookedStyles = new WeakMap();
2379
+ /** Frames with a named look laid over theirs (#1089), per look and frame:
2380
+ * the same look object each time, so `styleWithLook` keeps its styles. */
2381
+ const namedLookFrames = new WeakMap();
2382
+ /** `frame` with the named look `named` (#1089) laid over its own `look`
2383
+ * (`lookOver`): the order is the style, the template frame's look, then
2384
+ * the named look. `frame` itself when there is none. */
2385
+ export function withNamedLook(frame, named) {
2386
+ if (!named)
2387
+ return frame;
2388
+ let byFrame = namedLookFrames.get(named);
2389
+ if (!byFrame)
2390
+ namedLookFrames.set(named, (byFrame = new WeakMap()));
2391
+ const known = byFrame.get(frame);
2392
+ if (known)
2393
+ return known;
2394
+ const out = { ...frame, look: lookOver(frame.look, named) };
2395
+ byFrame.set(frame, out);
2396
+ return out;
2397
+ }
2126
2398
  /** `base` with what a frame's `look` sets (#976) written over it: colours
2127
2399
  * of the fill, border, stripe, icon, marker, title, text and label tab,
2128
2400
  * and the tracking of the body. A fill colour turns the fill on. A part