maven

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.33</version>
</dependency>

<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.32</version>
</dependency>

ftl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
---
title: ${title}

tags:

- ${tags}

categories:

<#list categories as category>
- ${category}
</#list>

comments: ${comments?string("true", "false")}

date: ${date}

updated: ${updated}

description: ${description}

keywords: "${keywords}"
---

java

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
322
323
324
325
326
327
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import freemarker.cache.ClassTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* @author shuzhuoi
*/
@Slf4j
public class MarkdownFileUpdater {

private static final Configuration cfg = new Configuration(Configuration.VERSION_2_3_33);

static {
try {
// 配置FreeMarker模板目录
// cfg.setDirectoryForTemplateLoading(new File("templates"));
// 使用 ClassTemplateLoader 从类路径中加载模板
ClassTemplateLoader loader = new ClassTemplateLoader(MarkdownFileUpdater.class, "/templates");
cfg.setTemplateLoader(loader);
cfg.setDefaultEncoding("UTF-8");
} catch (Exception e) {
log.info("Failed to set FreeMarker template directory", e);
}
}


// 指定文件夹路径
static String folderPath = "D:\\shuzhuoi\\md\\shuzhuo-md";

// 定义需要过滤掉的目录名
static List<String> excludedDirs = Arrays.asList(".git", ".history");

@SneakyThrows
public static void main(String[] args) {
File folder = new File(folderPath);

// 遍历文件夹中的所有.md文件
// hutool 不能排除文件夹,性能低下
// List<File> files = FileUtil.loopFiles(folder, file -> {
// String filename = file.getName().toLowerCase();
// if (excludedDirs.contains(filename)) {
// return false;
// }
// return filename.endsWith(".md");
// });
List<File> files = getMarkdownFiles(folderPath);
if (files != null) {
for (File file : files) {
log.info("Processing file: {}", file.getAbsolutePath());
try {
updateFile(file);
} catch (IOException e) {
log.info("Error updating file: {}", file.getAbsolutePath(), e);
}
}
} else {
log.warn("No Markdown files found in directory: {}", folderPath);
}
}

/**
* 获取指定目录下所有的 .md 文件,并过滤掉特定的目录
*
* @param rootDir 根目录
* @return .md 文件的文件列表
* @throws IOException 文件读取异常
*/
private static List<File> getMarkdownFiles(String rootDir) throws IOException {

try (Stream<Path> fileStream = Files.walk(Paths.get(rootDir), FileVisitOption.FOLLOW_LINKS)) {
return fileStream
// 过滤掉目录
.filter(path -> !isInExcludedDirectory(path, excludedDirs))
// 只选择以 .md 结尾的文件
.filter(path -> Files.isRegularFile(path) && path.toString().toLowerCase().endsWith(".md"))
// 转换为 File 对象并收集结果
.map(Path::toFile)
.collect(Collectors.toList());
}
}

/**
* 判断文件是否位于需要排除的目录中
*
* @param path 文件路径
* @param excludedDirs 排除的目录名列表
* @return 如果文件在排除的目录中,则返回 true,否则返回 false
*/
private static boolean isInExcludedDirectory(Path path, List<String> excludedDirs) {
for (Path part : path) {
if (excludedDirs.contains(part.toString())) {
return true;
}
}
return false;
}

private static void updateFile(File file) throws IOException {
// 获取文件创建时间和修改时间
BasicFileAttributes attrs = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
String creationDateStr = DateUtil.formatDateTime(new Date(attrs.creationTime().toMillis()));
String modifiedDateStr = DateUtil.formatDateTime(new Date(attrs.lastModifiedTime().toMillis()));

// 读取文件内容
List<String> lines = Files.readAllLines(Paths.get(file.getAbsolutePath()));
log.info("Reading file: {},file Lines: {}", file.getAbsolutePath(), lines.size());

boolean containsHeader = containsHeader(lines);
Map<String, Object> existingModel = new HashMap<>();

// 如果文件包含头部,我们读取已有的头部变量信息
if (containsHeader) {
log.info("File contains header, updating header: {}", file.getAbsolutePath());
existingModel = extractExistingHeader(lines);
}

// 获取文件名和父类文件夹名称
String fileName = file.getName();
String filePrefix = StrUtil.subBefore(fileName, ".", true);
List<String> categories = getParentDirectories(file, folderPath);

// 准备FreeMarker数据模型
Map<String, Object> model = new HashMap<>();
model.put("title", filePrefix);
if (categories.isEmpty()) {
model.put("tags", "otherTag");
} else {
model.put("tags", categories.get(categories.size() - 1));
}
model.put("categories", categories);
model.put("comments", true);


// 保留已有的 date 不更新
model.put("date", existingModel.getOrDefault("date", creationDateStr));

model.put("updated", modifiedDateStr);
model.put("description", filePrefix);

if (categories.isEmpty()) {
model.put("keywords", "otherKeywords");
} else {
model.put("keywords", categories.get(categories.size() - 1));
}

// 加载模板
Template template = cfg.getTemplate("template.ftl");

// 生成模板内容
StringWriter stringWriter = new StringWriter();
try {
template.process(model, stringWriter);
} catch (TemplateException e) {
log.error("Error processing FreeMarker template", e);
return;
}

String templateContent = stringWriter.toString();

// 更新模板后,将新内容覆盖到文件头部
// 去掉原有的头部部分
List<String> appendList;
if (containsHeader) {
appendList = removeExistingHeader(lines);
} else {
appendList = lines;
}
String content = templateContent + String.join("\n", appendList);
FileUtil.writeString(content, file, "UTF-8");

log.info("Updated file: {}", file.getAbsolutePath());
}

// 提取文件头部的变量
private static Map<String, Object> extractExistingHeader(List<String> lines) {
Map<String, Object> existingModel = new HashMap<>();
boolean inHeader = false;
String currentKey = null;
List<String> currentList = null;

for (String line : lines) {
line = line.trim();

if ("---".equals(line)) {
if (inHeader) {
// 第二个 "---",退出头部解析
if (currentKey != null) {
if (currentList != null) {
existingModel.put(currentKey, currentList);
} else if (line.isEmpty()) {
existingModel.put(currentKey, "");
}
}
break;
}
inHeader = true; // 第一个 "---",开始解析头部
continue;
}

if (inHeader) {
if (line.contains(":")) {
// 处理之前的键
if (currentKey != null) {
if (currentList != null) {
existingModel.put(currentKey, currentList);
} else if (!line.isEmpty()) {
existingModel.put(currentKey, line);
}
}

// 解析新的键值对
String[] parts = line.split(":", 2);
if (parts.length == 2) {
currentKey = parts[0].trim();
String value = parts[1].trim();

if ("comments".equals(currentKey)) {
existingModel.put(currentKey, "true".equalsIgnoreCase(value));
currentKey = null;
} else if ("tags".equals(currentKey) || "categories".equals(currentKey)) {
currentList = new ArrayList<>();
} else {
existingModel.put(currentKey, value);
currentKey = null;
}
}
} else if (currentList != null) {
// 处理列表项
if (line.startsWith("-")) {
currentList.add(line.substring(1).trim());
}
}
}
}

// 处理最后一个键
if (currentKey != null) {
if (currentList != null) {
existingModel.put(currentKey, currentList);
} else {
existingModel.put(currentKey, "");
}
}

return existingModel;
}

// 去掉文件中的现有头部(从第一个到第二个 "---" 之间的内容)
private static List<String> removeExistingHeader(List<String> lines) {
boolean inHeader = false;
int headerEndIndex = -1;

// 只删除头部的内容,保留其他的内容,尤其是正文中的 `---`
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i).trim();

// 头部标记开始
if ("---".equals(line)) {
if (inHeader) {
// 头部结束,记录头部的结束行
headerEndIndex = i;
break;
}
// 头部开始
inHeader = true;
}
}

// 如果找到头部结束符,截取头部后面的内容,避免影响正文中的 `---`
if (headerEndIndex != -1) {
List<String> strings = lines.subList(headerEndIndex + 1, lines.size());
return strings;
}
// 如果没有头部,直接返回原内容
return lines;

}

// 获取文件父目录的名称集合
private static List<String> getParentDirectories(File file, String excludePath) {
List<String> directories = new ArrayList<>();
File parent = file.getParentFile();
File excludeFile = new File(excludePath);

while (parent != null && !parent.equals(excludeFile)) {
// 从最顶级目录开始添加
directories.add(0, parent.getName());
parent = parent.getParentFile();
}

return directories;
}

private static boolean containsHeader(List<String> lines) {
// 只检查前面3行
int checkLimit = Math.min(lines.size(), 3);
for (int i = 0; i < checkLimit; i++) {
String line = lines.get(i).trim();
if (line.isEmpty()) {
continue;
}
if ("---".equals(line)) {
return true;
}
}
return false;
}
}