docxdriver 0.3.2__tar.gz → 0.4.0__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.2 → docxdriver-0.4.0}/Cargo.lock +13 -5
- {docxdriver-0.3.2 → docxdriver-0.4.0}/Cargo.toml +1 -1
- {docxdriver-0.3.2 → docxdriver-0.4.0}/PKG-INFO +32 -26
- docxdriver-0.4.0/README.md +87 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/Cargo.toml +6 -0
- docxdriver-0.4.0/crates/docxdriver-core/LICENSE +21 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/api.rs +38 -14
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/layout.rs +11 -4
- docxdriver-0.4.0/crates/docxdriver-core/src/commands/lists.rs +531 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/mod.rs +38 -9
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/readonly.rs +1 -3
- docxdriver-0.4.0/crates/docxdriver-core/src/commands/sections.rs +183 -0
- docxdriver-0.4.0/crates/docxdriver-core/src/commands/table_structure.rs +563 -0
- docxdriver-0.4.0/crates/docxdriver-core/src/commands/tables.rs +925 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/document.rs +109 -16
- docxdriver-0.4.0/crates/docxdriver-core/src/facade.rs +200 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/build.rs +41 -22
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/ctx.rs +4 -1
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/input.rs +5 -1
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/render.rs +6 -5
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/lib.rs +4 -1
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-python/Cargo.toml +2 -0
- docxdriver-0.4.0/crates/docxdriver-python/README.md +87 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0/crates/docxdriver-python}/python/docxdriver/__init__.py +2 -1
- docxdriver-0.4.0/crates/docxdriver-python/python/docxdriver/document.py +126 -0
- docxdriver-0.4.0/crates/docxdriver-python/python/docxdriver/plan.py +570 -0
- docxdriver-0.4.0/crates/docxdriver-python/src/lib.rs +199 -0
- docxdriver-0.4.0/crates/docxdriver-python/tests/test_document.py +75 -0
- docxdriver-0.4.0/crates/docxdriver-python/tests/test_plan.py +40 -0
- {docxdriver-0.3.2/crates/docxdriver-python → docxdriver-0.4.0}/python/docxdriver/__init__.py +2 -1
- docxdriver-0.4.0/python/docxdriver/document.py +126 -0
- docxdriver-0.4.0/python/docxdriver/plan.py +570 -0
- docxdriver-0.3.2/README.md +0 -81
- docxdriver-0.3.2/crates/docxdriver-core/examples/list_scaling.rs +0 -71
- docxdriver-0.3.2/crates/docxdriver-core/examples/plan_scaling.rs +0 -43
- docxdriver-0.3.2/crates/docxdriver-core/src/bin/generate_typed_schema.rs +0 -171
- docxdriver-0.3.2/crates/docxdriver-core/src/commands/lists.rs +0 -141
- docxdriver-0.3.2/crates/docxdriver-core/src/commands/table_structure.rs +0 -100
- docxdriver-0.3.2/crates/docxdriver-core/src/commands/tables.rs +0 -128
- docxdriver-0.3.2/crates/docxdriver-core/src/html/README.md +0 -14
- docxdriver-0.3.2/crates/docxdriver-core/tests/README.md +0 -35
- docxdriver-0.3.2/crates/docxdriver-core/tests/fixtures/projection-parity/features.json +0 -33
- docxdriver-0.3.2/crates/docxdriver-core/tests/fixtures/projection-parity/parity.docx +0 -0
- docxdriver-0.3.2/crates/docxdriver-core/tests/fixtures/projection-parity/prior-projection.json +0 -1393
- docxdriver-0.3.2/crates/docxdriver-core/tests/fixtures/ts-reference.docx +0 -0
- docxdriver-0.3.2/crates/docxdriver-core/tests/formatting_fidelity.rs +0 -840
- docxdriver-0.3.2/crates/docxdriver-core/tests/html_e2e.rs +0 -897
- docxdriver-0.3.2/crates/docxdriver-core/tests/interop.rs +0 -99
- docxdriver-0.3.2/crates/docxdriver-core/tests/package_layout.rs +0 -126
- docxdriver-0.3.2/crates/docxdriver-core/tests/projection_parity.rs +0 -531
- docxdriver-0.3.2/crates/docxdriver-core/tests/spike.rs +0 -44
- docxdriver-0.3.2/crates/docxdriver-core/tests/typed_api.rs +0 -2499
- docxdriver-0.3.2/crates/docxdriver-python/README.md +0 -81
- docxdriver-0.3.2/crates/docxdriver-python/python/docxdriver/repl/__init__.py +0 -7
- docxdriver-0.3.2/crates/docxdriver-python/python/docxdriver/repl/host.py +0 -562
- docxdriver-0.3.2/crates/docxdriver-python/python/docxdriver/repl/prelude.py +0 -3
- docxdriver-0.3.2/crates/docxdriver-python/python/docxdriver/repl/quotes.py +0 -325
- docxdriver-0.3.2/crates/docxdriver-python/src/lib.rs +0 -62
- docxdriver-0.3.2/crates/docxdriver-python/tests/test_host.py +0 -308
- docxdriver-0.3.2/python/docxdriver/repl/__init__.py +0 -7
- docxdriver-0.3.2/python/docxdriver/repl/host.py +0 -562
- docxdriver-0.3.2/python/docxdriver/repl/prelude.py +0 -3
- docxdriver-0.3.2/python/docxdriver/repl/quotes.py +0 -325
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/README.md +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/assets/blank.docx +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/comments.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/equations.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/headers.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/reading.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/revisions.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/typed.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/assets.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/chrome.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/compact.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/mathml_omml.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/mod.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/omml_latex.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/omml_mathml.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/parse.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/styles.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/surface.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/media.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/outcome.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/package.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-core/src/segments.rs +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/crates/docxdriver-python/.gitignore +0 -0
- {docxdriver-0.3.2 → docxdriver-0.4.0}/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.0"
|
|
420
420
|
dependencies = [
|
|
421
421
|
"clap",
|
|
422
422
|
"docxdriver-core",
|
|
@@ -427,7 +427,7 @@ dependencies = [
|
|
|
427
427
|
|
|
428
428
|
[[package]]
|
|
429
429
|
name = "docxdriver-core"
|
|
430
|
-
version = "0.
|
|
430
|
+
version = "0.4.0"
|
|
431
431
|
dependencies = [
|
|
432
432
|
"hex",
|
|
433
433
|
"latex2mathml",
|
|
@@ -444,7 +444,7 @@ dependencies = [
|
|
|
444
444
|
|
|
445
445
|
[[package]]
|
|
446
446
|
name = "docxdriver-mcp"
|
|
447
|
-
version = "0.
|
|
447
|
+
version = "0.4.0"
|
|
448
448
|
dependencies = [
|
|
449
449
|
"axum",
|
|
450
450
|
"cap-std",
|
|
@@ -460,18 +460,26 @@ dependencies = [
|
|
|
460
460
|
"tokio-util",
|
|
461
461
|
]
|
|
462
462
|
|
|
463
|
+
[[package]]
|
|
464
|
+
name = "docxdriver-monty-repl"
|
|
465
|
+
version = "0.4.0"
|
|
466
|
+
dependencies = [
|
|
467
|
+
"docxdriver-core",
|
|
468
|
+
]
|
|
469
|
+
|
|
463
470
|
[[package]]
|
|
464
471
|
name = "docxdriver-python"
|
|
465
|
-
version = "0.
|
|
472
|
+
version = "0.4.0"
|
|
466
473
|
dependencies = [
|
|
467
474
|
"docxdriver-core",
|
|
468
475
|
"pyo3",
|
|
476
|
+
"serde",
|
|
469
477
|
"serde_json",
|
|
470
478
|
]
|
|
471
479
|
|
|
472
480
|
[[package]]
|
|
473
481
|
name = "docxdriver-wasm"
|
|
474
|
-
version = "0.
|
|
482
|
+
version = "0.4.0"
|
|
475
483
|
dependencies = [
|
|
476
484
|
"docxdriver-core",
|
|
477
485
|
"js-sys",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docxdriver
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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 }
|
|
@@ -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.
|
|
@@ -159,9 +159,9 @@ pub struct RowFormat {
|
|
|
159
159
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
|
160
160
|
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
|
161
161
|
pub enum TableEdit {
|
|
162
|
-
InsertRows { row: u32, position: InsertPosition, count: u32 },
|
|
162
|
+
InsertRows { row: u32, position: InsertPosition, count: u32, #[serde(rename = "as", default)] alias: Option<String> },
|
|
163
163
|
DeleteRows { row: u32, count: u32 },
|
|
164
|
-
InsertColumns { column: u32, position: InsertPosition, count: u32 },
|
|
164
|
+
InsertColumns { column: u32, position: InsertPosition, count: u32, #[serde(rename = "as", default)] alias: Option<String> },
|
|
165
165
|
DeleteColumns { column: u32, count: u32 },
|
|
166
166
|
MergeCells { row: u32, column: u32, row_span: u32, column_span: u32 },
|
|
167
167
|
SplitCell { row: u32, column: u32 },
|
|
@@ -665,7 +665,9 @@ pub enum EditOp {
|
|
|
665
665
|
gutter: Option<Points>,
|
|
666
666
|
},
|
|
667
667
|
/// Selective section-property edits. Dimensions are points; omitted fields preserve source.
|
|
668
|
+
SetSectionBoundary { at: ParagraphAddress, #[serde(default)] remove: bool, #[serde(default)] break_type: Option<String> },
|
|
668
669
|
SetSectionLayout {
|
|
670
|
+
#[serde(default)] section_handle: Option<String>,
|
|
669
671
|
#[serde(default)]
|
|
670
672
|
section: Option<u32>,
|
|
671
673
|
#[serde(default)]
|
|
@@ -1480,7 +1482,7 @@ fn normalize_op(op: &mut EditOp) -> Result<(), String> {
|
|
|
1480
1482
|
return Err("levels requires a new list with 1..9 definitions covering the selected level".into());
|
|
1481
1483
|
}
|
|
1482
1484
|
for (i, def) in defs.iter().enumerate() {
|
|
1483
|
-
if !["bullet","decimal","lowerLetter","upperLetter","lowerRoman","upperRoman"].contains(&def.format.as_str()) || def.start ==
|
|
1485
|
+
if !["bullet","decimal","lowerLetter","upperLetter","lowerRoman","upperRoman"].contains(&def.format.as_str()) || def.start.is_some_and(|v|v == 0 || v > i32::MAX as u32) || def.label.is_empty() || def.label.len() > 255 || def.restart_after.is_some_and(|v| v as usize >= i) {
|
|
1484
1486
|
return Err("invalid list level format, label, start or restart ancestor".into());
|
|
1485
1487
|
}
|
|
1486
1488
|
if [def.indent_left,def.indent_right,def.first_line_indent].into_iter().flatten().any(|v| v.0.unsigned_abs() > 2_000_000) { return Err("list indentation is out of range".into()); }
|
|
@@ -1491,8 +1493,8 @@ fn normalize_op(op: &mut EditOp) -> Result<(), String> {
|
|
|
1491
1493
|
if def.marker_font.as_ref().is_some_and(|v| v.is_empty()) || def.marker_color.as_ref().is_some_and(|v| v.len() != 6 || !v.bytes().all(|c| c.is_ascii_hexdigit())) { return Err("invalid list marker font or six-digit RGB color".into()); }
|
|
1492
1494
|
}
|
|
1493
1495
|
}
|
|
1494
|
-
if at.is_empty() || *level > 8 ||
|
|
1495
|
-
return Err("set_list requires paragraphs, level 0..8 and
|
|
1496
|
+
if at.is_empty() || *level > 8 || start.is_some_and(|v|v == 0 || v > i32::MAX as u32) || num_id.is_some_and(|v|v == 0 || v > i32::MAX as u32) {
|
|
1497
|
+
return Err("set_list requires paragraphs, level 0..8 and IDs/starts in 1..2147483647".into());
|
|
1496
1498
|
}
|
|
1497
1499
|
if *remove && (num_id.is_some() || format.is_some() || start.is_some() || *level != 0) {
|
|
1498
1500
|
return Err("set_list remove cannot be combined with list settings".into());
|
|
@@ -1516,7 +1518,13 @@ fn normalize_op(op: &mut EditOp) -> Result<(), String> {
|
|
|
1516
1518
|
return Err("at must not be empty".into());
|
|
1517
1519
|
}
|
|
1518
1520
|
}
|
|
1519
|
-
EditOp::
|
|
1521
|
+
EditOp::SetSectionBoundary {remove,break_type,..} => {
|
|
1522
|
+
if *remove && break_type.is_some(){return Err("removing a boundary cannot set break_type".into());}
|
|
1523
|
+
if break_type.as_ref().is_some_and(|v|!matches!(v.as_str(),"nextPage"|"continuous"|"oddPage"|"evenPage"|"nextColumn")){return Err("unsupported section break_type".into());}
|
|
1524
|
+
}
|
|
1525
|
+
EditOp::SetSectionLayout { section, section_handle, page_size, width, height, orientation, columns, column_spacing, column_widths, column_separator, break_type, page_number_format, page_number_start, page_number_continue } => {
|
|
1526
|
+
if section.is_some() && section_handle.is_some() {return Err("section and section_handle are mutually exclusive".into());}
|
|
1527
|
+
if section_handle.as_ref().is_some_and(|v|!v.starts_with("section:")){return Err("section_handle must use an inventory handle".into());}
|
|
1520
1528
|
if section == &Some(0) { return Err("section must be positive".into()); }
|
|
1521
1529
|
if let Some(size) = page_size { if !matches!(size.as_str(), "letter" | "legal" | "a4" | "a3" | "a5") { return Err("page_size must be letter/legal/a4/a3/a5".into()); } }
|
|
1522
1530
|
if page_size.is_some() && (width.is_some() || height.is_some()) { return Err("page_size conflicts with width/height".into()); }
|
|
@@ -1524,11 +1532,13 @@ fn normalize_op(op: &mut EditOp) -> Result<(), String> {
|
|
|
1524
1532
|
if [width.as_ref(), height.as_ref()].into_iter().flatten().any(|value| value.0 <= 0 || value.0 > 31680) { return Err("page dimensions must be >0 and <=1584 points".into()); }
|
|
1525
1533
|
if orientation.as_ref().is_some_and(|v| !matches!(v.as_str(), "portrait" | "landscape")) { return Err("orientation must be portrait/landscape".into()); }
|
|
1526
1534
|
if columns.is_some_and(|v| v == 0 || v > 45) { return Err("columns must be between 1 and 45".into()); }
|
|
1527
|
-
if
|
|
1535
|
+
if column_spacing.is_some_and(|p| !(0..=31680).contains(&p.0)) { return Err("column_spacing must be between 0 and 1584 points".into()); }
|
|
1536
|
+
if column_widths.as_ref().is_some_and(|v| v.is_empty() || v.len() > 45 || v.iter().any(|p| p.0 <= 0 || p.0 > 31680)) { return Err("column_widths requires 1..45 positive widths of at most 1584 points".into()); }
|
|
1528
1537
|
if let (Some(count), Some(widths)) = (columns.as_ref(), column_widths.as_ref()) { if *count as usize != widths.len() { return Err("columns must match column_widths length".into()); } }
|
|
1529
1538
|
if break_type.as_ref().is_some_and(|v| !matches!(v.as_str(), "nextPage" | "continuous" | "oddPage" | "evenPage" | "nextColumn")) { return Err("unsupported section break_type".into()); }
|
|
1530
1539
|
if page_number_format.as_ref().is_some_and(|v| !matches!(v.as_str(), "decimal" | "upperRoman" | "lowerRoman" | "upperLetter" | "lowerLetter")) { return Err("unsupported page_number_format".into()); }
|
|
1531
|
-
if
|
|
1540
|
+
if page_number_continue == &Some(false) { return Err("page_number_continue only accepts true; use page_number_start to restart".into()); }
|
|
1541
|
+
if page_number_start.is_some_and(|value| value == 0 || value > i32::MAX as u32) { return Err("page_number_start must be between 1 and 2147483647".into()); }
|
|
1532
1542
|
if page_number_start.is_some() && page_number_continue == &Some(true) { return Err("page_number_start conflicts with page_number_continue".into()); }
|
|
1533
1543
|
if page_size.is_none() && width.is_none() && orientation.is_none() && columns.is_none() && column_spacing.is_none() && column_widths.is_none() && column_separator.is_none() && break_type.is_none() && page_number_format.is_none() && page_number_start.is_none() && page_number_continue.is_none() { return Err("set_section_layout requires a property".into()); }
|
|
1534
1544
|
}
|
|
@@ -1833,6 +1843,7 @@ fn fold_typed(
|
|
|
1833
1843
|
let resolved = match resolve_typed_aliases(op, &aliases) {
|
|
1834
1844
|
Ok(value) => value,
|
|
1835
1845
|
Err(error) => {
|
|
1846
|
+
attach_affected(&mut state, &mut reports, &affected_ids);
|
|
1836
1847
|
let report = PlanReport {
|
|
1837
1848
|
completed: index,
|
|
1838
1849
|
stopped_at: Some(index),
|
|
@@ -1872,7 +1883,7 @@ fn fold_typed(
|
|
|
1872
1883
|
report.summary.push_str(¬e);
|
|
1873
1884
|
}
|
|
1874
1885
|
}
|
|
1875
|
-
attach_affected(&state, &mut reports, &affected_ids);
|
|
1886
|
+
attach_affected(&mut state, &mut reports, &affected_ids);
|
|
1876
1887
|
let report = PlanReport {
|
|
1877
1888
|
completed: index,
|
|
1878
1889
|
stopped_at: Some(index),
|
|
@@ -1892,10 +1903,11 @@ fn fold_typed(
|
|
|
1892
1903
|
);
|
|
1893
1904
|
}
|
|
1894
1905
|
if let EditOp::InsertParagraph { alias: Some(alias), .. }
|
|
1895
|
-
| EditOp::InsertTable { alias: Some(alias), .. }
|
|
1906
|
+
| EditOp::InsertTable { alias: Some(alias), .. }
|
|
1907
|
+
| EditOp::EditTable { action: TableEdit::InsertRows { alias: Some(alias), .. } | TableEdit::InsertColumns { alias: Some(alias), .. }, .. } = op
|
|
1896
1908
|
{
|
|
1897
1909
|
if aliases.contains_key(alias) {
|
|
1898
|
-
attach_affected(&state, &mut reports, &affected_ids);
|
|
1910
|
+
attach_affected(&mut state, &mut reports, &affected_ids);
|
|
1899
1911
|
let report = PlanReport {
|
|
1900
1912
|
completed: index,
|
|
1901
1913
|
stopped_at: Some(index),
|
|
@@ -1917,7 +1929,7 @@ fn fold_typed(
|
|
|
1917
1929
|
)),
|
|
1918
1930
|
);
|
|
1919
1931
|
}
|
|
1920
|
-
if
|
|
1932
|
+
if matches!(op, EditOp::InsertTable { .. } | EditOp::EditTable { .. }) {
|
|
1921
1933
|
aliases.insert(alias.clone(), Value::String(format!("${alias}")));
|
|
1922
1934
|
}
|
|
1923
1935
|
if let Some(id) = outcome
|
|
@@ -1931,7 +1943,7 @@ fn fold_typed(
|
|
|
1931
1943
|
}
|
|
1932
1944
|
}
|
|
1933
1945
|
}
|
|
1934
|
-
attach_affected(&state, &mut reports, &affected_ids);
|
|
1946
|
+
attach_affected(&mut state, &mut reports, &affected_ids);
|
|
1935
1947
|
let report = PlanReport {
|
|
1936
1948
|
completed: plan.ops.len(),
|
|
1937
1949
|
stopped_at: None,
|
|
@@ -2013,10 +2025,11 @@ fn explain_selector_drift(
|
|
|
2013
2025
|
/// Fill each operation report with the rendered markup of the paragraphs it
|
|
2014
2026
|
/// affected, as they stand in `state`, from one render of the document.
|
|
2015
2027
|
fn attach_affected(
|
|
2016
|
-
state: &crate::commands::WorkState,
|
|
2028
|
+
state: &mut crate::commands::WorkState,
|
|
2017
2029
|
reports: &mut [OperationReport],
|
|
2018
2030
|
affected_ids: &[Vec<String>],
|
|
2019
2031
|
) {
|
|
2032
|
+
state.flush_lists();
|
|
2020
2033
|
let wanted = affected_ids.iter().flatten().cloned().collect();
|
|
2021
2034
|
let markup = crate::commands::affected_markup(state, &wanted);
|
|
2022
2035
|
for (report, ids) in reports.iter_mut().zip(affected_ids) {
|
|
@@ -2058,6 +2071,7 @@ fn operation_name(op: &EditOp) -> &'static str {
|
|
|
2058
2071
|
EditOp::InsertParagraph { .. } => "insert_paragraph",
|
|
2059
2072
|
EditOp::DeleteParagraphs { .. } => "delete_paragraphs",
|
|
2060
2073
|
EditOp::SetPageMargins { .. } => "set_page_margins",
|
|
2074
|
+
EditOp::SetSectionBoundary { .. } => "set_section_boundary",
|
|
2061
2075
|
EditOp::SetSectionLayout { .. } => "set_section_layout",
|
|
2062
2076
|
EditOp::SetFirstPageMode { .. } => "set_first_page_mode",
|
|
2063
2077
|
EditOp::SetEvenAndOddHeaders { .. } => "set_even_and_odd_headers",
|
|
@@ -2097,6 +2111,7 @@ fn resolve_typed_aliases(
|
|
|
2097
2111
|
| EditOp::ReplaceParagraph { at, .. }
|
|
2098
2112
|
| EditOp::FormatText { at, .. }
|
|
2099
2113
|
| EditOp::InsertTable { at, .. }
|
|
2114
|
+
| EditOp::SetSectionBoundary { at, .. }
|
|
2100
2115
|
| EditOp::FormatParagraph { at, .. }
|
|
2101
2116
|
| EditOp::CommentAdd { at, .. } => resolve(at)?,
|
|
2102
2117
|
EditOp::ReviseText { at: Some(at), .. } => resolve(at)?,
|
|
@@ -2110,6 +2125,12 @@ fn resolve_typed_aliases(
|
|
|
2110
2125
|
resolve(address)?;
|
|
2111
2126
|
}
|
|
2112
2127
|
}
|
|
2128
|
+
EditOp::SetSectionLayout { section_handle:Some(handle),.. } => {
|
|
2129
|
+
if let Some(alias)=handle.strip_prefix("section:$") {
|
|
2130
|
+
let Some(Value::String(id))=aliases.get(alias) else { return Err(diag("unknown_alias",format!("unknown section paragraph alias ${alias}"),None)); };
|
|
2131
|
+
*handle=format!("section:{id}");
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2113
2134
|
_ => {}
|
|
2114
2135
|
}
|
|
2115
2136
|
Ok(resolved)
|
|
@@ -2305,6 +2326,7 @@ fn typed_read(input: Option<&[u8]>, kind: &str, view: Option<&str>) -> crate::ou
|
|
|
2305
2326
|
map.insert("markup".into(), serde_json::json!(markup));
|
|
2306
2327
|
map.insert("css".into(), serde_json::json!(css));
|
|
2307
2328
|
map.insert("tables".into(), crate::commands::tables::inventory(&state.xml));
|
|
2329
|
+
map.insert("sections".into(), crate::commands::sections::inventory(&state.package,&state.xml,¶graph_ids,view==Some("original")));
|
|
2308
2330
|
map.insert("styles".into(), serde_json::json!(crate::html::styles::Styles::load(&state.package).catalog()));
|
|
2309
2331
|
if !equations.is_empty() {
|
|
2310
2332
|
map.insert("equations".into(), serde_json::json!(equations));
|
|
@@ -2379,6 +2401,7 @@ fn typed_read(input: Option<&[u8]>, kind: &str, view: Option<&str>) -> crate::ou
|
|
|
2379
2401
|
}).collect();
|
|
2380
2402
|
let mut result = result;
|
|
2381
2403
|
if let Some(map) = result.result.as_mut().and_then(Value::as_object_mut) {
|
|
2404
|
+
map.insert("sections".into(),crate::commands::sections::inventory(&state.package,&state.xml,¶graph_ids,view==Some("original")));
|
|
2382
2405
|
if let Some(Value::Array(paragraphs)) = map.get("paragraphs") {
|
|
2383
2406
|
let equations: Vec<Value> = paragraphs
|
|
2384
2407
|
.iter()
|
|
@@ -2409,6 +2432,7 @@ fn typed_read(input: Option<&[u8]>, kind: &str, view: Option<&str>) -> crate::ou
|
|
|
2409
2432
|
serde_json::json!(crate::html::assets::manifest(&state.package, false)),
|
|
2410
2433
|
);
|
|
2411
2434
|
map.insert("tables".into(), crate::commands::tables::inventory(&state.xml));
|
|
2435
|
+
map.insert("sections".into(), crate::commands::sections::inventory(&state.package,&state.xml,¶graph_ids,view==Some("original")));
|
|
2412
2436
|
map.insert("html".into(), Value::String(rendered.html));
|
|
2413
2437
|
}
|
|
2414
2438
|
if let Some(map) = result.result.as_mut().and_then(Value::as_object_mut) {
|
|
@@ -346,19 +346,22 @@ pub fn set_section_layout(
|
|
|
346
346
|
if page_size.is_some() || width.is_some() || orientation.is_some() {
|
|
347
347
|
if (orient == "landscape" && w < h) || (orient == "portrait" && w > h) { std::mem::swap(&mut w, &mut h); }
|
|
348
348
|
}
|
|
349
|
-
let area = w
|
|
349
|
+
let area = w.saturating_sub(dimension("pgMar", "left", 1440)).saturating_sub(dimension("pgMar", "right", 1440)).saturating_sub(dimension("pgMar", "gutter", 0));
|
|
350
350
|
let cols = xml.child_w(sect, "cols");
|
|
351
351
|
let old_widths: Vec<i64> = cols.map(|n| xml.doc.children(n).filter(|&n| xml.is_w(n, "col")).filter_map(|n| xml.attr(n, "w").and_then(|v| v.parse().ok())).collect()).unwrap_or_default();
|
|
352
352
|
let count = columns.map(|v| v as i64).or_else(|| column_widths.as_ref().map(|v| v.len() as i64)).unwrap_or_else(|| dimension("cols", "num", if old_widths.is_empty() { 1 } else { old_widths.len() as i64 }));
|
|
353
353
|
let gap = column_spacing.map(|p| p.0).unwrap_or_else(|| dimension("cols", "space", 720));
|
|
354
|
+
let original_gaps = cols.map(|n| xml.doc.children(n).filter(|&n| xml.is_w(n, "col")).collect::<Vec<_>>()).unwrap_or_default();
|
|
355
|
+
let inherited_gap_total = original_gaps.iter().take(original_gaps.len().saturating_sub(1)).fold(0_i64, |total, &n| total.saturating_add(xml.attr(n, "space").and_then(|v| v.parse::<i64>().ok()).unwrap_or(gap)));
|
|
356
|
+
let gap_total = if column_spacing.is_none() && column_widths.is_none() && columns.is_none() && !old_widths.is_empty() { inherited_gap_total } else { gap.saturating_mul(count.saturating_sub(1)) };
|
|
354
357
|
let unequal: Vec<i64> = column_widths.as_ref().map(|v| v.iter().map(|p| p.0).collect()).unwrap_or_else(|| old_widths.clone());
|
|
355
358
|
let changes_columns = columns.is_some() || column_spacing.is_some() || column_widths.is_some() || column_separator.is_some();
|
|
356
359
|
if changes_columns || width.is_some() || orientation.is_some() || page_size.is_some() {
|
|
357
|
-
if area <= 0 ||
|
|
358
|
-
if !unequal.is_empty() && columns.is_none() && (unequal.iter().
|
|
360
|
+
if area <= 0 || gap_total >= area { return Err(Outcome::blocked("columns do not fit the section text area")); }
|
|
361
|
+
if !unequal.is_empty() && columns.is_none() && (unequal.iter().fold(0_i64, |total, width| total.saturating_add(*width)).saturating_add(gap_total) > area) {
|
|
359
362
|
return Err(Outcome::blocked("column widths and spacing exceed the section text area"));
|
|
360
363
|
}
|
|
361
|
-
if let Some(widths) = column_widths { if widths.iter().
|
|
364
|
+
if let Some(widths) = column_widths { if widths.iter().fold(0_i64, |total, p| total.saturating_add(p.0)).saturating_add(gap_total) > area { return Err(Outcome::blocked("column widths and spacing exceed the section text area")); } }
|
|
362
365
|
}
|
|
363
366
|
if tracked { snapshot_section_properties(xml, sect, author, ctx)?; }
|
|
364
367
|
if page_size.is_some() || width.is_some() || orientation.is_some() {
|
|
@@ -375,6 +378,10 @@ pub fn set_section_layout(
|
|
|
375
378
|
for child in xml.doc.children(node).collect::<Vec<_>>() { if xml.is_w(child, "col") { xml.doc.remove_node(child); } }
|
|
376
379
|
set_attr(xml, node, "w:equalWidth", if column_widths.is_some() { "0" } else { "1" });
|
|
377
380
|
}
|
|
381
|
+
if column_spacing.is_some() && column_widths.is_none() && columns.is_none() {
|
|
382
|
+
let children: Vec<_> = xml.doc.children(node).filter(|&n| xml.is_w(n, "col")).collect();
|
|
383
|
+
for (index, &col) in children.iter().enumerate() { set_twips_attr(xml, col, "space", if index + 1 == children.len() { 0 } else { gap }); }
|
|
384
|
+
}
|
|
378
385
|
if let Some(widths) = column_widths {
|
|
379
386
|
for (index, width) in widths.iter().enumerate() {
|
|
380
387
|
let col = xml.doc.create_element("w:col");
|