me / src /runtime /shared.js
cheymin's picture
Upload 136 files
e1ae2c6 verified
function menavExtractDomain(url) {
if (!url) return '';
try {
// 移除协议部分 (http://, https://, etc.)
let domain = String(url).replace(/^[a-zA-Z]+:\/\//, '');
// 移除路径、查询参数和锚点
domain = domain.split('/')[0].split('?')[0].split('#')[0];
// 移除端口号(如果有)
domain = domain.split(':')[0];
return domain;
} catch (e) {
return String(url);
}
}
// URL 安全策略:默认仅允许 http/https(可加 mailto/tel)与相对链接;其他 scheme 降级为 '#'
function menavGetAllowedUrlSchemes() {
try {
const cfg =
window.MeNav && typeof window.MeNav.getConfig === 'function'
? window.MeNav.getConfig()
: null;
const fromConfig =
cfg &&
cfg.data &&
cfg.data.site &&
cfg.data.site.security &&
cfg.data.site.security.allowedSchemes;
if (Array.isArray(fromConfig) && fromConfig.length > 0) {
return fromConfig
.map((s) =>
String(s || '')
.trim()
.toLowerCase()
.replace(/:$/, '')
)
.filter(Boolean);
}
} catch (e) {
// 忽略,回退默认
}
return ['http', 'https', 'mailto', 'tel'];
}
function menavIsRelativeUrl(url) {
const s = String(url || '').trim();
return (
s.startsWith('#') ||
s.startsWith('/') ||
s.startsWith('./') ||
s.startsWith('../') ||
s.startsWith('?')
);
}
function menavSanitizeUrl(rawUrl, contextLabel) {
if (rawUrl === undefined || rawUrl === null) return '#';
const url = String(rawUrl).trim();
if (!url) return '#';
if (menavIsRelativeUrl(url)) return url;
// 明确拒绝协议相对 URL(//example.com),避免意外绕过策略
if (url.startsWith('//')) {
console.warn(`[MeNav][安全] 已拦截不安全 URL(协议相对形式):${contextLabel || ''}`, url);
return '#';
}
try {
const parsed = new URL(url);
const scheme = String(parsed.protocol || '')
.toLowerCase()
.replace(/:$/, '');
const allowed = menavGetAllowedUrlSchemes();
if (allowed.includes(scheme)) return url;
console.warn(`[MeNav][安全] 已拦截不安全 URL scheme:${contextLabel || ''}`, url);
return '#';
} catch (e) {
console.warn(`[MeNav][安全] 已拦截无法解析的 URL:${contextLabel || ''}`, url);
return '#';
}
}
// class token 清洗:仅允许字母/数字/下划线/中划线与空格分隔,避免属性/事件注入
function menavSanitizeClassList(rawClassList, contextLabel) {
const input = String(rawClassList || '').trim();
if (!input) return '';
const tokens = input
.split(/\s+/g)
.map((t) => t.trim())
.filter(Boolean)
.map((t) => t.replace(/[^\w-]/g, ''))
.filter(Boolean);
const sanitized = tokens.join(' ');
if (sanitized !== input) {
console.warn(`[MeNav][安全] 已清洗不安全的 icon class:${contextLabel || ''}`, rawClassList);
}
return sanitized;
}
// 版本号统一来源:优先读取 meta[menav-version],回退到 menav-config-data.version
function menavDetectVersion() {
try {
const meta = document.querySelector('meta[name="menav-version"]');
const v = meta ? String(meta.getAttribute('content') || '').trim() : '';
if (v) return v;
} catch (e) {
// 忽略
}
try {
const configData = document.getElementById('menav-config-data');
const raw = configData ? String(configData.textContent || '').trim() : '';
if (!raw) return '1.0.0';
const parsed = JSON.parse(raw);
const v = parsed && parsed.version ? String(parsed.version).trim() : '';
return v || '1.0.0';
} catch (e) {
return '1.0.0';
}
}
// 修复移动端 `100vh` 视口高度问题:用实际可视高度驱动布局,避免侧边栏/内容区底部被浏览器 UI 遮挡
function menavUpdateAppHeight() {
const viewportHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight;
document.documentElement.style.setProperty('--app-height', `${Math.round(viewportHeight)}px`);
}
module.exports = {
menavExtractDomain,
menavSanitizeUrl,
menavSanitizeClassList,
menavDetectVersion,
menavUpdateAppHeight,
};