docx-editor 0.2.2__tar.gz → 0.2.3__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.
- {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude-plugin/plugin.json +1 -1
- docx_editor-0.2.3/.github/workflows/on-release-main.yml +109 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/.gitignore +2 -1
- {docx_editor-0.2.2 → docx_editor-0.2.3}/CONTRIBUTING.md +38 -11
- {docx_editor-0.2.2 → docx_editor-0.2.3}/PKG-INFO +1 -1
- {docx_editor-0.2.2 → docx_editor-0.2.3}/docs/api.md +145 -13
- docx_editor-0.2.3/docs/index.md +62 -0
- docx_editor-0.2.3/docs/quickstart.md +218 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/__init__.py +6 -1
- {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/comments.py +2 -6
- {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/ooxml/pack.py +28 -6
- {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/track_changes.py +37 -23
- {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/workspace.py +3 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/xml_editor.py +35 -16
- {docx_editor-0.2.2 → docx_editor-0.2.3}/pyproject.toml +1 -1
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_comments.py +32 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_document.py +20 -0
- docx_editor-0.2.3/tests/test_ooxml.py +187 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_track_changes.py +241 -26
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_xml_editor.py +58 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tox.ini +1 -1
- {docx_editor-0.2.2 → docx_editor-0.2.3}/uv.lock +1 -1
- docx_editor-0.2.2/.github/workflows/on-release-main.yml +0 -68
- docx_editor-0.2.2/docs/index.md +0 -55
- docx_editor-0.2.2/docs/quickstart.md +0 -245
- docx_editor-0.2.2/tests/test_ooxml.py +0 -72
- {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude/commands/opsx/apply.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude/commands/opsx/archive.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude/commands/opsx/explore.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude/commands/opsx/propose.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude/skills/openspec-apply-change/SKILL.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude/skills/openspec-archive-change/SKILL.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude/skills/openspec-explore/SKILL.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude/skills/openspec-propose/SKILL.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude-plugin/marketplace.json +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/.github/actions/setup-python-env/action.yml +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/.github/workflows/main.yml +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/.github/workflows/validate-codecov-config.yml +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/.pre-commit-config.yaml +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/AGENTS.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/CLAUDE.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/LICENSE +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/Makefile +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/README.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/benchmarks/hash_anchored_vs_plain.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/codecov.yaml +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/document.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/exceptions.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/ooxml/__init__.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/ooxml/templates/comments.xml +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/ooxml/templates/commentsExtended.xml +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/ooxml/templates/commentsExtensible.xml +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/ooxml/templates/commentsIds.xml +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/ooxml/templates/people.xml +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/ooxml/unpack.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/mkdocs.yml +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-batch-edit/proposal.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-batch-edit/specs/text-operations/spec.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-batch-edit/tasks.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-paragraph-hash-anchors/design.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-paragraph-hash-anchors/proposal.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-paragraph-hash-anchors/specs/text-operations/spec.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-paragraph-hash-anchors/tasks.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-rewrite-paragraph/design.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-rewrite-paragraph/proposal.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-rewrite-paragraph/specs/text-operations/spec.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-rewrite-paragraph/tasks.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/archive/2026-01-29-add-cross-boundary-text-operations/design.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/archive/2026-01-29-add-cross-boundary-text-operations/proposal.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/archive/2026-01-29-add-cross-boundary-text-operations/specs/text-operations/spec.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/archive/2026-01-29-add-cross-boundary-text-operations/tasks.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/archive/2026-04-17-add-structured-error-types/.openspec.yaml +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/archive/2026-04-17-add-structured-error-types/design.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/archive/2026-04-17-add-structured-error-types/proposal.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/archive/2026-04-17-add-structured-error-types/specs/structured-errors/spec.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/archive/2026-04-17-add-structured-error-types/tasks.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/config.yaml +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/project.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/specs/structured-errors/spec.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/specs/text-operations/spec.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/skills/docx/SKILL.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/skills/docx/docx-js.md +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/conftest.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_batch_edit.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_coverage_gaps.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_cross_boundary_replace.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_data/empty.docx +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_data/long_content.docx +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_data/search_fixture.docx +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_data/simple.docx +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_data/special_chars.docx +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_data/test_document_with_errors.docx +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_data/with_tables.docx +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_error_quality.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_mixed_state.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_multi_wt.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_multi_wt_safety.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_nested_ins.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_paragraph_hash.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_review_fixes.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_rewrite_paragraph.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_text_map.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_track_changes_coverage.py +0 -0
- {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_workspace.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docx-editor",
|
|
3
3
|
"description": "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.3",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "pablospe"
|
|
7
7
|
},
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
name: release-main
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
|
|
9
|
+
set-version:
|
|
10
|
+
runs-on: ubuntu-24.04
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- name: Export tag
|
|
15
|
+
id: vars
|
|
16
|
+
run: |
|
|
17
|
+
release_version="${GITHUB_REF_NAME}"
|
|
18
|
+
if [[ ! "$release_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
19
|
+
echo "Release tag must be a plain X.Y.Z version, for example 0.3.0. Do not use a v prefix." >&2
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
echo "version=$release_version" >> "$GITHUB_OUTPUT"
|
|
23
|
+
if: ${{ github.event_name == 'release' }}
|
|
24
|
+
|
|
25
|
+
- name: Validate checked-in versions
|
|
26
|
+
run: |
|
|
27
|
+
python - <<'PY'
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import sys
|
|
31
|
+
import tomllib
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
expected = os.environ["RELEASE_VERSION"]
|
|
35
|
+
pyproject = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
|
|
36
|
+
plugin = json.loads(Path(".claude-plugin/plugin.json").read_text())["version"]
|
|
37
|
+
lock = tomllib.loads(Path("uv.lock").read_text())
|
|
38
|
+
locked = next(
|
|
39
|
+
package["version"]
|
|
40
|
+
for package in lock["package"]
|
|
41
|
+
if package["name"] == "docx-editor"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
versions = {
|
|
45
|
+
"release tag": expected,
|
|
46
|
+
"pyproject.toml": pyproject,
|
|
47
|
+
"uv.lock": locked,
|
|
48
|
+
".claude-plugin/plugin.json": plugin,
|
|
49
|
+
}
|
|
50
|
+
mismatches = {name: version for name, version in versions.items() if version != expected}
|
|
51
|
+
if mismatches:
|
|
52
|
+
for name, version in versions.items():
|
|
53
|
+
print(f"{name}: {version}", file=sys.stderr)
|
|
54
|
+
raise SystemExit("Release versions must all match before publishing.")
|
|
55
|
+
PY
|
|
56
|
+
env:
|
|
57
|
+
RELEASE_VERSION: ${{ steps.vars.outputs.version }}
|
|
58
|
+
if: ${{ github.event_name == 'release' }}
|
|
59
|
+
|
|
60
|
+
- name: Update project version
|
|
61
|
+
run: |
|
|
62
|
+
sed -i "s/^version = \".*\"/version = \"$RELEASE_VERSION\"/" pyproject.toml
|
|
63
|
+
env:
|
|
64
|
+
RELEASE_VERSION: ${{ steps.vars.outputs.version }}
|
|
65
|
+
if: ${{ github.event_name == 'release' }}
|
|
66
|
+
|
|
67
|
+
- name: Upload updated pyproject.toml
|
|
68
|
+
uses: actions/upload-artifact@v4
|
|
69
|
+
with:
|
|
70
|
+
name: pyproject-toml
|
|
71
|
+
path: pyproject.toml
|
|
72
|
+
|
|
73
|
+
publish:
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
needs: [set-version]
|
|
76
|
+
permissions:
|
|
77
|
+
id-token: write # Required for trusted publishing
|
|
78
|
+
steps:
|
|
79
|
+
- name: Check out
|
|
80
|
+
uses: actions/checkout@v4
|
|
81
|
+
|
|
82
|
+
- name: Set up the environment
|
|
83
|
+
uses: ./.github/actions/setup-python-env
|
|
84
|
+
|
|
85
|
+
- name: Download updated pyproject.toml
|
|
86
|
+
uses: actions/download-artifact@v4
|
|
87
|
+
with:
|
|
88
|
+
name: pyproject-toml
|
|
89
|
+
|
|
90
|
+
- name: Build package
|
|
91
|
+
run: uv build
|
|
92
|
+
|
|
93
|
+
- name: Publish package
|
|
94
|
+
run: uv publish
|
|
95
|
+
|
|
96
|
+
deploy-docs:
|
|
97
|
+
needs: publish
|
|
98
|
+
runs-on: ubuntu-latest
|
|
99
|
+
permissions:
|
|
100
|
+
contents: write # Required for gh-pages push
|
|
101
|
+
steps:
|
|
102
|
+
- name: Check out
|
|
103
|
+
uses: actions/checkout@v4
|
|
104
|
+
|
|
105
|
+
- name: Set up the environment
|
|
106
|
+
uses: ./.github/actions/setup-python-env
|
|
107
|
+
|
|
108
|
+
- name: Deploy documentation
|
|
109
|
+
run: uv run mkdocs gh-deploy --force
|
|
@@ -96,7 +96,7 @@ Now, validate that all unit tests are passing:
|
|
|
96
96
|
make test
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
8. Before raising a pull request you should also run tox.
|
|
100
100
|
This will run the tests across different versions of Python:
|
|
101
101
|
|
|
102
102
|
```bash
|
|
@@ -106,15 +106,15 @@ tox
|
|
|
106
106
|
This requires you to have multiple versions of python installed.
|
|
107
107
|
This step is also triggered in the CI/CD pipeline, so you could also choose to skip this step locally.
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
9. Commit your changes and push your branch to GitHub:
|
|
110
110
|
|
|
111
111
|
```bash
|
|
112
|
-
git add
|
|
112
|
+
git add <changed-files>
|
|
113
113
|
git commit -m "Your detailed description of your changes."
|
|
114
114
|
git push origin name-of-your-bugfix-or-feature
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
10. Submit a pull request through the GitHub website.
|
|
118
118
|
|
|
119
119
|
# Pull Request Guidelines
|
|
120
120
|
|
|
@@ -136,31 +136,58 @@ This project uses GitHub Releases to trigger automated publishing to PyPI and do
|
|
|
136
136
|
- `pyproject.toml` → `version = "X.Y.Z"`
|
|
137
137
|
- `.claude-plugin/plugin.json` → `"version": "X.Y.Z"`
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
The release workflow also re-stamps `pyproject.toml` from the release
|
|
140
|
+
tag before building. The manual bump is still required so `main`,
|
|
141
|
+
`uv.lock`, and plugin metadata are consistent before the release starts.
|
|
142
|
+
|
|
143
|
+
2. **Refresh `uv.lock`** so its pinned project version matches:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
uv lock
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Skipping this makes `make check` and release validation fail with
|
|
150
|
+
`The lockfile at 'uv.lock' needs to be updated, but '--locked' was provided.`
|
|
151
|
+
|
|
152
|
+
3. **Run the local checks**:
|
|
140
153
|
|
|
141
154
|
```bash
|
|
142
|
-
|
|
155
|
+
make check
|
|
156
|
+
make test
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
4. **Commit and push** the version bump:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
git add pyproject.toml .claude-plugin/plugin.json uv.lock
|
|
143
163
|
git commit -m "bump version to X.Y.Z"
|
|
144
164
|
git push origin main
|
|
145
165
|
```
|
|
146
166
|
|
|
147
|
-
|
|
167
|
+
5. **Create a GitHub Release**:
|
|
148
168
|
|
|
149
169
|
- Go to [Releases](https://github.com/pablospe/docx-editor/releases/new)
|
|
150
|
-
- Create a new tag matching the version: `X.Y.Z` (e.g., `0.3.0`)
|
|
170
|
+
- **Create a new tag matching the version: `X.Y.Z` (e.g., `0.3.0`). Do not use a `v` prefix.**
|
|
151
171
|
- Set the target branch to `main`
|
|
152
172
|
- Add release notes (use "Generate release notes" for a changelog)
|
|
153
173
|
- Click **Publish release**
|
|
154
174
|
|
|
155
|
-
|
|
175
|
+
6. **Automated CI** (`.github/workflows/on-release-main.yml`) will:
|
|
156
176
|
|
|
157
|
-
-
|
|
177
|
+
- Validate that the release tag, `pyproject.toml`, `uv.lock`, and `.claude-plugin/plugin.json` versions match
|
|
178
|
+
- Update `pyproject.toml` version from the release tag before packaging
|
|
158
179
|
- Build the package with `uv build`
|
|
159
180
|
- Publish to [PyPI](https://pypi.org/project/docx-editor/) via trusted publishing
|
|
160
181
|
- Deploy documentation to GitHub Pages with `mkdocs gh-deploy`
|
|
161
182
|
|
|
183
|
+
7. **Verify the release**:
|
|
184
|
+
|
|
185
|
+
- Check that the release workflow completed successfully in GitHub Actions
|
|
186
|
+
- Confirm the new version appears on [PyPI](https://pypi.org/project/docx-editor/)
|
|
187
|
+
- Confirm the documentation was deployed to GitHub Pages
|
|
188
|
+
|
|
162
189
|
## Notes
|
|
163
190
|
|
|
164
|
-
- The release tag **must** match the version format (e.g., `0.3.0`, no `v` prefix)
|
|
191
|
+
- The release tag **must** match the version format exactly (e.g., `0.3.0`, no `v` prefix)
|
|
165
192
|
- PyPI publishing uses [trusted publishing](https://docs.pypi.org/trusted-publishers/) (no API tokens needed)
|
|
166
193
|
- If you need to build and publish manually, you can use `make build-and-publish`
|
|
@@ -49,7 +49,73 @@ print(doc.source_path) # Path("/path/to/contract.docx")
|
|
|
49
49
|
|
|
50
50
|
### Track Changes Methods
|
|
51
51
|
|
|
52
|
-
#### `
|
|
52
|
+
#### `list_paragraphs(max_chars=80)`
|
|
53
|
+
|
|
54
|
+
List paragraphs with hash-anchored references.
|
|
55
|
+
|
|
56
|
+
**Parameters:**
|
|
57
|
+
|
|
58
|
+
- `max_chars` (int): Maximum preview length. Use 0 for refs only.
|
|
59
|
+
|
|
60
|
+
**Returns:** List of strings in the form `P{index}#{hash}| preview text`
|
|
61
|
+
|
|
62
|
+
**Example:**
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
refs = doc.list_paragraphs()
|
|
66
|
+
for ref in refs:
|
|
67
|
+
print(ref)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### `get_visible_text()`
|
|
71
|
+
|
|
72
|
+
Get flattened visible document text. Inserted text is included and deleted text is excluded.
|
|
73
|
+
|
|
74
|
+
**Returns:** Visible text with paragraphs separated by newlines (str)
|
|
75
|
+
|
|
76
|
+
**Example:**
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
text = doc.get_visible_text()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### `find_text(text, occurrence=0)`
|
|
83
|
+
|
|
84
|
+
Find text in the document, including text spanning XML element boundaries.
|
|
85
|
+
|
|
86
|
+
**Parameters:**
|
|
87
|
+
|
|
88
|
+
- `text` (str): Text to search for
|
|
89
|
+
- `occurrence` (int): Which occurrence to return. Defaults to 0.
|
|
90
|
+
|
|
91
|
+
**Returns:** Match information, or None if not found
|
|
92
|
+
|
|
93
|
+
**Example:**
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
match = doc.find_text("Aim: To")
|
|
97
|
+
if match and match.spans_boundary:
|
|
98
|
+
print("Text spans multiple XML contexts")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### `count_matches(text)`
|
|
102
|
+
|
|
103
|
+
Count visible text matches across the document.
|
|
104
|
+
|
|
105
|
+
**Parameters:**
|
|
106
|
+
|
|
107
|
+
- `text` (str): Text to search for
|
|
108
|
+
|
|
109
|
+
**Returns:** Number of occurrences found (int)
|
|
110
|
+
|
|
111
|
+
**Example:**
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
if doc.count_matches("Section 5") > 1:
|
|
115
|
+
print("Use paragraph refs and occurrence to target the intended match")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### `replace(find, replace_with, *, paragraph, occurrence=0)`
|
|
53
119
|
|
|
54
120
|
Replace text with tracked changes.
|
|
55
121
|
|
|
@@ -57,32 +123,37 @@ Replace text with tracked changes.
|
|
|
57
123
|
|
|
58
124
|
- `find` (str): Text to find and replace
|
|
59
125
|
- `replace_with` (str): Replacement text
|
|
126
|
+
- `paragraph` (str): Paragraph reference from `list_paragraphs()`, such as `P2#f3c1`
|
|
127
|
+
- `occurrence` (int): Which occurrence within the paragraph. Defaults to 0.
|
|
60
128
|
|
|
61
|
-
**Returns:**
|
|
129
|
+
**Returns:** Updated paragraph reference (str)
|
|
62
130
|
|
|
63
131
|
**Example:**
|
|
64
132
|
|
|
65
133
|
```python
|
|
66
|
-
doc.replace("30 days", "60 days")
|
|
134
|
+
ref = doc.replace("30 days", "60 days", paragraph="P2#f3c1")
|
|
135
|
+
doc.replace("net", "gross", paragraph=ref)
|
|
67
136
|
```
|
|
68
137
|
|
|
69
|
-
#### `delete(text)`
|
|
138
|
+
#### `delete(text, *, paragraph, occurrence=0)`
|
|
70
139
|
|
|
71
140
|
Mark text as deleted with tracked changes.
|
|
72
141
|
|
|
73
142
|
**Parameters:**
|
|
74
143
|
|
|
75
144
|
- `text` (str): Text to mark as deleted
|
|
145
|
+
- `paragraph` (str): Paragraph reference from `list_paragraphs()`, such as `P2#f3c1`
|
|
146
|
+
- `occurrence` (int): Which occurrence within the paragraph. Defaults to 0.
|
|
76
147
|
|
|
77
|
-
**Returns:**
|
|
148
|
+
**Returns:** Updated paragraph reference (str)
|
|
78
149
|
|
|
79
150
|
**Example:**
|
|
80
151
|
|
|
81
152
|
```python
|
|
82
|
-
doc.delete("obsolete clause")
|
|
153
|
+
ref = doc.delete("obsolete clause", paragraph="P5#d4e5")
|
|
83
154
|
```
|
|
84
155
|
|
|
85
|
-
#### `insert_after(anchor, text)`
|
|
156
|
+
#### `insert_after(anchor, text, *, paragraph, occurrence=0)`
|
|
86
157
|
|
|
87
158
|
Insert text after anchor with tracked changes.
|
|
88
159
|
|
|
@@ -90,16 +161,18 @@ Insert text after anchor with tracked changes.
|
|
|
90
161
|
|
|
91
162
|
- `anchor` (str): Text to find as insertion point
|
|
92
163
|
- `text` (str): Text to insert after the anchor
|
|
164
|
+
- `paragraph` (str): Paragraph reference from `list_paragraphs()`, such as `P2#f3c1`
|
|
165
|
+
- `occurrence` (int): Which occurrence within the paragraph. Defaults to 0.
|
|
93
166
|
|
|
94
|
-
**Returns:**
|
|
167
|
+
**Returns:** Updated paragraph reference (str)
|
|
95
168
|
|
|
96
169
|
**Example:**
|
|
97
170
|
|
|
98
171
|
```python
|
|
99
|
-
doc.insert_after("Section 5", " (as amended)")
|
|
172
|
+
ref = doc.insert_after("Section 5", " (as amended)", paragraph="P3#b2c4")
|
|
100
173
|
```
|
|
101
174
|
|
|
102
|
-
#### `insert_before(anchor, text)`
|
|
175
|
+
#### `insert_before(anchor, text, *, paragraph, occurrence=0)`
|
|
103
176
|
|
|
104
177
|
Insert text before anchor with tracked changes.
|
|
105
178
|
|
|
@@ -107,13 +180,72 @@ Insert text before anchor with tracked changes.
|
|
|
107
180
|
|
|
108
181
|
- `anchor` (str): Text to find as insertion point
|
|
109
182
|
- `text` (str): Text to insert before the anchor
|
|
183
|
+
- `paragraph` (str): Paragraph reference from `list_paragraphs()`, such as `P2#f3c1`
|
|
184
|
+
- `occurrence` (int): Which occurrence within the paragraph. Defaults to 0.
|
|
185
|
+
|
|
186
|
+
**Returns:** Updated paragraph reference (str)
|
|
187
|
+
|
|
188
|
+
**Example:**
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
ref = doc.insert_before("Section 6", "New clause: ", paragraph="P4#a7b2")
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
#### `rewrite_paragraph(ref, new_text)`
|
|
195
|
+
|
|
196
|
+
Rewrite a paragraph using tracked changes generated from a word-level diff.
|
|
197
|
+
|
|
198
|
+
**Parameters:**
|
|
199
|
+
|
|
200
|
+
- `ref` (str): Paragraph reference from `list_paragraphs()`
|
|
201
|
+
- `new_text` (str): Desired paragraph text
|
|
202
|
+
|
|
203
|
+
**Returns:** Updated paragraph reference (str)
|
|
204
|
+
|
|
205
|
+
**Example:**
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
ref = doc.rewrite_paragraph("P2#f3c1", "Payment is due within 60 days after invoice receipt.")
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### `batch_edit(operations)`
|
|
212
|
+
|
|
213
|
+
Apply multiple edits after validating paragraph hashes up front.
|
|
214
|
+
|
|
215
|
+
**Parameters:**
|
|
216
|
+
|
|
217
|
+
- `operations` (list[EditOperation]): Edit operations to apply
|
|
218
|
+
|
|
219
|
+
**Returns:** Updated paragraph references in input order (list[str])
|
|
220
|
+
|
|
221
|
+
**Example:**
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from docx_editor import EditOperation
|
|
225
|
+
|
|
226
|
+
new_refs = doc.batch_edit([
|
|
227
|
+
EditOperation(action="replace", find="old", replace_with="new", paragraph="P2#f3c1"),
|
|
228
|
+
EditOperation(action="delete", text="remove this", paragraph="P5#d4e5"),
|
|
229
|
+
])
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### `batch_rewrite(rewrites)`
|
|
233
|
+
|
|
234
|
+
Rewrite multiple paragraphs after validating paragraph hashes up front.
|
|
235
|
+
|
|
236
|
+
**Parameters:**
|
|
237
|
+
|
|
238
|
+
- `rewrites` (list[tuple[str, str]]): Pairs of paragraph ref and desired text
|
|
110
239
|
|
|
111
|
-
**Returns:**
|
|
240
|
+
**Returns:** Updated paragraph references in input order (list[str])
|
|
112
241
|
|
|
113
242
|
**Example:**
|
|
114
243
|
|
|
115
244
|
```python
|
|
116
|
-
doc.
|
|
245
|
+
new_refs = doc.batch_rewrite([
|
|
246
|
+
("P1#a7b2", "Updated first paragraph."),
|
|
247
|
+
("P3#c3d4", "Updated third paragraph."),
|
|
248
|
+
])
|
|
117
249
|
```
|
|
118
250
|
|
|
119
251
|
### Comment Methods
|
|
@@ -403,7 +535,7 @@ Raised when the specified text is not found in the document.
|
|
|
403
535
|
from docx_editor.exceptions import TextNotFoundError
|
|
404
536
|
|
|
405
537
|
try:
|
|
406
|
-
doc.replace("nonexistent text", "new text")
|
|
538
|
+
doc.replace("nonexistent text", "new text", paragraph="P2#f3c1")
|
|
407
539
|
except TextNotFoundError as e:
|
|
408
540
|
print(f"Text not found: {e}")
|
|
409
541
|
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# docx-editor
|
|
2
|
+
|
|
3
|
+
Pure Python library for Word document track changes and comments, without requiring Microsoft Word.
|
|
4
|
+
|
|
5
|
+
> **Note:** The PyPI package is named `docx-editor` because `docx-edit` was too similar to an existing package.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Hash-Anchored Paragraph References**: target edits with paragraph refs like `P2#f3c1`
|
|
10
|
+
- **Batch Editing**: apply multiple paragraph-scoped edits with upfront hash validation
|
|
11
|
+
- **Paragraph Rewrite**: rewrite a paragraph and generate tracked changes from the diff
|
|
12
|
+
- **Track Changes**: Replace, delete, and insert text with revision tracking
|
|
13
|
+
- **Comments**: Add, reply, resolve, and delete comments
|
|
14
|
+
- **Revision Management**: List, accept, and reject tracked changes
|
|
15
|
+
- **Cross-Boundary Editing**: Find and replace text spanning multiple XML elements
|
|
16
|
+
- **Cross-Platform**: Works on Linux, macOS, and Windows
|
|
17
|
+
- **No Dependencies**: Only requires `defusedxml` for secure XML parsing
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install docx-editor
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from docx_editor import Document
|
|
29
|
+
|
|
30
|
+
with Document.open("contract.docx", author="Legal Team") as doc:
|
|
31
|
+
# List paragraphs to get hash-anchored references.
|
|
32
|
+
for paragraph in doc.list_paragraphs():
|
|
33
|
+
print(paragraph)
|
|
34
|
+
# Example output:
|
|
35
|
+
# P1#a7b2| Introduction to the contract...
|
|
36
|
+
# P2#f3c1| Payment is due within 30 days...
|
|
37
|
+
|
|
38
|
+
# Edit methods require a paragraph ref and return the updated ref.
|
|
39
|
+
ref = doc.replace("30 days", "60 days", paragraph="P2#f3c1")
|
|
40
|
+
ref = doc.insert_after("Payment", " terms", paragraph=ref)
|
|
41
|
+
doc.delete("obsolete text", paragraph="P5#d4e5")
|
|
42
|
+
|
|
43
|
+
# Comments and revision management.
|
|
44
|
+
doc.add_comment("Section 5", "Please review")
|
|
45
|
+
revisions = doc.list_revisions()
|
|
46
|
+
if revisions:
|
|
47
|
+
doc.accept_revision(revisions[0].id)
|
|
48
|
+
|
|
49
|
+
doc.save()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Context Manager
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from docx_editor import Document
|
|
56
|
+
|
|
57
|
+
with Document.open("contract.docx") as doc:
|
|
58
|
+
ref = doc.list_paragraphs()[0].split("|", 1)[0]
|
|
59
|
+
doc.replace("old term", "new term", paragraph=ref)
|
|
60
|
+
doc.save()
|
|
61
|
+
# Automatically closes and cleans up workspace
|
|
62
|
+
```
|