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 `text` +- **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 = `${text} { 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 ``;