Skip to content

feat: replace unified.js with custom markdown parser/serializer#2624

Open
nperez0111 wants to merge 6 commits intomainfrom
feat/custom-md-parser
Open

feat: replace unified.js with custom markdown parser/serializer#2624
nperez0111 wants to merge 6 commits intomainfrom
feat/custom-md-parser

Conversation

@nperez0111
Copy link
Copy Markdown
Contributor

@nperez0111 nperez0111 commented Apr 3, 2026

Summary

Replace the unified.js ecosystem (~89 transitive packages) with two custom, zero-dependency files for bidirectional markdown↔HTML conversion in BlockNote.

Rationale

The unified/remark/rehype pipeline was the largest single dependency ecosystem in BlockNote, yet was used in only 2 files with a narrow feature set. The pipeline built two unnecessary intermediate ASTs (MDAST + HAST) for what is fundamentally a direct markdown↔HTML conversion, and required 6 custom handlers/plugins to work around its abstractions.

Changes

  • New markdownToHtml.ts (~600 LOC): Custom markdown→HTML parser with two-pass architecture (block tokenizer + inline parser), recursive list handling, GFM table support, and video URL detection
  • New htmlToMarkdown.ts (~500 LOC): Custom HTML→markdown serializer using DOMParser with proper list nesting, table column padding, toggle item handling, and lossy conversion support
  • Removed 12 direct dependencies: unified, remark-parse, remark-stringify, remark-gfm, remark-rehype, rehype-parse, rehype-stringify, rehype-remark, rehype-format, hast-util-from-dom, unist-util-visit, @types/hast
  • Deleted 3 custom rehype plugins: convertVideoToMarkdownRehypePlugin, removeUnderlinesRehypePlugin, addSpacesToCheckboxesRehypePlugin (their logic is now built into the serializer)
  • Removed unified deps from @blocknote/xl-ai: unified, remark-parse, remark-stringify
  • Expanded test coverage: Added 100+ new test cases for markdown parsing (headings, code blocks, tables, task lists, nested lists, inline styles, etc.) and export/parse equality round-trip tests
  • Minor behavioral improvement: Trailing spaces in emphasis are moved outside delimiters (**Bold** Heading) instead of escaped with HTML entities (**Bold **Heading), producing cleaner, more readable markdown

Impact

  • Eliminates ~89 transitive packages from the dependency tree
  • All existing markdown features preserved: GFM tables, strikethrough, task lists, nested lists, code blocks with language, blockquotes, images, video URL detection, hard breaks
  • Lossy conversions (colors, underlines, alignment) behave identically
  • The nestedLists.test.ts no longer uses unified for pretty-printing (uses raw innerHTML comparison instead)

Testing

  • 672 core unit tests passing (0 failures)
  • 136 markdown parse tests (headings, lists, tables, emphasis, code blocks, blockquotes, links, images, videos, etc.)
  • 328 export tests (paragraphs, lists, code blocks, tables, media, inline styles, hard breaks, etc.)
  • 113 export/parse equality round-trip tests
  • Only 1 existing snapshot changed (complex/misc.md) due to the cleaner emphasis handling
  • TypeScript compilation passes with no errors

Screenshots/Video

N/A - internal serialization change with no UI impact.

Checklist

  • Code follows the project's coding standards.
  • Unit tests covering the new feature have been added.
  • All existing tests pass.
  • The documentation has been updated to reflect the new feature

Additional Notes

The custom parser handles all CommonMark + GFM features that BlockNote needs: ATX/setext headings, fenced code blocks, GFM pipe tables, nested lists (with recursive content parsing for wide markers like * ), task lists, blockquotes, emphasis (bold/italic/strikethrough), inline code, links, images with video URL detection, hard line breaks, and backslash escapes. Features not needed by BlockNote (footnotes, definition lists, math, frontmatter, reference-style links) are intentionally omitted.

Summary by CodeRabbit

  • New Features

    • Added built-in HTML→Markdown and Markdown→HTML converters with richer handling of headings, nested/task lists, tables, code blocks, blockquotes, media, and inline formatting.
  • Chores

    • Replaced the prior external processing pipeline and removed several now-unused parsing/rehype/remark runtime dev dependencies and related helper plugins.
  • Tests

    • Greatly expanded Markdown export/import round‑trip test coverage and introduced file‑based snapshot validation for Markdown conversions.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blocknote Error Error Apr 11, 2026 4:15am
blocknote-website Ready Ready Preview Apr 11, 2026 4:15am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces unified/remark/rehype pipelines with self-contained TypeScript Markdown↔HTML converters, removes several rehype/remark plugins and related deps, updates exporter/parser integrations to call the new converters, and expands Markdown/HTML round‑trip tests and fixtures.

Changes

Cohort / File(s) Summary
Dependency Removals
packages/core/package.json, packages/xl-ai/package.json
Removed unified, remark-*, rehype-*, unist-util-visit, hast type dep, and related runtime/dev dependencies.
New Markdown→HTML
packages/core/src/api/parsers/markdown/markdownToHtml.ts
Added a standalone tokenizer + HTML emitter: ATX/setext headings, paragraphs, fenced code (with language), blockquotes, lists (ordered/bullet/task with nesting/start), GFM pipe tables with alignment, inline parsing (emphasis, code, links, images/videos) and exported markdownToHtml(markdown: string).
New HTML→Markdown
packages/core/src/api/exporters/markdown/htmlToMarkdown.ts
Added DOM-based HTML→Markdown serializer that recursively walks DOM nodes and emits GFM-like Markdown for block and inline elements; exports htmlToMarkdown(html: string).
Removed Rehype Plugins
packages/core/src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.ts, packages/core/src/api/exporters/markdown/util/convertVideoToMarkdownRehypePlugin.ts, packages/core/src/api/exporters/markdown/util/removeUnderlinesRehypePlugin.ts
Deleted plugins previously handling checkbox spacing, video→image conversion, and <u> stripping; those behaviors are incorporated into the new converters.
Exporter / Parser Integrations
packages/core/src/api/exporters/markdown/markdownExporter.ts, packages/core/src/api/parsers/markdown/parseMarkdown.ts
Replaced inline unified/rehype/remark pipelines with delegations to htmlToMarkdown() and markdownToHtml(); removed inline custom handlers and pipeline code.
Tests — Fixtures & Executors
tests/src/unit/core/formatConversion/export/exportTestInstances.ts, tests/src/unit/core/formatConversion/parse/parseTestInstances.ts, tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts, tests/src/unit/shared/formatConversion/exportParseEquality/exportParseEqualityTestExecutors.ts
Greatly expanded HTML export fixtures and Markdown parser cases; added Markdown round‑trip test instances and new executor testExportParseEqualityMarkdown which snapshots lossy Markdown round-trips to files.
Test Registration / Adjustments
tests/src/unit/core/formatConversion/exportParseEquality/runTests.test.ts, tests/src/unit/core/formatConversion/parse/nestedLists.test.ts
Added Vitest suite for Markdown export/parse equality; simplified nested-lists test to snapshot raw innerHTML (removed unified formatting step).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 I hopped through tags and tokens bright,

Swapped tangled plugins for code that’s light,
From markdown fields to HTML glade,
I stitched the paths that tests parade,
A tiny rabbit, converters made.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: replacing unified.js with a custom markdown parser/serializer, which is the central objective of this PR.
Description check ✅ Passed The description comprehensively covers Summary, Rationale, Changes, Impact, and Testing sections with specific details about code additions, dependency removals, test coverage, and behavioral changes. Documentation update status is noted as incomplete.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/custom-md-parser

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/src/unit/core/formatConversion/export/exportTestInstances.ts (1)

1148-2630: Consider extracting a helper for table cell creation.

The table tests provide excellent coverage of headers, spans, colors, alignment, and inline content. However, the default cell props are repeated extensively. A helper function could reduce boilerplate while maintaining readability.

💡 Optional helper function example
// Could be added at the top of the file or in a shared test utilities module
const createTableCell = (
  content: string | any[],
  overrides: Partial<{
    backgroundColor: string;
    colspan: number;
    rowspan: number;
    textAlignment: string;
    textColor: string;
  }> = {}
) => ({
  type: "tableCell" as const,
  content: typeof content === "string" ? [content] : content,
  props: {
    backgroundColor: "default",
    colspan: 1,
    rowspan: 1,
    textAlignment: "left",
    textColor: "default",
    ...overrides,
  },
});

// Usage example:
// createTableCell("Table Cell")
// createTableCell("Bold", { backgroundColor: "red", textColor: "blue" })

This is purely optional - the explicit definitions are also valid for test clarity and snapshot stability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/src/unit/core/formatConversion/export/exportTestInstances.ts` around
lines 1148 - 2630, Tests repeat the same default table cell object structure
many times; extract a small helper (e.g., createTableCell) and replace repeated
tableCell objects with calls to it to reduce boilerplate. Implement a helper
that accepts content (string or node array) and an overrides partial for props
(backgroundColor, colspan, rowspan, textAlignment, textColor), returns an object
with type: "tableCell", content normalized to an array, and props merged with
defaults, then update table test cases (used by testExportBlockNoteHTML) to call
createTableCell(...) instead of inlining identical props.
tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts (1)

391-827: Comprehensive test coverage for the new markdown parser/serializer.

The test cases cover the key markdown features well: block types, inline styles, nested structures, and a complex integration test. The structure is consistent and follows established patterns.

Optional: Consider adding edge case tests for deeper nesting (3+ levels) and special character escaping (e.g., *asterisks* within content, backticks in code) if the custom parser handles these scenarios. These could be added in a follow-up to ensure robustness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts`
around lines 391 - 827, Add tests for deeper nesting and special-character
escaping to the existing markdown test suite by appending new testCase entries
(executed with testExportParseEqualityMarkdown) to the array in
exportParseEqualityTestInstances.ts; specifically add a
"markdown/deeplyNestedLists" case with 3+ nested list items (use types like
bulletListItem and numberedListItem and children arrays) and a
"markdown/specialCharEscaping" case containing inline text nodes with literal
asterisks, backticks, and other markdown-significant characters (and a codeBlock
containing backticks) so the parser/serializer round-trip covers escaping and
nested-depth edge cases referenced by testExportParseEqualityMarkdown.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/api/parsers/markdown/markdownToHtml.ts`:
- Around line 128-143: The underscore delimiter is being treated the same as `*`
in markdownToHtml, causing intraword emphasis like `snake_case` to be rendered
incorrectly; before calling parseDelimited(text, i, delimiter, "<em>", "</em>")
when delimiter === "_" check the left- and right-flanking status (i.e., inspect
the previous and next characters and determine if both sides are alphanumeric)
and skip parsing if the delimiter is intraword (both left- and right-flanking).
Implement this by using or adding an isAlphanumeric(char) helper and computing
leftFlanking/rightFlanking from text[i-1] and text[i+1] (being careful at
bounds), and only call parseDelimited for "_" when it is not intraword; keep
behavior for "*" unchanged.

---

Nitpick comments:
In `@tests/src/unit/core/formatConversion/export/exportTestInstances.ts`:
- Around line 1148-2630: Tests repeat the same default table cell object
structure many times; extract a small helper (e.g., createTableCell) and replace
repeated tableCell objects with calls to it to reduce boilerplate. Implement a
helper that accepts content (string or node array) and an overrides partial for
props (backgroundColor, colspan, rowspan, textAlignment, textColor), returns an
object with type: "tableCell", content normalized to an array, and props merged
with defaults, then update table test cases (used by testExportBlockNoteHTML) to
call createTableCell(...) instead of inlining identical props.

In
`@tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts`:
- Around line 391-827: Add tests for deeper nesting and special-character
escaping to the existing markdown test suite by appending new testCase entries
(executed with testExportParseEqualityMarkdown) to the array in
exportParseEqualityTestInstances.ts; specifically add a
"markdown/deeplyNestedLists" case with 3+ nested list items (use types like
bulletListItem and numberedListItem and children arrays) and a
"markdown/specialCharEscaping" case containing inline text nodes with literal
asterisks, backticks, and other markdown-significant characters (and a codeBlock
containing backticks) so the parser/serializer round-trip covers escaping and
nested-depth edge cases referenced by testExportParseEqualityMarkdown.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d6a99fa6-56ef-4eec-b935-b34f2a74e929

📥 Commits

Reviewing files that changed from the base of the PR and between 38be5fd and f9da2df.

⛔ Files ignored due to path filters (248)
  • packages/core/src/api/parsers/html/util/__snapshots__/nestedLists.test.ts.snap is excluded by !**/*.snap, !**/__snapshots__/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/audio/basic.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/audio/button.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/audio/noName.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/complex/document.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h1.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h2.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h3.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h4.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h5.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h6.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/styled.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/toggleable.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/withCaption.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/withCode.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/lists/numberedListStart.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/paragraph/multiple.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/basic.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/multiple.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/nested.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/styled.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/withLink.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/backgroundColor.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/bold.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/boldItalicStrike.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/code.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/combined.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/italic.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/mixedInParagraph.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/strike.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/textColor.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/underline.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/advancedExample.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/cellTextAlignment.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/emptyCells.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/hardBreakInCell.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/headerRowsAndCols.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/linksInCells.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/singleCell.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/styledCellContent.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/video/withCaption.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/basic.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/button.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/noName.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/complex/document.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h1.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h2.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h3.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h4.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h5.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h6.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/styled.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/toggleable.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/image/withCaption.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/link/withCode.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/lists/numberedListStart.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/multiple.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/basic.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/multiple.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/nested.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/styled.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/withLink.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/backgroundColor.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/bold.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/boldItalicStrike.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/code.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/combined.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/italic.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/mixedInParagraph.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/strike.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/textColor.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/underline.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/table/advancedExample.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/table/cellTextAlignment.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/table/emptyCells.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/table/hardBreakInCell.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/table/headerRowsAndCols.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/table/linksInCells.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/table/singleCell.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/table/styledCellContent.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/video/withCaption.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/basic.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/button.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/noName.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/complex/document.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/complex/misc.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h1.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h2.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h3.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h4.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h5.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h6.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/styled.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/toggleable.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/withCaption.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/link/withCode.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/lists/numberedListStart.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/paragraph/multiple.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/basic.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/multiple.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/nested.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/styled.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/withLink.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/backgroundColor.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/bold.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/boldItalicStrike.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/code.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/combined.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/italic.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/mixedInParagraph.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/strike.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/textColor.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/underline.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/advancedExample.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/cellTextAlignment.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/emptyCells.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/hardBreakInCell.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/headerRowsAndCols.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/linksInCells.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/singleCell.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/styledCellContent.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/video/withCaption.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/audio/basic.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/audio/button.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/audio/noName.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/complex/document.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h1.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h2.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h3.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h4.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h5.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h6.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/styled.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/toggleable.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/image/withCaption.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/withCode.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/lists/numberedListStart.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/paragraph/multiple.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/basic.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/multiple.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/nested.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/styled.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/withLink.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/backgroundColor.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/bold.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/boldItalicStrike.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/code.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/combined.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/italic.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/mixedInParagraph.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/strike.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/textColor.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/underline.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/advancedExample.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/cellTextAlignment.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/emptyCells.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/hardBreakInCell.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/headerRowsAndCols.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/linksInCells.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/singleCell.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/styledCellContent.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/nodes/video/withCaption.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/bold.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/boldItalic.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/bulletList.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/checkList.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/codeBlock.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/complexDocument.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/divider.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/hardBreak.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/headingLevels.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/image.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/inlineCode.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/italic.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/link.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/mixedStyles.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/multipleParagraphs.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/nestedLists.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/numberedList.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/paragraph.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/quote.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/strike.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/table.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/video.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/adjacentFormattedRuns.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/adjacentLinks.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/backslashEscapes.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteMultiline.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteWithCode.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteWithLink.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/boldOnly.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/boldUnderscore.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/checkListBasic.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/checkListMixed.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/checkListNested.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockBasic.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockPython.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockTildes.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockWithLanguage.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockWithSpecialChars.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/complexDocument.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/deeplyNestedLists.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/emptyString.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/hardBreakBackslash.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/hardBreakMultiple.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH1.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH2.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH3.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH4.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH5.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH6.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingThenCode.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingWithInlineStyles.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/horizontalRuleAsterisks.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/horizontalRuleDashes.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/horizontalRuleUnderscores.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/imageWithAlt.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineCode.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineCodeWithSpecialChars.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineImage.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/italicOnly.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/italicUnderscore.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkAndText.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkBasic.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkInParagraph.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkWithStyledContent.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/listWithStyledItems.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/mixedInlineContent.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/mixedListTypes.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/multipleImages.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/multipleParagraphs.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedBulletLists.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedEmphasis.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedEmphasisComplex.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedOrderedLists.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/onlyWhitespace.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/orderedListStart.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/paragraphContinuation.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/setextH1.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/setextH2.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/strikethroughOnly.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableAlignment.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableBasic.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableFollowedByParagraph.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableThreeColumns.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableWithInlineFormatting.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableWithLinks.json is excluded by !**/__snapshots__/**
📒 Files selected for processing (15)
  • packages/core/package.json
  • packages/core/src/api/exporters/markdown/htmlToMarkdown.ts
  • packages/core/src/api/exporters/markdown/markdownExporter.ts
  • packages/core/src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.ts
  • packages/core/src/api/exporters/markdown/util/convertVideoToMarkdownRehypePlugin.ts
  • packages/core/src/api/exporters/markdown/util/removeUnderlinesRehypePlugin.ts
  • packages/core/src/api/parsers/html/util/nestedLists.test.ts
  • packages/core/src/api/parsers/markdown/markdownToHtml.ts
  • packages/core/src/api/parsers/markdown/parseMarkdown.ts
  • packages/xl-ai/package.json
  • tests/src/unit/core/formatConversion/export/exportTestInstances.ts
  • tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts
  • tests/src/unit/core/formatConversion/exportParseEquality/runTests.test.ts
  • tests/src/unit/core/formatConversion/parse/parseTestInstances.ts
  • tests/src/unit/shared/formatConversion/exportParseEquality/exportParseEqualityTestExecutors.ts
💤 Files with no reviewable changes (5)
  • packages/xl-ai/package.json
  • packages/core/package.json
  • packages/core/src/api/exporters/markdown/util/removeUnderlinesRehypePlugin.ts
  • packages/core/src/api/exporters/markdown/util/convertVideoToMarkdownRehypePlugin.ts
  • packages/core/src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/api/exporters/markdown/htmlToMarkdown.ts`:
- Around line 368-370: The table cell content is inserted verbatim into the
Markdown row, so characters like "|" and embedded newlines break GFM tables;
update the logic around serializeInlineContent(cell as HTMLElement) to escape
pipe characters and replace or encode newlines (e.g., convert "\n" to "<br>" or
"\n" to "\\n") before using colspan/rowspan to lay out the row; modify the code
that reads content, colspan, and rowspan so the escaped content is used for
building the row string (also apply the same escaping fix in the similar block
around the serializeInlineContent usage at the other location mentioned).
- Around line 460-467: The serializer currently injects raw alt/src/text/href
into markdown (see serializeImage and other serializers handling textContent,
alt, href, src), which can produce accidental headings, lists or break
link/image syntax; add and use escaping helpers (e.g., escapeLabel for
link/image labels, escapeDestination for URLs/targets, and escapeText for
paragraph/text runs) that 1) escape characters like ] and ) in
labels/destinations, 2) percent-encode or backslash-escape problematic
characters in destinations, and 3) prefix or escape leading markdown markers
(like "# ", "1. ") in textContent; then update serializeImage to call
escapeLabel(alt) and escapeDestination(src) before building `![...]()` and apply
the same helpers in the other affected serializers referenced (the branches
around textContent, href handling and the ranges you noted).
- Around line 593-595: The inline code handler in htmlToMarkdown produces
malformed markdown when the code contains backticks; update the "code" case in
htmlToMarkdown to compute the longest run of backticks in childEl.textContent
(e.g., scan with /`+/g), pick a delimiter of backticks one longer than that run,
and if the content begins or ends with a backtick or whitespace pad the content
with a single space inside the delimiters; then join delimiter + paddedContent +
delimiter instead of always a single backtick. Ensure you operate on
childEl.textContent (or "" fallback) and preserve original content otherwise.
- Around line 674-680: trimHardBreaks currently strips any trailing backslash
with result = result.replace(/\\$/, ""), which removes legitimate trailing
backslashes (e.g., paths/regex). Remove that final replacement and only trim
leading/trailing backslash+newline sequences (the existing /^(\\\n)+/ and
/(\\\n)+$/ patterns) so real trailing "\" characters are preserved; update tests
for trimHardBreaks to cover strings that legitimately end with a backslash.
- Around line 147-172: serializeCodeBlock always emits triple backticks which
breaks when the code contains ```; modify serializeCodeBlock to compute a fence
string longer than any run of backticks inside the extracted code (e.g., find
the longest consecutive sequence of '`' in code and create fence as that
length+1 backticks), then use that fence variable in place of the hardcoded
"```" when opening and closing the block and when returning the empty-block
case, keeping language and indent handling the same; update references to
code.endsWith("\n") logic to still append a single newline inside the fence
before the closing fence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 460294a2-949b-4d1a-a661-02f96ad629c6

📥 Commits

Reviewing files that changed from the base of the PR and between f9da2df and 837e367.

⛔ Files ignored due to path filters (3)
  • packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.test.ts.snap is excluded by !**/*.snap, !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/deeplyNestedLists.json is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/specialCharEscaping.json is excluded by !**/__snapshots__/**
📒 Files selected for processing (3)
  • packages/core/src/api/exporters/markdown/htmlToMarkdown.ts
  • packages/core/src/api/parsers/markdown/markdownToHtml.ts
  • tests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts

Comment on lines +147 to +172
function serializeCodeBlock(el: HTMLElement, ctx: SerializeContext): string {
const codeEl = el.querySelector("code");
if (!codeEl) {return "";}

const language =
codeEl.getAttribute("data-language") ||
extractLanguageFromClass(codeEl.className) ||
"";

// Extract code content, handling <br> elements as newlines
const code = extractCodeContent(codeEl);

// For empty code blocks, don't add a newline between the fences
if (!code) {
return ctx.indent + "```" + language + "\n```\n\n";
}

return (
ctx.indent +
"```" +
language +
"\n" +
code +
(code.endsWith("\n") ? "" : "\n") +
"```\n\n"
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Choose a fence that is longer than the code payload.

Always emitting triple backticks breaks as soon as the code block itself contains ```; the fence closes early and the remainder is serialized as normal markdown.

Possible fix
 function serializeCodeBlock(el: HTMLElement, ctx: SerializeContext): string {
   const codeEl = el.querySelector("code");
   if (!codeEl) {return "";}
@@
   // Extract code content, handling <br> elements as newlines
   const code = extractCodeContent(codeEl);
+  const longestRun = Math.max(
+    0,
+    ...((code.match(/`+/g) ?? []).map((run) => run.length))
+  );
+  const fence = "`".repeat(Math.max(3, longestRun + 1));
@@
   // For empty code blocks, don't add a newline between the fences
   if (!code) {
-    return ctx.indent + "```" + language + "\n```\n\n";
+    return ctx.indent + fence + language + "\n" + fence + "\n\n";
   }
@@
     ctx.indent +
-    "```" +
+    fence +
     language +
     "\n" +
     code +
     (code.endsWith("\n") ? "" : "\n") +
-    "```\n\n"
+    fence +
+    "\n\n"
   );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function serializeCodeBlock(el: HTMLElement, ctx: SerializeContext): string {
const codeEl = el.querySelector("code");
if (!codeEl) {return "";}
const language =
codeEl.getAttribute("data-language") ||
extractLanguageFromClass(codeEl.className) ||
"";
// Extract code content, handling <br> elements as newlines
const code = extractCodeContent(codeEl);
// For empty code blocks, don't add a newline between the fences
if (!code) {
return ctx.indent + "```" + language + "\n```\n\n";
}
return (
ctx.indent +
"```" +
language +
"\n" +
code +
(code.endsWith("\n") ? "" : "\n") +
"```\n\n"
);
function serializeCodeBlock(el: HTMLElement, ctx: SerializeContext): string {
const codeEl = el.querySelector("code");
if (!codeEl) {return "";}
const language =
codeEl.getAttribute("data-language") ||
extractLanguageFromClass(codeEl.className) ||
"";
// Extract code content, handling <br> elements as newlines
const code = extractCodeContent(codeEl);
const longestRun = Math.max(
0,
...((code.match(/`+/g) ?? []).map((run) => run.length))
);
const fence = "`".repeat(Math.max(3, longestRun + 1));
// For empty code blocks, don't add a newline between the fences
if (!code) {
return ctx.indent + fence + language + "\n" + fence + "\n\n";
}
return (
ctx.indent +
fence +
language +
"\n" +
code +
(code.endsWith("\n") ? "" : "\n") +
fence +
"\n\n"
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/api/exporters/markdown/htmlToMarkdown.ts` around lines 147
- 172, serializeCodeBlock always emits triple backticks which breaks when the
code contains ```; modify serializeCodeBlock to compute a fence string longer
than any run of backticks inside the extracted code (e.g., find the longest
consecutive sequence of '`' in code and create fence as that length+1
backticks), then use that fence variable in place of the hardcoded "```" when
opening and closing the block and when returning the empty-block case, keeping
language and indent handling the same; update references to code.endsWith("\n")
logic to still append a single newline inside the fence before the closing
fence.

Comment on lines +368 to +370
const content = serializeInlineContent(cell as HTMLElement).trim();
const colspan = parseInt(cell.getAttribute("colspan") || "1", 10);
const rowspan = parseInt(cell.getAttribute("rowspan") || "1", 10);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Escape table-cell content before laying out the row.

Cell text is inserted verbatim, so | creates extra columns and embedded newlines split the row. That makes otherwise valid table content serialize into invalid GFM.

Possible fix
-      const content = serializeInlineContent(cell as HTMLElement).trim();
+      const content = escapeTableCell(
+        serializeInlineContent(cell as HTMLElement).trim()
+      );
@@
 function formatTableRow(
   cells: string[],
   colWidths: number[],
   colCount: number
 ): string {
@@
   return "|" + parts.join("|") + "|";
 }
+
+function escapeTableCell(text: string): string {
+  return text
+    .replace(/\\/g, "\\\\")
+    .replace(/\|/g, "\\|")
+    .replace(/\r?\n/g, "<br>");
+}

Also applies to: 437-447

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/api/exporters/markdown/htmlToMarkdown.ts` around lines 368
- 370, The table cell content is inserted verbatim into the Markdown row, so
characters like "|" and embedded newlines break GFM tables; update the logic
around serializeInlineContent(cell as HTMLElement) to escape pipe characters and
replace or encode newlines (e.g., convert "\n" to "<br>" or "\n" to "\\n")
before using colspan/rowspan to lay out the row; modify the code that reads
content, colspan, and rowspan so the escaped content is used for building the
row string (also apply the same escaping fix in the similar block around the
serializeInlineContent usage at the other location mentioned).

Comment on lines +460 to +467
function serializeImage(el: HTMLElement, ctx: SerializeContext): string {
const src = el.getAttribute("src") || "";
const alt = el.getAttribute("alt") || "";
if (!src) {
return ctx.indent + "Add image\n\n";
}
return ctx.indent + `![${alt}](${src})\n\n`;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Escape raw text, labels, and destinations before interpolating markdown.

These branches concatenate textContent, alt, href, and src directly into markdown syntax. A literal paragraph that starts with # or 1. becomes a heading/list item on export, and ] / ) in labels or URLs will break link and image syntax instead of round-tripping literally.

Also applies to: 524-529, 555-645

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/api/exporters/markdown/htmlToMarkdown.ts` around lines 460
- 467, The serializer currently injects raw alt/src/text/href into markdown (see
serializeImage and other serializers handling textContent, alt, href, src),
which can produce accidental headings, lists or break link/image syntax; add and
use escaping helpers (e.g., escapeLabel for link/image labels, escapeDestination
for URLs/targets, and escapeText for paragraph/text runs) that 1) escape
characters like ] and ) in labels/destinations, 2) percent-encode or
backslash-escape problematic characters in destinations, and 3) prefix or escape
leading markdown markers (like "# ", "1. ") in textContent; then update
serializeImage to call escapeLabel(alt) and escapeDestination(src) before
building `![...]()` and apply the same helpers in the other affected serializers
referenced (the branches around textContent, href handling and the ranges you
noted).

Comment on lines +593 to +595
case "code":
result += "`" + (childEl.textContent || "") + "`";
break;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inline code spans need dynamic delimiters too.

A literal backtick inside <code> currently produces malformed markdown. Inline code needs a fence longer than the longest backtick run in the payload, plus padding when the content starts or ends with backticks or whitespace.

Possible fix
-        case "code":
-          result += "`" + (childEl.textContent || "") + "`";
-          break;
+        case "code": {
+          const text = childEl.textContent || "";
+          const longestRun = Math.max(
+            0,
+            ...((text.match(/`+/g) ?? []).map((run) => run.length))
+          );
+          const fence = "`".repeat(longestRun + 1);
+          const needsPadding =
+            text.startsWith("`") ||
+            text.endsWith("`") ||
+            /^\s/.test(text) ||
+            /\s$/.test(text);
+          result += fence + (needsPadding ? ` ${text} ` : text) + fence;
+          break;
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case "code":
result += "`" + (childEl.textContent || "") + "`";
break;
case "code": {
const text = childEl.textContent || "";
const longestRun = Math.max(
0,
...((text.match(/`+/g) ?? []).map((run) => run.length))
);
const fence = "`".repeat(longestRun + 1);
const needsPadding =
text.startsWith("`") ||
text.endsWith("`") ||
/^\s/.test(text) ||
/\s$/.test(text);
result += fence + (needsPadding ? ` ${text} ` : text) + fence;
break;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/api/exporters/markdown/htmlToMarkdown.ts` around lines 593
- 595, The inline code handler in htmlToMarkdown produces malformed markdown
when the code contains backticks; update the "code" case in htmlToMarkdown to
compute the longest run of backticks in childEl.textContent (e.g., scan with
/`+/g), pick a delimiter of backticks one longer than that run, and if the
content begins or ends with a backtick or whitespace pad the content with a
single space inside the delimiters; then join delimiter + paddedContent +
delimiter instead of always a single backtick. Ensure you operate on
childEl.textContent (or "" fallback) and preserve original content otherwise.

Comment on lines +674 to +680
function trimHardBreaks(content: string): string {
// Remove leading hard breaks
let result = content.replace(/^(\\\n)+/, "");
// Remove trailing hard breaks (including trailing backslash)
result = result.replace(/(\\\n)+$/, "");
result = result.replace(/\\$/, "");
return result;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don’t drop a real trailing backslash.

The final replacement removes any paragraph that legitimately ends with \, not just a serialized <br>. That corrupts content like Windows paths or regex snippets.

Possible fix
 function trimHardBreaks(content: string): string {
   // Remove leading hard breaks
   let result = content.replace(/^(\\\n)+/, "");
-  // Remove trailing hard breaks (including trailing backslash)
+  // Remove trailing hard breaks produced by `<br>`
   result = result.replace(/(\\\n)+$/, "");
-  result = result.replace(/\\$/, "");
   return result;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function trimHardBreaks(content: string): string {
// Remove leading hard breaks
let result = content.replace(/^(\\\n)+/, "");
// Remove trailing hard breaks (including trailing backslash)
result = result.replace(/(\\\n)+$/, "");
result = result.replace(/\\$/, "");
return result;
function trimHardBreaks(content: string): string {
// Remove leading hard breaks
let result = content.replace(/^(\\\n)+/, "");
// Remove trailing hard breaks produced by `<br>`
result = result.replace(/(\\\n)+$/, "");
return result;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/api/exporters/markdown/htmlToMarkdown.ts` around lines 674
- 680, trimHardBreaks currently strips any trailing backslash with result =
result.replace(/\\$/, ""), which removes legitimate trailing backslashes (e.g.,
paths/regex). Remove that final replacement and only trim leading/trailing
backslash+newline sequences (the existing /^(\\\n)+/ and /(\\\n)+$/ patterns) so
real trailing "\" characters are preserved; update tests for trimHardBreaks to
cover strings that legitimately end with a backslash.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 3, 2026

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/@blocknote/ariakit@2624

@blocknote/code-block

npm i https://pkg.pr.new/@blocknote/code-block@2624

@blocknote/core

npm i https://pkg.pr.new/@blocknote/core@2624

@blocknote/mantine

npm i https://pkg.pr.new/@blocknote/mantine@2624

@blocknote/react

npm i https://pkg.pr.new/@blocknote/react@2624

@blocknote/server-util

npm i https://pkg.pr.new/@blocknote/server-util@2624

@blocknote/shadcn

npm i https://pkg.pr.new/@blocknote/shadcn@2624

@blocknote/xl-ai

npm i https://pkg.pr.new/@blocknote/xl-ai@2624

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@2624

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/@blocknote/xl-email-exporter@2624

@blocknote/xl-multi-column

npm i https://pkg.pr.new/@blocknote/xl-multi-column@2624

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@2624

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@2624

commit: f067aee

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/api/parsers/markdown/markdownToHtml.ts`:
- Around line 753-757: The table-detection regexes for separatorLine and
headerLine are too strict because they require outer pipes and thus reject valid
GFM rows like "a | b"; update the checks in markdownToHtml.ts to accept rows
with or without leading/trailing pipes by relaxing the regular expressions (the
ones testing separatorLine and headerLine) to allow optional outer pipes,
mirroring parsePipeCells() behavior; apply the same fix to the equivalent checks
around the other occurrence (lines referenced in the comment) so both headerLine
and separatorLine validations permit missing outer pipes.
- Around line 482-499: The fenced-code detection currently uses
/^(`{3,}|~{3,})(.*)$/ so indented fences (up to three leading spaces) are
ignored; update the opening and closing fence regexes used in the block that
computes fenceMatch, fenceChar, fenceLen and the closingMatch RegExp to allow up
to three optional leading spaces (CommonMark: at most three). Specifically,
change the opening match to accept 0–3 leading spaces before the backtick/tilde
fence and change the closing RegExp built from fenceChar/fenceLen to also allow
0–3 leading spaces before the closing fence so lines like "  ```ts" correctly
enter the codeLines collection.
- Around line 301-309: The extracted url string (variable url in the
markdownToHtml logic using urlStart/parenEnd and findClosingParen) may include
an optional title like "title" after the destination, which currently becomes
part of the href and breaks links; fix by trimming the extracted url to only the
destination: if url begins with '<' and ends with '>' strip the angle-brackets,
otherwise split the url at the first unescaped whitespace (space or newline) and
keep the first token as the destination, then continue to pass that sanitized
string into escapeHtml(...) when building the <a> tag; update the logic near url
= text.substring(urlStart, parenEnd).trim() to perform this sanitization before
using escapeHtml(url) (keep parseInline(linkText) unchanged).
- Around line 253-260: The alt text detection in markdownToHtml.ts uses
text.indexOf("]", altStart) which fails on nested or escaped brackets; replace
that simple indexOf with the balanced-bracket helper used by parseLink() (the
same function that parseLink() calls to locate the matching closing bracket) so
altEnd is found with proper nested/escaped handling, keep the existing null
return when the helper returns -1, and then continue to the existing "(" check
and url parsing that uses findClosingParen.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7eba7c7b-8711-42b0-ac96-1536f5cdb8bd

📥 Commits

Reviewing files that changed from the base of the PR and between 7c3e17d and ec506e5.

⛔ Files ignored due to path filters (1)
  • tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteLazyContinuation.json is excluded by !**/__snapshots__/**
📒 Files selected for processing (2)
  • packages/core/src/api/parsers/markdown/markdownToHtml.ts
  • tests/src/unit/core/formatConversion/parse/parseTestInstances.ts
✅ Files skipped from review due to trivial changes (1)
  • tests/src/unit/core/formatConversion/parse/parseTestInstances.ts

nperez0111 and others added 6 commits April 11, 2026 06:06
…izer

Replace the remark/rehype/unified pipeline (~89 transitive packages) with two
custom, zero-dependency files for markdown↔HTML conversion. The unified ecosystem
was overkill for BlockNote's bounded feature set and required 6 custom plugins
to work around its abstractions.

New files:
- markdownToHtml.ts: block tokenizer + inline parser with recursive list handling
- htmlToMarkdown.ts: DOM-based serializer with proper list nesting and table support

Removes 12 direct dependencies: unified, remark-parse, remark-stringify, remark-gfm,
remark-rehype, rehype-parse, rehype-stringify, rehype-remark, rehype-format,
hast-util-from-dom, unist-util-visit, @types/hast

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add curly braces to single-line if/while statements and convert inner
function declaration to arrow function to fix no-inner-declarations error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ments

Use document.createElement instead of DOMParser and numeric constants
instead of Node.TEXT_NODE/ELEMENT_NODE for server-side compatibility.
Update snapshots to match new serializer output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Refactor parseInline() from monolithic if/else chain to ordered array of
  standalone tokenizer functions for better maintainability
- Batch consecutive plain-text characters for single escapeHtml() call
  instead of per-character escaping
- Improve emphasis closing delimiter check to reject single-char closers
  adjacent to same delimiter on either side (not just before)
- Add blockquote lazy continuation support per CommonMark spec
- Add blockquoteLazyContinuation test case

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Accept GFM tables with or without outer pipes
- Allow 0-3 leading spaces on fenced code blocks per CommonMark
- Strip optional titles from link URLs so they don't appear in href
- Use balanced bracket matching for image alt text
- Update 3 export snapshots for empty blocks after rebase

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nperez0111 nperez0111 force-pushed the feat/custom-md-parser branch from ec506e5 to f067aee Compare April 11, 2026 04:11
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (7)
packages/core/src/api/exporters/markdown/htmlToMarkdown.ts (5)

674-680: ⚠️ Potential issue | 🟡 Minor

Trailing backslash removal corrupts legitimate content.

Line 679 removes any trailing backslash, not just those from serialized <br> elements. This corrupts content like Windows paths (C:\Users\) or regex patterns that legitimately end with \.

Proposed fix
 function trimHardBreaks(content: string): string {
   // Remove leading hard breaks
   let result = content.replace(/^(\\\n)+/, "");
   // Remove trailing hard breaks (including trailing backslash)
   result = result.replace(/(\\\n)+$/, "");
-  result = result.replace(/\\$/, "");
   return result;
 }

The (\\\n)+$ pattern already handles trailing hard breaks. The additional \\$ replacement is over-aggressive.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/api/exporters/markdown/htmlToMarkdown.ts` around lines 674
- 680, The trimHardBreaks function over-aggressively strips any trailing
backslash via result.replace(/\\$/, "") which can corrupt legitimate trailing
backslashes (e.g., Windows paths or regexes); remove that final replace and rely
solely on the existing trailing hard-break regex (/(\\\n)+$/) to strip
serialized <br> sequences, leaving other trailing backslashes intact—update the
trimHardBreaks implementation (function name: trimHardBreaks) to drop the
result.replace(/\\$/, "") step.

368-370: ⚠️ Potential issue | 🟠 Major

Table cell content must be escaped.

Cell content containing | will create extra columns, and embedded newlines will split the row, producing invalid GFM tables.

Proposed fix
+function escapeTableCell(text: string): string {
+  return text
+    .replace(/\\/g, "\\\\")
+    .replace(/\|/g, "\\|")
+    .replace(/\r?\n/g, "<br>");
+}
+
 // In the cellElements.forEach callback:
-      const content = serializeInlineContent(cell as HTMLElement).trim();
+      const content = escapeTableCell(
+        serializeInlineContent(cell as HTMLElement).trim()
+      );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/api/exporters/markdown/htmlToMarkdown.ts` around lines 368
- 370, The table cell text produced by serializeInlineContent (assigned to
variable content) isn't escaped, so pipes and newlines break GFM tables; before
using content for table output (and after the existing .trim()), escape pipe
characters (replace '|' with '\|') and normalize embedded newlines (replace
'\r\n'/'\r'/'\n' with a safe inline representation such as '<br>' or a single
space) so colspan/rowspan handling (variables colspan, rowspan) receives a
GFM-safe cell string.

460-467: ⚠️ Potential issue | 🟠 Major

Link and image syntax characters must be escaped.

Raw insertion of alt, src, text, and href values into markdown link/image syntax will break when they contain characters like ], (, or ).

Example: alt="test]image" produces ![test]image](url) which parses incorrectly.

Proposed fix
+function escapeLinkText(text: string): string {
+  return text.replace(/\[/g, "\\[").replace(/\]/g, "\\]");
+}
+
+function escapeLinkDest(url: string): string {
+  // If URL contains spaces or parens, wrap in angle brackets
+  if (/[\s()]/.test(url)) {
+    return "<" + url.replace(/>/g, "%3E") + ">";
+  }
+  return url;
+}
+
 function serializeImage(el: HTMLElement, ctx: SerializeContext): string {
   const src = el.getAttribute("src") || "";
   const alt = el.getAttribute("alt") || "";
   if (!src) {
     return ctx.indent + "Add image\n\n";
   }
-  return ctx.indent + `![${alt}](${src})\n\n`;
+  return ctx.indent + `![${escapeLinkText(alt)}](${escapeLinkDest(src)})\n\n`;
 }

Apply similar escaping to serializeVideo, serializeBlockLink, serializeFigure, and inline link/image cases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/api/exporters/markdown/htmlToMarkdown.ts` around lines 460
- 467, The serializer inserts raw alt/src/text/href into markdown link/image
syntax which breaks when those values contain characters like ] (or
parentheses), so update serializeImage to escape alt text and link destination
characters (at minimum escape ] in alt/text and escape ( and ) in src/href, plus
escape backslashes) before interpolation; implement a small helper (e.g.,
escapeMarkdownLinkText/escapeMarkdownDestination) and apply it consistently in
serializeVideo, serializeBlockLink, serializeFigure and all inline link/image
serialization paths so all inserted values are properly escaped before producing
`![...]({src})` or `[...](href)` output.

147-172: ⚠️ Potential issue | 🟠 Major

Code fence length should adapt to code content.

The code block serializer always uses triple backticks, which will produce malformed markdown when the code itself contains triple backticks (the fence closes early).

Per CommonMark, the fence can be longer than any backtick sequence in the content.

Proposed fix
 function serializeCodeBlock(el: HTMLElement, ctx: SerializeContext): string {
   const codeEl = el.querySelector("code");
   if (!codeEl) {return "";}

   const language =
     codeEl.getAttribute("data-language") ||
     extractLanguageFromClass(codeEl.className) ||
     "";

   // Extract code content, handling <br> elements as newlines
   const code = extractCodeContent(codeEl);
+
+  // Compute fence length: must be longer than any backtick run in the code
+  const longestRun = Math.max(
+    0,
+    ...((code.match(/`+/g) ?? []).map((run) => run.length))
+  );
+  const fence = "`".repeat(Math.max(3, longestRun + 1));

   // For empty code blocks, don't add a newline between the fences
   if (!code) {
-    return ctx.indent + "```" + language + "\n```\n\n";
+    return ctx.indent + fence + language + "\n" + fence + "\n\n";
   }

   return (
     ctx.indent +
-    "```" +
+    fence +
     language +
     "\n" +
     code +
     (code.endsWith("\n") ? "" : "\n") +
-    "```\n\n"
+    fence +
+    "\n\n"
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/api/exporters/markdown/htmlToMarkdown.ts` around lines 147
- 172, serializeCodeBlock currently always uses "```" which breaks when code
contains equal-or-longer backtick runs; update serializeCodeBlock to compute a
fence string dynamically: examine the extracted code (from extractCodeContent)
to find the longest consecutive backtick sequence, set fence to a sequence of
backticks one longer than that, then use fence + language + "\n" for the opening
and fence + "\n\n" for the closing (respecting ctx.indent and the existing
newline handling). Ensure empty-code path also uses the computed fence instead
of hard-coded "```".

593-595: ⚠️ Potential issue | 🟠 Major

Inline code needs dynamic backtick fencing.

When code content contains backticks, single-backtick delimiters produce malformed markdown. CommonMark allows using multiple backticks (e.g., `` `code` ``) when the content contains backticks.

Proposed fix
-        case "code":
-          result += "`" + (childEl.textContent || "") + "`";
-          break;
+        case "code": {
+          const text = childEl.textContent || "";
+          const longestRun = Math.max(
+            0,
+            ...((text.match(/`+/g) ?? []).map((run) => run.length))
+          );
+          const fence = "`".repeat(longestRun + 1);
+          // Pad with space if content starts/ends with backtick
+          const needsPadding = text.startsWith("`") || text.endsWith("`");
+          result += fence + (needsPadding ? ` ${text} ` : text) + fence;
+          break;
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/api/exporters/markdown/htmlToMarkdown.ts` around lines 593
- 595, The inline code branch (case "code" in htmlToMarkdown.ts) currently wraps
text in a single backtick which breaks when the code contains backticks; update
it to compute the longest run of consecutive backticks in childEl.textContent
(e.g., scan for /`+/), set the fence to one more backtick than that (fence =
'`'.repeat(maxRun+1)), and wrap the content with that fence; additionally, if
the content starts or ends with a backtick or space, ensure you add a single
space between the fence and content (e.g., fence + " " + content + " " + fence)
to conform with CommonMark edge rules so the produced markdown remains valid.
packages/core/src/api/parsers/markdown/markdownToHtml.ts (2)

623-623: ⚠️ Potential issue | 🟠 Major

Table boundary checks are still stricter than tryParseTable.

These checks still require outer pipes, so valid GFM rows like a | b may be consumed as paragraph/blockquote text in these paths. Reuse tryParseTable(lines, i) here to keep boundary detection consistent.

Proposed fix
-        if (/^\s*\|(.+\|)+\s*$/.test(cur)) {break;} // table
+        if (tryParseTable(lines, i)) {break;} // table
...
-      if (/^\s*\|(.+\|)+\s*$/.test(nextLine)) {break;}
+      if (tryParseTable(lines, i)) {break;}

Also applies to: 751-751

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/api/parsers/markdown/markdownToHtml.ts` at line 623,
Current boundary checks use a strict regex (/^\s*\|(.+\|)+\s*$/) that requires
outer pipes and wrongly treats valid GFM rows like "a | b" as non-tables;
replace those regex checks with a call to tryParseTable(lines, i) so detection
matches the parser's canonical logic. Locate the two occurrences (the condition
using variable cur in the block where parsing decides to break) and change the
condition to if (tryParseTable(lines, i)) { break; } making sure to pass the
same lines and index variables used in that scope and not to alter index
advancement semantics.

512-529: ⚠️ Potential issue | 🟠 Major

Indented fenced code content is not de-indented.

You detect indented fences, but code lines are emitted with their original indentation. For fences opened with up to 3 spaces, content should strip up to that opening indent to match CommonMark behavior.

Proposed fix
-    const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
+    const fenceMatch = line.match(/^( {0,3})(`{3,}|~{3,})(.*)$/);
     if (fenceMatch) {
-      const fence = fenceMatch[1];
+      const fenceIndent = fenceMatch[1].length;
+      const fence = fenceMatch[2];
       const fenceChar = fence[0];
       const fenceLen = fence.length;
-      const language = fenceMatch[2].trim();
+      const language = fenceMatch[3].trim();
       const codeLines: string[] = [];
       i++;
       while (i < lines.length) {
         const closingMatch = lines[i].match(
           new RegExp(`^ {0,3}${fenceChar}{${fenceLen},}\\s*$`)
         );
         if (closingMatch) {
           i++;
           break;
         }
-        codeLines.push(lines[i]);
+        codeLines.push(
+          lines[i].startsWith(" ".repeat(fenceIndent))
+            ? lines[i].slice(fenceIndent)
+            : lines[i]
+        );
         i++;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/api/parsers/markdown/markdownToHtml.ts` around lines 512 -
529, The parser detects indented fenced code blocks via fenceMatch but does not
remove the opening indent from the captured code lines; update the fenced-block
handling (around fenceMatch / fence / fenceChar / fenceLen / codeLines / i in
markdownToHtml.ts) to compute the number of leading spaces from the original
opening line (up to 3) and strip up to that many spaces from the start of each
lines[i] before pushing into codeLines so that indented fenced content is
de-indented per CommonMark behavior.
🧹 Nitpick comments (2)
packages/core/src/api/exporters/markdown/htmlToMarkdown.ts (2)

664-668: Remove orphaned JSDoc comment.

This documentation comment has no associated function — it appears to be a remnant from removed code.

-/**
- * Escape leading character after emphasis if it could break parsing.
- * For example, "Heading" after "**Bold **" — the 'H' should be escaped
- * if the trailing space was escaped.
- */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/api/exporters/markdown/htmlToMarkdown.ts` around lines 664
- 668, Remove the orphaned JSDoc block beginning with "Escape leading character
after emphasis if it could break parsing..." in
packages/core/src/api/exporters/markdown/htmlToMarkdown.ts; this comment has no
associated function and should be deleted or moved next to the relevant helper
if it was intended to document a real function (search for that exact comment
text to locate it).

589-591: Consider applying trailing whitespace extraction to strikethrough.

The strong/em handlers extract trailing whitespace to produce cleaner markdown, but del/s doesn't. For consistency, strikethrough could use the same pattern.

Suggested change
         case "s":
-        case "del":
-          result += `~~${serializeInlineContent(childEl)}~~`;
+        case "del": {
+          const inner = serializeInlineContent(childEl);
+          const { content, trailing } = extractTrailingWhitespace(inner);
+          if (content) {
+            result += `~~${content}~~${trailing}`;
+          } else {
+            result += trailing;
+          }
           break;
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/api/exporters/markdown/htmlToMarkdown.ts` around lines 589
- 591, The strikethrough handlers for tag names "s" and "del" should mirror the
trailing-whitespace extraction used by the "strong"/"em" handlers: call
serializeInlineContent(childEl), detect and remove any trailing whitespace from
that serialized string, wrap the trimmed content in double tildes
(`~~${trimmed}~~`), and append the extracted trailing whitespace after the
closing tildes so the output preserves spacing; update the logic inside the case
"s"/"del" branch in htmlToMarkdown.ts to perform this extraction and
reconstruction using the same helper/pattern used by the strong/em handlers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/api/parsers/markdown/markdownToHtml.ts`:
- Line 620: The continuation scan currently only matches fences at column 0
using /^(`{3,}|~{3,})/, so indented fences like "  ```ts" are not recognized;
update the fence detection to allow up to three leading spaces (e.g. change
/^(`{3,}|~{3,})/ to /^[ \t]{0,3}(`{3,}|~{3,})/) in markdownToHtml.ts (apply the
same change to both occurrences around the current checks at the locations
shown) so indented code fences are treated as block boundaries during
continuation scans.

---

Duplicate comments:
In `@packages/core/src/api/exporters/markdown/htmlToMarkdown.ts`:
- Around line 674-680: The trimHardBreaks function over-aggressively strips any
trailing backslash via result.replace(/\\$/, "") which can corrupt legitimate
trailing backslashes (e.g., Windows paths or regexes); remove that final replace
and rely solely on the existing trailing hard-break regex (/(\\\n)+$/) to strip
serialized <br> sequences, leaving other trailing backslashes intact—update the
trimHardBreaks implementation (function name: trimHardBreaks) to drop the
result.replace(/\\$/, "") step.
- Around line 368-370: The table cell text produced by serializeInlineContent
(assigned to variable content) isn't escaped, so pipes and newlines break GFM
tables; before using content for table output (and after the existing .trim()),
escape pipe characters (replace '|' with '\|') and normalize embedded newlines
(replace '\r\n'/'\r'/'\n' with a safe inline representation such as '<br>' or a
single space) so colspan/rowspan handling (variables colspan, rowspan) receives
a GFM-safe cell string.
- Around line 460-467: The serializer inserts raw alt/src/text/href into
markdown link/image syntax which breaks when those values contain characters
like ] (or parentheses), so update serializeImage to escape alt text and link
destination characters (at minimum escape ] in alt/text and escape ( and ) in
src/href, plus escape backslashes) before interpolation; implement a small
helper (e.g., escapeMarkdownLinkText/escapeMarkdownDestination) and apply it
consistently in serializeVideo, serializeBlockLink, serializeFigure and all
inline link/image serialization paths so all inserted values are properly
escaped before producing `![...]({src})` or `[...](href)` output.
- Around line 147-172: serializeCodeBlock currently always uses "```" which
breaks when code contains equal-or-longer backtick runs; update
serializeCodeBlock to compute a fence string dynamically: examine the extracted
code (from extractCodeContent) to find the longest consecutive backtick
sequence, set fence to a sequence of backticks one longer than that, then use
fence + language + "\n" for the opening and fence + "\n\n" for the closing
(respecting ctx.indent and the existing newline handling). Ensure empty-code
path also uses the computed fence instead of hard-coded "```".
- Around line 593-595: The inline code branch (case "code" in htmlToMarkdown.ts)
currently wraps text in a single backtick which breaks when the code contains
backticks; update it to compute the longest run of consecutive backticks in
childEl.textContent (e.g., scan for /`+/), set the fence to one more backtick
than that (fence = '`'.repeat(maxRun+1)), and wrap the content with that fence;
additionally, if the content starts or ends with a backtick or space, ensure you
add a single space between the fence and content (e.g., fence + " " + content +
" " + fence) to conform with CommonMark edge rules so the produced markdown
remains valid.

In `@packages/core/src/api/parsers/markdown/markdownToHtml.ts`:
- Line 623: Current boundary checks use a strict regex (/^\s*\|(.+\|)+\s*$/)
that requires outer pipes and wrongly treats valid GFM rows like "a | b" as
non-tables; replace those regex checks with a call to tryParseTable(lines, i) so
detection matches the parser's canonical logic. Locate the two occurrences (the
condition using variable cur in the block where parsing decides to break) and
change the condition to if (tryParseTable(lines, i)) { break; } making sure to
pass the same lines and index variables used in that scope and not to alter
index advancement semantics.
- Around line 512-529: The parser detects indented fenced code blocks via
fenceMatch but does not remove the opening indent from the captured code lines;
update the fenced-block handling (around fenceMatch / fence / fenceChar /
fenceLen / codeLines / i in markdownToHtml.ts) to compute the number of leading
spaces from the original opening line (up to 3) and strip up to that many spaces
from the start of each lines[i] before pushing into codeLines so that indented
fenced content is de-indented per CommonMark behavior.

---

Nitpick comments:
In `@packages/core/src/api/exporters/markdown/htmlToMarkdown.ts`:
- Around line 664-668: Remove the orphaned JSDoc block beginning with "Escape
leading character after emphasis if it could break parsing..." in
packages/core/src/api/exporters/markdown/htmlToMarkdown.ts; this comment has no
associated function and should be deleted or moved next to the relevant helper
if it was intended to document a real function (search for that exact comment
text to locate it).
- Around line 589-591: The strikethrough handlers for tag names "s" and "del"
should mirror the trailing-whitespace extraction used by the "strong"/"em"
handlers: call serializeInlineContent(childEl), detect and remove any trailing
whitespace from that serialized string, wrap the trimmed content in double
tildes (`~~${trimmed}~~`), and append the extracted trailing whitespace after
the closing tildes so the output preserves spacing; update the logic inside the
case "s"/"del" branch in htmlToMarkdown.ts to perform this extraction and
reconstruction using the same helper/pattern used by the strong/em handlers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fd61dbe0-cc0c-42d4-a81a-e8d04d5f03ae

📥 Commits

Reviewing files that changed from the base of the PR and between ec506e5 and f067aee.

⛔ Files ignored due to path filters (127)
  • packages/core/src/api/parsers/html/util/__snapshots__/nestedLists.test.ts.snap is excluded by !**/*.snap, !**/__snapshots__/**
  • packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.test.ts.snap is excluded by !**/*.snap, !**/__snapshots__/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/audio/basic.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/audio/button.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/audio/noName.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/complex/document.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h1.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h2.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h3.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h4.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h5.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h6.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/styled.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/toggleable.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/withCaption.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/withCode.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/lists/numberedListStart.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/paragraph/multiple.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/basic.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/multiple.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/nested.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/styled.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/withLink.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/backgroundColor.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/bold.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/boldItalicStrike.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/code.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/combined.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/italic.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/mixedInParagraph.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/strike.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/textColor.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/underline.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/advancedExample.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/cellTextAlignment.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/emptyCells.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/hardBreakInCell.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/headerRowsAndCols.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/linksInCells.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/singleCell.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/styledCellContent.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/video/withCaption.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/basic.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/button.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/noName.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/complex/document.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h1.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h2.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h3.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h4.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h5.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h6.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/styled.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/toggleable.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/image/withCaption.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/link/withCode.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/lists/numberedListStart.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/multiple.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/basic.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/multiple.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/nested.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/styled.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/withLink.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/backgroundColor.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/bold.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/boldItalicStrike.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/code.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/combined.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/italic.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/mixedInParagraph.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/strike.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/textColor.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/style/underline.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/table/advancedExample.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/table/cellTextAlignment.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/table/emptyCells.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/table/hardBreakInCell.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/table/headerRowsAndCols.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/table/linksInCells.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/table/singleCell.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/table/styledCellContent.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/html/video/withCaption.html is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/basic.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/button.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/noName.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/complex/document.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/complex/misc.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/hardbreak/only.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h1.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h2.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h3.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h4.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h5.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h6.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/styled.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/toggleable.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/withCaption.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/link/withCode.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/lists/numberedListStart.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/pageBreak/basic.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/paragraph/empty.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/paragraph/multiple.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/basic.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/multiple.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/nested.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/styled.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/withLink.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/backgroundColor.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/bold.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/boldItalicStrike.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/code.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/combined.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/italic.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/mixedInParagraph.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/strike.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/textColor.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/underline.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/advancedExample.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/cellTextAlignment.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/emptyCells.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/hardBreakInCell.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/headerRowsAndCols.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/linksInCells.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/singleCell.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/styledCellContent.md is excluded by !**/__snapshots__/**
  • tests/src/unit/core/formatConversion/export/__snapshots__/markdown/video/withCaption.md is excluded by !**/__snapshots__/**
📒 Files selected for processing (10)
  • packages/core/package.json
  • packages/core/src/api/exporters/markdown/htmlToMarkdown.ts
  • packages/core/src/api/exporters/markdown/markdownExporter.ts
  • packages/core/src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.ts
  • packages/core/src/api/exporters/markdown/util/convertVideoToMarkdownRehypePlugin.ts
  • packages/core/src/api/exporters/markdown/util/removeUnderlinesRehypePlugin.ts
  • packages/core/src/api/parsers/html/util/nestedLists.test.ts
  • packages/core/src/api/parsers/markdown/markdownToHtml.ts
  • packages/core/src/api/parsers/markdown/parseMarkdown.ts
  • packages/xl-ai/package.json
💤 Files with no reviewable changes (5)
  • packages/xl-ai/package.json
  • packages/core/package.json
  • packages/core/src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.ts
  • packages/core/src/api/exporters/markdown/util/convertVideoToMarkdownRehypePlugin.ts
  • packages/core/src/api/exporters/markdown/util/removeUnderlinesRehypePlugin.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/core/src/api/exporters/markdown/markdownExporter.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/core/src/api/parsers/html/util/nestedLists.test.ts
  • packages/core/src/api/parsers/markdown/parseMarkdown.ts

// Stop on block-level markers
if (/^\s{0,3}>/.test(cur)) {break;} // new blockquote
if (/^(#{1,6})\s/.test(cur)) {break;} // heading
if (/^(`{3,}|~{3,})/.test(cur)) {break;} // code fence
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Indented fences are not treated as block boundaries in continuation scans.

The continuation checks only match fences at column 0, so lines like ```ts can be absorbed into paragraph/blockquote text instead of starting a code block.

Proposed fix
-        if (/^(`{3,}|~{3,})/.test(cur)) {break;} // code fence
+        if (/^\s{0,3}(`{3,}|~{3,})/.test(cur)) {break;} // code fence
...
-      if (/^(`{3,}|~{3,})/.test(nextLine)) {break;}
+      if (/^\s{0,3}(`{3,}|~{3,})/.test(nextLine)) {break;}

Also applies to: 747-747

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/api/parsers/markdown/markdownToHtml.ts` at line 620, The
continuation scan currently only matches fences at column 0 using
/^(`{3,}|~{3,})/, so indented fences like "  ```ts" are not recognized; update
the fence detection to allow up to three leading spaces (e.g. change
/^(`{3,}|~{3,})/ to /^[ \t]{0,3}(`{3,}|~{3,})/) in markdownToHtml.ts (apply the
same change to both occurrences around the current checks at the locations
shown) so indented code fences are treated as block boundaries during
continuation scans.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant