docxrender 0.1.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.
@@ -0,0 +1,273 @@
1
+ Metadata-Version: 2.1
2
+ Name: docxrender
3
+ Version: 0.1.0
4
+ Summary: Minimal DOCX rendering core for template, markdown, field refresh, and PDF conversion workflows
5
+ Author-Email: FuqingZhang <fuqin.zhang@proton.me>
6
+ License: MIT
7
+ Requires-Python: >=3.13
8
+ Requires-Dist: docxtpl>=0.20.2
9
+ Requires-Dist: python-docx>=1.2.0
10
+ Description-Content-Type: text/markdown
11
+
12
+ # docxrender
13
+
14
+ `docxrender` is a small Python package for Word-first DOCX rendering.
15
+
16
+ Its core boundary is intentionally narrow:
17
+
18
+ ```text
19
+ file_template + context + markdown_body + DocxStyle -> DOCX -> PDF
20
+ ```
21
+
22
+ The package owns technical rendering mechanics: DOCX template rendering,
23
+ markdown body insertion, Word style application, DOCX field handling, and
24
+ eventual LibreOffice-based PDF conversion. Product repositories own report
25
+ content, workflow resource layout, section rendering, manifest schemas, figure
26
+ selection, captions, and delivery directory semantics.
27
+
28
+ ## Status
29
+
30
+ Current implementation:
31
+
32
+ - Public style/options/result dataclasses are available.
33
+ - `write_docx(...)` can create a minimal DOCX from a DOCX template, context,
34
+ markdown body, image assets, and `DocxStyle`.
35
+ - Markdown support currently covers headings, paragraphs, hard line breaks,
36
+ ordered lists, tables, images, page breaks, and spacers.
37
+ - Basic Word styling is applied from caller-provided `DocxStyle`.
38
+ - DOCX field update/freeze behavior is implemented through DOCX XML rewriting.
39
+ - `write_docx(...)` can optionally refresh TOC/page fields through LibreOffice
40
+ UNO when `DocxFieldRefreshOptions` is provided.
41
+ - `convert_docx_to_pdf(...)` converts through LibreOffice UNO when the external
42
+ LibreOffice/UNO runtime is available.
43
+
44
+ ## Install For Local Development
45
+
46
+ ```bash
47
+ pdm install
48
+ ```
49
+
50
+ Runtime dependencies are declared in `pyproject.toml`:
51
+
52
+ - `docxtpl`
53
+ - `python-docx`
54
+
55
+ PDF conversion and DOCX field refresh are optional runtime features. They do
56
+ not require extra Python packages from `docxrender`, but they do require an
57
+ external LibreOffice/UNO runtime.
58
+
59
+ ```bash
60
+ libreoffice --headless --version
61
+ python -c "import uno"
62
+ ```
63
+
64
+ On Debian or Ubuntu, that runtime is typically installed outside Python:
65
+
66
+ ```bash
67
+ sudo apt install libreoffice python3-uno
68
+ ```
69
+
70
+ `docxrender` intentionally does not provide a `docxrender[pdf]` extra. Installing a
71
+ Python package should not silently install system software or require
72
+ administrator privileges. Base DOCX writing with `field_refresh=None` does not
73
+ import UNO and works without LibreOffice.
74
+
75
+ ## Public API
76
+
77
+ The stable public API is exported from the package root. Product repositories
78
+ should prefer `from docxrender import ...`; implementation modules such as
79
+ `docxrender.markdown` and `docxrender.docx` are technical
80
+ layers and are not compatibility-stable public contracts.
81
+
82
+ ```python
83
+ from docxrender import (
84
+ DocxWriter,
85
+ DocxFieldRefreshOptions,
86
+ DocxFontStyle,
87
+ DocxParagraphStyle,
88
+ DocxSizeStyle,
89
+ DocxStyle,
90
+ DocxTableStyle,
91
+ DocxWriteOptions,
92
+ write_docx,
93
+ )
94
+ ```
95
+
96
+ `DocxFieldRefreshOptions` is optional. Use it only when the caller has provided
97
+ a LibreOffice/UNO runtime and wants a DOCX whose TOC, page fields, or other
98
+ Word fields have been refreshed by LibreOffice:
99
+
100
+ ```python
101
+ DocxWriteOptions(
102
+ ...,
103
+ field_refresh=DocxFieldRefreshOptions(
104
+ exe_libreoffice=Path("/usr/bin/libreoffice"),
105
+ dir_user_profile=Path("tmp/lo-profile"),
106
+ should_require_toc=True,
107
+ should_freeze_fields=True,
108
+ ),
109
+ )
110
+ ```
111
+
112
+ Minimal fluent DOCX write example:
113
+
114
+ ```python
115
+ from pathlib import Path
116
+
117
+ from docxrender import DocxWriter
118
+
119
+ result = (
120
+ DocxWriter()
121
+ .with_fonts(
122
+ font_name_latin="Times New Roman",
123
+ font_name_body_east_asia="宋体",
124
+ font_name_heading_east_asia="宋体",
125
+ )
126
+ .with_sizes(
127
+ pt_title_page_title=36.0,
128
+ pt_title_page_meta=18.0,
129
+ pt_title_page_compiler=15.0,
130
+ pt_body=12.0,
131
+ pt_caption=10.5,
132
+ pt_table=12.0,
133
+ pt_heading_by_level={1: 16.0, 2: 14.0, 3: 12.0},
134
+ )
135
+ .with_table(
136
+ border_color="000000",
137
+ stripe_fill_color="D9D9D9",
138
+ border_size_main="12",
139
+ border_size_header="6",
140
+ line_spacing=1.5,
141
+ )
142
+ .with_paragraph(
143
+ line_spacing_body=1.5,
144
+ line_spacing_note=1.2,
145
+ first_line_indent_cm=0.74,
146
+ )
147
+ .write_docx(
148
+ file_template=Path("template.docx"),
149
+ file_out_docx=Path("report.docx"),
150
+ context={"report_title": "Example Report"},
151
+ markdown_body="# Summary\n\nBody text.",
152
+ dir_base=Path("."),
153
+ )
154
+ )
155
+ print(result.file_docx)
156
+ ```
157
+
158
+ `markdown_body` is the already-rendered Markdown body to insert into the DOCX
159
+ template. `dir_base` is the base directory used to resolve relative image paths
160
+ inside that Markdown body.
161
+
162
+ Explicit dataclass DOCX write example:
163
+
164
+ ```python
165
+ from pathlib import Path
166
+
167
+ from docxrender import (
168
+ DocxFontStyle,
169
+ DocxParagraphStyle,
170
+ DocxSizeStyle,
171
+ DocxStyle,
172
+ DocxTableStyle,
173
+ DocxWriteOptions,
174
+ write_docx,
175
+ )
176
+
177
+ style = DocxStyle(
178
+ fonts=DocxFontStyle(
179
+ font_name_latin="Times New Roman",
180
+ font_name_body_east_asia="宋体",
181
+ font_name_heading_east_asia="宋体",
182
+ ),
183
+ sizes=DocxSizeStyle(
184
+ pt_title_page_title=36.0,
185
+ pt_title_page_meta=18.0,
186
+ pt_title_page_compiler=15.0,
187
+ pt_body=12.0,
188
+ pt_caption=10.5,
189
+ pt_table=12.0,
190
+ pt_heading_by_level={1: 16.0, 2: 14.0, 3: 12.0},
191
+ ),
192
+ table=DocxTableStyle(
193
+ border_color="000000",
194
+ stripe_fill_color="D9D9D9",
195
+ border_size_main="12",
196
+ border_size_header="6",
197
+ line_spacing=1.5,
198
+ ),
199
+ paragraph=DocxParagraphStyle(
200
+ line_spacing_body=1.5,
201
+ line_spacing_note=1.2,
202
+ first_line_indent_cm=0.74,
203
+ ),
204
+ )
205
+
206
+ result = write_docx(
207
+ DocxWriteOptions(
208
+ file_template=Path("template.docx"),
209
+ file_out_docx=Path("report.docx"),
210
+ context={"report_title": "Example Report"},
211
+ markdown_body="# Summary\n\nBody text.",
212
+ dir_base=Path("."),
213
+ style=style,
214
+ )
215
+ )
216
+ print(result.file_docx)
217
+ ```
218
+
219
+ The template should contain a paragraph whose text is the body anchor token:
220
+
221
+ ```text
222
+ {{ body_anchor }}
223
+ ```
224
+
225
+ `docxrender` sets `body_anchor` in the template context when the caller does not
226
+ provide it.
227
+
228
+ ## Style Configuration
229
+
230
+ `docxrender` does not read TOML, JSON, YAML, or any other config file in its public
231
+ API. Callers convert their own configuration into `DocxStyle`.
232
+
233
+ The initial style model is based on:
234
+
235
+ ```text
236
+ /home/fqzhang/project/workflows/resources/common/report/style.toml
237
+ ```
238
+
239
+ That file is a reference for fields and defaults, not a runtime dependency of
240
+ the package.
241
+
242
+ ## Non-Goals
243
+
244
+ `docxrender` does not own:
245
+
246
+ - report manifest schemas
247
+ - workflow resource layout
248
+ - Jinja section discovery
249
+ - product-specific context builders
250
+ - figure registries or captions
251
+ - `Result/...` delivery path semantics
252
+ - `结果目录` text generation
253
+ - style config file readers
254
+
255
+ ## Tests
256
+
257
+ Run the current test suite:
258
+
259
+ ```bash
260
+ pdm run python -m pytest -v
261
+ ```
262
+
263
+ `ty` is available as an advisory type checker beside pyright:
264
+
265
+ ```bash
266
+ pdm run ty check .
267
+ ```
268
+
269
+ Pyright remains the primary type gate.
270
+
271
+ The suite currently covers public API construction, minimal DOCX writing,
272
+ markdown body insertion, basic style application, and the boundary that
273
+ `docxrender` does not import product repositories.
@@ -0,0 +1,262 @@
1
+ # docxrender
2
+
3
+ `docxrender` is a small Python package for Word-first DOCX rendering.
4
+
5
+ Its core boundary is intentionally narrow:
6
+
7
+ ```text
8
+ file_template + context + markdown_body + DocxStyle -> DOCX -> PDF
9
+ ```
10
+
11
+ The package owns technical rendering mechanics: DOCX template rendering,
12
+ markdown body insertion, Word style application, DOCX field handling, and
13
+ eventual LibreOffice-based PDF conversion. Product repositories own report
14
+ content, workflow resource layout, section rendering, manifest schemas, figure
15
+ selection, captions, and delivery directory semantics.
16
+
17
+ ## Status
18
+
19
+ Current implementation:
20
+
21
+ - Public style/options/result dataclasses are available.
22
+ - `write_docx(...)` can create a minimal DOCX from a DOCX template, context,
23
+ markdown body, image assets, and `DocxStyle`.
24
+ - Markdown support currently covers headings, paragraphs, hard line breaks,
25
+ ordered lists, tables, images, page breaks, and spacers.
26
+ - Basic Word styling is applied from caller-provided `DocxStyle`.
27
+ - DOCX field update/freeze behavior is implemented through DOCX XML rewriting.
28
+ - `write_docx(...)` can optionally refresh TOC/page fields through LibreOffice
29
+ UNO when `DocxFieldRefreshOptions` is provided.
30
+ - `convert_docx_to_pdf(...)` converts through LibreOffice UNO when the external
31
+ LibreOffice/UNO runtime is available.
32
+
33
+ ## Install For Local Development
34
+
35
+ ```bash
36
+ pdm install
37
+ ```
38
+
39
+ Runtime dependencies are declared in `pyproject.toml`:
40
+
41
+ - `docxtpl`
42
+ - `python-docx`
43
+
44
+ PDF conversion and DOCX field refresh are optional runtime features. They do
45
+ not require extra Python packages from `docxrender`, but they do require an
46
+ external LibreOffice/UNO runtime.
47
+
48
+ ```bash
49
+ libreoffice --headless --version
50
+ python -c "import uno"
51
+ ```
52
+
53
+ On Debian or Ubuntu, that runtime is typically installed outside Python:
54
+
55
+ ```bash
56
+ sudo apt install libreoffice python3-uno
57
+ ```
58
+
59
+ `docxrender` intentionally does not provide a `docxrender[pdf]` extra. Installing a
60
+ Python package should not silently install system software or require
61
+ administrator privileges. Base DOCX writing with `field_refresh=None` does not
62
+ import UNO and works without LibreOffice.
63
+
64
+ ## Public API
65
+
66
+ The stable public API is exported from the package root. Product repositories
67
+ should prefer `from docxrender import ...`; implementation modules such as
68
+ `docxrender.markdown` and `docxrender.docx` are technical
69
+ layers and are not compatibility-stable public contracts.
70
+
71
+ ```python
72
+ from docxrender import (
73
+ DocxWriter,
74
+ DocxFieldRefreshOptions,
75
+ DocxFontStyle,
76
+ DocxParagraphStyle,
77
+ DocxSizeStyle,
78
+ DocxStyle,
79
+ DocxTableStyle,
80
+ DocxWriteOptions,
81
+ write_docx,
82
+ )
83
+ ```
84
+
85
+ `DocxFieldRefreshOptions` is optional. Use it only when the caller has provided
86
+ a LibreOffice/UNO runtime and wants a DOCX whose TOC, page fields, or other
87
+ Word fields have been refreshed by LibreOffice:
88
+
89
+ ```python
90
+ DocxWriteOptions(
91
+ ...,
92
+ field_refresh=DocxFieldRefreshOptions(
93
+ exe_libreoffice=Path("/usr/bin/libreoffice"),
94
+ dir_user_profile=Path("tmp/lo-profile"),
95
+ should_require_toc=True,
96
+ should_freeze_fields=True,
97
+ ),
98
+ )
99
+ ```
100
+
101
+ Minimal fluent DOCX write example:
102
+
103
+ ```python
104
+ from pathlib import Path
105
+
106
+ from docxrender import DocxWriter
107
+
108
+ result = (
109
+ DocxWriter()
110
+ .with_fonts(
111
+ font_name_latin="Times New Roman",
112
+ font_name_body_east_asia="宋体",
113
+ font_name_heading_east_asia="宋体",
114
+ )
115
+ .with_sizes(
116
+ pt_title_page_title=36.0,
117
+ pt_title_page_meta=18.0,
118
+ pt_title_page_compiler=15.0,
119
+ pt_body=12.0,
120
+ pt_caption=10.5,
121
+ pt_table=12.0,
122
+ pt_heading_by_level={1: 16.0, 2: 14.0, 3: 12.0},
123
+ )
124
+ .with_table(
125
+ border_color="000000",
126
+ stripe_fill_color="D9D9D9",
127
+ border_size_main="12",
128
+ border_size_header="6",
129
+ line_spacing=1.5,
130
+ )
131
+ .with_paragraph(
132
+ line_spacing_body=1.5,
133
+ line_spacing_note=1.2,
134
+ first_line_indent_cm=0.74,
135
+ )
136
+ .write_docx(
137
+ file_template=Path("template.docx"),
138
+ file_out_docx=Path("report.docx"),
139
+ context={"report_title": "Example Report"},
140
+ markdown_body="# Summary\n\nBody text.",
141
+ dir_base=Path("."),
142
+ )
143
+ )
144
+ print(result.file_docx)
145
+ ```
146
+
147
+ `markdown_body` is the already-rendered Markdown body to insert into the DOCX
148
+ template. `dir_base` is the base directory used to resolve relative image paths
149
+ inside that Markdown body.
150
+
151
+ Explicit dataclass DOCX write example:
152
+
153
+ ```python
154
+ from pathlib import Path
155
+
156
+ from docxrender import (
157
+ DocxFontStyle,
158
+ DocxParagraphStyle,
159
+ DocxSizeStyle,
160
+ DocxStyle,
161
+ DocxTableStyle,
162
+ DocxWriteOptions,
163
+ write_docx,
164
+ )
165
+
166
+ style = DocxStyle(
167
+ fonts=DocxFontStyle(
168
+ font_name_latin="Times New Roman",
169
+ font_name_body_east_asia="宋体",
170
+ font_name_heading_east_asia="宋体",
171
+ ),
172
+ sizes=DocxSizeStyle(
173
+ pt_title_page_title=36.0,
174
+ pt_title_page_meta=18.0,
175
+ pt_title_page_compiler=15.0,
176
+ pt_body=12.0,
177
+ pt_caption=10.5,
178
+ pt_table=12.0,
179
+ pt_heading_by_level={1: 16.0, 2: 14.0, 3: 12.0},
180
+ ),
181
+ table=DocxTableStyle(
182
+ border_color="000000",
183
+ stripe_fill_color="D9D9D9",
184
+ border_size_main="12",
185
+ border_size_header="6",
186
+ line_spacing=1.5,
187
+ ),
188
+ paragraph=DocxParagraphStyle(
189
+ line_spacing_body=1.5,
190
+ line_spacing_note=1.2,
191
+ first_line_indent_cm=0.74,
192
+ ),
193
+ )
194
+
195
+ result = write_docx(
196
+ DocxWriteOptions(
197
+ file_template=Path("template.docx"),
198
+ file_out_docx=Path("report.docx"),
199
+ context={"report_title": "Example Report"},
200
+ markdown_body="# Summary\n\nBody text.",
201
+ dir_base=Path("."),
202
+ style=style,
203
+ )
204
+ )
205
+ print(result.file_docx)
206
+ ```
207
+
208
+ The template should contain a paragraph whose text is the body anchor token:
209
+
210
+ ```text
211
+ {{ body_anchor }}
212
+ ```
213
+
214
+ `docxrender` sets `body_anchor` in the template context when the caller does not
215
+ provide it.
216
+
217
+ ## Style Configuration
218
+
219
+ `docxrender` does not read TOML, JSON, YAML, or any other config file in its public
220
+ API. Callers convert their own configuration into `DocxStyle`.
221
+
222
+ The initial style model is based on:
223
+
224
+ ```text
225
+ /home/fqzhang/project/workflows/resources/common/report/style.toml
226
+ ```
227
+
228
+ That file is a reference for fields and defaults, not a runtime dependency of
229
+ the package.
230
+
231
+ ## Non-Goals
232
+
233
+ `docxrender` does not own:
234
+
235
+ - report manifest schemas
236
+ - workflow resource layout
237
+ - Jinja section discovery
238
+ - product-specific context builders
239
+ - figure registries or captions
240
+ - `Result/...` delivery path semantics
241
+ - `结果目录` text generation
242
+ - style config file readers
243
+
244
+ ## Tests
245
+
246
+ Run the current test suite:
247
+
248
+ ```bash
249
+ pdm run python -m pytest -v
250
+ ```
251
+
252
+ `ty` is available as an advisory type checker beside pyright:
253
+
254
+ ```bash
255
+ pdm run ty check .
256
+ ```
257
+
258
+ Pyright remains the primary type gate.
259
+
260
+ The suite currently covers public API construction, minimal DOCX writing,
261
+ markdown body insertion, basic style application, and the boundary that
262
+ `docxrender` does not import product repositories.
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "docxrender"
3
+ version = "0.1.0"
4
+ description = "Minimal DOCX rendering core for template, markdown, field refresh, and PDF conversion workflows"
5
+ authors = [
6
+ { name = "FuqingZhang", email = "fuqin.zhang@proton.me" },
7
+ ]
8
+ dependencies = [
9
+ "docxtpl>=0.20.2",
10
+ "python-docx>=1.2.0",
11
+ ]
12
+ requires-python = ">=3.13"
13
+ readme = "README.md"
14
+
15
+ [project.license]
16
+ text = "MIT"
17
+
18
+ [build-system]
19
+ requires = [
20
+ "pdm-backend",
21
+ ]
22
+ build-backend = "pdm.backend"
23
+
24
+ [tool.pdm]
25
+ distribution = true
26
+
27
+ [tool.ruff]
28
+ line-length = 88
29
+ target-version = "py313"
30
+
31
+ [tool.ruff.lint]
32
+ select = [
33
+ "E",
34
+ "F",
35
+ "I",
36
+ "UP",
37
+ ]
38
+
39
+ [tool.pyright]
40
+ include = [
41
+ "src",
42
+ "tests",
43
+ ]
44
+ pythonVersion = "3.13"
45
+ typeCheckingMode = "strict"
46
+ reportMissingTypeStubs = "none"
47
+
48
+ [dependency-groups]
49
+ dev = [
50
+ "ruff>=0.15.19",
51
+ "pyright>=1.1.411",
52
+ "pytest>=9.1.1",
53
+ "types-unopy>=2.0.0",
54
+ "twine>=6.2.0",
55
+ "ty>=0.0.54",
56
+ ]
@@ -0,0 +1,30 @@
1
+ from docxrender.api import convert_docx_to_pdf, write_docx
2
+ from docxrender.contracts import (
3
+ DocxFieldRefreshOptions,
4
+ DocxFontStyle,
5
+ DocxParagraphStyle,
6
+ DocxSizeStyle,
7
+ DocxStyle,
8
+ DocxTableStyle,
9
+ DocxToPdfOptions,
10
+ DocxToPdfResult,
11
+ DocxWriteOptions,
12
+ DocxWriteResult,
13
+ )
14
+ from docxrender.writer import DocxWriter
15
+
16
+ __all__ = [
17
+ "DocxWriter",
18
+ "DocxFieldRefreshOptions",
19
+ "DocxFontStyle",
20
+ "DocxParagraphStyle",
21
+ "DocxSizeStyle",
22
+ "DocxStyle",
23
+ "DocxTableStyle",
24
+ "DocxToPdfOptions",
25
+ "DocxToPdfResult",
26
+ "DocxWriteOptions",
27
+ "DocxWriteResult",
28
+ "convert_docx_to_pdf",
29
+ "write_docx",
30
+ ]
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, cast
4
+
5
+ from docx import Document
6
+ from docxtpl import DocxTemplate
7
+
8
+ from docxrender.contracts import (
9
+ DocxToPdfOptions,
10
+ DocxToPdfResult,
11
+ DocxWriteOptions,
12
+ DocxWriteResult,
13
+ )
14
+ from docxrender.docx.body import insert_markdown_blocks
15
+ from docxrender.docx.fields import (
16
+ write_docx_field_update_markers,
17
+ write_frozen_docx_fields,
18
+ )
19
+ from docxrender.docx.refresh import refresh_docx_fields
20
+ from docxrender.markdown import parse_markdown_blocks
21
+
22
+
23
+ def write_docx(options: DocxWriteOptions) -> DocxWriteResult:
24
+ """Write a DOCX file from a template, context, markdown body, and style.
25
+
26
+ Args:
27
+ options (DocxWriteOptions): DOCX writing options.
28
+
29
+ Returns:
30
+ DocxWriteResult: Result containing the written DOCX path.
31
+
32
+ Raises:
33
+ FileNotFoundError: The template or a referenced image does not exist.
34
+ RuntimeError: The rendered DOCX cannot be opened or written.
35
+ """
36
+
37
+ _write_template_docx(options)
38
+ markdown_blocks = parse_markdown_blocks(options.markdown_body)
39
+ document = Document(str(options.file_out_docx))
40
+ insert_markdown_blocks(
41
+ document,
42
+ markdown_blocks,
43
+ anchor_token=options.anchor_token,
44
+ dir_base=options.dir_base,
45
+ style=options.style,
46
+ )
47
+ document.save(str(options.file_out_docx))
48
+ if options.should_update_fields:
49
+ write_docx_field_update_markers(options.file_out_docx)
50
+ if options.field_refresh is None and options.should_freeze_fields:
51
+ write_frozen_docx_fields(options.file_out_docx)
52
+ refresh_docx_fields(options.file_out_docx, options=options.field_refresh)
53
+ return DocxWriteResult(file_docx=options.file_out_docx)
54
+
55
+
56
+ def convert_docx_to_pdf(options: DocxToPdfOptions) -> DocxToPdfResult:
57
+ """Convert a DOCX file to PDF through LibreOffice.
58
+
59
+ Args:
60
+ options (DocxToPdfOptions): DOCX-to-PDF conversion options.
61
+
62
+ Returns:
63
+ DocxToPdfResult: Result containing the written PDF path and optional refreshed
64
+ DOCX path.
65
+
66
+ Raises:
67
+ FileNotFoundError: The input DOCX does not exist.
68
+ RuntimeError: LibreOffice or UNO cannot load or convert the document.
69
+ """
70
+
71
+ from docxrender.pdf_uno import run_docx_to_pdf_pipeline
72
+
73
+ return run_docx_to_pdf_pipeline(options)
74
+
75
+
76
+ def _write_template_docx(options: DocxWriteOptions) -> None:
77
+ options.file_out_docx.parent.mkdir(parents=True, exist_ok=True)
78
+ template = cast(Any, DocxTemplate(str(options.file_template)))
79
+ context = dict(options.context)
80
+ context.setdefault("body_anchor", options.anchor_token)
81
+ template.render(context)
82
+ template.save(str(options.file_out_docx))