@hegemonart/get-design-done 1.34.1 → 1.34.3
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.
- package/.claude-plugin/marketplace.json +6 -3
- package/.claude-plugin/plugin.json +5 -2
- package/CHANGELOG.md +44 -0
- package/README.md +16 -0
- package/agents/design-context-builder.md +9 -5
- package/agents/design-verifier.md +8 -5
- package/agents/email-executor.md +148 -0
- package/agents/pdf-executor.md +144 -0
- package/connections/connections.md +4 -0
- package/connections/litmus.md +134 -0
- package/connections/print-renderer.md +113 -0
- package/package.json +1 -1
- package/reference/email-design.md +219 -0
- package/reference/print-design.md +223 -0
- package/reference/registry.json +14 -0
- package/scripts/lib/email/validate-email-html.cjs +157 -0
- package/scripts/lib/print/validate-print-css.cjs +155 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Print Design — Constraint Catalogue
|
|
2
|
+
|
|
3
|
+
This reference is the **print/PDF constraint catalogue**: the hard print-production rules a
|
|
4
|
+
print-ready document MUST honor. Print is a *constrained* output surface — the screen-RGB
|
|
5
|
+
HTML/CSS the web executor emits has no `@page` box model, no bleed/crop marks, no CMYK
|
|
6
|
+
color space, no font embedding, and no 300dpi raster guidance, so it cannot be sent to a
|
|
7
|
+
press as-is. This file is the **authority** that the `pdf-executor` (Phase 34.3-02) generates
|
|
8
|
+
against and the design-verifier's print branch (34.3-03) audits against; neither re-derives
|
|
9
|
+
these rules. The deterministic subset of this catalogue is checked by
|
|
10
|
+
[`scripts/lib/print/validate-print-css.cjs`](../scripts/lib/print/validate-print-css.cjs),
|
|
11
|
+
whose emitted `rule` ids are the constraint-ids defined below (the spec is the authority).
|
|
12
|
+
|
|
13
|
+
It is the print sibling of the other non-web output references. The files have distinct jobs
|
|
14
|
+
and must not be confused:
|
|
15
|
+
|
|
16
|
+
| File | Job |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `reference/platforms.md` (Phase 19) | Interaction **conventions** — navigation, safe areas, gestures, native typography, haptics. *Behavioral* knowledge for native/web screens. |
|
|
19
|
+
| `reference/native-platforms.md` (Phase 34.1) | The native **token bridge** — maps canonical CSS tokens to SwiftUI / Jetpack Compose / Flutter with a precision contract. *Token-identity* knowledge for native generators. |
|
|
20
|
+
| `reference/email-design.md` (Phase 34.2) | The email **constraint catalogue** — table layout, inline styles, MSO comments, dark-mode `color-scheme`. *Structural* knowledge an email template implements. |
|
|
21
|
+
| `reference/print-design.md` (Phase 34.3, this file) | The print **constraint catalogue** — the `@page` box model, bleed + crop marks, CMYK awareness, font embedding, and 300dpi raster fallback. *Production* knowledge a print/PDF document implements. |
|
|
22
|
+
|
|
23
|
+
Per **D-02** there is **no bundled `pdfkit` / `paged` / `puppeteer` / `playwright`
|
|
24
|
+
dependency**: the `pdf-executor` generates **Paged.js-compatible print HTML/CSS** as its
|
|
25
|
+
canonical artifact, and the PDF is rendered by the agent's contract or the *optional*
|
|
26
|
+
print-render connection, never by a build step. Per **D-03** print verify is this
|
|
27
|
+
catalogue's *static* validator plus an *optional* Paged.js-headless-Chrome / PDFKit
|
|
28
|
+
render-test connection that degrades to the static validator when absent. Per **D-10** the
|
|
29
|
+
static checks are deterministic (same CSS/HTML string → same result), with no network and
|
|
30
|
+
no PDF runtime, so the default `npm test` stays green on any machine.
|
|
31
|
+
|
|
32
|
+
Each constraint carries a **rule-id** (`PR-<CLASS>-NN`). Section 8 marks exactly which ids
|
|
33
|
+
the static validator asserts versus which are render-tested guidance only.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 1. Purpose
|
|
38
|
+
|
|
39
|
+
Phase 19 shipped platform *references*; Phase 23 shipped the token engine; Phase 34.1 added
|
|
40
|
+
native generators; Phase 34.2 added the email catalogue. Print/PDF is the last untouched
|
|
41
|
+
product surface, and its constraints are unlike screen, native, and email alike: a physical
|
|
42
|
+
trim with bleed and registration marks, a *subtractive* CMYK color space instead of additive
|
|
43
|
+
screen RGB, a print RIP with **no web fonts** (fonts must be embedded or outlined), and a
|
|
44
|
+
300dpi raster floor (vs the screen's 72/96dpi). Instead of each print document being authored
|
|
45
|
+
from memory, the constraints live once here (the catalogue) and once in the validator (the
|
|
46
|
+
statically-checkable subset), and the executor + verifier consume them. This file is the
|
|
47
|
+
single SC#9-print authority.
|
|
48
|
+
|
|
49
|
+
The catalogue is **prose + tables**, not an implementation. Illustrative snippets are kept to
|
|
50
|
+
2–3 lines. The implementation is `validate-print-css.cjs`.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 2. The print box model — `@page`
|
|
55
|
+
|
|
56
|
+
Print uses the CSS **`@page`** rule to define the page box: its `size` (a named page such as
|
|
57
|
+
`A4` / `Letter`, or an explicit `WIDTH HEIGHT` with physical units), its `margin`, and its
|
|
58
|
+
`marks` (`crop` / `cross`). Screen CSS has no page box at all — content flows in one
|
|
59
|
+
continuous viewport — so a print stylesheet that omits `@page` has no defined page geometry
|
|
60
|
+
and cannot paginate predictably. Paged.js consumes the `@page` CSS to paginate in headless
|
|
61
|
+
Chrome; PDFKit instead constructs the page box programmatically (`new PDFDocument({ size,
|
|
62
|
+
margins })`).
|
|
63
|
+
|
|
64
|
+
| Rule-id | Constraint |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| **PR-PAGE-01** | A print stylesheet MUST declare an `@page` rule — the print box model. Its absence means no defined page geometry. *(Statically checkable: absence of an `@page` rule is flagged.)* |
|
|
67
|
+
| PR-PAGE-02 | The `@page` rule SHOULD set `size` (named `A4`/`Letter` or explicit physical `WIDTH HEIGHT`) and `margin`; use `@page :first` / `:left` / `:right` for cover/spread-specific geometry. |
|
|
68
|
+
| PR-PAGE-03 | Control pagination with `break-before` / `break-after` / `break-inside: avoid` (and the legacy `page-break-*`) so headings, tables, and figures do not split badly across page boundaries. |
|
|
69
|
+
|
|
70
|
+
```css
|
|
71
|
+
@page { size: A4; margin: 12mm; marks: crop cross; bleed: 3mm; }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 3. Bleed + crop marks
|
|
77
|
+
|
|
78
|
+
Print is **trimmed** to a bleed box: any color or image that should reach the physical edge
|
|
79
|
+
must extend ~**3mm past the trim line** (the *bleed*), so that slight cutting variance never
|
|
80
|
+
exposes a white paper edge. **Crop/registration marks** tell the printer (and the trimming
|
|
81
|
+
guillotine) exactly where to cut, and align the CMYK separations on press. Screen CSS has no
|
|
82
|
+
notion of either. The CSS print idioms are the `bleed:` descriptor and `marks: crop` (and/or
|
|
83
|
+
`cross`) on `@page`; Paged.js supports both. A safe area is kept *inside* the trim so no
|
|
84
|
+
critical content sits in the cut-tolerance band.
|
|
85
|
+
|
|
86
|
+
| Rule-id | Constraint |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| **PR-BLEED-01** | A print document targeting edge-to-edge output MUST signal a bleed box / crop marks — a CSS `bleed:` declaration, a `marks: crop\|cross` declaration, or a documented bleed/crop-marks convention. *(Statically checkable: total absence of any bleed/marks signal is flagged.)* |
|
|
89
|
+
| PR-BLEED-02 | Bleed is conventionally **3mm** (≈0.125in); registration marks sit in the trim waste outside the bleed box so they are removed when the sheet is cut. |
|
|
90
|
+
| PR-BLEED-03 | Keep a **safe area** inside the trim (≈3–5mm) — critical text and logos stay inside it so cut tolerance never clips them. |
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 4. CMYK awareness
|
|
95
|
+
|
|
96
|
+
Print is **subtractive CMYK** (cyan/magenta/yellow/key-black ink on paper), not the additive
|
|
97
|
+
screen **RGB** the web executor emits. Pure-RGB output risks visible color shift on press:
|
|
98
|
+
bright RGB blues/greens fall outside the CMYK gamut and reproduce duller, and an untagged
|
|
99
|
+
document leaves the RIP to guess a conversion. A print artifact must therefore signal CMYK
|
|
100
|
+
awareness — a `cmyk()` color, a `color-profile` / `@color-profile` reference (an ICC/CMYK
|
|
101
|
+
target profile), or an explicit documented CMYK-target note. Exact ICC-profile correctness
|
|
102
|
+
and on-press gamut matching are **render-tested**, not statically assertable (see §8).
|
|
103
|
+
|
|
104
|
+
| Rule-id | Constraint |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| **PR-CMYK-01** | A print document MUST show CMYK awareness — a `cmyk()` color value, a `color-profile` / `@color-profile` reference, or a documented CMYK-target note. *(Statically checkable: total absence of any CMYK-awareness signal — pure screen-RGB only — is flagged.)* |
|
|
107
|
+
| PR-CMYK-02 | Prefer named spot/`cmyk()` values for brand colors that must match on press; flag wide-gamut RGB (`display-p3`, neon RGB) that will not survive CMYK conversion. |
|
|
108
|
+
| PR-CMYK-03 | Rich black (e.g. `C30 M30 Y30 K100`) for large solid areas; pure `K100` for small text to avoid registration fringing. *(ICC correctness is render-tested — §8.)* |
|
|
109
|
+
|
|
110
|
+
```css
|
|
111
|
+
:root { color-profile: url(./CoatedFOGRA39.icc); } /* CMYK target */
|
|
112
|
+
.brand { color: cmyk(0% 80% 95% 0%); }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 5. Font embedding
|
|
118
|
+
|
|
119
|
+
Print **RIPs have no web fonts** and no system-font-stack fallback chain — whatever font is
|
|
120
|
+
referenced must be **embedded** in the document (`@font-face` with an embedded `src:`) or the
|
|
121
|
+
text must be **outlined to vector**. A bare `font-family: Arial, sans-serif` system-font-stack
|
|
122
|
+
assumption is a print bug: the RIP may substitute a metrically-different face (reflowing the
|
|
123
|
+
layout) or fail to render the glyphs at all. PDFKit embeds fonts via `doc.registerFont(…)`;
|
|
124
|
+
Paged.js relies on `@font-face` declarations resolved before pagination.
|
|
125
|
+
|
|
126
|
+
| Rule-id | Constraint |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| **PR-FONT-01** | Fonts MUST be embedded or outlined — an `@font-face` rule with an embedded `src:`, or a documented font-embed/outline note. A bare system-font-stack assumption (no embed) is a print bug. *(Statically checkable: absence of any font-embed signal is flagged.)* |
|
|
129
|
+
| PR-FONT-02 | Embed only the weights/styles actually used (subset where possible) to keep the PDF small; ensure the license permits embedding. |
|
|
130
|
+
| PR-FONT-03 | Outline display/headline type to vector when exact rendering matters more than text selectability; keep body copy as embedded text for accessibility and reflow. |
|
|
131
|
+
|
|
132
|
+
```css
|
|
133
|
+
@font-face { font-family: "Brand"; src: url(./Brand.woff2) format("woff2"); }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 6. 300dpi raster fallback
|
|
139
|
+
|
|
140
|
+
Raster/image assets need **300dpi** at final print size or they pixelate — screen assets are
|
|
141
|
+
authored at 72/96dpi, which is ~3–4× too coarse for press. **Vector is preferred** wherever
|
|
142
|
+
possible (logos, icons, rules) because it is resolution-independent; where raster is
|
|
143
|
+
unavoidable (photography), it must carry a 300dpi guarantee. The CSS signals are
|
|
144
|
+
`image-resolution: 300dpi` (or `from-image`), a `min-resolution` media query, or a documented
|
|
145
|
+
300dpi note.
|
|
146
|
+
|
|
147
|
+
| Rule-id | Constraint |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| **PR-DPI-01** | Raster assets MUST carry a 300dpi raster-fallback signal — an `image-resolution:` declaration (`300dpi` / `from-image`), a `min-resolution` query, or a documented 300dpi note. *(Statically checkable: absence of any 300dpi signal is flagged.)* |
|
|
150
|
+
| PR-DPI-02 | Prefer vector (SVG/PDF) for logos, icons, and line art so they stay crisp at any output size; reserve raster for continuous-tone photography. |
|
|
151
|
+
| PR-DPI-03 | Size raster assets so their *effective* resolution at the placed dimensions is ≥300dpi; upscaling a 72dpi screen asset does not add real detail. |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 7. Print color + units
|
|
156
|
+
|
|
157
|
+
Print prefers **physical units** (`mm` / `cm` / `pt` / `in`) over screen `px`, because the
|
|
158
|
+
page is a physical object — `px` has no fixed physical size across RIPs. Print-safe color,
|
|
159
|
+
overprint, and knockout are production concerns the executor should honor; most are
|
|
160
|
+
**render-tested guidance** (see §8), not statically asserted by the validator.
|
|
161
|
+
|
|
162
|
+
| Rule-id | Constraint |
|
|
163
|
+
| --- | --- |
|
|
164
|
+
| PR-UNIT-01 | Use physical units (`mm`/`cm`/`pt`/`in`) for page geometry, margins, and bleed; reserve `px` for screen. *(Guidance.)* |
|
|
165
|
+
| PR-COLOR-01 | **Overprint vs knockout** — small black text overprints (prints on top) to avoid registration gaps; light-on-dark knocks out. *(Render-tested — §8.)* |
|
|
166
|
+
| PR-COLOR-02 | **Trap/registration** — adjacent CMYK separations are trapped (slightly overlapped) so misregistration on press shows no white gap. *(Render-tested — §8.)* |
|
|
167
|
+
| PR-COLOR-03 | **True vector tessellation** — complex vector fills/gradients must tessellate without seams in the RIP. *(Render-tested — §8.)* |
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 8. Statically-checkable vs render-tested
|
|
172
|
+
|
|
173
|
+
This table is the **contract** the validator's `rule` ids map to. The five rule-ids below are
|
|
174
|
+
the deterministic subset that `scripts/lib/print/validate-print-css.cjs` asserts via
|
|
175
|
+
regex/string analysis of the supplied print CSS/HTML string. Every other rule-id in this
|
|
176
|
+
catalogue is **render-tested guidance** — verified by the optional Paged.js-headless-Chrome /
|
|
177
|
+
PDFKit render-test connection (34.3-02), never asserted by the static validator.
|
|
178
|
+
|
|
179
|
+
| Rule-id | Check | Statically checked by the validator? | How verified otherwise |
|
|
180
|
+
| --- | --- | --- | --- |
|
|
181
|
+
| **PR-PAGE-01** | An `@page` rule is present (the print box model) | **YES** — absence flagged | — |
|
|
182
|
+
| **PR-BLEED-01** | A bleed box / crop-marks signal is present (`bleed:` / `marks:` / a documented bleed-marks note) | **YES** — total absence flagged | — |
|
|
183
|
+
| **PR-CMYK-01** | A CMYK-awareness signal is present (`cmyk(` / `color-profile` / `@color-profile` / a CMYK note) | **YES** — total absence (pure RGB) flagged | — |
|
|
184
|
+
| **PR-FONT-01** | A font-embed signal is present (`@font-face` with `src:` / a font-embed/outline note) | **YES** — absence flagged | — |
|
|
185
|
+
| **PR-DPI-01** | A 300dpi raster-fallback signal is present (`image-resolution:` / `min-resolution` / a 300dpi note) | **YES** — absence flagged | — |
|
|
186
|
+
| PR-PAGE-02..03 | `size`/`margin` set, sensible page breaks | No | Render test (paginated output) |
|
|
187
|
+
| PR-BLEED-02..03 | 3mm bleed value, marks in trim waste, safe area | No | Render test (preflight) |
|
|
188
|
+
| PR-CMYK-02..03 | In-gamut brand colors, rich-black vs K100, **ICC-profile correctness** | No | Render test (press proof / ICC) |
|
|
189
|
+
| PR-FONT-02..03 | Subsetted/licensed embeds, vector outlining | No | Render test (PDF inspect) |
|
|
190
|
+
| PR-DPI-02..03 | Vector-preferred, effective ≥300dpi at placed size | No | Render test (preflight) |
|
|
191
|
+
| PR-UNIT-01, PR-COLOR-01..03 | Physical units, **overprint/knockout**, **trap/registration**, **true vector tessellation** | No | Render test (press / RIP) |
|
|
192
|
+
|
|
193
|
+
Notes on the five statically-checked rules:
|
|
194
|
+
|
|
195
|
+
- **Each check is a presence/absence test.** The validator flags the *total absence* of a
|
|
196
|
+
catalogued signal class in the supplied string; the presence of **any one** accepted signal
|
|
197
|
+
for a class satisfies it. The checks are independent, so a document can satisfy four classes
|
|
198
|
+
and trip exactly one.
|
|
199
|
+
- **CMYK awareness is satisfied by any of three signals.** A `cmyk()` color, a
|
|
200
|
+
`color-profile` / `@color-profile` reference, **or** a documented `/* CMYK … */` note each
|
|
201
|
+
satisfy PR-CMYK-01 on their own — a fully RGB document with an explicit CMYK-target note
|
|
202
|
+
still passes (the note records the production intent the RIP needs).
|
|
203
|
+
- **Render-tested rules are out of the static validator's scope by design.** Overprint
|
|
204
|
+
behavior, ICC-profile correctness, trap/registration, and true vector tessellation require
|
|
205
|
+
an actual PDF RIP / rendering engine — they are catalogued here as executor guidance and
|
|
206
|
+
verified by the optional print-render connection, never asserted statically (D-03 / D-10).
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 9. Cross-references
|
|
211
|
+
|
|
212
|
+
- [`reference/email-design.md`](./email-design.md) — the email-constraint sibling
|
|
213
|
+
(table layout, inline styles, MSO comments, dark mode). The non-web siblings share the
|
|
214
|
+
catalogue-plus-static-validator shape.
|
|
215
|
+
- [`reference/native-platforms.md`](./native-platforms.md) — the native token-bridge sibling
|
|
216
|
+
(SwiftUI / Compose / Flutter). The other non-web output surface.
|
|
217
|
+
- [`reference/platforms.md`](./platforms.md) — the interaction-conventions sibling.
|
|
218
|
+
- [`scripts/lib/print/validate-print-css.cjs`](../scripts/lib/print/validate-print-css.cjs)
|
|
219
|
+
— the deterministic static validator that asserts the §8 subset; its `rule` ids are the
|
|
220
|
+
constraint-ids defined here.
|
|
221
|
+
- [`reference/registry.json`](./registry.json) — this catalogue is registered as the
|
|
222
|
+
`print-design` entry (type `heuristic`, phase `34.3`) so the registry round-trip test
|
|
223
|
+
(`test/suite/reference-registry.test.cjs`) stays green (D-05, the 34.1-01 / 34.2-01 lesson).
|
package/reference/registry.json
CHANGED
|
@@ -895,6 +895,20 @@
|
|
|
895
895
|
"type": "heuristic",
|
|
896
896
|
"phase": 34.1,
|
|
897
897
|
"description": "Phase 34.1 token-bridge spec — maps the canonical CSS-token form (Phase 23) to SwiftUI Color/Font/ViewModifier, Jetpack Compose Color/Shapes/Typography/MaterialTheme, and Flutter ThemeData/ColorScheme/TextTheme, with the precision contract (color hex→8-bit channels exact, dimension px→pt/dp/logical px) defining token-identity for the round-trip."
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
"name": "email-design",
|
|
901
|
+
"path": "reference/email-design.md",
|
|
902
|
+
"type": "heuristic",
|
|
903
|
+
"phase": 34.2,
|
|
904
|
+
"description": "Phase 34.2 email-constraint catalogue — table-based layout (not flexbox/grid/position), inline styles (not a <style> block), MSO conditional comments for Outlook, dark-mode color-scheme/prefers-color-scheme handling, ~600px max-width, image/alt rules, and top-20-client quirks; the authority the email-executor generates against and the design-verifier email branch audits against, and the rule-id source for scripts/lib/email/validate-email-html.cjs."
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
"name": "print-design",
|
|
908
|
+
"path": "reference/print-design.md",
|
|
909
|
+
"type": "heuristic",
|
|
910
|
+
"phase": 34.3,
|
|
911
|
+
"description": "Phase 34.3 print-constraint catalogue — @page size/margin/marks (the print box model), bleed box + crop/registration marks, CMYK color-space awareness (subtractive, not screen RGB), font embedding/outlining (print RIPs have no web fonts), and 300dpi raster-fallback guidance; the authority the pdf-executor generates against and the design-verifier print branch audits against, and the rule-id source for scripts/lib/print/validate-print-css.cjs."
|
|
898
912
|
}
|
|
899
913
|
]
|
|
900
914
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* email/validate-email-html.cjs — static email-HTML constraint validator
|
|
3
|
+
* (Phase 34.2-01).
|
|
4
|
+
*
|
|
5
|
+
* Pure, deterministic regex/string analysis of an email-HTML STRING. Same
|
|
6
|
+
* input -> identical output. It checks the statically-verifiable SUBSET of the
|
|
7
|
+
* constraint catalogue in `reference/email-design.md` §8 — the spec is the
|
|
8
|
+
* authority; the `rule` ids emitted here are constraint-ids defined there.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IS CHECKED (the four deterministic classes, emitted in a stable order):
|
|
11
|
+
* EM-LAYOUT-01 no `display:flex` / `display:grid` / `position:absolute|fixed`
|
|
12
|
+
* (and `sticky`) in any style — modern box primitives email
|
|
13
|
+
* clients drop. (Presence is flagged; table layout is expected.)
|
|
14
|
+
* EM-STYLE-01 no `<style>` block used as the PRIMARY styling mechanism. A
|
|
15
|
+
* `<style>` block is flagged when, after removing `@media { … }`
|
|
16
|
+
* groups, `@font-face`, `@import` and comments, residual CSS
|
|
17
|
+
* rules remain OR the block exceeds a generous size threshold.
|
|
18
|
+
* A small `@media`-only dark-mode/responsive block (EM-STYLE-04)
|
|
19
|
+
* is tolerated.
|
|
20
|
+
* EM-MSO-01 a full-email document (has <html>/<body> or a layout <table>)
|
|
21
|
+
* contains at least one MSO conditional comment
|
|
22
|
+
* (`<!--[if mso]>` / `<!--[if !mso]>`). Absence is flagged.
|
|
23
|
+
* A bare fragment is NOT flagged.
|
|
24
|
+
* EM-DARK-01 a color-scheme signal is present — a `<meta name="color-scheme">`
|
|
25
|
+
* and/or a CSS `color-scheme:` declaration and/or a
|
|
26
|
+
* `prefers-color-scheme` query. Total absence is flagged. A meta
|
|
27
|
+
* alone satisfies it (decoupled from any <style>).
|
|
28
|
+
*
|
|
29
|
+
* WHAT IS *NOT* CHECKED (catalogued in reference/email-design.md as render-tested
|
|
30
|
+
* guidance — verified by the optional Litmus / Email-on-Acid connection at
|
|
31
|
+
* 34.2-02, never by this validator): ~600px width, ghost tables/VML, bulletproof
|
|
32
|
+
* buttons, image width/height/alt, per-client (EM-CLIENT-01..20) pixel quirks.
|
|
33
|
+
*
|
|
34
|
+
* PURITY (D-02 / D-10): operates only on the passed string — no fs of the
|
|
35
|
+
* document, no network, no child-process spawn, no mjml runtime import, no Date,
|
|
36
|
+
* no process.env. This file has zero require() calls (node builtins included).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
'use strict';
|
|
40
|
+
|
|
41
|
+
// Modern box primitives email clients drop (EM-LAYOUT-01). Whitespace-tolerant.
|
|
42
|
+
const FLEX_RE = /display\s*:\s*flex\b/i;
|
|
43
|
+
const GRID_RE = /display\s*:\s*grid\b/i;
|
|
44
|
+
const POSITION_RE = /position\s*:\s*(?:absolute|fixed|sticky)\b/i;
|
|
45
|
+
|
|
46
|
+
// <style>…</style> block capture (EM-STYLE-01).
|
|
47
|
+
const STYLE_BLOCK_RE = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
|
|
48
|
+
const AT_MEDIA_GROUP_RE = /@media[^{]*\{(?:[^{}]*\{[^{}]*\})*[^{}]*\}/gi;
|
|
49
|
+
const AT_FONTFACE_GROUP_RE = /@font-face\s*\{[^{}]*\}/gi;
|
|
50
|
+
const AT_IMPORT_RE = /@import[^;]*;/gi;
|
|
51
|
+
const CSS_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
|
|
52
|
+
// A CSS rule = a selector followed by a `{ … }` declaration block.
|
|
53
|
+
const CSS_RULE_RE = /[^{}@;]+\{[^{}]*\}/;
|
|
54
|
+
// Generous size threshold: a <style> body this large is a primary sheet even if
|
|
55
|
+
// it parsed as @media-only (deterministic guard against huge tolerated blocks).
|
|
56
|
+
const STYLE_PRIMARY_CHAR_THRESHOLD = 1024;
|
|
57
|
+
|
|
58
|
+
// MSO conditional comments (EM-MSO-01).
|
|
59
|
+
const MSO_COMMENT_RE = /<!--\[if\s+(?:!\s*)?mso/i;
|
|
60
|
+
// "Full email" signal — only then is a missing MSO comment flagged.
|
|
61
|
+
const FULL_EMAIL_RE = /<html[\s>]|<body[\s>]|<table[\s>]/i;
|
|
62
|
+
|
|
63
|
+
// color-scheme signals (EM-DARK-01) — any one satisfies the check.
|
|
64
|
+
const META_COLOR_SCHEME_RE = /<meta\b[^>]*name\s*=\s*["']?\s*color-scheme\b/i;
|
|
65
|
+
const CSS_COLOR_SCHEME_RE = /color-scheme\s*:/i;
|
|
66
|
+
const PREFERS_COLOR_SCHEME_RE = /prefers-color-scheme/i;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Decide whether a captured <style> block body is a PRIMARY styling mechanism.
|
|
70
|
+
* Tolerates an @media-only (responsive/dark) block per EM-STYLE-04.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} body raw inner text of a <style>…</style>
|
|
73
|
+
* @returns {boolean} true when the block carries non-@media rules or is oversized
|
|
74
|
+
*/
|
|
75
|
+
function isPrimaryStyleBlock(body) {
|
|
76
|
+
if (body.length > STYLE_PRIMARY_CHAR_THRESHOLD) return true;
|
|
77
|
+
const residual = body
|
|
78
|
+
.replace(CSS_COMMENT_RE, '')
|
|
79
|
+
.replace(AT_MEDIA_GROUP_RE, '')
|
|
80
|
+
.replace(AT_FONTFACE_GROUP_RE, '')
|
|
81
|
+
.replace(AT_IMPORT_RE, '');
|
|
82
|
+
return CSS_RULE_RE.test(residual);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validate an email-HTML string against the statically-checkable constraint
|
|
87
|
+
* subset of reference/email-design.md §8.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} html the email HTML (the caller reads the file and passes the
|
|
90
|
+
* content — this function never touches the filesystem)
|
|
91
|
+
* @param {{ checks?: string[] }} [opts] reserved for future toggles; default
|
|
92
|
+
* runs all four classes
|
|
93
|
+
* @returns {{ ok: boolean, violations: Array<{ rule: string, detail: string }> }}
|
|
94
|
+
* `ok === (violations.length === 0)`; each violation's `rule` is a catalogued
|
|
95
|
+
* constraint-id and `detail` is a short human string.
|
|
96
|
+
*/
|
|
97
|
+
function validateEmailHtml(html, opts) {
|
|
98
|
+
if (typeof html !== 'string') {
|
|
99
|
+
throw new TypeError('validateEmailHtml(html): html must be a string');
|
|
100
|
+
}
|
|
101
|
+
void opts; // reserved
|
|
102
|
+
/** @type {Array<{ rule: string, detail: string }>} */
|
|
103
|
+
const violations = [];
|
|
104
|
+
|
|
105
|
+
// --- EM-LAYOUT-01: no flexbox/grid/absolute|fixed positioning -------------
|
|
106
|
+
const layoutHits = [];
|
|
107
|
+
if (FLEX_RE.test(html)) layoutHits.push('display:flex');
|
|
108
|
+
if (GRID_RE.test(html)) layoutHits.push('display:grid');
|
|
109
|
+
if (POSITION_RE.test(html)) layoutHits.push('position:absolute|fixed|sticky');
|
|
110
|
+
if (layoutHits.length > 0) {
|
|
111
|
+
violations.push({
|
|
112
|
+
rule: 'EM-LAYOUT-01',
|
|
113
|
+
detail: `email layout must use role="presentation" tables, not modern box primitives (found ${layoutHits.join(', ')})`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- EM-STYLE-01: no <style> block as the primary styling mechanism -------
|
|
118
|
+
STYLE_BLOCK_RE.lastIndex = 0;
|
|
119
|
+
let m;
|
|
120
|
+
let primaryStyle = false;
|
|
121
|
+
while ((m = STYLE_BLOCK_RE.exec(html)) !== null) {
|
|
122
|
+
if (isPrimaryStyleBlock(m[1])) {
|
|
123
|
+
primaryStyle = true;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (primaryStyle) {
|
|
128
|
+
violations.push({
|
|
129
|
+
rule: 'EM-STYLE-01',
|
|
130
|
+
detail: 'visual styling must be inline; a <style> block with non-@media rules is stripped by Gmail (only a small @media-only block is tolerated)',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// --- EM-MSO-01: an MSO conditional comment in a full-email document -------
|
|
135
|
+
if (FULL_EMAIL_RE.test(html) && !MSO_COMMENT_RE.test(html)) {
|
|
136
|
+
violations.push({
|
|
137
|
+
rule: 'EM-MSO-01',
|
|
138
|
+
detail: 'a full email must include an MSO conditional comment (<!--[if mso]> … <![endif]-->) for Outlook\'s Word rendering engine',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- EM-DARK-01: a color-scheme signal is present -------------------------
|
|
143
|
+
const hasColorScheme =
|
|
144
|
+
META_COLOR_SCHEME_RE.test(html) ||
|
|
145
|
+
CSS_COLOR_SCHEME_RE.test(html) ||
|
|
146
|
+
PREFERS_COLOR_SCHEME_RE.test(html);
|
|
147
|
+
if (!hasColorScheme) {
|
|
148
|
+
violations.push({
|
|
149
|
+
rule: 'EM-DARK-01',
|
|
150
|
+
detail: 'declare a color-scheme signal (<meta name="color-scheme">, CSS color-scheme:, or @media prefers-color-scheme) so clients keep the intended palette',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { ok: violations.length === 0, violations };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = { validateEmailHtml };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* print/validate-print-css.cjs — static print-CSS constraint validator
|
|
3
|
+
* (Phase 34.3-01).
|
|
4
|
+
*
|
|
5
|
+
* Pure, deterministic regex/string analysis of a print CSS/HTML STRING. Same
|
|
6
|
+
* input -> identical output. It checks the statically-verifiable SUBSET of the
|
|
7
|
+
* constraint catalogue in `reference/print-design.md` §8 — the spec is the
|
|
8
|
+
* authority; the `rule` ids emitted here are constraint-ids defined there.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IS CHECKED (the five deterministic classes, emitted in a stable order):
|
|
11
|
+
* PR-PAGE-01 an `@page` rule is present — the print box model. Its absence in
|
|
12
|
+
* a print stylesheet means no defined page geometry. (Absence flagged.)
|
|
13
|
+
* PR-BLEED-01 a bleed box / crop-marks signal is present — a CSS `bleed:`
|
|
14
|
+
* declaration, a `marks:` (crop|cross) declaration, or a documented
|
|
15
|
+
* bleed/crop-marks note. (Total absence flagged.)
|
|
16
|
+
* PR-CMYK-01 a CMYK-awareness signal is present — a `cmyk(` color, a
|
|
17
|
+
* `color-profile` / `@color-profile` reference, or a documented CMYK
|
|
18
|
+
* note. (Total RGB-only absence flagged.)
|
|
19
|
+
* PR-FONT-01 a font-embed signal is present — an `@font-face` rule carrying a
|
|
20
|
+
* `src:` (embedded font), or a documented font-embed/outline note. A
|
|
21
|
+
* bare system-font-stack with no embed is flagged.
|
|
22
|
+
* PR-DPI-01 a 300dpi raster-fallback signal is present — an `image-resolution:`
|
|
23
|
+
* declaration (300dpi / from-image), a `min-resolution` query, or a
|
|
24
|
+
* documented 300dpi note. (Absence flagged.)
|
|
25
|
+
*
|
|
26
|
+
* WHAT IS *NOT* CHECKED (catalogued in reference/print-design.md as render-tested
|
|
27
|
+
* guidance — verified by the optional Paged.js-headless-Chrome / PDFKit render
|
|
28
|
+
* connection at 34.3-02, never by this validator): exact overprint/knockout
|
|
29
|
+
* behavior, ICC-profile correctness / on-press gamut matching, trap/registration,
|
|
30
|
+
* true vector tessellation, and per-output preflight (PR-*-02/03, PR-UNIT-01,
|
|
31
|
+
* PR-COLOR-01..03).
|
|
32
|
+
*
|
|
33
|
+
* PURITY (D-02 / D-10): operates only on the passed string — no fs of the
|
|
34
|
+
* document, no network, no child-process spawn, no pdfkit/paged/puppeteer/
|
|
35
|
+
* playwright runtime import, no Date, no process.env. This file has zero
|
|
36
|
+
* require() calls (node builtins included).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
'use strict';
|
|
40
|
+
|
|
41
|
+
// --- PR-PAGE-01: an @page rule is present ---------------------------------
|
|
42
|
+
// Require an actual @page RULE (an optional `:pseudo` selector then a `{` block),
|
|
43
|
+
// not the bare token — so negative prose like "NO @page rule" in a comment does
|
|
44
|
+
// not count as a present rule.
|
|
45
|
+
const AT_PAGE_RE = /@page\b[^;{}]*\{/i;
|
|
46
|
+
|
|
47
|
+
// --- PR-BLEED-01: a bleed box / crop-marks signal -------------------------
|
|
48
|
+
// A CSS `bleed:` descriptor, a `marks:` (crop|cross) descriptor, or a
|
|
49
|
+
// documented bleed/crop-marks note. The declaration forms require a value after
|
|
50
|
+
// the colon (so a negative-prose "NO bleed box" comment does NOT match); the
|
|
51
|
+
// note form requires the word "crop"/"registration" adjacent to "mark(s)".
|
|
52
|
+
const BLEED_DECL_RE = /\bbleed\s*:\s*\S/i;
|
|
53
|
+
const MARKS_DECL_RE = /\bmarks\s*:\s*(?:crop|cross)\b/i;
|
|
54
|
+
const CROP_MARKS_NOTE_RE = /\b(?:crop|registration)\s+marks?\b/i;
|
|
55
|
+
|
|
56
|
+
// --- PR-CMYK-01: a CMYK-awareness signal ----------------------------------
|
|
57
|
+
// A cmyk() color, a color-profile / @color-profile reference, or a documented
|
|
58
|
+
// "CMYK" note (the word CMYK anywhere — a comment recording production intent).
|
|
59
|
+
const CMYK_FN_RE = /\bcmyk\s*\(/i;
|
|
60
|
+
const COLOR_PROFILE_RE = /@?color-profile\b/i;
|
|
61
|
+
const CMYK_NOTE_RE = /\bCMYK\b/i;
|
|
62
|
+
|
|
63
|
+
// --- PR-FONT-01: a font-embed signal --------------------------------------
|
|
64
|
+
// An @font-face rule whose body carries a `src:` (an embedded font), or a
|
|
65
|
+
// documented font-embed/outline note.
|
|
66
|
+
const FONT_FACE_SRC_RE = /@font-face\s*\{[^{}]*\bsrc\s*:/i;
|
|
67
|
+
const FONT_EMBED_NOTE_RE = /\b(?:font[\s-]*embed(?:ding|ded)?|embed(?:ded)?\s+font|outline(?:d)?\s+(?:to\s+)?(?:vector|font)|font[\s-]*outline)\b/i;
|
|
68
|
+
|
|
69
|
+
// --- PR-DPI-01: a 300dpi raster-fallback signal ---------------------------
|
|
70
|
+
// An image-resolution declaration, a min-resolution query, or a documented
|
|
71
|
+
// 300dpi note (300 immediately followed by dpi/ppi, optionally spaced/hyphenated).
|
|
72
|
+
const IMAGE_RESOLUTION_RE = /\bimage-resolution\s*:/i;
|
|
73
|
+
const MIN_RESOLUTION_RE = /\bmin-resolution\b/i;
|
|
74
|
+
const DPI_300_NOTE_RE = /\b300\s*-?\s*(?:dpi|ppi)\b/i;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate a print CSS/HTML string against the statically-checkable constraint
|
|
78
|
+
* subset of reference/print-design.md §8.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} input the print CSS (or HTML carrying a print stylesheet);
|
|
81
|
+
* the caller reads the file and passes the content — this function never
|
|
82
|
+
* touches the filesystem
|
|
83
|
+
* @param {{ checks?: string[] }} [opts] reserved for future toggles; default
|
|
84
|
+
* runs all five classes
|
|
85
|
+
* @returns {{ ok: boolean, violations: Array<{ rule: string, detail: string }> }}
|
|
86
|
+
* `ok === (violations.length === 0)`; each violation's `rule` is a catalogued
|
|
87
|
+
* constraint-id and `detail` is a short human string.
|
|
88
|
+
*/
|
|
89
|
+
function validatePrintCss(input, opts) {
|
|
90
|
+
if (typeof input !== 'string') {
|
|
91
|
+
throw new TypeError('validatePrintCss(input): input must be a string');
|
|
92
|
+
}
|
|
93
|
+
void opts; // reserved
|
|
94
|
+
/** @type {Array<{ rule: string, detail: string }>} */
|
|
95
|
+
const violations = [];
|
|
96
|
+
|
|
97
|
+
// --- PR-PAGE-01: an @page rule is present (the print box model) -----------
|
|
98
|
+
if (!AT_PAGE_RE.test(input)) {
|
|
99
|
+
violations.push({
|
|
100
|
+
rule: 'PR-PAGE-01',
|
|
101
|
+
detail: 'a print stylesheet must declare an @page rule (the print box model — size/margin/marks); none found',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- PR-BLEED-01: a bleed box / crop-marks signal -------------------------
|
|
106
|
+
const hasBleed =
|
|
107
|
+
BLEED_DECL_RE.test(input) ||
|
|
108
|
+
MARKS_DECL_RE.test(input) ||
|
|
109
|
+
CROP_MARKS_NOTE_RE.test(input);
|
|
110
|
+
if (!hasBleed) {
|
|
111
|
+
violations.push({
|
|
112
|
+
rule: 'PR-BLEED-01',
|
|
113
|
+
detail: 'declare a bleed box / crop marks (a `bleed:` declaration, a `marks: crop|cross` declaration, or a documented bleed/crop-marks note) so edge-to-edge content survives the trim',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- PR-CMYK-01: a CMYK-awareness signal ----------------------------------
|
|
118
|
+
const hasCmyk =
|
|
119
|
+
CMYK_FN_RE.test(input) ||
|
|
120
|
+
COLOR_PROFILE_RE.test(input) ||
|
|
121
|
+
CMYK_NOTE_RE.test(input);
|
|
122
|
+
if (!hasCmyk) {
|
|
123
|
+
violations.push({
|
|
124
|
+
rule: 'PR-CMYK-01',
|
|
125
|
+
detail: 'signal CMYK awareness (a cmyk() color, a color-profile/@color-profile reference, or a documented CMYK-target note); print is subtractive CMYK, not screen RGB',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --- PR-FONT-01: a font-embed signal --------------------------------------
|
|
130
|
+
const hasFontEmbed =
|
|
131
|
+
FONT_FACE_SRC_RE.test(input) ||
|
|
132
|
+
FONT_EMBED_NOTE_RE.test(input);
|
|
133
|
+
if (!hasFontEmbed) {
|
|
134
|
+
violations.push({
|
|
135
|
+
rule: 'PR-FONT-01',
|
|
136
|
+
detail: 'embed or outline fonts (an @font-face with an embedded src:, or a documented font-embed/outline note); print RIPs have no web fonts and no system-font fallback',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- PR-DPI-01: a 300dpi raster-fallback signal ---------------------------
|
|
141
|
+
const hasDpi =
|
|
142
|
+
IMAGE_RESOLUTION_RE.test(input) ||
|
|
143
|
+
MIN_RESOLUTION_RE.test(input) ||
|
|
144
|
+
DPI_300_NOTE_RE.test(input);
|
|
145
|
+
if (!hasDpi) {
|
|
146
|
+
violations.push({
|
|
147
|
+
rule: 'PR-DPI-01',
|
|
148
|
+
detail: 'provide a 300dpi raster-fallback signal (image-resolution: 300dpi/from-image, a min-resolution query, or a documented 300dpi note); screen 72/96dpi rasters pixelate in print',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { ok: violations.length === 0, violations };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = { validatePrintCss };
|