docxrender 0.1.0__py3-none-any.whl
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/__init__.py +30 -0
- docxrender/api.py +82 -0
- docxrender/contracts.py +256 -0
- docxrender/docx/__init__.py +1 -0
- docxrender/docx/body.py +369 -0
- docxrender/docx/fields.py +141 -0
- docxrender/docx/refresh.py +113 -0
- docxrender/markdown.py +177 -0
- docxrender/pdf_uno.py +608 -0
- docxrender/writer.py +423 -0
- docxrender-0.1.0.dist-info/METADATA +273 -0
- docxrender-0.1.0.dist-info/RECORD +14 -0
- docxrender-0.1.0.dist-info/WHEEL +4 -0
- docxrender-0.1.0.dist-info/entry_points.txt +4 -0
docxrender/writer.py
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Self
|
|
6
|
+
|
|
7
|
+
from docxrender.contracts import (
|
|
8
|
+
DocxFieldRefreshOptions,
|
|
9
|
+
DocxFontStyle,
|
|
10
|
+
DocxParagraphStyle,
|
|
11
|
+
DocxSizeStyle,
|
|
12
|
+
DocxStyle,
|
|
13
|
+
DocxTableStyle,
|
|
14
|
+
DocxWriteOptions,
|
|
15
|
+
DocxWriteResult,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_default_docx_style() -> DocxStyle:
|
|
20
|
+
"""Create the default DOCX style used by the fluent writer.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
DocxStyle: Complete style object modeled after the shared report style
|
|
24
|
+
defaults.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
return DocxStyle(
|
|
28
|
+
fonts=DocxFontStyle(
|
|
29
|
+
font_name_latin="Times New Roman",
|
|
30
|
+
font_name_body_east_asia="宋体",
|
|
31
|
+
font_name_heading_east_asia="宋体",
|
|
32
|
+
),
|
|
33
|
+
sizes=DocxSizeStyle(
|
|
34
|
+
pt_title_page_title=36.0,
|
|
35
|
+
pt_title_page_meta=18.0,
|
|
36
|
+
pt_title_page_compiler=15.0,
|
|
37
|
+
pt_body=12.0,
|
|
38
|
+
pt_caption=10.5,
|
|
39
|
+
pt_table=12.0,
|
|
40
|
+
pt_heading_by_level={
|
|
41
|
+
1: 16.0,
|
|
42
|
+
2: 14.0,
|
|
43
|
+
3: 12.0,
|
|
44
|
+
4: 12.0,
|
|
45
|
+
5: 12.0,
|
|
46
|
+
6: 12.0,
|
|
47
|
+
},
|
|
48
|
+
),
|
|
49
|
+
table=DocxTableStyle(
|
|
50
|
+
border_color="000000",
|
|
51
|
+
stripe_fill_color="D9D9D9",
|
|
52
|
+
border_size_main="12",
|
|
53
|
+
border_size_header="6",
|
|
54
|
+
line_spacing=1.5,
|
|
55
|
+
),
|
|
56
|
+
paragraph=DocxParagraphStyle(
|
|
57
|
+
line_spacing_body=1.5,
|
|
58
|
+
line_spacing_note=1.2,
|
|
59
|
+
first_line_indent_cm=0.74,
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class DocxWriter:
|
|
65
|
+
"""Fluent facade for configuring and writing DOCX files.
|
|
66
|
+
|
|
67
|
+
`DocxWriter` is an ergonomic wrapper around `DocxWriteOptions` and the
|
|
68
|
+
module-level `write_docx` function. It does not own a separate rendering
|
|
69
|
+
pipeline.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, style: DocxStyle | None = None) -> None:
|
|
73
|
+
"""Initialize a fluent DOCX writer.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
style (DocxStyle | None): Optional starting style. When omitted,
|
|
77
|
+
shared report-style defaults are used.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
self._style = style or create_default_docx_style()
|
|
81
|
+
self._field_refresh: DocxFieldRefreshOptions | None = None
|
|
82
|
+
|
|
83
|
+
def with_style(self, style: DocxStyle) -> Self:
|
|
84
|
+
"""Replace the writer style.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
style (DocxStyle): Complete style object to use for future writes.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Self: This writer, for method chaining.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
self._style = style
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def style(self) -> DocxStyle:
|
|
98
|
+
"""Current complete DOCX style.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
DocxStyle: Complete style after applying fluent overrides.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
return self._style
|
|
105
|
+
|
|
106
|
+
def with_fonts(
|
|
107
|
+
self,
|
|
108
|
+
*,
|
|
109
|
+
font_name_latin: str | None = None,
|
|
110
|
+
font_name_body_east_asia: str | None = None,
|
|
111
|
+
font_name_heading_east_asia: str | None = None,
|
|
112
|
+
) -> Self:
|
|
113
|
+
"""Override font settings.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
font_name_latin (str | None): Latin font name applied to runs.
|
|
117
|
+
font_name_body_east_asia (str | None): East Asian body font name.
|
|
118
|
+
font_name_heading_east_asia (str | None): East Asian heading font name.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Self: This writer, for method chaining.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
fonts = self._style.fonts
|
|
125
|
+
self._style = DocxStyle(
|
|
126
|
+
fonts=DocxFontStyle(
|
|
127
|
+
font_name_latin=font_name_latin or fonts.font_name_latin,
|
|
128
|
+
font_name_body_east_asia=(
|
|
129
|
+
font_name_body_east_asia or fonts.font_name_body_east_asia
|
|
130
|
+
),
|
|
131
|
+
font_name_heading_east_asia=(
|
|
132
|
+
font_name_heading_east_asia or fonts.font_name_heading_east_asia
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
sizes=self._style.sizes,
|
|
136
|
+
table=self._style.table,
|
|
137
|
+
paragraph=self._style.paragraph,
|
|
138
|
+
)
|
|
139
|
+
return self
|
|
140
|
+
|
|
141
|
+
def with_sizes(
|
|
142
|
+
self,
|
|
143
|
+
*,
|
|
144
|
+
pt_title_page_title: float | None = None,
|
|
145
|
+
pt_title_page_meta: float | None = None,
|
|
146
|
+
pt_title_page_compiler: float | None = None,
|
|
147
|
+
pt_body: float | None = None,
|
|
148
|
+
pt_caption: float | None = None,
|
|
149
|
+
pt_table: float | None = None,
|
|
150
|
+
pt_heading_by_level: Mapping[int, float] | None = None,
|
|
151
|
+
) -> Self:
|
|
152
|
+
"""Override size settings.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
pt_title_page_title (float | None): Title-page report title size.
|
|
156
|
+
pt_title_page_meta (float | None): Title-page metadata size.
|
|
157
|
+
pt_title_page_compiler (float | None): Compiler or organization size.
|
|
158
|
+
pt_body (float | None): Body paragraph text size.
|
|
159
|
+
pt_caption (float | None): Caption and note text size.
|
|
160
|
+
pt_table (float | None): Markdown table text size.
|
|
161
|
+
pt_heading_by_level (Mapping[int, float] | None): Heading sizes by level.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Self: This writer, for method chaining.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
self._style = DocxStyle(
|
|
168
|
+
fonts=self._style.fonts,
|
|
169
|
+
sizes=self._style.sizes.with_overrides(
|
|
170
|
+
pt_title_page_title=pt_title_page_title,
|
|
171
|
+
pt_title_page_meta=pt_title_page_meta,
|
|
172
|
+
pt_title_page_compiler=pt_title_page_compiler,
|
|
173
|
+
pt_body=pt_body,
|
|
174
|
+
pt_caption=pt_caption,
|
|
175
|
+
pt_table=pt_table,
|
|
176
|
+
pt_heading_by_level=pt_heading_by_level,
|
|
177
|
+
),
|
|
178
|
+
table=self._style.table,
|
|
179
|
+
paragraph=self._style.paragraph,
|
|
180
|
+
)
|
|
181
|
+
return self
|
|
182
|
+
|
|
183
|
+
def with_table(
|
|
184
|
+
self,
|
|
185
|
+
*,
|
|
186
|
+
border_color: str | None = None,
|
|
187
|
+
stripe_fill_color: str | None = None,
|
|
188
|
+
border_size_main: str | None = None,
|
|
189
|
+
border_size_header: str | None = None,
|
|
190
|
+
line_spacing: float | None = None,
|
|
191
|
+
) -> Self:
|
|
192
|
+
"""Override table style settings.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
border_color (str | None): WordprocessingML border color.
|
|
196
|
+
stripe_fill_color (str | None): Body-row stripe fill color.
|
|
197
|
+
border_size_main (str | None): Main border size in Word units.
|
|
198
|
+
border_size_header (str | None): Header border size in Word units.
|
|
199
|
+
line_spacing (float | None): Table paragraph line spacing.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Self: This writer, for method chaining.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
table = self._style.table
|
|
206
|
+
self._style = DocxStyle(
|
|
207
|
+
fonts=self._style.fonts,
|
|
208
|
+
sizes=self._style.sizes,
|
|
209
|
+
table=DocxTableStyle(
|
|
210
|
+
border_color=border_color or table.border_color,
|
|
211
|
+
stripe_fill_color=stripe_fill_color or table.stripe_fill_color,
|
|
212
|
+
border_size_main=border_size_main or table.border_size_main,
|
|
213
|
+
border_size_header=border_size_header or table.border_size_header,
|
|
214
|
+
line_spacing=(
|
|
215
|
+
line_spacing if line_spacing is not None else table.line_spacing
|
|
216
|
+
),
|
|
217
|
+
),
|
|
218
|
+
paragraph=self._style.paragraph,
|
|
219
|
+
)
|
|
220
|
+
return self
|
|
221
|
+
|
|
222
|
+
def with_paragraph(
|
|
223
|
+
self,
|
|
224
|
+
*,
|
|
225
|
+
line_spacing_body: float | None = None,
|
|
226
|
+
line_spacing_note: float | None = None,
|
|
227
|
+
first_line_indent_cm: float | None = None,
|
|
228
|
+
note_prefixes: tuple[str, ...] | None = None,
|
|
229
|
+
) -> Self:
|
|
230
|
+
"""Override paragraph style settings.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
line_spacing_body (float | None): Body paragraph line spacing.
|
|
234
|
+
line_spacing_note (float | None): Note paragraph line spacing.
|
|
235
|
+
first_line_indent_cm (float | None): First-line indent in centimeters.
|
|
236
|
+
note_prefixes (tuple[str, ...] | None): Prefixes classified as notes.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Self: This writer, for method chaining.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
paragraph = self._style.paragraph
|
|
243
|
+
self._style = DocxStyle(
|
|
244
|
+
fonts=self._style.fonts,
|
|
245
|
+
sizes=self._style.sizes,
|
|
246
|
+
table=self._style.table,
|
|
247
|
+
paragraph=DocxParagraphStyle(
|
|
248
|
+
line_spacing_body=(
|
|
249
|
+
line_spacing_body
|
|
250
|
+
if line_spacing_body is not None
|
|
251
|
+
else paragraph.line_spacing_body
|
|
252
|
+
),
|
|
253
|
+
line_spacing_note=(
|
|
254
|
+
line_spacing_note
|
|
255
|
+
if line_spacing_note is not None
|
|
256
|
+
else paragraph.line_spacing_note
|
|
257
|
+
),
|
|
258
|
+
first_line_indent_cm=(
|
|
259
|
+
first_line_indent_cm
|
|
260
|
+
if first_line_indent_cm is not None
|
|
261
|
+
else paragraph.first_line_indent_cm
|
|
262
|
+
),
|
|
263
|
+
note_prefixes=note_prefixes or paragraph.note_prefixes,
|
|
264
|
+
),
|
|
265
|
+
)
|
|
266
|
+
return self
|
|
267
|
+
|
|
268
|
+
def with_field_refresh(
|
|
269
|
+
self,
|
|
270
|
+
options: DocxFieldRefreshOptions | None = None,
|
|
271
|
+
*,
|
|
272
|
+
exe_libreoffice: Path | None = None,
|
|
273
|
+
dir_user_profile: Path | None = None,
|
|
274
|
+
file_out_docx_refreshed: Path | None = None,
|
|
275
|
+
file_listener_log: Path | None = None,
|
|
276
|
+
should_require_toc: bool = False,
|
|
277
|
+
should_freeze_fields: bool = False,
|
|
278
|
+
timeout_seconds: float = 30.0,
|
|
279
|
+
poll_interval_seconds: float = 0.5,
|
|
280
|
+
stable_checks: int = 2,
|
|
281
|
+
) -> Self:
|
|
282
|
+
"""Configure optional LibreOffice UNO field refresh.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
options (DocxFieldRefreshOptions | None): Complete refresh options.
|
|
286
|
+
exe_libreoffice (Path | None): LibreOffice executable path.
|
|
287
|
+
dir_user_profile (Path | None): Isolated LibreOffice profile directory.
|
|
288
|
+
file_out_docx_refreshed (Path | None): Optional refreshed DOCX output.
|
|
289
|
+
file_listener_log (Path | None): Optional listener log path.
|
|
290
|
+
should_require_toc (bool): Whether refreshed DOCX must contain TOC results.
|
|
291
|
+
should_freeze_fields (bool): Whether refreshed fields should be frozen.
|
|
292
|
+
timeout_seconds (float): Maximum wait time for refreshed DOCX validation.
|
|
293
|
+
poll_interval_seconds (float): Poll interval for validation.
|
|
294
|
+
stable_checks (int): Consecutive stable file-stat checks required.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Self: This writer, for method chaining.
|
|
298
|
+
|
|
299
|
+
Raises:
|
|
300
|
+
ValueError: `exe_libreoffice` or `dir_user_profile` is missing when
|
|
301
|
+
`options` is not provided.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
if options is not None:
|
|
305
|
+
self._field_refresh = options
|
|
306
|
+
return self
|
|
307
|
+
if exe_libreoffice is None or dir_user_profile is None:
|
|
308
|
+
raise ValueError(
|
|
309
|
+
"exe_libreoffice and dir_user_profile are required when "
|
|
310
|
+
"DocxFieldRefreshOptions is not provided."
|
|
311
|
+
)
|
|
312
|
+
self._field_refresh = DocxFieldRefreshOptions(
|
|
313
|
+
exe_libreoffice=exe_libreoffice,
|
|
314
|
+
dir_user_profile=dir_user_profile,
|
|
315
|
+
file_out_docx_refreshed=file_out_docx_refreshed,
|
|
316
|
+
file_listener_log=file_listener_log,
|
|
317
|
+
should_require_toc=should_require_toc,
|
|
318
|
+
should_freeze_fields=should_freeze_fields,
|
|
319
|
+
timeout_seconds=timeout_seconds,
|
|
320
|
+
poll_interval_seconds=poll_interval_seconds,
|
|
321
|
+
stable_checks=stable_checks,
|
|
322
|
+
)
|
|
323
|
+
return self
|
|
324
|
+
|
|
325
|
+
def build_style(self) -> DocxStyle:
|
|
326
|
+
"""Build the current complete DOCX style.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
DocxStyle: Complete style object.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
return self.style
|
|
333
|
+
|
|
334
|
+
def build_options(
|
|
335
|
+
self,
|
|
336
|
+
*,
|
|
337
|
+
file_template: Path,
|
|
338
|
+
file_out_docx: Path,
|
|
339
|
+
context: Mapping[str, Any],
|
|
340
|
+
markdown_body: str,
|
|
341
|
+
dir_base: Path,
|
|
342
|
+
anchor_token: str = "__REPORT_BODY_ANCHOR__",
|
|
343
|
+
should_update_fields: bool = True,
|
|
344
|
+
should_freeze_fields: bool = False,
|
|
345
|
+
field_refresh: DocxFieldRefreshOptions | None = None,
|
|
346
|
+
) -> DocxWriteOptions:
|
|
347
|
+
"""Build `DocxWriteOptions` from fluent settings and write inputs.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
file_template (Path): Input DOCX template path.
|
|
351
|
+
file_out_docx (Path): Output DOCX path to write.
|
|
352
|
+
context (Mapping[str, Any]): Template context passed to `docxtpl`.
|
|
353
|
+
markdown_body (str): Markdown body to insert into the DOCX.
|
|
354
|
+
dir_base (Path): Base directory used to resolve relative image paths.
|
|
355
|
+
anchor_token (str): Paragraph text marking markdown insertion point.
|
|
356
|
+
should_update_fields (bool): Whether fields should be marked for update.
|
|
357
|
+
should_freeze_fields (bool): Whether fields should be frozen after writing.
|
|
358
|
+
field_refresh (DocxFieldRefreshOptions | None): Optional per-call field
|
|
359
|
+
refresh override.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
DocxWriteOptions: Complete options for the core writer.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
return DocxWriteOptions(
|
|
366
|
+
file_template=file_template,
|
|
367
|
+
file_out_docx=file_out_docx,
|
|
368
|
+
context=context,
|
|
369
|
+
markdown_body=markdown_body,
|
|
370
|
+
dir_base=dir_base,
|
|
371
|
+
style=self.style,
|
|
372
|
+
anchor_token=anchor_token,
|
|
373
|
+
should_update_fields=should_update_fields,
|
|
374
|
+
should_freeze_fields=should_freeze_fields,
|
|
375
|
+
field_refresh=field_refresh or self._field_refresh,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def write_docx(
|
|
379
|
+
self,
|
|
380
|
+
*,
|
|
381
|
+
file_template: Path,
|
|
382
|
+
file_out_docx: Path,
|
|
383
|
+
context: Mapping[str, Any],
|
|
384
|
+
markdown_body: str,
|
|
385
|
+
dir_base: Path,
|
|
386
|
+
anchor_token: str = "__REPORT_BODY_ANCHOR__",
|
|
387
|
+
should_update_fields: bool = True,
|
|
388
|
+
should_freeze_fields: bool = False,
|
|
389
|
+
field_refresh: DocxFieldRefreshOptions | None = None,
|
|
390
|
+
) -> DocxWriteResult:
|
|
391
|
+
"""Write a DOCX file using the fluent writer settings.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
file_template (Path): Input DOCX template path.
|
|
395
|
+
file_out_docx (Path): Output DOCX path to write.
|
|
396
|
+
context (Mapping[str, Any]): Template context passed to `docxtpl`.
|
|
397
|
+
markdown_body (str): Markdown body to insert into the DOCX.
|
|
398
|
+
dir_base (Path): Base directory used to resolve relative image paths.
|
|
399
|
+
anchor_token (str): Paragraph text marking markdown insertion point.
|
|
400
|
+
should_update_fields (bool): Whether fields should be marked for update.
|
|
401
|
+
should_freeze_fields (bool): Whether fields should be frozen after writing.
|
|
402
|
+
field_refresh (DocxFieldRefreshOptions | None): Optional per-call field
|
|
403
|
+
refresh override.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
DocxWriteResult: Result containing the written DOCX path.
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
from docxrender.api import write_docx
|
|
410
|
+
|
|
411
|
+
return write_docx(
|
|
412
|
+
self.build_options(
|
|
413
|
+
file_template=file_template,
|
|
414
|
+
file_out_docx=file_out_docx,
|
|
415
|
+
context=context,
|
|
416
|
+
markdown_body=markdown_body,
|
|
417
|
+
dir_base=dir_base,
|
|
418
|
+
anchor_token=anchor_token,
|
|
419
|
+
should_update_fields=should_update_fields,
|
|
420
|
+
should_freeze_fields=should_freeze_fields,
|
|
421
|
+
field_refresh=field_refresh,
|
|
422
|
+
)
|
|
423
|
+
)
|
|
@@ -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,14 @@
|
|
|
1
|
+
docxrender-0.1.0.dist-info/METADATA,sha256=qSg_esY9mfCizzLt1bLY3QKChHJEz2-Q6vwN1f9dr_o,7372
|
|
2
|
+
docxrender-0.1.0.dist-info/WHEEL,sha256=VP-D4TPS230sME9Z3vb3INXvo1yt0924YRm5AOsk_dE,90
|
|
3
|
+
docxrender-0.1.0.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
|
4
|
+
docxrender/__init__.py,sha256=88Ui9rkkK-QJM8gfdQzpil18Jz9r-XwgtR2RimNJkso,659
|
|
5
|
+
docxrender/api.py,sha256=az-Sicnr60KOnoOtsV75izWlyha7fJvSwWfBeeOlGKE,2692
|
|
6
|
+
docxrender/contracts.py,sha256=xzuj6fVp4oqnX2XMz0KBqV4UcQZBgO95Udxezjz8sTc,9199
|
|
7
|
+
docxrender/docx/__init__.py,sha256=z-7-r9mkfr6l7UffCrRxhJCTxghnVSyClMlJnbBz4-8,39
|
|
8
|
+
docxrender/docx/body.py,sha256=IRmgwY3tyCs9NSIjd0Mt1429RTkCs6uRO61almlbViM,11805
|
|
9
|
+
docxrender/docx/fields.py,sha256=KYbmbkee9vd3mR0Y4Y0RxfQ6kuQQNWxokLRLmA0LCVk,4433
|
|
10
|
+
docxrender/docx/refresh.py,sha256=PgEqWHpmUlvyJvne3oV-ejXbulF6geEiTj-moa4J_Rg,3669
|
|
11
|
+
docxrender/markdown.py,sha256=mAy-lNzN0-EhcmT_6HQ97kDIf_4YGNzFaobEO_UtYhs,5045
|
|
12
|
+
docxrender/pdf_uno.py,sha256=yMTer-5ri_SDRl2lWqQ3XjbnOFgczJmwzDI1LulAj-8,20147
|
|
13
|
+
docxrender/writer.py,sha256=0ubd1viSWIcfuSQj-sA-4HZNPxW8geQNMnJndKSJiUQ,15152
|
|
14
|
+
docxrender-0.1.0.dist-info/RECORD,,
|