Files

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

#word#text#import#search

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 error

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.

docx_paragraphs(path: str) → listor an error

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.

docx_tables(path: str) → listor an error

The tables of a .docx, each a list of rows, each row a list of cell texts.

docx_properties(path: str) → anyor an error

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.

Get it

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

Or 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.docx

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

main.qwe
use docx_text.qweruse docx_text, docx_paragraphs from docx_text      // Quillo 0.6: only these

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

[dependencies]
zip = { version = "2", default-features = false, features = ["deflate"] }roxmltree = "0.20"

Example

docx_text.qwe
// 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

docx_text.qwer
//! 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)}
Thread
79Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.