Hexo 博客 Fluid 主题实现代码折叠和文字遮盖效果

方案

hexo 博客默认是没有代码折叠功能的,如果需要实现代码折叠功能,可以安装 hexo-sliding-spoiler 库、借助 Hexo 过滤器功能或者更换代码高亮渲染引擎。

hexo-sliding-spoiler 实现代码折叠

hexo-sliding-spoiler 提供 demo 演示:https://github.com/fletchto99/hexo-sliding-spoiler/blob/master/img/example.gif

安装命令

1
2
3
npm install hexo-sliding-spoiler --save
# or using yarn
yarn add hexo-sliding-spoiler

使用示例

1
2
3
{% spoiler title %}
content
{% endspoiler %}

带空格的需要使用双引号

1
2
3
{% spoiler "Several spaces in the title" %}
content
{% endspoiler %}

如果没起作用,可能是 hexo 没有检测到插件,可以到_config.yml 中加上

1
2
plugins:
- hexo-sliding-spoiler

使用示例

直接使用的话,会发现代码块和 spoiler 区域有间距,而且标题也是默认白色,如果想要修改的话,可以找到 node_modules\hexo-sliding-spoiler\assets\spoiler.css 进行修改。我的修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
.spoiler {
margin: 0;
padding: 0;
border: 1px solid #353535;
border-radius: 3px;
position: relative;
clear: both;
}

.spoiler .spoiler-title {

background: #303030;
margin: 0;
padding: 5px 15px;
color: #d6d6d6;
font-weight: bold;
font-size: 13px;
display: block;
cursor: pointer;
}

.spoiler .spoiler-title:before {
font-weight: bold;
}

.spoiler.collapsed .spoiler-title:before {
content: "Show ";
}

.spoiler.expanded .spoiler-title:before {
content: "Hide ";
}

.spoiler .spoiler-content {
padding: 0;
margin-bottom: 0;
-moz-transition-duration: 0.3s;
-webkit-transition-duration: 0.3s;
-o-transition-duration: 0.3s;
transition-duration: 0.3s;
-moz-transition-timing-function: ease-in-out;
-webkit-transition-timing-function: ease-in-out;
-o-transition-timing-function: ease-in-out;
transition-timing-function: ease-in-out;
}
.spoiler .spoiler-content figure {
margin: 0;
}
.spoiler.collapsed .spoiler-content {
overflow: auto;
max-height: 0;
}

.spoiler.expanded .spoiler-content {
max-height: 3000px;
overflow: auto;
}

.spoiler .spoiler-content p:first-child {
margin-top: 0 !important;
}

但还是不太完美,自由度不是很够,也可能是我不太会修改吧。

更换代码高亮渲染器实现代码折叠(目前使用的方案)

将渲染器更换为 Hexo Shiki Plugin

实际上我并不知道该插件可以实现代码折叠,而是在配置该插件时发现的代码折叠功能,该功能在插件内实际是 highlight_height_limit 项,代码超出此项设定的高度则自动折叠。

⚠️注意:

该插件在代码类型的处理上存在 bug,详见:issue

解决办法是所有代码类型以后只写小写。

hexo-spoiler 实现文字遮盖

hexo-sliding-spoiler 是受 hexo-spoiler 插件的启发而成的。hexo-spoiler 同样可以帮助你做到文字遮挡效果:

在线演示:http://htmlpreview.github.io/?https://github.com/unnamed42/hexo-spoiler/blob/master/example/index.html

recording

安装命令

1
npm install hexo-spoiler --save

和上面的插件一样,如果没有起作用可能是配置文件得配置一下

1
2
plugins:
- hexo-spoiler

语法

1
{% spoiler option:value text... %}

option:value 中的 value 为可配置选项:

Optionname 选项名 Type 类型 value 值 Effect 效果
style string blur or box 文本将被模糊处理或是被方框覆盖
color string All valid css color
NO spaces allowed for inline option!
仅在 style:box 时起作用;更改框的颜色。默认颜色为黑色。(不允许内联选项中存在空格)
p boolean(in _config.yml or front-matter)
string(in inline options)
empty or any string 遮盖文字将因 <p> 标签换行而非 < span > 标签。如果你想在剧透文本前后加一个新行,可以添加这个。
对于内联选项,分配任何值(除了 “false”),甚至忽略它会打开它;“false” 意味着关闭。默认状态为关闭。

这表的配置项都在_config.yml 中进行配置,例如:

1
2
3
4
5
# ... other configs
# be top-level
spoiler:
style: blur
p: true

对于单独的一篇文章,你可以在文章的 front-matter 中设置,例如:

1
2
3
4
5
6
7
---
title: blah blah
spoiler:
style: box
color: yellow
p: false
---

优先级:inline option 内联选项 > front-matter > _config.yml > default

warning

如果你改变了_config.yml,请运行 hexo clean 清除缓存。

Hexo 过滤器实现代码折叠(若不更换高亮渲染则推荐这个)

Kiyan 佬的文章启发了我,于是我自行钻研了一下才有了这一段。这一段内容实际是我后来才加上的,所以放在了这里,文章顺序有点奇怪见谅。

编写 js

由于 fluid 主题已经引入了 Bootstrap,所以我们可以编写一个 js 文件来满足我们的要求。

scripts 目录下创建 codeFloding.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 获取唯一 ID
function getUuid() {
// 生成一个随机的唯一标识符,由当前时间戳和随机数拼接而成
return Math.random().toString(36).substring(2, 8) + Date.now().toString(36);
}

// 注册 Hexo 的过滤器,在文章渲染后处理代码块
hexo.extend.filter.register(
"after_post_render",
(data) => {
// 获取代码高亮设置
const { line_number, lib } = hexo.theme.config.code.highlight;

let reg;
// 根据使用的高亮库设置正则表达式
if (lib === "highlightjs") {
if (line_number) {
reg = /(<figure class="highlight.+?>)(.+?hljs (.*?)".+?)(<\/figure>)/gims; // 处理包含行号的 figure
} else {
reg = /(<div class="code-wrapper.+?>)(.+?hljs (.*?)".+?)(<\/div>)/gims; // 处理不包含行号的 div
}
} else if (lib === "prismjs") {
reg = /(<div class="code-wrapper.+?>)(.+?data-language="(.*?)".+?)(<\/div>)/gims; // 处理 PrismJS 的代码块
}

// match begin inner lang end offset string 分别表示的意思是:匹配到的内容的开始、内容、语言、结束、偏移量、原始字符串
data.content = data.content.replace(reg, (match, begin, inner, lang, end, offset, string) => {
const collapseId = `collapse-${getUuid()}`; // 生成唯一的折叠 ID

// 创建一个容器,用于包裹按钮和语言提示
const collapseContainer = `
<div class="collapse-header">
<button class="collapse-btn collapsed" type="button" data-toggle="collapse" data-target="#${collapseId}">
<svg t="1730562455310" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9905">
<path d="M857.766234 511.488511L345.623017 0 297.174825 49.348412 759.846873 511.488511 297.174825 974.651588 345.623017 1022.977023z" fill="#ffffff" p-id="9906">
</path>
</svg>
</button>
<span>${lang}</span> <!-- 显示代码语言 -->
</div>
`;
// collapse表示默认收起,collapse show表示默认展开
const collapseDiv = `<div class="collapse" id="${collapseId}">${inner}</div>`;
return begin + collapseContainer + collapseDiv + end; // 返回修改后的内容
});
return data; // 返回处理后的数据
},
10000 // 设置优先级,使得在其他渲染后执行
);

这份代码的核心在于 <button class="collapse-btn collapsed"> 中的属性:

1
data-toggle="collapse" data-target="#${collapseId}"

为什么这么说呢?

当使用 data-toggle="collapse"data-target 属性时,Bootstrap 的 JavaScript 会自动识别这些属性并处理折叠效果。这意味着我们不需要手动编写任何其他 JavaScript 代码来控制折叠内容的显示和隐藏。

处理过程是:

  1. 当收起或展开按钮被点击时,Bootstrap 先查找与 data-target 属性对应的元素(这里是被加上了 id="${collapseId}"collapseDiv)。
  2. 根据当前状态,Bootstrap 会往查找到的带 data-target 值(这里是 collapseId)的标签中添加或移除 show 类,从而控制折叠内容的显示或隐藏。
    人话是:Bootstrap 会自动给 const collapseDiv 加上 show 类或移除来控制展开还是收起。collapseDiv 这里是代码块。
  3. 同时给按钮更新 aria-expanded 属性,以反映当前状态(值为 true 即展开反之则收起)。
  4. 同时给按钮增加 collapsed 类来标识收起状态,若无此类则表示展开。

所以,这里有两个值的设定需要注意:

  1. const collapseDivclass="collapse"
    若类名为 class="collapse" 表示默认将代码块收起,collapse show 表示默认展开。
  2. const collapseDiv 的类名若为不带 show 的类名表示默认收起,则 <button class="collapse-btn collapsed" 中的类名 class="collapse-btn" 需要加上 collapsed
    即:class="collapse-btn collapsed"collapseDiv 收起或展开的状态保持一致。

例如,我的代码默认行为是将代码块收起,所以要设置 const collapseDiv 为:

1
const collapseDiv = `<div class="collapse" id="${collapseId}">${inner}</div>`;

按钮 button 应设置为:

1
<button class="collapse-btn collapsed" ...略...

另外,你也可以找一个你喜欢的 svg 图用于设置按钮的图标。

相关推荐:阿里巴巴矢量图库

编写 css

source\css\ 目录下创建 codeFloding.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.collapse-header {
border-radius: 5px 5px 0 0; /* 设置圆角,使背景更柔和 */
position: relative; /* 使按钮可以定位 */
z-index: 1;
background-color: #303030; /* 设置背景颜色为深灰色 */
padding: 5px 5px; /* 添加上下左右各5px的内边距 */
}

.collapse-header span {
color: white;
}

.collapse-header button {
border: none; /* 去掉按钮的边框 */
margin-right: 5px; /* 按钮与语言文本之间的间隔 */
background-color: #303030; /* 按钮背景与容器背景相同 */
cursor: pointer; /* 鼠标悬停时显示为手指光标,表示可点击 */
transition: transform 0.3s ease; /* 添加平滑的旋转过渡效果 */
}

.collapse-header button:focus {
outline: none; /* 去掉按钮的聚焦样式,保持界面简洁 */
}

.collapse-header button svg {
width: 10px;
height: 10px;
fill: white;
transition: transform 0.3s ease; /* 添加过渡效果 */
}

/* 初始状态:箭头向右,为收起状态 */
.collapse-header .collapse-btn.collapsed {
transform: rotate(0deg);
}
/* 点击按钮后,箭头向下,为展开状态 */
.collapse-header .collapse-btn {
transform: rotate(90deg);
}

/* 为展开的内容区域添加底部连接效果 */
.collapse.show {
border-radius: 0 0 5px 5px;
background-color: #303030;
}
.category-collapse.collapse.show {
/* 将背景色设置为透明,使内容区域背景透明 */
background-color: transparent;
}

有兴趣可以自行修改 css 样式使其符合你的审美。

warning

请注意在_config.fluid.yml 中进行引入

一个可选的配置

因为我默认开启了代码块的语言类型显示,所以在代码的右上角会自动显示代码语言,如果点击按钮将代码展开,则会在 header 上出现两个代码语言文字,一左一右不是很美观。

于是我创建了 source\js\watch.js

前情提要

watch.js 是以前写的,你也可以找一个已经写过的 js 文件,在底部写,或是单独创建一个 js 文件用于编写。

为了解决上面的重复问题,编写 js 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 给代码收起按钮添加监听事件
const collapseBtns = document.querySelectorAll('.collapse-header button');
collapseBtns.forEach(button => {
button.addEventListener('click', () => {
// 获取当前按钮的类名
const btnClassName = button.className; // 使用 button 变量,而不是 collapseBtns
const codeTypeSpan = button.nextElementSibling; // 获取当前按钮后面的 span 标签
// console.log('当前获取到的按钮类名为:', btnClassName);
// console.log('当前获取到的 span 标签为:', codeTypeSpan);
// console.log('当前检测展开状态:', btnClassName.includes('collapsed'));

if (btnClassName.includes('collapsed')) { // 判断按钮是否在展开状态
// 展开之后将 span 标签设为隐藏
codeTypeSpan.style.display = 'none';
} else {
// 收起之后将 span 标签设为显示
codeTypeSpan.style.display = 'inline-block';
}
});
});

这样一来,点击按钮展开之后,header 的代码语言提示问题将会被设为隐藏(并非卸载)。

一个可选配置的优化

为什么说是优化呢?因为实际使用下来,我发现每次都要手动使用鼠标点击那么小的一个按钮实在过于折磨,不如直接点击 header 实现折叠或展开。修改就不多说了,如果你能看懂上面的配置,那么下面的配置也能,因为我只做了小小的修改。

codeFloding.js:将 data-toggledata-target 换到 header 上,并在 watch.js 中手动切换 button 的 class 类名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 获取唯一 ID
function getUuid() {
return Math.random().toString(36).substring(2, 8) + Date.now().toString(36);
}

// 注册 Hexo 的过滤器,在文章渲染后处理代码块
hexo.extend.filter.register(
"after_post_render",
(data) => {
const { line_number, lib } = hexo.theme.config.code.highlight;

let reg;
if (lib === "highlightjs") {
reg = line_number
? /(<figure class="highlight.+?>)(.+?hljs (.*?)".+?)(<\/figure>)/gims
: /(<div class="code-wrapper.+?>)(.+?hljs (.*?)".+?)(<\/div>)/gims;
} else if (lib === "prismjs") {
reg = /(<div class="code-wrapper.+?>)(.+?data-language="(.*?)".+?)(<\/div>)/gims;
}

data.content = data.content.replace(reg, (match, begin, inner, lang, end) => {
const collapseId = `collapse-${getUuid()}`;

const collapseContainer = `
<div class="collapse-header" data-toggle="collapse" data-target="#${collapseId}">
<button class="collapse-btn collapsed" type="button">
<svg t="1730562455310" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9905">
<path d="M857.766234 511.488511L345.623017 0 297.174825 49.348412 759.846873 511.488511 297.174825 974.651588 345.623017 1022.977023z" fill="#ffffff" p-id="9906"></path>
</svg>
</button>
<span>${lang}</span>
</div>
`;
const collapseDiv = `<div class="collapse" id="${collapseId}">${inner}</div>`;
return begin + collapseContainer + collapseDiv + end;
});
return data;
},
10000
);

codeFloding.css:增加了鼠标悬停在 header 时,将鼠标样式改为 pointer 提示用户

1
2
3
4
5
6
7
8
9
.collapse-header {
border-radius: 5px 5px 0 0; /* 设置圆角,使背景更柔和 */
position: relative; /* 使按钮可以定位 */
z-index: 1;
background-color: #303030; /* 设置背景颜色为深灰色 */
padding: 5px 5px; /* 添加上下左右各5px的内边距 */
cursor: pointer; /* 鼠标悬停时显示为手指光标 */
}
...略...

watch.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 给代码收起按钮和整个 header 添加监听事件
*/
const collapseHeaders = document.querySelectorAll('.collapse-header');
collapseHeaders.forEach(header => {
const button = header.querySelector('button');
const codeTypeSpan = header.querySelector('span');

header.addEventListener('click', () => {
console.log('header clicked');

// 手动切换按钮的类名
button.classList.toggle('collapsed');

// 根据按钮状态显示或隐藏 span
if (button.classList.contains('collapsed')) {
// console.log('收起代码块');
codeTypeSpan.style.display = 'inline-block';
} else {
// console.log('展开代码块');
codeTypeSpan.style.display = 'none';
}
});

button.addEventListener('click', (event) => {
// 阻止事件冒泡,避免触发 header 的点击事件
event.stopPropagation();
});
});

以上内容若有侵权,可联系删除