@digitalocean/do-markdownit
Version:
Markdown-It plugin for the DigitalOcean Community.
109 lines (91 loc) • 3.33 kB
JavaScript
/*
Copyright 2022 DigitalOcean
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
;
/**
* @module modifiers/fence_classes
*/
const safeObject = require('../util/safe_object');
const findTagOpen = require('../util/find_tag_open');
const findAttr = require('../util/find_attr');
/**
* @typedef {Object} FenceClassesOptions
* @property {string[]} [allowedClasses] List of case-sensitive classes that are allowed. If not an array, all classes are allowed.
*/
/**
* Filters classes on code and pre tags in fences.
*
* @example
* ```test
* hello
* world
* ```
*
* ```bad
* hello
* world
* ```
*
* <pre><code class="language-test">hello
* world
* </code></pre>
*
* <pre><code class="">hello
* world
* </code></pre>
*
* @type {import('markdown-it').PluginWithOptions<FenceClassesOptions>}
*/
module.exports = (md, options) => {
// Get the correct options
const optsObj = safeObject(options);
/**
* Filter classes for a given HTML tag in HTML content.
*
* @param {string} tagName Name of the HTML tag to filter classes for.
* @param {string} content Full HTML snippet in which the HTML tag is located.
* @returns {string}
* @private
*/
const filterTag = (tagName, content) => {
// Locate the tag
const tagPos = findTagOpen(tagName, content);
if (!tagPos) return content;
// Locate the class attribute
const tag = content.slice(tagPos.start, tagPos.end);
const classPos = findAttr('class', tag);
if (!classPos) return content;
// Extract the class attribute
const classes = tag.slice(classPos.start + 7, classPos.end - 1).split(' ');
const permitted = classes.filter(cls => optsObj.allowedClasses.includes(cls));
// Generate the new tag
const newTag = `${tag.slice(0, classPos.start + 7)}${permitted.join(' ')}${tag.slice(classPos.end - 1)}`;
// Return the content with the new tag
return `${content.slice(0, tagPos.start)}${newTag}${content.slice(tagPos.end)}`;
};
/**
* Wrap the fence render function to filter classes on pre and class tags.
*
* @param {import('markdown-it/lib/renderer').RenderRule} original Original render function to wrap.
* @returns {import('markdown-it/lib/renderer').RenderRule}
* @private
*/
const render = original => (tokens, idx, opts, env, self) => {
// Get the rendered content
const content = original(tokens, idx, opts, env, self);
// If no permitted classes, return the original content
if (!Array.isArray(optsObj.allowedClasses)) return content;
// Filter the pre and code tags if present
return filterTag('code', filterTag('pre', content));
};
md.renderer.rules.fence = render(md.renderer.rules.fence);
};