Commit 7420bb5
committed
feat(text): Field authoring — _Paragraph.add_field, _Field class, CT_TextField.text setter (Phase 3)
Public Python API for the headers/footers/slide-numbers/dates epic (#20).
Phase 3 adds the field-authoring surface that lets users create
auto-updating slide numbers, dates, and other PowerPoint-resolved fields
inside any paragraph. Builds on Phase 1 (PR #48) OOXML primitives and
Phase 2 (PR #49) slide/master public API.
Design source: scanny#797 ("Added a:fld type to paragraphs
for page numbers and datetimes"). Manually ported — per CLAUDE.md §2,
this fork's master had a repo-wide ruff format pass (PR #10) while
upstream did not, so cherry-pick conflicts on whitespace across every
touched file. Semantic diff re-derived against the current ruff-
formatted, post-Phase-1 source.
Changes:
- pptx.oxml.simpletypes.ST_FieldType (NEW) — XsdString subclass for
the `a:fld@type` attribute value, replacing the plain XsdString
declaration Phase 1 used as a placeholder.
- pptx.oxml.text.CT_TextField.text — read-only property from Phase 1
now has a setter. Writes through get_or_add_t() and routes the value
through CT_TextField._escape_ctrl_chars (NEW static method) which
replaces chars in `[\x00-\x08\x0B-\x1F]` with `_xNNNN_` uppercase-hex
form per OOXML §22.9.2.19, leaving `\t` (0x09) and `\n` (0x0A) alone.
- pptx.oxml.text.CT_TextParagraph.fld — ZeroOrMore("a:fld", successors=
("a:endParaRPr",)) accessor; the `a:pPr` successor tuple already named
`a:fld` per Phase 1 (forward declaration). xmlchemy auto-generates
`_add_fld()` from the ZeroOrMore.
- pptx.text.text._Field (NEW) — public-via-add_field-return-value class
wrapping `<a:fld>`. Leading-underscore private name matches `_Run` and
`_Paragraph`. Properties: `font` (Font wrapping rPr), `text` (read/
write, routes through the escaping setter), `type` (read/write,
str | None).
- pptx.text.text._Paragraph.add_field() (NEW) — appends a fresh
`<a:fld>` with a uuid4 GUID id wrapped in braces, uppercase hex —
matches what PowerPoint's "Insert → Slide Number" writes. Returns
a `_Field`; caller sets `type` and optionally `text`. The Run-style
symmetry is deliberate: users who know `add_run()` should not have to
learn a new pattern.
Out of scope for Phase 3 (deliberate):
- Field discovery during paragraph iteration — `p.runs` continues to
yield only `_Run` objects. Phase 4 will surface `_Field` instances
alongside, with a stable ordering rule.
- HandoutMaster Python class and watermark helper — Phase 5.
- MSO_FIELD_TYPE enum — `type` stays plain `str` for now to mirror
scanny#797. An enum can land in a later cleanup once the canonical
field-type list is settled.
Verification (local, CPython 3.14.4):
- python3 -m pytest tests/ -q → 3626 passed in 5.32s (+28 vs Phase 2 baseline)
- 14 new tests in tests/oxml/test_text.py (CT_TextField setter +
_escape_ctrl_chars + CT_TextParagraph.add_fld)
- 14 new tests in tests/text/test_text.py (Describe_Field ×10 +
Describe_Paragraph_add_field ×4)
- python3 -m ruff check src tests → All checks passed!
- python3 -m ruff format --check src tests → 216 files already formatted
- python3 -m behave features/ --no-color → 1048 scenarios, 0 failed
- python3 uat/uat_headers_footers_phase3.py → PASS (full <a:fld> with
id, type, and text round-tripped through save+reopen; GUID preserved
byte-for-byte at {2ED44585-07B5-4BC8-93B2-49122D50BCC2})
Refs #20.1 parent dfe9905 commit 7420bb5
5 files changed
Lines changed: 316 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
368 | 368 | | |
369 | 369 | | |
370 | 370 | | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
371 | 383 | | |
372 | 384 | | |
373 | 385 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
| |||
339 | 340 | | |
340 | 341 | | |
341 | 342 | | |
| 343 | + | |
342 | 344 | | |
343 | 345 | | |
344 | 346 | | |
| |||
348 | 350 | | |
349 | 351 | | |
350 | 352 | | |
351 | | - | |
| 353 | + | |
352 | 354 | | |
353 | 355 | | |
354 | 356 | | |
| |||
359 | 361 | | |
360 | 362 | | |
361 | 363 | | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
362 | 386 | | |
363 | 387 | | |
364 | 388 | | |
| |||
403 | 427 | | |
404 | 428 | | |
405 | 429 | | |
| 430 | + | |
406 | 431 | | |
407 | 432 | | |
408 | 433 | | |
409 | 434 | | |
410 | 435 | | |
411 | 436 | | |
412 | 437 | | |
| 438 | + | |
413 | 439 | | |
414 | 440 | | |
415 | 441 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
| |||
33 | 34 | | |
34 | 35 | | |
35 | 36 | | |
| 37 | + | |
36 | 38 | | |
37 | 39 | | |
38 | 40 | | |
| |||
582 | 584 | | |
583 | 585 | | |
584 | 586 | | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
585 | 602 | | |
586 | 603 | | |
587 | 604 | | |
| |||
888 | 905 | | |
889 | 906 | | |
890 | 907 | | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
| 914 | + | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
| 952 | + | |
| 953 | + | |
| 954 | + | |
| 955 | + | |
| 956 | + | |
| 957 | + | |
| 958 | + | |
| 959 | + | |
| 960 | + | |
| 961 | + | |
| 962 | + | |
| 963 | + | |
| 964 | + | |
| 965 | + | |
| 966 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
11 | 11 | | |
12 | | - | |
| 12 | + | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| |||
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
0 commit comments