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',
+ 'javascript: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>
+ );
+};