Word text
The text of Word documents: all of it, paragraph by paragraph with their styles, the tables as rows, and the title and author.
Version 1.0.0 · 4 functions · 2 crates
Functions
What use docx_text.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use docx_text, docx_paragraphs from docx_text (Quillo 0.6).
docx_text(path: str) → stror an errorAll the text of a .docx: a line for each paragraph, table rows with their cells parted by tabs. Deleted (tracked) text is left out.
docx_paragraphs(path: str) → listor an errorThe paragraphs outside tables, as { style, heading, list, text }: style the Word style's id ("Heading1", or null), heading its level (0 for Title, 1 to 9, else null), list true for list items. Empty paragraphs are left out: for importing a document's outline.
docx_tables(path: str) → listor an errorThe tables of a .docx, each a list of rows, each row a list of cell texts.
docx_properties(path: str) → anyor an errorWhat the file says about itself: { title, subject, creator, last_modified_by, created, modified, words, pages }, null when missing. Works for .xlsx and .pptx files too, but for words and pages.
Get it
$ quillo install docx_text # docx_text.qwer into this folder$ quillo install docx_text --example # and its example, docx_text.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/docx_text.qwer$ curl -O https://quillolang.org/plugins/files/docx_text.qwe$ curl -O https://quillolang.org/plugins/files/docx_text_sample.docxThen, in your program, all of it or only what you call:
use docx_text.qweruse docx_text, docx_paragraphs from docx_text // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
zip = { version = "2", default-features = false, features = ["deflate"] }roxmltree = "0.20"Example
// A Word document read for an import: its text, its outline and its table.//// quillo run docx_text.qwe then open http://localhost:8080/readuse "docx_text.qwer"let DOC = "docx_text_sample.docx"route GET /read = { text: docx_text(DOC)?, properties: docx_properties(DOC)? }route GET /outline = docx_paragraphs(DOC)? | filter((p) => p.heading != null) | map((p) => p.text)// The table's rows as records, its header row left out.route GET /hours { let rows = docx_tables(DOC)?[0] drop(rows, 1) | map((r) => { day: r[0], hours: r[1] })}$ quillo run docx_text.qwe$ curl 'localhost:8080/read'$ curl 'localhost:8080/outline'$ curl 'localhost:8080/hours'Source
//! The text of Word documents (.docx): all of it, paragraph by paragraph with their styles, the tables as rows, and the title and author.//! version: 1.0.0//!//! [dependencies]//! zip = { version = "2", default-features = false, features = ["deflate"] }//! roxmltree = "0.20"use serde_json::{json, Value};use std::io::Read;const MAX_XML: u64 = 64 * 1024 * 1024;fn part(path: &str, name: &str) -> Result<Option<String>, String> { let f = std::fs::File::open(path).map_err(|e| format!("{}: {}", path, e))?; let mut z = zip::ZipArchive::new(f).map_err(|_| format!("{}: not a .docx file (it is not a zip)", path))?; let Ok(entry) = z.by_name(name) else { return Ok(None) }; let mut s = String::new(); entry.take(MAX_XML).read_to_string(&mut s).map_err(|e| format!("{}: {}", path, e))?; Ok(Some(s))}fn document(path: &str) -> Result<String, String> { part(path, "word/document.xml")?.ok_or_else(|| format!("{}: not a Word document (no word/document.xml)", path))}fn is(n: &roxmltree::Node, name: &str) -> bool { n.is_element() && n.tag_name().name() == name}fn attr(n: &roxmltree::Node, name: &str) -> Option<String> { n.attributes().find(|a| a.name() == name).map(|a| a.value().to_string())}fn para_text(p: &roxmltree::Node) -> String { let mut s = String::new(); for d in p.descendants() { if is(&d, "t") { s.push_str(d.text().unwrap_or("")); } else if is(&d, "tab") && d.parent().map_or(false, |x| is(&x, "r")) { s.push('\t'); } else if is(&d, "br") || is(&d, "cr") { s.push('\n'); } } s}fn para_style(p: &roxmltree::Node) -> Option<String> { p.children().find(|c| is(c, "pPr")).and_then(|ppr| ppr.children().find(|c| is(c, "pStyle"))).and_then(|s| attr(&s, "val"))}fn table_rows(t: &roxmltree::Node) -> Vec<Vec<String>> { t.children() .filter(|r| is(r, "tr")) .map(|r| { r.children() .filter(|c| is(c, "tc")) .map(|c| c.descendants().filter(|p| is(p, "p")).map(|p| para_text(&p)).collect::<Vec<_>>().join("\n").trim().to_string()) .collect() }) .collect()}enum Block { Para(Option<String>, bool, String), Table(Vec<Vec<String>>),}// The body's paragraphs and tables in order; content controls (w:sdt) are looked into.fn blocks(xml: &str) -> Result<Vec<Block>, String> { let doc = roxmltree::Document::parse_with_options(xml, roxmltree::ParsingOptions { allow_dtd: false, nodes_limit: 20_000_000 }).map_err(|e| format!("the document's XML: {}", e))?; let body = doc.descendants().find(|n| is(n, "body")).ok_or("the document has no body")?; let mut out = vec![]; fn walk(n: roxmltree::Node, out: &mut Vec<Block>) { for c in n.children() { if is(&c, "p") { let list = c.descendants().any(|d| is(&d, "numPr")); out.push(Block::Para(para_style(&c), list, para_text(&c))); } else if is(&c, "tbl") { out.push(Block::Table(table_rows(&c))); } else if is(&c, "sdt") || is(&c, "sdtContent") || is(&c, "customXml") { walk(c, out); } } } walk(body, &mut out); Ok(out)}fn heading_level(style: &str) -> Option<i64> { let s = style.to_lowercase().replace(' ', ""); if s == "title" { return Some(0); } s.strip_prefix("heading").and_then(|n| n.parse().ok())}/// All the text of a .docx: a line for each paragraph, table rows with their cells parted by tabs. Deleted (tracked) text is left out.pub fn docx_text(path: &str) -> Result<String, String> { let mut lines: Vec<String> = vec![]; for b in blocks(&document(path)?)? { match b { Block::Para(_, _, t) => lines.push(t), Block::Table(rows) => lines.extend(rows.into_iter().map(|r| r.join("\t"))), } } let text = lines.join("\n"); Ok(text.trim().to_string())}/// The paragraphs outside tables, as `{ style, heading, list, text }`: `style` the Word style's id ("Heading1", or null), `heading` its level (0 for Title, 1 to 9, else null), `list` true for list items./// Empty paragraphs are left out: for importing a document's outline.pub fn docx_paragraphs(path: &str) -> Result<Vec<Value>, String> { Ok(blocks(&document(path)?)? .into_iter() .filter_map(|b| match b { Block::Para(style, list, text) if !text.trim().is_empty() => { let heading = style.as_deref().and_then(heading_level); Some(json!({ "style": style, "heading": heading, "list": list, "text": text })) } _ => None, }) .collect())}/// The tables of a .docx, each a list of rows, each row a list of cell texts.pub fn docx_tables(path: &str) -> Result<Vec<Vec<Vec<String>>>, String> { Ok(blocks(&document(path)?)?.into_iter().filter_map(|b| if let Block::Table(t) = b { Some(t) } else { None }).collect())}/// What the file says about itself: `{ title, subject, creator, last_modified_by, created, modified, words, pages }`, null when missing./// Works for .xlsx and .pptx files too, but for `words` and `pages`.pub fn docx_properties(path: &str) -> Result<Value, String> { let mut v = json!({ "title": null, "subject": null, "creator": null, "last_modified_by": null, "created": null, "modified": null, "words": null, "pages": null }); if let Some(core) = part(path, "docProps/core.xml")? { if let Ok(doc) = roxmltree::Document::parse(&core) { for (key, name) in [("title", "title"), ("subject", "subject"), ("creator", "creator"), ("last_modified_by", "lastModifiedBy"), ("created", "created"), ("modified", "modified")] { if let Some(t) = doc.descendants().find(|n| is(n, name)).and_then(|n| n.text()) { v[key] = Value::String(t.trim().to_string()); } } } } if let Some(app) = part(path, "docProps/app.xml")? { if let Ok(doc) = roxmltree::Document::parse(&app) { for (key, name) in [("words", "Words"), ("pages", "Pages")] { if let Some(n) = doc.descendants().find(|n| is(n, name)).and_then(|n| n.text()).and_then(|t| t.trim().parse::<i64>().ok()) { v[key] = n.into(); } } } } Ok(v)}Related
odf_text.qwer · 1.0.0The text of LibreOffice files (.odt, .ods, .odp): paragraphs with headings and lists, sheets as rows of typed cells, slides with their notes, and the title and author.7 views · 9 downloadsPDF text pdf_text.qwer · 1.0.0The text of a PDF, whole or by page, and the lines holding a word: for search, imports and reading invoices.7 views · 6 downloadsPowerPoint text pptx_text.qwer · 1.0.0The text of PowerPoint files: each slide's title, text and speaker notes in slide order, for search and imports.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 downloads7-Zip sevenz.qwer · 1.0.07-Zip archives listed, one file read, opened safely into a folder, and made from a folder.7 views · 8 downloadsThread 0 posts
On Word text: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.