【Godot4.3】MarkDown解析和生成类 - MDdoc

概述

一年多前写过GDSCript静态函数库用来在Godot中生成MarkDown文档内容。用在了自己的插件项目Script++中,用来快速生成脚本文件API文档的框架内容。

这次编写了一个集解析、生成MarkDown为一体的MDdoc类,可以更轻松的解决MarkDown文档的问题。

因为基础工作在5月份已经完成,我也是拖了好久重新拾起,所以就以此文为契机,复习自己的代码,并做一个比较详尽的文档。

内部类

  • MDdoc类可以将MarkDown文档元素按其顺序解析为对象后存入内部的数组。每个MarkDown文档元素对应MDdoc的一个内部类:
    • CodebBlock:代码块
    • Headding:标题,H1-H6
    • Paragraph:普通段落
    • Img:图片
    • UL:无序列表
    • OL:有序列表
    • Table:表格

目前为止的内部类都相当简单,简单的属性,加上重写的_to_string()和输出HTML用的to_html()方法。

CodebBlock

# 代码块
class CodebBlock:
	var language:String = ""
	var code:String
	
	func _to_string() -> String:
		return "\n```%s\n%s\n```" % [language,code]
		
	# 转化为HTML
	func to_html() -> String:
		return "\n<code>\n<pre>\n%s\n</pre>\n</code>\n" % code

Headding

# H1-H6
class Headding:
	var level:int
	var text:String
	
	func _to_string() -> String:
		return "%s %s" % ["#".repeat(level),text]
	
	# 转化为HTML
	func to_html() -> String:
		return "\n<h{level}>{text}</h{level}>\n".format({"level":level,"text":text})

Paragraph

# 段落
class Paragraph:
	var text:String
	
	func _to_string() -> String:
		return "\n%s\n" % text #"\n%s" % text
	# 转化为HTML
	func to_html() -> String:
		return "\n<p>{text}</p>\n".format({"text":text})

Img

# 图片
class Img:
	var src:String
	var desc:String
	
	func _to_string() -> String:
		return "\n![%s](%s)\n" % [desc,src]
	
	# 转化为HTML
	func to_html() -> String:
		return "\n<img src = \"{src}\" alt=\"{desc}\">\n".format({"src":src,"desc":desc})

UL

# 无序列表
class UL:
	var list:PackedStringArray
	
	func _to_string() -> String:
		return "- " + "\n- ".join(list)
	
	# 转化为HTML
	func to_html() -> String:
		return "<ul>\n\t<li>" + "\n\t<li>".join(list) + "\n</ul>\n"

OL

# 有序列表
class OL:
	var list:PackedStringArray
	
	func _to_string() -> String:
		var sttr:String
		for i in range(list.size()):
			sttr += "%d. %s\n" % [i+1,list[i]]
		return sttr
		
	# 转化为HTML
	func to_html() -> String:
		return "<ol>\n\t<li>" + "\n\t<li>".join(list) + "\n</ol>\n"

Table

# 表格
class Table:
	var _data:Array[PackedStringArray]
	
	func _to_string() -> String:
		var sttr:String
		var hrs:PackedStringArray
		hrs.resize(_data[0].size())
		hrs.fill("-".repeat(3))
		for y in range(_data.size()):
			if y == 0: # 第1行
				sttr += "| %s |\n" % " | ".join(_data[y])
				sttr += "| %s |\n" % " | ".join(hrs)
			else:
				sttr += "| %s |\n" % " | ".join(_data[y])
		return sttr

	# 转化为HTML
	func to_html() -> String:
		var sttr:String = "<table>\n"
		for y in range(_data.size()):
			if y == 0:
				sttr += "\t<tr>\n\t\t<th>%s\n" %  "<th>".join(_data[y])
			else:
				sttr += "\t<tr>\n\t\t<td>%s\n" %  "<td>".join(_data[y])  
		return sttr + "</table>"

对象数组

MDdoc的核心是load()parse()两个静态方法以及_doc数组以及重写的_to_string(),其中:

  • parse()为最核心的解析方法,是编写的重难点,将Markdown字符串解析为MDdoc实例
  • load()读取指定纯文本文件的内容,然后调用parse()将Markdown字符串解析为MDdoc实例
  • parse()解析的过程,就是用将Markdown字符串内容解析为上文中对应内部类的实例对象后按顺序添加到_doc数组中的过程。
  • MDdoc_to_string(),其内部会遍历_doc中的所有元素,并调用它们自身的_to_string(),把所有内容串接为一个多行字符串,也就是整个文档的纯文本形式。
class_name MDdoc

var _doc:Array  # 文档对象数组

# 加载Markdown文档解析为MDdoc实例
static func load(path:String) -> MDdoc:
	...

# 将Markdown字符串解析为MDdoc实例
static func parse(md:String) -> MDdoc:
    ...

# print()to_string()时返回的内容
func _to_string() -> String:
	var sttr:=""
	for ele in _doc:
		sttr+= ele.to_string() + "\n"
	return sttr

核心解析函数

  • MDdoc在设计解析函数时,使用了按行解析的思路,首先用String类型的split()方法,将需要解析的字符串以\n切分为字符串数组
  • 大量使用String类型的match()(通配符匹配)方法,而不是使用RegEx(正则表达式)。
  • 事实证明,按行解析+match()简易匹配的方式,让解析函数的编写难度大大下降。

下面是parse()方法的代码:

# 将Markdown字符串解析为MDdoc实例
static func parse(md:String) -> MDdoc:
	# ============ 处理代码块 ============ 
	var start_codeblock:bool  # 代码块开启标记
	var code_language:String
	var code:PackedStringArray
	# ============ 处理UL ============ 
	var ul_list:PackedStringArray
	var ol_list:PackedStringArray
	var table_lines:Array[PackedStringArray]
	
	var doc = MDdoc.new()
	var lines = md.split("\n",false)

	for i in range(lines.size()):
		var line = lines[i]
		# 代码块
		if line.match("```*"):
			if !start_codeblock:
				code_language = line.lstrip("```")
				start_codeblock = true  # 代码块开启标记
			else:
				start_codeblock = false
				doc.append_CodebBlock(code_language,"\n".join(code))
				code.clear();code_language="" # 还原初始状态
		else:
			if start_codeblock:
				code.append(line)
		# 非代码块
		if !start_codeblock:
			# H1-H6
			if is_headding(line):
				var lv = get_headding_level(line)
				doc.append_Headding(lv,line.lstrip("%s " % "#".repeat(lv)))
			# 图片
			elif line.match("![*](*)"):
				var img = line.lstrip("![").rstrip(")").split("](")
				doc.append_Img(img[1],img[0])
			# UL
			elif line.match("- *"):
				ul_list.append(line.lstrip("- "))
				if i < lines.size()-1: # 未达文档末尾
					if !lines[i+1].match("- *") or i+1 == lines.size()-1:
						doc.append_UL(ul_list)
						ul_list.clear()
			# OL
			elif line.match("*. *"):
				ol_list.append(line.split(".",true,1)[1])
				if i < lines.size()-1: # 未达文档末尾
					if !lines[i+1].match("*. *") or i+1 == lines.size()-1:
						doc.append_OL(ol_list)
						ol_list.clear()
			# 表格
			elif line.match("|*|"):
				var li = line.lstrip("|").rstrip("|").replace("_"," ")  # 去除首尾的|
				if !is_table_hr(line):
					table_lines.append(li.split("|"))
				if i == lines.size()-1: # 最后一行
					doc.append_Table(table_lines)
					table_lines.clear()
				else:
					if !lines[i+1].match("|*|"): # 未达文档末尾
						print(table_lines)
			else:# 被视为普通段落	
				if !line.match("```*"):
					doc.append_Paragraph(line)
	return doc

parse()解析时需要依赖以下几个函数:

# 是否是表格分割线
static func is_table_hr(line:String):
	var li = line.lstrip("|").rstrip("|")  # 去除首尾的|
	return  li.replace("-","").replace("|","").strip_edges() == ""

# 是否是H1-H6
static func is_headding(line:String):
	var bol
	for i in range(6):
		if line.match("%s *" % "#".repeat(i+1)):
			bol = true
	return bol

# 获取标题的等级
static func get_headding_level(line:String) -> int:
	var lv
	for i in range(6):
		if line.match("%s *" % "#".repeat(i+1)):
			lv = i + 1
	return lv

解析策略:

  • 在解析时,将MD文档元素划分为了代码块其他,代码块开始后将标记start_codeblock变量为true,直到代码块结束,在此期间,所有的代码行将被视为代码块的内容而不会被意外解析
  • 在非代码块元素的行解析时,只需要检测简单的字符串匹配模式即可,比如:
    • line.match("%s *" % "#".repeat(1))可以对应# XXX这样的一级标题
    • "![*](*)"匹配图片
    • "- *"匹配无序列表
    • "*. *"匹配有序列表等等
  • 目前版本当然还没有加入超链接和行内代码,期待后续改进

MarkDown解析测试

我们可以使用FileAccess读取一个.md文件的内容,然后用MDdoc.parse()解析纯文本。

# 读取.md的纯文本内容
var md = FileAccess.get_file_as_string("res://lib/数字与字符/test.md")
var doc = MDdoc.parse(md)   # 以字符串形式解析

或者直接使用MDdoc.load()

# 使用MDdoc.load()简化.md读取
var doc = MDdoc.load("res://lib/数字与字符/test.md")

解析完成后我们可以使用print()直接打印输出实例:

print(doc)

打印输出的结果就是文档本身的内容。

MarkDown生成测试

除了解析已有的MarkDown之外,MDdoc还能从零生成MarkDown文档,而且因为依然是基于_doc的对象数组,所以可以用to_html()方法,轻松的转化为HTML版本。

创建空文档实例

通过new()方法可以创建一个没有元素的MDdoc实例:

var md = MDdoc.new()  # 创建空文档实例

元素添加方法

对应每一种元素,有相应的append_开头的方法,可以在当前新建的MDdoc空实例上创建文档元素。

# 添加标题
func append_Headding(level:int,text:String):
	var h = Headding.new()
	h.level = level
	h.text = text
	_doc.append(h)

# 添加段落
func append_Paragraph(text:String):
	var p = Paragraph.new()
	p.text = text
	_doc.append(p)
	
# 添加UL
func append_UL(list:PackedStringArray):
	var ul = UL.new()
	ul.list = list.duplicate()
	_doc.append(ul)
	
# 添加UL
func append_OL(list:PackedStringArray):
	var ol = OL.new()
	ol.list = list.duplicate()
	_doc.append(ol)

# 添加图片
func append_Img(src:String,desc:String):
	var img = Img.new()
	img.src = src
	img.desc = desc
	_doc.append(img)

# 添加代码片段
func append_CodebBlock(language:String,code:String):
	var c = CodebBlock.new()
	c.language = language
	c.code = code
	_doc.append(c)

# 添加表格
func append_Table(table_lines:Array[PackedStringArray]):
	var table = Table.new()
	table._data = table_lines.duplicate()
	# 去除多余的空格
	for y in range(table._data.size()):
		for x in range(table._data[y].size()):
			table._data[y][x] = table._data[y][x].strip_edges()
	_doc.append(table)

生成测试

@tool
extends EditorScript

func _run() -> void:
	var etd = """
数据结构
	线性结构
		栈
		队列
		双端列表
		列表
	非线性结构
		图
		树
"""

	# 创建MDdoc实例
	var md = MDdoc.new()
	# 按顺序添加文档元素
	md.append_Headding(1,"这是一个测试")
	md.append_Headding(2,"概述")
	md.append_Paragraph("这是一段文本")
	md.append_Img("1.jpg","这是一张图片")
	md.append_Paragraph("这是一段文本")
	md.append_CodebBlock("swift",etd)

	# 输出
	print(md)

输出:

# 这是一个测试
## 概述

这是一段文本


![这是一张图片](1.jpg)


这是一段文本


```swift

数据结构
	线性结构
		栈
		队列
		双端列表
		列表
	非线性结构
		图
		树


目前版本只提供了append_也就是尾部顺序追加的方法,后续改进版本会提供更多的元素数组操作封装。

完整代码

以下是该类的完整代码:

# ========================================================
# 名称:MDdoc
# 类型:类
# 简介:专用于解析和生成MarkDown的类
# 作者:巽星石
# Godot版本:v4.2.2.stable.official [15073afe3]
# 创建时间:202443017:55:31
# 最后修改时间:20245122:42:45
# ========================================================
class_name MDdoc

var _doc:Array
# ============================ 内部类 ============================

# 代码块
class CodebBlock:
	var language:String = ""
	var code:String
	
	func _to_string() -> String:
		return "\n```%s\n%s\n```" % [language,code]

# H1-H6
class Headding:
	var level:int
	var text:String
	
	func _to_string() -> String:
		return "%s %s" % ["#".repeat(level),text]
# 段落
class Paragraph:
	var text:String
	
	func _to_string() -> String:
		return "p:%s" % text #"\n%s" % text

# 图片
class Img:
	var src:String
	var desc:String
	
	func _to_string() -> String:
		return "\n![%s](%s)\n" % [desc,src]

# 无序列表
class UL:
	var list:PackedStringArray
	
	func _to_string() -> String:
		return "- " + "\n- ".join(list)

# 有序列表
class OL:
	var list:PackedStringArray
	
	func _to_string() -> String:
		var sttr:String
		for i in range(list.size()):
			sttr += "%d. %s\n" % [i+1,list[i]]
		return sttr

# 表格
class Table:
	var _data:Array[PackedStringArray]
	
	func _to_string() -> String:
		var sttr:String
		var hrs:PackedStringArray
		hrs.resize(_data[0].size())
		hrs.fill("-".repeat(3))
		for y in range(_data.size()):
			if y == 0: # 第1行
				sttr += "| %s |\n" % " | ".join(_data[y])
				sttr += "| %s |\n" % " | ".join(hrs)
			else:
				sttr += "| %s |\n" % " | ".join(_data[y])
		return sttr

# ============================ 方法 ============================

# 添加标题
func append_Headding(level:int,text:String):
	var h = Headding.new()
	h.level = level
	h.text = text
	_doc.append(h)

# 添加段落
func append_Paragraph(text:String):
	var p = Paragraph.new()
	p.text = text
	_doc.append(p)
	
# 添加UL
func append_UL(list:PackedStringArray):
	var ul = UL.new()
	ul.list = list.duplicate()
	_doc.append(ul)
	
# 添加UL
func append_OL(list:PackedStringArray):
	var ol = OL.new()
	ol.list = list.duplicate()
	_doc.append(ol)

# 添加图片
func append_Img(src:String,desc:String):
	var img = Img.new()
	img.src = src
	img.desc = desc
	_doc.append(img)

# 添加代码片段
func append_CodebBlock(language:String,code:String):
	var c = CodebBlock.new()
	c.language = language
	c.code = code
	_doc.append(c)

# 添加表格
func append_Table(table_lines:Array[PackedStringArray]):
	var table = Table.new()
	table._data = table_lines.duplicate()
	# 去除多余的空格
	for y in range(table._data.size()):
		for x in range(table._data[y].size()):
			table._data[y][x] = table._data[y][x].strip_edges()
	_doc.append(table)

# ============================ 虚函数 ============================

func _init() -> void:
	_doc = []
	pass
	
func _to_string() -> String:
	var sttr:=""
	for ele in _doc:
		sttr+= ele.to_string() + "\n"
	return sttr

# 将Markdown文档解析为MDdoc实例
static func load(path:String) -> MDdoc:
	var md = FileAccess.get_file_as_string(path)
	var doc = MDdoc.new()
	doc.parse(md)
	return doc

# 将Markdown文档解析为MDdoc实例
static func parse(md:String) -> MDdoc:
	# ============ 处理代码块 ============ 
	var start_codeblock:bool  # 代码块开启标记
	var code_language:String
	var code:PackedStringArray
	# ============ 处理UL ============ 
	var ul_list:PackedStringArray
	var ol_list:PackedStringArray
	var table_lines:Array[PackedStringArray]
	
	var doc = MDdoc.new()
	var lines = md.split("\n",false)

	for i in range(lines.size()):
		var line = lines[i]
		# 代码块
		if line.match("```*"):
			if !start_codeblock:
				code_language = line.lstrip("```")
				start_codeblock = true  # 代码块开启标记
			else:
				start_codeblock = false
				doc.append_CodebBlock(code_language,"\n".join(code))
				code.clear();code_language="" # 还原初始状态
		else:
			if start_codeblock:
				code.append(line)
		# 非代码块
		if !start_codeblock:
			# H1-H6
			if is_headding(line):
				var lv = get_headding_level(line)
				doc.append_Headding(lv,line.lstrip("%s " % "#".repeat(lv)))
			# 图片
			elif line.match("![*](*)"):
				var img = line.lstrip("![").rstrip(")").split("](")
				doc.append_Img(img[1],img[0])
			# UL
			elif line.match("- *"):
				ul_list.append(line.lstrip("- "))
				if i < lines.size()-1: # 未达文档末尾
					if !lines[i+1].match("- *"):
						doc.append_UL(ul_list)
						ul_list.clear()
			# OL
			elif line.match("*. *"):
				ol_list.append(line.split(".",true,1)[1])
				if i < lines.size()-1: # 未达文档末尾
					if !lines[i+1].match("*. *"):
						doc.append_OL(ol_list)
						ol_list.clear()
			# 表格
			elif line.match("|*|"):
				var li = line.lstrip("|").rstrip("|")  # 去除首尾的|
				if !is_table_hr(line):
					table_lines.append(li.split("|"))
				if i == lines.size()-1: # 最后一行
					doc.append_Table(table_lines)
					table_lines.clear()
				else:
					if !lines[i+1].match("|*|"): # 未达文档末尾
						print(table_lines)
			else:# 被视为普通段落	
				if !line.match("```*"):
					doc.append_Paragraph(line)
	return doc

# 是否是表格分割线
static func is_table_hr(line:String):
	var li = line.lstrip("|").rstrip("|")  # 去除首尾的|
	return  li.replace("-","").replace("|","").strip_edges() == ""

# 是否是H1-H6
static func is_headding(line:String):
	var bol
	for i in range(6):
		if line.match("%s *" % "#".repeat(i+1)):
			bol = true
	return bol

# 获取标题的等级
static func get_headding_level(line:String) -> int:
	var lv
	for i in range(6):
		if line.match("%s *" % "#".repeat(i+1)):
			lv = i + 1
	return lv

  • 12
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

巽星石

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值