diff --git a/Cargo.lock b/Cargo.lock index a39c47d..10cb54e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,7 +151,25 @@ dependencies = [ ] [[package]] -name = "docx-core" +name = "docx-wasm" +version = "0.1.0" +dependencies = [ + "extend-docx", + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-test", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "extend-docx" version = "0.1.0" dependencies = [ "arc-swap", @@ -173,24 +191,6 @@ dependencies = [ "zlib-rs", ] -[[package]] -name = "docx-wasm" -version = "0.1.0" -dependencies = [ - "docx-core", - "js-sys", - "serde", - "serde_json", - "wasm-bindgen", - "wasm-bindgen-test", -] - -[[package]] -name = "either" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" - [[package]] name = "find-msvc-tools" version = "0.1.9" diff --git a/Cargo.toml b/Cargo.toml index c00f0d3..ec4c8e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/docx-core", "crates/docx-wasm"] +members = ["crates/extend-docx", "crates/docx-wasm"] resolver = "2" [workspace.package] diff --git a/README.md b/README.md index e1e04a8..c8f81af 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # react-docx [![npm version](https://img.shields.io/npm/v/%40extend-ai%2Freact-docx)](https://www.npmjs.com/package/@extend-ai/react-docx) +[![crates.io version](https://img.shields.io/crates/v/extend-docx)](https://crates.io/crates/extend-docx) `react-docx` is a DOCX viewer and editor for JavaScript and React applications, built as a monorepo. @@ -23,6 +24,14 @@ pnpm add @extend-ai/react-docx For React integrations, also install `react` and `react-dom`. The vanilla entry bundles Preact and does not require React. +For native Rust parsing, structured extraction, and serialization, use [`extend-docx`](./crates/extend-docx/README.md): + +```bash +cargo add extend-docx +``` + +Import it as `extend_docx` in Rust. The Rust crate and npm packages have independent release versions. + ## WebAssembly and Vite DOCX parsing and export use a WebAssembly binary shipped with the package. It loads on demand, and most bundlers can resolve it automatically. To host it at a specific URL, copy the binary to your application's public directory: @@ -459,6 +468,8 @@ const output = serializeDocx(model, pkg); ## Workspace Layout +- `crates/extend-docx`: the published Rust parser, document model, and serializer, `extend-docx` +- `crates/docx-wasm`: internal WebAssembly bindings for the JavaScript viewer - `packages/react-viewer`: the main published React package, `@extend-ai/react-docx` - `packages/ooxml-core`: OOXML package parsing and part graph handling - `packages/doc-model`: normalized document model @@ -522,7 +533,7 @@ pnpm test:docx-vs-libreoffice ## Publishing -Changesets are used for release management: +Changesets are used for npm release management: ```bash pnpm changeset @@ -530,3 +541,5 @@ pnpm version-packages pnpm build pnpm publish-packages ``` + +The Rust crate is released independently with `cargo publish -p extend-docx --locked`. See [Rust publishing](docs/rust-publishing.md) for validation and GitHub team ownership. diff --git a/crates/docx-core/examples/dump_doc_text.rs b/crates/docx-core/examples/dump_doc_text.rs deleted file mode 100644 index 50eb352..0000000 --- a/crates/docx-core/examples/dump_doc_text.rs +++ /dev/null @@ -1,37 +0,0 @@ -fn main() { - let path = std::env::args().nth(1).expect("path"); - let bytes = std::fs::read(&path).expect("read"); - let pkg = if path.ends_with(".doc") { - docx_core::parse_document_bytes(&bytes).expect("parse") - } else { - docx_core::parse_docx(&bytes).expect("parse_docx") - }; - let model = docx_core::build_doc_model(&pkg); - print!("{}", text(&model)); -} -fn text(model: &docx_core::DocModel) -> String { - let mut out = String::new(); - for node in &model.nodes { walk(node, &mut out); } - out -} -fn walk(node: &docx_core::DocNode, out: &mut String) { - match node { - docx_core::DocNode::Paragraph(p) => { - for c in &p.children { - if let docx_core::ParagraphChildNode::Text(r) = c { out.push_str(&r.text); } - } - out.push('\n'); - } - docx_core::DocNode::Table(t) => { - for row in &t.rows { for cell in &row.cells { for n in &cell.nodes { - match n { - docx_core::TableCellContentNode::Paragraph(p) => { - for c in &p.children { if let docx_core::ParagraphChildNode::Text(r) = c { out.push_str(&r.text); } } - out.push('\n'); - } - docx_core::TableCellContentNode::Table(t2) => walk(&docx_core::DocNode::Table(*t2.clone()), out), - } - } } } - } - } -} diff --git a/crates/docx-wasm/Cargo.toml b/crates/docx-wasm/Cargo.toml index 4e36dd7..a8ab323 100644 --- a/crates/docx-wasm/Cargo.toml +++ b/crates/docx-wasm/Cargo.toml @@ -3,12 +3,14 @@ name = "docx-wasm" version.workspace = true edition.workspace = true license.workspace = true +description = "Internal WebAssembly bindings for extend-docx and the React document viewer" +publish = false [lib] crate-type = ["cdylib", "rlib"] [dependencies] -docx-core = { path = "../docx-core" } +extend-docx = { path = "../extend-docx" } serde.workspace = true serde_json = { workspace = true, features = ["float_roundtrip"] } wasm-bindgen = "0.2" diff --git a/crates/docx-wasm/src/lib.rs b/crates/docx-wasm/src/lib.rs index 18dcfb0..55be97c 100644 --- a/crates/docx-wasm/src/lib.rs +++ b/crates/docx-wasm/src/lib.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use docx_core::{ +use extend_docx::{ build_doc_model, model_to_document_xml, package_to_bytes, parse_document_bytes, try_serialize_doc_model, DocModel, ImageRunNode, OoxmlPackage, OoxmlPart, }; @@ -22,7 +22,7 @@ fn doc_model_from_json(json_str: &str) -> Result { #[wasm_bindgen] pub fn extract_document_blocks_wasm(model_json: &str) -> Result { let model = doc_model_from_json(model_json)?; - serde_json::to_string(&docx_core::extract_document_blocks(&model)) + serde_json::to_string(&extend_docx::extract_document_blocks(&model)) .map_err(|error| JsValue::from_str(&error.to_string())) } @@ -35,7 +35,7 @@ pub fn extract_document_blocks_with_options_wasm( let options = serde_json::from_str(options_json).map_err(|error| { JsValue::from_str(&format!("Invalid block extraction options JSON: {error}")) })?; - serde_json::to_string(&docx_core::extract_document_blocks_with_options( + serde_json::to_string(&extend_docx::extract_document_blocks_with_options( &model, options, )) .map_err(|error| JsValue::from_str(&error.to_string())) @@ -44,7 +44,7 @@ pub fn extract_document_blocks_with_options_wasm( #[wasm_bindgen] pub fn inspect_document_source_wasm(bytes: &[u8]) -> Result { let package = parse_document_bytes(bytes).map_err(|error| JsValue::from_str(&error))?; - serde_json::to_string(&docx_core::document_source_graph(&package)) + serde_json::to_string(&extend_docx::document_source_graph(&package)) .map_err(|error| JsValue::from_str(&error.to_string())) } @@ -86,10 +86,10 @@ pub fn can_preserve_image_paint_source_wasm( None => None, }; let owner = owner_part_name.unwrap_or_else(|| match base.as_ref() { - Some(package) => docx_core::main_document_part_name(package), - None => docx_core::main_document_part_name(&docx_core::create_minimal_docx_package(None)), + Some(package) => extend_docx::main_document_part_name(package), + None => extend_docx::main_document_part_name(&extend_docx::create_minimal_docx_package(None)), }); - Ok(docx_core::can_preserve_image_paint_source(&image, base.as_ref(), &owner)) + Ok(extend_docx::can_preserve_image_paint_source(&image, base.as_ref(), &owner)) } fn js_bytes_to_vec(value: &JsValue, key: &str) -> Result, JsValue> { @@ -273,7 +273,7 @@ pub fn serialize_docx_wasm( base_package: Option, ) -> Result, JsValue> { let doc_model = value_to_doc_model(model)?; - docx_core::table_grid::validate_collapsed_table_grids(&doc_model) + extend_docx::table_grid::validate_collapsed_table_grids(&doc_model) .map_err(|error| JsValue::from_str(&error))?; let base = match base_package { Some(value) => Some(value_to_ooxml_package(&value)?), @@ -290,7 +290,7 @@ pub fn serialize_docx_from_json_wasm( base_package: Option, ) -> Result, JsValue> { let doc_model = doc_model_from_json(model_json)?; - docx_core::table_grid::validate_collapsed_table_grids(&doc_model) + extend_docx::table_grid::validate_collapsed_table_grids(&doc_model) .map_err(|error| JsValue::from_str(&error))?; let base = match base_package { Some(value) => Some(value_to_ooxml_package(&value)?), @@ -307,13 +307,13 @@ pub fn model_to_document_xml_wasm( base_package: Option, ) -> Result { let doc_model = value_to_doc_model(model)?; - docx_core::table_grid::validate_collapsed_table_grids(&doc_model) + extend_docx::table_grid::validate_collapsed_table_grids(&doc_model) .map_err(|error| JsValue::from_str(&error))?; let base = match base_package { Some(value) => Some(value_to_ooxml_package(&value)?), None => None, }; - docx_core::validate_image_paint_bounds(&doc_model, base.as_ref()) + extend_docx::validate_image_paint_bounds(&doc_model, base.as_ref()) .map_err(|error| JsValue::from_str(&error))?; Ok(model_to_document_xml(&doc_model, base.as_ref())) } @@ -324,13 +324,13 @@ pub fn model_to_document_xml_from_json_wasm( base_package: Option, ) -> Result { let doc_model = doc_model_from_json(model_json)?; - docx_core::table_grid::validate_collapsed_table_grids(&doc_model) + extend_docx::table_grid::validate_collapsed_table_grids(&doc_model) .map_err(|error| JsValue::from_str(&error))?; let base = match base_package { Some(value) => Some(value_to_ooxml_package(&value)?), None => None, }; - docx_core::validate_image_paint_bounds(&doc_model, base.as_ref()) + extend_docx::validate_image_paint_bounds(&doc_model, base.as_ref()) .map_err(|error| JsValue::from_str(&error))?; Ok(model_to_document_xml(&doc_model, base.as_ref())) } diff --git a/crates/extend-docx/CHANGELOG.md b/crates/extend-docx/CHANGELOG.md new file mode 100644 index 0000000..e7eb47b --- /dev/null +++ b/crates/extend-docx/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## 0.1.0 + +Initial crates.io release of Extend's shared Rust parser, previously named `docx-core` within the repository. + +- Publish the Cargo package as `extend-docx`, imported as `extend_docx`. +- Parse DOCX and Word 97–2003 binary DOC into a shared document model and structured blocks. +- Preserve source references and support DOCX serialization through the shared model. +- Support native Rust and WebAssembly, with optional native ZIP decompression enabled by default on native builds. + +Page geometry requires the viewer's layout engine. Rust versions are independent of the React viewer's npm versions. diff --git a/crates/docx-core/Cargo.toml b/crates/extend-docx/Cargo.toml similarity index 69% rename from crates/docx-core/Cargo.toml rename to crates/extend-docx/Cargo.toml index 7ceab03..390c1a9 100644 --- a/crates/docx-core/Cargo.toml +++ b/crates/extend-docx/Cargo.toml @@ -1,12 +1,15 @@ [package] -name = "docx-core" +name = "extend-docx" version.workspace = true edition.workspace = true license.workspace = true -description = "Shared native Rust and WebAssembly parsing for DOCX and legacy Word documents" +description = "Rust parser, document model, and serializer for DOCX and legacy Word documents" repository = "https://github.com/extend-hq/react-docx" +documentation = "https://docs.rs/extend-docx" +keywords = ["docx", "doc", "ooxml", "parser", "wasm"] +categories = ["parser-implementations", "text-processing"] readme = "README.md" -include = ["src/**", "examples/parse_blocks.rs", "README.md", "third-party/**", "LICENSE", "Cargo.toml"] +include = ["src/**", "examples/parse_blocks.rs", "README.md", "CHANGELOG.md", "third-party/**", "LICENSE", "Cargo.toml"] [features] default = ["native-deflate"] diff --git a/crates/docx-core/LICENSE b/crates/extend-docx/LICENSE similarity index 100% rename from crates/docx-core/LICENSE rename to crates/extend-docx/LICENSE diff --git a/crates/docx-core/README.md b/crates/extend-docx/README.md similarity index 69% rename from crates/docx-core/README.md rename to crates/extend-docx/README.md index 033e0af..6295f26 100644 --- a/crates/docx-core/README.md +++ b/crates/extend-docx/README.md @@ -1,13 +1,30 @@ -# docx-core +# extend-docx -A native Rust parser shared with the React viewer through WebAssembly. It accepts OOXML DOCX and Word 97–2003 binary DOC through the existing format adapter. No browser, Word installation, or network service is required for parsing. +[![crates.io version](https://img.shields.io/crates/v/extend-docx)](https://crates.io/crates/extend-docx) + +Extend's native Rust parser, document model, and serializer, shared with the React viewer through WebAssembly. It accepts OOXML DOCX and Word 97–2003 binary DOC through the existing format adapter. No browser, Word installation, or network service is required for parsing. + +## Installation + +```sh +cargo add extend-docx +``` + +The Cargo package is `extend-docx`; the Rust import is `extend_docx`. The source lives in `crates/extend-docx` within [extend-hq/react-docx](https://github.com/extend-hq/react-docx/tree/extend-docx-v0.1.0/crates/extend-docx). Rust crate versions are independent of the `@extend-ai/react-docx` npm versions. The internal `docx-wasm` package provides the JavaScript bindings and is not published to crates.io. Native builds enable `native-deflate` by default, using libdeflate for whole-buffer ZIP decompression and reusing its decoder across package entries. This requires a C compiler at build time. Disable default features to use the portable Rust decoder. WebAssembly always uses the Rust decoder. Both paths retain the same fallback for entries whose declared size cannot be used for bounded preallocation. +```toml +[dependencies] +extend-docx = { version = "0.1", default-features = false } +``` + +## Usage + ```rust -let package = docx_core::parse_document_bytes(&bytes)?; -let model = docx_core::build_doc_model(&package); -let blocks = docx_core::extract_document_blocks(&model); +let package = extend_docx::parse_document_bytes(&bytes)?; +let model = extend_docx::build_doc_model(&package); +let blocks = extend_docx::extract_document_blocks(&model); let json = serde_json::to_string(&blocks)?; ``` @@ -16,8 +33,8 @@ let json = serde_json::to_string(&blocks)?; Word spans are optional: ```rust -let options = docx_core::ExtractDocumentBlocksOptions { include_words: true }; -let blocks = docx_core::parse_document_blocks_with_options(&bytes, options)?; +let options = extend_docx::ExtractDocumentBlocksOptions { include_words: true }; +let blocks = extend_docx::parse_document_blocks_with_options(&bytes, options)?; ``` Use `extract_document_blocks_with_options(&model, options)` for an existing model, or add `--include-words` to the CLI. The TypeScript equivalent is `extractDocumentBlocks(model, { includeWords: true })`. Existing calls omit word data. @@ -68,69 +85,69 @@ Deserializing `BlockDetails` stores `Raw(Value)` to retain unknown fields, inclu ## Parser latency comparisons -Measured November 2, 2026 for the **unreleased parser optimizations**, against **docx-rs 0.4.22**, **office_oxide 0.1.15**, and **undoc 0.27.1**. The measured source is commit `dcce836072cb01a24f6490c97643f825d2d6902e`. The [published 0.13.0 results](../../docs/benchmarks-0.13.0.md) remain available separately. +Measured November 2, 2026 for the **optimized parser**, against **docx-rs 0.4.22**, **office_oxide 0.1.15**, and **undoc 0.27.1**. The measured source is commit `dcce836072cb01a24f6490c97643f825d2d6902e`. The `docx-core` labels in historical benchmark artifacts refer to this parser before its rename to `extend-docx`. The [npm release 0.13.0 results](https://github.com/extend-hq/react-docx/blob/extend-docx-v0.1.0/docs/benchmarks-0.13.0.md) remain available separately. -![Median and p95 native parser latency for DOCX, DOC, DOT, DOTX, DOCM, and DOTM](../../docs/readme-assets/docx-core-optimized-20261102-latency-overview.png) +![Median and p95 native parser latency for DOCX, DOC, DOT, DOTX, DOCM, and DOTM](https://raw.githubusercontent.com/extend-hq/react-docx/extend-docx-v0.1.0/docs/readme-assets/docx-core-optimized-20261102-latency-overview.png) -These are native optimized parse and model/IR timings; docx-core also includes structured-block projection. Each document has five warm samples, each averaging 1–20 parses. Charts use the common successful inputs for the parsers supporting each format. File reads, process startup, JSON serialization, destruction, and reference conversion are excluded. The p95 is across per-document medians, not request-level or browser latency. Measurements were taken on a shared development machine. +These are native optimized parse and model/IR timings; extend-docx also includes structured-block projection. Each document has five warm samples, each averaging 1–20 parses. Charts use the common successful inputs for the parsers supporting each format. File reads, process startup, JSON serialization, destruction, and reference conversion are excluded. The p95 is across per-document medians, not request-level or browser latency. Measurements were taken on a shared development machine. DOCX uses the separate seeded 1,000-input corpus. DOC and the template/macro formats pool the two multi-format cohorts. The additional 75 DOCX inputs are shown separately below because they became regressions after inspection and repair. Template and macro-enabled cohorts are small (2–12 inputs); these results do not establish universal performance. -Core has the lowest measured DOCX median (0.673 ms) and p95 (5.080 ms) in this run. Core remains slower than the other binary parsers on DOC; the two paths retain different amounts of structure. See the [full accuracy comparison and limitations](../../docs/benchmarks-optimized-20261102.md) when interpreting speed: the parsers retain different amounts of structure, and text F1 does not measure visual layout. +Core has the lowest measured DOCX median (0.673 ms) and p95 (5.080 ms) in this run. Core remains slower than the other binary parsers on DOC; the two paths retain different amounts of structure. See the [full accuracy comparison and limitations](https://github.com/extend-hq/react-docx/blob/extend-docx-v0.1.0/docs/benchmarks-optimized-20261102.md) when interpreting speed: the parsers retain different amounts of structure, and text F1 does not measure visual layout. Each format chart includes the cumulative latency distribution and latency against input bytes, parsed paragraph count, and feature occurrences. Complexity counts use the core result for the same document across every parser. The axes use logarithmic scales so small and large documents remain visible.
DOCX — seeded corpus: latency distribution and complexity -![DOCX — seeded corpus native latency distribution and complexity comparisons](../../docs/readme-assets/docx-core-optimized-20261102-latency-docx.png) +![DOCX — seeded corpus native latency distribution and complexity comparisons](https://raw.githubusercontent.com/extend-hq/react-docx/extend-docx-v0.1.0/docs/readme-assets/docx-core-optimized-20261102-latency-docx.png)
DOC: latency distribution and complexity -![DOC native latency distribution and complexity comparisons](../../docs/readme-assets/docx-core-optimized-20261102-latency-doc.png) +![DOC native latency distribution and complexity comparisons](https://raw.githubusercontent.com/extend-hq/react-docx/extend-docx-v0.1.0/docs/readme-assets/docx-core-optimized-20261102-latency-doc.png)
DOT: latency distribution and complexity -![DOT native latency distribution and complexity comparisons](../../docs/readme-assets/docx-core-optimized-20261102-latency-dot.png) +![DOT native latency distribution and complexity comparisons](https://raw.githubusercontent.com/extend-hq/react-docx/extend-docx-v0.1.0/docs/readme-assets/docx-core-optimized-20261102-latency-dot.png)
DOTX: latency distribution and complexity -![DOTX native latency distribution and complexity comparisons](../../docs/readme-assets/docx-core-optimized-20261102-latency-dotx.png) +![DOTX native latency distribution and complexity comparisons](https://raw.githubusercontent.com/extend-hq/react-docx/extend-docx-v0.1.0/docs/readme-assets/docx-core-optimized-20261102-latency-dotx.png)
DOCM: latency distribution and complexity -![DOCM native latency distribution and complexity comparisons](../../docs/readme-assets/docx-core-optimized-20261102-latency-docm.png) +![DOCM native latency distribution and complexity comparisons](https://raw.githubusercontent.com/extend-hq/react-docx/extend-docx-v0.1.0/docs/readme-assets/docx-core-optimized-20261102-latency-docm.png)
DOTM: latency distribution and complexity -![DOTM native latency distribution and complexity comparisons](../../docs/readme-assets/docx-core-optimized-20261102-latency-dotm.png) +![DOTM native latency distribution and complexity comparisons](https://raw.githubusercontent.com/extend-hq/react-docx/extend-docx-v0.1.0/docs/readme-assets/docx-core-optimized-20261102-latency-dotm.png)
DOCX — additional regression cohort: latency distribution and complexity -![DOCX — additional regression cohort native latency distribution and complexity comparisons](../../docs/readme-assets/docx-core-optimized-20261102-latency-docx-regressions.png) +![DOCX — additional regression cohort native latency distribution and complexity comparisons](https://raw.githubusercontent.com/extend-hq/react-docx/extend-docx-v0.1.0/docs/readme-assets/docx-core-optimized-20261102-latency-docx-regressions.png)
-The [summary](benchmarks/optimized-20261102/latency.json) and [chart data](benchmarks/optimized-20261102/latency.csv) contain timings and numeric complexity measurements, with no document names or parsed content. Regenerate the figures with [readme_charts.py](../../scripts/parser-benchmark/readme_charts.py) and the retained optimized benchmark runs and the label, asset-prefix, and measured-commit options. +The [summary](https://github.com/extend-hq/react-docx/blob/extend-docx-v0.1.0/crates/extend-docx/benchmarks/optimized-20261102/latency.json) and [chart data](https://github.com/extend-hq/react-docx/blob/extend-docx-v0.1.0/crates/extend-docx/benchmarks/optimized-20261102/latency.csv) contain timings and numeric complexity measurements, with no document names or parsed content. Regenerate the figures with [readme_charts.py](https://github.com/extend-hq/react-docx/blob/extend-docx-v0.1.0/scripts/parser-benchmark/readme_charts.py) and the retained optimized benchmark runs and the label, asset-prefix, and measured-commit options. ## Regression inputs -The public suite uses generated inputs and inline XML. Run `cargo test --workspace --locked` from the repository root. To smoke-test a local corpus explicitly, set `DOCX_TEST_CORPUS` to a directory containing DOC or DOCX inputs and run `cargo test -p docx-core --test doc_fixture -- --ignored`. Corpus documents and diagnostic outputs stay local and are not distributed with the package. +The public suite uses generated inputs and inline XML. Run `cargo test --workspace --locked` from the repository root. To smoke-test a local corpus explicitly, set `DOCX_TEST_CORPUS` to a directory containing DOC or DOCX inputs and run `cargo test -p extend-docx --test doc_fixture -- --ignored`. Corpus documents and diagnostic outputs stay local and are not distributed with the package. diff --git a/crates/docx-core/benchmarks/0.13.0/latency.csv b/crates/extend-docx/benchmarks/0.13.0/latency.csv similarity index 100% rename from crates/docx-core/benchmarks/0.13.0/latency.csv rename to crates/extend-docx/benchmarks/0.13.0/latency.csv diff --git a/crates/docx-core/benchmarks/0.13.0/latency.json b/crates/extend-docx/benchmarks/0.13.0/latency.json similarity index 100% rename from crates/docx-core/benchmarks/0.13.0/latency.json rename to crates/extend-docx/benchmarks/0.13.0/latency.json diff --git a/crates/docx-core/benchmarks/optimized-20261102/latency.csv b/crates/extend-docx/benchmarks/optimized-20261102/latency.csv similarity index 100% rename from crates/docx-core/benchmarks/optimized-20261102/latency.csv rename to crates/extend-docx/benchmarks/optimized-20261102/latency.csv diff --git a/crates/docx-core/benchmarks/optimized-20261102/latency.json b/crates/extend-docx/benchmarks/optimized-20261102/latency.json similarity index 100% rename from crates/docx-core/benchmarks/optimized-20261102/latency.json rename to crates/extend-docx/benchmarks/optimized-20261102/latency.json diff --git a/crates/docx-core/benchmarks/optimized-20261102/validation.json b/crates/extend-docx/benchmarks/optimized-20261102/validation.json similarity index 100% rename from crates/docx-core/benchmarks/optimized-20261102/validation.json rename to crates/extend-docx/benchmarks/optimized-20261102/validation.json diff --git a/crates/docx-core/examples/dump_doc_meta.rs b/crates/extend-docx/examples/dump_doc_meta.rs similarity index 82% rename from crates/docx-core/examples/dump_doc_meta.rs rename to crates/extend-docx/examples/dump_doc_meta.rs index 690f930..0b9a074 100644 --- a/crates/docx-core/examples/dump_doc_meta.rs +++ b/crates/extend-docx/examples/dump_doc_meta.rs @@ -2,11 +2,11 @@ fn main() { let path = std::env::args().nth(1).expect("path"); let bytes = std::fs::read(&path).expect("read"); let pkg = if path.ends_with(".doc") { - docx_core::parse_document_bytes(&bytes).expect("parse") + extend_docx::parse_document_bytes(&bytes).expect("parse") } else { - docx_core::parse_docx(&bytes).expect("parse_docx") + extend_docx::parse_docx(&bytes).expect("parse_docx") }; - let model = docx_core::build_doc_model(&pkg); + let model = extend_docx::build_doc_model(&pkg); println!("parts: {}", pkg.parts.len()); println!("binary assets: {:?}", pkg.binary_assets.keys().collect::>()); println!("headers: {}", model.metadata.header_sections.len()); diff --git a/crates/docx-core/examples/dump_doc_structure.rs b/crates/extend-docx/examples/dump_doc_structure.rs similarity index 64% rename from crates/docx-core/examples/dump_doc_structure.rs rename to crates/extend-docx/examples/dump_doc_structure.rs index f28ff70..f84e1e9 100644 --- a/crates/docx-core/examples/dump_doc_structure.rs +++ b/crates/extend-docx/examples/dump_doc_structure.rs @@ -2,21 +2,21 @@ fn main() { let path = std::env::args().nth(1).expect("path"); let bytes = std::fs::read(&path).expect("read"); let pkg = if path.ends_with(".doc") { - docx_core::parse_document_bytes(&bytes).expect("parse") + extend_docx::parse_document_bytes(&bytes).expect("parse") } else { - docx_core::parse_docx(&bytes).expect("parse_docx") + extend_docx::parse_docx(&bytes).expect("parse_docx") }; - let model = docx_core::build_doc_model(&pkg); + let model = extend_docx::build_doc_model(&pkg); for (i, node) in model.nodes.iter().enumerate() { match node { - docx_core::DocNode::Paragraph(p) => { + extend_docx::DocNode::Paragraph(p) => { let text: String = p.children.iter().filter_map(|c| match c { - docx_core::ParagraphChildNode::Text(r) => Some(r.text.clone()), + extend_docx::ParagraphChildNode::Text(r) => Some(r.text.clone()), _ => None, }).collect(); println!("{i:3} P {:?}", &text.chars().take(60).collect::()); } - docx_core::DocNode::Table(t) => { + extend_docx::DocNode::Table(t) => { println!("{i:3} T rows={} cols(first)={}", t.rows.len(), t.rows.first().map(|r| r.cells.len()).unwrap_or(0)); } diff --git a/crates/extend-docx/examples/dump_doc_text.rs b/crates/extend-docx/examples/dump_doc_text.rs new file mode 100644 index 0000000..7c41af7 --- /dev/null +++ b/crates/extend-docx/examples/dump_doc_text.rs @@ -0,0 +1,37 @@ +fn main() { + let path = std::env::args().nth(1).expect("path"); + let bytes = std::fs::read(&path).expect("read"); + let pkg = if path.ends_with(".doc") { + extend_docx::parse_document_bytes(&bytes).expect("parse") + } else { + extend_docx::parse_docx(&bytes).expect("parse_docx") + }; + let model = extend_docx::build_doc_model(&pkg); + print!("{}", text(&model)); +} +fn text(model: &extend_docx::DocModel) -> String { + let mut out = String::new(); + for node in &model.nodes { walk(node, &mut out); } + out +} +fn walk(node: &extend_docx::DocNode, out: &mut String) { + match node { + extend_docx::DocNode::Paragraph(p) => { + for c in &p.children { + if let extend_docx::ParagraphChildNode::Text(r) = c { out.push_str(&r.text); } + } + out.push('\n'); + } + extend_docx::DocNode::Table(t) => { + for row in &t.rows { for cell in &row.cells { for n in &cell.nodes { + match n { + extend_docx::TableCellContentNode::Paragraph(p) => { + for c in &p.children { if let extend_docx::ParagraphChildNode::Text(r) = c { out.push_str(&r.text); } } + out.push('\n'); + } + extend_docx::TableCellContentNode::Table(t2) => walk(&extend_docx::DocNode::Table(*t2.clone()), out), + } + } } } + } + } +} diff --git a/crates/docx-core/examples/parse_blocks.rs b/crates/extend-docx/examples/parse_blocks.rs similarity index 82% rename from crates/docx-core/examples/parse_blocks.rs rename to crates/extend-docx/examples/parse_blocks.rs index 96b0dce..8b03d8f 100644 --- a/crates/docx-core/examples/parse_blocks.rs +++ b/crates/extend-docx/examples/parse_blocks.rs @@ -1,6 +1,6 @@ fn main() -> Result<(), Box> { let mut path = None; - let mut options = docx_core::ExtractDocumentBlocksOptions::default(); + let mut options = extend_docx::ExtractDocumentBlocksOptions::default(); let mut positional_only = false; for argument in std::env::args_os().skip(1) { if !positional_only && argument == "--" { @@ -15,7 +15,7 @@ fn main() -> Result<(), Box> { } let path = path.ok_or("A document path is required")?; let bytes = std::fs::read(path)?; - let parsed = docx_core::parse_document_blocks_with_options(&bytes, options)?; + let parsed = extend_docx::parse_document_blocks_with_options(&bytes, options)?; serde_json::to_writer(std::io::stdout().lock(), &parsed)?; Ok(()) } diff --git a/crates/docx-core/examples/sweep_doc_corpus.rs b/crates/extend-docx/examples/sweep_doc_corpus.rs similarity index 85% rename from crates/docx-core/examples/sweep_doc_corpus.rs rename to crates/extend-docx/examples/sweep_doc_corpus.rs index 064aa52..1ddbb95 100644 --- a/crates/docx-core/examples/sweep_doc_corpus.rs +++ b/crates/extend-docx/examples/sweep_doc_corpus.rs @@ -22,16 +22,16 @@ fn main() { } }; let result = std::panic::catch_unwind(|| { - let pkg = docx_core::parse_document_bytes(&bytes)?; - let model = docx_core::build_doc_model(&pkg); + let pkg = extend_docx::parse_document_bytes(&bytes)?; + let model = extend_docx::build_doc_model(&pkg); let mut text_len = 0usize; let mut tables = 0usize; for node in &model.nodes { match node { - docx_core::DocNode::Table(_) => tables += 1, - docx_core::DocNode::Paragraph(p) => { + extend_docx::DocNode::Table(_) => tables += 1, + extend_docx::DocNode::Paragraph(p) => { for c in &p.children { - if let docx_core::ParagraphChildNode::Text(r) = c { + if let extend_docx::ParagraphChildNode::Text(r) = c { text_len += r.text.len(); } } diff --git a/crates/docx-core/src/doc/build.rs b/crates/extend-docx/src/doc/build.rs similarity index 100% rename from crates/docx-core/src/doc/build.rs rename to crates/extend-docx/src/doc/build.rs diff --git a/crates/docx-core/src/doc/cfb.rs b/crates/extend-docx/src/doc/cfb.rs similarity index 100% rename from crates/docx-core/src/doc/cfb.rs rename to crates/extend-docx/src/doc/cfb.rs diff --git a/crates/docx-core/src/doc/chp.rs b/crates/extend-docx/src/doc/chp.rs similarity index 100% rename from crates/docx-core/src/doc/chp.rs rename to crates/extend-docx/src/doc/chp.rs diff --git a/crates/docx-core/src/doc/comments.rs b/crates/extend-docx/src/doc/comments.rs similarity index 100% rename from crates/docx-core/src/doc/comments.rs rename to crates/extend-docx/src/doc/comments.rs diff --git a/crates/docx-core/src/doc/fib.rs b/crates/extend-docx/src/doc/fib.rs similarity index 100% rename from crates/docx-core/src/doc/fib.rs rename to crates/extend-docx/src/doc/fib.rs diff --git a/crates/docx-core/src/doc/fields.rs b/crates/extend-docx/src/doc/fields.rs similarity index 100% rename from crates/docx-core/src/doc/fields.rs rename to crates/extend-docx/src/doc/fields.rs diff --git a/crates/docx-core/src/doc/fkp.rs b/crates/extend-docx/src/doc/fkp.rs similarity index 100% rename from crates/docx-core/src/doc/fkp.rs rename to crates/extend-docx/src/doc/fkp.rs diff --git a/crates/docx-core/src/doc/fmt.rs b/crates/extend-docx/src/doc/fmt.rs similarity index 100% rename from crates/docx-core/src/doc/fmt.rs rename to crates/extend-docx/src/doc/fmt.rs diff --git a/crates/docx-core/src/doc/fonts.rs b/crates/extend-docx/src/doc/fonts.rs similarity index 100% rename from crates/docx-core/src/doc/fonts.rs rename to crates/extend-docx/src/doc/fonts.rs diff --git a/crates/docx-core/src/doc/images.rs b/crates/extend-docx/src/doc/images.rs similarity index 100% rename from crates/docx-core/src/doc/images.rs rename to crates/extend-docx/src/doc/images.rs diff --git a/crates/docx-core/src/doc/lists.rs b/crates/extend-docx/src/doc/lists.rs similarity index 100% rename from crates/docx-core/src/doc/lists.rs rename to crates/extend-docx/src/doc/lists.rs diff --git a/crates/docx-core/src/doc/mod.rs b/crates/extend-docx/src/doc/mod.rs similarity index 100% rename from crates/docx-core/src/doc/mod.rs rename to crates/extend-docx/src/doc/mod.rs diff --git a/crates/docx-core/src/doc/pap.rs b/crates/extend-docx/src/doc/pap.rs similarity index 100% rename from crates/docx-core/src/doc/pap.rs rename to crates/extend-docx/src/doc/pap.rs diff --git a/crates/docx-core/src/doc/pieces.rs b/crates/extend-docx/src/doc/pieces.rs similarity index 100% rename from crates/docx-core/src/doc/pieces.rs rename to crates/extend-docx/src/doc/pieces.rs diff --git a/crates/docx-core/src/doc/sep.rs b/crates/extend-docx/src/doc/sep.rs similarity index 100% rename from crates/docx-core/src/doc/sep.rs rename to crates/extend-docx/src/doc/sep.rs diff --git a/crates/docx-core/src/doc/sprm.rs b/crates/extend-docx/src/doc/sprm.rs similarity index 100% rename from crates/docx-core/src/doc/sprm.rs rename to crates/extend-docx/src/doc/sprm.rs diff --git a/crates/docx-core/src/doc/stsh.rs b/crates/extend-docx/src/doc/stsh.rs similarity index 100% rename from crates/docx-core/src/doc/stsh.rs rename to crates/extend-docx/src/doc/stsh.rs diff --git a/crates/docx-core/src/doc/tap.rs b/crates/extend-docx/src/doc/tap.rs similarity index 100% rename from crates/docx-core/src/doc/tap.rs rename to crates/extend-docx/src/doc/tap.rs diff --git a/crates/docx-core/src/doc/textboxes.rs b/crates/extend-docx/src/doc/textboxes.rs similarity index 100% rename from crates/docx-core/src/doc/textboxes.rs rename to crates/extend-docx/src/doc/textboxes.rs diff --git a/crates/docx-core/src/emf.rs b/crates/extend-docx/src/emf.rs similarity index 100% rename from crates/docx-core/src/emf.rs rename to crates/extend-docx/src/emf.rs diff --git a/crates/docx-core/src/lib.rs b/crates/extend-docx/src/lib.rs similarity index 94% rename from crates/docx-core/src/lib.rs rename to crates/extend-docx/src/lib.rs index 84f2085..adeed58 100644 --- a/crates/docx-core/src/lib.rs +++ b/crates/extend-docx/src/lib.rs @@ -1,3 +1,5 @@ +//! Native and WebAssembly-compatible Word document parsing, modeling, and serialization. + pub mod doc; pub mod emf; pub mod model; diff --git a/crates/docx-core/src/model.rs b/crates/extend-docx/src/model.rs similarity index 100% rename from crates/docx-core/src/model.rs rename to crates/extend-docx/src/model.rs diff --git a/crates/docx-core/src/package.rs b/crates/extend-docx/src/package.rs similarity index 100% rename from crates/docx-core/src/package.rs rename to crates/extend-docx/src/package.rs diff --git a/crates/docx-core/src/parse/body.rs b/crates/extend-docx/src/parse/body.rs similarity index 100% rename from crates/docx-core/src/parse/body.rs rename to crates/extend-docx/src/parse/body.rs diff --git a/crates/docx-core/src/parse/build.rs b/crates/extend-docx/src/parse/build.rs similarity index 100% rename from crates/docx-core/src/parse/build.rs rename to crates/extend-docx/src/parse/build.rs diff --git a/crates/docx-core/src/parse/cache.rs b/crates/extend-docx/src/parse/cache.rs similarity index 100% rename from crates/docx-core/src/parse/cache.rs rename to crates/extend-docx/src/parse/cache.rs diff --git a/crates/docx-core/src/parse/colors.rs b/crates/extend-docx/src/parse/colors.rs similarity index 100% rename from crates/docx-core/src/parse/colors.rs rename to crates/extend-docx/src/parse/colors.rs diff --git a/crates/docx-core/src/parse/content_types.rs b/crates/extend-docx/src/parse/content_types.rs similarity index 100% rename from crates/docx-core/src/parse/content_types.rs rename to crates/extend-docx/src/parse/content_types.rs diff --git a/crates/docx-core/src/parse/context.rs b/crates/extend-docx/src/parse/context.rs similarity index 100% rename from crates/docx-core/src/parse/context.rs rename to crates/extend-docx/src/parse/context.rs diff --git a/crates/docx-core/src/parse/document.rs b/crates/extend-docx/src/parse/document.rs similarity index 100% rename from crates/docx-core/src/parse/document.rs rename to crates/extend-docx/src/parse/document.rs diff --git a/crates/docx-core/src/parse/images.rs b/crates/extend-docx/src/parse/images.rs similarity index 100% rename from crates/docx-core/src/parse/images.rs rename to crates/extend-docx/src/parse/images.rs diff --git a/crates/docx-core/src/parse/math.rs b/crates/extend-docx/src/parse/math.rs similarity index 100% rename from crates/docx-core/src/parse/math.rs rename to crates/extend-docx/src/parse/math.rs diff --git a/crates/docx-core/src/parse/metadata.rs b/crates/extend-docx/src/parse/metadata.rs similarity index 100% rename from crates/docx-core/src/parse/metadata.rs rename to crates/extend-docx/src/parse/metadata.rs diff --git a/crates/docx-core/src/parse/mod.rs b/crates/extend-docx/src/parse/mod.rs similarity index 100% rename from crates/docx-core/src/parse/mod.rs rename to crates/extend-docx/src/parse/mod.rs diff --git a/crates/docx-core/src/parse/namespaces.rs b/crates/extend-docx/src/parse/namespaces.rs similarity index 100% rename from crates/docx-core/src/parse/namespaces.rs rename to crates/extend-docx/src/parse/namespaces.rs diff --git a/crates/docx-core/src/parse/numbering.rs b/crates/extend-docx/src/parse/numbering.rs similarity index 100% rename from crates/docx-core/src/parse/numbering.rs rename to crates/extend-docx/src/parse/numbering.rs diff --git a/crates/docx-core/src/parse/paragraph.rs b/crates/extend-docx/src/parse/paragraph.rs similarity index 100% rename from crates/docx-core/src/parse/paragraph.rs rename to crates/extend-docx/src/parse/paragraph.rs diff --git a/crates/docx-core/src/parse/re.rs b/crates/extend-docx/src/parse/re.rs similarity index 100% rename from crates/docx-core/src/parse/re.rs rename to crates/extend-docx/src/parse/re.rs diff --git a/crates/docx-core/src/parse/relationships.rs b/crates/extend-docx/src/parse/relationships.rs similarity index 100% rename from crates/docx-core/src/parse/relationships.rs rename to crates/extend-docx/src/parse/relationships.rs diff --git a/crates/docx-core/src/parse/root_plan_parity.rs b/crates/extend-docx/src/parse/root_plan_parity.rs similarity index 100% rename from crates/docx-core/src/parse/root_plan_parity.rs rename to crates/extend-docx/src/parse/root_plan_parity.rs diff --git a/crates/docx-core/src/parse/scan.rs b/crates/extend-docx/src/parse/scan.rs similarity index 100% rename from crates/docx-core/src/parse/scan.rs rename to crates/extend-docx/src/parse/scan.rs diff --git a/crates/docx-core/src/parse/sections.rs b/crates/extend-docx/src/parse/sections.rs similarity index 100% rename from crates/docx-core/src/parse/sections.rs rename to crates/extend-docx/src/parse/sections.rs diff --git a/crates/docx-core/src/parse/shapes.rs b/crates/extend-docx/src/parse/shapes.rs similarity index 100% rename from crates/docx-core/src/parse/shapes.rs rename to crates/extend-docx/src/parse/shapes.rs diff --git a/crates/docx-core/src/parse/shapes/tests.rs b/crates/extend-docx/src/parse/shapes/tests.rs similarity index 100% rename from crates/docx-core/src/parse/shapes/tests.rs rename to crates/extend-docx/src/parse/shapes/tests.rs diff --git a/crates/docx-core/src/parse/style.rs b/crates/extend-docx/src/parse/style.rs similarity index 100% rename from crates/docx-core/src/parse/style.rs rename to crates/extend-docx/src/parse/style.rs diff --git a/crates/docx-core/src/parse/styles.rs b/crates/extend-docx/src/parse/styles.rs similarity index 100% rename from crates/docx-core/src/parse/styles.rs rename to crates/extend-docx/src/parse/styles.rs diff --git a/crates/docx-core/src/parse/symbols.rs b/crates/extend-docx/src/parse/symbols.rs similarity index 100% rename from crates/docx-core/src/parse/symbols.rs rename to crates/extend-docx/src/parse/symbols.rs diff --git a/crates/docx-core/src/parse/table.rs b/crates/extend-docx/src/parse/table.rs similarity index 100% rename from crates/docx-core/src/parse/table.rs rename to crates/extend-docx/src/parse/table.rs diff --git a/crates/docx-core/src/parse/util.rs b/crates/extend-docx/src/parse/util.rs similarity index 100% rename from crates/docx-core/src/parse/util.rs rename to crates/extend-docx/src/parse/util.rs diff --git a/crates/docx-core/src/serialize.rs b/crates/extend-docx/src/serialize.rs similarity index 100% rename from crates/docx-core/src/serialize.rs rename to crates/extend-docx/src/serialize.rs diff --git a/crates/docx-core/src/serialize/tests.rs b/crates/extend-docx/src/serialize/tests.rs similarity index 100% rename from crates/docx-core/src/serialize/tests.rs rename to crates/extend-docx/src/serialize/tests.rs diff --git a/crates/docx-core/src/source_graph.rs b/crates/extend-docx/src/source_graph.rs similarity index 100% rename from crates/docx-core/src/source_graph.rs rename to crates/extend-docx/src/source_graph.rs diff --git a/crates/docx-core/src/structured.rs b/crates/extend-docx/src/structured.rs similarity index 100% rename from crates/docx-core/src/structured.rs rename to crates/extend-docx/src/structured.rs diff --git a/crates/docx-core/src/table_grid.rs b/crates/extend-docx/src/table_grid.rs similarity index 100% rename from crates/docx-core/src/table_grid.rs rename to crates/extend-docx/src/table_grid.rs diff --git a/crates/docx-core/src/wmf.rs b/crates/extend-docx/src/wmf.rs similarity index 100% rename from crates/docx-core/src/wmf.rs rename to crates/extend-docx/src/wmf.rs diff --git a/crates/docx-core/src/xml.rs b/crates/extend-docx/src/xml.rs similarity index 100% rename from crates/docx-core/src/xml.rs rename to crates/extend-docx/src/xml.rs diff --git a/crates/docx-core/src/xml_index.rs b/crates/extend-docx/src/xml_index.rs similarity index 100% rename from crates/docx-core/src/xml_index.rs rename to crates/extend-docx/src/xml_index.rs diff --git a/crates/docx-core/src/zip.rs b/crates/extend-docx/src/zip.rs similarity index 100% rename from crates/docx-core/src/zip.rs rename to crates/extend-docx/src/zip.rs diff --git a/crates/docx-core/tests/character_scale.rs b/crates/extend-docx/tests/character_scale.rs similarity index 99% rename from crates/docx-core/tests/character_scale.rs rename to crates/extend-docx/tests/character_scale.rs index bc51714..9016b30 100644 --- a/crates/docx-core/tests/character_scale.rs +++ b/crates/extend-docx/tests/character_scale.rs @@ -1,4 +1,4 @@ -use docx_core::{ +use extend_docx::{ build_doc_model, extract_document_blocks_with_options, serialize_doc_model, DocNode, ExtractDocumentBlocksOptions, OoxmlPackage, OoxmlPart, ParagraphChildNode, }; diff --git a/crates/docx-core/tests/control_source_preview.rs b/crates/extend-docx/tests/control_source_preview.rs similarity index 97% rename from crates/docx-core/tests/control_source_preview.rs rename to crates/extend-docx/tests/control_source_preview.rs index fe01ac4..7c52c70 100644 --- a/crates/docx-core/tests/control_source_preview.rs +++ b/crates/extend-docx/tests/control_source_preview.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use base64::Engine; -use docx_core::{ +use extend_docx::{ build_doc_model, extract_document_blocks, serialize_doc_model, DocModel, DocNode, FormFieldRunNode, OoxmlPackage, OoxmlPart, ParagraphChildNode, ParagraphNode, TableCellContentNode, @@ -212,8 +212,8 @@ fn bitmap_metafile_preview_renders_and_roundtrips_original_bytes() { let exported = serialize_doc_model(&model, Some(&source)); assert_eq!(exported.binary_assets, source.binary_assets); assert_eq!( - docx_core::parse::parse_part_relationships(&exported, "word/document.xml"), - docx_core::parse::parse_part_relationships(&source, "word/document.xml") + extend_docx::parse::parse_part_relationships(&exported, "word/document.xml"), + extend_docx::parse::parse_part_relationships(&source, "word/document.xml") ); assert!(exported.parts["word/document.xml"].content.contains(&xml)); assert_eq!( @@ -291,8 +291,8 @@ fn unchanged_export_preserves_control_markup_relationships_and_bytes() { assert!(exported.parts["word/document.xml"].content.contains(&xml)); assert_eq!(exported.binary_assets, source.binary_assets); assert_eq!( - docx_core::parse::parse_part_relationships(&exported, "word/document.xml"), - docx_core::parse::parse_part_relationships(&source, "word/document.xml") + extend_docx::parse::parse_part_relationships(&exported, "word/document.xml"), + extend_docx::parse::parse_part_relationships(&source, "word/document.xml") ); for name in [ "word/activeX/control.xml", diff --git a/crates/docx-core/tests/doc_fixture.rs b/crates/extend-docx/tests/doc_fixture.rs similarity index 95% rename from crates/docx-core/tests/doc_fixture.rs rename to crates/extend-docx/tests/doc_fixture.rs index ab6bfbd..c40b69e 100644 --- a/crates/docx-core/tests/doc_fixture.rs +++ b/crates/extend-docx/tests/doc_fixture.rs @@ -1,4 +1,4 @@ -use docx_core::{build_doc_model, parse_docx}; +use extend_docx::{build_doc_model, parse_docx}; use std::{env, fs, path::PathBuf}; #[test] diff --git a/crates/docx-core/tests/document_comments.rs b/crates/extend-docx/tests/document_comments.rs similarity index 99% rename from crates/docx-core/tests/document_comments.rs rename to crates/extend-docx/tests/document_comments.rs index 28c51e8..bc3840e 100644 --- a/crates/docx-core/tests/document_comments.rs +++ b/crates/extend-docx/tests/document_comments.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use docx_core::{ +use extend_docx::{ build_doc_model, create_minimal_docx_package, serialize_doc_model, DocumentCommentDefinition, OoxmlPackage, OoxmlPart, }; @@ -96,7 +96,7 @@ fn parses_comment_definitions_with_thread_and_resolution() { .nodes .iter() .find_map(|node| match node { - docx_core::model::DocNode::Paragraph(paragraph) => paragraph.source_xml.clone(), + extend_docx::model::DocNode::Paragraph(paragraph) => paragraph.source_xml.clone(), _ => None, }) .expect("paragraph source xml"); diff --git a/crates/docx-core/tests/emf_to_svg.rs b/crates/extend-docx/tests/emf_to_svg.rs similarity index 98% rename from crates/docx-core/tests/emf_to_svg.rs rename to crates/extend-docx/tests/emf_to_svg.rs index d4950b8..e5f46b7 100644 --- a/crates/docx-core/tests/emf_to_svg.rs +++ b/crates/extend-docx/tests/emf_to_svg.rs @@ -1,4 +1,4 @@ -use docx_core::emf::emf_to_svg; +use extend_docx::emf::emf_to_svg; fn vector_drawing() -> Vec { let mut bytes = vec![0u8; 88]; diff --git a/crates/docx-core/tests/image_paint_projection.rs b/crates/extend-docx/tests/image_paint_projection.rs similarity index 99% rename from crates/docx-core/tests/image_paint_projection.rs rename to crates/extend-docx/tests/image_paint_projection.rs index cf80fac..48bbb9f 100644 --- a/crates/docx-core/tests/image_paint_projection.rs +++ b/crates/extend-docx/tests/image_paint_projection.rs @@ -1,4 +1,4 @@ -use docx_core::{ +use extend_docx::{ build_doc_model, create_minimal_docx_package, extract_document_blocks, DocModel, DocNode, ImageRunNode, ParagraphChildNode, StructuredDocument, }; diff --git a/crates/docx-core/tests/inline_semantics.rs b/crates/extend-docx/tests/inline_semantics.rs similarity index 99% rename from crates/docx-core/tests/inline_semantics.rs rename to crates/extend-docx/tests/inline_semantics.rs index 6f40f69..7fb032e 100644 --- a/crates/docx-core/tests/inline_semantics.rs +++ b/crates/extend-docx/tests/inline_semantics.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use docx_core::{ +use extend_docx::{ build_doc_model, extract_document_blocks, serialize_doc_model, DocNode, OoxmlPackage, OoxmlPart, ParagraphChildNode, }; diff --git a/crates/docx-core/tests/paragraph_auto_spacing.rs b/crates/extend-docx/tests/paragraph_auto_spacing.rs similarity index 98% rename from crates/docx-core/tests/paragraph_auto_spacing.rs rename to crates/extend-docx/tests/paragraph_auto_spacing.rs index bc2f362..e2f70a6 100644 --- a/crates/docx-core/tests/paragraph_auto_spacing.rs +++ b/crates/extend-docx/tests/paragraph_auto_spacing.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; -use docx_core::model::{DocModel, DocNode, ParagraphNode, TableCellContentNode}; -use docx_core::{build_doc_model, serialize_doc_model, OoxmlPackage, OoxmlPart}; +use extend_docx::model::{DocModel, DocNode, ParagraphNode, TableCellContentNode}; +use extend_docx::{build_doc_model, serialize_doc_model, OoxmlPackage, OoxmlPart}; fn package(body: &str, styles: &str) -> OoxmlPackage { let document = format!( diff --git a/crates/docx-core/tests/paragraph_mark_run_style.rs b/crates/extend-docx/tests/paragraph_mark_run_style.rs similarity index 95% rename from crates/docx-core/tests/paragraph_mark_run_style.rs rename to crates/extend-docx/tests/paragraph_mark_run_style.rs index b4b674e..8c485fb 100644 --- a/crates/docx-core/tests/paragraph_mark_run_style.rs +++ b/crates/extend-docx/tests/paragraph_mark_run_style.rs @@ -1,9 +1,9 @@ use std::cell::RefCell; use std::collections::HashMap; -use docx_core::model::ParagraphChildNode; -use docx_core::parse::{empty_style_sheet, parse_paragraph, ParseContext}; -use docx_core::OoxmlPart; +use extend_docx::model::ParagraphChildNode; +use extend_docx::parse::{empty_style_sheet, parse_paragraph, ParseContext}; +use extend_docx::OoxmlPart; fn test_context<'a>( parts: &'a HashMap, diff --git a/crates/docx-core/tests/sdt_content_style.rs b/crates/extend-docx/tests/sdt_content_style.rs similarity index 97% rename from crates/docx-core/tests/sdt_content_style.rs rename to crates/extend-docx/tests/sdt_content_style.rs index dbd04a8..c677015 100644 --- a/crates/docx-core/tests/sdt_content_style.rs +++ b/crates/extend-docx/tests/sdt_content_style.rs @@ -1,8 +1,8 @@ use std::cell::RefCell; use std::collections::HashMap; -use docx_core::model::{FormFieldRunNode, ParagraphChildNode}; -use docx_core::parse::{empty_style_sheet, parse_paragraph, ParseContext}; +use extend_docx::model::{FormFieldRunNode, ParagraphChildNode}; +use extend_docx::parse::{empty_style_sheet, parse_paragraph, ParseContext}; fn parse_field(properties: &str, content: &str) -> FormFieldRunNode { let parts = HashMap::new(); diff --git a/crates/docx-core/tests/serialize_paragraph_properties.rs b/crates/extend-docx/tests/serialize_paragraph_properties.rs similarity index 98% rename from crates/docx-core/tests/serialize_paragraph_properties.rs rename to crates/extend-docx/tests/serialize_paragraph_properties.rs index 0411a57..5a5854f 100644 --- a/crates/docx-core/tests/serialize_paragraph_properties.rs +++ b/crates/extend-docx/tests/serialize_paragraph_properties.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; -use docx_core::model::DocNode; -use docx_core::{build_doc_model, serialize_doc_model, OoxmlPackage, OoxmlPart}; +use extend_docx::model::DocNode; +use extend_docx::{build_doc_model, serialize_doc_model, OoxmlPackage, OoxmlPart}; fn package_with_parts(parts: &[(&str, &str)]) -> OoxmlPackage { let mut map = HashMap::new(); diff --git a/crates/docx-core/tests/table_grid_shared_merge.rs b/crates/extend-docx/tests/table_grid_shared_merge.rs similarity index 98% rename from crates/docx-core/tests/table_grid_shared_merge.rs rename to crates/extend-docx/tests/table_grid_shared_merge.rs index 68d5adb..b8ff8a9 100644 --- a/crates/docx-core/tests/table_grid_shared_merge.rs +++ b/crates/extend-docx/tests/table_grid_shared_merge.rs @@ -1,8 +1,8 @@ use std::cell::RefCell; use std::collections::HashMap; -use docx_core::model::TableNode; -use docx_core::parse::{empty_style_sheet, parse_table, ParseContext}; +use extend_docx::model::TableNode; +use extend_docx::parse::{empty_style_sheet, parse_table, ParseContext}; fn cell(width: Option, span: i64, merge: Option<&str>) -> String { let width = width diff --git a/crates/docx-core/tests/table_grid_vmerge_regrid.rs b/crates/extend-docx/tests/table_grid_vmerge_regrid.rs similarity index 98% rename from crates/docx-core/tests/table_grid_vmerge_regrid.rs rename to crates/extend-docx/tests/table_grid_vmerge_regrid.rs index db2ac82..fb898ff 100644 --- a/crates/docx-core/tests/table_grid_vmerge_regrid.rs +++ b/crates/extend-docx/tests/table_grid_vmerge_regrid.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use std::collections::HashMap; -use docx_core::parse::{empty_style_sheet, parse_table, ParseContext}; +use extend_docx::parse::{empty_style_sheet, parse_table, ParseContext}; /// Placeholder-grid tables whose vertical-merge continuation cells are stamped /// with grid-matching widths must still regrid: the continuation cells inherit diff --git a/crates/docx-core/tests/table_no_wrap.rs b/crates/extend-docx/tests/table_no_wrap.rs similarity index 96% rename from crates/docx-core/tests/table_no_wrap.rs rename to crates/extend-docx/tests/table_no_wrap.rs index 8c774d7..51428d0 100644 --- a/crates/docx-core/tests/table_no_wrap.rs +++ b/crates/extend-docx/tests/table_no_wrap.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use docx_core::{build_doc_model, serialize_doc_model, DocNode, OoxmlPackage, OoxmlPart}; +use extend_docx::{build_doc_model, serialize_doc_model, DocNode, OoxmlPackage, OoxmlPart}; fn package(properties: &str) -> OoxmlPackage { let xml = format!(r#"{properties}Alpha Beta"#); diff --git a/crates/docx-core/tests/table_style_paragraph_spacing.rs b/crates/extend-docx/tests/table_style_paragraph_spacing.rs similarity index 92% rename from crates/docx-core/tests/table_style_paragraph_spacing.rs rename to crates/extend-docx/tests/table_style_paragraph_spacing.rs index c59d25e..7466019 100644 --- a/crates/docx-core/tests/table_style_paragraph_spacing.rs +++ b/crates/extend-docx/tests/table_style_paragraph_spacing.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; -use docx_core::model::{DocModel, ParagraphNode, TableCellContentNode}; -use docx_core::{build_doc_model, OoxmlPackage, OoxmlPart}; +use extend_docx::model::{DocModel, ParagraphNode, TableCellContentNode}; +use extend_docx::{build_doc_model, OoxmlPackage, OoxmlPart}; fn build_package(document_xml: &str, styles_xml: &str) -> OoxmlPackage { let mut parts = HashMap::new(); @@ -61,7 +61,7 @@ const DOCUMENT_XML: &str = r#" fn first_cell_paragraph(model: &DocModel, table_index: usize) -> &ParagraphNode { let mut seen = 0usize; for node in &model.nodes { - if let docx_core::model::DocNode::Table(table) = node { + if let extend_docx::model::DocNode::Table(table) = node { if seen == table_index { let cell = &table.rows[0].cells[0]; let TableCellContentNode::Paragraph(paragraph) = &cell.nodes[0] else { @@ -88,7 +88,7 @@ fn table_style_paragraph_spacing_overrides_doc_defaults_in_cells() { .nodes .iter() .find_map(|node| match node { - docx_core::model::DocNode::Paragraph(paragraph) => Some(paragraph), + extend_docx::model::DocNode::Paragraph(paragraph) => Some(paragraph), _ => None, }) .expect("body paragraph"); @@ -129,7 +129,7 @@ fn declared_default_table_style_resolves_properties_without_adding_style_referen ); let package = build_package(DOCUMENT_XML, &styles_xml); let model = build_doc_model(&package); - let docx_core::model::DocNode::Table(table) = &model.nodes[2] else { + let extend_docx::model::DocNode::Table(table) = &model.nodes[2] else { panic!("expected table"); }; let style = table.style.as_ref().expect("table properties"); @@ -144,9 +144,9 @@ fn declared_default_table_style_resolves_properties_without_adding_style_referen .style.as_ref().and_then(|style| style.spacing.as_ref()).expect("spacing"); assert_eq!(spacing.after_twips, Some(0)); assert_eq!(spacing.line_twips, Some(240)); - let exported = docx_core::serialize_doc_model(&model, Some(&package)); + let exported = extend_docx::serialize_doc_model(&model, Some(&package)); let reparsed = build_doc_model(&exported); - let docx_core::model::DocNode::Table(reparsed_table) = &reparsed.nodes[2] else { + let extend_docx::model::DocNode::Table(reparsed_table) = &reparsed.nodes[2] else { panic!("expected table"); }; assert_eq!(reparsed_table.source_xml, table.source_xml); @@ -166,7 +166,7 @@ fn explicit_table_reference_and_direct_properties_take_precedence_over_default() ] { let document = format!(r#"{properties}"#); let model = build_doc_model(&build_package(&document, &styles)); - let docx_core::model::DocNode::Table(table) = &model.nodes[0] else { + let extend_docx::model::DocNode::Table(table) = &model.nodes[0] else { panic!("expected table"); }; let margins = table.style.as_ref().and_then(|style| style.cell_margin_twips.as_ref()); diff --git a/crates/docx-core/tests/word_application_defaults.rs b/crates/extend-docx/tests/word_application_defaults.rs similarity index 98% rename from crates/docx-core/tests/word_application_defaults.rs rename to crates/extend-docx/tests/word_application_defaults.rs index ce27989..77bc9b4 100644 --- a/crates/docx-core/tests/word_application_defaults.rs +++ b/crates/extend-docx/tests/word_application_defaults.rs @@ -3,10 +3,10 @@ //! pitch in exported PDFs), recorded under output/playwright/word-breaks-20261012. use std::collections::HashMap; -use docx_core::model::{ +use extend_docx::model::{ DocModel, DocNode, ParagraphChildNode, ParagraphLineRule, ParagraphNode, TextStyle, }; -use docx_core::{build_doc_model, OoxmlPackage, OoxmlPart}; +use extend_docx::{build_doc_model, OoxmlPackage, OoxmlPart}; const W: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; diff --git a/crates/docx-core/tests/word_spans.rs b/crates/extend-docx/tests/word_spans.rs similarity index 98% rename from crates/docx-core/tests/word_spans.rs rename to crates/extend-docx/tests/word_spans.rs index f99239a..d933f7e 100644 --- a/crates/docx-core/tests/word_spans.rs +++ b/crates/extend-docx/tests/word_spans.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use docx_core::{ +use extend_docx::{ build_doc_model, extract_document_blocks, extract_document_blocks_with_options, parse_document_blocks, parse_document_blocks_with_options, DocumentBlock, ExtractDocumentBlocksOptions, OoxmlPackage, OoxmlPart, @@ -181,7 +181,7 @@ fn nested_and_auxiliary_story_words_use_their_own_source_nodes() { #[test] fn byte_parsing_keeps_word_ids_stable_and_old_api_unchanged() { let source = package("Stable words", &[]); - let bytes = docx_core::package_to_bytes(&source).unwrap(); + let bytes = extend_docx::package_to_bytes(&source).unwrap(); let options = ExtractDocumentBlocksOptions { include_words: true, }; diff --git a/crates/docx-core/third-party/dingbat-to-unicode-LICENSE b/crates/extend-docx/third-party/dingbat-to-unicode-LICENSE similarity index 100% rename from crates/docx-core/third-party/dingbat-to-unicode-LICENSE rename to crates/extend-docx/third-party/dingbat-to-unicode-LICENSE diff --git a/docs/benchmarks-0.13.0.md b/docs/benchmarks-0.13.0.md index a81fe58..2fa38a7 100644 --- a/docs/benchmarks-0.13.0.md +++ b/docs/benchmarks-0.13.0.md @@ -8,7 +8,7 @@ All four parsers were rerun on 1,329 distinct inputs: the 257-input multi-format Each cell is **body-text F1 · median / p95 milliseconds**. DOCX below uses the separate 1,000-input corpus; other formats pool both multi-format cohorts. Timing uses the common successful inputs for libraries supporting that format. Accuracy retains every reference-bearing input, including failed parses as zero. -| Format (inputs) | docx-core | docx-rs | office_oxide | undoc | +| Format (inputs) | extend-docx | docx-rs | office_oxide | undoc | | --- | --- | --- | --- | --- | | DOCX (1,000) | 99.983% · 0.800 / 6.641 | 99.740% · 1.212 / 9.593 | 99.917% · 0.816 / 5.793 | 99.988% · 1.204 / 13.712 | | DOC (217) | 98.748% · 0.268 / 5.664 | Unsupported | 97.979% · 0.063 / 1.285 | 95.293% · 0.027 / 0.417 | @@ -19,11 +19,11 @@ Each cell is **body-text F1 · median / p95 milliseconds**. DOCX below uses the The pooled DOC cohort has 217 declared-supported inputs and 215 usable references. Core parses 216/217; the remaining invalid document identifier is rejected by every compared binary parser. Nine pre-Word-97 and two encrypted acquired DOC inputs remain visible in the all-input reports but are outside declared support. The template and macro-enabled cohorts are small; their scores do not establish universal compatibility or lossless editing. -The 75 additional DOCX inputs score docx-core **100.000%**, docx-rs **95.658%**, office_oxide **90.578%**, undoc **98.644%**. On the 25 additional DOC inputs, core parses 25/25 and scores **99.961%**, up from **91.961%** on their first evaluation. Those inputs became regressions after their defects were inspected and repaired; they are not an untouched holdout. +The 75 additional DOCX inputs score extend-docx **100.000%**, docx-rs **95.658%**, office_oxide **90.578%**, undoc **98.644%**. On the 25 additional DOC inputs, core parses 25/25 and scores **99.961%**, up from **91.961%** on their first evaluation. Those inputs became regressions after their defects were inspected and repaired; they are not an untouched holdout. F1 is the harmonic mean of precision and recall for normalized paragraph-level character five-grams, macro-averaged across documents. It ignores whitespace and soft breaks and is sensitive to paragraph segmentation. The original score remains alongside ordered longest-common-subsequence character F1, exact-text counts, feature precision/recall/F1, and literal-content audits. Binary references are LibreOffice conversion proxies; OOXML references are independently extracted from source packages. -On the 1,000-DOCX corpus, ordered-text F1 is docx-core **99.995%**, docx-rs **99.747%**, office_oxide **99.926%**, undoc **99.975%**. Undoc has the slightly higher original five-gram score, while core has the higher ordered-text score. Core has the lowest measured median, but its 0.800 ms versus office_oxide's 0.816 ms is a small difference on a shared machine; core has a higher p95 than office_oxide. Core is slower than the other binary parsers on DOC. +On the 1,000-DOCX corpus, ordered-text F1 is extend-docx **99.995%**, docx-rs **99.747%**, office_oxide **99.926%**, undoc **99.975%**. Undoc has the slightly higher original five-gram score, while core has the higher ordered-text score. Core has the lowest measured median, but its 0.800 ms versus office_oxide's 0.816 ms is a small difference on a shared machine; core has a higher p95 than office_oxide. Core is slower than the other binary parsers on DOC. Timing measures native optimized parsing, model/IR construction, and core's additional structured-block projection. Every parser uses the same release executable and five warm samples; each sample averages 1–20 parses. File reads, process startup, serialization, destruction, and reference conversion are excluded. The p95 is across per-document medians, not request-level or browser latency. Measurements came from a shared development machine. diff --git a/docs/benchmarks-optimized-20261102.md b/docs/benchmarks-optimized-20261102.md index dda78d0..39204a4 100644 --- a/docs/benchmarks-optimized-20261102.md +++ b/docs/benchmarks-optimized-20261102.md @@ -36,7 +36,7 @@ This does not make the entire library lock-free: initialization, actor mailboxes Measured source: `dcce836072cb01a24f6490c97643f825d2d6902e`. Each cell is body-text five-gram F1 followed by median / p95 milliseconds. DOCX uses the seeded 1,000-input corpus; other formats pool both multi-format cohorts. -| Format | docx-core | docx-rs | office_oxide | undoc | +| Format | extend-docx | docx-rs | office_oxide | undoc | | --- | --- | --- | --- | --- | | DOCX | 99.983% · 0.673 / 5.080 | 99.740% · 1.193 / 9.499 | 99.917% · 0.750 / 5.380 | 99.988% · 1.130 / 14.260 | | DOC | 98.748% · 0.266 / 5.016 | Unsupported | 97.979% · 0.077 / 1.290 | 95.293% · 0.027 / 0.425 | @@ -55,7 +55,7 @@ An additional 1,088 concurrent parses across 272 inputs, at four and eight calle The 329 existing Rust tests pass, with one opt-in corpus test ignored. The WASM target check passes, and PR CI passes JavaScript, Rust/package build, browser smoke, and preview deployment. No new unit tests or corpus documents were added. -Complete interactive reports retain all feature tables and selectable complexity/performance axes under `output/doc-actor-perf-20261102/formats`, `holdout`, and `docx1k`. Original release and allocation-only runs are preserved separately. Charts and sanitized numeric data are linked from the [docx-core README](../crates/docx-core/README.md#parser-latency-comparisons). +Complete interactive reports retain all feature tables and selectable complexity/performance axes under `output/doc-actor-perf-20261102/formats`, `holdout`, and `docx1k`. Original release and allocation-only runs are preserved separately. Charts and sanitized numeric data are linked from the [extend-docx README](../crates/extend-docx/README.md#parser-latency-comparisons). ## Paired before/after measurements diff --git a/docs/playground-deployment.md b/docs/playground-deployment.md index b976e59..b368ef0 100644 --- a/docs/playground-deployment.md +++ b/docs/playground-deployment.md @@ -2,7 +2,7 @@ Vercel deploys the `apps/playground` application from the repository. Its build command runs `scripts/build-playground-vercel.sh`, which installs Rust 1.93.1 with the WASM target and wasm-pack 0.13.1, then runs the playground build. The dependency lockfile is enforced. -`scripts/build-playground.mjs` recompiles `docx-wasm` and `docx-core` before building the viewer and application. The deployed playground therefore follows the deployed Git revision without requiring an npm or crate release. Published packages retain their own released code until a separate release is cut. +`scripts/build-playground.mjs` recompiles `docx-wasm` and `extend-docx` before building the viewer and application. The deployed playground therefore follows the deployed Git revision without requiring an npm or crate release. Published packages retain their own released code until a separate release is cut. The build writes `/build-info.json` with the Git revision, Rust source SHA-256, WASM SHA-256, compiler version, and build timestamp. The application exposes this through its Build link. The build fails if the emitted WASM asset differs from the freshly compiled binary. The manifest is served with `Cache-Control: no-store`; the WASM asset has a content-based filename. diff --git a/docs/rust-publishing.md b/docs/rust-publishing.md new file mode 100644 index 0000000..41328fd --- /dev/null +++ b/docs/rust-publishing.md @@ -0,0 +1,41 @@ +# Publishing extend-docx + +`extend-docx` is the public Rust parser, model, and serializer. Its source is in `crates/extend-docx`, and consumers import it as `extend_docx`. The `docx-wasm` workspace member is an internal JavaScript bridge with `publish = false`. + +The Rust version is defined in `[workspace.package]` in the root `Cargo.toml`. It is independent of the npm release version managed by Changesets. + +## Release + +Authenticate with `cargo login` using a crates.io token scoped to the crate. The initial publication needs permission to publish a new crate. Keep tokens out of the repository and command history. + +Update the Rust version and lockfiles, then run: + +```sh +cargo test --workspace --locked +cargo check -p docx-wasm --target wasm32-unknown-unknown --locked +cargo check -p extend-docx --no-default-features --locked +cargo publish -p extend-docx --locked --dry-run +``` + +Review the package contents with `cargo package -p extend-docx --list`, commit the release changes, and publish: + +```sh +cargo publish -p extend-docx --locked +``` + +Verify the version on [crates.io](https://crates.io/crates/extend-docx) and the [API documentation](https://docs.rs/extend-docx). The README version badges track crates.io automatically. Tag the published source with `extend-docx-v` to distinguish Rust releases from npm tags. + +## Extend ownership + +The repository is maintained by the `extend-hq/engineering` GitHub team. After the initial publication, an individual crate owner who belongs to that team can add it: + +```sh +cargo owner --add github:extend-hq:engineering extend-docx +cargo owner --list extend-docx +``` + +Team owners can publish and yank versions. Individual owners manage ownership, so retain an individual maintainer alongside the team. crates.io must have permission to read the relevant GitHub organization membership. See [Cargo's ownership documentation](https://doc.rust-lang.org/cargo/reference/publishing.html#cargo-owner). + +## Historical benchmark names + +Older measurement records use `docx-core` as their engine identifier. The benchmark tools retain that identifier for compatibility, while their Cargo dependency and Rust imports use `extend-docx` and `extend_docx`. Both native runner names select the same parser. Historical chart labels using `docx-core` refer to this parser before its rename; the stored timings and cohort membership are unchanged. diff --git a/docs/structured-blocks.md b/docs/structured-blocks.md index 97d51d5..683e472 100644 --- a/docs/structured-blocks.md +++ b/docs/structured-blocks.md @@ -4,7 +4,7 @@ The shared Rust parser owns content identity. The browser owns the geometry of t ```mermaid flowchart LR - Upload[Original DOCX or DOC] --> Core[docx-core native Rust] + Upload[Original DOCX or DOC] --> Core[extend-docx native Rust] Core --> Blocks[Versioned blocks and source references] Core --> Source[Package XML and assets] Upload --> Wasm[Same parser compiled to WASM] @@ -102,13 +102,13 @@ Figures resolve their source IDs against the loaded model's embedded images, inc ## Extend integration -1. Add the native `docx-core` dependency to Extend's Rust/N-API package and expose parsing through the existing native worker boundary. Retain the original upload, package assets, parser version, upload hash, and source references. +1. Add the native `extend-docx` dependency to Extend's Rust/N-API package and expose parsing through the existing native worker boundary. Retain the original upload, package assets, parser version, upload hash, and source references. 2. Replace the Mammoth HTML/chunk conversion with the structured tree. Use block/source identity for extraction citations; source paragraphs and images can participate in chunks without assigning fake physical page numbers. 3. Add a DOCX source-reference variant beside the existing HTML byte anchor. Preserve DOCX details as typed extensions: MathML is not LaTeX, and form controls and merged-cell spans carry information beyond current Extend detail schemas. 4. Update Extend's viewer dependency and pass citation source references to the public geometry/highlight APIs. Share its existing page overlay UI through normalized rectangles. Parser block type names remain unchanged. 5. When physical page geometry must exist on the backend, run the matching renderer with pinned fonts, browser, layout options, and document version, materialize all pages, and persist that render fingerprint with the geometry. Rust parsing alone cannot determine browser or Word page breaks. -The `docx-core` crate can be consumed from this workspace or a pinned Git revision now. Registry publication and changes to Extend's application are separate deployment steps. +The `extend-docx` crate can be installed with `cargo add extend-docx`, consumed from this workspace, or pinned to a Git revision. Import it as `extend_docx`. Changes to Extend's application are separate deployment steps. ## Coverage and remaining work diff --git a/docs/supported-word-formats.md b/docs/supported-word-formats.md index 7cabe8b..90b9aac 100644 --- a/docs/supported-word-formats.md +++ b/docs/supported-word-formats.md @@ -2,7 +2,7 @@ The [0.13.0 release comparison](benchmarks-0.13.0.md) contains the final benchmarks rerun on merged main. -`docx-core::parse_document_bytes` and `parse_document_blocks` accept bytes, not a filename or MIME type. They dispatch compound-file input to the legacy Word adapter and other input to the OOXML ZIP reader. The document structures inside the container must still be valid; a ZIP or CFB signature alone does not establish Word support. +`extend-docx::parse_document_bytes` and `parse_document_blocks` accept bytes, not a filename or MIME type. They dispatch compound-file input to the legacy Word adapter and other input to the OOXML ZIP reader. The document structures inside the container must still be valid; a ZIP or CFB signature alone does not establish Word support. The standard Word input family comprises six extensions and five distinct MIME types: diff --git a/docs/word-parity-report.md b/docs/word-parity-report.md index e7c97ab..80ebbef 100644 --- a/docs/word-parity-report.md +++ b/docs/word-parity-report.md @@ -167,7 +167,7 @@ Earlier qualified shared-border, wrapped-figure, character spacing/scaling, empt Validation passes **760 TypeScript tests** (one skipped), **three browser geometry tests at 75%, 100% and 150% zoom**, **36 frozen geometry checks**, **46 Word-oracle harness tests**, workspace and Playground typechecks, and viewer/Playground builds. The follow-up geometry correction reran the typechecks, unit tests, viewer/Playground builds and expanded browser tests, adding isolated marks, invisible format/variation controls and self-clipping. The Rust/WASM parser is byte-identical to the preceding qualified build, which passed 129 Rust tests and matched Mammoth raw text in 17/17 retained upstream fixtures. Every corpus document was freshly parsed with that frozen native binary. [Frozen validation receipt](../output/playwright/tight-blocks-breaks-20261011/code-validation.json) and [follow-up validation](../output/playwright/corpus-tight-blocks-1000-20261011/post-corpus-geometry-verification.json). -The historical Mammoth corpus audit read all 1,000 with docx-core versus 997 with Mammoth; 934 matched after whitespace normalization and 967 retained Mammoth's characters in order. Thirty reading-order cases retained exact paragraph occurrences. That comparison was not rerun here and does not prove complete Mammoth converter/API compatibility or universal DOC/DOCX support. [Mammoth coverage](mammoth-coverage.md) and [structured blocks](structured-blocks.md). +The historical Mammoth corpus audit read all 1,000 with extend-docx versus 997 with Mammoth; 934 matched after whitespace normalization and 967 retained Mammoth's characters in order. Thirty reading-order cases retained exact paragraph occurrences. That comparison was not rerun here and does not prove complete Mammoth converter/API compatibility or universal DOC/DOCX support. [Mammoth coverage](mammoth-coverage.md) and [structured blocks](structured-blocks.md). Across the current corpus, 16 documents have raw source-range credit gaps (419 UTF-16 units), 689 have unresolved mappings, and 127 have raw DOM outliers. These require individual qualification and are not interchangeable with lost visible text or painted overflow. The backend emits source-bound content; authoritative browser boxes require mounted layout. Every word box has not been compared with Word. diff --git a/packages/react-viewer/tsup.config.ts b/packages/react-viewer/tsup.config.ts index 515967b..3af37c7 100644 --- a/packages/react-viewer/tsup.config.ts +++ b/packages/react-viewer/tsup.config.ts @@ -99,7 +99,7 @@ export default defineConfig([ await copyFile("node_modules/utif/LICENSE", "dist/utif-license.txt"); const requireFromUtif = createRequire(createRequire(import.meta.url).resolve("utif/package.json")); await copyFile(join(dirname(requireFromUtif.resolve("pako/package.json")), "LICENSE"), "dist/pako-license.txt"); - await copyFile("../../crates/docx-core/third-party/dingbat-to-unicode-LICENSE", "dist/dingbat-to-unicode-LICENSE"); + await copyFile("../../crates/extend-docx/third-party/dingbat-to-unicode-LICENSE", "dist/dingbat-to-unicode-LICENSE"); }, }, ]); diff --git a/packages/wasm/package.json b/packages/wasm/package.json index ce7b7a8..f364070 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -17,7 +17,7 @@ "files": ["dist", "pkg"], "scripts": { "build:wasm": "wasm-pack build ../../crates/docx-wasm --target web --out-dir \"$(pwd)/pkg\" -- --locked && mkdir -p src/generated && cp pkg/docx_wasm.js pkg/docx_wasm.d.ts pkg/docx_wasm_bg.wasm.d.ts pkg/docx_wasm_bg.wasm src/generated/ && cp pkg/docx_wasm_bg.wasm src/docx_wasm_bg.wasm", - "build:js": "tsup src/index.ts --format esm,cjs --dts --sourcemap --clean --shims && cp src/docx_wasm_bg.wasm dist/docx_wasm_bg.wasm && cp ../../crates/docx-core/third-party/dingbat-to-unicode-LICENSE dist/dingbat-to-unicode-LICENSE", + "build:js": "tsup src/index.ts --format esm,cjs --dts --sourcemap --clean --shims && cp src/docx_wasm_bg.wasm dist/docx_wasm_bg.wasm && cp ../../crates/extend-docx/third-party/dingbat-to-unicode-LICENSE dist/dingbat-to-unicode-LICENSE", "build": "pnpm run build:wasm && pnpm run build:js", "typecheck": "tsc -p tsconfig.json --noEmit", "clean": "rm -rf dist pkg" diff --git a/packages/wasm/src/docx_wasm_bg.wasm b/packages/wasm/src/docx_wasm_bg.wasm index a8e8198..4824e66 100644 Binary files a/packages/wasm/src/docx_wasm_bg.wasm and b/packages/wasm/src/docx_wasm_bg.wasm differ diff --git a/scripts/build-playground.mjs b/scripts/build-playground.mjs index f1cb2c4..3a0f082 100644 --- a/scripts/build-playground.mjs +++ b/scripts/build-playground.mjs @@ -17,7 +17,7 @@ function collect(directory) { else sourceFiles.push(name); } } -for (const crate of ["docx-core", "docx-wasm"]) { +for (const crate of ["extend-docx", "docx-wasm"]) { sourceFiles.push(`crates/${crate}/Cargo.toml`); collect(`crates/${crate}/src`); } diff --git a/scripts/parser-benchmark/Cargo.lock b/scripts/parser-benchmark/Cargo.lock index d7031ee..cfa8bd5 100644 --- a/scripts/parser-benchmark/Cargo.lock +++ b/scripts/parser-benchmark/Cargo.lock @@ -182,7 +182,7 @@ dependencies = [ ] [[package]] -name = "docx-core" +name = "extend-docx" version = "0.1.0" dependencies = [ "arc-swap", @@ -462,7 +462,7 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" name = "parser-benchmark" version = "0.1.0" dependencies = [ - "docx-core", + "extend-docx", "docx-rs", "flate2", "office_oxide", diff --git a/scripts/parser-benchmark/Cargo.toml b/scripts/parser-benchmark/Cargo.toml index ca39246..fe14973 100644 --- a/scripts/parser-benchmark/Cargo.toml +++ b/scripts/parser-benchmark/Cargo.toml @@ -7,7 +7,7 @@ publish = false [workspace] [dependencies] -docx-core = { path = "../../crates/docx-core" } +extend-docx = { path = "../../crates/extend-docx" } docx-rs = { version = "=0.4.22", default-features = false } office_oxide = "=0.1.15" undoc = { version = "=0.27.1", default-features = false, features = ["docx", "doc", "codepages"] } diff --git a/scripts/parser-benchmark/README.md b/scripts/parser-benchmark/README.md index 6a9fe86..e8f0c84 100644 --- a/scripts/parser-benchmark/README.md +++ b/scripts/parser-benchmark/README.md @@ -1,6 +1,8 @@ # Parser benchmark and optimization harness -The standalone Cargo workspace pins docx-rs 0.4.22, office_oxide 0.1.15, and undoc 0.27.1. It links the local docx-core. Release measurements use opt-level 3, thin LTO, and one codegen unit for every parser. The production WASM build uses opt-level 3 and wasm-opt -O3; record its compressed size alongside speed. +The standalone Cargo workspace pins docx-rs 0.4.22, office_oxide 0.1.15, and undoc 0.27.1. It links the local extend-docx. Release measurements use opt-level 3, thin LTO, and one codegen unit for every parser. The production WASM build uses opt-level 3 and wasm-opt -O3; record its compressed size alongside speed. + +The Rust package and import names are `extend-docx` and `extend_docx`. Historical benchmark records and the comparison scripts retain `docx-core` as the engine identifier so existing measurements remain comparable. The native runner accepts both `extend-docx` and `docx-core`; both select the same parser. Historical chart labels using `docx-core` refer to this parser before its rename. Install Python dependencies from `requirements.txt` into `.python-deps`, then build with `cargo build --release --manifest-path scripts/parser-benchmark/Cargo.toml`. Run commands from the repository root. Generated results and binary snapshots belong under `output/`. diff --git a/scripts/parser-benchmark/feedback_loop.py b/scripts/parser-benchmark/feedback_loop.py index b9fc422..279ad2c 100644 --- a/scripts/parser-benchmark/feedback_loop.py +++ b/scripts/parser-benchmark/feedback_loop.py @@ -64,7 +64,7 @@ def main(): parser.error('The frozen queue and digest executable must exist before starting a trial') directory = work / args.label directory.mkdir(parents=True) - source_paths = sorted((root / 'crates/docx-core/src').rglob('*.rs')) + source_paths = sorted((root / 'crates/extend-docx/src').rglob('*.rs')) source = {str(p.relative_to(root)): sha(p) for p in source_paths} result = {'label': args.label, 'status': 'running', 'started_at': time.time(), 'records': [], 'scope': 'Targeted tuning, not a random corpus estimate. Fast-profile core builds use identical flags; office_oxide uses the accepted release binary. Full native model and block parity is required. Production release/WASM validation remains separate.', diff --git a/scripts/parser-benchmark/report.py b/scripts/parser-benchmark/report.py index d1995a2..d6d41a5 100644 --- a/scripts/parser-benchmark/report.py +++ b/scripts/parser-benchmark/report.py @@ -67,7 +67,7 @@ def main(): rows.append([label,f"{ref['expected']:,} / {ref['documents']} docs",*[pct(d[e]['features'].get(key,{}).get('recall',0)) for e in ENGINES]]) parts.append(table(['Check','Source units / eligible docs',*ENGINES],rows)) parts.append('

These are recovery checks against source XML: exact normalized text anchors, with heading level or column span where stated. Failures remain in denominators. Header/footer/note/comment checks compare distinct paragraphs within the matching story. Text-box checks credit retained content even when a parser flattens the box. Vertical merges test retained starts, not reconstructed row-span accuracy. This is not a complete OOXML conformance or style-cascade audit.

') - parts.append('

Span interpretation: the horizontal-merge score is agreement with the literal source grid-span number, not visual merge accuracy. docx-core deliberately normalizes conflicting table grids. Of its 746 mismatches, 646 retained identical merged-cell text with a different numeric span, another 88 retained the cell text outside the same merged-cell anchor, and 12 had a different text anchor. Do not interpret its 83.09% raw-span agreement as 16.91% lost merges. See span-diagnostics.json and crates/docx-core/src/parse/table.rs.

') + parts.append('

Span interpretation: the horizontal-merge score is agreement with the literal source grid-span number, not visual merge accuracy. docx-core deliberately normalizes conflicting table grids. Of its 746 mismatches, 646 retained identical merged-cell text with a different numeric span, another 88 retained the cell text outside the same merged-cell anchor, and 12 had a different text anchor. Do not interpret its 83.09% raw-span agreement as 16.91% lost merges. See span-diagnostics.json and crates/extend-docx/src/parse/table.rs.

') parts.append('

The heading check identified 142 source heading text/level pairs absent or different in docx-core; inspected native-model cases had missing heading classifications or a different level. Conversely, its seven non-exact body-text cases contain symbols, form controls, or embedded objects. Some small text differences reflect generated checkbox/symbol content rather than lost source text.

') rows=[] for key,label in [('images','Embedded-image occurrence count'),('equations','Equation occurrence count'),('explicit_bold','Explicit bold text probe'),('explicit_italic','Explicit italic text probe'),('hyperlinks','Hyperlinked text probe')]: diff --git a/scripts/parser-benchmark/src/bin/concurrent.rs b/scripts/parser-benchmark/src/bin/concurrent.rs index 6584a04..ff1940a 100644 --- a/scripts/parser-benchmark/src/bin/concurrent.rs +++ b/scripts/parser-benchmark/src/bin/concurrent.rs @@ -40,7 +40,7 @@ fn canonicalize(value: &mut Value, document_id: &str) { } } -fn output_digest(model: &docx_core::DocModel, blocks: &docx_core::StructuredDocument) -> [u8; 32] { +fn output_digest(model: &extend_docx::DocModel, blocks: &extend_docx::StructuredDocument) -> [u8; 32] { let mut model = serde_json::to_value(model).unwrap(); let mut blocks = serde_json::to_value(blocks).unwrap(); let id = model["metadata"]["documentId"] @@ -79,9 +79,9 @@ fn run() { inputs .iter() .map(|bytes| { - let package = docx_core::parse_document_bytes(bytes).unwrap(); - let model = docx_core::build_doc_model(&package); - output_digest(&model, &docx_core::extract_document_blocks(&model)) + let package = extend_docx::parse_document_bytes(bytes).unwrap(); + let model = extend_docx::build_doc_model(&package); + output_digest(&model, &extend_docx::extract_document_blocks(&model)) }) .collect::>() } else { @@ -102,9 +102,9 @@ fn run() { for round in 0..rounds { for index in (worker..inputs.len()).step_by(workers) { let start = Instant::now(); - let package = docx_core::parse_document_bytes(&inputs[index]).unwrap(); - let model = docx_core::build_doc_model(&package); - let blocks = docx_core::extract_document_blocks(&model); + let package = extend_docx::parse_document_bytes(&inputs[index]).unwrap(); + let model = extend_docx::build_doc_model(&package); + let blocks = extend_docx::extract_document_blocks(&model); black_box((&package, &model, &blocks)); let ms = start.elapsed().as_secs_f64() * 1000.0; if audit { diff --git a/scripts/parser-benchmark/src/bin/direct_probe.rs b/scripts/parser-benchmark/src/bin/direct_probe.rs index f441cc9..232e991 100644 --- a/scripts/parser-benchmark/src/bin/direct_probe.rs +++ b/scripts/parser-benchmark/src/bin/direct_probe.rs @@ -6,9 +6,9 @@ use sha2::{Digest, Sha256}; fn run(path: &str) -> Result { let bytes = std::fs::read(path).map_err(|e| e.to_string())?; let input_sha256 = format!("{:x}", Sha256::digest(&bytes)); - let package = docx_core::parse_document_bytes(&bytes)?; - let model = docx_core::build_doc_model(&package); - let blocks = docx_core::extract_document_blocks(&model); + let package = extend_docx::parse_document_bytes(&bytes)?; + let model = extend_docx::build_doc_model(&package); + let blocks = extend_docx::extract_document_blocks(&model); let model_digest = direct_merkle::digest(&model).map_err(|e| e.to_string())?; let blocks_digest = direct_merkle::digest(&blocks).map_err(|e| e.to_string())?; Ok( diff --git a/scripts/parser-benchmark/src/bin/layout_probe.rs b/scripts/parser-benchmark/src/bin/layout_probe.rs index 6a3c8fb..25d5588 100644 --- a/scripts/parser-benchmark/src/bin/layout_probe.rs +++ b/scripts/parser-benchmark/src/bin/layout_probe.rs @@ -2,26 +2,26 @@ use std::mem::{align_of, size_of}; fn formatting_diagnostics() -> serde_json::Value { let xml = r#"FirstSecond"#; - let package = docx_core::create_minimal_docx_package(Some(xml)); - let mut model = docx_core::build_doc_model(&package); - let blocks = docx_core::extract_document_blocks(&model); + let package = extend_docx::create_minimal_docx_package(Some(xml)); + let mut model = extend_docx::build_doc_model(&package); + let blocks = extend_docx::extract_document_blocks(&model); let block_json = serde_json::to_value(&blocks).unwrap(); - let round_trip: docx_core::StructuredDocument = + let round_trip: extend_docx::StructuredDocument = serde_json::from_value(block_json.clone()).unwrap(); let typed_raw_round_trip = round_trip == blocks && serde_json::to_value(&round_trip).unwrap() == block_json; let mut extended = block_json.clone(); extended["blocks"][0]["details"]["extension"] = serde_json::json!({"nested": [null, true, {"value": 7}]}); - let imported: docx_core::StructuredDocument = serde_json::from_value(extended.clone()).unwrap(); + let imported: extend_docx::StructuredDocument = serde_json::from_value(extended.clone()).unwrap(); let imported_extensions_retained = serde_json::to_value(imported).unwrap() == extended; - let docx_core::DocNode::Paragraph(paragraph) = &mut model.nodes[0] else { + let extend_docx::DocNode::Paragraph(paragraph) = &mut model.nodes[0] else { panic!("expected paragraph"); }; - let docx_core::ParagraphChildNode::Text(first) = ¶graph.children[0] else { + let extend_docx::ParagraphChildNode::Text(first) = ¶graph.children[0] else { panic!("expected text"); }; - let docx_core::ParagraphChildNode::Text(second) = ¶graph.children[1] else { + let extend_docx::ParagraphChildNode::Text(second) = ¶graph.children[1] else { panic!("expected text"); }; let first_style = first.style.as_deref().unwrap(); @@ -36,14 +36,14 @@ fn formatting_diagnostics() -> serde_json::Value { == serde_json::to_value(first_style).unwrap(); let original_style = serde_json::to_value(first_style).unwrap(); let original_provenance = serde_json::to_value(¶graph.source_run_provenance).unwrap(); - let docx_core::ParagraphChildNode::Text(first) = &mut paragraph.children[0] else { + let extend_docx::ParagraphChildNode::Text(first) = &mut paragraph.children[0] else { unreachable!(); }; let style = first.style.as_mut().unwrap(); style.bold = Some(false); style.font_family = Some("monospace".into()); let changed_style = serde_json::to_value(style).unwrap(); - let docx_core::ParagraphChildNode::Text(second) = ¶graph.children[1] else { + let extend_docx::ParagraphChildNode::Text(second) = ¶graph.children[1] else { unreachable!(); }; let sibling_unchanged = @@ -79,17 +79,17 @@ fn main() { })); }; } - record!("core.TextStyle", docx_core::TextStyle); - record!("core.TextRunNode", docx_core::TextRunNode); - record!("core.ImageRunNode", docx_core::ImageRunNode); - record!("core.FormFieldRunNode", docx_core::FormFieldRunNode); - record!("core.ParagraphChildNode", docx_core::ParagraphChildNode); - record!("core.ParagraphNode", docx_core::ParagraphNode); - record!("core.TableNode", docx_core::TableNode); - record!("core.DocNode", docx_core::DocNode); + record!("core.TextStyle", extend_docx::TextStyle); + record!("core.TextRunNode", extend_docx::TextRunNode); + record!("core.ImageRunNode", extend_docx::ImageRunNode); + record!("core.FormFieldRunNode", extend_docx::FormFieldRunNode); + record!("core.ParagraphChildNode", extend_docx::ParagraphChildNode); + record!("core.ParagraphNode", extend_docx::ParagraphNode); + record!("core.TableNode", extend_docx::TableNode); + record!("core.DocNode", extend_docx::DocNode); record!( "core.ParagraphSourceTextPatchRun", - docx_core::ParagraphSourceTextPatchRun + extend_docx::ParagraphSourceTextPatchRun ); record!("oxide.RunProperties", office_oxide::docx::RunProperties); record!("oxide.Run", office_oxide::docx::Run); diff --git a/scripts/parser-benchmark/src/bin/probe.rs b/scripts/parser-benchmark/src/bin/probe.rs index ed41d70..acf2a0f 100644 --- a/scripts/parser-benchmark/src/bin/probe.rs +++ b/scripts/parser-benchmark/src/bin/probe.rs @@ -8,13 +8,13 @@ fn main() { let mut samples = Vec::new(); for index in 0..=repeats { let start = Instant::now(); - let package = docx_core::parse_document_bytes(&bytes).unwrap(); + let package = extend_docx::parse_document_bytes(&bytes).unwrap(); let package_ms = start.elapsed().as_secs_f64() * 1000.0; let start = Instant::now(); - let model = docx_core::build_doc_model(&package); + let model = extend_docx::build_doc_model(&package); let model_ms = start.elapsed().as_secs_f64() * 1000.0; let start = Instant::now(); - let blocks = docx_core::extract_document_blocks(&model); + let blocks = extend_docx::extract_document_blocks(&model); let blocks_ms = start.elapsed().as_secs_f64() * 1000.0; black_box((&package, &model, &blocks)); if index > 0 { diff --git a/scripts/parser-benchmark/src/compact.rs b/scripts/parser-benchmark/src/compact.rs index df1934f..3a2a77e 100644 --- a/scripts/parser-benchmark/src/compact.rs +++ b/scripts/parser-benchmark/src/compact.rs @@ -1,4 +1,4 @@ -use docx_core::*; +use extend_docx::*; use serde_json::{json, Value}; fn paragraph(p: &ParagraphNode) -> Value { diff --git a/scripts/parser-benchmark/src/main.rs b/scripts/parser-benchmark/src/main.rs index 459eede..188f9a3 100644 --- a/scripts/parser-benchmark/src/main.rs +++ b/scripts/parser-benchmark/src/main.rs @@ -6,7 +6,7 @@ use sha2::{Digest, Sha256}; mod compact; enum Parsed { - Core(docx_core::DocModel, docx_core::StructuredDocument), + Core(extend_docx::DocModel, extend_docx::StructuredDocument), Rs(docx_rs::Docx), Oxide(office_oxide::ir::DocumentIR), Undoc(undoc::Document), @@ -15,12 +15,12 @@ enum Parsed { fn parse(engine: &str, bytes: &Arc<[u8]>, format: &str) -> Result<(Parsed, [f64; 2]), String> { let start = Instant::now(); match engine { - "docx-core" => { - let package = docx_core::parse_document_bytes(bytes)?; - let model = docx_core::build_doc_model(&package); + "extend-docx" | "docx-core" => { + let package = extend_docx::parse_document_bytes(bytes)?; + let model = extend_docx::build_doc_model(&package); let parse_ms = start.elapsed().as_secs_f64() * 1000.0; let projection_start = Instant::now(); - let blocks = docx_core::extract_document_blocks(&model); + let blocks = extend_docx::extract_document_blocks(&model); let project_ms = projection_start.elapsed().as_secs_f64() * 1000.0; Ok((Parsed::Core(model, blocks), [parse_ms, project_ms])) } diff --git a/scripts/parser-benchmark/word_formats.py b/scripts/parser-benchmark/word_formats.py index ca2f7c4..295cc38 100644 --- a/scripts/parser-benchmark/word_formats.py +++ b/scripts/parser-benchmark/word_formats.py @@ -116,7 +116,7 @@ def run(out, repeats): 'seed': seed, 'repeats': repeats, 'load_start': os.getloadavg(), 'start_time': time.time(), 'timing': 'Warm parse plus native model/IR and core structured blocks; excludes input reads, output serialization, destruction, process startup, and reference conversion. Each sample averages adaptive 1–20 parses.', 'profile': 'release opt-level=3 thin LTO codegen-units=1', - 'source_sha256': {str(p.relative_to(ROOT)): sha(p) for root in (ROOT / 'crates/docx-core/src', ROOT / 'scripts/parser-benchmark/src') for p in sorted(root.rglob('*.rs'))}, + 'source_sha256': {str(p.relative_to(ROOT)): sha(p) for root in (ROOT / 'crates/extend-docx/src', ROOT / 'scripts/parser-benchmark/src') for p in sorted(root.rglob('*.rs'))}, 'scorer_sha256': {p.name: sha(p) for p in Path(__file__).parent.glob('*.py')}} save(out / 'format-environment.json', env) for index, (entry, engine) in enumerate(jobs): diff --git a/scripts/parser-feedback/Cargo.lock b/scripts/parser-feedback/Cargo.lock index 765b92a..a3ea7af 100644 --- a/scripts/parser-feedback/Cargo.lock +++ b/scripts/parser-feedback/Cargo.lock @@ -17,6 +17,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "arc-swap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380962b3d93b99642e50b9febf72c071e97e8b32d86eae9cf4ef582bb960ae6e" +dependencies = [ + "rustversion", +] + [[package]] name = "base64" version = "0.22.1" @@ -122,11 +131,19 @@ dependencies = [ ] [[package]] -name = "docx-core" +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "extend-docx" version = "0.1.0" dependencies = [ + "arc-swap", "base64", "base64-simd", + "itoa", "libdeflater", "memchr", "miniz_oxide 0.8.9", @@ -142,12 +159,6 @@ dependencies = [ "zlib-rs", ] -[[package]] -name = "either" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" - [[package]] name = "find-msvc-tools" version = "0.1.15" @@ -240,7 +251,7 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" name = "parser-feedback" version = "0.1.0" dependencies = [ - "docx-core", + "extend-docx", "flate2", "serde", "serde_json", @@ -328,6 +339,12 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + [[package]] name = "serde" version = "1.0.229" diff --git a/scripts/parser-feedback/Cargo.toml b/scripts/parser-feedback/Cargo.toml index 664008d..e315e1d 100644 --- a/scripts/parser-feedback/Cargo.toml +++ b/scripts/parser-feedback/Cargo.toml @@ -7,7 +7,7 @@ publish = false [workspace] [dependencies] -docx-core = { path = "../../crates/docx-core" } +extend-docx = { path = "../../crates/extend-docx" } serde = "1" serde_json = "1" flate2 = "1" diff --git a/scripts/parser-feedback/src/main.rs b/scripts/parser-feedback/src/main.rs index 19b56a3..19ba2a0 100644 --- a/scripts/parser-feedback/src/main.rs +++ b/scripts/parser-feedback/src/main.rs @@ -10,17 +10,17 @@ use std::{ }; enum Parsed { - Core(docx_core::DocModel, docx_core::StructuredDocument), + Core(extend_docx::DocModel, extend_docx::StructuredDocument), } fn parse(_: &str, bytes: &Arc<[u8]>, _: &str) -> Result<(Parsed, [f64; 3]), String> { let start = Instant::now(); - let package = docx_core::parse_document_bytes(bytes)?; + let package = extend_docx::parse_document_bytes(bytes)?; let package_ms = start.elapsed().as_secs_f64() * 1000.0; let start = Instant::now(); - let model = docx_core::build_doc_model(&package); + let model = extend_docx::build_doc_model(&package); let model_ms = start.elapsed().as_secs_f64() * 1000.0; let start = Instant::now(); - let blocks = docx_core::extract_document_blocks(&model); + let blocks = extend_docx::extract_document_blocks(&model); let blocks_ms = start.elapsed().as_secs_f64() * 1000.0; Ok(( Parsed::Core(model, blocks),