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(extend/injector): bring up new extend Injector #4049

Merged
merged 13 commits into from
Jun 16, 2020
1 change: 1 addition & 0 deletions lib/extend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ exports.Deployer = require('./deployer');
exports.Filter = require('./filter');
exports.Generator = require('./generator');
exports.Helper = require('./helper');
exports.Injector = require('./injector');
exports.Migrator = require('./migrator');
exports.Processor = require('./processor');
exports.Renderer = require('./renderer');
Expand Down
81 changes: 81 additions & 0 deletions lib/extend/injector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use strict';

const { Cache } = require('hexo-util');

class Injector {
constructor() {
this.store = {
head_begin: {},
head_end: {},
body_begin: {},
body_end: {}
};

this.cache = new Cache();
}

list() {
return this.store;
}

get(entry, to = 'default') {
return Array.from(this.store[entry][to] || []);
}

getText(entry, to = 'default') {
const arr = this.get(entry, to);
if (!arr || !arr.length) return '';
return arr.join('');
}

register(entry, value, to = 'default') {
if (!entry) throw new TypeError('entry is required');
if (typeof value === 'function') value = value();

const entryMap = this.store[entry] || this.store.head_end;
const valueSet = entryMap[to] || new Set();
jiangtj marked this conversation as resolved.
Show resolved Hide resolved
valueSet.add(value);
entryMap[to] = valueSet;
}

exec(data, locals = { page: {} }) {
let currentType = 'default';
const { page } = locals;

if (page.__index) currentType = 'home';
if (page.__post) currentType = 'post';
if (page.__page) currentType = 'page';
if (page.archive) currentType = 'archive';
if (page.category) currentType = 'category';
if (page.tag) currentType = 'tag';
if (page.layout) currentType = page.layout;

const injector = (data, pattern, flag, isBegin = true) => {
if (data.includes(`hexo injector ${flag}`)) return data;

const code = this.cache.apply(`${flag}-${currentType}-code`, () => {
const content = currentType === 'default' ? this.getText(flag, 'default') : this.getText(flag, currentType) + this.getText(flag, 'default');

if (!content.length) return '';
return '<!-- hexo injector ' + flag + ' start -->' + content + '<!-- hexo injector ' + flag + ' end -->';
});

// avoid unnesscary replace() for better performance
if (!code.length) return data;
return data.replace(pattern, str => { return isBegin ? str + code : code + str; });
};

// Inject head_begin
data = injector(data, /<head.*?>/, 'head_begin', true);
// Inject head_end
data = injector(data, '</head>', 'head_end', false);
// Inject body_begin
data = injector(data, /<body.*?>/, 'body_begin', true);
// Inject body_end
data = injector(data, '</body>', 'body_end', false);

return data;
}
}

module.exports = Injector;
8 changes: 5 additions & 3 deletions 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, Migrator, Processor, Renderer, Tag } = require('../extend');
const { Console, Deployer, Filter, Generator, Helper, Injector, Migrator, Processor, Renderer, Tag } = require('../extend');
const Render = require('./render');
const registerModels = require('./register_models');
const Post = require('./post');
Expand All @@ -24,8 +24,7 @@ const defaultConfig = require('./default_config');
const loadDatabase = require('./load_database');
const multiConfigPath = require('./multi_config_path');
const { sync } = require('resolve');
const full_url_for = require('../plugins/helper/full_url_for');
const { deepMerge } = require('hexo-util');
const { deepMerge, full_url_for } = require('hexo-util');

const libDir = dirname(__dirname);
const dbVersion = 1;
Expand Down Expand Up @@ -55,6 +54,7 @@ const createLoadThemeRoute = function(generatorResult, locals, ctx) {
if (view) {
log.debug(`Rendering HTML ${name}: ${magenta(path)}`);
return view.render(locals)
.then(result => ctx.extend.injector.exec(result, locals))
.then(result => ctx.execFilter('after_route_render', result, {
context: ctx,
args: [locals]
Expand Down Expand Up @@ -115,6 +115,7 @@ class Hexo extends EventEmitter {
filter: new Filter(),
generator: new Generator(),
helper: new Helper(),
injector: new Injector(),
migrator: new Migrator(),
processor: new Processor(),
renderer: new Renderer(),
Expand Down Expand Up @@ -219,6 +220,7 @@ class Hexo extends EventEmitter {
require('../plugins/filter')(this);
require('../plugins/generator')(this);
require('../plugins/helper')(this);
require('../plugins/injector')(this);
require('../plugins/processor')(this);
require('../plugins/renderer')(this);
require('../plugins/tag')(this);
Expand Down
6 changes: 6 additions & 0 deletions lib/plugins/injector/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict';

module.exports = ctx => {
// eslint-disable-next-line no-unused-vars
const { injector } = ctx.extend;
};
1 change: 1 addition & 0 deletions test/scripts/extend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe('Extend', () => {
require('./filter');
require('./generator');
require('./helper');
require('./injector');
require('./migrator');
require('./processor');
require('./renderer');
Expand Down
237 changes: 237 additions & 0 deletions test/scripts/extend/injector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
'use strict';

describe('Injector', () => {
const content = [
'<!DOCTYPE html>',
'<html lang="en">',
'<head id="head"><title>Test</title>',
'</head>',
'<body id="body">',
'<div></div>',
'<p></p>',
'</body>',
'</html>'
].join('');

const Injector = require('../../../lib/extend/injector');

it('register() - entry is required', () => {
const i = new Injector();

// no name
try {
i.register();
} catch (err) {
err.should.be
.instanceOf(TypeError)
.property('message', 'entry is required');
}
});

it('register() - string', () => {
const i = new Injector();

const str = '<link rel="stylesheet" href="DPlayer.min.css" />';
i.register('head_begin', str);
i.register('head_end', str, 'home');

i.get('head_begin').should.contains(str);
i.get('head_begin', 'default').should.contains(str);
i.get('head_end', 'home').should.contains(str);
});

it('register() - function', () => {
const i = new Injector();

const fn = () => '<link rel="stylesheet" href="DPlayer.min.css" />';
i.register('head_begin', fn);

i.get('head_begin').should.contains(fn());
});

it('register() - fallback when entry not exists', () => {
const i = new Injector();

const str = '<link rel="stylesheet" href="DPlayer.min.css" />';
i.register('foo', str);

i.get('head_end').should.contains(str);
});

it('list()', () => {
const i = new Injector();

i.register('body_begin', '<script src="DPlayer.min.js"></script>');

i.list().body_begin.default.should.be.instanceOf(Set);
[...i.list().body_begin.default].should.not.be.empty;
});

it('get()', () => {
const i = new Injector();
const str = '<script src="jquery.min.js"></script>';

i.register('body_begin', str);
i.register('body_end', str, 'home');

i.get('body_begin').should.be.instanceOf(Array);
i.get('body_begin').should.contains(str);
i.get('body_end', 'home').should.be.instanceOf(Array);
i.get('body_end', 'home').should.contains(str);

i.get('head_end').should.be.instanceOf(Array);
i.get('head_end').should.eql([]);
});

it('getText()', () => {
const i = new Injector();
const str = '<script src="jquery.min.js"></script>';

i.register('head_end', str);
i.register('body_end', str, 'home');

i.getText('body_end', 'home').should.eql(str);
i.getText('body_end').should.eql('');
});

it('exec() - default', () => {
const i = new Injector();
const result = i.exec(content);
result.should.contain('<head id="head"><title>Test</title></head>');
result.should.contain('<body id="body"><div></div><p></p></body>');
});

it('exec() - default', () => {
const i = new Injector();
const result = i.exec(content);
result.should.contain('<head id="head"><title>Test</title></head>');
result.should.contain('<body id="body"><div></div><p></p></body>');
});

it('exec() - insert code', () => {
const i = new Injector();

i.register('head_begin', '<!-- Powered by Hexo -->');
i.register('head_end', '<link href="prism.css" rel="stylesheet">');
i.register('head_end', '<link href="prism-linenumber.css" rel="stylesheet">');
i.register('body_begin', '<script>window.Prism = window.Prism || {}; window.Prism.manual = true;</script>');
i.register('body_end', '<script src="prism.js"></script>');

const result = i.exec(content);

result.should.contain('<head id="head"><!-- hexo injector head_begin start --><!-- Powered by Hexo --><!-- hexo injector head_begin end -->');
result.should.contain('<!-- hexo injector head_end start --><link href="prism.css" rel="stylesheet"><link href="prism-linenumber.css" rel="stylesheet"><!-- hexo injector head_end end --></head>');
result.should.contain('<body id="body"><!-- hexo injector body_begin start --><script>window.Prism = window.Prism || {}; window.Prism.manual = true;</script><!-- hexo injector body_begin end -->');
result.should.contain('<!-- hexo injector body_end start --><script src="prism.js"></script><!-- hexo injector body_end end --></body>');
});

it('exec() - no duplicate insert', () => {
const content = [
'<!DOCTYPE html>',
'<html lang="en">',
'<head id="head"><!-- hexo injector head_begin start --><!-- hexo injector head_begin end -->',
'<title>Test</title>',
'<!-- hexo injector head_end start --><link href="prism.css" rel="stylesheet"></head>',
'<body id="body"><!-- hexo injector body_begin start --><!-- hexo injector body_begin end -->',
'<div></div>',
'<p></p>',
'<!-- hexo injector body_end start --><script src="prism.js"></script><!-- hexo injector body_end end --></body>',
'</html>'
].join('');

const i = new Injector();

i.register('head_begin', '<!-- Powered by Hexo -->');
i.register('head_end', '<link href="prism.css" rel="stylesheet">');
i.register('head_end', '<link href="prism-linenumber.css" rel="stylesheet">');
i.register('body_begin', '<script>window.Prism = window.Prism || {}; window.Prism.manual = true;</script>');
i.register('body_end', '<script src="prism.js"></script>');

const result = i.exec(content);

result.should.contain('<head id="head"><!-- hexo injector head_begin start --><!-- hexo injector head_begin end -->');
result.should.contain('<!-- hexo injector head_end start --><link href="prism.css" rel="stylesheet"></head>');
result.should.contain('<body id="body"><!-- hexo injector body_begin start --><!-- hexo injector body_begin end -->');
result.should.contain('<!-- hexo injector body_end start --><script src="prism.js"></script><!-- hexo injector body_end end --></body>');
});

it('exec() - multi-line head & body', () => {
const content = [
'<!DOCTYPE html>',
'<html lang="en">',
'<head id="head"><title>Test</title>',
'</head>',
'<body id="body">',
'<div></div>',
'<p></p>',
'</body>',
'</html>'
].join('\n');

const i = new Injector();

i.register('head_begin', '<!-- Powered by Hexo -->');
i.register('head_end', '<link href="prism.css" rel="stylesheet">');
i.register('head_end', '<link href="prism-linenumber.css" rel="stylesheet">');
i.register('body_begin', '<script>window.Prism = window.Prism || {}; window.Prism.manual = true;</script>');
i.register('body_end', '<script src="prism.js"></script>');

const result = i.exec(content);

result.should.contain('<head id="head"><!-- hexo injector head_begin start --><!-- Powered by Hexo --><!-- hexo injector head_begin end -->');
result.should.contain('<!-- hexo injector head_end start --><link href="prism.css" rel="stylesheet"><link href="prism-linenumber.css" rel="stylesheet"><!-- hexo injector head_end end --></head>');
result.should.contain('<body id="body"><!-- hexo injector body_begin start --><script>window.Prism = window.Prism || {}; window.Prism.manual = true;</script><!-- hexo injector body_begin end -->');
result.should.contain('<!-- hexo injector body_end start --><script src="prism.js"></script><!-- hexo injector body_end end --></body>');
});

it('exec() - inject on specific page', () => {
const content = [
'<!DOCTYPE html>',
'<html lang="en">',
'<head id="head"><title>Test</title>',
'</head>',
'<body id="body">',
'<div></div>',
'<p></p>',
'</body>',
'</html>'
].join('\n');

const i = new Injector();

i.register('head_begin', '<!-- head_begin_default -->');
i.register('head_begin', '<!-- head_begin_home -->', 'home');
i.register('head_begin', '<!-- head_begin_post -->', 'post');
i.register('head_begin', '<!-- head_begin_page -->', 'page');
i.register('head_begin', '<!-- head_begin_archive -->', 'archive');
i.register('head_begin', '<!-- head_begin_category -->', 'category');
i.register('head_begin', '<!-- head_begin_tag -->', 'tag');

const result1 = i.exec(content, { page: {} });
const result2 = i.exec(content, { page: { __index: true } });
const result3 = i.exec(content, { page: { __post: true } });
const result4 = i.exec(content, { page: { __page: true } });
const result5 = i.exec(content, { page: { archive: true } });
const result6 = i.exec(content, { page: { category: true } });
const result7 = i.exec(content, { page: { tag: true } });

// home
result1.should.not.contain('<!-- head_begin_home -->');
result2.should.contain('<!-- head_begin_home --><!-- head_begin_default -->');
// post
result1.should.not.contain('<!-- head_begin_post -->');
result3.should.contain('<!-- head_begin_post --><!-- head_begin_default -->');
// page
result1.should.not.contain('<!-- head_begin_page -->');
result4.should.contain('<!-- head_begin_page --><!-- head_begin_default -->');
// archive
result1.should.not.contain('<!-- head_begin_archive -->');
result5.should.contain('<!-- head_begin_archive --><!-- head_begin_default -->');
// category
result1.should.not.contain('<!-- head_begin_category -->');
result6.should.contain('<!-- head_begin_category --><!-- head_begin_default -->');
// tag
result1.should.not.contain('<!-- head_begin_tag -->');
result7.should.contain('<!-- head_begin_tag --><!-- head_begin_default -->');
});
});