cml-rs 0.4.0

Content Markup Language (CML) v0.2 parser, generator, validator, and embedding store for structured documents
Documentation
//! CML v0.2 Integration Tests
//!
//! Tests the complete pipeline: parse → validate → generate → validate → parse

use cml_rs::generator::CmlGenerator;
use cml_rs::parser::CmlParser;
use cml_rs::validator::CmlValidator;

#[test]
fn test_full_pipeline_minimal() {
    let xml = r#"<cml profile="core" version="0.2" encoding="utf-8">
  <header>
    <title>Test Document</title>
  </header>
  <body>
    <paragraph>Hello, world!</paragraph>
  </body>
  <footer>
  </footer>
</cml>"#;

    // Parse
    let doc = CmlParser::parse_str(xml).expect("Parse failed");

    // Validate
    CmlValidator::validate(&doc).expect("Validation failed");

    // Generate
    let generated = CmlGenerator::generate(&doc).expect("Generation failed");

    // Parse again
    let doc2 = CmlParser::parse_str(&generated).expect("Re-parse failed");

    // Validate again
    CmlValidator::validate(&doc2).expect("Re-validation failed");

    // Compare
    assert_eq!(doc, doc2);
}

#[test]
fn test_full_pipeline_complex() {
    let xml = r#"<cml profile="law:constitution" version="0.2" encoding="utf-8" id="complex-doc">
  <header>
    <title>Complex Document</title>
    <author role="editor">Editor Name</author>
    <author role="contributor">Contributor Name</author>
    <date type="created" when="2025-01-01"/>
    <date type="modified" when="2025-12-22T10:30:00Z"/>
    <identifier scheme="doi">10.1234/complex</identifier>
    <identifier scheme="isbn">978-0-123456-78-9</identifier>
    <version>2.1</version>
    <description>A complex test document with multiple features.</description>
    <provenance>Created from original drafts</provenance>
    <source>https://example.org/source</source>
    <meta name="classification" value="public"/>
    <meta name="language" value="en"/>
  </header>
  <body>
    <section id="intro">
      <heading size="1">Introduction</heading>
      <paragraph>This document contains <em>various</em> <bo>elements</bo> to test roundtrip functionality.</paragraph>
      <aside side="right" type="note">
        <paragraph>This is a side note.</paragraph>
      </aside>
    </section>
    <section id="content">
      <heading size="1">Main Content</heading>
      <quote source="Famous Person" ref="quote-1">
        <paragraph>A memorable quotation goes here.</paragraph>
      </quote>
      <list type="ordered" style="roman">
        <item>First major point</item>
        <item>Second major point with <rf ref="intro" role="section">reference to intro</rf></item>
        <item>Third point with <lk ref="https://example.org">external link</lk></item>
      </list>
      <table id="summary-table">
        <header>
          <row>
            <column sort="asc"><cell>Category</cell></column>
            <column><cell>Value (<curr type="USD">amount</curr>)</cell></column>
          </row>
        </header>
        <body>
          <row>
            <column><cell>Budget</cell></column>
            <column><cell><curr type="USD" format="symbol">1000000</curr></cell></column>
          </row>
        </body>
        <footer>
          <caption>Summary of values</caption>
        </footer>
      </table>
      <code lang="python" copyable="true">def hello():
    print("Hello, CML!")</code>
    </section>
  </body>
  <footer>
    <signatures>
      <signature when="2025-12-22T15:00:00Z" role="author">Primary Author</signature>
    </signatures>
    <citations>
      <citation ref="quote-1" type="speech">Person, F. (2020). Famous Speech. <em>Speech Collection</em>.</citation>
    </citations>
    <annotations>
      <note type="editorial">Document reviewed and approved.</note>
    </annotations>
  </footer>
</cml>"#;

    // Parse
    let doc = CmlParser::parse_str(xml).expect("Parse failed");

    // Validate
    CmlValidator::validate(&doc).expect("Validation failed");

    // Generate
    let generated = CmlGenerator::generate(&doc).expect("Generation failed");

    // Verify no XML declaration
    assert!(!generated.starts_with("<?xml"));
    assert!(generated.starts_with("<cml"));

    // Parse again
    let doc2 = CmlParser::parse_str(&generated).expect("Re-parse failed");

    // Validate again
    CmlValidator::validate(&doc2).expect("Re-validation failed");

    // Compare
    assert_eq!(doc, doc2);
}

#[test]
fn test_validation_catches_invalid_version() {
    let xml = r#"<cml profile="core" version="1.0" encoding="utf-8">
  <header>
    <title>Test</title>
  </header>
  <body>
    <paragraph>Test</paragraph>
  </body>
  <footer>
  </footer>
</cml>"#;

    let doc = CmlParser::parse_str(xml).expect("Parse should succeed");
    let result = CmlValidator::validate(&doc);

    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("version"));
}

#[test]
fn test_validation_catches_duplicate_ids() {
    let xml = r#"<cml profile="core" version="0.2" encoding="utf-8">
  <header>
    <title>Test</title>
  </header>
  <body>
    <paragraph id="para-1">First</paragraph>
    <paragraph id="para-1">Second</paragraph>
  </body>
  <footer>
  </footer>
</cml>"#;

    let doc = CmlParser::parse_str(xml).expect("Parse should succeed");
    let result = CmlValidator::validate(&doc);

    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("Duplicate ID"));
}

#[test]
fn test_validation_catches_invalid_reference() {
    let xml = r#"<cml profile="core" version="0.2" encoding="utf-8">
  <header>
    <title>Test</title>
  </header>
  <body>
    <paragraph>Reference to <rf ref="nonexistent-id">nowhere</rf></paragraph>
  </body>
  <footer>
  </footer>
</cml>"#;

    let doc = CmlParser::parse_str(xml).expect("Parse should succeed");
    let result = CmlValidator::validate(&doc);

    assert!(result.is_err());
    assert!(result
        .unwrap_err()
        .to_string()
        .contains("Reference not found"));
}

#[test]
fn test_validation_accepts_pathless_references() {
    let xml = r#"<cml profile="core" version="0.2" encoding="utf-8">
  <header>
    <title>Test</title>
  </header>
  <body>
    <paragraph>Reference to <rf ref="president:47">President 47</rf></paragraph>
  </body>
  <footer>
  </footer>
</cml>"#;

    let doc = CmlParser::parse_str(xml).expect("Parse failed");
    CmlValidator::validate(&doc).expect("Validation should succeed");
}

#[test]
fn test_validation_accepts_external_urls() {
    let xml = r#"<cml profile="core" version="0.2" encoding="utf-8">
  <header>
    <title>Test</title>
  </header>
  <body>
    <paragraph>External <lk ref="https://example.org">link</lk></paragraph>
  </body>
  <footer>
  </footer>
</cml>"#;

    let doc = CmlParser::parse_str(xml).expect("Parse failed");
    CmlValidator::validate(&doc).expect("Validation should succeed");
}

#[test]
fn test_validation_catches_invalid_heading_size() {
    let xml = r#"<cml profile="core" version="0.2" encoding="utf-8">
  <header>
    <title>Test</title>
  </header>
  <body>
    <heading size="7">Invalid</heading>
  </body>
  <footer>
  </footer>
</cml>"#;

    let doc = CmlParser::parse_str(xml).expect("Parse should succeed");
    let result = CmlValidator::validate(&doc);

    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("heading size"));
}

#[test]
fn test_all_inline_elements() {
    let xml = r#"<cml profile="core" version="0.2" encoding="utf-8">
  <header>
    <title>Inline Elements Test</title>
  </header>
  <body>
    <paragraph>
      This is <em>emphasized</em> and <bo>bold</bo> and <un>underlined</un> and <st>struck</st> text.
      With <snip char="$">npm install</snip> and <key>Ctrl+C</key> elements.
      Reference: <rf ref="president:47">President 47</rf>.
      Topic: <tg ref="constitution">Constitutional Law</tg>.
      Link: <lk ref="https://example.org">Example Site</lk>.
      Currency: <curr type="USD">1500000</curr> and <curr type="EUR" format="symbol">350000</curr>.
      Break<end kind="line"/>here.
    </paragraph>
  </body>
  <footer>
  </footer>
</cml>"#;

    let doc = CmlParser::parse_str(xml).expect("Parse failed");
    CmlValidator::validate(&doc).expect("Validation failed");
    let generated = CmlGenerator::generate(&doc).expect("Generation failed");
    let doc2 = CmlParser::parse_str(&generated).expect("Re-parse failed");
    CmlValidator::validate(&doc2).expect("Re-validation failed");
    assert_eq!(doc, doc2);
}

#[test]
fn test_all_block_elements() {
    let xml = r#"<cml profile="core" version="0.2" encoding="utf-8">
  <header>
    <title>Block Elements Test</title>
  </header>
  <body>
    <section id="sec-1">
      <heading size="1">Section</heading>
      <paragraph>Section content</paragraph>
    </section>
    <heading size="2">Standalone Heading</heading>
    <paragraph>Regular paragraph</paragraph>
    <aside side="left" type="note">
      <paragraph>Aside content</paragraph>
    </aside>
    <quote source="Author" ref="president:47">
      <paragraph>Quote content</paragraph>
    </quote>
    <list type="ordered" style="numeric">
      <item>Item 1</item>
      <item>Item 2</item>
    </list>
    <table id="table-1">
      <header>
        <row>
          <column><cell>Header</cell></column>
        </row>
      </header>
      <body>
        <row>
          <column><cell>Data</cell></column>
        </row>
      </body>
      <footer>
        <caption>Table caption</caption>
      </footer>
    </table>
    <code lang="rust" copyable="true">fn main() {}</code>
    <break type="scene"/>
  </body>
  <footer>
  </footer>
</cml>"#;

    let doc = CmlParser::parse_str(xml).expect("Parse failed");
    CmlValidator::validate(&doc).expect("Validation failed");
    let generated = CmlGenerator::generate(&doc).expect("Generation failed");
    let doc2 = CmlParser::parse_str(&generated).expect("Re-parse failed");
    CmlValidator::validate(&doc2).expect("Re-validation failed");
    assert_eq!(doc, doc2);
}

#[test]
fn test_nested_structures() {
    let xml = r#"<cml profile="core" version="0.2" encoding="utf-8">
  <header>
    <title>Nested Structures Test</title>
  </header>
  <body>
    <section id="outer">
      <heading size="1">Outer Section</heading>
      <section id="inner">
        <heading size="2">Inner Section</heading>
        <paragraph>Nested content</paragraph>
      </section>
    </section>
    <list type="unordered">
      <item>First level
        <list type="ordered" style="alpha">
          <item>Nested first</item>
          <item>Nested second</item>
        </list>
      </item>
      <item>Another first level</item>
    </list>
    <paragraph>Text with <em type="stress"><bo>nested bold emphasis</bo></em> elements.</paragraph>
  </body>
  <footer>
  </footer>
</cml>"#;

    let doc = CmlParser::parse_str(xml).expect("Parse failed");
    CmlValidator::validate(&doc).expect("Validation failed");
    let generated = CmlGenerator::generate(&doc).expect("Generation failed");
    let doc2 = CmlParser::parse_str(&generated).expect("Re-parse failed");
    CmlValidator::validate(&doc2).expect("Re-validation failed");
    assert_eq!(doc, doc2);
}