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

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/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.4.10] - 2026-10-02
6
+
7
+ ### Added
8
+
9
+ - Added `startFrom(src, from)` to inline Markdown tokenizer extensions: a start hint that returns the first match at or after `from` (or `undefined`), so long paragraphs stay linear ([#13961](https://github.com/can1357/oh-my-pi/pull/13961) by [@sjawhar](https://github.com/sjawhar)).
10
+ - Added `this.source` and `this.end` for inline Markdown tokenizer extensions: the whole inline source and where the text being lexed ends in it, with one `this` per source that the link labels and emphasis inside it share, so a tokenizer can remember what it already scanned ([#13961](https://github.com/can1357/oh-my-pi/pull/13961) by [@sjawhar](https://github.com/sjawhar)).
11
+ - Added `mathSpanInContext` and `MathSpans` to `math-delimiters`, which find math spans without rescanning a run of unclosed openers ([#13961](https://github.com/can1357/oh-my-pi/pull/13961) by [@sjawhar](https://github.com/sjawhar)).
12
+
13
+ ### Fixed
14
+
15
+ - Fixed long Markdown paragraphs lexing slowly: a 44 KB paragraph with no blank line now parses in about 3 ms instead of 100 ms ([#13961](https://github.com/can1357/oh-my-pi/pull/13961) by [@sjawhar](https://github.com/sjawhar)).
16
+ - Fixed Markdown paragraphs with many unclosed `[`, `*` or `_`, or with a long address-like word and no dotted domain, lexing slowly: a 40 KB paragraph of each now lexes in 4-24 ms instead of 2-12 s ([#13961](https://github.com/can1357/oh-my-pi/pull/13961) by [@sjawhar](https://github.com/sjawhar)).
17
+ - Fixed Markdown paragraphs of deeply nested emphasis, links or images lexing slowly: 32 KB now lexes in about 50 ms instead of 7 s, and a long word inside every level no longer costs its length once per level, except in nested image labels that hold a backslash escape ([#13961](https://github.com/can1357/oh-my-pi/pull/13961) by [@sjawhar](https://github.com/sjawhar)).
18
+ - Fixed Markdown paragraphs with long or many unclosed runs of backticks, or `<http://` autolinks with no space or `>` after them, lexing slowly: 80 KB of each now lexes in about 50-60 ms instead of seconds (40 KB of one unclosed run took 10 s) ([#13961](https://github.com/can1357/oh-my-pi/pull/13961) by [@sjawhar](https://github.com/sjawhar)).
19
+ - Fixed Markdown paragraphs of nested brackets, URLs with long trailing punctuation, or unclosed HTML tags or comments lexing slowly: 80 KB of each now lexes in under 40 ms instead of 4-30 s ([#13961](https://github.com/can1357/oh-my-pi/pull/13961) by [@sjawhar](https://github.com/sjawhar)).
20
+ - Fixed deeply nested Markdown links and emphasis overflowing the stack early: in a fresh process links now nest about three times as deep before a stack overflow, and emphasis twice as deep ([#13961](https://github.com/can1357/oh-my-pi/pull/13961) by [@sjawhar](https://github.com/sjawhar)).
21
+
5
22
  ## [18.4.9] - 2026-10-01
6
23
 
7
24
  ### Added
@@ -184,16 +184,40 @@ export type TokensList = Token[] & {
184
184
  /** Context supplied to extension tokenizers. */
185
185
  export interface TokenizerThis {
186
186
  lexer: Lexer;
187
+ /**
188
+ * For an inline tokenizer, the whole inline source, such as a paragraph's text. The `src` it receives is the part
189
+ * of it before `end` that starts at `end - src.length`: inside a link label or the text of emphasis, `end` is where
190
+ * that text ends, right before its closer, and a token there must not reach past it. Inline tokenizers get one
191
+ * context object per inline source, the same for every call while the lexer works through it and the labels and
192
+ * emphasis inside it, so a tokenizer can keep state for it in a `WeakMap`.
193
+ */
194
+ source?: string;
195
+ /** For an inline tokenizer, where the text the lexer works through ends in `source` (see `source`). */
196
+ end?: number;
187
197
  }
188
198
  /** A tokenizer extension callback. */
189
199
  export type TokenizerExtensionFunction = (this: TokenizerThis, src: string, tokens: Token[] | TokensList) => Tokens.Generic | undefined;
190
200
  /** A tokenizer extension start hint. */
191
201
  export type TokenizerStartFunction = (this: TokenizerThis, src: string) => number | void;
202
+ /**
203
+ * An offset form of a start hint: the first index at or after `from` in `src` where the extension's tokenizer could
204
+ * match, or `undefined` (not -1) when there is none; anything else below `from` throws. The test at each index must
205
+ * read `src` only from that index on, so the answer for `from` is also the answer for every later offset up to it,
206
+ * and must still pass where `src` goes on past its end: for a link label or the text of emphasis, the lexer asks on
207
+ * the inline source it lies in, then asks again on the label or text at the index it got, if that lies inside.
208
+ */
209
+ export type TokenizerStartFromFunction = (this: TokenizerThis, src: string, from: number) => number | undefined;
192
210
  /** An inline or block tokenizer extension. */
193
211
  export interface TokenizerExtension {
194
212
  name: string;
195
213
  level: "block" | "inline";
196
214
  start?: TokenizerStartFunction;
215
+ /**
216
+ * Replaces `start` for inline text when present. Inline lexing calls it on the whole inline source and reuses
217
+ * the answer until lexing passes it, inside link labels and emphasis too, instead of calling `start` on the rest
218
+ * of the source at every text step, so a hint that stops at its answer keeps a long paragraph linear.
219
+ */
220
+ startFrom?: TokenizerStartFromFunction;
197
221
  tokenizer: TokenizerExtensionFunction;
198
222
  childTokens?: string[];
199
223
  }
@@ -0,0 +1,119 @@
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
+ export declare const PUNCTUATION: RegExp;
7
+ /** Whether an odd run of backslashes right before `index` escapes the character there. */
8
+ export declare function escapedAt(src: string, index: number): boolean;
9
+ export declare function canOpenDelimiter(src: string, index: number, width: number, marker: string, previous?: string): boolean;
10
+ export declare function canCloseDelimiter(src: string, index: number, marker: string): boolean;
11
+ /**
12
+ * Where a bracket opened in the root source closes. A search starts right after the opening bracket, and a bracket
13
+ * never begins an escape, so the escape pairing from there is the pairing from the start of the root source. The
14
+ * closer is then the first unescaped closing bracket at or after the start whose depth (openers minus closers
15
+ * before it, from the start of the root source) equals the depth at the start.
16
+ */
17
+ declare class BracketDepths {
18
+ #private;
19
+ constructor(src: string, open: number, close: number);
20
+ /** The offset of the bracket closing the one just before `start`, or -1. */
21
+ closeAfter(start: number): number;
22
+ /** Whether an unescaped closing bracket lies at or after `from`, right after a bracket, and before `to`. */
23
+ closesWithin(from: number, to: number): boolean;
24
+ }
25
+ /**
26
+ * The delimiters the emphasis rule's closer walk visits for one marker and width in the root source, typed the way
27
+ * the walk types them: a delimiter that can close is a closer, else one that can open is an opener. For width 1 they
28
+ * are every unescaped marker. For width 2 they are marker pairs taken two at a time from the start of each run of
29
+ * markers (after it when an odd run of backslashes escapes the start), which is where a walk that reaches the run
30
+ * from before it enters. The closer for an opener is then the first closer after it whose depth (openers minus
31
+ * closers before it) equals the depth where its walk begins, the first point where the walk's nesting count would
32
+ * drop below zero.
33
+ */
34
+ declare class EmphasisDelimiters {
35
+ #private;
36
+ constructor(src: string, marker: string, width: number);
37
+ /**
38
+ * Where the emphasis opened by the delimiter at `opener` closes in a source that ends at `end`, or -1 when the
39
+ * closer the walk meets first does not lie wholly before `end`. The walk reads nothing before its start. Past
40
+ * `end` it reads only for the source's last delimiter, which cannot open there and closes as it would at the end
41
+ * of the source: the sources inline lexing nests end right before a marker or "]", and the closing test reads
42
+ * either punctuation there like the end.
43
+ */
44
+ closeFor(opener: number, end: number): number;
45
+ }
46
+ /**
47
+ * The runs of backticks in the root source, for the code span rule. A span opened by `width` backticks closes at the
48
+ * first `width` backticks after its opening run that no backslash escapes: the start of a later run of at least
49
+ * `width`, or, in a run whose first backtick a backslash escapes, the backticks from its `width`th on, since the
50
+ * search for a closer goes on `width` past an escaped one.
51
+ */
52
+ declare class CodeRuns {
53
+ #private;
54
+ constructor(src: string);
55
+ /** Where the run of backticks holding the backtick at `at` ends. */
56
+ endOf(at: number): number;
57
+ /** Where the closer of a code span opened by `width` backticks that end at `from` starts, or -1. */
58
+ closerAfter(from: number, width: number): number;
59
+ }
60
+ /** The closer indexes of one root inline source, each built on first use. */
61
+ export declare class CloserIndexes {
62
+ #private;
63
+ constructor(src: string);
64
+ get square(): BracketDepths;
65
+ get round(): BracketDepths;
66
+ emphasis(marker: string, width: number): EmphasisDelimiters;
67
+ /**
68
+ * For each offset, where the HTML rule's scan for a tag's end from there stops: the first ">" outside quotes,
69
+ * where a quote runs to the next quote of its kind, or -1 at the end of the source.
70
+ */
71
+ get tagEnds(): Int32Array;
72
+ /** Every offset where "-->" starts, ascending. */
73
+ get commentEnds(): number[];
74
+ get codeRuns(): CodeRuns;
75
+ /**
76
+ * The first " " or ">" at or after `from`, or `Infinity`. Asked at offsets that never decrease, as the lexer's
77
+ * position does, so each search starts past the last one's answer.
78
+ */
79
+ spaceOrAngleFrom(from: number): number;
80
+ /** Every offset of a backslash before ASCII punctuation, ascending: where unescaping drops a backslash. */
81
+ get escapes(): number[];
82
+ }
83
+ /**
84
+ * The link, emphasis, code span, autolink and inline HTML rules' closer lookups for one inline source, which is the
85
+ * root source of `indexes` or a part of it that ends at `end`. Offsets are relative to `rest`, a suffix of the
86
+ * source. A closer the root source places at or past `end` does not exist in the source.
87
+ */
88
+ export declare class InlineClosers {
89
+ #private;
90
+ /** Where the source ends in the root source. */
91
+ readonly end: number;
92
+ constructor(indexes: CloserIndexes, end: number);
93
+ /** The closers of a part of `rest` that ends at `end`, lexed as its own source. */
94
+ nested(rest: string, end: number): InlineClosers;
95
+ /** Where the "[" just before `start` closes, or -1. */
96
+ closeSquare(rest: string, start: number): number;
97
+ /** Where the "(" just before `start` closes, or -1. */
98
+ closeRound(rest: string, start: number): number;
99
+ /** Whether `rest.slice(from, to)`, which starts right after a "[", holds an unescaped "]". */
100
+ squareCloserWithin(rest: string, from: number, to: number): boolean;
101
+ /** Whether `rest.slice(from, to)` holds a backslash before ASCII punctuation, which unescaping drops. */
102
+ escapeWithin(rest: string, from: number, to: number): boolean;
103
+ /** Where the emphasis opened by the `width` markers at the start of `rest` closes, or -1. */
104
+ closeEmphasis(rest: string, marker: string, width: number): number;
105
+ /** How many backticks start `rest`, which starts with one. */
106
+ codeOpenerWidth(rest: string): number;
107
+ /** Where the code span opened by the `width` backticks at the start of `rest` closes, or -1. */
108
+ closeCode(rest: string, width: number): number;
109
+ /**
110
+ * Where the URL autolink opened at the start of `rest` ends, its scheme ending at `from`: the first " " or ">"
111
+ * after the scheme if it is a ">" with text before it, or -1. One past the end of `rest` reads as no ">".
112
+ */
113
+ closeUrlAutolink(rest: string, from: number): number;
114
+ /** Where the HTML tag opened by the "<" at the start of `rest` ends: the first ">" after it outside quotes, or -1. */
115
+ closeTag(rest: string): number;
116
+ /** Where the first "-->" after the "<!--" at the start of `rest` starts, or -1. */
117
+ closeComment(rest: string): number;
118
+ }
119
+ export {};
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The plain-text step of inline lexing over one inline source: where text can end ({@link TextStops}, which also
3
+ * tells the bare-URL rule where it can match) and how it is appended ({@link TextRun}).
4
+ */
5
+ import type { Lexer, Token, TokenizerExtension, TokenizerStartFromFunction } from "./core.js";
6
+ /**
7
+ * The places where plain text can end in one root inline source, as offsets into it, shared by every source lexed
8
+ * inside it (a link label, the text of emphasis). Each stop is the first offset at or after a search offset where a
9
+ * test passes, and each test reads the source only from its own offset on, so a found stop stays the answer for every
10
+ * later search offset up to it. A stop is searched for again only after lexing passes it, starting past it, so all
11
+ * the text steps of a paragraph, at every nesting level, read it a bounded number of times instead of once per step.
12
+ * Search offsets never decrease: the lexer asks at its position for the bare-URL rule and one past it for the text
13
+ * step, its position only grows, and a nested source is lexed from its start to its end before the lexer goes on
14
+ * past it.
15
+ *
16
+ * A nested source is the part of the root source before `end`, and a stop that holds there is a stop of the root
17
+ * source whose match ends by `end`. The first root stop after an offset tells whether there is one: when its match
18
+ * runs past `end`, no stop that ends by `end` follows it, for the reason each search gives.
19
+ */
20
+ declare class RootTextStops {
21
+ #private;
22
+ constructor(src: string);
23
+ /** The first character that can start another token at or after `from`. */
24
+ tokenCharFrom(from: number): number;
25
+ /** The first bare-URL scheme at or after `from` that ends by `end`. */
26
+ schemeFrom(from: number, end: number): number;
27
+ /** The "@" of the run {@link mailFrom} last found. */
28
+ get mailAt(): number;
29
+ /** The first offset at or after `from` that starts a match of `/[A-Za-z0-9._+-]+@/` whose "@" is before `end`. */
30
+ mailFrom(from: number, end: number): number;
31
+ /** The first offset at or after `from` that starts a match of `/(?: {2,}|\\)\n/` whose "\n" is before `end`. */
32
+ hardBreakFrom(from: number, end: number): number;
33
+ /** Where `extension`'s `startFrom` hint points at or after `from`, asked again only once lexing passes it. */
34
+ startFrom(extension: TokenizerExtension, startFrom: TokenizerStartFromFunction, lexer: Lexer, from: number): number;
35
+ }
36
+ /**
37
+ * The places where one inline source's plain text can end, as offsets into that source: its root source's stops
38
+ * that hold before the source's end. The source is a root source, or a part of one lexed as its own source (a link
39
+ * label, the text of emphasis), which ends right before the delimiter that closes it.
40
+ */
41
+ export declare class TextStops {
42
+ #private;
43
+ /** The stops of `src`, a root source, or else the part of `root`'s source that ends at `end`. */
44
+ constructor(src: string, lexer: Lexer, root?: RootTextStops, end?: number);
45
+ /** The stops of `src`, the part of this source's root source that ends at `end`, lexed as its own source. */
46
+ nested(src: string, end: number): TextStops;
47
+ /**
48
+ * Whether the bare-URL rule can match at `pos`: its first alternative needs a scheme there, and its second a
49
+ * run of e-mail local characters from `pos` to an "@" (the only "@" such a run can reach) with a dotted domain
50
+ * right after that "@", which is the same test for every offset of the run.
51
+ */
52
+ bareUrlCanStart(pos: number): boolean;
53
+ /**
54
+ * Length of the plain text at the start of `rest`, a suffix of the source: the distance to the nearest later
55
+ * offset where another token could start or an inline extension's start hint points.
56
+ */
57
+ textLength(rest: string): number;
58
+ }
59
+ /**
60
+ * Appends plain text to one inline source's token list, merging into a text token before it as marked does. A text
61
+ * token this run created grows by taking a longer slice of the source instead of by concatenation, so its raw stays
62
+ * a flat string: reading its last character (the emphasis rule's look-behind) does not copy the whole token.
63
+ */
64
+ export declare class TextRun {
65
+ #private;
66
+ constructor(src: string);
67
+ /** Appends the source text from `start` to `end`. */
68
+ append(tokens: Token[], start: number, end: number): void;
69
+ }
70
+ export {};
@@ -41,5 +41,27 @@ export declare function mathOpenerAt(source: string, at: number): MathOpener | u
41
41
  * though a backslash precedes it.
42
42
  */
43
43
  export declare function mathSpanAt(source: string, at: number, from?: number): MathSpan | undefined;
44
+ /**
45
+ * The math spans of one source, found as {@link mathSpanAt} finds them with the escape scan stopping at the opener
46
+ * (the caller has consumed the escapes before it). A closer scan also answers for the openers of its kind that it
47
+ * passed, which meet the same closer or none, so asking at every opener of a long run reads the run once.
48
+ */
49
+ export declare class MathSpans {
50
+ #private;
51
+ constructor(source: string);
52
+ /** The span opened at `at` that ends by `end`, or `undefined`. */
53
+ spanAt(at: number, end?: number): MathSpan | undefined;
54
+ }
55
+ /**
56
+ * The span opened at the start of `src`, with `end` counted from there, for a marked inline tokenizer: `src` is the
57
+ * part of `context.source` before `context.end` that the tokenizer received, and the scans are remembered per
58
+ * context, which marked keeps for one inline source and the link labels and emphasis inside it. Only a span that ends
59
+ * by `context.end` counts. Inside a label or emphasis, `context.end` holds its closer (`]`, `*` or `_`), which is not
60
+ * a digit, so a `$` right before it closes there as it would at the end of the text.
61
+ */
62
+ export declare function mathSpanInContext(context: {
63
+ source?: string;
64
+ end?: number;
65
+ }, src: string): MathSpan | undefined;
44
66
  /** The own-line display block starting at offset 0, or `undefined`. */
45
67
  export declare function mathBlockAt(source: string): MathBlock | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-utils",
3
- "version": "18.4.9",
3
+ "version": "18.4.10",
4
4
  "description": "Shared utilities for pi packages",
5
5
  "keywords": [
6
6
  "cli",
@@ -57,7 +57,7 @@
57
57
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
58
58
  },
59
59
  "dependencies": {
60
- "@oh-my-pi/pi-natives": "18.4.9"
60
+ "@oh-my-pi/pi-natives": "18.4.10"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/bun": "^1.3.14"