XSS_上下文防御

XSS_上下文防御

  • 原来的分类页面太丑了,并且分类之间看的不是很清楚。于是我就想优化一下,添加新的样式。

  • 添加好了以后,我又觉得如果后期文章增多,页面就会非常杂乱,因此我又优化成了可以展开折叠的页面。

  • 优化成可以展开折叠的页面之后,看到最后一层分类的时候总感觉空荡荡的,总觉得到这样了,不如把所有的文章都直接显示出来得了。

于是便有了这篇文章。

分类折叠功能

在themes\next\layout\page.swig中,找到分类页面,替换成下面的代码:

1
2
3
4
5
6
7
8
{% elif page.type === 'categories' %}
<div class="category-all-page">
<div class="category-all-title">
{{ _p('counter.categories', site.categories.length) }}
</div>
<div class="category-all" id="categoryContainer">
{{ list_categories() }}
</div>

我的自定义CSS样式在source_data\styles.styl中,在你的自定义样式中添加:

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
/* 分类折叠样式 */
.category-toggle-icon {
display: inline-block;
width: 16px;
margin-right: 5px;
text-align: center;
cursor: pointer;
}

.category-list-child {
padding-left: 20px;
}

.category-list-link {
position: relative;
padding-left: 0 !important;
}

/* 添加手型光标表示可点击 */
.category-list-item > .category-list-link {
cursor: pointer;
}

/* 悬停效果 */
.category-list-item > .category-list-link:hover {
background-color: #f5f5f5;
}

创建js文件themes\next\source\js\category-collapse.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
document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('categoryContainer');
if (!container) return;

// 1. 添加折叠图标
container.querySelectorAll('.category-list-item').forEach(item => {
const childList = item.querySelector('ul');
if (childList) {
const header = item.querySelector('.category-list-link').parentNode;

// 创建折叠图标
const icon = document.createElement('i');
icon.className = 'fa fa-caret-right category-toggle-icon';
header.insertBefore(icon, header.firstChild);

// 添加点击事件处理
header.addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
e.preventDefault();
}

const isExpanded = childList.style.display !== 'none';
childList.style.display = isExpanded ? 'none' : 'block';
icon.classList.toggle('fa-caret-right', !isExpanded);
icon.classList.toggle('fa-caret-down', isExpanded);
});
}
});

// 2. 初始隐藏所有子分类
container.querySelectorAll('.category-list-item ul').forEach(ul => {
ul.style.display = 'none';
});
});

记得在themes\next\layout_layout.swig中的<body>前引用:

1
2
3
4
{{ partial('_scripts/noscript.swig', {}, {cache: theme.cache.enable}) }}
{% if page.type === 'categories' %}
<script src="/js/category-collapse.js"></script>
{% endif %}

没错就这么简单。剩下的可以优化一下CSS。

文章折叠展开

终于做好了,肝死我了:

themes\next\layout\page.swig

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
          {% elif page.type === 'categories' %}
<--
<div class="category-all-page">
<div class="category-all-title">
{{ _p('counter.categories', site.categories.length) }}
</div>
<div class="category-all">
{{ list_categories() }}
</div>
</div>
-->
<div class="category-all-page">
<div class="category-all-title">
{{ _p('counter.categories', site.categories.length) }}
</div>

<div class="category-container">
<!-- 左侧分类树 -->
<div class="category-tree">
<div class="category-all" id="categoryContainer">
{{ list_categories() }}
</div>
</div>

<!-- 右侧文章列表 -->
<div id="categoryPostsContainer" class="category-posts-container">
<div class="posts-collapse">
<div class="collection-title">
<h2 class="collection-header">
<span id="currentCategoryName"></span>
<small>{{ __('title.category') }}</small>
</h2>
</div>
<div id="categoryPostsList"></div>
</div>
</div>
</div>
</div>

themes\next\source\js\category-collapse.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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('categoryContainer');
if (!container) return;

// 遍历所有分类项
container.querySelectorAll('.category-list-item').forEach(item => {
const link = item.querySelector('.category-list-link');
const childList = item.querySelector('ul');
const count = item.querySelector('.category-list-count');

// 移除所有折叠图标(如果有)
const existingIcons = item.querySelectorAll('.category-toggle-icon');
existingIcons.forEach(icon => icon.remove());

// 如果有子分类,添加折叠图标到右侧
if (childList) {
const icon = document.createElement('i');
icon.className = 'fa fa-caret-down category-toggle-icon';
icon.style.marginLeft = '5px';
icon.style.cursor = 'pointer';

// 添加到计数后面
if (count) {
count.parentNode.insertBefore(icon, count.nextSibling);
} else {
link.parentNode.insertBefore(icon, link.nextSibling);
}

// 初始状态:第一级分类展开,其他级折叠
const isTopLevel = item.parentElement.classList.contains('category-list');
if (!isTopLevel) {
childList.style.display = 'none';
icon.classList.remove('fa-caret-down');
icon.classList.add('fa-caret-right');
}

// 添加折叠/展开事件
icon.addEventListener('click', function(e) {
e.stopPropagation();
const isExpanded = childList.style.display === 'block' || childList.style.display === '';
childList.style.display = isExpanded ? 'none' : 'block';
icon.classList.toggle('fa-caret-down', !isExpanded);
icon.classList.toggle('fa-caret-right', isExpanded);
});
}

// 为分类链接绑定点击事件
// 在分类链接点击事件中添加展开子分类功能
link.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();

// 获取分类名称和路径
const categoryName = link.textContent.trim();
const categoryPath = link.getAttribute('href');

// 如果有子分类,则切换展开/折叠状态
const childList = item.querySelector('ul');
const icon = item.querySelector('.category-toggle-icon');
if (childList && icon) {
// 切换展开/折叠状态
const isExpanded = childList.style.display === 'block' ||
childList.style.display === '';

childList.style.display = isExpanded ? 'none' : 'block';
icon.classList.toggle('fa-caret-down', !isExpanded);
icon.classList.toggle('fa-caret-right', isExpanded);
}

// 显示文章列表
loadCategoryPosts(categoryName, categoryPath);
});
});

// 加载分类文章的函数
function loadCategoryPosts(categoryName, categoryPath) {
// 移除所有激活状态
document.querySelectorAll('.category-list-link').forEach(link => {
link.classList.remove('active-category');
});
document.querySelectorAll('.category-list-item').forEach(item => {
item.classList.remove('active-ancestor');
});

// 为当前分类添加激活状态
const currentLink = document.querySelector(`.category-list-link[href="${categoryPath}"]`);
if (currentLink) {
currentLink.classList.add('active-category');

// 为当前分类的所有父级添加 active-ancestor
let parentItem = currentLink.closest('.category-list-child')?.closest('.category-list-item');
while (parentItem) {
parentItem.classList.add('active-ancestor');
parentItem = parentItem.closest('.category-list-child')?.closest('.category-list-item');
}
}

// 显示当前分类名称
document.getElementById('currentCategoryName').textContent = categoryName;

// 显示文章容器
const postsContainer = document.getElementById('categoryPostsContainer');
postsContainer.style.display = 'block';

// 滚动到分类容器顶部
document.querySelector('.category-container').scrollTop = 0;

// 获取文章列表容器
const postsList = document.getElementById('categoryPostsList');
postsList.innerHTML = '<div class="loading-spinner"></div><p>加载中...</p>';

// 发送AJAX请求获取分类文章
fetch(categoryPath)
.then(response => response.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');

// 确保选择正确的容器
const postsContainer = doc.querySelector('.category-posts');

if (postsContainer) {
postsList.innerHTML = postsContainer.innerHTML;

// 添加必要的类名
postsList.querySelectorAll('.post').forEach(post => {
post.classList.add('post-collapse-item');
});

postsList.querySelectorAll('.post-title-link').forEach(link => {
link.classList.add('post-title-link');
});
} else {
postsList.innerHTML = '<p>该分类下暂无文章</p>';
}
})
.catch(error => {
console.error('加载文章失败:', error);
postsList.innerHTML = '<p>加载文章失败,请稍后再试</p>';
});
}
// 在遍历分类项后添加以下代码
// 折叠除第一个顶级分类外的所有顶级分类
const topLevelItems = container.querySelectorAll('.category-list > .category-list-item');
topLevelItems.forEach((item, index) => {
if (index > 0) { // 跳过第一个分类
const childList = item.querySelector('ul');
const icon = item.querySelector('.category-toggle-icon');
if (childList && icon) {
childList.style.display = 'none';
icon.classList.remove('fa-caret-down');
icon.classList.add('fa-caret-right');
}
}
});

// 确保第一个分类的子分类展开
const firstTopLevelItem = topLevelItems[0];
if (firstTopLevelItem) {
const childList = firstTopLevelItem.querySelector('ul');
const icon = firstTopLevelItem.querySelector('.category-toggle-icon');
if (childList && icon) {
childList.style.display = 'block';
icon.classList.add('fa-caret-down');
icon.classList.remove('fa-caret-right');
}
}
// 默认加载第一个分类的文章
const firstCategoryLink = container.querySelector('.category-list-link');
if (firstCategoryLink) {
const categoryName = firstCategoryLink.textContent.trim();
const categoryPath = firstCategoryLink.getAttribute('href');
loadCategoryPosts(categoryName, categoryPath);
}
});

themes\next\layout\category.swig

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="post-block">
<div class="posts-collapse">
<div class="collection-title">
<h2 class="collection-header">
{{- page.category }}
<small>{{ __('title.category') }}</small>
</h2>
</div>
<div class="category-posts"> <!-- 添加这个类名 -->
{{ post_template.render(page.posts) }}
</div>
</div>
</div>

themes\next\layout_macro\post-collapse.swig

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
  <article class="post post-collapse-item" itemscope itemtype="http://schema.org/Article">
<header class="post-header">
<!--
<div class="post-meta">
<time itemprop="dateCreated"
datetime="{{ moment(post.date).format() }}"
content="{{ date(post.date, config.date_format) }}">
{{ date(post.date, 'MM-DD') }}
</time>
</div>
-->
<div class="post-title">
<span class="post-meta">
<time itemprop="dateCreated"
datetime="{{ moment(post.date).format() }}"
content="{{ date(post.date, config.date_format) }}">
{{ date(post.date, 'MM-DD') }}
</time>
</span>
{%- if post.link %}{# Link posts #}
{%- set postTitleIcon = '<i class="fa fa-external-link-alt"></i>' %}
{%- set postText = post.title or post.link %}
{{ next_url(post.link, postText + postTitleIcon, {class: 'post-title-link post-title-link-external', itemprop: 'url'}) }}
{% else %}
<a class="post-title-link" href="{{ url_for(post.path) }}" itemprop="url">
<span itemprop="name">{{ post.title or __('post.untitled') }}</span>
</a>
{%- endif %}
</div>

</header>
</article>

source_data\styles.styl

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
/* 分类页面布局 */
/* ===================== 滚动条美化 - 白色样式 ===================== */
/* 滚动条美化 */
@supports (scrollbar-color: auto) {
.category-tree,
.category-posts-container {
scrollbar-color: rgba(255, 255, 255, 0.5) rgba(240, 240, 240, 0.5);
scrollbar-width: thin;
}
}

/* ===================== 布局优化 ===================== */
.category-container {
display: flex;
margin-top: 30px;
gap: 30px;
height: 70vh;
border-radius: 80px;
.category-tree {
flex: 0 0 48%;
min-width: 0;
overflow-x: visible;
max-height: 100%;
overflow-y: auto; /* 恢复垂直滚动条 */
overflow-x: hidden;
padding-right: 10px;
display: flex;
flex-direction: column;
position: relative; /* 为滚动条定位做准备 */
direction: rtl;
/* 添加可滚动区域 */
/* 左侧分类树滚动区域 */

&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(200, 200, 200, 0.5);
}

}

/* 确保分类树内容正常显示 */
#categoryContainer {
direction: ltr; /* 内容从左到右 */
padding-right: 10px; /* 补偿滚动条宽度 */
}

.category-posts-container {
flex: 2;
display: none;
overflow-y: auto;
max-height: 100%;
background: rgba(255, 255, 255, 0.8); /* 添加半透明白色背景 */
border-radius: 8px; /* 添加圆角 */
padding: 10px; /* 增加内边距 */
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 添加阴影增强层次感 */
}
}

/* 调整分类列表 */
#categoryContainer {
display: block;
//padding: 10px 0;
.category-list {
display: block;
max-height: none;
margin-right: 8px;
}
}

/* 分类项样式优化 */
.category-list-item {
overflow: visible; /* 确保内容可见 */
display: flex;
min-width: 0; /* 允许内容收缩 */
flex-wrap: wrap;
align-items: center;
margin-bottom: 6px;
margin: 5px 0px;
position: relative;
width: 100%;
background: rgba(255, 255, 255, 0.2); /* 默认透明背景 */
border-radius: 6px;
padding: 5px 8px;
transition: all 0.3s ease;

/* 确保子分类不会导致溢出 */
.category-list-child {
width: 90%;
}

.category-list-link {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 2px 9px;
border-radius: 4px;
color: #2c3e50;
text-decoration: none !important;
transition: all 0.3s;

&:hover {
background-color: #cccccc;
color: #2841a4f7;

}
}

.category-list-count {
flex-shrink: 0;
background: #eaeef5;
color: #6c757d;
font-size: 0.6rem !important; /* 进一步缩小字体大小 */
padding: 2px 2px; /* 减小内边距 */
border-radius: 20px;
transition: all 0.3s;
min-width: 20px; /* 设置最小宽度 */
text-align: center; /* 居中显示 */
}

/* 折叠图标样式 */
.category-toggle-icon {
flex-shrink: 0;
color: #6c757d;
font-size: 0.9rem;
margin-left: 5px;
transition: transform 0.3s ease;
min-width: 30px;
}

&:hover .category-list-count {
background: #d1e0ff;
color: #4b6cb7;
}
}

/* 为不同层级添加背景色(降低透明度) */
/* 第一级分类 */
#categoryContainer > .category-list > .category-list-item {
background: rgba(255, 255, 255, 0.85); /* 降低透明度 */
}

/* 第二级分类 */
#categoryContainer > .category-list > .category-list-item > .category-list-child > .category-list-item {
background: rgba(200, 230, 255, 0.5); /* 降低透明度 */
}

/* 第三级分类 */
#categoryContainer > .category-list > .category-list-item > .category-list-child > .category-list-item > .category-list-child > .category-list-item {
background: rgba(230, 255, 230, 0.5); /* 降低透明度 */
}

/* 第四级分类 */
#categoryContainer > .category-list > .category-list-item > .category-list-child > .category-list-item > .category-list-child > .category-list-item > .category-list-child > .category-list-item {
background: rgba(255, 230, 230, 0.5); /* 降低透明度 */
}

/* 悬停效果 - 只改变当前项 */
/* 只悬停当前项 */
.category-list-item:hover {
background-color: rgba(240, 248, 255, 0.7) !important;
z-index: 20;
}

/* 添加以下样式到您的 styles.styl 文件中 */

/* 当前选中分类样式 */
.category-list-link.active-category {
background-color: #4b6cb7 !important;
color: white !important;

& + .category-list-count {
background: white !important;
color: #4b6cb7 !important;
}
}

/* 祖先分类样式 */
.category-list-item.active-ancestor > .category-list-link {
background-color: rgba(200, 230, 255, 0.7) !important;
}


/* 响应式设计优化 */
@media (max-width: 768px) {
.category-container {
.category-tree,
.category-posts-container {
flex: 0 0 100%; /* 移动端占满宽度 */
min-width: 100%;
max-height: none;
}

.category-tree {
direction: ltr; /* 移动端恢复默认方向 */
padding-right: 0;
max-height: 40vh;
}
}
}

/* 确保右侧标题可见 */
.collection-header {
color: #333333 !important;
text-shadow: none;
padding: 0;

#currentCategoryName {
font-weight: bold;
font-size: 1.5rem;
color: #222;
}

small {
color: #666 !important;
font-size: 1rem;
}
}

/* 文章列表视觉优化 */
#categoryPostsList {
* {
opacity: 1 !important;
}

.post-title-link,
.post-title-link span {
color: #111111 !important;
//font-weight: 600 !important;
font-size: 1rem !important;

&:hover {
color: #4b6cb7 !important;
}
}

.post-meta {
position: absolute;
left: 0;
top: 0;
display: inline-block;
padding: 2px 10px;
font-size: 0.9em;
color: #666;
font-size: 0.85rem !important;
border-radius: 4px;
margin: 3px 0 3px 5px;

time {
border: 0;
}
}

.post-header {
display: flex;
flex-direction: column;
align-items: center;
margin: 0px 15px;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
padding: 8px 12px;
}

.post-title {
display: block;
height: auto;
min_height: 1em;
overflow-wrap: break-word;
overflow: visible;
flex-grow: 1;
width: 100%;
background: #f8f8f8; /* 仅用于调试 */
line-height: 1.5;
font-size: 0;
padding: 15px 0 10px 0;
}


.collection-year {
font-size: 1.2rem;
font-weight: 600;
color: #444;
margin: 20px 0 10px;
padding-bottom: 5px;
border-bottom: 1px dashed #ddd;
}
}

/* 加载动画优化 */
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(75, 108, 183, 0.2);
border-radius: 50%;
border-top-color: #4b6cb7;
animation: spin 1s ease-in-out infinite;
margin-right: 10px;
}

@keyframes spin {
to { transform: rotate(360deg); }
}


/* 保持分类列表从左往右分布 */
.category-all-page .category-all {
direction: ltr;
margin-top: 0px;
}
#categoryPostsContainer {
.posts-collapse {
margin-left: 20px !important;
}
}

Here's something encrypted, password is required to continue reading.
阅读全文 »

Here's something encrypted, password is required to continue reading.
阅读全文 »

因为我是用了根据路径自动生成分类,但是使用hexo new -p /path/to/filename title命令的时候感觉很不方便,因为我希望我可以在同级目录下生成名字和标题一样的.md文件和文件夹。但是使用-p参数的时候我就得这样:

1
hexo new -p 渗透测试/弱口令/【渗透测试】弱口令 【渗透测试】弱口令

我得敲两遍标题,而且使用-p的时候不显示当前目录下的文件夹有哪些。觉得很不方便。因此我打算为我的hexo实现两个功能:

  1. 当我执行:hexo newp first/second/title 的时候,会在_post/first/second/下生成【second】title文件夹和【second】title.md文件,文章标题拼接为【second】title

  2. 在选择路径的时候按Tab键时,能像操作系统终端一样:

    1. 若只有一条路径符合我已经输入的路径,则自动补全。
    2. 若有多个目录符合我已经输入的路径,打印符合要求的所有路径。

经过不断的测试,我满足了我的需求,顿时觉得爽了很多。实现这两个功能的代码如下:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const { promisify } = require('util');

const mkdir = promisify(fs.mkdir);
const writeFile = promisify(fs.writeFile);
const readdir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);

hexo.extend.console.register('newp', 'Create new post with path (e.g., first/second/title)', {
options: [
{ name: '--auto-complete', desc: 'Enable tab completion' }
]
}, async function(args) {
const log = hexo.log || console.log;

if (args.autoComplete) {
return this.tabComplete(args);
}

const fullPath = args._[0];
if (!fullPath) {
log.error('Usage: hexo newp <path/levels/title>');
return;
}

// 处理 Windows 路径分隔符问题
const normalizedPath = fullPath.replace(/\\/g, '/');
const parts = normalizedPath.split('/');
const title = parts.pop();
const category = parts.join('/');
const lastDir = parts[parts.length - 1] || '';

// 生成文件夹名和文件名
const folderName = `【${lastDir}${title}`;
const fileName = `${folderName}.md`;

// 创建目录结构 - 确保文件夹和文件在同一层级
const baseDir = path.join(hexo.source_dir, '_posts', category);
const folderPath = path.join(baseDir, folderName);
const filePath = path.join(baseDir, fileName);

try {
// 创建文件夹(用于存放资源)
await mkdir(folderPath, { recursive: true });

// 使用模板文件
const templatePath = path.join(hexo.scaffold_dir, 'post.md');
let templateContent;

try {
templateContent = await readFile(templatePath, 'utf8');
} catch (e) {
// 如果模板文件不存在,使用默认模板
templateContent = [
'---',
'title: {{ title }}',
'date: {{ date }}',
'categories:',
'tags:',
'- private',
'description: 声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。',
'top:',
'comments: true',
'---'
].join('\n');
}

// 替换模板中的标题
const content = templateContent
.replace(/{{ title }}/g, folderName)
//.replace(/{{ date }}/g, new Date().toISOString());

// 创建 Markdown 文件
await writeFile(filePath, content);

log.info(`Created folder: ${folderPath}`);
log.info(`Created file: ${filePath}`);
} catch (error) {
log.error(`Error creating post: ${error.message}`);
}
});

// Tab 补全逻辑保持不变
hexo.extend.console.tabComplete = async function(args) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
completer: async (line) => {
const partial = line.trim().replace(/\\/g, '/');
const baseDir = path.join(hexo.source_dir, '_posts');

const matches = await this.findMatches(baseDir, partial);

if (matches.length === 1) {
const completed = matches[0] + '/';
return [[completed], completed];
} else if (matches.length > 1) {
console.log('\n' + matches.join('\n'));
}
return [matches, line];
}
});

rl.question('Enter post path: ', (line) => {
if (line.trim()) {
hexo.call('newp', { _: [line.trim()] }, () => {
rl.close();
});
} else {
rl.close();
}
});
};

// 路径匹配函数保持不变
hexo.extend.console.findMatches = async function(baseDir, partialPath) {
const parts = partialPath.split('/');
let currentDir = baseDir;
let existingPath = [];

for (const part of parts.slice(0, -1)) {
if (!part) continue;

const testDir = path.join(currentDir, part);
try {
if (!fs.existsSync(testDir)) break;
const stat = fs.statSync(testDir);
if (!stat.isDirectory()) break;

currentDir = testDir;
existingPath.push(part);
} catch (e) {
break;
}
}

const lastPartial = parts[parts.length - 1] || '';
let dirContents = [];

try {
dirContents = await readdir(currentDir, { withFileTypes: true });
} catch (e) {
return [];
}

return dirContents
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name)
.filter(name => name.startsWith(lastPartial))
.map(name => [...existingPath, name].join('/'));
};

只需要创建js文件:\blog\scripts\newp.js,将代码复制粘贴进去就可以了。

最终效果如下:

image-20250723143513850