From 9114dfbb9a66c63e7f2ae2d64456dde703197601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Fri, 21 Aug 2026 11:43:13 +0200 Subject: [PATCH 1/9] fix(comark): choose code fences that content cannot close MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pre, mermaid, and YAML attribute blocks picked a fixed 3-backtick fence (switching to tildes only when the content contained backticks). Code containing a ~~~ line closed the fence early on re-parse, turning inert code into live document structure (e.g. ::alert components) — and the same applied to mermaid content and YAML props blocks. Add pickFence(): scan the content for the longest line-start run of each fence character and emit the shorter side, one character longer than any run present. Also stop growing mermaid content by a blank line per round trip. --- .../src/internal/stringify/attributes.ts | 3 +- .../comark/src/internal/stringify/fence.ts | 24 ++++++++++ .../internal/stringify/handlers/mermaid.ts | 8 +++- .../src/internal/stringify/handlers/pre.ts | 3 +- packages/comark/test/roundtrip-safety.test.ts | 48 +++++++++++++++++++ 5 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 packages/comark/src/internal/stringify/fence.ts create mode 100644 packages/comark/test/roundtrip-safety.test.ts diff --git a/packages/comark/src/internal/stringify/attributes.ts b/packages/comark/src/internal/stringify/attributes.ts index adc76c8f..a50382b3 100644 --- a/packages/comark/src/internal/stringify/attributes.ts +++ b/packages/comark/src/internal/stringify/attributes.ts @@ -1,5 +1,6 @@ import { stringifyYaml } from '../yaml.ts' import { get } from '../../utils/index.ts' +import { pickFence } from './fence.ts' import type { NodeRenderData } from '../../types.ts' export interface ResolveAttributesOptions { @@ -270,6 +271,6 @@ export function comarkYamlAttributes( return `---\n${yamlContent}\n---` } - const fence = yamlContent.includes('```') ? '~~~' : '```' + const fence = pickFence(yamlContent) return `${fence}yaml [props]\n${yamlContent}\n${fence}` } diff --git a/packages/comark/src/internal/stringify/fence.ts b/packages/comark/src/internal/stringify/fence.ts new file mode 100644 index 00000000..87b435d9 --- /dev/null +++ b/packages/comark/src/internal/stringify/fence.ts @@ -0,0 +1,24 @@ +/** + * Choose a code fence for `content` that the content itself cannot close. + * + * A fence closes on any line with up to 3 leading spaces followed by a run of + * the same fence character at least as long as the opening fence. Scan for + * the longest such runs of both characters, then emit the character with the + * shorter maximum run, one character longer than that run (minimum 3). + */ +export function pickFence(content: string): string { + let maxBackticks = 0 + let maxTildes = 0 + for (const line of content.split('\n')) { + const match = /^ {0,3}(`+|~+)/.exec(line) + if (!match) continue + const run = match[1] + if (run[0] === '`') { + if (run.length > maxBackticks) maxBackticks = run.length + } else if (run.length > maxTildes) { + maxTildes = run.length + } + } + const char = maxBackticks <= maxTildes ? '`' : '~' + return char.repeat(Math.max(3, (char === '`' ? maxBackticks : maxTildes) + 1)) +} diff --git a/packages/comark/src/internal/stringify/handlers/mermaid.ts b/packages/comark/src/internal/stringify/handlers/mermaid.ts index 8a6fe236..c93ff458 100644 --- a/packages/comark/src/internal/stringify/handlers/mermaid.ts +++ b/packages/comark/src/internal/stringify/handlers/mermaid.ts @@ -1,14 +1,18 @@ import type { State } from 'comark/render' import type { ElementNode } from 'comark' import { comarkAttributes } from '../attributes.ts' +import { pickFence } from '../fence.ts' -const fence = '```' export function mermaid(node: ElementNode, state: State) { const [_, attributes] = node const { content, ...rest } = attributes const attrs = comarkAttributes(rest) + // Parsed fence bodies keep one trailing newline — drop it so serialization + // doesn't grow a blank line on every round trip. + const body = String(content ?? '').replace(/\n$/, '') + const fence = pickFence(body) - return `${fence}mermaid${attrs ? ` ${attrs}` : ''}\n${content}\n${fence}${state.context.blockSeparator}` + return `${fence}mermaid${attrs ? ` ${attrs}` : ''}\n${body}\n${fence}${state.context.blockSeparator}` } diff --git a/packages/comark/src/internal/stringify/handlers/pre.ts b/packages/comark/src/internal/stringify/handlers/pre.ts index 93268b8c..a4299693 100644 --- a/packages/comark/src/internal/stringify/handlers/pre.ts +++ b/packages/comark/src/internal/stringify/handlers/pre.ts @@ -2,6 +2,7 @@ import type { State } from 'comark/render' import type { ElementNode } from 'comark' import { textContent } from '../../../utils/index.ts' import { comarkAttributes, userBlockAttrs } from '../attributes.ts' +import { pickFence } from '../fence.ts' export function pre(node: ElementNode, state: State) { const [_, attributes, ...children] = node @@ -25,7 +26,7 @@ export function pre(node: ElementNode, state: State) { const meta = attributes.meta ? ' ' + attributes.meta : '' const code = String(node[1]?.code || textContent(node)).trim() - const fence = code.includes('```') ? '~~~' : '```' + const fence = pickFence(code) const fenceBlock = fence + language + filename + highlights + meta + '\n' + code + '\n' + fence // Extra user attrs that can't ride on the fence info string round-trip via diff --git a/packages/comark/test/roundtrip-safety.test.ts b/packages/comark/test/roundtrip-safety.test.ts new file mode 100644 index 00000000..3e53e6a5 --- /dev/null +++ b/packages/comark/test/roundtrip-safety.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest' +import { parseMarkdown } from '../src/parse' +import { renderMarkdown } from '../src/render' +import mermaid from '../src/plugins/mermaid' +import type { MarkdownDocument } from '../src/types' + +async function roundTrip(md: string, options?: Parameters[1]) { + const t1 = await parseMarkdown(md, options) + const rendered = await renderMarkdown(t1) + const t2 = await parseMarkdown(rendered, options) + return { t1, t2, rendered } +} + +describe('code fence selection', () => { + it('picks a fence that content with both ``` and ~~~ cannot close', async () => { + const code = 'let a = 1\n```\n~~~\n::alert\nowned\n::' + const doc = { + frontmatter: {}, + meta: {}, + nodes: [['pre', { language: 'js' }, ['code', { class: 'language-js' }, code]]], + } as unknown as MarkdownDocument + const rendered = await renderMarkdown(doc) + const t2 = await parseMarkdown(rendered) + expect(t2.nodes).toEqual(doc.nodes) + }) + + it('widens the fence past the longest backtick run in the content', async () => { + const code = 'const s = ""\n````\nend' + const doc = { + frontmatter: {}, + meta: {}, + nodes: [['pre', { language: 'js' }, ['code', { class: 'language-js' }, code]]], + } as unknown as MarkdownDocument + const rendered = await renderMarkdown(doc) + const t2 = await parseMarkdown(rendered) + expect(t2.nodes).toEqual(doc.nodes) + }) +}) + +describe('mermaid fence selection', () => { + it('does not let mermaid content escape its fence', async () => { + // A mermaid body containing ``` must not terminate the serialized fence + const md = '````mermaid\ngraph TD\n```\nA --> B\n````' + const { t1, t2 } = await roundTrip(md, { plugins: [mermaid()] }) + expect(t2.nodes).toEqual(t1.nodes) + }) +}) + From 7c53569c67f7fb8bf75bf2c34fa1531e8e8c766e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Fri, 21 Aug 2026 11:43:59 +0200 Subject: [PATCH 2/9] fix(comark): keep double quotes inside comarkAttributes values inert comarkAttributes emitted string values as key="value" with no escaping, so a value containing a double quote closed the attribute on re-parse and the remainder became new attacker-chosen attributes (or a binding resolving frontmatter data). The props parser does not unescape backslashes, so prefer single quotes when the value has none; fall back to \"-escaping when both quote kinds are present. --- .../src/internal/stringify/attributes.ts | 11 ++++++++++- packages/comark/test/roundtrip-safety.test.ts | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/comark/src/internal/stringify/attributes.ts b/packages/comark/src/internal/stringify/attributes.ts index a50382b3..5e189d80 100644 --- a/packages/comark/src/internal/stringify/attributes.ts +++ b/packages/comark/src/internal/stringify/attributes.ts @@ -177,7 +177,16 @@ export function comarkAttributes(attributes: Record) { return `${key}="${JSON.stringify(value).replace(/"/g, '\\"')}"` } - return `${key}="${value}"` + const str = String(value) + // A double quote inside a double-quoted value would terminate it early, + // letting the remainder become new attributes on re-parse. Single + // quotes round-trip cleanly when the value has no single quote; + // otherwise backslash-escape (the parser skips \" without terminating — + // safe, though it keeps the backslash in the value). + if (str.includes('"') && !str.includes("'")) { + return `${key}='${str}'` + } + return `${key}="${str.replace(/"/g, '\\"')}"` }) .join(' ') diff --git a/packages/comark/test/roundtrip-safety.test.ts b/packages/comark/test/roundtrip-safety.test.ts index 3e53e6a5..cf07f4d1 100644 --- a/packages/comark/test/roundtrip-safety.test.ts +++ b/packages/comark/test/roundtrip-safety.test.ts @@ -46,3 +46,21 @@ describe('mermaid fence selection', () => { }) }) +describe('comarkAttributes quoting', () => { + it('round-trips attribute values containing double quotes', async () => { + // The text sibling keeps the span inline in both parses (a lone + // `:span[...]` line is a leaf block component — pre-existing asymmetry). + const md = `say :span[hi]{title='a"b'} now` + const { t1, t2 } = await roundTrip(md) + expect(t2.nodes).toEqual(t1.nodes) + }) + + it('does not let a quoted value inject a new attribute on re-parse', async () => { + const md = `:span[hi]{title='x" bad="1'}` + const { t2 } = await roundTrip(md) + const span = (t2.nodes[0] as any[])[2] // p > span + expect(span[1].title).toBe('x" bad="1') + expect(span[1].bad).toBeUndefined() + }) +}) + From 530ef748c9ff3e97e2ed503457e42fa9106ef92f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Fri, 21 Aug 2026 11:44:35 +0200 Subject: [PATCH 3/9] fix(comark): escape component markers in text nodes during serialization A text node holding ::alert{...} (from an escaped or entity-encoded source) was emitted verbatim by renderMarkdown, so a re-parse turned literal text into a live component invocation; a bare :: line inside a block component also closed the fence early. escapeLeadingBlock now escapes lines starting with ':', and escapeInline escapes ':' that can start an inline component (after whitespace/start/*/_/[, before a name character) and '{' that opens an attribute block. --- .../comark/src/internal/stringify/state.ts | 39 +++++++++++++++++-- packages/comark/test/roundtrip-safety.test.ts | 30 ++++++++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/packages/comark/src/internal/stringify/state.ts b/packages/comark/src/internal/stringify/state.ts index e44ecbfd..f64c467f 100644 --- a/packages/comark/src/internal/stringify/state.ts +++ b/packages/comark/src/internal/stringify/state.ts @@ -211,10 +211,15 @@ function escapeHtml(text: string): string { // Characters that can start an inline markdown construct anywhere on a line: // `\` (escape), `` ` `` (code span), `*`/`_` (emphasis), `<` (raw HTML / -// autolink), `&` (character reference), `~` (strikethrough) and `[`/`]` -// (link/image). `\` is included so a literal backslash is preserved instead of -// merging with a following escape. -const inlineSyntax = /[\\`*_<&~[\]]/g +// autolink), `&` (character reference), `~` (strikethrough), `[`/`]` +// (link/image), plus the Comark markers `:` (inline component) and `{` +// (attribute block). `\` is included so a literal backslash is preserved +// instead of merging with a following escape. +const inlineSyntax = /[\\`*_<&~[\]{:]/g + +// Characters after which a `:` can start an inline component (`:name`). +// Mirrors ALLOWED_PREV_CHARS in the components plugin. +const COLON_PREV_CHARS = new Set([' ', '\t', '\n', '*', '_', '[']) /** * Escape characters in a markdown text node that would otherwise be @@ -264,6 +269,29 @@ function escapeInline(text: string): string { if (char === '&' && !/^&#?[a-zA-Z0-9]+;/.test(source.slice(offset))) { return char } + // `:` only starts an inline component in allowed positions, followed by a + // component-name character (`:name`, `:name[...]`, `:name{...}`). + if (char === ':') { + const prev = source[offset - 1] + const prevAllowed = prev === undefined || COLON_PREV_CHARS.has(prev) + const next = source[offset + 1] + if (prevAllowed && next !== undefined && /[a-zA-Z$]/.test(next)) { + return `\\${char}` + } + return char + } + // `{` only opens an attribute block when followed by a props-start + // character; `{{` (mustache) and `${` (template) never match. + if (char === '{') { + const prev = source[offset - 1] + if (prev === '{' || prev === '$') { + return char + } + if (/^\{[ \t]{0,3}[.#:a-zA-Z_]/.test(source.slice(offset, offset + 6))) { + return `\\${char}` + } + return char + } return `\\${char}` }) } @@ -288,5 +316,8 @@ function escapeLeadingBlock(line: string): string { if (/^\+([ \t]|$)/.test(line)) return `\\${line}` // Setext underline made of `=`. if (/^=+[ \t]*$/.test(line)) return `\\${line}` + // Comark block component / component fence: any run of leading colons + // (`:name`, `::name`, or a bare `::` fence close). + if (line[0] === ':') return `\\${line}` return line } diff --git a/packages/comark/test/roundtrip-safety.test.ts b/packages/comark/test/roundtrip-safety.test.ts index cf07f4d1..2e6cc090 100644 --- a/packages/comark/test/roundtrip-safety.test.ts +++ b/packages/comark/test/roundtrip-safety.test.ts @@ -46,6 +46,36 @@ describe('mermaid fence selection', () => { }) }) +describe('component marker escaping', () => { + it('keeps escaped :: markers as literal text through a round trip', async () => { + const { t2 } = await roundTrip('\\:\\:alert') + expect(t2.nodes).toEqual([['p', {}, '::alert']]) + }) + + it('keeps entity-encoded :: markers as literal text through a round trip', async () => { + const { t2 } = await roundTrip('::alert') + expect(t2.nodes).toEqual([['p', {}, '::alert']]) + }) + + it('escapes a bare :: line inside block component content', async () => { + // The middle paragraph is the literal text `::` (escaped in the source), + // which must not become a fence close after serialization. + const md = '::card\nfirst\n\n\\::\n\nsecond\n::' + const { t1, t2 } = await roundTrip(md) + expect(t2.nodes).toEqual(t1.nodes) + }) + + it('escapes inline component markers in text', async () => { + const { t2 } = await roundTrip('type \\:alert to continue') + expect(t2.nodes).toEqual([['p', {}, 'type :alert to continue']]) + }) + + it('escapes attribute block openers after inline elements', async () => { + const { t2 } = await roundTrip('**bold** \\{.red}') + expect(t2.nodes).toEqual([['p', {}, ['strong', {}, 'bold'], ' {.red}']]) + }) +}) + describe('comarkAttributes quoting', () => { it('round-trips attribute values containing double quotes', async () => { // The text sibling keeps the span inline in both parses (a lone From 4b22e62b76f7bd4387e7b31ba03807763ec0541d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Fri, 21 Aug 2026 11:45:07 +0200 Subject: [PATCH 4/9] style(comark): apply oxfmt to stringify internals --- packages/comark/test/roundtrip-safety.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/comark/test/roundtrip-safety.test.ts b/packages/comark/test/roundtrip-safety.test.ts index 2e6cc090..60f333b8 100644 --- a/packages/comark/test/roundtrip-safety.test.ts +++ b/packages/comark/test/roundtrip-safety.test.ts @@ -93,4 +93,3 @@ describe('comarkAttributes quoting', () => { expect(span[1].bad).toBeUndefined() }) }) - From 834e9799879b93ab6aba46b76395deff53f030df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Fri, 21 Aug 2026 13:09:13 +0200 Subject: [PATCH 5/9] test: update bundle size snapshot --- test/bundle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/bundle.test.ts b/test/bundle.test.ts index b553ccc9..8db1a1f5 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -67,7 +67,7 @@ describe('package bundle size', { timeout: 60_000 }, () => { "@comark/react": "43.6k (74 files)", "@comark/svelte": "43.9k (82 files)", "@comark/vue": "60.5k (78 files)", - "comark": "405k (154 files)", + "comark": "409k (160 files)", } `) }) From ebb1226fe7b05bbdbd845b29cd35da909895aeef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Fri, 21 Aug 2026 15:27:31 +0200 Subject: [PATCH 6/9] test: correct comark size and file count in bundle snapshot --- test/bundle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/bundle.test.ts b/test/bundle.test.ts index 8db1a1f5..6d62bb4d 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -67,7 +67,7 @@ describe('package bundle size', { timeout: 60_000 }, () => { "@comark/react": "43.6k (74 files)", "@comark/svelte": "43.9k (82 files)", "@comark/vue": "60.5k (78 files)", - "comark": "409k (160 files)", + "comark": "408k (156 files)", } `) }) From c41aaa224a03bd70a2b59ffa790d504dcb833ea5 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Tue, 25 Aug 2026 15:42:02 +0200 Subject: [PATCH 7/9] fix: remove duplicate escape html utility --- .../src/internal/stringify/attributes.ts | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/comark/src/internal/stringify/attributes.ts b/packages/comark/src/internal/stringify/attributes.ts index 0a014107..557cf70f 100644 --- a/packages/comark/src/internal/stringify/attributes.ts +++ b/packages/comark/src/internal/stringify/attributes.ts @@ -1,5 +1,5 @@ import { stringifyYaml } from '../yaml.ts' -import { get } from '../../utils/index.ts' +import { escapeHtml, get } from '../../utils/index.ts' import { isUnsafeUrlValue } from '../props-validation.ts' import { pickFence } from './fence.ts' import type { NodeRenderData } from '../../types.ts' @@ -227,16 +227,6 @@ export function comarkAttributes(attributes: Record) { return attrs.length > 0 ? `{${attrs}}` : '' } -/** - * Escape a value for interpolation into a double-quoted HTML attribute. - * Prevents attribute breakout (`"` terminating the value early) and markup - * injection (`<`/`>` closing the tag), which would otherwise bypass - * AST-level sanitization such as `comark/plugins/security`. - */ -function escapeHtmlAttribute(value: unknown): string { - return String(value).replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>') -} - // HTML attribute names must start with a letter/underscore/colon and may only // contain alphanumerics plus `_ : . -`. Anything else (quotes, spaces, …) // could break out of the attribute list, so such keys are dropped entirely. @@ -260,10 +250,10 @@ export function htmlAttributes(attributes: Record) { continue } if (typeof value === 'object' && value !== null) { - parts.push(`${key}="${escapeHtmlAttribute(JSON.stringify(value))}"`) + parts.push(`${key}="${escapeHtml(JSON.stringify(value))}"`) continue } - parts.push(`${key}="${escapeHtmlAttribute(value)}"`) + parts.push(`${key}="${escapeHtml(String(value))}"`) continue } @@ -274,11 +264,11 @@ export function htmlAttributes(attributes: Record) { if (value === false || value === null || value === undefined) continue if (typeof value === 'object') { - parts.push(`${key}="${escapeHtmlAttribute(JSON.stringify(value))}"`) + parts.push(`${key}="${escapeHtml(JSON.stringify(value))}"`) continue } - parts.push(`${key}="${escapeHtmlAttribute(value)}"`) + parts.push(`${key}="${escapeHtml(String(value))}"`) } return parts.join(' ') } From f27d920bfe71fb3c4a4a53ab143f5cf5a3ec5604 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Tue, 25 Aug 2026 15:46:39 +0200 Subject: [PATCH 8/9] test: add tests for pick fence utility --- packages/comark-vue/test/sink-props.test.ts | 2 +- packages/comark/test/fence.test.ts | 56 +++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 packages/comark/test/fence.test.ts diff --git a/packages/comark-vue/test/sink-props.test.ts b/packages/comark-vue/test/sink-props.test.ts index c3c765d6..68860153 100644 --- a/packages/comark-vue/test/sink-props.test.ts +++ b/packages/comark-vue/test/sink-props.test.ts @@ -7,7 +7,7 @@ import { MarkdownDocument } from '../src/components/MarkdownDocument.ts' function renderDocument(document: unknown) { const app = createSSRApp({ setup() { - return () => h(MarkdownDocument, { value: document }) + return () => h(MarkdownDocument, { value: document as any }) }, }) return renderToString(app as any) diff --git a/packages/comark/test/fence.test.ts b/packages/comark/test/fence.test.ts new file mode 100644 index 00000000..967b54b3 --- /dev/null +++ b/packages/comark/test/fence.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest' +import { pickFence } from '../src/internal/stringify/fence.ts' + +describe('pickFence', () => { + it('defaults to a 3-backtick fence for empty content', () => { + expect(pickFence('')).toBe('```') + }) + + it('defaults to a 3-backtick fence when content has no fence runs', () => { + expect(pickFence('hello\nworld')).toBe('```') + }) + + it('prefers the fence character with the shorter maximum run', () => { + // Backticks present, no tildes → switch to tildes so content cannot close it + expect(pickFence('```')).toBe('~~~') + expect(pickFence('line\n````\nline')).toBe('~~~') + expect(pickFence('`````')).toBe('~~~') + + // Tildes present, no backticks → stay on backticks + expect(pickFence('~~~')).toBe('```') + expect(pickFence('~~~~~')).toBe('```') + }) + + it('on a tie, prefers backticks one longer than the run', () => { + expect(pickFence('```\n~~~')).toBe('````') + expect(pickFence('````\n~~~~')).toBe('`````') + }) + + it('picks tildes when backticks need a longer fence', () => { + // maxB=4, maxT=3 → '~' length 4 + expect(pickFence('````\n~~~')).toBe('~~~~') + // maxB=4, maxT=3 → same + expect(pickFence('~~~\n````')).toBe('~~~~') + }) + + it('picks backticks when tildes need a longer fence', () => { + // maxB=3, maxT=5 → '`' length 4 + expect(pickFence('~~~~~\n```')).toBe('````') + }) + + it('only counts fence runs at the start of a line (up to 3 spaces)', () => { + // maxB=3, maxT=4 → prefer backticks length 4 + expect(pickFence(' ```\n ~~~~')).toBe('````') + // 4+ leading spaces is indented code, not a fence closer candidate + expect(pickFence(' ```\nhello')).toBe('```') + // Mid-line runs must not influence the fence + expect(pickFence('code with ``` inside\nand ~~~ too')).toBe('```') + }) + + it('uses the longest run of the chosen character, minimum 3', () => { + // only short backtick runs; still length-3 tilde fence + expect(pickFence('`\n``')).toBe('~~~') + // maxB=4, maxT=0 → ~~~ (min 3, not maxT+1) + expect(pickFence('`\n``\n```\n````')).toBe('~~~') + }) +}) From b7d363a9dd96fac3a517651a0bd3b72f900bb521 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:13:24 +0000 Subject: [PATCH 9/9] test: update bundle size snapshot --- test/bundle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/bundle.test.ts b/test/bundle.test.ts index a8f148fe..c788f46b 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -67,7 +67,7 @@ describe('package bundle size', { timeout: 60_000 }, () => { "@comark/react": "43.6k (74 files)", "@comark/svelte": "43.9k (82 files)", "@comark/vue": "60.5k (78 files)", - "comark": "408k (156 files)", + "comark": "422k (156 files)", } `) })