From d3bdd282574e17bb425ad83e49621cf4aed16c6e Mon Sep 17 00:00:00 2001 From: Ji ZHOU Date: Mon, 13 May 2024 00:02:28 +0800 Subject: [PATCH] [feat] add notebook support --- _config.yml | 47 ++++++ _data/icons.yml | 3 +- _data/widgets.yml | 6 + languages/en.yml | 3 + languages/zh-CN.yml | 3 + languages/zh-TW.yml | 3 + .../_partial/main/article/article_footer.ejs | 6 +- .../_partial/main/navbar/article_banner.ejs | 2 + .../_partial/main/navbar/breadcrumb/note.ejs | 8 + layout/_partial/main/navbar/dateinfo.ejs | 11 ++ layout/_partial/main/notebook/note_card.ejs | 36 +++++ layout/_partial/main/notebook/note_tags.ejs | 9 ++ .../_partial/main/notebook/notebook_card.ejs | 10 ++ layout/_partial/main/notebook/paginator.ejs | 9 ++ layout/_partial/scripts.ejs | 1 + layout/_partial/scripts/tagtree.ejs | 29 ++++ layout/_partial/sidebar/index_leftbar.ejs | 10 ++ layout/_partial/sidebar/index_rightbar.ejs | 10 ++ layout/_partial/sidebar/search.ejs | 4 + layout/_partial/widgets/recent.ejs | 10 ++ layout/_partial/widgets/tagtree.ejs | 59 +++++++ layout/notebooks.ejs | 8 + layout/notes.ejs | 12 ++ layout/page.ejs | 12 +- scripts/events/index.js | 1 + scripts/events/lib/notebooks.js | 153 ++++++++++++++++++ scripts/generators/notebooks.js | 71 ++++++++ source/css/_components/pages/notebook.styl | 65 ++++++++ 28 files changed, 598 insertions(+), 3 deletions(-) create mode 100644 layout/_partial/main/navbar/breadcrumb/note.ejs create mode 100755 layout/_partial/main/notebook/note_card.ejs create mode 100644 layout/_partial/main/notebook/note_tags.ejs create mode 100644 layout/_partial/main/notebook/notebook_card.ejs create mode 100644 layout/_partial/main/notebook/paginator.ejs create mode 100644 layout/_partial/scripts/tagtree.ejs create mode 100644 layout/_partial/widgets/tagtree.ejs create mode 100644 layout/notebooks.ejs create mode 100644 layout/notes.ejs create mode 100644 scripts/events/lib/notebooks.js create mode 100644 scripts/generators/notebooks.js create mode 100644 source/css/_components/pages/notebook.styl diff --git a/_config.yml b/_config.yml index 2b9329a2..349c4cd0 100755 --- a/_config.yml +++ b/_config.yml @@ -98,6 +98,29 @@ site_tree: menu_id: wiki # 未在 front-matter 中指定 menu_id 时,layout 为 wiki 的页面默认使用这里配置的 menu_id leftbar: tree, related, recent # for wiki rightbar: ghrepo, toc + # 笔记本列表页配置 + notebooks: + base_dir: notebooks # 笔记本列表页的路径。以及未指定 base_dir 的笔记本的路径前缀。 + menu_id: notebooks # 笔记本列表页高亮的主导航栏菜单按钮。 + # 笔记本列表页的左侧栏和右侧栏。 + leftbar: recent # recent within all notebooks + rightbar: null + # 笔记列表页配置 + notes: + # 笔记列表页和笔记页高亮的主导航栏菜单按钮。 + # 可以在笔记本 yaml 的 menu_id 字段中覆盖此参数。 + # 可以在笔记的 front-matter/menu_id 中覆盖此参数。 + menu_id: notebooks + # 笔记列表页的左侧栏和右侧栏。可以在笔记本 yaml 的 leftbar 和 rightbar 字段中覆盖此参数。 + leftbar: tagtree, recent # recent of current notebook + rightbar: null + # 笔记页配置 + note: + # 笔记页的左侧栏和右侧栏 + # 可以在笔记本 yaml 的 note_leftbar 和 note_rightbar 字段中覆盖此参数。 + # 可以在笔记的 front-matter/leftbar 和 rightbar 字段中覆盖此参数。 + leftbar: tagtree, recent # recent of current notebook + rightbar: toc # 作者信息配置 author: base_dir: author # 只影响自动生成的页面路径 @@ -116,6 +139,30 @@ site_tree: rightbar: toc +######## Notebook ######## +notebook: + # 如果没有指定 excerpt 和 description,将自动取多长的内容作为文章摘要。 + auto_excerpt: 128 + # 可以为某个 tag 设定图标(显示在标签树中)。 + tagcons: + '': solar:hashtag-linear + # 每页显示多少篇笔记。0 表示不分页,null 则 fallback 到 hexo 的配置。 + # 可以在笔记本 yaml 的 per_page 字段中覆盖此参数。 + per_page: null + # 笔记的排序方式。默认按照 updated 降序排序。 + # 可以在笔记本 yaml 的 order_by 字段中覆盖此参数。 + # 注意:置顶的笔记会始终排在最前面。 + # 在 front-matter 中设置 pin:true|number 或 sticky:true|number 来置顶。 + order_by: -updated + # 是否在笔记页面显示许可协议。false 表示不显示。true 表示沿用主题许可协议内容。也可以给定具体的文本指定协议内容。 + # 可以在笔记本 yaml 的 license 字段中覆盖此参数。 + # 可以在笔记的 front-matter/license 中覆盖此参数。 + license: false + # 是否在笔记页面显示分享按钮。 + # 可以在笔记本 yaml 的 share 字段中覆盖此参数。 + # 可以在笔记的 front-matter/share 中覆盖此参数。 + share: false + ######## Article ######## article: diff --git a/_data/icons.yml b/_data/icons.yml index 82a27b72..43cd458c 100644 --- a/_data/icons.yml +++ b/_data/icons.yml @@ -7,7 +7,8 @@ solar:documents-bold-duotone: solar:planet-bold-duotone: solar:notebook-bookmark-bold-duotone: - +solar:pin-linear: +solar:hashtag-linear: default:goback: diff --git a/_data/widgets.yml b/_data/widgets.yml index f6f11035..9d8a4c61 100644 --- a/_data/widgets.yml +++ b/_data/widgets.yml @@ -70,3 +70,9 @@ timeline: user: # 默认显示所有人的数据,设置名称可过滤为仅显示某人的数据,多个名称用英文逗号隔开,不要加空格 type: # 默认不用写,如果是友链朋友圈数据请写 fcircle limit: # 默认通过 api 上增加 per_page 来设置,如果是友链朋友圈,可通过这个设置数量 + +tagtree: + layout: tagtree + expand_all: false # 是否展开所有节点 + expand_active: true # 是否展开当前节点 + show_tagcon: true # 是否显示标签 icon diff --git a/languages/en.yml b/languages/en.yml index 689b4444..892bb904 100755 --- a/languages/en.yml +++ b/languages/en.yml @@ -3,6 +3,7 @@ btn: blog: Blog wiki: Wiki topic: Topic + notebook: Notebook recent_publish: Recent all_wiki: All Products category: Category @@ -18,6 +19,8 @@ btn: meta: recent_update: Recent Update + tag_tree: Tags + all_notes: All Notes toc: On This Page read_next: READ NEXT prev: Prev diff --git a/languages/zh-CN.yml b/languages/zh-CN.yml index 8c45c8ca..abfdd049 100755 --- a/languages/zh-CN.yml +++ b/languages/zh-CN.yml @@ -3,6 +3,7 @@ btn: blog: 文章 wiki: 文档 topic: 专栏 + notebook: 笔记本 recent_publish: 近期发布 all_wiki: 所有项目 category: 分类 @@ -18,6 +19,8 @@ btn: meta: recent_update: 最近更新 + tag_tree: 标签 + all_notes: 所有笔记 toc: 本文目录 read_next: 接下来阅读 prev: 回顾上一篇 diff --git a/languages/zh-TW.yml b/languages/zh-TW.yml index 56420c36..10ebc0d5 100755 --- a/languages/zh-TW.yml +++ b/languages/zh-TW.yml @@ -3,6 +3,7 @@ btn: blog: 網誌 wiki: 文檔 topic: 專欄 + notebook: 筆記本 recent_publish: 近期發布 all_wiki: 所有文檔 category: 分類 @@ -18,6 +19,8 @@ btn: meta: recent_update: 最近更新 + tag_tree: 標籤 + all_notes: 所有筆記 toc: 本文目錄 read_next: 接下來閱讀 prev: 回顧上一篇 diff --git a/layout/_partial/main/article/article_footer.ejs b/layout/_partial/main/article/article_footer.ejs index 8a0e29ac..e6af1d45 100644 --- a/layout/_partial/main/article/article_footer.ejs +++ b/layout/_partial/main/article/article_footer.ejs @@ -53,8 +53,10 @@ function layoutDiv() { if (theme.article.license && (page.license != false)) { license = page.license || theme.article.license } + } else if (page.license) { + license = page.license === true ? theme.article.license : page.license } - if (license.length > 0) { + if (license?.length > 0) { var author = null if (theme.authors) { if (page.author?.length > 0 && theme.authors[page.author] != null) { @@ -84,6 +86,8 @@ function layoutDiv() { } } else if (page.layout == 'post') { showSharePlugin = page.share != false + } else if (page.share) { + showSharePlugin = true } if (theme.article.share && showSharePlugin) { function socialButtons() { diff --git a/layout/_partial/main/navbar/article_banner.ejs b/layout/_partial/main/navbar/article_banner.ejs index 4372a3e5..14feadb8 100644 --- a/layout/_partial/main/navbar/article_banner.ejs +++ b/layout/_partial/main/navbar/article_banner.ejs @@ -34,6 +34,8 @@ function layoutBreadcrumb() { el += `${__("btn.home")}` if (theme.wiki.tree[page.wiki]) { el += partial('breadcrumb/wiki') + } else if (page.notebook) { + el += partial('breadcrumb/note') } else if (page.layout == 'post') { el += partial('breadcrumb/blog') } else { diff --git a/layout/_partial/main/navbar/breadcrumb/note.ejs b/layout/_partial/main/navbar/breadcrumb/note.ejs new file mode 100644 index 00000000..3e81c77f --- /dev/null +++ b/layout/_partial/main/navbar/breadcrumb/note.ejs @@ -0,0 +1,8 @@ +<%# 笔记页面的面包屑 %> + +<%= __('btn.notebook') %> +<% const notebook = theme.notebooks.tree[page.notebook] %> +<% if (notebook) { %> + + <%= notebook.name || notebook.title %> +<% } %> diff --git a/layout/_partial/main/navbar/dateinfo.ejs b/layout/_partial/main/navbar/dateinfo.ejs index 47c143ca..0695865a 100644 --- a/layout/_partial/main/navbar/dateinfo.ejs +++ b/layout/_partial/main/navbar/dateinfo.ejs @@ -7,6 +7,17 @@ function layoutDiv() { el += `${__("meta.updated") + __("symbol.colon")}` el += `` el += `` + } else if (page.notebook) { + // 更新日期 + el += `${__("meta.updated") + __("symbol.colon")}` + el += `` + el += `` + // 发布日期 + el += `` + el += `` + el += `${__("meta.created") + __("symbol.colon")}` + el += `` + el += `` } else { const author = theme.authors ? (theme.authors[page.author] || theme.default_author) : null if (author) { diff --git a/layout/_partial/main/notebook/note_card.ejs b/layout/_partial/main/notebook/note_card.ejs new file mode 100755 index 00000000..1a3722a3 --- /dev/null +++ b/layout/_partial/main/notebook/note_card.ejs @@ -0,0 +1,36 @@ +<%# 笔记卡片 %> +
+ <% if (note.cover) { %> +
+ cover +
+ <% } %> +

<%= note.title %>

+
+

+ <% if (note.excerpt) { %> + <%= strip_html(note.excerpt) %> + <% } else if (note.description) { %> + <%= note.description %> + <% } else if (note.content && theme.notebook.auto_excerpt > 0) { %> + <%= truncate(strip_html(note.content), { length: theme.notebook.auto_excerpt }) %> + <% } %> +

+
+
+ <% if (note.pin) { %> + <%- icon('solar:pin-linear') %> + <% } %> + + <%- icon('default:calendar') %> + + + <% if (note.tags) { %> + <% note.tags.forEach((tag, i) => { %> + > + <%= tag %> + + <% }) %> + <% } %> +
+
diff --git a/layout/_partial/main/notebook/note_tags.ejs b/layout/_partial/main/notebook/note_tags.ejs new file mode 100644 index 00000000..1ecd84b7 --- /dev/null +++ b/layout/_partial/main/notebook/note_tags.ejs @@ -0,0 +1,9 @@ +<%# 笔记的所属标签 %> +<% if (page.tags) { %> +
+ <% for (const t of page.tags) { %> + <% const tag = notebook.tagTree.get(t.toLowerCase()) %> + <%= tag.name %> + <% } %> +
+<% } %> diff --git a/layout/_partial/main/notebook/notebook_card.ejs b/layout/_partial/main/notebook/notebook_card.ejs new file mode 100644 index 00000000..aecab973 --- /dev/null +++ b/layout/_partial/main/notebook/notebook_card.ejs @@ -0,0 +1,10 @@ +<%# 笔记本信息卡片 %> +
+
icon
+
+

<%= notebook.title || notebook.name %>

+ <% if (notebook.description) { %> +

<%= notebook.description %>

+ <% } %> +
+
diff --git a/layout/_partial/main/notebook/paginator.ejs b/layout/_partial/main/notebook/paginator.ejs new file mode 100644 index 00000000..e076815b --- /dev/null +++ b/layout/_partial/main/notebook/paginator.ejs @@ -0,0 +1,9 @@ +<% if ((page.total || 0) > 1) { %> +
+ <%- paginator({ + prev_text: '', + next_text: '', + force_prev_next: true, + }) %> +
+<% } %> diff --git a/layout/_partial/scripts.ejs b/layout/_partial/scripts.ejs index 90bfb2e9..7c1047aa 100644 --- a/layout/_partial/scripts.ejs +++ b/layout/_partial/scripts.ejs @@ -18,6 +18,7 @@ function custom_inject() { <%- partial('scripts/defines') %> <%- partial('scripts/utils') %> <%- partial('scripts/sidebar') %> +<%- partial('scripts/tagtree') %> diff --git a/layout/_partial/scripts/tagtree.ejs b/layout/_partial/scripts/tagtree.ejs new file mode 100644 index 00000000..519f8bb5 --- /dev/null +++ b/layout/_partial/scripts/tagtree.ejs @@ -0,0 +1,29 @@ + diff --git a/layout/_partial/sidebar/index_leftbar.ejs b/layout/_partial/sidebar/index_leftbar.ejs index d40142d5..ffdf782d 100755 --- a/layout/_partial/sidebar/index_leftbar.ejs +++ b/layout/_partial/sidebar/index_leftbar.ejs @@ -2,12 +2,22 @@ const wiki = theme.wiki.tree[page.wiki] const topic = theme.topic.tree[page.topic] +const notebook = theme.notebooks.tree[page.notebook] if (page.leftbar == null) { const { site_tree } = theme var sidebar if (is_home()) { sidebar = site_tree.home.leftbar + } else if (page.layout === 'notebooks') { + // 笔记本列表页 + sidebar = site_tree.notebooks.leftbar + } else if (page.layout === 'notes') { + // 笔记列表页 + sidebar = notebook ? notebook.leftbar : site_tree.notes.leftbar + } else if (notebook) { + // 笔记本文章内页 + sidebar = page.leftbar ?? notebook.note_leftbar } else if (is_category() || is_tag() || is_archive() || ['categories', 'tags', 'archives'].includes(page.layout)) { sidebar = site_tree.index_blog.leftbar } else if (page.layout === 'index_topic') { diff --git a/layout/_partial/sidebar/index_rightbar.ejs b/layout/_partial/sidebar/index_rightbar.ejs index 96152e5f..9ffd270f 100644 --- a/layout/_partial/sidebar/index_rightbar.ejs +++ b/layout/_partial/sidebar/index_rightbar.ejs @@ -2,12 +2,22 @@ const wiki = theme.wiki.tree[page.wiki] const topic = theme.topic.tree[page.topic] +const notebook = theme.notebooks.tree[page.notebook] if (page.rightbar == null) { const { site_tree } = theme var sidebar if (is_home()) { sidebar = site_tree.home.rightbar + } else if (page.layout === 'notebooks') { + // 笔记本列表页 + sidebar = site_tree.notebooks.rightbar + } else if (page.layout === 'notes') { + // 笔记列表页 + sidebar = notebook ? notebook.rightbar : site_tree.notes.rightbar + } else if (notebook) { + // 笔记本文章内页 + sidebar = page.rightbar ?? notebook.note_rightbar } else if (is_category() || is_tag() || is_archive() || ['categories', 'tags', 'archives'].includes(page.layout)) { sidebar = site_tree.index_blog.rightbar } else if (page.layout === 'index_topic') { diff --git a/layout/_partial/sidebar/search.ejs b/layout/_partial/sidebar/search.ejs index e27f9c64..a36e2fb6 100644 --- a/layout/_partial/sidebar/search.ejs +++ b/layout/_partial/sidebar/search.ejs @@ -1,10 +1,14 @@ <% +const notebook = theme.notebooks.tree[page.notebook] if (item.filter == null) { item.filter = 'auto' } if (item.placeholder == null && item.filter == 'auto') { if (theme.wiki.tree[page.wiki]?.name) { item.placeholder = __('search.search_in', theme.wiki.tree[page.wiki]?.name) + } else if (notebook) { + item.placeholder = __('search.search_in', notebook.name || __('btn.notebook')) + item.filter = notebook.base_dir } } function layoutDiv() { diff --git a/layout/_partial/widgets/recent.ejs b/layout/_partial/widgets/recent.ejs index ab7b0cf8..c54094af 100644 --- a/layout/_partial/widgets/recent.ejs +++ b/layout/_partial/widgets/recent.ejs @@ -21,6 +21,10 @@ function layoutDiv() { return false }) arr = arr.sort((p1, p2) => p1.updated > p2.updated ? -1 : 1) + } else if (page.layout === 'notebooks') { + arr = site.pages.filter(p => p.notebook).sort('-updated') + } else if (page.notebook) { + arr = site.pages.filter(p => p.notebook === page.notebook).sort('-updated') } else { arr = site.posts.filter( p => p.title && p.title.length > 0) arr = arr.sort("updated", -1) @@ -38,6 +42,12 @@ function layoutDiv() { if (name) { el += '' + name + '' + ''; } + } else if (page.layout === 'notebooks') { + const notebook = theme.notebooks.tree[post.notebook] + const name = notebook?.name || post.notebook + if (name) { + el += '' + name + '' + ''; + } } el += post.title + ''; el += ''; diff --git a/layout/_partial/widgets/tagtree.ejs b/layout/_partial/widgets/tagtree.ejs new file mode 100644 index 00000000..da132b39 --- /dev/null +++ b/layout/_partial/widgets/tagtree.ejs @@ -0,0 +1,59 @@ +<%# 笔记本的标签树 %> +<% const notebook = theme.notebooks.tree[page.notebook] %> +<% const tagTree = notebook?.tagTree %> +<% const isLeaf = tag => tag.id === '' || tag.children.length === 0 %> +<% const getTagcon = tag => { + const tagcons = theme.notebook.tagcons || {} + return tagcons[tag.name] || tagcons[tag.id] || tagcons[tag.part] || tagcons[tag.part.toLowerCase()] || tagcons[''] +} %> + +<% function layoutTag(tag, level) { %> + <% const active = page.activeTag === tag.id %> + + 0) { %> style="padding-left: <%= level * 0.875 %>rem;"<% } %>> + <% if (tag.id === '') { %> + <%= __('meta.all_notes') %> + <% } else { %> + <% const tagcon = item.show_tagcon && getTagcon(tag) %> + <% if (tagcon) { %><%- icon(tagcon) %><% } %> + <%= tag.part %> + <% } %> + + + + + +<% } %> + +<% function layoutChildTags(tag, level) { %> + <% for (const child of tag.children) { %> + <%= tagAndSub(tagTree.get(child), level + 1) %> + <% } %> +<% } %> + +<% function tagAndSub(tag, level) { %> + <% if (!tag) return '' %> + <% const active = page.activeTag === tag.id %> + <% const expanded = item.expand_all || (item.expand_active && active) || page.activeTag?.startsWith(`${tag.id}/`) %> + <% const classes = [isLeaf(tag) ? ' leaf-tag' : ' parent-tag', expanded ? ' expanded' : ''] %> +
+ <%= layoutTag(tag, level) %> + <% if (!isLeaf(tag)) { %> + <%= layoutChildTags(tag, level) %> + <% } %> +
+<% } %> + +<% if (tagTree) { %> + +
+ <%= __('meta.tag_tree') %> +
+
+ <%= tagAndSub(tagTree.get(''), 0) %> + <% for (const child of tagTree.get('').children) { %> + <%= tagAndSub(tagTree.get(child), 0) %> + <% } %> +
+
+<% } %> diff --git a/layout/notebooks.ejs b/layout/notebooks.ejs new file mode 100644 index 00000000..e9e46c2b --- /dev/null +++ b/layout/notebooks.ejs @@ -0,0 +1,8 @@ +<%# 笔记本列表页主体部分 %> +<% for (const notebook of Object.values(theme.notebooks.tree)) { %> +
+ + <%- include('_partial/main/notebook/notebook_card', { notebook: notebook }) %> + +
+<% } %> diff --git a/layout/notes.ejs b/layout/notes.ejs new file mode 100644 index 00000000..ee221c79 --- /dev/null +++ b/layout/notes.ejs @@ -0,0 +1,12 @@ +<%# 笔记列表页主体部分 %> +
+ <% page.posts.each(post => { %> + + <%- include('_partial/main/notebook/note_card', { note: post }) %> + + <% }) %> +
+<%- include('_partial/main/notebook/paginator') %> diff --git a/layout/page.ejs b/layout/page.ejs index 02e64d96..cc2d9720 100755 --- a/layout/page.ejs +++ b/layout/page.ejs @@ -3,6 +3,13 @@ const { layout } = page // 是否使用 Heti 布局插件 const isUsingHeti = theme.plugins.heti?.enable +const notebook = theme.notebooks.tree[page.notebook] +if (notebook) { + page.menu_id ??= notebook.menu_id + page.license ??= notebook.license + page.share ??= notebook.share +} + // 默认的 menu_id if (page.menu_id == null) { if (page.wiki?.length > 0) { @@ -40,7 +47,10 @@ function layoutDiv() { if (page.content && page.content.length > 0) { el += page.content } - if (layout === 'post' || page.wiki) { + if (notebook) { + el += partial('_partial/main/notebook/note_tags', { notebook: notebook }) + } + if (layout === 'post' || page.wiki || notebook) { el += partial('_partial/main/article/article_footer') } el += `` diff --git a/scripts/events/index.js b/scripts/events/index.js index 924e80c2..478bdf6b 100644 --- a/scripts/events/index.js +++ b/scripts/events/index.js @@ -10,6 +10,7 @@ hexo.on('generateBefore', () => { require('./lib/doc_tree')(hexo); require('./lib/topic_tree')(hexo); require('./lib/utils')(hexo); + require('./lib/notebooks')(hexo); }); hexo.on('generateAfter', () => { diff --git a/scripts/events/lib/notebooks.js b/scripts/events/lib/notebooks.js new file mode 100644 index 00000000..bda07d51 --- /dev/null +++ b/scripts/events/lib/notebooks.js @@ -0,0 +1,153 @@ +/** + * notebooks.js v1 + */ + +'use strict' + +class NotePage { + constructor(page) { + this.id = page._id + this.notebook = page.notebook + this.title = page.title + this.tags = page.tags + this.path = page.path + this.path_key = page.path.replace('.html', '') + this.layout = page.layout + this.date = page.date + this.updated = page.updated || page.date + + const pin = page.pin ?? page.sticky ?? 0 + if (pin === true) { + this.pin = 1 + } else if (pin === false) { + this.pin = 0 + } else { + this.pin = pin + } + } +} + +function splitTag(tag) { + return tag.split('/').filter(t => t.length > 0) +} + +function prepareNotebook(id, info, ctx) { + const notebook = info + notebook.id = id + + if (notebook.base_dir) { + if (notebook.base_dir.startsWith('/')) { + notebook.base_dir = notebook.base_dir.substring(1) + } + if (notebook.base_dir.endsWith('/')) { + notebook.base_dir = notebook.base_dir.substring(0, notebook.base_dir.length - 1) + } + } else { + const notebooksBaseDir = ctx.theme.config.site_tree.notebooks.base_dir + notebook.base_dir = notebooksBaseDir ? `${notebooksBaseDir}/${id}` : id + } + + notebook.sort ||= 0 + notebook.auto_excerpt ||= ctx.theme.config.notebook.auto_excerpt || 0 + notebook.per_page ??= ctx.theme.config.notebook.per_page ?? ctx.config.per_page ?? 10 + notebook.order_by ||= ctx.theme.config.notebook.order_by || '-updated' + notebook.menu_id ??= ctx.theme.config.site_tree.notes.menu_id + notebook.license ??= ctx.theme.config.notebook.license + notebook.share ??= ctx.theme.config.notebook.share + + notebook.leftbar ??= ctx.theme.config.site_tree.notes.leftbar + notebook.rightbar ??= ctx.theme.config.site_tree.notes.rightbar + notebook.note_leftbar ??= ctx.theme.config.site_tree.note.leftbar + notebook.note_rightbar ??= ctx.theme.config.site_tree.note.rightbar + + const tagMap = new Map() // tagId: tagInfo + notebook.tagTree = tagMap + + const rootTag = { + id: '', + name: '', + part: '', + path: notebook.base_dir, + parent: null, // parent tag id + childSet: new Set(), // child tag ids + noteSet: new Set(), // note ids + } + tagMap.set(rootTag.id, rootTag) + + // Iterate through all notes in the notebook, build the tag tree. + const allPages = ctx.locals.get('pages') + const pages = allPages.filter(p => p.notebook === notebook.id) + for (const page of pages.data) { + rootTag.noteSet.add(page._id) + + if (!page.tags) { + continue + } + + for (const hierarchyTag of page.tags) { + const parts = splitTag(hierarchyTag) + let parent = rootTag + for (const part of parts) { + const tagName = parent.name ? `${parent.name}/${part}` : part + const tagId = tagName.toLowerCase() + let tag = tagMap.get(tagId) + if (tag == null) { + tag = { + id: tagId, + name: tagName, + part: part, + path: `${notebook.base_dir}/tags/${tagId}`, + parent: parent.id, + childSet: new Set(), + noteSet: new Set(), + } + tagMap.set(tagId, tag) + parent.childSet.add(tagId) + } + + tag.noteSet.add(page._id) + parent = tag + } + } + } + + notebook.noteMap = pages.map(p => new NotePage(p)).reduce((map, note) => { + map.set(note.id, note) + return map + }, new Map()) + + // Sort child tags for each tag. + for (const [_, tag] of tagMap) { + tag.children = Array.from(tag.childSet) + tag.children.sort() + } + + return notebook +} + +function getNotebooksObject(ctx) { + const notebooks = { + tree: {}, + } + + const data = ctx.locals.get('data') + const list = [] + for (const [key, info] of Object.entries(data)) { + if (!key.startsWith('notebooks/') || key.endsWith('.DS_Store')) { + continue + } + const id = key.substring(10) + list.push(prepareNotebook(id, info, ctx)) + } + list.sort((a, b) => a.sort - b.sort) + for (const info of list) { + notebooks.tree[info.id] = info + } + + return notebooks +} + +module.exports = ctx => { + const notebooks = getNotebooksObject(ctx) + ctx.theme.config.notebooks = notebooks +} diff --git a/scripts/generators/notebooks.js b/scripts/generators/notebooks.js new file mode 100644 index 00000000..b34462e7 --- /dev/null +++ b/scripts/generators/notebooks.js @@ -0,0 +1,71 @@ +/** + * notebooks v1 + */ +const pagination = require('hexo-pagination') + +function paginationWithEmpty(base, posts, options={}) { + const { layout, data = {} } = options + if (posts.length === 0) { + base = `${base}/` + return [{ + path: base, + layout: layout, + data: { + ...data, + base: base, + total: 1, + current: 1, + current_url: base, + posts: posts, + prev: 0, + prev_link: '', + next: 0, + next_link: '', + } + }] + } else { + return pagination(base, posts, options) + } +} + +hexo.extend.generator.register('notebooks', function (locals) { + const { site_tree, notebooks } = hexo.theme.config + if (notebooks.tree.length === 0) { + return [] + } + + const routes = [] + + // The index page of all notebooks. + routes.push({ + path: site_tree.notebooks.base_dir + '/index.html', + layout: ['notebooks'], + data: { + layout: 'notebooks', + menu_id: site_tree.notebooks.menu_id, + } + }) + + for (const notebook of Object.values(notebooks.tree)) { + const pages = locals.pages.filter(p => notebook.noteMap.has(p._id)).sort(notebook.order_by) + pages.data.sort((a, b) => notebook.noteMap.get(b._id).pin - notebook.noteMap.get(a._id).pin) + + // Note list pages (for every tag) of current notebook. + for (const [_, tag] of notebook.tagTree) { + const notes = pages.filter(p => tag.noteSet.has(p._id)) + const slices = paginationWithEmpty(tag.path, notes, { + perPage: notebook.per_page, + layout: ['notes'], + data: { + layout: 'notes', + menu_id: notebook.menu_id, + notebook: notebook.id, + activeTag: tag.id, + } + }) + routes.push(...slices) + } + } + + return routes +}) diff --git a/source/css/_components/pages/notebook.styl b/source/css/_components/pages/notebook.styl new file mode 100644 index 00000000..ec17780b --- /dev/null +++ b/source/css/_components/pages/notebook.styl @@ -0,0 +1,65 @@ +.md-text .tag-list + display: flex + flex-wrap: wrap + padding: 0 + margin-top: 2rem + a.tag + display: inline-flex + align-items: center + position: relative + color: var(--text-p2) + margin: 4px + padding: .5em .75rem + border-radius: 4px + background: var(--block) + font-size: $fs-13 + font-weight: 500 + &:before + content: "#" + margin-left: -2px + margin-right: 2px + opacity: .4 + &:hover + &:before + color $color-theme + opacity: 1 + color: var(--text) + background: var(--block-hover) + +.post-list .post-card .meta.cap .tag + &:before + content: "#" + margin-left: -2px + margin-right: 2px + opacity: .4 + +.widget-body.tag-tree .tag-subtree > a > .tag-switcher-wrapper + width: 1.75rem + height: 0.875rem + display: flex + justify-content: end + align-items: center + &:hover + color: $color-theme + +.widget-body.tag-tree .tag-subtree.parent-tag > a .tag-switcher + display: inline-block + height: 0.5rem + width: 0.5rem + border-width: 1px + border-style: none solid solid none + transform: translateX(-25%) rotate(-45deg) + +.widget-body.tag-tree .tag-subtree.parent-tag.expanded > a .tag-switcher + transform: translateY(-25%) rotate(45deg) + +.widget-body.tag-tree .tag-subtree.parent-tag > .tag-subtree + display: none + +.widget-body.tag-tree .tag-subtree.parent-tag.expanded > .tag-subtree + display: block + +.widget-body.tag-tree .tag-subtree .tagcon + font-size: smaller + opacity: 0.4 + margin-right: 0.25rem