diff --git a/AGENTS.md b/AGENTS.md
index 7be82c617..8e6291880 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -86,7 +86,7 @@ packages/comark/
│ │ ├── task-list.ts # GFM task lists
│ │ └── toc.ts # Table of contents
│ ├── utils/ # Shared utilities (comark/utils entry point)
-│ │ ├── index.ts # textContent(), visit(), visitAsync(), escapeHtml(), indent(), string/object utils
+│ │ ├── index.ts # textContent(), visit(), visitAsync(), escapeHtml(), isHtmlVoidElement(), indent(), string/object utils
│ │ ├── helpers.ts # defineComarkPlugin(), dedupePlugins()
│ │ ├── trace.ts # ComarkTracer helpers: noopTracer, withSpan() (comark/utils/trace)
│ │ └── caret.ts # Caret utilities for streaming
@@ -481,7 +481,15 @@ import { renderMarkdown } from 'comark/render'
// Document model types and utilities
import type { MarkdownDocument, Node, ElementNode, TextNode, CommentNode } from 'comark'
-import { textContent, visit, visitAsync, escapeHtml, isMarkdownDocument } from 'comark/utils'
+import {
+ textContent,
+ visit,
+ visitAsync,
+ escapeHtml,
+ isHtmlVoidElement,
+ HTML_VOID_ELEMENTS,
+ isMarkdownDocument,
+} from 'comark/utils'
import { noopTracer, withSpan } from 'comark/utils/trace'
// Core plugins — use when calling parseMarkdown() directly (framework-agnostic)
@@ -747,6 +755,16 @@ SPEC coverage: `SPEC/COMARK/component-nested-*-outdented.md`,
`SPEC/COMARK/component-nested-codeblock-indented.md`,
`SPEC/COMARK/codeblock-indented-content.md`.
+## Embedded HTML
+
+Contract: `packages/comark/SPEC/HTML/README.md`. HTML nodes carry
+`$: { html: 1, block: 0 | 1 }`. `block` means the tag outlived the paragraph
+that opened it, not that the tag is a block element. `html({ markdown: false })`
+keeps closed-block text literal; a blank line still nests real markdown under
+the open tag. ``)
+
+ expect(pre.nodes).toEqual([['pre', { $: { html: 1, block: 1 }, class: 'x' }, '\n const x = 1\n']])
+ expect(style.nodes).toEqual([['style', { $: { html: 1, block: 1 }, class: 'x' }, '\n .a { color: red; }\n']])
+ })
+
+ it('styles', async () => {
+ const result = await parseMarkdown(`
+
+
This is a warning message.
+ Your changes have been saved.
+ More information is available here.
`)
+
+ expect(result.nodes).toEqual([
+ [
+ 'style',
+ {
+ $: {
+ block: 1,
+ html: 1,
+ },
+ },
+ `\n .warning {
+ color: red;
+ }
+ .success {
+ color: green;
+ }
+
+ .info {
+ color: blue;
+ }
+ `,
+ ],
+ ['p', { $: { html: 1, block: 0 }, class: 'warning' }, 'This is a warning message.'],
+ ['p', { $: { html: 1, block: 0 }, class: 'success' }, 'Your changes have been saved.'],
+ ['p', { $: { html: 1, block: 0 }, class: 'info' }, 'More information is available here.'],
+ ])
+ })
+
+ it('two consecutive tags without a blank line between close and open', async () => {
+ const result = await parseMarkdown(`
+
+This is a warning message.
+
+
+Your changes have been saved.
+
+
+ `)
+
+ // First div closes inside its opening paragraph → block: 0 (text-only body).
+ // Second spans a free markdown paragraph → block: 1.
+ expect(result.nodes).toEqual([
+ ['div', { $: { html: 1, block: 0 } }, 'This is a warning message.'],
+ ['div', { $: { html: 1, block: 1 } }, 'Your changes have been saved.'],
+ ])
+ })
+})
+
+// HTML opened inside a markdown container must stay inside (and close with) that container.
+describe('HTML inside markdown containers', () => {
+ it('keeps inline HTML content inside a table cell', async () => {
+ const result = await parseMarkdown('| a |\n|---|\n| x |')
+
+ expect(result.nodes).toEqual([
+ [
+ 'table',
+ {},
+ ['thead', {}, ['tr', {}, ['th', {}, 'a']]],
+ ['tbody', {}, ['tr', {}, ['td', {}, ['b', { $: { html: 1, block: 0 } }, 'x']]]],
+ ],
+ ])
+ })
+
+ it('keeps block HTML content inside a blockquote', async () => {
+ const result = await parseMarkdown('> \n> hi\n>
')
+
+ expect(result.nodes).toEqual([['blockquote', {}, ['div', { $: { html: 1, block: 1 } }, 'hi']]])
+ })
+
+ it('closes an unclosed inline tag at the end of its list item', async () => {
+ const result = await parseMarkdown('- a\n- b')
+
+ expect(result.nodes).toEqual([['ul', {}, ['li', {}, ['span', { $: { html: 1, block: 1 } }, 'a']], ['li', {}, 'b']]])
+ })
+})
+
+describe('stray HTML close tags', () => {
+ it('keeps the surrounding paragraph when a close tag has no opener', async () => {
+ const result = await parseMarkdown(' orphan')
+
+ expect(result.nodes).toEqual([['p', {}, ' orphan']])
+ })
})
diff --git a/packages/comark/test/html-escape.test.ts b/packages/comark/test/html-escape.test.ts
index 81bc7a351..81120661a 100644
--- a/packages/comark/test/html-escape.test.ts
+++ b/packages/comark/test/html-escape.test.ts
@@ -27,6 +27,18 @@ describe('HTML attribute escaping', () => {
expect(html).not.toContain('title="a" onmouseover=alert(1)"')
})
+ it('decodes " inside quoted HTML attribute values', async () => {
+ const tree = await parseMarkdown('hi')
+ const p = tree.nodes[0] as [string, Record, ...Node[]]
+ const span = (Array.isArray(p[2]) ? p[2] : p) as [string, Record, ...Node[]]
+ expect(span[0]).toBe('span')
+ expect(span[1].title).toBe('A "quote"')
+
+ const html = await renderHtml('hi')
+ expect(html).toContain('title="A "quote""')
+ expect(html).not.toContain('title="A "quote""')
+ })
+
it('escapes quotes in component attribute props', async () => {
const html = await renderHtml(`:span[hi]{title='a" onmouseover=alert(1) x="b'}`)
expect(html).toContain('title="a" onmouseover=alert(1) x="b"')
@@ -59,6 +71,16 @@ describe('HTML attribute escaping', () => {
expect(html).toContain('title="a&b<c>d"')
})
+ it('escapes bare ampersands in URL attribute values', async () => {
+ // Query-string `&` must become `&` in HTML attrs (HTML5).
+ // Re-parse still yields bare `&` because attribute entities are decoded on parse.
+ const html = await renderHtml(`
`)
+ expect(html).toContain('src="https://x.com/?a=1&b=2"')
+ const tree = await parseMarkdown(html)
+ const img = tree.nodes[0] as [string, Record]
+ expect(img[1].src).toBe('https://x.com/?a=1&b=2')
+ })
+
it('escapes object attribute values as JSON with entities', async () => {
const html = await renderNodes([['div', { ':data': { x: '">
' } }, 'hi']])
expect(html).not.toContain('
')
diff --git a/packages/comark/test/index.test.ts b/packages/comark/test/index.test.ts
index e2d16478a..128c1fa37 100644
--- a/packages/comark/test/index.test.ts
+++ b/packages/comark/test/index.test.ts
@@ -178,7 +178,7 @@ function findMarkdownFiles(dir: string, baseDir: string = dir): string[] {
if (entry.isDirectory()) {
files.push(...findMarkdownFiles(fullPath, baseDir))
- } else if (entry.isFile() && entry.name.endsWith('.md')) {
+ } else if (entry.isFile() && entry.name.endsWith('.md') && entry.name !== 'README.md') {
files.push(relativePath)
}
}
diff --git a/packages/comark/test/is-html-void-element.test.ts b/packages/comark/test/is-html-void-element.test.ts
new file mode 100644
index 000000000..57e6d5ee7
--- /dev/null
+++ b/packages/comark/test/is-html-void-element.test.ts
@@ -0,0 +1,18 @@
+import { describe, expect, it } from 'vitest'
+import { HTML_VOID_ELEMENTS, isHtmlVoidElement } from '../src/utils/index.ts'
+
+describe('isHtmlVoidElement', () => {
+ it('matches the HTML void element set, case-insensitively', () => {
+ expect(isHtmlVoidElement('br')).toBe(true)
+ expect(isHtmlVoidElement('IMG')).toBe(true)
+ expect(isHtmlVoidElement('Wbr')).toBe(true)
+ expect(HTML_VOID_ELEMENTS.has('br')).toBe(true)
+ })
+
+ it('rejects elements that can have children', () => {
+ expect(isHtmlVoidElement('div')).toBe(false)
+ expect(isHtmlVoidElement('p')).toBe(false)
+ expect(isHtmlVoidElement('span')).toBe(false)
+ expect(isHtmlVoidElement('')).toBe(false)
+ })
+})
diff --git a/packages/comark/test/plugins/default-plugins.test.ts b/packages/comark/test/plugins/default-plugins.test.ts
index d47605e58..bcd970051 100644
--- a/packages/comark/test/plugins/default-plugins.test.ts
+++ b/packages/comark/test/plugins/default-plugins.test.ts
@@ -25,7 +25,7 @@ describe('default plugin options', () => {
it('parses HTML by default', async () => {
const tree = await parseMarkdown('Hello')
- expect(tree.nodes).toEqual([['p', {}, ['strong', { class: 'bold', $: { html: 1, block: 0 } }, 'Hello']]])
+ expect(tree.nodes).toEqual([['strong', { class: 'bold', $: { html: 1, block: 0 } }, 'Hello']])
})
it('parses frontmatter by default', async () => {
@@ -64,7 +64,7 @@ describe('default plugin options', () => {
registerDefaultPlugins: false,
plugins: [html()],
})
- expect(tree.nodes).toEqual([['p', {}, ['em', { $: { html: 1, block: 0 } }, 'hi']]])
+ expect(tree.nodes).toEqual([['em', { $: { html: 1, block: 0 } }, 'hi']])
})
it('treats attribute braces as plain text when registerDefaultPlugins is false', async () => {
diff --git a/packages/comark/test/plugins/summary.test.ts b/packages/comark/test/plugins/summary.test.ts
index c820e45f1..fca097c3d 100644
--- a/packages/comark/test/plugins/summary.test.ts
+++ b/packages/comark/test/plugins/summary.test.ts
@@ -13,7 +13,6 @@ After the fold.
describe('summary plugin', () => {
it('writes the nodes before the delimiter to tree.meta.summary', async () => {
const tree = await parseMarkdown(CONTENT, { plugins: [summary()] })
-
expect(tree.meta.summary).toEqual([['p', {}, 'Intro paragraph.']])
})
diff --git a/packages/comark/test/visit.test.ts b/packages/comark/test/visit.test.ts
index a6b6df40e..ebe8e1e52 100644
--- a/packages/comark/test/visit.test.ts
+++ b/packages/comark/test/visit.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { visit } from 'comark/utils'
+import { TREE_WALK_MAX_DEPTH, visit, visitAsync } from 'comark/utils'
import type { MarkdownDocument, Node } from 'comark'
describe('visit', () => {
@@ -409,4 +409,70 @@ describe('visit', () => {
expect(tree.nodes[1]).toEqual(['section', {}, 'Replaced'])
expect(tree.nodes[2]).toEqual(['span', {}, 'Keep'])
})
+
+ it('should visit nodes within the depth cap', () => {
+ // TREE_WALK_MAX_DEPTH - 1 wrappers + text — text is at depth N-1 and still visited.
+ let node: Node = 'Deep'
+ for (let i = TREE_WALK_MAX_DEPTH - 2; i >= 0; i--) {
+ node = [`d${i}`, {}, node]
+ }
+
+ const tree: MarkdownDocument = { nodes: [node], frontmatter: {}, meta: {} }
+ const visited: string[] = []
+ visit(
+ tree,
+ () => true,
+ (n) => {
+ visited.push(typeof n === 'string' ? n : String(n[0]))
+ }
+ )
+
+ const expected = Array.from({ length: TREE_WALK_MAX_DEPTH - 1 }, (_, i) => `d${i}`)
+ expected.push('Deep')
+ expect(visited).toEqual(expected)
+ })
+
+ it('should drop nodes past TREE_WALK_MAX_DEPTH element levels', () => {
+ // TREE_WALK_MAX_DEPTH + 1 wrappers + text — last wrapper and its children are past the cap.
+ let node: Node = 'Too deep'
+ for (let i = TREE_WALK_MAX_DEPTH; i >= 0; i--) {
+ node = [`d${i}`, {}, node]
+ }
+
+ const tree: MarkdownDocument = { nodes: [node], frontmatter: {}, meta: {} }
+ const visited: string[] = []
+ visit(
+ tree,
+ () => true,
+ (n) => {
+ visited.push(typeof n === 'string' ? n : String(n[0]))
+ }
+ )
+
+ expect(visited).toEqual(Array.from({ length: TREE_WALK_MAX_DEPTH }, (_, i) => `d${i}`))
+
+ let cursor = tree.nodes[0] as Node[]
+ for (let i = 0; i < TREE_WALK_MAX_DEPTH - 1; i++) {
+ expect(cursor[0]).toBe(`d${i}`)
+ cursor = cursor[2] as Node[]
+ }
+ // The last visited wrapper keeps no children: everything deeper is removed.
+ expect(cursor).toEqual([`d${TREE_WALK_MAX_DEPTH - 1}`, {}])
+ })
+
+ it('should drop deep subtrees in visitAsync so unvisited content cannot survive', async () => {
+ let node: Node = ['img', { onerror: 'alert(1)' }]
+ for (let i = TREE_WALK_MAX_DEPTH + 10; i >= 0; i--) {
+ node = ['blockquote', {}, node]
+ }
+
+ const tree: MarkdownDocument = { nodes: [node], frontmatter: {}, meta: {} }
+ await visitAsync(
+ tree,
+ () => true,
+ () => undefined
+ )
+
+ expect(JSON.stringify(tree.nodes)).not.toContain('onerror')
+ })
})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c06d21dc3..1d238427e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -323,9 +323,6 @@ catalogs:
hast-util-to-string:
specifier: ^3.0.1
version: 3.0.1
- htmlparser2:
- specifier: ^12.0.0
- version: 12.0.0
js-yaml:
specifier: ^5.4.2
version: 5.4.2
@@ -693,7 +690,7 @@ importers:
version: link:../../../packages/comark-vue
'@nuxt/ui':
specifier: 'catalog:'
- version: 4.11.2(c77efd868fa853b6efbc5c86610e740e)
+ version: 4.11.2(fa5844bfdc74c64dd01c986666e59821)
nuxt:
specifier: 'catalog:'
version: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.150.0)(@parcel/watcher@2.6.0)(@types/node@26.6.2)(@vercel/functions@3.9.8(ws@8.21.3))(@vue/compiler-sfc@3.5.43)(commander@15.0.0)(db0@0.3.4)(esbuild@0.28.2)(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.5)(oxc-parser@0.149.0)(oxlint@1.85.0)(rolldown@1.2.9)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.9)(rollup@4.63.4))(rollup@4.63.4)(sass@1.101.0)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue-tsc@3.3.11(typescript@6.0.3))(yaml@2.9.1)
@@ -1410,9 +1407,6 @@ importers:
entities:
specifier: 'catalog:'
version: 8.1.0
- htmlparser2:
- specifier: 'catalog:'
- version: 12.0.0
js-yaml:
specifier: 'catalog:'
version: 5.4.2
@@ -13104,7 +13098,7 @@ snapshots:
rc9: 3.1.0
std-env: 4.2.0
- '@nuxt/ui@4.11.2(c77efd868fa853b6efbc5c86610e740e)':
+ '@nuxt/ui@4.11.2(48eec6f14dd5da94c5723622cb20c05a)':
dependencies:
'@floating-ui/dom': 1.8.0
'@iconify/vue': 5.0.2(vue@3.5.43(typescript@6.0.3))
@@ -13174,7 +13168,6 @@ snapshots:
'@internationalized/date': 3.12.4
'@internationalized/number': 3.6.8
ai: 7.0.112(zod@4.6.5)
- vue-router: 5.3.1(@vue/compiler-sfc@3.5.43)(esbuild@0.28.2)(rolldown@1.2.9)(rollup@4.63.4)(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3))
zod: 4.6.5
transitivePeerDependencies:
- '@azure/app-configuration'
@@ -13352,6 +13345,130 @@ snapshots:
- vue
- webpack
+ '@nuxt/ui@4.11.2(fa5844bfdc74c64dd01c986666e59821)':
+ dependencies:
+ '@floating-ui/dom': 1.8.0
+ '@iconify/vue': 5.0.2(vue@3.5.43(typescript@6.0.3))
+ '@nuxt/fonts': 0.14.0(@vercel/functions@3.9.8(ws@8.21.3))(db0@0.3.4)(esbuild@0.28.2)(ioredis@5.11.1(supports-color@10.2.2))(magic-string@1.4.1)(magicast@0.5.5)(oxc-parser@0.149.0)(rolldown@1.2.9)(rollup@4.63.4)(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))
+ '@nuxt/icon': 2.5.1(magic-string@1.4.1)(magicast@0.5.5)(oxc-parser@0.149.0)(rolldown@1.2.9)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.9)(rollup@4.63.4)(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3))
+ '@nuxt/kit': 4.5.2(magic-string@1.4.1)(magicast@0.5.5)(oxc-parser@0.149.0)(rolldown@1.2.9)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.9)(rollup@4.63.4)(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))
+ '@nuxt/schema': 4.5.2
+ '@nuxtjs/color-mode': 4.0.1(magic-string@1.4.1)(magicast@0.5.5)(oxc-parser@0.149.0)(rolldown@1.2.9)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.9)(rollup@4.63.4)(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))
+ '@standard-schema/spec': 1.1.0
+ '@tailwindcss/postcss': 4.3.3
+ '@tailwindcss/vite': 4.3.3(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))
+ '@tanstack/vue-table': 8.21.3(vue@3.5.43(typescript@6.0.3))
+ '@tanstack/vue-virtual': 3.13.39(vue@3.5.43(typescript@6.0.3))
+ '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3)
+ '@tiptap/extension-bubble-menu': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)
+ '@tiptap/extension-code': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))
+ '@tiptap/extension-collaboration': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@tiptap/y-tiptap@3.0.9(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.4)(y-protocols@1.0.7(yjs@13.6.32))(yjs@13.6.32))(yjs@13.6.32)
+ '@tiptap/extension-drag-handle': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/extension-collaboration@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@tiptap/y-tiptap@3.0.9))(@tiptap/extension-node-range@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@tiptap/y-tiptap@3.0.9)
+ '@tiptap/extension-drag-handle-vue-3': 3.31.3(@tiptap/extension-drag-handle@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/extension-collaboration@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@tiptap/y-tiptap@3.0.9))(@tiptap/extension-node-range@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@tiptap/y-tiptap@3.0.9))(@tiptap/pm@3.31.3)(@tiptap/vue-3@3.31.3(@floating-ui/dom@1.8.0)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(vue@3.5.43(typescript@6.0.3)))(vue@3.5.43(typescript@6.0.3))
+ '@tiptap/extension-floating-menu': 3.31.3(@floating-ui/dom@1.8.0)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)
+ '@tiptap/extension-horizontal-rule': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)
+ '@tiptap/extension-image': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))
+ '@tiptap/extension-mention': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@tiptap/suggestion@3.31.3(@floating-ui/dom@1.8.0)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))
+ '@tiptap/extension-node-range': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)
+ '@tiptap/extension-placeholder': 3.31.3(@tiptap/extensions@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))
+ '@tiptap/markdown': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)
+ '@tiptap/pm': 3.31.3
+ '@tiptap/starter-kit': 3.31.3
+ '@tiptap/suggestion': 3.31.3(@floating-ui/dom@1.8.0)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)
+ '@tiptap/vue-3': 3.31.3(@floating-ui/dom@1.8.0)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(vue@3.5.43(typescript@6.0.3))
+ '@unhead/vue': 3.4.1(@oxc-project/types@0.150.0)(esbuild@0.28.2)(lightningcss@1.33.0)(oxc-parser@0.149.0)(rolldown@1.2.9)(rollup@4.63.4)(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3))
+ '@vueuse/core': 14.4.0(vue@3.5.43(typescript@6.0.3))
+ '@vueuse/integrations': 14.4.0(focus-trap@7.8.0)(fuse.js@7.5.0)(vue@3.5.43(typescript@6.0.3))
+ '@vueuse/shared': 14.4.0(vue@3.5.43(typescript@6.0.3))
+ colortranslator: 5.0.0
+ consola: 3.4.2
+ defu: 6.1.7
+ embla-carousel-auto-height: 8.6.0(embla-carousel@8.6.0)
+ embla-carousel-auto-scroll: 8.6.0(embla-carousel@8.6.0)
+ embla-carousel-autoplay: 8.6.0(embla-carousel@8.6.0)
+ embla-carousel-class-names: 8.6.0(embla-carousel@8.6.0)
+ embla-carousel-fade: 8.6.0(embla-carousel@8.6.0)
+ embla-carousel-vue: 8.6.0(vue@3.5.43(typescript@6.0.3))
+ embla-carousel-wheel-gestures: 8.1.0(embla-carousel@8.6.0)
+ fuse.js: 7.5.0
+ hookable: 6.1.2
+ knitwork: 1.3.0
+ magic-string: 1.4.1
+ mlly: 1.8.2
+ motion-v: 2.4.4(@vueuse/core@14.4.0(vue@3.5.43(typescript@6.0.3)))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(vue@3.5.43(typescript@6.0.3))
+ ohash: 2.0.12
+ pathe: 2.0.3
+ reka-ui: 2.10.4(vue@3.5.43(typescript@6.0.3))
+ scule: 1.3.0
+ tailwind-merge: 3.7.0
+ tailwind-variants: 3.3.1(tailwind-merge@3.7.0)(tailwindcss@4.3.3)
+ tailwindcss: 4.3.3
+ tinyglobby: 0.2.17
+ typescript: 6.0.3
+ ufo: 1.6.4
+ unplugin: 3.4.0(esbuild@0.28.2)(rolldown@1.2.9)(rollup@4.63.4)(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))
+ unplugin-auto-import: 21.1.0(@nuxt/kit@4.5.2(magic-string@1.4.1)(magicast@0.5.5)(oxc-parser@0.149.0)(rolldown@1.2.9)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.9)(rollup@4.63.4)(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))))(@vueuse/core@14.4.0(vue@3.5.43(typescript@6.0.3)))(esbuild@0.28.2)(oxc-parser@0.149.0)(rolldown@1.2.9)(rollup@4.63.4)(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))
+ unplugin-vue-components: 32.1.0(@nuxt/kit@4.5.2(magic-string@1.4.1)(magicast@0.5.5)(oxc-parser@0.149.0)(rolldown@1.2.9)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.9)(rollup@4.63.4)(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))))(esbuild@0.28.2)(rolldown@1.2.9)(rollup@4.63.4)(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3))
+ vaul-vue: 0.4.1(reka-ui@2.10.4(vue@3.5.43(typescript@6.0.3)))(vue@3.5.43(typescript@6.0.3))
+ vue-component-type-helpers: 3.3.11
+ optionalDependencies:
+ '@internationalized/date': 3.12.4
+ '@internationalized/number': 3.6.8
+ ai: 7.0.112(zod@4.6.5)
+ vue-router: 5.3.1(@vue/compiler-sfc@3.5.43)(esbuild@0.28.2)(rolldown@1.2.9)(rollup@4.63.4)(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3))
+ zod: 4.6.5
+ transitivePeerDependencies:
+ - '@azure/app-configuration'
+ - '@azure/cosmos'
+ - '@azure/data-tables'
+ - '@azure/identity'
+ - '@azure/keyvault-secrets'
+ - '@azure/storage-blob'
+ - '@capacitor/preferences'
+ - '@deno/kv'
+ - '@farmfe/core'
+ - '@netlify/blobs'
+ - '@oxc-project/types'
+ - '@planetscale/database'
+ - '@rsbuild/core'
+ - '@rspack/core'
+ - '@unhead/cli'
+ - '@upstash/redis'
+ - '@vercel/blob'
+ - '@vercel/functions'
+ - '@vercel/kv'
+ - '@vitejs/devtools-kit'
+ - '@vue/composition-api'
+ - async-validator
+ - aws4fetch
+ - axios
+ - bun-types-no-globals
+ - change-case
+ - db0
+ - drauu
+ - embla-carousel
+ - esbuild
+ - focus-trap
+ - idb-keyval
+ - ioredis
+ - jwt-decode
+ - lightningcss
+ - magicast
+ - nprogress
+ - oxc-parser
+ - qrcode
+ - react
+ - react-dom
+ - rolldown
+ - rollup
+ - sortablejs
+ - universal-cookie
+ - unloader
+ - uploadthing
+ - vite
+ - vue
+ - webpack
+
'@nuxt/vite-builder@4.5.2(0ca8c220c7096baa3c427dc8d7242a87)':
dependencies:
'@nuxt/kit': 4.5.2(magic-string@1.4.1)(magicast@0.5.5)(oxc-parser@0.149.0)(rolldown@1.2.9)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.9)(rollup@4.63.4)(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))
@@ -16144,7 +16261,7 @@ snapshots:
'@iconify-json/vscode-icons': 1.2.79
'@iconify/vue': 5.0.2(vue@3.5.43(typescript@6.0.3))
'@nuxt/kit': 4.5.2(magic-string@1.4.1)(magicast@0.5.5)(oxc-parser@0.149.0)(rolldown@1.2.9)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.9)(rollup@4.63.4)(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))
- '@nuxt/ui': 4.11.2(c77efd868fa853b6efbc5c86610e740e)
+ '@nuxt/ui': 4.11.2(48eec6f14dd5da94c5723622cb20c05a)
'@nuxtjs/mcp-toolkit': 0.21.0(@vue/compiler-sfc@3.5.43)(h3@1.15.11)(magic-string@1.4.1)(magicast@0.5.5)(oxc-parser@0.149.0)(rolldown@1.2.9)(rollup@4.63.4)(supports-color@10.2.2)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.9)(rollup@4.63.4)(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3))(zod@4.6.5)
'@nuxtjs/robots': 6.2.3(7a54b3daef8a97a6c1e9da9b246637d3)
'@nuxtjs/sitemap': 8.5.1(7a54b3daef8a97a6c1e9da9b246637d3)
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 51c697ee5..5bae928d7 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -93,7 +93,6 @@ catalog:
entities: ^8.1.0
github-slugger: ^2.0.0
hast-util-to-string: ^3.0.1
- htmlparser2: ^12.0.0
js-yaml: ^5.4.2
katex: ^0.18.7
markdown-exit: 1.3.0
diff --git a/test/bundle.test.ts b/test/bundle.test.ts
index bd7e87ff7..c0e22f1f8 100644
--- a/test/bundle.test.ts
+++ b/test/bundle.test.ts
@@ -60,14 +60,14 @@ describe('package bundle size', { timeout: 60_000 }, () => {
expect(report).toMatchInlineSnapshot(`
{
- "@comark/angular": "56.4k (72 files)",
+ "@comark/angular": "56.2k (72 files)",
"@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.9k (84 files)",
+ "@comark/svelte": "45.8k (84 files)",
"@comark/vue": "56.2k (80 files)",
- "comark": "377k (160 files)",
+ "comark": "381k (150 files)",
}
`)
})