refactor: 统一错误处理机制

- 引入 ConfigError/TemplateError/BuildError/FileError 与 wrapAsyncError,统一错误输出
- generator 入口接入 wrapAsyncError,确保命令行执行路径一致
- 兜底逻辑使用 instanceof,保留 BuildError/TemplateError 上下文信息
- 合并格式化提交(仅缩进/换行调整)
This commit is contained in:
rbetree
2026-01-16 02:25:03 +08:00
parent 1a90f8fbe3
commit 89c1c0330b
31 changed files with 313 additions and 89 deletions

View File

@@ -106,7 +106,6 @@ jobs:
git commit -m "chore(bookmarks): 导入书签并写回用户配置" git commit -m "chore(bookmarks): 导入书签并写回用户配置"
git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" HEAD:${{ github.ref_name }} git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" HEAD:${{ github.ref_name }}
# --- 书签处理步骤结束 --- # --- 书签处理步骤结束 ---
# --- 网站构建和部署步骤 --- # --- 网站构建和部署步骤 ---
# 同步时效性数据best-effortprojects 仓库信息、articles RSS 聚合 # 同步时效性数据best-effortprojects 仓库信息、articles RSS 聚合
# 说明: # 说明:

View File

@@ -44,7 +44,8 @@ async function main() {
logLevel: 'info', logLevel: 'info',
}); });
const outputs = result && result.metafile && result.metafile.outputs ? result.metafile.outputs : null; const outputs =
result && result.metafile && result.metafile.outputs ? result.metafile.outputs : null;
const outKey = outputs ? Object.keys(outputs).find((k) => k.endsWith('dist/script.js')) : ''; const outKey = outputs ? Object.keys(outputs).find((k) => k.endsWith('dist/script.js')) : '';
const bytes = outKey && outputs && outputs[outKey] ? outputs[outKey].bytes : 0; const bytes = outKey && outputs && outputs[outKey] ? outputs[outKey].bytes : 0;
if (bytes) { if (bytes) {
@@ -53,10 +54,12 @@ async function main() {
console.log('✅ runtime bundle 完成dist/script.js'); console.log('✅ runtime bundle 完成dist/script.js');
} }
} catch (error) { } catch (error) {
console.error('❌ runtime bundle 失败(禁止回退旧产物):', error && error.message ? error.message : error); console.error(
'❌ runtime bundle 失败(禁止回退旧产物):',
error && error.message ? error.message : error
);
process.exitCode = 1; process.exitCode = 1;
} }
} }
main(); main();

View File

@@ -1,6 +1,7 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const { FileError, wrapAsyncError } = require('./generator/utils/errors');
// 书签文件夹路径 - 使用相对路径 // 书签文件夹路径 - 使用相对路径
const BOOKMARKS_DIR = 'bookmarks'; const BOOKMARKS_DIR = 'bookmarks';
@@ -821,8 +822,11 @@ async function main() {
// 验证文件是否确实被创建 // 验证文件是否确实被创建
if (!fs.existsSync(MODULAR_OUTPUT_FILE)) { if (!fs.existsSync(MODULAR_OUTPUT_FILE)) {
console.error(`[ERROR] 文件未能创建: ${MODULAR_OUTPUT_FILE}`); throw new FileError('文件未能创建', MODULAR_OUTPUT_FILE, [
process.exit(1); '检查目录权限是否正确',
'确认磁盘空间是否充足',
'尝试手动创建目录: mkdir -p config/user/pages',
]);
} }
console.log('[SUCCESS] 文件保存成功'); console.log('[SUCCESS] 文件保存成功');
@@ -846,27 +850,33 @@ async function main() {
console.log('[INFO] 导航配置无需更新\n'); console.log('[INFO] 导航配置无需更新\n');
} }
} catch (writeError) { } catch (writeError) {
console.error(`[ERROR] 写入文件时出错:`, writeError); throw new FileError('写入文件时出错', MODULAR_OUTPUT_FILE, [
console.error('[ERROR] 错误堆栈:', writeError.stack); '检查文件路径是否正确',
process.exit(1); '确认目录权限是否正确',
`错误详情: ${writeError.message}`,
]);
} }
console.log('========================================'); console.log('========================================');
console.log('[SUCCESS] 书签处理完成!'); console.log('[SUCCESS] 书签处理完成!');
console.log('========================================'); console.log('========================================');
} catch (error) { } catch (error) {
console.error('[FATAL] 处理书签文件时发生错误:', error); // 如果是自定义错误,直接抛出
console.error('[ERROR] 错误堆栈:', error.stack); if (error instanceof FileError) {
process.exit(1); throw error;
}
// 否则包装为 FileError
throw new FileError('处理书签文件时发生错误', null, [
'检查书签 HTML 文件格式是否正确',
'确认配置目录结构是否完整',
`错误详情: ${error.message}`,
]);
} }
} }
// 启动处理 // 启动处理
if (require.main === module) { if (require.main === module) {
main().catch((err) => { wrapAsyncError(main)();
console.error('Unhandled error in bookmark processing:', err);
process.exit(1);
});
} }
module.exports = { module.exports = {

View File

@@ -1,14 +1,14 @@
// 生成端薄入口:保持对外导出稳定,内部实现位于 src/generator/main.js // 生成端薄入口:保持对外导出稳定,内部实现位于 src/generator/main.js
const impl = require('./generator/main'); const impl = require('./generator/main');
const { wrapAsyncError } = require('./generator/utils/errors');
module.exports = impl; module.exports = impl;
if (require.main === module) { if (require.main === module) {
if (typeof impl.main === 'function') { if (typeof impl.main === 'function') {
impl.main(); wrapAsyncError(impl.main)();
} else { } else {
console.error('generator main() 未导出,无法直接执行。'); console.error('generator main() 未导出,无法直接执行。');
process.exitCode = 1; process.exitCode = 1;
} }
} }

View File

@@ -156,4 +156,3 @@ module.exports = {
tryLoadArticlesFeedCache, tryLoadArticlesFeedCache,
buildArticlesCategoriesByPageCategories, buildArticlesCategoriesByPageCategories,
}; };

View File

@@ -4,7 +4,9 @@ const path = require('path');
function tryLoadProjectsRepoCache(pageId, config) { function tryLoadProjectsRepoCache(pageId, config) {
if (!pageId) return null; if (!pageId) return null;
const cacheDirFromEnv = process.env.PROJECTS_CACHE_DIR ? String(process.env.PROJECTS_CACHE_DIR) : ''; const cacheDirFromEnv = process.env.PROJECTS_CACHE_DIR
? String(process.env.PROJECTS_CACHE_DIR)
: '';
const cacheDirFromConfig = const cacheDirFromConfig =
config && config.site && config.site.github && config.site.github.cacheDir config && config.site && config.site.github && config.site.github.cacheDir
? String(config.site.github.cacheDir) ? String(config.site.github.cacheDir)
@@ -132,4 +134,3 @@ module.exports = {
applyRepoMetaToCategories, applyRepoMetaToCategories,
buildProjectsMeta, buildProjectsMeta,
}; };

View File

@@ -1,8 +1,15 @@
const fs = require('node:fs'); const fs = require('node:fs');
const { loadModularConfig } = require('./loader'); const { loadModularConfig } = require('./loader');
const { ensureConfigDefaults, validateConfig } = require('./validator'); const { ensureConfigDefaults, validateConfig } = require('./validator');
const { prepareRenderData, MENAV_EXTENSION_CONFIG_FILE, getSubmenuForNavItem, resolveTemplateNameForPage, buildExtensionConfig } = require('./resolver'); const {
prepareRenderData,
MENAV_EXTENSION_CONFIG_FILE,
getSubmenuForNavItem,
resolveTemplateNameForPage,
buildExtensionConfig,
} = require('./resolver');
const { assignCategorySlugs } = require('./slugs'); const { assignCategorySlugs } = require('./slugs');
const { ConfigError } = require('../utils/errors');
function loadConfig() { function loadConfig() {
let config = { let config = {
@@ -18,16 +25,17 @@ function loadConfig() {
if (hasUserModularConfig) { if (hasUserModularConfig) {
if (!fs.existsSync('config/user/site.yml')) { if (!fs.existsSync('config/user/site.yml')) {
console.error('[ERROR] 检测到 config/user/ 目录,但缺少 config/user/site.yml'); throw new ConfigError('检测到 config/user/ 目录,但缺少 config/user/site.yml', [
console.error( '由于配置采用"完全替换"策略,系统不会从 config/_default/ 补齐缺失配置',
'[ERROR] 由于配置采用"完全替换"策略,系统不会从 config/_default/ 补齐缺失配置。' '解决方法:先完整复制 config/_default/ 到 config/user/,再按需修改',
); '参考文档: config/README.md',
console.error('[ERROR] 解决方法:先完整复制 config/_default/ 到 config/user/,再按需修改。'); ]);
process.exit(1);
} }
if (!fs.existsSync('config/user/pages')) { if (!fs.existsSync('config/user/pages')) {
console.warn('[WARN] 检测到 config/user/ 目录,但缺少 config/user/pages/。部分页面内容可能为空。'); console.warn(
'[WARN] 检测到 config/user/ 目录,但缺少 config/user/pages/。部分页面内容可能为空。'
);
console.warn('[WARN] 建议:复制 config/_default/pages/ 到 config/user/pages/,再按需修改。'); console.warn('[WARN] 建议:复制 config/_default/pages/ 到 config/user/pages/,再按需修改。');
} }
@@ -35,10 +43,11 @@ function loadConfig() {
} else if (hasDefaultModularConfig) { } else if (hasDefaultModularConfig) {
config = loadModularConfig('config/_default'); config = loadModularConfig('config/_default');
} else { } else {
console.error('[ERROR] 未找到可用配置:缺少 config/user/ 或 config/_default/'); throw new ConfigError('未找到可用配置:缺少 config/user/ 或 config/_default/', [
console.error('[ERROR] 本版本已不再支持旧版单文件配置config.yml / config.yaml'); '本版本已不再支持旧版单文件配置config.yml / config.yaml',
console.error('[ERROR] 解决方法:使用模块化配置目录(建议从 config/_default/ 复制到 config/user/ 再修改)'); '解决方法:使用模块化配置目录(建议从 config/_default/ 复制到 config/user/ 再修改)',
process.exit(1); '参考文档: config/README.md',
]);
} }
config = ensureConfigDefaults(config); config = ensureConfigDefaults(config);

View File

@@ -27,8 +27,7 @@ function resolveTemplateNameForPage(pageId, config) {
if (!pageId) return 'page'; if (!pageId) return 'page';
const pageConfig = config && config[pageId] ? config[pageId] : null; const pageConfig = config && config[pageId] ? config[pageId] : null;
const explicit = const explicit = pageConfig && pageConfig.template ? String(pageConfig.template).trim() : '';
pageConfig && pageConfig.template ? String(pageConfig.template).trim() : '';
if (explicit) return explicit; if (explicit) return explicit;
const candidatePath = path.join(process.cwd(), 'templates', 'pages', `${pageId}.hbs`); const candidatePath = path.join(process.cwd(), 'templates', 'pages', `${pageId}.hbs`);

View File

@@ -2,7 +2,8 @@ const { escapeHtml } = require('../utils/html');
// 生成 GitHub Pages 的 404 回跳页:将 /<id> 形式的路径深链接转换为 /?page=<id> // 生成 GitHub Pages 的 404 回跳页:将 /<id> 形式的路径深链接转换为 /?page=<id>
function generate404Html(config) { function generate404Html(config) {
const siteTitle = config && config.site && typeof config.site.title === 'string' ? config.site.title : 'MeNav'; const siteTitle =
config && config.site && typeof config.site.title === 'string' ? config.site.title : 'MeNav';
const safeTitle = escapeHtml(siteTitle); const safeTitle = escapeHtml(siteTitle);
return `<!doctype html> return `<!doctype html>

View File

@@ -205,4 +205,3 @@ module.exports = {
generatePageContent, generatePageContent,
generateSearchResultsPage, generateSearchResultsPage,
}; };

View File

@@ -1,8 +1,15 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { getSubmenuForNavItem, assignCategorySlugs } = require('../config'); const { getSubmenuForNavItem, assignCategorySlugs } = require('../config');
const { tryLoadArticlesFeedCache, buildArticlesCategoriesByPageCategories } = require('../cache/articles'); const {
const { tryLoadProjectsRepoCache, applyRepoMetaToCategories, buildProjectsMeta } = require('../cache/projects'); tryLoadArticlesFeedCache,
buildArticlesCategoriesByPageCategories,
} = require('../cache/articles');
const {
tryLoadProjectsRepoCache,
applyRepoMetaToCategories,
buildProjectsMeta,
} = require('../cache/projects');
const { getPageConfigUpdatedAtMeta } = require('../utils/pageMeta'); const { getPageConfigUpdatedAtMeta } = require('../utils/pageMeta');
function prepareNavigationData(pageId, config) { function prepareNavigationData(pageId, config) {
@@ -32,7 +39,12 @@ function resolveTemplateName(pageId, data) {
let templateName = explicitTemplate || pageId; let templateName = explicitTemplate || pageId;
if (!explicitTemplate) { if (!explicitTemplate) {
const inferredTemplatePath = path.join(process.cwd(), 'templates', 'pages', `${templateName}.hbs`); const inferredTemplatePath = path.join(
process.cwd(),
'templates',
'pages',
`${templateName}.hbs`
);
if (!fs.existsSync(inferredTemplatePath)) { if (!fs.existsSync(inferredTemplatePath)) {
templateName = 'page'; templateName = 'page';
} }

View File

@@ -3,14 +3,29 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const { execFileSync } = require('child_process'); const { execFileSync } = require('child_process');
const { loadHandlebarsTemplates, getDefaultLayoutTemplate, renderTemplate } = require('./template/engine'); const {
loadHandlebarsTemplates,
getDefaultLayoutTemplate,
renderTemplate,
} = require('./template/engine');
const { MENAV_EXTENSION_CONFIG_FILE, loadConfig, getSubmenuForNavItem } = require('./config'); const { MENAV_EXTENSION_CONFIG_FILE, loadConfig, getSubmenuForNavItem } = require('./config');
const { generateNavigation, generateCategories, generateSocialLinks } = require('./html/components'); const {
generateNavigation,
generateCategories,
generateSocialLinks,
} = require('./html/components');
const { generate404Html } = require('./html/404'); const { generate404Html } = require('./html/404');
const { generateFontLinks, generateFontCss } = require('./html/fonts'); const { generateFontLinks, generateFontCss } = require('./html/fonts');
const { preparePageData } = require('./html/page-data'); const { preparePageData } = require('./html/page-data');
const { collectSitesRecursively } = require('./utils/sites'); const { collectSitesRecursively } = require('./utils/sites');
const {
BuildError,
ConfigError,
FileError,
TemplateError,
wrapAsyncError,
} = require('./utils/errors');
/** /**
* 渲染单个页面 * 渲染单个页面
@@ -219,7 +234,10 @@ function copyStaticFiles(config) {
if (config.site.favicon) { if (config.site.favicon) {
try { try {
if (fs.existsSync(`assets/${config.site.favicon}`)) { if (fs.existsSync(`assets/${config.site.favicon}`)) {
fs.copyFileSync(`assets/${config.site.favicon}`, `dist/${path.basename(config.site.favicon)}`); fs.copyFileSync(
`assets/${config.site.favicon}`,
`dist/${path.basename(config.site.favicon)}`
);
} else if (fs.existsSync(config.site.favicon)) { } else if (fs.existsSync(config.site.favicon)) {
fs.copyFileSync(config.site.favicon, `dist/${path.basename(config.site.favicon)}`); fs.copyFileSync(config.site.favicon, `dist/${path.basename(config.site.favicon)}`);
} else { } else {
@@ -270,20 +288,36 @@ function main() {
stdio: 'inherit', stdio: 'inherit',
}); });
} catch (error) { } catch (error) {
console.error('Error bundling runtime script:', error); throw new BuildError('构建运行时脚本失败', {
process.exit(1); 脚本路径: 'scripts/build-runtime.js',
错误信息: error.message,
});
} }
// 复制静态文件 // 复制静态文件
copyStaticFiles(config); copyStaticFiles(config);
} catch (e) { } catch (e) {
console.error('Error in main function:', e); // 如果是自定义错误,直接抛出,保留上下文/路径信息
process.exit(1); if (
e instanceof ConfigError ||
e instanceof TemplateError ||
e instanceof BuildError ||
e instanceof FileError
) {
throw e;
}
// 否则包装为 BuildError避免直接暴露底层异常
throw new BuildError('构建过程中发生错误', {
错误类型: e.name || 'Error',
错误信息: e.message || '未知错误',
});
} }
} }
if (require.main === module) { if (require.main === module) {
main(); // 使用 wrapAsyncError 包装主函数,自动处理错误
wrapAsyncError(main)();
} }
// 导出供测试使用的函数 // 导出供测试使用的函数

View File

@@ -0,0 +1,127 @@
/**
* 自定义错误类 - 配置相关错误
*/
class ConfigError extends Error {
constructor(message, suggestions = []) {
super(message);
this.name = 'ConfigError';
this.suggestions = suggestions;
}
}
/**
* 自定义错误类 - 模板相关错误
*/
class TemplateError extends Error {
constructor(message, templatePath = null) {
super(message);
this.name = 'TemplateError';
this.templatePath = templatePath;
}
}
/**
* 自定义错误类 - 构建相关错误
*/
class BuildError extends Error {
constructor(message, context = {}) {
super(message);
this.name = 'BuildError';
this.context = context;
}
}
/**
* 自定义错误类 - 文件操作相关错误
*/
class FileError extends Error {
constructor(message, filePath = null, suggestions = []) {
super(message);
this.name = 'FileError';
this.filePath = filePath;
this.suggestions = suggestions;
}
}
/**
* 统一错误处理器 - 专业紧凑版(中文)
* @param {Error} error - 错误对象
* @param {number} exitCode - 退出码,默认为 1
*/
function handleError(error, exitCode = 1) {
// 错误标题行
console.error(`\n${error.name}: ${error.message}`);
// 文件路径(如果有)
if (error.filePath || error.templatePath) {
const path = error.filePath || error.templatePath;
console.error(` 位置: ${path}`);
}
// 上下文信息(如果有)
if (error.context && Object.keys(error.context).length > 0) {
console.error('│');
for (const [key, value] of Object.entries(error.context)) {
console.error(`${key}: ${value}`);
}
}
// 修复建议(如果有)
if (error.suggestions && error.suggestions.length > 0) {
console.error('│');
console.error('➜ 解决方案:');
error.suggestions.forEach((suggestion, index) => {
console.error(` ${index + 1}. ${suggestion}`);
});
}
// DEBUG 提示(仅在非 DEBUG 模式下显示)
if (process.env.DEBUG) {
console.error('\n堆栈跟踪:');
console.error(error.stack);
} else {
console.error('\n设置 DEBUG=1 查看堆栈跟踪)');
}
console.error(); // 空行结束
process.exit(exitCode);
}
/**
* 包装异步函数,自动处理未捕获的错误
* @param {Function} fn - 异步函数
* @returns {Function} 包装后的函数
*/
function wrapAsyncError(fn) {
return async (...args) => {
try {
return await fn(...args);
} catch (error) {
// 如果是自定义错误,直接使用 handleError
if (
error instanceof ConfigError ||
error instanceof TemplateError ||
error instanceof BuildError ||
error instanceof FileError
) {
handleError(error);
} else {
// 否则包装为 BuildError
handleError(
new BuildError(error.message || '未知错误', {
原始错误类型: error.name || 'Error',
})
);
}
}
};
}
module.exports = {
ConfigError,
TemplateError,
BuildError,
FileError,
handleError,
wrapAsyncError,
};

View File

@@ -14,4 +14,3 @@ function escapeHtml(unsafe) {
module.exports = { module.exports = {
escapeHtml, escapeHtml,
}; };

View File

@@ -98,4 +98,3 @@ function getPageConfigUpdatedAtMeta(pageId) {
module.exports = { module.exports = {
getPageConfigUpdatedAtMeta, getPageConfigUpdatedAtMeta,
}; };

View File

@@ -17,7 +17,8 @@ function collectSitesRecursively(node, output) {
if (Array.isArray(node.subcategories)) if (Array.isArray(node.subcategories))
node.subcategories.forEach((child) => collectSitesRecursively(child, output)); node.subcategories.forEach((child) => collectSitesRecursively(child, output));
if (Array.isArray(node.groups)) node.groups.forEach((child) => collectSitesRecursively(child, output)); if (Array.isArray(node.groups))
node.groups.forEach((child) => collectSitesRecursively(child, output));
if (Array.isArray(node.subgroups)) if (Array.isArray(node.subgroups))
node.subgroups.forEach((child) => collectSitesRecursively(child, output)); node.subgroups.forEach((child) => collectSitesRecursively(child, output));
@@ -32,4 +33,3 @@ module.exports = {
normalizeUrlKey, normalizeUrlKey,
collectSitesRecursively, collectSitesRecursively,
}; };

View File

@@ -7,11 +7,16 @@ function detectHomePageId() {
// 1) 优先从生成端注入的配置数据读取(保持与实际导航顺序一致) // 1) 优先从生成端注入的配置数据读取(保持与实际导航顺序一致)
try { try {
const config = const config =
window.MeNav && typeof window.MeNav.getConfig === 'function' ? window.MeNav.getConfig() : null; window.MeNav && typeof window.MeNav.getConfig === 'function'
? window.MeNav.getConfig()
: null;
const injectedHomePageId = const injectedHomePageId =
config && config.data && config.data.homePageId ? String(config.data.homePageId).trim() : ''; config && config.data && config.data.homePageId ? String(config.data.homePageId).trim() : '';
if (injectedHomePageId) return injectedHomePageId; if (injectedHomePageId) return injectedHomePageId;
const nav = config && config.data && Array.isArray(config.data.navigation) ? config.data.navigation : null; const nav =
config && config.data && Array.isArray(config.data.navigation)
? config.data.navigation
: null;
const firstId = nav && nav[0] && nav[0].id ? String(nav[0].id).trim() : ''; const firstId = nav && nav[0] && nav[0].id ? String(nav[0].id).trim() : '';
if (firstId) return firstId; if (firstId) return firstId;
} catch (error) { } catch (error) {
@@ -109,4 +114,3 @@ document.addEventListener('DOMContentLoaded', () => {
initRouting(state, dom, { ui, search }); initRouting(state, dom, { ui, search });
}); });

View File

@@ -49,7 +49,8 @@ module.exports = function initRouting(state, dom, api) {
state.pages = document.querySelectorAll('.page'); state.pages = document.querySelectorAll('.page');
// 方案 A用 ?page=<id> 作为页面深链接(兼容 GitHub Pages 静态托管) // 方案 A用 ?page=<id> 作为页面深链接(兼容 GitHub Pages 静态托管)
const normalizeText = (value) => String(value === null || value === undefined ? '' : value).trim(); const normalizeText = (value) =>
String(value === null || value === undefined ? '' : value).trim();
const isValidPageId = (pageId) => { const isValidPageId = (pageId) => {
const id = normalizeText(pageId); const id = normalizeText(pageId);
@@ -164,7 +165,9 @@ module.exports = function initRouting(state, dom, api) {
const escapedId = escapeSelector(categoryId); const escapedId = escapeSelector(categoryId);
targetCategory = targetCategory =
targetPage.querySelector(`#${escapedId}`) || targetPage.querySelector(`#${escapedId}`) ||
targetPage.querySelector(`[data-type="category"][data-id="${escapeAttrValue(categoryId)}"]`); targetPage.querySelector(
`[data-type="category"][data-id="${escapeAttrValue(categoryId)}"]`
);
} }
// 回退:旧逻辑按文本包含匹配(兼容旧页面/旧数据) // 回退:旧逻辑按文本包含匹配(兼容旧页面/旧数据)
@@ -190,7 +193,8 @@ module.exports = function initRouting(state, dom, api) {
const desiredPosition = containerRect.height / 4; const desiredPosition = containerRect.height / 4;
// 计算需要滚动的位置 // 计算需要滚动的位置
const scrollPosition = contentElement.scrollTop + rect.top - containerRect.top - desiredPosition; const scrollPosition =
contentElement.scrollTop + rect.top - containerRect.top - desiredPosition;
// 执行滚动 // 执行滚动
contentElement.scrollTo({ contentElement.scrollTo({
@@ -234,7 +238,8 @@ module.exports = function initRouting(state, dom, api) {
// 支持 ?page=<id> 直接打开对应页面;无效时回退到首页 // 支持 ?page=<id> 直接打开对应页面;无效时回退到首页
const rawPageIdFromUrl = getRawPageIdFromUrl(); const rawPageIdFromUrl = getRawPageIdFromUrl();
const validatedPageIdFromUrl = getPageIdFromUrl(); const validatedPageIdFromUrl = getPageIdFromUrl();
const initialPageId = validatedPageIdFromUrl || (isValidPageId(state.homePageId) ? state.homePageId : 'home'); const initialPageId =
validatedPageIdFromUrl || (isValidPageId(state.homePageId) ? state.homePageId : 'home');
setActiveNavByPageId(initialPageId); setActiveNavByPageId(initialPageId);
showPage(initialPageId); showPage(initialPageId);
@@ -400,4 +405,3 @@ module.exports = function initRouting(state, dom, api) {
return { showPage }; return { showPage };
}; };

View File

@@ -48,18 +48,25 @@ module.exports = function initSearch(state, dom) {
// 兼容不同页面/卡片样式:优先取可见文本,其次回退到 data-*(确保 projects repo 卡片也能被搜索) // 兼容不同页面/卡片样式:优先取可见文本,其次回退到 data-*(确保 projects repo 卡片也能被搜索)
const dataTitle = card.dataset?.name || card.getAttribute('data-name') || ''; const dataTitle = card.dataset?.name || card.getAttribute('data-name') || '';
const dataDescription = card.dataset?.description || card.getAttribute('data-description') || ''; const dataDescription =
card.dataset?.description || card.getAttribute('data-description') || '';
const titleText = const titleText =
card.querySelector('h3')?.textContent || card.querySelector('.repo-title')?.textContent || dataTitle; card.querySelector('h3')?.textContent ||
card.querySelector('.repo-title')?.textContent ||
dataTitle;
const descriptionText = const descriptionText =
card.querySelector('p')?.textContent || card.querySelector('.repo-desc')?.textContent || dataDescription; card.querySelector('p')?.textContent ||
card.querySelector('.repo-desc')?.textContent ||
dataDescription;
const title = String(titleText || '').toLowerCase(); const title = String(titleText || '').toLowerCase();
const description = String(descriptionText || '').toLowerCase(); const description = String(descriptionText || '').toLowerCase();
const url = card.href || card.getAttribute('href') || '#'; const url = card.href || card.getAttribute('href') || '#';
const icon = const icon =
card.querySelector('i.icon-fallback')?.className || card.querySelector('i')?.className || ''; card.querySelector('i.icon-fallback')?.className ||
card.querySelector('i')?.className ||
'';
// 将卡片信息添加到索引中 // 将卡片信息添加到索引中
state.searchIndex.items.push({ state.searchIndex.items.push({
@@ -111,7 +118,9 @@ module.exports = function initSearch(state, dom) {
// 使用更高效的搜索算法 // 使用更高效的搜索算法
const matchedItems = state.searchIndex.items.filter((item) => { const matchedItems = state.searchIndex.items.filter((item) => {
return item.searchText.includes(searchTerm) || PinyinMatch.match(item.searchText, searchTerm); return (
item.searchText.includes(searchTerm) || PinyinMatch.match(item.searchText, searchTerm)
);
}); });
// 按页面分组结果 // 按页面分组结果

View File

@@ -47,7 +47,9 @@ module.exports = function highlightSearchTerm(card, searchTerm) {
while ((match = regex.exec(rawText)) !== null) { while ((match = regex.exec(rawText)) !== null) {
if (match.index > lastIndex) { if (match.index > lastIndex) {
fragment.appendChild(document.createTextNode(rawText.substring(lastIndex, match.index))); fragment.appendChild(
document.createTextNode(rawText.substring(lastIndex, match.index))
);
} }
const span = document.createElement('span'); const span = document.createElement('span');
@@ -87,4 +89,3 @@ module.exports = function highlightSearchTerm(card, searchTerm) {
console.error('Error highlighting search term'); console.error('Error highlighting search term');
} }
}; };

View File

@@ -22,4 +22,3 @@ module.exports = {
url: 'https://duckduckgo.com/?q=', url: 'https://duckduckgo.com/?q=',
}, },
}; };

View File

@@ -178,4 +178,3 @@ module.exports = function initUi(state, dom) {
initSidebarState, initSidebarState,
}; };
}; };

View File

@@ -15,4 +15,3 @@ require('./app');
// tooltip 独立模块:内部会按需监听 DOMContentLoaded // tooltip 独立模块:内部会按需监听 DOMContentLoaded
require('./tooltip'); require('./tooltip');

View File

@@ -17,7 +17,9 @@ module.exports = function addElement(type, parentId, data) {
const pageEl = parent.closest('.page'); const pageEl = parent.closest('.page');
const pageId = pageEl && pageEl.id ? String(pageEl.id).trim() : ''; const pageId = pageEl && pageEl.id ? String(pageEl.id).trim() : '';
const cfg = const cfg =
window.MeNav && typeof window.MeNav.getConfig === 'function' ? window.MeNav.getConfig() : null; window.MeNav && typeof window.MeNav.getConfig === 'function'
? window.MeNav.getConfig()
: null;
let templateName = ''; let templateName = '';
@@ -27,12 +29,16 @@ module.exports = function addElement(type, parentId, data) {
: null; : null;
const templateFromMap = const templateFromMap =
pageTemplates && pageId && pageTemplates[pageId] ? String(pageTemplates[pageId]).trim() : ''; pageTemplates && pageId && pageTemplates[pageId]
? String(pageTemplates[pageId]).trim()
: '';
// 兼容旧版cfg.data[pageId].template // 兼容旧版cfg.data[pageId].template
const legacyPageConfig = cfg && cfg.data && pageId ? cfg.data[pageId] : null; const legacyPageConfig = cfg && cfg.data && pageId ? cfg.data[pageId] : null;
const templateFromLegacy = const templateFromLegacy =
legacyPageConfig && legacyPageConfig.template ? String(legacyPageConfig.template).trim() : ''; legacyPageConfig && legacyPageConfig.template
? String(legacyPageConfig.template).trim()
: '';
if (templateFromMap) { if (templateFromMap) {
templateName = templateFromMap; templateName = templateFromMap;
@@ -55,16 +61,22 @@ module.exports = function addElement(type, parentId, data) {
const siteIcon = data.icon || 'fas fa-link'; const siteIcon = data.icon || 'fas fa-link';
const siteDescription = data.description || (data.url ? menavExtractDomain(data.url) : ''); const siteDescription = data.description || (data.url ? menavExtractDomain(data.url) : '');
const siteFaviconUrl = data && data.faviconUrl ? String(data.faviconUrl).trim() : ''; const siteFaviconUrl = data && data.faviconUrl ? String(data.faviconUrl).trim() : '';
const siteForceIconModeRaw = data && data.forceIconMode ? String(data.forceIconMode).trim() : ''; const siteForceIconModeRaw =
data && data.forceIconMode ? String(data.forceIconMode).trim() : '';
const siteForceIconMode = const siteForceIconMode =
siteForceIconModeRaw === 'manual' || siteForceIconModeRaw === 'favicon' ? siteForceIconModeRaw : ''; siteForceIconModeRaw === 'manual' || siteForceIconModeRaw === 'favicon'
? siteForceIconModeRaw
: '';
const safeSiteUrl = menavSanitizeUrl(siteUrl, 'addElement(site).url'); const safeSiteUrl = menavSanitizeUrl(siteUrl, 'addElement(site).url');
const safeSiteIcon = menavSanitizeClassList(siteIcon, 'addElement(site).icon'); const safeSiteIcon = menavSanitizeClassList(siteIcon, 'addElement(site).icon');
newSite.setAttribute('href', safeSiteUrl); newSite.setAttribute('href', safeSiteUrl);
newSite.title = siteName + (siteDescription ? ' - ' + siteDescription : ''); newSite.title = siteName + (siteDescription ? ' - ' + siteDescription : '');
newSite.setAttribute('data-tooltip', siteName + (siteDescription ? ' - ' + siteDescription : '')); newSite.setAttribute(
'data-tooltip',
siteName + (siteDescription ? ' - ' + siteDescription : '')
);
if (/^https?:\/\//i.test(safeSiteUrl)) { if (/^https?:\/\//i.test(safeSiteUrl)) {
newSite.target = '_blank'; newSite.target = '_blank';
newSite.rel = 'noopener'; newSite.rel = 'noopener';
@@ -185,8 +197,11 @@ module.exports = function addElement(type, parentId, data) {
// favicon 模式:优先加载 faviconUrl否则按 url 生成 // favicon 模式:优先加载 faviconUrl否则按 url 生成
try { try {
const cfg = const cfg =
window.MeNav && typeof window.MeNav.getConfig === 'function' ? window.MeNav.getConfig() : null; window.MeNav && typeof window.MeNav.getConfig === 'function'
const iconsMode = cfg && cfg.icons && cfg.icons.mode ? String(cfg.icons.mode).trim() : 'favicon'; ? window.MeNav.getConfig()
: null;
const iconsMode =
cfg && cfg.icons && cfg.icons.mode ? String(cfg.icons.mode).trim() : 'favicon';
const iconsRegion = const iconsRegion =
cfg && cfg.icons && cfg.icons.region ? String(cfg.icons.region).trim() : 'com'; cfg && cfg.icons && cfg.icons.region ? String(cfg.icons.region).trim() : 'com';
@@ -319,7 +334,10 @@ module.exports = function addElement(type, parentId, data) {
// 添加内容(用 DOM API 构建,避免 innerHTML 注入) // 添加内容(用 DOM API 构建,避免 innerHTML 注入)
const titleEl = document.createElement('h2'); const titleEl = document.createElement('h2');
const iconEl = document.createElement('i'); const iconEl = document.createElement('i');
iconEl.className = menavSanitizeClassList(data.icon || 'fas fa-folder', 'addElement(category).icon'); iconEl.className = menavSanitizeClassList(
data.icon || 'fas fa-folder',
'addElement(category).icon'
);
titleEl.appendChild(iconEl); titleEl.appendChild(iconEl);
titleEl.appendChild(document.createTextNode(' ' + String(data.name || '未命名分类'))); titleEl.appendChild(document.createTextNode(' ' + String(data.name || '未命名分类')));

View File

@@ -44,7 +44,8 @@ function findDefaultElement(type, id) {
// 全局 MeNav 对象 - 用于浏览器扩展 // 全局 MeNav 对象 - 用于浏览器扩展
const existing = window.MeNav && typeof window.MeNav === 'object' ? window.MeNav : {}; const existing = window.MeNav && typeof window.MeNav === 'object' ? window.MeNav : {};
const events = existing.events && typeof existing.events === 'object' ? existing.events : createMenavEvents(); const events =
existing.events && typeof existing.events === 'object' ? existing.events : createMenavEvents();
window.MeNav = Object.assign(existing, { window.MeNav = Object.assign(existing, {
version: menavDetectVersion(), version: menavDetectVersion(),

View File

@@ -107,7 +107,10 @@ module.exports = function updateElement(type, id, newData) {
if (newData.icon) { if (newData.icon) {
const iconElement = element.querySelector('i'); const iconElement = element.querySelector('i');
if (iconElement) { if (iconElement) {
iconElement.className = menavSanitizeClassList(newData.icon, 'updateElement(nav-item).icon'); iconElement.className = menavSanitizeClassList(
newData.icon,
'updateElement(nav-item).icon'
);
} }
element.setAttribute( element.setAttribute(
'data-icon', 'data-icon',

View File

@@ -180,7 +180,9 @@ function registerNestedApi() {
if (!activePage) return; if (!activePage) return;
const allElements = getCollapsibleNestedContainers(activePage); const allElements = getCollapsibleNestedContainers(activePage);
const collapsedElements = allElements.filter((element) => element.classList.contains('collapsed')); const collapsedElements = allElements.filter((element) =>
element.classList.contains('collapsed')
);
if (allElements.length === 0) return; if (allElements.length === 0) return;
// 如果收起的数量 >= 总数的一半,执行展开;否则执行收起 // 如果收起的数量 >= 总数的一半,执行展开;否则执行收起

View File

@@ -139,4 +139,3 @@ module.exports = {
menavDetectVersion, menavDetectVersion,
menavUpdateAppHeight, menavUpdateAppHeight,
}; };

View File

@@ -112,4 +112,3 @@ document.addEventListener('DOMContentLoaded', () => {
hoverMedia.addListener(syncTooltipEnabled); hoverMedia.addListener(syncTooltipEnabled);
} }
}); });

View File

@@ -13,4 +13,3 @@ test('P1-5404.html 回跳应将 /<id> 转为 ?page=<id>(并支持仓库前
assert.ok(html.includes('segments.length === 2'), '应支持仓库站点 /<repo>/<id>'); assert.ok(html.includes('segments.length === 2'), '应支持仓库站点 /<repo>/<id>');
assert.ok(html.includes('l.replace(target)'), '应使用 location.replace 执行回跳'); assert.ok(html.includes('l.replace(target)'), '应使用 location.replace 执行回跳');
}); });

View File

@@ -20,9 +20,7 @@ test('P1-7页面内不应注入整站 configJSON应仅保留扩展元信
const config = loadConfig(); const config = loadConfig();
const html = generateHTML(config); const html = generateHTML(config);
const match = html.match( const match = html.match(/<script id="menav-config-data"[^>]*>([\s\S]*?)<\/script>/m);
/<script id="menav-config-data"[^>]*>([\s\S]*?)<\/script>/m
);
assert.ok(match, '应输出 menav-config-data 脚本块'); assert.ok(match, '应输出 menav-config-data 脚本块');
const raw = String(match[1] || '').trim(); const raw = String(match[1] || '').trim();
@@ -53,4 +51,3 @@ test('P1-7页面内不应注入整站 configJSON应仅保留扩展元信
assert.ok(!/"sites"\s*:/.test(raw), '不应包含 sites 字段'); assert.ok(!/"sites"\s*:/.test(raw), '不应包含 sites 字段');
}); });
}); });