diff --git a/.github/workflows/cr.yml b/.github/workflows/cr.yml index 3e2541d4c1..b1c60388f1 100644 --- a/.github/workflows/cr.yml +++ b/.github/workflows/cr.yml @@ -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 }} diff --git a/packages/slidev/node/syntax/transform/html-list-indent.ts b/packages/slidev/node/syntax/transform/html-list-indent.ts new file mode 100644 index 0000000000..3de19d3022 --- /dev/null +++ b/packages/slidev/node/syntax/transform/html-list-indent.ts @@ -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('