当前位置: 首页 > news >正文

【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:Stringfunc _to_string() -> String:return "\n```%s\n%s\n```" % [language,code]# 转化为HTMLfunc to_html() -> String:return "\n<code>\n<pre>\n%s\n</pre>\n</code>\n" % code

Headding

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

Paragraph

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

Img

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

UL

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

OL

# 有序列表
class OL:var list:PackedStringArrayfunc _to_string() -> String:var sttr:Stringfor i in range(list.size()):sttr += "%d. %s\n" % [i+1,list[i]]return sttr# 转化为HTMLfunc 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:Stringvar hrs:PackedStringArrayhrs.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# 转化为HTMLfunc 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 MDdocvar _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:Stringvar code:PackedStringArray# ============ 处理UL ============ var ul_list:PackedStringArrayvar ol_list:PackedStringArrayvar 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 = falsedoc.append_CodebBlock(code_language,"\n".join(code))code.clear();code_language="" # 还原初始状态else:if start_codeblock:code.append(line)# 非代码块if !start_codeblock:# H1-H6if 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])# ULelif 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()# OLelif 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 bolfor i in range(6):if line.match("%s *" % "#".repeat(i+1)):bol = truereturn bol# 获取标题的等级
static func get_headding_level(line:String) -> int:var lvfor i in range(6):if line.match("%s *" % "#".repeat(i+1)):lv = i + 1return 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 = levelh.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 = srcimg.desc = desc_doc.append(img)# 添加代码片段
func append_CodebBlock(language:String,code:String):var c = CodebBlock.new()c.language = languagec.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 EditorScriptfunc _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 MDdocvar _doc:Array
# ============================ 内部类 ============================# 代码块
class CodebBlock:var language:String = ""var code:Stringfunc _to_string() -> String:return "\n```%s\n%s\n```" % [language,code]# H1-H6
class Headding:var level:intvar text:Stringfunc _to_string() -> String:return "%s %s" % ["#".repeat(level),text]
# 段落
class Paragraph:var text:Stringfunc _to_string() -> String:return "p:%s" % text #"\n%s" % text# 图片
class Img:var src:Stringvar desc:Stringfunc _to_string() -> String:return "\n![%s](%s)\n" % [desc,src]# 无序列表
class UL:var list:PackedStringArrayfunc _to_string() -> String:return "- " + "\n- ".join(list)# 有序列表
class OL:var list:PackedStringArrayfunc _to_string() -> String:var sttr:Stringfor 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:Stringvar hrs:PackedStringArrayhrs.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 = levelh.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 = srcimg.desc = desc_doc.append(img)# 添加代码片段
func append_CodebBlock(language:String,code:String):var c = CodebBlock.new()c.language = languagec.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 = []passfunc _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:Stringvar code:PackedStringArray# ============ 处理UL ============ var ul_list:PackedStringArrayvar ol_list:PackedStringArrayvar 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 = falsedoc.append_CodebBlock(code_language,"\n".join(code))code.clear();code_language="" # 还原初始状态else:if start_codeblock:code.append(line)# 非代码块if !start_codeblock:# H1-H6if 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])# ULelif 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()# OLelif 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 bolfor i in range(6):if line.match("%s *" % "#".repeat(i+1)):bol = truereturn bol# 获取标题的等级
static func get_headding_level(line:String) -> int:var lvfor i in range(6):if line.match("%s *" % "#".repeat(i+1)):lv = i + 1return lv

http://www.mrgr.cn/news/16772.html

相关文章:

  • 【MyBatis】MyBatis的一级缓存和二级缓存简介
  • 29. 双耳配对
  • FastAPI+Vue3零基础开发ERP系统项目实战课 20240831上课笔记 路径参数
  • OCI编程高级篇(十五) 设置字段数据入口
  • 【Kubernetes知识点问答题】第二篇
  • 【电子数据取证】Linux软件包管理器yum和编辑器vim
  • 【408DS算法题】031基础-判断二叉树是否是平衡二叉树
  • linux文件——文件系统——学习、理解、应用软硬件链接
  • PyTorch 的自动求导与计算图
  • 猫咪浮毛不再乱飞 希喂、霍尼韦尔、352宠物空气净化器功能实测
  • 如何在windows中使用hfd.sh aria2c下载huggingface文件
  • 【函数模板】函数模板的重载
  • 操作系统的发展历程与分类
  • python --计算两个月份的差值
  • Datawhale X 李宏毅苹果书 AI夏令营|机器学习基础之线性模型
  • 在Ubuntu 20.04上安装MySQL的方法
  • LeetCode - 10 正则表达式匹配
  • 手把手教你从开发进度划分测试
  • 【算法每日一练及解题思路】找出模式匹配字符串的异位词在原始字符串中出现的索引下标
  • 《QDebug 2024年8月》