CVE-2023-0107 (published 2023-01-07T04:15Z)

Cross-site Scripting (XSS) - Stored in GitHub repository usememos/memos prior to 0.10.0.

CVSS v3 base score: 5.4, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-79
cwe_name: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
extended_description: 
            Cross-site scripting (XSS) vulnerabilities occur when:
            
               Untrusted data enters a web application, typically from a web request.
               The web application dynamically generates a web page that contains this untrusted data.
               During page generation, the application does not prevent the data from containing content that is executable by a web browser, such as JavaScript, HTML tags, HTML attributes, mouse events, Flash, ActiveX, etc.
               A victim visits the generated web page through a web browser, which contains malicious script that was injected using the untrusted data.
               Since the script comes from a web page that was sent by the web server, the victim's web browser executes the malicious script in the context of the web server's domain.
               This effectively violates the intention of the web browser's same-origin policy, which states that scripts in one domain should not be able to access resources or run code in a different domain.
            
            There are three main kinds of XSS:
            
               
                  Type 1: Reflected XSS (or Non-Persistent) - 
         			The server reads data directly from the HTTP request and reflects it back in the HTTP response. Reflected XSS exploits occur when an attacker causes a victim to supply dangerous content to a vulnerable web application, which is then reflected back to the victim and executed by the web browser. The most common mechanism for delivering malicious content is to include it as a parameter in a URL that is posted publicly or e-mailed directly to the victim. URLs constructed in this manner constitute the core of many phishing schemes, whereby an attacker convinces a victim to visit a URL that refers to a vulnerable site. After the site reflects the attacker's content back to the victim, the content is executed by the victim's browser.
               
                  Type 2: Stored XSS (or Persistent) - 
               The application stores dangerous data in a database, message forum, visitor log, or other trusted data store. At a later time, the dangerous data is subsequently read back into the application and included in dynamic content. From an attacker's perspective, the optimal place to inject malicious content is in an area that is displayed to either many users or particularly interesting users. Interesting users typically have elevated privileges in the application or interact with sensitive data that is valuable to the attacker. If one of these users executes malicious content, the attacker may be able to perform privileged operations on behalf of the user or gain access to sensitive data belonging to the user. For example, the attacker might inject XSS into a log message, which might not be handled properly when an administrator views the logs.
            
               
                  Type 0: DOM-Based XSS - 
               In DOM-based XSS, the client performs the injection of XSS into the page; in the other types, the server performs the injection. DOM-based XSS generally involves server-controlled, trusted script that is sent to the client, such as Javascript that performs sanity checks on a form before the user submits it. If the server-supplied script processes user-supplied data and then injects it back into the web page (such as with dynamic HTML), then DOM-based XSS is possible.
            
            
            Once the malicious script is injected, the attacker can perform a variety of malicious activities. The attacker could transfer private information, such as cookies that may include session information, from the victim's machine to the attacker. The attacker could send malicious requests to a web site on behalf of the victim, which could be especially dangerous to the site if the victim has administrator privileges to manage that site. Phishing attacks could be used to emulate trusted web sites and trick the victim into entering a password, allowing the attacker to compromise the victim's account on that web site. Finally, the script could exploit a vulnerability in the web browser itself possibly taking over the victim's machine, sometimes referred to as "drive-by hacking."
            In many cases, the attack can be launched without the victim even being aware of it. Even with careful users, attackers frequently use a variety of methods to encode the malicious portion of the attack, such as URL encoding or Unicode, so the request looks less suspicious.
         
         

Fix information:
repo_url: https://github.com/usememos/memos
commit_details: repo_url: https://github.com/usememos/memos
author: boojack
author_date: 2023-01-07 00:13:49+08:00
msg: refactor: return jsx element instead of string in marked (#910)

* refactor: return jsx element instead of string in marked

* chore: update
repository: repo_url: https://github.com/usememos/memos
repo_name: usememos/memos
description: An open source, lightweight note-taking service. Easily capture and share your great thoughts.
repo_language: Go
owner: usememos
file_changes: filename: CreateTagDialog.tsx
change_type: ModificationType.MODIFY
diff: @@ -2,6 +2,7 @@ import { TextField } from "@mui/joy";
 import React, { useEffect, useState } from "react";
 import { useTagStore } from "../store/module";
 import { getTagSuggestionList } from "../helpers/api";
+import { matcher } from "../labs/marked/matcher";
 import Tag from "../labs/marked/parser/Tag";
 import Icon from "./Icon";
 import toastHelper from "./Toast";
@@ -10,7 +11,7 @@ import { generateDialog } from "./Dialog";
 type Props = DialogProps;
 
 const validateTagName = (tagName: string): boolean => {
-  const matchResult = Tag.matcher(`#${tagName}`);
+  const matchResult = matcher(`#${tagName}`, Tag.regexp);
   if (!matchResult || matchResult[1] !== tagName) {
     return false;
   }

programming_language: TypeScript
filename: Memo.tsx
change_type: ModificationType.MODIFY
diff: @@ -15,7 +15,6 @@ import "../less/memo.less";
 
 interface Props {
   memo: Memo;
-  highlightWord?: string;
 }
 
 export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => {
@@ -27,7 +26,7 @@ export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => {
 };
 
 const Memo: React.FC<Props> = (props: Props) => {
-  const { memo, highlightWord } = props;
+  const { memo } = props;
   const { t, i18n } = useTranslation();
   const navigate = useNavigate();
   const editorStore = useEditorStore();
@@ -143,9 +142,6 @@ const Memo: React.FC<Props> = (props: Props) => {
       if (imgUrl) {
         showPreviewImageDialog([imgUrl], 0);
       }
-    } else if (targetEl.tagName === "BUTTON" && targetEl.className === "codeblock-copy-btn") {
-      copy(targetEl.parentElement?.children[1].textContent ?? "");
-      toastHelper.success(t("message.succeed-copy-code"));
     }
   };
 
@@ -228,7 +224,6 @@ const Memo: React.FC<Props> = (props: Props) => {
       </div>
       <MemoContent
         content={memo.content}
-        highlightWord={highlightWord}
         onMemoContentClick={handleMemoContentClick}
         onMemoContentDoubleClick={handleMemoContentDoubleClick}
       />

programming_language: TypeScript
filename: MemoContent.tsx
change_type: ModificationType.MODIFY
diff: @@ -2,7 +2,6 @@ import { useEffect, useMemo, useRef, useState } from "react";
 import { useTranslation } from "react-i18next";
 import { useUserStore } from "../store/module";
 import { marked } from "../labs/marked";
-import { highlightWithWord } from "../labs/highlighter";
 import Icon from "./Icon";
 import "../less/memo-content.less";
 
@@ -12,7 +11,6 @@ export interface DisplayConfig {
 
 interface Props {
   content: string;
-  highlightWord?: string;
   className?: string;
   displayConfig?: Partial<DisplayConfig>;
   onMemoContentClick?: (e: React.MouseEvent) => void;
@@ -30,14 +28,14 @@ const defaultDisplayConfig: DisplayConfig = {
 };
 
 const MemoContent: React.FC<Props> = (props: Props) => {
-  const { className, content, highlightWord, onMemoContentClick, onMemoContentDoubleClick } = props;
+  const { className, content, onMemoContentClick, onMemoContentDoubleClick } = props;
+  const { t } = useTranslation();
+  const userStore = useUserStore();
+  const user = userStore.state.user;
   const foldedContent = useMemo(() => {
     const firstHorizontalRuleIndex = content.search(/^---$|^\*\*\*$|^___$/m);
     return firstHorizontalRuleIndex !== -1 ? content.slice(0, firstHorizontalRuleIndex) : content;
   }, [content]);
-  const { t } = useTranslation();
-  const userStore = useUserStore();
-  const user = userStore.state.user;
 
   const [state, setState] = useState<State>({
     expandButtonStatus: -1,
@@ -97,10 +95,9 @@ const MemoContent: React.FC<Props> = (props: Props) => {
         className={`memo-content-text ${state.expandButtonStatus === 0 ? "expanded" : ""}`}
         onClick={handleMemoContentClick}
         onDoubleClick={handleMemoContentDoubleClick}
-        dangerouslySetInnerHTML={{
-          __html: highlightWithWord(marked(state.expandButtonStatus === 0 ? foldedContent : content), highlightWord),
-        }}
-      ></div>
+      >
+        {marked(state.expandButtonStatus === 0 ? foldedContent : content)}
+      </div>
       {state.expandButtonStatus !== -1 && (
         <div className="expand-btn-container">
           <span className={`btn ${state.expandButtonStatus === 0 ? "expand-btn" : "fold-btn"}`} onClick={handleExpandBtnClick}>

programming_language: TypeScript
filename: MemoList.tsx
change_type: ModificationType.MODIFY
diff: @@ -19,7 +19,6 @@ const MemoList = () => {
   const memoDisplayTsOption = userStore.state.user?.setting.memoDisplayTsOption;
   const { memos, isFetching } = memoStore.state;
   const [isComplete, setIsComplete] = useState<boolean>(false);
-  const [highlightWord, setHighlightWord] = useState<string | undefined>("");
 
   const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId, visibility } = query ?? {};
   const shortcut = shortcutId ? shortcutStore.getShortcutById(shortcutId) : null;
@@ -107,7 +106,6 @@ const MemoList = () => {
     if (pageWrapper) {
       pageWrapper.scrollTo(0, 0);
     }
-    setHighlightWord(query?.text);
   }, [query]);
 
   useEffect(() => {
@@ -136,7 +134,7 @@ const MemoList = () => {
   return (
     <div className="memo-list-container">
       {sortedMemos.map((memo) => (
-        <Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} highlightWord={highlightWord} />
+        <Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} />
       ))}
       {isFetching ? (
         <div className="status-text-container fetching-tip">

programming_language: TypeScript
filename: index.ts
change_type: ModificationType.DELETE
diff: @@ -1,24 +0,0 @@
-import { escape } from "lodash";
-
-const walkthroughNodeWithKeyword = (node: HTMLElement, keyword: string) => {
-  if (node.nodeType === 3) {
-    const span = document.createElement("span");
-    span.innerHTML = node.nodeValue?.replace(new RegExp(keyword, "g"), `<mark>${keyword}</mark>`) ?? "";
-    node.parentNode?.insertBefore(span, node);
-    node.parentNode?.removeChild(node);
-  }
-  for (const child of Array.from(node.childNodes)) {
-    walkthroughNodeWithKeyword(<HTMLElement>child, keyword);
-  }
-  return node.innerHTML;
-};
-
-export const highlightWithWord = (html: string, keyword?: string): string => {
-  if (!keyword) {
-    return html;
-  }
-  keyword = escape(keyword);
-  const wrap = document.createElement("div");
-  wrap.innerHTML = escape(html);
-  return walkthroughNodeWithKeyword(wrap, keyword);
-};

programming_language: unknown
filename: index.tsx
change_type: ModificationType.RENAME
diff: @@ -1,8 +1,13 @@
+import { matcher } from "./matcher";
 import { blockElementParserList, inlineElementParserList } from "./parser";
 
-export const marked = (markdownStr: string, blockParsers = blockElementParserList, inlineParsers = inlineElementParserList): string => {
+export const marked = (
+  markdownStr: string,
+  blockParsers = blockElementParserList,
+  inlineParsers = inlineElementParserList
+): string | JSX.Element => {
   for (const parser of blockParsers) {
-    const matchResult = parser.matcher(markdownStr);
+    const matchResult = matcher(markdownStr, parser.regexp);
     if (!matchResult) {
       continue;
     }
@@ -10,12 +15,22 @@ export const marked = (markdownStr: string, blockParsers = blockElementParserLis
     const retainContent = markdownStr.slice(matchedStr.length);
 
     if (parser.name === "br") {
-      return parser.renderer(matchedStr) + marked(retainContent, blockParsers, inlineParsers);
+      return (
+        <>
+          {parser.renderer(matchedStr)}
+          {marked(retainContent, blockParsers, inlineParsers)}
+        </>
+      );
     } else {
       if (retainContent === "") {
         return parser.renderer(matchedStr);
       } else if (retainContent.startsWith("\n")) {
-        return parser.renderer(matchedStr) + marked(retainContent.slice(1), blockParsers, inlineParsers);
+        return (
+          <>
+            {parser.renderer(matchedStr)}
+            {marked(retainContent.slice(1), blockParsers, inlineParsers)}
+          </>
+        );
       }
     }
   }
@@ -24,7 +39,7 @@ export const marked = (markdownStr: string, blockParsers = blockElementParserLis
   let matchedIndex = -1;
 
   for (const parser of inlineParsers) {
-    const matchResult = parser.matcher(markdownStr);
+    const matchResult = matcher(markdownStr, parser.regexp);
     if (!matchResult) {
       continue;
     }
@@ -41,17 +56,23 @@ export const marked = (markdownStr: string, blockParsers = blockElementParserLis
   }
 
   if (matchedInlineParser) {
-    const matchResult = matchedInlineParser.matcher(markdownStr);
+    const matchResult = matcher(markdownStr, matchedInlineParser.regexp);
     if (matchResult) {
       const matchedStr = matchResult[0];
       const matchedLength = matchedStr.length;
       const prefixStr = markdownStr.slice(0, matchedIndex);
       const suffixStr = markdownStr.slice(matchedIndex + matchedLength);
-      return marked(prefixStr, [], inlineParsers) + matchedInlineParser.renderer(matchedStr) + marked(suffixStr, [], inlineParsers);
+      return (
+        <>
+          {marked(prefixStr, [], inlineParsers)}
+          {matchedInlineParser.renderer(matchedStr)}
+          {marked(suffixStr, [], inlineParsers)}
+        </>
+      );
     }
   }
 
-  return markdownStr;
+  return <>{markdownStr}</>;
 };
 
 interface MatchedNode {
@@ -64,7 +85,7 @@ export const getMatchedNodes = (markdownStr: string): MatchedNode[] => {
 
   const walkthough = (markdownStr: string, blockParsers = blockElementParserList, inlineParsers = inlineElementParserList): string => {
     for (const parser of blockParsers) {
-      const matchResult = parser.matcher(markdownStr);
+      const matchResult = matcher(markdownStr, parser.regexp);
       if (!matchResult) {
         continue;
       }
@@ -79,6 +100,7 @@ export const getMatchedNodes = (markdownStr: string): MatchedNode[] => {
         return walkthough(retainContent, blockParsers, inlineParsers);
       } else {
         if (retainContent.startsWith("\n")) {
+          walkthough(matchedStr, [], inlineParsers);
           return walkthough(retainContent.slice(1), blockParsers, inlineParsers);
         }
       }
@@ -88,7 +110,7 @@ export const getMatchedNodes = (markdownStr: string): MatchedNode[] => {
     let matchedIndex = -1;
 
     for (const parser of inlineParsers) {
-      const matchResult = parser.matcher(markdownStr);
+      const matchResult = matcher(markdownStr, parser.regexp);
       if (!matchResult) {
         continue;
       }
@@ -105,7 +127,7 @@ export const getMatchedNodes = (markdownStr: string): MatchedNode[] => {
     }
 
     if (matchedInlineParser) {
-      const matchResult = matchedInlineParser.matcher(markdownStr);
+      const matchResult = matcher(markdownStr, matchedInlineParser.regexp);
       if (matchResult) {
         const matchedStr = matchResult[0];
         const matchedLength = matchedStr.length;

programming_language: Rust
filename: marked.test.ts
change_type: ModificationType.DELETE
diff: @@ -1,174 +0,0 @@
-/* eslint-disable no-irregular-whitespace */
-import { describe, expect, test } from "@jest/globals";
-import { unescape } from "lodash-es";
-import { marked } from ".";
-
-describe("test marked parser", () => {
-  test("horizontal rule", () => {
-    const tests = [
-      {
-        markdown: `---
-This is some text after the horizontal rule.
-___
-This is some text after the horizontal rule.
-***
-This is some text after the horizontal rule.`,
-        want: `<hr><p>This is some text after the horizontal rule.</p><hr><p>This is some text after the horizontal rule.</p><hr><p>This is some text after the horizontal rule.</p>`,
-      },
-    ];
-    for (const t of tests) {
-      expect(unescape(marked(t.markdown))).toBe(t.want);
-    }
-  });
-  test("parse code block", () => {
-    const tests = [
-      {
-        markdown: `\`\`\`
-hello world!
-\`\`\``,
-        want: `<pre><button class="codeblock-copy-btn">copy</button><code class="language-plaintext">hello world!
-</code></pre>`,
-      },
-      {
-        markdown: `test code block
-
-\`\`\`js
-console.log("hello world!")
-\`\`\``,
-        want: `<p>test code block</p><br><pre><button class="codeblock-copy-btn">copy</button><code class="language-js"><span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">"hello world!"</span>)
-</code></pre>`,
-      },
-    ];
-
-    for (const t of tests) {
-      expect(unescape(marked(t.markdown))).toBe(t.want);
-    }
-  });
-  test("parse todo list block", () => {
-    const tests = [
-      {
-        markdown: `My task:
-- [ ] finish my homework
-- [x] yahaha`,
-        want: `<p>My task:</p><p class='li-container'><span class='todo-block todo' data-value='TODO'></span><span>finish my homework</span></p><p class='li-container'><span class='todo-block done' data-value='DONE'>✓</span><span>yahaha</span></p>`,
-      },
-    ];
-
-    for (const t of tests) {
-      expect(unescape(marked(t.markdown))).toBe(t.want);
-    }
-  });
-  test("parse list block", () => {
-    const tests = [
-      {
-        markdown: `This is a list
-* list 123
-1. 123123`,
-        want: `<p>This is a list</p><p class='li-container'><span class='ul-block'>•</span><span>list 123</span></p><p class='li-container'><span class='ol-block'>1.</span><span>123123</span></p>`,
-      },
-    ];
-
-    for (const t of tests) {
-      expect(unescape(marked(t.markdown))).toBe(t.want);
-    }
-  });
-  test("parse inline element", () => {
-    const tests = [
-      {
-        markdown: `Link: [baidu](https://baidu.com#1231)`,
-        want: `<p>Link: <a class='link' target='_blank' rel='noreferrer' href='https://baidu.com#1231'>baidu</a></p>`,
-      },
-    ];
-
-    for (const t of tests) {
-      expect(unescape(marked(t.markdown))).toBe(t.want);
-    }
-  });
-  test("parse inline code within inline element", () => {
-    const tests = [
-      {
-        markdown: `Link: [\`baidu\`](https://baidu.com)`,
-        want: `<p>Link: <a class='link' target='_blank' rel='noreferrer' href='https://baidu.com'><code>baidu</code></a></p>`,
-      },
-    ];
-
-    for (const t of tests) {
-      expect(unescape(marked(t.markdown))).toBe(t.want);
-    }
-  });
-  test("parse plain link", () => {
-    const tests = [
-      {
-        markdown: `Link:https://baidu.com#1231`,
-        want: `<p>Link:<a class='link' target='_blank' rel='noreferrer' href='https://baidu.com#1231'>https://baidu.com#1231</a></p>`,
-      },
-    ];
-
-    for (const t of tests) {
-      expect(unescape(marked(t.markdown))).toBe(t.want);
-    }
-  });
-  test("parse inline code", () => {
-    const tests = [
-      {
-        markdown: `Code: \`console.log("Hello world!")\``,
-        want: `<p>Code: <code>console.log("Hello world!")</code></p>`,
-      },
-    ];
-
-    for (const t of tests) {
-      expect(unescape(marked(t.markdown))).toBe(t.want);
-    }
-  });
-  test("parse bold and em text", () => {
-    const tests = [
-      {
-        markdown: `Important: **Minecraft**`,
-        want: `<p>Important: <strong>Minecraft</strong></p>`,
-      },
-      {
-        markdown: `Em: *Minecraft*`,
-        want: `<p>Em: <em>Minecraft</em></p>`,
-      },
-      {
-        markdown: `Important: ***Minecraft/123***`,
-        want: `<p>Important: <strong><em>Minecraft/123</em></strong></p>`,
-      },
-      {
-        markdown: `Important: ***[baidu](https://baidu.com)***`,
-        want: `<p>Important: <strong><em><a class='link' target='_blank' rel='noreferrer' href='https://baidu.com'>baidu</a></em></strong></p>`,
-      },
-    ];
-
-    for (const t of tests) {
-      expect(unescape(marked(t.markdown))).toBe(t.want);
-    }
-  });
-  test("parse full width space", () => {
-    const tests = [
-      {
-        markdown: `　　line1
-　　line2`,
-        want: `<p>　　line1</p><p>　　line2</p>`,
-      },
-    ];
-    for (const t of tests) {
-      expect(unescape(marked(t.markdown))).toBe(t.want);
-    }
-  });
-  test("parse heading", () => {
-    const tests = [
-      {
-        markdown: `# 123 `,
-        want: `<h1>123 </h1>`,
-      },
-      {
-        markdown: `## 123 `,
-        want: `<h2>123 </h2>`,
-      },
-    ];
-    for (const t of tests) {
-      expect(unescape(marked(t.markdown))).toBe(t.want);
-    }
-  });
-});

programming_language: unknown
filename: matcher.ts
change_type: ModificationType.ADD
diff: @@ -0,0 +1,4 @@
+export const matcher = (rawStr: string, regexp: RegExp) => {
+  const matchResult = rawStr.match(regexp);
+  return matchResult;
+};

programming_language: TypeScript
filename: Blockquote.ts
change_type: ModificationType.DELETE
diff: @@ -1,24 +0,0 @@
-import { escape } from "lodash";
-
-export const BLOCKQUOTE_REG = /^> ([^\n]+)/;
-
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(BLOCKQUOTE_REG);
-  return matchResult;
-};
-
-const renderer = (rawStr: string): string => {
-  const matchResult = matcher(rawStr);
-  if (!matchResult) {
-    return rawStr;
-  }
-
-  return `<blockquote>${escape(matchResult[1])}</blockquote>`;
-};
-
-export default {
-  name: "blockquote",
-  regex: BLOCKQUOTE_REG,
-  matcher,
-  renderer,
-};

programming_language: unknown
filename: Blockquote.tsx
change_type: ModificationType.ADD
diff: @@ -0,0 +1,19 @@
+import { escape } from "lodash";
+import { matcher } from "../matcher";
+
+export const BLOCKQUOTE_REG = /^> ([^\n]+)/;
+
+const renderer = (rawStr: string) => {
+  const matchResult = matcher(rawStr, BLOCKQUOTE_REG);
+  if (!matchResult) {
+    return <>{rawStr}</>;
+  }
+
+  return <blockquote>{escape(matchResult[1])}</blockquote>;
+};
+
+export default {
+  name: "blockquote",
+  regexp: BLOCKQUOTE_REG,
+  renderer,
+};

programming_language: TypeScript
filename: Bold.ts
change_type: ModificationType.DELETE
diff: @@ -1,27 +0,0 @@
-import { marked } from "..";
-import Link from "./Link";
-import PlainText from "./PlainText";
-
-export const BOLD_REG = /\*\*(.+?)\*\*/;
-
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(BOLD_REG);
-  return matchResult;
-};
-
-const renderer = (rawStr: string): string => {
-  const matchResult = matcher(rawStr);
-  if (!matchResult) {
-    return rawStr;
-  }
-
-  const parsedContent = marked(matchResult[1], [], [Link, PlainText]);
-  return `<strong>${parsedContent}</strong>`;
-};
-
-export default {
-  name: "bold",
-  regex: BOLD_REG,
-  matcher,
-  renderer,
-};

programming_language: unknown
filename: Bold.tsx
change_type: ModificationType.ADD
diff: @@ -0,0 +1,22 @@
+import { marked } from "..";
+import { matcher } from "../matcher";
+import Link from "./Link";
+import PlainText from "./PlainText";
+
+export const BOLD_REG = /\*\*(.+?)\*\*/;
+
+const renderer = (rawStr: string) => {
+  const matchResult = matcher(rawStr, BOLD_REG);
+  if (!matchResult) {
+    return <>{rawStr}</>;
+  }
+
+  const parsedContent = marked(matchResult[1], [], [Link, PlainText]);
+  return <strong>{parsedContent}</strong>;
+};
+
+export default {
+  name: "bold",
+  regexp: BOLD_REG,
+  renderer,
+};

programming_language: TypeScript
filename: BoldEmphasis.tsx
change_type: ModificationType.RENAME
diff: @@ -1,27 +1,26 @@
 import { marked } from "..";
+import { matcher } from "../matcher";
 import Link from "./Link";
 import PlainText from "./PlainText";
 
 export const BOLD_EMPHASIS_REG = /\*\*\*(.+?)\*\*\*/;
 
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(BOLD_EMPHASIS_REG);
-  return matchResult;
-};
-
-const renderer = (rawStr: string): string => {
-  const matchResult = matcher(rawStr);
+const renderer = (rawStr: string) => {
+  const matchResult = matcher(rawStr, BOLD_EMPHASIS_REG);
   if (!matchResult) {
     return rawStr;
   }
 
   const parsedContent = marked(matchResult[1], [], [Link, PlainText]);
-  return `<strong><em>${parsedContent}</em></strong>`;
+  return (
+    <strong>
+      <em>${parsedContent}</em>
+    </strong>
+  );
 };
 
 export default {
   name: "bold emphasis",
-  regex: BOLD_EMPHASIS_REG,
-  matcher,
+  regexp: BOLD_EMPHASIS_REG,
   renderer,
 };

programming_language: TypeScript
filename: Br.ts
change_type: ModificationType.DELETE
diff: @@ -1,17 +0,0 @@
-export const BR_REG = /^(\n+)/;
-
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(BR_REG);
-  return matchResult;
-};
-
-const renderer = (rawStr: string): string => {
-  return rawStr.replaceAll("\n", "<br>");
-};
-
-export default {
-  name: "br",
-  regex: BR_REG,
-  matcher,
-  renderer,
-};

programming_language: unknown
filename: Br.tsx
change_type: ModificationType.ADD
diff: @@ -0,0 +1,16 @@
+export const BR_REG = /^(\n+)/;
+
+const renderer = (rawStr: string) => {
+  const length = rawStr.split("\n").length - 1;
+  const brList = [];
+  for (let i = 0; i < length; i++) {
+    brList.push(<br />);
+  }
+  return <>{...brList}</>;
+};
+
+export default {
+  name: "br",
+  regexp: BR_REG,
+  renderer,
+};

programming_language: TypeScript
filename: CodeBlock.ts
change_type: ModificationType.DELETE
diff: @@ -1,37 +0,0 @@
-import { escape } from "lodash-es";
-import hljs from "highlight.js";
-
-export const CODE_BLOCK_REG = /^```(\S*?)\s([\s\S]*?)```/;
-
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(CODE_BLOCK_REG);
-  return matchResult;
-};
-
-const renderer = (rawStr: string): string => {
-  const matchResult = matcher(rawStr);
-  if (!matchResult) {
-    return rawStr;
-  }
-
-  const language = escape(matchResult[1]) || "plaintext";
-  let highlightedCode = hljs.highlightAuto(matchResult[2]).value;
-
-  try {
-    const temp = hljs.highlight(matchResult[2], {
-      language,
-    }).value;
-    highlightedCode = temp;
-  } catch (error) {
-    // do nth
-  }
-
-  return `<pre><button class="codeblock-copy-btn">copy</button><code class="language-${language}">${highlightedCode}</code></pre>`;
-};
-
-export default {
-  name: "code block",
-  regex: CODE_BLOCK_REG,
-  matcher,
-  renderer,
-};

programming_language: unknown
filename: CodeBlock.tsx
change_type: ModificationType.ADD
diff: @@ -0,0 +1,51 @@
+import copy from "copy-to-clipboard";
+import { escape } from "lodash-es";
+import hljs from "highlight.js";
+import { useTranslation } from "react-i18next";
+import { matcher } from "../matcher";
+import toastHelper from "../../../components/Toast";
+
+export const CODE_BLOCK_REG = /^```(\S*?)\s([\s\S]*?)```/;
+
+const renderer = (rawStr: string) => {
+  const { t } = useTranslation();
+  const matchResult = matcher(rawStr, CODE_BLOCK_REG);
+  if (!matchResult) {
+    return <>{rawStr}</>;
+  }
+
+  const language = escape(matchResult[1]) || "plaintext";
+  let highlightedCode = hljs.highlightAuto(matchResult[2]).value;
+
+  try {
+    const temp = hljs.highlight(matchResult[2], {
+      language,
+    }).value;
+    highlightedCode = temp;
+  } catch (error) {
+    // do nth
+  }
+
+  const handleCopyButtonClick = () => {
+    copy(matchResult[2]);
+    toastHelper.success(t("message.succeed-copy-code"));
+  };
+
+  return (
+    <pre>
+      <button
+        className="text-xs font-mono italic absolute top-0 right-0 px-2 leading-6 border btn-text rounded opacity-60"
+        onClick={handleCopyButtonClick}
+      >
+        copy
+      </button>
+      <code className={`language-${language}`}>{highlightedCode}</code>
+    </pre>
+  );
+};
+
+export default {
+  name: "code block",
+  regexp: CODE_BLOCK_REG,
+  renderer,
+};

programming_language: TypeScript
filename: DoneList.ts
change_type: ModificationType.DELETE
diff: @@ -1,26 +0,0 @@
-import { inlineElementParserList } from ".";
-import { marked } from "..";
-
-export const DONE_LIST_REG = /^- \[[xX]\] ([^\n]+)/;
-
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(DONE_LIST_REG);
-  return matchResult;
-};
-
-const renderer = (rawStr: string): string => {
-  const matchResult = matcher(rawStr);
-  if (!matchResult) {
-    return rawStr;
-  }
-
-  const parsedContent = marked(matchResult[1], [], inlineElementParserList);
-  return `<p class='li-container'><span class='todo-block done' data-value='DONE'>✓</span><span>${parsedContent}</span></p>`;
-};
-
-export default {
-  name: "done list",
-  regex: DONE_LIST_REG,
-  matcher,
-  renderer,
-};

programming_language: unknown
filename: DoneList.tsx
change_type: ModificationType.ADD
diff: @@ -0,0 +1,28 @@
+import { inlineElementParserList } from ".";
+import { marked } from "..";
+import { matcher } from "../matcher";
+
+export const DONE_LIST_REG = /^- \[[xX]\] ([^\n]+)/;
+
+const renderer = (rawStr: string) => {
+  const matchResult = matcher(rawStr, DONE_LIST_REG);
+  if (!matchResult) {
+    return rawStr;
+  }
+
+  const parsedContent = marked(matchResult[1], [], inlineElementParserList);
+  return (
+    <p className="li-container">
+      <span className="todo-block done" data-value="DONE">
+        ✓
+      </span>
+      <span>{parsedContent}</span>
+    </p>
+  );
+};
+
+export default {
+  name: "done list",
+  regexp: DONE_LIST_REG,
+  renderer,
+};

programming_language: TypeScript
filename: Emphasis.tsx
change_type: ModificationType.RENAME
diff: @@ -1,27 +1,22 @@
 import { marked } from "..";
+import { matcher } from "../matcher";
 import Link from "./Link";
 import PlainText from "./PlainText";
 
 export const EMPHASIS_REG = /\*(.+?)\*/;
 
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(EMPHASIS_REG);
-  return matchResult;
-};
-
-const renderer = (rawStr: string): string => {
-  const matchResult = matcher(rawStr);
+const renderer = (rawStr: string) => {
+  const matchResult = matcher(rawStr, EMPHASIS_REG);
   if (!matchResult) {
     return rawStr;
   }
 
   const parsedContent = marked(matchResult[1], [], [Link, PlainText]);
-  return `<em>${parsedContent}</em>`;
+  return <em>{parsedContent}</em>;
 };
 
 export default {
   name: "emphasis",
-  regex: EMPHASIS_REG,
-  matcher,
+  regexp: EMPHASIS_REG,
   renderer,
 };

programming_language: TypeScript
filename: Heading.ts
change_type: ModificationType.DELETE
diff: @@ -1,25 +0,0 @@
-import { escape } from "lodash";
-
-export const HEADING_REG = /^(#+) ([^\n]+)/;
-
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(HEADING_REG);
-  return matchResult;
-};
-
-const renderer = (rawStr: string): string => {
-  const matchResult = matcher(rawStr);
-  if (!matchResult) {
-    return rawStr;
-  }
-
-  const level = matchResult[1].length;
-  return `<h${level}>${escape(matchResult[2])}</h${level}>`;
-};
-
-export default {
-  name: "heading",
-  regex: HEADING_REG,
-  matcher,
-  renderer,
-};

programming_language: unknown
filename: Heading.tsx
change_type: ModificationType.ADD
diff: @@ -0,0 +1,29 @@
+import { escape } from "lodash";
+import { matcher } from "../matcher";
+
+export const HEADING_REG = /^(#+) ([^\n]+)/;
+
+const renderer = (rawStr: string) => {
+  const matchResult = matcher(rawStr, HEADING_REG);
+  if (!matchResult) {
+    return rawStr;
+  }
+
+  const level = matchResult[1].length;
+  if (level === 1) {
+    return <h1>{escape(matchResult[2])}</h1>;
+  } else if (level === 2) {
+    return <h2>{escape(matchResult[2])}</h2>;
+  } else if (level === 3) {
+    return <h3>{escape(matchResult[2])}</h3>;
+  } else if (level === 4) {
+    return <h4>{escape(matchResult[2])}</h4>;
+  }
+  return <h5>{escape(matchResult[2])}</h5>;
+};
+
+export default {
+  name: "heading",
+  regexp: HEADING_REG,
+  renderer,
+};

programming_language: TypeScript
filename: HorizontalRules.ts
change_type: ModificationType.DELETE
diff: @@ -1,18 +0,0 @@
-export const HORIZONTAL_RULES_REG = /^_{3}|^-{3}|^\*{3}/;
-
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(HORIZONTAL_RULES_REG);
-  return matchResult;
-};
-
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const renderer = (rawStr: string): string => {
-  return `<hr>`;
-};
-
-export default {
-  name: "horizontal rules",
-  regex: HORIZONTAL_RULES_REG,
-  matcher,
-  renderer,
-};

programming_language: unknown
filename: HorizontalRules.tsx
change_type: ModificationType.ADD
diff: @@ -0,0 +1,12 @@
+export const HORIZONTAL_RULES_REG = /^_{3}|^-{3}|^\*{3}/;
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const renderer = (rawStr: string) => {
+  return <hr />;
+};
+
+export default {
+  name: "horizontal rules",
+  regexp: HORIZONTAL_RULES_REG,
+  renderer,
+};

programming_language: TypeScript
filename: Image.tsx
change_type: ModificationType.RENAME
diff: @@ -1,26 +1,21 @@
 import { escape } from "lodash-es";
 import { absolutifyLink } from "../../../helpers/utils";
+import { matcher } from "../matcher";
 
 export const IMAGE_REG = /!\[.*?\]\((.+?)\)/;
 
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(IMAGE_REG);
-  return matchResult;
-};
-
-const renderer = (rawStr: string): string => {
-  const matchResult = matcher(rawStr);
+const renderer = (rawStr: string) => {
+  const matchResult = matcher(rawStr, IMAGE_REG);
   if (!matchResult) {
     return rawStr;
   }
 
   const imageUrl = absolutifyLink(escape(matchResult[1]));
-  return `<img class='img' src='${imageUrl}' />`;
+  return <img className="img" src={imageUrl} />;
 };
 
 export default {
   name: "image",
-  regex: IMAGE_REG,
-  matcher,
+  regexp: IMAGE_REG,
   renderer,
 };

programming_language: TypeScript
filename: InlineCode.ts
change_type: ModificationType.DELETE
diff: @@ -1,24 +0,0 @@
-import { escape } from "lodash-es";
-
-export const INLINE_CODE_REG = /`(.+?)`/;
-
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(INLINE_CODE_REG);
-  return matchResult;
-};
-
-const renderer = (rawStr: string): string => {
-  const matchResult = matcher(rawStr);
-  if (!matchResult) {
-    return rawStr;
-  }
-
-  return `<code>${escape(matchResult[1])}</code>`;
-};
-
-export default {
-  name: "inline code",
-  regex: INLINE_CODE_REG,
-  matcher,
-  renderer,
-};

programming_language: unknown
filename: InlineCode.tsx
change_type: ModificationType.ADD
diff: @@ -0,0 +1,19 @@
+import { escape } from "lodash-es";
+import { matcher } from "../matcher";
+
+export const INLINE_CODE_REG = /`(.+?)`/;
+
+const renderer = (rawStr: string) => {
+  const matchResult = matcher(rawStr, INLINE_CODE_REG);
+  if (!matchResult) {
+    return rawStr;
+  }
+
+  return <code>{escape(matchResult[1])}</code>;
+};
+
+export default {
+  name: "inline code",
+  regexp: INLINE_CODE_REG,
+  renderer,
+};

programming_language: TypeScript
filename: Link.tsx
change_type: ModificationType.RENAME
diff: @@ -5,26 +5,25 @@ import { marked } from "..";
 import InlineCode from "./InlineCode";
 import BoldEmphasis from "./BoldEmphasis";
 import PlainText from "./PlainText";
+import { matcher } from "../matcher";
 
 export const LINK_REG = /\[(.*?)\]\((.+?)\)+/;
 
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(LINK_REG);
-  return matchResult;
-};
-
-const renderer = (rawStr: string): string => {
-  const matchResult = matcher(rawStr);
+const renderer = (rawStr: string) => {
+  const matchResult = matcher(rawStr, LINK_REG);
   if (!matchResult) {
     return rawStr;
   }
   const parsedContent = marked(matchResult[1], [], [InlineCode, BoldEmphasis, Emphasis, Bold, PlainText]);
-  return `<a class='link' target='_blank' rel='noreferrer' href='${escape(matchResult[2])}'>${parsedContent}</a>`;
+  return (
+    <a className="link" target="_blank" rel="noreferrer" href={escape(matchResult[2])}>
+      {parsedContent}
+    </a>
+  );
 };
 
 export default {
   name: "link",
-  regex: LINK_REG,
-  matcher,
+  regexp: LINK_REG,
   renderer,
 };

programming_language: TypeScript
filename: OrderedList.ts
change_type: ModificationType.DELETE
diff: @@ -1,26 +0,0 @@
-import { inlineElementParserList } from ".";
-import { marked } from "..";
-
-export const ORDERED_LIST_REG = /^(\d+)\. (.+)/;
-
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(ORDERED_LIST_REG);
-  return matchResult;
-};
-
-const renderer = (rawStr: string): string => {
-  const matchResult = matcher(rawStr);
-  if (!matchResult) {
-    return rawStr;
-  }
-
-  const parsedContent = marked(matchResult[2], [], inlineElementParserList);
-  return `<p class='li-container'><span class='ol-block'>${matchResult[1]}.</span><span>${parsedContent}</span></p>`;
-};
-
-export default {
-  name: "ordered list",
-  regex: ORDERED_LIST_REG,
-  matcher,
-  renderer,
-};

programming_language: unknown
filename: OrderedList.tsx
change_type: ModificationType.ADD
diff: @@ -0,0 +1,26 @@
+import { inlineElementParserList } from ".";
+import { marked } from "..";
+import { matcher } from "../matcher";
+
+export const ORDERED_LIST_REG = /^(\d+)\. (.+)/;
+
+const renderer = (rawStr: string) => {
+  const matchResult = matcher(rawStr, ORDERED_LIST_REG);
+  if (!matchResult) {
+    return rawStr;
+  }
+
+  const parsedContent = marked(matchResult[2], [], inlineElementParserList);
+  return (
+    <p className="li-container">
+      <span className="ol-block">{matchResult[1]}.</span>
+      <span>{parsedContent}</span>
+    </p>
+  );
+};
+
+export default {
+  name: "ordered list",
+  regexp: ORDERED_LIST_REG,
+  renderer,
+};

programming_language: TypeScript
filename: Paragraph.tsx
change_type: ModificationType.RENAME
diff: @@ -3,19 +3,13 @@ import { marked } from "..";
 
 export const PARAGRAPH_REG = /^([^\n]+)/;
 
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(PARAGRAPH_REG);
-  return matchResult;
-};
-
-const renderer = (rawStr: string): string => {
+const renderer = (rawStr: string) => {
   const parsedContent = marked(rawStr, [], inlineElementParserList);
-  return `<p>${parsedContent}</p>`;
+  return <p>{parsedContent}</p>;
 };
 
 export default {
   name: "paragraph",
-  regex: PARAGRAPH_REG,
-  matcher,
+  regexp: PARAGRAPH_REG,
   renderer,
 };

programming_language: TypeScript
filename: PlainLink.ts
change_type: ModificationType.DELETE
diff: @@ -1,24 +0,0 @@
-import { escape } from "lodash-es";
-
-export const PLAIN_LINK_REG = /(https?:\/\/[^ ]+)/;
-
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(PLAIN_LINK_REG);
-  return matchResult;
-};
-
-const renderer = (rawStr: string): string => {
-  const matchResult = matcher(rawStr);
-  if (!matchResult) {
-    return rawStr;
-  }
-
-  return `<a class='link' target='_blank' rel='noreferrer' href='${escape(matchResult[1])}'>${escape(matchResult[1])}</a>`;
-};
-
-export default {
-  name: "plain link",
-  regex: PLAIN_LINK_REG,
-  matcher,
-  renderer,
-};

programming_language: unknown
filename: PlainLink.tsx
change_type: ModificationType.ADD
diff: @@ -0,0 +1,23 @@
+import { escape } from "lodash-es";
+import { matcher } from "../matcher";
+
+export const PLAIN_LINK_REG = /(https?:\/\/[^ ]+)/;
+
+const renderer = (rawStr: string) => {
+  const matchResult = matcher(rawStr, PLAIN_LINK_REG);
+  if (!matchResult) {
+    return rawStr;
+  }
+
+  return (
+    <a className="link" target="_blank" rel="noreferrer" href={escape(matchResult[1])}>
+      {escape(matchResult[1])}
+    </a>
+  );
+};
+
+export default {
+  name: "plain link",
+  regexp: PLAIN_LINK_REG,
+  renderer,
+};

programming_language: TypeScript
filename: PlainText.tsx
change_type: ModificationType.RENAME
diff: @@ -1,14 +1,10 @@
 import { escape } from "lodash-es";
+import { matcher } from "../matcher";
 
 export const PLAIN_TEXT_REG = /(.+)/;
 
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(PLAIN_TEXT_REG);
-  return matchResult;
-};
-
 const renderer = (rawStr: string): string => {
-  const matchResult = matcher(rawStr);
+  const matchResult = matcher(rawStr, PLAIN_TEXT_REG);
   if (!matchResult) {
     return rawStr;
   }
@@ -18,7 +14,6 @@ const renderer = (rawStr: string): string => {
 
 export default {
   name: "plain text",
-  regex: PLAIN_TEXT_REG,
-  matcher,
+  regexp: PLAIN_TEXT_REG,
   renderer,
 };

programming_language: TypeScript
filename: Strikethrough.ts
change_type: ModificationType.DELETE
diff: @@ -1,24 +0,0 @@
-import { escape } from "lodash";
-
-export const STRIKETHROUGH_REG = /~~(.+?)~~/;
-
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(STRIKETHROUGH_REG);
-  return matchResult;
-};
-
-const renderer = (rawStr: string): string => {
-  const matchResult = matcher(rawStr);
-  if (!matchResult) {
-    return rawStr;
-  }
-
-  return `<del>${escape(matchResult[1])}</del>`;
-};
-
-export default {
-  name: "Strikethrough",
-  regex: STRIKETHROUGH_REG,
-  matcher,
-  renderer,
-};

programming_language: unknown
filename: Strikethrough.tsx
change_type: ModificationType.ADD
diff: @@ -0,0 +1,19 @@
+import { escape } from "lodash";
+import { matcher } from "../matcher";
+
+export const STRIKETHROUGH_REG = /~~(.+?)~~/;
+
+const renderer = (rawStr: string) => {
+  const matchResult = matcher(rawStr, STRIKETHROUGH_REG);
+  if (!matchResult) {
+    return rawStr;
+  }
+
+  return <del>{escape(matchResult[1])}</del>;
+};
+
+export default {
+  name: "Strikethrough",
+  regexp: STRIKETHROUGH_REG,
+  renderer,
+};

programming_language: TypeScript
filename: Tag.ts
change_type: ModificationType.DELETE
diff: @@ -1,27 +0,0 @@
-import { escape } from "lodash-es";
-
-export const TAG_REG = /#([^\s#]+)/;
-
-export const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(TAG_REG);
-  if (matchResult) {
-    return matchResult;
-  }
-  return null;
-};
-
-const renderer = (rawStr: string): string => {
-  const matchResult = matcher(rawStr);
-  if (!matchResult) {
-    return rawStr;
-  }
-
-  return `<span class='tag-span'>#${escape(matchResult[1])}</span>`;
-};
-
-export default {
-  name: "tag",
-  regex: TAG_REG,
-  matcher,
-  renderer,
-};

programming_language: unknown
filename: Tag.tsx
change_type: ModificationType.ADD
diff: @@ -0,0 +1,19 @@
+import { escape } from "lodash-es";
+import { matcher } from "../matcher";
+
+export const TAG_REG = /#([^\s#]+)/;
+
+const renderer = (rawStr: string) => {
+  const matchResult = matcher(rawStr, TAG_REG);
+  if (!matchResult) {
+    return rawStr;
+  }
+
+  return <span className="tag-span">#{escape(matchResult[1])}</span>;
+};
+
+export default {
+  name: "tag",
+  regexp: TAG_REG,
+  renderer,
+};

programming_language: TypeScript
filename: TodoList.ts
change_type: ModificationType.DELETE
diff: @@ -1,26 +0,0 @@
-import { inlineElementParserList } from ".";
-import { marked } from "..";
-
-export const TODO_LIST_REG = /^- \[ \] ([^\n]+)/;
-
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(TODO_LIST_REG);
-  return matchResult;
-};
-
-const renderer = (rawStr: string): string => {
-  const matchResult = matcher(rawStr);
-  if (!matchResult) {
-    return rawStr;
-  }
-
-  const parsedContent = marked(matchResult[1], [], inlineElementParserList);
-  return `<p class='li-container'><span class='todo-block todo' data-value='TODO'></span><span>${parsedContent}</span></p>`;
-};
-
-export default {
-  name: "todo list",
-  regex: TODO_LIST_REG,
-  matcher,
-  renderer,
-};

programming_language: unknown
filename: TodoList.tsx
change_type: ModificationType.ADD
diff: @@ -0,0 +1,26 @@
+import { inlineElementParserList } from ".";
+import { marked } from "..";
+import { matcher } from "../matcher";
+
+export const TODO_LIST_REG = /^- \[ \] ([^\n]+)/;
+
+const renderer = (rawStr: string) => {
+  const matchResult = matcher(rawStr, TODO_LIST_REG);
+  if (!matchResult) {
+    return rawStr;
+  }
+
+  const parsedContent = marked(matchResult[1], [], inlineElementParserList);
+  return (
+    <p className="li-container">
+      <span className="todo-block todo" data-value="TODO"></span>
+      <span>{parsedContent}</span>
+    </p>
+  );
+};
+
+export default {
+  name: "todo list",
+  regexp: TODO_LIST_REG,
+  renderer,
+};

programming_language: TypeScript
filename: UnorderedList.ts
change_type: ModificationType.DELETE
diff: @@ -1,26 +0,0 @@
-import { inlineElementParserList } from ".";
-import { marked } from "..";
-
-export const UNORDERED_LIST_REG = /^[*-] ([^\n]+)/;
-
-const matcher = (rawStr: string) => {
-  const matchResult = rawStr.match(UNORDERED_LIST_REG);
-  return matchResult;
-};
-
-const renderer = (rawStr: string): string => {
-  const matchResult = matcher(rawStr);
-  if (!matchResult) {
-    return rawStr;
-  }
-
-  const parsedContent = marked(matchResult[1], [], inlineElementParserList);
-  return `<p class='li-container'><span class='ul-block'>•</span><span>${parsedContent}</span></p>`;
-};
-
-export default {
-  name: "unordered list",
-  regex: UNORDERED_LIST_REG,
-  matcher,
-  renderer,
-};

programming_language: unknown
filename: UnorderedList.tsx
change_type: ModificationType.ADD
diff: @@ -0,0 +1,26 @@
+import { inlineElementParserList } from ".";
+import { marked } from "..";
+import { matcher } from "../matcher";
+
+export const UNORDERED_LIST_REG = /^[*-] ([^\n]+)/;
+
+const renderer = (rawStr: string) => {
+  const matchResult = matcher(rawStr, UNORDERED_LIST_REG);
+  if (!matchResult) {
+    return rawStr;
+  }
+
+  const parsedContent = marked(matchResult[1], [], inlineElementParserList);
+  return (
+    <p className="li-container">
+      <span className="ul-block">•</span>
+      <span>${parsedContent}</span>
+    </p>
+  );
+};
+
+export default {
+  name: "unordered list",
+  regexp: UNORDERED_LIST_REG,
+  renderer,
+};

programming_language: TypeScript