每日一题:Markdown 文档解析

介绍

Markdown 因为其简洁的语法大受欢迎,已经成为大家写博客或文档时必备的技能点,众多博客平台都提倡用户使用 Markdown 语法进行文章书写,然后再发布后,实时的将其转化为常规的 HTML 页面渲染。

本题需要在已提供的基础项目中,使用 Nodejs 实现简易的 Markdown 文档解析器。

准备

开始答题前,需要先打开本题的项目代码文件夹,目录结构如下:

1
2
3
4
5
6
7
├── docs.md
├── images
│ └── md.jpg
├── index.html
└── js
├── index.js
└── parse.js

其中:

  • index.html 是主页面。
  • images 是图片文件夹。
  • docs.md 是需要解析的 Markdown 文件。
  • js/index.js 是提供的工具脚本,用于快速验证代码结果。
  • js/parse.js 是需要补充的脚本文件。

注意:打开环境后发现缺少项目代码,请手动键入下述命令进行下载:

1
2
cd /home/project
wget https://labfile.oss.aliyuncs.com/courses/18213/07.zip && unzip 07.zip && rm 07.zip

目标

js/parse.js 中实现几种特定的 Markdown 语法解析,目前初始文件中已实现标题解析(即从 # 前缀转换为 <hn> 标签),请你继续完善该文件 TODO 部分,完成剩余语法解析操作,具体需求如下:

  1. 对分隔符进行解析,Markdown 中使用 --- (三条及以上的短横线) 作为分隔符,将其解析成为 <hr> 标签:
1
2
3
4
5
<!-- Markdown -->
----

<!-- 对应 HTML -->
<hr>
  1. 对引用区块进行解析,Markdown 中使用 > 作为前缀,将其解析成为 <blockquote> 标签:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- Markdown  -->
> 引用区块1

> 多级引用区块2
> 多级引用区块2

<!-- 对应 HTML -->
<blockquote>
<p>引用区块1</p>
</blockquote>

<blockquote>
<p>多级引用区块2</p>
<p>多级引用区块2</p>
</blockquote>
  1. 对无序列表进行解析,Markdown 中使用 * 或者 - 作为前缀,将其解析成为 <ul> 标签:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- Markdown  -->
* 无序列表
* 无序列表
* 无序列表

或者:
- 无序列表
- 无序列表
- 无序列表

<!-- 对应 HTML -->
<ul>
<li>无序列表</li>
<li>无序列表</li>
<li>无序列表</li>
</ul>
  1. 对图片进行解析,Markdown 中使用 ![alt](link) 表示,将其解析成为 <img> 标签:
1
2
3
4
5
<!-- Markdown  -->
![图片](./images/md.jpg)

<!-- 对应 HTML -->
<img src="./images/md.jpg" alt="图片">
  1. 对文字效果进行解析,比如粗体效果,和行内代码块,将其分别解析成 <b>code 标签:
1
2
3
4
5
<!-- Markdown  -->
这是**粗体**的效果文字,这是内嵌的`代码行`

<!-- 对应 HTML -->
这是<b>粗体</b>的效果文字,这是内嵌的<code>代码行</code>

在验证代码效果时,你可以在终端运行:

1
node ./js/index.js

程序会将解析的结果输出到 index.html 文件中,然后通过浏览器查看输出的 index.html 是否符合解析要求(注意:程序不会实时的将结果更新到 index.html 文件中,在你的代码变更后,请重新执行上述命令)。

在题目所提供的数据的情况下,完成后的效果如下:

完成效果

规定

  • 请勿修改 js/parse.js 文件外的任何内容。
  • 请严格按照考试步骤操作,切勿修改考试默认提供项目中的文件名称、文件夹路径、class 名、id 名、图片名等,以免造成无法判题通过。

判分标准

  • 完成对分隔符的解析,得 5 分。
  • 完成对引用区块的解析,得 5 分。
  • 完成对图片,和文字效果的解析,得 5 分。
  • 完成对无序列表的解析,得 10 分。

总通过次数: 50 | 总提交次数: 124 | 通过率: 40.3%

难度: 中等 标签: 蓝桥杯真题, 2023, 省赛, Web 前端, Node.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
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
class Parser {
constructor() {
this.heading = /^(#{1,6}\s+)/;
this.blockQuote = /^(\>\s+)/;
this.unorderedList = /^((\*|-){1}\s+)/;
this.image = /\!\[(.*?)\]\((.*?)\)/g;
this.strongText = /\*{2}(.*?)\*{2}/g;
this.codeLine = /\`{1}(.*?)\`{1}/g;
// TODO: 补充分割符正则
this.hr = /^\-{3,}/;
}

// 获取单行内容
parseLineText(lineText) {
this.lineText = lineText;
}

// 是否是空行
isEmptyLine() {
return this.lineText === "";
}

// 是否为符合标题规范
isHeading() {
return this.heading.test(this.lineText);
}

isHr() {
return this.hr.test(this.lineText)
}

isBlockQuote() {
return this.blockQuote.test(this.lineText)
}

isList() {
return this.unorderedList.test(this.lineText)
}

isImage() {
return this.image.test(this.lineText)
}

isNomalLine() {
return true
}

isStrong() {
return this.strongText.test(this.lineText)
}

isCode() {
return this.codeLine.test(this.lineText)
}

// 解析标题
parseHeading() {
const temp = this.lineText.split(" ");
const headingLevel = temp[0].length;
const title = temp[1].trim();
return `<h${headingLevel}>${title}</h${headingLevel}>`;
}

parseHr() {
return `<hr>`
}

parseBlockQuote() {
const temp = this.lineText.split(" ");
const content = temp[1].trim();
return `<p>${content}</p>`;
}

parseList() {
const temp = this.lineText.split(" ");
const content = temp[1].trim();
return `<li>${content}</li>`;
}

parseImage() {
const myReg = /^\!\[(\S+)\]\((\S+)\)/
const str = this.lineText
myReg.test(str)
return `<img src="${RegExp.$2}" alt="${RegExp.$1}">`
}

parseStrong() {
return this.lineText.replace(this.strongText, `<b>${RegExp.$1}</b>`)
}

parseCodeLine() {
return this.lineText.replace(this.codeLine, `<code>${RegExp.$1}</code>`)
}

/**
* TODO: 请完成剩余各种语法的解析
* 1. 完成对分隔符的解析
* 2. 完成对引用区块的解析
* 3. 完成对图片,和文字效果的解析
* 4. 完成对无序列表的解析
*/
}

class Reader {
constructor(text) {
//获取全部原始文本
this.text = text;
this.lines = this.getLines();
this.parser = new Parser();
}

runParser() {
let currentLine = 0;
let hasParsed = [];
while (!this.reachToEndLine(currentLine)) {
// 获取行文本
this.parser.parseLineText(this.getLineText(currentLine));

// 判断空白行
if (this.parser.isEmptyLine()) {
currentLine++;
continue;
}

if (this.parser.isHeading()) {
hasParsed.push(this.parser.parseHeading());
currentLine++;
continue;
}
// TODO: 请完成剩余各种语法的解析

if (this.parser.isHr()) {
hasParsed.push(this.parser.parseHr());
currentLine++;
continue;
}

if (this.parser.isBlockQuote()) {
const preLine = this.getLineText(currentLine - 1);
const nextLine = this.getLineText(currentLine + 1);
if (!this.parser.blockQuote.test(preLine)) {
hasParsed.push(`<blockquote>`)
}
hasParsed.push(this.parser.parseBlockQuote())
currentLine++
if (!this.parser.blockQuote.test(nextLine)) {
hasParsed.push(`</blockquote>`)
}
continue
}

if (this.parser.isList()) {
const preLine = this.getLineText(currentLine - 1);
const nextLine = this.getLineText(currentLine + 1);
if (!this.parser.unorderedList.test(preLine)) {
hasParsed.push(`<ul>`)
}
hasParsed.push(this.parser.parseList())
currentLine++
if (!this.parser.unorderedList.test(nextLine)) {
hasParsed.push(`</ul>`)
}
continue
}

if (this.parser.isImage()) {
hasParsed.push(this.parser.parseImage());
currentLine++;
continue;
}

if (this.parser.isNomalLine()) {
let lineText = this.getLineText(currentLine)
if (this.parser.isStrong()) {
lineText = lineText.replace(this.parser.strongText, '<b>$1</b>')
}
if (this.parser.isCode()) {
lineText = lineText.replace(this.parser.codeLine, '<code>$1</code>')
}
hasParsed.push(lineText)
currentLine++
continue
}

currentLine++;
}
return hasParsed.join("");
}

getLineText(lineNum) {
return this.lines[lineNum];
}

getLines() {
this.lines = this.text.split("\n");
return this.lines;
}

reachToEndLine(line) {
return line >= this.lines.length;
}
}

module.exports = function parseMarkdown(markdownContent) {
return new Reader(markdownContent).runParser();
};