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
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 errorWrites 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 errorA .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 errorRecords 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.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/docx.qwer$ curl -O https://quillolang.org/plugins/files/docx.qweThen, in your program, all of it or only what you call:
use docx.qweruse docx_write, docx_write_text from docx // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
docx-rs = { version = "0.4", default-features = false }Example
// 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
//! 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)}Related
pdf_tools.qwer · 1.0.0Merge PDFs, pick and split their pages, count them, and read or set the title and author.8 views · 6 downloadsEPUB epub.qwer · 1.0.0E-books made from chapters of HTML or Markdown, with a title, author, cover and table of contents, and their details read back.8 views · 8 downloadsImages to PDF images_pdf.qwer · 1.0.0Pictures put into a PDF, one per page, fitted and centred, with captions if you like: photo reports, scans and receipts.7 views · 7 downloadsRTF rtf.qwer · 1.0.0RTF documents read as plain text with their title and author, and simple RTF written: headings, bold and italic runs and tables, for Word, Pages and TextEdit.7 views · 8 downloadsText encodings text_encoding.qwer · 1.0.0Text files in old encodings (Windows-1252, Latin-1, Shift_JIS, GBK, Big5...) detected and read as UTF-8, and written back in them.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 downloadsThread 0 posts
On Word documents: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.