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
@@ -0,0 +1,106 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import type {MarkdownParser} from './marks.mjs';
3
+ import type {Splice} from './ledger.mjs';
4
+
5
+ /** A parser instance or a factory for the caller's Markdown grammar. */
6
+ export type ParserFactory = MarkdownParser | ((options: {html: boolean; maxNesting: number}) => MarkdownParser);
7
+ export interface ImageDefinition { href: string; title: string; }
8
+ export type ImageCodec = 'image/jxl' | 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif' | 'image/svg+xml';
9
+ export interface DataImage {
10
+ codec: ImageCodec;
11
+ byteLength: number;
12
+ /** UTF-16 offset within the data URL. */
13
+ payloadStart: number;
14
+ encoding?: 'percent';
15
+ }
16
+ export interface AssetBlock {
17
+ id: string;
18
+ label: string;
19
+ source: string;
20
+ url: string;
21
+ title: string;
22
+ start: number;
23
+ end: number;
24
+ codec: ImageCodec;
25
+ byteLength: number;
26
+ encoding?: 'percent';
27
+ urlStart: number | null;
28
+ urlEnd: number | null;
29
+ payloadStart: number | null;
30
+ payloadEnd: number | null;
31
+ status: 'unverified' | 'asset_count_limit';
32
+ active: boolean;
33
+ topLevel: boolean;
34
+ }
35
+ export interface AssetIndex {
36
+ assets: Map<string, AssetBlock>;
37
+ blocks: AssetBlock[];
38
+ duplicateLabels: Set<string>;
39
+ appendixStart: number;
40
+ references: Record<string, ImageDefinition>;
41
+ }
42
+ export interface PackedAssetIndex {
43
+ blocks: (Omit<AssetBlock, 'source' | 'url'> & {url?: string})[];
44
+ references: Record<string, ImageDefinition | {row: number; title: string}>;
45
+ duplicateLabels: string[];
46
+ appendixStart: number;
47
+ }
48
+ export interface ImageProjection {
49
+ source: string;
50
+ records: {start: number; end: number; id: string; codec: ImageCodec; payload: string; url: string}[];
51
+ references: Record<string, ImageDefinition>;
52
+ }
53
+ export interface AssetInput {label: string; url: string; title?: string;}
54
+ export interface AddressedAsset extends AssetInput {id: string;}
55
+ export interface AssetAppend {
56
+ source: string;
57
+ id: string;
58
+ reference: string;
59
+ added: boolean;
60
+ suffix: string;
61
+ }
62
+ export interface AssetOmission {
63
+ start: number;
64
+ end: number;
65
+ chars: number;
66
+ domain: 'image_bytes';
67
+ reason: 'embedded_asset';
68
+ profile: 'embedded';
69
+ type: string;
70
+ bytes: number | null;
71
+ }
72
+ export interface AssetSourceStore {
73
+ readonly length: number;
74
+ contains(text: string, ignoreCase?: boolean): boolean;
75
+ readSlice(start: number, end: number): string;
76
+ read(): string;
77
+ }
78
+ export const IMAGE_LIMITS: Readonly<{bytes: number; sourceChars: number; dimension: number; pixels: number; assets: number}>;
79
+ /** Sets the default parser and opening-front-matter splitter for this module instance. */
80
+ export function configureParser(parser: ParserFactory | null, split?: ((source: string) => {bodyOffset: number}) | null): void;
81
+ export function normalizeLabel(label: unknown): string;
82
+ export function assetTitle(value: unknown): string;
83
+ export function installMarkdownImages<T extends MarkdownParser>(md: T): T;
84
+ export function markdownParser(factory?: ParserFactory): MarkdownParser;
85
+ export function markdownBodyOffset(source: string): number;
86
+ /** Inspects the URL spelling; does not decode or validate its image pixels. */
87
+ export function dataImage(url: unknown): DataImage | null;
88
+ export function parseAssets(source: string, factory?: ParserFactory, bodyOffset?: number): AssetIndex;
89
+ export function documentAssets(source: string, factory?: ParserFactory): AssetIndex;
90
+ export function packAssetIndex(index: AssetIndex, offset?: number): PackedAssetIndex;
91
+ /** Restores a packet from a trusted parse of this exact source. It is not input validation. */
92
+ export function restoreAssetIndex(source: string, packet: PackedAssetIndex, options?: {offset?: number; bodyOffset?: number; factory?: ParserFactory}): AssetIndex;
93
+ export function projectImageDefinitions(source: string, index: AssetIndex): ImageProjection;
94
+ export function blockwiseAssets<T extends {start: number; raw: string}>(source: string, rows: T[], parse?: (raw: string, row: T) => AssetIndex, bodyOffset?: number): AssetIndex | null;
95
+ export function imageEnvironment(source: string, factory?: ParserFactory): {references: Record<string, ImageDefinition>};
96
+ /** Uncertainty retains the definition: a parser failure returns true. */
97
+ export function referenceOccurs(source: string, id: string, factory?: ParserFactory): boolean;
98
+ export function mayRetireImageDefinitions(source: string | AssetSourceStore, splices: Splice[], factory?: ParserFactory, hint?: {excluded?: {start: number; end: number}}): boolean;
99
+ export function retireDeletedImageDefinitions(before: string, after: string, splices: Splice[], factory?: ParserFactory): Splice[];
100
+ export function assetOmissions(source: string): AssetOmission[];
101
+ export function isAssetBlock(raw: string): boolean;
102
+ export function escapeImageAlt(value: unknown): string;
103
+ export function serializeAsset(asset: AssetInput): string;
104
+ /** Appends or reuses an exact image definition; throws on conflicting labels or limits. */
105
+ export function appendAssetText(source: string, asset: AddressedAsset, lineEnding?: '\n' | '\r\n' | '\r', options?: {preserveLabel?: boolean}): AssetAppend;
106
+ export function appendAsset(source: string, asset: AddressedAsset): Promise<AssetAppend>;
@@ -165,7 +165,6 @@ function fontSource(bytes) {
165
165
 
166
166
  function writeFace(face, source) {
167
167
  const properties = {...face.properties, src: source};
168
- delete properties['unicode-range'];
169
168
  return '@font-face{' + Object.entries(properties).map(([name, value]) => name + ':' + value).join(';') + '}';
170
169
  }
171
170
 
@@ -188,8 +187,29 @@ function fontRuns(root, {settings, css = '', fontCss = '', faces = fontFaces(fon
188
187
  '--md-font-main': settings?.mainfont?.css || settings?.sansfont?.css || FAMILY,
189
188
  '--md-font-mono': settings?.monofont?.css || MONO}};
190
189
  const computed = new WeakMap();
191
- let priorFace = null, priorStyle = '';
192
- const breakRun = () => { priorFace = null; priorStyle = ''; };
190
+ let pending = null, segmenter;
191
+ const breakRun = () => {
192
+ if (!pending) return;
193
+ const {text, families, state} = pending;
194
+ let prior = new Set();
195
+ for (const {segment} of (segmenter ||= new Intl.Segmenter(undefined, {granularity: 'grapheme'})).segment(text)) {
196
+ const selected = new Set();
197
+ for (const character of segment) for (const family of families) {
198
+ const candidates = faces.filter(face => face.family === family && unicodeIncludes(face, character));
199
+ if (!candidates.length) continue;
200
+ candidates.sort((a, b) => stretchOrder(a, b, state['font-stretch']) ||
201
+ styleDistance(a, state['font-style']) - styleDistance(b, state['font-style']) ||
202
+ weightDistance(a, state['font-weight']) - weightDistance(b, state['font-weight']));
203
+ selected.add(candidates[0]); break;
204
+ }
205
+ for (const face of selected) {
206
+ if (prior.has(face)) face.runs[face.runs.length - 1] += segment;
207
+ else face.runs.push(segment);
208
+ }
209
+ prior = selected;
210
+ }
211
+ pending = null;
212
+ };
193
213
  function styleOf(element) {
194
214
  if (computed.has(element)) return computed.get(element);
195
215
  const parent = element === root ? initial : styleOf(element.parentElement || root);
@@ -231,22 +251,10 @@ function fontRuns(root, {settings, css = '', fontCss = '', faces = fontFaces(fon
231
251
  else if (state['text-transform'] === 'capitalize') text = text.replace(/(^|\s)(\p{L})/gu, (_, gap, letter) => gap + letter.toUpperCase());
232
252
  const families = splitCss(state['font-family'], ',').filter(name => /^\s*['"]/.test(name) || !CSS_GENERIC_FONT_FAMILIES.includes(unquote(name).toLowerCase()))
233
253
  .map(name => unquote(name).toLowerCase());
234
- const runStyle = [state['font-weight'], state['font-style'], state['font-stretch']].join('/');
235
- for (const character of text) {
236
- let selected = null;
237
- for (const family of families) {
238
- const candidates = faces.filter(face => face.family === family && unicodeIncludes(face, character));
239
- if (!candidates.length) continue;
240
- candidates.sort((a, b) => stretchOrder(a, b, state['font-stretch']) ||
241
- styleDistance(a, state['font-style']) - styleDistance(b, state['font-style']) ||
242
- weightDistance(a, state['font-weight']) - weightDistance(b, state['font-weight']));
243
- selected = candidates[0]; break;
244
- }
245
- if (!selected) { breakRun(); continue; }
246
- if (selected === priorFace && runStyle === priorStyle) selected.runs[selected.runs.length - 1] += character;
247
- else selected.runs.push(character);
248
- priorFace = selected; priorStyle = runStyle;
249
- }
254
+ const key = JSON.stringify([families, state['font-weight'], state['font-style'], state['font-stretch']]);
255
+ if (pending?.key !== key) breakRun();
256
+ if (pending) pending.text += text;
257
+ else pending = {text, families, state, key};
250
258
  }
251
259
  function walk(element) {
252
260
  const name = element.localName?.toLowerCase();
@@ -277,6 +285,7 @@ function fontRuns(root, {settings, css = '', fontCss = '', faces = fontFaces(fon
277
285
  if (boundary) breakRun();
278
286
  }
279
287
  walk(root);
288
+ breakRun();
280
289
  } finally {
281
290
  if (oldClass == null) root.removeAttribute('class'); else root.setAttribute('class', oldClass);
282
291
  }
@@ -0,0 +1,151 @@
1
+ // SPDX-License-Identifier: MIT
2
+ export {
3
+ INDENT_LIMIT,
4
+ SPACING_FIELDS,
5
+ alignments,
6
+ decodeLayoutAttribute,
7
+ fields,
8
+ formatLayout,
9
+ imageStyle,
10
+ indentLevel,
11
+ linesHeightCss,
12
+ objectHeight,
13
+ parseLayout,
14
+ parseLayoutAttribute,
15
+ stepIndent,
16
+ textLayout,
17
+ textStyle,
18
+ validLayout,
19
+ wrapColumnFloor,
20
+ wrapNeighbour,
21
+ wrapTextBlock,
22
+ wraps
23
+ } from './layout.mjs';
24
+ export {
25
+ FONT_STACKS,
26
+ INK_CLOSE,
27
+ INK_INT_MAX,
28
+ INK_KINDS,
29
+ INK_OPEN_PATTERN,
30
+ INK_PATH_MAX,
31
+ PAGE_BREAK_MARKER,
32
+ TEXT_COLOR_NAMES,
33
+ fontFamilyCss,
34
+ formatColorRun,
35
+ formatFontRun,
36
+ formatInkOpen,
37
+ formatInkOpenFromBody,
38
+ formatInkRun,
39
+ formatPageBreak,
40
+ hasInkMarker,
41
+ inkOpenBody,
42
+ isInkClose,
43
+ isPageBreakBlock,
44
+ isPageBreakLine,
45
+ matchInkOpen,
46
+ mayRetireInk,
47
+ normalizeFontFamily,
48
+ pairInkMarkers,
49
+ pairInkSpans,
50
+ pairMarkers,
51
+ parseInkBody,
52
+ parseInkOpen,
53
+ prepareInkPaste,
54
+ quotedWillSpan,
55
+ readInkMatch,
56
+ scanInkMarkers,
57
+ sliceInkArrows,
58
+ stripInkMarkers,
59
+ typography,
60
+ typographySourceRanges
61
+ } from './marks.mjs';
62
+ export {
63
+ alphaProfile,
64
+ bandsProfile,
65
+ besideSlices,
66
+ boxClearance,
67
+ captureFontAuthority,
68
+ columnsFilled,
69
+ createLineMeasurer,
70
+ createPreparationScope,
71
+ edgeLines,
72
+ edgeMarks,
73
+ flowLines,
74
+ frameScale,
75
+ imageBox,
76
+ imageOnly,
77
+ invalidateRichMetrics,
78
+ layoutNextRichInlineLineRange,
79
+ lineMetrics,
80
+ linesHeight,
81
+ linesSlices,
82
+ linesTop,
83
+ mapFragment,
84
+ materializeRichInlineLineRange,
85
+ parseProfile,
86
+ pictureSlices,
87
+ polygonProfile,
88
+ prepareRichParagraph,
89
+ rasterReserved,
90
+ rasterTiltProfile,
91
+ rotatedBoundsRad,
92
+ rotatedRasterAlpha,
93
+ serializeProfile,
94
+ slotSequence,
95
+ slotsForBand,
96
+ supportsRichStyle,
97
+ wrapShape
98
+ } from './model.mjs';
99
+ export {
100
+ evaluateWill,
101
+ hasWillMarkers,
102
+ parseWill,
103
+ stripOneTerminator,
104
+ terminatorWidth,
105
+ willGovern,
106
+ willIntentOf,
107
+ willMarkerOf,
108
+ willOffsetBefore,
109
+ willRegionsIn,
110
+ willTouchesMarker,
111
+ willTransition
112
+ } from './will.mjs';
113
+ export {
114
+ IMAGE_LIMITS,
115
+ appendAsset,
116
+ appendAssetText,
117
+ assetOmissions,
118
+ assetTitle,
119
+ blockwiseAssets,
120
+ configureParser,
121
+ dataImage,
122
+ documentAssets,
123
+ escapeImageAlt,
124
+ imageEnvironment,
125
+ installMarkdownImages,
126
+ isAssetBlock,
127
+ markdownBodyOffset,
128
+ markdownParser,
129
+ mayRetireImageDefinitions,
130
+ normalizeLabel,
131
+ packAssetIndex,
132
+ parseAssets,
133
+ projectImageDefinitions,
134
+ referenceOccurs,
135
+ restoreAssetIndex,
136
+ retireDeletedImageDefinitions,
137
+ serializeAsset
138
+ } from './assets.mjs';
139
+ export {
140
+ createMarkdownRenderer,
141
+ createPrintRenderer,
142
+ createRenderSanitizer,
143
+ createRenderStyles,
144
+ createRenderer,
145
+ exportFontCss,
146
+ fontRequests,
147
+ requireDocumentFonts,
148
+ waitForFontRequests,
149
+ waitForFonts
150
+ } from './render-entry.mjs';
151
+ export * as ledger from './ledger.mjs';
@@ -0,0 +1,61 @@
1
+ // SPDX-License-Identifier: MIT
2
+ export {};
3
+ export type Alignment = 'left' | 'center' | 'right' | 'justify';
4
+ export type Wrap = 'around' | 'beside' | 'box' | 'behind' | 'front';
5
+ export type LineSpacing = number | `${number}pt` | `${number}pt+`;
6
+ export interface TextLayout {
7
+ first?: number;
8
+ indent?: number;
9
+ marker?: number;
10
+ hyphens?: 'auto';
11
+ caps?: 'small';
12
+ hanging?: 'on';
13
+ before?: number;
14
+ after?: number;
15
+ line?: LineSpacing;
16
+ }
17
+ interface LayoutFacts extends TextLayout {
18
+ rotate?: number;
19
+ opacity?: number;
20
+ lock?: 'on';
21
+ }
22
+ type LayoutSize = {width?: number; lines?: never} | {lines?: number; width?: never};
23
+ type LayoutPosition =
24
+ | {align?: Alignment; wrap?: never; x?: never; y?: never}
25
+ | {align?: never; wrap: Wrap; x?: number; y?: number}
26
+ | {align?: never; wrap?: never; width: number; x?: number; y?: never};
27
+ /** Numeric bounds and precision are checked by validLayout and formatLayout. */
28
+ export type Layout = LayoutFacts & LayoutSize & LayoutPosition;
29
+ export type LayoutField = 'align' | 'width' | 'lines' | 'wrap' | 'x' | 'y' | 'rotate' | 'opacity' |
30
+ 'first' | 'indent' | 'marker' | 'lock' | 'hyphens' | 'caps' | 'hanging' | 'before' | 'after' | 'line';
31
+ export const fields: Set<LayoutField>;
32
+ export const SPACING_FIELDS: readonly ['before', 'after', 'line'];
33
+ export const alignments: Set<Alignment>;
34
+ export const wraps: Set<Wrap>;
35
+ export const INDENT_LIMIT: 64;
36
+ export function indentLevel(value: unknown): value is number;
37
+ export function stepIndent(value: number | null | undefined, step: number): number;
38
+ export function validLayout(value: unknown): value is Layout;
39
+ export function parseLayout(comment: string): Layout | null;
40
+ export function decodeLayoutAttribute(value: string | null | undefined): string;
41
+ export function parseLayoutAttribute(value: string | null | undefined): Layout | null;
42
+ export function textStyle(value: Layout, options?: {portable?: boolean; pointSize?: number; indentOffset?: number}): string;
43
+ export function textLayout(style: CSSStyleDeclaration | Readonly<Record<string, string>> | null | undefined,
44
+ options?: {listMarker?: boolean; pointSize?: number; offset?: number}): TextLayout;
45
+ export function imageStyle(value: Layout): string;
46
+ export function linesHeightCss(lines: number): string;
47
+ export interface ObjectSizeAttributes {
48
+ height?: number | `${number}` | 'auto';
49
+ viewport?: `${number}x${number}`;
50
+ resize?: 'scale' | 'reflow';
51
+ }
52
+ export function objectHeight(width: number, naturalHeight: number, attrs: ObjectSizeAttributes, line: number): number;
53
+ export function wrapTextBlock<T extends Element>(element: T | null | undefined): T | null;
54
+ export type WrapClassification<Stop> = string | false | null | undefined | {stop: Stop};
55
+ export function wrapNeighbour<T extends Element, Stop = T | null>(node: T,
56
+ classify: (node: Element) => WrapClassification<Stop>): Element | Stop | null;
57
+ export function wrapNeighbour<T, Stop = T | null>(node: T,
58
+ classify: (node: T) => WrapClassification<Stop>, step: (node: T, direction: 1 | -1) => T | null | undefined): T | Stop | null;
59
+ export function wrapColumnFloor(fontSize: number | null | undefined): number;
60
+ /** Throws TypeError when the supplied values do not belong to the closed v1 grammar. */
61
+ export function formatLayout(value: Layout): string;
@@ -10,13 +10,19 @@ export function documentTrailer(text: string, parts?: DocumentParts): string;
10
10
  /** Splits a saved file into its exact text and verified parts; throws on a damaged or mismatched comment. */
11
11
  export function readDocument(source: string): ReadParts;
12
12
  export function writeParts(text: string, parts?: DocumentParts): string;
13
- export function readParts(text: string, elements: unknown): {ledger: LedgerEnvelope | null; authorship: AuthorshipEnvelope | null};
13
+ /** Inert browser script elements or equivalent parsed records; no DOM dependency. */
14
+ export interface CarriedElement { id: string; type: string; textContent: string | null; tagName?: string; }
15
+ export function readParts(text: string, elements: Iterable<CarriedElement>): {ledger: LedgerEnvelope | null; authorship: AuthorshipEnvelope | null};
14
16
  export function validateParts(text: string, parts?: DocumentParts): {ledger: LedgerEnvelope | null; authorship: AuthorshipEnvelope | null};
15
- export function takeParts(html: string, text?: string): {html: string; ledger: LedgerEnvelope | null; authorship: AuthorshipEnvelope | null};
17
+ export function takeParts(html: string, text?: string | null): {html: string; ledger: LedgerEnvelope | null; authorship: AuthorshipEnvelope | null};
16
18
  export function encodeCarried(text: string): string;
17
19
  export function decodeCarried(text: string): string;
18
- export interface ComparisonBase { text: string; sha256: string; revision: string; name: string; by: string; at: string; }
19
- export function readBase(value: Partial<ComparisonBase>): ComparisonBase;
20
- export function writeBase(text: string, value: Partial<ComparisonBase>): string;
21
- export function readBaseElements(elements: unknown): ComparisonBase | null;
20
+ export interface ComparisonBase { text: string; sha256: string; revision: string; name: string; by: string; at: string; fonts?: string; }
21
+ export function readBase(value: ComparisonBase): ComparisonBase;
22
+ export function writeBase(text: string, value?: ComparisonBase | null): string;
23
+ export interface ComparisonBaseElement extends CarriedElement {
24
+ getAttribute?: (name: string) => string | null;
25
+ name?: string; sha256?: string; revision?: string; by?: string; at?: string; fonts?: string;
26
+ }
27
+ export function readBaseElements(elements: Iterable<ComparisonBaseElement>): ComparisonBase | null;
22
28
  export function takeBase(html: string): {html: string; base: ComparisonBase | null};
@@ -56,11 +56,19 @@ export function documentTrailer(text, options) {
56
56
  export function writeDocument(text, options) {
57
57
  return text + documentTrailer(text, options);
58
58
  }
59
+ function trailerOpening(source) {
60
+ const marker = '<!-- rapier-carried/1', at = source.lastIndexOf(marker);
61
+ if (at < 0) return null;
62
+ if (source.slice(at - 2, at + marker.length + 1) === OPEN) return {start: at - 2, body: at + marker.length + 1};
63
+ const before = /(?:\r\n|\r|\n){2}$/.exec(source.slice(Math.max(0, at - 4), at));
64
+ const after = /^(?:\r\n|\r|\n)/.exec(source.slice(at + marker.length, at + marker.length + 2));
65
+ return before && after ? {start: at - before[0].length, body: at + marker.length + after[0].length} : null;
66
+ }
59
67
  function unfinishedTrailer(source) {
60
- const at = source.lastIndexOf(OPEN);
61
- if (at < 0) return false;
68
+ const opening = trailerOpening(source);
69
+ if (!opening) return false;
62
70
  let fence = null;
63
- for (const line of source.slice(0, at).split('\n')) {
71
+ for (const line of source.slice(0, opening.start).split(/\r\n?|\n/)) {
64
72
  const found = /^ {0,3}(`{3,}|~{3,})/.exec(line);
65
73
  if (!found) continue;
66
74
  if (!fence) fence = found[1];
@@ -69,17 +77,18 @@ function unfinishedTrailer(source) {
69
77
  return !fence;
70
78
  }
71
79
  export function readDocument(source) {
72
- if (!source.endsWith(CLOSE)) {
80
+ const closing = /(?:\r\n|\r|\n)-->(?:\r\n|\r|\n)$/.exec(source.slice(-7));
81
+ if (!closing) {
73
82
  if (unfinishedTrailer(source)) throw fault('unfinished document metadata');
74
83
  return {text: source, ledger: null, authorship: null};
75
84
  }
76
- const at = source.lastIndexOf(OPEN);
77
- if (at < 0) return {text: source, ledger: null, authorship: null};
85
+ const opening = trailerOpening(source);
86
+ if (!opening) return {text: source, ledger: null, authorship: null};
78
87
  let parts;
79
- try { parts = JSON.parse(source.slice(at + OPEN.length, -CLOSE.length)); }
88
+ try { parts = JSON.parse(source.slice(opening.body, -closing[0].length)); }
80
89
  catch (_) { throw fault('invalid document metadata'); }
81
90
  if (!parts || Object.keys(parts).length !== 2 || !Object.hasOwn(parts, 'ledger') || !Object.hasOwn(parts, 'authorship')) throw fault('invalid document parts');
82
- const text = source.slice(0, at);
91
+ const text = source.slice(0, opening.start);
83
92
  return {text, ...validateParts(text, parts)};
84
93
  }
85
94
 
@@ -170,6 +170,7 @@ const expandedRows = new WeakMap();
170
170
  const sameSplices = (a, b) => a === b || a.length === b.length &&
171
171
  a.every((row, i) => row.pos === b[i].pos && row.removed === b[i].removed && row.inserted === b[i].inserted);
172
172
  const sameEffect = (a, b) => a === b || !!a && !!b && canonicalJSON(a) === canonicalJSON(b);
173
+ const RECENT_TEXT_LIMIT = 1 << 19;
173
174
  let recentLedgers = [];
174
175
  function* produceLedger({text, metadata, records, documentAuthority, revision, root, complete = false, earlier = records} = {}, native = false) {
175
176
  if (!_rapierValidMetadata(metadata)) throw fault('document metadata required');
@@ -239,9 +240,10 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
239
240
  const replayedMetadata = prepared.replayMetadata(startMetadata);
240
241
  if (!replayedMetadata || canonicalJSON(replayedMetadata) !== canonicalJSON(metadata)) throw fault('metadata does not describe this document');
241
242
  const ledger = {...payload, sha256: yield* checksum(envelopeParts(payload), native)};
242
- recentLedgers = [{rows: expanded, headText: text, headMetadata: canonicalJSON(metadata), headBytes, startText, startMetadata: {...startMetadata},
243
- startFacts, startBytes: bytes, documentAuthority, complete: payload.complete, startRoot, startRevision, chainRoot: payload.head.root,
244
- chainRevision: payload.head.revision}, ...recentLedgers.filter(row => row.rows[0] !== expanded[0] && row.documentAuthority === documentAuthority)].slice(0, 4);
243
+ const others = recentLedgers.filter(row => row.rows[0] !== expanded[0] && row.documentAuthority === documentAuthority);
244
+ recentLedgers = text.length + startText.length > RECENT_TEXT_LIMIT ? [] : [{rows: expanded, headText: text, headMetadata: canonicalJSON(metadata),
245
+ headBytes, startText, startMetadata: {...startMetadata}, startFacts, startBytes: bytes, documentAuthority, complete: payload.complete,
246
+ startRoot, startRevision, chainRoot: payload.head.root, chainRevision: payload.head.revision}, ...others].slice(0, 4);
245
247
  ownLedger(ledger);
246
248
  proofs.set(ledger, {shell: Array.from(envelopeParts({...ledger, start: {...ledger.start, text: ''}})).join(''), startText,
247
249
  text, metadata: {...metadata}, root, revision, prepared: _rapierPrepareHistoryRecords(ledger.records),
@@ -70,13 +70,42 @@ function sourceString(node) {
70
70
  }
71
71
  const replacements = new WeakMap(), texts = new WeakMap(), edits = new WeakMap();
72
72
  export const sourceEdit = source => edits.get(source) || null;
73
+ const INITIAL_LEAF_CHARS = 16384;
73
74
  export class HistorySource {
74
75
  #root;
75
- constructor(text) { this.#root = sourceLeaf(text); Object.freeze(this); }
76
+ constructor(text) {
77
+ if (typeof text === 'string' && text.length > INITIAL_LEAF_CHARS) {
78
+ const leaves = [];
79
+ for (let from = 0; from < text.length;) {
80
+ let to = Math.min(text.length, from + INITIAL_LEAF_CHARS);
81
+ if (to < text.length && (text.charCodeAt(to - 1) & 0xfc00) === 0xd800 && (text.charCodeAt(to) & 0xfc00) === 0xdc00) to++;
82
+ leaves.push(sourceLeaf(text, from, to));
83
+ from = to;
84
+ }
85
+ const build = (from, to) => from === to ? null : to - from === 1 ? leaves[from]
86
+ : sourceNode(build(from, (from + to) >> 1), build((from + to) >> 1, to));
87
+ this.#root = build(0, leaves.length);
88
+ } else this.#root = sourceLeaf(text);
89
+ Object.freeze(this);
90
+ }
76
91
  static owns(value) { return value !== null && typeof value === 'object' && #root in value; }
77
92
  static #fromRoot(root) { const source = new HistorySource(''); source.#root = root; return source; }
78
93
  static fromParts(parts) {
79
- const leaves = Array.from(parts, text => sourceLeaf(text)).filter(Boolean);
94
+ const leaves = [];
95
+ for (const text of parts) {
96
+ if (typeof text !== 'string') throw new TypeError('source string required');
97
+ if (text.length <= INITIAL_LEAF_CHARS) {
98
+ const leaf = sourceLeaf(text);
99
+ if (leaf) leaves.push(leaf);
100
+ continue;
101
+ }
102
+ for (let from = 0; from < text.length;) {
103
+ let to = Math.min(text.length, from + INITIAL_LEAF_CHARS);
104
+ if (to < text.length && (text.charCodeAt(to - 1) & 0xfc00) === 0xd800 && (text.charCodeAt(to) & 0xfc00) === 0xdc00) to++;
105
+ leaves.push(sourceLeaf(text, from, to));
106
+ from = to;
107
+ }
108
+ }
80
109
  const build = (from, to) => from === to ? null : to - from === 1 ? leaves[from]
81
110
  : sourceNode(build(from, Math.floor((from + to) / 2)), build(Math.floor((from + to) / 2), to));
82
111
  return HistorySource.#fromRoot(build(0, leaves.length));
@@ -1,5 +1,8 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import {parseWill, stripOneTerminator} from '../../agent/will.mjs';
3
+ import {diff} from '../../packages/rapier-diff/index.mjs';
4
+ import {transposeRecord} from './transport.mjs';
5
+ import {parseComments, commentSplices, derivedCommentEdits} from '../../spec/md-comments.mjs';
3
6
 
4
7
  export function willMergeConflict(base, ours, theirs) {
5
8
  const snapshot = text => {
@@ -450,6 +453,32 @@ function textRefineGroup({before, av, bv}, options) {
450
453
  }
451
454
  return textRefineLevel(before, av, bv, options, 'word');
452
455
  }
456
+ function mergeDerivedComments(base, ours, theirs, options) {
457
+ if (!base.includes('<!-- md-comments:')) return null;
458
+ try {
459
+ const parsed = [base, ours, theirs].map(parseComments);
460
+ if (parsed.some(value => !value.record || value.reason || !value.current)) return null;
461
+ const edits = [ours, theirs].map(text => {
462
+ const changed = diff(base, text, {_unit: 'char'}).edits;
463
+ if (changed.length > 64 || changed.some(row => row.coarse)) return null;
464
+ const rows = changed.map(row => ({pos: row.before[0], removed: base.slice(...row.before),
465
+ inserted: text.slice(...row.after)})).reverse();
466
+ const proof = derivedCommentEdits(base, rows);
467
+ if (!proof) return null;
468
+ const footer = commentSplices(base, proof.edits);
469
+ return footer.length === 1 ? {splices: [...proof.edits, ...footer], derivedCommentIndex: proof.edits.length} : null;
470
+ });
471
+ if (edits.some(value => !value)) return null;
472
+ const body = mergeText(...parsed.map(value => value.body), options);
473
+ if (!body.clean) return null;
474
+ const [oursId, theirsId] = textDeviceOptions(options);
475
+ const moved = transposeRecord(base, edits[1].splices, theirsId,
476
+ [{...edits[0], transaction: {id: oursId}}], edits[1].derivedCommentIndex);
477
+ if (parseComments(moved.text).body !== body.text) return null;
478
+ return {text: moved.text, clean: true, conflicts: []};
479
+ } catch (_) { return null; }
480
+ }
481
+
453
482
  export function mergeText(base, ours, theirs, options = {}) {
454
483
  canonicalNote(ours); canonicalNote(theirs); if (base !== null) canonicalNote(base);
455
484
  const unchanged = value => ({text: value, clean: !value.includes(TEXT_CONFLICT), conflicts: value.includes(TEXT_CONFLICT) ? [{kind: 'existing-conflict', advisory: true}] : []});
@@ -457,6 +486,10 @@ export function mergeText(base, ours, theirs, options = {}) {
457
486
  const willConflict = willMergeConflict(base, ours, theirs);
458
487
  if (!willConflict && base === ours) return unchanged(theirs);
459
488
  if (!willConflict && base === theirs) return unchanged(ours);
489
+ if (!willConflict && base !== null) {
490
+ const commented = mergeDerivedComments(base, ours, theirs, options);
491
+ if (commented) return commented;
492
+ }
460
493
  const conflicts = []; let text = '', a, b;
461
494
  const conflict = (before, a, b, kind) => {
462
495
  const ab = inspectTextConflicts(a), bb = inspectTextConflicts(b);