MarkDown文本解析器

Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档,然后转换成有效的XHTML(或者HTML)文档
项目简介:
这是一个对 markdown文件进行解析,转化为HTML文档的的程序。通过词法分析,构建文档的语法树,然后听过对语法树的深度优先遍历生成html文档。
应用技术:

  • 词法分析
  • 构建语法树
  • 深度优先遍历

在进行词法分析前,先了解一下markdown基础语法,以及与html标签的对应关系
基础语法:
在这里插入图片描述
在这里插入图片描述
标签对应关系:
在这里插入图片描述
下面就开始对MarkDown文档进行分析
通过对语法的了解发现,多数语法标识都是出现在行首,所以我在进行解析时,每次读取一行内容,并对行首进行分析,判断语法内容,有部分语法是可以出现在行内的,所以要做单独处理,这种后面再对它们进行单独处理。

在进行解析时既需要保存语法类型,又需要保存实际内容,如果出现嵌套的语法结构也要妥善保存起来,那么常见的容器无法直接满足需求,所以这里自定义一个Node结构体,来满足数据存储的需求。
在这里插入图片描述
定义一个markdown类,类中包含一个Node类型成员,来作为语法树的根节点。所有的基本语法类型都作为他的孩子结点进行保存。
在这里插入图片描述
下面就开始正式的语法类型判断,这里有一个细节,要把行首的空格去掉,因为他是和语法无关的,然后判断最简单的语法,即不需要包含内容的,如:空行,分割线。然后再对其他的另行判断。
在这里插入图片描述
这里要注意一个点,如果此行内容是在一个代码块结构中,那么不论是空行还是分割线,还是其他类型语法,都不做解析,直接存入代码块语法结点中。
代码块标识奇数次出现和偶数次出现要对代码块标识进行修改。以满足正常功能。
在行首语法中还有一个特殊的地方,无序列表和有序列表在HTML中不仅仅是个单行语法,在使用时,要先创建一个列表(<ul> / <ol>)然后列表的每一行内容则存储在单独的<li>中,所以在分析列表的时候也要对奇偶次进行标识,奇数次时,需要先创建一个列表结点,再将行内容以<li>结点存储,并作为其孩子结点。
如:在这里插入图片描述
在分析完行首语法后,还有一些特殊的语法,他们不一定出现在行首,所以对这部分进行单独处理,前面已经处理完空行,分割线,代码块,标题,无序列表,有序列表,引用节点,剩下的就都归类为普通段落,在插入内容时,再进行语法分析。(部分已经分析出行首语法类型的,插入内容时也要进行这一步,因为可能存在嵌套的语法结构。如:标题是斜体,那么在标题结点中就要嵌套一个斜体结点,在这个结点内才真正存放标题的内容)

由于下面的语法可能出现在行内任意位置,所以在处理的时候要对每一个字符进行分析,所以在下面处理的时候,每次分析一个字符,并进行存储。

如:

void markdown::insert(Node* child, const char* str)
	{
    
    
		//标记是否行内代码
		bool incode = false;
		//标记是否为粗体内
		bool instrong = false;
		//标记是否为斜体
		bool inem = false;
		//标记是否为删除线
		bool indline = false;
		//创建一个纯文本结点
		child->_child.push_back(new Node(nul));
		int size = strlen(str);
		//一次处理一个字符
		for (int i = 0; i < size; i++)
		{
    
    
			//行内代码
			if (str[i] == '`')
			{
    
    
				if (incode)
				{
    
    
					//如果当前是行内代码结束位置,创建一个新节点,存后面的内容
					child->_child.push_back(new Node(nul));
				}
				else
				{
    
    
					child->_child.push_back(new Node(code));
				}
				incode = !incode;
				continue;
			}

			//粗体
			if (str[i] == '*' && i + 1 < size && str[i + 1] == '*' && !incode)
			{
    
    
				if (instrong)
				{
    
    
					child->_child.push_back(new Node(nul));
				}
				else
				{
    
    
					child->_child.push_back(new Node(strong));
				}
				instrong = !instrong;
				++i;
				continue;
			}

			//斜体
			if (str[i] == '_' && !incode)
			{
    
    
				if (inem)
				{
    
    
					child->_child.push_back(new Node(nul));
				}
				else
				{
    
    
					child->_child.push_back(new Node(em));
				}
				inem = !inem;
				continue;
			}
			//普通文本
			child->_child.back()->elem[0] += str[i];

如果是链接或者文本要进行单独处理,因为HTML中链接的格式是<a href="url">链接文本</a> 图片的格式是 <img src="url" alt="图片无法加载时的替换文本"> ,如果把文本和地址存储在一起,在中间加入HTML的语法时会很麻烦,所以用到了前面定义的string elem [2] 的第二个位置,来存放地址。
如:

//图片
			if (str[i] == '!' && i + 1 < size && str[i + 1] == '[')
			{
    
    
				//创建图片节点
				child->_child.push_back(new Node(image));
				i += 2;
				//存放图片名字
				for (; i < size && str[i] != ']'; i++)
				{
    
    
					child->_child.back()->elem[0] += str[i];
				}
				//存放图片地址
				i += 2;
				for (; i < size && str[i] != ')'; i++)
				{
    
    
					child->_child.back()->elem[1] += str[i];
				}
				//创建一个节点,用于存放图片后内容
				child->_child.push_back(new Node(nul));
				continue;
			}

到此解析构语法,建语法树工作就完成了,下面就要遍历语法树,生成HTML文档了。

目前用到的标签,除分割线,纯文本,链接,图片,其他的在HTML中,每一种类型的标签都是成对出现的,我用两个数组把他们的前后节点分别存放起来,如果没有后置结点的,在数组中用空串来占位。链接和图片在生成时再单独处理。

// HTML前置标签
const std::string frontTag[] = {
    
    
	"", "<p>", "", "<ul>", "<ol>", "<li>", "<em>", "<strong>", "<hr color=#CCCCCC size=1/ >",
	"", "<blockquote>", "<h1>", "<h2>", "<h3>", "<h4>", "<h5>", "<h6>",
	"<pre><code>", "<code>", "<strike>"};
// HTML后置标签
const std::string backTag[] = {
    
    
	"", "</p>", "", "</ul>", "</ol>", "</li>", "</em>",
	"</strong>", "", "", "</blockquote>", "</h1>", "</h2>",
	"</h3>", "</h4>", "</h5>", "</h6>", "</code></pre>", "</code>", "</strike>" };

下面遍历语法树,并生成HTML文件(先写入一个字符串中,再导入文件),在每一个结点内容输入前,先插入前驱HTML标签,然后插入内容,最后插入后置HTML标签。如果有子节点,就进行深度优先遍历,输入所有子节点后再继续当前层遍历。

//使用深度优先遍历,把语法树转换为html文档
	void markdown::dfs(Node* root)
	{
    
    

		//插入前置标签
		_content += frontTag[root->_type];

		//插入当前内容
		//特殊内容处理
		//1.网址
		if (root->_type == href)
		{
    
    
			_content += "<a href=\"";
			_content += root->elem[1];
			_content += "\">";
			_content += root->elem[0];
			_content += "</a>";
		}
		//2.图片
		else if (root->_type == image)
		{
    
    
			_content += "<img alt=\"";
			_content += root->elem[0];
			_content += "\" src=\"";
			_content += root->elem[1];
			_content += "\" />";
		}
		//普通内容
		else
		{
    
    
			_content += root->elem[0];
		}

		//处理子节点
		for (auto a : root->_child)
			dfs(a);

		//插入后置标签
		_content += backTag[root->_type];
	}

下面只需要把存HTML文档的字符串,在写入文档前加上HTML的标准头给就行了,如下:

//生成HTML文档
	void markdown::createHtml()
	{
    
    
		std::string head =
			"<!DOCTYPE html><html><head>\
			<meta charset=\"utf-8\">\
			<title>Markdown</title>\
			</head><body>";
		std::string end = "</body></html>";
		
		ofstream fout("my.html");
		fout << head << _content << end;
	}

最后,功能完成后销毁语法树。

//销毁
	void markdown::destory(Node* root)
	{
    
    
		if (root)
		{
    
    
			for (auto ch : root->_child)
				destory(ch);

			delete root;
		}
	}

Congratulation!完成啦!
完整项目地址:MarkDown

猜你喜欢

转载自blog.csdn.net/Mmonster23/article/details/108600170