specra 0.2.13 → 0.2.15

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.
@@ -0,0 +1 @@
1
+ export function rehypeBasePath(options?: {}): (tree: any) => void;
@@ -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
@@ -48,18 +49,26 @@ export function specraMdsvexConfig(options = {}) {
48
49
  * Read the deployment basePath from specra.config.json if present.
49
50
  * Falls back to the BASE_PATH environment variable, then empty string.
50
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
+
51
62
  function resolveBasePath(configPath = path.join(process.cwd(), 'specra.config.json')) {
52
63
  // Environment variable takes priority
53
- if (process.env.BASE_PATH) return process.env.BASE_PATH
64
+ if (process.env.BASE_PATH) return normalizeBasePath(process.env.BASE_PATH)
54
65
 
55
66
  try {
56
67
  if (fs.existsSync(configPath)) {
57
68
  const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'))
58
69
  if (raw.deployment?.basePath) {
59
- const bp = raw.deployment.basePath
60
- // If custom domain is set, ignore basePath
61
70
  if (raw.deployment?.customDomain) return ''
62
- return bp.startsWith('/') ? bp : `/${bp}`
71
+ return normalizeBasePath(raw.deployment.basePath)
63
72
  }
64
73
  }
65
74
  } catch {
@@ -72,7 +81,7 @@ function resolveBasePath(configPath = path.join(process.cwd(), 'specra.config.js
72
81
  * Scan the docs/ directory and return prerender entries for all version root pages.
73
82
  * This ensures adapter-static discovers and prerenders every version, not just the active one.
74
83
  */
75
- function discoverVersionEntries(basePath = '', docsDir = path.join(process.cwd(), 'docs')) {
84
+ function discoverVersionEntries(docsDir = path.join(process.cwd(), 'docs')) {
76
85
  const entries = ['/']
77
86
  try {
78
87
  if (!fs.existsSync(docsDir)) return entries
@@ -80,7 +89,7 @@ function discoverVersionEntries(basePath = '', docsDir = path.join(process.cwd()
80
89
  const items = fs.readdirSync(docsDir, { withFileTypes: true })
81
90
  for (const item of items) {
82
91
  if (item.isDirectory() && /^v\d/.test(item.name)) {
83
- entries.push(`${basePath}/docs/${item.name}`)
92
+ entries.push(`/docs/${item.name}`)
84
93
  }
85
94
  }
86
95
  } catch {
@@ -99,11 +108,20 @@ export function specraConfig(options = {}) {
99
108
  const userPrerender = options.kit?.prerender || {}
100
109
  const basePath = options.kit?.paths?.base ?? resolveBasePath()
101
110
 
111
+ // Inject base path rehype plugin into mdsvex if basePath is set
112
+ const mdsvexOptions = options.mdsvex || {}
113
+ if (basePath) {
114
+ mdsvexOptions.rehypePlugins = [
115
+ ...(mdsvexOptions.rehypePlugins || []),
116
+ [rehypeBasePath, { basePath }],
117
+ ]
118
+ }
119
+
102
120
  return {
103
121
  extensions: ['.svelte', '.md', '.svx', '.mdx'],
104
122
  preprocess: [
105
123
  ...(vitePreprocess ? [vitePreprocess()] : []),
106
- mdsvex(specraMdsvexConfig(options.mdsvex || {}))
124
+ mdsvex(specraMdsvexConfig(mdsvexOptions))
107
125
  ],
108
126
  kit: {
109
127
  ...options.kit,
@@ -115,7 +133,7 @@ export function specraConfig(options = {}) {
115
133
  handleHttpError: 'warn',
116
134
  handleMissingId: 'warn',
117
135
  handleUnseenRoutes: 'warn',
118
- entries: discoverVersionEntries(basePath),
136
+ entries: discoverVersionEntries(),
119
137
  ...userPrerender,
120
138
  }
121
139
  }
@@ -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
- ? `/docs/${product}/${version}`
17
- : `/docs/${version}`
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
 
@@ -54,8 +55,8 @@
54
55
 
55
56
  const baseUrl = $derived(
56
57
  product && product !== '_default_'
57
- ? `/docs/${product}`
58
- : '/docs'
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
- ? `/docs/${product}/${version}`
21
- : `/docs/${version}`
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';
@@ -110,7 +111,7 @@
110
111
  >
111
112
  <Menu class="h-5 w-5" />
112
113
  </button>
113
- <a href="/" class="flex items-center gap-2">
114
+ <a href="{base}/" class="flex items-center gap-2">
114
115
  {#if !config.site.hideLogo}
115
116
  {#if config.site.logo}
116
117
  <Logo logo={config.site.logo} alt={config.site.title} className="w-18 object-contain" />
@@ -1,5 +1,6 @@
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;
@@ -11,10 +12,10 @@
11
12
  /** URL prefix: /docs/{product}/{version} for named products, /docs/{version} for default */
12
13
  const homeLink = $derived(
13
14
  product && product !== '_default_' && version
14
- ? `/docs/${product}/${version}`
15
+ ? `${base}/docs/${product}/${version}`
15
16
  : version
16
- ? `/docs/${version}`
17
- : '/'
17
+ ? `${base}/docs/${version}`
18
+ : `${base}/`
18
19
  );
19
20
  </script>
20
21
 
@@ -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(`/docs/${version}`);
111
+ goto(`${base}/docs/${version}`);
111
112
  } else {
112
- goto(`/docs/${product.slug}/${version}`);
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
- ? `/docs/${product}/${version}`
57
- : `/docs/${version}`
57
+ ? `${base}/docs/${product}/${version}`
58
+ : `${base}/docs/${version}`
58
59
  );
59
60
 
60
61
  const STORAGE_KEY = 'specra-sidebar-collapsed';
@@ -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
- ? `/docs/${product}/${version}`
36
- : version ? `/docs/${version}` : '/docs'
36
+ ? `${base}/docs/${product}/${version}`
37
+ : version ? `${base}/docs/${version}` : `${base}/docs`
37
38
  );
38
39
 
39
40
  let dropdownOpen = $state(false);
package/dist/mdx.js CHANGED
@@ -2,6 +2,7 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import matter from "gray-matter";
4
4
  import yaml from "js-yaml";
5
+ import { rehypeBasePath } from "./rehype-base-path.js";
5
6
  import { unified } from "unified";
6
7
  import remarkParse from "remark-parse";
7
8
  import remarkGfm from "remark-gfm";
@@ -395,15 +396,41 @@ function parseJsxExpression(expr) {
395
396
  /**
396
397
  * Process markdown content to HTML using remark/rehype pipeline.
397
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
+ }
398
420
  async function processMarkdownToHtml(markdown) {
399
- const result = await unified()
421
+ const basePath = resolveDeploymentBasePath();
422
+ const processor = unified()
400
423
  .use(remarkParse)
401
424
  .use(remarkGfm)
402
425
  .use(remarkMath)
403
426
  .use(remarkRehype, { allowDangerousHtml: true })
404
427
  .use(rehypeRaw)
405
428
  .use(rehypeSlug)
406
- .use(rehypeKatex)
429
+ .use(rehypeKatex);
430
+ if (basePath) {
431
+ processor.use(rehypeBasePath, { basePath });
432
+ }
433
+ const result = await processor
407
434
  .use(rehypeStringify)
408
435
  .process(markdown);
409
436
  return String(result);
@@ -1046,6 +1073,7 @@ async function processMarkdownToMdxNodes(markdown) {
1046
1073
  const dedented = dedentComponentChildren(preprocessed);
1047
1074
  // Ensure component block integrity in the markdown
1048
1075
  const normalized = ensureComponentBlockIntegrity(dedented);
1076
+ const basePath = resolveDeploymentBasePath();
1049
1077
  const processor = unified()
1050
1078
  .use(remarkParse)
1051
1079
  .use(remarkGfm)
@@ -1054,6 +1082,9 @@ async function processMarkdownToMdxNodes(markdown) {
1054
1082
  .use(rehypeRaw)
1055
1083
  .use(rehypeSlug)
1056
1084
  .use(rehypeKatex);
1085
+ if (basePath) {
1086
+ processor.use(rehypeBasePath, { basePath });
1087
+ }
1057
1088
  const mdast = processor.parse(normalized);
1058
1089
  const hast = await processor.run(mdast);
1059
1090
  // The hast root has children - process them into MdxNodes
@@ -0,0 +1,6 @@
1
+ import type { Root } from 'hast';
2
+ interface Options {
3
+ basePath?: string;
4
+ }
5
+ export declare function rehypeBasePath(options?: Options): (tree: Root) => void;
6
+ export {};
@@ -0,0 +1,29 @@
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
+ import { visit } from 'unist-util-visit';
8
+ export function rehypeBasePath(options = {}) {
9
+ const { basePath = '' } = options;
10
+ if (!basePath)
11
+ return () => { };
12
+ const cleanBase = basePath.replace(/\/$/, '');
13
+ return (tree) => {
14
+ visit(tree, 'element', (node) => {
15
+ if (node.tagName === 'a' && node.properties?.href) {
16
+ const href = node.properties.href;
17
+ if (typeof href === 'string' && href.startsWith('/') && !href.startsWith('//') && !href.startsWith(cleanBase + '/')) {
18
+ node.properties.href = cleanBase + href;
19
+ }
20
+ }
21
+ if (node.tagName === 'img' && node.properties?.src) {
22
+ const src = node.properties.src;
23
+ if (typeof src === 'string' && src.startsWith('/') && !src.startsWith('//') && !src.startsWith(cleanBase + '/')) {
24
+ node.properties.src = cleanBase + src;
25
+ }
26
+ }
27
+ });
28
+ };
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specra",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
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",