// ============================================================
// XML
// ============================================================
// 一个轻量、纯 MoonBit 的 XML 解析器:把文档解析成元素树。
// 支持:XML 声明、DOCTYPE、注释、CDATA、处理指令、自闭合标签、
// 单/双引号属性、命名实体(< > & " ')。
// 限制:忽略命名空间前缀解析、不校验 DTD;数字实体(NN;)原样保留。
///|
/// XML 节点:文本或子元素
pub(all) enum XmlNode {
Text(String)
Element(XmlElement)
} derive(Eq, @debug.Debug)
///|
/// XML 元素:标签名 + 属性 + 子节点
pub(all) struct XmlElement {
name : String
attrs : Array[(String, String)]
children : Array[XmlNode]
} derive(Eq, @debug.Debug)
///|
/// 读取并解析 XML 文件,返回根元素
pub fn read_xml(
file_path : String,
encoding? : Encoding? = None,
) -> XmlElement raise ReaderError {
parse_xml(read_txt(file_path, encoding~))
}
///|
/// 解析 XML 字符串,返回根元素
pub fn parse_xml(text : String) -> XmlElement raise ReaderError {
let chars = text.iter().to_array()
let n = chars.length()
let pos = @ref.new(0)
skip_misc(chars, n, pos)
if pos.val >= n || chars[pos.val] != '<' {
raise ReaderError::Parse("XML 缺少根元素")
}
parse_element(chars, n, pos)
}
///|
/// 该元素的全部文本内容(含子孙文本节点,按顺序拼接)
pub fn XmlElement::text(self : XmlElement) -> String {
let mut s = ""
for c in self.children {
match c {
Text(t) => s = s + t
Element(child) => s = s + child.text()
}
}
s
}
///|
/// 按属性名取值,不存在返回 None
pub fn XmlElement::attr(self : XmlElement, key : String) -> String? {
for kv in self.attrs {
let (k, v) = kv
if k == key {
return Some(v)
}
}
None
}
///|
/// 渲染为带缩进的 XML 字符串(用于展示)
pub fn XmlElement::to_string(self : XmlElement) -> String {
render_element(self, 0)
}
///|
/// 直接子元素(过滤掉文本节点)
pub fn XmlElement::child_elements(self : XmlElement) -> Array[XmlElement] {
let out : Array[XmlElement] = []
for c in self.children {
match c {
Element(child) => out.push(child)
Text(_) => ()
}
}
out
}
///|
/// 查找第一个名为 name 的后代元素(含自身),找不到返回 None
pub fn XmlElement::find(self : XmlElement, name : String) -> XmlElement? {
find_first(self, name)
}
///|
/// 查找所有名为 name 的后代元素(含自身)
pub fn XmlElement::find_all(
self : XmlElement,
name : String,
) -> Array[XmlElement] {
let out : Array[XmlElement] = []
collect_walk(self, name, out)
out
}
// ============================================================
// 内部遍历辅助函数
// ============================================================
///|
/// 收集所有名为 name 的后代元素(含自身),供 find_all 使用
fn collect_walk(e : XmlElement, name : String, out : Array[XmlElement]) -> Unit {
if e.name == name {
out.push(e)
}
for c in e.children {
match c {
Element(child) => collect_walk(child, name, out)
Text(_) => ()
}
}
}
///|
/// 查找第一个名为 name 的后代元素(含自身),供 find 使用
fn find_first(e : XmlElement, name : String) -> XmlElement? {
if e.name == name {
return Some(e)
}
for c in e.children {
match c {
Element(child) =>
match find_first(child, name) {
Some(x) => return Some(x)
None => ()
}
Text(_) => ()
}
}
None
}
// ============================================================
// 内部解析辅助函数
// ============================================================
///|
fn is_ws(c : Char) -> Bool {
c == ' ' || c == '\t' || c == '\r' || c == '\n'
}
///|
fn skip_ws(chars : Array[Char], n : Int, pos : Ref[Int]) -> Unit {
while pos.val < n && is_ws(chars[pos.val]) {
pos.val = pos.val + 1
}
}
///|
fn is_name_char(c : Char) -> Bool {
!is_ws(c) &&
c != '<' &&
c != '>' &&
c != '/' &&
c != '=' &&
c != '\'' &&
c != '"'
}
///|
/// 宽松读取名称:遇到空白或 < > / = 引号 之一时停止,可读中文等非 ASCII 名称
fn read_name(chars : Array[Char], n : Int, pos : Ref[Int]) -> String {
let buf : Array[Char] = []
while pos.val < n && is_name_char(chars[pos.val]) {
buf.push(chars[pos.val])
pos.val = pos.val + 1
}
String::from_array(buf)
}
///|
/// 跳过 XML 声明、注释、DOCTYPE 等顶层杂项,停在根元素 '<' 处
fn skip_misc(chars : Array[Char], n : Int, pos : Ref[Int]) -> Unit {
while pos.val < n {
skip_ws(chars, n, pos)
if pos.val >= n {
return
}
if chars[pos.val] != '<' {
return
}
if is_comment_start(chars, n, pos.val) {
skip_comment(chars, n, pos)
} else if is_cdata_start(chars, n, pos.val) {
skip_until_gt(chars, n, pos)
} else if pos.val + 1 < n &&
(chars[pos.val + 1] == '!' || chars[pos.val + 1] == '?') {
skip_until_gt(chars, n, pos)
} else {
return // 真正的根元素
}
}
}
///|
/// 解析一个元素,入口处 pos.val 指向 '<'
fn parse_element(
chars : Array[Char],
n : Int,
pos : Ref[Int],
) -> XmlElement raise ReaderError {
pos.val = pos.val + 1 // 跳过 '<'
let name = read_name(chars, n, pos)
if name == "" {
raise ReaderError::Parse("XML 元素名称为空")
}
let attrs = read_attrs(chars, n, pos)
skip_ws(chars, n, pos)
if pos.val < n && chars[pos.val] == '/' {
pos.val = pos.val + 1
skip_ws(chars, n, pos)
if pos.val < n && chars[pos.val] == '>' {
pos.val = pos.val + 1
}
return { name, attrs, children: [], }
}
if pos.val < n && chars[pos.val] == '>' {
pos.val = pos.val + 1
let children = parse_children(chars, n, pos, name)
return { name, attrs, children, }
}
raise ReaderError::Parse("XML 元素 <" + name + "> 格式错误")
}
///|
/// 解析属性列表,直到 '>' 或 '/>';支持 name="value" 与布尔属性
fn read_attrs(
chars : Array[Char],
n : Int,
pos : Ref[Int],
) -> Array[(String, String)] raise ReaderError {
let attrs : Array[(String, String)] = []
while pos.val < n {
skip_ws(chars, n, pos)
if pos.val >= n {
break
}
let c = chars[pos.val]
if c == '>' || c == '/' || c == '?' {
break
}
let an = read_name(chars, n, pos)
if an == "" {
break
}
skip_ws(chars, n, pos)
if pos.val < n && chars[pos.val] == '=' {
pos.val = pos.val + 1
let av = read_attr_value(chars, n, pos)
attrs.push((an, av))
} else {
attrs.push((an, "")) // 无值属性
}
}
attrs
}
///|
fn read_attr_value(
chars : Array[Char],
n : Int,
pos : Ref[Int],
) -> String raise ReaderError {
skip_ws(chars, n, pos)
if pos.val >= n {
return ""
}
let quote = chars[pos.val]
if quote != '"' && quote != '\'' {
raise ReaderError::Parse("XML 属性值缺少引号")
}
pos.val = pos.val + 1
let buf : Array[Char] = []
while pos.val < n && chars[pos.val] != quote {
buf.push(chars[pos.val])
pos.val = pos.val + 1
}
if pos.val < n {
pos.val = pos.val + 1 // 跳过结束引号
}
decode_entities(String::from_array(buf))
}
///|
/// 解析子节点,直到遇到匹配的闭合标签
fn parse_children(
chars : Array[Char],
n : Int,
pos : Ref[Int],
parent_name : String,
) -> Array[XmlNode] raise ReaderError {
let children : Array[XmlNode] = []
while pos.val < n {
let text = read_text(chars, n, pos)
if text != "" && !is_blank(text) {
children.push(XmlNode::Text(decode_entities(text)))
}
if pos.val >= n {
break
}
// 此时 chars[pos.val] == '<'
if pos.val + 1 < n && chars[pos.val + 1] == '/' {
if is_closing(chars, n, pos, parent_name) {
skip_closing(chars, n, pos)
return children
}
raise ReaderError::Parse(
"XML 标签不匹配: 期望 " + parent_name + ">",
)
}
if is_comment_start(chars, n, pos.val) {
skip_comment(chars, n, pos)
continue
}
if is_cdata_start(chars, n, pos.val) {
children.push(XmlNode::Text(read_cdata(chars, n, pos)))
continue
}
if pos.val + 1 < n &&
(chars[pos.val + 1] == '!' || chars[pos.val + 1] == '?') {
skip_until_gt(chars, n, pos)
continue
}
children.push(XmlNode::Element(parse_element(chars, n, pos)))
}
raise ReaderError::Parse(
"XML 元素 <" + parent_name + "> 缺少闭合标签",
)
}
///|
/// 读取文本直到下一个 '<'
fn read_text(chars : Array[Char], n : Int, pos : Ref[Int]) -> String {
let buf : Array[Char] = []
while pos.val < n && chars[pos.val] != '<' {
buf.push(chars[pos.val])
pos.val = pos.val + 1
}
String::from_array(buf)
}
///|
fn is_blank(s : String) -> Bool {
for c in s {
if !is_ws(c) {
return false
}
}
true
}
///|
/// 判断 pos 处是否为
fn is_closing(
chars : Array[Char],
n : Int,
pos : Ref[Int],
parent_name : String,
) -> Bool {
let i = pos.val
if i + 1 >= n || chars[i] != '<' || chars[i + 1] != '/' {
return false
}
let pn = parent_name.iter().to_array()
let k = pn.length()
if i + 2 + k > n {
return false
}
let mut j = 0
while j < k {
if chars[i + 2 + j] != pn[j] {
return false
}
j = j + 1
}
let after = i + 2 + k
if after < n {
let c = chars[after]
if c != '>' && c != '/' && !is_ws(c) {
return false
}
}
true
}
///|
fn skip_closing(chars : Array[Char], n : Int, pos : Ref[Int]) -> Unit {
pos.val = pos.val + 2 // 跳过 ''
while pos.val < n && chars[pos.val] != '>' {
pos.val = pos.val + 1
}
if pos.val < n {
pos.val = pos.val + 1
}
}
///|
fn skip_until_gt(chars : Array[Char], n : Int, pos : Ref[Int]) -> Unit {
while pos.val < n && chars[pos.val] != '>' {
pos.val = pos.val + 1
}
if pos.val < n {
pos.val = pos.val + 1
}
}
///|
fn skip_comment(chars : Array[Char], n : Int, pos : Ref[Int]) -> Unit {
pos.val = pos.val + 4 // 跳过 '