docxdriver 0.3.3__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {docxdriver-0.3.3 → docxdriver-0.4.1}/Cargo.lock +23 -5
- {docxdriver-0.3.3 → docxdriver-0.4.1}/Cargo.toml +1 -1
- {docxdriver-0.3.3 → docxdriver-0.4.1}/PKG-INFO +32 -26
- docxdriver-0.4.1/README.md +87 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/Cargo.toml +7 -0
- docxdriver-0.4.1/crates/docxdriver-core/LICENSE +21 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/api.rs +16 -1
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/commands/comments.rs +33 -10
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/commands/equations.rs +4 -2
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/commands/headers.rs +8 -4
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/commands/layout.rs +14 -4
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/commands/mod.rs +24 -2
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/commands/readonly.rs +4 -1
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/commands/revisions.rs +133 -14
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/commands/tables.rs +16 -6
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/commands/typed.rs +8 -21
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/document.rs +32 -22
- docxdriver-0.4.1/crates/docxdriver-core/src/facade.rs +200 -0
- docxdriver-0.4.1/crates/docxdriver-core/src/integrity/mod.rs +1519 -0
- docxdriver-0.4.1/crates/docxdriver-core/src/integrity/schema.rs +512 -0
- docxdriver-0.4.1/crates/docxdriver-core/src/integrity/tree.rs +503 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/lib.rs +5 -1
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/package.rs +54 -18
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/segments.rs +238 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-python/Cargo.toml +2 -0
- docxdriver-0.4.1/crates/docxdriver-python/README.md +87 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1/crates/docxdriver-python}/python/docxdriver/__init__.py +2 -1
- docxdriver-0.4.1/crates/docxdriver-python/python/docxdriver/document.py +126 -0
- docxdriver-0.4.1/crates/docxdriver-python/python/docxdriver/plan.py +570 -0
- docxdriver-0.4.1/crates/docxdriver-python/src/lib.rs +199 -0
- docxdriver-0.4.1/crates/docxdriver-python/tests/test_document.py +75 -0
- docxdriver-0.4.1/crates/docxdriver-python/tests/test_plan.py +40 -0
- {docxdriver-0.3.3/crates/docxdriver-python → docxdriver-0.4.1}/python/docxdriver/__init__.py +2 -1
- docxdriver-0.4.1/python/docxdriver/document.py +126 -0
- docxdriver-0.4.1/python/docxdriver/plan.py +570 -0
- docxdriver-0.3.3/README.md +0 -81
- docxdriver-0.3.3/crates/docxdriver-core/examples/list_scaling.rs +0 -71
- docxdriver-0.3.3/crates/docxdriver-core/examples/plan_scaling.rs +0 -43
- docxdriver-0.3.3/crates/docxdriver-core/src/bin/generate_typed_schema.rs +0 -172
- docxdriver-0.3.3/crates/docxdriver-core/src/html/README.md +0 -14
- docxdriver-0.3.3/crates/docxdriver-core/tests/README.md +0 -35
- docxdriver-0.3.3/crates/docxdriver-core/tests/fixtures/projection-parity/features.json +0 -33
- docxdriver-0.3.3/crates/docxdriver-core/tests/fixtures/projection-parity/parity.docx +0 -0
- docxdriver-0.3.3/crates/docxdriver-core/tests/fixtures/projection-parity/prior-projection.json +0 -1393
- docxdriver-0.3.3/crates/docxdriver-core/tests/fixtures/ts-reference.docx +0 -0
- docxdriver-0.3.3/crates/docxdriver-core/tests/formatting_fidelity.rs +0 -1148
- docxdriver-0.3.3/crates/docxdriver-core/tests/html_e2e.rs +0 -983
- docxdriver-0.3.3/crates/docxdriver-core/tests/interop.rs +0 -99
- docxdriver-0.3.3/crates/docxdriver-core/tests/package_layout.rs +0 -126
- docxdriver-0.3.3/crates/docxdriver-core/tests/projection_parity.rs +0 -531
- docxdriver-0.3.3/crates/docxdriver-core/tests/spike.rs +0 -44
- docxdriver-0.3.3/crates/docxdriver-core/tests/typed_api.rs +0 -2553
- docxdriver-0.3.3/crates/docxdriver-python/README.md +0 -81
- docxdriver-0.3.3/crates/docxdriver-python/python/docxdriver/repl/__init__.py +0 -7
- docxdriver-0.3.3/crates/docxdriver-python/python/docxdriver/repl/host.py +0 -562
- docxdriver-0.3.3/crates/docxdriver-python/python/docxdriver/repl/prelude.py +0 -3
- docxdriver-0.3.3/crates/docxdriver-python/python/docxdriver/repl/quotes.py +0 -325
- docxdriver-0.3.3/crates/docxdriver-python/src/lib.rs +0 -62
- docxdriver-0.3.3/crates/docxdriver-python/tests/test_host.py +0 -357
- docxdriver-0.3.3/python/docxdriver/repl/__init__.py +0 -7
- docxdriver-0.3.3/python/docxdriver/repl/host.py +0 -562
- docxdriver-0.3.3/python/docxdriver/repl/prelude.py +0 -3
- docxdriver-0.3.3/python/docxdriver/repl/quotes.py +0 -325
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/README.md +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/assets/blank.docx +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/commands/lists.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/commands/reading.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/commands/sections.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/commands/table_structure.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/html/assets.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/html/build.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/html/chrome.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/html/compact.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/html/ctx.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/html/input.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/html/mathml_omml.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/html/mod.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/html/omml_latex.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/html/omml_mathml.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/html/parse.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/html/render.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/html/styles.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/html/surface.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/media.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-core/src/outcome.rs +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/crates/docxdriver-python/.gitignore +0 -0
- {docxdriver-0.3.3 → docxdriver-0.4.1}/pyproject.toml +0 -0
|
@@ -416,7 +416,7 @@ dependencies = [
|
|
|
416
416
|
|
|
417
417
|
[[package]]
|
|
418
418
|
name = "docxdriver-cli"
|
|
419
|
-
version = "0.
|
|
419
|
+
version = "0.4.1"
|
|
420
420
|
dependencies = [
|
|
421
421
|
"clap",
|
|
422
422
|
"docxdriver-core",
|
|
@@ -427,10 +427,11 @@ dependencies = [
|
|
|
427
427
|
|
|
428
428
|
[[package]]
|
|
429
429
|
name = "docxdriver-core"
|
|
430
|
-
version = "0.
|
|
430
|
+
version = "0.4.1"
|
|
431
431
|
dependencies = [
|
|
432
432
|
"hex",
|
|
433
433
|
"latex2mathml",
|
|
434
|
+
"quick-xml",
|
|
434
435
|
"regex-lite",
|
|
435
436
|
"schemars 0.8.22",
|
|
436
437
|
"serde",
|
|
@@ -444,7 +445,7 @@ dependencies = [
|
|
|
444
445
|
|
|
445
446
|
[[package]]
|
|
446
447
|
name = "docxdriver-mcp"
|
|
447
|
-
version = "0.
|
|
448
|
+
version = "0.4.1"
|
|
448
449
|
dependencies = [
|
|
449
450
|
"axum",
|
|
450
451
|
"cap-std",
|
|
@@ -460,18 +461,26 @@ dependencies = [
|
|
|
460
461
|
"tokio-util",
|
|
461
462
|
]
|
|
462
463
|
|
|
464
|
+
[[package]]
|
|
465
|
+
name = "docxdriver-monty-repl"
|
|
466
|
+
version = "0.4.1"
|
|
467
|
+
dependencies = [
|
|
468
|
+
"docxdriver-core",
|
|
469
|
+
]
|
|
470
|
+
|
|
463
471
|
[[package]]
|
|
464
472
|
name = "docxdriver-python"
|
|
465
|
-
version = "0.
|
|
473
|
+
version = "0.4.1"
|
|
466
474
|
dependencies = [
|
|
467
475
|
"docxdriver-core",
|
|
468
476
|
"pyo3",
|
|
477
|
+
"serde",
|
|
469
478
|
"serde_json",
|
|
470
479
|
]
|
|
471
480
|
|
|
472
481
|
[[package]]
|
|
473
482
|
name = "docxdriver-wasm"
|
|
474
|
-
version = "0.
|
|
483
|
+
version = "0.4.1"
|
|
475
484
|
dependencies = [
|
|
476
485
|
"docxdriver-core",
|
|
477
486
|
"js-sys",
|
|
@@ -1180,6 +1189,15 @@ dependencies = [
|
|
|
1180
1189
|
"syn 2.0.118",
|
|
1181
1190
|
]
|
|
1182
1191
|
|
|
1192
|
+
[[package]]
|
|
1193
|
+
name = "quick-xml"
|
|
1194
|
+
version = "0.42.0"
|
|
1195
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1196
|
+
checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b"
|
|
1197
|
+
dependencies = [
|
|
1198
|
+
"memchr",
|
|
1199
|
+
]
|
|
1200
|
+
|
|
1183
1201
|
[[package]]
|
|
1184
1202
|
name = "quote"
|
|
1185
1203
|
version = "1.0.46"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docxdriver
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Python SDK for the native Rust DOCX engine
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -18,38 +18,44 @@ Install from PyPI:
|
|
|
18
18
|
python -m pip install docxdriver
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
contents as Python `bytes`.
|
|
24
|
-
|
|
25
|
-
The optional sandbox REPL harness is available as a subpackage in the same
|
|
26
|
-
distribution:
|
|
21
|
+
`Document` works on `.docx` bytes; typed plan builders live in
|
|
22
|
+
`docxdriver.plan`:
|
|
27
23
|
|
|
28
24
|
```python
|
|
29
|
-
from
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from docxdriver import Document, create
|
|
27
|
+
from docxdriver.plan import Plan, ReplaceText
|
|
28
|
+
|
|
29
|
+
doc = Document(Path("contract.docx").read_bytes())
|
|
30
|
+
doc.markup() # body with the ids plans address
|
|
31
|
+
doc.comments(); doc.revisions(); doc.styles(); doc.assets()
|
|
32
|
+
doc.find("thirty")
|
|
33
|
+
review = doc.review(Plan(author="Reviewer", ops=[
|
|
34
|
+
ReplaceText(at="0F537164", select="thirty", with_="sixty"),
|
|
35
|
+
]))
|
|
36
|
+
review.report # preview: each op's outcome
|
|
37
|
+
Path("contract.docx").write_bytes(doc.commit(review.key).data)
|
|
38
|
+
|
|
39
|
+
new = create(paragraphs=["Hello"]) # or create(html="<p>Hello</p>")
|
|
30
40
|
```
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
The host confines reads and writes to the provided workspace, verifies source
|
|
39
|
-
hashes and candidate output before commit, and performs atomic file replacement
|
|
40
|
-
with read-back verification. Commit keys are bounded and single-use. Cancellation
|
|
41
|
-
is drained before another host call so a write callback cannot be abandoned.
|
|
42
|
+
Commit keys are single-use. Natively `review.key` carries the key; in a
|
|
43
|
+
sandbox it is `None` and the host shows the key only to the model, so a
|
|
44
|
+
commit proves the review was read. Sandbox hosts written in Python build on
|
|
45
|
+
the stateless `review_plan(data, plan_json)` and
|
|
46
|
+
`commit_plan(data, plan_json, key)`, keeping each plan under a key they issue.
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
`permissive`; `controlled` and `authoritative` fail closed.
|
|
48
|
+
Engine diagnostics raise `ValueError`. The lower-level `execute_request`,
|
|
49
|
+
`canonical_plan_json`, `run_command`, `run_plan` and `run_request` remain.
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
`plan.py` and `document.py` stay within Monty's Python subset. Rust hosts get
|
|
52
|
+
the same text as `docxdriver_monty_repl::PLAN` and `DOCUMENT`, so sandbox code
|
|
53
|
+
and native code are interchangeable. The op dataclasses are generated from the
|
|
54
|
+
core schema; regenerate from the repository root with
|
|
55
|
+
`python scripts/generate-python-plan.py`.
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
```
|
|
57
|
+
Version 0.4.0 removed the `docxdriver.repl` sandbox host from this package. Its
|
|
58
|
+
source remains in `experimental/python-repl-host` for hosts to vendor.
|
|
53
59
|
|
|
54
60
|
Build and install a wheel from the repository root:
|
|
55
61
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# docxdriver
|
|
2
|
+
|
|
3
|
+
Python SDK for the Rust DOCX engine. It supports Python 3.10 and later and has no
|
|
4
|
+
Python runtime dependencies.
|
|
5
|
+
|
|
6
|
+
Install from PyPI:
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
python -m pip install docxdriver
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`Document` works on `.docx` bytes; typed plan builders live in
|
|
13
|
+
`docxdriver.plan`:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from docxdriver import Document, create
|
|
18
|
+
from docxdriver.plan import Plan, ReplaceText
|
|
19
|
+
|
|
20
|
+
doc = Document(Path("contract.docx").read_bytes())
|
|
21
|
+
doc.markup() # body with the ids plans address
|
|
22
|
+
doc.comments(); doc.revisions(); doc.styles(); doc.assets()
|
|
23
|
+
doc.find("thirty")
|
|
24
|
+
review = doc.review(Plan(author="Reviewer", ops=[
|
|
25
|
+
ReplaceText(at="0F537164", select="thirty", with_="sixty"),
|
|
26
|
+
]))
|
|
27
|
+
review.report # preview: each op's outcome
|
|
28
|
+
Path("contract.docx").write_bytes(doc.commit(review.key).data)
|
|
29
|
+
|
|
30
|
+
new = create(paragraphs=["Hello"]) # or create(html="<p>Hello</p>")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Commit keys are single-use. Natively `review.key` carries the key; in a
|
|
34
|
+
sandbox it is `None` and the host shows the key only to the model, so a
|
|
35
|
+
commit proves the review was read. Sandbox hosts written in Python build on
|
|
36
|
+
the stateless `review_plan(data, plan_json)` and
|
|
37
|
+
`commit_plan(data, plan_json, key)`, keeping each plan under a key they issue.
|
|
38
|
+
|
|
39
|
+
Engine diagnostics raise `ValueError`. The lower-level `execute_request`,
|
|
40
|
+
`canonical_plan_json`, `run_command`, `run_plan` and `run_request` remain.
|
|
41
|
+
|
|
42
|
+
`plan.py` and `document.py` stay within Monty's Python subset. Rust hosts get
|
|
43
|
+
the same text as `docxdriver_monty_repl::PLAN` and `DOCUMENT`, so sandbox code
|
|
44
|
+
and native code are interchangeable. The op dataclasses are generated from the
|
|
45
|
+
core schema; regenerate from the repository root with
|
|
46
|
+
`python scripts/generate-python-plan.py`.
|
|
47
|
+
|
|
48
|
+
Version 0.4.0 removed the `docxdriver.repl` sandbox host from this package. Its
|
|
49
|
+
source remains in `experimental/python-repl-host` for hosts to vendor.
|
|
50
|
+
|
|
51
|
+
Build and install a wheel from the repository root:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
python -m pip install "maturin>=1.9,<2"
|
|
55
|
+
maturin build --profile python --locked --manifest-path crates/docxdriver-python/Cargo.toml --out /tmp/docxdriver-wheels
|
|
56
|
+
python -m pip install /tmp/docxdriver-wheels/*.whl
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Publish to PyPI
|
|
60
|
+
|
|
61
|
+
The `Publish Python SDK` GitHub Actions workflow builds a source distribution
|
|
62
|
+
and wheels for Linux x86_64 and aarch64, macOS universal2, and Windows x86_64
|
|
63
|
+
when a `v*` tag is pushed. The tag must match the version in the root
|
|
64
|
+
`Cargo.toml` workspace package.
|
|
65
|
+
|
|
66
|
+
Before the first release, configure a PyPI trusted publisher for the
|
|
67
|
+
`docxdriver` project with these values:
|
|
68
|
+
|
|
69
|
+
- Owner: `jhnoel`
|
|
70
|
+
- Repository: `docxdriver`
|
|
71
|
+
- Workflow filename: `pypi.yml`
|
|
72
|
+
- GitHub environment: `pypi`
|
|
73
|
+
|
|
74
|
+
Create the `pypi` GitHub environment under the repository settings if you want
|
|
75
|
+
to add release approvals. Then update the workspace version, commit the change,
|
|
76
|
+
and push the matching tag (for example, `v0.2.7`). GitHub Actions publishes the
|
|
77
|
+
built distributions using PyPI's trusted publishing; no long-lived PyPI token is
|
|
78
|
+
needed.
|
|
79
|
+
|
|
80
|
+
The `python` Cargo profile keeps release optimizations and enables panic unwinding
|
|
81
|
+
so PyO3 can turn Rust panics into Python exceptions. ABI3 wheels support Python
|
|
82
|
+
3.10 and later on the built operating system and architecture. To run the Python
|
|
83
|
+
SDK and host tests after installing the wheel:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
python -m unittest discover -s crates/docxdriver-python/tests
|
|
87
|
+
```
|
|
@@ -4,7 +4,13 @@ description = "Artifact-first DOCX engine: (bytes, DocxCommand JSON) -> (bytes,
|
|
|
4
4
|
version.workspace = true
|
|
5
5
|
edition.workspace = true
|
|
6
6
|
license.workspace = true
|
|
7
|
+
repository.workspace = true
|
|
7
8
|
readme = "README.md"
|
|
9
|
+
keywords = ["docx", "ooxml", "word", "document", "track-changes"]
|
|
10
|
+
categories = ["text-processing", "parser-implementations"]
|
|
11
|
+
# Library sources and the runtime template only; tests, examples and the
|
|
12
|
+
# schema generator stay in the repository.
|
|
13
|
+
include = ["/src/**/*.rs", "!/src/bin/**", "/assets/blank.docx", "/README.md", "/LICENSE"]
|
|
8
14
|
|
|
9
15
|
[dependencies]
|
|
10
16
|
xmloxide = { version = "0.4.3", default-features = false }
|
|
@@ -18,5 +24,6 @@ sha2 = "0.10"
|
|
|
18
24
|
hex = "0.4"
|
|
19
25
|
toml = "0.8"
|
|
20
26
|
schemars = "0.8"
|
|
27
|
+
quick-xml = "0.42.0"
|
|
21
28
|
|
|
22
29
|
[dev-dependencies]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 docxdriver contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1957,7 +1957,22 @@ fn fold_typed(
|
|
|
1957
1957
|
crate::outcome::Outcome::ok("plan applied", serde_json::json!({})),
|
|
1958
1958
|
&ctx,
|
|
1959
1959
|
);
|
|
1960
|
-
|
|
1960
|
+
if out.status != crate::outcome::Status::Ok {
|
|
1961
|
+
return (
|
|
1962
|
+
report.clone(),
|
|
1963
|
+
None,
|
|
1964
|
+
Some((diag("package_write", out.summary, None), report)),
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
// Review and commit apply the same gate to the same bytes, so a plan
|
|
1968
|
+
// that reviews clean commits clean.
|
|
1969
|
+
if let Some(bytes) = &out.bytes {
|
|
1970
|
+
if let Err(failure) = crate::integrity::gate(input, bytes) {
|
|
1971
|
+
let diagnostic = diag("integrity_regression", failure.summary(), None);
|
|
1972
|
+
return (report.clone(), None, Some((diagnostic, report)));
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
(report, if commit { out.bytes } else { None }, None)
|
|
1961
1976
|
}
|
|
1962
1977
|
/// When a selector matched nothing but matches the same paragraph as it was
|
|
1963
1978
|
/// before the plan, name the earlier operations that changed that text:
|
|
@@ -28,7 +28,6 @@ const EXTENDED_PART: &str = "word/commentsExtended.xml";
|
|
|
28
28
|
const IDS_PART: &str = "word/commentsIds.xml";
|
|
29
29
|
const EXTENSIBLE_PART: &str = "word/commentsExtensible.xml";
|
|
30
30
|
const CONTENT_TYPES_PART: &str = "[Content_Types].xml";
|
|
31
|
-
const DOCUMENT_RELS_PART: &str = "word/_rels/document.xml.rels";
|
|
32
31
|
|
|
33
32
|
const W15_NS: &str = "http://schemas.microsoft.com/office/word/2012/wordml";
|
|
34
33
|
const W14_NS: &str = "http://schemas.microsoft.com/office/word/2010/wordml";
|
|
@@ -104,15 +103,18 @@ fn register_part(
|
|
|
104
103
|
}
|
|
105
104
|
}
|
|
106
105
|
|
|
107
|
-
//
|
|
108
|
-
let
|
|
106
|
+
// The main document part's relationship.
|
|
107
|
+
let rels_part = super::document_rels_part(package);
|
|
108
|
+
let mut rels = parse_part(package, &rels_part, "")?;
|
|
109
109
|
let root = rels
|
|
110
110
|
.doc
|
|
111
111
|
.root_element()
|
|
112
112
|
.ok_or_else(|| Outcome::error("malformed document.xml.rels"))?;
|
|
113
113
|
let target = part.strip_prefix("word/").unwrap_or(part);
|
|
114
114
|
let exists = rels.doc.children(root).any(|child| {
|
|
115
|
-
rels.is_local(child, "Relationship")
|
|
115
|
+
rels.is_local(child, "Relationship")
|
|
116
|
+
&& rels.doc.attribute(child, "Target") == Some(target)
|
|
117
|
+
&& rels.doc.attribute(child, "Type") == Some(rel_type)
|
|
116
118
|
});
|
|
117
119
|
if !exists {
|
|
118
120
|
let mut max_rid = 0u64;
|
|
@@ -132,7 +134,7 @@ fn register_part(
|
|
|
132
134
|
rels.doc.set_attribute(node, "Type", rel_type);
|
|
133
135
|
rels.doc.set_attribute(node, "Target", target);
|
|
134
136
|
rels.doc.append_child(root, node);
|
|
135
|
-
package.set(
|
|
137
|
+
package.set(&rels_part, rels.serialize());
|
|
136
138
|
}
|
|
137
139
|
Ok(())
|
|
138
140
|
}
|
|
@@ -279,14 +281,35 @@ fn next_para_id(records: &[CommentRecord], extra_occupied: &HashSet<u64>) -> u64
|
|
|
279
281
|
}
|
|
280
282
|
}
|
|
281
283
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
+
/// Paragraph ids in use: the in-memory document's (it may carry repaired or
|
|
285
|
+
/// new ids not yet written) and every `paraId` attribute in any XML part —
|
|
286
|
+
/// other comment paragraphs, headers, footers and notes. A paragraph id
|
|
287
|
+
/// names one paragraph across the whole document.
|
|
288
|
+
fn occupied_para_ids(package: &Package, xml: &DocxXml) -> HashSet<u64> {
|
|
289
|
+
static PARA_ID: std::sync::OnceLock<regex_lite::Regex> = std::sync::OnceLock::new();
|
|
290
|
+
let pattern = PARA_ID.get_or_init(|| {
|
|
291
|
+
regex_lite::Regex::new(r#"paraId(?:Parent)?\s*=\s*["']([0-9A-Fa-f]{8})["']"#).unwrap()
|
|
292
|
+
});
|
|
293
|
+
let mut occupied: HashSet<u64> = xml
|
|
294
|
+
.all_paragraphs()
|
|
284
295
|
.into_iter()
|
|
285
296
|
.filter_map(|node| {
|
|
286
297
|
xml.para_id(node)
|
|
287
298
|
.and_then(|value| u64::from_str_radix(value, 16).ok())
|
|
288
299
|
})
|
|
289
|
-
.collect()
|
|
300
|
+
.collect();
|
|
301
|
+
for (name, bytes) in package.parts() {
|
|
302
|
+
if !name.to_ascii_lowercase().ends_with(".xml") {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
let text = String::from_utf8_lossy(bytes);
|
|
306
|
+
occupied.extend(
|
|
307
|
+
pattern
|
|
308
|
+
.captures_iter(&text)
|
|
309
|
+
.filter_map(|c| u64::from_str_radix(&c[1], 16).ok()),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
occupied
|
|
290
313
|
}
|
|
291
314
|
|
|
292
315
|
/// Deterministic 8-hex durable id (FNV-1a over identifying fields), clamped
|
|
@@ -447,7 +470,7 @@ pub(crate) fn anchor_comment_at(
|
|
|
447
470
|
let comments = parse_part(package, COMMENTS_PART, EMPTY_COMMENTS)?;
|
|
448
471
|
let records = comment_records(&comments);
|
|
449
472
|
let id = next_comment_id(&records);
|
|
450
|
-
let para_id = format!("{:08X}", next_para_id(&records, &
|
|
473
|
+
let para_id = format!("{:08X}", next_para_id(&records, &occupied_para_ids(package, xml)));
|
|
451
474
|
|
|
452
475
|
insert_anchor_markers(xml, paragraph, start, end, id)?;
|
|
453
476
|
append_comment_record(package, id, ¶_id, None, author, date, text)?;
|
|
@@ -736,7 +759,7 @@ pub fn reply_comment_typed(
|
|
|
736
759
|
let parent_para_id = parent.para_id.clone();
|
|
737
760
|
let parent_id = parent.id.clone();
|
|
738
761
|
let id = next_comment_id(&records);
|
|
739
|
-
let para_id = format!("{:08X}", next_para_id(&records, &
|
|
762
|
+
let para_id = format!("{:08X}", next_para_id(&records, &occupied_para_ids(package, xml)));
|
|
740
763
|
append_comment_record(
|
|
741
764
|
package,
|
|
742
765
|
id,
|
|
@@ -101,6 +101,7 @@ pub(super) fn replace(
|
|
|
101
101
|
xml.doc.insert_before(target, holder);
|
|
102
102
|
xml.doc.detach(target);
|
|
103
103
|
xml.doc.append_child(deletion, target);
|
|
104
|
+
crate::segments::mark_deleted_leaves(xml, target, true);
|
|
104
105
|
let ins_id = xml.next_revision_id();
|
|
105
106
|
xml.doc.set_attribute(holder, "w:id", &ins_id.to_string());
|
|
106
107
|
xml.doc.set_attribute(holder, "w:author", author);
|
|
@@ -110,7 +111,7 @@ pub(super) fn replace(
|
|
|
110
111
|
xml.doc.detach(child);
|
|
111
112
|
xml.doc.insert_before(target, child);
|
|
112
113
|
}
|
|
113
|
-
xml
|
|
114
|
+
crate::segments::remove_content(xml, target);
|
|
114
115
|
xml.doc.remove_node(holder);
|
|
115
116
|
}
|
|
116
117
|
Ok((
|
|
@@ -156,8 +157,9 @@ pub(super) fn delete(
|
|
|
156
157
|
xml.doc.insert_before(target, deletion);
|
|
157
158
|
xml.doc.detach(target);
|
|
158
159
|
xml.doc.append_child(deletion, target);
|
|
160
|
+
crate::segments::mark_deleted_leaves(xml, target, true);
|
|
159
161
|
} else {
|
|
160
|
-
xml
|
|
162
|
+
crate::segments::remove_content(xml, target);
|
|
161
163
|
}
|
|
162
164
|
Ok((
|
|
163
165
|
format!("deleted equation {} in paragraph {at}", index + 1),
|
|
@@ -13,7 +13,6 @@ use crate::package::Package;
|
|
|
13
13
|
use super::layout::ensure_sect_pr;
|
|
14
14
|
|
|
15
15
|
const CONTENT_TYPES_PART: &str = "[Content_Types].xml";
|
|
16
|
-
const DOCUMENT_RELS_PART: &str = "word/_rels/document.xml.rels";
|
|
17
16
|
const SETTINGS_CT: &str =
|
|
18
17
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml";
|
|
19
18
|
const SETTINGS_REL: &str =
|
|
@@ -73,13 +72,18 @@ fn ensure_document_rel(
|
|
|
73
72
|
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>",
|
|
74
73
|
"<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"/>",
|
|
75
74
|
);
|
|
76
|
-
let
|
|
75
|
+
let rels_part = super::document_rels_part(package);
|
|
76
|
+
let mut rels = parse_part(package, &rels_part, default)?;
|
|
77
77
|
let root = rels
|
|
78
78
|
.doc
|
|
79
79
|
.root_element()
|
|
80
80
|
.ok_or_else(|| Outcome::error("malformed document.xml.rels"))?;
|
|
81
|
+
// Reuse only a relationship of the same type: a header id that names a
|
|
82
|
+
// footer relationship makes Word refuse the document.
|
|
81
83
|
if let Some(existing) = rels.doc.children(root).find(|&child| {
|
|
82
|
-
rels.is_local(child, "Relationship")
|
|
84
|
+
rels.is_local(child, "Relationship")
|
|
85
|
+
&& rels.doc.attribute(child, "Target") == Some(target)
|
|
86
|
+
&& rels.doc.attribute(child, "Type") == Some(rel_type)
|
|
83
87
|
}) {
|
|
84
88
|
return Ok(rels
|
|
85
89
|
.doc
|
|
@@ -104,7 +108,7 @@ fn ensure_document_rel(
|
|
|
104
108
|
rels.doc.set_attribute(node, "Type", rel_type);
|
|
105
109
|
rels.doc.set_attribute(node, "Target", target);
|
|
106
110
|
rels.doc.append_child(root, node);
|
|
107
|
-
package.set(
|
|
111
|
+
package.set(&rels_part, rels.serialize());
|
|
108
112
|
Ok(id)
|
|
109
113
|
}
|
|
110
114
|
|
|
@@ -188,10 +188,13 @@ fn ensure_property_child(
|
|
|
188
188
|
.iter()
|
|
189
189
|
.position(|name| *name == local)
|
|
190
190
|
.map_or(&[][..], |index| &sequence[index + 1..]);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
191
|
+
// Office extension elements (w14 text effects and the like) close the
|
|
192
|
+
// base sequence, before only the trailing revision record.
|
|
193
|
+
let trailing = sequence.last() == Some(&local);
|
|
194
|
+
let successor = xml.doc.children(parent).find(|&child| {
|
|
195
|
+
successors.iter().any(|name| xml.is_w(child, name))
|
|
196
|
+
|| (!trailing && xml.doc.is_element(child) && is_extension(xml, child))
|
|
197
|
+
});
|
|
195
198
|
if let Some(next) = successor {
|
|
196
199
|
xml.doc.insert_before(next, node);
|
|
197
200
|
} else {
|
|
@@ -200,6 +203,13 @@ fn ensure_property_child(
|
|
|
200
203
|
node
|
|
201
204
|
}
|
|
202
205
|
|
|
206
|
+
/// An element outside WordprocessingML: an Office extension, or the markup
|
|
207
|
+
/// compatibility wrapper that carries one.
|
|
208
|
+
fn is_extension(xml: &DocxXml, node: NodeId) -> bool {
|
|
209
|
+
let ns = xml.doc.node_namespace(node);
|
|
210
|
+
ns.is_some() && ns != Some(crate::document::W_NS)
|
|
211
|
+
}
|
|
212
|
+
|
|
203
213
|
pub(crate) fn set_child_val(xml: &mut DocxXml, parent: NodeId, local: &str, val: &str) {
|
|
204
214
|
let node = if matches!(local, "pStyle" | "jc") {
|
|
205
215
|
ensure_ppr_child(xml, parent, local)
|
|
@@ -27,6 +27,8 @@ pub struct ExecCtx {
|
|
|
27
27
|
pub now: String,
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
const STRICT_W_NS: &str = "http://purl.oclc.org/ooxml/wordprocessingml/main";
|
|
31
|
+
|
|
30
32
|
/// The parsed working set the operation fold runs over: one unzip + one
|
|
31
33
|
/// document.xml parse per typed plan, however many operations follow.
|
|
32
34
|
pub(crate) struct WorkState {
|
|
@@ -62,6 +64,14 @@ impl WorkState {
|
|
|
62
64
|
.map_err(|error| Outcome::error(error.to_string()))?;
|
|
63
65
|
DocxXml::parse(bytes).map_err(Outcome::error)?
|
|
64
66
|
};
|
|
67
|
+
// ISO Strict markup lives in other namespaces: the engine would read
|
|
68
|
+
// it as empty and write Transitional markup into it.
|
|
69
|
+
if xml.doc.root_element().and_then(|root| xml.doc.node_namespace(root)) == Some(STRICT_W_NS) {
|
|
70
|
+
return Err(Outcome::error(
|
|
71
|
+
"this document is saved as Strict Open XML, which docxdriver does not support; \
|
|
72
|
+
save it as a standard Word Document (.docx) and try again",
|
|
73
|
+
));
|
|
74
|
+
}
|
|
65
75
|
let table_positions = table_positions(&xml);
|
|
66
76
|
let table_handles = table_positions.iter().map(|(node, address)| (address.clone(), *node)).collect();
|
|
67
77
|
Ok(WorkState {
|
|
@@ -93,6 +103,16 @@ pub(crate) fn table_positions(xml: &DocxXml) -> HashMap<xmloxide::tree::NodeId,
|
|
|
93
103
|
map
|
|
94
104
|
}
|
|
95
105
|
|
|
106
|
+
/// The relationships part of the main document part (`word/_rels/document.xml.rels`
|
|
107
|
+
/// for the conventional name; a renamed main part has its own).
|
|
108
|
+
pub(crate) fn document_rels_part(package: &crate::package::Package) -> String {
|
|
109
|
+
let main = package.document_part_name();
|
|
110
|
+
match main.rsplit_once('/') {
|
|
111
|
+
Some((dir, file)) => format!("{dir}/_rels/{file}.rels"),
|
|
112
|
+
None => format!("_rels/{main}.rels"),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
96
116
|
/// sha256 of the exact input package bytes.
|
|
97
117
|
pub(crate) fn package_sha256(bytes: &[u8]) -> [u8; 32] {
|
|
98
118
|
use sha2::{Digest, Sha256};
|
|
@@ -100,14 +120,16 @@ pub(crate) fn package_sha256(bytes: &[u8]) -> [u8; 32] {
|
|
|
100
120
|
}
|
|
101
121
|
|
|
102
122
|
/// Attach the serialized package to a successful outcome when something
|
|
103
|
-
/// actually mutated
|
|
123
|
+
/// actually mutated. The one zip write. A dry run builds the bytes too, so
|
|
124
|
+
/// review runs the same integrity gate as commit; callers withhold them.
|
|
104
125
|
///
|
|
105
126
|
/// Every engine write persists paragraph identity first: existing valid
|
|
106
127
|
/// `w14:paraId` values are untouched, while missing/invalid/duplicate and
|
|
107
128
|
/// engine-created paragraphs receive the same deterministic values inspection
|
|
108
129
|
/// showed — one atomic write of repairs plus content operations.
|
|
109
130
|
pub(crate) fn emit(mut state: WorkState, outcome: Outcome, ctx: &ExecCtx) -> Outcome {
|
|
110
|
-
|
|
131
|
+
let _ = ctx;
|
|
132
|
+
if outcome.status != crate::outcome::Status::Ok || !state.mutated {
|
|
111
133
|
return outcome;
|
|
112
134
|
}
|
|
113
135
|
state.flush_lists();
|
|
@@ -219,7 +219,10 @@ pub fn create(command: &Value, ctx: &ExecCtx) -> Outcome {
|
|
|
219
219
|
return Outcome::ok(summary, result);
|
|
220
220
|
}
|
|
221
221
|
match package.to_bytes() {
|
|
222
|
-
Ok(bytes) =>
|
|
222
|
+
Ok(bytes) => match crate::integrity::gate_created(&bytes) {
|
|
223
|
+
Ok(()) => Outcome::ok_bytes(summary, result, bytes),
|
|
224
|
+
Err(failure) => Outcome::error(failure.summary()),
|
|
225
|
+
},
|
|
223
226
|
Err(error) => Outcome::error(error.to_string()),
|
|
224
227
|
}
|
|
225
228
|
}
|