diff --git a/README.md b/README.md index a85703c..59742f7 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ - [快速部署到GitHub Pages](#快速部署到github-pages) - [部署到服务器](#部署到服务器) - [设置配置文件](#设置配置文件) - - [使用单文件配置](#使用单文件配置) + - [使用双文件配置](#使用双文件配置) - [使用模块化配置](#使用模块化配置) - [书签导入功能](#书签导入功能) - [贡献](#贡献) @@ -35,7 +35,7 @@ - 👥 支持展示社交媒体链接 - 📝 支持多个内容页面(首页、项目、文章、友链) - 📌 支持从浏览器导入书签 -- 🧩 模块化配置/单文件配置 +- 🧩 模块化配置/双文件配置 - 🔄 可部署到GitHub Pages或任何服务器 ## 近期更新 @@ -146,7 +146,7 @@ npm install ``` 3. 修改配置 - - 可以选择使用单文件配置或模块化配置(见[设置配置文件](#设置配置文件)) + - 可以选择使用双文件配置或模块化配置(见[设置配置文件](#设置配置文件)) - 自定义网站内容、导航链接、社交媒体链接等 4. 本地预览 @@ -181,7 +181,7 @@ npm run dev #### 第二步:自定义配置 1. 创建个人配置文件: - - 可以使用单文件配置或模块化配置 + - 可以使用双文件配置或模块化配置 - 推荐使用模块化配置(见[使用模块化配置](#使用模块化配置)) - 提交您的配置文件到仓库 @@ -239,22 +239,24 @@ server { ## 设置配置文件 -MeNav支持两种配置方式:单文件配置和模块化配置(推荐)。 +MeNav支持两种配置方式:双文件配置和模块化配置(推荐)。 在加载配置时遵循以下优先级顺序: - -1. `config/user/` (模块化用户配置) -2. `config.user.yml`(单文件用户配置) +1. `config/user/` (模块化用户配置)(优先级最高) +2. `config.user.yml`和 `bookmarks.user.yml`(双文件用户配置) 3. `config/_default/` (模块化默认配置) -4. `config.yml`(单文件默认配置) +4. `config.yml`和`bookmarks.yml`(双文件默认配置)(优先级最低) -### 使用单文件配置 +**注意:** 各优先级配置间采用完全替换策略,而非合并。系统会选择存在的最高优先级配置,完全忽略其他低优先级配置。这确保了模块化配置和双文件配置都能独立使用,避免混合配置带来的意外行为。 -单文件配置是最简单的配置方式,适合小型网站和快速开始。 +### 使用双文件配置 + +双文件配置由 `config.yml`/`config.user.yml` 和 `bookmarks.yml`/`bookmarks.user.yml` 两个文件组成,适合小型网站和快速开始。 1. **创建配置文件**: - 复制 `config.yml` 为 `config.user.yml` - - 编辑 `config.user.yml` 文件 + - 对于书签配置,请勿手动创建 `bookmarks.user.yml`,它应通过[书签导入功能](#书签导入功能)自动生成 + - 编辑 `config.user.yml` 添加您的配置 2. **配置文件结构**: ```yaml @@ -376,6 +378,15 @@ config/ MeNav支持从浏览器导入书签,快速批量添加网站链接。 +在加载配置时遵循以下优先级顺序: + +1. `config/user/pages/bookmarks.yml` (模块化用户配置) +2. `bookmarks.user.yml`(双文件用户配置的书签部分) +3. `config/_default/pages/bookmarks.yml` (模块化默认配置) +4. `bookmarks.yml`(双文件默认配置的书签部分) + +**注意:** 与主配置一样,书签配置也采用完全替换策略,系统只会使用找到的最高优先级书签配置,完全忽略低优先级配置。 + ### 导入步骤 1. **从浏览器导出书签** @@ -392,9 +403,13 @@ MeNav支持从浏览器导入书签,快速批量添加网站链接。 3. **书签处理结果** - 生成的书签配置会保存到 `bookmarks.user.yml` - 如果使用模块化配置,也会同时保存到 `config/user/pages/bookmarks.yml` - - 书签会自动添加到导航菜单中 + - 自动开始构建 -> 有关书签导入功能的更多信息,请参阅源代码中的相关注释。 +### 注意事项 + +- 仅支持标准HTML格式的书签文件 +- 每次只会处理目录中最新的一个书签文件 +- 处理完成后,书签文件会被自动清除,以防止重复处理 ## 贡献 diff --git a/bookmarks/README.md b/bookmarks/README.md deleted file mode 100644 index 0880d7a..0000000 --- a/bookmarks/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# 书签导入目录 - -将从浏览器导出的HTML书签文件放在此目录中,系统会自动处理并导入到您的导航站。 - -## 使用步骤 - -1. 从浏览器导出书签为HTML文件: - - **Chrome**: 书签管理器 → 更多 → 导出书签 - - **Firefox**: 书签库 → 显示所有书签 → 导入和备份 → 导出书签为HTML - - **Edge**: 设置 → 收藏夹 → 管理收藏夹 → 导出为HTML文件 - -2. 将导出的HTML文件放入此目录中 - -3. 提交并推送到GitHub: - ```bash - git add bookmarks/你的书签文件.html - git commit -m "添加书签文件" - git push - ``` - -4. GitHub Actions将自动处理书签文件,生成`bookmarks.user.yml`,并重新构建站点 - -## 注意事项 - -- 仅支持标准HTML格式的书签文件 -- 每次只会处理目录中最新的一个书签文件 -- 处理完成后,书签文件会被自动清除,以防止重复处理 -- 已导入的书签可以在生成的`bookmarks.user.yml`文件中查看和编辑 -- 系统会优先使用`bookmarks.user.yml`的配置,如果存在`bookmarks.yml`,会自动合并两者的内容(用户配置优先) \ No newline at end of file diff --git a/src/generator.js b/src/generator.js index aafd0a5..f038dd7 100644 --- a/src/generator.js +++ b/src/generator.js @@ -16,48 +16,76 @@ function escapeHtml(unsafe) { } /** - * 从文件合并配置到主配置对象 - * @param {Object} config 主配置对象 + * 加载单个配置文件 * @param {string} filePath 配置文件路径 + * @returns {Object|null} 配置对象,如果文件不存在或加载失败则返回null */ -function mergeConfigFromFile(config, filePath) { +function loadSingleConfig(filePath) { if (fs.existsSync(filePath)) { try { const fileContent = fs.readFileSync(filePath, 'utf8'); const fileConfig = yaml.load(fileContent); - - // 提取文件名(不含扩展名)作为配置键 - const configKey = path.basename(filePath, path.extname(filePath)); - - // 如果是site或navigation文件,直接合并到主配置 - if (configKey === 'site' || configKey === 'navigation') { - if (!config[configKey]) config[configKey] = {}; - deepMerge(config[configKey], fileConfig); - } else { - // 其他配置直接合并到根级别 - deepMerge(config, fileConfig); - } - - console.log(`Loaded and merged configuration from ${filePath}`); + console.log(`Loaded configuration from ${filePath}`); + return fileConfig; } catch (e) { console.error(`Error loading configuration from ${filePath}:`, e); + return null; } } + return null; } /** - * 加载页面配置目录中的所有配置文件 - * @param {Object} config 主配置对象 - * @param {string} dirPath 页面配置目录路径 + * 加载模块化配置目录 + * @param {string} dirPath 配置目录路径 + * @returns {Object|null} 配置对象,如果目录不存在或加载失败则返回null */ -function loadPageConfigs(config, dirPath) { - if (fs.existsSync(dirPath)) { - const files = fs.readdirSync(dirPath).filter(file => +function loadModularConfig(dirPath) { + if (!fs.existsSync(dirPath)) { + return null; + } + + const config = { + site: {}, + navigation: [], + fonts: {}, + profile: {}, + social: [], + categories: [] + }; + + // 加载基础配置 + const siteConfigPath = path.join(dirPath, 'site.yml'); + if (fs.existsSync(siteConfigPath)) { + try { + const fileContent = fs.readFileSync(siteConfigPath, 'utf8'); + config.site = yaml.load(fileContent); + 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 pagesPath = path.join(dirPath, 'pages'); + if (fs.existsSync(pagesPath)) { + const files = fs.readdirSync(pagesPath).filter(file => file.endsWith('.yml') || file.endsWith('.yaml')); files.forEach(file => { try { - const filePath = path.join(dirPath, file); + const filePath = path.join(pagesPath, file); const fileContent = fs.readFileSync(filePath, 'utf8'); const fileConfig = yaml.load(fileContent); @@ -69,49 +97,12 @@ function loadPageConfigs(config, dirPath) { console.log(`Loaded page configuration from ${filePath}`); } catch (e) { - console.error(`Error loading page configuration from ${path.join(dirPath, file)}:`, e); + console.error(`Error loading page configuration from ${path.join(pagesPath, file)}:`, e); } }); } -} -/** - * 深度合并两个对象 - * @param {Object} target 目标对象 - * @param {Object} source 源对象 - * @returns {Object} 合并后的对象 - */ -function deepMerge(target, source) { - if (!source) return target; - - for (const key in source) { - if (source.hasOwnProperty(key)) { - if (typeof source[key] === 'object' && source[key] !== null) { - // 确保目标对象有这个属性 - if (!target[key]) { - if (Array.isArray(source[key])) { - target[key] = []; - } else { - target[key] = {}; - } - } - - // 递归合并 - if (Array.isArray(source[key])) { - // 对于数组,直接替换或添加 - target[key] = source[key]; - } else { - // 对于对象,递归合并 - deepMerge(target[key], source[key]); - } - } else { - // 对于基本类型,直接替换 - target[key] = source[key]; - } - } - } - - return target; + return config; } // 读取配置文件 @@ -126,54 +117,44 @@ function loadConfig() { categories: [] }; - // 处理配置目录结构,按照优先级从低到高加载 - // 4. 最低优先级: config.yml (传统默认配置) - if (fs.existsSync('config.yml')) { - const defaultConfigFile = fs.readFileSync('config.yml', 'utf8'); - const defaultConfig = yaml.load(defaultConfigFile); - deepMerge(config, defaultConfig); - console.log('Loaded legacy default config.yml'); - } + // 检查所有可能的配置来源是否存在 + const hasUserModularConfig = fs.existsSync('config/user'); + const hasUserLegacyConfig = fs.existsSync('config.user.yml'); + const hasDefaultModularConfig = fs.existsSync('config/_default'); + const hasDefaultLegacyConfig = fs.existsSync('config.yml'); - // 3. 其次优先级: config/_default/ 目录 - if (fs.existsSync('config/_default')) { - console.log('Loading modular default configuration from config/_default/'); - - // 加载基础配置 - mergeConfigFromFile(config, 'config/_default/site.yml'); - mergeConfigFromFile(config, 'config/_default/navigation.yml'); - - // 加载页面配置 - if (fs.existsSync('config/_default/pages')) { - loadPageConfigs(config, 'config/_default/pages'); - } - } - - // 2. 次高优先级: config.user.yml (传统用户配置) - if (fs.existsSync('config.user.yml')) { + // 根据优先级顺序选择最高优先级的配置 + if (hasUserModularConfig) { + // 1. 最高优先级: config/user/ 目录 + console.log('Using modular user configuration from config/user/ (highest priority)'); + config = loadModularConfig('config/user'); + } else if (hasUserLegacyConfig) { + // 2. 次高优先级: config.user.yml (传统用户配置) + console.log('Using legacy user configuration from config.user.yml'); const userConfigFile = fs.readFileSync('config.user.yml', 'utf8'); - const userConfig = yaml.load(userConfigFile); - - // 深度合并配置 - deepMerge(config, userConfig); - console.log('Merged legacy user configuration from config.user.yml'); + config = yaml.load(userConfigFile); + } else if (hasDefaultModularConfig) { + // 3. 其次优先级: config/_default/ 目录 + console.log('Using modular default configuration from config/_default/'); + config = loadModularConfig('config/_default'); + } else if (hasDefaultLegacyConfig) { + // 4. 最低优先级: config.yml (传统默认配置) + console.log('Using legacy default config.yml'); + const defaultConfigFile = fs.readFileSync('config.yml', 'utf8'); + config = yaml.load(defaultConfigFile); + } else { + console.log('No configuration found, using default empty config'); } - // 1. 最高优先级: config/user/ 目录 - if (fs.existsSync('config/user')) { - console.log('Loading modular user configuration from config/user/ (highest priority)'); - - // 覆盖基础配置 - mergeConfigFromFile(config, 'config/user/site.yml'); - mergeConfigFromFile(config, 'config/user/navigation.yml'); - - // 覆盖页面配置 - if (fs.existsSync('config/user/pages')) { - loadPageConfigs(config, 'config/user/pages'); - } - } + // 确保配置具有必要的结构 + config.site = config.site || {}; + config.navigation = config.navigation || []; + config.fonts = config.fonts || {}; + config.profile = config.profile || {}; + config.social = config.social || []; + config.categories = config.categories || []; - // 处理书签文件(保持现有功能) + // 处理书签文件(保持现有功能,但使用新逻辑) try { let bookmarksConfig = null; let bookmarksSource = null; @@ -310,18 +291,30 @@ function generateNavigation(navigation, config) { // 生成网站卡片HTML function generateSiteCards(sites) { + if (!sites || !Array.isArray(sites) || sites.length === 0) { + return `

暂无网站

`; + } + return sites.map(site => ` - - -

${escapeHtml(site.name)}

-

${escapeHtml(site.description)}

+
+ +

${escapeHtml(site.name || '未命名站点')}

+

${escapeHtml(site.description || '')}

`).join('\n'); } -// 生成分类HTML +// 生成分类板块 function generateCategories(categories) { - return categories.map(category => ` + if (!categories || !Array.isArray(categories) || categories.length === 0) { + return `
+

暂无分类

+

请在配置文件中添加分类

+
`; + } + + return categories.map(category => ` +

${escapeHtml(category.name)}

${generateSiteCards(category.sites)} @@ -331,45 +324,66 @@ function generateCategories(categories) { // 生成社交链接HTML function generateSocialLinks(social) { + if (!social || !Array.isArray(social) || social.length === 0) { + return ''; + } + return social.map(link => `
- +
- ${escapeHtml(link.name)} + ${escapeHtml(link.name || '社交链接')}
`).join('\n'); } // 生成欢迎区域和首页内容 function generateHomeContent(config) { + const profile = config.profile || {}; + return `
-

${escapeHtml(config.profile.title)}

-

${escapeHtml(config.profile.subtitle)}

-

${escapeHtml(config.profile.description)}

+

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

+

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

+

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

${generateCategories(config.categories)}`; } // 生成页面内容 function generatePageContent(pageId, data) { - // 如果是book、marks页面,使用bookmarks配置 - if (pageId === 'bookmarks' && data) { + // 确保数据对象存在且有必要的字段 + if (!data) { + console.error(`Missing data for page: ${pageId}`); return `
-

${escapeHtml(data.title)}

-

${escapeHtml(data.subtitle)}

+

页面未配置

+

请配置 ${pageId} 页面

+
`; + } + + // 设置默认值 + const title = data.title || `${pageId} 页面`; + const subtitle = data.subtitle || ''; + const categories = data.categories || []; + + // 如果是书签页面,使用bookmarks配置 + if (pageId === 'bookmarks') { + return ` +
+

${escapeHtml(title)}

+

${escapeHtml(subtitle)}

- ${generateCategories(data.categories)}`; + ${generateCategories(categories)}`; } return `
-

${escapeHtml(data.title)}

-

${escapeHtml(data.subtitle)}

+

${escapeHtml(title)}

+

${escapeHtml(subtitle)}

- ${generateCategories(data.categories)}`; + ${generateCategories(categories)}`; } // 生成搜索结果页面