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.
- docxrender-0.1.0/PKG-INFO +273 -0
- docxrender-0.1.0/README.md +262 -0
- docxrender-0.1.0/pyproject.toml +56 -0
- docxrender-0.1.0/src/docxrender/__init__.py +30 -0
- docxrender-0.1.0/src/docxrender/api.py +82 -0
- docxrender-0.1.0/src/docxrender/contracts.py +256 -0
- docxrender-0.1.0/src/docxrender/docx/__init__.py +1 -0
- docxrender-0.1.0/src/docxrender/docx/body.py +369 -0
- docxrender-0.1.0/src/docxrender/docx/fields.py +141 -0
- docxrender-0.1.0/src/docxrender/docx/refresh.py +113 -0
- docxrender-0.1.0/src/docxrender/markdown.py +177 -0
- docxrender-0.1.0/src/docxrender/pdf_uno.py +608 -0
- docxrender-0.1.0/src/docxrender/writer.py +423 -0
- docxrender-0.1.0/tests/__init__.py +0 -0
- docxrender-0.1.0/tests/test_public_contract.py +979 -0
|
@@ -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))
|