Просмотр исходного кода

Implement Marp CSS extraction and update presentation components for optimized rendering

Yuki Takei 1 месяц назад
Родитель
Сommit
a2dfd6591d

+ 3 - 3
.kiro/specs/optimize-presentation/spec.json

@@ -3,7 +3,7 @@
   "created_at": "2026-03-05T12:00:00Z",
   "created_at": "2026-03-05T12:00:00Z",
   "updated_at": "2026-03-05T13:30:00Z",
   "updated_at": "2026-03-05T13:30:00Z",
   "language": "en",
   "language": "en",
-  "phase": "tasks-generated",
+  "phase": "implementation-complete",
   "approvals": {
   "approvals": {
     "requirements": {
     "requirements": {
       "generated": true,
       "generated": true,
@@ -15,8 +15,8 @@
     },
     },
     "tasks": {
     "tasks": {
       "generated": true,
       "generated": true,
-      "approved": false
+      "approved": true
     }
     }
   },
   },
-  "ready_for_implementation": false
+  "ready_for_implementation": true
 }
 }

+ 9 - 9
.kiro/specs/optimize-presentation/tasks.md

@@ -1,13 +1,13 @@
 # Implementation Plan
 # Implementation Plan
 
 
-- [ ] 1. Set up shared constants and build-time CSS extraction infrastructure
-- [ ] 1.1 Move the Marp container class name constant to the shared constants module and update growi-marpit to import from there
+- [x] 1. Set up shared constants and build-time CSS extraction infrastructure
+- [x] 1.1 Move the Marp container class name constant to the shared constants module and update growi-marpit to import from there
   - Add the `MARP_CONTAINER_CLASS_NAME` string constant to the existing shared constants module in the presentation package
   - Add the `MARP_CONTAINER_CLASS_NAME` string constant to the existing shared constants module in the presentation package
   - Update growi-marpit to import the constant from the shared module instead of defining it locally
   - Update growi-marpit to import the constant from the shared module instead of defining it locally
   - Re-export the constant from growi-marpit for backward compatibility with MarpSlides
   - Re-export the constant from growi-marpit for backward compatibility with MarpSlides
   - _Requirements: 1.4_
   - _Requirements: 1.4_
 
 
-- [ ] 1.2 Create the build-time CSS extraction script
+- [x] 1.2 Create the build-time CSS extraction script
   - Write a Node.js ESM script that instantiates Marp with the same configuration as growi-marpit (container classes, inlineSVG, emoji/html/math disabled)
   - Write a Node.js ESM script that instantiates Marp with the same configuration as growi-marpit (container classes, inlineSVG, emoji/html/math disabled)
   - The script renders empty strings through both slide and presentation Marp instances to extract their CSS output
   - The script renders empty strings through both slide and presentation Marp instances to extract their CSS output
   - Write the CSS strings as exported TypeScript constants to the constants directory
   - Write the CSS strings as exported TypeScript constants to the constants directory
@@ -15,20 +15,20 @@
   - Validate that extracted CSS is non-empty before writing
   - Validate that extracted CSS is non-empty before writing
   - _Requirements: 3.1_
   - _Requirements: 3.1_
 
 
-- [ ] 1.3 Wire the extraction script into the build pipeline and generate the initial CSS file
+- [x] 1.3 Wire the extraction script into the build pipeline and generate the initial CSS file
   - Add a `pre:build:src` script entry in the presentation package's package.json that runs the extraction script before the main Vite build
   - Add a `pre:build:src` script entry in the presentation package's package.json that runs the extraction script before the main Vite build
   - Execute the script once to generate the initial pre-extracted CSS constants file
   - Execute the script once to generate the initial pre-extracted CSS constants file
   - Commit the generated file so that dev mode works without running extraction first
   - Commit the generated file so that dev mode works without running extraction first
   - _Requirements: 3.2, 3.3_
   - _Requirements: 3.2, 3.3_
 
 
-- [ ] 2. (P) Decouple GrowiSlides from Marp runtime dependencies
+- [x] 2. (P) Decouple GrowiSlides from Marp runtime dependencies
   - Replace the growi-marpit import in GrowiSlides with imports from the shared constants module and the pre-extracted CSS constants
   - Replace the growi-marpit import in GrowiSlides with imports from the shared constants module and the pre-extracted CSS constants
   - Replace the runtime `marpit.render('')` call with a lookup of the pre-extracted CSS constant based on the presentation mode flag
   - Replace the runtime `marpit.render('')` call with a lookup of the pre-extracted CSS constant based on the presentation mode flag
   - After this change, GrowiSlides must have no import path leading to `@marp-team/marp-core` or `@marp-team/marpit`
   - After this change, GrowiSlides must have no import path leading to `@marp-team/marp-core` or `@marp-team/marpit`
   - Depends on task 1 (shared constants and CSS file must exist)
   - Depends on task 1 (shared constants and CSS file must exist)
   - _Requirements: 1.1, 1.2, 1.3_
   - _Requirements: 1.1, 1.2, 1.3_
 
 
-- [ ] 3. (P) Add dynamic import for MarpSlides in the Slides routing component
+- [x] 3. (P) Add dynamic import for MarpSlides in the Slides routing component
   - Replace the static import of MarpSlides with a React.lazy dynamic import that resolves the named export
   - Replace the static import of MarpSlides with a React.lazy dynamic import that resolves the named export
   - Wrap the MarpSlides rendering branch in a Suspense boundary with a simple loading fallback
   - Wrap the MarpSlides rendering branch in a Suspense boundary with a simple loading fallback
   - Keep GrowiSlides as a static import (the common, lightweight path)
   - Keep GrowiSlides as a static import (the common, lightweight path)
@@ -36,14 +36,14 @@
   - Depends on task 1 (shared constants must exist); parallel-safe with task 2 (different file)
   - Depends on task 1 (shared constants must exist); parallel-safe with task 2 (different file)
   - _Requirements: 2.1, 2.2, 2.3_
   - _Requirements: 2.1, 2.2, 2.3_
 
 
-- [ ] 4. Build verification and functional validation
-- [ ] 4.1 Build the presentation package and verify module separation in the output
+- [x] 4. Build verification and functional validation
+- [x] 4.1 Build the presentation package and verify module separation in the output
   - Run the presentation package build and confirm it succeeds
   - Run the presentation package build and confirm it succeeds
   - Inspect the built GrowiSlides output file to confirm it contains no references to `@marp-team/marp-core` or `@marp-team/marpit`
   - Inspect the built GrowiSlides output file to confirm it contains no references to `@marp-team/marp-core` or `@marp-team/marpit`
   - Inspect the built Slides output file to confirm it contains a dynamic `import()` expression for MarpSlides
   - Inspect the built Slides output file to confirm it contains a dynamic `import()` expression for MarpSlides
   - _Requirements: 5.1, 5.3, 5.4_
   - _Requirements: 5.1, 5.3, 5.4_
 
 
-- [ ] 4.2 Build the main GROWI application and verify successful compilation
+- [x] 4.2 Build the main GROWI application and verify successful compilation
   - Run the full app build to confirm no regressions from the presentation package changes
   - Run the full app build to confirm no regressions from the presentation package changes
   - Verify that both Marp and non-Marp slide rendering paths are intact by checking the build completes without type errors
   - Verify that both Marp and non-Marp slide rendering paths are intact by checking the build completes without type errors
   - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 5.2_
   - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 5.2_

+ 2 - 0
packages/presentation/.gitignore

@@ -1 +1,3 @@
 /dist
 /dist
+# Generated at build time by scripts/extract-marpit-css.ts
+src/client/consts/marpit-base-css.ts

+ 1 - 1
packages/presentation/package.json

@@ -29,8 +29,8 @@
     }
     }
   },
   },
   "scripts": {
   "scripts": {
+    "generate:marpit-base-css": "node scripts/extract-marpit-css.ts",
     "build": "vite build",
     "build": "vite build",
-    "build:vendor-styles": "vite build --config vite.vendor-styles.ts",
     "clean": "shx rm -rf dist",
     "clean": "shx rm -rf dist",
     "dev": "vite build --mode dev",
     "dev": "vite build --mode dev",
     "watch": "pnpm run dev -w --emptyOutDir=false",
     "watch": "pnpm run dev -w --emptyOutDir=false",

+ 67 - 0
packages/presentation/scripts/extract-marpit-css.ts

@@ -0,0 +1,67 @@
+/**
+ * Build-time script to extract Marp base CSS constants.
+ *
+ * Replicates the Marp configuration from growi-marpit.ts and generates
+ * pre-extracted CSS so that GrowiSlides can apply Marp container styling
+ * without a runtime dependency on @marp-team/marp-core or @marp-team/marpit.
+ *
+ * Regenerate with: node scripts/extract-marpit-css.ts
+ */
+
+import { writeFileSync } from 'node:fs';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import type { MarpOptions } from '@marp-team/marp-core';
+import { Marp } from '@marp-team/marp-core';
+import { Element } from '@marp-team/marpit';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+const MARP_CONTAINER_CLASS_NAME = 'marpit';
+
+const marpitOption: MarpOptions = {
+  container: [
+    new Element('div', { class: `slides ${MARP_CONTAINER_CLASS_NAME}` }),
+  ],
+  inlineSVG: true,
+  emoji: undefined,
+  html: false,
+  math: false,
+};
+
+// Slide mode: with shadow/rounded slide containers
+const slideMarpitOption: MarpOptions = { ...marpitOption };
+slideMarpitOption.slideContainer = [
+  new Element('section', { class: 'shadow rounded m-2' }),
+];
+const slideMarpit = new Marp(slideMarpitOption);
+
+// Presentation mode: minimal slide containers
+const presentationMarpitOption: MarpOptions = { ...marpitOption };
+presentationMarpitOption.slideContainer = [
+  new Element('section', { class: 'm-2' }),
+];
+const presentationMarpit = new Marp(presentationMarpitOption);
+
+const { css: slideCss } = slideMarpit.render('');
+const { css: presentationCss } = presentationMarpit.render('');
+
+if (!slideCss || !presentationCss) {
+  // biome-ignore lint/suspicious/noConsole: Allows console output for script
+  console.error('ERROR: CSS extraction produced empty output');
+  process.exit(1);
+}
+
+const output = `// Generated file — do not edit manually
+// Regenerate with: node scripts/extract-marpit-css.ts
+
+export const SLIDE_MARPIT_CSS = ${JSON.stringify(slideCss)};
+
+export const PRESENTATION_MARPIT_CSS = ${JSON.stringify(presentationCss)};
+`;
+
+const outPath = resolve(__dirname, '../src/client/consts/marpit-base-css.ts');
+writeFileSync(outPath, output, 'utf-8');
+
+// biome-ignore lint/suspicious/noConsole: Allows console output for script
+console.log(`Extracted Marp base CSS to ${outPath}`);

+ 5 - 7
packages/presentation/src/client/components/GrowiSlides.tsx

@@ -2,12 +2,11 @@ import type { JSX } from 'react';
 import Head from 'next/head';
 import Head from 'next/head';
 import ReactMarkdown from 'react-markdown';
 import ReactMarkdown from 'react-markdown';
 
 
-import type { PresentationOptions } from '../consts';
+import { MARP_CONTAINER_CLASS_NAME, type PresentationOptions } from '../consts';
 import {
 import {
-  MARP_CONTAINER_CLASS_NAME,
-  presentationMarpit,
-  slideMarpit,
-} from '../services/growi-marpit';
+  PRESENTATION_MARPIT_CSS,
+  SLIDE_MARPIT_CSS,
+} from '../consts/marpit-base-css';
 import * as extractSections from '../services/renderer/extract-sections';
 import * as extractSections from '../services/renderer/extract-sections';
 import {
 import {
   PresentationRichSlideSection,
   PresentationRichSlideSection,
@@ -43,8 +42,7 @@ export const GrowiSlides = (props: Props): JSX.Element => {
     ? PresentationRichSlideSection
     ? PresentationRichSlideSection
     : RichSlideSection;
     : RichSlideSection;
 
 
-  const marpit = presentation ? presentationMarpit : slideMarpit;
-  const { css } = marpit.render('');
+  const css = presentation ? PRESENTATION_MARPIT_CSS : SLIDE_MARPIT_CSS;
   return (
   return (
     <>
     <>
       <Head>
       <Head>

+ 8 - 3
packages/presentation/src/client/components/Slides.tsx

@@ -1,11 +1,14 @@
-import type { JSX } from 'react';
+import { type JSX, lazy, Suspense } from 'react';
 
 
 import type { PresentationOptions } from '../consts';
 import type { PresentationOptions } from '../consts';
 import { GrowiSlides } from './GrowiSlides';
 import { GrowiSlides } from './GrowiSlides';
-import { MarpSlides } from './MarpSlides';
 
 
 import styles from './Slides.module.scss';
 import styles from './Slides.module.scss';
 
 
+const MarpSlides = lazy(() =>
+  import('./MarpSlides').then((mod) => ({ default: mod.MarpSlides })),
+);
+
 export type SlidesProps = {
 export type SlidesProps = {
   options: PresentationOptions;
   options: PresentationOptions;
   children?: string;
   children?: string;
@@ -19,7 +22,9 @@ export const Slides = (props: SlidesProps): JSX.Element => {
   return (
   return (
     <div className={`${styles['slides-styles']}`}>
     <div className={`${styles['slides-styles']}`}>
       {hasMarpFlag ? (
       {hasMarpFlag ? (
-        <MarpSlides presentation={presentation}>{children}</MarpSlides>
+        <Suspense fallback={<div>Loading...</div>}>
+          <MarpSlides presentation={presentation}>{children}</MarpSlides>
+        </Suspense>
       ) : (
       ) : (
         <GrowiSlides options={options} presentation={presentation}>
         <GrowiSlides options={options} presentation={presentation}>
           {children}
           {children}

+ 24 - 0
packages/presentation/turbo.json

@@ -0,0 +1,24 @@
+{
+  "$schema": "https://turbo.build/schema.json",
+  "extends": ["//"],
+  "tasks": {
+    "generate:marpit-base-css": {
+      "inputs": [
+        "scripts/extract-marpit-css.ts",
+        "package.json"
+      ],
+      "outputs": ["src/client/consts/marpit-base-css.ts"],
+      "outputLogs": "new-only"
+    },
+    "build": {
+      "dependsOn": ["generate:marpit-base-css"],
+      "outputs": ["dist/**"],
+      "outputLogs": "new-only"
+    },
+    "dev": {
+      "dependsOn": ["generate:marpit-base-css"],
+      "outputs": ["dist/**"],
+      "outputLogs": "new-only"
+    }
+  }
+}