|
1 | 1 | # SPDX-License-Identifier: MIT |
2 | | -"""Format-neutral brand APPEARANCE apply orchestration (font / size / color). |
| 2 | +"""Format-neutral brand APPEARANCE apply orchestration (font / size / color / |
| 3 | +geometry / table / numbering). |
3 | 4 |
|
4 | 5 | This is the shared control flow the per-format generators (docx and pptx today; |
5 | | -xlsx in a later PR) delegate to so the "read the three axes off the resolver op, |
6 | | -then brand each run only when its axis is unset" logic has exactly ONE writer |
7 | | -across kinds. |
| 6 | +xlsx in a later PR) delegate to so the "read the appearance axes off the resolver |
| 7 | +op, then brand each run/paragraph only when its axis is unset" logic has exactly |
| 8 | +ONE writer across kinds. |
8 | 9 |
|
9 | 10 | It is lxml / python-docx / pptx / openpyxl-FREE at import (like |
10 | | -:mod:`brandkit.common.text`): the per-axis run mutations and the set-only-when-unset |
11 | | -probes live behind a small BACKEND object the format adapter supplies (e.g. docx's |
12 | | -``DOCX_BACKEND`` wrapping ``run.font.name``/``.size``/``.color``; pptx's |
13 | | -``PPTX_BACKEND``). This module only: |
| 11 | +:mod:`brandkit.common.text`): the per-axis run/paragraph mutations and the |
| 12 | +set-only-when-unset probes live behind a small BACKEND object the format adapter |
| 13 | +supplies (e.g. docx's ``DOCX_BACKEND`` wrapping ``run.font.name``/``.size``/ |
| 14 | +``.color`` and the paragraph's ``w:pPr`` geometry; pptx's ``PPTX_BACKEND``). This |
| 15 | +module only: |
14 | 16 |
|
15 | 17 | 1. reads the captured brand axes off the resolver op (:func:`op_latin` / |
16 | | - :func:`op_size_hp` / :func:`op_color`) - STRICTLY from ``op.appearance``, never |
17 | | - a literal in the engine, so off-brand output stays impossible by construction; |
| 18 | + :func:`op_size_hp` / :func:`op_color` / :func:`op_geometry` / :func:`op_table` / |
| 19 | + :func:`op_numbering`) - STRICTLY from ``op.appearance``, never a literal in the |
| 20 | + engine, so off-brand output stays impossible by construction. The axes ride |
| 21 | + different pathways: font/size/color are run axes applied here through the |
| 22 | + backend; geometry is a paragraph axis applied here via the backend's |
| 23 | + ``set_geometry`` hook (docx-only today); table and numbering are docx-only and |
| 24 | + realized by dedicated writers OUTSIDE this orchestration, but declared in |
| 25 | + :data:`APPEARANCE_AXES` so the parity ledger measures them; |
18 | 26 | 2. resolves a run's ``color`` palette TOKEN to its captured ref |
19 | 27 | (:func:`resolve_run_color`), recording a graceful INFO finding for an unknown |
20 | 28 | token (the writer never fabricates a color); |
21 | | - 3. drives the backend to apply those axes (:func:`apply_role_appearance` over a |
22 | | - paragraph's runs; :func:`apply_run_color` for a single run), gating each write |
23 | | - on the backend's ``*_unset`` probe so an inherited-but-correct value is never |
24 | | - clobbered and re-runs stay byte-identical. |
| 29 | + 3. drives the backend to apply the run/paragraph axes |
| 30 | + (:func:`apply_role_appearance` over a paragraph's runs and geometry; |
| 31 | + :func:`apply_run_color` for a single run), gating each write on the backend's |
| 32 | + ``*_unset`` probe so an inherited-but-correct value is never clobbered and |
| 33 | + re-runs stay byte-identical; |
| 34 | + 4. keeps the parity ledger (Cluster E3): :func:`_record_degraded_axes` emits one |
| 35 | + INFO ``appearance_apply_degraded`` finding per captured axis the format backend |
| 36 | + does not declare it realizes, so an unmaterialized axis surfaces gracefully |
| 37 | + instead of silently dropping. |
25 | 38 |
|
26 | 39 | The brand guarantee is preserved end to end: every applied value comes only from |
27 | 40 | ``op.appearance`` / the resolved palette ref, the set-only-when-unset guard is |
@@ -283,15 +296,19 @@ def resolve_run_color( |
283 | 296 | def apply_role_appearance( |
284 | 297 | backend: AppearanceBackend, target, op, findings: list[Finding] |
285 | 298 | ) -> None: |
286 | | - """Apply captured brand typography (font, size, color) from the resolved op as |
287 | | - direct run formatting on ``target``'s runs (hyperlink runs included for docx). |
| 299 | + """Apply captured brand typography (font, size, color) and geometry from the |
| 300 | + resolved op as direct run/paragraph formatting on ``target`` (hyperlink runs |
| 301 | + included for docx). |
288 | 302 |
|
289 | | - The three axes are INDEPENDENT: each is applied only when the run's corresponding |
| 303 | + The run axes are INDEPENDENT: each is applied only when the run's corresponding |
290 | 304 | ``*_unset`` probe is true, so a role carrying a size but no font (or a color but |
291 | | - no font) still applies the axes it has. A target that exposes no runs (a docx |
292 | | - table here) yields nothing and is skipped. An empty appearance (a pre-capture |
293 | | - profile) returns before touching any run, so output stays byte-identical to |
294 | | - today.""" |
| 305 | + no font) still applies the axes it has. Geometry is a separate PARAGRAPH-level |
| 306 | + axis, applied per paragraph via the backend's ``set_geometry`` hook (its |
| 307 | + set-only-when-unset guard lives per PROPERTY inside the backend). Table and |
| 308 | + numbering are MEASURED here by the parity ledger but realized by dedicated |
| 309 | + format-specific writers elsewhere. A target that exposes no runs (a docx table |
| 310 | + here) yields nothing and is skipped. An empty appearance (a pre-capture profile) |
| 311 | + returns before touching any run, so output stays byte-identical to today.""" |
295 | 312 | # Parity ledger (Cluster E3): surface any captured axis this backend cannot |
296 | 313 | # realize BEFORE the early return, so a table/numbering-only op on a format |
297 | 314 | # without those writers is still measured. Appends findings only; it never |
|
0 commit comments