wordlive 0.8.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.
- wordlive-0.8.0/.github/workflows/docs.yml +60 -0
- wordlive-0.8.0/.github/workflows/release.yml +81 -0
- wordlive-0.8.0/.gitignore +16 -0
- wordlive-0.8.0/.python-version +1 -0
- wordlive-0.8.0/PKG-INFO +134 -0
- wordlive-0.8.0/README.md +116 -0
- wordlive-0.8.0/docs/cli.md +932 -0
- wordlive-0.8.0/docs/concepts.md +209 -0
- wordlive-0.8.0/docs/cookbook.md +903 -0
- wordlive-0.8.0/docs/design.md +123 -0
- wordlive-0.8.0/docs/errors.md +163 -0
- wordlive-0.8.0/docs/getting-started.md +155 -0
- wordlive-0.8.0/docs/index.md +68 -0
- wordlive-0.8.0/docs/python-api.md +169 -0
- wordlive-0.8.0/docs/stylesheets/extra.css +10 -0
- wordlive-0.8.0/feature-plan.md +325 -0
- wordlive-0.8.0/issues.md +442 -0
- wordlive-0.8.0/mkdocs.yml +86 -0
- wordlive-0.8.0/pyproject.toml +39 -0
- wordlive-0.8.0/scripts/e2e_test.py +810 -0
- wordlive-0.8.0/spec.md +247 -0
- wordlive-0.8.0/src/wordlive/__init__.py +85 -0
- wordlive-0.8.0/src/wordlive/_anchors.py +938 -0
- wordlive-0.8.0/src/wordlive/_app.py +82 -0
- wordlive-0.8.0/src/wordlive/_com.py +86 -0
- wordlive-0.8.0/src/wordlive/_comments.py +152 -0
- wordlive-0.8.0/src/wordlive/_document.py +488 -0
- wordlive-0.8.0/src/wordlive/_edit.py +69 -0
- wordlive-0.8.0/src/wordlive/_findreplace.py +149 -0
- wordlive-0.8.0/src/wordlive/_images.py +110 -0
- wordlive-0.8.0/src/wordlive/_lists.py +207 -0
- wordlive-0.8.0/src/wordlive/_sections.py +217 -0
- wordlive-0.8.0/src/wordlive/_selection.py +119 -0
- wordlive-0.8.0/src/wordlive/_styles.py +137 -0
- wordlive-0.8.0/src/wordlive/_tables.py +272 -0
- wordlive-0.8.0/src/wordlive/cli/__init__.py +1 -0
- wordlive-0.8.0/src/wordlive/cli/__main__.py +5 -0
- wordlive-0.8.0/src/wordlive/cli/commands.py +1495 -0
- wordlive-0.8.0/src/wordlive/cli/main.py +83 -0
- wordlive-0.8.0/src/wordlive/constants.py +155 -0
- wordlive-0.8.0/src/wordlive/exceptions.py +145 -0
- wordlive-0.8.0/src/wordlive/py.typed +0 -0
- wordlive-0.8.0/tests/__init__.py +0 -0
- wordlive-0.8.0/tests/conftest.py +640 -0
- wordlive-0.8.0/tests/test_anchors.py +462 -0
- wordlive-0.8.0/tests/test_cli.py +1396 -0
- wordlive-0.8.0/tests/test_comments.py +121 -0
- wordlive-0.8.0/tests/test_cursor.py +61 -0
- wordlive-0.8.0/tests/test_edit_scope.py +84 -0
- wordlive-0.8.0/tests/test_findreplace.py +145 -0
- wordlive-0.8.0/tests/test_images.py +253 -0
- wordlive-0.8.0/tests/test_lists.py +153 -0
- wordlive-0.8.0/tests/test_paragraphs.py +116 -0
- wordlive-0.8.0/tests/test_ranges.py +100 -0
- wordlive-0.8.0/tests/test_sections.py +167 -0
- wordlive-0.8.0/tests/test_smoke.py +46 -0
- wordlive-0.8.0/tests/test_styles.py +202 -0
- wordlive-0.8.0/tests/test_tables.py +250 -0
- wordlive-0.8.0/tests/test_track_changes.py +62 -0
- wordlive-0.8.0/uv.lock +640 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- 'docs/**'
|
|
8
|
+
- 'mkdocs.yml'
|
|
9
|
+
- 'src/wordlive/**'
|
|
10
|
+
- 'pyproject.toml'
|
|
11
|
+
- '.github/workflows/docs.yml'
|
|
12
|
+
pull_request:
|
|
13
|
+
paths:
|
|
14
|
+
- 'docs/**'
|
|
15
|
+
- 'mkdocs.yml'
|
|
16
|
+
- 'src/wordlive/**'
|
|
17
|
+
- 'pyproject.toml'
|
|
18
|
+
- '.github/workflows/docs.yml'
|
|
19
|
+
workflow_dispatch:
|
|
20
|
+
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
pages: write
|
|
24
|
+
id-token: write
|
|
25
|
+
|
|
26
|
+
concurrency:
|
|
27
|
+
group: pages
|
|
28
|
+
cancel-in-progress: false
|
|
29
|
+
|
|
30
|
+
jobs:
|
|
31
|
+
build:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v4
|
|
35
|
+
|
|
36
|
+
- uses: actions/setup-python@v5
|
|
37
|
+
with:
|
|
38
|
+
python-version: '3.13'
|
|
39
|
+
cache: pip
|
|
40
|
+
|
|
41
|
+
- name: Install docs deps
|
|
42
|
+
run: pip install -e ".[docs]"
|
|
43
|
+
|
|
44
|
+
- name: Build site
|
|
45
|
+
run: mkdocs build --strict
|
|
46
|
+
|
|
47
|
+
- uses: actions/upload-pages-artifact@v3
|
|
48
|
+
with:
|
|
49
|
+
path: site
|
|
50
|
+
|
|
51
|
+
deploy:
|
|
52
|
+
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
|
53
|
+
needs: build
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
environment:
|
|
56
|
+
name: github-pages
|
|
57
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
58
|
+
steps:
|
|
59
|
+
- id: deployment
|
|
60
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Push a version tag to publish: git tag v0.8.0 && git push origin v0.8.0
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
tags:
|
|
7
|
+
- 'v*'
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: '3.13'
|
|
21
|
+
|
|
22
|
+
- name: Verify tag matches the package version
|
|
23
|
+
run: |
|
|
24
|
+
version=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
25
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
26
|
+
echo "pyproject version: $version"
|
|
27
|
+
echo "release tag: $GITHUB_REF_NAME -> $tag"
|
|
28
|
+
if [ "$version" != "$tag" ]; then
|
|
29
|
+
echo "::error::Tag '$GITHUB_REF_NAME' does not match pyproject version '$version'. Bump the version in pyproject.toml or retag."
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
- name: Build sdist + wheel
|
|
34
|
+
run: |
|
|
35
|
+
python -m pip install --upgrade build twine
|
|
36
|
+
python -m build
|
|
37
|
+
twine check dist/*
|
|
38
|
+
|
|
39
|
+
- uses: actions/upload-artifact@v4
|
|
40
|
+
with:
|
|
41
|
+
name: dist
|
|
42
|
+
path: dist/
|
|
43
|
+
|
|
44
|
+
pypi-publish:
|
|
45
|
+
needs: build
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
# Must match the PyPI trusted-publisher "Environment name".
|
|
48
|
+
environment:
|
|
49
|
+
name: pypi
|
|
50
|
+
url: https://pypi.org/p/wordlive
|
|
51
|
+
permissions:
|
|
52
|
+
id-token: write # OIDC token for trusted publishing — no API token needed
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/download-artifact@v4
|
|
55
|
+
with:
|
|
56
|
+
name: dist
|
|
57
|
+
path: dist/
|
|
58
|
+
|
|
59
|
+
- name: Publish to PyPI
|
|
60
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
61
|
+
|
|
62
|
+
github-release:
|
|
63
|
+
needs: pypi-publish
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
permissions:
|
|
66
|
+
contents: write # to create the Release and upload assets
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/download-artifact@v4
|
|
69
|
+
with:
|
|
70
|
+
name: dist
|
|
71
|
+
path: dist/
|
|
72
|
+
|
|
73
|
+
- name: Create GitHub Release from the tag
|
|
74
|
+
env:
|
|
75
|
+
GH_TOKEN: ${{ github.token }}
|
|
76
|
+
run: >-
|
|
77
|
+
gh release create "$GITHUB_REF_NAME"
|
|
78
|
+
--repo "$GITHUB_REPOSITORY"
|
|
79
|
+
--title "$GITHUB_REF_NAME"
|
|
80
|
+
--generate-notes
|
|
81
|
+
dist/*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
wordlive-0.8.0/PKG-INFO
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wordlive
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: Drive a running Microsoft Word instance from Python — xlwings, but for Word, and LLM-friendly.
|
|
5
|
+
Author-email: "Thomas.Villani" <thomas.villani@gmail.com>
|
|
6
|
+
Requires-Python: >=3.13
|
|
7
|
+
Requires-Dist: click>=8.1
|
|
8
|
+
Requires-Dist: pywin32>=306; sys_platform == 'win32'
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest-mock>=3.12; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
12
|
+
Provides-Extra: docs
|
|
13
|
+
Requires-Dist: mkdocs-include-markdown-plugin>=6.2; extra == 'docs'
|
|
14
|
+
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
|
|
15
|
+
Requires-Dist: mkdocs>=1.6; extra == 'docs'
|
|
16
|
+
Requires-Dist: mkdocstrings[python]>=0.26; extra == 'docs'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# wordlive
|
|
20
|
+
|
|
21
|
+
Drive a running Microsoft Word instance from Python — `xlwings`, but for Word.
|
|
22
|
+
Built for both human scripting and LLM agents. Windows-only.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
pip install wordlive
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
(Requires Python 3.13+ and `pywin32` on Windows.)
|
|
31
|
+
|
|
32
|
+
## Python
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import wordlive as wl
|
|
36
|
+
|
|
37
|
+
with wl.attach() as word:
|
|
38
|
+
doc = word.documents.active
|
|
39
|
+
|
|
40
|
+
# Reads
|
|
41
|
+
outline = doc.outline()
|
|
42
|
+
bookmarks = doc.bookmarks.list()
|
|
43
|
+
|
|
44
|
+
# Polite writes — preserves the user's cursor and view, atomic Ctrl-Z.
|
|
45
|
+
with doc.edit("Update address block"):
|
|
46
|
+
doc.bookmarks["Address"].set_text("123 Main St")
|
|
47
|
+
doc.content_controls["Signatory"].set_text("Jane Doe")
|
|
48
|
+
doc.heading("Introduction").insert_paragraph_after("New context paragraph.")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## CLI
|
|
52
|
+
|
|
53
|
+
JSON in, JSON out — designed to drop straight into an LLM tool-use loop:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
wordlive status
|
|
57
|
+
wordlive outline # heading structure (heading:N)
|
|
58
|
+
wordlive outline --all # every paragraph (para:N) — alias of `paragraphs`
|
|
59
|
+
wordlive paragraphs # same: para:N, level, offsets, text
|
|
60
|
+
wordlive read bookmark Address
|
|
61
|
+
wordlive write bookmark Address --text "123 Main St"
|
|
62
|
+
|
|
63
|
+
# Insert a new paragraph relative to ANY anchor (heading, paragraph, bookmark, …):
|
|
64
|
+
wordlive insert --anchor-id heading:1 --text "..." # after (default)
|
|
65
|
+
wordlive insert --anchor-id para:3 --text "..." --before
|
|
66
|
+
|
|
67
|
+
# Address anchors by ID (the IDs `outline`/`paragraphs` emit — `heading:N`, `para:N`, `bookmark:NAME`, `cc:NAME`):
|
|
68
|
+
wordlive replace --anchor-id heading:3 --text "Updated section text"
|
|
69
|
+
wordlive go-to --anchor-id bookmark:Address
|
|
70
|
+
|
|
71
|
+
# Explicit cursor surface (the non-preferred mode — deliberately moves the cursor):
|
|
72
|
+
wordlive cursor read # where is the cursor? which para:N?
|
|
73
|
+
wordlive cursor write --text "inserted here" # type at the cursor
|
|
74
|
+
|
|
75
|
+
# Styles + paragraph formatting (atomic-undo):
|
|
76
|
+
wordlive style list
|
|
77
|
+
wordlive style apply --anchor-id heading:3 --name "Heading 2"
|
|
78
|
+
wordlive format-paragraph --anchor-id heading:3 --alignment center --space-before 6
|
|
79
|
+
|
|
80
|
+
# Tables (cells are anchors: table:N:R:C):
|
|
81
|
+
wordlive table list
|
|
82
|
+
wordlive table read 1
|
|
83
|
+
wordlive replace --anchor-id table:1:2:2 --text "$450"
|
|
84
|
+
wordlive table add-row --table 1 --values '["Lodging", "$600"]'
|
|
85
|
+
|
|
86
|
+
# Collaboration: comments + track changes (the polite, non-destructive surface):
|
|
87
|
+
wordlive comment add --anchor-id heading:3 --text "Please expand this." --author Bot
|
|
88
|
+
wordlive comment list
|
|
89
|
+
wordlive comment resolve --index 1
|
|
90
|
+
wordlive track on # record edits as revisions; `track off` to stop
|
|
91
|
+
|
|
92
|
+
# Lists & numbering (any anchor's paragraphs):
|
|
93
|
+
wordlive list apply --anchor-id heading:6 --type numbered
|
|
94
|
+
wordlive list restart --anchor-id heading:6
|
|
95
|
+
|
|
96
|
+
# Sections, headers & footers (header:S:WHICH / footer:S:WHICH):
|
|
97
|
+
wordlive section list
|
|
98
|
+
wordlive header write --section 1 --text "ACME Corporation"
|
|
99
|
+
wordlive footer read --section 1
|
|
100
|
+
|
|
101
|
+
# Batch multiple ops in a single Ctrl-Z:
|
|
102
|
+
wordlive exec --script ops.json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Where `ops.json` looks like:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"label": "Update report",
|
|
110
|
+
"ops": [
|
|
111
|
+
{"op": "write_bookmark", "name": "Address", "text": "123 Main St"},
|
|
112
|
+
{"op": "write_cc", "name": "Signatory", "text": "Jane Doe"},
|
|
113
|
+
{"op": "insert_paragraph", "anchor_id": "heading:3", "text": "New risk paragraph."},
|
|
114
|
+
{"op": "replace", "anchor_id": "heading:3", "text": "Updated section text"},
|
|
115
|
+
{"op": "apply_style", "anchor_id": "heading:3", "name": "Heading 2"},
|
|
116
|
+
{"op": "format_paragraph", "anchor_id": "heading:3", "alignment": "center", "space_before": 6}
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Exit codes: `0` ok, `2` anchor-not-found, `3` Word-busy, `4` Word-not-running, `1` other.
|
|
122
|
+
|
|
123
|
+
## Design
|
|
124
|
+
|
|
125
|
+
- **Politeness first** — operations preserve the user's `Selection`, view, and
|
|
126
|
+
scroll. The user keeps editing alongside you.
|
|
127
|
+
- **Semantic anchors over `Selection`** — operations target bookmarks, content
|
|
128
|
+
controls, or headings — never the live cursor unless you ask.
|
|
129
|
+
- **Atomic undo** — every `doc.edit()` opens a Word `UndoRecord`, so a single
|
|
130
|
+
Ctrl-Z reverts the whole block.
|
|
131
|
+
- **Escape hatch** — every wrapper exposes `.com` for the raw COM object;
|
|
132
|
+
you're never blocked by missing coverage.
|
|
133
|
+
|
|
134
|
+
See [`spec.md`](spec.md) for the full design.
|
wordlive-0.8.0/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# wordlive
|
|
2
|
+
|
|
3
|
+
Drive a running Microsoft Word instance from Python — `xlwings`, but for Word.
|
|
4
|
+
Built for both human scripting and LLM agents. Windows-only.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
pip install wordlive
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
(Requires Python 3.13+ and `pywin32` on Windows.)
|
|
13
|
+
|
|
14
|
+
## Python
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import wordlive as wl
|
|
18
|
+
|
|
19
|
+
with wl.attach() as word:
|
|
20
|
+
doc = word.documents.active
|
|
21
|
+
|
|
22
|
+
# Reads
|
|
23
|
+
outline = doc.outline()
|
|
24
|
+
bookmarks = doc.bookmarks.list()
|
|
25
|
+
|
|
26
|
+
# Polite writes — preserves the user's cursor and view, atomic Ctrl-Z.
|
|
27
|
+
with doc.edit("Update address block"):
|
|
28
|
+
doc.bookmarks["Address"].set_text("123 Main St")
|
|
29
|
+
doc.content_controls["Signatory"].set_text("Jane Doe")
|
|
30
|
+
doc.heading("Introduction").insert_paragraph_after("New context paragraph.")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## CLI
|
|
34
|
+
|
|
35
|
+
JSON in, JSON out — designed to drop straight into an LLM tool-use loop:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
wordlive status
|
|
39
|
+
wordlive outline # heading structure (heading:N)
|
|
40
|
+
wordlive outline --all # every paragraph (para:N) — alias of `paragraphs`
|
|
41
|
+
wordlive paragraphs # same: para:N, level, offsets, text
|
|
42
|
+
wordlive read bookmark Address
|
|
43
|
+
wordlive write bookmark Address --text "123 Main St"
|
|
44
|
+
|
|
45
|
+
# Insert a new paragraph relative to ANY anchor (heading, paragraph, bookmark, …):
|
|
46
|
+
wordlive insert --anchor-id heading:1 --text "..." # after (default)
|
|
47
|
+
wordlive insert --anchor-id para:3 --text "..." --before
|
|
48
|
+
|
|
49
|
+
# Address anchors by ID (the IDs `outline`/`paragraphs` emit — `heading:N`, `para:N`, `bookmark:NAME`, `cc:NAME`):
|
|
50
|
+
wordlive replace --anchor-id heading:3 --text "Updated section text"
|
|
51
|
+
wordlive go-to --anchor-id bookmark:Address
|
|
52
|
+
|
|
53
|
+
# Explicit cursor surface (the non-preferred mode — deliberately moves the cursor):
|
|
54
|
+
wordlive cursor read # where is the cursor? which para:N?
|
|
55
|
+
wordlive cursor write --text "inserted here" # type at the cursor
|
|
56
|
+
|
|
57
|
+
# Styles + paragraph formatting (atomic-undo):
|
|
58
|
+
wordlive style list
|
|
59
|
+
wordlive style apply --anchor-id heading:3 --name "Heading 2"
|
|
60
|
+
wordlive format-paragraph --anchor-id heading:3 --alignment center --space-before 6
|
|
61
|
+
|
|
62
|
+
# Tables (cells are anchors: table:N:R:C):
|
|
63
|
+
wordlive table list
|
|
64
|
+
wordlive table read 1
|
|
65
|
+
wordlive replace --anchor-id table:1:2:2 --text "$450"
|
|
66
|
+
wordlive table add-row --table 1 --values '["Lodging", "$600"]'
|
|
67
|
+
|
|
68
|
+
# Collaboration: comments + track changes (the polite, non-destructive surface):
|
|
69
|
+
wordlive comment add --anchor-id heading:3 --text "Please expand this." --author Bot
|
|
70
|
+
wordlive comment list
|
|
71
|
+
wordlive comment resolve --index 1
|
|
72
|
+
wordlive track on # record edits as revisions; `track off` to stop
|
|
73
|
+
|
|
74
|
+
# Lists & numbering (any anchor's paragraphs):
|
|
75
|
+
wordlive list apply --anchor-id heading:6 --type numbered
|
|
76
|
+
wordlive list restart --anchor-id heading:6
|
|
77
|
+
|
|
78
|
+
# Sections, headers & footers (header:S:WHICH / footer:S:WHICH):
|
|
79
|
+
wordlive section list
|
|
80
|
+
wordlive header write --section 1 --text "ACME Corporation"
|
|
81
|
+
wordlive footer read --section 1
|
|
82
|
+
|
|
83
|
+
# Batch multiple ops in a single Ctrl-Z:
|
|
84
|
+
wordlive exec --script ops.json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Where `ops.json` looks like:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"label": "Update report",
|
|
92
|
+
"ops": [
|
|
93
|
+
{"op": "write_bookmark", "name": "Address", "text": "123 Main St"},
|
|
94
|
+
{"op": "write_cc", "name": "Signatory", "text": "Jane Doe"},
|
|
95
|
+
{"op": "insert_paragraph", "anchor_id": "heading:3", "text": "New risk paragraph."},
|
|
96
|
+
{"op": "replace", "anchor_id": "heading:3", "text": "Updated section text"},
|
|
97
|
+
{"op": "apply_style", "anchor_id": "heading:3", "name": "Heading 2"},
|
|
98
|
+
{"op": "format_paragraph", "anchor_id": "heading:3", "alignment": "center", "space_before": 6}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Exit codes: `0` ok, `2` anchor-not-found, `3` Word-busy, `4` Word-not-running, `1` other.
|
|
104
|
+
|
|
105
|
+
## Design
|
|
106
|
+
|
|
107
|
+
- **Politeness first** — operations preserve the user's `Selection`, view, and
|
|
108
|
+
scroll. The user keeps editing alongside you.
|
|
109
|
+
- **Semantic anchors over `Selection`** — operations target bookmarks, content
|
|
110
|
+
controls, or headings — never the live cursor unless you ask.
|
|
111
|
+
- **Atomic undo** — every `doc.edit()` opens a Word `UndoRecord`, so a single
|
|
112
|
+
Ctrl-Z reverts the whole block.
|
|
113
|
+
- **Escape hatch** — every wrapper exposes `.com` for the raw COM object;
|
|
114
|
+
you're never blocked by missing coverage.
|
|
115
|
+
|
|
116
|
+
See [`spec.md`](spec.md) for the full design.
|