提取markdown中的目录

一、前言

markdown我就不夸了,非常好用,个人博客用的就是markdown编辑器,这篇文章是正则的一个练习,提取md文件中的所有标题,生成一个目录结构

二、正文

1. 规律

这里基本是废话,只是给没用过markdown的同学普及一下,在markdown中用 # 加上一个空格来表示一级标题,二级标题就用两个 #,其余标题依次类推,根据这个规律就可以完成关键部分的正则表达式,拿我一篇文章做演示吧。

文章内容在数据库中是这样存的(我提前去掉了`符号,防止使用字符串模板冲突)

1
# 1. 变量提升的概念\n\n变量提升简单来说就是把我们所写的类似于var a = 123这样的代码,声明提升到它所在作用域的顶端去执行,到我们代码所在的位置来赋值。\n\n注意一定是所载作用域的顶端, 如果提升的代码位于函数中, 则将变量和函数提升到函数的第一行, 代码块以及其他环境下同理\n\n# 2. 变量的提升\n\n> 使用var 声明的变量会被提前到所有代码执行之前\n\njavascript\nconsole.log(x)\nvar x = 123;\n\n实际上javascript执行代码的顺序应该是这样的\njavascript\nvar x;\nconsole.log(x);\nx = 123;\n// 因此打印的内容是undefined,因为x声明了, 但是没有赋值 \n\n\n# 3. 函数的提升\n\n> **使用函数声明创建的函数会被提前到所有代码执行之前**\n>\n> **但是通过构造方法和表达式方式创建的函数不会被提前**\n\njavascript\nconsole.log(fun);\nfun();\nvar fun = function(){\n    console.log(\"fun\");\n}\n\n\n运行结果:\n\nundefined\n\nUncaught TypeError : fun is not a function \n\n> **解释:** \n>\n> **通过var 声明的变量fun会被提前到所有代码执行之前, 所以在第一行打印fun会打印undefined, 第二行执行fun函数, 由于fun是undefined, 因此报错undefined不是个函数**\n\n通过变量声明创建的函数则不会出现这个问题\n\njavascript\nconsole.log(fun);\nfun();\nfunction fun(){\n}\n\n\n运行结果: \n\nf  fun() {}\n\n\n> **解释:** \n>\n> **由于通过函数声明创建函数, 因此函数被提前, 因此可以直接调用而不会出错**\n# 4. 变量提升和函数提升的顺序\n一个经典例题\njavascript\nfunction test(){\n    var a = 1;\n    function a(){}\n    console.log(a);\n}\ntest();\n\n实际输出结果如下\njavascript\nƒ a(){}\n\n结论:函数提升在变量提升之后\n也就是说javascript会先提升所有的变量, 然后才提升所有的函数, 先将a提升到所在作用域的顶端, 随后又将函数a提升到顶端, a函数也就覆盖了a变量(指针的指向发生了改变)\n# 5. Demo\n## 5.1 变量提升demo\n\njavascript\nconsole.log(v1);\nvar v1 = 100;\nfunction foo() {\n    console.log(v1);\n    var v1 = 200;\n    console.log(v1);\n}\nfoo();\nconsole.log(v1);\n\n// 运行结果:\n// undefined\n// undefined\n// 200\n// 100\n\n\n> **解释:**\n>\n> **作对这道题其实不难, 重要的是理解变量提升的概念,提升到当前作用域的最顶端**\n>\n> **浏览器的解析顺序应该是这样**\n\njavascript\nvar v1;\nconsole.log(v1); //undefined\nv1 = 100;\nfunction foo(){\n    var vl; // 函数作用域中v1于覆盖了全局作用域的v1\n    console.log(vl); //undefined\n    v1 = 200;\n    console.log(v1); // 200\n}\nfoo();\nconsole.log(v1); // 100(第三行赋值过,因此是100)\n\n\n\n\n## 5.2 函数提升的Demo\n\njavascript\nfoo();\n \nvar foo;\n \nfunction foo () {\n    console.log(1);\n}\n \nfoo = function () {\n    console.log(2);\n}\n// 运行结果: 1\n\n\n浏览器的编译过程应该是这样\n\njavascript\nfunction foo(){\n    console.log(1);\n}\nvar foo;\nfoo();\nfoo = function(){\n    console.log(2);\n}\n\n\n**因此输出结果应该是1, 如果在最后一行在调用一遍foo(), 则会再次输出2**\n\n

2. 正则表达式

比较常规,直接上代码

1
/(#{1,6})\s(.*)/g
  • #{1,6} 代表匹配1到6个 #
  • 后面必须匹配一个 \s 也就是markdown语法中规定的空格,而且只能为一个,不满足的就不是标题
  • . 可以匹配除了换行回车之外的所有字符,.* 就代表匹配多个任意字符,这样搭配就可以匹配整个标题(标题后面肯定换行,因此匹配到换行就结束了,因此可以匹配到整个标题,而不会匹配多余内容)

    3. 替换

上面的正则添加了两个分组,分别捕获了前面有几个 # 以及后面标题的内容,用于后面的替换。
在替换的时候只需要判断有几个 # 号,一个代表一级标题,就用 <h1></h1> 替换掉 # ,其余依次类推,html标签中间的内容就是我们的第二个捕获组中的内容,二者拼接就构成了想要的html标签了
代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
// 匹配所有标题的正则
let reg = /(#{1,6})\s(.*)/g;
// 匹配到的所有标题
let titleArr = str.match(reg);
// 将匹配结果中的#替换成对应的html标签
let replacedTitleArr = titleArr.map(item => {
return item.replace(reg, (match, p0, p1) => {
return `<h${p0.length}>${p1}</${p0.length}>`
})
})
// 通过map返回一个html标签的数组,通过join连接
document.querySelector(".nav").innerHTML = replacedTitleArr.join("")

因为所有函数的回调函数都是一行,因此可以通过一行代码完成这个功能

1
document.querySelector(".nav").innerHTML = str.match(/(#{1,6})\s(.*)/g).map(item => item.replace(/(#{1,6})\s(.*)/g, (match, p0, p1) => `<h${p0.length}>${p1}</${p0.length}>`)).join("");

html部分

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Title</title>
</head>
<body>
<div class="nav"></div>
</body>
</html>

三、总结

  • 正则表达式是程序员的基本素质
  • 通过学习正则才发现以前写的很多烂代码可以通过正则优化,不只是性能上的提升,更多的是代码的可维护性和可读性