|
| 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