From c1734be59d7c1390880e16dff6505d55085fb541 Mon Sep 17 00:00:00 2001
From: balibabu <cike8899@users.noreply.github.com>
Date: Mon, 18 Mar 2024 18:39:00 +0800
Subject: [PATCH] feat: add programming language highlighting for markdown
 syntax (#129)

---
 web/package-lock.json                       | 185 ++++++++++++++++++++
 web/package.json                            |   2 +
 web/src/pages/chat/chat-container/index.tsx |  26 ++-
 3 files changed, 211 insertions(+), 2 deletions(-)

diff --git a/web/package-lock.json b/web/package-lock.json
index 4c91d5f..b0ba350 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -25,6 +25,7 @@
         "react-markdown": "^9.0.1",
         "react-pdf-highlighter": "^6.1.0",
         "react-string-replace": "^1.1.1",
+        "react-syntax-highlighter": "^15.5.0",
         "remark-gfm": "^4.0.0",
         "umi": "^4.0.90",
         "umi-request": "^1.4.0",
@@ -36,6 +37,7 @@
         "@types/lodash": "^4.14.202",
         "@types/react": "^18.0.33",
         "@types/react-dom": "^18.0.11",
+        "@types/react-syntax-highlighter": "^15.5.11",
         "@types/uuid": "^9.0.8",
         "@umijs/lint": "^4.1.1",
         "@umijs/plugins": "^4.1.0",
@@ -2949,6 +2951,15 @@
       "dev": true,
       "peer": true
     },
+    "node_modules/@types/react-syntax-highlighter": {
+      "version": "15.5.11",
+      "resolved": "https://registry.npmmirror.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.11.tgz",
+      "integrity": "sha512-ZqIJl+Pg8kD+47kxUjvrlElrraSUrYa4h0dauY/U/FTUuprSCqvUj+9PNQNQzVc6AJgIWUUxn87/gqsMHNbRjw==",
+      "dev": true,
+      "dependencies": {
+        "@types/react": "*"
+      }
+    },
     "node_modules/@types/scheduler": {
       "version": "0.16.8",
       "resolved": "https://registry.npmmirror.com/@types/scheduler/-/scheduler-0.16.8.tgz",
@@ -8394,6 +8405,14 @@
         "reusify": "^1.0.4"
       }
     },
+    "node_modules/fault": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/fault/-/fault-1.0.4.tgz",
+      "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
+      "dependencies": {
+        "format": "^0.2.0"
+      }
+    },
     "node_modules/fb-watchman": {
       "version": "2.0.2",
       "resolved": "https://registry.npmmirror.com/fb-watchman/-/fb-watchman-2.0.2.tgz",
@@ -8669,6 +8688,14 @@
         "node": ">= 6"
       }
     },
+    "node_modules/format": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmmirror.com/format/-/format-0.2.2.tgz",
+      "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
+      "engines": {
+        "node": ">=0.4.x"
+      }
+    },
     "node_modules/formdata-polyfill": {
       "version": "4.0.10",
       "resolved": "https://registry.npmmirror.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@@ -9182,6 +9209,11 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/hast-util-parse-selector": {
+      "version": "2.2.5",
+      "resolved": "https://registry.npmmirror.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
+      "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ=="
+    },
     "node_modules/hast-util-to-jsx-runtime": {
       "version": "2.3.0",
       "resolved": "https://registry.npmmirror.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz",
@@ -9212,6 +9244,49 @@
         "@types/hast": "^3.0.0"
       }
     },
+    "node_modules/hastscript": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/hastscript/-/hastscript-6.0.0.tgz",
+      "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==",
+      "dependencies": {
+        "@types/hast": "^2.0.0",
+        "comma-separated-tokens": "^1.0.0",
+        "hast-util-parse-selector": "^2.0.0",
+        "property-information": "^5.0.0",
+        "space-separated-tokens": "^1.0.0"
+      }
+    },
+    "node_modules/hastscript/node_modules/@types/hast": {
+      "version": "2.3.10",
+      "resolved": "https://registry.npmmirror.com/@types/hast/-/hast-2.3.10.tgz",
+      "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==",
+      "dependencies": {
+        "@types/unist": "^2"
+      }
+    },
+    "node_modules/hastscript/node_modules/@types/unist": {
+      "version": "2.0.10",
+      "resolved": "https://registry.npmmirror.com/@types/unist/-/unist-2.0.10.tgz",
+      "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="
+    },
+    "node_modules/hastscript/node_modules/comma-separated-tokens": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
+      "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw=="
+    },
+    "node_modules/hastscript/node_modules/property-information": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmmirror.com/property-information/-/property-information-5.6.0.tgz",
+      "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==",
+      "dependencies": {
+        "xtend": "^4.0.0"
+      }
+    },
+    "node_modules/hastscript/node_modules/space-separated-tokens": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
+      "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA=="
+    },
     "node_modules/he": {
       "version": "1.2.0",
       "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
@@ -9220,6 +9295,14 @@
         "he": "bin/he"
       }
     },
+    "node_modules/highlight.js": {
+      "version": "10.7.3",
+      "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-10.7.3.tgz",
+      "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/history": {
       "version": "5.3.0",
       "resolved": "https://registry.npmmirror.com/history/-/history-5.3.0.tgz",
@@ -11020,6 +11103,15 @@
         "tslib": "^2.0.3"
       }
     },
+    "node_modules/lowlight": {
+      "version": "1.20.0",
+      "resolved": "https://registry.npmmirror.com/lowlight/-/lowlight-1.20.0.tgz",
+      "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
+      "dependencies": {
+        "fault": "^1.0.0",
+        "highlight.js": "~10.7.0"
+      }
+    },
     "node_modules/lru-cache": {
       "version": "5.1.1",
       "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -13409,6 +13501,14 @@
         "renderkid": "^3.0.0"
       }
     },
+    "node_modules/prismjs": {
+      "version": "1.29.0",
+      "resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.29.0.tgz",
+      "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/process": {
       "version": "0.11.10",
       "resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz",
@@ -14846,6 +14946,21 @@
         "node": ">=0.12.0"
       }
     },
+    "node_modules/react-syntax-highlighter": {
+      "version": "15.5.0",
+      "resolved": "https://registry.npmmirror.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz",
+      "integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==",
+      "dependencies": {
+        "@babel/runtime": "^7.3.1",
+        "highlight.js": "^10.4.1",
+        "lowlight": "^1.17.0",
+        "prismjs": "^1.27.0",
+        "refractor": "^3.6.0"
+      },
+      "peerDependencies": {
+        "react": ">= 0.14.0"
+      }
+    },
     "node_modules/reactcss": {
       "version": "1.2.3",
       "resolved": "https://registry.npmmirror.com/reactcss/-/reactcss-1.2.3.tgz",
@@ -15079,6 +15194,76 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/refractor": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmmirror.com/refractor/-/refractor-3.6.0.tgz",
+      "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==",
+      "dependencies": {
+        "hastscript": "^6.0.0",
+        "parse-entities": "^2.0.0",
+        "prismjs": "~1.27.0"
+      }
+    },
+    "node_modules/refractor/node_modules/character-entities": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/character-entities/-/character-entities-1.2.4.tgz",
+      "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw=="
+    },
+    "node_modules/refractor/node_modules/character-entities-legacy": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
+      "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA=="
+    },
+    "node_modules/refractor/node_modules/character-reference-invalid": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
+      "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="
+    },
+    "node_modules/refractor/node_modules/is-alphabetical": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
+      "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="
+    },
+    "node_modules/refractor/node_modules/is-alphanumerical": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
+      "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
+      "dependencies": {
+        "is-alphabetical": "^1.0.0",
+        "is-decimal": "^1.0.0"
+      }
+    },
+    "node_modules/refractor/node_modules/is-decimal": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/is-decimal/-/is-decimal-1.0.4.tgz",
+      "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw=="
+    },
+    "node_modules/refractor/node_modules/is-hexadecimal": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
+      "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="
+    },
+    "node_modules/refractor/node_modules/parse-entities": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/parse-entities/-/parse-entities-2.0.0.tgz",
+      "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
+      "dependencies": {
+        "character-entities": "^1.0.0",
+        "character-entities-legacy": "^1.0.0",
+        "character-reference-invalid": "^1.0.0",
+        "is-alphanumerical": "^1.0.0",
+        "is-decimal": "^1.0.0",
+        "is-hexadecimal": "^1.0.0"
+      }
+    },
+    "node_modules/refractor/node_modules/prismjs": {
+      "version": "1.27.0",
+      "resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.27.0.tgz",
+      "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/regenerate": {
       "version": "1.4.2",
       "resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz",
diff --git a/web/package.json b/web/package.json
index 251860c..b693c93 100644
--- a/web/package.json
+++ b/web/package.json
@@ -29,6 +29,7 @@
     "react-markdown": "^9.0.1",
     "react-pdf-highlighter": "^6.1.0",
     "react-string-replace": "^1.1.1",
+    "react-syntax-highlighter": "^15.5.0",
     "remark-gfm": "^4.0.0",
     "umi": "^4.0.90",
     "umi-request": "^1.4.0",
@@ -40,6 +41,7 @@
     "@types/lodash": "^4.14.202",
     "@types/react": "^18.0.33",
     "@types/react-dom": "^18.0.11",
+    "@types/react-syntax-highlighter": "^15.5.11",
     "@types/uuid": "^9.0.8",
     "@umijs/lint": "^4.1.1",
     "@umijs/plugins": "^4.1.0",
diff --git a/web/src/pages/chat/chat-container/index.tsx b/web/src/pages/chat/chat-container/index.tsx
index 1cca00f..575f06b 100644
--- a/web/src/pages/chat/chat-container/index.tsx
+++ b/web/src/pages/chat/chat-container/index.tsx
@@ -24,6 +24,7 @@ import classNames from 'classnames';
 import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
 import Markdown from 'react-markdown';
 import reactStringReplace from 'react-string-replace';
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
 import remarkGfm from 'remark-gfm';
 import { visitParents } from 'unist-util-visit-parents';
 import {
@@ -42,7 +43,11 @@ const getChunkIndex = (match: string) => Number(match.slice(2, -2));
 const rehypeWrapReference = () => {
   return function wrapTextTransform(tree: any) {
     visitParents(tree, 'text', (node, ancestors) => {
-      if (ancestors.at(-1).tagName !== 'custom-typography') {
+      const latestAncestor = ancestors.at(-1);
+      if (
+        latestAncestor.tagName !== 'custom-typography' &&
+        latestAncestor.tagName !== 'code'
+      ) {
         node.type = 'element';
         node.tagName = 'custom-typography';
         node.properties = {};
@@ -187,6 +192,23 @@ const MessageItem = ({
                       }: {
                         children: string;
                       }) => renderReference(children),
+                      code(props: any) {
+                        const { children, className, node, ...rest } = props;
+                        const match = /language-(\w+)/.exec(className || '');
+                        return match ? (
+                          <SyntaxHighlighter
+                            {...rest}
+                            PreTag="div"
+                            language={match[1]}
+                          >
+                            {String(children).replace(/\n$/, '')}
+                          </SyntaxHighlighter>
+                        ) : (
+                          <code {...rest} className={className}>
+                            {children}
+                          </code>
+                        );
+                      },
                     } as any
                   }
                 >
@@ -246,7 +268,7 @@ const ChatContainer = () => {
 
   const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {
     const value = e.target.value.trim();
-    const nextValue = value.replaceAll('\\n', '\n');
+    const nextValue = value.replaceAll('\\n', '\n').replaceAll('\\t', '\t');
     setValue(nextValue);
   };
 
-- 
GitLab