@samva/markup 0.1.2 → 0.3.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.
Files changed (93) hide show
  1. package/AGENTS.md +25 -5
  2. package/README.md +92 -26
  3. package/dist/{ast-DAhnAvQN.d.ts → ast-63I-VLQx.d.ts} +15 -2
  4. package/dist/ast.d.ts +2 -2
  5. package/dist/ast.js +14 -1
  6. package/dist/{checks-D-zFQt5R.js → checks-DXuERO4B.js} +36 -2
  7. package/dist/checks.d.ts +8 -1
  8. package/dist/checks.js +2 -2
  9. package/dist/compile-sms.d.ts +2 -2
  10. package/dist/compile-whatsapp.d.ts +2 -2
  11. package/dist/compile.d.ts +9 -84
  12. package/dist/compile.js +11 -920
  13. package/dist/compiler.d.ts +303 -0
  14. package/dist/compiler.js +1390 -0
  15. package/dist/components.d.ts +3 -3
  16. package/dist/corpus-qrUD5O6r.d.ts +254 -0
  17. package/dist/css-ar2lU3NC.d.ts +84 -0
  18. package/dist/diagnostics-Djd9ckLg.d.ts +90 -0
  19. package/dist/diagnostics.js +4 -1
  20. package/dist/elements-DtwKlFVH.d.ts +820 -0
  21. package/dist/email/components.d.ts +125 -0
  22. package/dist/email/components.js +210 -0
  23. package/dist/email/diagnostics.d.ts +2 -0
  24. package/dist/email/diagnostics.js +56 -0
  25. package/dist/email/elements.d.ts +2 -0
  26. package/dist/email/elements.js +299 -0
  27. package/dist/email/html.d.ts +42 -0
  28. package/dist/email/html.js +266 -0
  29. package/dist/email/jsx-dev-runtime.d.ts +26 -0
  30. package/dist/email/jsx-dev-runtime.js +12 -0
  31. package/dist/email/jsx-runtime.d.ts +2 -0
  32. package/dist/email/jsx-runtime.js +60 -0
  33. package/dist/email/render.d.ts +43 -0
  34. package/dist/email/render.js +6 -0
  35. package/dist/email/selection.d.ts +102 -0
  36. package/dist/email/selection.js +143 -0
  37. package/dist/email/source-edit.d.ts +67 -0
  38. package/dist/email/source-edit.js +185 -0
  39. package/dist/email/stylesheets.d.ts +15 -0
  40. package/dist/email/stylesheets.js +45 -0
  41. package/dist/email/text.d.ts +7 -0
  42. package/dist/email/text.js +100 -0
  43. package/dist/emit.d.ts +13 -12
  44. package/dist/emit.js +63 -63
  45. package/dist/input-schema-BIfzI63B.d.ts +23 -0
  46. package/dist/input-schema.d.ts +2 -0
  47. package/dist/input-schema.js +170 -0
  48. package/dist/jsx-dev-runtime.d.ts +4 -3
  49. package/dist/jsx-dev-runtime.js +9 -1
  50. package/dist/{jsx-runtime-eNIMY87F.d.ts → jsx-runtime-C5k_Zq0A.d.ts} +3 -3
  51. package/dist/jsx-runtime.d.ts +1 -1
  52. package/dist/jsx-runtime.js +20 -1
  53. package/dist/model.d.ts +1 -1
  54. package/dist/ops.d.ts +1 -1
  55. package/dist/ops.js +1 -1
  56. package/dist/{parser-DkQKIjDa.d.ts → parser-CGBS73Gz.d.ts} +2 -2
  57. package/dist/parser.d.ts +1 -1
  58. package/dist/presets/category-mapping.d.ts +1 -1
  59. package/dist/presets/corpus.d.ts +2 -0
  60. package/dist/presets/corpus.js +247 -0
  61. package/dist/presets/public-gallery.d.ts +290 -2
  62. package/dist/presets/public-gallery.js +428 -6
  63. package/dist/presets/public-handoff.d.ts +164 -2
  64. package/dist/presets/public-handoff.js +229 -61
  65. package/dist/presets/public-preview-artifacts.d.ts +2 -2
  66. package/dist/presets/public-preview-artifacts.js +25 -31
  67. package/dist/presets/public-preview.d.ts +65 -2
  68. package/dist/presets/public-preview.js +402 -1152
  69. package/dist/presets/sources.d.ts +35 -4
  70. package/dist/presets/sources.js +2328 -43
  71. package/dist/presets.d.ts +39 -5
  72. package/dist/presets.js +96 -0
  73. package/dist/printer.d.ts +1 -1
  74. package/dist/render-template.d.ts +5 -2
  75. package/dist/render-template.js +3 -1
  76. package/dist/render-ykQhtMs_.js +592 -0
  77. package/dist/source-locations-BCJQnIfU.d.ts +89 -0
  78. package/dist/source-locations.d.ts +2 -0
  79. package/dist/source-locations.js +125 -0
  80. package/dist/template-Dy5yOPyp.d.ts +60 -0
  81. package/dist/template.d.ts +2 -0
  82. package/dist/template.js +37 -0
  83. package/dist/tsx-source-instrumentation.d.ts +23 -0
  84. package/dist/tsx-source-instrumentation.js +97 -0
  85. package/dist/tsx-tracer.d.ts +116 -0
  86. package/dist/tsx-tracer.js +985 -0
  87. package/generated/sml-agent-contract.json +574 -782
  88. package/generated/sml-email-agent-contract.json +502 -677
  89. package/generated/sml-email-language-reference.md +94 -160
  90. package/generated/sml-language-reference.md +195 -220
  91. package/package.json +111 -9
  92. package/dist/public-gallery-D0Zv0pZE.d.ts +0 -611
  93. package/dist/public-preview-u1Z9aaGq.d.ts +0 -1168
@@ -1,26 +1,173 @@
1
- import { collectVariableReferences } from "../ast.js";
2
- import { parseSml } from "../parser.js";
3
- import { resolveTokenTable } from "../theme.js";
4
- import { baseTokens } from "../tokens.js";
5
- import { compileSource } from "../compile.js";
6
- import { RECOMMENDED_THEME, presetCatalog } from "../presets.js";
1
+ import { EMAIL_ELEMENTS } from "../email/elements.js";
2
+ import { presetCatalog } from "../presets.js";
7
3
  import { publicTemplates, templateCollections } from "./public-gallery.js";
8
- import { Result } from "effect";
4
+ import { Predicate, Result } from "effect";
5
+ import { parse } from "@babel/parser";
9
6
  //#region presets/public-handoff.ts
10
7
  const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
11
8
  const ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
12
9
  const MAX_HTTPS_ASSET_LENGTH = 2048;
13
10
  const MAX_DATA_IMAGE_ASSET_LENGTH = 32768;
14
11
  const DATA_IMAGE = /^data:image\/(?:avif|gif|jpeg|png|svg\+xml|webp)(?:;charset=[a-z0-9-]+)?(?:;base64)?,[^\r\n]+$/i;
15
- const isTemplateReferenceProvenanceSource = (value) => value === "sanitized-synthetic" || value === "reviewed-public";
16
- const PUBLIC_TEMPLATE_FONT_TOKENS = new Set(Object.keys(resolveTokenTable({
17
- base: baseTokens,
18
- overrides: [RECOMMENDED_THEME]
19
- }).fontFamilies));
20
- const FONT_WEIGHT_TOKENS = new Set(Object.keys(resolveTokenTable({
21
- base: baseTokens,
22
- overrides: [RECOMMENDED_THEME]
23
- }).fontWeights));
12
+ const PUBLIC_TEMPLATE_ASSET_PATH = /^\/templates\/assets\/[A-Za-z0-9][A-Za-z0-9._-]*\.(?:jpe?g|png|webp)$/;
13
+ const BLOCKED_PUBLIC_ASSET = /(?:unsplash|placehold|assets\.example)/i;
14
+ /** A sample value is non-empty when every string in it is, and no list or object is empty. */
15
+ const nonEmptySample = (value) => {
16
+ if (typeof value === "string") return nonEmpty(value);
17
+ if (Array.isArray(value)) return value.length > 0 && value.every(nonEmptySample);
18
+ if (Predicate.isObject(value)) {
19
+ const values = Object.values(value);
20
+ return values.length > 0 && values.every(nonEmptySample);
21
+ }
22
+ return true;
23
+ };
24
+ const isTemplateReferenceProvenanceSource = (value) => value === "samva-owned" || value === "sanitized-synthetic" || value === "reviewed-public" || value === "brand-recreation";
25
+ /** Bound authored source before parsing or mounting it. */
26
+ const PUBLIC_TEMPLATE_SOURCE_MAX_CHARACTERS = 512e3;
27
+ /** Bounds apply to decoded bytes, before evaluation or disk emission. */
28
+ const PUBLIC_TEMPLATE_PROJECT_MAX_BYTES = 8388608;
29
+ const PUBLIC_TEMPLATE_PROJECT_FILE_MAX_BYTES = 2097152;
30
+ const PUBLIC_TEMPLATE_PROJECT_MAX_FILES = 64;
31
+ const isSafePublicTemplateProjectPath = (path) => path.length <= 240 && path.split("/").every((part) => /^[A-Za-z0-9_][A-Za-z0-9._-]*$/.test(part) && part !== "." && part !== ".." && part !== "node_modules");
32
+ /** Reject traversal, collisions, malformed base64 and oversized authored projects. */
33
+ const isSafePublicTemplateProject = (project) => {
34
+ if (!isSafePublicTemplateProjectPath(project.entryPath) || !project.entryPath.endsWith(".tsx") || !/^[a-f0-9]{64}$/.test(project.digest) || project.files.length === 0 || project.files.length > 64) return false;
35
+ const paths = /* @__PURE__ */ new Set();
36
+ let total = 0;
37
+ for (const file of project.files) {
38
+ if (!isSafePublicTemplateProjectPath(file.path) || paths.has(file.path.toLowerCase())) return false;
39
+ paths.add(file.path.toLowerCase());
40
+ let bytes;
41
+ if (file.encoding === "utf8") {
42
+ if (file.content.length > 512e3) return false;
43
+ bytes = new TextEncoder().encode(file.content).length;
44
+ } else if (file.encoding === "base64") {
45
+ if (file.content.length > Math.ceil(2097152 / 3) * 4 || file.content.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(file.content)) return false;
46
+ if (btoa(atob(file.content)) !== file.content) return false;
47
+ bytes = file.content.length / 4 * 3 - (file.content.endsWith("==") ? 2 : file.content.endsWith("=") ? 1 : 0);
48
+ } else return false;
49
+ if (bytes > 2097152) return false;
50
+ total += bytes;
51
+ if (total > 8388608) return false;
52
+ }
53
+ for (const path of paths) if (path.split("/").slice(0, -1).some((_, index, parts) => paths.has(parts.slice(0, index + 1).join("/")))) return false;
54
+ return project.files.some((file) => file.path === project.entryPath && file.encoding === "utf8");
55
+ };
56
+ /** Digest the complete authored graph independently of caller file ordering. */
57
+ const publicTemplateProjectDigest = async (project) => {
58
+ const serialized = JSON.stringify({
59
+ entryPath: project.entryPath,
60
+ files: [...project.files].sort((a, b) => a.path.localeCompare(b.path)).map(({ path, content, encoding }) => ({
61
+ path,
62
+ content,
63
+ encoding
64
+ }))
65
+ });
66
+ const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(serialized)));
67
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
68
+ };
69
+ const isValidTemplateBrandRecreation = (brandRecreation, project) => brandRecreation !== void 0 && project !== void 0 && brandRecreation.brand.trim().length > 0 && brandRecreation.subject.trim().length > 0;
70
+ const PUBLIC_TEMPLATE_FONT_TOKENS = /* @__PURE__ */ new Set([
71
+ "sans",
72
+ "serif",
73
+ "mono"
74
+ ]);
75
+ const FONT_WEIGHT_TOKENS = /* @__PURE__ */ new Set([
76
+ "thin",
77
+ "extralight",
78
+ "light",
79
+ "normal",
80
+ "medium",
81
+ "semibold",
82
+ "bold",
83
+ "extrabold",
84
+ "black"
85
+ ]);
86
+ const parsePublicTemplateSource = (source) => {
87
+ if (source.length === 0 || source.length > 512e3) return void 0;
88
+ try {
89
+ const parsed = parse(source, {
90
+ sourceType: "module",
91
+ plugins: ["typescript", "jsx"]
92
+ });
93
+ return parsed.program.body.some((node) => node.type === "ExportDefaultDeclaration") ? parsed : void 0;
94
+ } catch {
95
+ return;
96
+ }
97
+ };
98
+ const PUBLIC_COMPONENT_TAGS = /* @__PURE__ */ new Set([
99
+ ...Object.keys(EMAIL_ELEMENTS),
100
+ "Email",
101
+ "Section",
102
+ "Columns",
103
+ "Column",
104
+ "Button",
105
+ "Spacer",
106
+ "Divider",
107
+ "Image",
108
+ "Link"
109
+ ]);
110
+ /** Only generic element names and literal class colors survive deidentification. */
111
+ const publicTemplateAuthoredStructure = (source) => {
112
+ const parsed = parsePublicTemplateSource(source);
113
+ if (parsed === void 0) return void 0;
114
+ const nodes = [];
115
+ const counts = /* @__PURE__ */ new Map();
116
+ const palette = /* @__PURE__ */ new Set();
117
+ const siblings = /* @__PURE__ */ new Map();
118
+ const walk = (value, depth) => {
119
+ if (Array.isArray(value)) {
120
+ for (const item of value) walk(item, depth);
121
+ return;
122
+ }
123
+ if (!Predicate.isObject(value)) return;
124
+ let childDepth = depth;
125
+ if (value.type === "JSXElement") {
126
+ const opening = value.openingElement;
127
+ if (!Predicate.isObject(opening)) return;
128
+ const name = opening.name;
129
+ const tag = Predicate.isObject(name) && typeof name.name === "string" && PUBLIC_COMPONENT_TAGS.has(name.name) ? name.name : "Component";
130
+ const childIndex = siblings.get(depth) ?? 0;
131
+ siblings.set(depth, childIndex + 1);
132
+ siblings.set(depth + 1, 0);
133
+ if (nodes.length < 200) nodes.push({
134
+ tag,
135
+ depth,
136
+ childIndex
137
+ });
138
+ counts.set(tag, (counts.get(tag) ?? 0) + 1);
139
+ if (Array.isArray(opening.attributes)) for (const attribute of opening.attributes) {
140
+ if (!Predicate.isObject(attribute) || !Predicate.isObject(attribute.name) || attribute.name.name !== "className") continue;
141
+ const literal = attribute.value;
142
+ if (!Predicate.isObject(literal) || literal.type !== "StringLiteral" || typeof literal.value !== "string") continue;
143
+ for (const match of literal.value.matchAll(/#([a-f0-9]{8}|[a-f0-9]{6}|[a-f0-9]{3})(?=[^a-f0-9]|$)/gi)) if (palette.size < 24) palette.add(`#${match[1].toLowerCase()}`);
144
+ }
145
+ childDepth += 1;
146
+ }
147
+ for (const [key, child] of Object.entries(value)) {
148
+ if ([
149
+ "loc",
150
+ "comments",
151
+ "leadingComments",
152
+ "trailingComments",
153
+ "innerComments",
154
+ "extra",
155
+ "tokens"
156
+ ].includes(key)) continue;
157
+ walk(child, childDepth);
158
+ }
159
+ };
160
+ walk(parsed.program, 0);
161
+ return {
162
+ basis: "authored-jsx",
163
+ nodes,
164
+ components: [...counts].sort(([a], [b]) => a.localeCompare(b)).slice(0, 64).map(([tag, count]) => ({
165
+ tag,
166
+ count
167
+ })),
168
+ palette: [...palette].sort()
169
+ };
170
+ };
24
171
  const sortedUnique = (values) => [...new Set(values)].sort();
25
172
  const sameStrings = (left, right) => JSON.stringify(sortedUnique(left)) === JSON.stringify(sortedUnique(right));
26
173
  const nonEmpty = (value) => value.trim().length > 0;
@@ -30,22 +177,28 @@ const issue = (artifactId, code, message) => ({
30
177
  message
31
178
  });
32
179
  /**
33
- * Public imports may only refer to existing font-family tokens. A source may
180
+ * Public imports may only refer to the default email Tailwind font-family tokens. A source may
34
181
  * still use the supported `font-*` weight utilities; this check deliberately
35
182
  * rejects a new family token instead of creating a corpus font-hosting path.
36
183
  */
37
184
  const unsupportedPublicTemplateFontTokens = (source) => {
38
185
  const unsupported = /* @__PURE__ */ new Set();
39
- for (const match of source.matchAll(/\bclass\s*=\s*(["'])(.*?)\1/gs)) for (const className of (match[2] ?? "").split(/\s+/)) {
186
+ for (const match of source.matchAll(/\bclassName\s*=\s*(["'])(.*?)\1/gs)) for (const className of (match[2] ?? "").split(/\s+/)) {
40
187
  if (!className.startsWith("font-")) continue;
41
188
  const token = className.slice(5);
42
189
  if (!PUBLIC_TEMPLATE_FONT_TOKENS.has(token) && !FONT_WEIGHT_TOKENS.has(token)) unsupported.add(token);
43
190
  }
44
191
  return [...unsupported].sort();
45
192
  };
46
- /** Accept only bounded HTTPS URLs or the preview's self-contained image data. */
193
+ /**
194
+ * Accept bounded HTTPS URLs, same-origin gallery files under `/templates/assets/`,
195
+ * or the preview's self-contained image data. Live Unsplash, placehold, and
196
+ * assets.example hosts are never safe for public sample data.
197
+ */
47
198
  const isSafePublicTemplateAsset = (value) => {
199
+ if (value.includes("..") || BLOCKED_PUBLIC_ASSET.test(value)) return false;
48
200
  if (value.startsWith("data:")) return value.length <= MAX_DATA_IMAGE_ASSET_LENGTH && DATA_IMAGE.test(value);
201
+ if (PUBLIC_TEMPLATE_ASSET_PATH.test(value)) return value.length <= MAX_HTTPS_ASSET_LENGTH;
49
202
  const url = URL.parse(value);
50
203
  return value.length <= MAX_HTTPS_ASSET_LENGTH && url?.protocol === "https:" && url.hostname.length > 0 && url.username.length === 0 && url.password.length === 0;
51
204
  };
@@ -59,7 +212,7 @@ const toTemplateReferencePacket = (artifact) => ({
59
212
  },
60
213
  preview: artifact.preview,
61
214
  publicMetadata: artifact.publicMetadata,
62
- ...artifact.provenance === void 0 ? {} : { provenance: artifact.provenance }
215
+ provenance: artifact.provenance
63
216
  });
64
217
  const jsonProjection = (kind, value) => ({
65
218
  kind,
@@ -67,38 +220,54 @@ const jsonProjection = (kind, value) => ({
67
220
  contents: `${JSON.stringify(value, null, 2)}\n`
68
221
  });
69
222
  /**
70
- * Emits the five destination projections as deterministic bytes. The importer
223
+ * Emits legacy projections plus the complete project as deterministic bytes. The importer
71
224
  * owns no checked-in destination files; callers choose an explicit output dir.
72
225
  */
73
- const emitTemplateReferencePacket = (packet) => [
74
- {
75
- kind: "source",
76
- relativePath: "preset.samva",
77
- contents: packet.preset.source
78
- },
79
- jsonProjection("catalog", {
80
- id: packet.preset.id,
81
- channel: packet.preset.channel,
82
- category: packet.preset.category,
83
- name: packet.preset.name,
84
- description: packet.preset.description,
85
- variables: packet.preset.variables
86
- }),
87
- jsonProjection("gallery", {
88
- presetId: packet.preset.id,
89
- ...packet.gallery,
90
- ...packet.provenance === void 0 ? {} : { provenance: packet.provenance }
91
- }),
92
- jsonProjection("preview", packet.preview),
93
- jsonProjection("editorial", {
94
- cardTitle: packet.publicMetadata.cardTitle,
95
- cardDescription: packet.publicMetadata.cardDescription,
96
- guidance: packet.publicMetadata.guidance,
97
- variables: packet.publicMetadata.variableDescriptions,
98
- seo: packet.publicMetadata.seo
99
- })
100
- ];
226
+ const emitTemplateReferencePacket = (packet) => {
227
+ const project = packet.preset.project;
228
+ if (project !== void 0 && (!isSafePublicTemplateProject(project) || project.files.find((file) => file.path === project.entryPath)?.content !== packet.preset.source)) throw new Error("Invalid public template project");
229
+ if (packet.provenance.source === "brand-recreation" && !isValidTemplateBrandRecreation(packet.provenance.brandRecreation, project)) throw new Error("Invalid public template brand recreation attribution");
230
+ return [
231
+ ...project === void 0 ? [] : [jsonProjection("project", {
232
+ ...project,
233
+ files: [...project.files].sort((a, b) => a.path.localeCompare(b.path))
234
+ }), ...[...project.files].sort((a, b) => a.path.localeCompare(b.path)).map((file) => ({
235
+ kind: "project-file",
236
+ relativePath: `project/${file.path}`,
237
+ contents: file.content,
238
+ encoding: file.encoding
239
+ }))],
240
+ {
241
+ kind: "source",
242
+ relativePath: "preset.tsx",
243
+ contents: packet.preset.source
244
+ },
245
+ jsonProjection("catalog", {
246
+ id: packet.preset.id,
247
+ channel: packet.preset.channel,
248
+ category: packet.preset.category,
249
+ name: packet.preset.name,
250
+ description: packet.preset.description,
251
+ variables: packet.preset.variables
252
+ }),
253
+ jsonProjection("gallery", {
254
+ presetId: packet.preset.id,
255
+ ...packet.gallery,
256
+ provenance: packet.provenance
257
+ }),
258
+ jsonProjection("preview", packet.preview),
259
+ jsonProjection("editorial", {
260
+ cardTitle: packet.publicMetadata.cardTitle,
261
+ cardDescription: packet.publicMetadata.cardDescription,
262
+ guidance: packet.publicMetadata.guidance,
263
+ variables: packet.publicMetadata.variableDescriptions,
264
+ seo: packet.publicMetadata.seo
265
+ })
266
+ ];
267
+ };
101
268
  /**
269
+ * Validates metadata and TSX syntax, never executing the template. The Vite importer
270
+ * verifies schema, fixtures and rendered content before writing projections.
102
271
  * Validates explicitly supplied, owner-approved artifacts without filesystem,
103
272
  * network, persistence, or discovery. Successful output is a projection for
104
273
  * the existing preset, public-gallery, preview, and editorial contracts; this
@@ -115,31 +284,29 @@ const validatePublicTemplateHandoff = (artifacts) => {
115
284
  for (const artifact of artifacts) {
116
285
  const id = artifact.preset.id;
117
286
  if (artifact.approval.state !== "approved" || !nonEmpty(artifact.approval.approvedBy) || !ISO_TIMESTAMP.test(artifact.approval.approvedAt)) issues.push(issue(id, "approval", "owner approval and an ISO-8601 approval timestamp are required"));
118
- if (artifact.provenance !== void 0 && (!isTemplateReferenceProvenanceSource(artifact.provenance.source) || !nonEmpty(artifact.provenance.referenceId) || !ISO_TIMESTAMP.test(artifact.provenance.recordedAt))) issues.push(issue(id, "provenance", "provenance requires a supported source, identifier, and ISO-8601 timestamp"));
287
+ if (artifact.provenance === void 0 || !isTemplateReferenceProvenanceSource(artifact.provenance.source) || !nonEmpty(artifact.provenance.referenceId) || !ISO_TIMESTAMP.test(artifact.provenance.recordedAt)) issues.push(issue(id, "provenance", "provenance requires a supported source, identifier, and ISO-8601 timestamp"));
119
288
  if (!SLUG.test(id) || !SLUG.test(artifact.gallery.slug)) issues.push(issue(id, "identity", "preset id and canonical slug must be lowercase kebab-case"));
120
289
  if (existingIds.has(id) || suppliedIds.has(id)) issues.push(issue(id, "duplicate-id", `preset id "${id}" is already present`));
121
290
  if (existingSlugs.has(artifact.gallery.slug) || suppliedSlugs.has(artifact.gallery.slug)) issues.push(issue(id, "duplicate-slug", `canonical slug "${artifact.gallery.slug}" is already present`));
122
291
  suppliedIds.add(id);
123
292
  suppliedSlugs.add(artifact.gallery.slug);
124
- const compiled = compileSource(artifact.preset.source, { theme: { overrides: [RECOMMENDED_THEME] } });
125
- if (Result.isFailure(compiled)) issues.push(issue(id, "source", compiled.failure.message));
126
- else if (artifact.preset.channel !== "email" || compiled.success.channel !== "email") issues.push(issue(id, "channel", "public template source must compile as email"));
293
+ const project = artifact.preset.project;
294
+ if (project !== void 0 && (!isSafePublicTemplateProject(project) || project.files.find((file) => file.path === project.entryPath)?.content !== artifact.preset.source)) issues.push(issue(id, "source", "project must contain safe bounded unique files and the matching UTF-8 entry source"));
295
+ if (artifact.provenance?.source === "brand-recreation" && !isValidTemplateBrandRecreation(artifact.provenance.brandRecreation, project)) issues.push(issue(id, "provenance", "brand recreations require a complete project, a referenced brand, and its email subject"));
296
+ if (parsePublicTemplateSource(artifact.preset.source) === void 0) issues.push(issue(id, "source", "source must be a bounded TSX module with a default export; execution is verified by the importer"));
297
+ if (artifact.preset.channel !== "email") issues.push(issue(id, "channel", "public template source must be an executable email"));
298
+ if (new Set(artifact.preset.variables).size !== artifact.preset.variables.length || artifact.preset.variables.some((value) => !nonEmpty(value))) issues.push(issue(id, "variables", "declared input fields must be non-empty and unique"));
127
299
  const unsupportedFontTokens = unsupportedPublicTemplateFontTokens(artifact.preset.source);
128
300
  if (unsupportedFontTokens.length > 0) issues.push(issue(id, "font-token", `unsupported public font token: ${unsupportedFontTokens.join(", ")}`));
129
- const parsed = parseSml(artifact.preset.source);
130
- if (Result.isSuccess(parsed)) {
131
- const sourceVariables = [...collectVariableReferences(parsed.success)];
132
- if (!sameStrings(artifact.preset.variables, sourceVariables)) issues.push(issue(id, "variables", "declared variables must exactly match source references"));
133
- }
134
301
  const sampleVariables = Object.keys(artifact.preview.sampleData);
135
302
  if (!sameStrings(sampleVariables, artifact.preset.variables)) issues.push(issue(id, "sample-data", "sample data must cover every declared variable exactly"));
136
- if (Object.values(artifact.preview.sampleData).some((value) => !nonEmpty(value))) issues.push(issue(id, "sample-data", "sample data values must be non-empty"));
303
+ if (Object.values(artifact.preview.sampleData).some((value) => !nonEmptySample(value))) issues.push(issue(id, "sample-data", "sample data values must be non-empty"));
137
304
  const assetVariables = artifact.preview.assets.map((asset) => asset.variable);
138
305
  if (new Set(assetVariables).size !== assetVariables.length) issues.push(issue(id, "assets", "asset variable references must be unique"));
139
306
  for (const asset of artifact.preview.assets) {
140
307
  const value = artifact.preview.sampleData[asset.variable];
141
308
  if (!artifact.preset.variables.includes(asset.variable)) issues.push(issue(id, "assets", `asset "${asset.variable}" is not a declared preset variable`));
142
- if (value === void 0 || !isSafePublicTemplateAsset(value)) issues.push(issue(id, "assets", `asset "${asset.variable}" must reference a bounded HTTPS or data:image sample URL`));
309
+ if (typeof value !== "string" || !isSafePublicTemplateAsset(value)) issues.push(issue(id, "assets", `asset "${asset.variable}" must reference a bounded HTTPS, /templates/assets, or data:image sample URL`));
143
310
  }
144
311
  const metadata = artifact.publicMetadata;
145
312
  const metadataStrings = [
@@ -178,6 +345,7 @@ const validatePublicTemplateHandoff = (artifacts) => {
178
345
  variables: artifact.preset.variables
179
346
  },
180
347
  source: artifact.preset.source,
348
+ ...artifact.preset.project === void 0 ? {} : { project: artifact.preset.project },
181
349
  gallery: reference.gallery,
182
350
  preview: artifact.preview,
183
351
  publicMetadata: artifact.publicMetadata,
@@ -186,6 +354,6 @@ const validatePublicTemplateHandoff = (artifacts) => {
186
354
  }));
187
355
  };
188
356
  //#endregion
189
- export { emitTemplateReferencePacket, isSafePublicTemplateAsset, toTemplateReferencePacket, unsupportedPublicTemplateFontTokens, validatePublicTemplateHandoff };
357
+ export { PUBLIC_TEMPLATE_PROJECT_FILE_MAX_BYTES, PUBLIC_TEMPLATE_PROJECT_MAX_BYTES, PUBLIC_TEMPLATE_PROJECT_MAX_FILES, PUBLIC_TEMPLATE_SOURCE_MAX_CHARACTERS, emitTemplateReferencePacket, isSafePublicTemplateAsset, isSafePublicTemplateProject, isSafePublicTemplateProjectPath, isValidTemplateBrandRecreation, publicTemplateAuthoredStructure, publicTemplateProjectDigest, toTemplateReferencePacket, unsupportedPublicTemplateFontTokens, validatePublicTemplateHandoff };
190
358
 
191
359
  //# sourceMappingURL=public-handoff.js.map
@@ -1,6 +1,6 @@
1
1
  import { EmailPresetId } from "../presets.js";
2
- import { n as PreviewQuality } from "../public-preview-u1Z9aaGq.js";
3
- import { t as PublicTemplate } from "../public-gallery-D0Zv0pZE.js";
2
+ import { PublicTemplate } from "./public-gallery.js";
3
+ import { PreviewQuality } from "./public-preview.js";
4
4
  import { Result } from "effect";
5
5
  //#region presets/public-preview-artifacts.d.ts
6
6
  type PreviewSurface = "card" | "detail" | "og";
@@ -1,9 +1,8 @@
1
- import { interpolate } from "../interpolate.js";
2
- import { compileSource } from "../compile.js";
3
- import { RECOMMENDED_THEME, presetCatalog } from "../presets.js";
1
+ import { presetCatalog } from "../presets.js";
2
+ import { registerPublicCorpusEntry } from "./corpus.js";
4
3
  import { publicTemplates } from "./public-gallery.js";
4
+ import { presetEmailAssets, presetEmailFixtures, presetModuleSource } from "./sources.js";
5
5
  import { publicTemplatePreviews } from "./public-preview.js";
6
- import { presetSource } from "./sources.js";
7
6
  import { Result } from "effect";
8
7
  //#region presets/public-preview-artifacts.ts
9
8
  const renderFrames = [
@@ -46,48 +45,43 @@ const renderInputs = (slug, html) => renderFrames.flatMap((frame) => ["light", "
46
45
  })));
47
46
  /** Build one static preview artifact without loading the editor runtime or performing I/O. */
48
47
  const buildPublicTemplatePreviewArtifact = (template) => {
49
- const source = presetSource(template.presetId);
50
- const preset = presetCatalog.find((entry) => entry.id === template.presetId);
51
- const preview = publicTemplatePreviews[template.presetId];
52
- if (source === void 0 || preset === void 0) return Result.fail({
53
- presetId: template.presetId,
54
- message: "canonical preset source is missing"
48
+ const registration = registerPublicCorpusEntry({
49
+ id: template.presetId,
50
+ corpus: template.corpus,
51
+ facets: template.facets
55
52
  });
56
- const compiled = compileSource(source, { theme: { overrides: [RECOMMENDED_THEME] } });
57
- if (Result.isFailure(compiled) || compiled.success.channel !== "email") return Result.fail({
53
+ if (Result.isFailure(registration)) return Result.fail({
58
54
  presetId: template.presetId,
59
- message: "canonical preset did not compile as email"
55
+ message: `public corpus registration failed: ${registration.failure.message}`
60
56
  });
61
- const variableSchema = preset.variables.map((name) => ({
62
- name,
63
- type: "string",
64
- required: true
65
- }));
66
- const rendered = interpolate(compiled.success.html, preview.sampleData, variableSchema);
67
- if (rendered.missing.size > 0) return Result.fail({
57
+ const source = presetModuleSource(template.presetId);
58
+ const fixture = presetEmailFixtures[template.presetId]?.default;
59
+ const preset = presetCatalog.find((entry) => entry.id === template.presetId);
60
+ const preview = publicTemplatePreviews[template.presetId];
61
+ if (source === void 0 || preset === void 0 || fixture === void 0) return Result.fail({
68
62
  presetId: template.presetId,
69
- message: `sample data is missing: ${[...rendered.missing].sort().join(", ")}`
63
+ message: "canonical preset source is missing"
70
64
  });
71
- const sampleData = preview.sampleData;
72
- const assets = [];
65
+ const sampleData = fixture.input;
66
+ const assets = new Set(presetEmailAssets[template.presetId]);
73
67
  for (const asset of preview.assets) {
74
68
  const value = sampleData[asset.variable];
75
- if (value === void 0) return Result.fail({
69
+ if (typeof value !== "string") return Result.fail({
76
70
  presetId: template.presetId,
77
- message: `sample data is missing asset: ${asset.variable}`
71
+ message: `sample data is missing a string asset: ${asset.variable}`
78
72
  });
79
- assets.push(value);
73
+ assets.add(value);
80
74
  }
81
75
  return Result.succeed({
82
76
  presetId: template.presetId,
83
77
  slug: template.slug,
84
- subject: preview.subject,
85
- preheader: preview.preheader,
78
+ subject: fixture.subject,
79
+ preheader: fixture.preheader,
86
80
  quality: preview.quality,
87
81
  source,
88
- html: rendered.rendered,
89
- assets,
90
- renders: renderInputs(template.slug, rendered.rendered)
82
+ html: fixture.html,
83
+ assets: [...assets],
84
+ renders: renderInputs(template.slug, fixture.html)
91
85
  });
92
86
  };
93
87
  /** Build only the requested canonical subset, preserving manifest order and aggregated failures. */
@@ -1,2 +1,65 @@
1
- import { i as publicTemplatePreviews, n as PreviewQuality, r as PublicTemplatePreview, t as PUBLIC_PREVIEW_LOGO_DATA_URI } from "../public-preview-u1Z9aaGq.js";
2
- export { PUBLIC_PREVIEW_LOGO_DATA_URI, PreviewQuality, PublicTemplatePreview, publicTemplatePreviews };
1
+ //#region presets/public-preview.d.ts
2
+ type PreviewQuality = "ready" | "asset-free";
3
+ /** A declared fixture value: JSON, so a template can take a list of stories as well as a string. */
4
+ type PreviewSampleValue = string | number | boolean | null | ReadonlyArray<PreviewSampleValue> | {
5
+ readonly [key: string]: PreviewSampleValue;
6
+ };
7
+ interface PublicTemplatePreview {
8
+ readonly subject: string;
9
+ readonly preheader: string;
10
+ readonly quality: PreviewQuality;
11
+ readonly sampleData: Readonly<Record<string, PreviewSampleValue>>;
12
+ readonly assets: ReadonlyArray<{
13
+ readonly variable: string;
14
+ readonly kind: "image" | "logo";
15
+ }>;
16
+ }
17
+ /** Gallery metadata augments the actual default fixture; it does not supply render input. */
18
+ declare const publicTemplatePreviews: {
19
+ readonly "linear-login-code": PublicTemplatePreview;
20
+ readonly "product-newsletter": PublicTemplatePreview;
21
+ readonly "order-receipt": PublicTemplatePreview;
22
+ readonly "vercel-invitation": PublicTemplatePreview;
23
+ readonly "product-launch-promo": PublicTemplatePreview;
24
+ readonly welcome: PublicTemplatePreview;
25
+ readonly "email-verification": PublicTemplatePreview;
26
+ readonly otp: PublicTemplatePreview;
27
+ readonly "password-reset": PublicTemplatePreview;
28
+ readonly receipt: PublicTemplatePreview;
29
+ readonly "payment-failed": PublicTemplatePreview;
30
+ readonly "shipping-update": PublicTemplatePreview;
31
+ readonly notification: PublicTemplatePreview;
32
+ readonly "team-invite": PublicTemplatePreview;
33
+ readonly "product-digest": PublicTemplatePreview;
34
+ readonly newsletter: PublicTemplatePreview;
35
+ readonly announcement: PublicTemplatePreview;
36
+ readonly promo: PublicTemplatePreview;
37
+ readonly "event-invite": PublicTemplatePreview;
38
+ readonly "membership-promo": PublicTemplatePreview;
39
+ readonly "onboarding-step": PublicTemplatePreview;
40
+ readonly "trial-ending": PublicTemplatePreview;
41
+ readonly "win-back": PublicTemplatePreview;
42
+ readonly "feedback-request": PublicTemplatePreview;
43
+ readonly "editorial-newsletter": PublicTemplatePreview;
44
+ readonly "member-referral-offer": PublicTemplatePreview;
45
+ readonly "catalog-sale-newsletter": PublicTemplatePreview;
46
+ readonly "limited-time-savings": PublicTemplatePreview;
47
+ readonly "rewards-milestone": PublicTemplatePreview;
48
+ readonly "order-delivered": PublicTemplatePreview;
49
+ readonly "travel-rewards-promo": PublicTemplatePreview;
50
+ readonly "events-roundup": PublicTemplatePreview;
51
+ readonly "product-onboarding-tour": PublicTemplatePreview;
52
+ readonly "product-updates-roundup": PublicTemplatePreview;
53
+ readonly "flash-getaway-offer": PublicTemplatePreview;
54
+ readonly "membership-benefits-promo": PublicTemplatePreview;
55
+ readonly "industry-news-digest": PublicTemplatePreview;
56
+ readonly "weekly-deals-circular": PublicTemplatePreview;
57
+ readonly "data-sharing-review": PublicTemplatePreview;
58
+ readonly "seasonal-sale-flyer": PublicTemplatePreview;
59
+ readonly "setup-checklist": PublicTemplatePreview;
60
+ readonly "feature-release-roundup": PublicTemplatePreview;
61
+ readonly "season-clearance-promo": PublicTemplatePreview;
62
+ };
63
+ //#endregion
64
+ export { PreviewQuality, PreviewSampleValue, PublicTemplatePreview, publicTemplatePreviews };
65
+ //# sourceMappingURL=public-preview.d.ts.map