From dbfe65b542bf6c35364074fb9f8af16fe1ab3654 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Fri, 16 Oct 2026 16:56:44 +0200 Subject: [PATCH 1/2] fix(react,svelte): forward HTML attributes to the root element --- docs/content/3.rendering/5.react.md | 2 + docs/content/3.rendering/6.svelte.md | 2 + .../test/root-attributes.test.ts | 73 ++++++++++ .../comark-react/src/components/Markdown.tsx | 10 +- .../src/components/MarkdownClient.tsx | 16 +- .../src/components/MarkdownDocument.tsx | 16 +- .../test/root-attributes.test.tsx | 137 ++++++++++++++++++ .../src/async/MarkdownAsync.svelte | 5 +- .../src/components/Markdown.svelte | 6 +- .../src/components/MarkdownDocument.svelte | 7 +- packages/comark-svelte/src/types.ts | 11 +- .../test/root-attributes.svelte.test.ts | 34 +++++ .../test/root-attributes.test.ts | 64 ++++++++ .../comark-vue/test/root-attributes.test.ts | 94 ++++++++++++ 14 files changed, 465 insertions(+), 12 deletions(-) create mode 100644 packages/comark-angular/test/root-attributes.test.ts create mode 100644 packages/comark-react/test/root-attributes.test.tsx create mode 100644 packages/comark-svelte/test/root-attributes.svelte.test.ts create mode 100644 packages/comark-svelte/test/root-attributes.test.ts create mode 100644 packages/comark-vue/test/root-attributes.test.ts diff --git a/docs/content/3.rendering/5.react.md b/docs/content/3.rendering/5.react.md index ceeb510a..0661f34f 100644 --- a/docs/content/3.rendering/5.react.md +++ b/docs/content/3.rendering/5.react.md @@ -105,6 +105,7 @@ Passing a document to `` skips parsing at runtime, but the **parser is | [`caret`](#streaming-caret) | `boolean \| { class: string }` | `false` | Append caret to last text node | | [`data`](#code-markdown-props-code-data) | `Record` | `undefined` | Runtime values referenced from markdown via `:prop="data.path"` | | `className` | `string` | `undefined` | CSS class for wrapper element | +| `...attributes` | `HTMLAttributes` | — | Any other HTML attribute (`id`, `style`, `role`, `data-*`, `aria-*`, ...) is set on the wrapper `
` | #### `options` @@ -418,6 +419,7 @@ export default async function DocsPage({ params }: { params: { slug: string } }) | [`caret`](#streaming-caret) | `boolean \| { class: string }` | `false` | Append a blinking caret to the last text node | | [`data`](#code-markdown-props-code-data) | `Record` | `undefined` | Runtime values referenced from markdown via `:prop="data.path"` | | `className` | `string` | `undefined` | CSS class for the wrapper `
` | +| `...attributes` | `HTMLAttributes` | — | Any other HTML attribute (`id`, `style`, `role`, `data-*`, `aria-*`, ...) is set on the wrapper `
` | ### `defineMarkdownDocumentComponent` diff --git a/docs/content/3.rendering/6.svelte.md b/docs/content/3.rendering/6.svelte.md index f1f6207d..adbf7b15 100644 --- a/docs/content/3.rendering/6.svelte.md +++ b/docs/content/3.rendering/6.svelte.md @@ -97,6 +97,7 @@ Passing a document to `` skips parsing at runtime, but the **parser is | [`caret`](#streaming-caret) | `boolean \| { class: string }` | `false` | Append caret to last text node | | [`data`](#code-markdown-props-code-data) | `Record` | `undefined` | Runtime values referenced from markdown via `:prop="data.path"` | | `class` | `string` | `''` | CSS class for wrapper element | +| `...attributes` | `HTMLAttributes` | — | Any other HTML attribute (`id`, `style`, `role`, `data-*`, `aria-*`, ...) is set on the wrapper `
` | #### `options` @@ -386,6 +387,7 @@ export const load: PageLoad = async ({ params, fetch }) => { | [`caret`](#streaming-caret) | `boolean \| { class: string }` | `false` | Append a blinking caret to the last text node | | [`data`](#code-markdown-props-code-data) | `Record` | `undefined` | Runtime values referenced from markdown via `:prop="data.path"` | | `class` | `string` | `''` | CSS class for wrapper element | +| `...attributes` | `HTMLAttributes` | — | Any other HTML attribute (`id`, `style`, `role`, `data-*`, `aria-*`, ...) is set on the wrapper `
` | ### `componentsManifest` diff --git a/packages/comark-angular/test/root-attributes.test.ts b/packages/comark-angular/test/root-attributes.test.ts new file mode 100644 index 00000000..05fec7d7 --- /dev/null +++ b/packages/comark-angular/test/root-attributes.test.ts @@ -0,0 +1,73 @@ +import '@angular/compiler' +import { describe, expect, it } from 'vitest' +import { Component, provideZonelessChangeDetection } from '@angular/core' +import { bootstrapApplication } from '@angular/platform-browser' +import { renderApplication } from '@angular/platform-server' +import { parseMarkdown, type MarkdownDocument as MarkdownDocumentType } from 'comark' +import { Markdown } from '../src/components/markdown.component.ts' +import { MarkdownDocument } from '../src/components/markdown-document.component.ts' + +/** + * Angular renders the component host element (``, + * ``) into the DOM, so plain HTML attributes + * (`data-*`, `aria-*`, `id`, `role`, ...) set on it stay on the root. + */ +const attrTemplate = + 'id="answer" role="region" data-testid="answer" data-message-id="42" aria-label="Assistant reply" class="custom"' + +async function renderTemplate(template: string, document?: MarkdownDocumentType): Promise { + class App { + document = document + } + Component({ + selector: 'app-root', + standalone: true, + imports: [Markdown, MarkdownDocument], + template, + })(App) + + return renderApplication( + (context) => bootstrapApplication(App, { providers: [provideZonelessChangeDetection()] }, context), + { document: '' } + ) +} + +function hostTag(html: string, selector: string): string { + return html.match(new RegExp(`<${selector}\\b[^>]*>`))?.[0] ?? '' +} + +function expectHostAttributes(html: string, selector: string) { + const host = hostTag(html, selector) + expect(host).toContain('id="answer"') + expect(host).toContain('role="region"') + expect(host).toContain('data-testid="answer"') + expect(host).toContain('data-message-id="42"') + expect(host).toContain('aria-label="Assistant reply"') + expect(host).toContain('class="custom"') +} + +describe('root element attributes', () => { + it('comark-markdown keeps static attributes on its host element', async () => { + const document = await parseMarkdown('Hello **world**') + const html = await renderTemplate(``, document) + expectHostAttributes(html, 'comark-markdown') + expect(html).toContain('world') + }) + + it('comark-markdown-document keeps static attributes on its host element', async () => { + const document = await parseMarkdown('Hello') + const html = await renderTemplate(``, document) + expectHostAttributes(html, 'comark-markdown-document') + }) + + it('comark-markdown-document keeps bound attributes on its host element', async () => { + const document = await parseMarkdown('Hello') + const html = await renderTemplate( + '', + document + ) + const host = hostTag(html, 'comark-markdown-document') + expect(host).toContain('aria-busy="true"') + expect(host).toContain('data-message-id="42"') + }) +}) diff --git a/packages/comark-react/src/components/Markdown.tsx b/packages/comark-react/src/components/Markdown.tsx index aecbd5fd..67d4b5fa 100644 --- a/packages/comark-react/src/components/Markdown.tsx +++ b/packages/comark-react/src/components/Markdown.tsx @@ -5,7 +5,11 @@ import { isMarkdownDocument } from 'comark/utils' import { MarkdownDocument } from './MarkdownDocument.tsx' import { MarkdownClient } from './MarkdownClient.tsx' -export interface MarkdownProps { +/** + * Every other HTML attribute (`id`, `style`, `role`, `data-*`, `aria-*`, ...) is + * set on the wrapper div. + */ +export interface MarkdownProps extends Omit, 'children'> { /** * The children content to parse and render */ @@ -114,11 +118,13 @@ export async function Markdown({ caret = false, data, className, + ...rest }: MarkdownProps) { // Pre-parsed document — skip parsing and render directly if (isMarkdownDocument(value)) { return ( (isMarkdownDocument(content) ? Promise.resolve(content) : parseMarkdown(content, { ...options, plugins })), + () => + isMarkdownDocument(content) + ? Promise.resolve(content) + : parseMarkdown(content, { ...options, ...(unwrap ? { unwrap } : {}), plugins }), [content] ) diff --git a/packages/comark-react/src/components/MarkdownDocument.tsx b/packages/comark-react/src/components/MarkdownDocument.tsx index e948f69d..4bbe5f4e 100644 --- a/packages/comark-react/src/components/MarkdownDocument.tsx +++ b/packages/comark-react/src/components/MarkdownDocument.tsx @@ -268,7 +268,11 @@ function renderNode( return null } -export interface MarkdownDocumentProps { +/** + * Every other HTML attribute (`id`, `style`, `role`, `data-*`, `aria-*`, ...) is + * set on the wrapper div. + */ +export interface MarkdownDocumentProps extends Omit, 'children'> { /** * The parsed Comark document to render — either a full `MarkdownDocument` or a bare * `Node[]`. When a node array is passed, frontmatter and meta @@ -341,6 +345,7 @@ export const MarkdownDocument: React.FC = ({ caret: caretProp = false, data, className, + ...rest }) => { const document = value ?? { nodes: [] } @@ -373,5 +378,12 @@ export const MarkdownDocument: React.FC = ({ }, [document, customComponents, componentsManifest, streaming, caret, data]) // Wrap in a fragment - return
{renderedNodes}
+ return ( +
+ {renderedNodes} +
+ ) } diff --git a/packages/comark-react/test/root-attributes.test.tsx b/packages/comark-react/test/root-attributes.test.tsx new file mode 100644 index 00000000..7be14e66 --- /dev/null +++ b/packages/comark-react/test/root-attributes.test.tsx @@ -0,0 +1,137 @@ +import { describe, expect, it } from 'vitest' +import React from 'react' +import { renderToReadableStream, renderToString } from 'react-dom/server' +import { parseMarkdown } from 'comark' +import { Markdown } from '../src/components/Markdown' +import { MarkdownDocument } from '../src/components/MarkdownDocument' +import { MarkdownLive } from '../src/components/MarkdownLive' +import { defineMarkdownComponent, defineMarkdownDocumentComponent } from '../src/index' + +/** + * HTML attributes that are not renderer options (`data-*`, `aria-*`, `id`, + * `role`, `style`, ...) land on the `comark-content` root element. + */ +const attrs = { + id: 'answer', + role: 'region', + 'data-testid': 'answer', + 'data-message-id': '42', + 'aria-label': 'Assistant reply', + 'aria-busy': true, + style: { color: 'red' }, +} + +function rootTag(html: string): string { + return html.match(/]*class="comark-content[^>]*>/)?.[0] ?? '' +} + +function expectRootAttributes(html: string) { + const root = rootTag(html) + expect(root).toContain('id="answer"') + expect(root).toContain('role="region"') + expect(root).toContain('data-testid="answer"') + expect(root).toContain('data-message-id="42"') + expect(root).toContain('aria-label="Assistant reply"') + expect(root).toContain('aria-busy="true"') + expect(root).toContain('style="color:red"') + expect(root).toContain('class="comark-content custom"') +} + +async function renderAsync(element: React.ReactElement): Promise { + const stream = await renderToReadableStream(element) + await stream.allReady + return new Response(stream).text() +} + +async function renderMarkdown(props: Record) { + const element = await Markdown(props as any) + return renderToString(element as React.ReactElement) +} + +describe('root element attributes', () => { + it('Markdown sets them on the root element', async () => { + const html = await renderMarkdown({ value: 'Hello **world**', className: 'custom', ...attrs }) + expectRootAttributes(html) + expect(html).toContain('world') + }) + + it('Markdown sets them when given a pre-parsed document', async () => { + const value = await parseMarkdown('Hello **world**') + expectRootAttributes(await renderMarkdown({ value, className: 'custom', ...attrs })) + }) + + it('Markdown keeps renderer options off the DOM', async () => { + const html = await renderMarkdown({ + value: 'Hello', + unwrap: true, + data: { a: 1 }, + options: {}, + plugins: [], + 'data-testid': 'answer', + }) + const root = rootTag(html) + expect(root).toContain('data-testid="answer"') + expect(root).not.toMatch(/unwrap|options|plugins|caret|streaming|components/) + expect(html).not.toContain('

') + }) + + it('Markdown sets them in streaming mode (MarkdownClient)', async () => { + const element = await Markdown({ value: 'Hello **world**', streaming: true, className: 'custom', ...attrs } as any) + const html = await renderAsync(element as React.ReactElement) + expectRootAttributes(html) + expect(html).toContain('world') + }) + + it('MarkdownDocument sets them on the root element', async () => { + const value = await parseMarkdown('Hello') + expectRootAttributes( + renderToString( + + ) + ) + }) + + it('MarkdownLive sets them on the root element', async () => { + const value = await parseMarkdown('Hello') + expectRootAttributes( + renderToString( + + ) + ) + }) + + it('defineMarkdownComponent forwards them', async () => { + const value = await parseMarkdown('Hello') + const Defined = defineMarkdownComponent({ className: 'custom' }) + expectRootAttributes( + await renderAsync( + + ) + ) + }) + + it('defineMarkdownDocumentComponent forwards them', async () => { + const value = await parseMarkdown('Hello') + const Defined = defineMarkdownDocumentComponent({ className: 'custom' }) + expectRootAttributes( + renderToString( + + ) + ) + }) +}) diff --git a/packages/comark-svelte/src/async/MarkdownAsync.svelte b/packages/comark-svelte/src/async/MarkdownAsync.svelte index 0cadd04d..afabf7d1 100644 --- a/packages/comark-svelte/src/async/MarkdownAsync.svelte +++ b/packages/comark-svelte/src/async/MarkdownAsync.svelte @@ -33,6 +33,7 @@ and wrap this component in a `` for pending/error states. import { isMarkdownDocument } from 'comark/utils' import MarkdownDocument from '../components/MarkdownDocument.svelte' import ResolveAsync from './ResolveAsync.svelte' + import type { RootAttributes } from '../types.js' let { value, @@ -45,7 +46,8 @@ and wrap this component in a `` for pending/error states. caret = false, data, class: className = '', - }: { + ...rest + }: RootAttributes & { value?: string | MarkdownDocumentType options?: Record plugins?: ComarkPlugin[] @@ -77,6 +79,7 @@ and wrap this component in a `` for pending/error states. {#if parsed} plugins?: ComarkPlugin[] @@ -92,6 +94,7 @@ This is an alert component {#if isMarkdownDocument(value)} {:else if parsed} import { untrack } from 'svelte' import type { MarkdownDocument as MarkdownDocumentType, ComponentManifest } from 'comark' - import type { ComponentResolver } from '../types.js' + import type { ComponentResolver, RootAttributes } from '../types.js' import MarkdownNode from './MarkdownNode.svelte' let { @@ -34,7 +34,8 @@ Supports custom component mappings and a streaming caret indicator. data, class: className = '', documentKey, - }: { + ...rest + }: RootAttributes & { value?: MarkdownDocumentType | { nodes: MarkdownDocumentType['nodes'] } components?: Record componentsManifest?: ComponentManifest @@ -79,7 +80,7 @@ Supports custom component mappings and a streaming caret indicator. }) -

+
{#each activeDocument.nodes as node, i (i)} , 'class' | 'children'> export interface ComponentResolverProps { promise: Promise @@ -18,7 +25,7 @@ export interface MarkdownNodeProps { caretClass?: string | null } -export interface MarkdownDocumentProps { +export interface MarkdownDocumentProps extends RootAttributes { /** The parsed Markdown document to render */ value?: MarkdownDocument components?: Record @@ -34,7 +41,7 @@ export interface MarkdownDocumentProps { documentKey?: string } -export interface MarkdownProps { +export interface MarkdownProps extends RootAttributes { /** The markdown content to parse and render, or a pre-parsed MarkdownDocument */ value?: string | MarkdownDocument options?: Omit diff --git a/packages/comark-svelte/test/root-attributes.svelte.test.ts b/packages/comark-svelte/test/root-attributes.svelte.test.ts new file mode 100644 index 00000000..94d4fada --- /dev/null +++ b/packages/comark-svelte/test/root-attributes.svelte.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' +import { render } from 'vitest-browser-svelte' +import Markdown from '../src/components/Markdown.svelte' + +describe('root element attributes (browser)', () => { + it('Markdown sets them on the root element after parsing a string', async () => { + const screen = await render(Markdown, { + value: 'Hello **world**', + class: 'custom', + id: 'answer', + 'data-testid': 'answer', + 'aria-label': 'Assistant reply', + 'aria-busy': true, + }) + + await expect.element(screen.getByTestId('answer')).toBeInTheDocument() + const root = screen.container.querySelector('.comark-content')! + expect(root.getAttribute('id')).toBe('answer') + expect(root.getAttribute('aria-label')).toBe('Assistant reply') + expect(root.getAttribute('aria-busy')).toBe('true') + expect(root.className).toBe('comark-content custom') + expect(root.textContent).toContain('Hello world') + }) + + it('updates them when the props change', async () => { + const screen = await render(Markdown, { value: 'Hello', streaming: true, 'aria-busy': true }) + await expect.element(screen.getByText('Hello')).toBeInTheDocument() + const root = screen.container.querySelector('.comark-content')! + expect(root.getAttribute('aria-busy')).toBe('true') + + await screen.rerender({ value: 'Hello', streaming: false, 'aria-busy': false }) + await expect.poll(() => root.getAttribute('aria-busy')).toBe('false') + }) +}) diff --git a/packages/comark-svelte/test/root-attributes.test.ts b/packages/comark-svelte/test/root-attributes.test.ts new file mode 100644 index 00000000..5d9ee38a --- /dev/null +++ b/packages/comark-svelte/test/root-attributes.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest' +import { render } from 'svelte/server' +import { parseMarkdown } from 'comark' +import Markdown from '../src/components/Markdown.svelte' +import MarkdownDocument from '../src/components/MarkdownDocument.svelte' +import MarkdownAsync from '../src/async/MarkdownAsync.svelte' + +/** + * HTML attributes that are not renderer options (`data-*`, `aria-*`, `id`, + * `role`, `style`, ...) land on the `comark-content` root element. + */ +const attrs = { + id: 'answer', + role: 'region', + 'data-testid': 'answer', + 'data-message-id': '42', + 'aria-label': 'Assistant reply', + 'aria-busy': true, + style: 'color: red', +} + +function rootTag(html: string): string { + return html.match(/]*class="comark-content[^>]*>/)?.[0] ?? '' +} + +function expectRootAttributes(html: string) { + const root = rootTag(html) + expect(root).toContain('id="answer"') + expect(root).toContain('role="region"') + expect(root).toContain('data-testid="answer"') + expect(root).toContain('data-message-id="42"') + expect(root).toContain('aria-label="Assistant reply"') + expect(root).toContain('aria-busy="true"') + expect(root).toContain('style="color: red"') + expect(root).toContain('class="comark-content custom"') +} + +describe('root element attributes', () => { + it('MarkdownDocument sets them on the root element', async () => { + const value = await parseMarkdown('Hello') + expectRootAttributes(render(MarkdownDocument, { props: { value, class: 'custom', ...attrs } }).body) + }) + + it('Markdown sets them when given a pre-parsed document', async () => { + const value = await parseMarkdown('Hello') + expectRootAttributes(render(Markdown, { props: { value, class: 'custom', ...attrs } }).body) + }) + + it('MarkdownAsync sets them on the root element', async () => { + const { body } = await render(MarkdownAsync, { props: { value: 'Hello **world**', class: 'custom', ...attrs } }) + expectRootAttributes(body) + expect(body.replace(//g, '')).toContain('world') + }) + + it('MarkdownAsync keeps renderer options off the DOM', async () => { + const { body } = await render(MarkdownAsync, { + props: { value: 'Hello', unwrap: true, data: { a: 1 }, options: {}, plugins: [], 'data-testid': 'answer' }, + }) + const root = rootTag(body) + expect(root).toContain('data-testid="answer"') + expect(root).not.toMatch(/unwrap|options|plugins|caret|streaming|components/) + expect(body).not.toContain('

') + }) +}) diff --git a/packages/comark-vue/test/root-attributes.test.ts b/packages/comark-vue/test/root-attributes.test.ts new file mode 100644 index 00000000..e3342b31 --- /dev/null +++ b/packages/comark-vue/test/root-attributes.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from 'vitest' +import { createSSRApp, h, Suspense } from 'vue' +import { renderToString } from '@vue/server-renderer' +import { parseMarkdown } from 'comark' +import { Markdown } from '../src/components/Markdown.ts' +import { MarkdownDocument } from '../src/components/MarkdownDocument.ts' +import { defineMarkdownComponent, defineMarkdownDocumentComponent } from '../src/index.ts' + +/** + * HTML attributes that are not component props (`data-*`, `aria-*`, `id`, + * `role`, `style`, ...) fall through to the `comark-content` root element. + */ +const attrs = { + id: 'answer', + role: 'region', + 'data-testid': 'answer', + 'data-message-id': '42', + 'aria-label': 'Assistant reply', + 'aria-busy': 'true', + style: 'color: red', + class: 'custom', +} + +function renderComponent(component: any, props: Record) { + const app = createSSRApp({ + setup() { + return () => h(Suspense, null, { default: () => h(component, props) }) + }, + }) + return renderToString(app as any) +} + +function rootTag(html: string): string { + return html.match(/]*class="[^"]*comark-content[^>]*>/)?.[0] ?? '' +} + +function expectRootAttributes(html: string) { + const root = rootTag(html) + expect(root).toContain('id="answer"') + expect(root).toContain('role="region"') + expect(root).toContain('data-testid="answer"') + expect(root).toContain('data-message-id="42"') + expect(root).toContain('aria-label="Assistant reply"') + expect(root).toContain('aria-busy="true"') + expect(root).toContain('style="color:red;"') + expect(root).toMatch(/class="[^"]*\bcustom\b/) +} + +describe('root element attributes', () => { + it('Markdown sets them on the root element', async () => { + const html = await renderComponent(Markdown, { value: 'Hello **world**', ...attrs }) + expectRootAttributes(html) + expect(html).toContain('world') + }) + + it('Markdown sets them in streaming mode', async () => { + expectRootAttributes(await renderComponent(Markdown, { value: 'Hello', streaming: true, ...attrs })) + }) + + it('Markdown sets them when given a pre-parsed document', async () => { + const value = await parseMarkdown('Hello') + expectRootAttributes(await renderComponent(Markdown, { value, ...attrs })) + }) + + it('Markdown keeps renderer options off the DOM', async () => { + const html = await renderComponent(Markdown, { + value: 'Hello', + unwrap: true, + data: { a: 1 }, + options: {}, + plugins: [], + 'data-testid': 'answer', + }) + const root = rootTag(html) + expect(root).toContain('data-testid="answer"') + expect(root).not.toMatch(/unwrap|options|plugins|caret|streaming|components/) + }) + + it('MarkdownDocument sets them on the root element', async () => { + const value = await parseMarkdown('Hello') + expectRootAttributes(await renderComponent(MarkdownDocument, { value, ...attrs })) + }) + + it('defineMarkdownComponent forwards them', async () => { + const Defined = defineMarkdownComponent({ name: 'Defined' }) + expectRootAttributes(await renderComponent(Defined, { value: 'Hello', ...attrs })) + }) + + it('defineMarkdownDocumentComponent forwards them', async () => { + const value = await parseMarkdown('Hello') + const Defined = defineMarkdownDocumentComponent({ name: 'Defined' }) + expectRootAttributes(await renderComponent(Defined, { value, ...attrs })) + }) +}) From 02c4c8d599b75b47bd49a499cfcd4794a3161617 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:24:12 +0000 Subject: [PATCH 2/2] test: update bundle size snapshot --- test/bundle.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/bundle.test.ts b/test/bundle.test.ts index 52349dcc..fd08d983 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -64,8 +64,8 @@ describe('package bundle size', { timeout: 60_000 }, () => { "@comark/ansi": "37.3k (98 files)", "@comark/html": "16.5k (58 files)", "@comark/nuxt": "11.9k (58 files)", - "@comark/react": "38.0k (76 files)", - "@comark/svelte": "45.8k (84 files)", + "@comark/react": "38.5k (76 files)", + "@comark/svelte": "46.5k (84 files)", "@comark/vue": "56.2k (80 files)", "comark": "385k (152 files)", }