build-search-index.js
3.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#!/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(/<script[\s\S]*?<\/script>/gi, ' ');
html = html.replace(/<style[\s\S]*?<\/style>/gi, ' ');
html = html.replace(/<svg[\s\S]*?<\/svg>/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(/<title>([\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}`));