Skip to content

Commit 7128a99

Browse files
committed
feat: 新增md转json脚本
1 parent 949e6b5 commit 7128a99

File tree

3 files changed

+199
-53
lines changed

3 files changed

+199
-53
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
"start": "NODE_ENV=production node bin/www",
77
"dev": "NODE_ENV=development ./node_modules/.bin/nodemon bin/www",
88
"prd": "pm2 start bin/www",
9-
"test": "echo \"Error: no test specified\" && exit 1"
9+
"test": "echo \"Error: no test specified\" && exit 1",
10+
"md2json": "node ./utils/transforMd2json.js"
1011
},
1112
"dependencies": {
1213
"@koa/cors": "^3.1.0",
14+
"commonmark": "^0.29.3",
1315
"debug": "^4.1.1",
1416
"koa": "^2.7.0",
1517
"koa-bodyparser": "^4.2.1",

routes/dailyProblem.js

Lines changed: 4 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const router = require("koa-router")();
22
const solutions = require("../static/solution/solutions.json");
3+
const problems = require("../static/problem/problem.json");
34
const { decrypt } = require("../utils/crypto");
45

56
const { success, fail } = require("../utils/request");
@@ -17,58 +18,9 @@ router.get("/api/v1/daily-problem", async (ctx) => {
1718
// 3. 根据 Day 几 计算出具体返回哪一个题目
1819
// !!注意: 如果用户指定的时间大于今天,则返回”题目不存在,仅支持查询历史每日一题“
1920

20-
const date = getDay(ctx.query.date || new Date().getTime()); // 用户指定的实际
21-
if (date === 2) {
22-
ctx.body = success({
23-
day: 2,
24-
title: "821. 字符的最短距离",
25-
link: "https://leetcode-cn.com/problems/plus-one",
26-
tags: ["基础篇", "数组"], // 目前所有 README 都是没有的。因此如果没有的话,你可以先不返回,有的话就返回。后面我慢慢补
27-
pres: ["数组的遍历(正向遍历和反向遍历)"],
28-
description: `
29-
给定一个字符串 S 和一个字符 C。返回一个代表字符串 S 中每个字符到字符串 S 中的字符 C 的最短距离的数组。
30-
31-
示例 1:
32-
33-
输入: S = "loveleetcode", C = 'e'
34-
输出: [3, 2, 1, 0, 1, 0, 0, 1, 2, 2, 1, 0]
35-
说明:
36-
37-
- 字符串 S 的长度范围为 [1, 10000]。
38-
- C 是一个单字符,且保证是字符串 S 里的字符。
39-
- S 和 C 中的所有字母均为小写字母。
40-
41-
`,
42-
});
43-
} else if (date <= 1) {
44-
ctx.body = success({
45-
day: 1,
46-
title: "66. 加一",
47-
whys: [
48-
"1. 由于是大家第一次打卡,因此出一个简单题。虽然是简单题,但是如果将加 1 改为加任意的数字,那么就变成了一个非常常见的面试题",
49-
],
50-
link: "https://leetcode-cn.com/problems/plus-one",
51-
tags: ["基础篇", "数组"], // 目前所有 README 都是没有的。因此如果没有的话,你可以先不返回,有的话就返回。后面我慢慢补
52-
pres: ["数组的遍历(正向遍历和反向遍历)"],
53-
description: `
54-
给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。
55-
56-
最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。
57-
58-
你可以假设除了整数 0 之外,这个整数不会以零开头。
59-
60-
示例 1:
61-
62-
输入: [1,2,3]
63-
输出: [1,2,4]
64-
解释: 输入数组表示数字 123。
65-
示例 2:
66-
67-
输入: [4,3,2,1]
68-
输出: [4,3,2,2]
69-
解释: 输入数组表示数字 4321。
70-
`,
71-
});
21+
const day = getDay(ctx.query.date || new Date().getTime()); // 用户指定的实际
22+
if (day in problems) {
23+
ctx.body = success(problems[day]);
7224
} else {
7325
ctx.body = fail({
7426
message: "当前暂时没有每日一题,请联系当前讲师进行处理~",

utils/transforMd2json.js

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
const path = require("path")
2+
const fs = require('fs')
3+
const commonmark = require('commonmark');
4+
// md文件存放的路径
5+
const inputDirPath = path.resolve(__dirname, '../static/md')
6+
7+
// json文件输出的路径
8+
const outputDirPath = path.resolve(__dirname, '../static')
9+
const problemJson = {};
10+
const solutionJson = {};
11+
const { encrypt } = require("./crypto.js");
12+
13+
// 需要放到题目描述里的标题
14+
// 因为部分题解不规范,标题名不能使用完全等于而应该使用include
15+
const problemTitleDimWordArr = ['入选', '地址', '描述', '前置', '公司']
16+
// 最终生成的题解的key,与上面的模糊匹配的词一一对应
17+
const problemTitleWordMap = ['why', 'link', 'description', 'pres', 'company']
18+
19+
20+
// 递归读取某一目录下的所有md文件
21+
function recursionAllMdFile (dir, cb) {
22+
const files = fs.readdirSync(dir);
23+
files.forEach((fileName) => {
24+
var fullPath = path.join(dir, fileName);
25+
const childFile = fs.statSync(fullPath);
26+
if (childFile.isDirectory()) {
27+
recursionAllMdFile(path.join(dir, fileName), cb); //递归读取文件
28+
} else {
29+
let fildData = fs.readFileSync(fullPath).toString();
30+
cb(fullPath, fildData)
31+
}
32+
});
33+
}
34+
35+
// 预先对文件内容进行处理
36+
// 1. 根据文件名过滤掉非题解的md
37+
// 2. 去除精选题解
38+
function preprocessFile(fullPath, fileData){
39+
let fileName = path.basename(fullPath).trim().toLowerCase()
40+
if( !/d[0-9]+.*\.md$/.test(fileName) || fileName.includes('selec')){
41+
return
42+
}
43+
transformFileToJSon(fullPath, fileData)
44+
}
45+
46+
// 通过遍历ast节点树找到type为text节点的值
47+
// isDeep 为false在找到第一个文案时就中止
48+
// isDeep 为true在找到下一个heading节点时中止
49+
function getAstNodeVal(walker, isDeep = false) {
50+
if(!walker.current) return null
51+
let result = [],
52+
now = walker.current;
53+
// 如果当前就是head节点那就将指针向后值一下
54+
if(now.type === 'heading') now = walker.next()
55+
do {
56+
if(!now.entering) continue
57+
let { node = {} } = now
58+
if(node.type === 'heading') break
59+
if (['text', 'code_block'].includes(node.type)) {
60+
result.push(node.literal || node.info);
61+
if(isDeep === false) break
62+
}
63+
}while((now = walker.next()))
64+
if(!result.length) return null
65+
return result.length > 1 ? result : result[0]
66+
}
67+
68+
// 当前标题是否属于题目描述的内容, 不属于返回-1, 属于则返回模糊匹配词组中的索引值
69+
function findIndexProblemDimWorld (title) {
70+
// 把括号内的内容删掉
71+
// 避免类似这样的标题: 题目地址(239. xxx)
72+
// 括号内的内容与关键字重复导致误判
73+
title = title.replace(/(\(.*\))/,'')
74+
return problemTitleDimWordArr.findIndex(item => title.includes(item))
75+
}
76+
77+
// 获取文件某行之后的所有内容(包含该行)
78+
function getFileDataAfterLine (fullPath, lineNum) {
79+
try {
80+
const data = fs.readFileSync(fullPath, 'UTF-8');
81+
const lines = data.split(/\r?\n/);
82+
return lines.slice(lineNum - 1)
83+
} catch (err) {
84+
console.error(err);
85+
}
86+
}
87+
88+
// 写入json对象
89+
function writeToJsonObject (fullPath, problemData, soluteContentStartLine){
90+
// 将题目相关的内容写入json
91+
problemData = formateProblemValue(problemData)
92+
problemJson[problemData.day] = problemData
93+
// 将题解相关的内容写入json
94+
let solutionFileData = getFileDataAfterLine(fullPath, soluteContentStartLine)
95+
solutionJson[problemData.day] = solutionFileData
96+
}
97+
98+
// 格式化题目的相关数据
99+
function formateProblemValue (data) {
100+
return Object.assign({
101+
day: 1,
102+
title: "当前暂时没有对应的数据,请联系当前讲师进行处理~",
103+
link: "当前暂时没有对应的数据,请联系当前讲师进行处理~",
104+
// tags: [], // 目前所有 README 都是没有的。因此如果没有的话,你可以先不返回,有的话就返回。后面我慢慢补
105+
pres: ["当前暂时没有对应的数据,请联系当前讲师进行处理~"],
106+
description: "当前暂时没有对应的数据,请联系当前讲师进行处理~",
107+
company: "暂无"
108+
}, data)
109+
}
110+
111+
// 将某个md文件解析为 题解与题目介绍
112+
function transformFileToJSon(fullPath, fileData){
113+
// 根据题解名获取这是第几天的题解和题目title
114+
let fileName = path.basename(fullPath).trim().toLowerCase()
115+
let problemData = {
116+
day: +fileName.match(/d([0-9]+)/)[1],
117+
// title: fileName.split('.').slice(1, -1).join('.')
118+
}
119+
let walker = new commonmark.Parser().parse(fileData.toString()).walker();
120+
let nowNode = walker.next(), nextNode
121+
while (nowNode) {
122+
// 当前讲义的基本格式为标题紧跟着是对应的内容,
123+
// 所以碰到 heading 类型的节点时,因此将ast的节点按heading进行分割
124+
if(nowNode.node.type === 'heading'){
125+
// 这里做下兼容处理,有部分md有一级标题,碰到就直接忽视,当前指针迭代到下一个head
126+
if(nowNode.node.level === 1){
127+
nowNode = walker.next()
128+
continue
129+
}
130+
131+
let key = getAstNodeVal(walker)
132+
// 如果不是题目相关的标题,代表从这一行开始就是题解的内容了
133+
// 结束ast循环,将该行即该行之下的内容全部截取,就是题解的md内容
134+
if(findIndexProblemDimWorld(key) === -1) break
135+
// 如果是 题目地址(821. xxx) 的形式,则在这里取一下括号内的内容做title,没有就显示为空
136+
if(/.*[\(].*?([0-9]+\..*)[\)]/.test(key)){
137+
problemData.title = key.match(/.*[\(].*?([0-9]+\..*)[\)]/)[1]
138+
}
139+
key = problemTitleWordMap[findIndexProblemDimWorld(key)]
140+
141+
nextNode = walker.next();
142+
while(walker.entering === false){
143+
nextNode = walker.next();
144+
}
145+
if(!nextNode) break
146+
let nextNodeVal = getAstNodeVal(walker, true)
147+
problemData[key] = nextNodeVal.length > 1 ? nextNodeVal : nextNodeVal[0]
148+
nowNode = nextNode
149+
} else {
150+
nowNode = walker.next()
151+
}
152+
}
153+
// 这一行(包括本行)之下的内容为题解,
154+
let hasSourceNode = walker.current
155+
while(!Array.isArray(hasSourceNode.sourcepos) && hasSourceNode){
156+
hasSourceNode = hasSourceNode.parent
157+
}
158+
let soluteContentStartLine = hasSourceNode ? hasSourceNode.sourcepos[0][0] : 1;
159+
// 将该文件解析出的内容写入json对象
160+
writeToJsonObject(fullPath, problemData, soluteContentStartLine)
161+
// 将该文件解析出的内容写入json文件
162+
// writeFile(fullPath, problemData, soluteContentStartLine)
163+
}
164+
165+
function run(){
166+
recursionAllMdFile(inputDirPath, preprocessFile)
167+
if (!fs.existsSync(outputDirPath)) {
168+
fs.mkdirSync(outputDirPath);
169+
}
170+
171+
// 将题目相关的内容写入json
172+
fs.writeFile(path.resolve(outputDirPath, `problem/problem.json`), JSON.stringify(problemJson, null, 4), function (err) {
173+
if (err) console.log(`problem.json写入失败`, err);
174+
})
175+
176+
// 将题解相关的内容写入json
177+
Object.keys(solutionJson).forEach((key) => {
178+
let content = encrypt(solutionJson[key].join('\n'))
179+
solutionJson[key] = {
180+
content
181+
}
182+
});
183+
fs.writeFile(path.resolve(outputDirPath, `solution/solutions.json`), JSON.stringify(solutionJson, null, 4), function (err) {
184+
if (err) console.log(`加密前的solution.json写入失败`, err);
185+
})
186+
return {
187+
problemJson,
188+
solutionJson
189+
}
190+
}
191+
192+
run()

0 commit comments

Comments
 (0)