@illusions-lab/mdi 2.0.18 → 2.0.20

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/README.md CHANGED
@@ -79,6 +79,46 @@ Applications should treat the IR version as a wire-protocol version. They
79
79
  must not infer grammar rules from object shapes or silently accept an
80
80
  unsupported version.
81
81
 
82
+ ## Searchable text blocks
83
+
84
+ `getMdiTextBlocks(source)` returns Rust-projected heading, paragraph, list,
85
+ blockquote, code, table, footnote, and HTML text in source order. Positions
86
+ such as `3:18` count one-based Unicode grapheme clusters; ruby readings are a
87
+ separate annotation channel anchored to the base-text range.
88
+
89
+ ```ts
90
+ import { getMdiTextBlocks, resolveMdiSourceSpan, resolveMdiSourceSpans, sourceSpansForTextRange } from "@illusions-lab/mdi";
91
+
92
+ const result = getMdiTextBlocks("{東京|とうきょう}");
93
+ const block = result.blocks[0];
94
+ console.log(block.text); // 東京
95
+ console.log(block.annotations[0].anchor); // { start: "1:1", end: "1:3" }
96
+ console.log(sourceSpansForTextRange(block, { start: "1:1", end: "1:3" }));
97
+ console.log(resolveMdiSourceSpan("{東京|とうきょう}", { startByte: 1, endByte: 7 }));
98
+ console.log(resolveMdiSourceSpans("same same", [{ startByte: 0, endByte: 4 }, { startByte: 5, endByte: 9 }]));
99
+ ```
100
+
101
+ `resolveMdiSourceSpan` accepts half-open UTF-8 byte offsets and returns ordered
102
+ `blockText` and `annotation` matches in canonical grapheme coordinates.
103
+ `coverage` is `complete`, `partial`, or `none`; each match is `exact` only when
104
+ its complete forward source coverage equals the requested span. Ruby base text
105
+ and readings are separate channels, and annotation indexes are zero-based.
106
+ Zero-width spans are valid and return no matches. Pure Markdown/MDI delimiters,
107
+ synthetic separators, and unmapped text do not acquire invented ranges, though
108
+ a delimiter token already owned by one projected grapheme (such as an explicit
109
+ break) can match. Reverse and forward mapping are therefore not generally
110
+ bijective, especially for annotations, multi-byte token mappings, partial
111
+ graphemes, discontinuous runs, and synthetic or unmapped text.
112
+ For multiple lookups against one document, use `resolveMdiSourceSpans`. It
113
+ validates the full array, performs one Rust parse/projection, and preserves
114
+ input order; repeated calls to the singular convenience API each parse anew.
115
+
116
+ Each source-derived grapheme is represented by a `sourceMap.runs` boundary;
117
+ table tabs/newlines and multi-paragraph joiners appear in `synthetic` and do
118
+ not receive invented source spans. `parseMdiTextPosition`,
119
+ `formatMdiTextPosition`, and `formatMdiTextRange` provide stateless coordinate
120
+ helpers.
121
+
82
122
  ## Rendering
83
123
 
84
124
  Rendering starts from the same Rust IR. Canonical MDI, plain text, HTML, EPUB,
@@ -2,6 +2,8 @@
2
2
  import * as mdiCore from "@illusions-lab/mdi-core";
3
3
  import { parse as parseYaml } from "yaml";
4
4
  var {
5
+ getMdiTextBlocksJson,
6
+ resolveMdiSourceSpansJson,
5
7
  parseMdiSyntaxJson,
6
8
  renderHtml: renderHtmlFromRust,
7
9
  renderEpub: renderEpubFromRust,
@@ -24,6 +26,7 @@ function requireLayoutSystem(profile) {
24
26
  }
25
27
  var MDI_SPEC_VERSION = "2.0";
26
28
  var MDI_IR_VERSION = "1.0";
29
+ var MDI_TEXT_PROJECTION_VERSION = "1.0";
27
30
  function parse(source) {
28
31
  if (typeof source !== "string") throw new TypeError("source must be a string");
29
32
  const result = JSON.parse(parseMdiSyntaxJson(source));
@@ -32,6 +35,144 @@ function parse(source) {
32
35
  }
33
36
  return result;
34
37
  }
38
+ function getMdiTextBlocks(source) {
39
+ if (typeof source !== "string") throw new TypeError("source must be a string");
40
+ const result = JSON.parse(getMdiTextBlocksJson(source));
41
+ if (result.projectionVersion !== MDI_TEXT_PROJECTION_VERSION) {
42
+ throw new Error(`Unsupported MDI text projection version: ${String(result.projectionVersion)}`);
43
+ }
44
+ return result;
45
+ }
46
+ function resolveMdiSourceSpan(source, span) {
47
+ return resolveMdiSourceSpans(source, [span])[0];
48
+ }
49
+ function resolveMdiSourceSpans(source, spans) {
50
+ if (typeof source !== "string") throw new TypeError("source must be a string");
51
+ if (!Array.isArray(spans)) throw new TypeError("spans must be an array");
52
+ if (spans.length === 0) return [];
53
+ const utf8 = utf8SourceBoundaries(source);
54
+ for (const [index, span] of spans.entries()) {
55
+ assertSourceSpanInput(span, `spans[${index}]`);
56
+ if (span.startByte > span.endByte) {
57
+ throw new RangeError(`spans[${index}].startByte must not exceed endByte`);
58
+ }
59
+ if (span.endByte > utf8.length) {
60
+ throw new RangeError(`spans[${index}] falls outside the UTF-8 source length`);
61
+ }
62
+ if (!utf8.boundaries.has(span.startByte) || !utf8.boundaries.has(span.endByte)) {
63
+ throw new RangeError(`spans[${index}] endpoints must be UTF-8 code-point boundaries`);
64
+ }
65
+ }
66
+ const results = JSON.parse(
67
+ resolveMdiSourceSpansJson(source, JSON.stringify(spans))
68
+ );
69
+ for (const result of results) {
70
+ if (result.projectionVersion !== MDI_TEXT_PROJECTION_VERSION) {
71
+ throw new Error(`Unsupported MDI text projection version: ${String(result.projectionVersion)}`);
72
+ }
73
+ }
74
+ return results;
75
+ }
76
+ function assertSourceSpanInput(span, name = "span") {
77
+ if (!span || typeof span !== "object" || Array.isArray(span)) {
78
+ throw new TypeError(`${name} must be an object`);
79
+ }
80
+ if (typeof span.startByte !== "number" || typeof span.endByte !== "number") {
81
+ throw new TypeError(`${name}.startByte and ${name}.endByte must be numbers`);
82
+ }
83
+ const isUint32 = (value) => Number.isInteger(value) && value >= 0 && value <= 4294967295;
84
+ if (!isUint32(span.startByte) || !isUint32(span.endByte)) {
85
+ throw new RangeError(`${name}.startByte and ${name}.endByte must be uint32 values`);
86
+ }
87
+ }
88
+ function utf8SourceBoundaries(source) {
89
+ let length = 0;
90
+ const boundaries = /* @__PURE__ */ new Set([0]);
91
+ for (const character of source) {
92
+ const codePoint = character.codePointAt(0);
93
+ length += codePoint <= 127 ? 1 : codePoint <= 2047 ? 2 : codePoint <= 65535 ? 3 : 4;
94
+ boundaries.add(length);
95
+ }
96
+ return { length, boundaries };
97
+ }
98
+ function parseMdiTextPosition(position) {
99
+ if (typeof position !== "string") throw new TypeError("position must be a string");
100
+ const match = /^([1-9]\d*):([1-9]\d*)$/.exec(position);
101
+ if (!match) throw new RangeError(`Invalid MDI text position: ${position}`);
102
+ const block = Number(match[1]);
103
+ const character = Number(match[2]);
104
+ if (!Number.isSafeInteger(block) || !Number.isSafeInteger(character)) {
105
+ throw new RangeError(`Invalid MDI text position: ${position}`);
106
+ }
107
+ return { block, character };
108
+ }
109
+ function formatMdiTextPosition(position) {
110
+ if (!position || typeof position !== "object") {
111
+ throw new TypeError("position must be an object");
112
+ }
113
+ if (!isPositiveSafeInteger(position.block) || !isPositiveSafeInteger(position.character)) {
114
+ throw new RangeError("position.block and position.character must be positive safe integers");
115
+ }
116
+ return `${position.block}:${position.character}`;
117
+ }
118
+ function formatMdiTextRange(range) {
119
+ if (!range || typeof range !== "object") throw new TypeError("range must be an object");
120
+ const start = parseMdiTextPosition(range.start);
121
+ const end = parseMdiTextPosition(range.end);
122
+ if (start.block !== end.block || end.character < start.character) {
123
+ throw new RangeError("range must be ordered within one text block");
124
+ }
125
+ return `${range.start}-${range.end}`;
126
+ }
127
+ function sourceSpansForTextRange(block, range) {
128
+ if (!block || typeof block !== "object") throw new TypeError("block must be an object");
129
+ const start = parseMdiTextPosition(range.start);
130
+ const end = parseMdiTextPosition(range.end);
131
+ if (start.block !== block.index || end.block !== block.index) {
132
+ throw new RangeError("range must belong to the supplied text block");
133
+ }
134
+ const blockEnd = parseMdiTextPosition(block.range.end).character;
135
+ if (end.character < start.character || start.character < 1 || end.character > blockEnd) {
136
+ throw new RangeError("range falls outside the supplied text block");
137
+ }
138
+ const spans = [];
139
+ let previousRunEnd = 1;
140
+ for (const run of block.sourceMap.runs) {
141
+ const runStartPosition = parseMdiTextPosition(run.range.start);
142
+ const runEndPosition = parseMdiTextPosition(run.range.end);
143
+ const runStart = runStartPosition.character;
144
+ const runEnd = runEndPosition.character;
145
+ const invalidRange = runStartPosition.block !== block.index || runEndPosition.block !== block.index || runEnd < runStart || runStart < previousRunEnd || runEnd > blockEnd;
146
+ const invalidBoundaries = run.sourceBoundaries.length !== runEnd - runStart + 1 || run.sourceBoundaries.some((boundary) => !Number.isSafeInteger(boundary) || boundary < 0) || run.sourceBoundaries.some((boundary, index) => index > 0 && boundary < run.sourceBoundaries[index - 1]) || block.span !== void 0 && run.sourceBoundaries.some(
147
+ (boundary) => boundary < block.span.startByte || boundary > block.span.endByte
148
+ );
149
+ if (invalidRange || invalidBoundaries) {
150
+ throw new Error("Invalid MDI text source run");
151
+ }
152
+ previousRunEnd = runEnd;
153
+ const overlapStart = Math.max(start.character, runStart);
154
+ const overlapEnd = Math.min(end.character, runEnd);
155
+ for (let character = overlapStart; character < overlapEnd; character += 1) {
156
+ const offset = character - runStart;
157
+ appendMergedSourceSpan(spans, {
158
+ startByte: run.sourceBoundaries[offset],
159
+ endByte: run.sourceBoundaries[offset + 1]
160
+ });
161
+ }
162
+ }
163
+ return spans;
164
+ }
165
+ function appendMergedSourceSpan(spans, next) {
166
+ const previous = spans.at(-1);
167
+ if (previous?.endByte === next.startByte) {
168
+ previous.endByte = next.endByte;
169
+ } else {
170
+ spans.push(next);
171
+ }
172
+ }
173
+ function isPositiveSafeInteger(value) {
174
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
175
+ }
35
176
  function renderHtml(source, options) {
36
177
  assertSource(source);
37
178
  assertHtmlOptions(options);
@@ -366,7 +507,15 @@ export {
366
507
  initializeMdi,
367
508
  MDI_SPEC_VERSION,
368
509
  MDI_IR_VERSION,
510
+ MDI_TEXT_PROJECTION_VERSION,
369
511
  parse,
512
+ getMdiTextBlocks,
513
+ resolveMdiSourceSpan,
514
+ resolveMdiSourceSpans,
515
+ parseMdiTextPosition,
516
+ formatMdiTextPosition,
517
+ formatMdiTextRange,
518
+ sourceSpansForTextRange,
370
519
  renderHtml,
371
520
  renderHtmlWithDiagnostics,
372
521
  prepareRender,
package/dist/index.cjs CHANGED
@@ -32,9 +32,14 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  MDI_IR_VERSION: () => MDI_IR_VERSION,
34
34
  MDI_SPEC_VERSION: () => MDI_SPEC_VERSION,
35
+ MDI_TEXT_PROJECTION_VERSION: () => MDI_TEXT_PROJECTION_VERSION,
36
+ formatMdiTextPosition: () => formatMdiTextPosition,
37
+ formatMdiTextRange: () => formatMdiTextRange,
38
+ getMdiTextBlocks: () => getMdiTextBlocks,
35
39
  initializeMdi: () => initializeMdi,
36
40
  parse: () => parse,
37
41
  parseMdiSyntax: () => parseMdiSyntax,
42
+ parseMdiTextPosition: () => parseMdiTextPosition,
38
43
  prepareRender: () => prepareRender,
39
44
  renderDocx: () => renderDocx,
40
45
  renderDocxWithDiagnostics: () => renderDocxWithDiagnostics,
@@ -48,13 +53,18 @@ __export(index_exports, {
48
53
  renderTextFormat: () => renderTextFormat,
49
54
  renderTextFormatWithDiagnostics: () => renderTextFormatWithDiagnostics,
50
55
  renderTextWithDiagnostics: () => renderTextWithDiagnostics,
56
+ resolveMdiSourceSpan: () => resolveMdiSourceSpan,
57
+ resolveMdiSourceSpans: () => resolveMdiSourceSpans,
51
58
  serializeMdi: () => serializeMdi,
59
+ sourceSpansForTextRange: () => sourceSpansForTextRange,
52
60
  toPublicationMdast: () => toPublicationMdast
53
61
  });
54
62
  module.exports = __toCommonJS(index_exports);
55
63
  var mdiCore = __toESM(require("@illusions-lab/mdi-core"), 1);
56
64
  var import_yaml = require("yaml");
57
65
  var {
66
+ getMdiTextBlocksJson,
67
+ resolveMdiSourceSpansJson,
58
68
  parseMdiSyntaxJson,
59
69
  renderHtml: renderHtmlFromRust,
60
70
  renderEpub: renderEpubFromRust,
@@ -77,6 +87,7 @@ function requireLayoutSystem(profile) {
77
87
  }
78
88
  var MDI_SPEC_VERSION = "2.0";
79
89
  var MDI_IR_VERSION = "1.0";
90
+ var MDI_TEXT_PROJECTION_VERSION = "1.0";
80
91
  function parse(source) {
81
92
  if (typeof source !== "string") throw new TypeError("source must be a string");
82
93
  const result = JSON.parse(parseMdiSyntaxJson(source));
@@ -85,6 +96,144 @@ function parse(source) {
85
96
  }
86
97
  return result;
87
98
  }
99
+ function getMdiTextBlocks(source) {
100
+ if (typeof source !== "string") throw new TypeError("source must be a string");
101
+ const result = JSON.parse(getMdiTextBlocksJson(source));
102
+ if (result.projectionVersion !== MDI_TEXT_PROJECTION_VERSION) {
103
+ throw new Error(`Unsupported MDI text projection version: ${String(result.projectionVersion)}`);
104
+ }
105
+ return result;
106
+ }
107
+ function resolveMdiSourceSpan(source, span) {
108
+ return resolveMdiSourceSpans(source, [span])[0];
109
+ }
110
+ function resolveMdiSourceSpans(source, spans) {
111
+ if (typeof source !== "string") throw new TypeError("source must be a string");
112
+ if (!Array.isArray(spans)) throw new TypeError("spans must be an array");
113
+ if (spans.length === 0) return [];
114
+ const utf8 = utf8SourceBoundaries(source);
115
+ for (const [index, span] of spans.entries()) {
116
+ assertSourceSpanInput(span, `spans[${index}]`);
117
+ if (span.startByte > span.endByte) {
118
+ throw new RangeError(`spans[${index}].startByte must not exceed endByte`);
119
+ }
120
+ if (span.endByte > utf8.length) {
121
+ throw new RangeError(`spans[${index}] falls outside the UTF-8 source length`);
122
+ }
123
+ if (!utf8.boundaries.has(span.startByte) || !utf8.boundaries.has(span.endByte)) {
124
+ throw new RangeError(`spans[${index}] endpoints must be UTF-8 code-point boundaries`);
125
+ }
126
+ }
127
+ const results = JSON.parse(
128
+ resolveMdiSourceSpansJson(source, JSON.stringify(spans))
129
+ );
130
+ for (const result of results) {
131
+ if (result.projectionVersion !== MDI_TEXT_PROJECTION_VERSION) {
132
+ throw new Error(`Unsupported MDI text projection version: ${String(result.projectionVersion)}`);
133
+ }
134
+ }
135
+ return results;
136
+ }
137
+ function assertSourceSpanInput(span, name = "span") {
138
+ if (!span || typeof span !== "object" || Array.isArray(span)) {
139
+ throw new TypeError(`${name} must be an object`);
140
+ }
141
+ if (typeof span.startByte !== "number" || typeof span.endByte !== "number") {
142
+ throw new TypeError(`${name}.startByte and ${name}.endByte must be numbers`);
143
+ }
144
+ const isUint32 = (value) => Number.isInteger(value) && value >= 0 && value <= 4294967295;
145
+ if (!isUint32(span.startByte) || !isUint32(span.endByte)) {
146
+ throw new RangeError(`${name}.startByte and ${name}.endByte must be uint32 values`);
147
+ }
148
+ }
149
+ function utf8SourceBoundaries(source) {
150
+ let length = 0;
151
+ const boundaries = /* @__PURE__ */ new Set([0]);
152
+ for (const character of source) {
153
+ const codePoint = character.codePointAt(0);
154
+ length += codePoint <= 127 ? 1 : codePoint <= 2047 ? 2 : codePoint <= 65535 ? 3 : 4;
155
+ boundaries.add(length);
156
+ }
157
+ return { length, boundaries };
158
+ }
159
+ function parseMdiTextPosition(position) {
160
+ if (typeof position !== "string") throw new TypeError("position must be a string");
161
+ const match = /^([1-9]\d*):([1-9]\d*)$/.exec(position);
162
+ if (!match) throw new RangeError(`Invalid MDI text position: ${position}`);
163
+ const block = Number(match[1]);
164
+ const character = Number(match[2]);
165
+ if (!Number.isSafeInteger(block) || !Number.isSafeInteger(character)) {
166
+ throw new RangeError(`Invalid MDI text position: ${position}`);
167
+ }
168
+ return { block, character };
169
+ }
170
+ function formatMdiTextPosition(position) {
171
+ if (!position || typeof position !== "object") {
172
+ throw new TypeError("position must be an object");
173
+ }
174
+ if (!isPositiveSafeInteger(position.block) || !isPositiveSafeInteger(position.character)) {
175
+ throw new RangeError("position.block and position.character must be positive safe integers");
176
+ }
177
+ return `${position.block}:${position.character}`;
178
+ }
179
+ function formatMdiTextRange(range) {
180
+ if (!range || typeof range !== "object") throw new TypeError("range must be an object");
181
+ const start = parseMdiTextPosition(range.start);
182
+ const end = parseMdiTextPosition(range.end);
183
+ if (start.block !== end.block || end.character < start.character) {
184
+ throw new RangeError("range must be ordered within one text block");
185
+ }
186
+ return `${range.start}-${range.end}`;
187
+ }
188
+ function sourceSpansForTextRange(block, range) {
189
+ if (!block || typeof block !== "object") throw new TypeError("block must be an object");
190
+ const start = parseMdiTextPosition(range.start);
191
+ const end = parseMdiTextPosition(range.end);
192
+ if (start.block !== block.index || end.block !== block.index) {
193
+ throw new RangeError("range must belong to the supplied text block");
194
+ }
195
+ const blockEnd = parseMdiTextPosition(block.range.end).character;
196
+ if (end.character < start.character || start.character < 1 || end.character > blockEnd) {
197
+ throw new RangeError("range falls outside the supplied text block");
198
+ }
199
+ const spans = [];
200
+ let previousRunEnd = 1;
201
+ for (const run of block.sourceMap.runs) {
202
+ const runStartPosition = parseMdiTextPosition(run.range.start);
203
+ const runEndPosition = parseMdiTextPosition(run.range.end);
204
+ const runStart = runStartPosition.character;
205
+ const runEnd = runEndPosition.character;
206
+ const invalidRange = runStartPosition.block !== block.index || runEndPosition.block !== block.index || runEnd < runStart || runStart < previousRunEnd || runEnd > blockEnd;
207
+ const invalidBoundaries = run.sourceBoundaries.length !== runEnd - runStart + 1 || run.sourceBoundaries.some((boundary) => !Number.isSafeInteger(boundary) || boundary < 0) || run.sourceBoundaries.some((boundary, index) => index > 0 && boundary < run.sourceBoundaries[index - 1]) || block.span !== void 0 && run.sourceBoundaries.some(
208
+ (boundary) => boundary < block.span.startByte || boundary > block.span.endByte
209
+ );
210
+ if (invalidRange || invalidBoundaries) {
211
+ throw new Error("Invalid MDI text source run");
212
+ }
213
+ previousRunEnd = runEnd;
214
+ const overlapStart = Math.max(start.character, runStart);
215
+ const overlapEnd = Math.min(end.character, runEnd);
216
+ for (let character = overlapStart; character < overlapEnd; character += 1) {
217
+ const offset = character - runStart;
218
+ appendMergedSourceSpan(spans, {
219
+ startByte: run.sourceBoundaries[offset],
220
+ endByte: run.sourceBoundaries[offset + 1]
221
+ });
222
+ }
223
+ }
224
+ return spans;
225
+ }
226
+ function appendMergedSourceSpan(spans, next) {
227
+ const previous = spans.at(-1);
228
+ if (previous?.endByte === next.startByte) {
229
+ previous.endByte = next.endByte;
230
+ } else {
231
+ spans.push(next);
232
+ }
233
+ }
234
+ function isPositiveSafeInteger(value) {
235
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
236
+ }
88
237
  function renderHtml(source, options) {
89
238
  assertSource(source);
90
239
  assertHtmlOptions(options);
@@ -418,9 +567,14 @@ var parseMdiSyntax = parse;
418
567
  0 && (module.exports = {
419
568
  MDI_IR_VERSION,
420
569
  MDI_SPEC_VERSION,
570
+ MDI_TEXT_PROJECTION_VERSION,
571
+ formatMdiTextPosition,
572
+ formatMdiTextRange,
573
+ getMdiTextBlocks,
421
574
  initializeMdi,
422
575
  parse,
423
576
  parseMdiSyntax,
577
+ parseMdiTextPosition,
424
578
  prepareRender,
425
579
  renderDocx,
426
580
  renderDocxWithDiagnostics,
@@ -434,6 +588,9 @@ var parseMdiSyntax = parse;
434
588
  renderTextFormat,
435
589
  renderTextFormatWithDiagnostics,
436
590
  renderTextWithDiagnostics,
591
+ resolveMdiSourceSpan,
592
+ resolveMdiSourceSpans,
437
593
  serializeMdi,
594
+ sourceSpansForTextRange,
438
595
  toPublicationMdast
439
596
  });
package/dist/index.d.cts CHANGED
@@ -76,6 +76,8 @@ type MdiDocxExportProfile = ExportProfile & {
76
76
  declare const MDI_SPEC_VERSION: "2.0";
77
77
  /** Version of the complete Rust-owned document IR. */
78
78
  declare const MDI_IR_VERSION: "1.0";
79
+ /** Version of the Rust-owned searchable text projection. */
80
+ declare const MDI_TEXT_PROJECTION_VERSION: "1.0";
79
81
  interface MdiParserCapabilities {
80
82
  mdi: boolean;
81
83
  commonMark: boolean;
@@ -89,6 +91,80 @@ interface MdiSourceSpan {
89
91
  /** Exclusive UTF-8 byte offset. */
90
92
  endByte: number;
91
93
  }
94
+ /** A canonical one-based `block:grapheme` text position. */
95
+ type MdiTextPosition = `${number}:${number}`;
96
+ interface MdiTextPositionValue {
97
+ block: number;
98
+ character: number;
99
+ }
100
+ interface MdiTextRange {
101
+ /** Inclusive. */
102
+ start: MdiTextPosition;
103
+ /** Exclusive. */
104
+ end: MdiTextPosition;
105
+ }
106
+ interface MdiTextSourceRun {
107
+ range: MdiTextRange;
108
+ /** One UTF-8 source boundary per grapheme, plus the final boundary. */
109
+ sourceBoundaries: number[];
110
+ }
111
+ interface MdiTextSourceMap {
112
+ runs: MdiTextSourceRun[];
113
+ synthetic: MdiTextRange[];
114
+ unmapped: MdiTextRange[];
115
+ }
116
+ type MdiAnnotationSourceMap = MdiTextSourceMap;
117
+ interface MdiTextAnnotation {
118
+ kind: "rubyReading";
119
+ text: string;
120
+ anchor: MdiTextRange;
121
+ span?: MdiSourceSpan;
122
+ sourceMap: MdiAnnotationSourceMap;
123
+ }
124
+ interface MdiTextBlock {
125
+ /** One-based source-order block number. */
126
+ index: number;
127
+ kind: "heading" | "paragraph" | "listItem" | "blockquote" | "code" | "table" | "footnote" | "html" | "other";
128
+ text: string;
129
+ range: MdiTextRange;
130
+ span?: MdiSourceSpan;
131
+ sourceMap: MdiTextSourceMap;
132
+ annotations: MdiTextAnnotation[];
133
+ node: MdiNode;
134
+ }
135
+ interface MdiTextBlocksResult {
136
+ projectionVersion: "1.0";
137
+ positionEncoding: "unicode-grapheme-cluster-1-based";
138
+ irVersion: typeof MDI_IR_VERSION;
139
+ syntaxVersion: typeof MDI_SPEC_VERSION;
140
+ capabilities: MdiParserCapabilities;
141
+ blocks: MdiTextBlock[];
142
+ document: MdiDocument;
143
+ diagnostics: MdiDiagnostic[];
144
+ }
145
+ type MdiSourceSpanCoverage = "complete" | "partial" | "none";
146
+ type MdiSourceSpanRelation = "exact" | "overlap";
147
+ interface MdiSourceSpanBlockTextMatch {
148
+ kind: "blockText";
149
+ blockIndex: number;
150
+ range: MdiTextRange;
151
+ relation: MdiSourceSpanRelation;
152
+ }
153
+ interface MdiSourceSpanAnnotationMatch {
154
+ kind: "annotation";
155
+ blockIndex: number;
156
+ /** Zero-based index in the containing block's annotations array. */
157
+ annotationIndex: number;
158
+ range: MdiTextRange;
159
+ relation: MdiSourceSpanRelation;
160
+ }
161
+ type MdiSourceSpanTextMatch = MdiSourceSpanBlockTextMatch | MdiSourceSpanAnnotationMatch;
162
+ interface MdiSourceSpanTextResolution {
163
+ projectionVersion: "1.0";
164
+ sourceSpan: MdiSourceSpan;
165
+ coverage: MdiSourceSpanCoverage;
166
+ matches: MdiSourceSpanTextMatch[];
167
+ }
92
168
  interface MdiDiagnostic {
93
169
  severity: "warning" | "error";
94
170
  code: string;
@@ -178,6 +254,32 @@ type MdiSyntaxDocument = MdiDocument;
178
254
  * language-neutral document IR. JavaScript performs no grammar work.
179
255
  */
180
256
  declare function parse(source: string): MdiSyntaxParseResult;
257
+ /**
258
+ * Parse once in Rust and return source-order plaintext blocks, annotations,
259
+ * and grapheme-precise UTF-8 source maps alongside the complete document IR.
260
+ */
261
+ declare function getMdiTextBlocks(source: string): MdiTextBlocksResult;
262
+ /**
263
+ * Resolve a half-open UTF-8 source span to all mapped canonical block and
264
+ * annotation ranges. Mapping semantics are implemented exclusively in Rust.
265
+ */
266
+ declare function resolveMdiSourceSpan(source: string, span: MdiSourceSpan): MdiSourceSpanTextResolution;
267
+ /**
268
+ * Resolve many half-open UTF-8 source spans with one Rust parse/projection.
269
+ * The returned resolutions preserve input order.
270
+ */
271
+ declare function resolveMdiSourceSpans(source: string, spans: readonly MdiSourceSpan[]): MdiSourceSpanTextResolution[];
272
+ /** Parse and validate a canonical one-based `block:character` position. */
273
+ declare function parseMdiTextPosition(position: string): MdiTextPositionValue;
274
+ /** Format a validated one-based text position. */
275
+ declare function formatMdiTextPosition(position: MdiTextPositionValue): MdiTextPosition;
276
+ /** Format a canonical full `start-end` range such as `3:18-3:24`. */
277
+ declare function formatMdiTextRange(range: MdiTextRange): string;
278
+ /**
279
+ * Resolve a text range to its source-derived UTF-8 spans. Synthetic table or
280
+ * paragraph separators are deliberately omitted.
281
+ */
282
+ declare function sourceSpansForTextRange(block: MdiTextBlock, range: MdiTextRange): MdiSourceSpan[];
181
283
  /** Render complete `.mdi` source to standalone semantic HTML in Rust. */
182
284
  declare function renderHtml(source: string, options?: MdiHtmlRenderOptions): string;
183
285
  /**
@@ -248,4 +350,4 @@ declare function renderTextFormatWithDiagnostics(source: string, format: MdiText
248
350
  /** @deprecated Use {@link parse}; it now parses the complete document. */
249
351
  declare const parseMdiSyntax: typeof parse;
250
352
 
251
- export { MDI_IR_VERSION, MDI_SPEC_VERSION, type MdiDiagnostic, type MdiDocument, type MdiDocxExportProfile, type MdiEpubExportOptions, type MdiFrontmatter, type MdiHeading, type MdiHtmlRenderOptions, type MdiNode, type MdiParserCapabilities, type MdiPublicationFrontmatter, type MdiPublicationRoot, type MdiRenderResult, type MdiRubyReading, type MdiSourceSpan, type MdiSyntaxDocument, type MdiSyntaxParseResult, type MdiTextFormat, initializeMdi, parse, parseMdiSyntax, prepareRender, renderDocx, renderDocxWithDiagnostics, renderDocxWithProfile, renderEpub, renderEpubWithDiagnostics, renderEpubWithProfile, renderHtml, renderHtmlWithDiagnostics, renderText, renderTextFormat, renderTextFormatWithDiagnostics, renderTextWithDiagnostics, serializeMdi, toPublicationMdast };
353
+ export { MDI_IR_VERSION, MDI_SPEC_VERSION, MDI_TEXT_PROJECTION_VERSION, type MdiAnnotationSourceMap, type MdiDiagnostic, type MdiDocument, type MdiDocxExportProfile, type MdiEpubExportOptions, type MdiFrontmatter, type MdiHeading, type MdiHtmlRenderOptions, type MdiNode, type MdiParserCapabilities, type MdiPublicationFrontmatter, type MdiPublicationRoot, type MdiRenderResult, type MdiRubyReading, type MdiSourceSpan, type MdiSourceSpanAnnotationMatch, type MdiSourceSpanBlockTextMatch, type MdiSourceSpanCoverage, type MdiSourceSpanRelation, type MdiSourceSpanTextMatch, type MdiSourceSpanTextResolution, type MdiSyntaxDocument, type MdiSyntaxParseResult, type MdiTextAnnotation, type MdiTextBlock, type MdiTextBlocksResult, type MdiTextFormat, type MdiTextPosition, type MdiTextPositionValue, type MdiTextRange, type MdiTextSourceMap, type MdiTextSourceRun, formatMdiTextPosition, formatMdiTextRange, getMdiTextBlocks, initializeMdi, parse, parseMdiSyntax, parseMdiTextPosition, prepareRender, renderDocx, renderDocxWithDiagnostics, renderDocxWithProfile, renderEpub, renderEpubWithDiagnostics, renderEpubWithProfile, renderHtml, renderHtmlWithDiagnostics, renderText, renderTextFormat, renderTextFormatWithDiagnostics, renderTextWithDiagnostics, resolveMdiSourceSpan, resolveMdiSourceSpans, serializeMdi, sourceSpansForTextRange, toPublicationMdast };
package/dist/index.d.ts CHANGED
@@ -76,6 +76,8 @@ type MdiDocxExportProfile = ExportProfile & {
76
76
  declare const MDI_SPEC_VERSION: "2.0";
77
77
  /** Version of the complete Rust-owned document IR. */
78
78
  declare const MDI_IR_VERSION: "1.0";
79
+ /** Version of the Rust-owned searchable text projection. */
80
+ declare const MDI_TEXT_PROJECTION_VERSION: "1.0";
79
81
  interface MdiParserCapabilities {
80
82
  mdi: boolean;
81
83
  commonMark: boolean;
@@ -89,6 +91,80 @@ interface MdiSourceSpan {
89
91
  /** Exclusive UTF-8 byte offset. */
90
92
  endByte: number;
91
93
  }
94
+ /** A canonical one-based `block:grapheme` text position. */
95
+ type MdiTextPosition = `${number}:${number}`;
96
+ interface MdiTextPositionValue {
97
+ block: number;
98
+ character: number;
99
+ }
100
+ interface MdiTextRange {
101
+ /** Inclusive. */
102
+ start: MdiTextPosition;
103
+ /** Exclusive. */
104
+ end: MdiTextPosition;
105
+ }
106
+ interface MdiTextSourceRun {
107
+ range: MdiTextRange;
108
+ /** One UTF-8 source boundary per grapheme, plus the final boundary. */
109
+ sourceBoundaries: number[];
110
+ }
111
+ interface MdiTextSourceMap {
112
+ runs: MdiTextSourceRun[];
113
+ synthetic: MdiTextRange[];
114
+ unmapped: MdiTextRange[];
115
+ }
116
+ type MdiAnnotationSourceMap = MdiTextSourceMap;
117
+ interface MdiTextAnnotation {
118
+ kind: "rubyReading";
119
+ text: string;
120
+ anchor: MdiTextRange;
121
+ span?: MdiSourceSpan;
122
+ sourceMap: MdiAnnotationSourceMap;
123
+ }
124
+ interface MdiTextBlock {
125
+ /** One-based source-order block number. */
126
+ index: number;
127
+ kind: "heading" | "paragraph" | "listItem" | "blockquote" | "code" | "table" | "footnote" | "html" | "other";
128
+ text: string;
129
+ range: MdiTextRange;
130
+ span?: MdiSourceSpan;
131
+ sourceMap: MdiTextSourceMap;
132
+ annotations: MdiTextAnnotation[];
133
+ node: MdiNode;
134
+ }
135
+ interface MdiTextBlocksResult {
136
+ projectionVersion: "1.0";
137
+ positionEncoding: "unicode-grapheme-cluster-1-based";
138
+ irVersion: typeof MDI_IR_VERSION;
139
+ syntaxVersion: typeof MDI_SPEC_VERSION;
140
+ capabilities: MdiParserCapabilities;
141
+ blocks: MdiTextBlock[];
142
+ document: MdiDocument;
143
+ diagnostics: MdiDiagnostic[];
144
+ }
145
+ type MdiSourceSpanCoverage = "complete" | "partial" | "none";
146
+ type MdiSourceSpanRelation = "exact" | "overlap";
147
+ interface MdiSourceSpanBlockTextMatch {
148
+ kind: "blockText";
149
+ blockIndex: number;
150
+ range: MdiTextRange;
151
+ relation: MdiSourceSpanRelation;
152
+ }
153
+ interface MdiSourceSpanAnnotationMatch {
154
+ kind: "annotation";
155
+ blockIndex: number;
156
+ /** Zero-based index in the containing block's annotations array. */
157
+ annotationIndex: number;
158
+ range: MdiTextRange;
159
+ relation: MdiSourceSpanRelation;
160
+ }
161
+ type MdiSourceSpanTextMatch = MdiSourceSpanBlockTextMatch | MdiSourceSpanAnnotationMatch;
162
+ interface MdiSourceSpanTextResolution {
163
+ projectionVersion: "1.0";
164
+ sourceSpan: MdiSourceSpan;
165
+ coverage: MdiSourceSpanCoverage;
166
+ matches: MdiSourceSpanTextMatch[];
167
+ }
92
168
  interface MdiDiagnostic {
93
169
  severity: "warning" | "error";
94
170
  code: string;
@@ -178,6 +254,32 @@ type MdiSyntaxDocument = MdiDocument;
178
254
  * language-neutral document IR. JavaScript performs no grammar work.
179
255
  */
180
256
  declare function parse(source: string): MdiSyntaxParseResult;
257
+ /**
258
+ * Parse once in Rust and return source-order plaintext blocks, annotations,
259
+ * and grapheme-precise UTF-8 source maps alongside the complete document IR.
260
+ */
261
+ declare function getMdiTextBlocks(source: string): MdiTextBlocksResult;
262
+ /**
263
+ * Resolve a half-open UTF-8 source span to all mapped canonical block and
264
+ * annotation ranges. Mapping semantics are implemented exclusively in Rust.
265
+ */
266
+ declare function resolveMdiSourceSpan(source: string, span: MdiSourceSpan): MdiSourceSpanTextResolution;
267
+ /**
268
+ * Resolve many half-open UTF-8 source spans with one Rust parse/projection.
269
+ * The returned resolutions preserve input order.
270
+ */
271
+ declare function resolveMdiSourceSpans(source: string, spans: readonly MdiSourceSpan[]): MdiSourceSpanTextResolution[];
272
+ /** Parse and validate a canonical one-based `block:character` position. */
273
+ declare function parseMdiTextPosition(position: string): MdiTextPositionValue;
274
+ /** Format a validated one-based text position. */
275
+ declare function formatMdiTextPosition(position: MdiTextPositionValue): MdiTextPosition;
276
+ /** Format a canonical full `start-end` range such as `3:18-3:24`. */
277
+ declare function formatMdiTextRange(range: MdiTextRange): string;
278
+ /**
279
+ * Resolve a text range to its source-derived UTF-8 spans. Synthetic table or
280
+ * paragraph separators are deliberately omitted.
281
+ */
282
+ declare function sourceSpansForTextRange(block: MdiTextBlock, range: MdiTextRange): MdiSourceSpan[];
181
283
  /** Render complete `.mdi` source to standalone semantic HTML in Rust. */
182
284
  declare function renderHtml(source: string, options?: MdiHtmlRenderOptions): string;
183
285
  /**
@@ -248,4 +350,4 @@ declare function renderTextFormatWithDiagnostics(source: string, format: MdiText
248
350
  /** @deprecated Use {@link parse}; it now parses the complete document. */
249
351
  declare const parseMdiSyntax: typeof parse;
250
352
 
251
- export { MDI_IR_VERSION, MDI_SPEC_VERSION, type MdiDiagnostic, type MdiDocument, type MdiDocxExportProfile, type MdiEpubExportOptions, type MdiFrontmatter, type MdiHeading, type MdiHtmlRenderOptions, type MdiNode, type MdiParserCapabilities, type MdiPublicationFrontmatter, type MdiPublicationRoot, type MdiRenderResult, type MdiRubyReading, type MdiSourceSpan, type MdiSyntaxDocument, type MdiSyntaxParseResult, type MdiTextFormat, initializeMdi, parse, parseMdiSyntax, prepareRender, renderDocx, renderDocxWithDiagnostics, renderDocxWithProfile, renderEpub, renderEpubWithDiagnostics, renderEpubWithProfile, renderHtml, renderHtmlWithDiagnostics, renderText, renderTextFormat, renderTextFormatWithDiagnostics, renderTextWithDiagnostics, serializeMdi, toPublicationMdast };
353
+ export { MDI_IR_VERSION, MDI_SPEC_VERSION, MDI_TEXT_PROJECTION_VERSION, type MdiAnnotationSourceMap, type MdiDiagnostic, type MdiDocument, type MdiDocxExportProfile, type MdiEpubExportOptions, type MdiFrontmatter, type MdiHeading, type MdiHtmlRenderOptions, type MdiNode, type MdiParserCapabilities, type MdiPublicationFrontmatter, type MdiPublicationRoot, type MdiRenderResult, type MdiRubyReading, type MdiSourceSpan, type MdiSourceSpanAnnotationMatch, type MdiSourceSpanBlockTextMatch, type MdiSourceSpanCoverage, type MdiSourceSpanRelation, type MdiSourceSpanTextMatch, type MdiSourceSpanTextResolution, type MdiSyntaxDocument, type MdiSyntaxParseResult, type MdiTextAnnotation, type MdiTextBlock, type MdiTextBlocksResult, type MdiTextFormat, type MdiTextPosition, type MdiTextPositionValue, type MdiTextRange, type MdiTextSourceMap, type MdiTextSourceRun, formatMdiTextPosition, formatMdiTextRange, getMdiTextBlocks, initializeMdi, parse, parseMdiSyntax, parseMdiTextPosition, prepareRender, renderDocx, renderDocxWithDiagnostics, renderDocxWithProfile, renderEpub, renderEpubWithDiagnostics, renderEpubWithProfile, renderHtml, renderHtmlWithDiagnostics, renderText, renderTextFormat, renderTextFormatWithDiagnostics, renderTextWithDiagnostics, resolveMdiSourceSpan, resolveMdiSourceSpans, serializeMdi, sourceSpansForTextRange, toPublicationMdast };
package/dist/index.js CHANGED
@@ -1,9 +1,14 @@
1
1
  import {
2
2
  MDI_IR_VERSION,
3
3
  MDI_SPEC_VERSION,
4
+ MDI_TEXT_PROJECTION_VERSION,
5
+ formatMdiTextPosition,
6
+ formatMdiTextRange,
7
+ getMdiTextBlocks,
4
8
  initializeMdi,
5
9
  parse,
6
10
  parseMdiSyntax,
11
+ parseMdiTextPosition,
7
12
  prepareRender,
8
13
  renderDocx,
9
14
  renderDocxWithDiagnostics,
@@ -17,15 +22,23 @@ import {
17
22
  renderTextFormat,
18
23
  renderTextFormatWithDiagnostics,
19
24
  renderTextWithDiagnostics,
25
+ resolveMdiSourceSpan,
26
+ resolveMdiSourceSpans,
20
27
  serializeMdi,
28
+ sourceSpansForTextRange,
21
29
  toPublicationMdast
22
- } from "./chunk-WEJ62HS6.js";
30
+ } from "./chunk-GMS5SFZO.js";
23
31
  export {
24
32
  MDI_IR_VERSION,
25
33
  MDI_SPEC_VERSION,
34
+ MDI_TEXT_PROJECTION_VERSION,
35
+ formatMdiTextPosition,
36
+ formatMdiTextRange,
37
+ getMdiTextBlocks,
26
38
  initializeMdi,
27
39
  parse,
28
40
  parseMdiSyntax,
41
+ parseMdiTextPosition,
29
42
  prepareRender,
30
43
  renderDocx,
31
44
  renderDocxWithDiagnostics,
@@ -39,6 +52,9 @@ export {
39
52
  renderTextFormat,
40
53
  renderTextFormatWithDiagnostics,
41
54
  renderTextWithDiagnostics,
55
+ resolveMdiSourceSpan,
56
+ resolveMdiSourceSpans,
42
57
  serializeMdi,
58
+ sourceSpansForTextRange,
43
59
  toPublicationMdast
44
60
  };
package/dist/node.cjs CHANGED
@@ -41,6 +41,8 @@ module.exports = __toCommonJS(node_exports);
41
41
  var mdiCore = __toESM(require("@illusions-lab/mdi-core"), 1);
42
42
  var import_yaml = require("yaml");
43
43
  var {
44
+ getMdiTextBlocksJson,
45
+ resolveMdiSourceSpansJson,
44
46
  parseMdiSyntaxJson,
45
47
  renderHtml: renderHtmlFromRust,
46
48
  renderEpub: renderEpubFromRust,
package/dist/node.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  parse,
3
3
  renderHtml
4
- } from "./chunk-WEJ62HS6.js";
4
+ } from "./chunk-GMS5SFZO.js";
5
5
 
6
6
  // src/node.ts
7
7
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@illusions-lab/mdi",
3
- "version": "2.0.18",
3
+ "version": "2.0.20",
4
4
  "description": "Thin JavaScript binding for the Rust-authoritative MDI parser",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,9 +32,9 @@
32
32
  "typecheck": "tsc --noEmit"
33
33
  },
34
34
  "dependencies": {
35
- "@illusions-lab/mdi-core": "^2.0.18",
36
- "@illusions-lab/mdi-export-profile": "^2.0.23",
37
- "@illusions-lab/mdi-to-epub": "^2.0.33",
35
+ "@illusions-lab/mdi-core": "^2.0.20",
36
+ "@illusions-lab/mdi-export-profile": "^2.0.25",
37
+ "@illusions-lab/mdi-to-epub": "^2.0.35",
38
38
  "@types/mdast": "^4.0.0",
39
39
  "yaml": "^2.0.0"
40
40
  },