chore: 初始化项目仓库
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
Reference in New Issue
Block a user