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

Merge pull request #9127 from weseek/imprv/153035-add-github-alert-notation

imprv: Add GitHub Markdown alerts
mergify[bot] 1 год назад
Родитель
Сommit
d6b427e680

+ 2 - 0
apps/app/package.json

@@ -273,6 +273,8 @@
     "react-hotkeys": "^2.0.0",
     "react-input-autosize": "^3.0.0",
     "react-toastify": "^9.1.3",
+    "remark-directive": "^3.0.0",
+    "remark-github-admonitions-to-directives": "^2.0.0",
     "rehype-rewrite": "^4.0.2",
     "replacestream": "^4.0.3",
     "sass": "^1.53.0",

+ 18 - 0
apps/app/src/client/services/renderer/renderer.tsx

@@ -10,6 +10,8 @@ import sanitize from 'rehype-sanitize';
 import slug from 'rehype-slug';
 import type { HtmlElementNode } from 'rehype-toc';
 import breaks from 'remark-breaks';
+import remarkDirective from 'remark-directive';
+import remarkGithubAdmonitionsToDirectives from 'remark-github-admonitions-to-directives';
 import math from 'remark-math';
 import deepmerge from 'ts-deepmerge';
 import type { Pluggable } from 'unified';
@@ -19,6 +21,7 @@ import { Header } from '~/client/components/ReactMarkdownComponents/Header';
 import { LightBox } from '~/client/components/ReactMarkdownComponents/LightBox';
 import { RichAttachment } from '~/client/components/ReactMarkdownComponents/RichAttachment';
 import { TableWithEditButton } from '~/client/components/ReactMarkdownComponents/TableWithEditButton';
+import * as callout from '~/features/callout';
 import * as mermaid from '~/features/mermaid';
 import type { RendererOptions } from '~/interfaces/renderer-options';
 import type { RendererConfig } from '~/interfaces/services/renderer';
@@ -65,6 +68,9 @@ export const generateViewOptions = (
     mermaid.remarkPlugin,
     xsvToTable.remarkPlugin,
     attachment.remarkPlugin,
+    remarkGithubAdmonitionsToDirectives,
+    remarkDirective,
+    callout.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
   );
@@ -78,6 +84,7 @@ export const generateViewOptions = (
       presentation.sanitizeOption,
       drawio.sanitizeOption,
       mermaid.sanitizeOption,
+      callout.sanitizeOption,
       attachment.sanitizeOption,
       lsxGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
@@ -112,6 +119,7 @@ export const generateViewOptions = (
     components.drawio = DrawioViewerWithEditButton;
     components.table = TableWithEditButton;
     components.mermaid = mermaid.MermaidViewer;
+    components.callout = callout.CalloutViewer;
     components.attachment = RichAttachment;
     components.img = LightBox;
   }
@@ -169,6 +177,9 @@ export const generateSimpleViewOptions = (
     mermaid.remarkPlugin,
     xsvToTable.remarkPlugin,
     attachment.remarkPlugin,
+    remarkGithubAdmonitionsToDirectives,
+    remarkDirective,
+    callout.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
   );
@@ -185,6 +196,7 @@ export const generateSimpleViewOptions = (
       presentation.sanitizeOption,
       drawio.sanitizeOption,
       mermaid.sanitizeOption,
+      callout.sanitizeOption,
       attachment.sanitizeOption,
       lsxGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
@@ -211,6 +223,7 @@ export const generateSimpleViewOptions = (
     components.gallery = refsGrowiDirective.GalleryImmutable;
     components.drawio = drawio.DrawioViewer;
     components.mermaid = mermaid.MermaidViewer;
+    components.callout = callout.CalloutViewer;
     components.attachment = RichAttachment;
     components.img = LightBox;
   }
@@ -262,6 +275,9 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     mermaid.remarkPlugin,
     xsvToTable.remarkPlugin,
     attachment.remarkPlugin,
+    remarkGithubAdmonitionsToDirectives,
+    remarkDirective,
+    callout.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
   );
@@ -274,6 +290,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
       getCommonSanitizeOption(config),
       drawio.sanitizeOption,
       mermaid.sanitizeOption,
+      callout.sanitizeOption,
       attachment.sanitizeOption,
       lsxGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
@@ -301,6 +318,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     components.gallery = refsGrowiDirective.GalleryImmutable;
     components.drawio = drawio.DrawioViewer;
     components.mermaid = mermaid.MermaidViewer;
+    components.callout = callout.CalloutViewer;
     components.attachment = RichAttachment;
     components.img = LightBox;
   }

+ 90 - 0
apps/app/src/features/callout/components/CalloutViewer.module.scss

@@ -0,0 +1,90 @@
+@use '@growi/core-styles/scss/bootstrap/init' as bs;
+
+// == Colors
+@include bs.color-mode(light) {
+  .callout-viewer {
+    --callout-accent-note: hsl(212, 92%, 45%);
+    --callout-accent-tip: hsl(137, 66%, 30%);
+    --callout-accent-important: hsl(261, 69%, 59%);
+    --callout-accent-warning: hsl(40, 100%, 30%);
+    --callout-accent-caution: hsl(356, 71%, 48%);
+  }
+}
+
+@include bs.color-mode(dark) {
+  .callout-viewer {
+    --callout-accent-note: hsl(215, 93%, 58%);
+    --callout-accent-tip: hsl(128, 49%, 49%);
+    --callout-accent-important: hsl(262, 89%, 71%);
+    --callout-accent-warning: hsl(41, 72%, 48%);
+    --callout-accent-caution: hsl(3, 93%, 63%);
+  }
+}
+
+.callout-viewer :global{
+
+  .callout {
+    padding: 0.5rem 1rem;
+    margin: 1rem 0rem;
+    color: inherit;
+  }
+
+  .callout-indicator {
+    display: flex;
+    align-items: center;
+    margin-bottom: 16px;
+    line-height: 1;
+  }
+
+  .callout-hint {
+    display: inline-block;
+    margin-right: 0.3rem;
+    vertical-align: text-bottom;
+    fill: currentColor;
+  }
+
+  .callout-note {
+    .callout-indicator {
+      color: var(--callout-accent-note);
+    }
+    border-left: .25em solid var(--callout-accent-note);
+  }
+
+  .callout-tip {
+    .callout-indicator {
+      color: var(--callout-accent-tip);
+    }
+    border-left: .25em solid var(--callout-accent-tip);
+  }
+
+  .callout-warning {
+    .callout-indicator {
+      color: var(--callout-accent-warning);
+    }
+    border-left:.25em solid var(--callout-accent-warning);
+  }
+
+  .callout-caution {
+    .callout-indicator {
+      color: var(--callout-accent-caution);
+    }
+    border-left:.25em solid var(--callout-accent-caution);
+  }
+
+  .callout-important {
+    .callout-indicator {
+      color: var(--callout-accent-important);
+    }
+    border-left:.25em solid var(--callout-accent-important);
+  }
+
+  .callout-content:first-child,
+  .callout-content:only-child {
+    margin-block-start: 0;
+  }
+
+  .callout-content:last-child,
+  .callout-content:only-child {
+    margin-block-end: 0;
+  }
+}

+ 64 - 0
apps/app/src/features/callout/components/CalloutViewer.tsx

@@ -0,0 +1,64 @@
+// Ref: https://github.com/Microflash/remark-callout-directives/blob/fabe4d8adc7738469f253836f0da346591ea2a2b/README.md
+
+import type { ReactNode } from 'react';
+import React from 'react';
+
+import { type Callout } from '../services/consts';
+
+import styles from './CalloutViewer.module.scss';
+
+const moduleClass = styles['callout-viewer'];
+
+type CALLOUT_TO = {
+  [key in Callout]: string;
+}
+
+const CALLOUT_TO_TITLE: CALLOUT_TO = {
+  note: 'Note',
+  tip: 'Tip',
+  important: 'Important',
+  warning: 'Warning',
+  caution: 'Caution',
+};
+
+const CALLOUT_TO_ICON: CALLOUT_TO = {
+  note: 'info',
+  tip: 'lightbulb',
+  important: 'feedback',
+  warning: 'warning',
+  caution: 'report',
+};
+
+type CalloutViewerProps = {
+  children: ReactNode,
+  node: Element,
+  name: string
+}
+
+export const CalloutViewer = React.memo((props: CalloutViewerProps): JSX.Element => {
+
+  const { node, name, children } = props;
+
+  if (node == null) {
+    return <></>;
+  }
+
+  return (
+    <div className={`${moduleClass} callout-viewer`}>
+      <div className={`callout callout-${CALLOUT_TO_TITLE[name].toLowerCase()}`}>
+        <div className="callout-indicator">
+          <div className="callout-hint">
+            <span className="material-symbols-outlined">{CALLOUT_TO_ICON[name]}</span>
+          </div>
+          <div className="callout-title">
+            {CALLOUT_TO_TITLE[name]}
+          </div>
+        </div>
+        <div className="callout-content">
+          {children}
+        </div>
+      </div>
+    </div>
+  );
+});
+CalloutViewer.displayName = 'CalloutViewer';

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

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

+ 2 - 0
apps/app/src/features/callout/index.ts

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

+ 23 - 0
apps/app/src/features/callout/services/callout.ts

@@ -0,0 +1,23 @@
+import type { ContainerDirective } from 'mdast-util-directive';
+import type { Plugin } from 'unified';
+import { visit } from 'unist-util-visit';
+
+import { AllCallout } from './consts';
+
+export const remarkPlugin: Plugin = () => {
+  return (tree) => {
+    visit(tree, 'containerDirective', (node: ContainerDirective) => {
+      if (AllCallout.some(name => name === node.name)) {
+        const data = node.data ?? (node.data = {});
+        data.hName = 'callout';
+        data.hProperties = {
+          name: node.name,
+        };
+      }
+    });
+  };
+};
+
+export const sanitizeOption = {
+  tagNames: ['callout'],
+};

+ 5 - 0
apps/app/src/features/callout/services/consts.ts

@@ -0,0 +1,5 @@
+// Ref: https://github.com/Microflash/remark-callout-directives/blob/fabe4d8adc7738469f253836f0da346591ea2a2b/themes/github/index.js
+// Ref: https://github.com/orgs/community/discussions/16925
+
+export const AllCallout = ['note', 'tip', 'important', 'warning', 'caution'] as const;
+export type Callout = typeof AllCallout[number];

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

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

+ 2 - 1
apps/app/src/features/mermaid/components/MermaidViewer.tsx

@@ -1,4 +1,5 @@
-import React, { useRef, useEffect, ReactNode } from 'react';
+import type { ReactNode } from 'react';
+import React, { useRef, useEffect } from 'react';
 
 import mermaid from 'mermaid';
 

+ 47 - 0
yarn.lock

@@ -12484,6 +12484,20 @@ md5@^2.2.1:
     crypt "~0.0.1"
     is-buffer "~1.1.1"
 
+mdast-util-directive@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-directive/-/mdast-util-directive-3.0.0.tgz#3fb1764e705bbdf0afb0d3f889e4404c3e82561f"
+  integrity sha512-JUpYOqKI4mM3sZcNxmF/ox04XYFFkNwr0CFlrQIkCwbvH0xzMCqkMqAde9wRd80VAhaUrwFwKm2nxretdT1h7Q==
+  dependencies:
+    "@types/mdast" "^4.0.0"
+    "@types/unist" "^3.0.0"
+    devlop "^1.0.0"
+    mdast-util-from-markdown "^2.0.0"
+    mdast-util-to-markdown "^2.0.0"
+    parse-entities "^4.0.0"
+    stringify-entities "^4.0.0"
+    unist-util-visit-parents "^6.0.0"
+
 mdast-util-find-and-replace@^3.0.0, mdast-util-find-and-replace@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz#a6fc7b62f0994e973490e45262e4bc07607b04e0"
@@ -12883,6 +12897,19 @@ micromark-core-commonmark@^2.0.0:
     micromark-util-symbol "^2.0.0"
     micromark-util-types "^2.0.0"
 
+micromark-extension-directive@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/micromark-extension-directive/-/micromark-extension-directive-3.0.1.tgz#67b3985bb991a69dbcae52664c57ee54b22f635a"
+  integrity sha512-VGV2uxUzhEZmaP7NSFo2vtq7M2nUD+WfmYQD+d8i/1nHbzE+rMy9uzTvUybBbNiVbrhOZibg3gbyoARGqgDWyg==
+  dependencies:
+    devlop "^1.0.0"
+    micromark-factory-space "^2.0.0"
+    micromark-factory-whitespace "^2.0.0"
+    micromark-util-character "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
+    parse-entities "^4.0.0"
+
 micromark-extension-frontmatter@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz#651c52ffa5d7a8eeed687c513cd869885882d67a"
@@ -15791,6 +15818,16 @@ remark-breaks@^4.0.0:
     mdast-util-newline-to-break "^2.0.0"
     unified "^11.0.0"
 
+remark-directive@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/remark-directive/-/remark-directive-3.0.0.tgz#34452d951b37e6207d2e2a4f830dc33442923268"
+  integrity sha512-l1UyWJ6Eg1VPU7Hm/9tt0zKtReJQNOA4+iDMAxTyZNWnJnFlbS/7zhiel/rogTLQ2vMYwDzSJa4BiVNqGlqIMA==
+  dependencies:
+    "@types/mdast" "^4.0.0"
+    mdast-util-directive "^3.0.0"
+    micromark-extension-directive "^3.0.0"
+    unified "^11.0.0"
+
 remark-emoji@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/remark-emoji/-/remark-emoji-5.0.0.tgz#14bd520b765cea3f00228258926ed59ad8125729"
@@ -15824,6 +15861,16 @@ remark-gfm@^4.0.0:
     remark-stringify "^11.0.0"
     unified "^11.0.0"
 
+remark-github-admonitions-to-directives@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/remark-github-admonitions-to-directives/-/remark-github-admonitions-to-directives-2.0.0.tgz#88a9a1dde87e65890f0eb5b48502cd798ca12543"
+  integrity sha512-/fXZWZrU+mr5VeRShPPnzUbWPmOktBAN1vqSwzktVdchhhsL1CqfdBwiQH7mkh8yaxOo/RtXysxlVLXwD2a/Dw==
+  dependencies:
+    "@types/mdast" "^4.0.0"
+    mdast-util-directive "^3.0.0"
+    unified "^11.0.0"
+    unist-util-visit "^5.0.0"
+
 remark-math@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/remark-math/-/remark-math-6.0.0.tgz#0acdf74675f1c195fea6efffa78582f7ed7fc0d7"