Browse Source

feat: implement PlantUmlViewer component with rendering status handling

Yuki Takei 4 days ago
parent
commit
d06c43fbe6

+ 31 - 0
apps/app/src/features/plantuml/components/PlantUmlViewer.tsx

@@ -0,0 +1,31 @@
+import React, { type JSX, useCallback, useRef } from 'react';
+import { GROWI_IS_CONTENT_RENDERING_ATTR } from '@growi/core/dist/consts';
+
+type PlantUmlViewerProps = {
+  src: string;
+};
+
+export const PlantUmlViewer = React.memo(
+  ({ src }: PlantUmlViewerProps): JSX.Element => {
+    const containerRef = useRef<HTMLDivElement>(null);
+
+    const handleLoaded = useCallback(() => {
+      containerRef.current?.setAttribute(
+        GROWI_IS_CONTENT_RENDERING_ATTR,
+        'false',
+      );
+    }, []);
+
+    return (
+      <div
+        ref={containerRef}
+        {...{ [GROWI_IS_CONTENT_RENDERING_ATTR]: 'true' }}
+      >
+        {/* biome-ignore lint/a11y/useAltText: PlantUML diagrams are purely visual */}
+        <img src={src} onLoad={handleLoaded} onError={handleLoaded} />
+      </div>
+    );
+  },
+);
+
+PlantUmlViewer.displayName = 'PlantUmlViewer';

+ 1 - 0
apps/app/src/features/plantuml/components/index.ts

@@ -0,0 +1 @@
+export { PlantUmlViewer } from './PlantUmlViewer';

+ 1 - 0
apps/app/src/features/plantuml/index.ts

@@ -1 +1,2 @@
+export * from './components';
 export * from './services';

+ 1 - 1
apps/app/src/features/plantuml/services/index.ts

@@ -1 +1 @@
-export { remarkPlugin } from './plantuml';
+export { remarkPlugin, sanitizeOption } from './plantuml';

+ 37 - 1
apps/app/src/features/plantuml/services/plantuml.ts

@@ -1,5 +1,7 @@
 import plantuml from '@akebifiky/remark-simple-plantuml';
-import type { Code } from 'mdast';
+import { GROWI_IS_CONTENT_RENDERING_ATTR } from '@growi/core/dist/consts';
+import type { Schema as SanitizeOption } from 'hast-util-sanitize';
+import type { Code, Image } from 'mdast';
 import type { Plugin } from 'unified';
 import { visit } from 'unist-util-visit';
 import urljoin from 'url-join';
@@ -28,6 +30,40 @@ export const remarkPlugin: Plugin<[PlantUMLPluginParams]> = (options) => {
       }
     });
 
+    // Let remark-simple-plantuml convert plantuml code blocks to image nodes
     simplePlantumlPlugin(tree, file);
+
+    // Transform plantuml image nodes into custom <plantuml> elements that carry
+    // the rendering-status attribute, allowing the auto-scroll system to detect
+    // and compensate for the layout shift caused by async image loading.
+    visit(tree, 'image', (node: Image) => {
+      if (plantumlUri.length === 0 || !node.url.startsWith(baseUrl)) {
+        return;
+      }
+
+      const src = node.url;
+
+      // Mutate the image node into a custom paragraph-like element.
+      // hName overrides the HTML tag; hProperties set element attributes.
+      const mutableNode = node as unknown as Record<string, unknown>;
+      mutableNode.type = 'paragraph';
+      mutableNode.children = [];
+      mutableNode.url = undefined;
+      mutableNode.alt = undefined;
+      mutableNode.data = {
+        hName: 'plantuml',
+        hProperties: {
+          src,
+          [GROWI_IS_CONTENT_RENDERING_ATTR]: 'true',
+        },
+      };
+    });
   };
 };
+
+export const sanitizeOption: SanitizeOption = {
+  tagNames: ['plantuml'],
+  attributes: {
+    plantuml: ['src', GROWI_IS_CONTENT_RENDERING_ATTR],
+  },
+};