refactor: 统一错误处理机制
- 引入 ConfigError/TemplateError/BuildError/FileError 与 wrapAsyncError,统一错误输出 - generator 入口接入 wrapAsyncError,确保命令行执行路径一致 - 兜底逻辑使用 instanceof,保留 BuildError/TemplateError 上下文信息 - 合并格式化提交(仅缩进/换行调整)
This commit is contained in:
1
.github/workflows/deploy.yml
vendored
1
.github/workflows/deploy.yml
vendored
@@ -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-effort):projects 仓库信息、articles RSS 聚合
|
# 同步时效性数据(best-effort):projects 仓库信息、articles RSS 聚合
|
||||||
# 说明:
|
# 说明:
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
src/generator/cache/articles.js
vendored
1
src/generator/cache/articles.js
vendored
@@ -156,4 +156,3 @@ module.exports = {
|
|||||||
tryLoadArticlesFeedCache,
|
tryLoadArticlesFeedCache,
|
||||||
buildArticlesCategoriesByPageCategories,
|
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) {
|
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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`);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -205,4 +205,3 @@ module.exports = {
|
|||||||
generatePageContent,
|
generatePageContent,
|
||||||
generateSearchResultsPage,
|
generateSearchResultsPage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出供测试使用的函数
|
// 导出供测试使用的函数
|
||||||
|
|||||||
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 = {
|
module.exports = {
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -98,4 +98,3 @@ function getPageConfigUpdatedAtMeta(pageId) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
getPageConfigUpdatedAtMeta,
|
getPageConfigUpdatedAtMeta,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 按页面分组结果
|
// 按页面分组结果
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,4 +22,3 @@ module.exports = {
|
|||||||
url: 'https://duckduckgo.com/?q=',
|
url: 'https://duckduckgo.com/?q=',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -178,4 +178,3 @@ module.exports = function initUi(state, dom) {
|
|||||||
initSidebarState,
|
initSidebarState,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,4 +15,3 @@ require('./app');
|
|||||||
|
|
||||||
// tooltip 独立模块:内部会按需监听 DOMContentLoaded
|
// tooltip 独立模块:内部会按需监听 DOMContentLoaded
|
||||||
require('./tooltip');
|
require('./tooltip');
|
||||||
|
|
||||||
|
|||||||
@@ -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 || '未命名分类')));
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
// 如果收起的数量 >= 总数的一半,执行展开;否则执行收起
|
// 如果收起的数量 >= 总数的一半,执行展开;否则执行收起
|
||||||
|
|||||||
@@ -139,4 +139,3 @@ module.exports = {
|
|||||||
menavDetectVersion,
|
menavDetectVersion,
|
||||||
menavUpdateAppHeight,
|
menavUpdateAppHeight,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -112,4 +112,3 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
hoverMedia.addListener(syncTooltipEnabled);
|
hoverMedia.addListener(syncTooltipEnabled);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,4 +13,3 @@ test('P1-5:404.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 执行回跳');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 字段');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user