Files
SuBlog/utils/content-ast.ts
T
sutong b7addb5c7a chore: 初始化项目仓库
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 21:27:39 +08:00

117 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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)
}