@effected/markdown 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Markdown.js CHANGED
@@ -3,7 +3,9 @@ import { isGuardExceeded, isRawMarkdownError } from "./internal/carriers.js";
3
3
  import { parseBlocks } from "./internal/blockParser.js";
4
4
  import { parsePhrasingText } from "./internal/phrasing.js";
5
5
  import { stringifyBlocks } from "./internal/stringify.js";
6
+ import { assertWithinNestingDepth } from "./internal/treeDepth.js";
6
7
  import { MarkdownDiagnostic } from "./MarkdownDiagnostic.js";
8
+ import { MarkdownTransform } from "./MarkdownTransform.js";
7
9
  import { Effect, Result, Schema, SchemaIssue, SchemaTransformation } from "effect";
8
10
 
9
11
  // src/Markdown.ts
@@ -282,8 +284,8 @@ var Markdown = class Markdown {
282
284
  * surgical editing goes through the offset-splice edit layer instead.
283
285
  *
284
286
  * **The canonical form is a stability commitment, so a test may assert
285
- * against these bytes.** There are no options to configure and therefore
286
- * nothing to drift, the choices below are pinned by byte-level tests, and
287
+ * against these bytes.** The one option, `canonical`, only selects
288
+ * between these bytes and the author's spelling; the choices below are pinned by byte-level tests, and
287
289
  * the engine is cross-checked against commonmark.js over the full
288
290
  * CommonMark 0.31.2 corpus. Changing any of them is a **breaking** change
289
291
  * to this package, never a patch. A consumer asserting on rendered
@@ -352,13 +354,24 @@ var Markdown = class Markdown {
352
354
  * trip on a tree nesting past the depth cap fails, and only synthesized
353
355
  * or decoded trees can nest that far.
354
356
  *
357
+ * **`canonical: true` ignores fidelity fields.** With
358
+ * {@link MarkdownStringifyOptions.canonical} set, every fidelity field is
359
+ * cleared first, so a parsed `* a` list, setext heading, `_em_`, `~~~` fence
360
+ * or `***` break all emit the canonical spelling. It is the same as
361
+ * serializing `MarkdownTransform.canonicalize(node)`, without the
362
+ * `Mdast.toMdast` / `Mdast.fromMdastResult` round trip. The default keeps
363
+ * honouring fidelity fields.
364
+ *
355
365
  * @param node - The document tree, or a single flow node, to serialize.
366
+ * @param options - Optional {@link MarkdownStringifyOptions}.
356
367
  * @returns A `Result` succeeding with markdown source, or failing with
357
368
  * {@link MarkdownStringifyError}.
358
369
  */
359
- static stringifyResult(node) {
370
+ static stringifyResult(node, options) {
360
371
  try {
361
- return Result.succeed(stringifyBlocks(node.type === "root" ? node.children : [node]));
372
+ if (options?.canonical === true) assertWithinNestingDepth(node);
373
+ const source = options?.canonical === true ? MarkdownTransform.canonicalize(node) : node;
374
+ return Result.succeed(stringifyBlocks(source.type === "root" ? source.children : [source]));
362
375
  } catch (caught) {
363
376
  if (isGuardExceeded(caught)) return Result.fail(new MarkdownStringifyError({ diagnostic: MarkdownDiagnostic.make({
364
377
  code: caught.reason,
@@ -394,10 +407,12 @@ var Markdown = class Markdown {
394
407
  * see {@link Markdown.stringifyResult}.
395
408
  *
396
409
  * @param node - The document tree, or a single flow node, to serialize.
410
+ * @param options - Optional {@link MarkdownStringifyOptions}; `canonical: true`
411
+ * discards fidelity fields.
397
412
  * @returns An `Effect` that succeeds with markdown source, or fails with
398
413
  * {@link MarkdownStringifyError}.
399
414
  */
400
- static stringify = Effect.fn("Markdown.stringify")((node) => Effect.fromResult(Markdown.stringifyResult(node)));
415
+ static stringify = Effect.fn("Markdown.stringify")((node, options) => Effect.fromResult(Markdown.stringifyResult(node, options)));
401
416
  /**
402
417
  * A `Schema<Root, string>` decoding markdown source into a {@link Root}
403
418
  * tree and encoding a tree back to canonical markdown via
@@ -1,4 +1,5 @@
1
1
  import { MARKDOWN_PARSE_ERROR_CODES } from "./internal/carriers.js";
2
+ import { LineIndex } from "@effected/source";
2
3
  import { Schema } from "effect";
3
4
 
4
5
  // src/MarkdownDiagnostic.ts
@@ -16,14 +17,20 @@ const MarkdownParseErrorCode = Schema.Literals(MARKDOWN_PARSE_ERROR_CODES);
16
17
  * zero-based `line`/`character`).
17
18
  *
18
19
  * @remarks
19
- * The five-field positional core (`code`/`offset`/`length`/`line`/`character`)
20
- * is structurally identical to `@effected/toml`'s `TomlDiagnostic` (and, by
21
- * the same cross-package contract, `@effected/jsonc`'s parse-error detail
22
- * shape and `@effected/yaml`'s `YamlDiagnostic`); `message` is this
23
- * package's additive extra. `line`/`character` here are zero-based to match
24
- * that contract — a different numbering from the one-based `line`/`column`
25
- * unist `Point`s carried on `MarkdownNode` positions, which is a deliberate,
26
- * unrelated convention for the AST rather than a mismatch to reconcile.
20
+ * Satisfies `@effected/source`'s `SourceDiagnostic<MarkdownParseErrorCode>`:
21
+ * the positional core (`code`/`message`/`offset`/`length`/`line`/`character`)
22
+ * the kit's format packages share, so a consumer printing or serving
23
+ * diagnostics (an LSP server, a CLI annotation) is written once against that
24
+ * shape. `line`/`character` are zero-based, `character` in UTF-16 code units,
25
+ * with `\n`, `\r\n` and a bare `\r` as line breaks: exactly
26
+ * `LineIndex.make(source).positionAt(offset)`, and the same index
27
+ * {@link MarkdownDocument.lineIndex} returns. An offset at the `\n` of a
28
+ * `\r\n` reads as the end of its line.
29
+ *
30
+ * The one-based `line`/`column` unist `Point`s carried on `MarkdownNode`
31
+ * positions read the same index, one-based: a deliberate, different
32
+ * convention for the AST rather than a mismatch to reconcile (see
33
+ * {@link MarkdownDocument.lineIndex} for the exact mapping).
27
34
  *
28
35
  * @public
29
36
  */
@@ -40,12 +47,20 @@ var MarkdownDiagnostic = class MarkdownDiagnostic extends Schema.Class("Markdown
40
47
  * from `offset` against the source text. Advanced — the parse entry
41
48
  * points call this for you.
42
49
  *
43
- * @param source - The source text the record's `offset` indexes into.
50
+ * @remarks
51
+ * Pass a line index already built over the source to materialize many
52
+ * records without rescanning the text; a string builds one. The
53
+ * parameter is the structural `SourceIndex`, so an index from any
54
+ * installed copy of `@effected/source` is accepted. An offset
55
+ * outside the source clamps into it, as `LineIndex.positionAt` does.
56
+ *
57
+ * @param source - The source text the record's `offset` indexes into, or
58
+ * a `LineIndex` over it.
44
59
  * @param raw - The engine record: `code`, `message`, `offset` and `length`.
45
60
  * @returns The diagnostic with `line` and `character` filled in.
46
61
  */
47
62
  static fromRaw(source, raw) {
48
- const { line, character } = lineChar(source, raw.offset);
63
+ const { line, character } = (typeof source === "string" ? LineIndex.make(source) : source).positionAt(raw.offset);
49
64
  return MarkdownDiagnostic.make({
50
65
  code: raw.code,
51
66
  message: raw.message,
@@ -56,32 +71,5 @@ var MarkdownDiagnostic = class MarkdownDiagnostic extends Schema.Class("Markdown
56
71
  });
57
72
  }
58
73
  };
59
- /**
60
- * Compute the zero-based line/character position of `offset` within `text`.
61
- * Recognizes `\n`, `\r` and `\r\n` as line breaks; a CRLF pair counts as a
62
- * single newline (toml `TomlDiagnostic` precedent — kept independent of
63
- * `internal/lineIndex.ts`, which answers a different, one-based question
64
- * for AST positions).
65
- */
66
- function lineChar(text, offset) {
67
- let line = 0;
68
- let lineStart = 0;
69
- const limit = Math.min(offset, text.length);
70
- for (let i = 0; i < limit; i++) {
71
- const ch = text.charCodeAt(i);
72
- if (ch === 10) {
73
- line++;
74
- lineStart = i + 1;
75
- } else if (ch === 13) {
76
- if (i + 1 < text.length && text.charCodeAt(i + 1) === 10) i++;
77
- line++;
78
- lineStart = i + 1;
79
- }
80
- }
81
- return {
82
- line,
83
- character: offset - lineStart
84
- };
85
- }
86
74
 
87
75
  export { MarkdownDiagnostic, MarkdownParseErrorCode };
@@ -4,8 +4,9 @@ import { scanFrontmatter } from "./internal/blocks/frontmatter.js";
4
4
  import { preprocessLines } from "./internal/preprocess.js";
5
5
  import { normalizeLabelText } from "./internal/references.js";
6
6
  import { MarkdownDiagnostic } from "./MarkdownDiagnostic.js";
7
- import { parsePassResult } from "./Markdown.js";
8
7
  import { rootHeadingBounds } from "./internal/sectionBounds.js";
8
+ import { parsePassResult } from "./Markdown.js";
9
+ import { LineIndex } from "@effected/source";
9
10
  import { Effect, Option, Result, Schema } from "effect";
10
11
 
11
12
  // src/MarkdownDocument.ts
@@ -122,6 +123,12 @@ const phrasingText = (nodes) => {
122
123
  return out;
123
124
  };
124
125
  /**
126
+ * Each document's line index, built at most once. A side table rather than a
127
+ * field: the index is derived from `source`, so it is no part of the
128
+ * document's schema, its equality or its encoded form.
129
+ */
130
+ const lineIndexes = /* @__PURE__ */ new WeakMap();
131
+ /**
125
132
  * A parsed markdown document: the original `source`, the mdast-shaped
126
133
  * {@link Root} tree, the non-fatal {@link MarkdownDiagnostic}s the parse
127
134
  * produced, and the link-reference `definitions` index.
@@ -213,6 +220,55 @@ var MarkdownDocument = class MarkdownDocument extends Schema.Class("MarkdownDocu
213
220
  return scanFrontmatter(preprocessLines(this.source), this.source) !== null;
214
221
  }
215
222
  /**
223
+ * The line index over `source`: offsets to zero-based `{ line, character }`
224
+ * and back, with exactly the rules this package's own positions use.
225
+ *
226
+ * @remarks
227
+ * A `LineIndex` from `@effected/source` (re-exported from this package's
228
+ * entry point), typed as the structural `SourceIndex` so a consumer on
229
+ * another installed copy of `@effected/source` can pass it on, with its
230
+ * default `"lsp"` line breaks: `\n`, `\r\n` and a
231
+ * bare `\r`, `character` in UTF-16 code units, an offset at the `\n` of a
232
+ * `\r\n` reading as the end of its line. So for any offset into
233
+ * `source`:
234
+ *
235
+ * - `doc.lineIndex.positionAt(offset)` is the `line`/`character` a
236
+ * {@link MarkdownDiagnostic} at that offset carries;
237
+ * - a node's unist `Point` has `line` = `lineAt(offset) + 1` and
238
+ * `column` = `offset - lineStart(line) + 1`: `positionAt` plus one on
239
+ * both axes everywhere except at the `\n` of a `\r\n`, which a Point
240
+ * counts as one column past the `\r` (as `vfile-location` does) and
241
+ * `positionAt` reads as the end of its line;
242
+ * - `offsetAt`, `spanOf` and `rangeOf` convert an LSP `Position` or
243
+ * `Range` back to an offset or a `MarkdownRange`-shaped
244
+ * `{ offset, length }`.
245
+ *
246
+ * A parsed document returns the index the parse itself built; a document
247
+ * constructed with `make` builds one on first access. Either way it is
248
+ * built once per document and shared by every later access. To index a
249
+ * text with no document, `LineIndex.make(text)` gives the same answers.
250
+ *
251
+ * @example
252
+ * ```ts
253
+ * import { MarkdownDocument } from "@effected/markdown";
254
+ * import { Result } from "effect";
255
+ *
256
+ * const result = MarkdownDocument.parseResult("# Title\r\n\r\nBody\n");
257
+ * if (Result.isSuccess(result)) {
258
+ * const index = result.success.lineIndex;
259
+ * console.log(index.positionAt(12)); // => { line: 2, character: 1 }
260
+ * console.log(index.offsetAt({ line: 2, character: 0 })); // => 11
261
+ * }
262
+ * ```
263
+ */
264
+ get lineIndex() {
265
+ const cached = lineIndexes.get(this);
266
+ if (cached !== void 0) return cached;
267
+ const index = LineIndex.make(this.source);
268
+ lineIndexes.set(this, index);
269
+ return index;
270
+ }
271
+ /**
216
272
  * Every heading in the document, in document order, wherever it sits —
217
273
  * including headings nested inside blockquotes and list items.
218
274
  *
@@ -379,12 +435,16 @@ var MarkdownDocument = class MarkdownDocument extends Schema.Class("MarkdownDocu
379
435
  * `MarkdownParseError`.
380
436
  */
381
437
  static parseResult(text, options) {
382
- return Result.map(parsePassResult(text, options), (pass) => MarkdownDocument.make({
383
- source: text,
384
- root: pass.root,
385
- diagnostics: pass.carriers.map((carrier) => MarkdownDiagnostic.fromRaw(text, carrier)),
386
- definitions: pass.refmap
387
- }));
438
+ return Result.map(parsePassResult(text, options), (pass) => {
439
+ const document = MarkdownDocument.make({
440
+ source: text,
441
+ root: pass.root,
442
+ diagnostics: pass.carriers.map((carrier) => MarkdownDiagnostic.fromRaw(pass.lineIndex, carrier)),
443
+ definitions: pass.refmap
444
+ });
445
+ lineIndexes.set(document, pass.lineIndex);
446
+ return document;
447
+ });
388
448
  }
389
449
  /**
390
450
  * Parse markdown into a {@link MarkdownDocument}. Defined in terms of
@@ -1,3 +1,4 @@
1
+ import { FIDELITY_FIELDS, clearFidelity } from "./internal/fidelity.js";
1
2
  import { rootHeadingBounds } from "./internal/sectionBounds.js";
2
3
 
3
4
  // src/MarkdownTransform.ts
@@ -76,6 +77,32 @@ var MarkdownTransform = class {
76
77
  } });
77
78
  }
78
79
  /**
80
+ * Return a copy of the tree with every fidelity field cleared, so
81
+ * `Markdown.stringify` emits the canonical form instead of the author's
82
+ * spelling.
83
+ *
84
+ * @remarks
85
+ * Clears `headingStyle`, `markerChar` (emphasis, strong, thematic break),
86
+ * `fenceChar` and `fenceLength`, `bulletChar` and `delimiter`, and
87
+ * `breakStyle`. `Text`'s `escapeStyle` is an emitter instruction rather
88
+ * than a recorded spelling and is kept. Positions and all mdast fields are
89
+ * untouched. The input is never mutated; a subtree carrying no fidelity
90
+ * field is returned as the same instance, following
91
+ * {@link MarkdownTransform.(map:1)}, on which this is built. Like `map` it
92
+ * recurses without a depth guard, so a tree nested past the stack throws a
93
+ * `RangeError`; `Markdown.stringifyResult` with `canonical: true` checks
94
+ * depth first and fails typed at the stringifier's own limit instead.
95
+ *
96
+ * `Markdown.stringify(tree, { canonical: true })` is exactly
97
+ * `Markdown.stringify(MarkdownTransform.canonicalize(tree))`.
98
+ *
99
+ * @param node - The node whose fidelity fields to clear.
100
+ * @returns The node with fidelity fields cleared.
101
+ */
102
+ static canonicalize(node) {
103
+ return mapNode(node, canonicalHandlers);
104
+ }
105
+ /**
79
106
  * Locate the root-level sections of one heading depth by child index —
80
107
  * no source text needed, so it works on a tree a
81
108
  * `MarkdownTransform.map` has reshaped.
@@ -140,6 +167,7 @@ var MarkdownTransform = class {
140
167
  }
141
168
  };
142
169
  const isHeadingDepth = (value) => Number.isInteger(value) && value >= 1 && value <= 6;
170
+ const canonicalHandlers = Object.fromEntries(Object.keys(FIDELITY_FIELDS).map((type) => [type, clearFidelity]));
143
171
  function mapNode(node, handlers) {
144
172
  let current = node;
145
173
  if ("children" in node) {
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![Node.js %3E%3D24.11.0](https://img.shields.io/badge/Node.js-%3E%3D24.11.0-5fa04e.svg)](https://nodejs.org/)
6
6
  [![TypeScript 7.0](https://img.shields.io/badge/TypeScript-7.0-3178c6.svg)](https://www.typescriptlang.org/)
7
7
 
8
- Zero-dependency CommonMark 0.31.2 and GFM parsing, editing and transformation expressed as Effect schemas and pure functions. Parse markdown into mdast-shaped nodes carrying byte offsets, navigate headings, sections and links, compute surgical offset-splice edits, normalize markers, decode frontmatter into a validated domain schema, project to and from plain mdast for the remark ecosystem, and walk a document as a `Stream`.
8
+ CommonMark 0.31.2 and GFM parsing, editing and transformation expressed as Effect schemas and pure functions. Parse markdown into mdast-shaped nodes carrying byte offsets, navigate headings, sections and links, compute surgical offset-splice edits, normalize markers, decode frontmatter into a validated domain schema, project to and from plain mdast for the remark ecosystem, and walk a document as a `Stream`.
9
9
 
10
10
  > **Pre-`1.0.0`.** This package is part of the `@effected/*` kit, built on stable
11
11
  > Effect v4 (`effect` `^4.0.0`) and still in `0.x` development. Stable Effect
@@ -41,7 +41,7 @@ npm install @effected/markdown effect
41
41
  pnpm add @effected/markdown effect
42
42
  ```
43
43
 
44
- Requires Node.js >=24.11.0. `effect` v4 is a peer dependency; the package itself adds no runtime dependencies.
44
+ Requires Node.js >=24.11.0. `effect` v4 is a peer dependency; the package's one runtime dependency is the dependency-free `@effected/source`, whose `LineIndex` it re-exports.
45
45
 
46
46
  `@effected/yaml`, `@effected/toml` and `@effected/jsonc` are **optional** peers, required only by the frontmatter codec module you import. A consumer decoding yaml frontmatter installs `@effected/yaml` and nothing else; a consumer who never touches frontmatter installs none of them.
47
47
 
@@ -314,7 +314,9 @@ Serialization matches `mdxJsxToMarkdown`'s defaults — `"` quotes, spaced self-
314
314
 
315
315
  ## The canonical form is stable
316
316
 
317
- `Markdown.stringify` takes no options. It emits one canonical form, that form is pinned by byte-level tests, the engine behind it is cross-checked against commonmark.js over the full CommonMark 0.31.2 corpus, and **changing any of it is a breaking change to this package rather than a patch**. So a test may assert on these bytes, and a pipeline that needs stable rendered markdown should serialize through here instead of a third-party stringifier whose defaults are free to move between releases.
317
+ `Markdown.stringify` emits one canonical form, that form is pinned by byte-level tests, the engine behind it is cross-checked against commonmark.js over the full CommonMark 0.31.2 corpus, and **changing any of it is a breaking change to this package rather than a patch**. So a test may assert on these bytes, and a pipeline that needs stable rendered markdown should serialize through here instead of a third-party stringifier whose defaults are free to move between releases.
318
+
319
+ By default a node's fidelity fields (`bulletChar`, `headingStyle`, `markerChar`, `fenceChar`, `delimiter`) win, so a parsed `* a` list or setext heading re-emits as written. To drop them, pass `{ canonical: true }` to `stringify` or `stringifyResult`, or call the pure `MarkdownTransform.canonicalize(tree)` to get a cleared copy of the tree (the input is never mutated). Both give identical output and replace the `Mdast.toMdast` / `Mdast.fromMdastResult` round trip.
318
320
 
319
321
  For a node carrying no fidelity field:
320
322
 
package/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { LineBreaks, LineIndex, LineIndexOptions, SourceDiagnostic, SourceIndex, SourceIndex as SourceIndex$1, SourceLocation, SourcePosition, SourceRange, SourceSpan } from "@effected/source";
1
2
  import { Data, Effect, Option, Result, Schema, Stream } from "effect";
2
3
  declare const Point_base: Schema.Class<Point, Schema.Struct<{
3
4
  readonly line: Schema.Number;
@@ -968,14 +969,20 @@ declare const MarkdownDiagnostic_base: Schema.Class<MarkdownDiagnostic, Schema.S
968
969
  * zero-based `line`/`character`).
969
970
  *
970
971
  * @remarks
971
- * The five-field positional core (`code`/`offset`/`length`/`line`/`character`)
972
- * is structurally identical to `@effected/toml`'s `TomlDiagnostic` (and, by
973
- * the same cross-package contract, `@effected/jsonc`'s parse-error detail
974
- * shape and `@effected/yaml`'s `YamlDiagnostic`); `message` is this
975
- * package's additive extra. `line`/`character` here are zero-based to match
976
- * that contract — a different numbering from the one-based `line`/`column`
977
- * unist `Point`s carried on `MarkdownNode` positions, which is a deliberate,
978
- * unrelated convention for the AST rather than a mismatch to reconcile.
972
+ * Satisfies `@effected/source`'s `SourceDiagnostic<MarkdownParseErrorCode>`:
973
+ * the positional core (`code`/`message`/`offset`/`length`/`line`/`character`)
974
+ * the kit's format packages share, so a consumer printing or serving
975
+ * diagnostics (an LSP server, a CLI annotation) is written once against that
976
+ * shape. `line`/`character` are zero-based, `character` in UTF-16 code units,
977
+ * with `\n`, `\r\n` and a bare `\r` as line breaks: exactly
978
+ * `LineIndex.make(source).positionAt(offset)`, and the same index
979
+ * {@link MarkdownDocument.lineIndex} returns. An offset at the `\n` of a
980
+ * `\r\n` reads as the end of its line.
981
+ *
982
+ * The one-based `line`/`column` unist `Point`s carried on `MarkdownNode`
983
+ * positions read the same index, one-based: a deliberate, different
984
+ * convention for the AST rather than a mismatch to reconcile (see
985
+ * {@link MarkdownDocument.lineIndex} for the exact mapping).
979
986
  *
980
987
  * @public
981
988
  */
@@ -985,11 +992,19 @@ export declare class MarkdownDiagnostic extends MarkdownDiagnostic_base {
985
992
  * from `offset` against the source text. Advanced — the parse entry
986
993
  * points call this for you.
987
994
  *
988
- * @param source - The source text the record's `offset` indexes into.
995
+ * @remarks
996
+ * Pass a line index already built over the source to materialize many
997
+ * records without rescanning the text; a string builds one. The
998
+ * parameter is the structural `SourceIndex`, so an index from any
999
+ * installed copy of `@effected/source` is accepted. An offset
1000
+ * outside the source clamps into it, as `LineIndex.positionAt` does.
1001
+ *
1002
+ * @param source - The source text the record's `offset` indexes into, or
1003
+ * a `LineIndex` over it.
989
1004
  * @param raw - The engine record: `code`, `message`, `offset` and `length`.
990
1005
  * @returns The diagnostic with `line` and `character` filled in.
991
1006
  */
992
- static fromRaw(source: string, raw: {
1007
+ static fromRaw(source: string | SourceIndex$1, raw: {
993
1008
  readonly code: MarkdownParseErrorCode;
994
1009
  readonly message: string;
995
1010
  readonly offset: number;
@@ -1013,6 +1028,19 @@ export declare const MarkdownDialect: Schema.Literals<readonly ["commonmark", "g
1013
1028
  * @public
1014
1029
  */
1015
1030
  export type MarkdownDialect = typeof MarkdownDialect.Type;
1031
+ /**
1032
+ * Options for `Markdown.stringify` and `Markdown.stringifyResult`.
1033
+ *
1034
+ * @public
1035
+ */
1036
+ interface MarkdownStringifyOptions {
1037
+ /**
1038
+ * Ignore fidelity fields (marker characters, fence style, heading
1039
+ * spelling) and emit the canonical form. Defaults to `false`, which honours
1040
+ * them.
1041
+ */
1042
+ readonly canonical?: boolean;
1043
+ }
1016
1044
  declare const MarkdownParseOptions_base: Schema.Class<MarkdownParseOptions, Schema.Struct<{
1017
1045
  readonly dialect: Schema.optionalKey<Schema.Literals<readonly ["commonmark", "gfm"]>>;
1018
1046
  readonly frontmatter: Schema.optionalKey<Schema.Boolean>;
@@ -1225,8 +1253,8 @@ export declare class Markdown {
1225
1253
  * surgical editing goes through the offset-splice edit layer instead.
1226
1254
  *
1227
1255
  * **The canonical form is a stability commitment, so a test may assert
1228
- * against these bytes.** There are no options to configure and therefore
1229
- * nothing to drift, the choices below are pinned by byte-level tests, and
1256
+ * against these bytes.** The one option, `canonical`, only selects
1257
+ * between these bytes and the author's spelling; the choices below are pinned by byte-level tests, and
1230
1258
  * the engine is cross-checked against commonmark.js over the full
1231
1259
  * CommonMark 0.31.2 corpus. Changing any of them is a **breaking** change
1232
1260
  * to this package, never a patch. A consumer asserting on rendered
@@ -1295,11 +1323,20 @@ export declare class Markdown {
1295
1323
  * trip on a tree nesting past the depth cap fails, and only synthesized
1296
1324
  * or decoded trees can nest that far.
1297
1325
  *
1326
+ * **`canonical: true` ignores fidelity fields.** With
1327
+ * {@link MarkdownStringifyOptions.canonical} set, every fidelity field is
1328
+ * cleared first, so a parsed `* a` list, setext heading, `_em_`, `~~~` fence
1329
+ * or `***` break all emit the canonical spelling. It is the same as
1330
+ * serializing `MarkdownTransform.canonicalize(node)`, without the
1331
+ * `Mdast.toMdast` / `Mdast.fromMdastResult` round trip. The default keeps
1332
+ * honouring fidelity fields.
1333
+ *
1298
1334
  * @param node - The document tree, or a single flow node, to serialize.
1335
+ * @param options - Optional {@link MarkdownStringifyOptions}.
1299
1336
  * @returns A `Result` succeeding with markdown source, or failing with
1300
1337
  * {@link MarkdownStringifyError}.
1301
1338
  */
1302
- static stringifyResult(node: Root | FlowContent): Result.Result<string, MarkdownStringifyError>;
1339
+ static stringifyResult(node: Root | FlowContent, options?: MarkdownStringifyOptions): Result.Result<string, MarkdownStringifyError>;
1303
1340
  /**
1304
1341
  * Serialize a {@link Root} tree, or a single {@link (FlowContent:type)} node, to
1305
1342
  * canonical markdown. Defined in terms of {@link Markdown.stringifyResult}
@@ -1323,10 +1360,12 @@ export declare class Markdown {
1323
1360
  * see {@link Markdown.stringifyResult}.
1324
1361
  *
1325
1362
  * @param node - The document tree, or a single flow node, to serialize.
1363
+ * @param options - Optional {@link MarkdownStringifyOptions}; `canonical: true`
1364
+ * discards fidelity fields.
1326
1365
  * @returns An `Effect` that succeeds with markdown source, or fails with
1327
1366
  * {@link MarkdownStringifyError}.
1328
1367
  */
1329
- static readonly stringify: (node: Root | FlowContent) => Effect.Effect<string, MarkdownStringifyError, never>;
1368
+ static readonly stringify: (node: Root | FlowContent, options?: MarkdownStringifyOptions | undefined) => Effect.Effect<string, MarkdownStringifyError, never>;
1330
1369
  /**
1331
1370
  * A `Schema<Root, string>` decoding markdown source into a {@link Root}
1332
1371
  * tree and encoding a tree back to canonical markdown via
@@ -1629,6 +1668,49 @@ export declare class MarkdownDocument extends MarkdownDocument_base {
1629
1668
  * checking repeatedly.
1630
1669
  */
1631
1670
  get hasFrontmatterBlock(): boolean;
1671
+ /**
1672
+ * The line index over `source`: offsets to zero-based `{ line, character }`
1673
+ * and back, with exactly the rules this package's own positions use.
1674
+ *
1675
+ * @remarks
1676
+ * A `LineIndex` from `@effected/source` (re-exported from this package's
1677
+ * entry point), typed as the structural `SourceIndex` so a consumer on
1678
+ * another installed copy of `@effected/source` can pass it on, with its
1679
+ * default `"lsp"` line breaks: `\n`, `\r\n` and a
1680
+ * bare `\r`, `character` in UTF-16 code units, an offset at the `\n` of a
1681
+ * `\r\n` reading as the end of its line. So for any offset into
1682
+ * `source`:
1683
+ *
1684
+ * - `doc.lineIndex.positionAt(offset)` is the `line`/`character` a
1685
+ * {@link MarkdownDiagnostic} at that offset carries;
1686
+ * - a node's unist `Point` has `line` = `lineAt(offset) + 1` and
1687
+ * `column` = `offset - lineStart(line) + 1`: `positionAt` plus one on
1688
+ * both axes everywhere except at the `\n` of a `\r\n`, which a Point
1689
+ * counts as one column past the `\r` (as `vfile-location` does) and
1690
+ * `positionAt` reads as the end of its line;
1691
+ * - `offsetAt`, `spanOf` and `rangeOf` convert an LSP `Position` or
1692
+ * `Range` back to an offset or a `MarkdownRange`-shaped
1693
+ * `{ offset, length }`.
1694
+ *
1695
+ * A parsed document returns the index the parse itself built; a document
1696
+ * constructed with `make` builds one on first access. Either way it is
1697
+ * built once per document and shared by every later access. To index a
1698
+ * text with no document, `LineIndex.make(text)` gives the same answers.
1699
+ *
1700
+ * @example
1701
+ * ```ts
1702
+ * import { MarkdownDocument } from "@effected/markdown";
1703
+ * import { Result } from "effect";
1704
+ *
1705
+ * const result = MarkdownDocument.parseResult("# Title\r\n\r\nBody\n");
1706
+ * if (Result.isSuccess(result)) {
1707
+ * const index = result.success.lineIndex;
1708
+ * console.log(index.positionAt(12)); // => { line: 2, character: 1 }
1709
+ * console.log(index.offsetAt({ line: 2, character: 0 })); // => 11
1710
+ * }
1711
+ * ```
1712
+ */
1713
+ get lineIndex(): SourceIndex$1;
1632
1714
  /**
1633
1715
  * Every heading in the document, in document order, wherever it sits —
1634
1716
  * including headings nested inside blockquotes and list items.
@@ -2873,6 +2955,30 @@ export declare class MarkdownTransform {
2873
2955
  * a heading depth or `min > max` — a wiring error, not a data error.
2874
2956
  */
2875
2957
  static shiftHeadings<N extends MarkdownNode>(node: N, by: number, options?: ShiftHeadingsOptions): N;
2958
+ /**
2959
+ * Return a copy of the tree with every fidelity field cleared, so
2960
+ * `Markdown.stringify` emits the canonical form instead of the author's
2961
+ * spelling.
2962
+ *
2963
+ * @remarks
2964
+ * Clears `headingStyle`, `markerChar` (emphasis, strong, thematic break),
2965
+ * `fenceChar` and `fenceLength`, `bulletChar` and `delimiter`, and
2966
+ * `breakStyle`. `Text`'s `escapeStyle` is an emitter instruction rather
2967
+ * than a recorded spelling and is kept. Positions and all mdast fields are
2968
+ * untouched. The input is never mutated; a subtree carrying no fidelity
2969
+ * field is returned as the same instance, following
2970
+ * {@link MarkdownTransform.(map:1)}, on which this is built. Like `map` it
2971
+ * recurses without a depth guard, so a tree nested past the stack throws a
2972
+ * `RangeError`; `Markdown.stringifyResult` with `canonical: true` checks
2973
+ * depth first and fails typed at the stringifier's own limit instead.
2974
+ *
2975
+ * `Markdown.stringify(tree, { canonical: true })` is exactly
2976
+ * `Markdown.stringify(MarkdownTransform.canonicalize(tree))`.
2977
+ *
2978
+ * @param node - The node whose fidelity fields to clear.
2979
+ * @returns The node with fidelity fields cleared.
2980
+ */
2981
+ static canonicalize<N extends MarkdownNode>(node: N): N;
2876
2982
  /**
2877
2983
  * Locate the root-level sections of one heading depth by child index —
2878
2984
  * no source text needed, so it works on a tree a
@@ -2956,120 +3062,22 @@ export type MarkdownVisitorEvent = Data.TaggedEnum<{
2956
3062
  *
2957
3063
  * @public
2958
3064
  */
2959
- export declare const MarkdownVisitorEvent: {
2960
- readonly $is: <Tag extends "Enter" | "Error" | "Exit">(tag: Tag) => (u: unknown) => u is Extract<{
2961
- readonly _tag: "Enter";
2962
- readonly node: MarkdownNode;
2963
- readonly path: MarkdownPath;
2964
- readonly depth: number;
2965
- }, {
2966
- readonly _tag: Tag;
2967
- }> | Extract<{
2968
- readonly _tag: "Error";
2969
- readonly diagnostic: MarkdownDiagnostic;
2970
- readonly path: MarkdownPath;
2971
- readonly depth: number;
2972
- }, {
2973
- readonly _tag: Tag;
2974
- }> | Extract<{
2975
- readonly _tag: "Exit";
2976
- readonly node: MarkdownNode;
2977
- readonly path: MarkdownPath;
2978
- readonly depth: number;
2979
- }, {
2980
- readonly _tag: Tag;
2981
- }>;
2982
- readonly $match: {
2983
- <Cases extends {
2984
- readonly Enter: (args: {
2985
- readonly _tag: "Enter";
2986
- readonly node: MarkdownNode;
2987
- readonly path: MarkdownPath;
2988
- readonly depth: number;
2989
- }) => any;
2990
- readonly Error: (args: {
2991
- readonly _tag: "Error";
2992
- readonly diagnostic: MarkdownDiagnostic;
2993
- readonly path: MarkdownPath;
2994
- readonly depth: number;
2995
- }) => any;
2996
- readonly Exit: (args: {
2997
- readonly _tag: "Exit";
2998
- readonly node: MarkdownNode;
2999
- readonly path: MarkdownPath;
3000
- readonly depth: number;
3001
- }) => any;
3002
- }>(cases: Cases): (value: {
3003
- readonly _tag: "Enter";
3004
- readonly node: MarkdownNode;
3005
- readonly path: MarkdownPath;
3006
- readonly depth: number;
3007
- } | {
3008
- readonly _tag: "Error";
3009
- readonly diagnostic: MarkdownDiagnostic;
3010
- readonly path: MarkdownPath;
3011
- readonly depth: number;
3012
- } | {
3013
- readonly _tag: "Exit";
3014
- readonly node: MarkdownNode;
3015
- readonly path: MarkdownPath;
3016
- readonly depth: number;
3017
- }) => import("effect/Unify").Unify<ReturnType<Cases["Enter" | "Error" | "Exit"]>>;
3018
- <Cases extends {
3019
- readonly Enter: (args: {
3020
- readonly _tag: "Enter";
3021
- readonly node: MarkdownNode;
3022
- readonly path: MarkdownPath;
3023
- readonly depth: number;
3024
- }) => any;
3025
- readonly Error: (args: {
3026
- readonly _tag: "Error";
3027
- readonly diagnostic: MarkdownDiagnostic;
3028
- readonly path: MarkdownPath;
3029
- readonly depth: number;
3030
- }) => any;
3031
- readonly Exit: (args: {
3032
- readonly _tag: "Exit";
3033
- readonly node: MarkdownNode;
3034
- readonly path: MarkdownPath;
3035
- readonly depth: number;
3036
- }) => any;
3037
- }>(value: {
3038
- readonly _tag: "Enter";
3039
- readonly node: MarkdownNode;
3040
- readonly path: MarkdownPath;
3041
- readonly depth: number;
3042
- } | {
3043
- readonly _tag: "Error";
3044
- readonly diagnostic: MarkdownDiagnostic;
3045
- readonly path: MarkdownPath;
3046
- readonly depth: number;
3047
- } | {
3048
- readonly _tag: "Exit";
3049
- readonly node: MarkdownNode;
3050
- readonly path: MarkdownPath;
3051
- readonly depth: number;
3052
- }, cases: Cases): import("effect/Unify").Unify<ReturnType<Cases["Enter" | "Error" | "Exit"]>>;
3053
- };
3054
- readonly Enter: Data.TaggedEnum.ConstructorFrom<{
3055
- readonly _tag: "Enter";
3056
- readonly node: MarkdownNode;
3057
- readonly path: MarkdownPath;
3058
- readonly depth: number;
3059
- }, "_tag">;
3060
- readonly Error: Data.TaggedEnum.ConstructorFrom<{
3061
- readonly _tag: "Error";
3062
- readonly diagnostic: MarkdownDiagnostic;
3063
- readonly path: MarkdownPath;
3064
- readonly depth: number;
3065
- }, "_tag">;
3066
- readonly Exit: Data.TaggedEnum.ConstructorFrom<{
3067
- readonly _tag: "Exit";
3068
- readonly node: MarkdownNode;
3069
- readonly path: MarkdownPath;
3070
- readonly depth: number;
3071
- }, "_tag">;
3072
- };
3065
+ export declare const MarkdownVisitorEvent: Data.TaggedEnum.Constructor<{
3066
+ readonly _tag: "Enter";
3067
+ readonly node: MarkdownNode;
3068
+ readonly path: MarkdownPath;
3069
+ readonly depth: number;
3070
+ } | {
3071
+ readonly _tag: "Error";
3072
+ readonly diagnostic: MarkdownDiagnostic;
3073
+ readonly path: MarkdownPath;
3074
+ readonly depth: number;
3075
+ } | {
3076
+ readonly _tag: "Exit";
3077
+ readonly node: MarkdownNode;
3078
+ readonly path: MarkdownPath;
3079
+ readonly depth: number;
3080
+ }>;
3073
3081
  /**
3074
3082
  * Walks a parsed markdown tree as a lazy sequence of `Enter`/`Exit` events in
3075
3083
  * document pre-order — synchronously with {@link MarkdownVisitor.walk}, or as
@@ -3395,5 +3403,5 @@ export declare const TomlFrontmatter: FrontmatterCodec;
3395
3403
  * @public
3396
3404
  */
3397
3405
  export declare const YamlFrontmatter: FrontmatterCodec;
3398
- export type { DocumentHeading, DocumentLink, FrontmatterCodec, FrontmatterResolveError, FrontmatterSchemaError, FrontmatterSchemaResolver, FrontmatterWriteError, LinkBearingNode, MarkdownNodeOfType, MarkdownNodeType, MarkdownPath, MarkdownPhrasingOnlyType, MarkdownRangeLike, MarkdownSegment, MarkdownTransformHandlers, MarkdownTransformResult, MdastNode, MdastToTextOptions, SectionHeadingMatch, SectionQueryOptions, SectionSpan, SectionSpanOptions, ShiftHeadingsOptions };
3406
+ export { type DocumentHeading, type DocumentLink, type FrontmatterCodec, type FrontmatterResolveError, type FrontmatterSchemaError, type FrontmatterSchemaResolver, type FrontmatterWriteError, type LineBreaks, LineIndex, type LineIndexOptions, type LinkBearingNode, type MarkdownNodeOfType, type MarkdownNodeType, type MarkdownPath, type MarkdownPhrasingOnlyType, type MarkdownRangeLike, type MarkdownSegment, type MarkdownStringifyOptions, type MarkdownTransformHandlers, type MarkdownTransformResult, type MdastNode, type MdastToTextOptions, type SectionHeadingMatch, type SectionQueryOptions, type SectionSpan, type SectionSpanOptions, type ShiftHeadingsOptions, type SourceDiagnostic, type SourceIndex, type SourceLocation, type SourcePosition, type SourceRange, type SourceSpan };
3399
3407
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -5,13 +5,14 @@ import { SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, Sc
5
5
  import { FrontmatterNewline, FrontmatterSource, FrontmatterSourceBlock, FrontmatterSourceSplit } from "./FrontmatterSource.js";
6
6
  import { JsonFrontmatter } from "./JsonFrontmatter.js";
7
7
  import { MarkdownDiagnostic, MarkdownParseErrorCode } from "./MarkdownDiagnostic.js";
8
+ import { MarkdownTransform } from "./MarkdownTransform.js";
8
9
  import { Markdown, MarkdownDialect, MarkdownParseError, MarkdownParseOptions, MarkdownStringifyError } from "./Markdown.js";
9
10
  import { DocumentSection, MarkdownDocument } from "./MarkdownDocument.js";
10
11
  import { CodeBlockStyle, MarkdownFormat, MarkdownFormattingOptions, MarkdownModificationError, MarkdownModificationErrorCode } from "./MarkdownFormat.js";
11
- import { MarkdownTransform } from "./MarkdownTransform.js";
12
12
  import { MarkdownVisitor, MarkdownVisitorEvent } from "./MarkdownVisitor.js";
13
13
  import { Mdast, MdastDecodeError } from "./Mdast.js";
14
14
  import { TomlFrontmatter } from "./TomlFrontmatter.js";
15
15
  import { YamlFrontmatter } from "./YamlFrontmatter.js";
16
+ import { LineIndex } from "@effected/source";
16
17
 
17
- export { Blockquote, Break, BreakStyle, BulletChar, Code, CodeBlockStyle, Definition, Delete, DocumentSection, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, FrontmatterContent, FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormat, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterMissingReason, FrontmatterNewline, FrontmatterSource, FrontmatterSourceBlock, FrontmatterSourceSplit, FrontmatterValidationError, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, JsonFrontmatter, Link, LinkReference, List, ListContent, ListDelimiter, ListItem, Markdown, MarkdownDiagnostic, MarkdownDialect, MarkdownDocument, MarkdownEdit, MarkdownFormat, MarkdownFormattingOptions, MarkdownFrontmatter, MarkdownModificationError, MarkdownModificationErrorCode, MarkdownNode, MarkdownParseError, MarkdownParseErrorCode, MarkdownParseOptions, MarkdownRange, MarkdownStringifyError, MarkdownTransform, MarkdownVisitor, MarkdownVisitorEvent, Mdast, MdastDecodeError, MdxFlowExpression, MdxJsxAttribute, MdxJsxAttributeContent, MdxJsxAttributeValueExpression, MdxJsxExpressionAttribute, MdxJsxFlowElement, MdxJsxTextElement, MdxTextExpression, MdxjsEsm, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar, TomlFrontmatter, YamlFrontmatter };
18
+ export { Blockquote, Break, BreakStyle, BulletChar, Code, CodeBlockStyle, Definition, Delete, DocumentSection, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, FrontmatterContent, FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormat, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterMissingReason, FrontmatterNewline, FrontmatterSource, FrontmatterSourceBlock, FrontmatterSourceSplit, FrontmatterValidationError, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, JsonFrontmatter, LineIndex, Link, LinkReference, List, ListContent, ListDelimiter, ListItem, Markdown, MarkdownDiagnostic, MarkdownDialect, MarkdownDocument, MarkdownEdit, MarkdownFormat, MarkdownFormattingOptions, MarkdownFrontmatter, MarkdownModificationError, MarkdownModificationErrorCode, MarkdownNode, MarkdownParseError, MarkdownParseErrorCode, MarkdownParseOptions, MarkdownRange, MarkdownStringifyError, MarkdownTransform, MarkdownVisitor, MarkdownVisitorEvent, Mdast, MdastDecodeError, MdxFlowExpression, MdxJsxAttribute, MdxJsxAttributeContent, MdxJsxAttributeValueExpression, MdxJsxExpressionAttribute, MdxJsxFlowElement, MdxJsxTextElement, MdxTextExpression, MdxjsEsm, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar, TomlFrontmatter, YamlFrontmatter };
@@ -1,12 +1,13 @@
1
- import { Frontmatter, Point, Position, Root } from "../MarkdownNode.js";
1
+ import { Frontmatter, Position, Root } from "../MarkdownNode.js";
2
2
  import { scanFrontmatter } from "./blocks/frontmatter.js";
3
3
  import { makeBlockNode } from "./blockTypes.js";
4
4
  import { columnsToNextTabStop, preprocessLines } from "./preprocess.js";
5
5
  import { isHtmlBlockEnd } from "./blocks/htmlBlock.js";
6
6
  import { blockDialect } from "./blockRegistry.js";
7
7
  import { GuardExceeded } from "./carriers.js";
8
- import { LineIndex } from "./lineIndex.js";
8
+ import { pointAt } from "./point.js";
9
9
  import { prepareInline } from "./rawInline.js";
10
+ import { LineIndex } from "@effected/source";
10
11
 
11
12
  // src/internal/blockParser.ts
12
13
  /** Upstream's cheap pre-filter: a line that cannot start any block. */
@@ -42,9 +43,7 @@ var BlockParser = class {
42
43
  constructor(text, dialect, dialectName, frontmatter = false) {
43
44
  this.lines = preprocessLines(text);
44
45
  this.frontmatterCapture = frontmatter ? scanFrontmatter(this.lines, text) : null;
45
- const starts = this.lines.map((line) => line.start);
46
- if (text.endsWith("\n")) starts.push(text.length);
47
- this.lineIndex = LineIndex.fromLineStarts(text, starts);
46
+ this.lineIndex = LineIndex.make(text);
48
47
  this.sourceLength = text.length;
49
48
  this.dialect = dialect;
50
49
  this.dialectName = dialectName;
@@ -306,19 +305,9 @@ var BlockParser = class {
306
305
  position(startOffset, endOffset) {
307
306
  const start = Math.min(Math.max(startOffset, 0), this.sourceLength);
308
307
  const end = Math.min(Math.max(endOffset, start), this.sourceLength);
309
- const startPoint = this.lineIndex.positionAt(start);
310
- const endPoint = this.lineIndex.positionAt(end);
311
308
  return Position.make({
312
- start: Point.make({
313
- line: startPoint.line,
314
- column: startPoint.column,
315
- offset: start
316
- }),
317
- end: Point.make({
318
- line: endPoint.line,
319
- column: endPoint.column,
320
- offset: end
321
- })
309
+ start: pointAt(this.lineIndex, start),
310
+ end: pointAt(this.lineIndex, end)
322
311
  });
323
312
  }
324
313
  /**
@@ -402,7 +391,8 @@ var BlockParser = class {
402
391
  rawInlines,
403
392
  carriers: [],
404
393
  refmap,
405
- footnoteLabels
394
+ footnoteLabels,
395
+ lineIndex: this.lineIndex
406
396
  };
407
397
  }
408
398
  };
@@ -0,0 +1,25 @@
1
+ // src/internal/fidelity.ts
2
+ /** Node type to the fidelity keys that node may carry. */
3
+ const FIDELITY_FIELDS = {
4
+ heading: ["headingStyle"],
5
+ emphasis: ["markerChar"],
6
+ strong: ["markerChar"],
7
+ thematicBreak: ["markerChar"],
8
+ code: ["fenceChar", "fenceLength"],
9
+ list: ["bulletChar", "delimiter"],
10
+ break: ["breakStyle"]
11
+ };
12
+ /**
13
+ * Return `node` without its fidelity keys; the same instance when it carries
14
+ * none, so structural sharing holds.
15
+ */
16
+ const clearFidelity = (node) => {
17
+ const keys = FIDELITY_FIELDS[node.type];
18
+ if (keys === void 0 || !keys.some((key) => key in node)) return node;
19
+ const rest = { ...node };
20
+ for (const key of keys) delete rest[key];
21
+ const ctor = node.constructor;
22
+ return new ctor(rest, { disableChecks: true });
23
+ };
24
+
25
+ export { FIDELITY_FIELDS, clearFidelity };
@@ -1,8 +1,9 @@
1
- import { Point, Position } from "../MarkdownNode.js";
1
+ import { Position } from "../MarkdownNode.js";
2
2
  import { preprocessLines } from "./preprocess.js";
3
- import { LineIndex } from "./lineIndex.js";
3
+ import { pointAt } from "./point.js";
4
4
  import { parseInlines } from "./inlineParser.js";
5
5
  import { trimWithSegments } from "./rawInline.js";
6
+ import { LineIndex } from "@effected/source";
6
7
 
7
8
  // src/internal/phrasing.ts
8
9
  const EMPTY_REFMAP = /* @__PURE__ */ new Map();
@@ -26,24 +27,14 @@ const parsePhrasingText = (text, dialect) => {
26
27
  if (trimmed.text.length === 0) return [];
27
28
  const first = trimmed.segments[0];
28
29
  const startOffset = first === void 0 ? 0 : first.sourceOffset;
29
- const lineIndex = LineIndex.fromLineStarts(text, lines.map((line) => line.start));
30
+ const lineIndex = LineIndex.make(text);
30
31
  const sourceLength = text.length;
31
32
  const positionOf = (start, end) => {
32
33
  const clampedStart = Math.min(Math.max(start, 0), sourceLength);
33
34
  const clampedEnd = Math.min(Math.max(end, clampedStart), sourceLength);
34
- const startPoint = lineIndex.positionAt(clampedStart);
35
- const endPoint = lineIndex.positionAt(clampedEnd);
36
35
  return Position.make({
37
- start: Point.make({
38
- line: startPoint.line,
39
- column: startPoint.column,
40
- offset: clampedStart
41
- }),
42
- end: Point.make({
43
- line: endPoint.line,
44
- column: endPoint.column,
45
- offset: clampedEnd
46
- })
36
+ start: pointAt(lineIndex, clampedStart),
37
+ end: pointAt(lineIndex, clampedEnd)
47
38
  });
48
39
  };
49
40
  return parseInlines({
@@ -0,0 +1,32 @@
1
+ import { Point } from "../MarkdownNode.js";
2
+
3
+ // src/internal/point.ts
4
+ /**
5
+ * The unist `Point` of `offset` within `index`'s text: one-based `line` and
6
+ * `column`, plus the offset itself.
7
+ *
8
+ * @remarks
9
+ * The caller clamps `offset` into `[0, text.length]` first, so `offset` is
10
+ * always the point's own offset.
11
+ *
12
+ * The column is `offset - lineStart(line) + 1`, NOT `positionAt(offset)`
13
+ * plus one. The two differ at exactly one offset class, the `\n` of a
14
+ * `\r\n`: `positionAt` reads it as the end of its line (LSP cannot name the
15
+ * middle of a terminator), while a unist Point counts it as one column past
16
+ * the `\r`, as `vfile-location` and `mdast-util-from-markdown` do. Node
17
+ * positions DO land there: an empty list item before a CRLF ends at its `\n`
18
+ * (`"*\r\na"` ends the item at offset 2, line 1, column 3), so the
19
+ * `positionAt` form would change published positions.
20
+ *
21
+ * @internal
22
+ */
23
+ const pointAt = (index, offset) => {
24
+ const line = index.lineAt(offset);
25
+ return Point.make({
26
+ line: line + 1,
27
+ column: offset - index.lineStart(line) + 1,
28
+ offset
29
+ });
30
+ };
31
+
32
+ export { pointAt };
@@ -0,0 +1,19 @@
1
+ import { GuardExceeded } from "./carriers.js";
2
+
3
+ // src/internal/treeDepth.ts
4
+ /**
5
+ * Throws {@link GuardExceeded} when `node` nests deeper than `MAX_NESTING_DEPTH`;
6
+ * returns otherwise. Never recurses, so it is safe on a tree of any depth.
7
+ *
8
+ * @internal
9
+ */
10
+ const assertWithinNestingDepth = (node) => {
11
+ const stack = [[node, 0]];
12
+ while (stack.length > 0) {
13
+ const [current, depth] = stack.pop();
14
+ if (current.type !== "root" && depth > 256) throw new GuardExceeded("NestingDepthExceeded", 256, depth, current.position.start.offset);
15
+ if ("children" in current) for (const child of current.children) stack.push([child, depth + 1]);
16
+ }
17
+ };
18
+
19
+ export { assertWithinNestingDepth };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/markdown",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "private": false,
5
5
  "description": "CommonMark and GFM markdown parse, edit and transform schemas for Effect",
6
6
  "keywords": [
@@ -38,10 +38,13 @@
38
38
  },
39
39
  "./package.json": "./package.json"
40
40
  },
41
+ "dependencies": {
42
+ "@effected/source": "^0.1.0"
43
+ },
41
44
  "peerDependencies": {
42
- "@effected/jsonc": "^0.20.0",
43
- "@effected/toml": "^0.16.0",
44
- "@effected/yaml": "^0.25.0",
45
+ "@effected/jsonc": "^0.21.0",
46
+ "@effected/toml": "^0.17.0",
47
+ "@effected/yaml": "^0.26.0",
45
48
  "effect": "^4.0.0"
46
49
  },
47
50
  "peerDependenciesMeta": {
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.59.1"
8
+ "packageVersion": "7.59.4"
9
9
  }
10
10
  ]
11
11
  }
@@ -1,75 +0,0 @@
1
- // src/internal/lineIndex.ts
2
- /**
3
- * A precomputed index of line-start offsets over a fixed source text,
4
- * answering `positionAt(offset)` in `O(log n)` via binary search rather than
5
- * rescanning the text per query.
6
- *
7
- * @remarks
8
- * Only `\n` begins a new line. A `\r` immediately before a `\n` (CRLF) is
9
- * left as the trailing character of the prior line — the pair still reads as
10
- * a single boundary because the following `\n` is what advances the line
11
- * counter, not the `\r`. A bare `\r` (old Mac line endings) is not
12
- * recognized as a break; CommonMark's preprocessing pass normalizes those
13
- * before this index is ever built.
14
- */
15
- var LineIndex = class LineIndex {
16
- text;
17
- lineStarts;
18
- constructor(text, lineStarts) {
19
- this.text = text;
20
- this.lineStarts = lineStarts;
21
- }
22
- /**
23
- * Build a {@link LineIndex} over `text` with one forward scan.
24
- *
25
- * @remarks
26
- * Recognizes only `\n`, which is why the block pass hands over its own
27
- * line table instead — see {@link LineIndex.fromLineStarts}.
28
- */
29
- static make(text) {
30
- const lineStarts = [0];
31
- for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) lineStarts.push(i + 1);
32
- return new LineIndex(text, lineStarts);
33
- }
34
- /**
35
- * Build a {@link LineIndex} over `text` from a line table someone else
36
- * already computed.
37
- *
38
- * @remarks
39
- * The parser splits lines on `\r\n`, `\n` AND a bare `\r`; this index's
40
- * own scan recognizes only `\n`. For any document without a bare `\r` the
41
- * two agree, but for one with them they disagree about what a line is, and
42
- * every reported line number would be wrong. Handing the parser's own
43
- * table over removes the possibility rather than documenting it: there is
44
- * one definition of a line, and it belongs to the preprocessor.
45
- *
46
- * `starts` must be ascending and begin at 0; a table that is neither is a
47
- * wiring bug and dies as a defect.
48
- */
49
- static fromLineStarts(text, starts) {
50
- if (starts.length === 0 || starts[0] !== 0) throw new TypeError("line index: a line table must be non-empty and start at offset 0");
51
- return new LineIndex(text, starts);
52
- }
53
- /**
54
- * The 1-based `{ line, column }` of `offset` within this index's text.
55
- * Out-of-range offsets clamp to the nearest valid position (`0` or
56
- * `text.length`) rather than throwing.
57
- */
58
- positionAt(offset) {
59
- const clamped = Math.min(Math.max(offset, 0), this.text.length);
60
- let low = 0;
61
- let high = this.lineStarts.length - 1;
62
- while (low < high) {
63
- const mid = low + high + 1 >> 1;
64
- if ((this.lineStarts[mid] ?? 0) <= clamped) low = mid;
65
- else high = mid - 1;
66
- }
67
- const lineStart = this.lineStarts[low] ?? 0;
68
- return {
69
- line: low + 1,
70
- column: clamped - lineStart + 1
71
- };
72
- }
73
- };
74
-
75
- export { LineIndex };