diff --git a/_config.yml b/_config.yml
index 2b9329a..349c4cd 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 82a27b7..43cd458 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 f6f1103..9d8a4c6 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 689b444..892bb90 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 8c45c8c..abfdd04 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 56420c3..10ebc0d 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 8a0e29a..e6af1d4 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 4372a3e..14feadb 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 0000000..3e81c77
--- /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 47c143c..0695865 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 0000000..1a3722a
--- /dev/null
+++ b/layout/_partial/main/notebook/note_card.ejs
@@ -0,0 +1,36 @@
+<%# 笔记卡片 %>
+
+ <% if (note.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 0000000..1ecd84b
--- /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 0000000..aecab97
--- /dev/null
+++ b/layout/_partial/main/notebook/notebook_card.ejs
@@ -0,0 +1,10 @@
+<%# 笔记本信息卡片 %>
+
+
+
+
<%= 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 0000000..e076815
--- /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 90bfb2e..7c1047a 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 0000000..519f8bb
--- /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 d40142d..ffdf782 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 96152e5..9ffd270 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 e27f9c6..a36e2fb 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 ab7b0cf..c54094a 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 0000000..da132b3
--- /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) { %>
+
+
+
+ <%= 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 0000000..e9e46c2
--- /dev/null
+++ b/layout/notebooks.ejs
@@ -0,0 +1,8 @@
+<%# 笔记本列表页主体部分 %>
+<% for (const notebook of Object.values(theme.notebooks.tree)) { %>
+
+<% } %>
diff --git a/layout/notes.ejs b/layout/notes.ejs
new file mode 100644
index 0000000..ee221c7
--- /dev/null
+++ b/layout/notes.ejs
@@ -0,0 +1,12 @@
+<%# 笔记列表页主体部分 %>
+
+<%- include('_partial/main/notebook/paginator') %>
diff --git a/layout/page.ejs b/layout/page.ejs
index 02e64d9..cc2d972 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 924e80c..478bdf6 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 0000000..bda07d5
--- /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 0000000..b34462e
--- /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 0000000..ec17780
--- /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