chore: 使用 Prettier 统一代码风格

This commit is contained in:
rbetree
2026-01-04 21:07:07 +08:00
parent 5ae8e99795
commit 82d6341c00
23 changed files with 3129 additions and 2805 deletions

View File

@@ -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);
});

File diff suppressed because it is too large Load Diff

View File

@@ -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,
};

View File

@@ -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,
};

View File

@@ -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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
});
// 注册非转义助手函数安全输出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,
};

View File

@@ -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,
};

File diff suppressed because it is too large Load Diff