Skip to content
← Back to Examples

Simple XML Parser

RustRubydata formatsintermediate

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 /> | text

Examples

<a>text</a>VALID

Simple element with text

<a><b>content</b></a>VALID

Nested elements

<a><b>nested</b><c>elements</c></a>VALID

Multiple nested elements

<br />VALID

Self-closing element

<div><p>Hello</p><br /><p>World</p></div>VALID

Mixed content

<a><b>mismatch</a></b>INVALID

Invalid: mismatched tags

<a>unclosedINVALID

Invalid: missing closing tag

Output Format

The parser returns XmlNode enum values representing elements, text, or empty elements, with validation results.

Structure

XmlNode

Enum: Element { name, children, text }, Text(String), EmptyElement { name }

validation

Returns valid/invalid status with error messages

Visual Overview

Simple XML Parser diagram

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

Rust
//! 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);
}
Ruby
# 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>")