/** * Nuxt Content v3 (minimark) AST 工具函数 * 集中管理 body AST 的文本提取、标题提取、slugify 等操作 */ // --- 文本提取 --- const SKIP_TAGS = new Set(['style', 'script', 'pre', 'code', 'head', 'meta', 'link']) export function extractTextFromAst(node: any): string { if (!node) return '' if (typeof node === 'string') return node if (Array.isArray(node)) { const tag = typeof node[0] === 'string' ? node[0] : '' if (SKIP_TAGS.has(tag)) return '' const start = typeof node[1] === 'object' && !Array.isArray(node[1]) && node[1] !== null ? 2 : 1 return node.slice(start).map(extractTextFromAst).join('') } if (typeof node === 'object') { if (typeof node.value === 'string') return node.value if (node.children) return extractTextFromAst(node.children) if (node.value) return extractTextFromAst(node.value) } return '' } // --- 标题提取 --- export interface Heading { id: string text: string level: number } function extractTextFromNode(node: any): string { if (!node) return '' if (typeof node === 'string') return node if (Array.isArray(node)) return node.map(extractTextFromNode).join('') if (typeof node === 'object') { if (typeof node.value === 'string') return node.value if (node.children) return extractTextFromNode(node.children) } return '' } export function extractHeadings(node: any, list: Heading[] = []): Heading[] { if (!node) return list if (Array.isArray(node)) { if (typeof node[0] === 'string' && /^h[1-6]$/.test(node[0])) { const level = Number(node[0].charAt(1)) const props = typeof node[1] === 'object' && !Array.isArray(node[1]) ? node[1] : {} const childStart = typeof node[1] === 'object' && !Array.isArray(node[1]) ? 2 : 1 const text = node.slice(childStart).map(extractTextFromNode).join('').trim() if (text) { list.push({ id: props.id || slugify(text), text, level }) } } for (const child of node) { if (Array.isArray(child) || (typeof child === 'object' && child !== null)) { extractHeadings(child, list) } } return list } if (typeof node === 'object') { const tag = node.tag || node.type if (tag && /^h[1-6]$/.test(tag)) { const level = Number(tag.charAt(1)) const text = extractTextFromNode(node).trim() if (text) { list.push({ id: node.props?.id || slugify(text), text, level }) } } if (node.children) extractHeadings(node.children, list) if (node.value) extractHeadings(node.value, list) if (node.body) extractHeadings(node.body, list) } return list } // --- slugify --- export function slugify(text: string): string { return text .trim() .toLowerCase() .replace(/[\s ]+/g, '-') .replace(/[^\p{L}\p{N}\-]/gu, '') .replace(/-+/g, '-') .replace(/^-|-$/g, '') || 'section' } // --- 去重(给 heading id 加后缀)--- export function deduplicateIds(list: Heading[]): Heading[] { const seen = new Map() for (const h of list) { if (seen.has(h.id)) { const n = (seen.get(h.id) || 1) + 1 seen.set(h.id, n) h.id = `${h.id}-${n}` } else { seen.set(h.id, 1) } } return list } // --- 从 body 提取所有标题(去重后)--- export function getHeadingsFromBody(body: Record | null | undefined): Heading[] { if (!body) return [] const list: Heading[] = [] extractHeadings(body.value || body, list) return deduplicateIds(list) }