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.
- package/LICENSE +12 -5
- package/README.md +134 -11
- package/dist/agent/vendor/BROWSER-ENTITIES.json +36 -0
- package/dist/agent/vendor/MARKDOWN-RUNTIME.json +136 -0
- package/dist/agent/vendor/MARKDOWNIT.json +582 -0
- package/dist/agent/vendor/NOTICE.txt +60 -0
- package/dist/agent/vendor/markdown-licenses/entities-LICENSE.txt +11 -0
- package/dist/agent/vendor/markdown-licenses/linkify-it-LICENSE.txt +22 -0
- package/dist/agent/vendor/markdown-licenses/markdown-it-LICENSE.txt +22 -0
- package/dist/agent/vendor/markdown-licenses/mdurl-LICENSE.txt +45 -0
- package/dist/agent/vendor/markdown-licenses/punycode.js-LICENSE-MIT.txt +20 -0
- package/dist/agent/vendor/markdown-licenses/uc.micro-LICENSE.txt +20 -0
- package/dist/agent/vendor/markdown-plugins.json +46 -0
- package/dist/agent/vendor/markdown-plugins.mjs +27 -0
- package/dist/agent/vendor/markdown-runtime.mjs +13 -0
- package/dist/kit/artifact-modules.mjs +19 -0
- package/dist/kit/assets.d.mts +106 -0
- package/dist/kit/export-fonts.mjs +28 -19
- package/dist/kit/index.d.mts +151 -0
- package/dist/kit/layout.d.mts +61 -0
- package/dist/kit/ledger/carried.d.mts +12 -6
- package/dist/kit/ledger/carried.mjs +17 -8
- package/dist/kit/ledger/format.mjs +5 -3
- package/dist/kit/ledger/source.mjs +31 -2
- package/dist/kit/ledger/text-merge.mjs +33 -0
- package/dist/kit/ledger/transport.mjs +61 -2
- package/dist/kit/ledger.d.mts +137 -31
- package/dist/kit/markdown-it.d.mts +2 -2
- package/dist/kit/marks.d.mts +241 -0
- package/dist/kit/marks.mjs +39 -1
- package/dist/kit/model.d.mts +191 -0
- package/dist/kit/pdf-source-digest.mjs +10 -0
- package/dist/kit/pdf-source.mjs +4 -1
- package/dist/kit/pdf-vectors.mjs +3 -2
- package/dist/kit/render-entry.d.mts +654 -0
- package/dist/kit/render-markdown.mjs +14 -8
- package/dist/kit/render-print.mjs +36 -7
- package/dist/kit/render.mjs +16 -14
- package/dist/kit/typography.d.mts +42 -0
- package/dist/kit/will.d.mts +26 -3
- package/dist/layout/initial-range.mjs +18 -0
- package/dist/layout/typography.mjs +2 -18
- package/dist/math/delimiters.mjs +38 -0
- package/dist/math/markdown-rules.mjs +103 -0
- package/dist/packages/rapier-diff/concerns.mjs +2 -2
- package/dist/spec/document-fonts.mjs +62 -16
- package/dist/spec/md-assets.mjs +1 -0
- package/dist/spec/md-comments.mjs +384 -0
- package/dist/spec/md-marks.mjs +9 -0
- package/dist/spec/md-parser-config.mjs +463 -0
- package/dist/spec/md-parser-runtime.mjs +23 -0
- 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
|
|
192
|
-
const breakRun = () => {
|
|
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
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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:
|
|
20
|
-
export function writeBase(text: string, value
|
|
21
|
-
export
|
|
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
|
|
61
|
-
if (
|
|
68
|
+
const opening = trailerOpening(source);
|
|
69
|
+
if (!opening) return false;
|
|
62
70
|
let fence = null;
|
|
63
|
-
for (const line of source.slice(0,
|
|
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
|
-
|
|
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
|
|
77
|
-
if (
|
|
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(
|
|
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,
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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) {
|
|
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 =
|
|
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);
|