rapier-markdown-kit 1.2.8 → 1.2.10

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 (61) hide show
  1. package/dist/agent/will.mjs +0 -6
  2. package/dist/images/gif.mjs +0 -3
  3. package/dist/images/playback.mjs +0 -10
  4. package/dist/kit/assets.mjs +0 -1
  5. package/dist/kit/conformance/measurer.mjs +0 -0
  6. package/dist/kit/conformance/run.mjs +1 -8
  7. package/dist/kit/export-fonts.mjs +0 -14
  8. package/dist/kit/ledger/authored-origin.mjs +0 -5
  9. package/dist/kit/ledger/authorship.mjs +0 -2
  10. package/dist/kit/ledger/carried.mjs +4 -7
  11. package/dist/kit/ledger/data.mjs +0 -8
  12. package/dist/kit/ledger/format.mjs +0 -25
  13. package/dist/kit/ledger/hash.mjs +11 -7
  14. package/dist/kit/ledger/history.mjs +10 -29
  15. package/dist/kit/ledger/inverse-source.mjs +0 -46
  16. package/dist/kit/ledger/journal-records.mjs +24 -56
  17. package/dist/kit/ledger/merge.mjs +0 -27
  18. package/dist/kit/ledger/text-merge.mjs +0 -20
  19. package/dist/kit/ledger/transport.mjs +0 -47
  20. package/dist/kit/ledger.mjs +0 -4
  21. package/dist/kit/markdown-it.mjs +0 -3
  22. package/dist/kit/pdf-glyphs.mjs +1 -10
  23. package/dist/kit/pdf-source.mjs +5 -5
  24. package/dist/kit/pdf-vectors.mjs +0 -3
  25. package/dist/kit/print-capitals.mjs +0 -4
  26. package/dist/kit/render-entry.mjs +0 -1
  27. package/dist/kit/render-ink.mjs +0 -5
  28. package/dist/kit/render-markdown.mjs +12 -117
  29. package/dist/kit/render-print.mjs +30 -61
  30. package/dist/kit/render-sanitize.mjs +7 -59
  31. package/dist/kit/render-styles.mjs +0 -7
  32. package/dist/kit/render-work.mjs +0 -12
  33. package/dist/kit/render.mjs +46 -176
  34. package/dist/layout/hyphenation.mjs +0 -23
  35. package/dist/layout/line-plan.mjs +0 -20
  36. package/dist/layout/model.mjs +0 -55
  37. package/dist/layout/paragraph-fit.mjs +50 -60
  38. package/dist/layout/preparation.mjs +23 -56
  39. package/dist/layout/prepared-paragraph.mjs +0 -25
  40. package/dist/layout/typography.mjs +103 -41
  41. package/dist/packages/rapier-diff/concerns.mjs +0 -16
  42. package/dist/packages/rapier-diff/diff.mjs +0 -7
  43. package/dist/packages/rapier-diff/index.mjs +0 -1
  44. package/dist/packages/rapier-diff/matching.mjs +0 -19
  45. package/dist/packages/rapier-diff/merge.mjs +0 -9
  46. package/dist/packages/rapier-diff/moves.mjs +0 -7
  47. package/dist/packages/rapier-diff/structure.mjs +0 -2
  48. package/dist/packages/rapier-diff/tokens.mjs +0 -6
  49. package/dist/packages/rapier-diff/walk.mjs +0 -12
  50. package/dist/spec/document-fonts.mjs +0 -9
  51. package/dist/spec/document-settings.mjs +5 -24
  52. package/dist/spec/frontmatter.mjs +0 -1
  53. package/dist/spec/html-blocks.mjs +0 -36
  54. package/dist/spec/md-assets.mjs +2 -61
  55. package/dist/spec/md-layout.mjs +0 -34
  56. package/dist/spec/md-marks.mjs +0 -51
  57. package/dist/spec/md-source.mjs +0 -16
  58. package/dist/spec/template-page.mjs +6 -0
  59. package/dist/spec/word-sidecar.mjs +0 -2
  60. package/package.json +1 -1
  61. package/style.css +32 -16
@@ -1,9 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import {next, weight, exceptions, maximum} from '../agent/vendor/hyphen-en-us/patterns.js';
3
3
 
4
- // An original matcher over the permitted Kuiken/Plain TeX data. Its maximum pattern
5
- // length bounds the work per input character. Its static prefix data need no decoder,
6
- // runtime trie or font. Prepared paragraphs own the resulting positions for their lifetime.
7
4
  function englishCuts(word) {
8
5
  if (!/^[A-Z]?[a-z]+$/.test(word)) return null;
9
6
  if (word.length < 6) return [];
@@ -25,13 +22,6 @@ function englishCuts(word) {
25
22
  return cuts;
26
23
  }
27
24
 
28
- // Ask the host's offline hyphenator for successive word cuts. A zero-width word exposes
29
- // its first available cut; increasing the minimum prefix then exposes the next one.
30
- // Merely reading every line at width zero misses useful cuts (e.g. justifi-ca-tion).
31
- // Each round writes every live word first, then reads every word: no per-word layout
32
- // loop. This supplies positions only, never reusable font metrics or a second shaper.
33
- // Three graphemes on either side and at most one automatic split per word are deliberate
34
- // typography policy. Hosts without a dictionary for the language naturally return no cuts.
35
25
  export function paragraphHyphens(words, text, items, offsets, language, document) {
36
26
  const result = new Map();
37
27
  if (!language) return result;
@@ -40,14 +30,10 @@ export function paragraphHyphens(words, text, items, offsets, language, document
40
30
  for (const word of words) {
41
31
  if (word.protected) continue;
42
32
  const body = text.slice(word.start, word.end);
43
- // The exact ASCII subset avoids Unicode-regexp setup for ordinary English.
44
33
  const match = /^[-!"#%&'()*,.\/:;?@\[\]\\_{}]*([A-Za-z]+)[-!"#%&'()*,.\/:;?@\[\]\\_{}]*$/.exec(body) ||
45
34
  /^\p{P}*([\p{L}\p{M}]+)\p{P}*$/u.exec(body);
46
35
  if (!match || /[\u00a0\u00ad\u202f\u2060]/u.test(body)) continue;
47
36
  const start = word.start + body.indexOf(match[1]), end = start + match[1].length;
48
- // The prepared source words and runs are ordered. Visit only the overlapping
49
- // runs, retaining the exact open-boundary policy even for empty authored runs.
50
- // A hard-break chunk can begin far into the shared paragraph run array.
51
37
  if (itemIndex < 0) {
52
38
  let lo = 0, hi = items.length;
53
39
  while (lo < hi) {
@@ -69,8 +55,6 @@ export function paragraphHyphens(words, text, items, offsets, language, document
69
55
  let entry = unique.get(match[1]);
70
56
  if (!entry) {
71
57
  if (match[1].length < 6) continue;
72
- // Exact English needs neither ICU grapheme setup nor native DOM probing.
73
- // Language admission belongs to preparation; this is only its fallback owner.
74
58
  segmenter ||= new Intl.Segmenter(language, {granularity:'grapheme'});
75
59
  const units = [...segmenter.segment(match[1])].map(part => part.index);
76
60
  if (units.length < 6) continue;
@@ -81,8 +65,6 @@ export function paragraphHyphens(words, text, items, offsets, language, document
81
65
  jobs.push({word,start,entry});
82
66
  }
83
67
  if (!jobs.length) return result;
84
- // If this host cannot expose its language opportunities, preserve native automatic
85
- // hyphenation for the paragraph instead of projecting a weaker whole-word substitute.
86
68
  if (!document?.body || !document.defaultView?.CSS.supports('hyphenate-limit-chars', '6 3 3')) return null;
87
69
  const host = document.createElement('span');
88
70
  host.setAttribute('aria-hidden','true'); host.inert = true;
@@ -91,8 +73,6 @@ export function paragraphHyphens(words, text, items, offsets, language, document
91
73
  let active = [...unique.values()];
92
74
  for (const entry of active) {
93
75
  const element = document.createElement('div'); element.lang = language;
94
- // Do not reset `all` here: Blink's language declaration is a UA style, and an
95
- // inline reset would erase it. The shadow root already isolates author selectors.
96
76
  element.style.cssText = 'display:block;width:0;font:16px serif;line-height:20px;hyphens:auto;hyphenate-limit-chars:6 3 3;white-space:normal;word-break:normal;overflow-wrap:normal;text-wrap:wrap;';
97
77
  element.textContent = entry.text; shadow.append(element); entry.element = element;
98
78
  }
@@ -105,8 +85,6 @@ export function paragraphHyphens(words, text, items, offsets, language, document
105
85
  const node = entry.element.firstChild, {units} = entry;
106
86
  const top = index => {
107
87
  range.setStart(node,units[index]); range.setEnd(node,units[index+1]);
108
- // At a native discretionary boundary a character Range can also report
109
- // the preceding line's hyphen rectangle. Its final rect owns the character.
110
88
  return [...range.getClientRects()].at(-1)?.top;
111
89
  };
112
90
  const first = top(0), last = top(units.length-2);
@@ -117,7 +95,6 @@ export function paragraphHyphens(words, text, items, offsets, language, document
117
95
  entry.cuts.push(units[lo]); entry.minimum = lo+1;
118
96
  if (entry.minimum <= units.length-4) next.push(entry);
119
97
  }
120
- // All reads above finish before any mutation for the next shared layout.
121
98
  for (const entry of next) entry.element.style.hyphenateLimitChars = '6 '+entry.minimum+' 3';
122
99
  active = next;
123
100
  }
@@ -4,8 +4,6 @@ const lastSource = line => line.fragments.at(-1);
4
4
  const afterSource = (point, previous) => !!point && (!previous || point.itemIndex > previous.itemIndex ||
5
5
  point.itemIndex === previous.itemIndex && point.sourceEnd > previous.sourceEnd);
6
6
 
7
- // A chip may continue on later rows but does not cross a picture within the same row.
8
- // Containment uses the original item and materialized source interval, never engine cursors.
9
7
  function insideChip(items, line) {
10
8
  const last = lastSource(line), item = items?.[last?.itemIndex];
11
9
  return item?.extraWidth > 0 && typeof item.text === 'string' &&
@@ -27,12 +25,6 @@ function beforeChip(flow, range, line, cursor, items, next, materialize) {
27
25
  return null;
28
26
  }
29
27
 
30
- // A lone alphabetic prefix in a narrow picture-side slot need not be split when the
31
- // same engine continuation fits its whole word in a later slot on this row. This is
32
- // a small placement policy, not a reconstruction of upstream legal break candidates.
33
- // Authored no-break spaces stay with this alphabetic group. Punctuation, discretionary
34
- // hyphens, mixed items and non-alphabetic scripts retain
35
- // the engine's choice. All ranges and cursors remain untouched public API values.
36
28
  function laterWholeWord(flow, cursor, line, items, bands, slot, next, materialize) {
37
29
  if (line.fragments.length !== 1) return false;
38
30
  const first = line.fragments[0], item = items?.[first.itemIndex], text = item?.text;
@@ -63,8 +55,6 @@ function laterWholeWord(flow, cursor, line, items, bands, slot, next, materializ
63
55
  return false;
64
56
  }
65
57
 
66
- // Upstream owns every emitted break and emergency advance. Ordinary CSS balance
67
- // stays native; Draw's existing token-only balance is kept independently below.
68
58
  export function linePlan(flow, width, top, obstacles, lineHeight, minWidth, direction, balance,
69
59
  slotsForBand, layoutNextRichInlineLineRange, materializeRichInlineLineRange, items = null) {
70
60
  if (!flow || balance) return null;
@@ -104,8 +94,6 @@ export function linePlan(flow, width, top, obstacles, lineHeight, minWidth, dire
104
94
  return null;
105
95
  }
106
96
 
107
- // Draw's numeric tokens predate the rich engine contract and are not vendor handles. This is
108
- // its existing whole-column greedy walk, with its existing balancing decisions below.
109
97
  function measuredPlan(flow, width, lineHeight, fontSize, next, materialize) {
110
98
  const lines = [], ranges = [];
111
99
  let cursor = {itemIndex: 0, segmentIndex: 0, graphemeIndex: 0};
@@ -119,11 +107,6 @@ function measuredPlan(flow, width, lineHeight, fontSize, next, materialize) {
119
107
  return null;
120
108
  }
121
109
 
122
- // Draw's existing `text-wrap: balance` approximation, over its own measured token breaks:
123
- // `balance` is the block's font size. Only a run of two to six greedy lines, each on the whole column, no word broken, balances.
124
- // The score breaker minimises (width - line)² per line with Minikin's penalties (4·width·font a line, 2·width·font a soft hyphen)
125
- // and 10000 on the break before the last word; it stands when there are three breaks to choose from and it keeps greedy's count.
126
- // Otherwise the narrowest width that keeps the count, bisected to a pixel up from 0.8 of the mean line, laid greedy.
127
110
  function balanced(flow, width, fontSize, lines, greedy, lineHeight, layoutNextRichInlineLineRange, materializeRichInlineLineRange) {
128
111
  const count = greedy.length;
129
112
  if (count < 2 || count > 6 || lines.some((line, index) => line.x !== 0 || line.width !== width || line.y !== index * lineHeight) ||
@@ -137,7 +120,6 @@ function balanced(flow, width, fontSize, lines, greedy, lineHeight, layoutNextRi
137
120
  const last = range.fragments.at(-1), item = flow.items[last.itemIndex];
138
121
  return last.end.graphemeIndex === 0 && item?.prepared.kinds[last.end.segmentIndex - 1] === 'soft-hyphen';
139
122
  };
140
- // Every break a line from `from` can end at inside the column, farthest first: each narrower try yields the break before.
141
123
  const reach = from => {
142
124
  const found = [];
143
125
  for (let limit = width, previous = Infinity; found.length < 4096;) {
@@ -195,8 +177,6 @@ function balanced(flow, width, fontSize, lines, greedy, lineHeight, layoutNextRi
195
177
  return solved?.length === count ? place(solved) : lines;
196
178
  }
197
179
 
198
- // Draw supplies its SVG advances. Every door uses the same line plan without a canvas;
199
- // the tokens retain the source offsets and keep words and Unicode clusters intact.
200
180
  export function measuredLines(tokens, width, lineHeight, fontSize) {
201
181
  if (!tokens.length) return [];
202
182
  const prepared = {kinds: tokens.map(token => token.space ? 'space' : 'text')}, flow = {items: [{prepared}]};
@@ -8,10 +8,6 @@ export {captureFontAuthority, createPreparationScope, invalidateRichMetrics, pre
8
8
 
9
9
  const finite = value => typeof value === 'number' && Number.isFinite(value);
10
10
  const space = code => code === 32 || code === 9 || code === 10 || code === 13 || code === 12;
11
- // Upstream materialization supplies ORIGINAL item UTF-16 offsets. The only local work is
12
- // mapping the observed displayed text through whitespace/soft-hyphen spelling in that exact
13
- // interval. Cursors and engine grapheme/segment tables remain opaque. A normalization the
14
- // mapper cannot reproduce retains the native paragraph, never an approximate source address.
15
11
  export function mapFragment(run, fragment, {whiteSpace = 'normal'} = {}) {
16
12
  const raw = typeof run === 'string' ? run : run?.raw;
17
13
  if (run?.break === 'never') whiteSpace = 'normal';
@@ -38,16 +34,12 @@ export function mapFragment(run, fragment, {whiteSpace = 'normal'} = {}) {
38
34
  }
39
35
  let text = pieces.join('');
40
36
  if (lastSoftHyphen === end - 1 && fragment.text === text + '-') {
41
- // Only the engine's actual materialized discretionary cut supplies this glyph.
42
37
  offsets[offsets.length - 1] = lastSoftHyphen;
43
38
  offsets.push(end); text += '-';
44
39
  }
45
40
  return text === fragment.text ? {text, offsets, start, end} : null;
46
41
  }
47
42
 
48
- // The line a picture is measured in (`lines`, spec/md-layout.mjs): the anchor's line box, its type's cap height, and where that
49
- // cap height stands in the first line box (the half-leading model: the baseline sits half the leading below the box's top plus
50
- // the ascent). Read from the font as the page lays it out, so a text size or a line spacing moves the picture with the words.
51
43
  export function createLineMeasurer(authority = null) {
52
44
  const measurers = new WeakMap();
53
45
  const finite = value => typeof value === 'number' && Number.isFinite(value);
@@ -57,8 +49,6 @@ export function createLineMeasurer(authority = null) {
57
49
  const computed = view.getComputedStyle(element), size = parseFloat(computed.fontSize) || 16;
58
50
  const font = computed.fontStyle + ' ' + computed.fontWeight + ' ' + size + 'px ' + computed.fontFamily;
59
51
  const document = element.ownerDocument, epoch = authority?.captureFontAuthority(document);
60
- // The factory is also serialized on its own by the offline typography script. Without
61
- // an injected authority it stores only the canvas, and measures afresh in its loaded font.
62
52
  if (authority) { if (!authority.fontReady(document, font, 'H')) return null; }
63
53
  else {
64
54
  try {
@@ -79,7 +69,6 @@ export function createLineMeasurer(authority = null) {
79
69
  const capital = context.measureText('H');
80
70
  metrics = {ascent: capital.fontBoundingBoxAscent, descent: capital.fontBoundingBoxDescent, cap: capital.actualBoundingBoxAscent};
81
71
  if (![metrics.ascent, metrics.descent, metrics.cap].every(finite) || !(metrics.cap > 0)) return null;
82
- // Only bounded metrics of this document's currently loaded faces are retained.
83
72
  if (state.metrics) {
84
73
  if (state.metrics.size === 64) state.metrics.delete(state.metrics.keys().next().value);
85
74
  state.metrics.set(font, metrics);
@@ -92,7 +81,6 @@ export function createLineMeasurer(authority = null) {
92
81
 
93
82
  export const lineMetrics = createLineMeasurer({captureFontAuthority, fontReady});
94
83
 
95
- // `lines` tall: from the first line's cap height to the Nth line's baseline, N - 1 lines and one cap.
96
84
  export function linesHeight(lines, metrics) {
97
85
  const finite = value => typeof value === 'number' && Number.isFinite(value);
98
86
  const height = Number.isInteger(lines) && lines > 0 && metrics && finite(metrics.line) && finite(metrics.cap)
@@ -100,15 +88,12 @@ export function linesHeight(lines, metrics) {
100
88
  return height > 0 ? height : null;
101
89
  }
102
90
 
103
- // A picture sized in lines keeps its standoff off the line after its last: below its foot (the Nth baseline) the gap stops
104
- // at that line's bottom, so exactly N lines stand beside it.
105
91
  export function linesSlices(slices, layout, metrics, y, height) {
106
92
  if (layout?.lines == null || !metrics || !(slices?.length)) return slices;
107
93
  const floor = y + height + Math.max(0, metrics.line - metrics.capTop - metrics.cap) - .5;
108
94
  return slices.filter(slice => slice.y < floor).map(slice => slice.y + slice.height > floor ? {...slice, height: floor - slice.y} : slice);
109
95
  }
110
96
 
111
- // Where a picture's top stands in its anchor before `y`: a picture sized in lines stands on the first line's cap height.
112
97
  export const linesTop = (layout, metrics) => layout?.lines != null && finite(metrics?.capTop) ? metrics.capTop : 0;
113
98
 
114
99
  export function imageBox(columnWidth, naturalWidth, naturalHeight, layout = {}, defaultWidth = naturalWidth, metrics = null) {
@@ -123,7 +108,6 @@ export function imageBox(columnWidth, naturalWidth, naturalHeight, layout = {},
123
108
  return finite(height) && height > 0 ? {x, width, height} : null;
124
109
  }
125
110
 
126
- // Fit-then-clamp: scale until the turned AABB fits the column, then clamp its x. One result for live editor, styled export and Share.
127
111
  export function rasterReserved(columnWidth, unrotated, rad) {
128
112
  if (!unrotated || ![columnWidth, unrotated.x, unrotated.width, unrotated.height].every(value => finite(value) && value >= 0) ||
129
113
  columnWidth <= 0 || unrotated.width <= 0 || unrotated.height <= 0) return null;
@@ -143,14 +127,8 @@ export function rasterReserved(columnWidth, unrotated, rad) {
143
127
 
144
128
 
145
129
  const shapeGrid = 48, alphaThreshold = .1, alphaOversample = 4, profileBands = new WeakMap();
146
- // Slices are cut at up to this many bands: a picture shown 6 px a band or finer keeps every row the profile holds.
147
130
  const sliceGridMax = shapeGrid * alphaOversample, slicePixelsPerBand = 6;
148
131
 
149
- // The picture's silhouette as 192 bands of horizontal runs, read from its alpha at that resolution both ways: a band holds the
150
- // ink of one sampled row, and a run's ends fall on a 192nd of the picture's width, so the standoff a slice adds (`pictureSlices`'
151
- // `gap`) is the least distance from the words to the paint however large the picture is drawn. At 48 rows and columns a run's
152
- // end drifted by a 48th of the width (9 px at 430), a thin stroke fell between two samples, and a tall picture's bands were
153
- // 20 px high. The descriptor an exported page carries (`serializeProfile`) still writes 48 bands.
154
132
  export function alphaProfile(image) {
155
133
  const src = image.currentSrc || image.getAttribute('src') || '';
156
134
  if (!image.complete || !image.naturalWidth || !image.naturalHeight || !src || /^data:image\/jpe?g[;,]/i.test(src)) return null;
@@ -185,7 +163,6 @@ export function alphaProfile(image) {
185
163
  spanAt(t) { const runs = runsAt(t); return runs.length ? [runs[0][0], runs.at(-1)[1]] : null; }};
186
164
  }
187
165
 
188
- // Scanline intersection over a normalized polygon; the one implementation for live and export wrap=box.
189
166
  export function polygonProfile(corners) {
190
167
  return {spanAt(t) {
191
168
  const xs = [];
@@ -200,15 +177,12 @@ export function polygonProfile(corners) {
200
177
  }};
201
178
  }
202
179
 
203
- // w'=w|cos|+h|sin|, h'=w|sin|+h|cos| (markdown-standard.md). Shared by browser.js, interchange.js and share.js.
204
180
  export function rotatedBoundsRad(width, height, rad) {
205
181
  if (!rad) return {width, height};
206
182
  return {width: width * Math.abs(Math.cos(rad)) + height * Math.abs(Math.sin(rad)),
207
183
  height: width * Math.abs(Math.sin(rad)) + height * Math.abs(Math.cos(rad))};
208
184
  }
209
185
 
210
- // The `wrap=box` obstacle for a turned raster: its own untilted rectangle's four corners, rotated
211
- // `rad` about its centre, normalized into the turned bounding box's own [0,1] space.
212
186
  export function rasterTiltProfile(width, height, rad) {
213
187
  const cs = Math.cos(rad), sn = Math.sin(rad), rotated = rotatedBoundsRad(width, height, rad);
214
188
  const local = [[-width / 2, -height / 2], [width / 2, -height / 2], [width / 2, height / 2], [-width / 2, height / 2]];
@@ -216,7 +190,6 @@ export function rasterTiltProfile(width, height, rad) {
216
190
  (rotated.height / 2 + lx * sn + ly * cs) / rotated.height]));
217
191
  }
218
192
 
219
- // Each 48x48 cell of the turned box is unrotated into the raster's own pixels and sampled.
220
193
  export function rotatedRasterAlpha(baseAlpha, width, height, rad) {
221
194
  if (!baseAlpha) return null;
222
195
  if (!rad) return baseAlpha;
@@ -241,7 +214,6 @@ export function rotatedRasterAlpha(baseAlpha, width, height, rad) {
241
214
  return bandsProfile(bands);
242
215
  }
243
216
 
244
- // One occupancy descriptor: the editor's profile sampled at 48 band centres, shipped as data-rapier-occupancy, read back here.
245
217
  export function bandsProfile(bands) {
246
218
  if (!Array.isArray(bands) || !bands.length) return null;
247
219
  const at = t => bands[Math.max(0, Math.min(bands.length - 1, Math.floor(t * bands.length)))] || [];
@@ -258,7 +230,6 @@ export function serializeProfile(profile, count = shapeGrid) {
258
230
  const clean = (runs || []).map(([a, b]) => [Math.max(0, Math.min(1, +a)), Math.max(0, Math.min(1, +b))]).filter(([a, b]) => Number.isFinite(a) && Number.isFinite(b) && b > a);
259
231
  bands.push(clean.map(([a, b]) => {
260
232
  let left = Math.round(a * 1e4), right = Math.round(b * 1e4);
261
- // A narrow occupied run must not round to an invalid zero-width descriptor.
262
233
  if (left === right) { left = Math.floor(a * 1e4); right = Math.ceil(b * 1e4); }
263
234
  return (left / 1e4) + '-' + (right / 1e4);
264
235
  }).join(','));
@@ -273,14 +244,8 @@ export function parseProfile(text) {
273
244
  return bandsProfile(bands);
274
245
  }
275
246
 
276
- // The standoff is round: a band's own rows are pushed out by the whole gap, and the rows above and below it, out to a gap away,
277
- // by what a circle of that radius allows at their distance, in three steps. A square standoff stood up to 14 px off a slanted
278
- // stroke and 10 off a level one; this stands 10 off both, within a step.
279
247
  const shoulderSteps = 3, shoulders = Array.from({length: shoulderSteps}, (_, k) => ({from: k / shoulderSteps, to: (k + 1) / shoulderSteps, reach: Math.sqrt(1 - ((k + .5) / shoulderSteps) ** 2)}));
280
248
 
281
- // The reference scale at an element (docs/markdown-standard.md, "The reference scale"): its unit, `--md-unit` (a registered
282
- // length, 16 px at M), over 16. Every length the planner adds of its own (the standoff, a band's height, the room between two
283
- // pictures) is that many reference pixels times it, so a zoom moves no word against a picture.
284
249
  export function frameScale(element) {
285
250
  const view = element?.ownerDocument?.defaultView;
286
251
  const unit = view ? parseFloat(view.getComputedStyle(element).getPropertyValue('--md-unit')) : NaN;
@@ -290,7 +255,6 @@ export function frameScale(element) {
290
255
  export function pictureSlices(profile, x, y, width, height, scale = 1) {
291
256
  const gap = 10 * scale;
292
257
  if (!profile) return [{x: x - gap, y: y - gap, width: width + gap * 2, height: height + gap * 2}];
293
- // A band is at most 6 reference px high at the shown size, never coarser than the descriptor's 48 nor finer than the profile's 192.
294
258
  const count = Math.max(shapeGrid, Math.min(sliceGridMax, Math.ceil((finite(height) && height > 0 ? height : 0) / (slicePixelsPerBand * scale))));
295
259
  let byCount = profileBands.get(profile);
296
260
  if (!byCount) profileBands.set(profile, byCount = new Map());
@@ -300,14 +264,12 @@ export function pictureSlices(profile, x, y, width, height, scale = 1) {
300
264
  let previous = new Map();
301
265
  for (let index = 0; index < count; index++) {
302
266
  const top = index / count, bottom = (index + 1) / count;
303
- // Sampled just inside the bottom edge: on the edge itself a band profile answers with the next band's row and the slice runs a row long.
304
267
  const runs = [top, (top + bottom) / 2, bottom - 1e-9].flatMap(t => {
305
268
  if (profile.runsAt) return profile.runsAt(t) || [];
306
269
  const span = profile.spanAt(t);
307
270
  return span ? [span] : [];
308
271
  }).filter(run => Number.isFinite(run[0]) && Number.isFinite(run[1]) && run[1] > run[0])
309
272
  .sort((a, b) => a[0] - b[0]);
310
- // Preserve transparent holes and gaps; only overlapping runs share an obstacle.
311
273
  const merged = [];
312
274
  for (const run of runs) {
313
275
  const tail = merged.at(-1);
@@ -361,10 +323,6 @@ export function slotsForBand(width, obstacles, top, height, minWidth = 40) {
361
323
  return slots;
362
324
  }
363
325
 
364
- // A picture moves a block's words only where its outline (with its standoff) meets them. The block is laid as it lies with no
365
- // picture beside it, balanced and aligned in the whole column; when no line of words there meets an obstacle, that is its
366
- // layout. Otherwise its lines flow around the outline. A short heading can fit beside the outline in a slot narrower
367
- // than minWidth, so start-aligned, unbalanced words need the same contact check.
368
326
  export function flowLines(flow, width, top, obstacles, lineHeight, minWidth, direction = 'ltr', balance = 0, align = 'start') {
369
327
  const items = paragraphItems(flow);
370
328
  const plan = linePlan(flow, width, top, obstacles, lineHeight, minWidth, direction, balance, slotsForBand, layoutNextRichInlineLineRange, materializeRichInlineLineRange, items);
@@ -383,8 +341,6 @@ export function flowLines(flow, width, top, obstacles, lineHeight, minWidth, dir
383
341
  return meets ? plan : free;
384
342
  }
385
343
 
386
- // ---- The one wrap shape (live: globalThis.RapierImageLayout; export: modules["layout/model.mjs"]) ----
387
- // `obstacles` in the paragraph's coordinates. Null when nothing to flow around or too narrow for a float.
388
344
  export function wrapShape(side, obstacles, width, top) {
389
345
  const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
390
346
  const rows = obstacles.filter(obstacle => (obstacle.x > width - (obstacle.x + obstacle.width) ? 'right' : 'left') === side);
@@ -417,10 +373,6 @@ export function wrapShape(side, obstacles, width, top) {
417
373
  return {boxWidth, boxHeight, startY, points};
418
374
  }
419
375
 
420
- // ---- What a block draws at its edge moves with its words (live and export alike) ----
421
- // A bordered box (a disclosure) cannot follow an outline line by line: it stands clear of it. `obstacles` in the box's
422
- // border-box coordinates, its band [top, top + height]: the insets that end the box where the outline begins on each
423
- // side, `fits` false when they leave less than `floor`; null when the outline does not meet the box.
424
376
  export function boxClearance(obstacles, width, top, height, floor) {
425
377
  let left = 0, right = 0;
426
378
  for (const row of obstacles) {
@@ -431,14 +383,12 @@ export function boxClearance(obstacles, width, top, height, floor) {
431
383
  return left || right ? {left, right, fits: width - left - right >= floor} : null;
432
384
  }
433
385
 
434
- // Where an element's content begins at the inline start (a right edge counts negative in a right-to-left block).
435
386
  function contentStart(element) {
436
387
  const style = element.ownerDocument.defaultView.getComputedStyle(element), box = element.getBoundingClientRect();
437
388
  return style.direction === 'rtl' ? -(box.right - (parseFloat(style.paddingRight) || 0) - (parseFloat(style.borderRightWidth) || 0))
438
389
  : box.left + (parseFloat(style.paddingLeft) || 0) + (parseFloat(style.borderLeftWidth) || 0);
439
390
  }
440
391
 
441
- // The lines of words in `root` (text outside `skip`), each with its block, where it starts and where its block's content starts.
442
392
  export function edgeLines(root, skip = () => false) {
443
393
  const doc = root.ownerDocument, view = doc.defaultView, lines = [], starts = new Map();
444
394
  const walker = doc.createTreeWalker(root, 4);
@@ -460,9 +410,6 @@ export function edgeLines(root, skip = () => false) {
460
410
  return lines;
461
411
  }
462
412
 
463
- // After a picture's floats settle: how far each line moved from where its block's lines began without them (`before`,
464
- // edgeLines read first). A list item's marker moves as far as its first line; a quote's bar moves to stand beside its
465
- // moved lines as it stood beside them before, in runs of one shift from the quote's top to its bottom.
466
413
  export function edgeMarks(root, before, skip = () => false) {
467
414
  const view = root.ownerDocument.defaultView, home = new Map();
468
415
  const byBlock = rows => {
@@ -488,7 +435,6 @@ export function edgeMarks(root, before, skip = () => false) {
488
435
  const shift = shiftOf(item);
489
436
  if (shift > .5) markers.push({item, shift});
490
437
  }
491
- // A numbered item's dotted line runs to the next circle: where the two circles moved apart it is not drawn (`steps`).
492
438
  const steps = [];
493
439
  for (const list of within('ol')) {
494
440
  const items = [...list.children].filter(child => child.tagName === 'LI');
@@ -517,7 +463,6 @@ export function edgeMarks(root, before, skip = () => false) {
517
463
  return {markers, bars, steps};
518
464
  }
519
465
 
520
- // ---- The one "paragraph holds only the picture": one meaningful child, the image or an <a> wrapping only it ----
521
466
  export function imageOnly(paragraph, image) {
522
467
  const meaningful = node => [...node.childNodes].filter(child => child.nodeType !== 8 &&
523
468
  !(child.nodeType === 3 && !child.textContent.trim()));
@@ -1,10 +1,4 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // Original paragraph-wide optimization over immutable Pretext geometry. Four spacing
3
- // classes and the discretionary run length are the actual transition policy, not
4
- // buckets approximating a continuous history. The numerical solver requests exact
5
- // missing edges; the preparation scope observes them before declaring a plan current.
6
- // Normal spaces retain 60–180% of their natural advance. At most two consecutive
7
- // discretionary endings are allowed, and the final line starts whole.
8
2
  export function createParagraphFitter() {
9
3
  const indexes = new WeakMap();
10
4
  function prepare(input) {
@@ -15,7 +9,6 @@ export function createParagraphFitter() {
15
9
  segment.words.forEach((word, index) => {
16
10
  prefix.push(prefix.at(-1) + word.width + (word.gap?.width || 0));
17
11
  gaps.push(gaps.at(-1) + (word.gap?.width || 0));
18
- // Conservative bound for interior whole words; no substring-width subtraction.
19
12
  minimum.push(minimum.at(-1) + Math.max(0, word.width - word.endTrim) + Math.max(0, (word.gap?.width || 0) * .6));
20
13
  word.breaks.forEach((cut, cutIndex) => points.push({word: index, cut, cutIndex, end: cut.offset, next: cut.offset, kind: cut.kind}));
21
14
  points.push({word: index + 1, cut: null, cutIndex: -1, end: word.end, next: word.gap?.end ?? word.end,
@@ -24,22 +17,23 @@ export function createParagraphFitter() {
24
17
  if (segment.words.length) points[0].end = points[0].next = segment.words[0].start;
25
18
  return {...segment, prefix, gaps, minimum, points};
26
19
  });
27
- // Numeric indexes are private and never written after this point. Their opaque
28
- // handle keeps hot numeric arrays out of public custody without freezing them
29
- // into a different element representation on each engine.
30
20
  const handle = Object.freeze({});
31
21
  indexes.set(handle, {text: input.text, segments});
32
22
  return handle;
33
23
  }
34
24
 
35
- function* solveSteps(handle, width, {indent = 0, left = 0, right = 0} = {}, known) {
25
+ function* solveSteps(handle, width, {indent = 0, left = 0, right = 0, widths = null} = {}, known) {
36
26
  const prepared = indexes.get(handle);
37
27
  if (!prepared || ![width, indent, left, right].every(Number.isFinite) || width <= 0) return null;
28
+ const measures = Array.isArray(widths) && widths.every(value => Number.isFinite(value) && value > 0) ? widths : [];
29
+ const measure = line => line < measures.length ? measures[line] : width;
30
+ const widest = Math.max(width, ...measures);
38
31
  const lines = []; let work = 0, visited = 0, totalCost = 0;
39
32
  for (let segmentIndex = 0; segmentIndex < prepared.segments.length; segmentIndex++) {
40
33
  const {words, points, prefix, gaps, minimum} = prepared.segments[segmentIndex];
41
34
  if (!words.length) continue;
42
- const ceiling = width + Math.max(0, left) + Math.max(0, right) + Math.max(0, -indent);
35
+ const base = lines.length, classes = Math.max(1, measures.length - base + 1);
36
+ const ceiling = widest + Math.max(0, left) + Math.max(0, right) + Math.max(0, -indent);
43
37
  const lastIndex = points.length - 1;
44
38
  let smallest = Infinity;
45
39
  for (let index = 0; index < words.length - 1; index++) smallest = Math.min(smallest, words[index].gap?.width || 0);
@@ -48,14 +42,11 @@ export function createParagraphFitter() {
48
42
  natural += words[0].startExtra;
49
43
  const opening = Math.min(Math.max(0, left), words[0].opening);
50
44
  const closing = Math.min(Math.max(0, right), words.at(-1).closing);
51
- const target = width - (segmentIndex === 0 ? indent : 0) + opening + closing;
45
+ const target = measure(base) - (segmentIndex === 0 ? indent : 0) + opening + closing;
52
46
  const count = words.length - 1;
53
47
  const delta = !count ? 0 : Math.max(-smallest * .4, Math.min(0, (target - natural) / count));
54
48
  const overflow = natural + delta * count - target;
55
49
  const cost = 1 + 100 * Math.max(0, overflow / target) ** 2;
56
- // Every edge costs at least one and every transition penalty is nonnegative.
57
- // This is the original start-to-final arithmetic, so a valid single edge below
58
- // two is the exact global optimum. Keep legal cuts for the overflow exception.
59
50
  if (Number.isFinite(natural) && target > 0 && cost < 2 && minimum[words.length - 1] - minimum[Math.min(words.length - 1, 1)] <= ceiling + .02 &&
60
51
  !(overflow > .02 && (count || words.at(-1).breaks.length))) {
61
52
  lines.push({start: points[0].next, end: points[lastIndex].end, next: points[lastIndex].next, kind: 'end', glyph: '',
@@ -63,16 +54,11 @@ export function createParagraphFitter() {
63
54
  totalCost += cost; work++; continue;
64
55
  }
65
56
 
66
- // A point's predecessors are final before that point is visited. Store its
67
- // winning cost and backpointer directly; later points cannot observe an old
68
- // replaced state. Build source line records only for the final chosen path.
69
- const costs = new Float64Array(points.length * 12);
57
+ const stride = classes * 12;
58
+ const costs = new Float64Array(points.length * stride);
70
59
  const previous = new Float64Array(costs.length), spacing = new Float64Array(costs.length);
71
- const prefixes = [], suffixes = [], reachable = new Uint16Array(points.length);
60
+ const prefixes = [], suffixes = [], reachable = new Uint16Array(points.length * classes);
72
61
  reachable[0] = 1 << 3;
73
- // The final line rejects a discretionary start. In its last word, automatic
74
- // points cannot lead to any later cut either. Authored soft points need a later
75
- // ordinary cut to reset that state. Retain breaks.length for overflow policy.
76
62
  let lastPlain = lastIndex - 1;
77
63
  while (lastPlain > 0 && (points[lastPlain].kind === 'soft' || points[lastPlain].kind === 'auto')) lastPlain--;
78
64
  for (let endIndex = 1; endIndex < points.length; endIndex++) {
@@ -88,9 +74,9 @@ export function createParagraphFitter() {
88
74
  while (minWord > start.word) smallest = Math.min(smallest, words[--minWord].gap?.width || 0);
89
75
  if (final && (start.kind === 'soft' || start.kind === 'auto')) continue;
90
76
  if (start.cut && end.cut && start.word === end.word && (start.kind === 'auto' || end.kind === 'auto')) continue;
91
- // Metric work is useful only if a prior state can actually reach this edge.
92
- const available = reachable[startIndex] & (discretionary ? 0x6db : 0xfff);
93
- if (!available) continue;
77
+ let any = 0;
78
+ for (let lineClass = 0; lineClass < classes; lineClass++) any |= reachable[startIndex * classes + lineClass];
79
+ if (!(any & (discretionary ? 0x6db : 0xfff))) continue;
94
80
  let natural;
95
81
  if (start.cut && end.cut && start.word === end.word) {
96
82
  natural = yield {segment: segmentIndex, word: start.word, from: start.cutIndex, to: end.cutIndex};
@@ -117,49 +103,53 @@ export function createParagraphFitter() {
117
103
  const count = lastWord - start.word, gapWidth = gaps[lastWord] - gaps[start.word];
118
104
  const opening = start.cut ? 0 : Math.min(Math.max(0, left), words[start.word].opening);
119
105
  const closing = end.cut ? 0 : Math.min(Math.max(0, right), words[lastWord].closing);
120
- const target = width - (segmentIndex === 0 && startIndex === 0 ? indent : 0) + opening + closing;
121
- if (target <= 0) continue;
122
- const remaining = target - natural;
123
- const delta = !count ? 0 : Math.max(-smallest * .4, Math.min(final ? 0 : smallest * .8, remaining / count));
124
- const overflow = natural + delta * count - target;
125
- // A truly unbreakable word/atom can stand alone at its natural width. Never
126
- // fabricate an emergency source cut or eject the surrounding paragraph.
127
- if (overflow > .02 && (count || start.cut || end.cut || words[lastWord].breaks.length)) continue;
128
- const residual = Math.max(0, remaining - delta * count);
129
- const ratio = gapWidth > 0 ? delta * count / gapWidth : 0;
130
- const fitness = final ? 1 : ratio < -.075 ? 0 : ratio <= .2 ? 1 : ratio <= .5 ? 2 : 3;
131
- let local = 1 + 100 * Math.max(0, overflow / target) ** 2;
132
- if (final && startIndex) local += 100 * Math.max(0, .45 - natural / target) ** 2 + (count === 0 ? 25 : 0);
133
- else if (!final) local += (ratio < 0 ? 100 : 12.5) * ratio ** 2 + 2400 * (residual / target) ** 2;
134
106
  const priorDiscretionary = start.kind === 'soft' || start.kind === 'auto';
135
- if (discretionary) local += 12 + (priorDiscretionary ? 12 : 0);
136
107
  if (final && priorDiscretionary) continue;
137
- // The mask visits exactly the occupied slots, in the original ascending
138
- // slot order, retaining both the objective and its deterministic ties.
139
- for (let active = available; active; active &= active - 1) {
140
- const slot = 31 - Math.clz32(active & -active), priorIndex = startIndex * 12 + slot;
141
- const hyphens = discretionary ? slot % 3 + 1 : 0;
142
- if (hyphens > 2) continue;
143
- work++;
144
- const cost = costs[priorIndex] + local + (!final && startIndex ? .6 * (fitness - Math.floor(slot / 3)) ** 2 : 0);
145
- const stateIndex = fitness * 3 + hyphens;
146
- const index = endIndex * 12 + stateIndex;
147
- if (reachable[endIndex] & 1 << stateIndex && costs[index] <= cost) continue;
148
- costs[index] = cost; previous[index] = priorIndex; spacing[index] = delta;
149
- reachable[endIndex] |= 1 << stateIndex;
108
+ for (let lineClass = 0; lineClass < classes; lineClass++) {
109
+ const available = reachable[startIndex * classes + lineClass] & (discretionary ? 0x6db : 0xfff);
110
+ if (!available) continue;
111
+ const target = measure(base + lineClass) - (segmentIndex === 0 && startIndex === 0 ? indent : 0) + opening + closing;
112
+ if (target <= 0) continue;
113
+ const remaining = target - natural;
114
+ const delta = !count ? 0 : Math.max(-smallest * .4, Math.min(final ? 0 : smallest * .8, remaining / count));
115
+ const overflow = natural + delta * count - target;
116
+ if (overflow > .02 && (count || start.cut || end.cut || words[lastWord].breaks.length)) continue;
117
+ const residual = Math.max(0, remaining - delta * count);
118
+ const ratio = gapWidth > 0 ? delta * count / gapWidth : 0;
119
+ const fitness = final ? 1 : ratio < -.075 ? 0 : ratio <= .2 ? 1 : ratio <= .5 ? 2 : 3;
120
+ let local = 1 + 100 * Math.max(0, overflow / target) ** 2;
121
+ if (final && startIndex) local += 100 * Math.max(0, .45 - natural / target) ** 2 + (count === 0 ? 25 : 0);
122
+ else if (!final) local += (ratio < 0 ? 100 : 12.5) * ratio ** 2 + 2400 * (residual / target) ** 2;
123
+ if (discretionary) local += 12 + (priorDiscretionary ? 12 : 0);
124
+ const set = !final && count && residual > 0 ? remaining / count : delta;
125
+ const nextClass = Math.min(lineClass + 1, classes - 1);
126
+ for (let active = available; active; active &= active - 1) {
127
+ const slot = 31 - Math.clz32(active & -active), priorIndex = startIndex * stride + lineClass * 12 + slot;
128
+ const hyphens = discretionary ? slot % 3 + 1 : 0;
129
+ if (hyphens > 2) continue;
130
+ work++;
131
+ const cost = costs[priorIndex] + local + (!final && startIndex ? .6 * (fitness - Math.floor(slot / 3)) ** 2 : 0);
132
+ const stateIndex = fitness * 3 + hyphens;
133
+ const index = endIndex * stride + nextClass * 12 + stateIndex;
134
+ const bit = endIndex * classes + nextClass;
135
+ if (reachable[bit] & 1 << stateIndex && costs[index] <= cost) continue;
136
+ costs[index] = cost; previous[index] = priorIndex; spacing[index] = set;
137
+ reachable[bit] |= 1 << stateIndex;
138
+ }
150
139
  }
151
140
  }
152
141
  }
153
142
  let best = -1;
154
- for (let slot = 0; slot < 12; slot++) if (reachable[lastIndex] & 1 << slot) {
155
- const index = lastIndex * 12 + slot;
156
- if (best < 0 || costs[index] < costs[best]) best = index;
157
- }
143
+ for (let lineClass = 0; lineClass < classes; lineClass++) for (let slot = 0; slot < 12; slot++)
144
+ if (reachable[lastIndex * classes + lineClass] & 1 << slot) {
145
+ const index = lastIndex * stride + lineClass * 12 + slot;
146
+ if (best < 0 || costs[index] < costs[best]) best = index;
147
+ }
158
148
  if (best < 0) return null;
159
149
  totalCost += costs[best];
160
150
  const chosen = [];
161
151
  for (let endIndex = lastIndex; endIndex;) {
162
- const priorIndex = previous[best], startIndex = Math.floor(priorIndex / 12);
152
+ const priorIndex = previous[best], startIndex = Math.floor(priorIndex / stride);
163
153
  const start = points[startIndex], end = points[endIndex], lastWord = end.cut ? end.word : end.word - 1;
164
154
  chosen.push({start: start.next, end: end.end, next: end.next, kind: end.kind, glyph: end.cut?.glyph || '',
165
155
  wordSpacing: spacing[best], opening: start.cut ? 0 : Math.min(Math.max(0, left), words[start.word].opening),