build: add precompiled CLI distribution
This commit is contained in:
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { checkProject, printIssues } from "./check.js";
|
||||||
|
import { joinBase, normalizeSiteBase } from "./lib/site-base.js";
|
||||||
|
import { auditBuiltDecks } from "./lib/overflow-audit.js";
|
||||||
|
import { formatOverflowAuditResult } from "./lib/overflow-report.js";
|
||||||
|
import { runSlidev } from "./lib/run-slidev.js";
|
||||||
|
import { resolveTheme } from "./lib/theme.js";
|
||||||
|
const cwd = process.cwd();
|
||||||
|
const siteBase = normalizeSiteBase(process.env.SITE_BASE);
|
||||||
|
let config;
|
||||||
|
let dist;
|
||||||
|
async function main() {
|
||||||
|
const result = await checkProject(cwd);
|
||||||
|
config = result.config;
|
||||||
|
dist = path.join(cwd, config.outDir);
|
||||||
|
const { decks, issues } = result;
|
||||||
|
printIssues(issues, cwd);
|
||||||
|
if (issues.some(issue => issue.level === 'error'))
|
||||||
|
throw new Error('内容检查失败,已停止构建');
|
||||||
|
const published = decks.filter(deck => !deck.draft);
|
||||||
|
await rm(dist, { recursive: true, force: true });
|
||||||
|
await mkdir(dist, { recursive: true });
|
||||||
|
for (const deck of published)
|
||||||
|
await buildDeck(deck);
|
||||||
|
await reportSlideOverflow(published);
|
||||||
|
await writeFile(path.join(dist, 'index.html'), renderIndex(published), 'utf8');
|
||||||
|
await writeFile(path.join(dist, '_headers'), renderHeaders(), 'utf8');
|
||||||
|
await writeFile(path.join(dist, '_redirects'), renderRedirects(published), 'utf8');
|
||||||
|
console.log(`\n构建完成:${published.length} 套演示 → ${path.relative(cwd, dist)}`);
|
||||||
|
}
|
||||||
|
async function buildDeck(deck) {
|
||||||
|
const output = path.join(dist, deck.slug);
|
||||||
|
const base = joinBase(siteBase, deck.slug);
|
||||||
|
console.log(`\nBuilding ${deck.slug} (${base})`);
|
||||||
|
await runSlidev([
|
||||||
|
'build',
|
||||||
|
deck.entry,
|
||||||
|
'--theme',
|
||||||
|
resolveTheme(config.theme),
|
||||||
|
'--out',
|
||||||
|
output,
|
||||||
|
'--base',
|
||||||
|
base,
|
||||||
|
'--router-mode',
|
||||||
|
'hash',
|
||||||
|
]);
|
||||||
|
const assets = path.join(deck.directory, 'assets');
|
||||||
|
await cp(assets, path.join(output, 'assets'), { recursive: true, force: true }).catch(() => undefined);
|
||||||
|
await copySpaEntry(output, 'presenter');
|
||||||
|
await copySpaEntry(output, 'overview');
|
||||||
|
await copySpaEntry(output, 'notes-edit');
|
||||||
|
}
|
||||||
|
async function copySpaEntry(output, route) {
|
||||||
|
const html = await readFile(path.join(output, 'index.html'), 'utf8');
|
||||||
|
const routeBootstrap = `<script>if(location.hash!==\"#/${route}\")location.hash=\"#/${route}\"</script>`;
|
||||||
|
const routedHtml = html.replace('</head>', `${routeBootstrap}</head>`);
|
||||||
|
const directory = path.join(output, route);
|
||||||
|
await mkdir(directory, { recursive: true });
|
||||||
|
await writeFile(path.join(directory, 'index.html'), routedHtml, 'utf8');
|
||||||
|
}
|
||||||
|
async function reportSlideOverflow(decks) {
|
||||||
|
if (!decks.length)
|
||||||
|
return;
|
||||||
|
console.log('\n检查编译后的幻灯片内容边界...');
|
||||||
|
const result = await auditBuiltDecks({ root: dist, decks, siteBase });
|
||||||
|
formatOverflowAuditResult(result, cwd).forEach(line => console.log(line));
|
||||||
|
}
|
||||||
|
function renderIndex(decks) {
|
||||||
|
const cards = decks.map((deck) => {
|
||||||
|
const href = joinBase(siteBase, deck.slug);
|
||||||
|
const cover = deck.cover
|
||||||
|
? `<img src="${escapeAttribute(`${href}${deck.cover.replace(/^\.\//, '')}`)}" alt="" loading="lazy">`
|
||||||
|
: `<div class="cover-fallback" aria-hidden="true"><span>${escapeHtml(deck.title.slice(0, 1))}</span></div>`;
|
||||||
|
const tags = deck.tags.map(tag => `<li>${escapeHtml(tag)}</li>`).join('');
|
||||||
|
return `<article class="deck-card">
|
||||||
|
<a class="cover" href="${escapeAttribute(href)}">${cover}</a>
|
||||||
|
<div class="deck-copy">
|
||||||
|
<p class="meta">${escapeHtml(deck.date || 'Undated')}${deck.author ? ` · ${escapeHtml(deck.author)}` : ''}</p>
|
||||||
|
<h2><a href="${escapeAttribute(href)}">${escapeHtml(deck.title)}</a></h2>
|
||||||
|
${deck.description ? `<p class="description">${escapeHtml(deck.description)}</p>` : ''}
|
||||||
|
${tags ? `<ul class="tags">${tags}</ul>` : ''}
|
||||||
|
</div>
|
||||||
|
</article>`;
|
||||||
|
}).join('\n');
|
||||||
|
return `<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<meta name="description" content="${escapeAttribute(config.description)}">
|
||||||
|
<title>${escapeHtml(config.title)}</title>
|
||||||
|
<style>${landingStyles()}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<p class="eyebrow">MARKDOWN PRESENTATIONS</p>
|
||||||
|
<h1>${escapeHtml(config.title)}</h1>
|
||||||
|
<p class="lede">${escapeHtml(config.description)}</p>
|
||||||
|
</header>
|
||||||
|
<section class="deck-list" aria-label="演示列表">
|
||||||
|
${cards || '<p>暂无公开演示。</p>'}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
function landingStyles() {
|
||||||
|
return `:root{font-family:Inter,"Noto Sans SC","PingFang SC","Microsoft YaHei",sans-serif;color:#222;background:#f6f5f2}*{box-sizing:border-box}body{margin:0}main{width:min(1120px,calc(100% - 40px));margin:auto;padding:72px 0 96px}header{border-bottom:2px solid #1d4ed8;padding-bottom:28px;margin-bottom:36px}.eyebrow{font-size:12px;font-weight:700;letter-spacing:.18em;color:#1d4ed8}h1{font-size:clamp(48px,8vw,82px);line-height:1;margin:14px 0}.lede{font-size:clamp(19px,2.2vw,26px);color:#555;margin:0}.deck-list{display:grid;gap:28px}.deck-card{display:grid;grid-template-columns:minmax(220px,36%) 1fr;background:#fff;border:1px solid #d9d7d2;border-radius:10px;overflow:hidden;box-shadow:0 12px 36px rgba(30,64,175,.08)}.cover{display:block;min-height:230px;background:#eae7e2}.cover img{display:block;width:100%;height:100%;min-height:230px;object-fit:cover}.cover-fallback{height:100%;min-height:230px;display:grid;place-items:center;background:linear-gradient(135deg,#eff6ff,#dbeafe);color:#1d4ed8;font-size:92px;font-weight:700}.deck-copy{padding:32px}.meta{font-size:13px;color:#777;margin:0 0 10px}h2{font-size:clamp(26px,3vw,38px);line-height:1.15;margin:0}h2 a{color:inherit;text-decoration:none}h2 a:hover{color:#1d4ed8}.description{font-size:17px;line-height:1.7;color:#555}.tags{display:flex;gap:8px;flex-wrap:wrap;list-style:none;padding:0;margin:24px 0 0}.tags li{font-size:12px;background:#eff6ff;color:#1d4ed8;padding:5px 9px;border-radius:999px}@media(max-width:680px){main{padding-top:44px}.deck-card{grid-template-columns:1fr}.cover,.cover img,.cover-fallback{min-height:190px}.deck-copy{padding:24px}}`;
|
||||||
|
}
|
||||||
|
function renderHeaders() {
|
||||||
|
return `/*.html\n Cache-Control: public, max-age=0, must-revalidate\n\n/*/assets/*\n Cache-Control: public, max-age=31536000, immutable\n`;
|
||||||
|
}
|
||||||
|
function renderRedirects(decks) {
|
||||||
|
return decks.map(deck => `${joinBase(siteBase, deck.slug)}* ${joinBase(siteBase, deck.slug)}index.html 200`).join('\n');
|
||||||
|
}
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return value.replace(/[&<>"']/g, character => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]);
|
||||||
|
}
|
||||||
|
function escapeAttribute(value) {
|
||||||
|
return escapeHtml(value);
|
||||||
|
}
|
||||||
|
await main().catch((error) => {
|
||||||
|
console.error(error instanceof Error ? error.message : error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
//# sourceMappingURL=build.js.map
|
||||||
File diff suppressed because one or more lines are too long
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { type ValidationIssue } from './lib/content.ts';
|
||||||
|
import { type EasySlidesConfig } from './lib/config.ts';
|
||||||
|
export declare function checkProject(cwd?: string, providedConfig?: EasySlidesConfig): Promise<{
|
||||||
|
config: EasySlidesConfig;
|
||||||
|
decks: import("./lib/content.ts").DeckMetadata[];
|
||||||
|
issues: ValidationIssue[];
|
||||||
|
staticOverflow: import("./lib/static-overflow.ts").StaticOverflowResult;
|
||||||
|
}>;
|
||||||
|
export declare function printIssues(issues: ValidationIssue[], cwd?: string): void;
|
||||||
|
export declare function runCheck(): Promise<void>;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { discoverDecks, validateDeck } from "./lib/content.js";
|
||||||
|
import { loadProjectConfig } from "./lib/config.js";
|
||||||
|
import { formatStaticOverflowResult } from "./lib/overflow-report.js";
|
||||||
|
import { auditStaticDecks } from "./lib/static-overflow.js";
|
||||||
|
import { hasPresenterToken } from "./lib/token.js";
|
||||||
|
export async function checkProject(cwd = process.cwd(), providedConfig) {
|
||||||
|
const config = providedConfig ?? await loadProjectConfig(cwd);
|
||||||
|
const decks = await discoverDecks(cwd, config.slidesDir);
|
||||||
|
const issues = [];
|
||||||
|
if (decks.length === 0)
|
||||||
|
issues.push({ level: 'error', file: path.join(cwd, config.slidesDir), message: `没有发现 ${config.slidesDir}/<slug>/slides.md` });
|
||||||
|
for (const deck of decks)
|
||||||
|
issues.push(...await validateDeck(deck));
|
||||||
|
const staticOverflow = await auditStaticDecks(decks);
|
||||||
|
if (!hasPresenterToken()) {
|
||||||
|
issues.push({
|
||||||
|
level: 'warning',
|
||||||
|
file: path.join(cwd, '.env'),
|
||||||
|
message: '未设置 PRESENTER_TOKEN;生产构建将隐藏演示者入口',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { config, decks, issues, staticOverflow };
|
||||||
|
}
|
||||||
|
export function printIssues(issues, cwd = process.cwd()) {
|
||||||
|
for (const issue of issues) {
|
||||||
|
const marker = issue.level === 'error' ? 'ERROR' : 'WARN ';
|
||||||
|
console.log(`${marker} ${path.relative(cwd, issue.file)}: ${issue.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function runCheck() {
|
||||||
|
const result = await checkProject();
|
||||||
|
printIssues(result.issues);
|
||||||
|
formatStaticOverflowResult(result.staticOverflow).forEach(line => console.log(line));
|
||||||
|
const errors = result.issues.filter(issue => issue.level === 'error');
|
||||||
|
if (errors.length > 0) {
|
||||||
|
console.error(`\n检查失败:${errors.length} 个错误`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const warnings = result.issues.filter(issue => issue.level === 'warning').length
|
||||||
|
+ result.staticOverflow.issues.length
|
||||||
|
+ result.staticOverflow.errors.length;
|
||||||
|
console.log(`\n检查通过:${result.decks.length} 套演示,${warnings} 个警告`);
|
||||||
|
}
|
||||||
|
if (import.meta.url === new URL(process.argv[1], 'file:').href)
|
||||||
|
await runCheck();
|
||||||
|
//# sourceMappingURL=check.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"check.js","sourceRoot":"","sources":["../scripts/check.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAA;AACtB,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,aAAa,EAAE,YAAY,EAAwB,MAAM,kBAAkB,CAAA;AACpF,OAAO,EAAE,iBAAiB,EAAyB,MAAM,iBAAiB,CAAA;AAC1E,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAA;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAElD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,EAAE,cAAiC;IACvF,MAAM,MAAM,GAAG,cAAc,IAAI,MAAM,iBAAiB,CAAC,GAAG,CAAC,CAAA;IAC7D,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,SAAS,CAAC,CAAA;IACxD,MAAM,MAAM,GAAsB,EAAE,CAAA;IAEpC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QACpB,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,QAAQ,MAAM,CAAC,SAAS,mBAAmB,EAAE,CAAC,CAAA;IAE/H,KAAK,MAAM,IAAI,IAAI,KAAK;QACtB,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC,CAAA;IAE1C,MAAM,cAAc,GAAG,MAAM,gBAAgB,CAAC,KAAK,CAAC,CAAA;IAEpD,IAAI,CAAC,iBAAiB,EAAE,EAAE,CAAC;QACzB,MAAM,CAAC,IAAI,CAAC;YACV,KAAK,EAAE,SAAS;YAChB,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC;YAC5B,OAAO,EAAE,kCAAkC;SAC5C,CAAC,CAAA;IACJ,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA;AAClD,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,MAAyB,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IACxE,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAA;QAC1D,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;IAC9E,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ;IAC5B,MAAM,MAAM,GAAG,MAAM,YAAY,EAAE,CAAA;IACnC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAC1B,0BAA0B,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;IACpF,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,OAAO,CAAC,CAAA;IACrE,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,OAAO,CAAC,KAAK,CAAC,UAAU,MAAM,CAAC,MAAM,MAAM,CAAC,CAAA;QAC5C,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;QACpB,OAAM;IACR,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,MAAM;UAC5E,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM;UACnC,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,CAAA;IACvC,OAAO,CAAC,GAAG,CAAC,UAAU,MAAM,CAAC,KAAK,CAAC,MAAM,QAAQ,QAAQ,MAAM,CAAC,CAAA;AAClE,CAAC;AAED,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI;IAC5D,MAAM,QAAQ,EAAE,CAAA"}
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
export {};
|
||||||
Executable
+58
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import path from 'node:path';
|
||||||
|
const rawArgs = process.argv.slice(2);
|
||||||
|
const command = rawArgs.shift();
|
||||||
|
const rootIndex = rawArgs.indexOf('--root');
|
||||||
|
if (rootIndex >= 0) {
|
||||||
|
const root = rawArgs[rootIndex + 1];
|
||||||
|
if (!root)
|
||||||
|
throw new Error('--root 需要一个目录');
|
||||||
|
rawArgs.splice(rootIndex, 2);
|
||||||
|
process.chdir(path.resolve(root));
|
||||||
|
}
|
||||||
|
process.argv = [process.argv[0], process.argv[1], ...rawArgs];
|
||||||
|
switch (command) {
|
||||||
|
case 'dev':
|
||||||
|
await import("./dev.js");
|
||||||
|
break;
|
||||||
|
case 'present':
|
||||||
|
await import("./present.js");
|
||||||
|
break;
|
||||||
|
case 'build':
|
||||||
|
await import("./build.js");
|
||||||
|
break;
|
||||||
|
case 'check':
|
||||||
|
await (await import("./check.js")).runCheck();
|
||||||
|
break;
|
||||||
|
case 'export':
|
||||||
|
await import("./export.js");
|
||||||
|
break;
|
||||||
|
case 'init':
|
||||||
|
await import("./init.js");
|
||||||
|
break;
|
||||||
|
case undefined:
|
||||||
|
case 'help':
|
||||||
|
case '--help':
|
||||||
|
case '-h':
|
||||||
|
printHelp();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(`未知命令:${command}`);
|
||||||
|
printHelp();
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
function printHelp() {
|
||||||
|
console.log(`easy-slides
|
||||||
|
|
||||||
|
用法:
|
||||||
|
easy-slides init [directory] [--engine <package-spec>]
|
||||||
|
easy-slides dev [slug|slides.md]
|
||||||
|
easy-slides present <slug>
|
||||||
|
easy-slides build
|
||||||
|
easy-slides check
|
||||||
|
easy-slides export <slug> --format pdf|png|pptx
|
||||||
|
|
||||||
|
全局选项:
|
||||||
|
--root <directory> 在指定内容项目中运行`);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=cli.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../scripts/cli.ts"],"names":[],"mappings":";AACA,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AACrC,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,EAAE,CAAA;AAC/B,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;AAC3C,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;IACnB,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,GAAG,CAAC,CAAC,CAAA;IACnC,IAAI,CAAC,IAAI;QACP,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAA;IAClC,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAA;IAC5B,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;AACnC,CAAC;AACD,OAAO,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,CAAA;AAE7D,QAAQ,OAAO,EAAE,CAAC;IAChB,KAAK,KAAK;QACR,MAAM,MAAM,CAAC,UAAU,CAAC,CAAA;QACxB,MAAK;IACP,KAAK,SAAS;QACZ,MAAM,MAAM,CAAC,cAAc,CAAC,CAAA;QAC5B,MAAK;IACP,KAAK,OAAO;QACV,MAAM,MAAM,CAAC,YAAY,CAAC,CAAA;QAC1B,MAAK;IACP,KAAK,OAAO;QACV,MAAM,CAAC,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;QAC7C,MAAK;IACP,KAAK,QAAQ;QACX,MAAM,MAAM,CAAC,aAAa,CAAC,CAAA;QAC3B,MAAK;IACP,KAAK,MAAM;QACT,MAAM,MAAM,CAAC,WAAW,CAAC,CAAA;QACzB,MAAK;IACP,KAAK,SAAS,CAAC;IACf,KAAK,MAAM,CAAC;IACZ,KAAK,QAAQ,CAAC;IACd,KAAK,IAAI;QACP,SAAS,EAAE,CAAA;QACX,MAAK;IACP;QACE,OAAO,CAAC,KAAK,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAA;QAChC,SAAS,EAAE,CAAA;QACX,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;AACxB,CAAC;AAED,SAAS,SAAS;IAChB,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;iCAWmB,CAAC,CAAA;AAClC,CAAC"}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { loadProjectConfig } from "./lib/config.js";
|
||||||
|
import { resolveDeck } from "./lib/content.js";
|
||||||
|
import { startDevOverflowAudit } from "./lib/dev-overflow-audit.js";
|
||||||
|
import { startSlidev, waitForSlidev } from "./lib/run-slidev.js";
|
||||||
|
import { resolveTheme } from "./lib/theme.js";
|
||||||
|
const input = process.argv.slice(2).find(argument => !argument.startsWith('-'));
|
||||||
|
const config = await loadProjectConfig();
|
||||||
|
const deck = await resolveDeck(input, process.cwd(), config.slidesDir);
|
||||||
|
const port = process.env.PORT || '3030';
|
||||||
|
const slidev = startSlidev([
|
||||||
|
deck.entry,
|
||||||
|
'--theme',
|
||||||
|
resolveTheme(config.theme),
|
||||||
|
'--open',
|
||||||
|
'--port',
|
||||||
|
port,
|
||||||
|
]);
|
||||||
|
const overflowAudit = startDevOverflowAudit({
|
||||||
|
deck,
|
||||||
|
});
|
||||||
|
slidev.once('exit', () => void overflowAudit.close());
|
||||||
|
try {
|
||||||
|
await waitForSlidev(slidev);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
await overflowAudit.close();
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=dev.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"dev.js","sourceRoot":"","sources":["../scripts/dev.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAA;AACtB,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAA;AACnE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE7C,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAA;AAC/E,MAAM,MAAM,GAAG,MAAM,iBAAiB,EAAE,CAAA;AACxC,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,SAAS,CAAC,CAAA;AACtE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,MAAM,CAAA;AACvC,MAAM,MAAM,GAAG,WAAW,CAAC;IACzB,IAAI,CAAC,KAAK;IACV,SAAS;IACT,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC;IAC1B,QAAQ;IACR,QAAQ;IACR,IAAI;CACL,CAAC,CAAA;AACF,MAAM,aAAa,GAAG,qBAAqB,CAAC;IAC1C,IAAI;CACL,CAAC,CAAA;AACF,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK,aAAa,CAAC,KAAK,EAAE,CAAC,CAAA;AAErD,IAAI,CAAC;IACH,MAAM,aAAa,CAAC,MAAM,CAAC,CAAA;AAC7B,CAAC;QACO,CAAC;IACP,MAAM,aAAa,CAAC,KAAK,EAAE,CAAA;AAC7B,CAAC"}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { mkdir } from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { loadProjectConfig } from "./lib/config.js";
|
||||||
|
import { resolveDeck } from "./lib/content.js";
|
||||||
|
import { runSlidev } from "./lib/run-slidev.js";
|
||||||
|
import { resolveTheme } from "./lib/theme.js";
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const slug = args.find(argument => !argument.startsWith('-'));
|
||||||
|
const formatIndex = args.indexOf('--format');
|
||||||
|
const format = formatIndex >= 0 ? args[formatIndex + 1] : 'pdf';
|
||||||
|
const allowed = new Set(['pdf', 'png', 'pptx', 'md']);
|
||||||
|
if (!allowed.has(format))
|
||||||
|
throw new Error(`不支持导出格式:${format}`);
|
||||||
|
const config = await loadProjectConfig();
|
||||||
|
const deck = await resolveDeck(slug, process.cwd(), config.slidesDir);
|
||||||
|
const exportDir = path.join(process.cwd(), config.exportDir);
|
||||||
|
await mkdir(exportDir, { recursive: true });
|
||||||
|
const output = path.join(exportDir, `${deck.slug}.${format}`);
|
||||||
|
const slidevArgs = [
|
||||||
|
'export',
|
||||||
|
deck.entry,
|
||||||
|
'--theme',
|
||||||
|
resolveTheme(config.theme),
|
||||||
|
'--format',
|
||||||
|
format,
|
||||||
|
'--output',
|
||||||
|
output,
|
||||||
|
];
|
||||||
|
// Slidev's one-piece exporter may capture off-screen lazy images before they
|
||||||
|
// are painted. Per-slide export renders each canvas as the active slide and is
|
||||||
|
// reliable for Markdown images across PNG, PDF, and screenshot-based PPTX.
|
||||||
|
if (format !== 'md')
|
||||||
|
slidevArgs.push('--per-slide');
|
||||||
|
await runSlidev(slidevArgs);
|
||||||
|
console.log(`导出完成:${path.relative(process.cwd(), output)}`);
|
||||||
|
//# sourceMappingURL=export.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"export.js","sourceRoot":"","sources":["../scripts/export.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAA;AACtB,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AACxC,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE7C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AAClC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAA;AAC7D,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;AAC5C,MAAM,MAAM,GAAG,WAAW,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;AAC/D,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;AAErD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;IACtB,MAAM,IAAI,KAAK,CAAC,WAAW,MAAM,EAAE,CAAC,CAAA;AAEtC,MAAM,MAAM,GAAG,MAAM,iBAAiB,EAAE,CAAA;AACxC,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,SAAS,CAAC,CAAA;AACrE,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,SAAS,CAAC,CAAA;AAC5D,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;AAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,IAAI,CAAC,IAAI,IAAI,MAAM,EAAE,CAAC,CAAA;AAE7D,MAAM,UAAU,GAAG;IACjB,QAAQ;IACR,IAAI,CAAC,KAAK;IACV,SAAS;IACT,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC;IAC1B,UAAU;IACV,MAAM;IACN,UAAU;IACV,MAAM;CACP,CAAA;AAED,6EAA6E;AAC7E,+EAA+E;AAC/E,2EAA2E;AAC3E,IAAI,MAAM,KAAK,IAAI;IACjB,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;AAEhC,MAAM,SAAS,CAAC,UAAU,CAAC,CAAA;AAE3B,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAA"}
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export { loadProjectConfig, normalizeConfig } from './lib/config.ts';
|
||||||
|
export type { EasySlidesConfig } from './lib/config.ts';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { loadProjectConfig, normalizeConfig } from "./lib/config.js";
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../scripts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA"}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { mkdir, writeFile } from 'node:fs/promises';
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
import path from 'node:path';
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const engineVersion = String(require('../package.json').version);
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const engineIndex = args.indexOf('--engine');
|
||||||
|
const engineSpec = engineIndex >= 0 ? args[engineIndex + 1] : `^${engineVersion}`;
|
||||||
|
if (!engineSpec)
|
||||||
|
throw new Error('--engine 需要 npm 版本、Git URL 或本地路径');
|
||||||
|
const targetInput = args.find((argument, index) => !argument.startsWith('-') && index !== engineIndex + 1) ?? '.';
|
||||||
|
const target = path.resolve(targetInput);
|
||||||
|
await mkdir(path.join(target, 'slides', 'welcome', 'assets'), { recursive: true });
|
||||||
|
await mkdir(path.join(target, '.github', 'workflows'), { recursive: true });
|
||||||
|
await writeNew('package.json', `${JSON.stringify({
|
||||||
|
name: path.basename(target),
|
||||||
|
private: true,
|
||||||
|
scripts: {
|
||||||
|
dev: 'easy-slides dev',
|
||||||
|
present: 'easy-slides present',
|
||||||
|
build: 'easy-slides build',
|
||||||
|
check: 'easy-slides check',
|
||||||
|
export: 'easy-slides export',
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
'@easy-slides/cli': engineSpec,
|
||||||
|
},
|
||||||
|
}, null, 2)}\n`);
|
||||||
|
await writeNew('easy-slides.config.mjs', `export default {
|
||||||
|
title: '${escapeJavaScript(path.basename(target))}',
|
||||||
|
description: 'Markdown presentations',
|
||||||
|
slidesDir: 'slides',
|
||||||
|
outDir: 'dist',
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
await writeNew(path.join('slides', 'welcome', 'slides.md'), `---
|
||||||
|
title: Welcome
|
||||||
|
description: Your first easy-slides deck
|
||||||
|
date: ${new Date().toISOString().slice(0, 10)}
|
||||||
|
tags: [Markdown]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Welcome
|
||||||
|
|
||||||
|
> Edit slides/welcome/slides.md and push to deploy.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Speaker notes are written in HTML comments.
|
||||||
|
-->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Content lives here
|
||||||
|
|
||||||
|
- The engine is an npm dependency
|
||||||
|
- This repository owns slides and assets
|
||||||
|
- The static build can deploy anywhere
|
||||||
|
`);
|
||||||
|
await writeNew('.gitignore', `node_modules/
|
||||||
|
dist/
|
||||||
|
exports/
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
`);
|
||||||
|
await writeNew('pnpm-workspace.yaml', `allowBuilds:
|
||||||
|
playwright-chromium: false
|
||||||
|
`);
|
||||||
|
await writeNew(path.join('.github', 'workflows', 'deploy-pages.yml'), `name: Deploy slides
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: \${{ steps.deployment.outputs.page_url }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 11.19.0
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: pnpm
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
- run: pnpm check
|
||||||
|
env:
|
||||||
|
PRESENTER_TOKEN: \${{ secrets.PRESENTER_TOKEN }}
|
||||||
|
- run: pnpm build
|
||||||
|
env:
|
||||||
|
SITE_BASE: /\${{ github.event.repository.name }}/
|
||||||
|
PRESENTER_TOKEN: \${{ secrets.PRESENTER_TOKEN }}
|
||||||
|
- uses: actions/configure-pages@v5
|
||||||
|
- uses: actions/upload-pages-artifact@v4
|
||||||
|
with:
|
||||||
|
path: dist
|
||||||
|
- id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
|
`);
|
||||||
|
console.log(`内容项目已创建:${target}`);
|
||||||
|
console.log('下一步:pnpm install && pnpm dev -- welcome');
|
||||||
|
async function writeNew(relative, content) {
|
||||||
|
const filename = path.join(target, relative);
|
||||||
|
await mkdir(path.dirname(filename), { recursive: true });
|
||||||
|
await writeFile(filename, content, { encoding: 'utf8', flag: 'wx' }).catch((error) => {
|
||||||
|
if (error.code === 'EEXIST')
|
||||||
|
throw new Error(`拒绝覆盖已有文件:${filename}`);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function escapeJavaScript(value) {
|
||||||
|
return value.replaceAll('\\', '\\\\').replaceAll("'", "\\'");
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=init.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"init.js","sourceRoot":"","sources":["../scripts/init.ts"],"names":[],"mappings":"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"}
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
export interface EasySlidesConfig {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
slidesDir: string;
|
||||||
|
outDir: string;
|
||||||
|
exportDir: string;
|
||||||
|
theme?: string;
|
||||||
|
}
|
||||||
|
export declare function loadProjectConfig(cwd?: string): Promise<EasySlidesConfig>;
|
||||||
|
export declare function normalizeConfig(value: Partial<EasySlidesConfig> | undefined): EasySlidesConfig;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
||||||
|
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
||||||
|
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
||||||
|
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
import { access, readFile } from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { pathToFileURL } from 'node:url';
|
||||||
|
const defaults = {
|
||||||
|
title: 'easy-slides',
|
||||||
|
description: 'Markdown presentations powered by easy-slides',
|
||||||
|
slidesDir: 'slides',
|
||||||
|
outDir: 'dist',
|
||||||
|
exportDir: 'exports',
|
||||||
|
};
|
||||||
|
export async function loadProjectConfig(cwd = process.cwd()) {
|
||||||
|
const candidates = [
|
||||||
|
'easy-slides.config.mjs',
|
||||||
|
'easy-slides.config.js',
|
||||||
|
'easy-slides.config.json',
|
||||||
|
];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const filename = path.join(cwd, candidate);
|
||||||
|
if (!(await exists(filename)))
|
||||||
|
continue;
|
||||||
|
const loaded = candidate.endsWith('.json')
|
||||||
|
? JSON.parse(await readFile(filename, 'utf8'))
|
||||||
|
: (await import(__rewriteRelativeImportExtension(`${pathToFileURL(filename).href}?t=${Date.now()}`))).default;
|
||||||
|
return normalizeConfig(loaded);
|
||||||
|
}
|
||||||
|
return { ...defaults };
|
||||||
|
}
|
||||||
|
export function normalizeConfig(value) {
|
||||||
|
return {
|
||||||
|
...defaults,
|
||||||
|
...value,
|
||||||
|
slidesDir: normalizeRelativePath(value?.slidesDir ?? defaults.slidesDir, 'slidesDir'),
|
||||||
|
outDir: normalizeRelativePath(value?.outDir ?? defaults.outDir, 'outDir'),
|
||||||
|
exportDir: normalizeRelativePath(value?.exportDir ?? defaults.exportDir, 'exportDir'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function normalizeRelativePath(value, field) {
|
||||||
|
const normalized = value.trim().replace(/[\\/]+$/, '');
|
||||||
|
const portable = normalized.replaceAll('\\', '/');
|
||||||
|
if (!normalized || path.isAbsolute(normalized) || portable === '..' || portable.startsWith('../'))
|
||||||
|
throw new Error(`${field} 必须是项目内的相对路径`);
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
async function exists(filename) {
|
||||||
|
try {
|
||||||
|
await access(filename);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=config.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../scripts/lib/config.ts"],"names":[],"mappings":";;;;;;;;AAAA,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAWxC,MAAM,QAAQ,GAAqB;IACjC,KAAK,EAAE,aAAa;IACpB,WAAW,EAAE,+CAA+C;IAC5D,SAAS,EAAE,QAAQ;IACnB,MAAM,EAAE,MAAM;IACd,SAAS,EAAE,SAAS;CACrB,CAAA;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IACzD,MAAM,UAAU,GAAG;QACjB,wBAAwB;QACxB,uBAAuB;QACvB,yBAAyB;KAC1B,CAAA;IAED,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAA;QAC1C,IAAI,CAAC,CAAC,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC3B,SAAQ;QAEV,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC;YACxC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAC9C,CAAC,CAAC,CAAC,MAAM,MAAM,kCAAC,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC,IAAI,MAAM,IAAI,CAAC,GAAG,EAAE,EAAE,EAAC,CAAC,CAAC,OAAO,CAAA;QAE7E,OAAO,eAAe,CAAC,MAAM,CAAC,CAAA;IAChC,CAAC;IAED,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAA;AACxB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAA4C;IAC1E,OAAO;QACL,GAAG,QAAQ;QACX,GAAG,KAAK;QACR,SAAS,EAAE,qBAAqB,CAAC,KAAK,EAAE,SAAS,IAAI,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC;QACrF,MAAM,EAAE,qBAAqB,CAAC,KAAK,EAAE,MAAM,IAAI,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;QACzE,SAAS,EAAE,qBAAqB,CAAC,KAAK,EAAE,SAAS,IAAI,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC;KACtF,CAAA;AACH,CAAC;AAED,SAAS,qBAAqB,CAAC,KAAa,EAAE,KAAa;IACzD,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;IACtD,MAAM,QAAQ,GAAG,UAAU,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IACjD,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC;QAC/F,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,cAAc,CAAC,CAAA;IACzC,OAAO,UAAU,CAAA;AACnB,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,QAAgB;IACpC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAA;QACtB,OAAO,IAAI,CAAA;IACb,CAAC;IACD,MAAM,CAAC;QACL,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC"}
|
||||||
Vendored
+26
@@ -0,0 +1,26 @@
|
|||||||
|
export interface DeckMetadata {
|
||||||
|
slug: string;
|
||||||
|
entry: string;
|
||||||
|
directory: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
author: string;
|
||||||
|
date: string;
|
||||||
|
tags: string[];
|
||||||
|
cover?: string;
|
||||||
|
draft: boolean;
|
||||||
|
}
|
||||||
|
export interface ValidationIssue {
|
||||||
|
level: 'error' | 'warning';
|
||||||
|
file: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
export declare function discoverDecks(cwd?: string, slidesDir?: string): Promise<DeckMetadata[]>;
|
||||||
|
export declare function readDeck(entry: string): Promise<DeckMetadata>;
|
||||||
|
export declare function resolveDeck(input: string | undefined, cwd?: string, slidesDir?: string): Promise<DeckMetadata>;
|
||||||
|
export declare function validateDeck(deck: DeckMetadata): Promise<ValidationIssue[]>;
|
||||||
|
export declare function extractMarkdownImages(markdown: string): {
|
||||||
|
source: string;
|
||||||
|
attributes: Record<string, string>;
|
||||||
|
}[];
|
||||||
|
export declare function parseAttributes(input: string): Record<string, string>;
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { access, readFile } from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import fg from 'fast-glob';
|
||||||
|
import matter from 'gray-matter';
|
||||||
|
const slugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
||||||
|
const allowedFits = new Set(['contain', 'cover', 'fill', 'scale-down', 'none']);
|
||||||
|
export async function discoverDecks(cwd = process.cwd(), slidesDir = 'slides') {
|
||||||
|
const entries = await fg(`${slidesDir.replaceAll('\\', '/')}/*/slides.md`, { cwd, absolute: true, onlyFiles: true });
|
||||||
|
const decks = await Promise.all(entries.map(entry => readDeck(entry)));
|
||||||
|
return decks.sort((a, b) => compareDecks(a, b));
|
||||||
|
}
|
||||||
|
export async function readDeck(entry) {
|
||||||
|
const source = await readFile(entry, 'utf8');
|
||||||
|
const parsed = matter(source);
|
||||||
|
const directory = path.dirname(entry);
|
||||||
|
const slug = path.basename(directory);
|
||||||
|
const tags = Array.isArray(parsed.data.tags)
|
||||||
|
? parsed.data.tags.map((tag) => String(tag).trim()).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
return {
|
||||||
|
slug,
|
||||||
|
entry,
|
||||||
|
directory,
|
||||||
|
title: String(parsed.data.title ?? '').trim(),
|
||||||
|
description: String(parsed.data.description ?? '').trim(),
|
||||||
|
author: String(parsed.data.author ?? '').trim(),
|
||||||
|
date: normalizeDate(parsed.data.date),
|
||||||
|
tags,
|
||||||
|
cover: parsed.data.cover ? String(parsed.data.cover).trim() : undefined,
|
||||||
|
draft: parsed.data.draft === true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export async function resolveDeck(input, cwd = process.cwd(), slidesDir = 'slides') {
|
||||||
|
const decks = await discoverDecks(cwd, slidesDir);
|
||||||
|
if (!input)
|
||||||
|
return decks.find(deck => !deck.draft) ?? decks[0];
|
||||||
|
const direct = path.resolve(cwd, input);
|
||||||
|
const bySlug = decks.find(deck => deck.slug === input);
|
||||||
|
const byPath = decks.find(deck => path.resolve(deck.entry) === direct);
|
||||||
|
const deck = bySlug ?? byPath;
|
||||||
|
if (!deck)
|
||||||
|
throw new Error(`找不到演示“${input}”。可用 slug:${decks.map(item => item.slug).join(', ') || '无'}`);
|
||||||
|
return deck;
|
||||||
|
}
|
||||||
|
export async function validateDeck(deck) {
|
||||||
|
const issues = [];
|
||||||
|
const source = await readFile(deck.entry, 'utf8');
|
||||||
|
const parsed = matter(source);
|
||||||
|
if (!slugPattern.test(deck.slug))
|
||||||
|
issues.push(error(deck.entry, '目录名必须是小写 kebab-case slug'));
|
||||||
|
if (!deck.title)
|
||||||
|
issues.push(error(deck.entry, 'frontmatter 缺少必填 title'));
|
||||||
|
if (deck.date && Number.isNaN(Date.parse(deck.date)))
|
||||||
|
issues.push(error(deck.entry, `date 不是有效日期:${deck.date}`));
|
||||||
|
if (parsed.data.tags !== undefined && !Array.isArray(parsed.data.tags))
|
||||||
|
issues.push(error(deck.entry, 'tags 必须是数组'));
|
||||||
|
if (parsed.data.theme && parsed.data.theme !== 'easy-jyy')
|
||||||
|
issues.push(warning(deck.entry, `当前主题为 ${parsed.data.theme},easy-jyy 主题能力可能不会生效`));
|
||||||
|
if (deck.cover && !isRemote(deck.cover)) {
|
||||||
|
const coverPath = path.resolve(deck.directory, deck.cover);
|
||||||
|
if (!(await exists(coverPath)))
|
||||||
|
issues.push(error(deck.entry, `封面不存在:${deck.cover}`));
|
||||||
|
}
|
||||||
|
for (const image of extractMarkdownImages(parsed.content)) {
|
||||||
|
if (!isRemote(image.source) && !image.source.startsWith('/') && !(await exists(path.resolve(deck.directory, image.source))))
|
||||||
|
issues.push(error(deck.entry, `图片不存在:${image.source}`));
|
||||||
|
if (image.attributes.fit && !allowedFits.has(image.attributes.fit))
|
||||||
|
issues.push(error(deck.entry, `图片 fit 不受支持:${image.attributes.fit}`));
|
||||||
|
if (image.attributes['max-height'] && !isSafeCssSize(image.attributes['max-height']))
|
||||||
|
issues.push(error(deck.entry, `图片 max-height 不合法:${image.attributes['max-height']}`));
|
||||||
|
}
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
export function extractMarkdownImages(markdown) {
|
||||||
|
const images = [];
|
||||||
|
const pattern = /!\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)\s*(?:\{([^}]*)\})?/g;
|
||||||
|
for (const match of markdown.matchAll(pattern)) {
|
||||||
|
images.push({
|
||||||
|
source: match[1],
|
||||||
|
attributes: parseAttributes(match[2] ?? ''),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return images;
|
||||||
|
}
|
||||||
|
export function parseAttributes(input) {
|
||||||
|
const attributes = {};
|
||||||
|
const pattern = /([\w-]+)=(?:"([^"]*)"|'([^']*)'|([^\s]+))/g;
|
||||||
|
for (const match of input.matchAll(pattern))
|
||||||
|
attributes[match[1]] = match[2] ?? match[3] ?? match[4] ?? '';
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
function normalizeDate(value) {
|
||||||
|
if (!value)
|
||||||
|
return '';
|
||||||
|
if (value instanceof Date)
|
||||||
|
return value.toISOString().slice(0, 10);
|
||||||
|
return String(value).trim();
|
||||||
|
}
|
||||||
|
function compareDecks(a, b) {
|
||||||
|
const byDate = (Date.parse(b.date || '1970-01-01') || 0) - (Date.parse(a.date || '1970-01-01') || 0);
|
||||||
|
return byDate || a.title.localeCompare(b.title, 'zh-CN');
|
||||||
|
}
|
||||||
|
function isRemote(value) {
|
||||||
|
return /^(?:https?:)?\/\//.test(value) || value.startsWith('data:');
|
||||||
|
}
|
||||||
|
function isSafeCssSize(value) {
|
||||||
|
return /^(?:\d+(?:\.\d+)?)(?:px|rem|em|vh|vw|%|vmin|vmax)$/.test(value);
|
||||||
|
}
|
||||||
|
async function exists(file) {
|
||||||
|
try {
|
||||||
|
await access(file);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function error(file, message) {
|
||||||
|
return { level: 'error', file, message };
|
||||||
|
}
|
||||||
|
function warning(file, message) {
|
||||||
|
return { level: 'warning', file, message };
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=content.js.map
|
||||||
File diff suppressed because one or more lines are too long
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
import type { DeckMetadata } from './content.ts';
|
||||||
|
interface DevOverflowAuditOptions {
|
||||||
|
cwd?: string;
|
||||||
|
debounceMs?: number;
|
||||||
|
deck: DeckMetadata;
|
||||||
|
output?: (line: string) => void;
|
||||||
|
}
|
||||||
|
export interface DevOverflowAuditController {
|
||||||
|
close: () => Promise<void>;
|
||||||
|
ready: Promise<void>;
|
||||||
|
}
|
||||||
|
export declare function startDevOverflowAudit(options: DevOverflowAuditOptions): DevOverflowAuditController;
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { watch } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { formatStaticOverflowResult, staticOverflowFingerprint } from "./overflow-report.js";
|
||||||
|
import { StaticOverflowAuditor } from "./static-overflow.js";
|
||||||
|
export function startDevOverflowAudit(options) {
|
||||||
|
const auditor = new StaticOverflowAuditor();
|
||||||
|
const cwd = options.cwd ?? process.cwd();
|
||||||
|
const debounceMs = options.debounceMs ?? 250;
|
||||||
|
const output = options.output ?? console.log;
|
||||||
|
let activeAudit;
|
||||||
|
let closed = false;
|
||||||
|
let fingerprint = '';
|
||||||
|
let queued = false;
|
||||||
|
let timer;
|
||||||
|
let watcher;
|
||||||
|
function scheduleAudit(delay = debounceMs) {
|
||||||
|
if (closed)
|
||||||
|
return;
|
||||||
|
if (timer)
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
timer = undefined;
|
||||||
|
void runAudit();
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
async function runAudit() {
|
||||||
|
if (closed)
|
||||||
|
return;
|
||||||
|
if (activeAudit) {
|
||||||
|
queued = true;
|
||||||
|
return activeAudit;
|
||||||
|
}
|
||||||
|
activeAudit = auditor.audit([options.deck])
|
||||||
|
.then((result) => {
|
||||||
|
if (closed)
|
||||||
|
return;
|
||||||
|
const nextFingerprint = staticOverflowFingerprint(result);
|
||||||
|
if (nextFingerprint !== fingerprint) {
|
||||||
|
fingerprint = nextFingerprint;
|
||||||
|
formatStaticOverflowResult(result, cwd).forEach(line => output(line));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (closed)
|
||||||
|
return;
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
const nextFingerprint = `error:${message}`;
|
||||||
|
if (nextFingerprint !== fingerprint) {
|
||||||
|
fingerprint = nextFingerprint;
|
||||||
|
output(`WARN ${path.relative(cwd, options.deck.entry)}: 无法完成静态内容检查:${message}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
activeAudit = undefined;
|
||||||
|
if (queued) {
|
||||||
|
queued = false;
|
||||||
|
scheduleAudit(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return activeAudit;
|
||||||
|
}
|
||||||
|
function startWatcher() {
|
||||||
|
const handleChange = () => scheduleAudit();
|
||||||
|
try {
|
||||||
|
watcher = watch(options.deck.directory, { recursive: true }, handleChange);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
watcher = watch(options.deck.entry, handleChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ready = (async () => {
|
||||||
|
output('\n检查开发内容中的静态溢出风险...');
|
||||||
|
startWatcher();
|
||||||
|
await runAudit();
|
||||||
|
})();
|
||||||
|
return {
|
||||||
|
ready,
|
||||||
|
async close() {
|
||||||
|
if (closed)
|
||||||
|
return;
|
||||||
|
closed = true;
|
||||||
|
watcher?.close();
|
||||||
|
if (timer)
|
||||||
|
clearTimeout(timer);
|
||||||
|
await ready.catch(() => undefined);
|
||||||
|
await activeAudit?.catch(() => undefined);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=dev-overflow-audit.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"dev-overflow-audit.js","sourceRoot":"","sources":["../../scripts/lib/dev-overflow-audit.ts"],"names":[],"mappings":"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"}
|
||||||
Vendored
+37
@@ -0,0 +1,37 @@
|
|||||||
|
export interface OverflowAuditDeck {
|
||||||
|
entry: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
export interface SlideOverflowIssue {
|
||||||
|
deck: OverflowAuditDeck;
|
||||||
|
heading: string;
|
||||||
|
horizontal: number;
|
||||||
|
slide: number;
|
||||||
|
vertical: number;
|
||||||
|
}
|
||||||
|
export interface OverflowAuditResult {
|
||||||
|
errors: Array<{
|
||||||
|
deck: OverflowAuditDeck;
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
issues: SlideOverflowIssue[];
|
||||||
|
}
|
||||||
|
interface OverflowAuditOptions {
|
||||||
|
decks: OverflowAuditDeck[];
|
||||||
|
root: string;
|
||||||
|
siteBase: string;
|
||||||
|
}
|
||||||
|
export interface OverflowAuditTarget {
|
||||||
|
deck: OverflowAuditDeck;
|
||||||
|
routerMode?: 'hash' | 'history';
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
export declare class OverflowAuditor {
|
||||||
|
private browser?;
|
||||||
|
private page?;
|
||||||
|
audit(targets: OverflowAuditTarget[]): Promise<OverflowAuditResult>;
|
||||||
|
close(): Promise<void>;
|
||||||
|
private ensurePage;
|
||||||
|
}
|
||||||
|
export declare function auditBuiltDecks(options: OverflowAuditOptions): Promise<OverflowAuditResult>;
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import { createReadStream } from 'node:fs';
|
||||||
|
import { stat } from 'node:fs/promises';
|
||||||
|
import { createServer } from 'node:http';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { chromium } from 'playwright-chromium';
|
||||||
|
const overflowTolerance = 2;
|
||||||
|
export class OverflowAuditor {
|
||||||
|
browser;
|
||||||
|
page;
|
||||||
|
async audit(targets) {
|
||||||
|
const page = await this.ensurePage();
|
||||||
|
const result = { errors: [], issues: [] };
|
||||||
|
for (const target of targets) {
|
||||||
|
try {
|
||||||
|
result.issues.push(...await auditDeck(page, target));
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
result.errors.push({
|
||||||
|
deck: target.deck,
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
async close() {
|
||||||
|
await this.browser?.close();
|
||||||
|
this.browser = undefined;
|
||||||
|
this.page = undefined;
|
||||||
|
}
|
||||||
|
async ensurePage() {
|
||||||
|
if (this.page && !this.page.isClosed())
|
||||||
|
return this.page;
|
||||||
|
await this.browser?.close().catch(() => undefined);
|
||||||
|
this.browser = await chromium.launch({ headless: true });
|
||||||
|
const context = await this.browser.newContext({ viewport: { height: 900, width: 1200 } });
|
||||||
|
await context.addInitScript(() => {
|
||||||
|
Object.defineProperty(navigator, 'wakeLock', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
async request(type) {
|
||||||
|
const sentinel = new EventTarget();
|
||||||
|
sentinel.released = false;
|
||||||
|
sentinel.type = type;
|
||||||
|
sentinel.release = async () => {
|
||||||
|
if (sentinel.released)
|
||||||
|
return;
|
||||||
|
sentinel.released = true;
|
||||||
|
sentinel.dispatchEvent(new Event('release'));
|
||||||
|
};
|
||||||
|
return sentinel;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.page = await context.newPage();
|
||||||
|
return this.page;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function auditBuiltDecks(options) {
|
||||||
|
const server = await startStaticServer(options.root, options.siteBase);
|
||||||
|
const address = server.address();
|
||||||
|
const origin = `http://127.0.0.1:${address.port}`;
|
||||||
|
const auditor = new OverflowAuditor();
|
||||||
|
try {
|
||||||
|
return await auditor.audit(options.decks.map(deck => ({
|
||||||
|
deck,
|
||||||
|
url: new URL(`${options.siteBase}${deck.slug}/`.replace(/\/{2,}/g, '/'), origin).href,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
await auditor.close();
|
||||||
|
await new Promise(resolve => server.close(() => resolve()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function auditDeck(page, target) {
|
||||||
|
const issues = [];
|
||||||
|
const overviewUrl = auditRouteUrl(target, 'overview');
|
||||||
|
await page.goto(overviewUrl.href, { timeout: 30_000, waitUntil: 'networkidle' });
|
||||||
|
await page.locator('.slidev-layout').first().waitFor({ state: 'attached', timeout: 15_000 });
|
||||||
|
await waitForPageAssets(page);
|
||||||
|
const slideNumbers = await page.locator('[class*="slidev-page-"]').evaluateAll((pages) => {
|
||||||
|
const numbers = pages.flatMap((page) => {
|
||||||
|
const pageClass = Array.from(page.classList).find(className => /^slidev-page-\d+$/.test(className));
|
||||||
|
const number = Number.parseInt(pageClass?.replace('slidev-page-', '') ?? '', 10);
|
||||||
|
return Number.isFinite(number) ? [number] : [];
|
||||||
|
});
|
||||||
|
return Array.from(new Set(numbers)).sort((a, b) => a - b);
|
||||||
|
});
|
||||||
|
if (slideNumbers.length && target.routerMode !== 'history') {
|
||||||
|
await page.evaluate((number) => {
|
||||||
|
window.location.hash = `#/${number}`;
|
||||||
|
}, slideNumbers[0]);
|
||||||
|
await page.reload({ timeout: 30_000, waitUntil: 'networkidle' });
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
await document.fonts?.ready;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const slide of slideNumbers) {
|
||||||
|
if (target.routerMode === 'history')
|
||||||
|
await page.goto(auditRouteUrl(target, String(slide)).href, { timeout: 30_000, waitUntil: 'networkidle' });
|
||||||
|
else
|
||||||
|
await page.evaluate((number) => {
|
||||||
|
window.location.hash = `#/${number}`;
|
||||||
|
}, slide);
|
||||||
|
await page.waitForFunction((number) => {
|
||||||
|
const layouts = Array.from(document.querySelectorAll(`.slidev-page-${number} .slidev-layout`));
|
||||||
|
return layouts.some(layout => layout.getBoundingClientRect().width > 0);
|
||||||
|
}, slide, { timeout: 10_000 });
|
||||||
|
await waitForSlideAssets(page, slide);
|
||||||
|
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))));
|
||||||
|
const measurement = await page.evaluate((number) => {
|
||||||
|
const layouts = Array.from(document.querySelectorAll(`.slidev-page-${number} .slidev-layout`));
|
||||||
|
const layout = layouts.find(item => item.getBoundingClientRect().width > 0) ?? layouts[0];
|
||||||
|
return {
|
||||||
|
heading: layout?.querySelector('h1, h2, h3')?.textContent?.trim() ?? '',
|
||||||
|
horizontal: layout ? Math.max(0, Math.ceil(layout.scrollWidth - layout.clientWidth)) : 0,
|
||||||
|
slide: number,
|
||||||
|
vertical: layout ? Math.max(0, Math.ceil(layout.scrollHeight - layout.clientHeight)) : 0,
|
||||||
|
};
|
||||||
|
}, slide);
|
||||||
|
if (measurement.vertical <= overflowTolerance && measurement.horizontal <= overflowTolerance)
|
||||||
|
continue;
|
||||||
|
issues.push({ deck: target.deck, ...measurement });
|
||||||
|
}
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
function auditRouteUrl(target, route) {
|
||||||
|
const url = target.routerMode === 'history'
|
||||||
|
? new URL(`${route.replace(/^\/+/, '')}${route === 'overview' ? '/' : ''}`, ensureTrailingSlash(target.url))
|
||||||
|
: new URL(target.url);
|
||||||
|
url.searchParams.set('easy-slides-audit', '1');
|
||||||
|
if (target.routerMode !== 'history')
|
||||||
|
url.hash = `#/${route}`;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
function ensureTrailingSlash(value) {
|
||||||
|
return value.endsWith('/') ? value : `${value}/`;
|
||||||
|
}
|
||||||
|
async function waitForPageAssets(page) {
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
await document.fonts?.ready;
|
||||||
|
const images = Array.from(document.images);
|
||||||
|
await Promise.race([
|
||||||
|
Promise.all(images.map(image => image.complete
|
||||||
|
? Promise.resolve()
|
||||||
|
: new Promise((resolve) => {
|
||||||
|
image.addEventListener('load', () => resolve(), { once: true });
|
||||||
|
image.addEventListener('error', () => resolve(), { once: true });
|
||||||
|
}))),
|
||||||
|
new Promise(resolve => setTimeout(resolve, 5_000)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function waitForSlideAssets(page, slide) {
|
||||||
|
await page.evaluate(async (number) => {
|
||||||
|
const layout = Array.from(document.querySelectorAll(`.slidev-page-${number} .slidev-layout`))
|
||||||
|
.find(item => item.getBoundingClientRect().width > 0);
|
||||||
|
const images = Array.from(layout?.querySelectorAll('img') ?? []);
|
||||||
|
await Promise.race([
|
||||||
|
Promise.all(images.map(image => image.complete
|
||||||
|
? Promise.resolve()
|
||||||
|
: new Promise((resolve) => {
|
||||||
|
image.addEventListener('load', () => resolve(), { once: true });
|
||||||
|
image.addEventListener('error', () => resolve(), { once: true });
|
||||||
|
}))),
|
||||||
|
new Promise(resolve => setTimeout(resolve, 5_000)),
|
||||||
|
]);
|
||||||
|
}, slide);
|
||||||
|
}
|
||||||
|
async function startStaticServer(root, siteBase) {
|
||||||
|
const server = createServer(async (request, response) => {
|
||||||
|
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
||||||
|
response.writeHead(405).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const requestUrl = new URL(request.url ?? '/', 'http://127.0.0.1');
|
||||||
|
const file = await resolveStaticFile(root, requestUrl.pathname, siteBase);
|
||||||
|
response.statusCode = 200;
|
||||||
|
response.setHeader('Content-Type', contentType(file));
|
||||||
|
if (request.method === 'HEAD') {
|
||||||
|
response.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createReadStream(file).pipe(response);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
response.writeHead(404).end('Not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
server.once('error', reject);
|
||||||
|
server.listen(0, '127.0.0.1', () => resolve());
|
||||||
|
});
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
async function resolveStaticFile(root, requestPath, siteBase) {
|
||||||
|
let pathname = decodeURIComponent(requestPath);
|
||||||
|
const basePrefix = siteBase === '/' ? '' : siteBase.replace(/\/$/, '');
|
||||||
|
if (basePrefix && pathname.startsWith(`${basePrefix}/`))
|
||||||
|
pathname = pathname.slice(basePrefix.length);
|
||||||
|
const candidate = path.resolve(root, pathname.replace(/^\/+/, ''));
|
||||||
|
const relative = path.relative(root, candidate);
|
||||||
|
if (relative.startsWith('..') || path.isAbsolute(relative))
|
||||||
|
throw new Error('Path outside static root');
|
||||||
|
const details = await stat(candidate);
|
||||||
|
return details.isDirectory() ? path.join(candidate, 'index.html') : candidate;
|
||||||
|
}
|
||||||
|
function contentType(file) {
|
||||||
|
const extension = path.extname(file).toLowerCase();
|
||||||
|
return {
|
||||||
|
'.css': 'text/css; charset=utf-8',
|
||||||
|
'.gif': 'image/gif',
|
||||||
|
'.html': 'text/html; charset=utf-8',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.js': 'text/javascript; charset=utf-8',
|
||||||
|
'.json': 'application/json; charset=utf-8',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.ttf': 'font/ttf',
|
||||||
|
'.webp': 'image/webp',
|
||||||
|
'.woff': 'font/woff',
|
||||||
|
'.woff2': 'font/woff2',
|
||||||
|
}[extension] ?? 'application/octet-stream';
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=overflow-audit.js.map
|
||||||
File diff suppressed because one or more lines are too long
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
import type { OverflowAuditResult } from './overflow-audit.ts';
|
||||||
|
import type { StaticOverflowResult } from './static-overflow.ts';
|
||||||
|
export declare function formatOverflowAuditResult(result: OverflowAuditResult, cwd?: string): string[];
|
||||||
|
export declare function overflowAuditFingerprint(result: OverflowAuditResult): string;
|
||||||
|
export declare function formatStaticOverflowResult(result: StaticOverflowResult, cwd?: string): string[];
|
||||||
|
export declare function staticOverflowFingerprint(result: StaticOverflowResult): string;
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
const overflowTolerance = 2;
|
||||||
|
export function formatOverflowAuditResult(result, cwd = process.cwd()) {
|
||||||
|
const lines = [];
|
||||||
|
for (const issue of result.issues) {
|
||||||
|
const dimensions = [
|
||||||
|
issue.vertical > overflowTolerance ? `纵向 ${issue.vertical}px` : '',
|
||||||
|
issue.horizontal > overflowTolerance ? `横向 ${issue.horizontal}px` : '',
|
||||||
|
].filter(Boolean).join('、');
|
||||||
|
const heading = issue.heading ? `(${issue.heading})` : '';
|
||||||
|
lines.push(`WARN ${path.relative(cwd, issue.deck.entry)}: 第 ${issue.slide} 页${heading}内容溢出:${dimensions};放映时可上下滚动,请优先拆页或精简内容`);
|
||||||
|
}
|
||||||
|
for (const error of result.errors)
|
||||||
|
lines.push(`WARN ${path.relative(cwd, error.deck.entry)}: 无法完成内容溢出检查:${error.message}`);
|
||||||
|
if (!lines.length)
|
||||||
|
lines.push('内容边界检查通过:未发现溢出');
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
export function overflowAuditFingerprint(result) {
|
||||||
|
return JSON.stringify({
|
||||||
|
errors: result.errors.map(error => [error.deck.entry, error.message]),
|
||||||
|
issues: result.issues.map(issue => [
|
||||||
|
issue.deck.entry,
|
||||||
|
issue.slide,
|
||||||
|
issue.heading,
|
||||||
|
issue.horizontal,
|
||||||
|
issue.vertical,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function formatStaticOverflowResult(result, cwd = process.cwd()) {
|
||||||
|
const lines = [];
|
||||||
|
for (const issue of result.issues) {
|
||||||
|
const directions = issue.directions
|
||||||
|
.map(direction => direction === 'vertical' ? '纵向' : '横向')
|
||||||
|
.join('、');
|
||||||
|
const heading = issue.heading ? `(${issue.heading})` : '';
|
||||||
|
lines.push(`WARN ${path.relative(cwd, issue.deck.entry)}: 第 ${issue.slide} 页${heading}存在静态${directions}溢出风险:${issue.reason};请优先拆页或精简内容(最终以 build 渲染检查为准)`);
|
||||||
|
}
|
||||||
|
for (const error of result.errors)
|
||||||
|
lines.push(`WARN ${path.relative(cwd, error.deck.entry)}: 无法完成静态内容检查:${error.message}`);
|
||||||
|
if (!lines.length)
|
||||||
|
lines.push('静态内容检查通过:未发现明显溢出风险(最终以 build 渲染检查为准)');
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
export function staticOverflowFingerprint(result) {
|
||||||
|
return JSON.stringify({
|
||||||
|
errors: result.errors.map(error => [error.deck.entry, error.message]),
|
||||||
|
issues: result.issues.map(issue => [
|
||||||
|
issue.deck.entry,
|
||||||
|
issue.slide,
|
||||||
|
issue.heading,
|
||||||
|
issue.directions,
|
||||||
|
issue.reason,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=overflow-report.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"overflow-report.js","sourceRoot":"","sources":["../../scripts/lib/overflow-report.ts"],"names":[],"mappings":"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"}
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import type { ChildProcess } from 'node:child_process';
|
||||||
|
interface RunSlidevOptions {
|
||||||
|
env?: NodeJS.ProcessEnv;
|
||||||
|
}
|
||||||
|
export declare function startSlidev(args: string[], options?: RunSlidevOptions): ChildProcess;
|
||||||
|
export declare function waitForSlidev(child: ChildProcess): Promise<void>;
|
||||||
|
export declare function runSlidev(args: string[], options?: RunSlidevOptions): Promise<void>;
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
import { hashPresenterToken } from "./token.js";
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const slidevCli = require.resolve('@slidev/cli/bin/slidev.mjs');
|
||||||
|
export function startSlidev(args, options = {}) {
|
||||||
|
const token = options.env?.PRESENTER_TOKEN ?? process.env.PRESENTER_TOKEN;
|
||||||
|
const presenterHash = token?.trim() ? hashPresenterToken(token) : '';
|
||||||
|
return spawn(process.execPath, [slidevCli, ...args], {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...options.env,
|
||||||
|
VITE_EASY_SLIDES_PRESENTER_HASH: presenterHash,
|
||||||
|
},
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export async function waitForSlidev(child) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
child.once('error', reject);
|
||||||
|
child.once('exit', (code, signal) => {
|
||||||
|
if (code === 0)
|
||||||
|
resolve();
|
||||||
|
else
|
||||||
|
reject(new Error(`Slidev 退出:${signal ?? `code ${code}`}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export async function runSlidev(args, options = {}) {
|
||||||
|
await waitForSlidev(startSlidev(args, options));
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=run-slidev.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"run-slidev.js","sourceRoot":"","sources":["../../scripts/lib/run-slidev.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAE1C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAE/C,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAC9C,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,4BAA4B,CAAC,CAAA;AAM/D,MAAM,UAAU,WAAW,CAAC,IAAc,EAAE,UAA4B,EAAE;IACxE,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE,eAAe,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe,CAAA;IACzE,MAAM,aAAa,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACpE,OAAO,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,GAAG,IAAI,CAAC,EAAE;QACnD,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;QAClB,GAAG,EAAE;YACH,GAAG,OAAO,CAAC,GAAG;YACd,GAAG,OAAO,CAAC,GAAG;YACd,+BAA+B,EAAE,aAAa;SAC/C;QACD,KAAK,EAAE,SAAS;KACjB,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAmB;IACrD,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QAC3B,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;YAClC,IAAI,IAAI,KAAK,CAAC;gBACZ,OAAO,EAAE,CAAA;;gBAET,MAAM,CAAC,IAAI,KAAK,CAAC,aAAa,MAAM,IAAI,QAAQ,IAAI,EAAE,EAAE,CAAC,CAAC,CAAA;QAC9D,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAc,EAAE,UAA4B,EAAE;IAC5E,MAAM,aAAa,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAA;AACjD,CAAC"}
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export declare function normalizeSiteBase(value?: string): string;
|
||||||
|
export declare function joinBase(base: string, ...parts: string[]): string;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export function normalizeSiteBase(value = '/') {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed || trimmed === '/')
|
||||||
|
return '/';
|
||||||
|
return `/${trimmed.replace(/^\/+|\/+$/g, '')}/`;
|
||||||
|
}
|
||||||
|
export function joinBase(base, ...parts) {
|
||||||
|
const normalized = normalizeSiteBase(base);
|
||||||
|
const suffix = parts
|
||||||
|
.map(part => part.replace(/^\/+|\/+$/g, ''))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('/');
|
||||||
|
return suffix ? `${normalized}${suffix}/` : normalized;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=site-base.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"site-base.js","sourceRoot":"","sources":["../../scripts/lib/site-base.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,iBAAiB,CAAC,KAAK,GAAG,GAAG;IAC3C,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;IAC5B,IAAI,CAAC,OAAO,IAAI,OAAO,KAAK,GAAG;QAC7B,OAAO,GAAG,CAAA;IAEZ,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,GAAG,CAAA;AACjD,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,GAAG,KAAe;IACvD,MAAM,UAAU,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAA;IAC1C,MAAM,MAAM,GAAG,KAAK;SACjB,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;SAC3C,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,CAAC,CAAA;IAEZ,OAAO,MAAM,CAAC,CAAC,CAAC,GAAG,UAAU,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,UAAU,CAAA;AACxD,CAAC"}
|
||||||
Vendored
+28
@@ -0,0 +1,28 @@
|
|||||||
|
import type { DeckMetadata } from './content.ts';
|
||||||
|
export type StaticOverflowDirection = 'horizontal' | 'vertical';
|
||||||
|
export interface StaticOverflowIssue {
|
||||||
|
deck: DeckMetadata;
|
||||||
|
directions: StaticOverflowDirection[];
|
||||||
|
heading: string;
|
||||||
|
reason: string;
|
||||||
|
slide: number;
|
||||||
|
}
|
||||||
|
export interface StaticOverflowResult {
|
||||||
|
errors: Array<{
|
||||||
|
deck: DeckMetadata;
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
issues: StaticOverflowIssue[];
|
||||||
|
}
|
||||||
|
interface StaticOverflowAuditorOptions {
|
||||||
|
onAnalyzeSlide?: (deck: DeckMetadata, slide: number) => void;
|
||||||
|
}
|
||||||
|
export declare class StaticOverflowAuditor {
|
||||||
|
private cache;
|
||||||
|
private onAnalyzeSlide?;
|
||||||
|
constructor(options?: StaticOverflowAuditorOptions);
|
||||||
|
audit(decks: DeckMetadata[]): Promise<StaticOverflowResult>;
|
||||||
|
private auditDeck;
|
||||||
|
}
|
||||||
|
export declare function auditStaticDecks(decks: DeckMetadata[]): Promise<StaticOverflowResult>;
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { parseSync } from '@slidev/parser';
|
||||||
|
const canvasHeight = 768;
|
||||||
|
const canvasWidth = 1024;
|
||||||
|
const slidePaddingX = 52;
|
||||||
|
const slidePaddingY = 42;
|
||||||
|
const contentHeight = canvasHeight - slidePaddingY * 2;
|
||||||
|
const contentWidth = canvasWidth - slidePaddingX * 2;
|
||||||
|
const twoColumnGap = 46;
|
||||||
|
const baseFontSize = 32;
|
||||||
|
const baseLineHeight = baseFontSize * 1.42;
|
||||||
|
const verticalRiskTolerance = 1.08;
|
||||||
|
const codeHeightPresets = new Map([
|
||||||
|
['easy-code-height-sm', 180],
|
||||||
|
['easy-code-height-md', 300],
|
||||||
|
['easy-code-height-lg', 420],
|
||||||
|
]);
|
||||||
|
export class StaticOverflowAuditor {
|
||||||
|
cache = new Map();
|
||||||
|
onAnalyzeSlide;
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.onAnalyzeSlide = options.onAnalyzeSlide;
|
||||||
|
}
|
||||||
|
async audit(decks) {
|
||||||
|
const result = { errors: [], issues: [] };
|
||||||
|
for (const deck of decks) {
|
||||||
|
try {
|
||||||
|
result.issues.push(...await this.auditDeck(deck));
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
result.errors.push({
|
||||||
|
deck,
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
async auditDeck(deck) {
|
||||||
|
const source = await readFile(deck.entry, 'utf8');
|
||||||
|
const parsed = parseSync(source, deck.entry);
|
||||||
|
const previous = this.cache.get(deck.entry) ?? new Map();
|
||||||
|
const next = new Map();
|
||||||
|
const issues = [];
|
||||||
|
parsed.slides.forEach((slide, index) => {
|
||||||
|
const number = index + 1;
|
||||||
|
const fingerprint = slide.revision ?? slide.raw;
|
||||||
|
const cached = previous.get(number);
|
||||||
|
if (cached?.fingerprint === fingerprint) {
|
||||||
|
next.set(number, cached);
|
||||||
|
issues.push(...cached.issues);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.onAnalyzeSlide?.(deck, number);
|
||||||
|
const slideIssues = analyzeSlide(deck, number, slide);
|
||||||
|
next.set(number, { fingerprint, issues: slideIssues });
|
||||||
|
issues.push(...slideIssues);
|
||||||
|
});
|
||||||
|
this.cache.set(deck.entry, next);
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function auditStaticDecks(decks) {
|
||||||
|
return await new StaticOverflowAuditor().audit(decks);
|
||||||
|
}
|
||||||
|
function analyzeSlide(deck, number, slide) {
|
||||||
|
const content = stripComments(slide.content);
|
||||||
|
const layout = String(slide.frontmatter.layout ?? (number === 1 ? 'cover' : 'default'));
|
||||||
|
const classNames = normalizeClasses(slide.frontmatter.class);
|
||||||
|
const heading = slide.title?.trim() || extractHeading(content);
|
||||||
|
const estimates = estimateSlide(content, layout, classNames, String(slide.frontmatter.imagePlacement ?? 'auto'));
|
||||||
|
const directions = [];
|
||||||
|
if (estimates.some(estimate => estimate.height > contentHeight * verticalRiskTolerance))
|
||||||
|
directions.push('vertical');
|
||||||
|
if (estimates.some(estimate => estimate.horizontalRisk))
|
||||||
|
directions.push('horizontal');
|
||||||
|
if (!directions.length)
|
||||||
|
return [];
|
||||||
|
const reason = directions.length === 2
|
||||||
|
? '估算内容高度超过画布容量,且存在无法可靠换行的宽内容'
|
||||||
|
: directions[0] === 'vertical'
|
||||||
|
? '估算内容高度超过 easy-jyy 画布容量'
|
||||||
|
: '存在无法可靠换行的宽内容';
|
||||||
|
return [{ deck, directions, heading, reason, slide: number }];
|
||||||
|
}
|
||||||
|
function estimateSlide(content, layout, classNames, imagePlacement) {
|
||||||
|
if (layout === 'two-cols') {
|
||||||
|
const slots = splitTwoColumnContent(content);
|
||||||
|
const headingEstimate = estimateRegion(slots.before, contentWidth, layout, classNames);
|
||||||
|
const columnWidth = (contentWidth - twoColumnGap) / 2;
|
||||||
|
const left = estimateRegion(slots.left, columnWidth, layout, classNames);
|
||||||
|
const right = estimateRegion(slots.right, columnWidth, layout, classNames);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
height: headingEstimate.height + Math.max(left.height, right.height),
|
||||||
|
horizontalRisk: headingEstimate.horizontalRisk || left.horizontalRisk || right.horizontalRisk,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const splitImageLayout = ['image-left', 'image-right'].includes(layout)
|
||||||
|
|| (layout === 'image-auto' && ['left', 'right'].includes(imagePlacement));
|
||||||
|
const width = splitImageLayout ? (contentWidth - 42) / 2 : contentWidth;
|
||||||
|
return [estimateRegion(content, width, layout, classNames)];
|
||||||
|
}
|
||||||
|
function splitTwoColumnContent(content) {
|
||||||
|
const leftMarker = /^::left::\s*$/m;
|
||||||
|
const rightMarker = /^::right::\s*$/m;
|
||||||
|
const leftMatch = leftMarker.exec(content);
|
||||||
|
const rightMatch = rightMarker.exec(content);
|
||||||
|
if (!leftMatch && !rightMatch)
|
||||||
|
return { before: content, left: '', right: '' };
|
||||||
|
const firstIndex = Math.min(leftMatch?.index ?? Number.POSITIVE_INFINITY, rightMatch?.index ?? Number.POSITIVE_INFINITY);
|
||||||
|
const before = content.slice(0, firstIndex);
|
||||||
|
const left = leftMatch
|
||||||
|
? content.slice(leftMatch.index + leftMatch[0].length, rightMatch && rightMatch.index > leftMatch.index ? rightMatch.index : undefined)
|
||||||
|
: '';
|
||||||
|
const right = rightMatch
|
||||||
|
? content.slice(rightMatch.index + rightMatch[0].length, leftMatch && leftMatch.index > rightMatch.index ? leftMatch.index : undefined)
|
||||||
|
: '';
|
||||||
|
return { before, left, right };
|
||||||
|
}
|
||||||
|
function estimateRegion(markdown, width, layout, classNames) {
|
||||||
|
const lines = markdown.replace(/\r/g, '').split('\n');
|
||||||
|
let height = 0;
|
||||||
|
let horizontalRisk = false;
|
||||||
|
for (let index = 0; index < lines.length;) {
|
||||||
|
const raw = lines[index];
|
||||||
|
const line = raw.trim();
|
||||||
|
if (!line || isStructuralLine(line)) {
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const fence = /^(?:`{3,}|~{3,})([^\s{]*)/.exec(line);
|
||||||
|
if (fence) {
|
||||||
|
const marker = line.startsWith('`') ? '`' : '~';
|
||||||
|
const markerLength = line.match(new RegExp(`^\\${marker}+`))?.[0].length ?? 3;
|
||||||
|
const language = fence[1].toLowerCase();
|
||||||
|
let end = index + 1;
|
||||||
|
while (end < lines.length && !new RegExp(`^\\s*\\${marker}{${markerLength},}`).test(lines[end]))
|
||||||
|
end++;
|
||||||
|
if (language === 'mermaid')
|
||||||
|
height += 180;
|
||||||
|
else
|
||||||
|
height += explicitCodeHeight(lines, index, classNames);
|
||||||
|
index = Math.min(lines.length, end + 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const heading = /^(#{1,4})\s+(.+)$/.exec(line);
|
||||||
|
if (heading) {
|
||||||
|
const level = heading[1].length;
|
||||||
|
const fontSize = level === 1 ? (layout === 'cover' ? 68 : 60) : level === 2 ? 48 : level === 3 ? 38 : 32;
|
||||||
|
const wraps = wrappedLines(heading[2], width, fontSize);
|
||||||
|
const margins = level === 1 ? 26 : level === 2 ? 39 : level === 3 ? 34 : 24;
|
||||||
|
height += wraps * fontSize * 1.15 + margins;
|
||||||
|
horizontalRisk ||= hasHorizontalRisk(heading[2], width, fontSize);
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line === '$$' || line.startsWith('$$')) {
|
||||||
|
let end = index + 1;
|
||||||
|
while (end < lines.length && !lines[end].includes('$$'))
|
||||||
|
end++;
|
||||||
|
height += 82 + Math.max(0, end - index - 2) * 38;
|
||||||
|
index = Math.min(lines.length, end + 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isStandaloneImage(line)) {
|
||||||
|
height += canvasHeight * 0.46 + 16;
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isTableRow(line) && isTableSeparator(lines[index + 1] ?? '')) {
|
||||||
|
const tableLines = [line];
|
||||||
|
index += 2;
|
||||||
|
while (index < lines.length && isTableRow(lines[index].trim())) {
|
||||||
|
tableLines.push(lines[index].trim());
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
const columnCount = Math.max(1, splitTableRow(tableLines[0]).length);
|
||||||
|
const cellWidth = width / columnCount - 28;
|
||||||
|
for (const row of tableLines) {
|
||||||
|
const rowWraps = Math.max(...splitTableRow(row).map(cell => wrappedLines(cell, cellWidth, 30)), 1);
|
||||||
|
height += rowWraps * 30 * 1.42 + 20;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isListLine(line)) {
|
||||||
|
const items = [];
|
||||||
|
while (index < lines.length && (isListLine(lines[index].trim()) || /^\s{2,}\S/.test(lines[index]))) {
|
||||||
|
if (isListLine(lines[index].trim()))
|
||||||
|
items.push(lines[index].trim().replace(/^(?:[-+*]|\d+[.)])\s+/, ''));
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
const itemHeight = items.reduce((sum, item) => sum + wrappedLines(item, width - baseFontSize * 1.2, baseFontSize) * baseLineHeight, 0);
|
||||||
|
height += itemHeight + Math.max(0, items.length - 1) * 10 + 28;
|
||||||
|
horizontalRisk ||= items.some(item => hasHorizontalRisk(item, width - baseFontSize * 1.2, baseFontSize));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.startsWith('>')) {
|
||||||
|
const quote = [];
|
||||||
|
while (index < lines.length && lines[index].trim().startsWith('>')) {
|
||||||
|
quote.push(lines[index].trim().replace(/^>\s?/, ''));
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
const fontSize = layout === 'quote' ? 46 : baseFontSize;
|
||||||
|
height += wrappedLines(quote.join(' '), width - 29, fontSize) * fontSize * (layout === 'quote' ? 1.35 : 1.42) + 48;
|
||||||
|
horizontalRisk ||= hasHorizontalRisk(quote.join(' '), width - 29, fontSize);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const paragraph = [];
|
||||||
|
while (index < lines.length && !isBlockStart(lines, index)) {
|
||||||
|
const text = stripMarkup(lines[index]);
|
||||||
|
if (text)
|
||||||
|
paragraph.push(text);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
if (paragraph.length) {
|
||||||
|
const text = paragraph.join(' ');
|
||||||
|
height += wrappedLines(text, width, baseFontSize) * baseLineHeight + 28;
|
||||||
|
horizontalRisk ||= hasHorizontalRisk(text, width, baseFontSize);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
horizontalRisk ||= explicitWidthRisk(markdown, width);
|
||||||
|
return { height, horizontalRisk };
|
||||||
|
}
|
||||||
|
function isBlockStart(lines, index) {
|
||||||
|
const line = lines[index].trim();
|
||||||
|
if (!line || isStructuralLine(line) || /^(?:`{3,}|~{3,}|#{1,4}\s|\$\$|>)/.test(line))
|
||||||
|
return true;
|
||||||
|
if (isStandaloneImage(line) || isListLine(line))
|
||||||
|
return true;
|
||||||
|
return isTableRow(line) && isTableSeparator(lines[index + 1] ?? '');
|
||||||
|
}
|
||||||
|
function explicitCodeHeight(lines, fenceIndex, classNames) {
|
||||||
|
const nearby = `${classNames.join(' ')} ${lines.slice(Math.max(0, fenceIndex - 8), fenceIndex + 1).join(' ')}`;
|
||||||
|
for (const [className, height] of codeHeightPresets) {
|
||||||
|
if (new RegExp(`(?:^|\\s|["'])${className}(?:$|\\s|["'])`).test(nearby))
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
const inlineHeight = /(?:height|max-height)\s*:\s*(\d+(?:\.\d+)?)px/i.exec(nearby);
|
||||||
|
return inlineHeight ? Number.parseFloat(inlineHeight[1]) : 0;
|
||||||
|
}
|
||||||
|
function wrappedLines(value, width, fontSize) {
|
||||||
|
const available = Math.max(1, width / fontSize);
|
||||||
|
return Math.max(1, Math.ceil(displayWidth(stripMarkup(value)) / available));
|
||||||
|
}
|
||||||
|
function displayWidth(value) {
|
||||||
|
let width = 0;
|
||||||
|
for (const character of value) {
|
||||||
|
if (/\s/.test(character))
|
||||||
|
width += 0.33;
|
||||||
|
else if (/[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe6f\uff01-\uff60\uffe0-\uffe6]/.test(character))
|
||||||
|
width += 1;
|
||||||
|
else
|
||||||
|
width += 0.56;
|
||||||
|
}
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
function hasHorizontalRisk(value, width, fontSize) {
|
||||||
|
const available = width / fontSize;
|
||||||
|
return stripMarkup(value)
|
||||||
|
.split(/\s+/)
|
||||||
|
.some(token => !/[\u2e80-\u9fff]/.test(token) && displayWidth(token) > available * 1.05);
|
||||||
|
}
|
||||||
|
function explicitWidthRisk(markdown, width) {
|
||||||
|
for (const match of markdown.matchAll(/(?:width\s*:\s*|width\s*=\s*["']?)(\d+(?:\.\d+)?)px/gi)) {
|
||||||
|
if (Number.parseFloat(match[1]) > width)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function normalizeClasses(value) {
|
||||||
|
if (Array.isArray(value))
|
||||||
|
return value.flatMap(item => String(item).split(/\s+/)).filter(Boolean);
|
||||||
|
return String(value ?? '').split(/\s+/).filter(Boolean);
|
||||||
|
}
|
||||||
|
function extractHeading(markdown) {
|
||||||
|
return /^(?:#{1,4})\s+(.+)$/m.exec(markdown)?.[1].trim() ?? '';
|
||||||
|
}
|
||||||
|
function stripComments(markdown) {
|
||||||
|
return markdown.replace(/<!--[\s\S]*?-->/g, '');
|
||||||
|
}
|
||||||
|
function stripMarkup(value) {
|
||||||
|
return value
|
||||||
|
.replace(/!\[[^\]]*\]\([^)]*\)(?:\{[^}]*\})?/g, '')
|
||||||
|
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
||||||
|
.replace(/<[^>]+>/g, ' ')
|
||||||
|
.replace(/[`*_~]/g, '')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
function isStructuralLine(line) {
|
||||||
|
return /^::(?:left|right|image)::$/i.test(line)
|
||||||
|
|| /^<\/?(?:v-clicks|div|span|template|slot)(?:\s[^>]*)?>$/i.test(line);
|
||||||
|
}
|
||||||
|
function isStandaloneImage(line) {
|
||||||
|
return /^!\[[^\]]*\]\([^)]*\)(?:\{[^}]*\})?$/.test(line);
|
||||||
|
}
|
||||||
|
function isListLine(line) {
|
||||||
|
return /^(?:[-+*]|\d+[.)])\s+/.test(line);
|
||||||
|
}
|
||||||
|
function isTableRow(line) {
|
||||||
|
return line.includes('|');
|
||||||
|
}
|
||||||
|
function isTableSeparator(line) {
|
||||||
|
return /^\s*\|?\s*:?-{3,}:?(?:\s*\|\s*:?-{3,}:?)+\s*\|?\s*$/.test(line);
|
||||||
|
}
|
||||||
|
function splitTableRow(line) {
|
||||||
|
return line.replace(/^\s*\||\|\s*$/g, '').split('|').map(cell => cell.trim());
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=static-overflow.js.map
|
||||||
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
export declare function resolveTheme(theme?: string): string;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
const directory = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const bundledTheme = path.resolve(directory, '..', 'theme');
|
||||||
|
const workspaceTheme = path.resolve(directory, '..', '..', 'packages', 'slidev-theme-easy-jyy');
|
||||||
|
export function resolveTheme(theme) {
|
||||||
|
if (theme)
|
||||||
|
return theme;
|
||||||
|
if (existsSync(path.join(bundledTheme, 'package.json')))
|
||||||
|
return bundledTheme;
|
||||||
|
if (existsSync(path.join(workspaceTheme, 'package.json')))
|
||||||
|
return workspaceTheme;
|
||||||
|
throw new Error('easy-slides 主题不存在;请重新安装或构建 @easy-slides/cli');
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=theme.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"theme.js","sourceRoot":"","sources":["../../scripts/lib/theme.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AAC9D,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,CAAA;AAC3D,MAAM,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,uBAAuB,CAAC,CAAA;AAE/F,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,IAAI,KAAK;QACP,OAAO,KAAK,CAAA;IACd,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;QACrD,OAAO,YAAY,CAAA;IACrB,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,cAAc,CAAC,CAAC;QACvD,OAAO,cAAc,CAAA;IACvB,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAA;AAChE,CAAC"}
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export declare function hashPresenterToken(token: string): string;
|
||||||
|
export declare function hasPresenterToken(env?: NodeJS.ProcessEnv): boolean;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
export function hashPresenterToken(token) {
|
||||||
|
return createHash('sha256').update(token.trim()).digest('hex');
|
||||||
|
}
|
||||||
|
export function hasPresenterToken(env = process.env) {
|
||||||
|
return Boolean(env.PRESENTER_TOKEN?.trim());
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=token.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"token.js","sourceRoot":"","sources":["../../scripts/lib/token.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAChE,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACpE,OAAO,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,EAAE,CAAC,CAAA;AAC7C,CAAC"}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { networkInterfaces } from 'node:os';
|
||||||
|
import qrcode from 'qrcode-terminal';
|
||||||
|
import { loadProjectConfig } from "./lib/config.js";
|
||||||
|
import { resolveDeck } from "./lib/content.js";
|
||||||
|
import { runSlidev } from "./lib/run-slidev.js";
|
||||||
|
import { resolveTheme } from "./lib/theme.js";
|
||||||
|
const input = process.argv.slice(2).find(argument => !argument.startsWith('-'));
|
||||||
|
const config = await loadProjectConfig();
|
||||||
|
const deck = await resolveDeck(input, process.cwd(), config.slidesDir);
|
||||||
|
const port = process.env.PORT || '3030';
|
||||||
|
const address = firstLanAddress();
|
||||||
|
const remoteToken = process.env.SLIDEV_REMOTE_TOKEN || 'easy-slides-local';
|
||||||
|
const remotePassword = encodeURIComponent(remoteToken);
|
||||||
|
const presenterUrl = `http://${address}:${port}/presenter/?password=${remotePassword}`;
|
||||||
|
const remoteUrl = `http://${address}:${port}/entry?password=${remotePassword}`;
|
||||||
|
console.log(`\n观众视图:http://${address}:${port}/`);
|
||||||
|
console.log(`演示者视图:${presenterUrl}`);
|
||||||
|
console.log(`手机遥控器:${remoteUrl}`);
|
||||||
|
qrcode.generate(presenterUrl, { small: true });
|
||||||
|
await runSlidev([
|
||||||
|
deck.entry,
|
||||||
|
'--theme',
|
||||||
|
resolveTheme(config.theme),
|
||||||
|
'--port',
|
||||||
|
port,
|
||||||
|
'--remote',
|
||||||
|
remoteToken,
|
||||||
|
'--bind',
|
||||||
|
'0.0.0.0',
|
||||||
|
]);
|
||||||
|
function firstLanAddress() {
|
||||||
|
for (const entries of Object.values(networkInterfaces())) {
|
||||||
|
for (const entry of entries ?? []) {
|
||||||
|
if (entry.family === 'IPv4' && !entry.internal)
|
||||||
|
return entry.address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'localhost';
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=present.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"present.js","sourceRoot":"","sources":["../scripts/present.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAA;AACtB,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAC3C,OAAO,MAAM,MAAM,iBAAiB,CAAA;AACpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE7C,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAA;AAC/E,MAAM,MAAM,GAAG,MAAM,iBAAiB,EAAE,CAAA;AACxC,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,SAAS,CAAC,CAAA;AACtE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,MAAM,CAAA;AACvC,MAAM,OAAO,GAAG,eAAe,EAAE,CAAA;AACjC,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,mBAAmB,CAAA;AAC1E,MAAM,cAAc,GAAG,kBAAkB,CAAC,WAAW,CAAC,CAAA;AACtD,MAAM,YAAY,GAAG,UAAU,OAAO,IAAI,IAAI,wBAAwB,cAAc,EAAE,CAAA;AACtF,MAAM,SAAS,GAAG,UAAU,OAAO,IAAI,IAAI,mBAAmB,cAAc,EAAE,CAAA;AAE9E,OAAO,CAAC,GAAG,CAAC,iBAAiB,OAAO,IAAI,IAAI,GAAG,CAAC,CAAA;AAChD,OAAO,CAAC,GAAG,CAAC,SAAS,YAAY,EAAE,CAAC,CAAA;AACpC,OAAO,CAAC,GAAG,CAAC,SAAS,SAAS,EAAE,CAAC,CAAA;AACjC,MAAM,CAAC,QAAQ,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;AAE9C,MAAM,SAAS,CAAC;IACd,IAAI,CAAC,KAAK;IACV,SAAS;IACT,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC;IAC1B,QAAQ;IACR,IAAI;IACJ,UAAU;IACV,WAAW;IACX,QAAQ;IACR,SAAS;CACV,CAAC,CAAA;AAEF,SAAS,eAAe;IACtB,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,CAAC,EAAE,CAAC;QACzD,KAAK,MAAM,KAAK,IAAI,OAAO,IAAI,EAAE,EAAE,CAAC;YAClC,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ;gBAC5C,OAAO,KAAK,CAAC,OAAO,CAAA;QACxB,CAAC;IACH,CAAC;IACD,OAAO,WAAW,CAAA;AACpB,CAAC"}
|
||||||
@@ -0,0 +1,699 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { sharedState, useNav } from '@slidev/client'
|
||||||
|
import { onBeforeUnmount, onMounted, watch } from 'vue'
|
||||||
|
|
||||||
|
type ScalableKind = 'table' | 'code'
|
||||||
|
|
||||||
|
interface ScaleMessage {
|
||||||
|
key: string
|
||||||
|
kind: ScalableKind
|
||||||
|
scale: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableBaseStyle {
|
||||||
|
tableFontSize: number
|
||||||
|
tableInlineFontSize: string
|
||||||
|
cells: Array<{
|
||||||
|
element: HTMLElement
|
||||||
|
inlinePaddingBottom: string
|
||||||
|
inlinePaddingLeft: string
|
||||||
|
inlinePaddingRight: string
|
||||||
|
inlinePaddingTop: string
|
||||||
|
paddingBottom: number
|
||||||
|
paddingLeft: number
|
||||||
|
paddingRight: number
|
||||||
|
paddingTop: number
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CodeBaseStyle {
|
||||||
|
codeFontSize: number
|
||||||
|
createdHost: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImageInteraction {
|
||||||
|
ariaLabel: string | null
|
||||||
|
role: string | null
|
||||||
|
tabIndex: string | null
|
||||||
|
click: (event: MouseEvent) => void
|
||||||
|
keydown: (event: KeyboardEvent) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImagePreviewRequest {
|
||||||
|
alt: string
|
||||||
|
id: string
|
||||||
|
sentAt: number
|
||||||
|
src: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImagePreviewAcknowledgement {
|
||||||
|
requestId: string
|
||||||
|
sentAt: number
|
||||||
|
viewerId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EasySlidesSharedState {
|
||||||
|
easySlidesImagePreview?: {
|
||||||
|
acknowledgement?: ImagePreviewAcknowledgement
|
||||||
|
request?: ImagePreviewRequest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PendingImagePreview {
|
||||||
|
cleanupTimer?: ReturnType<typeof setTimeout>
|
||||||
|
fallbackOpened: boolean
|
||||||
|
fallbackTimer: ReturnType<typeof setTimeout>
|
||||||
|
image: HTMLImageElement
|
||||||
|
requestId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const nav = useNav()
|
||||||
|
const easySharedState = sharedState as typeof sharedState & EasySlidesSharedState
|
||||||
|
const clientId = createClientId()
|
||||||
|
const mountedAt = Date.now()
|
||||||
|
const scales = new Map<string, number>()
|
||||||
|
const tableStyles = new WeakMap<HTMLTableElement, TableBaseStyle>()
|
||||||
|
const codeStyles = new WeakMap<HTMLPreElement, CodeBaseStyle>()
|
||||||
|
const imageInteractions = new Map<HTMLImageElement, ImageInteraction>()
|
||||||
|
let observer: MutationObserver | undefined
|
||||||
|
let resizeObserver: ResizeObserver | undefined
|
||||||
|
let channel: BroadcastChannel | undefined
|
||||||
|
let lightbox: HTMLElement | undefined
|
||||||
|
let lightboxImage: HTMLImageElement | undefined
|
||||||
|
let lightboxClose: HTMLButtonElement | undefined
|
||||||
|
let activeImage: HTMLImageElement | undefined
|
||||||
|
let pendingImagePreview: PendingImagePreview | undefined
|
||||||
|
let stopAcknowledgementWatch: (() => void) | undefined
|
||||||
|
let stopRequestWatch: (() => void) | undefined
|
||||||
|
let fitFrame: number | undefined
|
||||||
|
let lastHandledRequestId = easySharedState.easySlidesImagePreview?.request?.id ?? ''
|
||||||
|
let bodyOverflow = ''
|
||||||
|
|
||||||
|
function createClientId() {
|
||||||
|
if (typeof crypto.randomUUID === 'function')
|
||||||
|
return crypto.randomUUID()
|
||||||
|
return `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInteractiveRoute() {
|
||||||
|
if (/(?:^|\/)(?:overview|export|editor|notes|notes-edit|print)(?:\/|$)/.test(window.location.pathname))
|
||||||
|
return false
|
||||||
|
return nav.isPresenter.value || nav.hasPrimarySlide.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAuditRoute() {
|
||||||
|
return new URLSearchParams(window.location.search).has('easy-slides-audit')
|
||||||
|
}
|
||||||
|
|
||||||
|
function isProjectionRoute() {
|
||||||
|
return !isAuditRoute() && isInteractiveRoute() && !nav.isPresenter.value && nav.hasPrimarySlide.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampScale(scale: number) {
|
||||||
|
return Math.min(1.6, Math.max(0.6, Math.round(scale * 10) / 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSlideNumber(element: Element) {
|
||||||
|
const page = element.closest<HTMLElement>('[class*="slidev-page-"]')
|
||||||
|
const pageClass = page && Array.from(page.classList).find(className => /^slidev-page-\d+$/.test(className))
|
||||||
|
return pageClass?.replace('slidev-page-', '') ?? 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentKey(element: Element, kind: ScalableKind) {
|
||||||
|
const layout = element.closest('.slidev-layout')
|
||||||
|
const selector = kind === 'table' ? 'table' : 'pre'
|
||||||
|
const elements = layout ? Array.from(layout.querySelectorAll(selector)) : [element]
|
||||||
|
const index = Math.max(0, elements.indexOf(element))
|
||||||
|
return `slide:${findSlideNumber(element)}:${kind}:${index}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function createButton(label: string, text: string, onClick: () => void) {
|
||||||
|
const button = document.createElement('button')
|
||||||
|
button.type = 'button'
|
||||||
|
button.className = 'easy-content-scale-button'
|
||||||
|
button.setAttribute('aria-label', label)
|
||||||
|
button.title = label
|
||||||
|
button.textContent = text
|
||||||
|
button.addEventListener('pointerdown', event => event.stopPropagation())
|
||||||
|
button.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
onClick()
|
||||||
|
})
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
function kindLabel(kind: ScalableKind) {
|
||||||
|
return kind === 'table' ? '表格' : '代码'
|
||||||
|
}
|
||||||
|
|
||||||
|
function createToolbar(key: string, kind: ScalableKind) {
|
||||||
|
const label = kindLabel(kind)
|
||||||
|
const toolbar = document.createElement('nav')
|
||||||
|
toolbar.className = 'easy-content-scale-toolbar'
|
||||||
|
toolbar.setAttribute('aria-label', `${label}缩放`)
|
||||||
|
toolbar.dataset.easyScaleKey = key
|
||||||
|
toolbar.dataset.easyScaleKind = kind
|
||||||
|
|
||||||
|
const decrease = createButton(`缩小${label}`, '−', () => updateScale(key, kind, currentScale(key) - 0.1))
|
||||||
|
const reset = createButton(`重置${label}缩放`, '100%', () => updateScale(key, kind, 1))
|
||||||
|
reset.classList.add('easy-content-scale-reset')
|
||||||
|
const increase = createButton(`放大${label}`, '+', () => updateScale(key, kind, currentScale(key) + 0.1))
|
||||||
|
toolbar.append(decrease, reset, increase)
|
||||||
|
return toolbar
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentScale(key: string) {
|
||||||
|
return scales.get(key) ?? 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateScale(key: string, kind: ScalableKind, requestedScale: number, shouldBroadcast = true) {
|
||||||
|
const scale = clampScale(requestedScale)
|
||||||
|
scales.set(key, scale)
|
||||||
|
applyScale(key, kind, scale)
|
||||||
|
if (shouldBroadcast)
|
||||||
|
channel?.postMessage({ key, kind, scale } satisfies ScaleMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateResetButton(host: HTMLElement, scale: number) {
|
||||||
|
const reset = host.querySelector<HTMLButtonElement>('.easy-content-scale-reset')
|
||||||
|
if (!reset)
|
||||||
|
return
|
||||||
|
const percentage = `${Math.round(scale * 100)}%`
|
||||||
|
reset.textContent = percentage
|
||||||
|
reset.title = `${reset.getAttribute('aria-label')}(当前 ${percentage})`
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyScale(key: string, kind: ScalableKind, scale: number) {
|
||||||
|
const hostSelector = kind === 'table' ? '.easy-content-table-host' : '.easy-content-code-host'
|
||||||
|
document.querySelectorAll<HTMLElement>(hostSelector).forEach((host) => {
|
||||||
|
if (host.dataset.easyScaleKey !== key)
|
||||||
|
return
|
||||||
|
updateResetButton(host, scale)
|
||||||
|
|
||||||
|
if (kind === 'code') {
|
||||||
|
const code = host.querySelector<HTMLPreElement>('pre')
|
||||||
|
const base = code && codeStyles.get(code)
|
||||||
|
if (!code || !base)
|
||||||
|
return
|
||||||
|
if (scale === 1)
|
||||||
|
host.style.removeProperty('--easy-code-scaled-font-size')
|
||||||
|
else
|
||||||
|
host.style.setProperty('--easy-code-scaled-font-size', `${base.codeFontSize * scale}px`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = host.querySelector<HTMLTableElement>('table')
|
||||||
|
const base = table && tableStyles.get(table)
|
||||||
|
if (!table || !base)
|
||||||
|
return
|
||||||
|
if (scale === 1) {
|
||||||
|
table.style.fontSize = base.tableInlineFontSize
|
||||||
|
base.cells.forEach((cell) => {
|
||||||
|
cell.element.style.paddingTop = cell.inlinePaddingTop
|
||||||
|
cell.element.style.paddingRight = cell.inlinePaddingRight
|
||||||
|
cell.element.style.paddingBottom = cell.inlinePaddingBottom
|
||||||
|
cell.element.style.paddingLeft = cell.inlinePaddingLeft
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
table.style.fontSize = `${base.tableFontSize * scale}px`
|
||||||
|
base.cells.forEach((cell) => {
|
||||||
|
cell.element.style.paddingTop = `${cell.paddingTop * scale}px`
|
||||||
|
cell.element.style.paddingRight = `${cell.paddingRight * scale}px`
|
||||||
|
cell.element.style.paddingBottom = `${cell.paddingBottom * scale}px`
|
||||||
|
cell.element.style.paddingLeft = `${cell.paddingLeft * scale}px`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLightbox() {
|
||||||
|
const overlay = document.createElement('div')
|
||||||
|
overlay.className = 'easy-image-lightbox'
|
||||||
|
overlay.hidden = true
|
||||||
|
overlay.setAttribute('role', 'dialog')
|
||||||
|
overlay.setAttribute('aria-modal', 'true')
|
||||||
|
overlay.setAttribute('aria-label', '图片全屏预览')
|
||||||
|
|
||||||
|
const preview = document.createElement('img')
|
||||||
|
preview.className = 'easy-image-lightbox-preview'
|
||||||
|
preview.alt = ''
|
||||||
|
|
||||||
|
const close = document.createElement('button')
|
||||||
|
close.type = 'button'
|
||||||
|
close.className = 'easy-image-lightbox-close'
|
||||||
|
close.setAttribute('aria-label', '关闭图片全屏预览')
|
||||||
|
close.title = '关闭(Esc)'
|
||||||
|
close.textContent = '×'
|
||||||
|
close.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
closeLightbox()
|
||||||
|
})
|
||||||
|
|
||||||
|
overlay.addEventListener('pointerdown', event => event.stopPropagation())
|
||||||
|
overlay.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
if (event.target === overlay)
|
||||||
|
closeLightbox()
|
||||||
|
})
|
||||||
|
overlay.append(preview, close)
|
||||||
|
document.body.append(overlay)
|
||||||
|
lightbox = overlay
|
||||||
|
lightboxImage = preview
|
||||||
|
lightboxClose = close
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLightbox(src: string, alt: string, image?: HTMLImageElement) {
|
||||||
|
if (!lightbox || !lightboxImage || !lightboxClose)
|
||||||
|
return
|
||||||
|
const wasHidden = lightbox.hidden
|
||||||
|
activeImage = image
|
||||||
|
lightboxImage.src = src
|
||||||
|
lightboxImage.alt = alt
|
||||||
|
lightbox.hidden = false
|
||||||
|
if (wasHidden)
|
||||||
|
bodyOverflow = document.body.style.overflow
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
document.addEventListener('keydown', handleLightboxKeydown, true)
|
||||||
|
lightboxClose.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLightbox(image: HTMLImageElement) {
|
||||||
|
showLightbox(image.currentSrc || image.src, image.alt, image)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLightbox(restoreFocus = true) {
|
||||||
|
if (!lightbox || lightbox.hidden)
|
||||||
|
return
|
||||||
|
document.removeEventListener('keydown', handleLightboxKeydown, true)
|
||||||
|
lightbox.hidden = true
|
||||||
|
if (lightboxImage)
|
||||||
|
lightboxImage.removeAttribute('src')
|
||||||
|
document.body.style.overflow = bodyOverflow
|
||||||
|
if (restoreFocus)
|
||||||
|
activeImage?.focus()
|
||||||
|
activeImage = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLightboxKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
closeLightbox()
|
||||||
|
}
|
||||||
|
else if (event.key === 'Tab') {
|
||||||
|
event.preventDefault()
|
||||||
|
lightboxClose?.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateImagePreviewState(patch: NonNullable<EasySlidesSharedState['easySlidesImagePreview']>) {
|
||||||
|
easySharedState.easySlidesImagePreview = {
|
||||||
|
...easySharedState.easySlidesImagePreview,
|
||||||
|
...patch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPendingImagePreview() {
|
||||||
|
if (!pendingImagePreview)
|
||||||
|
return
|
||||||
|
clearTimeout(pendingImagePreview.fallbackTimer)
|
||||||
|
if (pendingImagePreview.cleanupTimer)
|
||||||
|
clearTimeout(pendingImagePreview.cleanupTimer)
|
||||||
|
pendingImagePreview = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestProjectionLightbox(image: HTMLImageElement) {
|
||||||
|
clearPendingImagePreview()
|
||||||
|
const request: ImagePreviewRequest = {
|
||||||
|
alt: image.alt,
|
||||||
|
id: createClientId(),
|
||||||
|
sentAt: Date.now(),
|
||||||
|
src: image.currentSrc || image.src,
|
||||||
|
}
|
||||||
|
const pending: PendingImagePreview = {
|
||||||
|
fallbackOpened: false,
|
||||||
|
fallbackTimer: setTimeout(() => {
|
||||||
|
if (pendingImagePreview?.requestId !== request.id)
|
||||||
|
return
|
||||||
|
pendingImagePreview.fallbackOpened = true
|
||||||
|
openLightbox(image)
|
||||||
|
pendingImagePreview.cleanupTimer = setTimeout(() => {
|
||||||
|
if (pendingImagePreview?.requestId === request.id)
|
||||||
|
pendingImagePreview = undefined
|
||||||
|
}, 10_000)
|
||||||
|
}, 500),
|
||||||
|
image,
|
||||||
|
requestId: request.id,
|
||||||
|
}
|
||||||
|
pendingImagePreview = pending
|
||||||
|
updateImagePreviewState({ request })
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateImage(image: HTMLImageElement) {
|
||||||
|
if (nav.isPresenter.value)
|
||||||
|
requestProjectionLightbox(image)
|
||||||
|
else
|
||||||
|
openLightbox(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImagePreviewAcknowledgement(acknowledgement: ImagePreviewAcknowledgement | undefined) {
|
||||||
|
if (!acknowledgement || !pendingImagePreview || acknowledgement.requestId !== pendingImagePreview.requestId)
|
||||||
|
return
|
||||||
|
const pending = pendingImagePreview
|
||||||
|
clearPendingImagePreview()
|
||||||
|
if (pending.fallbackOpened && activeImage === pending.image)
|
||||||
|
closeLightbox(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImagePreviewRequest(request: ImagePreviewRequest | undefined) {
|
||||||
|
if (!request || !isProjectionRoute() || request.id === lastHandledRequestId)
|
||||||
|
return
|
||||||
|
if (request.sentAt < mountedAt - 100 || Date.now() - request.sentAt > 10_000)
|
||||||
|
return
|
||||||
|
lastHandledRequestId = request.id
|
||||||
|
showLightbox(request.src, request.alt)
|
||||||
|
updateImagePreviewState({
|
||||||
|
acknowledgement: {
|
||||||
|
requestId: request.id,
|
||||||
|
sentAt: Date.now(),
|
||||||
|
viewerId: clientId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function enhanceImage(image: HTMLImageElement) {
|
||||||
|
if (imageInteractions.has(image) || image.closest('a, button') || image.closest('.easy-image-lightbox'))
|
||||||
|
return
|
||||||
|
|
||||||
|
const click = (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
activateImage(image)
|
||||||
|
}
|
||||||
|
const keydown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key !== 'Enter' && event.key !== ' ')
|
||||||
|
return
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
activateImage(image)
|
||||||
|
}
|
||||||
|
imageInteractions.set(image, {
|
||||||
|
ariaLabel: image.getAttribute('aria-label'),
|
||||||
|
role: image.getAttribute('role'),
|
||||||
|
tabIndex: image.getAttribute('tabindex'),
|
||||||
|
click,
|
||||||
|
keydown,
|
||||||
|
})
|
||||||
|
image.classList.add('easy-image-preview-trigger')
|
||||||
|
image.tabIndex = 0
|
||||||
|
image.setAttribute('role', 'button')
|
||||||
|
if (!image.hasAttribute('aria-label'))
|
||||||
|
image.setAttribute('aria-label', image.alt ? `全屏查看图片:${image.alt}` : '全屏查看图片')
|
||||||
|
image.addEventListener('click', click)
|
||||||
|
image.addEventListener('keydown', keydown)
|
||||||
|
}
|
||||||
|
|
||||||
|
function enhanceTable(table: HTMLTableElement) {
|
||||||
|
if (table.closest('.easy-content-table-host'))
|
||||||
|
return
|
||||||
|
|
||||||
|
const key = contentKey(table, 'table')
|
||||||
|
const cells = Array.from(table.querySelectorAll<HTMLElement>('th, td')).map((element) => {
|
||||||
|
const style = getComputedStyle(element)
|
||||||
|
return {
|
||||||
|
element,
|
||||||
|
inlinePaddingBottom: element.style.paddingBottom,
|
||||||
|
inlinePaddingLeft: element.style.paddingLeft,
|
||||||
|
inlinePaddingRight: element.style.paddingRight,
|
||||||
|
inlinePaddingTop: element.style.paddingTop,
|
||||||
|
paddingBottom: Number.parseFloat(style.paddingBottom),
|
||||||
|
paddingLeft: Number.parseFloat(style.paddingLeft),
|
||||||
|
paddingRight: Number.parseFloat(style.paddingRight),
|
||||||
|
paddingTop: Number.parseFloat(style.paddingTop),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
tableStyles.set(table, {
|
||||||
|
tableFontSize: Number.parseFloat(getComputedStyle(table).fontSize),
|
||||||
|
tableInlineFontSize: table.style.fontSize,
|
||||||
|
cells,
|
||||||
|
})
|
||||||
|
|
||||||
|
const host = document.createElement('div')
|
||||||
|
host.className = 'easy-content-scale-host easy-content-table-host'
|
||||||
|
host.dataset.easyScaleKey = key
|
||||||
|
host.dataset.easyScaleKind = 'table'
|
||||||
|
table.replaceWith(host)
|
||||||
|
host.append(table, createToolbar(key, 'table'))
|
||||||
|
applyScale(key, 'table', currentScale(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
function enhanceCode(code: HTMLPreElement) {
|
||||||
|
if (code.closest('.easy-content-code-host'))
|
||||||
|
return
|
||||||
|
|
||||||
|
const key = contentKey(code, 'code')
|
||||||
|
let host = code.closest<HTMLElement>('.slidev-code-wrapper')
|
||||||
|
const createdHost = !host
|
||||||
|
if (!host) {
|
||||||
|
host = document.createElement('div')
|
||||||
|
code.replaceWith(host)
|
||||||
|
host.append(code)
|
||||||
|
}
|
||||||
|
host.classList.add('easy-content-scale-host', 'easy-content-code-host')
|
||||||
|
host.dataset.easyScaleKey = key
|
||||||
|
host.dataset.easyScaleKind = 'code'
|
||||||
|
host.dataset.easyCodeHostCreated = createdHost ? 'true' : 'false'
|
||||||
|
codeStyles.set(code, {
|
||||||
|
codeFontSize: Number.parseFloat(getComputedStyle(code).fontSize),
|
||||||
|
createdHost,
|
||||||
|
})
|
||||||
|
host.append(createToolbar(key, 'code'))
|
||||||
|
applyScale(key, 'code', currentScale(key))
|
||||||
|
resizeObserver?.observe(code.closest('.slidev-layout') ?? host)
|
||||||
|
scheduleCodeHeightFit()
|
||||||
|
}
|
||||||
|
|
||||||
|
function enhanceContent(root: ParentNode = document) {
|
||||||
|
const images = Array.from(root.querySelectorAll<HTMLImageElement>('.slidev-layout img'))
|
||||||
|
const tables = Array.from(root.querySelectorAll<HTMLTableElement>('.slidev-layout table'))
|
||||||
|
const codes = Array.from(root.querySelectorAll<HTMLPreElement>('.slidev-layout pre'))
|
||||||
|
if (root instanceof HTMLImageElement && root.closest('.slidev-layout'))
|
||||||
|
images.unshift(root)
|
||||||
|
if (root instanceof HTMLTableElement && root.closest('.slidev-layout'))
|
||||||
|
tables.unshift(root)
|
||||||
|
if (root instanceof HTMLPreElement && root.closest('.slidev-layout'))
|
||||||
|
codes.unshift(root)
|
||||||
|
images.forEach(enhanceImage)
|
||||||
|
tables.forEach(enhanceTable)
|
||||||
|
codes.forEach(enhanceCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
function codeHeightRegion(code: HTMLPreElement) {
|
||||||
|
return code.closest<HTMLElement>('.easy-two-cols-grid > div')
|
||||||
|
?? code.closest<HTMLElement>('.slidev-layout')
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasExplicitCodeHeight(code: HTMLPreElement) {
|
||||||
|
if (code.style.height || code.style.maxHeight)
|
||||||
|
return true
|
||||||
|
return Boolean(getComputedStyle(code).getPropertyValue('--easy-code-height').trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
function availableCodeHeight(code: HTMLPreElement, region: HTMLElement) {
|
||||||
|
const layout = code.closest<HTMLElement>('.slidev-layout')
|
||||||
|
if (!layout || layout.clientHeight <= 0)
|
||||||
|
return 0
|
||||||
|
const layoutRect = layout.getBoundingClientRect()
|
||||||
|
const regionRect = region.getBoundingClientRect()
|
||||||
|
const codeRect = code.getBoundingClientRect()
|
||||||
|
if (layoutRect.height <= 0 || regionRect.height <= 0 || codeRect.width <= 0)
|
||||||
|
return 0
|
||||||
|
const scale = layoutRect.height / layout.clientHeight
|
||||||
|
if (!Number.isFinite(scale) || scale <= 0)
|
||||||
|
return 0
|
||||||
|
const regionPaddingBottom = Number.parseFloat(getComputedStyle(region).paddingBottom) || 0
|
||||||
|
const layoutPaddingBottom = Number.parseFloat(getComputedStyle(layout).paddingBottom) || 0
|
||||||
|
const regionBottom = regionRect.bottom - regionPaddingBottom * scale
|
||||||
|
const layoutBottom = layoutRect.bottom - layoutPaddingBottom * scale
|
||||||
|
return Math.max(0, Math.floor((Math.min(regionBottom, layoutBottom) - codeRect.top) / scale))
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHostLength(host: HTMLElement, property: string, value?: number) {
|
||||||
|
const next = value && value > 0 ? `${value}px` : ''
|
||||||
|
if (host.style.getPropertyValue(property) === next)
|
||||||
|
return
|
||||||
|
if (next)
|
||||||
|
host.style.setProperty(property, next)
|
||||||
|
else
|
||||||
|
host.style.removeProperty(property)
|
||||||
|
}
|
||||||
|
|
||||||
|
function fitCodeHeights() {
|
||||||
|
const groups = new Map<HTMLElement, HTMLPreElement[]>()
|
||||||
|
document.querySelectorAll<HTMLPreElement>('.easy-content-code-host pre').forEach((code) => {
|
||||||
|
const host = code.closest<HTMLElement>('.easy-content-code-host')
|
||||||
|
if (!host)
|
||||||
|
return
|
||||||
|
if (hasExplicitCodeHeight(code)) {
|
||||||
|
setHostLength(host, '--easy-code-auto-height')
|
||||||
|
setHostLength(host, '--easy-code-auto-max-height')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const region = codeHeightRegion(code)
|
||||||
|
if (!region)
|
||||||
|
return
|
||||||
|
const group = groups.get(region) ?? []
|
||||||
|
group.push(code)
|
||||||
|
groups.set(region, group)
|
||||||
|
})
|
||||||
|
|
||||||
|
groups.forEach((codes, region) => {
|
||||||
|
const single = codes.length === 1
|
||||||
|
codes.forEach((code) => {
|
||||||
|
const host = code.closest<HTMLElement>('.easy-content-code-host')
|
||||||
|
if (!host)
|
||||||
|
return
|
||||||
|
const available = availableCodeHeight(code, region)
|
||||||
|
if (!available)
|
||||||
|
return
|
||||||
|
setHostLength(host, '--easy-code-auto-height', single ? available : undefined)
|
||||||
|
setHostLength(host, '--easy-code-auto-max-height', available)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleCodeHeightFit() {
|
||||||
|
if (fitFrame !== undefined)
|
||||||
|
return
|
||||||
|
fitFrame = requestAnimationFrame(() => {
|
||||||
|
fitFrame = undefined
|
||||||
|
fitCodeHeights()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupImage(image: HTMLImageElement, interaction: ImageInteraction) {
|
||||||
|
image.removeEventListener('click', interaction.click)
|
||||||
|
image.removeEventListener('keydown', interaction.keydown)
|
||||||
|
image.classList.remove('easy-image-preview-trigger')
|
||||||
|
restoreAttribute(image, 'aria-label', interaction.ariaLabel)
|
||||||
|
restoreAttribute(image, 'role', interaction.role)
|
||||||
|
restoreAttribute(image, 'tabindex', interaction.tabIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreAttribute(element: Element, name: string, value: string | null) {
|
||||||
|
if (value === null)
|
||||||
|
element.removeAttribute(name)
|
||||||
|
else
|
||||||
|
element.setAttribute(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupTableHosts() {
|
||||||
|
document.querySelectorAll<HTMLElement>('.easy-content-table-host').forEach((host) => {
|
||||||
|
const table = host.querySelector<HTMLTableElement>('table')
|
||||||
|
const base = table && tableStyles.get(table)
|
||||||
|
if (!table)
|
||||||
|
return
|
||||||
|
if (base) {
|
||||||
|
table.style.fontSize = base.tableInlineFontSize
|
||||||
|
base.cells.forEach((cell) => {
|
||||||
|
cell.element.style.paddingTop = cell.inlinePaddingTop
|
||||||
|
cell.element.style.paddingRight = cell.inlinePaddingRight
|
||||||
|
cell.element.style.paddingBottom = cell.inlinePaddingBottom
|
||||||
|
cell.element.style.paddingLeft = cell.inlinePaddingLeft
|
||||||
|
})
|
||||||
|
}
|
||||||
|
host.replaceWith(table)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupCodeHosts() {
|
||||||
|
document.querySelectorAll<HTMLElement>('.easy-content-code-host').forEach((host) => {
|
||||||
|
const code = host.querySelector<HTMLPreElement>('pre')
|
||||||
|
const base = code && codeStyles.get(code)
|
||||||
|
host.querySelector(':scope > .easy-content-scale-toolbar')?.remove()
|
||||||
|
host.style.removeProperty('--easy-code-scaled-font-size')
|
||||||
|
host.style.removeProperty('--easy-code-auto-height')
|
||||||
|
host.style.removeProperty('--easy-code-auto-max-height')
|
||||||
|
host.classList.remove('easy-content-scale-host', 'easy-content-code-host')
|
||||||
|
delete host.dataset.easyScaleKey
|
||||||
|
delete host.dataset.easyScaleKind
|
||||||
|
delete host.dataset.easyCodeHostCreated
|
||||||
|
if (code && base?.createdHost)
|
||||||
|
host.replaceWith(code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupContent() {
|
||||||
|
clearPendingImagePreview()
|
||||||
|
closeLightbox()
|
||||||
|
imageInteractions.forEach((interaction, image) => cleanupImage(image, interaction))
|
||||||
|
imageInteractions.clear()
|
||||||
|
lightbox?.remove()
|
||||||
|
lightbox = undefined
|
||||||
|
lightboxImage = undefined
|
||||||
|
lightboxClose = undefined
|
||||||
|
cleanupTableHosts()
|
||||||
|
cleanupCodeHosts()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isScaleMessage(value: unknown): value is ScaleMessage {
|
||||||
|
if (!value || typeof value !== 'object')
|
||||||
|
return false
|
||||||
|
const message = value as Partial<ScaleMessage>
|
||||||
|
return typeof message.key === 'string'
|
||||||
|
&& (message.kind === 'table' || message.kind === 'code')
|
||||||
|
&& typeof message.scale === 'number'
|
||||||
|
&& Number.isFinite(message.scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!isInteractiveRoute())
|
||||||
|
return
|
||||||
|
createLightbox()
|
||||||
|
if ('BroadcastChannel' in window) {
|
||||||
|
channel = new BroadcastChannel(`easy-slides:content-scale:${import.meta.env.BASE_URL}`)
|
||||||
|
channel.addEventListener('message', (event) => {
|
||||||
|
if (isScaleMessage(event.data))
|
||||||
|
updateScale(event.data.key, event.data.kind, event.data.scale, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
stopAcknowledgementWatch = watch(
|
||||||
|
() => easySharedState.easySlidesImagePreview?.acknowledgement,
|
||||||
|
handleImagePreviewAcknowledgement,
|
||||||
|
)
|
||||||
|
stopRequestWatch = watch(
|
||||||
|
() => easySharedState.easySlidesImagePreview?.request,
|
||||||
|
handleImagePreviewRequest,
|
||||||
|
)
|
||||||
|
resizeObserver = new ResizeObserver(scheduleCodeHeightFit)
|
||||||
|
enhanceContent()
|
||||||
|
document.querySelectorAll<HTMLElement>('.slidev-layout').forEach(layout => resizeObserver?.observe(layout))
|
||||||
|
observer = new MutationObserver((records) => {
|
||||||
|
records.forEach(record => enhanceContent(record.target as ParentNode))
|
||||||
|
scheduleCodeHeightFit()
|
||||||
|
})
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true })
|
||||||
|
window.addEventListener('resize', scheduleCodeHeightFit)
|
||||||
|
void document.fonts?.ready.then(scheduleCodeHeightFit)
|
||||||
|
scheduleCodeHeightFit()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
observer?.disconnect()
|
||||||
|
resizeObserver?.disconnect()
|
||||||
|
channel?.close()
|
||||||
|
stopAcknowledgementWatch?.()
|
||||||
|
stopRequestWatch?.()
|
||||||
|
window.removeEventListener('resize', scheduleCodeHeightFit)
|
||||||
|
if (fitFrame !== undefined)
|
||||||
|
cancelAnimationFrame(fitFrame)
|
||||||
|
cleanupContent()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span hidden aria-hidden="true" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const configuredHash = import.meta.env.VITE_EASY_SLIDES_PRESENTER_HASH || ''
|
||||||
|
const isPresenter = computed(() => /\/presenter(?:\/\d+)?\/?$/.test(window.location.pathname))
|
||||||
|
const storageKey = `easy-slides:presenter:${import.meta.env.BASE_URL}`
|
||||||
|
const unlocked = ref(false)
|
||||||
|
const token = ref('')
|
||||||
|
const error = ref('')
|
||||||
|
const noteSize = ref(20)
|
||||||
|
const contrast = ref(false)
|
||||||
|
const gate = ref<HTMLDialogElement>()
|
||||||
|
const tokenInput = ref<HTMLInputElement>()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
unlocked.value = sessionStorage.getItem(storageKey) === configuredHash
|
||||||
|
|| (!configuredHash && import.meta.env.DEV)
|
||||||
|
syncBodyState()
|
||||||
|
await syncPresenterGate()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (gate.value?.open)
|
||||||
|
gate.value.close()
|
||||||
|
document.body.classList.remove('easy-presenter-active', 'easy-presenter-locked', 'easy-notes-contrast')
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(unlocked, () => void syncPresenterGate())
|
||||||
|
|
||||||
|
async function unlock() {
|
||||||
|
error.value = ''
|
||||||
|
const candidate = await sha256(token.value)
|
||||||
|
if (candidate !== configuredHash) {
|
||||||
|
error.value = 'Token 不正确'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sessionStorage.setItem(storageKey, configuredHash)
|
||||||
|
unlocked.value = true
|
||||||
|
token.value = ''
|
||||||
|
await nextTick()
|
||||||
|
syncBodyState()
|
||||||
|
}
|
||||||
|
|
||||||
|
function lock() {
|
||||||
|
sessionStorage.removeItem(storageKey)
|
||||||
|
unlocked.value = false
|
||||||
|
syncBodyState()
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPresenter() {
|
||||||
|
window.location.href = `${import.meta.env.BASE_URL}presenter/`
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeNotes(delta: number) {
|
||||||
|
noteSize.value = Math.min(38, Math.max(14, noteSize.value + delta))
|
||||||
|
document.documentElement.style.setProperty('--easy-presenter-note-size', `${noteSize.value}px`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleContrast() {
|
||||||
|
contrast.value = !contrast.value
|
||||||
|
syncBodyState()
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncBodyState() {
|
||||||
|
document.body.classList.toggle('easy-presenter-active', isPresenter.value && unlocked.value)
|
||||||
|
document.body.classList.toggle('easy-notes-contrast', isPresenter.value && unlocked.value && contrast.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncPresenterGate() {
|
||||||
|
await nextTick()
|
||||||
|
const shouldOpen = isPresenter.value && !unlocked.value
|
||||||
|
document.body.classList.toggle('easy-presenter-locked', shouldOpen)
|
||||||
|
if (!shouldOpen) {
|
||||||
|
if (gate.value?.open)
|
||||||
|
gate.value.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (gate.value && !gate.value.open)
|
||||||
|
gate.value.showModal()
|
||||||
|
tokenInput.value?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
function keepGateOpen(event: Event) {
|
||||||
|
event.preventDefault()
|
||||||
|
tokenInput.value?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sha256(value: string) {
|
||||||
|
const bytes = new TextEncoder().encode(value.trim())
|
||||||
|
const digest = await crypto.subtle.digest('SHA-256', bytes)
|
||||||
|
return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
v-if="!isPresenter && configuredHash"
|
||||||
|
class="easy-presenter-launcher"
|
||||||
|
type="button"
|
||||||
|
title="打开演示者模式"
|
||||||
|
@click="openPresenter"
|
||||||
|
>
|
||||||
|
演示者
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Teleport to="body">
|
||||||
|
<dialog
|
||||||
|
v-if="isPresenter && !unlocked"
|
||||||
|
ref="gate"
|
||||||
|
class="easy-presenter-gate"
|
||||||
|
aria-labelledby="easy-presenter-title"
|
||||||
|
@cancel="keepGateOpen"
|
||||||
|
@click.stop
|
||||||
|
@keydown.stop
|
||||||
|
@keyup.stop
|
||||||
|
@pointerdown.stop
|
||||||
|
>
|
||||||
|
<section class="easy-presenter-dialog">
|
||||||
|
<h1 id="easy-presenter-title">演示者模式</h1>
|
||||||
|
<p v-if="configuredHash">输入 token 后查看下一页、演讲备注和计时器。</p>
|
||||||
|
<p v-else>该站点没有配置演示者 token。</p>
|
||||||
|
<form v-if="configuredHash" @submit.prevent="unlock">
|
||||||
|
<input ref="tokenInput" v-model="token" type="password" autocomplete="current-password" autofocus aria-label="演示者 token">
|
||||||
|
<button type="submit">解锁</button>
|
||||||
|
</form>
|
||||||
|
<p v-if="error" class="easy-presenter-error" role="alert">{{ error }}</p>
|
||||||
|
</section>
|
||||||
|
</dialog>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
|
<nav v-if="isPresenter && unlocked" class="easy-presenter-tools" aria-label="提词器设置">
|
||||||
|
<button type="button" title="减小讲稿字号" @click="resizeNotes(-2)">A−</button>
|
||||||
|
<button type="button" title="增大讲稿字号" @click="resizeNotes(2)">A+</button>
|
||||||
|
<button type="button" title="切换高对比讲稿" @click="toggleContrast">◐</button>
|
||||||
|
<button type="button" title="锁定演示者模式" @click="lock">锁定</button>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useNav } from '@slidev/client'
|
||||||
|
import { onBeforeUnmount, onMounted, watch } from 'vue'
|
||||||
|
|
||||||
|
const nav = useNav()
|
||||||
|
|
||||||
|
function isInteractiveRoute() {
|
||||||
|
if (/(?:^|\/)(?:overview|export|editor|notes|notes-edit|print)(?:\/|$)/.test(window.location.pathname))
|
||||||
|
return false
|
||||||
|
return nav.isPresenter.value || nav.hasPrimarySlide.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncScrollFallback() {
|
||||||
|
document.body.classList.toggle('easy-slide-scroll-fallback', isInteractiveRoute())
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(syncScrollFallback)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[nav.isPresenter, nav.hasPrimarySlide],
|
||||||
|
syncScrollFallback,
|
||||||
|
)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.body.classList.remove('easy-slide-scroll-fallback')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span hidden aria-hidden="true" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onBeforeUnmount, onMounted } from 'vue'
|
||||||
|
|
||||||
|
let observer: MutationObserver | undefined
|
||||||
|
|
||||||
|
function enhanceImages(root: ParentNode = document) {
|
||||||
|
root.querySelectorAll<HTMLImageElement>('.slidev-layout img').forEach((image) => {
|
||||||
|
image.classList.add('easy-smart-image')
|
||||||
|
const fit = image.getAttribute('fit')
|
||||||
|
const position = image.getAttribute('position')
|
||||||
|
const maxHeight = image.getAttribute('max-height')
|
||||||
|
if (fit)
|
||||||
|
image.style.objectFit = fit
|
||||||
|
if (position)
|
||||||
|
image.style.objectPosition = position
|
||||||
|
if (maxHeight)
|
||||||
|
image.style.maxHeight = maxHeight
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
enhanceImages()
|
||||||
|
observer = new MutationObserver(records => records.forEach(record => enhanceImages(record.target as ParentNode)))
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => observer?.disconnect())
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span hidden aria-hidden="true" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import ContentScaleControls from './components/ContentScaleControls.vue'
|
||||||
|
import PresenterAccess from './components/PresenterAccess.vue'
|
||||||
|
import SlideScrollFallback from './components/SlideScrollFallback.vue'
|
||||||
|
import SmartImages from './components/SmartImages.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SmartImages />
|
||||||
|
<ContentScaleControls />
|
||||||
|
<SlideScrollFallback />
|
||||||
|
<PresenterAccess />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="slidev-layout easy-layout-center">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="slidev-layout easy-layout-cover">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="slidev-layout easy-layout-default">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
import { resolveImageSource } from '../utils/resolve-image'
|
||||||
|
|
||||||
|
type ImagePlacement = 'auto' | 'left' | 'right' | 'top' | 'bottom'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
image?: string
|
||||||
|
imageFit?: string
|
||||||
|
imagePosition?: string
|
||||||
|
imagePlacement?: ImagePlacement
|
||||||
|
}>(), {
|
||||||
|
imageFit: 'contain',
|
||||||
|
imagePosition: 'center',
|
||||||
|
imagePlacement: 'auto',
|
||||||
|
})
|
||||||
|
|
||||||
|
const imagePane = ref<HTMLElement>()
|
||||||
|
const detectedPlacement = ref<Exclude<ImagePlacement, 'auto'>>('right')
|
||||||
|
let observedImage: HTMLImageElement | undefined
|
||||||
|
let observer: MutationObserver | undefined
|
||||||
|
|
||||||
|
const placement = computed(() => props.imagePlacement === 'auto'
|
||||||
|
? detectedPlacement.value
|
||||||
|
: props.imagePlacement)
|
||||||
|
|
||||||
|
function detectPlacement() {
|
||||||
|
const image = imagePane.value?.querySelector<HTMLImageElement>('img')
|
||||||
|
if (!image)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (observedImage !== image) {
|
||||||
|
observedImage?.removeEventListener('load', detectPlacement)
|
||||||
|
observedImage = image
|
||||||
|
observedImage.addEventListener('load', detectPlacement)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image.naturalWidth > 0 && image.naturalHeight > 0)
|
||||||
|
detectedPlacement.value = image.naturalWidth / image.naturalHeight >= 1.6 ? 'bottom' : 'right'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
detectPlacement()
|
||||||
|
if (imagePane.value) {
|
||||||
|
observer = new MutationObserver(detectPlacement)
|
||||||
|
observer.observe(imagePane.value, { childList: true, subtree: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
observedImage?.removeEventListener('load', detectPlacement)
|
||||||
|
observer?.disconnect()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="slidev-layout easy-layout-image-split easy-layout-image-auto"
|
||||||
|
:class="`easy-image-${placement}`"
|
||||||
|
>
|
||||||
|
<div class="easy-image-content"><slot /></div>
|
||||||
|
<div ref="imagePane" class="easy-image-pane">
|
||||||
|
<slot name="image">
|
||||||
|
<img
|
||||||
|
v-if="image"
|
||||||
|
:src="resolveImageSource(image)"
|
||||||
|
alt=""
|
||||||
|
:style="{ objectFit: imageFit, objectPosition: imagePosition }"
|
||||||
|
>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { resolveImageSource } from '../utils/resolve-image'
|
||||||
|
|
||||||
|
withDefaults(defineProps<{
|
||||||
|
image?: string
|
||||||
|
imageFit?: string
|
||||||
|
imagePosition?: string
|
||||||
|
}>(), {
|
||||||
|
imageFit: 'cover',
|
||||||
|
imagePosition: 'center',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="slidev-layout easy-layout-image-full">
|
||||||
|
<slot name="image">
|
||||||
|
<img v-if="image" :src="resolveImageSource(image)" alt="" :style="{ objectFit: imageFit, objectPosition: imagePosition }">
|
||||||
|
</slot>
|
||||||
|
<div class="easy-image-overlay"><slot /></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { resolveImageSource } from '../utils/resolve-image'
|
||||||
|
|
||||||
|
withDefaults(defineProps<{
|
||||||
|
image?: string
|
||||||
|
imageFit?: string
|
||||||
|
imagePosition?: string
|
||||||
|
}>(), {
|
||||||
|
imageFit: 'contain',
|
||||||
|
imagePosition: 'center',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="slidev-layout easy-layout-image-split easy-image-left">
|
||||||
|
<div class="easy-image-content"><slot /></div>
|
||||||
|
<div class="easy-image-pane">
|
||||||
|
<slot name="image">
|
||||||
|
<img v-if="image" :src="resolveImageSource(image)" alt="" :style="{ objectFit: imageFit, objectPosition: imagePosition }">
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { resolveImageSource } from '../utils/resolve-image'
|
||||||
|
|
||||||
|
withDefaults(defineProps<{
|
||||||
|
image?: string
|
||||||
|
imageFit?: string
|
||||||
|
imagePosition?: string
|
||||||
|
}>(), {
|
||||||
|
imageFit: 'contain',
|
||||||
|
imagePosition: 'center',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="slidev-layout easy-layout-image-split easy-image-right">
|
||||||
|
<div class="easy-image-content"><slot /></div>
|
||||||
|
<div class="easy-image-pane">
|
||||||
|
<slot name="image">
|
||||||
|
<img v-if="image" :src="resolveImageSource(image)" alt="" :style="{ objectFit: imageFit, objectPosition: imagePosition }">
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="slidev-layout easy-layout-quote">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="slidev-layout easy-layout-section">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="slidev-layout easy-layout-two-cols">
|
||||||
|
<slot />
|
||||||
|
<div class="easy-two-cols-grid">
|
||||||
|
<div><slot name="left" /></div>
|
||||||
|
<div><slot name="right" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "slidev-theme-easy-jyy",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"description": "JYY-inspired presentation theme for easy-slides and Slidev",
|
||||||
|
"license": "MIT",
|
||||||
|
"private": false,
|
||||||
|
"type": "module",
|
||||||
|
"keywords": ["slidev-theme", "slidev"],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.12.0",
|
||||||
|
"slidev": ">=52.0.0"
|
||||||
|
},
|
||||||
|
"slidev": {
|
||||||
|
"defaults": {
|
||||||
|
"aspectRatio": "4/3",
|
||||||
|
"canvasWidth": 1024,
|
||||||
|
"colorSchema": "light",
|
||||||
|
"fonts": {
|
||||||
|
"sans": "Lato,Noto Sans SC,PingFang SC,Microsoft YaHei,sans-serif",
|
||||||
|
"mono": "Inconsolata,JetBrains Mono,Fira Code,monospace",
|
||||||
|
"serif": "Noto Serif SC,STKaiti,KaiTi,serif"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@slidev/client": "52.19.1",
|
||||||
|
"@slidev/types": "52.19.1",
|
||||||
|
"markdown-it-attrs": "4.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineMarkdownSetup } from '@slidev/types'
|
||||||
|
import markdownItAttrs from 'markdown-it-attrs'
|
||||||
|
|
||||||
|
export default defineMarkdownSetup(() => ({
|
||||||
|
markdownItSetup(md) {
|
||||||
|
md.use(markdownItAttrs)
|
||||||
|
},
|
||||||
|
}))
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
:root {
|
||||||
|
--easy-accent: #1d4ed8;
|
||||||
|
--easy-accent-dark: #1e40af;
|
||||||
|
--easy-accent-soft: #eff6ff;
|
||||||
|
--easy-ink: #222;
|
||||||
|
--easy-muted: #666;
|
||||||
|
--easy-border: #ddd;
|
||||||
|
--easy-code-bg: #eee;
|
||||||
|
--easy-code-font-size: 28px;
|
||||||
|
--easy-table-font-size: 30px;
|
||||||
|
--easy-table-padding-x: 14px;
|
||||||
|
--easy-table-padding-y: 10px;
|
||||||
|
--easy-slide-padding-x: 52px;
|
||||||
|
--easy-slide-padding-y: 42px;
|
||||||
|
--easy-presenter-note-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-table-sm {
|
||||||
|
--easy-table-font-size: 24px;
|
||||||
|
--easy-table-padding-x: 10px;
|
||||||
|
--easy-table-padding-y: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-table-md {
|
||||||
|
--easy-table-font-size: 30px;
|
||||||
|
--easy-table-padding-x: 14px;
|
||||||
|
--easy-table-padding-y: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-table-lg {
|
||||||
|
--easy-table-font-size: 36px;
|
||||||
|
--easy-table-padding-x: 18px;
|
||||||
|
--easy-table-padding-y: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-code-sm { --easy-code-font-size: 22px; }
|
||||||
|
.easy-code-md { --easy-code-font-size: 28px; }
|
||||||
|
.easy-code-lg { --easy-code-font-size: 34px; }
|
||||||
|
|
||||||
|
.easy-code-height-sm { --easy-code-height: 180px; }
|
||||||
|
.easy-code-height-md { --easy-code-height: 300px; }
|
||||||
|
.easy-code-height-lg { --easy-code-height: 420px; }
|
||||||
|
|
||||||
|
.slidev-layout {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: var(--easy-slide-padding-y) var(--easy-slide-padding-x);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1.5px solid var(--easy-border);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--easy-ink);
|
||||||
|
text-align: left;
|
||||||
|
font-family: Lato, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1.42;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout h1,
|
||||||
|
.slidev-layout h2,
|
||||||
|
.slidev-layout h3,
|
||||||
|
.slidev-layout h4 {
|
||||||
|
color: var(--easy-ink);
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout h1 {
|
||||||
|
margin: 0 0 26px;
|
||||||
|
font-size: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout h2 {
|
||||||
|
margin: 0 0 28px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 3px solid var(--easy-accent);
|
||||||
|
font-size: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout h3 {
|
||||||
|
margin: 20px 0 14px;
|
||||||
|
font-size: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout p,
|
||||||
|
.slidev-layout ul,
|
||||||
|
.slidev-layout ol {
|
||||||
|
margin-top: 14px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout ul,
|
||||||
|
.slidev-layout ol {
|
||||||
|
display: block;
|
||||||
|
padding-left: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout li + li {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout a {
|
||||||
|
color: var(--easy-accent);
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
text-underline-offset: .13em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout strong {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout blockquote {
|
||||||
|
margin: 24px 0;
|
||||||
|
padding: 4px 0 4px 24px;
|
||||||
|
border-left: 5px solid var(--easy-accent);
|
||||||
|
color: #1e3a5f;
|
||||||
|
font-family: "Noto Serif SC", STKaiti, KaiTi, serif;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout code {
|
||||||
|
font-family: Inconsolata, "JetBrains Mono", "Fira Code", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout :not(pre) > code {
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--easy-accent-soft);
|
||||||
|
color: var(--easy-accent-dark);
|
||||||
|
padding: .08em .28em;
|
||||||
|
font-size: .86em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout pre {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: var(--easy-code-height, var(--easy-code-auto-height, auto));
|
||||||
|
max-height: var(--easy-code-height, var(--easy-code-auto-max-height, var(--easy-code-auto-height, none)));
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--easy-code-bg);
|
||||||
|
padding: 18px 20px;
|
||||||
|
font-size: var(--easy-code-scaled-font-size, var(--easy-code-font-size)) !important;
|
||||||
|
line-height: 1.38 !important;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout pre code,
|
||||||
|
.slidev-layout pre code .line {
|
||||||
|
min-width: 0 !important;
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: var(--easy-table-font-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout th,
|
||||||
|
.slidev-layout td {
|
||||||
|
padding: var(--easy-table-padding-y) var(--easy-table-padding-x);
|
||||||
|
border-bottom: 1px solid #d5d5d5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout th {
|
||||||
|
background: #eee;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout tr:nth-child(even) {
|
||||||
|
background: #f7faff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 60vh;
|
||||||
|
margin: 16px auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout p:has(> img:only-child) {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
min-height: 46vh;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout p:has(> img:only-child) > img {
|
||||||
|
width: 100%;
|
||||||
|
height: 46vh;
|
||||||
|
max-height: 60vh;
|
||||||
|
margin: 0;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout img[fit="cover"] {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout img[fit="fill"] { object-fit: fill; }
|
||||||
|
.slidev-layout img[fit="scale-down"] { object-fit: scale-down; }
|
||||||
|
.slidev-layout img[fit="none"] { object-fit: none; }
|
||||||
|
|
||||||
|
.slidev-layout .katex-display,
|
||||||
|
.slidev-layout .mermaid {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-page-number {
|
||||||
|
border-radius: 5px;
|
||||||
|
background: rgba(0, 0, 0, .26);
|
||||||
|
padding: 3px 9px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
:root {
|
||||||
|
--easy-slide-padding-x: 34px;
|
||||||
|
--easy-slide-padding-y: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/* Shared content utilities for course and technical decks. */
|
||||||
|
.slidev-layout .lead {
|
||||||
|
color: var(--easy-accent);
|
||||||
|
font-size: 1.16em;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout .muted {
|
||||||
|
color: var(--easy-muted);
|
||||||
|
font-size: .72em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout .compact,
|
||||||
|
.slidev-layout.compact {
|
||||||
|
font-size: 27px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout .compact li + li,
|
||||||
|
.slidev-layout.compact li + li {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout .outline {
|
||||||
|
list-style: none;
|
||||||
|
margin: 22px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout .outline > li {
|
||||||
|
margin: 12px 0;
|
||||||
|
padding-left: 22px;
|
||||||
|
border-left: 5px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout .outline .current {
|
||||||
|
border-left-color: var(--easy-accent);
|
||||||
|
color: var(--easy-accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout .source-figure {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 56vh;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout .course-table {
|
||||||
|
font-size: 21px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout .course-table th,
|
||||||
|
.slidev-layout .course-table td {
|
||||||
|
padding: 6px 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slidev-layout .v-word {
|
||||||
|
color: var(--easy-accent);
|
||||||
|
font-size: 1.22em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-layout-cover .cover-identity {
|
||||||
|
width: 68%;
|
||||||
|
height: 20vh;
|
||||||
|
margin: 22px 0 0;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: left center;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import './base.css'
|
||||||
|
import './content.css'
|
||||||
|
import './layouts.css'
|
||||||
|
import './presenter.css'
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
.easy-layout-center,
|
||||||
|
.easy-layout-section,
|
||||||
|
.easy-layout-cover {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-layout-center {
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-layout-center h1,
|
||||||
|
.easy-layout-center h2 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-layout-cover h1 {
|
||||||
|
margin-bottom: 28px;
|
||||||
|
font-size: 68px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-layout-cover blockquote {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--easy-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-layout-section h1,
|
||||||
|
.easy-layout-section h2 {
|
||||||
|
max-width: 980px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-layout-quote {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-layout-quote blockquote {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--easy-accent);
|
||||||
|
font-size: 46px;
|
||||||
|
line-height: 1.35;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-layout-two-cols {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-layout-two-cols .easy-two-cols-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
gap: 46px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-layout-image-split {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
gap: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-layout-image-split.easy-image-left .easy-image-pane { order: -1; }
|
||||||
|
|
||||||
|
.easy-layout-image-auto.easy-image-top,
|
||||||
|
.easy-layout-image-auto.easy-image-bottom {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
|
gap: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-layout-image-auto.easy-image-top .easy-image-pane { order: -1; }
|
||||||
|
|
||||||
|
.easy-image-pane {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
grid-template-rows: minmax(0, 1fr);
|
||||||
|
place-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-image-pane img {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-content-scale-host {
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-content-table-host {
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-content-code-host {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-image-preview-trigger {
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-image-preview-trigger:focus-visible {
|
||||||
|
outline: 3px solid var(--easy-accent);
|
||||||
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-image-lightbox {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2147483647;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
grid-template-rows: minmax(0, 1fr);
|
||||||
|
place-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: clamp(16px, 4vmin, 42px);
|
||||||
|
overflow: hidden;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
background: rgba(0, 0, 0, .92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-image-lightbox-preview {
|
||||||
|
display: block;
|
||||||
|
place-self: center;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
max-inline-size: 100%;
|
||||||
|
max-block-size: 100%;
|
||||||
|
margin: 0;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-image-lightbox-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 22px;
|
||||||
|
display: grid;
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid rgba(255, 255, 255, .5);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0, 0, 0, .55);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0 0 4px;
|
||||||
|
font: 300 38px/1 Arial, sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-image-lightbox-close:hover,
|
||||||
|
.easy-image-lightbox-close:focus-visible {
|
||||||
|
border-color: #fff;
|
||||||
|
background: var(--easy-accent);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-content-scale-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 30;
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid rgba(180, 180, 180, .9);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: rgba(255, 255, 255, .94);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, .12);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .14s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-content-scale-host:hover > .easy-content-scale-toolbar,
|
||||||
|
.easy-content-scale-host:focus-within > .easy-content-scale-toolbar {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-content-scale-button {
|
||||||
|
min-width: 32px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #eee;
|
||||||
|
color: #222;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font: 600 14px/1 Lato, "Noto Sans SC", sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-content-scale-button:hover,
|
||||||
|
.easy-content-scale-button:focus-visible {
|
||||||
|
background: var(--easy-accent);
|
||||||
|
color: #fff;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-content-scale-reset {
|
||||||
|
min-width: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-content-code-host > .easy-content-scale-toolbar {
|
||||||
|
right: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.easy-slide-scroll-fallback .slidev-layout {
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior-y: contain;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: color-mix(in srgb, var(--easy-accent) 65%, transparent) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-image-content {
|
||||||
|
min-width: 0;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-layout-image-full {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-layout-image-full > img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-height: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-layout-image-full .easy-image-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: auto 0 0;
|
||||||
|
padding: 46px 54px;
|
||||||
|
background: linear-gradient(transparent, rgba(0, 0, 0, .78));
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-layout-image-full .easy-image-overlay :is(h1, h2, h3, p) {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-aspect-ratio: 4 / 3) {
|
||||||
|
.easy-layout-two-cols .easy-two-cols-grid,
|
||||||
|
.easy-layout-image-split {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
gap: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: none) {
|
||||||
|
.easy-content-scale-toolbar { opacity: .78; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.easy-content-scale-toolbar,
|
||||||
|
.easy-image-lightbox { display: none !important; }
|
||||||
|
|
||||||
|
body.easy-slide-scroll-fallback .slidev-layout { overflow: hidden !important; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
.easy-presenter-gate {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2147483646;
|
||||||
|
display: none;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100dvh;
|
||||||
|
max-width: none;
|
||||||
|
max-height: none;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
place-items: center;
|
||||||
|
overflow: auto;
|
||||||
|
background: #f6f5f2;
|
||||||
|
color: #222;
|
||||||
|
font-family: Lato, "Noto Sans SC", "PingFang SC", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-presenter-gate[open] {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-presenter-gate::backdrop {
|
||||||
|
background: #f6f5f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.easy-presenter-locked {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-presenter-dialog {
|
||||||
|
width: min(440px, calc(100vw - 40px));
|
||||||
|
border: 1px solid #d8d4d7;
|
||||||
|
border-top: 4px solid var(--easy-accent);
|
||||||
|
border-radius: 9px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 34px;
|
||||||
|
box-shadow: 0 20px 70px rgba(45, 26, 42, .14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-presenter-dialog h1 {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-presenter-dialog p {
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-presenter-dialog form {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-presenter-dialog input {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 11px 12px;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-presenter-dialog button,
|
||||||
|
.easy-presenter-tools button,
|
||||||
|
.easy-presenter-launcher {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--easy-accent);
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font: 600 14px/1.1 Lato, "Noto Sans SC", sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-presenter-error {
|
||||||
|
color: #a21d32 !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-presenter-launcher {
|
||||||
|
position: fixed;
|
||||||
|
right: 18px;
|
||||||
|
bottom: 18px;
|
||||||
|
z-index: 60;
|
||||||
|
opacity: .22;
|
||||||
|
transition: opacity .16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-presenter-launcher:hover,
|
||||||
|
.easy-presenter-launcher:focus-visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-presenter-tools {
|
||||||
|
position: fixed;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid #d4d0d3;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, .94);
|
||||||
|
box-shadow: 0 5px 22px rgba(0, 0, 0, .12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-presenter-tools button {
|
||||||
|
min-width: 34px;
|
||||||
|
background: #eee9ed;
|
||||||
|
color: #351231;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.easy-presenter-tools button:last-child {
|
||||||
|
background: #5d1357;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.easy-presenter-tools {
|
||||||
|
right: auto;
|
||||||
|
left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.easy-presenter-active :is(.presenter-notes, [class*="presenter"] [class*="notes"], [class*="presenter"] [class*="note"] p) {
|
||||||
|
font-size: var(--easy-presenter-note-size) !important;
|
||||||
|
line-height: 1.65 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.easy-notes-contrast :is(.presenter-notes, [class*="presenter"] [class*="notes"], [class*="presenter"] [class*="note"]) {
|
||||||
|
background: #050505 !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.easy-notes-contrast :is(.presenter-notes, [class*="presenter"] [class*="notes"], [class*="presenter"] [class*="note"]) * {
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
const deckBase = (import.meta as ImportMeta & { env: { BASE_URL: string } }).env.BASE_URL
|
||||||
|
|
||||||
|
export function resolveImageSource(source: string | undefined, base = deckBase) {
|
||||||
|
if (!source || isAbsoluteSource(source))
|
||||||
|
return source
|
||||||
|
|
||||||
|
const normalizedBase = base.endsWith('/') ? base : `${base}/`
|
||||||
|
return `${normalizedBase}${source.replace(/^\.\//, '')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAbsoluteSource(source: string) {
|
||||||
|
return source.startsWith('/')
|
||||||
|
|| source.startsWith('#')
|
||||||
|
|| /^[a-z][a-z\d+.-]*:/i.test(source)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user