This is an automated email from the ASF dual-hosted git repository.

voidmatcha pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new baeff2f5b8 [ZEPPELIN-6744] Add AI Assistant Markdown answer component
baeff2f5b8 is described below

commit baeff2f5b8ceb0f3932bb66af7a61d760931ae66
Author: Minho Jang <[email protected]>
AuthorDate: Fri Oct 16 23:20:01 2026 +0900

    [ZEPPELIN-6744] Add AI Assistant Markdown answer component
    
    ### What is this PR for?
    
    Add a React Markdown answer component for the AI Assistant.
    
    The component renders GFM tables and language-labelled code blocks, 
supports unclosed code fences during streaming, and reports paragraph reference 
clicks through a callback. HTML is displayed as text and dangerous link URLs 
are blocked.
    
    Includes Storybook examples and unit tests for rendering, input updates, 
paragraph references, and unsafe content.
    
    ### What type of PR is it?
    
    Feature
    
    ### What is the Jira issue?
    
    https://issues.apache.org/jira/browse/ZEPPELIN-6744
    
    ### How should this be tested?
    
    From `zeppelin-web-angular/projects/zeppelin-react`:
    
    ```bash
    npm ci
    npm test -- --maxWorkers=1
    npm run typecheck
    npm run lint
    npm run build:storybook
    npm run storybook
    ```
    
    Open **Assistant / MarkdownAnswer** and check both light and dark themes:
    
    - **GfmTable / SqlCode**: confirm tables and long code lines scroll within 
the answer, and code blocks show their language.
    - **PartialFence / Streaming**: confirm an unclosed fence remains readable 
as chunks arrive and the following paragraph appears after the fence closes.
    - **ParagraphReference**: click the reference and confirm its ID and name 
appear in the callback status.
    - **DangerousContent**: confirm HTML appears as text and dangerous URLs are 
not navigable.
    
    ### Questions:
    
    * Does the license files need to update?
      Yes. MIT license entries and texts for react-markdown and remark-gfm are 
included.
    
    * Is there breaking changes for older versions?
      No existing public API is changed.
    
    * Does this needs documentation?
      The React README includes a short component description and a pointer to 
its Storybook examples.
    
    
    Closes #5570 from miinhho/ZEPPELIN-6744-markdown-answer.
    
    Signed-off-by: YONGJAE LEE <[email protected]>
---
 LICENSE                                            |    6 +
 licenses/LICENSE-react-markdown-10.1.0             |   21 +
 licenses/LICENSE-remark-gfm-4.0.1                  |   22 +
 zeppelin-distribution/src/bin_license/LICENSE      |    6 +
 .../projects/zeppelin-react/README.md              |   10 +
 .../projects/zeppelin-react/package-lock.json      | 1613 +++++++++++++++++++-
 .../projects/zeppelin-react/package.json           |    4 +
 .../entities/assistant/ui/MarkdownAnswer.css.ts    |  192 +++
 .../entities/assistant/ui/MarkdownAnswer.spec.tsx  |  435 ++++++
 .../assistant/ui/MarkdownAnswer.stories.tsx        |  239 +++
 .../src/entities/assistant/ui/MarkdownAnswer.tsx   |  219 +++
 11 files changed, 2684 insertions(+), 83 deletions(-)

diff --git a/LICENSE b/LICENSE
index c0a88ab0b6..086859ca74 100644
--- a/LICENSE
+++ b/LICENSE
@@ -205,6 +205,12 @@ MIT licenses
 
 The following components are provided under the MIT License. See project link 
for details.
 The text of each license is also included at 
licenses/LICENSE-[project]-[version].txt.
+    (The MIT License) react-markdown 10.1.0
+      https://github.com/remarkjs/react-markdown
+      (see licenses/LICENSE-react-markdown-10.1.0)
+    (The MIT License) remark-gfm 4.0.1
+      https://github.com/remarkjs/remark-gfm
+      (see licenses/LICENSE-remark-gfm-4.0.1)
     (The MIT License) vanilla-extract
       (@vanilla-extract/css 1.21.3, @vanilla-extract/dynamic 2.1.5)
       
https://github.com/vanilla-extract-css/vanilla-extract/blob/master/LICENSE
diff --git a/licenses/LICENSE-react-markdown-10.1.0 
b/licenses/LICENSE-react-markdown-10.1.0
new file mode 100644
index 0000000000..f552fc9570
--- /dev/null
+++ b/licenses/LICENSE-react-markdown-10.1.0
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) Espen Hovlandsdal
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/licenses/LICENSE-remark-gfm-4.0.1 
b/licenses/LICENSE-remark-gfm-4.0.1
new file mode 100644
index 0000000000..bc8f165a62
--- /dev/null
+++ b/licenses/LICENSE-remark-gfm-4.0.1
@@ -0,0 +1,22 @@
+(The MIT License)
+
+Copyright (c) Titus Wormer <[email protected]>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/zeppelin-distribution/src/bin_license/LICENSE 
b/zeppelin-distribution/src/bin_license/LICENSE
index bbc9ab8d12..b68157fa1b 100644
--- a/zeppelin-distribution/src/bin_license/LICENSE
+++ b/zeppelin-distribution/src/bin_license/LICENSE
@@ -252,6 +252,12 @@ MIT licenses
 
 The following components are provided under the MIT License. See project link 
for details.
 The text of each license is also included at 
licenses/LICENSE-[project]-[version].txt.
+    (The MIT License) react-markdown 10.1.0
+      https://github.com/remarkjs/react-markdown
+      (see licenses/LICENSE-react-markdown-10.1.0)
+    (The MIT License) remark-gfm 4.0.1
+      https://github.com/remarkjs/remark-gfm
+      (see licenses/LICENSE-remark-gfm-4.0.1)
     (The MIT License) vanilla-extract
       (@vanilla-extract/css 1.21.3, @vanilla-extract/dynamic 2.1.5)
       
https://github.com/vanilla-extract-css/vanilla-extract/blob/master/LICENSE
diff --git a/zeppelin-web-angular/projects/zeppelin-react/README.md 
b/zeppelin-web-angular/projects/zeppelin-react/README.md
index 662c9d7ff4..4c45e4d7ad 100644
--- a/zeppelin-web-angular/projects/zeppelin-react/README.md
+++ b/zeppelin-web-angular/projects/zeppelin-react/README.md
@@ -192,3 +192,13 @@ npm run build:storybook
 npx playwright install chromium
 npm run test:storybook
 ```
+
+### Markdown answers
+
+`MarkdownAnswer` accepts answer text through `content` and renders GFM tables 
and language-labelled code blocks, including unclosed fences during streaming.
+
+Pass `describeParagraph` and `onOpenParagraph` to enable paragraph reference 
buttons; clicks are reported to the parent without navigating the notebook.
+
+HTML is displayed as text and dangerous link URLs are blocked.
+
+See **Assistant / MarkdownAnswer** in Storybook for complete answers, 
streaming, and paragraph reference examples in both themes.
diff --git a/zeppelin-web-angular/projects/zeppelin-react/package-lock.json 
b/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
index 6290a3a454..18dd833701 100644
--- a/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
+++ b/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
@@ -22,6 +22,8 @@
         "highlight.js": "^10.7.3",
         "react": "19.3.0",
         "react-dom": "19.3.0",
+        "react-markdown": "10.1.0",
+        "remark-gfm": "4.0.1",
         "rxjs": "^7.8.0",
         "xlsx-js-style": "1.2.0"
       },
@@ -32,6 +34,7 @@
         "@testing-library/dom": "10.4.1",
         "@testing-library/react": "16.3.2",
         "@types/file-saver": "2.0.7",
+        "@types/mdast": "4.0.4",
         "@types/node": "22.19.19",
         "@types/react": "19.3.0",
         "@types/react-dom": "19.3.0",
@@ -55,6 +58,7 @@
         "ts-loader": "9.6.2",
         "typescript": "5.9.3",
         "typescript-eslint": "^8.33.1",
+        "unified": "11.0.5",
         "vite": "8.2.1",
         "vitest": "4.1.11",
         "webpack": "5.105.4",
@@ -4271,6 +4275,15 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/debug": {
+      "version": "4.1.13",
+      "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz";,
+      "integrity": 
"sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/ms": "*"
+      }
+    },
     "node_modules/@types/deep-eql": {
       "version": "4.0.2",
       "resolved": 
"https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz";,
@@ -4311,9 +4324,17 @@
       "version": "1.0.9",
       "resolved": 
"https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz";,
       "integrity": 
"sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
-      "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/estree-jsx": {
+      "version": "1.0.5",
+      "resolved": 
"https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz";,
+      "integrity": 
"sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "*"
+      }
+    },
     "node_modules/@types/express": {
       "version": "5.0.6",
       "resolved": 
"https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz";,
@@ -4346,6 +4367,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/hast": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz";,
+      "integrity": 
"sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/unist": "*"
+      }
+    },
     "node_modules/@types/html-minifier-terser": {
       "version": "6.1.0",
       "resolved": 
"https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz";,
@@ -4367,6 +4397,21 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/mdast": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz";,
+      "integrity": 
"sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/unist": "*"
+      }
+    },
+    "node_modules/@types/ms": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz";,
+      "integrity": 
"sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+      "license": "MIT"
+    },
     "node_modules/@types/node": {
       "version": "22.19.19",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz";,
@@ -4395,7 +4440,6 @@
       "version": "19.3.0",
       "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz";,
       "integrity": 
"sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "csstype": "^3.2.2"
@@ -4449,6 +4493,12 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/unist": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz";,
+      "integrity": 
"sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+      "license": "MIT"
+    },
     "node_modules/@types/ws": {
       "version": "8.18.1",
       "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz";,
@@ -4692,6 +4742,12 @@
         "url": "https://opencollective.com/eslint";
       }
     },
+    "node_modules/@ungap/structured-clone": {
+      "version": "1.4.0",
+      "resolved": 
"https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz";,
+      "integrity": 
"sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==",
+      "license": "ISC"
+    },
     "node_modules/@vanilla-extract/babel-plugin-debug-ids": {
       "version": "1.2.2",
       "resolved": 
"https://registry.npmjs.org/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.2.2.tgz";,
@@ -5749,6 +5805,16 @@
         "url": "https://github.com/sponsors/ljharb";
       }
     },
+    "node_modules/bail": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz";,
+      "integrity": 
"sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
     "node_modules/balanced-match": {
       "version": "4.0.4",
       "resolved": 
"https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz";,
@@ -6061,6 +6127,16 @@
       ],
       "license": "CC-BY-4.0"
     },
+    "node_modules/ccount": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz";,
+      "integrity": 
"sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
     "node_modules/cfb": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz";,
@@ -6110,6 +6186,46 @@
         "url": "https://github.com/chalk/chalk?sponsor=1";
       }
     },
+    "node_modules/character-entities": {
+      "version": "2.0.2",
+      "resolved": 
"https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz";,
+      "integrity": 
"sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
+    "node_modules/character-entities-html4": {
+      "version": "2.1.0",
+      "resolved": 
"https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz";,
+      "integrity": 
"sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
+    "node_modules/character-entities-legacy": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz";,
+      "integrity": 
"sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
+    "node_modules/character-reference-invalid": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz";,
+      "integrity": 
"sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
     "node_modules/chart.js": {
       "version": "4.5.1",
       "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz";,
@@ -6275,6 +6391,16 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/comma-separated-tokens": {
+      "version": "2.0.3",
+      "resolved": 
"https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz";,
+      "integrity": 
"sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
     "node_modules/commander": {
       "version": "8.3.0",
       "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz";,
@@ -6624,7 +6750,6 @@
       "version": "4.4.3",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz";,
       "integrity": 
"sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "ms": "^2.1.3"
@@ -6645,6 +6770,19 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/decode-named-character-reference": {
+      "version": "1.3.0",
+      "resolved": 
"https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz";,
+      "integrity": 
"sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
+      "license": "MIT",
+      "dependencies": {
+        "character-entities": "^2.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
     "node_modules/dedent": {
       "version": "1.7.2",
       "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz";,
@@ -6701,26 +6839,6 @@
         "node": ">=16.9.0"
       }
     },
-    "node_modules/deepmerge-ts": {
-      "version": "8.0.2",
-      "resolved": 
"https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-8.0.2.tgz";,
-      "integrity": 
"sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "ko-fi",
-          "url": "https://ko-fi.com/rebeccastevens";
-        },
-        {
-          "type": "tidelift",
-          "url": "https://tidelift.com/funding/github/npm/deepmerge-ts";
-        }
-      ],
-      "license": "BSD-3-Clause",
-      "engines": {
-        "node": ">=16.9.0"
-      }
-    },
     "node_modules/default-browser": {
       "version": "5.5.0",
       "resolved": 
"https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz";,
@@ -6814,7 +6932,6 @@
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz";,
       "integrity": 
"sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=6"
@@ -6841,6 +6958,19 @@
         "node": ">=8"
       }
     },
+    "node_modules/devlop": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz";,
+      "integrity": 
"sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+      "license": "MIT",
+      "dependencies": {
+        "dequal": "^2.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
     "node_modules/dns-packet": {
       "version": "5.6.1",
       "resolved": 
"https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz";,
@@ -7590,6 +7720,16 @@
         "node": ">=4.0"
       }
     },
+    "node_modules/estree-util-is-identifier-name": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz";,
+      "integrity": 
"sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
     "node_modules/estree-walker": {
       "version": "3.0.3",
       "resolved": 
"https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz";,
@@ -7773,6 +7913,12 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz";,
+      "integrity": 
"sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+      "license": "MIT"
+    },
     "node_modules/fast-deep-equal": {
       "version": "3.1.3",
       "resolved": 
"https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz";,
@@ -8366,6 +8512,46 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/hast-util-to-jsx-runtime": {
+      "version": "2.3.6",
+      "resolved": 
"https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz";,
+      "integrity": 
"sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "^1.0.0",
+        "@types/hast": "^3.0.0",
+        "@types/unist": "^3.0.0",
+        "comma-separated-tokens": "^2.0.0",
+        "devlop": "^1.0.0",
+        "estree-util-is-identifier-name": "^3.0.0",
+        "hast-util-whitespace": "^3.0.0",
+        "mdast-util-mdx-expression": "^2.0.0",
+        "mdast-util-mdx-jsx": "^3.0.0",
+        "mdast-util-mdxjs-esm": "^2.0.0",
+        "property-information": "^7.0.0",
+        "space-separated-tokens": "^2.0.0",
+        "style-to-js": "^1.0.0",
+        "unist-util-position": "^5.0.0",
+        "vfile-message": "^4.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/hast-util-whitespace": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz";,
+      "integrity": 
"sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/hast": "^3.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
     "node_modules/he": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz";,
@@ -8444,6 +8630,16 @@
         "node": ">=12"
       }
     },
+    "node_modules/html-url-attributes": {
+      "version": "3.0.1",
+      "resolved": 
"https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz";,
+      "integrity": 
"sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
     "node_modules/html-webpack-plugin": {
       "version": "5.5.0",
       "resolved": 
"https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz";,
@@ -8657,6 +8853,12 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/inline-style-parser": {
+      "version": "0.2.7",
+      "resolved": 
"https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz";,
+      "integrity": 
"sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
+      "license": "MIT"
+    },
     "node_modules/internal-slot": {
       "version": "1.1.0",
       "resolved": 
"https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz";,
@@ -8692,6 +8894,30 @@
         "node": ">= 10"
       }
     },
+    "node_modules/is-alphabetical": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz";,
+      "integrity": 
"sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
+    "node_modules/is-alphanumerical": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz";,
+      "integrity": 
"sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+      "license": "MIT",
+      "dependencies": {
+        "is-alphabetical": "^2.0.0",
+        "is-decimal": "^2.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
     "node_modules/is-array-buffer": {
       "version": "3.0.5",
       "resolved": 
"https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz";,
@@ -8827,6 +9053,16 @@
         "url": "https://github.com/sponsors/ljharb";
       }
     },
+    "node_modules/is-decimal": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz";,
+      "integrity": 
"sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
     "node_modules/is-docker": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz";,
@@ -8912,6 +9148,16 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/is-hexadecimal": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz";,
+      "integrity": 
"sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
     "node_modules/is-in-ssh": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz";,
@@ -9020,7 +9266,6 @@
       "version": "4.1.0",
       "resolved": 
"https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz";,
       "integrity": 
"sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=12"
@@ -9945,6 +10190,16 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/longest-streak": {
+      "version": "3.1.0",
+      "resolved": 
"https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz";,
+      "integrity": 
"sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
     "node_modules/loose-envify": {
       "version": "1.4.0",
       "resolved": 
"https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz";,
@@ -10040,6 +10295,16 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/markdown-table": {
+      "version": "3.0.4",
+      "resolved": 
"https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz";,
+      "integrity": 
"sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
     "node_modules/math-intrinsics": {
       "version": "1.1.0",
       "resolved": 
"https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz";,
@@ -10050,81 +10315,949 @@
         "node": ">= 0.4"
       }
     },
-    "node_modules/mdn-data": {
-      "version": "2.27.1",
-      "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz";,
-      "integrity": 
"sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
-      "dev": true,
-      "license": "CC0-1.0"
-    },
-    "node_modules/media-query-parser": {
-      "version": "2.0.2",
-      "resolved": 
"https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz";,
-      "integrity": 
"sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==",
+    "node_modules/mdast-util-find-and-replace": {
+      "version": "3.0.3",
+      "resolved": 
"https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.3.tgz";,
+      "integrity": 
"sha512-xPgpDNl0/OXHsI7TlaIs22lWnH3KlpvZdXG1ET/m/YT2Hhdkx8lJV4kLphE6l9simyM4KPDFBwas08BgEob6Jw==",
       "license": "MIT",
       "dependencies": {
-        "@babel/runtime": "^7.12.5"
+        "@types/mdast": "^4.0.0",
+        "escape-string-regexp": "^5.0.0",
+        "unist-util-is": "^6.0.0",
+        "unist-util-visit-parents": "^6.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
       }
     },
-    "node_modules/media-typer": {
-      "version": "1.1.0",
-      "resolved": 
"https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz";,
-      "integrity": 
"sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
-      "dev": true,
+    
"node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
+      "version": "5.0.0",
+      "resolved": 
"https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz";,
+      "integrity": 
"sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
       "license": "MIT",
       "engines": {
-        "node": ">= 0.8"
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus";
       }
     },
-    "node_modules/memfs": {
-      "version": "4.80.0",
-      "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.80.0.tgz";,
-      "integrity": 
"sha512-N70W2isi44XdGn7uPo9NJ4zRukcdkwSFHlnYx42SWOH/+hzqNc8P7+tVoA2up4z7faHvNi1RSNCFjxoYQ71VPQ==",
-      "dev": true,
-      "license": "Apache-2.0",
+    "node_modules/mdast-util-from-markdown": {
+      "version": "2.1.0",
+      "resolved": 
"https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.1.0.tgz";,
+      "integrity": 
"sha512-KRLgmPaP2H1ul2bpoJkOS+9pAcDAAj9Nu1mqLnh8NLeJG5tlvj42TH80PIGBpROWlRXLk0lDrWNuZCFy/tsB4A==",
+      "license": "MIT",
       "dependencies": {
-        "@jsonjoy.com/fs-core": "4.80.0",
-        "@jsonjoy.com/fs-fsa": "4.80.0",
-        "@jsonjoy.com/fs-node": "4.80.0",
-        "@jsonjoy.com/fs-node-builtins": "4.80.0",
-        "@jsonjoy.com/fs-node-to-fsa": "4.80.0",
-        "@jsonjoy.com/fs-node-utils": "4.80.0",
-        "@jsonjoy.com/fs-print": "4.80.0",
-        "@jsonjoy.com/fs-snapshot": "4.80.0",
-        "@jsonjoy.com/json-pack": "^1.11.0",
-        "@jsonjoy.com/util": "^1.9.0",
-        "glob-to-regex.js": "^1.3.1",
-        "thingies": "^2.5.0",
-        "tree-dump": "^1.0.3",
-        "tslib": "^2.0.0"
+        "@types/mdast": "^4.0.0",
+        "@types/unist": "^3.0.0",
+        "decode-named-character-reference": "^1.0.0",
+        "devlop": "^1.0.0",
+        "mdast-util-to-string": "^4.0.0",
+        "micromark": "^4.0.0",
+        "micromark-util-decode-numeric-character-reference": "^2.0.0",
+        "micromark-util-decode-string": "^2.0.0",
+        "micromark-util-normalize-identifier": "^2.0.0",
+        "micromark-util-symbol": "^2.0.0",
+        "micromark-util-types": "^2.0.0",
+        "unist-util-stringify-position": "^4.0.0"
       },
       "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/streamich";
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
       }
     },
-    "node_modules/merge-descriptors": {
-      "version": "2.0.0",
-      "resolved": 
"https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz";,
-      "integrity": 
"sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
-      "dev": true,
+    "node_modules/mdast-util-gfm": {
+      "version": "3.1.0",
+      "resolved": 
"https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz";,
+      "integrity": 
"sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
       "license": "MIT",
-      "engines": {
-        "node": ">=18"
+      "dependencies": {
+        "mdast-util-from-markdown": "^2.0.0",
+        "mdast-util-gfm-autolink-literal": "^2.0.0",
+        "mdast-util-gfm-footnote": "^2.0.0",
+        "mdast-util-gfm-strikethrough": "^2.0.0",
+        "mdast-util-gfm-table": "^2.0.0",
+        "mdast-util-gfm-task-list-item": "^2.0.0",
+        "mdast-util-to-markdown": "^2.0.0"
       },
       "funding": {
-        "url": "https://github.com/sponsors/sindresorhus";
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
       }
     },
-    "node_modules/merge-stream": {
-      "version": "2.0.0",
-      "resolved": 
"https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz";,
-      "integrity": 
"sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/micromatch": {
-      "version": "4.0.8",
+    "node_modules/mdast-util-gfm-autolink-literal": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz";,
+      "integrity": 
"sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/mdast": "^4.0.0",
+        "ccount": "^2.0.0",
+        "devlop": "^1.0.0",
+        "mdast-util-find-and-replace": "^3.0.0",
+        "micromark-util-character": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/mdast-util-gfm-footnote": {
+      "version": "2.1.0",
+      "resolved": 
"https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz";,
+      "integrity": 
"sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/mdast": "^4.0.0",
+        "devlop": "^1.1.0",
+        "mdast-util-from-markdown": "^2.0.0",
+        "mdast-util-to-markdown": "^2.0.0",
+        "micromark-util-normalize-identifier": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/mdast-util-gfm-strikethrough": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.1.tgz";,
+      "integrity": 
"sha512-OuJHqvr455pwu2OaOrir7dbzqs4jlWKOtlu9L6GjcIjjQwNyw02VntQsr/Ek6/A13vgnXfUhBekWRUP1OkNivw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/mdast": "^4.0.0",
+        "mdast-util-from-markdown": "^2.0.0",
+        "mdast-util-to-markdown": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/mdast-util-gfm-table": {
+      "version": "2.0.0",
+      "resolved": 
"https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz";,
+      "integrity": 
"sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/mdast": "^4.0.0",
+        "devlop": "^1.0.0",
+        "markdown-table": "^3.0.0",
+        "mdast-util-from-markdown": "^2.0.0",
+        "mdast-util-to-markdown": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/mdast-util-gfm-task-list-item": {
+      "version": "2.0.0",
+      "resolved": 
"https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz";,
+      "integrity": 
"sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/mdast": "^4.0.0",
+        "devlop": "^1.0.0",
+        "mdast-util-from-markdown": "^2.0.0",
+        "mdast-util-to-markdown": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/mdast-util-mdx-expression": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz";,
+      "integrity": 
"sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree-jsx": "^1.0.0",
+        "@types/hast": "^3.0.0",
+        "@types/mdast": "^4.0.0",
+        "devlop": "^1.0.0",
+        "mdast-util-from-markdown": "^2.0.0",
+        "mdast-util-to-markdown": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/mdast-util-mdx-jsx": {
+      "version": "3.2.0",
+      "resolved": 
"https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz";,
+      "integrity": 
"sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree-jsx": "^1.0.0",
+        "@types/hast": "^3.0.0",
+        "@types/mdast": "^4.0.0",
+        "@types/unist": "^3.0.0",
+        "ccount": "^2.0.0",
+        "devlop": "^1.1.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-stringify-position": "^4.0.0",
+        "vfile-message": "^4.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/mdast-util-mdxjs-esm": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz";,
+      "integrity": 
"sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree-jsx": "^1.0.0",
+        "@types/hast": "^3.0.0",
+        "@types/mdast": "^4.0.0",
+        "devlop": "^1.0.0",
+        "mdast-util-from-markdown": "^2.0.0",
+        "mdast-util-to-markdown": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/mdast-util-phrasing": {
+      "version": "4.1.0",
+      "resolved": 
"https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz";,
+      "integrity": 
"sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/mdast": "^4.0.0",
+        "unist-util-is": "^6.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/mdast-util-to-hast": {
+      "version": "13.2.1",
+      "resolved": 
"https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz";,
+      "integrity": 
"sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/hast": "^3.0.0",
+        "@types/mdast": "^4.0.0",
+        "@ungap/structured-clone": "^1.0.0",
+        "devlop": "^1.0.0",
+        "micromark-util-sanitize-uri": "^2.0.0",
+        "trim-lines": "^3.0.0",
+        "unist-util-position": "^5.0.0",
+        "unist-util-visit": "^5.0.0",
+        "vfile": "^6.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/mdast-util-to-markdown": {
+      "version": "2.2.0",
+      "resolved": 
"https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.2.0.tgz";,
+      "integrity": 
"sha512-2Jn/ADrHFFyyniBMi0bncTbJqU78K3/TEYvzvwrWWzD9MYPUa/Gm4r3nOeKFQg/PDM5BsbDYdc8ackW4NTY6NA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/mdast": "^4.0.0",
+        "@types/unist": "^3.0.0",
+        "longest-streak": "^3.0.0",
+        "mdast-util-phrasing": "^4.0.0",
+        "mdast-util-to-string": "^4.0.0",
+        "micromark-util-character": "^2.0.0",
+        "micromark-util-classify-character": "^2.0.0",
+        "micromark-util-decode-string": "^2.0.0",
+        "micromark-util-html-tag-name": "^2.0.0",
+        "unist-util-visit": "^5.0.0",
+        "zwitch": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/mdast-util-to-string": {
+      "version": "4.0.0",
+      "resolved": 
"https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz";,
+      "integrity": 
"sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/mdast": "^4.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/mdn-data": {
+      "version": "2.27.1",
+      "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz";,
+      "integrity": 
"sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+      "dev": true,
+      "license": "CC0-1.0"
+    },
+    "node_modules/media-query-parser": {
+      "version": "2.0.2",
+      "resolved": 
"https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz";,
+      "integrity": 
"sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "1.1.0",
+      "resolved": 
"https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz";,
+      "integrity": 
"sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/memfs": {
+      "version": "4.80.0",
+      "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.80.0.tgz";,
+      "integrity": 
"sha512-N70W2isi44XdGn7uPo9NJ4zRukcdkwSFHlnYx42SWOH/+hzqNc8P7+tVoA2up4z7faHvNi1RSNCFjxoYQ71VPQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/fs-core": "4.80.0",
+        "@jsonjoy.com/fs-fsa": "4.80.0",
+        "@jsonjoy.com/fs-node": "4.80.0",
+        "@jsonjoy.com/fs-node-builtins": "4.80.0",
+        "@jsonjoy.com/fs-node-to-fsa": "4.80.0",
+        "@jsonjoy.com/fs-node-utils": "4.80.0",
+        "@jsonjoy.com/fs-print": "4.80.0",
+        "@jsonjoy.com/fs-snapshot": "4.80.0",
+        "@jsonjoy.com/json-pack": "^1.11.0",
+        "@jsonjoy.com/util": "^1.9.0",
+        "glob-to-regex.js": "^1.3.1",
+        "thingies": "^2.5.0",
+        "tree-dump": "^1.0.3",
+        "tslib": "^2.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich";
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "2.0.0",
+      "resolved": 
"https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz";,
+      "integrity": 
"sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus";
+      }
+    },
+    "node_modules/merge-stream": {
+      "version": "2.0.0",
+      "resolved": 
"https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz";,
+      "integrity": 
"sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/micromark": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.3.tgz";,
+      "integrity": 
"sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@types/debug": "^4.0.0",
+        "debug": "^4.0.0",
+        "decode-named-character-reference": "^1.0.0",
+        "devlop": "^1.0.0",
+        "micromark-core-commonmark": "^2.0.0",
+        "micromark-factory-space": "^2.0.0",
+        "micromark-util-character": "^2.0.0",
+        "micromark-util-chunked": "^2.0.0",
+        "micromark-util-combine-extensions": "^2.0.0",
+        "micromark-util-decode-numeric-character-reference": "^2.0.0",
+        "micromark-util-edit-map": "^1.0.0",
+        "micromark-util-encode": "^2.0.0",
+        "micromark-util-normalize-identifier": "^2.0.0",
+        "micromark-util-resolve-all": "^2.0.0",
+        "micromark-util-sanitize-uri": "^2.0.0",
+        "micromark-util-subtokenize": "^2.0.0",
+        "micromark-util-symbol": "^2.0.0",
+        "micromark-util-types": "^2.0.0"
+      }
+    },
+    "node_modules/micromark-core-commonmark": {
+      "version": "2.0.4",
+      "resolved": 
"https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.4.tgz";,
+      "integrity": 
"sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "decode-named-character-reference": "^1.0.0",
+        "devlop": "^1.0.0",
+        "micromark-factory-destination": "^2.0.0",
+        "micromark-factory-label": "^2.0.0",
+        "micromark-factory-space": "^2.1.0",
+        "micromark-factory-title": "^2.0.0",
+        "micromark-factory-whitespace": "^2.0.0",
+        "micromark-util-character": "^2.0.0",
+        "micromark-util-chunked": "^2.0.0",
+        "micromark-util-classify-character": "^2.0.0",
+        "micromark-util-edit-map": "^1.0.0",
+        "micromark-util-html-tag-name": "^2.0.0",
+        "micromark-util-normalize-identifier": "^2.0.0",
+        "micromark-util-resolve-all": "^2.0.0",
+        "micromark-util-subtokenize": "^2.0.0",
+        "micromark-util-symbol": "^2.0.0",
+        "micromark-util-types": "^2.0.3"
+      }
+    },
+    "node_modules/micromark-extension-gfm": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz";,
+      "integrity": 
"sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
+      "license": "MIT",
+      "dependencies": {
+        "micromark-extension-gfm-autolink-literal": "^2.0.0",
+        "micromark-extension-gfm-footnote": "^2.0.0",
+        "micromark-extension-gfm-strikethrough": "^2.0.0",
+        "micromark-extension-gfm-table": "^2.0.0",
+        "micromark-extension-gfm-tagfilter": "^2.0.0",
+        "micromark-extension-gfm-task-list-item": "^2.0.0",
+        "micromark-util-combine-extensions": "^2.0.0",
+        "micromark-util-types": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/micromark-extension-gfm-autolink-literal": {
+      "version": "2.1.0",
+      "resolved": 
"https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz";,
+      "integrity": 
"sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
+      "license": "MIT",
+      "dependencies": {
+        "micromark-util-character": "^2.0.0",
+        "micromark-util-sanitize-uri": "^2.0.0",
+        "micromark-util-symbol": "^2.0.0",
+        "micromark-util-types": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/micromark-extension-gfm-footnote": {
+      "version": "2.1.0",
+      "resolved": 
"https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz";,
+      "integrity": 
"sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
+      "license": "MIT",
+      "dependencies": {
+        "devlop": "^1.0.0",
+        "micromark-core-commonmark": "^2.0.0",
+        "micromark-factory-space": "^2.0.0",
+        "micromark-util-character": "^2.0.0",
+        "micromark-util-normalize-identifier": "^2.0.0",
+        "micromark-util-sanitize-uri": "^2.0.0",
+        "micromark-util-symbol": "^2.0.0",
+        "micromark-util-types": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/micromark-extension-gfm-strikethrough": {
+      "version": "2.1.0",
+      "resolved": 
"https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz";,
+      "integrity": 
"sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
+      "license": "MIT",
+      "dependencies": {
+        "devlop": "^1.0.0",
+        "micromark-util-chunked": "^2.0.0",
+        "micromark-util-classify-character": "^2.0.0",
+        "micromark-util-resolve-all": "^2.0.0",
+        "micromark-util-symbol": "^2.0.0",
+        "micromark-util-types": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/micromark-extension-gfm-table": {
+      "version": "2.1.2",
+      "resolved": 
"https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz";,
+      "integrity": 
"sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==",
+      "license": "MIT",
+      "dependencies": {
+        "devlop": "^1.0.0",
+        "micromark-factory-space": "^2.0.0",
+        "micromark-util-character": "^2.0.0",
+        "micromark-util-symbol": "^2.0.0",
+        "micromark-util-types": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/micromark-extension-gfm-tagfilter": {
+      "version": "2.0.0",
+      "resolved": 
"https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz";,
+      "integrity": 
"sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
+      "license": "MIT",
+      "dependencies": {
+        "micromark-util-types": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/micromark-extension-gfm-task-list-item": {
+      "version": "2.1.0",
+      "resolved": 
"https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz";,
+      "integrity": 
"sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
+      "license": "MIT",
+      "dependencies": {
+        "devlop": "^1.0.0",
+        "micromark-factory-space": "^2.0.0",
+        "micromark-util-character": "^2.0.0",
+        "micromark-util-symbol": "^2.0.0",
+        "micromark-util-types": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/micromark-factory-destination": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz";,
+      "integrity": 
"sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "micromark-util-character": "^2.0.0",
+        "micromark-util-symbol": "^2.0.0",
+        "micromark-util-types": "^2.0.0"
+      }
+    },
+    "node_modules/micromark-factory-label": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz";,
+      "integrity": 
"sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "devlop": "^1.0.0",
+        "micromark-util-character": "^2.0.0",
+        "micromark-util-symbol": "^2.0.0",
+        "micromark-util-types": "^2.0.0"
+      }
+    },
+    "node_modules/micromark-factory-space": {
+      "version": "2.1.0",
+      "resolved": 
"https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.1.0.tgz";,
+      "integrity": 
"sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "micromark-util-character": "^2.0.0",
+        "micromark-util-types": "^2.0.0"
+      }
+    },
+    "node_modules/micromark-factory-title": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz";,
+      "integrity": 
"sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "micromark-factory-space": "^2.0.0",
+        "micromark-util-character": "^2.0.0",
+        "micromark-util-symbol": "^2.0.0",
+        "micromark-util-types": "^2.0.0"
+      }
+    },
+    "node_modules/micromark-factory-whitespace": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz";,
+      "integrity": 
"sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "micromark-factory-space": "^2.0.0",
+        "micromark-util-character": "^2.0.0",
+        "micromark-util-symbol": "^2.0.0",
+        "micromark-util-types": "^2.0.0"
+      }
+    },
+    "node_modules/micromark-util-character": {
+      "version": "2.1.1",
+      "resolved": 
"https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz";,
+      "integrity": 
"sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "micromark-util-symbol": "^2.0.0",
+        "micromark-util-types": "^2.0.0"
+      }
+    },
+    "node_modules/micromark-util-chunked": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz";,
+      "integrity": 
"sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "micromark-util-symbol": "^2.0.0"
+      }
+    },
+    "node_modules/micromark-util-classify-character": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz";,
+      "integrity": 
"sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "micromark-util-character": "^2.0.0",
+        "micromark-util-symbol": "^2.0.0",
+        "micromark-util-types": "^2.0.0"
+      }
+    },
+    "node_modules/micromark-util-combine-extensions": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz";,
+      "integrity": 
"sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "micromark-util-chunked": "^2.0.0",
+        "micromark-util-types": "^2.0.0"
+      }
+    },
+    "node_modules/micromark-util-decode-numeric-character-reference": {
+      "version": "2.0.2",
+      "resolved": 
"https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz";,
+      "integrity": 
"sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "micromark-util-symbol": "^2.0.0"
+      }
+    },
+    "node_modules/micromark-util-decode-string": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz";,
+      "integrity": 
"sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "decode-named-character-reference": "^1.0.0",
+        "micromark-util-character": "^2.0.0",
+        "micromark-util-decode-numeric-character-reference": "^2.0.0",
+        "micromark-util-symbol": "^2.0.0"
+      }
+    },
+    "node_modules/micromark-util-edit-map": {
+      "version": "1.0.0",
+      "resolved": 
"https://registry.npmjs.org/micromark-util-edit-map/-/micromark-util-edit-map-1.0.0.tgz";,
+      "integrity": 
"sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "micromark-util-types": "^2.0.0"
+      }
+    },
+    "node_modules/micromark-util-encode": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz";,
+      "integrity": 
"sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/micromark-util-html-tag-name": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz";,
+      "integrity": 
"sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/micromark-util-normalize-identifier": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz";,
+      "integrity": 
"sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "micromark-util-symbol": "^2.0.0"
+      }
+    },
+    "node_modules/micromark-util-resolve-all": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz";,
+      "integrity": 
"sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "micromark-util-types": "^2.0.0"
+      }
+    },
+    "node_modules/micromark-util-sanitize-uri": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz";,
+      "integrity": 
"sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "micromark-util-character": "^2.0.0",
+        "micromark-util-encode": "^2.0.0",
+        "micromark-util-symbol": "^2.0.0"
+      }
+    },
+    "node_modules/micromark-util-subtokenize": {
+      "version": "2.1.0",
+      "resolved": 
"https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz";,
+      "integrity": 
"sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "devlop": "^1.0.0",
+        "micromark-util-chunked": "^2.0.0",
+        "micromark-util-symbol": "^2.0.0",
+        "micromark-util-types": "^2.0.0"
+      }
+    },
+    "node_modules/micromark-util-symbol": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz";,
+      "integrity": 
"sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/micromark-util-types": {
+      "version": "2.0.3",
+      "resolved": 
"https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz";,
+      "integrity": 
"sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs";
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified";
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
       "resolved": 
"https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz";,
       "integrity": 
"sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
       "dev": true,
@@ -10242,7 +11375,6 @@
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz";,
       "integrity": 
"sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/multicast-dns": {
@@ -10735,6 +11867,31 @@
         "node": ">=6"
       }
     },
+    "node_modules/parse-entities": {
+      "version": "4.0.2",
+      "resolved": 
"https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz";,
+      "integrity": 
"sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/unist": "^2.0.0",
+        "character-entities-legacy": "^3.0.0",
+        "character-reference-invalid": "^2.0.0",
+        "decode-named-character-reference": "^1.0.0",
+        "is-alphanumerical": "^2.0.0",
+        "is-decimal": "^2.0.0",
+        "is-hexadecimal": "^2.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
+    "node_modules/parse-entities/node_modules/@types/unist": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz";,
+      "integrity": 
"sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+      "license": "MIT"
+    },
     "node_modules/parse5": {
       "version": "8.0.1",
       "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz";,
@@ -11306,6 +12463,16 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/property-information": {
+      "version": "7.2.0",
+      "resolved": 
"https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz";,
+      "integrity": 
"sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
     "node_modules/proxy-addr": {
       "version": "2.0.8",
       "resolved": 
"https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz";,
@@ -11506,6 +12673,33 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/react-markdown": {
+      "version": "10.1.0",
+      "resolved": 
"https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz";,
+      "integrity": 
"sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/hast": "^3.0.0",
+        "@types/mdast": "^4.0.0",
+        "devlop": "^1.0.0",
+        "hast-util-to-jsx-runtime": "^2.0.0",
+        "html-url-attributes": "^3.0.0",
+        "mdast-util-to-hast": "^13.0.0",
+        "remark-parse": "^11.0.0",
+        "remark-rehype": "^11.0.0",
+        "unified": "^11.0.0",
+        "unist-util-visit": "^5.0.0",
+        "vfile": "^6.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      },
+      "peerDependencies": {
+        "@types/react": ">=18",
+        "react": ">=18"
+      }
+    },
     "node_modules/readable-stream": {
       "version": "3.6.2",
       "resolved": 
"https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz";,
@@ -11685,6 +12879,72 @@
         "node": ">= 0.10"
       }
     },
+    "node_modules/remark-gfm": {
+      "version": "4.0.1",
+      "resolved": 
"https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz";,
+      "integrity": 
"sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/mdast": "^4.0.0",
+        "mdast-util-gfm": "^3.0.0",
+        "micromark-extension-gfm": "^3.0.0",
+        "remark-parse": "^11.0.0",
+        "remark-stringify": "^11.0.0",
+        "unified": "^11.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/remark-parse": {
+      "version": "11.0.0",
+      "resolved": 
"https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz";,
+      "integrity": 
"sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/mdast": "^4.0.0",
+        "mdast-util-from-markdown": "^2.0.0",
+        "micromark-util-types": "^2.0.0",
+        "unified": "^11.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/remark-rehype": {
+      "version": "11.1.2",
+      "resolved": 
"https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz";,
+      "integrity": 
"sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/hast": "^3.0.0",
+        "@types/mdast": "^4.0.0",
+        "mdast-util-to-hast": "^13.0.0",
+        "unified": "^11.0.0",
+        "vfile": "^6.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/remark-stringify": {
+      "version": "11.0.0",
+      "resolved": 
"https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz";,
+      "integrity": 
"sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/mdast": "^4.0.0",
+        "mdast-util-to-markdown": "^2.0.0",
+        "unified": "^11.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
     "node_modules/renderkid": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz";,
@@ -12433,6 +13693,16 @@
         "source-map": "^0.6.0"
       }
     },
+    "node_modules/space-separated-tokens": {
+      "version": "2.0.2",
+      "resolved": 
"https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz";,
+      "integrity": 
"sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
     "node_modules/split": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz";,
@@ -12805,6 +14075,20 @@
         "url": "https://github.com/sponsors/ljharb";
       }
     },
+    "node_modules/stringify-entities": {
+      "version": "4.0.4",
+      "resolved": 
"https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz";,
+      "integrity": 
"sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+      "license": "MIT",
+      "dependencies": {
+        "character-entities-html4": "^2.0.0",
+        "character-entities-legacy": "^3.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
     "node_modules/strip-ansi": {
       "version": "6.0.1",
       "resolved": 
"https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz";,
@@ -12871,6 +14155,24 @@
         "webpack": "^5.0.0"
       }
     },
+    "node_modules/style-to-js": {
+      "version": "1.1.21",
+      "resolved": 
"https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz";,
+      "integrity": 
"sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
+      "license": "MIT",
+      "dependencies": {
+        "style-to-object": "1.0.14"
+      }
+    },
+    "node_modules/style-to-object": {
+      "version": "1.0.14",
+      "resolved": 
"https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz";,
+      "integrity": 
"sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
+      "license": "MIT",
+      "dependencies": {
+        "inline-style-parser": "0.2.7"
+      }
+    },
     "node_modules/stylis": {
       "version": "4.4.0",
       "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz";,
@@ -13218,6 +14520,26 @@
         "tslib": "2"
       }
     },
+    "node_modules/trim-lines": {
+      "version": "3.0.1",
+      "resolved": 
"https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz";,
+      "integrity": 
"sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
+    "node_modules/trough": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz";,
+      "integrity": 
"sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
+    },
     "node_modules/ts-api-utils": {
       "version": "2.5.0",
       "resolved": 
"https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz";,
@@ -13549,6 +14871,93 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/unified": {
+      "version": "11.0.5",
+      "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz";,
+      "integrity": 
"sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/unist": "^3.0.0",
+        "bail": "^2.0.0",
+        "devlop": "^1.0.0",
+        "extend": "^3.0.0",
+        "is-plain-obj": "^4.0.0",
+        "trough": "^2.0.0",
+        "vfile": "^6.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/unist-util-is": {
+      "version": "6.0.1",
+      "resolved": 
"https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz";,
+      "integrity": 
"sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/unist": "^3.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/unist-util-position": {
+      "version": "5.0.0",
+      "resolved": 
"https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz";,
+      "integrity": 
"sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/unist": "^3.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/unist-util-stringify-position": {
+      "version": "4.0.0",
+      "resolved": 
"https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz";,
+      "integrity": 
"sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/unist": "^3.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/unist-util-visit": {
+      "version": "5.1.0",
+      "resolved": 
"https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz";,
+      "integrity": 
"sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/unist": "^3.0.0",
+        "unist-util-is": "^6.0.0",
+        "unist-util-visit-parents": "^6.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/unist-util-visit-parents": {
+      "version": "6.0.2",
+      "resolved": 
"https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz";,
+      "integrity": 
"sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/unist": "^3.0.0",
+        "unist-util-is": "^6.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
     "node_modules/universalify": {
       "version": "2.0.1",
       "resolved": 
"https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz";,
@@ -13644,6 +15053,34 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/vfile": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz";,
+      "integrity": 
"sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/unist": "^3.0.0",
+        "vfile-message": "^4.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
+    "node_modules/vfile-message": {
+      "version": "4.0.3",
+      "resolved": 
"https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz";,
+      "integrity": 
"sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/unist": "^3.0.0",
+        "unist-util-stringify-position": "^4.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified";
+      }
+    },
     "node_modules/vite": {
       "version": "8.2.1",
       "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz";,
@@ -14554,6 +15991,16 @@
       "peerDependencies": {
         "zod": "^3.25.0 || ^4.0.0"
       }
+    },
+    "node_modules/zwitch": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz";,
+      "integrity": 
"sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm";
+      }
     }
   }
 }
diff --git a/zeppelin-web-angular/projects/zeppelin-react/package.json 
b/zeppelin-web-angular/projects/zeppelin-react/package.json
index 4052d7d2b8..bc236a11f2 100644
--- a/zeppelin-web-angular/projects/zeppelin-react/package.json
+++ b/zeppelin-web-angular/projects/zeppelin-react/package.json
@@ -35,6 +35,8 @@
     "highlight.js": "^10.7.3",
     "react": "19.3.0",
     "react-dom": "19.3.0",
+    "react-markdown": "10.1.0",
+    "remark-gfm": "4.0.1",
     "rxjs": "^7.8.0",
     "xlsx-js-style": "1.2.0"
   },
@@ -45,6 +47,7 @@
     "@testing-library/dom": "10.4.1",
     "@testing-library/react": "16.3.2",
     "@types/file-saver": "2.0.7",
+    "@types/mdast": "4.0.4",
     "@types/node": "22.19.19",
     "@types/react": "19.3.0",
     "@types/react-dom": "19.3.0",
@@ -68,6 +71,7 @@
     "ts-loader": "9.6.2",
     "typescript": "5.9.3",
     "typescript-eslint": "^8.33.1",
+    "unified": "11.0.5",
     "vite": "8.2.1",
     "vitest": "4.1.11",
     "webpack": "5.105.4",
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/MarkdownAnswer.css.ts
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/MarkdownAnswer.css.ts
new file mode 100644
index 0000000000..a39fa0701a
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/MarkdownAnswer.css.ts
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { globalStyle, style } from '@vanilla-extract/css';
+import { vars } from '@/shared/ui/assistant-theme/AssistantScope.css';
+
+export const answer = style({
+  boxSizing: 'border-box',
+  width: '100%',
+  minWidth: 0,
+  maxWidth: '100%',
+  color: vars.text,
+  fontFamily: vars.font,
+  fontSize: vars.fontSize,
+  lineHeight: vars.lineHeight,
+  overflowWrap: 'anywhere'
+});
+
+globalStyle(`${answer} *, ${answer} *::before, ${answer} *::after`, {
+  boxSizing: 'inherit'
+});
+
+globalStyle(`${answer} > :first-child`, { marginTop: 0 });
+globalStyle(`${answer} > :last-child`, { marginBottom: 0 });
+
+globalStyle(`${answer} p`, { margin: `0 0 ${vars.space3}` });
+globalStyle(`${answer} h1, ${answer} h2, ${answer} h3, ${answer} h4, ${answer} 
h5, ${answer} h6`, {
+  margin: `${vars.space4} 0 ${vars.space2}`,
+  color: vars.text,
+  fontWeight: 600,
+  lineHeight: 1.35
+});
+globalStyle(`${answer} h1`, { fontSize: '1.6em' });
+globalStyle(`${answer} h2`, { fontSize: '1.4em' });
+globalStyle(`${answer} h3`, { fontSize: '1.2em' });
+globalStyle(`${answer} h4, ${answer} h5, ${answer} h6`, { fontSize: '1em' });
+
+globalStyle(`${answer} ul, ${answer} ol`, {
+  margin: `0 0 ${vars.space3}`,
+  paddingLeft: vars.space5
+});
+globalStyle(`${answer} li + li`, { marginTop: vars.space1 });
+globalStyle(`${answer} li > p`, { marginBottom: vars.space1 });
+globalStyle(`${answer} li > ul, ${answer} li > ol`, {
+  marginTop: vars.space1,
+  marginBottom: 0
+});
+globalStyle(`${answer} input[type="checkbox"]`, {
+  marginRight: vars.space1,
+  accentColor: vars.accent
+});
+
+globalStyle(`${answer} blockquote`, {
+  margin: `0 0 ${vars.space3}`,
+  padding: `${vars.space1} 0 ${vars.space1} ${vars.space3}`,
+  borderLeft: `3px solid ${vars.borderStrong}`,
+  color: vars.textSecondary
+});
+globalStyle(`${answer} blockquote > :last-child`, { marginBottom: 0 });
+globalStyle(`${answer} hr`, {
+  margin: `${vars.space4} 0`,
+  border: 0,
+  borderTop: `1px solid ${vars.border}`
+});
+
+globalStyle(`${answer} a`, {
+  color: vars.accentText,
+  textDecoration: 'underline',
+  textUnderlineOffset: '2px'
+});
+globalStyle(`${answer} a:hover`, { color: vars.accentTextHover });
+globalStyle(`${answer} :focus-visible`, {
+  outline: `2px solid ${vars.focus}`,
+  outlineOffset: 2
+});
+
+globalStyle(`${answer} code`, {
+  padding: `1px ${vars.space1}`,
+  borderRadius: vars.radiusSm,
+  background: vars.fill,
+  color: vars.text,
+  fontFamily: vars.fontMono,
+  fontSize: vars.fontSizeSm
+});
+
+export const paragraphReference = style({
+  display: 'inline',
+  maxWidth: '100%',
+  margin: 0,
+  padding: `0 ${vars.space1}`,
+  border: `1px solid ${vars.accentBorder}`,
+  borderRadius: vars.radiusSm,
+  background: vars.accentBg,
+  color: vars.accentText,
+  font: 'inherit',
+  lineHeight: 'inherit',
+  textAlign: 'inherit',
+  whiteSpace: 'normal',
+  overflowWrap: 'anywhere',
+  verticalAlign: 'baseline',
+  cursor: 'pointer',
+  selectors: {
+    '&:hover': { color: vars.accentTextHover },
+    '&:focus-visible': { outline: `2px solid ${vars.focus}`, outlineOffset: 2 }
+  }
+});
+
+export const codeBlock = style({
+  minWidth: 0,
+  maxWidth: '100%',
+  marginBottom: vars.space3,
+  border: `1px solid ${vars.border}`,
+  borderRadius: vars.radius,
+  background: vars.fill
+});
+
+export const codeLanguage = style({
+  padding: `${vars.space1} ${vars.space3}`,
+  borderBottom: `1px solid ${vars.border}`,
+  color: vars.textSecondary,
+  fontFamily: vars.fontMono,
+  fontSize: vars.fontSizeXs,
+  overflowWrap: 'anywhere'
+});
+
+export const codeSource = style({
+  maxWidth: '100%',
+  margin: 0,
+  padding: vars.space3,
+  overflowX: 'auto',
+  overscrollBehaviorX: 'contain',
+  whiteSpace: 'pre',
+  overflowWrap: 'normal',
+  color: vars.text,
+  fontFamily: vars.fontMono,
+  fontSize: vars.fontSizeSm,
+  lineHeight: vars.lineHeight
+});
+
+globalStyle(`${codeSource} code`, {
+  padding: 0,
+  borderRadius: 0,
+  background: 'none',
+  fontSize: 'inherit',
+  whiteSpace: 'inherit'
+});
+
+export const tableScroll = style({
+  minWidth: 0,
+  maxWidth: '100%',
+  marginBottom: vars.space3,
+  overflowX: 'auto',
+  overscrollBehaviorX: 'contain',
+  border: `1px solid ${vars.border}`,
+  borderRadius: vars.radius
+});
+
+globalStyle(`${tableScroll} table`, {
+  width: '100%',
+  borderCollapse: 'collapse',
+  fontSize: vars.fontSizeSm
+});
+globalStyle(`${tableScroll} th, ${tableScroll} td`, {
+  padding: `${vars.space2} ${vars.space3}`,
+  borderBottom: `1px solid ${vars.border}`,
+  borderRight: `1px solid ${vars.border}`,
+  textAlign: 'left',
+  verticalAlign: 'top',
+  whiteSpace: 'nowrap',
+  overflowWrap: 'normal'
+});
+globalStyle(`${tableScroll} th`, {
+  background: vars.fill,
+  color: vars.text,
+  fontWeight: 600
+});
+globalStyle(`${tableScroll} th:last-child, ${tableScroll} td:last-child`, { 
borderRight: 0 });
+globalStyle(`${tableScroll} tbody tr:last-child td`, { borderBottom: 0 });
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/MarkdownAnswer.spec.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/MarkdownAnswer.spec.tsx
new file mode 100644
index 0000000000..629c6ae620
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/MarkdownAnswer.spec.tsx
@@ -0,0 +1,435 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { fireEvent, render, screen, within } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+import { visuallyHidden } from 
'@/shared/ui/assistant-theme/AssistantScope.css';
+import { MarkdownAnswer } from './MarkdownAnswer';
+
+const paragraphId = 'paragraph_1423500782552_-1439281894';
+const positiveParagraphId = 'paragraph_1423500782553_1439281894';
+const describeParagraph = (id: string) =>
+  id === paragraphId || id === positiveParagraphId ? { text: 'Paragraph 1', 
name: 'Sales query' } : undefined;
+
+const dangerousUrls = [
+  'javascript:alert%281%29',
+  'JaVaScRiPt:alert%281%29',
+  'java&#x73;cript:alert%281%29',
+  'data:text/html,%3Cscript%3Ealert%281%29%3C/script%3E',
+  'vbscript:msgbox%281%29',
+  'file:///etc/passwd'
+];
+
+describe('MarkdownAnswer', () => {
+  it('renders GFM table headings, cell values and column alignment', () => {
+    render(
+      <MarkdownAnswer
+        content={
+          '| Product | Region | Revenue |\n| :--- | :---: | ---: |\n| Notebook 
| Europe | 1200 |\n| Query | Asia | 840 |'
+        }
+      />
+    );
+
+    const table = screen.getByRole('table');
+    const headings = within(table).getAllByRole('columnheader');
+    const cells = within(table).getAllByRole('cell');
+    expect(headings.map(heading => heading.textContent)).toEqual(['Product', 
'Region', 'Revenue']);
+    expect(cells.map(cell => cell.textContent)).toEqual(['Notebook', 'Europe', 
'1200', 'Query', 'Asia', '840']);
+    expect(headings.map(heading => heading.style.textAlign)).toEqual(['left', 
'center', 'right']);
+    expect(cells.map(cell => cell.style.textAlign)).toEqual(['left', 'center', 
'right', 'left', 'center', 'right']);
+  });
+
+  it('replaces old content when the answer prop changes', () => {
+    const { rerender } = render(<MarkdownAnswer content={'# Old 
answer\n\nObsolete explanation.'} />);
+    expect(screen.getByRole('heading', { name: 'Old answer' })).toBeTruthy();
+
+    rerender(<MarkdownAnswer content={'## Updated answer\n\nFresh 
**results**.'} />);
+
+    expect(screen.queryByRole('heading', { name: 'Old answer' })).toBeNull();
+    expect(screen.queryByText('Obsolete explanation.')).toBeNull();
+    expect(screen.getByRole('heading', { name: 'Updated answer', level: 2 
})).toBeTruthy();
+    expect(screen.getByText('results').tagName).toBe('STRONG');
+  });
+
+  it('renders successive unclosed SQL fences and a following paragraph after 
the fence closes', () => {
+    const initialSource = 'SELECT product,\n';
+    const extendedSource = 'SELECT product, SUM(revenue)\nFROM sales\n';
+    const completedSource = `${extendedSource}GROUP BY product;\n`;
+    const { container, rerender } = render(<MarkdownAnswer 
content={`\`\`\`sql\n${initialSource}`} />);
+
+    expect(screen.getByText('sql')).toBeTruthy();
+    expect(container.querySelector('pre 
code')!.textContent).toBe(initialSource);
+
+    rerender(<MarkdownAnswer content={`\`\`\`sql\n${extendedSource}`} />);
+    expect(container.querySelector('pre 
code')!.textContent).toBe(extendedSource);
+
+    rerender(<MarkdownAnswer 
content={`\`\`\`sql\n${completedSource}\`\`\`\n\nRevenue is grouped by 
product.`} />);
+    expect(container.querySelector('pre 
code')!.textContent).toBe(completedSource);
+    expect(screen.getByText('Revenue is grouped by 
product.').tagName).toBe('P');
+  });
+
+  it.each([
+    { language: '', label: 'Code' },
+    { language: 'unknown-lang', label: 'unknown-lang' }
+  ])('preserves source text for a "$language" code fence', ({ language, label 
}) => {
+    const source = '<widget onclick="run()"> & value\n  keep indentation\n';
+    const { container } = render(<MarkdownAnswer 
content={`\`\`\`${language}\n${source}\`\`\``} />);
+
+    expect(screen.getByText(label)).toBeTruthy();
+    expect(container.querySelector('pre code')!.textContent).toBe(source);
+    expect(container.querySelector('widget')).toBeNull();
+  });
+
+  it.each([
+    { form: 'ordinary text', content: `See ${paragraphId} for the query.` },
+    { form: 'inline code', content: `See \`${paragraphId}\` for the query.` },
+    { form: 'explicit paragraph link', content: `[Open the 
query](#${paragraphId})` },
+    { form: 'positive suffix', content: `See ${positiveParagraphId}.`, id: 
positiveParagraphId }
+  ])('opens a known paragraph from $form without creating a navigation link', 
({ content, id = paragraphId }) => {
+    const onOpenParagraph = vi.fn();
+    render(
+      <MarkdownAnswer content={content} describeParagraph={describeParagraph} 
onOpenParagraph={onOpenParagraph} />
+    );
+
+    const button = screen.getByRole('button', { name: 'Paragraph 1: show Sales 
query in the notebook' });
+    expect(button.textContent).toBe('Paragraph 1');
+    expect(button.getAttribute('type')).toBe('button');
+    expect(screen.queryByRole('link')).toBeNull();
+
+    fireEvent.click(button);
+    expect(onOpenParagraph).toHaveBeenCalledExactlyOnceWith(id, 'Sales query');
+  });
+
+  it('uses the latest paragraph callback when the answer content is 
unchanged', () => {
+    const content = `See ${paragraphId}.`;
+    const originalCallback = vi.fn();
+    const updatedCallback = vi.fn();
+    const { rerender } = render(
+      <MarkdownAnswer content={content} describeParagraph={describeParagraph} 
onOpenParagraph={originalCallback} />
+    );
+
+    fireEvent.click(screen.getByRole('button', { name: 'Paragraph 1: show 
Sales query in the notebook' }));
+    expect(originalCallback).toHaveBeenCalledExactlyOnceWith(paragraphId, 
'Sales query');
+
+    rerender(
+      <MarkdownAnswer content={content} describeParagraph={describeParagraph} 
onOpenParagraph={updatedCallback} />
+    );
+
+    fireEvent.click(screen.getByRole('button', { name: 'Paragraph 1: show 
Sales query in the notebook' }));
+    expect(updatedCallback).toHaveBeenCalledExactlyOnceWith(paragraphId, 
'Sales query');
+    expect(originalCallback).toHaveBeenCalledTimes(1);
+  });
+
+  it('updates paragraph labels and availability without changing the answer 
content', () => {
+    const content = `See ${paragraphId}.`;
+    const onOpenParagraph = vi.fn();
+    const { container, rerender } = render(
+      <MarkdownAnswer content={content} describeParagraph={describeParagraph} 
onOpenParagraph={onOpenParagraph} />
+    );
+    expect(screen.getByRole('button', { name: 'Paragraph 1: show Sales query 
in the notebook' }).textContent).toBe(
+      'Paragraph 1'
+    );
+
+    rerender(
+      <MarkdownAnswer
+        content={content}
+        describeParagraph={() => ({ text: 'Updated paragraph', name: 'Updated 
query' })}
+        onOpenParagraph={onOpenParagraph}
+      />
+    );
+
+    expect(screen.queryByRole('button', { name: 'Paragraph 1: show Sales query 
in the notebook' })).toBeNull();
+    const button = screen.getByRole('button', { name: 'Updated paragraph: show 
Updated query in the notebook' });
+    expect(button.textContent).toBe('Updated paragraph');
+    fireEvent.click(button);
+    expect(onOpenParagraph).toHaveBeenCalledExactlyOnceWith(paragraphId, 
'Updated query');
+
+    rerender(
+      <MarkdownAnswer content={content} describeParagraph={() => undefined} 
onOpenParagraph={onOpenParagraph} />
+    );
+
+    expect(screen.queryByRole('button')).toBeNull();
+    expect(container.textContent).toBe(`See ${paragraphId}.`);
+  });
+
+  it('leaves fenced references and ordinary link labels untouched', () => {
+    const source = `${paragraphId}\n`;
+    const { container } = render(
+      <MarkdownAnswer
+        
content={`\`\`\`text\n${source}\`\`\`\n\n[${paragraphId}](https://example.com/query)`}
+        describeParagraph={describeParagraph}
+        onOpenParagraph={vi.fn()}
+      />
+    );
+
+    expect(container.querySelector('pre code')!.textContent).toBe(source);
+    expect(screen.getByRole('link', { name: paragraphId 
}).getAttribute('href')).toBe('https://example.com/query');
+    expect(screen.queryByRole('button')).toBeNull();
+  });
+
+  it('does not rewrite partial or numbered IDs, malformed IDs or larger 
inline-code values', () => {
+    const content = [
+      'Paragraph #3 and paragraph 3',
+      'paragraph_1',
+      'paragraph_1423500782552_',
+      'paragraph_1423500782552_-',
+      'paragraph_1423500782552_--1439281894',
+      `prefix${paragraphId}`,
+      `${paragraphId}suffix`,
+      `\`before ${paragraphId} after\``
+    ].join('\n\n');
+    const { container } = render(
+      <MarkdownAnswer content={content} describeParagraph={describeParagraph} 
onOpenParagraph={vi.fn()} />
+    );
+
+    expect(screen.queryByRole('button')).toBeNull();
+    expect(container.textContent).toContain('paragraph_1');
+    
expect(container.textContent).toContain('paragraph_1423500782552_--1439281894');
+    expect(container.textContent).toContain(`prefix${paragraphId}`);
+    expect(container.textContent).toContain(`${paragraphId}suffix`);
+    expect(screen.getByText(`before ${paragraphId} 
after`).tagName).toBe('CODE');
+  });
+
+  it('does not turn parts of Unicode identifiers into paragraph references', 
() => {
+    const content = [
+      `๐€${paragraphId}`,
+      `${paragraphId}๐€`,
+      `๐Ÿ™${paragraphId}`,
+      `${paragraphId}๐Ÿ™`,
+      `e\u0301${paragraphId}`,
+      `${paragraphId}\u0301`
+    ].join(' ');
+    const { container } = render(
+      <MarkdownAnswer content={content} describeParagraph={describeParagraph} 
onOpenParagraph={vi.fn()} />
+    );
+
+    expect(screen.queryByRole('button')).toBeNull();
+    expect(container.textContent).toBe(content);
+  });
+
+  it('preserves punctuation around adjacent references and reports each 
paragraph ID', () => {
+    const onOpenParagraph = vi.fn();
+    const { container } = render(
+      <MarkdownAnswer
+        content={`See ๐Ÿงช(${paragraphId}),${positiveParagraphId}; done.`}
+        describeParagraph={describeParagraph}
+        onOpenParagraph={onOpenParagraph}
+      />
+    );
+
+    expect(container.textContent).toBe('See ๐Ÿงช(Paragraph 1),Paragraph 1; 
done.');
+    const buttons = screen.getAllByRole('button', { name: 'Paragraph 1: show 
Sales query in the notebook' });
+    fireEvent.click(buttons[0]);
+    fireEvent.click(buttons[1]);
+    expect(onOpenParagraph).toHaveBeenNthCalledWith(1, paragraphId, 'Sales 
query');
+    expect(onOpenParagraph).toHaveBeenNthCalledWith(2, positiveParagraphId, 
'Sales query');
+  });
+
+  it('handles references inside nested Markdown and tables without rewriting 
source code or reference-link labels', () => {
+    const onOpenParagraph = vi.fn();
+    const source = `${paragraphId} ${positiveParagraphId}\n`;
+    const content = [
+      `> - **${paragraphId}**`,
+      '',
+      '| Query | Revenue |',
+      '| --- | ---: |',
+      `| \`${positiveParagraphId}\` | 1200 |`,
+      '',
+      `[${paragraphId}][source]`,
+      '',
+      '[source]: https://example.com/query',
+      '',
+      `\`\`\`text\n${source}\`\`\``
+    ].join('\n');
+    const { container } = render(
+      <MarkdownAnswer content={content} describeParagraph={describeParagraph} 
onOpenParagraph={onOpenParagraph} />
+    );
+
+    fireEvent.click(
+      within(screen.getByRole('list')).getByRole('button', { name: 'Paragraph 
1: show Sales query in the notebook' })
+    );
+    fireEvent.click(
+      within(screen.getByRole('table')).getByRole('button', { name: 'Paragraph 
1: show Sales query in the notebook' })
+    );
+    expect(onOpenParagraph).toHaveBeenNthCalledWith(1, paragraphId, 'Sales 
query');
+    expect(onOpenParagraph).toHaveBeenNthCalledWith(2, positiveParagraphId, 
'Sales query');
+    expect(container.querySelector('pre code')!.textContent).toBe(source);
+    expect(screen.getByRole('link', { name: paragraphId 
}).getAttribute('href')).toBe('https://example.com/query');
+  });
+
+  it('keeps paragraph descriptions and callbacks isolated between answers', () 
=> {
+    const salesCallback = vi.fn();
+    const inventoryCallback = vi.fn();
+    render(
+      <>
+        <MarkdownAnswer content={paragraphId} 
describeParagraph={describeParagraph} onOpenParagraph={salesCallback} />
+        <MarkdownAnswer
+          content={paragraphId}
+          describeParagraph={() => ({ text: 'Paragraph 2', name: 'Inventory 
query' })}
+          onOpenParagraph={inventoryCallback}
+        />
+      </>
+    );
+
+    fireEvent.click(screen.getByRole('button', { name: 'Paragraph 1: show 
Sales query in the notebook' }));
+    expect(salesCallback).toHaveBeenCalledExactlyOnceWith(paragraphId, 'Sales 
query');
+    expect(inventoryCallback).not.toHaveBeenCalled();
+    fireEvent.click(screen.getByRole('button', { name: 'Paragraph 2: show 
Inventory query in the notebook' }));
+    expect(inventoryCallback).toHaveBeenCalledExactlyOnceWith(paragraphId, 
'Inventory query');
+    expect(salesCallback).toHaveBeenCalledTimes(1);
+  });
+
+  it('keeps unknown paragraph references inactive without disabling ordinary 
anchors', () => {
+    const unknownId = 'paragraph_1423500782554_-99';
+    const { container } = render(
+      <MarkdownAnswer
+        content={`${unknownId}\n\n\`${unknownId}\`\n\n[Unknown 
query](#${unknownId})\n\n[Other section](#section)`}
+        describeParagraph={describeParagraph}
+        onOpenParagraph={vi.fn()}
+      />
+    );
+
+    expect(screen.queryByRole('button')).toBeNull();
+    expect(screen.queryByRole('link', { name: 'Unknown query' })).toBeNull();
+    const anchor = screen.getByRole('link', { name: 'Other section' });
+    expect(anchor.getAttribute('href')).toBe('#section');
+    expect(anchor.getAttribute('target')).toBeNull();
+    expect(container.textContent).toContain(unknownId);
+    expect(screen.getByText('Unknown query')).toBeTruthy();
+    expect(screen.getByText('Other section')).toBeTruthy();
+  });
+
+  it('leaves known references as text when the paragraph callback is absent', 
() => {
+    const { container } = render(
+      <MarkdownAnswer
+        content={`${paragraphId}\n\n\`${paragraphId}\`\n\n[Open the 
query](#${paragraphId})`}
+        describeParagraph={describeParagraph}
+      />
+    );
+
+    expect(screen.queryByRole('button')).toBeNull();
+    expect(screen.queryByRole('link')).toBeNull();
+    expect(container.textContent).toContain(paragraphId);
+    expect(screen.getByText('Open the query')).toBeTruthy();
+  });
+
+  it('leaves references as text when no paragraph description is available', 
() => {
+    const { container } = render(
+      <MarkdownAnswer content={`${paragraphId}\n\n[Open the 
query](#${paragraphId})`} onOpenParagraph={vi.fn()} />
+    );
+
+    expect(screen.queryByRole('button')).toBeNull();
+    expect(screen.queryByRole('link')).toBeNull();
+    expect(container.textContent).toContain(paragraphId);
+    expect(screen.getByText('Open the query')).toBeTruthy();
+  });
+
+  it('escapes raw HTML including scripts, embedded content and event 
attributes', () => {
+    const payload = [
+      '<script>alert("unsafe")</script>',
+      '<img src="https://example.com/tracker.png"; onerror="alert(1)">',
+      '<svg onload="alert(1)"><circle /></svg>',
+      '<iframe src="https://example.com/frame";></iframe>',
+      '<button onclick="alert(1)">Run</button>'
+    ].join('\n\n');
+    const { container } = render(<MarkdownAnswer content={payload} />);
+
+    
expect(container.textContent).toContain('<script>alert("unsafe")</script>');
+    expect(container.textContent).toContain('onerror="alert(1)"');
+    expect(container.textContent).toContain('onload="alert(1)"');
+    expect(container.textContent).toContain('onclick="alert(1)"');
+    expect(container.querySelector('script, img, svg, iframe, 
button')).toBeNull();
+    expect(container.querySelector('[onclick], [onerror], 
[onload]')).toBeNull();
+  });
+
+  describe('footnotes', () => {
+    const content = 'Revenue grew[^1].\n\n[^1]: Source: finance.';
+    const renderTwo = () =>
+      render(
+        <>
+          <MarkdownAnswer content={content} />
+          <MarkdownAnswer content={content} />
+        </>
+      );
+
+    it('gives every element of two answers its own id, and points each 
reference at its own heading', () => {
+      const { container } = renderTwo();
+
+      const ids = Array.from(container.querySelectorAll('[id]')).map(element 
=> element.id);
+      expect(ids.length).toBeGreaterThan(0);
+      expect(new Set(ids).size).toBe(ids.length);
+      const references = 
Array.from(container.querySelectorAll('a[data-footnote-ref]'));
+      expect(references).toHaveLength(2);
+      for (const reference of references) {
+        const heading = 
container.querySelector(`[id="${reference.getAttribute('aria-describedby')}"]`);
+        expect(heading?.textContent).toBe('Footnotes');
+        expect(heading?.closest('section')?.contains(reference)).toBe(false);
+      }
+      
expect(references[0].getAttribute('aria-describedby')).not.toBe(references[1].getAttribute('aria-describedby'));
+    });
+
+    it('hides the heading visually, and shows a footnote and moves focus to it 
without changing the URL hash', () => {
+      const { container } = renderTwo();
+
+      
expect(screen.getAllByText('Footnotes')[0].className).toBe(visuallyHidden);
+      const [reference] = 
Array.from(container.querySelectorAll('a[data-footnote-ref]'));
+      const target = 
container.querySelector(`[id="${reference.getAttribute('href')!.slice(1)}"]`) 
as HTMLElement;
+      const scrollIntoView = vi.fn();
+      target.scrollIntoView = scrollIntoView;
+      const hash = window.location.hash;
+
+      expect(fireEvent.click(reference)).toBe(false);
+
+      expect(scrollIntoView).toHaveBeenCalledOnce();
+      expect(document.activeElement).toBe(target);
+      expect(window.location.hash).toBe(hash);
+    });
+
+    it('keeps footnote references keyboard reachable after following a 
back-reference', () => {
+      const { container } = renderTwo();
+      const reference = 
container.querySelectorAll<HTMLAnchorElement>('a[data-footnote-ref]')[1];
+      const footnote = 
container.querySelector<HTMLElement>(`[id="${reference.getAttribute('href')!.slice(1)}"]`)!;
+      const backReference = 
footnote.querySelector<HTMLAnchorElement>('a[data-footnote-backref]')!;
+      const hash = window.location.hash;
+
+      fireEvent.click(reference);
+      expect(document.activeElement).toBe(footnote);
+      fireEvent.click(backReference);
+
+      expect(document.activeElement).toBe(reference);
+      expect(reference.tabIndex).toBe(0);
+      expect(window.location.hash).toBe(hash);
+    });
+  });
+
+  it.each(dangerousUrls)('renders the rejected URL %s as non-navigable text', 
url => {
+    const { container } = render(<MarkdownAnswer content={`[Unsafe 
link](${url})`} />);
+
+    expect(screen.getByText('Unsafe link')).toBeTruthy();
+    expect(screen.queryByRole('link')).toBeNull();
+    expect(container.querySelector('a, [href]')).toBeNull();
+  });
+
+  it('preserves safe HTTPS links with a protected new-tab target', () => {
+    render(<MarkdownAnswer content={'[Zeppelin 
documentation](https://zeppelin.apache.org/docs/latest/)'} />);
+
+    const link = screen.getByRole('link', { name: 'Zeppelin documentation' });
+    
expect(link.getAttribute('href')).toBe('https://zeppelin.apache.org/docs/latest/');
+    expect(link.getAttribute('target')).toBe('_blank');
+    
expect(link.getAttribute('rel')?.split(/\s+/)).toEqual(expect.arrayContaining(['noopener',
 'noreferrer']));
+  });
+});
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/MarkdownAnswer.stories.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/MarkdownAnswer.stories.tsx
new file mode 100644
index 0000000000..58aa97396d
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/MarkdownAnswer.stories.tsx
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useCallback, useState } from 'react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import { fn } from 'storybook/test';
+import { MarkdownAnswer, type MarkdownAnswerProps } from './MarkdownAnswer';
+
+const paragraphId = 'paragraph_1423500782552_-1439281894';
+const describeParagraph = (id: string) =>
+  id === paragraphId ? { text: 'Paragraph 1', name: 'Sales query' } : 
undefined;
+
+const streamingChunks = [
+  '## Sales summary\n\n```sql\nSELECT product,',
+  ' SUM(revenue) AS total_revenue\nFROM sales\n',
+  'GROUP BY product;\n```\n\nRevenue is grouped by product.'
+];
+
+const ParagraphReferenceExample = ({ onOpenParagraph, ...args }: 
MarkdownAnswerProps) => {
+  const [selectedParagraph, setSelectedParagraph] = useState('No paragraph 
selected.');
+
+  const handleOpenParagraph = useCallback(
+    (id: string, label: string) => {
+      onOpenParagraph?.(id, label);
+      setSelectedParagraph(`Selected ${label} (${id}).`);
+    },
+    [onOpenParagraph]
+  );
+
+  return (
+    <div>
+      <MarkdownAnswer {...args} onOpenParagraph={handleOpenParagraph} />
+      <p role="status">{selectedParagraph}</p>
+    </div>
+  );
+};
+
+const StreamingExample = (args: MarkdownAnswerProps) => {
+  const [step, setStep] = useState(0);
+  const completed = step === streamingChunks.length - 1;
+  const content = streamingChunks.slice(0, step + 1).join('');
+
+  return (
+    <div>
+      <button type="button" disabled={completed} onClick={() => 
setStep(current => current + 1)}>
+        {completed ? 'Answer complete' : 'Append next chunk'}
+      </button>
+      <button type="button" disabled={step === 0} onClick={() => setStep(0)} 
style={{ marginLeft: 8 }}>
+        Restart stream
+      </button>
+      <p role="status">
+        Chunk {step + 1} of {streamingChunks.length}
+      </p>
+      <MarkdownAnswer {...args} content={content} />
+    </div>
+  );
+};
+
+const meta = {
+  title: 'Assistant/MarkdownAnswer',
+  component: MarkdownAnswer,
+  args: { content: '' },
+  parameters: {
+    docs: {
+      description: {
+        component:
+          'Safe Markdown answers in the assistant sidebar. Tables and code 
scroll within the answer; paragraph references invoke a callback instead of 
navigating.'
+      }
+    }
+  }
+} satisfies Meta<typeof MarkdownAnswer>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+export const CompletedAnswer: Story = {
+  render: args => <ParagraphReferenceExample {...args} />,
+  args: {
+    content: [
+      '### Regional sales',
+      '',
+      'Northern Europe generated the highest revenue.',
+      '',
+      '| Region | Revenue |',
+      '| :--- | ---: |',
+      '| Northern Europe | 4200 |',
+      '| Asia Pacific | 2100 |',
+      '| North America | 1600 |',
+      '',
+      'Use this query to compare the regions:',
+      '',
+      '```sql',
+      `-- Source: ${paragraphId}`,
+      'SELECT sales_region, SUM(net_revenue_amount) AS revenue',
+      'FROM analytics.sales_transactions',
+      'GROUP BY sales_region;',
+      '```',
+      '',
+      `> [Inspect the source query](#${paragraphId}) before comparing other 
quarters.`,
+      '',
+      '[Zeppelin documentation](https://zeppelin.apache.org/docs/latest/)'
+    ].join('\n'),
+    describeParagraph,
+    onOpenParagraph: fn()
+  }
+};
+
+export const GfmTable: Story = {
+  decorators: [
+    Story => (
+      <div style={{ width: 280, maxWidth: '100%' }}>
+        <Story />
+      </div>
+    )
+  ],
+  args: {
+    content: [
+      '### Regional sales',
+      '',
+      'Revenue by product and region:',
+      '',
+      '| Product | Region | Quarter | Revenue | Orders | Average order | 
Source dataset |',
+      '| :--- | :---: | :---: | ---: | ---: | ---: | :--- |',
+      '| Enterprise notebook subscription | Northern Europe | Q1 | 128000 | 
320 | 400 | analytics.sales_transactions_2026_q1 |',
+      '| Managed query workspace | Asia Pacific | Q2 | 96000 | 240 | 400 | 
analytics.sales_transactions_2026_q2 |',
+      '| Interactive report package | North America | Q3 | 144000 | 360 | 400 
| analytics.sales_transactions_2026_q3 |',
+      '',
+      'Interactive reports generated the highest revenue.'
+    ].join('\n')
+  }
+};
+
+export const SqlCode: Story = {
+  args: {
+    content: [
+      '### Revenue query',
+      '',
+      'Use this query to compare products and regions for the first quarter:',
+      '',
+      '```sql',
+      'SELECT product_name, sales_region, fiscal_quarter, 
SUM(net_revenue_amount) AS total_net_revenue, COUNT(DISTINCT 
customer_account_identifier) AS distinct_customer_accounts',
+      'FROM analytics.sales_transactions',
+      "WHERE fiscal_quarter = '2026-Q1'",
+      'GROUP BY product_name, sales_region, fiscal_quarter;',
+      '```',
+      '',
+      'Run the query in a notebook paragraph to inspect the result.'
+    ].join('\n')
+  }
+};
+
+export const PartialFence: Story = {
+  args: {
+    content: [
+      '### Revenue query',
+      '',
+      '```sql',
+      'SELECT product, SUM(revenue) AS total_revenue',
+      'FROM sales',
+      'WHERE',
+      ''
+    ].join('\n')
+  }
+};
+
+export const ParagraphReference: Story = {
+  render: args => <ParagraphReferenceExample {...args} />,
+  args: {
+    content: [
+      '### Source query',
+      '',
+      `The revenue calculation uses ${paragraphId}.`,
+      '',
+      'The earlier query paragraph_1423500782554_-99 is no longer available.',
+      '',
+      '[Zeppelin documentation](https://zeppelin.apache.org/docs/latest/)'
+    ].join('\n'),
+    describeParagraph,
+    onOpenParagraph: fn()
+  }
+};
+
+export const DangerousContent: Story = {
+  args: {
+    content: [
+      '### Untrusted answer content',
+      '',
+      '<script>alert("This must stay text")</script>',
+      '',
+      '<img src="https://example.com/tracker.png"; onerror="alert(1)">',
+      '',
+      '<button onclick="alert(1)">This is not an executable button</button>',
+      '',
+      '[JavaScript link](javascript:alert%281%29)',
+      '',
+      '[Safe Zeppelin documentation](https://zeppelin.apache.org/docs/latest/)'
+    ].join('\n')
+  }
+};
+
+export const Footnotes: Story = {
+  args: {
+    content: [
+      '### Revenue',
+      '',
+      'Northern Europe generated the highest revenue[^1], ahead of Asia 
Pacific[^2].',
+      '',
+      '[^1]: Source: the finance warehouse, closed quarters only.',
+      '[^2]: Converted at the quarter-end exchange rate.'
+    ].join('\n')
+  }
+};
+
+export const Streaming: Story = {
+  render: args => <StreamingExample {...args} />,
+  args: { content: streamingChunks[0] },
+  argTypes: { content: { control: false } },
+  parameters: {
+    docs: {
+      description: {
+        story: 'Append chunks to an unclosed SQL fence, complete the answer, 
or restart the stream.'
+      }
+    }
+  }
+};
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/MarkdownAnswer.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/MarkdownAnswer.tsx
new file mode 100644
index 0000000000..b5c69baf09
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/MarkdownAnswer.tsx
@@ -0,0 +1,219 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createContext, useContext, useId, useMemo, type 
ComponentPropsWithoutRef, type MouseEvent } from 'react';
+import Markdown, { type Components, type ExtraProps } from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+import type { Link, Parent, Root, Text } from 'mdast';
+import type { Plugin, PluggableList } from 'unified';
+
+import { visuallyHidden } from 
'@/shared/ui/assistant-theme/AssistantScope.css';
+import * as styles from './MarkdownAnswer.css';
+
+export interface MarkdownAnswerProps {
+  /** Markdown text of the answer. While it is still arriving, it may end 
inside a code fence. */
+  content: string;
+  /** Describes a paragraph the answer refers to by ID, for its button. A 
reference it cannot describe is left as written, without a button. */
+  describeParagraph?: (paragraphId: string) => { text: string; name: string } 
| undefined;
+  /** Called when a paragraph button is clicked. Without it, references stay 
plain text. */
+  onOpenParagraph?: (paragraphId: string, label: string) => void;
+}
+
+type AnswerContextValue = Pick<MarkdownAnswerProps, 'describeParagraph' | 
'onOpenParagraph'> & {
+  /** Prefix of the footnote ids of this answer, so that two answers on one 
page do not share them. */
+  footnotePrefix: string;
+};
+
+const AnswerContext = createContext<AnswerContextValue>({ footnotePrefix: '' 
});
+const paragraphIdPattern = /^paragraph_\d+_-?\d+$/;
+
+const textReferences = (node: Text): Array<Text | Link> | undefined => {
+  // Match a complete ID without splitting Unicode letters, numbers or 
combining marks.
+  const pattern = new 
RegExp('(^|[^\\p{L}\\p{N}\\p{M}_+-])(paragraph_\\d+_-?\\d+)(?![\\p{L}\\p{N}\\p{M}_+-])',
 'gu');
+  let references: Array<Text | Link> | undefined;
+  let offset = 0;
+  let match: RegExpExecArray | null;
+
+  while ((match = pattern.exec(node.value)) !== null) {
+    const id = match[2];
+    const start = match.index + match[1].length;
+    references ??= [];
+    if (start > offset) {
+      references.push({ type: 'text', value: node.value.slice(offset, start) 
});
+    }
+    references.push({ type: 'link', url: `#${id}`, children: [{ type: 'text', 
value: id }] });
+    offset = start + id.length;
+  }
+
+  if (references && offset < node.value.length) {
+    references.push({ type: 'text', value: node.value.slice(offset) });
+  }
+  return references;
+};
+
+const transformReferences = (parent: Parent): void => {
+  let children: Parent['children'] | undefined;
+
+  for (let index = 0; index < parent.children.length; index++) {
+    const child = parent.children[index];
+    let references: Array<Text | Link> | undefined;
+    if (child.type === 'text') {
+      references = textReferences(child);
+    } else if (child.type === 'inlineCode' && 
paragraphIdPattern.test(child.value)) {
+      references = [{ type: 'link', url: `#${child.value}`, children: [child] 
}];
+    } else if (child.type !== 'link' && child.type !== 'linkReference' && 
'children' in child) {
+      transformReferences(child);
+    }
+
+    if (references) {
+      children ??= parent.children.slice(0, index);
+      children.push(...references);
+    } else {
+      children?.push(child);
+    }
+  }
+
+  if (children) parent.children = children;
+};
+
+const remarkParagraphReferences: Plugin<[], Root> = () => transformReferences;
+const remarkPlugins: PluggableList = [remarkGfm, remarkParagraphReferences];
+
+// remark-rehype gives the footnotes heading this fixed id and points every 
footnote reference at it.
+const footnoteLabelId = 'footnote-label';
+
+const AnswerLink = ({ node: _node, href, children, title, ...props }: 
ComponentPropsWithoutRef<'a'> & ExtraProps) => {
+  const { describeParagraph, onOpenParagraph, footnotePrefix } = 
useContext(AnswerContext);
+
+  if (!href) return <>{children}</>;
+
+  const fragment = href.startsWith('#');
+  const paragraphId = fragment ? href.slice(1) : undefined;
+  if (paragraphId && paragraphIdPattern.test(paragraphId)) {
+    const description = describeParagraph?.(paragraphId);
+    if (description && onOpenParagraph) {
+      return (
+        <button
+          type="button"
+          className={styles.paragraphReference}
+          aria-label={`${description.text}: show ${description.name} in the 
notebook`}
+          onClick={() => onOpenParagraph(paragraphId, description.name)}
+        >
+          {description.text}
+        </button>
+      );
+    }
+    return <>{children}</>;
+  }
+
+  // The URL hash belongs to the router, so show the target in the page 
instead of navigating to it.
+  const showTarget = (event: MouseEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    const target = document.getElementById(href.slice(1));
+    if (!target) return;
+    target.scrollIntoView?.({ block: 'nearest' });
+    // Preserve existing tab stops when moving keyboard focus to the target.
+    if (target.tabIndex < 0 && !target.hasAttribute('tabindex')) 
target.tabIndex = -1;
+    target.focus({ preventScroll: true });
+  };
+
+  // react-markdown has already applied its default URL safety transform.
+  return (
+    <a
+      {...props}
+      aria-describedby={
+        props['aria-describedby'] === footnoteLabelId
+          ? `${footnotePrefix}${footnoteLabelId}`
+          : props['aria-describedby']
+      }
+      href={href}
+      onClick={fragment ? showTarget : undefined}
+      title={title}
+      target={fragment ? undefined : '_blank'}
+      rel={fragment ? undefined : 'noopener noreferrer'}
+    >
+      {children}
+    </a>
+  );
+};
+
+const AnswerCodeBlock = ({ node, children, ...props }: 
ComponentPropsWithoutRef<'pre'> & ExtraProps) => {
+  const code = node?.children.find(child => child.type === 'element' && 
child.tagName === 'code');
+  const classNames = code?.type === 'element' ? code.properties.className : 
undefined;
+  const languageClass = Array.isArray(classNames)
+    ? classNames.find(className => typeof className === 'string' && 
className.startsWith('language-'))
+    : undefined;
+  const language = typeof languageClass === 'string' ? 
languageClass.slice('language-'.length) : 'Code';
+
+  return (
+    <div className={styles.codeBlock}>
+      <div className={styles.codeLanguage}>{language}</div>
+      <pre {...props} className={styles.codeSource} tabIndex={0} role="region" 
aria-label={`${language} block`}>
+        {children}
+      </pre>
+    </div>
+  );
+};
+
+const AnswerTable = ({ node: _node, children, ...props }: 
ComponentPropsWithoutRef<'table'> & ExtraProps) => {
+  return (
+    <div className={styles.tableScroll} role="region" aria-label="Scrollable 
table" tabIndex={0}>
+      <table {...props}>{children}</table>
+    </div>
+  );
+};
+
+const AnswerHeading = ({ node: _node, id, ...props }: 
ComponentPropsWithoutRef<'h2'> & ExtraProps) => {
+  const { footnotePrefix } = useContext(AnswerContext);
+  return <h2 {...props} id={id === footnoteLabelId ? `${footnotePrefix}${id}` 
: id} />;
+};
+
+const components: Components = {
+  h2: AnswerHeading,
+  a: AnswerLink,
+  pre: AnswerCodeBlock,
+  table: AnswerTable
+};
+
+/** Renders the Markdown of an assistant answer. HTML is shown as text and 
unsafe link URLs are removed. */
+export const MarkdownAnswer = ({ content, describeParagraph, onOpenParagraph 
}: MarkdownAnswerProps) => {
+  // Footnote ids must differ between the answers on one page.
+  const footnotePrefix = `${useId()}-`;
+  // The whole answer is parsed again each time this renders. Keying the 
parsed element on the text means typing in a
+  // sibling input does not parse it.
+  const markdown = useMemo(
+    () => (
+      <Markdown
+        remarkPlugins={remarkPlugins}
+        components={components}
+        remarkRehypeOptions={{
+          clobberPrefix: footnotePrefix,
+          footnoteLabelProperties: { className: [visuallyHidden] }
+        }}
+      >
+        {content}
+      </Markdown>
+    ),
+    [content, footnotePrefix]
+  );
+
+  return (
+    <AnswerContext.Provider value={{ describeParagraph, onOpenParagraph, 
footnotePrefix }}>
+      <div className={styles.answer}>{markdown}</div>
+    </AnswerContext.Provider>
+  );
+};

Reply via email to