b7addb5c7a
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
117 lines
3.4 KiB
TypeScript
117 lines
3.4 KiB
TypeScript
/**
|
||
* 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<string, number>()
|
||
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<string, any> | null | undefined): Heading[] {
|
||
if (!body) return []
|
||
const list: Heading[] = []
|
||
extractHeadings(body.value || body, list)
|
||
return deduplicateIds(list)
|
||
}
|