rapier-html 1.2.31 → 1.2.33
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/BUILD.json +4 -4
- package/README.md +9 -9
- package/dist/ledger/carried.mjs +14 -2
- package/dist/ledger/document-settings.mjs +429 -0
- package/dist/ledger/frontmatter.mjs +29 -0
- package/dist/ledger/md-marks.mjs +731 -0
- package/dist/ledger/md-source.mjs +153 -0
- package/dist/ledger/template-page.mjs +2 -4
- package/dist/rapier.html +51 -51
- package/dist/wrap.mjs +8 -2
- package/package.json +1 -1
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {markdownSourcePositions} from './md-source.mjs';
|
|
3
|
+
|
|
4
|
+
const inkReader = () => {
|
|
5
|
+
// SPDX-License-Identifier: MIT
|
|
6
|
+
const TEXT_COLOR_NAMES = Object.freeze({ green: '#1c7547', red: '#b32034', blue: '#175dc4', gold: '#865818', purple: '#7924d7' });
|
|
7
|
+
|
|
8
|
+
const COLOR_NAME_ALT = Object.keys(TEXT_COLOR_NAMES).join('|');
|
|
9
|
+
const COLOR_VALUE = '(?:#[0-9a-f]{6}|' + COLOR_NAME_ALT + ')';
|
|
10
|
+
|
|
11
|
+
const INK_KINDS = Object.freeze(['under', 'strike', 'ring', 'bracket', 'free', 'arrow', 'end']);
|
|
12
|
+
|
|
13
|
+
const INK_PATH_MAX = 160;
|
|
14
|
+
const INK_INT_MAX = 999999;
|
|
15
|
+
const INK_INT = '-?\\d{1,6}';
|
|
16
|
+
const INK_OPEN_BODY = '(' + INK_KINDS.join('|') + ')(?: ([1-9]\\d{0,5}))?(?: (' + COLOR_VALUE + '))?(?: w=([1-9]\\d?))?(?: box=(\\d{1,6}),(\\d{1,6}))?(?: at=(' + INK_INT + '),(' + INK_INT + '))?'
|
|
17
|
+
+ '(?: (' + INK_INT + ',' + INK_INT + '(?: ' + INK_INT + ',' + INK_INT + ')*))?';
|
|
18
|
+
const INK_OPEN_PATTERN = '<!--ink ' + INK_OPEN_BODY + '-->';
|
|
19
|
+
|
|
20
|
+
function readInkMatch(match) {
|
|
21
|
+
const linked = match[1] === 'arrow' || match[1] === 'end';
|
|
22
|
+
if (linked !== (match[2] != null) || (!linked && match[1] !== 'under' && match[1] !== 'strike' && match[9] == null)) return null;
|
|
23
|
+
if (match[1] === 'end' && match.slice(3, 10).some(value => value != null)) return null;
|
|
24
|
+
const width = match[4] == null ? null : Number(match[4]);
|
|
25
|
+
if (width != null && (width < 2 || width > 24 || width === 9)) return null;
|
|
26
|
+
const moves = match[9] == null ? [] : match[9].split(' ').map(pair => pair.split(',').map(Number));
|
|
27
|
+
if (moves.length > INK_PATH_MAX) return null;
|
|
28
|
+
const path = moves.length ? [moves[0]] : [];
|
|
29
|
+
for (let i = 1; i < moves.length; i++) {
|
|
30
|
+
const point = [path[i - 1][0] + moves[i][0], path[i - 1][1] + moves[i][1]];
|
|
31
|
+
if (Math.abs(point[0]) > INK_INT_MAX || Math.abs(point[1]) > INK_INT_MAX) return null;
|
|
32
|
+
path.push(point);
|
|
33
|
+
}
|
|
34
|
+
const colour = match[3] || null;
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
kind: match[1],
|
|
37
|
+
...(linked ? { id: Number(match[2]) } : {}),
|
|
38
|
+
hex: colour == null ? null : (colour.charCodeAt(0) === 0x23 ? colour : TEXT_COLOR_NAMES[colour]),
|
|
39
|
+
...(width == null ? {} : { width }),
|
|
40
|
+
box: match[5] == null ? null : [Number(match[5]), Number(match[6])],
|
|
41
|
+
at: match[7] == null ? null : [Number(match[7]), Number(match[8])],
|
|
42
|
+
path,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseInkOpen(comment) {
|
|
47
|
+
if (typeof comment !== 'string') return null;
|
|
48
|
+
const match = new RegExp('^' + INK_OPEN_PATTERN + '$').exec(comment);
|
|
49
|
+
return match && readInkMatch(match);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseInkBody(body) {
|
|
53
|
+
return typeof body === 'string' ? parseInkOpen('<!--ink ' + body + '-->') : null;
|
|
54
|
+
}
|
|
55
|
+
return { TEXT_COLOR_NAMES, COLOR_NAME_ALT, COLOR_VALUE, INK_KINDS, INK_PATH_MAX, INK_INT_MAX, INK_OPEN_PATTERN, readInkMatch, parseInkOpen, parseInkBody };
|
|
56
|
+
};
|
|
57
|
+
const { TEXT_COLOR_NAMES, COLOR_VALUE, INK_KINDS, INK_PATH_MAX, INK_INT_MAX, INK_OPEN_PATTERN, readInkMatch, parseInkOpen, parseInkBody } = inkReader();
|
|
58
|
+
export { readInkMatch, parseInkOpen, parseInkBody };
|
|
59
|
+
|
|
60
|
+
const TEXT_COLOR_HEX_TO_NAME = Object.freeze(
|
|
61
|
+
Object.fromEntries(Object.entries(TEXT_COLOR_NAMES).map(([name, hex]) => [hex, name]))
|
|
62
|
+
);
|
|
63
|
+
const PAGE_BREAK_MARKER = '<!--md-break:v1 page-->';
|
|
64
|
+
|
|
65
|
+
export { TEXT_COLOR_NAMES, PAGE_BREAK_MARKER };
|
|
66
|
+
|
|
67
|
+
export function formatColorRun(hex, content) {
|
|
68
|
+
return typography.wrap({color: hex}, content);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const FONT_STACKS = Object.freeze({
|
|
72
|
+
sans: Object.freeze({stack: 'sans', css: '', word: ''}),
|
|
73
|
+
serif: Object.freeze({stack: 'serif', css: "Georgia,'Palatino Linotype',Palatino,'Times New Roman',Times,serif", word: 'Georgia'}),
|
|
74
|
+
mono: Object.freeze({stack: 'mono', css: "ui-monospace,'Courier New',Courier,monospace", word: 'Courier New'}),
|
|
75
|
+
system: Object.freeze({stack: 'system', css: 'system-ui,sans-serif', word: 'Calibri'}),
|
|
76
|
+
hand: Object.freeze({stack: 'hand', css: 'cursive', word: 'Segoe Print'}),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export function normalizeFontFamily(value) {
|
|
80
|
+
if (typeof value !== 'string') return null;
|
|
81
|
+
const family = value.trim();
|
|
82
|
+
return family && family.length <= 256 && family.isWellFormed() && !/[\u0000-\u001f\u007f<>\u2028\u2029]/.test(family) && !family.includes('--') ? family : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function fontFamilyCss(value) {
|
|
86
|
+
const family = normalizeFontFamily(value);
|
|
87
|
+
if (family == null) return '';
|
|
88
|
+
const generic = Object.hasOwn(FONT_STACKS, family.toLowerCase()) ? FONT_STACKS[family.toLowerCase()] : null;
|
|
89
|
+
return generic ? generic.css || "'Geist',system-ui,sans-serif" : '"' + family.replace(/[\\"]/g, '\\$&') + '"';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function formatFontRun(family, content) {
|
|
93
|
+
return typography.wrap({family}, content);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createTypography({normalizeFontFamily, fontFamilyCss, FONT_STACKS}) {
|
|
97
|
+
const LIMIT = 4096, DEPTH = 64;
|
|
98
|
+
const generics = {serif: 'serif', 'sans-serif': 'sans', monospace: 'mono', cursive: 'hand',
|
|
99
|
+
fantasy: 'fantasy', 'system-ui': 'system', 'ui-serif': 'ui-serif', 'ui-sans-serif': 'ui-sans-serif',
|
|
100
|
+
'ui-monospace': 'ui-monospace', 'ui-rounded': 'ui-rounded', emoji: 'emoji', math: 'math', fangsong: 'fangsong'};
|
|
101
|
+
const escape = value => String(value).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
102
|
+
function size(value) {
|
|
103
|
+
if (typeof value !== 'string' || value.length > 48) return null;
|
|
104
|
+
const match = /^(\d+(?:\.\d{0,12})?|\.\d{1,12})\s*(pt|px)$/i.exec(value.trim());
|
|
105
|
+
if (!match) return null;
|
|
106
|
+
const number = Number(match[1]), unit = match[2].toLowerCase(), points = number * (unit === 'px' ? .75 : 1);
|
|
107
|
+
if (!(points > 0 && points <= 1638)) return null;
|
|
108
|
+
const [integer, fraction = ''] = match[1].split('.'), tail = fraction.replace(/0+$/, '');
|
|
109
|
+
return Object.freeze({css: (integer.replace(/^0+/, '') || '0') + (tail ? '.' + tail : '') + unit,
|
|
110
|
+
unit, value: number, points, halfPoints: points * 2});
|
|
111
|
+
}
|
|
112
|
+
function color(value) {
|
|
113
|
+
if (typeof value !== 'string') return null;
|
|
114
|
+
if (/^#[0-9a-f]{6}$/i.test(value.trim())) return value.trim().toLowerCase();
|
|
115
|
+
const rgb = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*1\s*)?\)$/i.exec(value.trim());
|
|
116
|
+
return rgb && rgb.slice(1).every(channel => Number(channel) <= 255)
|
|
117
|
+
? '#' + rgb.slice(1).map(channel => Number(channel).toString(16).padStart(2, '0')).join('') : null;
|
|
118
|
+
}
|
|
119
|
+
function familyCss(value) {
|
|
120
|
+
if (typeof value !== 'string' || value.length > 2048) return null;
|
|
121
|
+
const css = value.trim();
|
|
122
|
+
const stackKey = text => text.replace(/'/g, '"').replace(/"([\w-]+)"/g, '$1').replace(/\s*,\s*/g, ',');
|
|
123
|
+
for (const [name] of Object.entries(FONT_STACKS)) if ((!/^["']/.test(css) || css.includes(',')) && stackKey(css) === stackKey(fontFamilyCss(name)))
|
|
124
|
+
return Object.freeze({name, css: fontFamilyCss(name), custom: false});
|
|
125
|
+
if (Object.hasOwn(generics, css.toLowerCase())) return Object.freeze({name: generics[css.toLowerCase()], css: css.toLowerCase(), custom: false});
|
|
126
|
+
let name = css;
|
|
127
|
+
if (css[0] === '"' || css[0] === "'") {
|
|
128
|
+
if (css.at(-1) !== css[0]) return null;
|
|
129
|
+
name = '';
|
|
130
|
+
for (let at = 1; at < css.length - 1; at++) {
|
|
131
|
+
if (css[at] === css[0] || /[\r\n\f]/.test(css[at])) return null;
|
|
132
|
+
if (css[at] !== '\\') { name += css[at]; continue; }
|
|
133
|
+
const escaped = /^\\(?:([0-9a-f]{1,6})(?:\r\n|[ \t\r\n\f])?|([^\r\n\f]))/i.exec(css.slice(at));
|
|
134
|
+
if (!escaped || at + escaped[0].length > css.length - 1) return null;
|
|
135
|
+
const code = escaped[1] ? parseInt(escaped[1], 16) : 0;
|
|
136
|
+
name += escaped[1] ? code && code <= 0x10ffff && !(code >= 0xd800 && code <= 0xdfff) ? String.fromCodePoint(code) : '\ufffd' : escaped[2];
|
|
137
|
+
at += escaped[0].length - 1;
|
|
138
|
+
}
|
|
139
|
+
} else if (!/^[\p{L}_-][\p{L}\p{N}_ -]*$/u.test(css) || /^(?:inherit|initial|unset|revert|revert-layer)$/i.test(css)) return null;
|
|
140
|
+
name = normalizeFontFamily(name);
|
|
141
|
+
if (name && Object.hasOwn(FONT_STACKS, name.toLowerCase())) return null;
|
|
142
|
+
return name ? Object.freeze({name, css: '"' + name.replace(/[\\"]/g, '\\$&') + '"', custom: true}) : null;
|
|
143
|
+
}
|
|
144
|
+
function family(value) {
|
|
145
|
+
if (value && typeof value === 'object') {
|
|
146
|
+
const parsed = familyCss(value.css);
|
|
147
|
+
return parsed && parsed.name === value.name && parsed.custom === value.custom ? parsed : null;
|
|
148
|
+
}
|
|
149
|
+
const name = normalizeFontFamily(value);
|
|
150
|
+
return name ? familyCss(fontFamilyCss(name)) : null;
|
|
151
|
+
}
|
|
152
|
+
function normalize(value, removals = false) {
|
|
153
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('Typography requires size, color or family.');
|
|
154
|
+
const result = {};
|
|
155
|
+
for (const key of Object.keys(value)) {
|
|
156
|
+
if (!['size', 'color', 'family'].includes(key)) throw new TypeError('Unknown typography property: ' + key);
|
|
157
|
+
if (value[key] === null && removals) { result[key] = null; continue; }
|
|
158
|
+
const parsed = key === 'size' ? size(value[key])?.css : key === 'color' ? color(value[key]) : family(value[key]);
|
|
159
|
+
if (!parsed) throw new TypeError('Invalid typography ' + key + '.');
|
|
160
|
+
result[key] = parsed;
|
|
161
|
+
}
|
|
162
|
+
return Object.freeze(result);
|
|
163
|
+
}
|
|
164
|
+
function parseStyle(value, editable = false) {
|
|
165
|
+
if (typeof value !== 'string' || value.length > LIMIT) return editable ? null : Object.freeze({});
|
|
166
|
+
const parts = []; let quote = '', start = 0;
|
|
167
|
+
for (let at = 0; at <= value.length; at++) {
|
|
168
|
+
const ch = value[at];
|
|
169
|
+
if (quote) { if (ch === '\\') at++; else if (ch === quote) quote = ''; }
|
|
170
|
+
else if (ch === '"' || ch === "'") quote = ch;
|
|
171
|
+
else if (ch === ';' || at === value.length) { parts.push(value.slice(start, at)); start = at + 1; }
|
|
172
|
+
}
|
|
173
|
+
if (quote || parts.length > 32) return editable ? null : Object.freeze({});
|
|
174
|
+
const result = {};
|
|
175
|
+
for (const part of parts) {
|
|
176
|
+
if (!part.trim()) continue;
|
|
177
|
+
const colon = part.indexOf(':'); if (colon < 0) { if (editable) return null; continue; }
|
|
178
|
+
const key = part.slice(0, colon).trim().toLowerCase(), raw = part.slice(colon + 1).trim();
|
|
179
|
+
const parsed = key === 'font-size' ? size(raw)?.css : key === 'color' ? color(raw) : key === 'font-family' ? familyCss(raw) : null;
|
|
180
|
+
if (parsed) result[key === 'font-size' ? 'size' : key === 'font-family' ? 'family' : 'color'] = parsed;
|
|
181
|
+
else if (editable) return null;
|
|
182
|
+
}
|
|
183
|
+
return Object.freeze(result);
|
|
184
|
+
}
|
|
185
|
+
function style(value) {
|
|
186
|
+
const held = normalize(value), parts = [];
|
|
187
|
+
if (held.size) parts.push('font-size:' + held.size);
|
|
188
|
+
if (held.color) parts.push('color:' + held.color);
|
|
189
|
+
if (held.family) parts.push('font-family:' + held.family.css);
|
|
190
|
+
return parts.join(';');
|
|
191
|
+
}
|
|
192
|
+
const open = value => { const css = style(value); return css ? '<span style="' + escape(css) + '">' : ''; };
|
|
193
|
+
const wrap = (value, content) => {
|
|
194
|
+
const start = open(value);
|
|
195
|
+
if (!start || !content) return content;
|
|
196
|
+
const prefix = /^(?:[ \t]*(?:\r\n|\r(?!\n)|\n))+/.exec(content)?.[0] || '';
|
|
197
|
+
let body = content.slice(prefix.length);
|
|
198
|
+
const suffix = /(?:[ \t]*(?:\r\n|\r(?!\n)|\n))+[ \t]*$/.exec(body)?.[0] || '';
|
|
199
|
+
body = body.slice(0, body.length - suffix.length);
|
|
200
|
+
if (!body) return content;
|
|
201
|
+
if (/^\s*\n|\n[ \t]*\n/.test(body.replace(/\r\n?/g, '\n')))
|
|
202
|
+
throw new TypeError('Typography wraps one inline paragraph at a time.');
|
|
203
|
+
return prefix + start + body + '</span>' + suffix;
|
|
204
|
+
};
|
|
205
|
+
function parseOpen(source, decode = value => value, editable = false) {
|
|
206
|
+
if (typeof source !== 'string' || source.length > LIMIT + 1024 || !/^<span(?=[\s>])/i.test(source) || !source.endsWith('>') || /\/\s*>$/.test(source)) return null;
|
|
207
|
+
const attrs = /\s+([^\s=<>/"']+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s=<>`"']+)))?/y;
|
|
208
|
+
let at = 5, css = null;
|
|
209
|
+
while (at < source.length - 1) {
|
|
210
|
+
if (/^\s*>$/.test(source.slice(at))) break;
|
|
211
|
+
attrs.lastIndex = at; const match = attrs.exec(source);
|
|
212
|
+
if (!match) return null;
|
|
213
|
+
at = attrs.lastIndex;
|
|
214
|
+
if (match[1].toLowerCase() !== 'style') { if (editable) return null; continue; }
|
|
215
|
+
if (css !== null) return null;
|
|
216
|
+
css = decode(match[2] ?? match[3] ?? match[4] ?? '');
|
|
217
|
+
}
|
|
218
|
+
const facts = parseStyle(css, editable);
|
|
219
|
+
return facts && Object.keys(facts).length ? facts : null;
|
|
220
|
+
}
|
|
221
|
+
function nodeStyle(node) {
|
|
222
|
+
const result = {...parseStyle(node?.getAttribute?.('style') || '')};
|
|
223
|
+
const authoredSize = size(node?.getAttribute?.('data-md-size'));
|
|
224
|
+
const authoredColor = color(node?.getAttribute?.('data-md-color'));
|
|
225
|
+
const authoredFamily = normalizeFontFamily(node?.getAttribute?.('data-md-font'));
|
|
226
|
+
if (authoredSize) result.size = authoredSize.css;
|
|
227
|
+
if (authoredColor) result.color = authoredColor;
|
|
228
|
+
if (authoredFamily && !result.family) result.family = family(authoredFamily);
|
|
229
|
+
return Object.freeze(result);
|
|
230
|
+
}
|
|
231
|
+
function runs(root) {
|
|
232
|
+
let text = ''; const paint = [], leaves = [];
|
|
233
|
+
const visit = (node, inherited, blocked) => {
|
|
234
|
+
if (node.nodeType === 3) {
|
|
235
|
+
const value = String(node.nodeValue || '').replace(/\u2060/g, '\n'), start = text.length;
|
|
236
|
+
text += value;
|
|
237
|
+
for (let index = 0; index < value.length; index++) paint.push(inherited);
|
|
238
|
+
if (value) leaves.push({start, end: text.length, blocked});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (node.nodeType !== 1 && node !== root) return;
|
|
242
|
+
const own = nodeStyle(node), next = {...inherited, ...own};
|
|
243
|
+
const literal = blocked || /^(?:CODE|PRE|KBD|SAMP|MATH)$/.test(node.tagName) ||
|
|
244
|
+
node.classList?.contains('math-rendered') || node.classList?.contains('math-placeholder');
|
|
245
|
+
for (const child of node.childNodes || []) visit(child, next, !!literal);
|
|
246
|
+
};
|
|
247
|
+
visit(root, {}, false);
|
|
248
|
+
return {text, paint, leaves};
|
|
249
|
+
}
|
|
250
|
+
function install(md) {
|
|
251
|
+
if (md.__rapierTypography) return md;
|
|
252
|
+
md.__rapierTypography = true;
|
|
253
|
+
md.core.ruler.after('linkify', 'rapier-typography', state => {
|
|
254
|
+
for (let at = 0; at < state.tokens.length; at++) {
|
|
255
|
+
const block = state.tokens[at];
|
|
256
|
+
if (block.type !== 'inline' || !block.children || block.content.indexOf('<') < 0) continue;
|
|
257
|
+
const stack = []; let depth = 0, overflow = 0;
|
|
258
|
+
for (const token of block.children) {
|
|
259
|
+
if (token.type !== 'html_inline') { depth += token.nesting || 0; continue; }
|
|
260
|
+
const source = token.content, close = /^<\/span\s*>$/i.test(source);
|
|
261
|
+
if (!close && !/^<span(?=[\s>])/i.test(source)) continue;
|
|
262
|
+
if (overflow) { overflow += close ? -1 : 1; continue; }
|
|
263
|
+
if (!close) {
|
|
264
|
+
if (stack.length === DEPTH) { overflow = 1; continue; }
|
|
265
|
+
const facts = parseOpen(source, value => md.utils.unescapeAll(value.replace(/\\/g, '\\\\')));
|
|
266
|
+
stack.push({token, facts, depth, source}); continue;
|
|
267
|
+
}
|
|
268
|
+
const held = stack.pop();
|
|
269
|
+
if (!held || !held.facts || depth !== held.depth) continue;
|
|
270
|
+
const type = 'typography', opener = held.token;
|
|
271
|
+
opener.type = 'rapier_' + type + '_open'; opener.tag = 'span'; opener.nesting = 1; opener.content = '';
|
|
272
|
+
(opener.meta ||= {}).typography = Object.freeze({style: held.facts, open: held.source, close: source});
|
|
273
|
+
opener.attrSet('style', style(held.facts));
|
|
274
|
+
if (held.facts.color) opener.attrSet('data-md-color', held.facts.color);
|
|
275
|
+
if (held.facts.size) opener.attrSet('data-md-size', held.facts.size);
|
|
276
|
+
if (held.facts.family) opener.attrSet('data-md-font', held.facts.family.name);
|
|
277
|
+
token.type = 'rapier_' + type + '_close'; token.tag = 'span'; token.nesting = -1; token.content = '';
|
|
278
|
+
(token.meta ||= {}).typography = opener.meta.typography;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
return md;
|
|
283
|
+
}
|
|
284
|
+
return Object.freeze({size, color, family, familyCss, normalize, parseStyle, style, open, wrap, parseOpen, nodeStyle, runs, install});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export const typography = createTypography({normalizeFontFamily, fontFamilyCss, FONT_STACKS});
|
|
288
|
+
export function typographySourceRanges(source, parser) { return markdownSourcePositions(source, parser).typography; }
|
|
289
|
+
|
|
290
|
+
const INK_CLOSE = '<!--/ink-->';
|
|
291
|
+
export { INK_KINDS, INK_CLOSE, INK_PATH_MAX, INK_INT_MAX, INK_OPEN_PATTERN };
|
|
292
|
+
|
|
293
|
+
const inkInt = (value, name) => {
|
|
294
|
+
if (!Number.isInteger(value) || Math.abs(value) > INK_INT_MAX) throw new TypeError('ink: ' + name + ' must be an integer within ' + INK_INT_MAX);
|
|
295
|
+
return value;
|
|
296
|
+
};
|
|
297
|
+
const inkPair = (value, name, nonNegative = false) => {
|
|
298
|
+
if (!Array.isArray(value) || value.length !== 2) throw new TypeError('ink: ' + name + ' must be a pair of integers');
|
|
299
|
+
const pair = [inkInt(value[0], name), inkInt(value[1], name)];
|
|
300
|
+
if (nonNegative && (pair[0] < 0 || pair[1] < 0)) throw new TypeError('ink: ' + name + ' must not be negative');
|
|
301
|
+
return pair;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export function formatInkOpen(mark) {
|
|
305
|
+
if (!mark || typeof mark !== 'object') throw new TypeError('ink: a mark is an object');
|
|
306
|
+
if (!INK_KINDS.includes(mark.kind)) throw new TypeError('ink: the kind is one of ' + INK_KINDS.join(', '));
|
|
307
|
+
const linked = mark.kind === 'arrow' || mark.kind === 'end';
|
|
308
|
+
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');
|
|
309
|
+
const id = linked ? ' ' + mark.id : '';
|
|
310
|
+
if (mark.kind === 'end') {
|
|
311
|
+
if (mark.hex != null || mark.width != 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');
|
|
312
|
+
return '<!--ink end' + id + '-->';
|
|
313
|
+
}
|
|
314
|
+
let colour = '';
|
|
315
|
+
if (mark.hex != null) {
|
|
316
|
+
const value = TEXT_COLOR_HEX_TO_NAME[mark.hex] || mark.hex;
|
|
317
|
+
if (!new RegExp('^' + COLOR_VALUE + '$').test(value)) throw new TypeError('ink: the color is the color span\'s value');
|
|
318
|
+
colour = ' ' + value;
|
|
319
|
+
}
|
|
320
|
+
if (mark.width != null && (!Number.isInteger(mark.width) || mark.width < 2 || mark.width > 24)) throw new TypeError('ink: the width is an integer from 2 to 24, and 9 is spelled by leaving it out');
|
|
321
|
+
const width = mark.width == null || mark.width === 9 ? '' : ' w=' + mark.width;
|
|
322
|
+
const box = mark.box == null ? '' : ' box=' + inkPair(mark.box, 'box', true).join(',');
|
|
323
|
+
const at = mark.at == null ? '' : ' at=' + inkPair(mark.at, 'at').join(',');
|
|
324
|
+
const optionalPath = linked || mark.kind === 'under' || mark.kind === 'strike';
|
|
325
|
+
const path = mark.path == null && optionalPath ? [] : mark.path;
|
|
326
|
+
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');
|
|
327
|
+
const points = path.map(point => inkPair(point, 'path'));
|
|
328
|
+
const moves = points.length ? [points[0].join(',')] : [];
|
|
329
|
+
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(','));
|
|
330
|
+
return '<!--ink ' + mark.kind + id + colour + width + box + at + (moves.length ? ' ' + moves.join(' ') : '') + '-->';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function formatInkRun(mark, content) {
|
|
334
|
+
return formatInkOpen(mark) + content + INK_CLOSE;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function matchInkOpen(text) {
|
|
338
|
+
if (typeof text !== 'string') return null;
|
|
339
|
+
const match = new RegExp('^' + INK_OPEN_PATTERN).exec(text);
|
|
340
|
+
if (!match) return null;
|
|
341
|
+
const mark = readInkMatch(match);
|
|
342
|
+
return mark && { mark, length: match[0].length };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function inkOpenBody(opener) {
|
|
346
|
+
if (typeof opener !== 'string') return null;
|
|
347
|
+
const match = new RegExp('^' + INK_OPEN_PATTERN + '$').exec(opener);
|
|
348
|
+
return match && readInkMatch(match) ? opener.slice(8, -3) : null;
|
|
349
|
+
}
|
|
350
|
+
export function formatInkOpenFromBody(body) {
|
|
351
|
+
return parseInkBody(body) ? '<!--ink ' + body + '-->' : null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function isInkClose(comment) {
|
|
355
|
+
return comment === INK_CLOSE;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function mayRetireInk(source, rows) {
|
|
359
|
+
if (source.contains('<!--ink arrow ') || source.contains('<!--ink end ') || source.contains('--><!--')) return true;
|
|
360
|
+
let pieces = [{from: 0, length: source.length}], length = source.length;
|
|
361
|
+
const slice = (start, end) => {
|
|
362
|
+
const selected = [];
|
|
363
|
+
let at = 0;
|
|
364
|
+
for (const piece of pieces) {
|
|
365
|
+
const low = Math.max(start, at), high = Math.min(end, at + piece.length);
|
|
366
|
+
if (high > low) selected.push(piece.text === undefined
|
|
367
|
+
? {from: piece.from + low - at, length: high - low}
|
|
368
|
+
: {text: piece.text.slice(low - at, high - at), length: high - low});
|
|
369
|
+
at += piece.length;
|
|
370
|
+
if (at >= end) break;
|
|
371
|
+
}
|
|
372
|
+
return selected;
|
|
373
|
+
};
|
|
374
|
+
const read = (start, end) => slice(start, end).map(piece => piece.text === undefined
|
|
375
|
+
? source.readSlice(piece.from, piece.from + piece.length) : piece.text).join('');
|
|
376
|
+
for (const row of rows) {
|
|
377
|
+
const end = row.pos + row.removed.length;
|
|
378
|
+
if (row.pos < 0 || end > length) return true;
|
|
379
|
+
const local = read(Math.max(0, row.pos - 6), row.pos) + row.inserted + read(end, Math.min(length, end + 6));
|
|
380
|
+
if (local.includes('--><!--')) return true;
|
|
381
|
+
pieces = [...slice(0, row.pos), {text: row.inserted, length: row.inserted.length}, ...slice(end, length)];
|
|
382
|
+
length += row.inserted.length - row.removed.length;
|
|
383
|
+
}
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function scanInkMarkers(source) {
|
|
388
|
+
const text = String(source == null ? '' : source);
|
|
389
|
+
const scan = new RegExp(INK_OPEN_PATTERN + '|' + INK_CLOSE.replace(/[-]/g, '\\-'), 'g');
|
|
390
|
+
const markers = [];
|
|
391
|
+
let match;
|
|
392
|
+
while ((match = scan.exec(text))) {
|
|
393
|
+
if (match[0] === INK_CLOSE) { markers.push({ start: match.index, end: match.index + match[0].length, mark: null, close: true }); continue; }
|
|
394
|
+
const mark = readInkMatch(match);
|
|
395
|
+
if (mark) markers.push({ start: match.index, end: match.index + match[0].length, mark, close: false });
|
|
396
|
+
}
|
|
397
|
+
return markers;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function pairInkMarkers(source, markers = null) {
|
|
401
|
+
return pairMarkers(source, 'ink', markers);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function pairInkSpans(source, markers = null) {
|
|
405
|
+
return pairSpanMarkers(source, 'ink', markers);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function pairMarkers(source, kind, markers = null) {
|
|
409
|
+
const paired = pairSpanMarkers(source, kind, markers);
|
|
410
|
+
if (kind !== 'ink') return paired;
|
|
411
|
+
const groups = new Map();
|
|
412
|
+
const groupFor = id => {
|
|
413
|
+
if (!groups.has(id)) groups.set(id, { id, tail: null, head: null, count: 0 });
|
|
414
|
+
return groups.get(id);
|
|
415
|
+
};
|
|
416
|
+
for (const run of paired.runs) if (run.mark.id != null) {
|
|
417
|
+
const group = groupFor(run.mark.id);
|
|
418
|
+
group.count++;
|
|
419
|
+
group[run.mark.kind === 'arrow' ? 'tail' : 'head'] = run;
|
|
420
|
+
}
|
|
421
|
+
for (const marker of paired.strays) if (marker.mark?.id != null) groupFor(marker.mark.id).count++;
|
|
422
|
+
const arrows = [...groups.values()].filter(group => group.count === 2 && group.tail && group.head)
|
|
423
|
+
.map(({id, tail, head}) => ({id, tail, head}));
|
|
424
|
+
const complete = new Set(arrows.map(arrow => arrow.id)), runs = [], strays = paired.strays.slice();
|
|
425
|
+
for (const run of paired.runs) {
|
|
426
|
+
if (run.mark.id == null || complete.has(run.mark.id)) { runs.push(run); continue; }
|
|
427
|
+
strays.push({ start: run.start, end: run.innerStart, mark: run.mark, close: false },
|
|
428
|
+
{ start: run.innerEnd, end: run.end, mark: null, close: true, arrowId: run.mark.id });
|
|
429
|
+
}
|
|
430
|
+
strays.sort((a, b) => a.start - b.start);
|
|
431
|
+
return { runs, strays, arrows };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// as spec/frontmatter.mjs, copied here so this grammar stays inside the MIT kit's closure.
|
|
435
|
+
const WILL_BLOCK_TAGS = new Set('address article aside base basefont blockquote body caption center col colgroup dd details dialog dir div dl dt fieldset figcaption figure footer form frame frameset h1 h2 h3 h4 h5 h6 head header hr html iframe legend li link main menu menuitem nav noframes ol optgroup option p param search section summary table tbody td tfoot th thead title tr track ul'.split(' '));
|
|
436
|
+
const WILL_VOID_TAGS = new Set('area base br col embed hr img input link meta param source track wbr'.split(' '));
|
|
437
|
+
|
|
438
|
+
function willLineAt(text, start) {
|
|
439
|
+
let end = start;
|
|
440
|
+
while (end < text.length && text.charCodeAt(end) !== 13 && text.charCodeAt(end) !== 10) end++;
|
|
441
|
+
const eol = text.charCodeAt(end) === 13 && text.charCodeAt(end + 1) === 10 ? 2 : end < text.length ? 1 : 0;
|
|
442
|
+
return {start, end, next: end + eol, text: text.slice(start, end)};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function willOpeningFrame(text) {
|
|
446
|
+
const bom = text.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
447
|
+
const opening = willLineAt(text, bom);
|
|
448
|
+
if (!/^--- *$/.test(opening.text) || opening.next === opening.end) return null;
|
|
449
|
+
for (let at = opening.next; at < text.length;) {
|
|
450
|
+
const line = willLineAt(text, at);
|
|
451
|
+
if (/^(?:---|\.\.\.) *$/.test(line.text)) return {contentStart: opening.next, contentEnd: line.start, bodyStart: line.next};
|
|
452
|
+
if (line.next <= at) break;
|
|
453
|
+
at = line.next;
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function willColumns(line) {
|
|
459
|
+
let col = 0;
|
|
460
|
+
for (let i = 0; i < line.length; i++) {
|
|
461
|
+
const code = line.charCodeAt(i);
|
|
462
|
+
if (code === 32) col++;
|
|
463
|
+
else if (code === 9) col += 4 - (col % 4);
|
|
464
|
+
else return {col, index: i};
|
|
465
|
+
}
|
|
466
|
+
return {col, index: line.length};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function willQuoteInner(line) {
|
|
470
|
+
let i = 0, depth = 0;
|
|
471
|
+
while (i < line.length) {
|
|
472
|
+
let col = 0, j = i;
|
|
473
|
+
while (j < line.length && col < 3) {
|
|
474
|
+
const code = line.charCodeAt(j);
|
|
475
|
+
if (code === 32) { col++; j++; }
|
|
476
|
+
else if (code === 9) {
|
|
477
|
+
const adv = 4 - (col % 4);
|
|
478
|
+
if (col + adv > 3) break;
|
|
479
|
+
col += adv; j++;
|
|
480
|
+
} else break;
|
|
481
|
+
}
|
|
482
|
+
if (line.charCodeAt(j) !== 62) break;
|
|
483
|
+
depth++;
|
|
484
|
+
j++;
|
|
485
|
+
if (line.charCodeAt(j) === 32 || line.charCodeAt(j) === 9) j++;
|
|
486
|
+
i = j;
|
|
487
|
+
}
|
|
488
|
+
return depth ? line.slice(i) : null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function willListItem(line) {
|
|
492
|
+
const {col, index} = willColumns(line);
|
|
493
|
+
if (col >= 4) return null;
|
|
494
|
+
const rest = line.slice(index);
|
|
495
|
+
const marked = /^([-*+]|\d{1,9}[.)])([ \t]*)/.exec(rest);
|
|
496
|
+
if (!marked || (marked[2].length === 0 && index + marked[1].length < line.length)) return null;
|
|
497
|
+
let content = col + marked[1].length, padding = 0;
|
|
498
|
+
for (const ch of marked[2]) {
|
|
499
|
+
if (padding >= 4) break;
|
|
500
|
+
if (ch === ' ') { content++; padding++; }
|
|
501
|
+
else { const adv = 4 - (content % 4); content += adv; padding += adv; }
|
|
502
|
+
}
|
|
503
|
+
if (marked[2].length === 0) content++;
|
|
504
|
+
return {content};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function willFenceOpen(line) {
|
|
508
|
+
const {col, index} = willColumns(line);
|
|
509
|
+
if (col >= 4) return null;
|
|
510
|
+
const match = /^(`{3,}|~{3,})(.*)$/.exec(line.slice(index));
|
|
511
|
+
if (!match || (match[1].charCodeAt(0) === 96 && match[2].includes('`'))) return null;
|
|
512
|
+
return {ch: match[1][0], len: match[1].length};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function willFenceCloses(line, fence) {
|
|
516
|
+
const raw = fence.inQuote ? willQuoteInner(line) : line;
|
|
517
|
+
if (raw == null) return false;
|
|
518
|
+
const {col, index} = willColumns(raw);
|
|
519
|
+
if (col >= 4) return false;
|
|
520
|
+
const rest = raw.slice(index);
|
|
521
|
+
let i = 0;
|
|
522
|
+
while (i < rest.length && rest[i] === fence.ch) i++;
|
|
523
|
+
if (i < fence.len) return false;
|
|
524
|
+
for (let k = i; k < rest.length; k++) if (rest.charCodeAt(k) !== 32 && rest.charCodeAt(k) !== 9) return false;
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function willUpdateTags(item, line) {
|
|
529
|
+
const re = /<!--[\s\S]*?-->|<\/?([A-Za-z][A-Za-z0-9]*)\b[^>]*?>/g;
|
|
530
|
+
let match;
|
|
531
|
+
while ((match = re.exec(line))) {
|
|
532
|
+
if (match[0].charCodeAt(1) === 33) continue;
|
|
533
|
+
const name = match[1].toLowerCase();
|
|
534
|
+
const close = match[0].charCodeAt(1) === 47;
|
|
535
|
+
if (WILL_VOID_TAGS.has(name) || /\/\s*>$/.test(match[0])) continue;
|
|
536
|
+
if (close) {
|
|
537
|
+
const at = item.tags.lastIndexOf(name);
|
|
538
|
+
if (at >= 0) item.tags.splice(at, 1);
|
|
539
|
+
} else item.tags.push(name);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function willHtmlOpen(line) {
|
|
544
|
+
const {col, index} = willColumns(line);
|
|
545
|
+
if (col >= 4) return null;
|
|
546
|
+
const rest = line.slice(index);
|
|
547
|
+
const leaf = /^<(pre|script|style|textarea)(?:[ \t]|\/?>|$)/i.exec(rest);
|
|
548
|
+
if (leaf) return {kind: leaf[1].toLowerCase() === 'pre' ? 'pre' : 'raw', tag: leaf[1].toLowerCase()};
|
|
549
|
+
const block = /^<\/?([A-Za-z][A-Za-z0-9]*)(?:[ \t]|\/?>|$)/.exec(rest);
|
|
550
|
+
if (!block || !WILL_BLOCK_TAGS.has(block[1].toLowerCase())) return null;
|
|
551
|
+
const item = {kind: 'html', tags: []};
|
|
552
|
+
willUpdateTags(item, rest);
|
|
553
|
+
return item;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function willRawCloses(item, line) {
|
|
557
|
+
const close = new RegExp('</' + item.tag + '(?:[\\t\\n\\f />]|$)', 'i');
|
|
558
|
+
return close.test(line);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function willQuotedWhere(stack) {
|
|
562
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
563
|
+
const item = stack[i];
|
|
564
|
+
if (item.kind === 'fence') return 'fence';
|
|
565
|
+
if (item.kind === 'pre') return 'raw-pre';
|
|
566
|
+
if (item.kind === 'html') {
|
|
567
|
+
if (item.tags.includes('pre')) return 'raw-pre';
|
|
568
|
+
if (item.tags.includes('td') || item.tags.includes('th')) return 'raw-table';
|
|
569
|
+
if (item.tags.includes('div')) return 'raw-div';
|
|
570
|
+
return 'raw-html';
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function willLeaf(stack) {
|
|
577
|
+
const kind = stack[stack.length - 1]?.kind;
|
|
578
|
+
return kind === 'fence' || kind === 'pre' || kind === 'html' || kind === 'raw';
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export function quotedWillSpan(source, spans) {
|
|
582
|
+
const text = String(source ?? '');
|
|
583
|
+
const marks = Array.isArray(spans) ? spans : [];
|
|
584
|
+
if (!marks.length) return null;
|
|
585
|
+
const places = new Map();
|
|
586
|
+
const frame = willOpeningFrame(text);
|
|
587
|
+
if (frame) {
|
|
588
|
+
for (const span of marks) {
|
|
589
|
+
const start = span[0];
|
|
590
|
+
if (start >= frame.contentStart && start < frame.contentEnd) places.set(start, 'frontmatter');
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const stack = [];
|
|
594
|
+
for (let pos = frame ? frame.bodyStart : text.charCodeAt(0) === 0xfeff ? 1 : 0; pos < text.length;) {
|
|
595
|
+
const line = willLineAt(text, pos);
|
|
596
|
+
const blank = /^[ \t]*$/.test(line.text);
|
|
597
|
+
let closedHere = false;
|
|
598
|
+
while (stack.length) {
|
|
599
|
+
const top = stack[stack.length - 1];
|
|
600
|
+
if (top.kind === 'html' && blank) { stack.pop(); continue; }
|
|
601
|
+
if (top.kind === 'fence' && willFenceCloses(line.text, top)) { stack.pop(); closedHere = true; continue; }
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
let cut = -1;
|
|
605
|
+
for (let i = 0; i < stack.length; i++) {
|
|
606
|
+
const item = stack[i];
|
|
607
|
+
if (item.kind === 'list' && !blank && willColumns(line.text).col < item.content && !willListItem(line.text)) { cut = i; break; }
|
|
608
|
+
if (item.kind === 'quote' && !blank && willQuoteInner(line.text) == null) { cut = i; break; }
|
|
609
|
+
}
|
|
610
|
+
if (cut >= 0) stack.length = cut;
|
|
611
|
+
if (!places.has(line.start)) {
|
|
612
|
+
const where = willQuotedWhere(stack);
|
|
613
|
+
if (where) places.set(line.start, where);
|
|
614
|
+
}
|
|
615
|
+
const top = stack[stack.length - 1];
|
|
616
|
+
if (top?.kind === 'pre' && willRawCloses({tag: 'pre'}, line.text)) stack.pop();
|
|
617
|
+
else if (top?.kind === 'raw' && willRawCloses(top, line.text)) stack.pop();
|
|
618
|
+
else if (top?.kind === 'html' && !blank) willUpdateTags(top, line.text);
|
|
619
|
+
else if (!closedHere && !willLeaf(stack) && !blank) {
|
|
620
|
+
const inner = willQuoteInner(line.text);
|
|
621
|
+
const body = inner == null ? line.text : inner;
|
|
622
|
+
const inQuote = inner != null;
|
|
623
|
+
if (inQuote && stack[stack.length - 1]?.kind !== 'quote') stack.push({kind: 'quote'});
|
|
624
|
+
const fence = willFenceOpen(body);
|
|
625
|
+
if (fence) stack.push({kind: 'fence', ...fence, inQuote});
|
|
626
|
+
else {
|
|
627
|
+
const html = willHtmlOpen(body);
|
|
628
|
+
if (html) stack.push({...html, inQuote});
|
|
629
|
+
else if (!inQuote) {
|
|
630
|
+
const item = willListItem(line.text);
|
|
631
|
+
if (item && stack[stack.length - 1]?.kind !== 'list') stack.push({kind: 'list', content: item.content});
|
|
632
|
+
else if (item && stack[stack.length - 1]?.kind === 'list' && item.content > stack[stack.length - 1].content) stack.push({kind: 'list', content: item.content});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (line.next <= pos) break;
|
|
637
|
+
pos = line.next;
|
|
638
|
+
}
|
|
639
|
+
for (const span of marks) {
|
|
640
|
+
const where = places.get(span[0]);
|
|
641
|
+
if (where) return {start: span[0], end: span[1], where};
|
|
642
|
+
}
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function pairSpanMarkers(source, kind, markers = null) {
|
|
647
|
+
if (kind !== 'ink') throw new TypeError('Unknown paired marker kind');
|
|
648
|
+
const runs = [], strays = [], stack = [];
|
|
649
|
+
for (const marker of markers || scanInkMarkers(source)) {
|
|
650
|
+
if (!marker.close) { stack.push(marker); continue; }
|
|
651
|
+
const open = stack.pop();
|
|
652
|
+
if (!open) { strays.push(marker); continue; }
|
|
653
|
+
runs.push({ start: open.start, end: marker.end, innerStart: open.end, innerEnd: marker.start, mark: open.mark });
|
|
654
|
+
}
|
|
655
|
+
strays.push(...stack);
|
|
656
|
+
runs.sort((a, b) => a.start - b.start);
|
|
657
|
+
strays.sort((a, b) => a.start - b.start);
|
|
658
|
+
return { runs, strays };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export function sliceInkArrows(source, start = 0, end = String(source ?? '').length, markers = null) {
|
|
662
|
+
const text = String(source ?? '');
|
|
663
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start || end > text.length) throw new RangeError('ink: invalid source slice');
|
|
664
|
+
const kept = new Set();
|
|
665
|
+
for (const arrow of pairInkMarkers(text, markers).arrows) {
|
|
666
|
+
if (![arrow.tail, arrow.head].every(run => run.innerStart >= start && run.innerEnd <= end && run.innerStart < run.innerEnd)) continue;
|
|
667
|
+
kept.add(arrow.id);
|
|
668
|
+
start = Math.min(start, arrow.tail.start, arrow.head.start);
|
|
669
|
+
end = Math.max(end, arrow.tail.end, arrow.head.end);
|
|
670
|
+
}
|
|
671
|
+
const spans = pairInkSpans(text, markers), cuts = [];
|
|
672
|
+
for (const run of spans.runs) if (run.mark.id != null && !kept.has(run.mark.id)) cuts.push(
|
|
673
|
+
{ start: run.start, end: run.innerStart }, { start: run.innerEnd, end: run.end });
|
|
674
|
+
for (const marker of spans.strays) if (marker.mark?.id != null) cuts.push(marker);
|
|
675
|
+
let result = '', at = start;
|
|
676
|
+
for (const cut of cuts.sort((a, b) => a.start - b.start)) {
|
|
677
|
+
if (cut.end <= at || cut.start >= end) continue;
|
|
678
|
+
result += text.slice(at, Math.max(at, cut.start));
|
|
679
|
+
at = Math.min(end, cut.end);
|
|
680
|
+
}
|
|
681
|
+
return result + text.slice(at, end);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export function prepareInkPaste(source, existing = '', markers = null) {
|
|
685
|
+
const text = String(source ?? ''), paired = pairInkMarkers(text, markers), edits = [];
|
|
686
|
+
for (const marker of paired.strays) if (marker.mark?.id != null || marker.arrowId != null) edits.push({ start: marker.start, end: marker.end, text: '' });
|
|
687
|
+
const occupied = new Set(scanInkMarkers(existing).filter(marker => marker.mark?.id != null).map(marker => marker.mark.id));
|
|
688
|
+
const reserved = new Set([...occupied, ...paired.arrows.map(arrow => arrow.id)]);
|
|
689
|
+
let next = 1;
|
|
690
|
+
for (const arrow of paired.arrows) {
|
|
691
|
+
const runs = [arrow.tail, arrow.head];
|
|
692
|
+
if (runs.some(run => run.innerStart === run.innerEnd)) {
|
|
693
|
+
for (const run of runs) edits.push({ start: run.start, end: run.innerStart, text: '' }, { start: run.innerEnd, end: run.end, text: '' });
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
if (!occupied.has(arrow.id)) continue;
|
|
697
|
+
while (reserved.has(next) && next <= INK_INT_MAX) next++;
|
|
698
|
+
if (next > INK_INT_MAX) throw new RangeError('ink: no unused arrow pairing number');
|
|
699
|
+
reserved.add(next);
|
|
700
|
+
for (const run of runs) {
|
|
701
|
+
const prefix = '<!--ink ' + run.mark.kind + ' ';
|
|
702
|
+
edits.push({ start: run.start + prefix.length, end: run.start + prefix.length + String(arrow.id).length, text: String(next) });
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
let result = text;
|
|
706
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
707
|
+
return result;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export function stripInkMarkers(text) {
|
|
711
|
+
const source = String(text == null ? '' : text);
|
|
712
|
+
let result = '', at = 0;
|
|
713
|
+
for (const marker of scanInkMarkers(source)) { result += source.slice(at, marker.start); at = marker.end; }
|
|
714
|
+
return result + source.slice(at);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export function hasInkMarker(text) {
|
|
718
|
+
return scanInkMarkers(text).length > 0;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
export function formatPageBreak() {
|
|
722
|
+
return PAGE_BREAK_MARKER;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export function isPageBreakLine(text) {
|
|
726
|
+
return typeof text === 'string' && /^<!--md-break:v1 page-->\r?$/.test(text);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export function isPageBreakBlock(text) {
|
|
730
|
+
return typeof text === 'string' && /^<!--md-break:v1 page-->\r?\n?$/.test(text);
|
|
731
|
+
}
|