docx-editor 0.2.2__tar.gz → 0.2.4__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.
Files changed (104) hide show
  1. {docx_editor-0.2.2 → docx_editor-0.2.4}/.claude-plugin/plugin.json +1 -1
  2. docx_editor-0.2.4/.github/workflows/on-release-main.yml +109 -0
  3. {docx_editor-0.2.2 → docx_editor-0.2.4}/.gitignore +2 -1
  4. {docx_editor-0.2.2 → docx_editor-0.2.4}/CONTRIBUTING.md +38 -11
  5. {docx_editor-0.2.2 → docx_editor-0.2.4}/PKG-INFO +1 -1
  6. {docx_editor-0.2.2 → docx_editor-0.2.4}/docs/api.md +150 -14
  7. docx_editor-0.2.4/docs/index.md +62 -0
  8. docx_editor-0.2.4/docs/quickstart.md +218 -0
  9. {docx_editor-0.2.2 → docx_editor-0.2.4}/docx_editor/__init__.py +6 -1
  10. {docx_editor-0.2.2 → docx_editor-0.2.4}/docx_editor/comments.py +274 -33
  11. {docx_editor-0.2.2 → docx_editor-0.2.4}/docx_editor/document.py +30 -16
  12. {docx_editor-0.2.2 → docx_editor-0.2.4}/docx_editor/ooxml/pack.py +28 -6
  13. {docx_editor-0.2.2 → docx_editor-0.2.4}/docx_editor/track_changes.py +37 -23
  14. {docx_editor-0.2.2 → docx_editor-0.2.4}/docx_editor/workspace.py +3 -0
  15. {docx_editor-0.2.2 → docx_editor-0.2.4}/docx_editor/xml_editor.py +35 -16
  16. {docx_editor-0.2.2 → docx_editor-0.2.4}/pyproject.toml +1 -1
  17. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_comments.py +380 -60
  18. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_document.py +51 -12
  19. docx_editor-0.2.4/tests/test_ooxml.py +187 -0
  20. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_track_changes.py +241 -26
  21. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_xml_editor.py +58 -0
  22. {docx_editor-0.2.2 → docx_editor-0.2.4}/tox.ini +1 -1
  23. {docx_editor-0.2.2 → docx_editor-0.2.4}/uv.lock +1 -1
  24. docx_editor-0.2.2/.github/workflows/on-release-main.yml +0 -68
  25. docx_editor-0.2.2/docs/index.md +0 -55
  26. docx_editor-0.2.2/docs/quickstart.md +0 -245
  27. docx_editor-0.2.2/tests/test_ooxml.py +0 -72
  28. {docx_editor-0.2.2 → docx_editor-0.2.4}/.claude/commands/opsx/apply.md +0 -0
  29. {docx_editor-0.2.2 → docx_editor-0.2.4}/.claude/commands/opsx/archive.md +0 -0
  30. {docx_editor-0.2.2 → docx_editor-0.2.4}/.claude/commands/opsx/explore.md +0 -0
  31. {docx_editor-0.2.2 → docx_editor-0.2.4}/.claude/commands/opsx/propose.md +0 -0
  32. {docx_editor-0.2.2 → docx_editor-0.2.4}/.claude/skills/openspec-apply-change/SKILL.md +0 -0
  33. {docx_editor-0.2.2 → docx_editor-0.2.4}/.claude/skills/openspec-archive-change/SKILL.md +0 -0
  34. {docx_editor-0.2.2 → docx_editor-0.2.4}/.claude/skills/openspec-explore/SKILL.md +0 -0
  35. {docx_editor-0.2.2 → docx_editor-0.2.4}/.claude/skills/openspec-propose/SKILL.md +0 -0
  36. {docx_editor-0.2.2 → docx_editor-0.2.4}/.claude-plugin/marketplace.json +0 -0
  37. {docx_editor-0.2.2 → docx_editor-0.2.4}/.github/actions/setup-python-env/action.yml +0 -0
  38. {docx_editor-0.2.2 → docx_editor-0.2.4}/.github/workflows/main.yml +0 -0
  39. {docx_editor-0.2.2 → docx_editor-0.2.4}/.github/workflows/validate-codecov-config.yml +0 -0
  40. {docx_editor-0.2.2 → docx_editor-0.2.4}/.pre-commit-config.yaml +0 -0
  41. {docx_editor-0.2.2 → docx_editor-0.2.4}/AGENTS.md +0 -0
  42. {docx_editor-0.2.2 → docx_editor-0.2.4}/CLAUDE.md +0 -0
  43. {docx_editor-0.2.2 → docx_editor-0.2.4}/LICENSE +0 -0
  44. {docx_editor-0.2.2 → docx_editor-0.2.4}/Makefile +0 -0
  45. {docx_editor-0.2.2 → docx_editor-0.2.4}/README.md +0 -0
  46. {docx_editor-0.2.2 → docx_editor-0.2.4}/benchmarks/hash_anchored_vs_plain.py +0 -0
  47. {docx_editor-0.2.2 → docx_editor-0.2.4}/codecov.yaml +0 -0
  48. {docx_editor-0.2.2 → docx_editor-0.2.4}/docx_editor/exceptions.py +0 -0
  49. {docx_editor-0.2.2 → docx_editor-0.2.4}/docx_editor/ooxml/__init__.py +0 -0
  50. {docx_editor-0.2.2 → docx_editor-0.2.4}/docx_editor/ooxml/templates/comments.xml +0 -0
  51. {docx_editor-0.2.2 → docx_editor-0.2.4}/docx_editor/ooxml/templates/commentsExtended.xml +0 -0
  52. {docx_editor-0.2.2 → docx_editor-0.2.4}/docx_editor/ooxml/templates/commentsExtensible.xml +0 -0
  53. {docx_editor-0.2.2 → docx_editor-0.2.4}/docx_editor/ooxml/templates/commentsIds.xml +0 -0
  54. {docx_editor-0.2.2 → docx_editor-0.2.4}/docx_editor/ooxml/templates/people.xml +0 -0
  55. {docx_editor-0.2.2 → docx_editor-0.2.4}/docx_editor/ooxml/unpack.py +0 -0
  56. {docx_editor-0.2.2 → docx_editor-0.2.4}/mkdocs.yml +0 -0
  57. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/add-batch-edit/proposal.md +0 -0
  58. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/add-batch-edit/specs/text-operations/spec.md +0 -0
  59. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/add-batch-edit/tasks.md +0 -0
  60. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/add-paragraph-hash-anchors/design.md +0 -0
  61. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/add-paragraph-hash-anchors/proposal.md +0 -0
  62. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/add-paragraph-hash-anchors/specs/text-operations/spec.md +0 -0
  63. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/add-paragraph-hash-anchors/tasks.md +0 -0
  64. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/add-rewrite-paragraph/design.md +0 -0
  65. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/add-rewrite-paragraph/proposal.md +0 -0
  66. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/add-rewrite-paragraph/specs/text-operations/spec.md +0 -0
  67. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/add-rewrite-paragraph/tasks.md +0 -0
  68. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/archive/2026-01-29-add-cross-boundary-text-operations/design.md +0 -0
  69. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/archive/2026-01-29-add-cross-boundary-text-operations/proposal.md +0 -0
  70. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/archive/2026-01-29-add-cross-boundary-text-operations/specs/text-operations/spec.md +0 -0
  71. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/archive/2026-01-29-add-cross-boundary-text-operations/tasks.md +0 -0
  72. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/archive/2026-04-17-add-structured-error-types/.openspec.yaml +0 -0
  73. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/archive/2026-04-17-add-structured-error-types/design.md +0 -0
  74. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/archive/2026-04-17-add-structured-error-types/proposal.md +0 -0
  75. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/archive/2026-04-17-add-structured-error-types/specs/structured-errors/spec.md +0 -0
  76. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/changes/archive/2026-04-17-add-structured-error-types/tasks.md +0 -0
  77. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/config.yaml +0 -0
  78. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/project.md +0 -0
  79. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/specs/structured-errors/spec.md +0 -0
  80. {docx_editor-0.2.2 → docx_editor-0.2.4}/openspec/specs/text-operations/spec.md +0 -0
  81. {docx_editor-0.2.2 → docx_editor-0.2.4}/skills/docx/SKILL.md +0 -0
  82. {docx_editor-0.2.2 → docx_editor-0.2.4}/skills/docx/docx-js.md +0 -0
  83. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/conftest.py +0 -0
  84. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_batch_edit.py +0 -0
  85. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_coverage_gaps.py +0 -0
  86. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_cross_boundary_replace.py +0 -0
  87. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_data/empty.docx +0 -0
  88. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_data/long_content.docx +0 -0
  89. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_data/search_fixture.docx +0 -0
  90. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_data/simple.docx +0 -0
  91. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_data/special_chars.docx +0 -0
  92. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_data/test_document_with_errors.docx +0 -0
  93. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_data/with_tables.docx +0 -0
  94. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_error_quality.py +0 -0
  95. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_mixed_state.py +0 -0
  96. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_multi_wt.py +0 -0
  97. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_multi_wt_safety.py +0 -0
  98. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_nested_ins.py +0 -0
  99. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_paragraph_hash.py +0 -0
  100. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_review_fixes.py +0 -0
  101. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_rewrite_paragraph.py +0 -0
  102. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_text_map.py +0 -0
  103. {docx_editor-0.2.2 → docx_editor-0.2.4}/tests/test_track_changes_coverage.py +0 -0
  104. {docx_editor-0.2.2 → docx_editor-0.2.4}/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.2",
4
+ "version": "0.2.4",
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
@@ -211,4 +211,5 @@ marimo/_static/
211
211
  marimo/_lsp/
212
212
  __marimo__/
213
213
 
214
- .worktree
214
+ .worktree/
215
+ .worktrees/
@@ -96,7 +96,7 @@ Now, validate that all unit tests are passing:
96
96
  make test
97
97
  ```
98
98
 
99
- 9. Before raising a pull request you should also run tox.
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
- 10. Commit your changes and push your branch to GitHub:
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
- 11. Submit a pull request through the GitHub website.
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
- 2. **Commit and push** the version bump:
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
- git add pyproject.toml .claude-plugin/plugin.json
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
- 3. **Create a GitHub Release**:
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
- 4. **Automated CI** (`.github/workflows/on-release-main.yml`) will:
175
+ 6. **Automated CI** (`.github/workflows/on-release-main.yml`) will:
156
176
 
157
- - Update `pyproject.toml` version from the release tag
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`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docx-editor
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Edit docx with python
5
5
  Project-URL: Homepage, https://pablospe.github.io/docx-editor/
6
6
  Project-URL: Repository, https://github.com/pablospe/docx-editor
@@ -18,10 +18,14 @@ Open a Word document for editing.
18
18
 
19
19
  - `path` (str | Path): Path to the .docx file
20
20
  - `author` (str, optional): Author name for tracked changes. Defaults to system username.
21
- - `force_recreate` (bool): If True, delete existing workspace and create fresh. Defaults to False.
21
+ - `force_recreate` (bool): If True, delete any existing workspace (stale or in-sync) before opening — any unsaved edits in the workspace are discarded. Use this to recover from `WorkspaceSyncError`. Defaults to False.
22
22
 
23
23
  **Returns:** Document instance ready for editing
24
24
 
25
+ **Raises:**
26
+
27
+ - `WorkspaceSyncError`: If the source `.docx` was modified since the workspace was created. Pass `force_recreate=True` to discard the stale workspace and re-unpack from the current source. The workspace is never deleted silently.
28
+
25
29
  **Example:**
26
30
 
27
31
  ```python
@@ -49,7 +53,73 @@ print(doc.source_path) # Path("/path/to/contract.docx")
49
53
 
50
54
  ### Track Changes Methods
51
55
 
52
- #### `replace(find, replace_with)`
56
+ #### `list_paragraphs(max_chars=80)`
57
+
58
+ List paragraphs with hash-anchored references.
59
+
60
+ **Parameters:**
61
+
62
+ - `max_chars` (int): Maximum preview length. Use 0 for refs only.
63
+
64
+ **Returns:** List of strings in the form `P{index}#{hash}| preview text`
65
+
66
+ **Example:**
67
+
68
+ ```python
69
+ refs = doc.list_paragraphs()
70
+ for ref in refs:
71
+ print(ref)
72
+ ```
73
+
74
+ #### `get_visible_text()`
75
+
76
+ Get flattened visible document text. Inserted text is included and deleted text is excluded.
77
+
78
+ **Returns:** Visible text with paragraphs separated by newlines (str)
79
+
80
+ **Example:**
81
+
82
+ ```python
83
+ text = doc.get_visible_text()
84
+ ```
85
+
86
+ #### `find_text(text, occurrence=0)`
87
+
88
+ Find text in the document, including text spanning XML element boundaries.
89
+
90
+ **Parameters:**
91
+
92
+ - `text` (str): Text to search for
93
+ - `occurrence` (int): Which occurrence to return. Defaults to 0.
94
+
95
+ **Returns:** Match information, or None if not found
96
+
97
+ **Example:**
98
+
99
+ ```python
100
+ match = doc.find_text("Aim: To")
101
+ if match and match.spans_boundary:
102
+ print("Text spans multiple XML contexts")
103
+ ```
104
+
105
+ #### `count_matches(text)`
106
+
107
+ Count visible text matches across the document.
108
+
109
+ **Parameters:**
110
+
111
+ - `text` (str): Text to search for
112
+
113
+ **Returns:** Number of occurrences found (int)
114
+
115
+ **Example:**
116
+
117
+ ```python
118
+ if doc.count_matches("Section 5") > 1:
119
+ print("Use paragraph refs and occurrence to target the intended match")
120
+ ```
121
+
122
+ #### `replace(find, replace_with, *, paragraph, occurrence=0)`
53
123
 
54
124
  Replace text with tracked changes.
55
125
 
@@ -57,32 +127,37 @@ Replace text with tracked changes.
57
127
 
58
128
  - `find` (str): Text to find and replace
59
129
  - `replace_with` (str): Replacement text
130
+ - `paragraph` (str): Paragraph reference from `list_paragraphs()`, such as `P2#f3c1`
131
+ - `occurrence` (int): Which occurrence within the paragraph. Defaults to 0.
60
132
 
61
- **Returns:** The change ID of the insertion (int)
133
+ **Returns:** Updated paragraph reference (str)
62
134
 
63
135
  **Example:**
64
136
 
65
137
  ```python
66
- doc.replace("30 days", "60 days")
138
+ ref = doc.replace("30 days", "60 days", paragraph="P2#f3c1")
139
+ doc.replace("net", "gross", paragraph=ref)
67
140
  ```
68
141
 
69
- #### `delete(text)`
142
+ #### `delete(text, *, paragraph, occurrence=0)`
70
143
 
71
144
  Mark text as deleted with tracked changes.
72
145
 
73
146
  **Parameters:**
74
147
 
75
148
  - `text` (str): Text to mark as deleted
149
+ - `paragraph` (str): Paragraph reference from `list_paragraphs()`, such as `P2#f3c1`
150
+ - `occurrence` (int): Which occurrence within the paragraph. Defaults to 0.
76
151
 
77
- **Returns:** The change ID of the deletion (int)
152
+ **Returns:** Updated paragraph reference (str)
78
153
 
79
154
  **Example:**
80
155
 
81
156
  ```python
82
- doc.delete("obsolete clause")
157
+ ref = doc.delete("obsolete clause", paragraph="P5#d4e5")
83
158
  ```
84
159
 
85
- #### `insert_after(anchor, text)`
160
+ #### `insert_after(anchor, text, *, paragraph, occurrence=0)`
86
161
 
87
162
  Insert text after anchor with tracked changes.
88
163
 
@@ -90,16 +165,18 @@ Insert text after anchor with tracked changes.
90
165
 
91
166
  - `anchor` (str): Text to find as insertion point
92
167
  - `text` (str): Text to insert after the anchor
168
+ - `paragraph` (str): Paragraph reference from `list_paragraphs()`, such as `P2#f3c1`
169
+ - `occurrence` (int): Which occurrence within the paragraph. Defaults to 0.
93
170
 
94
- **Returns:** The change ID of the insertion (int)
171
+ **Returns:** Updated paragraph reference (str)
95
172
 
96
173
  **Example:**
97
174
 
98
175
  ```python
99
- doc.insert_after("Section 5", " (as amended)")
176
+ ref = doc.insert_after("Section 5", " (as amended)", paragraph="P3#b2c4")
100
177
  ```
101
178
 
102
- #### `insert_before(anchor, text)`
179
+ #### `insert_before(anchor, text, *, paragraph, occurrence=0)`
103
180
 
104
181
  Insert text before anchor with tracked changes.
105
182
 
@@ -107,13 +184,72 @@ Insert text before anchor with tracked changes.
107
184
 
108
185
  - `anchor` (str): Text to find as insertion point
109
186
  - `text` (str): Text to insert before the anchor
187
+ - `paragraph` (str): Paragraph reference from `list_paragraphs()`, such as `P2#f3c1`
188
+ - `occurrence` (int): Which occurrence within the paragraph. Defaults to 0.
189
+
190
+ **Returns:** Updated paragraph reference (str)
191
+
192
+ **Example:**
193
+
194
+ ```python
195
+ ref = doc.insert_before("Section 6", "New clause: ", paragraph="P4#a7b2")
196
+ ```
197
+
198
+ #### `rewrite_paragraph(ref, new_text)`
199
+
200
+ Rewrite a paragraph using tracked changes generated from a word-level diff.
201
+
202
+ **Parameters:**
203
+
204
+ - `ref` (str): Paragraph reference from `list_paragraphs()`
205
+ - `new_text` (str): Desired paragraph text
206
+
207
+ **Returns:** Updated paragraph reference (str)
208
+
209
+ **Example:**
210
+
211
+ ```python
212
+ ref = doc.rewrite_paragraph("P2#f3c1", "Payment is due within 60 days after invoice receipt.")
213
+ ```
214
+
215
+ #### `batch_edit(operations)`
216
+
217
+ Apply multiple edits after validating paragraph hashes up front.
218
+
219
+ **Parameters:**
220
+
221
+ - `operations` (list[EditOperation]): Edit operations to apply
222
+
223
+ **Returns:** Updated paragraph references in input order (list[str])
224
+
225
+ **Example:**
226
+
227
+ ```python
228
+ from docx_editor import EditOperation
229
+
230
+ new_refs = doc.batch_edit([
231
+ EditOperation(action="replace", find="old", replace_with="new", paragraph="P2#f3c1"),
232
+ EditOperation(action="delete", text="remove this", paragraph="P5#d4e5"),
233
+ ])
234
+ ```
235
+
236
+ #### `batch_rewrite(rewrites)`
237
+
238
+ Rewrite multiple paragraphs after validating paragraph hashes up front.
239
+
240
+ **Parameters:**
241
+
242
+ - `rewrites` (list[tuple[str, str]]): Pairs of paragraph ref and desired text
110
243
 
111
- **Returns:** The change ID of the insertion (int)
244
+ **Returns:** Updated paragraph references in input order (list[str])
112
245
 
113
246
  **Example:**
114
247
 
115
248
  ```python
116
- doc.insert_before("Section 6", "New clause: ")
249
+ new_refs = doc.batch_rewrite([
250
+ ("P1#a7b2", "Updated first paragraph."),
251
+ ("P3#c3d4", "Updated third paragraph."),
252
+ ])
117
253
  ```
118
254
 
119
255
  ### Comment Methods
@@ -403,7 +539,7 @@ Raised when the specified text is not found in the document.
403
539
  from docx_editor.exceptions import TextNotFoundError
404
540
 
405
541
  try:
406
- doc.replace("nonexistent text", "new text")
542
+ doc.replace("nonexistent text", "new text", paragraph="P2#f3c1")
407
543
  except TextNotFoundError as e:
408
544
  print(f"Text not found: {e}")
409
545
  ```
@@ -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
+ ```