| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| function slice(array, start, end) { |
| if (!array) return []; |
|
|
| if (typeof array === 'string') { |
| return end ? array.slice(start, end) : array.slice(start); |
| } |
|
|
| if (Array.isArray(array)) { |
| return end ? array.slice(start, end) : array.slice(start); |
| } |
|
|
| return []; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function concat() { |
| const args = Array.from(arguments); |
| const options = args.pop(); |
|
|
| |
| const validArrays = args.filter((arg) => Array.isArray(arg)); |
|
|
| if (validArrays.length === 0) { |
| return []; |
| } |
|
|
| return Array.prototype.concat.apply([], validArrays); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function size(value) { |
| if (!value) return 0; |
|
|
| if (Array.isArray(value) || typeof value === 'string') { |
| return value.length; |
| } |
|
|
| if (typeof value === 'object') { |
| return Object.keys(value).length; |
| } |
|
|
| return 0; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function first(array) { |
| if (!array || !Array.isArray(array) || array.length === 0) { |
| return undefined; |
| } |
|
|
| return array[0]; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function last(array) { |
| if (!array || !Array.isArray(array) || array.length === 0) { |
| return undefined; |
| } |
|
|
| return array[array.length - 1]; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| function range(start, end, step = 1) { |
| const result = []; |
|
|
| if (typeof start !== 'number' || typeof end !== 'number') { |
| return result; |
| } |
|
|
| if (step <= 0) { |
| step = 1; |
| } |
|
|
| for (let i = start; i <= end; i += step) { |
| result.push(i); |
| } |
|
|
| return result; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| function pick() { |
| const args = Array.from(arguments); |
| const options = args.pop(); |
|
|
| if (args.length < 1) { |
| return {}; |
| } |
|
|
| const obj = args[0]; |
| const keys = args.slice(1); |
|
|
| if (!obj || typeof obj !== 'object') { |
| return {}; |
| } |
|
|
| const result = {}; |
|
|
| keys.forEach((key) => { |
| if (obj.hasOwnProperty(key)) { |
| result[key] = obj[key]; |
| } |
| }); |
|
|
| return result; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function keys(object) { |
| if (!object || typeof object !== 'object') { |
| return []; |
| } |
|
|
| return Object.keys(object); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function encodeURIComponentHelper(text) { |
| if (text === undefined || text === null) return ''; |
| try { |
| return encodeURIComponent(String(text)); |
| } catch (e) { |
| return ''; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| function add(a, b) { |
| const numA = parseInt(a, 10) || 0; |
| const numB = parseInt(b, 10) || 0; |
| return numA + numB; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| function faviconV2Url(url, options) { |
| if (!url) return ''; |
|
|
| const region = options.data.root.icons?.region || 'com'; |
| const domain = region === 'cn' ? 't3.gstatic.cn' : 't3.gstatic.com'; |
|
|
| try { |
| const encodedUrl = encodeURIComponent(String(url)); |
| |
| return `https://${domain}/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodedUrl}&size=32&drop_404_icon=true`; |
| } catch (e) { |
| return ''; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| function faviconFallbackUrl(url, options) { |
| if (!url) return ''; |
|
|
| const region = options.data.root.icons?.region || 'com'; |
| const domain = region === 'cn' ? 't3.gstatic.com' : 't3.gstatic.cn'; |
|
|
| try { |
| const encodedUrl = encodeURIComponent(String(url)); |
| |
| return `https://${domain}/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodedUrl}&size=32&drop_404_icon=true`; |
| } catch (e) { |
| return ''; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function safeUrl(url, options) { |
| const raw = String(url || '').trim(); |
| if (!raw) return '#'; |
|
|
| |
| if ( |
| raw.startsWith('#') || |
| raw.startsWith('/') || |
| raw.startsWith('./') || |
| raw.startsWith('../') || |
| raw.startsWith('?') |
| ) { |
| return raw; |
| } |
|
|
| |
| if (raw.startsWith('//')) { |
| console.warn(`[WARN] 已拦截不安全 URL(协议相对形式):${raw}`); |
| return '#'; |
| } |
|
|
| const allowedFromConfig = |
| options && |
| options.data && |
| options.data.root && |
| options.data.root.site && |
| options.data.root.site.security && |
| options.data.root.site.security.allowedSchemes; |
|
|
| const allowedSchemes = |
| Array.isArray(allowedFromConfig) && allowedFromConfig.length > 0 |
| ? allowedFromConfig |
| .map((s) => |
| String(s || '') |
| .trim() |
| .toLowerCase() |
| .replace(/:$/, '') |
| ) |
| .filter(Boolean) |
| : ['http', 'https', 'mailto', 'tel']; |
|
|
| try { |
| const parsed = new URL(raw); |
| const scheme = String(parsed.protocol || '') |
| .toLowerCase() |
| .replace(/:$/, ''); |
| if (allowedSchemes.includes(scheme)) return raw; |
| console.warn(`[WARN] 已拦截不安全 URL scheme:${raw}`); |
| return '#'; |
| } catch (e) { |
| console.warn(`[WARN] 已拦截无法解析的 URL:${raw}`); |
| return '#'; |
| } |
| } |
|
|
| |
| module.exports = { |
| slice, |
| concat, |
| size, |
| first, |
| last, |
| range, |
| pick, |
| keys, |
| encodeURIComponent: encodeURIComponentHelper, |
| add, |
| faviconV2Url, |
| faviconFallbackUrl, |
| safeUrl, |
| }; |
|
|