refactor: 优化重构后的系统并添加文档

This commit is contained in:
Zuoling Rong
2025-05-09 00:08:01 +08:00
parent 9ea6cb1f09
commit 4c3c5d23fa
13 changed files with 960 additions and 659 deletions

View File

@@ -1,7 +0,0 @@
- name: 首页
icon: fas fa-home
id: home
active: true
- name: 书签
icon: fas fa-bookmark
id: bookmarks

View File

@@ -1,29 +0,0 @@
title: 我的书签
subtitle: 从浏览器导入的书签收藏
categories:
- name: 技术资源
icon: fas fa-folder
sites:
- name: GitHub
url: https://github.com/
icon: fab fa-github
description: "从书签导入: GitHub"
- name: Stack Overflow
url: https://stackoverflow.com/
icon: fab fa-stack-overflow
description: "从书签导入: Stack Overflow"
- name: MDN Web Docs
url: https://developer.mozilla.org/
icon: fas fa-link
description: "从书签导入: MDN Web Docs"
- name: 社交媒体
icon: fas fa-folder
sites:
- name: Twitter
url: https://twitter.com/
icon: fab fa-twitter
description: "从书签导入: Twitter"
- name: LinkedIn
url: https://www.linkedin.com/
icon: fab fa-linkedin
description: "从书签导入: LinkedIn"

View File

@@ -1,100 +0,0 @@
categories:
- name: 常用网站
icon: fas fa-star
sites:
- name: Linux.do
url: https://linux.do/
icon: fab fa-linux
description: 新的理想型社区
- name: Google
url: https://www.google.com
icon: fab fa-google
description: 全球最大的搜索引擎
- name: GitHub
url: https://www.github.com
icon: fab fa-github
description: 代码托管平台
- name: Stack Overflow
url: https://stackoverflow.com
icon: fab fa-stack-overflow
description: 程序员问答社区
- name: ChatGPT
url: https://chat.openai.com
icon: fas fa-robot
description: AI智能助手
- name: 学习资源
icon: fas fa-graduation-cap
sites:
- name: 哔哩哔哩
url: https://www.bilibili.com
icon: fas fa-play-circle
description: 视频学习平台
- name: 知乎
url: https://www.zhihu.com
icon: fas fa-question-circle
description: 问答社区
- name: 掘金
url: https://juejin.cn
icon: fas fa-book
description: 高质量技术社区
- name: LeetCode
url: https://leetcode.cn
icon: fas fa-code
description: 算法刷题平台
- name: 开发工具
icon: fas fa-tools
sites:
- name: VS Code
url: https://code.visualstudio.com
icon: fas fa-code
description: 强大的代码编辑器
- name: Postman
url: https://www.postman.com
icon: fas fa-paper-plane
description: API调试工具
- name: Git
url: https://git-scm.com
icon: fab fa-git-alt
description: 版本控制工具
- name: Docker
url: https://www.docker.com
icon: fab fa-docker
description: 容器化平台
- name: 设计资源
icon: fas fa-palette
sites:
- name: Figma
url: https://www.figma.com
icon: fab fa-figma
description: 在线设计工具
- name: Dribbble
url: https://dribbble.com
icon: fab fa-dribbble
description: 设计师社区
- name: Behance
url: https://www.behance.net
icon: fab fa-behance
description: 创意设计平台
- name: IconFont
url: https://www.iconfont.cn
icon: fas fa-icons
description: 图标资源库
- name: 在线工具
icon: fas fa-wrench
sites:
- name: JSON Editor
url: https://jsoneditoronline.org
icon: fas fa-code-branch
description: JSON在线编辑器
- name: Can I Use
url: https://caniuse.com
icon: fas fa-browser
description: 浏览器兼容性查询
- name: TinyPNG
url: https://tinypng.com
icon: fas fa-compress
description: 图片压缩工具
- name: Carbon
url: https://carbon.now.sh
icon: fas fa-code
description: 代码图片生成器

View File

@@ -1,38 +0,0 @@
title: 我的导航
description: 个人网络导航站
author: Your Name
favicon: favicon.ico
logo_text: 导航站11
fonts:
title:
family: Poppins
weight: 600
source: google
subtitle:
family: Quicksand
weight: 500
source: google
body:
family: Noto Sans SC
weight: 400
source: google
profile:
title: Hello,
subtitle: Welcome to My Navigation
description: 导航菜单
social:
- name: GitHub
url: https://github.com
icon: fab fa-github
- name: Telegram
url: https://t.me
icon: fab fa-telegram
- name: Twitter
url: https://twitter.com
icon: fab fa-twitter
- name: Steam
url: https://steam.com
icon: fab fa-steam

57
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"ansi-styles": "^6.2.1",
"color-convert": "^2.0.1",
"color-name": "^2.0.0",
"handlebars": "^4.7.8",
"has-flag": "^5.0.1",
"js-yaml": "^4.1.0",
"mime-db": "^1.52.0",
@@ -522,6 +523,27 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/handlebars": {
"version": "4.7.8",
"resolved": "https://registry.npmmirror.com/handlebars/-/handlebars-4.7.8.tgz",
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"wordwrap": "^1.0.0"
},
"bin": {
"handlebars": "bin/handlebars"
},
"engines": {
"node": ">=0.4.7"
},
"optionalDependencies": {
"uglify-js": "^3.1.4"
}
},
"node_modules/has-flag": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-5.0.1.tgz",
@@ -708,7 +730,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -731,6 +752,12 @@
"node": ">= 0.6"
}
},
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"license": "MIT"
},
"node_modules/npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
@@ -966,6 +993,15 @@
"dev": true,
"license": "ISC"
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -1045,6 +1081,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/uglify-js": {
"version": "3.19.3",
"resolved": "https://registry.npmmirror.com/uglify-js/-/uglify-js-3.19.3.tgz",
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
"license": "BSD-2-Clause",
"optional": true,
"bin": {
"uglifyjs": "bin/uglifyjs"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/update-check": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz",
@@ -1108,6 +1157,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
"license": "MIT"
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",

103
src/README.md Normal file
View File

@@ -0,0 +1,103 @@
# MeNav 源代码目录结构
## 目录概述
`src` 目录包含 MeNav 项目的所有源代码,按照功能和职责进行了明确的分层组织:
- 根目录:核心功能实现
- `helpers` 子目录:辅助功能实现
## 源代码结构分工
### 根目录函数(核心功能)
根目录下的js文件实现了应用的核心功能每个文件负责特定领域
- **generator.js**: 站点生成器的核心实现
- 负责将配置文件转换为HTML网站
- 处理模板渲染、配置解析和文件输出
- 控制整个生成流程
- **script.js**: 客户端功能的核心实现
- 处理用户界面交互逻辑
- 实现搜索、导航、主题切换等功能
- 管理页面状态和响应式布局
- **bookmark-processor.js**: 书签处理工具
- 转换浏览器书签为MeNav配置格式
- 提供书签导入功能
- **migrate-config.js**: 配置迁移工具
- 处理配置文件格式升级
- 确保向后兼容性
### helpers 目录函数(辅助功能)
`helpers` 目录下的函数提供辅助性支持主要服务于Handlebars模板系统
- **formatters.js**: 格式化函数
- 日期格式化
- 文本处理
- 内容展示辅助
- **conditions.js**: 条件判断函数
- 比较操作
- 逻辑运算
- 条件检查
- **utils.js**: 工具函数
- 数组处理
- 对象操作
- 通用辅助方法
- **index.js**: 助手函数注册中心
- 统一导入所有助手函数
- 提供注册函数到Handlebars实例的方法
- 定义核心HTML处理函数
## 函数职责分工
### 核心函数(根目录)
根目录下的函数负责"做什么"——实现具体的业务逻辑和功能:
- 应用入口和流程控制
- 用户交互响应
- 数据处理和转换
- 文件生成和输出
这些函数通常:
- 直接面向最终用户需求
- 控制程序主流程
- 调用辅助函数完成特定任务
### 辅助函数helpers目录
helpers目录下的函数负责"怎么做"——提供可重用的工具方法:
- 为模板渲染提供扩展能力
- 处理数据格式化和转换
- 提供通用的条件判断逻辑
- 实现常见的工具操作
这些函数通常:
- 高度重用性和通用性
- 专注于单一职责
- 被核心函数调用
## 扩展和修改指南
### 添加新的核心功能
如需添加新的核心功能应在src根目录创建新的js文件或修改现有文件确保
- 文件名清晰反映功能用途
- 保持单一责任原则
- 适当调用辅助函数,避免重复实现通用功能
### 添加新的辅助函数
如需添加新的辅助函数应在helpers目录下相应文件中添加并在index.js中注册
- 按功能类型放入正确的文件formatters/conditions/utils
- 编写清晰的JSDoc注释
- 更新README.md文档
- 在index.js中添加导出和注册

View File

@@ -74,7 +74,6 @@ function getLatestBookmarkFile() {
try {
// 确保书签目录存在
if (!fs.existsSync(BOOKMARKS_DIR)) {
console.log('Creating bookmarks directory');
fs.mkdirSync(BOOKMARKS_DIR, { recursive: true });
return null;
}
@@ -84,7 +83,6 @@ function getLatestBookmarkFile() {
.filter(file => file.toLowerCase().endsWith('.html'));
if (files.length === 0) {
console.log('No bookmark HTML files found');
return null;
}
@@ -97,7 +95,6 @@ function getLatestBookmarkFile() {
// 找出最新的文件
fileStats.sort((a, b) => b.mtime - a.mtime);
const latestFile = fileStats[0].file;
console.log(`Found latest bookmark file: ${latestFile}`);
return path.join(BOOKMARKS_DIR, latestFile);
} catch (error) {
@@ -213,8 +210,6 @@ ${yamlString}`;
// 更新导航以包含书签页面
function updateNavigationWithBookmarks() {
console.log('Checking navigation configuration for bookmarks page...');
// 模块化配置文件
const modularUserNavFile = path.join(CONFIG_USER_DIR, 'navigation.yml');
const modularDefaultNavFile = 'config/_default/navigation.yml';
@@ -226,9 +221,6 @@ function updateNavigationWithBookmarks() {
// 1. 首选: 模块化用户导航配置
if (fs.existsSync(modularUserNavFile)) {
navigationUpdated = updateNavigationFile(modularUserNavFile);
if (navigationUpdated) {
console.log(`Updated modular user navigation file: ${modularUserNavFile}`);
}
}
// 2. 其次: 模块化默认导航配置
else if (fs.existsSync(modularDefaultNavFile)) {
@@ -245,59 +237,13 @@ function updateNavigationWithBookmarks() {
// 写入用户导航文件
fs.writeFileSync(modularUserNavFile, defaultNavContent, 'utf8');
console.log(`Created user navigation file based on default: ${modularUserNavFile}`);
// 更新新创建的文件
navigationUpdated = updateNavigationFile(modularUserNavFile);
if (navigationUpdated) {
console.log(`Updated newly created navigation file: ${modularUserNavFile}`);
}
} catch (error) {
console.error(`Error creating user navigation file:`, error);
}
}
// 3. 如果都不存在,创建一个基本的导航文件
else {
try {
// 确保目录存在
if (!fs.existsSync(CONFIG_USER_DIR)) {
fs.mkdirSync(CONFIG_USER_DIR, { recursive: true });
}
// 创建基本导航配置
const basicNav = [
{
name: "首页",
icon: "fas fa-home",
id: "home",
active: true
},
{
name: "书签",
icon: "fas fa-bookmark",
id: "bookmarks",
active: false
}
];
// 写入用户导航文件
fs.writeFileSync(
modularUserNavFile,
yaml.dump(basicNav, { indent: 2, lineWidth: -1, quotingType: '"' }),
'utf8'
);
console.log(`Created basic navigation file: ${modularUserNavFile}`);
navigationUpdated = true;
} catch (error) {
console.error(`Error creating basic navigation file:`, error);
}
}
if (!navigationUpdated) {
console.log('Did not update any navigation configuration with bookmarks page');
}
return navigationUpdated;
}
// 更新单个导航配置文件
@@ -344,31 +290,22 @@ function updateNavigationFile(filePath) {
// 主函数
async function main() {
console.log('Starting bookmark processing...');
// 获取最新的书签文件
const bookmarkFile = getLatestBookmarkFile();
if (!bookmarkFile) {
console.log('No bookmark file to process.');
return;
}
try {
// 读取文件内容
console.log(`Reading bookmark file: ${bookmarkFile}`);
const htmlContent = fs.readFileSync(bookmarkFile, 'utf8');
// 解析书签
const bookmarks = parseBookmarks(htmlContent);
console.log(`Found ${bookmarks.categories.length} categories with bookmarks`);
if (bookmarks.categories.length === 0) {
console.error('ERROR: No bookmark categories found in the HTML file. Processing aborted.');
return;
}
console.log('Categories found:');
bookmarks.categories.forEach(cat => {
console.log(`- ${cat.name}: ${cat.sites.length} sites`);
});
// 生成YAML
const yaml = generateBookmarksYaml(bookmarks);
@@ -377,35 +314,23 @@ async function main() {
return;
}
// 显示将要写入的YAML前几行
console.log('Generated YAML preview (first 5 lines):');
console.log(yaml.split('\n').slice(0, 5).join('\n') + '\n...');
try {
// 确保目标目录存在
if (!fs.existsSync(CONFIG_USER_PAGES_DIR)) {
console.log(`Creating output directory structure: ${CONFIG_USER_PAGES_DIR}`);
fs.mkdirSync(CONFIG_USER_PAGES_DIR, { recursive: true });
}
// 保存YAML到模块化位置
console.log(`Writing bookmarks configuration to: ${MODULAR_OUTPUT_FILE}`);
fs.writeFileSync(MODULAR_OUTPUT_FILE, yaml, 'utf8');
// 验证文件是否确实被创建
if (fs.existsSync(MODULAR_OUTPUT_FILE)) {
const stats = fs.statSync(MODULAR_OUTPUT_FILE);
console.log(`Successfully saved bookmarks configuration (${stats.size} bytes)`);
} else {
if (!fs.existsSync(MODULAR_OUTPUT_FILE)) {
console.error(`ERROR: File was not created: ${MODULAR_OUTPUT_FILE}`);
process.exit(1);
}
// 更新导航
updateNavigationWithBookmarks();
// 不再删除原始HTML文件留给GitHub Actions处理
console.log(`Processing complete. HTML files will be cleaned up by GitHub Actions workflow.`);
} catch (writeError) {
console.error(`ERROR writing file:`, writeError);
process.exit(1);

View File

@@ -28,7 +28,6 @@ function loadHandlebarsTemplates() {
const layoutPath = path.join(layoutsDir, file);
const layoutContent = fs.readFileSync(layoutPath, 'utf8');
handlebars.registerPartial(layoutName, layoutContent);
console.log(`Registered layout template: ${layoutName}`);
}
});
} else {
@@ -44,23 +43,45 @@ function loadHandlebarsTemplates() {
const componentPath = path.join(componentsDir, file);
const componentContent = fs.readFileSync(componentPath, 'utf8');
handlebars.registerPartial(componentName, componentContent);
console.log(`Registered component template: ${componentName}`);
}
});
} else {
throw new Error('Components directory not found. Cannot proceed without component templates.');
}
// 识别并注册默认布局模板
// 识别并检查默认布局模板是否存在
const defaultLayoutPath = path.join(layoutsDir, 'default.hbs');
if (fs.existsSync(defaultLayoutPath)) {
console.log('Default layout template found and registered.');
return true;
} else {
if (!fs.existsSync(defaultLayoutPath)) {
throw new Error('Default layout template not found. Cannot proceed without default layout.');
}
}
/**
* 获取默认布局模板
* @returns {Object} 包含模板路径和编译的模板函数
*/
function getDefaultLayoutTemplate() {
const defaultLayoutPath = path.join(process.cwd(), 'templates', 'layouts', 'default.hbs');
// 检查默认布局模板是否存在
if (!fs.existsSync(defaultLayoutPath)) {
throw new Error('Default layout template not found. Cannot proceed without default layout.');
}
try {
// 读取布局内容并编译模板
const layoutContent = fs.readFileSync(defaultLayoutPath, 'utf8');
const layoutTemplate = handlebars.compile(layoutContent);
return {
path: defaultLayoutPath,
template: layoutTemplate
};
} catch (error) {
throw new Error(`Error loading default layout template: ${error.message}`);
}
}
// 渲染Handlebars模板函数
function renderTemplate(templateName, data, useLayout = true) {
const templatePath = path.join(process.cwd(), 'templates', 'pages', `${templateName}.hbs`);
@@ -82,23 +103,16 @@ function renderTemplate(templateName, data, useLayout = true) {
return pageContent;
}
// 使用布局模板
const defaultLayoutPath = path.join(process.cwd(), 'templates', 'layouts', 'default.hbs');
if (!fs.existsSync(defaultLayoutPath)) {
throw new Error('Default layout template not found. Cannot proceed without layout.');
}
try {
// 使用辅助函数获取默认布局模板
const { template: layoutTemplate } = getDefaultLayoutTemplate();
// 准备布局数据,包含页面内容
const layoutData = {
...data,
body: pageContent
};
// 加载默认布局模板
const layoutContent = fs.readFileSync(defaultLayoutPath, 'utf8');
const layoutTemplate = handlebars.compile(layoutContent);
// 渲染完整页面
return layoutTemplate(layoutData);
} catch (layoutError) {
@@ -123,23 +137,31 @@ function escapeHtml(unsafe) {
}
/**
* 加载单个配置文件
* 统一处理配置文件加载错误
* @param {string} filePath 配置文件路径
* @param {Error} error 错误对象
*/
function handleConfigLoadError(filePath, error) {
console.error(`Error loading configuration from ${filePath}:`, error);
}
/**
* 安全地加载YAML配置文件
* @param {string} filePath 配置文件路径
* @returns {Object|null} 配置对象如果文件不存在或加载失败则返回null
*/
function loadSingleConfig(filePath) {
if (fs.existsSync(filePath)) {
try {
const fileContent = fs.readFileSync(filePath, 'utf8');
const fileConfig = yaml.load(fileContent);
console.log(`Loaded configuration from ${filePath}`);
return fileConfig;
} catch (e) {
console.error(`Error loading configuration from ${filePath}:`, e);
return null;
}
}
function safeLoadYamlConfig(filePath) {
if (!fs.existsSync(filePath)) {
return null;
}
try {
const fileContent = fs.readFileSync(filePath, 'utf8');
return yaml.load(fileContent);
} catch (error) {
handleConfigLoadError(filePath, error);
return null;
}
}
/**
@@ -163,34 +185,22 @@ function loadModularConfig(dirPath) {
// 加载基础配置
const siteConfigPath = path.join(dirPath, 'site.yml');
if (fs.existsSync(siteConfigPath)) {
try {
const fileContent = fs.readFileSync(siteConfigPath, 'utf8');
const siteConfig = yaml.load(fileContent);
// 将site.yml中的内容分配到正确的配置字段
config.site = siteConfig;
// 提取特殊字段到顶层配置
if (siteConfig.fonts) config.fonts = siteConfig.fonts;
if (siteConfig.profile) config.profile = siteConfig.profile;
if (siteConfig.social) config.social = siteConfig.social;
console.log(`Loaded site configuration from ${siteConfigPath}`);
} catch (e) {
console.error(`Error loading site configuration from ${siteConfigPath}:`, e);
}
const siteConfig = safeLoadYamlConfig(siteConfigPath);
if (siteConfig) {
// 将site.yml中的内容分配到正确的配置字段
config.site = siteConfig;
// 提取特殊字段到顶层配置
if (siteConfig.fonts) config.fonts = siteConfig.fonts;
if (siteConfig.profile) config.profile = siteConfig.profile;
if (siteConfig.social) config.social = siteConfig.social;
}
// 加载导航配置
const navConfigPath = path.join(dirPath, 'navigation.yml');
if (fs.existsSync(navConfigPath)) {
try {
const fileContent = fs.readFileSync(navConfigPath, 'utf8');
config.navigation = yaml.load(fileContent);
console.log(`Loaded navigation configuration from ${navConfigPath}`);
} catch (e) {
console.error(`Error loading navigation configuration from ${navConfigPath}:`, e);
}
const navConfig = safeLoadYamlConfig(navConfigPath);
if (navConfig) {
config.navigation = navConfig;
}
// 加载页面配置
@@ -200,11 +210,10 @@ function loadModularConfig(dirPath) {
file.endsWith('.yml') || file.endsWith('.yaml'));
files.forEach(file => {
try {
const filePath = path.join(pagesPath, file);
const fileContent = fs.readFileSync(filePath, 'utf8');
const fileConfig = yaml.load(fileContent);
const filePath = path.join(pagesPath, file);
const fileConfig = safeLoadYamlConfig(filePath);
if (fileConfig) {
// 提取文件名(不含扩展名)作为配置键
const configKey = path.basename(file, path.extname(file));
@@ -215,10 +224,6 @@ function loadModularConfig(dirPath) {
// 将页面配置添加到主配置对象
config[configKey] = fileConfig;
console.log(`Loaded page configuration from ${filePath}`);
} catch (e) {
console.error(`Error loading page configuration from ${path.join(pagesPath, file)}:`, e);
}
});
}
@@ -254,15 +259,6 @@ function ensureConfigDefaults(config) {
modeToggle: true
};
// 确保主题颜色设置存在
if (!result.site.theme) {
result.site.theme = {
primary: '#4a89dc',
background: '#f5f7fa',
modeToggle: true
};
}
// 用户资料默认值
result.profile = result.profile || {};
result.profile.title = result.profile.title || '欢迎使用';
@@ -300,42 +296,51 @@ function validateConfig(config) {
return false;
}
// 检查必要的顶级属性
if (!config.site) {
console.warn('配置警告: 缺少site配置节点');
}
// 导航配置检查
if (!Array.isArray(config.navigation)) {
console.warn('配置警告: navigation不是数组类型');
}
// 验证分类和站点结构
if (!Array.isArray(config.categories)) {
console.warn('配置警告: categories不是数组类型');
} else {
// 检查分类结构
config.categories.forEach((category, index) => {
if (!category.name) {
console.warn(`配置警告: 第${index+1}个分类没有name属性`);
}
if (!Array.isArray(category.sites)) {
console.warn(`配置警告: 分类 "${category.name || `#${index+1}`}" 的sites不是数组类型`);
} else {
// 检查站点URL
category.sites.forEach((site, siteIndex) => {
if (!site.url) {
console.warn(`配置警告: 分类 "${category.name || `#${index+1}`}" 中第${siteIndex+1}个站点缺少url属性`);
}
});
}
});
}
// 所有其他验证被移除,因为它们只是检查但没有实际操作
// 配置默认值和数据修复已经在ensureConfigDefaults函数中处理
return true;
}
/**
* 获取导航项的子菜单数据
* @param {Object} navItem 导航项对象
* @param {Object} config 配置对象
* @returns {Array|null} 子菜单数据数组或null
*/
function getSubmenuForNavItem(navItem, config) {
if (!navItem || !navItem.id || !config) {
return null;
}
// 首页页面添加子菜单(分类)
if (navItem.id === 'home' && Array.isArray(config.categories)) {
return config.categories;
}
// 书签页面添加子菜单(分类)
else if (navItem.id === 'bookmarks' && config.bookmarks && Array.isArray(config.bookmarks.categories)) {
return config.bookmarks.categories;
}
// 项目页面添加子菜单
else if (navItem.id === 'projects' && config.projects && Array.isArray(config.projects.categories)) {
return config.projects.categories;
}
// 文章页面添加子菜单
else if (navItem.id === 'articles' && config.articles && Array.isArray(config.articles.categories)) {
return config.articles.categories;
}
// 友链页面添加子菜单
else if (navItem.id === 'friends' && config.friends && Array.isArray(config.friends.categories)) {
return config.friends.categories;
}
// 通用处理:任意自定义页面的子菜单生成
else if (config[navItem.id] && config[navItem.id].categories && Array.isArray(config[navItem.id].categories)) {
return config[navItem.id].categories;
}
return null;
}
/**
* 准备渲染数据,添加模板所需的特殊属性
* @param {Object} config 配置对象
@@ -355,7 +360,7 @@ function prepareRenderData(config) {
// 确保navigation是数组
if (!Array.isArray(renderData.navigation)) {
renderData.navigation = [];
console.warn('Warning: navigation is not an array. Using empty array instead.');
// 移除警告日志,数据处理逻辑保留
}
// 添加导航项的活动状态标记和子菜单
@@ -368,30 +373,10 @@ function prepareRenderData(config) {
active: index === 0 // 兼容原有逻辑
};
// 为导航项添加子菜单
// 首页页面添加子菜单(分类)
if (item.id === 'home' && Array.isArray(renderData.categories)) {
navItem.submenu = renderData.categories;
}
// 书签页面添加子菜单(分类)
else if (item.id === 'bookmarks' && renderData.bookmarks && Array.isArray(renderData.bookmarks.categories)) {
navItem.submenu = renderData.bookmarks.categories;
}
// 项目页面添加子菜单
else if (item.id === 'projects' && renderData.projects && Array.isArray(renderData.projects.categories)) {
navItem.submenu = renderData.projects.categories;
}
// 文章页面添加子菜单
else if (item.id === 'articles' && renderData.articles && Array.isArray(renderData.articles.categories)) {
navItem.submenu = renderData.articles.categories;
}
// 友链页面添加子菜单
else if (item.id === 'friends' && renderData.friends && Array.isArray(renderData.friends.categories)) {
navItem.submenu = renderData.friends.categories;
}
// 通用处理:任意自定义页面的子菜单生成
else if (renderData[item.id] && renderData[item.id].categories && Array.isArray(renderData[item.id].categories)) {
navItem.submenu = renderData[item.id].categories;
// 使用辅助函数获取子菜单
const submenu = getSubmenuForNavItem(navItem, renderData);
if (submenu) {
navItem.submenu = submenu;
}
return navItem;
@@ -428,22 +413,18 @@ function loadConfig() {
// 根据优先级顺序选择最高优先级的配置
if (hasUserModularConfig) {
// 1. 最高优先级: config/user/ 目录
console.log('Using modular user configuration from config/user/ (highest priority)');
config = loadModularConfig('config/user');
} else if (hasDefaultModularConfig) {
// 2. 次高优先级: config/_default/ 目录
console.log('Using modular default configuration from config/_default/');
config = loadModularConfig('config/_default');
} else {
// 3. 最低优先级: 旧版单文件配置 (config.yml or config.yaml)
console.log('Using legacy single-file configuration');
const legacyConfigPath = fs.existsSync('config.yml') ? 'config.yml' : 'config.yaml';
if (fs.existsSync(legacyConfigPath)) {
try {
const fileContent = fs.readFileSync(legacyConfigPath, 'utf8');
config = yaml.load(fileContent);
console.log(`Loaded legacy configuration from ${legacyConfigPath}`);
} catch (e) {
console.error(`Error loading configuration from ${legacyConfigPath}:`, e);
}
@@ -457,7 +438,7 @@ function loadConfig() {
config = ensureConfigDefaults(config);
if (!validateConfig(config)) {
console.warn('Configuration validation warnings found. Continuing with defaults.');
// 移除警告日志,保留函数调用
}
// 准备渲染数据
@@ -471,72 +452,15 @@ function generateNavigation(navigation, config) {
return navigation.map(nav => {
// 根据页面ID获取对应的子菜单项分类
let submenuItems = '';
// 首页页面添加子菜单(分类)
if (nav.id === 'home' && Array.isArray(config.categories)) {
// 使用辅助函数获取子菜单数据
const submenu = getSubmenuForNavItem(nav, config);
// 如果存在子菜单生成HTML
if (submenu && Array.isArray(submenu)) {
submenuItems = `
<div class="submenu">
${config.categories.map(category => `
<a href="#${category.name}" class="submenu-item" data-page="${nav.id}" data-category="${category.name}">
<i class="${escapeHtml(category.icon)}"></i>
<span>${escapeHtml(category.name)}</span>
</a>
`).join('')}
</div>`;
}
// 书签页面添加子菜单(分类)
else if (nav.id === 'bookmarks' && config.bookmarks && Array.isArray(config.bookmarks.categories)) {
submenuItems = `
<div class="submenu">
${config.bookmarks.categories.map(category => `
<a href="#${category.name}" class="submenu-item" data-page="${nav.id}" data-category="${category.name}">
<i class="${escapeHtml(category.icon)}"></i>
<span>${escapeHtml(category.name)}</span>
</a>
`).join('')}
</div>`;
}
// 项目页面添加子菜单
else if (nav.id === 'projects' && config.projects && Array.isArray(config.projects.categories)) {
submenuItems = `
<div class="submenu">
${config.projects.categories.map(category => `
<a href="#${category.name}" class="submenu-item" data-page="${nav.id}" data-category="${category.name}">
<i class="${escapeHtml(category.icon)}"></i>
<span>${escapeHtml(category.name)}</span>
</a>
`).join('')}
</div>`;
}
// 文章页面添加子菜单
else if (nav.id === 'articles' && config.articles && Array.isArray(config.articles.categories)) {
submenuItems = `
<div class="submenu">
${config.articles.categories.map(category => `
<a href="#${category.name}" class="submenu-item" data-page="${nav.id}" data-category="${category.name}">
<i class="${escapeHtml(category.icon)}"></i>
<span>${escapeHtml(category.name)}</span>
</a>
`).join('')}
</div>`;
}
// 友链页面添加子菜单
else if (nav.id === 'friends' && config.friends && Array.isArray(config.friends.categories)) {
submenuItems = `
<div class="submenu">
${config.friends.categories.map(category => `
<a href="#${category.name}" class="submenu-item" data-page="${nav.id}" data-category="${category.name}">
<i class="${escapeHtml(category.icon)}"></i>
<span>${escapeHtml(category.name)}</span>
</a>
`).join('')}
</div>`;
}
// 通用处理:任意自定义页面的子菜单生成
else if (config[nav.id] && config[nav.id].categories && Array.isArray(config[nav.id].categories)) {
submenuItems = `
<div class="submenu">
${config[nav.id].categories.map(category => `
${submenu.map(category => `
<a href="#${category.name}" class="submenu-item" data-page="${nav.id}" data-category="${category.name}">
<i class="${escapeHtml(category.icon)}"></i>
<span>${escapeHtml(category.name)}</span>
@@ -623,22 +547,9 @@ function generateSocialLinks(social) {
</a>`).join('\n');
}
// 生成欢迎区域和首页内容
function generateHomeContent(config) {
const profile = config.profile || {};
return `
<div class="welcome-section">
<h2>${escapeHtml(profile.title || '欢迎使用')}</h2>
<h3>${escapeHtml(profile.subtitle || '个人导航站')}</h3>
<p class="subtitle">${escapeHtml(profile.description || '快速访问您的常用网站')}</p>
</div>
${generateCategories(config.categories)}`;
}
// 生成页面内容
// 生成页面内容(包括首页和其他页面)
function generatePageContent(pageId, data) {
// 确保数据对象存在且有必要的字段
// 确保数据对象存在
if (!data) {
console.error(`Missing data for page: ${pageId}`);
return `
@@ -648,13 +559,23 @@ function generatePageContent(pageId, data) {
</div>`;
}
// 设置默认值
const title = data.title || `${pageId} 页面`;
const subtitle = data.subtitle || '';
const categories = data.categories || [];
// 如果是书签页面使用bookmarks配置
if (pageId === 'bookmarks') {
// 首页使用profile数据其他页面使用自身数据
if (pageId === 'home') {
const profile = data.profile || {};
return `
<div class="welcome-section">
<h2>${escapeHtml(profile.title || '欢迎使用')}</h2>
<h3>${escapeHtml(profile.subtitle || '个人导航站')}</h3>
<p class="subtitle">${escapeHtml(profile.description || '快速访问您的常用网站')}</p>
</div>
${generateCategories(data.categories)}`;
} else {
// 其他页面使用通用结构
const title = data.title || `${pageId} 页面`;
const subtitle = data.subtitle || '';
const categories = data.categories || [];
return `
<div class="welcome-section">
<h2>${escapeHtml(title)}</h2>
@@ -662,13 +583,6 @@ function generatePageContent(pageId, data) {
</div>
${generateCategories(categories)}`;
}
return `
<div class="welcome-section">
<h2>${escapeHtml(title)}</h2>
<p class="subtitle">${escapeHtml(subtitle)}</p>
</div>
${generateCategories(categories)}`;
}
// 生成搜索结果页面
@@ -762,30 +676,10 @@ function renderPage(pageId, config) {
active: nav.id === pageId // 兼容原有逻辑
};
// 确保子菜单信息正确
// 首页页面添加子菜单(分类)
if (nav.id === 'home' && Array.isArray(config.categories)) {
navItem.submenu = config.categories;
}
// 书签页面添加子菜单(分类)
else if (nav.id === 'bookmarks' && config.bookmarks && Array.isArray(config.bookmarks.categories)) {
navItem.submenu = config.bookmarks.categories;
}
// 项目页面添加子菜单
else if (nav.id === 'projects' && config.projects && Array.isArray(config.projects.categories)) {
navItem.submenu = config.projects.categories;
}
// 文章页面添加子菜单
else if (nav.id === 'articles' && config.articles && Array.isArray(config.articles.categories)) {
navItem.submenu = config.articles.categories;
}
// 友链页面添加子菜单
else if (nav.id === 'friends' && config.friends && Array.isArray(config.friends.categories)) {
navItem.submenu = config.friends.categories;
}
// 通用处理:任意自定义页面的子菜单生成
else if (config[nav.id] && config[nav.id].categories && Array.isArray(config[nav.id].categories)) {
navItem.submenu = config[nav.id].categories;
// 使用辅助函数获取子菜单
const submenu = getSubmenuForNavItem(navItem, config);
if (submenu) {
navItem.submenu = submenu;
}
return navItem;
@@ -813,10 +707,6 @@ function renderPage(pageId, config) {
* @returns {Object} 包含所有页面HTML的对象
*/
function generateAllPagesHTML(config) {
// 初始化模板系统这已经在main中执行过但为了确保我们在这里再次调用
loadHandlebarsTemplates();
console.log('Handlebars templates available. Using template rendering.');
// 页面内容集合
const pages = {};
@@ -859,17 +749,10 @@ function generateHTML(config) {
const navigationData = config.navigation.map(nav => {
const navItem = { ...nav };
// 根据页面ID获取对应的子菜单项分类
if (nav.id === 'home' && Array.isArray(config.categories)) {
navItem.submenu = config.categories;
}
// 书签页面添加子菜单(分类)
else if (nav.id === 'bookmarks' && config.bookmarks && Array.isArray(config.bookmarks.categories)) {
navItem.submenu = config.bookmarks.categories;
}
// 其他页面添加子菜单
else if (config[nav.id] && config[nav.id].categories && Array.isArray(config[nav.id].categories)) {
navItem.submenu = config[nav.id].categories;
// 使用辅助函数获取子菜单
const submenu = getSubmenuForNavItem(navItem, config);
if (submenu) {
navItem.submenu = submenu;
}
return navItem;
@@ -898,15 +781,8 @@ function generateHTML(config) {
};
try {
// 使用Handlebars模板
const defaultLayoutPath = path.join(process.cwd(), 'templates', 'layouts', 'default.hbs');
if (!fs.existsSync(defaultLayoutPath)) {
throw new Error('Default layout template not found.');
}
// 加载默认布局模板
const layoutContent = fs.readFileSync(defaultLayoutPath, 'utf8');
const layoutTemplate = handlebars.compile(layoutContent);
// 使用辅助函数获取默认布局模板
const { template: layoutTemplate } = getDefaultLayoutTemplate();
// 渲染模板
return layoutTemplate(layoutData);
@@ -926,7 +802,6 @@ function copyStaticFiles(config) {
// 复制CSS文件
try {
fs.copyFileSync('assets/style.css', 'dist/style.css');
console.log('Copied style.css to dist/');
} catch (e) {
console.error('Error copying style.css:', e);
}
@@ -934,7 +809,6 @@ function copyStaticFiles(config) {
// 复制JavaScript文件
try {
fs.copyFileSync('src/script.js', 'dist/script.js');
console.log('Copied script.js to dist/');
} catch (e) {
console.error('Error copying script.js:', e);
}
@@ -944,10 +818,8 @@ function copyStaticFiles(config) {
try {
if (fs.existsSync(`assets/${config.site.favicon}`)) {
fs.copyFileSync(`assets/${config.site.favicon}`, `dist/${path.basename(config.site.favicon)}`);
console.log(`Copied favicon: ${config.site.favicon} to dist/`);
} else if (fs.existsSync(config.site.favicon)) {
fs.copyFileSync(config.site.favicon, `dist/${path.basename(config.site.favicon)}`);
console.log(`Copied favicon: ${config.site.favicon} to dist/`);
} else {
console.warn(`Warning: Favicon file not found: ${config.site.favicon}`);
}
@@ -957,98 +829,10 @@ function copyStaticFiles(config) {
}
}
// 处理模板文件,替换占位符
function processTemplate(template, config) {
const currentYear = new Date().getFullYear();
const googleFontsLink = generateGoogleFontsLink(config);
const fontVariables = generateFontVariables(config);
// 使用Handlebars渲染
const defaultLayoutPath = path.join(process.cwd(), 'templates', 'layouts', 'default.hbs');
if (!fs.existsSync(defaultLayoutPath)) {
throw new Error('Default layout template not found. Cannot proceed.');
}
// 确保config.navigation是数组
if (!Array.isArray(config.navigation)) {
throw new Error('config.navigation is not an array in processTemplate.');
}
// 准备导航数据添加submenu字段
const navigationData = config.navigation.map(nav => {
const navItem = { ...nav };
// 根据页面ID获取对应的子菜单项分类
if (nav.id === 'home' && Array.isArray(config.categories)) {
navItem.submenu = config.categories;
}
// 书签页面添加子菜单(分类)
else if (nav.id === 'bookmarks' && config.bookmarks && Array.isArray(config.bookmarks.categories)) {
navItem.submenu = config.bookmarks.categories;
}
// 项目页面添加子菜单
else if (nav.id === 'projects' && config.projects && Array.isArray(config.projects.categories)) {
navItem.submenu = config.projects.categories;
}
// 文章页面添加子菜单
else if (nav.id === 'articles' && config.articles && Array.isArray(config.articles.categories)) {
navItem.submenu = config.articles.categories;
}
// 友链页面添加子菜单
else if (nav.id === 'friends' && config.friends && Array.isArray(config.friends.categories)) {
navItem.submenu = config.friends.categories;
}
// 通用处理:任意自定义页面的子菜单生成
else if (config[nav.id] && config[nav.id].categories && Array.isArray(config[nav.id].categories)) {
navItem.submenu = config[nav.id].categories;
}
return navItem;
});
// 准备模板数据
const templateData = {
site: config.site || {},
navigation: generateNavigation(config.navigation, config),
navigationData: navigationData, // 带有子菜单的导航数据
social: Array.isArray(config.social) ? config.social : [], // 社交数据
categories: Array.isArray(config.categories) ? config.categories : [],
profile: config.profile || {},
googleFontsLink: googleFontsLink,
fontVariables: fontVariables,
currentYear: currentYear,
socialLinks: generateSocialLinks(config.social), // 使用生成的HTML
searchResults: generateSearchResultsPage(config),
body: config.content || '' // 支持布局模板用
};
// 加载默认布局模板
const layoutContent = fs.readFileSync(defaultLayoutPath, 'utf8');
const layoutTemplate = handlebars.compile(layoutContent);
// 渲染模板
return layoutTemplate(templateData);
}
// 调试函数
function debugConfig(config) {
console.log('==== DEBUG INFO ====');
console.log('Navigation items:', config.navigation.map(nav => nav.id));
console.log('Has bookmarks config:', !!config.bookmarks);
if (config.bookmarks) {
console.log('Bookmarks title:', config.bookmarks.title);
console.log('Bookmarks categories:', config.bookmarks.categories.length);
}
console.log('==================');
}
// 主函数
function main() {
const config = loadConfig();
// 输出调试信息
debugConfig(config);
try {
// 确保dist目录存在
if (!fs.existsSync('dist')) {
@@ -1058,15 +842,11 @@ function main() {
// 初始化Handlebars模板系统
loadHandlebarsTemplates();
console.log('Handlebars templates are initialized.');
// 使用generateHTML函数生成完整的HTML
const htmlContent = generateHTML(config);
console.log('Successfully rendered all pages using Handlebars templates.');
// 生成HTML
fs.writeFileSync('dist/index.html', htmlContent);
console.log('Successfully generated dist/index.html');
// 复制静态文件
copyStaticFiles(config);

382
src/helpers/README.md Normal file
View File

@@ -0,0 +1,382 @@
# MeNav Handlebars 助手函数说明文档
## 目录
- [助手函数概述](#助手函数概述)
- [助手函数分类](#助手函数分类)
- [格式化函数](#格式化函数)
- [条件判断函数](#条件判断函数)
- [工具函数](#工具函数)
- [核心函数](#核心函数)
- [使用方法](#使用方法)
- [函数详解](#函数详解)
- [扩展指南](#扩展指南)
- [最佳实践](#最佳实践)
## 助手函数概述
MeNav 项目使用 Handlebars 助手函数扩展模板功能,使模板更加强大和灵活。助手函数可用于:
- 数据格式化(日期、文本等)
- 条件判断和逻辑控制
- 数组与对象操作
- HTML 安全处理
所有助手函数都在 `src/helpers/` 目录下定义,并通过 `src/helpers/index.js` 统一注册到 Handlebars 实例。
## 助手函数分类
MeNav 的助手函数分为四类:
### 格式化函数
位置:`src/helpers/formatters.js`
提供各种数据格式化功能,包括:
- 日期格式化
- 文本长度限制
- 大小写转换
- 调试数据显示
### 条件判断函数
位置:`src/helpers/conditions.js`
提供条件判断与逻辑操作功能,包括:
- 相等与不等判断
- 通用比较操作
- 空值检查
- 逻辑运算(与、或、非)
### 工具函数
位置:`src/helpers/utils.js`
提供各种实用工具功能,包括:
- 数组与字符串操作
- 集合长度计算
- 范围数组生成
- 对象属性选择
### 核心函数
位置:`src/helpers/index.js`
提供基础的 HTML 处理功能:
- HTML 转义
- 安全输出 HTML
## 使用方法
在 Handlebars 模板中使用助手函数有多种方式:
### 1. 内联表达式
用于生成内容的助手函数:
```handlebars
{{formatDate created "YYYY-MM-DD"}}
{{limit description 100}}
{{json data}}
```
### 2. 块级表达式
用于控制结构的助手函数:
```handlebars
{{#ifEquals type "article"}}
<span class="badge">文章</span>
{{else}}
<span class="badge">页面</span>
{{/ifEquals}}
{{#each (range 1 5)}}
<span>{{this}}</span>
{{/each}}
```
### 3. 助手函数组合
多个助手函数可以组合使用:
```handlebars
{{#each (slice items 0 5)}}
<li>{{toUpperCase name}}</li>
{{/each}}
```
## 函数详解
### 格式化函数
#### formatDate
格式化日期:
```handlebars
{{formatDate date "YYYY-MM-DD"}} {{!-- 2023-05-15 --}}
{{formatDate date "YYYY年MM月DD日"}} {{!-- 2023年05月15日 --}}
{{formatDate date "YYYY-MM-DD HH:mm:ss"}} {{!-- 2023-05-15 14:30:00 --}}
```
支持的格式:
- `YYYY`: 四位年份
- `MM`: 两位月份
- `DD`: 两位日期
- `HH`: 两位小时24小时制
- `mm`: 两位分钟
- `ss`: 两位秒数
#### limit
限制文本长度,超出部分显示省略号:
```handlebars
{{limit "这是一段很长的文本内容" 5}} {{!-- 这是一段... --}}
```
#### toLowerCase / toUpperCase
转换文本大小写:
```handlebars
{{toLowerCase "Hello"}} {{!-- hello --}}
{{toUpperCase "world"}} {{!-- WORLD --}}
```
#### json
将对象转换为 JSON 字符串(用于调试):
```handlebars
{{json this}}
```
### 条件判断函数
#### ifEquals / ifNotEquals
比较两个值是否相等/不相等:
```handlebars
{{#ifEquals status "active"}}
当前状态:活跃
{{else}}
当前状态:非活跃
{{/ifEquals}}
```
#### ifCond
通用条件比较:
```handlebars
{{#ifCond count ">" 0}}
{{count}} 个项目
{{else}}
没有项目
{{/ifCond}}
```
支持的运算符:
- `==`, `===`, `!=`, `!==`
- `<`, `<=`, `>`, `>=`
- `&&`, `||`
#### isEmpty / isNotEmpty
检查值是否为空:
```handlebars
{{#isEmpty items}}
<p>暂无数据</p>
{{else}}
<ul>
{{#each items}}
<li>{{this}}</li>
{{/each}}
</ul>
{{/isEmpty}}
```
#### and / or / not
逻辑操作:
```handlebars
{{#and isPremium isActive}}
高级活跃用户
{{/and}}
{{#or isPremium isAdmin}}
有访问权限
{{/or}}
{{#not isDisabled}}
此功能可用
{{/not}}
```
### 工具函数
#### slice
数组或字符串切片:
```handlebars
{{#each (slice array 0 3)}}
<li>{{this}}</li>
{{/each}}
```
#### concat
合并数组:
```handlebars
{{#each (concat array1 array2)}}
<li>{{this}}</li>
{{/each}}
```
#### size
获取数组、字符串或对象的长度/大小:
```handlebars
总共 {{size items}} 个项目
```
#### first / last
获取数组的第一个/最后一个元素:
```handlebars
第一项: {{first items}}
最后一项: {{last items}}
```
#### range
创建一个连续范围的数组:
```handlebars
{{#each (range 1 5)}}
<span>{{this}}</span>
{{/each}}
```
#### pick
从对象中选择指定的属性:
```handlebars
{{json (pick user "name" "email")}}
```
#### keys
将对象的所有键转换为数组:
```handlebars
{{#each (keys object)}}
<li>{{this}}</li>
{{/each}}
```
### 核心函数
#### escapeHtml
转义 HTML 特殊字符:
```handlebars
{{escapeHtml content}}
```
#### safeHtml
安全输出 HTML不转义
```handlebars
{{safeHtml htmlContent}}
```
## 扩展指南
### 添加新的助手函数
1. 选择适当的分类文件(`formatters.js``conditions.js``utils.js`
2. 添加新的函数并导出
3. 函数会自动通过 `index.js` 中的 `registerAllHelpers` 注册
示例:添加一个新的格式化函数到 `formatters.js`
```javascript
/**
* 将数字格式化为带千位分隔符的字符串
* @param {number} number 要格式化的数字
* @returns {string} 格式化后的字符串
* @example {{formatNumber 1000000}} -> 1,000,000
*/
function formatNumber(number) {
if (typeof number !== 'number') return '';
return number.toLocaleString();
}
// 在导出中添加新函数
module.exports = {
formatDate,
limit,
toLowerCase,
toUpperCase,
json,
formatNumber // 添加新函数
};
```
### 添加新的分类
如果需要添加新的分类:
1.`src/helpers/` 创建新的 JS 文件
2.`index.js` 中导入并注册新的助手函数集
```javascript
const newHelpers = require('./new-helpers');
function registerAllHelpers(handlebars) {
// 现有注册代码...
// 注册新的助手函数
Object.entries(newHelpers).forEach(([name, helper]) => {
handlebars.registerHelper(name, helper);
});
}
```
## 最佳实践
1. **文档化函数** - 使用 JSDoc 风格为所有函数添加文档注释
- 描述函数功能
- 列出参数和返回值
- 提供使用示例
2. **参数校验** - 增加参数类型和有效性检查
- 检查必要参数是否存在
- 验证参数类型
- 为无效输入提供默认值或空结果
3. **命名规范**
- 使用描述性名称,清晰表达函数用途
- 遵循现有命名风格(如 `kebab-case`
- 保持命名一致性(如条件判断函数以 `is``if` 开头)
4. **避免副作用** - 助手函数应为纯函数,不修改传入的数据
5. **保持简单** - 每个助手函数应只完成一个明确的任务

View File

@@ -16,19 +16,16 @@ function registerAllHelpers(handlebars) {
// 注册格式化助手函数
Object.entries(formatters).forEach(([name, helper]) => {
handlebars.registerHelper(name, helper);
console.log(`Registered formatter helper: ${name}`);
});
// 注册条件判断助手函数
Object.entries(conditions).forEach(([name, helper]) => {
handlebars.registerHelper(name, helper);
console.log(`Registered condition helper: ${name}`);
});
// 注册工具类助手函数
Object.entries(utils).forEach(([name, helper]) => {
handlebars.registerHelper(name, helper);
console.log(`Registered utility helper: ${name}`);
});
// 注册HTML转义函数作为助手函数方便在模板中调用
@@ -51,8 +48,6 @@ function registerAllHelpers(handlebars) {
}
return new handlebars.SafeString(text);
});
console.log('All Handlebars helpers registered successfully.');
}
// 导出所有助手函数和注册函数

View File

@@ -21,8 +21,7 @@ const CONFIG_USER_PAGES_DIR = path.join(CONFIG_USER_DIR, 'pages');
* 迁移旧式配置文件到模块化格式
*/
function migrateConfiguration() {
console.log('\n======== MeNav 配置迁移工具 ========');
console.log('将旧式双文件配置转换为模块化配置\n');
console.log('MeNav 配置迁移工具');
// 检查是否存在旧式配置文件
const hasUserConfig = fs.existsSync(LEGACY_USER_CONFIG_FILE);
@@ -50,7 +49,6 @@ function migrateConfiguration() {
// 迁移主配置文件
if (configFile) {
try {
console.log(`迁移配置文件: ${configFile}`);
const configContent = fs.readFileSync(configFile, 'utf8');
const config = yaml.load(configContent);
@@ -85,7 +83,6 @@ function migrateConfiguration() {
`# 由migrate-config.js从${configFile}迁移\n# 生成于 ${new Date().toISOString()}\n\n${siteYaml}`,
'utf8'
);
console.log('✓ 已创建站点配置文件: site.yml');
}
// 提取导航配置
@@ -96,7 +93,6 @@ function migrateConfiguration() {
`# 由migrate-config.js从${configFile}迁移\n# 生成于 ${new Date().toISOString()}\n\n${navigationYaml}`,
'utf8'
);
console.log('✓ 已创建导航配置文件: navigation.yml');
}
// 提取所有页面配置
@@ -116,7 +112,6 @@ function migrateConfiguration() {
`# 由migrate-config.js从${configFile}迁移\n# 生成于 ${new Date().toISOString()}\n\n${pageYaml}`,
'utf8'
);
console.log(`✓ 已创建页面配置文件: ${pageId}.yml`);
}
});
@@ -134,7 +129,6 @@ function migrateConfiguration() {
`# 由migrate-config.js从${configFile}迁移\n# 生成于 ${new Date().toISOString()}\n\n${homeYaml}`,
'utf8'
);
console.log('✓ 已创建首页配置文件: home.yml');
}
} catch (error) {
console.error(`迁移配置文件${configFile}时出错:`, error);
@@ -146,28 +140,17 @@ function migrateConfiguration() {
if (bookmarksFile) {
try {
console.log(`\n迁移书签配置文件: ${bookmarksFile}`);
// 直接复制书签配置文件
fs.copyFileSync(
bookmarksFile,
path.join(CONFIG_USER_PAGES_DIR, 'bookmarks.yml')
);
console.log('✓ 已创建书签配置文件: bookmarks.yml');
} catch (error) {
console.error(`迁移书签配置文件${bookmarksFile}时出错:`, error);
}
}
console.log('\n迁移完成!');
console.log('您现在可以删除旧的配置文件:');
if (hasUserConfig) console.log(`- ${LEGACY_USER_CONFIG_FILE}`);
if (hasDefaultConfig) console.log(`- ${LEGACY_CONFIG_FILE}`);
if (hasUserBookmarks) console.log(`- ${LEGACY_USER_BOOKMARKS_FILE}`);
if (hasDefaultBookmarks) console.log(`- ${LEGACY_BOOKMARKS_FILE}`);
console.log('\n新的模块化配置文件位于');
console.log(`- ${CONFIG_USER_DIR}/`);
console.log(`- ${CONFIG_USER_PAGES_DIR}/`);
console.log('迁移完成!');
}
// 如果直接运行该脚本,则执行迁移

View File

@@ -173,7 +173,6 @@ document.addEventListener('DOMContentLoaded', () => {
});
searchIndex.initialized = true;
console.log('Search index initialized with', searchIndex.items.length, 'items');
} catch (error) {
console.error('Error initializing search index:', error);
searchIndex.initialized = true; // 防止反复尝试初始化
@@ -343,7 +342,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
section.style.display = 'none';
} catch (sectionError) {
console.error('Error clearing search section:', sectionError);
console.error('Error clearing search section');
}
});
@@ -366,10 +365,8 @@ document.addEventListener('DOMContentLoaded', () => {
section.style.display = 'block';
}
} catch (gridError) {
console.error(`Error updating search results for ${pageId}:`, gridError);
console.error('Error updating search results grid');
}
} else {
console.warn(`Search section for page "${pageId}" not found`);
}
});
@@ -393,11 +390,11 @@ document.addEventListener('DOMContentLoaded', () => {
searchBox.classList.toggle('has-results', hasResults);
searchBox.classList.toggle('no-results', !hasResults);
} catch (uiError) {
console.error('Error updating search UI:', uiError);
console.error('Error updating search UI');
}
});
} catch (searchError) {
console.error('Error performing search:', searchError);
console.error('Error performing search');
}
}
@@ -503,7 +500,7 @@ document.addEventListener('DOMContentLoaded', () => {
description.appendChild(descFragment);
}
} catch (error) {
console.error('Error highlighting search term:', error);
console.error('Error highlighting search term');
}
}
@@ -532,7 +529,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
section.style.display = 'none';
} catch (sectionError) {
console.error('Error clearing search section:', sectionError);
console.error('Error clearing search section');
}
});
@@ -557,12 +554,12 @@ document.addEventListener('DOMContentLoaded', () => {
page.classList.toggle('active', page.id === 'home');
});
}
} catch (uiError) {
console.error('Error resetting search UI:', uiError);
} catch (resetError) {
console.error('Error resetting search UI');
}
});
} catch (error) {
console.error('Error in resetSearch:', error);
console.error('Error in resetSearch');
}
}

255
templates/README.md Normal file
View File

@@ -0,0 +1,255 @@
# MeNav 模板系统说明文档
## 目录
- [模板系统概述](#模板系统概述)
- [目录结构](#目录结构)
- [模板类型](#模板类型)
- [布局模板](#布局模板)
- [页面模板](#页面模板)
- [组件模板](#组件模板)
- [模板数据流](#模板数据流)
- [模板使用示例](#模板使用示例)
- [最佳实践](#最佳实践)
- [扩展指南](#扩展指南)
## 模板系统概述
MeNav 项目使用 Handlebars 作为模板引擎,实现了组件化架构,将页面内容与逻辑分离。模板系统的核心优势:
- **组件复用** - 通过组件拆分实现代码复用
- **结构清晰** - 布局、页面、组件分离管理
- **扩展灵活** - 易于添加新页面和组件
- **维护简便** - 修改单个组件不影响其他部分
## 目录结构
```
templates/
├── layouts/ # 布局模板 - 定义页面整体结构
│ └── default.hbs # 默认布局
├── pages/ # 页面模板 - 对应不同页面内容
│ ├── home.hbs # 首页
│ ├── bookmarks.hbs # 书签页
│ └── ...
├── components/ # 组件模板 - 可复用的界面元素
│ ├── navigation.hbs # 导航组件
│ ├── site-card.hbs # 站点卡片组件
│ ├── category.hbs # 分类组件
│ └── ...
└── README.md # 本文档
```
## 模板类型
### 布局模板
布局模板定义了整个页面的HTML结构包含头部、导航栏、内容区和底部等基本框架。
**位置**: `templates/layouts/`
**主要布局**:
- `default.hbs` - 默认布局,定义整个页面框架
**示例**:
```handlebars
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>{{site.title}}</title>
<!-- 其他头部元素 -->
</head>
<body>
<div class="layout">
<!-- 导航部分 -->
<nav class="sidebar">
{{> navigation navigationData}}
</nav>
<!-- 内容区域 -->
<main class="content">
{{#each pages}}
<div class="page {{@key}}{{#if @first}} active{{/if}}" id="{{@key}}">
{{{this}}}
</div>
{{/each}}
</main>
</div>
</body>
</html>
```
### 页面模板
页面模板对应网站的不同页面,每个页面模板通常包含多个组件组合。
**位置**: `templates/pages/`
**主要页面**:
- `home.hbs` - 首页
- `bookmarks.hbs` - 书签页
- `search-results.hbs` - 搜索结果
- 其他自定义页面
**示例** (`home.hbs`):
```handlebars
<div class="welcome-section">
<h2>{{profile.title}}</h2>
<h3>{{profile.subtitle}}</h3>
<p class="subtitle">{{profile.description}}</p>
</div>
{{#each categories}}
{{> category}}
{{/each}}
```
### 组件模板
组件是可复用的UI元素用于在不同页面中重复使用。
**位置**: `templates/components/`
**主要组件**:
- `navigation.hbs` - 导航菜单
- `site-card.hbs` - 站点卡片
- `category.hbs` - 分类容器
- `social-links.hbs` - 社交链接
- `search-results.hbs` - 搜索结果展示
**示例** (`site-card.hbs`):
```handlebars
{{#if url}}
<a href="{{url}}" class="site-card{{#if style}} site-card-{{style}}{{/if}}" title="{{name}} - {{description}}" {{#if external}}target="_blank" rel="noopener"{{/if}}>
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}"></i>
<h3>{{#if name}}{{name}}{{else}}未命名站点{{/if}}</h3>
<p>{{description}}</p>
</a>
{{/if}}
```
## 模板数据流
MeNav 模板系统的数据流如下:
1. `generator.js` 加载配置文件并处理数据
2. 数据通过 Handlebars 上下文传递给模板
3. 布局模板 (`layouts/default.hbs`) 作为外层容器
4. 页面模板 (`pages/*.hbs`) 填充布局中的内容区域
5. 组件模板 (`components/*.hbs`) 在页面中通过 `{{> component-name}}` 引用
主要数据对象:
- `site` - 网站配置信息
- `navigationData` - 导航菜单数据
- `categories` - 分类和站点数据
- `profile` - 个人资料数据
- `social` - 社交链接数据
## 模板使用示例
### 引用组件
在页面或其他组件中引用组件:
```handlebars
{{> navigation navigationData}}
{{> site-card}}
```
### 条件渲染
根据条件显示内容:
```handlebars
{{#if profile.title}}
<h2>{{profile.title}}</h2>
{{else}}
<h2>欢迎使用</h2>
{{/if}}
```
### 循环渲染
循环渲染数据列表:
```handlebars
{{#each categories}}
<section class="category" id="{{name}}">
<h2><i class="{{icon}}"></i> {{name}}</h2>
<div class="sites-grid">
{{#each sites}}
{{> site-card}}
{{/each}}
</div>
</section>
{{/each}}
```
## 最佳实践
1. **组件粒度** - 保持组件的适当粒度,既不过大也不过小
- 过大:难以复用和维护
- 过小:增加复杂性和引用管理难度
2. **数据传递** - 使用合适的方式传递数据
- 直接上下文:`{{> component}}` (继承父上下文)
- 指定数据:`{{> component customData}}` (传递特定数据)
3. **命名规范**
- 使用连字符命名:`site-card.hbs``search-results.hbs`
- 使用描述性名称,体现组件用途
4. **注释**
- 对复杂逻辑添加注释说明
- 标注可选参数和默认行为
## 扩展指南
### 添加新页面
1.`templates/pages/` 创建新的 `.hbs` 文件
2.`config/_default/navigation.yml` 添加页面配置
3. 页面内容可引用现有组件或创建新组件
示例:
```handlebars
<!-- templates/pages/about.hbs -->
<div class="about-page">
<h2>关于我</h2>
<p>{{about.description}}</p>
{{#if about.skills}}
<div class="skills">
<h3>技能</h3>
<ul>
{{#each about.skills}}
<li>{{this}}</li>
{{/each}}
</ul>
</div>
{{/if}}
</div>
```
### 添加新组件
1.`templates/components/` 创建新的 `.hbs` 文件
2. 在页面或其他组件中引用
示例:
```handlebars
<!-- templates/components/skill-card.hbs -->
<div class="skill-card">
<h4>{{name}}</h4>
<div class="skill-level" data-level="{{level}}">
<div class="skill-bar" style="width: {{level}}%"></div>
</div>
</div>
```
使用新组件:
```handlebars
{{#each skills}}
{{> skill-card}}
{{/each}}
```