feat: replace unified.js with custom markdown parser/serializer#2624
feat: replace unified.js with custom markdown parser/serializer#2624nperez0111 wants to merge 6 commits intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughReplaces 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
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 |
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
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.
@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: |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
tests/src/unit/core/formatConversion/parse/__snapshots__/markdown/blockquoteLazyContinuation.jsonis excluded by!**/__snapshots__/**
📒 Files selected for processing (2)
packages/core/src/api/parsers/markdown/markdownToHtml.tstests/src/unit/core/formatConversion/parse/parseTestInstances.ts
✅ Files skipped from review due to trivial changes (1)
- tests/src/unit/core/formatConversion/parse/parseTestInstances.ts
…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>
ec506e5 to
f067aee
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (7)
packages/core/src/api/exporters/markdown/htmlToMarkdown.ts (5)
674-680:⚠️ Potential issue | 🟡 MinorTrailing 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 | 🟠 MajorTable 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 | 🟠 MajorLink and image syntax characters must be escaped.
Raw insertion of
alt,src,text, andhrefvalues 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 + `\n\n`; + return ctx.indent + `})\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 `` or `[...](href)` output.
147-172:⚠️ Potential issue | 🟠 MajorCode 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 | 🟠 MajorInline 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 | 🟠 MajorTable boundary checks are still stricter than
tryParseTable.These checks still require outer pipes, so valid GFM rows like
a | bmay be consumed as paragraph/blockquote text in these paths. ReusetryParseTable(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 | 🟠 MajorIndented 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/emhandlers extract trailing whitespace to produce cleaner markdown, butdel/sdoesn'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 `` 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
⛔ Files ignored due to path filters (127)
packages/core/src/api/parsers/html/util/__snapshots__/nestedLists.test.ts.snapis excluded by!**/*.snap,!**/__snapshots__/**packages/server-util/src/context/__snapshots__/ServerBlockNoteEditor.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/hardbreak/only.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/pageBreak/basic.mdis excluded by!**/__snapshots__/**tests/src/unit/core/formatConversion/export/__snapshots__/markdown/paragraph/empty.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__/**
📒 Files selected for processing (10)
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.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 |
There was a problem hiding this comment.
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.
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