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(image): postAsset to prepend post's relative path #159

Merged
merged 12 commits into from
Aug 21, 2020
8 changes: 7 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
os:
- linux
- windows

language: node_js

cache:
Expand All @@ -9,7 +13,9 @@ node_js:
- "14"

script:
- npm run eslint
- if [[ $TRAVIS_OS_NAME == "linux" ]]; then
npm run eslint;
fi
- npm run test-cov

after_script:
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ marked:
headerIds: true
lazyload: false
prependRoot: false
postAsset: false
external_link:
enable: false
exclude: []
Expand All @@ -55,6 +56,11 @@ marked:
root: /blog/
```
* `![text](/path/to/image.jpg)` becomes `<img src="/blog/path/to/image.jpg" alt="text">`
- **postAsset** - Resolve post asset's image path to relative path and prepend root valuewhen [`post_asset_folder`](https://hexo.io/docs/asset-folders) is enabled.
* "image.jpg" is located at "/2020/01/02/foo/image.jpg", which is a post asset of "/2020/01/02/foo/".
* `![](image.jpg)` becomes `<img src="/2020/01/02/foo/image.jpg">`
* Requires `prependRoot:` to be enabled.
* This may reduce performance due to additional processing.
- **external_link**
* **enable** - Open external links in a new tab.
* **exclude** - Exclude hostname. Specify subdomain when applicable, including `www`.
Expand Down
70 changes: 44 additions & 26 deletions lib/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,31 @@
const marked = require('marked');
const { encodeURL, slugize, stripHTML, url_for, isExternalLink } = require('hexo-util');
const MarkedRenderer = marked.Renderer;
const { parse } = require('url');
const { sep } = require('path');

const anchorId = (str, transformOption) => {
return slugize(str.trim(), {transform: transformOption});
};

class Renderer extends MarkedRenderer {
constructor() {
constructor(hexo) {
super();
this._headingId = {};
this.hexo = hexo;
}

// Add id attribute to headings
heading(text, level) {
if (!this.options.headerIds) {
const { headerIds, modifyAnchors } = this.options;
const { _headingId } = this;

if (!headerIds) {
return `<h${level}>${text}</h${level}>`;
}
const transformOption = this.options.modifyAnchors;

const transformOption = modifyAnchors;
let id = anchorId(stripHTML(text), transformOption);
const headingId = this._headingId;
const headingId = _headingId;

// Add a number after id if repeated
if (headingId[id]) {
Expand All @@ -37,14 +42,15 @@ class Renderer extends MarkedRenderer {

// Support AutoLink option
link(href, title, text) {
const { options } = this;
const { external_link } = options;
if (options.sanitizeUrl) {
const { autolink, external_link, sanitizeUrl } = this.options;
const { url: urlCfg } = this.hexo.config;

if (sanitizeUrl) {
if (href.startsWith('javascript:') || href.startsWith('vbscript:') || href.startsWith('data:')) {
href = '';
}
}
if (!options.autolink && href === text && title == null) {
if (!autolink && href === text && title == null) {
return href;
}

Expand All @@ -56,7 +62,7 @@ class Renderer extends MarkedRenderer {
const target = ' target="_blank"';
const noopener = ' rel="noopener"';
const nofollowTag = ' rel="noopener external nofollow noreferrer"';
if (isExternalLink(href, options.config.url, external_link.exclude)) {
if (isExternalLink(href, urlCfg, external_link.exclude)) {
if (external_link.enable && external_link.nofollow) {
out += target + nofollowTag;
} else if (external_link.enable) {
Expand All @@ -83,17 +89,24 @@ class Renderer extends MarkedRenderer {

// Prepend root to image path
image(href, title, text) {
const { options } = this;

if (!parse(href).hostname && !options.config.relative_link
&& options.prependRoot) {
href = url_for.call(options, href);
const { hexo, options } = this;
const { relative_link } = hexo.config;
const { lazyload, prependRoot, postId } = options;

if (!/^(#|\/\/|http(s)?:)/.test(href) && !relative_link && prependRoot) {
if (!/^(\/|\\)/.test(href) && postId) {
curbengh marked this conversation as resolved.
Show resolved Hide resolved
const PostAsset = hexo.model('PostAsset');
// slug requires platform-specific path
const asset = PostAsset.findOne({ post: postId, slug: href.replace(/\/|\\/g, sep) });
Copy link
Member

@SukkaW SukkaW Aug 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using findOne, is it possible to use findById directly?

https://github.com/hexojs/hexo/blob/master/lib/plugins/processor/post.js

Also, each post should only find PostAsset for once (not for every image). A cache should be implemented.

Copy link
Member

@SukkaW SukkaW Aug 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

// ...

const asset = postAssetCache.apply(postId, () => PostAsset.findOne({ post: postId, slug: href.replace(/\/|\\/g, sep) }));

And see if there is any performance improvement.

It could also be:

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

// ...

// Only init cache when prependRoot is enabled
if (!relative_link && prependRoot) {
  postAssetCache = postAssetCache || new Cache();
}

const asset = postAssetCache.apply(postId, () => PostAsset.findOne({ post: postId, slug: href.replace(/\/|\\/g, sep) }));

if (asset) href = encodeURL(('/' + asset.path).replace(/\\/g, '/').replace(/\/{2,}/g, '/'));
}
href = url_for.call(hexo, href);
}

let out = `<img src="${encodeURL(href)}"`;
if (text) out += ` alt="${text}"`;
if (title) out += ` title="${title}"`;
if (options.lazyload) out += ' loading="lazy"';
if (lazyload) out += ' loading="lazy"';

out += '>';
return out;
Expand All @@ -105,19 +118,24 @@ marked.setOptions({
});

module.exports = function(data, options) {
const siteCfg = Object.assign({}, {
config: {
url: this.config.url,
root: this.config.root,
relative_link: this.config.relative_link
}
});
const { post_asset_folder, marked: markedCfg } = this.config;
const { prependRoot, postAsset } = markedCfg;
const { path, text } = data;

// exec filter to extend renderer.
const renderer = new Renderer();
const renderer = new Renderer(this);
this.execFilterSync('marked:renderer', renderer, {context: this});

return marked(data.text, Object.assign({
let postId = '';
if (path && post_asset_folder && prependRoot && postAsset) {
const Post = this.model('Post');
// Windows compatibility, Post.findOne() requires forward slash
const source = path.replace(this.source_dir, '').replace('\\', '/');
const post = Post.findOne({ source });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may support Page in future.

postId = post ? post._id : '';
}

return marked(text, Object.assign({
renderer
}, this.config.marked, options, siteCfg));
}, markedCfg, options, { postId }));
};
95 changes: 85 additions & 10 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
'use strict';

require('chai').should();
const { encodeURL, escapeHTML } = require('hexo-util');
const { encodeURL, escapeHTML, url_for } = require('hexo-util');
const Hexo = require('hexo');
const { join } = require('path').posix;
const { sep } = require('path');

describe('Marked renderer', () => {
const hexo = new Hexo(__dirname, {silent: true});
const ctx = Object.assign(hexo, {
config: {
marked: {}
}
const defaultCfg = JSON.parse(JSON.stringify(Object.assign(hexo.config, {
marked: {}
})));

before(async () => {
hexo.config.permalink = ':title';
await hexo.init();
});

beforeEach(() => {
hexo.config = JSON.parse(JSON.stringify(defaultCfg));
});

const r = require('../lib/renderer').bind(hexo);
Expand Down Expand Up @@ -88,7 +97,7 @@ describe('Marked renderer', () => {

it('should render headings without headerIds when disabled', () => {
const body = '## hexo-server';
ctx.config.marked.headerIds = false;
hexo.config.marked.headerIds = false;

const result = r({text: body});

Expand Down Expand Up @@ -545,7 +554,7 @@ describe('Marked renderer', () => {
`![](${urlB})`
].join('\n');

const r = require('../lib/renderer').bind(ctx);
const r = require('../lib/renderer').bind(hexo);

const result = r({text: body});

Expand All @@ -562,7 +571,7 @@ describe('Marked renderer', () => {
'![a"b](http://bar.com/b.jpg "c>d")'
].join('\n');

const r = require('../lib/renderer').bind(ctx);
const r = require('../lib/renderer').bind(hexo);

const result = r({text: body});

Expand All @@ -579,9 +588,9 @@ describe('Marked renderer', () => {
'![foo](/aaa/bbb.jpg)'
].join('\n');

ctx.config.marked.lazyload = true;
hexo.config.marked.lazyload = true;

const r = require('../lib/renderer').bind(ctx);
const r = require('../lib/renderer').bind(hexo);

const result = r({ text: body });

Expand All @@ -591,9 +600,75 @@ describe('Marked renderer', () => {
].join('\n'));
});

describe('postAsset', () => {
const Post = hexo.model('Post');
const PostAsset = hexo.model('PostAsset');

beforeEach(() => {
hexo.config.post_asset_folder = true;
hexo.config.marked = {
prependRoot: true,
postAsset: true
};
});

it('should prepend post path', async () => {
const asset = 'img/bar.svg';
const slug = asset.replace(/\//g, sep);
const content = `![](${asset})`;
const post = await Post.insert({
source: '_posts/foo.md',
slug: 'foo'
});
const postasset = await PostAsset.insert({
_id: `source/_posts/foo/${asset}`,
slug,
post: post._id
});

const expected = url_for.call(hexo, join(post.path, asset));
const result = r({ text: content, path: post.full_source });
result.should.eql(`<p><img src="${expected}"></p>\n`);

// should not be Windows path
expected.includes('\\').should.eql(false);

await PostAsset.removeById(postasset._id);
await Post.removeById(post._id);
});

it('should not modify non-post asset', async () => {
const asset = 'bar.svg';
const siteasset = '/logo/brand.png';
const site = 'http://lorem.ipsum/dolor/huri.bun';
const content = `![](${asset})\n![](${siteasset})\n![](${site})`;
const post = await Post.insert({
source: '_posts/foo.md',
slug: 'foo'
});
const postasset = await PostAsset.insert({
_id: `source/_posts/foo/${asset}`,
slug: asset,
post: post._id
});

const result = r({ text: content, path: post.full_source });
result.should.eql([
`<p><img src="${url_for.call(hexo, join(post.path, asset))}">`,
`<img src="${siteasset}">`,
`<img src="${site}"></p>`
].join('\n') + '\n');

await PostAsset.removeById(postasset._id);
await Post.removeById(post._id);
});
});

describe('exec filter to extend', () => {
it('should execute filter registered to marked:renderer', () => {
const hexo = new Hexo(__dirname, {silent: true});
hexo.config.marked = {};

hexo.extend.filter.register('marked:renderer', renderer => {
renderer.image = function(href, title, text) {
return `<img data-src="${encodeURL(href)}">`;
Expand Down