Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
## Input

```md
---
# Heading
```

## AST

```json
{
"frontmatter": {},
"meta": {},
"nodes": [
[
"hr",
{}
],
[
"h1",
{
"id": "heading"
},
"Heading"
]
]
}
```

## HTML

```html
<hr />
<h1 id="heading">Heading</h1>
```

## Markdown

```md
---

# Heading
```
32 changes: 32 additions & 0 deletions packages/comark/SPEC/common-mark/horizontal-rule-bare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Input

```md
---
```

## AST

```json
{
"frontmatter": {},
"meta": {},
"nodes": [
[
"hr",
{}
]
]
}
```

## HTML

```html
<hr />
```

## Markdown

```md
---
```
14 changes: 10 additions & 4 deletions packages/comark/src/internal/parse/auto-close/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@ import { closeTables } from './table.ts'
* Processes markdown in O(n) time by scanning character-by-character
*
* @param markdown - The markdown content to auto-close
* @param options - `frontmatter` completes an unclosed leading frontmatter block.
* @returns The markdown with unclosed syntax closed
*/
export function autoCloseMarkdown(markdown: string): string {
export function autoCloseMarkdown(markdown: string, options: { frontmatter?: boolean } = {}): string {
if (!markdown || markdown === '') return markdown

const lines = markdown.split('\n')
const n = lines.length

// Single linear pass to collect document state
let inFrontmatter = false
// Whether the open frontmatter block has received any content. Empty
// frontmatter is not valid (`parseFrontmatter` ignores it), so a lone `---`
// is a thematic break, not an unclosed frontmatter block to complete.
let frontmatterHasContent = false
let inBlockMath = false
let tableStart = -1
// Tag name when inside a raw-text HTML element (`<style>`, `<script>`,
Expand Down Expand Up @@ -57,12 +62,13 @@ export function autoCloseMarkdown(markdown: string): string {
}

// Frontmatter: only starts at document line 0
if (idx === 0 && trimmed === '---') {
if (idx === 0 && options.frontmatter && trimmed === '---') {
inFrontmatter = true
continue
}
if (inFrontmatter) {
if (trimmed === '---') inFrontmatter = false
else if (trimmed !== '') frontmatterHasContent = true
continue
}

Expand Down Expand Up @@ -152,8 +158,8 @@ export function autoCloseMarkdown(markdown: string): string {
result = closeTables(result)
}

// Close unclosed frontmatter
if (inFrontmatter) {
// Complete an unclosed frontmatter block only when `frontmatter` is enabled,
if (inFrontmatter && frontmatterHasContent) {
const lastTrimmed = lines[lastIdx].trim()
if (lastTrimmed === '-' || lastTrimmed === '--') {
result += '-'.repeat(3 - lastTrimmed.length)
Expand Down
2 changes: 1 addition & 1 deletion packages/comark/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export function createParse<const TPlugins extends readonly ComarkPlugin<any, an
}

if (autoClose) {
state.markdown = autoCloseMarkdown(state.markdown)
state.markdown = autoCloseMarkdown(state.markdown, { frontmatter: opts.streaming })
}

for (const plugin of options.plugins || []) {
Expand Down
43 changes: 31 additions & 12 deletions packages/comark/test/auto-close.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,30 +497,41 @@ describe('frontmatter', () => {
const expected = '---\ntitle: Test\n---'
expect(autoCloseMarkdown(input)).toBe(expected)
})
it('should handle frontmatter partial', () => {
// Partial frontmatter is only completed when `frontmatter: true` β€” for a
// complete document a leading `---` with no closing delimiter is a thematic
// break, so completing it would swallow the body (see the default cases below).
it('should complete partial frontmatter when frontmatter option is enabled', () => {
const input = '---\ntitle: Test'
const expected = '---\ntitle: Test\n---'
expect(autoCloseMarkdown(input)).toBe(expected)
expect(autoCloseMarkdown(input, { frontmatter: true })).toBe(expected)
})
it('should handle frontmatter partial', () => {
it('should complete a partial closing delimiter (-) when frontmatter option is enabled', () => {
const input = '---\ntitle: Test\n-'
const expected = '---\ntitle: Test\n---'
expect(autoCloseMarkdown(input)).toBe(expected)
expect(autoCloseMarkdown(input, { frontmatter: true })).toBe(expected)
})
it('should handle frontmatter partial', () => {
it('should complete a partial closing delimiter (--) when frontmatter option is enabled', () => {
const input = '---\ntitle: Test\n--'
const expected = '---\ntitle: Test\n---'
expect(autoCloseMarkdown(input)).toBe(expected)
expect(autoCloseMarkdown(input, { frontmatter: true })).toBe(expected)
})
it('should handle frontmatter partial 2', () => {
it('should complete partial frontmatter with an empty value when frontmatter option is enabled', () => {
const input = '---\ntitle: '
const expected = '---\ntitle: \n---'
expect(autoCloseMarkdown(input)).toBe(expected)
expect(autoCloseMarkdown(input, { frontmatter: true })).toBe(expected)
})
it('should handle frontmatter partial 3', () => {
it('should complete partial frontmatter with a trailing newline when frontmatter option is enabled', () => {
const input = '---\ntitle: Test\n'
const expected = '---\ntitle: Test\n---'
expect(autoCloseMarkdown(input)).toBe(expected)
expect(autoCloseMarkdown(input, { frontmatter: true })).toBe(expected)
})

it('should not complete unclosed frontmatter by default', () => {
// Default (`frontmatter: false`): a leading `---` with content but no close
// is a thematic break followed by body β€” completing it would swallow the body.
const input = '---\ntitle: Test'
expect(autoCloseMarkdown(input)).toBe(input)
expect(autoCloseMarkdown('---\n# Heading')).toBe('---\n# Heading')
})

it('should handle frontmatter with content after', () => {
Expand All @@ -541,9 +552,17 @@ describe('frontmatter', () => {
expect(autoCloseMarkdown(input)).toBe(expected)
})

it('should handle just opening ---', () => {
it('should not complete a lone --- (thematic break, not frontmatter)', () => {
// A lone `---` has no frontmatter content; completing it to `---\n---`
// would parse as two thematic breaks (#268).
const input = '---'
const expected = '---\n---'
const expected = '---'
expect(autoCloseMarkdown(input)).toBe(expected)
})

it('should not complete an empty --- opener', () => {
const input = '---\n'
const expected = '---\n'
expect(autoCloseMarkdown(input)).toBe(expected)
})

Expand Down
Loading