Skip to content

Commit 060dfa9

Browse files
authored
feat: introduce unwrap feature in parser (#275)
1 parent 8d2171d commit 060dfa9

30 files changed

Lines changed: 485 additions & 61 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ describe('functionUnderTest', () => {
479479
const result = await parse(markdownContent, {
480480
autoUnwrap: true, // Remove <p> wrappers from single-paragraph containers
481481
autoClose: true, // Auto-close incomplete syntax
482+
unwrap: 'p', // Strip top-level wrapper tags (MDC unwrap); merges paragraphs
482483
})
483484

484485
result.nodes // ComarkNode[]

docs/content/3.rendering/3.vue.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ Pass markdown content via the default slot or the `markdown` prop:
9797
| `markdown` | `string` | `undefined` | Alternative to default slot |
9898
| [`options`](#code-comark-props-code-options) | `ParseOptions` | `{}` | Parser options (autoUnwrap, autoClose, etc.) |
9999
| [`plugins`](#code-comark-props-code-plugins) | `ComarkPlugin[]` | `[]` | Array of plugins |
100+
| `unwrap` | `boolean \| string \| string[]` | `false` | Strip wrapper tags (MDC `unwrap`) — `true` unwraps `<p>`; a comma/space-separated string or array peels tags sequentially, e.g. `"ul li"` |
100101
| [`components`](#code-comark-props-code-components) | `Record<string, Component>` | `{}` | Custom Vue component mappings |
101102
| [`componentsManifest`](#code-comark-props-code-componentsmanifest) | `ComponentManifest` | `undefined` | Dynamic component resolver |
102103
| [`streaming`](#streaming) | `boolean` | `false` | Enable streaming mode |

docs/content/3.rendering/4.nuxt.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Pass markdown via the default slot or the `markdown` prop:
9999
| `markdown` | `string` | `undefined` | Alternative to default slot |
100100
| [`options`](#code-comark-props-code-options) | `ParseOptions` | `{}` | Parser options (autoUnwrap, autoClose, etc.) |
101101
| [`plugins`](#code-comark-props-code-plugins) | `ComarkPlugin[]` | `[]` | Array of plugins |
102+
| `unwrap` | `boolean \| string \| string[]` | `false` | Strip wrapper tags (MDC `unwrap`) — `true` unwraps `<p>`; a comma/space-separated string or array peels tags sequentially, e.g. `"ul li"` |
102103
| [`components`](#code-comark-props-code-components) | `Record<string, Component>` | `{}` | Custom Vue component mappings |
103104
| [`componentsManifest`](#code-comark-props-code-componentsmanifest) | `ComponentManifest` | `undefined` | Dynamic component resolver |
104105
| [`streaming`](#streaming) | `boolean` | `false` | Enable streaming mode |

docs/content/3.rendering/5.react.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ Pass markdown content via `children` or the `markdown` prop:
8686
| `markdown` | `string` | `''` | Alternative to children |
8787
| [`options`](#code-comark-props-code-options) | `ParseOptions` | `{}` | Parser options (autoUnwrap, autoClose, etc.) |
8888
| [`plugins`](#code-comark-props-code-plugins) | `ComarkPlugin[]` | `[]` | Array of plugins |
89+
| `unwrap` | `boolean \| string \| string[]` | `false` | Strip wrapper tags (MDC `unwrap`) — `true` unwraps `<p>`; a comma/space-separated string or array peels tags sequentially, e.g. `"ul li"` |
8990
| [`components`](#code-comark-props-code-components) | `Record<string, ComponentType>` | `{}` | Custom React component mappings |
9091
| [`componentsManifest`](#code-comark-props-code-componentsmanifest) | `(name: string) => Promise<Component>` | `undefined` | Dynamic component resolver |
9192
| [`streaming`](#streaming) | `boolean` | `false` | Enable streaming mode |

docs/content/3.rendering/6.svelte.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ Pass markdown content via the `markdown` prop:
7171
| `markdown` | `string` | `''` | Markdown content to parse and render |
7272
| [`options`](#code-comark-props-code-options) | `ParseOptions` | `{}` | Parser options (autoUnwrap, autoClose, etc.) |
7373
| [`plugins`](#code-comark-props-code-plugins) | `ComarkPlugin[]` | `[]` | Array of plugins |
74+
| `unwrap` | `boolean \| string \| string[]` | `false` | Strip wrapper tags (MDC `unwrap`) — `true` unwraps `<p>`; a comma/space-separated string or array peels tags sequentially, e.g. `"ul li"` |
7475
| [`components`](#code-comark-props-code-components) | `Record<string, Component>` | `{}` | Custom Svelte component mappings |
7576
| [`componentsManifest`](#code-comark-props-code-componentsmanifest) | `ComponentManifest` | `undefined` | Dynamic component resolver |
7677
| [`streaming`](#streaming) | `boolean` | `false` | Enable streaming mode |

docs/content/3.rendering/7.angular.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ This is **markdown** with Comark components.
8181
| `markdown` | `string` | `''` | Markdown content to parse and render |
8282
| [`options`](#code-options) | `ParseOptions` | `{}` | Parser options (autoUnwrap, autoClose, etc.) |
8383
| [`plugins`](#code-plugins) | `ComarkPlugin[]` | `[]` | Array of plugins |
84+
| `unwrap` | `boolean \| string \| string[]` | `false` | Strip wrapper tags (MDC `unwrap`) — `true` unwraps `<p>`; a comma/space-separated string or array peels tags sequentially, e.g. `"ul li"` |
8485
| [`components`](#code-components) | `Record<string, Type<any>>` | `{}` | Custom Angular component mappings |
8586
| [`streaming`](#streaming) | `boolean` | `false` | Enable streaming mode |
8687
| `summary` | `boolean` | `false` | Only render content before `<!-- more -->` |

docs/content/5.api/1.parse.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,9 +363,42 @@ Both `parse()` and `createParse()` accept the same `ParseOptions`:
363363
|--------|------|---------|-------------|
364364
| `autoUnwrap` | `boolean` | `true` | Remove unnecessary `<p>` wrappers from single-element containers |
365365
| `autoClose` | `boolean` | `true` | Auto-close incomplete markdown syntax |
366+
| `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. |
366367
| `html` | `boolean` | `true` | Parse embedded HTML tags into AST nodes |
367368
| `linkify` | `boolean` | `true` | Auto-convert URL-like text into links. Set `false` to disable |
368369
| `headingIds` | `boolean` | `true` | Auto-generate `id` attributes for `h1``h6` headings. Set `false` to disable |
369370
| `plugins` | `ComarkPlugin[]` | `[]` | Array of plugins to apply |
370371

372+
### Inline rendering
373+
374+
Use `unwrap: 'p'` (or just `unwrap: true`) to render markdown without the
375+
wrapping `<p>`, which is handy for buttons, badges, and other inline hosts.
376+
Adjacent text is merged into a single string, matching MDC's `unwrap`:
377+
378+
```ts
379+
await parse('Hello **world**', { unwrap: 'p' })
380+
// nodes: ['Hello ', ['strong', {}, 'world']]
381+
382+
await parse('a\n\nb', { unwrap: true })
383+
// nodes: ['ab'] (paragraphs merged, no separator)
384+
```
385+
386+
Tags are applied **sequentially**, each descending one level into the result of
387+
the previous one — so a space-separated (or comma-separated) list peels nested
388+
wrappers. `'*'` matches any tag:
389+
390+
```ts
391+
// Unwrap the <ul>, then the <li> inside it
392+
await parse('- Buy milk', { unwrap: 'ul li' })
393+
// nodes: ['Buy milk']
394+
```
395+
396+
On the framework components this is exposed as an `unwrap` shorthand prop:
397+
398+
```vue
399+
<UButton><Comark :markdown="text" unwrap /></UButton>
400+
<UButton><Comark :markdown="text" unwrap="ul li" /></UButton>
401+
<UButton><Comark :markdown="text" :options="{ unwrap: 'p' }" /></UButton>
402+
```
403+
371404
---

docs/content/7.kb/2.migration-from-mdc.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ Props that changed:
381381
| `data` || `tree.frontmatter` contains frontmatter, not a separate prop |
382382
| `tag` || Wrapper is always `<div class="comark-content">` |
383383
| `prose` || `Prose*` component resolution is automatic |
384-
| `unwrap` | | Use `autoUnwrap` in parse options instead |
384+
| `unwrap` | `unwrap` prop / parse option | `<Comark unwrap />` or `:options="{ unwrap: 'p' }"` — see [inline rendering](/api/parse#inline-rendering) |
385385
| `class` || Style the wrapper via CSS |
386386
| `components` | `components` | Same purpose, same shape |
387387
|| `componentsManifest` | New: dynamic async component resolver |

packages/comark-angular/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"release": "release-it"
4646
},
4747
"dependencies": {
48-
"comark": "catalog:"
48+
"comark": "workspace:*"
4949
},
5050
"devDependencies": {
5151
"@angular/common": "^22.0.6",

packages/comark-angular/src/components/comark.component.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ export class ComarkComponent implements OnChanges {
4747
/** Additional plugins to use */
4848
@Input() plugins: ParseOptions['plugins'] = []
4949

50+
/**
51+
* Strip wrapper tags from the top level of the tree — shorthand for
52+
* `options.unwrap`. `true` unwraps `<p>`; a space-separated string or array
53+
* unwraps the listed tags.
54+
*/
55+
@Input() unwrap: boolean | string | string[] = false
56+
5057
/** Custom component mappings for element tags */
5158
@Input() components: Record<string, Type<any>> = {}
5259

@@ -69,10 +76,21 @@ export class ComarkComponent implements OnChanges {
6976
constructor(private cdr: ChangeDetectorRef) {}
7077

7178
ngOnChanges(changes: SimpleChanges): void {
72-
if (changes['options'] || changes['plugins']) {
73-
this.serializedParse = createSerializedParse({ ...this.options, plugins: this.plugins })
79+
if (changes['options'] || changes['plugins'] || changes['unwrap']) {
80+
this.serializedParse = createSerializedParse({
81+
...this.options,
82+
...(this.unwrap ? { unwrap: this.unwrap } : {}),
83+
plugins: this.plugins,
84+
})
7485
}
75-
if (changes['markdown'] || changes['options'] || changes['plugins'] || changes['streaming'] || changes['summary']) {
86+
if (
87+
changes['markdown'] ||
88+
changes['options'] ||
89+
changes['plugins'] ||
90+
changes['unwrap'] ||
91+
changes['streaming'] ||
92+
changes['summary']
93+
) {
7694
this.parseMarkdown()
7795
}
7896
}

0 commit comments

Comments
 (0)