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

refactor highlight: add extend api for highlight #5095

Merged
merged 14 commits into from
Dec 14, 2022
3 changes: 2 additions & 1 deletion lib/extend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ const Deployer = require('./deployer');
const Filter = require('./filter');
const Generator = require('./generator');
const Helper = require('./helper');
const Highlight = require('./syntax_highlight');
const Injector = require('./injector');
const Migrator = require('./migrator');
const Processor = require('./processor');
const Renderer = require('./renderer');
const Tag = require('./tag');

module.exports = {
Console, Deployer, Filter, Generator, Helper, Injector, Migrator, Processor, Renderer, Tag
Console, Deployer, Filter, Generator, Helper, Highlight, Injector, Migrator, Processor, Renderer, Tag
};
29 changes: 29 additions & 0 deletions lib/extend/syntax_highlight.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

class SyntaxHighlight {
constructor() {
this.store = {};
}

register(name, fn) {
if (typeof fn !== 'function') throw new TypeError('fn must be a function');

this.store[name] = fn;
}

query(name) {
return name && this.store[name];
}

exec(name, options) {
const fn = this.store[name];

if (!fn) throw new TypeError(`syntax highlighter ${name} is not registered`);
const ctx = options.context;
const args = options.args || [];

return Reflect.apply(fn, ctx, args);
}
}

module.exports = SyntaxHighlight;
3 changes: 1 addition & 2 deletions lib/hexo/default_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ module.exports = {
post_asset_folder: false,
relative_link: false,
future: true,
syntax_highlighter: 'highlight.js',
highlight: {
enable: true,
auto_detect: false,
line_number: true,
tab_replace: '',
Expand All @@ -51,7 +51,6 @@ module.exports = {
hljs: false
},
prismjs: {
enable: false,
preprocess: true,
line_number: true,
tab_replace: ''
Expand Down
4 changes: 3 additions & 1 deletion lib/hexo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const Module = require('module');
const { runInThisContext } = require('vm');
const { version } = require('../../package.json');
const { logger } = require('hexo-log');
const { Console, Deployer, Filter, Generator, Helper, Injector, Migrator, Processor, Renderer, Tag } = require('../extend');
const { Console, Deployer, Filter, Generator, Helper, Highlight, Injector, Migrator, Processor, Renderer, Tag } = require('../extend');
const Render = require('./render');
const registerModels = require('./register_models');
const Post = require('./post');
Expand Down Expand Up @@ -123,6 +123,7 @@ class Hexo extends EventEmitter {
filter: new Filter(),
generator: new Generator(),
helper: new Helper(),
highlight: new Highlight(),
injector: new Injector(),
migrator: new Migrator(),
processor: new Processor(),
Expand Down Expand Up @@ -224,6 +225,7 @@ class Hexo extends EventEmitter {
require('../plugins/filter')(this);
require('../plugins/generator')(this);
require('../plugins/helper')(this);
require('../plugins/highlight')(this);
require('../plugins/injector')(this);
require('../plugins/processor')(this);
require('../plugins/renderer')(this);
Expand Down
2 changes: 1 addition & 1 deletion lib/hexo/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ class Post {
}

const options = data.markdown || {};
if (!config.highlight.enable) options.highlight = null;
if (!config.syntax_highlighter) options.highlight = null;

ctx.log.debug('Rendering post: %s', magenta(source));
// Render with markdown or other renderer
Expand Down
132 changes: 47 additions & 85 deletions lib/plugins/filter/before_post_render/backtick_code_block.js
Original file line number Diff line number Diff line change
@@ -1,111 +1,73 @@
'use strict';

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[^\S\r\n]?(\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 dataContent = data.content;

const hljsCfg = this.config.highlight || {};
const prismCfg = this.config.prismjs || {};
module.exports = ctx => {
return function backtickCodeBlock(data) {
const dataContent = data.content;

if ((!dataContent.includes('```') && !dataContent.includes('~~~')) || (!hljsCfg.enable && !prismCfg.enable)) return;
if ((!dataContent.includes('```') && !dataContent.includes('~~~')) || !ctx.extend.highlight.query(ctx.config.syntax_highlighter)) return;

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

// neither highlight or prismjs is enabled, return escaped content directly.
if (!hljsCfg.enable && !prismCfg.enable) return escapeSwigTag($0);
// neither highlight or prismjs is enabled, return escaped content directly.
if (!ctx.extend.highlight.query(ctx.config.syntax_highlighter)) return escapeSwigTag($0);

// Extract language and caption of code blocks
const args = _args.split('=').shift();
let lang, caption;
// Extract language and caption of code blocks
const args = _args.split('=').shift();
let lang, caption;

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

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

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

if (match[3]) {
caption += `<a href="${match[3]}">${match[4] ? match[4] : 'link'}</a>`;
if (match[3]) {
caption += `<a href="${match[3]}">${match[4] ? match[4] : 'link'}</a>`;
}
}
}
}
}

// PR #3765
if (start.includes('>')) {
// heading of last line is already removed by the top RegExp "rBacktick"
const depth = start.split('>').length - 1;
const regexp = new RegExp(`^([^\\S\\r\\n]*>){0,${depth}}([^\\S\\r\\n]|$)`, 'mg');
content = content.replace(regexp, '');
}

// 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,
caption
};

content = prismHighlight(content, options);
} else if (hljsCfg.enable) {
if (!highlight) highlight = require('hexo-util').highlight;
// PR #3765
if (start.includes('>')) {
// heading of last line is already removed by the top RegExp "rBacktick"
const depth = start.split('>').length - 1;
const regexp = new RegExp(`^([^\\S\\r\\n]*>){0,${depth}}([^\\S\\r\\n]|$)`, 'mg');
content = content.replace(regexp, '');
}

const options = {
hljs: hljsCfg.hljs,
autoDetect: hljsCfg.auto_detect,
gutter: hljsCfg.line_number,
tab: hljsCfg.tab_replace,
wrap: hljsCfg.wrap,
lang,
languageAttr: hljsCfg.language_attr,
caption
caption,
lines_length: content.split('\n').length
};
// setup line number by inline
_args = _args.replace('=+', '=');

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;
}
}

if (Array.isArray(hljsCfg.exclude_languages) && hljsCfg.exclude_languages.includes(options.lang)) {
// Only wrap with <pre><code class="lang"></code></pre>
options.wrap = false;
options.gutter = false;
options.autoDetect = false;
// setup firstLineNumber;
if (_args.includes('=')) {
options.firstLineNumber = _args.split('=')[1] || 1;
}

content = highlight(content, options);
}

return start
+ '<hexoPostRenderCodeBlock>'
+ escapeSwigTag(content)
+ '</hexoPostRenderCodeBlock>'
+ end;
});
}

module.exports = backtickCodeBlock;
content = ctx.extend.highlight.exec(ctx.config.syntax_highlighter, {
context: ctx,
args: [content, options]
});

return start
+ '<hexoPostRenderCodeBlock>'
+ escapeSwigTag(content)
+ '</hexoPostRenderCodeBlock>'
+ end;
});
};
};
2 changes: 1 addition & 1 deletion lib/plugins/filter/before_post_render/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
module.exports = ctx => {
const { filter } = ctx.extend;

filter.register('before_post_render', require('./backtick_code_block'));
filter.register('before_post_render', require('./backtick_code_block')(ctx));
filter.register('before_post_render', require('./titlecase'));
};
44 changes: 44 additions & 0 deletions lib/plugins/highlight/highlight.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict';

// Lazy require highlight.js
let highlight;

module.exports = function highlightFilter(code, options) {
const hljsCfg = this.config.highlight || {};
const line_threshold = options.line_threshold || hljsCfg.line_threshold || 0;
const shouldUseLineNumbers = typeof options.line_number === 'undefined' ? hljsCfg.line_number : options.line_number;
const surpassesLineThreshold = options.lines_length > line_threshold;
const gutter = shouldUseLineNumbers && surpassesLineThreshold;
const languageAttr = typeof options.language_attr === 'undefined' ? hljsCfg.language_attr : options.language_attr;

const hljsOptions = {
autoDetect: hljsCfg.auto_detect,
caption: options.caption,
firstLine: options.firstLine,
gutter,
hljs: hljsCfg.hljs,
lang: options.lang,
languageAttr,
mark: options.mark,
tab: hljsCfg.tab_replace,
wrap: hljsCfg.wrap
};
if (hljsCfg.first_line_number === 'inline') {
if (typeof options.firstLineNumber !== 'undefined') {
hljsOptions.firstLine = options.firstLineNumber;
} else {
hljsOptions.gutter = false;
}
}

if (Array.isArray(hljsCfg.exclude_languages) && hljsCfg.exclude_languages.includes(hljsOptions.lang)) {
// Only wrap with <pre><code class="lang"></code></pre>
hljsOptions.wrap = false;
hljsOptions.gutter = false;
hljsOptions.autoDetect = false;
}

if (!highlight) highlight = require('hexo-util').highlight;

return highlight(code, hljsOptions);
};
8 changes: 8 additions & 0 deletions lib/plugins/highlight/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict';

module.exports = ctx => {
const { highlight } = ctx.extend;

highlight.register('highlight.js', require('./highlight'));
highlight.register('prismjs', require('./prism'));
};
26 changes: 26 additions & 0 deletions lib/plugins/highlight/prism.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use strict';

// Lazy require prismjs
let prismHighlight;

module.exports = function(code, options) {
const prismjsCfg = this.config.prismjs || {};
const line_threshold = options.line_threshold || prismjsCfg.line_threshold || 0;
const shouldUseLineNumbers = typeof options.line_number === 'undefined' ? prismjsCfg.line_number : options.line_number;
const surpassesLineThreshold = options.lines_length > line_threshold;
const lineNumber = shouldUseLineNumbers && surpassesLineThreshold;

const prismjsOptions = {
caption: options.caption,
firstLine: options.firstLine,
isPreprocess: prismjsCfg.preprocess,
lang: options.lang,
lineNumber,
mark: options.mark,
tab: prismjsCfg.tab_replace
};

if (!prismHighlight) prismHighlight = require('hexo-util').prismHighlight;

return prismHighlight(code, prismjsOptions);
};
Loading