refactor(completions): centralize content scanning
Add a browser-safe ContentRegistry owning path and inline content stores, then derive completions from it. Remove the old block-processor, directive-scanner, and link-source modules in favor of scanDoc, deriveCompletions, and injectSparkDirectives, and register an inline resolver for the frontend file index.
This commit is contained in:
+48
-105
@@ -8,17 +8,12 @@ import { networkInterfaces } from "os";
|
||||
import { fileURLToPath } from "url";
|
||||
import { createJournalServer } from "../journal.js";
|
||||
import {
|
||||
scanCompletions,
|
||||
type CompletionsPayload,
|
||||
} from "../completions/index.js";
|
||||
import {
|
||||
processBlocks,
|
||||
type ProcessedBlocks,
|
||||
} from "../completions/block-processor.js";
|
||||
import {
|
||||
scanDirectives,
|
||||
type DirectiveScanResult,
|
||||
} from "../completions/directive-scanner.js";
|
||||
scanDoc,
|
||||
deriveCompletions,
|
||||
injectSparkDirectives,
|
||||
type ContentRegistry,
|
||||
} from "../content-registry.js";
|
||||
import type { CompletionsPayload } from "../completions/types.js";
|
||||
|
||||
interface ContentIndex {
|
||||
[path: string]: string;
|
||||
@@ -88,20 +83,10 @@ function getBestIP(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描目录内的 .md 等文件,生成内容索引与块数据
|
||||
* 扫描目录内的 .md 等文件,构建内容注册表(路径索引 + 每文档内联内容)
|
||||
*/
|
||||
export function scanDirectory(dir: string): {
|
||||
index: ContentIndex;
|
||||
blocks: ProcessedBlocks;
|
||||
directiveResults: DirectiveScanResult[];
|
||||
} {
|
||||
const index: ContentIndex = {};
|
||||
const blocks: ProcessedBlocks = {
|
||||
declarations: [],
|
||||
tagModifiers: [],
|
||||
};
|
||||
const directiveResults: DirectiveScanResult[] = [];
|
||||
const mdFiles: { content: string; relPath: string }[] = [];
|
||||
export function buildRegistry(dir: string): ContentRegistry {
|
||||
const registry: ContentRegistry = { pathIndex: {}, docContent: {} };
|
||||
|
||||
function scan(currentPath: string, relativePath: string) {
|
||||
const entries = readdirSync(currentPath);
|
||||
@@ -125,13 +110,11 @@ export function scanDirectory(dir: string): {
|
||||
try {
|
||||
const content = readFileSync(fullPath, "utf-8");
|
||||
if (entry.endsWith(".md")) {
|
||||
const result = processBlocks(content, normalizedRelPath, index);
|
||||
index[normalizedRelPath] = result.stripped;
|
||||
blocks.declarations.push(...result.blocks.declarations);
|
||||
blocks.tagModifiers.push(...result.blocks.tagModifiers);
|
||||
mdFiles.push({ content: result.stripped, relPath: normalizedRelPath });
|
||||
const result = scanDoc(content, normalizedRelPath);
|
||||
registry.pathIndex[normalizedRelPath] = result.stripped;
|
||||
registry.docContent[normalizedRelPath] = result.content;
|
||||
} else {
|
||||
index[normalizedRelPath] = content;
|
||||
registry.pathIndex[normalizedRelPath] = content;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`读取文件失败:${fullPath}`, e);
|
||||
@@ -142,31 +125,17 @@ export function scanDirectory(dir: string): {
|
||||
|
||||
scan(dir, "");
|
||||
|
||||
// ---- Directive scanning pass (after all blocks processed) ----
|
||||
const posixDir = dir.split(sep).join("/");
|
||||
for (const { content, relPath } of mdFiles) {
|
||||
const fileDir = posixRelDir(relPath);
|
||||
const result = scanDirectives(content, relPath, index, fileDir);
|
||||
|
||||
// Apply rewritten content back to index
|
||||
index[relPath] = result.rewritten;
|
||||
|
||||
// Inject new index entries (inline CSV bodies)
|
||||
for (const [key, value] of Object.entries(result.newIndexEntries)) {
|
||||
index[key] = value;
|
||||
}
|
||||
|
||||
directiveResults.push(result);
|
||||
// ---- Inject data-spark into real-file spark table directives ----
|
||||
for (const [relPath, content] of Object.entries(registry.pathIndex)) {
|
||||
if (!relPath.endsWith(".md")) continue;
|
||||
registry.pathIndex[relPath] = injectSparkDirectives(
|
||||
content,
|
||||
relPath,
|
||||
registry,
|
||||
);
|
||||
}
|
||||
|
||||
return { index, blocks, directiveResults };
|
||||
}
|
||||
|
||||
/** Get the POSIX directory of a file path */
|
||||
function posixRelDir(filePath: string): string {
|
||||
const parts = filePath.split("/");
|
||||
parts.pop();
|
||||
return parts.join("/") || ".";
|
||||
return registry;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,6 +200,7 @@ function createRequestHandler(
|
||||
distDir: string,
|
||||
getIndex: () => ContentIndex,
|
||||
getCompletions: () => CompletionsPayload,
|
||||
getRegistry: () => ContentRegistry,
|
||||
) {
|
||||
return (req: IncomingMessage, res: ServerResponse) => {
|
||||
const url = req.url || "/";
|
||||
@@ -248,6 +218,12 @@ function createRequestHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
// 1c. 处理 /__CONTENT_REGISTRY.json(含每文档内联内容,供运行时解析)
|
||||
if (filePath === "/__CONTENT_REGISTRY.json") {
|
||||
sendJson(res, getRegistry());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 处理 /static/ 目录(从 dist/web)
|
||||
if (filePath.startsWith("/static/")) {
|
||||
if (tryServeStatic(res, filePath, distDir)) {
|
||||
@@ -308,12 +284,7 @@ export function createContentServer(
|
||||
distPath: string = distDir,
|
||||
host: string = "0.0.0.0",
|
||||
): ContentServer {
|
||||
let contentIndex: ContentIndex = {};
|
||||
let collectedBlocks: ProcessedBlocks = {
|
||||
declarations: [],
|
||||
tagModifiers: [],
|
||||
};
|
||||
let directiveResults: DirectiveScanResult[] = [];
|
||||
let registry: ContentRegistry = { pathIndex: {}, docContent: {} };
|
||||
let completionsIndex: CompletionsPayload = {
|
||||
dice: [],
|
||||
links: [],
|
||||
@@ -322,21 +293,18 @@ export function createContentServer(
|
||||
tagModifiers: [],
|
||||
};
|
||||
|
||||
/** 从当前内容索引和已收集的块重新扫描补全数据 */
|
||||
/** 从当前注册表重新派生补全数据 */
|
||||
function recomputeCompletions(): void {
|
||||
completionsIndex = scanCompletions(contentIndex, collectedBlocks, directiveResults);
|
||||
completionsIndex = deriveCompletions(registry);
|
||||
console.log(
|
||||
`[completions] dice=${completionsIndex.dice.length} links=${completionsIndex.links.length} sparkTables=${completionsIndex.sparkTables.length} declarations=${completionsIndex.declarations.length} tagModifiers=${completionsIndex.tagModifiers.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 扫描内容目录生成索引
|
||||
// 扫描内容目录生成注册表
|
||||
console.log("正在扫描内容目录...");
|
||||
const scanResult = scanDirectory(contentDir);
|
||||
contentIndex = scanResult.index;
|
||||
collectedBlocks = scanResult.blocks;
|
||||
directiveResults = scanResult.directiveResults;
|
||||
console.log(`已索引 ${Object.keys(contentIndex).length} 个文件`);
|
||||
registry = buildRegistry(contentDir);
|
||||
console.log(`已索引 ${Object.keys(registry.pathIndex).length} 个文件`);
|
||||
recomputeCompletions();
|
||||
|
||||
// 监听文件变化
|
||||
@@ -356,20 +324,10 @@ export function createContentServer(
|
||||
path.endsWith(".svg")
|
||||
) {
|
||||
try {
|
||||
const content = readFileSync(path, "utf-8");
|
||||
const relPath = "/" + relative(contentDir, path).split(sep).join("/");
|
||||
if (relPath.endsWith(".md")) {
|
||||
const result = processBlocks(content, relPath, contentIndex);
|
||||
contentIndex[relPath] = result.stripped;
|
||||
// Re-scan to get fresh blocks (simpler than per-file merge)
|
||||
const rescan = scanDirectory(contentDir);
|
||||
collectedBlocks = rescan.blocks;
|
||||
directiveResults = rescan.directiveResults;
|
||||
recomputeCompletions();
|
||||
} else {
|
||||
contentIndex[relPath] = content;
|
||||
}
|
||||
console.log(`[新增] ${relPath}`);
|
||||
// 全量重建注册表以刷新跨文件派生(spark 注入)
|
||||
registry = buildRegistry(contentDir);
|
||||
recomputeCompletions();
|
||||
console.log(`[新增] ${path}`);
|
||||
} catch (e) {
|
||||
console.error(`读取新增文件失败:${path}`, e);
|
||||
}
|
||||
@@ -383,19 +341,9 @@ export function createContentServer(
|
||||
path.endsWith(".svg")
|
||||
) {
|
||||
try {
|
||||
const content = readFileSync(path, "utf-8");
|
||||
const relPath = "/" + relative(contentDir, path).split(sep).join("/");
|
||||
if (relPath.endsWith(".md")) {
|
||||
const result = processBlocks(content, relPath, contentIndex);
|
||||
contentIndex[relPath] = result.stripped;
|
||||
const rescan = scanDirectory(contentDir);
|
||||
collectedBlocks = rescan.blocks;
|
||||
directiveResults = rescan.directiveResults;
|
||||
recomputeCompletions();
|
||||
} else {
|
||||
contentIndex[relPath] = content;
|
||||
}
|
||||
console.log(`[更新] ${relPath}`);
|
||||
registry = buildRegistry(contentDir);
|
||||
recomputeCompletions();
|
||||
console.log(`[更新] ${path}`);
|
||||
} catch (e) {
|
||||
console.error(`读取更新文件失败:${path}`, e);
|
||||
}
|
||||
@@ -408,15 +356,9 @@ export function createContentServer(
|
||||
path.endsWith(".yarn") ||
|
||||
path.endsWith(".svg")
|
||||
) {
|
||||
const relPath = "/" + relative(contentDir, path).split(sep).join("/");
|
||||
delete contentIndex[relPath];
|
||||
console.log(`[删除] ${relPath}`);
|
||||
if (relPath.endsWith(".md")) {
|
||||
const rescan = scanDirectory(contentDir);
|
||||
collectedBlocks = rescan.blocks;
|
||||
directiveResults = rescan.directiveResults;
|
||||
recomputeCompletions();
|
||||
}
|
||||
registry = buildRegistry(contentDir);
|
||||
recomputeCompletions();
|
||||
console.log(`[删除] ${path}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -427,8 +369,9 @@ export function createContentServer(
|
||||
const handleRequest = createRequestHandler(
|
||||
contentDir,
|
||||
distPath,
|
||||
() => contentIndex,
|
||||
() => registry.pathIndex,
|
||||
() => completionsIndex,
|
||||
() => registry,
|
||||
);
|
||||
const server = createServer(handleRequest);
|
||||
|
||||
@@ -452,7 +395,7 @@ export function createContentServer(
|
||||
return {
|
||||
server,
|
||||
watcher,
|
||||
index: contentIndex,
|
||||
index: registry.pathIndex,
|
||||
completions: completionsIndex,
|
||||
close() {
|
||||
console.log("正在关闭内容服务器...");
|
||||
|
||||
Reference in New Issue
Block a user