@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.
@@ -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).
@@ -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 };