best-email-builder 0.2.0 → 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 (37) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +102 -0
  3. package/dist/builder.js +44 -0
  4. package/dist/builder.js.map +1 -0
  5. package/dist/{chunk-MTALS5GC.js → chunk-3M36VVXS.js} +36 -118
  6. package/dist/chunk-3M36VVXS.js.map +1 -0
  7. package/dist/chunk-57CTQNTK.js +56 -0
  8. package/dist/chunk-57CTQNTK.js.map +1 -0
  9. package/dist/chunk-BVOVAEEJ.js +73 -0
  10. package/dist/chunk-BVOVAEEJ.js.map +1 -0
  11. package/dist/chunk-ILRAQTHS.js +3677 -0
  12. package/dist/chunk-ILRAQTHS.js.map +1 -0
  13. package/dist/chunk-JOC752I3.js +529 -0
  14. package/dist/chunk-JOC752I3.js.map +1 -0
  15. package/dist/chunk-ZSUHOCCE.js +1351 -0
  16. package/dist/chunk-ZSUHOCCE.js.map +1 -0
  17. package/dist/index.js +86 -4865
  18. package/dist/index.js.map +1 -1
  19. package/dist/library.js +31 -0
  20. package/dist/library.js.map +1 -0
  21. package/dist/server.js +15 -11
  22. package/dist/server.js.map +1 -1
  23. package/dist/styles.css +1 -1
  24. package/dist/types/adapters/grapesjs/engineConfig.d.ts +0 -14
  25. package/dist/types/builder.d.ts +8 -0
  26. package/dist/types/core/constants.d.ts +1 -1
  27. package/dist/types/core/scrollbars.d.ts +13 -0
  28. package/dist/types/defaultEditor.d.ts +8 -0
  29. package/dist/types/features/templates/gallery.d.ts +7 -0
  30. package/dist/types/index.d.ts +4 -9
  31. package/dist/types/library.d.ts +5 -0
  32. package/dist/types/storage/client.d.ts +4 -0
  33. package/dist/types/ui/EmailStudio.d.ts +42 -0
  34. package/dist/types/ui/library/TemplateLibrary.d.ts +41 -0
  35. package/dist/types/ui/primitives/button.d.ts +1 -1
  36. package/package.json +18 -1
  37. package/dist/chunk-MTALS5GC.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,59 @@ All notable changes are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.3.0] — 2026-10-13
8
+
9
+ ### Added
10
+ - **`TemplateLibrary`** — the first screen: a template bank in three columns
11
+ (searchable list of your templates and of the ready-made bases, details of
12
+ the selected one, rendered preview on desktop / mobile). *Edit*, *New
13
+ template* and *Start from this base* hand the template to the host through
14
+ `onEdit` / `onCreate`; *Duplicate*, *Delete*, *Copy HTML* and *Download*
15
+ work in place. *Favorites*: the star of the details moves an entry to a
16
+ section at the top (`favoritesKey`). Search with Ctrl+K, Esc clears it.
17
+ Follows its own width down to phones. French included.
18
+ - **`EmailStudio`** — `TemplateLibrary` and `EmailBuilder` wired together.
19
+ Each screen still works alone.
20
+ The editor opened from the bank has no *Templates* button: the bases are
21
+ picked in the bank, and the gallery would replace the email being edited
22
+ (`builderProps.templates` brings it back).
23
+ - **Two entry points, one per block**: `best-email-builder/library` (block A,
24
+ the bank — no editor, no GrapesJS) and `best-email-builder/builder`
25
+ (block B, the editor — nothing of the bank). A unit test bundles each one
26
+ and fails if it reaches the other. The README shows block B under a bank
27
+ of your own (what to pass in, what comes back) and block A with your own
28
+ editor; a story drives block B from a home-made bank.
29
+ - Storage contract: optional `list()` and `remove(id)`, implemented by
30
+ `createHttpStorage()` (the server already had the routes) and
31
+ `createMemoryStorage()`.
32
+ - `withPlaceholderImages(html)` draws a grey frame for images not chosen yet
33
+ in compiled HTML.
34
+
35
+ ### Changed
36
+ - Demo, three pages: a landing page (a background that follows the pointer,
37
+ a marquee of the editors it can replace, a new mark), the template bank,
38
+ and the editor — each page has its own address (`#/templates`,
39
+ `#/editor/…`), so a reload keeps it. The bank and the editor share one
40
+ window of the same size, under a bar with the steps and a *Full screen* /
41
+ *Exit full screen* button. Type: Geist, Bricolage Grotesque for the title.
42
+ - Thin scrollbars everywhere in the builder and the
43
+ bank, inside the canvas and the preview frames (preview only, never the
44
+ saved HTML): a 5.6 px gutter, a thin thumb, no arrows, shown only while the
45
+ pointer is over the block. They also replace GrapesJS's own dark ones.
46
+ - The preview (in the bank and in the editor's *Preview*) switches device
47
+ with the editor toolbar's switch, and narrows and widens like the canvas
48
+ (350 ms).
49
+ - `types` and `typesVersions` in `package.json`: TypeScript projects still on
50
+ `moduleResolution: "node"` find the types of every entry (`.`, `/builder`,
51
+ `/library`, `/server`).
52
+ - `EmailBuilder` no longer imports the package entry
53
+ (`createDefaultEmailEditor` lives in its own module).
54
+
55
+ ### Fixed
56
+ - Demo: after a first save, switching the language reopened a blank editor
57
+ (or the base), and saving again created a duplicate; it now reopens the
58
+ saved template.
59
+
7
60
  ## [0.2.0] — 2026-10-12
8
61
 
9
62
  ### Added
package/README.md CHANGED
@@ -103,6 +103,106 @@ component (the canvas needs the DOM). With Next.js, load it with
103
103
  `dynamic(() => import(...), { ssr: false })` or render it from a
104
104
  `'use client'` component.
105
105
 
106
+ ### Two blocks, each one on its own
107
+
108
+ The package has two blocks. An application installs one, the other, or both,
109
+ and each has its own entry point, so one never brings the other's code:
110
+
111
+ | Block | Import | What it is |
112
+ |---|---|---|
113
+ | A — the bank | `import { TemplateLibrary } from 'best-email-builder/library'` | Your templates and the ready-made bases in a searchable list, the selected one's details, its rendered preview (desktop / mobile). *Edit*, *New template* and *Start from this base* only call `onEdit(template)` / `onCreate(start)` — it never mounts an editor, and does not load GrapesJS. |
114
+ | B — the editor | `import { EmailBuilder } from 'best-email-builder/builder'` | The editor (above). Opens whatever the host hands it; knows nothing of the bank. |
115
+ | A + B | `import { EmailStudio } from 'best-email-builder'` | The two wired together: the bank, then the editor, then back to the bank with the template just saved selected. The editor has no *Templates* button there — the bases are in the bank. |
116
+
117
+ `best-email-builder` (the main entry) still exports everything; the two
118
+ sub-entries are for an application that wants one block only. Both use the
119
+ same stylesheet, `best-email-builder/styles.css`.
120
+
121
+ #### A + B, nothing to wire
122
+
123
+ ```jsx
124
+ import { EmailStudio, createHttpStorage } from 'best-email-builder';
125
+
126
+ <EmailStudio storage={createHttpStorage()} locale="fr" sender={sender} />
127
+ ```
128
+
129
+ #### Block B with your own bank
130
+
131
+ Your bank (a table, a sidebar, a modal — anything) only decides what the
132
+ editor opens, and hears back when it saves or closes:
133
+
134
+ | Your bank wants to… | Pass to `EmailBuilder` |
135
+ |---|---|
136
+ | open a saved template | `templateId={id}` (the editor loads it from `storage`) |
137
+ | start from content it has (a base, an import, a copy) | `initialMjml={mjml}` and, optionally, `initialMeta={{ subject, preheader }}` |
138
+ | start from a blank email | neither |
139
+ | refresh its list after a save | `onSaved={(template) => …}` — `template.id` is set from the first save on |
140
+ | show itself again | `onClose={() => …}` (the *Back* button) |
141
+ | hide the editor's own gallery of bases | `templates={[]}` |
142
+
143
+ Give the editor a `key` that changes with what it opens, so switching from one
144
+ template to another starts a fresh editor.
145
+
146
+ ```jsx
147
+ import { useState } from 'react';
148
+ import { EmailBuilder, createHttpStorage } from 'best-email-builder/builder';
149
+
150
+ const storage = createHttpStorage();
151
+
152
+ function Emails() {
153
+ const [open, setOpen] = useState(null); // null = my bank
154
+
155
+ if (!open) {
156
+ return (
157
+ <MyTemplateList // your block A
158
+ onOpen={(id) => setOpen({ key: id, templateId: id })}
159
+ onNew={() => setOpen({ key: `new-${Date.now()}` })}
160
+ />
161
+ );
162
+ }
163
+ return (
164
+ <EmailBuilder
165
+ key={open.key}
166
+ storage={storage}
167
+ locale="fr"
168
+ templates={[]}
169
+ templateId={open.templateId ?? null}
170
+ initialMjml={open.mjml}
171
+ initialMeta={open.meta}
172
+ onSaved={() => refreshMyList()}
173
+ onClose={() => setOpen(null)}
174
+ />
175
+ );
176
+ }
177
+ ```
178
+
179
+ Your list can read the same templates with `storage.list()` (summaries, newest
180
+ first) or from your own API — the editor only needs the `id`.
181
+
182
+ #### Block A with your own editor
183
+
184
+ ```jsx
185
+ import { TemplateLibrary, createHttpStorage } from 'best-email-builder/library';
186
+
187
+ <TemplateLibrary
188
+ storage={createHttpStorage()}
189
+ locale="fr"
190
+ onEdit={(template) => router.push(`/emails/${template.id}`)}
191
+ onCreate={(start) => openMyEditor(start)} // start: null (new), or { baseId, mjml, meta: { subject, preheader } }
192
+ onClose={closeModal} // optional: shows a close button
193
+ />
194
+ ```
195
+
196
+ The library needs `storage.list()`; *Delete* appears when the storage has
197
+ `remove()`, the preview uses `render()` (else the HTML compiled at the last
198
+ save). Its props: `storage`, `locale`, `messages`, `onNotify`, `sender`,
199
+ `templates` (the bases, `[]` hides them), `title`, `description`,
200
+ `selectedId`, `favoritesKey` (the star of the details moves an entry to a
201
+ *Favorites* section at the top; kept in `localStorage` under this key, `null`
202
+ turns favorites off), `onEdit`, `onCreate`, `onClose`, `className`. Like the
203
+ builder, it fills its parent and follows its own width (three columns, then
204
+ two, then list and details as two screens).
205
+
106
206
  ### 2. The server routes
107
207
 
108
208
  ```js
@@ -194,7 +294,9 @@ or use `createHttpStorage()` with the server handler:
194
294
 
195
295
  ```ts
196
296
  interface TemplateStorage {
297
+ list?(): Promise<Template[]>; // TemplateLibrary: newest first, no document/MJML
197
298
  load(id): Promise<Template>;
299
+ remove?(id): Promise<void>; // TemplateLibrary: enables Delete
198
300
  create(payload): Promise<{ template; warnings }>;
199
301
  duplicate(id): Promise<{ template; warnings }>;
200
302
  save(id, payload): Promise<{ template; warnings; version }>;
@@ -0,0 +1,44 @@
1
+ import {
2
+ EmailBuilder,
3
+ EmailBuilderBoundary
4
+ } from "./chunk-ILRAQTHS.js";
5
+ import {
6
+ STARTER_MJML,
7
+ StorageError,
8
+ TEMPLATE_GALLERY,
9
+ buildGallery,
10
+ createHttpStorage,
11
+ createMemoryStorage,
12
+ createStarterMjml
13
+ } from "./chunk-ZSUHOCCE.js";
14
+ import "./chunk-57CTQNTK.js";
15
+ import {
16
+ CUSTOM_GROUP,
17
+ DEFAULT_VARIABLES,
18
+ EMAIL_BUILDER_VERSION,
19
+ LOCALES,
20
+ createTranslator,
21
+ defineVariables,
22
+ mergeVariables,
23
+ translatorFor
24
+ } from "./chunk-3M36VVXS.js";
25
+ export {
26
+ CUSTOM_GROUP,
27
+ DEFAULT_VARIABLES,
28
+ EMAIL_BUILDER_VERSION,
29
+ EmailBuilder,
30
+ EmailBuilderBoundary,
31
+ LOCALES,
32
+ STARTER_MJML,
33
+ StorageError,
34
+ TEMPLATE_GALLERY,
35
+ buildGallery,
36
+ createHttpStorage,
37
+ createMemoryStorage,
38
+ createStarterMjml,
39
+ createTranslator,
40
+ defineVariables,
41
+ mergeVariables,
42
+ translatorFor
43
+ };
44
+ //# sourceMappingURL=builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,5 +1,5 @@
1
1
  // src/core/constants.js
2
- var EMAIL_BUILDER_VERSION = "0.2.0";
2
+ var EMAIL_BUILDER_VERSION = "0.3.0";
3
3
  var EMAIL_DOCUMENT_SCHEMA_VERSION = 1;
4
4
  var EMAIL_DEVICES = ["desktop", "mobile"];
5
5
  var EMAIL_MOBILE_WIDTH = 375;
@@ -88,21 +88,38 @@ var fr = {
88
88
  "A sale with a discount code and two products": "Une vente avec un code promo et deux produits",
89
89
  "A tip": "Une astuce",
90
90
  "Add a block": "Ajouter un bloc",
91
+ "Add to favorites": "Ajouter aux favoris",
92
+ "All templates": "Tous les mod\xE8les",
93
+ "All your templates are in the favorites.": "Tous vos mod\xE8les sont dans les favoris.",
91
94
  "An empty page to build from scratch": "Une page vide pour partir de z\xE9ro",
92
95
  "An evening of talks and drinks with the team and our customers.": "Une soir\xE9e de rencontres autour d'un verre avec l'\xE9quipe et nos clients.",
93
96
  "An event": "Un \xE9v\xE9nement",
94
97
  "Article {n}": "Article {n}",
95
98
  "Background image": "Image de fond",
96
99
  "Background image with a title and a button": "Image de fond avec un titre et un bouton",
100
+ "Base": "Base",
101
+ "Base \u2014 not editable: start from it to create your template.": "Base non modifiable \u2014 partez-en pour cr\xE9er votre mod\xE8le.",
102
+ "Bases": "Bases",
97
103
  "Blank": "Vierge",
98
104
  "Book my seat": "Je r\xE9serve ma place",
105
+ "Builder": "Constructeur",
99
106
  "Buy": "Acheter",
107
+ "by {name}": "par {name}",
108
+ "Choose a template": "Choisissez un mod\xE8le",
109
+ "Clear the search": "Effacer la recherche",
110
+ "Copy HTML": "Copier le HTML",
111
+ "Could not load the templates.": "Impossible de charger les mod\xE8les.",
112
+ "Created": "Cr\xE9\xE9",
100
113
  "Date, place and a button to book a seat": "Date, lieu et un bouton pour r\xE9server",
114
+ "Delete \u201C{name}\u201D?": "Supprimer \xAB {name} \xBB ?",
101
115
  "Direction": "Disposition",
116
+ "Download": "T\xE9l\xE9charger",
102
117
  "Edit: {element}": "Modifier : {element}",
103
118
  "Email settings": "R\xE9glages de l'e-mail",
119
+ "Email template library": "Banque de mod\xE8les emails",
104
120
  "Event invitation": "Invitation",
105
121
  "Facebook": "Facebook",
122
+ "Favorites": "Favoris",
106
123
  "Fill the section": "Remplir la section",
107
124
  "Get started": "Commencer",
108
125
  "GitHub": "GitHub",
@@ -110,6 +127,7 @@ var fr = {
110
127
  "Heading": "Titre",
111
128
  "Hero banner": "Banni\xE8re",
112
129
  "Horizontal": "Horizontale",
130
+ "HTML copied.": "HTML copi\xE9.",
113
131
  "Icon background": "Fond de l'ic\xF4ne",
114
132
  "Icon size": "Taille des ic\xF4nes",
115
133
  "Icons linking to your profiles": "Ic\xF4nes vers vos profils",
@@ -127,8 +145,12 @@ var fr = {
127
145
  "Monthly news: banner, two articles, highlights": "Les nouvelles du mois : banni\xE8re, deux articles, br\xE8ves",
128
146
  "Network": "R\xE9seau",
129
147
  "Newsletter": "Newsletter",
148
+ "No preview: this storage cannot render emails.": "Pas d'aper\xE7u : ce stockage ne sait pas compiler les e-mails.",
149
+ "No template yet. Start from a base, or create a new one.": "Aucun mod\xE8le pour l\u2019instant. Partez d\u2019une base, ou cr\xE9ez-en un.",
150
+ "Nothing matches \u201C{query}\u201D.": "Aucun r\xE9sultat pour \xAB {query} \xBB.",
130
151
  "Offer valid until Sunday midnight, while stocks last.": "Offre valable jusqu'\xE0 dimanche minuit, dans la limite des stocks.",
131
152
  "One sentence about it.": "Une phrase pour en parler.",
153
+ "Origin": "Origine",
132
154
  "Original size": "Taille d'origine",
133
155
  "Our offices, 1 Main Street": "Nos bureaux, 1 rue Principale",
134
156
  "Picking a template replaces the email being edited.": "Choisir un mod\xE8le remplace l'e-mail en cours de modification.",
@@ -137,12 +159,16 @@ var fr = {
137
159
  "Profile URL": "Adresse du profil",
138
160
  "Promotion": "Promotion",
139
161
  "Read more": "Lire la suite",
162
+ "Ready-made templates to copy into your campaigns.": "Mod\xE8les pr\xEAts \xE0 copier dans vos campagnes.",
163
+ "Remove from favorites": "Retirer des favoris",
140
164
  "Replace": "Remplacer",
141
165
  "Replace the current email?": "Remplacer l'e-mail actuel ?",
142
166
  "Reply to this email: a person reads it.": "R\xE9pondez \xE0 cet e-mail : une personne le lit.",
143
167
  "Row of links to your site": "Rang\xE9e de liens vers votre site",
144
168
  "Save the date \u2014 seats are limited.": "Notez la date \u2014 les places sont limit\xE9es.",
169
+ "Search templates": "Rechercher un mod\xE8le",
145
170
  "Seats are limited: book yours now, it takes ten seconds.": "Les places sont limit\xE9es : r\xE9servez la v\xF4tre, cela prend dix secondes.",
171
+ "Select a template on the left.": "S\xE9lectionnez un mod\xE8le \xE0 gauche.",
146
172
  "Shop the sale": "Voir les offres",
147
173
  "Simple": "Simple",
148
174
  "So we can tailor what we send you.": "Pour adapter ce que nous vous envoyons.",
@@ -150,14 +176,21 @@ var fr = {
150
176
  "Social links": "R\xE9seaux sociaux",
151
177
  "Space around each icon": "Espace autour de chaque ic\xF4ne",
152
178
  "Start from a template": "Partir d'un mod\xE8le",
179
+ "Start from this base": "Partir de cette base",
153
180
  "Take a tour of what you can do.": "Faites le tour de ce que vous pouvez faire.",
181
+ "Template deleted.": "Mod\xE8le supprim\xE9.",
182
+ "Template duplicated.": "Mod\xE8le dupliqu\xE9.",
154
183
  "Templates": "Mod\xE8les",
155
184
  "Thank you for joining us. Here is how to get the most out of your account.": "Merci de nous avoir rejoints. Voici comment tirer le meilleur de votre compte.",
185
+ "The browser refused to copy.": "Le navigateur a refus\xE9 la copie.",
156
186
  "The news of the month": "Les nouvelles du mois",
187
+ "The template and its versions will be deleted.": "Le mod\xE8le et ses versions seront supprim\xE9s.",
188
+ "The template preview appears here.": "L'aper\xE7u du mod\xE8le s'affiche ici.",
157
189
  "This weekend only, with the code SPRING30.": "Ce week-end seulement, avec le code SPRING30.",
158
190
  "Three steps to get started.": "Trois \xE9tapes pour bien commencer.",
159
191
  "Thursday, 7 pm": "Jeudi, 19 h",
160
192
  "Two or three sentences that sum up the story and make people want the rest.": "Deux ou trois phrases qui r\xE9sument l'article et donnent envie de lire la suite.",
193
+ "Updated": "Modifi\xE9",
161
194
  "Vertical": "Verticale",
162
195
  "Welcome": "Bienvenue",
163
196
  "Welcome aboard!": "Bienvenue \xE0 bord !",
@@ -576,66 +609,6 @@ function applyServiceLinks(html, { unsubscribeUrl }) {
576
609
  return String(html || "").replace(UNSUBSCRIBE, () => escapeAttribute(unsubscribeUrl)).replace(MIRROR, "#");
577
610
  }
578
611
 
579
- // src/features/variables/service.js
580
- function escapeHtml(text) {
581
- return String(text ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
582
- }
583
- function decodeHtml(text) {
584
- return String(text ?? "").replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&");
585
- }
586
- var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
587
- function patternOf(key) {
588
- return new RegExp(`\\{\\{\\s*${escapeRegExp(key)}\\s*(?:\\|([^{}]*))?\\}\\}`, "g");
589
- }
590
- var ANY_TAG = /\{\{\s*([A-Za-z][\w.]*)\s*(?:\|[^{}]*)?\}\}/g;
591
- var textVariables = (variables) => variables.filter((v) => v.kind !== "link");
592
- var present = (value) => value !== null && value !== void 0 && String(value) !== "";
593
- function replace(text, values, variables, html) {
594
- let out = String(text || "");
595
- for (const variable of textVariables(variables)) {
596
- out = out.replace(patternOf(variable.key), (_, inline) => {
597
- const value = values?.[variable.key];
598
- if (present(value)) return html ? escapeHtml(value) : String(value);
599
- const fallback = inline !== void 0 ? (html ? decodeHtml(inline) : inline).trim() : variable.fallback || "";
600
- return html ? escapeHtml(fallback) : fallback;
601
- });
602
- }
603
- return out;
604
- }
605
- function applyVariables(html, values, variables = DEFAULT_VARIABLES) {
606
- return replace(html, values, variables, true);
607
- }
608
- function applyVariablesToText(text, values, variables = DEFAULT_VARIABLES) {
609
- return replace(text, values, variables, false);
610
- }
611
- function mapVariables(html, mapping, variables = DEFAULT_VARIABLES) {
612
- let out = String(html || "");
613
- for (const variable of textVariables(variables)) {
614
- if (!Object.prototype.hasOwnProperty.call(mapping, variable.key)) continue;
615
- out = out.replace(patternOf(variable.key), () => String(mapping[variable.key]));
616
- }
617
- return out;
618
- }
619
- function usedVariables(text, variables = DEFAULT_VARIABLES) {
620
- const source = String(text || "");
621
- return variables.filter((v) => patternOf(v.key).test(source)).map((v) => v.key);
622
- }
623
- function unknownVariables(text, variables = DEFAULT_VARIABLES, ignore = ["unsubscribe", "mirror"]) {
624
- const known = /* @__PURE__ */ new Set([...variables.map((v) => v.key), ...ignore]);
625
- const found = /* @__PURE__ */ new Set();
626
- for (const match of String(text || "").matchAll(ANY_TAG)) {
627
- if (!known.has(match[1])) found.add(match[1]);
628
- }
629
- return [...found];
630
- }
631
- function sampleValues(variables = DEFAULT_VARIABLES, overrides = {}) {
632
- const values = Object.fromEntries(textVariables(variables).map((v) => [v.key, v.sample]));
633
- for (const [key, value] of Object.entries(overrides)) {
634
- if (present(value)) values[key] = String(value);
635
- }
636
- return values;
637
- }
638
-
639
612
  // src/validation/images.js
640
613
  var MAX_IMAGE_BYTES = 2 * 1024 * 1024;
641
614
  var IMAGE_TYPES = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif" };
@@ -658,51 +631,6 @@ function imageUploadError(file, { t = identity } = {}) {
658
631
  return null;
659
632
  }
660
633
 
661
- // src/validation/urls.js
662
- var LINK_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "action", "formaction", "xlink:href"]);
663
- var IMAGE_ATTRIBUTES = /* @__PURE__ */ new Set([
664
- "src",
665
- "srcset",
666
- "background-url",
667
- "background",
668
- "poster",
669
- "icon-unwrapped-url",
670
- "icon-wrapped-url"
671
- ]);
672
- var NAMED_ENTITIES = {
673
- colon: ":",
674
- tab: " ",
675
- newline: "\n",
676
- lpar: "(",
677
- rpar: ")",
678
- amp: "&",
679
- quot: '"',
680
- apos: "'",
681
- lt: "<",
682
- gt: ">",
683
- nbsp: " "
684
- };
685
- function decodeEntities(value) {
686
- return String(value ?? "").replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);?/gi, (whole, body) => {
687
- const key = String(body).toLowerCase();
688
- if (key.startsWith("#x")) return String.fromCodePoint(Number.parseInt(key.slice(2), 16) || 0);
689
- if (key.startsWith("#")) return String.fromCodePoint(Number.parseInt(key.slice(1), 10) || 0);
690
- return NAMED_ENTITIES[key] ?? whole;
691
- });
692
- }
693
- function isMergeTag(value) {
694
- return /^\{\{\s*[a-zA-Z][\w.]*\s*\}\}$/.test(String(value || "").trim());
695
- }
696
- function isSafeUrl(raw, kind) {
697
- const value = decodeEntities(raw).trim();
698
- if (!value) return true;
699
- if (isMergeTag(value)) return true;
700
- const compact = value.replace(/[\s\u0000-\u001f\u007f]+/g, "").toLowerCase();
701
- if (kind === "image") return compact.startsWith("https://") || isAssetPath(value);
702
- if (compact.startsWith("#")) return true;
703
- return /^(https?:\/\/|mailto:|tel:)/.test(compact);
704
- }
705
-
706
634
  export {
707
635
  EMAIL_BUILDER_VERSION,
708
636
  EMAIL_DOCUMENT_SCHEMA_VERSION,
@@ -732,16 +660,6 @@ export {
732
660
  IMAGE_TYPES,
733
661
  ASSET_KEY_PATTERN,
734
662
  detectImage,
735
- imageUploadError,
736
- LINK_ATTRIBUTES,
737
- IMAGE_ATTRIBUTES,
738
- decodeEntities,
739
- isSafeUrl,
740
- applyVariables,
741
- applyVariablesToText,
742
- mapVariables,
743
- usedVariables,
744
- unknownVariables,
745
- sampleValues
663
+ imageUploadError
746
664
  };
747
- //# sourceMappingURL=chunk-MTALS5GC.js.map
665
+ //# sourceMappingURL=chunk-3M36VVXS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/constants.js","../src/core/document.js","../src/i18n/fr.js","../src/i18n/index.js","../src/features/variables/catalog.js","../src/renderer/links.js","../src/validation/images.js"],"sourcesContent":["// @ts-check\n/**\n * Email builder constants.\n *\n * `EMAIL_BUILDER_VERSION` versions THIS package (UI, blocks, rendering);\n * `EMAIL_DOCUMENT_SCHEMA_VERSION` versions the stored JSON envelope (the\n * project JSON). Both are written with every template, so a document read by\n * a newer version knows whether it must be migrated (see `core/document.js`).\n */\n\nexport const EMAIL_BUILDER_VERSION = '0.3.0';\n\nexport const EMAIL_DOCUMENT_SCHEMA_VERSION = 1;\n\n/** @typedef {'desktop'|'mobile'} EmailDevice */\n\n/** @type {EmailDevice[]} */\nexport const EMAIL_DEVICES = ['desktop', 'mobile'];\n\n/** Content column width — the email client standard. */\nexport const EMAIL_CONTENT_WIDTH = 600;\n\n/** Mobile preview frame width (standard iPhone, the common worst case). */\nexport const EMAIL_MOBILE_WIDTH = 375;\n","// @ts-check\n/**\n * DOCUMENT envelope — what gets stored (the project JSON).\n *\n * HTML is only a compilation output; what gets edited is the engine's\n * project. It is not stored bare but wrapped with:\n *\n * schemaVersion the shape of THIS envelope (migrations, see MIGRATIONS);\n * engine the engine that can read `project` back;\n * engineVersion its version at write time;\n * builderVersion the builder version that wrote it;\n * project the engine data, opaque to the rest of the code.\n *\n * A document from another engine, a future schema or a malformed one is\n * REJECTED on read rather than half-loaded: a template silently emptied and\n * then saved again would be lost.\n *\n * Dependency-free module: testable outside the browser.\n */\n\nimport { EMAIL_BUILDER_VERSION, EMAIL_DOCUMENT_SCHEMA_VERSION } from './constants.js';\n\n/**\n * @typedef {{\n * schemaVersion: number,\n * engine: string,\n * engineVersion: string,\n * builderVersion: string,\n * project: Record<string, any>,\n * }} EmailDocument\n */\n\n/**\n * Envelope migrations, from N to N+1. Empty while only one schema exists; the\n * first entry will be `1: (doc) => ({ ...doc, schemaVersion: 2 })`.\n *\n * @type {Record<number, (document: any) => any>}\n */\nconst MIGRATIONS = {};\n\nexport class EmailDocumentError extends Error {\n /** @param {string} message */\n constructor(message) {\n super(message);\n this.name = 'EmailDocumentError';\n this.status = 422;\n }\n}\n\nfunction isPlainObject(value) {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value);\n}\n\n/**\n * @param {{engine: string, engineVersion: string, project: object}} parts\n * @returns {EmailDocument}\n */\nexport function createDocument({ engine, engineVersion, project }) {\n if (!engine) throw new EmailDocumentError('Document: missing engine');\n if (!isPlainObject(project)) throw new EmailDocumentError('Document: invalid engine project');\n return {\n schemaVersion: EMAIL_DOCUMENT_SCHEMA_VERSION,\n engine,\n engineVersion: String(engineVersion || ''),\n builderVersion: EMAIL_BUILDER_VERSION,\n project: /** @type {Record<string, any>} */ (project),\n };\n}\n\n/**\n * Reads a document (object or JSON string), migrates it to the current schema\n * and validates it.\n *\n * @param {unknown} raw\n * @returns {EmailDocument}\n * @throws {EmailDocumentError}\n */\nexport function readDocument(raw) {\n let value = raw;\n if (typeof value === 'string') {\n try {\n value = JSON.parse(value);\n } catch {\n throw new EmailDocumentError('Unreadable document: invalid JSON');\n }\n }\n if (!isPlainObject(value)) throw new EmailDocumentError('Unreadable document: object expected');\n\n /** @type {any} */\n let document = { ...(/** @type {object} */ (value)) };\n const version = Number(document.schemaVersion);\n if (!Number.isInteger(version) || version < 1) {\n throw new EmailDocumentError('Unreadable document: missing schema version');\n }\n if (version > EMAIL_DOCUMENT_SCHEMA_VERSION) {\n throw new EmailDocumentError(\n `Document written by a newer version of the builder (schema ${version}) — ` +\n 'reload the page'\n );\n }\n for (let v = version; v < EMAIL_DOCUMENT_SCHEMA_VERSION; v += 1) {\n const migrate = MIGRATIONS[v];\n if (!migrate) throw new EmailDocumentError(`Document: no migration from schema ${v}`);\n document = migrate(document);\n }\n\n if (typeof document.engine !== 'string' || !document.engine) {\n throw new EmailDocumentError('Unreadable document: missing engine');\n }\n if (!isPlainObject(document.project)) {\n throw new EmailDocumentError('Unreadable document: missing engine project');\n }\n\n return {\n schemaVersion: EMAIL_DOCUMENT_SCHEMA_VERSION,\n engine: document.engine,\n engineVersion: String(document.engineVersion || ''),\n builderVersion: String(document.builderVersion || ''),\n project: document.project,\n };\n}\n\n/** @param {EmailDocument} document */\nexport function serializeDocument(document) {\n return JSON.stringify(readDocument(document));\n}\n","// French locale. Keys are the English source strings (see src/i18n/index.js).\n// Missing keys: `node scripts/i18n-keys.mjs fr`.\n\n/** @type {Record<string, string>} */\nexport const fr = {\n \"1. Complete your profile\": \"1. Complétez votre profil\",\n \"2. Explore\": \"2. Explorez\",\n \"3. Ask us anything\": \"3. Posez-nous vos questions\",\n \"30% off everything\": \"-30 % sur tout\",\n \"30% off this weekend only\": \"-30 % ce week-end seulement\",\n \"4 columns\": \"4 colonnes\",\n \"4-column section\": \"Section en 4 colonnes\",\n \"A new feature\": \"Une nouveauté\",\n \"A sale with a discount code and two products\": \"Une vente avec un code promo et deux produits\",\n \"A tip\": \"Une astuce\",\n \"Add a block\": \"Ajouter un bloc\",\n \"Add to favorites\": \"Ajouter aux favoris\",\n \"All templates\": \"Tous les modèles\",\n \"All your templates are in the favorites.\": \"Tous vos modèles sont dans les favoris.\",\n \"An empty page to build from scratch\": \"Une page vide pour partir de zéro\",\n \"An evening of talks and drinks with the team and our customers.\": \"Une soirée de rencontres autour d'un verre avec l'équipe et nos clients.\",\n \"An event\": \"Un événement\",\n \"Article {n}\": \"Article {n}\",\n \"Background image\": \"Image de fond\",\n \"Background image with a title and a button\": \"Image de fond avec un titre et un bouton\",\n \"Base\": \"Base\",\n \"Base — not editable: start from it to create your template.\": \"Base non modifiable — partez-en pour créer votre modèle.\",\n \"Bases\": \"Bases\",\n \"Blank\": \"Vierge\",\n \"Book my seat\": \"Je réserve ma place\",\n \"Builder\": \"Constructeur\",\n \"Buy\": \"Acheter\",\n \"by {name}\": \"par {name}\",\n \"Choose a template\": \"Choisissez un modèle\",\n \"Clear the search\": \"Effacer la recherche\",\n \"Copy HTML\": \"Copier le HTML\",\n \"Could not load the templates.\": \"Impossible de charger les modèles.\",\n \"Created\": \"Créé\",\n \"Date, place and a button to book a seat\": \"Date, lieu et un bouton pour réserver\",\n \"Delete “{name}”?\": \"Supprimer « {name} » ?\",\n \"Direction\": \"Disposition\",\n \"Download\": \"Télécharger\",\n \"Edit: {element}\": \"Modifier : {element}\",\n \"Email settings\": \"Réglages de l'e-mail\",\n \"Email template library\": \"Banque de modèles emails\",\n \"Event invitation\": \"Invitation\",\n \"Facebook\": \"Facebook\",\n \"Favorites\": \"Favoris\",\n \"Fill the section\": \"Remplir la section\",\n \"Get started\": \"Commencer\",\n \"GitHub\": \"GitHub\",\n \"Greets a new customer and lists the first steps\": \"Accueille un nouveau client et liste les premières étapes\",\n \"Heading\": \"Titre\",\n \"Hero banner\": \"Bannière\",\n \"Horizontal\": \"Horizontale\",\n \"HTML copied.\": \"HTML copié.\",\n \"Icon background\": \"Fond de l'icône\",\n \"Icon size\": \"Taille des icônes\",\n \"Icons linking to your profiles\": \"Icônes vers vos profils\",\n \"Image + text\": \"Image + texte\",\n \"Image beside a title, text and button\": \"Image à côté d'un titre, d'un texte et d'un bouton\",\n \"Image position\": \"Position de l'image\",\n \"Image size\": \"Taille de l'image\",\n \"In brief\": \"En bref\",\n \"Instagram\": \"Instagram\",\n \"Large bold title\": \"Grand titre en gras\",\n \"LinkedIn\": \"LinkedIn\",\n \"Logo, headline, text and a button\": \"Logo, titre, texte et un bouton\",\n \"Menu\": \"Menu\",\n \"Menu link\": \"Lien du menu\",\n \"Monthly news: banner, two articles, highlights\": \"Les nouvelles du mois : bannière, deux articles, brèves\",\n \"Network\": \"Réseau\",\n \"Newsletter\": \"Newsletter\",\n \"No preview: this storage cannot render emails.\": \"Pas d'aperçu : ce stockage ne sait pas compiler les e-mails.\",\n \"No template yet. Start from a base, or create a new one.\": \"Aucun modèle pour l’instant. Partez d’une base, ou créez-en un.\",\n \"Nothing matches “{query}”.\": \"Aucun résultat pour « {query} ».\",\n \"Offer valid until Sunday midnight, while stocks last.\": \"Offre valable jusqu'à dimanche minuit, dans la limite des stocks.\",\n \"One sentence about it.\": \"Une phrase pour en parler.\",\n \"Origin\": \"Origine\",\n \"Original size\": \"Taille d'origine\",\n \"Our offices, 1 Main Street\": \"Nos bureaux, 1 rue Principale\",\n \"Picking a template replaces the email being edited.\": \"Choisir un modèle remplace l'e-mail en cours de modification.\",\n \"Product one\": \"Produit un\",\n \"Product two\": \"Produit deux\",\n \"Profile URL\": \"Adresse du profil\",\n \"Promotion\": \"Promotion\",\n \"Read more\": \"Lire la suite\",\n \"Ready-made templates to copy into your campaigns.\": \"Modèles prêts à copier dans vos campagnes.\",\n \"Remove from favorites\": \"Retirer des favoris\",\n \"Replace\": \"Remplacer\",\n \"Replace the current email?\": \"Remplacer l'e-mail actuel ?\",\n \"Reply to this email: a person reads it.\": \"Répondez à cet e-mail : une personne le lit.\",\n \"Row of links to your site\": \"Rangée de liens vers votre site\",\n \"Save the date — seats are limited.\": \"Notez la date — les places sont limitées.\",\n \"Search templates\": \"Rechercher un modèle\",\n \"Seats are limited: book yours now, it takes ten seconds.\": \"Les places sont limitées : réservez la vôtre, cela prend dix secondes.\",\n \"Select a template on the left.\": \"Sélectionnez un modèle à gauche.\",\n \"Shop the sale\": \"Voir les offres\",\n \"Simple\": \"Simple\",\n \"So we can tailor what we send you.\": \"Pour adapter ce que nous vous envoyons.\",\n \"Social link\": \"Réseau social\",\n \"Social links\": \"Réseaux sociaux\",\n \"Space around each icon\": \"Espace autour de chaque icône\",\n \"Start from a template\": \"Partir d'un modèle\",\n \"Start from this base\": \"Partir de cette base\",\n \"Take a tour of what you can do.\": \"Faites le tour de ce que vous pouvez faire.\",\n \"Template deleted.\": \"Modèle supprimé.\",\n \"Template duplicated.\": \"Modèle dupliqué.\",\n \"Templates\": \"Modèles\",\n \"Thank you for joining us. Here is how to get the most out of your account.\": \"Merci de nous avoir rejoints. Voici comment tirer le meilleur de votre compte.\",\n \"The browser refused to copy.\": \"Le navigateur a refusé la copie.\",\n \"The news of the month\": \"Les nouvelles du mois\",\n \"The template and its versions will be deleted.\": \"Le modèle et ses versions seront supprimés.\",\n \"The template preview appears here.\": \"L'aperçu du modèle s'affiche ici.\",\n \"This weekend only, with the code SPRING30.\": \"Ce week-end seulement, avec le code SPRING30.\",\n \"Three steps to get started.\": \"Trois étapes pour bien commencer.\",\n \"Thursday, 7 pm\": \"Jeudi, 19 h\",\n \"Two or three sentences that sum up the story and make people want the rest.\": \"Deux ou trois phrases qui résument l'article et donnent envie de lire la suite.\",\n \"Updated\": \"Modifié\",\n \"Vertical\": \"Verticale\",\n \"Welcome\": \"Bienvenue\",\n \"Welcome aboard!\": \"Bienvenue à bord !\",\n \"Welcome, {name}!\": \"Bienvenue, {name} !\",\n \"What changed, what is coming, and two stories worth your time.\": \"Ce qui a changé, ce qui arrive, et deux articles qui valent le détour.\",\n \"When\": \"Quand\",\n \"Where\": \"Où\",\n \"Whole image\": \"Image entière\",\n \"With the code SPRING30, until Sunday midnight.\": \"Avec le code SPRING30, jusqu'à dimanche minuit.\",\n \"X\": \"X\",\n \"You're invited\": \"On vous invite !\",\n \"YouTube\": \"YouTube\",\n \"{label} — {side}\": \"{label} — {side}\",\n \"{max} test addresses at most\": \"{max} adresses de test au maximum\",\n \"{message} Your version can be saved as a copy.\": \"{message} Votre version peut être enregistrée comme copie.\",\n \"{name} (copy)\": \"{name} (copie)\",\n \"{sent} sent, {failed} failed.\": \"{sent} parti(s), {failed} en échec.\",\n \"{sent} test(s) sent — check your inbox.\": \"{sent} test(s) parti(s) — ouvrez votre messagerie avant de continuer.\",\n \"1 column\": \"1 colonne\",\n \"2 columns\": \"2 colonnes\",\n \"2-column section\": \"Section en 2 colonnes\",\n \"25%\": \"25 %\",\n \"3 columns\": \"3 colonnes\",\n \"3-column section\": \"Section en 3 colonnes\",\n \"50%\": \"50 %\",\n \"600 px (standard)\": \"600 px (standard)\",\n \"640 px\": \"640 px\",\n \"75%\": \"75 %\",\n \"Address not allowed\": \"Adresse refusée\",\n \"Addresses\": \"Adresses\",\n \"Alignment\": \"Alignement\",\n \"Alt text\": \"Texte alternatif\",\n \"An https:// address is required\": \"Une adresse https:// est requise\",\n \"attribute {name} not allowed\": \"attribut {name} interdit\",\n \"Authentication required\": \"Authentification requise\",\n \"Autosave\": \"Sauvegarde automatique\",\n \"Autosave paused\": \"Sauvegarde automatique suspendue\",\n \"Autosaved at {time}\": \"Enregistré automatiquement à {time}\",\n \"Autosaving…\": \"Sauvegarde automatique…\",\n \"Available in every template.\": \"Disponible dans tous les modèles.\",\n \"Back\": \"Retour\",\n \"Background\": \"Fond\",\n \"base tag not allowed\": \"balise base interdite\",\n \"Blocks\": \"Blocs\",\n \"Bold\": \"Gras\",\n \"Bottom\": \"Bas\",\n \"Button\": \"Bouton\",\n \"Button background\": \"Fond du bouton\",\n \"Button padding\": \"Marges du bouton\",\n \"Button without link ({count})\": \"Bouton sans lien ({count})\",\n \"Call to action\": \"Appel à l’action\",\n \"Campaign\": \"Campagne\",\n \"Campaign name\": \"Nom de la campagne\",\n \"Cancel\": \"Annuler\",\n \"CDATA section not allowed\": \"section CDATA interdite\",\n \"Center\": \"Centre\",\n \"Close\": \"Fermer\",\n \"Colors\": \"Couleurs\",\n \"Column\": \"Colonne\",\n \"Company\": \"Société\",\n \"component script not allowed\": \"script de composant interdit\",\n \"conditional comment not allowed\": \"commentaire conditionnel interdit\",\n \"Confirm\": \"Confirmer\",\n \"Content\": \"Contenu\",\n \"Content width\": \"Largeur du contenu\",\n \"Copy saved: “{name}”.\": \"Copie enregistrée : « {name} ».\",\n \"Corner radius\": \"Arrondi\",\n \"Create\": \"Créer\",\n \"Create “{name}”\": \"Créer « {name} »\",\n \"current\": \"actuelle\",\n \"Custom\": \"Personnalisées\",\n \"Custom variables are not configured\": \"Les variables personnalisées ne sont pas configurées\",\n \"Dashed\": \"Tirets\",\n \"Default\": \"Par défaut\",\n \"Delete\": \"Supprimer\",\n \"Delete (Del)\": \"Supprimer (Suppr)\",\n \"Delete the variable “{name}”?\": \"Supprimer la variable « {name} » ?\",\n \"Describes the image if it is blocked\": \"Décrit l’image si elle est bloquée\",\n \"Deselect\": \"Désélectionner\",\n \"Desktop\": \"Bureau\",\n \"Device\": \"Appareil\",\n \"Dismiss\": \"Masquer\",\n \"Divider\": \"Séparateur\",\n \"Dotted\": \"Pointillés\",\n \"Double-click the text to edit it.\": \"Double-cliquez sur le texte pour le modifier.\",\n \"Duplicate\": \"Dupliquer\",\n \"Edit\": \"Modifier\",\n \"Edit “{name}”\": \"Modifier « {name} »\",\n \"Edit variable\": \"Modifier la variable\",\n \"Element actions\": \"Actions sur l’élément\",\n \"Email\": \"Courriel\",\n \"Email address\": \"Adresse e-mail\",\n \"Email preview\": \"Aperçu du courriel\",\n \"embed not allowed\": \"embed interdit\",\n \"Empty file\": \"Fichier vide\",\n \"Empty MJML\": \"MJML vide\",\n \"Empty MJML compilation\": \"Compilation MJML vide\",\n \"Empty subject\": \"Objet vide\",\n \"executable content not allowed\": \"contenu exécutable interdit\",\n \"external resource not allowed\": \"ressource externe interdite\",\n \"File missing\": \"Fichier manquant\",\n \"Final rendering, variables filled with samples.\": \"Rendu final, variables remplies d’exemples.\",\n \"First name\": \"Prénom\",\n \"Fit to text\": \"Ajustée au texte\",\n \"Font\": \"Police\",\n \"forbidden element in project: {tag}\": \"élément interdit dans le projet : {tag}\",\n \"form field not allowed\": \"champ de formulaire interdit\",\n \"form not allowed\": \"formulaire interdit\",\n \"Format rejected: PNG, JPEG or GIF expected\": \"Format refusé : PNG, JPEG ou GIF attendu\",\n \"frame not allowed\": \"frame interdite\",\n \"Full length\": \"Toute la largeur\",\n \"Full width\": \"Pleine largeur\",\n \"Full-width section\": \"Section pleine largeur\",\n \"Give the template a name before saving it.\": \"Donnez un libellé au modèle avant de l’enregistrer.\",\n \"Group\": \"Groupe\",\n \"Height\": \"Hauteur\",\n \"Hello {name},\": \"Bonjour {name},\",\n \"Hex code\": \"Code hexadécimal\",\n \"Horizontal rule\": \"Filet horizontal\",\n \"HTML button not allowed (use the Button block)\": \"bouton HTML interdit (utiliser le bloc Bouton)\",\n \"iframe not allowed\": \"iframe interdite\",\n \"Ignored (invalid): {email}\": \"Ignorée (invalide) : {email}\",\n \"Image\": \"Image\",\n \"image rejected ({name})\": \"image refusée ({name})\",\n \"image rejected ({name}): https only\": \"image refusée ({name}) : https seulement\",\n \"Image too large: {max} MB maximum\": \"Image trop lourde : {max} Mo au maximum\",\n \"Image without alt text ({count})\": \"Texte alternatif manquant ({count} image(s))\",\n \"Image without file ({count})\": \"Image sans fichier ({count})\",\n \"Inbox\": \"Boîte de réception\",\n \"Indicative preview — check it with “Send test”.\": \"Aperçu indicatif — validez avec « Envoyer un test ».\",\n \"inline style rejected\": \"style en ligne refusé\",\n \"Insecure http link ({count})\": \"Lien non sécurisé en http ({count})\",\n \"Insert a variable in the preview text\": \"Insérer une variable dans le texte d’aperçu\",\n \"Insert a variable in the subject\": \"Insérer une variable dans l’objet\",\n \"Internal error\": \"Erreur interne\",\n \"Invalid JSON\": \"JSON invalide\",\n \"Justify\": \"Justifié\",\n \"Key is required\": \"Balise requise\",\n \"Last name\": \"Nom\",\n \"Layout\": \"Mise en page\",\n \"Learn more\": \"Découvrir\",\n \"Leave\": \"Quitter\",\n \"Leave the builder?\": \"Quitter le constructeur ?\",\n \"Left\": \"Gauche\",\n \"Length\": \"Longueur\",\n \"Line color\": \"Couleur du filet\",\n \"Line height\": \"Interligne\",\n \"Line style\": \"Trait\",\n \"Link\": \"Lien\",\n \"Link on click\": \"Lien au clic\",\n \"Link URL\": \"Adresse du lien\",\n \"Links\": \"Liens\",\n \"Loading the builder\": \"Chargement du constructeur\",\n \"Loading…\": \"Chargement…\",\n \"Lowercase letters, digits, \\\"_\\\" and \\\".\\\" only, starting with a letter\": \"Minuscules, chiffres, « _ » et « . » seulement, en commençant par une lettre\",\n \"MathML not allowed\": \"MathML interdit\",\n \"Message is {size} KB: Gmail clips messages over {limit} KB\": \"Message de {size} Ko : Gmail le coupe au-delà de {limit} Ko\",\n \"meta tag not allowed\": \"balise meta interdite\",\n \"Middle\": \"Milieu\",\n \"missing <mj-body>\": \"<mj-body> manquant\",\n \"Missing unsubscribe link\": \"Lien de désinscription absent\",\n \"mj-font (external font) is outside the builder\": \"mj-font (police externe) est hors du constructeur\",\n \"mj-include would read a file from the server\": \"mj-include lirait un fichier du serveur\",\n \"mj-raw (free HTML) is not allowed\": \"mj-raw (HTML libre) est refusé\",\n \"mj-style (free CSS) is outside the builder\": \"mj-style (CSS libre) est hors du constructeur\",\n \"MJML too large ({size} KB, maximum {max} KB)\": \"MJML trop volumineux ({size} ko, maximum {max} ko)\",\n \"Mobile\": \"Mobile\",\n \"Move down\": \"Descendre\",\n \"Move left\": \"Déplacer à gauche\",\n \"Move right\": \"Déplacer à droite\",\n \"Move up\": \"Monter\",\n \"Multipart form expected\": \"Formulaire multipart attendu\",\n \"Name\": \"Libellé\",\n \"Name is required\": \"Libellé requis\",\n \"Name required\": \"Libellé requis\",\n \"New template\": \"Nouveau modèle\",\n \"New variable\": \"Nouvelle variable\",\n \"No changes\": \"Aucune modification\",\n \"No preview text\": \"Aucun texte d’aperçu\",\n \"No subject\": \"Sans objet\",\n \"No usable test address\": \"Aucune adresse de test exploitable\",\n \"No variable found.\": \"Aucune variable trouvée.\",\n \"No versions yet.\": \"Aucune version.\",\n \"Normal\": \"Normale\",\n \"Nothing to fix.\": \"Aucun point à corriger.\",\n \"Nothing to show yet.\": \"Rien à afficher pour l’instant.\",\n \"object not allowed\": \"object interdit\",\n \"Order number\": \"Numéro de commande\",\n \"Padding\": \"Marges\",\n \"Page background\": \"Fond de page\",\n \"Paragraph or heading\": \"Paragraphe ou titre\",\n \"Photo, visual or logo\": \"Photo, visuel ou logo\",\n \"Pick a colour\": \"Choisir une couleur\",\n \"Points to check\": \"Points à vérifier\",\n \"Preview\": \"Aperçu\",\n \"Preview and tests\": \"Aperçu et tests\",\n \"Preview text\": \"Texte d’aperçu\",\n \"Project rejected: {issues}\": \"Projet refusé : {issues}\",\n \"project too deep\": \"projet trop profond\",\n \"project too large ({size} KB, maximum {max} KB)\": \"projet trop volumineux ({size} ko, maximum {max} ko)\",\n \"Properties\": \"Propriétés\",\n \"Recipient\": \"Destinataire\",\n \"Redo\": \"Rétablir\",\n \"Redo (Ctrl+Y)\": \"Rétablir (Ctrl+Y)\",\n \"Refresh\": \"Rafraîchir\",\n \"Rendering…\": \"Rendu en cours…\",\n \"Request too large\": \"Requête trop volumineuse\",\n \"Restore\": \"Recharger\",\n \"Restore version {n}\": \"Recharger la version {n}\",\n \"Restore version {n}?\": \"Recharger la version {n} ?\",\n \"Results\": \"Résultats\",\n \"Resume\": \"Reprendre\",\n \"Resume the draft?\": \"Reprendre le brouillon ?\",\n \"Retry\": \"Réessayer\",\n \"Right\": \"Droite\",\n \"Route not found\": \"Route introuvable\",\n \"Sample value\": \"Valeur d’exemple\",\n \"Save\": \"Enregistrer\",\n \"Save a copy\": \"Enregistrer une copie\",\n \"Save the template first\": \"Enregistrez d’abord le modèle\",\n \"Saved\": \"Enregistré\",\n \"Saved — version {n}.\": \"Enregistré — version {n}.\",\n \"Saved at {time}\": \"Enregistré à {time}\",\n \"Saved, with {count} MJML warning(s): {message}\": \"Enregistré, avec {count} avertissement(s) MJML : {message}\",\n \"Saving…\": \"Enregistrement…\",\n \"script not allowed\": \"script interdit\",\n \"Search variables…\": \"Rechercher une variable…\",\n \"Section\": \"Section\",\n \"Select a text or a button first.\": \"Sélectionnez d’abord un texte ou un bouton.\",\n \"Send\": \"Envoyer\",\n \"Send a test\": \"Envoyer un test\",\n \"Shown after the subject in the inbox\": \"Affiché après l’objet dans la boîte de réception\",\n \"Size\": \"Taille\",\n \"Solid\": \"Plein\",\n \"Spacer\": \"Espace\",\n \"Spacing\": \"Espacement\",\n \"Start over\": \"Partir de zéro\",\n \"style sheet not allowed\": \"feuille de style interdite\",\n \"Subject\": \"Objet\",\n \"SVG not allowed\": \"SVG interdit\",\n \"Tag\": \"Balise\",\n \"tag not allowed: <{tag}>\": \"balise non admise : <{tag}>\",\n \"Template\": \"Modèle\",\n \"Template changed in the meantime\": \"Modèle modifié entre-temps\",\n \"Template changed in the meantime — reload it before saving\": \"Modèle modifié entre-temps — rechargez-le avant d’enregistrer\",\n \"Template changed in the meantime by {user} — reload it before saving\": \"Modèle modifié entre-temps par {user} — rechargez-le avant d’enregistrer\",\n \"Template not found\": \"Modèle introuvable\",\n \"Template rejected: {issues}\": \"Modèle refusé : {issues}\",\n \"Template rejected: {issues} (+{more})\": \"Modèle refusé : {issues} (+{more})\",\n \"Template saved.\": \"Modèle enregistré.\",\n \"Templates that use it will show its “value if empty”.\": \"Les modèles qui l’utilisent afficheront sa « valeur si vide ».\",\n \"Test\": \"Tester\",\n \"Test sending is not configured\": \"L’envoi de test n’est pas configuré\",\n \"Text\": \"Texte\",\n \"The builder could not load.\": \"Le constructeur n’a pas pu se charger.\",\n \"The builder could not start.\": \"Le constructeur n’a pas pu démarrer.\",\n \"the document must start with <mjml>\": \"le document doit commencer par <mjml>\",\n \"The draft will be offered again next time.\": \"Le brouillon reste proposé à la prochaine ouverture.\",\n \"The email on screen, to reviewers’ addresses.\": \"Le courriel à l’écran, aux adresses de relecture.\",\n \"The template could not be loaded.\": \"Le modèle n’a pas pu être chargé.\",\n \"Thickness\": \"Épaisseur\",\n \"This key is already used\": \"Cette balise existe déjà\",\n \"To check\": \"À vérifier\",\n \"Too many test sends — try again later\": \"Trop d’envois de test — réessayez plus tard\",\n \"Too many uploads — try again later\": \"Trop de dépôts d’images — réessayez plus tard\",\n \"Top\": \"Haut\",\n \"Typography\": \"Typographie\",\n \"Undo\": \"Annuler\",\n \"Undo (Ctrl+Z)\": \"Annuler (Ctrl+Z)\",\n \"Unknown variables (rendered empty): {tags}\": \"Variables inconnues (affichées vides) : {tags}\",\n \"Unsaved changes\": \"Modifications non enregistrées\",\n \"Unsaved changes will be lost.\": \"Les modifications non enregistrées seront perdues.\",\n \"Unsaved changes will be replaced.\": \"Les modifications non enregistrées seront remplacées.\",\n \"Unsaved template from {when}.\": \"Modèle non enregistré du {when}.\",\n \"Unsubscribe\": \"Se désinscrire\",\n \"Unsubscribe link\": \"Lien de désinscription\",\n \"Upload\": \"Déposer\",\n \"Upload failed\": \"Dépôt impossible\",\n \"URL rejected ({name})\": \"adresse refusée ({name})\",\n \"Value if empty\": \"Valeur si vide\",\n \"Variable “{name}” created and inserted.\": \"Variable « {name} » créée et insérée.\",\n \"Variable “{name}” created.\": \"Variable « {name} » créée.\",\n \"Variable “{name}” saved.\": \"Variable « {name} » enregistrée.\",\n \"Variable deleted.\": \"Variable supprimée.\",\n \"Variable not found\": \"Variable introuvable\",\n \"Variables\": \"Variables\",\n \"Version {n}\": \"Version {n}\",\n \"Version {n} restored — save to keep it.\": \"Version {n} rechargée — enregistrez pour la garder.\",\n \"Version not found\": \"Version introuvable\",\n \"Versions\": \"Versions\",\n \"Vertical alignment\": \"Alignement vertical\",\n \"Vertical space\": \"Vide vertical\",\n \"Weight\": \"Graisse\",\n \"When a recipient has none\": \"Quand un destinataire n’en a pas\",\n \"Width\": \"Largeur\",\n \"Your company\": \"Votre société\",\n \"Your headline\": \"Titre du message\",\n \"Your text.\": \"Votre texte.\",\n \"“{name}” template applied.\": \"Modèle « {name} » appliqué.\",\n \"€49 instead of €70\": \"49 € au lieu de 70 €\",\n};\n","// @ts-check\n/**\n * Translations.\n *\n * Source strings are written in English directly in the code and double as\n * translation keys (`t('Save')`). A locale is a plain object mapping those\n * English strings to translated ones; a missing entry falls back to English.\n * Placeholders use `{name}`: `t('Saved — version {n}.', { n: 3 })`.\n *\n * The same translator serves the browser (builder interface) and the server\n * (validation and API error messages).\n */\n\nimport { fr } from './fr.js';\n\n/** @typedef {Record<string, string>} Messages */\n/** @typedef {(text: string, params?: Record<string, string|number|null|undefined>) => string} Translate */\n\n/** Built-in locales. English is the source language: an empty dictionary. */\nexport const LOCALES = /** @type {Record<string, Messages>} */ ({ en: {}, fr });\n\n/**\n * @param {Messages} [messages]\n * @returns {Translate}\n */\nexport function createTranslator(messages = {}) {\n return (text, params) => {\n const template = Object.prototype.hasOwnProperty.call(messages, text) ? messages[text] : text;\n if (!params) return template;\n return template.replace(/\\{(\\w+)\\}/g, (match, key) =>\n params[key] === undefined || params[key] === null ? match : String(params[key])\n );\n };\n}\n\n/**\n * Translator for a locale code (`'fr'`, `'fr-FR'`, `'en'`…), optionally\n * extended with application-specific strings.\n *\n * @param {string} [locale]\n * @param {Messages} [overrides]\n * @returns {Translate}\n */\nexport function translatorFor(locale = 'en', overrides = {}) {\n const base = LOCALES[String(locale).toLowerCase()] || LOCALES[String(locale).slice(0, 2).toLowerCase()] || {};\n return createTranslator({ ...base, ...overrides });\n}\n\n/** English, untouched: the default everywhere a translator is optional. */\nexport const identity = createTranslator();\n","// @ts-check\n/**\n * Variables catalog — the merge tags a template can contain.\n *\n * The editor only inserts TAGS (`{{contact.firstname}}`); it never knows a\n * value. Rendering (server side) replaces them:\n *\n * preview, test → sample values (`sample`), the real address for a test;\n * sending → each recipient's values (`applyVariables`), or the merge\n * tags of an email provider (`mapVariables`).\n *\n * A missing value falls back to the tag's inline default\n * (`{{contact.firstname|there}}`), then to the variable's `fallback`, then to\n * an empty string — never to the raw tag.\n *\n * Two kinds of variables:\n * - SYSTEM variables, given by the application (`variables` prop / option),\n * read-only in the interface — this list is the default;\n * - CUSTOM variables, created by users in the builder and kept by the\n * storage (`listVariables` / `saveVariable` / `deleteVariable`), shared by\n * every template.\n *\n * The unsubscribe link is `{{ unsubscribe }}`: the sending pipeline replaces it\n * with the right link (see `applyServiceLinks`).\n *\n * Dependency-free: shared by the browser and the server.\n */\n\nimport { identity } from '../../i18n/index.js';\n\n/**\n * @typedef {{\n * key: string,\n * label: string,\n * group: string,\n * sample: string,\n * fallback?: string,\n * description?: string,\n * token?: string,\n * kind?: 'text'|'link',\n * custom?: boolean,\n * }} EmailVariable\n */\n\n/** @type {EmailVariable[]} */\nexport const DEFAULT_VARIABLES = [\n { key: 'contact.firstname', label: 'First name', group: 'Recipient', sample: 'Jane' },\n { key: 'contact.lastname', label: 'Last name', group: 'Recipient', sample: 'Doe' },\n { key: 'contact.email', label: 'Email address', group: 'Recipient', sample: 'jane.doe@example.com' },\n { key: 'company.name', label: 'Company', group: 'Recipient', sample: 'Example Inc.' },\n { key: 'campaign.name', label: 'Campaign name', group: 'Campaign', sample: 'Newsletter' },\n {\n key: 'unsubscribe',\n label: 'Unsubscribe link',\n group: 'Links',\n sample: '',\n token: '{{ unsubscribe }}',\n kind: 'link',\n },\n];\n\n/** Group of custom variables created without a group. */\nexport const CUSTOM_GROUP = 'Custom';\n\n/** Keys reserved by the sending pipeline. */\nconst RESERVED_KEYS = new Set(['unsubscribe', 'mirror']);\n\nexport const VARIABLE_LIMITS = { key: 60, label: 80, group: 40, sample: 200, fallback: 200, description: 200 };\n\nconst KEY_SHAPE = /^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*$/;\n\n/** Tag inserted by the editor. @param {EmailVariable} variable */\nexport function tokenOf(variable) {\n return variable.token || `{{${variable.key}}}`;\n}\n\n/**\n * @param {string} key\n * @param {EmailVariable[]} [variables]\n */\nexport function findVariable(key, variables = DEFAULT_VARIABLES) {\n return variables.find((v) => v.key === key) || null;\n}\n\n/**\n * A key proposed from a label: \"Order number\" → `order_number`,\n * \"Numéro de commande\" → `numero_de_commande`.\n *\n * @param {string} label\n */\nexport function keyFromLabel(label) {\n const key = String(label || '')\n .normalize('NFD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .replace(/^[^a-z]+/, '')\n .slice(0, VARIABLE_LIMITS.key)\n .replace(/_+$/, '');\n return key || 'variable';\n}\n\n/**\n * Checks an application-provided catalog (keys must be unique and safe to\n * embed in a tag).\n *\n * @param {EmailVariable[]} variables\n * @returns {EmailVariable[]}\n */\nexport function defineVariables(variables) {\n const seen = new Set();\n for (const variable of variables) {\n if (!variable || !KEY_SHAPE.test(variable.key || '')) {\n throw new Error(`variables: invalid key \"${variable?.key}\"`);\n }\n if (seen.has(variable.key)) throw new Error(`variables: duplicate key \"${variable.key}\"`);\n seen.add(variable.key);\n }\n return variables;\n}\n\nconst clean = (value, max) => String(value ?? '').replace(/\\s+/g, ' ').trim().slice(0, max);\n\n/**\n * Validates and normalizes a CUSTOM variable typed by a user.\n * Returns `{ variable }` or `{ errors: { field: message } }`.\n *\n * @param {Partial<EmailVariable>} input\n * @param {{\n * existing?: EmailVariable[],\n * editing?: string|null,\n * t?: import('../../i18n/index.js').Translate,\n * }} [options] `editing`: key of the variable being edited (its key cannot change)\n */\nexport function validateCustomVariable(input, { existing = [], editing = null, t = identity } = {}) {\n /** @type {Record<string, string>} */\n const errors = {};\n const label = clean(input?.label, VARIABLE_LIMITS.label);\n const key = editing || String(input?.key || '').trim();\n if (!label) errors.label = t('Name is required');\n if (!editing) {\n if (!key) errors.key = t('Key is required');\n else if (key.length > VARIABLE_LIMITS.key || !KEY_SHAPE.test(key)) {\n errors.key = t('Lowercase letters, digits, \"_\" and \".\" only, starting with a letter');\n } else if (RESERVED_KEYS.has(key) || existing.some((v) => v.key === key)) {\n errors.key = t('This key is already used');\n }\n }\n if (Object.keys(errors).length) return { errors };\n return {\n variable: {\n key,\n label,\n group: clean(input?.group, VARIABLE_LIMITS.group) || CUSTOM_GROUP,\n sample: clean(input?.sample, VARIABLE_LIMITS.sample),\n fallback: clean(input?.fallback, VARIABLE_LIMITS.fallback),\n description: clean(input?.description, VARIABLE_LIMITS.description),\n custom: true,\n },\n };\n}\n\n/**\n * System variables first, then custom ones (a custom variable never replaces\n * a system one).\n *\n * @param {EmailVariable[]} system\n * @param {EmailVariable[]} custom\n */\nexport function mergeVariables(system, custom = []) {\n const keys = new Set(system.map((v) => v.key));\n return [...system, ...custom.filter((v) => !keys.has(v.key)).map((v) => ({ ...v, custom: true }))];\n}\n","// @ts-check\n/**\n * Service links and uploaded images in a rendered email.\n *\n * `{{ unsubscribe }}` → the unsubscribe link. For a preview or a test it is\n * an INERT link: it can be displayed and clicked but\n * unsubscribes nobody — least of all the person\n * proofreading their test;\n * `{{ mirror }}` → \"view online\" link of some providers, `#` here.\n *\n * UPLOADED IMAGES: a template keeps a RELATIVE path (`/email-assets/<key>`),\n * made absolute at render time with the public URL of the environment that\n * sends. An absolute URL frozen at save time would point to whichever\n * environment saved the template.\n *\n * An uploaded image path is any root-relative path ending with an asset key:\n * 32 hex characters of the content hash and a known extension. The prefix is\n * the application's choice (`assetsPath` option of the server handler).\n */\n\nconst UNSUBSCRIBE = /\\{\\{\\s*unsubscribe\\s*\\}\\}|\\[UNSUBSCRIBE\\]/gi;\nconst MIRROR = /\\{\\{\\s*mirror\\s*\\}\\}/gi;\n\n/** Default public path of uploaded images. */\nexport const DEFAULT_ASSETS_PATH = '/email-assets/';\n\nconst ASSET_PATH_PATTERN = /^\\/(?:[A-Za-z0-9._~-]+\\/)*[a-f0-9]{32}\\.(png|jpg|gif)$/;\nconst RELATIVE_ASSET = /(^|[\"'(=\\s])(\\/(?:[A-Za-z0-9._~-]+\\/)*[a-f0-9]{32}\\.(?:png|jpg|gif))(?=[\"')\\s>]|$)/g;\n\n/** Is this the path of an uploaded image? @param {string} value */\nexport function isAssetPath(value) {\n const path = String(value || '').trim();\n return ASSET_PATH_PATTERN.test(path) && !path.split('/').some((part) => part === '..' || part === '.');\n}\n\n/**\n * Makes uploaded image paths absolute. Without a base URL the HTML is\n * returned as is (preview inside the application, same origin).\n *\n * @param {string} html\n * @param {string|null|undefined} baseUrl\n */\nexport function absoluteAssetUrls(html, baseUrl) {\n const root = String(baseUrl || '').replace(/\\/+$/, '');\n if (!root) return String(html || '');\n return String(html || '').replace(RELATIVE_ASSET, (match, before, path) =>\n isAssetPath(path) ? `${before}${root}${path}` : match\n );\n}\n\nfunction escapeAttribute(text) {\n return String(text ?? '')\n .replace(/&/g, '&amp;')\n .replace(/\"/g, '&quot;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;');\n}\n\n/**\n * @param {string} html\n * @param {{unsubscribeUrl: string}} links\n */\nexport function applyServiceLinks(html, { unsubscribeUrl }) {\n return String(html || '')\n .replace(UNSUBSCRIBE, () => escapeAttribute(unsubscribeUrl))\n .replace(MIRROR, '#');\n}\n","// @ts-check\n/**\n * Images UPLOADED in the builder — one rule shared by the browser (immediate\n * rejection, storage/client.js) and the server (storage/server/assetFile.js).\n *\n * PNG, JPEG, GIF: the three formats every email client displays. WebP and SVG\n * do not display in Outlook, and an SVG can carry script.\n * The server reads the type from the file BYTES; the browser, from `file.type`.\n */\n\nimport { identity } from '../i18n/index.js';\n\n/** Maximum image size: beyond it, the email becomes slow to open. */\nexport const MAX_IMAGE_BYTES = 2 * 1024 * 1024;\n\n/** Accepted types → extension of the public key. */\nexport const IMAGE_TYPES = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/gif': 'gif' };\n\n/** Public key: 32 hex characters of the hash + extension. */\nexport const ASSET_KEY_PATTERN = /^[a-f0-9]{32}\\.(png|jpg|gif)$/;\n\nconst SIGNATURES = [\n { type: 'image/png', bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] },\n { type: 'image/jpeg', bytes: [0xff, 0xd8, 0xff] },\n { type: 'image/gif', bytes: [0x47, 0x49, 0x46, 0x38] },\n];\n\n/**\n * Image type detected from the file signature, or `null`.\n * @param {Uint8Array} bytes\n * @returns {{type: string, ext: string}|null}\n */\nexport function detectImage(bytes) {\n if (!bytes || bytes.length < 8) return null;\n const found = SIGNATURES.find((s) => s.bytes.every((b, i) => bytes[i] === b));\n return found ? { type: found.type, ext: IMAGE_TYPES[found.type] } : null;\n}\n\n/**\n * Immediate rejection of a file the server would refuse, or `null`.\n * @param {{type?: string, size?: number}} file\n * @param {{t?: import('../i18n/index.js').Translate}} [options]\n */\nexport function imageUploadError(file, { t = identity } = {}) {\n if (!(String(file?.type || '') in IMAGE_TYPES)) return t('Format rejected: PNG, JPEG or GIF expected');\n if (Number(file?.size || 0) > MAX_IMAGE_BYTES) {\n return t('Image too large: {max} MB maximum', { max: MAX_IMAGE_BYTES / (1024 * 1024) });\n }\n return null;\n}\n"],"mappings":"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","names":[]}