feat: extract and index inline file blocks in markdown
This commit is contained in:
parent
9a5ee695c2
commit
2a9eee6410
|
|
@ -11,6 +11,7 @@ import {
|
||||||
scanCompletions,
|
scanCompletions,
|
||||||
type CompletionsPayload,
|
type CompletionsPayload,
|
||||||
} from "../completions/index.js";
|
} from "../completions/index.js";
|
||||||
|
import { extractInlineBlocks } from "../inline-blocks.js";
|
||||||
|
|
||||||
interface ContentIndex {
|
interface ContentIndex {
|
||||||
[path: string]: string;
|
[path: string]: string;
|
||||||
|
|
@ -105,7 +106,16 @@ export function scanDirectory(dir: string): ContentIndex {
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const content = readFileSync(fullPath, "utf-8");
|
const content = readFileSync(fullPath, "utf-8");
|
||||||
index[normalizedRelPath] = content;
|
if (entry.endsWith(".md")) {
|
||||||
|
const stripped = extractInlineBlocks(
|
||||||
|
content,
|
||||||
|
normalizedRelPath,
|
||||||
|
index,
|
||||||
|
);
|
||||||
|
index[normalizedRelPath] = stripped;
|
||||||
|
} else {
|
||||||
|
index[normalizedRelPath] = content;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`读取文件失败:${fullPath}`, e);
|
console.error(`读取文件失败:${fullPath}`, e);
|
||||||
}
|
}
|
||||||
|
|
@ -295,9 +305,18 @@ export function createContentServer(
|
||||||
try {
|
try {
|
||||||
const content = readFileSync(path, "utf-8");
|
const content = readFileSync(path, "utf-8");
|
||||||
const relPath = "/" + relative(contentDir, path).split(sep).join("/");
|
const relPath = "/" + relative(contentDir, path).split(sep).join("/");
|
||||||
contentIndex[relPath] = content;
|
if (relPath.endsWith(".md")) {
|
||||||
|
const stripped = extractInlineBlocks(
|
||||||
|
content,
|
||||||
|
relPath,
|
||||||
|
contentIndex,
|
||||||
|
);
|
||||||
|
contentIndex[relPath] = stripped;
|
||||||
|
recomputeCompletions();
|
||||||
|
} else {
|
||||||
|
contentIndex[relPath] = content;
|
||||||
|
}
|
||||||
console.log(`[新增] ${relPath}`);
|
console.log(`[新增] ${relPath}`);
|
||||||
if (relPath.endsWith(".md")) recomputeCompletions();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`读取新增文件失败:${path}`, e);
|
console.error(`读取新增文件失败:${path}`, e);
|
||||||
}
|
}
|
||||||
|
|
@ -312,9 +331,18 @@ export function createContentServer(
|
||||||
try {
|
try {
|
||||||
const content = readFileSync(path, "utf-8");
|
const content = readFileSync(path, "utf-8");
|
||||||
const relPath = "/" + relative(contentDir, path).split(sep).join("/");
|
const relPath = "/" + relative(contentDir, path).split(sep).join("/");
|
||||||
contentIndex[relPath] = content;
|
if (relPath.endsWith(".md")) {
|
||||||
|
const stripped = extractInlineBlocks(
|
||||||
|
content,
|
||||||
|
relPath,
|
||||||
|
contentIndex,
|
||||||
|
);
|
||||||
|
contentIndex[relPath] = stripped;
|
||||||
|
recomputeCompletions();
|
||||||
|
} else {
|
||||||
|
contentIndex[relPath] = content;
|
||||||
|
}
|
||||||
console.log(`[更新] ${relPath}`);
|
console.log(`[更新] ${relPath}`);
|
||||||
if (relPath.endsWith(".md")) recomputeCompletions();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`读取更新文件失败:${path}`, e);
|
console.error(`读取更新文件失败:${path}`, e);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { posix } from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex to match a fenced code block with a `file="..."` attribute, e.g.:
|
||||||
|
*
|
||||||
|
* ```csv file="card-data.csv"
|
||||||
|
* name, cost, effect
|
||||||
|
* Fireball, 1, Blast em
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ```yaml file="monsters.yaml"
|
||||||
|
* goblin:
|
||||||
|
* hp: 7
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The language identifier is optional — only `file="..."` matters.
|
||||||
|
*
|
||||||
|
* Group 1: the info string after the opening fence (e.g. `csv file="card-data.csv"`)
|
||||||
|
* Group 2: the code block body
|
||||||
|
*/
|
||||||
|
const INLINE_FILE_BLOCK_RE = /^```\w*\s+file="([^"]+)"(?:\s+\w+="[^"]*")*\s*\n([\s\S]*?)```\s*$/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process fenced code blocks that contain a `file="..."` attribute.
|
||||||
|
*
|
||||||
|
* For each block like:
|
||||||
|
* ```lang file="card-data.csv"
|
||||||
|
* content
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* 1. Strips the block from the returned text (so it doesn't render)
|
||||||
|
* 2. Injects a synthetic entry into `index` at the resolved file path
|
||||||
|
*
|
||||||
|
* @param content - Raw file content
|
||||||
|
* @param fileRelativePath - Normalized path of the containing file (e.g. "/rules/combat.md")
|
||||||
|
* @param index - The content index to inject synthetic entries into
|
||||||
|
* @returns Content with inline blocks removed
|
||||||
|
*/
|
||||||
|
export function extractInlineBlocks(
|
||||||
|
content: string,
|
||||||
|
fileRelativePath: string,
|
||||||
|
index: Record<string, string>,
|
||||||
|
): string {
|
||||||
|
const matches: { filename: string; body: string }[] = [];
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
|
INLINE_FILE_BLOCK_RE.lastIndex = 0;
|
||||||
|
|
||||||
|
while ((match = INLINE_FILE_BLOCK_RE.exec(content)) !== null) {
|
||||||
|
matches.push({ filename: match[1], body: match[2] });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.length === 0) return content;
|
||||||
|
|
||||||
|
const stripped = content.replace(INLINE_FILE_BLOCK_RE, "");
|
||||||
|
|
||||||
|
for (const { filename, body } of matches) {
|
||||||
|
const fileDir = posix.dirname(fileRelativePath);
|
||||||
|
const resolvedPath = posix.join(fileDir, filename);
|
||||||
|
|
||||||
|
index[resolvedPath] = body;
|
||||||
|
console.log(
|
||||||
|
`[inline] Extracted ${resolvedPath} from ${fileRelativePath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stripped;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue