Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
"start": "NODE_ENV=production node bin/www",
"dev": "NODE_ENV=development ./node_modules/.bin/nodemon bin/www",
"prd": "pm2 start bin/www",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"md2json": "node ./utils/transforMd2json.js"
},
"dependencies": {
"@koa/cors": "^3.1.0",
"commonmark": "^0.29.3",
"debug": "^4.1.1",
"koa": "^2.7.0",
"koa-bodyparser": "^4.2.1",
Expand Down
56 changes: 4 additions & 52 deletions routes/dailyProblem.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const router = require("koa-router")();
const solutions = require("../static/solution/solutions.json");
const problems = require("../static/problem/problem.json");
const { decrypt } = require("../utils/crypto");

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

const date = getDay(ctx.query.date || new Date().getTime()); // 用户指定的实际
if (date === 2) {
ctx.body = success({
day: 2,
title: "821. 字符的最短距离",
link: "https://leetcode-cn.com/problems/plus-one",
tags: ["基础篇", "数组"], // 目前所有 README 都是没有的。因此如果没有的话,你可以先不返回,有的话就返回。后面我慢慢补
pres: ["数组的遍历(正向遍历和反向遍历)"],
description: `
给定一个字符串 S 和一个字符 C。返回一个代表字符串 S 中每个字符到字符串 S 中的字符 C 的最短距离的数组。

示例 1:

输入: S = "loveleetcode", C = 'e'
输出: [3, 2, 1, 0, 1, 0, 0, 1, 2, 2, 1, 0]
说明:

- 字符串 S 的长度范围为 [1, 10000]。
- C 是一个单字符,且保证是字符串 S 里的字符。
- S 和 C 中的所有字母均为小写字母。

`,
});
} else if (date <= 1) {
ctx.body = success({
day: 1,
title: "66. 加一",
whys: [
"1. 由于是大家第一次打卡,因此出一个简单题。虽然是简单题,但是如果将加 1 改为加任意的数字,那么就变成了一个非常常见的面试题",
],
link: "https://leetcode-cn.com/problems/plus-one",
tags: ["基础篇", "数组"], // 目前所有 README 都是没有的。因此如果没有的话,你可以先不返回,有的话就返回。后面我慢慢补
pres: ["数组的遍历(正向遍历和反向遍历)"],
description: `
给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。

最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。

你可以假设除了整数 0 之外,这个整数不会以零开头。

示例 1:

输入: [1,2,3]
输出: [1,2,4]
解释: 输入数组表示数字 123。
示例 2:

输入: [4,3,2,1]
输出: [4,3,2,2]
解释: 输入数组表示数字 4321。
`,
});
const day = getDay(ctx.query.date || new Date().getTime()); // 用户指定的实际
if (day in problems) {
ctx.body = success(problems[day]);
} else {
ctx.body = fail({
message: "当前暂时没有每日一题,请联系当前讲师进行处理~",
Expand Down
192 changes: 192 additions & 0 deletions utils/transforMd2json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
const path = require("path")
const fs = require('fs')
const commonmark = require('commonmark');
// md文件存放的路径
const inputDirPath = path.resolve(__dirname, '../static/md')

// json文件输出的路径
const outputDirPath = path.resolve(__dirname, '../static')
const problemJson = {};
const solutionJson = {};
const { encrypt } = require("./crypto.js");

// 需要放到题目描述里的标题
// 因为部分题解不规范,标题名不能使用完全等于而应该使用include
const problemTitleDimWordArr = ['入选', '地址', '描述', '前置', '公司']
// 最终生成的题解的key,与上面的模糊匹配的词一一对应
const problemTitleWordMap = ['why', 'link', 'description', 'pres', 'company']


// 递归读取某一目录下的所有md文件
function recursionAllMdFile (dir, cb) {
const files = fs.readdirSync(dir);
files.forEach((fileName) => {
var fullPath = path.join(dir, fileName);
const childFile = fs.statSync(fullPath);
if (childFile.isDirectory()) {
recursionAllMdFile(path.join(dir, fileName), cb); //递归读取文件
} else {
let fildData = fs.readFileSync(fullPath).toString();
cb(fullPath, fildData)
}
});
}

// 预先对文件内容进行处理
// 1. 根据文件名过滤掉非题解的md
// 2. 去除精选题解
function preprocessFile(fullPath, fileData){
let fileName = path.basename(fullPath).trim().toLowerCase()
if( !/d[0-9]+.*\.md$/.test(fileName) || fileName.includes('selec')){
return
}
transformFileToJSon(fullPath, fileData)
}

// 通过遍历ast节点树找到type为text节点的值
// isDeep 为false在找到第一个文案时就中止
// isDeep 为true在找到下一个heading节点时中止
function getAstNodeVal(walker, isDeep = false) {
if(!walker.current) return null
let result = [],
now = walker.current;
// 如果当前就是head节点那就将指针向后值一下
if(now.type === 'heading') now = walker.next()
do {
if(!now.entering) continue
let { node = {} } = now
if(node.type === 'heading') break
if (['text', 'code_block'].includes(node.type)) {
result.push(node.literal || node.info);
if(isDeep === false) break
}
}while((now = walker.next()))
if(!result.length) return null
return result.length > 1 ? result : result[0]
}

// 当前标题是否属于题目描述的内容, 不属于返回-1, 属于则返回模糊匹配词组中的索引值
function findIndexProblemDimWorld (title) {
// 把括号内的内容删掉
// 避免类似这样的标题: 题目地址(239. xxx)
// 括号内的内容与关键字重复导致误判
title = title.replace(/(\(.*\))/,'')
return problemTitleDimWordArr.findIndex(item => title.includes(item))
}

// 获取文件某行之后的所有内容(包含该行)
function getFileDataAfterLine (fullPath, lineNum) {
try {
const data = fs.readFileSync(fullPath, 'UTF-8');
const lines = data.split(/\r?\n/);
return lines.slice(lineNum - 1)
} catch (err) {
console.error(err);
}
}

// 写入json对象
function writeToJsonObject (fullPath, problemData, soluteContentStartLine){
// 将题目相关的内容写入json
problemData = formateProblemValue(problemData)
problemJson[problemData.day] = problemData
// 将题解相关的内容写入json
let solutionFileData = getFileDataAfterLine(fullPath, soluteContentStartLine)
solutionJson[problemData.day] = solutionFileData
}

// 格式化题目的相关数据
function formateProblemValue (data) {
return Object.assign({
day: 1,
title: "当前暂时没有对应的数据,请联系当前讲师进行处理~",
link: "当前暂时没有对应的数据,请联系当前讲师进行处理~",
// tags: [], // 目前所有 README 都是没有的。因此如果没有的话,你可以先不返回,有的话就返回。后面我慢慢补
pres: ["当前暂时没有对应的数据,请联系当前讲师进行处理~"],
description: "当前暂时没有对应的数据,请联系当前讲师进行处理~",
company: "暂无"
}, data)
}

// 将某个md文件解析为 题解与题目介绍
function transformFileToJSon(fullPath, fileData){
// 根据题解名获取这是第几天的题解和题目title
let fileName = path.basename(fullPath).trim().toLowerCase()
let problemData = {
day: +fileName.match(/d([0-9]+)/)[1],
// title: fileName.split('.').slice(1, -1).join('.')
}
let walker = new commonmark.Parser().parse(fileData.toString()).walker();
let nowNode = walker.next(), nextNode
while (nowNode) {
// 当前讲义的基本格式为标题紧跟着是对应的内容,
// 所以碰到 heading 类型的节点时,因此将ast的节点按heading进行分割
if(nowNode.node.type === 'heading'){
// 这里做下兼容处理,有部分md有一级标题,碰到就直接忽视,当前指针迭代到下一个head
if(nowNode.node.level === 1){
nowNode = walker.next()
continue
}

let key = getAstNodeVal(walker)
// 如果不是题目相关的标题,代表从这一行开始就是题解的内容了
// 结束ast循环,将该行即该行之下的内容全部截取,就是题解的md内容
if(findIndexProblemDimWorld(key) === -1) break
// 如果是 题目地址(821. xxx) 的形式,则在这里取一下括号内的内容做title,没有就显示为空
if(/题目地址.*[\((].*?([0-9]+\..*)[\))]/.test(key)){
problemData.title = key.match(/题目地址.*[\((].*?([0-9]+\..*)[\))]/)[1]
}
key = problemTitleWordMap[findIndexProblemDimWorld(key)]

nextNode = walker.next();
while(walker.entering === false){
nextNode = walker.next();
}
if(!nextNode) break
let nextNodeVal = getAstNodeVal(walker, true)
problemData[key] = nextNodeVal.length > 1 ? nextNodeVal : nextNodeVal[0]
nowNode = nextNode
} else {
nowNode = walker.next()
}
}
// 这一行(包括本行)之下的内容为题解,
let hasSourceNode = walker.current
while(!Array.isArray(hasSourceNode.sourcepos) && hasSourceNode){
hasSourceNode = hasSourceNode.parent
}
let soluteContentStartLine = hasSourceNode ? hasSourceNode.sourcepos[0][0] : 1;
// 将该文件解析出的内容写入json对象
writeToJsonObject(fullPath, problemData, soluteContentStartLine)
// 将该文件解析出的内容写入json文件
// writeFile(fullPath, problemData, soluteContentStartLine)
}

function run(){
recursionAllMdFile(inputDirPath, preprocessFile)
if (!fs.existsSync(outputDirPath)) {
fs.mkdirSync(outputDirPath);
}

// 将题目相关的内容写入json
fs.writeFile(path.resolve(outputDirPath, `problem/problem.json`), JSON.stringify(problemJson, null, 4), function (err) {
if (err) console.log(`problem.json写入失败`, err);
})

// 将题解相关的内容写入json
Object.keys(solutionJson).forEach((key) => {
let content = encrypt(solutionJson[key].join('\n'))
solutionJson[key] = {
content
}
});
fs.writeFile(path.resolve(outputDirPath, `solution/solutions.json`), JSON.stringify(solutionJson, null, 4), function (err) {
if (err) console.log(`加密前的solution.json写入失败`, err);
})
return {
problemJson,
solutionJson
}
}

run()