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/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,,