Files
hexo-renderer-multi-markdow…/lib/renderer/markdown-it-prism/index.js
amehime dcf26d20a3 fix
2020-10-06 18:27:33 +08:00

296 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const Prism = require('prismjs');
const loadLanguages = require('prismjs/components/');
const pangu = require('pangu');
const LanguagesTip = require('./lang');
const { escapeHTML, unescapeHTML } = require('hexo-util');
const escapeSwigTag = str => str.replace(/{/g, '{').replace(/}/g, '}');
const unescapeSwigTag = str => str.replace(/{/g, '{').replace(/}/g, '}');
loadLanguages.silent = true;
/**
* Initialisation function of the plugin. This function is not called directly by clients, but is rather provided
* to MarkdownIts {@link MarkdownIt.use} function.
*
* @param {MarkdownIt} markdownit
* The markdown it instance the plugin is being registered to.
* @param {MarkdownItPrismOptions} options
* The options this plugin is being initialised with.
*/
module.exports = function (md, options) {
config = {
plugins: ['autolinker', 'show-invisibles', 'normalize-whitespace', 'diff-highlight'], // {String[]} Names of Prism plugins to load
init: () => {}, // Callback for Prism initialisation
defaultLanguageForUnknown: undefined, // The language to use for code blocks that specify a language that Prism does not know
defaultLanguageForUnspecified: undefined, // The language to use for code block that do not specify a language
defaultLanguage: undefined, // Shorthand to set both {@code defaultLanguageForUnknown} and {@code defaultLanguageForUnspecified} to the same value
line_number: true,
...options
};
checkLanguageOption(config, 'defaultLanguage');
checkLanguageOption(config, 'defaultLanguageForUnknown');
checkLanguageOption(config, 'defaultLanguageForUnspecified');
config.defaultLanguageForUnknown = config.defaultLanguageForUnknown || config.defaultLanguage;
config.defaultLanguageForUnspecified = config.defaultLanguageForUnspecified || config.defaultLanguage;
config.plugins.forEach(loadPrismPlugin);
Prism.hooks.add('wrap', function (env) {
if (env.type == 'comment') {
env.content = pangu.spacing(env.content)
}
});
config.init(Prism);
const defaultRenderer = md.renderer.rules.fence.bind(md.renderer.rules)
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
const token = tokens[idx]
const info = token.info
const text = token.content.trim()
const lang = info.trim().split(" ")[0]
var code = null
let [langToUse, langShow, prismLang] = selectLanguage(config, lang);
const {
firstLine = 1,
caption = '',
mark = false,
command = false
} = getOptions(info.slice(lang.length));
if (prismLang) {
code = Prism.highlight(unescapeSwigTag(text), prismLang, langToUse);
} else if(lang == 'raw') {
code = escapeHTML(pangu.spacing(unescapeSwigTag(text)));
langShow = null;
}
if(code) {
code = escapeSwigTag(code);
const lines = code.split('\n');
let content = '';
for (let i = 0, len = lines.length; i < len; i++) {
let line = lines[i];
let append = '';
let lineno = Number(firstLine) + i
if (mark && mark.includes(lineno)) {
content += `<tr class="marked">`;
} else {
content += `<tr>`;
}
content += `<td data-num="${lineno}"></td>`;
if (command) {
content += `<td data-command="${command[lineno]||""}"></td>`;
}
content += `<td><pre>${line}</pre></td></tr>`;
}
let result = `<figure class="highlight${langToUse ? ` ${langToUse}` : ''}">`;
result += `<figcaption data-lang="${langShow ? langShow:''}">${caption}</figcaption>`;
result += `<table>${content}</table></figure>`;
return result;
}
if (lang == 'info') {
return `<pre class="info"><code>${escapeHTML(pangu.spacing(unescapeSwigTag(text)))}</code></pre>`;
} else {
return defaultRenderer(tokens, idx, options, env, self);
}
}
}
/**
* Loads the provided Prism plugin.a
* @param name
* Name of the plugin to load
* @throws {Error} If there is no plugin with the provided {@code name}
*/
function loadPrismPlugin(name) {
try {
require(`prismjs/plugins/${name}/prism-${name}`);
} catch (e) {
throw new Error(`Cannot load Prism plugin "${name}". Please check the spelling.`);
}
}
/**
* Checks whether an option represents a valid Prism language
*
* @param {MarkdownItPrismOptions} options
* The options that have been used to initialise the plugin.
* @param optionName
* The key of the option insides {@code options} that shall be checked.
* @throws {Error} If the option is not set to a valid Prism language.
*/
function checkLanguageOption(options, optionName) {
const language = options[optionName];
if (language !== undefined && loadPrismLang(language) === undefined) {
throw new Error(`Bad option ${optionName}: There is no Prism language '${language}'.`);
}
}
/**
* Select the language to use for highlighting, based on the provided options and the specified language.
*
* @param {Object} options
* The options that were used to initialise the plugin.
* @param {String} lang
* Code of the language to highlight the text in.
* @return {Array} An array where the first element is the name of the language to use, and the second element is the PRISM language object for that language.
*/
function selectLanguage(options, lang) {
let langToUse = lang;
if (langToUse === '' && options.defaultLanguageForUnspecified !== undefined) {
langToUse = options.defaultLanguageForUnspecified;
}
let langShow = LanguagesTip[langToUse] || langToUse;
let prismLang = loadPrismLang(langToUse);
if (prismLang === undefined && options.defaultLanguageForUnknown !== undefined) {
langToUse = options.defaultLanguageForUnknown;
prismLang = loadPrismLang(langToUse);
}
return [langToUse, langShow, prismLang];
}
/**
* Loads the provided {@code lang} into prism.
*
* @param {String} lang
* Code of the language to load.
* @return {Object} The Prism language object for the provided {@code lang} code. {@code undefined} if the language is not known to Prism.
*/
function loadPrismLang(lang) {
if (!lang) return undefined;
let langObject = Prism.languages[lang];
if (langObject === undefined) {
loadLanguages([lang]);
langObject = Prism.languages[lang];
}
return langObject;
}
function getOptions(info) {
const rFirstLine = /\s*first_line:(\d+)/i;
const rMark = /\s*mark:([0-9,-]+)/i;
const rCommand = /\s*command:\((\S[\S\s]*)\)/i;
const rSubCommand = /"+(\S[\S\s]*)"(:([0-9,-]+))?/i;
const rCaptionUrlTitle = /(\S[\S\s]*)\s+(https?:\/\/)(\S+)\s+(.+)/i;
const rCaptionUrl = /(\S[\S\s]*)\s+(https?:\/\/)(\S+)/i;
const rCaption = /(\S[\S\s]*)/;
let first_line = 1;
if (rFirstLine.test(info)) {
info = info.replace(rFirstLine, (match, _first_line) => {
first_line = _first_line;
return '';
});
}
let mark = false;
if (rMark.test(info)) {
mark = [];
info = info.replace(rMark, (match, _mark) => {
mark = _mark.split(',').reduce(
(prev, cur) => lineRange(prev, cur, false), mark);
return '';
})
}
let command = false;
if (rCommand.test(info)) {
command = {}
info = info.replace(rCommand, (match, _command) => {
_command.split('||').forEach((cmd) => {
if (rSubCommand.test(cmd)) {
const match = cmd.match(rSubCommand);
if (match[1]) {
command = match[3].split(',').reduce(
(prev, cur) => lineRange(prev, cur, match[1]), command);
} else {
command[1] = match[1];
}
}
})
return '';
});
}
let caption = '';
if (rCaptionUrlTitle.test(info)) {
const match = info.match(rCaptionUrlTitle);
caption = `<span>${match[1]}</span><a href="${match[2]}${match[3]}">${match[4]}</a>`;
} else if (rCaptionUrl.test(info)) {
const match = info.match(rCaptionUrl);
caption = `<span>${match[1]}</span><a href="${match[2]}${match[3]}">link</a>`;
} else if (rCaption.test(info)) {
const match = info.match(rCaption);
caption = `<span>${match[1]}</span>`;
}
return {
firstLine: first_line,
caption,
mark,
command
};
}
function lineRange(prev, cur, value) {
let prevd = function (key) {
if (value) {
prev[key] = value;
} else {
prev.push(key)
}
}
if (/-/.test(cur)) {
let a = Number(cur.substr(0, cur.indexOf('-')));
let b = Number(cur.substr(cur.indexOf('-') + 1));
if (b < a) { // switch a & b
const temp = a;
a = b;
b = temp;
}
for (; a <= b; a++) {
prevd(a)
// prev[a] = value;
}
return prev;
}
prevd(Number(cur))
return prev;
}
function replaceTabs(str, tab) {
return str.replace(/^\t+/gm, match => {
let result = '';
for (let i = 0, len = match.length; i < len; i++) {
result += tab;
}
return result;
});
}