@dominikcz/greg 0.9.27
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/README.md +397 -0
- package/bin/greg.js +241 -0
- package/bin/init.js +351 -0
- package/bin/templates/docs/getting-started.md +47 -0
- package/bin/templates/docs/index.md +11 -0
- package/bin/templates/greg.config.js +39 -0
- package/bin/templates/greg.config.ts +38 -0
- package/bin/templates/index.html +16 -0
- package/bin/templates/src/App.svelte +5 -0
- package/bin/templates/src/app.css +20 -0
- package/bin/templates/src/main.js +9 -0
- package/bin/templates/svelte.config.js +1 -0
- package/bin/templates/tsconfig.json +21 -0
- package/bin/templates/vite.config.js +23 -0
- package/docs/__partials/markdown/examples/basic.md +4 -0
- package/docs/__partials/markdown/examples/diff.md +10 -0
- package/docs/__partials/markdown/examples/focus.md +5 -0
- package/docs/__partials/markdown/examples/language-title.md +3 -0
- package/docs/__partials/markdown/examples/line-highlighting.md +5 -0
- package/docs/__partials/markdown/examples/line-numbers.md +5 -0
- package/docs/__partials/note.md +4 -0
- package/docs/guide/__shared-warning.md +4 -0
- package/docs/guide/asset-handling.md +88 -0
- package/docs/guide/deploying.md +162 -0
- package/docs/guide/getting-started.md +334 -0
- package/docs/guide/index.md +23 -0
- package/docs/guide/localization.md +290 -0
- package/docs/guide/markdown/code.md +95 -0
- package/docs/guide/markdown/components-and-mermaid.md +43 -0
- package/docs/guide/markdown/containers.md +110 -0
- package/docs/guide/markdown/header-anchors.md +34 -0
- package/docs/guide/markdown/includes.md +84 -0
- package/docs/guide/markdown/index.md +20 -0
- package/docs/guide/markdown/inline-attributes.md +21 -0
- package/docs/guide/markdown/links-and-toc.md +64 -0
- package/docs/guide/markdown/math.md +54 -0
- package/docs/guide/markdown/syntax-highlighting.md +75 -0
- package/docs/guide/routing.md +150 -0
- package/docs/guide/using-svelte.md +88 -0
- package/docs/guide/versioning.md +281 -0
- package/docs/incompatibilities.md +48 -0
- package/docs/index.md +43 -0
- package/docs/reference/badge.md +100 -0
- package/docs/reference/carbon-ads.md +46 -0
- package/docs/reference/code-group.md +126 -0
- package/docs/reference/home-page.md +232 -0
- package/docs/reference/index.md +18 -0
- package/docs/reference/markdowndocs.md +275 -0
- package/docs/reference/outline.md +79 -0
- package/docs/reference/search.md +263 -0
- package/docs/reference/steps.md +200 -0
- package/docs/reference/team-page.md +189 -0
- package/docs/reference/theme.md +150 -0
- package/fakeDocsGenerator/generate_docs.js +310 -0
- package/package.json +92 -0
- package/scripts/build-versions.js +609 -0
- package/scripts/generate-static.js +79 -0
- package/scripts/render-markdown.js +420 -0
- package/src/lib/MarkdownDocs/AiChat.svelte +936 -0
- package/src/lib/MarkdownDocs/BackToTop.svelte +68 -0
- package/src/lib/MarkdownDocs/Breadcrumb.svelte +68 -0
- package/src/lib/MarkdownDocs/DocsNavigation.svelte +149 -0
- package/src/lib/MarkdownDocs/DocsSiteHeader.svelte +758 -0
- package/src/lib/MarkdownDocs/DocsVersionSwitcher.svelte +103 -0
- package/src/lib/MarkdownDocs/MarkdownDocs.svelte +2115 -0
- package/src/lib/MarkdownDocs/MarkdownRenderer.svelte +487 -0
- package/src/lib/MarkdownDocs/Outline.svelte +238 -0
- package/src/lib/MarkdownDocs/PrevNext.svelte +115 -0
- package/src/lib/MarkdownDocs/SearchModal.svelte +1241 -0
- package/src/lib/MarkdownDocs/TreeView.svelte +32 -0
- package/src/lib/MarkdownDocs/TreeViewItem.svelte +219 -0
- package/src/lib/MarkdownDocs/VersionOutdatedNotice.svelte +72 -0
- package/src/lib/MarkdownDocs/__tests__/codeDirectives.test.js +54 -0
- package/src/lib/MarkdownDocs/__tests__/common.test.js +41 -0
- package/src/lib/MarkdownDocs/__tests__/docsExamplesLint.test.js +77 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/docs/markdown/__partial-basic.md +3 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/docs/markdown/snippet.js +9 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/includes/part.md +11 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/includes/wrapper.md +5 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/snippets/sample.js +8 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/snippets/sample.md +5 -0
- package/src/lib/MarkdownDocs/__tests__/helpers.js +67 -0
- package/src/lib/MarkdownDocs/__tests__/localeUtils.test.js +204 -0
- package/src/lib/MarkdownDocs/__tests__/markdown.test.js +704 -0
- package/src/lib/MarkdownDocs/__tests__/markdownRendererRuntime.test.js +65 -0
- package/src/lib/MarkdownDocs/__tests__/searchIndexBuilder.test.js +117 -0
- package/src/lib/MarkdownDocs/__tests__/sqliteStore.test.js +202 -0
- package/src/lib/MarkdownDocs/__tests__/useRouter.test.js +16 -0
- package/src/lib/MarkdownDocs/ai/adapters/customAdapter.js +14 -0
- package/src/lib/MarkdownDocs/ai/adapters/customAdapter.ts +43 -0
- package/src/lib/MarkdownDocs/ai/adapters/ollamaAdapter.js +81 -0
- package/src/lib/MarkdownDocs/ai/adapters/ollamaAdapter.ts +116 -0
- package/src/lib/MarkdownDocs/ai/adapters/openaiAdapter.js +92 -0
- package/src/lib/MarkdownDocs/ai/adapters/openaiAdapter.ts +137 -0
- package/src/lib/MarkdownDocs/ai/aiProvider.ts +31 -0
- package/src/lib/MarkdownDocs/ai/characters.js +52 -0
- package/src/lib/MarkdownDocs/ai/characters.ts +69 -0
- package/src/lib/MarkdownDocs/ai/chunkStore.ts +25 -0
- package/src/lib/MarkdownDocs/ai/chunker.js +85 -0
- package/src/lib/MarkdownDocs/ai/chunker.ts +135 -0
- package/src/lib/MarkdownDocs/ai/docLinker.js +26 -0
- package/src/lib/MarkdownDocs/ai/docLinker.ts +36 -0
- package/src/lib/MarkdownDocs/ai/promptBuilder.js +33 -0
- package/src/lib/MarkdownDocs/ai/promptBuilder.ts +53 -0
- package/src/lib/MarkdownDocs/ai/ragPipeline.js +54 -0
- package/src/lib/MarkdownDocs/ai/ragPipeline.ts +106 -0
- package/src/lib/MarkdownDocs/ai/stores/memoryStore.js +88 -0
- package/src/lib/MarkdownDocs/ai/stores/memoryStore.ts +112 -0
- package/src/lib/MarkdownDocs/ai/stores/sqliteStore.ts +372 -0
- package/src/lib/MarkdownDocs/ai/types.ts +71 -0
- package/src/lib/MarkdownDocs/aiServer.js +288 -0
- package/src/lib/MarkdownDocs/codeDirectives.js +191 -0
- package/src/lib/MarkdownDocs/codeFenceInfo.js +45 -0
- package/src/lib/MarkdownDocs/codeGroup.ts +46 -0
- package/src/lib/MarkdownDocs/common.ts +47 -0
- package/src/lib/MarkdownDocs/docsUtils.js +281 -0
- package/src/lib/MarkdownDocs/index.plugins.js +22 -0
- package/src/lib/MarkdownDocs/layouts/LayoutDoc.svelte +8 -0
- package/src/lib/MarkdownDocs/layouts/LayoutHome.svelte +58 -0
- package/src/lib/MarkdownDocs/layouts/LayoutPage.svelte +9 -0
- package/src/lib/MarkdownDocs/loadGregConfig.js +82 -0
- package/src/lib/MarkdownDocs/localeUtils.ts +682 -0
- package/src/lib/MarkdownDocs/markdownRendererRuntime.ts +314 -0
- package/src/lib/MarkdownDocs/mermaidThemes.js +319 -0
- package/src/lib/MarkdownDocs/navigationUtils.js +22 -0
- package/src/lib/MarkdownDocs/rehypeCodeGroup.js +326 -0
- package/src/lib/MarkdownDocs/rehypeCodeTitle.js +96 -0
- package/src/lib/MarkdownDocs/rehypeToc.js +170 -0
- package/src/lib/MarkdownDocs/remarkCodeMeta.js +22 -0
- package/src/lib/MarkdownDocs/remarkContainers.js +329 -0
- package/src/lib/MarkdownDocs/remarkCustomAnchors.js +42 -0
- package/src/lib/MarkdownDocs/remarkEscapeSvelte.js +33 -0
- package/src/lib/MarkdownDocs/remarkGlobalComponents.js +65 -0
- package/src/lib/MarkdownDocs/remarkImports.js +461 -0
- package/src/lib/MarkdownDocs/remarkImportsBrowser.js +349 -0
- package/src/lib/MarkdownDocs/remarkInlineAttrs.js +95 -0
- package/src/lib/MarkdownDocs/remarkMathToHtml.js +138 -0
- package/src/lib/MarkdownDocs/searchIndexBuilder.js +497 -0
- package/src/lib/MarkdownDocs/searchServer.js +263 -0
- package/src/lib/MarkdownDocs/treeViewTypes.ts +11 -0
- package/src/lib/MarkdownDocs/useRouter.svelte.ts +114 -0
- package/src/lib/MarkdownDocs/useSplitter.svelte.ts +33 -0
- package/src/lib/MarkdownDocs/versioningDefaults.js +20 -0
- package/src/lib/MarkdownDocs/vitePluginAiServer.js +204 -0
- package/src/lib/MarkdownDocs/vitePluginCopyDocs.js +153 -0
- package/src/lib/MarkdownDocs/vitePluginFrontmatter.js +109 -0
- package/src/lib/MarkdownDocs/vitePluginGregConfig.js +108 -0
- package/src/lib/MarkdownDocs/vitePluginSearchIndex.js +57 -0
- package/src/lib/MarkdownDocs/vitePluginSearchServer.js +190 -0
- package/src/lib/components/Badge.svelte +59 -0
- package/src/lib/components/Button.svelte +138 -0
- package/src/lib/components/CarbonAds.svelte +99 -0
- package/src/lib/components/CodeGroup.svelte +102 -0
- package/src/lib/components/Feature.svelte +209 -0
- package/src/lib/components/Features.svelte +123 -0
- package/src/lib/components/Hero.svelte +399 -0
- package/src/lib/components/Image.svelte +128 -0
- package/src/lib/components/Link.svelte +105 -0
- package/src/lib/components/SocialLink.svelte +84 -0
- package/src/lib/components/SocialLinks.svelte +33 -0
- package/src/lib/components/Steps.svelte +143 -0
- package/src/lib/components/TeamMember.svelte +273 -0
- package/src/lib/components/TeamMembers.svelte +81 -0
- package/src/lib/components/TeamPage.svelte +65 -0
- package/src/lib/components/TeamPageSection.svelte +108 -0
- package/src/lib/components/TeamPageTitle.svelte +89 -0
- package/src/lib/components/index.js +24 -0
- package/src/lib/portal/context.js +12 -0
- package/src/lib/portal/index.js +3 -0
- package/src/lib/portal/portal.svelte +14 -0
- package/src/lib/portal/slot.svelte +8 -0
- package/src/lib/scss/__code.scss +128 -0
- package/src/lib/scss/__containers.scss +99 -0
- package/src/lib/scss/__markdown.scss +447 -0
- package/src/lib/scss/__scrollbar.scss +60 -0
- package/src/lib/scss/__steps.scss +100 -0
- package/src/lib/scss/__theme.scss +238 -0
- package/src/lib/scss/__toc.scss +55 -0
- package/src/lib/scss/__utilities.scss +7 -0
- package/src/lib/scss/greg.scss +9 -0
- package/src/lib/spinner/spinner.svelte +42 -0
- package/svelte.config.js +146 -0
- package/types/index.d.ts +456 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* searchIndexBuilder.js
|
|
3
|
+
*
|
|
4
|
+
* Shared index-building logic used by both:
|
|
5
|
+
* - vitePluginSearchIndex (emits search-index.json as a build asset / dev middleware)
|
|
6
|
+
* - vitePluginSearchServer (serves /api/search in dev + preview)
|
|
7
|
+
* - searchServer.js (standalone production search server)
|
|
8
|
+
*
|
|
9
|
+
* The built index is cached per (docsDir, srcDir) pair so that when both
|
|
10
|
+
* plugins are active they share a single build pass.
|
|
11
|
+
* Call `invalidateSearchIndexCache()` to clear the cache (e.g. on file change).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
15
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
16
|
+
|
|
17
|
+
// ── Per-process cache ─────────────────────────────────────────────────────────
|
|
18
|
+
/** @type {Map<string, Promise<SearchEntry[]>>} */
|
|
19
|
+
const _cache = new Map();
|
|
20
|
+
const INCLUDE_RE = /^<!--\s*@include:\s*([^\s]+)\s*-->$/;
|
|
21
|
+
const BRACES_RE = /\{([^}]*)\}\s*$/;
|
|
22
|
+
const REGION_RE = /^(.*?)#([^#{]+)$/;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {{ heading: string; anchor: string; content: string }} SearchSection
|
|
26
|
+
* @typedef {{ id: string; title: string; sections: SearchSection[] }} SearchEntry
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
// ── File helpers ──────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Recursively collect all *.md files, skipping partial files/folders that start with `__`.
|
|
33
|
+
* @param {string} dir
|
|
34
|
+
* @param {string[]} [fileList]
|
|
35
|
+
* @returns {string[]}
|
|
36
|
+
*/
|
|
37
|
+
export function walkDir(dir, fileList = []) {
|
|
38
|
+
if (!existsSync(dir)) return fileList;
|
|
39
|
+
for (const file of readdirSync(dir)) {
|
|
40
|
+
if (file.startsWith('__')) continue;
|
|
41
|
+
const filePath = join(dir, file);
|
|
42
|
+
if (statSync(filePath).isDirectory()) {
|
|
43
|
+
walkDir(filePath, fileList);
|
|
44
|
+
} else if (file.endsWith('.md') && !file.startsWith('__')) {
|
|
45
|
+
fileList.push(filePath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return fileList;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Strip markdown syntax → plain, searchable text.
|
|
53
|
+
* @param {string} text
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
export function cleanMarkdown(text) {
|
|
57
|
+
return text
|
|
58
|
+
.replace(/^---[\s\S]*?---\n?/, '') // YAML frontmatter
|
|
59
|
+
.replace(/```[\s\S]*?```/g, '') // fenced code blocks
|
|
60
|
+
.replace(/~~~[\s\S]*?~~~/g, '')
|
|
61
|
+
.replace(/`([^`]+)`/g, '$1') // inline code — keep value
|
|
62
|
+
.replace(/!\[.*?\]\(.*?\)/g, '') // images
|
|
63
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // links — keep text
|
|
64
|
+
.replace(/\[([^\]]+)\]\[.*?\]/g, '$1')
|
|
65
|
+
.replace(/\*\*\*([^*]+)\*\*\*/g, '$1') // bold-italic
|
|
66
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1') // bold
|
|
67
|
+
.replace(/\*([^*]+)\*/g, '$1') // italic
|
|
68
|
+
.replace(/___([^_]+)___/g, '$1')
|
|
69
|
+
.replace(/__([^_]+)__/g, '$1')
|
|
70
|
+
.replace(/_([^_]+)_/g, '$1')
|
|
71
|
+
.replace(/<[^>]+>/g, ' ') // HTML tags
|
|
72
|
+
.replace(/^[-*_]{3,}\s*$/gm, '') // horizontal rules
|
|
73
|
+
.replace(/^>\s*/gm, '') // blockquotes
|
|
74
|
+
.replace(/\|[-:]+\|[-|: ]*/g, '\n') // table separators
|
|
75
|
+
.replace(/\|/g, ' ')
|
|
76
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
77
|
+
.trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseBraces(value) {
|
|
81
|
+
const match = String(value).match(BRACES_RE);
|
|
82
|
+
if (!match) return { text: String(value).trim(), braces: '' };
|
|
83
|
+
return { text: String(value).slice(0, match.index).trim(), braces: match[1].trim() };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseRegion(value) {
|
|
87
|
+
const match = String(value).match(REGION_RE);
|
|
88
|
+
if (!match) return { filePart: String(value).trim(), regionOrAnchor: '' };
|
|
89
|
+
return { filePart: match[1].trim(), regionOrAnchor: match[2].trim() };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseRange(value) {
|
|
93
|
+
const text = String(value ?? '').trim();
|
|
94
|
+
if (!text) return null;
|
|
95
|
+
if (/^\d+$/.test(text)) {
|
|
96
|
+
const n = Number(text);
|
|
97
|
+
return { start: n, end: n };
|
|
98
|
+
}
|
|
99
|
+
const dashMatch = text.match(/^(\d+)\s*-\s*(\d+)$/);
|
|
100
|
+
if (dashMatch) {
|
|
101
|
+
return { start: Number(dashMatch[1]), end: Number(dashMatch[2]) };
|
|
102
|
+
}
|
|
103
|
+
const commaMatch = text.match(/^(\d*)\s*,\s*(\d*)$/);
|
|
104
|
+
if (!commaMatch) return null;
|
|
105
|
+
const start = commaMatch[1] ? Number(commaMatch[1]) : null;
|
|
106
|
+
const end = commaMatch[2] ? Number(commaMatch[2]) : null;
|
|
107
|
+
return { start, end };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function selectLines(content, rangeText) {
|
|
111
|
+
const range = parseRange(rangeText);
|
|
112
|
+
if (!range) return content;
|
|
113
|
+
const lines = content.split(/\r?\n/);
|
|
114
|
+
const start = range.start ?? 1;
|
|
115
|
+
const end = range.end ?? lines.length;
|
|
116
|
+
const from = Math.max(1, start);
|
|
117
|
+
const to = Math.max(from, Math.min(end, lines.length));
|
|
118
|
+
return lines.slice(from - 1, to).join('\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function escapeRegExp(value) {
|
|
122
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function selectRegion(content, regionName) {
|
|
126
|
+
if (!regionName) return content;
|
|
127
|
+
const lines = content.split(/\r?\n/);
|
|
128
|
+
const escapedName = escapeRegExp(regionName);
|
|
129
|
+
const startRe = new RegExp(`#region\\s+${escapedName}\\s*$`, 'i');
|
|
130
|
+
const endRe = new RegExp(`#endregion\\s+${escapedName}\\s*$`, 'i');
|
|
131
|
+
|
|
132
|
+
let start = -1;
|
|
133
|
+
let end = -1;
|
|
134
|
+
for (let i = 0; i < lines.length; i++) {
|
|
135
|
+
if (start === -1 && startRe.test(lines[i])) {
|
|
136
|
+
start = i;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (start !== -1 && endRe.test(lines[i])) {
|
|
140
|
+
end = i;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (start === -1 || end === -1 || end <= start) return content;
|
|
146
|
+
return lines.slice(start + 1, end).join('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getHeadingId(rawHeadingText) {
|
|
150
|
+
const customIdMatch = rawHeadingText.match(/\s*\{#([^}]+)\}\s*$/);
|
|
151
|
+
if (customIdMatch?.[1]) return customIdMatch[1].trim().toLowerCase();
|
|
152
|
+
return rawHeadingText
|
|
153
|
+
.replace(/\s*\{#([^}]+)\}\s*$/, '')
|
|
154
|
+
.toLowerCase()
|
|
155
|
+
.replace(/<[^>]+>/g, '')
|
|
156
|
+
.replace(/[^\w\s-]/g, '')
|
|
157
|
+
.trim()
|
|
158
|
+
.replace(/\s+/g, '-');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function selectMarkdownSectionByAnchor(content, anchor) {
|
|
162
|
+
if (!anchor) return content;
|
|
163
|
+
const lines = content.split(/\r?\n/);
|
|
164
|
+
const headingRe = /^(#{1,6})\s+(.+)$/;
|
|
165
|
+
const wanted = anchor.toLowerCase();
|
|
166
|
+
|
|
167
|
+
let startIndex = -1;
|
|
168
|
+
let startDepth = 0;
|
|
169
|
+
for (let i = 0; i < lines.length; i++) {
|
|
170
|
+
const match = lines[i].match(headingRe);
|
|
171
|
+
if (!match) continue;
|
|
172
|
+
const depth = match[1].length;
|
|
173
|
+
const id = getHeadingId(match[2]);
|
|
174
|
+
if (id === wanted) {
|
|
175
|
+
startIndex = i;
|
|
176
|
+
startDepth = depth;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (startIndex === -1) return content;
|
|
182
|
+
|
|
183
|
+
let endIndex = lines.length;
|
|
184
|
+
for (let i = startIndex + 1; i < lines.length; i++) {
|
|
185
|
+
const match = lines[i].match(headingRe);
|
|
186
|
+
if (!match) continue;
|
|
187
|
+
const depth = match[1].length;
|
|
188
|
+
if (depth <= startDepth) {
|
|
189
|
+
endIndex = i;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return lines.slice(startIndex, endIndex).join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function stripFrontmatter(content) {
|
|
198
|
+
return String(content).replace(/^---[\r\n][\s\S]*?[\r\n]---[\r\n]?/, '');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function resolveImportPath(specPath, currentDir, sourceRoot, docsDir) {
|
|
202
|
+
if (specPath === '@') return sourceRoot;
|
|
203
|
+
if (specPath.startsWith('@/')) return resolve(sourceRoot, specPath.slice(2));
|
|
204
|
+
if (specPath.startsWith('@')) return resolve(sourceRoot, specPath.slice(1));
|
|
205
|
+
if (specPath.startsWith('/')) return resolve(docsDir, specPath.slice(1));
|
|
206
|
+
return resolve(currentDir, specPath);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parseIncludeSpec(raw) {
|
|
210
|
+
const withBraces = parseBraces(raw);
|
|
211
|
+
const withRegion = parseRegion(withBraces.text);
|
|
212
|
+
return {
|
|
213
|
+
filePart: withRegion.filePart,
|
|
214
|
+
regionOrAnchor: withRegion.regionOrAnchor,
|
|
215
|
+
rangePart: withBraces.braces,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function resolveIncludeContent(rawSpec, currentDir, docsDir, sourceRoot, stack) {
|
|
220
|
+
const parsed = parseIncludeSpec(rawSpec);
|
|
221
|
+
const includePath = resolveImportPath(parsed.filePart, currentDir, sourceRoot, docsDir);
|
|
222
|
+
|
|
223
|
+
if (stack.includes(includePath) || !existsSync(includePath)) return '';
|
|
224
|
+
|
|
225
|
+
let content = readFileSync(includePath, 'utf-8');
|
|
226
|
+
if (parsed.regionOrAnchor) {
|
|
227
|
+
const byRegion = selectRegion(content, parsed.regionOrAnchor);
|
|
228
|
+
content = byRegion === content
|
|
229
|
+
? selectMarkdownSectionByAnchor(content, parsed.regionOrAnchor)
|
|
230
|
+
: byRegion;
|
|
231
|
+
}
|
|
232
|
+
if (parsed.rangePart) content = selectLines(content, parsed.rangePart);
|
|
233
|
+
|
|
234
|
+
if (includePath.endsWith('.md')) {
|
|
235
|
+
content = stripFrontmatter(content);
|
|
236
|
+
content = resolveMarkdownIncludes(content, dirname(includePath), docsDir, sourceRoot, [...stack, includePath]);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return content;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function resolveMarkdownIncludes(content, currentDir, docsDir, sourceRoot, stack) {
|
|
243
|
+
const lines = String(content).split(/\r?\n/);
|
|
244
|
+
const out = [];
|
|
245
|
+
let activeFence = null;
|
|
246
|
+
|
|
247
|
+
for (const line of lines) {
|
|
248
|
+
const trimmed = line.trim();
|
|
249
|
+
|
|
250
|
+
const fenceMatch = trimmed.match(/^(```+|~~~+)/);
|
|
251
|
+
if (fenceMatch) {
|
|
252
|
+
const fence = fenceMatch[1][0];
|
|
253
|
+
if (!activeFence) {
|
|
254
|
+
activeFence = fence;
|
|
255
|
+
} else if (activeFence === fence) {
|
|
256
|
+
activeFence = null;
|
|
257
|
+
}
|
|
258
|
+
out.push(line);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (activeFence) {
|
|
263
|
+
out.push(line);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const includeMatch = trimmed.match(INCLUDE_RE);
|
|
268
|
+
if (includeMatch?.[1]) {
|
|
269
|
+
out.push(resolveIncludeContent(includeMatch[1].trim(), currentDir, docsDir, sourceRoot, stack));
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
out.push(line);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return out.join('\n');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Split a markdown document into sections delimited by headings.
|
|
281
|
+
* @param {string} markdown
|
|
282
|
+
* @returns {SearchSection[]}
|
|
283
|
+
*/
|
|
284
|
+
export function extractSections(markdown) {
|
|
285
|
+
const lines = markdown.split('\n');
|
|
286
|
+
const sections = [];
|
|
287
|
+
let currentHeading = '';
|
|
288
|
+
let currentAnchor = '';
|
|
289
|
+
let currentLines = [];
|
|
290
|
+
|
|
291
|
+
const flush = () => {
|
|
292
|
+
const content = cleanMarkdown(currentLines.join('\n')).trim();
|
|
293
|
+
if (content || currentHeading) {
|
|
294
|
+
sections.push({ heading: currentHeading, anchor: currentAnchor, content });
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
for (const line of lines) {
|
|
299
|
+
const m = line.match(/^(#{1,6})\s+(.+)/);
|
|
300
|
+
if (m) {
|
|
301
|
+
flush();
|
|
302
|
+
currentHeading = m[2].replace(/\s*\{[#.][^}]*\}\s*$/, '').trim();
|
|
303
|
+
currentAnchor = currentHeading
|
|
304
|
+
.toLowerCase()
|
|
305
|
+
.replace(/[^\w\s-]/g, '')
|
|
306
|
+
.replace(/\s+/g, '-')
|
|
307
|
+
.replace(/^-+|-+$/g, '');
|
|
308
|
+
currentLines = [];
|
|
309
|
+
} else {
|
|
310
|
+
currentLines.push(line);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
flush();
|
|
314
|
+
|
|
315
|
+
return sections.filter(s => s.content || s.heading);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── Index builder (cached) ────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Build the full search index from all docs in `docsDir` (or multiple dirs).
|
|
322
|
+
* Results are cached per (docsDir+srcDir) so both Vite plugins share one pass.
|
|
323
|
+
*
|
|
324
|
+
* @param {string | string[]} docsDir Absolute path(s) to the docs directory/directories.
|
|
325
|
+
* @param {string} srcDir SPA route prefix, e.g. '/docs'.
|
|
326
|
+
* @returns {Promise<SearchEntry[]>}
|
|
327
|
+
*/
|
|
328
|
+
export function buildSearchIndex(docsDir, srcDir) {
|
|
329
|
+
const dirs = Array.isArray(docsDir) ? docsDir : [docsDir];
|
|
330
|
+
const key = dirs.join('|') + '::' + srcDir;
|
|
331
|
+
if (_cache.has(key)) return /** @type {Promise<SearchEntry[]>} */ (_cache.get(key));
|
|
332
|
+
|
|
333
|
+
const promise = (async () => {
|
|
334
|
+
const index = [];
|
|
335
|
+
|
|
336
|
+
for (const dir of dirs) {
|
|
337
|
+
const files = walkDir(dir);
|
|
338
|
+
const sourceRoot = resolve(dir, '..');
|
|
339
|
+
|
|
340
|
+
for (const filePath of files) {
|
|
341
|
+
let content;
|
|
342
|
+
try { content = readFileSync(filePath, 'utf-8'); } catch { continue; }
|
|
343
|
+
content = resolveMarkdownIncludes(content, dirname(filePath), dir, sourceRoot, [filePath]);
|
|
344
|
+
|
|
345
|
+
const relPath = relative(dir, filePath)
|
|
346
|
+
.replace(/\\/g, '/')
|
|
347
|
+
.replace(/\.md$/, '');
|
|
348
|
+
|
|
349
|
+
let routePath;
|
|
350
|
+
if (relPath === 'index') {
|
|
351
|
+
routePath = srcDir;
|
|
352
|
+
} else if (relPath.endsWith('/index')) {
|
|
353
|
+
routePath = srcDir + '/' + relPath.slice(0, -6);
|
|
354
|
+
} else {
|
|
355
|
+
routePath = srcDir + '/' + relPath;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const sections = extractSections(content);
|
|
359
|
+
const title =
|
|
360
|
+
sections[0]?.heading ||
|
|
361
|
+
relPath.split('/').pop()
|
|
362
|
+
.replace(/-/g, ' ')
|
|
363
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
364
|
+
|
|
365
|
+
index.push({
|
|
366
|
+
id: routePath,
|
|
367
|
+
title,
|
|
368
|
+
sections: sections.map(({ heading, anchor, content }) => ({ heading, anchor, content })),
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return index;
|
|
374
|
+
})();
|
|
375
|
+
|
|
376
|
+
_cache.set(key, promise);
|
|
377
|
+
return promise;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Clear the build cache. Call when markdown files change. */
|
|
381
|
+
export function invalidateSearchIndexCache() {
|
|
382
|
+
_cache.clear();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ── Fuse.js result → SearchResult ───────────────────────────────────────────
|
|
386
|
+
// These helpers run on the server side (inside the Vite plugin / standalone server).
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* @param {string} str
|
|
390
|
+
* @returns {string}
|
|
391
|
+
*/
|
|
392
|
+
export function escapeHtml(str) {
|
|
393
|
+
return str
|
|
394
|
+
.replace(/&/g, '&')
|
|
395
|
+
.replace(/</g, '<')
|
|
396
|
+
.replace(/>/g, '>')
|
|
397
|
+
.replace(/"/g, '"');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* @param {string} text
|
|
402
|
+
* @param {[number,number][]} indices
|
|
403
|
+
* @returns {string}
|
|
404
|
+
*/
|
|
405
|
+
function highlightText(text, indices) {
|
|
406
|
+
if (!indices?.length) return escapeHtml(text);
|
|
407
|
+
let html = '';
|
|
408
|
+
let last = 0;
|
|
409
|
+
for (const [s, e] of indices) {
|
|
410
|
+
if (s >= text.length) break;
|
|
411
|
+
if (s > last) html += escapeHtml(text.slice(last, s));
|
|
412
|
+
html += `<mark>${escapeHtml(text.slice(s, e + 1))}</mark>`;
|
|
413
|
+
last = e + 1;
|
|
414
|
+
}
|
|
415
|
+
html += escapeHtml(text.slice(last));
|
|
416
|
+
return html;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* @param {string} text
|
|
421
|
+
* @param {[number,number][]} indices
|
|
422
|
+
* @param {number} [contextLen]
|
|
423
|
+
* @returns {string}
|
|
424
|
+
*/
|
|
425
|
+
function getExcerptHtml(text, indices, contextLen = 150) {
|
|
426
|
+
if (!text) return '';
|
|
427
|
+
if (!indices?.length) {
|
|
428
|
+
return escapeHtml(text.slice(0, contextLen)) + (text.length > contextLen ? '…' : '');
|
|
429
|
+
}
|
|
430
|
+
const firstMatchStart = indices[0][0];
|
|
431
|
+
const from = Math.max(0, firstMatchStart - 50);
|
|
432
|
+
const to = Math.min(text.length, from + contextLen);
|
|
433
|
+
const sliced = text.slice(from, to);
|
|
434
|
+
const adjusted = indices
|
|
435
|
+
.map(([s, e]) => [s - from, e - from])
|
|
436
|
+
.filter(([s, e]) => e >= 0 && s < sliced.length)
|
|
437
|
+
.map(([s, e]) => [Math.max(0, s), Math.min(sliced.length - 1, e)]);
|
|
438
|
+
|
|
439
|
+
const prefix = from > 0 ? '…' : '';
|
|
440
|
+
const suffix = to < text.length ? '…' : '';
|
|
441
|
+
let html = prefix;
|
|
442
|
+
let last = 0;
|
|
443
|
+
for (const [s, e] of adjusted) {
|
|
444
|
+
if (s > last) html += escapeHtml(sliced.slice(last, s));
|
|
445
|
+
html += `<mark>${escapeHtml(sliced.slice(s, e + 1))}</mark>`;
|
|
446
|
+
last = e + 1;
|
|
447
|
+
}
|
|
448
|
+
html += escapeHtml(sliced.slice(last)) + suffix;
|
|
449
|
+
return html;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Convert a raw Fuse.js result object into a client-ready SearchResult.
|
|
454
|
+
* @param {any} fuseResult
|
|
455
|
+
* @returns {{ id: string; title: string; titleHtml: string; sectionTitle: string; sectionTitleHtml: string; sectionAnchor: string; excerptHtml: string; score: number }}
|
|
456
|
+
*/
|
|
457
|
+
export function buildFuseResult(fuseResult) {
|
|
458
|
+
const { item, matches = [], score = 1 } = fuseResult;
|
|
459
|
+
|
|
460
|
+
const sorted = [...matches].sort(
|
|
461
|
+
(a, b) =>
|
|
462
|
+
b.indices.reduce((s, [x, y]) => s + (y - x), 0) -
|
|
463
|
+
a.indices.reduce((s, [x, y]) => s + (y - x), 0),
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
const titleMatch = sorted.find(m => m.key === 'title');
|
|
467
|
+
const sectionContent = sorted.find(m => m.key === 'sections.content');
|
|
468
|
+
const sectionHeading = sorted.find(m => m.key === 'sections.heading');
|
|
469
|
+
|
|
470
|
+
let excerptHtml = '', sectionTitle = '', sectionTitleHtml = '', sectionAnchor = '';
|
|
471
|
+
|
|
472
|
+
if (sectionHeading) {
|
|
473
|
+
const sec = item.sections[sectionHeading.refIndex];
|
|
474
|
+
sectionTitle = sec?.heading ?? '';
|
|
475
|
+
sectionTitleHtml = highlightText(sectionTitle, sectionHeading.indices);
|
|
476
|
+
sectionAnchor = sec?.anchor ?? '';
|
|
477
|
+
// Heading match is usually the most relevant intent signal.
|
|
478
|
+
excerptHtml = escapeHtml((sec?.content ?? '').slice(0, 150));
|
|
479
|
+
} else if (sectionContent) {
|
|
480
|
+
const sec = item.sections[sectionContent.refIndex];
|
|
481
|
+
sectionTitle = sec?.heading ?? '';
|
|
482
|
+
sectionTitleHtml = escapeHtml(sectionTitle);
|
|
483
|
+
sectionAnchor = sec?.anchor ?? '';
|
|
484
|
+
excerptHtml = getExcerptHtml(sectionContent.value, sectionContent.indices);
|
|
485
|
+
} else {
|
|
486
|
+
sectionTitleHtml = escapeHtml(sectionTitle);
|
|
487
|
+
excerptHtml = escapeHtml((item.sections[0]?.content ?? '').slice(0, 150));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (!sectionTitleHtml) sectionTitleHtml = escapeHtml(sectionTitle);
|
|
491
|
+
|
|
492
|
+
const titleHtml = titleMatch
|
|
493
|
+
? highlightText(item.title, titleMatch.indices)
|
|
494
|
+
: escapeHtml(item.title);
|
|
495
|
+
|
|
496
|
+
return { id: item.id, title: item.title, titleHtml, sectionTitle, sectionTitleHtml, sectionAnchor, excerptHtml, score };
|
|
497
|
+
}
|