diff --git a/.travis.yml b/.travis.yml
index 4535a4e..ca85cef 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,3 +1,7 @@
+os:
+ - linux
+ - windows
+
language: node_js
cache:
@@ -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:
diff --git a/README.md b/README.md
index 7b824b7..8b45140 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,7 @@ marked:
headerIds: true
lazyload: false
prependRoot: false
+ postAsset: false
external_link:
enable: false
exclude: []
@@ -55,6 +56,10 @@ marked:
root: /blog/
```
* `![text](/path/to/image.jpg)` becomes ``
+- **postAsset** - Resolve post asset's image path to relative path and prepend root value when [`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 ``
+ * Requires `prependRoot:` to be enabled.
- **external_link**
* **enable** - Open external links in a new tab.
* **exclude** - Exclude hostname. Specify subdomain when applicable, including `www`.
diff --git a/lib/renderer.js b/lib/renderer.js
index 0ca933d..c5e3137 100644
--- a/lib/renderer.js
+++ b/lib/renderer.js
@@ -3,26 +3,30 @@
const marked = require('marked');
const { encodeURL, slugize, stripHTML, url_for, isExternalLink } = require('hexo-util');
const MarkedRenderer = marked.Renderer;
-const { parse } = require('url');
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 `${text}`;
}
- 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]) {
@@ -37,14 +41,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;
}
@@ -56,7 +61,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) {
@@ -83,17 +88,25 @@ 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, postPath } = options;
+
+ if (!/^(#|\/\/|http(s)?:)/.test(href) && !relative_link && prependRoot) {
+ if (!href.startsWith('/') && !href.startsWith('\\') && postPath) {
+ const PostAsset = hexo.model('PostAsset');
+ // findById requires forward slash
+ const asset = PostAsset.findById(postPath + href.replace(/\\/g, '/'));
+ // asset.path is backward slash in Windows
+ if (asset) href = asset.path.replace(/\\/g, '/');
+ }
+ href = url_for.call(hexo, href);
}
let out = `';
return out;
@@ -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, source_dir } = 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 postPath = '';
+ if (path && post_asset_folder && prependRoot && postAsset) {
+ const Post = this.model('Post');
+ // Windows compatibility, Post.findOne() requires forward slash
+ const source = path.substring(this.source_dir.length).replace(/\\/g, '/');
+ const post = Post.findOne({ source });
+ postPath = post ? source_dir + '/_posts/' + post.slug + '/' : '';
+ }
+
+ return marked(text, Object.assign({
renderer
- }, this.config.marked, options, siteCfg));
+ }, markedCfg, options, { postPath }));
};
diff --git a/test/index.js b/test/index.js
index b81e7a9..b5053cb 100644
--- a/test/index.js
+++ b/test/index.js
@@ -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);
@@ -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});
@@ -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});
@@ -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});
@@ -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 });
@@ -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(`
\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([
+ ``,
+ ``,
+ `
`
+ ].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 ``;