@dominikcz/greg 0.9.28 → 0.9.31
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/bin/init.js +1 -1
- package/bin/templates/greg.config.js +1 -1
- package/bin/templates/greg.config.ts +1 -1
- package/bin/templates/vite.config.js +1 -1
- package/docs/guide/asset-handling.md +15 -0
- package/docs/guide/markdown/links-and-toc.md +16 -0
- package/package.json +1 -1
- package/src/lib/MarkdownDocs/MarkdownDocs.svelte +4 -0
- package/src/lib/MarkdownDocs/MarkdownRenderer.svelte +21 -1
- package/src/lib/MarkdownDocs/__tests__/helpers.js +3 -0
- package/src/lib/MarkdownDocs/__tests__/markdown.test.js +51 -0
- package/src/lib/MarkdownDocs/markdownRendererRuntime.ts +12 -0
- package/src/lib/MarkdownDocs/remarkFlexRow.js +86 -0
- package/src/lib/MarkdownDocs/vitePluginCopyDocs.js +44 -21
- package/src/lib/MarkdownDocs/vitePluginFrontmatter.js +11 -1
- package/src/lib/components/MarkdownImagePreview.svelte +345 -0
- package/src/lib/scss/__markdown.scss +21 -0
- package/svelte.config.js +2 -0
- package/types/index.d.ts +2 -0
package/bin/init.js
CHANGED
|
@@ -186,7 +186,7 @@ async function main() {
|
|
|
186
186
|
|
|
187
187
|
// ── Derive values ─────────────────────────────────────────────────────────
|
|
188
188
|
const docsDir = docsPath.replace(/^\.\//, '').replace(/\/$/, '');
|
|
189
|
-
const rootPath = '
|
|
189
|
+
const rootPath = '';
|
|
190
190
|
const ext = useTS ? 'ts' : 'js';
|
|
191
191
|
const vars = { TITLE: title, DESCRIPTION: desc, DOCS_DIR: docsDir, ROOT_PATH: rootPath, EXT: ext };
|
|
192
192
|
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from '@dominikcz/greg/plugins'
|
|
10
10
|
|
|
11
11
|
const docsDir = process.env.GREG_DOCS_DIR || '{{DOCS_DIR}}'
|
|
12
|
-
const docsBase = process.env.GREG_DOCS_BASE || '
|
|
12
|
+
const docsBase = process.env.GREG_DOCS_BASE || ''
|
|
13
13
|
|
|
14
14
|
export default defineConfig({
|
|
15
15
|
plugins: [
|
|
@@ -18,6 +18,21 @@ Relative paths are resolved from the location of the `.md` file:
|
|
|
18
18
|

|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
By default, Markdown images are rendered as thumbnails. Click an image to open
|
|
22
|
+
the full-size preview.
|
|
23
|
+
|
|
24
|
+
When preview is enabled, a caption is rendered under the thumbnail:
|
|
25
|
+
- uses `title` when present,
|
|
26
|
+
- otherwise falls back to `alt` text.
|
|
27
|
+
|
|
28
|
+
You can disable this behavior globally in `greg.config.js`:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
export default {
|
|
32
|
+
markdownImagePreview: false,
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
21
36
|
Absolute paths are resolved from the project root's `public/` directory:
|
|
22
37
|
|
|
23
38
|
```md
|
|
@@ -62,3 +62,19 @@ Output:
|
|
|
62
62
|
By default h2 and h3 headings are included. The right-side **Outline** panel is
|
|
63
63
|
a persistent alternative. See `outline` prop in the
|
|
64
64
|
[`<MarkdownDocs>` reference](/reference/markdowndocs).
|
|
65
|
+
|
|
66
|
+
## Flex row list - `[[ ... ]]`
|
|
67
|
+
|
|
68
|
+
Use `[[ ... ]]` to render multiple markdown blocks in a horizontal wrapping row.
|
|
69
|
+
Each non-empty line inside the block becomes one flex item.
|
|
70
|
+
|
|
71
|
+
```md
|
|
72
|
+
[[
|
|
73
|
+

|
|
74
|
+

|
|
75
|
+

|
|
76
|
+
]]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Use line breaks (Enter) to separate items. Brace syntax (`{...}`) and one-line
|
|
80
|
+
comma-only syntax (for example `[[One, Two]]`) are not supported.
|
package/package.json
CHANGED
|
@@ -164,6 +164,8 @@
|
|
|
164
164
|
* { id, title, titleHtml, sectionTitle, sectionTitleHtml?, sectionAnchor, excerptHtml, score }
|
|
165
165
|
*/
|
|
166
166
|
searchProvider?: (query: string, limit?: number) => Promise<any[]>;
|
|
167
|
+
/** Render markdown images as thumbnails with click-to-preview overlay. */
|
|
168
|
+
markdownImagePreview?: boolean;
|
|
167
169
|
};
|
|
168
170
|
|
|
169
171
|
type VersionManifestEntry = {
|
|
@@ -209,6 +211,7 @@
|
|
|
209
211
|
(gregConfig as any).outline ?? ([2, 3] as [number, number]),
|
|
210
212
|
mermaidTheme = (gregConfig as any).mermaidTheme,
|
|
211
213
|
mermaidThemes,
|
|
214
|
+
markdownImagePreview = (gregConfig as any).markdownImagePreview ?? true,
|
|
212
215
|
breadcrumb = (gregConfig as any).breadcrumb ?? false,
|
|
213
216
|
backToTop = (gregConfig as any).backToTop ?? false,
|
|
214
217
|
lastModified: globalLastModified = (gregConfig as any).lastModified ?? false,
|
|
@@ -1733,6 +1736,7 @@
|
|
|
1733
1736
|
docsPrefix={withBase(currentSrcDir)}
|
|
1734
1737
|
{mermaidTheme}
|
|
1735
1738
|
{mermaidThemes}
|
|
1739
|
+
enableImagePreview={markdownImagePreview}
|
|
1736
1740
|
colorTheme={theme}
|
|
1737
1741
|
/>
|
|
1738
1742
|
{#if lastModified && activeFrontmatter?._mtime}
|
|
@@ -203,6 +203,8 @@
|
|
|
203
203
|
* When `'dark'`, automatically selects `mermaidTheme + '-dark'` if available.
|
|
204
204
|
*/
|
|
205
205
|
colorTheme?: "light" | "dark";
|
|
206
|
+
/** Enable markdown image thumbnails + click-to-preview overlay. */
|
|
207
|
+
enableImagePreview?: boolean;
|
|
206
208
|
};
|
|
207
209
|
let {
|
|
208
210
|
markdown,
|
|
@@ -211,6 +213,7 @@
|
|
|
211
213
|
mermaidTheme = DEFAULT_MERMAID_THEME,
|
|
212
214
|
mermaidThemes: extraThemes = {},
|
|
213
215
|
colorTheme,
|
|
216
|
+
enableImagePreview = true,
|
|
214
217
|
}: Props = $props();
|
|
215
218
|
|
|
216
219
|
/** Combined theme map: built-ins overridden/extended by user-supplied themes. */
|
|
@@ -288,6 +291,19 @@
|
|
|
288
291
|
};
|
|
289
292
|
}
|
|
290
293
|
|
|
294
|
+
// ── Rehype plugin: markdown image component hydration ──────────────────────
|
|
295
|
+
// Replaces plain markdown <img> with <markdownimage> so runtime hydration can
|
|
296
|
+
// mount a Svelte component (thumbnail + modal + caption logic).
|
|
297
|
+
function rehypeMarkdownImageThumbs() {
|
|
298
|
+
return (tree: any) => {
|
|
299
|
+
if (!enableImagePreview) return;
|
|
300
|
+
visit(tree, "element", (node: any) => {
|
|
301
|
+
if (node.tagName !== "img") return;
|
|
302
|
+
node.tagName = "markdownimage";
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
291
307
|
// ── Rehype plugin: shiki code blocks ─────────────────────────────────────────
|
|
292
308
|
|
|
293
309
|
function rehypeShiki() {
|
|
@@ -421,6 +437,7 @@
|
|
|
421
437
|
getRehypePluginEntries({
|
|
422
438
|
rehypeStepsWrapper,
|
|
423
439
|
rehypeMermaid,
|
|
440
|
+
rehypeMarkdownImageThumbs,
|
|
424
441
|
rehypeShiki,
|
|
425
442
|
}),
|
|
426
443
|
);
|
|
@@ -482,6 +499,9 @@
|
|
|
482
499
|
</script>
|
|
483
500
|
|
|
484
501
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
485
|
-
<div
|
|
502
|
+
<div
|
|
503
|
+
class="markdown-renderer markdown-body"
|
|
504
|
+
bind:this={containerEl}
|
|
505
|
+
>
|
|
486
506
|
{@html html}
|
|
487
507
|
</div>
|
|
@@ -11,6 +11,7 @@ import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
|
|
11
11
|
import rehypeCodeGroup from '../rehypeCodeGroup.js';
|
|
12
12
|
import rehypeCodeTitle from '../rehypeCodeTitle.js';
|
|
13
13
|
import { remarkContainers, rehypeContainers } from '../remarkContainers.js';
|
|
14
|
+
import { remarkFlexRow } from '../remarkFlexRow.js';
|
|
14
15
|
import { rehypeTocPlaceholder } from '../rehypeToc.js';
|
|
15
16
|
import { remarkCodeMeta } from '../remarkCodeMeta.js';
|
|
16
17
|
import { remarkImports } from '../remarkImports.js';
|
|
@@ -26,6 +27,7 @@ async function getProcessor(opts = {}) {
|
|
|
26
27
|
if (opts.containers || opts.toc || opts.imports) {
|
|
27
28
|
return unified()
|
|
28
29
|
.use(remarkParse)
|
|
30
|
+
.use(remarkFlexRow)
|
|
29
31
|
.use(remarkImports, opts.imports ?? defaultImportsOptions)
|
|
30
32
|
.use(remarkCodeMeta)
|
|
31
33
|
.use(remarkContainers, opts.containers ?? {})
|
|
@@ -41,6 +43,7 @@ async function getProcessor(opts = {}) {
|
|
|
41
43
|
if (!_processor) {
|
|
42
44
|
_processor = unified()
|
|
43
45
|
.use(remarkParse)
|
|
46
|
+
.use(remarkFlexRow)
|
|
44
47
|
.use(remarkImports, defaultImportsOptions)
|
|
45
48
|
.use(remarkCodeMeta)
|
|
46
49
|
.use(remarkContainers)
|
|
@@ -147,6 +147,57 @@ describe('rehypeTocPlaceholder — [[TOC]]', () => {
|
|
|
147
147
|
});
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
+
describe('remarkFlexRow — [[ line\nline ]]', () => {
|
|
151
|
+
it('renders top-level flex container with wrapped items', async () => {
|
|
152
|
+
const html = await processMarkdown('[[\nOne\nTwo\nThree\n]]');
|
|
153
|
+
expect(html).toContain('class="markdown-flex-row"');
|
|
154
|
+
expect((html.match(/class="markdown-flex-item"/g) ?? []).length).toBe(3);
|
|
155
|
+
expect(html).toContain('>One<');
|
|
156
|
+
expect(html).toContain('>Two<');
|
|
157
|
+
expect(html).toContain('>Three<');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('supports markdown content inside line items', async () => {
|
|
161
|
+
const html = await processMarkdown('[[\n**Bold**\n[Link](https://example.com)\n]]');
|
|
162
|
+
expect(html).toContain('<strong>Bold</strong>');
|
|
163
|
+
expect(html).toContain('<a href="https://example.com">Link</a>');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('accepts newline-delimited items without commas', async () => {
|
|
167
|
+
const html = await processMarkdown('[[\nOne\nTwo\n]]');
|
|
168
|
+
expect(html).toContain('class="markdown-flex-row"');
|
|
169
|
+
expect((html.match(/class="markdown-flex-item"/g) ?? []).length).toBe(2);
|
|
170
|
+
expect(html).toContain('>One<');
|
|
171
|
+
expect(html).toContain('>Two<');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('works inside list items', async () => {
|
|
175
|
+
const html = await processMarkdown('- [[\n A\n B\n ]]');
|
|
176
|
+
expect(html).toContain('class="markdown-flex-row"');
|
|
177
|
+
expect((html.match(/class="markdown-flex-item"/g) ?? []).length).toBe(2);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('supports image markdown inside items', async () => {
|
|
181
|
+
const html = await processMarkdown('[[\n\n\n]]');
|
|
182
|
+
expect(html).toContain('src="/one.png"');
|
|
183
|
+
expect(html).toContain('src="/two.png"');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('supports multiline syntax with optional trailing commas', async () => {
|
|
187
|
+
const html = await processMarkdown('[[\n,\n\n]]');
|
|
188
|
+
expect(html).toContain('class="markdown-flex-row"');
|
|
189
|
+
expect((html.match(/class="markdown-flex-item"/g) ?? []).length).toBe(2);
|
|
190
|
+
expect(html).toContain('src="/one.png"');
|
|
191
|
+
expect(html).toContain('src="/two.png"');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('does not parse one-line comma-separated syntax', async () => {
|
|
195
|
+
const html = await processMarkdown('[[One, Two]]');
|
|
196
|
+
expect(html).not.toContain('class="markdown-flex-row"');
|
|
197
|
+
expect(html).toContain('[[One, Two]]');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
150
201
|
// ─── Custom Containers ─────────────────────────────────────────────────────────
|
|
151
202
|
|
|
152
203
|
describe('remarkContainers — ::: info', () => {
|
|
@@ -12,11 +12,13 @@ import rehypeCodeTitle from "./rehypeCodeTitle.js";
|
|
|
12
12
|
import { remarkCodeMeta } from "./remarkCodeMeta.js";
|
|
13
13
|
import { remarkCustomAnchors } from "./remarkCustomAnchors.js";
|
|
14
14
|
import { remarkInlineAttrs } from "./remarkInlineAttrs.js";
|
|
15
|
+
import { remarkFlexRow } from "./remarkFlexRow.js";
|
|
15
16
|
import { remarkImportsBrowser } from "./remarkImportsBrowser.js";
|
|
16
17
|
|
|
17
18
|
import Badge from "../components/Badge.svelte";
|
|
18
19
|
import Button from "../components/Button.svelte";
|
|
19
20
|
import Image from "../components/Image.svelte";
|
|
21
|
+
import MarkdownImagePreview from "../components/MarkdownImagePreview.svelte";
|
|
20
22
|
import Link from "../components/Link.svelte";
|
|
21
23
|
import CodeGroup from "../components/CodeGroup.svelte";
|
|
22
24
|
import Hero from "../components/Hero.svelte";
|
|
@@ -170,6 +172,10 @@ export const COMPONENT_REGISTRY: Record<string, ComponentHydrationEntry> = {
|
|
|
170
172
|
component: Image,
|
|
171
173
|
buildProps: buildPropsFromAttributes,
|
|
172
174
|
},
|
|
175
|
+
markdownimage: {
|
|
176
|
+
component: MarkdownImagePreview,
|
|
177
|
+
buildProps: buildPropsFromAttributes,
|
|
178
|
+
},
|
|
173
179
|
link: {
|
|
174
180
|
component: Link,
|
|
175
181
|
buildProps: buildPropsFromAttributes,
|
|
@@ -252,6 +258,7 @@ export function getRemarkPluginEntries(
|
|
|
252
258
|
return [
|
|
253
259
|
{ name: "remark-parse", plugin: remarkParse },
|
|
254
260
|
{ name: "remark-gfm", plugin: remarkGfm },
|
|
261
|
+
{ name: "remark-flex-row", plugin: remarkFlexRow },
|
|
255
262
|
{ name: "remark-inline-attrs", plugin: remarkInlineAttrs },
|
|
256
263
|
{
|
|
257
264
|
name: "remark-imports-browser",
|
|
@@ -278,6 +285,7 @@ export function getRemarkPluginEntries(
|
|
|
278
285
|
export function getRehypePluginEntries(deps: {
|
|
279
286
|
rehypeStepsWrapper: any;
|
|
280
287
|
rehypeMermaid: any;
|
|
288
|
+
rehypeMarkdownImageThumbs: any;
|
|
281
289
|
rehypeShiki: any;
|
|
282
290
|
}): PluginEntry[] {
|
|
283
291
|
return [
|
|
@@ -289,6 +297,10 @@ export function getRehypePluginEntries(deps: {
|
|
|
289
297
|
},
|
|
290
298
|
{ name: "rehype-steps-wrapper", plugin: deps.rehypeStepsWrapper },
|
|
291
299
|
{ name: "rehype-mermaid", plugin: deps.rehypeMermaid },
|
|
300
|
+
{
|
|
301
|
+
name: "rehype-markdown-image-thumbs",
|
|
302
|
+
plugin: deps.rehypeMarkdownImageThumbs,
|
|
303
|
+
},
|
|
292
304
|
{ name: "rehype-shiki", plugin: deps.rehypeShiki },
|
|
293
305
|
{ name: "rehype-containers", plugin: rehypeContainers },
|
|
294
306
|
{ name: "rehype-code-group", plugin: rehypeCodeGroup },
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { unified } from 'unified';
|
|
2
|
+
import remarkParse from 'remark-parse';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import { visit } from 'unist-util-visit';
|
|
5
|
+
|
|
6
|
+
function parseFlexPattern(value) {
|
|
7
|
+
const raw = String(value ?? '').trim();
|
|
8
|
+
if (!raw.startsWith('[[') || !raw.endsWith(']]')) return null;
|
|
9
|
+
|
|
10
|
+
const inner = raw.slice(2, -2).trim();
|
|
11
|
+
if (!inner) return [];
|
|
12
|
+
if (/^toc$/i.test(inner)) return null;
|
|
13
|
+
|
|
14
|
+
return parseLineItems(inner);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseLineItems(inner) {
|
|
18
|
+
if (!inner.includes('\n')) return null;
|
|
19
|
+
|
|
20
|
+
const parts = inner
|
|
21
|
+
.split('\n')
|
|
22
|
+
.map((line) => line.trim())
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.map((line) => line.replace(/,$/, '').trim())
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
|
|
27
|
+
return parts.length ? parts : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseMarkdownFragment(fragment, parser) {
|
|
31
|
+
const tree = parser.parse(fragment);
|
|
32
|
+
return Array.isArray(tree?.children) ? tree.children : [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createFlexItemNode(children) {
|
|
36
|
+
return {
|
|
37
|
+
type: 'flexRowItem',
|
|
38
|
+
data: {
|
|
39
|
+
hName: 'div',
|
|
40
|
+
hProperties: { className: ['markdown-flex-item'] },
|
|
41
|
+
},
|
|
42
|
+
children,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createFlexRowNode(items, parser) {
|
|
47
|
+
return {
|
|
48
|
+
type: 'flexRowBlock',
|
|
49
|
+
data: {
|
|
50
|
+
hName: 'div',
|
|
51
|
+
hProperties: { className: ['markdown-flex-row'] },
|
|
52
|
+
},
|
|
53
|
+
children: items.map((item) =>
|
|
54
|
+
createFlexItemNode(parseMarkdownFragment(item, parser)),
|
|
55
|
+
),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function remarkFlexRow() {
|
|
60
|
+
const parser = unified().use(remarkParse).use(remarkGfm);
|
|
61
|
+
|
|
62
|
+
return (tree, file) => {
|
|
63
|
+
const source = typeof file?.value === 'string' ? file.value : '';
|
|
64
|
+
|
|
65
|
+
visit(tree, 'paragraph', (node, index, parent) => {
|
|
66
|
+
if (!parent || index == null) return;
|
|
67
|
+
|
|
68
|
+
let raw = '';
|
|
69
|
+
const start = node?.position?.start?.offset;
|
|
70
|
+
const end = node?.position?.end?.offset;
|
|
71
|
+
|
|
72
|
+
if (typeof start === 'number' && typeof end === 'number' && end >= start) {
|
|
73
|
+
raw = source.slice(start, end);
|
|
74
|
+
} else {
|
|
75
|
+
raw = (node.children ?? [])
|
|
76
|
+
.map((child) => child?.value ?? '')
|
|
77
|
+
.join('');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const parsedItems = parseFlexPattern(raw);
|
|
81
|
+
if (!parsedItems || parsedItems.length === 0) return;
|
|
82
|
+
|
|
83
|
+
parent.children[index] = createFlexRowNode(parsedItems, parser);
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* vitePluginCopyDocs
|
|
3
3
|
*
|
|
4
|
-
* Copies docs
|
|
4
|
+
* Copies docs/** files (and optional extra static dirs) to the build output
|
|
5
5
|
* directory as-is, so they can be fetched at runtime by the browser without
|
|
6
6
|
* being compiled by mdsvex/rollup.
|
|
7
7
|
*
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* The `staticDirs` option (default: ['snippets']) lists additional project-root
|
|
12
12
|
* directories whose files should also be served/copied verbatim. This is
|
|
13
|
-
* needed for
|
|
13
|
+
* needed for snippet include syntax that references files from static dirs.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import fs from 'node:fs';
|
|
@@ -25,11 +25,21 @@ export function vitePluginCopyDocs({ docsDir = 'docs', srcDir = '/docs', staticD
|
|
|
25
25
|
let outDir = 'dist';
|
|
26
26
|
let viteBase = '/';
|
|
27
27
|
|
|
28
|
+
function isTraversableDirectory(entry, fullPath) {
|
|
29
|
+
if (entry.isDirectory()) return true;
|
|
30
|
+
if (!entry.isSymbolicLink()) return false;
|
|
31
|
+
try {
|
|
32
|
+
return fs.statSync(fullPath).isDirectory();
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
function* walkAll(dir) {
|
|
29
39
|
if (!fs.existsSync(dir)) return;
|
|
30
40
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
31
41
|
const full = path.join(dir, entry.name);
|
|
32
|
-
if (entry
|
|
42
|
+
if (isTraversableDirectory(entry, full)) {
|
|
33
43
|
yield* walkAll(full);
|
|
34
44
|
} else if (entry.isFile()) {
|
|
35
45
|
yield full;
|
|
@@ -37,16 +47,29 @@ export function vitePluginCopyDocs({ docsDir = 'docs', srcDir = '/docs', staticD
|
|
|
37
47
|
}
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
function* walkMd(dir) {
|
|
41
|
-
for (const full of walkAll(dir)) {
|
|
42
|
-
if (full.endsWith('.md')) yield full;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
50
|
function toPosix(value) {
|
|
47
51
|
return String(value).replace(/\\/g, '/');
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
function getContentType(filePath) {
|
|
55
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
56
|
+
switch (ext) {
|
|
57
|
+
case '.md': return 'text/plain; charset=utf-8';
|
|
58
|
+
case '.txt': return 'text/plain; charset=utf-8';
|
|
59
|
+
case '.json': return 'application/json; charset=utf-8';
|
|
60
|
+
case '.js': return 'text/javascript; charset=utf-8';
|
|
61
|
+
case '.css': return 'text/css; charset=utf-8';
|
|
62
|
+
case '.svg': return 'image/svg+xml';
|
|
63
|
+
case '.png': return 'image/png';
|
|
64
|
+
case '.jpg':
|
|
65
|
+
case '.jpeg': return 'image/jpeg';
|
|
66
|
+
case '.webp': return 'image/webp';
|
|
67
|
+
case '.gif': return 'image/gif';
|
|
68
|
+
case '.pdf': return 'application/pdf';
|
|
69
|
+
default: return 'application/octet-stream';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
50
73
|
function resolveRootPrefix() {
|
|
51
74
|
const cleaned = trimSlashes(srcDir);
|
|
52
75
|
return cleaned ? '/' + cleaned : '';
|
|
@@ -62,7 +85,7 @@ export function vitePluginCopyDocs({ docsDir = 'docs', srcDir = '/docs', staticD
|
|
|
62
85
|
},
|
|
63
86
|
|
|
64
87
|
/**
|
|
65
|
-
* In dev mode: serve
|
|
88
|
+
* In dev mode: serve docs files and staticDirs files.
|
|
66
89
|
* Vite doesn't auto-serve project files outside public/ as raw assets.
|
|
67
90
|
*/
|
|
68
91
|
configureServer(server) {
|
|
@@ -86,17 +109,17 @@ export function vitePluginCopyDocs({ docsDir = 'docs', srcDir = '/docs', staticD
|
|
|
86
109
|
? '/' + rawUrl.slice(base.length).replace(/^\/+/, '')
|
|
87
110
|
: rawUrl;
|
|
88
111
|
|
|
89
|
-
// Docs markdown
|
|
90
|
-
const
|
|
91
|
-
? (url === rootPrefix || url.startsWith(rootPrefix + '/'))
|
|
92
|
-
: url.startsWith('/')
|
|
93
|
-
if (
|
|
112
|
+
// Docs files (markdown + local assets like images/pdf)
|
|
113
|
+
const isDocsPath = rootPrefix
|
|
114
|
+
? (url === rootPrefix || url.startsWith(rootPrefix + '/'))
|
|
115
|
+
: url.startsWith('/');
|
|
116
|
+
if (isDocsPath) {
|
|
94
117
|
const rel = url.slice(rootPrefix.length).replace(/^\//, '');
|
|
95
118
|
for (const dir of (Array.isArray(docsDir) ? docsDir : [docsDir])) {
|
|
96
119
|
const filePath = path.resolve(root, dir, rel);
|
|
97
|
-
if (fs.existsSync(filePath)) {
|
|
98
|
-
res.setHeader('Content-Type',
|
|
99
|
-
res.end(fs.readFileSync(filePath
|
|
120
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
121
|
+
res.setHeader('Content-Type', getContentType(filePath));
|
|
122
|
+
res.end(fs.readFileSync(filePath));
|
|
100
123
|
return;
|
|
101
124
|
}
|
|
102
125
|
}
|
|
@@ -118,15 +141,15 @@ export function vitePluginCopyDocs({ docsDir = 'docs', srcDir = '/docs', staticD
|
|
|
118
141
|
});
|
|
119
142
|
},
|
|
120
143
|
|
|
121
|
-
/** After bundle is written, copy
|
|
144
|
+
/** After bundle is written, copy docs files and staticDirs verbatim. */
|
|
122
145
|
writeBundle() {
|
|
123
146
|
let count = 0;
|
|
124
147
|
const rootPrefix = trimSlashes(resolveRootPrefix());
|
|
125
148
|
|
|
126
|
-
// Copy
|
|
149
|
+
// Copy docs files from all source dirs
|
|
127
150
|
for (const dir of (Array.isArray(docsDir) ? docsDir : [docsDir])) {
|
|
128
151
|
const docsRoot = path.resolve(root, dir);
|
|
129
|
-
for (const srcFile of
|
|
152
|
+
for (const srcFile of walkAll(docsRoot)) {
|
|
130
153
|
const rel = toPosix(path.relative(docsRoot, srcFile));
|
|
131
154
|
const destFile = path.join(outDir, rootPrefix, rel);
|
|
132
155
|
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
@@ -44,6 +44,16 @@ export function vitePluginFrontmatter({ docsDir = 'docs', srcDir = '/docs' } = {
|
|
|
44
44
|
return cleaned ? `/${cleaned}` : '/';
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function isTraversableDirectory(entry, fullPath) {
|
|
48
|
+
if (entry.isDirectory()) return true;
|
|
49
|
+
if (!entry.isSymbolicLink()) return false;
|
|
50
|
+
try {
|
|
51
|
+
return fs.statSync(fullPath).isDirectory();
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
47
57
|
/** Collect all .md paths and return the virtual module source. */
|
|
48
58
|
function buildModule() {
|
|
49
59
|
const absDirs = (Array.isArray(docsDir) ? docsDir : [docsDir]).map(d => path.resolve(root, d));
|
|
@@ -56,7 +66,7 @@ export function vitePluginFrontmatter({ docsDir = 'docs', srcDir = '/docs' } = {
|
|
|
56
66
|
catch { return; }
|
|
57
67
|
for (const item of items) {
|
|
58
68
|
const full = path.join(dir, item.name);
|
|
59
|
-
if (item
|
|
69
|
+
if (isTraversableDirectory(item, full)) {
|
|
60
70
|
walk(full, baseDir);
|
|
61
71
|
} else if (item.isFile() && item.name.endsWith('.md') && !item.name.startsWith('__')) {
|
|
62
72
|
const rel = path.relative(baseDir, full).replace(/\\/g, '/');
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
type Props = {
|
|
3
|
+
src?: string;
|
|
4
|
+
alt?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
width?: number | string;
|
|
7
|
+
height?: number | string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
let { src = "", alt = "", title = "", width, height }: Props = $props();
|
|
11
|
+
|
|
12
|
+
let isOpen = $state(false);
|
|
13
|
+
let zoom = $state(1);
|
|
14
|
+
let isDragging = $state(false);
|
|
15
|
+
let dragStartX = $state(0);
|
|
16
|
+
let dragStartY = $state(0);
|
|
17
|
+
let dragStartScrollLeft = $state(0);
|
|
18
|
+
let dragStartScrollTop = $state(0);
|
|
19
|
+
let viewportEl = $state<HTMLDivElement | null>(null);
|
|
20
|
+
|
|
21
|
+
const MIN_ZOOM = 1;
|
|
22
|
+
const MAX_ZOOM = 6;
|
|
23
|
+
const ZOOM_STEP = 0.25;
|
|
24
|
+
|
|
25
|
+
const caption = $derived((String(title || "").trim() || String(alt || "").trim()));
|
|
26
|
+
const zoomLabel = $derived(`${Math.round(zoom * 100)}%`);
|
|
27
|
+
const isPannable = $derived(zoom > 1);
|
|
28
|
+
|
|
29
|
+
const previewImageStyle = $derived.by(() => {
|
|
30
|
+
if (zoom <= 1) return undefined;
|
|
31
|
+
return `width: ${Math.round(zoom * 100)}%; max-width: none; max-height: none;`;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function clampZoom(value: number) {
|
|
35
|
+
return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, value));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function setZoom(value: number) {
|
|
39
|
+
zoom = clampZoom(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function zoomIn() {
|
|
43
|
+
setZoom(zoom + ZOOM_STEP);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function zoomOut() {
|
|
47
|
+
setZoom(zoom - ZOOM_STEP);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resetZoom() {
|
|
51
|
+
setZoom(1);
|
|
52
|
+
isDragging = false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function beginPan(event: MouseEvent) {
|
|
56
|
+
if (!isPannable || !viewportEl) return;
|
|
57
|
+
isDragging = true;
|
|
58
|
+
dragStartX = event.clientX;
|
|
59
|
+
dragStartY = event.clientY;
|
|
60
|
+
dragStartScrollLeft = viewportEl.scrollLeft;
|
|
61
|
+
dragStartScrollTop = viewportEl.scrollTop;
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function continuePan(event: MouseEvent) {
|
|
66
|
+
if (!isDragging || !viewportEl) return;
|
|
67
|
+
const deltaX = event.clientX - dragStartX;
|
|
68
|
+
const deltaY = event.clientY - dragStartY;
|
|
69
|
+
viewportEl.scrollLeft = dragStartScrollLeft - deltaX;
|
|
70
|
+
viewportEl.scrollTop = dragStartScrollTop - deltaY;
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function endPan() {
|
|
75
|
+
isDragging = false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function openPreview() {
|
|
79
|
+
if (!src) return;
|
|
80
|
+
isOpen = true;
|
|
81
|
+
resetZoom();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function closePreview() {
|
|
85
|
+
isOpen = false;
|
|
86
|
+
resetZoom();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
90
|
+
if (event.key === "Escape" && isOpen) {
|
|
91
|
+
closePreview();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function handleWheel(event: WheelEvent) {
|
|
96
|
+
if (!isOpen) return;
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
if (event.deltaY < 0) {
|
|
99
|
+
zoomIn();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
zoomOut();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
$effect(() => {
|
|
106
|
+
if (typeof window === "undefined" || !isOpen) return;
|
|
107
|
+
window.addEventListener("keydown", handleKeydown);
|
|
108
|
+
return () => {
|
|
109
|
+
window.removeEventListener("keydown", handleKeydown);
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
$effect(() => {
|
|
114
|
+
if (typeof window === "undefined" || !isDragging) return;
|
|
115
|
+
window.addEventListener("mousemove", continuePan);
|
|
116
|
+
window.addEventListener("mouseup", endPan);
|
|
117
|
+
return () => {
|
|
118
|
+
window.removeEventListener("mousemove", continuePan);
|
|
119
|
+
window.removeEventListener("mouseup", endPan);
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
{#if src}
|
|
125
|
+
<figure class="markdown-image-figure">
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
class="markdown-image-thumb"
|
|
129
|
+
onclick={openPreview}
|
|
130
|
+
aria-label={caption ? `Open image preview: ${caption}` : "Open image preview"}
|
|
131
|
+
>
|
|
132
|
+
<img src={src} alt={alt} title={title || undefined} width={width} height={height} />
|
|
133
|
+
</button>
|
|
134
|
+
{#if caption}
|
|
135
|
+
<figcaption class="markdown-image-caption">{caption}</figcaption>
|
|
136
|
+
{/if}
|
|
137
|
+
</figure>
|
|
138
|
+
{/if}
|
|
139
|
+
|
|
140
|
+
{#if isOpen}
|
|
141
|
+
<div class="markdown-image-preview" role="dialog" aria-modal="true" aria-label="Image preview">
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
class="markdown-image-preview__backdrop"
|
|
145
|
+
aria-label="Close image preview"
|
|
146
|
+
onclick={closePreview}
|
|
147
|
+
></button>
|
|
148
|
+
<div class="markdown-image-preview__content">
|
|
149
|
+
<div class="markdown-image-preview__toolbar" role="group" aria-label="Image zoom controls">
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
class="markdown-image-preview__zoom-btn"
|
|
153
|
+
aria-label="Zoom out"
|
|
154
|
+
onclick={zoomOut}
|
|
155
|
+
disabled={zoom <= MIN_ZOOM}
|
|
156
|
+
>
|
|
157
|
+
−
|
|
158
|
+
</button>
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
class="markdown-image-preview__zoom-btn markdown-image-preview__zoom-label"
|
|
162
|
+
aria-label="Reset zoom"
|
|
163
|
+
onclick={resetZoom}
|
|
164
|
+
>
|
|
165
|
+
{zoomLabel}
|
|
166
|
+
</button>
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
class="markdown-image-preview__zoom-btn"
|
|
170
|
+
aria-label="Zoom in"
|
|
171
|
+
onclick={zoomIn}
|
|
172
|
+
disabled={zoom >= MAX_ZOOM}
|
|
173
|
+
>
|
|
174
|
+
+
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
178
|
+
<div
|
|
179
|
+
class="markdown-image-preview__viewport"
|
|
180
|
+
class:is-pannable={isPannable}
|
|
181
|
+
class:is-dragging={isDragging}
|
|
182
|
+
bind:this={viewportEl}
|
|
183
|
+
onwheel={handleWheel}
|
|
184
|
+
onmousedown={beginPan}
|
|
185
|
+
>
|
|
186
|
+
<img
|
|
187
|
+
src={src}
|
|
188
|
+
alt={alt || caption || "Preview image"}
|
|
189
|
+
class="markdown-image-preview__image"
|
|
190
|
+
style={previewImageStyle}
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
class="markdown-image-preview__close"
|
|
196
|
+
aria-label="Close image preview"
|
|
197
|
+
onclick={closePreview}
|
|
198
|
+
>
|
|
199
|
+
×
|
|
200
|
+
</button>
|
|
201
|
+
{#if caption}
|
|
202
|
+
<p class="markdown-image-preview__caption">{caption}</p>
|
|
203
|
+
{/if}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
{/if}
|
|
207
|
+
|
|
208
|
+
<style>
|
|
209
|
+
.markdown-image-figure {
|
|
210
|
+
margin: 1rem 0;
|
|
211
|
+
max-width: 100%;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.markdown-image-thumb {
|
|
215
|
+
display: block;
|
|
216
|
+
padding: 0;
|
|
217
|
+
border: 0;
|
|
218
|
+
background: transparent;
|
|
219
|
+
cursor: zoom-in;
|
|
220
|
+
max-width: 100%;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.markdown-image-thumb img {
|
|
224
|
+
display: block;
|
|
225
|
+
max-width: 100%;
|
|
226
|
+
max-height: 220px;
|
|
227
|
+
width: auto;
|
|
228
|
+
border-radius: 8px;
|
|
229
|
+
border: 1px solid var(--greg-border-color);
|
|
230
|
+
transition: transform 0.14s ease, box-shadow 0.14s ease;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.markdown-image-thumb:hover img {
|
|
234
|
+
transform: translateY(-1px);
|
|
235
|
+
box-shadow: 0 8px 18px color-mix(in srgb, var(--greg-color) 16%, transparent);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.markdown-image-caption {
|
|
239
|
+
margin-top: 0.45rem;
|
|
240
|
+
font-size: 0.9rem;
|
|
241
|
+
color: var(--fgColor-muted);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.markdown-image-preview {
|
|
245
|
+
position: fixed;
|
|
246
|
+
inset: 0;
|
|
247
|
+
z-index: 1200;
|
|
248
|
+
display: grid;
|
|
249
|
+
place-items: center;
|
|
250
|
+
padding: 1.25rem;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.markdown-image-preview__backdrop {
|
|
254
|
+
position: absolute;
|
|
255
|
+
inset: 0;
|
|
256
|
+
border: none;
|
|
257
|
+
background: color-mix(in srgb, #000 74%, transparent);
|
|
258
|
+
cursor: zoom-out;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.markdown-image-preview__content {
|
|
262
|
+
position: relative;
|
|
263
|
+
max-width: min(92vw, 1500px);
|
|
264
|
+
max-height: 92vh;
|
|
265
|
+
z-index: 1;
|
|
266
|
+
display: grid;
|
|
267
|
+
gap: 0.5rem;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.markdown-image-preview__toolbar {
|
|
271
|
+
display: inline-flex;
|
|
272
|
+
gap: 0.4rem;
|
|
273
|
+
justify-self: center;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.markdown-image-preview__zoom-btn {
|
|
277
|
+
border: 1px solid rgb(255 255 255 / 0.4);
|
|
278
|
+
border-radius: 999px;
|
|
279
|
+
background: rgb(0 0 0 / 0.6);
|
|
280
|
+
color: #fff;
|
|
281
|
+
min-width: 2rem;
|
|
282
|
+
height: 2rem;
|
|
283
|
+
cursor: pointer;
|
|
284
|
+
font-size: 0.95rem;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.markdown-image-preview__zoom-btn:disabled {
|
|
288
|
+
opacity: 0.45;
|
|
289
|
+
cursor: not-allowed;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.markdown-image-preview__zoom-label {
|
|
293
|
+
padding: 0 0.7rem;
|
|
294
|
+
width: auto;
|
|
295
|
+
min-width: 4.25rem;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.markdown-image-preview__viewport {
|
|
299
|
+
overflow: auto;
|
|
300
|
+
max-width: min(92vw, 1500px);
|
|
301
|
+
max-height: 84vh;
|
|
302
|
+
padding: 0.15rem;
|
|
303
|
+
cursor: default;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.markdown-image-preview__viewport.is-pannable {
|
|
307
|
+
cursor: grab;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.markdown-image-preview__viewport.is-dragging {
|
|
311
|
+
cursor: grabbing;
|
|
312
|
+
user-select: none;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.markdown-image-preview__image {
|
|
316
|
+
display: block;
|
|
317
|
+
margin: 0 auto;
|
|
318
|
+
max-width: 100%;
|
|
319
|
+
max-height: 82vh;
|
|
320
|
+
border-radius: 0.625rem;
|
|
321
|
+
box-shadow: 0 18px 40px rgb(0 0 0 / 0.45);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.markdown-image-preview__close {
|
|
325
|
+
position: absolute;
|
|
326
|
+
top: -0.75rem;
|
|
327
|
+
right: -0.75rem;
|
|
328
|
+
width: 2rem;
|
|
329
|
+
height: 2rem;
|
|
330
|
+
border: 1px solid rgb(255 255 255 / 0.4);
|
|
331
|
+
border-radius: 999px;
|
|
332
|
+
background: rgb(0 0 0 / 0.6);
|
|
333
|
+
color: #fff;
|
|
334
|
+
line-height: 1;
|
|
335
|
+
cursor: pointer;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.markdown-image-preview__caption {
|
|
339
|
+
margin-top: 0.65rem;
|
|
340
|
+
margin-bottom: 0;
|
|
341
|
+
color: #fff;
|
|
342
|
+
text-align: center;
|
|
343
|
+
text-shadow: 0 1px 2px rgb(0 0 0 / 0.7);
|
|
344
|
+
}
|
|
345
|
+
</style>
|
|
@@ -73,6 +73,27 @@
|
|
|
73
73
|
margin: 1rem 0 0.4rem 0;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
.markdown-flex-row {
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-wrap: wrap;
|
|
79
|
+
gap: 0.75rem;
|
|
80
|
+
align-items: flex-start;
|
|
81
|
+
margin: 1rem 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.markdown-flex-item {
|
|
85
|
+
min-width: 0;
|
|
86
|
+
flex: 0 1 auto;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.markdown-flex-item > :first-child {
|
|
90
|
+
margin-top: 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.markdown-flex-item > :last-child {
|
|
94
|
+
margin-bottom: 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
76
97
|
img {
|
|
77
98
|
max-width: 100%;
|
|
78
99
|
border-style: none;
|
package/svelte.config.js
CHANGED
|
@@ -11,6 +11,7 @@ import { remarkImports } from './src/lib/MarkdownDocs/remarkImports.js';
|
|
|
11
11
|
import { remarkGlobalComponents } from './src/lib/MarkdownDocs/remarkGlobalComponents.js';
|
|
12
12
|
import { remarkCustomAnchors } from './src/lib/MarkdownDocs/remarkCustomAnchors.js';
|
|
13
13
|
import { remarkInlineAttrs } from './src/lib/MarkdownDocs/remarkInlineAttrs.js';
|
|
14
|
+
import { remarkFlexRow } from './src/lib/MarkdownDocs/remarkFlexRow.js';
|
|
14
15
|
import { remarkEscapeSvelte } from './src/lib/MarkdownDocs/remarkEscapeSvelte.js';
|
|
15
16
|
import { remarkMathToHtml } from './src/lib/MarkdownDocs/remarkMathToHtml.js';
|
|
16
17
|
import { parseCodeDirectives, decorateHighlightedCodeHtml } from './src/lib/MarkdownDocs/codeDirectives.js';
|
|
@@ -103,6 +104,7 @@ const mdsvexOptions = {
|
|
|
103
104
|
},
|
|
104
105
|
},
|
|
105
106
|
remarkPlugins: [
|
|
107
|
+
remarkFlexRow,
|
|
106
108
|
remarkInlineAttrs,
|
|
107
109
|
remarkGlobalComponents,
|
|
108
110
|
remarkCodeMeta,
|
package/types/index.d.ts
CHANGED
|
@@ -342,6 +342,8 @@ export type GregConfig = {
|
|
|
342
342
|
version?: string;
|
|
343
343
|
/** Site title shown in the header. */
|
|
344
344
|
mainTitle?: string;
|
|
345
|
+
/** Render markdown images as thumbnails with click-to-preview overlay. Default: true. */
|
|
346
|
+
markdownImagePreview?: boolean;
|
|
345
347
|
/**
|
|
346
348
|
* VitePress-compatible outline setting.
|
|
347
349
|
* false – disable outline
|