refactor: 统一错误处理机制
- 引入 ConfigError/TemplateError/BuildError/FileError 与 wrapAsyncError,统一错误输出 - generator 入口接入 wrapAsyncError,确保命令行执行路径一致 - 兜底逻辑使用 instanceof,保留 BuildError/TemplateError 上下文信息 - 合并格式化提交(仅缩进/换行调整)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
const { FileError, wrapAsyncError } = require('./generator/utils/errors');
|
||||
|
||||
// 书签文件夹路径 - 使用相对路径
|
||||
const BOOKMARKS_DIR = 'bookmarks';
|
||||
@@ -821,8 +822,11 @@ async function main() {
|
||||
|
||||
// 验证文件是否确实被创建
|
||||
if (!fs.existsSync(MODULAR_OUTPUT_FILE)) {
|
||||
console.error(`[ERROR] 文件未能创建: ${MODULAR_OUTPUT_FILE}`);
|
||||
process.exit(1);
|
||||
throw new FileError('文件未能创建', MODULAR_OUTPUT_FILE, [
|
||||
'检查目录权限是否正确',
|
||||
'确认磁盘空间是否充足',
|
||||
'尝试手动创建目录: mkdir -p config/user/pages',
|
||||
]);
|
||||
}
|
||||
|
||||
console.log('[SUCCESS] 文件保存成功');
|
||||
@@ -846,27 +850,33 @@ async function main() {
|
||||
console.log('[INFO] 导航配置无需更新\n');
|
||||
}
|
||||
} catch (writeError) {
|
||||
console.error(`[ERROR] 写入文件时出错:`, writeError);
|
||||
console.error('[ERROR] 错误堆栈:', writeError.stack);
|
||||
process.exit(1);
|
||||
throw new FileError('写入文件时出错', MODULAR_OUTPUT_FILE, [
|
||||
'检查文件路径是否正确',
|
||||
'确认目录权限是否正确',
|
||||
`错误详情: ${writeError.message}`,
|
||||
]);
|
||||
}
|
||||
|
||||
console.log('========================================');
|
||||
console.log('[SUCCESS] 书签处理完成!');
|
||||
console.log('========================================');
|
||||
} catch (error) {
|
||||
console.error('[FATAL] 处理书签文件时发生错误:', error);
|
||||
console.error('[ERROR] 错误堆栈:', error.stack);
|
||||
process.exit(1);
|
||||
// 如果是自定义错误,直接抛出
|
||||
if (error instanceof FileError) {
|
||||
throw error;
|
||||
}
|
||||
// 否则包装为 FileError
|
||||
throw new FileError('处理书签文件时发生错误', null, [
|
||||
'检查书签 HTML 文件格式是否正确',
|
||||
'确认配置目录结构是否完整',
|
||||
`错误详情: ${error.message}`,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 启动处理
|
||||
if (require.main === module) {
|
||||
main().catch((err) => {
|
||||
console.error('Unhandled error in bookmark processing:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
wrapAsyncError(main)();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// 生成端薄入口:保持对外导出稳定,内部实现位于 src/generator/main.js
|
||||
const impl = require('./generator/main');
|
||||
const { wrapAsyncError } = require('./generator/utils/errors');
|
||||
|
||||
module.exports = impl;
|
||||
|
||||
if (require.main === module) {
|
||||
if (typeof impl.main === 'function') {
|
||||
impl.main();
|
||||
wrapAsyncError(impl.main)();
|
||||
} else {
|
||||
console.error('generator main() 未导出,无法直接执行。');
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1
src/generator/cache/articles.js
vendored
1
src/generator/cache/articles.js
vendored
@@ -156,4 +156,3 @@ module.exports = {
|
||||
tryLoadArticlesFeedCache,
|
||||
buildArticlesCategoriesByPageCategories,
|
||||
};
|
||||
|
||||
|
||||
5
src/generator/cache/projects.js
vendored
5
src/generator/cache/projects.js
vendored
@@ -4,7 +4,9 @@ const path = require('path');
|
||||
function tryLoadProjectsRepoCache(pageId, config) {
|
||||
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 =
|
||||
config && config.site && config.site.github && config.site.github.cacheDir
|
||||
? String(config.site.github.cacheDir)
|
||||
@@ -132,4 +134,3 @@ module.exports = {
|
||||
applyRepoMetaToCategories,
|
||||
buildProjectsMeta,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
const fs = require('node:fs');
|
||||
const { loadModularConfig } = require('./loader');
|
||||
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 { ConfigError } = require('../utils/errors');
|
||||
|
||||
function loadConfig() {
|
||||
let config = {
|
||||
@@ -18,16 +25,17 @@ function loadConfig() {
|
||||
|
||||
if (hasUserModularConfig) {
|
||||
if (!fs.existsSync('config/user/site.yml')) {
|
||||
console.error('[ERROR] 检测到 config/user/ 目录,但缺少 config/user/site.yml。');
|
||||
console.error(
|
||||
'[ERROR] 由于配置采用"完全替换"策略,系统不会从 config/_default/ 补齐缺失配置。'
|
||||
);
|
||||
console.error('[ERROR] 解决方法:先完整复制 config/_default/ 到 config/user/,再按需修改。');
|
||||
process.exit(1);
|
||||
throw new ConfigError('检测到 config/user/ 目录,但缺少 config/user/site.yml', [
|
||||
'由于配置采用"完全替换"策略,系统不会从 config/_default/ 补齐缺失配置',
|
||||
'解决方法:先完整复制 config/_default/ 到 config/user/,再按需修改',
|
||||
'参考文档: config/README.md',
|
||||
]);
|
||||
}
|
||||
|
||||
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/,再按需修改。');
|
||||
}
|
||||
|
||||
@@ -35,10 +43,11 @@ function loadConfig() {
|
||||
} else if (hasDefaultModularConfig) {
|
||||
config = loadModularConfig('config/_default');
|
||||
} else {
|
||||
console.error('[ERROR] 未找到可用配置:缺少 config/user/ 或 config/_default/。');
|
||||
console.error('[ERROR] 本版本已不再支持旧版单文件配置(config.yml / config.yaml)。');
|
||||
console.error('[ERROR] 解决方法:使用模块化配置目录(建议从 config/_default/ 复制到 config/user/ 再修改)。');
|
||||
process.exit(1);
|
||||
throw new ConfigError('未找到可用配置:缺少 config/user/ 或 config/_default/', [
|
||||
'本版本已不再支持旧版单文件配置(config.yml / config.yaml)',
|
||||
'解决方法:使用模块化配置目录(建议从 config/_default/ 复制到 config/user/ 再修改)',
|
||||
'参考文档: config/README.md',
|
||||
]);
|
||||
}
|
||||
|
||||
config = ensureConfigDefaults(config);
|
||||
|
||||
@@ -27,8 +27,7 @@ function resolveTemplateNameForPage(pageId, config) {
|
||||
if (!pageId) return 'page';
|
||||
|
||||
const pageConfig = config && config[pageId] ? config[pageId] : null;
|
||||
const explicit =
|
||||
pageConfig && pageConfig.template ? String(pageConfig.template).trim() : '';
|
||||
const explicit = pageConfig && pageConfig.template ? String(pageConfig.template).trim() : '';
|
||||
if (explicit) return explicit;
|
||||
|
||||
const candidatePath = path.join(process.cwd(), 'templates', 'pages', `${pageId}.hbs`);
|
||||
|
||||
@@ -2,7 +2,8 @@ const { escapeHtml } = require('../utils/html');
|
||||
|
||||
// 生成 GitHub Pages 的 404 回跳页:将 /<id> 形式的路径深链接转换为 /?page=<id>
|
||||
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);
|
||||
|
||||
return `<!doctype html>
|
||||
|
||||
@@ -205,4 +205,3 @@ module.exports = {
|
||||
generatePageContent,
|
||||
generateSearchResultsPage,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getSubmenuForNavItem, assignCategorySlugs } = require('../config');
|
||||
const { tryLoadArticlesFeedCache, buildArticlesCategoriesByPageCategories } = require('../cache/articles');
|
||||
const { tryLoadProjectsRepoCache, applyRepoMetaToCategories, buildProjectsMeta } = require('../cache/projects');
|
||||
const {
|
||||
tryLoadArticlesFeedCache,
|
||||
buildArticlesCategoriesByPageCategories,
|
||||
} = require('../cache/articles');
|
||||
const {
|
||||
tryLoadProjectsRepoCache,
|
||||
applyRepoMetaToCategories,
|
||||
buildProjectsMeta,
|
||||
} = require('../cache/projects');
|
||||
const { getPageConfigUpdatedAtMeta } = require('../utils/pageMeta');
|
||||
|
||||
function prepareNavigationData(pageId, config) {
|
||||
@@ -32,7 +39,12 @@ function resolveTemplateName(pageId, data) {
|
||||
let templateName = explicitTemplate || pageId;
|
||||
|
||||
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)) {
|
||||
templateName = 'page';
|
||||
}
|
||||
|
||||
@@ -3,14 +3,29 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
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 { generateNavigation, generateCategories, generateSocialLinks } = require('./html/components');
|
||||
const {
|
||||
generateNavigation,
|
||||
generateCategories,
|
||||
generateSocialLinks,
|
||||
} = require('./html/components');
|
||||
const { generate404Html } = require('./html/404');
|
||||
const { generateFontLinks, generateFontCss } = require('./html/fonts');
|
||||
const { preparePageData } = require('./html/page-data');
|
||||
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) {
|
||||
try {
|
||||
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)) {
|
||||
fs.copyFileSync(config.site.favicon, `dist/${path.basename(config.site.favicon)}`);
|
||||
} else {
|
||||
@@ -270,20 +288,36 @@ function main() {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error bundling runtime script:', error);
|
||||
process.exit(1);
|
||||
throw new BuildError('构建运行时脚本失败', {
|
||||
脚本路径: 'scripts/build-runtime.js',
|
||||
错误信息: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
// 复制静态文件
|
||||
copyStaticFiles(config);
|
||||
} 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) {
|
||||
main();
|
||||
// 使用 wrapAsyncError 包装主函数,自动处理错误
|
||||
wrapAsyncError(main)();
|
||||
}
|
||||
|
||||
// 导出供测试使用的函数
|
||||
|
||||
127
src/generator/utils/errors.js
Normal file
127
src/generator/utils/errors.js
Normal 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,
|
||||
};
|
||||
@@ -14,4 +14,3 @@ function escapeHtml(unsafe) {
|
||||
module.exports = {
|
||||
escapeHtml,
|
||||
};
|
||||
|
||||
|
||||
@@ -98,4 +98,3 @@ function getPageConfigUpdatedAtMeta(pageId) {
|
||||
module.exports = {
|
||||
getPageConfigUpdatedAtMeta,
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ function collectSitesRecursively(node, output) {
|
||||
|
||||
if (Array.isArray(node.subcategories))
|
||||
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))
|
||||
node.subgroups.forEach((child) => collectSitesRecursively(child, output));
|
||||
|
||||
@@ -32,4 +33,3 @@ module.exports = {
|
||||
normalizeUrlKey,
|
||||
collectSitesRecursively,
|
||||
};
|
||||
|
||||
|
||||
@@ -7,11 +7,16 @@ function detectHomePageId() {
|
||||
// 1) 优先从生成端注入的配置数据读取(保持与实际导航顺序一致)
|
||||
try {
|
||||
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 =
|
||||
config && config.data && config.data.homePageId ? String(config.data.homePageId).trim() : '';
|
||||
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() : '';
|
||||
if (firstId) return firstId;
|
||||
} catch (error) {
|
||||
@@ -109,4 +114,3 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
initRouting(state, dom, { ui, search });
|
||||
});
|
||||
|
||||
|
||||
@@ -49,7 +49,8 @@ module.exports = function initRouting(state, dom, api) {
|
||||
state.pages = document.querySelectorAll('.page');
|
||||
|
||||
// 方案 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 id = normalizeText(pageId);
|
||||
@@ -164,7 +165,9 @@ module.exports = function initRouting(state, dom, api) {
|
||||
const escapedId = escapeSelector(categoryId);
|
||||
targetCategory =
|
||||
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 scrollPosition = contentElement.scrollTop + rect.top - containerRect.top - desiredPosition;
|
||||
const scrollPosition =
|
||||
contentElement.scrollTop + rect.top - containerRect.top - desiredPosition;
|
||||
|
||||
// 执行滚动
|
||||
contentElement.scrollTo({
|
||||
@@ -234,7 +238,8 @@ module.exports = function initRouting(state, dom, api) {
|
||||
// 支持 ?page=<id> 直接打开对应页面;无效时回退到首页
|
||||
const rawPageIdFromUrl = getRawPageIdFromUrl();
|
||||
const validatedPageIdFromUrl = getPageIdFromUrl();
|
||||
const initialPageId = validatedPageIdFromUrl || (isValidPageId(state.homePageId) ? state.homePageId : 'home');
|
||||
const initialPageId =
|
||||
validatedPageIdFromUrl || (isValidPageId(state.homePageId) ? state.homePageId : 'home');
|
||||
|
||||
setActiveNavByPageId(initialPageId);
|
||||
showPage(initialPageId);
|
||||
@@ -400,4 +405,3 @@ module.exports = function initRouting(state, dom, api) {
|
||||
|
||||
return { showPage };
|
||||
};
|
||||
|
||||
|
||||
@@ -48,18 +48,25 @@ module.exports = function initSearch(state, dom) {
|
||||
|
||||
// 兼容不同页面/卡片样式:优先取可见文本,其次回退到 data-*(确保 projects repo 卡片也能被搜索)
|
||||
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 =
|
||||
card.querySelector('h3')?.textContent || card.querySelector('.repo-title')?.textContent || dataTitle;
|
||||
card.querySelector('h3')?.textContent ||
|
||||
card.querySelector('.repo-title')?.textContent ||
|
||||
dataTitle;
|
||||
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 description = String(descriptionText || '').toLowerCase();
|
||||
const url = card.href || card.getAttribute('href') || '#';
|
||||
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({
|
||||
@@ -111,7 +118,9 @@ module.exports = function initSearch(state, dom) {
|
||||
|
||||
// 使用更高效的搜索算法
|
||||
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)
|
||||
);
|
||||
});
|
||||
|
||||
// 按页面分组结果
|
||||
|
||||
@@ -47,7 +47,9 @@ module.exports = function highlightSearchTerm(card, searchTerm) {
|
||||
|
||||
while ((match = regex.exec(rawText)) !== null) {
|
||||
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');
|
||||
@@ -87,4 +89,3 @@ module.exports = function highlightSearchTerm(card, searchTerm) {
|
||||
console.error('Error highlighting search term');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -22,4 +22,3 @@ module.exports = {
|
||||
url: 'https://duckduckgo.com/?q=',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -178,4 +178,3 @@ module.exports = function initUi(state, dom) {
|
||||
initSidebarState,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -15,4 +15,3 @@ require('./app');
|
||||
|
||||
// tooltip 独立模块:内部会按需监听 DOMContentLoaded
|
||||
require('./tooltip');
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ module.exports = function addElement(type, parentId, data) {
|
||||
const pageEl = parent.closest('.page');
|
||||
const pageId = pageEl && pageEl.id ? String(pageEl.id).trim() : '';
|
||||
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 = '';
|
||||
|
||||
@@ -27,12 +29,16 @@ module.exports = function addElement(type, parentId, data) {
|
||||
: null;
|
||||
|
||||
const templateFromMap =
|
||||
pageTemplates && pageId && pageTemplates[pageId] ? String(pageTemplates[pageId]).trim() : '';
|
||||
pageTemplates && pageId && pageTemplates[pageId]
|
||||
? String(pageTemplates[pageId]).trim()
|
||||
: '';
|
||||
|
||||
// 兼容旧版:cfg.data[pageId].template
|
||||
const legacyPageConfig = cfg && cfg.data && pageId ? cfg.data[pageId] : null;
|
||||
const templateFromLegacy =
|
||||
legacyPageConfig && legacyPageConfig.template ? String(legacyPageConfig.template).trim() : '';
|
||||
legacyPageConfig && legacyPageConfig.template
|
||||
? String(legacyPageConfig.template).trim()
|
||||
: '';
|
||||
|
||||
if (templateFromMap) {
|
||||
templateName = templateFromMap;
|
||||
@@ -55,16 +61,22 @@ module.exports = function addElement(type, parentId, data) {
|
||||
const siteIcon = data.icon || 'fas fa-link';
|
||||
const siteDescription = data.description || (data.url ? menavExtractDomain(data.url) : '');
|
||||
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 =
|
||||
siteForceIconModeRaw === 'manual' || siteForceIconModeRaw === 'favicon' ? siteForceIconModeRaw : '';
|
||||
siteForceIconModeRaw === 'manual' || siteForceIconModeRaw === 'favicon'
|
||||
? siteForceIconModeRaw
|
||||
: '';
|
||||
|
||||
const safeSiteUrl = menavSanitizeUrl(siteUrl, 'addElement(site).url');
|
||||
const safeSiteIcon = menavSanitizeClassList(siteIcon, 'addElement(site).icon');
|
||||
|
||||
newSite.setAttribute('href', safeSiteUrl);
|
||||
newSite.title = siteName + (siteDescription ? ' - ' + siteDescription : '');
|
||||
newSite.setAttribute('data-tooltip', siteName + (siteDescription ? ' - ' + siteDescription : ''));
|
||||
newSite.setAttribute(
|
||||
'data-tooltip',
|
||||
siteName + (siteDescription ? ' - ' + siteDescription : '')
|
||||
);
|
||||
if (/^https?:\/\//i.test(safeSiteUrl)) {
|
||||
newSite.target = '_blank';
|
||||
newSite.rel = 'noopener';
|
||||
@@ -185,8 +197,11 @@ module.exports = function addElement(type, parentId, data) {
|
||||
// favicon 模式:优先加载 faviconUrl;否则按 url 生成
|
||||
try {
|
||||
const cfg =
|
||||
window.MeNav && typeof window.MeNav.getConfig === 'function' ? window.MeNav.getConfig() : null;
|
||||
const iconsMode = cfg && cfg.icons && cfg.icons.mode ? String(cfg.icons.mode).trim() : 'favicon';
|
||||
window.MeNav && typeof window.MeNav.getConfig === 'function'
|
||||
? window.MeNav.getConfig()
|
||||
: null;
|
||||
const iconsMode =
|
||||
cfg && cfg.icons && cfg.icons.mode ? String(cfg.icons.mode).trim() : 'favicon';
|
||||
const iconsRegion =
|
||||
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 注入)
|
||||
const titleEl = document.createElement('h2');
|
||||
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(document.createTextNode(' ' + String(data.name || '未命名分类')));
|
||||
|
||||
|
||||
@@ -44,7 +44,8 @@ function findDefaultElement(type, id) {
|
||||
|
||||
// 全局 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, {
|
||||
version: menavDetectVersion(),
|
||||
|
||||
@@ -107,7 +107,10 @@ module.exports = function updateElement(type, id, newData) {
|
||||
if (newData.icon) {
|
||||
const iconElement = element.querySelector('i');
|
||||
if (iconElement) {
|
||||
iconElement.className = menavSanitizeClassList(newData.icon, 'updateElement(nav-item).icon');
|
||||
iconElement.className = menavSanitizeClassList(
|
||||
newData.icon,
|
||||
'updateElement(nav-item).icon'
|
||||
);
|
||||
}
|
||||
element.setAttribute(
|
||||
'data-icon',
|
||||
|
||||
@@ -180,7 +180,9 @@ function registerNestedApi() {
|
||||
if (!activePage) return;
|
||||
|
||||
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;
|
||||
|
||||
// 如果收起的数量 >= 总数的一半,执行展开;否则执行收起
|
||||
|
||||
@@ -139,4 +139,3 @@ module.exports = {
|
||||
menavDetectVersion,
|
||||
menavUpdateAppHeight,
|
||||
};
|
||||
|
||||
|
||||
@@ -112,4 +112,3 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
hoverMedia.addListener(syncTooltipEnabled);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user