From 4fdb961088391bc211a4f6d12beecef76893b20b Mon Sep 17 00:00:00 2001 From: Hien Hoang Date: Tue, 27 Oct 2026 13:12:13 +0700 Subject: [PATCH] Replace the remaining url.parse() calls with the WHATWG URL API (#1003) --- scripts/filters/post.js | 7 +++---- scripts/helpers/engine.js | 11 ++++++++--- scripts/helpers/next-url.js | 9 ++++----- test/helpers/engine.js | 30 ++++++++++++++++++++++++++++++ test/helpers/next-url.js | 11 +++++++++++ 5 files changed, 56 insertions(+), 12 deletions(-) diff --git a/scripts/filters/post.js b/scripts/filters/post.js index 2faa1f9..ccdf16c 100644 --- a/scripts/filters/post.js +++ b/scripts/filters/post.js @@ -2,7 +2,6 @@ 'use strict'; -const { parse } = require('url'); const { unescapeHTML } = require('hexo-util'); hexo.extend.filter.register('after_post_render', data => { @@ -13,7 +12,7 @@ hexo.extend.filter.register('after_post_render', data => { data.content = data.content.replace(/(]*)\ssrc=/ig, '$1 data-src='); } if (theme.exturl) { - const siteHost = parse(config.url).hostname || config.url; + const siteHost = URL.parse(config.url)?.hostname || config.url; // External URL icon const exturlIcon = theme.exturl_icon ? '' : ''; data.content = data.content.replace(/]*\shref="([^"]+)"[^>]*>([^<]+)<\/a>/ig, (match, href, html) => { @@ -21,8 +20,8 @@ hexo.extend.filter.register('after_post_render', data => { if (!href) return match; // Exit if the url has same host with `config.url`, which means it's an internal link. - const link = parse(href); - if (!link.protocol || link.hostname === siteHost) return match; + const link = URL.parse(href); + if (!link?.protocol || link.hostname === siteHost) return match; // Return encrypted URL with title. const title = match.match(/title="([^"]+)"/); diff --git a/scripts/helpers/engine.js b/scripts/helpers/engine.js index 13b1e7c..4eb419b 100644 --- a/scripts/helpers/engine.js +++ b/scripts/helpers/engine.js @@ -3,7 +3,6 @@ 'use strict'; const crypto = require('crypto'); -const { parse } = require('url'); const nextFont = require('./font'); const nextUrl = require('./next-url'); const { getVendors } = require('../events/lib/utils'); @@ -58,16 +57,22 @@ hexo.extend.helper.register('next_data', function(name, ...data) { }`; }); +// Scheme and hostname of an absolute URL, or '' for a relative one. +function urlOrigin(url) { + const data = url && URL.parse(url); + return data?.hostname ? data.protocol + '//' + data.hostname : ''; +} + hexo.extend.helper.register('next_pre', function() { if (!this.theme.preconnect) return ''; const { enable, host } = this.theme.font; const { internal, plugins, custom_cdn_url } = this.theme.vendors; const links = { - local : this.theme.js && parse(this.theme.js).hostname ? parse(this.theme.js).protocol + '//' + parse(this.theme.js).hostname : '', + local : urlOrigin(this.theme.js), jsdelivr: 'https://cdn.jsdelivr.net', unpkg : 'https://unpkg.com', cdnjs : 'https://cdnjs.cloudflare.com', - custom : custom_cdn_url && parse(custom_cdn_url).hostname ? parse(custom_cdn_url).protocol + '//' + parse(custom_cdn_url).hostname : '' + custom : urlOrigin(custom_cdn_url) }; const h = enable ? host || 'https://fonts.googleapis.com' : ''; const i = links[internal]; diff --git a/scripts/helpers/next-url.js b/scripts/helpers/next-url.js index 9f88b58..08f2505 100644 --- a/scripts/helpers/next-url.js +++ b/scripts/helpers/next-url.js @@ -1,19 +1,18 @@ 'use strict'; const { htmlTag } = require('hexo-util'); -const { parse } = require('url'); module.exports = function(path, text, options = {}, decode = false) { const { config, theme } = this; - const data = parse(path); - const siteHost = parse(config.url).hostname || config.url; + const data = URL.parse(path); + const siteHost = URL.parse(config.url)?.hostname || config.url; let exturl = ''; let tag = 'a'; let attrs = { href: this.url_for(path) }; // If `exturl` enabled, set spanned links only on external links. - if (theme.exturl && data.protocol && data.hostname !== siteHost) { + if (theme.exturl && data?.protocol && data.hostname !== siteHost) { tag = 'span'; exturl = 'exturl'; const encoded = Buffer.from(path).toString('base64'); @@ -37,7 +36,7 @@ module.exports = function(path, text, options = {}, decode = false) { } // If it's external link, rewrite attributes. - if (data.protocol && data.hostname !== siteHost) { + if (data?.protocol && data.hostname !== siteHost) { attrs.external = null; if (!theme.exturl) { diff --git a/test/helpers/engine.js b/test/helpers/engine.js index c66f008..6980c9f 100644 --- a/test/helpers/engine.js +++ b/test/helpers/engine.js @@ -70,4 +70,34 @@ describe('engine', () => { it('does not apply script loading options to stylesheets', () => { nextVendors('stylesheet', { async: true }).should.eql(''); }); + + describe('next_pre', () => { + const nextPre = (theme = {}) => helpers.next_pre.call({ + theme: { + preconnect: true, + js : 'js', + font : { enable: false }, + vendors : { internal: 'local', plugins: 'local', custom_cdn_url: '' }, + ...theme + } + }); + + it('skips a relative local script path', () => { + nextPre().should.eql(''); + }); + + it('uses the scheme and hostname of an absolute local script URL', () => { + nextPre({ js: 'https://static.example.com/next/js' }).should.eql(''); + }); + + it('drops the port and path of a custom CDN URL', () => { + nextPre({ + vendors: { internal: 'custom', plugins: 'custom', custom_cdn_url: 'https://Cdn.Example.com:8443/npm/${npm_name}@${version}/${minified}' } + }).should.eql(''); + }); + + it('skips a protocol-relative custom CDN URL', () => { + nextPre({ vendors: { internal: 'custom', plugins: 'custom', custom_cdn_url: '//cdn.example.com' } }).should.eql(''); + }); + }); }); diff --git a/test/helpers/next-url.js b/test/helpers/next-url.js index e1548b0..c06724b 100644 --- a/test/helpers/next-url.js +++ b/test/helpers/next-url.js @@ -46,4 +46,15 @@ describe('next-url', () => { const encoded = btoa('https://theme-next.js.org'); nextUrl('https://theme-next.js.org', 'Text', { class: 'theme-link' }).should.eql(`Text`); }); + + it('relative and anchor links stay links with exturl enabled', () => { + hexo.theme.exturl = true; + nextUrl('/child/', 'Text').should.eql('Text'); + nextUrl('#comments', 'Text').should.eql('Text'); + }); + + it('an absolute link to the site itself is internal', () => { + hexo.theme.exturl = false; + nextUrl('https://EXAMPLE.com/about/', 'Text').should.eql('Text'); + }); });