@berlysia/vertical-writing-slide-system 0.0.17 → 0.0.19

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/cli.js CHANGED
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // @ts-check
3
3
 
4
- import { execSync } from "child_process";
5
- import { mkdir, mkdtemp, cp, rm, writeFile, access } from "node:fs/promises";
4
+ import { writeFile, access } from "node:fs/promises";
6
5
  import { resolve } from "node:path";
7
6
  import { parseArgs } from "node:util";
8
7
  import { build, createServer } from "vite";
@@ -15,7 +14,13 @@ async function ensureIndexHtml() {
15
14
  return;
16
15
  } catch {
17
16
  // index.html doesn't exist, create it
18
- // Use relative paths that Vite can resolve during development
17
+ // Use absolute paths from the library location for external projects
18
+ const libPath = import.meta.dirname;
19
+ const libSrcPath = resolve(libPath, "src");
20
+
21
+ // Convert to relative paths from project root that Vite can resolve
22
+ const relativeSrcPath = `/@fs${libSrcPath}`;
23
+
19
24
  const indexHtmlContent = `<!doctype html>
20
25
  <html lang="ja">
21
26
  <head>
@@ -26,13 +31,13 @@ async function ensureIndexHtml() {
26
31
  <link rel="preconnect" href="https://fonts.googleapis.com">
27
32
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
28
33
  <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP&family=Noto+Sans+Mono:wght@100..900&display=swap" rel="stylesheet">
29
- <link rel="stylesheet" href="/src/index.css" />
30
- <link rel="stylesheet" media="screen" href="/src/screen.css" />
31
- <link rel="stylesheet" media="print" href="/src/print.css" />
34
+ <link rel="stylesheet" href="${relativeSrcPath}/index.css" />
35
+ <link rel="stylesheet" media="screen" href="${relativeSrcPath}/screen.css" />
36
+ <link rel="stylesheet" media="print" href="${relativeSrcPath}/print.css" />
32
37
  </head>
33
38
  <body>
34
39
  <div id="root"></div>
35
- <script type="module" src="/src/main.tsx"></script>
40
+ <script type="module" src="${relativeSrcPath}/main.tsx"></script>
36
41
  </body>
37
42
  </html>`;
38
43
 
@@ -46,6 +51,9 @@ async function runDev() {
46
51
  const libPath = import.meta.dirname;
47
52
  const projectPath = process.cwd();
48
53
 
54
+ // Ensure index.html exists for external projects
55
+ await ensureIndexHtml();
56
+
49
57
  const server = await createServer({
50
58
  root: projectPath, // Use project as root for proper file watching
51
59
  configFile: resolve(libPath, "vite.config.ts"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@berlysia/vertical-writing-slide-system",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "vertical-slides": "./cli.js"
@@ -17,9 +17,9 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "@emotion/react": "^11.14.0",
20
+ "@mdx-js/mdx": "^3.1.0",
20
21
  "@mdx-js/react": "^3.1.0",
21
22
  "@mdx-js/rollup": "^3.1.0",
22
- "@mdx-js/mdx": "^3.1.0",
23
23
  "@vitejs/plugin-react-swc": "^3.5.0",
24
24
  "prompts": "^2.4.2",
25
25
  "react": "^19.0.0",
@@ -37,12 +37,14 @@
37
37
  "@eslint/js": "^9.21.0",
38
38
  "@playwright/experimental-ct-react": "^1.51.0",
39
39
  "@playwright/test": "^1.51.0",
40
+ "@types/hast": "^3.0.4",
40
41
  "@types/mdast": "^4.0.4",
41
42
  "@types/mdx": "^2.0.13",
42
43
  "@types/node": "^22.13.9",
43
44
  "@types/prompts": "^2.4.9",
44
45
  "@types/react": "^19.0.10",
45
46
  "@types/react-dom": "^19.0.4",
47
+ "@types/unist": "^3.0.3",
46
48
  "eslint": "^9.21.0",
47
49
  "eslint-plugin-react-hooks": "^5.2.0",
48
50
  "eslint-plugin-react-refresh": "^0.4.18",
@@ -57,6 +59,7 @@
57
59
  "dev": "vite",
58
60
  "build": "tsc -b && vite build",
59
61
  "build:cli": "tsc --project tsconfig.cli.json",
62
+ "preversion": "pnpm build:cli",
60
63
  "lint": "eslint .",
61
64
  "preview": "vite preview",
62
65
  "build:pages": "node scripts/build-pages.ts",
package/src/App.tsx CHANGED
@@ -1,13 +1,38 @@
1
- import { useState, useRef, useEffect } from "react";
1
+ import { useState, useRef, useEffect, useCallback } from "react";
2
2
  import slidesContent from "virtual:slides.js";
3
3
 
4
4
  function App() {
5
- const [writingMode, setWritingMode] = useState("vertical-rl");
5
+ const [writingMode, setWritingMode] = useState(() => {
6
+ const saved = sessionStorage.getItem("slide-writing-mode");
7
+ return saved ?? "vertical-rl";
8
+ });
6
9
  const isVertical = writingMode !== "horizontal-tb";
7
10
  const slidesRef = useRef<HTMLDivElement>(null);
8
11
 
9
- const [fontSize, setFontSize] = useState(42);
10
- const [withAbsoluteFontSize, setWithAbsoluteFontSize] = useState(false);
12
+ const [fontSize, setFontSize] = useState(() => {
13
+ const saved = sessionStorage.getItem("slide-font-size");
14
+ return saved ? Number(saved) : 42;
15
+ });
16
+ const [withAbsoluteFontSize, setWithAbsoluteFontSize] = useState(() => {
17
+ const saved = sessionStorage.getItem("slide-with-absolute-font-size");
18
+ return saved === "true";
19
+ });
20
+
21
+ // 状態変更時にsessionStorageに保存
22
+ useEffect(() => {
23
+ sessionStorage.setItem("slide-writing-mode", writingMode);
24
+ }, [writingMode]);
25
+
26
+ useEffect(() => {
27
+ sessionStorage.setItem("slide-font-size", fontSize.toString());
28
+ }, [fontSize]);
29
+
30
+ useEffect(() => {
31
+ sessionStorage.setItem(
32
+ "slide-with-absolute-font-size",
33
+ withAbsoluteFontSize.toString(),
34
+ );
35
+ }, [withAbsoluteFontSize]);
11
36
 
12
37
  // ロード時にハッシュが入ってたらそのページにスクロール
13
38
  useEffect(() => {
@@ -44,7 +69,7 @@ function App() {
44
69
  };
45
70
  }, []);
46
71
 
47
- function gotoNextSlide(forward = true) {
72
+ const gotoNextSlide = useCallback((forward = true) => {
48
73
  const currentHash = location.hash;
49
74
  const currentIndex = parseInt(currentHash.replace("#page-", ""));
50
75
  const nextIndex = forward ? currentIndex + 1 : currentIndex - 1;
@@ -52,7 +77,7 @@ function App() {
52
77
  return;
53
78
  }
54
79
  location.hash = `#page-${nextIndex}`;
55
- }
80
+ }, []);
56
81
 
57
82
  // keydownイベントでページ送り
58
83
  useEffect(() => {
package/src/index.css CHANGED
@@ -40,7 +40,7 @@ code {
40
40
  .wrapper {
41
41
  width: 100%;
42
42
  height: 100%;
43
- padding: 1.2em 1.2em;
43
+ padding: 1.2rem 1.2rem;
44
44
 
45
45
  position: relative;
46
46
 
@@ -60,20 +60,20 @@ code {
60
60
  }
61
61
 
62
62
  h1 {
63
- font-size: 1.4em;
63
+ font-size: 1.4rem;
64
64
  font-weight: bold;
65
65
  margin: 0;
66
- margin-block-end: 0.2em;
66
+ margin-block-end: 0.2rem;
67
67
  }
68
68
  ul,
69
69
  ol {
70
70
  list-style-position: outside;
71
71
  margin: 0;
72
- margin-block-end: 1em;
72
+ margin-block-end: 1rem;
73
73
  }
74
74
  ul ul,
75
75
  ol ol {
76
- margin-block-end: 1em;
76
+ margin-block-end: 1rem;
77
77
  }
78
78
  ul ul ul,
79
79
  ol ol ol {
@@ -81,7 +81,7 @@ code {
81
81
  }
82
82
  p {
83
83
  margin: 0;
84
- margin-block-end: 0.5em;
84
+ margin-block-end: 0.5rem;
85
85
  }
86
86
 
87
87
  .wm-toggle {
@@ -9,16 +9,75 @@ import remarkParse from "remark-parse";
9
9
  import remarkRehype from "remark-rehype";
10
10
  import rehypeStringify from "rehype-stringify";
11
11
  import remarkSlideImages from "./remark-slide-images";
12
+ import { visit } from "unist-util-visit";
13
+ import type { Node } from "unist";
14
+ import type { Element, Text, ElementContent } from "hast";
12
15
 
13
- async function processMarkdown(markdown: string, base: string) {
16
+ /**
17
+ * CSS抽出用のrehypeプラグイン
18
+ * HTMLからstyleタグを抽出し、外部で管理
19
+ */
20
+ function rehypeExtractStyles(extractedStyles: string[]) {
21
+ return () => {
22
+ return (tree: Node) => {
23
+ visit(tree, "element", (node: Element) => {
24
+ if (node.tagName === "style" && node.children) {
25
+ // styleタグの内容を抽出
26
+ const textContent = node.children
27
+ .filter(
28
+ (child: ElementContent): child is Text => child.type === "text",
29
+ )
30
+ .map((child: Text) => child.value)
31
+ .join("");
32
+ if (textContent.trim()) {
33
+ extractedStyles.push(textContent);
34
+ }
35
+ // styleタグをHTMLから削除
36
+ node.children = [];
37
+ node.tagName = "div";
38
+ node.properties = { style: "display: none;" };
39
+ }
40
+ });
41
+ };
42
+ };
43
+ }
44
+
45
+ async function processMarkdown(
46
+ markdown: string,
47
+ base: string,
48
+ extractedStyles: string[],
49
+ ) {
14
50
  return await unified()
15
51
  .use(remarkParse)
16
52
  .use(remarkSlideImages, { base })
17
53
  .use(remarkRehype, { allowDangerousHtml: true })
54
+ .use(rehypeExtractStyles(extractedStyles))
18
55
  .use(rehypeStringify, { allowDangerousHtml: true })
19
56
  .process(markdown);
20
57
  }
21
58
 
59
+ /**
60
+ * 隣接CSSファイルを検索して読み込み
61
+ */
62
+ function loadAdjacentCSS(slidesDir: string, collection: string): string[] {
63
+ const collectionDir = path.resolve(slidesDir, collection);
64
+ const cssPath = path.resolve(collectionDir, "style.css");
65
+
66
+ if (fs.existsSync(cssPath)) {
67
+ try {
68
+ const cssContent = fs.readFileSync(cssPath, "utf-8");
69
+ if (cssContent.trim()) {
70
+ logger.info("Loaded adjacent CSS file: style.css");
71
+ return [cssContent];
72
+ }
73
+ } catch {
74
+ logger.warn("Failed to read CSS file: style.css");
75
+ }
76
+ }
77
+
78
+ return [];
79
+ }
80
+
22
81
  export interface SlidesPluginOptions {
23
82
  /** Directory containing the slides (absolute path) */
24
83
  slidesDir?: string;
@@ -124,6 +183,7 @@ export default async function slidesPlugin(
124
183
  let base: string;
125
184
  let compiledSlides: string[] = [];
126
185
  let resolvedConfig: ResolvedConfig;
186
+ let slideStyles: string[] = [];
127
187
  return {
128
188
  name: "vite-plugin-slides",
129
189
  configResolved(config: ResolvedConfig) {
@@ -181,19 +241,47 @@ export default async function slidesPlugin(
181
241
 
182
242
  const content = fs.readFileSync(filePath, "utf-8");
183
243
 
244
+ // 隣接CSSファイルを読み込み
245
+ const adjacentStyles = loadAdjacentCSS(
246
+ config.slidesDir,
247
+ config.collection,
248
+ );
249
+ slideStyles = [...adjacentStyles];
250
+
184
251
  if (!isMdx) {
185
252
  const slides = content.split(/^\s*(?:---|\*\*\*|___)\s*$/m);
253
+ const extractedStyles: string[] = [];
186
254
  const processedSlides = await Promise.all(
187
- slides.map((slide) => processMarkdown(slide, base)),
255
+ slides.map((slide) =>
256
+ processMarkdown(slide, base, extractedStyles),
257
+ ),
188
258
  );
259
+
260
+ // 抽出されたスタイルを追加
261
+ slideStyles = [...slideStyles, ...extractedStyles];
262
+
189
263
  return `export default ${JSON.stringify(processedSlides.map((p) => p.value))}`;
190
264
  }
191
265
 
192
266
  const slides = content.split(/^\s*(?:---|\*\*\*|___)\s*$/m);
193
267
 
268
+ // MDXにもCSS抽出を適用(MDXの場合はJSXのstyleタグを抽出)
269
+ const extractedStyles: string[] = [];
194
270
  const processedSlides = await Promise.all(
195
271
  slides.map(async (slideContent) => {
196
- const result = await compile(slideContent, {
272
+ // MDX内のstyleタグを手動で抽出(簡易実装)
273
+ const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
274
+ let match;
275
+ while ((match = styleRegex.exec(slideContent)) !== null) {
276
+ if (match[1].trim()) {
277
+ extractedStyles.push(match[1].trim());
278
+ }
279
+ }
280
+
281
+ // styleタグを削除したコンテンツでMDXコンパイル
282
+ const cleanedContent = slideContent.replace(styleRegex, "");
283
+
284
+ const result = await compile(cleanedContent, {
197
285
  outputFormat: "program",
198
286
  development: false,
199
287
  jsxImportSource: "react",
@@ -204,6 +292,9 @@ export default async function slidesPlugin(
204
292
  }),
205
293
  );
206
294
 
295
+ // 抽出されたスタイルを追加
296
+ slideStyles = [...slideStyles, ...extractedStyles];
297
+
207
298
  compiledSlides = processedSlides;
208
299
 
209
300
  const numberOfSlides = slides.length;
@@ -225,7 +316,13 @@ export default async function slidesPlugin(
225
316
  return filename.replace(/\.[jt]sx?$/, "");
226
317
  }
227
318
 
228
- // Return as a module
319
+ // スライド固有のCSSを文字列として生成
320
+ const slideStylesString =
321
+ slideStyles.length > 0
322
+ ? JSON.stringify(slideStyles.join("\n\n"))
323
+ : "null";
324
+
325
+ // Return as a module with CSS injection
229
326
  return `
230
327
  ${slideComponentsFilenames.map((filename) => `import * as ${filenameToComponentName(filename)} from '@components/${filename}';`).join("\n")}
231
328
 
@@ -233,6 +330,20 @@ export default async function slidesPlugin(
233
330
 
234
331
  ${compiledSlides.map((_, index) => `import Slide${formatSlideIndex(index)} from '${virtualFilePageId(index)}';`).join("\n")}
235
332
 
333
+ // スライド固有のCSSを注入
334
+ const slideStyles = ${slideStylesString};
335
+ if (slideStyles && typeof document !== 'undefined') {
336
+ const existingStyleElement = document.getElementById('slide-custom-styles');
337
+ if (existingStyleElement) {
338
+ existingStyleElement.textContent = slideStyles;
339
+ } else {
340
+ const styleElement = document.createElement('style');
341
+ styleElement.id = 'slide-custom-styles';
342
+ styleElement.textContent = slideStyles;
343
+ document.head.appendChild(styleElement);
344
+ }
345
+ }
346
+
236
347
  // provide slide components to each slide
237
348
  // Wrap SlideN components to provide SlideComponents
238
349
  ${compiledSlides
@@ -434,7 +545,7 @@ export default async function slidesPlugin(
434
545
 
435
546
  if (
436
547
  absolutePath.includes(absoluteSlidesDir) &&
437
- /\.(?:md|mdx)$/.test(absolutePath)
548
+ /\.(?:md|mdx|css)$/.test(absolutePath)
438
549
  ) {
439
550
  logger.info(`Slide file changed: ${absolutePath}`);
440
551
  reloadModule();
@@ -451,7 +562,7 @@ export default async function slidesPlugin(
451
562
 
452
563
  if (
453
564
  absolutePath.includes(absoluteSlidesDir) &&
454
- /\.(?:md|mdx)$/.test(absolutePath)
565
+ /\.(?:md|mdx|css)$/.test(absolutePath)
455
566
  ) {
456
567
  logger.info(`Slide file added: ${absolutePath}`);
457
568
  reloadModule();
@@ -468,7 +579,7 @@ export default async function slidesPlugin(
468
579
 
469
580
  if (
470
581
  absolutePath.includes(absoluteSlidesDir) &&
471
- /\.(?:md|mdx)$/.test(absolutePath)
582
+ /\.(?:md|mdx|css)$/.test(absolutePath)
472
583
  ) {
473
584
  logger.info(`Slide file deleted: ${absolutePath}`);
474
585
  reloadModule();