specra 0.2.51 → 0.2.55
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/config/rehype-base-path.d.ts +1 -0
- package/config/svelte-config.js +95 -6
- package/dist/components/docs/Breadcrumb.svelte +3 -2
- package/dist/components/docs/CategoryIndex.svelte +5 -4
- package/dist/components/docs/DocNavigation.svelte +3 -2
- package/dist/components/docs/Footer.svelte +10 -1
- package/dist/components/docs/Header.svelte +19 -3
- package/dist/components/docs/Header.svelte.d.ts +15 -2
- package/dist/components/docs/MdxContent.svelte +1 -8
- package/dist/components/docs/MdxContent.svelte.d.ts +10 -0
- package/dist/components/docs/NotFoundContent.svelte +11 -2
- package/dist/components/docs/NotFoundContent.svelte.d.ts +1 -0
- package/dist/components/docs/ProductSwitcher.svelte +3 -2
- package/dist/components/docs/SidebarMenuItems.svelte +7 -10
- package/dist/components/docs/TabGroups.svelte +3 -2
- package/dist/mdx-components.d.ts +22 -0
- package/dist/mdx-security.js +12 -11
- package/dist/mdx.js +42 -3
- package/dist/rehype-base-path.d.ts +15 -0
- package/dist/rehype-base-path.js +32 -0
- package/dist/sidebar-utils.js +3 -10
- package/dist/styles/globals.css +19 -8
- package/dist/utils.d.ts +6 -8
- package/dist/utils.js +8 -15
- package/package.json +4 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function rehypeBasePath(options?: {}): (tree: any) => void;
|
package/config/svelte-config.js
CHANGED
|
@@ -19,6 +19,7 @@ import rehypeKatex from 'rehype-katex'
|
|
|
19
19
|
import rehypeRaw from 'rehype-raw'
|
|
20
20
|
import fs from 'fs'
|
|
21
21
|
import path from 'path'
|
|
22
|
+
import { rehypeBasePath } from '../dist/rehype-base-path.js'
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Get mdsvex preprocessor config with all Specra remark/rehype plugins
|
|
@@ -45,8 +46,59 @@ export function specraMdsvexConfig(options = {}) {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
49
|
+
* Read the deployment basePath from specra.config.json if present.
|
|
50
|
+
* Falls back to the BASE_PATH environment variable, then empty string.
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* Normalize a base path: ensure leading slash, strip trailing slash.
|
|
54
|
+
* "task_flow" → "/task_flow", "/task_flow/" → "/task_flow", "" → ""
|
|
55
|
+
*/
|
|
56
|
+
function normalizeBasePath(bp) {
|
|
57
|
+
if (!bp) return ''
|
|
58
|
+
let normalized = bp.startsWith('/') ? bp : `/${bp}`
|
|
59
|
+
return normalized.replace(/\/+$/, '')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveBasePath(configPath = path.join(process.cwd(), 'specra.config.json')) {
|
|
63
|
+
// Environment variable takes priority
|
|
64
|
+
if (process.env.BASE_PATH) return normalizeBasePath(process.env.BASE_PATH)
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
if (fs.existsSync(configPath)) {
|
|
68
|
+
const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'))
|
|
69
|
+
if (raw.deployment?.basePath) {
|
|
70
|
+
if (raw.deployment?.customDomain) return ''
|
|
71
|
+
return normalizeBasePath(raw.deployment.basePath)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Ignore parse errors
|
|
76
|
+
}
|
|
77
|
+
return ''
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Scan the docs/ directory and return prerender entries for every
|
|
82
|
+
* version-root page across all products and the default (no-product)
|
|
83
|
+
* layout. SvelteKit's prerender crawler picks up child pages by
|
|
84
|
+
* following links, but the version-root pages themselves are leaves
|
|
85
|
+
* with no inbound link from a prerendered ancestor (the product
|
|
86
|
+
* dropdown switches between them client-side), so adapter-static
|
|
87
|
+
* needs them seeded explicitly. Without these entries, navigating
|
|
88
|
+
* from one product to another in a static build fetches a
|
|
89
|
+
* non-existent `__data.json` and falls back through to the SPA
|
|
90
|
+
* `index.html` — the client then JSON.parse's HTML and throws
|
|
91
|
+
* `Unexpected token '<', "<!doctype html>"`.
|
|
92
|
+
*
|
|
93
|
+
* Layouts we handle:
|
|
94
|
+
* docs/v1/... → emit `/docs/v1`
|
|
95
|
+
* docs/v2/... → emit `/docs/v2`
|
|
96
|
+
* docs/sdk/_product_.json → recurse: docs/sdk/v1 → emit `/docs/sdk/v1`
|
|
97
|
+
* docs/api/_product_.json → recurse: docs/api/v1 → emit `/docs/api/v1`
|
|
98
|
+
*
|
|
99
|
+
* `_product_.json` is the marker file Specra uses to identify a
|
|
100
|
+
* multi-product layout (the same marker the SDK's product loader
|
|
101
|
+
* reads).
|
|
50
102
|
*/
|
|
51
103
|
function discoverVersionEntries(docsDir = path.join(process.cwd(), 'docs')) {
|
|
52
104
|
const entries = ['/']
|
|
@@ -55,31 +107,68 @@ function discoverVersionEntries(docsDir = path.join(process.cwd(), 'docs')) {
|
|
|
55
107
|
|
|
56
108
|
const items = fs.readdirSync(docsDir, { withFileTypes: true })
|
|
57
109
|
for (const item of items) {
|
|
58
|
-
if (item.isDirectory()
|
|
110
|
+
if (!item.isDirectory()) continue
|
|
111
|
+
|
|
112
|
+
// Default-product version: docs/v1 → /docs/v1
|
|
113
|
+
if (/^v\d/.test(item.name)) {
|
|
59
114
|
entries.push(`/docs/${item.name}`)
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Multi-product: directory has a _product_.json marker. Recurse
|
|
119
|
+
// into it and emit one entry per version subdirectory.
|
|
120
|
+
const productDir = path.join(docsDir, item.name)
|
|
121
|
+
const productMarker = path.join(productDir, '_product_.json')
|
|
122
|
+
if (!fs.existsSync(productMarker)) continue
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const versionItems = fs.readdirSync(productDir, { withFileTypes: true })
|
|
126
|
+
for (const v of versionItems) {
|
|
127
|
+
if (v.isDirectory() && /^v\d/.test(v.name)) {
|
|
128
|
+
entries.push(`/docs/${item.name}/${v.name}`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// Ignore product-dir read errors — leave that product unseeded.
|
|
60
133
|
}
|
|
61
134
|
}
|
|
62
135
|
} catch {
|
|
63
|
-
// Ignore errors — fall back to just '/'
|
|
136
|
+
// Ignore top-level errors — fall back to just '/'.
|
|
64
137
|
}
|
|
65
138
|
return entries
|
|
66
139
|
}
|
|
67
140
|
|
|
68
141
|
/**
|
|
69
|
-
* Create a full SvelteKit config with Specra defaults
|
|
142
|
+
* Create a full SvelteKit config with Specra defaults.
|
|
143
|
+
* Automatically reads deployment.basePath from specra.config.json
|
|
144
|
+
* for GitHub Pages deployments.
|
|
70
145
|
*/
|
|
71
146
|
export function specraConfig(options = {}) {
|
|
72
147
|
const { vitePreprocess } = options.vitePreprocess || {}
|
|
73
148
|
const userPrerender = options.kit?.prerender || {}
|
|
149
|
+
const basePath = options.kit?.paths?.base ?? resolveBasePath()
|
|
150
|
+
|
|
151
|
+
// Inject base path rehype plugin into mdsvex if basePath is set
|
|
152
|
+
const mdsvexOptions = options.mdsvex || {}
|
|
153
|
+
if (basePath) {
|
|
154
|
+
mdsvexOptions.rehypePlugins = [
|
|
155
|
+
...(mdsvexOptions.rehypePlugins || []),
|
|
156
|
+
[rehypeBasePath, { basePath }],
|
|
157
|
+
]
|
|
158
|
+
}
|
|
74
159
|
|
|
75
160
|
return {
|
|
76
161
|
extensions: ['.svelte', '.md', '.svx', '.mdx'],
|
|
77
162
|
preprocess: [
|
|
78
163
|
...(vitePreprocess ? [vitePreprocess()] : []),
|
|
79
|
-
mdsvex(specraMdsvexConfig(
|
|
164
|
+
mdsvex(specraMdsvexConfig(mdsvexOptions))
|
|
80
165
|
],
|
|
81
166
|
kit: {
|
|
82
167
|
...options.kit,
|
|
168
|
+
paths: {
|
|
169
|
+
...options.kit?.paths,
|
|
170
|
+
base: basePath,
|
|
171
|
+
},
|
|
83
172
|
prerender: {
|
|
84
173
|
handleHttpError: 'warn',
|
|
85
174
|
handleMissingId: 'warn',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { ChevronRight } from 'lucide-svelte';
|
|
3
|
+
import { base } from '$app/paths';
|
|
3
4
|
import { getConfigContext } from '../../stores/config.js';
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
@@ -13,8 +14,8 @@
|
|
|
13
14
|
|
|
14
15
|
let docsBase = $derived(
|
|
15
16
|
product && product !== '_default_'
|
|
16
|
-
?
|
|
17
|
-
:
|
|
17
|
+
? `${base}/docs/${product}/${version}`
|
|
18
|
+
: `${base}/docs/${version}`
|
|
18
19
|
);
|
|
19
20
|
|
|
20
21
|
const configStore = getConfigContext();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { FileText, ArrowRight } from 'lucide-svelte';
|
|
3
|
+
import { base } from '$app/paths';
|
|
3
4
|
import type { SpecraConfig } from '../../config.types.js';
|
|
4
5
|
import type { Snippet } from 'svelte';
|
|
5
6
|
|
|
@@ -42,8 +43,8 @@
|
|
|
42
43
|
// Match direct children of this category path
|
|
43
44
|
if (!docPath.startsWith(categoryPath + '/')) return false;
|
|
44
45
|
const remaining = docPath.slice(categoryPath.length + 1);
|
|
45
|
-
// Only direct children (no further slashes
|
|
46
|
-
return !remaining.includes('/')
|
|
46
|
+
// Only direct children (no further slashes)
|
|
47
|
+
return !remaining.includes('/');
|
|
47
48
|
})
|
|
48
49
|
.sort((a, b) => {
|
|
49
50
|
const posA = a.meta?.sidebar_position ?? 999;
|
|
@@ -54,8 +55,8 @@
|
|
|
54
55
|
|
|
55
56
|
const baseUrl = $derived(
|
|
56
57
|
product && product !== '_default_'
|
|
57
|
-
?
|
|
58
|
-
:
|
|
58
|
+
? `${base}/docs/${product}`
|
|
59
|
+
: `${base}/docs`
|
|
59
60
|
);
|
|
60
61
|
|
|
61
62
|
// Note: We always use '/docs' as the base for non-product routes.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { ChevronLeft, ChevronRight } from 'lucide-svelte';
|
|
3
|
+
import { base } from '$app/paths';
|
|
3
4
|
|
|
4
5
|
interface NavDoc {
|
|
5
6
|
title: string;
|
|
@@ -17,8 +18,8 @@
|
|
|
17
18
|
|
|
18
19
|
let docsBase = $derived(
|
|
19
20
|
product && product !== '_default_'
|
|
20
|
-
?
|
|
21
|
-
:
|
|
21
|
+
? `${base}/docs/${product}/${version}`
|
|
22
|
+
: `${base}/docs/${version}`
|
|
22
23
|
);
|
|
23
24
|
</script>
|
|
24
25
|
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { SpecraConfig } from '../../config.types.js';
|
|
3
|
+
import { base } from '$app/paths';
|
|
3
4
|
import Logo from './Logo.svelte';
|
|
4
5
|
|
|
6
|
+
/** Prefix internal links with base path */
|
|
7
|
+
function resolveHref(href: string): string {
|
|
8
|
+
if (href.startsWith('/') && !href.startsWith('//')) {
|
|
9
|
+
return `${base}${href}`;
|
|
10
|
+
}
|
|
11
|
+
return href;
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
interface Props {
|
|
6
15
|
config: SpecraConfig;
|
|
7
16
|
}
|
|
@@ -25,7 +34,7 @@
|
|
|
25
34
|
{#each column.items as item, itemIdx (itemIdx)}
|
|
26
35
|
<li>
|
|
27
36
|
<a
|
|
28
|
-
href={item.href}
|
|
37
|
+
href={resolveHref(item.href)}
|
|
29
38
|
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
30
39
|
>
|
|
31
40
|
{item.label}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Search, Menu, Github, Twitter, MessageCircle } from 'lucide-svelte';
|
|
3
|
+
import { base } from '$app/paths';
|
|
3
4
|
import { getConfigContext } from '../../stores/config.js';
|
|
4
5
|
import { sidebarStore } from '../../stores/sidebar.js';
|
|
5
6
|
import VersionSwitcher from './VersionSwitcher.svelte';
|
|
@@ -21,7 +22,8 @@
|
|
|
21
22
|
hidden?: boolean;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
/** Flat shape (from page components) */
|
|
26
|
+
interface ProductItemFlat {
|
|
25
27
|
slug: string;
|
|
26
28
|
label: string;
|
|
27
29
|
icon?: string;
|
|
@@ -30,13 +32,27 @@
|
|
|
30
32
|
isDefault: boolean;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
/** Nested shape (from SDK getProducts()) */
|
|
36
|
+
interface ProductItemNested {
|
|
37
|
+
slug: string;
|
|
38
|
+
config: {
|
|
39
|
+
label: string;
|
|
40
|
+
icon?: string;
|
|
41
|
+
badge?: string;
|
|
42
|
+
activeVersion?: string;
|
|
43
|
+
};
|
|
44
|
+
isDefault: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type ProductInput = ProductItemFlat | ProductItemNested;
|
|
48
|
+
|
|
33
49
|
interface Props {
|
|
34
50
|
currentVersion: string;
|
|
35
51
|
versions: string[];
|
|
36
52
|
versionsMeta?: VersionMeta[];
|
|
37
53
|
versionBanner?: BannerConfig;
|
|
38
54
|
config?: SpecraConfig;
|
|
39
|
-
products?:
|
|
55
|
+
products?: ProductInput[];
|
|
40
56
|
currentProduct?: string;
|
|
41
57
|
subheader?: Snippet;
|
|
42
58
|
}
|
|
@@ -95,7 +111,7 @@
|
|
|
95
111
|
>
|
|
96
112
|
<Menu class="h-5 w-5" />
|
|
97
113
|
</button>
|
|
98
|
-
<a href="/" class="flex items-center gap-2">
|
|
114
|
+
<a href="{base}/" class="flex items-center gap-2">
|
|
99
115
|
{#if !config.site.hideLogo}
|
|
100
116
|
{#if config.site.logo}
|
|
101
117
|
<Logo logo={config.site.logo} alt={config.site.title} className="w-18 object-contain" />
|
|
@@ -7,7 +7,8 @@ interface VersionMeta {
|
|
|
7
7
|
badge?: string;
|
|
8
8
|
hidden?: boolean;
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
/** Flat shape (from page components) */
|
|
11
|
+
interface ProductItemFlat {
|
|
11
12
|
slug: string;
|
|
12
13
|
label: string;
|
|
13
14
|
icon?: string;
|
|
@@ -15,13 +16,25 @@ interface ProductItem {
|
|
|
15
16
|
activeVersion?: string;
|
|
16
17
|
isDefault: boolean;
|
|
17
18
|
}
|
|
19
|
+
/** Nested shape (from SDK getProducts()) */
|
|
20
|
+
interface ProductItemNested {
|
|
21
|
+
slug: string;
|
|
22
|
+
config: {
|
|
23
|
+
label: string;
|
|
24
|
+
icon?: string;
|
|
25
|
+
badge?: string;
|
|
26
|
+
activeVersion?: string;
|
|
27
|
+
};
|
|
28
|
+
isDefault: boolean;
|
|
29
|
+
}
|
|
30
|
+
type ProductInput = ProductItemFlat | ProductItemNested;
|
|
18
31
|
interface Props {
|
|
19
32
|
currentVersion: string;
|
|
20
33
|
versions: string[];
|
|
21
34
|
versionsMeta?: VersionMeta[];
|
|
22
35
|
versionBanner?: BannerConfig;
|
|
23
36
|
config?: SpecraConfig;
|
|
24
|
-
products?:
|
|
37
|
+
products?: ProductInput[];
|
|
25
38
|
currentProduct?: string;
|
|
26
39
|
subheader?: Snippet;
|
|
27
40
|
}
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import MdxContent from './MdxContent.svelte';
|
|
3
3
|
import type { Component } from 'svelte';
|
|
4
|
-
|
|
5
|
-
interface MdxNode {
|
|
6
|
-
type: 'html' | 'component';
|
|
7
|
-
content?: string;
|
|
8
|
-
name?: string;
|
|
9
|
-
props?: Record<string, any>;
|
|
10
|
-
children?: MdxNode[];
|
|
11
|
-
}
|
|
4
|
+
import type { MdxNode } from '../../mdx.js';
|
|
12
5
|
|
|
13
6
|
interface Props {
|
|
14
7
|
nodes: MdxNode[];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import MdxContent from './MdxContent.svelte';
|
|
2
|
+
import type { Component } from 'svelte';
|
|
3
|
+
import type { MdxNode } from '../../mdx.js';
|
|
4
|
+
interface Props {
|
|
5
|
+
nodes: MdxNode[];
|
|
6
|
+
components: Record<string, Component>;
|
|
7
|
+
}
|
|
8
|
+
declare const MdxContent: Component<Props, {}, "">;
|
|
9
|
+
type MdxContent = ReturnType<typeof MdxContent>;
|
|
10
|
+
export default MdxContent;
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { FileQuestion, Home, ArrowLeft } from 'lucide-svelte';
|
|
3
|
+
import { base } from '$app/paths';
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
version?: string;
|
|
7
|
+
product?: string;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
let { version = '' }: Props = $props();
|
|
10
|
+
let { version = '', product = '' }: Props = $props();
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
/** URL prefix: /docs/{product}/{version} for named products, /docs/{version} for default */
|
|
13
|
+
const homeLink = $derived(
|
|
14
|
+
product && product !== '_default_' && version
|
|
15
|
+
? `${base}/docs/${product}/${version}`
|
|
16
|
+
: version
|
|
17
|
+
? `${base}/docs/${version}`
|
|
18
|
+
: `${base}/`
|
|
19
|
+
);
|
|
11
20
|
</script>
|
|
12
21
|
|
|
13
22
|
<div class="flex flex-col items-center justify-center min-h-[60vh] text-center px-4">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { ChevronDown, Check } from 'lucide-svelte';
|
|
3
3
|
import { goto } from '$app/navigation';
|
|
4
|
+
import { base } from '$app/paths';
|
|
4
5
|
import { browser } from '$app/environment';
|
|
5
6
|
import Icon from './Icon.svelte';
|
|
6
7
|
|
|
@@ -107,9 +108,9 @@
|
|
|
107
108
|
|
|
108
109
|
const version = product.activeVersion || 'v1.0.0';
|
|
109
110
|
if (product.isDefault) {
|
|
110
|
-
goto(
|
|
111
|
+
goto(`${base}/docs/${version}`);
|
|
111
112
|
} else {
|
|
112
|
-
goto(
|
|
113
|
+
goto(`${base}/docs/${product.slug}/${version}`);
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { page } from '$app/stores';
|
|
3
|
+
import { base } from '$app/paths';
|
|
3
4
|
import { ChevronRight, ChevronDown, Lock } from 'lucide-svelte';
|
|
4
5
|
import type { SpecraConfig } from '../../config.types.js';
|
|
5
6
|
import Icon from './Icon.svelte';
|
|
@@ -50,11 +51,11 @@
|
|
|
50
51
|
|
|
51
52
|
let { docs = [], version, product, onLinkClick, config, activeTabGroup }: Props = $props();
|
|
52
53
|
|
|
53
|
-
/** URL prefix: /docs/{product}/{version} for named products, /docs/{version} for default */
|
|
54
|
+
/** URL prefix: {base}/docs/{product}/{version} for named products, {base}/docs/{version} for default */
|
|
54
55
|
let docsBase = $derived(
|
|
55
56
|
product && product !== '_default_'
|
|
56
|
-
?
|
|
57
|
-
:
|
|
57
|
+
? `${base}/docs/${product}/${version}`
|
|
58
|
+
: `${base}/docs/${version}`
|
|
58
59
|
);
|
|
59
60
|
|
|
60
61
|
const STORAGE_KEY = 'specra-sidebar-collapsed';
|
|
@@ -134,9 +135,8 @@
|
|
|
134
135
|
if (isIndexFile) {
|
|
135
136
|
rootGroups[groupName].position = doc.sidebar_position ?? 999;
|
|
136
137
|
rootGroups[groupName].icon = doc.categoryIcon;
|
|
137
|
-
} else {
|
|
138
|
-
rootGroups[groupName].items.push(doc);
|
|
139
138
|
}
|
|
139
|
+
rootGroups[groupName].items.push(doc);
|
|
140
140
|
return;
|
|
141
141
|
}
|
|
142
142
|
|
|
@@ -179,17 +179,14 @@
|
|
|
179
179
|
if (doc.categoryIcon) {
|
|
180
180
|
currentLevel[folder].icon = doc.categoryIcon;
|
|
181
181
|
}
|
|
182
|
-
} else {
|
|
183
|
-
currentLevel[folder].items.push(doc);
|
|
184
182
|
}
|
|
183
|
+
currentLevel[folder].items.push(doc);
|
|
185
184
|
}
|
|
186
185
|
|
|
187
186
|
currentLevel = currentLevel[folder].children;
|
|
188
187
|
}
|
|
189
188
|
} else {
|
|
190
|
-
|
|
191
|
-
standalone.push(doc);
|
|
192
|
-
}
|
|
189
|
+
standalone.push(doc);
|
|
193
190
|
}
|
|
194
191
|
});
|
|
195
192
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { ChevronDown } from 'lucide-svelte';
|
|
3
3
|
import { goto } from '$app/navigation';
|
|
4
|
+
import { base } from '$app/paths';
|
|
4
5
|
import type { TabGroup, SpecraConfig } from '../../config.types.js';
|
|
5
6
|
import Icon from './Icon.svelte';
|
|
6
7
|
|
|
@@ -32,8 +33,8 @@
|
|
|
32
33
|
|
|
33
34
|
let docsBase = $derived(
|
|
34
35
|
product && product !== '_default_' && version
|
|
35
|
-
?
|
|
36
|
-
: version ?
|
|
36
|
+
? `${base}/docs/${product}/${version}`
|
|
37
|
+
: version ? `${base}/docs/${version}` : `${base}/docs`
|
|
37
38
|
);
|
|
38
39
|
|
|
39
40
|
let dropdownOpen = $state(false);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MDX/mdsvex component map for Specra documentation.
|
|
3
|
+
*
|
|
4
|
+
* In mdsvex, custom components are imported directly in .svx files.
|
|
5
|
+
* This file exports the component map for programmatic use.
|
|
6
|
+
*
|
|
7
|
+
* Usage in .svx files:
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script>
|
|
10
|
+
* import { Callout, CodeBlock, Tabs, Tab } from 'specra/components'
|
|
11
|
+
* </script>
|
|
12
|
+
*
|
|
13
|
+
* <Callout type="info">This is a callout</Callout>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import type { Component } from 'svelte';
|
|
17
|
+
import { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference } from './components/docs';
|
|
18
|
+
export { Callout, Accordion, AccordionItem, Tabs, Tab, Image, Video, Card, CardGrid, ImageCard, ImageCardGrid, Steps, Step, Icon, Mermaid, Math, Columns, Column, DocBadge, Tooltip, Frame, CodeBlock, Timeline, TimelineItem, ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference, };
|
|
19
|
+
/**
|
|
20
|
+
* Component map for passing to layout components that render MDX content.
|
|
21
|
+
*/
|
|
22
|
+
export declare const mdxComponents: Record<string, Component>;
|
package/dist/mdx-security.js
CHANGED
|
@@ -44,22 +44,23 @@ export function validatePathWithinDirectory(filePath, allowedDir) {
|
|
|
44
44
|
* These patterns can execute arbitrary code during SSR
|
|
45
45
|
*/
|
|
46
46
|
const DANGEROUS_PATTERNS = [
|
|
47
|
-
// JavaScript execution
|
|
48
|
-
|
|
49
|
-
/
|
|
50
|
-
/
|
|
51
|
-
/
|
|
47
|
+
// JavaScript execution — require expression context (after { ; = or line start)
|
|
48
|
+
// to avoid false positives on prose like "bulk import (CSV...)" or "fetch (data)"
|
|
49
|
+
/(?:^|[{;=,])\s*eval\s*\(/gim,
|
|
50
|
+
/(?:^|[{;=,])\s*Function\s*\(/gim,
|
|
51
|
+
/(?:^|[{;=,])\s*import\s*\(/gim,
|
|
52
|
+
/(?:^|[{;=,])\s*require\s*\(/gim,
|
|
52
53
|
// File system access
|
|
53
54
|
/fs\.[a-z]+/gi,
|
|
54
|
-
/readFile/
|
|
55
|
-
/writeFile/
|
|
55
|
+
/(?:^|[{;=,])\s*readFile/gim,
|
|
56
|
+
/(?:^|[{;=,])\s*writeFile/gim,
|
|
56
57
|
/process\.env/gi,
|
|
57
|
-
// Network requests during SSR
|
|
58
|
-
/fetch\s*\(/
|
|
58
|
+
// Network requests during SSR — require expression context
|
|
59
|
+
/(?:^|[{;=,])\s*fetch\s*\(/gim,
|
|
59
60
|
// Dangerous Node.js modules
|
|
60
61
|
/child_process/gi,
|
|
61
|
-
/exec\s*\(/
|
|
62
|
-
/spawn\s*\(/
|
|
62
|
+
/(?:^|[{;=,])\s*exec\s*\(/gim,
|
|
63
|
+
/(?:^|[{;=,])\s*spawn\s*\(/gim,
|
|
63
64
|
// Script tag injection
|
|
64
65
|
/<script[>\s]/gi,
|
|
65
66
|
/javascript:/gi,
|
package/dist/mdx.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import matter from "gray-matter";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
import { rehypeBasePath } from "./rehype-base-path.js";
|
|
4
6
|
import { unified } from "unified";
|
|
5
7
|
import remarkParse from "remark-parse";
|
|
6
8
|
import remarkGfm from "remark-gfm";
|
|
@@ -394,15 +396,41 @@ function parseJsxExpression(expr) {
|
|
|
394
396
|
/**
|
|
395
397
|
* Process markdown content to HTML using remark/rehype pipeline.
|
|
396
398
|
*/
|
|
399
|
+
function normalizeBasePath(bp) {
|
|
400
|
+
if (!bp)
|
|
401
|
+
return '';
|
|
402
|
+
let normalized = bp.startsWith('/') ? bp : `/${bp}`;
|
|
403
|
+
return normalized.replace(/\/+$/, '');
|
|
404
|
+
}
|
|
405
|
+
function resolveDeploymentBasePath() {
|
|
406
|
+
if (process.env.BASE_PATH)
|
|
407
|
+
return normalizeBasePath(process.env.BASE_PATH);
|
|
408
|
+
try {
|
|
409
|
+
const configPath = path.join(process.cwd(), 'specra.config.json');
|
|
410
|
+
if (fs.existsSync(configPath)) {
|
|
411
|
+
const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
412
|
+
if (raw.deployment?.basePath && !raw.deployment?.customDomain) {
|
|
413
|
+
return normalizeBasePath(raw.deployment.basePath);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch { /* ignore */ }
|
|
418
|
+
return '';
|
|
419
|
+
}
|
|
397
420
|
async function processMarkdownToHtml(markdown) {
|
|
398
|
-
const
|
|
421
|
+
const basePath = resolveDeploymentBasePath();
|
|
422
|
+
const processor = unified()
|
|
399
423
|
.use(remarkParse)
|
|
400
424
|
.use(remarkGfm)
|
|
401
425
|
.use(remarkMath)
|
|
402
426
|
.use(remarkRehype, { allowDangerousHtml: true })
|
|
403
427
|
.use(rehypeRaw)
|
|
404
428
|
.use(rehypeSlug)
|
|
405
|
-
.use(rehypeKatex)
|
|
429
|
+
.use(rehypeKatex);
|
|
430
|
+
if (basePath) {
|
|
431
|
+
processor.use(rehypeBasePath, { basePath });
|
|
432
|
+
}
|
|
433
|
+
const result = await processor
|
|
406
434
|
.use(rehypeStringify)
|
|
407
435
|
.process(markdown);
|
|
408
436
|
return String(result);
|
|
@@ -1045,6 +1073,7 @@ async function processMarkdownToMdxNodes(markdown) {
|
|
|
1045
1073
|
const dedented = dedentComponentChildren(preprocessed);
|
|
1046
1074
|
// Ensure component block integrity in the markdown
|
|
1047
1075
|
const normalized = ensureComponentBlockIntegrity(dedented);
|
|
1076
|
+
const basePath = resolveDeploymentBasePath();
|
|
1048
1077
|
const processor = unified()
|
|
1049
1078
|
.use(remarkParse)
|
|
1050
1079
|
.use(remarkGfm)
|
|
@@ -1053,6 +1082,9 @@ async function processMarkdownToMdxNodes(markdown) {
|
|
|
1053
1082
|
.use(rehypeRaw)
|
|
1054
1083
|
.use(rehypeSlug)
|
|
1055
1084
|
.use(rehypeKatex);
|
|
1085
|
+
if (basePath) {
|
|
1086
|
+
processor.use(rehypeBasePath, { basePath });
|
|
1087
|
+
}
|
|
1056
1088
|
const mdast = processor.parse(normalized);
|
|
1057
1089
|
const hast = await processor.run(mdast);
|
|
1058
1090
|
// The hast root has children - process them into MdxNodes
|
|
@@ -1135,7 +1167,14 @@ function readDocFromFile(filePath, originalSlug) {
|
|
|
1135
1167
|
return null;
|
|
1136
1168
|
}
|
|
1137
1169
|
const fileContents = fs.readFileSync(filePath, "utf8");
|
|
1138
|
-
const { data, content } = matter(fileContents
|
|
1170
|
+
const { data, content } = matter(fileContents, {
|
|
1171
|
+
engines: {
|
|
1172
|
+
yaml: {
|
|
1173
|
+
parse: (str) => yaml.load(str),
|
|
1174
|
+
stringify: (obj) => yaml.dump(obj),
|
|
1175
|
+
},
|
|
1176
|
+
},
|
|
1177
|
+
});
|
|
1139
1178
|
// Security: Validate MDX content for dangerous patterns
|
|
1140
1179
|
const securityCheck = validateMDXSecurity(content, {
|
|
1141
1180
|
strictMode: process.env.NODE_ENV === 'production',
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rehype plugin that prefixes internal absolute links with a base path.
|
|
3
|
+
* Internal links start with "/" and don't start with "http" or "//".
|
|
4
|
+
*
|
|
5
|
+
* Used for GitHub Pages deployments where the site lives under a subpath.
|
|
6
|
+
*
|
|
7
|
+
* Manually walks the tree to avoid ESM/CJS issues with unist-util-visit
|
|
8
|
+
* when loaded from svelte-config.js at Node.js config time.
|
|
9
|
+
*/
|
|
10
|
+
import type { Root } from 'hast';
|
|
11
|
+
interface Options {
|
|
12
|
+
basePath?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function rehypeBasePath(options?: Options): (tree: Root) => void;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function walkElements(nodes, fn) {
|
|
2
|
+
for (const node of nodes) {
|
|
3
|
+
if (node.type === 'element') {
|
|
4
|
+
fn(node);
|
|
5
|
+
if (node.children) {
|
|
6
|
+
walkElements(node.children, fn);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function rehypeBasePath(options = {}) {
|
|
12
|
+
const { basePath = '' } = options;
|
|
13
|
+
if (!basePath)
|
|
14
|
+
return () => { };
|
|
15
|
+
const cleanBase = basePath.replace(/\/$/, '');
|
|
16
|
+
return (tree) => {
|
|
17
|
+
walkElements(tree.children, (node) => {
|
|
18
|
+
if (node.tagName === 'a' && node.properties?.href) {
|
|
19
|
+
const href = node.properties.href;
|
|
20
|
+
if (typeof href === 'string' && href.startsWith('/') && !href.startsWith('//') && !href.startsWith(cleanBase + '/')) {
|
|
21
|
+
node.properties.href = cleanBase + href;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (node.tagName === 'img' && node.properties?.src) {
|
|
25
|
+
const src = node.properties.src;
|
|
26
|
+
if (typeof src === 'string' && src.startsWith('/') && !src.startsWith('//') && !src.startsWith(cleanBase + '/')) {
|
|
27
|
+
node.properties.src = cleanBase + src;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
}
|
package/dist/sidebar-utils.js
CHANGED
|
@@ -79,13 +79,10 @@ export function buildSidebarStructure(docs) {
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
if (isIndexFile) {
|
|
82
|
-
// Use categoryPosition if available (from _category_.json), otherwise sidebar_position from frontmatter
|
|
83
82
|
rootGroups[groupName].position = doc.categoryPosition ?? doc.meta.sidebar_position ?? 999;
|
|
84
83
|
rootGroups[groupName].icon = doc.categoryIcon;
|
|
85
84
|
}
|
|
86
|
-
|
|
87
|
-
rootGroups[groupName].items.push(doc);
|
|
88
|
-
}
|
|
85
|
+
rootGroups[groupName].items.push(doc);
|
|
89
86
|
return;
|
|
90
87
|
}
|
|
91
88
|
if (pathParts.length > 1) {
|
|
@@ -127,17 +124,13 @@ export function buildSidebarStructure(docs) {
|
|
|
127
124
|
currentLevel[folder].defaultCollapsed = doc.categoryCollapsed;
|
|
128
125
|
}
|
|
129
126
|
}
|
|
130
|
-
|
|
131
|
-
currentLevel[folder].items.push(doc);
|
|
132
|
-
}
|
|
127
|
+
currentLevel[folder].items.push(doc);
|
|
133
128
|
}
|
|
134
129
|
currentLevel = currentLevel[folder].children;
|
|
135
130
|
}
|
|
136
131
|
}
|
|
137
132
|
else {
|
|
138
|
-
|
|
139
|
-
standalone.push(doc);
|
|
140
|
-
}
|
|
133
|
+
standalone.push(doc);
|
|
141
134
|
}
|
|
142
135
|
});
|
|
143
136
|
return { rootGroups, standalone };
|
package/dist/styles/globals.css
CHANGED
|
@@ -159,28 +159,39 @@
|
|
|
159
159
|
text-decoration: none;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
/*
|
|
162
|
+
/* Prose text links - underlined for readability */
|
|
163
163
|
main .prose a {
|
|
164
164
|
color: var(--color-primary);
|
|
165
|
-
text-decoration:
|
|
166
|
-
|
|
165
|
+
text-decoration: underline;
|
|
166
|
+
text-underline-offset: 3px;
|
|
167
|
+
text-decoration-thickness: 1px;
|
|
168
|
+
text-decoration-color: color-mix(in srgb, var(--color-primary) 40%, transparent);
|
|
169
|
+
transition: text-decoration-color 0.2s ease;
|
|
167
170
|
}
|
|
168
171
|
|
|
169
172
|
main .prose a:hover {
|
|
170
|
-
text-decoration:
|
|
171
|
-
color: var(--color-primary);
|
|
173
|
+
text-decoration-color: var(--color-primary);
|
|
172
174
|
}
|
|
173
175
|
|
|
174
|
-
/* Prose links in documentation */
|
|
175
176
|
.prose a {
|
|
176
177
|
color: var(--color-primary);
|
|
177
|
-
text-decoration:
|
|
178
|
+
text-decoration: underline;
|
|
179
|
+
text-underline-offset: 3px;
|
|
180
|
+
text-decoration-thickness: 1px;
|
|
181
|
+
text-decoration-color: color-mix(in srgb, var(--color-primary) 40%, transparent);
|
|
178
182
|
font-weight: 500;
|
|
179
183
|
}
|
|
180
184
|
|
|
181
185
|
.prose a:hover {
|
|
186
|
+
text-decoration-color: var(--color-primary);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* Navigation/UI links inside prose - no underline */
|
|
190
|
+
.prose .not-prose a,
|
|
191
|
+
.prose nav a,
|
|
192
|
+
.prose [data-doc-nav] a,
|
|
193
|
+
.prose .doc-navigation a {
|
|
182
194
|
text-decoration: none !important;
|
|
183
|
-
color: var(--color-primary);
|
|
184
195
|
}
|
|
185
196
|
|
|
186
197
|
/* Sidebar links - no underline on hover */
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { type ClassValue } from 'clsx';
|
|
2
2
|
export declare function cn(...inputs: ClassValue[]): string;
|
|
3
3
|
/**
|
|
4
|
-
* Get the correct asset path based on deployment configuration
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - GitHub Pages without custom domain: Uses basePath from config
|
|
8
|
-
* - Static hosting with custom domain: No basePath needed
|
|
4
|
+
* Get the correct asset path based on deployment configuration.
|
|
5
|
+
* Uses SvelteKit's base path (from kit.paths.base) which is resolved
|
|
6
|
+
* from deployment.basePath in specra.config.json or the BASE_PATH env var.
|
|
9
7
|
*
|
|
10
|
-
* @param
|
|
11
|
-
* @returns The properly formatted asset path
|
|
8
|
+
* @param assetPath - The asset path (can start with or without '/')
|
|
9
|
+
* @returns The properly formatted asset path with base prefix
|
|
12
10
|
*/
|
|
13
|
-
export declare function getAssetPath(
|
|
11
|
+
export declare function getAssetPath(assetPath: string): string;
|
package/dist/utils.js
CHANGED
|
@@ -4,27 +4,20 @@ export function cn(...inputs) {
|
|
|
4
4
|
return twMerge(clsx(inputs));
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
|
-
* Get the correct asset path based on deployment configuration
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - GitHub Pages without custom domain: Uses basePath from config
|
|
11
|
-
* - Static hosting with custom domain: No basePath needed
|
|
7
|
+
* Get the correct asset path based on deployment configuration.
|
|
8
|
+
* Uses SvelteKit's base path (from kit.paths.base) which is resolved
|
|
9
|
+
* from deployment.basePath in specra.config.json or the BASE_PATH env var.
|
|
12
10
|
*
|
|
13
|
-
* @param
|
|
14
|
-
* @returns The properly formatted asset path
|
|
11
|
+
* @param assetPath - The asset path (can start with or without '/')
|
|
12
|
+
* @returns The properly formatted asset path with base prefix
|
|
15
13
|
*/
|
|
16
|
-
export function getAssetPath(
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
// Normalize the input path: ensure it starts with '/'
|
|
20
|
-
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
21
|
-
// If we have a basePath (GitHub Pages without custom domain), prepend it
|
|
14
|
+
export function getAssetPath(assetPath) {
|
|
15
|
+
const basePath = process.env.BASE_PATH || '';
|
|
16
|
+
const normalizedPath = assetPath.startsWith('/') ? assetPath : `/${assetPath}`;
|
|
22
17
|
if (basePath) {
|
|
23
|
-
// Normalize basePath: remove trailing slash, ensure leading slash
|
|
24
18
|
const normalizedBase = basePath.startsWith('/') ? basePath : `/${basePath}`;
|
|
25
19
|
const cleanBase = normalizedBase.replace(/\/$/, '');
|
|
26
20
|
return `${cleanBase}${normalizedPath}`;
|
|
27
21
|
}
|
|
28
|
-
// Default: return the normalized path (works for Vercel, custom domains, and dev)
|
|
29
22
|
return normalizedPath;
|
|
30
23
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specra",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.55",
|
|
4
4
|
"description": "A modern documentation library for SvelteKit with built-in versioning, API reference generation, full-text search, and MDX support",
|
|
5
5
|
"svelte": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -78,13 +78,14 @@
|
|
|
78
78
|
"date-fns": "^4.1.0",
|
|
79
79
|
"embla-carousel-svelte": "^8.5.1",
|
|
80
80
|
"gray-matter": "^4.0.3",
|
|
81
|
+
"hast-util-to-html": "^9.0.0",
|
|
82
|
+
"js-yaml": "^4.1.1",
|
|
81
83
|
"katex": "^0.16.27",
|
|
82
84
|
"lucide-svelte": "^0.454.0",
|
|
83
85
|
"mdsvex": "^0.12.0",
|
|
84
86
|
"meilisearch": "^0.54.0",
|
|
85
87
|
"mermaid": "^11.12.2",
|
|
86
88
|
"mode-watcher": "^0.5.0",
|
|
87
|
-
"hast-util-to-html": "^9.0.0",
|
|
88
89
|
"rehype-katex": "^7.0.1",
|
|
89
90
|
"rehype-raw": "^7.0.0",
|
|
90
91
|
"rehype-slug": "^6.0.0",
|
|
@@ -103,6 +104,7 @@
|
|
|
103
104
|
"@sveltejs/kit": "^2.0.0",
|
|
104
105
|
"@sveltejs/package": "^2.0.0",
|
|
105
106
|
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
|
107
|
+
"@types/js-yaml": "^4.0.9",
|
|
106
108
|
"@types/node": "^22",
|
|
107
109
|
"svelte": "^5.0.0",
|
|
108
110
|
"svelte-check": "^4.0.0",
|