///|
let pdigit : Parser[Char, Char] = @combinator.pchar_such_that(is_digit)
///|
test "pdigit" {
let seq = Seq::from_string("123456")
let result = pdigit.repeat().run(seq).unwrap()
println(result)
}
///|
let phexdigit : Parser[Char, Char] = @combinator.pchar_such_that(is_hex_digit)
///|
fn is_digit(c : Char) -> Bool {
match c {
'0'..='9' => true
_ => false
}
}
///|
fn is_hex_digit(c : Char) -> Bool {
match c {
'0'..='9' | 'a'..='f' | 'A'..='F' => true
_ => false
}
}
///|
fn is_name_start_char(c : Char) -> Bool {
match c {
':' | '_' | 'a'..='z' | 'A'..='Z' => true
'\u00C0'..='\u00D6' => true
'\u00D8'..='\u00F6' => true
'\u00F8'..='\u02FF' => true
'\u0370'..='\u037D' => true
'\u037F'..='\u1FFF' => true
'\u200C'..='\u200D' => true
'\u2070'..='\u218F' => true
'\u2C00'..='\u2FEF' => true
'\u3001'..='\uD7FF' => true
'\uF900'..='\uFDCF' => true
'\uFDF0'..='\uFFFD' => true
'\u{10000}'..='\u{EFFFF}' => true
_ => false
}
}
///|
fn is_name_char(c : Char) -> Bool {
if is_name_start_char(c) {
return true
}
match c {
'-'
| '.'
| '0'..='9'
| '\u00B7'
| '\u0300'..='\u036F'
| '\u203F'..='\u2040' => true
_ => false
}
}
///|
fn is_char(c : Char) -> Bool {
match c {
'\u0009'
| '\u000A'
| '\u000D'
| '\u0020'..='\uD7FF'
| '\uE000'..='\uFFFD'
| '\u{10000}'..='\u{10FFFF}' => true
_ => false
}
}
///|
/// Parses a sequence of whitespace characters (space, tab, carriage return, line
/// feed) into a string.
///
/// Returns a parser that, when applied to an input sequence:
///
/// * Consumes zero or more whitespace characters.
/// * Concatenates all consumed whitespace characters into a single string.
/// * Returns the concatenated string.
///
/// Example:
///
/// ```moonbit check
/// test {
/// let result = pwhite_space().run(@combinator.Seq::from_string(" \t\n\rtext"))
/// debug_inspect(
/// result,
/// content=(
/// #|Some((" \t\n\r", text))
/// ),
/// )
/// }
/// ```
pub fn pwhite_space() -> Parser[Char, String] {
fn is_ws(c : Char) {
c == '\u0020' || c == '\u0009' || c == '\u000D' || c == '\u000A'
}
@combinator.pchar_such_that(is_ws)
.and_then(@combinator.pchar_such_that(is_ws).repeat())
.map(fn(tuple) {
let (ws, arr) = tuple
arr.iter().fold(init=ws.to_string(), fn(s, c) { s + c.to_string() })
})
}
///|
/// Parses XML text content excluding special characters ('<' and '&').
///
/// Parameters:
///
/// * `input` : A sequence of characters to be parsed. Should not contain any
/// instances of '<' or '&'.
///
/// Returns a `Parser[Char, String]` that, when applied to an input sequence:
///
/// * Succeeds and produces a `String` representing the concatenated characters
/// that are not '<' or '&'.
/// * Fails if any character in the sequence is '<' or '&'.
///
/// Example:
///
/// ```moonbit check
/// test {
/// let result = ptext().run(@combinator.Seq::from_string("Hello World"))
/// debug_inspect(
/// result,
/// content=(
/// #|Some(("Hello World"))
/// ),
/// )
/// let result = ptext().run(@combinator.Seq::from_string("Text with & and <"))
/// debug_inspect(
/// result,
/// content=(
/// #|Some(("Text with ", & and <))
/// ),
/// ) // stops parsing at '&'
/// let result = ptext().run(@combinator.Seq::from_string(""))
/// debug_inspect(
/// result,
/// content=(
/// #|Some((""))
/// ),
/// )
/// }
/// ```
pub fn ptext() -> Parser[Char, String] {
@combinator.pchar_such_that(fn(c) { c != '<' && c != '&' })
.repeat()
.map(fn(chars) {
let mut content = ""
chars.each(fn(c) { content = content + c.to_string() })
content
})
}
// 解析 XML 标签名
///|
/// Parses an XML name according to the XML 1.0 specification. An XML name must
/// start with a name start character (letter, underscore, or colon) followed by
/// zero or more name characters (letters, digits, dots, hyphens, underscores, or
/// colons).
///
/// Returns a parser that, when successful, produces a string containing the
/// parsed XML name.
///
/// Example:
///
/// ```moonbit check
/// test {
/// // Valid XML names
/// let result1 = pname().run(@combinator.Seq::from_string("element1"))
/// debug_inspect(
/// result1,
/// content=(
/// #|Some(("element1"))
/// ),
/// )
///
/// // Invalid XML names (starting with digit)
/// let result2 = pname().run(@combinator.Seq::from_string("1element"))
/// debug_inspect(result2, content="None")
/// }
/// ```
pub fn pname() -> Parser[Char, String] {
@combinator.pchar_such_that(is_name_start_char)
.and_then(@combinator.pchar_such_that(is_name_char).repeat())
.map(fn(tuple) {
let (start, rest) = tuple
let mut name = start.to_string()
for c in rest {
name += c.to_string()
}
name
})
}
///|
test "pname/valid_name" {
// Boundary case: Valid XML name with minimum length
let result = pname().run(Seq::from_string("a"))
debug_inspect(
result,
content=(
#|Some(("a"))
),
)
// Boundary case: Valid XML name with typical characters
let result = pname().run(Seq::from_string("root"))
debug_inspect(
result,
content=(
#|Some(("root"))
),
)
// Boundary case: Valid XML name with mixed characters
let result = pname().run(Seq::from_string("xml:Root123"))
debug_inspect(
result,
content=(
#|Some(("xml:Root123"))
),
)
// Boundary case: Valid XML name with extended characters
let result = pname().run(Seq::from_string("🐇"))
debug_inspect(
result,
content=(
#|Some(("🐇"))
),
)
}
///|
test "panic pname/invalid_name" {
// Boundary case: Invalid XML name with invalid start character
let result = pname().run(Seq::from_string("1invalid"))
debug_inspect(result, content="None")
// Boundary case: Invalid XML name with invalid character in the name
let result = pname().run(Seq::from_string("root%"))
debug_inspect(result, content="None")
let result = pname().run(Seq::from_string("\u{FFFFF}"))
debug_inspect(result, content="None")
}
///|
fn pattValue() -> Parser[Char, String] {
fn pattValue_quote(quote : Char) -> Parser[Char, String] {
@combinator.pchar(quote)
.and_then(
@combinator.pchar_such_that(fn(c) { c != quote && c != '<' && c != '&' })
.map(fn(c) { c.to_string() })
.or_else(preference())
.repeat()
.and_then(@combinator.pchar(quote))
.map(fn(tuple) {
let chars = tuple.0
let mut value = ""
chars.each(fn(s) { value = value + s })
value
}),
)
.omit_first()
}
pattValue_quote('"').or_else(pattValue_quote('\''))
}
// 解析 XML 属性
///|
/// Parses an XML attribute, which consists of a name followed by an equals sign
/// and a quoted string value.
///
/// Parameters:
///
/// * `input` : A sequence of characters representing the XML attribute to be
/// parsed. The sequence should start with a valid XML name followed by "=" and a
/// double-quoted string value.
///
/// Returns a `Parser[Char, (String, String)]` that, when applied to an input
/// sequence:
///
/// * Succeeds with a tuple containing the attribute name and value if parsing is
/// successful
/// * Fails if the input does not match the expected XML attribute format
///
/// Example:
///
/// ```moonbit check
/// test {
/// let result = pattribute().run(@combinator.Seq::from_string("name=\"value\""))
/// debug_inspect(
/// result,
/// content=(
/// #|Some((("name", "value")))
/// ),
/// )
/// }
/// ```
pub fn pattribute() -> Parser[Char, (String, String)] {
pname()
.and_then(pwhite_space().optional())
.omit_second()
.and_then(
@combinator.pchar('=')
.and_then(pwhite_space().optional())
.omit_second()
.and_then(pattValue())
.omit_first(),
)
.map(fn(tuple) { (tuple.0, tuple.1) })
}
///|
test "pattribute/valid" {
// Basic valid attribute
let result = pattribute().run(Seq::from_string("name=\"1234567890\""))
debug_inspect(
result,
content=(
#|Some((("name", "1234567890")))
),
)
// Single quotes
let result2 = pattribute().run(Seq::from_string("name='value'"))
debug_inspect(
result2,
content=(
#|Some((("name", "value")))
),
)
}
///|
test "pattribute/invalid_format" {
// Missing quotes around value
let result = pattribute().run(Seq::from_string("name=value"))
debug_inspect(result, content="None")
// Missing equals sign
let result2 = pattribute().run(Seq::from_string("name\"value\""))
debug_inspect(result2, content="None")
// quotes not matching
let result3 = pattribute().run(Seq::from_string("name=\"value'"))
debug_inspect(result3, content="None")
let result4 = pattribute().run(Seq::from_string("name='value\""))
debug_inspect(result4, content="None")
}
///|
/// Parses a sequence of XML attributes, each followed by optional whitespace.
/// Combines all parsed attributes into a map where keys are attribute names and
/// values are corresponding attribute values.
///
/// Returns a parser that produces a map from attribute names to their values.
/// The parser succeeds even if no attributes are present, returning an empty
/// map.
///
/// Example:
///
/// ```moonbit check
/// test {
/// let result = pattributes().run(
/// @combinator.Seq::from_string(" name=\"value\" type=\"text\""),
/// )
/// let attrs = result.unwrap().0
/// debug_inspect(attrs.get("name"), content="Some(\"value\")")
/// debug_inspect(attrs.get("type"), content="Some(\"text\")")
/// }
/// ```
pub fn pattributes() -> Parser[Char, Map[String, String]] {
pwhite_space()
.and_then(pattribute())
.omit_first()
.repeat()
.map(fn(attrs) {
attrs.fold(init=Map::new(), fn(map, tuple) {
map.set(tuple.0, tuple.1)
map
})
})
}
///|
test "pattributes/empty" {
// Test case for empty attributes
let result = pattributes().run(Seq::from_string(""))
debug_inspect(
result,
content=(
#|Some(({}))
),
)
}
///|
test "pattributes/single" {
// Test case for single attribute
let result = pattributes().run(Seq::from_string(" name=\"value\""))
let map = result.unwrap().0
debug_inspect(map.length(), content="1")
debug_inspect(map.get("name"), content="Some(\"value\")")
}
///|
test "pattributes/multiple" {
// Test case for multiple attributes
let result = pattributes().run(
Seq::from_string(" name=\"value\" lang=\"en\""),
)
let map = result.unwrap().0
debug_inspect(map.length(), content="2")
debug_inspect(map.get("name"), content="Some(\"value\")")
}
///|
test "panic pattributes/invalid_format" {
// Test case for invalid attribute format
let result = pattributes().run(Seq::from_string("name=value"))
debug_inspect(result, content="None")
}
///|
fn trim_text(text : String) -> (String, String, String) {
// text should not be empty
let seq = Seq::from_string(text)
let (head_WS, rest) = match pwhite_space().run(seq) {
Some((ws, rest)) => (ws, rest)
None => ("", seq)
}
if rest.is_empty() {
return (head_WS, "", "")
}
let seq = Seq::from_string(text.rev())
let tail_WS = match pwhite_space().run(seq) {
Some((ws, _)) => ws.rev()
None => ""
}
let start = head_WS.length()
let end = text.length() - tail_WS.length()
let content = text[start:end].to_owned()
(head_WS, content, tail_WS)
}
// 解析 XML 元素
///|
/// Parses an XML element, which can be either an empty element (e.g., `
`)
/// or an element with content (e.g., `...`). The parser handles
/// nested elements, text content, CDATA sections, processing instructions,
/// comments, and entity references.
///
/// Parameters:
///
/// * `input` : A sequence of characters representing an XML element. The
/// sequence should start with an opening tag (e.g., ``) and end with either
/// a self-closing tag (e.g., `/>`) or a matching closing tag (e.g., ` `).
///
/// Returns a parser that produces an `XMLElement` structure containing:
///
/// * The element's name
/// * A map of attribute names to their values
/// * A queue of child nodes (`XMLChildren`)
///
/// Example:
///
/// ```moonbit check
/// test {
/// // Parse a simple XML element with attributes and text content
/// let input = "Hello, World! "
/// let result = pelement().run(@combinator.Seq::from_string(input))
/// let element = result.unwrap().0
/// debug_inspect(
/// element.name,
/// content=(
/// #|"user"
/// ),
/// )
/// debug_inspect(element.attributes.get("id"), content="Some(\"1\")")
/// debug_inspect(element.attributes.get("name"), content="Some(\"John\")")
/// }
/// ```
pub fn pelement() -> Parser[Char, XMLElement] {
let element_ref : Ref[Parser[Char, XMLElement]] = {
val: Parser::new(_ => None),
}
fn pcontent() -> Parser[Char, Array[XMLChildren]] {
let element_parser = @combinator.Parser::from_ref(element_ref).map(fn(e) {
Element(e)
})
let reference_parser = preference().map(fn(s) { Reference(s) })
let cdata_parser = pcdata().map(fn(s) { CDATA(s) })
let pi_parser = ppi().map(fn(pi) { XMLChildren::PI(pi) })
let comment_parser = pcomment().map(fn(s) { XMLChildren::Comment(s) })
let tail_parser = element_parser
.or_else(reference_parser)
.or_else(cdata_parser)
.or_else(pi_parser)
.or_else(comment_parser)
.and_then(ptext().optional())
.repeat()
let parser = ptext()
.optional()
.map(fn(op) {
match op {
Some(text) =>
match trim_text(text) {
(head_WS, "", _) => [XMLChildren::WhiteSpace(head_WS)]
(head_WS, content, "") => [WhiteSpace(head_WS), Text(content)]
(head_WS, content, tail_WS) =>
[WhiteSpace(head_WS), Text(content), WhiteSpace(tail_WS)]
}
None => []
}
})
.and_then(tail_parser)
.map(fn(tuple) {
let (queue, arr) = tuple
arr.each(fn(pair) {
let (child, op) = pair
queue.push(child)
match op {
Some(text) => queue.push(Text(text))
None => ()
}
})
queue
})
parser
}
let empty_parser = @combinator.pchar('<')
.and_then(pname())
.omit_first()
.and_then(pattributes())
.and_then(pwhite_space().optional())
.omit_second()
.and_then(@combinator.pstring("/>"))
.omit_second()
.map(fn(tuple) {
let (name, attributes) = tuple
{ name, empty_element: true, attributes, children: [] }
})
let children_parser = @combinator.pchar('<')
.and_then(pname())
.omit_first()
.and_then(pattributes())
.and_then(pwhite_space().optional())
.omit_second()
.and_then(@combinator.pchar('>'))
.omit_second()
.map(fn(x) { x })
.and_then(pcontent())
.map(fn(x) { x })
.and_then(
@combinator.pstring("")
.and_then(pname())
.omit_first()
.and_then(pwhite_space().optional())
.omit_second()
.and_then(@combinator.pchar('>'))
.omit_second(),
)
.map(fn(tuple) {
let (((name_start, attrs), children), name_end) = tuple
if name_start != name_end {
// TODO raiseError
// raise XMLParseError("Mismatched start and end tags")
println("Mismatched start and end tags")
}
{ name: name_start, empty_element: false, attributes: attrs, children }
})
// 这么组合的性能不够好。
let parser = empty_parser.or_else(children_parser)
element_ref.val = parser
return element_ref.val
}
///|
test "pelement/empty" {
// Test empty element with no attributes
let result = pelement().run(Seq::from_string("
"))
debug_inspect(
result,
content=(
#|Some((XMLElement:
))
),
)
// Test empty element with multiple attributes and special characters
let result2 = pelement().run(
Seq::from_string(
"",
),
)
let element = result2.unwrap().0
debug_inspect(
element.name,
content=(
#|"meta"
),
)
debug_inspect(element.attributes.get("name"), content="Some(\"description\")")
debug_inspect(
element.attributes.get("content"),
content="Some(\"Test ∧ Demo\")",
)
debug_inspect(element.attributes.get("charset"), content="Some(\"UTF-8\")")
debug_inspect(element.children.length(), content="0")
}
///|
test "pelement/complex" {
let xml =
#|
#| Some text
#| Child text
#|
#| content]]>
#|
#|
#| <reference>
#|
#|
#| Deep text
#|
#|
#|
#|
let result = pelement().run(@combinator.Seq::from_string(xml))
let element = result.unwrap().0
// Check root element
debug_inspect(
element.name,
content=(
#|"root"
),
)
debug_inspect(element.attributes.get("id"), content="Some(\"main\")")
}
///|
fn peek_char_seq(seq : Seq[Char], n : Int) -> String? {
// help me return Some(first n char to string) in seq, or return None
Seq::peek_char(seq, n)
}
///|
/// Parses a CDATA section in XML, which allows text containing characters that
/// would otherwise need to be escaped. A CDATA section starts with "". The content between these markers is treated as plain
/// text, not XML markup.
///
/// Parameters:
///
/// * `seq` : The input sequence of characters to be parsed. Expected to start
/// with a CDATA section.
///
/// Returns a parser that produces the content of the CDATA section as a string,
/// excluding the CDATA markers.
///
/// Example:
///
/// ```moonbit check
/// test {
/// let input = " XML & content]]>"
/// let result = pcdata().run(@combinator.Seq::from_string(input))
/// debug_inspect(
/// result,
/// content=(
/// #|Some(("Some XML & content"))
/// ),
/// )
/// let input = ""
/// let result = pcdata().run(@combinator.Seq::from_string(input))
/// debug_inspect(
/// result,
/// content=(
/// #|Some((""))
/// ),
/// )
/// }
/// ```
pub fn pcdata() -> Parser[Char, String] {
let ptail_cdata : Parser[Char, String] = @combinator.Parser::new(fn(seq) {
let peek3 = peek_char_seq(seq, 3)
match peek3 {
Some("]]>") => None
_ =>
match seq.uncons() {
Some((c, rest)) => Some((c.to_string(), rest))
None => None
}
}
})
@combinator.pstring(""))
.omit_second()
.map(arr => arr.join(""))
}
///|
/// Parses an XML comment. A comment in XML starts with "". The parser captures all characters between these delimiters, excluding
/// the delimiters themselves.
///
/// Parameters:
///
/// * `seq` : A sequence of characters representing the XML content to be parsed.
/// The sequence should start with an XML comment.
///
/// Returns a parser that produces a string containing the comment content when
/// successful, or fails if the input is not a valid XML comment.
///
/// Example:
///
/// ```moonbit check
/// test {
/// let comment = ""
/// let result = pcomment().run(@combinator.Seq::from_string(comment))
/// debug_inspect(
/// result,
/// content=(
/// #|Some((" This is a comment "))
/// ),
/// )
/// }
/// ```
pub fn pcomment() -> Parser[Char, String] {
let pvalidNonDashChar = @combinator.pchar_such_that(c => {
is_char(c) && c != '-'
}).map(fn(c) { c.to_string() })
let pdashFollowedByValidChar = @combinator.pchar('-')
.and_then(pvalidNonDashChar)
.map(fn(tuple) {
let (_, c) = tuple
"-" + c
})
let pcommentContent = pvalidNonDashChar
.or_else(pdashFollowedByValidChar)
.repeat()
.map(arr => arr.join(""))
@combinator.pstring(""))
.omit_second()
}
///|
fn not_xml(c : Char) -> Bool {
c != 'x' && c != 'X' && c != 'm' && c != 'M' && c != 'l' && c != 'L'
}
///|
pub fn ppiTarget() -> Parser[Char, String] {
@combinator.pchar_such_that(is_name_start_char)
.and_then(@combinator.pchar_such_that(is_name_char).repeat())
.map(fn(tuple) {
let (start, rest) = tuple
let mut name = start.to_string()
for c in rest {
name += c.to_string()
}
name
})
}
///|
/// Parses an XML processing instruction (PI) from a sequence of characters. A
/// processing instruction begins with "", followed by arbitrary content not
/// containing "?>", and ends with "?>".
///
/// Returns a parser that, when applied to an input sequence:
///
/// * Succeeds with the content of the processing instruction (excluding the ""
/// and "?>" delimiters) if a valid PI is found
/// * Fails if the input does not start with a valid processing instruction
///
/// Example:
///
/// ```moonbit check
/// test {
/// let result = ppi().run(@combinator.Seq::from_string(""))
/// debug_inspect(
/// result,
/// content=(
/// #|Some(("php echo 'Hello' "))
/// ),
/// )
/// let result = ppi().run(@combinator.Seq::from_string("?>"))
/// debug_inspect(
/// result,
/// content=(
/// #|Some((""))
/// ),
/// )
/// let result = ppi().run(@combinator.Seq::from_string("' Char*)))? '?>'
/// PITarget ::= Name - (('X' | 'x') ('M' | 'm') ('L' | 'l'))
pub fn ppi() -> Parser[Char, String] {
let ptail_pi : Parser[Char, Char] = @combinator.Parser::new(fn(seq) {
let peek2 = peek_char_seq(seq, 2)
match peek2 {
Some("?>") => None
_ =>
match seq.uncons() {
Some((c, rest)) => Some((c, rest))
None => None
}
}
})
@combinator.pstring("")
.and_then(ptail_pi.repeat())
.omit_first()
.and_then(@combinator.pstring("?>"))
.omit_second()
.map(arr => arr.fold(init="", (str, char) => str + char.to_string()))
}
///|
test "pcomment/empty" {
// Empty comment
let result = pcomment().run(Seq::from_string(""))
debug_inspect(
result,
content=(
#|Some((""))
),
)
}
///|
test "pcomment/with_content" {
// Comment with various content including special characters
let result = pcomment().run(
Seq::from_string(""),
)
debug_inspect(
result,
content=(
#|Some((" This is a comment with and &symbols& "))
),
)
}
///|
test "pcomment/invalid" {
// Incomplete comment
let result = pcomment().run(Seq::from_string(""))
debug_inspect(result2, content="None")
}
///|
/// Parses an XML entity reference of the form `&name;`, where `name` is a valid
/// XML name.
///
/// Returns a `Parser` that accepts a sequence of characters and produces a
/// `String` containing the complete entity reference. For example, parsing
/// `<` will return `"<"`.
///
/// Example:
///
/// ```moonbit check
/// test {
/// let result = pEntityRef().run(@combinator.Seq::from_string("<"))
/// debug_inspect(
/// result,
/// content=(
/// #|Some(("<"))
/// ),
/// )
/// }
/// ```
pub fn pEntityRef() -> Parser[Char, String] {
@combinator.pchar('&')
.and_then(pname())
.and_then(@combinator.pchar(';'))
.map(fn(tuple) {
let ((_and, name), _semicolon) = tuple
"&" + name + ";"
})
}
///|
/// Parses a character reference in XML, which can be either a decimal reference
/// (\d;) or a hexadecimal reference (\h;) where 'd' is a sequence of
/// decimal digits and 'h' is a sequence of hexadecimal digits.
///
/// Returns a `Parser[Char, String]` that, when applied to an input sequence:
///
/// * For decimal references, matches patterns like "{"
/// * For hexadecimal references, matches patterns like "¥"
/// * Returns the matched reference as a string, including the delimiters
///
/// Example:
///
/// ```moonbit check
/// test {
/// let decimal = pcharRef().run(@combinator.Seq::from_string("{"))
/// let hex = pcharRef().run(@combinator.Seq::from_string("¥"))
/// debug_inspect(
/// decimal,
/// content=(
/// #|Some(("#123;"))
/// ),
/// )
/// debug_inspect(
/// hex,
/// content=(
/// #|Some(("#A5;"))
/// ),
/// )
/// }
/// ```
pub fn pcharRef() -> Parser[Char, String] {
let pdecimal = @combinator.pstring("")
.and_then(pdigit)
.and_then(pdigit.repeat())
.and_then(@combinator.pchar(';'))
.map(fn(tuple) {
let (((_hash, digit), digits), _semicolon) = tuple
"#" +
digit.to_string() +
digits.fold(init="", fn(str, char) { str + char.to_string() }) +
";"
})
let phex = @combinator.pstring("")
.and_then(phexdigit)
.and_then(phexdigit.repeat())
.and_then(@combinator.pchar(';'))
.map(fn(tuple) {
let (((_hash, digit), digits), _semicolon) = tuple
"#" +
digit.to_string() +
digits.fold(init="", fn(str, char) { str + char.to_string() }) +
";"
})
pdecimal.or_else(phex)
}
///|
/// Parses an XML reference, which can be either an entity reference (like
/// `&`) or a character reference (like ` ` or ` `).
///
/// Returns a parser that succeeds with the reference string if the input matches
/// either an entity reference or a character reference pattern, and fails
/// otherwise.
///
/// Example:
///
/// ```moonbit check
/// test {
/// // Entity reference
/// let entity = preference().run(@combinator.Seq::from_string("&"))
/// debug_inspect(
/// entity,
/// content=(
/// #|Some(("&"))
/// ),
/// )
///
/// // Character reference (decimal)
/// let decimal = preference().run(@combinator.Seq::from_string(" "))
/// debug_inspect(
/// decimal,
/// content=(
/// #|Some(("#32;"))
/// ),
/// )
///
/// // Character reference (hexadecimal)
/// let hex = preference().run(@combinator.Seq::from_string(" "))
/// debug_inspect(
/// hex,
/// content=(
/// #|Some(("#20;"))
/// ),
/// )
/// }
/// ```
pub fn preference() -> Parser[Char, String] {
pEntityRef().or_else(pcharRef())
}
///|
/// Parses an XML prolog declaration, which appears at the beginning of an XML
/// document. The prolog typically contains information about XML version,
/// encoding, and standalone status.
///
/// Returns a parser that, when applied to an input sequence:
///
/// * Succeeds with a map containing prolog attributes if the input starts with a
/// valid XML prolog
/// * Fails if the input does not match the XML prolog syntax: ``
///
/// Example:Have
/// let input = ""
/// let result = pprolog().run(Seq::from_string(input))
/// let attrs = result.unwrap().0
/// debug_inspect(attrs.get("version"), content="Some(\"1.0\")")
/// debug_inspect(attrs.get("encoding"), content="Some(\"UTF-8\")")
/// ```
pub fn pprolog() -> Parser[Char, Map[String, String]] {
@combinator.pstring(""))
.omit_second()
}
///|
test "pprolog/valid_prolog_with_spaces" {
let result = pprolog().run(
Seq::from_string(
"",
),
)
debug_inspect(
result,
content=(
#|Some(({ "version": "1.0", "encoding": "UTF-8", "standalone": "yes" }))
),
)
}
///|
test "pprolog/minimal_valid_prolog" {
let result = pprolog().run(Seq::from_string(""))
debug_inspect(
result,
content=(
#|Some(({}))
),
)
}
///|
/// Parses a Document Type Definition (DTD) declaration in an XML document.
/// Starts with "\", capturing all characters in between
/// except for the closing angle bracket.
///
/// Returns a parser that, when applied to an input sequence:
///
/// * Succeeds with the contents of the DTD declaration as a string, excluding
/// the opening "\" delimiters
/// * Fails if the input doesn't match the expected DTD format
///
/// Example:
///
/// ```moonbit check
/// test {
/// let result = pdtd()
/// .run(@combinator.Seq::from_string(""))
/// .unwrap().0
/// debug_inspect(result, content="")
/// }
/// ```
pub fn pdtd() -> Parser[Char, DocTypeDecl] {
@combinator.pstring("'))
.omit_second()
.map(fn(tuple) {
let ((name, externalID), intSubset) = tuple
{ name, externalID, intSubset }
})
}
///|
/// Parses a complete XML document string into an `XMLDocument` structure.
/// Handles XML prolog, DOCTYPE declaration (DTD), and the root element with its
/// contents. Supports whitespace between major components.
///
/// Parameters:
///
/// * `seq` : A sequence of characters representing the XML document to be
/// parsed. Should contain optional XML prolog, optional DOCTYPE declaration, and
/// exactly one root element.
///
/// Returns a `Parser[Char, XMLDocument]` that, when applied to an input
/// sequence:
///
/// * Succeeds with an `XMLDocument` containing version (defaults to "1.0"),
/// encoding (defaults to "UTF-8"), standalone flag, and the parsed root element.
/// * Fails if the input is not a well-formed XML document.
///
/// Example:
///
/// ```moonbit check
/// test {
/// let xml = "Text "
/// let result = pxml().run(@combinator.Seq::from_string(xml))
/// let doc = result.unwrap().0
/// debug_inspect(
/// doc.version,
/// content=(
/// #|"1.0"
/// ),
/// )
/// debug_inspect(
/// doc.root.name,
/// content=(
/// #|"root"
/// ),
/// )
/// }
/// ```
pub fn pxml() -> Parser[Char, XMLDocument] {
pwhite_space()
.optional()
.and_then(pprolog().optional())
.omit_first()
.and_then(pmisc().repeat())
.omit_second()
.and_then(pdtd().and_then(pmisc().repeat()).omit_second().optional())
.and_then(pwhite_space().optional())
.omit_second()
.and_then(pelement())
.map(fn(tuple) {
let ((map, dtd), element) = tuple
let map = match map {
Some(m) => m
None => Map::new()
}
{
version: map.get("version").unwrap_or("1.0"),
encoding: map.get("encoding").unwrap_or("UTF-8"),
standalone: match map.get("standalone") {
Some("yes") => true
_ => false
},
dtd,
root: element,
}
})
}
///|
test "pxml/complex_nested" {
let xml =
#|
#|
#|
#| My Document
#|
#|
#|
#|
#| content & data]]>
#|
#| Normal text
#| Special chars: < > &
#|
#|
#|
#|
let result = pxml().run(Seq::from_string(xml))
let doc = result.unwrap().0
// Check basic document properties
debug_inspect(
doc.version,
content=(
#|"1.0"
),
)
debug_inspect(
doc.encoding,
content=(
#|"UTF-8"
),
)
debug_inspect(doc.standalone, content="false")
// Check root element
debug_inspect(
doc.root.name,
content=(
#|"root"
),
)
debug_inspect(doc.root.attributes.get("id"), content="Some(\"main\")")
}
///|
test "pxml/mixed_content" {
let xml =
#|
#|
#| Text before
#|
#| Text between
#|
#| in CDATA]]>
#|
#|
#| Nested text
#|
#| More nested text
#|
#| Text after
#|
#|
let result = pxml().run(Seq::from_string(xml))
let doc = result.unwrap().0
debug_inspect(
doc.version,
content=(
#|"1.0"
),
)
debug_inspect(
doc.root.name,
content=(
#|"root"
),
)
// Check that root has children
debug_inspect(doc.root.children.is_empty(), content="false")
}
///|
test "pxml/basic_xml_no_attributes" {
let xml = " "
let result = pxml().run(Seq::from_string(xml))
let document = result.unwrap().0
debug_inspect(
document.version,
content=(
#|"1.0"
),
)
debug_inspect(
document.encoding,
content=(
#|"UTF-8"
),
)
debug_inspect(document.standalone, content="false")
debug_inspect(
document.root.name,
content=(
#|"root"
),
)
debug_inspect(document.root.attributes.length(), content="0")
}
///|
test "pxml/basic_xml_with_attributes" {
let xml = " "
let result = pxml().run(Seq::from_string(xml))
let document = result.unwrap().0
debug_inspect(
document.version,
content=(
#|"1.1"
),
)
debug_inspect(
document.encoding,
content=(
#|"ISO-8859-1"
),
)
debug_inspect(document.standalone, content="true")
debug_inspect(
document.root.name,
content=(
#|"root"
),
)
debug_inspect(document.root.attributes.length(), content="2")
debug_inspect(
document.root.attributes.get("attr1"),
content="Some(\"value1\")",
)
debug_inspect(
document.root.attributes.get("attr2"),
content="Some(\"value2\")",
)
}
///|
test "pxml/mismatched_start_end_tags" {
let xml = " "
ignore(pxml().run(Seq::from_string(xml)))
}
///|
/// Parses an XML string and converts it into an XMLDocument structure. The input
/// string should contain a well-formed XML document with a single root element.
///
/// Parameters:
///
/// * `xml_string` : A string containing the XML document to be parsed.
///
/// Returns an `Option` type containing the parsed `XMLDocument` if successful,
/// or `None` if the parsing fails due to invalid XML syntax.
///
/// Example:
///
/// ```moonbit check
/// test {
/// let xml = "Content "
/// let doc = xml_from_string(xml).unwrap()
/// debug_inspect(
/// doc.root.name,
/// content=(
/// #|"root"
/// ),
/// )
/// }
/// ```
pub fn xml_from_string(s : String) -> XMLDocument? {
let result = pxml().run(Seq::from_string(s))
match result {
Some((doc, _)) => Some(doc)
None => None
}
}
///|
fn iter_to_seq(iter : Iter[Char]) -> Seq[Char] {
Seq::from_array(iter.to_array()[:])
}
///|
/// This function is slower than xml_from_string. Have O(n^2) complexity.
pub fn xml_from_iter(iter : Iter[Char]) -> XMLDocument? {
let seq = iter_to_seq(iter)
let result = pxml().run(seq)
match result {
Some((doc, _)) => Some(doc)
None => None
}
}