@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@berlysia/vertical-writing-slide-system",
3
- "version": "0.0.30",
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",
@@ -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
- async function createIndexPage(slideNames) {
26
- const slides = slideNames
27
- .map((name) => ` <li><a href="${name}/">${name}</a></li>`)
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
+ }
@@ -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 { Plugin, ViteDevServer, ResolvedConfig } from "vite";
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 = content.split(/^\s*(?:---|\*\*\*|___)\s*$/m);
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 = content.split(/^\s*(?:---|\*\*\*|___)\s*$/m);
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 (slideContent) => {
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(slideContent)) !== null) {
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(slideContent);
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 = slideContent.replace(styleRegex, "");
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
- if (!fs.existsSync(consumerIndexHtml)) {
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>Vertical Writing Slides</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
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "compilerOptions": {
3
+ "composite": true,
3
4
  "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
5
  "target": "ES2020",
5
6
  "useDefineForClassFields": true,
@@ -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": ["vite.config.ts"]
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
  }