Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
918fc8e
feat(parse)!: heal incomplete markdown only while streaming
benjamincanac Sep 10, 2026
e3046b7
Merge remote-tracking branch 'origin/main' into fix/auto-close-stream…
benjamincanac Sep 10, 2026
48f7a86
chore: refresh the bundle size snapshot
benjamincanac Sep 10, 2026
89514b8
docs(auto-close): fix review nits in autoClose docs and examples
benjamincanac Sep 10, 2026
18e41d8
Merge remote-tracking branch 'origin/main' into HEAD
benjamincanac Sep 11, 2026
9ba38d1
fix(docs): drop the misleading streaming tab
benjamincanac Sep 11, 2026
2c71815
refactor(auto-close): trim dead paths flagged in review
benjamincanac Sep 11, 2026
1422aa4
chore: refresh the bundle size snapshot
benjamincanac Sep 11, 2026
c4585f2
test(parse): cover the per-call streaming argument on parseMarkdown
benjamincanac Sep 11, 2026
141db83
fix(auto-close): respect a trailing backtick run when closing a code …
benjamincanac Sep 11, 2026
584c1ed
fix(auto-close): keep closers for markers pushed by one delimiter run
benjamincanac Sep 11, 2026
4178713
test(auto-close): assert healing is idempotent
benjamincanac Sep 11, 2026
d0625ad
docs: document the per-call parse options
benjamincanac Sep 11, 2026
677ac8f
chore: refresh the bundle size snapshot
benjamincanac Sep 11, 2026
1c645ce
test(auto-close): plain describe name
benjamincanac Sep 11, 2026
b46ff89
Merge remote-tracking branch 'origin/main' into fix/auto-close-stream…
benjamincanac Sep 15, 2026
7d2bb15
chore: trim the branch to the autoClose default change
benjamincanac Sep 15, 2026
eb14a08
chore: refresh the bundle size snapshot
benjamincanac Sep 15, 2026
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ describe('functionUnderTest', () => {
```typescript
const result = await parseMarkdown(markdownContent, {
autoUnwrap: true, // Remove <p> wrappers from single-paragraph containers
autoClose: true, // Auto-close incomplete syntax; also accepts (markdown) => string
autoClose: 'streaming', // Heal incomplete syntax while streaming; `true` heals every parse, also accepts (markdown) => string
unwrap: 'p', // Strip top-level wrapper tags (MDC unwrap); merges paragraphs
registerDefaultPlugins: true, // frontmatter, html, alert, task-list, components, attributes; false to disable
})
Expand Down
2 changes: 1 addition & 1 deletion docs/content/3.rendering/2.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ This is a **bold** statement with a [link](https://example.com).
| [`plugins`](#render-options-plugins) | `ComarkPlugin[]` | `[]` | Array of plugins |
| [`components`](#render-options-components) | `Record<string, fn>` | `{}` | Custom component renderers |
| [`data`](#render-options-data) | `Record<string, any>` | `undefined` | Data passed to component renderers |
| `autoClose` | `boolean` | `true` | Close incomplete Markdown and components before parsing |
| `autoClose` | `boolean \| 'streaming' \| fn` | `'streaming'` | Close incomplete Markdown and components before parsing. `'streaming'` only closes on a streaming parse, so a plain render leaves input as written |
| `autoUnwrap` | `boolean` | `true` | Remove a single paragraph wrapper inside components |
| `linkify` | `boolean` | `true` | Convert URL-like text into links |
| `registerDefaultPlugins` | `boolean` | `true` | Register default plugins (`frontmatter`, `html`, `alert`, `task-list`, `components`, `attributes`) |
Expand Down
4 changes: 2 additions & 2 deletions docs/content/3.rendering/3.vue.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ import { AppMarkdown } from './markdown'
| [`extends`](#code-markdown-code-definemarkdowncomponent-code-extends) | `ReturnType<typeof defineMarkdownComponent>` | `undefined` | Inherit plugins and components from another component |
| `name` | `string` | `undefined` | Component name for debugging |
| `autoUnwrap` | `boolean` | `true` | Automatically unwrap single block elements |
| `autoClose` | `boolean` | `true` | Auto-close incomplete markdown syntax |
| `autoClose` | `boolean \| 'streaming' \| fn` | `'streaming'` | Auto-close incomplete markdown syntax. Only closes on a streaming parse by default |
| `linkify` | `boolean` | `true` | Auto-convert URL-like text into links |
| `registerDefaultPlugins` | `boolean` | `true` | Register default plugins (`frontmatter`, `html`, `alert`, `task-list`, `components`, `attributes`) |
| [`plugins`](#code-markdown-props-code-plugins) | `ComarkPlugin[]` | `[]` | Array of plugins |
Expand Down Expand Up @@ -744,7 +744,7 @@ async function askAI(prompt: string) {
```

::callout{icon="i-lucide-info" color="info"}
`autoClose` is enabled by default: incomplete syntax like `**bold text` is automatically closed on every parse. Disable with `:options="{ autoClose: false }"`.
`autoClose` defaults to `'streaming'`: incomplete syntax like `**bold text` is closed when the `streaming` prop is set, and left as written otherwise. Set `:options="{ autoClose: true }"` to close incomplete syntax on every parse, or `:options="{ autoClose: false }"` to never close it.
::

### Caret
Expand Down
2 changes: 1 addition & 1 deletion docs/content/3.rendering/4.nuxt.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ async function askAI(prompt: string) {
```

::callout{icon="i-lucide-info" color="info"}
`autoClose` is enabled by default: incomplete syntax like `**bold text` is automatically closed on every parse. Disable with `:options="{ autoClose: false }"`.
`autoClose` defaults to `'streaming'`: incomplete syntax like `**bold text` is closed when the `streaming` prop is set, and left as written otherwise. Set `:options="{ autoClose: true }"` to close incomplete syntax on every parse, or `:options="{ autoClose: false }"` to never close it.
::

### Caret
Expand Down
4 changes: 2 additions & 2 deletions docs/content/3.rendering/5.react.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ export default function App() {
| [`extends`](#code-markdown-code-definemarkdowncomponent-code-extends) | `ReturnType<typeof defineMarkdownComponent>` | `undefined` | Inherit plugins and components from another component |
| `name` | `string` | `undefined` | Component name for debugging |
| `autoUnwrap` | `boolean` | `true` | Automatically unwrap single block elements |
| `autoClose` | `boolean` | `true` | Auto-close incomplete markdown syntax |
| `autoClose` | `boolean \| 'streaming' \| fn` | `'streaming'` | Auto-close incomplete markdown syntax. Only closes on a streaming parse by default |
| `linkify` | `boolean` | `true` | Auto-convert URL-like text into links |
| `registerDefaultPlugins` | `boolean` | `true` | Register default plugins (`frontmatter`, `html`, `alert`, `task-list`, `components`, `attributes`) |
| [`plugins`](#code-markdown-props-code-plugins) | `ComarkPlugin[]` | `[]` | Array of plugins |
Expand Down Expand Up @@ -677,7 +677,7 @@ export default function AiChat() {
```

::callout{icon="i-lucide-info" color="info"}
`autoClose` is enabled by default: incomplete syntax like `**bold text` is automatically closed on every parse. Disable with `options={{ autoClose: false }}`.
`autoClose` defaults to `'streaming'`: incomplete syntax like `**bold text` is closed when the `streaming` prop is set, and left as written otherwise. Set `options={{ autoClose: true }}` to close incomplete syntax on every parse, or `options={{ autoClose: false }}` to never close it.
::

### Caret
Expand Down
2 changes: 1 addition & 1 deletion docs/content/3.rendering/6.svelte.md
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ Set `streaming` to `true` while content is being received, then `false` when don
```

::callout{icon="i-lucide-info" color="info"}
`autoClose` is enabled by default: incomplete syntax like `**bold text` is automatically closed on every parse. Disable with `options={{ autoClose: false }}`.
`autoClose` defaults to `'streaming'`: incomplete syntax like `**bold text` is closed when the `streaming` prop is set, and left as written otherwise. Set `options={{ autoClose: true }}` to close incomplete syntax on every parse, or `options={{ autoClose: false }}` to never close it.
::

### Caret
Expand Down
2 changes: 1 addition & 1 deletion docs/content/3.rendering/7.angular.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ export class AiChatComponent {
```

::callout{icon="i-lucide-info" color="info"}
`autoClose` is enabled by default: incomplete syntax like `**bold text` is automatically closed on every parse. Disable with `[options]="{ autoClose: false }"`.
`autoClose` defaults to `'streaming'`: incomplete syntax like `**bold text` is closed when the `streaming` prop is set, and left as written otherwise. Set `[options]="{ autoClose: true }"` to close incomplete syntax on every parse, or `[options]="{ autoClose: false }"` to never close it.
::

### Caret
Expand Down
2 changes: 1 addition & 1 deletion docs/content/3.rendering/8.ansi.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ This is a bold statement with a link (https://example.com).
| [`data`](#render-options-data) | `Record<string, any>` | `undefined` | Data passed to component renderers |
| `colors` | `boolean` | `true`* | Emit ANSI escape codes |
| `width` | `number` | `80` | Terminal width for HR and code block headers |
| `autoClose` | `boolean` | `true` | Close incomplete Markdown and components before parsing |
| `autoClose` | `boolean \| 'streaming' \| fn` | `'streaming'` | Close incomplete Markdown and components before parsing. `'streaming'` only closes on a streaming parse, so a plain render leaves input as written |
| `autoUnwrap` | `boolean` | `true` | Remove a single paragraph wrapper inside components |
| `linkify` | `boolean` | `true` | Convert URL-like text into links |
| `registerDefaultPlugins` | `boolean` | `true` | Register default plugins (`frontmatter`, `html`, `alert`, `task-list`, `components`, `attributes`) |
Expand Down
7 changes: 4 additions & 3 deletions docs/content/5.reference/1.parse.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ links:
variant: soft
---

## `parseMarkdown(source, options?)`{lang="ts"}
## `parseMarkdown(source, options?, parseOptions?)`{lang="ts"}

Parses Markdown from a string and returns a complete `MarkdownDocument`. Default plugins add frontmatter, alerts, task lists, HTML, components, and attributes to the standard Markdown parser.

**Parameters:**

- `source` - The Markdown content as a string
- `options?` - Parser options including plugins
- `parseOptions?` - Per-call options, `{ streaming?: boolean }`. Pass `{ streaming: true }` for a chunk of a stream so the default `autoClose` heals incomplete syntax.

**Returns:** `MarkdownDocument` object containing:

Expand Down Expand Up @@ -224,7 +225,7 @@ import toc from 'comark/plugins/toc'
// Create a parser with specific configuration
const parse = createMarkdownParser({
autoUnwrap: true,
autoClose: true,
autoClose: 'streaming',
plugins: [
shiki({
themes: { light: githubLight, dark: githubDark }
Expand Down Expand Up @@ -360,7 +361,7 @@ Both `parseMarkdown()` and `createMarkdownParser()` accept the same `ParserOptio
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `autoUnwrap` | `boolean` | `true` | Remove unnecessary `<p>` wrappers from single-element containers |
| `autoClose` | `boolean \| (markdown: string) => string` | `true` | Auto-close incomplete markdown syntax, or use a custom completion function |
| `autoClose` | `boolean \| 'streaming' \| (markdown: string) => string` | `'streaming'` | Auto-close incomplete markdown syntax. `'streaming'` only heals a parse called with `{ streaming: true }`; `true` heals every parse |
| `unwrap` | `boolean \| string \| string[]` | `false` | Remove wrapper tags from the tree, hoisting their children (MDC `unwrap` behaviour). `true` unwraps `p`; a comma/whitespace-separated string or array unwraps the listed tags; `'*'` matches any tag. Tags apply sequentially (each descends one level), and adjacent text is merged into a single string. |
| `html` | `boolean` | `true` | **Deprecated** (warns). Prefer `registerDefaultPlugins: false` and register `html()` explicitly. `html: false` still skips the default html plugin. |
| `linkify` | `boolean` | `true` | Auto-convert URL-like text into links. Set `false` to disable |
Expand Down
20 changes: 16 additions & 4 deletions docs/content/5.reference/2.auto-close.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,31 @@ autoCloseMarkdown('hello *', { dropTrailingOpeners: true })
::

::tip
`autoCloseMarkdown` is also available as a parse option: set `autoClose: true` (default) in `parseMarkdown()` or `createMarkdownParser()` to apply it automatically.
`autoCloseMarkdown` is also available as a parse option: `autoClose` in `parseMarkdown()` or `createMarkdownParser()` applies it for you while streaming.
::

### Parser integration

`autoClose` is enabled by default in `parseMarkdown()` and `createMarkdownParser()`. You can disable it or provide a custom completion function:
`autoClose` defaults to `'streaming'`, so healing runs on a parse called with `{ streaming: true }` and a plain parse follows CommonMark:

```typescript
import { parseMarkdown } from 'comark'

await parseMarkdown('a _b')
// 'a _b' stays literal text

await parseMarkdown('a _b', {}, { streaming: true })
// ['em', {}, 'b']
```

You can force it on, turn it off, or provide a custom completion function:

::code-group
```typescript [Enabled (default)]
```typescript [Always]
import { parseMarkdown } from 'comark'

const result = await parseMarkdown(content, {
autoClose: true // default
autoClose: true
})
```

Expand Down
4 changes: 2 additions & 2 deletions docs/content/5.reference/3.reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import shiki from 'comark/plugins/shiki'

const result = await parseMarkdown(source, {
autoUnwrap: true, // Remove <p> wrappers from single-paragraph containers
autoClose: true, // Auto-close incomplete syntax
autoClose: 'streaming', // Auto-close incomplete syntax while streaming
plugins: [shiki()] // HTML parsing is on by default via the html plugin
})

Expand Down Expand Up @@ -296,7 +296,7 @@ type Node =
```typescript
interface ParserOptions {
autoUnwrap?: boolean // default: true
autoClose?: boolean | AutoCloseFunction // default: true
autoClose?: boolean | 'streaming' | AutoCloseFunction // default: 'streaming'
unwrap?: boolean | string | string[] // strip top-level wrapper tags, e.g. 'p' (default: false)
/** @deprecated Prefer registerDefaultPlugins: false */
html?: boolean // default: true
Expand Down
2 changes: 1 addition & 1 deletion docs/content/7.kb/2.migration-from-mdc.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ const document = await parseMarkdown(md, { plugins: [emoji()] })
{
plugins: ComarkPlugin[], // ordered array, not a record
autoUnwrap: true, // removes <p> from single-paragraph containers
autoClose: true, // completes incomplete syntax (useful for streaming)
autoClose: 'streaming', // completes incomplete syntax while streaming
// HTML, components, attributes, alerts, task-list, frontmatter are on by default
}
```
Expand Down
2 changes: 1 addition & 1 deletion docs/skills/comark/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A guide for using Comark in AI agent and LLM-powered applications where markdown

LLMs stream markdown token-by-token. Standard markdown parsers expect complete input. They fail or produce broken output on partial streams. Comark was built to handle exactly this:

- **`autoClose`** (default: `true`): incomplete syntax like `**bold text` is automatically closed on every parse, so partial tokens always render correctly
- **`autoClose`** (default: `'streaming'`): incomplete syntax like `**bold text` is closed when you parse with `{ streaming: true }`, so partial tokens always render correctly. Set `autoClose: true` to close on every parse
- **Streaming mode**: re-renders efficiently as content arrives
- **Caret indicator**: shows a live cursor during generation
- **ANSI rendering**: styled terminal output for CLI agents
Expand Down
2 changes: 1 addition & 1 deletion docs/skills/comark/references/parsing-ast.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ interface MarkdownDocument {
```typescript
interface ParserOptions {
autoUnwrap?: boolean // Remove unnecessary <p> wrappers (default: true)
autoClose?: boolean // Auto-close unclosed syntax (default: true)
autoClose?: boolean | 'streaming' | ((markdown: string) => string) // Auto-close unclosed syntax (default: 'streaming')
plugins?: ComarkPlugin[] // Enable plugins (e.g., highlight, emoji, toc)
}
```
Expand Down
2 changes: 1 addition & 1 deletion docs/skills/comark/references/rendering-svelte.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ The `caret` prop appends a blinking cursor indicator to the last text node durin
<Markdown value={content} streaming={isStreaming} caret={{ class: 'my-caret' }} />
```

`autoClose` is enabled by default: incomplete syntax like `**bold text` is automatically closed on every parse.
`autoClose` defaults to `'streaming'`: incomplete syntax like `**bold text` is closed when the `streaming` prop is set, and left as written otherwise.

---

Expand Down
2 changes: 1 addition & 1 deletion docs/skills/migrate-mdc-to-comark/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ The migration has two parts: **Core Package** (programmatic API) and **Nuxt Modu
{
plugins: ComarkPlugin[], // ordered array, not a record
autoUnwrap: true, // removes <p> from single-paragraph containers
autoClose: true, // completes incomplete syntax (useful for streaming)
autoClose: 'streaming', // completes incomplete syntax while streaming
// HTML, components, attributes, alerts, task-list, frontmatter are on by default
}
```
Expand Down
6 changes: 4 additions & 2 deletions packages/comark-ansi/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,9 +437,11 @@ describe('renderAnsi', () => {
})

it('passes parser and renderer options through', async () => {
const output = await renderAnsi('**bold', { autoClose: false, colors: false })
// The default only heals while streaming, so force it on to prove the option
// reaches the parser.
const output = await renderAnsi('**bold', { autoClose: true, colors: false })
expect(output).not.toContain('\x1B[')
expect(output).toContain('\\*\\*bold')
expect(output).not.toContain('\\*\\*bold')
})

it('disables colors when NO_COLOR is present', async () => {
Expand Down
7 changes: 4 additions & 3 deletions packages/comark-html/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ describe('renderHtml', () => {
})

it('passes parser options through', async () => {
const html = await renderHtml('**bold', { autoClose: false })
expect(html).toContain('**bold')
expect(html).not.toContain('<strong>')
// The default only heals while streaming, so force it on to prove the option
// reaches the parser.
const html = await renderHtml('**bold', { autoClose: true })
expect(html).toContain('<strong>')
})
})

Expand Down
11 changes: 9 additions & 2 deletions packages/comark-react/src/components/MarkdownClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ function MarkdownContent({
}

export function MarkdownClient({ children, value, options = {}, plugins = [], ...rest }: MarkdownProps) {
const streaming = rest.streaming ?? false

const content = isMarkdownDocument(value)
? value
: children
Expand All @@ -46,8 +48,13 @@ export function MarkdownClient({ children, value, options = {}, plugins = [], ..
// Note: options/plugins should be stable references (defined outside render or memoized).
// Pre-parsed documents resolve immediately without calling parseMarkdown().
const parsePromise = useMemo(
() => (isMarkdownDocument(content) ? Promise.resolve(content) : parseMarkdown(content, { ...options, plugins })),
[content]
() =>
isMarkdownDocument(content)
? Promise.resolve(content)
: // `streaming` must reach the parser, not just the renderer: it drives
// auto-close healing and incremental node reuse.
parseMarkdown(content, { ...options, plugins }, { streaming }),
[content, streaming]
)

// Keep showing the previous parsed result while a new parse is pending —
Expand Down
31 changes: 31 additions & 0 deletions packages/comark-react/test/streaming.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest'
import React from 'react'
import { renderToReadableStream } from 'react-dom/server'
import { Markdown } from '../src/index'

async function renderAsync(element: React.ReactElement): Promise<string> {
const stream = await renderToReadableStream(element)
await stream.allReady
return new Response(stream).text()
}

// The `streaming` prop has to reach the parser, not just the renderer: auto-close
// heals only on a streaming parse.
describe('<Markdown streaming>', () => {
it('heals incomplete markdown while streaming', async () => {
const html = await renderAsync(
<Markdown
value="Hello **wor"
streaming
/>
)
expect(html).toContain('<strong>')
expect(html).toContain('wor')
})

it('leaves incomplete markdown alone when not streaming', async () => {
const html = await renderAsync(<Markdown value="Hello **wor" />)
expect(html).not.toContain('<strong>')
expect(html).toContain('**wor')
})
})
2 changes: 1 addition & 1 deletion packages/comark-svelte/src/async/MarkdownAsync.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ and wrap this component in a `<svelte:boundary>` for pending/error states.
: // `parse` directly mutates `plugins` which creates an infinite effect loop
// so we copy it before passing it in so it gets a regular JS array and we get to still
// track dependencies from an external perspective
await parseMarkdown(content, { ...options, ...(unwrap ? { unwrap } : {}), plugins: [...plugins] }),
await parseMarkdown(content, { ...options, ...(unwrap ? { unwrap } : {}), plugins: [...plugins] }, { streaming }),
)
</script>

Expand Down
2 changes: 1 addition & 1 deletion packages/comark-svelte/src/components/Markdown.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ This is an alert component
// `parse` directly mutates `plugins` which creates an infinite effect loop
// so we copy it before passing it in so it gets a regular JS array and we get to still
// track dependencies from an external perspective
parseMarkdown(content, { ...options, ...(unwrap ? { unwrap } : {}), plugins: [...plugins] }).then((result) => {
parseMarkdown(content, { ...options, ...(unwrap ? { unwrap } : {}), plugins: [...plugins] }, { streaming }).then((result) => {
if (currentVersion > appliedVersion) {
appliedVersion = currentVersion
parsed = result
Expand Down
23 changes: 23 additions & 0 deletions packages/comark-svelte/test/streaming.svelte.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,29 @@ describe('streaming mode', () => {
await expect.element(screen.getByText('Hello world')).toBeInTheDocument()
})

// The `streaming` prop has to reach the parser, not just the renderer: auto-close
// heals only on a streaming parse. Asserted on markup because 'wor' is a
// substring of '**wor'.
it('heals incomplete bold from the streaming prop alone', async () => {
const screen = await render(Markdown, {
value: 'Hello **wor',
streaming: true,
})

await expect.poll(() => screen.container.innerHTML).toContain('<strong>')
expect(screen.container.innerHTML).not.toContain('**wor')
})

it('leaves incomplete bold alone when not streaming', async () => {
const screen = await render(Markdown, {
value: 'Hello **wor',
streaming: false,
})

await expect.poll(() => screen.container.innerHTML).toContain('**wor')
expect(screen.container.innerHTML).not.toContain('<strong>')
})

it('handles incomplete heading during streaming', async () => {
const screen = await render(Markdown, {
value: '# Hell',
Expand Down
Loading
Loading