rapier-markdown-kit 1.2.45 → 1.2.48

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 (52) hide show
  1. package/LICENSE +12 -5
  2. package/README.md +134 -11
  3. package/dist/agent/vendor/BROWSER-ENTITIES.json +36 -0
  4. package/dist/agent/vendor/MARKDOWN-RUNTIME.json +136 -0
  5. package/dist/agent/vendor/MARKDOWNIT.json +582 -0
  6. package/dist/agent/vendor/NOTICE.txt +60 -0
  7. package/dist/agent/vendor/markdown-licenses/entities-LICENSE.txt +11 -0
  8. package/dist/agent/vendor/markdown-licenses/linkify-it-LICENSE.txt +22 -0
  9. package/dist/agent/vendor/markdown-licenses/markdown-it-LICENSE.txt +22 -0
  10. package/dist/agent/vendor/markdown-licenses/mdurl-LICENSE.txt +45 -0
  11. package/dist/agent/vendor/markdown-licenses/punycode.js-LICENSE-MIT.txt +20 -0
  12. package/dist/agent/vendor/markdown-licenses/uc.micro-LICENSE.txt +20 -0
  13. package/dist/agent/vendor/markdown-plugins.json +46 -0
  14. package/dist/agent/vendor/markdown-plugins.mjs +27 -0
  15. package/dist/agent/vendor/markdown-runtime.mjs +13 -0
  16. package/dist/kit/artifact-modules.mjs +19 -0
  17. package/dist/kit/assets.d.mts +106 -0
  18. package/dist/kit/export-fonts.mjs +28 -19
  19. package/dist/kit/index.d.mts +151 -0
  20. package/dist/kit/layout.d.mts +61 -0
  21. package/dist/kit/ledger/carried.d.mts +12 -6
  22. package/dist/kit/ledger/carried.mjs +17 -8
  23. package/dist/kit/ledger/format.mjs +5 -3
  24. package/dist/kit/ledger/source.mjs +31 -2
  25. package/dist/kit/ledger/text-merge.mjs +33 -0
  26. package/dist/kit/ledger/transport.mjs +61 -2
  27. package/dist/kit/ledger.d.mts +137 -31
  28. package/dist/kit/markdown-it.d.mts +2 -2
  29. package/dist/kit/marks.d.mts +241 -0
  30. package/dist/kit/marks.mjs +39 -1
  31. package/dist/kit/model.d.mts +191 -0
  32. package/dist/kit/pdf-source-digest.mjs +10 -0
  33. package/dist/kit/pdf-source.mjs +4 -1
  34. package/dist/kit/pdf-vectors.mjs +3 -2
  35. package/dist/kit/render-entry.d.mts +654 -0
  36. package/dist/kit/render-markdown.mjs +14 -8
  37. package/dist/kit/render-print.mjs +36 -7
  38. package/dist/kit/render.mjs +16 -14
  39. package/dist/kit/typography.d.mts +42 -0
  40. package/dist/kit/will.d.mts +26 -3
  41. package/dist/layout/initial-range.mjs +18 -0
  42. package/dist/layout/typography.mjs +2 -18
  43. package/dist/math/delimiters.mjs +38 -0
  44. package/dist/math/markdown-rules.mjs +103 -0
  45. package/dist/packages/rapier-diff/concerns.mjs +2 -2
  46. package/dist/spec/document-fonts.mjs +62 -16
  47. package/dist/spec/md-assets.mjs +1 -0
  48. package/dist/spec/md-comments.mjs +384 -0
  49. package/dist/spec/md-marks.mjs +9 -0
  50. package/dist/spec/md-parser-config.mjs +463 -0
  51. package/dist/spec/md-parser-runtime.mjs +23 -0
  52. package/package.json +30 -8
@@ -715,7 +715,7 @@ function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
715
715
  }
716
716
  root.innerHTML = sanitizeRapierHtml(rendered, options.page ? 'source' : 'export');
717
717
  if (cutAt >= 0) _rapierAppendNestingCut(root, markdown, cutAt);
718
- _rapierApplyBlockDirection(root);
718
+ _rapierApplyBlockDirection(root, false, options);
719
719
 
720
720
  _rapierAssignHeadingSlugs(root, RAPIER_RENDERED_HEADING_SELECTOR, false, options.carry?.slugs, options.carry?.headingIds);
721
721
  _rapierDisambiguateRenderedAnchors(root);
@@ -791,7 +791,7 @@ async function _rapierRenderSemanticRootAsync(canonical, metadata, options = {})
791
791
  if (html) append();
792
792
  if (cutAt >= 0) _rapierAppendNestingCut(root, markdown, cutAt);
793
793
  parsed = tokens = env = labels = null;
794
- await finishAsync(_rapierSemanticDecorations(root, options.carry), work);
794
+ await finishAsync(_rapierSemanticDecorations(root, options.carry, options), work);
795
795
  await pause(work, .6);
796
796
  complete = true;
797
797
  return root;
@@ -846,8 +846,8 @@ function _rapierReadPartDecorations(canonical, metadata, options = {}) {
846
846
  return Object.freeze({headingIds, captionStart});
847
847
  }
848
848
 
849
- function* _rapierSemanticDecorations(root, carry = null) {
850
- yield* _rapierApplyBlockDirectionSteps(root); yield;
849
+ function* _rapierSemanticDecorations(root, carry = null, options = {}) {
850
+ yield* _rapierApplyBlockDirectionSteps(root, false, options); yield;
851
851
  yield* _rapierAssignHeadingSlugsSteps(root, RAPIER_RENDERED_HEADING_SELECTOR, false, carry?.slugs, carry?.headingIds); yield;
852
852
  yield* _rapierDisambiguateRenderedAnchorsSteps(root); yield;
853
853
  for (const code of root.querySelectorAll('pre > code')) { _rapierNormalizeCodeElement(code); yield; }
@@ -1159,14 +1159,20 @@ function _rapierRawHtmlFragment(source, rewrite, parser) {
1159
1159
  return out;
1160
1160
  }
1161
1161
 
1162
- function _rapierApplyBlockDirection(root) { return finish(_rapierApplyBlockDirectionSteps(root)); }
1162
+ function _rapierApplyBlockDirection(root, includeRoot = false, options = {}) { return finish(_rapierApplyBlockDirectionSteps(root, includeRoot, options)); }
1163
1163
 
1164
- function* _rapierApplyBlockDirectionSteps(root) {
1164
+ function* _rapierApplyBlockDirectionSteps(root, includeRoot = false, {authoredTableDirection = false} = {}) {
1165
1165
  if (!root || typeof root.querySelectorAll !== 'function') return;
1166
+ const tableContainers = new Set();
1167
+ if (authoredTableDirection) for (const table of root.querySelectorAll('table')) {
1168
+ for (let parent = table.parentElement; parent && parent !== root && !tableContainers.has(parent); parent = parent.parentElement) {
1169
+ tableContainers.add(parent); yield;
1170
+ }
1171
+ }
1166
1172
  const blocks = new Set(_RAPIER_DIR_BLOCKS.split(',')), elements = root.ownerDocument.createTreeWalker(root, 1);
1167
- for (let el = elements.nextNode(); el; el = elements.nextNode()) {
1173
+ for (let el = includeRoot ? root : elements.nextNode(); el; el = elements.nextNode()) {
1168
1174
  yield;
1169
- if (!blocks.has(el.localName)) continue;
1175
+ if (!blocks.has(el.localName) || tableContainers.has(el)) continue;
1170
1176
  let literal = false;
1171
1177
  for (let parent = el.parentElement; parent; parent = parent.parentElement)
1172
1178
  if (parent.localName === 'pre' || parent.localName === 'code') { literal = true; break; }
@@ -2,11 +2,12 @@
2
2
  import {collectPdfVectors} from './pdf-vectors.mjs';
3
3
  import {collectPdfGlyphs} from './pdf-glyphs.mjs';
4
4
  import {pdfFontSources, waitForFonts} from './export-fonts.mjs';
5
- import {apply as applyTypography} from '../layout/typography.mjs';
5
+ import {printTypography} from './artifact-modules.mjs';
6
6
  import {documentLanguage, pageSizeOf, paperForLanguage} from '../spec/document-settings.mjs';
7
7
  import {applyPrintCapitals} from './print-capitals.mjs';
8
8
  import {stripOneTerminator as _rapierWillStripOneTerminator} from '../agent/will.mjs';
9
9
  import {quotedWillSpan} from '../spec/md-marks.mjs';
10
+ import {PDF_SOURCE_DIGEST, pdfSourceDigest} from './pdf-source-digest.mjs';
10
11
  const RAPIER_WILL_SENTINEL_MARK = '⁣';
11
12
  // SPDX-License-Identifier: MIT
12
13
  function createPrintRenderer(runtime) {
@@ -229,7 +230,8 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
229
230
  if (bodyWidth < 1 || bodyHeight < 1) refuse('pdf_page_geometry_invalid');
230
231
  const tiled = bodyWidth > CAPTURE_EDGE || bodyHeight > CAPTURE_EDGE || bodyWidth * bodyHeight > CAPTURE_PIXELS;
231
232
  const frame = document.createElement('iframe');
232
- let vectors, backdrop = null, nativeGlyphs, filmSession;
233
+ let vectors, backdrop = null, nativeGlyphs, filmSession, typography;
234
+ const omitted = [];
233
235
  let rejectAbort;
234
236
  const aborted = new Promise((_, reject) => { rejectAbort = reject; });
235
237
  aborted.catch(() => {});
@@ -281,7 +283,18 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
281
283
  main.getBoundingClientRect();
282
284
  if (fontCss) await step(waitForFonts(main, {fontCss, css, settings}));
283
285
  await step(into.fonts.ready);
284
- for (const image of main.querySelectorAll('img')) { image.loading = 'eager'; await step(image.decode()); }
286
+ for (const image of main.querySelectorAll('img')) {
287
+ image.loading = 'eager';
288
+ try { await step(image.decode()); }
289
+ catch (error) {
290
+ if (error?.name !== 'EncodingError') throw error;
291
+ const source = image.getAttribute('src') || '';
292
+ omitted.push(source.startsWith('data:') ? image.getAttribute('alt') || 'a picture' : source.slice(0, 120));
293
+ const holder = image.closest('p,figure');
294
+ image.remove();
295
+ if (holder && !holder.textContent.trim() && !holder.querySelector('img,svg,math,table,pre')) holder.remove();
296
+ }
297
+ }
285
298
  wrapMasks(main);
286
299
  check();
287
300
  for (const node of [main, ...main.querySelectorAll('*')]) {
@@ -303,7 +316,8 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
303
316
  main.style.setProperty(name, value, 'important');
304
317
  globalThis.RapierMath?.fit(main);
305
318
  applyPrintCapitals(main);
306
- applyTypography(main, {all: true});
319
+ typography = printTypography(main, globalThis.RapierArtifactLayoutDependencies);
320
+ typography?.apply(main, {all: true});
307
321
  ink?.(main);
308
322
  const count = Math.max(1, Math.ceil((main.scrollWidth - 1) / bodyWidth));
309
323
  if (!Number.isSafeInteger(count)) refuse('pdf_page_geometry_invalid');
@@ -421,9 +435,9 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
421
435
  const bytes = await writeRasterPdf({pages, textPages, linkPages, vectorPages: vectors.pages, canonical, bom, filename, title, font, width, height, margins, backdrop: backdrop?.pages[0] || null,
422
436
  pixelWidth: bodyWidth, pixelHeight: bodyHeight, pageNumbers: settings?.pagestyle === 'plain', maxBytes, check});
423
437
  check();
424
- return {bytes, pages: count, painted: nativeGlyphs.painted};
438
+ return {bytes, pages: count, painted: nativeGlyphs.painted, omitted};
425
439
  } catch (error) { throw spoken(error); }
426
- finally { filmSession?.close(); backdrop?.restore(); vectors?.restore(); nativeGlyphs?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
440
+ finally { filmSession?.close(); backdrop?.restore(); vectors?.restore(); nativeGlyphs?.restore(); typography?.retire(frame.contentDocument); signal?.removeEventListener('abort', abort); frame.remove(); }
427
441
  }
428
442
 
429
443
  async function _rapierPrintTextPages(main, pageWidth, count, check, origin = main.getBoundingClientRect(), nativeNodes = null, markerNodes = null) {
@@ -442,6 +456,18 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
442
456
  }
443
457
  const marker = markerNodes?.get(node);
444
458
  if (marker && marker.page < count) pages[marker.page].push({...marker, block: node, carrier: null, marker: true});
459
+ const taskItem = node.nodeType === 1 && node.matches('input[type="checkbox"]') && node.closest('li');
460
+ const taskParent = taskItem && node.parentElement;
461
+ if (taskItem && taskParent.firstElementChild === node && (taskParent === taskItem || taskParent.localName === 'p' && taskParent.parentElement === taskItem && taskItem.firstElementChild === taskParent)) {
462
+ const box = node.getBoundingClientRect(), style = into.defaultView.getComputedStyle(node);
463
+ if (box.width > 0 && box.height > 0 && style.visibility !== 'hidden' && style.visibility !== 'collapse') {
464
+ const page = Math.max(0, Math.min(count - 1, Math.floor((box.left - origin.left + .01) / pageWidth)));
465
+ const size = parseFloat(style.fontSize) || 16, width = Math.min(size, box.width), height = Math.min(size, box.height);
466
+ const text = node.checked ? '[x] ' : '[ ] ', part = {text, x: box.left - origin.left - page * pageWidth + (box.width - width) / 2,
467
+ y: box.top - origin.top + (box.height - height) / 2, width, height};
468
+ pages[page].push({...part, parts: [part], block: taskItem, carrier: null, marker: true});
469
+ }
470
+ }
445
471
  const formula = node.nodeType === 1 && node.matches('.math-rendered[data-math-src]') && node.querySelector('svg');
446
472
  if (node.nodeType === 1 && !formula || node.nodeType === 3 && element.closest('.math-rendered[data-math-src]')) continue;
447
473
  let value;
@@ -885,7 +911,8 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
885
911
  cursor += (id + ' 0 obj\n').length + objects[id].length + 8;
886
912
  }
887
913
  const xrefAt = cursor;
888
- const xref = encode('xref\n0 ' + objects.length + '\n0000000000 65535 f \n' + offsets.slice(1).map(at => String(at).padStart(10, '0') + ' 00000 n \n').join('') + 'trailer\n<< /Size ' + objects.length + ' /Root 1 0 R /Info ' + info + ' 0 R >>\nstartxref\n' + xrefAt + '\n%%EOF\n');
914
+ const table = 'xref\n0 ' + objects.length + '\n0000000000 65535 f \n' + offsets.slice(1).map(at => String(at).padStart(10, '0') + ' 00000 n \n').join('') + 'trailer\n<< /Size ' + objects.length + ' /Root 1 0 R /Info ' + info + ' 0 R ' + PDF_SOURCE_DIGEST + ' >>\nstartxref\n' + xrefAt + '\n%%EOF\n';
915
+ const xref = encode(table), digestAt = xrefAt + table.indexOf(PDF_SOURCE_DIGEST) + PDF_SOURCE_DIGEST.indexOf('<') + 1;
889
916
  if (cursor + xref.length > maxBytes) fail('export_too_large');
890
917
  const bytes = new Uint8Array(cursor + xref.length);
891
918
  bytes.set(header, 0);
@@ -899,6 +926,8 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
899
926
  bytes.set(suffix, at); at += suffix.length;
900
927
  }
901
928
  bytes.set(xref, at);
929
+ bytes.set(encode(await pdfSourceDigest(bytes, digestAt, bytes.length - 1)), digestAt);
930
+ check();
902
931
  return bytes;
903
932
  }
904
933
  export {createPrintRenderer, applyPrintCapitals, RAPIER_WILL_SENTINEL_MARK as sentinel};
@@ -9,6 +9,7 @@ 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
11
  import {isTemplatePage, isPageBorderLayout} from '../spec/template-page.mjs';
12
+ import {_rapierArtifactFactoryModules} from './artifact-modules.mjs';
12
13
  export function positionNativeDiagram(drawing) {
13
14
  const surface=drawing?.closest?.('.diagram-block,.diagram-live-preview');
14
15
  if(!surface?.clientWidth)return;
@@ -49,8 +50,7 @@ async function _rapierBuildArtifact(options, providedContext) {
49
50
  const opts = options || {};
50
51
  const context = providedContext || _rapierBuildInterchangeContext(opts);
51
52
  const preference = _rapierArtifactPreference('theme', ['system', 'light', 'dark']);
52
- const theme = opts.print ? 'light' : preference === 'system'
53
- ? (globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : preference;
53
+ const theme = opts.print ? 'light' : preference;
54
54
  const metadata = context.metadata;
55
55
  const base = context.baseName;
56
56
  const work = context.work || opts.work;
@@ -113,8 +113,8 @@ async function _rapierBuildArtifact(options, providedContext) {
113
113
  const styledRoot = work ? await _rapierProjectStyledRootAsync(context.semanticRoot, styledOptions)
114
114
  : _rapierProjectStyledRoot(context.semanticRoot, styledOptions);
115
115
  styledRoot.setAttribute('lang', language);
116
- const darkPaper = theme === 'dark' && _rapierArtifactPreference('invertImageAlpha', ['true', 'false']) !== 'false';
117
- const templatePage = _rapierTemplatePage(context.canonical, settings, styledRoot, darkPaper);
116
+ const paperTheme = _rapierArtifactPreference('invertImageAlpha', ['true', 'false']) !== 'false' ? theme : 'light';
117
+ const templatePage = _rapierTemplatePage(context.canonical, settings, styledRoot, paperTheme);
118
118
  if (completePage && typeof _rapierFinalizeExportCode === 'function') await _rapierFinalizeExportCode(styledRoot);
119
119
  if (!opts.print) _rapierAnnotateExportBoxPolygons(styledRoot);
120
120
  if (completePage || opts.print) {
@@ -204,7 +204,7 @@ async function _rapierBuildArtifact(options, providedContext) {
204
204
  };
205
205
  }
206
206
 
207
- function _rapierTemplatePage(canonical, settings, root, darkPaper = false) {
207
+ function _rapierTemplatePage(canonical, settings, root, paperTheme = 'light') {
208
208
  if (!isTemplatePage(canonical)) return null;
209
209
  const paper = pageSizeOf(settings);
210
210
  let url = '';
@@ -219,9 +219,11 @@ function _rapierTemplatePage(canonical, settings, root, darkPaper = false) {
219
219
  const margins = ['top', 'right', 'bottom', 'left'].map(side => (settings?.geometry?.[side] ?? 1440) / 15);
220
220
  const padding = margins.map(value => value + 'px').join(' ');
221
221
  const ground = 'background:var(--rapier-template-border) 0 0/100% 100% no-repeat';
222
- const shown = darkPaper && globalThis.RapierImageAssets.dataImage(url)?.codec === 'image/svg+xml' ? _rapierDarkPaperSvgUrl(url, '#fafafa') : url;
222
+ const shown = paperTheme !== 'light' && globalThis.RapierImageAssets.dataImage(url)?.codec === 'image/svg+xml' ? _rapierDarkPaperSvgUrl(url, '#fafafa') : url;
223
+ const screenInk = shown !== url ? '@media screen' + (paperTheme === 'system' ? ' and (prefers-color-scheme:dark)' : '')
224
+ + '{:root{--rapier-template-border:url("' + shown + '")}}\n' : '';
223
225
  const css = ':root{--rapier-template-border:url("' + url + '")}\n'
224
- + '@media screen{' + (shown !== url ? ':root{--rapier-template-border:url("' + shown + '")}' : '') + '.rapier-template-border{display:none}body{background:var(--md-color-inset)}.rapier-page{box-sizing:border-box;width:min(100%,' + width + 'px);min-height:' + height + 'px;margin:0 auto;padding:' + padding
226
+ + screenInk + '@media screen{.rapier-template-border{display:none}body{background:var(--md-color-inset)}.rapier-page{box-sizing:border-box;width:min(100%,' + width + 'px);min-height:' + height + 'px;margin:0 auto;padding:' + padding
225
227
  + ';background:var(--rapier-template-border) 0 0/100% ' + height + 'px repeat-y,var(--md-color-bg);color:var(--md-color-text)}}\n'
226
228
  + '@media screen and (max-width:' + width + 'px){.rapier-page{min-height:auto;aspect-ratio:' + width + '/' + height + ';padding:'
227
229
  + margins.map(value => 'min(' + value + 'px,' + Number((value / width * 100).toFixed(4)) + '%)').join(' ') + ';background-size:100% auto,auto}}\n'
@@ -347,9 +349,14 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
347
349
  yield;
348
350
  cache.innerHTML = sanitizeRapierHtml(cache.innerHTML, 'export');
349
351
  }
350
- return root;
351
352
  }
352
- return yield* cleanTree(root, sanitizeRapierHtml, 'export');
353
+ const admitted = opts.sanitized ? root : yield* cleanTree(root, sanitizeRapierHtml, 'export');
354
+ for (const list of admitted.querySelectorAll('ol[start]')) {
355
+ yield;
356
+ const start = list.getAttribute('start');
357
+ if (/^-?\d{1,9}$/.test(start)) list.style.setProperty('counter-reset', 'rapier-ol ' + (Number(start) - 1));
358
+ }
359
+ return admitted;
353
360
  }
354
361
 
355
362
  function _rapierDocxRenderStyles(root) {
@@ -1279,11 +1286,6 @@ function _rapierImageOpenRunsList(tokens) {
1279
1286
  return list;
1280
1287
  }
1281
1288
 
1282
- function _rapierArtifactFactoryModules(dependencies, group) {
1283
- const paths = dependencies.groups && dependencies.groups[group] ? dependencies.groups[group] : Object.keys(dependencies.factories);
1284
- return paths.map(path => 'modules[' + JSON.stringify(path) + '] = (' + Function.prototype.toString.call(dependencies.factories[path]) + ')();').join('\n');
1285
- }
1286
-
1287
1289
  function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1288
1290
  if (!root || !metadata || !geometry || !preparation) return null;
1289
1291
  const doc = root.ownerDocument, view = doc.defaultView;
@@ -0,0 +1,42 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import type {RichTextItem, PreparationOptions, Rect, SlotsForBand, SlotSequence, FlowPlan,
3
+ FlowFragment, FlowSlot} from './model.mjs';
4
+
5
+ export interface TypographyApplyResult {
6
+ changed: number;
7
+ released: number;
8
+ pending: boolean;
9
+ prepared: number;
10
+ }
11
+ export interface InitialRange {start: number; end: number; text: string}
12
+ export interface TypographySlotInput {
13
+ width: number;
14
+ top: number;
15
+ obstacles: readonly Rect[];
16
+ lineHeight: number;
17
+ minWidth: number;
18
+ slotsForBand: SlotsForBand;
19
+ slotSequence: SlotSequence;
20
+ columnsFilled(slots: readonly FlowSlot[], placed: readonly FlowSlot[]): boolean;
21
+ justify: boolean;
22
+ hyphenate: boolean;
23
+ }
24
+ export interface DerivedFlowFragment extends FlowFragment {
25
+ derive: true;
26
+ glyph?: string;
27
+ }
28
+ export interface TypographyController {
29
+ /** Pending work requires a subsequent call after the host yields. */
30
+ apply(root: Element | Document | DocumentFragment, options?: {all?: boolean; editing?: boolean}): TypographyApplyResult;
31
+ clear(root: Element | Document | DocumentFragment, options?: {forget?: boolean; keepSelection?: boolean}): void;
32
+ clearSelection(root: Element | Document | DocumentFragment): number;
33
+ firstLineEnd(paragraph: Element | null | undefined): number | null;
34
+ initialRange(input: string | Pick<Node, 'textContent'> | null | undefined): InitialRange | null;
35
+ protectInitial(root: Element | Document | DocumentFragment): void;
36
+ invalidateFonts(document?: Document | null): number;
37
+ retire(root: Element | Document | DocumentFragment, options?: {documentChanged?: boolean}): void;
38
+ slotPlan(paragraph: Element, items: readonly RichTextItem[],
39
+ options: Pick<PreparationOptions, 'language' | 'wordBreak'>, input: TypographySlotInput): FlowPlan<DerivedFlowFragment> | null;
40
+ }
41
+ /** Creates an independent disposable DOM projection owner; it never changes Markdown source. */
42
+ export function createTypography(): TypographyController;
@@ -2,7 +2,7 @@
2
2
  // Will/1: permissions a Markdown document gives agents, as HTML comments on their own lines.
3
3
  // <!-- will/1 keep --> ... <!-- /will --> keep: no change; append: add after the region's text only; edit: any change
4
4
  export type WillLaw = 'keep' | 'append' | 'edit';
5
- export type WillFault = 'malformed_marker' | 'unpaired_marker';
5
+ export type WillFault = 'malformed_marker' | 'unpaired_marker' | 'unknown_version' | 'unknown_law' | 'intent_over_bound';
6
6
  /** A paired region; `start` to `end` is its text between the marker lines (UTF-16 offsets). */
7
7
  export interface WillRegion {
8
8
  index: number; law: WillLaw; intent: string | null;
@@ -40,6 +40,29 @@ export function willMarkerOf(line: string): WillMarker | null;
40
40
  export function terminatorWidth(last: number, prior: number): number;
41
41
  /** The text without one trailing line terminator. */
42
42
  export function stripOneTerminator(text: string): string;
43
+ /** A sequential source edit in UTF-16 coordinates; removed must match the current source. */
44
+ export interface WillSplice {pos: number; removed: string; inserted: string;}
45
+ export interface WillRestoreOptions {
46
+ /** Exact splice objects the host has proved undo its own earlier act; text equality is not authority. */
47
+ restores?: readonly WillSplice[] | null;
48
+ }
49
+ export interface WillTransitionOptions extends WillRestoreOptions {
50
+ /** Optional output map populated with admitted contiguous append-tail inverse rows. */
51
+ restored?: Map<number, WillSplice[]> | null;
52
+ }
53
+ export type WillTransitionRefusal =
54
+ | {law: 'keep'; rule: 'before_faulted' | 'result_faulted'; faults: {mode: WillFault; line: number}[]; start: number; end: number}
55
+ | {law: WillLaw; rule: 'marker_span_touched'; editIndex: number; region: number; start: number; end: number}
56
+ | {law: 'keep'; rule: 'marker_sequence_mismatch'; start: number; end: number}
57
+ | {law: 'keep' | 'append'; rule: 'law_violated'; region: number; start: number; end: number};
58
+ /** Maps a resulting UTF-16 offset back through sequential rows; -1 means it lies in inserted text. */
59
+ export function willOffsetBefore(rows: readonly WillSplice[], offset: number): number;
60
+ /** Checks marker identity and body laws after the host has proved the sequential splices. Null admits the transition. */
61
+ export function willTransition(beforeText: string, afterText: string, splices: readonly WillSplice[],
62
+ markerSplices?: readonly WillSplice[], options?: WillTransitionOptions): WillTransitionRefusal | null;
63
+ export type WillEvaluation =
64
+ | {outcome: 'applied'}
65
+ | {outcome: 'invalid'; reason: 'precondition'; rule: 'invalid_unicode' | 'malformed_splice' | 'out_of_range_splice' | 'source_mismatch'}
66
+ | ({outcome: 'refused'; reason: 'document_law'} & WillTransitionRefusal);
43
67
  /** Judges sequential UTF-16 source splices, including the resulting marker identities and protected text. */
44
- export function evaluateWill(beforeText: string, splices: {pos: number; removed: string; inserted: string}[]):
45
- {outcome: 'applied'} | {outcome: 'invalid' | 'refused'; reason: string; rule: string; law?: WillLaw; region?: number};
68
+ export function evaluateWill(beforeText: string, splices: readonly WillSplice[], options?: WillRestoreOptions): WillEvaluation;
@@ -0,0 +1,18 @@
1
+ // SPDX-License-Identifier: MIT
2
+ let graphemes;
3
+ export const segment = text => (graphemes ||= new Intl.Segmenter(undefined, {granularity: 'grapheme'})).segment(text);
4
+ export const opening = /^[\p{Ps}\p{Pi}\p{Pf}"']+$/u;
5
+
6
+ export function initialRange(input) {
7
+ const text = typeof input === 'string' ? input : input?.textContent;
8
+ if (typeof text !== 'string') return null;
9
+ const start = /^\s*/u.exec(text)[0].length;
10
+ if (start === text.length) return null;
11
+ let end = start;
12
+ for (const part of segment(text.slice(start))) {
13
+ if (/^\s/u.test(part.segment)) break;
14
+ end = start + part.index + part.segment.length;
15
+ if (!opening.test(part.segment)) break;
16
+ }
17
+ return end > start ? {start, end, text: text.slice(start, end)} : null;
18
+ }
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import {createPreparationScope, supportsRichStyle, captureFontAuthority} from './preparation.mjs';
3
3
  import {createParagraphFitter} from './paragraph-fit.mjs';
4
+ import {initialRange, segment, opening} from './initial-range.mjs';
4
5
 
5
6
  export function createTypography() {
6
7
  const fitter = createParagraphFitter();
@@ -16,25 +17,8 @@ export function createTypography() {
16
17
  if (!value) { value = createPreparationScope(document); documents.set(document, value); }
17
18
  return value;
18
19
  }
19
- let graphemes;
20
- const segment = text => (graphemes ||= new Intl.Segmenter(undefined, {granularity: 'grapheme'})).segment(text);
21
- const opening = /^[\p{Ps}\p{Pi}\p{Pf}"']+$/u;
22
20
  const closing = /^[\p{Pe}\p{Pi}\p{Pf}"'.,;:!?]+$/u;
23
21
 
24
- function initialRange(input) {
25
- const text = typeof input === 'string' ? input : input?.textContent;
26
- if (typeof text !== 'string') return null;
27
- const start = /^\s*/u.exec(text)[0].length;
28
- if (start === text.length) return null;
29
- let end = start;
30
- for (const part of segment(text.slice(start))) {
31
- if (/^\s/u.test(part.segment)) break;
32
- end = start + part.index + part.segment.length;
33
- if (!opening.test(part.segment)) break;
34
- }
35
- return end > start ? {start, end, text: text.slice(start, end)} : null;
36
- }
37
-
38
22
  const flowFloat = '.rapier-flow-float';
39
23
  const own = paragraph => [paragraph, ...[...paragraph.querySelectorAll('*')].filter(element => !element.closest(flowFloat))];
40
24
 
@@ -770,7 +754,7 @@ export const apply = typography.apply;
770
754
  export const clear = typography.clear;
771
755
  export const clearSelection = typography.clearSelection;
772
756
  export const firstLineEnd = typography.firstLineEnd;
773
- export const initialRange = typography.initialRange;
757
+ export {initialRange};
774
758
  export const protectInitial = typography.protectInitial;
775
759
  export const invalidateFonts = typography.invalidateFonts;
776
760
  export const retire = typography.retire;
@@ -0,0 +1,38 @@
1
+ // SPDX-License-Identifier: MIT
2
+ export function createMathMatcher(source, {singleDollar = false, brackets = true} = {}) {
3
+ if (typeof source !== 'string') throw new TypeError('Math boundaries require source text.');
4
+ const markers = new Map(), next = Object.create(null);
5
+ let slashes = 0, line = 0;
6
+ for (let at = 0; at < source.length;) {
7
+ const char = source[at];
8
+ if (char === '\\') { slashes++; at++; continue; }
9
+ if (char === '\n' || char === '\r') line++;
10
+ if (char === '$') {
11
+ let end = at + 1;
12
+ while (source[end] === '$') end++;
13
+ const count = end - at;
14
+ if (!(slashes & 1) && (count === 2 || count === 1 && singleDollar))
15
+ markers.set(at, {at, marker: count === 2 ? '$$' : '$', line});
16
+ at = end;
17
+ } else {
18
+ if (brackets && slashes & 1 && '()[]'.includes(char))
19
+ markers.set(at - 1, {at: at - 1, marker: '\\' + char, line});
20
+ at++;
21
+ }
22
+ slashes = 0;
23
+ }
24
+ for (const entry of [...markers.values()].reverse()) {
25
+ const closing = entry.marker === '\\(' ? '\\)' : entry.marker === '\\[' ? '\\]' : entry.marker;
26
+ entry.close = next[closing];
27
+ next[entry.marker] = entry;
28
+ }
29
+ return function match(start, limit = source.length) {
30
+ const open = markers.get(start), close = open?.close;
31
+ if (!close || open.marker === '\\)' || open.marker === '\\]' || close.at + close.marker.length > limit) return null;
32
+ const bodyStart = start + open.marker.length, bodyEnd = close.at;
33
+ if (open.marker === '$' && (open.line !== close.line || bodyStart === bodyEnd ||
34
+ /\s/.test(source[bodyStart]) || /\s/.test(source[bodyEnd - 1]) || /[0-9]/.test(source[bodyEnd + 1] || ''))) return null;
35
+ return {start, end: bodyEnd + close.marker.length, bodyStart, bodyEnd,
36
+ open: open.marker, close: close.marker, displayMode: open.marker === '$$' || open.marker === '\\['};
37
+ };
38
+ }
@@ -0,0 +1,103 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import {createMathMatcher} from './delimiters.mjs';
3
+
4
+ export function installMathRules(md, {singleDollar = false, brackets = true, ruleName = 'rapier_math', inLink = false} = {}) {
5
+ const authored = new WeakMap();
6
+ md.core.ruler.before('normalize', ruleName + '_source', state => {
7
+ authored.set(state.tokens, {source: state.src});
8
+ });
9
+ md.core.ruler.after('block', ruleName + '_source_release', state => authored.delete(state.tokens));
10
+ const tokenize = md.block.tokenize;
11
+ md.block.tokenize = function(state, startLine, endLine) {
12
+ const first = state.tokens.length;
13
+ try { return tokenize.call(this, state, startLine, endLine); }
14
+ finally {
15
+ for (let index = first; index < state.tokens.length; index++) {
16
+ const token = state.tokens[index];
17
+ if (token.type !== 'fence' || !token.map || token.meta?.mathSource !== undefined ||
18
+ String(token.info || '').trim().split(/\s+/)[0].toLowerCase() !== 'math') continue;
19
+ const held = authored.get(state.tokens), source = held?.source ?? state.src;
20
+ let value;
21
+ if (!state.rapierSourceSlice && source === state.src) value = state.getLines(token.map[0], token.map[1], state.blkIndent, false);
22
+ else {
23
+ if (!state.rapierSourceSlice) {
24
+ const starts = text => [0, ...Array.from(text.matchAll(/\r\n?|\n/g), match => match.index + match[0].length)];
25
+ held.rawLines ||= starts(source);
26
+ held.lines ||= starts(state.src);
27
+ }
28
+ const view = Object.create(state), parts = [];
29
+ view.bMarks = []; view.eMarks = [];
30
+ for (let line = token.map[0]; line < token.map[1]; line++) {
31
+ const shift = state.rapierSourceSlice ? 0 : held.rawLines[line] - held.lines[line];
32
+ view.src = state.rapierSourceSlice
33
+ ? state.rapierSourceSlice(state.bMarks[line], state.eMarks[line])
34
+ : source.slice(state.bMarks[line] + shift, state.eMarks[line] + shift);
35
+ view.bMarks[line] = 0; view.eMarks[line] = view.src.length;
36
+ parts.push(view.getLines(line, line + 1, state.blkIndent, false));
37
+ if (line + 1 < token.map[1]) parts.push(state.rapierSourceSlice
38
+ ? state.rapierSourceSlice(state.eMarks[line], state.eMarks[line] + 1)
39
+ : source.slice(state.eMarks[line] + shift, held.rawLines[line + 1]));
40
+ }
41
+ value = parts.join('');
42
+ }
43
+ token.meta = {...token.meta, mathSource: value};
44
+ }
45
+ }
46
+ };
47
+ const renderText = md.renderer.renderInlineAsText;
48
+ md.renderer.renderInlineAsText = function(tokens, options, env) {
49
+ return renderText.call(this, tokens.map(token =>
50
+ token.type === 'math_inline' || token.type === 'math_block'
51
+ ? {...token, type: 'text', content: token.meta?.mathSource ?? token.content}
52
+ : token), options, env);
53
+ };
54
+ const matchers = new WeakMap();
55
+ function match(state, at, limit) {
56
+ let entry = matchers.get(state);
57
+ if (!entry || entry.source !== state.src) {
58
+ entry = {source: state.src, match: createMathMatcher(state.src, {singleDollar, brackets})};
59
+ matchers.set(state, entry);
60
+ }
61
+ return entry.match(at, limit);
62
+ }
63
+ md.inline.ruler.before('escape', ruleName, (state, silent) => {
64
+ if (!inLink && state.linkLevel > 0) return false;
65
+ if (state.src[state.pos] !== '$' && state.src[state.pos] !== '\\') return false;
66
+ const span = match(state, state.pos, state.posMax);
67
+ if (!span) return false;
68
+ if (!silent) {
69
+ const token = state.push(span.displayMode ? 'math_block' : 'math_inline', '', 0);
70
+ token.markup = span.open;
71
+ token.content = state.src.slice(span.bodyStart, span.bodyEnd);
72
+ token.meta = {mathSource: state.src.slice(span.start, span.end)};
73
+ }
74
+ state.pos = span.end;
75
+ return true;
76
+ });
77
+ md.block.ruler.before('fence', ruleName, (state, startLine, endLine, silent) => {
78
+ if (state.sCount[startLine] - state.blkIndent >= 4) return false;
79
+ const start = state.bMarks[startLine] + state.tShift[startLine];
80
+ if (state.src.slice(start, start + 2) !== '$$' && (!brackets || state.src.slice(start, start + 2) !== '\\[')) return false;
81
+ const span = match(state, start, state.eMarks[endLine - 1]);
82
+ if (!span?.displayMode) return false;
83
+ let last = startLine;
84
+ while (last < endLine && state.eMarks[last] < span.end) last++;
85
+ if (last >= endLine || span.bodyEnd < state.bMarks[last] + state.tShift[last] ||
86
+ state.src.slice(span.end, state.eMarks[last]).trim()) return false;
87
+ if (silent) return true;
88
+ const parts = [];
89
+ for (let line = startLine; line <= last; line++) {
90
+ const from = line === startLine ? span.bodyStart : state.bMarks[line] + state.tShift[line];
91
+ const to = line === last ? span.bodyEnd : state.eMarks[line];
92
+ parts.push(state.src.slice(from, to));
93
+ }
94
+ const token = state.push('math_block', 'math', 0);
95
+ token.block = true;
96
+ token.markup = span.open;
97
+ token.content = parts.join('\n');
98
+ token.meta = {mathSource: span.open + token.content + span.close};
99
+ token.map = [startLine, last + 1];
100
+ state.line = last + 1;
101
+ return true;
102
+ }, {alt: ['paragraph', 'reference', 'blockquote', 'list']});
103
+ }
@@ -5,7 +5,7 @@ export function separate(c, endA = c.before.length, endB = c.after.length) {
5
5
  const {before, after, safe, raw, spend} = c;
6
6
  if (!raw) for (let i = c.result.length - 1; i > 0; i--) {
7
7
  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;
8
- if (side >= 0 && !/\S/.test(text.slice(...p[side]))) {
8
+ if (!c.evidence && side >= 0 && !/\S/.test(text.slice(...p[side]))) {
9
9
  let n = 0;
10
10
  while (p[0][1] + n < q[0][0] && text[p[side][0] + n] === text[p[side][1] + n] && spend()) n++;
11
11
  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)))
@@ -20,7 +20,7 @@ export function separate(c, endA = c.before.length, endB = c.after.length) {
20
20
  keep[3][0] = x[0] === x[1] ? 2 : y[0] === y[1] ? 0 : 1;
21
21
  c.result.splice(i - 1, 2, [x, y, false, keep[3]]);
22
22
  }
23
- if (!raw) for (let i = 0; i < c.result.length; i++) {
23
+ if (!raw && !c.evidence) for (let i = 0; i < c.result.length; i++) {
24
24
  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;
25
25
  if (side < 0 || p[2]) continue;
26
26
  const text = side ? after : before;