Skip to content

Commit 9b2eca1

Browse files
committed
feat: compatibility with markdown-it-attrs
1 parent 643b813 commit 9b2eca1

File tree

6 files changed

+109
-14
lines changed

6 files changed

+109
-14
lines changed

package-lock.json

Lines changed: 20 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"eslint": "7.32.0",
6262
"jest": "27.0.6",
6363
"markdown-it": "12.2.0",
64+
"markdown-it-attrs": "4.0.0",
6465
"npm-run-all": "4.1.5",
6566
"semantic-release": "17.4.5",
6667
"typescript": "4.3.5"

src/index.ts

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import Prism, {Grammar} from 'prismjs'
22
import loadLanguages from 'prismjs/components/'
33
import MarkdownIt from 'markdown-it'
4+
import {RenderRule} from 'markdown-it/lib/renderer'
45

56
interface Options {
7+
/**
8+
* Prism plugins to load.
9+
*/
610
plugins: string[]
711
/**
812
* Callback for Prism initialisation. Useful for initialising plugins.
@@ -36,7 +40,7 @@ const DEFAULTS: Options = {
3640

3741

3842
/**
39-
* Loads the provided {@code lang} into prism.
43+
* Loads the provided `lang` into prism.
4044
*
4145
* @param lang
4246
* Code of the language to load.
@@ -53,10 +57,10 @@ function loadPrismLang(lang: string): Grammar | undefined {
5357
}
5458

5559
/**
56-
* Loads the provided Prism plugin.a
60+
* Loads the provided Prism plugin.
5761
* @param name
58-
* Name of the plugin to load
59-
* @throws {Error} If there is no plugin with the provided {@code name}
62+
* Name of the plugin to load.
63+
* @throws {Error} If there is no plugin with the provided `name`.
6064
*/
6165
function loadPrismPlugin(name: string): void {
6266
try {
@@ -74,7 +78,7 @@ function loadPrismPlugin(name: string): void {
7478
* The options that were used to initialise the plugin.
7579
* @param lang
7680
* Code of the language to highlight the text in.
77-
* @return The name of the language to use and the Prism language object for that language.
81+
* @return The name of the language to use and the Prism language object for that language.
7882
*/
7983
function selectLanguage(options: Options, lang: string): [string, Grammar | undefined] {
8084
let langToUse = lang
@@ -93,21 +97,64 @@ function selectLanguage(options: Options, lang: string): [string, Grammar | unde
9397
* Highlights the provided text using Prism.
9498
*
9599
* @param markdownit
96-
* The markdown-it instance
100+
* The markdown-it instance.
97101
* @param options
98102
* The options that have been used to initialise the plugin.
99103
* @param text
100104
* The text to highlight.
101105
* @param lang
102106
* Code of the language to highlight the text in.
103-
* @return {@code text} wrapped in {@code <pre>} and {@code <code>}, both equipped with the appropriate class
104-
* (markdown-it’s langPrefix + lang). If Prism knows {@code lang}, {@code text} will be highlighted by it.
107+
* @return If Prism knows the language that {@link selectLanguage} returns for `lang`, the `text` highlighted for that language. Otherwise, `text`
108+
* html-escaped.
105109
*/
106110
function highlight(markdownit: MarkdownIt, options: Options, text: string, lang: string): string {
107111
const [langToUse, prismLang] = selectLanguage(options, lang)
108-
const code = prismLang ? Prism.highlight(text, prismLang, langToUse) : markdownit.utils.escapeHtml(text)
109-
const classAttribute = langToUse ? ` class="${markdownit.options.langPrefix}${markdownit.utils.escapeHtml(langToUse)}"` : ''
110-
return `<pre${classAttribute}><code${classAttribute}>${code}</code></pre>`
112+
return prismLang ? Prism.highlight(text, prismLang, langToUse) : markdownit.utils.escapeHtml(text)
113+
}
114+
115+
/**
116+
* Construct the class name for the provided `lang`.
117+
*
118+
* @param markdownit
119+
* The markdown-it instance.
120+
* @param lang
121+
* The selected language.
122+
* @return the class to use for `lang`.
123+
*/
124+
function languageClass(markdownit: MarkdownIt, lang: string): string {
125+
return markdownit.options.langPrefix + markdownit.utils.escapeHtml(lang)
126+
}
127+
128+
/**
129+
* Patch the `<pre>` and `<code>` tags produced by the `existingRule` for fenced code blocks.
130+
*
131+
* @param markdownit
132+
* The markdown-it instance.
133+
* @param options
134+
* The options that have been used to initialise the plugin.
135+
* @param existingRule
136+
* The currently configured render rule for fenced code blocks.
137+
*/
138+
function applyCodeAttributes(markdownit: MarkdownIt, options: Options, existingRule: RenderRule): RenderRule {
139+
return (tokens, idx, renderOptions, env, self) => {
140+
const fenceToken = tokens[idx]
141+
const info = fenceToken.info ? markdownit.utils.unescapeAll(fenceToken.info).trim() : ''
142+
const lang = info.split(/(\s+)/g)[0]
143+
const [langToUse] = selectLanguage(options, lang)
144+
if (!langToUse) {
145+
return existingRule(tokens, idx, renderOptions, env, self)
146+
} else {
147+
fenceToken.info = langToUse
148+
const existingResult = existingRule(tokens, idx, renderOptions, env, self)
149+
const langClass = languageClass(markdownit, langToUse)
150+
return existingResult.replace(
151+
/<((?:pre|code)[^>]*?)(?:\s+class="([^"]*)"([^>]*))?>/g,
152+
(match, tagStart, existingClasses?: string, tagEnd?) =>
153+
existingClasses?.includes(langClass) ? match
154+
: `<${tagStart} class="${existingClasses ? `${existingClasses} ` : ''}${langClass}"${tagEnd || ''}>`
155+
)
156+
}
157+
}
111158
}
112159

113160
/**
@@ -152,4 +199,5 @@ export default function markdownItPrism(markdownit: MarkdownIt, useroptions: Opt
152199

153200
// register ourselves as highlighter
154201
markdownit.options.highlight = (text, lang) => highlight(markdownit, options, text, lang)
202+
markdownit.renderer.rules.fence = applyCodeAttributes(markdownit, options, markdownit.renderer.rules.fence || (() => ''))
155203
}

test/test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import markdownit from 'markdown-it'
2+
// @ts-ignore markdown-it-attrs has no types and it’s not worth the effort adding a *.d.ts file
3+
import markdownItAttrs from 'markdown-it-attrs'
24
import markdownItPrism from '../src'
35
import fs from 'fs'
46

@@ -133,8 +135,14 @@ describe('markdown-it-prism', () => {
133135

134136
describe('plugin support', () => {
135137

136-
afterEach(() => {
137-
jest.resetModules()
138+
afterEach(() => jest.resetModules())
139+
140+
it('allows to use markdown-it-attrs', async () => {
141+
expect(markdownit()
142+
.use(markdownItPrism)
143+
.use(markdownItAttrs)
144+
.render(await read('input/with-attrs.md'))
145+
).toEqual(await read('expected/fenced-with-attrs.html'))
138146
})
139147

140148
it('allows to use Prism plugins', async () => {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<h1>Test</h1>
2+
<p>This is a fenced code block:</p>
3+
<pre class="language-java"><code class="classname language-java" data-custom="value"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Foo</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
4+
<span class="token keyword">public</span> <span class="token class-name">Foo</span><span class="token punctuation">(</span>bar<span class="token punctuation">)</span> <span class="token punctuation">{</span>
5+
<span class="token class-name">System</span><span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>bar<span class="token punctuation">)</span><span class="token punctuation">;</span>
6+
<span class="token punctuation">}</span>
7+
<span class="token punctuation">}</span>
8+
</code></pre>

testdata/input/with-attrs.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Test
2+
3+
This is a fenced code block:
4+
5+
```java {.classname data-custom=value}
6+
public class Foo() {
7+
public Foo(bar) {
8+
System.out.println(bar);
9+
}
10+
}
11+
```

0 commit comments

Comments
 (0)