From fc3ddba3feba8d635868f6925f6dbedf1b3e21a1 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Tue, 28 Apr 2026 16:01:46 +0200 Subject: [PATCH 1/2] fix(stringify): handle multiple paragraphs in a list item --- packages/comark-ansi/src/handlers/li.ts | 29 ++++++++-- packages/comark-ansi/src/handlers/p.ts | 7 ++- .../shiki-ordered-list-nested-codeblock.md | 9 ++- .../ordered-list-mixed-blocks-nested.md | 17 ++++-- .../ordered-list-multiple-blocks.md | 8 ++- .../ordered-list-nested-codeblock.md | 17 ++++-- .../SPEC/common-mark/ordered-list-nested.md | 8 ++- .../common-mark/unordered-list-blockquote.md | 8 ++- .../unordered-list-multi-paragraph.md | 55 +++++++++++++++++++ .../unordered-list-nested-blockquotes.md | 16 ++++-- .../unordered-list-nested-deep-codeblock.md | 25 +++++++-- .../SPEC/common-mark/unordered-list-nested.md | 8 ++- .../SPEC/common-mark/unordered-list-table.md | 8 ++- packages/comark/SPEC/common-mark/xyz.md | 26 ++++++--- .../src/internal/parse/token-processor.ts | 14 +---- .../src/internal/stringify/handlers/html.ts | 2 +- .../src/internal/stringify/handlers/li.ts | 23 +++++--- .../src/internal/stringify/handlers/mdc.ts | 2 +- .../src/internal/stringify/handlers/ol.ts | 2 +- .../src/internal/stringify/handlers/p.ts | 6 +- .../src/internal/stringify/handlers/ul.ts | 2 +- .../comark/src/internal/stringify/indent.ts | 15 ----- packages/comark/src/utils/index.ts | 16 ++++++ 23 files changed, 239 insertions(+), 84 deletions(-) create mode 100644 packages/comark/SPEC/common-mark/unordered-list-multi-paragraph.md delete mode 100644 packages/comark/src/internal/stringify/indent.ts diff --git a/packages/comark-ansi/src/handlers/li.ts b/packages/comark-ansi/src/handlers/li.ts index 91eb662f..41be59bc 100644 --- a/packages/comark-ansi/src/handlers/li.ts +++ b/packages/comark-ansi/src/handlers/li.ts @@ -1,5 +1,10 @@ import type { NodeHandler } from 'comark/render' import type { ComarkElement, ComarkNode } from 'comark' +import { indent } from 'comark/utils' + +// Block elements that need explicit indentation in list items. +// Note: ol/ul are handled by their own handlers which manage indentation via listIndent context. +const blockElements = new Set(['pre', 'blockquote', 'table']) export const li: NodeHandler = async (node, state) => { const children = node.slice(2) as ComarkNode[] @@ -14,15 +19,31 @@ export const li: NodeHandler = async (node, state) => { prefix += input[1].checked || input[1][':checked'] ? '[x] ' : '[ ] ' } - let content = '' + const prefixWidth = prefix.length + let result = '' for (const child of children) { - content += await state.one(child, state, node) + const rendered = await state.one(child, state, node) + if (result && Array.isArray(child)) { + if (blockElements.has(child[0] as string)) { + // Block-level child: put on its own line and indent to align with list prefix + const indented = indent(rendered, { width: prefixWidth }) + result = result.trimEnd() + '\n' + indented.trimEnd() + '\n' + continue + } + + if (child[0] === 'p') { + const indented = indent(rendered, { width: prefixWidth }) + result = result.trimEnd() + '\n\n' + indented.trimEnd() + '\n' + continue + } + } + result += rendered } - content = content.trim() + result = result.trim() if (typeof order === 'number') { state.applyContext({ order: order + 1 }) } - return `${prefix}${content}\n` + return `${prefix}${result}\n` } diff --git a/packages/comark-ansi/src/handlers/p.ts b/packages/comark-ansi/src/handlers/p.ts index 000450bc..e226cf87 100644 --- a/packages/comark-ansi/src/handlers/p.ts +++ b/packages/comark-ansi/src/handlers/p.ts @@ -1,11 +1,16 @@ import type { NodeHandler } from 'comark/render' import type { ComarkNode } from 'comark' -export const p: NodeHandler = async (node, state) => { +export const p: NodeHandler = async (node, state, parent) => { const children = node.slice(2) as ComarkNode[] let result = '' for (const child of children) { result += await state.one(child, state, node) } + + if (parent?.[0] === 'li') { + return result + } + return result + '\n\n' } diff --git a/packages/comark/SPEC/COMARK/shiki-ordered-list-nested-codeblock.md b/packages/comark/SPEC/COMARK/shiki-ordered-list-nested-codeblock.md index 469eb5c0..945114bc 100644 --- a/packages/comark/SPEC/COMARK/shiki-ordered-list-nested-codeblock.md +++ b/packages/comark/SPEC/COMARK/shiki-ordered-list-nested-codeblock.md @@ -32,7 +32,11 @@ options: [ "li", {}, - "Setup:", + [ + "p", + {}, + "Setup:" + ], [ "pre", { @@ -100,7 +104,8 @@ options: ```html
  1. - Setup:
    let x = 1;
    +

    Setup:

    +
    let x = 1;
``` diff --git a/packages/comark/SPEC/common-mark/ordered-list-mixed-blocks-nested.md b/packages/comark/SPEC/common-mark/ordered-list-mixed-blocks-nested.md index 9f72dfb1..c5c320bf 100644 --- a/packages/comark/SPEC/common-mark/ordered-list-mixed-blocks-nested.md +++ b/packages/comark/SPEC/common-mark/ordered-list-mixed-blocks-nested.md @@ -29,7 +29,11 @@ [ "li", {}, - "Complex item:", + [ + "p", + {}, + "Complex item:" + ], [ "pre", { @@ -54,7 +58,11 @@ [ "li", {}, - "Nested with table:", + [ + "p", + {}, + "Nested with table:" + ], [ "table", {}, @@ -108,13 +116,14 @@ ```html
  1. - Complex item:
    code()
    +

    Complex item:

    +
    code()
    A note
    • - Nested with table: +

      Nested with table:

      diff --git a/packages/comark/SPEC/common-mark/ordered-list-multiple-blocks.md b/packages/comark/SPEC/common-mark/ordered-list-multiple-blocks.md index bf589b5d..9ab16b4b 100644 --- a/packages/comark/SPEC/common-mark/ordered-list-multiple-blocks.md +++ b/packages/comark/SPEC/common-mark/ordered-list-multiple-blocks.md @@ -25,7 +25,11 @@ [ "li", {}, - "First point:", + [ + "p", + {}, + "First point:" + ], [ "blockquote", {}, @@ -60,7 +64,7 @@ ```html
      1. - First point: +

        First point:

        A quote here
        diff --git a/packages/comark/SPEC/common-mark/ordered-list-nested-codeblock.md b/packages/comark/SPEC/common-mark/ordered-list-nested-codeblock.md index fb335a0b..0b06ad07 100644 --- a/packages/comark/SPEC/common-mark/ordered-list-nested-codeblock.md +++ b/packages/comark/SPEC/common-mark/ordered-list-nested-codeblock.md @@ -22,14 +22,22 @@ [ "li", {}, - "First level", + [ + "p", + {}, + "First level" + ], [ "ol", {}, [ "li", {}, - "Second level with code:", + [ + "p", + {}, + "Second level with code:" + ], [ "pre", { @@ -56,10 +64,11 @@ ```html
        1. - First level +

          First level

          1. - Second level with code:
            let x = 42;
            +

            Second level with code:

            +
            let x = 42;
        2. diff --git a/packages/comark/SPEC/common-mark/ordered-list-nested.md b/packages/comark/SPEC/common-mark/ordered-list-nested.md index be3d36a7..a9ebc0a9 100644 --- a/packages/comark/SPEC/common-mark/ordered-list-nested.md +++ b/packages/comark/SPEC/common-mark/ordered-list-nested.md @@ -32,7 +32,11 @@ [ "li", {}, - "Third item", + [ + "p", + {}, + "Third item" + ], [ "ol", {}, @@ -65,7 +69,7 @@
        3. First item
        4. Second item
        5. - Third item +

          Third item

          1. Indented item
          2. Indented item
          3. diff --git a/packages/comark/SPEC/common-mark/unordered-list-blockquote.md b/packages/comark/SPEC/common-mark/unordered-list-blockquote.md index 4bc5f10e..631e7483 100644 --- a/packages/comark/SPEC/common-mark/unordered-list-blockquote.md +++ b/packages/comark/SPEC/common-mark/unordered-list-blockquote.md @@ -19,7 +19,11 @@ [ "li", {}, - "Item with quote:", + [ + "p", + {}, + "Item with quote:" + ], [ "blockquote", {}, @@ -36,7 +40,7 @@ ```html
            • - Item with quote: +

              Item with quote:

              This is a blockquote
              diff --git a/packages/comark/SPEC/common-mark/unordered-list-multi-paragraph.md b/packages/comark/SPEC/common-mark/unordered-list-multi-paragraph.md new file mode 100644 index 00000000..38ff0388 --- /dev/null +++ b/packages/comark/SPEC/common-mark/unordered-list-multi-paragraph.md @@ -0,0 +1,55 @@ +## Input + +```md +- para1 + + para2 +``` + +## AST + +```json +{ + "frontmatter": {}, + "meta": {}, + "nodes": [ + [ + "ul", + {}, + [ + "li", + {}, + [ + "p", + {}, + "para1" + ], + [ + "p", + {}, + "para2" + ] + ] + ] + ] +} +``` + +## HTML + +```html +
                +
              • +

                para1

                +

                para2

                +
              • +
              +``` + +## Markdown + +```md +- para1 + + para2 +``` diff --git a/packages/comark/SPEC/common-mark/unordered-list-nested-blockquotes.md b/packages/comark/SPEC/common-mark/unordered-list-nested-blockquotes.md index dbc5401a..83a11f72 100644 --- a/packages/comark/SPEC/common-mark/unordered-list-nested-blockquotes.md +++ b/packages/comark/SPEC/common-mark/unordered-list-nested-blockquotes.md @@ -23,7 +23,11 @@ [ "li", {}, - "Item with quote:", + [ + "p", + {}, + "Item with quote:" + ], [ "blockquote", {}, @@ -35,7 +39,11 @@ [ "li", {}, - "Nested item with quote:", + [ + "p", + {}, + "Nested item with quote:" + ], [ "blockquote", {}, @@ -54,13 +62,13 @@ ```html
              • - Item with quote: +

                Item with quote:

                Quote level 1
                • - Nested item with quote: +

                  Nested item with quote:

                  Quote level 2
                  diff --git a/packages/comark/SPEC/common-mark/unordered-list-nested-deep-codeblock.md b/packages/comark/SPEC/common-mark/unordered-list-nested-deep-codeblock.md index 65f84631..5c1bf82b 100644 --- a/packages/comark/SPEC/common-mark/unordered-list-nested-deep-codeblock.md +++ b/packages/comark/SPEC/common-mark/unordered-list-nested-deep-codeblock.md @@ -23,21 +23,33 @@ [ "li", {}, - "Level 1", + [ + "p", + {}, + "Level 1" + ], [ "ul", {}, [ "li", {}, - "Level 2", + [ + "p", + {}, + "Level 2" + ], [ "ul", {}, [ "li", {}, - "Level 3 with code:", + [ + "p", + {}, + "Level 3 with code:" + ], [ "pre", { @@ -66,13 +78,14 @@ ```html
                  • - Level 1 +

                    Level 1

                    • - Level 2 +

                      Level 2

                      • - Level 3 with code:
                        print("deep")
                        +

                        Level 3 with code:

                        +
                        print("deep")
                    • diff --git a/packages/comark/SPEC/common-mark/unordered-list-nested.md b/packages/comark/SPEC/common-mark/unordered-list-nested.md index d5117dd7..55396345 100644 --- a/packages/comark/SPEC/common-mark/unordered-list-nested.md +++ b/packages/comark/SPEC/common-mark/unordered-list-nested.md @@ -32,7 +32,11 @@ [ "li", {}, - "Third item", + [ + "p", + {}, + "Third item" + ], [ "ul", {}, @@ -65,7 +69,7 @@
                    • First item
                    • Second item
                    • - Third item +

                      Third item

                      • Indented item
                      • Indented item
                      • diff --git a/packages/comark/SPEC/common-mark/unordered-list-table.md b/packages/comark/SPEC/common-mark/unordered-list-table.md index 89508aa6..0a9ed526 100644 --- a/packages/comark/SPEC/common-mark/unordered-list-table.md +++ b/packages/comark/SPEC/common-mark/unordered-list-table.md @@ -21,7 +21,11 @@ [ "li", {}, - "Item with table:", + [ + "p", + {}, + "Item with table:" + ], [ "table", {}, @@ -73,7 +77,7 @@ ```html
                        • - Item with table: +

                          Item with table:

      diff --git a/packages/comark/SPEC/common-mark/xyz.md b/packages/comark/SPEC/common-mark/xyz.md index 0b966f59..bc390447 100644 --- a/packages/comark/SPEC/common-mark/xyz.md +++ b/packages/comark/SPEC/common-mark/xyz.md @@ -171,13 +171,17 @@ And here's a code block: [ "li", {}, - "Third item with ", [ - "a", - { - "href": "https://example.com" - }, - "a link" + "p", + {}, + "Third item with ", + [ + "a", + { + "href": "https://example.com" + }, + "a link" + ] ], [ "ul", @@ -216,7 +220,11 @@ And here's a code block: [ "li", {}, - "Third numbered item", + [ + "p", + {}, + "Third numbered item" + ], [ "ul", {}, @@ -284,7 +292,7 @@ And here's a code block:
    • First item
    • Second item with bold
    • - Third item with a link +

      Third item with a link

      • Nested item one
      • Nested item two
      • @@ -296,7 +304,7 @@ And here's a code block:
      • First numbered item
      • Second numbered item
      • - Third numbered item +

        Third numbered item

        • Nested unordered item
        • Another nested item
        • diff --git a/packages/comark/src/internal/parse/token-processor.ts b/packages/comark/src/internal/parse/token-processor.ts index 1375d4f0..b443190c 100644 --- a/packages/comark/src/internal/parse/token-processor.ts +++ b/packages/comark/src/internal/parse/token-processor.ts @@ -411,18 +411,8 @@ function processBlockToken( if (token.type === 'list_item_open') { const attrs = processAttributes(token.attrs, { handleBoolean: false, handleJSON: false }) const children = processBlockChildren(tokens, startIndex + 1, 'list_item_close', false, false, true, state) - // Unwrap paragraphs in list items - const unwrapped: ComarkNode[] = [] - for (const child of children.nodes) { - if (Array.isArray(child) && child[0] === 'p') { - // Unwrap paragraph, add its children directly - unwrapped.push(...(child.slice(2) as ComarkNode[])) - } else { - unwrapped.push(child) - } - } - if (unwrapped.length > 0) { - return { node: ['li', attrs, ...unwrapped] as ComarkNode, nextIndex: children.nextIndex + 1 } + if (children.nodes.length > 0) { + return { node: ['li', attrs, ...children.nodes] as ComarkNode, nextIndex: children.nextIndex + 1 } } return { node: null, nextIndex: children.nextIndex + 1 } } diff --git a/packages/comark/src/internal/stringify/handlers/html.ts b/packages/comark/src/internal/stringify/handlers/html.ts index c40a88ba..b5b13052 100644 --- a/packages/comark/src/internal/stringify/handlers/html.ts +++ b/packages/comark/src/internal/stringify/handlers/html.ts @@ -1,7 +1,7 @@ import type { State } from 'comark/render' import type { ComarkElement } from 'comark' import { htmlAttributes } from '../attributes.ts' -import { indent } from '../indent.ts' +import { indent } from '../../../utils/index.ts' const textBlocks = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'td', 'th']) const selfCloseTags = new Set(['br', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr']) diff --git a/packages/comark/src/internal/stringify/handlers/li.ts b/packages/comark/src/internal/stringify/handlers/li.ts index 168e312f..94c9c246 100644 --- a/packages/comark/src/internal/stringify/handlers/li.ts +++ b/packages/comark/src/internal/stringify/handlers/li.ts @@ -1,6 +1,6 @@ import type { State } from 'comark/render' import type { ComarkElement, ComarkNode } from 'comark' -import { indent } from '../indent.ts' +import { indent } from '../../../utils/index.ts' // Block elements that need explicit indentation in list items. // Note: ol/ul are handled by their own handlers which manage indentation via listIndent context. @@ -29,14 +29,21 @@ export async function li(node: ComarkElement, state: State) { for (const child of children) { const rendered = await state.one(child, state, node) - - if (Array.isArray(child) && blockElements.has(child[0] as string)) { - // Block-level child: put on its own line and indent to align with list prefix - const indented = indent(rendered, { width: prefixWidth }) - result = result.trimEnd() + '\n' + indented.trimEnd() + '\n' - } else { - result += rendered + if (result && Array.isArray(child)) { + if (blockElements.has(child[0] as string)) { + // Block-level child: put on its own line and indent to align with list prefix + const indented = indent(rendered, { width: prefixWidth }) + result = result.trimEnd() + '\n' + indented.trimEnd() + '\n' + continue + } + + if (child[0] === 'p') { + const indented = indent(rendered, { width: prefixWidth }) + result = result.trimEnd() + '\n\n' + indented.trimEnd() + '\n' + continue + } } + result += rendered } result = result.trim() diff --git a/packages/comark/src/internal/stringify/handlers/mdc.ts b/packages/comark/src/internal/stringify/handlers/mdc.ts index d11f36d3..28c2bfdb 100644 --- a/packages/comark/src/internal/stringify/handlers/mdc.ts +++ b/packages/comark/src/internal/stringify/handlers/mdc.ts @@ -1,6 +1,6 @@ import type { State } from 'comark/render' import type { ComarkElement, ComarkNode } from 'comark' -import { indent } from '../indent.ts' +import { indent } from '../../../utils/index.ts' import { comarkAttributes, comarkYamlAttributes } from '../attributes.ts' import { html } from './html.ts' diff --git a/packages/comark/src/internal/stringify/handlers/ol.ts b/packages/comark/src/internal/stringify/handlers/ol.ts index 33921f59..6a411e00 100644 --- a/packages/comark/src/internal/stringify/handlers/ol.ts +++ b/packages/comark/src/internal/stringify/handlers/ol.ts @@ -1,6 +1,6 @@ import type { State } from 'comark/render' import type { ComarkElement, ComarkNode } from 'comark' -import { indent } from '../indent.ts' +import { indent } from '../../../utils/index.ts' export async function ol(node: ComarkElement, state: State) { const children = node.slice(2) as ComarkNode[] diff --git a/packages/comark/src/internal/stringify/handlers/p.ts b/packages/comark/src/internal/stringify/handlers/p.ts index 0f563ab9..a4f39b49 100644 --- a/packages/comark/src/internal/stringify/handlers/p.ts +++ b/packages/comark/src/internal/stringify/handlers/p.ts @@ -1,12 +1,16 @@ import type { State } from 'comark/render' import type { ComarkElement, ComarkNode } from 'comark' -export async function p(node: ComarkElement, state: State) { +export async function p(node: ComarkElement, state: State, parent?: ComarkElement) { const children = node.slice(2) as ComarkNode[] let result = '' for (const child of children) { result += await state.one(child, state, node) } + + if (parent?.[0] === 'li') { + return result + } return result + state.context.blockSeparator } diff --git a/packages/comark/src/internal/stringify/handlers/ul.ts b/packages/comark/src/internal/stringify/handlers/ul.ts index 2b6e4e25..22672775 100644 --- a/packages/comark/src/internal/stringify/handlers/ul.ts +++ b/packages/comark/src/internal/stringify/handlers/ul.ts @@ -1,6 +1,6 @@ import type { State } from 'comark/render' import type { ComarkElement, ComarkNode } from 'comark' -import { indent } from '../indent.ts' +import { indent } from '../../../utils/index.ts' export async function ul(node: ComarkElement, state: State) { const children = node.slice(2) as ComarkNode[] diff --git a/packages/comark/src/internal/stringify/indent.ts b/packages/comark/src/internal/stringify/indent.ts deleted file mode 100644 index c59daf0f..00000000 --- a/packages/comark/src/internal/stringify/indent.ts +++ /dev/null @@ -1,15 +0,0 @@ -export function indent( - text: string, - { ignoreFirstLine = false, level = 1, width }: { ignoreFirstLine?: boolean; level?: number; width?: number } = {} -) { - const pad = width ? ' '.repeat(width) : ' '.repeat(level) - return text - .split('\n') - .map((line, index) => { - if (ignoreFirstLine && index === 0) { - return line - } - return line ? pad + line : line - }) - .join('\n') -} diff --git a/packages/comark/src/utils/index.ts b/packages/comark/src/utils/index.ts index 43888f05..b75542fa 100644 --- a/packages/comark/src/utils/index.ts +++ b/packages/comark/src/utils/index.ts @@ -88,6 +88,22 @@ export function visit( // #region String Utils +export function indent( + text: string, + { ignoreFirstLine = false, level = 1, width }: { ignoreFirstLine?: boolean; level?: number; width?: number } = {} +) { + const pad = width ? ' '.repeat(width) : ' '.repeat(level) + return text + .split('\n') + .map((line, index) => { + if (ignoreFirstLine && index === 0) { + return line + } + return line ? pad + line : line + }) + .join('\n') +} + /** * Convert a string to pascal case * @param str - The string to convert From b0e30a6874d8e2452a324cf05517fc71a967a88d Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Tue, 28 Apr 2026 16:47:19 +0200 Subject: [PATCH 2/2] chore: add list no unwrap test --- .../common-mark/unordered-list-no-unwrap.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 packages/comark/SPEC/common-mark/unordered-list-no-unwrap.md diff --git a/packages/comark/SPEC/common-mark/unordered-list-no-unwrap.md b/packages/comark/SPEC/common-mark/unordered-list-no-unwrap.md new file mode 100644 index 00000000..348be789 --- /dev/null +++ b/packages/comark/SPEC/common-mark/unordered-list-no-unwrap.md @@ -0,0 +1,50 @@ +--- +options: + autoUnwrap: false +--- + +## Input + +```md +- para1 +``` + +## AST + +```json +{ + "frontmatter": {}, + "meta": {}, + "nodes": [ + [ + "ul", + {}, + [ + "li", + {}, + [ + "p", + {}, + "para1" + ] + ] + ] + ] +} +``` + +## HTML + +```html +
            +
          • +

            para1

            +
          • +
          +``` + +## Markdown + +```md +- para1 +```