From 97ff43a50febc73fc0ffe7b57a9bc6b019dce3cf Mon Sep 17 00:00:00 2001 From: Bitshifter-9 Date: Mon, 17 Nov 2025 20:28:07 +0530 Subject: [PATCH 1/5] fix: keep lists valid inside html blocks --- .../node/syntax/transform/html-list-indent.ts | 165 ++++++++++++++++++ .../slidev/node/syntax/transform/index.ts | 2 + test/__snapshots__/transform.test.ts.snap | 13 ++ test/transform.test.ts | 18 ++ 4 files changed, 198 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..70a1ad0a77 --- /dev/null +++ b/packages/slidev/node/syntax/transform/html-list-indent.ts @@ -0,0 +1,165 @@ +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 + + const char = trimmed[0] + if (char !== '`' && char !== '~') + return null + + let size = 1 + while (size < trimmed.length && trimmed[size] === char) + size++ + + if (size < 3) + return null + + return { char, size } +} + +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) { + const currentFenceChar = fenceMatch.char + if (fenceChar === null) { + fenceChar = currentFenceChar + fenceSize = fenceMatch.size + } + else if (currentFenceChar === 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('