From 3684eae14a7362cb6bbefceaf6cb6588c41917f4 Mon Sep 17 00:00:00 2001 From: Hendrik Heil Date: Fri, 26 Jun 2026 10:05:11 +0200 Subject: [PATCH 1/2] fix(stringify): render ordered-list `start` as native numbering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An ordered list with a non-1 `start` (e.g. `5. 6. 7.`) round-tripped to the verbose `::ol{start="5"}\n1. …\n::` wrapper instead of native markdown numbering. `ol.ts` hardcoded the item counter to `order: 1`, and nothing marked `start` as implicit, so `userBlockAttrs('ol', …)` treated it as a real user attr and forced the `::ol{…}` block form. Seed the counter from `start` and add `ol: { drop: ['start'] }` to `IMPLICIT_ATTRS` so `start` is recognized as conveyed by the native syntax. Now `5. 6. 7.` round-trips losslessly, and a legacy `::ol{start="…"}` wrapper normalizes to clean native numbering. Genuine non-`start` attrs still wrap. Co-Authored-By: Claude Opus 4.8 --- .../ordered-list-start-wrapper-normalizes.md | 51 +++++++++++++++++ .../SPEC/common-mark/ordered-list-start.md | 57 +++++++++++++++++++ .../src/internal/stringify/attributes.ts | 3 + .../src/internal/stringify/handlers/ol.ts | 8 ++- 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 packages/comark/SPEC/COMARK/ordered-list-start-wrapper-normalizes.md create mode 100644 packages/comark/SPEC/common-mark/ordered-list-start.md diff --git a/packages/comark/SPEC/COMARK/ordered-list-start-wrapper-normalizes.md b/packages/comark/SPEC/COMARK/ordered-list-start-wrapper-normalizes.md new file mode 100644 index 00000000..ebdcb618 --- /dev/null +++ b/packages/comark/SPEC/COMARK/ordered-list-start-wrapper-normalizes.md @@ -0,0 +1,51 @@ +## Input + +```md +::ol{start="3"} +1. apple +2. banana +:: +``` + +## AST + +```json +{ + "frontmatter": {}, + "meta": {}, + "nodes": [ + [ + "ol", + { + "start": "3" + }, + [ + "li", + {}, + "apple" + ], + [ + "li", + {}, + "banana" + ] + ] + ] +} +``` + +## HTML + +```html +
    +
  1. apple
  2. +
  3. banana
  4. +
+``` + +## Markdown + +```md +3. apple +4. banana +``` diff --git a/packages/comark/SPEC/common-mark/ordered-list-start.md b/packages/comark/SPEC/common-mark/ordered-list-start.md new file mode 100644 index 00000000..36a2fee8 --- /dev/null +++ b/packages/comark/SPEC/common-mark/ordered-list-start.md @@ -0,0 +1,57 @@ +## Input + +```md +5. First item +6. Second item +7. Third item +``` + +## AST + +```json +{ + "frontmatter": {}, + "meta": {}, + "nodes": [ + [ + "ol", + { + "start": "5" + }, + [ + "li", + {}, + "First item" + ], + [ + "li", + {}, + "Second item" + ], + [ + "li", + {}, + "Third item" + ] + ] + ] +} +``` + +## HTML + +```html +
    +
  1. First item
  2. +
  3. Second item
  4. +
  5. Third item
  6. +
+``` + +## Markdown + +```md +5. First item +6. Second item +7. Third item +``` diff --git a/packages/comark/src/internal/stringify/attributes.ts b/packages/comark/src/internal/stringify/attributes.ts index 9469d607..0d74c1de 100644 --- a/packages/comark/src/internal/stringify/attributes.ts +++ b/packages/comark/src/internal/stringify/attributes.ts @@ -97,6 +97,9 @@ export function resolveAttribute(attrs: Record, renderData: Nod // is implicit in `- [ ]`) so they should not echo back as user attrs. const IMPLICIT_ATTRS: Record = { blockquote: { drop: ['as'] }, + // `start` is conveyed by the native ordered-list numbering (`5. …`), so it + // must not echo back as a `::ol{start=…}` wrapper. + ol: { drop: ['start'] }, ul: { classBlocklist: ['contains-task-list'] }, li: { classBlocklist: ['task-list-item'] }, // `language`/`filename`/`highlights`/`meta` ride on the fence info string. diff --git a/packages/comark/src/internal/stringify/handlers/ol.ts b/packages/comark/src/internal/stringify/handlers/ol.ts index 20e799db..58a59575 100644 --- a/packages/comark/src/internal/stringify/handlers/ol.ts +++ b/packages/comark/src/internal/stringify/handlers/ol.ts @@ -6,7 +6,13 @@ import { comarkAttributes, userBlockAttrs } from '../attributes.ts' export async function ol(node: ComarkElement, state: State) { const children = node.slice(2) as ComarkNode[] - const revert = state.applyContext({ list: true, order: 1, listIndent: 3 }) + // A non-1 `start` is conveyed by the native list numbering (`5. …`), so seed + // the item counter from it instead of forcing 1. `userBlockAttrs` drops + // `start` (see IMPLICIT_ATTRS), so it never also leaks into a `::ol{…}` wrapper. + const start = Number((node[1] as Record)?.start) + const order = Number.isInteger(start) && start >= 1 ? start : 1 + + const revert = state.applyContext({ list: true, order, listIndent: 3 }) let result = '' for (const child of children) { From d0b3b62b49f0d920d5a6203acea0459d3035bf48 Mon Sep 17 00:00:00 2001 From: Hendrik Heil Date: Mon, 29 Jun 2026 17:06:19 +0200 Subject: [PATCH 2/2] refactor: trim ordered-list start comments and drop stray optional chain Co-Authored-By: Claude Opus 4.8 --- packages/comark/src/internal/stringify/attributes.ts | 2 -- packages/comark/src/internal/stringify/handlers/ol.ts | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/comark/src/internal/stringify/attributes.ts b/packages/comark/src/internal/stringify/attributes.ts index 0d74c1de..1156b403 100644 --- a/packages/comark/src/internal/stringify/attributes.ts +++ b/packages/comark/src/internal/stringify/attributes.ts @@ -97,8 +97,6 @@ export function resolveAttribute(attrs: Record, renderData: Nod // is implicit in `- [ ]`) so they should not echo back as user attrs. const IMPLICIT_ATTRS: Record = { blockquote: { drop: ['as'] }, - // `start` is conveyed by the native ordered-list numbering (`5. …`), so it - // must not echo back as a `::ol{start=…}` wrapper. ol: { drop: ['start'] }, ul: { classBlocklist: ['contains-task-list'] }, li: { classBlocklist: ['task-list-item'] }, diff --git a/packages/comark/src/internal/stringify/handlers/ol.ts b/packages/comark/src/internal/stringify/handlers/ol.ts index 58a59575..eca4b98f 100644 --- a/packages/comark/src/internal/stringify/handlers/ol.ts +++ b/packages/comark/src/internal/stringify/handlers/ol.ts @@ -6,10 +6,8 @@ import { comarkAttributes, userBlockAttrs } from '../attributes.ts' export async function ol(node: ComarkElement, state: State) { const children = node.slice(2) as ComarkNode[] - // A non-1 `start` is conveyed by the native list numbering (`5. …`), so seed - // the item counter from it instead of forcing 1. `userBlockAttrs` drops - // `start` (see IMPLICIT_ATTRS), so it never also leaks into a `::ol{…}` wrapper. - const start = Number((node[1] as Record)?.start) + // `start` is carried by the native numbering; IMPLICIT_ATTRS drops it from user attrs. + const start = Number((node[1] as Record).start) const order = Number.isInteger(start) && start >= 1 ? start : 1 const revert = state.applyContext({ list: true, order, listIndent: 3 })