Skip to content

Markdown: task-item wrapped/indented continuation lines render as code blocks #7909

Description

@janole

Affected Packages

@tiptap/core, @tiptap/markdown, @tiptap/extension-list

Version(s)

3.23.2, 3.24.0

Bug Description

We have a local patch for this issue, which I ran into when opening a markdown note containing an LLM-generated task list, and which Claude Code then diagnosed, so please take this bug report with a grain of salt:

When a task list item has a wrapped (soft-continued) or nested line indented more than baseIndentSize (2) spaces past the marker, that content renders as an indented code block instead of joining the item's paragraph.

This is a very common authoring style — aligning the continuation under the checkbox text column (- [ ] is 6 columns wide), which is also what many LLMs emit. Plain bullet (-) lists are unaffected (they go through marked's native list tokenizer); only task items hit parseIndentedBlocks. CommonMark renderers (remark/react-markdown) handle the same input correctly, because an indented code block cannot interrupt a paragraph.

Input markdown:

- [ ] Agent tab renders the composer in all states (no session
      empty transcript + composer).

Result: the second line (empty transcript + composer).) becomes a code block under the task item instead of being merged into the item's text.

Root cause — in @tiptap/core's parseIndentedBlocks (packages/core/src/utilities/markdown/parseIndentedBlocks.ts), nested content is dedented by a fixed amount:

const dedentedNested = nestedContent
    .map(nestedLine => nestedLine.slice(indentLevel + baseIndentSize)) // Remove base indent + 2 spaces
    .join("\n");

When the continuation is indented more than baseIndentSize past the marker, residual leading whitespace (≥4 spaces) survives the dedent, and the downstream marked.blockTokens(...) (or the recursive task parser's fallback) parses it as an indented code block.

Minimal, DOM-free repro of the root cause:

import { marked } from "marked";

// Continuation line collected by parseIndentedBlocks for the item above
// (indented to the checkbox text column = 6 spaces).
const nested = ["      empty transcript + composer)."];
const indentLevel = 0;     // from the item pattern match
const baseIndentSize = 2;  // default

// CURRENT behavior: dedent by a FIXED indentLevel + baseIndentSize (=2) -> 4 spaces survive
const dedented = nested.map((l) => l.slice(indentLevel + baseIndentSize)).join("\n");
console.log(JSON.stringify(dedented)); // "    empty transcript + composer)."
console.log(new marked.Lexer().blockTokens(dedented).map((t) => t.type));
// -> [ 'code' ]   ❌  expected a paragraph

Suggested fix — dedent by the actual minimum indentation of the non-empty nested lines (preserving relative nesting), instead of a fixed indentLevel + baseIndentSize. The mixed bullet/task path in @tiptap/markdown already does exactly this (Math.min(...indents)), so this also removes an internal inconsistency:

const nestedIndents = nestedContent
    .filter(nestedLine => nestedLine.trim())
    .map(nestedLine => nestedLine.length - nestedLine.trimStart().length);
const nestedDedent = nestedIndents.length > 0
    ? Math.min(...nestedIndents)
    : indentLevel + baseIndentSize;
const dedentedNested = nestedContent
    .map(nestedLine => (nestedLine.trim() ? nestedLine.slice(nestedDedent) : ""))
    .join("\n");

With this change the repro yields a paragraph, and genuinely-nested sub-lists/sub-tasks still dedent correctly (relative indentation preserved); fenced code blocks (```) are unaffected.

Related: #7220 / PR #7225 — same function (parseIndentedBlocks), different defect.

Browser Used

Chrome

Code Example URL

https://codesandbox.io/p/devbox/adoring-wright-kmqhlt

Expected Behavior

The wrapped/over-indented task-item continuation joins the item's paragraph (matching CommonMark and marked's native list handling), rather than rendering as an indented code block.

Additional Context (Optional)

No response

Dependency Updates

  • Yes, I've updated all my dependencies.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: markdownMarkdown parsing and serializationcomplexity: mediumModerate change, possibly multiple filesimpact: mediumAffects some users or workflowsstatus: triageNeeds initial review and categorization

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions