Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add prism highlight support #4119

Merged
merged 10 commits into from
Jun 8, 2020
6 changes: 6 additions & 0 deletions lib/hexo/default_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ module.exports = {
wrap: true,
hljs: false
},
prismjs: {
enable: false,
preprocess: true,
line_number: true,
tab_replace: ''
},
// Category & Tag
default_category: 'uncategorized',
category_map: {},
Expand Down
90 changes: 59 additions & 31 deletions lib/plugins/filter/before_post_render/backtick_code_block.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,38 @@
'use strict';

const { highlight } = require('hexo-util');
let highlight, prismHighlight;

const rBacktick = /^((?:[^\S\r\n]*>){0,3}[^\S\r\n]*)(`{3,}|~{3,})[^\S\r\n]*((?:.*?[^`\s])?)[^\S\r\n]*\n((?:[\s\S]*?\n)?)(?:(?:[^\S\r\n]*>){0,3}[^\S\r\n]*)\2(\n+|$)/gm;
const rAllOptions = /([^\s]+)\s+(.+?)\s+(https?:\/\/\S+|\/\S+)\s*(.+)?/;
const rLangCaption = /([^\s]+)\s*(.+)?/;

const escapeSwigTag = str => str.replace(/{/g, '{').replace(/}/g, '}');

function backtickCodeBlock(data) {
const config = this.config.highlight || {};
if (!config.enable) return;
const hljsCfg = this.config.highlight || {};
const prismCfg = this.config.prismjs || {};

data.content = data.content.replace(rBacktick, ($0, start, $2, _args, _content, end) => {
let content = _content.replace(/\n$/, '');
const args = _args.split('=').shift();

const options = {
hljs: config.hljs,
autoDetect: config.auto_detect,
gutter: config.line_number,
tab: config.tab_replace,
wrap: config.wrap
};

if (options.gutter) {
config.first_line_number = config.first_line_number || 'always1';
if (config.first_line_number === 'inline') {
// neither highlight or prismjs is enabled, return escaped content directly.
if (!hljsCfg.enable && !prismCfg.enable) return escapeSwigTag($0);

// setup line number by inline
_args = _args.replace('=+', '=');
options.gutter = _args.includes('=');

// setup fiestLineNumber;
options.firstLine = options.gutter ? _args.split('=')[1] || 1 : 0;
}
}
// Extrace langauge and caption of code blocks
const args = _args.split('=').shift();
let lang, caption;

if (args) {
const match = rAllOptions.exec(args) || rLangCaption.exec(args);


if (match) {
options.lang = match[1];
lang = match[1];

if (match[2]) {
options.caption = `<span>${match[2]}</span>`;
caption = `<span>${match[2]}</span>`;

if (match[3]) {
options.caption += `<a href="${match[3]}">${match[4] ? match[4] : 'link'}</a>`;
caption += `<a href="${match[3]}">${match[4] ? match[4] : 'link'}</a>`;
}
}
}
Expand All @@ -59,11 +46,52 @@ function backtickCodeBlock(data) {
content = content.replace(regexp, '');
}

content = highlight(content, options)
.replace(/{/g, '&#123;')
.replace(/}/g, '&#125;');
// Since prismjs have better performance, so prismjs should have higher priority.
if (prismCfg.enable) {
if (!prismHighlight) prismHighlight = require('hexo-util').prismHighlight;

const options = {
lineNumber: prismCfg.line_number,
tab: prismCfg.tab_replace,
isPreprocess: prismCfg.preprocess,
lang
};

content = prismHighlight(content, options);
} else if (hljsCfg.enable) {
if (!highlight) highlight = require('hexo-util').highlight;

const options = {
hljs: hljsCfg.hljs,
autoDetect: hljsCfg.auto_detect,
gutter: hljsCfg.line_number,
tab: hljsCfg.tab_replace,
wrap: hljsCfg.wrap,
lang,
caption
};

if (options.gutter) {
hljsCfg.first_line_number = hljsCfg.first_line_number || 'always1';
if (hljsCfg.first_line_number === 'inline') {

// setup line number by inline
_args = _args.replace('=+', '=');
options.gutter = _args.includes('=');

// setup firstLineNumber;
options.firstLine = options.gutter ? _args.split('=')[1] || 1 : 0;
}
}

content = highlight(content, options);
}

return `${start}<!--hexoPostRenderEscape:${content}:hexoPostRenderEscape-->${end}`;
return start
+ '<!--hexoPostRenderEscape:'
+ escapeSwigTag(content)
+ ':hexoPostRenderEscape-->'
+ end;
});
}

Expand Down
70 changes: 47 additions & 23 deletions lib/plugins/tag/code.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

// Based on: https://raw.github.com/imathis/octopress/master/plugins/code_block.rb

const { escapeHTML, highlight } = require('hexo-util');
const { escapeHTML, highlight, prismHighlight } = require('hexo-util');

const rCaptionUrlTitle = /(\S[\S\s]*)\s+(https?:\/\/\S+)\s+(.+)/i;
const rCaptionUrl = /(\S[\S\s]*)\s+(https?:\/\/\S+)/i;
Expand All @@ -25,13 +25,13 @@ const rCaption = /\S[\S\s]*/;
* Example: `mark:1,4-7,10` will mark line 1, 4 to 7 and 10.
* @param {Object} wrap Wrap the code block in <table>, value must be a boolean
* @returns {String} Code snippet with code highlighting
*/
*/

function getHighlightOptions(config, args) {
function parseArgs(args) {
const _else = [];
const len = args.length;
let lang = '';
let { line_number, wrap } = config;
let lang,
line_number, wrap;
let firstLine = 1;
const mark = [];
for (let i = 0; i < len; i++) {
Expand Down Expand Up @@ -86,8 +86,9 @@ function getHighlightOptions(config, args) {
}

const arg = _else.join(' ');
let caption = '';
let match;
// eslint-disable-next-line one-var
let match, caption = '';

if ((match = arg.match(rCaptionUrlTitle)) != null) {
caption = `<span>${match[1]}</span><a href="${match[2]}">${match[3]}</a>`;
} else if ((match = arg.match(rCaptionUrl)) != null) {
Expand All @@ -100,37 +101,60 @@ function getHighlightOptions(config, args) {
lang,
firstLine,
caption,
gutter: line_number,
hljs: config.hljs,
line_number,
mark,
tab: config.tab_replace,
autoDetect: config.auto_detect,
wrap
};
}

module.exports = ctx => function codeTag(args, content) {
const config = ctx.config.highlight || {};
let { enable } = config;
const hljsCfg = ctx.config.highlight || {};
const prismjsCfg = ctx.config.prismjs || {};

let index;
let enableHighlight = true;

if ((index = args.findIndex(item => item.startsWith('highlight:'))) !== -1) {
const arg = args[index];
const _enable = arg.slice(10);
enable = _enable === 'true';
const highlightStr = arg.slice(10);
enableHighlight = highlightStr === 'true';
args.splice(index, 1);
}

if (!enable) {
content = escapeHTML(content);
return `<pre><code>${content}</code></pre>`;
// If 'hilight: false' is given, return escaped code directly
// If neither highlight.js nor prism.js is enabled, return escaped code directly
if (!enableHighlight || (!hljsCfg.enable && !prismjsCfg.enable)) {
return `<pre><code>${escapeHTML(content)}</code></pre>`;
}

content = highlight(content, getHighlightOptions(config, args));

content = content.replace(/{/g, '&#123;')
.replace(/}/g, '&#125;');
const { lang, firstLine, caption, line_number, mark, wrap } = parseArgs(args);

if (prismjsCfg.enable) {
const prismjsOption = {
lang,
firstLine,
lineNumber: typeof line_number !== 'undefined' ? line_number : prismjsCfg.line_number,
mark,
tab: prismjsCfg.tab_replace,
isPreprocess: prismjsCfg.preprocess
};

content = prismHighlight(content, prismjsOption);
} else {
const hljsOption = {
lang: typeof lang !== 'undefined' ? lang : '',
firstLine,
caption,
gutter: typeof line_number !== 'undefined' ? line_number : hljsCfg.line_number,
hljs: hljsCfg.hljs,
mark,
tab: hljsCfg.tab_replace,
autoDetect: hljsCfg.auto_detect,
wrap: typeof wrap === 'undefined' ? wrap : hljsCfg.wrap
};

content = highlight(content, hljsOption);
}

return content;
return content.replace(/{/g, '&#123;').replace(/}/g, '&#125;');
};
34 changes: 22 additions & 12 deletions lib/plugins/tag/include_code.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const fs = require('hexo-fs');
const { basename, extname, join } = require('path');
const { highlight } = require('hexo-util');
const { highlight, prismHighlight } = require('hexo-util');

const rCaptionTitleFile = /(.*)?(?:\s+|^)(\/*\S+)/;
const rLang = /\s*lang:(\w+)/i;
Expand All @@ -17,7 +17,6 @@ const rTo = /\s*to:(\d+)/i;
*/

module.exports = ctx => function includeCodeTag(args) {
const config = ctx.config.highlight || {};
let codeDir = ctx.config.code_dir;
let arg = args.join(' ');

Expand Down Expand Up @@ -47,22 +46,31 @@ module.exports = ctx => function includeCodeTag(args) {

const path = match[2];

// If the title is not defined, use file name instead
const title = match[1] || basename(path);

// If the language is not defined, use file extension instead
lang = lang || extname(path).substring(1);

const src = join(ctx.source_dir, codeDir, path);

// If the title is not defined, use file name instead
const title = match[1] || basename(path);
const caption = `<span>${title}</span><a href="${ctx.config.root}${codeDir}${path}">view raw</a>`;

const options = {
const hljsCfg = ctx.config.highlight || {};
const prismjsCfg = ctx.config.prismjs || {};

const hljsOptions = {
lang,
caption,
gutter: config.line_number,
hljs: config.hljs,
tab: config.tab_replace
gutter: hljsCfg.line_number,
hljs: hljsCfg.hljs,
tab: hljsCfg.tab_replace
};

const prismjsOptions = {
lang,
lineNumber: prismjsCfg.line_number,
tab: prismjsCfg.tab_replace,
isPreprocess: prismjsCfg.preprocess
};

return fs.exists(src).then(exist => {
Expand All @@ -73,10 +81,12 @@ module.exports = ctx => function includeCodeTag(args) {
const lines = code.split('\n');
code = lines.slice(from, to).join('\n').trim();

if (!config.enable) {
return `<pre><code>${code}</code></pre>`;
if (prismjsCfg.enable) {
return prismHighlight(code, prismjsOptions);
} else if (hljsCfg.enable) {
return highlight(code, hljsOptions);
}

return highlight(code, options);
return `<pre><code>${code}</code></pre>`;
});
};
Loading