diff --git a/README.md b/README.md index f5a0664..1da74bc 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ 📋 静态一键部署 | ⚡ 自动化构建 | 🔖 支持书签导入 -> MeNav是一个轻量级、高度可定制的个人导航网站生成器,让您轻松创建属于自己的导航主页。无需数据库和后端服务,完全静态部署,支持一键Fork部署到GitHub Pages,还可以从浏览器书签一键导入网站。配合 [MarksVault](https://github.com/rbetree/MarksVault) 浏览器扩展,更支持书签自动同步和导航站自动更新。MeNav is a lightweight, highly customizable personal navigation website generator. One-click deployment to GitHub Pages, automated build, bookmark import support, and more. +> MeNav 是一个轻量级、高度可定制的个人导航网站生成器,让您轻松创建属于自己的导航主页。无需数据库和后端服务,完全静态部署,支持一键 Fork 部署到 GitHub Pages,还可以从浏览器书签一键导入网站。配合 [MarksVault](https://github.com/rbetree/MarksVault) 浏览器扩展,更支持书签自动同步和导航站自动更新。 如果觉得项目有用,欢迎⭐Star/Fork支持,谢谢! @@ -36,7 +36,7 @@ ## 目录 -- [预览](#预览--preview) +- [预览](#预览) - [功能特点](#功能特点) - [近期更新](#近期更新) - [技术栈](#技术栈) @@ -50,7 +50,7 @@ - [配置指南](#设置配置文件) - [书签导入](#书签导入功能) - [常见问题](#常见问题) -- [Star-History](#Star-History) +- [Star-History](#star-history) ## 快速预览 @@ -460,14 +460,7 @@ MeNav 支持从浏览器导入书签,快速批量添加网站链接;也支
如何使用MarksVault扩展自动同步书签? -MarksVault浏览器扩展与MeNav的集成相当简单: - -1. 首先,从[GitHub仓库](https://github.com/rbetree/MarksVault)下载并安装MarksVault扩展 -2. 打开扩展,进入同步设置: - - 设置GitHub令牌(需要有对目标仓库的写入权限) - - 配置目标仓库:填写您的用户名和fork的MeNav仓库名 - - 确认bookmarks文件夹路径(默认即可) -3. 使用扩展的任务功能,自动推送书签到项目 +MarksVault 扩展集成的完整说明请见:[`bookmarks/README.md`](bookmarks/README.md) 的 “MarksVault 扩展集成” 章节。
## Star-History diff --git a/bookmarks/README.md b/bookmarks/README.md index 87bf6e6..969d840 100644 --- a/bookmarks/README.md +++ b/bookmarks/README.md @@ -29,12 +29,12 @@ ## 配置加载优先级(完全替换) -书签页配置遵循项目的“完全替换”策略:系统只会使用找到的最高优先级配置,不会把默认配置与用户配置合并。 +书签页配置同样遵循项目的“完全替换”策略:系统只会选择一套配置目录加载,不会把 `user` 与 `_default` 混合合并。 -优先级(高 → 低): +- 若存在 `config/user/`:书签页配置应位于 `config/user/pages/bookmarks.yml`(通常由导入脚本生成) +- 否则:使用 `config/_default/pages/bookmarks.yml`(默认示例) -1. `config/user/pages/bookmarks.yml`(用户配置,通常由导入脚本生成) -2. `config/_default/pages/bookmarks.yml`(默认配置) +> 提示:一旦创建 `config/user/`,`config/_default/` 会被完全忽略,因此不要指望从默认配置“兜底补齐缺失项”。 ## MarksVault 扩展集成 @@ -54,6 +54,7 @@ ```bash npm run import-bookmarks ``` + - 若 `config/user/` 不存在,导入脚本会先从 `config/_default/` 初始化一份用户配置(因为配置采用“完全替换”策略,需要完整配置才能正常生成站点)。 (可选)若希望生成结果保持确定性(便于版本管理,减少时间戳导致的无意义 diff): ```bash MENAV_BOOKMARKS_DETERMINISTIC=1 npm run import-bookmarks diff --git a/config/README.md b/config/README.md index d65b807..b67b444 100644 --- a/config/README.md +++ b/config/README.md @@ -59,7 +59,7 @@ MeNav 配置系统采用“完全替换”策略(不合并),按以下优 2. **字段与结构的权威参考**: - 全局配置:[`_default/site.yml`](_default/site.yml) - 页面配置:[`_default/pages/`](_default/pages/) -3. **多层级嵌套书签示例**:[`_default/pages/bookmarks-four-level.yml`](_default/pages/bookmarks-four-level.yml)(2~4 层结构均有覆盖) +3. **多层级嵌套书签示例**:[`_default/pages/bookmarks-four-level.yml`](_default/pages/bookmarks-four-level.yml)(示例展示到 `groups`;`subgroups` 可参考下方说明或由导入脚本生成) ## 模块化配置文件 @@ -69,7 +69,7 @@ MeNav 配置系统采用“完全替换”策略(不合并),按以下优 - 网站标题、描述和关键词 - 作者信息和版权声明 -- 字体配置和主题设置 +- 字体配置、图标模式等全局设置 - 全局元数据和站点参数 - 个人资料和社交媒体链接 - 导航菜单配置(侧边栏导航项、页面标题和图标、页面顺序和可见性) @@ -151,6 +151,22 @@ MeNav 配置系统采用“完全替换”策略(不合并),按以下优 4. `subgroups`:子分组 5. `sites`:站点(叶子节点) +若你需要第 4 层(`subgroups`),结构示例(片段): + +```yaml +categories: + - name: 示例分类 + subcategories: + - name: 示例子分类 + groups: + - name: 示例分组 + subgroups: + - name: 示例子分组 + sites: + - name: 示例站点 + url: https://example.com +``` + #### 向后兼容性 - 原有二层结构(`categories -> sites`)无需修改即可继续使用 @@ -159,12 +175,16 @@ MeNav 配置系统采用“完全替换”策略(不合并),按以下优 ## 配置优先级 -配置项的优先级从高到低为: +MeNav 配置系统采用“完全替换”策略:只会选择一套目录加载,不会把 `user` 与 `_default` 混合合并。 -1. 用户页面配置 (`user/pages/*.yml`) -2. 用户网站配置 (`user/site.yml`) -3. 默认页面配置 (`_default/pages/*.yml`) -4. 默认网站配置 (`_default/site.yml`) +- 若存在 `config/user/`:只加载 `config/user/`,并**完全忽略** `config/_default/` +- 否则:加载 `config/_default/` + +在“同一套目录”内,各文件的关系是: + +- `site.yml`:站点全局配置(包含 `navigation` 等) +- `pages/*.yml`:各页面配置(文件名需与 `navigation.id` 对应) +- `navigation.yml`:仅在 `site.yml` 未提供 `navigation` 时回退使用(兼容旧版本;推荐迁移到 `site.yml`) ## 配置示例 @@ -182,15 +202,20 @@ profile: subtitle: "我收藏的精选网站" description: "这是一个用于快速访问常用网站的个人导航页面。" -# 主题和样式设置 -theme: - default: "light" - toggleIcon: true - # 字体配置 fonts: - title: "Roboto, sans-serif" - content: "Noto Sans SC, sans-serif" + title: + family: Roboto + weight: 600 + source: google + subtitle: + family: Noto Sans SC + weight: 500 + source: google + body: + family: Noto Sans SC + weight: 400 + source: google # 社交媒体链接 social: diff --git a/src/helpers/README.md b/src/helpers/README.md index 4abc2a3..94237ef 100644 --- a/src/helpers/README.md +++ b/src/helpers/README.md @@ -179,6 +179,14 @@ MeNav 的助手函数分为四类: {{json this}} ``` +#### extractDomain + +从 URL 中提取“干净的域名”(不包含协议、路径与查询串),常用于站点描述兜底显示: + +```handlebars +{{extractDomain url}} +``` + ### 条件判断函数 #### ifEquals / ifNotEquals @@ -311,6 +319,22 @@ MeNav 的助手函数分为四类: {{/each}} ``` +#### encodeURIComponent + +对字符串做 URL 组件编码,常用于拼接第三方请求参数(例如 favicon 的 `url=` 参数): + +```handlebars +{{encodeURIComponent url}} +``` + +#### add + +数字加法,用于根据层级动态计算标题级别等场景: + +```handlebars +... +``` + ### 核心函数 #### escapeHtml diff --git a/src/script.js b/src/script.js index 683a4e0..5f1dffc 100644 --- a/src/script.js +++ b/src/script.js @@ -398,13 +398,13 @@ function updateCategoryToggleIcon(state) { } } -window.MeNav.toggleCategory = function(categoryName, subcategoryName = null, groupName = null) { - const selector = groupName - ? `[data-name="${categoryName}"] [data-name="${subcategoryName}"] [data-name="${groupName}"]` - : subcategoryName - ? `[data-name="${categoryName}"] [data-name="${subcategoryName}"]` - : `[data-name="${categoryName}"]`; - +window.MeNav.toggleCategory = function(categoryName, subcategoryName = null, groupName = null, subgroupName = null) { + let selector = `[data-name="${categoryName}"]`; + + if (subcategoryName) selector += ` [data-name="${subcategoryName}"]`; + if (groupName) selector += ` [data-name="${groupName}"]`; + if (subgroupName) selector += ` [data-name="${subgroupName}"]`; + const element = document.querySelector(selector); if (element) { toggleNestedElement(element); @@ -500,6 +500,11 @@ function extractNestedData(element) { if (groups.length > 0) { data.groups = Array.from(groups).map(group => extractNestedData(group)); } + + const subgroups = element.querySelectorAll(':scope > .group-content > .subgroups-container > .group'); + if (subgroups.length > 0) { + data.subgroups = Array.from(subgroups).map(subgroup => extractNestedData(subgroup)); + } const sites = element.querySelectorAll(':scope > .category-content > .sites-grid > .site-card, :scope > .group-content > .sites-grid > .site-card'); if (sites.length > 0) { diff --git a/templates/README.md b/templates/README.md index a69ff85..983eaa1 100644 --- a/templates/README.md +++ b/templates/README.md @@ -134,13 +134,13 @@ templates/ #### category.hbs - 分类容器组件 -`category.hbs` 是支持多层级嵌套的核心组件,可以递归渲染分类和子分类结构。 +`category.hbs` 是多层级嵌套的核心组件,可渲染 `categories -> subcategories -> groups -> sites` 的结构;更深一层的 `subgroups` 由 `group.hbs` 负责渲染。 **功能特性**: -- 支持无限层级的分类嵌套 -- 自动计算标题层级(h2, h3, h4...) -- 根据层级自动应用对应的CSS类 -- 支持三种内容类型:子分类、分组、站点 +- 支持 2~4 层嵌套(`categories -> subcategories -> groups -> subgroups -> sites`,其中 `subgroups` 可选) +- 自动计算标题层级(h2/h3/h4/h5) +- 根据层级自动应用对应的 CSS 类(如 `category-level-2`、`group-level-4`) +- 分类容器支持三种内容:子分类、分组、站点(分组内可继续包含子分组) **递归渲染原理**: 通过在模板内部调用自身实现递归渲染: @@ -171,6 +171,7 @@ templates/ **功能特性**: - 支持在分类内创建站点分组 +- 支持子分组(`subgroups`,用于第 4 层结构) - 自动应用层级样式 - 支持展开/折叠功能 - 与category.hbs保持一致的层级系统 @@ -188,7 +189,7 @@ templates/ #### 多层级嵌套结构示例 -典型的四层级结构:分类 → 子分类 → 分组 → 站点 +典型的(最多 4 层)结构:分类 → 子分类 → 分组 → 子分组 → 站点(`subgroups` 可选) ```yaml # 配置示例 @@ -201,13 +202,16 @@ categories: groups: - name: "框架" icon: "fas fa-cubes" - sites: - - name: "React" - url: "https://reactjs.org" + subgroups: + - name: "React生态" icon: "fab fa-react" - - name: "Vue" - url: "https://vuejs.org" - icon: "fab fa-vuejs" + sites: + - name: "React" + url: "https://reactjs.org" + icon: "fab fa-react" + - name: "Next.js" + url: "https://nextjs.org" + icon: "fas fa-triangle" ``` 对应的模板渲染: @@ -224,22 +228,32 @@ categories:

前端开发

-
-
- -
-
-

框架

-
-
-
- - {{> site-card site}} -
-
-
-
-
+
+
+ +
+
+

框架

+
+
+
+ +
+
+
React生态
+
+
+
+ + {{> site-card site}} +
+
+
+
+
+
+
+
@@ -251,16 +265,16 @@ categories: - **层级1 (level=1)**: 顶级分类,使用h2标题 - **层级2 (level=2)**: 子分类,使用h3标题 - **层级3 (level=3)**: 分组,使用h4标题 -- **层级4+**: 更深层级,继续递增标题层级 +- **层级4 (level=4)**: 子分组,使用h5标题(用于 4 层结构) 每个层级都有对应的CSS类: -- `category-level-1`, `category-level-2`, ... -- `group-level-1`, `group-level-2`, ... +- `category-level-1`, `category-level-2` +- `group-level-3`, `group-level-4` 这种设计确保了: 1. 语义化的HTML结构 2. 一致的视觉层级 -3. 可扩展的嵌套深度 +3. 可预测的嵌套深度(当前导入脚本与样式保证到 level=4) 4. 灵活的样式定制 ### 站点图标渲染(favicon/manual) @@ -271,22 +285,27 @@ categories: ```handlebars {{#if url}} - + {{#ifEquals @root.icons.mode "favicon"}} {{#ifHttpUrl url}} - - {{name}} favicon - +
+ + {{name}} favicon + +
{{else}} {{/ifHttpUrl}} @@ -294,8 +313,8 @@ categories: {{/ifEquals}}

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

-

{{#if description}}{{description}}{{else}}{{url}}{{/if}}

-
+

{{#if description}}{{description}}{{else}}{{extractDomain url}}{{/if}}

+ {{/if}} ``` diff --git a/templates/components/group.hbs b/templates/components/group.hbs index 6b8dcf0..3c6905e 100644 --- a/templates/components/group.hbs +++ b/templates/components/group.hbs @@ -15,6 +15,14 @@
+ {{#if subgroups}} +
+ {{#each subgroups}} + {{> group level=4}} + {{/each}} +
+ {{/if}} + {{#if sites}}
{{#if sites.length}} @@ -25,8 +33,12 @@

暂无网站

{{/if}}
- {{else}} -

暂无网站

{{/if}} + + {{#unless subgroups}} + {{#unless sites}} +

暂无网站

+ {{/unless}} + {{/unless}}
- \ No newline at end of file + diff --git a/test/bookmark-processor.node-test.js b/test/bookmark-processor.node-test.js index c238fd1..cc07a00 100644 --- a/test/bookmark-processor.node-test.js +++ b/test/bookmark-processor.node-test.js @@ -55,6 +55,62 @@ test('parseBookmarks:解析书签栏、根目录书签与图标映射', () => assert.equal(tools.sites[0].name, 'Google'); }); +test('templates:subgroups(第4层)应可渲染到页面', () => { + const Handlebars = require('handlebars'); + const { registerAllHelpers } = require('../src/helpers'); + + const hbs = Handlebars.create(); + registerAllHelpers(hbs); + + const category = fs.readFileSync(path.join(__dirname, '..', 'templates', 'components', 'category.hbs'), 'utf8'); + const group = fs.readFileSync(path.join(__dirname, '..', 'templates', 'components', 'group.hbs'), 'utf8'); + const siteCard = fs.readFileSync(path.join(__dirname, '..', 'templates', 'components', 'site-card.hbs'), 'utf8'); + const page = fs.readFileSync(path.join(__dirname, '..', 'templates', 'pages', 'bookmarks.hbs'), 'utf8'); + + hbs.registerPartial('category', category); + hbs.registerPartial('group', group); + hbs.registerPartial('site-card', siteCard); + + const tpl = hbs.compile(page); + + const html = tpl({ + title: '我的书签', + subtitle: '测试 subgroups 渲染', + icons: { mode: 'manual' }, + categories: [ + { + name: '技术', + icon: 'fas fa-code', + subcategories: [ + { + name: '前端', + icon: 'fas fa-laptop-code', + groups: [ + { + name: '框架', + icon: 'fas fa-cubes', + subgroups: [ + { + name: 'React生态', + icon: 'fab fa-react', + sites: [ + { name: 'React', url: 'https://reactjs.org/', icon: 'fab fa-react', description: 'React官方' }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }); + + assert.ok(html.includes('subgroups-container'), '应输出 subgroups-container 容器'); + assert.ok(html.includes('group-level-4'), '应输出 level=4 的 group 样式类'); + assert.ok(html.includes('React生态'), '应渲染子分组标题文本'); +}); + test('generateBookmarksYaml:生成 YAML 且可被解析', () => { const bookmarks = { categories: [ @@ -122,4 +178,3 @@ test('ensureUserConfigInitialized/ensureUserSiteYmlExists:可在空目录初 process.chdir(originalCwd); } }); -