feat: replace unified.js with custom markdown parser/serializer#2624
feat: replace unified.js with custom markdown parser/serializer#2624nperez0111 wants to merge 4 commits intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughReplaces the unified/remark/rehype pipeline with custom TypeScript Markdown↔HTML converters, removes related rehype/remark plugins and dependencies, updates exporter/parser calls, and expands Markdown/HTML round‑trip tests and fixtures. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
…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>
46e50e3 to
f9da2df
Compare
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (248)
packages/core/src/api/parsers/html/util/__snapshots__/nestedLists.test.ts.snapis excluded by!**/*.snap,!**/__snapshots__/**pnpm-lock.yamlis excluded by!**/pnpm-lock.yamltests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/audio/basic.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/audio/button.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/audio/noName.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/complex/document.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h1.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h2.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h3.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h4.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h5.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/h6.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/styled.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/heading/toggleable.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/image/withCaption.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/link/withCode.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/lists/numberedListStart.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/paragraph/multiple.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/basic.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/multiple.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/nested.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/styled.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/quote/withLink.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/backgroundColor.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/bold.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/boldItalicStrike.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/code.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/combined.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/italic.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/mixedInParagraph.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/strike.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/textColor.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/style/underline.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/advancedExample.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/cellTextAlignment.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/emptyCells.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/hardBreakInCell.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/headerRowsAndCols.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/linksInCells.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/singleCell.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/table/styledCellContent.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/video/withCaption.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/basic.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/button.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/audio/noName.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/complex/document.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h1.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h2.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h3.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h4.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h5.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/h6.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/styled.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/heading/toggleable.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/image/withCaption.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/link/withCode.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/lists/numberedListStart.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/paragraph/multiple.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/basic.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/multiple.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/nested.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/styled.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/quote/withLink.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/style/backgroundColor.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/style/bold.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/style/boldItalicStrike.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/style/code.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/style/combined.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/style/italic.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/style/mixedInParagraph.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/style/strike.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/style/textColor.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/style/underline.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/table/advancedExample.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/table/cellTextAlignment.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/table/emptyCells.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/table/hardBreakInCell.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/table/headerRowsAndCols.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/table/linksInCells.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/table/singleCell.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/table/styledCellContent.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/html/video/withCaption.htmlis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/basic.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/button.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/audio/noName.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/complex/document.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/complex/misc.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h1.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h2.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h3.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h4.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h5.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/h6.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/styled.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/heading/toggleable.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/image/withCaption.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/link/withCode.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/lists/numberedListStart.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/paragraph/multiple.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/basic.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/multiple.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/nested.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/styled.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/quote/withLink.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/backgroundColor.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/bold.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/boldItalicStrike.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/code.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/combined.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/italic.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/mixedInParagraph.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/strike.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/textColor.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/style/underline.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/advancedExample.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/cellTextAlignment.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/emptyCells.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/hardBreakInCell.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/headerRowsAndCols.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/linksInCells.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/singleCell.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/table/styledCellContent.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/video/withCaption.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/audio/basic.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/audio/button.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/audio/noName.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/complex/document.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h1.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h2.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h3.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h4.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h5.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/h6.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/styled.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/heading/toggleable.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/image/withCaption.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/link/withCode.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/lists/numberedListStart.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/paragraph/multiple.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/basic.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/multiple.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/nested.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/styled.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/quote/withLink.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/backgroundColor.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/bold.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/boldItalicStrike.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/code.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/combined.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/italic.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/mixedInParagraph.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/strike.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/textColor.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/style/underline.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/advancedExample.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/cellTextAlignment.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/emptyCells.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/hardBreakInCell.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/headerRowsAndCols.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/linksInCells.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/singleCell.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/table/styledCellContent.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/nodes/video/withCaption.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/bold.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/boldItalic.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/bulletList.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/checkList.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/codeBlock.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/complexDocument.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/divider.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/hardBreak.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/headingLevels.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/image.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/inlineCode.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/italic.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/link.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/mixedStyles.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/multipleParagraphs.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/nestedLists.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/numberedList.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/paragraph.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/quote.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/strike.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/table.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/video.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/adjacentFormattedRuns.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/adjacentLinks.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/backslashEscapes.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteMultiline.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteWithCode.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteWithLink.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/boldOnly.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/boldUnderscore.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/checkListBasic.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/checkListMixed.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/checkListNested.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockBasic.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockPython.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockTildes.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockWithLanguage.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/codeBlockWithSpecialChars.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/complexDocument.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/deeplyNestedLists.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/emptyString.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/hardBreakBackslash.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/hardBreakMultiple.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH1.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH2.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH3.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH4.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH5.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingH6.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingThenCode.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/headingWithInlineStyles.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/horizontalRuleAsterisks.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/horizontalRuleDashes.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/horizontalRuleUnderscores.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/imageWithAlt.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineCode.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineCodeWithSpecialChars.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/inlineImage.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/italicOnly.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/italicUnderscore.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkAndText.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkBasic.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkInParagraph.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/linkWithStyledContent.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/listWithStyledItems.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/mixedInlineContent.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/mixedListTypes.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/multipleImages.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/multipleParagraphs.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedBulletLists.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedEmphasis.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedEmphasisComplex.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/nestedOrderedLists.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/onlyWhitespace.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/orderedListStart.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/paragraphContinuation.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/setextH1.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/setextH2.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/strikethroughOnly.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableAlignment.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableBasic.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableFollowedByParagraph.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableThreeColumns.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableWithInlineFormatting.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/tableWithLinks.jsonis excluded by!**/__snapshots__/**
📒 Files selected for processing (15)
packages/core/package.jsonpackages/core/src/api/exporters/markdown/htmlToMarkdown.tspackages/core/src/api/exporters/markdown/markdownExporter.tspackages/core/src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.tspackages/core/src/api/exporters/markdown/util/convertVideoToMarkdownRehypePlugin.tspackages/core/src/api/exporters/markdown/util/removeUnderlinesRehypePlugin.tspackages/core/src/api/parsers/html/util/nestedLists.test.tspackages/core/src/api/parsers/markdown/markdownToHtml.tspackages/core/src/api/parsers/markdown/parseMarkdown.tspackages/xl-ai/package.jsontests/src/unit/core/formatConversion/export/exportTestInstances.tstests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.tstests/src/unit/core/formatConversion/exportParseEquality/runTests.test.tstests/src/unit/core/formatConversion/parse/parseTestInstances.tstests/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
…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>
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (3)
packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.test.ts.snapis excluded by!**/*.snap,!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/deeplyNestedLists.jsonis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/exportParseEquality/__snapshots__/markdown/markdown/specialCharEscaping.jsonis excluded by!**/__snapshots__/**
📒 Files selected for processing (3)
packages/core/src/api/exporters/markdown/htmlToMarkdown.tspackages/core/src/api/parsers/markdown/markdownToHtml.tstests/src/unit/core/formatConversion/exportParseEquality/exportParseEqualityTestInstances.ts
| 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" | ||
| ); |
There was a problem hiding this comment.
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.
| 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.
| const content = serializeInlineContent(cell as HTMLElement).trim(); | ||
| const colspan = parseInt(cell.getAttribute("colspan") || "1", 10); | ||
| const rowspan = parseInt(cell.getAttribute("rowspan") || "1", 10); |
There was a problem hiding this comment.
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).
| 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 + `\n\n`; | ||
| } |
There was a problem hiding this comment.
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).
| case "code": | ||
| result += "`" + (childEl.textContent || "") + "`"; | ||
| break; |
There was a problem hiding this comment.
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.
| 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.
| 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; |
There was a problem hiding this comment.
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.
| 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.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@blocknote/ariakit
@blocknote/code-block
@blocknote/core
@blocknote/mantine
@blocknote/react
@blocknote/server-util
@blocknote/shadcn
@blocknote/xl-ai
@blocknote/xl-docx-exporter
@blocknote/xl-email-exporter
@blocknote/xl-multi-column
@blocknote/xl-odt-exporter
@blocknote/xl-pdf-exporter
commit: |
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
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 detectionhtmlToMarkdown.ts(~500 LOC): Custom HTML→markdown serializer using DOMParser with proper list nesting, table column padding, toggle item handling, and lossy conversion supportunified,remark-parse,remark-stringify,remark-gfm,remark-rehype,rehype-parse,rehype-stringify,rehype-remark,rehype-format,hast-util-from-dom,unist-util-visit,@types/hastconvertVideoToMarkdownRehypePlugin,removeUnderlinesRehypePlugin,addSpacesToCheckboxesRehypePlugin(their logic is now built into the serializer)@blocknote/xl-ai:unified,remark-parse,remark-stringify**Bold** Heading) instead of escaped with HTML entities (**Bold **Heading), producing cleaner, more readable markdownImpact
nestedLists.test.tsno longer uses unified for pretty-printing (uses raw innerHTML comparison instead)Testing
complex/misc.md) due to the cleaner emphasis handlingScreenshots/Video
N/A - internal serialization change with no UI impact.
Checklist
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
Chores
Tests