Simple XML Parser
Parse simplified XML structures with nested elements, text content, and tag matching validation. Educational example for understanding tree-based parsing.
Key Concepts
- tag matching
- nested structures
- self-closing tags
- tree construction
- validation
Why Parse This Format?
XML remains important for enterprise systems, document formats, and configuration files. Understanding XML parsing helps with many real-world data interchange scenarios.
Use Cases
- Configuration file parsing
- Document format processing
- SOAP web services
- RSS/Atom feed parsing
- SVG graphics processing
Input Format
Simple XML with opening tags <name>, closing tags </name>, and self-closing tags <name />. Text content between tags. No attributes or namespaces in this simplified version.
<tag>content</tag> | <tag /> | textExamples
<a>text</a>VALIDSimple element with text
<a><b>content</b></a>VALIDNested elements
<a><b>nested</b><c>elements</c></a>VALIDMultiple nested elements
<br />VALIDSelf-closing element
<div><p>Hello</p><br /><p>World</p></div>VALIDMixed content
<a><b>mismatch</a></b>INVALIDInvalid: mismatched tags
<a>unclosedINVALIDInvalid: missing closing tag
Output Format
The parser returns XmlNode enum values representing elements, text, or empty elements, with validation results.
Structure
XmlNodeEnum: Element { name, children, text }, Text(String), EmptyElement { name }
validationReturns valid/invalid status with error messages
Visual Overview
How the Code Works
This section explains how the parsing rules map to the format semantics.
Tag Name Parsing
.rule("tag_name", re("[a-zA-Z][a-zA-Z0-9_-]*"))Tag names follow standard XML identifier rules: Names start with a letter, then allow letters, digits, underscores, and hyphens.
Opening and Closing Tags
.rule(
"open_tag",
seq(vec![dynamic(str("<")), dynamic(re("[a-zA-Z][a-zA-Z0-9_-]*")), dynamic(str(">"))]),
)
.rule(
"close_tag",
seq(vec![dynamic(str("</")), dynamic(re("[a-zA-Z][a-zA-Z0-9_-]*")), dynamic(str(">"))]),
)
.rule(
"self_closing_tag",
seq(vec![dynamic(str("<")), dynamic(re("[a-zA-Z][a-zA-Z0-9_-]*")), dynamic(re("[ \\t]*/[ \\t]*")), dynamic(str(">"))]),
)Tags come in three varieties: Self-closing tags (`<br />`) are handled separately from paired tags.
Tag Matching Validation
pub fn validate_xml(input: &str) -> XmlParseResult {
let mut tag_stack: Vec<String> = Vec::new();
while pos < chars.len() {
if chars[pos] == '<' {
if tag_content.starts_with('/') {
// Closing tag
if let Some(expected) = tag_stack.pop() {
if expected != tag_name {
return XmlParseResult {
valid: false,
message: format!("Tag mismatch: expected </{}>, found </{}>", expected, tag_name),
};
}
}
} else if !tag_content.ends_with('/') {
// Opening tag (not self-closing)
tag_stack.push(tag_name.to_string());
}
}
}
if !tag_stack.is_empty() {
return XmlParseResult {
valid: false,
message: format!("Unclosed tags: {}", tag_stack.join(", ")),
};
}
}XML requires matching open/close tags: A stack tracks open tags; mismatched or unclosed tags produce errors.
Element Parsing
pub fn parse_xml(input: &str) -> Result<Vec<XmlNode>, String> {
while pos < chars.len() {
// Collect text before tag
while pos < chars.len() && chars[pos] != '<' {
pos += 1;
}
if pos > text_start {
nodes.push(XmlNode::Text(text));
}
// Parse tag
if chars[pos] == '<' {
// Extract tag content
}
}
}Elements are parsed into a tree structure:
Element Parsing
pub fn parse_xml(input: &str) -> Result<Vec<XmlNode>, String> {
while pos < chars.len() {
// Collect text before tag
while pos < chars.len() && chars[pos] != '<' {
pos += 1;
}
if pos > text_start {
nodes.push(XmlNode::Text(text));
}
// Parse tag
if chars[pos] == '<' {
// Extract tag content
}
}
}Elements are parsed into a tree structure:
Source Code
//! Simple XML Parser Example
//!
//! This example demonstrates parsing simple XML-like structures.
//! Shows handling of nested tags, text content, and tag matching.
//! Based on the Parslet simple_xml.rb example.
//!
//! Note: This is a simplified XML parser for educational purposes.
//! It does not handle all XML complexities (namespaces, attributes, CDATA, etc.)
//!
//! Run with: cargo run --example simple_xml --no-default-features
#![allow(clippy::print_literal)]
#![allow(clippy::get_first)]
use parsanol::portable::{
parser_dsl::{choice, dynamic, re, seq, str, GrammarBuilder},
AstArena, Grammar, PortableParser,
};
/// Build a simple XML grammar
fn build_xml_grammar() -> Grammar {
GrammarBuilder::new()
// Text content: anything except angle brackets
.rule("text", re("[^<>]*"))
// Tag name: alphabetic characters
.rule("tag_name", re("[a-zA-Z][a-zA-Z0-9_-]*"))
// Opening tag: <name>
.rule(
"open_tag",
seq(vec![
dynamic(str("<")),
dynamic(re("[a-zA-Z][a-zA-Z0-9_-]*")),
dynamic(str(">")),
]),
)
// Closing tag: </name>
.rule(
"close_tag",
seq(vec![
dynamic(str("</")),
dynamic(re("[a-zA-Z][a-zA-Z0-9_-]*")),
dynamic(str(">")),
]),
)
// Self-closing tag: <name />
.rule(
"self_closing_tag",
seq(vec![
dynamic(str("<")),
dynamic(re("[a-zA-Z][a-zA-Z0-9_-]*")),
dynamic(re("[ \\t]*/[ \\t]*")),
dynamic(str(">")),
]),
)
// Element: open tag + content + close tag OR self-closing
.rule(
"element",
choice(vec![
dynamic(seq(vec![
dynamic(str("<")),
dynamic(re("[a-zA-Z][a-zA-Z0-9_-]*")),
dynamic(str(">")),
dynamic(re("[^<>]*")), // content
dynamic(str("</")),
dynamic(re("[a-zA-Z][a-zA-Z0-9_-]*")),
dynamic(str(">")),
])),
dynamic(seq(vec![
dynamic(str("<")),
dynamic(re("[a-zA-Z][a-zA-Z0-9_-]*")),
dynamic(re("[ \\t]*/[ \\t]*")),
dynamic(str(">")),
])),
]),
)
// Document: sequence of elements and text
.rule("document", re("[^<>]*(?:<[^>]+>[^<>]*)*"))
.build()
}
/// XML node types
#[derive(Debug, Clone)]
pub enum XmlNode {
/// Element with tag name, children, and text content
Element {
name: String,
children: Vec<XmlNode>,
text: Option<String>,
},
/// Text content
Text(String),
/// Self-closing element
EmptyElement { name: String },
}
impl std::fmt::Display for XmlNode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
XmlNode::Element {
name,
children,
text,
} => {
write!(f, "<{}>", name)?;
if let Some(t) = text {
write!(f, "{}", t)?;
}
for child in children {
write!(f, "{}", child)?;
}
write!(f, "</{}>", name)
}
XmlNode::Text(t) => write!(f, "{}", t),
XmlNode::EmptyElement { name } => write!(f, "<{} />", name),
}
}
}
/// Parse result
#[derive(Debug, Clone)]
pub struct XmlParseResult {
pub valid: bool,
pub message: String,
}
/// Validate XML by checking tag matching
pub fn validate_xml(input: &str) -> XmlParseResult {
let grammar = build_xml_grammar();
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(&grammar, input, &mut arena);
if parser.parse().is_err() {
return XmlParseResult {
valid: false,
message: "Failed to parse XML structure".to_string(),
};
}
// Manual tag matching validation
let mut tag_stack: Vec<String> = Vec::new();
let mut pos = 0;
let chars: Vec<char> = input.chars().collect();
while pos < chars.len() {
if chars[pos] == '<' {
// Find closing >
let end = chars[pos..].iter().position(|&c| c == '>');
if let Some(end) = end {
let tag_content: String = chars[pos + 1..pos + end].iter().collect();
let tag_content = tag_content.trim();
if let Some(stripped) = tag_content.strip_prefix('/') {
// Closing tag
let tag_name = stripped.trim();
if let Some(expected) = tag_stack.pop() {
if expected != tag_name {
return XmlParseResult {
valid: false,
message: format!(
"Tag mismatch: expected </{}>, found </{}>",
expected, tag_name
),
};
}
} else {
return XmlParseResult {
valid: false,
message: format!("Unexpected closing tag </{}>", tag_name),
};
}
} else if tag_content.ends_with('/') {
// Self-closing tag - skip
} else {
// Opening tag
let tag_name = tag_content.split_whitespace().next().unwrap_or(tag_content);
tag_stack.push(tag_name.to_string());
}
pos += end + 1;
} else {
pos += 1;
}
} else {
pos += 1;
}
}
if !tag_stack.is_empty() {
return XmlParseResult {
valid: false,
message: format!("Unclosed tags: {}", tag_stack.join(", ")),
};
}
XmlParseResult {
valid: true,
message: "XML is valid".to_string(),
}
}
/// Parse XML into a simple structure
pub fn parse_xml(input: &str) -> Result<Vec<XmlNode>, String> {
let grammar = build_xml_grammar();
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(&grammar, input, &mut arena);
parser
.parse()
.map_err(|e| format!("Parse error: {:?}", e))?;
// Manual parsing into XmlNode structure
let mut nodes = Vec::new();
let mut pos = 0;
let chars: Vec<char> = input.chars().collect();
while pos < chars.len() {
// Collect text before tag
let text_start = pos;
while pos < chars.len() && chars[pos] != '<' {
pos += 1;
}
if pos > text_start {
let text: String = chars[text_start..pos].iter().collect();
let text = text.trim();
if !text.is_empty() {
nodes.push(XmlNode::Text(text.to_string()));
}
}
// Parse tag
if pos < chars.len() && chars[pos] == '<' {
let tag_start = pos;
// Find closing >
while pos < chars.len() && chars[pos] != '>' {
pos += 1;
}
if pos < chars.len() {
pos += 1; // Include >
let tag: String = chars[tag_start..pos].iter().collect();
nodes.push(XmlNode::Text(tag)); // Simplified - just store as text
}
}
}
Ok(nodes)
}
fn main() {
println!("Simple XML Parser Example");
println!("=========================\n");
let examples = [
// Valid XML
("<a>text</a>", true),
("<a><b>content</b></a>", true),
("<a><b>nested</b><c>elements</c></a>", true),
("<root>some text in the tags</root>", true),
("<br />", true),
("<div><p>Hello</p><br /><p>World</p></div>", true),
// Invalid XML
("<a><b>mismatch</a></b>", false),
("<a>unclosed", false),
("<a></b>", false),
];
println!("{:<50} | {:<8} | {}", "XML", "Valid?", "Message");
println!("{}", "-".repeat(80));
for (xml, expected_valid) in examples {
let result = validate_xml(xml);
let status = if result.valid == expected_valid {
"✓"
} else {
"✗"
};
println!(
"{:<50} | {:<8} | {} {}",
xml, result.valid, status, result.message
);
}
// Detailed example
println!("\nDetailed Parsing:");
println!("-----------------");
let xml = "<book><title>XML Parsing</title><author>John Doe</author></book>";
println!("Input: {}", xml);
let result = validate_xml(xml);
println!("Valid: {}", result.valid);
if let Ok(nodes) = parse_xml(xml) {
println!("Nodes: {:?}", nodes.len());
for node in &nodes {
if let XmlNode::Text(t) = node {
println!(" - {}", t);
}
}
}
// Tag nesting visualization
println!("\nTag Nesting Example:");
println!("--------------------");
let nested = "<root><level1><level2><level3>deep</level3></level2></level1></root>";
println!("Input: {}", nested);
let result = validate_xml(nested);
println!("Result: {} - {}", result.valid, result.message);
}
# frozen_string_literal: true
# A simple xml parser. It is simple in the respect as that it doesn't address
# any of the complexities of XML. This is ruby 1.9.
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
require "pp"
require "parsanol/parslet"
class XML < Parsanol::Parser
root :document
rule(:document) do
(tag(close: false).as(:o) >> document.as(:i) >> tag(close: true).as(:c)) |
text
end
# Perhaps we could have some syntax sugar to make this more easy?
#
def tag(opts = {})
close = opts[:close] || false
parslet = str("<")
parslet >>= str("/") if close
parslet >>= (str(">").absent? >> match("[a-zA-Z]")).repeat(1).as(:name)
parslet >> str(">")
end
rule(:text) do
match("[^<>]").repeat(0)
end
end
def check(xml)
r = XML.new.parse(xml)
# We'll validate the tree by reducing valid pairs of tags into simply the
# string "verified". If the transformation ends on a string, then the
# document was 'valid'.
#
t = Parsanol::Transform.new do
rule(
o: { name: simple(:tag) },
c: { name: simple(:tag) },
i: simple(:t),
) { "verified" }
end
t.apply(r)
end
pp check("<a><b>some text in the tags</b></a>")
pp check("<b><b>some text in the tags</b></a>")