rapier-markdown-kit 1.1.32 → 1.1.49
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/dist/layout/model.mjs +4 -2
- package/dist/spec/md-marks.mjs +185 -71
- package/package.json +1 -1
- package/style.css +32 -2
package/dist/layout/model.mjs
CHANGED
|
@@ -27,10 +27,12 @@ function normalize(raw) {
|
|
|
27
27
|
return {text: pieces.join(''), offsets, trimStart: start, trimEnd: end};
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export function prepareRun(raw, font, letterSpacing = 0) {
|
|
30
|
+
export function prepareRun(raw, font, letterSpacing = 0, measured = null) {
|
|
31
31
|
if (typeof raw !== 'string' || typeof font !== 'string' || !font.trim() || !finite(letterSpacing)) return null;
|
|
32
32
|
const normalized = normalize(raw);
|
|
33
|
-
|
|
33
|
+
// A paragraph's rich-inline plan has already measured this run. Map those same segments to source
|
|
34
|
+
// instead of measuring and segmenting it again; standalone callers still prepare their own run.
|
|
35
|
+
const prepared = measured || prepareWithSegments(normalized.text, font, {letterSpacing});
|
|
34
36
|
if (!Array.isArray(prepared.segments) || prepared.segments.join('') !== normalized.text) return null;
|
|
35
37
|
const segmentOffsets = [0];
|
|
36
38
|
for (const segment of prepared.segments) segmentOffsets.push(segmentOffsets.at(-1) + segment.length);
|
package/dist/spec/md-marks.mjs
CHANGED
|
@@ -1,12 +1,75 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// The one place these literal grammars are written (docs/markdown-standard.md, "Text colour", "Page break").
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// The exported page retains this reader alone. The editor uses the same closure, then adds its writers and scans.
|
|
5
|
+
const inkReader = () => {
|
|
6
|
+
// SPDX-License-Identifier: MIT
|
|
7
|
+
const TEXT_COLOR_NAMES = Object.freeze({ green: '#1c7547', red: '#b32034', blue: '#175dc4', gold: '#865818', purple: '#7924d7' });
|
|
8
|
+
|
|
9
|
+
const COLOR_NAME_ALT = Object.keys(TEXT_COLOR_NAMES).join('|');
|
|
10
|
+
const COLOR_VALUE = '(?:#[0-9a-f]{6}|' + COLOR_NAME_ALT + ')';
|
|
11
|
+
|
|
12
|
+
// Ink (docs/markdown-standard.md, "Ink"; the design docs/briefs/ink.md): a pen stroke that stays with the words it
|
|
13
|
+
// marks, a paired comment with the colour span's shape. The opener holds the stroke:
|
|
14
|
+
// <!--ink KIND [NUMBER] [COLOUR] [box=W,H] [at=X,Y] [PATH]-->words<!--/ink-->
|
|
15
|
+
// KIND is the stroke's kind, decided once when it was lifted; COLOUR is exactly the colour span's value, red when
|
|
16
|
+
// absent; box is the frame's size when the stroke was drawn and at the frame's offset from the words' box, both in
|
|
17
|
+
// hundredths of an em; PATH is the stroke, its first point absolute in the frame and the rest as moves, hundredths of an
|
|
18
|
+
// em. One spelling: the fields in this order, one space between, integers only. A comment that does not read this way
|
|
19
|
+
// is ordinary comment text and marks nothing. Ink pairs do not nest in each other. Arrow/end alone take a pairing
|
|
20
|
+
// number, an end takes no other data; under, strike and arrow may omit a path for a line derived from their words.
|
|
21
|
+
const INK_KINDS = Object.freeze(['under', 'strike', 'ring', 'bracket', 'free', 'arrow', 'end']);
|
|
22
|
+
|
|
23
|
+
const INK_PATH_MAX = 160;
|
|
24
|
+
const INK_INT_MAX = 999999;
|
|
25
|
+
const INK_INT = '-?\\d{1,6}';
|
|
26
|
+
const INK_OPEN_BODY = '(' + INK_KINDS.join('|') + ')(?: ([1-9]\\d{0,5}))?(?: (' + COLOR_VALUE + '))?(?: box=(\\d{1,6}),(\\d{1,6}))?(?: at=(' + INK_INT + '),(' + INK_INT + '))?'
|
|
27
|
+
+ '(?: (' + INK_INT + ',' + INK_INT + '(?: ' + INK_INT + ',' + INK_INT + ')*))?';
|
|
28
|
+
const INK_OPEN_PATTERN = '<!--ink ' + INK_OPEN_BODY + '-->';
|
|
29
|
+
|
|
30
|
+
// Relocated into the parse Worker by .toString() with parseInkOpen and isInkClose (engine.js _buildParseWorkerSource):
|
|
31
|
+
// reads only its parameter and the planted INK_PATH_MAX, INK_INT_MAX and TEXT_COLOR_NAMES; parseInkOpen reads
|
|
32
|
+
// INK_OPEN_PATTERN and this function; isInkClose reads INK_CLOSE.
|
|
33
|
+
function readInkMatch(match) {
|
|
34
|
+
const linked = match[1] === 'arrow' || match[1] === 'end';
|
|
35
|
+
if (linked !== (match[2] != null) || (!linked && match[1] !== 'under' && match[1] !== 'strike' && match[8] == null)) return null;
|
|
36
|
+
if (match[1] === 'end' && match.slice(3, 9).some(value => value != null)) return null;
|
|
37
|
+
const moves = match[8] == null ? [] : match[8].split(' ').map(pair => pair.split(',').map(Number));
|
|
38
|
+
if (moves.length > INK_PATH_MAX) return null;
|
|
39
|
+
const path = moves.length ? [moves[0]] : [];
|
|
40
|
+
for (let i = 1; i < moves.length; i++) {
|
|
41
|
+
const point = [path[i - 1][0] + moves[i][0], path[i - 1][1] + moves[i][1]];
|
|
42
|
+
if (Math.abs(point[0]) > INK_INT_MAX || Math.abs(point[1]) > INK_INT_MAX) return null;
|
|
43
|
+
path.push(point);
|
|
44
|
+
}
|
|
45
|
+
const colour = match[3] || null;
|
|
46
|
+
return Object.freeze({
|
|
47
|
+
kind: match[1],
|
|
48
|
+
...(linked ? { id: Number(match[2]) } : {}),
|
|
49
|
+
hex: colour == null ? null : (colour.charCodeAt(0) === 0x23 ? colour : TEXT_COLOR_NAMES[colour]),
|
|
50
|
+
box: match[4] == null ? null : [Number(match[4]), Number(match[5])],
|
|
51
|
+
at: match[6] == null ? null : [Number(match[6]), Number(match[7])],
|
|
52
|
+
path,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseInkOpen(comment) {
|
|
57
|
+
if (typeof comment !== 'string') return null;
|
|
58
|
+
const match = new RegExp('^' + INK_OPEN_PATTERN + '$').exec(comment);
|
|
59
|
+
return match && readInkMatch(match);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseInkBody(body) {
|
|
63
|
+
return typeof body === 'string' ? parseInkOpen('<!--ink ' + body + '-->') : null;
|
|
64
|
+
}
|
|
65
|
+
return { TEXT_COLOR_NAMES, COLOR_NAME_ALT, COLOR_VALUE, INK_KINDS, INK_PATH_MAX, INK_INT_MAX, INK_OPEN_PATTERN, readInkMatch, parseInkOpen, parseInkBody };
|
|
66
|
+
};
|
|
67
|
+
const { TEXT_COLOR_NAMES, COLOR_NAME_ALT, COLOR_VALUE, INK_KINDS, INK_PATH_MAX, INK_INT_MAX, INK_OPEN_PATTERN, readInkMatch, parseInkOpen, parseInkBody } = inkReader();
|
|
68
|
+
export { readInkMatch, parseInkOpen, parseInkBody };
|
|
69
|
+
|
|
5
70
|
const TEXT_COLOR_HEX_TO_NAME = Object.freeze(
|
|
6
71
|
Object.fromEntries(Object.entries(TEXT_COLOR_NAMES).map(([name, hex]) => [hex, name]))
|
|
7
72
|
);
|
|
8
|
-
const COLOR_NAME_ALT = Object.keys(TEXT_COLOR_NAMES).join('|');
|
|
9
|
-
const COLOR_VALUE = '(?:#[0-9a-f]{6}|' + COLOR_NAME_ALT + ')';
|
|
10
73
|
const COLOR_CLOSE = '<!--/c-->';
|
|
11
74
|
const PAGE_BREAK_MARKER = '<!--md-break:v1 page-->';
|
|
12
75
|
|
|
@@ -67,24 +130,8 @@ export function hasColorMarker(text) {
|
|
|
67
130
|
return new RegExp('<!--(?:c ' + COLOR_VALUE + '|/c)-->').test(String(text == null ? '' : text));
|
|
68
131
|
}
|
|
69
132
|
|
|
70
|
-
// Ink (docs/markdown-standard.md, "Ink"; the design docs/briefs/ink.md): a pen stroke that stays with the words it
|
|
71
|
-
// marks, a paired comment with the colour span's shape. The opener holds the stroke:
|
|
72
|
-
// <!--ink KIND [COLOUR] [box=W,H] [at=X,Y] PATH-->words<!--/ink-->
|
|
73
|
-
// KIND is the stroke's kind, decided once when it was lifted; COLOUR is exactly the colour span's value, red when
|
|
74
|
-
// absent; box is the frame's size when the stroke was drawn and at the frame's offset from the words' box, both in
|
|
75
|
-
// hundredths of an em; PATH is the stroke, its first point absolute in the frame and the rest as moves, hundredths of an
|
|
76
|
-
// em. One spelling: the fields in this order, one space between, integers only. A comment that does not read this way
|
|
77
|
-
// is ordinary comment text and marks nothing. Ink pairs do not nest in each other.
|
|
78
|
-
const INK_KINDS = Object.freeze(['under', 'strike', 'ring', 'bracket', 'free']);
|
|
79
133
|
const INK_CLOSE = '<!--/ink-->';
|
|
80
|
-
|
|
81
|
-
const INK_INT_MAX = 999999;
|
|
82
|
-
const INK_INT = '-?\\d{1,6}';
|
|
83
|
-
const INK_OPEN_BODY = '(' + INK_KINDS.join('|') + ')(?: (' + COLOR_VALUE + '))?(?: box=(\\d{1,6}),(\\d{1,6}))?(?: at=(' + INK_INT + '),(' + INK_INT + '))?'
|
|
84
|
-
+ ' (' + INK_INT + ',' + INK_INT + '(?: ' + INK_INT + ',' + INK_INT + ')*)';
|
|
85
|
-
const INK_OPEN_PATTERN = '<!--ink ' + INK_OPEN_BODY + '-->';
|
|
86
|
-
|
|
87
|
-
export { INK_KINDS, INK_CLOSE, INK_PATH_MAX, INK_INT_MAX };
|
|
134
|
+
export { INK_KINDS, INK_CLOSE, INK_PATH_MAX, INK_INT_MAX, INK_OPEN_PATTERN };
|
|
88
135
|
|
|
89
136
|
const inkInt = (value, name) => {
|
|
90
137
|
if (!Number.isInteger(value) || Math.abs(value) > INK_INT_MAX) throw new TypeError('ink: ' + name + ' must be an integer within ' + INK_INT_MAX);
|
|
@@ -97,11 +144,18 @@ const inkPair = (value, name, nonNegative = false) => {
|
|
|
97
144
|
return pair;
|
|
98
145
|
};
|
|
99
146
|
|
|
100
|
-
// {kind, hex?, box?, at?, path} to the opener; the path's points are absolute in the frame and become moves after the
|
|
147
|
+
// {kind, id?, hex?, box?, at?, path?} to the opener; the path's points are absolute in the frame and become moves after the
|
|
101
148
|
// first. Refuses with a TypeError rather than writing a spelling the reader would not read.
|
|
102
149
|
export function formatInkOpen(mark) {
|
|
103
150
|
if (!mark || typeof mark !== 'object') throw new TypeError('ink: a mark is an object');
|
|
104
151
|
if (!INK_KINDS.includes(mark.kind)) throw new TypeError('ink: the kind is one of ' + INK_KINDS.join(', '));
|
|
152
|
+
const linked = mark.kind === 'arrow' || mark.kind === 'end';
|
|
153
|
+
if (linked ? !Number.isInteger(mark.id) || mark.id <= 0 || mark.id > INK_INT_MAX : mark.id != null) throw new TypeError('ink: only an arrow or its end has a positive pairing number');
|
|
154
|
+
const id = linked ? ' ' + mark.id : '';
|
|
155
|
+
if (mark.kind === 'end') {
|
|
156
|
+
if (mark.hex != null || mark.box != null || mark.at != null || (mark.path != null && (!Array.isArray(mark.path) || mark.path.length))) throw new TypeError('ink: an arrow end holds only its pairing number');
|
|
157
|
+
return '<!--ink end' + id + '-->';
|
|
158
|
+
}
|
|
105
159
|
let colour = '';
|
|
106
160
|
if (mark.hex != null) {
|
|
107
161
|
const value = TEXT_COLOR_HEX_TO_NAME[mark.hex] || mark.hex;
|
|
@@ -110,36 +164,19 @@ export function formatInkOpen(mark) {
|
|
|
110
164
|
}
|
|
111
165
|
const box = mark.box == null ? '' : ' box=' + inkPair(mark.box, 'box', true).join(',');
|
|
112
166
|
const at = mark.at == null ? '' : ' at=' + inkPair(mark.at, 'at').join(',');
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
167
|
+
const optionalPath = linked || mark.kind === 'under' || mark.kind === 'strike';
|
|
168
|
+
const path = mark.path == null && optionalPath ? [] : mark.path;
|
|
169
|
+
if (!Array.isArray(path) || (!path.length && !optionalPath) || path.length > INK_PATH_MAX) throw new TypeError('ink: the path holds 1 to ' + INK_PATH_MAX + ' points, or is absent on an arrow, underline or strike');
|
|
170
|
+
const points = path.map(point => inkPair(point, 'path'));
|
|
171
|
+
const moves = points.length ? [points[0].join(',')] : [];
|
|
116
172
|
for (let i = 1; i < points.length; i++) moves.push([inkInt(points[i][0] - points[i - 1][0], 'path move'), inkInt(points[i][1] - points[i - 1][1], 'path move')].join(','));
|
|
117
|
-
return '<!--ink ' + mark.kind + colour + box + at + ' ' + moves.join(' ') + '-->';
|
|
173
|
+
return '<!--ink ' + mark.kind + id + colour + box + at + (moves.length ? ' ' + moves.join(' ') : '') + '-->';
|
|
118
174
|
}
|
|
119
175
|
|
|
120
176
|
export function formatInkRun(mark, content) {
|
|
121
177
|
return formatInkOpen(mark) + content + INK_CLOSE;
|
|
122
178
|
}
|
|
123
179
|
|
|
124
|
-
function readInkMatch(match) {
|
|
125
|
-
const moves = match[7].split(' ').map(pair => pair.split(',').map(Number));
|
|
126
|
-
if (moves.length > INK_PATH_MAX) return null;
|
|
127
|
-
const path = [moves[0]];
|
|
128
|
-
for (let i = 1; i < moves.length; i++) {
|
|
129
|
-
const point = [path[i - 1][0] + moves[i][0], path[i - 1][1] + moves[i][1]];
|
|
130
|
-
if (Math.abs(point[0]) > INK_INT_MAX || Math.abs(point[1]) > INK_INT_MAX) return null;
|
|
131
|
-
path.push(point);
|
|
132
|
-
}
|
|
133
|
-
const colour = match[2] || null;
|
|
134
|
-
return Object.freeze({
|
|
135
|
-
kind: match[1],
|
|
136
|
-
hex: colour == null ? null : (colour.charCodeAt(0) === 0x23 ? colour : TEXT_COLOR_NAMES[colour]),
|
|
137
|
-
box: match[3] == null ? null : [Number(match[3]), Number(match[4])],
|
|
138
|
-
at: match[5] == null ? null : [Number(match[5]), Number(match[6])],
|
|
139
|
-
path,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
180
|
// Returns {mark, length in UTF-16 units} or null; not end-anchored.
|
|
144
181
|
export function matchInkOpen(text) {
|
|
145
182
|
if (typeof text !== 'string') return null;
|
|
@@ -149,12 +186,6 @@ export function matchInkOpen(text) {
|
|
|
149
186
|
return mark && { mark, length: match[0].length };
|
|
150
187
|
}
|
|
151
188
|
|
|
152
|
-
export function parseInkOpen(comment) {
|
|
153
|
-
if (typeof comment !== 'string') return null;
|
|
154
|
-
const match = new RegExp('^' + INK_OPEN_PATTERN + '$').exec(comment);
|
|
155
|
-
return match && readInkMatch(match);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
189
|
// The opener's body is what a rendered span carries (a renderer's sanitizer drops an attribute whose value holds the
|
|
159
190
|
// comment's own close): the opener after `<!--ink ` and before `-->`, so the attribute reads `under red box=… 0,95 …`,
|
|
160
191
|
// and only a body that reads as a mark goes back. Read and write through the same grammar, so an invalid body cannot
|
|
@@ -164,9 +195,6 @@ export function inkOpenBody(opener) {
|
|
|
164
195
|
const match = new RegExp('^' + INK_OPEN_PATTERN + '$').exec(opener);
|
|
165
196
|
return match && readInkMatch(match) ? opener.slice(8, -3) : null;
|
|
166
197
|
}
|
|
167
|
-
export function parseInkBody(body) {
|
|
168
|
-
return typeof body === 'string' ? parseInkOpen('<!--ink ' + body + '-->') : null;
|
|
169
|
-
}
|
|
170
198
|
export function formatInkOpenFromBody(body) {
|
|
171
199
|
return parseInkBody(body) ? '<!--ink ' + body + '-->' : null;
|
|
172
200
|
}
|
|
@@ -190,43 +218,129 @@ export function scanInkMarkers(source) {
|
|
|
190
218
|
return markers;
|
|
191
219
|
}
|
|
192
220
|
|
|
193
|
-
//
|
|
221
|
+
// Ink nests so independent strokes can share the same words. A marker that pairs with nothing is text
|
|
194
222
|
// (it shows nothing and marks nothing) and is listed under strays.
|
|
195
|
-
export function pairInkMarkers(source) {
|
|
196
|
-
return pairMarkers(source, 'ink');
|
|
223
|
+
export function pairInkMarkers(source, markers = null) {
|
|
224
|
+
return pairMarkers(source, 'ink', markers);
|
|
197
225
|
}
|
|
198
226
|
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
export function
|
|
227
|
+
// A document parser may supply its semantic markers, leaving literal code comments untouched.
|
|
228
|
+
// Local inline parsing and source planners need an endpoint's span even when its mate is in another block.
|
|
229
|
+
export function pairInkSpans(source, markers = null) {
|
|
230
|
+
return pairSpanMarkers(source, 'ink', markers);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Colour keeps its first opener; ink pairs each closer with the innermost opener. The shared range
|
|
234
|
+
// shape lets custody owners keep every independent stroke, including strokes on identical words.
|
|
235
|
+
export function pairMarkers(source, kind, markers = null) {
|
|
236
|
+
const paired = pairSpanMarkers(source, kind, markers);
|
|
237
|
+
if (kind !== 'ink') return paired;
|
|
238
|
+
const groups = new Map();
|
|
239
|
+
const groupFor = id => {
|
|
240
|
+
if (!groups.has(id)) groups.set(id, { id, tail: null, head: null, count: 0 });
|
|
241
|
+
return groups.get(id);
|
|
242
|
+
};
|
|
243
|
+
for (const run of paired.runs) if (run.mark.id != null) {
|
|
244
|
+
const group = groupFor(run.mark.id);
|
|
245
|
+
group.count++;
|
|
246
|
+
group[run.mark.kind === 'arrow' ? 'tail' : 'head'] = run;
|
|
247
|
+
}
|
|
248
|
+
for (const marker of paired.strays) if (marker.mark?.id != null) groupFor(marker.mark.id).count++;
|
|
249
|
+
const arrows = [...groups.values()].filter(group => group.count === 2 && group.tail && group.head)
|
|
250
|
+
.map(({id, tail, head}) => ({id, tail, head}));
|
|
251
|
+
const complete = new Set(arrows.map(arrow => arrow.id)), runs = [], strays = paired.strays.slice();
|
|
252
|
+
for (const run of paired.runs) {
|
|
253
|
+
if (run.mark.id == null || complete.has(run.mark.id)) { runs.push(run); continue; }
|
|
254
|
+
strays.push({ start: run.start, end: run.innerStart, mark: run.mark, close: false },
|
|
255
|
+
{ start: run.innerEnd, end: run.end, mark: null, close: true, arrowId: run.mark.id });
|
|
256
|
+
}
|
|
257
|
+
strays.sort((a, b) => a.start - b.start);
|
|
258
|
+
return { runs, strays, arrows };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function pairSpanMarkers(source, kind, markers = null) {
|
|
202
262
|
if (kind !== 'ink' && kind !== 'color') throw new TypeError('Unknown paired marker kind');
|
|
203
263
|
const color = kind === 'color';
|
|
204
|
-
const runs = [], strays = [];
|
|
205
|
-
|
|
206
|
-
for (const marker of color ? scanColorMarkers(source) : scanInkMarkers(source)) {
|
|
264
|
+
const runs = [], strays = [], stack = [];
|
|
265
|
+
for (const marker of markers || (color ? scanColorMarkers(source) : scanInkMarkers(source))) {
|
|
207
266
|
if (color ? marker.hex !== null : !marker.close) {
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
if (color) continue;
|
|
211
|
-
}
|
|
212
|
-
open = marker;
|
|
267
|
+
if (color && stack.length) { strays.push(marker); continue; }
|
|
268
|
+
stack.push(marker);
|
|
213
269
|
continue;
|
|
214
270
|
}
|
|
271
|
+
const open = stack.pop();
|
|
215
272
|
if (!open) { strays.push(marker); continue; }
|
|
216
273
|
runs.push({ start: open.start, end: marker.end, innerStart: open.end, innerEnd: marker.start,
|
|
217
274
|
...(color ? { hex: open.hex } : { mark: open.mark }) });
|
|
218
|
-
open = null;
|
|
219
275
|
}
|
|
220
|
-
|
|
276
|
+
strays.push(...stack);
|
|
277
|
+
runs.sort((a, b) => a.start - b.start);
|
|
278
|
+
strays.sort((a, b) => a.start - b.start);
|
|
221
279
|
return { runs, strays };
|
|
222
280
|
}
|
|
223
281
|
|
|
282
|
+
// Copy carries an arrow only when both complete anchor words are selected. A partial selection removes just
|
|
283
|
+
// its endpoint comments from the copied slice, including a comment clipped at the selection's boundary.
|
|
284
|
+
export function sliceInkArrows(source, start = 0, end = String(source ?? '').length, markers = null) {
|
|
285
|
+
const text = String(source ?? '');
|
|
286
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start || end > text.length) throw new RangeError('ink: invalid source slice');
|
|
287
|
+
const kept = new Set();
|
|
288
|
+
for (const arrow of pairInkMarkers(text, markers).arrows) {
|
|
289
|
+
if (![arrow.tail, arrow.head].every(run => run.innerStart >= start && run.innerEnd <= end && run.innerStart < run.innerEnd)) continue;
|
|
290
|
+
kept.add(arrow.id);
|
|
291
|
+
start = Math.min(start, arrow.tail.start, arrow.head.start);
|
|
292
|
+
end = Math.max(end, arrow.tail.end, arrow.head.end);
|
|
293
|
+
}
|
|
294
|
+
const spans = pairInkSpans(text, markers), cuts = [];
|
|
295
|
+
for (const run of spans.runs) if (run.mark.id != null && !kept.has(run.mark.id)) cuts.push(
|
|
296
|
+
{ start: run.start, end: run.innerStart }, { start: run.innerEnd, end: run.end });
|
|
297
|
+
for (const marker of spans.strays) if (marker.mark?.id != null) cuts.push(marker);
|
|
298
|
+
let result = '', at = start;
|
|
299
|
+
for (const cut of cuts.sort((a, b) => a.start - b.start)) {
|
|
300
|
+
if (cut.end <= at || cut.start >= end) continue;
|
|
301
|
+
result += text.slice(at, Math.max(at, cut.start));
|
|
302
|
+
at = Math.min(end, cut.end);
|
|
303
|
+
}
|
|
304
|
+
return result + text.slice(at, end);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// The paste owns only its incoming markers: an orphan cannot adopt an existing document's endpoint, and a
|
|
308
|
+
// complete pair with an occupied number gets a new number in both openers without rewriting its stroke bytes.
|
|
309
|
+
export function prepareInkPaste(source, existing = '', markers = null) {
|
|
310
|
+
const text = String(source ?? ''), paired = pairInkMarkers(text, markers), edits = [];
|
|
311
|
+
for (const marker of paired.strays) if (marker.mark?.id != null || marker.arrowId != null) edits.push({ start: marker.start, end: marker.end, text: '' });
|
|
312
|
+
const occupied = new Set(scanInkMarkers(existing).filter(marker => marker.mark?.id != null).map(marker => marker.mark.id));
|
|
313
|
+
const reserved = new Set([...occupied, ...paired.arrows.map(arrow => arrow.id)]);
|
|
314
|
+
let next = 1;
|
|
315
|
+
for (const arrow of paired.arrows) {
|
|
316
|
+
const runs = [arrow.tail, arrow.head];
|
|
317
|
+
if (runs.some(run => run.innerStart === run.innerEnd)) {
|
|
318
|
+
for (const run of runs) edits.push({ start: run.start, end: run.innerStart, text: '' }, { start: run.innerEnd, end: run.end, text: '' });
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (!occupied.has(arrow.id)) continue;
|
|
322
|
+
while (reserved.has(next) && next <= INK_INT_MAX) next++;
|
|
323
|
+
if (next > INK_INT_MAX) throw new RangeError('ink: no unused arrow pairing number');
|
|
324
|
+
reserved.add(next);
|
|
325
|
+
for (const run of runs) {
|
|
326
|
+
const prefix = '<!--ink ' + run.mark.kind + ' ';
|
|
327
|
+
edits.push({ start: run.start + prefix.length, end: run.start + prefix.length + String(arrow.id).length, text: String(next) });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
let result = text;
|
|
331
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
|
|
224
335
|
export function stripInkMarkers(text) {
|
|
225
|
-
|
|
336
|
+
const source = String(text == null ? '' : text);
|
|
337
|
+
let result = '', at = 0;
|
|
338
|
+
for (const marker of scanInkMarkers(source)) { result += source.slice(at, marker.start); at = marker.end; }
|
|
339
|
+
return result + source.slice(at);
|
|
226
340
|
}
|
|
227
341
|
|
|
228
342
|
export function hasInkMarker(text) {
|
|
229
|
-
return
|
|
343
|
+
return scanInkMarkers(text).length > 0;
|
|
230
344
|
}
|
|
231
345
|
|
|
232
346
|
// The only spelling.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-markdown-kit",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.49",
|
|
4
4
|
"description": "Read, write and style Self-contained Markdown: the universal stylesheet, picture layout, text marks, picture appendix, line planner and Will/1 markers. MIT, no dependencies.",
|
|
5
5
|
"author": "Jack Skipworth",
|
|
6
6
|
"license": "MIT",
|
package/style.css
CHANGED
|
@@ -471,7 +471,8 @@
|
|
|
471
471
|
.md-render ol>li::before{
|
|
472
472
|
content:counter(rapier-ol);
|
|
473
473
|
position:absolute;
|
|
474
|
-
|
|
474
|
+
/* The circle is centred on the item's first line: the li's own padding, plus half of what the 2rem circle is taller than one line. */
|
|
475
|
+
inset-inline-start:-2.75rem; top:calc(0.15rem + (var(--md-line) - 2rem) / 2);
|
|
475
476
|
width:2rem; height:2rem;
|
|
476
477
|
display:flex; align-items:center; justify-content:center;
|
|
477
478
|
border:2px solid var(--md-color-line);
|
|
@@ -483,7 +484,8 @@
|
|
|
483
484
|
box-sizing:border-box;
|
|
484
485
|
z-index:1;
|
|
485
486
|
}
|
|
486
|
-
.md-render ol>li:not(:last-child)::after
|
|
487
|
+
.md-render ol>li:not(:last-child)::after,
|
|
488
|
+
.md-render ol[data-rapier-list-continues]>li:last-child::after{
|
|
487
489
|
content:'';
|
|
488
490
|
position:absolute;
|
|
489
491
|
|
|
@@ -493,6 +495,20 @@
|
|
|
493
495
|
border-inline-start:3px dotted var(--md-color-line);
|
|
494
496
|
z-index:0;
|
|
495
497
|
}
|
|
498
|
+
/* A heading between two parts of one numbered list (the second part's start follows the first's last number;
|
|
499
|
+
_rapierMarkListContinuations marks them): the heading stands on the margin the item text stands on, and the dotted
|
|
500
|
+
line runs on, out of the first part's last item, down the heading's own height, to the next circle. */
|
|
501
|
+
.md-render ol[data-rapier-list-continues]>li:last-child::after{bottom:calc(-1 * var(--md-line))}
|
|
502
|
+
.md-render :is(h1,h2,h3,h4,h5,h6)[data-rapier-list-through]{position:relative;padding-inline-start:2.75rem}
|
|
503
|
+
.md-render :is(h1,h2,h3,h4,h5,h6)[data-rapier-list-through]::before{
|
|
504
|
+
content:'';
|
|
505
|
+
position:absolute;
|
|
506
|
+
inset-inline-start:calc(1rem - 1.5px);
|
|
507
|
+
top:0;
|
|
508
|
+
bottom:calc(var(--md-line) / -2);
|
|
509
|
+
border-inline-start:3px dotted var(--md-color-line);
|
|
510
|
+
z-index:0;
|
|
511
|
+
}
|
|
496
512
|
.md-render li{margin:0 0 calc(var(--md-line) / 4)}
|
|
497
513
|
.md-render li:last-child{margin-bottom:0}
|
|
498
514
|
.md-render li>ul,.md-render li>ol{margin:calc(var(--md-line) / 4) 0 0}
|
|
@@ -556,6 +572,7 @@
|
|
|
556
572
|
}
|
|
557
573
|
.md-render strong{font-weight:var(--md-fw-semibold)}
|
|
558
574
|
.md-render em{font-style:italic}
|
|
575
|
+
.md-render :not(pre)>code{line-height:1}
|
|
559
576
|
.md-render code{font-family:var(--md-font-mono);font-size:0.85em;
|
|
560
577
|
background:var(--md-color-inset);border:1px solid var(--md-color-border);padding:1px 5px}
|
|
561
578
|
.md-render pre{margin:0 0 var(--md-line);padding:var(--md-space-3) var(--md-space-4);
|
|
@@ -755,6 +772,12 @@
|
|
|
755
772
|
pre-computed --md-color-dark here, so the common case survives even a round-trip that kept
|
|
756
773
|
data-md-color but dropped style, exactly like highlight's own swatch constants. */
|
|
757
774
|
.md-render [data-md-color]{--md-color-dark:var(--md-color);color:light-dark(var(--md-color),var(--md-color-dark))}
|
|
775
|
+
/* Ink (docs/markdown-standard.md, "Ink"): the span shows nothing; the ink layer draws the stroke over the words it marks. */
|
|
776
|
+
.md-render span.rapier-ink-mark{all:unset;display:inline}
|
|
777
|
+
.rapier-ink-layer{--md-ink:#b32034;--md-ink-dark:#b32034;position:relative;display:block;width:0;height:0;margin:0;padding:0;border:0;overflow:visible;pointer-events:none;z-index:3}
|
|
778
|
+
.rapier-ink-layer svg{position:absolute;display:block;margin:0;padding:0;overflow:visible}
|
|
779
|
+
.rapier-ink-layer path{fill:light-dark(var(--md-ink),var(--md-ink-dark));stroke:none;opacity:.92}
|
|
780
|
+
@media print{.rapier-ink-layer path{fill:var(--md-ink)}}
|
|
758
781
|
.md-render [data-md-color="#1c7547"]{--md-color-dark:#70c390}
|
|
759
782
|
.md-render [data-md-color="#b32034"]{--md-color-dark:#ff8385}
|
|
760
783
|
.md-render [data-md-color="#175dc4"]{--md-color-dark:#7bafff}
|
|
@@ -799,6 +822,13 @@
|
|
|
799
822
|
/* A wide table scrolls inside its wrap; the wrap carries the block's line, so a scrollbar sits under the table, not a line below it. */
|
|
800
823
|
.md-render .table-scroll-wrap{width:100%;margin:0 0 var(--md-line);overflow-x:auto;-webkit-overflow-scrolling:touch;padding-bottom:2px}
|
|
801
824
|
.md-render .table-scroll-wrap table{min-width:100%;margin-bottom:0}
|
|
825
|
+
/* Single-axis overflow leaves vertical sticky positioning with the document's scroller.
|
|
826
|
+
Older engines keep the ordinary sideways wrapper; screen-only rules leave print unchanged. */
|
|
827
|
+
@media screen{
|
|
828
|
+
.md-render{--md-table-header-top:0px}
|
|
829
|
+
.md-render .table-scroll-wrap{overflow:auto clip}
|
|
830
|
+
.md-render thead{position:sticky;top:var(--md-table-header-top,0px);z-index:2;background:var(--md-color-bg)}
|
|
831
|
+
}
|
|
802
832
|
@media print{
|
|
803
833
|
.md-render [data-md-break]{border:0!important;height:0!important;margin:0!important;break-before:page;page-break-before:always}
|
|
804
834
|
.md-render [data-md-break]::after{content:none!important}
|