Skip to content

Commit

Permalink
feat: compatibility with markdown-it-attrs
Browse files Browse the repository at this point in the history
  • Loading branch information
jGleitz committed Aug 23, 2021
1 parent 643b813 commit 9b2eca1
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 14 deletions.
21 changes: 20 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"eslint": "7.32.0",
"jest": "27.0.6",
"markdown-it": "12.2.0",
"markdown-it-attrs": "4.0.0",
"npm-run-all": "4.1.5",
"semantic-release": "17.4.5",
"typescript": "4.3.5"
Expand Down
70 changes: 59 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import Prism, {Grammar} from 'prismjs'
import loadLanguages from 'prismjs/components/'
import MarkdownIt from 'markdown-it'
import {RenderRule} from 'markdown-it/lib/renderer'

interface Options {
/**
* Prism plugins to load.
*/
plugins: string[]
/**
* Callback for Prism initialisation. Useful for initialising plugins.
Expand Down Expand Up @@ -36,7 +40,7 @@ const DEFAULTS: Options = {


/**
* Loads the provided {@code lang} into prism.
* Loads the provided `lang` into prism.
*
* @param lang
* Code of the language to load.
Expand All @@ -53,10 +57,10 @@ function loadPrismLang(lang: string): Grammar | undefined {
}

/**
* Loads the provided Prism plugin.a
* Loads the provided Prism plugin.
* @param name
* Name of the plugin to load
* @throws {Error} If there is no plugin with the provided {@code name}
* Name of the plugin to load.
* @throws {Error} If there is no plugin with the provided `name`.
*/
function loadPrismPlugin(name: string): void {
try {
Expand All @@ -74,7 +78,7 @@ function loadPrismPlugin(name: string): void {
* The options that were used to initialise the plugin.
* @param lang
* Code of the language to highlight the text in.
* @return The name of the language to use and the Prism language object for that language.
* @return The name of the language to use and the Prism language object for that language.
*/
function selectLanguage(options: Options, lang: string): [string, Grammar | undefined] {
let langToUse = lang
Expand All @@ -93,21 +97,64 @@ function selectLanguage(options: Options, lang: string): [string, Grammar | unde
* Highlights the provided text using Prism.
*
* @param markdownit
* The markdown-it instance
* The markdown-it instance.
* @param options
* The options that have been used to initialise the plugin.
* @param text
* The text to highlight.
* @param lang
* Code of the language to highlight the text in.
* @return {@code text} wrapped in {@code <pre>} and {@code <code>}, both equipped with the appropriate class
* (markdown-it’s langPrefix + lang). If Prism knows {@code lang}, {@code text} will be highlighted by it.
* @return If Prism knows the language that {@link selectLanguage} returns for `lang`, the `text` highlighted for that language. Otherwise, `text`
* html-escaped.
*/
function highlight(markdownit: MarkdownIt, options: Options, text: string, lang: string): string {
const [langToUse, prismLang] = selectLanguage(options, lang)
const code = prismLang ? Prism.highlight(text, prismLang, langToUse) : markdownit.utils.escapeHtml(text)
const classAttribute = langToUse ? ` class="${markdownit.options.langPrefix}${markdownit.utils.escapeHtml(langToUse)}"` : ''
return `<pre${classAttribute}><code${classAttribute}>${code}</code></pre>`
return prismLang ? Prism.highlight(text, prismLang, langToUse) : markdownit.utils.escapeHtml(text)
}

/**
* Construct the class name for the provided `lang`.
*
* @param markdownit
* The markdown-it instance.
* @param lang
* The selected language.
* @return the class to use for `lang`.
*/
function languageClass(markdownit: MarkdownIt, lang: string): string {
return markdownit.options.langPrefix + markdownit.utils.escapeHtml(lang)
}

/**
* Patch the `<pre>` and `<code>` tags produced by the `existingRule` for fenced code blocks.
*
* @param markdownit
* The markdown-it instance.
* @param options
* The options that have been used to initialise the plugin.
* @param existingRule
* The currently configured render rule for fenced code blocks.
*/
function applyCodeAttributes(markdownit: MarkdownIt, options: Options, existingRule: RenderRule): RenderRule {
return (tokens, idx, renderOptions, env, self) => {
const fenceToken = tokens[idx]
const info = fenceToken.info ? markdownit.utils.unescapeAll(fenceToken.info).trim() : ''
const lang = info.split(/(\s+)/g)[0]
const [langToUse] = selectLanguage(options, lang)
if (!langToUse) {
return existingRule(tokens, idx, renderOptions, env, self)
} else {
fenceToken.info = langToUse
const existingResult = existingRule(tokens, idx, renderOptions, env, self)
const langClass = languageClass(markdownit, langToUse)
return existingResult.replace(
/<((?:pre|code)[^>]*?)(?:\s+class="([^"]*)"([^>]*))?>/g,
(match, tagStart, existingClasses?: string, tagEnd?) =>
existingClasses?.includes(langClass) ? match
: `<${tagStart} class="${existingClasses ? `${existingClasses} ` : ''}${langClass}"${tagEnd || ''}>`
)
}
}
}

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

// register ourselves as highlighter
markdownit.options.highlight = (text, lang) => highlight(markdownit, options, text, lang)
markdownit.renderer.rules.fence = applyCodeAttributes(markdownit, options, markdownit.renderer.rules.fence || (() => ''))
}
12 changes: 10 additions & 2 deletions test/test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import markdownit from 'markdown-it'
// @ts-ignore markdown-it-attrs has no types and it’s not worth the effort adding a *.d.ts file
import markdownItAttrs from 'markdown-it-attrs'
import markdownItPrism from '../src'
import fs from 'fs'

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

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

afterEach(() => {
jest.resetModules()
afterEach(() => jest.resetModules())

it('allows to use markdown-it-attrs', async () => {
expect(markdownit()
.use(markdownItPrism)
.use(markdownItAttrs)
.render(await read('input/with-attrs.md'))
).toEqual(await read('expected/fenced-with-attrs.html'))
})

it('allows to use Prism plugins', async () => {
Expand Down
8 changes: 8 additions & 0 deletions testdata/expected/fenced-with-attrs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<h1>Test</h1>
<p>This is a fenced code block:</p>
<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>
<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>
<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>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre>
11 changes: 11 additions & 0 deletions testdata/input/with-attrs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Test

This is a fenced code block:

```java {.classname data-custom=value}
public class Foo() {
public Foo(bar) {
System.out.println(bar);
}
}
```

0 comments on commit 9b2eca1

Please sign in to comment.