@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.
- package/README.md +127 -3
- package/fesm2022/h-k-dev-angular-email-editor-actions.mjs +179 -0
- package/fesm2022/h-k-dev-angular-email-editor-actions.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-address-chip.mjs +285 -0
- package/fesm2022/h-k-dev-angular-email-editor-address-chip.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-address-input.mjs +686 -0
- package/fesm2022/h-k-dev-angular-email-editor-address-input.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-address.mjs +489 -0
- package/fesm2022/h-k-dev-angular-email-editor-address.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-anchor.mjs +64 -0
- package/fesm2022/h-k-dev-angular-email-editor-anchor.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-attachment-chip.mjs +387 -0
- package/fesm2022/h-k-dev-angular-email-editor-attachment-chip.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-attachment-chips.mjs +80 -0
- package/fesm2022/h-k-dev-angular-email-editor-attachment-chips.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-chat-input.mjs +346 -0
- package/fesm2022/h-k-dev-angular-email-editor-chat-input.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-focus.mjs +68 -0
- package/fesm2022/h-k-dev-angular-email-editor-focus.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-image.mjs +132 -0
- package/fesm2022/h-k-dev-angular-email-editor-image.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-material.mjs +124 -0
- package/fesm2022/h-k-dev-angular-email-editor-material.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-metrics.mjs +243 -0
- package/fesm2022/h-k-dev-angular-email-editor-metrics.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-palette.mjs +87 -0
- package/fesm2022/h-k-dev-angular-email-editor-palette.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-proposal.mjs +307 -0
- package/fesm2022/h-k-dev-angular-email-editor-proposal.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-suggestion-menu.mjs +392 -0
- package/fesm2022/h-k-dev-angular-email-editor-suggestion-menu.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor.mjs +18818 -0
- package/fesm2022/h-k-dev-angular-email-editor.mjs.map +1 -0
- package/package.json +130 -6
- package/styles/_scroll.scss +101 -0
- package/types/h-k-dev-angular-email-editor-actions.d.ts +147 -0
- package/types/h-k-dev-angular-email-editor-address-chip.d.ts +193 -0
- package/types/h-k-dev-angular-email-editor-address-input.d.ts +328 -0
- package/types/h-k-dev-angular-email-editor-address.d.ts +125 -0
- package/types/h-k-dev-angular-email-editor-anchor.d.ts +44 -0
- package/types/h-k-dev-angular-email-editor-attachment-chip.d.ts +271 -0
- package/types/h-k-dev-angular-email-editor-attachment-chips.d.ts +48 -0
- package/types/h-k-dev-angular-email-editor-chat-input.d.ts +101 -0
- package/types/h-k-dev-angular-email-editor-focus.d.ts +40 -0
- package/types/h-k-dev-angular-email-editor-image.d.ts +66 -0
- package/types/h-k-dev-angular-email-editor-material.d.ts +65 -0
- package/types/h-k-dev-angular-email-editor-metrics.d.ts +110 -0
- package/types/h-k-dev-angular-email-editor-palette.d.ts +51 -0
- package/types/h-k-dev-angular-email-editor-proposal.d.ts +170 -0
- package/types/h-k-dev-angular-email-editor-suggestion-menu.d.ts +177 -0
- 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 ` ` 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;\"> ☰ </span>\n <span class=\"mj-menu-icon-close\" style=\"display:none;mso-hide:all;\"> ⊗ </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 (`&`, `©`, `©`) — 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">· </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 (`&&`) 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 };
|