rapier-markdown-kit 1.1.104 → 1.2.5

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 +8 -4
  2. package/README.md +50 -7
  3. package/dist/images/gif.mjs +49 -0
  4. package/dist/images/playback.mjs +103 -0
  5. package/dist/kit/conformance/03-wrap-around-silhouette.md +1 -1
  6. package/dist/kit/conformance/04-wrap-box.md +1 -1
  7. package/dist/kit/conformance/05-behind.md +1 -1
  8. package/dist/kit/conformance/06-front.md +1 -1
  9. package/dist/kit/conformance/07-rotate-raster.md +1 -1
  10. package/dist/kit/conformance/08-drawing-rotation.md +1 -1
  11. package/dist/kit/conformance/09-drawing-ring-interior.expected.json +18 -18
  12. package/dist/kit/conformance/09-drawing-ring-interior.md +1 -1
  13. package/dist/kit/conformance/10-both-sides.md +1 -1
  14. package/dist/kit/conformance/11-neighbour-skips-image.md +1 -1
  15. package/dist/kit/conformance/{12-heading-barrier.expected.json → 12-rule-barrier.expected.json} +1 -1
  16. package/dist/kit/conformance/{12-heading-barrier.md → 12-rule-barrier.md} +2 -2
  17. package/dist/kit/conformance/README.md +19 -28
  18. package/dist/kit/conformance/run.mjs +10 -8
  19. package/dist/kit/ledger/authored-origin.mjs +49 -0
  20. package/dist/kit/ledger/authorship.mjs +18 -9
  21. package/dist/kit/ledger/carried.mjs +10 -5
  22. package/dist/kit/ledger/data.mjs +7 -4
  23. package/dist/kit/ledger/format.mjs +37 -11
  24. package/dist/kit/ledger/hash.mjs +31 -15
  25. package/dist/kit/ledger/history.mjs +48 -10
  26. package/dist/kit/ledger/inverse-source.mjs +111 -49
  27. package/dist/kit/ledger/journal-records.mjs +24 -29
  28. package/dist/kit/ledger/merge.mjs +51 -15
  29. package/dist/kit/ledger/text-merge.mjs +75 -29
  30. package/dist/kit/ledger/transport.mjs +30 -45
  31. package/dist/kit/pdf-vectors.mjs +6 -2
  32. package/dist/kit/print-capitals.mjs +0 -1
  33. package/dist/kit/rapier-ledger.mjs +4 -2
  34. package/dist/kit/render-ink.mjs +1 -1
  35. package/dist/kit/render-markdown.mjs +155 -31
  36. package/dist/kit/render-print.mjs +173 -61
  37. package/dist/kit/render-sanitize.mjs +25 -15
  38. package/dist/kit/render-styles.mjs +3 -3
  39. package/dist/kit/render.mjs +37 -115
  40. package/dist/layout/model.mjs +10 -8
  41. package/dist/layout/paragraph-fit.mjs +1 -15
  42. package/dist/layout/typography.mjs +2 -1
  43. package/dist/packages/rapier-diff/concerns.mjs +163 -0
  44. package/dist/packages/rapier-diff/diff.mjs +68 -0
  45. package/dist/packages/rapier-diff/index.mjs +4 -701
  46. package/dist/packages/rapier-diff/matching.mjs +157 -0
  47. package/dist/packages/rapier-diff/merge.mjs +226 -0
  48. package/dist/packages/rapier-diff/moves.mjs +130 -0
  49. package/dist/packages/rapier-diff/structure.mjs +50 -0
  50. package/dist/packages/rapier-diff/tokens.mjs +72 -0
  51. package/dist/packages/rapier-diff/walk.mjs +219 -0
  52. package/dist/spec/document-fonts.mjs +1 -1
  53. package/dist/spec/document-settings.mjs +11 -164
  54. package/dist/spec/html-blocks.mjs +618 -79
  55. package/dist/spec/md-assets.mjs +29 -6
  56. package/dist/spec/md-layout.mjs +9 -1
  57. package/dist/spec/md-marks.mjs +25 -147
  58. package/dist/spec/md-source.mjs +69 -26
  59. package/package.json +1 -1
  60. package/style.css +7 -51
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {htmlImageRecord, mountHtmlBlocks} from '../spec/html-blocks.mjs';
2
+ import {htmlImageRecord, htmlRuntimeSource} from '../spec/html-blocks.mjs';
3
3
  import {finish, finishAsync, cloneTree, cleanTree, serializeTree, encodeUtf8Steps, pause} from './render-work.mjs';
4
4
  import {fontFamilyCss} from '../spec/md-marks.mjs';
5
5
  import {readFontAssets, documentFontsCss} from '../spec/document-fonts.mjs';
@@ -8,43 +8,6 @@ import {documentLanguage, documentSettingsFonts} from '../spec/document-settings
8
8
  import {createLineMeasurer, linesHeight} from '../layout/model.mjs';
9
9
  import {markPrintCapitals, createPrintCapitals, followingPrintProse} from './print-capitals.mjs';
10
10
  import {freezePublishedInk} from './render-ink.mjs';
11
- // The document's renderer. DOM, codecs and host state are explicit inputs.
12
- // Rapier generates attribute selectors for dark paper. Project those owned rules onto their
13
- // SVG elements before export removes stylesheets; the inert host and the editor share this pass.
14
- export function presentNativeDiagram(drawing) {
15
- const winners=new Map();
16
- for(const style of drawing.querySelectorAll('style')){
17
- const media=/^\s*@media\s*\(\s*prefers-color-scheme\s*:\s*dark\s*\)\s*\{([\s\S]*)\}\s*$/.exec(style.textContent);
18
- if(media)for(const rule of media[1].matchAll(/([^{}]+)\{([^{}]*)\}/g)){
19
- // The browser's CSS sanitizer serializes owned hex colours as rgb()/rgba().
20
- const properties=[...rule[2].matchAll(/(?:^|;)\s*(fill|stroke|color|stop-color)\s*:\s*(#[\da-f]{3,8}|rgba?\([\d.,%/\s]+\)|none)\s*(?:!important)?(?=;|$)/gi)];
21
- for(const selector of rule[1].split(',')){
22
- // Draw's selectors contain only attributes, descendants and immediate children.
23
- if(!/^\s*\[[^\[\]{};]+\](?:\s*(?:>\s*)?\[[^\[\]{};]+\])*\s*$/.test(selector))continue;
24
- const rank=(selector.match(/\[/g)||[]).length;
25
- for(const element of [...(drawing.matches(selector)?[drawing]:[]),...drawing.querySelectorAll(selector)]){
26
- let paints=winners.get(element);if(!paints)winners.set(element,paints=new Map());
27
- for(const [,property,dark] of properties){const light=element.getAttribute(property);
28
- if(light&&(paints.get(property)?.rank??-1)<=rank)paints.set(property,{light,dark,rank});}
29
- }
30
- }
31
- }
32
- style.remove();
33
- }
34
- for(const [element,paints] of winners)for(const [property,{light,dark}] of paints)
35
- element.style.setProperty(property,'light-dark('+light+', '+dark+')','important');
36
- drawing.classList.add('rapier-native-flowchart');drawing.setAttribute('role','img');
37
- drawing.setAttribute('aria-roledescription',drawing.getAttribute('data-rapier-diagram-kind')||'flowchart');
38
- const width=Number(drawing.getAttribute('width')),
39
- sizes=[...drawing.querySelectorAll('text[font-size]')].map(text=>Number(text.getAttribute('font-size'))).filter(size=>Number.isFinite(size)&&size>0),
40
- minimum=sizes.reduce((smallest,size)=>Math.min(smallest,size),Infinity);
41
- if(Number.isFinite(width)&&width>0){
42
- const readable=Math.ceil(width*14/(Number.isFinite(minimum)?minimum:14)*1000)/1000;
43
- drawing.style.width='clamp('+readable+'px, 100%, '+width+'px)';
44
- }
45
- drawing.style.maxWidth='none';
46
- }
47
-
48
11
  // Set the first view once. Subsequent rendering keeps the person's horizontal position.
49
12
  export function positionNativeDiagram(drawing) {
50
13
  const surface=drawing?.closest?.('.diagram-block,.diagram-live-preview');
@@ -67,8 +30,9 @@ export function positionNativeDiagram(drawing) {
67
30
  surface.scrollLeft=Math.max(0,Math.min(surface.scrollWidth-surface.clientWidth,left));
68
31
  }
69
32
 
33
+ // The document's renderer. DOM, codecs and host state are explicit inputs.
70
34
  function createRenderer(runtime) {
71
- const {RAPIER_COLOR_CLOSE, RapierLedgerCarried, RapierPageReturnAddress, RapierTextCodec, _rapierArtifactHighlight, _rapierArtifactPreference, _rapierArtifactStyles, _rapierBlobDataUrl, _rapierBuildInterchangeContext, _rapierDocumentNameIsAdmissible, _rapierLedgerParts, _rapierDrawReadSVGRecipe, _rapierDrawShapeProfileFor, _rapierExportFontCss, _rapierFillDiagram, _rapierFinalizeExportCode, _rapierFormatColorOpen, _rapierLanguageClass, _rapierLineStartOffsets, _rapierMarkdownEnvironment, _rapierPortableHtml, _rapierPrepareInterchangeContext, _rapierProjectPortableRoot, _rapierProviders, _rapierSourceCharEscaped, _rapierSourceLineSpan, crypto, document, escapeRapierHtmlText, globalThis, rapierConfirm, sanitizeRapierHtml} = runtime;
35
+ const {RapierLedgerCarried, RapierPageReturnAddress, RapierTextCodec, _rapierArtifactHighlight, _rapierArtifactPreference, _rapierArtifactStyles, _rapierBlobDataUrl, _rapierBuildInterchangeContext, _rapierDocumentNameIsAdmissible, _rapierLedgerParts, _rapierDrawReadSVGRecipe, _rapierDrawShapeProfileFor, _rapierExportFontCss, _rapierFillDiagram, _rapierFinalizeExportCode, _rapierLanguageClass, _rapierLineStartOffsets, _rapierMarkdownEnvironment, _rapierPortableHtml, _rapierPrepareInterchangeContext, _rapierProjectPortableRoot, _rapierProviders, _rapierSourceCharEscaped, _rapierSourceLineSpan, crypto, document, escapeRapierHtmlText, globalThis, sanitizeRapierHtml} = runtime;
72
36
  let md = runtime.md;
73
37
  async function render(source, options = {}) {
74
38
  if (typeof source !== 'string') throw new TypeError('Document source must be a string');
@@ -157,14 +121,16 @@ async function _rapierBuildArtifact(options, providedContext) {
157
121
  styledRoot.setAttribute('lang', language);
158
122
  if (completePage && typeof _rapierFinalizeExportCode === 'function') await _rapierFinalizeExportCode(styledRoot);
159
123
  // _rapierProjectStyledRoot already returns the sanitized projection.
160
- // Print runs shared paragraph typography, but keeps its existing picture layout;
161
- // picture-shape annotations have no consumer in that artifact.
124
+ // Picture-shape annotations have no consumer in a printed artifact.
162
125
  if (!opts.print) _rapierAnnotateExportBoxPolygons(styledRoot);
126
+ // Paper keeps each picture's wrap through the same static float layout a page uses.
127
+ if (completePage || opts.print) {
128
+ if (work) await finishAsync(_rapierSharedPageLayoutSteps(styledRoot), work);
129
+ else _rapierSharedPageLayout(styledRoot);
130
+ }
163
131
  if (completePage && !opts.print) {
164
- if (work) {
165
- await finishAsync(_rapierSharedPageLayoutSteps(styledRoot), work);
166
- await finishAsync(_rapierShareCapLongCodeBlocksSteps(styledRoot), work);
167
- } else { _rapierSharedPageLayout(styledRoot); _rapierShareCapLongCodeBlocks(styledRoot); }
132
+ if (work) await finishAsync(_rapierShareCapLongCodeBlocksSteps(styledRoot), work);
133
+ else _rapierShareCapLongCodeBlocks(styledRoot);
168
134
  }
169
135
  if (typeof opts.afterRoot === 'function') await opts.afterRoot(styledRoot);
170
136
  markPrintCapitals(styledRoot);
@@ -185,8 +151,6 @@ async function _rapierBuildArtifact(options, providedContext) {
185
151
  // rules of its own -- Share, for the no-script float it alone writes -- appends them.
186
152
  // Print retains its existing factory resources. HTML adds only the optional small Sans subset below.
187
153
  const includeFonts = !!opts.print;
188
- const zenFontCss = opts.print && styledRoot.querySelector('svg[aria-roledescription="zenuml"]')
189
- ? (_rapierProviders?.mermaid?.exportFontCss?.() || '') : '';
190
154
  const willFont = opts.print && opts.willFont ? opts.willFont : null;
191
155
  const settingsCss = _rapierDocumentSettingsCss(settings);
192
156
  const refined = styledRoot.querySelector('p[data-md-hyphens="auto"],p[data-md-lines],p[data-md-caps="small"],p[data-md-hanging="on"],img[data-rapier-print-lines]');
@@ -227,7 +191,7 @@ async function _rapierBuildArtifact(options, providedContext) {
227
191
  // nonce out of it and be admitted.
228
192
  const layoutScript = opts.scripts !== false ? _rapierArtifactLayoutScript(styledRoot, nonce, {pictures:!opts.print}) : '';
229
193
  const inkScript = !opts.print && opts.scripts !== false ? _rapierArtifactInkScript(styledRoot, nonce) : '';
230
- const htmlScript = opts.scripts !== false && styledRoot.querySelector('[data-rapier-html]') ? '<script nonce="' + nonce + '">(' + mountHtmlBlocks.toString().replace(/<\/script/gi, '<\\/script') + ')(document);</script>' : '';
194
+ const htmlScript = !opts.print && opts.scripts !== false && styledRoot.querySelector('[data-rapier-html],[data-rapier-animation]') ? '<script nonce="' + nonce + '">' + htmlRuntimeSource().replace(/<\/script/gi, '<\\/script') + '</script>' : '';
231
195
  const page = '<!DOCTYPE html>\n'
232
196
  + '<html lang="' + escapeRapierHtmlText(language) + '" data-rapier-theme="' + theme + '" data-highlights="' + highlights + '" data-invert-image-alpha="' + (invertImageAlpha ? 'on' : 'off') + '">\n'
233
197
  + '<head>\n<meta charset="UTF-8">\n' + (carrier ? RapierPageReturnAddress.PAGE_SEED + '\n' : '')
@@ -243,7 +207,7 @@ async function _rapierBuildArtifact(options, providedContext) {
243
207
  + '<meta name="generator" content="Rapier ' + escapeRapierHtmlText(version) + '">\n'
244
208
  + '<title>' + escapeRapierHtmlText(pageTitle) + '</title>\n'
245
209
  + '<style>' + css + '</style>\n</head>\n'
246
- + '<body class="' + bodyClass + '"><main class="' + docClass + '" data-md-theme="' + theme + '"' + (settings?.space ? ' data-md-space' : '') + '>'
210
+ + '<body class="' + bodyClass + '"><main class="' + docClass + '" data-md-theme="' + theme + '">'
247
211
  + bodyHtml + '</main>' + carrier + layoutScript + inkScript + htmlScript + '</body>\n</html>';
248
212
 
249
213
  return {
@@ -301,49 +265,6 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
301
265
  if (lang) code.classList.add('language-' + String(lang).replace(/[^a-z0-9_-]/gi, ''));
302
266
  }
303
267
  } else {
304
- if (root.hasAttribute('data-md-space')) {
305
- // One raw HTML token may have several siblings. Its authored heights stay inside one line-sized box.
306
- for (let element = root.firstElementChild; element;) {
307
- const group = element.getAttribute('data-md-html-block');
308
- if (group === null) { element = element.nextElementSibling; continue; }
309
- let first = element;
310
- // Sanitizing a container or repairing a table can leave its text before the first surviving tag.
311
- while (first.previousSibling && first.previousSibling.nodeType !== 1) { first = first.previousSibling; yield; }
312
- const wrap = document.createElement('div');
313
- wrap.className = 'md-space-block';
314
- root.insertBefore(wrap, first);
315
- let node = first;
316
- while (node && (node.nodeType !== 1 || node.getAttribute('data-md-html-block') === group)) {
317
- const next = node.nextSibling;
318
- wrap.appendChild(node);
319
- node = next;
320
- yield;
321
- }
322
- element = wrap.nextElementSibling;
323
- }
324
- // A removed raw container can leave only text. Reserve that surviving content in this projection.
325
- for (let first = root.firstChild; first;) {
326
- if (first.nodeType === 1) { first = first.nextSibling; continue; }
327
- let end = first, hasText = false;
328
- while (end && end.nodeType !== 1) {
329
- if (end.nodeType === 3 && end.nodeValue.trim()) hasText = true;
330
- end = end.nextSibling;
331
- yield;
332
- }
333
- if (hasText) {
334
- const wrap = document.createElement('div');
335
- wrap.className = 'md-space-block';
336
- root.insertBefore(wrap, first);
337
- for (let node = first; node !== end;) {
338
- const next = node.nextSibling;
339
- wrap.appendChild(node);
340
- node = next;
341
- yield;
342
- }
343
- }
344
- first = end;
345
- }
346
- }
347
268
  for (const code of root.querySelectorAll('pre > code')) { yield;
348
269
  if (code.parentElement?.classList.contains('diagram-source') &&
349
270
  code.closest('.diagram-block')?.getAttribute('data-diagram-state') === 'ready') continue;
@@ -465,11 +386,15 @@ function _rapierPaintCode(el, html) {
465
386
 
466
387
  function _rapierPrefixPortableAnchors(root, baseName) { return finish(_rapierPrefixPortableAnchorsSteps(root, baseName)); }
467
388
 
468
- function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
389
+ // The id an anchor carries on a portable page: the document's name, then the anchor with what an id cannot hold made a hyphen.
390
+ function _rapierPortableAnchorId(baseName, id) {
469
391
  const slug = String(baseName || 'document').toLowerCase()
470
392
  .replace(/[^a-z0-9]+/g, '-')
471
393
  .replace(/^-+|-+$/g, '') || 'document';
472
- const prefix = 'rapier-' + slug + '-';
394
+ return 'rapier-' + slug + '-' + (String(id).replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '') || 'anchor');
395
+ }
396
+
397
+ function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
473
398
  const ids = new Map(), svgIds = new Map();
474
399
  const used = new Set();
475
400
  const scopeOf = element => {
@@ -486,11 +411,10 @@ function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
486
411
  for (const element of root.querySelectorAll('[id]')) { yield;
487
412
  const oldId = element.id;
488
413
  if (!oldId) continue;
489
- const base = oldId.replace(/[^a-z0-9_-]+/gi, '-')
490
- .replace(/^-+|-+$/g, '') || 'anchor';
491
- let nextId = prefix + base;
414
+ const first = _rapierPortableAnchorId(baseName, oldId);
415
+ let nextId = first;
492
416
  let suffix = 2;
493
- while (used.has(nextId)) nextId = prefix + base + '-' + suffix++;
417
+ while (used.has(nextId)) nextId = first + '-' + suffix++;
494
418
  used.add(nextId);
495
419
  // A document link resolves the first matching anchor, as it did before renaming.
496
420
  // SVG references resolve within their own drawing, never the last repeated drawing.
@@ -693,16 +617,18 @@ function _rapierSharedResolve(text, ids, images, definitions) {
693
617
  let broken = false;
694
618
  const src = value => {
695
619
  const found = images.get(value);
696
- if (typeof found !== 'string' || !/^data:image\/(?:png|jpeg|webp|svg\+xml|jxl);base64,[A-Za-z0-9+/]+={0,2}$/i.test(found)) { broken = true; return null; }
620
+ if (typeof found !== 'string' || !/^data:image\/(?:png|jpeg|webp|gif|svg\+xml|jxl);base64,[A-Za-z0-9+/]+={0,2}$/i.test(found)) { broken = true; return null; }
697
621
  return found;
698
622
  };
699
623
  // Authored # characters were encoded. A raw id after ]( can only be a structural
700
624
  // substitution; matching its destination lets nested alt brackets survive unchanged.
701
625
  const inline = new RegExp('(\\]\\([ \\t\\r\\n]*(?:&lt;)?)#' + id + '(?=>?[ \\t\\r\\n]*\\)|>?[ \\t\\r\\n]+["\'(])', 'g');
702
- const definition = new RegExp('(^ {0,3}\\[((?:\\\\.|[^\\]\\\\])+)\\]:[ \\t]*(?:(?:\\r\\n|\\r|\\n)[ \\t]*)?(?:&lt;)?)#' + id + '(?=>?[ \\t]*$|>?[ \\t]+["\'(])', 'gm');
626
+ // A definition may sit in a quote or a list item, as the writer found it: its line starts with their markers.
627
+ const definition = new RegExp('(^[ \\t>]*(?:(?:[-+*]|\\d{1,9}[.)])[ \\t]+[ \\t>]*)*\\[((?:\\\\.|[^\\]\\\\])+)\\]:[ \\t]*(?:(?:\\r\\n|\\r|\\n)[ \\t>]*)?(?:&lt;)?)#' + id + '(?=>?[ \\t]*$|>?[ \\t]+["\'(])', 'gm');
703
628
  const swapInline = (match, lead, value) => { const found = src(value); return found == null ? match : lead + found; };
704
629
  const swapDefinition = (match, lead, label, value) => {
705
- if (!definitions.has(md.utils.normalizeReference(_rapierSharedDecode(label)))) return match;
630
+ const decoded = _rapierSharedDecode(label), quoted = decoded.replace(/\r?\n[ \t]*(?:>[ \t]*)+/g, ' ');
631
+ if (!definitions.has(md.utils.normalizeReference(decoded)) && !definitions.has(md.utils.normalizeReference(quoted))) return match;
706
632
  const found = src(value);
707
633
  return found == null ? match : lead + found;
708
634
  };
@@ -877,7 +803,7 @@ function* _rapierSharedPageLayoutSteps(root) {
877
803
  // `layout.y` already owns `shape-outside` for its own inset reservation (below); the two are not
878
804
  // combinable (a float has exactly one `shape-outside` value), so a `y`-offset `around` picture
879
805
  // keeps that inset instead of silhouette wrapping -- a documented, narrow degradation.
880
- if (layout.wrap === 'around' && !layout.rotate && !layout.y) {
806
+ if (layout.wrap === 'around' && !layout.rotate && !layout.y && !image.dataset.rapierContour?.endsWith('-box')) {
881
807
  const src = image.getAttribute('src');
882
808
  if (src) {
883
809
  paragraph.style.setProperty('shape-outside', 'url("' + src + '")');
@@ -905,16 +831,12 @@ function* _rapierSharedPageLayoutSteps(root) {
905
831
  for (const {image, paragraph, layout} of positionedAssignments) {
906
832
  yield;
907
833
  paragraph.style.cssText = 'position:relative;height:0;margin:0;padding:0;line-height:0';
908
- const x = layout.x ?? 50, naturalWidth = Number(image.getAttribute('width')) || 0;
909
- if (layout.width != null) {
910
- image.style.width = layout.width + '%';
911
- image.style.left = (x - layout.width / 2) + '%';
912
- } else if (naturalWidth > 0) {
913
- image.style.width = naturalWidth + 'px';
914
- image.style.left = 'calc(' + x + '% - ' + (naturalWidth / 2) + 'px)';
915
- } else {
916
- image.style.left = x + '%';
917
- }
834
+ // Without x the picture starts at the left edge, as everywhere else (layout/model.mjs imageBox).
835
+ const x = layout.x, naturalWidth = Number(image.getAttribute('width')) || 0;
836
+ if (layout.width != null) image.style.width = layout.width + '%';
837
+ else if (naturalWidth > 0) image.style.width = naturalWidth + 'px';
838
+ image.style.left = x == null ? '0' : layout.width != null ? (x - layout.width / 2) + '%'
839
+ : naturalWidth > 0 ? 'calc(' + x + '% - ' + (naturalWidth / 2) + 'px)' : x + '%';
918
840
  image.style.position = 'absolute';
919
841
  image.style.top = (layout.y || 0) + 'em';
920
842
  image.style.margin = '0';
@@ -1567,6 +1489,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1567
1489
  // wrap=around reads data-rapier-occupancy, else alpha. Cache key: src, dimensions, layout.wrap, rotate angle.
1568
1490
  // A turned raster's profile is recomputed here as pictureProfile does, never shipped.
1569
1491
  function profile(image, layout) {
1492
+ if (layout.wrap === 'around' && image.dataset?.rapierContour?.endsWith('-box')) return null;
1570
1493
  const src = image.currentSrc || image.getAttribute('src') || '', rotateRad = (layout.rotate || 0) * Math.PI / 180;
1571
1494
  const cached = profiles.get(image);
1572
1495
  if (cached?.src === src && cached.width === image.naturalWidth && cached.height === image.naturalHeight &&
@@ -1780,8 +1703,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1780
1703
  if (image.hasAttribute('data-rapier-html')) {
1781
1704
  const attrs = JSON.parse(decodeURIComponent(image.getAttribute('data-rapier-html'))).attrs;
1782
1705
  const line = parseFloat(computed.lineHeight) || (parseFloat(computed.fontSize) || 16) * 1.6;
1783
- unrotated.height = !attrs.height || attrs.height === 'auto'
1784
- ? Number(image.dataset.rapierNaturalHeight) || box(image).height : Number(attrs.height) * line;
1706
+ unrotated.height = metadata.objectHeight(unrotated.width, Number(image.dataset.rapierNaturalHeight) || box(image).height, attrs, line);
1785
1707
  }
1786
1708
  // rasterReserved (layout/model.mjs).
1787
1709
  const rasterRad = (layout.rotate || 0) * Math.PI / 180;
@@ -1936,6 +1858,6 @@ function _rapierArtifactTypographyScript(root) {
1936
1858
  '})();';
1937
1859
  }
1938
1860
 
1939
- return {_rapierDataImageDestinationsAsync, _rapierSharedSourceFormsAsync, _rapierPrefixPortableAnchorsSteps, _rapierProjectStyledRootAsync, _rapierBuildArtifact, _rapierBuildPublishedPage, _rapierProjectStyledRoot, _rapierDocxRenderStyles, _rapierPaintCode, _rapierPrefixPortableAnchors, _rapierDocumentSettingsOf, _rapierDocumentSettingsCss, _rapierExportedPageCsp, _rapierArtifactNonce, _rapierAnnotateExportBoxPolygons, _rapierSharedSourceHash, _rapierSharedSourceForms, _rapierSharedSourceCarrier, _rapierSharedResolve, _rapierReadSharedDocument, _rapierSharedPageFallbackCss, _rapierShareWrapKind, _rapierSharePictureParagraph, _rapierSharedPageLayout, _rapierShareCapLongCodeBlocks, _rapierRequireOfflinePageImages, _rapierCssValueUrls, _rapierBuildSharedPage, _rapierImageSources, _rapierImageDestinations, _rapierScanMarkdownImages, _rapierParseMarkdownImageDestination, _rapierImageOpenRunsList, _rapierArtifactFactoryModules, _rapierProjectArtifactLayout, _rapierArtifactLayoutScript, _rapierArtifactInkScript , render};
1861
+ return {_rapierDataImageDestinationsAsync, _rapierSharedSourceFormsAsync, _rapierPrefixPortableAnchorsSteps, _rapierPortableAnchorId, _rapierProjectStyledRootAsync, _rapierBuildArtifact, _rapierBuildPublishedPage, _rapierProjectStyledRoot, _rapierDocxRenderStyles, _rapierPaintCode, _rapierPrefixPortableAnchors, _rapierDocumentSettingsOf, _rapierDocumentSettingsCss, _rapierExportedPageCsp, _rapierArtifactNonce, _rapierAnnotateExportBoxPolygons, _rapierSharedSourceHash, _rapierSharedSourceForms, _rapierSharedSourceCarrier, _rapierSharedResolve, _rapierReadSharedDocument, _rapierSharedPageFallbackCss, _rapierShareWrapKind, _rapierSharePictureParagraph, _rapierSharedPageLayout, _rapierShareCapLongCodeBlocks, _rapierRequireOfflinePageImages, _rapierCssValueUrls, _rapierBuildSharedPage, _rapierImageSources, _rapierImageDestinations, _rapierScanMarkdownImages, _rapierParseMarkdownImageDestination, _rapierImageOpenRunsList, _rapierArtifactFactoryModules, _rapierProjectArtifactLayout, _rapierArtifactLayoutScript, _rapierArtifactInkScript , render};
1940
1862
  }
1941
1863
  export {createRenderer};
@@ -251,13 +251,11 @@ export function bandsProfile(bands) {
251
251
  export function serializeProfile(profile, count = shapeGrid) {
252
252
  if (!profile) return '';
253
253
  const bands = [];
254
- let any = false;
255
254
  for (let index = 0; index < count; index++) {
256
255
  const t = (index + .5) / count;
257
256
  let runs = profile.runsAt ? profile.runsAt(t) : null;
258
257
  if (!runs) { const span = profile.spanAt ? profile.spanAt(t) : null; runs = span ? [span] : []; }
259
258
  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);
260
- if (clean.length) any = true;
261
259
  bands.push(clean.map(([a, b]) => {
262
260
  let left = Math.round(a * 1e4), right = Math.round(b * 1e4);
263
261
  // A narrow occupied run must not round to an invalid zero-width descriptor.
@@ -265,13 +263,13 @@ export function serializeProfile(profile, count = shapeGrid) {
265
263
  return (left / 1e4) + '-' + (right / 1e4);
266
264
  }).join(','));
267
265
  }
268
- return any ? bands.join('|') : '';
266
+ return bands.join('|');
269
267
  }
270
268
 
271
269
  export function parseProfile(text) {
272
- if (typeof text !== 'string' || !text) return null;
270
+ if (typeof text !== 'string' || !text || text.length > 262144) return null;
273
271
  const bands = text.split('|').map(band => band ? band.split(',').map(run => run.split('-').map(Number)) : []);
274
- if (bands.length < 2 || bands.some(band => band.some(run => run.length !== 2 || !run.every(Number.isFinite) || run[0] < 0 || run[1] > 1 || run[1] <= run[0]))) return null;
272
+ if (bands.length < 2 || bands.length > 192 || bands.some(band => band.length > 192 || band.some(run => run.length !== 2 || !run.every(Number.isFinite) || run[0] < 0 || run[1] > 1 || run[1] <= run[0]))) return null;
275
273
  return bandsProfile(bands);
276
274
  }
277
275
 
@@ -309,9 +307,13 @@ export function pictureSlices(profile, x, y, width, height, scale = 1) {
309
307
  return span ? [span] : [];
310
308
  }).filter(run => Number.isFinite(run[0]) && Number.isFinite(run[1]) && run[1] > run[0])
311
309
  .sort((a, b) => a[0] - b[0]);
312
- // Words wrap the outer contour: a band reserves from its leftmost ink to its rightmost, so no word lands
313
- // inside a hollow outline or between two strokes of one picture.
314
- const merged = runs.length ? [[runs[0][0], Math.max(...runs.map(run => run[1]))]] : [];
310
+ // Preserve transparent holes and gaps; only overlapping runs share an obstacle.
311
+ const merged = [];
312
+ for (const run of runs) {
313
+ const tail = merged.at(-1);
314
+ if (tail && run[0] <= tail[1]) tail[1] = Math.max(tail[1], run[1]);
315
+ else merged.push([...run]);
316
+ }
315
317
  const current = new Map();
316
318
  for (const [left, right] of merged) {
317
319
  const key = left + ':' + right;
@@ -172,20 +172,6 @@ export function createParagraphFitter() {
172
172
  return {lines, work, cost: totalCost, approximate: false};
173
173
  }
174
174
 
175
- function solve(prepared, width, options, observe) {
176
- const work = solveSteps(prepared, width, options);
177
- try {
178
- let step = work.next();
179
- while (!step.done) {
180
- if (step.value && !observe) return null;
181
- step = work.next(step.value ? observe(step.value) : undefined);
182
- }
183
- return step.value;
184
- } finally { work.return(); }
185
- }
186
- return {prepare, solve, solveSteps};
175
+ return {prepare, solveSteps};
187
176
  }
188
177
 
189
- const fitter = createParagraphFitter();
190
- export const prepareParagraph = fitter.prepare;
191
- export const fitParagraph = fitter.solve;
@@ -3,7 +3,8 @@ import {createPreparationScope, supportsRichStyle, captureFontAuthority} from '.
3
3
  import {createParagraphFitter} from './paragraph-fit.mjs';
4
4
 
5
5
  // Layout projections keep the original text in place. The factory also runs in an exported page.
6
- export function createTypography(fitter = createParagraphFitter()) {
6
+ export function createTypography() {
7
+ const fitter = createParagraphFitter();
7
8
  const mark = 'data-rapier-typography', boundary = mark + '-break', hang = mark + '-hang';
8
9
  const protectedInitial = mark + '-protected-initial';
9
10
  const gapMark = mark + '-gap', fitMark = mark + '-fit';
@@ -0,0 +1,163 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import {letter, within, prev, next} from './tokens.mjs';
3
+
4
+ // Separator-only pieces join the edit beside them; adjacent blocks each carry their own line break.
5
+ export function separate(c, endA = c.before.length, endB = c.after.length) {
6
+ const {before, after, safe, raw, spend} = c;
7
+ // A separator-only piece beside another edit joins it; equal separators at its ends stay unchanged.
8
+ if (!raw) for (let i = c.result.length - 1; i > 0; i--) {
9
+ const p = c.result[i - 1], q = c.result[i], side = p[0][0] === p[0][1] ? 1 : p[1][0] === p[1][1] ? 0 : -1, text = side ? after : before;
10
+ if (side >= 0 && !/\S/.test(text.slice(...p[side]))) {
11
+ let n = 0;
12
+ while (p[0][1] + n < q[0][0] && text[p[side][0] + n] === text[p[side][1] + n] && spend()) n++;
13
+ if (p[0][1] + n === q[0][0] && p.slice(0, 2).every((span, k) => safe(k ? after : before, span[0] + n) && safe(k ? after : before, span[1] + n)))
14
+ for (const span of p.slice(0, 2)) { span[0] += n; span[1] += n; }
15
+ }
16
+ if (p[2] || q[2] || p[0][1] !== q[0][0] || p[1][1] !== q[1][0] ||
17
+ ![p, q].some(r => !/\S/.test(before.slice(...r[0]) + after.slice(...r[1])))) continue;
18
+ const x = [p[0][0], q[0][1]], y = [p[1][0], q[1][1]], keep = /\S/.test(before.slice(...q[0]) + after.slice(...q[1])) ? q : p;
19
+ // CRLF steps as one separator.
20
+ for (let k; x[0] < x[1] && y[0] < y[1] && /\s/.test(before[x[1] - 1]) && before[x[1] - 1] === after[y[1] - 1] &&
21
+ (k = before.endsWith('\r\n', x[1]) && after.endsWith('\r\n', y[1]) ? 2 : 1) <= Math.min(x[1] - x[0], y[1] - y[0]) &&
22
+ safe(before, x[1] - k) && safe(after, y[1] - k);) { x[1] -= k; y[1] -= k; }
23
+ keep[3][0] = x[0] === x[1] ? 2 : y[0] === y[1] ? 0 : 1;
24
+ c.result.splice(i - 1, 2, [x, y, false, keep[3]]);
25
+ }
26
+ // Repeated closing lines can attach to the wrong block. Rotate an exact insertion/deletion to the
27
+ // next blank-line boundary, without crossing another edit or splitting a character.
28
+ if (!raw) for (let i = 0; i < c.result.length; i++) {
29
+ const p = c.result[i], q = c.result[i + 1], side = p[0][0] === p[0][1] ? 1 : p[1][0] === p[1][1] ? 0 : -1;
30
+ if (side < 0 || p[2]) continue;
31
+ const text = side ? after : before;
32
+ const room = Math.min((q?.[0][0] ?? endA) - p[0][1], (q?.[1][0] ?? endB) - p[1][1]);
33
+ let n = 0, shift = 0;
34
+ while (n < room && text[p[side][0] + n] === text[p[side][1] + n] && spend()) {
35
+ n++;
36
+ if (text[p[side][0] + n - 1] === '\n' && /^\r?\n/.test(text.slice(p[side][0] + n))) { shift = n; break; }
37
+ }
38
+ if (shift && p.slice(0, 2).every((span, k) => safe(k ? after : before, span[0] + shift) && safe(k ? after : before, span[1] + shift)))
39
+ for (const span of p.slice(0, 2)) { span[0] += shift; span[1] += shift; }
40
+ }
41
+ // Adjacent blocks deleted or inserted together: when the first carries the line break before it, each carries
42
+ // its own, so any one restores apart from its neighbours.
43
+ if (!raw) for (let i = 1; i < c.result.length; i++) {
44
+ const p = c.result[i - 1], q = c.result[i], kind = p[3][0], side = kind ? 1 : 0, text = side ? after : before;
45
+ if (kind % 2 || q[3][0] !== kind || p[2] || q[2] || p[side][1] !== q[side][0] || p[1 - side][0] !== q[1 - side][0]) continue;
46
+ const blank = /^\r?\n/.test(text.slice(...p[side])) && !/^\r?\n/.test(text.slice(...q[side])) && /(\r?\n)(?:[ \t]*\r?\n)+$/.exec(text.slice(...p[side]));
47
+ if (blank && blank.index > 0) p[side][1] = q[side][0] -= blank[0].length - blank[1].length;
48
+ }
49
+ }
50
+
51
+ // Refine the outer walk and each move's inner walk before assigning transport ownership.
52
+ export function phrases(c) {
53
+ const {before, after, raw, code} = c;
54
+ // Whole-word replacements separated only by spaces read and restore as one phrase, and one number stays one number.
55
+ if (!raw) for (let i = c.result.length - 1; i > 0; i--) {
56
+ const p = c.result[i - 1], q = c.result[i], gap = before.slice(p[0][1], q[0][0]);
57
+ if (p[3][0] !== 1 || q[3][0] !== 1 || p[3][3] || q[3][3] || p[2] || q[2] || gap !== after.slice(p[1][1], q[1][0])) continue;
58
+ // A time, a date, a version or a decimal is one number.
59
+ const number = /^(?:[.,:/-]\d*)+$/.test(gap) && /\d/.test(before[p[0][1] - 1] + after[p[1][1] - 1]) && /\d/.test(before[q[0][0]] + after[q[1][0]]);
60
+ if (!number && (code || !/^[^\S\r\n]+$/.test(gap) || within(before, p[0][0]) || within(after, p[1][0]) ||
61
+ within(before, q[0][1]) || within(after, q[1][1]))) continue;
62
+ p[0][1] = q[0][1]; p[1][1] = q[1][1]; c.result.splice(i, 1);
63
+ }
64
+ // Paired inline markers added or removed around unchanged text on one line are one concern.
65
+ if (!raw && !code) for (let i = 1; i < c.result.length; i++) {
66
+ const p = c.result[i - 1], q = c.result[i], kind = p[3][0], side = kind ? 1 : 0, text = side ? after : before;
67
+ if (kind % 2 || kind !== q[3][0] || p[2] || q[2] || p[3][3] || q[3][3]) continue;
68
+ const open = text.slice(...p[side]), close = text.slice(...q[side]), inner = text.slice(p[side][1], q[side][0]);
69
+ if (!inner.trim() || inner.includes('\n') || !(open === close && /^(?:[*_]{1,3}|~~|==|`+)$/.test(open) ||
70
+ open === '[' && /^\]\([^\s()]*\)$/.test(close))) continue;
71
+ p[3] = q[3] = [kind, 'word', p[3][2], [[p[0][0], q[0][1]], [p[1][0], q[1][1]]]];
72
+ }
73
+ }
74
+
75
+ // Readable concerns: phrases, numbers and paired markers, then the public edits and changes with context.
76
+ export function concerns(c) {
77
+ phrases(c);
78
+ const {before, after, spend, safe, budget, raw, code, evidence, structures, initial, options, pa, pb} = c, lb = c.lb;
79
+ const overlaps = (a, b) => a[0] < b[1] && a[1] > b[0];
80
+ // Siblings are ordered and disjoint: the parts holding a range are found by descent, outermost first.
81
+ const containing = (parts, from, to, out = []) => {
82
+ let lo = 0, hi = parts.length;
83
+ while (lo < hi) { const mid = lo + hi >>> 1; if (parts[mid].end < to) lo = mid + 1; else hi = mid; }
84
+ for (let k = lo; k < parts.length && parts[k].start <= from && spend(); k++)
85
+ if (parts[k].end >= to) { out.push(parts[k]); containing(parts[k].children, from, to, out); }
86
+ return out;
87
+ };
88
+ // Edits are ordered and disjoint on the after side.
89
+ const touched = (edits, range) => {
90
+ let lo = 0, hi = edits.length;
91
+ while (lo < hi) { const mid = lo + hi >>> 1; if (edits[mid].after[1] <= range[0]) lo = mid + 1; else hi = mid; }
92
+ return lo < edits.length && overlaps(range, edits[lo].after);
93
+ };
94
+ const changes = [], edits = [];
95
+ let normalized;
96
+ const meaning = structures?.[0]?.meaning;
97
+ const format = typeof meaning === 'string' && meaning === structures[1].meaning;
98
+ for (let i = 0; i < c.result.length; i++) {
99
+ const item = c.result[i];
100
+ if (raw === 'char' && !evidence) {
101
+ const a = item[0], b = item[1], next = c.result[i + 1];
102
+ if (item[3][0] !== 1) {
103
+ const room = Math.min((next?.[0][0] ?? before.length) - a[1], (next?.[1][0] ?? after.length) - b[1]);
104
+ let n = 0;
105
+ while (n < room && before[a[0] + n] === after[b[0] + n] && spend()) n++;
106
+ while (n && (!safe(before, a[0] + n) || !safe(before, a[1] + n) || !safe(after, b[0] + n) || !safe(after, b[1] + n))) n--;
107
+ a[0] += n; a[1] += n; b[0] += n; b[1] += n;
108
+ }
109
+ const p = normalized;
110
+ if (p && p[0][0] === p[0][1] && item[3][0] === 2 && p[0][0] === a[0] && p[1][1] === b[0]) {
111
+ p[1][1] = b[1]; if (item[2]) edits.at(-1).coarse = true; continue;
112
+ }
113
+ normalized = item;
114
+ }
115
+ const group = item[3];
116
+ if (!raw && !group[3]) group[3] = [[...item[0]], [...item[1]]];
117
+ if (!group[4]) {
118
+ const context = group[3];
119
+ // Context completes a word only where an edge falls inside it.
120
+ if (!raw) for (const [text, span] of [[before, context[0]], [after, context[1]]]) {
121
+ const start = within(text, span[0]), end = within(text, span[1]);
122
+ for (let c; start && letter(c = prev(text, span[0])) && spend() && safe(text, span[0] - c.length);) span[0] -= c.length;
123
+ for (let c; end && letter(c = next(text, span[1])) && spend() && safe(text, span[1] + c.length);) span[1] += c.length;
124
+ }
125
+ group[4] = []; changes.push(group);
126
+ if (format) group[0] = 5;
127
+ }
128
+ group[4].push(edits.length);
129
+ edits.push({before: item[0], after: item[1], ...(item[2] ? {coarse: true} : {})});
130
+ }
131
+ if (structures && !raw && budget.left >= 0) {
132
+ const oldNames = new Set(pa.filter(p => p.nameAt).map(p => p.name)), newNames = new Set(pb.filter(p => p.nameAt).map(p => p.name));
133
+ const linksTo = new Map();
134
+ if (spend(pa.length + pb.length + lb.length)) lb.forEach((link, k) => linksTo.set(link.to, [...linksTo.get(link.to) || [], k]));
135
+ for (const change of changes) {
136
+ if (change[0] === 5) continue;
137
+ if (budget.left < 0) break;
138
+ const range = change[3];
139
+ const older = containing(structures[0].parts, ...range[0]), newer = containing(structures[1].parts, ...range[1]);
140
+ const part = (change[0] === 0 ? older : newer).at(-1) || older.at(-1);
141
+ if (!change[2] && part?.name) { change[2] = part.name; change[1] = part.kind; }
142
+ const names = new Set([change[2], ...older.map(p => p.name), ...newer.map(p => p.name)]), breaks = [], affects = [];
143
+ for (const k of [...names].flatMap(name => linksTo.get(name) || []).sort((a, b) => a - b)) {
144
+ const {at, to} = lb[k];
145
+ if (!newNames.has(to)) { if (oldNames.has(to)) breaks.push({at, to}); continue; }
146
+ if (!spend() || touched(edits, at)) continue;
147
+ const p = containing(structures[1].parts, ...at).filter(p => /^(paragraph|call|item|task|cell|declaration)$/.test(p.kind)).at(-1);
148
+ affects.push(p && !touched(edits, [p.start, p.end]) ? [p.start, p.end] : at);
149
+ }
150
+ if (breaks.length) change[6] = breaks;
151
+ if (affects.length) change[5] = affects;
152
+ }
153
+ }
154
+ if (options._stats) Object.assign(options._stats, {work: Math.max(0, initial - Math.max(0, budget.left)), coarse: edits.some(c => c.coarse)});
155
+ // Concern tuples: kind, unit, name, context, edits, affects, breaks. Wording inside a move shares its context.
156
+ const moves = new Map(changes.map((change, i) => change[0] === 3 && [change[3], i + 1]).filter(Boolean));
157
+ return {edits, changes: changes.map(([kind, unit, name, context, owned, affects, breaks], i) => ({
158
+ id: i + 1, kind: ['delete', 'replace', 'insert', 'move', 'rename', 'format'][kind], unit, edits: owned,
159
+ ...(name ? {name} : {}), ...(context ? {context: {before: context[0], after: context[1]}} : {}),
160
+ ...(kind !== 3 && moves.has(context) ? {move: moves.get(context)} : {}),
161
+ ...(affects ? {affects} : {}), ...(breaks ? {breaks} : {})
162
+ }))};
163
+ }
@@ -0,0 +1,68 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // One comparison: observe structure, walk the source into exact records, find moves and renames, then read them as
3
+ // concerns. Each stage lives in its own module and shares one comparison context.
4
+ import {observe} from './structure.mjs';
5
+ import {boundaries, tokenizer, wordSegmenter} from './tokens.mjs';
6
+ import {matcher} from './matching.mjs';
7
+ import {walker} from './walk.mjs';
8
+ import {moves} from './moves.mjs';
9
+ import {separate, concerns} from './concerns.mjs';
10
+
11
+ export function diff(before, after, options = {}) {
12
+ if (before === after) return {edits: [], changes: []};
13
+ const budget = options._budget || {left: Number.isFinite(options.limit) ? Math.max(0, options.limit) : 1e7};
14
+ const spend = (n = 1) => (budget.left -= n) >= 0, safe = boundaries(), raw = options._unit;
15
+ // Internal records: [before range, after range, coarse, concern, transported fragments].
16
+ const c = {before, after, options, budget, spend, safe, raw, chars: options.unit === 'char', initial: budget.left,
17
+ segmenter: wordSegmenter(), result: [], code: options.as === 'code'};
18
+ // A Markdown fence holds code inside prose; only code outside fences makes the text code.
19
+ if (options.as !== 'prose' && options.as !== 'code') c.code = /(?:^|\n)\s*(?:[{}]|(?:function|class|const|let|var|import|export|def|fn|func|package)\s+\w[^]{0,80}[({=:;])/
20
+ .test((before.slice(0, 4096) + '\n' + after.slice(0, 4096)).replace(/^ {0,3}(`{3,}|~{3,})[^]*?(?:^ {0,3}\1[`~]*[ \t]*$|(?![^]))/gm, ''));
21
+ let fallback;
22
+ if (options.structure && (!raw || options._structure)) {
23
+ let structures;
24
+ try {
25
+ structures = options._observed || [observe(before, options.structure, spend, safe), observe(after, options.structure, spend, safe)];
26
+ if (structures.some(value => !value || value.invalid)) throw null;
27
+ const [pa, pb] = structures.map(value => value.flat), aliases = new Map();
28
+ const oldByKey = new Map(pa.filter(p => p.key != null).map(p => [p.key, p]));
29
+ const oldNames = new Set(pa.map(p => p.name)), newNames = new Set(pb.map(p => p.name));
30
+ for (const p of pb) {
31
+ const old = oldByKey.get(p.key);
32
+ if (old?.nameAt && p.nameAt && old.kind === p.kind && !oldNames.has(p.name) && !newNames.has(old.name)) aliases.set(p, old);
33
+ }
34
+ Object.assign(c, {structures, pa, pb, la: structures[0].links, lb: structures[1].links, aliases});
35
+ } catch {
36
+ // A failed reader preserves an explicit prose choice and reports why structure was not used.
37
+ const failed = structures?.find(value => !value || value.invalid);
38
+ fallback = {structure: failed?.threw ? 'threw' : budget.left < 0 ? 'limit' : 'invalid', ...(failed?.threw ? {error: failed.error} : {})};
39
+ c.code = options.as !== 'prose';
40
+ }
41
+ }
42
+ c.tokens = tokenizer(c); c.matches = matcher(spend, budget);
43
+ const {walk, operations} = Object.assign(c, walker(c)), {structures, pa, pb} = c;
44
+ const evidence = c.evidence = options.from && operations(options.from);
45
+ if (options.from && !evidence) (fallback ||= {}).from = budget.left < 0 ? 'limit' : 'invalid';
46
+ if (evidence) for (const span of evidence) walk(...span, structures ? 0 : raw === 'char' ? 3 : 1, pa, pb);
47
+ else walk(0, before.length, 0, after.length, structures ? 0 : raw === 'word' ? 2 : 1,
48
+ structures?.[0]?.parts, structures?.[1]?.parts);
49
+ separate(c);
50
+ moves(c);
51
+ // A heading edit can re-record its children. Compare both walks after transport is known: unchanged moved
52
+ // fragments cost nothing, so a smaller plain walk cannot discard a move just by aligning some of its lines.
53
+ if (structures && !evidence && !raw && c.result.some(r => r[0][0] === r[0][1]) && c.result.some(r => r[1][0] === r[1][1]) &&
54
+ budget.left >= c.initial - budget.left) {
55
+ const size = (rows, transport = false) => rows.reduce((n, r) => n + (!transport && r[3][0] === 3 ? 0 : r[0][1] - r[0][0] + r[1][1] - r[1][0]), 0);
56
+ const walked = c.result, links = c.lb, cost = size(walked);
57
+ if (cost) {
58
+ c.result = []; c.lb = structures[1].links;
59
+ walk(0, before.length, 0, after.length, 1);
60
+ separate(c); moves(c);
61
+ const plain = size(c.result);
62
+ if (budget.left < 0 || c.result.some(r => r[2]) || plain > cost ||
63
+ plain === cost && size(c.result, true) >= size(walked, true)) { c.result = walked; c.lb = links; }
64
+ }
65
+ }
66
+ const {edits, changes} = concerns(c);
67
+ return {edits, ...(fallback && !raw ? {fallback} : {}), changes};
68
+ }