html-bundler-webpack-plugin
Version:
Generates complete single-page or multi-page website from source assets. Built-in support for Markdown, Eta, EJS, Handlebars, Nunjucks, Pug. Alternative to html-webpack-plugin.
140 lines (119 loc) • 3.54 kB
JavaScript
const { loadModule } = require('../../../Common/FileUtils');
const parse5 = loadModule('parse5');
const parser = {
entries: {},
entryIndex: 0,
langPrefix: '',
/**
* @param {string} prefix
* @public
* @api
*/
init({ langPrefix }) {
this.langPrefix = langPrefix;
},
/**
* @param {string} text
* @param {string} language
* @returns {string}
* @abstract The abstract method must be overridden with your implementation.
* @public
* @api
*/
highlight(text, language) {
throw new Error('You have to implement the abstract method highlight().');
},
/**
* @param {string} text
* @returns {string}
* @public
* @api
*/
highlightAll(text) {
const document = parse5.parseFragment(text);
this.entryIndex = 0;
this.entries = {};
// autodetect code in document and save highlighted code in entries
this.walk(document);
// text contains marked placeholders that are replaced with highlighted code
text = parse5.serialize(document);
for (let marker in this.entries) {
const { lang, value } = this.entries[marker];
text = text.replace(marker, this.highlight(value, lang));
}
return text;
},
/**
* @param {{name: string, value: string}[]} attrs The node attributes.
* @returns {string}
* @private
*/
getLanguage(attrs) {
let lang = '';
if (attrs != null) {
const attr = attrs.find(
(item) => item.name === 'class' && (item.value.startsWith(this.langPrefix) || item.value.startsWith('lang-'))
);
if (attr) {
lang = attr.value.replace(this.langPrefix, '').replace('lang-', '');
}
}
return lang;
},
/**
* @param {{attrs: {name: string, value: string}[]}} node The node
* @param {string} lang
* @private
*/
setLanguage(node, lang) {
const attrClass = node.attrs.find((item) => item.name === 'class');
const className = this.langPrefix + lang;
if (attrClass) {
let classes = attrClass.value.split(' ').filter((item) => !item.startsWith(this.langPrefix));
classes.push(className);
attrClass.value = classes.join(' ');
} else {
node.attrs.push({ name: 'class', value: className });
}
},
/**
* Traverse parsed HTML nodes and detect a code block with language.
*
* @param {{} | []} obj A traversable object.
* @private
*/
walk(obj) {
for (let key in obj) {
const node = obj[key];
if (node.childNodes != null) {
this.walk(node.childNodes);
} else if (Array.isArray(node)) {
this.walk(node);
}
if (node.nodeName === '#text') {
let parentNode = node.parentNode;
if (parentNode != null && parentNode.nodeName === 'code') {
// match language in class name of `code` or `pre` tags
let language = this.getLanguage(parentNode.attrs);
parentNode = parentNode.parentNode;
if (parentNode != null && parentNode.nodeName === 'pre') {
if (!language) {
language = this.getLanguage(parentNode.attrs);
}
// set lang prefix as class name in <pre> tag
this.setLanguage(parentNode, language);
}
if (language) {
let marker = `__HL_MARKER_${this.entryIndex++}__`;
this.entries[marker] = {
lang: language,
value: node.value,
};
node.value = marker;
}
}
}
}
},
};
module.exports = parser;