From 7d86564f32edf111760f4244f60bf0286f925f89 Mon Sep 17 00:00:00 2001 From: Snack Date: Thu, 30 Jul 2026 17:42:11 +0900 Subject: [PATCH 1/3] fix(breaks): preserve newlines in whitespace-sensitive elements The breaks plugin converted every newline in string children to a `br` node, including inside code and math blocks where newlines must stay literal. Skip visiting whitespace-sensitive tags (`pre`, `code`, `math`, `script`, `style`, `textarea`) so their content is untouched. Fixes #311 Co-Authored-By: Claude Fable 5 --- packages/comark/src/plugins/breaks.ts | 5 ++++- packages/comark/test/breaks.test.ts | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/comark/src/plugins/breaks.ts b/packages/comark/src/plugins/breaks.ts index 5d98ae95..093aba02 100644 --- a/packages/comark/src/plugins/breaks.ts +++ b/packages/comark/src/plugins/breaks.ts @@ -1,12 +1,15 @@ import { defineComarkPlugin } from '../utils/helpers.ts' import { visit } from '../utils/index.ts' +// Tags whose text content is whitespace-sensitive and must keep literal newlines +const IGNORED_TAGS = new Set(['pre', 'code', 'math', 'script', 'style', 'textarea']) + export default defineComarkPlugin(() => ({ name: 'breaks', post(state) { visit( state.tree, - (node) => Array.isArray(node) && node.length > 2, + (node) => Array.isArray(node) && node.length > 2 && !IGNORED_TAGS.has(node[0] as string), (node) => { const parent = node as any[] const newParent = [parent[0], parent[1]] diff --git a/packages/comark/test/breaks.test.ts b/packages/comark/test/breaks.test.ts index e0391fb0..7df57c94 100644 --- a/packages/comark/test/breaks.test.ts +++ b/packages/comark/test/breaks.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { parse } from '../src/parse' import breaks from '../src/plugins/breaks' +import math from '../src/plugins/math' describe('breaks plugin', () => { it('should replace all occurrences of \n with the comark :br component', async () => { @@ -9,4 +10,21 @@ describe('breaks plugin', () => { expect(tree.nodes).toEqual([['p', {}, 'She said "hello world" to', ['br', {}], 'him.']]) }) + + it('should not replace \n inside code blocks', async () => { + const md = 'soft\nbreak\n\n```\nline1\nline2\n```' + const tree = await parse(md, { plugins: [breaks()] }) + + expect(tree.nodes).toEqual([ + ['p', {}, 'soft', ['br', {}], 'break'], + ['pre', {}, ['code', {}, 'line1\nline2']], + ]) + }) + + it('should not replace \n inside math blocks', async () => { + const md = '$$\nx = 1 \\\\\ny = 2\n$$' + const tree = await parse(md, { plugins: [math(), breaks()] }) + + expect(tree.nodes).toEqual([['math', { class: 'math block', content: 'x = 1 \\\\\ny = 2' }, 'x = 1 \\\\\ny = 2']]) + }) }) From a4497920ac76a99e28eca8b020596f34b4a4b59b Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 30 Jul 2026 15:38:23 +0200 Subject: [PATCH 2/3] Update bundle.test.ts --- 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 ed554af1..a0674c76 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -67,7 +67,7 @@ describe('package bundle size', { timeout: 60_000 }, () => { "@comark/react": "40.5k (58 files)", "@comark/svelte": "40.6k (66 files)", "@comark/vue": "57.3k (62 files)", - "comark": "372k (136 files)", + "comark": "374k (136 files", } `) }) From 67557b938db59ae110b57b021097cfdcba41a617 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 30 Jul 2026 15:48:25 +0200 Subject: [PATCH 3/3] Update bundle.test.ts --- 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 85e44758..ec6f74d5 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -67,7 +67,7 @@ describe('package bundle size', { timeout: 60_000 }, () => { "@comark/react": "40.5k (58 files)", "@comark/svelte": "40.6k (66 files)", "@comark/vue": "57.3k (62 files)", - "comark": "374k (136 files", + "comark": "374k (136 files)", } `) })