Skip to content

Commit

Permalink
feat(extend/injector): bring up new extend Injector (#4049)
Browse files Browse the repository at this point in the history
* feat(injector): bring up lib/extend/injector

* test(injector): bring up extend/injector

* feat(injector): bring up filter

* test(filter): bring up injectot test cases

* feat(inject): add support for inject dest

* feat: return before data.replace if no inject

* feat(injector): utilize cache

* refactor(injector): use new data structure

Apply suggestions from code review by @dailyrandomphoto

* feat(injector): support page.layout

* refactor(injector): simplify the filter

Apply code suggestions from code review by @dailyrandomphoto

* refactor(injector): split injector from filter

Make sure injector is executed before after_route_render

* refactor(injector): add exec() method

- Remove injectorFilter from plugin dir
- Move cache into Injector Class
- Remove decache
  • Loading branch information
SukkaW authored Jun 16, 2020
1 parent edef5c2 commit d117819
Show file tree
Hide file tree
Showing 6 changed files with 331 additions and 3 deletions.
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();
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 -->');
});
});

0 comments on commit d117819

Please sign in to comment.