chore: 使用 Prettier 统一代码风格
This commit is contained in:
@@ -25,12 +25,16 @@ function ensureUserConfigInitialized() {
|
||||
|
||||
if (fs.existsSync(CONFIG_DEFAULT_DIR)) {
|
||||
fs.cpSync(CONFIG_DEFAULT_DIR, CONFIG_USER_DIR, { recursive: true });
|
||||
console.log('[INFO] 检测到 config/user 不存在,已从 config/_default 初始化用户配置(完全替换策略需要完整配置)。');
|
||||
console.log(
|
||||
'[INFO] 检测到 config/user 不存在,已从 config/_default 初始化用户配置(完全替换策略需要完整配置)。'
|
||||
);
|
||||
return { initialized: true, source: '_default' };
|
||||
}
|
||||
|
||||
fs.mkdirSync(CONFIG_USER_DIR, { recursive: true });
|
||||
console.log('[WARN] 未找到默认配置目录 config/_default,已创建空的 config/user 目录;建议补齐 site.yml 与 pages/*.yml。');
|
||||
console.log(
|
||||
'[WARN] 未找到默认配置目录 config/_default,已创建空的 config/user 目录;建议补齐 site.yml 与 pages/*.yml。'
|
||||
);
|
||||
return { initialized: true, source: 'empty' };
|
||||
}
|
||||
|
||||
@@ -48,7 +52,9 @@ function ensureUserSiteYmlExists() {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('[WARN] 未找到可用的 site.yml,无法自动更新导航;请手动在 config/user/site.yml 添加 navigation(含 id: bookmarks)。');
|
||||
console.log(
|
||||
'[WARN] 未找到可用的 site.yml,无法自动更新导航;请手动在 config/user/site.yml 添加 navigation(含 id: bookmarks)。'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -63,7 +69,7 @@ function upsertBookmarksNavInSiteYml(siteYmlPath) {
|
||||
|
||||
const navigation = loaded.navigation;
|
||||
|
||||
if (Array.isArray(navigation) && navigation.some(item => item && item.id === 'bookmarks')) {
|
||||
if (Array.isArray(navigation) && navigation.some((item) => item && item.id === 'bookmarks')) {
|
||||
return { updated: false, reason: 'already_present' };
|
||||
}
|
||||
|
||||
@@ -72,7 +78,7 @@ function upsertBookmarksNavInSiteYml(siteYmlPath) {
|
||||
}
|
||||
|
||||
const lines = raw.split(/\r?\n/);
|
||||
const navLineIndex = lines.findIndex(line => /^navigation\s*:/.test(line));
|
||||
const navLineIndex = lines.findIndex((line) => /^navigation\s*:/.test(line));
|
||||
|
||||
const itemIndent = ' ';
|
||||
const propIndent = `${itemIndent} `;
|
||||
@@ -149,15 +155,15 @@ const ICON_MAPPING = {
|
||||
'netflix.com': 'fas fa-film',
|
||||
'trello.com': 'fab fa-trello',
|
||||
'wordpress.com': 'fab fa-wordpress',
|
||||
'jira': 'fab fa-jira',
|
||||
jira: 'fab fa-jira',
|
||||
'atlassian.com': 'fab fa-atlassian',
|
||||
'dropbox.com': 'fab fa-dropbox',
|
||||
'npm': 'fab fa-npm',
|
||||
npm: 'fab fa-npm',
|
||||
'docker.com': 'fab fa-docker',
|
||||
'python.org': 'fab fa-python',
|
||||
'javascript': 'fab fa-js',
|
||||
javascript: 'fab fa-js',
|
||||
'php.net': 'fab fa-php',
|
||||
'java': 'fab fa-java',
|
||||
java: 'fab fa-java',
|
||||
'codepen.io': 'fab fa-codepen',
|
||||
'behance.net': 'fab fa-behance',
|
||||
'dribbble.com': 'fab fa-dribbble',
|
||||
@@ -166,10 +172,10 @@ const ICON_MAPPING = {
|
||||
'flickr.com': 'fab fa-flickr',
|
||||
'github.io': 'fab fa-github',
|
||||
'airbnb.com': 'fab fa-airbnb',
|
||||
'bitcoin': 'fab fa-bitcoin',
|
||||
bitcoin: 'fab fa-bitcoin',
|
||||
'paypal.com': 'fab fa-paypal',
|
||||
'ethereum': 'fab fa-ethereum',
|
||||
'steam': 'fab fa-steam',
|
||||
ethereum: 'fab fa-ethereum',
|
||||
steam: 'fab fa-steam',
|
||||
};
|
||||
|
||||
// 获取最新的书签文件
|
||||
@@ -183,8 +189,9 @@ function getLatestBookmarkFile() {
|
||||
}
|
||||
|
||||
// 获取目录中的所有HTML文件
|
||||
const files = fs.readdirSync(BOOKMARKS_DIR)
|
||||
.filter(file => file.toLowerCase().endsWith('.html'));
|
||||
const files = fs
|
||||
.readdirSync(BOOKMARKS_DIR)
|
||||
.filter((file) => file.toLowerCase().endsWith('.html'));
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('[WARN] 未找到任何HTML书签文件');
|
||||
@@ -192,18 +199,18 @@ function getLatestBookmarkFile() {
|
||||
}
|
||||
|
||||
// 获取文件状态,按最后修改时间排序
|
||||
const fileStats = files.map(file => ({
|
||||
const fileStats = files.map((file) => ({
|
||||
file,
|
||||
mtime: fs.statSync(path.join(BOOKMARKS_DIR, file)).mtime
|
||||
mtime: fs.statSync(path.join(BOOKMARKS_DIR, file)).mtime,
|
||||
}));
|
||||
|
||||
// 找出最新的文件
|
||||
fileStats.sort((a, b) => b.mtime - a.mtime);
|
||||
const latestFile = fileStats[0].file;
|
||||
const latestFilePath = path.join(BOOKMARKS_DIR, latestFile);
|
||||
|
||||
|
||||
console.log('[INFO] 选择最新的书签文件:', latestFile);
|
||||
|
||||
|
||||
return latestFilePath;
|
||||
} catch (error) {
|
||||
console.error('[ERROR] 查找书签文件时出错:', error);
|
||||
@@ -213,44 +220,45 @@ function getLatestBookmarkFile() {
|
||||
|
||||
// 解析书签HTML内容,支持2-4层级嵌套结构
|
||||
function parseBookmarks(htmlContent) {
|
||||
|
||||
// 正则表达式匹配文件夹和书签
|
||||
const folderRegex = /<DT><H3([^>]*)>(.*?)<\/H3>/g;
|
||||
const bookmarkRegex = /<DT><A HREF="([^"]+)"[^>]*>(.*?)<\/A>/g;
|
||||
|
||||
|
||||
// 储存解析结果
|
||||
const bookmarks = {
|
||||
categories: []
|
||||
categories: [],
|
||||
};
|
||||
|
||||
|
||||
// 提取根路径书签(书签栏容器内但不在任何子文件夹内的书签)
|
||||
function extractRootBookmarks(htmlContent) {
|
||||
// 找到书签栏文件夹标签
|
||||
const bookmarkBarMatch = htmlContent.match(/<DT><H3[^>]*PERSONAL_TOOLBAR_FOLDER[^>]*>([^<]+)<\/H3>/i);
|
||||
const bookmarkBarMatch = htmlContent.match(
|
||||
/<DT><H3[^>]*PERSONAL_TOOLBAR_FOLDER[^>]*>([^<]+)<\/H3>/i
|
||||
);
|
||||
if (!bookmarkBarMatch) {
|
||||
return [];
|
||||
}
|
||||
const bookmarkBarStart = bookmarkBarMatch.index + bookmarkBarMatch[0].length;
|
||||
|
||||
|
||||
// 找到书签栏后面的 <DL><p> 标签
|
||||
const remainingAfterBar = htmlContent.substring(bookmarkBarStart);
|
||||
const dlMatch = remainingAfterBar.match(/<DL><p>/i);
|
||||
if (!dlMatch) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
const bookmarkBarContentStart = bookmarkBarStart + dlMatch.index + dlMatch[0].length;
|
||||
|
||||
|
||||
// 找到书签栏内容的结束位置
|
||||
let depth = 1;
|
||||
let pos = bookmarkBarContentStart;
|
||||
let bookmarkBarContentEnd = htmlContent.length;
|
||||
|
||||
|
||||
while (pos < htmlContent.length && depth > 0) {
|
||||
const remaining = htmlContent.substring(pos);
|
||||
const dlStartIndex = remaining.search(/<DL><p>/i);
|
||||
const dlEndIndex = remaining.search(/<\/DL><p>/i);
|
||||
|
||||
|
||||
if (dlStartIndex !== -1 && (dlEndIndex === -1 || dlStartIndex < dlEndIndex)) {
|
||||
depth++;
|
||||
pos += dlStartIndex + '<DL><p>'.length;
|
||||
@@ -265,35 +273,38 @@ function parseBookmarks(htmlContent) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const bookmarkBarContent = htmlContent.substring(bookmarkBarContentStart, bookmarkBarContentEnd);
|
||||
|
||||
|
||||
const bookmarkBarContent = htmlContent.substring(
|
||||
bookmarkBarContentStart,
|
||||
bookmarkBarContentEnd
|
||||
);
|
||||
|
||||
// 现在提取书签栏内所有子文件夹的范围
|
||||
const subfolderRanges = [];
|
||||
const folderRegex = /<DT><H3[^>]*>([^<]+)<\/H3>/g;
|
||||
let folderMatch;
|
||||
|
||||
|
||||
while ((folderMatch = folderRegex.exec(bookmarkBarContent)) !== null) {
|
||||
const folderName = folderMatch[1].trim();
|
||||
const folderStart = folderMatch.index + folderMatch[0].length;
|
||||
|
||||
|
||||
// 找到这个文件夹内容的结束位置
|
||||
let folderDepth = 0;
|
||||
let folderPos = folderStart;
|
||||
let folderContentEnd = bookmarkBarContent.length;
|
||||
|
||||
|
||||
// 跳过空白直到找到 <DL><p>
|
||||
const afterFolder = bookmarkBarContent.substring(folderPos);
|
||||
const folderDLMatch = afterFolder.match(/<DL><p>/i);
|
||||
if (folderDLMatch) {
|
||||
folderDepth = 1;
|
||||
folderPos += folderDLMatch.index + folderDLMatch[0].length;
|
||||
|
||||
|
||||
while (folderPos < bookmarkBarContent.length && folderDepth > 0) {
|
||||
const remaining = bookmarkBarContent.substring(folderPos);
|
||||
const dlStartIdx = remaining.search(/<DL><p>/i);
|
||||
const dlEndIdx = remaining.search(/<\/DL><p>/i);
|
||||
|
||||
|
||||
if (dlStartIdx !== -1 && (dlEndIdx === -1 || dlStartIdx < dlEndIdx)) {
|
||||
folderDepth++;
|
||||
folderPos += dlStartIdx + '<DL><p>'.length;
|
||||
@@ -308,25 +319,25 @@ function parseBookmarks(htmlContent) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
subfolderRanges.push({
|
||||
name: folderName,
|
||||
start: folderMatch.index,
|
||||
end: folderContentEnd
|
||||
end: folderContentEnd,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 提取不在任何子文件夹范围内的书签
|
||||
const rootSites = [];
|
||||
const bookmarkRegex = /<DT><A HREF="([^"]+)"[^>]*>(.*?)<\/A>/g;
|
||||
let bookmarkMatch;
|
||||
|
||||
|
||||
while ((bookmarkMatch = bookmarkRegex.exec(bookmarkBarContent)) !== null) {
|
||||
const bookmarkPos = bookmarkMatch.index;
|
||||
const url = bookmarkMatch[1];
|
||||
const name = bookmarkMatch[2].trim();
|
||||
|
||||
|
||||
// 检查这个书签是否在任何子文件夹范围内
|
||||
let inFolder = false;
|
||||
for (const folder of subfolderRanges) {
|
||||
@@ -335,9 +346,8 @@ function parseBookmarks(htmlContent) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!inFolder) {
|
||||
|
||||
// 基于URL选择适当的图标
|
||||
let icon = 'fas fa-link';
|
||||
for (const [keyword, iconClass] of Object.entries(ICON_MAPPING)) {
|
||||
@@ -346,49 +356,49 @@ function parseBookmarks(htmlContent) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
rootSites.push({
|
||||
name: name,
|
||||
url: url,
|
||||
icon: icon,
|
||||
description: ''
|
||||
description: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return rootSites;
|
||||
}
|
||||
|
||||
|
||||
// 递归解析嵌套文件夹
|
||||
function parseNestedFolder(htmlContent, parentPath = [], level = 1) {
|
||||
const folders = [];
|
||||
|
||||
|
||||
// 第一步:扫描所有文件夹,记录它们的完整范围
|
||||
const folderRanges = [];
|
||||
const scanRegex = /<DT><H3([^>]*)>(.*?)<\/H3>/g;
|
||||
let scanMatch;
|
||||
|
||||
|
||||
while ((scanMatch = scanRegex.exec(htmlContent)) !== null) {
|
||||
const folderName = scanMatch[2].trim();
|
||||
const folderStart = scanMatch.index;
|
||||
const folderHeaderEnd = scanMatch.index + scanMatch[0].length;
|
||||
|
||||
|
||||
// 找到文件夹内容的结束位置
|
||||
let depth = 0;
|
||||
let pos = folderHeaderEnd;
|
||||
|
||||
|
||||
// 跳过空白直到找到 <DL><p>
|
||||
const afterFolder = htmlContent.substring(pos);
|
||||
const folderDLMatch = afterFolder.match(/<DL><p>/i);
|
||||
if (folderDLMatch) {
|
||||
depth = 1;
|
||||
pos += folderDLMatch.index + folderDLMatch[0].length;
|
||||
|
||||
|
||||
while (pos < htmlContent.length && depth > 0) {
|
||||
const remaining = htmlContent.substring(pos);
|
||||
const dlStartIdx = remaining.search(/<DL><p>/i);
|
||||
const dlEndIdx = remaining.search(/<\/DL><p>/i);
|
||||
|
||||
|
||||
if (dlStartIdx !== -1 && (dlEndIdx === -1 || dlStartIdx < dlEndIdx)) {
|
||||
depth++;
|
||||
pos += dlStartIdx + '<DL><p>'.length;
|
||||
@@ -401,7 +411,7 @@ function parseBookmarks(htmlContent) {
|
||||
name: folderName,
|
||||
start: folderStart,
|
||||
headerEnd: folderHeaderEnd,
|
||||
end: folderEnd
|
||||
end: folderEnd,
|
||||
});
|
||||
}
|
||||
pos += '</DL><p>'.length;
|
||||
@@ -411,16 +421,16 @@ function parseBookmarks(htmlContent) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 第二步:只处理当前层级的文件夹(不在其他文件夹内部的)
|
||||
for (let i = 0; i < folderRanges.length; i++) {
|
||||
const currentFolder = folderRanges[i];
|
||||
|
||||
|
||||
// 检查这个文件夹是否在其他文件夹内部
|
||||
let isNested = false;
|
||||
for (let j = 0; j < folderRanges.length; j++) {
|
||||
if (i === j) continue; // 跳过自己
|
||||
|
||||
|
||||
const otherFolder = folderRanges[j];
|
||||
// 如果当前文件夹的起始位置在另一个文件夹的范围内,说明它是嵌套的
|
||||
if (currentFolder.start > otherFolder.start && currentFolder.end <= otherFolder.end) {
|
||||
@@ -428,43 +438,43 @@ function parseBookmarks(htmlContent) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (isNested) {
|
||||
continue; // 跳过嵌套的文件夹,它们会被递归调用处理
|
||||
}
|
||||
|
||||
|
||||
const folderName = currentFolder.name;
|
||||
const folderStart = currentFolder.start;
|
||||
const folderHeaderEnd = currentFolder.headerEnd;
|
||||
const folderEnd = currentFolder.end;
|
||||
|
||||
|
||||
// 提取文件夹内容(保留完整的HTML结构供递归使用)
|
||||
// 从headerEnd到end之间包含完整的<DL><p>...</DL><p>结构
|
||||
const folderContent = htmlContent.substring(folderHeaderEnd, folderEnd);
|
||||
|
||||
|
||||
// 验证是否有有效的容器结构
|
||||
if (!/<DL><p>/i.test(folderContent)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// 解析文件夹内容
|
||||
const folder = {
|
||||
name: folderName,
|
||||
icon: 'fas fa-folder',
|
||||
path: [...parentPath, folderName]
|
||||
path: [...parentPath, folderName],
|
||||
};
|
||||
|
||||
|
||||
// 检查是否包含子文件夹 - 创建新的正则实例避免干扰主循环
|
||||
const testFolderRegex = /<DT><H3([^>]*)>(.*?)<\/H3>/;
|
||||
const hasSubfolders = testFolderRegex.test(folderContent);
|
||||
|
||||
|
||||
// 先解析当前层级的书签
|
||||
const currentLevelSites = parseSitesInFolder(folderContent, folderName);
|
||||
|
||||
|
||||
if (hasSubfolders && level < 4) {
|
||||
// 递归解析子文件夹
|
||||
const subfolders = parseNestedFolder(folderContent, folder.path, level + 1);
|
||||
|
||||
|
||||
// 根据层级深度决定数据结构
|
||||
if (level === 1) {
|
||||
folder.subcategories = subfolders;
|
||||
@@ -473,7 +483,7 @@ function parseBookmarks(htmlContent) {
|
||||
} else if (level === 3) {
|
||||
folder.subgroups = subfolders;
|
||||
}
|
||||
|
||||
|
||||
// 添加当前层级的书签(如果有)
|
||||
if (currentLevelSites.length > 0) {
|
||||
folder.sites = currentLevelSites;
|
||||
@@ -482,53 +492,54 @@ function parseBookmarks(htmlContent) {
|
||||
// 解析书签
|
||||
folder.sites = currentLevelSites;
|
||||
}
|
||||
|
||||
|
||||
// 只添加包含内容的文件夹
|
||||
const hasContent = folder.sites && folder.sites.length > 0 ||
|
||||
folder.subcategories && folder.subcategories.length > 0 ||
|
||||
folder.groups && folder.groups.length > 0 ||
|
||||
folder.subgroups && folder.subgroups.length > 0;
|
||||
|
||||
const hasContent =
|
||||
(folder.sites && folder.sites.length > 0) ||
|
||||
(folder.subcategories && folder.subcategories.length > 0) ||
|
||||
(folder.groups && folder.groups.length > 0) ||
|
||||
(folder.subgroups && folder.subgroups.length > 0);
|
||||
|
||||
if (hasContent) {
|
||||
folders.push(folder);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return folders;
|
||||
}
|
||||
|
||||
|
||||
// 解析文件夹中的书签(仅当前层级,排除子文件夹内的书签)
|
||||
function parseSitesInFolder(folderContent) {
|
||||
const sites = [];
|
||||
let siteCount = 0;
|
||||
|
||||
|
||||
// 首先找到所有子文件夹的范围
|
||||
const subfolderRanges = [];
|
||||
const folderRegex = /<DT><H3[^>]*>([^<]+)<\/H3>/g;
|
||||
let folderMatch;
|
||||
|
||||
|
||||
while ((folderMatch = folderRegex.exec(folderContent)) !== null) {
|
||||
const folderName = folderMatch[1].trim();
|
||||
const folderStart = folderMatch.index;
|
||||
const folderHeaderEnd = folderMatch.index + folderMatch[0].length;
|
||||
|
||||
|
||||
// 找到这个文件夹内容的结束位置
|
||||
let folderDepth = 0;
|
||||
let folderPos = folderHeaderEnd;
|
||||
let folderContentEnd = folderContent.length;
|
||||
|
||||
|
||||
// 跳过空白直到找到 <DL><p>
|
||||
const afterFolder = folderContent.substring(folderPos);
|
||||
const folderDLMatch = afterFolder.match(/<DL><p>/i);
|
||||
if (folderDLMatch) {
|
||||
folderDepth = 1;
|
||||
folderPos += folderDLMatch.index + folderDLMatch[0].length;
|
||||
|
||||
|
||||
while (folderPos < folderContent.length && folderDepth > 0) {
|
||||
const remaining = folderContent.substring(folderPos);
|
||||
const dlStartIdx = remaining.search(/<DL><p>/i);
|
||||
const dlEndIdx = remaining.search(/<\/DL><p>/i);
|
||||
|
||||
|
||||
if (dlStartIdx !== -1 && (dlEndIdx === -1 || dlStartIdx < dlEndIdx)) {
|
||||
folderDepth++;
|
||||
folderPos += dlStartIdx + '<DL><p>'.length;
|
||||
@@ -543,24 +554,24 @@ function parseBookmarks(htmlContent) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
subfolderRanges.push({
|
||||
name: folderName,
|
||||
start: folderStart,
|
||||
end: folderContentEnd
|
||||
end: folderContentEnd,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 现在提取不在任何子文件夹范围内的书签
|
||||
const bookmarkRegex = /<DT><A HREF="([^"]+)"[^>]*>(.*?)<\/A>/g;
|
||||
let bookmarkMatch;
|
||||
|
||||
|
||||
while ((bookmarkMatch = bookmarkRegex.exec(folderContent)) !== null) {
|
||||
const bookmarkPos = bookmarkMatch.index;
|
||||
const url = bookmarkMatch[1];
|
||||
const name = bookmarkMatch[2].trim();
|
||||
|
||||
|
||||
// 检查这个书签是否在任何子文件夹范围内
|
||||
let inSubfolder = false;
|
||||
for (const folder of subfolderRanges) {
|
||||
@@ -569,9 +580,8 @@ function parseBookmarks(htmlContent) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!inSubfolder) {
|
||||
|
||||
// 基于URL选择适当的图标
|
||||
let icon = 'fas fa-link'; // 默认图标
|
||||
for (const [keyword, iconClass] of Object.entries(ICON_MAPPING)) {
|
||||
@@ -580,24 +590,26 @@ function parseBookmarks(htmlContent) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sites.push({
|
||||
name: name,
|
||||
url: url,
|
||||
icon: icon,
|
||||
description: ''
|
||||
description: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return sites;
|
||||
}
|
||||
|
||||
|
||||
// 开始解析
|
||||
const rootSites = extractRootBookmarks(htmlContent);
|
||||
|
||||
|
||||
// 找到书签栏文件夹(PERSONAL_TOOLBAR_FOLDER)
|
||||
const bookmarkBarMatch = htmlContent.match(/<DT><H3[^>]*PERSONAL_TOOLBAR_FOLDER[^>]*>([^<]+)<\/H3>/i);
|
||||
const bookmarkBarMatch = htmlContent.match(
|
||||
/<DT><H3[^>]*PERSONAL_TOOLBAR_FOLDER[^>]*>([^<]+)<\/H3>/i
|
||||
);
|
||||
if (!bookmarkBarMatch) {
|
||||
console.log('[WARN] 未找到书签栏文件夹(PERSONAL_TOOLBAR_FOLDER),使用备用方案');
|
||||
// 备用方案:使用第一个 <DL><p> 标签
|
||||
@@ -610,12 +622,12 @@ function parseBookmarks(htmlContent) {
|
||||
let dlEnd = htmlContent.length;
|
||||
let depth = 1;
|
||||
let pos = dlStart;
|
||||
|
||||
|
||||
while (pos < htmlContent.length && depth > 0) {
|
||||
const remainingContent = htmlContent.substring(pos);
|
||||
const dlStartIndex = remainingContent.search(/<DL><p>/i);
|
||||
const dlEndIndex = remainingContent.search(/<\/DL><p>/i);
|
||||
|
||||
|
||||
if (dlStartIndex !== -1 && (dlEndIndex === -1 || dlStartIndex < dlEndIndex)) {
|
||||
depth++;
|
||||
pos += dlStartIndex + '<DL><p>'.length;
|
||||
@@ -626,14 +638,14 @@ function parseBookmarks(htmlContent) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dlEnd = pos - '</DL><p>'.length;
|
||||
const bookmarksBarContent = htmlContent.substring(dlStart, dlEnd);
|
||||
bookmarks.categories = parseNestedFolder(bookmarksBarContent);
|
||||
}
|
||||
} else {
|
||||
const bookmarkBarStart = bookmarkBarMatch.index + bookmarkBarMatch[0].length;
|
||||
|
||||
|
||||
// 找到书签栏后面的 <DL><p> 标签
|
||||
const remainingAfterBar = htmlContent.substring(bookmarkBarStart);
|
||||
const dlMatch = remainingAfterBar.match(/<DL><p>/i);
|
||||
@@ -642,17 +654,17 @@ function parseBookmarks(htmlContent) {
|
||||
bookmarks.categories = [];
|
||||
} else {
|
||||
const bookmarkBarContentStart = bookmarkBarStart + dlMatch.index + dlMatch[0].length;
|
||||
|
||||
|
||||
// 找到书签栏内容的结束位置
|
||||
let depth = 1;
|
||||
let pos = bookmarkBarContentStart;
|
||||
let bookmarkBarContentEnd = htmlContent.length;
|
||||
|
||||
|
||||
while (pos < htmlContent.length && depth > 0) {
|
||||
const remaining = htmlContent.substring(pos);
|
||||
const dlStartIndex = remaining.search(/<DL><p>/i);
|
||||
const dlEndIndex = remaining.search(/<\/DL><p>/i);
|
||||
|
||||
|
||||
if (dlStartIndex !== -1 && (dlEndIndex === -1 || dlStartIndex < dlEndIndex)) {
|
||||
depth++;
|
||||
pos += dlStartIndex + '<DL><p>'.length;
|
||||
@@ -667,16 +679,19 @@ function parseBookmarks(htmlContent) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const bookmarkBarContent = htmlContent.substring(bookmarkBarContentStart, bookmarkBarContentEnd);
|
||||
|
||||
|
||||
const bookmarkBarContent = htmlContent.substring(
|
||||
bookmarkBarContentStart,
|
||||
bookmarkBarContentEnd
|
||||
);
|
||||
|
||||
// 解析书签栏内的子文件夹作为顶层分类(跳过书签栏本身)
|
||||
bookmarks.categories = parseNestedFolder(bookmarkBarContent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log(`[INFO] 解析完成 - 共找到 ${bookmarks.categories.length} 个顶层分类`);
|
||||
|
||||
|
||||
// 如果存在根路径书签,创建"根目录书签"特殊分类并插入到首位
|
||||
if (rootSites.length > 0) {
|
||||
console.log(`[INFO] 创建"根目录书签"特殊分类,包含 ${rootSites.length} 个书签`);
|
||||
@@ -684,14 +699,14 @@ function parseBookmarks(htmlContent) {
|
||||
name: '根目录书签',
|
||||
icon: 'fas fa-star',
|
||||
path: ['根目录书签'],
|
||||
sites: rootSites
|
||||
sites: rootSites,
|
||||
};
|
||||
|
||||
|
||||
// 插入到数组首位
|
||||
bookmarks.categories.unshift(rootCategory);
|
||||
console.log(`[INFO] "根目录书签"已插入到分类列表首位`);
|
||||
}
|
||||
|
||||
|
||||
return bookmarks;
|
||||
}
|
||||
|
||||
@@ -702,29 +717,28 @@ function generateBookmarksYaml(bookmarks) {
|
||||
const bookmarksPage = {
|
||||
title: '我的书签',
|
||||
subtitle: '从浏览器导入的书签收藏',
|
||||
categories: bookmarks.categories
|
||||
categories: bookmarks.categories,
|
||||
};
|
||||
|
||||
|
||||
// 转换为YAML
|
||||
const yamlString = yaml.dump(bookmarksPage, {
|
||||
indent: 2,
|
||||
lineWidth: -1,
|
||||
quotingType: '"'
|
||||
quotingType: '"',
|
||||
});
|
||||
|
||||
|
||||
// 添加注释(可选确定性输出,方便版本管理)
|
||||
const deterministic = process.env.MENAV_BOOKMARKS_DETERMINISTIC === '1';
|
||||
const timestampLine = deterministic
|
||||
? ''
|
||||
: `# 由bookmark-processor.js生成于 ${new Date().toISOString()}\n`;
|
||||
|
||||
const yamlWithComment =
|
||||
`# 自动生成的书签配置文件
|
||||
const yamlWithComment = `# 自动生成的书签配置文件
|
||||
${timestampLine}# 若要更新,请将新的书签HTML文件放入bookmarks/目录
|
||||
# 此文件使用模块化配置格式,位于config/user/pages/目录下
|
||||
|
||||
${yamlString}`;
|
||||
|
||||
|
||||
return yamlWithComment;
|
||||
} catch (error) {
|
||||
console.error('Error generating YAML:', error);
|
||||
@@ -757,7 +771,7 @@ async function main() {
|
||||
console.log('[INFO] 书签处理脚本启动');
|
||||
console.log('[INFO] 时间:', new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }));
|
||||
console.log('========================================\n');
|
||||
|
||||
|
||||
// 获取最新的书签文件
|
||||
console.log('[步骤 1/5] 查找书签文件...');
|
||||
const bookmarkFile = getLatestBookmarkFile();
|
||||
@@ -766,13 +780,13 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
console.log('[SUCCESS] 找到书签文件\n');
|
||||
|
||||
|
||||
try {
|
||||
// 读取文件内容
|
||||
console.log('[步骤 2/5] 读取书签文件...');
|
||||
const htmlContent = fs.readFileSync(bookmarkFile, 'utf8');
|
||||
console.log('[SUCCESS] 文件读取成功,大小:', htmlContent.length, '字符\n');
|
||||
|
||||
|
||||
// 解析书签
|
||||
console.log('[步骤 3/5] 解析书签结构...');
|
||||
const bookmarks = parseBookmarks(htmlContent);
|
||||
@@ -781,7 +795,7 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
console.log('[SUCCESS] 解析完成\n');
|
||||
|
||||
|
||||
// 生成YAML
|
||||
console.log('[步骤 4/5] 生成YAML配置...');
|
||||
const yamlContent = generateBookmarksYaml(bookmarks);
|
||||
@@ -790,7 +804,7 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
console.log('[SUCCESS] YAML生成成功\n');
|
||||
|
||||
|
||||
// 保存文件
|
||||
console.log('[步骤 5/5] 保存配置文件...');
|
||||
try {
|
||||
@@ -801,19 +815,19 @@ async function main() {
|
||||
if (!fs.existsSync(CONFIG_USER_PAGES_DIR)) {
|
||||
fs.mkdirSync(CONFIG_USER_PAGES_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
// 保存YAML到模块化位置
|
||||
fs.writeFileSync(MODULAR_OUTPUT_FILE, yamlContent, 'utf8');
|
||||
|
||||
|
||||
// 验证文件是否确实被创建
|
||||
if (!fs.existsSync(MODULAR_OUTPUT_FILE)) {
|
||||
console.error(`[ERROR] 文件未能创建: ${MODULAR_OUTPUT_FILE}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
console.log('[SUCCESS] 文件保存成功');
|
||||
console.log('[INFO] 输出文件:', MODULAR_OUTPUT_FILE, '\n');
|
||||
|
||||
|
||||
// 更新导航
|
||||
console.log('[附加步骤] 更新导航配置...');
|
||||
const navUpdateResult = updateNavigationWithBookmarks();
|
||||
@@ -822,24 +836,24 @@ async function main() {
|
||||
} else if (navUpdateResult.reason === 'already_present') {
|
||||
console.log('[INFO] 导航配置已包含书签入口,无需更新\n');
|
||||
} else if (navUpdateResult.reason === 'no_navigation_config') {
|
||||
console.log('[WARN] 未找到可更新的导航配置文件;请确认 config/user/site.yml 存在且包含 navigation\n');
|
||||
console.log(
|
||||
'[WARN] 未找到可更新的导航配置文件;请确认 config/user/site.yml 存在且包含 navigation\n'
|
||||
);
|
||||
} else if (navUpdateResult.reason === 'error') {
|
||||
console.log('[WARN] 导航更新失败,请手动检查配置文件格式(详见错误信息)\n');
|
||||
console.error(navUpdateResult.error);
|
||||
} else {
|
||||
console.log('[INFO] 导航配置无需更新\n');
|
||||
}
|
||||
|
||||
} catch (writeError) {
|
||||
console.error(`[ERROR] 写入文件时出错:`, writeError);
|
||||
console.error('[ERROR] 错误堆栈:', writeError.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
console.log('========================================');
|
||||
console.log('[SUCCESS] 书签处理完成!');
|
||||
console.log('========================================');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[FATAL] 处理书签文件时发生错误:', error);
|
||||
console.error('[ERROR] 错误堆栈:', error.stack);
|
||||
@@ -849,7 +863,7 @@ async function main() {
|
||||
|
||||
// 启动处理
|
||||
if (require.main === module) {
|
||||
main().catch(err => {
|
||||
main().catch((err) => {
|
||||
console.error('Unhandled error in bookmark processing:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
897
src/generator.js
897
src/generator.js
File diff suppressed because it is too large
Load Diff
@@ -39,25 +39,25 @@ function ifNotEquals(v1, v2, options) {
|
||||
function ifCond(v1, operator, v2, options) {
|
||||
switch (operator) {
|
||||
case '==':
|
||||
return (v1 == v2) ? options.fn(this) : options.inverse(this);
|
||||
return v1 == v2 ? options.fn(this) : options.inverse(this);
|
||||
case '===':
|
||||
return (v1 === v2) ? options.fn(this) : options.inverse(this);
|
||||
return v1 === v2 ? options.fn(this) : options.inverse(this);
|
||||
case '!=':
|
||||
return (v1 != v2) ? options.fn(this) : options.inverse(this);
|
||||
return v1 != v2 ? options.fn(this) : options.inverse(this);
|
||||
case '!==':
|
||||
return (v1 !== v2) ? options.fn(this) : options.inverse(this);
|
||||
return v1 !== v2 ? options.fn(this) : options.inverse(this);
|
||||
case '<':
|
||||
return (v1 < v2) ? options.fn(this) : options.inverse(this);
|
||||
return v1 < v2 ? options.fn(this) : options.inverse(this);
|
||||
case '<=':
|
||||
return (v1 <= v2) ? options.fn(this) : options.inverse(this);
|
||||
return v1 <= v2 ? options.fn(this) : options.inverse(this);
|
||||
case '>':
|
||||
return (v1 > v2) ? options.fn(this) : options.inverse(this);
|
||||
return v1 > v2 ? options.fn(this) : options.inverse(this);
|
||||
case '>=':
|
||||
return (v1 >= v2) ? options.fn(this) : options.inverse(this);
|
||||
return v1 >= v2 ? options.fn(this) : options.inverse(this);
|
||||
case '&&':
|
||||
return (v1 && v2) ? options.fn(this) : options.inverse(this);
|
||||
return v1 && v2 ? options.fn(this) : options.inverse(this);
|
||||
case '||':
|
||||
return (v1 || v2) ? options.fn(this) : options.inverse(this);
|
||||
return v1 || v2 ? options.fn(this) : options.inverse(this);
|
||||
default:
|
||||
return options.inverse(this);
|
||||
}
|
||||
@@ -74,19 +74,19 @@ function isEmpty(value, options) {
|
||||
if (value === null || value === undefined) {
|
||||
return options.fn(this);
|
||||
}
|
||||
|
||||
|
||||
if (typeof value === 'string' && value.trim() === '') {
|
||||
return options.fn(this);
|
||||
}
|
||||
|
||||
|
||||
if (Array.isArray(value) && value.length === 0) {
|
||||
return options.fn(this);
|
||||
}
|
||||
|
||||
|
||||
if (typeof value === 'object' && Object.keys(value).length === 0) {
|
||||
return options.fn(this);
|
||||
}
|
||||
|
||||
|
||||
return options.inverse(this);
|
||||
}
|
||||
|
||||
@@ -98,9 +98,9 @@ function isEmpty(value, options) {
|
||||
* @example {{#isNotEmpty items}}有内容{{else}}无内容{{/isNotEmpty}}
|
||||
*/
|
||||
function isNotEmpty(value, options) {
|
||||
return isEmpty(value, {
|
||||
return isEmpty(value, {
|
||||
fn: options.inverse,
|
||||
inverse: options.fn
|
||||
inverse: options.fn,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ function isNotEmpty(value, options) {
|
||||
* @example {{#and isPremium isActive}}高级活跃用户{{else}}其他用户{{/and}}
|
||||
*/
|
||||
function and(a, b, options) {
|
||||
return (a && b) ? options.fn(this) : options.inverse(this);
|
||||
return a && b ? options.fn(this) : options.inverse(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,7 +125,7 @@ function and(a, b, options) {
|
||||
* @example {{#or isPremium isAdmin}}有权限{{else}}无权限{{/or}}
|
||||
*/
|
||||
function or(a, b, options) {
|
||||
return (a || b) ? options.fn(this) : options.inverse(this);
|
||||
return a || b ? options.fn(this) : options.inverse(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,14 +137,14 @@ function or(a, b, options) {
|
||||
function orHelper() {
|
||||
// 最后一个参数是options对象
|
||||
const options = arguments[arguments.length - 1];
|
||||
|
||||
|
||||
// 检查是否至少有一个为true的参数
|
||||
for (let i = 0; i < arguments.length - 1; i++) {
|
||||
if (arguments[i]) {
|
||||
return options.fn(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return options.inverse(this);
|
||||
}
|
||||
|
||||
@@ -184,5 +184,5 @@ module.exports = {
|
||||
or,
|
||||
orHelper,
|
||||
not,
|
||||
ifHttpUrl
|
||||
ifHttpUrl,
|
||||
};
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
*/
|
||||
function formatDate(date, format) {
|
||||
if (!date) return '';
|
||||
|
||||
|
||||
// 将字符串转换为日期对象
|
||||
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
||||
|
||||
|
||||
if (!(dateObj instanceof Date) || isNaN(dateObj)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
// 获取日期组件
|
||||
const year = dateObj.getFullYear();
|
||||
const month = dateObj.getMonth() + 1;
|
||||
@@ -27,10 +27,10 @@ function formatDate(date, format) {
|
||||
const hours = dateObj.getHours();
|
||||
const minutes = dateObj.getMinutes();
|
||||
const seconds = dateObj.getSeconds();
|
||||
|
||||
|
||||
// 格式化日期字符串
|
||||
if (!format) format = 'YYYY-MM-DD';
|
||||
|
||||
|
||||
return format
|
||||
.replace('YYYY', year)
|
||||
.replace('MM', month.toString().padStart(2, '0'))
|
||||
@@ -49,13 +49,13 @@ function formatDate(date, format) {
|
||||
*/
|
||||
function limit(text, length) {
|
||||
if (!text) return '';
|
||||
|
||||
|
||||
text = String(text);
|
||||
|
||||
|
||||
if (text.length <= length) {
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
return text.substring(0, length) + '...';
|
||||
}
|
||||
|
||||
@@ -99,17 +99,17 @@ function json(obj) {
|
||||
*/
|
||||
function extractDomain(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);
|
||||
@@ -123,5 +123,5 @@ module.exports = {
|
||||
toLowerCase,
|
||||
toUpperCase,
|
||||
json,
|
||||
extractDomain
|
||||
};
|
||||
extractDomain,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Handlebars助手函数中心
|
||||
*
|
||||
*
|
||||
* 导入并重导出所有助手函数,方便在generator中统一注册
|
||||
*/
|
||||
|
||||
@@ -17,32 +17,32 @@ function registerAllHelpers(handlebars) {
|
||||
Object.entries(formatters).forEach(([name, helper]) => {
|
||||
handlebars.registerHelper(name, helper);
|
||||
});
|
||||
|
||||
|
||||
// 注册条件判断助手函数
|
||||
Object.entries(conditions).forEach(([name, helper]) => {
|
||||
handlebars.registerHelper(name, helper);
|
||||
});
|
||||
|
||||
|
||||
// 注册工具类助手函数
|
||||
Object.entries(utils).forEach(([name, helper]) => {
|
||||
handlebars.registerHelper(name, helper);
|
||||
});
|
||||
|
||||
|
||||
// 注册HTML转义函数(作为助手函数,方便在模板中调用)
|
||||
handlebars.registerHelper('escapeHtml', function(text) {
|
||||
handlebars.registerHelper('escapeHtml', function (text) {
|
||||
if (text === undefined || text === null) {
|
||||
return '';
|
||||
}
|
||||
return String(text)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
});
|
||||
|
||||
|
||||
// 注册非转义助手函数(安全输出HTML)
|
||||
handlebars.registerHelper('safeHtml', function(text) {
|
||||
handlebars.registerHelper('safeHtml', function (text) {
|
||||
if (text === undefined || text === null) {
|
||||
return '';
|
||||
}
|
||||
@@ -55,5 +55,5 @@ module.exports = {
|
||||
formatters,
|
||||
conditions,
|
||||
utils,
|
||||
registerAllHelpers
|
||||
};
|
||||
registerAllHelpers,
|
||||
};
|
||||
|
||||
@@ -13,15 +13,15 @@
|
||||
*/
|
||||
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 [];
|
||||
}
|
||||
|
||||
@@ -34,14 +34,14 @@ function slice(array, start, end) {
|
||||
function concat() {
|
||||
const args = Array.from(arguments);
|
||||
const options = args.pop(); // 最后一个参数是Handlebars的options对象
|
||||
|
||||
|
||||
// 过滤掉非数组参数
|
||||
const validArrays = args.filter(arg => Array.isArray(arg));
|
||||
|
||||
const validArrays = args.filter((arg) => Array.isArray(arg));
|
||||
|
||||
if (validArrays.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
return Array.prototype.concat.apply([], validArrays);
|
||||
}
|
||||
|
||||
@@ -53,15 +53,15 @@ function concat() {
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ function first(array) {
|
||||
if (!array || !Array.isArray(array) || array.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
return array[0];
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ function last(array) {
|
||||
if (!array || !Array.isArray(array) || array.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
return array[array.length - 1];
|
||||
}
|
||||
|
||||
@@ -103,19 +103,19 @@ function last(array) {
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -129,26 +129,26 @@ function range(start, end, step = 1) {
|
||||
function pick() {
|
||||
const args = Array.from(arguments);
|
||||
const options = args.pop(); // 最后一个参数是Handlebars的options对象
|
||||
|
||||
|
||||
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 => {
|
||||
|
||||
keys.forEach((key) => {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ function keys(object) {
|
||||
if (!object || typeof object !== 'object') {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
return Object.keys(object);
|
||||
}
|
||||
|
||||
@@ -274,15 +274,23 @@ function safeUrl(url, options) {
|
||||
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'];
|
||||
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(/:$/, '');
|
||||
const scheme = String(parsed.protocol || '')
|
||||
.toLowerCase()
|
||||
.replace(/:$/, '');
|
||||
if (allowedSchemes.includes(scheme)) return raw;
|
||||
console.warn(`[WARN] 已拦截不安全 URL scheme:${raw}`);
|
||||
return '#';
|
||||
@@ -306,5 +314,5 @@ module.exports = {
|
||||
add,
|
||||
faviconV2Url,
|
||||
faviconFallbackUrl,
|
||||
safeUrl
|
||||
safeUrl,
|
||||
};
|
||||
|
||||
3953
src/script.js
3953
src/script.js
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user