markdown-codec 6.2.0 → 6.3.1

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.
Files changed (42) hide show
  1. package/README.md +7 -4
  2. package/dist/block/block.cjs +11 -3
  3. package/dist/block/block.d.cts +6 -2
  4. package/dist/block/block.d.ts +6 -2
  5. package/dist/block/block.js +7 -3
  6. package/dist/block/definitions.d.cts +1 -1
  7. package/dist/block/definitions.d.ts +1 -1
  8. package/dist/diagnostics/diagnostics.cjs +3 -0
  9. package/dist/diagnostics/diagnostics.d.cts +1 -1
  10. package/dist/diagnostics/diagnostics.d.ts +1 -1
  11. package/dist/diagnostics/diagnostics.js +3 -0
  12. package/dist/{diagnostics-CH-xa2-s.d.cts → diagnostics-CnamCy6I.d.cts} +3 -0
  13. package/dist/{diagnostics-CH-xa2-s.d.ts → diagnostics-CnamCy6I.d.ts} +3 -0
  14. package/dist/emit/emit.cjs +109 -5
  15. package/dist/emit/emit.js +109 -5
  16. package/dist/emit/inline.cjs +4 -3
  17. package/dist/emit/inline.d.cts +1 -1
  18. package/dist/emit/inline.d.ts +1 -1
  19. package/dist/emit/inline.js +4 -3
  20. package/dist/emit/table.d.cts +1 -1
  21. package/dist/emit/table.d.ts +1 -1
  22. package/dist/index.d.cts +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/lower/front-matter.cjs +2 -2
  25. package/dist/lower/front-matter.d.cts +1 -1
  26. package/dist/lower/front-matter.d.ts +1 -1
  27. package/dist/lower/front-matter.js +1 -1
  28. package/dist/lower/inline.cjs +7 -1
  29. package/dist/lower/inline.d.cts +1 -1
  30. package/dist/lower/inline.d.ts +1 -1
  31. package/dist/lower/inline.js +7 -1
  32. package/dist/lower/lower.cjs +1 -0
  33. package/dist/lower/lower.js +1 -0
  34. package/dist/options/options.d.cts +1 -1
  35. package/dist/options/options.d.ts +1 -1
  36. package/dist/read.d.cts +1 -1
  37. package/dist/read.d.ts +1 -1
  38. package/dist/shared/line-ending.cjs +7 -0
  39. package/dist/shared/line-ending.d.cts +5 -0
  40. package/dist/shared/line-ending.d.ts +5 -0
  41. package/dist/shared/line-ending.js +5 -0
  42. package/package.json +2 -2
package/README.md CHANGED
@@ -197,6 +197,9 @@ Every construct `src/lower`/`src/emit` cannot represent losslessly is a document
197
197
  - **`md/raw-html-preserved-as-text` / `md/raw-html-dropped`** — raw HTML kept as literal text (default) or dropped; never interpreted. The preserved text's verbatim original quarantines as markdown residue on its node, and this package's own writer re-emits that residue as-is.
198
198
  - **`md/front-matter-key-unmapped`** — no YAML/TOML engine; only five known `LayoutMetadata` keys recognised. The verbatim original block rides the package-level residue table (`readMarkdown`'s `documentPackage.source.frontmatter`), which `writeMarkdown` re-emits as-is.
199
199
  - **`md/heading-level-clamped`** — styleId beyond `Heading6` (from another format) clamps to level 6 via document-schema.js's shared `clampHeadingLevel()`.
200
+ - **`md/heading-line-break-collapsed`** — a hard or soft break embedded in a heading's own runs (most commonly a setext heading read with more than one physical line) is promoted to setext output whenever the level admits one (1 or 2) and the break's own placement is safe to promote, the only markdown grammar that can hold a genuine line break inside heading text; a level 3-6 heading with the same shape has no such fallback and always collapses the break to a single space instead, as does a level 1/2 heading whose break placement is unsafe to promote (see `md/heading-line-break-unsafe-for-setext` below).
201
+ - **`md/heading-line-break-unsafe-for-setext`** — a level 1/2 heading's own embedded break sits where promoting to setext would violate one of the three clauses packed into setext's own grammar sentence (spec 0.31.2, "Setext headings"): "one or more lines of text, not interrupted by a blank line, of which the first line does not have more than 3 spaces of indentation, followed by a setext heading underline. The lines of text must be such that, were they not followed by the setext heading underline, they would be interpreted as a paragraph: they cannot be interpretable as a code fence, ATX heading, block quote, thematic break, list item, or HTML block." Reported when promoting would leave a blank line immediately before the underline (a trailing break) or in the middle of the heading's own text (an interior break between two embedded breaks, or a break immediately followed by incidental whitespace-only content); when the heading's own first content line opens with 4 or more columns of space/tab indentation — CommonMark's own tab-stop rule counts a tab by the same 4-column stop, so a single leading tab is exactly as unsafe as 4 leading spaces — since that line would read back as an indented code block rather than heading text; or when a line AFTER the first would itself be read as one of the spec's six interrupting constructs (a code fence, ATX heading, block quote, thematic break, list item, or HTML block) instead of ordinary paragraph continuation text — reachable from a bold or strikethrough run that becomes empty immediately after a break, whose bare pair of emphasis markers spells as a thematic break (`____`/`****`) or a code-fence opener (`~~~~`). ATX collapses the break to a single space instead of promoting into any of these corrupt reparses. A break at the very START of the heading's own text stays eligible for setext, but that exemption covers only a single leading blank line, and only the blank line itself: it sits before the text-and-underline run even begins, so it reparses as ordinary inter-block whitespace ahead of the heading rather than corrupting anything. A second consecutive leading blank line is not exempt — CommonMark's own list-item grammar allows at most one, so a second closes a list item as empty and spills the heading out with its list membership lost, even though it would be harmless at top level or inside a blockquote — and the indentation clause above still applies in full to the line that follows the leading blank line, the heading's own first real content: a leading break immediately followed by 4 or more columns of indentation is refused exactly as an unbroken heading in the same shape would be, since promoting would read that following line back as an indented code block regardless of the leading break ahead of it.
202
+ - **`md/heading-style-overridden-for-line-break`** — the setext promotion above overriding the effective `headingStyle: 'atx'` (this option's own default just as much as an explicit caller choice); that one heading renders as setext regardless because ATX cannot hold the embedded break at all.
200
203
  - **`md/adjacent-links-merged`** / **`md/code-span-as-monospace-run`** — same-destination adjacent links merge; monospace runs emit as code spans.
201
204
  - **`md/paragraph-indent-dropped`** — `indentLeftPt` without a recognised styleId; indent dropped, paragraph renders.
202
205
  - **`md/list-numid-fallback`** — a foreign or absent `numId` (depth-only `ContentListMembership`) falls back to a plain bullet list.
@@ -228,11 +231,11 @@ Emission is the inverse and validates first: a section's markers must pair as ba
228
231
 
229
232
  | Corpus | Examples | Passing round trip | Rate |
230
233
  | ------------------------------------------------------------------------------------- | -------- | ------------------ | ----- |
231
- | CommonMark 0.31.2 (`assets/commonmark/spec.json`) | 652 | 516 | 79.1% |
232
- | GFM tagged extensions (table/strikethrough/autolink/task-list, `assets/gfm/spec.txt`) | 23 | 22 | 95.7% |
233
- | Combined | 675 | 538 | 79.7% |
234
+ | CommonMark 0.31.2 (`assets/commonmark/spec.json`) | 652 | 575 | 88.2% |
235
+ | GFM tagged extensions (table/strikethrough/autolink/task-list, `assets/gfm/spec.txt`) | 23 | 23 | 100% |
236
+ | Combined | 675 | 598 | 88.6% |
234
237
 
235
- Every non-passing example is named individually in `src/test-support/conformance-exclusions.ts`, attributed to a closed set of causes (shrink-only — see [Conventions](#conventions)): most commonly a soft line break collapsing to a space, a nested/unresolved image title, an emphasis-span collision, or a blockquote whose heading content skips its container pair.
238
+ Every non-passing example is named individually in `src/test-support/conformance-exclusions.ts`, attributed to a closed set of causes (shrink-only — see [Conventions](#conventions)): most commonly an emphasis-span collision, an image with no `data:` URI source for the harness to embed, or a blockquote whose heading content skips its container pair.
236
239
 
237
240
  **Optional real-world corpus.** `test/corpus/` (gitignored) holds a `pnpm test:corpus` project for a manual sanity check against sibling READMEs on disk — asserts no throw and real content on reparse, not byte fidelity. Not part of `pnpm test`; run locally before significant parser/lower/emit changes.
238
241
 
@@ -5,6 +5,7 @@ const require_diagnostics_diagnostics = require("../diagnostics/diagnostics.cjs"
5
5
  const require_html_html = require("../html/html.cjs");
6
6
  const require_inline_entity = require("../inline/entity.cjs");
7
7
  const require_inline_inline = require("../inline/inline.cjs");
8
+ const require_shared_line_ending = require("../shared/line-ending.cjs");
8
9
  const require_block_definitions = require("./definitions.cjs");
9
10
  const require_block_line = require("./line.cjs");
10
11
  const require_block_list = require("./list.cjs");
@@ -14,7 +15,6 @@ const require_block_table = require("./table.cjs");
14
15
  const TASK_LIST_MARKER_PATTERN = /^\[([ xX])\][ \t]/;
15
16
  const NUL_REPLACEMENT = "�";
16
17
  const NUL_PATTERN = /\0/g;
17
- const LINE_ENDING_PATTERN = /\r\n|\n|\r/;
18
18
  const MAYBE_SPECIAL_PATTERN = /^[#$`~*+_=<>[0-9|:-]/;
19
19
  const ATX_MARKER_PATTERN = /^#{1,6}(?:[ \t]+|$)/;
20
20
  const ATX_ONLY_CLOSING_SEQUENCE_PATTERN = /^[ \t]*#+[ \t]*$/;
@@ -67,7 +67,7 @@ var BlockParser = class {
67
67
  this.maxNesting = options.maxNesting ?? 250;
68
68
  }
69
69
  parse(source) {
70
- const lines = source.split(LINE_ENDING_PATTERN);
70
+ const lines = source.split(require_shared_line_ending.LINE_ENDING_PATTERN);
71
71
  const count = source.endsWith("\n") || source.endsWith("\r") ? lines.length - 1 : lines.length;
72
72
  for (let index = 0; index < count; index += 1) {
73
73
  const text = lines[index];
@@ -470,8 +470,12 @@ var BlockParser = class {
470
470
  node.literal = breakIndex === -1 ? "" : node.content.slice(breakIndex + 1);
471
471
  }
472
472
  };
473
+ const BLOCK_EDGE_SPACE_OR_TAB_PATTERN = /^[ \t]+|[ \t]+$/g;
474
+ function trimBlockContent(text) {
475
+ return (text.endsWith("\n") ? text.slice(0, -1) : text).replace(BLOCK_EDGE_SPACE_OR_TAB_PATTERN, "");
476
+ }
473
477
  function toInlineChildren(content, context) {
474
- return require_inline_inline.parseInlines(content.trim(), context.references, context.footnotes, context.options);
478
+ return require_inline_inline.parseInlines(trimBlockContent(content), context.references, context.footnotes, context.options);
475
479
  }
476
480
  function toHeadingNode(node, context) {
477
481
  return {
@@ -618,4 +622,8 @@ function parseMarkdown(source, options = {}) {
618
622
  };
619
623
  }
620
624
  //#endregion
625
+ exports.ATX_MARKER_PATTERN = ATX_MARKER_PATTERN;
626
+ exports.CODE_FENCE_PATTERN = CODE_FENCE_PATTERN;
627
+ exports.MATH_BLOCK_MARKER_PATTERN = MATH_BLOCK_MARKER_PATTERN;
628
+ exports.THEMATIC_BREAK_PATTERN = THEMATIC_BREAK_PATTERN;
621
629
  exports.parseMarkdown = parseMarkdown;
@@ -1,9 +1,13 @@
1
1
  import { s as MarkdownDocumentNode } from "../ast-CNfTgS0N.cjs";
2
2
  import { r as FootnoteLabelSet } from "../footnote-CKk4JbLk.cjs";
3
- import { i as MarkdownDiagnosticSink } from "../diagnostics-CH-xa2-s.cjs";
3
+ import { i as MarkdownDiagnosticSink } from "../diagnostics-CnamCy6I.cjs";
4
4
  import { LinkReferenceMap } from "../inline/link.cjs";
5
5
  import { t as InlineParseOptions } from "../inline-CnO86zMw.cjs";
6
6
  //#region src/block/block.d.ts
7
+ declare const ATX_MARKER_PATTERN: RegExp;
8
+ declare const CODE_FENCE_PATTERN: RegExp;
9
+ declare const MATH_BLOCK_MARKER_PATTERN: RegExp;
10
+ declare const THEMATIC_BREAK_PATTERN: RegExp;
7
11
  interface MarkdownParseOptions extends InlineParseOptions {
8
12
  readonly gfmTables?: boolean;
9
13
  readonly gfmTaskLists?: boolean;
@@ -18,4 +22,4 @@ interface ParsedMarkdown {
18
22
  }
19
23
  declare function parseMarkdown(source: string, options?: MarkdownParseOptions): ParsedMarkdown;
20
24
  //#endregion
21
- export { MarkdownParseOptions, ParsedMarkdown, parseMarkdown };
25
+ export { ATX_MARKER_PATTERN, CODE_FENCE_PATTERN, MATH_BLOCK_MARKER_PATTERN, MarkdownParseOptions, ParsedMarkdown, THEMATIC_BREAK_PATTERN, parseMarkdown };
@@ -1,9 +1,13 @@
1
1
  import { s as MarkdownDocumentNode } from "../ast-CNfTgS0N.js";
2
2
  import { r as FootnoteLabelSet } from "../footnote-CKk4JbLk.js";
3
- import { i as MarkdownDiagnosticSink } from "../diagnostics-CH-xa2-s.js";
3
+ import { i as MarkdownDiagnosticSink } from "../diagnostics-CnamCy6I.js";
4
4
  import { LinkReferenceMap } from "../inline/link.js";
5
5
  import { t as InlineParseOptions } from "../inline-DItZ-VVw.js";
6
6
  //#region src/block/block.d.ts
7
+ declare const ATX_MARKER_PATTERN: RegExp;
8
+ declare const CODE_FENCE_PATTERN: RegExp;
9
+ declare const MATH_BLOCK_MARKER_PATTERN: RegExp;
10
+ declare const THEMATIC_BREAK_PATTERN: RegExp;
7
11
  interface MarkdownParseOptions extends InlineParseOptions {
8
12
  readonly gfmTables?: boolean;
9
13
  readonly gfmTaskLists?: boolean;
@@ -18,4 +22,4 @@ interface ParsedMarkdown {
18
22
  }
19
23
  declare function parseMarkdown(source: string, options?: MarkdownParseOptions): ParsedMarkdown;
20
24
  //#endregion
21
- export { MarkdownParseOptions, ParsedMarkdown, parseMarkdown };
25
+ export { ATX_MARKER_PATTERN, CODE_FENCE_PATTERN, MATH_BLOCK_MARKER_PATTERN, MarkdownParseOptions, ParsedMarkdown, THEMATIC_BREAK_PATTERN, parseMarkdown };
@@ -4,6 +4,7 @@ import { MarkdownDiagnosticCodes, MarkdownNestingLimitExceededError, NOOP_MARKDO
4
4
  import { matchHtmlBlockStart, matchesHtmlBlockEnd } from "../html/html.js";
5
5
  import { unescapeString } from "../inline/entity.js";
6
6
  import { parseInlines } from "../inline/inline.js";
7
+ import { LINE_ENDING_PATTERN } from "../shared/line-ending.js";
7
8
  import { extractDefinitions } from "./definitions.js";
8
9
  import { LineCursor } from "./line.js";
9
10
  import { finalizeListTightness, listsMatch, parseListMarker } from "./list.js";
@@ -13,7 +14,6 @@ import { fitRowToColumns, parseTableDelimiterRow, splitTableRow } from "./table.
13
14
  const TASK_LIST_MARKER_PATTERN = /^\[([ xX])\][ \t]/;
14
15
  const NUL_REPLACEMENT = "�";
15
16
  const NUL_PATTERN = /\0/g;
16
- const LINE_ENDING_PATTERN = /\r\n|\n|\r/;
17
17
  const MAYBE_SPECIAL_PATTERN = /^[#$`~*+_=<>[0-9|:-]/;
18
18
  const ATX_MARKER_PATTERN = /^#{1,6}(?:[ \t]+|$)/;
19
19
  const ATX_ONLY_CLOSING_SEQUENCE_PATTERN = /^[ \t]*#+[ \t]*$/;
@@ -469,8 +469,12 @@ var BlockParser = class {
469
469
  node.literal = breakIndex === -1 ? "" : node.content.slice(breakIndex + 1);
470
470
  }
471
471
  };
472
+ const BLOCK_EDGE_SPACE_OR_TAB_PATTERN = /^[ \t]+|[ \t]+$/g;
473
+ function trimBlockContent(text) {
474
+ return (text.endsWith("\n") ? text.slice(0, -1) : text).replace(BLOCK_EDGE_SPACE_OR_TAB_PATTERN, "");
475
+ }
472
476
  function toInlineChildren(content, context) {
473
- return parseInlines(content.trim(), context.references, context.footnotes, context.options);
477
+ return parseInlines(trimBlockContent(content), context.references, context.footnotes, context.options);
474
478
  }
475
479
  function toHeadingNode(node, context) {
476
480
  return {
@@ -617,4 +621,4 @@ function parseMarkdown(source, options = {}) {
617
621
  };
618
622
  }
619
623
  //#endregion
620
- export { parseMarkdown };
624
+ export { ATX_MARKER_PATTERN, CODE_FENCE_PATTERN, MATH_BLOCK_MARKER_PATTERN, THEMATIC_BREAK_PATTERN, parseMarkdown };
@@ -1,4 +1,4 @@
1
- import { i as MarkdownDiagnosticSink } from "../diagnostics-CH-xa2-s.cjs";
1
+ import { i as MarkdownDiagnosticSink } from "../diagnostics-CnamCy6I.cjs";
2
2
  import { LinkReferenceDefinition } from "../inline/link.cjs";
3
3
  //#region src/block/definitions.d.ts
4
4
  declare function extractDefinitions(content: string, references: Map<string, LinkReferenceDefinition>, sink?: MarkdownDiagnosticSink, startLine?: number): string;
@@ -1,4 +1,4 @@
1
- import { i as MarkdownDiagnosticSink } from "../diagnostics-CH-xa2-s.js";
1
+ import { i as MarkdownDiagnosticSink } from "../diagnostics-CnamCy6I.js";
2
2
  import { LinkReferenceDefinition } from "../inline/link.js";
3
3
  //#region src/block/definitions.d.ts
4
4
  declare function extractDefinitions(content: string, references: Map<string, LinkReferenceDefinition>, sink?: MarkdownDiagnosticSink, startLine?: number): string;
@@ -26,6 +26,9 @@ const MarkdownDiagnosticCodes = {
26
26
  ADJACENT_LINKS_MERGED: "md/adjacent-links-merged",
27
27
  CODE_SPAN_AS_MONOSPACE_RUN: "md/code-span-as-monospace-run",
28
28
  PARAGRAPH_INDENT_DROPPED: "md/paragraph-indent-dropped",
29
+ HEADING_LINE_BREAK_COLLAPSED: "md/heading-line-break-collapsed",
30
+ HEADING_LINE_BREAK_UNSAFE_FOR_SETEXT: "md/heading-line-break-unsafe-for-setext",
31
+ HEADING_STYLE_OVERRIDDEN_FOR_LINE_BREAK: "md/heading-style-overridden-for-line-break",
29
32
  LIST_NUMID_FALLBACK: "md/list-numid-fallback",
30
33
  TABLE_CELL_FORMATTING_DROPPED: "md/table-cell-formatting-dropped",
31
34
  TABLE_CELL_MULTI_PARAGRAPH_JOINED: "md/table-cell-multi-paragraph-joined"
@@ -1,2 +1,2 @@
1
- import { a as MarkdownInputTooLargeError, c as MarkdownNestingLimitExceededError, d as MarkdownUnbalancedConstructMarkersError, f as MarkdownUnsupportedDocumentKindError, i as MarkdownDiagnosticSink, l as MarkdownPackageFlattenError, m as NOOP_MARKDOWN_DIAGNOSTIC_SINK, n as MarkdownDiagnosticCodes, o as MarkdownInvalidRunConstructExtentError, p as MarkdownWriteError, r as MarkdownDiagnosticSeverity, s as MarkdownInvalidUtf8Error, t as MarkdownDiagnostic, u as MarkdownParseError } from "../diagnostics-CH-xa2-s.cjs";
1
+ import { a as MarkdownInputTooLargeError, c as MarkdownNestingLimitExceededError, d as MarkdownUnbalancedConstructMarkersError, f as MarkdownUnsupportedDocumentKindError, i as MarkdownDiagnosticSink, l as MarkdownPackageFlattenError, m as NOOP_MARKDOWN_DIAGNOSTIC_SINK, n as MarkdownDiagnosticCodes, o as MarkdownInvalidRunConstructExtentError, p as MarkdownWriteError, r as MarkdownDiagnosticSeverity, s as MarkdownInvalidUtf8Error, t as MarkdownDiagnostic, u as MarkdownParseError } from "../diagnostics-CnamCy6I.cjs";
2
2
  export { MarkdownDiagnostic, MarkdownDiagnosticCodes, MarkdownDiagnosticSeverity, MarkdownDiagnosticSink, MarkdownInputTooLargeError, MarkdownInvalidRunConstructExtentError, MarkdownInvalidUtf8Error, MarkdownNestingLimitExceededError, MarkdownPackageFlattenError, MarkdownParseError, MarkdownUnbalancedConstructMarkersError, MarkdownUnsupportedDocumentKindError, MarkdownWriteError, NOOP_MARKDOWN_DIAGNOSTIC_SINK };
@@ -1,2 +1,2 @@
1
- import { a as MarkdownInputTooLargeError, c as MarkdownNestingLimitExceededError, d as MarkdownUnbalancedConstructMarkersError, f as MarkdownUnsupportedDocumentKindError, i as MarkdownDiagnosticSink, l as MarkdownPackageFlattenError, m as NOOP_MARKDOWN_DIAGNOSTIC_SINK, n as MarkdownDiagnosticCodes, o as MarkdownInvalidRunConstructExtentError, p as MarkdownWriteError, r as MarkdownDiagnosticSeverity, s as MarkdownInvalidUtf8Error, t as MarkdownDiagnostic, u as MarkdownParseError } from "../diagnostics-CH-xa2-s.js";
1
+ import { a as MarkdownInputTooLargeError, c as MarkdownNestingLimitExceededError, d as MarkdownUnbalancedConstructMarkersError, f as MarkdownUnsupportedDocumentKindError, i as MarkdownDiagnosticSink, l as MarkdownPackageFlattenError, m as NOOP_MARKDOWN_DIAGNOSTIC_SINK, n as MarkdownDiagnosticCodes, o as MarkdownInvalidRunConstructExtentError, p as MarkdownWriteError, r as MarkdownDiagnosticSeverity, s as MarkdownInvalidUtf8Error, t as MarkdownDiagnostic, u as MarkdownParseError } from "../diagnostics-CnamCy6I.js";
2
2
  export { MarkdownDiagnostic, MarkdownDiagnosticCodes, MarkdownDiagnosticSeverity, MarkdownDiagnosticSink, MarkdownInputTooLargeError, MarkdownInvalidRunConstructExtentError, MarkdownInvalidUtf8Error, MarkdownNestingLimitExceededError, MarkdownPackageFlattenError, MarkdownParseError, MarkdownUnbalancedConstructMarkersError, MarkdownUnsupportedDocumentKindError, MarkdownWriteError, NOOP_MARKDOWN_DIAGNOSTIC_SINK };
@@ -25,6 +25,9 @@ const MarkdownDiagnosticCodes = {
25
25
  ADJACENT_LINKS_MERGED: "md/adjacent-links-merged",
26
26
  CODE_SPAN_AS_MONOSPACE_RUN: "md/code-span-as-monospace-run",
27
27
  PARAGRAPH_INDENT_DROPPED: "md/paragraph-indent-dropped",
28
+ HEADING_LINE_BREAK_COLLAPSED: "md/heading-line-break-collapsed",
29
+ HEADING_LINE_BREAK_UNSAFE_FOR_SETEXT: "md/heading-line-break-unsafe-for-setext",
30
+ HEADING_STYLE_OVERRIDDEN_FOR_LINE_BREAK: "md/heading-style-overridden-for-line-break",
28
31
  LIST_NUMID_FALLBACK: "md/list-numid-fallback",
29
32
  TABLE_CELL_FORMATTING_DROPPED: "md/table-cell-formatting-dropped",
30
33
  TABLE_CELL_MULTI_PARAGRAPH_JOINED: "md/table-cell-multi-paragraph-joined"
@@ -33,6 +33,9 @@ declare const MarkdownDiagnosticCodes: {
33
33
  readonly ADJACENT_LINKS_MERGED: "md/adjacent-links-merged";
34
34
  readonly CODE_SPAN_AS_MONOSPACE_RUN: "md/code-span-as-monospace-run";
35
35
  readonly PARAGRAPH_INDENT_DROPPED: "md/paragraph-indent-dropped";
36
+ readonly HEADING_LINE_BREAK_COLLAPSED: "md/heading-line-break-collapsed";
37
+ readonly HEADING_LINE_BREAK_UNSAFE_FOR_SETEXT: "md/heading-line-break-unsafe-for-setext";
38
+ readonly HEADING_STYLE_OVERRIDDEN_FOR_LINE_BREAK: "md/heading-style-overridden-for-line-break";
36
39
  readonly LIST_NUMID_FALLBACK: "md/list-numid-fallback";
37
40
  readonly TABLE_CELL_FORMATTING_DROPPED: "md/table-cell-formatting-dropped";
38
41
  readonly TABLE_CELL_MULTI_PARAGRAPH_JOINED: "md/table-cell-multi-paragraph-joined";
@@ -33,6 +33,9 @@ declare const MarkdownDiagnosticCodes: {
33
33
  readonly ADJACENT_LINKS_MERGED: "md/adjacent-links-merged";
34
34
  readonly CODE_SPAN_AS_MONOSPACE_RUN: "md/code-span-as-monospace-run";
35
35
  readonly PARAGRAPH_INDENT_DROPPED: "md/paragraph-indent-dropped";
36
+ readonly HEADING_LINE_BREAK_COLLAPSED: "md/heading-line-break-collapsed";
37
+ readonly HEADING_LINE_BREAK_UNSAFE_FOR_SETEXT: "md/heading-line-break-unsafe-for-setext";
38
+ readonly HEADING_STYLE_OVERRIDDEN_FOR_LINE_BREAK: "md/heading-style-overridden-for-line-break";
36
39
  readonly LIST_NUMID_FALLBACK: "md/list-numid-fallback";
37
40
  readonly TABLE_CELL_FORMATTING_DROPPED: "md/table-cell-formatting-dropped";
38
41
  readonly TABLE_CELL_MULTI_PARAGRAPH_JOINED: "md/table-cell-multi-paragraph-joined";
@@ -2,6 +2,13 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_inline_footnote = require("../inline/footnote.cjs");
3
3
  require("../defaults/defaults.cjs");
4
4
  const require_diagnostics_diagnostics = require("../diagnostics/diagnostics.cjs");
5
+ const require_html_html = require("../html/html.cjs");
6
+ const require_shared_line_ending = require("../shared/line-ending.cjs");
7
+ require("../scan/scan.cjs");
8
+ const require_block_line = require("../block/line.cjs");
9
+ const require_block_list = require("../block/list.cjs");
10
+ const require_block_table = require("../block/table.cjs");
11
+ const require_block_block = require("../block/block.cjs");
5
12
  const require_shared_list_id = require("../shared/list-id.cjs");
6
13
  const require_shared_style_constants = require("../shared/style-constants.cjs");
7
14
  const require_emit_front_matter = require("./front-matter.cjs");
@@ -19,7 +26,7 @@ const SETEXT_LEVEL_2_CHAR = "-";
19
26
  const MIN_SETEXT_UNDERLINE_LENGTH = 1;
20
27
  function renderSetextHeading(level, text) {
21
28
  const underlineChar = level === 1 ? SETEXT_LEVEL_1_CHAR : SETEXT_LEVEL_2_CHAR;
22
- const firstLine = text.split("\n")[0] ?? "";
29
+ const firstLine = text.split(require_shared_line_ending.LINE_ENDING_PATTERN)[0] ?? "";
23
30
  return `${text}\n${underlineChar.repeat(Math.max(MIN_SETEXT_UNDERLINE_LENGTH, firstLine.length))}`;
24
31
  }
25
32
  const MIN_CODE_FENCE_LENGTH = 3;
@@ -50,12 +57,78 @@ function terminatesCleanly(styleId) {
50
57
  if (styleId === void 0 || styleId === "Quote" || styleId === "HTMLPreformatted") return false;
51
58
  return styleId === "CodeBlock" || styleId === "MathBlock" || styleId === "HorizontalRule" || require_shared_style_constants.parseHeadingStyleId(styleId) !== void 0;
52
59
  }
53
- function canInterruptOpenParagraph(styleId, context) {
60
+ const BLANK_OR_WHITESPACE_ONLY_LINE = /^[ \t]*$/;
61
+ function firstContentLineIndex(text) {
62
+ return text.split(require_shared_line_ending.LINE_ENDING_PATTERN).findIndex((line) => !BLANK_OR_WHITESPACE_ONLY_LINE.test(line));
63
+ }
64
+ function leadingIndentColumns(line) {
65
+ let column = 0;
66
+ for (const char of line) if (char === " ") column += 1;
67
+ else if (char === " ") column += 4 - column % 4;
68
+ else break;
69
+ return column;
70
+ }
71
+ const MAX_LEADING_BLANK_LINES_FOR_SETEXT = 1;
72
+ function interruptsSetextParagraph(line, atBlockStart) {
73
+ const cursor = new require_block_line.LineCursor(line);
74
+ if (cursor.indented) return false;
75
+ if (cursor.peekNextNonspace() === ">") return true;
76
+ const rest = cursor.restFromNextNonspace();
77
+ if (require_block_block.ATX_MARKER_PATTERN.test(rest) || require_block_block.CODE_FENCE_PATTERN.test(rest) || require_block_block.THEMATIC_BREAK_PATTERN.test(rest) || require_block_block.MATH_BLOCK_MARKER_PATTERN.test(rest) || require_html_html.matchHtmlBlockStart(rest, !atBlockStart) !== void 0) return true;
78
+ return require_block_list.parseListMarker(cursor, !atBlockStart) !== void 0;
79
+ }
80
+ function tableDelimiterRowPromotesPrecedingLine(line, precedingLine) {
81
+ const cursor = new require_block_line.LineCursor(line);
82
+ if (cursor.indented) return false;
83
+ return require_block_table.parseTableDelimiterRow(cursor.restFromNextNonspace())?.length === require_block_table.splitTableRow(precedingLine).length;
84
+ }
85
+ function unsafeSetextBreakReason(text) {
86
+ let sawContentLine = false;
87
+ let leadingBlankLines = 0;
88
+ let precedingLine = "";
89
+ for (const line of text.split(require_shared_line_ending.LINE_ENDING_PATTERN)) {
90
+ const isBlank = BLANK_OR_WHITESPACE_ONLY_LINE.test(line);
91
+ if (!sawContentLine) {
92
+ if (isBlank) {
93
+ leadingBlankLines += 1;
94
+ if (leadingBlankLines > MAX_LEADING_BLANK_LINES_FOR_SETEXT) return "blank-line";
95
+ continue;
96
+ }
97
+ sawContentLine = true;
98
+ if (leadingIndentColumns(line) >= 4) return "leading-indentation";
99
+ if (interruptsSetextParagraph(line, true)) return "interrupting-line";
100
+ precedingLine = line;
101
+ continue;
102
+ }
103
+ if (isBlank) return "blank-line";
104
+ if (interruptsSetextParagraph(line, false) || tableDelimiterRowPromotesPrecedingLine(line, precedingLine)) return "interrupting-line";
105
+ precedingLine = line;
106
+ }
107
+ return sawContentLine ? void 0 : "blank-line";
108
+ }
109
+ function embedsUnsafeBreakForSetext(text) {
110
+ return unsafeSetextBreakReason(text) !== void 0;
111
+ }
112
+ function unsafeSetextDiagnosticMessage(level, reason, embedsLineBreak) {
113
+ const hazard = reason === "leading-indentation" ? `the line that would become the setext heading's own first line of text opens with 4 or more columns of space/tab indentation -- setext's own grammar (spec 0.31.2) requires the first line to have "not more than 3 spaces of indentation", so promoting would read that line back as an indented code block instead of heading text` : reason === "interrupting-line" ? `a line of the heading's own text -- including possibly its first -- would itself be interpretable as a code fence, ATX heading, block quote, thematic break, list item, HTML block, math block, or GFM table delimiter row -- setext's own grammar (spec 0.31.2) requires every one of the heading's lines to instead read as ordinary paragraph text, so promoting would read that line back as the start of a fresh, unrelated block (or, for a table delimiter row, a conversion of the line before it) rather than more of the heading` : `promoting would leave the setext underline with no heading text of its own to attach to -- either a blank line immediately before it (a trailing break), a blank line in the middle of the heading's own text (a genuinely interior break), or the heading's own rendered text being entirely blank to begin with -- setext's own grammar requires "one or more lines of text, not interrupted by a blank line"`;
114
+ return embedsLineBreak ? `a level ${String(level)} heading's own content contains a line break, but ${hazard}; ATX collapses the break to a single space instead of promoting into a corrupt reparse` : `the effective WriteMarkdownOptions.headingStyle is 'setext' (an explicit caller choice -- a break-free heading only reaches this path when requested outright), but ${hazard}; rendered as ATX instead of promoting into a corrupt reparse`;
115
+ }
116
+ function willRenderAsSetext(paragraph, level, context) {
117
+ if (level > MAX_SETEXT_LEVEL) return false;
118
+ const text = require_emit_inline.emitRuns(paragraph.runs, {
119
+ sink: require_diagnostics_diagnostics.NOOP_MARKDOWN_DIAGNOSTIC_SINK,
120
+ emphasisMarker: context.emphasisMarker
121
+ }, paragraph.constructs);
122
+ if (embedsUnsafeBreakForSetext(text)) return false;
123
+ return context.headingStyle === "setext" || require_shared_line_ending.LINE_ENDING_PATTERN.test(text);
124
+ }
125
+ function canInterruptOpenParagraph(paragraph, context) {
126
+ const styleId = paragraph.styleId;
54
127
  if (styleId === void 0 || styleId === "Quote" || styleId === "HTMLPreformatted") return false;
55
128
  if (styleId === "CodeBlock" || styleId === "MathBlock") return true;
56
129
  if (styleId === "HorizontalRule") return context.thematicBreakChar !== SETEXT_LEVEL_2_CHAR;
57
130
  const headingLevel = require_shared_style_constants.parseHeadingStyleId(styleId);
58
- if (headingLevel !== void 0) return !(context.headingStyle === "setext" && headingLevel <= MAX_SETEXT_LEVEL);
131
+ if (headingLevel !== void 0) return !willRenderAsSetext(paragraph, (0, document_schema_js.clampHeadingLevel)(headingLevel), context);
59
132
  return false;
60
133
  }
61
134
  function quoteDepthOf(paragraph) {
@@ -83,7 +156,38 @@ function renderParagraphBody(paragraph, context) {
83
156
  message: `heading level ${String(headingLevel)} exceeds ATX's own six-"#" ceiling and is clamped to ${String(level)}`
84
157
  });
85
158
  const text = require_emit_inline.emitRuns(paragraph.runs, context, paragraph.constructs);
86
- if (context.headingStyle === "setext" && level <= MAX_SETEXT_LEVEL) return renderSetextHeading(level, text);
159
+ const embedsLineBreak = require_shared_line_ending.LINE_ENDING_PATTERN.test(text);
160
+ const unsafeSetextReason = unsafeSetextBreakReason(text);
161
+ const unsafeForSetext = unsafeSetextReason !== void 0;
162
+ const setextRequested = context.headingStyle === "setext" || embedsLineBreak;
163
+ if (setextRequested && level <= MAX_SETEXT_LEVEL && !unsafeForSetext) {
164
+ if (context.headingStyle !== "setext") {
165
+ const leadingBreakAbsorbed = firstContentLineIndex(text) > 0;
166
+ context.sink({
167
+ code: require_diagnostics_diagnostics.MarkdownDiagnosticCodes.HEADING_STYLE_OVERRIDDEN_FOR_LINE_BREAK,
168
+ severity: "info",
169
+ message: leadingBreakAbsorbed ? `the effective WriteMarkdownOptions.headingStyle is 'atx' (its own default, or an explicit caller choice), but this level ${String(level)} heading's own content contains a line break ATX has no way to hold -- rendered as setext instead, though the heading's own leading blank line is absorbed as ordinary space ahead of it on read-back rather than reproduced inside it` : `the effective WriteMarkdownOptions.headingStyle is 'atx' (its own default, or an explicit caller choice), but this level ${String(level)} heading's own content contains a line break ATX has no way to hold -- rendered as setext instead so the break survives`
170
+ });
171
+ }
172
+ return renderSetextHeading(level, text);
173
+ }
174
+ if (setextRequested && level <= MAX_SETEXT_LEVEL && unsafeForSetext) {
175
+ context.sink({
176
+ code: require_diagnostics_diagnostics.MarkdownDiagnosticCodes.HEADING_LINE_BREAK_UNSAFE_FOR_SETEXT,
177
+ severity: "info",
178
+ message: unsafeSetextDiagnosticMessage(level, unsafeSetextReason, embedsLineBreak)
179
+ });
180
+ if (embedsLineBreak) return `${"#".repeat(level)} ${text.replace(require_shared_line_ending.ESCAPED_HARD_BREAK_PATTERN, " ").split(require_shared_line_ending.LINE_ENDING_PATTERN).join(" ")}`;
181
+ return `${"#".repeat(level)} ${text}`;
182
+ }
183
+ if (embedsLineBreak) {
184
+ context.sink({
185
+ code: require_diagnostics_diagnostics.MarkdownDiagnosticCodes.HEADING_LINE_BREAK_COLLAPSED,
186
+ severity: "info",
187
+ message: `a level ${String(level)} heading's own content contains a line break; only setext's own level-1/2 grammar can hold one, so ATX collapses it to a single space`
188
+ });
189
+ return `${"#".repeat(level)} ${text.replace(require_shared_line_ending.ESCAPED_HARD_BREAK_PATTERN, " ").split(require_shared_line_ending.LINE_ENDING_PATTERN).join(" ")}`;
190
+ }
87
191
  return `${"#".repeat(level)} ${text}`;
88
192
  }
89
193
  return require_emit_inline.emitRuns(paragraph.runs, context, paragraph.constructs);
@@ -242,7 +346,7 @@ function collectListItem(items, start, level, itemId) {
242
346
  };
243
347
  }
244
348
  function emitItemCanInterrupt(item, context) {
245
- if (!isConstructItem(item)) return item.block.kind === "paragraph" ? canInterruptOpenParagraph(item.block.styleId, context) : true;
349
+ if (!isConstructItem(item)) return item.block.kind === "paragraph" ? canInterruptOpenParagraph(item.block, context) : true;
246
350
  if (isMaterialisedDivision(item)) return true;
247
351
  const first = item.children[0];
248
352
  return first === void 0 ? true : emitItemCanInterrupt(first, context);
package/dist/emit/emit.js CHANGED
@@ -1,6 +1,13 @@
1
1
  import { isValidFootnoteLabel } from "../inline/footnote.js";
2
2
  import "../defaults/defaults.js";
3
3
  import { MarkdownDiagnosticCodes, MarkdownInvalidRunConstructExtentError, MarkdownUnbalancedConstructMarkersError, MarkdownUnsupportedDocumentKindError, NOOP_MARKDOWN_DIAGNOSTIC_SINK } from "../diagnostics/diagnostics.js";
4
+ import { matchHtmlBlockStart } from "../html/html.js";
5
+ import { ESCAPED_HARD_BREAK_PATTERN, LINE_ENDING_PATTERN } from "../shared/line-ending.js";
6
+ import "../scan/scan.js";
7
+ import { LineCursor } from "../block/line.js";
8
+ import { parseListMarker } from "../block/list.js";
9
+ import { parseTableDelimiterRow, splitTableRow } from "../block/table.js";
10
+ import { ATX_MARKER_PATTERN, CODE_FENCE_PATTERN, MATH_BLOCK_MARKER_PATTERN, THEMATIC_BREAK_PATTERN } from "../block/block.js";
4
11
  import { parseListNumId } from "../shared/list-id.js";
5
12
  import { CODE_BLOCK_STYLE_ID, HORIZONTAL_RULE_STYLE_ID, HTML_PREFORMATTED_STYLE_ID, MATH_BLOCK_STYLE_ID, QUOTE_STYLE_ID, parseHeadingStyleId } from "../shared/style-constants.js";
6
13
  import { emitFrontMatter } from "./front-matter.js";
@@ -18,7 +25,7 @@ const SETEXT_LEVEL_2_CHAR = "-";
18
25
  const MIN_SETEXT_UNDERLINE_LENGTH = 1;
19
26
  function renderSetextHeading(level, text) {
20
27
  const underlineChar = level === 1 ? SETEXT_LEVEL_1_CHAR : SETEXT_LEVEL_2_CHAR;
21
- const firstLine = text.split("\n")[0] ?? "";
28
+ const firstLine = text.split(LINE_ENDING_PATTERN)[0] ?? "";
22
29
  return `${text}\n${underlineChar.repeat(Math.max(MIN_SETEXT_UNDERLINE_LENGTH, firstLine.length))}`;
23
30
  }
24
31
  const MIN_CODE_FENCE_LENGTH = 3;
@@ -49,12 +56,78 @@ function terminatesCleanly(styleId) {
49
56
  if (styleId === void 0 || styleId === "Quote" || styleId === "HTMLPreformatted") return false;
50
57
  return styleId === "CodeBlock" || styleId === "MathBlock" || styleId === "HorizontalRule" || parseHeadingStyleId(styleId) !== void 0;
51
58
  }
52
- function canInterruptOpenParagraph(styleId, context) {
59
+ const BLANK_OR_WHITESPACE_ONLY_LINE = /^[ \t]*$/;
60
+ function firstContentLineIndex(text) {
61
+ return text.split(LINE_ENDING_PATTERN).findIndex((line) => !BLANK_OR_WHITESPACE_ONLY_LINE.test(line));
62
+ }
63
+ function leadingIndentColumns(line) {
64
+ let column = 0;
65
+ for (const char of line) if (char === " ") column += 1;
66
+ else if (char === " ") column += 4 - column % 4;
67
+ else break;
68
+ return column;
69
+ }
70
+ const MAX_LEADING_BLANK_LINES_FOR_SETEXT = 1;
71
+ function interruptsSetextParagraph(line, atBlockStart) {
72
+ const cursor = new LineCursor(line);
73
+ if (cursor.indented) return false;
74
+ if (cursor.peekNextNonspace() === ">") return true;
75
+ const rest = cursor.restFromNextNonspace();
76
+ if (ATX_MARKER_PATTERN.test(rest) || CODE_FENCE_PATTERN.test(rest) || THEMATIC_BREAK_PATTERN.test(rest) || MATH_BLOCK_MARKER_PATTERN.test(rest) || matchHtmlBlockStart(rest, !atBlockStart) !== void 0) return true;
77
+ return parseListMarker(cursor, !atBlockStart) !== void 0;
78
+ }
79
+ function tableDelimiterRowPromotesPrecedingLine(line, precedingLine) {
80
+ const cursor = new LineCursor(line);
81
+ if (cursor.indented) return false;
82
+ return parseTableDelimiterRow(cursor.restFromNextNonspace())?.length === splitTableRow(precedingLine).length;
83
+ }
84
+ function unsafeSetextBreakReason(text) {
85
+ let sawContentLine = false;
86
+ let leadingBlankLines = 0;
87
+ let precedingLine = "";
88
+ for (const line of text.split(LINE_ENDING_PATTERN)) {
89
+ const isBlank = BLANK_OR_WHITESPACE_ONLY_LINE.test(line);
90
+ if (!sawContentLine) {
91
+ if (isBlank) {
92
+ leadingBlankLines += 1;
93
+ if (leadingBlankLines > MAX_LEADING_BLANK_LINES_FOR_SETEXT) return "blank-line";
94
+ continue;
95
+ }
96
+ sawContentLine = true;
97
+ if (leadingIndentColumns(line) >= 4) return "leading-indentation";
98
+ if (interruptsSetextParagraph(line, true)) return "interrupting-line";
99
+ precedingLine = line;
100
+ continue;
101
+ }
102
+ if (isBlank) return "blank-line";
103
+ if (interruptsSetextParagraph(line, false) || tableDelimiterRowPromotesPrecedingLine(line, precedingLine)) return "interrupting-line";
104
+ precedingLine = line;
105
+ }
106
+ return sawContentLine ? void 0 : "blank-line";
107
+ }
108
+ function embedsUnsafeBreakForSetext(text) {
109
+ return unsafeSetextBreakReason(text) !== void 0;
110
+ }
111
+ function unsafeSetextDiagnosticMessage(level, reason, embedsLineBreak) {
112
+ const hazard = reason === "leading-indentation" ? `the line that would become the setext heading's own first line of text opens with 4 or more columns of space/tab indentation -- setext's own grammar (spec 0.31.2) requires the first line to have "not more than 3 spaces of indentation", so promoting would read that line back as an indented code block instead of heading text` : reason === "interrupting-line" ? `a line of the heading's own text -- including possibly its first -- would itself be interpretable as a code fence, ATX heading, block quote, thematic break, list item, HTML block, math block, or GFM table delimiter row -- setext's own grammar (spec 0.31.2) requires every one of the heading's lines to instead read as ordinary paragraph text, so promoting would read that line back as the start of a fresh, unrelated block (or, for a table delimiter row, a conversion of the line before it) rather than more of the heading` : `promoting would leave the setext underline with no heading text of its own to attach to -- either a blank line immediately before it (a trailing break), a blank line in the middle of the heading's own text (a genuinely interior break), or the heading's own rendered text being entirely blank to begin with -- setext's own grammar requires "one or more lines of text, not interrupted by a blank line"`;
113
+ return embedsLineBreak ? `a level ${String(level)} heading's own content contains a line break, but ${hazard}; ATX collapses the break to a single space instead of promoting into a corrupt reparse` : `the effective WriteMarkdownOptions.headingStyle is 'setext' (an explicit caller choice -- a break-free heading only reaches this path when requested outright), but ${hazard}; rendered as ATX instead of promoting into a corrupt reparse`;
114
+ }
115
+ function willRenderAsSetext(paragraph, level, context) {
116
+ if (level > MAX_SETEXT_LEVEL) return false;
117
+ const text = emitRuns(paragraph.runs, {
118
+ sink: NOOP_MARKDOWN_DIAGNOSTIC_SINK,
119
+ emphasisMarker: context.emphasisMarker
120
+ }, paragraph.constructs);
121
+ if (embedsUnsafeBreakForSetext(text)) return false;
122
+ return context.headingStyle === "setext" || LINE_ENDING_PATTERN.test(text);
123
+ }
124
+ function canInterruptOpenParagraph(paragraph, context) {
125
+ const styleId = paragraph.styleId;
53
126
  if (styleId === void 0 || styleId === "Quote" || styleId === "HTMLPreformatted") return false;
54
127
  if (styleId === "CodeBlock" || styleId === "MathBlock") return true;
55
128
  if (styleId === "HorizontalRule") return context.thematicBreakChar !== SETEXT_LEVEL_2_CHAR;
56
129
  const headingLevel = parseHeadingStyleId(styleId);
57
- if (headingLevel !== void 0) return !(context.headingStyle === "setext" && headingLevel <= MAX_SETEXT_LEVEL);
130
+ if (headingLevel !== void 0) return !willRenderAsSetext(paragraph, clampHeadingLevel(headingLevel), context);
58
131
  return false;
59
132
  }
60
133
  function quoteDepthOf(paragraph) {
@@ -82,7 +155,38 @@ function renderParagraphBody(paragraph, context) {
82
155
  message: `heading level ${String(headingLevel)} exceeds ATX's own six-"#" ceiling and is clamped to ${String(level)}`
83
156
  });
84
157
  const text = emitRuns(paragraph.runs, context, paragraph.constructs);
85
- if (context.headingStyle === "setext" && level <= MAX_SETEXT_LEVEL) return renderSetextHeading(level, text);
158
+ const embedsLineBreak = LINE_ENDING_PATTERN.test(text);
159
+ const unsafeSetextReason = unsafeSetextBreakReason(text);
160
+ const unsafeForSetext = unsafeSetextReason !== void 0;
161
+ const setextRequested = context.headingStyle === "setext" || embedsLineBreak;
162
+ if (setextRequested && level <= MAX_SETEXT_LEVEL && !unsafeForSetext) {
163
+ if (context.headingStyle !== "setext") {
164
+ const leadingBreakAbsorbed = firstContentLineIndex(text) > 0;
165
+ context.sink({
166
+ code: MarkdownDiagnosticCodes.HEADING_STYLE_OVERRIDDEN_FOR_LINE_BREAK,
167
+ severity: "info",
168
+ message: leadingBreakAbsorbed ? `the effective WriteMarkdownOptions.headingStyle is 'atx' (its own default, or an explicit caller choice), but this level ${String(level)} heading's own content contains a line break ATX has no way to hold -- rendered as setext instead, though the heading's own leading blank line is absorbed as ordinary space ahead of it on read-back rather than reproduced inside it` : `the effective WriteMarkdownOptions.headingStyle is 'atx' (its own default, or an explicit caller choice), but this level ${String(level)} heading's own content contains a line break ATX has no way to hold -- rendered as setext instead so the break survives`
169
+ });
170
+ }
171
+ return renderSetextHeading(level, text);
172
+ }
173
+ if (setextRequested && level <= MAX_SETEXT_LEVEL && unsafeForSetext) {
174
+ context.sink({
175
+ code: MarkdownDiagnosticCodes.HEADING_LINE_BREAK_UNSAFE_FOR_SETEXT,
176
+ severity: "info",
177
+ message: unsafeSetextDiagnosticMessage(level, unsafeSetextReason, embedsLineBreak)
178
+ });
179
+ if (embedsLineBreak) return `${"#".repeat(level)} ${text.replace(ESCAPED_HARD_BREAK_PATTERN, " ").split(LINE_ENDING_PATTERN).join(" ")}`;
180
+ return `${"#".repeat(level)} ${text}`;
181
+ }
182
+ if (embedsLineBreak) {
183
+ context.sink({
184
+ code: MarkdownDiagnosticCodes.HEADING_LINE_BREAK_COLLAPSED,
185
+ severity: "info",
186
+ message: `a level ${String(level)} heading's own content contains a line break; only setext's own level-1/2 grammar can hold one, so ATX collapses it to a single space`
187
+ });
188
+ return `${"#".repeat(level)} ${text.replace(ESCAPED_HARD_BREAK_PATTERN, " ").split(LINE_ENDING_PATTERN).join(" ")}`;
189
+ }
86
190
  return `${"#".repeat(level)} ${text}`;
87
191
  }
88
192
  return emitRuns(paragraph.runs, context, paragraph.constructs);
@@ -241,7 +345,7 @@ function collectListItem(items, start, level, itemId) {
241
345
  };
242
346
  }
243
347
  function emitItemCanInterrupt(item, context) {
244
- if (!isConstructItem(item)) return item.block.kind === "paragraph" ? canInterruptOpenParagraph(item.block.styleId, context) : true;
348
+ if (!isConstructItem(item)) return item.block.kind === "paragraph" ? canInterruptOpenParagraph(item.block, context) : true;
245
349
  if (isMaterialisedDivision(item)) return true;
246
350
  const first = item.children[0];
247
351
  return first === void 0 ? true : emitItemCanInterrupt(first, context);
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_inline_footnote = require("../inline/footnote.cjs");
3
3
  const require_diagnostics_diagnostics = require("../diagnostics/diagnostics.cjs");
4
+ const require_shared_line_ending = require("../shared/line-ending.cjs");
4
5
  require("../shared/style-constants.cjs");
5
6
  //#region src/emit/inline.ts
6
7
  const ESCAPE_CHARS = /* @__PURE__ */ new Set([
@@ -40,9 +41,9 @@ function escapeMarkdownText(text) {
40
41
  let index = 0;
41
42
  while (index < text.length) {
42
43
  const char = text.charAt(index);
43
- if (char === "\n") {
44
+ if (char === "\n" || char === "\r") {
44
45
  out += "\\\n";
45
- index += 1;
46
+ index += char === "\r" && text.charAt(index + 1) === "\n" ? 2 : 1;
46
47
  continue;
47
48
  }
48
49
  if (ESCAPE_CHARS.has(char)) {
@@ -191,7 +192,7 @@ function emitRuns(runs, context, constructs) {
191
192
  return out;
192
193
  }
193
194
  function emitRunsSingleLine(runs, context, constructs) {
194
- return emitRuns(runs, context, constructs).replace(/\\\n/g, " ");
195
+ return emitRuns(runs, context, constructs).replace(require_shared_line_ending.ESCAPED_HARD_BREAK_PATTERN, " ").split(require_shared_line_ending.LINE_ENDING_PATTERN).join(" ");
195
196
  }
196
197
  //#endregion
197
198
  exports.emitRuns = emitRuns;
@@ -1,4 +1,4 @@
1
- import { i as MarkdownDiagnosticSink } from "../diagnostics-CH-xa2-s.cjs";
1
+ import { i as MarkdownDiagnosticSink } from "../diagnostics-CnamCy6I.cjs";
2
2
  import { ContentRun, RunConstructExtent } from "document-schema.js";
3
3
  //#region src/emit/inline.d.ts
4
4
  interface InlineEmitContext {
@@ -1,4 +1,4 @@
1
- import { i as MarkdownDiagnosticSink } from "../diagnostics-CH-xa2-s.js";
1
+ import { i as MarkdownDiagnosticSink } from "../diagnostics-CnamCy6I.js";
2
2
  import { ContentRun, RunConstructExtent } from "document-schema.js";
3
3
  //#region src/emit/inline.d.ts
4
4
  interface InlineEmitContext {
@@ -1,5 +1,6 @@
1
1
  import { isValidFootnoteLabel } from "../inline/footnote.js";
2
2
  import { MarkdownDiagnosticCodes } from "../diagnostics/diagnostics.js";
3
+ import { ESCAPED_HARD_BREAK_PATTERN, LINE_ENDING_PATTERN } from "../shared/line-ending.js";
3
4
  import "../shared/style-constants.js";
4
5
  //#region src/emit/inline.ts
5
6
  const ESCAPE_CHARS = /* @__PURE__ */ new Set([
@@ -39,9 +40,9 @@ function escapeMarkdownText(text) {
39
40
  let index = 0;
40
41
  while (index < text.length) {
41
42
  const char = text.charAt(index);
42
- if (char === "\n") {
43
+ if (char === "\n" || char === "\r") {
43
44
  out += "\\\n";
44
- index += 1;
45
+ index += char === "\r" && text.charAt(index + 1) === "\n" ? 2 : 1;
45
46
  continue;
46
47
  }
47
48
  if (ESCAPE_CHARS.has(char)) {
@@ -190,7 +191,7 @@ function emitRuns(runs, context, constructs) {
190
191
  return out;
191
192
  }
192
193
  function emitRunsSingleLine(runs, context, constructs) {
193
- return emitRuns(runs, context, constructs).replace(/\\\n/g, " ");
194
+ return emitRuns(runs, context, constructs).replace(ESCAPED_HARD_BREAK_PATTERN, " ").split(LINE_ENDING_PATTERN).join(" ");
194
195
  }
195
196
  //#endregion
196
197
  export { emitRuns, emitRunsSingleLine, escapeLinkDestination, escapeMarkdownText, renderLinkTitle };
@@ -1,4 +1,4 @@
1
- import { i as MarkdownDiagnosticSink } from "../diagnostics-CH-xa2-s.cjs";
1
+ import { i as MarkdownDiagnosticSink } from "../diagnostics-CnamCy6I.cjs";
2
2
  import { InlineEmitContext } from "./inline.cjs";
3
3
  import { ContentTable } from "document-schema.js";
4
4
  //#region src/emit/table.d.ts
@@ -1,4 +1,4 @@
1
- import { i as MarkdownDiagnosticSink } from "../diagnostics-CH-xa2-s.js";
1
+ import { i as MarkdownDiagnosticSink } from "../diagnostics-CnamCy6I.js";
2
2
  import { InlineEmitContext } from "./inline.js";
3
3
  import { ContentTable } from "document-schema.js";
4
4
  //#region src/emit/table.d.ts
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as MarkdownInputTooLargeError, c as MarkdownNestingLimitExceededError, d as MarkdownUnbalancedConstructMarkersError, f as MarkdownUnsupportedDocumentKindError, i as MarkdownDiagnosticSink, m as NOOP_MARKDOWN_DIAGNOSTIC_SINK, n as MarkdownDiagnosticCodes, o as MarkdownInvalidRunConstructExtentError, p as MarkdownWriteError, r as MarkdownDiagnosticSeverity, s as MarkdownInvalidUtf8Error, t as MarkdownDiagnostic, u as MarkdownParseError } from "./diagnostics-CH-xa2-s.cjs";
1
+ import { a as MarkdownInputTooLargeError, c as MarkdownNestingLimitExceededError, d as MarkdownUnbalancedConstructMarkersError, f as MarkdownUnsupportedDocumentKindError, i as MarkdownDiagnosticSink, m as NOOP_MARKDOWN_DIAGNOSTIC_SINK, n as MarkdownDiagnosticCodes, o as MarkdownInvalidRunConstructExtentError, p as MarkdownWriteError, r as MarkdownDiagnosticSeverity, s as MarkdownInvalidUtf8Error, t as MarkdownDiagnostic, u as MarkdownParseError } from "./diagnostics-CnamCy6I.cjs";
2
2
  import { MarkdownBytesSchema, markdownCodec, markdownContentCodec } from "./codec.cjs";
3
3
  import { n as MarkdownImageResolver, r as MarkdownResolvedImageBytes, t as MarkdownImageResolveContext } from "./image-B5L0HpAJ.cjs";
4
4
  import { MarkdownBulletListMarker, MarkdownCodeFenceChar, MarkdownEmphasisMarker, MarkdownHeadingStyle, MarkdownLineEnding, MarkdownOrderedListDelimiter, MarkdownThematicBreakChar, ReadMarkdownOptions, WriteMarkdownOptions, WriteMarkdownStyleOptions } from "./options/options.cjs";
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as MarkdownInputTooLargeError, c as MarkdownNestingLimitExceededError, d as MarkdownUnbalancedConstructMarkersError, f as MarkdownUnsupportedDocumentKindError, i as MarkdownDiagnosticSink, m as NOOP_MARKDOWN_DIAGNOSTIC_SINK, n as MarkdownDiagnosticCodes, o as MarkdownInvalidRunConstructExtentError, p as MarkdownWriteError, r as MarkdownDiagnosticSeverity, s as MarkdownInvalidUtf8Error, t as MarkdownDiagnostic, u as MarkdownParseError } from "./diagnostics-CH-xa2-s.js";
1
+ import { a as MarkdownInputTooLargeError, c as MarkdownNestingLimitExceededError, d as MarkdownUnbalancedConstructMarkersError, f as MarkdownUnsupportedDocumentKindError, i as MarkdownDiagnosticSink, m as NOOP_MARKDOWN_DIAGNOSTIC_SINK, n as MarkdownDiagnosticCodes, o as MarkdownInvalidRunConstructExtentError, p as MarkdownWriteError, r as MarkdownDiagnosticSeverity, s as MarkdownInvalidUtf8Error, t as MarkdownDiagnostic, u as MarkdownParseError } from "./diagnostics-CnamCy6I.js";
2
2
  import { MarkdownBytesSchema, markdownCodec, markdownContentCodec } from "./codec.js";
3
3
  import { n as MarkdownImageResolver, r as MarkdownResolvedImageBytes, t as MarkdownImageResolveContext } from "./image-C390OIB3.js";
4
4
  import { MarkdownBulletListMarker, MarkdownCodeFenceChar, MarkdownEmphasisMarker, MarkdownHeadingStyle, MarkdownLineEnding, MarkdownOrderedListDelimiter, MarkdownThematicBreakChar, ReadMarkdownOptions, WriteMarkdownOptions, WriteMarkdownStyleOptions } from "./options/options.js";
@@ -1,10 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_diagnostics_diagnostics = require("../diagnostics/diagnostics.cjs");
3
+ const require_shared_line_ending = require("../shared/line-ending.cjs");
3
4
  //#region src/lower/front-matter.ts
4
5
  const LEADING_DELIMITER_PATTERN = /^---[ \t]*$/;
5
6
  const CLOSING_DELIMITER_PATTERN = /^(?:---|\.\.\.)[ \t]*$/;
6
7
  const KEY_VALUE_LINE_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(.*)$/;
7
- const LINE_ENDING_PATTERN = /\r\n|\n|\r/;
8
8
  function parseScalar(raw) {
9
9
  const trimmed = raw.trim();
10
10
  const isDoubleQuoted = trimmed.length >= 2 && trimmed.startsWith("\"") && trimmed.endsWith("\"");
@@ -16,7 +16,7 @@ function parseKeywordList(raw) {
16
16
  return (trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed).split(",").map((item) => parseScalar(item)).filter((item) => item.length > 0);
17
17
  }
18
18
  function extractFrontMatter(source, sink = require_diagnostics_diagnostics.NOOP_MARKDOWN_DIAGNOSTIC_SINK) {
19
- const lines = source.split(LINE_ENDING_PATTERN);
19
+ const lines = source.split(require_shared_line_ending.LINE_ENDING_PATTERN);
20
20
  const firstLine = lines[0];
21
21
  if (firstLine === void 0 || !LEADING_DELIMITER_PATTERN.test(firstLine)) return {
22
22
  metadata: {},
@@ -1,4 +1,4 @@
1
- import { i as MarkdownDiagnosticSink } from "../diagnostics-CH-xa2-s.cjs";
1
+ import { i as MarkdownDiagnosticSink } from "../diagnostics-CnamCy6I.cjs";
2
2
  import { LayoutMetadata } from "document-schema.js";
3
3
  //#region src/lower/front-matter.d.ts
4
4
  interface FrontMatterResult {
@@ -1,4 +1,4 @@
1
- import { i as MarkdownDiagnosticSink } from "../diagnostics-CH-xa2-s.js";
1
+ import { i as MarkdownDiagnosticSink } from "../diagnostics-CnamCy6I.js";
2
2
  import { LayoutMetadata } from "document-schema.js";
3
3
  //#region src/lower/front-matter.d.ts
4
4
  interface FrontMatterResult {
@@ -1,9 +1,9 @@
1
1
  import { MarkdownDiagnosticCodes, NOOP_MARKDOWN_DIAGNOSTIC_SINK } from "../diagnostics/diagnostics.js";
2
+ import { LINE_ENDING_PATTERN } from "../shared/line-ending.js";
2
3
  //#region src/lower/front-matter.ts
3
4
  const LEADING_DELIMITER_PATTERN = /^---[ \t]*$/;
4
5
  const CLOSING_DELIMITER_PATTERN = /^(?:---|\.\.\.)[ \t]*$/;
5
6
  const KEY_VALUE_LINE_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(.*)$/;
6
- const LINE_ENDING_PATTERN = /\r\n|\n|\r/;
7
7
  function parseScalar(raw) {
8
8
  const trimmed = raw.trim();
9
9
  const isDoubleQuoted = trimmed.length >= 2 && trimmed.startsWith("\"") && trimmed.endsWith("\"");
@@ -33,7 +33,13 @@ function lowerInlineNodeInto(node, style, context, runs, extents) {
33
33
  if (node.value.length > 0) runs.push(buildRun(node.value, style));
34
34
  return;
35
35
  case "softBreak":
36
- runs.push(buildRun(" ", style));
36
+ runs.push({
37
+ ...buildRun(" ", style),
38
+ source: {
39
+ format: "markdown",
40
+ xml: "\n"
41
+ }
42
+ });
37
43
  return;
38
44
  case "hardBreak":
39
45
  runs.push(buildRun("\n", style));
@@ -1,5 +1,5 @@
1
1
  import { v as MarkdownInlineNode } from "../ast-CNfTgS0N.cjs";
2
- import { i as MarkdownDiagnosticSink } from "../diagnostics-CH-xa2-s.cjs";
2
+ import { i as MarkdownDiagnosticSink } from "../diagnostics-CnamCy6I.cjs";
3
3
  import { ContentRun, RunConstructExtent } from "document-schema.js";
4
4
  //#region src/lower/inline.d.ts
5
5
  interface InlineLowerContext {
@@ -1,5 +1,5 @@
1
1
  import { v as MarkdownInlineNode } from "../ast-CNfTgS0N.js";
2
- import { i as MarkdownDiagnosticSink } from "../diagnostics-CH-xa2-s.js";
2
+ import { i as MarkdownDiagnosticSink } from "../diagnostics-CnamCy6I.js";
3
3
  import { ContentRun, RunConstructExtent } from "document-schema.js";
4
4
  //#region src/lower/inline.d.ts
5
5
  interface InlineLowerContext {
@@ -32,7 +32,13 @@ function lowerInlineNodeInto(node, style, context, runs, extents) {
32
32
  if (node.value.length > 0) runs.push(buildRun(node.value, style));
33
33
  return;
34
34
  case "softBreak":
35
- runs.push(buildRun(" ", style));
35
+ runs.push({
36
+ ...buildRun(" ", style),
37
+ source: {
38
+ format: "markdown",
39
+ xml: "\n"
40
+ }
41
+ });
36
42
  return;
37
43
  case "hardBreak":
38
44
  runs.push(buildRun("\n", style));
@@ -137,6 +137,7 @@ function lowerCodeBlock(node, context) {
137
137
  kind: "paragraph",
138
138
  runs: [require_lower_inline.lowerCodeBlockRun(node.literal.replace(/\n$/, ""))],
139
139
  styleId: require_shared_style_constants.CODE_BLOCK_STYLE_ID,
140
+ preformatted: true,
140
141
  ...info.language !== void 0 ? { codeLanguage: info.language } : {},
141
142
  ...info.remainder !== void 0 ? { source: {
142
143
  format: "markdown",
@@ -136,6 +136,7 @@ function lowerCodeBlock(node, context) {
136
136
  kind: "paragraph",
137
137
  runs: [lowerCodeBlockRun(node.literal.replace(/\n$/, ""))],
138
138
  styleId: CODE_BLOCK_STYLE_ID,
139
+ preformatted: true,
139
140
  ...info.language !== void 0 ? { codeLanguage: info.language } : {},
140
141
  ...info.remainder !== void 0 ? { source: {
141
142
  format: "markdown",
@@ -1,4 +1,4 @@
1
- import { i as MarkdownDiagnosticSink } from "../diagnostics-CH-xa2-s.cjs";
1
+ import { i as MarkdownDiagnosticSink } from "../diagnostics-CnamCy6I.cjs";
2
2
  import { n as MarkdownImageResolver } from "../image-B5L0HpAJ.cjs";
3
3
  import { Margins, PageSize } from "document-schema.js";
4
4
  //#region src/options/options.d.ts
@@ -1,4 +1,4 @@
1
- import { i as MarkdownDiagnosticSink } from "../diagnostics-CH-xa2-s.js";
1
+ import { i as MarkdownDiagnosticSink } from "../diagnostics-CnamCy6I.js";
2
2
  import { n as MarkdownImageResolver } from "../image-C390OIB3.js";
3
3
  import { Margins, PageSize } from "document-schema.js";
4
4
  //#region src/options/options.d.ts
package/dist/read.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as MarkdownDiagnostic } from "./diagnostics-CH-xa2-s.cjs";
1
+ import { t as MarkdownDiagnostic } from "./diagnostics-CnamCy6I.cjs";
2
2
  import { ReadMarkdownOptions } from "./options/options.cjs";
3
3
  import { ContentDocument, DocumentTree } from "document-schema.js";
4
4
  //#region src/read.d.ts
package/dist/read.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as MarkdownDiagnostic } from "./diagnostics-CH-xa2-s.js";
1
+ import { t as MarkdownDiagnostic } from "./diagnostics-CnamCy6I.js";
2
2
  import { ReadMarkdownOptions } from "./options/options.js";
3
3
  import { ContentDocument, DocumentTree } from "document-schema.js";
4
4
  //#region src/read.d.ts
@@ -0,0 +1,7 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/shared/line-ending.ts
3
+ const LINE_ENDING_PATTERN = /\r\n|\n|\r/;
4
+ const ESCAPED_HARD_BREAK_PATTERN = new RegExp(`\\\\(?:${LINE_ENDING_PATTERN.source})`, "g");
5
+ //#endregion
6
+ exports.ESCAPED_HARD_BREAK_PATTERN = ESCAPED_HARD_BREAK_PATTERN;
7
+ exports.LINE_ENDING_PATTERN = LINE_ENDING_PATTERN;
@@ -0,0 +1,5 @@
1
+ //#region src/shared/line-ending.d.ts
2
+ declare const LINE_ENDING_PATTERN: RegExp;
3
+ declare const ESCAPED_HARD_BREAK_PATTERN: RegExp;
4
+ //#endregion
5
+ export { ESCAPED_HARD_BREAK_PATTERN, LINE_ENDING_PATTERN };
@@ -0,0 +1,5 @@
1
+ //#region src/shared/line-ending.d.ts
2
+ declare const LINE_ENDING_PATTERN: RegExp;
3
+ declare const ESCAPED_HARD_BREAK_PATTERN: RegExp;
4
+ //#endregion
5
+ export { ESCAPED_HARD_BREAK_PATTERN, LINE_ENDING_PATTERN };
@@ -0,0 +1,5 @@
1
+ //#region src/shared/line-ending.ts
2
+ const LINE_ENDING_PATTERN = /\r\n|\n|\r/;
3
+ const ESCAPED_HARD_BREAK_PATTERN = new RegExp(`\\\\(?:${LINE_ENDING_PATTERN.source})`, "g");
4
+ //#endregion
5
+ export { ESCAPED_HARD_BREAK_PATTERN, LINE_ENDING_PATTERN };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markdown-codec",
3
- "version": "6.2.0",
3
+ "version": "6.3.1",
4
4
  "description": "Hand-written CommonMark+GFM <-> DocumentTree codec, built on document-schema.js",
5
5
  "type": "module",
6
6
  "repository": {
@@ -83,7 +83,7 @@
83
83
  "license": "MIT",
84
84
  "packageManager": "pnpm@11.6.0",
85
85
  "dependencies": {
86
- "document-schema.js": "^6.1.0",
86
+ "document-schema.js": "^6.2.0",
87
87
  "zod": "^4.4.3"
88
88
  },
89
89
  "devDependencies": {