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
2 changes: 1 addition & 1 deletion docs/content/2.syntax/3.attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ const variable = "value"
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</blockquote>
<pre language="ts" attr="value"><code class="language-ts">const variable = "value"</code></pre>
<pre data-language="ts" attr="value"><code class="language-ts">const variable = "value"</code></pre>
```
::

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from '@angular/core'
import type { ElementNode, Node as MarkdownAstNode, NodeRenderData } from 'comark'
import { resolveIfWrapper, selectIfBranch, shouldRenderIf, type IfProps } from 'comark/plugins/binding'
import { pascalCase, resolveAttributes } from 'comark/utils'
import { pascalCase, resolveAttributes, toNativeAttributes } from 'comark/utils'

interface StructuralComponent extends Type<any> {
ɵcomarkIf?: boolean
Expand Down Expand Up @@ -196,10 +196,11 @@ export class MarkdownNode implements OnChanges {
tag: string,
attrs: Record<string, any>,
children: MarkdownAstNode[],
childrenRenderData: NodeRenderData
childrenRenderData: NodeRenderData,
node?: MarkdownAstNode
): void {
const el = this.renderer.createElement(tag)
this.applyAttributes(el, attrs)
this.applyAttributes(el, node ? toNativeAttributes(tag, node, attrs) : attrs)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// `innerHTML` from document attributes is never applied — resolveAttributes
// drops DOM sink props, and raw HTML has its own explicit parse path.
Expand All @@ -216,7 +217,14 @@ export class MarkdownNode implements OnChanges {
children: MarkdownAstNode[],
childrenRenderData: NodeRenderData
): void {
this.renderNativeEl(this.elementRef.nativeElement as HTMLElement, tag, attrs, children, childrenRenderData)
this.renderNativeEl(
this.elementRef.nativeElement as HTMLElement,
tag,
attrs,
children,
childrenRenderData,
this.node
)
}

/** Evaluate an `::if` before rendering any of its descendants. */
Expand Down Expand Up @@ -385,7 +393,7 @@ export class MarkdownNode implements OnChanges {
const resolved = resolveAttributes(childProps, renderData, { parseJson: true })
const hasOwnAttrs = Object.keys(resolved).length > 0
const childRenderData: NodeRenderData = hasOwnAttrs ? { ...renderData, props: resolved } : renderData
this.renderNativeEl(parentEl, childTag, resolved, grandChildren, childRenderData)
this.renderNativeEl(parentEl, childTag, resolved, grandChildren, childRenderData, child)
}
}
}
Expand Down
11 changes: 8 additions & 3 deletions packages/comark-react/src/components/MarkdownDocument.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
NodeRenderData,
} from 'comark'
import React, { lazy, Suspense, useMemo } from 'react'
import { pascalCase, camelCase, resolveAttributes } from 'comark/utils'
import { pascalCase, camelCase, resolveAttributes, toNativeAttributes } from 'comark/utils'
import { findLastTextNodeAndAppendNode, getCaret } from '../utils/caret.ts'

/**
Expand Down Expand Up @@ -141,7 +141,7 @@ function renderNode(
// remapping (`class` → `className`, string `style` → object, `tabindex`
// → `tabIndex`).
const resolved = resolveAttributes(nodeProps, renderData, { parseJson: true })
const props: Record<string, any> = {}
let props: Record<string, any> = {}
for (const k in resolved) {
const v = resolved[k]
if (k === 'className' || k === 'class') {
Expand All @@ -155,6 +155,11 @@ function renderNode(
}
}

const scopeProps = props
if (!customComponent) {
props = toNativeAttributes(tag, node, props)
}

if (typeof Component !== 'string' && (Component as any)?.propTypes?.__node !== undefined) {
props.__node = node
}
Expand All @@ -173,7 +178,7 @@ function renderNode(
// own attributes. Bare wrappers (`<p>`, `<ul>`, `<li>`, …) must keep the
// parent's scope so bindings like `{{ props.x }}` reach across them.
const hasOwnAttrs = Object.keys(resolved).length > 0
const childrenRenderData: NodeRenderData = hasOwnAttrs ? { ...renderData, props } : renderData
const childrenRenderData: NodeRenderData = hasOwnAttrs ? { ...renderData, props: scopeProps } : renderData
// Separate template elements (slots) from regular children
const slots: Record<string, React.ReactNode[]> = {}
const regularChildren: React.ReactNode[] = []
Expand Down
53 changes: 53 additions & 0 deletions packages/comark-react/test/code-block.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest'
import { renderToString } from 'react-dom/server'
import { parseMarkdown } from 'comark'
import { MarkdownDocument } from '../src/components/MarkdownDocument'

const markdown = '```ruby {1} [app.rb] foo=bar\nputs 1\n```'

describe('code blocks', () => {
it('should render code block props as data attributes on native pre', async () => {
const tree = await parseMarkdown(markdown)
const html = renderToString(<MarkdownDocument value={tree} />)
expect(html).toContain(
'<pre data-language="ruby" data-highlights="[1]" data-filename="app.rb" data-meta="foo=bar"><code class="language-ruby">'
)
})

it('should pass code block props unchanged to a custom pre component', async () => {
const tree = await parseMarkdown(markdown)
function ProsePre({ language, filename, highlights, meta }: any) {
return <div data-props={JSON.stringify({ language, filename, highlights, meta })} />
}
const html = renderToString(
<MarkdownDocument
value={tree}
components={{ ProsePre }}
/>
)
expect(html).toContain(
'{&quot;language&quot;:&quot;ruby&quot;,&quot;filename&quot;:&quot;app.rb&quot;,&quot;highlights&quot;:[1],&quot;meta&quot;:&quot;foo=bar&quot;}'
)
})

it('should keep authored language on a raw HTML pre', () => {
const tree = {
nodes: [['pre', { $: { html: 1, block: 1 }, language: 'ruby' }, 'puts 1']],
frontmatter: {},
meta: {},
} as any
const html = renderToString(<MarkdownDocument value={tree} />)
expect(html).toContain('<pre language="ruby">')
expect(html).not.toContain('data-language')
})

it('should keep code block props in the binding scope of native pre children', () => {
const tree = {
nodes: [['pre', { language: 'ruby' }, ['code', { ':data-lang': 'props.language' }, 'x']]],
frontmatter: {},
meta: {},
} as any
const html = renderToString(<MarkdownDocument value={tree} />)
expect(html).toContain('<code data-lang="ruby">')
})
})
8 changes: 5 additions & 3 deletions packages/comark-svelte/src/components/MarkdownNode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ naturally appears inline after the deepest trailing text node.
import MarkdownNode from './MarkdownNode.svelte'
import ComarkComponent from './ComarkComponent.svelte'
import Resolve from './Resolve.svelte'
import { resolveAttributes } from 'comark/utils'
import { resolveAttributes, toNativeAttributes } from 'comark/utils'

const EMPTY_RENDER_DATA: NodeRenderData = { frontmatter: {}, meta: {}, data: {}, props: {} }

Expand Down Expand Up @@ -197,6 +197,8 @@ naturally appears inline after the deepest trailing text node.
// Only shadow the parent's `props` scope when the current element has its
// own attributes. Bare wrappers (`<p>`, `<ul>`, `<li>`, …) must keep the
// parent's scope so bindings like `{{ props.x }}` reach across them.
let nativeProps = $derived(tag ? toNativeAttributes(tag, node, mappedProps) : mappedProps)

let childrenRenderData = $derived<NodeRenderData>(
Object.keys(mappedProps).length > 0
? { ...renderData, props: mappedProps }
Expand Down Expand Up @@ -283,9 +285,9 @@ naturally appears inline after the deepest trailing text node.
{@render renderChildren()}
</Resolver>
{:else if isVoid}
<svelte:element this={tag} {...mappedProps} />
<svelte:element this={tag} {...nativeProps} />
{:else if tag}
<svelte:element this={tag} {...mappedProps}>
<svelte:element this={tag} {...nativeProps}>
{@render renderChildren()}
</svelte:element>
{/if}
23 changes: 23 additions & 0 deletions packages/comark-svelte/test/MarkdownNode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,3 +384,26 @@ Footer slot content.
expect(output).toContain('content')
})
})

describe('MarkdownNode code blocks', () => {
it('renders code block props as data attributes on native pre', async () => {
const tree = await parseMarkdown('```ruby {1} [app.rb] foo=bar\nputs 1\n```')
const { body } = render(MarkdownNode, { props: { node: tree.nodes[0] } })
expect(html(body)).toContain(
'<pre data-language="ruby" data-highlights="[1]" data-filename="app.rb" data-meta="foo=bar"><code class="language-ruby">'
)
})

it('keeps authored language on a raw HTML pre', () => {
const node = ['pre', { $: { html: 1, block: 1 }, language: 'ruby' }, 'puts 1']
const { body } = render(MarkdownNode, { props: { node } })
expect(html(body)).toContain('<pre language="ruby">')
expect(html(body)).not.toContain('data-language')
})

it('keeps code block props in the binding scope of native pre children', () => {
const node = ['pre', { language: 'ruby' }, ['code', { ':data-lang': 'props.language' }, 'x']]
const { body } = render(MarkdownNode, { props: { node } })
expect(html(body)).toContain('<code data-lang="ruby">')
})
})
10 changes: 7 additions & 3 deletions packages/comark-vue/src/components/MarkdownDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
toRaw,
} from 'vue'
import { findLastTextNodeAndAppendNode, getCaret } from '../utils/caret.ts'
import { pascalCase, resolveAttributes } from 'comark/utils'
import { pascalCase, resolveAttributes, toNativeAttributes } from 'comark/utils'

// Cache for dynamically resolved components
const asyncComponentCache = new Map<string, any>()
Expand Down Expand Up @@ -142,14 +142,18 @@ function renderNode(
// Resolve `:prefix` bindings and let Vue-specific attribute mapping run
// on top (e.g. `className` → `class`).
const resolved = resolveAttributes(nodeProps, renderData, { parseJson: true })
const props: Record<string, any> = {}
let props: Record<string, any> = {}
for (const k in resolved) {
if (k === 'className') {
props.class = resolved[k]
} else {
props[k] = resolved[k]
}
}
const scopeProps = props
if (!customComponent) {
props = toNativeAttributes(tag, node, props)
}

// @ts-expect-error - component might be a Vue component
if (component?.props?.__node || component?.__asyncResolved?.props?.__node) {
Expand All @@ -169,7 +173,7 @@ function renderNode(
// own attributes. Bare wrappers (`<p>`, `<ul>`, `<li>`, …) must keep the
// parent's scope so bindings like `{{ props.x }}` reach across them.
const hasOwnAttrs = Object.keys(resolved).length > 0
const childrenRenderData = hasOwnAttrs ? { ...renderData, props } : renderData
const childrenRenderData = hasOwnAttrs ? { ...renderData, props: scopeProps } : renderData
// Separate template elements (slots) from regular children
const slots: Record<string, () => (VNode | string)[]> = {}
const regularChildren: (VNode | string)[] = []
Expand Down
67 changes: 67 additions & 0 deletions packages/comark-vue/test/code-block.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it } from 'vitest'
import { createSSRApp, defineComponent, h } from 'vue'
import { renderToString } from '@vue/server-renderer'
import { parseMarkdown } from 'comark'
import { MarkdownDocument } from '../src/components/MarkdownDocument'

const markdown = '```ruby {1} [app.rb] foo=bar\nputs 1\n```'

async function renderTree(components: Record<string, any> = {}) {
const tree = await parseMarkdown(markdown)
return renderToString(createSSRApp({ render: () => h(MarkdownDocument, { value: tree, components }) }))
}

describe('code blocks', () => {
it('should render code block props as data attributes on native pre', async () => {
const html = await renderTree()
expect(html).toContain(
'<pre data-language="ruby" data-highlights="[1]" data-filename="app.rb" data-meta="foo=bar"><code class="language-ruby">'
)
})

it('should pass code block props unchanged to a custom pre component', async () => {
const ProsePre = defineComponent({
props: ['language', 'filename', 'highlights', 'meta'],
setup(props, { slots }) {
return () =>
h(
'div',
{
'data-props': JSON.stringify({
language: props.language,
filename: props.filename,
highlights: props.highlights,
meta: props.meta,
}),
},
slots.default?.()
)
},
})
const html = await renderTree({ ProsePre })
expect(html).toContain(
'{&quot;language&quot;:&quot;ruby&quot;,&quot;filename&quot;:&quot;app.rb&quot;,&quot;highlights&quot;:[1],&quot;meta&quot;:&quot;foo=bar&quot;}'
)
})

it('should keep authored language on a raw HTML pre', async () => {
const tree = {
nodes: [['pre', { $: { html: 1, block: 1 }, language: 'ruby' }, 'puts 1']],
frontmatter: {},
meta: {},
} as any
const html = await renderToString(createSSRApp({ render: () => h(MarkdownDocument, { value: tree }) }))
expect(html).toContain('<pre language="ruby">')
expect(html).not.toContain('data-language')
})

it('should keep code block props in the binding scope of native pre children', async () => {
const tree = {
nodes: [['pre', { language: 'ruby' }, ['code', { ':data-lang': 'props.language' }, 'x']]],
frontmatter: {},
meta: {},
} as any
const html = await renderToString(createSSRApp({ render: () => h(MarkdownDocument, { value: tree }) }))
expect(html).toContain('<code data-lang="ruby">')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const variable = "value"
## HTML

```html
<pre language="ts" class="border-2 border-primary"><code class="language-ts">const variable = "value"</code></pre>
<pre data-language="ts" class="border-2 border-primary"><code class="language-ts">const variable = "value"</code></pre>
```

## Markdown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const variable = "value"
## HTML

```html
<pre language="ts" attr="value" class="shiki shiki-themes min-light nord dark:nord . class" style="background-color:#ffffff;color:#212121;--shiki-dark-bg:#2e3440;--shiki-dark:#d8dee9"><code class="language-ts"><span class="line" style="display: inline"><span style="color:#D32F2F;--shiki-dark:#81A1C1">const</span><span style="color:#1976D2;--shiki-dark:#D8DEE9"> variable</span><span style="color:#D32F2F;--shiki-dark:#81A1C1"> =</span><span style="color:#22863A;--shiki-dark:#ECEFF4"> "</span><span style="color:#22863A;--shiki-dark:#A3BE8C">value</span><span style="color:#22863A;--shiki-dark:#ECEFF4">"</span></span></code></pre>
<pre data-language="ts" attr="value" class="shiki shiki-themes min-light nord dark:nord . class" style="background-color:#ffffff;color:#212121;--shiki-dark-bg:#2e3440;--shiki-dark:#d8dee9"><code class="language-ts"><span class="line" style="display: inline"><span style="color:#D32F2F;--shiki-dark:#81A1C1">const</span><span style="color:#1976D2;--shiki-dark:#D8DEE9"> variable</span><span style="color:#D32F2F;--shiki-dark:#81A1C1"> =</span><span style="color:#22863A;--shiki-dark:#ECEFF4"> "</span><span style="color:#22863A;--shiki-dark:#A3BE8C">value</span><span style="color:#22863A;--shiki-dark:#ECEFF4">"</span></span></code></pre>
```

## Markdown
Expand Down
2 changes: 1 addition & 1 deletion packages/comark/SPEC/COMARK/attributes/wrapped-pre.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const variable = "value"
## HTML

```html
<pre language="ts" attr="value"><code class="language-ts">const variable = "value"</code></pre>
<pre data-language="ts" attr="value"><code class="language-ts">const variable = "value"</code></pre>
```

## Markdown
Expand Down
2 changes: 1 addition & 1 deletion packages/comark/SPEC/COMARK/codeblock-diff-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
## HTML

```html
<pre language="diff"><code class="language-diff">- const oldValue = 42;
<pre data-language="diff"><code class="language-diff">- const oldValue = 42;
+ const newValue = 100;
const unchanged = "same";</code></pre>
```
Expand Down
2 changes: 1 addition & 1 deletion packages/comark/SPEC/COMARK/codeblock-empty.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
## HTML

```html
<pre language="javascript"><code class="language-javascript"></code></pre>
<pre data-language="javascript"><code class="language-javascript"></code></pre>
```

## Markdown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function hello() {
## HTML

```html
<pre language="javascript" highlights="[1,2,3]" filename="@[...slug].ts" meta="meta=meta-value"><code class="language-javascript">function hello() {
<pre data-language="javascript" data-highlights="[1,2,3]" data-filename="@[...slug].ts" data-meta="meta=meta-value"><code class="language-javascript">function hello() {
console.log("Hello, World!");
}</code></pre>
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function hello() {
## HTML

```html
<pre language="javascript" filename="@[...slug].ts" highlights="[1,2,3,5,9,10,11]" meta="meta=meta-value"><code class="language-javascript">function hello() {
<pre data-language="javascript" data-filename="@[...slug].ts" data-highlights="[1,2,3,5,9,10,11]" data-meta="meta=meta-value"><code class="language-javascript">function hello() {
console.log("Hello, World!");
}</code></pre>
```
Expand Down
2 changes: 1 addition & 1 deletion packages/comark/SPEC/COMARK/codeblock-filename.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function hello() {
## HTML

```html
<pre language="javascript" filename="hello.js"><code class="language-javascript">function hello() {
<pre data-language="javascript" data-filename="hello.js"><code class="language-javascript">function hello() {
console.log("Hello, World!");
}</code></pre>
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func main() {
## HTML

```html
<pre language="go" highlights="[1,3,5,6,7,10]"><code class="language-go">package main
<pre data-language="go" data-highlights="[1,3,5,6,7,10]"><code class="language-go">package main

import "fmt"

Expand Down
Loading
Loading