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

Merge pull request #9392 from weseek/imprv/callout-custom-label

imprv: GitHub Alert with directive syntax
mergify[bot] 1 год назад
Родитель
Сommit
b455604a95

+ 20 - 0
apps/app/resource/locales/en_US/sandbox-markdown.md

@@ -33,6 +33,26 @@
 > Advises about risks or negative outcomes of certain actions.
 ```
 
+You can also use [directive syntax](https://talk.commonmark.org/t/generic-directives-plugins-syntax/444).
+
+:::note
+Useful information that users should know, even when skimming content.
+:::
+
+:::tip[Custom Label]
+Useful information that users should know, even when skimming content.
+:::
+
+```markdown
+:::note
+Useful information that users should know, even when skimming content.
+:::
+
+:::tip[Custom Label]
+Useful information that users should know, even when skimming content.
+:::
+```
+
 
 # Quote text
 - Use quoted expressions by putting `>` at the beginning of the paragraph

+ 20 - 0
apps/app/resource/locales/fr_FR/sandbox-markdown.md

@@ -33,6 +33,26 @@
 > Advises about risks or negative outcomes of certain actions.
 ```
 
+Vous pouvez également utiliser la [syntaxe de directive](https://talk.commonmark.org/t/generic-directives-plugins-syntax/444).
+
+:::note
+Useful information that users should know, even when skimming content.
+:::
+
+:::tip[Custom Label]
+Useful information that users should know, even when skimming content.
+:::
+
+```markdown
+:::note
+Useful information that users should know, even when skimming content.
+:::
+
+:::tip[Custom Label]
+Useful information that users should know, even when skimming content.
+:::
+```
+
 
 # Autres
 ## Citations

+ 20 - 0
apps/app/resource/locales/ja_JP/sandbox-markdown.md

@@ -33,6 +33,26 @@
 > Advises about risks or negative outcomes of certain actions.
 ```
 
+[directive](https://talk.commonmark.org/t/generic-directives-plugins-syntax/444) を使って記述することもできます。
+
+:::note
+Useful information that users should know, even when skimming content.
+:::
+
+:::tip[Custom Label]
+Useful information that users should know, even when skimming content.
+:::
+
+```markdown
+:::note
+Useful information that users should know, even when skimming content.
+:::
+
+:::tip[Custom Label]
+Useful information that users should know, even when skimming content.
+:::
+```
+
 
 # テキストの引用
 - 行頭に `>` を記述することで引用表現を記述できます

+ 20 - 0
apps/app/resource/locales/zh_CN/sandbox-markdown.md

@@ -33,6 +33,26 @@
 > Advises about risks or negative outcomes of certain actions.
 ```
 
+您还可以使用[directive 语法](https://talk.commonmark.org/t/generic-directives-plugins-syntax/444)。
+
+:::note
+Useful information that users should know, even when skimming content.
+:::
+
+:::tip[Custom Label]
+Useful information that users should know, even when skimming content.
+:::
+
+```markdown
+:::note
+Useful information that users should know, even when skimming content.
+:::
+
+:::tip[Custom Label]
+Useful information that users should know, even when skimming content.
+:::
+```
+
 
 # 引用
 - 在段落开头放置 `>` 即可使用带引号的表达式

+ 9 - 6
apps/app/src/features/callout/components/CalloutViewer.tsx

@@ -13,7 +13,7 @@ type CALLOUT_TO = {
   [key in Callout]: string;
 }
 
-const CALLOUT_TO_TITLE: CALLOUT_TO = {
+const CALLOUT_TO_TYPE: CALLOUT_TO = {
   note: 'Note',
   tip: 'Tip',
   important: 'Important',
@@ -36,12 +36,15 @@ const CALLOUT_TO_ICON: CALLOUT_TO = {
 type CalloutViewerProps = {
   children: ReactNode,
   node: Element,
-  name: string
+  type: string,
+  label?: string,
 }
 
 export const CalloutViewer = React.memo((props: CalloutViewerProps): JSX.Element => {
 
-  const { node, name, children } = props;
+  const {
+    node, type, label, children,
+  } = props;
 
   if (node == null) {
     return <></>;
@@ -49,13 +52,13 @@ export const CalloutViewer = React.memo((props: CalloutViewerProps): JSX.Element
 
   return (
     <div className={`${moduleClass} callout-viewer`}>
-      <div className={`callout callout-${CALLOUT_TO_TITLE[name].toLowerCase()}`}>
+      <div className={`callout callout-${CALLOUT_TO_TYPE[type].toLowerCase()}`}>
         <div className="callout-indicator">
           <div className="callout-hint">
-            <span className="material-symbols-outlined">{CALLOUT_TO_ICON[name]}</span>
+            <span className="material-symbols-outlined">{CALLOUT_TO_ICON[type]}</span>
           </div>
           <div className="callout-title">
-            {CALLOUT_TO_TITLE[name]}
+            {label ?? CALLOUT_TO_TYPE[type]}
           </div>
         </div>
         <div className="callout-content">

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

@@ -0,0 +1,80 @@
+import type { ContainerDirective } from 'mdast-util-directive';
+import remarkDirective from 'remark-directive';
+import remarkParse from 'remark-parse';
+import { unified } from 'unified';
+import { visit } from 'unist-util-visit';
+import { describe, it, expect } from 'vitest';
+
+import * as callout from './callout';
+
+describe('remarkPlugin', () => {
+  it('should transform containerDirective to callout', () => {
+    const processor = unified()
+      .use(remarkParse)
+      .use(remarkDirective)
+      .use(callout.remarkPlugin);
+
+    const markdown = `
+:::info
+This is an info callout.
+:::
+    `;
+
+    const tree = processor.parse(markdown);
+    processor.runSync(tree);
+
+    let calloutNode;
+    visit(tree, 'containerDirective', (node) => {
+      calloutNode = node;
+    });
+
+    expect(calloutNode).toBeDefined();
+
+    assert(calloutNode?.data?.hName != null);
+    assert(calloutNode?.data?.hProperties != null);
+
+    expect(calloutNode.data.hName).toBe('callout');
+    expect(calloutNode.data.hProperties.type).toBe('info');
+    expect(calloutNode.data.hProperties.label).toBe('info');
+
+    assert('children' in calloutNode.children[0]);
+    assert('value' in calloutNode.children[0].children[0]);
+
+    expect(calloutNode.children[0].children[0].value).toBe('This is an info callout.');
+  });
+
+  it('should transform containerDirective to callout with custom label', () => {
+    const processor = unified()
+      .use(remarkParse)
+      .use(remarkDirective)
+      .use(callout.remarkPlugin);
+
+    const markdown = `
+:::info[CUSTOM LABEL]
+This is an info callout.
+:::
+    `;
+
+    const tree = processor.parse(markdown);
+    processor.runSync(tree);
+
+    let calloutNode: ContainerDirective | undefined;
+    visit(tree, 'containerDirective', (node) => {
+      calloutNode = node;
+    });
+
+    expect(calloutNode).toBeDefined();
+
+    assert(calloutNode?.data?.hName != null);
+    assert(calloutNode?.data?.hProperties != null);
+
+    expect(calloutNode.data.hName).toBe('callout');
+    expect(calloutNode.data.hProperties.type).toBe('info');
+    expect(calloutNode.data.hProperties.label).toBe('CUSTOM LABEL');
+
+    assert('children' in calloutNode.children[0]);
+    assert('value' in calloutNode.children[0].children[0]);
+
+    expect(calloutNode.children[0].children[0].value).toBe('This is an info callout.');
+  });
+});

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

@@ -1,3 +1,4 @@
+import type { Paragraph, Text } from 'mdast';
 import type { ContainerDirective } from 'mdast-util-directive';
 import type { Plugin } from 'unified';
 import { visit } from 'unist-util-visit';
@@ -8,11 +9,26 @@ export const remarkPlugin: Plugin = () => {
   return (tree) => {
     visit(tree, 'containerDirective', (node: ContainerDirective) => {
       if (AllCallout.some(name => name === node.name.toLowerCase())) {
+        const type = node.name.toLowerCase();
         const data = node.data ?? (node.data = {});
+
+        // extract directive label
+        const paragraphs = (node.children ?? []).filter((child): child is Paragraph => child.type === 'paragraph');
+        const paragraphForDirectiveLabel = paragraphs.find(p => p.data?.directiveLabel);
+        const label = paragraphForDirectiveLabel != null
+          ? (paragraphForDirectiveLabel.children[0] as Text).value
+          : undefined;
+        // remove directive label from children
+        if (paragraphForDirectiveLabel != null) {
+          node.children.splice(node.children.indexOf(paragraphForDirectiveLabel), 1);
+        }
+
         data.hName = 'callout';
         data.hProperties = {
-          name: node.name.toLocaleLowerCase(),
+          type,
+          label: label ?? type,
         };
+
       }
     });
   };
@@ -20,4 +36,7 @@ export const remarkPlugin: Plugin = () => {
 
 export const sanitizeOption = {
   tagNames: ['callout'],
+  attributes: {
+    callout: ['type', 'label'],
+  },
 };