rapier-markdown-kit 1.1.0

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 (55) hide show
  1. package/LICENSE +40 -0
  2. package/README.md +102 -0
  3. package/dist/agent/vendor/pretext/LICENSE +21 -0
  4. package/dist/agent/vendor/pretext/NOTICE +15 -0
  5. package/dist/agent/vendor/pretext/SOURCE.json +85 -0
  6. package/dist/agent/vendor/pretext/analysis.js +1234 -0
  7. package/dist/agent/vendor/pretext/bidi.js +151 -0
  8. package/dist/agent/vendor/pretext/generated/bidi-data.js +3831 -0
  9. package/dist/agent/vendor/pretext/layout.js +308 -0
  10. package/dist/agent/vendor/pretext/line-break.js +636 -0
  11. package/dist/agent/vendor/pretext/line-text.js +44 -0
  12. package/dist/agent/vendor/pretext/measurement.js +189 -0
  13. package/dist/agent/vendor/pretext/package.json +3 -0
  14. package/dist/agent/vendor/pretext/rich-inline.js +291 -0
  15. package/dist/agent/will.mjs +167 -0
  16. package/dist/kit/assets.mjs +3 -0
  17. package/dist/kit/conformance/01-inline.expected.json +82 -0
  18. package/dist/kit/conformance/01-inline.md +10 -0
  19. package/dist/kit/conformance/02-width-x-align.expected.json +83 -0
  20. package/dist/kit/conformance/02-width-x-align.md +12 -0
  21. package/dist/kit/conformance/03-wrap-around-silhouette.expected.json +177 -0
  22. package/dist/kit/conformance/03-wrap-around-silhouette.md +12 -0
  23. package/dist/kit/conformance/04-wrap-box.expected.json +173 -0
  24. package/dist/kit/conformance/04-wrap-box.md +12 -0
  25. package/dist/kit/conformance/05-behind.expected.json +122 -0
  26. package/dist/kit/conformance/05-behind.md +12 -0
  27. package/dist/kit/conformance/06-front.expected.json +122 -0
  28. package/dist/kit/conformance/06-front.md +12 -0
  29. package/dist/kit/conformance/07-rotate-raster.expected.json +179 -0
  30. package/dist/kit/conformance/07-rotate-raster.md +12 -0
  31. package/dist/kit/conformance/08-drawing-rotation.expected.json +188 -0
  32. package/dist/kit/conformance/08-drawing-rotation.md +12 -0
  33. package/dist/kit/conformance/09-drawing-ring-interior.expected.json +229 -0
  34. package/dist/kit/conformance/09-drawing-ring-interior.md +12 -0
  35. package/dist/kit/conformance/10-both-sides.expected.json +221 -0
  36. package/dist/kit/conformance/10-both-sides.md +10 -0
  37. package/dist/kit/conformance/11-neighbour-skips-image.expected.json +160 -0
  38. package/dist/kit/conformance/11-neighbour-skips-image.md +13 -0
  39. package/dist/kit/conformance/12-heading-barrier.expected.json +164 -0
  40. package/dist/kit/conformance/12-heading-barrier.md +12 -0
  41. package/dist/kit/conformance/13-rtl-text.expected.json +179 -0
  42. package/dist/kit/conformance/13-rtl-text.md +8 -0
  43. package/dist/kit/conformance/README.md +99 -0
  44. package/dist/kit/conformance/measurer.mjs +0 -0
  45. package/dist/kit/conformance/run.mjs +123 -0
  46. package/dist/kit/index.mjs +6 -0
  47. package/dist/kit/layout.mjs +2 -0
  48. package/dist/kit/marks.mjs +2 -0
  49. package/dist/kit/model.mjs +3 -0
  50. package/dist/kit/will.mjs +2 -0
  51. package/dist/layout/model.mjs +364 -0
  52. package/dist/spec/md-assets.mjs +323 -0
  53. package/dist/spec/md-layout.mjs +117 -0
  54. package/dist/spec/md-marks.mjs +78 -0
  55. package/package.json +33 -0
@@ -0,0 +1,189 @@
1
+ /*! Pretext 0.0.9 | MIT | Copyright (c) 2026 Pretext contributors */
2
+ import { getSharedGraphemeSegmenter } from './analysis.js';
3
+ let measureContext = null;
4
+ const segmentMetricCaches = new Map();
5
+ let cachedEngineProfile = null;
6
+ const MAX_PREFIX_FIT_GRAPHEMES = 96;
7
+ const emojiPresentationRe = /\p{Emoji_Presentation}/u;
8
+ const maybeEmojiRe = /[\p{Emoji_Presentation}\p{Extended_Pictographic}\p{Regional_Indicator}\uFE0F\u20E3]/u;
9
+ const emojiCorrectionCache = new Map();
10
+ export function getMeasureContext() {
11
+ if (measureContext !== null) return measureContext;
12
+ if (typeof OffscreenCanvas !== 'undefined') {
13
+ measureContext = new OffscreenCanvas(1, 1).getContext('2d');
14
+ return measureContext;
15
+ }
16
+ if (typeof document !== 'undefined') {
17
+ measureContext = document.createElement('canvas').getContext('2d');
18
+ return measureContext;
19
+ }
20
+ throw new Error('Text measurement requires OffscreenCanvas or a DOM canvas context.');
21
+ }
22
+ export function getSegmentMetricCache(font) {
23
+ let cache = segmentMetricCaches.get(font);
24
+ if (!cache) {
25
+ cache = new Map();
26
+ segmentMetricCaches.set(font, cache);
27
+ }
28
+ return cache;
29
+ }
30
+ export function getSegmentMetrics(seg, cache) {
31
+ let metrics = cache.get(seg);
32
+ if (metrics === undefined) {
33
+ const ctx = getMeasureContext();
34
+ metrics = {
35
+ width: ctx.measureText(seg).width
36
+ };
37
+ cache.set(seg, metrics);
38
+ }
39
+ return metrics;
40
+ }
41
+ export function getEngineProfile() {
42
+ if (cachedEngineProfile !== null) return cachedEngineProfile;
43
+ if (typeof navigator === 'undefined') {
44
+ cachedEngineProfile = {
45
+ geckoAsciiLineBreaks: false,
46
+ lineFitEpsilon: 0.005,
47
+ carryCJKAfterClosingQuote: false,
48
+ breakKeepAllAfterPunctuation: true,
49
+ preferPrefixWidthsForBreakableRuns: false
50
+ };
51
+ return cachedEngineProfile;
52
+ }
53
+ const ua = navigator.userAgent;
54
+ const vendor = navigator.vendor;
55
+ const isSafari = vendor === 'Apple Computer, Inc.' && ua.includes('Safari/') && !ua.includes('Chrome/') && !ua.includes('Chromium/') && !ua.includes('CriOS/') && !ua.includes('FxiOS/') && !ua.includes('EdgiOS/');
56
+ const isChromium = ua.includes('Chrome/') || ua.includes('Chromium/') || ua.includes('CriOS/') || ua.includes('Edg/');
57
+ const isGecko = ua.includes('Firefox/') && !ua.includes('FxiOS/');
58
+ cachedEngineProfile = {
59
+ geckoAsciiLineBreaks: isGecko,
60
+ lineFitEpsilon: isSafari ? 1 / 64 : 0.005,
61
+ carryCJKAfterClosingQuote: isChromium,
62
+ breakKeepAllAfterPunctuation: !isSafari,
63
+ preferPrefixWidthsForBreakableRuns: isSafari
64
+ };
65
+ return cachedEngineProfile;
66
+ }
67
+ export function parseFontSize(font) {
68
+ const m = font.match(/(?:^|\D)(\d+(?:\.\d+)?)\s*px/);
69
+ return m ? parseFloat(m[1]) : 16;
70
+ }
71
+ function isEmojiGrapheme(g) {
72
+ return emojiPresentationRe.test(g) || g.includes('\uFE0F');
73
+ }
74
+ export function textMayContainEmoji(text) {
75
+ return maybeEmojiRe.test(text);
76
+ }
77
+ function getEmojiCorrection(font) {
78
+ let correction = emojiCorrectionCache.get(font);
79
+ if (correction !== undefined) return correction;
80
+ const fontSize = parseFontSize(font);
81
+ const ctx = getMeasureContext();
82
+ ctx.font = font;
83
+ const canvasW = ctx.measureText('\u{1F600}').width;
84
+ correction = 0;
85
+ if (canvasW > fontSize + 0.5 && typeof document !== 'undefined' && document.body !== null) {
86
+ const span = document.createElement('span');
87
+ span.style.font = font;
88
+ span.style.display = 'inline-block';
89
+ span.style.visibility = 'hidden';
90
+ span.style.position = 'absolute';
91
+ span.textContent = '\u{1F600}';
92
+ document.body.appendChild(span);
93
+ const domW = span.getBoundingClientRect().width;
94
+ document.body.removeChild(span);
95
+ if (canvasW - domW > 0.5) {
96
+ correction = canvasW - domW;
97
+ }
98
+ }
99
+ emojiCorrectionCache.set(font, correction);
100
+ return correction;
101
+ }
102
+ function countEmojiGraphemes(text) {
103
+ let count = 0;
104
+ const graphemeSegmenter = getSharedGraphemeSegmenter();
105
+ for (const g of graphemeSegmenter.segment(text)){
106
+ if (isEmojiGrapheme(g.segment)) count++;
107
+ }
108
+ return count;
109
+ }
110
+ function getEmojiCount(seg, metrics) {
111
+ if (metrics.emojiCount === undefined) {
112
+ metrics.emojiCount = countEmojiGraphemes(seg);
113
+ }
114
+ return metrics.emojiCount;
115
+ }
116
+ export function getCorrectedSegmentWidth(seg, metrics, emojiCorrection) {
117
+ if (emojiCorrection === 0) return metrics.width;
118
+ return metrics.width - getEmojiCount(seg, metrics) * emojiCorrection;
119
+ }
120
+ export function getSegmentBreakableFitAdvances(seg, metrics, cache, emojiCorrection, mode) {
121
+ if (metrics.breakableFitAdvances !== undefined && metrics.breakableFitMode === mode) {
122
+ return metrics.breakableFitAdvances;
123
+ }
124
+ metrics.breakableFitMode = mode;
125
+ const graphemeSegmenter = getSharedGraphemeSegmenter();
126
+ const graphemes = [];
127
+ for (const gs of graphemeSegmenter.segment(seg)){
128
+ graphemes.push(gs.segment);
129
+ }
130
+ if (graphemes.length <= 1) {
131
+ metrics.breakableFitAdvances = null;
132
+ return metrics.breakableFitAdvances;
133
+ }
134
+ if (mode === 'sum-graphemes') {
135
+ const advances = [];
136
+ for (const grapheme of graphemes){
137
+ const graphemeMetrics = getSegmentMetrics(grapheme, cache);
138
+ advances.push(getCorrectedSegmentWidth(grapheme, graphemeMetrics, emojiCorrection));
139
+ }
140
+ metrics.breakableFitAdvances = advances;
141
+ return metrics.breakableFitAdvances;
142
+ }
143
+ if (mode === 'pair-context' || graphemes.length > MAX_PREFIX_FIT_GRAPHEMES) {
144
+ const advances = [];
145
+ let previousGrapheme = null;
146
+ let previousWidth = 0;
147
+ for (const grapheme of graphemes){
148
+ const graphemeMetrics = getSegmentMetrics(grapheme, cache);
149
+ const currentWidth = getCorrectedSegmentWidth(grapheme, graphemeMetrics, emojiCorrection);
150
+ if (previousGrapheme === null) {
151
+ advances.push(currentWidth);
152
+ } else {
153
+ const pair = previousGrapheme + grapheme;
154
+ const pairMetrics = getSegmentMetrics(pair, cache);
155
+ advances.push(getCorrectedSegmentWidth(pair, pairMetrics, emojiCorrection) - previousWidth);
156
+ }
157
+ previousGrapheme = grapheme;
158
+ previousWidth = currentWidth;
159
+ }
160
+ metrics.breakableFitAdvances = advances;
161
+ return metrics.breakableFitAdvances;
162
+ }
163
+ const advances = [];
164
+ let prefix = '';
165
+ let prefixWidth = 0;
166
+ for (const grapheme of graphemes){
167
+ prefix += grapheme;
168
+ const prefixMetrics = getSegmentMetrics(prefix, cache);
169
+ const nextPrefixWidth = getCorrectedSegmentWidth(prefix, prefixMetrics, emojiCorrection);
170
+ advances.push(nextPrefixWidth - prefixWidth);
171
+ prefixWidth = nextPrefixWidth;
172
+ }
173
+ metrics.breakableFitAdvances = advances;
174
+ return metrics.breakableFitAdvances;
175
+ }
176
+ export function getFontMeasurementState(font, needsEmojiCorrection) {
177
+ const ctx = getMeasureContext();
178
+ ctx.font = font;
179
+ const cache = getSegmentMetricCache(font);
180
+ const emojiCorrection = needsEmojiCorrection ? getEmojiCorrection(font) : 0;
181
+ return {
182
+ cache,
183
+ emojiCorrection
184
+ };
185
+ }
186
+ export function clearMeasurementCaches() {
187
+ segmentMetricCaches.clear();
188
+ emojiCorrectionCache.clear();
189
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,291 @@
1
+ /*! Pretext 0.0.9 | MIT | Copyright (c) 2026 Pretext contributors */
2
+ import { prepareWithSegments } from './layout.js';
3
+ import { buildLineTextFromRange, getLineTextCache } from './line-text.js';
4
+ import { stepPreparedLineGeometry } from './line-break.js';
5
+ import { getFontMeasurementState, getSegmentMetrics } from './measurement.js';
6
+ const EMPTY_LAYOUT_CURSOR = {
7
+ segmentIndex: 0,
8
+ graphemeIndex: 0
9
+ };
10
+ const RICH_INLINE_START_CURSOR = {
11
+ itemIndex: 0,
12
+ segmentIndex: 0,
13
+ graphemeIndex: 0
14
+ };
15
+ function getInternalPreparedRichInline(prepared) {
16
+ return prepared;
17
+ }
18
+ function cloneCursor(cursor) {
19
+ return {
20
+ segmentIndex: cursor.segmentIndex,
21
+ graphemeIndex: cursor.graphemeIndex
22
+ };
23
+ }
24
+ function isLineStartCursor(cursor) {
25
+ return cursor.segmentIndex === 0 && cursor.graphemeIndex === 0;
26
+ }
27
+ function isCollapsibleBoundaryWhitespace(code) {
28
+ return code === 0x20 || code === 0x09 || code === 0x0A || code === 0x0C || code === 0x0D;
29
+ }
30
+ function getCollapsedSpaceWidth(font, letterSpacing) {
31
+ const { cache } = getFontMeasurementState(font, false);
32
+ return getSegmentMetrics(' ', cache).width + letterSpacing;
33
+ }
34
+ function measureWholeItem(prepared) {
35
+ const end = {
36
+ segmentIndex: 0,
37
+ graphemeIndex: 0
38
+ };
39
+ return stepPreparedLineGeometry(prepared, end, Number.POSITIVE_INFINITY);
40
+ }
41
+ function endsInsideFirstSegment(segmentIndex, graphemeIndex) {
42
+ return segmentIndex === 0 && graphemeIndex > 0;
43
+ }
44
+ export function prepareRichInline(items) {
45
+ const preparedItems = Array.from({
46
+ length: items.length
47
+ });
48
+ let pendingGapWidth = null;
49
+ let breakAfterPreviousItem = false;
50
+ for(let index = 0; index < items.length; index++){
51
+ const item = items[index];
52
+ const letterSpacing = item.letterSpacing ?? 0;
53
+ let start = 0;
54
+ while(start < item.text.length && isCollapsibleBoundaryWhitespace(item.text.charCodeAt(start)))start++;
55
+ if (start === item.text.length) {
56
+ if (start > 0 && pendingGapWidth === null) {
57
+ pendingGapWidth = getCollapsedSpaceWidth(item.font, letterSpacing);
58
+ }
59
+ continue;
60
+ }
61
+ let end = item.text.length;
62
+ while(end > start && isCollapsibleBoundaryWhitespace(item.text.charCodeAt(end - 1)))end--;
63
+ const hasLeadingWhitespace = start > 0;
64
+ const hasTrailingWhitespace = end < item.text.length;
65
+ const trimmedText = item.text.slice(start, end);
66
+ const gapBefore = pendingGapWidth ?? (hasLeadingWhitespace ? getCollapsedSpaceWidth(item.font, letterSpacing) : 0);
67
+ const prepared = prepareWithSegments(trimmedText, item.font, letterSpacing === 0 ? undefined : {
68
+ letterSpacing
69
+ });
70
+ const wholeWidth = measureWholeItem(prepared);
71
+ const establishesLine = wholeWidth !== null || prepared.kinds.includes('zero-width-break');
72
+ const preparedItem = {
73
+ break: item.break ?? 'normal',
74
+ breakBefore: pendingGapWidth !== null || hasLeadingWhitespace || breakAfterPreviousItem,
75
+ establishesLine,
76
+ extraWidth: item.extraWidth ?? 0,
77
+ gapBefore,
78
+ naturalWidth: wholeWidth ?? 0,
79
+ prepared
80
+ };
81
+ preparedItems[index] = preparedItem;
82
+ if (establishesLine) breakAfterPreviousItem = prepared.kinds.at(-1) === 'zero-width-break';
83
+ pendingGapWidth = hasTrailingWhitespace ? getCollapsedSpaceWidth(item.font, letterSpacing) : null;
84
+ }
85
+ return {
86
+ items: preparedItems
87
+ };
88
+ }
89
+ function stepRichInlineLine(flow, maxWidth, cursor, collectFragment) {
90
+ if (flow.items.length === 0 || cursor.itemIndex >= flow.items.length) return null;
91
+ const safeWidth = Math.max(1, maxWidth);
92
+ let hasContent = false;
93
+ let lineWidth = 0;
94
+ let remainingWidth = safeWidth;
95
+ let itemIndex = cursor.itemIndex;
96
+ lineLoop: while(itemIndex < flow.items.length){
97
+ const item = flow.items[itemIndex];
98
+ if (item === undefined) {
99
+ itemIndex++;
100
+ cursor.segmentIndex = 0;
101
+ cursor.graphemeIndex = 0;
102
+ continue;
103
+ }
104
+ if (!isLineStartCursor(cursor) && cursor.segmentIndex === item.prepared.segments.length && cursor.graphemeIndex === 0) {
105
+ itemIndex++;
106
+ cursor.segmentIndex = 0;
107
+ cursor.graphemeIndex = 0;
108
+ continue;
109
+ }
110
+ if (!item.establishesLine) {
111
+ collectFragment?.(itemIndex, 0, 0, cloneCursor(EMPTY_LAYOUT_CURSOR), {
112
+ segmentIndex: item.prepared.segments.length,
113
+ graphemeIndex: 0
114
+ });
115
+ itemIndex++;
116
+ cursor.segmentIndex = 0;
117
+ cursor.graphemeIndex = 0;
118
+ continue;
119
+ }
120
+ const gapBefore = hasContent ? item.gapBefore : 0;
121
+ const atItemStart = isLineStartCursor(cursor);
122
+ if (item.break === 'never') {
123
+ if (!atItemStart) {
124
+ itemIndex++;
125
+ cursor.segmentIndex = 0;
126
+ cursor.graphemeIndex = 0;
127
+ continue;
128
+ }
129
+ const occupiedWidth = item.naturalWidth + item.extraWidth;
130
+ const totalWidth = gapBefore + occupiedWidth;
131
+ if (hasContent && totalWidth > remainingWidth) break lineLoop;
132
+ collectFragment?.(itemIndex, gapBefore, occupiedWidth, cloneCursor(EMPTY_LAYOUT_CURSOR), {
133
+ segmentIndex: item.prepared.segments.length,
134
+ graphemeIndex: 0
135
+ });
136
+ hasContent = true;
137
+ lineWidth += totalWidth;
138
+ remainingWidth = safeWidth - lineWidth;
139
+ itemIndex++;
140
+ cursor.segmentIndex = 0;
141
+ cursor.graphemeIndex = 0;
142
+ continue;
143
+ }
144
+ const reservedWidth = gapBefore + item.extraWidth;
145
+ if (hasContent && reservedWidth > remainingWidth) break lineLoop;
146
+ if (atItemStart) {
147
+ const totalWidth = reservedWidth + item.naturalWidth;
148
+ if (totalWidth <= remainingWidth) {
149
+ collectFragment?.(itemIndex, gapBefore, item.naturalWidth + item.extraWidth, cloneCursor(EMPTY_LAYOUT_CURSOR), {
150
+ segmentIndex: item.prepared.segments.length,
151
+ graphemeIndex: 0
152
+ });
153
+ hasContent = true;
154
+ lineWidth += totalWidth;
155
+ remainingWidth = safeWidth - lineWidth;
156
+ itemIndex++;
157
+ cursor.segmentIndex = 0;
158
+ cursor.graphemeIndex = 0;
159
+ continue;
160
+ }
161
+ }
162
+ const availableWidth = Math.max(1, remainingWidth - reservedWidth);
163
+ const lineEnd = {
164
+ segmentIndex: cursor.segmentIndex,
165
+ graphemeIndex: cursor.graphemeIndex
166
+ };
167
+ const lineWidthForItem = stepPreparedLineGeometry(item.prepared, lineEnd, availableWidth);
168
+ if (lineWidthForItem === null) {
169
+ itemIndex++;
170
+ cursor.segmentIndex = 0;
171
+ cursor.graphemeIndex = 0;
172
+ continue;
173
+ }
174
+ if (cursor.segmentIndex === lineEnd.segmentIndex && cursor.graphemeIndex === lineEnd.graphemeIndex) {
175
+ itemIndex++;
176
+ cursor.segmentIndex = 0;
177
+ cursor.graphemeIndex = 0;
178
+ continue;
179
+ }
180
+ const itemOccupiedWidth = lineWidthForItem + item.extraWidth;
181
+ const lineWidthContribution = gapBefore + itemOccupiedWidth;
182
+ if (hasContent && atItemStart && lineWidthContribution > remainingWidth) break lineLoop;
183
+ if (hasContent && atItemStart && item.breakBefore && endsInsideFirstSegment(lineEnd.segmentIndex, lineEnd.graphemeIndex)) {
184
+ break lineLoop;
185
+ }
186
+ collectFragment?.(itemIndex, gapBefore, itemOccupiedWidth, cloneCursor(cursor), {
187
+ segmentIndex: lineEnd.segmentIndex,
188
+ graphemeIndex: lineEnd.graphemeIndex
189
+ });
190
+ hasContent = true;
191
+ lineWidth += lineWidthContribution;
192
+ remainingWidth = safeWidth - lineWidth;
193
+ if (lineEnd.segmentIndex === item.prepared.segments.length && lineEnd.graphemeIndex === 0) {
194
+ itemIndex++;
195
+ cursor.segmentIndex = 0;
196
+ cursor.graphemeIndex = 0;
197
+ continue;
198
+ }
199
+ cursor.segmentIndex = lineEnd.segmentIndex;
200
+ cursor.graphemeIndex = lineEnd.graphemeIndex;
201
+ break;
202
+ }
203
+ if (!hasContent) return null;
204
+ cursor.itemIndex = itemIndex;
205
+ return lineWidth;
206
+ }
207
+ export function layoutNextRichInlineLineRange(prepared, maxWidth, start = RICH_INLINE_START_CURSOR) {
208
+ const flow = getInternalPreparedRichInline(prepared);
209
+ const end = {
210
+ itemIndex: start.itemIndex,
211
+ segmentIndex: start.segmentIndex,
212
+ graphemeIndex: start.graphemeIndex
213
+ };
214
+ const fragments = [];
215
+ const width = stepRichInlineLine(flow, maxWidth, end, (itemIndex, gapBefore, occupiedWidth, fragmentStart, fragmentEnd)=>{
216
+ fragments.push({
217
+ itemIndex,
218
+ gapBefore,
219
+ occupiedWidth,
220
+ start: fragmentStart,
221
+ end: fragmentEnd
222
+ });
223
+ });
224
+ if (width === null) return null;
225
+ return {
226
+ fragments,
227
+ width,
228
+ end
229
+ };
230
+ }
231
+ function materializeFragmentText(item, fragment) {
232
+ return buildLineTextFromRange(item.prepared, getLineTextCache(item.prepared), fragment.start.segmentIndex, fragment.start.graphemeIndex, fragment.end.segmentIndex, fragment.end.graphemeIndex);
233
+ }
234
+ export function materializeRichInlineLineRange(prepared, line) {
235
+ const flow = getInternalPreparedRichInline(prepared);
236
+ const fragments = [];
237
+ for(let i = 0; i < line.fragments.length; i++){
238
+ const fragment = line.fragments[i];
239
+ const item = flow.items[fragment.itemIndex];
240
+ if (item === undefined) throw new Error('Missing rich-text inline item for fragment');
241
+ fragments.push({
242
+ itemIndex: fragment.itemIndex,
243
+ text: materializeFragmentText(item, fragment),
244
+ gapBefore: fragment.gapBefore,
245
+ occupiedWidth: fragment.occupiedWidth,
246
+ start: fragment.start,
247
+ end: fragment.end
248
+ });
249
+ }
250
+ return {
251
+ fragments,
252
+ width: line.width,
253
+ end: line.end
254
+ };
255
+ }
256
+ export function walkRichInlineLineRanges(prepared, maxWidth, onLine) {
257
+ let lineCount = 0;
258
+ const cursor = {
259
+ ...RICH_INLINE_START_CURSOR
260
+ };
261
+ while(true){
262
+ const line = layoutNextRichInlineLineRange(prepared, maxWidth, cursor);
263
+ if (line === null) return lineCount;
264
+ cursor.itemIndex = line.end.itemIndex;
265
+ cursor.segmentIndex = line.end.segmentIndex;
266
+ cursor.graphemeIndex = line.end.graphemeIndex;
267
+ onLine(line);
268
+ lineCount++;
269
+ }
270
+ }
271
+ export function measureRichInlineStats(prepared, maxWidth) {
272
+ const flow = getInternalPreparedRichInline(prepared);
273
+ let lineCount = 0;
274
+ let maxLineWidth = 0;
275
+ const cursor = {
276
+ itemIndex: 0,
277
+ segmentIndex: 0,
278
+ graphemeIndex: 0
279
+ };
280
+ while(true){
281
+ const lineWidth = stepRichInlineLine(flow, maxWidth, cursor);
282
+ if (lineWidth === null) {
283
+ return {
284
+ lineCount,
285
+ maxLineWidth
286
+ };
287
+ }
288
+ lineCount++;
289
+ if (lineWidth > maxLineWidth) maxLineWidth = lineWidth;
290
+ }
291
+ }
@@ -0,0 +1,167 @@
1
+ // Rapier Will/1 grammar and range laws. SPDX-License-Identifier: MIT.
2
+ const _RAPIER_WILL_INTENT_LIMIT = 512;
3
+
4
+ const _RAPIER_WILL_LAWS = Object.freeze({ edit: true, append: true, keep: true });
5
+ const _RAPIER_WILL_OPENER_PREFIX = '<!-- will/';
6
+ const _RAPIER_WILL_CLOSER_PREFIX = '<!-- /will';
7
+ const _RAPIER_WILL_CLOSER_EXACT = '<!-- /will -->';
8
+ const _RAPIER_WILL_CLOSE_SUFFIX = ' -->';
9
+ const _RAPIER_WILL_UNSPACED_OPENER = '<!--will/';
10
+ const _RAPIER_WILL_UNSPACED_CLOSER = '<!--/will';
11
+
12
+ function hasWillMarkers(text) {
13
+ return /^<!-- ?(?:will\/|\/will)/m.test(String(text == null ? '' : text));
14
+ }
15
+
16
+ function _rapierWillMarkerOf(content) {
17
+ const line = String(content == null ? '' : content);
18
+ if (line.startsWith(_RAPIER_WILL_CLOSER_PREFIX)) {
19
+ if (line !== _RAPIER_WILL_CLOSER_EXACT) {
20
+ return { kind: 'near', fault: 'malformed_marker', law: '', intent: null, content: line };
21
+ }
22
+ return { kind: 'close', law: '', intent: null, content: line };
23
+ }
24
+ if (!line.startsWith(_RAPIER_WILL_OPENER_PREFIX)) {
25
+ if (line.startsWith(_RAPIER_WILL_UNSPACED_OPENER) || line.startsWith(_RAPIER_WILL_UNSPACED_CLOSER)) {
26
+ return { kind: 'near', fault: 'malformed_marker', law: '', intent: null, content: line };
27
+ }
28
+ return null;
29
+ }
30
+ const near = fault => ({ kind: 'near', fault, law: '', intent: null, content: line });
31
+ if (!line.endsWith(_RAPIER_WILL_CLOSE_SUFFIX)) return near('malformed_marker');
32
+ const middle = line.slice(_RAPIER_WILL_OPENER_PREFIX.length,
33
+ line.length - _RAPIER_WILL_CLOSE_SUFFIX.length);
34
+ const gap = middle.indexOf(' ');
35
+ if ((gap < 0 ? middle : middle.slice(0, gap)) !== '1') return near('unknown_version');
36
+ if (gap < 0) return near('malformed_marker');
37
+ const rest = middle.slice(gap + 1);
38
+ const word = (/^[^:\s]*/.exec(rest))[0];
39
+ if (!word) return near('malformed_marker');
40
+ let intent;
41
+ if (rest === word) intent = null;
42
+ else if (rest.startsWith(word + ': ')) intent = rest.slice(word.length + 2);
43
+ else return near('malformed_marker');
44
+ if (!Object.prototype.hasOwnProperty.call(_RAPIER_WILL_LAWS, word)) return near('unknown_law');
45
+ if (intent !== null) {
46
+ if (!intent.length || intent.indexOf('--') >= 0) return near('malformed_marker');
47
+ if ([...intent].length > _RAPIER_WILL_INTENT_LIMIT) return near('intent_over_bound');
48
+ }
49
+ // The words after the colon are document data, never permission (R85b); willGovern alone decides.
50
+ return { kind: 'open', law: word, intent, content: line };
51
+ }
52
+
53
+ // Scan CR, CRLF and LF lines directly into Will's reducer, including a trailing empty line.
54
+ // Offsets remain authored UTF-16 coordinates; no editor helper or whole-document line tables.
55
+ function _rapierWillParse(text) {
56
+ const source = String(text == null ? '' : text);
57
+ const will = {
58
+ present: false, fault: '', faults: [], blocks: [], markers: [], regions: [], text: source,
59
+ };
60
+ let open = null;
61
+ for (let index = 0, start = 0; start <= source.length; index++) {
62
+ let contentEnd = start;
63
+ while (contentEnd < source.length && source.charCodeAt(contentEnd) !== 13 && source.charCodeAt(contentEnd) !== 10) contentEnd++;
64
+ let lineEnd = contentEnd;
65
+ if (lineEnd < source.length) {
66
+ lineEnd++;
67
+ if (source.charCodeAt(contentEnd) === 13 && source.charCodeAt(lineEnd) === 10) lineEnd++;
68
+ }
69
+ const row = { start, contentEnd, lineEnd };
70
+ start = contentEnd < source.length ? lineEnd : source.length + 1;
71
+ const marker = _rapierWillMarkerOf(source.slice(row.start, row.contentEnd));
72
+ if (!marker) continue;
73
+ will.present = true;
74
+ will.blocks.push({ start: row.start, end: row.lineEnd });
75
+ const stamped = {
76
+ ...marker, line: index, start: row.start, end: row.lineEnd, contentEnd: row.contentEnd,
77
+ };
78
+ if (marker.kind === 'near') {
79
+ will.faults.push({ mode: marker.fault, line: index, start: row.start, end: row.lineEnd });
80
+ continue;
81
+ }
82
+ if (marker.kind === 'close') {
83
+ if (!open) {
84
+ will.faults.push({ mode: 'unpaired_marker', line: index, start: row.start, end: row.lineEnd });
85
+ continue;
86
+ }
87
+ will.markers.push(open, stamped);
88
+ will.regions.push({
89
+ index: will.regions.length, law: open.law, intent: open.intent,
90
+ openerLine: open.line, openerStart: open.start, openerEnd: open.end,
91
+ openerContentEnd: open.contentEnd,
92
+ closerLine: index, closerStart: stamped.start, closerEnd: stamped.end,
93
+ start: open.end, end: stamped.start,
94
+ });
95
+ open = null;
96
+ continue;
97
+ }
98
+ if (open) {
99
+ will.faults.push({ mode: 'unpaired_marker', line: index, start: row.start, end: row.lineEnd });
100
+ continue;
101
+ }
102
+ open = stamped;
103
+ }
104
+ if (open) will.faults.push({ mode: 'unpaired_marker', line: open.line, start: open.start, end: open.end });
105
+ will.faults.sort((a, b) => a.start - b.start);
106
+ will.fault = will.faults.length ? will.faults[0].mode : '';
107
+ return will;
108
+ }
109
+
110
+ function _rapierWillRegionsIn(will, start, end) {
111
+ const found = [];
112
+ for (const region of will.regions) {
113
+ const touches = start === end
114
+ ? start >= region.start && start <= region.end
115
+ : start < region.end && region.start < end;
116
+ if (touches) found.push(region);
117
+ }
118
+ return found;
119
+ }
120
+
121
+ function _rapierWillTouchesMarker(will, start, end) {
122
+ const touches = (from, to) => start === end
123
+ ? start > from && start < to
124
+ : start < to && from < end;
125
+ for (const region of will.regions) {
126
+ if (touches(region.openerStart, region.openerEnd) ||
127
+ touches(region.closerStart, region.closerEnd)) return region;
128
+ }
129
+ for (const block of will.blocks) if (touches(block.start, block.end)) return { law: 'keep' };
130
+ return null;
131
+ }
132
+
133
+ function _rapierWillGovern(will, start, end) {
134
+ if (will.faults.length) return 'keep';
135
+ if (_rapierWillTouchesMarker(will, start, end)) return 'keep';
136
+ let law = 'edit';
137
+ for (const region of _rapierWillRegionsIn(will, start, end)) {
138
+ if (region.law === 'keep') return 'keep';
139
+ if (region.law === 'append') law = 'append';
140
+ }
141
+ return law;
142
+ }
143
+
144
+ function _rapierWillIntentOf(will, start, end) {
145
+ if (!will.present || will.faults.length) return '';
146
+ const found = _rapierWillRegionsIn(will, start, end);
147
+ if (found.length !== 1) return '';
148
+ const region = found[0];
149
+ // Disclosed with the region's law, never in place of it: under keep and append the words widen nothing.
150
+ return region.intent && start >= region.start && end <= region.end ? region.intent : '';
151
+ }
152
+
153
+
154
+ function _rapierWillTerminatorWidth(last, prior) {
155
+ if (last === 10) return prior === 13 ? 2 : 1;
156
+ if (last === 13) return 1;
157
+ return 0;
158
+ }
159
+
160
+ function _rapierWillStripOneTerminator(text) {
161
+ const end = text.length;
162
+ return end ? text.slice(0, end - _rapierWillTerminatorWidth(
163
+ text.charCodeAt(end - 1), end >= 2 ? text.charCodeAt(end - 2) : -1)) : text;
164
+ }
165
+
166
+
167
+ export { hasWillMarkers, _rapierWillParse as parseWill, _rapierWillMarkerOf as willMarkerOf, _rapierWillRegionsIn as willRegionsIn, _rapierWillTouchesMarker as willTouchesMarker, _rapierWillGovern as willGovern, _rapierWillIntentOf as willIntentOf, _rapierWillStripOneTerminator as stripOneTerminator, _rapierWillTerminatorWidth as terminatorWidth };
@@ -0,0 +1,3 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Re-export from ../spec/md-assets.mjs only: ../images/assets.mjs pulls AGPL decoders into the MIT graph (tools/check-kit.mjs).
3
+ export * from '../spec/md-assets.mjs';