specra 0.2.13 → 0.2.14

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
@@ -72,7 +73,7 @@ function resolveBasePath(configPath = path.join(process.cwd(), 'specra.config.js
72
73
  * Scan the docs/ directory and return prerender entries for all version root pages.
73
74
  * This ensures adapter-static discovers and prerenders every version, not just the active one.
74
75
  */
75
- function discoverVersionEntries(basePath = '', docsDir = path.join(process.cwd(), 'docs')) {
76
+ function discoverVersionEntries(docsDir = path.join(process.cwd(), 'docs')) {
76
77
  const entries = ['/']
77
78
  try {
78
79
  if (!fs.existsSync(docsDir)) return entries
@@ -80,7 +81,7 @@ function discoverVersionEntries(basePath = '', docsDir = path.join(process.cwd()
80
81
  const items = fs.readdirSync(docsDir, { withFileTypes: true })
81
82
  for (const item of items) {
82
83
  if (item.isDirectory() && /^v\d/.test(item.name)) {
83
- entries.push(`${basePath}/docs/${item.name}`)
84
+ entries.push(`/docs/${item.name}`)
84
85
  }
85
86
  }
86
87
  } catch {
@@ -99,11 +100,20 @@ export function specraConfig(options = {}) {
99
100
  const userPrerender = options.kit?.prerender || {}
100
101
  const basePath = options.kit?.paths?.base ?? resolveBasePath()
101
102
 
103
+ // Inject base path rehype plugin into mdsvex if basePath is set
104
+ const mdsvexOptions = options.mdsvex || {}
105
+ if (basePath) {
106
+ mdsvexOptions.rehypePlugins = [
107
+ ...(mdsvexOptions.rehypePlugins || []),
108
+ [rehypeBasePath, { basePath }],
109
+ ]
110
+ }
111
+
102
112
  return {
103
113
  extensions: ['.svelte', '.md', '.svx', '.mdx'],
104
114
  preprocess: [
105
115
  ...(vitePreprocess ? [vitePreprocess()] : []),
106
- mdsvex(specraMdsvexConfig(options.mdsvex || {}))
116
+ mdsvex(specraMdsvexConfig(mdsvexOptions))
107
117
  ],
108
118
  kit: {
109
119
  ...options.kit,
@@ -115,7 +125,7 @@ export function specraConfig(options = {}) {
115
125
  handleHttpError: 'warn',
116
126
  handleMissingId: 'warn',
117
127
  handleUnseenRoutes: 'warn',
118
- entries: discoverVersionEntries(basePath),
128
+ entries: discoverVersionEntries(),
119
129
  ...userPrerender,
120
130
  }
121
131
  }
@@ -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,36 @@ function parseJsxExpression(expr) {
395
396
  /**
396
397
  * Process markdown content to HTML using remark/rehype pipeline.
397
398
  */
399
+ function resolveDeploymentBasePath() {
400
+ if (process.env.BASE_PATH)
401
+ return process.env.BASE_PATH;
402
+ try {
403
+ const configPath = path.join(process.cwd(), 'specra.config.json');
404
+ if (fs.existsSync(configPath)) {
405
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
406
+ if (raw.deployment?.basePath && !raw.deployment?.customDomain) {
407
+ const bp = raw.deployment.basePath;
408
+ return bp.startsWith('/') ? bp : `/${bp}`;
409
+ }
410
+ }
411
+ }
412
+ catch { /* ignore */ }
413
+ return '';
414
+ }
398
415
  async function processMarkdownToHtml(markdown) {
399
- const result = await unified()
416
+ const basePath = resolveDeploymentBasePath();
417
+ const processor = unified()
400
418
  .use(remarkParse)
401
419
  .use(remarkGfm)
402
420
  .use(remarkMath)
403
421
  .use(remarkRehype, { allowDangerousHtml: true })
404
422
  .use(rehypeRaw)
405
423
  .use(rehypeSlug)
406
- .use(rehypeKatex)
424
+ .use(rehypeKatex);
425
+ if (basePath) {
426
+ processor.use(rehypeBasePath, { basePath });
427
+ }
428
+ const result = await processor
407
429
  .use(rehypeStringify)
408
430
  .process(markdown);
409
431
  return String(result);
@@ -1046,6 +1068,7 @@ async function processMarkdownToMdxNodes(markdown) {
1046
1068
  const dedented = dedentComponentChildren(preprocessed);
1047
1069
  // Ensure component block integrity in the markdown
1048
1070
  const normalized = ensureComponentBlockIntegrity(dedented);
1071
+ const basePath = resolveDeploymentBasePath();
1049
1072
  const processor = unified()
1050
1073
  .use(remarkParse)
1051
1074
  .use(remarkGfm)
@@ -1054,6 +1077,9 @@ async function processMarkdownToMdxNodes(markdown) {
1054
1077
  .use(rehypeRaw)
1055
1078
  .use(rehypeSlug)
1056
1079
  .use(rehypeKatex);
1080
+ if (basePath) {
1081
+ processor.use(rehypeBasePath, { basePath });
1082
+ }
1057
1083
  const mdast = processor.parse(normalized);
1058
1084
  const hast = await processor.run(mdast);
1059
1085
  // 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.14",
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",