@orkestrel/markdown 0.0.10 → 0.0.12

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.
@@ -28,6 +28,7 @@ export declare interface BlockquoteNode {
28
28
  * unrecognized character, so coalescing keeps the AST clean and assertion-friendly.
29
29
  *
30
30
  * @param nodes - The inline nodes (possibly with adjacent text runs)
31
+ * @param spans - The optional operation-owned node span recorder
31
32
  * @returns The nodes with consecutive text nodes concatenated
32
33
  *
33
34
  * @example
@@ -36,7 +37,7 @@ export declare interface BlockquoteNode {
36
37
  * // [{ element: 'text', value: 'ab' }]
37
38
  * ```
38
39
  */
39
- export declare function coalesceText(nodes: readonly InlineNode[]): readonly InlineNode[];
40
+ export declare function coalesceText(nodes: readonly InlineNode[], spans?: Map<MarkdownNode, MarkdownSpan>): readonly InlineNode[];
40
41
 
41
42
  /**
42
43
  * A fenced code block - ```` ```lang ````. `code` is the verbatim block content (no
@@ -107,14 +108,16 @@ export declare const codeSpanShape: ObjectShape<{
107
108
  * @param lines - The markdown lines to scan.
108
109
  * @param start - The index of the first list item.
109
110
  * @param depth - The current recursion depth (each item recurses at `depth + 1`).
111
+ * @param spans - The optional operation-owned node span recorder.
112
+ * @param end - The original-source end of this line run, including a removed terminator.
110
113
  * @returns The parsed list node and the index of the first line after it.
111
114
  *
112
115
  * @example
113
116
  * ```ts
114
- * collectList(['- item'], 0, 0) // { node: { element: 'list', ... }, next: 1 }
117
+ * collectList(splitLines('- item'), 0, 0) // { node: { element: 'list', ... }, next: 1 }
115
118
  * ```
116
119
  */
117
- export declare function collectList(lines: readonly string[], start: number, depth: number): {
120
+ export declare function collectList(lines: readonly MarkdownSource[], start: number, depth: number, spans?: Map<MarkdownNode, MarkdownSpan>, end?: number): {
118
121
  readonly node: ListNode;
119
122
  readonly next: number;
120
123
  };
@@ -125,14 +128,15 @@ export declare function collectList(lines: readonly string[], start: number, dep
125
128
  *
126
129
  * @param lines - The markdown lines to scan.
127
130
  * @param start - The index of the header row.
131
+ * @param spans - The optional operation-owned node span recorder.
128
132
  * @returns The parsed table node and the index of the first line after it.
129
133
  *
130
134
  * @example
131
135
  * ```ts
132
- * collectTable(['| a |', '| - |'], 0) // { node: { element: 'table', ... }, next: 2 }
136
+ * collectTable(splitLines('| a |\n| - |'), 0) // { node: { element: 'table', ... }, next: 2 }
133
137
  * ```
134
138
  */
135
- export declare function collectTable(lines: readonly string[], start: number): {
139
+ export declare function collectTable(lines: readonly MarkdownSource[], start: number, spans?: Map<MarkdownNode, MarkdownSpan>): {
136
140
  readonly node: TableNode;
137
141
  readonly next: number;
138
142
  };
@@ -345,22 +349,23 @@ export declare function extractFence(line: string): {
345
349
  } | undefined;
346
350
 
347
351
  /**
348
- * Extract an ATX heading line (`#` … `######` followed by text) into its
349
- * `{ level, text }`, or `undefined` when `line` is not a heading. A run of more than 6
350
- * `#`s, or `#`s not followed by whitespace + text, is not a
351
- * heading; an optional closing `###` run is stripped.
352
+ * Extracts an ATX heading line (`#` … `######` followed by text) into its level,
353
+ * trimmed text, and the text's offset inside the line. A run of more than 6 `#`s, or
354
+ * `#`s not followed by whitespace + text, is not a heading; an optional closing
355
+ * `###` run is stripped.
352
356
  *
353
357
  * @param line - The candidate line
354
- * @returns The heading level (1–6) and its raw inline text, or `undefined`
358
+ * @returns The heading level (1–6), raw inline text, and text offset, or `undefined`
355
359
  *
356
360
  * @example
357
361
  * ```ts
358
- * extractHeading('## Title') // { level: 2, text: 'Title' }
362
+ * extractHeading('## Title') // { level: 2, text: 'Title', offset: 3 }
359
363
  * ```
360
364
  */
361
365
  export declare function extractHeading(line: string): {
362
366
  readonly level: number;
363
367
  readonly text: string;
368
+ readonly offset: number;
364
369
  } | undefined;
365
370
 
366
371
  /**
@@ -837,6 +842,22 @@ export declare function isThematicBreakNode(node: MarkdownNode): node is Themati
837
842
  */
838
843
  export declare function isWhitespace(character: string): boolean;
839
844
 
845
+ /**
846
+ * Joins offset-bearing markdown sources while mapping a separator to the original
847
+ * region between adjacent mapped sources.
848
+ *
849
+ * @param sources - The sources to join
850
+ * @param separator - The derived text inserted between sources
851
+ * @returns The joined text and every source-backed segment
852
+ *
853
+ * @example
854
+ * ```ts
855
+ * joinSources(splitLines('a\nb'), '\n')
856
+ * // { text: 'a\nb', segments: [...] }
857
+ * ```
858
+ */
859
+ export declare function joinSources(sources: readonly MarkdownSource[], separator: string): MarkdownSource;
860
+
840
861
  /** A GFM hard line break - two or more trailing spaces before a newline. */
841
862
  export declare interface LineBreakNode {
842
863
  readonly element: 'break';
@@ -933,18 +954,77 @@ export declare interface ListNode {
933
954
  readonly items: readonly ListItemNode[];
934
955
  }
935
956
 
957
+ /**
958
+ * Locates an emphasis run at `start` (`*` / `_`, doubled for strong) - finds the nearest
959
+ * matching closing run of the same marker + width while skipping complete nested
960
+ * runs from the other marker family, and requires non-space immediately inside both
961
+ * delimiters (the CommonMark flanking simplification that blocks `* x *`). Returns
962
+ * the content and syntax bounds, or `undefined` when no valid closer exists (it then degrades to
963
+ * a literal marker).
964
+ *
965
+ * @param source - The inline source text
966
+ * @param start - The index of the opening marker
967
+ * @param to - The exclusive end of the scan window
968
+ * @returns The content and syntax bounds, or `undefined`
969
+ *
970
+ * @example
971
+ * ```ts
972
+ * locateEmphasis('*em*', 0, 4) // { strong: false, open: 1, close: 3, end: 4 }
973
+ * ```
974
+ */
975
+ export declare function locateEmphasis(source: string, start: number, to: number): {
976
+ readonly strong: boolean;
977
+ readonly open: number;
978
+ readonly close: number;
979
+ readonly end: number;
980
+ } | undefined;
981
+
982
+ /**
983
+ * Locates a link `[text](href)` at `start` - the text runs to a BALANCED `]`, then `(`
984
+ * must immediately follow and the destination runs to the matching `)` (both respect
985
+ * nested delimiters + escapes). Returns the label close and syntax end, or `undefined` when the shape
986
+ * does not hold (it then degrades to a literal `[`).
987
+ *
988
+ * @param source - The inline source text
989
+ * @param start - The index of the opening `[`
990
+ * @param to - The exclusive end of the scan window
991
+ * @returns The label close and syntax end indices, or `undefined`
992
+ *
993
+ * @example
994
+ * ```ts
995
+ * locateLink('[text](url)', 0, 11) // { close: 5, end: 11 }
996
+ * ```
997
+ */
998
+ export declare function locateLink(source: string, start: number, to: number): {
999
+ readonly close: number;
1000
+ readonly end: number;
1001
+ } | undefined;
1002
+
936
1003
  /**
937
1004
  * A stateful, parsed markdown document - wraps a typed {@link MarkdownDocument} AST
938
1005
  * with the query (`find` / `filter` / `reduce` / iteration), rewrite (`map`), fold, and
939
1006
  * streaming operations {@link MarkdownInterface} declares.
940
1007
  *
941
1008
  * @remarks
942
- * - **Construction.** Given a `string`, the constructor runs {@link parseDocument} (the
943
- * block phase then the inline phase) to build the AST. Given a {@link MarkdownDocument},
944
- * the document is adopted AS-IS and is NOT re-validated - a caller adopting an
945
- * untrusted value should gate it with `isMarkdownDocument` first.
1009
+ * - **Construction.** Given a `string`, the constructor runs {@link parseProvenance} (the
1010
+ * block phase then the inline phase) once, keeping the AST and a COPY of the span map
1011
+ * that parse recorded. Given a {@link MarkdownDocument}, the document is adopted AS-IS
1012
+ * and is NOT re-validated - gate an untrusted value with `isMarkdownDocument` first.
1013
+ * - **Provenance.** {@link span} reads the region of the ORIGINAL constructor string a
1014
+ * node was produced from, and it is handle-relative: a string-constructed handle exposes
1015
+ * the regions of the nodes it parsed, an adopted document exposes none, and a node from
1016
+ * another handle reports `undefined` here whatever that handle reports. Each call
1017
+ * returns a fresh value. A node reports the region THIS handle holds for its identity,
1018
+ * else the region of the direct input a rewrite named for it, else `undefined`: a text
1019
+ * run the parse joined from adjacent scanner output reports the region enclosing its
1020
+ * parts, and only a rewrite output that holds no region of its own and was assembled
1021
+ * from separate source nodes reports `undefined`.
1022
+ * {@link map} carries provenance across the rewrite: an unchanged node keeps its
1023
+ * region, a one-source replacement takes the region of the node it replaced, and a
1024
+ * rebuilt parent takes its original's.
946
1025
  * - **Immutable.** {@link map} never mutates the stored AST - it returns a NEW `Markdown`
947
- * instance; the document root invariant (`element: 'document'`) always holds.
1026
+ * instance; the document root invariant (`element: 'document'`) always holds. An
1027
+ * identity rewrite still returns a new handle, over the same document tree.
948
1028
  * - **Traversal order.** {@link walk} and the `find` / `filter` / `reduce` queries built
949
1029
  * on it walk the AST depth-first, pre-order, root-inclusive (via {@link walkNodes});
950
1030
  * `stream` is shallow - only the document's direct block children.
@@ -966,6 +1046,24 @@ export declare class Markdown implements MarkdownInterface {
966
1046
  constructor(input: string | MarkdownDocument);
967
1047
  /** The stored {@link MarkdownDocument} AST root. */
968
1048
  get document(): MarkdownDocument;
1049
+ /**
1050
+ * Reads the region of the original markdown string a node of this handle's tree was
1051
+ * produced from.
1052
+ *
1053
+ * @param node - The node whose provenance to read
1054
+ * @returns A fresh {@link MarkdownSpan}, or `undefined` when this handle holds no
1055
+ * region for the node
1056
+ *
1057
+ * @example
1058
+ * ```ts
1059
+ * const source = '# Title\n\npara'
1060
+ * const markdown = new Markdown(source)
1061
+ * const heading = markdown.find(isHeadingNode)
1062
+ * const span = heading && markdown.span(heading)
1063
+ * span && source.slice(span.start, span.end) // '# Title'
1064
+ * ```
1065
+ */
1066
+ span(node: MarkdownNode): MarkdownSpan | undefined;
969
1067
  /**
970
1068
  * THE deep traversal - a lazy, depth-first, pre-order, root-inclusive generator
971
1069
  * over every {@link MarkdownNode} in the document. `find` / `filter` / `reduce`
@@ -988,7 +1086,15 @@ export declare class Markdown implements MarkdownInterface {
988
1086
  find(predicate: (node: MarkdownNode) => boolean): MarkdownNode | undefined;
989
1087
  filter<T extends MarkdownNode>(guard: (node: MarkdownNode) => node is T): readonly T[];
990
1088
  filter(predicate: (node: MarkdownNode) => boolean): readonly MarkdownNode[];
991
- /** Rewrites the AST bottom-up (copy-on-write) and returns a new {@link Markdown}. */
1089
+ /**
1090
+ * Rewrites the AST bottom-up (copy-on-write) and returns a new {@link Markdown},
1091
+ * carrying each output node's provenance across the rewrite. A rewrite that returns
1092
+ * its node unchanged shares that subtree instead of copying it, so an identity
1093
+ * rewrite copies no node and still returns a new handle.
1094
+ *
1095
+ * @param rewrite - The bottom-up node rewrite
1096
+ * @returns A new handle over the rewritten document
1097
+ */
992
1098
  map(rewrite: MarkdownRewriteHandler): MarkdownInterface;
993
1099
  /** Folds the AST depth-first, pre-order into an accumulator. */
994
1100
  reduce<T>(callback: (accumulator: T, node: MarkdownNode) => T, initial: T): T;
@@ -1027,6 +1133,35 @@ export declare interface MarkdownCell {
1027
1133
  readonly inlines: readonly InlineNode[];
1028
1134
  }
1029
1135
 
1136
+ /**
1137
+ * Pairs a rewritten value with the input node each rewritten node was produced from -
1138
+ * what `rewriteDocument` returns, so provenance survives a rewrite instead of ending at
1139
+ * it. `T` is the rewritten value: the document for a whole-document rewrite.
1140
+ *
1141
+ * @remarks
1142
+ * `derivations` is keyed by the nodes of the OUTPUT, and each entry names the DIRECT
1143
+ * input the rewrite drew that output from. {@link MarkdownInterface.map} resolves each
1144
+ * output node against the source handle's own spans in a fixed order, and follows no
1145
+ * second derivation edge:
1146
+ *
1147
+ * - the output identity's OWN span in the source handle wins, whatever the map says,
1148
+ * so an identity the rewrite reused - one node returned for several inputs, or a
1149
+ * node the handler moved elsewhere in the tree - keeps the region it already had;
1150
+ * - otherwise the span of the direct input the entry names, where that input has one;
1151
+ * - otherwise none. Where the output identity holds no region of its own, a node mapped
1152
+ * to `undefined`, a node whose direct input has no span, and a node with no entry at
1153
+ * all each report `undefined`. Own-region resolution runs first, so an identity that
1154
+ * does hold a region keeps it in every one of those cases.
1155
+ *
1156
+ * An absent entry does not by itself mean the output node kept its identity. A node
1157
+ * the handler synthesized beneath its replacement is absent too, and it reports no
1158
+ * span because the rewrite named no input for it.
1159
+ */
1160
+ export declare type MarkdownDerivation<T> = readonly [
1161
+ value: T,
1162
+ derivations: ReadonlyMap<MarkdownNode, MarkdownNode | undefined>
1163
+ ];
1164
+
1030
1165
  /**
1031
1166
  * The root of a parsed markdown AST - the ordered block children of the whole
1032
1167
  * document. The value {@link MarkdownInterface.document} holds.
@@ -1105,10 +1240,10 @@ export declare interface MarkdownHandlers<T> {
1105
1240
  * of demand. Cancellable via the returned stream's own `cancel()`, async-iterable
1106
1241
  * wherever the platform supports it (Node, Deno, and browsers that ship the
1107
1242
  * proposal), and pipeable through any {@link TransformStream} / {@link WritableStream}.
1108
- * - **The seven-method surface.** `document` (the AST root), `walk` (the deep
1109
- * traversal), `find` / `filter` / `reduce` (queries built on `walk`), `map` (the
1110
- * bottom-up rewrite), `fold` (the total catamorphism), and `stream` (the shallow,
1111
- * backpressured top-level source).
1243
+ * - **The surface.** `document` (the AST root), `walk` (the deep traversal), `find` /
1244
+ * `filter` / `reduce` (queries built on `walk`), `span` (the region of the original
1245
+ * markdown a node was parsed from), `map` (the bottom-up rewrite), `fold` (the
1246
+ * total catamorphism), and `stream` (the shallow, backpressured top-level source).
1112
1247
  */
1113
1248
  export declare interface MarkdownInterface {
1114
1249
  /** The stored {@link MarkdownDocument} AST root. */
@@ -1131,6 +1266,26 @@ export declare interface MarkdownInterface {
1131
1266
  filter<T extends MarkdownNode>(guard: (node: MarkdownNode) => node is T): readonly T[];
1132
1267
  /** Collects every node (depth-first, pre-order) matching a predicate. */
1133
1268
  filter(predicate: (node: MarkdownNode) => boolean): readonly MarkdownNode[];
1269
+ /**
1270
+ * Reads the region of the original markdown string a node was produced from.
1271
+ *
1272
+ * @param node - A node of this handle's document.
1273
+ * @returns A fresh {@link MarkdownSpan}, or `undefined` when this handle holds no
1274
+ * region for the node.
1275
+ *
1276
+ * @remarks
1277
+ * Provenance is per handle and per node identity, so a node reports a region only
1278
+ * where THIS handle holds coordinates for it. A handle constructed from an adopted
1279
+ * {@link MarkdownDocument} reports `undefined` for every node: it parsed no string,
1280
+ * so no coordinates exist to report. A text run the PARSE joined from adjacent
1281
+ * scanner output reports the region enclosing its parts rather than `undefined`;
1282
+ * only a REWRITE output that holds no region of its own and was assembled from
1283
+ * separate source nodes reports `undefined`. The region a node does report is the
1284
+ * original source it was produced from, which can include syntax its value drops
1285
+ * and characters that normalization removed. Each call returns a fresh value rather
1286
+ * than the stored one.
1287
+ */
1288
+ span(node: MarkdownNode): MarkdownSpan | undefined;
1134
1289
  /** Rewrites the AST bottom-up (copy-on-write) and returns a new {@link MarkdownInterface}. */
1135
1290
  map(rewrite: MarkdownRewriteHandler): MarkdownInterface;
1136
1291
  /** Folds the AST depth-first, pre-order into an accumulator. */
@@ -1152,6 +1307,24 @@ export declare interface MarkdownInterface {
1152
1307
  */
1153
1308
  export declare type MarkdownNode = MarkdownDocument | BlockNode | ListItemNode | InlineNode;
1154
1309
 
1310
+ /**
1311
+ * Pairs a parsed document with the {@link MarkdownSpan} of each of its nodes - what
1312
+ * `parseProvenance` returns, and what `parseDocument` projects the document out of.
1313
+ *
1314
+ * @remarks
1315
+ * `spans` is keyed by node identity, so it addresses the nodes of THAT document and
1316
+ * no other. A node the parse merged from adjacent scanner output - the text run
1317
+ * `coalesceText` joins - is present and carries the region ENCLOSING its parts, from
1318
+ * the first part's `start` to the last part's `end`, which can include original text
1319
+ * lying between them. Absence means the parse recorded no region for the node, not
1320
+ * that the node was assembled from more than one region. Destructure it as
1321
+ * `const [document, spans] = parseProvenance(markdown)`.
1322
+ */
1323
+ export declare type MarkdownParseResult = readonly [
1324
+ document: MarkdownDocument,
1325
+ spans: ReadonlyMap<MarkdownNode, MarkdownSpan>
1326
+ ];
1327
+
1155
1328
  /**
1156
1329
  * What one HTML node projects to on the way to markdown - the fold value
1157
1330
  * `htmlToMarkdown` carries up the AST.
@@ -1194,6 +1367,91 @@ export declare interface MarkdownProjection {
1194
1367
  */
1195
1368
  export declare type MarkdownRewriteHandler = (node: MarkdownNode) => MarkdownNode;
1196
1369
 
1370
+ /**
1371
+ * Maps one run of a {@link MarkdownSource} back to the region of the ORIGINAL
1372
+ * markdown string it was taken from.
1373
+ *
1374
+ * @remarks
1375
+ * `offset` addresses {@link MarkdownSource.text}; `start` and `end` address the
1376
+ * original string. The run's original length derives from `end - start` rather than
1377
+ * being stored beside them, so no length member exists to drift. The run's DERIVED
1378
+ * extent ends where the next segment's `offset` begins, so a run may cover more of the
1379
+ * original than it holds derived: the separator run `joinSources` records over a
1380
+ * normalized `\r\n` terminator is one derived code unit over a two-unit original
1381
+ * region.
1382
+ *
1383
+ * `projectSpan` resolves a derived position `p` against that shape by the following
1384
+ * rules rather than by a single affine relation:
1385
+ *
1386
+ * - strictly inside the run, `p` projects to `start + (p - offset)`;
1387
+ * - at the run's derived end, `p` projects to `end`, so the boundary claims the run's
1388
+ * whole original region instead of the prefix an affine step would reach - which is
1389
+ * how the one-unit `\r\n` separator run above reports its two-unit region;
1390
+ * - a zero-width `p` that coincides with a later segment's `offset` resolves through the
1391
+ * LAST segment whose `offset` equals `p`, skipping every earlier segment at that
1392
+ * position whatever its extent, so a discontinuous abutment reports that final run's
1393
+ * `start` rather than the earlier run's `end`.
1394
+ *
1395
+ * The mapping is therefore affine strictly inside a run and clamped at its end.
1396
+ */
1397
+ export declare interface MarkdownSegment {
1398
+ /** The first code unit of the run inside {@link MarkdownSource.text}. */
1399
+ readonly offset: number;
1400
+ /** The first code unit of the original-string region the run was produced from, inclusive. */
1401
+ readonly start: number;
1402
+ /** The code unit one past that region's last, exclusive. */
1403
+ readonly end: number;
1404
+ }
1405
+
1406
+ /**
1407
+ * Pairs a piece of derived markdown text with the runs mapping it back to the
1408
+ * original string - what `splitLines` returns per line, so every phase downstream of
1409
+ * it keeps original coordinates instead of reconstructing them from node values.
1410
+ *
1411
+ * @remarks
1412
+ * `text` is the line a parser reads: its terminator, `>` quote marker, or leading
1413
+ * indent already removed. `segments` run in ascending `offset` order, one run per
1414
+ * contiguous stretch of the original; a piece assembled from separate stretches
1415
+ * carries one segment per stretch.
1416
+ *
1417
+ * The runs need not cover every position of `text`. `joinSources` records a segment
1418
+ * for its separator only where the two sides leave a gap in the original, so joining
1419
+ * two abutting regions with a separator leaves that separator's derived position
1420
+ * uncovered. `projectSpan` resolves a range's two boundaries against the runs
1421
+ * independently: it reports `undefined` when either boundary lands in an uncovered
1422
+ * position, and it bridges an uncovered interior when both boundaries resolve. Test
1423
+ * coverage with `projectSpan` rather than assuming it.
1424
+ */
1425
+ export declare interface MarkdownSource {
1426
+ /** The derived text a parser reads. */
1427
+ readonly text: string;
1428
+ /** The runs mapping `text` back to the original string, in ascending `offset` order. */
1429
+ readonly segments: readonly MarkdownSegment[];
1430
+ }
1431
+
1432
+ /**
1433
+ * Addresses a half-open region of the ORIGINAL markdown string, in UTF-16 code units -
1434
+ * `start` inclusive, `end` exclusive. The provenance a parse records for a node and
1435
+ * {@link MarkdownInterface.span} reads back.
1436
+ *
1437
+ * @remarks
1438
+ * The coordinates address the string the handle was constructed from, never the line
1439
+ * text a later phase walks, so `markdown.slice(span.start, span.end)` returns the
1440
+ * ORIGINAL source region the node was produced from. That region is not the node's
1441
+ * value: it carries the syntax the value drops, such as a `\` escape marker, and the
1442
+ * characters that normalization removed, such as a trailing space the paragraph phase
1443
+ * trimmed. The text node of `'a \nb'` has the `value` `a\nb` and reports
1444
+ * `{ start: 0, end: 4 }`, which slices the whole `a \nb`. Read a value off the node
1445
+ * and a region off the source; never derive either from the other. The region's length
1446
+ * is `end - start`; no length member exists to drift from the two offsets.
1447
+ */
1448
+ export declare interface MarkdownSpan {
1449
+ /** The first code unit of the region, inclusive. */
1450
+ readonly start: number;
1451
+ /** The code unit one past the region's last, exclusive. */
1452
+ readonly end: number;
1453
+ }
1454
+
1197
1455
  /**
1198
1456
  * Project a {@link MarkdownNode} into an unsanitized {@link HTMLDocument}.
1199
1457
  *
@@ -1280,6 +1538,22 @@ export declare function mergeProjections(children: readonly MarkdownProjection[]
1280
1538
  */
1281
1539
  export declare function normalizeInlines(nodes: readonly InlineNode[], breaks: boolean): readonly InlineNode[];
1282
1540
 
1541
+ /**
1542
+ * Normalizes one paragraph line while retaining the full source run consumed by a
1543
+ * trailing-space hard break.
1544
+ *
1545
+ * @param source - The offset-bearing paragraph line
1546
+ * @param breaks - If `true`, preserves a trailing run of at least two spaces as the
1547
+ * scanner's two-space hard-break syntax; if `false`, trims the line normally
1548
+ * @returns The normalized line and its original-string segments
1549
+ *
1550
+ * @example
1551
+ * ```ts
1552
+ * normalizeParagraphLine(splitLines('text \nnext')[0], true).text // 'text '
1553
+ * ```
1554
+ */
1555
+ export declare function normalizeParagraphLine(source: MarkdownSource, breaks: boolean): MarkdownSource;
1556
+
1283
1557
  /** A paragraph - a run of non-blank lines that is not another block; `children` its inline content. */
1284
1558
  export declare interface ParagraphNode {
1285
1559
  readonly element: 'paragraph';
@@ -1293,14 +1567,16 @@ export declare interface ParagraphNode {
1293
1567
  *
1294
1568
  * @param lines - The markdown lines to parse.
1295
1569
  * @param depth - The current recursion depth (blockquotes/lists increment it).
1570
+ * @param spans - The optional operation-owned node span recorder.
1571
+ * @param end - The original-source end of this line run, including a removed terminator.
1296
1572
  * @returns The parsed block nodes.
1297
1573
  *
1298
1574
  * @example
1299
1575
  * ```ts
1300
- * parseBlocks(['# Hi'], 0) // [{ element: 'heading', level: 1, children: [...] }]
1576
+ * parseBlocks(splitLines('# Hi'), 0) // [{ element: 'heading', level: 1, children: [...] }]
1301
1577
  * ```
1302
1578
  */
1303
- export declare function parseBlocks(lines: readonly string[], depth: number): readonly BlockNode[];
1579
+ export declare function parseBlocks(lines: readonly MarkdownSource[], depth: number, spans?: Map<MarkdownNode, MarkdownSpan>, end?: number): readonly BlockNode[];
1304
1580
 
1305
1581
  /**
1306
1582
  * Parses a markdown string into a typed {@link MarkdownDocument} AST via the
@@ -1320,6 +1596,14 @@ export declare function parseDocument(markdown: string): MarkdownDocument;
1320
1596
  */
1321
1597
  export declare function parseInline(text: string): readonly InlineNode[];
1322
1598
 
1599
+ /**
1600
+ * Parses a markdown string into a document and its original-source spans.
1601
+ *
1602
+ * @param markdown - The markdown source to parse.
1603
+ * @returns The parsed document and its node-identity span map.
1604
+ */
1605
+ export declare function parseProvenance(markdown: string): MarkdownParseResult;
1606
+
1323
1607
  /**
1324
1608
  * Project one HTML leaf - a text node, a comment, or a doctype - to its
1325
1609
  * {@link MarkdownProjection}.
@@ -1420,6 +1704,23 @@ export declare function projectionToBlocks(projection: MarkdownProjection): read
1420
1704
  */
1421
1705
  export declare function projectionToInlines(projection: MarkdownProjection): readonly InlineNode[];
1422
1706
 
1707
+ /**
1708
+ * Projects a derived text range through its segments to a half-open region of the
1709
+ * original markdown string.
1710
+ *
1711
+ * @param source - The offset-bearing source carrying the range
1712
+ * @param from - The inclusive derived-text boundary
1713
+ * @param to - The exclusive derived-text boundary
1714
+ * @returns The original-string span, or `undefined` when either boundary is unmapped
1715
+ *
1716
+ * @example
1717
+ * ```ts
1718
+ * projectSpan({ text: 'a', segments: [{ offset: 0, start: 4, end: 5 }] }, 0, 1)
1719
+ * // { start: 4, end: 5 }
1720
+ * ```
1721
+ */
1722
+ export declare function projectSpan(source: MarkdownSource, from: number, to: number): MarkdownSpan | undefined;
1723
+
1423
1724
  /**
1424
1725
  * Render a {@link MarkdownNode} to sanitized canonical HTML.
1425
1726
  *
@@ -1479,13 +1780,14 @@ export declare function renderMarkdown(node: MarkdownNode): string;
1479
1780
  * always holds). A table's inline cells and a list's items ARE rewritten.
1480
1781
  *
1481
1782
  * @remarks
1482
- * Never mutates `document` - every level is rebuilt into a fresh object/array, even
1483
- * when `rewrite` returns its input unchanged. When `rewrite` returns a node whose
1484
- * `element` does not fit the slot it was called for (a block slot handed a
1783
+ * Never mutates `document`. An unchanged subtree keeps its input identity. A parent
1784
+ * is rebuilt only when an accepted child changes, and the returned derivation map
1785
+ * associates each rebuilt output with its input node. When `rewrite` returns a node
1786
+ * whose `element` does not fit the slot it was called for (a block slot handed a
1485
1787
  * non-{@link BlockNode}, an inline slot handed a non-{@link InlineNode}, a list-item
1486
- * slot handed a non-`listItem`), the ill-fitting result is discarded and the
1487
- * freshly-rebuilt (unrewritten-at-this-level) node is kept instead - `rewriteDocument`
1488
- * stays total and never produces a structurally invalid document.
1788
+ * slot handed a non-`listItem`), the ill-fitting result is discarded and the accepted
1789
+ * input child is reused - `rewriteDocument` stays total and never produces a
1790
+ * structurally invalid document.
1489
1791
  *
1490
1792
  * Descent is capped at {@link MAX_DEPTH}, the same cap {@link walkNodes} and
1491
1793
  * {@link foldNode} observe: at `depth >= MAX_DEPTH` the subtree is passed through
@@ -1495,16 +1797,16 @@ export declare function renderMarkdown(node: MarkdownNode): string;
1495
1797
  *
1496
1798
  * @param document - The document AST to rewrite
1497
1799
  * @param rewrite - The bottom-up {@link MarkdownRewriteHandler}
1498
- * @returns A new, rewritten {@link MarkdownDocument}
1800
+ * @returns The rewritten document and its output-to-input derivations
1499
1801
  *
1500
1802
  * @example
1501
1803
  * ```ts
1502
- * rewriteDocument(document, (node) =>
1804
+ * const [rewritten, derivations] = rewriteDocument(document, (node) =>
1503
1805
  * node.element === 'text' ? { element: 'text', value: node.value.toUpperCase() } : node,
1504
1806
  * )
1505
1807
  * ```
1506
1808
  */
1507
- export declare function rewriteDocument(document: MarkdownDocument, rewrite: MarkdownRewriteHandler): MarkdownDocument;
1809
+ export declare function rewriteDocument(document: MarkdownDocument, rewrite: MarkdownRewriteHandler): MarkdownDerivation<MarkdownDocument>;
1508
1810
 
1509
1811
  /**
1510
1812
  * Scan an inline code span at `start` (a `` ` ``-run … a matching `` ` ``-run of the
@@ -1528,25 +1830,25 @@ export declare function scanCode(source: string, start: number, to: number): {
1528
1830
  } | undefined;
1529
1831
 
1530
1832
  /**
1531
- * Scan an emphasis run at `start` (`*` / `_`, doubled for strong) - finds the nearest
1532
- * matching closing run of the same marker + width while skipping complete nested
1533
- * runs from the other marker family, and requires non-space immediately inside both
1534
- * delimiters (the CommonMark flanking simplification that blocks `* x *`). Returns
1535
- * the emphasis node, or `undefined` when no valid closer exists (it then degrades to
1536
- * a literal marker).
1833
+ * Scans an emphasis run at `start` (`*` / `_`, doubled for strong) - finds the nearest
1834
+ * matching closing run of the same marker + width while skipping complete nested runs
1835
+ * from the other marker family, and requires non-space immediately inside both
1836
+ * delimiters (the CommonMark flanking simplification that blocks `* x *`) through
1837
+ * {@link locateEmphasis}, and returns the parsed node and end index. Returns
1838
+ * `undefined` when no valid closer exists (it then degrades to a literal marker).
1537
1839
  *
1538
1840
  * @param source - The inline source text
1539
1841
  * @param start - The index of the opening marker
1540
1842
  * @param to - The exclusive end of the scan window
1541
- * @param depth - The current inline-recursion depth (defaults to 0 at the entry point);
1542
- * at {@link MAX_DEPTH} the emphasis's children degrade to literal text instead of
1543
- * recursing further
1544
- * @returns The parsed {@link EmphasisNode} + end index, or `undefined`
1843
+ * @param depth - The current inline-recursion depth, forwarded to {@link scanInline}
1844
+ * incremented by one for the run's children. At {@link MAX_DEPTH} that recursion
1845
+ * emits the content as a single literal text node instead of scanning it.
1846
+ * @returns The parsed emphasis and end index, or `undefined` when no closer exists
1545
1847
  *
1546
1848
  * @example
1547
1849
  * ```ts
1548
1850
  * scanEmphasis('*em*', 0, 4)
1549
- * // { node: { element: 'emphasis', strong: false, children: [...] }, end: 4 }
1851
+ * // { node: { element: 'emphasis', strong: false, children: [{ element: 'text', value: 'em' }] }, end: 4 }
1550
1852
  * ```
1551
1853
  */
1552
1854
  export declare function scanEmphasis(source: string, start: number, to: number, depth?: number): {
@@ -1579,23 +1881,48 @@ export declare function scanEmphasis(source: string, start: number, to: number,
1579
1881
  export declare function scanInline(source: string, from: number, to: number, depth?: number): readonly InlineNode[];
1580
1882
 
1581
1883
  /**
1582
- * Scan a link `[text](href)` at `start` - the text runs to a BALANCED `]`, then `(`
1884
+ * Scans an offset-bearing inline window with the same engine as {@link scanInline}
1885
+ * and records each emitted node against the original markdown string.
1886
+ *
1887
+ * @param source - The offset-bearing inline source
1888
+ * @param from - The inclusive start of the scan window
1889
+ * @param to - The exclusive end of the scan window
1890
+ * @param spans - The operation-owned node span recorder
1891
+ * @param depth - The current inline-recursion depth
1892
+ * @returns The parsed inline nodes before adjacent text coalescing
1893
+ *
1894
+ * @example
1895
+ * ```ts
1896
+ * scanInlineSource(
1897
+ * { text: 'hi *there*', segments: [{ offset: 0, start: 0, end: 10 }] },
1898
+ * 0,
1899
+ * 10,
1900
+ * new Map(),
1901
+ * )
1902
+ * // [{ element: 'text', value: 'hi ' }, { element: 'emphasis', ... }]
1903
+ * ```
1904
+ */
1905
+ export declare function scanInlineSource(source: MarkdownSource, from: number, to: number, spans: Map<MarkdownNode, MarkdownSpan>, depth?: number): readonly InlineNode[];
1906
+
1907
+ /**
1908
+ * Scans a link `[text](href)` at `start` - the text runs to a BALANCED `]`, then `(`
1583
1909
  * must immediately follow and the destination runs to the matching `)` (both respect
1584
- * nested delimiters + escapes). Returns the link node, or `undefined` when the shape
1585
- * does not hold (it then degrades to a literal `[`).
1910
+ * nested delimiters + escapes) through {@link locateLink}, and returns the parsed node
1911
+ * and end index. Returns `undefined` when the shape does not hold (it then degrades to
1912
+ * a literal `[`).
1586
1913
  *
1587
1914
  * @param source - The inline source text
1588
1915
  * @param start - The index of the opening `[`
1589
1916
  * @param to - The exclusive end of the scan window
1590
- * @param depth - The current inline-recursion depth (defaults to 0 at the entry point);
1591
- * at {@link MAX_DEPTH} the link's text children degrade to literal text instead of
1592
- * recursing further
1593
- * @returns The parsed {@link LinkNode} + end index, or `undefined`
1917
+ * @param depth - The current inline-recursion depth, forwarded to {@link scanInline}
1918
+ * incremented by one for the link text's children. At {@link MAX_DEPTH} that
1919
+ * recursion emits the text as a single literal text node instead of scanning it.
1920
+ * @returns The parsed link and end index, or `undefined` when the shape does not hold
1594
1921
  *
1595
1922
  * @example
1596
1923
  * ```ts
1597
1924
  * scanLink('[text](url)', 0, 11)
1598
- * // { node: { element: 'link', href: 'url', children: [...] }, end: 11 }
1925
+ * // { node: { element: 'link', href: 'url', children: [{ element: 'text', value: 'text' }] }, end: 11 }
1599
1926
  * ```
1600
1927
  */
1601
1928
  export declare function scanLink(source: string, start: number, to: number, depth?: number): {
@@ -1604,25 +1931,42 @@ export declare function scanLink(source: string, start: number, to: number, dept
1604
1931
  } | undefined;
1605
1932
 
1606
1933
  /**
1607
- * Normalize line endings to `\n` and split a markdown document into its lines - CRLF
1608
- * (`\r\n`) and bare CR (`\r`) both collapse to `\n` first, so a Windows-origin
1609
- * document parses identically. A single trailing newline does not yield a final
1610
- * empty line.
1934
+ * Slices derived markdown text and narrows each intersecting source segment to the
1935
+ * same text-relative range.
1936
+ *
1937
+ * @param source - The offset-bearing source to slice
1938
+ * @param from - The inclusive text offset
1939
+ * @param to - The exclusive text offset
1940
+ * @returns The sliced text and its narrowed original-string segments
1941
+ *
1942
+ * @example
1943
+ * ```ts
1944
+ * sliceSource({ text: 'abc', segments: [{ offset: 0, start: 4, end: 7 }] }, 1, 3)
1945
+ * // { text: 'bc', segments: [{ offset: 0, start: 5, end: 7 }] }
1946
+ * ```
1947
+ */
1948
+ export declare function sliceSource(source: MarkdownSource, from: number, to: number): MarkdownSource;
1949
+
1950
+ /**
1951
+ * Splits a markdown document into offset-bearing lines while normalizing CRLF and
1952
+ * bare CR terminators at the line boundary. A single trailing terminator does not
1953
+ * yield a final empty line.
1611
1954
  *
1612
1955
  * @param markdown - The raw markdown source
1613
- * @returns The document's lines, line-terminators stripped
1956
+ * @returns The document's lines with their original-string coordinates
1614
1957
  *
1615
1958
  * @example
1616
1959
  * ```ts
1617
- * splitLines('a\r\nb\nc') // ['a', 'b', 'c']
1960
+ * splitLines('a\r\nb') // [{ text: 'a', segments: [{ offset: 0, start: 0, end: 1 }] }, ...]
1618
1961
  * ```
1619
1962
  */
1620
- export declare function splitLines(markdown: string): readonly string[];
1963
+ export declare function splitLines(markdown: string): readonly MarkdownSource[];
1621
1964
 
1622
1965
  /**
1623
1966
  * Split one GFM table row into its cell strings - outer pipes are optional, an escaped
1624
1967
  * pipe (`\|`) inside a cell is NOT a separator (it becomes a literal `|`), and the
1625
- * empty leading / trailing cell produced by an outer `|` is dropped.
1968
+ * empty leading / trailing cell produced by an outer `|` is dropped. Derives the string
1969
+ * form from {@link splitTableSources}, which owns the escaped-pipe splitting rule.
1626
1970
  *
1627
1971
  * @param row - The raw table row line
1628
1972
  * @returns The row's cells, in column order
@@ -1634,6 +1978,20 @@ export declare function splitLines(markdown: string): readonly string[];
1634
1978
  */
1635
1979
  export declare function splitTableRow(row: string): readonly string[];
1636
1980
 
1981
+ /**
1982
+ * Splits an offset-bearing GFM table row into offset-bearing cells, retaining the
1983
+ * complete source spelling of an escaped pipe while exposing its literal value.
1984
+ *
1985
+ * @param row - The offset-bearing table row
1986
+ * @returns The row's cells with their original-string coordinates
1987
+ *
1988
+ * @example
1989
+ * ```ts
1990
+ * splitTableSources(splitLines('| a\\|b |')[0]).map((cell) => cell.text) // [' a|b ']
1991
+ * ```
1992
+ */
1993
+ export declare function splitTableSources(row: MarkdownSource): readonly MarkdownSource[];
1994
+
1637
1995
  /**
1638
1996
  * Whether the line at `index` starts a NEW block kind (heading / fence / thematic
1639
1997
  * break / blockquote / list / table) - the paragraph collector stops at such a line
@@ -1653,18 +2011,20 @@ export declare function splitTableRow(row: string): readonly string[];
1653
2011
  export declare function startsBlock(lines: readonly string[], index: number): boolean;
1654
2012
 
1655
2013
  /**
1656
- * Strip one level of blockquote marker (`>` plus one optional following space) from a
1657
- * blockquote line, so the de-quoted lines re-parse as nested blocks.
2014
+ * Strips one level of blockquote marker (`>` plus one optional following space) from
2015
+ * an offset-bearing blockquote line, so the de-quoted source re-parses as nested
2016
+ * blocks without losing its original coordinates.
1658
2017
  *
1659
- * @param line - A blockquote line (per {@link isQuote})
1660
- * @returns The line with its leading `>` (and one space) removed
2018
+ * @param source - A blockquote line (per {@link isQuote})
2019
+ * @returns The source with its leading `>` and optional space removed
1661
2020
  *
1662
2021
  * @example
1663
2022
  * ```ts
1664
- * stripQuote('> text') // 'text'
2023
+ * stripQuote({ text: '> text', segments: [{ offset: 0, start: 0, end: 6 }] })
2024
+ * // { text: 'text', segments: [{ offset: 0, start: 2, end: 6 }] }
1665
2025
  * ```
1666
2026
  */
1667
- export declare function stripQuote(line: string): string;
2027
+ export declare function stripQuote(source: MarkdownSource): MarkdownSource;
1668
2028
 
1669
2029
  /**
1670
2030
  * The horizontal alignment of a GFM table column, as declared by its delimiter row
@@ -1784,6 +2144,20 @@ export declare const thematicBreakShape: ObjectShape<{
1784
2144
  */
1785
2145
  export declare function trimInlines(nodes: readonly InlineNode[]): readonly InlineNode[];
1786
2146
 
2147
+ /**
2148
+ * Trims an offset-bearing source without losing the coordinates of its retained text.
2149
+ *
2150
+ * @param source - The source to trim
2151
+ * @returns The trimmed text and its narrowed original-string segments
2152
+ *
2153
+ * @example
2154
+ * ```ts
2155
+ * trimSource({ text: ' a ', segments: [{ offset: 0, start: 4, end: 7 }] })
2156
+ * // { text: 'a', segments: [{ offset: 0, start: 5, end: 6 }] }
2157
+ * ```
2158
+ */
2159
+ export declare function trimSource(source: MarkdownSource): MarkdownSource;
2160
+
1787
2161
  /**
1788
2162
  * Resolve backslash escapes in a raw string to their literal characters - used for a
1789
2163
  * link `href` (which is not otherwise inline-parsed) and any plain text run.