Skip to content

Commit a394a14

Browse files
authoredOct 5, 2021
Added more plugin tests (#1969)
1 parent e8f84a6 commit a394a14

File tree

11 files changed

+723
-511
lines changed

11 files changed

+723
-511
lines changed
 

‎package-lock.json

+280-431
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
"gzip-size": "^5.1.1",
5858
"htmlparser2": "^4.0.0",
5959
"http-server": "^0.12.3",
60-
"jsdom": "^13.0.0",
60+
"jsdom": "^16.7.0",
6161
"mocha": "^6.2.0",
6262
"node-fetch": "^2.6.0",
6363
"npm-run-all": "^4.1.5",

‎tests/helper/prism-dom-util.js

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const { assert } = require('chai');
2+
const PrismLoader = require('./prism-loader');
3+
4+
/**
5+
* @typedef {import("./prism-loader").PrismDOM} PrismDOM
6+
* @typedef {import("./prism-loader").PrismWindow} PrismWindow
7+
*/
8+
9+
module.exports = {
10+
/**
11+
* @param {PrismWindow} window
12+
*/
13+
createUtil(window) {
14+
const { Prism, document } = window;
15+
16+
const util = {
17+
assert: {
18+
highlight({ language = 'none', code, expected }) {
19+
assert.strictEqual(Prism.highlight(code, Prism.languages[language], language), expected);
20+
},
21+
highlightElement({ language = 'none', code, expected }) {
22+
const element = document.createElement('CODE');
23+
element.classList.add('language-' + language);
24+
element.textContent = code;
25+
26+
Prism.highlightElement(element);
27+
28+
assert.strictEqual(element.innerHTML, expected);
29+
}
30+
},
31+
};
32+
33+
return util;
34+
},
35+
36+
/**
37+
* Creates a Prism DOM instance that will be automatically cleaned up after the given test suite finished.
38+
*
39+
* @param {ReturnType<typeof import('mocha')["suite"]>} suite
40+
* @param {Partial<Record<"languages" | "plugins", string | string[]>>} options
41+
*/
42+
createScopedPrismDom(suite, options = {}) {
43+
const dom = PrismLoader.createPrismDOM();
44+
45+
suite.afterAll(function () {
46+
dom.window.close();
47+
});
48+
49+
if (options.languages) {
50+
dom.loadLanguages(options.languages);
51+
}
52+
if (options.plugins) {
53+
dom.loadPlugins(options.plugins);
54+
}
55+
56+
return dom;
57+
}
58+
};

‎tests/helper/prism-loader.js

+90-8
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,34 @@
11
'use strict';
2-
32
const fs = require('fs');
3+
const { JSDOM } = require('jsdom');
44
const components = require('../../components.json');
55
const getLoader = require('../../dependencies');
6-
const languagesCatalog = components.languages;
76
const coreChecks = require('./checks');
7+
const { languages: languagesCatalog, plugins: pluginsCatalog } = components;
8+
89

10+
/**
11+
* @typedef {import('../../components/prism-core')} Prism
12+
*/
913

1014
/**
1115
* @typedef PrismLoaderContext
12-
* @property {import('../../components/prism-core')} Prism The Prism instance.
16+
* @property {Prism} Prism The Prism instance.
1317
* @property {Set<string>} loaded A set of loaded components.
1418
*/
1519

20+
/**
21+
* @typedef {import("jsdom").DOMWindow & { Prism: Prism }} PrismWindow
22+
*
23+
* @typedef PrismDOM
24+
* @property {JSDOM} dom
25+
* @property {PrismWindow} window
26+
* @property {Document} document
27+
* @property {Prism} Prism
28+
* @property {(languages: string | string[]) => void} loadLanguages
29+
* @property {(plugins: string | string[]) => void} loadPlugins
30+
*/
31+
1632
/** @type {Map<string, string>} */
1733
const fileSourceCache = new Map();
1834
/** @type {() => any} */
@@ -39,6 +55,54 @@ module.exports = {
3955
return context.Prism;
4056
},
4157

58+
/**
59+
* Creates a new JavaScript DOM instance with Prism being loaded.
60+
*
61+
* @returns {PrismDOM}
62+
*/
63+
createPrismDOM() {
64+
const dom = new JSDOM(``, {
65+
runScripts: 'outside-only'
66+
});
67+
const window = dom.window;
68+
69+
window.self = window; // set self for plugins
70+
window.eval(this.loadComponentSource('core'));
71+
72+
/** The set of loaded languages and plugins */
73+
const loaded = new Set();
74+
75+
/**
76+
* Loads the given languages or plugins.
77+
*
78+
* @param {string | string[]} languagesOrPlugins
79+
*/
80+
const load = (languagesOrPlugins) => {
81+
getLoader(components, toArray(languagesOrPlugins), [...loaded]).load(id => {
82+
let source;
83+
if (languagesCatalog[id]) {
84+
source = this.loadComponentSource(id);
85+
} else if (pluginsCatalog[id]) {
86+
source = this.loadPluginSource(id);
87+
} else {
88+
throw new Error(`Language or plugin '${id}' not found.`);
89+
}
90+
91+
window.eval(source);
92+
loaded.add(id);
93+
});
94+
};
95+
96+
return {
97+
dom,
98+
window: /** @type {PrismWindow} */ (window),
99+
document: window.document,
100+
Prism: window.Prism,
101+
loadLanguages: load,
102+
loadPlugins: load,
103+
};
104+
},
105+
42106
/**
43107
* Loads the given languages and appends the config to the given Prism object.
44108
*
@@ -48,11 +112,7 @@ module.exports = {
48112
* @returns {PrismLoaderContext}
49113
*/
50114
loadLanguages(languages, context) {
51-
if (typeof languages === 'string') {
52-
languages = [languages];
53-
}
54-
55-
getLoader(components, languages, [...context.loaded]).load(id => {
115+
getLoader(components, toArray(languages), [...context.loaded]).load(id => {
56116
if (!languagesCatalog[id]) {
57117
throw new Error(`Language '${id}' not found.`);
58118
}
@@ -112,6 +172,17 @@ module.exports = {
112172
return this.loadFileSource(__dirname + '/../../components/prism-' + name + '.js');
113173
},
114174

175+
/**
176+
* Loads the given plugin's file source as string
177+
*
178+
* @private
179+
* @param {string} name
180+
* @returns {string}
181+
*/
182+
loadPluginSource(name) {
183+
return this.loadFileSource(`${__dirname}/../../plugins/${name}/prism-${name}.js`);
184+
},
185+
115186
/**
116187
* Loads the given file source as string
117188
*
@@ -127,3 +198,14 @@ module.exports = {
127198
return content;
128199
}
129200
};
201+
202+
/**
203+
* Wraps the given value in an array if it's not an array already.
204+
*
205+
* @param {T[] | T} value
206+
* @returns {T[]}
207+
* @template T
208+
*/
209+
function toArray(value) {
210+
return Array.isArray(value) ? value : [value];
211+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
const { assert } = require('chai');
2+
const { createScopedPrismDom } = require('../../helper/prism-dom-util');
3+
4+
5+
class DummyClipboard {
6+
constructor() {
7+
this.text = '';
8+
}
9+
async readText() {
10+
return this.text;
11+
}
12+
/** @param {string} data */
13+
writeText(data) {
14+
this.text = data;
15+
return Promise.resolve();
16+
}
17+
}
18+
19+
describe('Copy to Clipboard', function () {
20+
const { Prism, document, window } = createScopedPrismDom(this, {
21+
languages: 'javascript',
22+
plugins: 'copy-to-clipboard',
23+
});
24+
25+
26+
it('should work', function () {
27+
const clipboard = new DummyClipboard();
28+
window.navigator.clipboard = clipboard;
29+
30+
document.body.innerHTML = `<pre class="language-none"><code>foo</code></pre>`;
31+
Prism.highlightAll();
32+
33+
const button = document.querySelector('button');
34+
assert.notStrictEqual(button, null);
35+
36+
button.click();
37+
38+
assert.strictEqual(clipboard.text, 'foo');
39+
});
40+
41+
it('should copy the current text even after the code block changes its text', function () {
42+
const clipboard = new DummyClipboard();
43+
window.navigator.clipboard = clipboard;
44+
45+
document.body.innerHTML = `<pre class="language-none"><code>foo</code></pre>`;
46+
Prism.highlightAll();
47+
48+
const button = document.querySelector('button');
49+
assert.notStrictEqual(button, null);
50+
51+
button.click();
52+
53+
assert.strictEqual(clipboard.text, 'foo');
54+
55+
// change text
56+
document.querySelector('code').textContent = 'bar';
57+
// and click
58+
button.click();
59+
60+
assert.strictEqual(clipboard.text, 'bar');
61+
62+
// change text
63+
document.querySelector('code').textContent = 'baz';
64+
Prism.highlightAll();
65+
// and click
66+
button.click();
67+
68+
assert.strictEqual(clipboard.text, 'baz');
69+
});
70+
71+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
const { createUtil, createScopedPrismDom } = require('../../helper/prism-dom-util');
2+
3+
4+
describe('Custom class', function () {
5+
const { Prism, window } = createScopedPrismDom(this, {
6+
languages: 'javascript',
7+
plugins: 'custom-class'
8+
});
9+
const util = createUtil(window);
10+
11+
12+
it('should set prefix', function () {
13+
Prism.plugins.customClass.prefix('prism-');
14+
15+
util.assert.highlight({
16+
language: 'javascript',
17+
code: `var a = true;`,
18+
expected: `<span class="prism-token prism-keyword">var</span> a <span class="prism-token prism-operator">=</span> <span class="prism-token prism-boolean">true</span><span class="prism-token prism-punctuation">;</span>`
19+
});
20+
});
21+
22+
it('should reset prefix', function () {
23+
Prism.plugins.customClass.prefix('');
24+
25+
util.assert.highlight({
26+
language: 'javascript',
27+
code: `var a = true;`,
28+
expected: `<span class="token keyword">var</span> a <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span>`
29+
});
30+
});
31+
32+
it('should map class names using a function', function () {
33+
Prism.plugins.customClass.map(function (cls, language) {
34+
return `${language}-${cls}`;
35+
});
36+
37+
util.assert.highlight({
38+
language: 'javascript',
39+
code: `var a = true;`,
40+
expected: `<span class="javascript-token javascript-keyword">var</span> a <span class="javascript-token javascript-operator">=</span> <span class="javascript-token javascript-boolean">true</span><span class="javascript-token javascript-punctuation">;</span>`
41+
});
42+
});
43+
44+
it('should map class names using an object', function () {
45+
Prism.plugins.customClass.map({
46+
boolean: 'b',
47+
keyword: 'kw',
48+
operator: 'op',
49+
punctuation: 'p'
50+
});
51+
52+
util.assert.highlight({
53+
language: 'javascript',
54+
code: `var a = true;`,
55+
expected: `<span class="token kw">var</span> a <span class="token op">=</span> <span class="token b">true</span><span class="token p">;</span>`
56+
});
57+
});
58+
59+
it('should reset map', function () {
60+
Prism.plugins.customClass.map({});
61+
62+
util.assert.highlight({
63+
language: 'javascript',
64+
code: `var a = true;`,
65+
expected: `<span class="token keyword">var</span> a <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span>`
66+
});
67+
});
68+
69+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const { createUtil, createScopedPrismDom } = require('../../helper/prism-dom-util');
2+
3+
4+
describe('Highlight Keywords', function () {
5+
const { window } = createScopedPrismDom(this, {
6+
languages: 'javascript',
7+
plugins: 'highlight-keywords'
8+
});
9+
const util = createUtil(window);
10+
11+
12+
it('should highlight keywords', function () {
13+
util.assert.highlightElement({
14+
language: 'javascript',
15+
code: `import * from ''; const foo;`,
16+
expected: `<span class="token keyword keyword-import">import</span> <span class="token operator">*</span> <span class="token keyword keyword-from">from</span> <span class="token string">''</span><span class="token punctuation">;</span> <span class="token keyword keyword-const">const</span> foo<span class="token punctuation">;</span>`
17+
});
18+
});
19+
20+
});

‎tests/plugins/keep-markup/test.js

+34-71
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,52 @@
1-
/* eslint-disable no-undef */
2-
const expect = require('chai').expect;
3-
const jsdom = require('jsdom');
4-
const { JSDOM } = jsdom;
1+
const { assert } = require('chai');
2+
const { createScopedPrismDom } = require('../../helper/prism-dom-util');
53

6-
require('../../../prism');
7-
// fake DOM
8-
global.self = {};
9-
global.self.Prism = Prism;
10-
global.document = {};
11-
document.createRange = function () {
12-
};
13-
global.self.document = document;
144

15-
require('../../../plugins/keep-markup/prism-keep-markup');
5+
describe('Keep Markup', function () {
6+
const { Prism, document } = createScopedPrismDom(this, {
7+
plugins: 'keep-markup'
8+
});
9+
1610

17-
describe('Prism Keep Markup Plugin', function () {
11+
/**
12+
* @param {string} html
13+
* @param {string} language
14+
*/
15+
function highlightInElement(html, language = 'none') {
16+
const pre = document.createElement('pre');
17+
pre.className = `language-${language}`;
18+
pre.innerHTML = `<code>${html}</code>`;
1819

19-
function execute(code) {
20-
const start = [];
21-
const end = [];
22-
const nodes = [];
23-
document.createRange = function () {
24-
return {
25-
setStart: function (node, offset) {
26-
start.push({ node, offset });
27-
},
28-
setEnd: function (node, offset) {
29-
end.push({ node, offset });
30-
},
31-
extractContents: function () {
32-
return new JSDOM('').window.document.createTextNode('');
33-
},
34-
insertNode: function (node) {
35-
nodes.push(node);
36-
},
37-
detach: function () {
38-
}
39-
};
40-
};
41-
const beforeHighlight = Prism.hooks.all['before-highlight'][0];
42-
const afterHighlight = Prism.hooks.all['after-highlight'][0];
43-
const env = {
44-
element: new JSDOM(code).window.document.getElementsByTagName('code')[0],
45-
language: 'javascript'
46-
};
47-
beforeHighlight(env);
48-
afterHighlight(env);
49-
return { start, end, nodes };
20+
Prism.highlightElement(pre);
21+
22+
return pre.querySelector('code').innerHTML;
23+
}
24+
25+
/**
26+
* @param {string} html
27+
* @param {string} language
28+
*/
29+
function keepMarkup(html, language = 'none') {
30+
assert.equal(highlightInElement(html, language), html);
5031
}
5132

5233
it('should keep <span> markup', function () {
53-
const result = execute(`<code class="language-javascript">x<span>a</span>y</code>`);
54-
expect(result.start.length).to.equal(1);
55-
expect(result.end.length).to.equal(1);
56-
expect(result.nodes.length).to.equal(1);
57-
expect(result.nodes[0].nodeName).to.equal('SPAN');
34+
keepMarkup(`x<span>a</span>y`);
5835
});
5936
it('should preserve markup order', function () {
60-
const result = execute(`<code class="language-javascript">x<a></a><b></b>y</code>`);
61-
expect(result.start.length).to.equal(2);
62-
expect(result.start[0].offset).to.equal(0);
63-
expect(result.start[0].node.textContent).to.equal('y');
64-
expect(result.start[1].offset).to.equal(0);
65-
expect(result.start[1].node.textContent).to.equal('y');
66-
expect(result.end.length).to.equal(2);
67-
expect(result.end[0].offset).to.equal(0);
68-
expect(result.end[0].node.textContent).to.equal('y');
69-
expect(result.end[1].offset).to.equal(0);
70-
expect(result.end[1].node.textContent).to.equal('y');
71-
expect(result.nodes.length).to.equal(2);
72-
expect(result.nodes[0].nodeName).to.equal('A');
73-
expect(result.nodes[1].nodeName).to.equal('B');
37+
keepMarkup(`x<a></a><b></b>y`);
7438
});
75-
it('should keep last <span> markup', function () {
76-
const result = execute(`<code class="language-javascript">xy<span>a</span></code>`);
77-
expect(result.start.length).to.equal(1);
78-
expect(result.end.length).to.equal(1);
79-
expect(result.nodes.length).to.equal(1);
80-
expect(result.nodes[0].nodeName).to.equal('SPAN');
39+
40+
it('should keep last markup', function () {
41+
keepMarkup(`xy<span>a</span>`);
42+
keepMarkup(`xy<a>a</a>`);
8143
});
44+
8245
// The markup is removed if it's the last element and the element's name is a single letter: a(nchor), b(old), i(talic)...
8346
// https://github.com/PrismJS/prism/issues/1618
8447
/*
8548
it('should keep last single letter empty markup', function () {
86-
const result = execute(`<code class="language-javascript">xy<a></a></code>`)
49+
const result = execute(`<code class="language-none">xy<a></a></code>`)
8750
expect(result.start.length).to.equal(1)
8851
expect(result.end.length).to.equal(1)
8952
expect(result.nodes.length).to.equal(1)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const { createUtil, createScopedPrismDom } = require('../../helper/prism-dom-util');
2+
3+
4+
describe('Show Invisibles', function () {
5+
const { window } = createScopedPrismDom(this, {
6+
languages: 'javascript',
7+
plugins: 'show-invisibles'
8+
});
9+
const util = createUtil(window);
10+
11+
12+
it('should show invisible characters', function () {
13+
util.assert.highlightElement({
14+
language: 'javascript',
15+
code: ` \t\n\r\n\t\t`,
16+
expected: `<span class="token space"> </span><span class="token space"> </span><span class="token tab">\t</span><span class="token lf">\n</span><span class="token crlf">\n</span><span class="token tab">\t</span><span class="token tab">\t</span>`
17+
});
18+
});
19+
20+
it('should show invisible characters inside tokens', function () {
21+
util.assert.highlightElement({
22+
language: 'javascript',
23+
code: `/* \n */`,
24+
expected: `<span class="token comment">/*<span class="token space"> </span><span class="token lf">\n</span><span class="token space"> </span>*/</span>`
25+
});
26+
});
27+
28+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const { assert } = require('chai');
2+
const { createScopedPrismDom } = require('../../helper/prism-dom-util');
3+
4+
5+
describe('Show language', function () {
6+
const { Prism, document } = createScopedPrismDom(this, {
7+
languages: ['markup', 'javascript'],
8+
plugins: 'show-language'
9+
});
10+
11+
12+
function test(expectedLanguage, code) {
13+
document.body.innerHTML = code;
14+
Prism.highlightAll();
15+
16+
assert.strictEqual(document.querySelector('.toolbar-item > span').textContent, expectedLanguage);
17+
}
18+
19+
it('should work with component titles', function () {
20+
// simple title
21+
test('JavaScript', `<pre class="language-javascript"><code>foo</code></pre>`);
22+
test('Markup', `<pre class="language-markup"><code>foo</code></pre>`);
23+
24+
// aliases with the same title
25+
test('JavaScript', `<pre class="language-js"><code>foo</code></pre>`);
26+
27+
// aliases with a different title
28+
test('HTML', `<pre class="language-html"><code>foo</code></pre>`);
29+
test('SVG', `<pre class="language-svg"><code>foo</code></pre>`);
30+
});
31+
32+
it('should work with custom titles', function () {
33+
test('Foo', `<pre class="language-javascript" data-language="Foo"><code>foo</code></pre>`);
34+
});
35+
36+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const { assert } = require('chai');
2+
const { createScopedPrismDom } = require('../../helper/prism-dom-util');
3+
4+
5+
describe('Show language', function () {
6+
const { Prism, document } = createScopedPrismDom(this, {
7+
languages: 'markup',
8+
plugins: 'unescaped-markup'
9+
});
10+
11+
12+
function test(expectedText, code) {
13+
document.body.innerHTML = code;
14+
Prism.highlightAll();
15+
16+
assert.strictEqual(document.querySelector('code').textContent, expectedText);
17+
}
18+
19+
it('should work with comments', function () {
20+
test('\n<p>Example</p>\n', `<pre class="language-javascript"><code><!--
21+
<p>Example</p>
22+
--></code></pre>`);
23+
24+
test('\n<p>Example 2</p>\n', `<pre><code class="language-javascript"><!--
25+
<p>Example 2</p>
26+
--></code></pre>`);
27+
});
28+
29+
it('should work with script tags', function () {
30+
test('<p>Example</p>', `<script class="language-javascript" type="text/plain"><p>Example</p></script>`);
31+
32+
// inherit language
33+
test('<p>Example 2</p>', `<div class="language-javascript"><script type="text/plain"><p>Example 2</p></script></div>`);
34+
});
35+
36+
});

0 commit comments

Comments
 (0)
Please sign in to comment.