refactor: 优化重构后的系统并添加文档
This commit is contained in:
@@ -1,7 +0,0 @@
|
||||
- name: 首页
|
||||
icon: fas fa-home
|
||||
id: home
|
||||
active: true
|
||||
- name: 书签
|
||||
icon: fas fa-bookmark
|
||||
id: bookmarks
|
||||
@@ -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"
|
||||
@@ -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: 代码图片生成器
|
||||
@@ -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
57
package-lock.json
generated
@@ -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
103
src/README.md
Normal 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中添加导出和注册
|
||||
@@ -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);
|
||||
|
||||
490
src/generator.js
490
src/generator.js
@@ -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,24 +137,32 @@ 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)) {
|
||||
function safeLoadYamlConfig(filePath) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 yaml.load(fileContent);
|
||||
} catch (error) {
|
||||
handleConfigLoadError(filePath, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载模块化配置目录
|
||||
@@ -163,11 +185,8 @@ 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);
|
||||
|
||||
const siteConfig = safeLoadYamlConfig(siteConfigPath);
|
||||
if (siteConfig) {
|
||||
// 将site.yml中的内容分配到正确的配置字段
|
||||
config.site = siteConfig;
|
||||
|
||||
@@ -175,22 +194,13 @@ function loadModularConfig(dirPath) {
|
||||
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 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 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.');
|
||||
// 移除警告日志,保留函数调用
|
||||
}
|
||||
|
||||
// 准备渲染数据
|
||||
@@ -472,71 +453,14 @@ function generateNavigation(navigation, config) {
|
||||
// 根据页面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>`;
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
// 首页使用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 || [];
|
||||
|
||||
// 如果是书签页面,使用bookmarks配置
|
||||
if (pageId === 'bookmarks') {
|
||||
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
382
src/helpers/README.md
Normal 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. **保持简单** - 每个助手函数应只完成一个明确的任务
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
// 导出所有助手函数和注册函数
|
||||
|
||||
@@ -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('迁移完成!');
|
||||
}
|
||||
|
||||
// 如果直接运行该脚本,则执行迁移
|
||||
|
||||
@@ -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
255
templates/README.md
Normal 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}}
|
||||
```
|
||||
Reference in New Issue
Block a user