Files

Word documents

Word documents written from blocks: headings, paragraphs with bold and italic, lists, tables and page breaks.

Version 1.0.0 · 3 functions · 1 crate

#word#reports#export#files

Functions

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

docx_write(path: str, blocks: list) → intor an error

Writes a .docx at path from blocks, in order: the number of blocks written. Each block is one of { heading, level } (1 to 3), { text, bold, italic, underline, color, size, align } (size in points; align left, center, right or justify; "\n" breaks a line), { runs: [{ text, bold, ... }] } for mixed styles in one paragraph, { bullets: [...] }, { numbered: [...] }, { table: [[...], ...], header } (the first row bold unless header is false) and { page_break: true }; plain text is a paragraph.

docx_write_text(path: str, title: str, text: str) → intor an error

A .docx with title as its heading and text below it, a paragraph for each part between blank lines: for letters and notes.

docx_write_table(path: str, title: str, rows: list, columns: list) → intor an error

Records as a table in a .docx, under title: columns are the header and the order (null is an empty cell). The number of rows.

Get it

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

Or with curl:

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

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

main.qwe
use docx.qweruse docx_write, docx_write_text from docx      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
docx-rs = { version = "0.4", default-features = false }

Example

docx.qwe
// A monthly report and a customer export as Word documents.////   quillo run docx.qwe      then open http://localhost:8080/report.docxuse "docx.qwer"let orders = [    { id: 1041, customer: "Ada Lovelace", total: 120.5, status: "paid" },    { id: 1042, customer: "Grace Hopper", total: 99, status: "sent" },    { id: 1043, customer: "Alan Turing", total: null, status: "draft" },]fn report(path) = docx_write(path, [    { heading: "Monthly report", level: 1 },    { runs: [{ text: "Sales rose by " }, { text: "12%", bold: true }, { text: " in October.", italic: true }] },    { heading: "Highlights", level: 2 },    { bullets: ["Three new shops", "A second roaster", "Card payments everywhere"] },    { table: [["Shop", "Orders", "Revenue"], ["Lyon", 412, "18,250.50"], ["Nantes", 380, "16,410.00"]] },    { page_break: true },    { text: "Prepared by the Quillo Café office.", size: 9, align: "right", color: "#666666" },])route GET /report.docx {    report("data/report.docx")?    file("data/report.docx")}route GET /report = { blocks: report("data/report.docx")?, orders: docx_write_table("data/orders.docx", "Orders", orders, ["id", "customer", "total", "status"])? }route GET /letter.docx {    docx_write_text("data/letter.docx", "Dear Ada,", "Thank you for your order.\n\nIt leaves the roastery on Monday.")?    file("data/letter.docx")}
$ quillo run docx.qwe$ curl 'localhost:8080/report'$ curl 'localhost:8080/report.docx'

Source

docx.qwer
//! Word documents written from a list of blocks: headings, paragraphs with bold and italic, lists, tables and page breaks.//! version: 1.0.0//!//! [dependencies]//! docx-rs = { version = "0.4", default-features = false }use docx_rs::*;use serde_json::Value;const MAX_BLOCKS: usize = 20_000;fn parent_dir(path: &str) -> Result<(), String> {    match std::path::Path::new(path).parent() {        Some(d) if !d.as_os_str().is_empty() => std::fs::create_dir_all(d).map_err(|e| format!("{}: {}", d.display(), e)),        _ => Ok(()),    }}fn s(v: &Value) -> String {    match v {        Value::Null => String::new(),        Value::String(s) => s.clone(),        v => v.to_string(),    }}// Now as "2026-10-29T13:05:00Z", for the file's created and modified dates.fn now_iso() -> String {    let secs = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs() as i64);    let (days, rem) = (secs.div_euclid(86_400), secs.rem_euclid(86_400));    // Howard Hinnant's civil_from_days.    let z = days + 719_468;    let era = z.div_euclid(146_097);    let doe = z - era * 146_097;    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);    let mp = (5 * doy + 2) / 153;    let d = doy - (153 * mp + 2) / 5 + 1;    let m = if mp < 10 { mp + 3 } else { mp - 9 };    let y = yoe + era * 400 + if m <= 2 { 1 } else { 0 };    format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, m, d, rem / 3600, rem % 3600 / 60, rem % 60)}fn base() -> Docx {    let heading = |id: &str, name: &str, size: usize| Style::new(id, StyleType::Paragraph).name(name).based_on("Normal").size(size).bold();    let now = now_iso();    Docx::new()        .created_at(&now)        .updated_at(&now)        .default_size(22)        .add_style(heading("Heading1", "heading 1", 36))        .add_style(heading("Heading2", "heading 2", 30))        .add_style(heading("Heading3", "heading 3", 26))        .add_abstract_numbering(AbstractNumbering::new(1).add_level(            Level::new(0, Start::new(1), NumberFormat::new("bullet"), LevelText::new("•"), LevelJc::new("left")).indent(Some(720), Some(SpecialIndentType::Hanging(360)), None, None),        ))        .add_numbering(Numbering::new(1, 1))        .add_abstract_numbering(AbstractNumbering::new(2).add_level(            Level::new(0, Start::new(1), NumberFormat::new("decimal"), LevelText::new("%1."), LevelJc::new("left")).indent(Some(720), Some(SpecialIndentType::Hanging(360)), None, None),        ))        .add_numbering(Numbering::new(2, 2))}fn run(text: &str, o: &Value) -> Run {    let mut r = Run::new();    for (i, line) in text.split('\n').enumerate() {        if i > 0 {            r = r.add_break(BreakType::TextWrapping);        }        r = r.add_text(line);    }    if o["bold"] == true {        r = r.bold();    }    if o["italic"] == true {        r = r.italic();    }    if o["underline"] == true {        r = r.underline("single");    }    if let Some(c) = o["color"].as_str() {        r = r.color(c.trim_start_matches('#'));    }    if let Some(pt) = o["size"].as_f64() {        r = r.size((pt.clamp(4.0, 200.0) * 2.0) as usize);    }    r}fn paragraph(b: &Value) -> Paragraph {    let mut p = Paragraph::new();    match &b["runs"] {        Value::Array(runs) => {            for r in runs {                p = p.add_run(run(&s(&r["text"]), r));            }        }        _ => p = p.add_run(run(&s(&b["text"]), b)),    }    let align = match b["align"].as_str() {        Some("center") => Some(AlignmentType::Center),        Some("right") => Some(AlignmentType::Right),        Some("justify") => Some(AlignmentType::Both),        _ => None,    };    if let Some(a) = align {        p = p.align(a);    }    p}fn table(rows: &[Value], header: bool) -> Table {    let rows: Vec<TableRow> = rows        .iter()        .enumerate()        .map(|(i, row)| {            let cells = match row {                Value::Array(cells) => cells.clone(),                v => vec![v.clone()],            };            TableRow::new(                cells                    .iter()                    .map(|c| {                        let mut r = Run::new().add_text(s(c));                        if header && i == 0 {                            r = r.bold();                        }                        let cell = TableCell::new().add_paragraph(Paragraph::new().add_run(r));                        if header && i == 0 { cell.shading(Shading::new().fill("E8ECF2")) } else { cell }                    })                    .collect(),            )        })        .collect();    Table::new(rows)}fn add_block(mut doc: Docx, b: &Value) -> Result<Docx, String> {    if let Some(h) = b.get("heading") {        let level = b["level"].as_i64().unwrap_or(1).clamp(1, 3);        let p = Paragraph::new().add_run(Run::new().add_text(s(h))).style(&format!("Heading{}", level)).outline_lvl((level - 1) as usize);        doc = doc.add_paragraph(p);    } else if let Some(Value::Array(items)) = b.get("bullets").or(b.get("numbered")) {        let id = if b.get("bullets").is_some() { 1 } else { 2 };        for it in items {            let p = if it.is_object() { paragraph(it) } else { Paragraph::new().add_run(Run::new().add_text(s(it))) };            doc = doc.add_paragraph(p.numbering(NumberingId::new(id), IndentLevel::new(0)));        }    } else if let Some(Value::Array(rows)) = b.get("table") {        doc = doc.add_table(table(rows, b["header"] != false)).add_paragraph(Paragraph::new());    } else if b["page_break"] == true {        doc = doc.add_paragraph(Paragraph::new().add_run(Run::new().add_break(BreakType::Page)));    } else if b.get("text").is_some() || b.get("runs").is_some() {        doc = doc.add_paragraph(paragraph(b));    } else if let Value::String(t) = b {        doc = doc.add_paragraph(Paragraph::new().add_run(Run::new().add_text(t.as_str())));    } else {        return Err(format!("a block is {{ heading }}, {{ text }}, {{ runs }}, {{ bullets }}, {{ numbered }}, {{ table }} or {{ page_break: true }}, not {}", b));    }    Ok(doc)}fn save(doc: Docx, path: &str) -> Result<(), String> {    parent_dir(path)?;    let f = std::fs::File::create(path).map_err(|e| format!("{}: {}", path, e))?;    doc.build().pack(f).map_err(|e| format!("{}: {}", path, e))}/// Writes a .docx at `path` from `blocks`, in order: the number of blocks written. Each block is one of/// `{ heading, level }` (1 to 3), `{ text, bold, italic, underline, color, size, align }` (size in points; align left, center, right or justify; "\n" breaks a line),/// `{ runs: [{ text, bold, ... }] }` for mixed styles in one paragraph, `{ bullets: [...] }`, `{ numbered: [...] }`,/// `{ table: [[...], ...], header }` (the first row bold unless `header` is false) and `{ page_break: true }`; plain text is a paragraph.pub fn docx_write(path: &str, blocks: Vec<serde_json::Value>) -> Result<i64, String> {    if blocks.len() > MAX_BLOCKS {        return Err(format!("more than {} blocks", MAX_BLOCKS));    }    let mut doc = base();    for b in &blocks {        doc = add_block(doc, b)?;    }    save(doc, path)?;    Ok(blocks.len() as i64)}/// A .docx with `title` as its heading and `text` below it, a paragraph for each part between blank lines: for letters and notes.pub fn docx_write_text(path: &str, title: &str, text: &str) -> Result<i64, String> {    let mut blocks = vec![];    if !title.is_empty() {        blocks.push(serde_json::json!({ "heading": title, "level": 1 }));    }    for para in text.replace("\r\n", "\n").split("\n\n").map(str::trim).filter(|p| !p.is_empty()) {        blocks.push(serde_json::json!({ "text": para }));    }    docx_write(path, blocks)}/// Records as a table in a .docx, under `title`: `columns` are the header and the order (null is an empty cell). The number of rows.pub fn docx_write_table(path: &str, title: &str, rows: Vec<std::collections::HashMap<String, serde_json::Value>>, columns: Vec<String>) -> Result<i64, String> {    let mut table = vec![Value::Array(columns.iter().map(|c| Value::String(c.clone())).collect())];    for r in &rows {        table.push(Value::Array(columns.iter().map(|c| r.get(c).cloned().unwrap_or(Value::Null)).collect()));    }    docx_write(path, vec![serde_json::json!({ "heading": title, "level": 1 }), serde_json::json!({ "table": table })])?;    Ok(rows.len() as i64)}
Thread
67Useful? Like it, or add to its thread below.

Thread 0 posts

On Word documents: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.