rapier-markdown-kit 1.1.102 → 1.1.104

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 (60) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +213 -53
  3. package/dist/agent/vendor/hyphen-en-us/LICENSE +14 -0
  4. package/dist/agent/vendor/hyphen-en-us/SOURCE.json +33 -0
  5. package/dist/agent/vendor/hyphen-en-us/hyph-en-us.hyp.txt +14 -0
  6. package/dist/agent/vendor/hyphen-en-us/hyph-en-us.pat.txt +4938 -0
  7. package/dist/agent/vendor/hyphen-en-us/patterns.js +25 -0
  8. package/dist/agent/vendor/pretext/LICENSE +208 -0
  9. package/dist/agent/vendor/pretext/NOTICE +26 -13
  10. package/dist/agent/vendor/pretext/SOURCE.json +213 -45
  11. package/dist/agent/vendor/pretext/analysis.js +675 -1224
  12. package/dist/agent/vendor/pretext/entry-geometry.js +183 -0
  13. package/dist/agent/vendor/pretext/gecko-line-breaks.js +688 -0
  14. package/dist/agent/vendor/pretext/generated/engine-break-data.js +57 -0
  15. package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +80 -0
  16. package/dist/agent/vendor/pretext/graphemes.js +98 -0
  17. package/dist/agent/vendor/pretext/han-kerning.js +253 -0
  18. package/dist/agent/vendor/pretext/layout.js +352 -290
  19. package/dist/agent/vendor/pretext/line-break.js +1154 -588
  20. package/dist/agent/vendor/pretext/line-breaks.js +892 -0
  21. package/dist/agent/vendor/pretext/line-text.js +81 -39
  22. package/dist/agent/vendor/pretext/measurement.js +1105 -176
  23. package/dist/agent/vendor/pretext/prepare.js +807 -0
  24. package/dist/agent/vendor/pretext/rich-inline.js +1242 -273
  25. package/dist/agent/will.mjs +2 -0
  26. package/dist/kit/assets.mjs +5 -1
  27. package/dist/kit/conformance/run.mjs +4 -4
  28. package/dist/kit/export-fonts.mjs +26 -11
  29. package/dist/kit/index.mjs +1 -0
  30. package/dist/kit/ledger/carried.mjs +9 -7
  31. package/dist/kit/ledger/data.mjs +73 -13
  32. package/dist/kit/ledger/format.mjs +166 -53
  33. package/dist/kit/ledger/hash.mjs +75 -18
  34. package/dist/kit/ledger/history.mjs +143 -115
  35. package/dist/kit/ledger/inverse-source.mjs +158 -19
  36. package/dist/kit/ledger/journal-records.mjs +292 -67
  37. package/dist/kit/model.mjs +1 -1
  38. package/dist/kit/pdf-glyphs.mjs +4 -4
  39. package/dist/kit/render-markdown.mjs +259 -122
  40. package/dist/kit/render-sanitize.mjs +24 -4
  41. package/dist/kit/render-work.mjs +99 -20
  42. package/dist/kit/render.mjs +101 -65
  43. package/dist/kit/typography.mjs +2 -0
  44. package/dist/layout/hyphenation.mjs +127 -0
  45. package/dist/layout/line-plan.mjs +88 -96
  46. package/dist/layout/model.mjs +61 -80
  47. package/dist/layout/paragraph-fit.mjs +191 -0
  48. package/dist/layout/preparation.mjs +373 -0
  49. package/dist/layout/prepared-paragraph.mjs +223 -0
  50. package/dist/layout/typography.mjs +345 -111
  51. package/dist/packages/rapier-diff/index.mjs +288 -196
  52. package/dist/spec/document-fonts.mjs +13 -4
  53. package/dist/spec/md-assets.mjs +149 -30
  54. package/dist/spec/md-layout.mjs +5 -2
  55. package/dist/spec/md-marks.mjs +218 -3
  56. package/dist/spec/md-source.mjs +122 -0
  57. package/package.json +2 -1
  58. package/style.css +9 -0
  59. package/dist/agent/vendor/pretext/bidi.js +0 -151
  60. package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
@@ -1,14 +1,23 @@
1
1
  // SPDX-License-Identifier: MIT
2
+ import {createPreparationScope, supportsRichStyle, captureFontAuthority} from './preparation.mjs';
3
+ import {createParagraphFitter} from './paragraph-fit.mjs';
2
4
 
3
5
  // Layout projections keep the original text in place. The factory also runs in an exported page.
4
- export function createTypography() {
6
+ export function createTypography(fitter = createParagraphFitter()) {
5
7
  const mark = 'data-rapier-typography', boundary = mark + '-break', hang = mark + '-hang';
6
8
  const protectedInitial = mark + '-protected-initial';
9
+ const gapMark = mark + '-gap', fitMark = mark + '-fit';
7
10
  const exclude = 'code,kbd,samp,math,script,style,.math-rendered,[hidden]';
8
11
  const candidates = 'p[data-md-hyphens="auto"],p[data-md-lines],p[data-md-hanging="on"],p[' + mark + ']';
9
12
  const properties = ['--md-drop-cap-font', '--md-hang-start'];
10
- const states = new WeakMap(), documents = new WeakMap();
11
- const graphemes = new Intl.Segmenter(undefined, {granularity: 'grapheme'});
13
+ const states = new WeakMap(), documents = new WeakMap(), live = new Set();
14
+ function scope(document) {
15
+ let value = documents.get(document);
16
+ if (!value) { value = createPreparationScope(document); documents.set(document, value); }
17
+ return value;
18
+ }
19
+ let graphemes;
20
+ const segment = text => (graphemes ||= new Intl.Segmenter(undefined, {granularity: 'grapheme'})).segment(text);
12
21
  const opening = /^[\p{Ps}\p{Pi}\p{Pf}"']+$/u;
13
22
  const closing = /^[\p{Pe}\p{Pi}\p{Pf}"'.,;:!?]+$/u;
14
23
 
@@ -18,7 +27,7 @@ export function createTypography() {
18
27
  const start = /^\s*/u.exec(text)[0].length;
19
28
  if (start === text.length) return null;
20
29
  let end = start;
21
- for (const part of graphemes.segment(text.slice(start))) {
30
+ for (const part of segment(text.slice(start))) {
22
31
  if (/^\s/u.test(part.segment)) break;
23
32
  end = start + part.index + part.segment.length;
24
33
  if (!opening.test(part.segment)) break;
@@ -30,11 +39,12 @@ export function createTypography() {
30
39
  return [...(root?.matches?.(selector) ? [root] : []), ...(root?.querySelectorAll?.(selector) || [])];
31
40
  }
32
41
 
33
- function runs(paragraph) {
34
- const walker = paragraph.ownerDocument.createTreeWalker(paragraph, 4), result = [];
42
+ function runs(paragraph, hard = null) {
43
+ const walker = paragraph.ownerDocument.createTreeWalker(paragraph, hard ? 5 : 4), result = [];
35
44
  let start = 0;
36
45
  while (walker.nextNode()) {
37
46
  const node = walker.currentNode;
47
+ if (node.nodeType !== 3) { if (node.tagName === 'BR') hard.push(start); continue; }
38
48
  if (node.length) result.push({node, start, end: start + node.length});
39
49
  start += node.length;
40
50
  }
@@ -55,7 +65,7 @@ export function createTypography() {
55
65
  // This data-only guard is safe on an active edit surface, including during composition.
56
66
  function protectInitial(root) {
57
67
  for (const paragraph of paragraphs(root, 'p[data-md-lines],p[data-md-hanging="on"],p[' + protectedInitial + ']')) {
58
- const initial = initialRange(paragraph), letter = initial && [...graphemes.segment(initial.text)].at(-1);
68
+ const initial = initialRange(paragraph), letter = initial && [...segment(initial.text)].at(-1);
59
69
  const at = initial && point(runs(paragraph), initial.start + letter.index);
60
70
  const enabled = paragraph.hasAttribute('data-md-lines') || paragraph.getAttribute('data-md-hanging') === 'on';
61
71
  paragraph.toggleAttribute(protectedInitial, Boolean(enabled && at?.node.parentElement?.closest(exclude)));
@@ -92,7 +102,7 @@ export function createTypography() {
92
102
  const items = runs(paragraph), text = paragraph.textContent;
93
103
  const initial = paragraph.hasAttribute('data-md-lines') ? initialRange(text) : null;
94
104
  let first = null, end = null;
95
- for (const part of graphemes.segment(text)) {
105
+ for (const part of segment(text)) {
96
106
  if (initial && part.index < initial.end) continue;
97
107
  if (/^\s+$/u.test(part.segment)) continue;
98
108
  const rects = boxes(items, part.index, part.index + part.segment.length, height);
@@ -108,6 +118,7 @@ export function createTypography() {
108
118
  function setProperty(paragraph, state, name, value) {
109
119
  if (!state.properties.has(name)) state.properties.set(name, [paragraph.style.getPropertyValue(name), paragraph.style.getPropertyPriority(name)]);
110
120
  paragraph.style.setProperty(name, value);
121
+ state.properties.get(name)[2] = paragraph.style.getPropertyValue(name);
111
122
  }
112
123
 
113
124
  function selectionBefore(paragraph) {
@@ -164,96 +175,48 @@ export function createTypography() {
164
175
  for (const {move, pieces} of restore) { move.node.data = move.before; for (const piece of pieces.slice(1)) piece.remove(); }
165
176
  }
166
177
 
167
- function clear(root) {
178
+ function clear(root, {forget = true} = {}) {
168
179
  const list = paragraphs(root, 'p[' + mark + '],p[' + protectedInitial + ']');
169
180
  for (const paragraph of list) {
170
181
  if (!paragraph.hasAttribute(mark)) { paragraph.removeAttribute(protectedInitial); continue; }
171
182
  const selected = selectionBefore(paragraph);
172
- for (const node of paragraph.querySelectorAll('[' + boundary + '],[' + hang + ']')) node.replaceWith(...node.childNodes);
173
183
  const state = states.get(paragraph);
184
+ state?.work?.return();
185
+ state?.fitWork?.return();
186
+ for (const node of paragraph.querySelectorAll('[' + boundary + '],[' + hang + '],[' + gapMark + ']')) node.replaceWith(...node.childNodes);
174
187
  restoreInitial(state);
175
188
  for (const name of properties) {
176
189
  const previous = state?.properties.get(name);
190
+ if (!previous || paragraph.style.getPropertyValue(name) !== previous[2] || paragraph.style.getPropertyPriority(name)) continue;
177
191
  if (previous?.[0]) paragraph.style.setProperty(name, previous[0], previous[1]);
178
192
  else paragraph.style.removeProperty(name);
179
193
  }
180
194
  paragraph.removeAttribute(mark); paragraph.removeAttribute(mark + '-ragged');
195
+ paragraph.removeAttribute(fitMark);
181
196
  paragraph.removeAttribute(protectedInitial);
182
197
  if (!paragraph.getAttribute('style')) paragraph.removeAttribute('style');
183
198
  paragraph.normalize();
184
199
  states.delete(paragraph);
200
+ live.delete(paragraph);
201
+ if (forget) scope(paragraph.ownerDocument).forget(paragraph);
185
202
  restoreSelection(paragraph, selected);
186
203
  }
187
204
  }
188
205
 
189
- function fontEpoch(document) {
190
- let entry = documents.get(document);
191
- if (!entry) {
192
- entry = {epoch: 0}; documents.set(document, entry);
193
- document.fonts?.addEventListener('loadingdone', () => { entry.epoch++; });
194
- }
195
- return entry.epoch;
196
- }
197
-
198
- function signature(paragraph, style, width, hangingGeometry) {
199
- return [paragraph.innerHTML, width, hangingGeometry, style.font, style.lineHeight, style.letterSpacing, style.wordSpacing,
200
- style.writingMode, style.direction, style.textIndent, style.paddingInlineStart, style.paddingInlineEnd,
201
- paragraph.getAttribute('data-md-layout'), paragraph.getAttribute('data-md-align'), paragraph.getAttribute('data-md-lines'),
202
- paragraph.getAttribute('data-md-hyphens'), paragraph.getAttribute('data-md-caps'), paragraph.getAttribute('data-md-hanging'),
203
- paragraph.closest('[lang]')?.getAttribute('lang'), fontEpoch(paragraph.ownerDocument)].join('\u0000');
204
- }
205
-
206
- function insertBoundary(paragraph, items, offset) {
207
- const at = point(items, offset);
208
- if (!at || at.node.parentElement?.closest(exclude + ',[' + boundary + ']')) return false;
209
- const separator = /^[\t\n\r\f ]+/.exec(at.node.data.slice(at.offset));
210
- if (!separator) return false;
211
- let text = at.node;
212
- const end = at.offset + separator[0].length;
213
- if (end < text.length) text.splitText(end);
214
- if (at.offset) text = text.splitText(at.offset);
215
- const node = paragraph.ownerDocument.createElement('span'); node.setAttribute(boundary, '');
216
- text.before(node); node.append(text);
217
- return true;
218
- }
219
-
220
- function stretchedLines(paragraph, style, maximum) {
221
- const text = paragraph.textContent, items = runs(paragraph), height = ordinaryHeight(paragraph, style), lines = [];
222
- for (const match of text.matchAll(/[\t\n\r\f \u00a0\u202f]+/gu)) {
223
- const start = point(items, match.index), end = point(items, match.index + match[0].length, true);
224
- if (start?.node.parentElement?.closest(exclude) || end?.node.parentElement?.closest(exclude)) continue;
225
- for (const box of boxes(items, match.index, match.index + match[0].length, height)) {
226
- if (box.width <= maximum + .1 || lines.some(line => sameLine(line.box, box))) continue;
227
- lines.push({box, end: -1});
228
- }
229
- }
230
- if (!lines.length) return {items, lines};
231
- for (const match of text.matchAll(/[^\t\n\r\f ]+/gu)) {
232
- const rects = boxes(items, match.index, match.index + match[0].length, height);
233
- if (!rects.length) continue;
234
- const last = rects[rects.length - 1];
235
- for (const line of lines) if (sameLine(line.box, last)) line.end = match.index + match[0].length;
236
- }
237
- return {items, lines};
238
- }
239
-
240
- function justify(paragraph, style) {
241
- const maximum = parseFloat(style.fontSize) * .6;
242
- if (!(maximum > 0) || style.writingMode !== 'horizontal-tb') return 0;
243
- let count = 0;
244
- // Each released line changes the native pretty-wrap solution. Use only the earliest current
245
- // line, then measure again so obsolete later endpoints cannot create isolated words.
246
- for (let pass = 0; pass < 64; pass++) {
247
- const {items, lines} = stretchedLines(paragraph, style, maximum);
248
- if (!lines.length) return count;
249
- const end = lines.map(line => line.end).filter(end => end > 0 && end < paragraph.textContent.trimEnd().length).sort((a, b) => a - b)[0];
250
- if (!end || !insertBoundary(paragraph, items, end)) break;
251
- count++;
252
- }
253
- // If no safe ordinary-word boundary can release the line, retain the natural paragraph flow.
254
- for (const node of paragraph.querySelectorAll('[' + boundary + ']')) node.replaceWith(...node.childNodes);
255
- paragraph.setAttribute(mark + '-ragged', '');
256
- return 0;
206
+ // Native clipboard HTML must see the original inline tree. Clear the selected
207
+ // projection before the browser/host captures its Range, retaining current metrics.
208
+ // The host schedules ordinary refinement again after the synchronous clipboard event.
209
+ function clearSelection(root) {
210
+ const selection=(root?.ownerDocument || root)?.getSelection?.();
211
+ if (!selection?.rangeCount || selection.isCollapsed) return 0;
212
+ const range=selection.getRangeAt(0), common=range.commonAncestorContainer;
213
+ let within=common.nodeType===1?common:common.parentElement;
214
+ if (!within || !root.contains(within)) within=root;
215
+ else within=within.closest('p['+mark+']') || within;
216
+ const selected=paragraphs(within,'p['+mark+']').filter(paragraph=>
217
+ states.has(paragraph) && root.contains(paragraph) && range.intersectsNode(paragraph));
218
+ for (const paragraph of selected) clear(paragraph,{forget:false});
219
+ return selected.length;
257
220
  }
258
221
 
259
222
  // Optical alignment uses the available margin; paint containment and narrow pages must not
@@ -282,11 +245,11 @@ export function createTypography() {
282
245
 
283
246
  function hangPunctuation(paragraph, state, style, edge) {
284
247
  if (paragraph.ownerDocument.defaultView.CSS.supports('hanging-punctuation', 'first last allow-end')) return;
285
- const text = paragraph.textContent, parts = [...graphemes.segment(text)], items = runs(paragraph);
248
+ const text = paragraph.textContent, parts = [...segment(text)], items = runs(paragraph);
286
249
  const initial = initialRange(text);
287
250
  if (initial && !paragraph.hasAttribute(protectedInitial)) {
288
251
  let end = initial.start;
289
- for (const part of graphemes.segment(initial.text)) {
252
+ for (const part of segment(initial.text)) {
290
253
  if (!opening.test(part.segment)) break;
291
254
  end = initial.start + part.index + part.segment.length;
292
255
  }
@@ -315,50 +278,321 @@ export function createTypography() {
315
278
  node.before(wrapper); wrapper.append(node);
316
279
  }
317
280
 
318
- function apply(root, {all = false} = {}) {
319
- let changed = 0, released = 0;
281
+ const shaping = ['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'fontStretch', 'fontKerning', 'fontOpticalSizing',
282
+ 'fontFeatureSettings', 'fontVariationSettings', 'fontVariant', 'fontVariantLigatures', 'fontVariantCaps',
283
+ 'fontVariantNumeric', 'fontVariantEastAsian', 'fontVariantPosition', 'fontVariantAlternates', 'fontSizeAdjust',
284
+ 'fontSynthesis', 'fontSynthesisWeight', 'fontSynthesisStyle', 'fontSynthesisSmallCaps', 'letterSpacing', 'wordSpacing',
285
+ 'textRendering', 'textTransform', 'direction', 'unicodeBidi', 'writingMode', 'verticalAlign', 'lineHeight',
286
+ 'whiteSpace', 'wordBreak', 'hyphens', 'hyphenateCharacter', 'lineBreak', 'hangingPunctuation', 'tabSize', 'textWrapStyle'];
287
+ const admission = ['display', 'position', 'cssFloat', 'transform', 'zoom', 'paddingLeft', 'paddingRight',
288
+ 'borderLeftWidth', 'borderRightWidth', 'marginLeft', 'marginRight', 'textIndent'];
289
+ const attributes = ['data-md-layout', 'data-md-align', 'data-md-lines', 'data-md-hyphens', 'data-md-caps', 'data-md-hanging', 'style', 'class', 'dir', 'lang'];
290
+ const shapeKey = style => shaping.map(name => style[name]).join('|');
291
+
292
+ function inspect(paragraph, parents, prior, baseStyle) {
293
+ const document = paragraph.ownerDocument, view = document.defaultView, styles = new Map(), keys = [captureFontAuthority(document)];
294
+ let supported = document === globalThis.document;
295
+ for (let element = paragraph.parentElement; element; element = element.parentElement) {
296
+ const s = view.getComputedStyle(element);
297
+ keys.push(s.transform, s.zoom, s.whiteSpace, s.direction, s.writingMode,
298
+ s.hyphens, s.hangingPunctuation, s.textWrapStyle, element.getAttribute('lang'));
299
+ if (s.transform !== 'none' || !['1', 'normal'].includes(s.zoom) || s.whiteSpace !== 'normal') supported = false;
300
+ }
301
+ for (const element of parents) {
302
+ const computed = element === paragraph ? baseStyle : view.getComputedStyle(element);
303
+ const s = Object.fromEntries([...shaping, ...admission].map(name => [name, computed[name]]));
304
+ // The fit's own CSS suppresses native rewrapping. Source attributes and ancestor
305
+ // whitespace remain in the key; clearing reads the current unprojected styles anew.
306
+ if (prior?.plan) for (const name of ['whiteSpace', 'hyphens', 'hangingPunctuation', 'textWrapStyle'])
307
+ s[name] = prior.saved.styles.get(element)?.[name] ?? s[name];
308
+ const before = view.getComputedStyle(element, '::before').content, after = view.getComputedStyle(element, '::after').content;
309
+ styles.set(element, s);
310
+ keys.push(shapeKey(s), ...admission.map(name => s[name]), before, after);
311
+ if (s.transform !== 'none' || !['1','normal'].includes(s.zoom) || s.cssFloat !== 'none' ||
312
+ !['static','relative'].includes(s.position) || element !== paragraph && element.tagName !== 'BR' && s.display !== 'inline' ||
313
+ !['none','normal'].includes(before) || !['none','normal'].includes(after)) supported = false;
314
+ }
315
+ keys.push(...attributes.map(name => paragraph.getAttribute(name)));
316
+ return {key: keys.join('\u0000'), generation:keys[0], styles, parents, supported};
317
+ }
318
+
319
+ function snapshot(paragraph, inspection) {
320
+ const text = paragraph.textContent, language = paragraph.closest('[lang]')?.getAttribute('lang') ?? '';
321
+ if (!inspection.supported || !text.trim() || paragraph.hasAttribute('data-md-lines') ||
322
+ paragraph.getAttribute('data-md-caps') === 'small' || paragraph.closest('[data-rapier-published-ink]') ||
323
+ /[^\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}\p{Script=Common}\p{Script=Inherited}]/u.test(text) ||
324
+ /[\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/u.test(text)) return null;
325
+ const hard = [], items = runs(paragraph, hard), atoms = [];
326
+ for (const element of inspection.parents) {
327
+ if (element !== paragraph && !/^(A|SPAN|EM|I|STRONG|B|DEL|S|MARK|CODE|KBD|SAMP|U|INS|ABBR|BR)$/.test(element.tagName)) return null;
328
+ if (element.hasAttribute('hidden') || element.getAttribute('aria-hidden') === 'true' || element.hasAttribute('data-rapier-source') ||
329
+ element.hasAttribute('lang') && element.getAttribute('lang').toLowerCase() !== language.toLowerCase() ||
330
+ ['white-space','text-wrap','hyphens','hanging-punctuation','word-spacing'].some(name => element.style.getPropertyPriority(name))) return null;
331
+ const s = inspection.styles.get(element), atom = element !== paragraph && (/^(CODE|KBD|SAMP)$/.test(element.tagName) || s.whiteSpace === 'nowrap');
332
+ if (s.hyphenateCharacter && s.hyphenateCharacter !== 'auto') return null;
333
+ if (!supportsRichStyle({...s, hyphens:'manual', hangingPunctuation:'none', whiteSpace: atom ? 'nowrap' : s.whiteSpace}, language) ||
334
+ !atom && s.whiteSpace !== 'normal') return null;
335
+ const extras = ['paddingLeft','paddingRight','borderLeftWidth','borderRightWidth','marginLeft','marginRight']
336
+ .reduce((sum,name) => sum + Math.abs(parseFloat(s[name]) || 0),0);
337
+ if (atom) {
338
+ const contained = items.filter(item => element.contains(item.node));
339
+ if (!contained.length || atoms.some(previous => previous.element.contains(element))) continue;
340
+ const start = contained[0].start, end = contained.at(-1).end;
341
+ if (/^[\t\r\n\f ]|[\t\r\n\f ]$/.test(text.slice(start,end)) || hard.some(at => at > start && at < end) ||
342
+ contained.some(item => shapeKey(inspection.styles.get(item.node.parentElement)) !== shapeKey(s))) return null;
343
+ atoms.push({element,start,end,extraWidth:extras});
344
+ } else if (element !== paragraph && extras > .01) return null;
345
+ }
346
+ const inline = [], paragraphBreaks = [0]; let hardIndex = 0;
347
+ for (const run of items) {
348
+ let broke = false;
349
+ while (hardIndex < hard.length && hard[hardIndex] <= run.start) { paragraphBreaks.push(inline.length); hardIndex++; broke = true; }
350
+ const atom = atoms.find(atom => atom.start <= run.start && run.start < atom.end);
351
+ if (atom && atom.start !== run.start) continue;
352
+ const s = inspection.styles.get(run.node.parentElement), end = atom?.end ?? run.end;
353
+ const font = s.fontStyle + ' ' + s.fontWeight + ' ' + s.fontSize + ' ' + s.fontFamily;
354
+ const letterSpacing = parseFloat(s.letterSpacing) || 0, body = text.slice(run.start,end), last = inline.at(-1);
355
+ const hyphenate = s.hyphens === 'auto' && !atom;
356
+ if (s.hyphens === 'none' && body.includes('\u00ad')) return null;
357
+ if (!broke && !atom && last && last.break !== 'never' && last.font === font && last.letterSpacing === letterSpacing && last.hyphenate === hyphenate) last.text += body;
358
+ else inline.push({text:body,font,letterSpacing,hyphenate,...(atom?{break:'never',extraWidth:atom.extraWidth}:{})});
359
+ }
360
+ while (hardIndex++ < hard.length) paragraphBreaks.push(inline.length);
361
+ paragraphBreaks.push(inline.length);
362
+ return {...inspection,text,items,atoms,inline,paragraphBreaks,language};
363
+ }
364
+
365
+ function dimensions(paragraph, style, box, hanging) {
366
+ const left = box.left + (parseFloat(style.borderLeftWidth)||0) + (parseFloat(style.paddingLeft)||0);
367
+ const right = box.right - (parseFloat(style.borderRightWidth)||0) - (parseFloat(style.paddingRight)||0);
368
+ let clipLeft = 0, clipRight = paragraph.ownerDocument.defaultView.innerWidth;
369
+ if (hanging) for (let element = paragraph; element; element = element.parentElement) {
370
+ const s = paragraph.ownerDocument.defaultView.getComputedStyle(element);
371
+ if (!/^(hidden|clip|scroll|auto)$/.test(s.overflowX) && !/(?:^|\s)(paint|content|strict)(?:\s|$)/.test(s.contain) && s.contentVisibility !== 'auto') continue;
372
+ const bounds = element.getBoundingClientRect();
373
+ clipLeft = Math.max(clipLeft,bounds.left+(parseFloat(s.borderLeftWidth)||0));
374
+ clipRight = Math.min(clipRight,bounds.right-(parseFloat(s.borderRightWidth)||0));
375
+ }
376
+ if (hanging && paragraph.ownerDocument.defaultView.matchMedia('print').matches) {
377
+ const content = paragraph.closest('.md-render')?.getBoundingClientRect();
378
+ if (content) { clipLeft = Math.max(clipLeft,content.left); clipRight = Math.min(clipRight,content.right); }
379
+ }
380
+ const width = Math.floor((right-left)*64)/64;
381
+ const indent = style.textIndent.endsWith('%') ? parseFloat(style.textIndent)*width/100 : parseFloat(style.textIndent)||0;
382
+ return {width,indent,left:hanging?Math.max(0,left-clipLeft):0,right:hanging?Math.max(0,clipRight-right):0};
383
+ }
384
+
385
+ const topology = plan => plan && plan.lines.map(line => [line.start,line.end,line.next,line.kind,line.glyph].join(':')).join('|');
386
+ function updateProjection(projection, plan) {
387
+ for (const {node,property,line,sign} of projection.bindings) {
388
+ const value = sign * plan.lines[line][property] + 'px';
389
+ const name = property === 'opening' ? 'marginInlineStart' : property === 'closing' ? 'marginInlineEnd' : 'wordSpacing';
390
+ if (node.style[name] !== value) node.style[name] = value;
391
+ }
392
+ }
393
+
394
+ function project(paragraph, saved, geometry, plan) {
395
+ if (paragraph.textContent !== saved.text) return false;
396
+ const operations = [], inserts = [], bindings = [];
397
+ for (let lineIndex = 0; lineIndex < plan.lines.length; lineIndex++) {
398
+ const line = plan.lines[lineIndex];
399
+ const words = geometry.segments[line.segment].words;
400
+ {
401
+ let start = line.start;
402
+ const add = end => { if (end > start && /[\t\r\n\f ]/.test(saved.text.slice(start,end)))
403
+ operations.push({start,end,spacing:true,line:lineIndex}); };
404
+ for (let index = line.firstWord; index <= line.lastWord; index++) {
405
+ const word = words[index];
406
+ if (!word.protected && !/[\u00a0\u202f\u2060]/.test(saved.text.slice(word.start,word.end))) continue;
407
+ add(word.start); start = word.end;
408
+ }
409
+ add(line.end);
410
+ }
411
+ if (line.kind === 'space') operations.push({start:line.end,end:line.next,boundary:true});
412
+ else if (line.kind !== 'end') inserts.push({offset:line.end,glyph:line.glyph});
413
+ const first = words[line.firstWord], last = words[line.lastWord];
414
+ if (first.opening && line.start === first.start) operations.push({start:first.start,end:first.openEnd,opening:true,line:lineIndex});
415
+ if (last.closing && line.end === last.end) operations.push({start:last.closeStart,end:last.end,closing:true,line:lineIndex});
416
+ }
417
+ // Sweep interval endpoints once. Each original text node is split from right to left;
418
+ // links, comments, IDs and inline ancestors never move or get reconstructed.
419
+ const events = new Map();
420
+ const event = offset => { let value = events.get(offset); if (!value) { value={start:[],end:[]}; events.set(offset,value); } return value; };
421
+ for (const op of operations) { event(op.start).start.push(op); event(op.end).end.push(op); }
422
+ for (const insert of inserts) event(insert.offset);
423
+ const offsets = [...events.keys()].sort((a,b)=>a-b), active = new Set(), pieces = [];
424
+ let cursor = 0;
425
+ for (const item of runs(paragraph)) {
426
+ let start = item.start;
427
+ while (start < item.end) {
428
+ while (cursor < offsets.length && offsets[cursor] <= start) {
429
+ const value = events.get(offsets[cursor++]);
430
+ for (const op of value.end) active.delete(op);
431
+ for (const op of value.start) active.add(op);
432
+ }
433
+ const end = Math.min(item.end,offsets[cursor] ?? item.end);
434
+ pieces.push({item,start,end,operations:[...active]}); start=end;
435
+ }
436
+ }
437
+ // Validate every zero-source insertion before the first write, so a refused source
438
+ // coordinate cannot leave a partially projected paragraph behind.
439
+ const sourceEnds = new Set(pieces.map(piece => piece.end));
440
+ if (inserts.some(insert => !sourceEnds.has(insert.offset))) return false;
441
+ const ends = new Map();
442
+ for (let index=pieces.length-1;index>=0;index--) {
443
+ const piece=pieces[index], {item,start,end}=piece; let node=item.node;
444
+ const from=start-item.start,to=end-item.start;
445
+ if (to<node.length) node.splitText(to);
446
+ if (from) node=node.splitText(from);
447
+ let outer=node;
448
+ if (piece.operations.length) {
449
+ const wrapper=paragraph.ownerDocument.createElement('span'); wrapper.setAttribute(gapMark,'');
450
+ for (const op of piece.operations) {
451
+ if (op.spacing) bindings.push({node:wrapper,property:'wordSpacing',line:op.line,sign:1});
452
+ if (op.boundary) {
453
+ wrapper.setAttribute(mark+'-separator','');
454
+ if (end===op.end) wrapper.setAttribute(boundary,'');
455
+ }
456
+ if (op.opening && start===op.start) {wrapper.setAttribute(hang,'start');bindings.push({node:wrapper,property:'opening',line:op.line,sign:-1});}
457
+ if (op.closing && end===op.end) {wrapper.setAttribute(hang,'end');bindings.push({node:wrapper,property:'closing',line:op.line,sign:-1});}
458
+ }
459
+ node.before(wrapper);wrapper.append(node);outer=wrapper;
460
+ }
461
+ ends.set(end,outer);
462
+ }
463
+ for (const insert of inserts) {
464
+ const at=ends.get(insert.offset);
465
+ const marker=paragraph.ownerDocument.createElement('span');marker.setAttribute(boundary,insert.glyph?'hyphen':'');
466
+ if (insert.glyph) marker.style.setProperty('--md-fit-hyphen',JSON.stringify(insert.glyph));
467
+ at.after(marker);
468
+ }
469
+ const projection = {bindings,topology:topology(plan)};
470
+ updateProjection(projection,plan);
471
+ paragraph.setAttribute(fitMark,'');return projection;
472
+ }
473
+
474
+ function apply(root, {all=false}={}) {
475
+ const started = performance.now();
476
+ const generation = captureFontAuthority(root?.ownerDocument || globalThis.document);
477
+ for (const paragraph of live) {
478
+ const state = states.get(paragraph);
479
+ if (!paragraph.isConnected || state && state.generation !== generation) clear(paragraph);
480
+ }
481
+ const jobs=[];let pending=false,changed=0,released=0,preparedCount=0;
320
482
  for (const paragraph of paragraphs(root)) {
321
483
  if (paragraph.closest('table,pre,code,h1,h2,h3,h4,h5,h6,.block-edit') ||
322
- paragraph.isContentEditable && !paragraph.closest('.block-read') ||
323
- !paragraph.isConnected || !all && paragraph.checkVisibility && !paragraph.checkVisibility({contentVisibilityAuto: true})) continue;
324
- const view = paragraph.ownerDocument.defaultView;
484
+ paragraph.isContentEditable && !paragraph.closest('.block-read')) {clear(paragraph);continue;}
485
+ if (!paragraph.isConnected || !all && paragraph.checkVisibility && !paragraph.checkVisibility({contentVisibilityAuto:true})) continue;
486
+ const view=paragraph.ownerDocument.defaultView;
325
487
  if (!view || !paragraph.getClientRects().length) continue;
326
- let style = view.getComputedStyle(paragraph);
327
- const box = paragraph.getBoundingClientRect(), width = box.width, prior = states.get(paragraph);
328
- const hanging = paragraph.getAttribute('data-md-hanging') === 'on';
329
- const edge = hanging ? hangingEdge(paragraph, style, box) : null;
330
- const geometry = hanging ? [edge, box.left, box.right].join(',') : '';
331
- if (!(width > 0) || prior?.signature === signature(paragraph, style, width, geometry)) continue;
332
- const selected = selectionBefore(paragraph);
333
- clear(paragraph);
334
- const cap = paragraph.hasAttribute('data-md-lines');
335
- const justified = paragraph.getAttribute('data-md-hyphens') === 'auto' && paragraph.getAttribute('data-md-align') === 'justify';
336
- if (!cap && !hanging && !justified) continue;
337
- const state = {properties: new Map(), moves: [], signature: ''}; states.set(paragraph, state);
338
- paragraph.setAttribute(mark, '');
339
- protectInitial(paragraph);
488
+ const style=view.getComputedStyle(paragraph),box=paragraph.getBoundingClientRect(),prior=states.get(paragraph);
489
+ if (!all && (box.bottom < -100 || box.top > view.innerHeight+100)) continue;
490
+ const hanging=paragraph.getAttribute('data-md-hanging')==='on',size=dimensions(paragraph,style,box,hanging);
491
+ if (!(size.width>0)) continue;
492
+ const parents=prior?.parents || [paragraph,...paragraph.querySelectorAll('*')];
493
+ const inspection=inspect(paragraph,parents,prior,style);
494
+ const reusable=prior?.html===paragraph.innerHTML && prior.key===inspection.key && (!prior.geometry || scope(paragraph.ownerDocument).current(prior.geometry));
495
+ const sizeKey=[size.width,size.indent,size.left,size.right].join('|');
496
+ if (reusable && !prior.pending && prior.sizeKey===sizeKey) continue;
497
+ // Preparation survives reflow; a suspended fit belongs to its captured width,
498
+ // indent and margins. Cancel only that plan when dimensions have changed.
499
+ const fitWork=reusable && prior.sizeKey===sizeKey?prior.fitWork:null;
500
+ if (prior?.fitWork && prior.fitWork!==fitWork) {prior.fitWork.return();prior.fitWork=null;}
501
+ if (!all && jobs.length===4) {if (!reusable) clear(paragraph);pending=true;continue;}
502
+ jobs.push({paragraph,view,style,inspection,size,sizeKey,hanging,selected:selectionBefore(paragraph),
503
+ cap:paragraph.hasAttribute('data-md-lines'),justified:paragraph.getAttribute('data-md-align')==='justify' && paragraph.getAttribute('data-md-hyphens')==='auto',
504
+ saved:reusable?prior.saved:null,geometry:reusable?prior.geometry:null,prepared:reusable?prior.prepared:null,
505
+ work:reusable?prior.work:null,fitWork,
506
+ prior,reusable,needsClear:paragraph.hasAttribute(mark)});
507
+ }
508
+ // Remove obsolete source/style projections before yielding. Unprepared paragraphs
509
+ // use native layout until their turn; no stale font plan remains on the page.
510
+ for (const job of jobs) if (job.needsClear && !job.saved) clear(job.paragraph);
511
+ function stateFor(job) {
512
+ return {properties:new Map(),moves:[],saved:job.saved,geometry:job.geometry,prepared:job.prepared,plan:job.plan,
513
+ work:job.work,fitWork:job.fitWork,parents:job.inspection.parents,key:job.inspection.key,generation:job.inspection.generation,sizeKey:job.sizeKey};
514
+ }
515
+ function suspend(job) {
516
+ const state=stateFor(job);
517
+ // A reusable projection may remain readable while a new width is being fitted.
518
+ // Retain its restoration custody until it is updated or explicitly cleared.
519
+ if (job.reusable && job.prior) for (const name of ['properties','moves','projection','plan']) state[name]=job.prior[name];
520
+ state.pending=true;state.html=job.paragraph.innerHTML;
521
+ states.set(job.paragraph,state);live.add(job.paragraph);job.paragraph.setAttribute(mark,'');pending=true;
522
+ }
523
+ // Both preparation and fitting yield through the same captured source/font owner.
524
+ // Native text remains readable until its plan is ready. Each continuation
525
+ // rereads the current source/style/font and width; it cannot publish a suspended plan.
526
+ // A single native shaping call can exceed this allowance, so this is not a deadline.
527
+ for (let jobIndex = 0; jobIndex < jobs.length; jobIndex++) {
528
+ if (!all && jobIndex && performance.now() - started >= 8) {pending=true;break;}
529
+ const job = jobs[jobIndex];
530
+ if (!job.saved && job.justified) {
531
+ if (job.needsClear) job.inspection=inspect(job.paragraph,[job.paragraph,...job.paragraph.querySelectorAll('*')],null,job.view.getComputedStyle(job.paragraph));
532
+ job.saved=snapshot(job.paragraph,job.inspection);
533
+ if (job.saved) {
534
+ job.work=scope(job.paragraph.ownerDocument).prepareSteps(job.paragraph,job.saved.inline,{policy:'paragraph',language:job.saved.language,
535
+ whiteSpace:'normal',wordBreak:job.saved.styles.get(job.paragraph).wordBreak,hanging:job.hanging,paragraphBreaks:job.saved.paragraphBreaks});
536
+ if (!all && performance.now()-started>=8) {suspend(job);continue;}
537
+ }
538
+ }
539
+ if (job.work) {
540
+ for (;;) {
541
+ const step=job.work.next();
542
+ if (step.done) {job.geometry=step.value;job.work=null;preparedCount++;break;}
543
+ if (!all && performance.now()-started>=8) break;
544
+ }
545
+ if (job.work || !all && performance.now()-started>=8) {suspend(job);continue;}
546
+ }
547
+ if (job.geometry && !job.prepared) job.prepared=fitter.prepare(job.geometry);
548
+ if (job.prepared && !job.fitWork) job.fitWork=scope(job.paragraph.ownerDocument).fitSteps(job.paragraph,job.geometry,
549
+ known=>fitter.solveSteps(job.prepared,job.size.width,job.size,known));
550
+ if (job.fitWork) {
551
+ for (;;) {
552
+ const step=job.fitWork.next();
553
+ if (step.done) {job.plan=step.value;job.fitWork=null;break;}
554
+ if (!all && performance.now()-started>=8) break;
555
+ }
556
+ if (job.fitWork) {suspend(job);continue;}
557
+ }
558
+ job.projection=job.reusable && job.plan && job.prior?.projection?.topology===topology(job.plan) ? job.prior.projection : null;
559
+ if (job.needsClear && job.saved && !job.projection) clear(job.paragraph,{forget:false});
560
+ const {paragraph,view,hanging,cap,justified}=job;
561
+ if (!cap && !hanging && !justified) {restoreSelection(paragraph,job.selected);continue;}
562
+ const state=stateFor(job);
563
+ states.set(paragraph,state);live.add(paragraph);paragraph.setAttribute(mark,'');
564
+ if (cap || hanging) protectInitial(paragraph);
340
565
  if (cap && !paragraph.hasAttribute(protectedInitial)) {
341
- const initial = initialRange(paragraph), letter = initial && [...graphemes.segment(initial.text)].at(-1);
342
- const at = initial && point(runs(paragraph), initial.start + letter.index);
343
- if (at?.node.parentElement) setProperty(paragraph, state, '--md-drop-cap-font', view.getComputedStyle(at.node.parentElement).fontFamily);
344
- if (initial && letter) unifyInitial(paragraph, state, initial, initial.start + letter.index);
566
+ const initial=initialRange(paragraph),letter=initial && [...segment(initial.text)].at(-1);
567
+ const at=initial && point(runs(paragraph),initial.start+letter.index);
568
+ if (at?.node.parentElement) setProperty(paragraph,state,'--md-drop-cap-font',view.getComputedStyle(at.node.parentElement).fontFamily);
569
+ if (initial && letter) unifyInitial(paragraph,state,initial,initial.start+letter.index);
345
570
  }
346
- if (hanging) hangPunctuation(paragraph, state, style, edge);
347
- style = view.getComputedStyle(paragraph);
348
- if (justified) released += justify(paragraph, style);
349
- state.signature = signature(paragraph, view.getComputedStyle(paragraph), width, geometry);
350
- restoreSelection(paragraph, selected);
351
- changed++;
571
+ if (job.projection) {updateProjection(job.projection,job.plan);state.projection=job.projection;}
572
+ else if (job.plan) state.projection=project(paragraph,job.saved,job.geometry,job.plan);
573
+ if (state.projection) released+=job.plan.lines.filter(line=>line.kind!=='end').length;
574
+ else if (hanging) hangPunctuation(paragraph,state,job.style,hangingEdge(paragraph,job.style,paragraph.getBoundingClientRect()));
575
+ state.html=paragraph.innerHTML;
576
+ restoreSelection(paragraph,job.selected);changed++;
352
577
  }
353
- return {changed, released};
578
+ return {changed,released,pending,prepared:preparedCount};
354
579
  }
355
580
 
356
- return {apply, clear, firstLineEnd, initialRange, protectInitial};
581
+ function retire(root, {documentChanged=false}={}) {
582
+ clear(root);
583
+ for (const paragraph of live) if (!paragraph.isConnected) clear(paragraph);
584
+ scope(root?.ownerDocument || root || globalThis.document).retire({documentChanged});
585
+ }
586
+ const invalidateFonts = document => scope(document || globalThis.document).invalidateFonts();
587
+ return {apply,clear,clearSelection,firstLineEnd,initialRange,protectInitial,invalidateFonts,retire};
357
588
  }
358
589
 
359
590
  const typography = createTypography();
360
591
  export const apply = typography.apply;
361
592
  export const clear = typography.clear;
593
+ export const clearSelection = typography.clearSelection;
362
594
  export const firstLineEnd = typography.firstLineEnd;
363
595
  export const initialRange = typography.initialRange;
364
596
  export const protectInitial = typography.protectInitial;
597
+ export const invalidateFonts = typography.invalidateFonts;
598
+ export const retire = typography.retire;