@ai-matrx/content-ir 0.11.6 → 0.12.0
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 +52 -0
- package/README.md +2 -0
- package/dist/index.cjs +1441 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1424 -9
- package/dist/index.js.map +1 -1
- package/dist/source.cjs +1437 -0
- package/dist/source.cjs.map +1 -0
- package/dist/source.d.cts +306 -0
- package/dist/source.d.ts +306 -0
- package/dist/source.js +1418 -0
- package/dist/source.js.map +1 -0
- package/package.json +16 -3
package/dist/source.d.ts
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE OFFSET-PRESERVING SOURCE TOKENIZER (rich-content PLAN decision 6).
|
|
3
|
+
*
|
|
4
|
+
* Cuts stored markdown into blocks that cover the text EXACTLY: the
|
|
5
|
+
* concatenation of every block's `raw` is the input, always, byte for byte.
|
|
6
|
+
* Nothing is trimmed, normalized, or re-serialized here — the tokenizer only
|
|
7
|
+
* draws boundaries. Every custom construct the platform stores (kinds and
|
|
8
|
+
* `__kind` JSON, typed JSON blocks, special fences, XML sections including
|
|
9
|
+
* attribute-bearing, mid-line and unknown containers, `{{variables}}`,
|
|
10
|
+
* `<matrxcite/>`, math, raw HTML, code fences, HTML comments and pinned
|
|
11
|
+
* anchors `<!--@a:…-->`) is an ISLAND: an atomic span no editor may parse
|
|
12
|
+
* into its own model. Islands that sit inside running prose (a variable, a
|
|
13
|
+
* citation, inline math) stay inside their paragraph as `inlines`, so a
|
|
14
|
+
* visual editor keeps the paragraph whole and holds the island as an atom.
|
|
15
|
+
*
|
|
16
|
+
* Block kinds:
|
|
17
|
+
* - `prose` — a markdown paragraph run (headings, lists, tables…): the only
|
|
18
|
+
* thing a visual editor may re-serialize, and only when edited.
|
|
19
|
+
* - `island` — a protected construct; `islandType` says which.
|
|
20
|
+
* - `gap` — the line terminator(s) and blank lines between blocks. Never
|
|
21
|
+
* edited; they keep the document's exact spacing.
|
|
22
|
+
*
|
|
23
|
+
* Streaming-tolerant: never throws on partial input. A construct whose
|
|
24
|
+
* closer has not arrived yet becomes an island running to the end of the text
|
|
25
|
+
* with `complete: false` — the same ownership rule the renderer's splitter
|
|
26
|
+
* applies, so a half-streamed fence or XML section is never reflowed.
|
|
27
|
+
*
|
|
28
|
+
* Boundaries follow matrx-frontend's renderer splitter (`content-splitter-v2.ts`)
|
|
29
|
+
* wherever it decides where a construct ends, so the editor and the renderer
|
|
30
|
+
* agree on what is locked. Offsets are UTF-16 (`start`/`end`) and code points
|
|
31
|
+
* (`startCp`/`endCp`).
|
|
32
|
+
*/
|
|
33
|
+
type SourceBlockKind = "prose" | "island" | "gap";
|
|
34
|
+
/** Island types that own whole blocks. */
|
|
35
|
+
type BlockIslandType =
|
|
36
|
+
/** ``` / ~~~ fenced code of any language (meta.lang, meta.special). */
|
|
37
|
+
"fence"
|
|
38
|
+
/** A bare JSON object block — typed JSON, `__kind` instances, directives. */
|
|
39
|
+
| "json"
|
|
40
|
+
/** A known attribute-less XML region (`<thinking>`, `<flashcards>`, …). */
|
|
41
|
+
| "xml_region"
|
|
42
|
+
/** An attribute-bearing XML element (`<artifact …>`, `<decision …>`, …). */
|
|
43
|
+
| "xml_attr"
|
|
44
|
+
/** Any other line-leading, balanced XML element. */
|
|
45
|
+
| "xml_container"
|
|
46
|
+
/** Allow-listed raw HTML block. */
|
|
47
|
+
| "html_block"
|
|
48
|
+
/** `<!-- … -->` comment. */
|
|
49
|
+
| "html_comment"
|
|
50
|
+
/** A pinned annotation anchor `<!--@a:id-->`. */
|
|
51
|
+
| "anchor"
|
|
52
|
+
/** `$$ … $$` display math on its own lines. */
|
|
53
|
+
| "math_block"
|
|
54
|
+
/** YAML front matter at the start of the text. */
|
|
55
|
+
| "front_matter"
|
|
56
|
+
/** A box-drawing tree diagram. */
|
|
57
|
+
| "tree";
|
|
58
|
+
/** Island types that live inside a prose block. */
|
|
59
|
+
type InlineIslandType = "variable" | "cite" | "math_inline" | "html_tag" | "xml_tag" | "xml_inline" | "html_comment" | "anchor" | "kind_json" | "media_ref";
|
|
60
|
+
type IslandType = BlockIslandType | InlineIslandType;
|
|
61
|
+
type IslandMeta = Readonly<Record<string, string | number | boolean>>;
|
|
62
|
+
interface SourceSpan {
|
|
63
|
+
/** UTF-16 offset of the first code unit. */
|
|
64
|
+
readonly start: number;
|
|
65
|
+
/** UTF-16 offset just past the last code unit. */
|
|
66
|
+
readonly end: number;
|
|
67
|
+
/** Code-point offset of `start`. */
|
|
68
|
+
readonly startCp: number;
|
|
69
|
+
/** Code-point offset of `end`. */
|
|
70
|
+
readonly endCp: number;
|
|
71
|
+
/** Exactly `text.slice(start, end)`. */
|
|
72
|
+
readonly raw: string;
|
|
73
|
+
}
|
|
74
|
+
interface SourceIsland extends SourceSpan {
|
|
75
|
+
readonly islandType: IslandType;
|
|
76
|
+
/** True when the island sits inside a prose block. */
|
|
77
|
+
readonly inline: boolean;
|
|
78
|
+
/** False when the construct's closer never arrived (streaming / malformed). */
|
|
79
|
+
readonly complete: boolean;
|
|
80
|
+
readonly meta: IslandMeta;
|
|
81
|
+
}
|
|
82
|
+
interface SourceBlock extends SourceSpan {
|
|
83
|
+
readonly kind: SourceBlockKind;
|
|
84
|
+
/** Set only on `island` blocks. */
|
|
85
|
+
readonly islandType: BlockIslandType | null;
|
|
86
|
+
readonly complete: boolean;
|
|
87
|
+
readonly meta: IslandMeta;
|
|
88
|
+
/** Inline islands of a `prose` block, in document order. Empty otherwise. */
|
|
89
|
+
readonly inlines: readonly SourceIsland[];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Cut `text` into blocks whose raw concatenation is exactly `text`.
|
|
93
|
+
* Never throws; unclosed constructs are islands with `complete: false`.
|
|
94
|
+
*/
|
|
95
|
+
declare function tokenizeSource(text: string): SourceBlock[];
|
|
96
|
+
/** The exact source the blocks were cut from. */
|
|
97
|
+
declare function joinSource(blocks: readonly SourceBlock[]): string;
|
|
98
|
+
/** Every island — block-level and inline — in document order. */
|
|
99
|
+
declare function listIslands(blocks: readonly SourceBlock[]): SourceIsland[];
|
|
100
|
+
/** The block containing UTF-16 offset `offset` (the later block at a boundary). */
|
|
101
|
+
declare function blockAt(blocks: readonly SourceBlock[], offset: number): SourceBlock | null;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* SAVE = SPLICE (rich-content PLAN decision 6).
|
|
105
|
+
*
|
|
106
|
+
* An editor never hands back a whole re-serialized document. It hands back
|
|
107
|
+
* per-block replacements for exactly the blocks the person changed; every
|
|
108
|
+
* other byte of the stored text is written back as it was. This module
|
|
109
|
+
* applies those replacements and returns:
|
|
110
|
+
*
|
|
111
|
+
* - the new text,
|
|
112
|
+
* - a change set (old range → new range, in UTF-16 AND code points,
|
|
113
|
+
* narrowed to the bytes that actually differ) that the annotation layer
|
|
114
|
+
* uses to carry every live anchor through the save
|
|
115
|
+
* (STORE-DESIGN §3.19 — the ProseMirror / Google Docs position-mapping
|
|
116
|
+
* model), and
|
|
117
|
+
* - an integrity report: every island outside the edited ranges must still
|
|
118
|
+
* be the same island, at its mapped position, in the new text. An edit
|
|
119
|
+
* that opens an unclosed fence and swallows a kind below it is caught
|
|
120
|
+
* here instead of being saved silently.
|
|
121
|
+
*
|
|
122
|
+
* A no-edit save (or edits whose text equals the original block) returns the
|
|
123
|
+
* original string itself — byte-identical, zero changes.
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/** Replace `[start, end)` (UTF-16, on block boundaries) of the original with `text`. */
|
|
127
|
+
interface SourceEdit {
|
|
128
|
+
readonly start: number;
|
|
129
|
+
readonly end: number;
|
|
130
|
+
readonly text: string;
|
|
131
|
+
}
|
|
132
|
+
interface SourceChange {
|
|
133
|
+
/** The block-aligned range the edit named, in the ORIGINAL text. */
|
|
134
|
+
readonly blockStart: number;
|
|
135
|
+
readonly blockEnd: number;
|
|
136
|
+
/** The narrowed range that actually differs: old text … */
|
|
137
|
+
readonly oldStart: number;
|
|
138
|
+
readonly oldEnd: number;
|
|
139
|
+
/** … and where its replacement sits in the NEW text. */
|
|
140
|
+
readonly newStart: number;
|
|
141
|
+
readonly newEnd: number;
|
|
142
|
+
readonly oldStartCp: number;
|
|
143
|
+
readonly oldEndCp: number;
|
|
144
|
+
readonly newStartCp: number;
|
|
145
|
+
readonly newEndCp: number;
|
|
146
|
+
}
|
|
147
|
+
interface DisturbedIsland {
|
|
148
|
+
readonly islandType: IslandType;
|
|
149
|
+
readonly inline: boolean;
|
|
150
|
+
/** Where the island was in the original (UTF-16). */
|
|
151
|
+
readonly start: number;
|
|
152
|
+
readonly end: number;
|
|
153
|
+
/** Where it should have been in the new text. */
|
|
154
|
+
readonly mappedStart: number;
|
|
155
|
+
}
|
|
156
|
+
interface SpliceIntegrity {
|
|
157
|
+
/** True when every untouched island survived intact and outside bytes are identical. */
|
|
158
|
+
readonly ok: boolean;
|
|
159
|
+
/** Always true by construction; checked anyway because it is THE guarantee. */
|
|
160
|
+
readonly bytesOutsideEditsIdentical: boolean;
|
|
161
|
+
readonly disturbed: readonly DisturbedIsland[];
|
|
162
|
+
}
|
|
163
|
+
interface SpliceResult {
|
|
164
|
+
readonly text: string;
|
|
165
|
+
readonly changed: boolean;
|
|
166
|
+
readonly changes: readonly SourceChange[];
|
|
167
|
+
readonly integrity: SpliceIntegrity;
|
|
168
|
+
/** Tokenization of the new text. */
|
|
169
|
+
readonly blocks: readonly SourceBlock[];
|
|
170
|
+
}
|
|
171
|
+
type SourceSpliceErrorCode = "out_of_range" | "misaligned" | "overlap" | "stale_blocks" | "integrity";
|
|
172
|
+
declare class SourceSpliceError extends Error {
|
|
173
|
+
readonly code: SourceSpliceErrorCode;
|
|
174
|
+
constructor(code: SourceSpliceErrorCode, message: string);
|
|
175
|
+
}
|
|
176
|
+
interface SpliceOptions {
|
|
177
|
+
/** The tokenization of `original`, when the caller already holds it. */
|
|
178
|
+
readonly blocks?: readonly SourceBlock[];
|
|
179
|
+
/** Throw `SourceSpliceError("integrity")` instead of reporting a disturbed island. */
|
|
180
|
+
readonly requireIntegrity?: boolean;
|
|
181
|
+
}
|
|
182
|
+
/** An edit that replaces one block's raw text. */
|
|
183
|
+
declare function blockEdit(block: SourceBlock, text: string): SourceEdit;
|
|
184
|
+
/** An edit that replaces a contiguous run of blocks `[first, last]`. */
|
|
185
|
+
declare function blockRangeEdit(first: SourceBlock, last: SourceBlock, text: string): SourceEdit;
|
|
186
|
+
/**
|
|
187
|
+
* Apply block-aligned replacements to `original`. Bytes outside the edits are
|
|
188
|
+
* copied, never rebuilt; an edit equal to its block changes nothing.
|
|
189
|
+
*/
|
|
190
|
+
declare function spliceSave(original: string, edits: readonly SourceEdit[], options?: SpliceOptions): SpliceResult;
|
|
191
|
+
interface MapOptions {
|
|
192
|
+
/** -1 keeps a position at an insertion point before the inserted text; 1 (default) moves it after. */
|
|
193
|
+
readonly assoc?: -1 | 1;
|
|
194
|
+
/** Offset unit of `pos` and of the result. Default `utf16`. */
|
|
195
|
+
readonly unit?: "utf16" | "codepoint";
|
|
196
|
+
}
|
|
197
|
+
interface MappedPosition {
|
|
198
|
+
readonly pos: number;
|
|
199
|
+
/** True when the position sat strictly inside replaced text. */
|
|
200
|
+
readonly deleted: boolean;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Carry a position in the old text through a change set (ProseMirror StepMap
|
|
204
|
+
* semantics): positions before a change stay, positions after shift by the
|
|
205
|
+
* size difference, a position strictly inside replaced text is `deleted` and
|
|
206
|
+
* lands on the replacement's edge chosen by `assoc`.
|
|
207
|
+
*/
|
|
208
|
+
declare function mapPosition(changes: readonly SourceChange[], pos: number, options?: MapOptions): MappedPosition;
|
|
209
|
+
interface MappedRange {
|
|
210
|
+
readonly start: number;
|
|
211
|
+
readonly end: number;
|
|
212
|
+
/** True when a change overlapped the range's interior. */
|
|
213
|
+
readonly touched: boolean;
|
|
214
|
+
/** True when the whole range was replaced away. */
|
|
215
|
+
readonly collapsed: boolean;
|
|
216
|
+
}
|
|
217
|
+
/** Carry an anchor range through a change set; it stays inside its text when edits happen around it. */
|
|
218
|
+
declare function mapRange(changes: readonly SourceChange[], start: number, end: number, unit?: "utf16" | "codepoint"): MappedRange;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* UTF-16 ⇄ code-point offset conversion.
|
|
222
|
+
*
|
|
223
|
+
* JavaScript strings are indexed in UTF-16 code units; Postgres `position()`,
|
|
224
|
+
* `substring()` and `length()` count code points. Every offset this module
|
|
225
|
+
* emits is given in BOTH units so an anchor captured on either side of the
|
|
226
|
+
* wire can be mapped without re-deriving the text.
|
|
227
|
+
*/
|
|
228
|
+
/** Indexes the surrogate pairs of one text so conversion is O(log pairs). */
|
|
229
|
+
interface CodePointIndex {
|
|
230
|
+
/** Code-point offset of a UTF-16 offset. */
|
|
231
|
+
toCodePoint(utf16: number): number;
|
|
232
|
+
/** UTF-16 offset of a code-point offset. */
|
|
233
|
+
toUtf16(codePoint: number): number;
|
|
234
|
+
/** Code-point length of the text. */
|
|
235
|
+
readonly lengthCp: number;
|
|
236
|
+
}
|
|
237
|
+
/** True when `offset` sits between the two halves of a surrogate pair. */
|
|
238
|
+
declare function splitsSurrogatePair(text: string, offset: number): boolean;
|
|
239
|
+
declare function buildCodePointIndex(text: string): CodePointIndex;
|
|
240
|
+
/** One-shot conversion; build an index when converting many offsets. */
|
|
241
|
+
declare function toCodePointOffset(text: string, utf16: number): number;
|
|
242
|
+
/** One-shot conversion; build an index when converting many offsets. */
|
|
243
|
+
declare function toUtf16Offset(text: string, codePoint: number): number;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* THE single-dollar math rule — the one predicate that decides whether
|
|
247
|
+
* `$…$` is inline math or currency.
|
|
248
|
+
*
|
|
249
|
+
* Moved here from matrx-frontend `components/markdown-core/math-normalizer.ts`
|
|
250
|
+
* (2026-09-23) so the renderer's normalizer and the source tokenizer can never
|
|
251
|
+
* disagree about which bytes are math: the normalizer imports this function.
|
|
252
|
+
*
|
|
253
|
+
* Pandoc's rule — the opening `$` is not followed by whitespace, the closing
|
|
254
|
+
* `$` is not preceded by whitespace and not followed by a digit — plus a math
|
|
255
|
+
* signal: a TeX command or operator (`\alpha`, `^`, `_`, `{`, `=`, …), or a
|
|
256
|
+
* lone variable (`$x$`, `$n$`, `$x'$`). Content that opens with a digit needs
|
|
257
|
+
* a real TeX signal, so a price range can never read as math.
|
|
258
|
+
*/
|
|
259
|
+
declare function isSingleDollarMath(content: string, after: string | undefined): boolean;
|
|
260
|
+
/**
|
|
261
|
+
* Where a single-dollar math span opened at `open` (a lone `$`) closes, or -1.
|
|
262
|
+
* Same scan the normalizer uses: same line, backslash-escapes skipped, the
|
|
263
|
+
* closer is a `$` not followed by another `$`, and the content passes
|
|
264
|
+
* {@link isSingleDollarMath}. Returns the offset just past the closing `$`.
|
|
265
|
+
*/
|
|
266
|
+
declare function singleDollarMathEnd(text: string, open: number, limit?: number): number;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* XML tag reading + the generic-container balance scanner.
|
|
270
|
+
*
|
|
271
|
+
* Ported VERBATIM (behaviour) from matrx-frontend
|
|
272
|
+
* `components/mardown-display/blocks/xml/readXmlTag.ts` and the
|
|
273
|
+
* `XmlContainerTracker` in
|
|
274
|
+
* `components/mardown-display/markdown-classification/processors/utils/content-splitter-v2.ts`
|
|
275
|
+
* so the source tokenizer ends a generic XML section exactly where the
|
|
276
|
+
* renderer does. The renderer copies stay for rendering (PLAN decision 6);
|
|
277
|
+
* a divergence between the two is a defect in whichever changed last.
|
|
278
|
+
*/
|
|
279
|
+
interface ReadXmlTag {
|
|
280
|
+
tagName: string;
|
|
281
|
+
raw: string;
|
|
282
|
+
isClosing: boolean;
|
|
283
|
+
isSelfClosing: boolean;
|
|
284
|
+
attributes: Array<{
|
|
285
|
+
name: string;
|
|
286
|
+
value: string;
|
|
287
|
+
}>;
|
|
288
|
+
}
|
|
289
|
+
declare function readXmlTag(content: string, start: number): ReadXmlTag | null;
|
|
290
|
+
/**
|
|
291
|
+
* Balances ONE unknown root tag line by line, ignoring lookalike tags inside
|
|
292
|
+
* fenced code, inline code spans, comments and CDATA. `consumeLine` returns
|
|
293
|
+
* the offset (within that line) just past the root's closing tag, or null.
|
|
294
|
+
*/
|
|
295
|
+
declare class XmlContainerTracker {
|
|
296
|
+
readonly rootTag: string;
|
|
297
|
+
private depth;
|
|
298
|
+
private inComment;
|
|
299
|
+
private inCdata;
|
|
300
|
+
private inlineTicks;
|
|
301
|
+
private fenceMarker;
|
|
302
|
+
constructor(rootTag: string);
|
|
303
|
+
consumeLine(line: string, startOffset?: number): number | null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export { type BlockIslandType, type CodePointIndex, type DisturbedIsland, type InlineIslandType, type IslandMeta, type IslandType, type MapOptions, type MappedPosition, type MappedRange, type ReadXmlTag, type SourceBlock, type SourceBlockKind, type SourceChange, type SourceEdit, type SourceIsland, type SourceSpan, SourceSpliceError, type SourceSpliceErrorCode, type SpliceIntegrity, type SpliceOptions, type SpliceResult, XmlContainerTracker, blockAt, blockEdit, blockRangeEdit, buildCodePointIndex, isSingleDollarMath, joinSource, listIslands, mapPosition, mapRange, readXmlTag, singleDollarMathEnd, spliceSave, splitsSurrogatePair, toCodePointOffset, toUtf16Offset, tokenizeSource };
|