Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/cr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:
- name: Build
run: nr build

- run: nlx pkg-pr-new publish './packages/create-app' './packages/client' './packages/create-theme' './packages/parser' './packages/slidev' './packages/types' --pnpm
- name: Publish packages (skip on forks)
if: github.repository == 'slidevjs/slidev'
run: nlx pkg-pr-new publish './packages/create-app' './packages/client' './packages/create-theme' './packages/parser' './packages/slidev' './packages/types' --pnpm
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
189 changes: 189 additions & 0 deletions packages/slidev/node/syntax/transform/html-list-indent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import type { MarkdownTransformContext } from '@slidev/types'

const voidTags = new Set([
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr',
])

const rawContentTags = new Set([
'pre',
'code',
'script',
'style',
'textarea',
])

const openTagRE = /^\s*<([A-Z][\w:-]*)(?:\s[^>]*)?>\s*$/i
const closeTagRE = /^\s*<\/([A-Z][\w:-]*)\s*>\s*$/i
const listMarkerRE = /^(\s*)(?:[-+*]|\d{1,9}[.)])\s+/

interface HtmlStackItem {
tag: string
indent: number
}

function countIndent(input: string): number {
let count = 0
for (const ch of input) {
if (ch === ' ')
count += 1
else if (ch === '\t')
count += 2
else
break
}
return count
}

function repeatSpaces(length: number) {
return ' '.repeat(Math.max(length, 0))
}

function matchFence(line: string): { char: '`' | '~', size: number } | null {
const trimmed = line.trimStart()
if (!trimmed)
return null
if (trimmed.startsWith('```')) {
const match = trimmed.match(/^`{3,}/)
return match ? { char: '`', size: match[0].length } : null
}
if (trimmed.startsWith('~~~')) {
const match = trimmed.match(/^~{3,}/)
return match ? { char: '~', size: match[0].length } : null
}
return null
}

/**
* Workaround for prettier-plugin-slidev removing indentation from lists inside HTML blocks.
* This ensures lists inside HTML blocks maintain proper indentation to prevent Vue parse errors.
*
* Note: This is a workaround. The proper fix should be in prettier-plugin-slidev to preserve
* indentation during formatting. This transformer ensures compatibility even when Prettier
* removes the required indentation.
*
* @see https://github.com/slidevjs/slidev/issues/2337
*/
export function transformHtmlListIndent(ctx: MarkdownTransformContext) {
const code = ctx.s.toString()
if (!code.includes('<'))
return

const lines = code.split(/\r?\n/)
const newline = code.includes('\r\n') ? '\r\n' : '\n'

const stack: HtmlStackItem[] = []
let fenceChar: '`' | '~' | null = null
let fenceSize = 0
let changed = false
let hasVueComponentAfterLastHtml = false

for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const trimmed = line.trim()

const fenceMatch = matchFence(line)
if (fenceMatch) {
if (fenceChar === null) {
fenceChar = fenceMatch.char
fenceSize = fenceMatch.size
}
else if (fenceMatch.char === fenceChar && fenceMatch.size >= fenceSize) {
fenceChar = null
fenceSize = 0
}
continue
}

if (fenceChar)
continue

if (!trimmed) {
continue
}

const closeMatch = closeTagRE.exec(trimmed)
if (closeMatch) {
const tagName = closeMatch[1].toLowerCase()
// Check if this is a Vue component closing tag
const isVueComponent = tagName.startsWith('v-') || tagName === 'template'
if (isVueComponent) {
hasVueComponentAfterLastHtml = false
continue
}
for (let idx = stack.length - 1; idx >= 0; idx--) {
if (stack[idx].tag === tagName) {
stack.splice(idx)
break
}
}
continue
}

if (trimmed.startsWith('<!--'))
continue

const openMatch = openTagRE.exec(trimmed)
if (openMatch) {
const tagName = openMatch[1]
const tagNameLower = tagName.toLowerCase()
// Skip Vue components (v-*), PascalCase components, and template tags
const isPascalCase = tagName.length > 1 && tagName[0] >= 'A' && tagName[0] <= 'Z' && tagName[1] >= 'a' && tagName[1] <= 'z'
if (tagNameLower.startsWith('v-') || isPascalCase || tagNameLower === 'template') {
hasVueComponentAfterLastHtml = true
continue
}
if (!voidTags.has(tagNameLower) && !trimmed.endsWith('/>')) {
hasVueComponentAfterLastHtml = false
stack.push({
tag: tagNameLower,
indent: countIndent(line),
})
}
continue
}

if (!stack.length)
continue

if (stack.some(item => rawContentTags.has(item.tag)))
continue

// Skip lists if there's a Vue component after the last HTML tag
if (hasVueComponentAfterLastHtml)
continue

const listMatch = listMarkerRE.exec(line)
if (!listMatch)
continue

const currentIndent = countIndent(listMatch[1])
const baseIndent = stack.at(-1)!.indent
const targetIndent = Math.max(baseIndent, 2)

if (currentIndent >= targetIndent)
continue

const content = line.slice(listMatch[1].length)
lines[i] = `${repeatSpaces(targetIndent)}${content}`
changed = true
}

if (!changed)
return

ctx.s.overwrite(0, code.length, lines.join(newline))
}
2 changes: 2 additions & 0 deletions packages/slidev/node/syntax/transform/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { MarkdownTransformer, ResolvedSlidevOptions } from '@slidev/types'
import setupTransformers from '../../setups/transformers'
import { transformCodeWrapper } from './code-wrapper'
import { transformHtmlListIndent } from './html-list-indent'
import { transformPageCSS } from './in-page-css'
import { transformKaTexWrapper } from './katex-wrapper'
import { transformMagicMove } from './magic-move'
Expand All @@ -16,6 +17,7 @@ export async function getMarkdownTransformers(options: ResolvedSlidevOptions): P
...extras.pre,

transformSnippet,
transformHtmlListIndent,
options.data.config.highlighter === 'shiki' && transformMagicMove,

...extras.preCodeblock,
Expand Down
13 changes: 13 additions & 0 deletions test/__snapshots__/transform.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ function _foo() {
"
`;

exports[`html list indent within html blocks 1`] = `
"
<div>
<div>

- A
- B

</div>
</div>
"
`;

exports[`inline CSS 1`] = `
"
# Page
Expand Down
18 changes: 18 additions & 0 deletions test/transform.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
import { expect, it } from 'vitest'
import { transformCodeWrapper } from '../packages/slidev/node/syntax/transform/code-wrapper'
import { transformHtmlListIndent } from '../packages/slidev/node/syntax/transform/html-list-indent'
import { transformPageCSS } from '../packages/slidev/node/syntax/transform/in-page-css'
import { transformMermaid } from '../packages/slidev/node/syntax/transform/mermaid'
import { transformPlantUml } from '../packages/slidev/node/syntax/transform/plant-uml'
import { transformSlotSugar } from '../packages/slidev/node/syntax/transform/slot-sugar'
import { transformSnippet } from '../packages/slidev/node/syntax/transform/snippet'
import { createTransformContext } from './_tutils'

it('html list indent within html blocks', () => {
const ctx = createTransformContext(`
<div>
<div>

- A
- B

</div>
</div>
`)

transformHtmlListIndent(ctx)

expect(ctx.s.toString()).toMatchSnapshot()
})

it('slot-sugar', () => {
const ctx = createTransformContext(`
# Page
Expand Down