arve0 / markdown-it-attrs

Add classes, identifiers and attributes to your markdown with {} curly brackets, similar to pandoc's header attributes
MIT License
295 stars 58 forks source link

Attributes on fenced code block should go on the outer `<pre>`, not the inner `<code>` #152

Open LeaVerou opened 1 year ago

LeaVerou commented 1 year ago

In general, it seems reasonable that when a Markdown structure returns multiple nested elements, that setting attributes on it would go on the container, rather than any descendant. Currently, setting attributes on a fenced code block sets them on the inner <code> element instead of the outer <pre>.

Markdown-it versions:

├── markdown-it-anchor@8.6.7
├── markdown-it-attrs@4.1.6
├── markdown-it@13.0.1

Example input:

```js { data-file="index.js" }
foo();

Current output:

<pre><code data-file="index.js" class="language-js">foo();
</code></pre>

Expected output:

<pre data-file="index.js"><code class="language-js">foo();
</code></pre>
arve0 commented 1 year ago

Hi 👋 I believe that is a common issue that has been reported multiple times, addressed in the readme: https://github.com/arve0/markdown-it-attrs#custom-rendering

This plug-in do not alter original rendering from markdown-it.

Let me know if I’m wrong (away from computer right now 🏖️).

TrebledJ commented 3 weeks ago

Got this to work by copying and modifying the original fence rule.

  md.renderer.rules.fence = function (tokens, idx, options, env, slf) {
    const token = tokens[idx]
    const info = token.info ? unescapeAll(token.info).trim() : ''
    let langName = ''
    let langAttrs = ''

    if (info) {
      const arr = info.split(/(\s+)/g)
      langName = arr[0]
      langAttrs = arr.slice(2).join('')
    }

    let highlighted
    if (options.highlight) {
      highlighted = options.highlight(token.content, langName, langAttrs) || escapeHtml(token.content)
    } else {
      highlighted = escapeHtml(token.content)
    }

    if (highlighted.indexOf('<pre') === 0) {
      return highlighted + '\n'
    }

    // If language exists, inject class gently, without modifying original token.
    // May be, one day we will add .deepClone() for token and simplify this part, but
    // now we prefer to keep things local.
    if (info) {
      const i = token.attrIndex('class')
      const tmpAttrs = token.attrs ? token.attrs.slice() : []

      if (i < 0) {
        tmpAttrs.push(['class', options.langPrefix + langName])
      } else {
        tmpAttrs[i] = tmpAttrs[i].slice()
        tmpAttrs[i][1] += ' ' + options.langPrefix + langName
      }

      // Fake token just to render attributes
      const tmpToken = {
        attrs: tmpAttrs
      }

-     return `<pre><code${slf.renderAttrs(tmpToken)}>${highlighted}</code></pre>\n`
+     return `<pre${slf.renderAttrs(tmpToken)}><code class="${options.langPrefix}${langName}">${highlighted}</code></pre>\n`
    }

-   return `<pre><code${slf.renderAttrs(token)}>${highlighted}</code></pre>\n`    
+   return `<pre${slf.renderAttrs(token)}><code>${highlighted}</code></pre>\n`
  }

A bit of a dirty hack, but should be more robust than the example presented in the Custom Rendering section (which I tried, but it missed various features). This should also be more performant compared to calling the default rule then replacing with regex.