100 lines
3.3 KiB
JavaScript
100 lines
3.3 KiB
JavaScript
/**
|
||
* 共享纯函数模块:URL 匹配 + HTML 转义
|
||
* 同时在浏览器(content script / popup)和 Node 测试环境下可用。
|
||
* 浏览器中挂载到全局作用域,Node 中通过 module.exports 暴露。
|
||
*/
|
||
(function (root, factory) {
|
||
if (typeof module === "object" && module.exports) {
|
||
module.exports = factory();
|
||
} else {
|
||
const api = factory();
|
||
root.isDomainMatch = api.isDomainMatch;
|
||
root.escapeHtml = api.escapeHtml;
|
||
}
|
||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||
"use strict";
|
||
|
||
/**
|
||
* 判断当前页面 URL 是否匹配配置的 pattern。
|
||
* 兼容两种格式:
|
||
* 1. 纯 hostname:"example.com" / "*.example.com" —— 只匹配域名
|
||
* 2. hostname + path:"example.com/login" / "*.example.com/admin/*" —— 域名和路径都要匹配
|
||
* 路径部分支持 "*" 通配:
|
||
* - "login" 精确匹配 pathname 为 /login
|
||
* - "login/*" 前缀匹配:/login 或 /login/xxx
|
||
* - "*" 匹配任意路径
|
||
* - "a/* /b" 路径内任意位置通配(转正则)
|
||
* @param {string} url 当前页面完整 URL
|
||
* @param {string} pattern 配置的匹配规则
|
||
* @returns {boolean}
|
||
*/
|
||
function isDomainMatch(url, pattern) {
|
||
if (!url || !pattern) return false;
|
||
try {
|
||
const u = new URL(url);
|
||
const hostname = u.hostname;
|
||
const rawPattern = String(pattern).trim();
|
||
|
||
// 带 / 的规则走 hostname + path 匹配
|
||
if (rawPattern.includes("/")) {
|
||
const slashIdx = rawPattern.indexOf("/");
|
||
const hostPart = rawPattern.slice(0, slashIdx);
|
||
const pathPart = rawPattern.slice(slashIdx + 1) || "";
|
||
if (!matchHost(hostname, hostPart)) return false;
|
||
return matchPath(u.pathname, pathPart);
|
||
}
|
||
|
||
// 纯 hostname 匹配
|
||
return matchHost(hostname, rawPattern);
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function matchHost(hostname, hostPart) {
|
||
if (hostPart.startsWith("*.")) {
|
||
const suffix = hostPart.slice(2);
|
||
return hostname === suffix || hostname.endsWith("." + suffix);
|
||
}
|
||
if (hostPart.includes("*")) {
|
||
const regexStr = hostPart.replace(/\./g, "\\.").replace(/\*/g, ".*");
|
||
return new RegExp("^" + regexStr + "$").test(hostname);
|
||
}
|
||
return hostname === hostPart;
|
||
}
|
||
|
||
function matchPath(pathname, pathPart) {
|
||
// pathname 始终以 / 开头,去掉前导 / 再比较
|
||
const pagePath = pathname.startsWith("/") ? pathname.slice(1) : pathname;
|
||
if (!pathPart) return true;
|
||
if (pathPart === "*") return true;
|
||
// 末尾 /* 表示前缀匹配
|
||
if (pathPart.endsWith("/*")) {
|
||
const prefix = pathPart.slice(0, -2);
|
||
return pagePath === prefix || pagePath.startsWith(prefix + "/");
|
||
}
|
||
if (pathPart.includes("*")) {
|
||
const regexStr = pathPart.replace(/\//g, "\\/").replace(/\*/g, ".*");
|
||
return new RegExp("^" + regexStr + "$").test(pagePath);
|
||
}
|
||
return pagePath === pathPart;
|
||
}
|
||
|
||
/**
|
||
* HTML 转义,防止配置中的特殊字符破坏页面结构。
|
||
* @param {*} str
|
||
* @returns {string}
|
||
*/
|
||
function escapeHtml(str) {
|
||
if (str == null) return "";
|
||
return String(str)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
return { isDomainMatch, escapeHtml };
|
||
});
|