diff --git a/config/user/navigation.yml b/config/user/navigation.yml deleted file mode 100644 index aa483df..0000000 --- a/config/user/navigation.yml +++ /dev/null @@ -1,7 +0,0 @@ -- name: 首页 - icon: fas fa-home - id: home - active: true -- name: 书签 - icon: fas fa-bookmark - id: bookmarks \ No newline at end of file diff --git a/config/user/pages/bookmarks.yml b/config/user/pages/bookmarks.yml deleted file mode 100644 index 8e6b00d..0000000 --- a/config/user/pages/bookmarks.yml +++ /dev/null @@ -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" \ No newline at end of file diff --git a/config/user/pages/home.yml b/config/user/pages/home.yml deleted file mode 100644 index 8007cd7..0000000 --- a/config/user/pages/home.yml +++ /dev/null @@ -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: 代码图片生成器 \ No newline at end of file diff --git a/config/user/site.yml b/config/user/site.yml deleted file mode 100644 index c6f24e1..0000000 --- a/config/user/site.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b12dbeb..3f91664 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..30d8411 --- /dev/null +++ b/src/README.md @@ -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中添加导出和注册 \ No newline at end of file diff --git a/src/bookmark-processor.js b/src/bookmark-processor.js index 11a2748..9ca4a49 100644 --- a/src/bookmark-processor.js +++ b/src/bookmark-processor.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); diff --git a/src/generator.js b/src/generator.js index 8dd0c9f..5c90829 100644 --- a/src/generator.js +++ b/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,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 = ` `; - } - // 书签页面添加子菜单(分类) - else if (nav.id === 'bookmarks' && config.bookmarks && Array.isArray(config.bookmarks.categories)) { - submenuItems = ` - `; - } - // 项目页面添加子菜单 - else if (nav.id === 'projects' && config.projects && Array.isArray(config.projects.categories)) { - submenuItems = ` - `; - } - // 文章页面添加子菜单 - else if (nav.id === 'articles' && config.articles && Array.isArray(config.articles.categories)) { - submenuItems = ` - `; - } - // 友链页面添加子菜单 - else if (nav.id === 'friends' && config.friends && Array.isArray(config.friends.categories)) { - submenuItems = ` - `; - } - // 通用处理:任意自定义页面的子菜单生成 - else if (config[nav.id] && config[nav.id].categories && Array.isArray(config[nav.id].categories)) { - submenuItems = ` - `; } - // 设置默认值 - 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 ` +
+

${escapeHtml(profile.title || '欢迎使用')}

+

${escapeHtml(profile.subtitle || '个人导航站')}

+

${escapeHtml(profile.description || '快速访问您的常用网站')}

+
+${generateCategories(data.categories)}`; + } else { + // 其他页面使用通用结构 + const title = data.title || `${pageId} 页面`; + const subtitle = data.subtitle || ''; + const categories = data.categories || []; + return `

${escapeHtml(title)}

@@ -662,13 +583,6 @@ function generatePageContent(pageId, data) {
${generateCategories(categories)}`; } - - return ` -
-

${escapeHtml(title)}

-

${escapeHtml(subtitle)}

-
- ${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); diff --git a/src/helpers/README.md b/src/helpers/README.md new file mode 100644 index 0000000..1605de8 --- /dev/null +++ b/src/helpers/README.md @@ -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"}} + 文章 +{{else}} + 页面 +{{/ifEquals}} + +{{#each (range 1 5)}} + {{this}} +{{/each}} +``` + +### 3. 助手函数组合 + +多个助手函数可以组合使用: + +```handlebars +{{#each (slice items 0 5)}} +
  • {{toUpperCase name}}
  • +{{/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}} +

    暂无数据

    +{{else}} + +{{/isEmpty}} +``` + +#### and / or / not + +逻辑操作: + +```handlebars +{{#and isPremium isActive}} + 高级活跃用户 +{{/and}} + +{{#or isPremium isAdmin}} + 有访问权限 +{{/or}} + +{{#not isDisabled}} + 此功能可用 +{{/not}} +``` + +### 工具函数 + +#### slice + +数组或字符串切片: + +```handlebars +{{#each (slice array 0 3)}} +
  • {{this}}
  • +{{/each}} +``` + +#### concat + +合并数组: + +```handlebars +{{#each (concat array1 array2)}} +
  • {{this}}
  • +{{/each}} +``` + +#### size + +获取数组、字符串或对象的长度/大小: + +```handlebars +总共 {{size items}} 个项目 +``` + +#### first / last + +获取数组的第一个/最后一个元素: + +```handlebars +第一项: {{first items}} +最后一项: {{last items}} +``` + +#### range + +创建一个连续范围的数组: + +```handlebars +{{#each (range 1 5)}} + {{this}} +{{/each}} +``` + +#### pick + +从对象中选择指定的属性: + +```handlebars +{{json (pick user "name" "email")}} +``` + +#### keys + +将对象的所有键转换为数组: + +```handlebars +{{#each (keys object)}} +
  • {{this}}
  • +{{/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. **保持简单** - 每个助手函数应只完成一个明确的任务 \ No newline at end of file diff --git a/src/helpers/index.js b/src/helpers/index.js index 294b66c..f9a7a1f 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -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.'); } // 导出所有助手函数和注册函数 diff --git a/src/migrate-config.js b/src/migrate-config.js index 9aa43be..c6fc89a 100644 --- a/src/migrate-config.js +++ b/src/migrate-config.js @@ -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('迁移完成!'); } // 如果直接运行该脚本,则执行迁移 diff --git a/src/script.js b/src/script.js index 33206dd..32b5fb9 100644 --- a/src/script.js +++ b/src/script.js @@ -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'); } } diff --git a/templates/README.md b/templates/README.md new file mode 100644 index 0000000..70f53de --- /dev/null +++ b/templates/README.md @@ -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 + + + + + {{site.title}} + + + +
    + + + + +
    + {{#each pages}} +
    + {{{this}}} +
    + {{/each}} +
    +
    + + +``` + +### 页面模板 + +页面模板对应网站的不同页面,每个页面模板通常包含多个组件组合。 + +**位置**: `templates/pages/` + +**主要页面**: +- `home.hbs` - 首页 +- `bookmarks.hbs` - 书签页 +- `search-results.hbs` - 搜索结果 +- 其他自定义页面 + +**示例** (`home.hbs`): +```handlebars +
    +

    {{profile.title}}

    +

    {{profile.subtitle}}

    +

    {{profile.description}}

    +
    +{{#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}} + + +

    {{#if name}}{{name}}{{else}}未命名站点{{/if}}

    +

    {{description}}

    +
    +{{/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}} +

    {{profile.title}}

    +{{else}} +

    欢迎使用

    +{{/if}} +``` + +### 循环渲染 + +循环渲染数据列表: + +```handlebars +{{#each categories}} +
    +

    {{name}}

    +
    + {{#each sites}} + {{> site-card}} + {{/each}} +
    +
    +{{/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 + +
    +

    关于我

    +

    {{about.description}}

    + + {{#if about.skills}} +
    +

    技能

    + +
    + {{/if}} +
    +``` + +### 添加新组件 + +1. 在 `templates/components/` 创建新的 `.hbs` 文件 +2. 在页面或其他组件中引用 + +示例: +```handlebars + +
    +

    {{name}}

    +
    +
    +
    +
    +``` + +使用新组件: +```handlebars +{{#each skills}} + {{> skill-card}} +{{/each}} +```