From 1e8d5a61be728197f5106d92cba9c71e35c71501 Mon Sep 17 00:00:00 2001 From: Bitshifter-9 Date: Tue, 18 Nov 2025 09:30:43 +0530 Subject: [PATCH 1/2] fix: workaround for prettier removing list indentation in HTML blocks This adds a transformer that ensures lists inside HTML blocks maintain proper indentation to prevent Vue parse errors when prettier-plugin-slidev removes the required indentation. 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. Fixes #2337 --- .../node/syntax/transform/html-list-indent.ts | 170 ++++++++++++++++++ .../slidev/node/syntax/transform/index.ts | 2 + test/__snapshots__/transform.test.ts.snap | 13 ++ test/transform.test.ts | 18 ++ 4 files changed, 203 insertions(+) create mode 100644 packages/slidev/node/syntax/transform/html-list-indent.ts 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..d5dd173f3f --- /dev/null +++ b/packages/slidev/node/syntax/transform/html-list-indent.ts @@ -0,0 +1,170 @@ +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 + + 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() + for (let idx = stack.length - 1; idx >= 0; idx--) { + if (stack[idx].tag === tagName) { + stack.splice(idx) + break + } + } + continue + } + + if (trimmed.startsWith('