From f180bb8fd34bd0b402a86b62615b3df9ca165737 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 28 Sep 2026 15:30:46 +0000 Subject: [PATCH 1/6] fix: render code block props as `data-*` attributes on native `pre` --- docs/content/2.syntax/3.attributes.md | 2 +- .../src/components/markdown-node.component.ts | 4 +- .../src/components/MarkdownDocument.tsx | 8 +++- .../comark-react/test/code-block.test.tsx | 32 +++++++++++++ .../src/components/MarkdownNode.svelte | 6 ++- .../comark-svelte/test/MarkdownNode.test.ts | 10 ++++ .../src/components/MarkdownDocument.ts | 7 ++- packages/comark-vue/test/code-block.test.ts | 46 +++++++++++++++++++ .../COMARK/attributes/wrapped-pre-class.md | 2 +- .../attributes/wrapped-pre-highlighted.md | 2 +- .../SPEC/COMARK/attributes/wrapped-pre.md | 2 +- .../SPEC/COMARK/codeblock-diff-syntax.md | 2 +- .../comark/SPEC/COMARK/codeblock-empty.md | 2 +- .../codeblock-filename-highlight-lines.md | 2 +- .../codeblock-filename-highlight-lines2.md | 2 +- .../comark/SPEC/COMARK/codeblock-filename.md | 2 +- .../COMARK/codeblock-highlight-complex.md | 2 +- .../SPEC/COMARK/codeblock-indented-content.md | 2 +- .../SPEC/COMARK/codeblock-language-only.md | 2 +- .../SPEC/COMARK/codeblock-multiple-meta.md | 2 +- .../SPEC/COMARK/codeblock-nested-4backtick.md | 2 +- .../SPEC/COMARK/codeblock-nested-tilde.md | 2 +- .../comark/SPEC/COMARK/codeblock-pnpm-add.md | 2 +- .../SPEC/COMARK/codeblock-rust-example.md | 2 +- .../SPEC/COMARK/codeblock-special-chars.md | 2 +- .../component-nested-codeblock-indented.md | 2 +- .../component-nested-codeblock-outdented.md | 2 +- .../SPEC/COMARK/component-nested-codeblock.md | 2 +- .../COMARK/component-slot-nested-codeblock.md | 2 +- .../SPEC/COMARK/component-with-codeblock.md | 2 +- .../SPEC/COMARK/shiki-codeblock-dual-theme.md | 2 +- .../shiki-codeblock-highlight-complex.md | 2 +- .../COMARK/shiki-codeblock-language-meta.md | 2 +- .../COMARK/shiki-codeblock-language-only.md | 2 +- .../COMARK/shiki-codeblock-rust-example.md | 2 +- .../COMARK/shiki-codeblock-special-chars.md | 2 +- .../SPEC/COMARK/shiki-codeblock-twoslash.md | 2 +- .../shiki-codeblock-with-empty-lines.md | 2 +- .../shiki-ordered-list-nested-codeblock.md | 2 +- .../common-mark/codeblock-with-empty-lines.md | 2 +- .../ordered-list-mixed-blocks-nested.md | 2 +- .../ordered-list-multiple-blocks.md | 2 +- .../ordered-list-nested-codeblock.md | 2 +- .../unordered-list-nested-deep-codeblock.md | 2 +- .../src/internal/stringify/handlers/html.ts | 5 +- packages/comark/src/utils/index.ts | 19 ++++++++ 46 files changed, 165 insertions(+), 46 deletions(-) create mode 100644 packages/comark-react/test/code-block.test.tsx create mode 100644 packages/comark-vue/test/code-block.test.ts diff --git a/docs/content/2.syntax/3.attributes.md b/docs/content/2.syntax/3.attributes.md index 6654e833..a80a0c50 100644 --- a/docs/content/2.syntax/3.attributes.md +++ b/docs/content/2.syntax/3.attributes.md @@ -351,7 +351,7 @@ const variable = "value"

Paragraph 1

Paragraph 2

-
const variable = "value"
+
const variable = "value"
``` :: diff --git a/packages/comark-angular/src/components/markdown-node.component.ts b/packages/comark-angular/src/components/markdown-node.component.ts index 9869759f..901f3cc9 100644 --- a/packages/comark-angular/src/components/markdown-node.component.ts +++ b/packages/comark-angular/src/components/markdown-node.component.ts @@ -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 { ɵcomarkIf?: boolean @@ -199,7 +199,7 @@ export class MarkdownNode implements OnChanges { childrenRenderData: NodeRenderData ): void { const el = this.renderer.createElement(tag) - this.applyAttributes(el, attrs) + this.applyAttributes(el, toNativeAttributes(tag, attrs)) // `innerHTML` from document attributes is never applied — resolveAttributes // drops DOM sink props, and raw HTML has its own explicit parse path. diff --git a/packages/comark-react/src/components/MarkdownDocument.tsx b/packages/comark-react/src/components/MarkdownDocument.tsx index eced24f3..d6feb99f 100644 --- a/packages/comark-react/src/components/MarkdownDocument.tsx +++ b/packages/comark-react/src/components/MarkdownDocument.tsx @@ -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' /** @@ -141,7 +141,7 @@ function renderNode( // remapping (`class` → `className`, string `style` → object, `tabindex` // → `tabIndex`). const resolved = resolveAttributes(nodeProps, renderData, { parseJson: true }) - const props: Record = {} + let props: Record = {} for (const k in resolved) { const v = resolved[k] if (k === 'className' || k === 'class') { @@ -155,6 +155,10 @@ function renderNode( } } + if (!customComponent) { + props = toNativeAttributes(tag, props) + } + if (typeof Component !== 'string' && (Component as any)?.propTypes?.__node !== undefined) { props.__node = node } diff --git a/packages/comark-react/test/code-block.test.tsx b/packages/comark-react/test/code-block.test.tsx new file mode 100644 index 00000000..4d6ff62d --- /dev/null +++ b/packages/comark-react/test/code-block.test.tsx @@ -0,0 +1,32 @@ +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() + expect(html).toContain( + '
'
+    )
+  })
+
+  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 
+ } + const html = renderToString( + + ) + expect(html).toContain( + '{"language":"ruby","filename":"app.rb","highlights":[1],"meta":"foo=bar"}' + ) + }) +}) diff --git a/packages/comark-svelte/src/components/MarkdownNode.svelte b/packages/comark-svelte/src/components/MarkdownNode.svelte index dc9db091..cc3ec223 100644 --- a/packages/comark-svelte/src/components/MarkdownNode.svelte +++ b/packages/comark-svelte/src/components/MarkdownNode.svelte @@ -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: {} } @@ -191,6 +191,10 @@ naturally appears inline after the deepest trailing text node. } } + if (!Component && !componentPromise) { + mappedProps = toNativeAttributes(tag, mappedProps) + } + return { isText, tag, isVoid, children, Component, componentPromise, mappedProps } }) diff --git a/packages/comark-svelte/test/MarkdownNode.test.ts b/packages/comark-svelte/test/MarkdownNode.test.ts index fa3f8d63..fb018d40 100644 --- a/packages/comark-svelte/test/MarkdownNode.test.ts +++ b/packages/comark-svelte/test/MarkdownNode.test.ts @@ -384,3 +384,13 @@ 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( + '
'
+    )
+  })
+})
diff --git a/packages/comark-vue/src/components/MarkdownDocument.ts b/packages/comark-vue/src/components/MarkdownDocument.ts
index 530870c3..c2398c05 100644
--- a/packages/comark-vue/src/components/MarkdownDocument.ts
+++ b/packages/comark-vue/src/components/MarkdownDocument.ts
@@ -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()
@@ -142,7 +142,7 @@ 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 = {}
+    let props: Record = {}
     for (const k in resolved) {
       if (k === 'className') {
         props.class = resolved[k]
@@ -150,6 +150,9 @@ function renderNode(
         props[k] = resolved[k]
       }
     }
+    if (!customComponent) {
+      props = toNativeAttributes(tag, props)
+    }
 
     // @ts-expect-error - component might be a Vue component
     if (component?.props?.__node || component?.__asyncResolved?.props?.__node) {
diff --git a/packages/comark-vue/test/code-block.test.ts b/packages/comark-vue/test/code-block.test.ts
new file mode 100644
index 00000000..72a07405
--- /dev/null
+++ b/packages/comark-vue/test/code-block.test.ts
@@ -0,0 +1,46 @@
+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 = {}) {
+  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(
+      '
'
+    )
+  })
+
+  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(
+      '{"language":"ruby","filename":"app.rb","highlights":[1],"meta":"foo=bar"}'
+    )
+  })
+})
diff --git a/packages/comark/SPEC/COMARK/attributes/wrapped-pre-class.md b/packages/comark/SPEC/COMARK/attributes/wrapped-pre-class.md
index a30f2a16..82c7b87d 100644
--- a/packages/comark/SPEC/COMARK/attributes/wrapped-pre-class.md
+++ b/packages/comark/SPEC/COMARK/attributes/wrapped-pre-class.md
@@ -36,7 +36,7 @@ const variable = "value"
 ## HTML
 
 ```html
-
const variable = "value"
+
const variable = "value"
``` ## Markdown diff --git a/packages/comark/SPEC/COMARK/attributes/wrapped-pre-highlighted.md b/packages/comark/SPEC/COMARK/attributes/wrapped-pre-highlighted.md index 4327a366..4e813c65 100644 --- a/packages/comark/SPEC/COMARK/attributes/wrapped-pre-highlighted.md +++ b/packages/comark/SPEC/COMARK/attributes/wrapped-pre-highlighted.md @@ -95,7 +95,7 @@ const variable = "value" ## HTML ```html -
const variable = "value"
+
const variable = "value"
``` ## Markdown diff --git a/packages/comark/SPEC/COMARK/attributes/wrapped-pre.md b/packages/comark/SPEC/COMARK/attributes/wrapped-pre.md index 5135cf2f..a042e03f 100644 --- a/packages/comark/SPEC/COMARK/attributes/wrapped-pre.md +++ b/packages/comark/SPEC/COMARK/attributes/wrapped-pre.md @@ -36,7 +36,7 @@ const variable = "value" ## HTML ```html -
const variable = "value"
+
const variable = "value"
``` ## Markdown diff --git a/packages/comark/SPEC/COMARK/codeblock-diff-syntax.md b/packages/comark/SPEC/COMARK/codeblock-diff-syntax.md index 529d74d2..cb4d20ee 100644 --- a/packages/comark/SPEC/COMARK/codeblock-diff-syntax.md +++ b/packages/comark/SPEC/COMARK/codeblock-diff-syntax.md @@ -35,7 +35,7 @@ ## HTML ```html -
- const oldValue = 42;
+
- const oldValue = 42;
 + const newValue = 100;
   const unchanged = "same";
``` diff --git a/packages/comark/SPEC/COMARK/codeblock-empty.md b/packages/comark/SPEC/COMARK/codeblock-empty.md index 8341bc52..40d1acc7 100644 --- a/packages/comark/SPEC/COMARK/codeblock-empty.md +++ b/packages/comark/SPEC/COMARK/codeblock-empty.md @@ -32,7 +32,7 @@ ## HTML ```html -
+
``` ## Markdown diff --git a/packages/comark/SPEC/COMARK/codeblock-filename-highlight-lines.md b/packages/comark/SPEC/COMARK/codeblock-filename-highlight-lines.md index b9067992..990f1da7 100644 --- a/packages/comark/SPEC/COMARK/codeblock-filename-highlight-lines.md +++ b/packages/comark/SPEC/COMARK/codeblock-filename-highlight-lines.md @@ -42,7 +42,7 @@ function hello() { ## HTML ```html -
function hello() {
+
function hello() {
   console.log("Hello, World!");
 }
``` diff --git a/packages/comark/SPEC/COMARK/codeblock-filename-highlight-lines2.md b/packages/comark/SPEC/COMARK/codeblock-filename-highlight-lines2.md index 99bd42a7..b6aa7b5f 100644 --- a/packages/comark/SPEC/COMARK/codeblock-filename-highlight-lines2.md +++ b/packages/comark/SPEC/COMARK/codeblock-filename-highlight-lines2.md @@ -46,7 +46,7 @@ function hello() { ## HTML ```html -
function hello() {
+
function hello() {
   console.log("Hello, World!");
 }
``` diff --git a/packages/comark/SPEC/COMARK/codeblock-filename.md b/packages/comark/SPEC/COMARK/codeblock-filename.md index 54aff39f..c76c53ff 100644 --- a/packages/comark/SPEC/COMARK/codeblock-filename.md +++ b/packages/comark/SPEC/COMARK/codeblock-filename.md @@ -36,7 +36,7 @@ function hello() { ## HTML ```html -
function hello() {
+
function hello() {
   console.log("Hello, World!");
 }
``` diff --git a/packages/comark/SPEC/COMARK/codeblock-highlight-complex.md b/packages/comark/SPEC/COMARK/codeblock-highlight-complex.md index 2ae4a928..31daec06 100644 --- a/packages/comark/SPEC/COMARK/codeblock-highlight-complex.md +++ b/packages/comark/SPEC/COMARK/codeblock-highlight-complex.md @@ -51,7 +51,7 @@ func main() { ## HTML ```html -
package main
+
package main
 
 import "fmt"
 
diff --git a/packages/comark/SPEC/COMARK/codeblock-indented-content.md b/packages/comark/SPEC/COMARK/codeblock-indented-content.md
index da267f2c..cd2642e8 100644
--- a/packages/comark/SPEC/COMARK/codeblock-indented-content.md
+++ b/packages/comark/SPEC/COMARK/codeblock-indented-content.md
@@ -41,7 +41,7 @@
 
 ```html
 
-  
  ::accordion
+  
  ::accordion
     ::
``` diff --git a/packages/comark/SPEC/COMARK/codeblock-language-only.md b/packages/comark/SPEC/COMARK/codeblock-language-only.md index 158b5cfe..614735eb 100644 --- a/packages/comark/SPEC/COMARK/codeblock-language-only.md +++ b/packages/comark/SPEC/COMARK/codeblock-language-only.md @@ -34,7 +34,7 @@ console.log(greeting) ## HTML ```html -
const greeting: string = "Hello, World!"
+
const greeting: string = "Hello, World!"
 console.log(greeting)
``` diff --git a/packages/comark/SPEC/COMARK/codeblock-multiple-meta.md b/packages/comark/SPEC/COMARK/codeblock-multiple-meta.md index 4860adbf..5e07782b 100644 --- a/packages/comark/SPEC/COMARK/codeblock-multiple-meta.md +++ b/packages/comark/SPEC/COMARK/codeblock-multiple-meta.md @@ -35,7 +35,7 @@ def greet(name): ## HTML ```html -
def greet(name):
+
def greet(name):
     return f"Hello, {name}!"
``` diff --git a/packages/comark/SPEC/COMARK/codeblock-nested-4backtick.md b/packages/comark/SPEC/COMARK/codeblock-nested-4backtick.md index a4e634e1..389dcc57 100644 --- a/packages/comark/SPEC/COMARK/codeblock-nested-4backtick.md +++ b/packages/comark/SPEC/COMARK/codeblock-nested-4backtick.md @@ -40,7 +40,7 @@ ## HTML ```html -
::code-preview
+
::code-preview
 `inline code`
 
 #code
diff --git a/packages/comark/SPEC/COMARK/codeblock-nested-tilde.md b/packages/comark/SPEC/COMARK/codeblock-nested-tilde.md
index 850b2d1c..7552fc9a 100644
--- a/packages/comark/SPEC/COMARK/codeblock-nested-tilde.md
+++ b/packages/comark/SPEC/COMARK/codeblock-nested-tilde.md
@@ -40,7 +40,7 @@
 ## HTML
 
 ```html
-
::code-preview
+
::code-preview
 `inline code`
 
 #code
diff --git a/packages/comark/SPEC/COMARK/codeblock-pnpm-add.md b/packages/comark/SPEC/COMARK/codeblock-pnpm-add.md
index 0a98ce1b..b08a6db0 100644
--- a/packages/comark/SPEC/COMARK/codeblock-pnpm-add.md
+++ b/packages/comark/SPEC/COMARK/codeblock-pnpm-add.md
@@ -33,7 +33,7 @@ pnpm add comark
 ## HTML
 
 ```html
-
pnpm add comark
+
pnpm add comark
``` ## Markdown diff --git a/packages/comark/SPEC/COMARK/codeblock-rust-example.md b/packages/comark/SPEC/COMARK/codeblock-rust-example.md index 3e0076a5..edd2e890 100644 --- a/packages/comark/SPEC/COMARK/codeblock-rust-example.md +++ b/packages/comark/SPEC/COMARK/codeblock-rust-example.md @@ -44,7 +44,7 @@ fn main() { ## HTML ```html -
fn main() {
+
fn main() {
     let x = 5;
     let y = 10;
     println!("Sum: {}", x + y);
diff --git a/packages/comark/SPEC/COMARK/codeblock-special-chars.md b/packages/comark/SPEC/COMARK/codeblock-special-chars.md
index 5e161034..275763a2 100644
--- a/packages/comark/SPEC/COMARK/codeblock-special-chars.md
+++ b/packages/comark/SPEC/COMARK/codeblock-special-chars.md
@@ -38,7 +38,7 @@
 ## HTML
 
 ```html
-
<div class="container">
+
<div class="container">
   <h1>Title & Subtitle</h1>
   <p>Text with "quotes" and 'apostrophes'</p>
   <script>alert('XSS < > test');</script>
diff --git a/packages/comark/SPEC/COMARK/component-nested-codeblock-indented.md b/packages/comark/SPEC/COMARK/component-nested-codeblock-indented.md
index 3de36f54..e2089e85 100644
--- a/packages/comark/SPEC/COMARK/component-nested-codeblock-indented.md
+++ b/packages/comark/SPEC/COMARK/component-nested-codeblock-indented.md
@@ -50,7 +50,7 @@
 ```html
 
   
-    
  ::accordion
+    
  ::accordion
       ::
diff --git a/packages/comark/SPEC/COMARK/component-nested-codeblock-outdented.md b/packages/comark/SPEC/COMARK/component-nested-codeblock-outdented.md index f827062a..bbecf01e 100644 --- a/packages/comark/SPEC/COMARK/component-nested-codeblock-outdented.md +++ b/packages/comark/SPEC/COMARK/component-nested-codeblock-outdented.md @@ -50,7 +50,7 @@ ```html -
  ::accordion
+    
  ::accordion
       ::
diff --git a/packages/comark/SPEC/COMARK/component-nested-codeblock.md b/packages/comark/SPEC/COMARK/component-nested-codeblock.md index ee3222fb..79f073e4 100644 --- a/packages/comark/SPEC/COMARK/component-nested-codeblock.md +++ b/packages/comark/SPEC/COMARK/component-nested-codeblock.md @@ -42,7 +42,7 @@ hello ```html -
::alert
+  
::alert
   hello
   ::
diff --git a/packages/comark/SPEC/COMARK/component-slot-nested-codeblock.md b/packages/comark/SPEC/COMARK/component-slot-nested-codeblock.md index 1262bc37..d567db4b 100644 --- a/packages/comark/SPEC/COMARK/component-slot-nested-codeblock.md +++ b/packages/comark/SPEC/COMARK/component-slot-nested-codeblock.md @@ -50,7 +50,7 @@ hello ```html diff --git a/packages/comark/SPEC/COMARK/component-with-codeblock.md b/packages/comark/SPEC/COMARK/component-with-codeblock.md index 28bb212e..52498641 100644 --- a/packages/comark/SPEC/COMARK/component-with-codeblock.md +++ b/packages/comark/SPEC/COMARK/component-with-codeblock.md @@ -62,7 +62,7 @@ options: ```html -
---
+  
---
   title: The Mountains Website
   description: A website about the most iconic mountains in the world.
   ---
diff --git a/packages/comark/SPEC/COMARK/shiki-codeblock-dual-theme.md b/packages/comark/SPEC/COMARK/shiki-codeblock-dual-theme.md
index b36dc44a..f31f9ff1 100644
--- a/packages/comark/SPEC/COMARK/shiki-codeblock-dual-theme.md
+++ b/packages/comark/SPEC/COMARK/shiki-codeblock-dual-theme.md
@@ -161,7 +161,7 @@ console.log(greeting)
 ## HTML
 
 ```html
-
const greeting: string = "Hello, World!"
+
const greeting: string = "Hello, World!"
 console.log(greeting)
``` diff --git a/packages/comark/SPEC/COMARK/shiki-codeblock-highlight-complex.md b/packages/comark/SPEC/COMARK/shiki-codeblock-highlight-complex.md index e0de7ee6..63b220d2 100644 --- a/packages/comark/SPEC/COMARK/shiki-codeblock-highlight-complex.md +++ b/packages/comark/SPEC/COMARK/shiki-codeblock-highlight-complex.md @@ -396,7 +396,7 @@ func main() { ## HTML ```html -
package main
+
package main
 
 import "fmt"
 
diff --git a/packages/comark/SPEC/COMARK/shiki-codeblock-language-meta.md b/packages/comark/SPEC/COMARK/shiki-codeblock-language-meta.md
index d9eceaea..355170c2 100644
--- a/packages/comark/SPEC/COMARK/shiki-codeblock-language-meta.md
+++ b/packages/comark/SPEC/COMARK/shiki-codeblock-language-meta.md
@@ -131,7 +131,7 @@ console.log(greeting)
 ## HTML
 
 ```html
-
const greeting: string = "Hello, World!"
+
const greeting: string = "Hello, World!"
 console.log(greeting)
``` diff --git a/packages/comark/SPEC/COMARK/shiki-codeblock-language-only.md b/packages/comark/SPEC/COMARK/shiki-codeblock-language-only.md index a15c100f..152b0ef3 100644 --- a/packages/comark/SPEC/COMARK/shiki-codeblock-language-only.md +++ b/packages/comark/SPEC/COMARK/shiki-codeblock-language-only.md @@ -123,7 +123,7 @@ console.log(greeting) ## HTML ```html -
const greeting: string = "Hello, World!"
+
const greeting: string = "Hello, World!"
 console.log(greeting)
``` diff --git a/packages/comark/SPEC/COMARK/shiki-codeblock-rust-example.md b/packages/comark/SPEC/COMARK/shiki-codeblock-rust-example.md index 812bbba6..78f35900 100644 --- a/packages/comark/SPEC/COMARK/shiki-codeblock-rust-example.md +++ b/packages/comark/SPEC/COMARK/shiki-codeblock-rust-example.md @@ -236,7 +236,7 @@ fn main() { ## HTML ```html -
fn main() {
+
fn main() {
     let x = 5;
     let y = 10;
     println!("Sum: {}", x + y);
diff --git a/packages/comark/SPEC/COMARK/shiki-codeblock-special-chars.md b/packages/comark/SPEC/COMARK/shiki-codeblock-special-chars.md
index 90c0f2d1..2b394fbf 100644
--- a/packages/comark/SPEC/COMARK/shiki-codeblock-special-chars.md
+++ b/packages/comark/SPEC/COMARK/shiki-codeblock-special-chars.md
@@ -286,7 +286,7 @@ options:
 ## HTML
 
 ```html
-
<div class="container">
+
<div class="container">
   <h1>Title & Subtitle</h1>
   <p>Text with "quotes" and 'apostrophes'</p>
   <script>alert('XSS < > test');</script>
diff --git a/packages/comark/SPEC/COMARK/shiki-codeblock-twoslash.md b/packages/comark/SPEC/COMARK/shiki-codeblock-twoslash.md
index 5e21947e..8eecfc2c 100644
--- a/packages/comark/SPEC/COMARK/shiki-codeblock-twoslash.md
+++ b/packages/comark/SPEC/COMARK/shiki-codeblock-twoslash.md
@@ -150,7 +150,7 @@ const message = "Hello from twoslash"
 ## HTML
 
 ```html
-
const 
+
const 
const message: "Hello from twoslash"
message
= "Hello from twoslash"
``` diff --git a/packages/comark/SPEC/COMARK/shiki-codeblock-with-empty-lines.md b/packages/comark/SPEC/COMARK/shiki-codeblock-with-empty-lines.md index b54295fd..51ab7fc4 100644 --- a/packages/comark/SPEC/COMARK/shiki-codeblock-with-empty-lines.md +++ b/packages/comark/SPEC/COMARK/shiki-codeblock-with-empty-lines.md @@ -136,7 +136,7 @@ More content here. ## HTML ```html -
# My Page Title
+
# My Page Title
 
 This is the opening paragraph used as the description.
 
diff --git a/packages/comark/SPEC/COMARK/shiki-ordered-list-nested-codeblock.md b/packages/comark/SPEC/COMARK/shiki-ordered-list-nested-codeblock.md
index 8257ae0c..e9f6998f 100644
--- a/packages/comark/SPEC/COMARK/shiki-ordered-list-nested-codeblock.md
+++ b/packages/comark/SPEC/COMARK/shiki-ordered-list-nested-codeblock.md
@@ -104,7 +104,7 @@ options:
 
  1. Setup:

    -
    let x = 1;
    +
    let x = 1;
``` diff --git a/packages/comark/SPEC/common-mark/codeblock-with-empty-lines.md b/packages/comark/SPEC/common-mark/codeblock-with-empty-lines.md index 44808550..11f58ec2 100644 --- a/packages/comark/SPEC/common-mark/codeblock-with-empty-lines.md +++ b/packages/comark/SPEC/common-mark/codeblock-with-empty-lines.md @@ -49,7 +49,7 @@ More content here. ## HTML ```html -
# My Page Title
+
# My Page Title
 
 This is the opening paragraph used as the description.
 
diff --git a/packages/comark/SPEC/common-mark/ordered-list-mixed-blocks-nested.md b/packages/comark/SPEC/common-mark/ordered-list-mixed-blocks-nested.md
index c5c320bf..adc72f19 100644
--- a/packages/comark/SPEC/common-mark/ordered-list-mixed-blocks-nested.md
+++ b/packages/comark/SPEC/common-mark/ordered-list-mixed-blocks-nested.md
@@ -117,7 +117,7 @@
 
  1. Complex item:

    -
    code()
    +
    code()
    A note
    diff --git a/packages/comark/SPEC/common-mark/ordered-list-multiple-blocks.md b/packages/comark/SPEC/common-mark/ordered-list-multiple-blocks.md index 9ab16b4b..e0554ebb 100644 --- a/packages/comark/SPEC/common-mark/ordered-list-multiple-blocks.md +++ b/packages/comark/SPEC/common-mark/ordered-list-multiple-blocks.md @@ -68,7 +68,7 @@
    A quote here
    -
    code()
    +
    code()
  2. Second point
diff --git a/packages/comark/SPEC/common-mark/ordered-list-nested-codeblock.md b/packages/comark/SPEC/common-mark/ordered-list-nested-codeblock.md index 0b06ad07..72f7cea4 100644 --- a/packages/comark/SPEC/common-mark/ordered-list-nested-codeblock.md +++ b/packages/comark/SPEC/common-mark/ordered-list-nested-codeblock.md @@ -68,7 +68,7 @@
  1. Second level with code:

    -
    let x = 42;
    +
    let x = 42;
diff --git a/packages/comark/SPEC/common-mark/unordered-list-nested-deep-codeblock.md b/packages/comark/SPEC/common-mark/unordered-list-nested-deep-codeblock.md index 5c1bf82b..bdda3174 100644 --- a/packages/comark/SPEC/common-mark/unordered-list-nested-deep-codeblock.md +++ b/packages/comark/SPEC/common-mark/unordered-list-nested-deep-codeblock.md @@ -85,7 +85,7 @@
  • Level 3 with code:

    -
    print("deep")
    +
    print("deep")
diff --git a/packages/comark/src/internal/stringify/handlers/html.ts b/packages/comark/src/internal/stringify/handlers/html.ts index 4cced776..4bf69302 100644 --- a/packages/comark/src/internal/stringify/handlers/html.ts +++ b/packages/comark/src/internal/stringify/handlers/html.ts @@ -1,7 +1,7 @@ import type { State } from 'comark/render' import type { ElementNode } from 'comark' import { htmlAttributes } from '../attributes.ts' -import { indent } from '../../../utils/index.ts' +import { indent, toNativeAttributes } from '../../../utils/index.ts' const textBlocks = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'td', 'th']) const selfCloseTags = new Set(['br', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr']) @@ -99,7 +99,8 @@ export async function html(node: ElementNode, state: State, parent?: ElementNode state.applyContext(revert) } - const attrs = Object.keys(attributes).length > 0 ? ` ${htmlAttributes(attributes)}` : '' + const nativeAttributes = toNativeAttributes(String(tag), attributes) + const attrs = Object.keys(nativeAttributes).length > 0 ? ` ${htmlAttributes(nativeAttributes)}` : '' if (isSelfClose) { return `<${tag}${attrs}>` + (!parent && !isInline ? state.context.blockSeparator : '') diff --git a/packages/comark/src/utils/index.ts b/packages/comark/src/utils/index.ts index 76790343..ca0ab81c 100644 --- a/packages/comark/src/utils/index.ts +++ b/packages/comark/src/utils/index.ts @@ -121,6 +121,25 @@ export async function visitAsync( } } +const CODE_BLOCK_PROPS = new Set(['language', 'filename', 'highlights', 'meta']) + +/** Maps code block props on a native `
` to `data-*` attributes. */
+export function toNativeAttributes>(tag: string, attributes: T): T {
+  if (tag !== 'pre') return attributes
+  let changed = false
+  const result: Record = {}
+  for (const key in attributes) {
+    if (CODE_BLOCK_PROPS.has(key)) {
+      changed = true
+      const value = attributes[key]
+      result[`data-${key}`] = value !== null && typeof value === 'object' ? JSON.stringify(value) : value
+    } else {
+      result[key] = attributes[key]
+    }
+  }
+  return changed ? (result as T) : attributes
+}
+
 // #region String Utils
 
 const HTML_ESCAPE_RE = /[&<>"]/g

From f74db9d520be8246b113e443c989bd0a5cbd154e Mon Sep 17 00:00:00 2001
From: Daniel Roe 
Date: Mon, 28 Sep 2026 16:26:35 +0000
Subject: [PATCH 2/6] fix: keep code block props in binding scope of native
 `pre` children

---
 .../comark-react/src/components/MarkdownDocument.tsx   |  3 ++-
 packages/comark-react/test/code-block.test.tsx         | 10 ++++++++++
 .../comark-svelte/src/components/MarkdownNode.svelte   | 10 ++++------
 packages/comark-svelte/test/MarkdownNode.test.ts       |  6 ++++++
 packages/comark-vue/src/components/MarkdownDocument.ts |  3 ++-
 packages/comark-vue/test/code-block.test.ts            | 10 ++++++++++
 6 files changed, 34 insertions(+), 8 deletions(-)

diff --git a/packages/comark-react/src/components/MarkdownDocument.tsx b/packages/comark-react/src/components/MarkdownDocument.tsx
index d6feb99f..7b998ba8 100644
--- a/packages/comark-react/src/components/MarkdownDocument.tsx
+++ b/packages/comark-react/src/components/MarkdownDocument.tsx
@@ -155,6 +155,7 @@ function renderNode(
       }
     }
 
+    const scopeProps = props
     if (!customComponent) {
       props = toNativeAttributes(tag, props)
     }
@@ -177,7 +178,7 @@ function renderNode(
     // own attributes. Bare wrappers (`

`, `

    `, `
  • `, …) 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 = {} const regularChildren: React.ReactNode[] = [] diff --git a/packages/comark-react/test/code-block.test.tsx b/packages/comark-react/test/code-block.test.tsx index 4d6ff62d..81bcd5a4 100644 --- a/packages/comark-react/test/code-block.test.tsx +++ b/packages/comark-react/test/code-block.test.tsx @@ -29,4 +29,14 @@ describe('code blocks', () => { '{"language":"ruby","filename":"app.rb","highlights":[1],"meta":"foo=bar"}' ) }) + + 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() + expect(html).toContain('') + }) }) diff --git a/packages/comark-svelte/src/components/MarkdownNode.svelte b/packages/comark-svelte/src/components/MarkdownNode.svelte index cc3ec223..1a4ea7d8 100644 --- a/packages/comark-svelte/src/components/MarkdownNode.svelte +++ b/packages/comark-svelte/src/components/MarkdownNode.svelte @@ -191,16 +191,14 @@ naturally appears inline after the deepest trailing text node. } } - if (!Component && !componentPromise) { - mappedProps = toNativeAttributes(tag, mappedProps) - } - return { isText, tag, isVoid, children, Component, componentPromise, mappedProps } }) // Only shadow the parent's `props` scope when the current element has its // own attributes. Bare wrappers (`

    `, `

      `, `
    • `, …) must keep the // parent's scope so bindings like `{{ props.x }}` reach across them. + let nativeProps = $derived(tag ? toNativeAttributes(tag, mappedProps) : mappedProps) + let childrenRenderData = $derived( Object.keys(mappedProps).length > 0 ? { ...renderData, props: mappedProps } @@ -287,9 +285,9 @@ naturally appears inline after the deepest trailing text node. {@render renderChildren()} {:else if isVoid} - + {:else if tag} - + {@render renderChildren()} {/if} diff --git a/packages/comark-svelte/test/MarkdownNode.test.ts b/packages/comark-svelte/test/MarkdownNode.test.ts index fb018d40..b6b2f556 100644 --- a/packages/comark-svelte/test/MarkdownNode.test.ts +++ b/packages/comark-svelte/test/MarkdownNode.test.ts @@ -393,4 +393,10 @@ describe('MarkdownNode code blocks', () => { '
      '
           )
         })
      +
      +  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('')
      +  })
       })
      diff --git a/packages/comark-vue/src/components/MarkdownDocument.ts b/packages/comark-vue/src/components/MarkdownDocument.ts
      index c2398c05..2da5ea01 100644
      --- a/packages/comark-vue/src/components/MarkdownDocument.ts
      +++ b/packages/comark-vue/src/components/MarkdownDocument.ts
      @@ -150,6 +150,7 @@ function renderNode(
               props[k] = resolved[k]
             }
           }
      +    const scopeProps = props
           if (!customComponent) {
             props = toNativeAttributes(tag, props)
           }
      @@ -172,7 +173,7 @@ function renderNode(
           // own attributes. Bare wrappers (`

      `, `

        `, `
      • `, …) 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 (VNode | string)[]> = {} const regularChildren: (VNode | string)[] = [] diff --git a/packages/comark-vue/test/code-block.test.ts b/packages/comark-vue/test/code-block.test.ts index 72a07405..2c6de050 100644 --- a/packages/comark-vue/test/code-block.test.ts +++ b/packages/comark-vue/test/code-block.test.ts @@ -43,4 +43,14 @@ describe('code blocks', () => { '{"language":"ruby","filename":"app.rb","highlights":[1],"meta":"foo=bar"}' ) }) + + 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('') + }) }) From 8fedac9ef7fe67e1e923fdb1b42b67574cc3299f Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 28 Sep 2026 16:26:36 +0000 Subject: [PATCH 3/6] chore: update bundle size snapshot --- test/bundle.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/bundle.test.ts b/test/bundle.test.ts index 7d61a1e7..80e1b350 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -64,9 +64,9 @@ describe('package bundle size', { timeout: 60_000 }, () => { "@comark/ansi": "37.3k (98 files)", "@comark/html": "16.5k (58 files)", "@comark/nuxt": "11.8k (58 files)", - "@comark/react": "37.7k (76 files)", - "@comark/svelte": "45.7k (84 files)", - "@comark/vue": "56.0k (80 files)", + "@comark/react": "37.9k (76 files)", + "@comark/svelte": "45.8k (84 files)", + "@comark/vue": "56.1k (80 files)", "comark": "374k (160 files)", } `) From 4f5d0c0bb181adb5cf53924fbc247b9332947a7b Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Fri, 2 Oct 2026 14:14:13 +0200 Subject: [PATCH 4/6] fix: do not change html blocks --- .../src/components/markdown-node.component.ts | 14 +++++++++++--- .../src/components/MarkdownDocument.tsx | 2 +- packages/comark-react/test/code-block.test.tsx | 11 +++++++++++ .../src/components/MarkdownNode.svelte | 2 +- packages/comark-svelte/test/MarkdownNode.test.ts | 7 +++++++ .../comark-vue/src/components/MarkdownDocument.ts | 2 +- packages/comark-vue/test/code-block.test.ts | 11 +++++++++++ .../comark/src/internal/stringify/handlers/html.ts | 2 +- packages/comark/src/utils/index.ts | 12 +++++++++--- packages/comark/test/html-escape.test.ts | 10 ++++++++++ packages/comark/test/html-roundtrip.test.ts | 1 + 11 files changed, 64 insertions(+), 10 deletions(-) diff --git a/packages/comark-angular/src/components/markdown-node.component.ts b/packages/comark-angular/src/components/markdown-node.component.ts index 901f3cc9..ae752b84 100644 --- a/packages/comark-angular/src/components/markdown-node.component.ts +++ b/packages/comark-angular/src/components/markdown-node.component.ts @@ -196,10 +196,11 @@ export class MarkdownNode implements OnChanges { tag: string, attrs: Record, children: MarkdownAstNode[], - childrenRenderData: NodeRenderData + childrenRenderData: NodeRenderData, + node?: MarkdownAstNode ): void { const el = this.renderer.createElement(tag) - this.applyAttributes(el, toNativeAttributes(tag, attrs)) + this.applyAttributes(el, node ? toNativeAttributes(tag, node, attrs) : attrs) // `innerHTML` from document attributes is never applied — resolveAttributes // drops DOM sink props, and raw HTML has its own explicit parse path. @@ -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. */ diff --git a/packages/comark-react/src/components/MarkdownDocument.tsx b/packages/comark-react/src/components/MarkdownDocument.tsx index 7b998ba8..e948f69d 100644 --- a/packages/comark-react/src/components/MarkdownDocument.tsx +++ b/packages/comark-react/src/components/MarkdownDocument.tsx @@ -157,7 +157,7 @@ function renderNode( const scopeProps = props if (!customComponent) { - props = toNativeAttributes(tag, props) + props = toNativeAttributes(tag, node, props) } if (typeof Component !== 'string' && (Component as any)?.propTypes?.__node !== undefined) { diff --git a/packages/comark-react/test/code-block.test.tsx b/packages/comark-react/test/code-block.test.tsx index 81bcd5a4..601fa358 100644 --- a/packages/comark-react/test/code-block.test.tsx +++ b/packages/comark-react/test/code-block.test.tsx @@ -30,6 +30,17 @@ describe('code blocks', () => { ) }) + 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() + expect(html).toContain('
        ')
        +    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']]],
        diff --git a/packages/comark-svelte/src/components/MarkdownNode.svelte b/packages/comark-svelte/src/components/MarkdownNode.svelte
        index 1a4ea7d8..23cf98ba 100644
        --- a/packages/comark-svelte/src/components/MarkdownNode.svelte
        +++ b/packages/comark-svelte/src/components/MarkdownNode.svelte
        @@ -197,7 +197,7 @@ 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 (`

        `, `

          `, `
        • `, …) must keep the // parent's scope so bindings like `{{ props.x }}` reach across them. - let nativeProps = $derived(tag ? toNativeAttributes(tag, mappedProps) : mappedProps) + let nativeProps = $derived(tag ? toNativeAttributes(tag, node, mappedProps) : mappedProps) let childrenRenderData = $derived( Object.keys(mappedProps).length > 0 diff --git a/packages/comark-svelte/test/MarkdownNode.test.ts b/packages/comark-svelte/test/MarkdownNode.test.ts index b6b2f556..1901c2ad 100644 --- a/packages/comark-svelte/test/MarkdownNode.test.ts +++ b/packages/comark-svelte/test/MarkdownNode.test.ts @@ -394,6 +394,13 @@ describe('MarkdownNode code blocks', () => { ) }) + 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('
          ')
          +    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 } })
          diff --git a/packages/comark-vue/src/components/MarkdownDocument.ts b/packages/comark-vue/src/components/MarkdownDocument.ts
          index 2da5ea01..1201fdd8 100644
          --- a/packages/comark-vue/src/components/MarkdownDocument.ts
          +++ b/packages/comark-vue/src/components/MarkdownDocument.ts
          @@ -152,7 +152,7 @@ function renderNode(
               }
               const scopeProps = props
               if (!customComponent) {
          -      props = toNativeAttributes(tag, props)
          +      props = toNativeAttributes(tag, node, props)
               }
           
               // @ts-expect-error - component might be a Vue component
          diff --git a/packages/comark-vue/test/code-block.test.ts b/packages/comark-vue/test/code-block.test.ts
          index 2c6de050..157f57c8 100644
          --- a/packages/comark-vue/test/code-block.test.ts
          +++ b/packages/comark-vue/test/code-block.test.ts
          @@ -44,6 +44,17 @@ describe('code blocks', () => {
               )
             })
           
          +  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('
          ')
          +    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']]],
          diff --git a/packages/comark/src/internal/stringify/handlers/html.ts b/packages/comark/src/internal/stringify/handlers/html.ts
          index 4bf69302..af5feed6 100644
          --- a/packages/comark/src/internal/stringify/handlers/html.ts
          +++ b/packages/comark/src/internal/stringify/handlers/html.ts
          @@ -99,7 +99,7 @@ export async function html(node: ElementNode, state: State, parent?: ElementNode
               state.applyContext(revert)
             }
           
          -  const nativeAttributes = toNativeAttributes(String(tag), attributes)
          +  const nativeAttributes = toNativeAttributes(String(tag), node, attributes)
             const attrs = Object.keys(nativeAttributes).length > 0 ? ` ${htmlAttributes(nativeAttributes)}` : ''
           
             if (isSelfClose) {
          diff --git a/packages/comark/src/utils/index.ts b/packages/comark/src/utils/index.ts
          index ca0ab81c..34332f3c 100644
          --- a/packages/comark/src/utils/index.ts
          +++ b/packages/comark/src/utils/index.ts
          @@ -123,9 +123,15 @@ export async function visitAsync(
           
           const CODE_BLOCK_PROPS = new Set(['language', 'filename', 'highlights', 'meta'])
           
          -/** Maps code block props on a native `
          ` to `data-*` attributes. */
          -export function toNativeAttributes>(tag: string, attributes: T): T {
          -  if (tag !== 'pre') return attributes
          +/**
          + * Maps code block props on a native `
          ` to `data-*` attributes.
          + *
          + * Raw HTML (`node[1].$.html`) is left unchanged so authored attributes round-trip.
          + * The attribute bag does not include `$` — `resolveAttributes` strips it — so the source
          + * node is required.
          + */
          +export function toNativeAttributes>(tag: string, node: Node, attributes: T): T {
          +  if (tag !== 'pre' || !Array.isArray(node) || node[1]?.$?.html === 1) return attributes
             let changed = false
             const result: Record = {}
             for (const key in attributes) {
          diff --git a/packages/comark/test/html-escape.test.ts b/packages/comark/test/html-escape.test.ts
          index 6551678d..81bc7a35 100644
          --- a/packages/comark/test/html-escape.test.ts
          +++ b/packages/comark/test/html-escape.test.ts
          @@ -11,6 +11,16 @@ const renderNodes = async (nodes: Node[]) =>
             render({ nodes, frontmatter: {}, meta: {} }, { format: 'text/html', blockSeparator: '\n' })
           
           describe('HTML attribute escaping', () => {
          +  it('keeps authored language on a raw pre and remaps fence language', async () => {
          +    const raw = await renderHtml('
          \nputs 1\n
          ') + expect(raw).toContain('
          ')
          +    expect(raw).not.toContain('data-language')
          +
          +    const fence = await renderHtml('```ruby\nputs 1\n```')
          +    expect(fence).toContain('
          ')
          +    expect(fence).not.toContain(' language="ruby"')
          +  })
          +
             it('escapes double quotes in raw-HTML attribute values', async () => {
               const html = await renderHtml(`hi`)
               expect(html).toContain('title="a" onmouseover=alert(1)"')
          diff --git a/packages/comark/test/html-roundtrip.test.ts b/packages/comark/test/html-roundtrip.test.ts
          index 87f1a423..47dae02c 100644
          --- a/packages/comark/test/html-roundtrip.test.ts
          +++ b/packages/comark/test/html-roundtrip.test.ts
          @@ -28,6 +28,7 @@ describe('raw HTML round-trip (parse → renderMarkdown → parse)', () => {
               'nested divs': '
          \n
          one
          \n
          two
          \n
          ', 'single-child video': '', 'mixed text and element children': '
          \nsome text\n
          cap
          \n
          ', + 'pre with authored language attribute': '
          \nputs 1\n
          ', } for (const [name, md] of Object.entries(cases)) { From 9e40805218c163ce18879dbeb04f364e0f101b5a Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Fri, 2 Oct 2026 14:22:37 +0200 Subject: [PATCH 5/6] Update markdown-node.component.ts --- .../comark-angular/src/components/markdown-node.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/comark-angular/src/components/markdown-node.component.ts b/packages/comark-angular/src/components/markdown-node.component.ts index ae752b84..9acf28d8 100644 --- a/packages/comark-angular/src/components/markdown-node.component.ts +++ b/packages/comark-angular/src/components/markdown-node.component.ts @@ -393,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) } } } From e02890f88276b160795f09561c1c2cfe4d860af7 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Fri, 2 Oct 2026 14:35:33 +0200 Subject: [PATCH 6/6] Update bundle.test.ts --- test/bundle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/bundle.test.ts b/test/bundle.test.ts index 80e1b350..4ce4d51e 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -67,7 +67,7 @@ describe('package bundle size', { timeout: 60_000 }, () => { "@comark/react": "37.9k (76 files)", "@comark/svelte": "45.8k (84 files)", "@comark/vue": "56.1k (80 files)", - "comark": "374k (160 files)", + "comark": "375k (160 files)", } `) })