@berlysia/vertical-writing-slide-system 0.0.30 → 0.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/scripts/build-pages.js +46 -5
- package/src/App.tsx +0 -10
- package/src/types/gray-matter.d.ts +14 -0
- package/src/types/slide-metadata.js +1 -0
- package/src/types/slide-metadata.ts +27 -0
- package/src/virtual-slides.d.ts +6 -0
- package/src/vite-plugin-slides.ts +105 -10
- package/tsconfig.app.json +1 -0
- package/tsconfig.node.json +9 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@berlysia/vertical-writing-slide-system",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.32",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"vertical-slides": "./cli.js"
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"@mdx-js/react": "^3.1.0",
|
|
22
22
|
"@mdx-js/rollup": "^3.1.0",
|
|
23
23
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
|
24
|
+
"gray-matter": "^4.0.3",
|
|
24
25
|
"prompts": "^2.4.2",
|
|
25
26
|
"react": "^19.0.0",
|
|
26
27
|
"react-dom": "^19.0.0",
|
package/scripts/build-pages.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "fs";
|
|
2
|
-
import { mkdir, readdir, stat, cp, writeFile, mkdtemp, rm } from "fs/promises";
|
|
2
|
+
import { mkdir, readdir, stat, cp, writeFile, mkdtemp, rm, readFile, } from "fs/promises";
|
|
3
3
|
import { join, resolve } from "path";
|
|
4
4
|
import { build } from "vite";
|
|
5
|
+
import matter from "gray-matter";
|
|
5
6
|
const defaultSlidesDir = resolve(import.meta.dirname, "..", "slides");
|
|
6
7
|
const pagesDir = "pages";
|
|
7
8
|
// Ensure pages directory exists
|
|
@@ -22,9 +23,49 @@ async function buildSlide(slideName) {
|
|
|
22
23
|
await cp(tmpDir, slideOutputDir, { recursive: true });
|
|
23
24
|
await rm(tmpDir, { recursive: true });
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* スライドコレクションからメタデータを取得
|
|
28
|
+
*/
|
|
29
|
+
async function getSlideMetadata(slidesDir, slideName) {
|
|
30
|
+
const mdxPath = join(slidesDir, slideName, "index.mdx");
|
|
31
|
+
const mdPath = join(slidesDir, slideName, "index.md");
|
|
32
|
+
let filePath;
|
|
33
|
+
if (existsSync(mdxPath)) {
|
|
34
|
+
filePath = mdxPath;
|
|
35
|
+
}
|
|
36
|
+
else if (existsSync(mdPath)) {
|
|
37
|
+
filePath = mdPath;
|
|
38
|
+
}
|
|
39
|
+
if (!filePath) {
|
|
40
|
+
return { title: slideName };
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const content = await readFile(filePath, "utf-8");
|
|
44
|
+
const parsed = matter(content);
|
|
45
|
+
return {
|
|
46
|
+
title: slideName,
|
|
47
|
+
...parsed.data,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.warn(`Failed to parse frontmatter for ${slideName}: ${error}`);
|
|
52
|
+
return { title: slideName };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function createIndexPage(slideNames, slidesDir) {
|
|
56
|
+
// 各スライドのメタデータを取得
|
|
57
|
+
const slideMetadataList = await Promise.all(slideNames.map(async (name) => ({
|
|
58
|
+
name,
|
|
59
|
+
metadata: await getSlideMetadata(slidesDir, name),
|
|
60
|
+
})));
|
|
61
|
+
const slides = slideMetadataList
|
|
62
|
+
.map(({ name, metadata }) => {
|
|
63
|
+
const displayTitle = metadata.title || name;
|
|
64
|
+
const description = metadata.description
|
|
65
|
+
? ` - ${metadata.description}`
|
|
66
|
+
: "";
|
|
67
|
+
return ` <li><a href="${name}/">${displayTitle}</a>${description}</li>`;
|
|
68
|
+
})
|
|
28
69
|
.join("\n");
|
|
29
70
|
const html = `<!DOCTYPE html>
|
|
30
71
|
<html>
|
|
@@ -56,7 +97,7 @@ export async function buildPages(options = {}) {
|
|
|
56
97
|
for (const slide of slides) {
|
|
57
98
|
await buildSlide(slide);
|
|
58
99
|
}
|
|
59
|
-
await createIndexPage(slides);
|
|
100
|
+
await createIndexPage(slides, resolvedSlidesDir);
|
|
60
101
|
}
|
|
61
102
|
// CLI entry point
|
|
62
103
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
package/src/App.tsx
CHANGED
|
@@ -114,20 +114,10 @@ function App() {
|
|
|
114
114
|
gotoNextSlide(false);
|
|
115
115
|
}
|
|
116
116
|
};
|
|
117
|
-
const handleWheel = (event: WheelEvent) => {
|
|
118
|
-
if (event.deltaY > 0) {
|
|
119
|
-
gotoNextSlide();
|
|
120
|
-
} else if (event.deltaY < 0) {
|
|
121
|
-
gotoNextSlide(false);
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
117
|
const controller = new AbortController();
|
|
125
118
|
window.addEventListener("keydown", handleKeydown, {
|
|
126
119
|
signal: controller.signal,
|
|
127
120
|
});
|
|
128
|
-
window.addEventListener("wheel", handleWheel, {
|
|
129
|
-
signal: controller.signal,
|
|
130
|
-
});
|
|
131
121
|
return () => {
|
|
132
122
|
controller.abort();
|
|
133
123
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare module "gray-matter" {
|
|
2
|
+
interface GrayMatterFile {
|
|
3
|
+
data: any;
|
|
4
|
+
content: string;
|
|
5
|
+
excerpt?: string;
|
|
6
|
+
orig: string | Buffer;
|
|
7
|
+
language: string;
|
|
8
|
+
matter: string;
|
|
9
|
+
stringify(lang?: string): string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function matter(input: string | Buffer): GrayMatterFile;
|
|
13
|
+
export = matter;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* スライドのメタデータ型定義
|
|
3
|
+
*/
|
|
4
|
+
export interface SlideMetadata {
|
|
5
|
+
/** スライドのタイトル */
|
|
6
|
+
title?: string;
|
|
7
|
+
/** スライドの説明 */
|
|
8
|
+
description?: string;
|
|
9
|
+
/** 作成者 */
|
|
10
|
+
author?: string;
|
|
11
|
+
/** 作成日時 */
|
|
12
|
+
date?: string;
|
|
13
|
+
/** その他のカスタムフィールド */
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* frontmatterをパースした結果
|
|
19
|
+
*/
|
|
20
|
+
export interface ParsedSlideFile {
|
|
21
|
+
/** パースされたメタデータ */
|
|
22
|
+
metadata: SlideMetadata;
|
|
23
|
+
/** メタデータを除いたコンテンツ */
|
|
24
|
+
content: string;
|
|
25
|
+
/** 元のファイル内容 */
|
|
26
|
+
originalContent: string;
|
|
27
|
+
}
|
package/src/virtual-slides.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
2
|
|
|
3
3
|
declare module "virtual:slides.js" {
|
|
4
|
+
import type { ParsedScript } from "./script-manager";
|
|
5
|
+
import type { SlideMetadata } from "./types/slide-metadata";
|
|
6
|
+
|
|
4
7
|
type SlideContent = string | (() => JSX.Element) | React.ComponentType;
|
|
5
8
|
const content: SlideContent[];
|
|
6
9
|
export default content;
|
|
10
|
+
|
|
11
|
+
export const slideScripts: ParsedScript;
|
|
12
|
+
export const slideMetadata: SlideMetadata;
|
|
7
13
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Plugin,
|
|
3
|
+
ViteDevServer,
|
|
4
|
+
ResolvedConfig,
|
|
5
|
+
HtmlTagDescriptor,
|
|
6
|
+
} from "vite";
|
|
2
7
|
import * as fs from "node:fs";
|
|
3
8
|
import * as path from "node:path";
|
|
4
9
|
import { mkdirSync, readdirSync } from "node:fs";
|
|
@@ -14,6 +19,8 @@ import { ScriptManager, type ParsedScript } from "./script-manager";
|
|
|
14
19
|
import { visit } from "unist-util-visit";
|
|
15
20
|
import type { Node } from "unist";
|
|
16
21
|
import type { Element, Text, ElementContent } from "hast";
|
|
22
|
+
import matter from "gray-matter";
|
|
23
|
+
import type { SlideMetadata, ParsedSlideFile } from "./types/slide-metadata";
|
|
17
24
|
|
|
18
25
|
/**
|
|
19
26
|
* CSS抽出用のrehypeプラグイン
|
|
@@ -124,6 +131,37 @@ function loadAdjacentScripts(
|
|
|
124
131
|
return result;
|
|
125
132
|
}
|
|
126
133
|
|
|
134
|
+
/**
|
|
135
|
+
* ファイル内容からfrontmatterをパースして、メタデータとコンテンツに分離
|
|
136
|
+
*/
|
|
137
|
+
function parseSlideFile(
|
|
138
|
+
content: string,
|
|
139
|
+
collectionName: string,
|
|
140
|
+
): ParsedSlideFile {
|
|
141
|
+
try {
|
|
142
|
+
const parsed = matter(content);
|
|
143
|
+
const metadata: SlideMetadata = {
|
|
144
|
+
// デフォルトタイトルとしてコレクション名を使用
|
|
145
|
+
title: collectionName,
|
|
146
|
+
...parsed.data,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
metadata,
|
|
151
|
+
content: parsed.content,
|
|
152
|
+
originalContent: content,
|
|
153
|
+
};
|
|
154
|
+
} catch (error) {
|
|
155
|
+
logger.warn(`Failed to parse frontmatter: ${error}`);
|
|
156
|
+
// パースに失敗した場合はfrontmatterなしとして扱う
|
|
157
|
+
return {
|
|
158
|
+
metadata: { title: collectionName },
|
|
159
|
+
content,
|
|
160
|
+
originalContent: content,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
127
165
|
export interface SlidesPluginOptions {
|
|
128
166
|
/** Directory containing the slides (absolute path) */
|
|
129
167
|
slidesDir?: string;
|
|
@@ -231,6 +269,7 @@ export default async function slidesPlugin(
|
|
|
231
269
|
let resolvedConfig: ResolvedConfig;
|
|
232
270
|
let slideStyles: string[] = [];
|
|
233
271
|
let slideScripts: ParsedScript = { external: [], inline: [] };
|
|
272
|
+
let slideMetadata: SlideMetadata = { title: config.collection };
|
|
234
273
|
return {
|
|
235
274
|
name: "vite-plugin-slides",
|
|
236
275
|
configResolved(config: ResolvedConfig) {
|
|
@@ -288,6 +327,13 @@ export default async function slidesPlugin(
|
|
|
288
327
|
|
|
289
328
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
290
329
|
|
|
330
|
+
// frontmatterをパースしてメタデータとコンテンツを分離
|
|
331
|
+
const parsedFile = parseSlideFile(content, config.collection);
|
|
332
|
+
slideMetadata = parsedFile.metadata;
|
|
333
|
+
const slideContent = parsedFile.content;
|
|
334
|
+
|
|
335
|
+
logger.info(`Slide metadata: ${JSON.stringify(slideMetadata)}`);
|
|
336
|
+
|
|
291
337
|
// 隣接CSSファイルを読み込み
|
|
292
338
|
const adjacentStyles = loadAdjacentCSS(
|
|
293
339
|
config.slidesDir,
|
|
@@ -306,7 +352,7 @@ export default async function slidesPlugin(
|
|
|
306
352
|
};
|
|
307
353
|
|
|
308
354
|
if (!isMdx) {
|
|
309
|
-
const slides =
|
|
355
|
+
const slides = slideContent.split(/^\s*(?:---|\*\*\*|___)\s*$/m);
|
|
310
356
|
const extractedStyles: string[] = [];
|
|
311
357
|
const extractedScripts: ParsedScript = { external: [], inline: [] };
|
|
312
358
|
const processedSlides = await Promise.all(
|
|
@@ -349,29 +395,29 @@ export default async function slidesPlugin(
|
|
|
349
395
|
`;
|
|
350
396
|
}
|
|
351
397
|
|
|
352
|
-
const slides =
|
|
398
|
+
const slides = slideContent.split(/^\s*(?:---|\*\*\*|___)\s*$/m);
|
|
353
399
|
|
|
354
400
|
// MDXにもCSS・スクリプト抽出を適用
|
|
355
401
|
const extractedStyles: string[] = [];
|
|
356
402
|
const extractedScripts: ParsedScript = { external: [], inline: [] };
|
|
357
403
|
const processedSlides = await Promise.all(
|
|
358
|
-
slides.map(async (
|
|
404
|
+
slides.map(async (slide) => {
|
|
359
405
|
// MDX内のstyleタグを手動で抽出(簡易実装)
|
|
360
406
|
const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
|
361
407
|
let match;
|
|
362
|
-
while ((match = styleRegex.exec(
|
|
408
|
+
while ((match = styleRegex.exec(slide)) !== null) {
|
|
363
409
|
if (match[1].trim()) {
|
|
364
410
|
extractedStyles.push(match[1].trim());
|
|
365
411
|
}
|
|
366
412
|
}
|
|
367
413
|
|
|
368
414
|
// MDX内のscriptタグを抽出
|
|
369
|
-
const parsedScripts = ScriptManager.parseScripts(
|
|
415
|
+
const parsedScripts = ScriptManager.parseScripts(slide);
|
|
370
416
|
extractedScripts.external.push(...parsedScripts.external);
|
|
371
417
|
extractedScripts.inline.push(...parsedScripts.inline);
|
|
372
418
|
|
|
373
419
|
// style・scriptタグを削除したコンテンツでMDXコンパイル
|
|
374
|
-
let cleanedContent =
|
|
420
|
+
let cleanedContent = slide.replace(styleRegex, "");
|
|
375
421
|
cleanedContent =
|
|
376
422
|
ScriptManager.removeScriptsFromContent(cleanedContent);
|
|
377
423
|
|
|
@@ -446,6 +492,9 @@ export default async function slidesPlugin(
|
|
|
446
492
|
// スライド固有のスクリプトを外部から利用可能にする
|
|
447
493
|
export const slideScripts = ${slideScriptsString};
|
|
448
494
|
|
|
495
|
+
// スライドのメタデータを外部から利用可能にする
|
|
496
|
+
export const slideMetadata = ${JSON.stringify(slideMetadata)};
|
|
497
|
+
|
|
449
498
|
// provide slide components to each slide
|
|
450
499
|
// Wrap SlideN components to provide SlideComponents
|
|
451
500
|
${compiledSlides
|
|
@@ -533,12 +582,15 @@ export default async function slidesPlugin(
|
|
|
533
582
|
// Generate HTML file if none exists in consumer project
|
|
534
583
|
const consumerIndexHtml = path.resolve(resolvedConfig.root, "index.html");
|
|
535
584
|
|
|
536
|
-
|
|
585
|
+
// CLIモードでは常にメタデータ付きHTMLを生成
|
|
586
|
+
const isExternalCLI = process.cwd() !== resolvedConfig.root;
|
|
587
|
+
|
|
588
|
+
if (!fs.existsSync(consumerIndexHtml) || isExternalCLI) {
|
|
537
589
|
// Find the main JS and CSS files in the bundle
|
|
538
590
|
const mainJsFile = Object.keys(bundle).find(
|
|
539
591
|
(fileName) =>
|
|
540
592
|
fileName.startsWith("assets/") &&
|
|
541
|
-
fileName.includes("main-") &&
|
|
593
|
+
(fileName.includes("index-") || fileName.includes("main-")) &&
|
|
542
594
|
fileName.endsWith(".js"),
|
|
543
595
|
);
|
|
544
596
|
const mainCssFile = Object.keys(bundle).find(
|
|
@@ -548,6 +600,7 @@ export default async function slidesPlugin(
|
|
|
548
600
|
|
|
549
601
|
if (!mainJsFile) {
|
|
550
602
|
logger.error("Could not find main JS file in bundle");
|
|
603
|
+
logger.error("Available bundle files: " + Object.keys(bundle));
|
|
551
604
|
return;
|
|
552
605
|
}
|
|
553
606
|
|
|
@@ -555,12 +608,15 @@ export default async function slidesPlugin(
|
|
|
555
608
|
? `<link rel="stylesheet" href="./${mainCssFile}">`
|
|
556
609
|
: "<!-- CSS is included in the JS bundle -->";
|
|
557
610
|
|
|
611
|
+
const pageTitle = slideMetadata?.title || "Vertical Writing Slides";
|
|
558
612
|
const virtualIndexHtml = `<!doctype html>
|
|
559
613
|
<html lang="ja">
|
|
560
614
|
<head>
|
|
561
615
|
<meta charset="UTF-8" />
|
|
562
616
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
563
|
-
<title
|
|
617
|
+
<title>${pageTitle}</title>
|
|
618
|
+
${slideMetadata?.description ? `<meta name="description" content="${slideMetadata.description}" />` : ""}
|
|
619
|
+
${slideMetadata?.author ? `<meta name="author" content="${slideMetadata.author}" />` : ""}
|
|
564
620
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
565
621
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
566
622
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP&family=Noto+Sans+Mono:wght@100..900&display=swap" rel="stylesheet">
|
|
@@ -713,5 +769,44 @@ export default async function slidesPlugin(
|
|
|
713
769
|
// No additional cleanup needed since we're using Vite's built-in watcher
|
|
714
770
|
};
|
|
715
771
|
},
|
|
772
|
+
|
|
773
|
+
transformIndexHtml(html: string) {
|
|
774
|
+
// 既存の <title> タグを削除
|
|
775
|
+
const htmlWithoutTitle = html.replace(/<title>.*?<\/title>/i, "");
|
|
776
|
+
|
|
777
|
+
// メタデータタグを構築
|
|
778
|
+
const title = slideMetadata?.title || config.collection;
|
|
779
|
+
const tags: HtmlTagDescriptor[] = [
|
|
780
|
+
{
|
|
781
|
+
tag: "title",
|
|
782
|
+
children: title,
|
|
783
|
+
},
|
|
784
|
+
];
|
|
785
|
+
|
|
786
|
+
if (slideMetadata?.description) {
|
|
787
|
+
tags.push({
|
|
788
|
+
tag: "meta",
|
|
789
|
+
attrs: {
|
|
790
|
+
name: "description",
|
|
791
|
+
content: slideMetadata.description,
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (slideMetadata?.author) {
|
|
797
|
+
tags.push({
|
|
798
|
+
tag: "meta",
|
|
799
|
+
attrs: {
|
|
800
|
+
name: "author",
|
|
801
|
+
content: slideMetadata.author,
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return {
|
|
807
|
+
html: htmlWithoutTitle,
|
|
808
|
+
tags,
|
|
809
|
+
};
|
|
810
|
+
},
|
|
716
811
|
};
|
|
717
812
|
}
|
package/tsconfig.app.json
CHANGED
package/tsconfig.node.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
+
"composite": true,
|
|
3
4
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
5
|
"target": "ES2022",
|
|
5
|
-
"lib": ["ES2023"],
|
|
6
|
+
"lib": ["ES2023", "DOM"],
|
|
6
7
|
"module": "ESNext",
|
|
7
8
|
"skipLibCheck": true,
|
|
8
9
|
|
|
@@ -20,5 +21,11 @@
|
|
|
20
21
|
"noFallthroughCasesInSwitch": true,
|
|
21
22
|
"noUncheckedSideEffectImports": true
|
|
22
23
|
},
|
|
23
|
-
"include": [
|
|
24
|
+
"include": [
|
|
25
|
+
"vite.config.ts",
|
|
26
|
+
"src/vite-plugin-slides.ts",
|
|
27
|
+
"src/remark-slide-images.ts",
|
|
28
|
+
"src/script-manager.ts",
|
|
29
|
+
"src/types/**/*.ts"
|
|
30
|
+
]
|
|
24
31
|
}
|