@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.
Files changed (183) hide show
  1. package/README.md +397 -0
  2. package/bin/greg.js +241 -0
  3. package/bin/init.js +351 -0
  4. package/bin/templates/docs/getting-started.md +47 -0
  5. package/bin/templates/docs/index.md +11 -0
  6. package/bin/templates/greg.config.js +39 -0
  7. package/bin/templates/greg.config.ts +38 -0
  8. package/bin/templates/index.html +16 -0
  9. package/bin/templates/src/App.svelte +5 -0
  10. package/bin/templates/src/app.css +20 -0
  11. package/bin/templates/src/main.js +9 -0
  12. package/bin/templates/svelte.config.js +1 -0
  13. package/bin/templates/tsconfig.json +21 -0
  14. package/bin/templates/vite.config.js +23 -0
  15. package/docs/__partials/markdown/examples/basic.md +4 -0
  16. package/docs/__partials/markdown/examples/diff.md +10 -0
  17. package/docs/__partials/markdown/examples/focus.md +5 -0
  18. package/docs/__partials/markdown/examples/language-title.md +3 -0
  19. package/docs/__partials/markdown/examples/line-highlighting.md +5 -0
  20. package/docs/__partials/markdown/examples/line-numbers.md +5 -0
  21. package/docs/__partials/note.md +4 -0
  22. package/docs/guide/__shared-warning.md +4 -0
  23. package/docs/guide/asset-handling.md +88 -0
  24. package/docs/guide/deploying.md +162 -0
  25. package/docs/guide/getting-started.md +334 -0
  26. package/docs/guide/index.md +23 -0
  27. package/docs/guide/localization.md +290 -0
  28. package/docs/guide/markdown/code.md +95 -0
  29. package/docs/guide/markdown/components-and-mermaid.md +43 -0
  30. package/docs/guide/markdown/containers.md +110 -0
  31. package/docs/guide/markdown/header-anchors.md +34 -0
  32. package/docs/guide/markdown/includes.md +84 -0
  33. package/docs/guide/markdown/index.md +20 -0
  34. package/docs/guide/markdown/inline-attributes.md +21 -0
  35. package/docs/guide/markdown/links-and-toc.md +64 -0
  36. package/docs/guide/markdown/math.md +54 -0
  37. package/docs/guide/markdown/syntax-highlighting.md +75 -0
  38. package/docs/guide/routing.md +150 -0
  39. package/docs/guide/using-svelte.md +88 -0
  40. package/docs/guide/versioning.md +281 -0
  41. package/docs/incompatibilities.md +48 -0
  42. package/docs/index.md +43 -0
  43. package/docs/reference/badge.md +100 -0
  44. package/docs/reference/carbon-ads.md +46 -0
  45. package/docs/reference/code-group.md +126 -0
  46. package/docs/reference/home-page.md +232 -0
  47. package/docs/reference/index.md +18 -0
  48. package/docs/reference/markdowndocs.md +275 -0
  49. package/docs/reference/outline.md +79 -0
  50. package/docs/reference/search.md +263 -0
  51. package/docs/reference/steps.md +200 -0
  52. package/docs/reference/team-page.md +189 -0
  53. package/docs/reference/theme.md +150 -0
  54. package/fakeDocsGenerator/generate_docs.js +310 -0
  55. package/package.json +92 -0
  56. package/scripts/build-versions.js +609 -0
  57. package/scripts/generate-static.js +79 -0
  58. package/scripts/render-markdown.js +420 -0
  59. package/src/lib/MarkdownDocs/AiChat.svelte +936 -0
  60. package/src/lib/MarkdownDocs/BackToTop.svelte +68 -0
  61. package/src/lib/MarkdownDocs/Breadcrumb.svelte +68 -0
  62. package/src/lib/MarkdownDocs/DocsNavigation.svelte +149 -0
  63. package/src/lib/MarkdownDocs/DocsSiteHeader.svelte +758 -0
  64. package/src/lib/MarkdownDocs/DocsVersionSwitcher.svelte +103 -0
  65. package/src/lib/MarkdownDocs/MarkdownDocs.svelte +2115 -0
  66. package/src/lib/MarkdownDocs/MarkdownRenderer.svelte +487 -0
  67. package/src/lib/MarkdownDocs/Outline.svelte +238 -0
  68. package/src/lib/MarkdownDocs/PrevNext.svelte +115 -0
  69. package/src/lib/MarkdownDocs/SearchModal.svelte +1241 -0
  70. package/src/lib/MarkdownDocs/TreeView.svelte +32 -0
  71. package/src/lib/MarkdownDocs/TreeViewItem.svelte +219 -0
  72. package/src/lib/MarkdownDocs/VersionOutdatedNotice.svelte +72 -0
  73. package/src/lib/MarkdownDocs/__tests__/codeDirectives.test.js +54 -0
  74. package/src/lib/MarkdownDocs/__tests__/common.test.js +41 -0
  75. package/src/lib/MarkdownDocs/__tests__/docsExamplesLint.test.js +77 -0
  76. package/src/lib/MarkdownDocs/__tests__/fixtures/docs/markdown/__partial-basic.md +3 -0
  77. package/src/lib/MarkdownDocs/__tests__/fixtures/docs/markdown/snippet.js +9 -0
  78. package/src/lib/MarkdownDocs/__tests__/fixtures/includes/part.md +11 -0
  79. package/src/lib/MarkdownDocs/__tests__/fixtures/includes/wrapper.md +5 -0
  80. package/src/lib/MarkdownDocs/__tests__/fixtures/snippets/sample.js +8 -0
  81. package/src/lib/MarkdownDocs/__tests__/fixtures/snippets/sample.md +5 -0
  82. package/src/lib/MarkdownDocs/__tests__/helpers.js +67 -0
  83. package/src/lib/MarkdownDocs/__tests__/localeUtils.test.js +204 -0
  84. package/src/lib/MarkdownDocs/__tests__/markdown.test.js +704 -0
  85. package/src/lib/MarkdownDocs/__tests__/markdownRendererRuntime.test.js +65 -0
  86. package/src/lib/MarkdownDocs/__tests__/searchIndexBuilder.test.js +117 -0
  87. package/src/lib/MarkdownDocs/__tests__/sqliteStore.test.js +202 -0
  88. package/src/lib/MarkdownDocs/__tests__/useRouter.test.js +16 -0
  89. package/src/lib/MarkdownDocs/ai/adapters/customAdapter.js +14 -0
  90. package/src/lib/MarkdownDocs/ai/adapters/customAdapter.ts +43 -0
  91. package/src/lib/MarkdownDocs/ai/adapters/ollamaAdapter.js +81 -0
  92. package/src/lib/MarkdownDocs/ai/adapters/ollamaAdapter.ts +116 -0
  93. package/src/lib/MarkdownDocs/ai/adapters/openaiAdapter.js +92 -0
  94. package/src/lib/MarkdownDocs/ai/adapters/openaiAdapter.ts +137 -0
  95. package/src/lib/MarkdownDocs/ai/aiProvider.ts +31 -0
  96. package/src/lib/MarkdownDocs/ai/characters.js +52 -0
  97. package/src/lib/MarkdownDocs/ai/characters.ts +69 -0
  98. package/src/lib/MarkdownDocs/ai/chunkStore.ts +25 -0
  99. package/src/lib/MarkdownDocs/ai/chunker.js +85 -0
  100. package/src/lib/MarkdownDocs/ai/chunker.ts +135 -0
  101. package/src/lib/MarkdownDocs/ai/docLinker.js +26 -0
  102. package/src/lib/MarkdownDocs/ai/docLinker.ts +36 -0
  103. package/src/lib/MarkdownDocs/ai/promptBuilder.js +33 -0
  104. package/src/lib/MarkdownDocs/ai/promptBuilder.ts +53 -0
  105. package/src/lib/MarkdownDocs/ai/ragPipeline.js +54 -0
  106. package/src/lib/MarkdownDocs/ai/ragPipeline.ts +106 -0
  107. package/src/lib/MarkdownDocs/ai/stores/memoryStore.js +88 -0
  108. package/src/lib/MarkdownDocs/ai/stores/memoryStore.ts +112 -0
  109. package/src/lib/MarkdownDocs/ai/stores/sqliteStore.ts +372 -0
  110. package/src/lib/MarkdownDocs/ai/types.ts +71 -0
  111. package/src/lib/MarkdownDocs/aiServer.js +288 -0
  112. package/src/lib/MarkdownDocs/codeDirectives.js +191 -0
  113. package/src/lib/MarkdownDocs/codeFenceInfo.js +45 -0
  114. package/src/lib/MarkdownDocs/codeGroup.ts +46 -0
  115. package/src/lib/MarkdownDocs/common.ts +47 -0
  116. package/src/lib/MarkdownDocs/docsUtils.js +281 -0
  117. package/src/lib/MarkdownDocs/index.plugins.js +22 -0
  118. package/src/lib/MarkdownDocs/layouts/LayoutDoc.svelte +8 -0
  119. package/src/lib/MarkdownDocs/layouts/LayoutHome.svelte +58 -0
  120. package/src/lib/MarkdownDocs/layouts/LayoutPage.svelte +9 -0
  121. package/src/lib/MarkdownDocs/loadGregConfig.js +82 -0
  122. package/src/lib/MarkdownDocs/localeUtils.ts +682 -0
  123. package/src/lib/MarkdownDocs/markdownRendererRuntime.ts +314 -0
  124. package/src/lib/MarkdownDocs/mermaidThemes.js +319 -0
  125. package/src/lib/MarkdownDocs/navigationUtils.js +22 -0
  126. package/src/lib/MarkdownDocs/rehypeCodeGroup.js +326 -0
  127. package/src/lib/MarkdownDocs/rehypeCodeTitle.js +96 -0
  128. package/src/lib/MarkdownDocs/rehypeToc.js +170 -0
  129. package/src/lib/MarkdownDocs/remarkCodeMeta.js +22 -0
  130. package/src/lib/MarkdownDocs/remarkContainers.js +329 -0
  131. package/src/lib/MarkdownDocs/remarkCustomAnchors.js +42 -0
  132. package/src/lib/MarkdownDocs/remarkEscapeSvelte.js +33 -0
  133. package/src/lib/MarkdownDocs/remarkGlobalComponents.js +65 -0
  134. package/src/lib/MarkdownDocs/remarkImports.js +461 -0
  135. package/src/lib/MarkdownDocs/remarkImportsBrowser.js +349 -0
  136. package/src/lib/MarkdownDocs/remarkInlineAttrs.js +95 -0
  137. package/src/lib/MarkdownDocs/remarkMathToHtml.js +138 -0
  138. package/src/lib/MarkdownDocs/searchIndexBuilder.js +497 -0
  139. package/src/lib/MarkdownDocs/searchServer.js +263 -0
  140. package/src/lib/MarkdownDocs/treeViewTypes.ts +11 -0
  141. package/src/lib/MarkdownDocs/useRouter.svelte.ts +114 -0
  142. package/src/lib/MarkdownDocs/useSplitter.svelte.ts +33 -0
  143. package/src/lib/MarkdownDocs/versioningDefaults.js +20 -0
  144. package/src/lib/MarkdownDocs/vitePluginAiServer.js +204 -0
  145. package/src/lib/MarkdownDocs/vitePluginCopyDocs.js +153 -0
  146. package/src/lib/MarkdownDocs/vitePluginFrontmatter.js +109 -0
  147. package/src/lib/MarkdownDocs/vitePluginGregConfig.js +108 -0
  148. package/src/lib/MarkdownDocs/vitePluginSearchIndex.js +57 -0
  149. package/src/lib/MarkdownDocs/vitePluginSearchServer.js +190 -0
  150. package/src/lib/components/Badge.svelte +59 -0
  151. package/src/lib/components/Button.svelte +138 -0
  152. package/src/lib/components/CarbonAds.svelte +99 -0
  153. package/src/lib/components/CodeGroup.svelte +102 -0
  154. package/src/lib/components/Feature.svelte +209 -0
  155. package/src/lib/components/Features.svelte +123 -0
  156. package/src/lib/components/Hero.svelte +399 -0
  157. package/src/lib/components/Image.svelte +128 -0
  158. package/src/lib/components/Link.svelte +105 -0
  159. package/src/lib/components/SocialLink.svelte +84 -0
  160. package/src/lib/components/SocialLinks.svelte +33 -0
  161. package/src/lib/components/Steps.svelte +143 -0
  162. package/src/lib/components/TeamMember.svelte +273 -0
  163. package/src/lib/components/TeamMembers.svelte +81 -0
  164. package/src/lib/components/TeamPage.svelte +65 -0
  165. package/src/lib/components/TeamPageSection.svelte +108 -0
  166. package/src/lib/components/TeamPageTitle.svelte +89 -0
  167. package/src/lib/components/index.js +24 -0
  168. package/src/lib/portal/context.js +12 -0
  169. package/src/lib/portal/index.js +3 -0
  170. package/src/lib/portal/portal.svelte +14 -0
  171. package/src/lib/portal/slot.svelte +8 -0
  172. package/src/lib/scss/__code.scss +128 -0
  173. package/src/lib/scss/__containers.scss +99 -0
  174. package/src/lib/scss/__markdown.scss +447 -0
  175. package/src/lib/scss/__scrollbar.scss +60 -0
  176. package/src/lib/scss/__steps.scss +100 -0
  177. package/src/lib/scss/__theme.scss +238 -0
  178. package/src/lib/scss/__toc.scss +55 -0
  179. package/src/lib/scss/__utilities.scss +7 -0
  180. package/src/lib/scss/greg.scss +9 -0
  181. package/src/lib/spinner/spinner.svelte +42 -0
  182. package/svelte.config.js +146 -0
  183. package/types/index.d.ts +456 -0
@@ -0,0 +1,153 @@
1
+ /**
2
+ * vitePluginCopyDocs
3
+ *
4
+ * Copies docs/**\/*.md (and optional extra static dirs) to the build output
5
+ * directory as-is, so they can be fetched at runtime by the browser without
6
+ * being compiled by mdsvex/rollup.
7
+ *
8
+ * This is the key enabler of the "runtime markdown" architecture: Vite only
9
+ * compiles the app shell, and page content is fetched on demand.
10
+ *
11
+ * The `staticDirs` option (default: ['snippets']) lists additional project-root
12
+ * directories whose files should also be served/copied verbatim. This is
13
+ * needed for `<<< @”‹/snippets/file.js` style snippet includes.
14
+ */
15
+
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+
19
+ function trimSlashes(value) {
20
+ return String(value || '').replace(/^\/+|\/+$/g, '');
21
+ }
22
+
23
+ export function vitePluginCopyDocs({ docsDir = 'docs', srcDir = '/docs', staticDirs = ['snippets'] } = {}) {
24
+ let root = process.cwd();
25
+ let outDir = 'dist';
26
+ let viteBase = '/';
27
+
28
+ function* walkAll(dir) {
29
+ if (!fs.existsSync(dir)) return;
30
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
31
+ const full = path.join(dir, entry.name);
32
+ if (entry.isDirectory()) {
33
+ yield* walkAll(full);
34
+ } else if (entry.isFile()) {
35
+ yield full;
36
+ }
37
+ }
38
+ }
39
+
40
+ function* walkMd(dir) {
41
+ for (const full of walkAll(dir)) {
42
+ if (full.endsWith('.md')) yield full;
43
+ }
44
+ }
45
+
46
+ function toPosix(value) {
47
+ return String(value).replace(/\\/g, '/');
48
+ }
49
+
50
+ function resolveRootPrefix() {
51
+ const cleaned = trimSlashes(srcDir);
52
+ return cleaned ? '/' + cleaned : '';
53
+ }
54
+
55
+ return {
56
+ name: 'greg:copy-docs',
57
+
58
+ configResolved(config) {
59
+ root = config.root;
60
+ outDir = path.resolve(config.root, config.build.outDir);
61
+ viteBase = config.base ?? '/';
62
+ },
63
+
64
+ /**
65
+ * In dev mode: serve .md files and staticDirs files as plain text.
66
+ * Vite doesn't auto-serve project files outside public/ as raw assets.
67
+ */
68
+ configureServer(server) {
69
+ const rootPrefix = resolveRootPrefix();
70
+ server.middlewares.use((req, res, next) => {
71
+ const originalUrl = req.url ?? '';
72
+ const [urlPath, query = ''] = originalUrl.split('?');
73
+ const rawUrl = urlPath ?? '';
74
+
75
+ // Let Vite handle module requests like `/docs/file.md?import`.
76
+ if (query) {
77
+ next();
78
+ return;
79
+ }
80
+
81
+ // Strip the Vite base prefix (e.g. /greg/) from the URL.
82
+ // configureServer middleware runs before Vite's own base-stripping
83
+ // middleware, so req.url still contains the full base prefix.
84
+ const base = viteBase === '/' ? '' : viteBase.replace(/\/$/, '');
85
+ const url = (base && rawUrl.startsWith(base))
86
+ ? '/' + rawUrl.slice(base.length).replace(/^\/+/, '')
87
+ : rawUrl;
88
+
89
+ // Docs markdown files
90
+ const isDocsMarkdown = rootPrefix
91
+ ? (url === rootPrefix || url.startsWith(rootPrefix + '/')) && url.endsWith('.md')
92
+ : url.startsWith('/') && url.endsWith('.md');
93
+ if (isDocsMarkdown) {
94
+ const rel = url.slice(rootPrefix.length).replace(/^\//, '');
95
+ for (const dir of (Array.isArray(docsDir) ? docsDir : [docsDir])) {
96
+ const filePath = path.resolve(root, dir, rel);
97
+ if (fs.existsSync(filePath)) {
98
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
99
+ res.end(fs.readFileSync(filePath, 'utf8'));
100
+ return;
101
+ }
102
+ }
103
+ }
104
+
105
+ // Extra static dirs (snippets etc.)
106
+ for (const dir of staticDirs) {
107
+ if (url.startsWith('/' + dir + '/') || url === '/' + dir) {
108
+ const filePath = path.resolve(root, url.slice(1));
109
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
110
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
111
+ res.end(fs.readFileSync(filePath, 'utf8'));
112
+ return;
113
+ }
114
+ }
115
+ }
116
+
117
+ next();
118
+ });
119
+ },
120
+
121
+ /** After bundle is written, copy .md files and staticDirs verbatim. */
122
+ writeBundle() {
123
+ let count = 0;
124
+ const rootPrefix = trimSlashes(resolveRootPrefix());
125
+
126
+ // Copy markdown docs from all source dirs
127
+ for (const dir of (Array.isArray(docsDir) ? docsDir : [docsDir])) {
128
+ const docsRoot = path.resolve(root, dir);
129
+ for (const srcFile of walkMd(docsRoot)) {
130
+ const rel = toPosix(path.relative(docsRoot, srcFile));
131
+ const destFile = path.join(outDir, rootPrefix, rel);
132
+ fs.mkdirSync(path.dirname(destFile), { recursive: true });
133
+ fs.copyFileSync(srcFile, destFile);
134
+ count++;
135
+ }
136
+ }
137
+
138
+ // Copy extra static dirs
139
+ for (const dir of staticDirs) {
140
+ const dirRoot = path.resolve(root, dir);
141
+ for (const srcFile of walkAll(dirRoot)) {
142
+ const rel = path.relative(root, srcFile);
143
+ const destFile = path.join(outDir, rel);
144
+ fs.mkdirSync(path.dirname(destFile), { recursive: true });
145
+ fs.copyFileSync(srcFile, destFile);
146
+ count++;
147
+ }
148
+ }
149
+
150
+ console.log(`\x1b[32m✓\x1b[0m greg:copy-docs – ${count} files copied to ${path.relative(root, outDir)}/`);
151
+ },
152
+ };
153
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * vitePluginFrontmatter
3
+ *
4
+ * Scans all markdown files under `docsDir` at build/dev time, extracts their
5
+ * YAML frontmatter using js-yaml and exposes the result as a virtual module:
6
+ *
7
+ * import frontmatters from 'virtual:greg-frontmatter';
8
+ * // → Record<string, { title?, order?, layout?, hero?, features?, ... }>
9
+ * // keys are route-prefixed paths, e.g. '/docs/guide/index.md'
10
+ *
11
+ * HMR: when a .md file changes its virtual module is invalidated so the dev
12
+ * server reloads navigation/layout info without a full page reload.
13
+ */
14
+
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import yaml from 'js-yaml';
18
+
19
+ const VIRTUAL_ID = 'virtual:greg-frontmatter';
20
+ const RESOLVED_ID = '\0' + VIRTUAL_ID;
21
+
22
+ /** Extract the YAML block between the first pair of `---` lines. */
23
+ function parseFrontmatter(content) {
24
+ const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
25
+ if (!m) return {};
26
+ try {
27
+ const parsed = yaml.load(m[1]);
28
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
29
+ ? parsed
30
+ : {};
31
+ } catch {
32
+ return {};
33
+ }
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Plugin
38
+ // ---------------------------------------------------------------------------
39
+ export function vitePluginFrontmatter({ docsDir = 'docs', srcDir = '/docs' } = {}) {
40
+ let root = process.cwd();
41
+
42
+ function normalizeSrcDir(value) {
43
+ const cleaned = String(value ?? '').replace(/^\/+|\/+$/g, '');
44
+ return cleaned ? `/${cleaned}` : '/';
45
+ }
46
+
47
+ /** Collect all .md paths and return the virtual module source. */
48
+ function buildModule() {
49
+ const absDirs = (Array.isArray(docsDir) ? docsDir : [docsDir]).map(d => path.resolve(root, d));
50
+ const normalizedSrcDir = normalizeSrcDir(srcDir);
51
+ const entries = {};
52
+
53
+ function walk(dir, baseDir) {
54
+ let items;
55
+ try { items = fs.readdirSync(dir, { withFileTypes: true }); }
56
+ catch { return; }
57
+ for (const item of items) {
58
+ const full = path.join(dir, item.name);
59
+ if (item.isDirectory()) {
60
+ walk(full, baseDir);
61
+ } else if (item.isFile() && item.name.endsWith('.md') && !item.name.startsWith('__')) {
62
+ const rel = path.relative(baseDir, full).replace(/\\/g, '/');
63
+ const viteKey = normalizedSrcDir === '/'
64
+ ? `/${rel}`
65
+ : `${normalizedSrcDir}/${rel}`; // e.g. /docs/guide/index.md
66
+ try {
67
+ const content = fs.readFileSync(full, 'utf8');
68
+ const stat = fs.statSync(full);
69
+ entries[viteKey] = {
70
+ ...parseFrontmatter(content),
71
+ _mtime: stat.mtime.toISOString(),
72
+ };
73
+ } catch { /* skip unreadable files */ }
74
+ }
75
+ }
76
+ }
77
+
78
+ for (const absDocsDir of absDirs) {
79
+ walk(absDocsDir, absDocsDir);
80
+ }
81
+ return `export default ${JSON.stringify(entries)};`;
82
+ }
83
+
84
+ return {
85
+ name: 'greg:frontmatter',
86
+
87
+ configResolved(config) {
88
+ root = config.root;
89
+ },
90
+
91
+ resolveId(id) {
92
+ if (id === VIRTUAL_ID) return RESOLVED_ID;
93
+ },
94
+
95
+ load(id) {
96
+ if (id === RESOLVED_ID) return buildModule();
97
+ },
98
+
99
+ handleHotUpdate({ file, server }) {
100
+ if (file.endsWith('.md')) {
101
+ const mod = server.moduleGraph.getModuleById(RESOLVED_ID);
102
+ if (mod) {
103
+ server.moduleGraph.invalidateModule(mod);
104
+ server.hot.send({ type: 'full-reload' });
105
+ }
106
+ }
107
+ },
108
+ };
109
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * vitePluginGregConfig
3
+ *
4
+ * Reads the project-root `greg.config.js` file and exposes its default export
5
+ * as the `virtual:greg-config` virtual module.
6
+ *
7
+ * When the file is absent the module resolves to `export default {}`.
8
+ *
9
+ * Supported config keys (all optional):
10
+ * srcDir string – physical docs source directory, relative to project root (default: 'docs')
11
+ * srcExclude string[] – glob patterns to exclude from docs source (VitePress-compatible, default: [])
12
+ * docsBase string – URL prefix for the docs section, e.g. 'docs' → URLs like /docs/guide (default: 'docs')
13
+ * srcDir string – @deprecated, use docsBase instead
14
+ * version string – version badge text
15
+ * mainTitle string – site title shown in the header
16
+ * outline OutlineOption – global outline setting (VitePress-compatible)
17
+ * mermaidTheme string – default mermaid theme key
18
+ * carbonAds { code, placement } | null
19
+ * breadcrumb boolean – show breadcrumb above content
20
+ * backToTop boolean – show Back To Top button
21
+ * lastModified boolean – show last-modified date below content
22
+ * locales Record<string, LocaleConfig> – VitePress-style locale map
23
+ * sidebar 'auto' | SidebarItem[]
24
+ *
25
+ * HMR: changing greg.config.* or prv/greg.config.js triggers a full page reload.
26
+ */
27
+
28
+ import path from 'node:path';
29
+ import { fileURLToPath } from 'node:url';
30
+ import { loadGregConfig, resolveGregConfigPaths } from './loadGregConfig.js';
31
+
32
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
33
+ /** Absolute path to greg's own components directory (inside the package). */
34
+ const GREG_COMPONENTS_DIR = path.resolve(__dirname, '../components');
35
+
36
+ const VIRTUAL_ID = 'virtual:greg-config';
37
+ const RESOLVED_ID = '\0' + VIRTUAL_ID;
38
+
39
+ export function vitePluginGregConfig() {
40
+ let root = process.cwd();
41
+ let { mainConfigPath, prvConfigPath } = resolveGregConfigPaths(root);
42
+
43
+ function isWatchedConfig(file) {
44
+ return file === mainConfigPath || file === prvConfigPath;
45
+ }
46
+
47
+ return {
48
+ name: 'greg:config',
49
+
50
+ config() {
51
+ return {
52
+ resolve: {
53
+ alias: {
54
+ '$components': GREG_COMPONENTS_DIR,
55
+ },
56
+ },
57
+ optimizeDeps: {
58
+ exclude: ['@dominikcz/greg'],
59
+ include: [
60
+ '@dominikcz/greg > unified',
61
+ '@dominikcz/greg > remark-parse',
62
+ '@dominikcz/greg > remark-gfm',
63
+ '@dominikcz/greg > remark-rehype',
64
+ '@dominikcz/greg > rehype-stringify',
65
+ '@dominikcz/greg > rehype-slug',
66
+ '@dominikcz/greg > rehype-autolink-headings',
67
+ '@dominikcz/greg > unist-util-visit',
68
+ '@dominikcz/greg > shiki',
69
+ '@dominikcz/greg > fuse.js',
70
+ '@dominikcz/greg > mermaid',
71
+ ],
72
+ },
73
+ };
74
+ },
75
+
76
+ configResolved(config) {
77
+ root = config.root;
78
+ ({ mainConfigPath, prvConfigPath } = resolveGregConfigPaths(root));
79
+ },
80
+
81
+ resolveId(id) {
82
+ if (id === VIRTUAL_ID) return RESOLVED_ID;
83
+ },
84
+
85
+ async load(id) {
86
+ if (id !== RESOLVED_ID) return;
87
+ if (!mainConfigPath && !prvConfigPath) return `export default {};`;
88
+ try {
89
+ const config = await loadGregConfig(root);
90
+ return `export default ${JSON.stringify(config)};`;
91
+ } catch (e) {
92
+ console.warn('[greg] Failed to load config:', e.message);
93
+ return `export default {};`;
94
+ }
95
+ },
96
+
97
+ handleHotUpdate({ file, server }) {
98
+ ({ mainConfigPath, prvConfigPath } = resolveGregConfigPaths(root));
99
+ if (isWatchedConfig(file)) {
100
+ const mod = server.moduleGraph.getModuleById(RESOLVED_ID);
101
+ if (mod) {
102
+ server.moduleGraph.invalidateModule(mod);
103
+ server.hot.send({ type: 'full-reload' });
104
+ }
105
+ }
106
+ },
107
+ };
108
+ }
@@ -0,0 +1,57 @@
1
+ import { resolve } from 'node:path';
2
+ import { buildSearchIndex, invalidateSearchIndexCache } from './searchIndexBuilder.js';
3
+
4
+ /**
5
+ * Vite plugin: serves /search-index.json in dev and emits it as a build asset.
6
+ *
7
+ * The build is shared with vitePluginSearchServer via the module-level cache
8
+ * in searchIndexBuilder.js — both plugins pay the I/O cost only once.
9
+ *
10
+ * @param {object} options
11
+ * @param {string} [options.docsDir='docs'] - directory (relative to project root)
12
+ * @param {string} [options.srcDir='/docs'] - SPA route prefix
13
+ */
14
+ export function vitePluginSearchIndex({ docsDir = 'docs', srcDir = '/docs' } = {}) {
15
+ let resolvedDocsDirs;
16
+
17
+ return {
18
+ name: 'vite-plugin-search-index',
19
+
20
+ configResolved(config) {
21
+ const dirs = Array.isArray(docsDir) ? docsDir : [docsDir];
22
+ resolvedDocsDirs = dirs.map(d => resolve(config.root, d));
23
+ },
24
+
25
+ // Dev-server: answer GET /search-index.json with the cached index
26
+ configureServer(server) {
27
+ // Invalidate cache when any markdown file changes
28
+ server.watcher.on('change', f => { if (f.endsWith('.md')) invalidateSearchIndexCache(); });
29
+ server.watcher.on('add', f => { if (f.endsWith('.md')) invalidateSearchIndexCache(); });
30
+ server.watcher.on('unlink', f => { if (f.endsWith('.md')) invalidateSearchIndexCache(); });
31
+
32
+ server.middlewares.use(async (req, res, next) => {
33
+ if (req.url !== '/search-index.json' || req.method !== 'GET') return next();
34
+ try {
35
+ const index = await buildSearchIndex(resolvedDocsDirs, srcDir);
36
+ res.writeHead(200, {
37
+ 'Content-Type': 'application/json; charset=utf-8',
38
+ 'Cache-Control': 'no-cache',
39
+ });
40
+ res.end(JSON.stringify(index));
41
+ } catch (err) {
42
+ next(err);
43
+ }
44
+ });
45
+ },
46
+
47
+ // Production build: emit search-index.json as a static asset
48
+ async generateBundle() {
49
+ const index = await buildSearchIndex(resolvedDocsDirs, srcDir);
50
+ this.emitFile({
51
+ type: 'asset',
52
+ fileName: 'search-index.json',
53
+ source: JSON.stringify(index),
54
+ });
55
+ },
56
+ };
57
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * vitePluginSearchServer
3
+ *
4
+ * Vite plugin that exposes a GET /api/search?q=&limit= search endpoint
5
+ * in both dev (`configureServer`) and preview (`configurePreviewServer`) modes.
6
+ *
7
+ * The search index is built on first request and cached in memory via the
8
+ * shared `buildSearchIndex` from searchIndexBuilder.js.
9
+ * Both this plugin and vitePluginSearchIndex share the same cache, so the
10
+ * docs are only scanned once per process start (or after a file change).
11
+ *
12
+ * For production deployments, use the standalone `searchServer.js` instead
13
+ * (invoked via `greg search-server`).
14
+ *
15
+ * @param {object} options
16
+ * @param {string} [options.docsDir='docs'] - docs directory (relative to project root)
17
+ * @param {string} [options.srcDir='/docs'] - SPA route prefix
18
+ * @param {string} [options.searchUrl='/api/search'] - URL path for the search endpoint
19
+ */
20
+
21
+ import path from 'node:path';
22
+ import Fuse from 'fuse.js';
23
+ import { buildSearchIndex, buildFuseResult, invalidateSearchIndexCache } from './searchIndexBuilder.js';
24
+
25
+ const FUSE_OPTIONS = {
26
+ includeScore: true,
27
+ includeMatches: true,
28
+ threshold: 0.4,
29
+ ignoreLocation: true,
30
+ minMatchCharLength: 2,
31
+ keys: [
32
+ { name: 'title', weight: 3 },
33
+ { name: 'sections.heading', weight: 2 },
34
+ { name: 'sections.content', weight: 1 },
35
+ ],
36
+ };
37
+
38
+ export function vitePluginSearchServer({
39
+ docsDir = 'docs',
40
+ srcDir = '/docs',
41
+ searchUrl = '/api/search',
42
+ fuzzy = {},
43
+ } = {}) {
44
+ let resolvedDocsDir;
45
+ let viteBase = '/';
46
+ const fuzzyConfig = /** @type {{ threshold?: number; minMatchCharLength?: number; ignoreLocation?: boolean }} */ (fuzzy);
47
+ const threshold = Number.isFinite(Number(fuzzyConfig.threshold))
48
+ ? Number(fuzzyConfig.threshold)
49
+ : 0.35;
50
+ const minMatchCharLength = Number.isFinite(Number(fuzzyConfig.minMatchCharLength))
51
+ ? Math.max(1, Number(fuzzyConfig.minMatchCharLength))
52
+ : 3;
53
+ const ignoreLocation = fuzzyConfig.ignoreLocation !== false;
54
+ const fuseOptions = {
55
+ includeScore: true,
56
+ includeMatches: true,
57
+ threshold,
58
+ ignoreLocation,
59
+ minMatchCharLength,
60
+ keys: FUSE_OPTIONS.keys,
61
+ };
62
+
63
+ // Fuse instance, lazily built and cached between requests.
64
+ // Cleared whenever a .md file changes.
65
+ /** @type {Fuse<any> | null} */
66
+ let fuseCache = null;
67
+ /** @type {Promise<Fuse<any>> | null} */
68
+ let buildPromise = null;
69
+
70
+ async function loadFuse() {
71
+ if (fuseCache) return fuseCache;
72
+ if (!buildPromise) {
73
+ buildPromise = buildSearchIndex(resolvedDocsDir, srcDir).then(index => {
74
+ fuseCache = new Fuse(index, fuseOptions);
75
+ return fuseCache;
76
+ }).catch(err => {
77
+ buildPromise = null; // allow retry
78
+ throw err;
79
+ });
80
+ }
81
+ return buildPromise;
82
+ }
83
+
84
+ function invalidate() {
85
+ fuseCache = null;
86
+ buildPromise = null;
87
+ invalidateSearchIndexCache();
88
+ }
89
+
90
+ function normalizePath(value) {
91
+ const raw = String(value ?? '').trim();
92
+ if (!raw || raw === '/') return '/';
93
+ return '/' + raw.replace(/^\/+|\/+$/g, '');
94
+ }
95
+
96
+ function isPathInLocale(id, localeRoot, baseRoot, localeRoots) {
97
+ const currentRoot = normalizePath(localeRoot);
98
+ const normalizedBase = normalizePath(baseRoot);
99
+ const normalizedId = normalizePath(id);
100
+ const roots = (localeRoots ?? []).map(normalizePath);
101
+
102
+ const inCurrentRoot = currentRoot === '/'
103
+ ? normalizedId.startsWith('/')
104
+ : (normalizedId === currentRoot || normalizedId.startsWith(currentRoot + '/'));
105
+
106
+ if (!inCurrentRoot) {
107
+ return false;
108
+ }
109
+
110
+ if (currentRoot === normalizedBase) {
111
+ const otherRoots = roots.filter((rp) => rp !== currentRoot);
112
+ if (otherRoots.some((rp) => normalizedId === rp || normalizedId.startsWith(rp + '/'))) {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ return true;
118
+ }
119
+
120
+ /** Connect-compatible middleware factory */
121
+ function middleware() {
122
+ return async (req, res, next) => {
123
+ const urlStr = req.url ?? '';
124
+ const qIdx = urlStr.indexOf('?');
125
+ const rawPathname = qIdx >= 0 ? urlStr.slice(0, qIdx) : urlStr;
126
+ // Strip the Vite base prefix — configureServer runs before Vite's
127
+ // own base-stripping middleware, so req.url still has the full prefix.
128
+ const base = viteBase === '/' ? '' : viteBase.replace(/\/$/, '');
129
+ const pathname = (base && rawPathname.startsWith(base))
130
+ ? '/' + rawPathname.slice(base.length).replace(/^\/+/, '')
131
+ : rawPathname;
132
+ if (pathname !== searchUrl || req.method !== 'GET') return next();
133
+
134
+ const params = new URLSearchParams(qIdx >= 0 ? urlStr.slice(qIdx + 1) : '');
135
+ const q = (params.get('q') ?? '').trim();
136
+ // Cap limit to protect server from expensive searches
137
+ const limit = Math.min(Math.max(parseInt(params.get('limit') ?? '10', 10) || 10, 1), 50);
138
+ const localeRoot = params.get('localeRoot');
139
+ const baseRoot = params.get('baseRoot');
140
+ const localeRoots = (params.get('localeRoots') ?? '')
141
+ .split(',')
142
+ .map(v => v.trim())
143
+ .filter(Boolean);
144
+
145
+ try {
146
+ const fuse = await loadFuse();
147
+ let results = q ? fuse.search(q, { limit }).map(buildFuseResult) : [];
148
+ if (localeRoot && baseRoot) {
149
+ results = results.filter((result) =>
150
+ isPathInLocale(result.id, localeRoot, baseRoot, localeRoots),
151
+ );
152
+ }
153
+ const body = JSON.stringify({ query: q, results });
154
+ res.writeHead(200, {
155
+ 'Content-Type': 'application/json; charset=utf-8',
156
+ 'Cache-Control': 'no-cache',
157
+ 'Content-Length': Buffer.byteLength(body),
158
+ });
159
+ res.end(body);
160
+ } catch (err) {
161
+ console.error('[greg:search-server]', err.message);
162
+ const body = JSON.stringify({ query: q, results: [] });
163
+ res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
164
+ res.end(body);
165
+ }
166
+ };
167
+ }
168
+
169
+ return {
170
+ name: 'greg:search-server',
171
+
172
+ configResolved(config) {
173
+ const dirs = Array.isArray(docsDir) ? docsDir : [docsDir];
174
+ resolvedDocsDir = dirs.map(d => path.resolve(config.root, d));
175
+ viteBase = config.base ?? '/';
176
+ },
177
+
178
+ configureServer(server) {
179
+ server.middlewares.use(middleware());
180
+ // Invalidate Fuse cache on any markdown file change so search stays fresh
181
+ server.watcher.on('change', f => { if (f.endsWith('.md')) invalidate(); });
182
+ server.watcher.on('add', f => { if (f.endsWith('.md')) invalidate(); });
183
+ server.watcher.on('unlink', f => { if (f.endsWith('.md')) invalidate(); });
184
+ },
185
+
186
+ configurePreviewServer(server) {
187
+ server.middlewares.use(middleware());
188
+ },
189
+ };
190
+ }
@@ -0,0 +1,59 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+
4
+ type Props = {
5
+ text?: string;
6
+ type?: "info" | "tip" | "warning" | "danger";
7
+ children?: Snippet;
8
+ };
9
+
10
+ let { text, type = "tip", children }: Props = $props();
11
+ </script>
12
+
13
+ <span class="badge {type}">
14
+ {#if children}
15
+ {@render children()}
16
+ {:else}
17
+ {text}
18
+ {/if}
19
+ </span>
20
+
21
+ <style>
22
+ .badge {
23
+ display: inline-block;
24
+ margin-left: 2px;
25
+ border: 1px solid transparent;
26
+ border-radius: 12px;
27
+ padding: 0 10px;
28
+ line-height: 22px;
29
+ font-size: 12px;
30
+ font-weight: 500;
31
+ white-space: nowrap;
32
+ transform: translateY(-2px);
33
+ vertical-align: middle;
34
+ }
35
+
36
+ .badge.info {
37
+ border-color: var(--greg-info-border);
38
+ color: var(--greg-info-text);
39
+ background-color: var(--greg-info-bg);
40
+ }
41
+
42
+ .badge.tip {
43
+ border-color: var(--greg-tip-border);
44
+ color: var(--greg-tip-text);
45
+ background-color: var(--greg-tip-bg);
46
+ }
47
+
48
+ .badge.warning {
49
+ border-color: var(--greg-warning-border);
50
+ color: var(--greg-warning-text);
51
+ background-color: var(--greg-warning-bg);
52
+ }
53
+
54
+ .badge.danger {
55
+ border-color: var(--greg-danger-border);
56
+ color: var(--greg-danger-text);
57
+ background-color: var(--greg-danger-bg);
58
+ }
59
+ </style>