Skip to content

Commit

Permalink
Allow logging for Prism to be toggled (#75)
Browse files Browse the repository at this point in the history
* Allow logging for Prism to be toggled

* Fix mocking in tests, avoid cross-contamination

* Make eslint happy
  • Loading branch information
MattIPv4 committed May 12, 2023
1 parent 5895e28 commit 5515d0c
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ See `PUBLISH.md` for instructions on how to publish a new version.
-->


- (minor) Allow logging for Prism to be toggled
- (patch) Manually track loaded Prism components

# v1.8.0 - aaf8532
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,7 @@ Pass options for this plugin as the `prismjs` property of the `do-markdownit` pl
Set this property to `false` to disable this plugin.

- `delimiter` (`string`, optional, defaults to `','`): String to split fence information on.
- `logging` (`boolean`, optional, defaults to `false`): Whether to log errors to the console.
</details>

### limit_tokens
Expand Down
12 changes: 7 additions & 5 deletions modifiers/prismjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const { languages, languageAliases, getDependencies } = require('../util/prism_u
/**
* @typedef {Object} PrismJsOptions
* @property {string} [delimiter=','] String to split fence information on.
* @property {boolean} [logging=false] Whether to log errors to the console.
*/

/**
Expand All @@ -43,16 +44,17 @@ const loaded = new Set();
* Helper to load in a Prism component if not yet loaded.
*
* @param {string} component Prism component name to be loaded.
* @param {boolean} logging Whether to log errors to the console.
* @private
*/
const loadComponent = component => {
const loadComponent = (component, logging) => {
if (loaded.has(component)) return;
try {
// eslint-disable-next-line import/no-dynamic-require
require(`../vendor/prismjs/components/prism-${component}`)(Prism);
loaded.add(component);
} catch (err) {
console.error('Failed to load Prism component', component, err);
if (logging) console.error('Failed to load Prism component', component, err);
}
};

Expand Down Expand Up @@ -171,8 +173,8 @@ module.exports = (md, options) => {
const { before, inside, after } = extractCodeBlock(rendered, language);

// Load requirements for language
getDependencies(language.clean).forEach(loadComponent);
loadComponent(language.clean);
getDependencies(language.clean).forEach(dep => loadComponent(dep, !!optsObj.logging));
loadComponent(language.clean, !!optsObj.logging);

// If we failed to load the language (it's a dynamic require), return original
if (!(language.clean in Prism.languages)) return rendered;
Expand All @@ -184,7 +186,7 @@ module.exports = (md, options) => {
return `${before}${highlighted}${after}`;
} catch (err) {
// Fallback to no Prism if render fails
console.error('Bad Prism render occurred', err);
if (optsObj.logging) console.error('Bad Prism render occurred', err);
return rendered;
}
};
Expand Down
85 changes: 79 additions & 6 deletions modifiers/prismjs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ limitations under the License.

'use strict';

const md = require('markdown-it')().use(require('./prismjs'));
const md = require('markdown-it')().use(require('./prismjs'), { logging: true });

it('handles a code fence with a language (adding the class to the pre + highlighting)', () => {
expect(md.render('```nginx\nserver {\n try_files test =404;\n}\n```')).toBe(`<pre class="language-nginx"><code class="language-nginx"><span class="token directive"><span class="token keyword">server</span></span> <span class="token punctuation">{</span>
Expand All @@ -33,11 +33,26 @@ it('handles a code fence with a language alias', () => {
});

it('does not repeatedly load a modifier component', () => {
const error = jest.spyOn(global.console, 'error');
md.render('```ts\nconsole.log("test");\n```\n\n```ts\nconsole.log("test");\n```');
expect(error).not.toHaveBeenCalledWith(expect.stringContaining('Failed to load Prism component'), 'js-templates', expect.anything());
error.mockReset();
error.mockRestore();
// Reset modules in case the modifier was already loaded
jest.resetModules();

// Mock the modifier to track how many times it is called
const callCount = jest.fn();
jest.doMock('../vendor/prismjs/components/prism-js-templates', () => {
const actual = jest.requireActual('../vendor/prismjs/components/prism-js-templates');
return (...args) => {
callCount();
return actual(...args);
};
});

// Process some Markdown that uses a language with the modifier twice
const mdTemp = require('markdown-it')().use(require('./prismjs'));
mdTemp.render('```ts\nconsole.log("test");\n```\n\n```ts\nconsole.log("test");\n```');
expect(callCount).toHaveBeenCalledTimes(1);

// Clean up the mock
jest.dontMock('../vendor/prismjs/components/prism-js-templates');
});

it('does not pollute global scope', () => {
Expand All @@ -51,6 +66,64 @@ it('does not pollute global scope', () => {
expect(global.Prism).toBeUndefined();
});

describe('Error logging', () => {
it('does log errors when logging is enabled', () => {
// Reset modules in case the language was already loaded
jest.resetModules();

// Mock the language to track how many times it is called, and to throw an error
const callCount = jest.fn();
jest.doMock('../vendor/prismjs/components/prism-typescript', () => () => {
callCount();
throw new Error('test');
});

// Spy on console.error to check that it is called
const error = jest.spyOn(global.console, 'error');

// Process some Markdown that uses the language
const mdTemp = require('markdown-it')().use(require('./prismjs'), { logging: true });
mdTemp.render('```ts\nconsole.log("test");\n```');

// Check the mock was called and the error was logged
expect(callCount).toHaveBeenCalledTimes(1);
expect(error).toHaveBeenCalledWith('Failed to load Prism component', 'typescript', expect.any(Error));

// Clean up the mocks
error.mockReset();
error.mockRestore();
jest.dontMock('../vendor/prismjs/components/prism-typescript');
});

it('does not log errors when logging is disabled', () => {
// Reset modules in case the language was already loaded
jest.resetModules();

// Mock the component to track how many times it is called, and to throw an error
const callCount = jest.fn();
jest.doMock('../vendor/prismjs/components/prism-typescript', () => () => {
callCount();
throw new Error('test');
});

// Spy on console.error to check that it is called
const error = jest.spyOn(global.console, 'error');

// Process some Markdown that uses the language
const mdTemp = require('markdown-it')().use(require('./prismjs'));
mdTemp.render('```ts\nconsole.log("test");\n```');

// Check the mock was called and the error was logged
expect(callCount).toHaveBeenCalledTimes(1);
expect(error).not.toHaveBeenCalled();

// Clean up the mocks
error.mockReset();
error.mockRestore();
jest.dontMock('../vendor/prismjs/components/prism-typescript');
});
});

describe('HTML preservation', () => {
// This does pollute the tests, no longer isolated, but is needed to inject HTML into the code blocks
const mdHtml = require('markdown-it')()
Expand Down

0 comments on commit 5515d0c

Please sign in to comment.