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.
- package/LICENSE +40 -0
- package/README.md +102 -0
- package/dist/agent/vendor/pretext/LICENSE +21 -0
- package/dist/agent/vendor/pretext/NOTICE +15 -0
- package/dist/agent/vendor/pretext/SOURCE.json +85 -0
- package/dist/agent/vendor/pretext/analysis.js +1234 -0
- package/dist/agent/vendor/pretext/bidi.js +151 -0
- package/dist/agent/vendor/pretext/generated/bidi-data.js +3831 -0
- package/dist/agent/vendor/pretext/layout.js +308 -0
- package/dist/agent/vendor/pretext/line-break.js +636 -0
- package/dist/agent/vendor/pretext/line-text.js +44 -0
- package/dist/agent/vendor/pretext/measurement.js +189 -0
- package/dist/agent/vendor/pretext/package.json +3 -0
- package/dist/agent/vendor/pretext/rich-inline.js +291 -0
- package/dist/agent/will.mjs +167 -0
- package/dist/kit/assets.mjs +3 -0
- package/dist/kit/conformance/01-inline.expected.json +82 -0
- package/dist/kit/conformance/01-inline.md +10 -0
- package/dist/kit/conformance/02-width-x-align.expected.json +83 -0
- package/dist/kit/conformance/02-width-x-align.md +12 -0
- package/dist/kit/conformance/03-wrap-around-silhouette.expected.json +177 -0
- package/dist/kit/conformance/03-wrap-around-silhouette.md +12 -0
- package/dist/kit/conformance/04-wrap-box.expected.json +173 -0
- package/dist/kit/conformance/04-wrap-box.md +12 -0
- package/dist/kit/conformance/05-behind.expected.json +122 -0
- package/dist/kit/conformance/05-behind.md +12 -0
- package/dist/kit/conformance/06-front.expected.json +122 -0
- package/dist/kit/conformance/06-front.md +12 -0
- package/dist/kit/conformance/07-rotate-raster.expected.json +179 -0
- package/dist/kit/conformance/07-rotate-raster.md +12 -0
- package/dist/kit/conformance/08-drawing-rotation.expected.json +188 -0
- package/dist/kit/conformance/08-drawing-rotation.md +12 -0
- package/dist/kit/conformance/09-drawing-ring-interior.expected.json +229 -0
- package/dist/kit/conformance/09-drawing-ring-interior.md +12 -0
- package/dist/kit/conformance/10-both-sides.expected.json +221 -0
- package/dist/kit/conformance/10-both-sides.md +10 -0
- package/dist/kit/conformance/11-neighbour-skips-image.expected.json +160 -0
- package/dist/kit/conformance/11-neighbour-skips-image.md +13 -0
- package/dist/kit/conformance/12-heading-barrier.expected.json +164 -0
- package/dist/kit/conformance/12-heading-barrier.md +12 -0
- package/dist/kit/conformance/13-rtl-text.expected.json +179 -0
- package/dist/kit/conformance/13-rtl-text.md +8 -0
- package/dist/kit/conformance/README.md +99 -0
- package/dist/kit/conformance/measurer.mjs +0 -0
- package/dist/kit/conformance/run.mjs +123 -0
- package/dist/kit/index.mjs +6 -0
- package/dist/kit/layout.mjs +2 -0
- package/dist/kit/marks.mjs +2 -0
- package/dist/kit/model.mjs +3 -0
- package/dist/kit/will.mjs +2 -0
- package/dist/layout/model.mjs +364 -0
- package/dist/spec/md-assets.mjs +323 -0
- package/dist/spec/md-layout.mjs +117 -0
- package/dist/spec/md-marks.mjs +78 -0
- 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,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 };
|