'use strict';
const hljs = require('highlight.js');
const stripIndent = require('strip-indent');
const alias = require('../highlight_alias.json');
function highlightUtil(str, options = {}) {
if (typeof str !== 'string') throw new TypeError('str must be a string!');
str = stripIndent(str);
const useHljs = Object.prototype.hasOwnProperty.call(options, 'hljs') ? options.hljs : false;
const {
gutter = true,
firstLine = 1,
caption,
mark = [],
languageAttr = false,
tab
} = options;
let { wrap = true } = options;
hljs.configure({ classPrefix: useHljs ? 'hljs-' : ''});
const data = highlight(str, options);
const lang = options.lang || data.language || '';
const classNames = (useHljs ? 'hljs' : 'highlight') + (lang ? ` ${lang}` : '');
if (gutter && !wrap) wrap = true; // arbitrate conflict ("gutter:true" takes priority over "wrap:false")
const before = useHljs ? `
` : '
';
const after = useHljs ? '
' : '';
const lines = data.value.split('\n');
let numbers = '';
let content = '';
for (let i = 0, len = lines.length; i < len; i++) {
let line = lines[i];
if (tab) line = replaceTabs(line, tab);
numbers += `${Number(firstLine) + i} `;
content += formatLine(line, Number(firstLine) + i, mark, options, wrap);
}
let codeCaption = '';
if (caption) {
codeCaption = wrap ? `${caption}` : `
${caption}
`;
}
if (!wrap) {
// if original content has one trailing newline, replace it only once, else remove all trailing newlines
content = /\r?\n$/.test(data.value) ? content.replace(/\n$/, '') : content.trimEnd();
return `
${codeCaption}${content}
`;
}
let result = `
`;
result += codeCaption;
result += '
';
if (gutter) {
result += `
${numbers}
`;
}
result += `
${before}${content}${after}
`;
result += '
';
return result;
}
function formatLine(line, lineno, marked, options, wrap) {
const useHljs = (options.hljs || false) || !wrap;
const br = wrap ? ' ' : '\n';
let res = useHljs ? '' : '${line}`;
} else {
res += useHljs ? line : `">${line}`;
}
res += br;
return res;
}
function replaceTabs(str, tab) {
return str.replace(/\t+/, match => tab.repeat(match.length));
}
function highlight(str, options) {
let { lang } = options;
const { autoDetect = false } = options;
if (lang) {
lang = lang.toLowerCase();
} else if (autoDetect) {
const result = hljs.highlightAuto(str);
return closeTags(result);
}
if (!lang || !alias.aliases[lang]) {
lang = 'plaintext';
}
const res = hljs.highlight(str, {
language: lang,
ignoreIllegals: true
});
return closeTags(res);
}
// https://github.com/hexojs/hexo-util/issues/10
function closeTags(res) {
const tokenStack = [];
res.value = res.value.split('\n').map(line => {
const prepend = tokenStack.map(token => ``).join('');
const matches = line.matchAll(/(|<\/span>)/g);
for (const match of matches) {
if (match[0] === '') tokenStack.shift();
else tokenStack.unshift(match[2]);
}
const append = ''.repeat(tokenStack.length);
return prepend + line + append;
}).join('\n');
return res;
}
module.exports = highlightUtil;