@illusions-lab/mdi 2.0.17 → 2.0.19

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,29 @@ 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, 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
+ ```
98
+
99
+ Each source-derived grapheme is represented by a `sourceMap.runs` boundary;
100
+ table tabs/newlines and multi-paragraph joiners appear in `synthetic` and do
101
+ not receive invented source spans. `parseMdiTextPosition`,
102
+ `formatMdiTextPosition`, and `formatMdiTextRange` provide stateless coordinate
103
+ helpers.
104
+
82
105
  ## Rendering
83
106
 
84
107
  Rendering starts from the same Rust IR. Canonical MDI, plain text, HTML, EPUB,
@@ -2,6 +2,7 @@
2
2
  import * as mdiCore from "@illusions-lab/mdi-core";
3
3
  import { parse as parseYaml } from "yaml";
4
4
  var {
5
+ getMdiTextBlocksJson,
5
6
  parseMdiSyntaxJson,
6
7
  renderHtml: renderHtmlFromRust,
7
8
  renderEpub: renderEpubFromRust,
@@ -24,6 +25,7 @@ function requireLayoutSystem(profile) {
24
25
  }
25
26
  var MDI_SPEC_VERSION = "2.0";
26
27
  var MDI_IR_VERSION = "1.0";
28
+ var MDI_TEXT_PROJECTION_VERSION = "1.0";
27
29
  function parse(source) {
28
30
  if (typeof source !== "string") throw new TypeError("source must be a string");
29
31
  const result = JSON.parse(parseMdiSyntaxJson(source));
@@ -32,6 +34,92 @@ function parse(source) {
32
34
  }
33
35
  return result;
34
36
  }
37
+ function getMdiTextBlocks(source) {
38
+ if (typeof source !== "string") throw new TypeError("source must be a string");
39
+ const result = JSON.parse(getMdiTextBlocksJson(source));
40
+ if (result.projectionVersion !== MDI_TEXT_PROJECTION_VERSION) {
41
+ throw new Error(`Unsupported MDI text projection version: ${String(result.projectionVersion)}`);
42
+ }
43
+ return result;
44
+ }
45
+ function parseMdiTextPosition(position) {
46
+ if (typeof position !== "string") throw new TypeError("position must be a string");
47
+ const match = /^([1-9]\d*):([1-9]\d*)$/.exec(position);
48
+ if (!match) throw new RangeError(`Invalid MDI text position: ${position}`);
49
+ const block = Number(match[1]);
50
+ const character = Number(match[2]);
51
+ if (!Number.isSafeInteger(block) || !Number.isSafeInteger(character)) {
52
+ throw new RangeError(`Invalid MDI text position: ${position}`);
53
+ }
54
+ return { block, character };
55
+ }
56
+ function formatMdiTextPosition(position) {
57
+ if (!position || typeof position !== "object") {
58
+ throw new TypeError("position must be an object");
59
+ }
60
+ if (!isPositiveSafeInteger(position.block) || !isPositiveSafeInteger(position.character)) {
61
+ throw new RangeError("position.block and position.character must be positive safe integers");
62
+ }
63
+ return `${position.block}:${position.character}`;
64
+ }
65
+ function formatMdiTextRange(range) {
66
+ if (!range || typeof range !== "object") throw new TypeError("range must be an object");
67
+ const start = parseMdiTextPosition(range.start);
68
+ const end = parseMdiTextPosition(range.end);
69
+ if (start.block !== end.block || end.character < start.character) {
70
+ throw new RangeError("range must be ordered within one text block");
71
+ }
72
+ return `${range.start}-${range.end}`;
73
+ }
74
+ function sourceSpansForTextRange(block, range) {
75
+ if (!block || typeof block !== "object") throw new TypeError("block must be an object");
76
+ const start = parseMdiTextPosition(range.start);
77
+ const end = parseMdiTextPosition(range.end);
78
+ if (start.block !== block.index || end.block !== block.index) {
79
+ throw new RangeError("range must belong to the supplied text block");
80
+ }
81
+ const blockEnd = parseMdiTextPosition(block.range.end).character;
82
+ if (end.character < start.character || start.character < 1 || end.character > blockEnd) {
83
+ throw new RangeError("range falls outside the supplied text block");
84
+ }
85
+ const spans = [];
86
+ let previousRunEnd = 1;
87
+ for (const run of block.sourceMap.runs) {
88
+ const runStartPosition = parseMdiTextPosition(run.range.start);
89
+ const runEndPosition = parseMdiTextPosition(run.range.end);
90
+ const runStart = runStartPosition.character;
91
+ const runEnd = runEndPosition.character;
92
+ const invalidRange = runStartPosition.block !== block.index || runEndPosition.block !== block.index || runEnd < runStart || runStart < previousRunEnd || runEnd > blockEnd;
93
+ 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(
94
+ (boundary) => boundary < block.span.startByte || boundary > block.span.endByte
95
+ );
96
+ if (invalidRange || invalidBoundaries) {
97
+ throw new Error("Invalid MDI text source run");
98
+ }
99
+ previousRunEnd = runEnd;
100
+ const overlapStart = Math.max(start.character, runStart);
101
+ const overlapEnd = Math.min(end.character, runEnd);
102
+ for (let character = overlapStart; character < overlapEnd; character += 1) {
103
+ const offset = character - runStart;
104
+ appendMergedSourceSpan(spans, {
105
+ startByte: run.sourceBoundaries[offset],
106
+ endByte: run.sourceBoundaries[offset + 1]
107
+ });
108
+ }
109
+ }
110
+ return spans;
111
+ }
112
+ function appendMergedSourceSpan(spans, next) {
113
+ const previous = spans.at(-1);
114
+ if (previous?.endByte === next.startByte) {
115
+ previous.endByte = next.endByte;
116
+ } else {
117
+ spans.push(next);
118
+ }
119
+ }
120
+ function isPositiveSafeInteger(value) {
121
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
122
+ }
35
123
  function renderHtml(source, options) {
36
124
  assertSource(source);
37
125
  assertHtmlOptions(options);
@@ -366,7 +454,13 @@ export {
366
454
  initializeMdi,
367
455
  MDI_SPEC_VERSION,
368
456
  MDI_IR_VERSION,
457
+ MDI_TEXT_PROJECTION_VERSION,
369
458
  parse,
459
+ getMdiTextBlocks,
460
+ parseMdiTextPosition,
461
+ formatMdiTextPosition,
462
+ formatMdiTextRange,
463
+ sourceSpansForTextRange,
370
464
  renderHtml,
371
465
  renderHtmlWithDiagnostics,
372
466
  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,
@@ -49,12 +54,14 @@ __export(index_exports, {
49
54
  renderTextFormatWithDiagnostics: () => renderTextFormatWithDiagnostics,
50
55
  renderTextWithDiagnostics: () => renderTextWithDiagnostics,
51
56
  serializeMdi: () => serializeMdi,
57
+ sourceSpansForTextRange: () => sourceSpansForTextRange,
52
58
  toPublicationMdast: () => toPublicationMdast
53
59
  });
54
60
  module.exports = __toCommonJS(index_exports);
55
61
  var mdiCore = __toESM(require("@illusions-lab/mdi-core"), 1);
56
62
  var import_yaml = require("yaml");
57
63
  var {
64
+ getMdiTextBlocksJson,
58
65
  parseMdiSyntaxJson,
59
66
  renderHtml: renderHtmlFromRust,
60
67
  renderEpub: renderEpubFromRust,
@@ -77,6 +84,7 @@ function requireLayoutSystem(profile) {
77
84
  }
78
85
  var MDI_SPEC_VERSION = "2.0";
79
86
  var MDI_IR_VERSION = "1.0";
87
+ var MDI_TEXT_PROJECTION_VERSION = "1.0";
80
88
  function parse(source) {
81
89
  if (typeof source !== "string") throw new TypeError("source must be a string");
82
90
  const result = JSON.parse(parseMdiSyntaxJson(source));
@@ -85,6 +93,92 @@ function parse(source) {
85
93
  }
86
94
  return result;
87
95
  }
96
+ function getMdiTextBlocks(source) {
97
+ if (typeof source !== "string") throw new TypeError("source must be a string");
98
+ const result = JSON.parse(getMdiTextBlocksJson(source));
99
+ if (result.projectionVersion !== MDI_TEXT_PROJECTION_VERSION) {
100
+ throw new Error(`Unsupported MDI text projection version: ${String(result.projectionVersion)}`);
101
+ }
102
+ return result;
103
+ }
104
+ function parseMdiTextPosition(position) {
105
+ if (typeof position !== "string") throw new TypeError("position must be a string");
106
+ const match = /^([1-9]\d*):([1-9]\d*)$/.exec(position);
107
+ if (!match) throw new RangeError(`Invalid MDI text position: ${position}`);
108
+ const block = Number(match[1]);
109
+ const character = Number(match[2]);
110
+ if (!Number.isSafeInteger(block) || !Number.isSafeInteger(character)) {
111
+ throw new RangeError(`Invalid MDI text position: ${position}`);
112
+ }
113
+ return { block, character };
114
+ }
115
+ function formatMdiTextPosition(position) {
116
+ if (!position || typeof position !== "object") {
117
+ throw new TypeError("position must be an object");
118
+ }
119
+ if (!isPositiveSafeInteger(position.block) || !isPositiveSafeInteger(position.character)) {
120
+ throw new RangeError("position.block and position.character must be positive safe integers");
121
+ }
122
+ return `${position.block}:${position.character}`;
123
+ }
124
+ function formatMdiTextRange(range) {
125
+ if (!range || typeof range !== "object") throw new TypeError("range must be an object");
126
+ const start = parseMdiTextPosition(range.start);
127
+ const end = parseMdiTextPosition(range.end);
128
+ if (start.block !== end.block || end.character < start.character) {
129
+ throw new RangeError("range must be ordered within one text block");
130
+ }
131
+ return `${range.start}-${range.end}`;
132
+ }
133
+ function sourceSpansForTextRange(block, range) {
134
+ if (!block || typeof block !== "object") throw new TypeError("block must be an object");
135
+ const start = parseMdiTextPosition(range.start);
136
+ const end = parseMdiTextPosition(range.end);
137
+ if (start.block !== block.index || end.block !== block.index) {
138
+ throw new RangeError("range must belong to the supplied text block");
139
+ }
140
+ const blockEnd = parseMdiTextPosition(block.range.end).character;
141
+ if (end.character < start.character || start.character < 1 || end.character > blockEnd) {
142
+ throw new RangeError("range falls outside the supplied text block");
143
+ }
144
+ const spans = [];
145
+ let previousRunEnd = 1;
146
+ for (const run of block.sourceMap.runs) {
147
+ const runStartPosition = parseMdiTextPosition(run.range.start);
148
+ const runEndPosition = parseMdiTextPosition(run.range.end);
149
+ const runStart = runStartPosition.character;
150
+ const runEnd = runEndPosition.character;
151
+ const invalidRange = runStartPosition.block !== block.index || runEndPosition.block !== block.index || runEnd < runStart || runStart < previousRunEnd || runEnd > blockEnd;
152
+ 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(
153
+ (boundary) => boundary < block.span.startByte || boundary > block.span.endByte
154
+ );
155
+ if (invalidRange || invalidBoundaries) {
156
+ throw new Error("Invalid MDI text source run");
157
+ }
158
+ previousRunEnd = runEnd;
159
+ const overlapStart = Math.max(start.character, runStart);
160
+ const overlapEnd = Math.min(end.character, runEnd);
161
+ for (let character = overlapStart; character < overlapEnd; character += 1) {
162
+ const offset = character - runStart;
163
+ appendMergedSourceSpan(spans, {
164
+ startByte: run.sourceBoundaries[offset],
165
+ endByte: run.sourceBoundaries[offset + 1]
166
+ });
167
+ }
168
+ }
169
+ return spans;
170
+ }
171
+ function appendMergedSourceSpan(spans, next) {
172
+ const previous = spans.at(-1);
173
+ if (previous?.endByte === next.startByte) {
174
+ previous.endByte = next.endByte;
175
+ } else {
176
+ spans.push(next);
177
+ }
178
+ }
179
+ function isPositiveSafeInteger(value) {
180
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
181
+ }
88
182
  function renderHtml(source, options) {
89
183
  assertSource(source);
90
184
  assertHtmlOptions(options);
@@ -418,9 +512,14 @@ var parseMdiSyntax = parse;
418
512
  0 && (module.exports = {
419
513
  MDI_IR_VERSION,
420
514
  MDI_SPEC_VERSION,
515
+ MDI_TEXT_PROJECTION_VERSION,
516
+ formatMdiTextPosition,
517
+ formatMdiTextRange,
518
+ getMdiTextBlocks,
421
519
  initializeMdi,
422
520
  parse,
423
521
  parseMdiSyntax,
522
+ parseMdiTextPosition,
424
523
  prepareRender,
425
524
  renderDocx,
426
525
  renderDocxWithDiagnostics,
@@ -435,5 +534,6 @@ var parseMdiSyntax = parse;
435
534
  renderTextFormatWithDiagnostics,
436
535
  renderTextWithDiagnostics,
437
536
  serializeMdi,
537
+ sourceSpansForTextRange,
438
538
  toPublicationMdast
439
539
  });
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,57 @@ 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
+ }
92
145
  interface MdiDiagnostic {
93
146
  severity: "warning" | "error";
94
147
  code: string;
@@ -178,6 +231,22 @@ type MdiSyntaxDocument = MdiDocument;
178
231
  * language-neutral document IR. JavaScript performs no grammar work.
179
232
  */
180
233
  declare function parse(source: string): MdiSyntaxParseResult;
234
+ /**
235
+ * Parse once in Rust and return source-order plaintext blocks, annotations,
236
+ * and grapheme-precise UTF-8 source maps alongside the complete document IR.
237
+ */
238
+ declare function getMdiTextBlocks(source: string): MdiTextBlocksResult;
239
+ /** Parse and validate a canonical one-based `block:character` position. */
240
+ declare function parseMdiTextPosition(position: string): MdiTextPositionValue;
241
+ /** Format a validated one-based text position. */
242
+ declare function formatMdiTextPosition(position: MdiTextPositionValue): MdiTextPosition;
243
+ /** Format a canonical full `start-end` range such as `3:18-3:24`. */
244
+ declare function formatMdiTextRange(range: MdiTextRange): string;
245
+ /**
246
+ * Resolve a text range to its source-derived UTF-8 spans. Synthetic table or
247
+ * paragraph separators are deliberately omitted.
248
+ */
249
+ declare function sourceSpansForTextRange(block: MdiTextBlock, range: MdiTextRange): MdiSourceSpan[];
181
250
  /** Render complete `.mdi` source to standalone semantic HTML in Rust. */
182
251
  declare function renderHtml(source: string, options?: MdiHtmlRenderOptions): string;
183
252
  /**
@@ -248,4 +317,4 @@ declare function renderTextFormatWithDiagnostics(source: string, format: MdiText
248
317
  /** @deprecated Use {@link parse}; it now parses the complete document. */
249
318
  declare const parseMdiSyntax: typeof parse;
250
319
 
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 };
320
+ 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 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, 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,57 @@ 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
+ }
92
145
  interface MdiDiagnostic {
93
146
  severity: "warning" | "error";
94
147
  code: string;
@@ -178,6 +231,22 @@ type MdiSyntaxDocument = MdiDocument;
178
231
  * language-neutral document IR. JavaScript performs no grammar work.
179
232
  */
180
233
  declare function parse(source: string): MdiSyntaxParseResult;
234
+ /**
235
+ * Parse once in Rust and return source-order plaintext blocks, annotations,
236
+ * and grapheme-precise UTF-8 source maps alongside the complete document IR.
237
+ */
238
+ declare function getMdiTextBlocks(source: string): MdiTextBlocksResult;
239
+ /** Parse and validate a canonical one-based `block:character` position. */
240
+ declare function parseMdiTextPosition(position: string): MdiTextPositionValue;
241
+ /** Format a validated one-based text position. */
242
+ declare function formatMdiTextPosition(position: MdiTextPositionValue): MdiTextPosition;
243
+ /** Format a canonical full `start-end` range such as `3:18-3:24`. */
244
+ declare function formatMdiTextRange(range: MdiTextRange): string;
245
+ /**
246
+ * Resolve a text range to its source-derived UTF-8 spans. Synthetic table or
247
+ * paragraph separators are deliberately omitted.
248
+ */
249
+ declare function sourceSpansForTextRange(block: MdiTextBlock, range: MdiTextRange): MdiSourceSpan[];
181
250
  /** Render complete `.mdi` source to standalone semantic HTML in Rust. */
182
251
  declare function renderHtml(source: string, options?: MdiHtmlRenderOptions): string;
183
252
  /**
@@ -248,4 +317,4 @@ declare function renderTextFormatWithDiagnostics(source: string, format: MdiText
248
317
  /** @deprecated Use {@link parse}; it now parses the complete document. */
249
318
  declare const parseMdiSyntax: typeof parse;
250
319
 
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 };
320
+ 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 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, 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,
@@ -18,14 +23,20 @@ import {
18
23
  renderTextFormatWithDiagnostics,
19
24
  renderTextWithDiagnostics,
20
25
  serializeMdi,
26
+ sourceSpansForTextRange,
21
27
  toPublicationMdast
22
- } from "./chunk-WEJ62HS6.js";
28
+ } from "./chunk-JTDOLXFG.js";
23
29
  export {
24
30
  MDI_IR_VERSION,
25
31
  MDI_SPEC_VERSION,
32
+ MDI_TEXT_PROJECTION_VERSION,
33
+ formatMdiTextPosition,
34
+ formatMdiTextRange,
35
+ getMdiTextBlocks,
26
36
  initializeMdi,
27
37
  parse,
28
38
  parseMdiSyntax,
39
+ parseMdiTextPosition,
29
40
  prepareRender,
30
41
  renderDocx,
31
42
  renderDocxWithDiagnostics,
@@ -40,5 +51,6 @@ export {
40
51
  renderTextFormatWithDiagnostics,
41
52
  renderTextWithDiagnostics,
42
53
  serializeMdi,
54
+ sourceSpansForTextRange,
43
55
  toPublicationMdast
44
56
  };
package/dist/node.cjs CHANGED
@@ -41,6 +41,7 @@ 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,
44
45
  parseMdiSyntaxJson,
45
46
  renderHtml: renderHtmlFromRust,
46
47
  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-JTDOLXFG.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.17",
3
+ "version": "2.0.19",
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.17",
36
- "@illusions-lab/mdi-export-profile": "^2.0.22",
37
- "@illusions-lab/mdi-to-epub": "^2.0.32",
35
+ "@illusions-lab/mdi-core": "^2.0.19",
36
+ "@illusions-lab/mdi-export-profile": "^2.0.24",
37
+ "@illusions-lab/mdi-to-epub": "^2.0.34",
38
38
  "@types/mdast": "^4.0.0",
39
39
  "yaml": "^2.0.0"
40
40
  },