Data formats

XML writer

XML built from nested maps in the order given, with attributes, text, repeated elements and CDATA, escaped as XML needs.

Version 1.0.0 · 2 functions · 2 crates

#xml#export#soap#sitemaps

Functions

What use xml_write.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use xml_build, xml_build_with from xml_write (Quillo 0.6).

xml_build(root: str, value: any) → stror an error

XML text for a root element holding value, after an <?xml version="1.0" encoding="UTF-8"?> line. In maps, keys are child elements in the order given, @name keys attributes, #text the element's text, #cdata a CDATA section and #comment a comment; a list repeats its element; null is an empty element (<x/>), and an attribute holding null is left out. Text and attributes are escaped (&, <, "). Names that XML does not allow are an error.

xml_build_with(root: str, value: any, options: any) → stror an error

As xml_build, with options: indent (spaces, 0 to 8; 0 writes one line) and declaration (false leaves out the <?xml line, for a fragment to put inside other XML).

Get it

$ quillo install xml_write               # xml_write.qwer into this folder$ quillo install xml_write --example     # and its example, xml_write.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/xml_write.qwer$ curl -O https://quillolang.org/plugins/files/xml_write.qwe

Then, in your program, all of it or only what you call:

main.qwe
use xml_write.qweruse xml_build, xml_build_with from xml_write      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
quick-xml = "0.36"serde_json = { version = "1", features = ["preserve_order"] }

Example

xml_write.qwe
// A sitemap, and a SOAP request to a partner, built from maps.////   quillo run xml_write.qwe      then open http://localhost:8080/sitemap.xmluse "xml_write.qwer"let PAGES = [{ path: "/", changed: "2026-11-02" }, { path: "/products?sort=new&page=2", changed: "2026-11-01" }, { path: "/about", changed: "2026-10-20" }]route GET /sitemap.xml = respond(200, xml_build_with("urlset", {    "@xmlns": "http://www.sitemaps.org/schemas/sitemap/0.9",    url: PAGES | map((p) => { loc: `https://shop.example.com${p.path}`, lastmod: p.changed }),}, { indent: 2 })?, { "content-type": "application/xml" })route GET /soap {    let body = xml_build_with("soap:Envelope", {        "@xmlns:soap": "http://schemas.xmlsoap.org/soap/envelope/",        "@xmlns:m": "http://partner.example.com/stock",        "soap:Header": null,        "soap:Body": { "m:GetStock": { "m:Sku": "B7", "m:Warehouse": { "@code": "AMS-1", "#text": "Amsterdam <Noord> & co" }, "m:Note": { "#cdata": "<b>urgent</b>" } } },    }, { declaration: true })?    // In a real app: http.post("https://partner.example.com/soap", body, { "content-type": "text/xml; charset=utf-8", soapaction: "GetStock" })    respond(200, body, { "content-type": "text/xml" })}route GET /bad = { error: xml_build("order", { "1st item": "x" }).error }
$ quillo run xml_write.qwe$ curl 'localhost:8080/sitemap.xml'$ curl 'localhost:8080/soap'

Source

xml_write.qwer
//! XML built from nested maps, in the order you give: attributes, text, repeated elements and CDATA, escaped as XML needs, for SOAP requests, sitemaps, invoices (UBL) and partner feeds.//! version: 1.0.0//!//! [dependencies]//! quick-xml = "0.36"//! serde_json = { version = "1", features = ["preserve_order"] }use quick_xml::events::{BytesCData, BytesDecl, BytesEnd, BytesStart, BytesText, Event};use quick_xml::Writer;use serde_json::Value;fn valid_name(n: &str) -> bool {    let mut c = n.chars();    matches!(c.next(), Some(f) if f.is_alphabetic() || f == '_') && n.chars().all(|x| x.is_alphanumeric() || "_-.:".contains(x))}fn text_of(v: &Value) -> String {    match v {        Value::String(s) => s.clone(),        Value::Null => String::new(),        Value::Bool(b) => b.to_string(),        Value::Number(n) => n.to_string(),        other => other.to_string(),    }}fn err<E: std::fmt::Display>(e: E) -> String {    e.to_string()}fn element<W: std::io::Write>(w: &mut Writer<W>, name: &str, v: &Value, depth: usize) -> Result<(), String> {    if depth > 200 {        return Err("nested more than 200 deep".into());    }    if !valid_name(name) {        return Err(format!("`{}` is not an XML element name", name));    }    match v {        // A list is the element repeated.        Value::Array(items) => {            for it in items {                element(w, name, it, depth + 1)?;            }            Ok(())        }        Value::Object(m) => {            let mut start = BytesStart::new(name);            for (k, x) in m {                if let Some(a) = k.strip_prefix('@') {                    if !valid_name(a) {                        return Err(format!("`{}` is not an XML attribute name", a));                    }                    if !x.is_null() {                        start.push_attribute((a, text_of(x).as_str()));                    }                }            }            let children: Vec<(&String, &Value)> = m.iter().filter(|(k, _)| !k.starts_with('@')).collect();            if children.is_empty() {                return w.write_event(Event::Empty(start)).map_err(err);            }            w.write_event(Event::Start(start)).map_err(err)?;            for (k, x) in children {                match k.as_str() {                    "#text" => w.write_event(Event::Text(BytesText::new(&text_of(x)))).map_err(err)?,                    "#cdata" => {                        let t = text_of(x);                        // `]]>` cannot be inside one CDATA section: it is split between two.                        let parts: Vec<&str> = t.split("]]>").collect();                        for (i, part) in parts.iter().enumerate() {                            let p = format!("{}{}{}", if i > 0 { ">" } else { "" }, part, if i + 1 < parts.len() { "]]" } else { "" });                            w.write_event(Event::CData(BytesCData::new(p))).map_err(err)?;                        }                    }                    "#comment" => {                        let t = text_of(x).replace("--", "- -");                        w.write_event(Event::Comment(BytesText::from_escaped(format!(" {} ", t)))).map_err(err)?;                    }                    _ => element(w, k, x, depth + 1)?,                }            }            w.write_event(Event::End(BytesEnd::new(name))).map_err(err)        }        Value::Null => w.write_event(Event::Empty(BytesStart::new(name))).map_err(err),        other => {            w.write_event(Event::Start(BytesStart::new(name))).map_err(err)?;            w.write_event(Event::Text(BytesText::new(&text_of(other)))).map_err(err)?;            w.write_event(Event::End(BytesEnd::new(name))).map_err(err)        }    }}fn build(root: &str, value: &Value, declaration: bool, indent: usize) -> Result<String, String> {    let mut out = Vec::new();    {        let mut w = if indent > 0 { Writer::new_with_indent(&mut out, b' ', indent.min(8)) } else { Writer::new(&mut out) };        if declaration {            w.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None))).map_err(|e| e.to_string())?;        }        if value.is_array() {            return Err("the root is one element: give a map, not a list".into());        }        element(&mut w, root, value, 0)?;    }    let mut s = String::from_utf8(out).map_err(|e| e.to_string())?;    if indent > 0 {        s.push('\n');    }    Ok(s)}/// XML text for a `root` element holding `value`, after an `<?xml version="1.0" encoding="UTF-8"?>` line. In maps, keys are/// child elements in the order given, `@name` keys attributes, `#text` the element's text, `#cdata` a CDATA section and/// `#comment` a comment; a list repeats its element; null is an empty element (`<x/>`), and an attribute holding null is left/// out. Text and attributes are escaped (`&`, `<`, `"`). Names that XML does not allow are an error.pub fn xml_build(root: &str, value: serde_json::Value) -> Result<String, String> {    build(root, &value, true, 0)}/// As xml_build, with `options`: `indent` (spaces, 0 to 8; 0 writes one line) and `declaration` (false leaves out the/// `<?xml` line, for a fragment to put inside other XML).pub fn xml_build_with(root: &str, value: serde_json::Value, options: serde_json::Value) -> Result<String, String> {    let indent = options["indent"].as_i64().unwrap_or(0).clamp(0, 8) as usize;    let decl = options["declaration"].as_bool().unwrap_or(true);    build(root, &value, decl, indent)}
Thread
76Useful? Like it, or add to its thread below.

Thread 0 posts

On XML writer: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.