diff --git a/lib/hexo/validate_config.ts b/lib/hexo/validate_config.ts index c7ca6477bd..73f97dede3 100644 --- a/lib/hexo/validate_config.ts +++ b/lib/hexo/validate_config.ts @@ -1,3 +1,4 @@ +import assert from 'assert'; import type Hexo from './index'; export = (ctx: Hexo): void => { @@ -12,6 +13,8 @@ export = (ctx: Hexo): void => { try { // eslint-disable-next-line no-new new URL(config.url); + // eslint-disable-next-line no-new + assert(new URL(config.url).protocol.startsWith('http')); } catch { throw new TypeError('Invalid config detected: "url" should be a valid URL!'); } diff --git a/lib/plugins/filter/after_render/external_link.ts b/lib/plugins/filter/after_render/external_link.ts index 7a0168d842..411502fc17 100644 --- a/lib/plugins/filter/after_render/external_link.ts +++ b/lib/plugins/filter/after_render/external_link.ts @@ -1,5 +1,5 @@ import { isExternalLink } from 'hexo-util'; -import Hexo from '../../../hexo'; +import type Hexo from '../../../hexo'; let EXTERNAL_LINK_SITE_ENABLED = true; const rATag = /]+?\s+?)href=["']((?:https?:|\/\/)[^<>"']+)["'][^<>]*>/gi; @@ -7,6 +7,10 @@ const rTargetAttr = /target=/i; const rRelAttr = /rel=/i; const rRelStrAttr = /rel=["']([^<>"']*)["']/i; +const addNoopener = (relStr: string, rel: string) => { + return rel.includes('noopenner') ? relStr : `rel="${rel} noopener"`; +}; + function externalLinkFilter(this: Hexo, data: string): string { if (!EXTERNAL_LINK_SITE_ENABLED) return; @@ -17,18 +21,30 @@ function externalLinkFilter(this: Hexo, data: string): string { return; } - return data.replace(rATag, (str, href) => { - if (!isExternalLink(href, url, external_link.exclude as any) || rTargetAttr.test(str)) return str; + let result = ''; + let lastIndex = 0; + let match; + + while ((match = rATag.exec(data)) !== null) { + result += data.slice(lastIndex, match.index); - if (rRelAttr.test(str)) { - str = str.replace(rRelStrAttr, (relStr, rel) => { - return rel.includes('noopenner') ? relStr : `rel="${rel} noopener"`; - }); - return str.replace('href=', 'target="_blank" href='); + const str = match[0]; + const href = match[1]; + + if (!isExternalLink(href, url, external_link.exclude as any) || rTargetAttr.test(str)) { + result += str; + } else { + if (rRelAttr.test(str)) { + result += str.replace(rRelStrAttr, addNoopener).replace('href=', 'target="_blank" href='); + } else { + result += str.replace('href=', 'target="_blank" rel="noopener" href='); + } } + lastIndex = rATag.lastIndex; + } + result += data.slice(lastIndex); - return str.replace('href=', 'target="_blank" rel="noopener" href='); - }); + return result; } export = externalLinkFilter; diff --git a/package.json b/package.json index a2bd0843db..e2b76338bd 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "build": "tsc -b", "clean": "tsc -b --clean", "eslint": "eslint lib test", - "pretest": "npm run clean && npm run build", "test": "mocha test/scripts/**/*.ts --require ts-node/register", "test-cov": "c8 --reporter=lcovonly npm test -- --no-parallel", "prepare": "husky" diff --git a/test/scripts/box/file.ts b/test/scripts/box/file.ts index edb485d812..3e3f65b5c4 100644 --- a/test/scripts/box/file.ts +++ b/test/scripts/box/file.ts @@ -38,7 +38,10 @@ describe('File', () => { params: {foo: 'bar'} }); - before(async () => { + // NOTE: Do not use `arrow function` here. + // See https://mochajs.org/#arrow-functions + before(async function() { + this.timeout(20000); await Promise.all([ writeFile(file.source, body), hexo.init() diff --git a/test/scripts/hexo/validate_config.ts b/test/scripts/hexo/validate_config.ts index c414c2a68d..f64d43a22a 100644 --- a/test/scripts/hexo/validate_config.ts +++ b/test/scripts/hexo/validate_config.ts @@ -54,6 +54,20 @@ describe('Validate config', () => { } }); + + it('config.url - not start with xx://', () => { + // @ts-ignore + hexo.config.url = 'localhost:4000'; + + try { + validateConfig(hexo); + should.fail(); + } catch (e) { + e.name.should.eql('TypeError'); + e.message.should.eql('Invalid config detected: "url" should be a valid URL!'); + } + }); + // #4510 it('config.url - slash', () => { hexo.config.url = '/';