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.
Files changed (104) hide show
  1. {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude-plugin/plugin.json +1 -1
  2. docx_editor-0.2.3/.github/workflows/on-release-main.yml +109 -0
  3. {docx_editor-0.2.2 → docx_editor-0.2.3}/.gitignore +2 -1
  4. {docx_editor-0.2.2 → docx_editor-0.2.3}/CONTRIBUTING.md +38 -11
  5. {docx_editor-0.2.2 → docx_editor-0.2.3}/PKG-INFO +1 -1
  6. {docx_editor-0.2.2 → docx_editor-0.2.3}/docs/api.md +145 -13
  7. docx_editor-0.2.3/docs/index.md +62 -0
  8. docx_editor-0.2.3/docs/quickstart.md +218 -0
  9. {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/__init__.py +6 -1
  10. {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/comments.py +2 -6
  11. {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/ooxml/pack.py +28 -6
  12. {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/track_changes.py +37 -23
  13. {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/workspace.py +3 -0
  14. {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/xml_editor.py +35 -16
  15. {docx_editor-0.2.2 → docx_editor-0.2.3}/pyproject.toml +1 -1
  16. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_comments.py +32 -0
  17. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_document.py +20 -0
  18. docx_editor-0.2.3/tests/test_ooxml.py +187 -0
  19. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_track_changes.py +241 -26
  20. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_xml_editor.py +58 -0
  21. {docx_editor-0.2.2 → docx_editor-0.2.3}/tox.ini +1 -1
  22. {docx_editor-0.2.2 → docx_editor-0.2.3}/uv.lock +1 -1
  23. docx_editor-0.2.2/.github/workflows/on-release-main.yml +0 -68
  24. docx_editor-0.2.2/docs/index.md +0 -55
  25. docx_editor-0.2.2/docs/quickstart.md +0 -245
  26. docx_editor-0.2.2/tests/test_ooxml.py +0 -72
  27. {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude/commands/opsx/apply.md +0 -0
  28. {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude/commands/opsx/archive.md +0 -0
  29. {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude/commands/opsx/explore.md +0 -0
  30. {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude/commands/opsx/propose.md +0 -0
  31. {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude/skills/openspec-apply-change/SKILL.md +0 -0
  32. {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude/skills/openspec-archive-change/SKILL.md +0 -0
  33. {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude/skills/openspec-explore/SKILL.md +0 -0
  34. {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude/skills/openspec-propose/SKILL.md +0 -0
  35. {docx_editor-0.2.2 → docx_editor-0.2.3}/.claude-plugin/marketplace.json +0 -0
  36. {docx_editor-0.2.2 → docx_editor-0.2.3}/.github/actions/setup-python-env/action.yml +0 -0
  37. {docx_editor-0.2.2 → docx_editor-0.2.3}/.github/workflows/main.yml +0 -0
  38. {docx_editor-0.2.2 → docx_editor-0.2.3}/.github/workflows/validate-codecov-config.yml +0 -0
  39. {docx_editor-0.2.2 → docx_editor-0.2.3}/.pre-commit-config.yaml +0 -0
  40. {docx_editor-0.2.2 → docx_editor-0.2.3}/AGENTS.md +0 -0
  41. {docx_editor-0.2.2 → docx_editor-0.2.3}/CLAUDE.md +0 -0
  42. {docx_editor-0.2.2 → docx_editor-0.2.3}/LICENSE +0 -0
  43. {docx_editor-0.2.2 → docx_editor-0.2.3}/Makefile +0 -0
  44. {docx_editor-0.2.2 → docx_editor-0.2.3}/README.md +0 -0
  45. {docx_editor-0.2.2 → docx_editor-0.2.3}/benchmarks/hash_anchored_vs_plain.py +0 -0
  46. {docx_editor-0.2.2 → docx_editor-0.2.3}/codecov.yaml +0 -0
  47. {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/document.py +0 -0
  48. {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/exceptions.py +0 -0
  49. {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/ooxml/__init__.py +0 -0
  50. {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/ooxml/templates/comments.xml +0 -0
  51. {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/ooxml/templates/commentsExtended.xml +0 -0
  52. {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/ooxml/templates/commentsExtensible.xml +0 -0
  53. {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/ooxml/templates/commentsIds.xml +0 -0
  54. {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/ooxml/templates/people.xml +0 -0
  55. {docx_editor-0.2.2 → docx_editor-0.2.3}/docx_editor/ooxml/unpack.py +0 -0
  56. {docx_editor-0.2.2 → docx_editor-0.2.3}/mkdocs.yml +0 -0
  57. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-batch-edit/proposal.md +0 -0
  58. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-batch-edit/specs/text-operations/spec.md +0 -0
  59. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-batch-edit/tasks.md +0 -0
  60. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-paragraph-hash-anchors/design.md +0 -0
  61. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-paragraph-hash-anchors/proposal.md +0 -0
  62. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-paragraph-hash-anchors/specs/text-operations/spec.md +0 -0
  63. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-paragraph-hash-anchors/tasks.md +0 -0
  64. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-rewrite-paragraph/design.md +0 -0
  65. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-rewrite-paragraph/proposal.md +0 -0
  66. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-rewrite-paragraph/specs/text-operations/spec.md +0 -0
  67. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/add-rewrite-paragraph/tasks.md +0 -0
  68. {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
  69. {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
  70. {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
  71. {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
  72. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/archive/2026-04-17-add-structured-error-types/.openspec.yaml +0 -0
  73. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/archive/2026-04-17-add-structured-error-types/design.md +0 -0
  74. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/archive/2026-04-17-add-structured-error-types/proposal.md +0 -0
  75. {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
  76. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/changes/archive/2026-04-17-add-structured-error-types/tasks.md +0 -0
  77. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/config.yaml +0 -0
  78. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/project.md +0 -0
  79. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/specs/structured-errors/spec.md +0 -0
  80. {docx_editor-0.2.2 → docx_editor-0.2.3}/openspec/specs/text-operations/spec.md +0 -0
  81. {docx_editor-0.2.2 → docx_editor-0.2.3}/skills/docx/SKILL.md +0 -0
  82. {docx_editor-0.2.2 → docx_editor-0.2.3}/skills/docx/docx-js.md +0 -0
  83. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/conftest.py +0 -0
  84. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_batch_edit.py +0 -0
  85. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_coverage_gaps.py +0 -0
  86. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_cross_boundary_replace.py +0 -0
  87. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_data/empty.docx +0 -0
  88. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_data/long_content.docx +0 -0
  89. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_data/search_fixture.docx +0 -0
  90. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_data/simple.docx +0 -0
  91. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_data/special_chars.docx +0 -0
  92. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_data/test_document_with_errors.docx +0 -0
  93. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_data/with_tables.docx +0 -0
  94. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_error_quality.py +0 -0
  95. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_mixed_state.py +0 -0
  96. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_multi_wt.py +0 -0
  97. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_multi_wt_safety.py +0 -0
  98. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_nested_ins.py +0 -0
  99. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_paragraph_hash.py +0 -0
  100. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_review_fixes.py +0 -0
  101. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_rewrite_paragraph.py +0 -0
  102. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_text_map.py +0 -0
  103. {docx_editor-0.2.2 → docx_editor-0.2.3}/tests/test_track_changes_coverage.py +0 -0
  104. {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.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
@@ -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.3
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
@@ -49,7 +49,73 @@ print(doc.source_path) # Path("/path/to/contract.docx")
49
49
 
50
50
  ### Track Changes Methods
51
51
 
52
- #### `replace(find, replace_with)`
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:** The change ID of the insertion (int)
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:** The change ID of the deletion (int)
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:** The change ID of the insertion (int)
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:** The change ID of the insertion (int)
240
+ **Returns:** Updated paragraph references in input order (list[str])
112
241
 
113
242
  **Example:**
114
243
 
115
244
  ```python
116
- doc.insert_before("Section 6", "New clause: ")
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
+ ```