From ae4c95fe719ce7edb4ecbb21eecbe002aab3534d Mon Sep 17 00:00:00 2001 From: Sharath Kumar <7642133+builtbysharath@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:34:13 +0530 Subject: [PATCH] Fix container fence when content has a line of colons --- lib/index.js | 39 +++++++++++++++++++++-- test.js | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index 6008625..435c7d7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -146,7 +146,7 @@ export function directiveToMarkdown(options) { */ function handleDirective(node, _, state, info) { const tracker = state.createTracker(info) - const sequence = fence(node) + let sequence = fence(node) const exit = state.enter(node.type) let value = tracker.move(sequence + (node.name || '')) const attributesValue = attributes(node, state) @@ -200,7 +200,19 @@ export function directiveToMarkdown(options) { if (shallow && shallow.children && shallow.children.length > 0) { value += tracker.move('\n') - value += tracker.move(state.containerFlow(shallow, tracker.current())) + const content = state.containerFlow(shallow, tracker.current()) + value += tracker.move(content) + + // A line in the content of colons with only spaces and tabs around them + // (such as `:::` in code) closes the container if it has as many colons + // as the fence, so make the fence longer. + const size = longestColonLine(content) + 1 + + if (size > sequence.length) { + const extra = ':'.repeat(size - sequence.length) + value = extra + value + sequence += extra + } } value += tracker.move('\n' + sequence) @@ -560,6 +572,29 @@ function inlineDirectiveLabel(node) { ) } +/** + * Get the size of the longest sequence of colons on a line with only spaces + * and tabs around it. + * + * @param {string} value + * Serialized markdown. + * @returns {number} + * Size of the longest sequence, `0` if there are none. + */ +function longestColonLine(value) { + const expression = /^[\t ]*:+[\t ]*$/gm + let size = 0 + /** @type {RegExpExecArray | null} */ + let match + + while ((match = expression.exec(value))) { + const count = ccount(match[0], ':') + if (count > size) size = count + } + + return size +} + /** * Determine the fence size for a directive node. * diff --git a/test.js b/test.js index 6ae75ea..dc4635d 100644 --- a/test.js +++ b/test.js @@ -1051,6 +1051,96 @@ test('directiveToMarkdown()', async function (t) { } ) + await t.test( + 'should serialize containers w/ more colons than a line of colons in code', + async function () { + assert.equal( + toMarkdown( + { + type: 'containerDirective', + name: 'a', + children: [{type: 'code', value: 'b\n:::\nc'}] + }, + {extensions: [directiveToMarkdown()]} + ), + '::::a\n```\nb\n:::\nc\n```\n::::\n' + ) + } + ) + + await t.test( + 'should serialize containers w/ more colons than a line of colons, spaces, and tabs', + async function () { + assert.equal( + toMarkdown( + { + type: 'containerDirective', + name: 'a', + children: [{type: 'code', value: ' \t::::: '}] + }, + {extensions: [directiveToMarkdown()]} + ), + '::::::a\n```\n \t::::: \n```\n::::::\n' + ) + } + ) + + await t.test( + 'should not serialize containers w/ more colons for a line w/ colons and other characters', + async function () { + assert.equal( + toMarkdown( + { + type: 'containerDirective', + name: 'a', + children: [{type: 'code', value: ':::b\n::: c'}] + }, + {extensions: [directiveToMarkdown()]} + ), + ':::a\n```\n:::b\n::: c\n```\n:::\n' + ) + } + ) + + await t.test( + 'should serialize outer containers w/ more colons than inner containers that have code', + async function () { + assert.equal( + toMarkdown( + { + type: 'containerDirective', + name: 'a', + children: [ + { + type: 'containerDirective', + name: 'b', + children: [{type: 'code', value: ':::'}] + } + ] + }, + {extensions: [directiveToMarkdown()]} + ), + ':::::a\n::::b\n```\n:::\n```\n::::\n:::::\n' + ) + } + ) + + await t.test( + 'should roundtrip a container w/ a directive in code', + async function () { + const value = '::::a\n```markdown\n:::b\nc\n:::\n```\n::::\n' + const tree = fromMarkdown(value, { + extensions: [directive()], + mdastExtensions: [directiveFromMarkdown()] + }) + + assert.equal( + toMarkdown(tree, {extensions: [directiveToMarkdown()]}), + value + ) + } + ) + await t.test( 'should escape a `:` in phrasing when followed by an alpha', async function () {