@oh-my-pi/pi-utils 18.4.9 → 18.4.11

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.
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Where brackets, emphasis, code spans, autolinks and inline HTML close in inline Markdown. The indexes are built
3
+ * over one root inline source, each on first use, and shared by every source lexed inside it (a link label, the text
4
+ * of emphasis), so lexing nested sources builds no index of its own and keeps none alive per nesting level.
5
+ */
6
+
7
+ export const PUNCTUATION = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/;
8
+
9
+ /** Whether an odd run of backslashes right before `index` escapes the character there. */
10
+ export function escapedAt(src: string, index: number): boolean {
11
+ let at = index - 1;
12
+ while (at >= 0 && src.charCodeAt(at) === 0x5c /* \ */) at--;
13
+ return (index - 1 - at) % 2 === 1;
14
+ }
15
+
16
+ export function canOpenDelimiter(src: string, index: number, width: number, marker: string, previous = "\n"): boolean {
17
+ const before = index === 0 ? previous : src[index - 1]!;
18
+ if (index === 0 && before === marker) return false;
19
+ const after = src[index + width];
20
+ if (after === undefined || /\s/.test(after)) return false;
21
+ if (marker === "_" && /[\p{L}\p{N}]/u.test(before) && /[\p{L}\p{N}]/u.test(after)) return false;
22
+ return true;
23
+ }
24
+
25
+ export function canCloseDelimiter(src: string, index: number, marker: string): boolean {
26
+ const before = src[index - 1];
27
+ const after = src[index + 1] ?? "\n";
28
+ if (before === undefined || /\s/.test(before)) return false;
29
+ if (marker === "_" && /[\p{L}\p{N}]/u.test(before) && /[\p{L}\p{N}]/u.test(after)) return false;
30
+ return !PUNCTUATION.test(before) || /\s/.test(after) || PUNCTUATION.test(after);
31
+ }
32
+
33
+ /** Index of the first element of the ascending `values` at or above `target`. */
34
+ function lowerBound(values: readonly number[], target: number): number {
35
+ let low = 0;
36
+ let high = values.length;
37
+ while (low < high) {
38
+ const mid = (low + high) >>> 1;
39
+ if (values[mid]! < target) low = mid + 1;
40
+ else high = mid;
41
+ }
42
+ return low;
43
+ }
44
+
45
+ /** Closing-delimiter offsets by the depth before each, every list ascending. */
46
+ class ClosersByDepth {
47
+ readonly #offsets = new Map<number, number[]>();
48
+
49
+ /** Records a closer at `offset`, which lies past every closer recorded before it. */
50
+ add(depth: number, offset: number): void {
51
+ let offsets = this.#offsets.get(depth);
52
+ if (!offsets) this.#offsets.set(depth, (offsets = []));
53
+ offsets.push(offset);
54
+ }
55
+
56
+ /** The first closer at `depth` at or after `from`, or -1. */
57
+ first(depth: number, from: number): number {
58
+ const offsets = this.#offsets.get(depth);
59
+ if (!offsets) return -1;
60
+ const at = lowerBound(offsets, from);
61
+ return at === offsets.length ? -1 : offsets[at]!;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Where a bracket opened in the root source closes. A search starts right after the opening bracket, and a bracket
67
+ * never begins an escape, so the escape pairing from there is the pairing from the start of the root source. The
68
+ * closer is then the first unescaped closing bracket at or after the start whose depth (openers minus closers
69
+ * before it, from the start of the root source) equals the depth at the start.
70
+ */
71
+ class BracketDepths {
72
+ // The depth before each offset.
73
+ readonly #depth: Int32Array;
74
+ readonly #closers = new ClosersByDepth();
75
+ // Every unescaped closing bracket, ascending.
76
+ readonly #all: number[] = [];
77
+
78
+ constructor(src: string, open: number, close: number) {
79
+ const depth = new Int32Array(src.length + 1);
80
+ let level = 0;
81
+ for (let i = 0; i < src.length; i++) {
82
+ depth[i] = level;
83
+ const code = src.charCodeAt(i);
84
+ if (code === 0x5c /* \ */) {
85
+ depth[++i] = level;
86
+ } else if (code === open) {
87
+ level++;
88
+ } else if (code === close) {
89
+ this.#closers.add(level, i);
90
+ this.#all.push(i);
91
+ level--;
92
+ }
93
+ }
94
+ depth[src.length] = level;
95
+ this.#depth = depth;
96
+ }
97
+
98
+ /** The offset of the bracket closing the one just before `start`, or -1. */
99
+ closeAfter(start: number): number {
100
+ return this.#closers.first(this.#depth[start]!, start);
101
+ }
102
+
103
+ /** Whether an unescaped closing bracket lies at or after `from`, right after a bracket, and before `to`. */
104
+ closesWithin(from: number, to: number): boolean {
105
+ const at = lowerBound(this.#all, from);
106
+ return at < this.#all.length && this.#all[at]! < to;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * The delimiters the emphasis rule's closer walk visits for one marker and width in the root source, typed the way
112
+ * the walk types them: a delimiter that can close is a closer, else one that can open is an opener. For width 1 they
113
+ * are every unescaped marker. For width 2 they are marker pairs taken two at a time from the start of each run of
114
+ * markers (after it when an odd run of backslashes escapes the start), which is where a walk that reaches the run
115
+ * from before it enters. The closer for an opener is then the first closer after it whose depth (openers minus
116
+ * closers before it) equals the depth where its walk begins, the first point where the walk's nesting count would
117
+ * drop below zero.
118
+ */
119
+ class EmphasisDelimiters {
120
+ readonly #src: string;
121
+ readonly #marker: string;
122
+ readonly #width: number;
123
+ // Delimiter offsets, ascending, and the depth before each.
124
+ readonly #at: number[] = [];
125
+ readonly #depth: number[] = [];
126
+ readonly #closers = new ClosersByDepth();
127
+
128
+ constructor(src: string, marker: string, width: number) {
129
+ this.#src = src;
130
+ this.#marker = marker;
131
+ this.#width = width;
132
+ let level = 0;
133
+ for (let run = src.indexOf(marker); run !== -1;) {
134
+ let end = run + 1;
135
+ while (src[end] === marker) end++;
136
+ for (let at = escapedAt(src, run) ? run + width : run; at + width <= end; at += width) {
137
+ this.#at.push(at);
138
+ this.#depth.push(level);
139
+ if (canCloseDelimiter(src, at, marker)) {
140
+ this.#closers.add(level, at);
141
+ level--;
142
+ } else if (canOpenDelimiter(src, at, width, marker)) {
143
+ level++;
144
+ }
145
+ }
146
+ run = src.indexOf(marker, end);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Where the emphasis opened by the delimiter at `opener` closes in a source that ends at `end`, or -1 when the
152
+ * closer the walk meets first does not lie wholly before `end`. The walk reads nothing before its start. Past
153
+ * `end` it reads only for the source's last delimiter, which cannot open there and closes as it would at the end
154
+ * of the source: the sources inline lexing nests end right before a marker or "]", and the closing test reads
155
+ * either punctuation there like the end.
156
+ */
157
+ closeFor(opener: number, end: number): number {
158
+ const src = this.#src;
159
+ const marker = this.#marker;
160
+ const width = this.#width;
161
+ const walk = opener + width;
162
+ // A pair right after a width-2 opener has markers on both sides, so the walk takes it as the closer.
163
+ if (width === 2 && src[walk] === marker && src[walk + 1] === marker) return walk + width <= end ? walk : -1;
164
+ const next = lowerBound(this.#at, walk);
165
+ if (next === this.#at.length) return -1;
166
+ const close = this.#closers.first(this.#depth[next]!, this.#at[next]!);
167
+ return close !== -1 && close + width <= end ? close : -1;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * The runs of backticks in the root source, for the code span rule. A span opened by `width` backticks closes at the
173
+ * first `width` backticks after its opening run that no backslash escapes: the start of a later run of at least
174
+ * `width`, or, in a run whose first backtick a backslash escapes, the backticks from its `width`th on, since the
175
+ * search for a closer goes on `width` past an escaped one.
176
+ */
177
+ class CodeRuns {
178
+ // Per run, in order: where it starts and ends, and whether a backslash escapes it.
179
+ readonly #starts: number[] = [];
180
+ readonly #ends: number[] = [];
181
+ readonly #escaped: boolean[] = [];
182
+ // A segment tree over the runs of the widest closer each run holds: its length, or half of it when escaped.
183
+ readonly #widest: Int32Array;
184
+ readonly #leaves: number;
185
+
186
+ constructor(src: string) {
187
+ const widths: number[] = [];
188
+ for (let at = src.indexOf("`"); at !== -1;) {
189
+ let end = at + 1;
190
+ while (src.charCodeAt(end) === 0x60 /* ` */) end++;
191
+ const escaped = escapedAt(src, at);
192
+ this.#starts.push(at);
193
+ this.#ends.push(end);
194
+ this.#escaped.push(escaped);
195
+ widths.push(escaped ? (end - at) >> 1 : end - at);
196
+ at = src.indexOf("`", end);
197
+ }
198
+ let leaves = 1;
199
+ while (leaves < widths.length) leaves <<= 1;
200
+ const widest = new Int32Array(2 * leaves);
201
+ for (let run = 0; run < widths.length; run++) widest[leaves + run] = widths[run]!;
202
+ for (let node = leaves - 1; node >= 1; node--) widest[node] = Math.max(widest[2 * node]!, widest[2 * node + 1]!);
203
+ this.#widest = widest;
204
+ this.#leaves = leaves;
205
+ }
206
+
207
+ /** Where the run of backticks holding the backtick at `at` ends. */
208
+ endOf(at: number): number {
209
+ return this.#ends[lowerBound(this.#starts, at + 1) - 1]!;
210
+ }
211
+
212
+ /** Where the closer of a code span opened by `width` backticks that end at `from` starts, or -1. */
213
+ closerAfter(from: number, width: number): number {
214
+ const run = this.#firstHolding(lowerBound(this.#starts, from), width);
215
+ return run === -1 ? -1 : this.#starts[run]! + (this.#escaped[run] ? width : 0);
216
+ }
217
+
218
+ /** The first run at or after `first` that holds a closer of `width` backticks, or -1. */
219
+ #firstHolding(first: number, width: number): number {
220
+ if (first >= this.#starts.length) return -1;
221
+ const widest = this.#widest;
222
+ const leaves = this.#leaves;
223
+ let node = leaves + first;
224
+ if (widest[node]! >= width) return first;
225
+ // Up to the nearest subtree on the right that holds one, then down to its first run that does.
226
+ for (;;) {
227
+ if (node === 1) return -1;
228
+ if ((node & 1) === 0 && widest[node + 1]! >= width) break;
229
+ node >>= 1;
230
+ }
231
+ node++;
232
+ while (node < leaves) node = widest[2 * node]! >= width ? 2 * node : 2 * node + 1;
233
+ return node - leaves;
234
+ }
235
+ }
236
+
237
+ /** The closer indexes of one root inline source, each built on first use. */
238
+ export class CloserIndexes {
239
+ readonly #src: string;
240
+ #square: BracketDepths | undefined;
241
+ #round: BracketDepths | undefined;
242
+ readonly #emphasis = new Map<string, EmphasisDelimiters>();
243
+ #tagEnds: Int32Array | undefined;
244
+ #commentEnds: number[] | undefined;
245
+ #escapes: number[] | undefined;
246
+ #codeRuns: CodeRuns | undefined;
247
+ // The first " " and the first ">" at or after the offset each was last searched from.
248
+ #space = -1;
249
+ #angle = -1;
250
+
251
+ constructor(src: string) {
252
+ this.#src = src;
253
+ }
254
+
255
+ get square(): BracketDepths {
256
+ return (this.#square ??= new BracketDepths(this.#src, 0x5b /* [ */, 0x5d /* ] */));
257
+ }
258
+
259
+ get round(): BracketDepths {
260
+ return (this.#round ??= new BracketDepths(this.#src, 0x28 /* ( */, 0x29 /* ) */));
261
+ }
262
+
263
+ emphasis(marker: string, width: number): EmphasisDelimiters {
264
+ const key = marker.repeat(width);
265
+ let delimiters = this.#emphasis.get(key);
266
+ if (!delimiters) this.#emphasis.set(key, (delimiters = new EmphasisDelimiters(this.#src, marker, width)));
267
+ return delimiters;
268
+ }
269
+
270
+ /**
271
+ * For each offset, where the HTML rule's scan for a tag's end from there stops: the first ">" outside quotes,
272
+ * where a quote runs to the next quote of its kind, or -1 at the end of the source.
273
+ */
274
+ get tagEnds(): Int32Array {
275
+ if (this.#tagEnds) return this.#tagEnds;
276
+ const src = this.#src;
277
+ const ends = new Int32Array(src.length + 1);
278
+ ends[src.length] = -1;
279
+ let nextDouble = -1;
280
+ let nextSingle = -1;
281
+ for (let at = src.length - 1; at >= 0; at--) {
282
+ const code = src.charCodeAt(at);
283
+ if (code === 0x3e /* > */) {
284
+ ends[at] = at;
285
+ } else if (code === 0x22 /* " */) {
286
+ ends[at] = nextDouble === -1 ? -1 : ends[nextDouble + 1]!;
287
+ nextDouble = at;
288
+ } else if (code === 0x27 /* ' */) {
289
+ ends[at] = nextSingle === -1 ? -1 : ends[nextSingle + 1]!;
290
+ nextSingle = at;
291
+ } else {
292
+ ends[at] = ends[at + 1]!;
293
+ }
294
+ }
295
+ return (this.#tagEnds = ends);
296
+ }
297
+
298
+ /** Every offset where "-->" starts, ascending. */
299
+ get commentEnds(): number[] {
300
+ if (this.#commentEnds) return this.#commentEnds;
301
+ const src = this.#src;
302
+ const ends: number[] = [];
303
+ for (let at = src.indexOf("-->"); at !== -1; at = src.indexOf("-->", at + 1)) ends.push(at);
304
+ return (this.#commentEnds = ends);
305
+ }
306
+
307
+ get codeRuns(): CodeRuns {
308
+ return (this.#codeRuns ??= new CodeRuns(this.#src));
309
+ }
310
+
311
+ /**
312
+ * The first " " or ">" at or after `from`, or `Infinity`. Asked at offsets that never decrease, as the lexer's
313
+ * position does, so each search starts past the last one's answer.
314
+ */
315
+ spaceOrAngleFrom(from: number): number {
316
+ const src = this.#src;
317
+ if (this.#space < from) {
318
+ const at = src.indexOf(" ", from);
319
+ this.#space = at === -1 ? Infinity : at;
320
+ }
321
+ if (this.#angle < from) {
322
+ const at = src.indexOf(">", from);
323
+ this.#angle = at === -1 ? Infinity : at;
324
+ }
325
+ return Math.min(this.#space, this.#angle);
326
+ }
327
+
328
+ /** Every offset of a backslash before ASCII punctuation, ascending: where unescaping drops a backslash. */
329
+ get escapes(): number[] {
330
+ if (this.#escapes) return this.#escapes;
331
+ const src = this.#src;
332
+ const escapes: number[] = [];
333
+ for (let at = src.indexOf("\\"); at !== -1; at = src.indexOf("\\", at + 1)) {
334
+ if (PUNCTUATION.test(src[at + 1] ?? "")) escapes.push(at);
335
+ }
336
+ return (this.#escapes = escapes);
337
+ }
338
+ }
339
+
340
+ /**
341
+ * The link, emphasis, code span, autolink and inline HTML rules' closer lookups for one inline source, which is the
342
+ * root source of `indexes` or a part of it that ends at `end`. Offsets are relative to `rest`, a suffix of the
343
+ * source. A closer the root source places at or past `end` does not exist in the source.
344
+ */
345
+ export class InlineClosers {
346
+ readonly #indexes: CloserIndexes;
347
+ /** Where the source ends in the root source. */
348
+ readonly end: number;
349
+
350
+ constructor(indexes: CloserIndexes, end: number) {
351
+ this.#indexes = indexes;
352
+ this.end = end;
353
+ }
354
+
355
+ /** The closers of a part of `rest` that ends at `end`, lexed as its own source. */
356
+ nested(rest: string, end: number): InlineClosers {
357
+ return new InlineClosers(this.#indexes, this.end - rest.length + end);
358
+ }
359
+
360
+ /** Where the "[" just before `start` closes, or -1. */
361
+ closeSquare(rest: string, start: number): number {
362
+ return this.#close(this.#indexes.square, rest, start);
363
+ }
364
+
365
+ /** Where the "(" just before `start` closes, or -1. */
366
+ closeRound(rest: string, start: number): number {
367
+ return this.#close(this.#indexes.round, rest, start);
368
+ }
369
+
370
+ /** Whether `rest.slice(from, to)`, which starts right after a "[", holds an unescaped "]". */
371
+ squareCloserWithin(rest: string, from: number, to: number): boolean {
372
+ const pos = this.end - rest.length;
373
+ return this.#indexes.square.closesWithin(pos + from, pos + to);
374
+ }
375
+
376
+ /** Whether `rest.slice(from, to)` holds a backslash before ASCII punctuation, which unescaping drops. */
377
+ escapeWithin(rest: string, from: number, to: number): boolean {
378
+ const pos = this.end - rest.length;
379
+ const escapes = this.#indexes.escapes;
380
+ const at = lowerBound(escapes, pos + from);
381
+ return at < escapes.length && escapes[at]! + 1 < pos + to;
382
+ }
383
+
384
+ /** Where the emphasis opened by the `width` markers at the start of `rest` closes, or -1. */
385
+ closeEmphasis(rest: string, marker: string, width: number): number {
386
+ const pos = this.end - rest.length;
387
+ const close = this.#indexes.emphasis(marker, width).closeFor(pos, this.end);
388
+ return close === -1 ? -1 : close - pos;
389
+ }
390
+
391
+ /** How many backticks start `rest`, which starts with one. */
392
+ codeOpenerWidth(rest: string): number {
393
+ const pos = this.end - rest.length;
394
+ return Math.min(this.#indexes.codeRuns.endOf(pos), this.end) - pos;
395
+ }
396
+
397
+ /** Where the code span opened by the `width` backticks at the start of `rest` closes, or -1. */
398
+ closeCode(rest: string, width: number): number {
399
+ const pos = this.end - rest.length;
400
+ const close = this.#indexes.codeRuns.closerAfter(pos + width, width);
401
+ return close === -1 || close + width > this.end ? -1 : close - pos;
402
+ }
403
+
404
+ /**
405
+ * Where the URL autolink opened at the start of `rest` ends, its scheme ending at `from`: the first " " or ">"
406
+ * after the scheme if it is a ">" with text before it, or -1. One past the end of `rest` reads as no ">".
407
+ */
408
+ closeUrlAutolink(rest: string, from: number): number {
409
+ const pos = this.end - rest.length;
410
+ const close = this.#indexes.spaceOrAngleFrom(pos + from) - pos;
411
+ return close > from && rest.charCodeAt(close) === 0x3e /* > */ ? close : -1;
412
+ }
413
+
414
+ /** Where the HTML tag opened by the "<" at the start of `rest` ends: the first ">" after it outside quotes, or -1. */
415
+ closeTag(rest: string): number {
416
+ const pos = this.end - rest.length;
417
+ const close = this.#indexes.tagEnds[pos + 1]!;
418
+ return close === -1 || close >= this.end ? -1 : close - pos;
419
+ }
420
+
421
+ /** Where the first "-->" after the "<!--" at the start of `rest` starts, or -1. */
422
+ closeComment(rest: string): number {
423
+ const pos = this.end - rest.length;
424
+ const ends = this.#indexes.commentEnds;
425
+ const at = lowerBound(ends, pos + 4);
426
+ return at === ends.length || ends[at]! + 3 > this.end ? -1 : ends[at]! - pos;
427
+ }
428
+
429
+ #close(depths: BracketDepths, rest: string, start: number): number {
430
+ const pos = this.end - rest.length;
431
+ const close = depths.closeAfter(pos + start);
432
+ return close === -1 || close >= this.end ? -1 : close - pos;
433
+ }
434
+ }