rapier-markdown-kit 1.1.32 → 1.1.50
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/README.md +1 -1
- package/dist/kit/conformance/README.md +6 -6
- package/dist/kit/conformance/run.mjs +1 -1
- 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/README.md
CHANGED
|
@@ -217,7 +217,7 @@ node node_modules/rapier-markdown-kit/dist/kit/conformance/run.mjs
|
|
|
217
217
|
|
|
218
218
|
Thirteen small documents, each with the line boxes Rapier's own export produced for it. **Preservation**: every
|
|
219
219
|
layout comment round-trips exactly (12 of 12 that carry one). **Presentation**: how closely the planned lines match
|
|
220
|
-
(
|
|
220
|
+
(3 agree within 2px, 10 are close with a named cause, none differ; `dist/kit/conformance/README.md`).
|
|
221
221
|
`--impl path/to/module.mjs` runs another implementation against the same documents; `--tolerance 2` sets the pixel
|
|
222
222
|
tolerance.
|
|
223
223
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# The conformance kit
|
|
2
2
|
|
|
3
3
|
Thirteen small documents, each covering one placement fact from [the Markdown
|
|
4
|
-
standard](
|
|
4
|
+
standard](https://rapier.website/markdown-standard): inline (no layout comment), width/x/align, `wrap=around`
|
|
5
5
|
with a real alpha silhouette, `wrap=box`, `behind`, `front`, `rotate=` on a plain raster, a foreign
|
|
6
6
|
SVG "drawing" already turned in its own bytes, a ring drawing whose unfilled interior takes a line of
|
|
7
7
|
text, both-sides wrapping around a centred picture, the neighbour search skipping a second, unrelated
|
|
@@ -75,18 +75,18 @@ Prints two separate tables (preservation and presentation are different claims):
|
|
|
75
75
|
- **Presentation** — does the planned line count and each line's left/right agree with the recorded
|
|
76
76
|
fact, within `--tolerance` pixels (default 2). This depends entirely on the measurer supplied;
|
|
77
77
|
`run.mjs`'s own default (`measurer.mjs`, R77) replays Rapier's real Pretext trace wherever one was
|
|
78
|
-
captured, falling back to the character-glyph sum only where none exists. Current result: **
|
|
79
|
-
AGREE outright** (`04-wrap-box`, `
|
|
78
|
+
captured, falling back to the character-glyph sum only where none exists. Current result: **3/13
|
|
79
|
+
AGREE outright** (`04-wrap-box`, `10-both-sides`, `12-heading-barrier` — every
|
|
80
80
|
one a case where the compared edge is pure obstacle-slot geometry, unaffected by text fidelity
|
|
81
|
-
either way), **
|
|
81
|
+
either way), **10 CLOSE, 0 DIFFERS**, each CLOSE row's cause named rather than left as "close
|
|
82
82
|
enough": `01-inline`/`02-width-x-align`/`05-behind`/`06-front` have no real trace at all (see
|
|
83
83
|
`measurements` above) so still rest on the character-sum fallback; `03-wrap-around-silhouette`,
|
|
84
84
|
`11-neighbour-skips-image` and `13-rtl-text` run Pretext for real during export (`13-rtl-text` at
|
|
85
85
|
100% — every query of its own answered from the real trace) yet still carry a small, constant
|
|
86
|
-
left-edge delta (
|
|
86
|
+
left-edge delta (9.64px) that is a picture-obstacle *geometry* fact, not a text one: their
|
|
87
87
|
`profileHint` is a hand-computed, quantized alpha-band descriptor for a picture Rapier's own export
|
|
88
88
|
never serializes an occupancy descriptor for, and that descriptor's own precision — not the
|
|
89
|
-
measurer — is the residual gap. `09-drawing-ring-interior` plans the recorded 14 lines, but one row
|
|
89
|
+
measurer — is the residual gap. `07-rotate-raster` and `08-drawing-rotation` have right-edge geometry deltas of 4.48px and 5.31px, with no measurer calls. `09-drawing-ring-interior` plans the recorded 14 lines, but one row
|
|
90
90
|
differs by a whole slot (115.08px left, 292.09px right): its recorded trace, taken before a word
|
|
91
91
|
that must break inside itself was given the widest slot of its row, cuts "ordinary" into "ordinar"
|
|
92
92
|
and "y" in the ring's narrow left slot, where the planner now leaves that slot empty and sets the
|
|
@@ -114,7 +114,7 @@ console.log(`reads CLOSE rather than AGREE, none of them "close enough": (a) no
|
|
|
114
114
|
console.log(`-- their picture pushes no flow obstacle, so layout/interchange.js's own prepare() never calls Pretext for this paragraph during export (see`);
|
|
115
115
|
console.log(`measurer.mjs) -- the from-scratch character-sum approximation disagrees with real shaping by a bounded, named amount; (b) 03/11/13 DO run`);
|
|
116
116
|
console.log(`Pretext for real (hits > 0, 13-rtl-text at 100% -- every query answered from the real trace) yet still carry a small, constant left-edge delta`);
|
|
117
|
-
console.log(`(
|
|
117
|
+
console.log(`(9.64px today, a constant from before this measurer existed) -- a picture-obstacle geometry fact, not a text one: these`);
|
|
118
118
|
console.log(`fixtures' profileHint is a hand-computed, quantized alpha band descriptor for a raster Rapier's own export never serialized an occupancy`);
|
|
119
119
|
console.log(`descriptor for (conformance/README.md, "profileHint"), and that descriptor's own precision -- not this lane's measurer -- is the residual gap; (c) 09 plans the recorded 14 lines but one row`);
|
|
120
120
|
console.log(`differs by a whole slot: its recorded trace cuts a word in the ring's narrow left slot, and the planner now gives a word that must break inside itself`);
|
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.50",
|
|
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}
|