@effected/markdown 0.22.0 → 0.23.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
@@ -4,8 +4,8 @@ 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
9
  import { Effect, Option, Result, Schema } from "effect";
10
10
 
11
11
  // src/MarkdownDocument.ts
@@ -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
@@ -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
@@ -1013,6 +1013,19 @@ export declare const MarkdownDialect: Schema.Literals<readonly ["commonmark", "g
1013
1013
  * @public
1014
1014
  */
1015
1015
  export type MarkdownDialect = typeof MarkdownDialect.Type;
1016
+ /**
1017
+ * Options for `Markdown.stringify` and `Markdown.stringifyResult`.
1018
+ *
1019
+ * @public
1020
+ */
1021
+ interface MarkdownStringifyOptions {
1022
+ /**
1023
+ * Ignore fidelity fields (marker characters, fence style, heading
1024
+ * spelling) and emit the canonical form. Defaults to `false`, which honours
1025
+ * them.
1026
+ */
1027
+ readonly canonical?: boolean;
1028
+ }
1016
1029
  declare const MarkdownParseOptions_base: Schema.Class<MarkdownParseOptions, Schema.Struct<{
1017
1030
  readonly dialect: Schema.optionalKey<Schema.Literals<readonly ["commonmark", "gfm"]>>;
1018
1031
  readonly frontmatter: Schema.optionalKey<Schema.Boolean>;
@@ -1225,8 +1238,8 @@ export declare class Markdown {
1225
1238
  * surgical editing goes through the offset-splice edit layer instead.
1226
1239
  *
1227
1240
  * **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
1241
+ * against these bytes.** The one option, `canonical`, only selects
1242
+ * between these bytes and the author's spelling; the choices below are pinned by byte-level tests, and
1230
1243
  * the engine is cross-checked against commonmark.js over the full
1231
1244
  * CommonMark 0.31.2 corpus. Changing any of them is a **breaking** change
1232
1245
  * to this package, never a patch. A consumer asserting on rendered
@@ -1295,11 +1308,20 @@ export declare class Markdown {
1295
1308
  * trip on a tree nesting past the depth cap fails, and only synthesized
1296
1309
  * or decoded trees can nest that far.
1297
1310
  *
1311
+ * **`canonical: true` ignores fidelity fields.** With
1312
+ * {@link MarkdownStringifyOptions.canonical} set, every fidelity field is
1313
+ * cleared first, so a parsed `* a` list, setext heading, `_em_`, `~~~` fence
1314
+ * or `***` break all emit the canonical spelling. It is the same as
1315
+ * serializing `MarkdownTransform.canonicalize(node)`, without the
1316
+ * `Mdast.toMdast` / `Mdast.fromMdastResult` round trip. The default keeps
1317
+ * honouring fidelity fields.
1318
+ *
1298
1319
  * @param node - The document tree, or a single flow node, to serialize.
1320
+ * @param options - Optional {@link MarkdownStringifyOptions}.
1299
1321
  * @returns A `Result` succeeding with markdown source, or failing with
1300
1322
  * {@link MarkdownStringifyError}.
1301
1323
  */
1302
- static stringifyResult(node: Root | FlowContent): Result.Result<string, MarkdownStringifyError>;
1324
+ static stringifyResult(node: Root | FlowContent, options?: MarkdownStringifyOptions): Result.Result<string, MarkdownStringifyError>;
1303
1325
  /**
1304
1326
  * Serialize a {@link Root} tree, or a single {@link (FlowContent:type)} node, to
1305
1327
  * canonical markdown. Defined in terms of {@link Markdown.stringifyResult}
@@ -1323,10 +1345,12 @@ export declare class Markdown {
1323
1345
  * see {@link Markdown.stringifyResult}.
1324
1346
  *
1325
1347
  * @param node - The document tree, or a single flow node, to serialize.
1348
+ * @param options - Optional {@link MarkdownStringifyOptions}; `canonical: true`
1349
+ * discards fidelity fields.
1326
1350
  * @returns An `Effect` that succeeds with markdown source, or fails with
1327
1351
  * {@link MarkdownStringifyError}.
1328
1352
  */
1329
- static readonly stringify: (node: Root | FlowContent) => Effect.Effect<string, MarkdownStringifyError, never>;
1353
+ static readonly stringify: (node: Root | FlowContent, options?: MarkdownStringifyOptions | undefined) => Effect.Effect<string, MarkdownStringifyError, never>;
1330
1354
  /**
1331
1355
  * A `Schema<Root, string>` decoding markdown source into a {@link Root}
1332
1356
  * tree and encoding a tree back to canonical markdown via
@@ -2873,6 +2897,30 @@ export declare class MarkdownTransform {
2873
2897
  * a heading depth or `min > max` — a wiring error, not a data error.
2874
2898
  */
2875
2899
  static shiftHeadings<N extends MarkdownNode>(node: N, by: number, options?: ShiftHeadingsOptions): N;
2900
+ /**
2901
+ * Return a copy of the tree with every fidelity field cleared, so
2902
+ * `Markdown.stringify` emits the canonical form instead of the author's
2903
+ * spelling.
2904
+ *
2905
+ * @remarks
2906
+ * Clears `headingStyle`, `markerChar` (emphasis, strong, thematic break),
2907
+ * `fenceChar` and `fenceLength`, `bulletChar` and `delimiter`, and
2908
+ * `breakStyle`. `Text`'s `escapeStyle` is an emitter instruction rather
2909
+ * than a recorded spelling and is kept. Positions and all mdast fields are
2910
+ * untouched. The input is never mutated; a subtree carrying no fidelity
2911
+ * field is returned as the same instance, following
2912
+ * {@link MarkdownTransform.(map:1)}, on which this is built. Like `map` it
2913
+ * recurses without a depth guard, so a tree nested past the stack throws a
2914
+ * `RangeError`; `Markdown.stringifyResult` with `canonical: true` checks
2915
+ * depth first and fails typed at the stringifier's own limit instead.
2916
+ *
2917
+ * `Markdown.stringify(tree, { canonical: true })` is exactly
2918
+ * `Markdown.stringify(MarkdownTransform.canonicalize(tree))`.
2919
+ *
2920
+ * @param node - The node whose fidelity fields to clear.
2921
+ * @returns The node with fidelity fields cleared.
2922
+ */
2923
+ static canonicalize<N extends MarkdownNode>(node: N): N;
2876
2924
  /**
2877
2925
  * Locate the root-level sections of one heading depth by child index —
2878
2926
  * no source text needed, so it works on a tree a
@@ -3395,5 +3443,5 @@ export declare const TomlFrontmatter: FrontmatterCodec;
3395
3443
  * @public
3396
3444
  */
3397
3445
  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 };
3446
+ export type { DocumentHeading, DocumentLink, FrontmatterCodec, FrontmatterResolveError, FrontmatterSchemaError, FrontmatterSchemaResolver, FrontmatterWriteError, LinkBearingNode, MarkdownNodeOfType, MarkdownNodeType, MarkdownPath, MarkdownPhrasingOnlyType, MarkdownRangeLike, MarkdownSegment, MarkdownStringifyOptions, MarkdownTransformHandlers, MarkdownTransformResult, MdastNode, MdastToTextOptions, SectionHeadingMatch, SectionQueryOptions, SectionSpan, SectionSpanOptions, ShiftHeadingsOptions };
3399
3447
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -5,10 +5,10 @@ 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";
@@ -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 };
@@ -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.23.0",
4
4
  "private": false,
5
5
  "description": "CommonMark and GFM markdown parse, edit and transform schemas for Effect",
6
6
  "keywords": [
@@ -41,7 +41,7 @@
41
41
  "peerDependencies": {
42
42
  "@effected/jsonc": "^0.20.0",
43
43
  "@effected/toml": "^0.16.0",
44
- "@effected/yaml": "^0.25.0",
44
+ "@effected/yaml": "^0.25.1",
45
45
  "effect": "^4.0.0"
46
46
  },
47
47
  "peerDependenciesMeta": {