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 +59 -0
- package/dist/cli.js +22 -4
- package/dist/index.d.ts +7 -0
- package/dist/index.js +51 -9
- package/dist/plugin-slides.js +19 -3
- package/package.json +1 -1
- package/src/preview/components/SlideDeck.vue +4 -3
- package/src/preview/composables/__mocks__/slidev-config.ts +15 -0
- package/src/preview/composables/__mocks__/slidev-content.ts +3 -0
- package/src/preview/composables/useConfig.test.ts +158 -0
- package/src/preview/composables/useConfig.ts +31 -0
- package/src/preview/composables/useSlides.test.ts +1 -1
- package/src/preview/composables/useSlides.ts +32 -16
- package/src/preview/lib/pathJoin.test.ts +120 -0
- package/src/preview/lib/pathJoin.ts +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Slidev Workspace
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/slidev-workspace)
|
|
4
|
+
[](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
|
|
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,
|
|
70
|
+
const { background, slidePath, domain, baseUrl } = params;
|
|
26
71
|
if (!background) return "";
|
|
27
72
|
if (isUrl(background)) return background;
|
|
28
73
|
try {
|
|
29
|
-
|
|
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
|
-
|
|
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,
|
package/dist/plugin-slides.js
CHANGED
|
@@ -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
|
@@ -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">
|
|
7
|
+
<h1 class="text-3xl font-bold mb-2">{{ hero.title }}</h1>
|
|
8
8
|
<p class="text-muted-foreground">
|
|
9
|
-
|
|
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
|
-
|
|
45
|
+
baseUrl: string;
|
|
46
|
+
domain: string;
|
|
27
47
|
}): string {
|
|
28
|
-
const { background, slidePath,
|
|
48
|
+
const { background, slidePath, domain, baseUrl } = params;
|
|
29
49
|
|
|
30
|
-
if (!background)
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
+
}
|