Skip to content

fix: allow indented code blocks#2505

Merged
antfu merged 9 commits intoslidevjs:mainfrom
kermanx:fix/code-block-with-indent
Apr 8, 2026
Merged

fix: allow indented code blocks#2505
antfu merged 9 commits intoslidevjs:mainfrom
kermanx:fix/code-block-with-indent

Conversation

@kermanx
Copy link
Copy Markdown
Member

@kermanx kermanx commented Mar 26, 2026

This PR refactors the Markdown transformation pipeline. All regex-based transformers are now markdown-it plugins, which are more robust and efficient.

A little breaking change since custom transformers that depends on transformation orders may no longer work correctly.

This PR also adds support for indented code blocks:

1. item

   ```ts [filename.ts]{1,2}{maxHeight:'200px'}
   line
   ```

which should render to

  1. item

    line

And avoids transforming Slidev syntax in code blocks (close #2463):

```
<<< 1 // Will not be transformed
```

@netlify
Copy link
Copy Markdown

netlify bot commented Mar 26, 2026

Deploy Preview for slidev failed.

Name Link
🔨 Latest commit 9144ea0
🔍 Latest deploy log https://app.netlify.com/projects/slidev/deploys/69d5c1480d69f70008d990c0

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 26, 2026

Open in StackBlitz

@slidev/client

npm i https://pkg.pr.new/@slidev/client@2505

create-slidev

npm i https://pkg.pr.new/create-slidev@2505

create-slidev-theme

npm i https://pkg.pr.new/create-slidev-theme@2505

@slidev/parser

npm i https://pkg.pr.new/@slidev/parser@2505

@slidev/cli

npm i https://pkg.pr.new/@slidev/cli@2505

@slidev/types

npm i https://pkg.pr.new/@slidev/types@2505

commit: b445334

@kermanx kermanx marked this pull request as draft March 26, 2026 12:36
@kermanx
Copy link
Copy Markdown
Member Author

kermanx commented Mar 27, 2026

image 😅

@kermanx kermanx marked this pull request as ready for review March 27, 2026 04:26
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors Slidev’s Markdown transformation pipeline by moving regex-based transformers into markdown-exit/markdown-it-style plugins and introducing a dedicated codeblock transformer hook, enabling robust handling of indented fences and preventing Slidev syntax transformations inside code blocks.

Changes:

  • Replaces regex-based Markdown transformers with markdown-it plugins + a new CodeblockTransformer API.
  • Adds codeblock-level transformation pipeline (Mermaid / PlantUML / Monaco / Magic Move / wrapper) integrated into Markdown rendering.
  • Updates/rewrites tests to cover new plugin-based behavior and indented code blocks.

Reviewed changes

Copilot reviewed 50 out of 53 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
test/utils.test.ts Switches Markdown parser in tests from markdown-it to markdown-exit.
test/transform.test.ts Removes legacy regex-transform snapshot tests (replaced by new plugin/codeblock tests elsewhere).
test/transform-magic-move.test.ts Removes legacy Magic Move transform test (now covered via codeblock pipeline/integration tests).
test/transform-all.test.ts Removes legacy “transform-all” snapshot test (superseded by integration tests).
test/snapshots/transform.test.ts.snap Removes snapshots tied to deleted legacy transform tests.
test/snapshots/transform-all.test.ts.snap Removes snapshots tied to deleted legacy transform tests.
packages/vscode/syntaxes/slidev.example.md Updates example to include indented fenced block formatting.
packages/vscode/src/views/annotations.ts Aligns codeblock line annotations with detected indentation (indent becomes numeric column).
packages/types/src/transform.ts Introduces CodeblockTransformContext / CodeblockTransformer and helper definitions.
packages/types/src/setups.ts Extends transformers setup return type to support codeblocks transformers; marks some legacy arrays deprecated.
packages/slidev/node/vite/markdown.ts Hooks new transformers setup + passes codeblock transformers into markdown-it plugin setup.
packages/slidev/node/vite/loaders.ts Updates imports to new syntax module locations.
packages/slidev/node/syntax/utils.ts Moves shared helpers (normalizeRangeStr, escapeVueInCode) into syntax-level utilities.
packages/slidev/node/syntax/transform/utils.ts Removes legacy transform utilities (code/comment block scanning).
packages/slidev/node/syntax/transform/snippet.ts Removes legacy regex-based snippet transformer (replaced with markdown-it plugin).
packages/slidev/node/syntax/transform/slot-sugar.ts Removes legacy regex-based slot sugar transformer (replaced with markdown-it plugin).
packages/slidev/node/syntax/transform/plant-uml.ts Removes legacy regex-based PlantUML transformer (replaced with codeblock transformer).
packages/slidev/node/syntax/transform/monaco.ts Removes legacy regex-based Monaco transformer (replaced with codeblock transformer).
packages/slidev/node/syntax/transform/mermaid.ts Removes legacy regex-based Mermaid transformer (replaced with codeblock transformer).
packages/slidev/node/syntax/transform/magic-move.ts Removes legacy regex-based Magic Move transformer (replaced with codeblock transformer).
packages/slidev/node/syntax/transform/katex-wrapper.ts Removes legacy KaTeX wrapper transformer (moved into markdown-it KaTeX plugin behavior).
packages/slidev/node/syntax/transform/index.ts Removes legacy getMarkdownTransformers pipeline (now plugin-based).
packages/slidev/node/syntax/transform/in-page-css.ts Removes legacy in-page CSS transformer (replaced with scoped-style plugin).
packages/slidev/node/syntax/transform/code-wrapper.ts Removes legacy code wrapper transformer (replaced with codeblock wrapper transformer).
packages/slidev/node/syntax/snippet.ts Adds markdown-it snippet import rule (supports indentation + avoids codeblock transformation).
packages/slidev/node/syntax/snippet.test.ts Adds unit tests for snippet import including indented contexts and codeblock non-transformation.
packages/slidev/node/syntax/slot-sugar.ts Adds markdown-it slot marker compilation pass.
packages/slidev/node/syntax/slot-sugar.test.ts Adds unit tests for slot sugar + “no transform in code block”.
packages/slidev/node/syntax/shiki.test.ts Adds minimal shiki plugin integration test.
packages/slidev/node/syntax/scoped.ts Adds HTML renderer wrapper to auto-add scoped to <style> tags outside code blocks.
packages/slidev/node/syntax/scoped.test.ts Adds tests for scoped-style behavior and codeblock non-transformation.
packages/slidev/node/syntax/markdown-it/markdown-it-v-drag.ts Hardens source map lookup for missing env.id.
packages/slidev/node/syntax/markdown-it/markdown-it-shiki.ts Removes escapeVueInCode postprocess hook (now handled by wrapper transformer).
packages/slidev/node/syntax/markdown-it/markdown-it-katex.ts Refactors KaTeX parsing/rendering and adds wrapper/ranges support in renderer.
packages/slidev/node/syntax/markdown-it/index.ts Removes legacy markdown-it plugin index (replaced by packages/slidev/node/syntax/index.ts).
packages/slidev/node/syntax/link.test.ts Adds unit tests for internal/external link rendering.
packages/slidev/node/syntax/katex.test.ts Adds unit tests for inline/block KaTeX + no-transform in code blocks.
packages/slidev/node/syntax/integration.test.ts Adds end-to-end integration test covering combined syntax and “no transform inside code blocks”.
packages/slidev/node/syntax/index.ts New unified markdown-it plugin setup including codeblock transform pipeline.
packages/slidev/node/syntax/escape-code.test.ts Adds unit test for inline code Vue escaping.
packages/slidev/node/syntax/drag.test.ts Adds unit tests for v-drag component/directive transformation.
packages/slidev/node/syntax/codeblock/wrapper.ts Adds CodeBlockWrapper codeblock transformer (ranges/title/options parsing + highlight delegation).
packages/slidev/node/syntax/codeblock/plant-uml.ts Adds PlantUML codeblock transformer.
packages/slidev/node/syntax/codeblock/monaco.ts Adds Monaco codeblock transformer (diff/run support).
packages/slidev/node/syntax/codeblock/mermaid.ts Adds Mermaid codeblock transformer.
packages/slidev/node/syntax/codeblock/magic-move.ts Adds Magic Move codeblock transformer for quadruple-fenced blocks.
packages/slidev/node/syntax/codeblock/index.ts Installs fence renderer interception and executes codeblock transformer chain.
packages/slidev/node/setups/transformers.ts Aggregates codeblocks transformers in setup returns.
packages/slidev/node/commands/shared.ts Updates link plugin import path to new syntax module location.
packages/client/builtin/CodeBlockWrapper.vue Changes title default from undefined to empty string.
docs/custom/config-transformers.md Updates docs for new transformer APIs and codeblock transformer usage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// This applies before the Markdown is parsed, per slide
pre: [mySyntax],
// This applies per Markdown code block
codeblock: [myCodeblock],
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The docs example uses codeblock, but the implemented setup return type and aggregation logic use codeblocks (plural). As written, the sample won’t register codeblock transformers. Update the docs to use the correct key (codeblocks) to match TransformersSetupReturn and setupTransformers().

Suggested change
codeblock: [myCodeblock],
codeblocks: [myCodeblock],

Copilot uses AI. Check for mistakes.
const [, options] = match
const optionsProp = options ? `v-bind="${options}"` : ''
const encoded = encodePlantUml(code.trim())
return `<PlantUml ${optionsProp} code="${encoded}" server=${JSON.stringify(plantUmlServer)} />`
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

When plantUmlServer is undefined, JSON.stringify(plantUmlServer) yields undefined, producing server=undefined in the generated markup. This passes the literal string "undefined" to Vue and can break downstream expectations. Prefer omitting the server attribute when not configured, or bind it explicitly (e.g., :server=${JSON.stringify(plantUmlServer)}) and only include it when defined.

Suggested change
return `<PlantUml ${optionsProp} code="${encoded}" server=${JSON.stringify(plantUmlServer)} />`
const serverProp = plantUmlServer === undefined ? '' : ` :server=${JSON.stringify(plantUmlServer)}`
return `<PlantUml ${optionsProp} code="${encoded}"${serverProp} />`

Copilot uses AI. Check for mistakes.
Comment on lines +165 to +168
const encoded = lz.compressToBase64(content)

const token = state.push('html_block', '', 0)
token.content = `<Monaco writable="${filepath}" code-lz="${encoded}" lang="${lang}" v-bind="${meta}" />\n`
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The filepath is interpolated directly into an HTML/Vue attribute (writable=\"${filepath}\") without escaping. A path containing quotes or angle brackets can break the attribute and potentially inject additional markup. Use JSON.stringify(filepath) (as the previous implementation did) or otherwise escape attribute values before interpolation.

Suggested change
const encoded = lz.compressToBase64(content)
const token = state.push('html_block', '', 0)
token.content = `<Monaco writable="${filepath}" code-lz="${encoded}" lang="${lang}" v-bind="${meta}" />\n`
const safeFilepath = JSON.stringify(filepath).slice(1, -1)
const encoded = lz.compressToBase64(content)
const token = state.push('html_block', '', 0)
token.content = `<Monaco writable="${safeFilepath}" code-lz="${encoded}" lang="${lang}" v-bind="${meta}" />\n`

Copilot uses AI. Check for mistakes.

const monacoEnabled = config.monaco === true || config.monaco === mode
if (!monacoEnabled) {
return renderHighlighted({ info: `${lang} ${rest}` })
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

If Monaco is disabled, this returns the raw highlighted fence HTML and short-circuits the transformer chain, meaning the normal CodeBlockWrapper transformer will never run for {monaco...} fences. Previously, {monaco} markers were removed and the code block still went through the standard wrapper pipeline. Consider returning undefined/null here (so the wrapper transformer can handle the block) and ensuring the {monaco...} marker doesn’t get mis-parsed as line ranges (see wrapper regex comment).

Suggested change
return renderHighlighted({ info: `${lang} ${rest}` })
return

Copilot uses AI. Check for mistakes.
import { defineCodeblockTransformer } from '@slidev/types'
import { escapeVueInCode, normalizeRangeStr } from '../utils'

const RE_BLOCK_INFO = /^([\w'-]+)?(?:[ \t]*|[ \t][ \w\t'-]*)(?:\[([^\]]*)\])?[ \t]*(?:\{([\w*,|-]+)\}[ \t]*(\{[^}]*\})?([^\r\n]*))?/
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The range capture \\{([\\w*,|-]+)\\} currently accepts arbitrary word characters, so markers like {monaco} can be misinterpreted as a line-range string and end up in :ranges (e.g. ['monaco']), which can break range parsing/highlighting. Tighten the range pattern to match only valid range syntax (typically digits plus ,|-*), so non-range meta markers aren’t treated as ranges.

Suggested change
const RE_BLOCK_INFO = /^([\w'-]+)?(?:[ \t]*|[ \t][ \w\t'-]*)(?:\[([^\]]*)\])?[ \t]*(?:\{([\w*,|-]+)\}[ \t]*(\{[^}]*\})?([^\r\n]*))?/
const RE_BLOCK_INFO = /^([\w'-]+)?(?:[ \t]*|[ \t][ \w\t'-]*)(?:\[([^\]]*)\])?[ \t]*(?:\{([\d,|\-*]+)\}[ \t]*(\{[^}]*\})?([^\r\n]*))?/

Copilot uses AI. Check for mistakes.
@antfu antfu merged commit 2bb691d into slidevjs:main Apr 8, 2026
3 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants