#!/usr/bin/env node /** * build-search-index.js * * 扫 docs/prd/newfunction/*.html,提取 title + headings + body 纯文本, * 输出 assets/search-index.json (~30-80KB)。 * * 零依赖,用 regex 拆 HTML。每个 page 取前 4KB 纯文本入 index。 */ const fs = require('fs'); const path = require('path'); const DOCS_DIR = path.resolve(__dirname, '../docs/prd/newfunction'); const OUT_PATH = path.join(DOCS_DIR, 'assets/search-index.json'); const SKIP_FILES = new Set(['index.html']); // index 不需要搜(hero 没什么文本) // 把 search-index.json / build-manifest.json 等也排除(即使它们存在) const SKIP_PATTERN = /^(search-index|build-manifest|_)/; function stripTagsKeepText(html){ // 移除 script / style 块 html = html.replace(//gi, ' '); html = html.replace(//gi, ' '); html = html.replace(//gi, ' '); // SVG 大块图像跳过 // 标签转空格 html = html.replace(/<[^>]+>/g, ' '); // 实体 html = html.replace(/ /g, ' ') .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/'/g, "'"); // 合并空白 html = html.replace(/\s+/g, ' ').trim(); return html; } function extractTitle(html){ const t = html.match(/([\s\S]*?)<\/title>/i); return t ? t[1].replace(/\s*·\s*LemonBoss PRD\s*$/, '').trim() : null; } function extractH1(html){ const m = html.match(/<h1[^>]*>([\s\S]*?)<\/h1>/i); return m ? stripTagsKeepText(m[1]) : null; } function extractHeadings(html){ const out = []; const re = /<h([2-4])[^>]*>([\s\S]*?)<\/h\1>/gi; let m; while ((m = re.exec(html)) !== null){ out.push(stripTagsKeepText(m[2])); } return out; } function extractArticleBody(html){ // <article class="body">...</article> if present, else everything inside <main>...</main> let m = html.match(/<article\s+class="body"[^>]*>([\s\S]*?)<\/article>/i); if (!m) m = html.match(/<main\s+class="page"[^>]*>([\s\S]*?)<\/main>/i); return m ? m[1] : html; } function extractDataDefs(html){ // 抽出 SDD MVCP 的 data-def-name 作为额外搜索 token const out = []; const re = /data-def-name="([^"]+)"/g; let m; while ((m = re.exec(html)) !== null){ out.push(m[1]); } return out; } function buildEntry(file, html){ const title = extractTitle(html) || extractH1(html) || file; const headings = extractHeadings(html); const body = extractArticleBody(html); const text = stripTagsKeepText(body).slice(0, 4000); const defs = extractDataDefs(html); return { f: file, title, h1: extractH1(html), headings, defs, content: text, }; } const files = fs.readdirSync(DOCS_DIR) .filter(f => f.endsWith('.html')) .filter(f => !SKIP_FILES.has(f) && !SKIP_PATTERN.test(f)) .sort(); const entries = []; for (const f of files){ const html = fs.readFileSync(path.join(DOCS_DIR, f), 'utf8'); entries.push(buildEntry(f, html)); } const result = { version: '1', generated: new Date().toISOString(), pages: entries.length, entries, }; fs.mkdirSync(path.dirname(OUT_PATH), { recursive: true }); fs.writeFileSync(OUT_PATH, JSON.stringify(result)); const size = fs.statSync(OUT_PATH).size; console.log(`✓ search-index.json built`); console.log(` pages : ${entries.length}`); console.log(` size : ${(size/1024).toFixed(1)} KB`); console.log(` path : ${OUT_PATH}`); // Brief preview console.log(`\n--- first 3 entries (title only) ---`); entries.slice(0, 3).forEach(e => console.log(` · ${e.f.padEnd(34)} → ${e.title}`));