slidev-workspace 0.2.2 → 0.3.0

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/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # Slidev Workspace
2
+
3
+ [![npm version](https://badge.fury.io/js/slidev-workspace.svg)](https://badge.fury.io/js/slidev-workspace)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ Slidev Workspace is a specialized command-line tool designed to manage and showcase multiple [Slidev](https://sli.dev) presentations. It provides a unified web interface to browse, search, and access Slidev presentations distributed across different directories.
7
+
8
+ - **[Slidev Workspace Starter](https://github.com/leochiu-a/slidev-workspace-starter)** - Ready-to-use template
9
+ - **[Live Demo](https://leochiu-a.github.io/slidev-workspace-starter/)** - See it in action
10
+ - **[Documentation](https://leochiu-a.github.io/slidev-workspace/)**
11
+
12
+ ## Features
13
+
14
+ ✨ **Multi-presentation management** - Organize multiple Slidev presentations in one workspace
15
+ 📱 **Responsive interface** - Clean, modern UI for presentation management
16
+ 🔧 **Easy configuration** - Simple YAML-based configuration
17
+ 📦 **Build & Deploy** - Built-in commands for production builds
18
+ 🎨 **Thumbnail previews** - Visual presentation previews in the workspace
19
+
20
+ ## Usage
21
+
22
+ Slidev Workspace provides two flexible ways to work with your presentations:
23
+
24
+ ### Built-in Preview Interface
25
+
26
+ Use the command-line tool for an out-of-the-box solution:
27
+
28
+ ```bash
29
+ slidev-workspace preview
30
+ ```
31
+
32
+ This launches a responsive web interface with presentation management, search functionality, and thumbnail previews - perfect for users who want to get started quickly without building custom UI.
33
+
34
+ ### Content API
35
+
36
+ For developers who need custom UI integration, access slide data programmatically:
37
+
38
+ ```typescript
39
+ import { useSlides } from "slidev-workspace";
40
+
41
+ const { slides } = useSlides();
42
+ ```
43
+
44
+ The `useSlides` composable returns frontmatter data from all discovered presentations, enabling you to build entirely custom interfaces while leveraging Slidev Workspace's presentation discovery and parsing capabilities.
45
+
46
+ ## Quick Start
47
+
48
+ Get started in 5 minutes! See our [Quick Start Guide](https://leochiu-a.github.io/slidev-workspace/getting-started/quick-start.html).
49
+
50
+ ## Documentation
51
+
52
+ - 📚 [Quick Start Guide](https://leochiu-a.github.io/slidev-workspace/getting-started/quick-start.html) - Get up and running in 5 minutes
53
+ - 🚀 [Deployment Guide](https://leochiu-a.github.io/slidev-workspace/getting-started/deploy.html) - Deploy to GitHub Pages
54
+
55
+ ## Related Projects
56
+
57
+ - [Slidev](https://sli.dev) - Presentation slides for developers
58
+ - [Vue.js](https://vuejs.org) - The progressive JavaScript framework
59
+ - [Vite](https://vitejs.dev) - Next generation frontend tooling
package/dist/cli.js CHANGED
@@ -17,7 +17,11 @@ const DEFAULT_CONFIG = {
17
17
  slidesDir: ["./slides"],
18
18
  outputDir: "./dist",
19
19
  baseUrl: "/",
20
- exclude: ["node_modules", ".git"]
20
+ exclude: ["node_modules", ".git"],
21
+ hero: {
22
+ title: "Slide Deck",
23
+ description: "Browse all available slide decks and use the search function to quickly find what you need."
24
+ }
21
25
  };
22
26
  function loadConfig(workingDir) {
23
27
  const configPaths = [
@@ -60,6 +64,7 @@ function resolveSlidesDirs(config, workingDir) {
60
64
  //#region src/scripts/getSlideFrontmatter.ts
61
65
  function getSlideFrontmatterByPath(slideDir, slideName) {
62
66
  try {
67
+ const config = loadConfig();
63
68
  const fullPath = join$1(slideDir, slideName, "slides.md");
64
69
  if (!existsSync$1(fullPath)) {
65
70
  console.warn(`File not found: ${fullPath}`);
@@ -81,7 +86,8 @@ function getSlideFrontmatterByPath(slideDir, slideName) {
81
86
  fullPath,
82
87
  sourceDir: slideDir,
83
88
  frontmatter,
84
- content: content.replace(frontmatterMatch[0], "")
89
+ content: content.replace(frontmatterMatch[0], ""),
90
+ baseUrl: config.baseUrl
85
91
  };
86
92
  } catch (error) {
87
93
  console.error(`Error parsing frontmatter for ${slideName} in ${slideDir}:`, error);
@@ -238,7 +244,7 @@ function slidesPlugin() {
238
244
  });
239
245
  },
240
246
  resolveId(id) {
241
- if (id === "slidev:content") return id;
247
+ if (id === "slidev:content" || id === "slidev:config") return id;
242
248
  },
243
249
  load(id) {
244
250
  if (id === "slidev:content") try {
@@ -250,6 +256,16 @@ export default slidesData;`;
250
256
  return `export const slidesData = [];
251
257
  export default slidesData;`;
252
258
  }
259
+ if (id === "slidev:config") try {
260
+ const config = loadConfig();
261
+ const configData = { hero: config.hero };
262
+ return `export const configData = ${JSON.stringify(configData, null, 2)};
263
+ export default configData;`;
264
+ } catch (error) {
265
+ console.error("Error loading config:", error);
266
+ return `export const configData = { hero: {} };
267
+ export default configData;`;
268
+ }
253
269
  }
254
270
  };
255
271
  }
@@ -301,7 +317,9 @@ async function buildAllSlides() {
301
317
  console.log(`📦 Building slide: ${slideName}`);
302
318
  try {
303
319
  const baseUrl = config.baseUrl.endsWith("/") ? config.baseUrl : config.baseUrl + "/";
304
- const buildCmd = `pnpm --filter "./slides/${slideName}" run build --base ${baseUrl}${slideName}/`;
320
+ const subDir = slideDir.startsWith(workspaceCwd) ? slideDir.replace(workspaceCwd, "").replace(/^\//, "") : slideDir;
321
+ const buildCmd = `pnpm --filter "./${subDir}" run build --base ${baseUrl}${slideName}/`;
322
+ console.log(buildCmd);
305
323
  execSync(buildCmd, {
306
324
  cwd: workspaceCwd,
307
325
  stdio: "inherit"
package/dist/index.d.ts CHANGED
@@ -27,6 +27,8 @@ interface SlideInfo {
27
27
  sourceDir: string;
28
28
  frontmatter: SlideFrontmatter;
29
29
  content: string;
30
+ /** The base URL of the slide, which is defined in slidev-workspace.yml */
31
+ baseUrl: string;
30
32
  }
31
33
  interface SlideData {
32
34
  title: string;
@@ -49,11 +51,16 @@ declare function useSlides(): {
49
51
  };
50
52
  //#endregion
51
53
  //#region src/types/config.d.ts
54
+ interface HeroConfig {
55
+ title: string;
56
+ description: string;
57
+ }
52
58
  interface SlidevWorkspaceConfig {
53
59
  slidesDir: string[];
54
60
  outputDir: string;
55
61
  baseUrl: string;
56
62
  exclude: string[];
63
+ hero: HeroConfig;
57
64
  }
58
65
  //#endregion
59
66
  export { type SlideData, type SlideFrontmatter, type SlideInfo, type SlidevWorkspaceConfig, useSlides };
package/dist/index.js CHANGED
@@ -3,6 +3,33 @@ import { computed, ref } from "vue";
3
3
  //#region src/preview/constants/env.ts
4
4
  const IS_DEVELOPMENT = import.meta.env.MODE === "development";
5
5
 
6
+ //#endregion
7
+ //#region src/preview/lib/pathJoin.ts
8
+ /**
9
+ * Join multiple path segments into a single path
10
+ * - Removes duplicate slashes
11
+ * - Handles leading and trailing slashes properly
12
+ * - Works with both relative and absolute paths
13
+ *
14
+ * @example
15
+ * pathJoin('/base/', '/path/', 'file.jpg') // '/base/path/file.jpg'
16
+ * pathJoin('base', 'path', 'file.jpg') // 'base/path/file.jpg'
17
+ * pathJoin('/base/', 'path') // '/base/path'
18
+ */
19
+ function pathJoin(...segments) {
20
+ if (segments.length === 0) return "";
21
+ const filtered = segments.filter((segment) => segment !== "");
22
+ if (filtered.length === 0) return "";
23
+ const isAbsolute = filtered[0].startsWith("/");
24
+ const processed = filtered.map((segment) => {
25
+ return segment.replace(/^\/+|\/+$/g, "");
26
+ });
27
+ const nonEmpty = processed.filter((segment) => segment !== "");
28
+ if (nonEmpty.length === 0) return isAbsolute ? "/" : "";
29
+ const joined = nonEmpty.join("/");
30
+ return isAbsolute ? `/${joined}` : joined;
31
+ }
32
+
6
33
  //#endregion
7
34
  //#region src/preview/composables/useSlides.ts
8
35
  /**
@@ -18,19 +45,33 @@ function isUrl(str) {
18
45
  }
19
46
  }
20
47
  /**
21
- * Resolve background image path
22
- * If the background is not a URL, construct the full path using slide.path as base
48
+ * Resolve background image path.
49
+ * If the background is not a URL, construct the full path using slide.path as the base.
50
+ *
51
+ * Example (development mode):
52
+ * {
53
+ * background: "/bg1.jpg",
54
+ * slidePath: "/slides-presentation-1",
55
+ * baseUrl: "/slidev-workspace-starter/",
56
+ * domain: "http://localhost:3001" // domain of the current server
57
+ * }
58
+ * returns: "http://localhost:3001/slides-presentation-1/bg1.jpg"
59
+ *
60
+ * Example (production mode):
61
+ * {
62
+ * background: "/bg1.jpg",
63
+ * slidePath: "/slides-presentation-1",
64
+ * baseUrl: "/slidev-workspace-starter/",
65
+ * domain: "https://my-slides.com" // domain of the current server
66
+ * }
67
+ * returns: "https://my-slides.com/slidev-workspace-starter/slides-presentation-1/bg1.jpg"
23
68
  */
24
69
  function resolveBackgroundPath(params) {
25
- const { background, slidePath, devServerUrl } = params;
70
+ const { background, slidePath, domain, baseUrl } = params;
26
71
  if (!background) return "";
27
72
  if (isUrl(background)) return background;
28
73
  try {
29
- const domain = IS_DEVELOPMENT ? devServerUrl : window.location.origin;
30
- const basePath = slidePath.endsWith("/") ? slidePath : slidePath + "/";
31
- const relativeBg = background.startsWith("/") ? background.slice(1) : background;
32
- const resolvedUrl = new URL(basePath + relativeBg, domain);
33
- return resolvedUrl.href;
74
+ return IS_DEVELOPMENT ? new URL(pathJoin(slidePath, background), domain).href : new URL(pathJoin(baseUrl, slidePath, background), domain).href;
34
75
  } catch (error) {
35
76
  console.error("Failed to resolve background path:", error);
36
77
  return background;
@@ -60,7 +101,8 @@ function useSlides() {
60
101
  const imageUrl = background ? resolveBackgroundPath({
61
102
  background: slide.frontmatter.background,
62
103
  slidePath: slide.path,
63
- devServerUrl
104
+ baseUrl: slide.baseUrl,
105
+ domain: IS_DEVELOPMENT ? devServerUrl : window.location.origin
64
106
  }) : "https://cover.sli.dev";
65
107
  return {
66
108
  title: slide.frontmatter.title || slide.path,
@@ -8,7 +8,11 @@ const DEFAULT_CONFIG = {
8
8
  slidesDir: ["./slides"],
9
9
  outputDir: "./dist",
10
10
  baseUrl: "/",
11
- exclude: ["node_modules", ".git"]
11
+ exclude: ["node_modules", ".git"],
12
+ hero: {
13
+ title: "Slide Deck",
14
+ description: "Browse all available slide decks and use the search function to quickly find what you need."
15
+ }
12
16
  };
13
17
  function loadConfig(workingDir) {
14
18
  const configPaths = [
@@ -51,6 +55,7 @@ function resolveSlidesDirs(config, workingDir) {
51
55
  //#region src/scripts/getSlideFrontmatter.ts
52
56
  function getSlideFrontmatterByPath(slideDir, slideName) {
53
57
  try {
58
+ const config = loadConfig();
54
59
  const fullPath = join(slideDir, slideName, "slides.md");
55
60
  if (!existsSync(fullPath)) {
56
61
  console.warn(`File not found: ${fullPath}`);
@@ -72,7 +77,8 @@ function getSlideFrontmatterByPath(slideDir, slideName) {
72
77
  fullPath,
73
78
  sourceDir: slideDir,
74
79
  frontmatter,
75
- content: content.replace(frontmatterMatch[0], "")
80
+ content: content.replace(frontmatterMatch[0], ""),
81
+ baseUrl: config.baseUrl
76
82
  };
77
83
  } catch (error) {
78
84
  console.error(`Error parsing frontmatter for ${slideName} in ${slideDir}:`, error);
@@ -229,7 +235,7 @@ function slidesPlugin() {
229
235
  });
230
236
  },
231
237
  resolveId(id) {
232
- if (id === "slidev:content") return id;
238
+ if (id === "slidev:content" || id === "slidev:config") return id;
233
239
  },
234
240
  load(id) {
235
241
  if (id === "slidev:content") try {
@@ -241,6 +247,16 @@ export default slidesData;`;
241
247
  return `export const slidesData = [];
242
248
  export default slidesData;`;
243
249
  }
250
+ if (id === "slidev:config") try {
251
+ const config = loadConfig();
252
+ const configData = { hero: config.hero };
253
+ return `export const configData = ${JSON.stringify(configData, null, 2)};
254
+ export default configData;`;
255
+ } catch (error) {
256
+ console.error("Error loading config:", error);
257
+ return `export const configData = { hero: {} };
258
+ export default configData;`;
259
+ }
244
260
  }
245
261
  };
246
262
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidev-workspace",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "A workspace tool for managing multiple Slidev presentations with API-based content management",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -4,10 +4,9 @@
4
4
  >
5
5
  <div class="max-w-5xl mx-auto">
6
6
  <div class="mb-8">
7
- <h1 class="text-3xl font-bold mb-2">Slide Deck</h1>
7
+ <h1 class="text-3xl font-bold mb-2">{{ hero.title }}</h1>
8
8
  <p class="text-muted-foreground">
9
- Browse all available slide decks and use the search function to
10
- quickly find what you need.
9
+ {{ hero.description }}
11
10
  </p>
12
11
  </div>
13
12
 
@@ -52,11 +51,13 @@
52
51
  <script setup lang="ts">
53
52
  import { ref, computed } from "vue";
54
53
  import { useSlides } from "../composables/useSlides";
54
+ import { useConfig } from "../composables/useConfig";
55
55
  import { Input } from "../components/ui/input";
56
56
  import SlideCard from "./SlideCard.vue";
57
57
 
58
58
  const searchTerm = ref("");
59
59
  const { slides, slidesCount } = useSlides();
60
+ const { hero } = useConfig();
60
61
 
61
62
  const filteredSlides = computed(() => {
62
63
  if (!searchTerm.value) return slides.value;
@@ -0,0 +1,15 @@
1
+ import type { HeroConfig } from "../../../types/config";
2
+
3
+ interface ConfigData {
4
+ hero: HeroConfig;
5
+ }
6
+
7
+ const mockConfigData: ConfigData = {
8
+ hero: {
9
+ title: "Slide Deck",
10
+ description:
11
+ "Browse all available slide decks and use the search function to quickly find what you need.",
12
+ },
13
+ };
14
+
15
+ export default mockConfigData;
@@ -17,6 +17,7 @@ const mockSlidesData: SlideInfo[] = [
17
17
  class: "text-center",
18
18
  },
19
19
  content: "# Slide content",
20
+ baseUrl: "/slidev-workspace-starter/",
20
21
  },
21
22
  {
22
23
  id: "slide-2",
@@ -32,6 +33,7 @@ const mockSlidesData: SlideInfo[] = [
32
33
  },
33
34
  },
34
35
  content: "# Another slide",
36
+ baseUrl: "/slidev-workspace-starter/",
35
37
  },
36
38
  {
37
39
  id: "slide-3",
@@ -40,6 +42,7 @@ const mockSlidesData: SlideInfo[] = [
40
42
  sourceDir: "/path/to/slides",
41
43
  frontmatter: {},
42
44
  content: "# Minimal slide",
45
+ baseUrl: "/slidev-workspace-starter/",
43
46
  },
44
47
  ];
45
48
 
@@ -0,0 +1,158 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2
+
3
+ describe("useConfig", () => {
4
+ beforeEach(() => {
5
+ vi.clearAllMocks();
6
+ });
7
+
8
+ afterEach(() => {
9
+ vi.restoreAllMocks();
10
+ });
11
+
12
+ /**
13
+ * Helper function to setup useConfig and wait for data to load
14
+ */
15
+ async function setupUseConfig(configData?: {
16
+ hero: { title: string; description: string };
17
+ }) {
18
+ vi.resetModules();
19
+
20
+ // Mock the virtual module
21
+ vi.doMock("slidev:config", () => ({
22
+ default: configData || {
23
+ hero: {
24
+ title: "Slide Deck",
25
+ description:
26
+ "Browse all available slide decks and use the search function to quickly find what you need.",
27
+ },
28
+ },
29
+ }));
30
+
31
+ const { useConfig } = await import("./useConfig");
32
+ const result = useConfig();
33
+
34
+ // Wait for data to finish loading
35
+ await vi.waitFor(
36
+ () => {
37
+ // Check if config has loaded by comparing with expected values
38
+ if (configData) {
39
+ expect(result.hero.value.title).toBe(configData.hero.title);
40
+ } else {
41
+ expect(result.hero.value.title).toBe("Slide Deck");
42
+ }
43
+ },
44
+ { timeout: 1000, interval: 10 },
45
+ );
46
+
47
+ return result;
48
+ }
49
+
50
+ describe("hero config", () => {
51
+ it("should return hero config from slidev:config", async () => {
52
+ const result = await setupUseConfig({
53
+ hero: {
54
+ title: "Test Workspace",
55
+ description: "Test Description",
56
+ },
57
+ });
58
+
59
+ expect(result.hero.value.title).toBe("Test Workspace");
60
+ expect(result.hero.value.description).toBe("Test Description");
61
+ });
62
+
63
+ it("should provide reactive hero config", async () => {
64
+ const result = await setupUseConfig({
65
+ hero: {
66
+ title: "Initial Title",
67
+ description: "Initial Description",
68
+ },
69
+ });
70
+
71
+ // Initial values
72
+ expect(result.hero.value.title).toBe("Initial Title");
73
+ expect(result.hero.value.description).toBe("Initial Description");
74
+
75
+ // Hero should be reactive
76
+ expect(result.hero.value).toBeDefined();
77
+ expect(typeof result.hero.value).toBe("object");
78
+ });
79
+
80
+ it("should return default hero values when no config provided", async () => {
81
+ const result = await setupUseConfig();
82
+
83
+ expect(result.hero.value.title).toBe("Slide Deck");
84
+ expect(result.hero.value.description).toContain("Browse all available");
85
+ });
86
+ });
87
+
88
+ describe("HeroConfig properties", () => {
89
+ it("should have title and description properties", async () => {
90
+ const result = await setupUseConfig({
91
+ hero: {
92
+ title: "Test Title",
93
+ description: "Test Description",
94
+ },
95
+ });
96
+
97
+ const hero = result.hero.value;
98
+
99
+ expect("title" in hero).toBe(true);
100
+ expect("description" in hero).toBe(true);
101
+ expect(typeof hero.title).toBe("string");
102
+ expect(typeof hero.description).toBe("string");
103
+ });
104
+
105
+ it("should handle long descriptions", async () => {
106
+ const longDescription =
107
+ "This is a very long description that could contain multiple sentences and paragraphs to describe the workspace";
108
+
109
+ const result = await setupUseConfig({
110
+ hero: {
111
+ title: "My Workspace",
112
+ description: longDescription,
113
+ },
114
+ });
115
+
116
+ expect(result.hero.value.description).toBe(longDescription);
117
+ expect(result.hero.value.description.length).toBeGreaterThan(50);
118
+ });
119
+
120
+ it("should handle special characters in title and description", async () => {
121
+ const result = await setupUseConfig({
122
+ hero: {
123
+ title: "My Slides & Presentations",
124
+ description: "Browse <all> available slides @ our company",
125
+ },
126
+ });
127
+
128
+ expect(result.hero.value.title).toContain("&");
129
+ expect(result.hero.value.description).toContain("<");
130
+ expect(result.hero.value.description).toContain("@");
131
+ });
132
+ });
133
+
134
+ describe("error handling", () => {
135
+ it("should use default values when import fails", async () => {
136
+ vi.resetModules();
137
+
138
+ // Mock import to throw error
139
+ vi.doMock("slidev:config", () => {
140
+ throw new Error("Failed to load config");
141
+ });
142
+
143
+ const { useConfig } = await import("./useConfig");
144
+ const result = useConfig();
145
+
146
+ // Should fall back to default values
147
+ await vi.waitFor(
148
+ () => {
149
+ expect(result.hero.value.title).toBe("Slide Deck");
150
+ expect(result.hero.value.description).toContain(
151
+ "Browse all available",
152
+ );
153
+ },
154
+ { timeout: 1000, interval: 10 },
155
+ );
156
+ });
157
+ });
158
+ });
@@ -0,0 +1,31 @@
1
+ import { computed, ref } from "vue";
2
+ import type { HeroConfig } from "../../types/config.js";
3
+
4
+ const DEFAULT_CONFIG = {
5
+ hero: {
6
+ title: "Slide Deck",
7
+ description:
8
+ "Browse all available slide decks and use the search function to quickly find what you need.",
9
+ },
10
+ };
11
+
12
+ export function useConfig() {
13
+ const heroData = ref<HeroConfig>(DEFAULT_CONFIG.hero);
14
+
15
+ const loadConfigData = async () => {
16
+ try {
17
+ const module = await import("slidev:config");
18
+ heroData.value = module.default?.hero || heroData.value;
19
+ } catch (error) {
20
+ console.warn("Failed to load config data:", error);
21
+ }
22
+ };
23
+
24
+ loadConfigData();
25
+
26
+ const hero = computed(() => heroData.value);
27
+
28
+ return {
29
+ hero,
30
+ };
31
+ }
@@ -201,7 +201,7 @@ describe("useSlides (Production Mode)", () => {
201
201
  const firstSlide = result.slides.value[0];
202
202
 
203
203
  expect(firstSlide.image).toBe(
204
- "https://my-slides.com/slides-presentation-1/bg1.jpg",
204
+ "https://my-slides.com/slidev-workspace-starter/slides-presentation-1/bg1.jpg",
205
205
  );
206
206
  });
207
207
 
@@ -1,6 +1,7 @@
1
1
  import { computed, ref } from "vue";
2
2
  import type { SlideData, SlideInfo } from "../../types/slide";
3
3
  import { IS_DEVELOPMENT } from "../constants/env";
4
+ import { pathJoin } from "../lib/pathJoin";
4
5
 
5
6
  /**
6
7
  * Check if a string is a valid URL
@@ -17,33 +18,47 @@ function isUrl(str: string | undefined): boolean {
17
18
  }
18
19
 
19
20
  /**
20
- * Resolve background image path
21
- * If the background is not a URL, construct the full path using slide.path as base
21
+ * Resolve background image path.
22
+ * If the background is not a URL, construct the full path using slide.path as the base.
23
+ *
24
+ * Example (development mode):
25
+ * {
26
+ * background: "/bg1.jpg",
27
+ * slidePath: "/slides-presentation-1",
28
+ * baseUrl: "/slidev-workspace-starter/",
29
+ * domain: "http://localhost:3001" // domain of the current server
30
+ * }
31
+ * returns: "http://localhost:3001/slides-presentation-1/bg1.jpg"
32
+ *
33
+ * Example (production mode):
34
+ * {
35
+ * background: "/bg1.jpg",
36
+ * slidePath: "/slides-presentation-1",
37
+ * baseUrl: "/slidev-workspace-starter/",
38
+ * domain: "https://my-slides.com" // domain of the current server
39
+ * }
40
+ * returns: "https://my-slides.com/slidev-workspace-starter/slides-presentation-1/bg1.jpg"
22
41
  */
23
42
  function resolveBackgroundPath(params: {
24
43
  background: string | undefined;
25
44
  slidePath: string;
26
- devServerUrl: string;
45
+ baseUrl: string;
46
+ domain: string;
27
47
  }): string {
28
- const { background, slidePath, devServerUrl } = params;
48
+ const { background, slidePath, domain, baseUrl } = params;
29
49
 
30
- if (!background) return "";
50
+ if (!background) {
51
+ return "";
52
+ }
31
53
 
32
54
  if (isUrl(background)) {
33
55
  return background;
34
56
  }
35
57
 
36
58
  try {
37
- const domain = IS_DEVELOPMENT ? devServerUrl : window.location.origin;
38
- // Ensure slidePath ends with / for proper path resolution
39
- const basePath = slidePath.endsWith("/") ? slidePath : slidePath + "/";
40
- // Remove leading / from background to treat it as relative
41
- const relativeBg = background.startsWith("/")
42
- ? background.slice(1)
43
- : background;
44
- const resolvedUrl = new URL(basePath + relativeBg, domain);
45
-
46
- return resolvedUrl.href;
59
+ return IS_DEVELOPMENT
60
+ ? new URL(pathJoin(slidePath, background), domain).href
61
+ : new URL(pathJoin(baseUrl, slidePath, background), domain).href;
47
62
  } catch (error) {
48
63
  console.error("Failed to resolve background path:", error);
49
64
  return background;
@@ -86,7 +101,8 @@ export function useSlides() {
86
101
  ? resolveBackgroundPath({
87
102
  background: slide.frontmatter.background,
88
103
  slidePath: slide.path,
89
- devServerUrl,
104
+ baseUrl: slide.baseUrl,
105
+ domain: IS_DEVELOPMENT ? devServerUrl : window.location.origin,
90
106
  })
91
107
  : "https://cover.sli.dev";
92
108
 
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { pathJoin } from "./pathJoin";
3
+
4
+ describe("pathJoin", () => {
5
+ describe("basic functionality", () => {
6
+ it("should join simple path segments", () => {
7
+ expect(pathJoin("a", "b", "c")).toBe("a/b/c");
8
+ });
9
+
10
+ it("should handle single segment", () => {
11
+ expect(pathJoin("path")).toBe("path");
12
+ });
13
+
14
+ it("should return empty string for no arguments", () => {
15
+ expect(pathJoin()).toBe("");
16
+ });
17
+
18
+ it("should return empty string for all empty segments", () => {
19
+ expect(pathJoin("", "", "")).toBe("");
20
+ });
21
+ });
22
+
23
+ describe("leading slashes", () => {
24
+ it("should preserve leading slash for absolute paths", () => {
25
+ expect(pathJoin("/a", "b", "c")).toBe("/a/b/c");
26
+ });
27
+
28
+ it("should not add leading slash for relative paths", () => {
29
+ expect(pathJoin("a", "b", "c")).toBe("a/b/c");
30
+ });
31
+
32
+ it("should preserve leading slash with trailing slashes", () => {
33
+ expect(pathJoin("/a/", "b/", "c/")).toBe("/a/b/c");
34
+ });
35
+ });
36
+
37
+ describe("trailing slashes", () => {
38
+ it("should remove trailing slashes from segments", () => {
39
+ expect(pathJoin("a/", "b/", "c/")).toBe("a/b/c");
40
+ });
41
+
42
+ it("should handle segments with both leading and trailing slashes", () => {
43
+ expect(pathJoin("/a/", "/b/", "/c/")).toBe("/a/b/c");
44
+ });
45
+ });
46
+
47
+ describe("duplicate slashes", () => {
48
+ it("should remove duplicate slashes between segments", () => {
49
+ expect(pathJoin("a//", "//b", "c")).toBe("a/b/c");
50
+ });
51
+
52
+ it("should handle multiple consecutive slashes", () => {
53
+ expect(pathJoin("a///", "///b///", "///c")).toBe("a/b/c");
54
+ });
55
+
56
+ it("should handle absolute paths with multiple slashes", () => {
57
+ expect(pathJoin("///a", "b", "c")).toBe("/a/b/c");
58
+ });
59
+ });
60
+
61
+ describe("empty segments", () => {
62
+ it("should filter out empty segments", () => {
63
+ expect(pathJoin("a", "", "b", "", "c")).toBe("a/b/c");
64
+ });
65
+
66
+ it("should handle empty segments with slashes", () => {
67
+ expect(pathJoin("/a", "", "/b", "", "c")).toBe("/a/b/c");
68
+ });
69
+ });
70
+
71
+ describe("real-world use cases", () => {
72
+ it("should join base URL and relative path", () => {
73
+ expect(pathJoin("/base/", "/path/", "file.jpg")).toBe(
74
+ "/base/path/file.jpg",
75
+ );
76
+ });
77
+
78
+ it("should join without leading slash", () => {
79
+ expect(pathJoin("base", "path", "file.jpg")).toBe("base/path/file.jpg");
80
+ });
81
+
82
+ it("should join base and path segments", () => {
83
+ expect(pathJoin("/base/", "path")).toBe("/base/path");
84
+ });
85
+
86
+ it("should handle slidev-workspace paths", () => {
87
+ expect(
88
+ pathJoin(
89
+ "/slidev-workspace-starter",
90
+ "/slides-presentation-1/",
91
+ "bg1.jpg",
92
+ ),
93
+ ).toBe("/slidev-workspace-starter/slides-presentation-1/bg1.jpg");
94
+ });
95
+
96
+ it("should handle relative paths for slide backgrounds", () => {
97
+ expect(pathJoin("slides-presentation-1/", "bg1.jpg")).toBe(
98
+ "slides-presentation-1/bg1.jpg",
99
+ );
100
+ });
101
+ });
102
+
103
+ describe("edge cases", () => {
104
+ it("should handle only slashes", () => {
105
+ expect(pathJoin("/", "/", "/")).toBe("/");
106
+ });
107
+
108
+ it("should handle single slash", () => {
109
+ expect(pathJoin("/")).toBe("/");
110
+ });
111
+
112
+ it("should handle path with dots", () => {
113
+ expect(pathJoin("path", "to", "../file.jpg")).toBe("path/to/../file.jpg");
114
+ });
115
+
116
+ it("should handle file extensions", () => {
117
+ expect(pathJoin("/path", "file.name.ext")).toBe("/path/file.name.ext");
118
+ });
119
+ });
120
+ });
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Join multiple path segments into a single path
3
+ * - Removes duplicate slashes
4
+ * - Handles leading and trailing slashes properly
5
+ * - Works with both relative and absolute paths
6
+ *
7
+ * @example
8
+ * pathJoin('/base/', '/path/', 'file.jpg') // '/base/path/file.jpg'
9
+ * pathJoin('base', 'path', 'file.jpg') // 'base/path/file.jpg'
10
+ * pathJoin('/base/', 'path') // '/base/path'
11
+ */
12
+ export function pathJoin(...segments: string[]): string {
13
+ if (segments.length === 0) return "";
14
+
15
+ // Filter out empty segments
16
+ const filtered = segments.filter((segment) => segment !== "");
17
+ if (filtered.length === 0) return "";
18
+
19
+ // Check if path should start with /
20
+ const isAbsolute = filtered[0].startsWith("/");
21
+
22
+ // Process each segment: remove leading and trailing slashes
23
+ const processed = filtered.map((segment) => {
24
+ return segment.replace(/^\/+|\/+$/g, "");
25
+ });
26
+
27
+ // Filter out empty strings after processing
28
+ const nonEmpty = processed.filter((segment) => segment !== "");
29
+
30
+ if (nonEmpty.length === 0) {
31
+ return isAbsolute ? "/" : "";
32
+ }
33
+
34
+ // Join with single slash
35
+ const joined = nonEmpty.join("/");
36
+
37
+ // Add leading slash if original path was absolute
38
+ return isAbsolute ? `/${joined}` : joined;
39
+ }