oxedyne/daimond/src/wasm/office.rs
19.2 KiB, 1 run
created by r2519314175:989, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! The Office document edge: a `.docx` read into prose the panel and the model can both use. |
| 2 | //! |
| 3 | //! # Why this returns Markdown rather than HTML |
| 4 | //! |
| 5 | //! It would be easy to hand back HTML -- `fe2o3_text::doc::html` renders the same tree -- and it |
| 6 | //! would be worse in three ways at once. |
| 7 | //! |
| 8 | //! The document being read is **a stranger's**. It arrived by mail, or from a share, or the user |
| 9 | //! dragged it in; nothing about it is ours. HTML from a stranger reaching this origin is the risk |
| 10 | //! `viewer.js` spends its opening comment on, and the answer there is a frame with |
| 11 | //! `sandbox="allow-scripts"` and never `allow-same-origin`. Markdown needs none of that, because |
| 12 | //! `DaimondRender.md` already sanitises -- it drops `script style iframe form input button svg` |
| 13 | //! whole -- and it is the sanitiser this app already trusts for agent-written prose. |
| 14 | //! |
| 15 | //! It is also the form the *model* reads best. `## Findings` says what it is; `<h2>` has to be |
| 16 | //! explained, in every request, forever. And it means the Doc panel and `file_read` render the same |
| 17 | //! bytes by the same path, so a user and a model looking at one document are looking at one document. |
| 18 | //! |
| 19 | //! # It says what it did not draw, in PARTS rather than in a sentence |
| 20 | //! |
| 21 | //! A reading view that quietly dropped a chart would be lying by omission, and the number is the |
| 22 | //! whole of the information: a reader told only that *something* is missing has been told that the |
| 23 | //! reader cannot be trusted, and nothing else. So `undrawn` carries a kind and a count for each -- |
| 24 | //! `[{ kind: "TextBox", n: 3 }, { kind: "Chart", n: 1 }]` -- and the panel composes the sentence. |
| 25 | //! |
| 26 | //! Handing over the finished English would have been shorter by twenty lines and wrong. |
| 27 | //! `viewer.js` says of itself that every user-visible string in it goes through `opts.t`, "so the |
| 28 | //! file holds no English of its own that a translation pass cannot reach", and a phrase built in |
| 29 | //! Rust and printed verbatim is exactly the English a translation pass cannot reach. The MODEL is |
| 30 | //! addressed in English by every other tool note and keeps the sentence; the USER is not. |
| 31 | |
| 32 | use crate::wasm::to_js_err; |
| 33 | |
| 34 | use oxedyne_fe2o3_core::prelude::*; |
| 35 | use oxedyne_fe2o3_file::office::docx; |
| 36 | use oxedyne_fe2o3_file::office::edit::Find; |
| 37 | use oxedyne_fe2o3_file::office::odf; |
| 38 | use oxedyne_fe2o3_file::office::sheet::Ref; |
| 39 | use oxedyne_fe2o3_file::office::xlsx; |
| 40 | use oxedyne_fe2o3_text::doc::markdown; |
| 41 | |
| 42 | use wasm_bindgen::JsCast; |
| 43 | use wasm_bindgen::prelude::*; |
| 44 | |
| 45 | /// Reads a `.docx` into the prose it holds. |
| 46 | /// |
| 47 | /// Answers an object: `{ markdown, undrawn, macros, tracked }`. `undrawn` is `null` where everything |
| 48 | /// in the document reached the page, and a ready-made phrase where it did not. |
| 49 | #[wasm_bindgen] |
| 50 | pub fn office_read_doc(bytes: &[u8], media: &str) -> Result<JsValue, JsValue> { |
| 51 | // One door for both vocabularies. What a reader wants out of a text document is the same thing |
| 52 | // whichever it was written in, and the neutral tree is where the two meet -- which is the whole |
| 53 | // point of having one. |
| 54 | if media == "Odt" { |
| 55 | let r = odf::text::read(bytes).map_err(to_js_err)?; |
| 56 | let out = js_sys::Object::new(); |
| 57 | set(&out, "markdown", &JsValue::from_str(&markdown::write::render(&r.doc)))?; |
| 58 | let undrawn = js_sys::Array::new(); |
| 59 | if r.images > 0 { |
| 60 | let one = js_sys::Object::new(); |
| 61 | set(&one, "kind", &JsValue::from_str("image"))?; |
| 62 | set(&one, "n", &JsValue::from_f64(r.images as i32 as f64))?; |
| 63 | undrawn.push(&one); |
| 64 | } |
| 65 | set(&out, "undrawn", &undrawn.into())?; |
| 66 | set(&out, "macros", &JsValue::from_bool(r.macros))?; |
| 67 | set(&out, "tracked", &JsValue::from_f64(0.0))?; |
| 68 | set(&out, "blocks", &JsValue::from_f64(r.doc.blocks.len() as i32 as f64))?; |
| 69 | return Ok(out.into()); |
| 70 | } |
| 71 | let r = docx::read(bytes).map_err(to_js_err)?; |
| 72 | let out = js_sys::Object::new(); |
| 73 | let md = markdown::write::render(&r.doc); |
| 74 | set(&out, "markdown", &JsValue::from_str(&md))?; |
| 75 | let undrawn = js_sys::Array::new(); |
| 76 | for (kind, n) in &r.undrawn { |
| 77 | let one = js_sys::Object::new(); |
| 78 | set(&one, "kind", &JsValue::from_str(name_of(*kind)))?; |
| 79 | set(&one, "n", &JsValue::from_f64(*n as i32 as f64))?; |
| 80 | undrawn.push(&one); |
| 81 | } |
| 82 | set(&out, "undrawn", &undrawn.into())?; |
| 83 | set(&out, "macros", &JsValue::from_bool(r.macros))?; |
| 84 | // `i32` and not `usize`: a `u64` crosses this boundary as a `BigInt`, which every arithmetic |
| 85 | // operation on the JS side then refuses to mix with a number. |
| 86 | set(&out, "tracked", &JsValue::from_f64(r.tracked as i32 as f64))?; |
| 87 | set(&out, "blocks", &JsValue::from_f64(r.doc.blocks.len() as i32 as f64))?; |
| 88 | Ok(out.into()) |
| 89 | } |
| 90 | |
| 91 | /// Reads a `.xlsx` into the sheets it holds. |
| 92 | /// |
| 93 | /// Answers `{ sheets: [{ name, cols, rows, formulas, cut }], macros, missing }`, where each sheet's |
| 94 | /// `rows` is an array of arrays of strings -- the values AS STORED, which is what the person who |
| 95 | /// wrote the file saw. Nothing is recalculated; see `oxedyne_fe2o3_file::office::sheet` for why that |
| 96 | /// is the correct answer and not a missing feature. |
| 97 | /// |
| 98 | /// Each sheet is cut to a rectangle before it crosses the boundary. A sheet may be a hundred |
| 99 | /// thousand rows and the panel can draw a screenful, so sending the rest would cost the copy, the |
| 100 | /// JS heap and the DOM for something nobody looks at. `cut` says whether it happened, and the panel |
| 101 | /// says so on screen -- a silent truncation reads as a corrupt file. |
| 102 | #[wasm_bindgen] |
| 103 | pub fn office_read_sheet( |
| 104 | bytes: &[u8], |
| 105 | media: &str, |
| 106 | max_rows: u32, |
| 107 | max_cols: u32, |
| 108 | ) |
| 109 | -> Result<JsValue, JsValue> |
| 110 | { |
| 111 | use oxedyne_fe2o3_file::office::sheet::{Range, Ref, col_name}; |
| 112 | |
| 113 | // The same door for both, for the same reason as `office_read_doc`. |
| 114 | let (book, macros, missing) = match media { |
| 115 | "Ods" => { |
| 116 | let r = odf::sheet::read(bytes).map_err(to_js_err)?; |
| 117 | (r.book, r.macros, Vec::new()) |
| 118 | } |
| 119 | _ => { |
| 120 | let r = xlsx::read(bytes).map_err(to_js_err)?; |
| 121 | (r.book, r.macros, r.missing) |
| 122 | } |
| 123 | }; |
| 124 | let out = js_sys::Object::new(); |
| 125 | let sheets = js_sys::Array::new(); |
| 126 | for s in &book.sheets { |
| 127 | let one = js_sys::Object::new(); |
| 128 | set(&one, "name", &JsValue::from_str(&s.name))?; |
| 129 | let (rows, cols) = s.size(); |
| 130 | set(&one, "rows", &JsValue::from_f64(rows as i32 as f64))?; |
| 131 | set(&one, "cols", &JsValue::from_f64(cols as i32 as f64))?; |
| 132 | let keep_rows = (rows as u32).min(max_rows.max(1)); |
| 133 | let keep_cols = (cols as u32).min(max_cols.max(1)); |
| 134 | set(&one, "cut", &JsValue::from_bool( |
| 135 | keep_rows as usize != rows || keep_cols as usize != cols))?; |
| 136 | // The column letters, so the grid a person reads is addressed the way a person addresses it |
| 137 | // -- and the way `sheet_read` takes a range, which is the same vocabulary. |
| 138 | let heads = js_sys::Array::new(); |
| 139 | for c in 0..keep_cols { |
| 140 | heads.push(&JsValue::from_str(&col_name(c))); |
| 141 | } |
| 142 | set(&one, "heads", &heads.into())?; |
| 143 | let grid = js_sys::Array::new(); |
| 144 | let mut formulas = 0usize; |
| 145 | if keep_rows > 0 && keep_cols > 0 { |
| 146 | let window = s.window(&Range { |
| 147 | from: Ref { col: 0, row: 0 }, |
| 148 | to: Ref { col: keep_cols - 1, row: keep_rows - 1 }, |
| 149 | }); |
| 150 | for line in &window { |
| 151 | let js_line = js_sys::Array::new(); |
| 152 | for cell in line { |
| 153 | if cell.formula.is_some() { |
| 154 | formulas += 1; |
| 155 | } |
| 156 | js_line.push(&JsValue::from_str(&cell.value.show())); |
| 157 | } |
| 158 | grid.push(&js_line.into()); |
| 159 | } |
| 160 | } |
| 161 | set(&one, "cells", &grid.into())?; |
| 162 | set(&one, "formulas", &JsValue::from_f64(formulas as i32 as f64))?; |
| 163 | sheets.push(&one); |
| 164 | } |
| 165 | set(&out, "sheets", &sheets.into())?; |
| 166 | set(&out, "macros", &JsValue::from_bool(macros))?; |
| 167 | let gone = js_sys::Array::new(); |
| 168 | for name in &missing { |
| 169 | gone.push(&JsValue::from_str(name)); |
| 170 | } |
| 171 | set(&out, "missing", &gone.into())?; |
| 172 | Ok(out.into()) |
| 173 | } |
| 174 | |
| 175 | /// Writes Markdown out as the bytes of a `.docx`. |
| 176 | /// |
| 177 | /// The app action behind "save as Word". **The model does not emit document XML and there is no tool |
| 178 | /// that lets it**: it writes Markdown, which it does well, and the conversion from there is |
| 179 | /// deterministic code. Every design where the model emits the format puts it in the position of |
| 180 | /// getting a file format right, which it will sometimes not; this removes the failure class rather |
| 181 | /// than mitigating it. |
| 182 | #[wasm_bindgen] |
| 183 | pub fn office_write_docx(md: &str) -> Result<Vec<u8>, JsValue> { |
| 184 | let doc = markdown::parse(md).map_err(to_js_err)?; |
| 185 | let (bytes, _left) = docx::write(&doc).map_err(to_js_err)?; |
| 186 | Ok(bytes) |
| 187 | } |
| 188 | |
| 189 | /// Writes Markdown out as the bytes of whichever Office format is named. |
| 190 | /// |
| 191 | /// The one door for "save as a document", and the reason there is one rather than six is the argument |
| 192 | /// `office_write_docx` already makes: **the model never emits document XML**. It writes Markdown, the |
| 193 | /// conversion is deterministic code, and the failure class where a model gets a file format slightly |
| 194 | /// wrong does not exist rather than being mitigated. |
| 195 | /// |
| 196 | /// What each format makes of the same prose differs, and the difference is not a loss: a `.docx` and an |
| 197 | /// `.odt` take the whole document; a `.pptx` and an `.odp` split it at its headings into slides; a |
| 198 | /// `.xlsx` and an `.ods` take its TABLES, one sheet each, because a spreadsheet built out of paragraphs |
| 199 | /// would be a spreadsheet with one long column in it. |
| 200 | #[wasm_bindgen] |
| 201 | pub fn office_write(md: &str, media: &str) -> Result<Vec<u8>, JsValue> { |
| 202 | use oxedyne_fe2o3_file::office::deck::Deck; |
| 203 | use oxedyne_fe2o3_file::office::pptx; |
| 204 | use oxedyne_fe2o3_file::office::sheet::Book; |
| 205 | use oxedyne_fe2o3_file::office::xlsx; |
| 206 | |
| 207 | let doc = markdown::parse(md).map_err(to_js_err)?; |
| 208 | let bytes = match media { |
| 209 | "Docx" => docx::write(&doc).map_err(to_js_err)?.0, |
| 210 | "Odt" => odf::text::write(&doc).map_err(to_js_err)?.0, |
| 211 | "Xlsx" => xlsx::write(&Book::from_doc(&doc)).map_err(to_js_err)?, |
| 212 | "Ods" => odf::sheet::write(&Book::from_doc(&doc)).map_err(to_js_err)?, |
| 213 | "Pptx" => pptx::write(&Deck::from_doc(&doc)).map_err(to_js_err)?.0, |
| 214 | "Odp" => odf::slides::write(&Deck::from_doc(&doc)).map_err(to_js_err)?.0, |
| 215 | other => return Err(to_js_err(err!( |
| 216 | "'{}' is not a document format this writes. It writes Docx, Odt, Xlsx, Ods, Pptx and \ |
| 217 | Odp.", other; Invalid, Input, Unimplemented))), |
| 218 | }; |
| 219 | Ok(bytes) |
| 220 | } |
| 221 | |
| 222 | /// Replaces text in a document that already exists, leaving every other byte of it alone. |
| 223 | /// |
| 224 | /// `edits` is JSON: `[{"find": "...", "replace": "...", "nth": 1}]`, `nth` optional and counted from |
| 225 | /// one through the whole document. **An unmatched `find` is an error naming the string** and NOTHING is |
| 226 | /// written -- a caller told "the document was edited" has no way to discover that one of its four |
| 227 | /// replacements did nothing, so a silent no-op would be reported to the user as a change. |
| 228 | /// |
| 229 | /// Only `.docx` and `.odt`. A deck is not edited here and it is not an oversight: a slide is a position |
| 230 | /// on a canvas, and changing the words without knowing the geometry puts text over other text. A |
| 231 | /// spreadsheet is edited by `office_edit_sheet`, whose unit is a cell. |
| 232 | #[wasm_bindgen] |
| 233 | pub fn office_edit_doc(bytes: &[u8], media: &str, edits: &str) -> Result<Vec<u8>, JsValue> { |
| 234 | let asks = finds_of(edits)?; |
| 235 | let out = match media { |
| 236 | "Odt" => odf::text::edit(bytes, &asks).map_err(to_js_err)?.bytes, |
| 237 | "Docx" => docx::edit::edit(bytes, &asks).map_err(to_js_err)?.bytes, |
| 238 | other => return Err(to_js_err(err!( |
| 239 | "'{}' is not a format whose text can be edited in place. Word and OpenDocument text \ |
| 240 | documents can; a presentation cannot, because a slide is a position on a canvas and \ |
| 241 | changing its words without knowing the geometry puts text over other text.", other; |
| 242 | Invalid, Input, Unimplemented))), |
| 243 | }; |
| 244 | Ok(out) |
| 245 | } |
| 246 | |
| 247 | /// Writes cells into a spreadsheet that already exists. |
| 248 | /// |
| 249 | /// `edits` is JSON: `[{"sheet": "Sheet1", "ref": "B2", "value": "3.5"}]`, or `"formula": "=B2*C2"` in |
| 250 | /// place of the value. `sheet` may be left out for the first sheet. A `ref` outside the sheet is |
| 251 | /// written rather than refused -- the sheet grows -- but a SHEET that is not there is an error naming |
| 252 | /// what sheets the workbook has. |
| 253 | #[wasm_bindgen] |
| 254 | pub fn office_edit_sheet(bytes: &[u8], media: &str, edits: &str) -> Result<Vec<u8>, JsValue> { |
| 255 | use oxedyne_fe2o3_file::office::xlsx; |
| 256 | |
| 257 | let asks = cells_of(edits)?; |
| 258 | let out = match media { |
| 259 | "Ods" => { |
| 260 | let sets: Vec<odf::sheet::Set> = asks.into_iter() |
| 261 | .map(|(sheet, at, value, formula)| odf::sheet::Set { sheet, at, value, formula }) |
| 262 | .collect(); |
| 263 | odf::sheet::edit(bytes, &sets).map_err(to_js_err)?.bytes |
| 264 | } |
| 265 | "Xlsx" => { |
| 266 | let sets: Vec<xlsx::edit::Set> = asks.into_iter() |
| 267 | .map(|(sheet, at, value, formula)| xlsx::edit::Set { sheet, at, value, formula }) |
| 268 | .collect(); |
| 269 | xlsx::edit::edit(bytes, &sets).map_err(to_js_err)?.bytes |
| 270 | } |
| 271 | other => return Err(to_js_err(err!( |
| 272 | "'{}' is not a spreadsheet, so it has no cells to write.", other; |
| 273 | Invalid, Input, Unimplemented))), |
| 274 | }; |
| 275 | Ok(out) |
| 276 | } |
| 277 | |
| 278 | /// The find-and-replace edits a JSON array asks for. |
| 279 | /// |
| 280 | /// The browser's own parser rather than a second one written here: this JSON carries a user's prose, |
| 281 | /// and a hand-rolled scan would be a second unescaping implementation to keep exactly in step with the |
| 282 | /// first. |
| 283 | fn finds_of(edits: &str) -> Result<Vec<Find>, JsValue> { |
| 284 | let list = array_of(edits, "an edit")?; |
| 285 | let mut out = Vec::new(); |
| 286 | for one in list.iter() { |
| 287 | let find = str_of(&one, "find").unwrap_or_default(); |
| 288 | if find.is_empty() { |
| 289 | return Err(to_js_err(err!( |
| 290 | "An edit carries no 'find', so there is nothing to look for."; Invalid, Input))); |
| 291 | } |
| 292 | out.push(Find { |
| 293 | find, |
| 294 | replace: str_of(&one, "replace").unwrap_or_default(), |
| 295 | // A zero would ask for an occurrence before the first, which the edit itself refuses in |
| 296 | // terms that say so; it is not silently turned into "every". |
| 297 | nth: num_of(&one, "nth").map(|n| n as usize), |
| 298 | }); |
| 299 | } |
| 300 | if out.is_empty() { |
| 301 | return Err(to_js_err(err!("No edits were given, so there is nothing to do."; Invalid, Input))); |
| 302 | } |
| 303 | Ok(out) |
| 304 | } |
| 305 | |
| 306 | /// The cells a JSON array asks to be written. |
| 307 | fn cells_of(edits: &str) -> Result<Vec<(Option<String>, Ref, Option<String>, Option<String>)>, JsValue> { |
| 308 | let list = array_of(edits, "a cell")?; |
| 309 | let mut out = Vec::new(); |
| 310 | for one in list.iter() { |
| 311 | let at = str_of(&one, "ref").unwrap_or_default(); |
| 312 | let at = Ref::parse(&at).map_err(to_js_err)?; |
| 313 | let value = str_of(&one, "value"); |
| 314 | let formula = str_of(&one, "formula"); |
| 315 | if value.is_none() && formula.is_none() { |
| 316 | return Err(to_js_err(err!( |
| 317 | "The write to {} carries neither a value nor a formula, so it says nothing. To empty \ |
| 318 | a cell, give it a value of \"\".", at.name(); Invalid, Input))); |
| 319 | } |
| 320 | out.push((str_of(&one, "sheet").filter(|s| !s.trim().is_empty()), at, value, formula)); |
| 321 | } |
| 322 | if out.is_empty() { |
| 323 | return Err(to_js_err(err!("No cells were given, so there is nothing to write."; Invalid, Input))); |
| 324 | } |
| 325 | Ok(out) |
| 326 | } |
| 327 | |
| 328 | /// The JSON as the array of objects it has to be. |
| 329 | fn array_of(json: &str, what: &str) -> Result<js_sys::Array, JsValue> { |
| 330 | let val = js_sys::JSON::parse(json).map_err(|_| to_js_err(err!( |
| 331 | "The edits are not JSON. They are a list of objects, one for each {}.", what; |
| 332 | Invalid, Input)))?; |
| 333 | match val.dyn_into::<js_sys::Array>() { |
| 334 | Ok(a) => Ok(a), |
| 335 | Err(_) => Err(to_js_err(err!( |
| 336 | "The edits are not a LIST. They are a JSON array of objects, one for each {}.", what; |
| 337 | Invalid, Input))), |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | /// One string property of an object, absent where it is not a string. |
| 342 | fn str_of(obj: &JsValue, key: &str) -> Option<String> { |
| 343 | js_sys::Reflect::get(obj, &JsValue::from_str(key)).ok().and_then(|v| v.as_string()) |
| 344 | } |
| 345 | |
| 346 | /// One number property of an object. |
| 347 | fn num_of(obj: &JsValue, key: &str) -> Option<f64> { |
| 348 | js_sys::Reflect::get(obj, &JsValue::from_str(key)).ok().and_then(|v| v.as_f64()) |
| 349 | } |
| 350 | |
| 351 | /// What a document written from this Markdown would leave out, in PARTS, or nothing. |
| 352 | /// |
| 353 | /// Asked before the write rather than after, so a caller can warn before the file exists rather than |
| 354 | /// explain after it does. |
| 355 | /// |
| 356 | /// # It answers in parts and not in a sentence, and that is not a style preference |
| 357 | /// |
| 358 | /// This used to compose the finished English -- *"1 image is not carried into the document: cover.png"* |
| 359 | /// -- which is exactly the mistake `office_read_doc` was built to avoid, twenty lines above. `viewer.js` |
| 360 | /// says of itself that every user-visible string in it goes through `opts.t`, "so the file holds no |
| 361 | /// English of its own that a translation pass cannot reach", and a phrase built in Rust and printed |
| 362 | /// verbatim is precisely the English a translation pass cannot reach. So `undrawn` carries a kind and a |
| 363 | /// count and the panel composes the sentence, and this now does the same: `[{ kind, n, names }]`. |
| 364 | /// |
| 365 | /// One export kept the rule and its sibling broke it, which is what happens when the rule lives in a |
| 366 | /// comment rather than in the shape of the answer. |
| 367 | /// |
| 368 | /// `names` is the source each image was written with -- a path out of the user's own Markdown, not |
| 369 | /// English -- so it passes through as it is, and a caller that wants to say *which* file has it. |
| 370 | #[wasm_bindgen] |
| 371 | pub fn office_write_left(md: &str, media: &str) -> Result<JsValue, JsValue> { |
| 372 | use oxedyne_fe2o3_file::office::deck::Deck; |
| 373 | use oxedyne_fe2o3_file::office::pptx; |
| 374 | |
| 375 | let doc = markdown::parse(md).map_err(to_js_err)?; |
| 376 | // A spreadsheet has no `Left` of its own: what it leaves out is everything in the document that is |
| 377 | // not a table, which is a different question and one `office_write`'s own note answers. |
| 378 | let (images, notes) = match media { |
| 379 | "Docx" => (docx::write(&doc).map_err(to_js_err)?.1.images, 0), |
| 380 | "Odt" => (odf::text::write(&doc).map_err(to_js_err)?.1.images, 0), |
| 381 | "Pptx" => { |
| 382 | let left = pptx::write(&Deck::from_doc(&doc)).map_err(to_js_err)?.1; |
| 383 | (left.images, left.notes) |
| 384 | } |
| 385 | "Odp" => { |
| 386 | let left = odf::slides::write(&Deck::from_doc(&doc)).map_err(to_js_err)?.1; |
| 387 | (left.images, left.notes) |
| 388 | } |
| 389 | "Xlsx" | "Ods" => (Vec::new(), 0), |
| 390 | other => return Err(to_js_err(err!( |
| 391 | "'{}' is not a document format this writes, so there is nothing to say about what it \ |
| 392 | would leave out.", other; Invalid, Input, Unimplemented))), |
| 393 | }; |
| 394 | if images.is_empty() && notes == 0 { |
| 395 | return Ok(JsValue::NULL); |
| 396 | } |
| 397 | let out = js_sys::Array::new(); |
| 398 | if !images.is_empty() { |
| 399 | let one = js_sys::Object::new(); |
| 400 | set(&one, "kind", &JsValue::from_str("image"))?; |
| 401 | // `i32` and not `usize`, for the reason `office_read_doc` gives: a `u64` crosses this boundary |
| 402 | // as a `BigInt`, which every arithmetic operation on the JS side then refuses to mix. |
| 403 | set(&one, "n", &JsValue::from_f64(images.len() as i32 as f64))?; |
| 404 | let names = js_sys::Array::new(); |
| 405 | for name in &images { |
| 406 | names.push(&JsValue::from_str(name)); |
| 407 | } |
| 408 | set(&one, "names", &names.into())?; |
| 409 | out.push(&one); |
| 410 | } |
| 411 | if notes > 0 { |
| 412 | let one = js_sys::Object::new(); |
| 413 | set(&one, "kind", &JsValue::from_str("notes"))?; |
| 414 | set(&one, "n", &JsValue::from_f64(notes as i32 as f64))?; |
| 415 | set(&one, "names", &js_sys::Array::new().into())?; |
| 416 | out.push(&one); |
| 417 | } |
| 418 | Ok(out.into()) |
| 419 | } |
| 420 | |
| 421 | /// The name a kind travels under, which is the key the panel looks its wording up by. |
| 422 | /// |
| 423 | /// Written out rather than derived from `Debug`, because a name a translation file is keyed on is |
| 424 | /// part of the interface and must not change because somebody renamed a variant. |
| 425 | fn name_of(kind: docx::read::Undrawable) -> &'static str { |
| 426 | use docx::read::Undrawable as U; |
| 427 | match kind { |
| 428 | U::Image => "image", |
| 429 | U::Chart => "chart", |
| 430 | U::Diagram => "diagram", |
| 431 | U::TextBox => "textbox", |
| 432 | U::Object => "object", |
| 433 | U::Equation => "equation", |
| 434 | U::Footnote => "footnote", |
| 435 | U::Endnote => "endnote", |
| 436 | U::Comment => "comment", |
| 437 | } |
| 438 | } |
| 439 | |
| 440 | /// Sets one property on an object, turning a failed set into a rejection rather than dropping it. |
| 441 | fn set(obj: &js_sys::Object, key: &str, value: &JsValue) -> Result<(), JsValue> { |
| 442 | match js_sys::Reflect::set(obj, &JsValue::from_str(key), value) { |
| 443 | Ok(_) => Ok(()), |
| 444 | Err(e) => Err(e), |
| 445 | } |
| 446 | } |