@ubean/content 0.4.6 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +87 -2
- package/dist/index.js +3 -2
- package/dist/{runtime-Dhw-W7yi.js → runtime-Cdi5T8aE.js} +15 -2
- package/dist/runtime-DQ65gYP0.d.ts +75 -0
- package/dist/runtime.d.ts +3 -2
- package/dist/runtime.js +2 -2
- package/dist/search-CNUu8Xvf.js +328 -0
- package/dist/{runtime-bK3ae0UQ.d.ts → types-B9HP8tUf.d.ts} +71 -64
- package/dist/vite.d.ts +8 -0
- package/dist/vite.js +7 -1
- package/dist/vue.d.ts +42 -0
- package/dist/vue.js +66 -0
- package/package.json +13 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,90 @@
|
|
|
1
|
+
import { _ as SearchSection, a as ContentModuleOptions, b as SectionSearchOptions, c as ContentSchema, d as ContentTocItem, f as ContentType, g as SearchHit, h as ParsedContentMeta, i as ContentFieldSchema, l as ContentSearchOptions, m as MarkdownNode, n as ContentCollection, o as ContentNavigationItem, p as GenerateSearchSectionsOptions, r as ContentDocument, s as ContentQueryBuilder, t as ContentBody, u as ContentSourceConfig, v as SectionQueryOptions, y as SectionSearchEngine } from "./types-B9HP8tUf.js";
|
|
1
2
|
import ubeanContentPlugin, { UbeanContentOptions } from "./vite.js";
|
|
2
|
-
import {
|
|
3
|
+
import { S as parseMarkdown, _ as createQueryBuilder, a as getCollection, b as parseContent, c as parseContentFile, d as registerContent, f as ContentSourceScanConfig, g as createContentCollection, h as buildNavigation, i as fetchContentNavigation, l as queryCollection, m as scanContentSources, n as configureContentRuntime, o as getContentItem, p as ScanContentSourcesOptions, r as defineCollection, s as listCollections, t as bootstrapContentFromDisk, u as queryCollectionSearchSections, v as defineContentCollection, x as parseFrontmatter, y as generateId } from "./runtime-DQ65gYP0.js";
|
|
4
|
+
//#region src/search.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* 将单个文档按标题层级切分为可搜索的 sections。
|
|
7
|
+
*
|
|
8
|
+
* - 首个标题之前的内容为一个 intro section(level 0,title 为页面标题)
|
|
9
|
+
* - [minHeading, maxHeading] 范围内的标题开启新 section,并维护父级标题链
|
|
10
|
+
* - 范围外的标题(如 minHeading=2 时的 h1)视为正文内容
|
|
11
|
+
* - `_draft` / `_partial` / `_empty` 文档由上层过滤
|
|
12
|
+
*/
|
|
13
|
+
export declare function splitDocumentIntoSearchSections(doc: ContentDocument, options?: GenerateSearchSectionsOptions): SearchSection[];
|
|
14
|
+
/** 将文档集合批量切分为 sections */
|
|
15
|
+
export declare function generateSearchSections(documents: ContentDocument[], options?: GenerateSearchSectionsOptions): SearchSection[];
|
|
16
|
+
/** 将 `{ collection: documents }` 快照转换为 `{ collection: sections }` payload */
|
|
17
|
+
export declare function generateSearchSectionsSnapshot(snapshot: Record<string, ContentDocument[]>, options?: GenerateSearchSectionsOptions): Record<string, SearchSection[]>;
|
|
18
|
+
/**
|
|
19
|
+
* CJK 感知分词器(与引擎共用):
|
|
20
|
+
* - 拉丁词按 Unicode 词字符切分
|
|
21
|
+
* - CJK 连续段优先用 `Intl.Segmenter`(词级),降级为 unigram + bigram
|
|
22
|
+
* - 混合词(如 `vue每个`)按 CJK 子段切分
|
|
23
|
+
*/
|
|
24
|
+
export declare function tokenizeText(text: string): string[];
|
|
25
|
+
/**
|
|
26
|
+
* 创建章节搜索引擎。
|
|
27
|
+
*
|
|
28
|
+
* 优先使用 MiniSearch(可选依赖,支持前缀 + 模糊匹配 + 字段加权),
|
|
29
|
+
* 未安装时降级为内置 fallback 引擎(精确/前缀匹配)。
|
|
30
|
+
* 两条路径共享 CJK 感知分词器。
|
|
31
|
+
*/
|
|
32
|
+
export declare function createSectionSearch(sections: SearchSection[], options?: SectionSearchOptions): Promise<SectionSearchEngine>;
|
|
33
|
+
/** 一次性搜索便捷函数(每次调用重建引擎,适合低频场景;高频场景请缓存 `createSectionSearch` 结果) */
|
|
34
|
+
export declare function searchSections(sections: SearchSection[], query: string, options?: SectionSearchOptions & SectionQueryOptions): Promise<SearchHit[]>;
|
|
35
|
+
interface ResolvedContentSearchConfig {
|
|
36
|
+
/** 输出 `__search.json` sections payload */
|
|
37
|
+
sections: boolean;
|
|
38
|
+
/** 构建后运行 Pagefind 索引 */
|
|
39
|
+
pagefind: boolean;
|
|
40
|
+
/** 透传给 Pagefind `createIndex()` 的选项 */
|
|
41
|
+
pagefindOptions: Record<string, any>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 解析构建端搜索配置。
|
|
45
|
+
*
|
|
46
|
+
* 默认策略:SSG 模式且内容模块启用时默认开启(pagefind 未安装时优雅跳过);
|
|
47
|
+
* 其他模式需显式 `content: { search: { provider: 'pagefind' } }` 开启。
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveContentSearchConfig(content: boolean | {
|
|
50
|
+
search?: ContentSearchOptions | false;
|
|
51
|
+
} | undefined, context?: {
|
|
52
|
+
ssg: boolean;
|
|
53
|
+
}): ResolvedContentSearchConfig;
|
|
54
|
+
interface PagefindIndexOptions {
|
|
55
|
+
/** 包含构建产物的静态目录(含 HTML) */
|
|
56
|
+
siteDir: string;
|
|
57
|
+
/** 索引输出目录(默认 `<siteDir>/pagefind`) */
|
|
58
|
+
outputPath?: string;
|
|
59
|
+
/** 传入 `addDirectory` 的 glob(默认匹配所有 `.html` 文件) */
|
|
60
|
+
glob?: string;
|
|
61
|
+
/** 透传给 `createIndex()` 的选项 */
|
|
62
|
+
createIndexOptions?: Record<string, any>;
|
|
63
|
+
logger?: {
|
|
64
|
+
info?: (msg: string) => void;
|
|
65
|
+
warn?: (msg: string) => void;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* 注入自定义 pagefind 模块加载器。默认动态 `import('pagefind')`。
|
|
69
|
+
* 测试用:pnpm 隐藏 hoist 会让工作区内任意位置可解析 pagefind,
|
|
70
|
+
* 注入失败加载器可确定性覆盖「未安装」分支。
|
|
71
|
+
*/
|
|
72
|
+
loadPagefind?: () => Promise<any>;
|
|
73
|
+
}
|
|
74
|
+
interface PagefindIndexResult {
|
|
75
|
+
indexed: boolean;
|
|
76
|
+
reason?: 'pagefind-not-installed' | 'no-html' | 'error';
|
|
77
|
+
error?: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 构建后运行 Pagefind 索引(Node API)。
|
|
81
|
+
*
|
|
82
|
+
* 扫描 `siteDir` 下的 HTML 生成分片索引(按查询词按需加载,
|
|
83
|
+
* 10k 页站点总网络开销 < 300KB),输出到 `<siteDir>/pagefind/`。
|
|
84
|
+
* `pagefind` 为可选依赖,未安装时返回 `pagefind-not-installed` 由调用方降级提示。
|
|
85
|
+
*/
|
|
86
|
+
export declare function runPagefindIndex(options: PagefindIndexOptions): Promise<PagefindIndexResult>;
|
|
87
|
+
//#endregion
|
|
3
88
|
//#region src/routing.d.ts
|
|
4
89
|
interface ContentPageRouteOptions {
|
|
5
90
|
/** URL prefix, e.g. `/blog`. `_path` `/hello` becomes `/blog/hello`. */
|
|
@@ -83,4 +168,4 @@ interface LiveCollection extends Omit<ContentCollection, 'documents' | 'list' |
|
|
|
83
168
|
invalidate(): void;
|
|
84
169
|
}
|
|
85
170
|
//#endregion
|
|
86
|
-
export { type ContentBody, type ContentCollection, type ContentDocument, type ContentFieldSchema, type ContentModuleOptions, type ContentNavigationItem, type ContentPageRouteOptions, type ContentQueryBuilder, type ContentSchema, type ContentSourceConfig, type ContentSourceScanConfig, type ContentTocItem, type ContentType, type DiscoverContentPageRoutesOptions, type LiveCollection, type LiveCollectionCacheOptions, type LiveCollectionEntry, type LiveCollectionLoader, type LiveCollectionLoaderParams, type LiveCollectionOptions, type MarkdownNode, type ParsedContentMeta, type ScanContentSourcesOptions, type UbeanContentOptions, bootstrapContentFromDisk, buildNavigation, configureContentRuntime, createContentCollection, createQueryBuilder, defineCollection, defineContentCollection, fetchContentNavigation, fetchContentNavigation as fetchNavigation, generateId, getCollection, getContentItem, listCollections, parseContent, parseContentFile, parseFrontmatter, parseMarkdown, queryCollection, queryCollection as queryContent, registerContent, scanContentSources, ubeanContentPlugin };
|
|
171
|
+
export { type ContentBody, type ContentCollection, type ContentDocument, type ContentFieldSchema, type ContentModuleOptions, type ContentNavigationItem, type ContentPageRouteOptions, type ContentQueryBuilder, type ContentSchema, type ContentSearchOptions, type ContentSourceConfig, type ContentSourceScanConfig, type ContentTocItem, type ContentType, type DiscoverContentPageRoutesOptions, type GenerateSearchSectionsOptions, type LiveCollection, type LiveCollectionCacheOptions, type LiveCollectionEntry, type LiveCollectionLoader, type LiveCollectionLoaderParams, type LiveCollectionOptions, type MarkdownNode, type PagefindIndexOptions, type PagefindIndexResult, type ParsedContentMeta, type ResolvedContentSearchConfig, type ScanContentSourcesOptions, type SearchHit, type SearchSection, type SectionQueryOptions, type SectionSearchEngine, type SectionSearchOptions, type UbeanContentOptions, bootstrapContentFromDisk, buildNavigation, configureContentRuntime, createContentCollection, createQueryBuilder, defineCollection, defineContentCollection, fetchContentNavigation, fetchContentNavigation as fetchNavigation, generateId, getCollection, getContentItem, listCollections, parseContent, parseContentFile, parseFrontmatter, parseMarkdown, queryCollection, queryCollection as queryContent, queryCollectionSearchSections, registerContent, scanContentSources, ubeanContentPlugin };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { _ as
|
|
1
|
+
import { _ as generateId, a as getCollection, b as parseMarkdown, c as parseContentFile, d as registerContent, f as scanContentSources, g as defineContentCollection, h as createQueryBuilder, i as fetchContentNavigation, l as queryCollection, m as createContentCollection, n as configureContentRuntime, o as getContentItem, p as buildNavigation, r as defineCollection, s as listCollections, t as bootstrapContentFromDisk, u as queryCollectionSearchSections, v as parseContent, y as parseFrontmatter } from "./runtime-Cdi5T8aE.js";
|
|
2
|
+
import { a as runPagefindIndex, c as tokenizeText, i as resolveContentSearchConfig, n as generateSearchSections, o as searchSections, r as generateSearchSectionsSnapshot, s as splitDocumentIntoSearchSections, t as createSectionSearch } from "./search-CNUu8Xvf.js";
|
|
2
3
|
import ubeanContentPlugin from "./vite.js";
|
|
3
4
|
//#region src/routing.ts
|
|
4
5
|
function joinPrefix(prefix, path) {
|
|
@@ -31,4 +32,4 @@ function discoverContentPageRoutes(rootDir, options = {}) {
|
|
|
31
32
|
return extractContentPageRoutes(Object.values(collections).flat(), { includeDrafts });
|
|
32
33
|
}
|
|
33
34
|
//#endregion
|
|
34
|
-
export { bootstrapContentFromDisk, buildNavigation, configureContentRuntime, createContentCollection, createQueryBuilder, defineCollection, defineContentCollection, discoverContentPageRoutes, extractContentPageRoutes, fetchContentNavigation, fetchContentNavigation as fetchNavigation, generateId, getCollection, getContentItem, listCollections, parseContent, parseContentFile, parseFrontmatter, parseMarkdown, queryCollection, queryCollection as queryContent, registerContent, scanContentSources, ubeanContentPlugin };
|
|
35
|
+
export { bootstrapContentFromDisk, buildNavigation, configureContentRuntime, createContentCollection, createQueryBuilder, createSectionSearch, defineCollection, defineContentCollection, discoverContentPageRoutes, extractContentPageRoutes, fetchContentNavigation, fetchContentNavigation as fetchNavigation, generateId, generateSearchSections, generateSearchSectionsSnapshot, getCollection, getContentItem, listCollections, parseContent, parseContentFile, parseFrontmatter, parseMarkdown, queryCollection, queryCollection as queryContent, queryCollectionSearchSections, registerContent, resolveContentSearchConfig, runPagefindIndex, scanContentSources, searchSections, splitDocumentIntoSearchSections, tokenizeText, ubeanContentPlugin };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as generateSearchSections } from "./search-CNUu8Xvf.js";
|
|
1
2
|
import { join, relative } from "pathe";
|
|
2
3
|
import { getBasename, getDirname, getExtension, getStem, normalizePath, pathToTitle } from "@ubean/shared";
|
|
3
4
|
import { kebabCase } from "scule";
|
|
@@ -129,7 +130,7 @@ function parseMarkdown(content) {
|
|
|
129
130
|
flushParagraph();
|
|
130
131
|
const depth = headingMatch[1].length;
|
|
131
132
|
const text = headingMatch[2].replace(/[#*_`~[\]]/g, "").trim();
|
|
132
|
-
const id = kebabCase(text);
|
|
133
|
+
const id = kebabCase(text.replace(/\s+/g, "-"));
|
|
133
134
|
while (headingDepths[headingDepths.length - 1] >= depth) {
|
|
134
135
|
headingDepths.pop();
|
|
135
136
|
headingStack.pop();
|
|
@@ -561,6 +562,18 @@ async function fetchContentNavigation(collectionName) {
|
|
|
561
562
|
if (!collectionName) navigationCache = nav;
|
|
562
563
|
return nav;
|
|
563
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* 全文检索数据原语:将 collection 中的文档按标题层级切分为可搜索 sections。
|
|
567
|
+
*
|
|
568
|
+
* 与搜索引擎解耦 —— 结果可直接交给 `createSectionSearch`(内置 fallback /
|
|
569
|
+
* MiniSearch)或 Fuse.js 等任意引擎。SSG 构建时由 CLI 序列化为 `__search.json`。
|
|
570
|
+
*/
|
|
571
|
+
async function queryCollectionSearchSections(collectionName, options = {}) {
|
|
572
|
+
const collection = collections.get(collectionName);
|
|
573
|
+
if (!collection) throw new Error(`Collection "${collectionName}" not found. Available collections: ${listCollections().join(", ")}`);
|
|
574
|
+
const docs = await collection.list();
|
|
575
|
+
return generateSearchSections(docs, options);
|
|
576
|
+
}
|
|
564
577
|
function registerContent(collectionName, documents) {
|
|
565
578
|
let collection = collections.get(collectionName);
|
|
566
579
|
if (!collection) {
|
|
@@ -590,4 +603,4 @@ function bootstrapContentFromDisk(rootDir, options = {}) {
|
|
|
590
603
|
return loaded;
|
|
591
604
|
}
|
|
592
605
|
//#endregion
|
|
593
|
-
export {
|
|
606
|
+
export { generateId as _, getCollection as a, parseMarkdown as b, parseContentFile as c, registerContent as d, scanContentSources as f, defineContentCollection as g, createQueryBuilder as h, fetchContentNavigation as i, queryCollection as l, createContentCollection as m, configureContentRuntime as n, getContentItem as o, buildNavigation as p, defineCollection as r, listCollections as s, bootstrapContentFromDisk as t, queryCollectionSearchSections as u, parseContent as v, parseFrontmatter as y };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { _ as SearchSection, a as ContentModuleOptions, c as ContentSchema, n as ContentCollection, o as ContentNavigationItem, p as GenerateSearchSectionsOptions, r as ContentDocument, s as ContentQueryBuilder, t as ContentBody } from "./types-B9HP8tUf.js";
|
|
2
|
+
//#region src/core.d.ts
|
|
3
|
+
declare function generateId(path: string, extension?: string): string;
|
|
4
|
+
declare function parseFrontmatter(content: string): {
|
|
5
|
+
data: Record<string, any>;
|
|
6
|
+
content: string;
|
|
7
|
+
};
|
|
8
|
+
declare function parseMarkdown(content: string): ContentBody;
|
|
9
|
+
declare function createQueryBuilder(documents: ContentDocument[]): ContentQueryBuilder;
|
|
10
|
+
declare function buildNavigation(documents: ContentDocument[], _options?: {
|
|
11
|
+
fields?: string[];
|
|
12
|
+
}): ContentNavigationItem[];
|
|
13
|
+
declare function parseContent(raw: string, filePath: string, options?: {
|
|
14
|
+
type?: string;
|
|
15
|
+
}): ContentDocument;
|
|
16
|
+
declare function defineContentCollection(collection: {
|
|
17
|
+
name: string;
|
|
18
|
+
source: string;
|
|
19
|
+
type?: any;
|
|
20
|
+
schema?: ContentSchema;
|
|
21
|
+
documents?: ContentDocument[];
|
|
22
|
+
}): ContentCollection;
|
|
23
|
+
declare function createContentCollection(name: string, source: string, documents?: ContentDocument[]): ContentCollection;
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/scan.d.ts
|
|
26
|
+
interface ContentSourceScanConfig {
|
|
27
|
+
dir: string;
|
|
28
|
+
prefix?: string;
|
|
29
|
+
type?: string;
|
|
30
|
+
}
|
|
31
|
+
interface ScanContentSourcesOptions {
|
|
32
|
+
sources?: Record<string, ContentSourceScanConfig>;
|
|
33
|
+
defaultDir?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read content source directories on disk into named document arrays.
|
|
37
|
+
*
|
|
38
|
+
* Used by the Vite plugin (dev), production server bootstrap, and
|
|
39
|
+
* prerender URL discovery so `queryCollection` is not Vite-only.
|
|
40
|
+
*/
|
|
41
|
+
declare function scanContentSources(rootDir: string, options?: ScanContentSourcesOptions): Record<string, ContentDocument[]>;
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/runtime.d.ts
|
|
44
|
+
declare function configureContentRuntime(_options?: Partial<ContentModuleOptions>): void;
|
|
45
|
+
declare function defineCollection(config: {
|
|
46
|
+
name: string;
|
|
47
|
+
source: string;
|
|
48
|
+
type?: any;
|
|
49
|
+
schema?: ContentSchema;
|
|
50
|
+
}): ContentCollection;
|
|
51
|
+
declare function getCollection(name: string): ContentCollection | undefined;
|
|
52
|
+
declare function listCollections(): string[];
|
|
53
|
+
declare function queryCollection(name: string): Promise<ContentQueryBuilder>;
|
|
54
|
+
declare function getContentItem(collection: string, path: string): Promise<ContentDocument | null>;
|
|
55
|
+
declare function fetchContentNavigation(collectionName?: string): Promise<ContentNavigationItem[]>;
|
|
56
|
+
/**
|
|
57
|
+
* 全文检索数据原语:将 collection 中的文档按标题层级切分为可搜索 sections。
|
|
58
|
+
*
|
|
59
|
+
* 与搜索引擎解耦 —— 结果可直接交给 `createSectionSearch`(内置 fallback /
|
|
60
|
+
* MiniSearch)或 Fuse.js 等任意引擎。SSG 构建时由 CLI 序列化为 `__search.json`。
|
|
61
|
+
*/
|
|
62
|
+
declare function queryCollectionSearchSections(collectionName: string, options?: GenerateSearchSectionsOptions): Promise<SearchSection[]>;
|
|
63
|
+
declare function registerContent(collectionName: string, documents: ContentDocument[]): ContentCollection;
|
|
64
|
+
declare function parseContentFile(raw: string, filePath: string, options?: {
|
|
65
|
+
type?: string;
|
|
66
|
+
}): ContentDocument;
|
|
67
|
+
/**
|
|
68
|
+
* Fill the in-memory collection map from disk (production SSR / tests).
|
|
69
|
+
*
|
|
70
|
+
* Dev already does this via the Vite plugin; call this from a custom
|
|
71
|
+
* server entry if you skip the generated production snapshot.
|
|
72
|
+
*/
|
|
73
|
+
declare function bootstrapContentFromDisk(rootDir: string, options?: ScanContentSourcesOptions): Record<string, ContentDocument[]>;
|
|
74
|
+
//#endregion
|
|
75
|
+
export { parseMarkdown as S, createQueryBuilder as _, getCollection as a, parseContent as b, parseContentFile as c, registerContent as d, ContentSourceScanConfig as f, createContentCollection as g, buildNavigation as h, fetchContentNavigation as i, queryCollection as l, scanContentSources as m, configureContentRuntime as n, getContentItem as o, ScanContentSourcesOptions as p, defineCollection as r, listCollections as s, bootstrapContentFromDisk as t, queryCollectionSearchSections as u, defineContentCollection as v, parseFrontmatter as x, generateId as y };
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { _ as SearchSection, a as ContentModuleOptions, c as ContentSchema, d as ContentTocItem, f as ContentType, g as SearchHit, l as ContentSearchOptions, m as MarkdownNode, n as ContentCollection, o as ContentNavigationItem, p as GenerateSearchSectionsOptions, r as ContentDocument, s as ContentQueryBuilder, t as ContentBody, u as ContentSourceConfig } from "./types-B9HP8tUf.js";
|
|
2
|
+
import { S as parseMarkdown, _ as createQueryBuilder, a as getCollection, b as parseContent, c as parseContentFile, d as registerContent, g as createContentCollection, h as buildNavigation, i as fetchContentNavigation, l as queryCollection, n as configureContentRuntime, o as getContentItem, r as defineCollection, s as listCollections, t as bootstrapContentFromDisk, u as queryCollectionSearchSections, v as defineContentCollection, x as parseFrontmatter, y as generateId } from "./runtime-DQ65gYP0.js";
|
|
3
|
+
export { type ContentBody, type ContentCollection, type ContentDocument, type ContentModuleOptions, type ContentNavigationItem, type ContentQueryBuilder, type ContentSchema, type ContentSearchOptions, type ContentSourceConfig, type ContentTocItem, type ContentType, type GenerateSearchSectionsOptions, type MarkdownNode, type SearchHit, type SearchSection, bootstrapContentFromDisk, buildNavigation, configureContentRuntime, createContentCollection, createQueryBuilder, defineCollection, defineContentCollection, fetchContentNavigation, generateId, getCollection, getContentItem, listCollections, parseContent, parseContentFile, parseFrontmatter, parseMarkdown, queryCollection, queryCollectionSearchSections, registerContent };
|
package/dist/runtime.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as
|
|
2
|
-
export { bootstrapContentFromDisk, buildNavigation, configureContentRuntime, createContentCollection, createQueryBuilder, defineCollection, defineContentCollection, fetchContentNavigation, generateId, getCollection, getContentItem, listCollections, parseContent, parseContentFile, parseFrontmatter, parseMarkdown, queryCollection, registerContent };
|
|
1
|
+
import { _ as generateId, a as getCollection, b as parseMarkdown, c as parseContentFile, d as registerContent, g as defineContentCollection, h as createQueryBuilder, i as fetchContentNavigation, l as queryCollection, m as createContentCollection, n as configureContentRuntime, o as getContentItem, p as buildNavigation, r as defineCollection, s as listCollections, t as bootstrapContentFromDisk, u as queryCollectionSearchSections, v as parseContent, y as parseFrontmatter } from "./runtime-Cdi5T8aE.js";
|
|
2
|
+
export { bootstrapContentFromDisk, buildNavigation, configureContentRuntime, createContentCollection, createQueryBuilder, defineCollection, defineContentCollection, fetchContentNavigation, generateId, getCollection, getContentItem, listCollections, parseContent, parseContentFile, parseFrontmatter, parseMarkdown, queryCollection, queryCollectionSearchSections, registerContent };
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { join } from "pathe";
|
|
2
|
+
//#region src/search.ts
|
|
3
|
+
const DEFAULT_IGNORED_TAGS = ["pre"];
|
|
4
|
+
function extractNodeText(node, ignoredTags) {
|
|
5
|
+
if (node.type === "text") return node.value || "";
|
|
6
|
+
if (node.type === "element") {
|
|
7
|
+
if (ignoredTags.includes(node.tag || "")) return "";
|
|
8
|
+
return (node.children || []).map((child) => extractNodeText(child, ignoredTags)).join("");
|
|
9
|
+
}
|
|
10
|
+
return "";
|
|
11
|
+
}
|
|
12
|
+
function isHeadingNode(node) {
|
|
13
|
+
if (node.type !== "element") return false;
|
|
14
|
+
const match = node.tag?.match(/^h([1-6])$/);
|
|
15
|
+
if (!match) return false;
|
|
16
|
+
return {
|
|
17
|
+
depth: Number(match[1]),
|
|
18
|
+
text: extractNodeText(node, []).trim(),
|
|
19
|
+
anchor: node.props?.id || ""
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 将单个文档按标题层级切分为可搜索的 sections。
|
|
24
|
+
*
|
|
25
|
+
* - 首个标题之前的内容为一个 intro section(level 0,title 为页面标题)
|
|
26
|
+
* - [minHeading, maxHeading] 范围内的标题开启新 section,并维护父级标题链
|
|
27
|
+
* - 范围外的标题(如 minHeading=2 时的 h1)视为正文内容
|
|
28
|
+
* - `_draft` / `_partial` / `_empty` 文档由上层过滤
|
|
29
|
+
*/
|
|
30
|
+
function splitDocumentIntoSearchSections(doc, options = {}) {
|
|
31
|
+
const ignoredTags = options.ignoredTags ?? DEFAULT_IGNORED_TAGS;
|
|
32
|
+
const minHeading = options.minHeading ?? 1;
|
|
33
|
+
const maxHeading = options.maxHeading ?? 6;
|
|
34
|
+
const sections = [];
|
|
35
|
+
if (!doc.body?.children?.length) return sections;
|
|
36
|
+
let current = null;
|
|
37
|
+
/** 父级标题栈:`[{ level, title }]`,新标题弹出所有 >= 自身 level 的项 */
|
|
38
|
+
const headingStack = [];
|
|
39
|
+
const buffer = [];
|
|
40
|
+
function pushSection() {
|
|
41
|
+
if (!current) return;
|
|
42
|
+
current.content = buffer.join(" ").replace(/\s+/g, " ").trim();
|
|
43
|
+
if (current.content || current.level > 0) sections.push(current);
|
|
44
|
+
buffer.length = 0;
|
|
45
|
+
}
|
|
46
|
+
for (const node of doc.body.children) {
|
|
47
|
+
const heading = isHeadingNode(node);
|
|
48
|
+
if (heading && heading.depth >= minHeading && heading.depth <= maxHeading) {
|
|
49
|
+
pushSection();
|
|
50
|
+
while (headingStack.length > 0 && headingStack[headingStack.length - 1].level >= heading.depth) headingStack.pop();
|
|
51
|
+
const titles = headingStack.map((h) => h.title);
|
|
52
|
+
headingStack.push({
|
|
53
|
+
level: heading.depth,
|
|
54
|
+
title: heading.text
|
|
55
|
+
});
|
|
56
|
+
current = {
|
|
57
|
+
id: heading.anchor ? `${doc._path}#${heading.anchor}` : doc._path,
|
|
58
|
+
title: heading.text,
|
|
59
|
+
titles,
|
|
60
|
+
level: heading.depth,
|
|
61
|
+
content: ""
|
|
62
|
+
};
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!current) current = {
|
|
66
|
+
id: doc._path,
|
|
67
|
+
title: doc.title || doc._path,
|
|
68
|
+
titles: [],
|
|
69
|
+
level: 0,
|
|
70
|
+
content: ""
|
|
71
|
+
};
|
|
72
|
+
const text = extractNodeText(node, ignoredTags);
|
|
73
|
+
if (text.trim()) buffer.push(text.trim());
|
|
74
|
+
}
|
|
75
|
+
pushSection();
|
|
76
|
+
return sections;
|
|
77
|
+
}
|
|
78
|
+
/** 过滤不可搜索文档(草稿 / partial / 空文件) */
|
|
79
|
+
function isSearchableDocument(doc) {
|
|
80
|
+
return !doc._draft && !doc._partial && !doc._empty;
|
|
81
|
+
}
|
|
82
|
+
/** 将文档集合批量切分为 sections */
|
|
83
|
+
function generateSearchSections(documents, options = {}) {
|
|
84
|
+
return documents.filter(isSearchableDocument).flatMap((doc) => splitDocumentIntoSearchSections(doc, options));
|
|
85
|
+
}
|
|
86
|
+
/** 将 `{ collection: documents }` 快照转换为 `{ collection: sections }` payload */
|
|
87
|
+
function generateSearchSectionsSnapshot(snapshot, options = {}) {
|
|
88
|
+
const payload = {};
|
|
89
|
+
for (const [name, documents] of Object.entries(snapshot)) payload[name] = generateSearchSections(documents, options);
|
|
90
|
+
return payload;
|
|
91
|
+
}
|
|
92
|
+
/** CJK 表意文字 / 假名 / 谚文区间 */
|
|
93
|
+
const CJK_REGEX = /[\u2E80-\u9FFF\u3040-\u30FF\u31F0-\u31FF\uAC00-\uD7AF\uF900-\uFAFF]/;
|
|
94
|
+
const CJK_RUN_REGEX = /[\u2E80-\u9FFF\u3040-\u30FF\u31F0-\u31FF\uAC00-\uD7AF\uF900-\uFAFF]+/g;
|
|
95
|
+
const WORD_RUN_REGEX = /[\p{L}\p{N}]+/gu;
|
|
96
|
+
let segmenter;
|
|
97
|
+
function getSegmenter() {
|
|
98
|
+
if (segmenter !== void 0) return segmenter;
|
|
99
|
+
try {
|
|
100
|
+
segmenter = new Intl.Segmenter("en", { granularity: "word" });
|
|
101
|
+
} catch {
|
|
102
|
+
segmenter = null;
|
|
103
|
+
}
|
|
104
|
+
return segmenter;
|
|
105
|
+
}
|
|
106
|
+
function segmentCjkRun(run) {
|
|
107
|
+
const seg = getSegmenter();
|
|
108
|
+
if (seg) {
|
|
109
|
+
const tokens = [];
|
|
110
|
+
for (const part of seg.segment(run)) if (part.isWordLike) tokens.push(part.segment.toLowerCase());
|
|
111
|
+
return tokens;
|
|
112
|
+
}
|
|
113
|
+
const chars = Array.from(run.toLowerCase());
|
|
114
|
+
const tokens = [...chars];
|
|
115
|
+
for (let i = 0; i < chars.length - 1; i++) tokens.push(chars[i] + chars[i + 1]);
|
|
116
|
+
return tokens;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* CJK 感知分词器(与引擎共用):
|
|
120
|
+
* - 拉丁词按 Unicode 词字符切分
|
|
121
|
+
* - CJK 连续段优先用 `Intl.Segmenter`(词级),降级为 unigram + bigram
|
|
122
|
+
* - 混合词(如 `vue每个`)按 CJK 子段切分
|
|
123
|
+
*/
|
|
124
|
+
function tokenizeText(text) {
|
|
125
|
+
if (!text) return [];
|
|
126
|
+
const normalized = text.toLowerCase();
|
|
127
|
+
const tokens = [];
|
|
128
|
+
let wordMatch;
|
|
129
|
+
WORD_RUN_REGEX.lastIndex = 0;
|
|
130
|
+
while ((wordMatch = WORD_RUN_REGEX.exec(normalized)) !== null) {
|
|
131
|
+
const word = wordMatch[0];
|
|
132
|
+
if (!CJK_REGEX.test(word)) {
|
|
133
|
+
tokens.push(word);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
let cursor = 0;
|
|
137
|
+
CJK_RUN_REGEX.lastIndex = 0;
|
|
138
|
+
let cjkMatch;
|
|
139
|
+
while ((cjkMatch = CJK_RUN_REGEX.exec(word)) !== null) {
|
|
140
|
+
if (cjkMatch.index > cursor) tokens.push(word.slice(cursor, cjkMatch.index));
|
|
141
|
+
tokens.push(...segmentCjkRun(cjkMatch[0]));
|
|
142
|
+
cursor = cjkMatch.index + cjkMatch[0].length;
|
|
143
|
+
}
|
|
144
|
+
if (cursor < word.length) tokens.push(word.slice(cursor));
|
|
145
|
+
}
|
|
146
|
+
return tokens;
|
|
147
|
+
}
|
|
148
|
+
function toHit(section, score) {
|
|
149
|
+
return {
|
|
150
|
+
id: section.id,
|
|
151
|
+
title: section.title,
|
|
152
|
+
titles: section.titles,
|
|
153
|
+
level: section.level,
|
|
154
|
+
content: section.content,
|
|
155
|
+
score
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** 内置 fallback 引擎:词元精确/前缀匹配 + 标题加权打分(无编辑距离容错) */
|
|
159
|
+
function createFallbackEngine(sections) {
|
|
160
|
+
const indexed = sections.map((section) => ({
|
|
161
|
+
section,
|
|
162
|
+
titleTokens: tokenizeText(`${section.title} ${section.titles.join(" ")}`),
|
|
163
|
+
contentTokens: tokenizeText(section.content)
|
|
164
|
+
}));
|
|
165
|
+
return {
|
|
166
|
+
engine: "fallback",
|
|
167
|
+
search(query, options = {}) {
|
|
168
|
+
const terms = tokenizeText(query);
|
|
169
|
+
if (terms.length === 0) return [];
|
|
170
|
+
const titleBoost = options.titleBoost ?? 4;
|
|
171
|
+
const contentBoost = options.contentBoost ?? 1;
|
|
172
|
+
const hits = [];
|
|
173
|
+
for (const entry of indexed) {
|
|
174
|
+
let score = 0;
|
|
175
|
+
let matched = false;
|
|
176
|
+
for (const term of terms) {
|
|
177
|
+
let termScore = 0;
|
|
178
|
+
for (const token of entry.titleTokens) if (token === term) termScore = Math.max(termScore, titleBoost);
|
|
179
|
+
else if (token.startsWith(term)) termScore = Math.max(termScore, titleBoost / 2);
|
|
180
|
+
for (const token of entry.contentTokens) if (token === term) termScore = Math.max(termScore, contentBoost);
|
|
181
|
+
else if (token.startsWith(term)) termScore = Math.max(termScore, contentBoost / 2);
|
|
182
|
+
if (termScore > 0) matched = true;
|
|
183
|
+
score += termScore;
|
|
184
|
+
}
|
|
185
|
+
if (matched) hits.push(toHit(entry.section, score));
|
|
186
|
+
}
|
|
187
|
+
hits.sort((a, b) => b.score - a.score);
|
|
188
|
+
const limit = options.limit ?? 20;
|
|
189
|
+
return hits.slice(0, limit);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 创建章节搜索引擎。
|
|
195
|
+
*
|
|
196
|
+
* 优先使用 MiniSearch(可选依赖,支持前缀 + 模糊匹配 + 字段加权),
|
|
197
|
+
* 未安装时降级为内置 fallback 引擎(精确/前缀匹配)。
|
|
198
|
+
* 两条路径共享 CJK 感知分词器。
|
|
199
|
+
*/
|
|
200
|
+
async function createSectionSearch(sections, options = {}) {
|
|
201
|
+
const moduleId = "minisearch";
|
|
202
|
+
let MiniSearch = null;
|
|
203
|
+
try {
|
|
204
|
+
const mod = await import(
|
|
205
|
+
/* @vite-ignore */
|
|
206
|
+
moduleId
|
|
207
|
+
);
|
|
208
|
+
MiniSearch = mod.default ?? mod.MiniSearch ?? mod;
|
|
209
|
+
} catch {
|
|
210
|
+
MiniSearch = null;
|
|
211
|
+
}
|
|
212
|
+
if (MiniSearch && typeof MiniSearch === "function") {
|
|
213
|
+
const miniSearch = new MiniSearch({
|
|
214
|
+
fields: ["title", "content"],
|
|
215
|
+
storeFields: [],
|
|
216
|
+
tokenize: tokenizeText,
|
|
217
|
+
...options.miniSearch
|
|
218
|
+
});
|
|
219
|
+
miniSearch.addAll(sections);
|
|
220
|
+
const searchOptions = {
|
|
221
|
+
prefix: true,
|
|
222
|
+
fuzzy: .2,
|
|
223
|
+
boost: {
|
|
224
|
+
title: 4,
|
|
225
|
+
content: 1
|
|
226
|
+
},
|
|
227
|
+
...options.searchOptions
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
engine: "minisearch",
|
|
231
|
+
search(query, queryOptions = {}) {
|
|
232
|
+
const limit = queryOptions.limit ?? 20;
|
|
233
|
+
const results = miniSearch.search(query, {
|
|
234
|
+
...searchOptions,
|
|
235
|
+
boost: {
|
|
236
|
+
...searchOptions.boost,
|
|
237
|
+
...queryOptions.titleBoost !== void 0 ? { title: queryOptions.titleBoost } : {},
|
|
238
|
+
...queryOptions.contentBoost !== void 0 ? { content: queryOptions.contentBoost } : {}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
const byId = new Map(sections.map((s) => [s.id, s]));
|
|
242
|
+
const hits = [];
|
|
243
|
+
for (const result of results) {
|
|
244
|
+
const section = byId.get(result.id);
|
|
245
|
+
if (section) hits.push(toHit(section, result.score));
|
|
246
|
+
}
|
|
247
|
+
return hits.slice(0, limit);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return createFallbackEngine(sections);
|
|
252
|
+
}
|
|
253
|
+
/** 一次性搜索便捷函数(每次调用重建引擎,适合低频场景;高频场景请缓存 `createSectionSearch` 结果) */
|
|
254
|
+
async function searchSections(sections, query, options = {}) {
|
|
255
|
+
const { miniSearch, searchOptions, ...queryOptions } = options;
|
|
256
|
+
return (await createSectionSearch(sections, {
|
|
257
|
+
miniSearch,
|
|
258
|
+
searchOptions
|
|
259
|
+
})).search(query, queryOptions);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* 解析构建端搜索配置。
|
|
263
|
+
*
|
|
264
|
+
* 默认策略:SSG 模式且内容模块启用时默认开启(pagefind 未安装时优雅跳过);
|
|
265
|
+
* 其他模式需显式 `content: { search: { provider: 'pagefind' } }` 开启。
|
|
266
|
+
*/
|
|
267
|
+
function resolveContentSearchConfig(content, context = { ssg: false }) {
|
|
268
|
+
if (content === false) return {
|
|
269
|
+
sections: false,
|
|
270
|
+
pagefind: false,
|
|
271
|
+
pagefindOptions: {}
|
|
272
|
+
};
|
|
273
|
+
const search = content === true || content === void 0 ? void 0 : content.search;
|
|
274
|
+
if (search === false || search?.provider === false) return {
|
|
275
|
+
sections: false,
|
|
276
|
+
pagefind: false,
|
|
277
|
+
pagefindOptions: {}
|
|
278
|
+
};
|
|
279
|
+
const enabled = Boolean(search) || context.ssg;
|
|
280
|
+
return {
|
|
281
|
+
sections: enabled ? search?.sections ?? true : false,
|
|
282
|
+
pagefind: enabled,
|
|
283
|
+
pagefindOptions: search?.pagefind ?? {}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* 构建后运行 Pagefind 索引(Node API)。
|
|
288
|
+
*
|
|
289
|
+
* 扫描 `siteDir` 下的 HTML 生成分片索引(按查询词按需加载,
|
|
290
|
+
* 10k 页站点总网络开销 < 300KB),输出到 `<siteDir>/pagefind/`。
|
|
291
|
+
* `pagefind` 为可选依赖,未安装时返回 `pagefind-not-installed` 由调用方降级提示。
|
|
292
|
+
*/
|
|
293
|
+
async function runPagefindIndex(options) {
|
|
294
|
+
const { siteDir, outputPath, glob, createIndexOptions = {}, logger } = options;
|
|
295
|
+
let pagefind;
|
|
296
|
+
try {
|
|
297
|
+
pagefind = await (options.loadPagefind?.() ?? import(
|
|
298
|
+
/* @vite-ignore */
|
|
299
|
+
"pagefind"
|
|
300
|
+
));
|
|
301
|
+
} catch {
|
|
302
|
+
return {
|
|
303
|
+
indexed: false,
|
|
304
|
+
reason: "pagefind-not-installed"
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
const { index } = await pagefind.createIndex(createIndexOptions);
|
|
309
|
+
if (index?.on) index.on("error", (err) => {
|
|
310
|
+
logger?.warn?.(`[pagefind] index error: ${err instanceof Error ? err.message : String(err)}`);
|
|
311
|
+
});
|
|
312
|
+
await index.addDirectory({
|
|
313
|
+
path: siteDir,
|
|
314
|
+
glob: glob || "**/*.{html}"
|
|
315
|
+
});
|
|
316
|
+
await index.writeFiles({ outputPath: outputPath ?? join(siteDir, "pagefind") });
|
|
317
|
+
logger?.info?.(`[pagefind] search index written to ${outputPath ?? join(siteDir, "pagefind")}`);
|
|
318
|
+
return { indexed: true };
|
|
319
|
+
} catch (err) {
|
|
320
|
+
return {
|
|
321
|
+
indexed: false,
|
|
322
|
+
reason: "error",
|
|
323
|
+
error: err instanceof Error ? err.message : String(err)
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
//#endregion
|
|
328
|
+
export { runPagefindIndex as a, tokenizeText as c, resolveContentSearchConfig as i, generateSearchSections as n, searchSections as o, generateSearchSectionsSnapshot as r, splitDocumentIntoSearchSections as s, createSectionSearch as t };
|
|
@@ -144,71 +144,78 @@ interface ContentNavigationItem {
|
|
|
144
144
|
draft?: boolean;
|
|
145
145
|
children?: ContentNavigationItem[];
|
|
146
146
|
}
|
|
147
|
-
//#endregion
|
|
148
|
-
//#region src/core.d.ts
|
|
149
|
-
declare function generateId(path: string, extension?: string): string;
|
|
150
|
-
declare function parseFrontmatter(content: string): {
|
|
151
|
-
data: Record<string, any>;
|
|
152
|
-
content: string;
|
|
153
|
-
};
|
|
154
|
-
declare function parseMarkdown(content: string): ContentBody;
|
|
155
|
-
declare function createQueryBuilder(documents: ContentDocument[]): ContentQueryBuilder;
|
|
156
|
-
declare function buildNavigation(documents: ContentDocument[], _options?: {
|
|
157
|
-
fields?: string[];
|
|
158
|
-
}): ContentNavigationItem[];
|
|
159
|
-
declare function parseContent(raw: string, filePath: string, options?: {
|
|
160
|
-
type?: string;
|
|
161
|
-
}): ContentDocument;
|
|
162
|
-
declare function defineContentCollection(collection: {
|
|
163
|
-
name: string;
|
|
164
|
-
source: string;
|
|
165
|
-
type?: any;
|
|
166
|
-
schema?: ContentSchema;
|
|
167
|
-
documents?: ContentDocument[];
|
|
168
|
-
}): ContentCollection;
|
|
169
|
-
declare function createContentCollection(name: string, source: string, documents?: ContentDocument[]): ContentCollection;
|
|
170
|
-
//#endregion
|
|
171
|
-
//#region src/scan.d.ts
|
|
172
|
-
interface ContentSourceScanConfig {
|
|
173
|
-
dir: string;
|
|
174
|
-
prefix?: string;
|
|
175
|
-
type?: string;
|
|
176
|
-
}
|
|
177
|
-
interface ScanContentSourcesOptions {
|
|
178
|
-
sources?: Record<string, ContentSourceScanConfig>;
|
|
179
|
-
defaultDir?: string;
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Read content source directories on disk into named document arrays.
|
|
183
|
-
*
|
|
184
|
-
* Used by the Vite plugin (dev), production server bootstrap, and
|
|
185
|
-
* prerender URL discovery so `queryCollection` is not Vite-only.
|
|
186
|
-
*/
|
|
187
|
-
declare function scanContentSources(rootDir: string, options?: ScanContentSourcesOptions): Record<string, ContentDocument[]>;
|
|
188
|
-
//#endregion
|
|
189
|
-
//#region src/runtime.d.ts
|
|
190
|
-
declare function configureContentRuntime(_options?: Partial<ContentModuleOptions>): void;
|
|
191
|
-
declare function defineCollection(config: {
|
|
192
|
-
name: string;
|
|
193
|
-
source: string;
|
|
194
|
-
type?: any;
|
|
195
|
-
schema?: ContentSchema;
|
|
196
|
-
}): ContentCollection;
|
|
197
|
-
declare function getCollection(name: string): ContentCollection | undefined;
|
|
198
|
-
declare function listCollections(): string[];
|
|
199
|
-
declare function queryCollection(name: string): Promise<ContentQueryBuilder>;
|
|
200
|
-
declare function getContentItem(collection: string, path: string): Promise<ContentDocument | null>;
|
|
201
|
-
declare function fetchContentNavigation(collectionName?: string): Promise<ContentNavigationItem[]>;
|
|
202
|
-
declare function registerContent(collectionName: string, documents: ContentDocument[]): ContentCollection;
|
|
203
|
-
declare function parseContentFile(raw: string, filePath: string, options?: {
|
|
204
|
-
type?: string;
|
|
205
|
-
}): ContentDocument;
|
|
206
147
|
/**
|
|
207
|
-
*
|
|
148
|
+
* 全文检索的文档章节(数据原语)。
|
|
208
149
|
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
150
|
+
* 将文档按标题层级切分为可搜索的 sections,与搜索引擎解耦:
|
|
151
|
+
* 构建时序列化为静态 JSON(`__search.json`),客户端可交给
|
|
152
|
+
* MiniSearch / Fuse.js 或内置 fallback 引擎。
|
|
211
153
|
*/
|
|
212
|
-
|
|
154
|
+
interface SearchSection {
|
|
155
|
+
/** `页面路径` 或 `页面路径#锚点` */
|
|
156
|
+
id: string;
|
|
157
|
+
/** 章节标题(首段无标题时为页面标题) */
|
|
158
|
+
title: string;
|
|
159
|
+
/** 父级标题链(面包屑),如 `['指南', '安装']` */
|
|
160
|
+
titles: string[];
|
|
161
|
+
/** 标题层级,首段为 0,h1~h6 为 1~6 */
|
|
162
|
+
level: number;
|
|
163
|
+
/** 章节纯文本内容 */
|
|
164
|
+
content: string;
|
|
165
|
+
}
|
|
166
|
+
/** `queryCollectionSearchSections` / `generateSearchSections` 的切分选项 */
|
|
167
|
+
interface GenerateSearchSectionsOptions {
|
|
168
|
+
/** 提取文本时忽略的标签(如 `['pre', 'code']`),默认 `['pre']` */
|
|
169
|
+
ignoredTags?: string[];
|
|
170
|
+
/** 切分的最小标题级别(默认 1,即 h1 起切分) */
|
|
171
|
+
minHeading?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
172
|
+
/** 切分的最大标题级别(默认 6) */
|
|
173
|
+
maxHeading?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
174
|
+
}
|
|
175
|
+
/** 搜索命中结果(引擎无关) */
|
|
176
|
+
interface SearchHit {
|
|
177
|
+
id: string;
|
|
178
|
+
title: string;
|
|
179
|
+
titles: string[];
|
|
180
|
+
level: number;
|
|
181
|
+
content: string;
|
|
182
|
+
/** 相关性得分,越高越相关 */
|
|
183
|
+
score: number;
|
|
184
|
+
}
|
|
185
|
+
/** 章节搜索引擎实例(MiniSearch 或内置 fallback) */
|
|
186
|
+
interface SectionSearchEngine {
|
|
187
|
+
engine: 'minisearch' | 'fallback';
|
|
188
|
+
search(query: string, options?: SectionQueryOptions): SearchHit[];
|
|
189
|
+
}
|
|
190
|
+
/** 引擎查询选项 */
|
|
191
|
+
interface SectionQueryOptions {
|
|
192
|
+
limit?: number;
|
|
193
|
+
/** 标题命中权重(默认 4) */
|
|
194
|
+
titleBoost?: number;
|
|
195
|
+
/** 正文命中权重(默认 1) */
|
|
196
|
+
contentBoost?: number;
|
|
197
|
+
}
|
|
198
|
+
/** `createSectionSearch` 的引擎选项 */
|
|
199
|
+
interface SectionSearchOptions {
|
|
200
|
+
/** MiniSearch 选项(fields/storeFields 由内部固定,此处可传 tokenize/processTerm) */
|
|
201
|
+
miniSearch?: Record<string, any>;
|
|
202
|
+
/** MiniSearch 搜索选项(默认 `{ prefix: true, fuzzy: 0.2 }`) */
|
|
203
|
+
searchOptions?: Record<string, any>;
|
|
204
|
+
}
|
|
205
|
+
/** 内容搜索配置(`UbeanContentOptions.search`) */
|
|
206
|
+
interface ContentSearchOptions {
|
|
207
|
+
/**
|
|
208
|
+
* SSG 搜索引擎:`'pagefind'`(默认)构建后扫描静态 HTML 生成分片索引;
|
|
209
|
+
* `false` 禁用。pagefind 未安装时构建端优雅跳过。
|
|
210
|
+
*/
|
|
211
|
+
provider?: 'pagefind' | false;
|
|
212
|
+
/**
|
|
213
|
+
* 构建时输出 sections JSON payload(`__search.json`),
|
|
214
|
+
* 供 `useContentSearch` / 自定义引擎使用。默认 `true`。
|
|
215
|
+
*/
|
|
216
|
+
sections?: boolean;
|
|
217
|
+
/** 透传给 Pagefind Node API `createIndex()` 的选项 */
|
|
218
|
+
pagefind?: Record<string, any>;
|
|
219
|
+
}
|
|
213
220
|
//#endregion
|
|
214
|
-
export {
|
|
221
|
+
export { SearchSection as _, ContentModuleOptions as a, SectionSearchOptions as b, ContentSchema as c, ContentTocItem as d, ContentType as f, SearchHit as g, ParsedContentMeta as h, ContentFieldSchema as i, ContentSearchOptions as l, MarkdownNode as m, ContentCollection as n, ContentNavigationItem as o, GenerateSearchSectionsOptions as p, ContentDocument as r, ContentQueryBuilder as s, ContentBody as t, ContentSourceConfig as u, SectionQueryOptions as v, SectionSearchEngine as y };
|
package/dist/vite.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { l as ContentSearchOptions } from "./types-B9HP8tUf.js";
|
|
1
2
|
import { Plugin } from "vite";
|
|
2
3
|
//#region src/vite.d.ts
|
|
3
4
|
export interface UbeanContentOptions {
|
|
@@ -19,6 +20,13 @@ export interface UbeanContentOptions {
|
|
|
19
20
|
experimental?: {
|
|
20
21
|
watch?: boolean;
|
|
21
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* 全文检索配置(SSG 默认开启):
|
|
25
|
+
* - `provider: 'pagefind'`(默认)构建后扫描静态 HTML 生成分片索引
|
|
26
|
+
* - `sections: true`(默认)输出 `__search.json` 供 `useContentSearch` 使用
|
|
27
|
+
* - dev server 自动提供 `/__search.json`(与 SSG 产物同构)
|
|
28
|
+
*/
|
|
29
|
+
search?: ContentSearchOptions | false;
|
|
22
30
|
}
|
|
23
31
|
export declare function ubeanContentPlugin(userOptions?: UbeanContentOptions): Plugin;
|
|
24
32
|
//#endregion
|
package/dist/vite.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { d as
|
|
1
|
+
import { d as registerContent, f as scanContentSources, n as configureContentRuntime } from "./runtime-Cdi5T8aE.js";
|
|
2
|
+
import { r as generateSearchSectionsSnapshot } from "./search-CNUu8Xvf.js";
|
|
2
3
|
import { defu } from "defu";
|
|
3
4
|
import { join } from "pathe";
|
|
4
5
|
//#region src/vite.ts
|
|
@@ -59,6 +60,11 @@ export default collections;
|
|
|
59
60
|
return null;
|
|
60
61
|
},
|
|
61
62
|
configureServer(server) {
|
|
63
|
+
if (options.search !== false) server.middlewares.use("/__search.json", (_req, res) => {
|
|
64
|
+
const payload = generateSearchSectionsSnapshot(loadedDocuments);
|
|
65
|
+
res.setHeader("Content-Type", "application/json");
|
|
66
|
+
res.end(JSON.stringify(payload));
|
|
67
|
+
});
|
|
62
68
|
if (options.experimental?.watch) {
|
|
63
69
|
const watchPatterns = Object.values(options.sources || {}).map((s) => {
|
|
64
70
|
return join(s.dir || options.defaultDir, "**/*.{md,mdx,json,yaml,yml}");
|
package/dist/vue.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { _ as SearchSection, b as SectionSearchOptions, g as SearchHit, v as SectionQueryOptions } from "./types-B9HP8tUf.js";
|
|
2
|
+
import { Ref } from "vue";
|
|
3
|
+
//#region src/vue.d.ts
|
|
4
|
+
export interface UseContentSearchOptions {
|
|
5
|
+
/**
|
|
6
|
+
* 直接提供 sections(SSR 场景:由 `queryCollectionSearchSections` 生成后传入)。
|
|
7
|
+
* 提供时跳过 fetch。
|
|
8
|
+
*/
|
|
9
|
+
sections?: SearchSection[] | (() => SearchSection[] | Promise<SearchSection[]>);
|
|
10
|
+
/**
|
|
11
|
+
* sections JSON payload 的 URL(dev 由 Vite 插件提供,SSG 由构建产物提供)。
|
|
12
|
+
* 默认 `/__search.json`。
|
|
13
|
+
*/
|
|
14
|
+
sectionsUrl?: string;
|
|
15
|
+
/** 仅搜索指定 collections(`__search.json` 为 `{ [collection]: sections }`) */
|
|
16
|
+
collections?: string | string[];
|
|
17
|
+
/** 立即初始化引擎(默认 true);`false` 时延迟到首次 search */
|
|
18
|
+
immediate?: boolean;
|
|
19
|
+
/** 引擎选项(MiniSearch 可用时的选项与降级 fallback 的权重) */
|
|
20
|
+
searchOptions?: SectionSearchOptions;
|
|
21
|
+
}
|
|
22
|
+
export interface UseContentSearchReturn {
|
|
23
|
+
status: Ref<'idle' | 'loading' | 'ready' | 'error'>;
|
|
24
|
+
error: Ref<Error | null>;
|
|
25
|
+
results: Ref<SearchHit[]>;
|
|
26
|
+
/** 手动触发引擎初始化(`immediate: false` 时使用) */
|
|
27
|
+
init: () => Promise<void>;
|
|
28
|
+
/** 执行搜索;空查询清空结果。引擎懒初始化 */
|
|
29
|
+
search: (query: string, options?: SectionQueryOptions) => Promise<SearchHit[]>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 客户端全文搜索 composable。
|
|
33
|
+
*
|
|
34
|
+
* 数据源优先级:
|
|
35
|
+
* 1. `options.sections`(SSR 直传 / 静态导入)
|
|
36
|
+
* 2. `fetch(options.sectionsUrl ?? '/__search.json')`(dev 中间件 / SSG 构建产物)
|
|
37
|
+
*
|
|
38
|
+
* 引擎:优先 MiniSearch(可选依赖,前缀 + 模糊匹配),未安装时降级内置
|
|
39
|
+
* fallback 引擎(精确/前缀匹配)。两者共享 CJK 感知分词器。
|
|
40
|
+
*/
|
|
41
|
+
export declare function useContentSearch(options?: UseContentSearchOptions): UseContentSearchReturn;
|
|
42
|
+
//#endregion
|
package/dist/vue.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { t as createSectionSearch } from "./search-CNUu8Xvf.js";
|
|
2
|
+
import { ref } from "vue";
|
|
3
|
+
//#region src/vue.ts
|
|
4
|
+
/**
|
|
5
|
+
* 客户端全文搜索 composable。
|
|
6
|
+
*
|
|
7
|
+
* 数据源优先级:
|
|
8
|
+
* 1. `options.sections`(SSR 直传 / 静态导入)
|
|
9
|
+
* 2. `fetch(options.sectionsUrl ?? '/__search.json')`(dev 中间件 / SSG 构建产物)
|
|
10
|
+
*
|
|
11
|
+
* 引擎:优先 MiniSearch(可选依赖,前缀 + 模糊匹配),未安装时降级内置
|
|
12
|
+
* fallback 引擎(精确/前缀匹配)。两者共享 CJK 感知分词器。
|
|
13
|
+
*/
|
|
14
|
+
function useContentSearch(options = {}) {
|
|
15
|
+
const status = ref("idle");
|
|
16
|
+
const error = ref(null);
|
|
17
|
+
const results = ref([]);
|
|
18
|
+
let engine = null;
|
|
19
|
+
let initPromise = null;
|
|
20
|
+
function filterCollections(payload) {
|
|
21
|
+
const wanted = options.collections ? Array.isArray(options.collections) ? options.collections : [options.collections] : null;
|
|
22
|
+
if (!wanted) return Object.values(payload).flat();
|
|
23
|
+
return wanted.flatMap((name) => payload[name] ?? []);
|
|
24
|
+
}
|
|
25
|
+
async function loadSections() {
|
|
26
|
+
if (options.sections) return typeof options.sections === "function" ? await options.sections() : options.sections;
|
|
27
|
+
const res = await fetch(options.sectionsUrl ?? "/__search.json");
|
|
28
|
+
if (!res.ok) throw new Error(`Failed to load search sections from ${options.sectionsUrl ?? "/__search.json"}: HTTP ${res.status}`);
|
|
29
|
+
return filterCollections(await res.json());
|
|
30
|
+
}
|
|
31
|
+
async function init() {
|
|
32
|
+
if (engine || status.value === "loading") return initPromise ?? Promise.resolve();
|
|
33
|
+
status.value = "loading";
|
|
34
|
+
error.value = null;
|
|
35
|
+
initPromise = (async () => {
|
|
36
|
+
try {
|
|
37
|
+
const sections = await loadSections();
|
|
38
|
+
engine = await createSectionSearch(sections, options.searchOptions);
|
|
39
|
+
status.value = "ready";
|
|
40
|
+
} catch (err) {
|
|
41
|
+
engine = null;
|
|
42
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
43
|
+
status.value = "error";
|
|
44
|
+
} finally {
|
|
45
|
+
initPromise = null;
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
return initPromise;
|
|
49
|
+
}
|
|
50
|
+
async function search(query, queryOptions = {}) {
|
|
51
|
+
if (!engine) await init();
|
|
52
|
+
if (!engine) return [];
|
|
53
|
+
results.value = query.trim() ? engine.search(query, queryOptions) : [];
|
|
54
|
+
return results.value;
|
|
55
|
+
}
|
|
56
|
+
if (options.immediate !== false) init();
|
|
57
|
+
return {
|
|
58
|
+
status,
|
|
59
|
+
error,
|
|
60
|
+
results,
|
|
61
|
+
init,
|
|
62
|
+
search
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { useContentSearch };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ubean/content",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "File-based content module for ubean (markdown/MDX/YAML/JSON)",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -21,10 +21,14 @@
|
|
|
21
21
|
"./runtime": {
|
|
22
22
|
"types": "./dist/runtime.d.ts",
|
|
23
23
|
"import": "./dist/runtime.js"
|
|
24
|
+
},
|
|
25
|
+
"./vue": {
|
|
26
|
+
"types": "./dist/vue.d.ts",
|
|
27
|
+
"import": "./dist/vue.js"
|
|
24
28
|
}
|
|
25
29
|
},
|
|
26
30
|
"dependencies": {
|
|
27
|
-
"@ubean/shared": "0.4.
|
|
31
|
+
"@ubean/shared": "0.4.7",
|
|
28
32
|
"defu": "6.1.7",
|
|
29
33
|
"pathe": "^2.0.3",
|
|
30
34
|
"scule": "^1.3.0"
|
|
@@ -33,14 +37,19 @@
|
|
|
33
37
|
"@types/node": "^26.5.0",
|
|
34
38
|
"typescript": "npm:typescript-native-bridge@latest",
|
|
35
39
|
"vite": "npm:@voidzero-dev/vite-plus-core@0.3.1",
|
|
36
|
-
"vite-plus": "0.3.1"
|
|
40
|
+
"vite-plus": "0.3.1",
|
|
41
|
+
"vue": "^3.5.42"
|
|
37
42
|
},
|
|
38
43
|
"peerDependencies": {
|
|
39
|
-
"vite": "^5.0.0 || ^6.0.0"
|
|
44
|
+
"vite": "^5.0.0 || ^6.0.0",
|
|
45
|
+
"vue": "^3.5.0"
|
|
40
46
|
},
|
|
41
47
|
"peerDependenciesMeta": {
|
|
42
48
|
"vite": {
|
|
43
49
|
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"vue": {
|
|
52
|
+
"optional": true
|
|
44
53
|
}
|
|
45
54
|
},
|
|
46
55
|
"optionalDependencies": {},
|