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..9acf28d8 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 @@ -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, 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. */ @@ -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) } } } diff --git a/packages/comark-react/src/components/MarkdownDocument.tsx b/packages/comark-react/src/components/MarkdownDocument.tsx index eced24f3..e948f69d 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,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 } @@ -173,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 new file mode 100644 index 00000000..601fa358 --- /dev/null +++ b/packages/comark-react/test/code-block.test.tsx @@ -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() + 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"}' + ) + }) + + 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']]],
    +      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 dc9db091..23cf98ba 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: {} }
     
    @@ -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 (`

    `, `

      `, `
    • `, …) 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( Object.keys(mappedProps).length > 0 ? { ...renderData, props: mappedProps } @@ -283,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 fa3f8d63..1901c2ad 100644 --- a/packages/comark-svelte/test/MarkdownNode.test.ts +++ b/packages/comark-svelte/test/MarkdownNode.test.ts @@ -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( + '
      '
      +    )
      +  })
      +
      +  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 } })
      +    expect(html(body)).toContain('')
      +  })
      +})
      diff --git a/packages/comark-vue/src/components/MarkdownDocument.ts b/packages/comark-vue/src/components/MarkdownDocument.ts
      index 530870c3..1201fdd8 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,10 @@ function renderNode(
               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) {
      @@ -169,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 new file mode 100644 index 00000000..157f57c8 --- /dev/null +++ b/packages/comark-vue/test/code-block.test.ts @@ -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 = {}) { + 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"}'
        +    )
        +  })
        +
        +  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']]],
        +      frontmatter: {},
        +      meta: {},
        +    } as any
        +    const html = await renderToString(createSSRApp({ render: () => h(MarkdownDocument, { value: tree }) }))
        +    expect(html).toContain('')
        +  })
        +})
        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..af5feed6 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), node, 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..34332f3c 100644 --- a/packages/comark/src/utils/index.ts +++ b/packages/comark/src/utils/index.ts @@ -121,6 +121,31 @@ export async function visitAsync( } } +const CODE_BLOCK_PROPS = new Set(['language', 'filename', 'highlights', 'meta']) + +/** + * 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) {
        +    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
        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)) { diff --git a/test/bundle.test.ts b/test/bundle.test.ts index 7d61a1e7..4ce4d51e 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -64,10 +64,10 @@ 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": "374k (160 files)", + "@comark/react": "37.9k (76 files)", + "@comark/svelte": "45.8k (84 files)", + "@comark/vue": "56.1k (80 files)", + "comark": "375k (160 files)", } `) })