@samva/markup 0.11.3 → 0.12.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## @samva/markup@0.11.4
2
+
3
+ ### Diagnostics carry their upgrade recipe
4
+
5
+ A template diagnostic whose code marks an incompatible template format now includes `upgrade`, the
6
+ agent instructions for migrating the template, in structured results and after `Fix:` in formatted
7
+ output.
8
+
1
9
  ## @samva/markup@0.11.2
2
10
 
3
11
  ### Published files no longer point at missing source maps
@@ -1,6 +1,6 @@
1
1
  import { t as BrandPlugin } from "./brand-plugin-CHhcei-j.js";
2
- import { r as EmailDiagnostic } from "./diagnostics-B-Gym34R.js";
3
- import { r as EmailNode } from "./jsx-runtime-ylIOnaSf.js";
2
+ import { r as EmailDiagnostic } from "./diagnostics-CUYlQ_6x.js";
3
+ import { r as EmailNode } from "./jsx-runtime-DC7xG0aA.js";
4
4
  //#region src/email/brand.d.ts
5
5
  interface EmailBrandAddress {
6
6
  readonly street1: string;
package/dist/brand.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as BrandPlugin } from "./brand-plugin-CHhcei-j.js";
2
- import { _ as EmailBrandSocialLink, a as assetUrl, b as renderedFooterMarkers, c as isExternalAsset, d as BrandFooterProps, f as BrandLogoProps, g as EmailBrandFooter, h as EmailBrandAssets, i as assetEntry, l as BRAND_COMPONENT_CLASSES, m as EmailBrandAddress, n as AssetEntry, o as checkAssetBase, p as EmailBrand, r as AssetManifest, s as isAssetPath, t as ASSET_CONTENT_TYPES, u as BrandComponents, v as RenderedFooterMarkers, x as withoutUnsubscribeRow, y as brandComponents } from "./assets-B0yseju_.js";
2
+ import { _ as EmailBrandSocialLink, a as assetUrl, b as renderedFooterMarkers, c as isExternalAsset, d as BrandFooterProps, f as BrandLogoProps, g as EmailBrandFooter, h as EmailBrandAssets, i as assetEntry, l as BRAND_COMPONENT_CLASSES, m as EmailBrandAddress, n as AssetEntry, o as checkAssetBase, p as EmailBrand, r as AssetManifest, s as isAssetPath, t as ASSET_CONTENT_TYPES, u as BrandComponents, v as RenderedFooterMarkers, x as withoutUnsubscribeRow, y as brandComponents } from "./assets-DCULM6QD.js";
3
3
  import { t as Diagnostic } from "./diagnostic-model-DxthUZBT.js";
4
4
  import { Result } from "better-result";
5
5
  //#region src/email/samva-brand-plugin.d.ts
@@ -1,9 +1,9 @@
1
1
  import { n as parseBrandSpecifier, t as BrandPlugin } from "./brand-plugin-CHhcei-j.js";
2
- import { a as TemplateDiagnostic, r as EmailDiagnostic } from "./diagnostics-B-Gym34R.js";
3
- import { r as EmailNode } from "./jsx-runtime-ylIOnaSf.js";
4
- import { n as AssetEntry, p as EmailBrand, u as BrandComponents } from "./assets-B0yseju_.js";
5
- import { C as Compound, D as EmittedPosition, S as CompiledSelector, T as matchesSelector, _ as Specificity, b as parseStylesheet, ct as TemplateIr, g as CssRule, h as CssDeclaration, r as RenderedEmail, v as Stylesheet, w as classListOf, x as Ancestry, y as compileSelector } from "./render-DQKz4Aco.js";
6
- import { a as TEMPLATE_NAME_PATTERN, n as EmailContent } from "./template-DMPwxMSq.js";
2
+ import { a as TemplateDiagnostic, r as EmailDiagnostic } from "./diagnostics-CUYlQ_6x.js";
3
+ import { r as EmailNode } from "./jsx-runtime-DC7xG0aA.js";
4
+ import { n as AssetEntry, p as EmailBrand, u as BrandComponents } from "./assets-DCULM6QD.js";
5
+ import { C as Compound, D as EmittedPosition, S as CompiledSelector, T as matchesSelector, _ as Specificity, b as parseStylesheet, ct as TemplateIr, g as CssRule, h as CssDeclaration, r as RenderedEmail, v as Stylesheet, w as classListOf, x as Ancestry, y as compileSelector } from "./render-C28ucTh8.js";
6
+ import { a as TEMPLATE_NAME_PATTERN, n as EmailContent } from "./template-B-UDXK8a.js";
7
7
  import { caniemail } from "caniemail";
8
8
  //#region src/email/compatibility.d.ts
9
9
  type CaniemailClient = Parameters<typeof caniemail>[0]["clients"][number];
package/dist/compiler.js CHANGED
@@ -3,11 +3,11 @@ import { n as TEMPLATE_NAME_PATTERN } from "./template-zLUjcwfF.js";
3
3
  import { D as samvaBrandPlugin, T as parseBrandSpecifier, _ as renderIr, w as brandSpecifierPattern } from "./html-DjPxwjLi.js";
4
4
  import { c as parseBrandCss, d as formatColor, f as parseColor, i as brandTailwindCss, u as downlevelColors } from "./theme-css-CEUrDpqq.js";
5
5
  import { a as downlevelMediaQuery, c as renderHeadCss, i as matchesSelector, l as resolveVariables, n as applyStylesheet, o as finalizeValue, r as classListOf, s as foldCalc, u as unquote } from "./ir-De6pTH51.js";
6
- import { i as lineColumn, t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
7
- import { a as lowerProject, d as resolveFromDirectory, f as resolveThemeImports, i as completed, l as Findings, n as introducedProfileErrors, r as loadProject, t as checkStaticProfile, u as projectDirectory } from "./profile-PUfi3GPH.js";
6
+ import { i as buildLineMap, s as lineColumn } from "./diagnostic-codes-BduqCv_h.js";
7
+ import { a as lowerProject, d as resolveFromDirectory, f as resolveThemeImports, i as completed, l as Findings, n as introducedProfileErrors, r as loadProject, t as checkStaticProfile, u as projectDirectory } from "./profile-CWZ2kkt-.js";
8
8
  import { c as checkAssetBase, i as withoutUnsubscribeRow, l as isAssetPath, n as brandComponents, o as assetEntry, s as assetUrl, t as BRAND_COMPONENT_CLASSES } from "./brand-DTctcDmf.js";
9
- import { t as EmailCompileError } from "./diagnostics-CnKFefKz.js";
10
- import { n as compileEmail, r as renderEmailContent } from "./render-D2ht57ZZ.js";
9
+ import { t as EmailCompileError } from "./diagnostics-C0Tr8-eQ.js";
10
+ import { n as compileEmail, r as renderEmailContent } from "./render-BVW-ESn-.js";
11
11
  import { caniemail, parseHtml, rawData } from "caniemail";
12
12
  import { generate, ident, parse, walk } from "css-tree";
13
13
  import { parse as parse$1 } from "@babel/parser";
@@ -0,0 +1,437 @@
1
+ //#region src/diagnostic-model.ts
2
+ /** Newline-offset line map. Build once per source, query with `lineColumn`. */
3
+ const buildLineMap = (source) => {
4
+ const lineStarts = [0];
5
+ for (let index = 0; index < source.length; index++) {
6
+ const code = source.charCodeAt(index);
7
+ if (code === 10 || code === 13 && source.charCodeAt(index + 1) !== 10 || code === 8232 || code === 8233) lineStarts.push(index + 1);
8
+ }
9
+ return lineStarts;
10
+ };
11
+ /** 1-based line/column for an absolute offset, via binary search. */
12
+ const lineColumn = (lineMap, offset) => {
13
+ let low = 0;
14
+ let high = lineMap.length - 1;
15
+ while (low < high) {
16
+ const middle = low + high + 1 >> 1;
17
+ if (lineMap[middle] <= offset) low = middle;
18
+ else high = middle - 1;
19
+ }
20
+ return {
21
+ line: low + 1,
22
+ column: offset - lineMap[low] + 1
23
+ };
24
+ };
25
+ /** Build one formatter for a batch of diagnostics against the same source. */
26
+ const createDiagnosticFormatter = (source) => {
27
+ const lineMap = buildLineMap(source);
28
+ return (diagnostic) => {
29
+ const { line, column } = lineColumn(lineMap, diagnostic.span.start);
30
+ return `line ${line}:${column} ${diagnostic.message}`;
31
+ };
32
+ };
33
+ /** "line 12:8 unknown class ..." — the shape agents iterate on. */
34
+ const formatDiagnostic = (source, diagnostic) => createDiagnosticFormatter(source)(diagnostic);
35
+ //#endregion
36
+ //#region src/diagnostic-codes.ts
37
+ const codes = (table) => table;
38
+ const DIAGNOSTIC_CODES = codes({
39
+ "no-template": {
40
+ summary: "The entry does not default-export a template.",
41
+ fix: "Default-export defineTemplate({ ... }) imported from @samva/markup."
42
+ },
43
+ "legacy-definition": {
44
+ summary: "The entry default-exports something other than defineTemplate.",
45
+ fix: "Import { defineTemplate } from \"@samva/markup\" and default-export defineTemplate({ ... }).",
46
+ upgrade: [
47
+ "Read the entry and its imports without executing them. For a default-exported defineEmail definition, import defineTemplate from \"@samva/markup\" and replace the definition call.",
48
+ "Preserve the entry path, template id, schema, named fixtures, content, design, bindings, conditions, project partials, assets, theme and brand imports. Replace render(input) returning { subject, preheader?, body } with email: { subject: (input) => ..., preheader: (input) => ..., body: (input) => ... }, retaining its parameter bindings and each returned expression; omit preheader when absent.",
49
+ "Inline module constants only when they are literal strings, numbers or booleans and their bindings are unambiguous; never substitute a fixture value for an input binding. Keep each channel function a single static-profile expression, and keep subject and preheader off <Email>.",
50
+ "Helper calls, `Math.*`, general arithmetic, render statements, spreads, computed properties and runtime clock or seed have no direct conversion. Rewrite with static-profile bindings, conditions, .map, .filter, project partials or `fmt.*` where equivalent; arithmetic is allowed only in formatter arguments and conditions. Otherwise compute the value in the sender, declare a payload field in the schema and every fixture, and explain the required sender change. Never guess time, randomness or computed data.",
51
+ "Explicit text has no email-channel field: plain text is derived from the body. If deriving it changes intended content, or another construct cannot be preserved, explain the tradeoff and ask rather than silently discard it. Do not drop fixtures, weaken validation or hide diagnostics.",
52
+ "Convert every affected entry in the project at its existing path, including emails/ entries. Compile each entry, render every declared fixture and inspect the affected output. Resolve errors before requesting one saved version for the whole project, approved by the user; do not publish."
53
+ ].join("\n\n")
54
+ },
55
+ "dynamic-definition": {
56
+ summary: "A definition field is spread or computed instead of written out.",
57
+ fix: "Write each field as name: value."
58
+ },
59
+ "invalid-template-id": {
60
+ summary: "The template id is not a lowercase kebab-case string literal.",
61
+ fix: "Write id: \"order-shipped\"."
62
+ },
63
+ "missing-schema": {
64
+ summary: "The template declares no input schema.",
65
+ fix: "Add schema: jsonSchema<Input>({ ... })."
66
+ },
67
+ "schema-not-static": {
68
+ summary: "The schema cannot be read without running code.",
69
+ fix: "Write the schema as a literal jsonSchema<Input>({ type: 'object', ... })."
70
+ },
71
+ "invalid-schema": {
72
+ summary: "The schema uses JSON Schema keywords the compiler cannot validate against.",
73
+ fix: "Use only the JSON Schema keywords the package README lists."
74
+ },
75
+ "missing-fixtures": {
76
+ summary: "The template declares no fixtures.",
77
+ fix: "Add fixtures: { default: { ... } } with at least one valid payload."
78
+ },
79
+ "fixtures-not-static": {
80
+ summary: "A fixture is computed instead of written as data.",
81
+ fix: "Write each fixture as a literal object; an imported asset is allowed."
82
+ },
83
+ "invalid-fixture-name": {
84
+ summary: "A fixture key is not a single path segment.",
85
+ fix: "Use letters, digits, '.', '_' and '-', starting with a letter or digit."
86
+ },
87
+ "fixture-invalid": {
88
+ summary: "A fixture does not match the schema.",
89
+ fix: "Change the fixture, or the schema, so every fixture is a payload a send could carry."
90
+ },
91
+ "invalid-locale": {
92
+ summary: "The template's default locale is not a string literal.",
93
+ fix: "Write locale: \"en-US\"."
94
+ },
95
+ "no-channel": {
96
+ summary: "The template declares no channel body.",
97
+ fix: "Add an email body."
98
+ },
99
+ "invalid-channel": {
100
+ summary: "A channel field is not shaped as the channel requires.",
101
+ fix: "Write email: { subject, preheader?, body } with each a function of the input."
102
+ },
103
+ "not-a-function": {
104
+ summary: "A channel field is not a function of the input.",
105
+ fix: "Write (input) => ... in place or declare the function in the project."
106
+ },
107
+ "missing-subject": {
108
+ summary: "The email channel has no subject.",
109
+ fix: "Add subject: (input) => ...."
110
+ },
111
+ "missing-body": {
112
+ summary: "The email channel has no body.",
113
+ fix: "Add body: (input) => (<Email>...</Email>)."
114
+ },
115
+ "multiple-roots": {
116
+ summary: "The body renders nothing or more than one root.",
117
+ fix: "Wrap the body in a single element such as <Email>."
118
+ },
119
+ "preheader-without-shell": {
120
+ summary: "A preheader is set but the body has no <Email> shell to hold it.",
121
+ fix: "Wrap the body in <Email>."
122
+ },
123
+ "wrong-channel": {
124
+ summary: "A channel body does not return its own channel component.",
125
+ fix: "Return one element imported from the channel package.",
126
+ channelOnly: true
127
+ },
128
+ "syntax-error": {
129
+ summary: "A project file does not parse as TSX.",
130
+ fix: "Fix the syntax at the reported location."
131
+ },
132
+ "non-project-import": {
133
+ summary: "The file imports something other than @samva/markup or a project file.",
134
+ fix: "Remove the import, or copy the code you need into a project file."
135
+ },
136
+ "moved-import": {
137
+ summary: "The file imports an @samva/markup entry that moved.",
138
+ fix: "Import email components from \"@samva/markup/email\".",
139
+ upgrade: ["Replace every import from \"@samva/markup/email/components\" with \"@samva/markup/email\" in every project file, templates and partials alike. The imported names are unchanged; change only the specifier.", "Compile each entry and render every declared fixture to confirm the output is unchanged. Resolve errors before requesting one saved version for the whole project, approved by the user; do not publish."].join("\n\n")
140
+ },
141
+ "unresolved-import": {
142
+ summary: "An import names no file in the project.",
143
+ fix: "Check the path and the file extension."
144
+ },
145
+ "recursive-partial": {
146
+ summary: "A partial renders itself; partials are inlined at compile time.",
147
+ fix: "Flatten the recursion, or repeat the structure with .map."
148
+ },
149
+ "nesting-limit": {
150
+ summary: "The template nests deeper than the compiler reads.",
151
+ fix: "Flatten the structure."
152
+ },
153
+ "dynamic-expression": {
154
+ summary: "An expression outside the static profile: a call other than fmt.* and .map, a computed value, or a spread.",
155
+ fix: "Compute the value where the message is sent and bind it from the payload."
156
+ },
157
+ "statement-in-body": {
158
+ summary: "A body contains a statement (const, if, a loop); a body is one expression.",
159
+ fix: "Use cond && <X />, cond ? <A /> : <B /> and .map, or send the computed value in the payload."
160
+ },
161
+ "unsupported-pattern": {
162
+ summary: "A parameter pattern or default value the profile cannot bind.",
163
+ fix: "Use plain names or destructuring without defaults; declare the field required or guard it."
164
+ },
165
+ "hook-call": {
166
+ summary: "A hook was called; templates have no state, effects or context.",
167
+ fix: "Read the input through the function's parameter and send anything else in the payload."
168
+ },
169
+ "unknown-formatter": {
170
+ summary: "fmt.<name> is not a formatter.",
171
+ fix: "Use fmt.money, number, date, time, plural or list."
172
+ },
173
+ "formatter-arguments": {
174
+ summary: "A formatter got the wrong number of arguments.",
175
+ fix: "Match the formatter's signature in the contract."
176
+ },
177
+ "reserved-character": {
178
+ summary: "Text contains a private-use character the compiler reserves.",
179
+ fix: "Remove the characters U+E000 to U+E003."
180
+ },
181
+ "reserved-url": {
182
+ summary: "A URL is the unsubscribe placeholder only BrandFooter can write.",
183
+ fix: "Render <BrandFooter /> instead."
184
+ },
185
+ "reserved-attribute": {
186
+ summary: "An attribute is written only by a brand component.",
187
+ fix: "Render BrandFooter from the brand import instead of marking an element as the footer."
188
+ },
189
+ "dynamic-class": {
190
+ summary: "A class name is built at run time, and Tailwind cannot see it.",
191
+ fix: "Write classes literally, or choose between literal lists: input.vip ? \"bg-amber-100\" : \"bg-white\"."
192
+ },
193
+ "too-many-class-variants": {
194
+ summary: "A class list chooses between more literal lists than the compiler resolves.",
195
+ fix: "Split the choice into fewer conditions, or move part of it to a partial."
196
+ },
197
+ "invalid-style": {
198
+ summary: "A style prop is not a static property object.",
199
+ fix: "Write style={{ backgroundColor: \"#fff\" }} with literal values, or use classes."
200
+ },
201
+ "unknown-field": {
202
+ summary: "A binding names a field the schema does not declare.",
203
+ fix: "Add the field to the schema, or bind a field it declares (the message suggests the nearest)."
204
+ },
205
+ "unguarded-optional": {
206
+ summary: "An optional field is read outside a guard, so a send without it would render a blank.",
207
+ fix: "Guard it with {input.field && ...}, or make the field required."
208
+ },
209
+ "invalid-binding-type": {
210
+ summary: "A binding has the wrong type for where it is used.",
211
+ fix: "Bind a field of the type the position needs, or send it in that type."
212
+ },
213
+ "unknown-component": {
214
+ summary: "A component the profile cannot read.",
215
+ fix: "Import components from @samva/markup/email, the brand import, or the project."
216
+ },
217
+ "dynamic-numeric-prop": {
218
+ summary: "A prop that sizes layout at compile time is not a number literal.",
219
+ fix: "Write the number literally, for example height={44}."
220
+ },
221
+ "button-dynamic-label": {
222
+ summary: "A Button label is read at render time, so classic Outlook cannot size it.",
223
+ fix: "Give the button a width: <Button width={200}>."
224
+ },
225
+ "invalid-columns-each": {
226
+ summary: "<Columns each> is not a list from the input.",
227
+ fix: "Write each={input.items} with a callback returning <Column>."
228
+ },
229
+ "invalid-columns-per": {
230
+ summary: "<Columns per> is not a positive integer literal.",
231
+ fix: "Write per={2}."
232
+ },
233
+ "invalid-button-number": {
234
+ summary: "A Button size prop is not a finite number.",
235
+ fix: "Pass a finite number."
236
+ },
237
+ "unsupported-element": {
238
+ summary: "An element outside the supported email vocabulary.",
239
+ fix: "Use an element from the contract's element table."
240
+ },
241
+ "unsupported-attribute": {
242
+ summary: "An attribute the element does not support.",
243
+ fix: "Use a supported attribute, or data-* and aria-*."
244
+ },
245
+ "missing-attribute": {
246
+ summary: "An element is missing a required attribute.",
247
+ fix: "Add the attribute, for example alt on img."
248
+ },
249
+ "invalid-attribute-value": {
250
+ summary: "An attribute value is not a string or number, or contains control characters.",
251
+ fix: "Pass plain text."
252
+ },
253
+ "duplicate-attribute": {
254
+ summary: "An attribute is written twice.",
255
+ fix: "Keep one."
256
+ },
257
+ "invalid-content": {
258
+ summary: "An element holds content its content model does not allow.",
259
+ fix: "Move the child to a parent that accepts it."
260
+ },
261
+ "void-element-children": {
262
+ summary: "A void element has children.",
263
+ fix: "Remove the children."
264
+ },
265
+ "event-handler": {
266
+ summary: "An event handler; email clients strip scripting.",
267
+ fix: "Remove the handler; use a link for interaction."
268
+ },
269
+ "dangerous-html": {
270
+ summary: "dangerouslySetInnerHTML; there is no raw-HTML escape hatch.",
271
+ fix: "Write the markup as JSX."
272
+ },
273
+ "unsafe-url": {
274
+ summary: "A URL uses a scheme email cannot send safely.",
275
+ fix: "Use an https URL, a mailto: or tel: link, or a bound URL."
276
+ },
277
+ "unsafe-raw-text": {
278
+ summary: "Raw text content contains markup that could end its element.",
279
+ fix: "Remove the offending sequence."
280
+ },
281
+ "unsupported-style-property": {
282
+ summary: "A style property name is not a CSS property.",
283
+ fix: "Write camelCase (backgroundColor) or a custom property (--brand)."
284
+ },
285
+ "invalid-style-value": {
286
+ summary: "A style value is not a string or number.",
287
+ fix: "Pass a string or number."
288
+ },
289
+ "unsafe-style-value": {
290
+ summary: "A style value contains CSS that could break out of its declaration.",
291
+ fix: "Remove the offending characters."
292
+ },
293
+ "missing-head": {
294
+ summary: "Styles that cannot be inlined need a <head> to live in.",
295
+ fix: "Wrap the message in <Email>, or write those declarations inline."
296
+ },
297
+ "invalid-subject": {
298
+ summary: "The subject is not one line of text.",
299
+ fix: "Return a single-line string."
300
+ },
301
+ "invalid-preheader": {
302
+ summary: "The preheader is not a string.",
303
+ fix: "Return a string."
304
+ },
305
+ "invalid-text": {
306
+ summary: "The plain-text body is not a string.",
307
+ fix: "Return a string."
308
+ },
309
+ "button-outlook-fallback": {
310
+ summary: "Classic Outlook needs a height and a plain string label to draw the button.",
311
+ fix: "Give <Button> a height and a string label."
312
+ },
313
+ "unmatched-class": {
314
+ summary: "A class produced no styles.",
315
+ fix: "Use a class the theme or Tailwind defines, or write the style inline."
316
+ },
317
+ "tailwind-dynamic-class": {
318
+ summary: "A class list is assembled at run time.",
319
+ fix: "Write the classes literally."
320
+ },
321
+ "css-parse-error": {
322
+ summary: "A stylesheet does not parse.",
323
+ fix: "Fix the CSS at the reported location."
324
+ },
325
+ "css-import-unresolved": {
326
+ summary: "A stylesheet @import cannot be resolved.",
327
+ fix: "Import the stylesheet from the template module, or use the theme.css layers."
328
+ },
329
+ "css-unsupported-at-rule": {
330
+ summary: "An at-rule is not applied to email output.",
331
+ fix: "Remove it or move the declarations into a class rule."
332
+ },
333
+ "css-conditional-variable": {
334
+ summary: "A custom property is redefined inside a conditional at-rule.",
335
+ fix: "Keep the override on a class rule."
336
+ },
337
+ "email-font-fallback": {
338
+ summary: "A font stack names a project font without a generic fallback.",
339
+ fix: "End the stack with sans-serif, serif or monospace."
340
+ },
341
+ "email-font-family": {
342
+ summary: "@font-face declares no font-family.",
343
+ fix: "Add font-family."
344
+ },
345
+ "email-font-src": {
346
+ summary: "@font-face declares no usable url() src.",
347
+ fix: "Add a url() pointing at a .woff2 file."
348
+ },
349
+ "email-font-format": {
350
+ summary: "@font-face uses a format other than WOFF2.",
351
+ fix: "Convert the file to .woff2."
352
+ },
353
+ "email-font-missing": {
354
+ summary: "@font-face names a file the project does not hold.",
355
+ fix: "Add the file; paths resolve from the stylesheet's directory."
356
+ },
357
+ "invalid-asset-base": {
358
+ summary: "The asset base is not an https URL or a path.",
359
+ fix: "Use an https URL, an absolute path, or a relative path."
360
+ },
361
+ "brand-unavailable": {
362
+ summary: "The build has no brand to resolve a brand import.",
363
+ fix: "Create or select the brand the import names."
364
+ },
365
+ "brand-import-conflict": {
366
+ summary: "The project imports two different brands.",
367
+ fix: "Import one brand specifier everywhere."
368
+ },
369
+ "caniemail/unknown-coverage": {
370
+ summary: "A feature has no client-support data.",
371
+ fix: "Verify it in the target clients."
372
+ },
373
+ "invalid-attribute": {
374
+ summary: "A channel component attribute has a value the channel does not accept.",
375
+ fix: "Use one of the listed values.",
376
+ channelOnly: true
377
+ },
378
+ "unknown-attribute": {
379
+ summary: "A channel component attribute the component does not have.",
380
+ fix: "Use one of the listed attributes.",
381
+ channelOnly: true
382
+ },
383
+ "sms-markup": {
384
+ summary: "An SMS body contains an element; SMS is plain text.",
385
+ fix: "Write text and bindings, with a newline for a line break.",
386
+ channelOnly: true
387
+ },
388
+ "whatsapp-structure": {
389
+ summary: "WhatsApp content sits outside Header, Body, Footer or Buttons.",
390
+ fix: "Move it into <WhatsApp.Body>.",
391
+ channelOnly: true
392
+ },
393
+ "whatsapp-body-required": {
394
+ summary: "A WhatsApp template has no body.",
395
+ fix: "Add <WhatsApp.Body>...</WhatsApp.Body>.",
396
+ channelOnly: true
397
+ },
398
+ "whatsapp-conditional": {
399
+ summary: "A WhatsApp part branches or contains elements; Meta registers its text once.",
400
+ fix: "Choose the wording in the payload, or write separate templates.",
401
+ channelOnly: true
402
+ },
403
+ "whatsapp-header-media": {
404
+ summary: "A WhatsApp media header is not exactly one URL.",
405
+ fix: "Write the URL as the only child: a literal or one binding.",
406
+ channelOnly: true
407
+ },
408
+ "whatsapp-header-variables": {
409
+ summary: "A WhatsApp header text takes more than one variable.",
410
+ fix: "Keep one binding in the header and move the rest to the body.",
411
+ channelOnly: true
412
+ },
413
+ "whatsapp-length": {
414
+ summary: "A WhatsApp part exceeds Meta's length limit.",
415
+ fix: "Shorten the text.",
416
+ channelOnly: true
417
+ },
418
+ "whatsapp-button-text": {
419
+ summary: "A WhatsApp button has no label or value between its tags.",
420
+ fix: "Write the label or value as text between the tags.",
421
+ channelOnly: true
422
+ },
423
+ "whatsapp-button-limit": {
424
+ summary: "The conditions can produce more buttons of a kind than Meta allows.",
425
+ fix: "Remove buttons, or split the template.",
426
+ channelOnly: true
427
+ }
428
+ });
429
+ /** Whether a diagnostic identifier selects an agent-driven format upgrade. */
430
+ const hasUpgradeRecipe = (code) => Object.entries(DIAGNOSTIC_CODES).some(([identifier, spec]) => identifier === code && "upgrade" in spec);
431
+ /** Families whose code carries a suffix chosen at run time. */
432
+ const DIAGNOSTIC_CODE_FAMILIES = { "caniemail/": {
433
+ summary: "A CSS property, HTML element or attribute that a target email client ignores or partly supports (from the caniemail.com data), reported as caniemail/<feature>.",
434
+ fix: "Use the alternative the message names, such as an email component instead of display: flex."
435
+ } };
436
+ //#endregion
437
+ export { createDiagnosticFormatter as a, buildLineMap as i, DIAGNOSTIC_CODE_FAMILIES as n, formatDiagnostic as o, hasUpgradeRecipe as r, lineColumn as s, DIAGNOSTIC_CODES as t };
@@ -12,7 +12,8 @@ var EmailCompileError = class extends TaggedError("EmailCompileError") {
12
12
  const formatEmailDiagnostic = (diagnostic) => {
13
13
  const origin = diagnostic.origins[0];
14
14
  const head = `${origin === void 0 ? "<unknown source>" : `${origin.fileName}:${origin.lineNumber}:${origin.columnNumber}`} ${diagnostic.code}: ${diagnostic.message}`;
15
- return diagnostic.fix === void 0 ? head : `${head}\n Fix: ${diagnostic.fix}`;
15
+ const fixed = diagnostic.fix === void 0 ? head : `${head}\n Fix: ${diagnostic.fix}`;
16
+ return diagnostic.upgrade === void 0 ? fixed : `${fixed}\n Upgrade: ${diagnostic.upgrade}`;
16
17
  };
17
18
  /** True when a set of findings must stop the compile or refuse a publication. */
18
19
  const hasBlockingDiagnostic = (diagnostics) => diagnostics.some((diagnostic) => diagnostic.severity === "error");
@@ -31,6 +31,8 @@ interface EmailDiagnostic {
31
31
  /** A finding of the static compiler: an `EmailDiagnostic` plus the change that resolves it. */
32
32
  interface TemplateDiagnostic extends EmailDiagnostic {
33
33
  readonly fix?: string | undefined;
34
+ /** Agent instructions for migrating a template made incompatible by a compiler change. */
35
+ readonly upgrade?: string | undefined;
34
36
  }
35
37
  declare const EmailCompileError_base: import("better-result").TaggedErrorClass<"EmailCompileError">;
36
38
  declare class EmailCompileError extends EmailCompileError_base<{
@@ -1,4 +1,4 @@
1
- import { a as TemplateDiagnostic, i as EmailDiagnosticSeverity, n as EmailCompileError, o as formatEmailDiagnostic, r as EmailDiagnostic, s as hasBlockingDiagnostic, t as EmailCompatibilityAssessment } from "./diagnostics-B-Gym34R.js";
1
+ import { a as TemplateDiagnostic, i as EmailDiagnosticSeverity, n as EmailCompileError, o as formatEmailDiagnostic, r as EmailDiagnostic, s as hasBlockingDiagnostic, t as EmailCompatibilityAssessment } from "./diagnostics-CUYlQ_6x.js";
2
2
  import { a as createDiagnosticFormatter, i as buildLineMap, n as DiagnosticRepair, o as formatDiagnostic, r as Span, s as lineColumn, t as Diagnostic } from "./diagnostic-model-DxthUZBT.js";
3
3
  //#region src/diagnostic-codes.d.ts
4
4
  interface DiagnosticCodeSpec {
@@ -1,404 +1,3 @@
1
- import { i as lineColumn, n as createDiagnosticFormatter, r as formatDiagnostic, t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
2
- import { n as formatEmailDiagnostic, r as hasBlockingDiagnostic, t as EmailCompileError } from "./diagnostics-CnKFefKz.js";
3
- //#region src/diagnostic-codes.ts
4
- const codes = (table) => table;
5
- const DIAGNOSTIC_CODES = codes({
6
- "no-template": {
7
- summary: "The entry does not default-export a template.",
8
- fix: "Default-export defineTemplate({ ... }) imported from @samva/markup."
9
- },
10
- "legacy-definition": {
11
- summary: "The entry default-exports something other than defineTemplate.",
12
- fix: "Import { defineTemplate } from \"@samva/markup\" and default-export defineTemplate({ ... }).",
13
- upgrade: [
14
- "Read the entry and its imports without executing them. For a default-exported defineEmail definition, import defineTemplate from \"@samva/markup\" and replace the definition call.",
15
- "Preserve the entry path, template id, schema, named fixtures, content, design, bindings, conditions, project partials, assets, theme and brand imports. Replace render(input) returning { subject, preheader?, body } with email: { subject: (input) => ..., preheader: (input) => ..., body: (input) => ... }, retaining its parameter bindings and each returned expression; omit preheader when absent.",
16
- "Inline module constants only when they are literal strings, numbers or booleans and their bindings are unambiguous; never substitute a fixture value for an input binding. Keep each channel function a single static-profile expression, and keep subject and preheader off <Email>.",
17
- "Helper calls, `Math.*`, general arithmetic, render statements, spreads, computed properties and runtime clock or seed have no direct conversion. Rewrite with static-profile bindings, conditions, .map, .filter, project partials or `fmt.*` where equivalent; arithmetic is allowed only in formatter arguments and conditions. Otherwise compute the value in the sender, declare a payload field in the schema and every fixture, and explain the required sender change. Never guess time, randomness or computed data.",
18
- "Explicit text has no email-channel field: plain text is derived from the body. If deriving it changes intended content, or another construct cannot be preserved, explain the tradeoff and ask rather than silently discard it. Do not drop fixtures, weaken validation or hide diagnostics.",
19
- "Convert every affected entry in the project at its existing path, including emails/ entries. Compile each entry, render every declared fixture and inspect the affected output. Resolve errors before requesting one saved version for the whole project, approved by the user; do not publish."
20
- ].join("\n\n")
21
- },
22
- "dynamic-definition": {
23
- summary: "A definition field is spread or computed instead of written out.",
24
- fix: "Write each field as name: value."
25
- },
26
- "invalid-template-id": {
27
- summary: "The template id is not a lowercase kebab-case string literal.",
28
- fix: "Write id: \"order-shipped\"."
29
- },
30
- "missing-schema": {
31
- summary: "The template declares no input schema.",
32
- fix: "Add schema: jsonSchema<Input>({ ... })."
33
- },
34
- "schema-not-static": {
35
- summary: "The schema cannot be read without running code.",
36
- fix: "Write the schema as a literal jsonSchema<Input>({ type: 'object', ... })."
37
- },
38
- "invalid-schema": {
39
- summary: "The schema uses JSON Schema keywords the compiler cannot validate against.",
40
- fix: "Use only the JSON Schema keywords the package README lists."
41
- },
42
- "missing-fixtures": {
43
- summary: "The template declares no fixtures.",
44
- fix: "Add fixtures: { default: { ... } } with at least one valid payload."
45
- },
46
- "fixtures-not-static": {
47
- summary: "A fixture is computed instead of written as data.",
48
- fix: "Write each fixture as a literal object; an imported asset is allowed."
49
- },
50
- "invalid-fixture-name": {
51
- summary: "A fixture key is not a single path segment.",
52
- fix: "Use letters, digits, '.', '_' and '-', starting with a letter or digit."
53
- },
54
- "fixture-invalid": {
55
- summary: "A fixture does not match the schema.",
56
- fix: "Change the fixture, or the schema, so every fixture is a payload a send could carry."
57
- },
58
- "invalid-locale": {
59
- summary: "The template's default locale is not a string literal.",
60
- fix: "Write locale: \"en-US\"."
61
- },
62
- "no-channel": {
63
- summary: "The template declares no channel body.",
64
- fix: "Add an email body."
65
- },
66
- "invalid-channel": {
67
- summary: "A channel field is not shaped as the channel requires.",
68
- fix: "Write email: { subject, preheader?, body } with each a function of the input."
69
- },
70
- "not-a-function": {
71
- summary: "A channel field is not a function of the input.",
72
- fix: "Write (input) => ... in place or declare the function in the project."
73
- },
74
- "missing-subject": {
75
- summary: "The email channel has no subject.",
76
- fix: "Add subject: (input) => ...."
77
- },
78
- "missing-body": {
79
- summary: "The email channel has no body.",
80
- fix: "Add body: (input) => (<Email>...</Email>)."
81
- },
82
- "multiple-roots": {
83
- summary: "The body renders nothing or more than one root.",
84
- fix: "Wrap the body in a single element such as <Email>."
85
- },
86
- "preheader-without-shell": {
87
- summary: "A preheader is set but the body has no <Email> shell to hold it.",
88
- fix: "Wrap the body in <Email>."
89
- },
90
- "wrong-channel": {
91
- summary: "A channel body does not return its own channel component.",
92
- fix: "Return one element imported from the channel package.",
93
- channelOnly: true
94
- },
95
- "syntax-error": {
96
- summary: "A project file does not parse as TSX.",
97
- fix: "Fix the syntax at the reported location."
98
- },
99
- "non-project-import": {
100
- summary: "The file imports something other than @samva/markup or a project file.",
101
- fix: "Remove the import, or copy the code you need into a project file."
102
- },
103
- "moved-import": {
104
- summary: "The file imports an @samva/markup entry that moved.",
105
- fix: "Import email components from \"@samva/markup/email\".",
106
- upgrade: ["Replace every import from \"@samva/markup/email/components\" with \"@samva/markup/email\" in every project file, templates and partials alike. The imported names are unchanged; change only the specifier.", "Compile each entry and render every declared fixture to confirm the output is unchanged. Resolve errors before requesting one saved version for the whole project, approved by the user; do not publish."].join("\n\n")
107
- },
108
- "unresolved-import": {
109
- summary: "An import names no file in the project.",
110
- fix: "Check the path and the file extension."
111
- },
112
- "recursive-partial": {
113
- summary: "A partial renders itself; partials are inlined at compile time.",
114
- fix: "Flatten the recursion, or repeat the structure with .map."
115
- },
116
- "nesting-limit": {
117
- summary: "The template nests deeper than the compiler reads.",
118
- fix: "Flatten the structure."
119
- },
120
- "dynamic-expression": {
121
- summary: "An expression outside the static profile: a call other than fmt.* and .map, a computed value, or a spread.",
122
- fix: "Compute the value where the message is sent and bind it from the payload."
123
- },
124
- "statement-in-body": {
125
- summary: "A body contains a statement (const, if, a loop); a body is one expression.",
126
- fix: "Use cond && <X />, cond ? <A /> : <B /> and .map, or send the computed value in the payload."
127
- },
128
- "unsupported-pattern": {
129
- summary: "A parameter pattern or default value the profile cannot bind.",
130
- fix: "Use plain names or destructuring without defaults; declare the field required or guard it."
131
- },
132
- "hook-call": {
133
- summary: "A hook was called; templates have no state, effects or context.",
134
- fix: "Read the input through the function's parameter and send anything else in the payload."
135
- },
136
- "unknown-formatter": {
137
- summary: "fmt.<name> is not a formatter.",
138
- fix: "Use fmt.money, number, date, time, plural or list."
139
- },
140
- "formatter-arguments": {
141
- summary: "A formatter got the wrong number of arguments.",
142
- fix: "Match the formatter's signature in the contract."
143
- },
144
- "reserved-character": {
145
- summary: "Text contains a private-use character the compiler reserves.",
146
- fix: "Remove the characters U+E000 to U+E003."
147
- },
148
- "reserved-url": {
149
- summary: "A URL is the unsubscribe placeholder only BrandFooter can write.",
150
- fix: "Render <BrandFooter /> instead."
151
- },
152
- "reserved-attribute": {
153
- summary: "An attribute is written only by a brand component.",
154
- fix: "Render BrandFooter from the brand import instead of marking an element as the footer."
155
- },
156
- "dynamic-class": {
157
- summary: "A class name is built at run time, and Tailwind cannot see it.",
158
- fix: "Write classes literally, or choose between literal lists: input.vip ? \"bg-amber-100\" : \"bg-white\"."
159
- },
160
- "too-many-class-variants": {
161
- summary: "A class list chooses between more literal lists than the compiler resolves.",
162
- fix: "Split the choice into fewer conditions, or move part of it to a partial."
163
- },
164
- "invalid-style": {
165
- summary: "A style prop is not a static property object.",
166
- fix: "Write style={{ backgroundColor: \"#fff\" }} with literal values, or use classes."
167
- },
168
- "unknown-field": {
169
- summary: "A binding names a field the schema does not declare.",
170
- fix: "Add the field to the schema, or bind a field it declares (the message suggests the nearest)."
171
- },
172
- "unguarded-optional": {
173
- summary: "An optional field is read outside a guard, so a send without it would render a blank.",
174
- fix: "Guard it with {input.field && ...}, or make the field required."
175
- },
176
- "invalid-binding-type": {
177
- summary: "A binding has the wrong type for where it is used.",
178
- fix: "Bind a field of the type the position needs, or send it in that type."
179
- },
180
- "unknown-component": {
181
- summary: "A component the profile cannot read.",
182
- fix: "Import components from @samva/markup/email, the brand import, or the project."
183
- },
184
- "dynamic-numeric-prop": {
185
- summary: "A prop that sizes layout at compile time is not a number literal.",
186
- fix: "Write the number literally, for example height={44}."
187
- },
188
- "button-dynamic-label": {
189
- summary: "A Button label is read at render time, so classic Outlook cannot size it.",
190
- fix: "Give the button a width: <Button width={200}>."
191
- },
192
- "invalid-columns-each": {
193
- summary: "<Columns each> is not a list from the input.",
194
- fix: "Write each={input.items} with a callback returning <Column>."
195
- },
196
- "invalid-columns-per": {
197
- summary: "<Columns per> is not a positive integer literal.",
198
- fix: "Write per={2}."
199
- },
200
- "invalid-button-number": {
201
- summary: "A Button size prop is not a finite number.",
202
- fix: "Pass a finite number."
203
- },
204
- "unsupported-element": {
205
- summary: "An element outside the supported email vocabulary.",
206
- fix: "Use an element from the contract's element table."
207
- },
208
- "unsupported-attribute": {
209
- summary: "An attribute the element does not support.",
210
- fix: "Use a supported attribute, or data-* and aria-*."
211
- },
212
- "missing-attribute": {
213
- summary: "An element is missing a required attribute.",
214
- fix: "Add the attribute, for example alt on img."
215
- },
216
- "invalid-attribute-value": {
217
- summary: "An attribute value is not a string or number, or contains control characters.",
218
- fix: "Pass plain text."
219
- },
220
- "duplicate-attribute": {
221
- summary: "An attribute is written twice.",
222
- fix: "Keep one."
223
- },
224
- "invalid-content": {
225
- summary: "An element holds content its content model does not allow.",
226
- fix: "Move the child to a parent that accepts it."
227
- },
228
- "void-element-children": {
229
- summary: "A void element has children.",
230
- fix: "Remove the children."
231
- },
232
- "event-handler": {
233
- summary: "An event handler; email clients strip scripting.",
234
- fix: "Remove the handler; use a link for interaction."
235
- },
236
- "dangerous-html": {
237
- summary: "dangerouslySetInnerHTML; there is no raw-HTML escape hatch.",
238
- fix: "Write the markup as JSX."
239
- },
240
- "unsafe-url": {
241
- summary: "A URL uses a scheme email cannot send safely.",
242
- fix: "Use an https URL, a mailto: or tel: link, or a bound URL."
243
- },
244
- "unsafe-raw-text": {
245
- summary: "Raw text content contains markup that could end its element.",
246
- fix: "Remove the offending sequence."
247
- },
248
- "unsupported-style-property": {
249
- summary: "A style property name is not a CSS property.",
250
- fix: "Write camelCase (backgroundColor) or a custom property (--brand)."
251
- },
252
- "invalid-style-value": {
253
- summary: "A style value is not a string or number.",
254
- fix: "Pass a string or number."
255
- },
256
- "unsafe-style-value": {
257
- summary: "A style value contains CSS that could break out of its declaration.",
258
- fix: "Remove the offending characters."
259
- },
260
- "missing-head": {
261
- summary: "Styles that cannot be inlined need a <head> to live in.",
262
- fix: "Wrap the message in <Email>, or write those declarations inline."
263
- },
264
- "invalid-subject": {
265
- summary: "The subject is not one line of text.",
266
- fix: "Return a single-line string."
267
- },
268
- "invalid-preheader": {
269
- summary: "The preheader is not a string.",
270
- fix: "Return a string."
271
- },
272
- "invalid-text": {
273
- summary: "The plain-text body is not a string.",
274
- fix: "Return a string."
275
- },
276
- "button-outlook-fallback": {
277
- summary: "Classic Outlook needs a height and a plain string label to draw the button.",
278
- fix: "Give <Button> a height and a string label."
279
- },
280
- "unmatched-class": {
281
- summary: "A class produced no styles.",
282
- fix: "Use a class the theme or Tailwind defines, or write the style inline."
283
- },
284
- "tailwind-dynamic-class": {
285
- summary: "A class list is assembled at run time.",
286
- fix: "Write the classes literally."
287
- },
288
- "css-parse-error": {
289
- summary: "A stylesheet does not parse.",
290
- fix: "Fix the CSS at the reported location."
291
- },
292
- "css-import-unresolved": {
293
- summary: "A stylesheet @import cannot be resolved.",
294
- fix: "Import the stylesheet from the template module, or use the theme.css layers."
295
- },
296
- "css-unsupported-at-rule": {
297
- summary: "An at-rule is not applied to email output.",
298
- fix: "Remove it or move the declarations into a class rule."
299
- },
300
- "css-conditional-variable": {
301
- summary: "A custom property is redefined inside a conditional at-rule.",
302
- fix: "Keep the override on a class rule."
303
- },
304
- "email-font-fallback": {
305
- summary: "A font stack names a project font without a generic fallback.",
306
- fix: "End the stack with sans-serif, serif or monospace."
307
- },
308
- "email-font-family": {
309
- summary: "@font-face declares no font-family.",
310
- fix: "Add font-family."
311
- },
312
- "email-font-src": {
313
- summary: "@font-face declares no usable url() src.",
314
- fix: "Add a url() pointing at a .woff2 file."
315
- },
316
- "email-font-format": {
317
- summary: "@font-face uses a format other than WOFF2.",
318
- fix: "Convert the file to .woff2."
319
- },
320
- "email-font-missing": {
321
- summary: "@font-face names a file the project does not hold.",
322
- fix: "Add the file; paths resolve from the stylesheet's directory."
323
- },
324
- "invalid-asset-base": {
325
- summary: "The asset base is not an https URL or a path.",
326
- fix: "Use an https URL, an absolute path, or a relative path."
327
- },
328
- "brand-unavailable": {
329
- summary: "The build has no brand to resolve a brand import.",
330
- fix: "Create or select the brand the import names."
331
- },
332
- "brand-import-conflict": {
333
- summary: "The project imports two different brands.",
334
- fix: "Import one brand specifier everywhere."
335
- },
336
- "caniemail/unknown-coverage": {
337
- summary: "A feature has no client-support data.",
338
- fix: "Verify it in the target clients."
339
- },
340
- "invalid-attribute": {
341
- summary: "A channel component attribute has a value the channel does not accept.",
342
- fix: "Use one of the listed values.",
343
- channelOnly: true
344
- },
345
- "unknown-attribute": {
346
- summary: "A channel component attribute the component does not have.",
347
- fix: "Use one of the listed attributes.",
348
- channelOnly: true
349
- },
350
- "sms-markup": {
351
- summary: "An SMS body contains an element; SMS is plain text.",
352
- fix: "Write text and bindings, with a newline for a line break.",
353
- channelOnly: true
354
- },
355
- "whatsapp-structure": {
356
- summary: "WhatsApp content sits outside Header, Body, Footer or Buttons.",
357
- fix: "Move it into <WhatsApp.Body>.",
358
- channelOnly: true
359
- },
360
- "whatsapp-body-required": {
361
- summary: "A WhatsApp template has no body.",
362
- fix: "Add <WhatsApp.Body>...</WhatsApp.Body>.",
363
- channelOnly: true
364
- },
365
- "whatsapp-conditional": {
366
- summary: "A WhatsApp part branches or contains elements; Meta registers its text once.",
367
- fix: "Choose the wording in the payload, or write separate templates.",
368
- channelOnly: true
369
- },
370
- "whatsapp-header-media": {
371
- summary: "A WhatsApp media header is not exactly one URL.",
372
- fix: "Write the URL as the only child: a literal or one binding.",
373
- channelOnly: true
374
- },
375
- "whatsapp-header-variables": {
376
- summary: "A WhatsApp header text takes more than one variable.",
377
- fix: "Keep one binding in the header and move the rest to the body.",
378
- channelOnly: true
379
- },
380
- "whatsapp-length": {
381
- summary: "A WhatsApp part exceeds Meta's length limit.",
382
- fix: "Shorten the text.",
383
- channelOnly: true
384
- },
385
- "whatsapp-button-text": {
386
- summary: "A WhatsApp button has no label or value between its tags.",
387
- fix: "Write the label or value as text between the tags.",
388
- channelOnly: true
389
- },
390
- "whatsapp-button-limit": {
391
- summary: "The conditions can produce more buttons of a kind than Meta allows.",
392
- fix: "Remove buttons, or split the template.",
393
- channelOnly: true
394
- }
395
- });
396
- /** Whether a diagnostic identifier selects an agent-driven format upgrade. */
397
- const hasUpgradeRecipe = (code) => Object.entries(DIAGNOSTIC_CODES).some(([identifier, spec]) => identifier === code && "upgrade" in spec);
398
- /** Families whose code carries a suffix chosen at run time. */
399
- const DIAGNOSTIC_CODE_FAMILIES = { "caniemail/": {
400
- summary: "A CSS property, HTML element or attribute that a target email client ignores or partly supports (from the caniemail.com data), reported as caniemail/<feature>.",
401
- fix: "Use the alternative the message names, such as an email component instead of display: flex."
402
- } };
403
- //#endregion
1
+ import { a as createDiagnosticFormatter, i as buildLineMap, n as DIAGNOSTIC_CODE_FAMILIES, o as formatDiagnostic, r as hasUpgradeRecipe, s as lineColumn, t as DIAGNOSTIC_CODES } from "./diagnostic-codes-BduqCv_h.js";
2
+ import { n as formatEmailDiagnostic, r as hasBlockingDiagnostic, t as EmailCompileError } from "./diagnostics-C0Tr8-eQ.js";
404
3
  export { DIAGNOSTIC_CODES, DIAGNOSTIC_CODE_FAMILIES, EmailCompileError, buildLineMap, createDiagnosticFormatter, formatDiagnostic, formatEmailDiagnostic, hasBlockingDiagnostic, hasUpgradeRecipe, lineColumn };
package/dist/edit.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { n as isObject } from "./guards-eV1qLbf2.js";
2
2
  import { d as completeJsxSourceLocation, u as JSX_SOURCE_LOCATION_PROP } from "./jsx-runtime-CBlexoeR.js";
3
- import { t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
4
- import { c as WHATSAPP_HEADER_TYPES, n as introducedProfileErrors, o as SMS_CATEGORIES, s as WHATSAPP_CATEGORIES, t as checkStaticProfile } from "./profile-PUfi3GPH.js";
3
+ import { i as buildLineMap } from "./diagnostic-codes-BduqCv_h.js";
4
+ import { c as WHATSAPP_HEADER_TYPES, n as introducedProfileErrors, o as SMS_CATEGORIES, s as WHATSAPP_CATEGORIES, t as checkStaticProfile } from "./profile-CWZ2kkt-.js";
5
5
  import { parse } from "@babel/parser";
6
6
  //#region src/source-edit.ts
7
7
  const node = (value) => isObject(value) && "type" in value && typeof value.type === "string" && "start" in value && typeof value.start === "number" && "end" in value && typeof value.end === "number";
@@ -1,5 +1,5 @@
1
1
  import { n as JsxSource } from "../source-locations-C8wCsiAy.js";
2
- import { a as JSX, i as Fragment, n as EmailComponent, r as EmailNode } from "../jsx-runtime-ylIOnaSf.js";
2
+ import { a as JSX, i as Fragment, n as EmailComponent, r as EmailNode } from "../jsx-runtime-DC7xG0aA.js";
3
3
  //#region src/email/jsx-dev-runtime.d.ts
4
4
  /** Development JSX retains compiler-provided origins directly on the semantic tree. */
5
5
  declare const jsxDEV: (type: string | EmailComponent<Record<string, unknown>> | typeof Fragment, props: Record<string, unknown> | null, _key?: unknown, _staticChildren?: boolean, source?: JsxSource) => EmailNode;
@@ -1,2 +1,2 @@
1
- import { a as JSX, c as jsx, d as rawMarkup, f as reportEmailDiagnostic, i as Fragment, l as jsxs, n as EmailComponent, o as currentEmailOrigins, r as EmailNode, s as drainEmailDiagnostics, t as EmailChild, u as normalizeChildren } from "../jsx-runtime-ylIOnaSf.js";
1
+ import { a as JSX, c as jsx, d as rawMarkup, f as reportEmailDiagnostic, i as Fragment, l as jsxs, n as EmailComponent, o as currentEmailOrigins, r as EmailNode, s as drainEmailDiagnostics, t as EmailChild, u as normalizeChildren } from "../jsx-runtime-DC7xG0aA.js";
2
2
  export { EmailChild, EmailComponent, EmailNode, Fragment, JSX, currentEmailOrigins, drainEmailDiagnostics, jsx, jsxs, normalizeChildren, rawMarkup, reportEmailDiagnostic };
package/dist/email.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { b as EmailStyle, r as EmailNode, t as EmailChild } from "./jsx-runtime-ylIOnaSf.js";
1
+ import { b as EmailStyle, r as EmailNode, t as EmailChild } from "./jsx-runtime-DC7xG0aA.js";
2
2
  //#region src/email/components.d.ts
3
3
  /** Marks the hidden preview block so the renderer can read it back and the text lane can keep it. */
4
4
  declare const PREHEADER_ATTRIBUTE = "data-samva-preheader";
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { i as TEMPLATE_ID_PATTERN, o as TemplateInput, r as SmlTemplate, s as defineTemplate, t as EmailChannel } from "./template-DMPwxMSq.js";
1
+ import { i as TEMPLATE_ID_PATTERN, o as TemplateInput, r as SmlTemplate, s as defineTemplate, t as EmailChannel } from "./template-B-UDXK8a.js";
2
2
  export { type EmailChannel, type SmlTemplate, TEMPLATE_ID_PATTERN, type TemplateInput, defineTemplate };
@@ -1,5 +1,5 @@
1
1
  import { r as JsxSourceLocation } from "./source-locations-C8wCsiAy.js";
2
- import { r as EmailDiagnostic } from "./diagnostics-B-Gym34R.js";
2
+ import { r as EmailDiagnostic } from "./diagnostics-CUYlQ_6x.js";
3
3
  //#region src/email/elements.d.ts
4
4
  /** How one attribute is spelled and what values it accepts. */
5
5
  interface EmailAttributeSpec {
@@ -6,7 +6,7 @@ import { FORMATTER_NAMES } from "./fmt.js";
6
6
  import { i as drainEmailDiagnostics, t as EmailNode } from "./jsx-runtime-CBlexoeR.js";
7
7
  import { a as Email, d as equalColumnWidth, i as Divider, l as Section, n as Column, o as Image, r as Columns, s as Link, t as Button, u as Spacer } from "./components-Bl6DT21n.js";
8
8
  import { n as applyStylesheet } from "./ir-De6pTH51.js";
9
- import { i as lineColumn, t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
9
+ import { i as buildLineMap, s as lineColumn, t as DIAGNOSTIC_CODES } from "./diagnostic-codes-BduqCv_h.js";
10
10
  import { l as isAssetPath, n as brandComponents } from "./brand-DTctcDmf.js";
11
11
  import { parse } from "css-tree";
12
12
  import { parse as parse$1 } from "@babel/parser";
@@ -122,7 +122,7 @@ var Findings = class {
122
122
  };
123
123
  }
124
124
  add(source, node, code, message, fix, severity = "error") {
125
- this.items.push({
125
+ this.report({
126
126
  code,
127
127
  severity,
128
128
  message,
@@ -130,6 +130,14 @@ var Findings = class {
130
130
  origins: [this.locate(source, node)]
131
131
  });
132
132
  }
133
+ /** Attach the registry recipe wherever a known template finding enters the collection. */
134
+ report(diagnostic) {
135
+ const spec = DIAGNOSTIC_CODES[diagnostic.code];
136
+ this.items.push({
137
+ ...diagnostic,
138
+ ...spec.upgrade === void 0 ? {} : { upgrade: spec.upgrade }
139
+ });
140
+ }
133
141
  get blocking() {
134
142
  return this.items.some((item) => item.severity === "error");
135
143
  }
@@ -3457,7 +3465,7 @@ const lowerProject = (options, project, findings, inputs) => {
3457
3465
  ...head,
3458
3466
  children: [...head.children ?? [], ...additions]
3459
3467
  }));
3460
- if (!edited.done) findings.items.push({
3468
+ if (!edited.done) findings.report({
3461
3469
  code: "missing-head",
3462
3470
  severity: "error",
3463
3471
  message: "Styles that cannot be inlined — media queries, dark-scheme rules and pseudo-selectors — need a <head> to live in.",
@@ -3496,7 +3504,7 @@ const lowerProject = (options, project, findings, inputs) => {
3496
3504
  for (const finding of checks) {
3497
3505
  const source = finding.src;
3498
3506
  const file = sources[source?.[2] ?? 0] ?? options.entry;
3499
- findings.items.push({
3507
+ findings.report({
3500
3508
  code: finding.code,
3501
3509
  severity: "error",
3502
3510
  message: finding.message,
@@ -2,7 +2,7 @@ import { S as emailElement, o as serializeEmailHtml } from "./html-DjPxwjLi.js";
2
2
  import { i as drainEmailDiagnostics, t as EmailNode } from "./jsx-runtime-CBlexoeR.js";
3
3
  import { f as preheaderNode } from "./components-Bl6DT21n.js";
4
4
  import { n as applyStylesheet } from "./ir-De6pTH51.js";
5
- import { r as hasBlockingDiagnostic, t as EmailCompileError } from "./diagnostics-CnKFefKz.js";
5
+ import { r as hasBlockingDiagnostic, t as EmailCompileError } from "./diagnostics-C0Tr8-eQ.js";
6
6
  //#region src/email/text.ts
7
7
  const PARAGRAPH = "\n\n";
8
8
  const LINE = "\n";
@@ -1,8 +1,8 @@
1
1
  import { t as BrandPlugin } from "./brand-plugin-CHhcei-j.js";
2
2
  import { r as JsxSourceLocation } from "./source-locations-C8wCsiAy.js";
3
- import { r as EmailDiagnostic } from "./diagnostics-B-Gym34R.js";
4
- import { r as EmailNode } from "./jsx-runtime-ylIOnaSf.js";
5
- import { n as EmailContent } from "./template-DMPwxMSq.js";
3
+ import { r as EmailDiagnostic } from "./diagnostics-CUYlQ_6x.js";
4
+ import { r as EmailNode } from "./jsx-runtime-DC7xG0aA.js";
5
+ import { n as EmailContent } from "./template-B-UDXK8a.js";
6
6
  import "css-tree";
7
7
  //#region src/ir.d.ts
8
8
  /** The IR version this package writes. A publication pins the version it was built with. */
package/dist/render.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as isUrlAttribute, S as emailElement, _ as EmailGlobalProps, b as EmailStyle, g as EmailElementSpec, h as EmailAttributeSpec, m as EMAIL_GLOBAL_ATTRIBUTES, p as EMAIL_ELEMENTS, r as EmailNode, v as EmailIntrinsicElements, x as EmailTag, y as EmailLayout } from "./jsx-runtime-ylIOnaSf.js";
2
- import { $ as IrWhatsApp, A as emailAuthoringTarget, B as IrElement, D as EmittedPosition, E as EmailElementSelection, F as emailSelections, G as IrPredicate, H as IrFormatter, I as renderIrPreview, J as IrRawPart, K as IrPreheader, L as IrArithmetic, M as emailSelectionAncestry, N as emailSelectionIndex, O as INSTANCE_PATH_ATTRIBUTE, P as emailSelectionSiblings, Q as IrValue, R as IrComparison, U as IrIf, V as IrEmail, W as IrNode, X as IrSource, Y as IrSms, Z as IrStyle, a as renderEmailContent, at as RenderedPosition, c as checkAttribute, ct as TemplateIr, d as escapeText, et as IrWhatsAppButton, f as kebab, i as compileEmail, it as RenderedIrWhatsApp, j as emailDefinitionInstances, k as IrPreview, l as declarationValue, m as serializeStyle, n as EmailCompileOutput, nt as RenderOptions, o as EmailHtmlOutput, ot as RenderedWhatsAppButton, p as serializeEmailHtml, q as IrRaw, r as RenderedEmail, rt as RenderedIrEmail, s as SerializeEmailOptions, st as SML_IR_VERSION, t as EMAIL_DOCTYPE, tt as IrWhatsAppButtonNode, u as escapeAttribute, z as IrEach } from "./render-DQKz4Aco.js";
1
+ import { C as isUrlAttribute, S as emailElement, _ as EmailGlobalProps, b as EmailStyle, g as EmailElementSpec, h as EmailAttributeSpec, m as EMAIL_GLOBAL_ATTRIBUTES, p as EMAIL_ELEMENTS, r as EmailNode, v as EmailIntrinsicElements, x as EmailTag, y as EmailLayout } from "./jsx-runtime-DC7xG0aA.js";
2
+ import { $ as IrWhatsApp, A as emailAuthoringTarget, B as IrElement, D as EmittedPosition, E as EmailElementSelection, F as emailSelections, G as IrPredicate, H as IrFormatter, I as renderIrPreview, J as IrRawPart, K as IrPreheader, L as IrArithmetic, M as emailSelectionAncestry, N as emailSelectionIndex, O as INSTANCE_PATH_ATTRIBUTE, P as emailSelectionSiblings, Q as IrValue, R as IrComparison, U as IrIf, V as IrEmail, W as IrNode, X as IrSource, Y as IrSms, Z as IrStyle, a as renderEmailContent, at as RenderedPosition, c as checkAttribute, ct as TemplateIr, d as escapeText, et as IrWhatsAppButton, f as kebab, i as compileEmail, it as RenderedIrWhatsApp, j as emailDefinitionInstances, k as IrPreview, l as declarationValue, m as serializeStyle, n as EmailCompileOutput, nt as RenderOptions, o as EmailHtmlOutput, ot as RenderedWhatsAppButton, p as serializeEmailHtml, q as IrRaw, r as RenderedEmail, rt as RenderedIrEmail, s as SerializeEmailOptions, st as SML_IR_VERSION, t as EMAIL_DOCTYPE, tt as IrWhatsAppButtonNode, u as escapeAttribute, z as IrEach } from "./render-C28ucTh8.js";
3
3
  //#region src/render-ir.d.ts
4
4
  interface IrRenderDiagnostic {
5
5
  readonly code: string;
package/dist/render.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { C as isUrlAttribute, S as emailElement, _ as renderIr, a as kebab, b as EMAIL_ELEMENTS, c as INSTANCE_PATH_ATTRIBUTE, d as emailSelectionAncestry, f as emailSelectionIndex, g as IrRenderError, h as renderIrPreview, i as escapeText, l as emailAuthoringTarget, m as emailSelections, n as declarationValue, o as serializeEmailHtml, p as emailSelectionSiblings, r as escapeAttribute, s as serializeStyle, t as checkAttribute, u as emailDefinitionInstances, v as renderIrSms, x as EMAIL_GLOBAL_ATTRIBUTES, y as renderIrWhatsApp } from "./html-DjPxwjLi.js";
2
2
  import { t as SML_IR_VERSION } from "./ir-De6pTH51.js";
3
- import { i as deriveEmailText, n as compileEmail, r as renderEmailContent, t as EMAIL_DOCTYPE } from "./render-D2ht57ZZ.js";
3
+ import { i as deriveEmailText, n as compileEmail, r as renderEmailContent, t as EMAIL_DOCTYPE } from "./render-BVW-ESn-.js";
4
4
  //#region src/sms-segments.ts
5
5
  const GSM7_EXTENDED = /* @__PURE__ */ new Set([
6
6
  "[",
package/dist/sms.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { r as EmailNode, t as EmailChild } from "./jsx-runtime-ylIOnaSf.js";
1
+ import { r as EmailNode, t as EmailChild } from "./jsx-runtime-DC7xG0aA.js";
2
2
  import { t as SmsCategory } from "./channel-specs-C2feJwuP.js";
3
3
  //#region src/sms.d.ts
4
4
  interface SmsProps {
@@ -1,4 +1,4 @@
1
- import { r as EmailNode } from "./jsx-runtime-ylIOnaSf.js";
1
+ import { r as EmailNode } from "./jsx-runtime-DC7xG0aA.js";
2
2
  import { r as InputSchema } from "./input-schema-7dPFVvNE.js";
3
3
  import { StandardJSONSchemaV1 } from "@standard-schema/spec";
4
4
  //#region src/template.d.ts
@@ -1,4 +1,4 @@
1
- import { r as EmailNode, t as EmailChild } from "./jsx-runtime-ylIOnaSf.js";
1
+ import { r as EmailNode, t as EmailChild } from "./jsx-runtime-DC7xG0aA.js";
2
2
  import { n as WhatsAppCategory, r as WhatsAppHeaderType } from "./channel-specs-C2feJwuP.js";
3
3
  //#region src/whatsapp.d.ts
4
4
  interface WhatsAppProps {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samva/markup",
3
- "version": "0.11.3",
3
+ "version": "0.12.0",
4
4
  "description": "Type-safe authoring and compilation primitives for Samva templates",
5
5
  "keywords": [
6
6
  "email",
@@ -1,36 +0,0 @@
1
- //#region src/diagnostic-model.ts
2
- /** Newline-offset line map. Build once per source, query with `lineColumn`. */
3
- const buildLineMap = (source) => {
4
- const lineStarts = [0];
5
- for (let index = 0; index < source.length; index++) {
6
- const code = source.charCodeAt(index);
7
- if (code === 10 || code === 13 && source.charCodeAt(index + 1) !== 10 || code === 8232 || code === 8233) lineStarts.push(index + 1);
8
- }
9
- return lineStarts;
10
- };
11
- /** 1-based line/column for an absolute offset, via binary search. */
12
- const lineColumn = (lineMap, offset) => {
13
- let low = 0;
14
- let high = lineMap.length - 1;
15
- while (low < high) {
16
- const middle = low + high + 1 >> 1;
17
- if (lineMap[middle] <= offset) low = middle;
18
- else high = middle - 1;
19
- }
20
- return {
21
- line: low + 1,
22
- column: offset - lineMap[low] + 1
23
- };
24
- };
25
- /** Build one formatter for a batch of diagnostics against the same source. */
26
- const createDiagnosticFormatter = (source) => {
27
- const lineMap = buildLineMap(source);
28
- return (diagnostic) => {
29
- const { line, column } = lineColumn(lineMap, diagnostic.span.start);
30
- return `line ${line}:${column} ${diagnostic.message}`;
31
- };
32
- };
33
- /** "line 12:8 unknown class ..." — the shape agents iterate on. */
34
- const formatDiagnostic = (source, diagnostic) => createDiagnosticFormatter(source)(diagnostic);
35
- //#endregion
36
- export { lineColumn as i, createDiagnosticFormatter as n, formatDiagnostic as r, buildLineMap as t };