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
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 errorXML 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 errorAs 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.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/xml_write.qwer$ curl -O https://quillolang.org/plugins/files/xml_write.qweThen, in your program, all of it or only what you call:
use xml_write.qweruse xml_build, xml_build_with from xml_write // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
quick-xml = "0.36"serde_json = { version = "1", features = ["preserve_order"] }Example
// 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 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)}Related
xml.qwer · 1.0.0XML read as nested maps, the texts and attributes of the elements you name, and text escaped for XML.7 views · 6 downloadsAvro avro.qwer · 1.0.0Apache Avro data files: rows written with a schema, as Kafka, Hadoop and BigQuery use, and read back with the schema they carry.9 views · 6 downloadsBibTeX bibtex.qwer · 1.0.0BibTeX and BibLaTeX bibliographies read, with LaTeX accents and macros resolved; reference lists in APA or IEEE style, CSL-JSON for Zotero and pandoc, and entries written back.7 views · 6 downloadsCSV csv.qwer · 1.0.0CSV in and out: rows as maps keyed by the header, with quotes, commas and line breaks in cells handled.8 views · 8 downloadsFixed width fixed_width.qwer · 1.0.0Fixed-width records read and written from a column spec: bank payment files, payroll exports and mainframe formats.8 views · 8 downloadsINI ini.qwer · 1.0.0INI files read and written, sections and keys: php.ini, setup.cfg, .gitconfig and the settings of older software.8 views · 7 downloadsThread 0 posts
On XML writer: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.