feature: vor und nachteile

This commit is contained in:
Hikyu
2026-05-31 13:54:31 +02:00
parent 552a947a1c
commit e3dad17446
23270 changed files with 3024794 additions and 200 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2021 Anthony Fu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+103
View File
@@ -0,0 +1,103 @@
<br>
<p align="center">
<a href="https://sli.dev" target="_blank">
<img src="https://sli.dev/logo-title.png" alt="Slidev" height="250" width="250"/>
</a>
</p>
<p align="center">
Presentation <b>slide</b>s for <b>dev</b>elopers 🧑‍💻👩‍💻👨‍💻
</p>
<p align="center">
<a href="https://www.npmjs.com/package/@slidev/cli" target="__blank"><img src="https://img.shields.io/npm/v/@slidev/cli?color=2B90B6&label=" alt="NPM version"></a>
<a href="https://www.npmjs.com/package/@slidev/cli" target="__blank"><img alt="NPM Downloads" src="https://img.shields.io/npm/dm/@slidev/cli?color=349dbe&label="></a>
<a href="https://sli.dev/" target="__blank"><img src="https://img.shields.io/static/v1?label=&message=docs%20%26%20demos&color=45b8cd" alt="Docs & Demos"></a>
<a href="https://sli.dev/resources/theme-gallery" target="__blank"><img src="https://img.shields.io/static/v1?label=&message=themes&color=4ec5d4" alt="Themes"></a>
<br>
<a href="https://github.com/slidevjs/slidev" target="__blank"><img alt="GitHub stars" src="https://img.shields.io/github/stars/slidevjs/slidev?style=social"></a>
</p>
<p align="center">
<a href="https://twitter.com/antfu7/status/1389604687502995457">Video Preview</a> | <a href="https://sli.dev">Documentation</a>
</p>
<div align="center">
<table>
<tbody>
<td align="center">
<img width="2000" height="0" alt="" aria-hiden><br>
<sub>Made possible by my <a href="https://github.com/sponsors/antfu">Sponsor Program 💖</a></sub><br>
<img width="2000" height="0" alt="" aria-hiden>
</td>
</tbody>
</table>
</div>
## Features
- 📝 [**Markdown-based**](https://sli.dev/guide/syntax) - foucus on content and use your favorite editor
- 🧑‍💻 [**Developer Friendly**](https://sli.dev/guide/syntax#code-blocks) - built-in code highlighting, live coding, etc.
- 🎨 [**Themable**](https://sli.dev/resources/theme-gallery) - theme can be shared and used with npm packages
- 🌈 [**Stylish**](https://sli.dev/guide/syntax#embedded-styles) - on-demand utilities via [UnoCSS](https://github.com/unocss/unocss).
- 🤹 [**Interactive**](https://sli.dev/custom/directory-structure#components) - embedding Vue components seamlessly
- 🎙 [**Presenter Mode**](https://sli.dev/guide/ui#presenter-mode) - use another window, or even your phone to control your slides
- 🎨 [**Drawing**](https://sli.dev/features/drawing) - draw and annotate on your slides
- 🧮 [**LaTeX**](https://sli.dev/features/latex) - built-in LaTeX math equations support
- 📰 [**Diagrams**](https://sli.dev/guide/syntax#diagrams) - creates diagrams using textual descriptions with [Mermaid.js](https://mermaid.js.org/)
- 🌟 [**Icons**](https://sli.dev/features/icons) - access to icons from any icon set directly
- 💻 [**Editor**](https://sli.dev/guide/index#editor) - integrated editor, or the [VSCode extension](https://sli.dev/features/vscode-extension)
- 🎥 [**Recording**](https://sli.dev/features/recording) - built-in recording and camera view
- 📤 [**Portable**](https://sli.dev/guide/exporting) - export into PDF, PNGs, or PPTX
- ⚡️ [**Fast**](https://vitejs.dev) - instant reloading powered by [Vite](https://vitejs.dev)
- 🛠 [**Hackable**](https://sli.dev/custom/) - using Vite plugins, Vue components, or any npm packages
## Getting Started
### Try it Online ⚡️
[sli.dev/new](https://sli.dev/new)
[![](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://sli.dev/new)
### Init Project Locally
Install [Node.js >=18](https://nodejs.org/) and run the following command:
```bash
npm init slidev
```
Documentation:
**[English](https://sli.dev)** | [中文文档](https://cn.sli.dev) | [Français](https://fr.sli.dev) | [Español](https://es.sli.dev) | [Русский](https://ru.sli.dev) | [Português-BR](https://br.sli.dev)
Discord: [chat.sli.dev](https://chat.sli.dev)
For a full example, you can check the [demo](https://github.com/slidevjs/slidev/blob/main/demo) folder, which is also the source file for [my previous talk](https://antfu.me/posts/composable-vue-vueday-2021).
## Tech Stack
- [Vite](https://vitejs.dev) - An extremely fast frontend tooling
- [Vue 3](https://v3.vuejs.org/) powered [Markdown](https://daringfireball.net/projects/markdown/syntax) - Focus on the content while having the power of HTML and Vue components whenever needed
- [UnoCSS](https://github.com/unocss/unocss) - On-demand utility-first CSS engine, style your slides at ease
- [Shiki](https://github.com/shikijs/shiki), [Monaco Editor](https://github.com/Microsoft/monaco-editor) - First-class code snippets support with live coding capability
- [RecordRTC](https://recordrtc.org) - Built-in recording and camera view
- [VueUse](https://vueuse.org) family - [`@vueuse/core`](https://github.com/vueuse/vueuse), [`@vueuse/motion`](https://github.com/vueuse/motion), etc.
- [Iconify](https://iconify.design/) - Icon sets collection.
- [Drauu](https://github.com/antfu/drauu) - Drawing and annotations support
- [KaTeX](https://katex.org/) - LaTeX math rendering.
- [Mermaid](https://mermaid-js.github.io/mermaid) - Textual Diagrams.
## Sponsors
This project is made possible by all the sponsors supporting my work:
<p align="center">
<a href="https://github.com/sponsors/antfu">
<img src='https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg' alt="Logos from Sponsors" />
</a>
</p>
## License
MIT License © 2021 [Anthony Fu](https://github.com/antfu)
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env node
'use strict'
import ('../dist/cli.js')
+76
View File
@@ -0,0 +1,76 @@
import {
resolveViteConfigs
} from "./chunk-7E4FQJTV.js";
import "./chunk-UNQ5DBLZ.js";
// node/commands/build.ts
import http from "node:http";
import { resolve } from "node:path";
import connect from "connect";
import fs from "fs-extra";
import sirv from "sirv";
import { build as viteBuild } from "vite";
async function build(options, viteConfig = {}, args) {
const indexPath = resolve(options.userRoot, "index.html");
let originalIndexHTML;
if (fs.existsSync(indexPath))
originalIndexHTML = await fs.readFile(indexPath, "utf-8");
await fs.writeFile(indexPath, options.utils.indexHtml, "utf-8");
let config = void 0;
try {
const inlineConfig = await resolveViteConfigs(
options,
{
plugins: [
{
name: "resolve-config",
configResolved(_config) {
config = _config;
}
}
],
build: {
chunkSizeWarningLimit: 2e3
}
},
viteConfig,
"build"
);
await viteBuild(inlineConfig);
} finally {
if (originalIndexHTML != null)
await fs.writeFile(indexPath, originalIndexHTML, "utf-8");
else
await fs.unlink(indexPath);
}
const outDir = resolve(options.userRoot, config.build.outDir);
await fs.copyFile(resolve(outDir, "index.html"), resolve(outDir, "404.html"));
const redirectsPath = resolve(outDir, "_redirects");
if (!fs.existsSync(redirectsPath))
await fs.writeFile(redirectsPath, `${config.base}* ${config.base}index.html 200
`, "utf-8");
if ([true, "true", "auto"].includes(options.data.config.download)) {
const { exportSlides, getExportOptions } = await import("./export-I2C456L6.js");
const port = 12445;
const app = connect();
const server = http.createServer(app);
app.use(
config.base,
sirv(outDir, {
etag: true,
single: true,
dev: true
})
);
server.listen(port);
await exportSlides({
port,
base: config.base,
...getExportOptions(args, options, outDir, "slidev-exported.pdf")
});
server.close();
}
}
export {
build
};
File diff suppressed because it is too large Load Diff
+190
View File
@@ -0,0 +1,190 @@
// node/resolver.ts
import * as fs from "node:fs";
import { dirname, join, relative, resolve } from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
import { parseNi, run } from "@antfu/ni";
import { ensurePrefix, slash } from "@antfu/utils";
import globalDirs from "global-directory";
import { underline, yellow } from "kolorist";
import { resolvePath } from "mlly";
import prompts from "prompts";
import { resolveGlobal } from "resolve-global";
import { findClosestPkgJsonPath, findDepPkgJsonPath } from "vitefu";
var cliRoot = fileURLToPath(new URL("..", import.meta.url));
var isInstalledGlobally = {};
async function resolveImportUrl(id) {
return toAtFS(await resolveImportPath(id, true));
}
function toAtFS(path) {
return `/@fs${ensurePrefix("/", slash(path))}`;
}
async function resolveImportPath(importName, ensure = false) {
try {
return await resolvePath(importName, {
url: import.meta.url
});
} catch {
}
if (isInstalledGlobally.value) {
try {
return resolveGlobal(importName);
} catch {
}
}
if (ensure)
throw new Error(`Failed to resolve package "${importName}"`);
}
async function findPkgRoot(dep, parent, ensure = false) {
const pkgJsonPath = await findDepPkgJsonPath(dep, parent);
const path = pkgJsonPath ? dirname(pkgJsonPath) : isInstalledGlobally.value ? await findGlobalPkgRoot(dep, false) : void 0;
if (ensure && !path)
throw new Error(`Failed to resolve package "${dep}"`);
return path;
}
async function findGlobalPkgRoot(name, ensure = false) {
const yarnPath = join(globalDirs.yarn.packages, name);
if (fs.existsSync(`${yarnPath}/package.json`))
return yarnPath;
const npmPath = join(globalDirs.npm.packages, name);
if (fs.existsSync(`${npmPath}/package.json`))
return npmPath;
if (ensure)
throw new Error(`Failed to resolve global package "${name}"`);
}
async function resolveEntry(entryRaw) {
if (!fs.existsSync(entryRaw) && !entryRaw.endsWith(".md") && !/[/\\]/.test(entryRaw))
entryRaw += ".md";
const entry = resolve(entryRaw);
if (!fs.existsSync(entry)) {
const { create } = await prompts({
name: "create",
type: "confirm",
initial: "Y",
message: `Entry file ${yellow(`"${entry}"`)} does not exist, do you want to create it?`
});
if (create)
fs.copyFileSync(resolve(cliRoot, "template.md"), entry);
else
process.exit(0);
}
return slash(entry);
}
function createResolver(type, officials) {
async function promptForInstallation(pkgName) {
const { confirm } = await prompts({
name: "confirm",
initial: "Y",
type: "confirm",
message: `The ${type} "${pkgName}" was not found ${underline(isInstalledGlobally.value ? "globally" : "in your project")}, do you want to install it now?`
});
if (!confirm)
process.exit(1);
if (isInstalledGlobally.value)
await run(parseNi, ["-g", pkgName]);
else
await run(parseNi, [pkgName]);
}
return async function(name, importer) {
const { userRoot } = await getRoots();
if (name === "none")
return ["", null];
if (name[0] === "/")
return [name, name];
if (name.startsWith("@/"))
return [name, resolve(userRoot, name.slice(2))];
if (name[0] === "." || name[0] !== "@" && name.includes("/"))
return [name, resolve(dirname(importer), name)];
if (name.startsWith(`@slidev/${type}-`) || name.startsWith(`slidev-${type}-`)) {
const pkgRoot = await findPkgRoot(name, importer);
if (!pkgRoot)
await promptForInstallation(name);
return [name, await findPkgRoot(name, importer, true)];
}
{
const possiblePkgNames = [
`@slidev/${type}-${name}`,
`slidev-${type}-${name}`,
name
];
for (const pkgName2 of possiblePkgNames) {
const pkgRoot = await findPkgRoot(pkgName2, importer);
if (pkgRoot)
return [pkgName2, pkgRoot];
}
}
const pkgName = officials[name] ?? (name[0] === "@" ? name : `slidev-${type}-${name}`);
await promptForInstallation(pkgName);
return [pkgName, await findPkgRoot(pkgName, importer, true)];
};
}
function getUserPkgJson(userRoot) {
const path = resolve(userRoot, "package.json");
if (fs.existsSync(path))
return JSON.parse(fs.readFileSync(path, "utf-8"));
return {};
}
function hasWorkspacePackageJSON(root) {
const path = join(root, "package.json");
try {
fs.accessSync(path, fs.constants.R_OK);
} catch {
return false;
}
const content = JSON.parse(fs.readFileSync(path, "utf-8")) || {};
return !!content.workspaces;
}
function hasRootFile(root) {
const ROOT_FILES = [
// '.git',
// https://pnpm.js.org/workspaces/
"pnpm-workspace.yaml"
// https://rushjs.io/pages/advanced/config_files/
// 'rush.json',
// https://nx.dev/latest/react/getting-started/nx-setup
// 'workspace.json',
// 'nx.json'
];
return ROOT_FILES.some((file) => fs.existsSync(join(root, file)));
}
function searchForWorkspaceRoot(current, root = current) {
if (hasRootFile(current))
return current;
if (hasWorkspacePackageJSON(current))
return current;
const dir = dirname(current);
if (!dir || dir === current)
return root;
return searchForWorkspaceRoot(dir, root);
}
var rootsInfo = null;
async function getRoots(entry) {
if (rootsInfo)
return rootsInfo;
if (!entry)
throw new Error("[slidev] Cannot find roots without entry");
const userRoot = dirname(entry);
isInstalledGlobally.value = slash(relative(userRoot, process.argv[1])).includes("/.pnpm/") || (await import("is-installed-globally")).default;
const clientRoot = await findPkgRoot("@slidev/client", cliRoot, true);
const closestPkgRoot = dirname(await findClosestPkgJsonPath(userRoot) || userRoot);
const userPkgJson = getUserPkgJson(closestPkgRoot);
const userWorkspaceRoot = searchForWorkspaceRoot(closestPkgRoot);
rootsInfo = {
cliRoot,
clientRoot,
userRoot,
userPkgJson,
userWorkspaceRoot
};
return rootsInfo;
}
export {
isInstalledGlobally,
resolveImportUrl,
toAtFS,
resolveImportPath,
resolveEntry,
createResolver,
getRoots
};
+29
View File
@@ -0,0 +1,29 @@
import {
resolveViteConfigs
} from "./chunk-7E4FQJTV.js";
// node/commands/serve.ts
import { join } from "node:path";
import process from "node:process";
import { createServer as createViteServer } from "vite";
async function createServer(options, viteConfig = {}, serverOptions) {
process.env.EDITOR = process.env.EDITOR || "code";
const inlineConfig = await resolveViteConfigs(
options,
{
optimizeDeps: {
entries: [
join(options.clientRoot, "main.ts")
]
}
},
viteConfig,
"serve",
serverOptions
);
return await createViteServer(inlineConfig);
}
export {
createServer
};
+2
View File
@@ -0,0 +1,2 @@
export { }
+602
View File
@@ -0,0 +1,602 @@
import {
createServer
} from "./chunk-UVOSJW3S.js";
import {
getThemeMeta,
loadSetups,
parser,
resolveAddons,
resolveOptions,
resolveTheme,
version
} from "./chunk-7E4FQJTV.js";
import {
getRoots,
isInstalledGlobally,
resolveEntry
} from "./chunk-UNQ5DBLZ.js";
// node/cli.ts
import { exec } from "node:child_process";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import * as readline from "node:readline";
import { verifyConfig } from "@slidev/parser";
import equal from "fast-deep-equal";
import fs from "fs-extra";
import { getPort } from "get-port-please";
import { blue, bold, cyan, dim, gray, green, lightCyan, underline, yellow } from "kolorist";
import openBrowser from "open";
import yargs from "yargs";
// node/setups/preparser.ts
import { uniq } from "@antfu/utils";
import { injectPreparserExtensionLoader } from "@slidev/parser/fs";
function setupPreparser() {
injectPreparserExtensionLoader(async (headmatter, filepath, mode) => {
const addons = Array.isArray(headmatter?.addons) ? headmatter.addons : [];
const { userRoot } = await getRoots();
const roots = uniq([
...await resolveAddons(addons),
userRoot
]);
const returns = await loadSetups(roots, "preparser.ts", [{ filepath, headmatter, mode }]);
return returns.flat();
});
}
// node/cli.ts
var CONFIG_RESTART_FIELDS = [
"monaco",
"routerMode",
"fonts",
"css",
"mdc",
"editor",
"theme"
];
var FILES_CREATE_RESTART = [
"global-bottom.vue",
"global-top.vue",
"uno.config.js",
"uno.config.ts",
"unocss.config.js",
"unocss.config.ts"
];
var FILES_CHANGE_RESTART = [
"setup/shiki.ts",
"setup/katex.ts",
"setup/preparser.ts"
];
setupPreparser();
var cli = yargs(process.argv.slice(2)).scriptName("slidev").usage("$0 [args]").version(version).strict().showHelpOnFail(false).alias("h", "help").alias("v", "version");
cli.command(
"* [entry]",
"Start a local server for Slidev",
(args) => commonOptions(args).option("port", {
alias: "p",
type: "number",
describe: "port"
}).option("open", {
alias: "o",
default: false,
type: "boolean",
describe: "open in browser"
}).option("remote", {
type: "string",
describe: "listen public host and enable remote control"
}).option("tunnel", {
default: false,
type: "boolean",
describe: "open a Cloudflare Quick Tunnel to make Slidev available on the internet"
}).option("log", {
default: "warn",
type: "string",
choices: ["error", "warn", "info", "silent"],
describe: "log level"
}).option("inspect", {
default: false,
type: "boolean",
describe: "enable the inspect plugin for debugging"
}).option("force", {
alias: "f",
default: false,
type: "boolean",
describe: "force the optimizer to ignore the cache and re-bundle "
}).option("bind", {
type: "string",
default: "0.0.0.0",
describe: "specify which IP addresses the server should listen on in remote mode"
}).strict().help(),
async ({ entry, theme, port: userPort, open, log, remote, tunnel, force, inspect, bind }) => {
let server;
let port = 3030;
let lastRemoteUrl;
let restartTimer;
function restartServer() {
clearTimeout(restartTimer);
restartTimer = setTimeout(() => {
console.log(yellow("\n restarting...\n"));
initServer();
}, 500);
}
async function initServer() {
if (server)
await server.close();
const options = await resolveOptions({ entry, remote, theme, inspect }, "dev");
const host = remote !== void 0 ? bind : "localhost";
port = userPort || await getPort({
port: 3030,
random: false,
portRange: [3030, 4e3],
host
});
server = await createServer(
options,
{
server: {
port,
strictPort: true,
open,
host,
// @ts-expect-error Vite <= 4
force
},
optimizeDeps: {
// Vite 5
force
},
logLevel: log
},
{
async loadData(loadedSource) {
const { data: oldData, entry: entry2 } = options;
const loaded = await parser.load(options.userRoot, entry2, loadedSource, "dev");
const themeRaw = theme || loaded.headmatter.theme || "default";
if (options.themeRaw !== themeRaw) {
console.log(yellow("\n restarting on theme change\n"));
restartServer();
return false;
}
const themeMeta = options.themeRoots[0] ? await getThemeMeta(themeRaw, options.themeRoots[0]) : void 0;
const newData = {
...loaded,
themeMeta,
config: parser.resolveConfig(loaded.headmatter, themeMeta, entry2)
};
if (CONFIG_RESTART_FIELDS.some((i) => !equal(newData.config[i], oldData.config[i]))) {
console.log(yellow("\n restarting on config change\n"));
restartServer();
return false;
}
if (newData.features.katex && !oldData.features.katex || newData.features.monaco && !oldData.features.monaco) {
console.log(yellow("\n restarting on feature change\n"));
restartServer();
return false;
}
return newData;
}
}
);
await server.listen();
let tunnelUrl = "";
if (tunnel) {
if (remote != null)
tunnelUrl = await openTunnel(port);
else
console.log(yellow("\n --remote is required for tunneling, Cloudflare Quick Tunnel is not enabled.\n"));
}
let publicIp;
if (remote)
publicIp = await import("public-ip").then((r) => r.publicIpv4());
lastRemoteUrl = printInfo(options, port, remote, tunnelUrl, publicIp);
}
async function openTunnel(port2) {
const { startTunnel } = await import("untun");
const tunnel2 = await startTunnel({
port: port2,
acceptCloudflareNotice: true
});
return await tunnel2?.getURL() ?? "";
}
const SHORTCUTS = [
{
name: "r",
fullname: "restart",
action() {
initServer();
}
},
{
name: "o",
fullname: "open",
action() {
openBrowser(`http://localhost:${port}`);
}
},
{
name: "e",
fullname: "edit",
action() {
exec(`code "${entry}"`);
}
},
{
name: "q",
fullname: "quit",
action() {
try {
server?.close();
} finally {
process.exit();
}
}
},
{
name: "c",
fullname: "qrcode",
async action() {
if (!lastRemoteUrl)
return;
await import("uqr").then(async (r) => {
const code = r.renderUnicodeCompact(lastRemoteUrl);
console.log(`
${dim(" QR Code for remote control: ")}
${blue(lastRemoteUrl)}
`);
console.log(code.split("\n").map((i) => ` ${i}`).join("\n"));
const publicIp = await import("public-ip").then((r2) => r2.publicIpv4());
if (publicIp)
console.log(`
${dim(" Public IP: ")} ${blue(publicIp)}
`);
});
}
}
];
function bindShortcut() {
process.stdin.resume();
process.stdin.setEncoding("utf8");
readline.emitKeypressEvents(process.stdin);
if (process.stdin.isTTY)
process.stdin.setRawMode(true);
process.stdin.on("keypress", (str, key) => {
if (key.ctrl && key.name === "c") {
process.exit();
} else {
const [sh] = SHORTCUTS.filter((item) => item.name === str);
if (sh) {
try {
sh.action();
} catch (err) {
console.error(`Failed to execute shortcut ${sh.fullname}`, err);
}
}
}
});
}
initServer();
bindShortcut();
const { watch } = await import("chokidar");
const watcher = watch([
...FILES_CREATE_RESTART,
...FILES_CHANGE_RESTART
], {
ignored: ["node_modules", ".git"],
ignoreInitial: true
});
watcher.on("unlink", (file) => {
console.log(yellow(`
file ${file} removed, restarting...
`));
restartServer();
});
watcher.on("add", (file) => {
console.log(yellow(`
file ${file} added, restarting...
`));
restartServer();
});
watcher.on("change", (file) => {
if (typeof file !== "string")
return;
if (FILES_CREATE_RESTART.includes(file))
return;
console.log(yellow(`
file ${file} changed, restarting...
`));
restartServer();
});
}
);
cli.command(
"build [entry..]",
"Build hostable SPA",
(args) => exportOptions(commonOptions(args)).option("out", {
alias: "o",
type: "string",
default: "dist",
describe: "output dir"
}).option("base", {
type: "string",
describe: "output base"
}).option("download", {
alias: "d",
type: "boolean",
describe: "allow download as PDF"
}).option("inspect", {
default: false,
type: "boolean",
describe: "enable the inspect plugin for debugging"
}).strict().help(),
async (args) => {
const { entry, theme, base, download, out, inspect } = args;
const { build } = await import("./build-N6BEUQKX.js");
for (const entryFile of entry) {
const options = await resolveOptions({ entry: entryFile, theme, inspect, download }, "build");
printInfo(options);
await build(
options,
{
base,
build: {
outDir: entry.length === 1 ? out : path.join(out, path.basename(entryFile, ".md"))
}
},
{ ...args, entry: entryFile }
);
}
}
);
cli.command(
"format [entry..]",
"Format the markdown file",
(args) => commonOptions(args).strict().help(),
async ({ entry }) => {
for (const entryFile of entry) {
const md = await parser.parse(await fs.readFile(entryFile, "utf-8"), entryFile);
parser.prettify(md);
await parser.save(md);
}
}
);
cli.command(
"theme [subcommand]",
"Theme related operations",
(command) => {
return command.command(
"eject",
"Eject current theme into local file system",
(args) => commonOptions(args).option("dir", {
type: "string",
default: "theme"
}),
async ({ entry: entryRaw, dir, theme: themeInput }) => {
const entry = await resolveEntry(entryRaw);
const roots = await getRoots(entry);
const data = await parser.load(roots.userRoot, entry);
let themeRaw = themeInput || data.headmatter.theme;
themeRaw = themeRaw === null ? "none" : themeRaw || "default";
if (themeRaw === "none") {
console.error('Cannot eject theme "none"');
process.exit(1);
}
if ("/.".includes(themeRaw[0]) || themeRaw[0] !== "@" && themeRaw.includes("/")) {
console.error("Theme is already ejected");
process.exit(1);
}
const [name, root] = await resolveTheme(themeRaw, entry);
await fs.copy(root, path.resolve(dir), {
filter: (i) => !/node_modules|.git/.test(path.relative(root, i))
});
const dirPath = `./${dir}`;
const firstSlide = data.entry.slides[0];
firstSlide.frontmatter.theme = dirPath;
parser.prettifySlide(firstSlide);
await parser.save(data.entry);
console.log(`Theme "${name}" ejected successfully to "${dirPath}"`);
}
);
},
() => {
cli.showHelp();
process.exit(1);
}
);
cli.command(
"export [entry..]",
"Export slides to PDF",
(args) => exportOptions(commonOptions(args)).strict().help(),
async (args) => {
const { entry, theme } = args;
const { exportSlides, getExportOptions } = await import("./export-I2C456L6.js");
const port = await getPort(12445);
let warned = false;
for (const entryFile of entry) {
const options = await resolveOptions({ entry: entryFile, theme }, "export");
if (options.data.config.browserExporter !== false && !warned) {
warned = true;
console.log(lightCyan("[Slidev] Try the new browser exporter!"));
console.log(
lightCyan("You can use the browser exporter instead by starting the dev server as normal and visit"),
`${blue("localhost:")}${dim("<port>")}${blue("/export")}
`
);
}
const server = await createServer(
options,
{
server: { port },
clearScreen: false
}
);
await server.listen(port);
printInfo(options);
const result = await exportSlides({
port,
...getExportOptions({ ...args, entry: entryFile }, options)
});
console.log(`${green(" \u2713 ")}${dim("exported to ")}./${result}
`);
server.close();
}
process.exit(0);
}
);
cli.command(
"export-notes [entry..]",
"Export slide notes to PDF",
(args) => args.positional("entry", {
default: "slides.md",
type: "string",
describe: "path to the slides markdown entry"
}).option("output", {
type: "string",
describe: "path to the output"
}).option("timeout", {
default: 3e4,
type: "number",
describe: "timeout for rendering the print page"
}).option("wait", {
default: 0,
type: "number",
describe: "wait for the specified ms before exporting"
}).strict().help(),
async ({
entry,
output,
timeout,
wait
}) => {
const { exportNotes } = await import("./export-I2C456L6.js");
const port = await getPort(12445);
for (const entryFile of entry) {
const options = await resolveOptions({ entry: entryFile }, "export");
const server = await createServer(
options,
{
server: { port },
clearScreen: false
}
);
await server.listen(port);
printInfo(options);
const result = await exportNotes({
port,
output: output || (options.data.config.exportFilename ? `${options.data.config.exportFilename}-notes` : `${path.basename(entryFile, ".md")}-export-notes`),
timeout,
wait
});
console.log(`${green(" \u2713 ")}${dim("exported to ")}./${result}
`);
server.close();
}
process.exit(0);
}
);
cli.help().parse();
function commonOptions(args) {
return args.positional("entry", {
default: "slides.md",
type: "string",
describe: "path to the slides markdown entry"
}).option("theme", {
alias: "t",
type: "string",
describe: "override theme"
});
}
function exportOptions(args) {
return args.option("output", {
type: "string",
describe: "path to the output"
}).option("format", {
type: "string",
choices: ["pdf", "png", "pptx", "md"],
describe: "output format"
}).option("timeout", {
type: "number",
describe: "timeout for rendering the print page"
}).option("wait", {
type: "number",
describe: "wait for the specified ms before exporting"
}).option("wait-until", {
type: "string",
choices: ["networkidle", "load", "domcontentloaded", "none"],
describe: "wait until the specified event before exporting each slide"
}).option("range", {
type: "string",
describe: 'page ranges to export, for example "1,4-5,6"'
}).option("dark", {
type: "boolean",
describe: "export as dark theme"
}).option("with-clicks", {
alias: "c",
type: "boolean",
describe: "export pages for every clicks"
}).option("executable-path", {
type: "string",
describe: "executable to override playwright bundled browser"
}).option("with-toc", {
type: "boolean",
describe: "export pages with outline"
}).option("per-slide", {
type: "boolean",
describe: "slide slides slide by slide. Works better with global components, but will break cross slide links and TOC in PDF"
}).option("scale", {
type: "number",
describe: "scale factor for image export"
}).option("omit-background", {
type: "boolean",
describe: "export png pages without the default browser background"
});
}
function printInfo(options, port, remote, tunnelUrl, publicIp) {
console.log();
console.log();
console.log(` ${cyan("\u25CF") + blue("\u25A0") + yellow("\u25B2")}`);
console.log(`${bold(" Slidev")} ${blue(`v${version}`)} ${isInstalledGlobally.value ? yellow("(global)") : ""}`);
console.log();
verifyConfig(options.data.config, options.data.themeMeta, (v) => console.warn(yellow(` ! ${v}`)));
console.log(dim(" theme ") + (options.theme ? green(options.theme) : gray("none")));
console.log(dim(" css engine ") + blue("unocss"));
console.log(dim(" entry ") + dim(path.normalize(path.dirname(options.entry)) + path.sep) + path.basename(options.entry));
if (port) {
const query = remote ? `?password=${remote}` : "";
const presenterPath = `${options.data.config.routerMode === "hash" ? "/#/" : "/"}presenter/${query}`;
const entryPath = `${options.data.config.routerMode === "hash" ? "/#/" : "/"}entry${query}/`;
const overviewPath = `${options.data.config.routerMode === "hash" ? "/#/" : "/"}overview${query}/`;
console.log();
console.log(`${dim(" public slide show ")} > ${cyan(`http://localhost:${bold(port)}/`)}`);
if (query)
console.log(`${dim(" private slide show ")} > ${cyan(`http://localhost:${bold(port)}/${query}`)}`);
if (options.utils.define.__SLIDEV_FEATURE_PRESENTER__)
console.log(`${dim(" presenter mode ")} > ${blue(`http://localhost:${bold(port)}${presenterPath}`)}`);
console.log(`${dim(" slides overview ")} > ${blue(`http://localhost:${bold(port)}${overviewPath}`)}`);
if (options.utils.define.__SLIDEV_FEATURE_BROWSER_EXPORTER__)
console.log(`${dim(" export slides")} > ${blue(`http://localhost:${bold(port)}/export/`)}`);
if (options.inspect)
console.log(`${dim(" vite inspector")} > ${yellow(`http://localhost:${bold(port)}/__inspect/`)}`);
let lastRemoteUrl = "";
if (remote !== void 0) {
Object.values(os.networkInterfaces()).forEach((v) => (v || []).filter((details) => String(details.family).slice(-1) === "4" && !details.address.includes("127.0.0.1")).forEach(({ address }) => {
lastRemoteUrl = `http://${address}:${port}${entryPath}`;
console.log(`${dim(" remote control ")} > ${blue(lastRemoteUrl)}`);
}));
if (publicIp) {
lastRemoteUrl = `http://${publicIp}:${port}${entryPath}`;
console.log(`${dim(" remote control ")} > ${blue(lastRemoteUrl)}`);
}
if (tunnelUrl) {
lastRemoteUrl = `${tunnelUrl}${entryPath}`;
console.log(`${dim(" remote via tunnel")} > ${yellow(lastRemoteUrl)}`);
}
} else {
console.log(`${dim(" remote control ")} > ${dim("pass --remote to enable")}`);
}
console.log();
console.log(`${dim(" shortcuts ")} > ${underline("r")}${dim("estart | ")}${underline("o")}${dim("pen | ")}${underline("e")}${dim("dit | ")}${underline("q")}${dim("uit")}${lastRemoteUrl ? ` | ${dim("qr")}${underline("c")}${dim("ode")}` : ""}`);
return lastRemoteUrl;
}
}
+527
View File
@@ -0,0 +1,527 @@
import {
getRoots
} from "./chunk-UNQ5DBLZ.js";
// node/commands/export.ts
import { Buffer } from "node:buffer";
import path from "node:path";
import process from "node:process";
import { clearUndefined, slash } from "@antfu/utils";
import { outlinePdfFactory } from "@lillallol/outline-pdf";
import { parseRangeString } from "@slidev/parser/core";
import { Presets, SingleBar } from "cli-progress";
import fs from "fs-extra";
import { blue, cyan, dim, green, yellow } from "kolorist";
import { resolve } from "mlly";
import * as pdfLib from "pdf-lib";
import { PDFDocument } from "pdf-lib";
function addToTree(tree, info, slideIndexes, level = 1) {
const titleLevel = info.level;
if (titleLevel && titleLevel > level && tree.length > 0 && tree[tree.length - 1].titleLevel < titleLevel) {
addToTree(tree[tree.length - 1].children, info, slideIndexes, level + 1);
} else {
tree.push({
no: info.index,
children: [],
level,
titleLevel: titleLevel ?? level,
path: String(slideIndexes[info.index + 1]),
hideInToc: Boolean(info.frontmatter?.hideInToc),
title: info.title
});
}
}
function makeOutline(tree) {
return tree.map(({ title, path: path2, level, children }) => {
const rootOutline = title ? `${path2}|${"-".repeat(level - 1)}|${title}` : null;
const childrenOutline = makeOutline(children);
return childrenOutline.length > 0 ? `${rootOutline}
${childrenOutline}` : rootOutline;
}).filter((outline) => !!outline).join("\n");
}
function createSlidevProgress(indeterminate = false) {
function getSpinner(n = 0) {
return [cyan("\u25CF"), green("\u25C6"), blue("\u25A0"), yellow("\u25B2")][n % 4];
}
let current = 0;
let spinner = 0;
let timer;
const progress = new SingleBar({
clearOnComplete: true,
hideCursor: true,
format: ` {spin} ${yellow("rendering")}${indeterminate ? dim(yellow("...")) : " {bar} {value}/{total}"}`,
linewrap: false,
barsize: 30
}, Presets.shades_grey);
return {
bar: progress,
start(total) {
progress.start(total, 0, { spin: getSpinner(spinner) });
timer = setInterval(() => {
spinner += 1;
progress.update(current, { spin: getSpinner(spinner) });
}, 200);
},
update(v) {
current = v;
progress.update(v, { spin: getSpinner(spinner) });
},
stop() {
clearInterval(timer);
progress.stop();
}
};
}
async function exportNotes({
port = 18724,
base = "/",
output = "notes",
timeout = 3e4,
wait = 0
}) {
const { chromium } = await importPlaywright();
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
const progress = createSlidevProgress(true);
progress.start(1);
if (!output.endsWith(".pdf"))
output = `${output}.pdf`;
await page.goto(`http://localhost:${port}${base}presenter/print`, { waitUntil: "networkidle", timeout });
await page.waitForLoadState("networkidle");
await page.emulateMedia({ media: "screen" });
if (wait)
await page.waitForTimeout(wait);
await page.pdf({
path: output,
margin: {
left: 0,
top: 0,
right: 0,
bottom: 0
},
printBackground: true,
preferCSSPageSize: true
});
progress.stop();
browser.close();
return output;
}
async function exportSlides({
port = 18724,
total = 0,
range,
format = "pdf",
output = "slides",
slides,
base = "/",
timeout = 3e4,
wait = 0,
dark = false,
routerMode = "history",
width = 1920,
height = 1080,
withClicks = false,
executablePath = void 0,
withToc = false,
perSlide = false,
scale = 1,
waitUntil,
omitBackground = false
}) {
const pages = parseRangeString(total, range);
const { chromium } = await importPlaywright();
const browser = await chromium.launch({
executablePath
});
const context = await browser.newContext({
viewport: {
width,
// Calculate height for every slides to be in the viewport to trigger the rendering of iframes (twitter, youtube...)
height: perSlide ? height : height * pages.length
},
deviceScaleFactor: scale
});
const page = await context.newPage();
const progress = createSlidevProgress(!perSlide);
async function go(no, clicks) {
const query = new URLSearchParams();
if (withClicks)
query.set("print", "clicks");
else
query.set("print", "true");
if (range)
query.set("range", range);
if (clicks)
query.set("clicks", clicks);
const url = routerMode === "hash" ? `http://localhost:${port}${base}?${query}#${no}` : `http://localhost:${port}${base}${no}?${query}`;
await page.goto(url, {
waitUntil,
timeout
});
if (waitUntil)
await page.waitForLoadState(waitUntil);
await page.emulateMedia({ colorScheme: dark ? "dark" : "light", media: "screen" });
const slide = no === "print" ? page.locator("body") : page.locator(`[data-slidev-no="${no}"]`);
await slide.waitFor();
{
const elements = slide.locator(".slidev-slide-loading");
const count = await elements.count();
for (let index = 0; index < count; index++)
await elements.nth(index).waitFor({ state: "detached" });
}
{
const elements = slide.locator("[data-waitfor]");
const count = await elements.count();
for (let index = 0; index < count; index++) {
const element = elements.nth(index);
const attribute = await element.getAttribute("data-waitfor");
if (attribute) {
await element.locator(attribute).waitFor({ state: "visible" }).catch((e) => {
console.error(e);
process.exitCode = 1;
});
}
}
}
{
const frames = page.frames();
await Promise.all(frames.map((frame) => frame.waitForLoadState()));
}
{
const container = slide.locator("#mermaid-rendering-container");
const count = await container.count();
if (count > 0) {
while (true) {
const element = container.locator("div").first();
if (await element.count() === 0)
break;
await element.waitFor({ state: "detached" });
}
await container.evaluate((node) => node.style.display = "none");
}
}
{
const elements = slide.locator(".monaco-aria-container");
const count = await elements.count();
for (let index = 0; index < count; index++) {
const element = elements.nth(index);
await element.evaluate((node) => node.style.display = "none");
}
}
if (wait)
await page.waitForTimeout(wait);
}
async function getSlidesIndex() {
const clicksBySlide = {};
const slides2 = page.locator(".print-slide-container");
const count = await slides2.count();
for (let i = 0; i < count; i++) {
const id = await slides2.nth(i).getAttribute("id") || "";
const path2 = Number(id.split("-")[0]);
clicksBySlide[path2] = (clicksBySlide[path2] || 0) + 1;
}
const slideIndexes = Object.fromEntries(Object.entries(clicksBySlide).reduce((acc, [path2, clicks], i) => {
acc.push([path2, clicks + (acc[i - 1]?.[1] ?? 0)]);
return acc;
}, []));
return slideIndexes;
}
function getClicksFromUrl(url) {
return url.match(/clicks=([1-9]\d*)/)?.[1];
}
async function genPageWithClicks(fn, no, clicks) {
await fn(no, clicks);
if (withClicks) {
await page.keyboard.press("ArrowRight", { delay: 100 });
const _clicks = getClicksFromUrl(page.url());
if (_clicks && clicks !== _clicks)
await genPageWithClicks(fn, no, _clicks);
}
}
async function genPagePdfPerSlide() {
const buffers = [];
const genPdfBuffer = async (i, clicks) => {
await go(i, clicks);
const pdf = await page.pdf({
width,
height,
margin: {
left: 0,
top: 0,
right: 0,
bottom: 0
},
pageRanges: "1",
printBackground: true,
preferCSSPageSize: true
});
buffers.push(pdf);
};
let idx = 0;
for (const i of pages) {
await genPageWithClicks(genPdfBuffer, i);
progress.update(++idx);
}
let mergedPdf = await PDFDocument.create({});
for (const pdfBytes of buffers) {
const pdf = await PDFDocument.load(pdfBytes);
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
copiedPages.forEach((page2) => {
mergedPdf.addPage(page2);
});
}
addPdfMetadata(mergedPdf);
if (withToc)
mergedPdf = await addTocToPdf(mergedPdf);
const buffer = await mergedPdf.save();
await fs.writeFile(output, buffer);
}
async function genPagePdfOnePiece() {
await go("print");
await page.pdf({
path: output,
width,
height,
margin: {
left: 0,
top: 0,
right: 0,
bottom: 0
},
printBackground: true,
preferCSSPageSize: true
});
let pdfData = await fs.readFile(output);
let pdf = await PDFDocument.load(pdfData);
addPdfMetadata(pdf);
if (withToc)
pdf = await addTocToPdf(pdf);
pdfData = Buffer.from(await pdf.save());
await fs.writeFile(output, pdfData);
}
async function genPagePngOnePiece(writeToDisk) {
const result = [];
await go("print");
await fs.emptyDir(output);
const slideContainers = page.locator(".print-slide-container");
const count = await slideContainers.count();
for (let i = 0; i < count; i++) {
progress.update(i + 1);
const id = await slideContainers.nth(i).getAttribute("id") || "";
const slideNo = +id.split("-")[0];
const buffer = await slideContainers.nth(i).screenshot({
omitBackground
});
result.push({ slideIndex: slideNo - 1, buffer });
if (writeToDisk)
await fs.writeFile(path.join(output, `${withClicks ? id : slideNo}.png`), buffer);
}
return result;
}
async function genPagePngPerSlide(writeToDisk) {
const result = [];
const genScreenshot = async (no, clicks) => {
await go(no, clicks);
const buffer = await page.screenshot({
omitBackground
});
result.push({ slideIndex: no - 1, buffer });
if (writeToDisk) {
await fs.writeFile(
path.join(output, `${no.toString().padStart(2, "0")}${clicks ? `-${clicks}` : ""}.png`),
buffer
);
}
};
for (const no of pages)
await genPageWithClicks(genScreenshot, no);
return result;
}
function genPagePdf() {
if (!output.endsWith(".pdf"))
output = `${output}.pdf`;
return perSlide ? genPagePdfPerSlide() : genPagePdfOnePiece();
}
function genPagePng(writeToDisk = true) {
return perSlide ? genPagePngPerSlide(writeToDisk) : genPagePngOnePiece(writeToDisk);
}
async function genPageMd() {
const files = await fs.readdir(output);
const mds = files.map((file, i, files2) => {
const slideIndex = getSlideIndex(file);
const mdImg = `![${slides[slideIndex]?.title}](./${slash(path.join(output, file))})
`;
if ((i + 1 === files2.length || getSlideIndex(files2[i + 1]) !== slideIndex) && slides[slideIndex]?.note)
return `${mdImg}${slides[slideIndex]?.note}
`;
return mdImg;
});
if (!output.endsWith(".md"))
output = `${output}.md`;
await fs.writeFile(output, mds.join(""));
}
async function genPagePptx(pngs) {
const { default: PptxGenJS } = await import("pptxgenjs");
const pptx = new PptxGenJS();
const layoutName = `${width}x${height}`;
pptx.defineLayout({
name: layoutName,
width: width / 96,
height: height / 96
});
pptx.layout = layoutName;
const titleSlide = slides[0];
pptx.author = titleSlide?.frontmatter?.author;
pptx.company = "Created using Slidev";
if (titleSlide?.title)
pptx.title = titleSlide?.title;
if (titleSlide?.frontmatter?.info)
pptx.subject = titleSlide?.frontmatter?.info;
pngs.forEach(({ slideIndex, buffer: buffer2 }) => {
const slide = pptx.addSlide();
slide.background = {
data: `data:image/png;base64,${buffer2.toString("base64")}`
};
const note = slides[slideIndex].note;
if (note)
slide.addNotes(note);
});
const buffer = await pptx.write({
outputType: "nodebuffer"
});
if (!output.endsWith(".pptx"))
output = `${output}.pptx`;
await fs.writeFile(output, buffer);
}
function getSlideIndex(file) {
const slideId = file.substring(0, file.indexOf(".")).split("-")[0];
return Number(slideId) - 1;
}
function addPdfMetadata(pdf) {
const titleSlide = slides[0];
if (titleSlide?.title)
pdf.setTitle(titleSlide.title);
if (titleSlide?.frontmatter?.info)
pdf.setSubject(titleSlide.frontmatter.info);
if (titleSlide?.frontmatter?.author)
pdf.setAuthor(titleSlide.frontmatter.author);
if (titleSlide?.frontmatter?.keywords) {
if (Array.isArray(titleSlide?.frontmatter?.keywords))
pdf.setKeywords(titleSlide?.frontmatter?.keywords);
else
pdf.setKeywords(titleSlide?.frontmatter?.keywords.split(","));
}
}
async function addTocToPdf(pdf) {
const outlinePdf = outlinePdfFactory(pdfLib);
const slideIndexes = await getSlidesIndex();
const tocTree = slides.filter((slide) => slide.title).reduce((acc, slide) => {
addToTree(acc, slide, slideIndexes);
return acc;
}, []);
const outline = makeOutline(tocTree);
return await outlinePdf({ outline, pdf });
}
progress.start(pages.length);
if (format === "pdf") {
await genPagePdf();
} else if (format === "png") {
await genPagePng();
} else if (format === "md") {
await genPagePng();
await genPageMd();
} else if (format === "pptx") {
const buffers = await genPagePng(false);
await genPagePptx(buffers);
} else {
throw new Error(`Unsupported exporting format "${format}"`);
}
progress.stop();
browser.close();
return output;
}
function getExportOptions(args, options, outDir, outFilename) {
const config = {
...options.data.config.export,
...args,
...clearUndefined({
waitUntil: args["wait-until"],
withClicks: args["with-clicks"],
executablePath: args["executable-path"],
withToc: args["with-toc"],
perSlide: args["per-slide"],
omitBackground: args["omit-background"]
})
};
const {
entry,
output,
format,
timeout,
wait,
waitUntil,
range,
dark,
withClicks,
executablePath,
withToc,
perSlide,
scale,
omitBackground
} = config;
outFilename = output || options.data.config.exportFilename || outFilename || `${path.basename(entry, ".md")}-export`;
if (outDir)
outFilename = path.join(outDir, outFilename);
return {
output: outFilename,
slides: options.data.slides,
total: options.data.slides.length,
range,
format: format || "pdf",
timeout: timeout ?? 3e4,
wait: wait ?? 0,
waitUntil: waitUntil === "none" ? void 0 : waitUntil ?? "networkidle",
dark: dark || options.data.config.colorSchema === "dark",
routerMode: options.data.config.routerMode,
width: options.data.config.canvasWidth,
height: Math.round(options.data.config.canvasWidth / options.data.config.aspectRatio),
withClicks: withClicks ?? format === "pptx",
executablePath,
withToc: withToc || false,
perSlide: perSlide || false,
scale: scale || 2,
omitBackground: omitBackground ?? false
};
}
async function importPlaywright() {
const { userRoot, userWorkspaceRoot } = await getRoots();
try {
return await import(await resolve("playwright-chromium", { url: userRoot }));
} catch {
}
if (userWorkspaceRoot !== userRoot) {
try {
return await import(await resolve("playwright-chromium", { url: userWorkspaceRoot }));
} catch {
}
}
const { resolveGlobal } = await import("resolve-global");
try {
const imported = await import(resolveGlobal("playwright-chromium"));
return imported.default ?? imported;
} catch {
}
try {
return await import("playwright-chromium");
} catch {
}
throw new Error("The exporting for Slidev is powered by Playwright, please install it via `npm i -D playwright-chromium`");
}
export {
exportNotes,
exportSlides,
getExportOptions
};
+14
View File
@@ -0,0 +1,14 @@
import * as vite from 'vite';
import { InlineConfig, PluginOption } from 'vite';
import { ResolvedSlidevOptions, SlidevServerOptions, SlidevEntryOptions, ResolvedSlidevUtils, SlidevPluginOptions } from '@slidev/types';
import * as fs from '@slidev/parser/fs';
export { fs as parser };
declare function createServer(options: ResolvedSlidevOptions, viteConfig?: InlineConfig, serverOptions?: SlidevServerOptions): Promise<vite.ViteDevServer>;
declare function resolveOptions(entryOptions: SlidevEntryOptions, mode: ResolvedSlidevOptions['mode']): Promise<ResolvedSlidevOptions>;
declare function createDataUtils(resolved: Omit<ResolvedSlidevOptions, 'utils'>): Promise<ResolvedSlidevUtils>;
declare function ViteSlidevPlugin(options: ResolvedSlidevOptions, pluginOptions?: SlidevPluginOptions, serverOptions?: SlidevServerOptions): Promise<PluginOption[]>;
export { ViteSlidevPlugin, createDataUtils, createServer, resolveOptions };
+17
View File
@@ -0,0 +1,17 @@
import {
createServer
} from "./chunk-UVOSJW3S.js";
import {
ViteSlidevPlugin,
createDataUtils,
parser,
resolveOptions
} from "./chunk-7E4FQJTV.js";
import "./chunk-UNQ5DBLZ.js";
export {
ViteSlidevPlugin,
createDataUtils,
createServer,
parser,
resolveOptions
};
+126
View File
@@ -0,0 +1,126 @@
{
"name": "@slidev/cli",
"type": "module",
"version": "0.50.0",
"description": "Presentation slides for developers",
"author": "antfu <anthonyfu117@hotmail.com>",
"license": "MIT",
"funding": "https://github.com/sponsors/antfu",
"homepage": "https://sli.dev",
"repository": {
"type": "git",
"url": "https://github.com/slidevjs/slidev"
},
"bugs": "https://github.com/slidevjs/slidev/issues",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./*": "./*"
},
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"slidev": "./bin/slidev.mjs"
},
"files": [
"bin",
"dist",
"template.md"
],
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"playwright-chromium": "^1.10.0"
},
"peerDependenciesMeta": {
"playwright-chromium": {
"optional": true
}
},
"dependencies": {
"@antfu/ni": "^0.23.1",
"@antfu/utils": "^0.7.10",
"@iconify-json/carbon": "^1.2.5",
"@iconify-json/ph": "^1.2.2",
"@iconify-json/svg-spinners": "^1.2.2",
"@lillallol/outline-pdf": "^4.0.0",
"@shikijs/markdown-it": "^1.24.2",
"@shikijs/twoslash": "^1.24.2",
"@shikijs/vitepress-twoslash": "^1.24.2",
"@unocss/extractor-mdc": "^0.65.1",
"@unocss/reset": "^0.65.1",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"chokidar": "^4.0.2",
"cli-progress": "^3.12.0",
"connect": "^3.7.0",
"debug": "^4.4.0",
"fast-deep-equal": "^3.1.3",
"fast-glob": "^3.3.2",
"fs-extra": "^11.2.0",
"get-port-please": "^3.1.2",
"global-directory": "^4.0.1",
"htmlparser2": "^9.1.0",
"is-installed-globally": "^1.0.0",
"jiti": "^2.4.2",
"katex": "^0.16.17",
"kolorist": "^1.8.0",
"local-pkg": "^0.5.1",
"lz-string": "^1.5.0",
"magic-string": "^0.30.17",
"magic-string-stack": "^0.1.1",
"markdown-it": "^14.1.0",
"markdown-it-footnote": "^4.0.0",
"markdown-it-mdc": "^0.2.5",
"micromatch": "^4.0.8",
"mlly": "^1.7.3",
"monaco-editor": "0.51.0",
"open": "^10.1.0",
"pdf-lib": "^1.17.1",
"plantuml-encoder": "^1.4.0",
"postcss-nested": "^7.0.2",
"pptxgenjs": "^3.12.0",
"prompts": "^2.4.2",
"public-ip": "^7.0.1",
"resolve-from": "^5.0.0",
"resolve-global": "^2.0.0",
"semver": "^7.6.3",
"shiki": "^1.24.2",
"shiki-magic-move": "^0.5.2",
"sirv": "^3.0.0",
"source-map-js": "^1.2.1",
"typescript": "5.6.3",
"unocss": "^0.65.1",
"unplugin-icons": "^0.22.0",
"unplugin-vue-components": "^0.28.0",
"unplugin-vue-markdown": "^0.28.0",
"untun": "^0.1.3",
"uqr": "^0.1.2",
"vite": "^6.0.3",
"vite-plugin-inspect": "^0.10.3",
"vite-plugin-remote-assets": "^0.6.0",
"vite-plugin-static-copy": "^2.2.0",
"vite-plugin-vue-server-ref": "^0.4.2",
"vitefu": "^1.0.4",
"vue": "^3.5.13",
"yaml": "^2.6.1",
"yargs": "^17.7.2",
"@slidev/client": "0.50.0",
"@slidev/parser": "0.50.0",
"@slidev/types": "0.50.0"
},
"devDependencies": {
"@hedgedoc/markdown-it-plugins": "^2.1.4",
"@types/micromatch": "^4.0.9",
"@types/plantuml-encoder": "^1.4.2"
},
"scripts": {
"build": "rimraf dist && tsup node/index.ts node/cli.ts",
"dev": "nr build --watch",
"start": "esno node/cli.ts"
}
}
+433
View File
@@ -0,0 +1,433 @@
---
# try also 'default' to start simple
theme: seriph
# random image from a curated Unsplash collection by Anthony
# like them? see https://unsplash.com/collections/94734566/slidev
background: https://cover.sli.dev
# apply any unocss classes to the current slide
class: 'text-center'
# some information about the slides, markdown enabled
info: |
## Slidev Starter Template
Presentation slides for developers.
Learn more at [Sli.dev](https://sli.dev)
transition: slide-left
title: Welcome to Slidev
mdc: true
---
# Welcome to Slidev
Presentation slides for developers
<div class="pt-12">
<span @click="$slidev.nav.next" class="px-2 py-1 rounded cursor-pointer" flex="~ justify-center items-center gap-2" hover="bg-white bg-opacity-10">
Press Space for next page <div class="i-carbon:arrow-right inline-block"/>
</span>
</div>
<div class="abs-br m-6 flex gap-2">
<button @click="$slidev.nav.openInEditor()" title="Open in Editor" class="text-xl slidev-icon-btn opacity-50 !border-none !hover:text-white">
<div class="i-carbon:edit" />
</button>
<a href="https://github.com/slidevjs/slidev" target="_blank" alt="GitHub" title="Open in GitHub"
class="text-xl slidev-icon-btn opacity-50 !border-none !hover:text-white">
<carbon-logo-github />
</a>
</div>
<!--
The last comment block of each slide will be treated as slide notes. It will be visible and editable in Presenter Mode along with the slide. [Read more in the docs](https://sli.dev/guide/syntax.html#notes)
-->
---
transition: fade-out
---
# What is Slidev?
Slidev is a slide maker and accompanying presentation tool designed for developers. It consists of the following features:
- 📝 **Text-based** - focus on the content with Markdown, and apply styles later
- 🎨 **Themable** - themes can be shared and used as npm packages
- 🧑‍💻 **Developer Friendly** - code highlighting, live coding with autocompletion
- 🤹 **Interactive** - embedding Vue components to enhance your slides
- 🎥 **Recording** - built-in recording and camera view
- 📤 **Portable** - export to PDF, PPTX, PNGs, or even a hostable SPA
- 🛠 **Hackable** - virtually anything that's possible on a webpage is possible in Slidev
<br>
<br>
Read more about Slidev in [Why Slidev?](https://sli.dev/guide/why)
<!--
You can have `style` tags in markdown to override the style for the current page.
Learn more: https://sli.dev/guide/syntax#embedded-styles
-->
<style>
h1 {
background-color: #2B90B6;
background-image: linear-gradient(45deg, #4EC5D4 10%, #146b8c 20%);
background-size: 100%;
-webkit-background-clip: text;
-moz-background-clip: text;
-webkit-text-fill-color: transparent;
-moz-text-fill-color: transparent;
}
</style>
<!--
Here is another comment.
-->
---
layout: default
---
# Table of contents
```html
<Toc minDepth="1" maxDepth="1"></Toc>
```
<Toc maxDepth="1"></Toc>
---
transition: slide-up
level: 2
---
# Navigation
Hover on the bottom-left corner to see the navigation's control panel, [learn more](https://sli.dev/guide/navigation.html)
## Keyboard Shortcuts
| | |
| --- | --- |
| <kbd>right</kbd> / <kbd>space</kbd>| next animation or slide |
| <kbd>left</kbd> / <kbd>shift</kbd><kbd>space</kbd> | previous animation or slide |
| <kbd>up</kbd> | previous slide |
| <kbd>down</kbd> | next slide |
<!-- https://sli.dev/guide/animations.html#click-animation -->
<img
v-click
class="absolute -bottom-9 -left-7 w-80 opacity-50"
src="https://sli.dev/assets/arrow-bottom-left.svg"
alt=""
/>
<p v-after class="absolute bottom-23 left-45 opacity-30 transform -rotate-10">Here!</p>
---
layout: image-right
image: https://cover.sli.dev
---
# Code
Use code snippets and get automatic highlighting, and even types hover![^1]
```ts {all|5|7|7-8|10|all} twoslash
// TwoSlash enables TypeScript hover information
// and errors in markdown code blocks
// More at https://shiki.style/packages/twoslash
import { computed, ref } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
doubled.value = 2
```
<arrow v-click="[4, 5]" x1="350" y1="310" x2="195" y2="334" color="#953" width="2" arrowSize="1" />
<!-- This allow you to embed external code blocks -->
<!-- <<< @/snippets/external.ts#snippet -->
<!-- Footer -->
[^1]: [Learn More](https://sli.dev/guide/syntax.html#line-highlighting)
<!-- Inline style -->
<style>
.footnotes-sep {
@apply mt-5 opacity-10;
}
.footnotes {
@apply text-sm opacity-75;
}
.footnote-backref {
display: none;
}
</style>
---
# Components
<div grid="~ cols-2 gap-4">
<div>
You can use Vue components directly inside your slides.
We have provided a few built-in components like `<Tweet/>` and `<Youtube/>` that you can use directly. Adding your own custom components is also super easy.
```html
<Counter :count="10" />
```
<!-- ./components/Counter.vue -->
<Counter :count="10" m="t-4" />
Check out [the guides](https://sli.dev/builtin/components.html) for more.
</div>
<div>
```html
<Tweet id="1390115482657726468" />
```
<Tweet id="1390115482657726468" scale="0.65" />
</div>
</div>
<!--
Presenter notes with **bold**, *italic*, and ~~strike~~ text.
Also, HTML elements are valid:
<div class="flex w-full">
<span style="flex-grow: 1;">Left content</span>
<span>Right content</span>
</div>
-->
---
class: px-20
---
# Themes
Slidev comes with powerful theming support. Themes can provide styles, layouts, components, or even configurations for tools. Switch between themes on a per-slide basis with just **one change** in your frontmatter:
<div grid="~ cols-2 gap-2" m="t-2">
```yaml
---
theme: default
---
```
```yaml
---
theme: seriph
---
```
<img border="rounded" src="https://github.com/slidevjs/themes/blob/main/screenshots/theme-default/01.png?raw=true" alt="">
<img border="rounded" src="https://github.com/slidevjs/themes/blob/main/screenshots/theme-seriph/01.png?raw=true" alt="">
</div>
Read more about [How to use a theme](https://sli.dev/guide/theme-addon#use-theme) and
check out the [Awesome Themes Gallery](https://sli.dev/resources/theme-gallery).
---
preload: false
---
# Animations
Animations are powered by [@vueuse/motion](https://motion.vueuse.org/).
```html
<div
v-motion
:initial="{ x: -80 }"
:enter="{ x: 0 }">
Slidev
</div>
```
<div class="w-60 relative mt-6">
<div class="relative w-40 h-40">
<img
v-motion
:initial="{ x: 800, y: -100, scale: 1.5, rotate: -50 }"
:enter="final"
class="absolute top-0 left-0 right-0 bottom-0"
src="https://sli.dev/logo-square.png"
alt=""
/>
<img
v-motion
:initial="{ y: 500, x: -100, scale: 2 }"
:enter="final"
class="absolute top-0 left-0 right-0 bottom-0"
src="https://sli.dev/logo-circle.png"
alt=""
/>
<img
v-motion
:initial="{ x: 600, y: 400, scale: 2, rotate: 100 }"
:enter="final"
class="absolute top-0 left-0 right-0 bottom-0"
src="https://sli.dev/logo-triangle.png"
alt=""
/>
</div>
<div
class="text-5xl absolute top-14 left-40 text-[#2B90B6] -z-1"
v-motion
:initial="{ x: -80, opacity: 0}"
:enter="{ x: 0, opacity: 1, transition: { delay: 2000, duration: 1000 } }">
Slidev
</div>
</div>
<!-- vue script setup scripts can be directly used in markdown, and will only affect the current page -->
<script setup lang="ts">
const final = {
x: 0,
y: 0,
rotate: 0,
scale: 1,
transition: {
type: 'spring',
damping: 10,
stiffness: 20,
mass: 2
}
}
</script>
<div
v-motion
:initial="{ x:35, y: 40, opacity: 0}"
:enter="{ y: 0, opacity: 1, transition: { delay: 3500 } }">
[Learn More](https://sli.dev/guide/animations.html#motion)
</div>
---
# LaTeX
LaTeX is supported out-of-box powered by [KaTeX](https://katex.org/).
<br>
Inline $\sqrt{3x-1}+(1+x)^2$
Block
$$ {1|3|all}
\begin{array}{c}
\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} &
= \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
\nabla \cdot \vec{\mathbf{B}} & = 0
\end{array}
$$
<br>
[Learn more](https://sli.dev/guide/syntax#latex)
---
# Diagrams
You can create diagrams / graphs from textual descriptions, directly in your Markdown.
<div class="grid grid-cols-4 gap-5 pt-4 -mb-6">
```mermaid {scale: 0.5, alt: 'A simple sequence diagram'}
sequenceDiagram
Alice->John: Hello John, how are you?
Note over Alice,John: A typical interaction
```
```mermaid {theme: 'neutral', scale: 0.8}
graph TD
B[Text] --> C{Decision}
C -->|One| D[Result 1]
C -->|Two| E[Result 2]
```
```mermaid
mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectivness<br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
Pen and paper
Mermaid
```
```plantuml {scale: 0.7}
@startuml
package "Some Group" {
HTTP - [First Component]
[Another Component]
}
node "Other Groups" {
FTP - [Second Component]
[First Component] --> FTP
}
cloud {
[Example 1]
}
database "MySql" {
folder "This is my folder" {
[Folder 3]
}
frame "Foo" {
[Frame 4]
}
}
[Another Component] --> [Example 1]
[Example 1] --> [Folder 3]
[Folder 3] --> [Frame 4]
@enduml
```
</div>
[Learn More](https://sli.dev/guide/syntax.html#diagrams)
---
layout: center
class: text-center
---
# Learn More
[Documentation](https://sli.dev) · [GitHub](https://github.com/slidevjs/slidev) · [Showcases](https://sli.dev/showcases.html)