From 1cd3c75f03c1e5601e41fd781e4e5d901d8de509 Mon Sep 17 00:00:00 2001 From: Artem Belik Date: Thu, 6 Aug 2026 12:36:27 +0300 Subject: [PATCH 1/3] feat(components): added Markdown component (#DS-5225) --- package.json | 2 + packages/components/package.json | 14 +- .../src/components/Markdown/Markdown.mdx | 103 ++++++ .../components/Markdown/Markdown.module.css | 294 ++++++++++++++++++ .../components/Markdown/Markdown.stories.tsx | 250 +++++++++++++++ .../src/components/Markdown/Markdown.test.tsx | 102 ++++++ .../src/components/Markdown/Markdown.tsx | 104 +++++++ .../src/components/Markdown/index.ts | 2 + .../src/components/Markdown/types.ts | 14 + packages/components/src/components/index.ts | 1 + pnpm-lock.yaml | 67 +++- tools/api-extractor/config.json | 1 + .../components/Markdown.api.md | 23 ++ 13 files changed, 959 insertions(+), 18 deletions(-) create mode 100644 packages/components/src/components/Markdown/Markdown.mdx create mode 100644 packages/components/src/components/Markdown/Markdown.module.css create mode 100644 packages/components/src/components/Markdown/Markdown.stories.tsx create mode 100644 packages/components/src/components/Markdown/Markdown.test.tsx create mode 100644 packages/components/src/components/Markdown/Markdown.tsx create mode 100644 packages/components/src/components/Markdown/index.ts create mode 100644 packages/components/src/components/Markdown/types.ts create mode 100644 tools/public_api_guard/components/Markdown.api.md diff --git a/package.json b/package.json index 32c5a372..650e0984 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "chalk": "^4.1.2", "commit-and-tag-version": "^12.5.0", "country-flag-icons": "^1.6.20", + "dompurify": "^3.4.13", "dotenv": "^16.4.7", "eslint": "^9.39.4", "eslint-config-prettier": "^9.1.2", @@ -92,6 +93,7 @@ "husky": "^9.1.6", "jsdom": "^25.0.1", "lightningcss": "^1.29.3", + "marked": "^18.0.9", "nano-staged": "^1.0.2", "postcss-lightningcss": "^1.1.0", "postcss-mixins": "^11.0.3", diff --git a/packages/components/package.json b/packages/components/package.json index 71a3caa6..35f6cdb1 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -30,19 +30,29 @@ }, "sideEffects": false, "dependencies": { + "@internationalized/date": "^3.12.0", "@koobiq/design-tokens": "^3.17.2", "@koobiq/logger": "workspace:*", "@koobiq/react-core": "workspace:*", "@koobiq/react-icons": "^12.0.0", "@koobiq/react-primitives": "workspace:*", "@types/react-transition-group": "^4.4.12", - "react-transition-group": "^4.4.5", - "@internationalized/date": "^3.12.0" + "react-transition-group": "^4.4.5" }, "peerDependencies": { "@koobiq/design-tokens": "^3.17.2", "@koobiq/react-icons": "^12.0.0", + "dompurify": "^3.4.13", + "marked": "^18.0.9", "react": "18.x || 19.x", "react-dom": "18.x || 19.x" + }, + "peerDependenciesMeta": { + "dompurify": { + "optional": true + }, + "marked": { + "optional": true + } } } diff --git a/packages/components/src/components/Markdown/Markdown.mdx b/packages/components/src/components/Markdown/Markdown.mdx new file mode 100644 index 00000000..a1c52fb4 --- /dev/null +++ b/packages/components/src/components/Markdown/Markdown.mdx @@ -0,0 +1,103 @@ +import { + Meta, + Story, + Props, + Status, +} from '../../../../../.storybook/components'; + +import * as Stories from './Markdown.stories'; + + + +# Markdown + + + +`Markdown` renders a Markdown string as HTML. + +For the component to work, install the [`marked`](https://www.npmjs.com/package/marked) and +[`dompurify`](https://www.npmjs.com/package/dompurify) peer dependencies: + +```bash +npm install marked dompurify +``` + +## Import + +```tsx +import { Markdown } from '@koobiq/react-components'; +``` + +## Usage + +Pass the Markdown text as `children`. + + + +## Headers + + + +### Headers combinations + + + +## Paragraph + + + +## Text emphasis + + + +## Blockquote + + + +## Lists + + + +## Inline code + + + +## Code block + + + +## Horizontal rule + + + +## Link + + + +## Image + + + +## Table + + + +## Line break + +To create a line break, add two trailing spaces at the end of the line. + + + +Alternatively, pass `{ breaks: true }` via `markedOptions` and use a plain `\n` in the text. + + + +## Large article + +A larger combined example, useful as a visual sanity check for spacing and typography. + + + +## Props + + diff --git a/packages/components/src/components/Markdown/Markdown.module.css b/packages/components/src/components/Markdown/Markdown.module.css new file mode 100644 index 00000000..f3ba32a6 --- /dev/null +++ b/packages/components/src/components/Markdown/Markdown.module.css @@ -0,0 +1,294 @@ +.markdown { + display: block; + color: var(--kbq-foreground-contrast); + background: var(--kbq-markdown-bg, transparent); +} + +.markdownOutput > :first-child { + margin-block-start: 0; +} + +.markdownOutput > :last-child { + margin-block-end: 0; +} + +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + color: var(--kbq-foreground-contrast); +} + +:is(.h1, .h2, .h3, .h4, .h5, .h6) + :is(.h1, .h2, .h3, .h4, .h5, .h6) { + margin-block-start: 0; +} + +.h1 { + margin-block: var(--kbq-markdown-h1-size-margin-top) + var(--kbq-markdown-h1-size-margin-bottom); + font-size: var(--kbq-md-typography-md-h1-font-size); + line-height: var(--kbq-md-typography-md-h1-line-height); + font-weight: var(--kbq-md-typography-md-h1-font-weight); + letter-spacing: var(--kbq-md-typography-md-h1-letter-spacing); +} + +.h2 { + margin-block: var(--kbq-markdown-h2-size-margin-top) + var(--kbq-markdown-h2-size-margin-bottom); + font-size: var(--kbq-md-typography-md-h2-font-size); + line-height: var(--kbq-md-typography-md-h2-line-height); + font-weight: var(--kbq-md-typography-md-h2-font-weight); + letter-spacing: var(--kbq-md-typography-md-h2-letter-spacing); +} + +.h3 { + margin-block: var(--kbq-markdown-h3-size-margin-top) + var(--kbq-markdown-h3-size-margin-bottom); + font-size: var(--kbq-md-typography-md-h3-font-size); + line-height: var(--kbq-md-typography-md-h3-line-height); + font-weight: var(--kbq-md-typography-md-h3-font-weight); + letter-spacing: var(--kbq-md-typography-md-h3-letter-spacing); +} + +.h4 { + margin-block: var(--kbq-markdown-h4-size-margin-top) + var(--kbq-markdown-h4-size-margin-bottom); + font-size: var(--kbq-md-typography-md-h4-font-size); + line-height: var(--kbq-md-typography-md-h4-line-height); + font-weight: var(--kbq-md-typography-md-h4-font-weight); + letter-spacing: var(--kbq-md-typography-md-h4-letter-spacing); +} + +.h5 { + margin-block: var(--kbq-markdown-h5-size-margin-top) + var(--kbq-markdown-h5-size-margin-bottom); + font-size: var(--kbq-md-typography-md-h5-font-size); + line-height: var(--kbq-md-typography-md-h5-line-height); + font-weight: var(--kbq-md-typography-md-h5-font-weight); + letter-spacing: var(--kbq-md-typography-md-h5-letter-spacing); +} + +.h6 { + margin-block: var(--kbq-markdown-h6-size-margin-top) + var(--kbq-markdown-h6-size-margin-bottom); + font-size: var(--kbq-md-typography-md-h6-font-size); + line-height: var(--kbq-md-typography-md-h6-line-height); + font-weight: var(--kbq-md-typography-md-h6-font-weight); + letter-spacing: var(--kbq-md-typography-md-h6-letter-spacing); + text-transform: var(--kbq-md-typography-md-h6-text-transform); +} + +.p { + margin-block: var(--kbq-markdown-p-size-margin-top) + var(--kbq-markdown-p-size-margin-bottom); + color: var(--kbq-foreground-contrast); + font-size: var(--kbq-md-typography-md-body-font-size); + line-height: var(--kbq-md-typography-md-body-line-height); + letter-spacing: var(--kbq-md-typography-md-body-letter-spacing); +} + +.ul, +.ol { + margin-block: var(--kbq-markdown-list-size-margin-top) + var(--kbq-markdown-list-size-margin-bottom); + color: var(--kbq-foreground-contrast); + font-size: var(--kbq-md-typography-md-body-font-size); + line-height: var(--kbq-md-typography-md-body-line-height); +} + +.p + .ul, +.p + .ol { + margin-block-start: var(--kbq-markdown-list-size-margin-top-after-paragraph); +} + +.ul { + position: relative; + list-style: none; + padding-inline: var(--kbq-size-xxl) 0; + padding-block: 0; +} + +.ul > .li::before { + content: '—'; + display: block; + position: absolute; + inset-inline-start: 0; + inline-size: 24px; + text-align: start; +} + +.ol { + list-style: none; + padding: 0; + counter-reset: li; +} + +.ol > .li { + counter-increment: li; +} + +.ol > .li::before { + content: counter(li) '.'; + padding-inline-end: var(--kbq-markdown-list-size-ol-number-padding-right); +} + +.ol > .li > * { + display: inline-block; +} + +.li { + margin-block-end: var(--kbq-markdown-list-size-item-margin-bottom); +} + +.blockquote { + margin-block: var(--kbq-markdown-blockquote-size-margin-top) + var(--kbq-markdown-blockquote-size-margin-bottom); + margin-inline: 0; + padding: var(--kbq-markdown-blockquote-size-padding); + border-inline-start: var(--kbq-markdown-blockquote-size-line-width) solid + var(--kbq-markdown-blockquote-line); + color: var(--kbq-markdown-blockquote-text); + background: var(--kbq-markdown-blockquote-background); +} + +.blockquote > .p { + margin-block: 0; +} + +.pre { + position: relative; + overflow-x: auto; + margin-block: var(--kbq-markdown-code-size-multiline-margin-top) + var(--kbq-markdown-code-size-multiline-margin-bottom); + padding: var(--kbq-markdown-code-size-multiline-padding); + border-radius: var(--kbq-markdown-code-size-border-radius); + color: var(--kbq-markdown-code-text); + background-color: var(--kbq-markdown-code-background); +} + +.code { + border-radius: var(--kbq-markdown-code-size-border-radius); + font-family: var(--kbq-md-typography-md-inline-code-font-family); + font-size: var(--kbq-md-typography-md-inline-code-font-size); + line-height: var(--kbq-md-typography-md-inline-code-line-height); +} + +.code:not(.pre > .code) { + white-space: nowrap; + padding: var(--kbq-markdown-code-size-inline-padding); +} + +.code:not(.pre > .code, .a .code) { + color: var(--kbq-markdown-code-text); + background-color: var(--kbq-markdown-code-background); +} + +.a .code { + background-color: var(--kbq-markdown-code-background); + font-weight: var(--kbq-md-typography-md-inline-code-as-link-font-weight); +} + +.img { + max-inline-size: 100%; + margin-block: var(--kbq-markdown-image-size-margin-top) + var(--kbq-markdown-image-size-margin-bottom); +} + +.img + em { + display: block; + margin-block: var(--kbq-markdown-image-size-caption-margin-top) + var(--kbq-markdown-image-size-caption-margin-bottom); + color: var(--kbq-markdown-image-caption-text); + font-size: var(--kbq-md-typography-md-caption-font-size); + line-height: var(--kbq-md-typography-md-caption-line-height); +} + +.hr { + block-size: 0; + margin-block: var(--kbq-markdown-hr-size-margin-vertical); + border: none; + border-block-end: var(--kbq-markdown-hr-size-width) solid + var(--kbq-markdown-hr-color); +} + +.table { + max-inline-size: 100%; + overflow-x: auto; + display: table; + border-collapse: collapse; + margin-block-end: var(--kbq-markdown-table-size-margin-bottom); + color: var(--kbq-markdown-table-body); +} + +.table .td, +.table .th { + padding: var(--kbq-markdown-table-size-padding); + font-weight: 400; + vertical-align: top; +} + +.thead { + border-block-end: var(--kbq-markdown-table-size-border-width) solid + var(--kbq-markdown-table-border); + text-align: start; + color: var(--kbq-markdown-table-header); + font-size: var(--kbq-md-typography-md-table-header-font-size); + line-height: var(--kbq-md-typography-md-table-header-line-height); +} + +.thead .th:first-child { + padding-inline-start: 0; +} + +.thead .th:last-child { + padding-inline-end: 0; +} + +.tbody { + font-size: var(--kbq-md-typography-md-table-cell-font-size); + line-height: var(--kbq-md-typography-md-table-cell-line-height); +} + +.tbody .td:first-child { + padding-inline-start: 0; +} + +.tbody .td:last-child { + padding-inline-end: 0; +} + +.a { + color: var(--kbq-foreground-theme); + text-decoration-line: underline; + text-decoration-color: var(--kbq-line-theme-less); + outline: var(--kbq-markdown-link-size-state-focused-outline-width) solid + transparent; + outline-offset: var(--kbq-markdown-link-size-state-focused-outline-offset); + transition: + color var(--kbq-transition-default), + outline-color var(--kbq-transition-default), + text-decoration-color var(--kbq-transition-default); +} + +.a:hover { + color: var(--kbq-states-foreground-theme-hover); +} + +.a:active { + color: var(--kbq-states-foreground-theme-active); +} + +.a:focus-visible { + outline-color: var(--kbq-states-line-focus-theme); +} + +.a:visited { + color: var(--kbq-foreground-visited); + text-decoration-color: var(--kbq-line-visited); +} + +.a:has(.code) { + text-decoration-line: none; +} diff --git a/packages/components/src/components/Markdown/Markdown.stories.tsx b/packages/components/src/components/Markdown/Markdown.stories.tsx new file mode 100644 index 00000000..689dcb3c --- /dev/null +++ b/packages/components/src/components/Markdown/Markdown.stories.tsx @@ -0,0 +1,250 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Markdown } from './index.js'; + +const meta = { + title: 'Components/Markdown', + component: Markdown, + parameters: { + layout: 'padded', + }, + tags: ['status:new', 'date:2026-08-05'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Overview: Story = { + render: () => ( + + {`# Heading 1 +## Heading 2 +### Heading 3 +#### Heading 4 +##### Heading 5 +###### Heading 6`} + + ), +}; + +export const Headers: Story = { + render: () => ( + + {`# Heading 1 +## Heading 2 +### Heading 3 +#### Heading 4 +##### Heading 5 +###### Heading 6`} + + ), +}; + +export const HeadersCombinations: Story = { + render: () => ( + + {`## Data Protection System +### Security Levels +#### Threat Types + +Recommended password length is 12 characters, encryption key length is 256 bits. Weak passwords are not rejected, the system raises security requirements. + +### Attack Detection + +Monitoring happens in real time. Multiple incidents form a unified threat picture. New events are displayed in the security log. If there are many threats, critical ones are highlighted with priority. Most often attacks are detected automatically, but an administrator can initiate a manual check.`} + + ), +}; + +export const Paragraph: Story = { + render: () => ( + + {`A brute-force attack is a cryptanalytic attack that can, in theory, be used to attempt to decrypt any encrypted data (except for data encrypted in an information-theoretically secure manner). + +Such an attack might be used when it is not possible to take advantage of other weaknesses in an encryption system (if any exist) that would make the task easier.`} + + ), +}; + +export const TextEmphasis: Story = { + render: () => ( + + {`**bold** +__bold__ + +_italic_ +*italic* + +***bold and italic*** +___bold and italic___`} + + ), +}; + +export const Blockquote: Story = { + render: () => ( + + {`> A [brute-force attack](#) is a cryptanalytic attack that can, in theory, be used to attempt to decrypt any encrypted data (except for data encrypted in an information-theoretically secure manner). Such an attack might be used when it is not possible to take advantage of other weaknesses in an encryption system (if any exist) that would make the task easier.`} + + ), +}; + +export const Lists: Story = { + render: () => ( + + {`### Ordered List: + +1. First item +2. Second item +3. Third item +4. Fourth item + +### Unordered List: + +- First item +- Second item +- Third item +- Fourth item`} + + ), +}; + +export const InlineCode: Story = { + render: () => ( + {'Use the `bypassSecurityTrustHtml()` function'} + ), +}; + +export const CodeBlock: Story = { + render: () => ( + + {`\`\`\`html + + + + + + Koobiq + + + + + +\`\`\``} + + ), +}; + +export const Divider: Story = { + render: () => {'---'}, +}; + +export const Link: Story = { + render: () => ( + + {`[Link](#) + +A [\`brute-force\` attack](#) is a cryptanalytic attack that can, in theory, be used to attempt to decrypt any encrypted data (except for data encrypted in an information-theoretically secure manner).`} + + ), +}; + +export const Image: Story = { + render: () => ( + + {`![With caption text](https://koobiq.io/assets/images/markdown/markdown-image.png) +*Caption*`} + + ), +}; + +export const Table: Story = { + render: () => ( + + {`| Default | Left align | Center align | Right align | +| ---------- | :--------- | :----------: | ----------: | +| Babable | Zillya | ClamAV | Acronis | +| Cybereason | McAfee | Cyren | Zoner | +| ESET NOD32 | Alibaba | eScan | Dr.Web |`} + + ), +}; + +export const LineBreak: Story = { + render: () => {'First line \nSecond line'}, +}; + +export const LineBreakWithMarkedOptions: Story = { + render: () => ( + + {'First line\nSecond line'} + + ), +}; + +export const Article: Story = { + render: () => ( + + {`# Cybersecurity Fundamentals + +![With caption text](https://koobiq.io/assets/images/markdown/markdown-image.png) +*Caption* + +## Network Security + +Cybersecurity is the practice of protecting systems, networks, and programs from digital attacks. These cyberattacks are usually aimed at accessing, changing, or destroying sensitive information; extorting money from users; or interrupting normal business processes. + +Implementing effective cybersecurity measures is particularly challenging today because there are more devices than people, and attackers are becoming more innovative. A successful cybersecurity approach has multiple layers of protection spread across the computers, networks, programs, or data that one intends to keep safe. + +## Data Protection + +### Encryption Standards + +Network security involves protecting the usability and integrity of network and data. It includes both hardware and software technologies. Effective network security manages access to the network and targets a variety of threats and stops them from entering or spreading on your network. + +### Authentication Methods + +Multi-factor authentication adds an extra layer of security by requiring users to provide two or more verification factors to gain access to a resource. This significantly reduces the risk of unauthorized access even if passwords are compromised. + +## Threat Detection + +![With caption text](https://koobiq.io/assets/images/markdown/markdown-image.png) +*Image with a caption* + +Advanced persistent threats represent sophisticated, long-term cyber attacks where intruders gain access to a network and remain undetected for extended periods. These attacks are typically aimed at stealing data rather than causing damage to the network or organization. Threat detection systems use machine learning and behavioral analysis to identify suspicious activities and potential security breaches before they can cause significant damage. + +> Security is not a product, but a process that requires continuous monitoring and improvement. + +Incident response planning is crucial for organizations to effectively handle security breaches when they occur. A well-structured incident response plan helps minimize damage and recovery time. + +1. Identify and assess the security incident +2. Contain the threat to prevent further damage +3. Eradicate the threat from all affected systems +4. Recover normal operations and monitor for signs of weakness + +Risk assessment and vulnerability management are ongoing processes that help organizations identify potential security gaps. Regular security audits and penetration testing help ensure that security measures remain effective against evolving threats. + +Cybersecurity awareness training is essential for all employees as human error remains one of the leading causes of security breaches. Regular training helps staff recognize phishing attempts, social engineering tactics, and other common attack vectors. + +### Security Monitoring + +Security information and event management systems collect and analyze security-related data from various sources across an organization's infrastructure. + +\`\`\`bash +$ nmap -sS -O 192.168.1.1 +\`\`\` + +### Compliance Standards + +#### Regulatory Requirements + +Organizations must comply with various cybersecurity regulations and standards depending on their industry and location. These may include GDPR, HIPAA, SOX, and industry-specific requirements that mandate specific security controls and practices. + +- Regular security assessments and audits +- Implementation of appropriate technical and organizational measures +- Incident reporting and breach notification procedures + +Continuous improvement in cybersecurity requires staying updated with the latest threats, technologies, and best practices in the field.`} + + ), +}; diff --git a/packages/components/src/components/Markdown/Markdown.test.tsx b/packages/components/src/components/Markdown/Markdown.test.tsx new file mode 100644 index 00000000..b8618735 --- /dev/null +++ b/packages/components/src/components/Markdown/Markdown.test.tsx @@ -0,0 +1,102 @@ +import { createRef } from 'react'; + +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; + +import { Markdown } from './index.js'; + +describe('Markdown', () => { + it('should accept the ref', () => { + const ref = createRef(); + const { container } = render(# Heading); + expect(ref.current).toBe(container.firstElementChild); + }); + + it('should accept a custom class', () => { + const { container } = render( + # Heading + ); + + expect(container.firstElementChild).toHaveClass('foo'); + }); + + it('should render headers', () => { + render({'# Heading 1\n\n## Heading 2'}); + + expect( + screen.getByRole('heading', { level: 1, name: 'Heading 1' }) + ).toBeInTheDocument(); + + expect( + screen.getByRole('heading', { level: 2, name: 'Heading 2' }) + ).toBeInTheDocument(); + }); + + it('should render a paragraph', () => { + render(Some paragraph text.); + expect(screen.getByText('Some paragraph text.')).toBeInTheDocument(); + }); + + it('should render a list', () => { + render({'- First item\n- Second item'}); + + expect(screen.getByText('First item')).toBeInTheDocument(); + expect(screen.getByText('Second item')).toBeInTheDocument(); + }); + + it('should render a table', () => { + render({'| A | B |\n| - | - |\n| 1 | 2 |'}); + + expect(screen.getByRole('table')).toBeInTheDocument(); + expect(screen.getByText('A')).toBeInTheDocument(); + expect(screen.getByText('1')).toBeInTheDocument(); + }); + + it('should render a link with href', () => { + render({'[koobiq](https://www.koobiq.io)'}); + + const link = screen.getByRole('link', { name: 'koobiq' }); + expect(link).toHaveAttribute('href', 'https://www.koobiq.io'); + }); + + it('should apply markedOptions to the parser', () => { + const { container } = render( + + {'First line\nSecond line'} + + ); + + expect(container.querySelector('br')).toBeInTheDocument(); + }); + + it('should re-render when children change', () => { + const { rerender } = render(# First); + expect(screen.getByRole('heading', { name: 'First' })).toBeInTheDocument(); + + rerender(# Second); + expect(screen.getByRole('heading', { name: 'Second' })).toBeInTheDocument(); + + expect( + screen.queryByRole('heading', { name: 'First' }) + ).not.toBeInTheDocument(); + }); + + it('should sanitize a raw script tag', () => { + const { container } = render( + {''} + ); + + expect(container.querySelector('script')).not.toBeInTheDocument(); + }); + + it('should strip event handler attributes from raw HTML', () => { + const { container } = render( + + {''} + + ); + + const img = container.querySelector('img'); + expect(img).not.toHaveAttribute('onerror'); + }); +}); diff --git a/packages/components/src/components/Markdown/Markdown.tsx b/packages/components/src/components/Markdown/Markdown.tsx new file mode 100644 index 00000000..0365c326 --- /dev/null +++ b/packages/components/src/components/Markdown/Markdown.tsx @@ -0,0 +1,104 @@ +'use client'; + +import { forwardRef, useMemo } from 'react'; + +import { clsx, useSsr } from '@koobiq/react-core'; +import DOMPurify from 'dompurify'; +import { marked } from 'marked'; +import type { MarkedOptions } from 'marked'; + +import s from './Markdown.module.css'; +import type { MarkdownProps } from './types'; + +/** + * Tags whose opening tag gets a class injected, keeping any existing + * attributes (e.g. GFM table cells carry their own `align` attribute). + * The lookahead requires a following whitespace, `/`, or `>`, so `th`/`p` + * don't accidentally match inside `thead`/`pre`. + */ +const TAGS_TO_CLASS_ALIAS = [ + 'a', + 'blockquote', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hr', + 'li', + 'ol', + 'table', + 'tbody', + 'thead', + 'th', + 'td', + 'tr', + 'ul', + 'p', + 'pre', + 'code', + 'img', +] as const; + +type MarkdownClassMap = Record; + +const injectClass = (html: string, tag: string, className: string): string => { + if (!className) return html; + + return html.replaceAll( + new RegExp(`<${tag}(?=[\\s/>])`, 'g'), + `<${tag} class="${className}"` + ); +}; + +/** Applies the component's CSS module classes to the raw HTML produced by `marked`. */ +const applyMarkdownClasses = ( + html: string, + classes: MarkdownClassMap +): string => { + let result = html; + + for (const tag of TAGS_TO_CLASS_ALIAS) { + result = injectClass(result, tag, classes[tag]); + } + + return result; +}; + +/** Parses a Markdown string into an HTML string. Does not sanitize the result. */ +const parseMarkdownToHtml = ( + markdown: string, + options?: MarkedOptions +): string => marked.parse(markdown, { ...options, async: false }); + +/** + * Markdown renders a Markdown string as HTML, using the `marked` parser and + * sanitizing the result with DOMPurify before injecting it into the DOM. + */ +export const Markdown = forwardRef( + (props, ref) => { + const { children, markedOptions, className, ...other } = props; + const { isBrowser } = useSsr(); + + const classedHtml = useMemo( + () => + applyMarkdownClasses(parseMarkdownToHtml(children, markedOptions), s), + [children, markedOptions] + ); + + // DOMPurify requires a DOM; nothing is rendered until mounted in the browser. + const sanitizedHtml = isBrowser ? DOMPurify.sanitize(classedHtml) : ''; + + return ( +
+
+
+ ); + } +); + +Markdown.displayName = 'Markdown'; diff --git a/packages/components/src/components/Markdown/index.ts b/packages/components/src/components/Markdown/index.ts new file mode 100644 index 00000000..c5c27091 --- /dev/null +++ b/packages/components/src/components/Markdown/index.ts @@ -0,0 +1,2 @@ +export * from './Markdown.js'; +export * from './types.js'; diff --git a/packages/components/src/components/Markdown/types.ts b/packages/components/src/components/Markdown/types.ts new file mode 100644 index 00000000..6ba89a0c --- /dev/null +++ b/packages/components/src/components/Markdown/types.ts @@ -0,0 +1,14 @@ +import type { ComponentPropsWithoutRef } from 'react'; + +import type { MarkedOptions } from 'marked'; + +export interface MarkdownProps extends Omit< + ComponentPropsWithoutRef<'div'>, + 'children' +> { + /** Markdown text to convert into HTML. */ + children: string; + + /** Options passed through to the underlying `marked` parser. */ + markedOptions?: MarkedOptions; +} diff --git a/packages/components/src/components/index.ts b/packages/components/src/components/index.ts index 438226c6..cc3495a4 100644 --- a/packages/components/src/components/index.ts +++ b/packages/components/src/components/index.ts @@ -27,6 +27,7 @@ export * from './SidePanel'; export * from './Popover'; export * from './Tooltip'; export * from './List'; +export * from './Markdown'; export * from './AnimatedIcon'; export * from './Select'; export * from './SelectNext'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2cae3875..5f4d0d6c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,6 +121,9 @@ importers: country-flag-icons: specifier: ^1.6.20 version: 1.6.20 + dompurify: + specifier: ^3.4.13 + version: 3.4.13 dotenv: specifier: ^16.4.7 version: 16.4.7 @@ -132,7 +135,7 @@ importers: version: 9.1.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^51.4.1 version: 51.4.1(eslint@9.39.4(jiti@2.6.1)) @@ -166,6 +169,9 @@ importers: lightningcss: specifier: ^1.29.3 version: 1.29.3 + marked: + specifier: ^18.0.9 + version: 18.0.9 nano-staged: specifier: ^1.0.2 version: 1.0.2 @@ -262,6 +268,12 @@ importers: '@types/react-transition-group': specifier: ^4.4.12 version: 4.4.12(@types/react@19.2.7) + dompurify: + specifier: ^3.4.13 + version: 3.4.13 + marked: + specifier: ^18.0.9 + version: 18.0.9 react: specifier: ^19.0.0 version: 19.2.3 @@ -539,7 +551,7 @@ importers: version: 9.39.4(jiti@2.6.1) eslint-config-next: specifier: ^16.2.9 - version: 16.2.9(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) + version: 16.2.9(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) jsdom: specifier: ^25.0.1 version: 25.0.1 @@ -3211,6 +3223,9 @@ packages: '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -3801,7 +3816,7 @@ packages: basic-ftp@5.1.0: resolution: {integrity: sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==} engines: {node: '>=10.0.0'} - deprecated: Security vulnerability fixed in 5.2.1, please upgrade + deprecated: Security vulnerability fixed in 5.2.0, please upgrade bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} @@ -4605,6 +4620,9 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dompurify@3.4.13: + resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==} + dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} @@ -6467,6 +6485,11 @@ packages: engines: {node: '>= 18'} hasBin: true + marked@18.0.9: + resolution: {integrity: sha512-/Sa4qiiHZxf0/FQdBBowr9q4r10krCwMvpK48FUBdXdUXScDxiQGR9zCPrFgRVR5LU3iySOiIjy09ZQvADir1w==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -11985,12 +12008,15 @@ snapshots: '@types/triple-beam@1.3.5': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/parser': 8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.61.0 '@typescript-eslint/type-utils': 8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) '@typescript-eslint/utils': 8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) @@ -12236,7 +12262,7 @@ snapshots: obug: 2.1.3 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@8.0.13(@types/node@25.8.0)(esbuild@0.27.7)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.15))(tsx@4.19.3)(yaml@2.9.0)) + vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.9)(jsdom@25.0.1)(vite@7.3.3(@types/node@25.8.0)(jiti@2.6.1)(lightningcss@1.29.3)(sugarss@5.0.1(postcss@8.5.15))(tsx@4.19.3)(yaml@2.9.0)) '@vitest/expect@3.2.4': dependencies: @@ -13495,6 +13521,10 @@ snapshots: '@babel/runtime': 7.29.2 csstype: 3.1.3 + dompurify@3.4.13: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dot-prop@5.3.0: dependencies: is-obj: 2.0.0 @@ -13873,13 +13903,13 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-next@16.2.9(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3): + eslint-config-next@16.2.9(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3): dependencies: '@next/eslint-plugin-next': 16.2.9 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.6.1)) @@ -13913,7 +13943,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -13924,21 +13954,22 @@ snapshots: tinyglobby: 0.2.17 unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: + '@typescript-eslint/parser': 8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -13949,7 +13980,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) hasown: 2.0.3 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -13960,6 +13991,8 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -15911,6 +15944,8 @@ snapshots: marked@13.0.3: {} + marked@18.0.9: {} + math-intrinsics@1.1.0: {} mathml-tag-names@2.1.3: {} @@ -18393,7 +18428,7 @@ snapshots: typescript-eslint@8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) '@typescript-eslint/parser': 8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3) '@typescript-eslint/utils': 8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) diff --git a/tools/api-extractor/config.json b/tools/api-extractor/config.json index ed92420e..9d220102 100644 --- a/tools/api-extractor/config.json +++ b/tools/api-extractor/config.json @@ -32,6 +32,7 @@ "layout", "Link", "List", + "Markdown", "Menu", "Modal", "Navbar", diff --git a/tools/public_api_guard/components/Markdown.api.md b/tools/public_api_guard/components/Markdown.api.md new file mode 100644 index 00000000..d54f98e3 --- /dev/null +++ b/tools/public_api_guard/components/Markdown.api.md @@ -0,0 +1,23 @@ +## API Report File for "koobiq-react" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ComponentPropsWithoutRef } from 'react'; +import { ForwardRefExoticComponent } from 'react'; +import type { MarkedOptions } from 'marked'; +import { RefAttributes } from 'react'; + +// @public +export const Markdown: ForwardRefExoticComponent>; + +// @public (undocumented) +export interface MarkdownProps extends Omit, 'children'> { + children: string; + markedOptions?: MarkedOptions; +} + +// (No @packageDocumentation comment for this package) + +``` From 5b2ecb977f6c30d5e4fb64a5f53b278a06c1c0b1 Mon Sep 17 00:00:00 2001 From: Artem Belik Date: Thu, 6 Aug 2026 13:05:35 +0300 Subject: [PATCH 2/3] refactor: review --- .../src/components/Markdown/Markdown.test.tsx | 30 +++++++++++++++++++ .../src/components/Markdown/Markdown.tsx | 17 +++++++++-- .../src/components/Markdown/types.ts | 2 +- .../components/Markdown.api.md | 2 +- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/packages/components/src/components/Markdown/Markdown.test.tsx b/packages/components/src/components/Markdown/Markdown.test.tsx index b8618735..03f4923e 100644 --- a/packages/components/src/components/Markdown/Markdown.test.tsx +++ b/packages/components/src/components/Markdown/Markdown.test.tsx @@ -99,4 +99,34 @@ describe('Markdown', () => { const img = container.querySelector('img'); expect(img).not.toHaveAttribute('onerror'); }); + + it('should not crash and should ignore a dangerouslySetInnerHTML prop', () => { + expect(() => + render( + // eslint-disable-next-line react/no-danger-with-children -- verifying the runtime guard strips this prop for untyped callers + window.xssTriggered = true;', + }} + > + # Heading + + ) + ).not.toThrow(); + + expect( + screen.getByRole('heading', { name: 'Heading' }) + ).toBeInTheDocument(); + }); + + it('should keep the language class on a fenced code block alongside the injected class', () => { + const { container } = render( + {'```js\nconst a = 1;\n```'} + ); + + const code = container.querySelector('pre > code'); + expect(code).toHaveClass('language-js'); + expect(code?.className.split(' ').length).toBeGreaterThan(1); + }); }); diff --git a/packages/components/src/components/Markdown/Markdown.tsx b/packages/components/src/components/Markdown/Markdown.tsx index 0365c326..d4a9f267 100644 --- a/packages/components/src/components/Markdown/Markdown.tsx +++ b/packages/components/src/components/Markdown/Markdown.tsx @@ -46,9 +46,19 @@ type MarkdownClassMap = Record; const injectClass = (html: string, tag: string, className: string): string => { if (!className) return html; + // Captures existing attributes (e.g. fenced code blocks emit ``) + // so an existing class attribute is merged into rather than duplicated. return html.replaceAll( - new RegExp(`<${tag}(?=[\\s/>])`, 'g'), - `<${tag} class="${className}"` + new RegExp(`<${tag}(?=[\\s/>])([^>]*)>`, 'g'), + (_match, attrs: string) => { + const classMatch = attrs.match(/\sclass="([^"]*)"/); + + if (classMatch) { + return `<${tag}${attrs.replace(classMatch[0], ` class="${classMatch[1]} ${className}"`)}>`; + } + + return `<${tag} class="${className}"${attrs}>`; + } ); }; @@ -79,6 +89,9 @@ const parseMarkdownToHtml = ( export const Markdown = forwardRef( (props, ref) => { const { children, markedOptions, className, ...other } = props; + // Defensive: MarkdownProps already excludes this, but strip it at runtime + // too, since passing it alongside the output div's own JSX child throws. + delete (other as Record).dangerouslySetInnerHTML; const { isBrowser } = useSsr(); const classedHtml = useMemo( diff --git a/packages/components/src/components/Markdown/types.ts b/packages/components/src/components/Markdown/types.ts index 6ba89a0c..6dc01622 100644 --- a/packages/components/src/components/Markdown/types.ts +++ b/packages/components/src/components/Markdown/types.ts @@ -4,7 +4,7 @@ import type { MarkedOptions } from 'marked'; export interface MarkdownProps extends Omit< ComponentPropsWithoutRef<'div'>, - 'children' + 'children' | 'dangerouslySetInnerHTML' > { /** Markdown text to convert into HTML. */ children: string; diff --git a/tools/public_api_guard/components/Markdown.api.md b/tools/public_api_guard/components/Markdown.api.md index d54f98e3..be02e654 100644 --- a/tools/public_api_guard/components/Markdown.api.md +++ b/tools/public_api_guard/components/Markdown.api.md @@ -13,7 +13,7 @@ import { RefAttributes } from 'react'; export const Markdown: ForwardRefExoticComponent>; // @public (undocumented) -export interface MarkdownProps extends Omit, 'children'> { +export interface MarkdownProps extends Omit, 'children' | 'dangerouslySetInnerHTML'> { children: string; markedOptions?: MarkedOptions; } From 9de1109e58f25eca8bc98898a6af2528dcb8c242 Mon Sep 17 00:00:00 2001 From: Artem Belik Date: Thu, 6 Aug 2026 18:35:51 +0300 Subject: [PATCH 3/3] refactor: review --- package.json | 3 - packages/components/package.json | 14 +- .../src/components/Markdown/Markdown.mdx | 11 - .../components/Markdown/Markdown.module.css | 100 +++--- .../components/Markdown/Markdown.stories.tsx | 8 - .../src/components/Markdown/Markdown.test.tsx | 53 +-- .../src/components/Markdown/Markdown.tsx | 110 +----- .../src/components/Markdown/types.ts | 5 - pnpm-lock.yaml | 337 +++++++++++++++--- .../components/Markdown.api.md | 2 - 10 files changed, 362 insertions(+), 281 deletions(-) diff --git a/package.json b/package.json index 650e0984..51a0ec86 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,6 @@ "chalk": "^4.1.2", "commit-and-tag-version": "^12.5.0", "country-flag-icons": "^1.6.20", - "dompurify": "^3.4.13", "dotenv": "^16.4.7", "eslint": "^9.39.4", "eslint-config-prettier": "^9.1.2", @@ -93,7 +92,6 @@ "husky": "^9.1.6", "jsdom": "^25.0.1", "lightningcss": "^1.29.3", - "marked": "^18.0.9", "nano-staged": "^1.0.2", "postcss-lightningcss": "^1.1.0", "postcss-mixins": "^11.0.3", @@ -101,7 +99,6 @@ "react": "^19.2.3", "react-dom": "^19.2.3", "react-hook-form": "^7.68.0", - "remark-gfm": "^4.0.1", "rimraf": "^6.0.1", "rollup-preserve-directives": "^1.1.3", "storybook": "^10.3.6", diff --git a/packages/components/package.json b/packages/components/package.json index 35f6cdb1..89c68bf8 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -37,22 +37,14 @@ "@koobiq/react-icons": "^12.0.0", "@koobiq/react-primitives": "workspace:*", "@types/react-transition-group": "^4.4.12", - "react-transition-group": "^4.4.5" + "react-markdown": "^10.1.0", + "react-transition-group": "^4.4.5", + "remark-gfm": "^4.0.1" }, "peerDependencies": { "@koobiq/design-tokens": "^3.17.2", "@koobiq/react-icons": "^12.0.0", - "dompurify": "^3.4.13", - "marked": "^18.0.9", "react": "18.x || 19.x", "react-dom": "18.x || 19.x" - }, - "peerDependenciesMeta": { - "dompurify": { - "optional": true - }, - "marked": { - "optional": true - } } } diff --git a/packages/components/src/components/Markdown/Markdown.mdx b/packages/components/src/components/Markdown/Markdown.mdx index a1c52fb4..0c3e02f8 100644 --- a/packages/components/src/components/Markdown/Markdown.mdx +++ b/packages/components/src/components/Markdown/Markdown.mdx @@ -15,13 +15,6 @@ import * as Stories from './Markdown.stories'; `Markdown` renders a Markdown string as HTML. -For the component to work, install the [`marked`](https://www.npmjs.com/package/marked) and -[`dompurify`](https://www.npmjs.com/package/dompurify) peer dependencies: - -```bash -npm install marked dompurify -``` - ## Import ```tsx @@ -88,10 +81,6 @@ To create a line break, add two trailing spaces at the end of the line. -Alternatively, pass `{ breaks: true }` via `markedOptions` and use a plain `\n` in the text. - - - ## Large article A larger combined example, useful as a visual sanity check for spacing and typography. diff --git a/packages/components/src/components/Markdown/Markdown.module.css b/packages/components/src/components/Markdown/Markdown.module.css index f3ba32a6..3d6f26bf 100644 --- a/packages/components/src/components/Markdown/Markdown.module.css +++ b/packages/components/src/components/Markdown/Markdown.module.css @@ -12,20 +12,20 @@ margin-block-end: 0; } -.h1, -.h2, -.h3, -.h4, -.h5, -.h6 { +.markdownOutput h1, +.markdownOutput h2, +.markdownOutput h3, +.markdownOutput h4, +.markdownOutput h5, +.markdownOutput h6 { color: var(--kbq-foreground-contrast); } -:is(.h1, .h2, .h3, .h4, .h5, .h6) + :is(.h1, .h2, .h3, .h4, .h5, .h6) { +.markdownOutput :is(h1, h2, h3, h4, h5, h6) + :is(h1, h2, h3, h4, h5, h6) { margin-block-start: 0; } -.h1 { +.markdownOutput h1 { margin-block: var(--kbq-markdown-h1-size-margin-top) var(--kbq-markdown-h1-size-margin-bottom); font-size: var(--kbq-md-typography-md-h1-font-size); @@ -34,7 +34,7 @@ letter-spacing: var(--kbq-md-typography-md-h1-letter-spacing); } -.h2 { +.markdownOutput h2 { margin-block: var(--kbq-markdown-h2-size-margin-top) var(--kbq-markdown-h2-size-margin-bottom); font-size: var(--kbq-md-typography-md-h2-font-size); @@ -43,7 +43,7 @@ letter-spacing: var(--kbq-md-typography-md-h2-letter-spacing); } -.h3 { +.markdownOutput h3 { margin-block: var(--kbq-markdown-h3-size-margin-top) var(--kbq-markdown-h3-size-margin-bottom); font-size: var(--kbq-md-typography-md-h3-font-size); @@ -52,7 +52,7 @@ letter-spacing: var(--kbq-md-typography-md-h3-letter-spacing); } -.h4 { +.markdownOutput h4 { margin-block: var(--kbq-markdown-h4-size-margin-top) var(--kbq-markdown-h4-size-margin-bottom); font-size: var(--kbq-md-typography-md-h4-font-size); @@ -61,7 +61,7 @@ letter-spacing: var(--kbq-md-typography-md-h4-letter-spacing); } -.h5 { +.markdownOutput h5 { margin-block: var(--kbq-markdown-h5-size-margin-top) var(--kbq-markdown-h5-size-margin-bottom); font-size: var(--kbq-md-typography-md-h5-font-size); @@ -70,7 +70,7 @@ letter-spacing: var(--kbq-md-typography-md-h5-letter-spacing); } -.h6 { +.markdownOutput h6 { margin-block: var(--kbq-markdown-h6-size-margin-top) var(--kbq-markdown-h6-size-margin-bottom); font-size: var(--kbq-md-typography-md-h6-font-size); @@ -80,7 +80,7 @@ text-transform: var(--kbq-md-typography-md-h6-text-transform); } -.p { +.markdownOutput p { margin-block: var(--kbq-markdown-p-size-margin-top) var(--kbq-markdown-p-size-margin-bottom); color: var(--kbq-foreground-contrast); @@ -89,8 +89,8 @@ letter-spacing: var(--kbq-md-typography-md-body-letter-spacing); } -.ul, -.ol { +.markdownOutput ul, +.markdownOutput ol { margin-block: var(--kbq-markdown-list-size-margin-top) var(--kbq-markdown-list-size-margin-bottom); color: var(--kbq-foreground-contrast); @@ -98,19 +98,19 @@ line-height: var(--kbq-md-typography-md-body-line-height); } -.p + .ul, -.p + .ol { +.markdownOutput p + ul, +.markdownOutput p + ol { margin-block-start: var(--kbq-markdown-list-size-margin-top-after-paragraph); } -.ul { +.markdownOutput ul { position: relative; list-style: none; padding-inline: var(--kbq-size-xxl) 0; padding-block: 0; } -.ul > .li::before { +.markdownOutput ul > li::before { content: '—'; display: block; position: absolute; @@ -119,30 +119,30 @@ text-align: start; } -.ol { +.markdownOutput ol { list-style: none; padding: 0; counter-reset: li; } -.ol > .li { +.markdownOutput ol > li { counter-increment: li; } -.ol > .li::before { +.markdownOutput ol > li::before { content: counter(li) '.'; padding-inline-end: var(--kbq-markdown-list-size-ol-number-padding-right); } -.ol > .li > * { +.markdownOutput ol > li > * { display: inline-block; } -.li { +.markdownOutput li { margin-block-end: var(--kbq-markdown-list-size-item-margin-bottom); } -.blockquote { +.markdownOutput blockquote { margin-block: var(--kbq-markdown-blockquote-size-margin-top) var(--kbq-markdown-blockquote-size-margin-bottom); margin-inline: 0; @@ -153,11 +153,11 @@ background: var(--kbq-markdown-blockquote-background); } -.blockquote > .p { +.markdownOutput blockquote > p { margin-block: 0; } -.pre { +.markdownOutput pre { position: relative; overflow-x: auto; margin-block: var(--kbq-markdown-code-size-multiline-margin-top) @@ -168,35 +168,35 @@ background-color: var(--kbq-markdown-code-background); } -.code { +.markdownOutput code { border-radius: var(--kbq-markdown-code-size-border-radius); font-family: var(--kbq-md-typography-md-inline-code-font-family); font-size: var(--kbq-md-typography-md-inline-code-font-size); line-height: var(--kbq-md-typography-md-inline-code-line-height); } -.code:not(.pre > .code) { +.markdownOutput code:not(pre > code) { white-space: nowrap; padding: var(--kbq-markdown-code-size-inline-padding); } -.code:not(.pre > .code, .a .code) { +.markdownOutput code:not(pre > code, a code) { color: var(--kbq-markdown-code-text); background-color: var(--kbq-markdown-code-background); } -.a .code { +.markdownOutput a code { background-color: var(--kbq-markdown-code-background); font-weight: var(--kbq-md-typography-md-inline-code-as-link-font-weight); } -.img { +.markdownOutput img { max-inline-size: 100%; margin-block: var(--kbq-markdown-image-size-margin-top) var(--kbq-markdown-image-size-margin-bottom); } -.img + em { +.markdownOutput img + em { display: block; margin-block: var(--kbq-markdown-image-size-caption-margin-top) var(--kbq-markdown-image-size-caption-margin-bottom); @@ -205,7 +205,7 @@ line-height: var(--kbq-md-typography-md-caption-line-height); } -.hr { +.markdownOutput hr { block-size: 0; margin-block: var(--kbq-markdown-hr-size-margin-vertical); border: none; @@ -213,7 +213,7 @@ var(--kbq-markdown-hr-color); } -.table { +.markdownOutput table { max-inline-size: 100%; overflow-x: auto; display: table; @@ -222,14 +222,14 @@ color: var(--kbq-markdown-table-body); } -.table .td, -.table .th { +.markdownOutput table td, +.markdownOutput table th { padding: var(--kbq-markdown-table-size-padding); font-weight: 400; vertical-align: top; } -.thead { +.markdownOutput thead { border-block-end: var(--kbq-markdown-table-size-border-width) solid var(--kbq-markdown-table-border); text-align: start; @@ -238,28 +238,28 @@ line-height: var(--kbq-md-typography-md-table-header-line-height); } -.thead .th:first-child { +.markdownOutput thead th:first-child { padding-inline-start: 0; } -.thead .th:last-child { +.markdownOutput thead th:last-child { padding-inline-end: 0; } -.tbody { +.markdownOutput tbody { font-size: var(--kbq-md-typography-md-table-cell-font-size); line-height: var(--kbq-md-typography-md-table-cell-line-height); } -.tbody .td:first-child { +.markdownOutput tbody td:first-child { padding-inline-start: 0; } -.tbody .td:last-child { +.markdownOutput tbody td:last-child { padding-inline-end: 0; } -.a { +.markdownOutput a { color: var(--kbq-foreground-theme); text-decoration-line: underline; text-decoration-color: var(--kbq-line-theme-less); @@ -272,23 +272,23 @@ text-decoration-color var(--kbq-transition-default); } -.a:hover { +.markdownOutput a:hover { color: var(--kbq-states-foreground-theme-hover); } -.a:active { +.markdownOutput a:active { color: var(--kbq-states-foreground-theme-active); } -.a:focus-visible { +.markdownOutput a:focus-visible { outline-color: var(--kbq-states-line-focus-theme); } -.a:visited { +.markdownOutput a:visited { color: var(--kbq-foreground-visited); text-decoration-color: var(--kbq-line-visited); } -.a:has(.code) { +.markdownOutput a:has(code) { text-decoration-line: none; } diff --git a/packages/components/src/components/Markdown/Markdown.stories.tsx b/packages/components/src/components/Markdown/Markdown.stories.tsx index 689dcb3c..d1cedc0b 100644 --- a/packages/components/src/components/Markdown/Markdown.stories.tsx +++ b/packages/components/src/components/Markdown/Markdown.stories.tsx @@ -174,14 +174,6 @@ export const LineBreak: Story = { render: () => {'First line \nSecond line'}, }; -export const LineBreakWithMarkedOptions: Story = { - render: () => ( - - {'First line\nSecond line'} - - ), -}; - export const Article: Story = { render: () => ( diff --git a/packages/components/src/components/Markdown/Markdown.test.tsx b/packages/components/src/components/Markdown/Markdown.test.tsx index 03f4923e..4313fe83 100644 --- a/packages/components/src/components/Markdown/Markdown.test.tsx +++ b/packages/components/src/components/Markdown/Markdown.test.tsx @@ -1,11 +1,18 @@ import { createRef } from 'react'; import { render, screen } from '@testing-library/react'; +import { renderToStaticMarkup } from 'react-dom/server'; import { describe, it, expect } from 'vitest'; import { Markdown } from './index.js'; describe('Markdown', () => { + it('should render on the server', () => { + const html = renderToStaticMarkup(# Heading); + + expect(html).toContain('

Heading

'); + }); + it('should accept the ref', () => { const ref = createRef(); const { container } = render(# Heading); @@ -59,16 +66,6 @@ describe('Markdown', () => { expect(link).toHaveAttribute('href', 'https://www.koobiq.io'); }); - it('should apply markedOptions to the parser', () => { - const { container } = render( - - {'First line\nSecond line'} - - ); - - expect(container.querySelector('br')).toBeInTheDocument(); - }); - it('should re-render when children change', () => { const { rerender } = render(# First); expect(screen.getByRole('heading', { name: 'First' })).toBeInTheDocument(); @@ -81,52 +78,24 @@ describe('Markdown', () => { ).not.toBeInTheDocument(); }); - it('should sanitize a raw script tag', () => { + it('should not render raw HTML', () => { const { container } = render( {''} ); expect(container.querySelector('script')).not.toBeInTheDocument(); - }); - it('should strip event handler attributes from raw HTML', () => { - const { container } = render( - - {''} - + expect(container).toHaveTextContent( + '' ); - - const img = container.querySelector('img'); - expect(img).not.toHaveAttribute('onerror'); - }); - - it('should not crash and should ignore a dangerouslySetInnerHTML prop', () => { - expect(() => - render( - // eslint-disable-next-line react/no-danger-with-children -- verifying the runtime guard strips this prop for untyped callers - window.xssTriggered = true;', - }} - > - # Heading - - ) - ).not.toThrow(); - - expect( - screen.getByRole('heading', { name: 'Heading' }) - ).toBeInTheDocument(); }); - it('should keep the language class on a fenced code block alongside the injected class', () => { + it('should keep the language class on a fenced code block', () => { const { container } = render( {'```js\nconst a = 1;\n```'} ); const code = container.querySelector('pre > code'); expect(code).toHaveClass('language-js'); - expect(code?.className.split(' ').length).toBeGreaterThan(1); }); }); diff --git a/packages/components/src/components/Markdown/Markdown.tsx b/packages/components/src/components/Markdown/Markdown.tsx index d4a9f267..b70a2f88 100644 --- a/packages/components/src/components/Markdown/Markdown.tsx +++ b/packages/components/src/components/Markdown/Markdown.tsx @@ -1,114 +1,28 @@ 'use client'; -import { forwardRef, useMemo } from 'react'; +import { forwardRef } from 'react'; -import { clsx, useSsr } from '@koobiq/react-core'; -import DOMPurify from 'dompurify'; -import { marked } from 'marked'; -import type { MarkedOptions } from 'marked'; +import { clsx } from '@koobiq/react-core'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; import s from './Markdown.module.css'; import type { MarkdownProps } from './types'; -/** - * Tags whose opening tag gets a class injected, keeping any existing - * attributes (e.g. GFM table cells carry their own `align` attribute). - * The lookahead requires a following whitespace, `/`, or `>`, so `th`/`p` - * don't accidentally match inside `thead`/`pre`. - */ -const TAGS_TO_CLASS_ALIAS = [ - 'a', - 'blockquote', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'hr', - 'li', - 'ol', - 'table', - 'tbody', - 'thead', - 'th', - 'td', - 'tr', - 'ul', - 'p', - 'pre', - 'code', - 'img', -] as const; +const remarkPlugins = [remarkGfm]; -type MarkdownClassMap = Record; - -const injectClass = (html: string, tag: string, className: string): string => { - if (!className) return html; - - // Captures existing attributes (e.g. fenced code blocks emit ``) - // so an existing class attribute is merged into rather than duplicated. - return html.replaceAll( - new RegExp(`<${tag}(?=[\\s/>])([^>]*)>`, 'g'), - (_match, attrs: string) => { - const classMatch = attrs.match(/\sclass="([^"]*)"/); - - if (classMatch) { - return `<${tag}${attrs.replace(classMatch[0], ` class="${classMatch[1]} ${className}"`)}>`; - } - - return `<${tag} class="${className}"${attrs}>`; - } - ); -}; - -/** Applies the component's CSS module classes to the raw HTML produced by `marked`. */ -const applyMarkdownClasses = ( - html: string, - classes: MarkdownClassMap -): string => { - let result = html; - - for (const tag of TAGS_TO_CLASS_ALIAS) { - result = injectClass(result, tag, classes[tag]); - } - - return result; -}; - -/** Parses a Markdown string into an HTML string. Does not sanitize the result. */ -const parseMarkdownToHtml = ( - markdown: string, - options?: MarkedOptions -): string => marked.parse(markdown, { ...options, async: false }); - -/** - * Markdown renders a Markdown string as HTML, using the `marked` parser and - * sanitizing the result with DOMPurify before injecting it into the DOM. - */ +/** Markdown renders a Markdown string as React elements. */ export const Markdown = forwardRef( (props, ref) => { - const { children, markedOptions, className, ...other } = props; - // Defensive: MarkdownProps already excludes this, but strip it at runtime - // too, since passing it alongside the output div's own JSX child throws. - delete (other as Record).dangerouslySetInnerHTML; - const { isBrowser } = useSsr(); - - const classedHtml = useMemo( - () => - applyMarkdownClasses(parseMarkdownToHtml(children, markedOptions), s), - [children, markedOptions] - ); - - // DOMPurify requires a DOM; nothing is rendered until mounted in the browser. - const sanitizedHtml = isBrowser ? DOMPurify.sanitize(classedHtml) : ''; + const { children, className, ...other } = props; return (
-
+
+ + {children} + +
); } diff --git a/packages/components/src/components/Markdown/types.ts b/packages/components/src/components/Markdown/types.ts index 6dc01622..e001131b 100644 --- a/packages/components/src/components/Markdown/types.ts +++ b/packages/components/src/components/Markdown/types.ts @@ -1,14 +1,9 @@ import type { ComponentPropsWithoutRef } from 'react'; -import type { MarkedOptions } from 'marked'; - export interface MarkdownProps extends Omit< ComponentPropsWithoutRef<'div'>, 'children' | 'dangerouslySetInnerHTML' > { /** Markdown text to convert into HTML. */ children: string; - - /** Options passed through to the underlying `marked` parser. */ - markedOptions?: MarkedOptions; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f4d0d6c..fef25af3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,9 +121,6 @@ importers: country-flag-icons: specifier: ^1.6.20 version: 1.6.20 - dompurify: - specifier: ^3.4.13 - version: 3.4.13 dotenv: specifier: ^16.4.7 version: 16.4.7 @@ -135,7 +132,7 @@ importers: version: 9.1.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + version: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^51.4.1 version: 51.4.1(eslint@9.39.4(jiti@2.6.1)) @@ -169,9 +166,6 @@ importers: lightningcss: specifier: ^1.29.3 version: 1.29.3 - marked: - specifier: ^18.0.9 - version: 18.0.9 nano-staged: specifier: ^1.0.2 version: 1.0.2 @@ -193,9 +187,6 @@ importers: react-hook-form: specifier: ^7.68.0 version: 7.68.0(react@19.2.3) - remark-gfm: - specifier: ^4.0.1 - version: 4.0.1 rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -268,21 +259,21 @@ importers: '@types/react-transition-group': specifier: ^4.4.12 version: 4.4.12(@types/react@19.2.7) - dompurify: - specifier: ^3.4.13 - version: 3.4.13 - marked: - specifier: ^18.0.9 - version: 18.0.9 react: specifier: ^19.0.0 version: 19.2.3 react-dom: specifier: ^19.0.0 version: 19.2.3(react@19.2.3) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.2.7)(react@19.2.3) react-transition-group: specifier: ^4.4.5 version: 4.4.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 packages/core: dependencies: @@ -551,7 +542,7 @@ importers: version: 9.39.4(jiti@2.6.1) eslint-config-next: specifier: ^16.2.9 - version: 16.2.9(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) + version: 16.2.9(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) jsdom: specifier: ^25.0.1 version: 25.0.1 @@ -3165,6 +3156,9 @@ packages: '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -3174,6 +3168,9 @@ packages: '@types/fs-extra@11.0.4': resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -3223,8 +3220,8 @@ packages: '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} - '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -3296,6 +3293,9 @@ packages: resolution: {integrity: sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + '@unrs/resolver-binding-android-arm-eabi@1.12.2': resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} cpu: [arm] @@ -3997,9 +3997,18 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} @@ -4127,6 +4136,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -4620,9 +4632,6 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} - dompurify@3.4.13: - resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==} - dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} @@ -4985,6 +4994,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -5508,6 +5520,12 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + heap-js@2.7.1: resolution: {integrity: sha512-EQfezRg0NCZGNlhlDR3Evrw1FVL2G3LhU7EgPoxufQKruNBSYA8MiRPHeWbU+36o+Fhel0wMwM+sLEiBAlNLJA==} engines: {node: '>=10.0.0'} @@ -5550,6 +5568,9 @@ packages: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} @@ -5642,6 +5663,9 @@ packages: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + install-artifact-from-github@1.4.0: resolution: {integrity: sha512-+y6WywKZREw5rq7U2jvr2nmZpT7cbWbQQ0N/qfcseYnzHFz2cZz1Et52oY+XttYuYeTkI8Y+R2JNWj68MpQFSg==} hasBin: true @@ -5669,6 +5693,12 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-array-buffer@3.0.4: resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} engines: {node: '>= 0.4'} @@ -5742,6 +5772,9 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5775,6 +5808,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -6485,11 +6521,6 @@ packages: engines: {node: '>= 18'} hasBin: true - marked@18.0.9: - resolution: {integrity: sha512-/Sa4qiiHZxf0/FQdBBowr9q4r10krCwMvpK48FUBdXdUXScDxiQGR9zCPrFgRVR5LU3iySOiIjy09ZQvADir1w==} - engines: {node: '>= 20'} - hasBin: true - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -6521,9 +6552,21 @@ packages: mdast-util-gfm@3.0.0: resolution: {integrity: sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==} + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + mdast-util-to-markdown@2.1.2: resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} @@ -7095,6 +7138,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-imports-exports@0.2.4: resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} @@ -7378,6 +7424,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -7494,6 +7543,12 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': ^19.0.0 + react: ^19.0.0 + react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} @@ -7585,6 +7640,9 @@ packages: remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} @@ -7873,6 +7931,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -8011,6 +8072,9 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -8042,6 +8106,12 @@ packages: stubs@3.0.0: resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -8284,6 +8354,9 @@ packages: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + trim-newlines@3.0.1: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} engines: {node: '>=8'} @@ -8466,6 +8539,9 @@ packages: unist-util-is@6.0.0: resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} @@ -11955,6 +12031,10 @@ snapshots: '@types/esrecurse@4.3.1': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + '@types/estree@1.0.8': {} '@types/estree@1.0.9': {} @@ -11964,6 +12044,10 @@ snapshots: '@types/jsonfile': 6.1.4 '@types/node': 25.8.0 + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -12008,15 +12092,14 @@ snapshots: '@types/triple-beam@1.3.5': {} - '@types/trusted-types@2.0.7': - optional: true + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/parser': 8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.61.0 '@typescript-eslint/type-utils': 8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) '@typescript-eslint/utils': 8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) @@ -12159,6 +12242,8 @@ snapshots: '@typescript-eslint/types': 8.61.0 eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.3': {} + '@unrs/resolver-binding-android-arm-eabi@1.12.2': optional: true @@ -12262,7 +12347,7 @@ snapshots: obug: 2.1.3 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.9)(jsdom@25.0.1)(vite@7.3.3(@types/node@25.8.0)(jiti@2.6.1)(lightningcss@1.29.3)(sugarss@5.0.1(postcss@8.5.15))(tsx@4.19.3)(yaml@2.9.0)) + vitest: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@8.0.13(@types/node@25.8.0)(esbuild@0.27.7)(jiti@2.6.1)(sugarss@5.0.1(postcss@8.5.15))(tsx@4.19.3)(yaml@2.9.0)) '@vitest/expect@3.2.4': dependencies: @@ -12926,8 +13011,14 @@ snapshots: char-regex@1.0.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + character-entities@2.0.2: {} + character-reference-invalid@2.0.1: {} + chardet@2.1.1: {} check-error@2.1.1: {} @@ -13042,6 +13133,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} + commander@10.0.1: {} commander@2.20.3: {} @@ -13521,10 +13614,6 @@ snapshots: '@babel/runtime': 7.29.2 csstype: 3.1.3 - dompurify@3.4.13: - optionalDependencies: - '@types/trusted-types': 2.0.7 - dot-prop@5.3.0: dependencies: is-obj: 2.0.0 @@ -13903,13 +13992,13 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-next@16.2.9(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3): + eslint-config-next@16.2.9(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3): dependencies: '@next/eslint-plugin-next': 16.2.9 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.6.1)) @@ -13943,7 +14032,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -13954,22 +14043,21 @@ snapshots: tinyglobby: 0.2.17 unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -13980,7 +14068,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) hasown: 2.0.3 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -13991,8 +14079,6 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -14216,6 +14302,8 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + estree-walker@2.0.2: {} estree-walker@3.0.3: @@ -15003,6 +15091,30 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + heap-js@2.7.1: {} hermes-estree@0.25.1: {} @@ -15037,6 +15149,8 @@ snapshots: html-tags@3.3.1: {} + html-url-attributes@3.0.1: {} + http-cache-semantics@4.2.0: optional: true @@ -15127,6 +15241,8 @@ snapshots: ini@4.1.1: {} + inline-style-parser@0.2.7: {} + install-artifact-from-github@1.4.0: optional: true @@ -15151,6 +15267,13 @@ snapshots: ipaddr.js@1.9.1: {} + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-array-buffer@3.0.4: dependencies: call-bind: 1.0.7 @@ -15235,6 +15358,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-decimal@2.0.1: {} + is-docker@3.0.0: {} is-extglob@2.1.1: {} @@ -15265,6 +15390,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 @@ -15944,8 +16071,6 @@ snapshots: marked@13.0.3: {} - marked@18.0.9: {} - math-intrinsics@1.1.0: {} mathml-tag-names@2.1.3: {} @@ -16031,11 +16156,62 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + mdast-util-phrasing@4.1.0: dependencies: '@types/mdast': 4.0.4 unist-util-is: 6.0.0 + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.3 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + mdast-util-to-markdown@2.1.2: dependencies: '@types/mdast': 4.0.4 @@ -16736,6 +16912,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.0.2 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-imports-exports@0.2.4: dependencies: parse-statements: 1.0.11 @@ -16989,6 +17175,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@7.2.0: {} + proto-list@1.2.4: {} proto3-json-serializer@3.0.4: @@ -17209,6 +17397,24 @@ snapshots: react-is@17.0.2: {} + react-markdown@10.1.0(@types/react@19.2.7)(react@19.2.3): + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/react': 19.2.7 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.3 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-refresh@0.18.0: {} react-stately@3.45.0(react@19.2.3): @@ -17381,6 +17587,14 @@ snapshots: transitivePeerDependencies: - supports-color + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + remark-stringify@11.0.0: dependencies: '@types/mdast': 4.0.4 @@ -17772,6 +17986,8 @@ snapshots: source-map@0.6.1: {} + space-separated-tokens@2.0.2: {} + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -17962,6 +18178,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -17984,6 +18205,14 @@ snapshots: stubs@3.0.0: {} + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.3): dependencies: client-only: 0.0.1 @@ -18291,6 +18520,8 @@ snapshots: dependencies: punycode: 2.3.1 + trim-lines@3.0.1: {} + trim-newlines@3.0.1: {} triple-beam@1.4.1: {} @@ -18428,7 +18659,7 @@ snapshots: typescript-eslint@8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) '@typescript-eslint/parser': 8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3) '@typescript-eslint/utils': 8.61.0(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3) @@ -18507,6 +18738,10 @@ snapshots: dependencies: '@types/unist': 3.0.3 + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position@4.0.0: dependencies: '@types/unist': 3.0.3 diff --git a/tools/public_api_guard/components/Markdown.api.md b/tools/public_api_guard/components/Markdown.api.md index be02e654..5b92c071 100644 --- a/tools/public_api_guard/components/Markdown.api.md +++ b/tools/public_api_guard/components/Markdown.api.md @@ -6,7 +6,6 @@ import type { ComponentPropsWithoutRef } from 'react'; import { ForwardRefExoticComponent } from 'react'; -import type { MarkedOptions } from 'marked'; import { RefAttributes } from 'react'; // @public @@ -15,7 +14,6 @@ export const Markdown: ForwardRefExoticComponent, 'children' | 'dangerouslySetInnerHTML'> { children: string; - markedOptions?: MarkedOptions; } // (No @packageDocumentation comment for this package)