docxcast 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.
- docxcast-0.1.0/.gitignore +15 -0
- docxcast-0.1.0/.python-version +1 -0
- docxcast-0.1.0/LICENSE +21 -0
- docxcast-0.1.0/PKG-INFO +118 -0
- docxcast-0.1.0/README.md +91 -0
- docxcast-0.1.0/SYNTAX.md +416 -0
- docxcast-0.1.0/docxcast/__init__.py +41 -0
- docxcast-0.1.0/docxcast/_ooxml.py +191 -0
- docxcast-0.1.0/docxcast/derive.py +203 -0
- docxcast-0.1.0/docxcast/render.py +306 -0
- docxcast-0.1.0/docxcast/schema.py +272 -0
- docxcast-0.1.0/pyproject.toml +45 -0
- docxcast-0.1.0/tests/__init__.py +0 -0
- docxcast-0.1.0/tests/conftest.py +8 -0
- docxcast-0.1.0/tests/fixtures/resume-template.docx +0 -0
- docxcast-0.1.0/tests/helpers.py +150 -0
- docxcast-0.1.0/tests/test_golden_resume.py +172 -0
- docxcast-0.1.0/tests/test_robustness.py +79 -0
- docxcast-0.1.0/tests/test_synthetic.py +481 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
docxcast-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Flow Jiang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
docxcast-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: docxcast
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DocXCast: Turn Word content controls into a typed schema, then cast your data back into documents
|
|
5
|
+
Project-URL: Homepage, https://github.com/flowjzh/docxcast
|
|
6
|
+
Project-URL: Repository, https://github.com/flowjzh/docxcast.git
|
|
7
|
+
Project-URL: Issues, https://github.com/flowjzh/docxcast/issues
|
|
8
|
+
Author-email: Flow Jiang <flowjzh@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: content-controls,docx,llm,schema,structured-data,template,word
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: python-docx>=1.2.0
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# DocXCast
|
|
29
|
+
|
|
30
|
+
<img width="600" alt="DocXCast" src="https://github.com/user-attachments/assets/dc3287be-341a-4d88-a18d-71acc48c0169" />
|
|
31
|
+
|
|
32
|
+
> **"Your template is the mold. Data is poured in, documents take shape."**
|
|
33
|
+
|
|
34
|
+
### 🪄 About
|
|
35
|
+
|
|
36
|
+
**DocXCast** turns a Word document's **content controls** into a typed schema, and casts your data back into a formatted document.
|
|
37
|
+
|
|
38
|
+
**The Idea:**
|
|
39
|
+
|
|
40
|
+
A `.docx` template is a natural schema editor. In Word's Developer tab, anyone can insert content controls and describe them:
|
|
41
|
+
|
|
42
|
+
| Word control property | Role in DocXCast |
|
|
43
|
+
|---|---|
|
|
44
|
+
| **Title** | field name (e.g. `name`) |
|
|
45
|
+
| **Tag** | extraction requirement for the LLM (e.g. `the candidate's full name`) |
|
|
46
|
+
| **Repeating section** | array of records |
|
|
47
|
+
| **Building block gallery** | `Section` with its own extraction rule |
|
|
48
|
+
| **Group control** with a `name?` Title | conditional block (see below) |
|
|
49
|
+
| Locked control (`sdtLocked`) | required field |
|
|
50
|
+
|
|
51
|
+
The full control inventory — every construct's Word-visible pseudo-structure and the OOXML it is made of — lives in **[SYNTAX.md](SYNTAX.md)**.
|
|
52
|
+
|
|
53
|
+
**Two independent capabilities:**
|
|
54
|
+
|
|
55
|
+
1. `derive_schema(template)` — read the controls and produce a `Schema`, serializable to JSON Schema for LLM structured extraction.
|
|
56
|
+
2. `render(template, data)` — pour data back into the template: clone repeating sections, fill values, unwrap controls, keeping every run's formatting.
|
|
57
|
+
|
|
58
|
+
### ⚡ Key Features
|
|
59
|
+
|
|
60
|
+
* **Your template IS the schema** — field names, extraction requirements and repeats all live in the docx, editable by non-programmers.
|
|
61
|
+
* **Typed control mapping** — text → `string`, dropdown → `enum`, date / date-time → `format`, checkbox → `boolean`, picture → derived and removed.
|
|
62
|
+
* **LLM-friendly contract validation** — `schema.validate(data)` returns structured, machine-readable issues (`path` / `code` / `message` / `expected` / `got`) that can be fed back into an LLM correction loop.
|
|
63
|
+
* **Lenient by default, strict on demand** — missing fields keep their template values; `strict=True` raises on the first error.
|
|
64
|
+
* **Clean output** — controls are unwrapped by default; `keep_controls=True` keeps them for round-trip editing.
|
|
65
|
+
* **Zero magic** — a single runtime dependency (`python-docx`).
|
|
66
|
+
|
|
67
|
+
### 🚀 Quick Start
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from docxcast import derive_schema, render
|
|
71
|
+
|
|
72
|
+
schema = derive_schema('resume-template.docx')
|
|
73
|
+
|
|
74
|
+
# hand this to your LLM as the extraction contract
|
|
75
|
+
json_schema = schema.to_json_schema()
|
|
76
|
+
|
|
77
|
+
# validate what the LLM extracted
|
|
78
|
+
report = schema.validate(data)
|
|
79
|
+
if not report.ok:
|
|
80
|
+
# feed report.issues back into the LLM for correction
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
# cast data into the template; controls are unwrapped
|
|
84
|
+
result = render('resume-template.docx', data)
|
|
85
|
+
result.save('resume-output.docx')
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 📏 Validation
|
|
89
|
+
|
|
90
|
+
`Schema.validate()` never raises; it returns a `ValidationResult`:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
result.ok # False if any error-level issue exists
|
|
94
|
+
result.errors # missing_required / type_mismatch / enum_invalid / format_invalid
|
|
95
|
+
result.warnings # unknown_key
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`render(..., strict=True)` raises `RenderError` on the first error-level issue.
|
|
99
|
+
|
|
100
|
+
### 🔀 Conditional Blocks
|
|
101
|
+
|
|
102
|
+
Wrap a block in a **group control** and mark the guarded control's Title with a trailing `?` (TS-style): a falsy value removes the whole group — label text included — while a truthy value fills normally.
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
Nationality: [nationality?] truthy → "Nationality: Chinese" falsy → the line is gone
|
|
106
|
+
|
|
107
|
+
Work Experiences both the heading and the repeat
|
|
108
|
+
[repeat: work_experiences?] disappear when the array is empty
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The `?` is a render directive only — the derived schema (and the LLM contract) see a plain `nationality` field. Marker placement, inline vs block-level drops, positional `has_prev?` / `has_next?` separators and every misuse error live in **[SYNTAX.md](SYNTAX.md)**.
|
|
112
|
+
|
|
113
|
+
### ⚠️ Limitations
|
|
114
|
+
|
|
115
|
+
* `picture` controls are recognized in the schema but removed on render (no image insertion yet).
|
|
116
|
+
* Date values are written verbatim; the template's display mask is not enforced.
|
|
117
|
+
* Unnamed controls are skipped in the schema — every field needs a Title to be addressable.
|
|
118
|
+
* No whitespace control: text outside a dropped inline conditional span survives — put separators inside the span.
|
docxcast-0.1.0/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# DocXCast
|
|
2
|
+
|
|
3
|
+
<img width="600" alt="DocXCast" src="https://github.com/user-attachments/assets/dc3287be-341a-4d88-a18d-71acc48c0169" />
|
|
4
|
+
|
|
5
|
+
> **"Your template is the mold. Data is poured in, documents take shape."**
|
|
6
|
+
|
|
7
|
+
### 🪄 About
|
|
8
|
+
|
|
9
|
+
**DocXCast** turns a Word document's **content controls** into a typed schema, and casts your data back into a formatted document.
|
|
10
|
+
|
|
11
|
+
**The Idea:**
|
|
12
|
+
|
|
13
|
+
A `.docx` template is a natural schema editor. In Word's Developer tab, anyone can insert content controls and describe them:
|
|
14
|
+
|
|
15
|
+
| Word control property | Role in DocXCast |
|
|
16
|
+
|---|---|
|
|
17
|
+
| **Title** | field name (e.g. `name`) |
|
|
18
|
+
| **Tag** | extraction requirement for the LLM (e.g. `the candidate's full name`) |
|
|
19
|
+
| **Repeating section** | array of records |
|
|
20
|
+
| **Building block gallery** | `Section` with its own extraction rule |
|
|
21
|
+
| **Group control** with a `name?` Title | conditional block (see below) |
|
|
22
|
+
| Locked control (`sdtLocked`) | required field |
|
|
23
|
+
|
|
24
|
+
The full control inventory — every construct's Word-visible pseudo-structure and the OOXML it is made of — lives in **[SYNTAX.md](SYNTAX.md)**.
|
|
25
|
+
|
|
26
|
+
**Two independent capabilities:**
|
|
27
|
+
|
|
28
|
+
1. `derive_schema(template)` — read the controls and produce a `Schema`, serializable to JSON Schema for LLM structured extraction.
|
|
29
|
+
2. `render(template, data)` — pour data back into the template: clone repeating sections, fill values, unwrap controls, keeping every run's formatting.
|
|
30
|
+
|
|
31
|
+
### ⚡ Key Features
|
|
32
|
+
|
|
33
|
+
* **Your template IS the schema** — field names, extraction requirements and repeats all live in the docx, editable by non-programmers.
|
|
34
|
+
* **Typed control mapping** — text → `string`, dropdown → `enum`, date / date-time → `format`, checkbox → `boolean`, picture → derived and removed.
|
|
35
|
+
* **LLM-friendly contract validation** — `schema.validate(data)` returns structured, machine-readable issues (`path` / `code` / `message` / `expected` / `got`) that can be fed back into an LLM correction loop.
|
|
36
|
+
* **Lenient by default, strict on demand** — missing fields keep their template values; `strict=True` raises on the first error.
|
|
37
|
+
* **Clean output** — controls are unwrapped by default; `keep_controls=True` keeps them for round-trip editing.
|
|
38
|
+
* **Zero magic** — a single runtime dependency (`python-docx`).
|
|
39
|
+
|
|
40
|
+
### 🚀 Quick Start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from docxcast import derive_schema, render
|
|
44
|
+
|
|
45
|
+
schema = derive_schema('resume-template.docx')
|
|
46
|
+
|
|
47
|
+
# hand this to your LLM as the extraction contract
|
|
48
|
+
json_schema = schema.to_json_schema()
|
|
49
|
+
|
|
50
|
+
# validate what the LLM extracted
|
|
51
|
+
report = schema.validate(data)
|
|
52
|
+
if not report.ok:
|
|
53
|
+
# feed report.issues back into the LLM for correction
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
# cast data into the template; controls are unwrapped
|
|
57
|
+
result = render('resume-template.docx', data)
|
|
58
|
+
result.save('resume-output.docx')
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 📏 Validation
|
|
62
|
+
|
|
63
|
+
`Schema.validate()` never raises; it returns a `ValidationResult`:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
result.ok # False if any error-level issue exists
|
|
67
|
+
result.errors # missing_required / type_mismatch / enum_invalid / format_invalid
|
|
68
|
+
result.warnings # unknown_key
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`render(..., strict=True)` raises `RenderError` on the first error-level issue.
|
|
72
|
+
|
|
73
|
+
### 🔀 Conditional Blocks
|
|
74
|
+
|
|
75
|
+
Wrap a block in a **group control** and mark the guarded control's Title with a trailing `?` (TS-style): a falsy value removes the whole group — label text included — while a truthy value fills normally.
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
Nationality: [nationality?] truthy → "Nationality: Chinese" falsy → the line is gone
|
|
79
|
+
|
|
80
|
+
Work Experiences both the heading and the repeat
|
|
81
|
+
[repeat: work_experiences?] disappear when the array is empty
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The `?` is a render directive only — the derived schema (and the LLM contract) see a plain `nationality` field. Marker placement, inline vs block-level drops, positional `has_prev?` / `has_next?` separators and every misuse error live in **[SYNTAX.md](SYNTAX.md)**.
|
|
85
|
+
|
|
86
|
+
### ⚠️ Limitations
|
|
87
|
+
|
|
88
|
+
* `picture` controls are recognized in the schema but removed on render (no image insertion yet).
|
|
89
|
+
* Date values are written verbatim; the template's display mask is not enforced.
|
|
90
|
+
* Unnamed controls are skipped in the schema — every field needs a Title to be addressable.
|
|
91
|
+
* No whitespace control: text outside a dropped inline conditional span survives — put separators inside the span.
|
docxcast-0.1.0/SYNTAX.md
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# DocXCast Template Syntax
|
|
2
|
+
|
|
3
|
+
Every construct docxcast recognizes, in two views: the pseudo-structure a
|
|
4
|
+
template author sees in Word, and the OOXML it is made of. Word writes more
|
|
5
|
+
into `w:sdtPr` than docxcast reads (`w:rPr`, `w:id`, `w:placeholder`,
|
|
6
|
+
`w:showingPlcHdr`, `w:appearance` — all ignored); what follows lists only the
|
|
7
|
+
nodes that carry meaning.
|
|
8
|
+
|
|
9
|
+
**Pseudo-structure notation**
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
▐ title ▌ one content control; the text inside is its Title
|
|
13
|
+
plain text literal document text (labels, separators, decoration)
|
|
14
|
+
┌─ kind ─┐ a block-level control wrapping the enclosed lines
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Namespaces**: `w` = WordprocessingML, `w14`/`w15` = Microsoft Word 2010/2012
|
|
18
|
+
extensions (checkbox, repeating section).
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Addressing: Title, Tag, Lock
|
|
23
|
+
|
|
24
|
+
Shared by every control kind — set them in Word's *Control Properties*
|
|
25
|
+
dialog:
|
|
26
|
+
|
|
27
|
+
| Word property | XML | Role |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| **Title** | `<w:alias w:val="name"/>` | the field name — the only address |
|
|
30
|
+
| **Tag** | `<w:tag w:val="the candidate's full name"/>` | free-text remark, handed to the LLM as the extraction requirement |
|
|
31
|
+
| **Content control cannot be deleted** | `<w:lock w:val="sdtLocked"/>` | required field (`w:lockType` works too; `sdtContentLocked` alone does not) |
|
|
32
|
+
|
|
33
|
+
A control without a Title is unaddressable (see [Unnamed controls](#unnamed-controls)).
|
|
34
|
+
The Tag never names a field.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Leaf controls
|
|
39
|
+
|
|
40
|
+
### Text control → `string` field
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
In Word (Developer tab → Plain Text / Rich Text):
|
|
44
|
+
|
|
45
|
+
Name: ▐ name ▌
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```xml
|
|
49
|
+
<w:p>
|
|
50
|
+
<w:r><w:t>Name: </w:t></w:r>
|
|
51
|
+
<w:sdt>
|
|
52
|
+
<w:sdtPr>
|
|
53
|
+
<w:alias w:val="name"/>
|
|
54
|
+
<w:tag w:val="the candidate's full name"/>
|
|
55
|
+
</w:sdtPr>
|
|
56
|
+
<w:sdtContent>
|
|
57
|
+
<w:r><w:t>Click or tap here to enter text.</w:t></w:r>
|
|
58
|
+
</w:sdtContent>
|
|
59
|
+
</w:sdt>
|
|
60
|
+
</w:p>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Derives `Field(name, remark)`. Renders the value into the first content run
|
|
64
|
+
(formatting kept, extra runs dropped); no value → the template text stays.
|
|
65
|
+
|
|
66
|
+
### Dropdown → `enum` field
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
Status: ▐ employment_status ▾ ▁ Employed ▁ Unemployed ▁
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```xml
|
|
73
|
+
<w:sdt>
|
|
74
|
+
<w:sdtPr>
|
|
75
|
+
<w:alias w:val="employment_status"/>
|
|
76
|
+
<w:tag w:val="the candidate's current employment status"/>
|
|
77
|
+
<w:dropDownList>
|
|
78
|
+
<w:listItem w:displayText="Employed" w:value="employed"/>
|
|
79
|
+
<w:listItem w:displayText="Unemployed" w:value="unemployed"/>
|
|
80
|
+
</w:dropDownList>
|
|
81
|
+
</w:sdtPr>
|
|
82
|
+
<w:sdtContent>
|
|
83
|
+
<w:r><w:t>Choose an item.</w:t></w:r>
|
|
84
|
+
</w:sdtContent>
|
|
85
|
+
</w:sdt>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Derives `Field(enum=[employed, unemployed], enum_labels=[Employed, Unemployed])`;
|
|
89
|
+
the JSON Schema description carries the `Employed=employed` mapping so the
|
|
90
|
+
LLM answers with the value. Renders the **display text** (`Employed`), not
|
|
91
|
+
the value. List items without `w:displayText` are skipped — that is how Word
|
|
92
|
+
stores the placeholder entry.
|
|
93
|
+
|
|
94
|
+
### Date picker → `format` field
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
First working year: ▐ start_work_date [yyyy] ▌
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```xml
|
|
101
|
+
<w:sdt>
|
|
102
|
+
<w:sdtPr>
|
|
103
|
+
<w:alias w:val="start_work_date"/>
|
|
104
|
+
<w:tag w:val="year the candidate started working"/>
|
|
105
|
+
<w:date>
|
|
106
|
+
<w:dateFormat w:val="yyyy"/>
|
|
107
|
+
<w:storeMappedDataAs w:val="date"/>
|
|
108
|
+
</w:date>
|
|
109
|
+
</w:sdtPr>
|
|
110
|
+
<w:sdtContent/>
|
|
111
|
+
</w:sdt>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The picker's own display mask names the finest unit the value carries
|
|
115
|
+
(quoted literals in the mask are decoration, not tokens):
|
|
116
|
+
|
|
117
|
+
| `w:dateFormat` tokens | format | value shape |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| contains `d` | `date` | `2025-01-31` |
|
|
120
|
+
| contains `M` | `date-month` | `2025-01` |
|
|
121
|
+
| otherwise (e.g. `yyyy`) | `date-year` | `2025` |
|
|
122
|
+
| — (bare picker) | `date` | full ISO date |
|
|
123
|
+
| `w:storeMappedDataAs="dateTime"` | `date-time` | `2026-08-13 10:30:00` |
|
|
124
|
+
|
|
125
|
+
Values are validated against these shapes and written verbatim on render —
|
|
126
|
+
the template's display mask is not re-applied.
|
|
127
|
+
|
|
128
|
+
### Checkbox → `boolean` field
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
Married with children: ▐ is_married_with_children ☐ ▌
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```xml
|
|
135
|
+
<w:sdt>
|
|
136
|
+
<w:sdtPr>
|
|
137
|
+
<w:alias w:val="is_married_with_children"/>
|
|
138
|
+
<w:tag w:val="whether the candidate is married with children"/>
|
|
139
|
+
<w14:checkbox>
|
|
140
|
+
<w14:checked w14:val="0"/>
|
|
141
|
+
<w14:checkedState w14:val="2612"/>
|
|
142
|
+
<w14:uncheckedState w14:val="2610"/>
|
|
143
|
+
</w14:checkbox>
|
|
144
|
+
</w:sdtPr>
|
|
145
|
+
<w:sdtContent>
|
|
146
|
+
<w:r><w:t>☐</w:t></w:r>
|
|
147
|
+
</w:sdtContent>
|
|
148
|
+
</w:sdt>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Derives `Field(type=boolean)`. Renders `w14:checked` 1/0 and the glyph from
|
|
152
|
+
`checkedState`/`uncheckedState` (hex char codes — `2612` ☒ / `2610` ☐ here;
|
|
153
|
+
fallback glyphs when the states are absent).
|
|
154
|
+
|
|
155
|
+
### Picture control
|
|
156
|
+
|
|
157
|
+
```xml
|
|
158
|
+
<w:sdtPr>
|
|
159
|
+
<w:alias w:val="photo"/>
|
|
160
|
+
<w:picture/>
|
|
161
|
+
</w:sdtPr>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Derives a plain `string` field (a path/URL contract for the caller) and is
|
|
165
|
+
**always removed** on render — no image insertion yet.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Repeating section → array
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
┌─ repeating: work_experiences ────────────────────────────┐
|
|
173
|
+
│ ┌─ item ───────────────────────────────────────────────┐ │
|
|
174
|
+
│ │ ▐ start_date ▌ — ▐ end_date ▌ ▐ company ▌ ▐ title ▌ │ │
|
|
175
|
+
│ └──────────────────────────────────────────────────────┘ │
|
|
176
|
+
└──────────────────────────────────────────────────────────┘
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```xml
|
|
180
|
+
<w:sdt>
|
|
181
|
+
<w:sdtPr>
|
|
182
|
+
<w:alias w:val="work_experiences"/>
|
|
183
|
+
<w:tag w:val="jobs held, one item per job"/>
|
|
184
|
+
<w15:repeatingSection/>
|
|
185
|
+
</w:sdtPr>
|
|
186
|
+
<w:sdtContent>
|
|
187
|
+
<w:sdt>
|
|
188
|
+
<w:sdtPr>
|
|
189
|
+
<w15:repeatingSectionItem/>
|
|
190
|
+
</w:sdtPr>
|
|
191
|
+
<w:sdtContent>
|
|
192
|
+
<!-- one item's paragraphs: the controls above -->
|
|
193
|
+
</w:sdtContent>
|
|
194
|
+
</w:sdt>
|
|
195
|
+
</w:sdtContent>
|
|
196
|
+
</w:sdt>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Derives `Repeat(fields=…)` → `{"type": "array", "items": {…}}`. The item
|
|
200
|
+
`w15:repeatingSectionItem` is the clone prototype: each array entry renders a
|
|
201
|
+
deep copy with its own data; any template content in the repeat outside the
|
|
202
|
+
item is discarded. `null`/`[]` → zero items, the whole control disappears.
|
|
203
|
+
A repeat without a Title is unaddressable — skipped and omitted.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Building block gallery → nested section
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
┌─ section: basic_info ────────────────────────────────────┐
|
|
211
|
+
│ ▐ name ▌ ▐ employment_status ▾ ▌ ▐ start_work_date ▌ │
|
|
212
|
+
└──────────────────────────────────────────────────────────┘
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```xml
|
|
216
|
+
<w:sdt>
|
|
217
|
+
<w:sdtPr>
|
|
218
|
+
<w:alias w:val="basic_info"/>
|
|
219
|
+
<w:tag w:val="the candidate's basic information"/>
|
|
220
|
+
<w:docPartList/>
|
|
221
|
+
</w:sdtPr>
|
|
222
|
+
<w:sdtContent>
|
|
223
|
+
<!-- paragraphs and controls, parsed as children -->
|
|
224
|
+
</w:sdtContent>
|
|
225
|
+
</w:sdt>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Derives `Section(children=…)` → a nested object: data arrives as
|
|
229
|
+
`{"basic_info": {"name": …, …}}`. `null` → the whole section is removed.
|
|
230
|
+
Insert via Developer tab → **Building Block Gallery** (Word writes
|
|
231
|
+
`w:docPartList`); the chosen gallery itself is irrelevant.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Group control → union field
|
|
236
|
+
|
|
237
|
+
One field with two renderings: the value, **or** one of the group's literal
|
|
238
|
+
texts. The workhorse example — an end date that may also be "Present":
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
╭──────────────────────╮
|
|
242
|
+
2024-02 — │ ▐ end_date ▌ Present │ ← one inline group
|
|
243
|
+
╰──────────────────────╯
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```xml
|
|
247
|
+
<w:r><w:t>2024-02 — </w:t></w:r>
|
|
248
|
+
<w:sdt>
|
|
249
|
+
<w:sdtPr>
|
|
250
|
+
<w:group/>
|
|
251
|
+
</w:sdtPr>
|
|
252
|
+
<w:sdtContent>
|
|
253
|
+
<w:sdt>
|
|
254
|
+
<w:sdtPr>
|
|
255
|
+
<w:alias w:val="end_date"/>
|
|
256
|
+
<w:tag w:val="when the job ended"/>
|
|
257
|
+
<w:date><w:dateFormat w:val="yyyy-MM"/><w:storeMappedDataAs w:val="date"/></w:date>
|
|
258
|
+
</w:sdtPr>
|
|
259
|
+
<w:sdtContent/>
|
|
260
|
+
</w:sdt>
|
|
261
|
+
<w:r><w:t>Present</w:t></w:r>
|
|
262
|
+
</w:sdtContent>
|
|
263
|
+
</w:sdt>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Rules (enforced by `DeriveError`):
|
|
267
|
+
|
|
268
|
+
- exactly **one** control member; the rest is literal text
|
|
269
|
+
- addressed **once** — the group's own Title, or the member's; never both
|
|
270
|
+
- literal text outside the control becomes the field's `alternatives`
|
|
271
|
+
(`end_date` derives `anyOf[date-month, enum["Present"]]` and either branch
|
|
272
|
+
validates)
|
|
273
|
+
- the member's kind gives type/format; `sdtLocked` on group or member marks
|
|
274
|
+
required
|
|
275
|
+
- a union group is **inline** — its content lives inside one paragraph
|
|
276
|
+
|
|
277
|
+
Render picks one branch: a value that equals an alternative keeps the
|
|
278
|
+
literal and drops the control; any other value fills the control and deletes
|
|
279
|
+
the literal runs. `keep_controls=True` keeps both sides editable instead.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Group control → conditional block
|
|
284
|
+
|
|
285
|
+
The same `w:group` with a **trailing `?`** on the address (TS-style
|
|
286
|
+
`nationality?`) turns it into a display gate: falsy value removes the whole
|
|
287
|
+
group — label and all — truthy fills transparently with the literal text
|
|
288
|
+
kept as decoration.
|
|
289
|
+
|
|
290
|
+
### A labeled line
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
┌─ group (anonymous) ─────────────┐
|
|
294
|
+
│ Nationality: ▐ nationality? ▌ │
|
|
295
|
+
└─────────────────────────────────┘
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
```xml
|
|
299
|
+
<w:sdt>
|
|
300
|
+
<w:sdtPr>
|
|
301
|
+
<w:group/>
|
|
302
|
+
</w:sdtPr>
|
|
303
|
+
<w:sdtContent>
|
|
304
|
+
<w:p>
|
|
305
|
+
<w:r><w:t>Nationality: </w:t></w:r>
|
|
306
|
+
<w:sdt>
|
|
307
|
+
<w:sdtPr>
|
|
308
|
+
<w:alias w:val="nationality?"/>
|
|
309
|
+
<w:tag w:val="the candidate's nationality"/>
|
|
310
|
+
</w:sdtPr>
|
|
311
|
+
<w:sdtContent/>
|
|
312
|
+
</w:sdt>
|
|
313
|
+
</w:p>
|
|
314
|
+
</w:sdtContent>
|
|
315
|
+
</w:sdt>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### A heading plus its repeat
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
┌─ group (anonymous) ──────────────────────────────────────┐
|
|
322
|
+
│ Work Experiences │
|
|
323
|
+
│ ┌─ repeating: work_experiences? ───────────────────────┐ │
|
|
324
|
+
│ │ ┌─ item ───────────────────────────────────────────┐ │ │
|
|
325
|
+
│ │ │ ▐ company ▌ ▐ title ▌ │ │ │
|
|
326
|
+
│ │ └──────────────────────────────────────────────────┘ │ │
|
|
327
|
+
│ └──────────────────────────────────────────────────────┘ │
|
|
328
|
+
└──────────────────────────────────────────────────────────┘
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
The same XML shapes as above — the repeat simply carries
|
|
332
|
+
`<w:alias w:val="work_experiences?"/>` and lives inside the group's
|
|
333
|
+
`w:sdtContent` next to the heading paragraph.
|
|
334
|
+
|
|
335
|
+
Semantics:
|
|
336
|
+
|
|
337
|
+
- **falsy** = `None`, `""`, `False`, `0`, `[]`, `{}` (Python truthiness) —
|
|
338
|
+
the whole group disappears from the output
|
|
339
|
+
- **truthy** — the guarded member fills by its own kind (control, repeat, or
|
|
340
|
+
section); literal text stays as decoration, never becomes `alternatives`
|
|
341
|
+
- the marker may sit on the group's own Title instead, with no control
|
|
342
|
+
inside — that guards **static text** on a field
|
|
343
|
+
(`┌─ group: nationality? ─ "Foreign candidate" ┐`)
|
|
344
|
+
- **inline** group → only the wrapped span drops, the paragraph survives;
|
|
345
|
+
**block-level** group → whole paragraphs drop. Wrap the paragraph mark to
|
|
346
|
+
make a line vanish completely.
|
|
347
|
+
- the `?` is a render directive only — the derived schema (and the LLM)
|
|
348
|
+
see the plain `nationality` / `work_experiences` field
|
|
349
|
+
|
|
350
|
+
### Positional guards — separators between repeat items
|
|
351
|
+
|
|
352
|
+
Inside a repeating item, a group addressed `has_next?` (or `has_prev?`)
|
|
353
|
+
shows only **between** items — separators and divider lines that appear once
|
|
354
|
+
per gap, never before the first or after the last item:
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
┌─ repeating: skills ──────────────────────────────────────┐
|
|
358
|
+
│ ┌─ item ───────────────────────────────────────────────┐ │
|
|
359
|
+
│ │ ▐ skill ▌ │ │
|
|
360
|
+
│ │ ┌─ group: has_next? ───────────────────────────────┐ │ │
|
|
361
|
+
│ │ │ , │ │ │
|
|
362
|
+
│ │ └──────────────────────────────────────────────────┘ │ │
|
|
363
|
+
│ └──────────────────────────────────────────────────────┘ │
|
|
364
|
+
└──────────────────────────────────────────────────────────┘
|
|
365
|
+
|
|
366
|
+
1 item → Python
|
|
367
|
+
3 items → Python, Go, Rust (separator only between)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
```xml
|
|
371
|
+
<!-- inside the repeatingSectionItem's sdtContent -->
|
|
372
|
+
<w:sdt>
|
|
373
|
+
<w:sdtPr>
|
|
374
|
+
<w:group/>
|
|
375
|
+
<w:alias w:val="has_next?"/>
|
|
376
|
+
</w:sdtPr>
|
|
377
|
+
<w:sdtContent>
|
|
378
|
+
<w:p><w:r><w:t>,</w:t></w:r></w:p>
|
|
379
|
+
</w:sdtContent>
|
|
380
|
+
</w:sdt>
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
`has_prev`/`has_next` are reserved names — a data field may not take them.
|
|
384
|
+
The guards exist only in repeat-item scope; they are not schema nodes.
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## Unnamed controls
|
|
389
|
+
|
|
390
|
+
| Shape | Schema | Render |
|
|
391
|
+
|---|---|---|
|
|
392
|
+
| unnamed text control | skipped | unwrapped, template text kept |
|
|
393
|
+
| unnamed section / repeat | skipped | omitted |
|
|
394
|
+
| unnamed group with no addressed member | skipped | unwrapped in place |
|
|
395
|
+
|
|
396
|
+
## What fails `derive_schema`
|
|
397
|
+
|
|
398
|
+
| Template shape | Error |
|
|
399
|
+
|---|---|
|
|
400
|
+
| `name?` on a control/repeat/section not inside a group | a trailing `?` marks a conditional block — wrap it |
|
|
401
|
+
| block-level group whose address has no `?` | block-level groups are conditional — marker missing |
|
|
402
|
+
| any field/group named `has_prev` / `has_next` | reserved for positional guards |
|
|
403
|
+
| `has_prev?` / `has_next?` outside a repeating item (incl. under a section — section fill uses its own dict) | positional guards only exist inside repeat items |
|
|
404
|
+
| two addressed members in one group | a group is one field, address it once |
|
|
405
|
+
| two control members in one group | one control, the rest literal text |
|
|
406
|
+
|
|
407
|
+
## Render invariants
|
|
408
|
+
|
|
409
|
+
- Controls are **unwrapped** by default (`keep_controls=True` keeps the
|
|
410
|
+
structure for round-trip editing; conditional drops keep the group too).
|
|
411
|
+
- Any drop path tops an emptied table cell back up with one empty `w:p` —
|
|
412
|
+
a `w:tc` must retain a paragraph.
|
|
413
|
+
- Missing leaf values keep their template text (lenient); `strict=True`
|
|
414
|
+
raises `RenderError` on the first error-level issue instead.
|
|
415
|
+
- No whitespace control: text outside a dropped inline span (an orphaned
|
|
416
|
+
separator) survives — put the separator inside the span.
|