mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 15:03:57 +08:00
docs: add VitePress documentation site
- Add docs directory with VitePress configuration - Add GitHub Actions workflow for docs build and deploy - Support bilingual (English/Chinese) documentation - Include search, custom theme, and responsive design
This commit is contained in:
185
docs/scripts/build-search-index.mjs
Normal file
185
docs/scripts/build-search-index.mjs
Normal file
@@ -0,0 +1,185 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import FlexSearch from 'flexsearch'
|
||||
import {
|
||||
createFlexSearchIndex,
|
||||
FLEXSEARCH_INDEX_VERSION
|
||||
} from '../.vitepress/search/flexsearch.mjs'
|
||||
|
||||
const ROOT_DIR = process.cwd()
|
||||
const PUBLIC_DIR = path.join(ROOT_DIR, 'public')
|
||||
|
||||
const EXCLUDED_DIRS = new Set([
|
||||
'.github',
|
||||
'.vitepress',
|
||||
'.workflow',
|
||||
'node_modules',
|
||||
'public',
|
||||
'scripts'
|
||||
])
|
||||
|
||||
function toPosixPath(filePath) {
|
||||
return filePath.replaceAll(path.sep, '/')
|
||||
}
|
||||
|
||||
function getLocaleKey(relativePosixPath) {
|
||||
return relativePosixPath.startsWith('zh/') ? 'zh' : 'root'
|
||||
}
|
||||
|
||||
function toPageUrl(relativePosixPath) {
|
||||
const withoutExt = relativePosixPath.replace(/\.md$/i, '')
|
||||
|
||||
if (withoutExt === 'index') return '/'
|
||||
if (withoutExt.endsWith('/index')) return `/${withoutExt.slice(0, -'/index'.length)}/`
|
||||
return `/${withoutExt}`
|
||||
}
|
||||
|
||||
function extractTitle(markdown, relativePosixPath) {
|
||||
const normalized = markdown.replaceAll('\r\n', '\n')
|
||||
|
||||
const frontmatterMatch = normalized.match(/^---\n([\s\S]*?)\n---\n/)
|
||||
if (frontmatterMatch) {
|
||||
const fm = frontmatterMatch[1]
|
||||
const titleLine = fm
|
||||
.split('\n')
|
||||
.map((l) => l.trim())
|
||||
.find((l) => l.toLowerCase().startsWith('title:'))
|
||||
|
||||
if (titleLine) {
|
||||
const raw = titleLine.slice('title:'.length).trim()
|
||||
return raw.replace(/^['"]|['"]$/g, '') || undefined
|
||||
}
|
||||
}
|
||||
|
||||
const firstH1 = normalized.match(/^#\s+(.+)\s*$/m)
|
||||
if (firstH1?.[1]) return firstH1[1].trim()
|
||||
|
||||
const fallback = path.basename(relativePosixPath, '.md')
|
||||
return fallback
|
||||
.replaceAll('-', ' ')
|
||||
.replaceAll('_', ' ')
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase())
|
||||
}
|
||||
|
||||
function stripFrontmatter(markdown) {
|
||||
const normalized = markdown.replaceAll('\r\n', '\n')
|
||||
return normalized.replace(/^---\n[\s\S]*?\n---\n/, '')
|
||||
}
|
||||
|
||||
function stripMarkdown(markdown) {
|
||||
return (
|
||||
markdown
|
||||
// SFC blocks
|
||||
.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, ' ')
|
||||
// Code fences
|
||||
.replace(/```[\s\S]*?```/g, ' ')
|
||||
.replace(/~~~[\s\S]*?~~~/g, ' ')
|
||||
// Inline code
|
||||
.replace(/`[^`]*`/g, ' ')
|
||||
// Images and links
|
||||
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1')
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
||||
// Headings / blockquotes
|
||||
.replace(/^#{1,6}\s+/gm, '')
|
||||
.replace(/^>\s?/gm, '')
|
||||
// Lists
|
||||
.replace(/^\s*[-*+]\s+/gm, '')
|
||||
.replace(/^\s*\d+\.\s+/gm, '')
|
||||
// Emphasis
|
||||
.replace(/[*_~]+/g, ' ')
|
||||
// HTML tags
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
// Collapse whitespace
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
)
|
||||
}
|
||||
|
||||
async function collectMarkdownFiles(dir) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true })
|
||||
const files = []
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
if (EXCLUDED_DIRS.has(entry.name)) continue
|
||||
files.push(...(await collectMarkdownFiles(path.join(dir, entry.name))))
|
||||
continue
|
||||
}
|
||||
|
||||
if (!entry.isFile()) continue
|
||||
if (!entry.name.toLowerCase().endsWith('.md')) continue
|
||||
files.push(path.join(dir, entry.name))
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
async function buildIndexForLocale(localeKey, relativePosixPaths) {
|
||||
const index = createFlexSearchIndex(FlexSearch)
|
||||
const docs = []
|
||||
|
||||
let nextId = 1
|
||||
for (const rel of relativePosixPaths) {
|
||||
const abs = path.join(ROOT_DIR, rel)
|
||||
const markdown = await fs.readFile(abs, 'utf-8')
|
||||
|
||||
const title = extractTitle(markdown, rel)
|
||||
const content = stripMarkdown(stripFrontmatter(markdown))
|
||||
const url = toPageUrl(rel)
|
||||
|
||||
const searchable = `${title}\n${content}`.trim()
|
||||
if (!searchable) continue
|
||||
|
||||
const id = nextId++
|
||||
index.add(id, searchable)
|
||||
docs.push({
|
||||
id,
|
||||
title,
|
||||
url,
|
||||
excerpt: content.slice(0, 180)
|
||||
})
|
||||
}
|
||||
|
||||
const exported = {}
|
||||
await index.export((key, data) => {
|
||||
exported[key] = data
|
||||
})
|
||||
|
||||
return {
|
||||
version: FLEXSEARCH_INDEX_VERSION,
|
||||
locale: localeKey,
|
||||
index: exported,
|
||||
docs
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await fs.mkdir(PUBLIC_DIR, { recursive: true })
|
||||
|
||||
const allMarkdownAbs = await collectMarkdownFiles(ROOT_DIR)
|
||||
const allMarkdownRel = allMarkdownAbs
|
||||
.map((abs) => toPosixPath(path.relative(ROOT_DIR, abs)))
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
|
||||
const byLocale = new Map([
|
||||
['root', []],
|
||||
['zh', []]
|
||||
])
|
||||
|
||||
for (const rel of allMarkdownRel) {
|
||||
const localeKey = getLocaleKey(rel)
|
||||
byLocale.get(localeKey)?.push(rel)
|
||||
}
|
||||
|
||||
for (const [localeKey, relFiles] of byLocale.entries()) {
|
||||
const payload = await buildIndexForLocale(localeKey, relFiles)
|
||||
const outFile = path.join(PUBLIC_DIR, `search-index.${localeKey}.json`)
|
||||
await fs.writeFile(outFile, JSON.stringify(payload), 'utf-8')
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user