@h-k-dev/angular-email-editor 0.0.0-placeholder.0 → 0.0.1

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 (51) hide show
  1. package/README.md +127 -3
  2. package/fesm2022/h-k-dev-angular-email-editor-actions.mjs +179 -0
  3. package/fesm2022/h-k-dev-angular-email-editor-actions.mjs.map +1 -0
  4. package/fesm2022/h-k-dev-angular-email-editor-address-chip.mjs +285 -0
  5. package/fesm2022/h-k-dev-angular-email-editor-address-chip.mjs.map +1 -0
  6. package/fesm2022/h-k-dev-angular-email-editor-address-input.mjs +686 -0
  7. package/fesm2022/h-k-dev-angular-email-editor-address-input.mjs.map +1 -0
  8. package/fesm2022/h-k-dev-angular-email-editor-address.mjs +489 -0
  9. package/fesm2022/h-k-dev-angular-email-editor-address.mjs.map +1 -0
  10. package/fesm2022/h-k-dev-angular-email-editor-anchor.mjs +64 -0
  11. package/fesm2022/h-k-dev-angular-email-editor-anchor.mjs.map +1 -0
  12. package/fesm2022/h-k-dev-angular-email-editor-attachment-chip.mjs +387 -0
  13. package/fesm2022/h-k-dev-angular-email-editor-attachment-chip.mjs.map +1 -0
  14. package/fesm2022/h-k-dev-angular-email-editor-attachment-chips.mjs +80 -0
  15. package/fesm2022/h-k-dev-angular-email-editor-attachment-chips.mjs.map +1 -0
  16. package/fesm2022/h-k-dev-angular-email-editor-chat-input.mjs +346 -0
  17. package/fesm2022/h-k-dev-angular-email-editor-chat-input.mjs.map +1 -0
  18. package/fesm2022/h-k-dev-angular-email-editor-focus.mjs +68 -0
  19. package/fesm2022/h-k-dev-angular-email-editor-focus.mjs.map +1 -0
  20. package/fesm2022/h-k-dev-angular-email-editor-image.mjs +132 -0
  21. package/fesm2022/h-k-dev-angular-email-editor-image.mjs.map +1 -0
  22. package/fesm2022/h-k-dev-angular-email-editor-material.mjs +124 -0
  23. package/fesm2022/h-k-dev-angular-email-editor-material.mjs.map +1 -0
  24. package/fesm2022/h-k-dev-angular-email-editor-metrics.mjs +243 -0
  25. package/fesm2022/h-k-dev-angular-email-editor-metrics.mjs.map +1 -0
  26. package/fesm2022/h-k-dev-angular-email-editor-palette.mjs +87 -0
  27. package/fesm2022/h-k-dev-angular-email-editor-palette.mjs.map +1 -0
  28. package/fesm2022/h-k-dev-angular-email-editor-proposal.mjs +307 -0
  29. package/fesm2022/h-k-dev-angular-email-editor-proposal.mjs.map +1 -0
  30. package/fesm2022/h-k-dev-angular-email-editor-suggestion-menu.mjs +392 -0
  31. package/fesm2022/h-k-dev-angular-email-editor-suggestion-menu.mjs.map +1 -0
  32. package/fesm2022/h-k-dev-angular-email-editor.mjs +18818 -0
  33. package/fesm2022/h-k-dev-angular-email-editor.mjs.map +1 -0
  34. package/package.json +130 -6
  35. package/styles/_scroll.scss +101 -0
  36. package/types/h-k-dev-angular-email-editor-actions.d.ts +147 -0
  37. package/types/h-k-dev-angular-email-editor-address-chip.d.ts +193 -0
  38. package/types/h-k-dev-angular-email-editor-address-input.d.ts +328 -0
  39. package/types/h-k-dev-angular-email-editor-address.d.ts +125 -0
  40. package/types/h-k-dev-angular-email-editor-anchor.d.ts +44 -0
  41. package/types/h-k-dev-angular-email-editor-attachment-chip.d.ts +271 -0
  42. package/types/h-k-dev-angular-email-editor-attachment-chips.d.ts +48 -0
  43. package/types/h-k-dev-angular-email-editor-chat-input.d.ts +101 -0
  44. package/types/h-k-dev-angular-email-editor-focus.d.ts +40 -0
  45. package/types/h-k-dev-angular-email-editor-image.d.ts +66 -0
  46. package/types/h-k-dev-angular-email-editor-material.d.ts +65 -0
  47. package/types/h-k-dev-angular-email-editor-metrics.d.ts +110 -0
  48. package/types/h-k-dev-angular-email-editor-palette.d.ts +51 -0
  49. package/types/h-k-dev-angular-email-editor-proposal.d.ts +170 -0
  50. package/types/h-k-dev-angular-email-editor-suggestion-menu.d.ts +177 -0
  51. package/types/h-k-dev-angular-email-editor.d.ts +3959 -0
@@ -0,0 +1,3959 @@
1
+ import { Schema, NodeSpec, MarkSpec, ParseRule, TagParseRule, Node, DOMParser, Mark, MarkType, NodeType, ResolvedPos } from 'prosemirror-model';
2
+ import { Command, EditorState, Plugin, PluginKey, Transaction } from 'prosemirror-state';
3
+ import { InputRule } from 'prosemirror-inputrules';
4
+ import * as _h_k_dev_angular_email_editor from '@h-k-dev/angular-email-editor';
5
+ import { AddressRules } from '@h-k-dev/angular-email-editor/address';
6
+ import { EditorView } from 'prosemirror-view';
7
+ import * as i0 from '@angular/core';
8
+
9
+ /** Passed to every extension factory once the schema has been built. */
10
+ interface ExtensionContext {
11
+ schema: Schema;
12
+ /** All extensions of the editor, so aggregators (e.g. a suggestion menu) can introspect them. */
13
+ extensions: readonly Extension[];
14
+ }
15
+ /**
16
+ * A command factory: called with user arguments (e.g. a heading level),
17
+ * returns a ProseMirror {@link Command} ready to run against the editor.
18
+ */
19
+ type CommandFactory = (...args: any[]) => Command;
20
+ interface SuggestionItemBase {
21
+ /** Stable and locale-neutral (`'heading-2'`, `'table'`): what a menu's
22
+ `i18n` labels are keyed by, and what a renderer tracks rows by. Unique
23
+ within its level. */
24
+ id: string;
25
+ /** Shown, and matched first — the menu's `i18n` may translate it. */
26
+ title: string;
27
+ /** Extra strings the query is matched against besides the title. */
28
+ keywords?: string[];
29
+ /** A secondary line for the row — a merge tag's path, a shortcut. */
30
+ detail?: string;
31
+ /** An icon name, for renderers that show one (the demo: Material). */
32
+ icon?: string;
33
+ /** The heading a renderer groups rows under — a stable id (`'blocks'`,
34
+ `'styling'`, `'color'`), worded by the trigger's `sections` (see
35
+ `SuggestionTrigger`). Rows are listed in the order given, so a
36
+ section's rows are listed together, and a renderer shows the heading
37
+ where the section changes. */
38
+ section?: string;
39
+ /** A colour the row shows in an icon's place — a palette's rows. */
40
+ swatch?: string;
41
+ }
42
+ /** A suggestion that acts: picking it deletes the trigger and query text,
43
+ then runs `command` at that spot. The only kind a {@link SuggestionGroup}
44
+ may hold. */
45
+ interface SuggestionCommandItem extends SuggestionItemBase {
46
+ /** Runs after the trigger and query text have been deleted. */
47
+ command: Command;
48
+ /** Whether it can run right now, when the item wants to say. A menu does
49
+ not offer a row that says no; a button for it shows disabled. Absent,
50
+ a menu always offers the row — it never asks a plain command, which a
51
+ host may not have written to the dry-run convention — and a button
52
+ asks the command itself (see {@link isActionEnabled}). */
53
+ isEnabled?: (state: EditorState) => boolean;
54
+ children?: never;
55
+ }
56
+ /**
57
+ * Something an extension can do, declared once and shown wherever a host
58
+ * wants it: a row of a `/` menu, a toolbar button, a bubble menu, a menu
59
+ * item. It is a {@link SuggestionCommandItem} — the same id, words, icon and
60
+ * command — that also knows its state, so a button can show it.
61
+ *
62
+ * An action is permanent and named; a *suggestion* in the narrower sense (a
63
+ * merge tag for `{{fi`, a template, a correction) is an answer to a moment
64
+ * and comes from a {@link SuggestionSource}. Both end in the same row.
65
+ */
66
+ interface EditorAction extends SuggestionCommandItem {
67
+ /** Whether it is on at the selection — a toggle's pressed state. Absent
68
+ for what has no "on": inserting a table, sending. */
69
+ isActive?: (state: EditorState) => boolean;
70
+ }
71
+ /** Whether `action` can run on `state`: its own `isEnabled` when it has
72
+ one, else the command is asked the ProseMirror way — run without a
73
+ dispatch, it only says whether it would apply. An extension's commands
74
+ keep to that; declare `isEnabled` where the answer is too costly to ask
75
+ on every transaction. */
76
+ declare const isActionEnabled: (action: EditorAction, state: EditorState) => boolean;
77
+ /** What a {@link SuggestionSource} is asked for. */
78
+ interface SuggestionRequest {
79
+ /** The search string of the level being asked for. */
80
+ query: string;
81
+ /** `null` for the first page, else the `nextCursor` the previous page
82
+ returned. */
83
+ cursor: string | null;
84
+ /** Aborted once the answer is no longer wanted — a newer query, a
85
+ dismissal, a destroyed editor. Hand it to `fetch` / `HttpClient`. */
86
+ signal: AbortSignal;
87
+ }
88
+ /** One page of suggestions. A non-null `nextCursor` means more pages exist:
89
+ the menu reports `hasMore` and fetches the next on `loadMore()`. */
90
+ interface SuggestionPage<T extends SuggestionItem = SuggestionItem> {
91
+ items: T[];
92
+ nextCursor?: string | null;
93
+ }
94
+ /** A server-backed (or any computed) list of suggestions, asked per query
95
+ and per page; it owns its own matching. A bare array is one page with
96
+ nothing after it. A rejected promise is the menu's `error`. */
97
+ type SuggestionSource<T extends SuggestionItem = SuggestionItem> = (request: SuggestionRequest) => T[] | SuggestionPage<T> | Promise<T[] | SuggestionPage<T>>;
98
+ /**
99
+ * A suggestion that opens a second level: picking it rewrites the query to
100
+ * `<word> ` (its title, lowercased, spaces as hyphens) after the trigger, and
101
+ * whatever is typed after that space searches its children. Two levels, no
102
+ * more — a group's children are command items.
103
+ *
104
+ * `children` is either a list, which the menu filters and ranks like the
105
+ * first level, or a {@link SuggestionSource}.
106
+ */
107
+ interface SuggestionGroup extends SuggestionItemBase {
108
+ /** Shown after `<trigger><word> `, until the group's own query is typed —
109
+ "Search templates…". The second level's counterpart of a trigger's
110
+ `placeholder`, handed to the session's decoration the same way; the
111
+ menu's `i18n` may translate it. */
112
+ placeholder?: string;
113
+ children: readonly SuggestionCommandItem[] | SuggestionSource<SuggestionCommandItem>;
114
+ command?: never;
115
+ }
116
+ /** An entry in a suggestion menu. */
117
+ type SuggestionItem = SuggestionCommandItem | SuggestionGroup;
118
+ interface BaseExtension {
119
+ name: string;
120
+ /** Named commands exposed on the editor, e.g. `editor.commands.toggleBold()`. */
121
+ commands?: (ctx: ExtensionContext) => Record<string, CommandFactory>;
122
+ /** Key bindings. Earlier extensions in the array win over later ones. */
123
+ keymap?: (ctx: ExtensionContext) => Record<string, Command>;
124
+ /** Markdown-style input rules, e.g. `# ` becoming a heading. */
125
+ inputRules?: (ctx: ExtensionContext) => InputRule[];
126
+ /** Arbitrary ProseMirror plugins (decorations, paste handling, ...). */
127
+ plugins?: (ctx: ExtensionContext) => Plugin[];
128
+ /** What this extension can do, for every surface that shows it — a `/`
129
+ menu's rows, a toolbar's buttons. Gathered by {@link extensionActions}. */
130
+ actions?: (ctx: ExtensionContext) => EditorAction[];
131
+ }
132
+ interface NodeExtension extends BaseExtension {
133
+ type: 'node';
134
+ /** Marks this node as the document root. Exactly one extension must set it. */
135
+ topNode?: boolean;
136
+ spec: NodeSpec;
137
+ }
138
+ interface MarkExtension extends BaseExtension {
139
+ type: 'mark';
140
+ spec: MarkSpec;
141
+ }
142
+ /** Behaviour-only extension: history, base keymap, future `/`-command, ... */
143
+ interface FunctionalExtension extends BaseExtension {
144
+ type: 'extension';
145
+ }
146
+ type Extension = NodeExtension | MarkExtension | FunctionalExtension;
147
+ declare const defineNode: (extension: Omit<NodeExtension, "type">) => NodeExtension;
148
+ declare const defineMark: (extension: Omit<MarkExtension, "type">) => MarkExtension;
149
+ declare const defineExtension: (extension: Omit<FunctionalExtension, "type">) => FunctionalExtension;
150
+ /** Every action the editor's extensions declare, in kit order. */
151
+ declare const extensionActions: (ctx: ExtensionContext) => EditorAction[];
152
+ /** The extensions' actions as the rows of a `/` menu — the same list, seen as
153
+ suggestions: `createSuggestionMenu({ trigger: '/', items: extensionSuggestions })`. */
154
+ declare const extensionSuggestions: (ctx: ExtensionContext) => SuggestionItem[];
155
+
156
+ /**
157
+ * Builds a ProseMirror {@link Schema} from a list of extensions.
158
+ * Node and mark order follows the array order, which matters for
159
+ * parse precedence and mark nesting.
160
+ */
161
+ declare function createSchema(extensions: Extension[]): Schema;
162
+
163
+ /**
164
+ * How markup is parsed through the schema.
165
+ *
166
+ * - `email` — the default for everything a host or the source pane hands the
167
+ * editor. Every tag, attribute and CSS property that Apple Mail, Outlook or
168
+ * Gmail applies is recognized and kept **exactly as authored** (see
169
+ * `email-vocabulary.ts`): MJML's output, a hand-built table template, the
170
+ * document envelope, Outlook conditional comments. Text lines, headings and
171
+ * images become real editor nodes carrying their authored attributes;
172
+ * layout structure stays structure. Anything no floor client applies is
173
+ * dropped.
174
+ * - `preserve` — the opt-in for special needs: like `email`, but nothing is
175
+ * dropped for being outside the vocabulary (custom elements, `data-*`,
176
+ * framework attributes, invented CSS). Executable content is still refused.
177
+ * - `repair` — the schema is law: markup is rewritten into the composer's
178
+ * canonical form. The clipboard and the reply/forward/import seeds use it —
179
+ * pasted Word or Docs markup is cleaned, not kept.
180
+ */
181
+ type ParseMode = 'email' | 'preserve' | 'repair';
182
+ /** Which parse rules take part: the `repair` set, or the `authored` set
183
+ shared by `email` and `preserve` (they differ only in the vocabulary
184
+ filter that runs before the parse). */
185
+ type RuleScope = 'repair' | 'authored';
186
+ /** The rule scope a mode parses with. */
187
+ declare function scopeOf(mode: ParseMode): RuleScope;
188
+ /** Custom key on a parse rule restricting it to one scope. */
189
+ declare const PARSE_SCOPE_KEY = "aeeParseScope";
190
+ /** A parse rule that only takes part in the given scope. */
191
+ declare function ruleForScope<T extends ParseRule>(scope: RuleScope, rule: T): T;
192
+ /** The scope a rule is restricted to, if any. */
193
+ declare function ruleScope(rule: ParseRule | TagParseRule): RuleScope | undefined;
194
+
195
+ /** Serializes a document (or any node) to an HTML string, e.g. for the email
196
+ body. A document carrying an envelope (see {@link DocumentEnvelope})
197
+ serializes as a whole document — doctype, head and all. */
198
+ declare function serializeToHTML(doc: Node, schema: Schema): string;
199
+ interface ParseOptions {
200
+ /** `repair` (this function's default — canonical form), `email` (the
201
+ editor's default — the email vocabulary, kept exactly as authored) or
202
+ `preserve` (anything but executable content). See {@link ParseMode}. */
203
+ mode?: ParseMode;
204
+ }
205
+ /**
206
+ * Parses an HTML string into a document conforming to the schema.
207
+ *
208
+ * - `repair` (the default here; paste, seeds, templates): parsing is repair
209
+ * (principle 2). A builder's export leans on its stylesheet, on wrapper
210
+ * tables and on hiding what a client cannot show; the canonical schema
211
+ * reads none of those, so the import pipeline (import-html.ts) folds,
212
+ * unwraps and drops them first.
213
+ * - `email` (the editor's default) and `preserve`: the markup is kept as
214
+ * authored — comments survive as nodes, a document envelope (`<!doctype>`,
215
+ * `<html>`, `<head>`, `<body>` attributes) lands on the document node's
216
+ * `envelope` attribute, and `email` first corrects what another editor
217
+ * stored wrongly (corrections.ts: `mailto://`, Word's `<o:p>`, Bootstrap
218
+ * borders, an inline around blocks, a video embed) and then drops whatever
219
+ * no floor client applies (email-vocabulary.ts).
220
+ *
221
+ * Every mode then shares the structural repairs. Tables are where foreign
222
+ * markup breaks the rules hardest: real mail arrives with rows of unequal
223
+ * length and spans that reach past the grid, and `repairTables` normalizes
224
+ * the composer's own tables so every pure consumer of the parser —
225
+ * `importedDocument`, `replyDocument`, the source pane's round trip — sees
226
+ * the same rectangle the editor would. `{{path}}` tokens in running text
227
+ * become `mergeTag` pills (`promoteMergeTags`): the serialized email carries
228
+ * the raw Handlebars-flavoured text, and parse restores the structured form.
229
+ * Button atoms lose marks the parser painted from their own `font-weight`
230
+ * (`bareButtons`) — the box is already bold.
231
+ */
232
+ declare function parseHTML(html: string, schema: Schema, options?: ParseOptions): Node;
233
+ /**
234
+ * A command inserting an HTML fragment at the selection — a template, a
235
+ * snippet — parsed exactly as {@link parseHTML} parses a whole document, so
236
+ * tables are repaired and merge tags promoted on the way in. Block content
237
+ * dropped into an empty paragraph takes its place.
238
+ */
239
+ declare const insertHTML: (html: string, options?: ParseOptions) => Command;
240
+ /** A command that puts `html` in the whole document's place — an example
241
+ loaded over what is written, as one undoable change; the caret lands at
242
+ its end. A whole document parsed in the `email` or `preserve` mode brings
243
+ its envelope along. */
244
+ declare const replaceHTML: (html: string, options?: ParseOptions) => Command;
245
+
246
+ /**
247
+ * What a whole HTML document needs before the schema parses its body — the
248
+ * two things a builder's export (MJML, Stripo, Mailchimp) leans on that the
249
+ * canonical email HTML never has:
250
+ *
251
+ * 1. **A stylesheet.** The head's `<style>` rules are folded into the
252
+ * elements they match ({@link inlineStyles}), so the mobile-first pattern
253
+ * — `width: 100%` inline, `width: 50% !important` in a `min-width` media
254
+ * query — comes out the way a desktop client draws it.
255
+ * 2. **Wrapper tables.** Every section, column and image sits in a one-cell
256
+ * `role="presentation"` table for Outlook's sake; each one would parse as
257
+ * a table of ours with its content hoisted out beside it. They are
258
+ * unwrapped ({@link unwrapLayoutTables}) — only the builders' own, which
259
+ * the canonical form never emits, and never one that carries a fill or a
260
+ * padding: that is a band, the section node's own.
261
+ * 3. **Hidden elements.** What a builder hides with `display: none` — the
262
+ * trigger of a hamburger menu, a preview text, a desktop-only or
263
+ * mobile-only variant — is dropped ({@link dropHidden}), rather than
264
+ * read as text standing in the message: the schema has no notion of
265
+ * hidden, and the client the import is drawn for would not show it.
266
+ * 4. **Inheritance.** A builder writes the colour, the size, the face and
267
+ * the alignment on a wrapping `<div>` or `<td>` and lets CSS carry them
268
+ * down; the schema reads a paragraph's alignment off the paragraph and
269
+ * a colour off a `<span>`. What an ancestor declares is written down
270
+ * onto the blocks and the runs beneath it ({@link inheritTextStyles}),
271
+ * so it reaches the words the way a client's cascade would.
272
+ */
273
+ /**
274
+ * Drops the comments, and every element hidden with an inline
275
+ * `display: none` (the sheet's having been folded in, a rule's counts too). A builder's export hides
276
+ * the part of a trick the client cannot pull off — the label of an
277
+ * MJML hamburger menu, whose ☰ would otherwise stand in the message as
278
+ * text — and the alternative of a responsive pair; a preview text sits
279
+ * in such a block as well, and goes with it: an email of ours carries no
280
+ * hidden text. Not `visibility` or `mso-hide`: those are a client's
281
+ * concern, not a rendering's.
282
+ */
283
+ /**
284
+ * Drops the whitespace a client never shows: the text nodes of nothing but
285
+ * spaces inside a box whose own `font-size` is 0. A builder kills the gaps
286
+ * between a row of inline-block boxes that way — MJML's navbar links, its
287
+ * grouped columns — and the row is laid out as the boxes abutting, their
288
+ * own padding apart. Read as text, each gap would be a space at the
289
+ * paragraph's size: 13px over four links, and the last one wraps.
290
+ */
291
+ declare function dropZeroSizedWhitespace(root: ParentNode): void;
292
+ declare function dropHidden(root: ParentNode): void;
293
+ /**
294
+ * Writes down, on each element, the `width` its author gave it inline —
295
+ * `data-aee-own-width` — before {@link inlineStyles} folds a sheet's
296
+ * `!important` over it. A column's own width is what says whether it
297
+ * stacks on a phone (`width: 100%`, the mobile-first pattern, capped by a
298
+ * `min-width` rule) or holds its share (`width: 50%`, a builder's grouped
299
+ * columns): folded at 600px both read `50%`, and only the author's own
300
+ * word tells them apart.
301
+ */
302
+ declare function noteOwnWidths(root: ParentNode): void;
303
+ /** The width the import is drawn at: an email's container. A `min-width`
304
+ media query at or below it applies; a `max-width` one below it does not. */
305
+ declare const IMPORT_VIEWPORT_WIDTH = 600;
306
+ interface Declaration {
307
+ name: string;
308
+ value: string;
309
+ important: boolean;
310
+ }
311
+ /**
312
+ * Folds the document's `<style>` rules into inline styles, then drops the
313
+ * blocks. The cascade is the simple one an email tool can afford: a rule
314
+ * applies to what its selector matches, in document order, later rules
315
+ * over earlier ones; an inline declaration stands unless the rule is
316
+ * `!important` (the mobile-first pattern relies on exactly that);
317
+ * specificity is not weighed. Media queries are answered for
318
+ * {@link IMPORT_VIEWPORT_WIDTH}; other at-rules and selectors the document
319
+ * cannot match (`:hover`, `::before`) are skipped.
320
+ */
321
+ declare function inlineStyles(doc: Document, viewportWidth?: number): void;
322
+ interface StyleRule {
323
+ selector: string;
324
+ declarations: Declaration[];
325
+ }
326
+ /** The style rules of a sheet, flattened: those inside a media query that
327
+ holds at `viewportWidth`, in order; other at-rules dropped. */
328
+ declare function parseRules(css: string, viewportWidth: number): StyleRule[];
329
+ /** Whether a media query list holds for a screen `viewportWidth` wide:
330
+ `screen`/`all`, `min-width` and `max-width` in px, `and` between them;
331
+ `not`, `print` and any other feature do not hold. */
332
+ declare function mediaMatches(query: string, viewportWidth: number): boolean;
333
+ /**
334
+ * Takes the builders' wrapper tables out: a one-row, one-cell
335
+ * `role="presentation"` table carrying the attributes the builders write
336
+ * (`cellpadding`, `cellspacing`, `border="0"`) whose cell holds nothing but
337
+ * elements — a column, another table, an image — is replaced by what it
338
+ * holds. A cell written right-to-left (`direction: rtl`, MJML's way to put
339
+ * an image on the right) hands its children over in reverse, which is the
340
+ * order a client draws them in. Innermost first, until none is left. A
341
+ * table with text in its one cell is a table, and stays one; the
342
+ * canonical form's own tables carry none of those attributes.
343
+ */
344
+ declare function unwrapLayoutTables(root: ParentNode): void;
345
+ /** A box's four sides, in px — top, right, bottom, left. */
346
+ type Box = [number, number, number, number];
347
+ /** An element's padding, where it declares one that is not all zeros —
348
+ the CSSOM's longhands, else the attribute's own shorthand. */
349
+ declare function paddingOf(el: HTMLElement): Box | null;
350
+ /** Shares a dissolved box's padding out among the blocks it held, the way
351
+ the box laid them out: the sides on every block, the top on the first,
352
+ the bottom on the last — on top of any padding of their own. A table or
353
+ a list takes none (they read no padding); a paragraph, a heading, a div
354
+ do, and a paragraph's becomes its spacing. */
355
+ declare function distributePadding(box: Box, blocks: HTMLElement[]): void;
356
+ /** Whether an element lays out as a block — a column, a paragraph, a
357
+ table — as against a run of a line (a link, an image, a span). */
358
+ declare function isBlock(el: Element): boolean;
359
+ /** Whether a cell holds a block of its own — what a section's does, as
360
+ against a button's wrapper (one anchor) or an image's (one `<img>`). */
361
+ declare function holdsBlock(cell: Element): boolean;
362
+ /** Whether a cell declares a padding that is not all zeros. */
363
+ declare function isPadded(cell: Element): boolean;
364
+ /** Whether a cell holds a column — a builder's inline-block div, or our
365
+ own centring div — which is what a section's cell holds. */
366
+ declare function holdsColumn(cell: Element): boolean;
367
+ /**
368
+ * Writes down what CSS would inherit. A builder puts `color`, `font-size`,
369
+ * `font-family` and `text-align` on a wrapping `<div>` (MJML's text
370
+ * block), or `align` on a cell, and the words beneath inherit them; the
371
+ * schema reads a colour off a `<span>` and an alignment off the paragraph
372
+ * itself, so the cascade is materialised: each declaring element hands
373
+ * the properties to the blocks under it that declare none of their own
374
+ * (alignment and line height among them), and wraps its runs of inline content in a
375
+ * `<span>` carrying the colour, the size and the face — an anchor's own
376
+ * colour included, which is how a navbar's black links stay black. Top
377
+ * down, so a grandchild takes the nearer ancestor's word. A `font-size`
378
+ * of 0 (a builder's way to kill the gaps between inline-block columns)
379
+ * is not a size and is not passed on.
380
+ */
381
+ declare function inheritTextStyles(root: ParentNode): void;
382
+
383
+ /** Applies every correction to a parsed document, in place. */
384
+ declare function correctStoredMistakes(dom: Document): void;
385
+ /** `mailto://` and `tel://` back to the scheme a client reads; `http://` off
386
+ a link that is one placeholder and nothing else. */
387
+ declare function repairLinkSchemes(root: ParentNode): void;
388
+ /** The page a viewer opens for an embedded player's address. */
389
+ declare function videoPage(src: string): string;
390
+ /** Each embedded player becomes a link to its page, where it stood. */
391
+ declare function embedsAsLinks(root: ParentNode): void;
392
+ /** `<o:p>` replaced by what it holds (an `&nbsp;` keeps an empty line). */
393
+ declare function unwrapOfficeParagraphMarks(root: ParentNode): void;
394
+ /** `Mso…` class names no `<style>` in the document mentions. */
395
+ declare function dropDeadOfficeClasses(dom: Document): void;
396
+ /** Bordered tables whose borders came from a class no stylesheet here
397
+ defines get them inline; declarations the author wrote win. */
398
+ declare function inlineBootstrapTableBorders(dom: Document): void;
399
+ /**
400
+ * `<font color><div>a</div>b</font>` → `<div><font color>a</font></div><font color>b</font>`.
401
+ * Each run of inline children keeps a copy of the wrapper; each block gets
402
+ * one inside it, around its own content — recursively, so a block of blocks
403
+ * hands it on down. An `<hr>` or an empty block takes none.
404
+ */
405
+ declare function lowerInlinesAroundBlocks(root: ParentNode): void;
406
+
407
+ /**
408
+ * Tag classifications shared by the source language (scanner, formatter) and
409
+ * the preserving parser. One vocabulary, so "what is a block" never differs
410
+ * between the pane that formats the source and the pane that parses it.
411
+ */
412
+ /** Elements that never have content and never a closing tag. */
413
+ declare const VOID_TAGS: Set<string>;
414
+ /** Phrasing-level containers: they lay out inline and become marks (or, when
415
+ they wrap blocks, generic containers) in the preserving parse. */
416
+ declare const INLINE_TAGS: Set<string>;
417
+ /** Void elements that lay out inline. */
418
+ declare const INLINE_VOID_TAGS: Set<string>;
419
+ /** Table sections: structure the browser injects and the serializer emits on
420
+ its own — they are walked through, never nodes of their own, wherever the
421
+ surrounding table is canonical. */
422
+ declare const TABLE_SECTION_TAGS: Set<string>;
423
+ /** Tags that make up a document's envelope rather than its content. Gmail
424
+ strips the head anyway, so they are neither content nor a deliverability
425
+ warning — just the envelope MJML-class tooling emits. */
426
+ declare const DOCUMENT_TAGS: Set<string>;
427
+ /** Never preserved, in any mode: executable or embedding content has no
428
+ place in an email and the parse refuses it outright. */
429
+ declare const REFUSED_TAGS: Set<string>;
430
+ /** Whether an element takes part in block layout among its siblings —
431
+ everything that is neither phrasing content nor an inline void. */
432
+ declare function isBlockLevel(tag: string): boolean;
433
+
434
+ /** Attributes as an ordered list of pairs — JSON-plain, so ProseMirror can
435
+ compare node attrs, and ordered, so the round trip keeps authored order. */
436
+ type AttributePairs = [string, string][];
437
+ /**
438
+ * A DOM element with its attributes set **verbatim**. Array output specs run
439
+ * `style` through the CSSOM (`style.cssText = …`), which reformats it and
440
+ * drops every declaration the browser does not know — MJML's `mso-*` hints
441
+ * for Outlook would vanish. Preserved markup therefore builds its own
442
+ * elements; `setAttribute` keeps the string as authored.
443
+ */
444
+ declare function verbatimElement(tag: string, attrs: Iterable<[string, string | null | undefined]>, content: boolean): {
445
+ dom: HTMLElement;
446
+ contentDOM?: HTMLElement;
447
+ };
448
+
449
+ /** Placeholder element the comment pre-pass turns DOM comments into:
450
+ ProseMirror's parser never sees comment nodes, elements it does. */
451
+ declare const COMMENT_TAG = "aee-comment";
452
+ /** An element's attributes, verbatim and in order — minus what must never be
453
+ emitted: event handlers, `srcdoc`, script URLs, styles with script hooks. */
454
+ declare function preservedAttributes(element: Element): AttributePairs;
455
+ /** Comment text with its inner indentation dropped: `<!--[if mso]>\n <xml>`
456
+ and `<!--[if mso]>\n<xml>` are the same comment, and the formatter must
457
+ be free to re-indent inner lines without changing the document. */
458
+ declare function normalizeCommentText(text: string): string;
459
+ /** CSS with every line trimmed and re-indented by brace depth — idempotent,
460
+ whitespace-only, so the head styles are a byte-stable fixpoint no matter
461
+ how the source pane indents them. */
462
+ declare function normalizeCss(css: string): string[];
463
+ /** Turns every comment under `root` into a placeholder element the parse
464
+ rules can see. Runs on the throwaway DOM a parse builds, never on live DOM. */
465
+ declare function materializeComments(root: globalThis.Node): void;
466
+ /**
467
+ * Drops the edge whitespace inside phrasing elements that are displayed as
468
+ * boxes (`<a style="display: inline-block">` — MJML's navbar links and
469
+ * buttons come as `<a> home </a>`). A box drops the whitespace at the start
470
+ * and end of its own line boxes, so this is rendering-neutral in a client —
471
+ * but the editor view keeps whitespace (`pre-wrap`, as an editor must), where
472
+ * those spaces would take width and wrap a navbar the client keeps on one
473
+ * line. Runs on the throwaway DOM a parse builds.
474
+ */
475
+ declare function trimInlineBoxes(root: Element): void;
476
+ /**
477
+ * Whether an element takes part in block layout among its siblings: a
478
+ * block-level tag, or a phrasing element wrapping blocks (`<a href><table>`).
479
+ * Images are inline (the composer's image node is an inline atom), so a
480
+ * `td > img` is a cell holding an image, and `td > a > img` a linked image.
481
+ */
482
+ declare function isBlockish(element: Element): boolean;
483
+ /** Whether an element lays its children out as blocks (any block-ish child). */
484
+ declare function hasBlockChildren(element: Element): boolean;
485
+ /** Replaces emitted comment placeholders under `root` with comment nodes. */
486
+ declare function realizeComments(root: Element): void;
487
+ /** A foreign block container — anything holding block-level children. */
488
+ declare const HtmlElement: _h_k_dev_angular_email_editor.NodeExtension;
489
+ /** A structural block holding inline content — a `<td>` with a link (MJML's
490
+ button cell), a `<center>` or `<label>`-free text container. Text lines
491
+ (`div`, `p`) and headings are the composer's own nodes instead; see
492
+ `authored.ts`. Enter inserts a line break here (see {@link AuthoredEnter})
493
+ — splitting a table cell would add a cell. */
494
+ declare const HtmlTextElement: _h_k_dev_angular_email_editor.NodeExtension;
495
+ /** A foreign void element among inline content (`<img>` in a cell, `<input>`). */
496
+ declare const HtmlVoid: _h_k_dev_angular_email_editor.NodeExtension;
497
+ /** A foreign void element among blocks (`<hr class>`, an `<input>` between divs). */
498
+ declare const HtmlVoidBlock: _h_k_dev_angular_email_editor.NodeExtension;
499
+ /** A comment between blocks — MJML's Outlook conditionals live here. Hidden
500
+ in the editor (a browser renders nothing for it either); the serializer
501
+ emits a real comment node. */
502
+ declare const HtmlComment: _h_k_dev_angular_email_editor.NodeExtension;
503
+ /** A comment among inline content. */
504
+ declare const HtmlCommentInline: _h_k_dev_angular_email_editor.NodeExtension;
505
+ /**
506
+ * A `<style>` in the body, text kept raw (whitespace-normalized). Hidden in
507
+ * the editor — a live `<style>` inside the editable DOM would restyle the host
508
+ * app; the `DocumentStyles` extension applies it scoped, like the head's.
509
+ * Also carries the refusals: executable content is never parsed, in any mode,
510
+ * and a body `<style>` dies in repair mode (its text would leak otherwise).
511
+ */
512
+ declare const HtmlRaw: _h_k_dev_angular_email_editor.NodeExtension;
513
+ /** A phrasing element no canonical mark claims (`<span class="…">`,
514
+ `<label>`, `<font face>`), kept verbatim. Instances nest freely
515
+ (`excludes: ''`), outermost first, so `<a><span>…` survives as authored. */
516
+ declare const HtmlInline: _h_k_dev_angular_email_editor.MarkExtension;
517
+ /**
518
+ * Enter inside a structural text block ({@link HtmlTextElement}) inserts a
519
+ * line break instead of splitting it: splitting a `<td>` adds a table cell,
520
+ * splitting a `<center>` duplicates a container. Must sit before the kit's
521
+ * `SplitKeepingMarks`, whose split would otherwise claim Enter.
522
+ */
523
+ declare const AuthoredEnter: _h_k_dev_angular_email_editor.FunctionalExtension;
524
+ /** The whole family, in schema order: block nodes, then inline atoms, then
525
+ the mark. Both kits include it; the mode decides whether it takes part. */
526
+ declare const preserveExtensions: (_h_k_dev_angular_email_editor.NodeExtension | _h_k_dev_angular_email_editor.MarkExtension)[];
527
+ /**
528
+ * The schema's parser for a mode. `repair` is the plain schema parser minus
529
+ * the authored rules. `email` and `preserve` share one parser (they differ
530
+ * only in the vocabulary filter applied before it runs): it drops style
531
+ * rules (they would mark authored text up — `font-weight: bold` on an
532
+ * authored `<div>` must stay a style, not become a `<strong>`), lets the
533
+ * nodes that carry authored markup claim theirs, strictifies every other
534
+ * canonical node rule, and lets the preserve family take the rest.
535
+ */
536
+ declare function createDOMParser(schema: Schema, mode: ParseMode): DOMParser;
537
+
538
+ interface DocumentEnvelope {
539
+ /** The doctype as re-emitted (`<!doctype html>`), or null when absent. */
540
+ doctype: string | null;
541
+ /** Attributes of `<html>`, in authored order. */
542
+ html: AttributePairs;
543
+ /** The head in canonical line form, `\n`-joined, unindented; `''` when empty. */
544
+ head: string;
545
+ /** Attributes of `<body>`, in authored order. */
546
+ body: AttributePairs;
547
+ }
548
+ /** Whether HTML carries a document envelope rather than being a bare body
549
+ fragment — a doctype or an explicit `html`/`head`/`body` tag. */
550
+ declare function hasDocumentEnvelope(html: string): boolean;
551
+ /** Parses HTML into a throwaway document. A bare fragment is parsed inside
552
+ an explicit `<body>`: otherwise the HTML parser files a leading comment
553
+ or `<style>` under the head, and body-only consumers would lose it. */
554
+ declare function parseDocument(html: string): Document;
555
+ /** Reads the envelope off a parsed document. */
556
+ declare function readEnvelope(dom: Document): DocumentEnvelope;
557
+ /**
558
+ * The head, one child per line: `<title>`/`<meta>`/`<link>` on a line each,
559
+ * `<style>` with its CSS trimmed and re-indented by nesting, comments with
560
+ * their inner indentation dropped. Whitespace-only normalization, idempotent —
561
+ * so the head string is a fixpoint of parse → serialize → format → parse.
562
+ * Executable content (`<script>`, `<base>`) never makes it in.
563
+ */
564
+ declare function canonicalHead(head: Element | null): string;
565
+ /** The canonical document: envelope lines around the zero-whitespace body. */
566
+ declare function serializeDocument(envelope: DocumentEnvelope, body: string): string;
567
+ /** An opening tag with escaped attribute values; a valueless attribute is
568
+ written bare (`<td nowrap>`), matching how the browser serializes it. */
569
+ declare function openTag(tag: string, attrs: AttributePairs): string;
570
+ /** One attribute as the source writes it: `name="value"` — bare only for an
571
+ HTML boolean, where the name alone is the value. An empty value anywhere
572
+ else is spelled out (`alt=""` is a decision, not a leftover). */
573
+ declare function attributeString(name: string, value: string): string;
574
+ /** Whether the attribute is one of HTML's booleans — written bare. */
575
+ declare function isBooleanAttribute(name: string): boolean;
576
+ declare function escapeText(text: string): string;
577
+ declare function escapeAttribute(value: string): string;
578
+
579
+ /**
580
+ * The email vocabulary: every tag, attribute and CSS property that at least
581
+ * one of the floor clients — **Apple Mail, Outlook, Gmail** — applies. This is
582
+ * what the default `email` parse mode recognizes and keeps *exactly as
583
+ * authored*; it is the reason MJML's output survives untouched, because MJML
584
+ * emits nothing a floor client ignores.
585
+ *
586
+ * Curation rule: grounded in caniemail's feature list (a feature supported by
587
+ * any one of the three clients is usable), plus Outlook's own dialect
588
+ * (`mso-*` properties, VML and Office namespaced elements), plus the legacy
589
+ * presentational HTML every email toolchain still relies on (`bgcolor`,
590
+ * `cellpadding`, `align`, …). Anything outside — framework attributes
591
+ * (`ng-*`, `v-*`), `data-*`, typos, invented CSS properties, custom elements —
592
+ * is dropped by the `email` parse and kept only in `preserve` mode, which is
593
+ * the opt-in for special needs.
594
+ *
595
+ * Deliberately static: filtering by what the *running* browser knows
596
+ * (`CSS.supports`, `in style`) would make canonical output depend on the
597
+ * engine — and no browser knows `mso-*`.
598
+ */
599
+ /** Tags applied by at least one floor client. */
600
+ declare const EMAIL_TAGS: Set<string>;
601
+ /** Attributes applied by at least one floor client, on any element — the
602
+ global set, the legacy presentational set, and the per-element ones
603
+ flattened (an `href` on a `<div>` is inert, not harmful). */
604
+ declare const EMAIL_ATTRIBUTES: Set<string>;
605
+ /** CSS properties applied by at least one floor client — caniemail's
606
+ property list plus the universally supported basics it doesn't track. */
607
+ declare const EMAIL_CSS_PROPERTIES: Set<string>;
608
+ /** Whether a tag is applied by at least one floor client. */
609
+ declare function isEmailTag(tag: string): boolean;
610
+ /** Whether an attribute is applied by at least one floor client. Outlook's
611
+ own elements keep all their attributes — VML is its own vocabulary. */
612
+ declare function isEmailAttribute(name: string, tag?: string): boolean;
613
+ /**
614
+ * Whether a CSS property is applied by at least one floor client: a listed
615
+ * property, a vendor-prefixed one (`-webkit-`, `-moz-`, `-ms-`) of a listed
616
+ * or known vendor-only property, Outlook's `mso-*`, or a custom property
617
+ * (Apple Mail supports CSS variables).
618
+ */
619
+ declare function isEmailCssProperty(property: string): boolean;
620
+ /** A style attribute split into declarations at top-level semicolons —
621
+ never inside quotes or parentheses, so `url(data:image/png;base64,…)`
622
+ stays one declaration. Each entry keeps its exact original text. */
623
+ declare function splitDeclarations(style: string): string[];
624
+ /** The property name of one declaration, lower-cased; `null` when the
625
+ declaration has no `:` (empty or malformed). */
626
+ declare function declarationProperty(declaration: string): string | null;
627
+ /**
628
+ * A style attribute with every declaration outside the email vocabulary
629
+ * removed. **Byte-identical when nothing is dropped** — the kept declarations
630
+ * keep their exact text and separators, so authored styles round-trip as
631
+ * written. Returns the dropped property names alongside.
632
+ */
633
+ declare function filterEmailStyle(style: string): {
634
+ style: string;
635
+ dropped: string[];
636
+ };
637
+ /**
638
+ * Applies the email vocabulary to a throwaway DOM, in place: elements
639
+ * outside it are unwrapped (their content stays, as the repair parse does) —
640
+ * except executable ones, which go with their content, never leaking it as
641
+ * text — attributes outside it are removed, style declarations outside it
642
+ * are removed. Comments and text are untouched. Runs before the `email` parse.
643
+ */
644
+ declare function applyEmailVocabulary(root: Element): void;
645
+
646
+ /**
647
+ * Authored markup on the composer's own nodes and marks.
648
+ *
649
+ * In the `email` parse mode every node and mark the schema knows recognizes
650
+ * the markup its parse rule accepts — MJML's styled `<div>` is a paragraph, a
651
+ * hand-written `<table>` a table, `<b class="x">` bold — while the element's
652
+ * tag and attributes ride along **exactly as authored** in an `html` attr.
653
+ * For a node whose rendering is several elements deep (a table and its
654
+ * `<tbody>`, a section's band), every element on the way to the content is
655
+ * kept.
656
+ *
657
+ * The rule for rendering is one sentence: **verbatim until edited, then the
658
+ * edit patched in.** While the modelled attributes are what the parse read,
659
+ * the authored elements render as written. Once a command changes them (an
660
+ * alignment, a cell fill, a link's href), the node's canonical rendering is
661
+ * computed twice — for what was parsed and for what is now — and only that
662
+ * difference is applied to the authored markup: an attribute set or removed,
663
+ * a style declaration replaced in place, appended or dropped. Everything the
664
+ * edit did not touch stays byte for byte.
665
+ *
666
+ * `html` is `null` for the composer's own canonical output and for everything
667
+ * a command creates, which is why the golden corpus is unaffected.
668
+ *
669
+ * Installed once, for every node and mark, by the schema builder
670
+ * ({@link withAuthoredMarkup}); a spec opts out with `authoredMarkup: false`.
671
+ */
672
+
673
+ interface AuthoredElement {
674
+ /** Lower-cased tag name as authored. */
675
+ tag: string;
676
+ /** Attributes verbatim, in authored order. */
677
+ attrs: AttributePairs;
678
+ }
679
+ interface AuthoredMarkup {
680
+ /** The authored elements from the node's outer element down to the one
681
+ holding its content — one entry for most nodes. */
682
+ path: AuthoredElement[];
683
+ /** The modelled attributes as the parse read them (`html` excluded): the
684
+ baseline an edit is measured against. */
685
+ parsed: Record<string, unknown>;
686
+ }
687
+ /** The attribute holding authored markup, on every node and mark. */
688
+ declare const AUTHORED_ATTR = "html";
689
+ /** Spec key: `false` opts a node or mark out of authored markup (the
690
+ preserve family, which is verbatim by construction). */
691
+ declare const AUTHORED_SPEC_KEY = "authoredMarkup";
692
+ /** Spec key: style values to write instead of *removing* a declaration an
693
+ edit took away — `{ 'text-align': 'left' }` on the paragraph, because an
694
+ authored line usually sits in a container with an alignment of its own
695
+ (MJML cells centre) and a removed declaration would inherit that. */
696
+ declare const AUTHORED_STYLE_DEFAULTS_KEY = "authoredStyleDefaults";
697
+ type Subject = Node | Mark;
698
+ declare function authoredMarkupOf(subject: Subject): AuthoredMarkup | null;
699
+ /** A node or mark spec with the `html` attribute and authored rendering —
700
+ applied by `createSchema` to every spec that does not opt out. */
701
+ declare function withAuthoredMarkup<T extends NodeSpec | MarkSpec>(spec: T): T;
702
+ /** The node's (or mark's) canonical rendering, authored markup ignored —
703
+ what the parse compares an element against. `emit` picks the email's
704
+ rendering over the editor view's. */
705
+ declare function canonicalRender(subject: Subject, emit: boolean): {
706
+ dom: globalThis.Node;
707
+ contentDOM?: HTMLElement;
708
+ } | null;
709
+ /** The elements from a rendering's outer element down to its content hole
710
+ (or the outer element alone for a leaf). */
711
+ declare function renderedPath(rendered: {
712
+ dom: globalThis.Node;
713
+ contentDOM?: HTMLElement;
714
+ }): Element[] | null;
715
+ /** The value of one attribute, or null. */
716
+ declare function authoredAttribute(markup: AuthoredElement, name: string): string | null;
717
+ /**
718
+ * Sets one CSS declaration in the markup's `style`, touching nothing else:
719
+ * an existing declaration keeps its place and spacing, a new one is appended
720
+ * in the style's own punctuation (`a:b;` stays compact, `a: b;` stays spaced).
721
+ */
722
+ declare function withStyleDeclaration(markup: AuthoredElement, property: string, value: string): AuthoredElement;
723
+ /** Removes one CSS declaration from the markup's `style` (and the attribute
724
+ when nothing is left). */
725
+ declare function withoutStyleDeclaration(markup: AuthoredElement, property: string): AuthoredElement;
726
+ /** Sets one attribute, in place when present, appended otherwise. */
727
+ declare function withAttribute(markup: AuthoredElement, name: string, value: string): AuthoredElement;
728
+ declare function withoutAttribute(markup: AuthoredElement, name: string): AuthoredElement;
729
+ /** The markup of a block split off an authored one: everything inherited
730
+ but the attributes that must stay unique (`id`). */
731
+ declare function inheritedMarkup(markup: AuthoredMarkup): AuthoredMarkup;
732
+ /**
733
+ * For a NodeView that builds its own elements (the table's scroll wrapper,
734
+ * the columns' resize box): the attributes the view should put on the
735
+ * element standing for path entry `index`, or null when the node carries no
736
+ * authored markup — then the view keeps its canonical attributes. Classes the
737
+ * view sets itself are kept alongside the authored ones by
738
+ * {@link applyAuthoredAttributes}.
739
+ */
740
+ declare function authoredViewAttributes(node: Node, index: number): AttributePairs | null;
741
+ /** Puts a node's authored attributes on a view-built element: every other
742
+ attribute goes, except the view's own classes, which stay beside the
743
+ authored ones. No authored markup: nothing changes. */
744
+ declare function applyAuthoredAttributes(element: HTMLElement, node: Node, index: number): void;
745
+
746
+ /**
747
+ * MJML's "Worldly" reference template, compiled — the target the preserving
748
+ * parse is measured against: a document envelope with head styles and media
749
+ * queries, Outlook conditional comments in head and body, nested layout
750
+ * tables with legacy attributes, VML inside conditionals, a checkbox-driven
751
+ * navbar, images with `height="auto"`. It must survive the round trip and
752
+ * render identically in every pane.
753
+ */
754
+ declare const MJML_WORLDLY_HTML = "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n\n<head>\n <title></title>\n <!--[if !mso]><!-->\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n <!--<![endif]-->\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <style type=\"text/css\">\n #outlook a {\n padding: 0;\n }\n\n body {\n margin: 0;\n padding: 0;\n -webkit-text-size-adjust: 100%;\n -ms-text-size-adjust: 100%;\n }\n\n table,\n td {\n border-collapse: collapse;\n mso-table-lspace: 0pt;\n mso-table-rspace: 0pt;\n }\n\n img {\n border: 0;\n height: auto;\n line-height: 100%;\n outline: none;\n text-decoration: none;\n -ms-interpolation-mode: bicubic;\n }\n\n p {\n display: block;\n margin: 13px 0;\n }\n </style>\n <!--[if mso]>\n <noscript>\n <xml>\n <o:OfficeDocumentSettings>\n <o:AllowPNG/>\n <o:PixelsPerInch>96</o:PixelsPerInch>\n </o:OfficeDocumentSettings>\n </xml>\n </noscript>\n <![endif]-->\n <!--[if lte mso 11]>\n <style type=\"text/css\">\n .mj-outlook-group-fix { width:100% !important; }\n </style>\n <![endif]-->\n <!--[if !mso]><!-->\n <link href=\"https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700\" rel=\"stylesheet\" type=\"text/css\">\n <style type=\"text/css\">\n @import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);\n </style>\n <!--<![endif]-->\n <style type=\"text/css\">\n @media only screen and (min-width:480px) {\n .mj-column-per-100 {\n width: 100% !important;\n max-width: 100%;\n }\n\n .mj-column-per-50 {\n width: 50% !important;\n max-width: 50%;\n }\n\n .mj-column-per-33 {\n width: 33% !important;\n max-width: 33%;\n }\n\n .mj-column-per-33-33333333333333 {\n width: 33.33333333333333% !important;\n max-width: 33.33333333333333%;\n }\n }\n </style>\n <style media=\"screen and (min-width:480px)\">\n .moz-text-html .mj-column-per-100 {\n width: 100% !important;\n max-width: 100%;\n }\n\n .moz-text-html .mj-column-per-50 {\n width: 50% !important;\n max-width: 50%;\n }\n\n .moz-text-html .mj-column-per-33 {\n width: 33% !important;\n max-width: 33%;\n }\n\n .moz-text-html .mj-column-per-33-33333333333333 {\n width: 33.33333333333333% !important;\n max-width: 33.33333333333333%;\n }\n </style>\n <style type=\"text/css\">\n @media only screen and (max-width:479px) {\n table.mj-full-width-mobile {\n width: 100% !important;\n }\n\n td.mj-full-width-mobile {\n width: auto !important;\n }\n }\n\n noinput.mj-menu-checkbox {\n display: block !important;\n max-height: none !important;\n visibility: visible !important;\n }\n\n @media only screen and (max-width:479px) {\n .mj-menu-checkbox[type=\"checkbox\"]~.mj-inline-links {\n display: none !important;\n }\n\n .mj-menu-checkbox[type=\"checkbox\"]:checked~.mj-inline-links,\n .mj-menu-checkbox[type=\"checkbox\"]~.mj-menu-trigger {\n display: block !important;\n max-width: none !important;\n max-height: none !important;\n font-size: inherit !important;\n }\n\n .mj-menu-checkbox[type=\"checkbox\"]~.mj-inline-links>a {\n display: block !important;\n }\n\n .mj-menu-checkbox[type=\"checkbox\"]:checked~.mj-menu-trigger .mj-menu-icon-close {\n display: block !important;\n }\n\n .mj-menu-checkbox[type=\"checkbox\"]:checked~.mj-menu-trigger .mj-menu-icon-open {\n display: none !important;\n }\n }\n </style>\n</head>\n\n<body style=\"word-spacing:normal;background-color:#d2eeff;\">\n <div aria-roledescription=\"email\" role=\"article\" lang=\"und\" dir=\"auto\" style=\"word-spacing:normal;background-color:#d2eeff;\">\n <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:600px;\" width=\"600\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n <div style=\"margin:0px auto;max-width:600px;\">\n <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n <tbody>\n <tr>\n <td style=\"direction:ltr;font-size:0px;padding:0px;padding-bottom:20px;padding-top:20px;text-align:center;\">\n <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"width:600px;\" ><![endif]-->\n <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:top;width:300px;\" ><![endif]-->\n <div class=\"mj-column-per-50 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:50%;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;padding:0px;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n <tbody>\n <tr>\n <td align=\"left\" style=\"font-size:0px;padding:0 20px;word-break:break-word;\">\n <div style=\"font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:11px;line-height:1;text-align:left;color:#000000;\"><a href=\"https://mjml.io\" style=\"text-decoration: none; color: inherit;\">[[HEADLINE]]</a></div>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td><td style=\"vertical-align:top;width:300px;\" ><![endif]-->\n <div class=\"mj-column-per-50 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:50%;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;padding:0px;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n <tbody>\n <tr>\n <td align=\"right\" style=\"font-size:0px;padding:0 20px;word-break:break-word;\">\n <div style=\"font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:11px;line-height:1;text-align:right;color:#000000;\"><a href=\"https://mjml.io\" style=\"text-decoration: none; color: inherit;\">[[PERMALINK_LABEL]]</a></div>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td></tr></table><![endif]-->\n </div>\n <!--[if mso | IE]></td></tr></table><![endif]-->\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td></tr></table><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:600px;\" width=\"600\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;\">\n <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n <tbody>\n <tr>\n <td style=\"direction:ltr;font-size:0px;padding:0px;text-align:center;\">\n <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;padding:0px;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n <tbody>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:10px 0;word-break:break-word;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n <tbody>\n <tr>\n <td style=\"width:105px;\">\n <img alt=\"Worldly logo\" src=\"https://static.mailjet.com/mjml-website/templates/worldly-logo.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;\" width=\"105\" height=\"auto\">\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td></tr></table><![endif]-->\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td></tr></table><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:600px;\" width=\"600\" bgcolor=\"#ffe118\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n <div style=\"background:#ffe118;background-color:#ffe118;margin:0px auto;max-width:600px;\">\n <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffe118;background-color:#ffe118;width:100%;\">\n <tbody>\n <tr>\n <td style=\"direction:ltr;font-size:0px;padding:20px;text-align:center;\">\n <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:560px;\" ><![endif]-->\n <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;padding:0px;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n <tbody>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n <!--[if !mso]><!--><input type=\"checkbox\" id=\"29f3f9bba1fb0a37\" class=\"mj-menu-checkbox\" style=\"display:none !important; max-height:0; visibility:hidden;\"><!--<![endif]-->\n <div class=\"mj-menu-trigger\" style=\"display:none;max-height:0px;max-width:0px;font-size:0px;overflow:hidden;\">\n <label for=\"29f3f9bba1fb0a37\" class=\"mj-menu-label\" style=\"display:block;cursor:pointer;mso-hide:all;-moz-user-select:none;user-select:none;color:#444444;font-size:30px;font-family:Ubuntu, Helvetica, Arial, sans-serif;text-transform:uppercase;text-decoration:none;line-height:30px;padding:10px;\" align=\"center\">\n <span class=\"mj-menu-icon-open\" style=\"mso-hide:all;\"> &#9776; </span>\n <span class=\"mj-menu-icon-close\" style=\"display:none;mso-hide:all;\"> &#8855; </span>\n </label>\n </div>\n <div class=\"mj-inline-links\" style>\n <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" align=\"center\"><tr><td style=\"padding:0 35px;\" class=\"\" ><![endif]-->\n <a class=\"mj-link\" href=\"https://mjml.iohttps://mjml.io\" target=\"_blank\" style=\"display: inline-block; color: #000000; font-family: Ubuntu, Helvetica, Arial, sans-serif; font-size: 12px; font-weight: bold; line-height: 22px; text-decoration: none; text-transform: uppercase; padding: 0 35px;\"> home </a>\n <!--[if mso | IE]></td><td style=\"padding:0 35px;\" class=\"\" ><![endif]-->\n <a class=\"mj-link\" href=\"https://mjml.iohttps://mjml.io\" target=\"_blank\" style=\"display: inline-block; color: #000000; font-family: Ubuntu, Helvetica, Arial, sans-serif; font-size: 12px; font-weight: bold; line-height: 22px; text-decoration: none; text-transform: uppercase; padding: 0 35px;\"> Summer deals </a>\n <!--[if mso | IE]></td><td style=\"padding:0 35px;\" class=\"\" ><![endif]-->\n <a class=\"mj-link\" href=\"https://mjml.iohttps://mjml.io\" target=\"_blank\" style=\"display: inline-block; color: #000000; font-family: Ubuntu, Helvetica, Arial, sans-serif; font-size: 12px; font-weight: bold; line-height: 22px; text-decoration: none; text-transform: uppercase; padding: 0 35px;\"> Our blog </a>\n <!--[if mso | IE]></td><td style=\"padding:0 35px;\" class=\"\" ><![endif]-->\n <a class=\"mj-link\" href=\"https://mjml.iohttps://mjml.io\" target=\"_blank\" style=\"display: inline-block; color: #000000; font-family: Ubuntu, Helvetica, Arial, sans-serif; font-size: 12px; font-weight: bold; line-height: 22px; text-decoration: none; text-transform: uppercase; padding: 0 35px;\"> Follow us </a>\n <!--[if mso | IE]></td></tr></table><![endif]-->\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td></tr></table><![endif]-->\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td></tr></table><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:600px;\" width=\"600\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><v:rect style=\"width:600px;\" xmlns:v=\"urn:schemas-microsoft-com:vml\" fill=\"true\" stroke=\"false\"><v:fill origin=\"0, -0.5\" position=\"0, -0.5\" src=\"https://static.mailjet.com/mjml-website/templates/worldly-hero.jpg\" type=\"frame\" size=\"1,1\" aspect=\"atleast\" /><v:textbox style=\"mso-fit-shape-to-text:true\" inset=\"0,0,0,0\"><![endif]-->\n <div style=\"background:url('https://static.mailjet.com/mjml-website/templates/worldly-hero.jpg') center top / cover no-repeat;background-position:center top;background-repeat:no-repeat;background-size:cover;margin:0px auto;max-width:600px;\">\n <div style=\"line-height:0;font-size:0;\">\n <table align=\"center\" background=\"https://static.mailjet.com/mjml-website/templates/worldly-hero.jpg\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:url('https://static.mailjet.com/mjml-website/templates/worldly-hero.jpg') center top / cover no-repeat;background-position:center top;background-repeat:no-repeat;background-size:cover;width:100%;\">\n <tbody>\n <tr>\n <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:300px;\" ><![endif]-->\n <div class=\"mj-column-per-50 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;padding:0px;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n <tbody>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:10px;word-break:break-word;\">\n <div style=\"font-family:Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif;font-size:28px;line-height:32px;text-align:center;color:#ffffff;\">ARE YOU <br> SEASCAPE?</div>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:300px;\" ><![endif]-->\n <div class=\"mj-column-per-50 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n <tbody>\n <tr>\n <td style=\"vertical-align:bottom;padding:0px;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n <tbody>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:0px;padding-top:180px;word-break:break-word;\">\n <div style=\"font-family:Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif;font-size:28px;line-height:32px;text-align:center;color:#ffffff;\">OR <br> CITYSCAPE?</div>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td></tr></table><![endif]-->\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n <!--[if mso | IE]></v:textbox></v:rect></td></tr></table><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:600px;\" width=\"600\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;\">\n <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n <tbody>\n <tr>\n <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:300px;\" ><![endif]-->\n <div class=\"mj-column-per-50 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;padding:0px;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n <tbody>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n <div style=\"font-family:Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif;font-size:15px;line-height:1;text-align:center;color:#BD8714;\">\n <p>SUNNIEST DESTINATIONS</p>\n </div>\n </td>\n </tr>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n <tbody>\n <tr>\n <td style=\"width:240px;\">\n <img alt=\"Maldives, Corse : -15%\" src=\"https://static.mailjet.com/mjml-website/templates/worldly-offer1.jpg\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;\" width=\"240\" height=\"auto\">\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:20px 25px;word-break:break-word;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n <tbody>\n <tr>\n <td align=\"center\" bgcolor=\"#bd8714\" role=\"presentation\" style=\"border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#bd8714;\" valign=\"middle\">\n <a href=\"https://mjml.io\" style=\"display: inline-block; background: #bd8714; color: #FFFFFF; font-family: Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; line-height: 120%; margin: 0; text-decoration: none; text-transform: none; padding: 10px 25px; mso-padding-alt: 0px; border-radius: 3px;\" target=\"_blank\"> BOOK NOW </a>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n <tbody>\n <tr>\n <td style=\"width:240px;\">\n <img alt=\"Tahiti, Bora Bora : -10 %\" src=\"https://static.mailjet.com/mjml-website/templates/worldly-offer2.jpg\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;\" width=\"240\" height=\"auto\">\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:20px 25px;word-break:break-word;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n <tbody>\n <tr>\n <td align=\"center\" bgcolor=\"#bd8714\" role=\"presentation\" style=\"border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#bd8714;\" valign=\"middle\">\n <a href=\"https://mjml.io\" style=\"display: inline-block; background: #bd8714; color: #FFFFFF; font-family: Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; line-height: 120%; margin: 0; text-decoration: none; text-transform: none; padding: 10px 25px; mso-padding-alt: 0px; border-radius: 3px;\" target=\"_blank\"> BOOK NOW </a>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:top;width:300px;\" ><![endif]-->\n <div class=\"mj-column-per-50 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;padding:0px;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n <tbody>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n <div style=\"font-family:Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif;font-size:15px;line-height:1;text-align:center;color:#BD8714;\">\n <p>CHEAP CITY TOUR</p>\n </div>\n </td>\n </tr>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n <tbody>\n <tr>\n <td style=\"width:240px;\">\n <img alt=\"Stockholm, Rome : from 162 \u20AC / day\" src=\"https://static.mailjet.com/mjml-website/templates/worldly-offer3.jpg\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;\" width=\"240\" height=\"auto\">\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:20px 25px;word-break:break-word;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n <tbody>\n <tr>\n <td align=\"center\" bgcolor=\"#48b6bf\" role=\"presentation\" style=\"border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#48b6bf;\" valign=\"middle\">\n <a href=\"https://mjml.io\" style=\"display: inline-block; background: #48b6bf; color: #FFFFFF; font-family: Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; line-height: 120%; margin: 0; text-decoration: none; text-transform: none; padding: 10px 25px; mso-padding-alt: 0px; border-radius: 3px;\" target=\"_blank\"> BOOK NOW </a>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n <tbody>\n <tr>\n <td style=\"width:240px;\">\n <img alt=\"New York, London : -30 %\" src=\"https://static.mailjet.com/mjml-website/templates/worldly-offer4.jpg\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;\" width=\"240\" height=\"auto\">\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:20px 25px;word-break:break-word;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n <tbody>\n <tr>\n <td align=\"center\" bgcolor=\"#48b6bf\" role=\"presentation\" style=\"border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#48b6bf;\" valign=\"middle\">\n <a href=\"https://mjml.io\" style=\"display: inline-block; background: #48b6bf; color: #FFFFFF; font-family: Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; line-height: 120%; margin: 0; text-decoration: none; text-transform: none; padding: 10px 25px; mso-padding-alt: 0px; border-radius: 3px;\" target=\"_blank\"> BOOK NOW </a>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td></tr></table><![endif]-->\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td></tr></table><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:600px;\" width=\"600\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;\">\n <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n <tbody>\n <tr>\n <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:300px;\" ><![endif]-->\n <div class=\"mj-column-per-50 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;padding:0px;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n <tbody>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:0 25px;word-break:break-word;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n <tbody>\n <tr>\n <td style=\"width:250px;\">\n <a href=\"https://mjml.io\" target=\"_blank\" style=\"text-decoration: none; color: inherit;\">\n <img alt=\"NEW : Donwload your city guide\" src=\"https://static.mailjet.com/mjml-website/templates/worldly-image1.jpg\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;\" width=\"250\" height=\"auto\">\n </a>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:top;width:300px;\" ><![endif]-->\n <div class=\"mj-column-per-50 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;padding:0px;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n <tbody>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:0 25px;word-break:break-word;\">\n <div style=\"font-family:Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif;font-size:13px;font-weight:bold;line-height:1;text-align:center;color:#000000;\">\n <p>RATE YOUR STAY !</p>\n </div>\n </td>\n </tr>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n <tbody>\n <tr>\n <td style=\"width:131px;\">\n <a href=\"https://mjml.io\" target=\"_blank\" style=\"text-decoration: none; color: inherit;\">\n <img alt=\"Book rate\" src=\"https://static.mailjet.com/mjml-website/templates/worldly-image2.jpg\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;\" width=\"131\" height=\"auto\">\n </a>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td></tr></table><![endif]-->\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td></tr></table><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:600px;\" width=\"600\" bgcolor=\"#4b525c\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n <div style=\"background:#4b525c;background-color:#4b525c;margin:0px auto;max-width:600px;\">\n <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#4b525c;background-color:#4b525c;width:100%;\">\n <tbody>\n <tr>\n <td style=\"direction:ltr;font-size:0px;padding:0px;text-align:center;\">\n <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:198px;\" ><![endif]-->\n <div class=\"mj-column-per-33 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;padding:0px;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n <tbody>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n <tbody>\n <tr>\n <td style=\"width:30px;\">\n <img alt=\"Best companies for your flights\" src=\"https://static.mailjet.com/mjml-website/templates/worldly-icon1.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;\" width=\"30\" height=\"auto\">\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:0 25px;word-break:break-word;\">\n <div style=\"font-family:Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif;font-size:15px;line-height:1;text-align:center;color:#ffffff;\">\n <p> BEST COMPANIES FOR YOUR FLIGHTS </p>\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:top;width:198px;\" ><![endif]-->\n <div class=\"mj-column-per-33 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;padding:0px;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n <tbody>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n <tbody>\n <tr>\n <td style=\"width:29px;\">\n <img alt=\"Insurances included\" src=\"https://static.mailjet.com/mjml-website/templates/worldly-icon2.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;\" width=\"29\" height=\"auto\">\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:0 25px;word-break:break-word;\">\n <div style=\"font-family:Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif;font-size:15px;line-height:1;text-align:center;color:#ffffff;\">\n <p> INSURANCES INCLUDED </p>\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:top;width:198px;\" ><![endif]-->\n <div class=\"mj-column-per-33 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;padding:0px;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n <tbody>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n <tbody>\n <tr>\n <td style=\"width:31px;\">\n <img alt=\"Hotels best selection\" src=\"https://static.mailjet.com/mjml-website/templates/worldly-icon3.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;\" width=\"31\" height=\"auto\">\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:0 25px;word-break:break-word;\">\n <div style=\"font-family:Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif;font-size:15px;line-height:1;text-align:center;color:#ffffff;\">\n <p> HOTELS BEST SELECTION </p>\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td></tr></table><![endif]-->\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td></tr></table><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:600px;\" width=\"600\" bgcolor=\"#4395b1\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n <div style=\"background:#4395b1;background-color:#4395b1;margin:0px auto;max-width:600px;\">\n <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#4395b1;background-color:#4395b1;width:100%;\">\n <tbody>\n <tr>\n <td style=\"direction:ltr;font-size:0px;padding:0px;text-align:center;\">\n <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:200px;\" ><![endif]-->\n <div class=\"mj-column-per-33-33333333333333 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;padding:0px;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n <tbody>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n <tbody>\n <tr>\n <td style=\"width:97px;\">\n <img alt=\"Call us : 0 800 123 456\" src=\"https://static.mailjet.com/mjml-website/templates/worldly-footerlogo.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;\" width=\"97\" height=\"auto\">\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:top;width:200px;\" ><![endif]-->\n <div class=\"mj-column-per-33-33333333333333 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;padding:0px;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n <tbody>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n <tbody>\n <tr>\n <td style=\"width:117px;\">\n <a href=\"https://mjml.io\" target=\"_blank\" style=\"text-decoration: none; color: inherit;\">\n <img alt=\"Meet us : find an agency\" src=\"https://static.mailjet.com/mjml-website/templates/worldly-contact1.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;\" width=\"117\" height=\"auto\">\n </a>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:top;width:200px;\" ><![endif]-->\n <div class=\"mj-column-per-33-33333333333333 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;padding:0px;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n <tbody>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n <tbody>\n <tr>\n <td style=\"width:110px;\">\n <a href=\"https://mjml.io\" target=\"_blank\" style=\"text-decoration: none; color: inherit;\">\n <img alt=\"ASK US : find an expert\" src=\"https://static.mailjet.com/mjml-website/templates/worldly-contact2.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;\" width=\"110\" height=\"auto\">\n </a>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td></tr></table><![endif]-->\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td></tr></table><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:600px;\" width=\"600\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n <div style=\"margin:0px auto;max-width:600px;\">\n <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n <tbody>\n <tr>\n <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;padding:0px;\">\n <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n <tbody>\n <tr>\n <td align=\"center\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n <div style=\"font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:11px;line-height:1;text-align:center;color:#000000;\">[[DELIVERY_INFO]] <p>[[POSTAL_ADDRESS]]</p>\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td></tr></table><![endif]-->\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--[if mso | IE]></td></tr></table><![endif]-->\n </div>\n</body>\n\n</html>\n";
755
+
756
+ type HtmlTokenType = 'delimiter' | 'tagName' | 'attributeName' | 'attributeValue' | 'comment';
757
+ interface HtmlToken {
758
+ type: HtmlTokenType;
759
+ from: number;
760
+ to: number;
761
+ }
762
+ interface HtmlTag {
763
+ /** Lower-cased tag name. */
764
+ name: string;
765
+ kind: 'open' | 'close';
766
+ selfClosing: boolean;
767
+ /** False when the tag never reaches its `>` (e.g. `<div` at end of input). */
768
+ terminated: boolean;
769
+ from: number;
770
+ to: number;
771
+ nameFrom: number;
772
+ nameTo: number;
773
+ }
774
+ interface HtmlScan {
775
+ tokens: HtmlToken[];
776
+ tags: HtmlTag[];
777
+ }
778
+ /** The responsiveness ledger's floor: below it iOS Mail auto-inflates text
779
+ and reflows the layout. The picker never offers less; a hand-typed size
780
+ is linted. */
781
+ declare const MIN_FONT_SIZE = 14;
782
+ /** The ledger's line budget: about this many characters fit a 320px line at
783
+ 14px. A longer run with no break point forces the email to scroll
784
+ sideways. */
785
+ declare const MAX_UNBROKEN_RUN = 40;
786
+ /** How the source is read: the parse mode of the visual editor it feeds.
787
+ `repair` (the default) lints and formats for the canonical import;
788
+ `email` and `preserve` for markup kept as authored — the document
789
+ envelope formats whole, a wide style breaks reversibly, comments are
790
+ content, and `email` warns on whatever the email vocabulary drops. */
791
+ interface SourceOptions {
792
+ mode?: ParseMode;
793
+ }
794
+ interface HtmlDiagnostic {
795
+ from: number;
796
+ to: number;
797
+ severity: 'error' | 'warning';
798
+ message: string;
799
+ }
800
+ /** Tags the composer's own blocks are built from — the autocomplete's
801
+ suggestions. What the parse *accepts* is wider: see `email-vocabulary.ts`. */
802
+ declare const EMAIL_SAFE_TAGS: Set<string>;
803
+ /** Single-pass scanner producing highlight tokens and a tag event stream. */
804
+ declare function scanHTML(source: string): HtmlScan;
805
+ /** Balance-checks the tag stream: unclosed tags, stray closers, closed void
806
+ elements, and an error for a comment that never closes. What else is
807
+ flagged follows the parse mode (see {@link SourceOptions}): in `repair`,
808
+ tags outside the email-safe set and comments the schema drops; in
809
+ `email`, every tag, attribute and CSS property outside the email
810
+ vocabulary — exactly what that parse drops (the document envelope is
811
+ inside it: Gmail strips the head, and that is fine). */
812
+ declare function lintHTML(source: string, scan?: HtmlScan, options?: SourceOptions): HtmlDiagnostic[];
813
+ /** Complete character references (`&amp;`, `&#169;`, `&#xA9;`) — spans that
814
+ edits and selection endpoints must treat as atomic: splitting one breaks
815
+ the reference and changes the decoded text. */
816
+ declare function entitySpans(source: string): [number, number][];
817
+ /** Text regions of the source — everything outside tags and comments. */
818
+ /** The runs of source outside tags and comments — running text, where tokens
819
+ live. Exported for the highlighter. */
820
+ declare function textRegions(source: string, scan: HtmlScan): [number, number][];
821
+ /** Names of tags still open at the end of the source, outermost first.
822
+ Powers `</` auto-completion: the innermost open tag is the last entry. */
823
+ declare function openTags(source: string, scan?: HtmlScan): string[];
824
+ /** What the cursor is in the middle of typing, for autocomplete. */
825
+ type CompletionContext = {
826
+ kind: 'tag';
827
+ query: string;
828
+ } | {
829
+ kind: 'closing';
830
+ query: string;
831
+ } | {
832
+ kind: 'attribute';
833
+ tag: string;
834
+ query: string;
835
+ existing: string[];
836
+ } | {
837
+ kind: 'style-property';
838
+ tag: string;
839
+ query: string;
840
+ };
841
+ /**
842
+ * Derives the completion context at a source offset — like the slash menu,
843
+ * this reads the *document*, not keystrokes, so it survives any way the text
844
+ * got there. Returns `null` when the cursor is in plain prose or a
845
+ * non-`style` attribute value.
846
+ */
847
+ declare function completionContextAt(source: string, offset: number): CompletionContext | null;
848
+ /**
849
+ * Pretty-prints HTML for the source editor: block tags on their own indented
850
+ * lines, inline content kept together. Parses through the browser's DOM, so
851
+ * malformed input comes back auto-corrected — formatting is also repair.
852
+ *
853
+ * A whole document (doctype, `<html>`, `<head>`) formats as one: the head is
854
+ * printed in exactly the canonical line form the parse stores (see
855
+ * `canonicalHead`), so the pretty source and the canonical document can never
856
+ * disagree about it.
857
+ */
858
+ /** The width the formatter optimizes for — Prettier's. Email HTML is built
859
+ for arbitrary line lengths; the formatter only ever adds whitespace the
860
+ parser discards, so a break lands on a space in running text (never
861
+ inside a tag, an attribute value or a `{{ }}` token) or between a tag's
862
+ attributes. A word wider than the line stays whole and overflows. */
863
+ declare const FORMAT_WIDTH = 80;
864
+ declare function formatHTML(html: string, indent?: string, width?: number, options?: SourceOptions): string;
865
+
866
+ /**
867
+ * The dual-contrast rule (we don't fight dark mode): a text color is only
868
+ * offered if it reads against both a white and a near-black background, so
869
+ * it survives light mode and forced dark-mode inversion without a single
870
+ * dark-mode declaration in the output.
871
+ *
872
+ * Honest math note: 4.5:1 (WCAG AA body text) against *both* backgrounds is
873
+ * mathematically empty — no color satisfies it. 3:1, the WCAG threshold for
874
+ * large text and UI components, leaves a band of mid-tones; that band is the
875
+ * palette. Enforcement lives here, at the affordance — colors hand-typed in
876
+ * the HTML source are the author's own responsibility and are never policed.
877
+ */
878
+ /** Light-mode reference background. */
879
+ declare const DUAL_CONTRAST_LIGHT = "#ffffff";
880
+ /** Typical forced-inversion background (Gmail dark mode territory). */
881
+ declare const DUAL_CONTRAST_DARK = "#121212";
882
+ /** WCAG large-text/UI threshold — see the math note above for why not 4.5. */
883
+ declare const DUAL_CONTRAST_MIN_RATIO = 3;
884
+ /** WCAG relative luminance of a `#rrggbb` color. */
885
+ declare function relativeLuminance(hex: string): number;
886
+ /** WCAG contrast ratio between two `#rrggbb` colors, 1..21. */
887
+ declare function contrastRatio(a: string, b: string): number;
888
+ /** Whether a color reads against both the light and the dark reference. */
889
+ declare function passesDualContrast(hex: string): boolean;
890
+ interface PaletteColor {
891
+ name: string;
892
+ value: string;
893
+ /** Offered although it fails its palette's rule — and why. The palette
894
+ tests hold every other swatch to the rule, and this one to failing it,
895
+ so an exception is always a decision on record, never an accident. */
896
+ exception?: string;
897
+ }
898
+ /**
899
+ * The dual-contrast rule for *backgrounds* (principle 9, again). Dark mode
900
+ * hits a fill in two different ways, and a fill must survive both:
901
+ *
902
+ * 1. **Non-transforming dark modes** (Apple Mail, and any surface that only
903
+ * flips the *default text* to near-white while explicit styles stay):
904
+ * near-white text on an untouched pale fill is unreadable. No palette can
905
+ * fix this — the fix is that every fill is **paired with an explicit
906
+ * text colour** inline ({@link fillTextColor}), so the text on a fill
907
+ * never depends on the client's default.
908
+ * 2. **Forced inversion** (Gmail/Outlook dark mode): fill *and* text are
909
+ * recoloured together — a pale fill becomes dark, its near-black text
910
+ * near-white; a dark fill becomes light, its white text dark. The pair
911
+ * must still read after that flip.
912
+ *
913
+ * Unlike the text rule (mid-tones), this selects the **ends**: pale tints
914
+ * paired with near-black text, and near-blacks paired with white. Mid-tone
915
+ * fills fail one side and are excluded — enforcement at the affordance, never
916
+ * policing hand-typed source.
917
+ */
918
+ declare const DUAL_BACKGROUND_MIN_RATIO = 4.5;
919
+ /**
920
+ * The text colour a pale fill carries (highlight span, table cell, column
921
+ * panel) alongside its `background-color`. Near-black, so it reads on every
922
+ * pale fill in light mode and in non-transforming dark modes; its complement
923
+ * is near-white, so forced inversion keeps the pair readable. Emitted as an
924
+ * *artifact of the fill* — parsing absorbs it back to "no explicit colour"
925
+ * (see {@link isFillTextColor}), so it round-trips clean.
926
+ */
927
+ declare const FILL_TEXT_COLOR = "#202124";
928
+ /** {@link FILL_TEXT_COLOR} as the CSSOM serialises it. */
929
+ declare const FILL_TEXT_COLOR_RGB = "rgb(32, 33, 36)";
930
+ /** The text colour a dark fill carries: white — the mirror of
931
+ {@link FILL_TEXT_COLOR}, for the same two dark-mode reasons. */
932
+ declare const FILL_TEXT_COLOR_LIGHT = "#ffffff";
933
+ /** The text a fill is paired with: white on a dark fill, near-black on
934
+ anything else (a colour this cannot read included). */
935
+ declare function fillTextColor(background: string): string;
936
+ /** Whether a parsed `color` is the text paired with `background` (hex or
937
+ CSSOM rgb form) — an emit artifact to absorb, not an authored colour to
938
+ keep. */
939
+ declare function isFillTextColor(raw: string | null | undefined, background: string): boolean;
940
+ /** Whether a fill reads with the text it is paired with ({@link fillTextColor})
941
+ both as-is (light mode and non-transforming dark modes) and after forced
942
+ inversion. */
943
+ declare function passesDualBackground(hex: string): boolean;
944
+ /**
945
+ * The curated background palette: white, pale tints and a near-black, all
946
+ * passing {@link passesDualBackground} — proven by test. Shared by every
947
+ * background affordance (text highlight, table cells, column panels) so fills
948
+ * read the same everywhere and survive inversion. Black carries white text.
949
+ */
950
+ declare const emailBackgroundPalette: PaletteColor[];
951
+ /**
952
+ * The curated text palette: mid-tone hues that pass {@link passesDualContrast}
953
+ * — proven by test, not by promise. This is what a color picker should offer
954
+ * instead of an arbitrary hex input.
955
+ *
956
+ * Black and white are the two exceptions (no neutral near either end passes:
957
+ * the band runs from #616161 to #949494), offered because a writer needs
958
+ * them — black to take coloured text back to plain, white for text on a dark
959
+ * fill.
960
+ */
961
+ declare const emailTextPalette: PaletteColor[];
962
+
963
+ /**
964
+ * The client-support data module (M3): a curated, versioned subset of what
965
+ * caniemail documents about our floor clients. Pure data + lookups — the
966
+ * lint engine, tooltips and (later) autocomplete all read from here.
967
+ *
968
+ * Curation rule: only entries we are confident about, phrased as what
969
+ * *actually happens* in the client. Accuracy beats coverage.
970
+ */
971
+ type EmailClient = 'gmail' | 'outlook-desktop' | 'outlook-web' | 'apple-mail' | 'yahoo';
972
+ declare const CLIENT_LABELS: Record<EmailClient, string>;
973
+ declare function clientList(clients: EmailClient[]): string;
974
+ interface CssSupportIssue {
975
+ /** Lower-cased CSS property this entry describes. */
976
+ property: string;
977
+ /** Restrict to matching values (e.g. display:flex, background:url(…)). */
978
+ valuePattern?: RegExp;
979
+ /** Restrict to these tags (e.g. padding is fine on table cells). */
980
+ onTags?: string[];
981
+ /** Clients where the declaration does not do what it says. */
982
+ ignoredBy: EmailClient[];
983
+ /** What actually happens — this is the tooltip. */
984
+ note: string;
985
+ }
986
+ declare const CSS_SUPPORT: CssSupportIssue[];
987
+ /** Issues that apply to a concrete declaration on a concrete tag. */
988
+ declare function findCssIssues(property: string, value: string, tag: string): CssSupportIssue[];
989
+ /** Attributes worth suggesting per email-safe tag; `'*'` applies to all.
990
+ The schema knows what is legal — autocomplete offers only that. */
991
+ declare const EMAIL_TAG_ATTRIBUTES: Record<string, string[]>;
992
+ /** Style properties safe enough to *suggest*. Anything here that still has
993
+ a CSS_SUPPORT entry (max-width, padding) is legitimate but situational —
994
+ the lint explains the situation when it applies. */
995
+ declare const EMAIL_SAFE_STYLE_PROPERTIES: string[];
996
+ /** Gmail clips messages above ~102 KB — the recipient sees a truncated email
997
+ with a "[View entire message]" link, and tracking of anything below the
998
+ clip is lost. The hardest size constraint in email. */
999
+ declare const GMAIL_CLIP_BYTES: number;
1000
+ interface SizeBudget {
1001
+ bytes: number;
1002
+ limit: number;
1003
+ level: 'ok' | 'warning' | 'error';
1004
+ }
1005
+ /** UTF-8 size of the canonical HTML against the Gmail clipping limit;
1006
+ warning from 80% of the budget, error above it. */
1007
+ declare function emailSizeBudget(html: string): SizeBudget;
1008
+
1009
+ /** The clients a rendering can be drawn for: the three that matter, and
1010
+ that read HTML differently enough to look at one by one. */
1011
+ type RenderingClient = 'apple-mail' | 'gmail' | 'outlook-desktop';
1012
+ declare const RENDERING_CLIENTS: readonly RenderingClient[];
1013
+ interface ClientRenderingOptions {
1014
+ /** Draw the client's dark mode: Gmail's forced inversion, Apple Mail's
1015
+ and Outlook's own dark surfaces. */
1016
+ dark?: boolean;
1017
+ /** The reading pane: a phone draws the message edge to edge (iOS Mail,
1018
+ the Gmail app), a desktop pane keeps a margin round it. Default
1019
+ desktop. */
1020
+ pane?: 'phone' | 'desktop';
1021
+ }
1022
+ /**
1023
+ * The email as a client would draw it — a simulation, in a browser, of what
1024
+ * each client keeps of the HTML and what it drops, from the same
1025
+ * client-support data the linter reads. Not a screenshot service: what the
1026
+ * data says a client ignores is taken out, what only that client reads is
1027
+ * put in, and the client's own surface (its default face, size and colours)
1028
+ * is laid underneath. The result is a whole document for a sandboxed frame.
1029
+ *
1030
+ * - **Apple Mail** reads it all: the document as it is, on Apple's surface.
1031
+ * - **Gmail** drops the comments, the form controls (a hamburger's
1032
+ * checkbox and label), every `<style>` rule whose selector it cannot
1033
+ * match (pseudo-classes, attribute selectors, sibling combinators), and
1034
+ * the declarations the data marks as ignored there (position, flex and
1035
+ * grid, transforms, animation). Its dark mode inverts the colours.
1036
+ * - **Outlook on Windows** reads what the Word engine reads: the
1037
+ * conditional comments meant for it are opened (`[if mso]`, `[if mso |
1038
+ * IE]`, `[if gte mso 9]`) and the parts kept from it are taken out
1039
+ * (`[if !mso]><!-->` … `<!--<![endif]`); media queries go; the
1040
+ * declarations the data marks as ignored go (`max-width`, a padding on
1041
+ * anything but a cell, `border-radius`, background images, floats,
1042
+ * shadows, and the rest), and an `inline-block` block stacks. VML is left
1043
+ * standing — a browser draws none of it, so a picture behind text shows
1044
+ * as the colour beneath, which is what an Outlook without VML shows too.
1045
+ *
1046
+ * Takes a fragment (the editor's canonical HTML) or a whole document (an
1047
+ * import as it was pasted): a document keeps its head — its `<style>`,
1048
+ * subject to the client — with the client's surface laid before it.
1049
+ */
1050
+ declare function renderForClient(html: string, client: RenderingClient, options?: ClientRenderingOptions): string;
1051
+ /** Gmail keeps a rule whose selector it can match — elements, classes,
1052
+ ids, descendants — and drops one with a pseudo-class, an attribute
1053
+ selector or a sibling combinator; media queries it keeps. */
1054
+ declare function keepGmailRules(css: string): string;
1055
+ /** The Word engine reads no media query. */
1056
+ declare function dropMediaQueries(css: string): string;
1057
+
1058
+ /**
1059
+ * The `text/plain` projection of canonical email HTML — the multipart
1060
+ * alternative every well-formed email should carry (spam scoring cares, and
1061
+ * some recipients read it). Blockquotes become `> `, lists become `- ` /
1062
+ * `1. `, links keep their URL, images fall back to their alt text.
1063
+ */
1064
+ declare function emailPlainText(html: string): string;
1065
+
1066
+ interface EmailDocumentOptions {
1067
+ /** The body's language (`en`, `ja`, `de`, …) — screen readers pick their
1068
+ voice from it, and for CJK it picks the glyph set: without `lang="ja"`
1069
+ a client may draw Japanese text with Chinese forms. */
1070
+ lang?: string;
1071
+ /** Text direction; `auto` (the default) lets the first strong character
1072
+ decide, so a right-to-left body renders right-to-left unasked. */
1073
+ dir?: 'ltr' | 'rtl' | 'auto';
1074
+ /** The document `<title>` — usually the subject. Webmail ignores it; the
1075
+ clients that open the message as a page (web views, some Android apps)
1076
+ show it. */
1077
+ title?: string;
1078
+ /** The inbox snippet shown next to the subject. Without it the client
1079
+ takes the first text it finds — often an image's alt or a greeting. */
1080
+ previewText?: string;
1081
+ }
1082
+ /**
1083
+ * The body's head and shell: the fixes every mail client needs, none of which
1084
+ * belongs in the editor's canonical HTML.
1085
+ *
1086
+ * - Outlook on Windows scales everything by the screen DPI unless told the
1087
+ * document is 96 DPI (`o:PixelsPerInch`), and blocks PNGs without
1088
+ * `o:AllowPNG`. The Office XML sits in a conditional comment only Outlook
1089
+ * reads; every other client drops comments.
1090
+ * - iOS enlarges small text unless `-webkit-text-size-adjust` says not to,
1091
+ * and Apple Mail rewraps layouts unless
1092
+ * `x-apple-disable-message-reformatting` is set.
1093
+ * - The body sits in `role="article" aria-roledescription="email"`, which
1094
+ * screen readers announce as the message, carrying `lang` and `dir`.
1095
+ *
1096
+ * Everything the email needs is inline. The head's one `<style>` rule is an
1097
+ * enhancement nothing depends on: on a phone a stacked column takes the
1098
+ * whole width instead of its desktop cap (columns.ts, `STACK_RULE`). A
1099
+ * client that strips the head (Gmail's app with a non-Google account)
1100
+ * keeps the cap — narrower, still whole — and loses nothing else, which is
1101
+ * the same rule the canonical HTML already keeps.
1102
+ *
1103
+ * The result is for the transport, not for the editor: parse and
1104
+ * `emailPlainText` take the fragment `serializeToHTML` returns (the preview
1105
+ * text would otherwise read as body text).
1106
+ */
1107
+ declare function emailDocument(html: string, options?: EmailDocumentOptions): string;
1108
+
1109
+ /**
1110
+ * The inbound message a reply or forward is built from. Everything here is
1111
+ * caller-supplied **data**: the documents render what becomes content — the
1112
+ * reply attribution line, the forwarded-message header block — and
1113
+ * {@link replyEnvelope} reads the addressing to propose who the answer goes
1114
+ * to. Sending stays the host's.
1115
+ *
1116
+ * Address fields are header form — mailboxes separated by commas, a name
1117
+ * holding a comma quoted: `Jane <jane@x>, "Miller, Bob" <bob@x>`.
1118
+ */
1119
+ interface InboundMessage {
1120
+ /** The inbound body as HTML. Preferred over `text` when both exist. */
1121
+ html?: string;
1122
+ /** The `text/plain` part — the content when no HTML part exists. */
1123
+ text?: string;
1124
+ /** Sender, rendered exactly as given ("Jane Doe" or "Jane <jane@x>"). */
1125
+ from?: string;
1126
+ /** Sent date: a `Date` (formatted via `locale`) or a preformatted string. */
1127
+ date?: Date | string;
1128
+ /** The forward header's Subject line, and what a reply's subject answers. */
1129
+ subject?: string;
1130
+ /** The forward header's To line, and who a reply-all copies. */
1131
+ to?: string;
1132
+ /** Who a reply-all copies besides `to`. Never rendered. */
1133
+ cc?: string;
1134
+ /** Where the sender asked answers to go — a reply goes there instead of
1135
+ `from` when it is set. Never rendered. */
1136
+ replyTo?: string;
1137
+ }
1138
+ interface ComposeSeedOptions {
1139
+ /** BCP 47 locale for formatting a `Date`-typed `date`. Default `en-US`.
1140
+ A string `date` is always used verbatim — pass one for full control. */
1141
+ locale?: string;
1142
+ /** The words the seeds write into the document, in the host's language.
1143
+ Each one left out stays English. */
1144
+ labels?: ComposeSeedLabels;
1145
+ }
1146
+ /**
1147
+ * The words a reply or forward writes into the document. Plain text, never
1148
+ * markup — the seeds put them into text nodes, so a sender's `<` stays a `<`.
1149
+ * A host with translation files maps its keys here once:
1150
+ *
1151
+ * ```ts
1152
+ * labels: {
1153
+ * attribution: ({ from, date }) =>
1154
+ * from ? translate.instant('REPLY.WROTE', { from, date }) : null,
1155
+ * forwarded: translate.instant('FORWARD.HEADER'),
1156
+ * from: translate.instant('FORWARD.FROM'),
1157
+ * }
1158
+ * ```
1159
+ */
1160
+ interface ComposeSeedLabels {
1161
+ /** The reply's attribution line, from what is known of the message — the
1162
+ sender as given, the date already formatted for `locale`; either may be
1163
+ missing. A function, not a pattern, because languages order the two
1164
+ differently ("Am {date} schrieb {from}:") and each decides what a
1165
+ partial line says. Null or empty: no line; the quote stands alone.
1166
+ Default: "On {date}, {from} wrote:", "{from} wrote:", "On {date}:". */
1167
+ attribution?: (known: {
1168
+ from?: string;
1169
+ date?: string;
1170
+ }) => string | null;
1171
+ /** The prefix a reply's subject starts with, space included. Default "Re: ". */
1172
+ re?: string;
1173
+ /** The prefix a forward's subject starts with, space included. Default "Fwd: ". */
1174
+ fwd?: string;
1175
+ /** The forward's opening line. Default "---------- Forwarded message ---------". */
1176
+ forwarded?: string;
1177
+ /** The forward header's field names. Default "From", "Date", "Subject", "To". */
1178
+ from?: string;
1179
+ date?: string;
1180
+ subject?: string;
1181
+ to?: string;
1182
+ }
1183
+ /**
1184
+ * The shape modern MIME parsers hand back — postal-mime's `Email`, and
1185
+ * anything structurally like it. Accepted **duck-typed** so this library
1186
+ * never depends on any parser: parsing `.eml` is a solved problem with a
1187
+ * decade of edge-case scar tissue (postal-mime, mailparser), and we don't
1188
+ * compete with it. {@link toInboundMessage} is the whole integration.
1189
+ */
1190
+ interface ParsedEmailLike {
1191
+ html?: string | null;
1192
+ text?: string | null;
1193
+ subject?: string | null;
1194
+ /** ISO string (postal-mime), `Date`, or any human-readable string. */
1195
+ date?: string | Date | null;
1196
+ from?: ParsedAddressLike | null;
1197
+ to?: ParsedAddressLike[] | null;
1198
+ cc?: ParsedAddressLike[] | null;
1199
+ replyTo?: ParsedAddressLike[] | null;
1200
+ }
1201
+ interface ParsedAddressLike {
1202
+ name?: string | null;
1203
+ address?: string | null;
1204
+ /** A group's members (postal-mime's shape): the group's name is no mailbox. */
1205
+ group?: ParsedAddressLike[] | null;
1206
+ }
1207
+ /**
1208
+ * Bridges a parser result into the seeds — the only glue an `.eml` import
1209
+ * needs, front- or backend-parsed alike:
1210
+ *
1211
+ * ```ts
1212
+ * const parsed = await PostalMime.parse(file); // File is a Blob
1213
+ * html.set(importedDocument(toInboundMessage(parsed))); // import it
1214
+ * // …or replyDocument(toInboundMessage(parsed)) to answer it.
1215
+ * ```
1216
+ *
1217
+ * Every field is optional and null-tolerant, so a partial parse still seeds
1218
+ * a sensible document. A parseable date becomes a `Date` (the seeds format
1219
+ * it via Intl with your locale); anything else passes through verbatim.
1220
+ */
1221
+ declare function toInboundMessage(parsed: ParsedEmailLike, options?: {
1222
+ addressRules?: AddressRules;
1223
+ }): InboundMessage;
1224
+ /**
1225
+ * The quoted history of a reply: the attribution line and the inbound
1226
+ * message wrapped in a blockquote — what the editor keeps beside the body
1227
+ * (`createEditor({ quoted })`, `QuotedHistory`), so no template, import or
1228
+ * source edit ever replaces it.
1229
+ *
1230
+ * The history **is** the schema's blockquote — deliberately not a dedicated
1231
+ * node: our output carries no classes, so it would have no honest parse
1232
+ * discriminator against a plain blockquote (Gmail's `class="gmail_quote"`
1233
+ * drops on parse like every class). Keeping it beside the body is what
1234
+ * tells it apart.
1235
+ */
1236
+ declare function replyQuote(inbound: InboundMessage, options?: ComposeSeedOptions): string;
1237
+ /**
1238
+ * The reply as one document — an empty line to type on, then
1239
+ * {@link replyQuote}'s history *in* the body, where it is ordinary text. For
1240
+ * a host without the kept history; with it, seed the body empty and pass
1241
+ * `replyQuote` as `quoted`.
1242
+ */
1243
+ declare function replyDocument(inbound: InboundMessage, options?: ComposeSeedOptions): string;
1244
+ /**
1245
+ * The history of a forward: the conventional forwarded-message header block
1246
+ * (only the lines whose data was supplied), a separating empty line, then
1247
+ * the inbound message **unquoted** — a forward passes the message along, it
1248
+ * doesn't comment on it. Kept beside the body like {@link replyQuote}.
1249
+ */
1250
+ declare function forwardQuote(inbound: InboundMessage, options?: ComposeSeedOptions): string;
1251
+ /** The forward as one document — an empty line, then {@link forwardQuote}'s
1252
+ history in the body. See {@link replyDocument}. */
1253
+ declare function forwardDocument(inbound: InboundMessage, options?: ComposeSeedOptions): string;
1254
+ /**
1255
+ * A body with its quoted history under it — the message as it goes out,
1256
+ * for a host that holds the two as strings (a preview, its own send). A
1257
+ * whole document keeps its head; the history lands at the end of its body.
1258
+ */
1259
+ declare function withQuotedHistory(html: string, quoted: string | null, options?: ParseOptions): string;
1260
+ /** Who an answer is for: the sender, everyone on the message, or — a
1261
+ forward — nobody yet. */
1262
+ type ReplyKind = 'reply' | 'reply-all' | 'forward';
1263
+ interface ReplyEnvelopeOptions extends ComposeSeedOptions {
1264
+ /** The account's own addresses. A reply-all never copies them, and a
1265
+ reply to one's own message goes to the people it went to. */
1266
+ self?: string | readonly string[];
1267
+ /** Who the answer goes to instead of the sender — a CRM's customer, say.
1268
+ A reply-all then copies the sender with everyone else. */
1269
+ to?: readonly string[];
1270
+ /** The address rule to read the headers by — the host's, when it has one
1271
+ (the same it provides to the address input). Ours by default. */
1272
+ addressRules?: AddressRules;
1273
+ }
1274
+ /** The envelope an answer starts with. Addresses in header form, each
1275
+ recipient once — what `[email-address-input]` holds. */
1276
+ interface ReplyEnvelope {
1277
+ to: string[];
1278
+ cc: string[];
1279
+ subject: string;
1280
+ }
1281
+ /**
1282
+ * Who an answer goes to and what it is called — the envelope half of the
1283
+ * seed, beside {@link replyDocument} / {@link forwardDocument}'s body. Pure;
1284
+ * pass the same options to both:
1285
+ *
1286
+ * ```ts
1287
+ * const seed = { locale, labels, self: account.email };
1288
+ * const { to, cc, subject } = replyEnvelope(inbound, 'reply-all', seed);
1289
+ * html.set(replyDocument(inbound, seed));
1290
+ * ```
1291
+ *
1292
+ * - A reply goes to `replyTo` when the message names one, else to `from`;
1293
+ * answering one's own message, to the people it went to.
1294
+ * - A reply-all copies everyone else on the message — sender, To, Cc —
1295
+ * never `self`, never who it is already to; one entry per address, under
1296
+ * the first name it came with.
1297
+ * - A forward addresses nobody yet.
1298
+ * - The subject takes `labels.re` / `labels.fwd` once. A prefix of the same
1299
+ * family the subject already carries is replaced, in any of the languages
1300
+ * below or the host's own ("AW: Re: RE: x" → "Re: x"), numbered repeats
1301
+ * ("Re[2]:") included; the other family's stays, so a forwarded reply
1302
+ * reads "Fwd: Re: x", as every major client writes it.
1303
+ */
1304
+ declare function replyEnvelope(inbound: InboundMessage, kind: ReplyKind, options?: ReplyEnvelopeOptions): ReplyEnvelope;
1305
+ /**
1306
+ * The imported message as the document itself — the `.eml`-drop / paste law:
1307
+ * the body parses through the email schema (full strip, which doubles as
1308
+ * sanitization) and *becomes* the document; nothing else of the message
1309
+ * survives into it. Pair with `parseEml`:
1310
+ * `html.set(importedDocument(parseEml(raw)))`.
1311
+ */
1312
+ declare function importedDocument(inbound: InboundMessage): string;
1313
+ /**
1314
+ * What an import will lose — the *legibility of loss* half of the import law.
1315
+ * The parse itself never reports (it just repairs); this walks the inbound
1316
+ * HTML against the schema's own parse vocabulary and says what won't survive,
1317
+ * so the host can tell the user instead of losing content silently.
1318
+ */
1319
+ interface ImportLoss {
1320
+ /** Elements whose tag the schema has no parse rule for — the element is
1321
+ removed on parse (its text content may still survive, unwrapped). */
1322
+ removedElements: number;
1323
+ /** The distinct removed tags, most frequent first. */
1324
+ removedTags: string[];
1325
+ /** Images pointing at `cid:` MIME parts — they parse in, but stay
1326
+ unresolvable until the attachments story lands. */
1327
+ inlineImages: number;
1328
+ }
1329
+ /** Computes the {@link ImportLoss} for an inbound message — pure, derived
1330
+ from the same HTML `importedDocument` consumes:
1331
+ `const loss = importLoss(inbound)` alongside the import, then surface it
1332
+ ("3 elements outside the schema removed (o:p, font)…"). */
1333
+ declare function importLoss(inbound: InboundMessage): ImportLoss;
1334
+
1335
+ interface EditorOptions {
1336
+ /** Element the editable view is mounted into. */
1337
+ parent: HTMLElement;
1338
+ extensions: Extension[];
1339
+ /** Initial content as HTML. */
1340
+ content?: string;
1341
+ /** The quoted history a reply or forward answers, as HTML (`replyQuote`,
1342
+ `forwardQuote`) — kept beside the body, never replaced with it. See
1343
+ `QuotedHistory`. */
1344
+ quoted?: string | null;
1345
+ /** How `content` and `setContent` parse markup. Default `email`: every
1346
+ tag, attribute and CSS property Apple Mail, Outlook or Gmail applies is
1347
+ recognized — as the kit's own nodes and marks wherever their rules
1348
+ accept it (a table is a table, `<b>` is bold) — and kept exactly as
1349
+ authored (authored.ts); a whole document keeps its head and
1350
+ conditional comments. `preserve` additionally keeps what no floor
1351
+ client applies — the opt-in for special needs. `repair` rewrites into
1352
+ canonical blocks through the import pipeline (import-html.ts), as the
1353
+ paste path always does. */
1354
+ parseMode?: ParseMode;
1355
+ /** DOM attributes for the editable element. */
1356
+ attributes?: Record<string, string>;
1357
+ /** Called after every transaction that changed the document. */
1358
+ onUpdate?: (editor: Editor) => void;
1359
+ }
1360
+ interface Editor {
1361
+ view: EditorView;
1362
+ schema: Schema;
1363
+ readonly state: EditorState;
1364
+ /**
1365
+ * Named commands collected from all extensions, bound to the live view:
1366
+ * `editor.commands.toggleBold()` runs immediately and returns whether it applied.
1367
+ */
1368
+ commands: Record<string, (...args: any[]) => boolean>;
1369
+ /** Every action the editor's extensions declare, in kit order — what a
1370
+ toolbar, a bubble menu or a `/` menu shows. */
1371
+ readonly actions: readonly EditorAction[];
1372
+ /** Runs a raw ProseMirror command against the current state. */
1373
+ exec(command: Command): boolean;
1374
+ /**
1375
+ * Calls `listener` after every change of the editor's state — the text,
1376
+ * the selection, a stored mark — and returns the way to stop. Unlike
1377
+ * `onUpdate` (document changes, given at creation) it can be asked of an
1378
+ * editor that already exists, which is what lets a toolbar bind to one.
1379
+ */
1380
+ subscribe(listener: (editor: Editor) => void): () => void;
1381
+ /** Whether a node or mark with the given name (and attrs) is active at the selection. */
1382
+ isActive(name: string, attrs?: Record<string, any>): boolean;
1383
+ getHTML(): string;
1384
+ /** Replaces the document with the given HTML, applied as a minimal diff:
1385
+ unchanged content keeps its positions, so selection, scroll and plugin
1386
+ state survive. The change counts as an external sync — it never enters
1387
+ the undo history and never fires `onUpdate`, so editors mirroring each
1388
+ other cannot echo. */
1389
+ setContent(html: string): void;
1390
+ /** The quoted history, as canonical HTML, or null — the part of the
1391
+ message {@link getHTML} leaves out and the send intent puts back. */
1392
+ getQuoted(): string | null;
1393
+ /** Replaces the quoted history — sanitized through the schema — or drops
1394
+ it. Same external-sync semantics as {@link setContent}. */
1395
+ setQuoted(html: string | null): void;
1396
+ /** The document as plain text: top-level blocks joined with newlines. */
1397
+ getText(): string;
1398
+ /** Replaces the document with plain text, one default block per line.
1399
+ Same external-sync semantics as {@link setContent}. */
1400
+ setText(text: string): void;
1401
+ focus(): void;
1402
+ destroy(): void;
1403
+ }
1404
+ declare function createEditor(options: EditorOptions): Editor;
1405
+ /**
1406
+ * The selected node that carries `type` as an attribute instead of as a
1407
+ * mark — or null. An atom that paints its own text (a button: its label is
1408
+ * an attribute, and it allows no marks) names the marks it takes this way
1409
+ * in its spec, `markAttrs: ['bold', 'italic']`, each a boolean attribute of
1410
+ * the mark's name. Bold and italic then work on it as on text — the
1411
+ * toolbar, the bubble menu, Mod-B — through {@link isMarkActive} and the
1412
+ * kit's `toggleMark`. The atom is the selection's whole content (clicked,
1413
+ * or dragged over with nothing but whitespace, `soleInlineAtom`); in a
1414
+ * range with text the kit's `setMark` flips it along with the text.
1415
+ */
1416
+ declare function selectedMarkAtom(state: EditorState, type: MarkType): {
1417
+ pos: number;
1418
+ node: Node;
1419
+ } | null;
1420
+ /** Whether the mark is on in the selection: at a caret, what typing would
1421
+ carry; in a range, whether any of it has the mark — text marked, or an
1422
+ atom whose attribute for it is on (a bold button). */
1423
+ declare function isMarkActive(state: EditorState, type: MarkType): boolean;
1424
+ /** Checks the selection's whole ancestor chain, so wrapper nodes
1425
+ (blockquote, lists) report active even though the cursor's direct
1426
+ parent is a paragraph. */
1427
+ declare function isNodeActive(state: EditorState, type: NodeType, attrs?: Record<string, any>): boolean;
1428
+
1429
+ /** Semantic HTML output (`<p>` paragraphs) for content rendered in the app. */
1430
+ declare const richTextExtensions: Extension[];
1431
+ /**
1432
+ * Email-safe output: `<div>` lines instead of `<p>` (mail clients render
1433
+ * paragraph margins as double spacing) and empty lines as `<div><br></div>`.
1434
+ */
1435
+ declare const emailExtensions: Extension[];
1436
+ /**
1437
+ * The parallel source-editor kit: the document is HTML source text, one
1438
+ * `codeLine` per line, with highlighting, linting and formatting instead of
1439
+ * rich-text nodes. Developed alongside the email kit on the same extension
1440
+ * contract — only the extension set differs.
1441
+ */
1442
+ declare const htmlSourceExtensions: Extension[];
1443
+
1444
+ interface HtmlLanguageOptions {
1445
+ /** Called with fresh diagnostics whenever the text is rescanned: once
1446
+ typing rests, and at once for a wholesale change. */
1447
+ onDiagnostics?: (diagnostics: HtmlDiagnostic[]) => void;
1448
+ /** How long typing must rest, in ms, before the whole text is rescanned
1449
+ — highlighting and lint rebuilt from scratch. Until then each edit
1450
+ only moves the existing decorations along. Default
1451
+ {@link TYPING_REST}. */
1452
+ rescanDelay?: number;
1453
+ /** The parse mode of the visual editor this source feeds — lint and
1454
+ format follow it (see `SourceOptions`). Default `email`, the editor's
1455
+ own default. */
1456
+ parseMode?: ParseMode;
1457
+ }
1458
+ /**
1459
+ * How long typing must rest before work that covers the whole text runs, in
1460
+ * ms — tuned for the fastest typists, not the average: around 300 words a
1461
+ * minute a record typist lands a key every ~40 ms (~30 in a burst), so five
1462
+ * missed keys is a stop, not a gap between two. Shorter would fire mid-burst;
1463
+ * longer only keeps a finished edit waiting.
1464
+ */
1465
+ declare const TYPING_REST = 200;
1466
+ /** The document's source text: top-level lines joined with newlines. */
1467
+ declare function docText(doc: Node): string;
1468
+ /**
1469
+ * Maps offsets in the joined source text back to ProseMirror positions.
1470
+ * Line i's text starts at `linePm[i]`; the newline between lines has no
1471
+ * position of its own, so offsets into it clamp to the line end.
1472
+ */
1473
+ declare function createOffsetMapper(doc: Node): (offset: number) => number;
1474
+ /** Maps a ProseMirror position to its offset in the joined source text. */
1475
+ declare function textOffsetAt(doc: Node, pos: number): number;
1476
+ /**
1477
+ * The HTML language service of the source editor kit: syntax-highlight and
1478
+ * lint decorations over the document treated as HTML source text, a
1479
+ * `formatDocument` command (Shift-Alt-F and Mod-S, VS Code style), tag auto-closing
1480
+ * while typing, format-on-blur when the markup is error-free, and line-aware
1481
+ * pasting. Diagnostics render as wavy underlines with the message as tooltip.
1482
+ *
1483
+ * Formatting is presentation-only: the indentation and line breaks it inserts
1484
+ * are exactly the whitespace the email schema's parser discards, so the
1485
+ * serialized output never changes.
1486
+ */
1487
+ declare const createHtmlLanguage: (options?: HtmlLanguageOptions) => FunctionalExtension;
1488
+ /** Default instance for kits; use {@link createHtmlLanguage} for callbacks. */
1489
+ declare const HtmlLanguage: FunctionalExtension;
1490
+
1491
+ interface AutocompleteItem {
1492
+ label: string;
1493
+ }
1494
+ interface AutocompleteState {
1495
+ open: boolean;
1496
+ query: string;
1497
+ items: AutocompleteItem[];
1498
+ activeIndex: number;
1499
+ /** Applies an item: replaces the typed query with the completion. */
1500
+ select: (item: AutocompleteItem) => void;
1501
+ }
1502
+ interface AutocompleteOptions {
1503
+ /** The floating listbox; contents rendered by the host app from the state
1504
+ received through `onChange`. Needs a `position: relative` ancestor. */
1505
+ element: HTMLElement;
1506
+ /** Gap between the line and the menu, in px. */
1507
+ offset?: number;
1508
+ /** Notified when the menu opens, closes, filters, or moves its highlight. */
1509
+ onChange?: (state: AutocompleteState) => void;
1510
+ }
1511
+ /**
1512
+ * IDE-style completion for the HTML source editor, offering only the
1513
+ * email-safe vocabulary: tags after `<`, the currently open tags after `</`,
1514
+ * per-tag attributes inside a tag, and safe style properties inside
1515
+ * `style="…"`. Same interaction contract as the slash menu — the host app
1516
+ * renders the listbox from the emitted state.
1517
+ */
1518
+ declare const createHtmlAutocomplete: (options: AutocompleteOptions) => FunctionalExtension;
1519
+
1520
+ interface SourceMarksOptions {
1521
+ /** The rich-text extension set whose schema defines the mark semantics —
1522
+ pass the same kit the visual editor runs so both sides toggle alike. */
1523
+ extensions: Extension[];
1524
+ /** The parse mode of the visual editor this source pane mirrors. Default
1525
+ `email`, the editor's own default; pass the visual editor's mode when it
1526
+ chose another, so a toggle never drops markup the visual side keeps. */
1527
+ parseMode?: ParseMode;
1528
+ }
1529
+ /**
1530
+ * Rich-text mark editing for the HTML source editor: every mark extension's
1531
+ * keymap (Mod-B, Mod-I, ...) and commands are mirrored, but instead of string
1532
+ * surgery the selected source is parsed through the rich schema, the original
1533
+ * mark command runs on the resulting document, and the source is replaced
1534
+ * with the re-serialized, formatted result. Toggling therefore behaves
1535
+ * exactly like the visual editor — it *is* the visual editor's command.
1536
+ *
1537
+ * Side effect by design: the command pipeline canonicalizes, so running one
1538
+ * also repairs and formats the document, like Shift-Alt-F does.
1539
+ */
1540
+ declare const createSourceMarks: ({ extensions, parseMode, }: SourceMarksOptions) => FunctionalExtension;
1541
+
1542
+ /**
1543
+ * Pre-parse cleanup for pasted HTML. Deliberately minimal: the email schema
1544
+ * already discards classes, ids, unknown tags and unknown styles on parse —
1545
+ * this only removes what would otherwise *leak through* the parse:
1546
+ *
1547
+ * - `<style>`/`<script>`/… subtrees (their text content would survive),
1548
+ * - Word's namespaced tags (`<o:p>`, `<w:sdt>`, `<v:shape>`, …),
1549
+ * - Word's fake list glyphs (`<span style="mso-list:Ignore">·&nbsp;</span>`),
1550
+ * which would paste literal bullet characters into the text.
1551
+ *
1552
+ * Everything else — `class` soup, `mso-*` style properties, Google Docs'
1553
+ * `<b style="font-weight:normal">` wrapper — dies in the schema parse, where
1554
+ * it belongs.
1555
+ */
1556
+ declare function sanitizePastedHTML(html: string): string;
1557
+ /** Cleans clipboard HTML before it reaches the schema parse — and parses it
1558
+ in `repair` mode: the source pane may be law, the clipboard never is.
1559
+ Pasted class soup, foreign tables and Word's markup are still repaired
1560
+ into canonical form, exactly as before the preserving parse existed. */
1561
+ declare const PasteHygiene: _h_k_dev_angular_email_editor.FunctionalExtension;
1562
+
1563
+ declare const ClearFormatting: _h_k_dev_angular_email_editor.FunctionalExtension;
1564
+
1565
+ /**
1566
+ * One line of source code. The document of the HTML source kit is a flat list
1567
+ * of these; the source text is the lines joined with newlines. `code: true`
1568
+ * makes ProseMirror treat pastes as plain text, and `marks: ''` keeps
1569
+ * formatting out — colour comes from decorations, not marks.
1570
+ */
1571
+ declare const CodeLine: _h_k_dev_angular_email_editor.NodeExtension;
1572
+
1573
+ /**
1574
+ * The document root. Two attributes, neither of them content:
1575
+ *
1576
+ * - `envelope` holds a parsed document's surroundings — doctype,
1577
+ * `<html>`/`<body>` attributes, the canonical head — when the source
1578
+ * carried them (see `DocumentEnvelope`); `null` for a bare body fragment,
1579
+ * which is what the composer produces on its own. The serializer wraps the
1580
+ * body in it, the editor never shows it. It comes *with* content: a
1581
+ * replacement brings its own.
1582
+ * - `quoted` holds the quoted history a reply or forward answers — canonical
1583
+ * HTML, or `null` (see `QuotedHistory`). It stays *beside* the content: a
1584
+ * replacement of the body never touches it; only a deliberate step does.
1585
+ */
1586
+ declare const Document$1: _h_k_dev_angular_email_editor.NodeExtension;
1587
+ /** The document attributes that come with content — what a replacement of
1588
+ the body carries over from the new document. Every other one stays. */
1589
+ declare const CONTENT_ATTRS: readonly string[];
1590
+
1591
+ declare const Paragraph: _h_k_dev_angular_email_editor.NodeExtension;
1592
+
1593
+ type ParagraphAlignment = 'center' | 'right' | null;
1594
+ /** One indent level, in px — Gmail's step, and what its own indented
1595
+ messages carry, so the two round-trip into each other. */
1596
+ declare const INDENT_STEP = 40;
1597
+ /** The deepest indent offered: eight steps are 320px, a phone screen's
1598
+ worth of margin — past that a line has no room left to say anything. */
1599
+ declare const INDENT_MAX = 8;
1600
+ /**
1601
+ * Email flavour of the paragraph. Mail clients render the default margins of
1602
+ * `<p>` as double spacing, so email bodies use `<div>` lines instead — the
1603
+ * same model contenteditable composers (Gmail, Outlook) produce natively.
1604
+ *
1605
+ * A line carries its alignment and its indent (in steps of
1606
+ * {@link INDENT_STEP}); both serialize as inline style, the only style mail
1607
+ * clients keep, and both stay on the next line when Enter is pressed at the
1608
+ * line's end (see `splitKeepingMarks`), as they do in Gmail.
1609
+ */
1610
+ declare const EmailParagraph: _h_k_dev_angular_email_editor.NodeExtension;
1611
+
1612
+ declare const Text: _h_k_dev_angular_email_editor.NodeExtension;
1613
+
1614
+ declare const HardBreak: _h_k_dev_angular_email_editor.NodeExtension;
1615
+
1616
+ /**
1617
+ * Personalization tokens — `{{cf_70}}`, `{{customer_gender == 'male' ? 'Herr'
1618
+ * : 'Frau'}}` — as **text with a mark** (decided 2026-09-02; they were inline
1619
+ * atoms before). The raw `{{expression}}` is ordinary document text, braces
1620
+ * included, so the cursor lands anywhere inside it, it is edited like any
1621
+ * text, and a long expression wraps across lines like text. The `mergeTag`
1622
+ * mark only paints the pill, editor-only: the serialized email carries the
1623
+ * raw text, stored and re-emitted **verbatim** — the expression is another
1624
+ * system's program (an AngularJS-style evaluator, a Handlebars renderer, …)
1625
+ * and rewriting so much as its spacing could change what a string literal or
1626
+ * date format means. The editor protects; it never interprets.
1627
+ *
1628
+ * Two invariants, kept by the extension's `appendTransaction` after every
1629
+ * change (and by {@link promoteMergeTags} at parse time):
1630
+ *
1631
+ * 0. **A token is padded canonically**: `{{ expr }}`, one space each side
1632
+ * ({@link canonicalMergeTagInner}) — the only rewrite the editor makes
1633
+ * to a token, applied on parse, on completion, and by the formatter; a
1634
+ * token under the cursor is left alone until the cursor leaves it.
1635
+ * 1. **The mark covers exactly the tokens.** Text that reads `{{…}}` (per
1636
+ * {@link isMergeTagExpression}) carries the mark, nothing else does —
1637
+ * type a token by hand and it becomes a pill on the closing brace,
1638
+ * delete a brace and the pill is gone. Derived from the text, never
1639
+ * stored: the round trip cannot lie.
1640
+ * 2. **Formatting is all-or-nothing on a token.** Bold, italic, colour, a
1641
+ * link: whatever applies to any part of a token applies to the whole of
1642
+ * it — Ctrl-B with the cursor inside bolds the entire `{{…}}`, so the
1643
+ * value it renders to ("Mr Wild") is bold as a whole. A mark added or
1644
+ * removed over part of a token is widened to the token; a token that
1645
+ * arrives partially formatted (a paste, the source pane) is repaired to
1646
+ * whole.
1647
+ *
1648
+ * What is *not* a token: Handlebars block helpers and partials (`{{#if}}`,
1649
+ * `{{/each}}`, `{{>partial}}`) — half of a block is not a value and pairing
1650
+ * them is a renderer's job — plus anything nested, multi-line, or past the
1651
+ * length ceiling. Those stay plain text.
1652
+ */
1653
+ /** The longest expression the editor protects as one token, in characters
1654
+ inside the braces. Real templates run long — a salutation with four
1655
+ branches and string concatenation passes 300 — so the ceiling is a
1656
+ runaway guard, not a style limit: past it, stray braces around ordinary
1657
+ prose stay prose. Measured on the trimmed expression; the scanner and the
1658
+ source-pane lint mask allow it plus the two padding spaces (regex
1659
+ literals cannot read a constant). */
1660
+ declare const MAX_MERGE_TAG_LENGTH = 1000;
1661
+ declare const isMergeTagPath: (path: string) => boolean;
1662
+ /** An inner text the editor will protect as one token: single-line, sanely
1663
+ sized, no nested braces, not block syntax (a Handlebars helper / closer /
1664
+ `{{else}}` / partial / comment) — half of a block is not a value. */
1665
+ declare function isMergeTagExpression(raw: string): boolean;
1666
+ /** The canonical padding of a token: `{{ expr }}` — one space each side of
1667
+ the trimmed expression. Edge whitespace is insignificant to every dialect
1668
+ we transport (AngularJS `$parse`, Handlebars), so this is the one
1669
+ rewrite the editor allows itself on a token; the expression itself stays
1670
+ byte-verbatim. Handlebars' `{{~ … ~}}` whitespace control and `{{& …}}`
1671
+ need their sigil against the braces: those are left as written. */
1672
+ declare function canonicalMergeTagInner(expr: string): string;
1673
+ /** Every token in running text at its canonical padding — the source
1674
+ formatter's rule (Shift-Alt-F, Mod-S, format-on-blur), and the same form
1675
+ the schema serializes. Pure. */
1676
+ declare function normalizeMergeTagText(text: string): string;
1677
+ /** Every token in a piece of running text, as offsets: the whole `{{ … }}`
1678
+ and the expression inside it — what a highlighter paints (the source
1679
+ pane mutes the braces, Angular-template style). Pure. */
1680
+ declare function scanMergeTags(text: string, options?: {
1681
+ multiline?: boolean;
1682
+ }): {
1683
+ from: number;
1684
+ to: number;
1685
+ expr: [number, number];
1686
+ }[];
1687
+ /** A token's place in the document: `[from, to)` around the braces. */
1688
+ interface MergeTagRange {
1689
+ from: number;
1690
+ to: number;
1691
+ /** The inner expression, verbatim. */
1692
+ expr: string;
1693
+ }
1694
+ /** Every token in the document, in document order — derived from the text,
1695
+ which is the only truth about them. */
1696
+ declare function mergeTagRanges(doc: Node): MergeTagRange[];
1697
+ /** Every token's raw expression, in document order — for a host that wants
1698
+ to evaluate or audit the template as written. */
1699
+ declare function mergeTagExpressions(doc: Node): string[];
1700
+ /** The token at `pos` — inside it or at either edge — if any. */
1701
+ declare function mergeTagAt(doc: Node, pos: number): MergeTagRange | undefined;
1702
+ /** A command inserting the token for `path` at the selection — what a `{{`
1703
+ suggestion runs. The token is text; the pill follows from the mark's
1704
+ invariants. */
1705
+ declare const insertMergeTag: (path: string) => Command;
1706
+ /** Whether a selection lies within one token — the bubble menu stays away
1707
+ then (formatting still works from the keyboard, on the whole token). */
1708
+ declare function selectionInsideMergeTag(state: EditorState): boolean;
1709
+ /** Whether the caret stands strictly inside a token — between its braces,
1710
+ at neither edge. What a `{{` suggestion menu's `allow` turns down: there
1711
+ the braces before the caret open a token that is already written, and
1712
+ picking a row would write a second one into it. */
1713
+ declare function caretInsideMergeTag(state: EditorState): boolean;
1714
+ /**
1715
+ * Applies both invariants to a parsed document — the token half of "parsing
1716
+ * is repair", run by `parseHTML` right after the table repair. A schema
1717
+ * without the mark (the HTML source editor) passes straight through.
1718
+ */
1719
+ declare function promoteMergeTags(doc: Node, schema: Schema): Node;
1720
+ declare const MergeTag: _h_k_dev_angular_email_editor.MarkExtension;
1721
+ /**
1722
+ * The *required fields* of the document: every identifier the token
1723
+ * expressions read that the host must supply a value for, deduplicated, in
1724
+ * first-use order. This is a lex over the tokens' expressions, not a text
1725
+ * parse of the HTML — and it understands enough expression grammar to
1726
+ * exclude what is not a field:
1727
+ *
1728
+ * - string literals (`'Herr'`, `"+5 days"`),
1729
+ * - filter names (`x | formatPrice` — the identifier after a single `|`),
1730
+ * - function calls (`round(…)`, `parseFloat(…)`),
1731
+ * - dotted tails (`customer.name` needs `customer`, not `name`),
1732
+ * - locals assigned inside any token (`mwst = …; mwst` — `mwst` is
1733
+ * computed, not fetched; the assignment is seen document-wide),
1734
+ * - literals and builtins (`true`, `null`, `now`, …).
1735
+ *
1736
+ * `{{ mwst = round(parseFloat(cf_70) * 0.19, 2); mwst }}` therefore requires
1737
+ * exactly `cf_70`.
1738
+ */
1739
+ declare function mergeTagFields(doc: Node): string[];
1740
+
1741
+ /** The ledger's conservative scale: reads at 320px and on a desktop alike. */
1742
+ declare const HEADING_SIZES: Record<number, number>;
1743
+ /** A heading's canonical inline style. No margins — lines carry none
1744
+ (principle 3), spacing is an empty line; no `line-height` — Outlook
1745
+ substitutes its own (the client-support module says so) and our output
1746
+ stays lint-clean. Incoming heading styles never survive: the scale is the
1747
+ schema's, not the author's. */
1748
+ declare const headingStyle: (level: number) => string;
1749
+ declare const Heading: _h_k_dev_angular_email_editor.NodeExtension;
1750
+
1751
+ /** The ledger's answer to reply chains whose margins stack until text is one
1752
+ word per line: a small fixed indent and a rule, no margins at all — each
1753
+ nesting level costs 14px, never more. The grey is the divider's, safe
1754
+ against both white and near-black (the dual-contrast rule). */
1755
+ declare const BLOCKQUOTE_STYLE = "margin: 0px; padding-left: 12px; border-left: 2px solid rgb(224, 224, 224);";
1756
+ declare const Blockquote: _h_k_dev_angular_email_editor.NodeExtension;
1757
+
1758
+ /**
1759
+ * Inline images — the half of the attachment story that lives in the
1760
+ * document. Dropped and pasted image files enter the editor as data-URL
1761
+ * images (the only thing a browser editor can hold on its own), but a data
1762
+ * URL is not an email image: Gmail and Outlook refuse it and the recipient
1763
+ * sees a hole. The email form is a `cid:` reference to a `multipart/related`
1764
+ * part — and assembling MIME is the host's job, not ours.
1765
+ *
1766
+ * So the editor reports the truth about the document at send time
1767
+ * ({@link promoteInlineImages}): every data-URL image is decoded into its
1768
+ * bytes and re-pointed at a generated Content-ID *in the payload copy only*;
1769
+ * the document itself keeps its data URL (the editor still has to display
1770
+ * it, and the round trip never learns about the promotion). Images that
1771
+ * already reference a `cid:` — an imported reply's inline parts — are
1772
+ * reported with the bytes the configured registry holds for them, else by
1773
+ * id alone: the host received those parts with the import and owns them.
1774
+ */
1775
+ interface InlineImage {
1776
+ /** The Content-ID the payload's HTML references as `src="cid:<cid>"` —
1777
+ bare, without the `cid:` scheme or the `<…>` the MIME header wraps it
1778
+ in. */
1779
+ cid: string;
1780
+ /** The image bytes, typed — present for images the editor holds itself
1781
+ (dropped, pasted, picked), `null` for pre-existing `cid:` references
1782
+ whose part the host already has. */
1783
+ blob: Blob | null;
1784
+ }
1785
+ interface PromotedInlineImages {
1786
+ /** A copy of the document with every data-URL image pointing at its
1787
+ `cid:` — serialize this for the payload; the original is untouched. */
1788
+ doc: Node;
1789
+ /** Every inline part the payload references, in document order, each id
1790
+ once. */
1791
+ images: InlineImage[];
1792
+ }
1793
+ /** Decodes a `data:` URL into a typed `Blob` — base64 or percent-encoded,
1794
+ the media type without its parameters. `null` for anything that is not a
1795
+ well-formed data URL, so a malformed source is left alone rather than
1796
+ sent as garbage. */
1797
+ declare function decodeDataUrl(src: string): Blob | null;
1798
+ /** The send-time projection: data-URL images become `cid:` parts in a copy
1799
+ of the document; every inline reference — promoted or pre-existing — is
1800
+ listed once, in document order. Pure: the input document is never
1801
+ modified, and an image whose data URL cannot be decoded keeps its source
1802
+ (the source-pane lint names it). Identical data URLs share one part. */
1803
+ declare function promoteInlineImages(doc: Node, registry?: InlineImageRegistry): PromotedInlineImages;
1804
+ /**
1805
+ * The inline image registry — the half of the attachment story that the
1806
+ * editor owns (decided 2026-09-02, the Gmail model): one map per composer
1807
+ * from Content-ID to bytes, with a display URL for each. Two writers — the
1808
+ * editor (a dropped, pasted or picked file) and the host (an import's MIME
1809
+ * parts) — and four readers: the editor view, the preview, the send intent,
1810
+ * and the host persisting a draft's parts. With a registry configured
1811
+ * ({@link createInlineImages}) a drop produces `src="cid:…"` at once and the
1812
+ * document stays light; without one, today's data-URL path still applies.
1813
+ * The contract is framework-free; the Angular service implements it with a
1814
+ * signal underneath.
1815
+ */
1816
+ interface InlineImageRegistry {
1817
+ /** Registers bytes and returns their Content-ID — a supplied one is kept
1818
+ (an import's part), otherwise one is generated. Re-registering a cid
1819
+ replaces its bytes. */
1820
+ add(blob: Blob, cid?: string): string;
1821
+ /** The display URL for the editor (an object URL); undefined for a part
1822
+ the registry does not hold. */
1823
+ resolve(cid: string): string | undefined;
1824
+ /** The bytes; undefined when the host owns the part. */
1825
+ blob(cid: string): Blob | undefined;
1826
+ /** Change notification, so views can re-resolve; returns the unsubscribe. */
1827
+ subscribe?(listener: () => void): () => void;
1828
+ }
1829
+ interface InlineImageStoreOptions {
1830
+ /** How display URLs are made (default `URL.createObjectURL`) … */
1831
+ createUrl?: (blob: Blob) => string;
1832
+ /** … and released (default `URL.revokeObjectURL`). */
1833
+ revokeUrl?: (url: string) => void;
1834
+ }
1835
+ /** The in-memory registry. Parts stay registered after their image is
1836
+ deleted from the document (undo brings it back); the send intent reports
1837
+ only what the document references, and so should a host's draft. Every
1838
+ URL is released by {@link revokeAll} when the composer goes away. */
1839
+ declare class InlineImageStore implements InlineImageRegistry {
1840
+ #private;
1841
+ private readonly options;
1842
+ constructor(options?: InlineImageStoreOptions);
1843
+ add(blob: Blob, cid?: string): string;
1844
+ resolve(cid: string): string | undefined;
1845
+ blob(cid: string): Blob | undefined;
1846
+ has(cid: string): boolean;
1847
+ /** Every registered Content-ID, in registration order. */
1848
+ cids(): string[];
1849
+ subscribe(listener: () => void): () => void;
1850
+ /** Releases every display URL and forgets every part. */
1851
+ revokeAll(): void;
1852
+ }
1853
+ /** The registry configured for an editor, if any — plugin state, so the
1854
+ Image node, the send intent and the host all read the same one. */
1855
+ declare const inlineImagesKey: PluginKey<InlineImagesState>;
1856
+ interface InlineImagesState {
1857
+ registry: InlineImageRegistry;
1858
+ /** Bumped on every registry change: the image decorations carry it, so a
1859
+ node view re-resolves its `cid:` when a part arrives late. */
1860
+ version: number;
1861
+ }
1862
+ declare const inlineImageRegistry: (state: EditorState) => InlineImageRegistry | undefined;
1863
+ /**
1864
+ * The registry extension — the roadmap's "`cid:` resolver input", and the
1865
+ * editor's write side. Hands the registry to the Image node (drops register
1866
+ * their bytes and insert `cid:` sources; node views resolve them for display)
1867
+ * and to the send intent (bytes for `inlineImages`). A registry change is a
1868
+ * meta transaction that bumps a version carried by a node decoration on every
1869
+ * `cid:` image, which is how ProseMirror re-renders those node views without
1870
+ * the document changing.
1871
+ */
1872
+ declare const createInlineImages: (options: {
1873
+ registry: InlineImageRegistry;
1874
+ }) => FunctionalExtension;
1875
+ /** Every Content-ID the html's images reference, once each, in document
1876
+ order — the parts a draft has to keep (the registry holds more: a
1877
+ deleted image's part stays for undo). Pure. */
1878
+ declare function inlineImageCids(html: string): string[];
1879
+ /** Rewrites every `cid:` image source the resolver knows — the preview's
1880
+ projection (a sandboxed, opaque-origin frame cannot load the editor's
1881
+ blob URLs, so the preview resolves to data URLs). Unknown cids are left
1882
+ untouched. Pure. */
1883
+ declare function rewriteInlineImageSources(html: string, resolve: (cid: string) => string | undefined): string;
1884
+
1885
+ interface ImageAttrs {
1886
+ /** `null` is a placeholder: a sized frame awaiting its file (see the
1887
+ NodeView) — serialized as an `<img>` without `src`, linted as unfilled. */
1888
+ src: string | null;
1889
+ alt?: string | null;
1890
+ title?: string | null;
1891
+ /** Natural display width in px, capped at {@link MAX_IMAGE_WIDTH}. */
1892
+ width?: number | null;
1893
+ }
1894
+ /** Email convention: wider than this overflows phones and gets Gmail-clipped
1895
+ layouts; parse and file drops both cap to it. */
1896
+ declare const MAX_IMAGE_WIDTH = 600;
1897
+ /** Reads a dropped/pasted/picked image file into insertable attrs: alt
1898
+ defaulted from the filename, natural width capped for email, and the
1899
+ source — a `cid:` when a registry holds the bytes (the Gmail model: the
1900
+ document stays light, the part travels with the send intent), else a
1901
+ data URL (the registry-less fallback, promoted at send). */
1902
+ declare function readImageFile(file: File, registry?: InlineImageRegistry): Promise<ImageAttrs>;
1903
+ /**
1904
+ * The drop-claim rule (decided 2026-09-02): the editor takes a file drop only
1905
+ * when *every* file is an image. Pure images are content and embed inline;
1906
+ * a mixed drop — an image alongside a PDF, say — is an attachment gesture,
1907
+ * so the editor leaves the event untouched and it bubbles *whole* to the
1908
+ * host's dropzone. Nothing to decide, nothing silently lost: the host
1909
+ * attaches everything, and may still embed the images itself via
1910
+ * `readImageFile` + `insertImage` if it wants to.
1911
+ */
1912
+ declare function claimedImageFiles(files: Iterable<File>): File[];
1913
+ /**
1914
+ * Whether an image drag the editor will embed is over it — the drag phase of
1915
+ * the claim rule (`isImageDrag`), told to the host.
1916
+ *
1917
+ * A host that wraps the editor in a file dropzone needs this to say the
1918
+ * right thing while a file hangs over the text: its zone reports a drag it
1919
+ * would take, and an image drag is exactly the one it will *not* get — the
1920
+ * editor claims those as they come over it (`claimImageDrag`), so the zone
1921
+ * stands down and reads nothing. This is the other half, from the editor
1922
+ * that knows: on while an image-only drag is over the editing
1923
+ * surface, off when it leaves, drops, or turns out to be mixed. Reported on
1924
+ * change only. The drop line (`ImageDropLine`) is what tracks it — this is
1925
+ * the same state, spoken.
1926
+ *
1927
+ * And it is where the host says how the editor tells that zone "this one is
1928
+ * mine" — `claim`. The library depends on no dropzone; any of them fits:
1929
+ *
1930
+ * // @h-k-dev/angular-file-drop: a mark the zone reads.
1931
+ * createImageDrag({ claim: claimDragEvent, onChange })
1932
+ *
1933
+ * // dropzone.js, ngx-file-drop, a hand-written zone — anything that
1934
+ * // listens for the drag events bubbling up to it: they never get there.
1935
+ * createImageDrag({ claim: (event) => event.stopPropagation(), onChange })
1936
+ */
1937
+ declare const createImageDrag: (options: ImageDragOptions) => FunctionalExtension;
1938
+ interface ImageDragOptions {
1939
+ /** Told when an image-only drag comes over the editing surface, and when
1940
+ it is gone. */
1941
+ onChange?: (over: boolean) => void;
1942
+ /**
1943
+ * Claims a drag event for the editor, so a dropzone around it stands
1944
+ * down: called on every `dragenter` and `dragover` of an image-only drag,
1945
+ * and on the `drop` the editor takes (after its `preventDefault()`).
1946
+ * Never for a mixed drag or a file the editor will not embed — those stay
1947
+ * the zone's. What it does is the zone's protocol: mark the event, stop
1948
+ * it bubbling, flip a flag the zone checks.
1949
+ */
1950
+ claim?: (event: DragEvent) => void;
1951
+ }
1952
+ interface ImageDropTarget {
1953
+ /** The caret — the image is inserted inline where the cursor sits (Gmail's
1954
+ rule: the pointer only says the drag is over the editor). */
1955
+ pos: number;
1956
+ /** The drop line on screen: the bottom of the visual line the image is
1957
+ about to join, spanning that line's block. */
1958
+ top: number;
1959
+ left: number;
1960
+ width: number;
1961
+ }
1962
+ /** Where an image drag drops — the caret, exactly what the drop uses — and
1963
+ the line to draw for it: under the visual line the caret is on (the
1964
+ caret's own box gives the line's bottom, the surrounding block its
1965
+ width). Gmail's rule: the image lands where the cursor sat before the
1966
+ drag; the pointer decides only that the drag is over the editor. */
1967
+ declare function imageDropTarget(view: EditorView): ImageDropTarget;
1968
+ /** How long a keyboard-opened chooser waits for the pointer to move before
1969
+ it opens regardless — inside the browser's window of user activation
1970
+ (five seconds in Chromium), so the click still counts. */
1971
+ declare const FILE_CHOOSER_POINTER_WAIT = 2500;
1972
+ /**
1973
+ * Opens a file chooser the way the pointer stays visible in it. Chromium on
1974
+ * Windows hides the mouse pointer while a key is typed and shows it again
1975
+ * on the next mouse move *it* handles; the OS file dialog is modal and
1976
+ * takes the moves itself, so a chooser opened from a keyboard action — a
1977
+ * `/image` picked with Enter — runs with no pointer at all. Where the last
1978
+ * input was a key on Windows, the opening waits for the pointer to move
1979
+ * (the page shows it again on that move), or for a moment to pass
1980
+ * ({@link FILE_CHOOSER_POINTER_WAIT}) for the writer who never reaches for
1981
+ * the mouse. Anywhere else, and after a click, it opens at once.
1982
+ */
1983
+ declare function openFileChooser(open: () => void, options?: {
1984
+ keyboard?: boolean;
1985
+ windows?: boolean;
1986
+ wait?: number;
1987
+ }): void;
1988
+ /** Floor for a resized image: below this a pad has nothing left to grab. */
1989
+ declare const MIN_IMAGE_WIDTH = 40;
1990
+ /** Room an image leaves at the end of its line for the caret (decided
1991
+ 2026-09-24): the widest an image gets in a line is the line less this,
1992
+ so the caret can always stand to its right — on a drop, and at the top
1993
+ of a resize. Wider, and the caret's spot (ProseMirror's 1px separator)
1994
+ wraps to the next line, with nothing beside the image to click or to
1995
+ start a drag from. */
1996
+ declare const CARET_ROOM = 4;
1997
+ /** `width` fitted under a line's `ceiling` (see `CARET_ROOM`): an unknown
1998
+ width stays unknown (fluid, `max-width: 100%`), and without a ceiling
1999
+ the width stands. */
2000
+ declare function fitImageWidth(width: number | null | undefined, ceiling: number | null): number | null;
2001
+ /** Width a fresh placeholder opens at — the ledger's phone width; the pads
2002
+ take it from there. */
2003
+ declare const PLACEHOLDER_WIDTH = 320;
2004
+ /** A placeholder keeps the frame its author sized: the picked file supplies
2005
+ the source (and the alt when the placeholder has none); `width` stays. */
2006
+ declare function filledPlaceholderAttrs(placeholder: ImageAttrs, picked: ImageAttrs): ImageAttrs;
2007
+ /**
2008
+ * The image a selection holds and nothing else, or null: a click's node
2009
+ * selection, or a range that covers the image alone — a drag over it, or
2010
+ * Shift-arrow, that caught nothing but whitespace along the way (spaces,
2011
+ * tabs, a line break: no character, no token, no second atom; see
2012
+ * `soleInlineAtom`). What an image's own chrome (a bubble menu, its
2013
+ * actions) keys on; a range with text in it is a text selection.
2014
+ */
2015
+ declare function selectedImage(state: EditorState): {
2016
+ pos: number;
2017
+ node: Node;
2018
+ } | null;
2019
+ /** The selected image's alt text: `''` when it has none, `null` when no
2020
+ image is the selection (see {@link selectedImage}). */
2021
+ declare function selectedImageAlt(state: EditorState): string | null;
2022
+ /**
2023
+ * Inline image — Gmail's and Proton's model (decided 2026-09-02): the image
2024
+ * sits in the text line like a character, so the caret can stand right
2025
+ * beside it at the image's height and typing continues next to it. The
2026
+ * serialized form is then Gmail's own (`<div><img …></div>` for an image on
2027
+ * a line of its own), which is as safe as email HTML gets. Sizing is the
2028
+ * responsiveness-ledger hybrid: the `width`
2029
+ * *attribute* for Outlook (which ignores `max-width` entirely) plus
2030
+ * `width:100%; max-width:<n>px; height:auto` for everyone else — fluid on
2031
+ * phones, capped at natural size on desktop, no media queries. `float` never
2032
+ * parses and never serializes. Dropped or pasted image files insert as
2033
+ * data-URL images — what a browser editor can hold on its own; the send
2034
+ * intent promotes them to `cid:` parts (`promoteInlineImages`), and the
2035
+ * source pane lints them until then. Which drops the editor claims at all
2036
+ * is {@link claimedImageFiles}: all images, or none.
2037
+ */
2038
+ declare const Image: _h_k_dev_angular_email_editor.NodeExtension;
2039
+
2040
+ /** A block-level rule. Ledger row: horizontal rule → `width: 100%`, done —
2041
+ no fixed pixel width to overflow a phone. Colour is a faint neutral line,
2042
+ not text, so it is exempt from the dual-contrast rule. */
2043
+ declare const Divider: _h_k_dev_angular_email_editor.NodeExtension;
2044
+
2045
+ /** The button's canonical styling — the *border-based* bulletproof button:
2046
+ the touch target (≥ 44px tall, per the ledger) comes from borders in the
2047
+ background colour, not from padding, because Outlook's Word engine ignores
2048
+ padding on an anchor (the client-support module says so) and would render
2049
+ a padded button as a bare coloured text run — while it does draw borders
2050
+ on inline elements, so the box survives there too. No `height`. No
2051
+ `border-radius` — Outlook squares it anyway, and dropping it keeps our own
2052
+ output lint-clean. rgb() colours, not hex: the browser normalizes hex to
2053
+ rgb on the serialize round trip, so the canonical form must already be
2054
+ rgb to stay stable. The label's weight and slant are the button's
2055
+ own (`bold`, `italic`): this is a default button's. */
2056
+ declare const BUTTON_STYLE: string;
2057
+ /** The default button's fill — the primary blue — and the text on it. */
2058
+ declare const BUTTON_FILL = "rgb(26, 115, 232)";
2059
+ /** The canonical style of a button with that label styling and fill: bold
2060
+ is `font-weight: bold` and not bold `normal` (said, never left out — a
2061
+ client's own anchor styling may be bold); italic adds `font-style`
2062
+ after the weight, and not italic leaves it out; the fill (hex, null for
2063
+ the default blue) colours the box and its borders, in the rgb form the
2064
+ CSSOM prints, with the words in `color` (hex) or, null, the text the
2065
+ fill pairs (`fillTextColor`; white on the default blue). */
2066
+ declare function buttonStyle({ bold, italic, background, color, fontFamily, }: {
2067
+ bold: boolean;
2068
+ italic: boolean;
2069
+ background?: string | null;
2070
+ color?: string | null;
2071
+ fontFamily?: string | null;
2072
+ }): string;
2073
+ /**
2074
+ * A call-to-action button: an inline atom that serializes to a bordered
2075
+ * `inline-block` anchor — the email-safe "fake button" every client,
2076
+ * Outlook included, renders as a tappable coloured box. `display: inline-block` in the style is also the parse
2077
+ * discriminator: it is what tells a button apart from an ordinary link, so the
2078
+ * two never collide on the round trip.
2079
+ *
2080
+ * Inline, like an image, so it sits in a paragraph *or* a table cell
2081
+ * (`inline*`) — cells stay textblocks; they do not open to headings or
2082
+ * nested tables. It is an **atom** (label and href are attributes, not
2083
+ * editable content): a contentEditable `<a>` would ignore the node
2084
+ * boundary and unwrap when you type. Selected text becomes one, and a
2085
+ * button linked text again (`button-link`, a toggle); a selected button's
2086
+ * link is set with `setButtonHref`, its words with `setButtonLabel`. The
2087
+ * label is bold or italic as a whole — attributes, not marks: the kit's own
2088
+ * Bold and Italic (the toolbar's, Mod-B, Mod-I) flip them on a selected
2089
+ * button (`markAttrs`, see `selectedMarkAtom`).
2090
+ */
2091
+ declare const Button: _h_k_dev_angular_email_editor.NodeExtension;
2092
+ /** Where a clicked button is, for the host's link editor to stand on. */
2093
+ interface ButtonEditTarget {
2094
+ pos: number;
2095
+ /** The button's box in viewport coordinates — null where nothing is laid
2096
+ out (a test DOM). */
2097
+ rect: DOMRect | null;
2098
+ }
2099
+ interface ButtonEditOptions {
2100
+ /** A button was clicked, and is now the selection: open its link editor.
2101
+ (A click never follows the button's link — its editor's own "open"
2102
+ is the way out.) */
2103
+ onEdit: (target: ButtonEditTarget) => void;
2104
+ }
2105
+ /**
2106
+ * What a click on a button does, told to the host: the kit selects the
2107
+ * button and never follows its link; this hands the host the moment to
2108
+ * open its link editor on it — the one a text link opens with, so a button
2109
+ * is edited the way a link is. Without it, a click only selects the button
2110
+ * (and a bubble menu can offer the editor).
2111
+ *
2112
+ * createButtonEdit({ onEdit: () => linkEditor.show() })
2113
+ */
2114
+ declare const createButtonEdit: (options: ButtonEditOptions) => FunctionalExtension;
2115
+ /** The button a selection holds and nothing else, or null: a click's node
2116
+ selection, or a range — a drag's, Shift-arrow's — that covers the button
2117
+ and whitespace only (see `soleInlineAtom`). A range with text in it is
2118
+ a text selection: the button in it takes the text styling with the
2119
+ text (its `bold`, its `italic`). */
2120
+ declare function selectedButton(state: EditorState): {
2121
+ pos: number;
2122
+ node: Node;
2123
+ } | null;
2124
+ /** A button's `href` before it has been given one — the placeholder
2125
+ `insertButton` and a text without a link start from. */
2126
+ declare const UNSET_BUTTON_HREF = "#";
2127
+ /**
2128
+ * The parser paints `font-weight: bold` from {@link BUTTON_STYLE} onto the
2129
+ * atom as a wrapping mark — it asks the parent, not the node, whether bold
2130
+ * is allowed. The box already is bold; those marks would re-serialize as a
2131
+ * `<strong>` around every button. Strip them so parse stays a fixpoint.
2132
+ */
2133
+ declare function bareButtons(doc: Node, schema: Schema): Node;
2134
+
2135
+ /** An inline atom with a box of its own — what a range can cover like a
2136
+ character, and a click selects whole: an image, a button. Not a line
2137
+ break, which has no box to paint. */
2138
+ declare function isInlineAtom(node: Node): boolean;
2139
+ /** The marks an atom takes as boolean attributes of the mark's name — a
2140
+ button's `bold` and `italic` (`markAttrs` in its spec). Empty for the
2141
+ rest. */
2142
+ declare function markAttrsOf(node: Node): readonly string[];
2143
+ interface InlineAtomAt {
2144
+ pos: number;
2145
+ node: Node;
2146
+ }
2147
+ /**
2148
+ * The inline atom a selection holds and nothing else, or null: a click's
2149
+ * node selection, or a range — a drag's, Shift-arrow's — whose content is
2150
+ * that one atom. Whitespace does not count: a range that took the spaces or
2151
+ * tabs around an image, or the line break after it, still holds the image
2152
+ * alone (a hundred spaces are no space). A letter does, and so does a
2153
+ * second atom — a range over an image and a button is a text selection.
2154
+ *
2155
+ * What an atom's own chrome keys on: an image's bubble menu, a button's,
2156
+ * their actions. `type` narrows it to one node type.
2157
+ */
2158
+ declare function soleInlineAtom(state: EditorState, type?: string): InlineAtomAt | null;
2159
+
2160
+ /** The class a text range puts on every inline atom it covers — an image,
2161
+ a button: the highlight the app draws, since the browser draws none. */
2162
+ declare const IN_SELECTION_CLASS = "aee-atom--in-selection";
2163
+ /**
2164
+ * Inline atoms — an image, a button — selected the way characters are, by
2165
+ * a click-drag over them, and shown selected when they are.
2166
+ *
2167
+ * **Why by hand.** An atom is a non-editable island in the editable text,
2168
+ * and the browser's selection will not treat it as a character: a
2169
+ * selection based inside the island never leaves it, and one based
2170
+ * beside it — anywhere on its line — will not grow over it either (Chrome
2171
+ * keeps a selection from crossing an editing boundary, and snaps a point
2172
+ * over the island to its nearer side). The atom's element, draggable,
2173
+ * answers a press with a drag of itself besides. Only a drag from another
2174
+ * line, passing clear across the atom, covers it on its own.
2175
+ *
2176
+ * **So.** A press on an atom, or on a line that holds one, is kept from
2177
+ * the browser at `mousedown` (its default is prevented — after
2178
+ * ProseMirror's own handling of the press, which stays: a press released
2179
+ * in place is still its click, the node selection, a button's link
2180
+ * editor), and the plugin makes the selection itself from the press,
2181
+ * following the pointer through `posAtCoords` (`dragRange`). A press
2182
+ * elsewhere is the browser's drag, corrected for the stretch the pointer
2183
+ * spends over an atom — on each move, and again whenever ProseMirror reads
2184
+ * the browser's selection back, so the two never disagree. Where the
2185
+ * browser puts a DOM endpoint *inside* an atom's element (past it at the
2186
+ * line's end), ProseMirror reads the point as "before the atom": such an
2187
+ * endpoint covers the atom too. A *selected* atom's press is ProseMirror's
2188
+ * whole: its drag moves the node, as in Gmail. Shift-arrow from a selected
2189
+ * atom grows a range that keeps it (`extendFromAtom`).
2190
+ *
2191
+ * **Showing.** A range that covers an atom is a text selection (it copies
2192
+ * and deletes it as a character), but the element is contenteditable false,
2193
+ * so the browser never paints `::selection` on it: {@link IN_SELECTION_CLASS}
2194
+ * is the highlight the app draws instead. A click is a node selection;
2195
+ * `ProseMirror-selectednode` covers that.
2196
+ *
2197
+ * What an atom's own chrome (a bubble menu) keys on is {@link soleInlineAtom}:
2198
+ * the atom alone, clicked or dragged over with nothing but whitespace.
2199
+ */
2200
+ declare const InlineAtomSelection: _h_k_dev_angular_email_editor.FunctionalExtension;
2201
+
2202
+ /**
2203
+ * Email data tables: a real `<table>` (the most client-compatible layout there
2204
+ * is), driven by **prosemirror-tables** — the same model Tiptap and friends
2205
+ * run on. That library owns the hard parts we were re-deriving by hand: the
2206
+ * `TableMap` (grid geometry through merged cells), `CellSelection`
2207
+ * (shift-drag a rectangle of cells), and `fixTables` (repair of ragged or
2208
+ * overlapping tables). This is a *data* table (it stays tabular, the user
2209
+ * scrolls on a phone); `/columns` is the spongy layout that stacks. Styles are
2210
+ * longhand-only + rgb() so they survive the CSSOM serialize round trip
2211
+ * identically across engines (see ROADMAP).
2212
+ *
2213
+ * Two deliberate departures from the library's defaults:
2214
+ * - **No pixel column widths.** The library's own `columnResizing` plugin
2215
+ * writes `colwidth` in *pixels* — the responsiveness ledger's central trap
2216
+ * — so it stays off. Our `ColumnResize` extension reuses the same
2217
+ * `colwidth` attr but holds **percentages**, serialized as `width: n%` on
2218
+ * the cells (never a `<colgroup>` — enough clients strip it): fluid at
2219
+ * every viewport, read by fixed layout in every client, identical in the
2220
+ * editor and the email.
2221
+ * - **No header cells.** `<th>` parses (as a plain cell) but never
2222
+ * serializes: an email table is presentational, and a header row that
2223
+ * renders bold in one client and not another is a lie we'd rather not tell.
2224
+ *
2225
+ * `colspan`/`rowspan`, by contrast, are fully email-safe — Outlook's Word
2226
+ * engine handles both — so merged cells are a real feature here, and imported
2227
+ * mail keeps its shape instead of being flattened.
2228
+ */
2229
+ /** The table's serialized style. Width and offset are attrs (percent; width
2230
+ defaults to 100, offset to 0) so the whole table can be resized from
2231
+ either edge — the same email-honest unit as the columns: fluid at every
2232
+ viewport, and nested percentages compose (a 50% column of an 80% table is
2233
+ 40% of the container, in every client and in the editor alike). The
2234
+ offset serializes as `margin-left` — inline margins on tables are what
2235
+ Outlook's own Word composer emits, so its engine reads them, and a client
2236
+ that strips them degrades the table gracefully to left-aligned. */
2237
+ declare const tableStyle: (width: number, offset: number) => string;
2238
+ /** How a cell's content sits across its width. The same three the rest of the
2239
+ editor aligns by (a paragraph's `align`), so one set of buttons drives
2240
+ both — and `left` is the default, which canonicalizes to `null` and
2241
+ serializes nothing. */
2242
+ type CellAlignment = 'center' | 'right' | null;
2243
+ /** How a cell's content sits down its height. `top` is the default and *is*
2244
+ serialized: a cell with no `vertical-align` reads as `middle` in enough
2245
+ clients (it is the HTML default) that leaving it out would make the email
2246
+ disagree with the editor. */
2247
+ type CellVerticalAlignment = 'top' | 'middle' | 'bottom';
2248
+ /**
2249
+ * A px-only padding (the shorthand, or any set of longhands) off parsed
2250
+ * markup, canonicalized through the CSSOM so serialize → parse → serialize is
2251
+ * a fixpoint. Anything else — %, em, negative, calc — repairs away: padding
2252
+ * in the email is a deliberate authored choice, in the one unit every client
2253
+ * reads the same.
2254
+ */
2255
+ declare function parsePadding(dom: HTMLElement): string | null;
2256
+ /** The bordered table's default grid color: Excel's classic gridline
2257
+ blue-gray (#D0D7E5) — the shade a spreadsheet-shaped table is expected to
2258
+ wear, light enough to organize without shouting, and legible on both
2259
+ white and dark-mode-inverted grounds. Serialized per *cell* (`border` on
2260
+ `<td>` is the bulletproof border in email — Outlook's Word engine renders
2261
+ it), with `border-collapse` on the table keeping the grid single-lined. */
2262
+ declare const TABLE_BORDER_COLOR = "rgb(208, 215, 229)";
2263
+ /** The bordered table's *real* cell padding — Excel-style breathing room, so
2264
+ the rendered email is readable inside its grid. Deliberately the same
2265
+ value as the editor's editorial default spacing (the app's `.aee-editor
2266
+ table td` rule): a bordered and a plain table feel identical while
2267
+ composing; only what the recipient gets differs — the preset ships its
2268
+ padding, the plain table ships none unless authored. */
2269
+ declare const TABLE_CELL_PADDING = "8px 12px";
2270
+ /** Canonical percentage: one decimal at most, no trailing zero (25%, 33.3%). */
2271
+ declare const formatPct: (n: number) => string;
2272
+ /** No column below 10%: at 320px that is ~32px — the width where a column
2273
+ stops being a column. The resize drag clamps here, and the add-column
2274
+ rescale floors here. */
2275
+ declare const MIN_COLUMN_PCT = 10;
2276
+ /** No table below 20% of its container — narrower stops being a table. */
2277
+ declare const MIN_TABLE_PCT = 20;
2278
+ declare const TableCell: _h_k_dev_angular_email_editor.NodeExtension;
2279
+ declare const TableRow: _h_k_dev_angular_email_editor.NodeExtension;
2280
+ declare const Table: _h_k_dev_angular_email_editor.NodeExtension;
2281
+ /**
2282
+ * Normalizes every table in a freshly parsed document: ragged rows are padded,
2283
+ * overlong spans are trimmed. The transaction-driven half lives in the node's
2284
+ * repair plugin; this is the pure-function path, so `importedDocument` and
2285
+ * `replyDocument` produce the same repaired canonical HTML the editor would.
2286
+ * A schema without tables (the HTML source editor) passes straight through.
2287
+ */
2288
+ declare function repairTables(doc: Node, schema: Schema): Node;
2289
+ /** Sets (or clears) the `background` attr of the cell the cursor is in — or of
2290
+ every cell in a cell selection, which the library's `setCellAttr` handles.
2291
+ Exposed for the app's fill affordance. */
2292
+ declare function setCellBackground(color: string | null): Command;
2293
+ interface TableContext {
2294
+ table: Node;
2295
+ tablePos: number;
2296
+ tableDepth: number;
2297
+ rowIndex: number;
2298
+ colIndex: number;
2299
+ cols: number;
2300
+ rows: number;
2301
+ }
2302
+ /** Locates the table around the selection: the enclosing table node plus the
2303
+ current row/column indices. Indices are *grid* coordinates (a merged cell
2304
+ reports its top-left corner), because that is what the geometry means once
2305
+ spans exist. `null` when the cursor is outside any table. */
2306
+ declare function findTableContext(state: EditorState): TableContext | null;
2307
+ /**
2308
+ * Appends a column at the table's end — what the editor's `+` pill on the
2309
+ * table's right flank commits (the `ColumnResize` NodeView renders it).
2310
+ * Position-addressed rather than selection-relative, because the pill knows
2311
+ * which table it belongs to regardless of where the caret is. Runs the same
2312
+ * width rescale as every other column add.
2313
+ */
2314
+ declare function addColumnAtEnd(tablePos: number): Command;
2315
+ /** The row twin: appends a row at the table's end for the pill under the
2316
+ bottom edge. No width math — rows carry none. */
2317
+ declare function addRowAtEnd(tablePos: number): Command;
2318
+ /**
2319
+ * Selects a whole row (or column) of the table at `tablePos` — what a grip
2320
+ * on the row's flank does when it is pressed.
2321
+ *
2322
+ * The selection is the handle's whole trick: once the row is a
2323
+ * {@link CellSelection}, every command the menu offers is the ordinary
2324
+ * selection-relative one — insert, delete, fill, align — and the row lights
2325
+ * up while its menu is open, which is the feedback the gesture needs. The
2326
+ * library builds the selection out of the two cells at the row's ends, which
2327
+ * is also how a shift-drag across the row ends up.
2328
+ */
2329
+ declare function selectRow(tablePos: number, index: number): Command;
2330
+ /** The column twin. */
2331
+ declare function selectColumn(tablePos: number, index: number): Command;
2332
+ /**
2333
+ * Moves the selected row one place up or down (`offset` −1 or 1) — the
2334
+ * menu's Move row up/down, and the gesture a grip exists for.
2335
+ *
2336
+ * Rows move as whole nodes: the table's children are reordered and written
2337
+ * back in one transaction, so nothing is re-derived and the move is one undo
2338
+ * step. A merged cell reaching across either boundary makes the move a lie —
2339
+ * the grid would have to be rebuilt around it, and "move" is not a promise
2340
+ * to restructure — so the command refuses, and the menu item shows disabled.
2341
+ */
2342
+ declare function moveRow(offset: number): Command;
2343
+ /** Duplicates the selected row (or rows) straight below — the menu's
2344
+ Duplicate row. Refused where a merged cell crosses the block's edges, for
2345
+ the same reason a move is. */
2346
+ declare function duplicateRow(): Command;
2347
+ /** The column twins. A column is not a node — it is one cell per row — so
2348
+ these rebuild every row with its cells reordered (or copied), which is
2349
+ exactly what the browser's own column-less table model forces. The same
2350
+ span rule applies: a cell spanning a boundary the column crosses refuses
2351
+ the move. */
2352
+ declare function moveColumn(offset: number): Command;
2353
+ declare function duplicateColumn(): Command;
2354
+ /** Backspace/Delete with a whole structural unit selected: the user marked
2355
+ it — deleting "all the content" of a table, row or column *is* deleting
2356
+ the unit, and leaving an empty husk behind would be the one thing that
2357
+ selection did not ask for. Full grid → the table; full-width rows → those
2358
+ rows; full-height columns → those columns. Any lesser selection returns
2359
+ false and falls through to `deleteCellSelection` (clear contents), the
2360
+ library default. */
2361
+ declare const deleteFullySelected: Command;
2362
+ /** The cell the caret is in, as its position in the document — null for a
2363
+ cell selection (which knows its cells) and outside any table. */
2364
+ declare function caretCellPos(state: EditorState): number | null;
2365
+ /** Deletes the content of the caret's cell, or of every selected cell. */
2366
+ declare const clearCells: Command;
2367
+
2368
+ type ColumnsAlignment = 'left' | 'center' | 'right';
2369
+ /**
2370
+ * The block's own alignment, expressed as auto margins against its `max-width`.
2371
+ *
2372
+ * Auto margins are normally the thing to distrust in email (Outlook's Word
2373
+ * engine handles them poorly), but they are *safe here by the same pairing
2374
+ * logic as `width: 100%` + `max-width`*: Outlook ignores `max-width` entirely,
2375
+ * so the container spans the full width there and has nothing to align — the
2376
+ * only clients where the cap is visible are the ones that honour auto margins.
2377
+ *
2378
+ * `left` carries **no declaration** — the same rule the email paragraph uses for
2379
+ * its default, so an unaligned block stays free of styling. Longhand only
2380
+ * (never the `margin: 0 auto` shorthand): shorthands re-serialize
2381
+ * non-deterministically through the CSSOM and break the canonical fixpoint.
2382
+ */
2383
+ declare const containerStyle: (align: ColumnsAlignment) => string;
2384
+ /**
2385
+ * The most columns a block will grow to. At 4 the even split is already down
2386
+ * to ~140px per column — enough for an icon-and-caption feature row, but the
2387
+ * floor for readable prose; a fifth would put every column below it. The cap
2388
+ * lives on the *command* (the affordance is the enforcement, like the colour
2389
+ * palette): authored markup with more columns still parses — parsing is
2390
+ * repair, not opinion.
2391
+ */
2392
+ declare const MAX_COLUMNS = 4;
2393
+ /** No column cap below this: ~an icon-and-caption column. The boundary drag
2394
+ clamps here from both sides. */
2395
+ declare const MIN_COLUMN_CAP = 120;
2396
+ /** Every column's px cap, in order — the block's geometry, straight from the
2397
+ model: a column's left edge is the sum of the caps before it. */
2398
+ declare function columnCaps(columns: Node): number[];
2399
+ /**
2400
+ * Sets the caps of the two columns meeting at `boundary` — what the boundary
2401
+ * drag commits. `leftCap` is the left column's new cap in px; the pair's
2402
+ * total is conserved (the block's budget never changes, so a phone still
2403
+ * stacks exactly as before), and both sides clamp at {@link MIN_COLUMN_CAP}.
2404
+ * Position-addressed: the NodeView knows its block wherever the caret is.
2405
+ */
2406
+ declare function setColumnsBoundary(columnsPos: number, boundary: number, leftCap: number): Command;
2407
+ /**
2408
+ * The class a stacking column carries in the sent email — the hook of the
2409
+ * one rule the email document's head holds ({@link STACK_RULE}): on a phone
2410
+ * a stacked column takes the whole width instead of its desktop cap. Where
2411
+ * a client drops the head, the column keeps its cap: narrower, still whole.
2412
+ * The editor's HTML never carries it ({@link markStackingColumns} adds it
2413
+ * at send), so the canonical form stays class-free.
2414
+ */
2415
+ declare const STACK_CLASS = "aee-stack";
2416
+ /** The phone rule for {@link STACK_CLASS} — MJML's breakpoint. */
2417
+ declare const STACK_RULE: string;
2418
+ /** Canonical HTML with every stacking column carrying {@link STACK_CLASS}
2419
+ — for the email document only. */
2420
+ declare function markStackingColumns(html: string): string;
2421
+ /** A single column: an `inline-block` div, recognised on parse by that style
2422
+ (a `<div>`, so it never collides with the inline-block button `<a>`). */
2423
+ declare const Column: _h_k_dev_angular_email_editor.NodeExtension;
2424
+ /** The column container: a plain `<div>` whose direct children are column
2425
+ divs. The child check discriminates it from an ordinary paragraph div. */
2426
+ declare const Columns: _h_k_dev_angular_email_editor.NodeExtension;
2427
+ /** Exposed for the app to detect when the cursor is inside a columns block. */
2428
+ declare function findColumnsContext(state: EditorState): {
2429
+ pos: number;
2430
+ node: Node;
2431
+ } | null;
2432
+ /** The single `column` enclosing the selection (the innermost, if nested).
2433
+ Exposed so the app can route a fill to the column the cursor sits in. */
2434
+ declare function findColumnContext(state: EditorState): {
2435
+ pos: number;
2436
+ node: Node;
2437
+ } | null;
2438
+ /** Inserts an empty column after the cursor's, up to {@link MAX_COLUMNS};
2439
+ every cap re-splits evenly and the cursor lands in the new column. */
2440
+ declare const addColumn: Command;
2441
+ /** Deletes the cursor's column and re-splits the rest evenly. Refuses on the
2442
+ last column — emptying the block is {@link deleteColumns}' job, on its own
2443
+ explicit affordance (mirrors the table's deleteRow/deleteTable split). */
2444
+ declare const removeColumn: Command;
2445
+ /** Deletes the whole columns block (the table's `deleteTable` sibling). */
2446
+ declare const deleteColumns: Command;
2447
+ /** Sets (or clears) the `background` of the `column` the cursor is in. */
2448
+ declare function setColumnBackground(color: string | null): Command;
2449
+ /**
2450
+ * The hybrid's other half, for the email as sent: round every columns
2451
+ * block a fixed table the Word engine reads and no one else sees — one
2452
+ * cell per column, the column's cap as its width — inside `[if mso]`
2453
+ * conditional comments, before, between and after the fluid columns.
2454
+ * Outlook lays the columns side by side at their caps (it ignores
2455
+ * `inline-block` and `max-width`, and would stack them); every other
2456
+ * client drops the comments and keeps the fluid divs. MJML's own way, and
2457
+ * the one way to a row in Outlook. A serializer's pass over the rendered
2458
+ * DOM (`serializeToHTML`): the comments stand between the columns, where
2459
+ * no node's own `emitDOM` can put them; on parse they are dropped, and
2460
+ * the block re-emits them, so the round trip holds.
2461
+ */
2462
+ declare function outlookColumns(root: HTMLElement): void;
2463
+
2464
+ /** How wide a section's content runs: the email's container, centred in
2465
+ the band. */
2466
+ declare const SECTION_CONTENT_MAX = 600;
2467
+ /** A band's default breathing room, above and below — MJML's own. */
2468
+ declare const SECTION_PADDING = "20px 0px";
2469
+ /** The content column's inline inset inside the band. */
2470
+ declare const SECTION_INSET = 16;
2471
+ /**
2472
+ * A section: a full-width band of the email — a fill running edge to edge
2473
+ * across the reader's window with the content centred in the 600px column
2474
+ * inside it — the one layout effect a 600px table cannot produce, and what
2475
+ * makes a newsletter read as header band, hero, footer rather than one
2476
+ * white sheet. MJML's `mj-section`, rendered our way.
2477
+ *
2478
+ * **Rendered once, for every client.** MJML renders a section twice — a
2479
+ * `<div>` for the clients that read `max-width`, and the same content again
2480
+ * in a 600px table inside an Outlook-only conditional comment — because
2481
+ * Outlook's Word engine ignores `max-width` and `inline-block`. Gmail drops
2482
+ * every comment, and its app strips the head for non-Google accounts, so
2483
+ * what both keep is what this emits: one `role="presentation"` table,
2484
+ * 100% wide, its one cell carrying the fill as `bgcolor` *and* as an
2485
+ * inline `background-color` (Outlook reads the attribute, everyone else
2486
+ * the style), and inside it a `<div>` capped at 600px with auto margins.
2487
+ * Where that div cannot be honoured (Outlook) the content runs the band's
2488
+ * full width — the graceful half of the same bargain the columns block
2489
+ * strikes. Nothing here needs a stylesheet or a comment.
2490
+ *
2491
+ * **The fill pairs its text.** A filled band writes an explicit text colour
2492
+ * (`fillTextColor`), since the dark modes that flip default text keep the
2493
+ * fill; the swatches are the dual-safe background palette, so both survive
2494
+ * a forced inversion. `bgcolor` carries the same colour as hex for Outlook.
2495
+ *
2496
+ * **A background image, for both clients.** A band may carry an image
2497
+ * behind its content (MJML's `background-url`): the cell gets it as a
2498
+ * `background` attribute *and* as inline `background-image`, centred,
2499
+ * covering, not repeating — Gmail and the rest read those — with the fill
2500
+ * colour beneath for the clients that hold images back. Outlook's Word
2501
+ * engine reads neither, and draws VML: the cell's content is wrapped in a
2502
+ * `v:rect` with a `v:fill` of the image, inside `[if mso]` conditional
2503
+ * comments that every other client discards. That is the one place the
2504
+ * email carries a comment — MJML's translation of the same idea, and the
2505
+ * only way to a picture behind text in Outlook; without it the colour
2506
+ * shows, which is the fallback anyway. A band without an image emits no
2507
+ * comment at all.
2508
+ *
2509
+ * **On parse** a section is a one-cell presentation table whose cell holds
2510
+ * nothing but elements and carries a fill, an image or a padding — a
2511
+ * builder's section (MJML's, with the fill on its table or wrapping div,
2512
+ * the image as the table's `background` or a `url()` in a style) as much
2513
+ * as our own. A one-cell table with words in it is a table, and stays one.
2514
+ * Only an `http(s)` image is taken.
2515
+ */
2516
+ declare const Section: _h_k_dev_angular_email_editor.NodeExtension;
2517
+ /** Whether a URL may stand as a band's image: `http(s)` only — never a
2518
+ script, a data URL (Gmail shows nothing for it) or a file. */
2519
+ declare function isSectionImageUrl(url: string | null): url is string;
2520
+ /** The background an element declares inline — the CSSOM's reading, else
2521
+ the attribute's own words (`background: #fff` as a builder writes it,
2522
+ which not every engine expands). */
2523
+ declare function declaredBackground(el: Element): string;
2524
+ /** The section around the cursor, innermost. */
2525
+ declare function findSectionContext(state: EditorState): {
2526
+ pos: number;
2527
+ node: Node;
2528
+ } | null;
2529
+ /** Sets (or clears) the fill of the section the cursor is in. */
2530
+ declare function setSectionBackground(color: string | null): Command;
2531
+ /** Puts an image behind (or takes it from) the section the cursor is in.
2532
+ A URL that is not `http(s)` is refused: the command answers false. */
2533
+ declare function setSectionImage(url: string | null): Command;
2534
+
2535
+ /** Depth of the innermost list wrapping the selection start, or null. */
2536
+ declare const findListDepth: ($from: ResolvedPos) => number | null;
2537
+ /**
2538
+ * Wraps the selection in a list, lifts out of it when already in that list,
2539
+ * or converts the list in place when it is of the other flavour.
2540
+ */
2541
+ declare const toggleList: (listType: NodeType, itemType: NodeType) => Command;
2542
+ /**
2543
+ * Tiptap-inspired helper: Intercepts the default splitListItem transaction
2544
+ * and ensures marks (bold, italic, etc.) are carried over to the new list item.
2545
+ */
2546
+ declare const splitListItemKeepMarks: (itemType: NodeType) => Command;
2547
+ /** The ledger's answer to the 40px-per-level default indent that eats a
2548
+ third of a phone screen: a small explicit indent, no margins (lines carry
2549
+ none). Nested lists compound 24px per level, readable at 320px. */
2550
+ declare const LIST_STYLE = "margin: 0px; padding-left: 24px;";
2551
+ declare const ListItem: _h_k_dev_angular_email_editor.NodeExtension;
2552
+ declare const BulletList: _h_k_dev_angular_email_editor.NodeExtension;
2553
+ declare const OrderedList: _h_k_dev_angular_email_editor.NodeExtension;
2554
+
2555
+ /** What a menu lets its scroll behaviour do. */
2556
+ interface MenuScrollApi {
2557
+ /** Report the menu closed; the next selection change brings it back. */
2558
+ close(): void;
2559
+ /** Report the menu's box afresh, where it is open. */
2560
+ refresh(): void;
2561
+ }
2562
+ /**
2563
+ * What a floating menu does when what it stands over scrolls — the
2564
+ * editor's own scroll container, or the page:
2565
+ *
2566
+ * - `'close'` (the default): the menu goes. The box it reported is in
2567
+ * viewport coordinates, and stale the moment the editor scrolls under it.
2568
+ * - `'follow'`: the box is reported afresh, once per frame, so a host that
2569
+ * repositions on the box's change keeps the menu over the selection.
2570
+ * - `'keep'`: nothing — the menu stays where it was reported.
2571
+ * - a function: the host's own say, per event — close for the editor's
2572
+ * scroller but follow for the page, or close past a threshold.
2573
+ *
2574
+ * The listener is the window's, in the capture phase, so every scroll
2575
+ * container on the page is heard without being marked (no `cdkScrollable`
2576
+ * needed), and only a scroll that moves the editor counts: the document's,
2577
+ * or an ancestor's. A scrollable list inside the host's own menu never
2578
+ * closes the menu it is in.
2579
+ */
2580
+ type MenuScroll = 'close' | 'follow' | 'keep' | ((event: Event, menu: MenuScrollApi) => void);
2581
+ /** Starts watching for {@link MenuScroll}; returns what stops it. */
2582
+ declare function watchMenuScroll(view: EditorView, scroll: MenuScroll | undefined, menu: MenuScrollApi): () => void;
2583
+
2584
+ interface BubbleMenuState {
2585
+ isOpen: boolean;
2586
+ boundingBox: DOMRect | null;
2587
+ /** What the menu is for: `'image'` when an image is the whole selection
2588
+ — clicked, or dragged over with nothing but whitespace beside it
2589
+ (`selectedImage`); `'button'` likewise for a button (`selectedButton`)
2590
+ — the box is then the node's own; `'text'` otherwise: a range with
2591
+ text in it, or with an image and a button both. A host shows each
2592
+ one's own tools. Absent while closed. */
2593
+ kind?: 'text' | 'image' | 'button';
2594
+ }
2595
+ interface BubbleMenuOptions {
2596
+ /** How long a selection stands before the menu *opens* over it (150ms):
2597
+ a drag's or a Shift-Arrow's intermediate ranges never pop a menu. A
2598
+ menu that is already open follows the selection at once — see the
2599
+ plugin view. */
2600
+ updateDelay?: number;
2601
+ onStateChange: (state: BubbleMenuState) => void;
2602
+ shouldShow?: (state: EditorState) => boolean;
2603
+ /** What the menu does when the editor scrolls under it — closes, by
2604
+ default. See {@link MenuScroll}. */
2605
+ scroll?: MenuScroll;
2606
+ }
2607
+ declare const defaultBubbleShouldShow: (state: EditorState) => boolean;
2608
+ declare const createBubbleMenu: (options: BubbleMenuOptions) => FunctionalExtension;
2609
+
2610
+ /** The layout blocks the editor treats as structures in their own right: their
2611
+ shape is invisible in the exported email (our tables are borderless; columns
2612
+ are bare inline-block divs), so the editor has to reveal it — and they carry
2613
+ block-level commands (align, add/remove row…) that need somewhere to live.
2614
+ One definition, shared by the guides and the block menu. */
2615
+ type LayoutBlockType = 'table' | 'columns' | 'section';
2616
+ interface LayoutBlock {
2617
+ pos: number;
2618
+ node: Node;
2619
+ type: LayoutBlockType;
2620
+ }
2621
+ /** The innermost `table`/`columns`/`section` ancestor of the selection, or null. */
2622
+ declare function layoutBlockAt(state: EditorState): LayoutBlock | null;
2623
+ /** Class on the layout block the cursor is currently inside. */
2624
+ declare const GUIDES_ACTIVE_CLASS = "aee-guides-active";
2625
+ /** Class on the editor root while the peek modifier is held — reveals *every*
2626
+ layout block's guides at once. */
2627
+ declare const GUIDES_PEEK_CLASS = "aee-guides-peek";
2628
+ /**
2629
+ * How long Ctrl must be held before guides appear. Ctrl is also the prefix for
2630
+ * every shortcut (Ctrl-B, Ctrl-C, Ctrl-Z), so revealing instantly would make the
2631
+ * grid flash on every one of them. A hold delay — cancelled by any other key —
2632
+ * separates "I'm peeking at the layout" from "I'm running a shortcut".
2633
+ *
2634
+ * Kept just under the ~100ms that still reads as instant: a real chord presses
2635
+ * its letter 30–80ms after the modifier, so this catches shortcuts while a
2636
+ * deliberate hold feels immediate. The rest of the debouncing is perceptual —
2637
+ * the guides *fade* in (see the `.aee-guides-*` transition in the app's global
2638
+ * styles), so anything slipping past this timer is a soft partial fade rather
2639
+ * than a hard blink. Longer than this and a deliberate peek just feels laggy.
2640
+ */
2641
+ declare const GUIDES_PEEK_DELAY_MS = 100;
2642
+ /**
2643
+ * One shared mechanism for showing the structure of layout blocks (tables and
2644
+ * columns alike) while editing — never in the email itself.
2645
+ *
2646
+ * Three ways in, all resolved in CSS against a border that is *always reserved
2647
+ * as transparent*, so revealing a guide never shifts layout:
2648
+ * 1. **Cursor inside** — the block being edited is tagged {@link GUIDES_ACTIVE_CLASS}.
2649
+ * 2. **Hover** — pure CSS on the block itself; nothing to coordinate here.
2650
+ * 3. **Peek** — hold Ctrl ({@link GUIDES_PEEK_DELAY_MS}) to reveal every block
2651
+ * at once via {@link GUIDES_PEEK_CLASS} on the editor root.
2652
+ *
2653
+ * The app owns the pixels (see the `.aee-guides-*` rules in its global styles);
2654
+ * this extension only supplies the state.
2655
+ */
2656
+ declare const LayoutGuides: _h_k_dev_angular_email_editor.FunctionalExtension;
2657
+
2658
+ interface BlockMenuState {
2659
+ isOpen: boolean;
2660
+ /** The layout block's own rect — the menu anchors to the block, not the caret. */
2661
+ boundingBox: DOMRect | null;
2662
+ /** Which block the cursor is in, so the app can offer its commands. */
2663
+ block: LayoutBlockType | null;
2664
+ }
2665
+ interface BlockMenuOptions {
2666
+ onStateChange: (state: BlockMenuState) => void;
2667
+ /**
2668
+ * Resolves the rendered menu element, lazily — it lives in an overlay, so it
2669
+ * only exists while the menu is open.
2670
+ *
2671
+ * Without it the menu is **mouse-only**: it stays open only while the editor
2672
+ * has focus, so Tabbing toward it blurs the editor and closes the very thing
2673
+ * you were reaching for. Given this, focus landing inside the menu counts as
2674
+ * still-active, which is what makes {@link BLOCK_MENU_FOCUS_KEY} usable.
2675
+ */
2676
+ menuElement?: () => HTMLElement | null | undefined;
2677
+ /** What the menu does when the editor scrolls under it — closes, by
2678
+ default. See {@link MenuScroll}. */
2679
+ scroll?: MenuScroll;
2680
+ }
2681
+ /**
2682
+ * Moves focus into the menu — the ARIA "jump to toolbar" gesture, and the only
2683
+ * way to reach a contextual toolbar that is anchored to the caret's block.
2684
+ * Alt-F10 follows CKEditor's convention; bare F10 is out because browsers give
2685
+ * it to their own menu bar, and Alt alone would too (a chord is safe).
2686
+ */
2687
+ declare const BLOCK_MENU_FOCUS_KEY = "Alt-F10";
2688
+ /**
2689
+ * The block menu: the bubble menu's sibling for *layout blocks*.
2690
+ *
2691
+ * The bubble menu answers "what can I do to this text?" and anchors to the
2692
+ * selection. This answers "what can I do to this block?" and anchors to the
2693
+ * block itself. It exists because block-level commands kept piling up with
2694
+ * nowhere to live — the table's structural commands (`addRowAt`, `deleteColumnAt`,
2695
+ * …) have been library-only, and column alignment would have been the third
2696
+ * feature to hit the same wall.
2697
+ *
2698
+ * It opens on a **bare cursor** inside a table/columns block, so it never
2699
+ * competes with the text bubble menu (which needs a non-empty selection) —
2700
+ * the two are mutually exclusive by construction, never stacked.
2701
+ *
2702
+ * Reachable by keyboard: press {@link BLOCK_MENU_FOCUS_KEY} to move focus into
2703
+ * it, Escape (wired by the app) to come back. See {@link BlockMenuOptions.menuElement}
2704
+ * for why that needs the element.
2705
+ *
2706
+ * Deliberately *not* the reverted Notion-style overlay: no pointer tracking, no
2707
+ * hover handles, no steppers. It is one calm toolbar, positioned by the app.
2708
+ */
2709
+ declare const createBlockMenu: (options: BlockMenuOptions) => FunctionalExtension;
2710
+
2711
+ interface SuggestionMenuState {
2712
+ open: boolean;
2713
+ /** The trigger this state answers to — `/`, `{{`, `@`. A menu of several
2714
+ triggers reports the one that is open. */
2715
+ trigger: string;
2716
+ /** What that trigger's list is called ({@link SuggestionTrigger.label}) —
2717
+ the listbox's accessible name on level 1. Null when it was given none. */
2718
+ label: string | null;
2719
+ /** 1 on the menu's own items; 2 inside a {@link SuggestionGroup} —
2720
+ `<trigger><word> <query>`. */
2721
+ level: 1 | 2;
2722
+ /** The group whose children are listed, at level 2; null at level 1. */
2723
+ parent: SuggestionGroup | null;
2724
+ /** The search string of the current level: everything after the trigger
2725
+ at level 1, everything after `<word> ` at level 2. */
2726
+ query: string;
2727
+ /** Items matching the query: static matches ranked title-first (given
2728
+ order within a tier), then what a source answered for this query — the
2729
+ menu's {@link SuggestionMenuOptions.source} at level 1, the group's
2730
+ children source at level 2. Titles and keywords are already translated. */
2731
+ items: SuggestionItem[];
2732
+ /** Index of the keyboard-highlighted item. */
2733
+ activeIndex: number;
2734
+ /** A source's first page for the current query is in flight (or waiting
2735
+ out {@link SuggestionMenuOptions.debounce}) — render a "Searching…"
2736
+ row. The menu counts as open while loading, even with no items yet. */
2737
+ loading: boolean;
2738
+ /** The source's items on screen answer the *previous* query: they stay
2739
+ until the new page lands, so the list is diffed rather than torn down
2740
+ and rebuilt on every keystroke. Enter and Tab pass over a stale row. */
2741
+ stale: boolean;
2742
+ /** Where the session sits in the document: the trigger's first character
2743
+ to the caret. Null while closed. */
2744
+ range: {
2745
+ from: number;
2746
+ to: number;
2747
+ } | null;
2748
+ /** The trigger's box in viewport coordinates, measured when called — for
2749
+ the renderer to place the menu after it has rendered its rows. Null
2750
+ while closed. */
2751
+ clientRect: () => SuggestionRect | null;
2752
+ /** A further page is in flight — render a loading row at the list's end. */
2753
+ loadingMore: boolean;
2754
+ /** The source has more pages: the renderer's scroll should call
2755
+ {@link loadMore} near the end (ArrowDown on the last row already does). */
2756
+ hasMore: boolean;
2757
+ /** Why the source's last answer failed; null while it has not. Cleared by
2758
+ the next query. The menu stays open to say so. */
2759
+ error: unknown;
2760
+ /** The id the listbox element must carry: the editor points at it
2761
+ (`aria-controls`) while the menu is open. */
2762
+ listboxId: string;
2763
+ /** The id the option at `index` must carry (with `role="option"`): the
2764
+ editor points at the highlighted one (`aria-activedescendant`), and the
2765
+ pointer's highlight finds its row by it. */
2766
+ optionId: (index: number) => string;
2767
+ /** The heading for a section id (an item's `section`): the trigger's
2768
+ `sections` wording, else the library's own ({@link
2769
+ suggestionSectionTitles}), else the id itself. Asked as headings are
2770
+ rendered, so a function given as `sections` is heard afresh. */
2771
+ sectionTitle: (id: string) => string;
2772
+ /** Applies an item. A command item removes the trigger and query text,
2773
+ then runs its command; a group rewrites the query to `<word> `,
2774
+ opening level 2. */
2775
+ select: (item: SuggestionItem) => void;
2776
+ /** Level 2 back to level 1 with an empty query — the pointer's way; the
2777
+ keyboard's is Backspace over the space. A no-op at level 1. */
2778
+ back: () => void;
2779
+ /** Fetches the next page and appends it. A no-op while anything is loading,
2780
+ when no page is left, or when the menu is closed — safe from any scroll
2781
+ handler without guards. */
2782
+ loadMore: () => void;
2783
+ /** Closes the menu as Escape does — for a renderer's own close control. It
2784
+ stays shut for this trigger until the caret has been somewhere else. A
2785
+ no-op while closed. */
2786
+ dismiss: () => void;
2787
+ }
2788
+ /** A box in viewport coordinates. */
2789
+ interface SuggestionRect {
2790
+ left: number;
2791
+ right: number;
2792
+ top: number;
2793
+ bottom: number;
2794
+ }
2795
+ /** A translation for one item, keyed by its id in
2796
+ {@link SuggestionMenuOptions.i18n}. */
2797
+ interface SuggestionItemLabel {
2798
+ /** Replaces the item's title — shown, ranked first, and (for a group)
2799
+ the word written into the query. The original title stays searchable. */
2800
+ title?: string;
2801
+ /** Extra search strings, added to the item's own — never replacing them,
2802
+ so the original words keep working in every locale. */
2803
+ keywords?: string[];
2804
+ /** Replaces a group's `placeholder`. */
2805
+ placeholder?: string;
2806
+ }
2807
+ /** What a trigger's {@link SuggestionTrigger.allow} is asked about: a session
2808
+ that would open. */
2809
+ interface SuggestionAllowProps {
2810
+ state: EditorState;
2811
+ /** The trigger's first character to the caret. */
2812
+ range: {
2813
+ from: number;
2814
+ to: number;
2815
+ };
2816
+ /** The text after the trigger, leading whitespace dropped. */
2817
+ query: string;
2818
+ }
2819
+ /** One trigger of a suggestion menu: what opens it, what it lists, what its
2820
+ query may look like. Any string is a trigger — `'/'`, `'{{'`, `'@'`,
2821
+ `'||'`. */
2822
+ interface SuggestionTrigger {
2823
+ /** What opens the menu when typed: `'/'`, `'{{'`, `'@'`. */
2824
+ trigger: string;
2825
+ /** What this trigger's list is called — "Insert block", "Personalization
2826
+ tokens". Handed on as {@link SuggestionMenuState.label}: the listbox's
2827
+ accessible name. A function is asked afresh whenever the menu opens,
2828
+ like `i18n`: the words of the language in use. */
2829
+ label?: string | (() => string);
2830
+ /** Only at the start of a line or after whitespace — `/` in a URL stays
2831
+ text. In a script that writes no spaces (Japanese, Chinese) every
2832
+ character counts as one: `/` opens right after こんにちは. Default true. Either way a trigger right after its own first
2833
+ character never opens: not `//`, not Handlebars' `{{{`. */
2834
+ startOfWord?: boolean;
2835
+ /** What the text between the trigger and the caret may be. Default: not
2836
+ starting with whitespace (typing `/ ` dismisses), spaces inside allowed
2837
+ (`/heading 2`), and no trigger character (a later one starts a new
2838
+ session). A merge-tag menu narrows it to a path: `/^ ?[\w.]*$/`. The
2839
+ query handed on is that text with its leading whitespace dropped. */
2840
+ query?: RegExp;
2841
+ /** The longest query that still counts as one. Default 100. */
2842
+ maxLength?: number;
2843
+ /** The menu's own items — a list, or a function of the editor's extensions
2844
+ (`extensionSuggestions` for the kit's `/` commands). Filtered and
2845
+ ranked by the query; groups among them open a second level. */
2846
+ items?: readonly SuggestionItem[] | ((ctx: ExtensionContext) => readonly SuggestionItem[]);
2847
+ /**
2848
+ * The menu's dynamic items, asked per query and per page — a server
2849
+ * catalogue, a search backend. Its answers are appended after the static
2850
+ * matches, **not** re-filtered: the source owns its matching. Stale answers
2851
+ * are dropped and their requests aborted (a newer query, a dismissal, a
2852
+ * destroyed editor) — the host never race-guards. A group's children
2853
+ * source works the same way at level 2.
2854
+ */
2855
+ source?: SuggestionSource;
2856
+ /** Shown after the bare trigger, until a query is typed — "Type to
2857
+ filter…". It is handed to the session's decoration as
2858
+ `data-placeholder`; the host's stylesheet shows it (see
2859
+ {@link createSuggestionMenu}, "Marked in the text"). Inside a group the
2860
+ group's own takes its place: `/templates ` shows
2861
+ {@link SuggestionGroup.placeholder}. A function is asked afresh whenever
2862
+ the menu opens. */
2863
+ placeholder?: string | (() => string);
2864
+ /**
2865
+ * The host's veto: asked for every session the text would open, and the
2866
+ * menu stays shut — for this trigger; one further back may still open —
2867
+ * when it says no. For what the text alone cannot tell: a caret that is
2868
+ * not typing a query but standing in something already written. The `{{`
2869
+ * menu needs one — the braces before a caret *inside* `{{ firstName }}`
2870
+ * open that token, and picking a row there would write a second token into
2871
+ * the first:
2872
+ *
2873
+ * allow: ({ state }) => !caretInsideMergeTag(state)
2874
+ *
2875
+ * Asked once per editor state, so it may look at the document.
2876
+ */
2877
+ allow?: (props: SuggestionAllowProps) => boolean;
2878
+ /** Milliseconds to sit on a keystroke before asking a source for a new
2879
+ query — a server should see the settled query, not every letter.
2880
+ Default 300; 0 asks at once (an in-memory source). Static matches never
2881
+ wait, and neither does a further page. */
2882
+ debounce?: number;
2883
+ /**
2884
+ * Translations by item id, at either level: a title, extra keywords, a
2885
+ * group's placeholder. A map — or a function of the id, which is asked
2886
+ * afresh every time the menu opens: hand it a translation service's lookup
2887
+ * and a language switch reaches the menu, its search included, with
2888
+ * nothing to re-create. Whatever it answers is *added* to the item's own
2889
+ * words, so the original language keeps matching beside the chosen one.
2890
+ */
2891
+ i18n?: Readonly<Record<string, SuggestionItemLabel>> | ((id: string) => SuggestionItemLabel | undefined);
2892
+ /**
2893
+ * The headings of the items' sections (`SuggestionItem.section`), by id:
2894
+ * a map, or a function of the id, asked whenever a heading is rendered —
2895
+ * hand it a translation service's lookup and a language switch reaches
2896
+ * the headings too. What it leaves out is worded by the library
2897
+ * ({@link suggestionSectionTitles}), and an id neither knows shows as it
2898
+ * is.
2899
+ */
2900
+ sections?: Readonly<Record<string, string>> | ((id: string) => string | undefined);
2901
+ /** The listbox id ({@link SuggestionMenuState.listboxId}); unique by default. */
2902
+ id?: string;
2903
+ }
2904
+ /** The library's wording of the sections its own actions declare — and of
2905
+ the ones a host commonly adds: its palette's rows (`color`), its groups. */
2906
+ declare const suggestionSectionTitles: Readonly<Record<string, string>>;
2907
+ /**
2908
+ * A suggestion menu: the element it shows in, and the triggers that open it
2909
+ * — one, written inline, or several in `triggers`:
2910
+ *
2911
+ * createSuggestionMenu({ element, onChange, trigger: '/', items: extensionSuggestions })
2912
+ * createSuggestionMenu({ element, onChange, triggers: [
2913
+ * { trigger: '/', label: 'Insert block', items: extensionSuggestions },
2914
+ * { trigger: '{{', label: 'Tokens', startOfWord: false, source: fetchMergeTags },
2915
+ * { trigger: '||', label: 'Snippets', source: fetchSnippets },
2916
+ * ] })
2917
+ */
2918
+ interface SuggestionMenuOptions extends Partial<SuggestionTrigger> {
2919
+ /**
2920
+ * The menu's element, rendered by the host from the
2921
+ * {@link SuggestionMenuState} it receives through `onChange`. The plugin
2922
+ * never writes to it — placing and showing it is the renderer's, after its
2923
+ * rows are in (see `clientRect`) — it only listens: a press on it keeps the
2924
+ * editor's focus, a press anywhere else dismisses.
2925
+ */
2926
+ element: HTMLElement;
2927
+ /** Further triggers that open this same menu — after the inline one, when
2928
+ there is one. They never open together (the trigger nearest the caret
2929
+ wins), so one element and one `onChange` serve them all: the state says
2930
+ whose list it is (`trigger`, `label`, `listboxId`). */
2931
+ triggers?: readonly SuggestionTrigger[];
2932
+ /** Notified when the menu opens, closes, filters, loads, or moves — with
2933
+ the state of the trigger that is open, or of the one that just closed. */
2934
+ onChange?: (state: SuggestionMenuState) => void;
2935
+ }
2936
+ /**
2937
+ * A suggestion menu: typing `trigger` opens a list under the caret, the text
2938
+ * after it searches the list, and picking an item replaces trigger and query
2939
+ * with what the item does. One factory for every such menu — the `/` command
2940
+ * menu and the `{{` merge-tag picker are two triggers with different
2941
+ * options, in one menu (one element, one state) or in a menu each:
2942
+ *
2943
+ * createSuggestionMenu({ element, onChange, triggers: [
2944
+ * { trigger: '/', items: extensionSuggestions },
2945
+ * { trigger: '{{', startOfWord: false, query: /^ ?[\w.]*$/, source: fetchMergeTags },
2946
+ * ] })
2947
+ *
2948
+ * The host renders the list from the state (the library's
2949
+ * `email-suggestion-menu` pair does it); this plugin does the rest —
2950
+ * detection, filtering, the paged and debounced source, keyboard navigation,
2951
+ * applying. It never writes to the menu element: showing and placing it are
2952
+ * the renderer's, after its rows are in (`clientRect` says where).
2953
+ *
2954
+ * **Read from the text.** A session is a caret right after
2955
+ * `<trigger><query>`, however the text got there, and nothing else is held:
2956
+ * undo, paste, and moving the caret back into a query all land right.
2957
+ *
2958
+ * **Two levels.** A {@link SuggestionGroup} ("Templates") picked on level 1
2959
+ * rewrites the query to `templates `; from then on the words after that
2960
+ * space search the group's children. Backspace over the space is level 1
2961
+ * again.
2962
+ *
2963
+ * **One at a time.** Several menus on one editor never open together: when
2964
+ * two sessions overlap (`/templates {{`), the trigger nearest the caret wins
2965
+ * and the other stays shut until it is gone.
2966
+ *
2967
+ * **Marked in the text.** While a session lasts, its trigger and query are
2968
+ * wrapped in a decoration — `<span class="aee-suggestion" data-trigger="/">`,
2969
+ * with `data-level`, and with `aee-suggestion-empty` and a
2970
+ * `data-placeholder` while the level's query is empty (the trigger's after
2971
+ * `/`, the group's after `/templates `) — for the host's stylesheet: the
2972
+ * library paints nothing.
2973
+ * Nothing of it is in the document or the HTML. The session, and the place
2974
+ * where Escape dismissed one, live in the plugin's state and move with the
2975
+ * text: what a sync writes before a dismissed trigger does not reopen it.
2976
+ *
2977
+ * **One highlight.** The arrows move it and so does the pointer, over the
2978
+ * rows that carry the state's option ids — hover is not a second one.
2979
+ *
2980
+ * **The editor is the combobox.** Focus never leaves it: while the menu is
2981
+ * open the editable carries `aria-controls` (the listbox) and
2982
+ * `aria-activedescendant` (the highlighted option), whose ids the state
2983
+ * hands the renderer.
2984
+ */
2985
+ declare const createSuggestionMenu: (options: SuggestionMenuOptions) => FunctionalExtension;
2986
+
2987
+ /** Where a stream is writing: what it has written so far. */
2988
+ interface ContentStreamRange {
2989
+ from: number;
2990
+ to: number;
2991
+ }
2992
+ /** What the extension reports: whether something is streaming in, and where. */
2993
+ interface ContentStreamState {
2994
+ streaming: boolean;
2995
+ range: ContentStreamRange | null;
2996
+ }
2997
+ /** How a stream shows what has come in — see {@link ContentStreamOptions.reveal}. */
2998
+ type ContentStreamReveal = 'block' | 'character' | 'instant';
2999
+ interface ContentStreamOptions {
3000
+ /** A caret after the last thing written — a widget, `span.aee-stream-caret`,
3001
+ for the host's stylesheet; the library paints nothing. Default true. */
3002
+ caret?: boolean;
3003
+ /**
3004
+ * How what has come in is *revealed*. A model's tokens come in bursts — a
3005
+ * word, a pause, three words — and written as they come they read like a
3006
+ * slide show.
3007
+ *
3008
+ * - `'block'` (the default): a block — a paragraph, a list item, a line of
3009
+ * plain text — shows *whole*, once all of it is in: when the next one
3010
+ * starts, or the stream ends. Nothing types; blocks that land together
3011
+ * follow each other a beat apart. The caret waits after the last one.
3012
+ * - `'character'`: a steady pace, a character or a few a frame, never
3013
+ * splitting one: faster when it falls behind, slower as it catches up.
3014
+ * - `'instant'`: each piece the moment it comes.
3015
+ *
3016
+ * The stream ends — and `done` settles — once all of it shows.
3017
+ */
3018
+ reveal?: ContentStreamReveal;
3019
+ /**
3020
+ * How long, in ms, what was just revealed is *fresh*: it carries
3021
+ * `aee-stream-fresh` — with `--email-stream-fade-in` set to this duration
3022
+ * — for the host's stylesheet to fade it in; the library paints nothing.
3023
+ * A block revealed whole carries it *itself* (the `p`, the `li`), so it
3024
+ * can be wiped or moved as well as faded; text that joins a line already
3025
+ * there, and everything under `'character'`, is wrapped in a `span`.
3026
+ * Default 600; 0 marks nothing.
3027
+ */
3028
+ fadeIn?: number;
3029
+ /** Escape stops the stream mid-way. Default true. */
3030
+ stopOnEscape?: boolean;
3031
+ /** Told when a stream starts, writes, and ends. */
3032
+ onChange?: (state: ContentStreamState) => void;
3033
+ }
3034
+ /** What {@link streamContent}'s callback writes with. */
3035
+ interface ContentStreamWriter {
3036
+ /** Appends a piece of the content — a token, a word, a chunk of a
3037
+ response body. A no-op once the stream has been stopped. */
3038
+ write(partial: string): void;
3039
+ /** A `WritableStream` that writes what is piped into it — text, or bytes
3040
+ decoded as UTF-8: `response.body.pipeTo(writer.getWritableStream())`. */
3041
+ getWritableStream(): WritableStream<string | Uint8Array>;
3042
+ /** Aborts when the stream is stopped — Escape, `stop()`, the editor going
3043
+ away. Hand it to `fetch` or to the model's client. */
3044
+ readonly signal: AbortSignal;
3045
+ }
3046
+ interface StreamContentOptions {
3047
+ /**
3048
+ * What the pieces are. `'text'` (the default) is appended as it comes.
3049
+ * `'html'` is *re-read as a whole* on every write: the pieces collect in a
3050
+ * buffer, the buffer is parsed through the schema, and what has been
3051
+ * written so far is replaced by the result — so a list or a bold word
3052
+ * takes shape as it streams, and a tag split over two pieces never shows
3053
+ * as text.
3054
+ */
3055
+ format?: 'text' | 'html';
3056
+ /** Rewrites the buffer before it is parsed (`'html'`) — strip a code
3057
+ fence, turn Markdown into HTML. Given everything received so far. */
3058
+ transform?: (buffer: string) => string;
3059
+ /** Whether what is written enters the undo history. Default true: the
3060
+ pieces undo as one. `false` keeps the writing out of it — for content
3061
+ that is only *proposed* (see `proposeContent`), which is committed or
3062
+ taken back as a whole later, so that neither undoes anything else. */
3063
+ history?: boolean;
3064
+ }
3065
+ /** A stream in progress. */
3066
+ interface ContentStreamRun {
3067
+ /** Stops it where it is: what shows stays, what has come in but not shown
3068
+ yet does not. */
3069
+ stop(): void;
3070
+ /** True once the callback has finished and all of it shows; false when it
3071
+ was stopped first. Rejects with what the callback threw, if it was not
3072
+ an abort. */
3073
+ readonly done: Promise<boolean>;
3074
+ }
3075
+ /** What a transaction of a stream says about where the stream is now —
3076
+ for another plugin (a proposal's) to follow the range as precisely as
3077
+ the stream itself does: `undefined` when the transaction is not the
3078
+ stream's own, `null` when the stream ended with it. */
3079
+ declare const streamedRange: (tr: Transaction) => ContentStreamRange | null | undefined;
3080
+ /** Whether content is streaming into the editor — for an action that starts
3081
+ one to say it cannot run again yet. */
3082
+ declare const isStreaming: (state: EditorState) => boolean;
3083
+ /** Where content is streaming in, if it is. */
3084
+ declare const streamingRange: (state: EditorState) => ContentStreamRange | null;
3085
+ /**
3086
+ * Lets content **stream into the document** — a model's answer, a slow
3087
+ * import — and owns everything around that: where the next piece goes while
3088
+ * the writer keeps typing, how it shows — block by block — a caret, the fade
3089
+ * on what is new, `aria-busy` on the editor, Escape to stop, the abort signal,
3090
+ * the end. Install it, then call {@link streamContent}:
3091
+ *
3092
+ * createEditor({ extensions: [...kit, createContentStream()] })
3093
+ *
3094
+ * streamContent(editor.view, editor.state.selection.from, async ({ write, signal }) => {
3095
+ * for await (const token of model.stream(prompt, { signal })) write(token);
3096
+ * });
3097
+ *
3098
+ * One stream at a time per editor. What it writes is ordinary content in
3099
+ * ordinary transactions: it is in the document, in the HTML, and in the undo
3100
+ * history — pieces that follow each other closely undo as one.
3101
+ */
3102
+ declare const createContentStream: (options?: ContentStreamOptions) => FunctionalExtension;
3103
+ /**
3104
+ * Streams content into the document at `target` — a position, or a range
3105
+ * that the first piece replaces ("rewrite this selection"). `callback` does
3106
+ * the writing; the stream ends when it returns and all it wrote shows.
3107
+ *
3108
+ * Needs {@link createContentStream} among the editor's extensions, and no
3109
+ * other stream running (ask {@link isStreaming}).
3110
+ */
3111
+ declare function streamContent(view: EditorView, target: number | ContentStreamRange, callback: (writer: ContentStreamWriter) => Promise<void> | void, options?: StreamContentOptions): ContentStreamRun;
3112
+
3113
+ /** What the extension reports: whether something is proposed, and where. */
3114
+ interface ContentProposalState {
3115
+ range: ContentStreamRange | null;
3116
+ /** The proposal is still being written. */
3117
+ streaming: boolean;
3118
+ }
3119
+ interface ContentProposalOptions {
3120
+ /** The class on what is proposed — `aee-proposal` — for the host's
3121
+ stylesheet: the library paints nothing. */
3122
+ className?: string;
3123
+ /** Told when a proposal starts, grows, and ends — accepted or discarded. */
3124
+ onChange?: (state: ContentProposalState) => void;
3125
+ }
3126
+ /** Whether something is proposed in the editor — written, but not the
3127
+ writer's yet. */
3128
+ declare const isProposing: (state: EditorView["state"]) => boolean;
3129
+ /** Where the proposal stands, if there is one. */
3130
+ declare const proposalRange: (state: EditorView["state"]) => ContentStreamRange | null;
3131
+ /**
3132
+ * Lets content be **proposed** into the document — an assistant's answer,
3133
+ * shown in the text where it would go, before the writer decides. It is
3134
+ * real content, in the document and in the HTML, so a list forms and a
3135
+ * bold phrase arrives bold where they will stand; but it is marked
3136
+ * (`aee-proposal`, for the host to paint), it stays out of the undo history,
3137
+ * and it ends one of two ways: {@link acceptProposal} commits it, as one
3138
+ * change — one undo — and {@link discardProposal} takes it out, leaving the
3139
+ * document and its history exactly as they were. Nothing else touches
3140
+ * either: a second proposal replaces the first, the editor going away
3141
+ * discards.
3142
+ *
3143
+ * Opt-in, on top of the content stream:
3144
+ *
3145
+ * createEditor({ extensions: [...kit, createContentStream(), createContentProposal()] })
3146
+ *
3147
+ * proposeContent(editor.view, editor.state.selection.from, async ({ write, signal }) => {
3148
+ * for await (const token of model.stream(prompt, { signal })) write(token);
3149
+ * }, { format: 'html' });
3150
+ *
3151
+ * The Angular side — `injectProposal`, the accept and discard triggers, an
3152
+ * input's keys — is `@h-k-dev/angular-email-editor/proposal`, its own entry: a host
3153
+ * that brings its own panel imports what it uses.
3154
+ */
3155
+ declare const createContentProposal: (options?: ContentProposalOptions) => FunctionalExtension;
3156
+ /**
3157
+ * Proposes content at `target` — a position, or a range the proposal
3158
+ * replaces — streamed in by `callback` (see `streamContent`, whose options
3159
+ * these are). A proposal already there is discarded first. Needs
3160
+ * {@link createContentProposal} and `createContentStream` in the kit.
3161
+ */
3162
+ declare function proposeContent(view: EditorView, target: number | ContentStreamRange, callback: (writer: ContentStreamWriter) => Promise<void> | void, options?: Omit<StreamContentOptions, 'history'>): ContentStreamRun;
3163
+ /**
3164
+ * Revises a *part* of the proposal: `range`, inside it, is replaced by what
3165
+ * `callback` streams — "rewrite this bit" — while the rest of the proposal
3166
+ * stands, still proposed, and the whole is accepted or discarded together
3167
+ * as before. Null (and nothing streamed) when no proposal holds the range.
3168
+ */
3169
+ declare function reviseProposal(view: EditorView, range: ContentStreamRange, callback: (writer: ContentStreamWriter) => Promise<void> | void, options?: Omit<StreamContentOptions, 'history'>): ContentStreamRun | null;
3170
+ /** Stops the writing where it is; what is proposed stays proposed. */
3171
+ declare function stopProposal(view: EditorView): void;
3172
+ /**
3173
+ * Takes the proposal into the document for good: the same content, now
3174
+ * the writer's own — one change in the history, so one undo takes all of
3175
+ * it back. The caret lands after it. False when nothing is proposed.
3176
+ */
3177
+ declare function acceptProposal(view: EditorView): boolean;
3178
+ /**
3179
+ * Takes the proposal out, as if it had never been written: the document
3180
+ * and its history are as they were, the caret where the proposal began.
3181
+ * False when nothing is proposed.
3182
+ */
3183
+ declare function discardProposal(view: EditorView): boolean;
3184
+
3185
+ /** A box in viewport coordinates — what `coordsAtPos` answers. */
3186
+ interface MenuAnchor {
3187
+ left: number;
3188
+ top: number;
3189
+ bottom: number;
3190
+ }
3191
+ /**
3192
+ * Where a floating menu goes for an anchor line: under it, clamped to its
3193
+ * scroll container, flipped above when below would overflow the visible
3194
+ * area. Read-only — it measures and returns, and the caller writes `left` /
3195
+ * `top` in its own write phase, so measuring never follows a write in the
3196
+ * same task. The element must be `position: absolute` inside a
3197
+ * `position: relative` scroll container; its size is read as rendered, so
3198
+ * call it after its contents are in.
3199
+ */
3200
+ declare function measureMenuPlacement(anchor: MenuAnchor, element: HTMLElement, offset: number): {
3201
+ left: number;
3202
+ top: number;
3203
+ } | null;
3204
+
3205
+ /**
3206
+ * An *expression dialect*: what the `{{…}}` tokens mean. The merge-tag mark
3207
+ * is dialect-neutral — it finds tokens, keeps the pill honest and formats
3208
+ * them whole — and a dialect layers meaning on top, opt-in, one per editor:
3209
+ * a parser for the token language, exact required fields from the parse,
3210
+ * syntax diagnostics before the template ever reaches a renderer. The first
3211
+ * dialect is AngularJS expressions (`createAngularExpressions`); Handlebars
3212
+ * follows. Without a dialect the generic lexer in `mergeTagFields` stands in.
3213
+ */
3214
+ interface ExpressionDialect {
3215
+ /** A short name, e.g. `angular`. */
3216
+ readonly name: string;
3217
+ /** The identifiers the document's tokens read — the values the host must
3218
+ supply — deduplicated, in first-use order. */
3219
+ requiredFields(doc: Node): string[];
3220
+ /** Diagnostics for one token's inner expression, positions relative to it. */
3221
+ analyze(expr: string): ExpressionIssue[];
3222
+ }
3223
+ /** A problem inside one expression, `[from, to)` relative to its text. */
3224
+ interface ExpressionIssue {
3225
+ from: number;
3226
+ to: number;
3227
+ message: string;
3228
+ }
3229
+ /** A problem in the document: a token's issue at document positions. */
3230
+ interface ExpressionDiagnostic extends ExpressionIssue {
3231
+ /** The token's inner expression, verbatim. */
3232
+ expr: string;
3233
+ }
3234
+ /** The dialect installed on an editor, as plugin state, so the send intent
3235
+ and the host read the one the editor was configured with. */
3236
+ declare const expressionDialectKey: PluginKey<{
3237
+ dialect: ExpressionDialect;
3238
+ }>;
3239
+ declare const expressionDialect: (state: EditorState) => ExpressionDialect | undefined;
3240
+
3241
+ type AngularNode = {
3242
+ type: 'Literal';
3243
+ value: string | number | boolean | null | undefined;
3244
+ } | {
3245
+ type: 'Identifier';
3246
+ name: string;
3247
+ } | {
3248
+ type: 'Member';
3249
+ object: AngularNode;
3250
+ property: AngularNode;
3251
+ computed: boolean;
3252
+ } | {
3253
+ type: 'Call';
3254
+ callee: AngularNode;
3255
+ args: AngularNode[];
3256
+ } | {
3257
+ type: 'Unary';
3258
+ operator: string;
3259
+ argument: AngularNode;
3260
+ } | {
3261
+ type: 'Binary';
3262
+ operator: string;
3263
+ left: AngularNode;
3264
+ right: AngularNode;
3265
+ } | {
3266
+ type: 'Conditional';
3267
+ test: AngularNode;
3268
+ consequent: AngularNode;
3269
+ alternate: AngularNode;
3270
+ } | {
3271
+ type: 'Assignment';
3272
+ target: AngularNode;
3273
+ value: AngularNode;
3274
+ } | {
3275
+ type: 'Filter';
3276
+ name: string;
3277
+ input: AngularNode;
3278
+ args: AngularNode[];
3279
+ } | {
3280
+ type: 'Array';
3281
+ elements: AngularNode[];
3282
+ } | {
3283
+ type: 'Program';
3284
+ statements: AngularNode[];
3285
+ };
3286
+ /** Parses one token's inner expression. Throws an {@link ExpressionError}
3287
+ (message + position) on the first problem. */
3288
+ declare function parseAngularExpression(expr: string): AngularNode;
3289
+ /** The parse's verdict on one expression: its issues (at most one — the
3290
+ first — today), empty when it is a valid program. */
3291
+ declare function analyzeAngularExpression(expr: string): ExpressionIssue[];
3292
+ /** The required fields of a set of expressions: values read anywhere, minus
3293
+ locals assigned anywhere (an assignment is seen across all tokens), in
3294
+ first-use order. An expression that does not parse contributes nothing —
3295
+ the diagnostic says why. */
3296
+ declare function angularRequiredFields(expressions: string[]): string[];
3297
+ declare const angularDialect: ExpressionDialect;
3298
+ interface AngularExpressionsOptions {
3299
+ /** Called with the document's expression diagnostics whenever they change
3300
+ (an empty list when every token parses) — the host's status strip. */
3301
+ onDiagnostics?: (diagnostics: ExpressionDiagnostic[]) => void;
3302
+ }
3303
+ /**
3304
+ * Installs the AngularJS dialect on an editor: the send intent's required
3305
+ * fields come from the parse, every token is checked after each change, a
3306
+ * problem is underlined in place (`.aee-expr-error`, the app owns the
3307
+ * pixels) and reported through `onDiagnostics` with document positions —
3308
+ * so a host can count it in its status strip and jump to it. Opt-in: an
3309
+ * editor without it keeps the generic lexer and no checking.
3310
+ */
3311
+ declare const createAngularExpressions: (options?: AngularExpressionsOptions) => FunctionalExtension;
3312
+
3313
+ /** **bold** */
3314
+ declare const starInputRegex: RegExp;
3315
+ /** **bold** while pasting. */
3316
+ declare const starPasteRegex: RegExp;
3317
+ /** __bold__ */
3318
+ declare const underscoreInputRegex: RegExp;
3319
+ /** __bold__ while pasting. */
3320
+ declare const underscorePasteRegex: RegExp;
3321
+ /**
3322
+ * Email specific html bold parsing rules.
3323
+ * Watchout for normlization against
3324
+ * - numeric font weights
3325
+ * - case-insensitive font weights
3326
+ * - inline styles resetting the weight back to normal
3327
+ * - inline styles resetting the weight back to 400
3328
+ * - inline styles resetting the weight back to normal
3329
+ * - inline styles resetting the weight back to 400
3330
+ */
3331
+ declare const Bold: _h_k_dev_angular_email_editor.MarkExtension;
3332
+
3333
+ /** *italic* */
3334
+ declare const italicStarInputRegex: RegExp;
3335
+ /** *italic* while pasting. */
3336
+ declare const italicStarPasteRegex: RegExp;
3337
+ /** _italic_ */
3338
+ declare const italicUnderscoreInputRegex: RegExp;
3339
+ /** _italic_ while pasting. */
3340
+ declare const italicUnderscorePasteRegex: RegExp;
3341
+ declare const Italic: _h_k_dev_angular_email_editor.MarkExtension;
3342
+
3343
+ declare const Underline: _h_k_dev_angular_email_editor.MarkExtension;
3344
+
3345
+ interface LinkAttrs {
3346
+ href: string;
3347
+ title?: string | null;
3348
+ target?: string;
3349
+ }
3350
+ interface LinkRange {
3351
+ from: number;
3352
+ to: number;
3353
+ attrs: LinkAttrs;
3354
+ }
3355
+ /** The contiguous extent of the link at `pos`, if any — the range that a
3356
+ link edit from a bare cursor (no selection) applies to. */
3357
+ declare function linkRangeAt(state: EditorState, pos: number): LinkRange | null;
3358
+ /** A simple security check to prevent XSS. Shared by every URL-bearing
3359
+ extension (links, images): script URLs are refused on parse and on the
3360
+ inserting command alike — the schema is the sanitizer. */
3361
+ declare function isSafeUrl(url: string | null): boolean;
3362
+ /**
3363
+ * What a person typed into a link field → the href it means: a scheme or a
3364
+ * `#fragment` as written, an address `mailto:`, a `{{ token }}` untouched
3365
+ * (it becomes a URL at send), anything else `https://`. Empty stays empty.
3366
+ */
3367
+ declare function normalizeHref(raw: string): string;
3368
+ /** Why a typed link cannot be applied — see {@link hrefProblem}. */
3369
+ type HrefProblem = 'required' | 'unsafe' | 'invalid';
3370
+ /**
3371
+ * Whether what was typed into a link field can be a link in an email, after
3372
+ * {@link normalizeHref}: `'required'` for nothing (or a bare `#`),
3373
+ * `'unsafe'` for a script or `data:` URL — refused like `isSafeUrl`, plus
3374
+ * `data:`, the phishing favourite no mail client should be handed —
3375
+ * `'invalid'` for what is no URL at all (no host, a scheme mail cannot
3376
+ * use, a host without a dot), null when it is fine. A link holding a
3377
+ * `{{ token }}` passes: its shape is known only once it is filled in.
3378
+ */
3379
+ declare function hrefProblem(raw: string): HrefProblem | null;
3380
+ /** Paste a URL onto selected text and the text becomes the link, rather than
3381
+ replacing the selection with the raw URL (the Gmail/Docs behaviour). */
3382
+ declare const linkPastePlugin: Plugin<any>;
3383
+ declare const linkClickPlugin: Plugin<any>;
3384
+ declare const Link: _h_k_dev_angular_email_editor.MarkExtension;
3385
+
3386
+ /**
3387
+ * Allow only color values that can't smuggle extra declarations into the
3388
+ * `style` attribute: hex, rgb[a]/hsl[a] functional notation, or a bare CSS
3389
+ * named color. Mirrors the security-consciousness of the link mark's
3390
+ * `isSafeUrl`. `node.style.color` is already normalised by the browser's
3391
+ * CSSOM, but the legacy `<font color>` path and the programmatic `setColor`
3392
+ * command take raw strings, so we gate both.
3393
+ */
3394
+ declare function isSafeColor(color: string | null | undefined): color is string;
3395
+ /**
3396
+ * Normalise any CSS color to a hex string the editor stores internally.
3397
+ * Call this before passing a color value to setColor — never store oklch/lab/hsl
3398
+ * in the mark attrs, because toDOM emits them verbatim into email HTML.
3399
+ */
3400
+ declare function toEmailSafeColor(raw: string): string | null;
3401
+ type TextTransform = 'uppercase' | 'lowercase' | 'capitalize';
3402
+ /** A `text-transform` the mark takes: the three cases; `none` and the
3403
+ rest are the words as written. */
3404
+ declare function parseTextTransform(raw: string | null | undefined): TextTransform | null;
3405
+ declare const ALLOWED_SIZES: readonly [10, 12, 14, 16, 18, 24, 32];
3406
+ type FontSize = (typeof ALLOWED_SIZES)[number];
3407
+ declare function isSafeFontSize(value: unknown): value is FontSize;
3408
+ /**
3409
+ * Parse a font-size value from the DOM into one of our allowed sizes.
3410
+ * Handles px strings ("16px"), plain numbers, and rejects everything else.
3411
+ */
3412
+ declare function parseFontSize(raw: string | null | undefined): FontSize | null;
3413
+ /**
3414
+ * The sizes the toolbar picker offers: the phone-safe subset (≥14px). Below
3415
+ * ~13px iOS auto-inflates text and reflows the layout (responsiveness ledger,
3416
+ * principle 8), so we don't *offer* those — but {@link parseFontSize} still
3417
+ * accepts the full {@link ALLOWED_SIZES} range, because a hand-typed size in
3418
+ * the HTML source pane is the author's own responsibility, exactly like a
3419
+ * hand-typed hex colour.
3420
+ */
3421
+ declare const emailFontSizes: FontSize[];
3422
+ /** A curated, email-safe font stack the toolbar offers. */
3423
+ interface EmailFont {
3424
+ /** Toolbar label. */
3425
+ name: string;
3426
+ /** The exact `font-family` value emitted — the canonical stored form. */
3427
+ stack: string;
3428
+ }
3429
+ /**
3430
+ * The curated font stacks — the picker offers only these, no free-form fonts
3431
+ * (principle 7: if a mainstream client can't render it, we don't emit it).
3432
+ *
3433
+ * Deliberately built from *single-word* family identifiers plus a generic
3434
+ * fallback: a stack like `Courier New` would round-trip through the CSSOM
3435
+ * (serialization builds real elements and re-reads them) and come back quoted
3436
+ * as `"Courier New"` in Chrome but unquoted in jsdom — a byte-instability that
3437
+ * breaks canonical determinism and makes tests disagree with the runtime (the
3438
+ * same trap the longhand/`rgb()` rule guards against). Bare identifiers and
3439
+ * generic keywords serialize identically everywhere.
3440
+ */
3441
+ declare const emailFontFamilies: EmailFont[];
3442
+ declare function isSafeFontFamily(value: unknown): value is string;
3443
+ /**
3444
+ * Parse a font-family value from the DOM into one of our curated stacks,
3445
+ * returning the *canonical* stack string (so a cosmetically different but
3446
+ * equivalent input normalises to the exact bytes we emit). A stack of
3447
+ * someone else's reads as the curated one it falls back to — the first of
3448
+ * its families a curated stack names, its generic last: a builder's web
3449
+ * font (`Ubuntu, Helvetica, Arial, sans-serif`) is Sans-serif, the face a
3450
+ * client without that font draws anyway. Nothing to fall back to, no font.
3451
+ */
3452
+ declare function parseFontFamily(raw: string | null | undefined): string | null;
3453
+ /**
3454
+ * TipTap-style `textStyle` mark: a `<span style="…">` container holding inline
3455
+ * text-styling attributes — `color`, `fontSize`, `fontFamily` and
3456
+ * `backgroundColor` (the highlighter). All hang off one shared span and merge
3457
+ * into a single `style` string instead of nesting wrapper tags.
3458
+ *
3459
+ * Modeled on `@tiptap/extension-text-style` + `@tiptap/extension-color`: each
3460
+ * primitive is an *attribute* on the span, not its own mark, so `setColor`,
3461
+ * `setFontSize`, … all coalesce onto the same element. `backgroundColor` fills
3462
+ * only from the curated dual-safe background palette (principle 9); a hand-typed
3463
+ * fill in the HTML source is the author's own responsibility, never policed.
3464
+ */
3465
+ declare const TextStyle: _h_k_dev_angular_email_editor.MarkExtension;
3466
+
3467
+ declare const History: _h_k_dev_angular_email_editor.FunctionalExtension;
3468
+
3469
+ declare const ColumnResize: _h_k_dev_angular_email_editor.FunctionalExtension;
3470
+
3471
+ /** The declared width of each grid column (`null` = unspecified, shares the
3472
+ leftover) — read from the first cell in the column that declares one. */
3473
+ declare function columnWidths(table: Node): (number | null)[];
3474
+ /** Every column's effective share, normalized to sum to 100: declared widths
3475
+ as declared, undeclared columns splitting the leftover equally — the same
3476
+ distribution fixed layout uses, so these numbers *are* the pixels and the
3477
+ boundary lines can be placed by pure arithmetic. */
3478
+ declare function effectiveWidths(table: Node): number[];
3479
+ /**
3480
+ * Sets the widths of the two columns meeting at `boundary` (the line between
3481
+ * grid columns `boundary` and `boundary + 1`) — what a resize drag commits.
3482
+ * `leftPct` is the left column's new share; the pair's total is conserved, so
3483
+ * only these two columns move. Clamped so neither side drops below
3484
+ * {@link MIN_COLUMN_PCT}. Every cell touching the two columns gets its
3485
+ * `colwidth` entries set (spans included, sum-preserving), so the widths
3486
+ * survive first-row edits and row deletion alike.
3487
+ */
3488
+ declare function setColumnBoundary(tablePos: number, boundary: number, leftPct: number): Command;
3489
+ /**
3490
+ * Sets the table's box — left offset and width, both percent of the
3491
+ * container — what dragging either outer edge commits. The left edge moves
3492
+ * the offset (the right edge stays put), the right edge moves the width;
3493
+ * the pair clamps so the offset stays ≥ 0, the width keeps its floor, and
3494
+ * together they never pass 100.
3495
+ *
3496
+ * `absorb` is the Word rule that keeps columns independent of the table's
3497
+ * edges: column widths are percentages *of the table*, so changing the table
3498
+ * width would slide every interior boundary proportionally. With `absorb`,
3499
+ * the edge-adjacent column ('first' for a left-edge drag, 'last' for a
3500
+ * right-edge one) takes the entire change and every other column is rescaled
3501
+ * by oldWidth/newWidth — same absolute size, boundaries exactly where they
3502
+ * were (to the 0.1% rounding). The width additionally floors where the
3503
+ * absorbing column would hit {@link MIN_COLUMN_PCT}. Without `absorb`,
3504
+ * columns scale proportionally.
3505
+ */
3506
+ declare function setTableBox(tablePos: number, offsetPct: number, widthPct: number, absorb?: 'first' | 'last'): Command;
3507
+ /** Width-only convenience over {@link setTableBox}: keeps the offset. */
3508
+ declare function setTableWidth(tablePos: number, widthPct: number): Command;
3509
+
3510
+ /** Which band — or which cell — of which table a grip stands for, and
3511
+ where it is on screen. */
3512
+ interface TableHandleTarget {
3513
+ kind: 'row' | 'column' | 'cell';
3514
+ /** The band's index in the grid, counted from the top / the start; a
3515
+ cell's column. */
3516
+ index: number;
3517
+ /** A cell's row. */
3518
+ row?: number;
3519
+ /** The table's position in the document — what the select commands take. */
3520
+ tablePos: number;
3521
+ /** The grip's own rect: what the app's menu anchors to. */
3522
+ boundingBox: DOMRect;
3523
+ }
3524
+ interface TableHandlesOptions {
3525
+ /** A grip was pressed (its band is selected by then), or the handles have
3526
+ nothing to offer any more (`null`) — the table went away under them. */
3527
+ onOpen: (target: TableHandleTarget | null) => void;
3528
+ /** What a grip says to assistive tech, translated by the host. Called with
3529
+ the band's 1-based number, as a person counts rows; for the caret
3530
+ cell's grip, the cell's column. */
3531
+ label?: (kind: 'row' | 'column' | 'cell', number: number) => string;
3532
+ }
3533
+ /**
3534
+ * The row and column grips: a handle on each row's flank and above each
3535
+ * column, the gesture every table editor has settled on (Notion's, Google
3536
+ * Docs', Domternal's). Pressing one selects that whole band and hands the
3537
+ * host a target to open a menu at — insert, move, duplicate, fill, align,
3538
+ * delete — all of which are the ordinary selection-relative commands once
3539
+ * the band is selected.
3540
+ *
3541
+ * **The grips are widget decorations inside the band's first cell**, not an
3542
+ * overlay measured against the table. That is the whole trick: a decoration
3543
+ * in a cell is positioned by that cell's own box, so a row grip is exactly
3544
+ * as tall as its row and a column grip exactly as wide as its column,
3545
+ * through merged cells, wrapped text and a resize drag alike — with no
3546
+ * measurement, no ResizeObserver and no pointer tracking, which is the same
3547
+ * bargain the boundary lines strike (see column-resize.ts).
3548
+ *
3549
+ * A band whose first cell is covered by a merge from above (or from the
3550
+ * left) has no cell of its own to hang a grip in, and gets none: there is
3551
+ * nothing there to grab that would not be a lie about what it selects.
3552
+ *
3553
+ * What a press *opens* is the host's: the extension reports the band and the
3554
+ * grip's rect, and the app renders the menu (see the app's table-menu),
3555
+ * exactly as the block menu is arranged.
3556
+ *
3557
+ * Decorations are built when a table's *grid* changes and then mapped, the
3558
+ * way Notion and Tiptap keep their chrome off the keystroke path. Typing
3559
+ * inside a cell is not a grid change — the widgets stay the same DOM nodes,
3560
+ * so a pointer resting on a grip is never rebuilt out from under itself.
3561
+ * The expanded state is a cheap attribute write on those nodes, not a
3562
+ * decoration rebuild: `aria-expanded` is what the stylesheet (and a screen
3563
+ * reader) already keys off.
3564
+ */
3565
+ declare const createTableHandles: (options: TableHandlesOptions) => FunctionalExtension;
3566
+
3567
+ /**
3568
+ * Boundary drag for the `/columns` layout block — the table drag's twin, with
3569
+ * the layout model underneath (see ROADMAP, "Columns and tables — decided").
3570
+ *
3571
+ * Same vocabulary: full-height primary lines on every boundary, hover to
3572
+ * reveal, deferred commit (only the line moves, one transaction on release,
3573
+ * one undo step), window-level listeners so the pointer can't outrun the
3574
+ * handle, guides pinned for the drag's lifetime.
3575
+ *
3576
+ * Different model: a table splits 100% of itself; columns redistribute the
3577
+ * email's px *budget* — the `max-width` caps that make the block fluid
3578
+ * (side by side where the caps fit, stacked on a phone). The drag moves px
3579
+ * between two neighbouring caps and conserves their sum, so the block's
3580
+ * stacking behaviour never changes, only the split.
3581
+ *
3582
+ * Geometry, as always, from the model: the NodeView's *box* carries the
3583
+ * container style (the email's centred 600px box), so a boundary's `left` is
3584
+ * simply the sum of the caps before it, in px. And "stacked" — the one state
3585
+ * where a horizontal drag means nothing — is a CSS container query on that
3586
+ * same box: the caps sum to a constant budget, so the block stacks at a
3587
+ * constant width, and the lines hide themselves. No measurement, no
3588
+ * observer.
3589
+ */
3590
+ declare const ColumnsResize: _h_k_dev_angular_email_editor.FunctionalExtension;
3591
+
3592
+ /**
3593
+ * A real cursor in the gaps *between* block nodes — ProseMirror's own answer to
3594
+ * the trap every layout block falls into: our table and columns are
3595
+ * `isolating`, so when one is the last thing in the document there is no text
3596
+ * position after it, and clicking the empty space below it does nothing.
3597
+ *
3598
+ * With this, that click (and ArrowDown/ArrowRight out of a block, and a
3599
+ * click *between* two adjacent blocks) lands a blinking gap cursor; typing
3600
+ * there creates a paragraph. It is the mouse sibling of the block escapes we
3601
+ * hand-rolled per node — one mechanism, every block, no per-node code.
3602
+ *
3603
+ * Ordering note: this must sit *after* the layout nodes in the kit. Extension
3604
+ * plugins run in kit order and gap-cursor claims the arrow keys, so the
3605
+ * table's own ArrowDown escape (which writes a paragraph below a trailing
3606
+ * table rather than parking a cursor in the gap) has to be reached first.
3607
+ *
3608
+ * Pixels: `.ProseMirror-gapcursor` in the app's global styles, transcribed
3609
+ * from `prosemirror-gapcursor/style/gapcursor.css` (MIT) and scoped to
3610
+ * `.aee-editor` like everything else we ship.
3611
+ */
3612
+ declare const Gapcursor: _h_k_dev_angular_email_editor.FunctionalExtension;
3613
+
3614
+ /**
3615
+ * The quoted history a reply or forward answers — Gmail's `⋯` — kept
3616
+ * **beside** the body, never in it.
3617
+ *
3618
+ * - **It stays.** The history is the document's `quoted` attribute, not its
3619
+ * content: a template applied, an `.eml` dropped, the source pane
3620
+ * rewritten — anything that replaces the body (`setContent`, `setText`,
3621
+ * `replaceHTML`) leaves it where it is. `getHTML()` is the body alone.
3622
+ * - **Only a deliberate step changes it.** The trash drops it; Edit moves it
3623
+ * into the body, where it is ordinary text from then on. Both are steps of
3624
+ * the document, so one undo brings it back — Edit's undo takes the text
3625
+ * out of the body again in the same step.
3626
+ * - **It is sent.** The send intent puts it under the body
3627
+ * ({@link withQuoted}): HTML, plain text and inline images alike.
3628
+ * - **It is what the schema keeps.** Whatever comes in is parsed through the
3629
+ * schema the way a paste is — foreign markup repaired, unsafe markup gone —
3630
+ * and held as canonical HTML.
3631
+ *
3632
+ * It renders as a widget after the last block — read-only, folded behind a
3633
+ * `⋯` toggle, with Edit and a trash beside it. Behaviour and structure ship
3634
+ * here; pixels are the host's, through `.aee-quoted` and its `data-slot`
3635
+ * parts (`bar`, `fold`, `edit`, `remove`, `content`), the fold button keeping
3636
+ * its `.aee-quote-fold` class.
3637
+ */
3638
+ interface QuotedHistoryLabels {
3639
+ /** The fold toggle while folded. Default "Show quoted text". */
3640
+ show?: QuotedHistoryLabel;
3641
+ /** The fold toggle while open. Default "Hide quoted text". */
3642
+ hide?: QuotedHistoryLabel;
3643
+ /** Moves the history into the body. Default "Edit quoted text". */
3644
+ edit?: QuotedHistoryLabel;
3645
+ /** Drops the history. Default "Remove quoted text". */
3646
+ remove?: QuotedHistoryLabel;
3647
+ }
3648
+ /** A word, or a function asked for it each time the widget is drawn or
3649
+ brought up to date — so a language switched at runtime reaches it. */
3650
+ type QuotedHistoryLabel = string | (() => string);
3651
+ interface QuotedHistoryOptions {
3652
+ /** The widget's words, in the host's language; each left out stays English. */
3653
+ labels?: QuotedHistoryLabels;
3654
+ }
3655
+ /** The quoted history, as canonical HTML, or null. */
3656
+ declare function quotedHistory(state: EditorState): string | null;
3657
+ /** Whether the quoted history is folded behind its `⋯` — true while there
3658
+ is one and the user has not opened it. */
3659
+ declare function isQuotedHistoryFolded(state: EditorState): boolean;
3660
+ /** The document with its quoted history under the body — what is sent. */
3661
+ declare function withQuoted(doc: Node): Node;
3662
+ /** Sets the quoted history — sanitized through the schema — or drops it.
3663
+ False when that is what it already is. A host's own sync marks the
3664
+ transaction as such (the editor's `setQuoted` does). */
3665
+ declare const setQuotedHistory: (html: string | null) => Command;
3666
+ /** The trash: the history goes — as a step, so undo brings it back. */
3667
+ declare const removeQuotedHistory: Command;
3668
+ /** Edit: the history moves to the end of the body, as ordinary text, and
3669
+ stops being kept — one step, so one undo reverses both. */
3670
+ declare const editQuotedHistory: Command;
3671
+ /** The quoted history as an editor extension — see {@link QuotedHistoryLabels}
3672
+ for its words. The email kit carries it as {@link QuotedHistory}. */
3673
+ declare function createQuotedHistory(options?: QuotedHistoryOptions): FunctionalExtension;
3674
+ /** The email kit's quoted history, in English. */
3675
+ declare const QuotedHistory: FunctionalExtension;
3676
+
3677
+ /**
3678
+ * Each source line's indentation, as the custom property `--aee-line-indent`
3679
+ * (in characters) on its `.aee-code-line` — what a host needs for VS Code's
3680
+ * soft wrap: a wrapped line continues under its own indentation, not at the
3681
+ * margin (`wrappingIndent: "same"`). The pixels are the host's:
3682
+ *
3683
+ * .aee-code-line {
3684
+ * padding-inline-start: calc(var(--aee-line-indent, 0) * 1ch);
3685
+ * text-indent: calc(var(--aee-line-indent, 0) * -1ch);
3686
+ * }
3687
+ *
3688
+ * The first row starts at the margin (its spaces fill the indent); every row
3689
+ * the line wraps into starts where its text does.
3690
+ *
3691
+ * Decorations, not the line's `toDOM`: a line's element is not re-rendered
3692
+ * when its text changes, so an indent typed or deleted would never reach it.
3693
+ * An edit rebuilds only the lines it touched — a keystroke in a 3000-line
3694
+ * template costs one line.
3695
+ */
3696
+ declare const CodeLineIndent: _h_k_dev_angular_email_editor.FunctionalExtension;
3697
+
3698
+ /**
3699
+ * Line folding for sending — the HTML as compact as ever, its lines kept
3700
+ * short enough to survive any transport.
3701
+ *
3702
+ * RFC 5322 caps a line at 998 octets. A mailer that sends the HTML part
3703
+ * quoted-printable or base64 never meets the cap; one that sends it 7bit or
3704
+ * 8bit lets a relay cut a longer line wherever it reaches the cap — inside a
3705
+ * tag, inside a word — which is where the stray `!` and the broken layout of
3706
+ * a forwarded Exchange mail come from. Our serialized body is one line.
3707
+ *
3708
+ * **Two edits only**, both whitespace the HTML tokenizer ignores:
3709
+ * - a space or tab *between the attributes of a start tag* becomes a line
3710
+ * break;
3711
+ * - where a line has no such space (MJML's runs of Outlook conditionals and
3712
+ * end tags), a line break goes right before the `>` that closes a tag —
3713
+ * `</td⏎>` is `</td>`, Prettier prints it — never after a `/`, where it
3714
+ * would undo a self-closing slash.
3715
+ *
3716
+ * Never touched: text (its spaces may be `pre`), attribute values, comments
3717
+ * (Outlook's conditional ones are markup to Outlook, kept byte for byte),
3718
+ * doctypes, CDATA, processing instructions, the content of raw text
3719
+ * elements (`style`, `title`, `textarea`, `noscript`, …) — and *anything*
3720
+ * after `<svg>`, `<math>`, `<script>` or `<plaintext>`: in foreign content
3721
+ * other elements hold raw text, a script's end hides behind escapes, and
3722
+ * where they end takes the parser's whole tree, not a tokenizer. A line with
3723
+ * nowhere to fold stays long — a transport encoding carries it.
3724
+ *
3725
+ * The tokenizer is the HTML standard's (§13.2.5) in every state a fold
3726
+ * depends on — tag and attribute states, where `<` opens a tag, the end of
3727
+ * a comment, of raw text. Where it is unsure it folds nothing: a missed fold
3728
+ * costs a long line, a wrong one the email.
3729
+ */
3730
+ /** Default fold width, in octets: well inside the 998 RFC 5322 allows,
3731
+ leaving room for a transport's CR and dot-stuffing. */
3732
+ declare const FOLD_WIDTH = 900;
3733
+ /**
3734
+ * `html` with its long lines folded — see above. Lines are measured in
3735
+ * octets (UTF-8); a line that fits is never touched, and folding twice
3736
+ * changes nothing.
3737
+ */
3738
+ declare function foldHTML(html: string, width?: number): string;
3739
+
3740
+ /** Editor root class while the document carries an envelope — the host's
3741
+ hook for rendering a template like a client would (browser typography
3742
+ rather than the composer's), see the example app's styles. */
3743
+ declare const DOCUMENT_CLASS = "aee-document";
3744
+ /**
3745
+ * Applies a preserved document's stylesheets to the editor view, so the
3746
+ * visual pane renders an MJML-class template exactly as the preview does:
3747
+ * the head's `<style>` blocks (media queries included — that is what makes
3748
+ * MJML columns responsive), its `<link rel="stylesheet">`s, and any `<style>`
3749
+ * in the body. Everything is wrapped in `@scope (.<this editor>) { … }`, so
3750
+ * a template's `table, td { … }` or `p { margin: 13px 0 }` reaches this
3751
+ * editor's content and nothing else on the page (`body { … }` rules match
3752
+ * nothing inside a scope and simply do not apply). `@import`s are hoisted
3753
+ * ahead of the scope block, where CSS requires them. The `<body>`'s own
3754
+ * inline style and `bgcolor` land on the editor root, the closest thing the
3755
+ * editable view has to a body.
3756
+ *
3757
+ * Behaviour only: the style element sits beside the editor's DOM and is
3758
+ * removed with it. No pixels of its own.
3759
+ */
3760
+ declare const DocumentStyles: _h_k_dev_angular_email_editor.FunctionalExtension;
3761
+ /** One stylesheet text: hoisted imports, then everything else under `@scope`. */
3762
+ declare function scopedDocumentCss(doc: Node, scope: string): string;
3763
+
3764
+ /**
3765
+ * What the composer hands the host when the user asks to send: the canonical
3766
+ * HTML, its `text/plain` projection — the two parts of a well-formed
3767
+ * `multipart/alternative` body — the inline image parts that HTML references
3768
+ * (`multipart/related`, should the host want it), and the personalization
3769
+ * fields the body requires. Everything else (addressing, headers, transport)
3770
+ * is the host's; nothing network-shaped lives in this library.
3771
+ */
3772
+ interface SendIntent {
3773
+ /** The canonical HTML — with every data-URL image already promoted to a
3774
+ `cid:` reference (see {@link promoteInlineImages}); the document in the
3775
+ editor keeps its data URLs. Folded for the wire (see {@link foldHTML}):
3776
+ no line longer than the width wherever a space between a tag's
3777
+ attributes allows it, so a relay's 998-octet cut never lands in the
3778
+ middle of a tag — the same email to every parser, only line breaks
3779
+ where spaces were. */
3780
+ html: string;
3781
+ text: string;
3782
+ /** Every inline image `html` references, in document order: promoted
3783
+ data-URL images carry their bytes, pre-existing `cid:` references
3784
+ (an imported reply's parts, which the host already owns) carry none.
3785
+ Empty when the body has no inline images — plain
3786
+ `multipart/alternative` then. */
3787
+ inlineImages: InlineImage[];
3788
+ /** Every field identifier the body's merge tags read, in first-use order —
3789
+ the values the host must resolve before the mail can render. Straight
3790
+ off the document's nodes (see `mergeTagFields`), so a host without its
3791
+ own template engine never text-parses the HTML to discover them; a host
3792
+ with one (an AngularJS-expression evaluator, say) is free to ignore
3793
+ this and scan `html` itself. Empty when the body is template-free. */
3794
+ requiredFields: string[];
3795
+ }
3796
+ interface SendIntentOptions {
3797
+ /** Called with the ready payload whenever the user requests a send —
3798
+ `/send`, Mod-Enter (Gmail's shortcut), or the `requestSend` command. */
3799
+ onSend: (intent: SendIntent) => void;
3800
+ }
3801
+ /**
3802
+ * The send affordance — an *intent*, not a transport. The user's "send" is
3803
+ * just another editor action; what it produces is a payload event the host
3804
+ * wires to its own transport, exactly like the seed side (`replyDocument`)
3805
+ * feeds the same `html` signal from the other direction.
3806
+ *
3807
+ * Three ways in, one payload: the `/send` slash item, Mod-Enter, and the
3808
+ * `requestSend` command (for a toolbar button). The slash menu deletes the
3809
+ * `/send` query text *before* running the item's command, so the emitted
3810
+ * HTML is always clean of it.
3811
+ */
3812
+ declare const createSendIntent: (options: SendIntentOptions) => FunctionalExtension;
3813
+
3814
+ /**
3815
+ * Fills the document's placeholders in with their values — the step a CRM
3816
+ * runs before a templated mail goes out ("Platzhalter einsetzen").
3817
+ *
3818
+ * A host whose evaluator takes text (core's TextParser: `parseStrings`)
3819
+ * would otherwise push the whole HTML body through it and take new HTML
3820
+ * back. That loses three ways: an expression with `&&`, `>` or `<` arrives
3821
+ * entity-escaped (`&amp;&amp;`) and fails to compile, a value is spliced
3822
+ * into the markup unescaped, and the swapped body takes the undo history
3823
+ * with it. Here the tokens are read off the document, where they are text:
3824
+ *
3825
+ * - the host is asked for each distinct expression **once, raw** —
3826
+ * `cf_3 > 0 && cf_4 ? 'x' : ''`, never an entity in sight;
3827
+ * - each value goes in **as text**, in the token's place and with its
3828
+ * formatting (a bold `{{ customer_lastName }}` becomes the bold name), a
3829
+ * line break in a value a hard break;
3830
+ * - all of it is **one transaction**: one Mod-Z brings the tokens back.
3831
+ *
3832
+ * Tokens in running text and in any string attribute of a node or mark — a
3833
+ * link's or a button's `href`, a button's label, an image's `src` or `alt`
3834
+ * — are filled alike, everywhere a renderer of the HTML would have looked.
3835
+ * With a selection holding tokens, only those are filled; else the whole
3836
+ * document.
3837
+ *
3838
+ * The request is asynchronous: the user may go on writing while the host
3839
+ * answers. The scope is mapped through every change made meanwhile, tokens
3840
+ * are found again when the values arrive, and a token edited in between is
3841
+ * left alone (its expression is not the one asked for). One request at a
3842
+ * time — {@link mergeTagResolutionPending} says when one is out.
3843
+ *
3844
+ * Bound to Mod-Shift-Enter — beside Mod-Enter's send, "fill in, then send";
3845
+ * free in every browser and on every keyboard layout — and declared as the
3846
+ * `resolve-placeholders` action, so it is a `/` menu row and a toolbar
3847
+ * button with no further wiring.
3848
+ */
3849
+ /** What became of a request — for the host's message ("3 Platzhalter ohne
3850
+ Wert"). */
3851
+ interface MergeTagResolutionReport {
3852
+ /** Where the request applied. */
3853
+ scope: 'selection' | 'document';
3854
+ /** How many token occurrences were replaced by a value. */
3855
+ filled: number;
3856
+ /** Expressions whose value came back empty — a field without a value, or
3857
+ one the evaluator could not compute (most say `''` for both). Their
3858
+ tokens are gone. */
3859
+ empty: string[];
3860
+ /** Expressions the host left unresolved (`null`): their tokens stay. */
3861
+ kept: string[];
3862
+ }
3863
+ interface MergeTagResolutionOptions {
3864
+ /**
3865
+ * Values for the distinct expressions asked for, in the same order — each
3866
+ * the trimmed text between the braces, as written. A string replaces the
3867
+ * token (`''` removes it); `null` or `undefined` leaves it as it is. The
3868
+ * editor never evaluates anything itself: this is the host's evaluator.
3869
+ *
3870
+ * ```ts
3871
+ * resolve: async (expressions) => {
3872
+ * const raw = Object.fromEntries(expressions.map((e, i) => [i, `{{${e}}}`]));
3873
+ * const parsed = await textParser.parseStrings(raw, referenceId, {});
3874
+ * return expressions.map((_, i) => parsed[i]);
3875
+ * }
3876
+ * ```
3877
+ */
3878
+ resolve: (expressions: string[]) => Promise<readonly (string | null | undefined)[]>;
3879
+ /** Called once the values are in the document. */
3880
+ onResolved?: (report: MergeTagResolutionReport) => void;
3881
+ /** Called when `resolve` rejects — the document is left as it was. */
3882
+ onError?: (error: unknown) => void;
3883
+ /** Called with `true` when a request goes out and `false` when it is
3884
+ settled — the host's spinner. */
3885
+ onPendingChange?: (pending: boolean) => void;
3886
+ }
3887
+ /** Whether a request is out — the action is disabled meanwhile, and a
3888
+ host can show it. */
3889
+ declare function mergeTagResolutionPending(state: EditorState): boolean;
3890
+ /**
3891
+ * The placeholder fill: `resolve` asks the host, `onResolved` reports. See
3892
+ * the file comment for the contract; the command is `resolveMergeTags`, the
3893
+ * action `resolve-placeholders`, the key Mod-Shift-Enter.
3894
+ */
3895
+ declare const createMergeTagResolution: (options: MergeTagResolutionOptions) => FunctionalExtension;
3896
+
3897
+ /**
3898
+ * Selected text in contenteditable is natively draggable, which is never
3899
+ * wanted in a composer. Node drags (e.g. images) stay allowed.
3900
+ */
3901
+ declare const NoTextDrag: _h_k_dev_angular_email_editor.FunctionalExtension;
3902
+
3903
+ /**
3904
+ * The marks active at the cursor that should *continue past a break* — dropping
3905
+ * any that opt out via `splittable: false` (e.g. `link`, so a break inside a
3906
+ * link doesn't drag it onto the next line).
3907
+ *
3908
+ * This is the single rule both break paths share: forced line breaks
3909
+ * (Shift-Enter, {@link ../nodes/hard-break}) and paragraph splits (Enter,
3910
+ * below). Formatting like font/colour/bold has no `splittable` flag, so it
3911
+ * survives — matching Gmail, where a font chosen on a bare cursor sticks for
3912
+ * the rest of the passage you type, new lines included.
3913
+ */
3914
+ declare function marksAcrossBreak(state: EditorState): readonly Mark[] | 0;
3915
+ /**
3916
+ * Binds Enter to a mark-preserving paragraph split. Must sit *after* the list
3917
+ * and blockquote extensions in the kit (so their own Enter handling wins inside
3918
+ * those structures) and *before* {@link ../base-keymap} (whose plain
3919
+ * `splitBlock` this replaces as the fallback). Lists already keep marks via
3920
+ * `splitListItemKeepMarks`; this closes the same gap for ordinary `<div>` lines.
3921
+ */
3922
+ declare const SplitKeepingMarks: _h_k_dev_angular_email_editor.FunctionalExtension;
3923
+
3924
+ /**
3925
+ * Enter, Backspace, Delete, select-all, ... Keep this last in the extension
3926
+ * list so node/mark specific bindings take precedence.
3927
+ */
3928
+ declare const BaseKeymap: _h_k_dev_angular_email_editor.FunctionalExtension;
3929
+
3930
+ /**
3931
+ * The inline image registry as an Angular service — the wiring around the
3932
+ * framework-free {@link InlineImageStore}: one per composer (provide it on
3933
+ * the composer component, never in root — two composers must not share
3934
+ * parts), a signal that bumps on every change so the preview and the host
3935
+ * can react, data URLs for the preview (a sandboxed, opaque-origin frame
3936
+ * cannot load the editor's blob URLs), and every URL revoked with the
3937
+ * composer. The editor pane hands it to `createInlineImages`; the host feeds
3938
+ * it an import's parts and reads the referenced ones for a draft.
3939
+ */
3940
+ declare class InlineImages extends InlineImageStore {
3941
+ #private;
3942
+ /** Bumps on every registered part (and again once its data URL is ready);
3943
+ read it in a `computed`/`effect` to re-derive from the registry. */
3944
+ readonly version: i0.Signal<number>;
3945
+ constructor();
3946
+ add(blob: Blob, cid?: string): string;
3947
+ revokeAll(): void;
3948
+ /** The part as a data URL, once read; undefined until then. */
3949
+ dataUrl(cid: string): string | undefined;
3950
+ /** The preview's projection of the canonical HTML: every `cid:` source the
3951
+ registry holds becomes a data URL. Reads {@link version}, so a
3952
+ `computed` over it re-runs when a part arrives. */
3953
+ previewHtml(html: string): string;
3954
+ static ɵfac: i0.ɵɵFactoryDeclaration<InlineImages, never>;
3955
+ static ɵprov: i0.ɵɵInjectableDeclaration<any>;
3956
+ }
3957
+
3958
+ export { AUTHORED_ATTR, AUTHORED_SPEC_KEY, AUTHORED_STYLE_DEFAULTS_KEY, AuthoredEnter, BLOCKQUOTE_STYLE, BLOCK_MENU_FOCUS_KEY, BUTTON_FILL, BUTTON_STYLE, BaseKeymap, Blockquote, Bold, BulletList, Button, CARET_ROOM, CLIENT_LABELS, COMMENT_TAG, CONTENT_ATTRS, CSS_SUPPORT, ClearFormatting, CodeLine, CodeLineIndent, Column, ColumnResize, Columns, ColumnsResize, DOCUMENT_CLASS, DOCUMENT_TAGS, DUAL_BACKGROUND_MIN_RATIO, DUAL_CONTRAST_DARK, DUAL_CONTRAST_LIGHT, DUAL_CONTRAST_MIN_RATIO, Divider, Document$1 as Document, DocumentStyles, EMAIL_ATTRIBUTES, EMAIL_CSS_PROPERTIES, EMAIL_SAFE_STYLE_PROPERTIES, EMAIL_SAFE_TAGS, EMAIL_TAGS, EMAIL_TAG_ATTRIBUTES, EmailParagraph, FILE_CHOOSER_POINTER_WAIT, FILL_TEXT_COLOR, FILL_TEXT_COLOR_LIGHT, FILL_TEXT_COLOR_RGB, FOLD_WIDTH, FORMAT_WIDTH, GMAIL_CLIP_BYTES, GUIDES_ACTIVE_CLASS, GUIDES_PEEK_CLASS, GUIDES_PEEK_DELAY_MS, Gapcursor, HEADING_SIZES, HardBreak, Heading, History, HtmlComment, HtmlCommentInline, HtmlElement, HtmlInline, HtmlLanguage, HtmlRaw, HtmlTextElement, HtmlVoid, HtmlVoidBlock, IMPORT_VIEWPORT_WIDTH, INDENT_MAX, INDENT_STEP, INLINE_TAGS, INLINE_VOID_TAGS, IN_SELECTION_CLASS, Image, InlineAtomSelection, InlineImageStore, InlineImages, Italic, LIST_STYLE, LayoutGuides, Link, ListItem, MAX_COLUMNS, MAX_IMAGE_WIDTH, MAX_MERGE_TAG_LENGTH, MAX_UNBROKEN_RUN, MIN_COLUMN_CAP, MIN_COLUMN_PCT, MIN_FONT_SIZE, MIN_IMAGE_WIDTH, MIN_TABLE_PCT, MJML_WORLDLY_HTML, MergeTag, NoTextDrag, OrderedList, PARSE_SCOPE_KEY, PLACEHOLDER_WIDTH, Paragraph, PasteHygiene, QuotedHistory, REFUSED_TAGS, RENDERING_CLIENTS, SECTION_CONTENT_MAX, SECTION_INSET, SECTION_PADDING, STACK_CLASS, STACK_RULE, Section, SplitKeepingMarks, TABLE_BORDER_COLOR, TABLE_CELL_PADDING, TABLE_SECTION_TAGS, TYPING_REST, Table, TableCell, TableRow, Text, TextStyle, UNSET_BUTTON_HREF, Underline, VOID_TAGS, acceptProposal, addColumn, addColumnAtEnd, addRowAtEnd, analyzeAngularExpression, angularDialect, angularRequiredFields, applyAuthoredAttributes, applyEmailVocabulary, attributeString, authoredAttribute, authoredMarkupOf, authoredViewAttributes, bareButtons, buttonStyle, canonicalHead, canonicalMergeTagInner, canonicalRender, caretCellPos, caretInsideMergeTag, claimedImageFiles, clearCells, clientList, columnCaps, columnWidths, completionContextAt, containerStyle, contrastRatio, correctStoredMistakes, createAngularExpressions, createBlockMenu, createBubbleMenu, createButtonEdit, createContentProposal, createContentStream, createDOMParser, createEditor, createHtmlAutocomplete, createHtmlLanguage, createImageDrag, createInlineImages, createMergeTagResolution, createOffsetMapper, createQuotedHistory, createSchema, createSendIntent, createSourceMarks, createSuggestionMenu, createTableHandles, declarationProperty, declaredBackground, decodeDataUrl, defaultBubbleShouldShow, defineExtension, defineMark, defineNode, deleteColumns, deleteFullySelected, discardProposal, distributePadding, docText, dropDeadOfficeClasses, dropHidden, dropMediaQueries, dropZeroSizedWhitespace, duplicateColumn, duplicateRow, editQuotedHistory, effectiveWidths, emailBackgroundPalette, emailDocument, emailExtensions, emailFontFamilies, emailFontSizes, emailPlainText, emailSizeBudget, emailTextPalette, embedsAsLinks, entitySpans, escapeAttribute, escapeText, expressionDialect, expressionDialectKey, extensionActions, extensionSuggestions, fillTextColor, filledPlaceholderAttrs, filterEmailStyle, findColumnContext, findColumnsContext, findCssIssues, findListDepth, findSectionContext, findTableContext, fitImageWidth, foldHTML, formatHTML, formatPct, forwardDocument, forwardQuote, hasBlockChildren, hasDocumentEnvelope, headingStyle, holdsBlock, holdsColumn, hrefProblem, htmlSourceExtensions, imageDropTarget, importLoss, importedDocument, inheritTextStyles, inheritedMarkup, inlineBootstrapTableBorders, inlineImageCids, inlineImageRegistry, inlineImagesKey, inlineStyles, insertHTML, insertMergeTag, isActionEnabled, isBlock, isBlockLevel, isBlockish, isBooleanAttribute, isEmailAttribute, isEmailCssProperty, isEmailTag, isFillTextColor, isInlineAtom, isMarkActive, isMergeTagExpression, isMergeTagPath, isNodeActive, isPadded, isProposing, isQuotedHistoryFolded, isSafeColor, isSafeFontFamily, isSafeFontSize, isSafeUrl, isSectionImageUrl, isStreaming, italicStarInputRegex, italicStarPasteRegex, italicUnderscoreInputRegex, italicUnderscorePasteRegex, keepGmailRules, layoutBlockAt, linkClickPlugin, linkPastePlugin, linkRangeAt, lintHTML, lowerInlinesAroundBlocks, markAttrsOf, markStackingColumns, marksAcrossBreak, materializeComments, measureMenuPlacement, mediaMatches, mergeTagAt, mergeTagExpressions, mergeTagFields, mergeTagRanges, mergeTagResolutionPending, moveColumn, moveRow, normalizeCommentText, normalizeCss, normalizeHref, normalizeMergeTagText, noteOwnWidths, openFileChooser, openTag, openTags, outlookColumns, paddingOf, parseAngularExpression, parseDocument, parseFontFamily, parseFontSize, parseHTML, parsePadding, parseRules, parseTextTransform, passesDualBackground, passesDualContrast, preserveExtensions, preservedAttributes, promoteInlineImages, promoteMergeTags, proposalRange, proposeContent, quotedHistory, readEnvelope, readImageFile, realizeComments, relativeLuminance, removeColumn, removeQuotedHistory, renderForClient, renderedPath, repairLinkSchemes, repairTables, replaceHTML, replyDocument, replyEnvelope, replyQuote, reviseProposal, rewriteInlineImageSources, richTextExtensions, ruleForScope, ruleScope, sanitizePastedHTML, scanHTML, scanMergeTags, scopeOf, scopedDocumentCss, selectColumn, selectRow, selectedButton, selectedImage, selectedImageAlt, selectedMarkAtom, selectionInsideMergeTag, serializeDocument, serializeToHTML, setCellBackground, setColumnBackground, setColumnBoundary, setColumnsBoundary, setQuotedHistory, setSectionBackground, setSectionImage, setTableBox, setTableWidth, soleInlineAtom, splitDeclarations, splitListItemKeepMarks, starInputRegex, starPasteRegex, stopProposal, streamContent, streamedRange, streamingRange, suggestionSectionTitles, tableStyle, textOffsetAt, textRegions, toEmailSafeColor, toInboundMessage, toggleList, trimInlineBoxes, underscoreInputRegex, underscorePasteRegex, unwrapLayoutTables, unwrapOfficeParagraphMarks, verbatimElement, videoPage, watchMenuScroll, withAttribute, withAuthoredMarkup, withQuoted, withQuotedHistory, withStyleDeclaration, withoutAttribute, withoutStyleDeclaration };
3959
+ export type { AngularExpressionsOptions, AngularNode, AttributePairs, AuthoredElement, AuthoredMarkup, AutocompleteItem, AutocompleteOptions, AutocompleteState, BlockMenuOptions, BlockMenuState, BubbleMenuOptions, BubbleMenuState, ButtonEditOptions, ButtonEditTarget, CellAlignment, CellVerticalAlignment, ClientRenderingOptions, CommandFactory, CompletionContext, ComposeSeedLabels, ComposeSeedOptions, ContentProposalOptions, ContentProposalState, ContentStreamOptions, ContentStreamRange, ContentStreamReveal, ContentStreamRun, ContentStreamState, ContentStreamWriter, CssSupportIssue, DocumentEnvelope, Editor, EditorAction, EditorOptions, EmailClient, EmailDocumentOptions, EmailFont, ExpressionDiagnostic, ExpressionDialect, ExpressionIssue, Extension, ExtensionContext, FontSize, FunctionalExtension, HrefProblem, HtmlDiagnostic, HtmlLanguageOptions, HtmlScan, HtmlTag, HtmlToken, HtmlTokenType, ImageAttrs, ImageDragOptions, ImageDropTarget, ImportLoss, InboundMessage, InlineAtomAt, InlineImage, InlineImageRegistry, InlineImageStoreOptions, LayoutBlock, LayoutBlockType, LinkAttrs, LinkRange, MarkExtension, MenuAnchor, MenuScroll, MenuScrollApi, MergeTagRange, MergeTagResolutionOptions, MergeTagResolutionReport, NodeExtension, PaletteColor, ParagraphAlignment, ParseMode, ParseOptions, ParsedAddressLike, ParsedEmailLike, PromotedInlineImages, QuotedHistoryLabel, QuotedHistoryLabels, QuotedHistoryOptions, RenderingClient, ReplyEnvelope, ReplyEnvelopeOptions, ReplyKind, RuleScope, SendIntent, SendIntentOptions, SizeBudget, SourceMarksOptions, SourceOptions, StreamContentOptions, SuggestionAllowProps, SuggestionCommandItem, SuggestionGroup, SuggestionItem, SuggestionItemLabel, SuggestionMenuOptions, SuggestionMenuState, SuggestionPage, SuggestionRect, SuggestionRequest, SuggestionSource, SuggestionTrigger, TableHandleTarget, TableHandlesOptions, TextTransform };