Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions packages/comark/src/internal/parse/auto-close/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@ import { closeTables } from './table.ts'
* @param options - `frontmatter` completes an unclosed leading frontmatter block.
* `syntax: false` disables Comark component-fence handling (`::` closers and
* props braces), for input parsed without the components plugin.
* `attributes: true` still treats `{...}` as an attribute scope (so `_` / `*`
* inside values are not closed) without enabling component fences.
* @returns The markdown with unclosed syntax closed
*/
export function autoCloseMarkdown(markdown: string, options: { frontmatter?: boolean; syntax?: boolean } = {}): string {
export function autoCloseMarkdown(
markdown: string,
options: { frontmatter?: boolean; syntax?: boolean; attributes?: boolean } = {}
): string {
if (!markdown || markdown === '') return markdown

const syntaxEnabled = options.syntax !== false
const attributesEnabled = options.attributes ?? syntaxEnabled

const lines = markdown.split('\n')
const n = lines.length
Expand Down Expand Up @@ -152,7 +158,7 @@ export function autoCloseMarkdown(markdown: string, options: { frontmatter?: boo
// Fix inline markers on last line (skip inside block-level structures)
const lastIdx = n - 1
if (!inFrontmatter && !inBlockMath && lines[lastIdx].trim() !== '$$') {
lines[lastIdx] = closeInlineMarkersLinear(lines[lastIdx], syntaxEnabled)
lines[lastIdx] = closeInlineMarkersLinear(lines[lastIdx], attributesEnabled)
}

let result = lines.join('\n')
Expand Down Expand Up @@ -256,9 +262,9 @@ function scanDelimiterRun(line: string, start: number, marker: string) {
* Closes inline markers (*, **, ***, ~~, `, $, $$, [, () on the last line
* without using regex - pure character scanning in O(n) time
*
* With `syntax` false, `{...}` is literal text instead of an attribute scope.
* With `attributesEnabled` false, `{...}` is literal text instead of an attribute scope.
*/
function closeInlineMarkersLinear(line: string, syntax: boolean): string {
function closeInlineMarkersLinear(line: string, attributesEnabled: boolean): string {
const len = line.length
if (len === 0) return line

Expand Down Expand Up @@ -318,11 +324,11 @@ function closeInlineMarkersLinear(line: string, syntax: boolean): string {
continue
}

if (syntax && ch === '{' && prevCh !== ' ') {
if (attributesEnabled && ch === '{' && prevCh !== ' ') {
inAttributes++
continue
}
if (syntax && ch === '}') {
if (attributesEnabled && ch === '}') {
if (inAttributes > 0) inAttributes--
continue
}
Expand Down
1 change: 1 addition & 0 deletions packages/comark/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export function createMarkdownParser<const TPlugins extends readonly ComarkPlugi
autoCloseMarkdown(state.markdown, {
frontmatter: hasPlugin('frontmatter') && opts.streaming,
syntax: hasPlugin('components'),
attributes: hasPlugin('components') || hasPlugin('attributes'),
})
)
}
Expand Down
13 changes: 13 additions & 0 deletions packages/comark/test/auto-close.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,19 @@ describe('autoCloseMarkdown - syntax option', () => {
expect(autoCloseMarkdown('text{.cls **bold', { syntax: false })).toBe('text{.cls **bold**')
})

it('skips _ inside attribute values when attributes are enabled without component syntax', () => {
expect(
autoCloseMarkdown('this is a [link with an attribute](https://example.com){target="_blank"}', {
syntax: false,
attributes: true,
})
).toBe('this is a [link with an attribute](https://example.com){target="_blank"}')
})

it('does not treat unclosed :: as a component when only attributes are enabled', () => {
expect(autoCloseMarkdown('::alert\nContent', { syntax: false, attributes: true })).toBe('::alert\nContent')
})

it('still completes frontmatter with syntax: false', () => {
expect(autoCloseMarkdown('---\ntitle: Hello', { frontmatter: true, syntax: false })).toBe('---\ntitle: Hello\n---')
})
Expand Down
23 changes: 23 additions & 0 deletions packages/comark/test/plugins/default-plugins.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import { parseMarkdown } from '../../src/parse'
import attributes from '../../src/plugins/attributes'
import components from '../../src/plugins/components'
import frontmatter from '../../src/plugins/frontmatter'
import html from '../../src/plugins/html'
Expand Down Expand Up @@ -128,5 +129,27 @@ describe('default plugin options', () => {
})
expect(tree.nodes).toEqual([['alert', {}, 'Content']])
})

it('renders attributes when only attributes enabled', async () => {
const tree = await parseMarkdown('this is a [link with an attribute](https://example.com){target="_blank"}', {
registerDefaultPlugins: false,
plugins: [attributes()],
})
expect(tree.nodes).toEqual([
[
'p',
{},
'this is a ',
[
'a',
{
href: 'https://example.com',
target: '_blank',
},
'link with an attribute',
],
],
])
})
})
})