@dawnlit/outlook-bridge 1.1.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -183
- package/dist/backend.d.ts +151 -0
- package/dist/backend.js +3 -0
- package/dist/backend.js.map +1 -0
- package/dist/bridge.d.ts +56 -0
- package/dist/bridge.js +507 -0
- package/dist/bridge.js.map +1 -0
- package/dist/errors.d.ts +81 -43
- package/dist/errors.js +98 -52
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/index.js +25 -52
- package/dist/index.js.map +1 -1
- package/dist/mac/accounts.d.ts +7 -2
- package/dist/mac/accounts.js +23 -6
- package/dist/mac/accounts.js.map +1 -1
- package/dist/mac/attachments.d.ts +5 -25
- package/dist/mac/attachments.js +40 -99
- package/dist/mac/attachments.js.map +1 -1
- package/dist/mac/bounces.d.ts +12 -27
- package/dist/mac/bounces.js +95 -112
- package/dist/mac/bounces.js.map +1 -1
- package/dist/mac/cleanup.d.ts +11 -25
- package/dist/mac/cleanup.js +50 -76
- package/dist/mac/cleanup.js.map +1 -1
- package/dist/mac/drafts.d.ts +13 -34
- package/dist/mac/drafts.js +95 -169
- package/dist/mac/drafts.js.map +1 -1
- package/dist/mac/folders.d.ts +10 -21
- package/dist/mac/folders.js +33 -47
- package/dist/mac/folders.js.map +1 -1
- package/dist/mac/index.d.ts +2 -22
- package/dist/mac/index.js +19 -81
- package/dist/mac/index.js.map +1 -1
- package/dist/mac/profile.d.ts +13 -0
- package/dist/mac/profile.js +149 -0
- package/dist/mac/profile.js.map +1 -0
- package/dist/mac/read.d.ts +18 -38
- package/dist/mac/read.js +154 -210
- package/dist/mac/read.js.map +1 -1
- package/dist/mac/run.d.ts +35 -39
- package/dist/mac/run.js +97 -102
- package/dist/mac/run.js.map +1 -1
- package/dist/mac/scripts.d.ts +78 -86
- package/dist/mac/scripts.js +125 -115
- package/dist/mac/scripts.js.map +1 -1
- package/dist/mac/send.d.ts +10 -20
- package/dist/mac/send.js +64 -86
- package/dist/mac/send.js.map +1 -1
- package/dist/mac/signatures.d.ts +2 -2
- package/dist/mac/signatures.js +9 -11
- package/dist/mac/signatures.js.map +1 -1
- package/dist/mac/templates.d.ts +14 -26
- package/dist/mac/templates.js +75 -113
- package/dist/mac/templates.js.map +1 -1
- package/dist/mail.d.ts +77 -45
- package/dist/mail.js +166 -73
- package/dist/mail.js.map +1 -1
- package/dist/runtime.d.ts +76 -43
- package/dist/runtime.js +103 -48
- package/dist/runtime.js.map +1 -1
- package/dist/shared/args.d.ts +48 -0
- package/dist/shared/args.js +216 -0
- package/dist/shared/args.js.map +1 -0
- package/dist/shared/attachmentMatch.d.ts +28 -16
- package/dist/shared/attachmentMatch.js +60 -22
- package/dist/shared/attachmentMatch.js.map +1 -1
- package/dist/shared/bounceReport.d.ts +11 -0
- package/dist/shared/bounceReport.js +45 -0
- package/dist/shared/bounceReport.js.map +1 -0
- package/dist/shared/bounceRules.d.ts +38 -10
- package/dist/shared/bounceRules.js +50 -32
- package/dist/shared/bounceRules.js.map +1 -1
- package/dist/shared/exec.d.ts +21 -0
- package/dist/shared/exec.js +47 -0
- package/dist/shared/exec.js.map +1 -0
- package/dist/shared/json.d.ts +12 -6
- package/dist/shared/json.js +29 -23
- package/dist/shared/json.js.map +1 -1
- package/dist/shared/locate.d.ts +39 -0
- package/dist/shared/locate.js +129 -0
- package/dist/shared/locate.js.map +1 -0
- package/dist/shared/replyBody.d.ts +42 -32
- package/dist/shared/replyBody.js +84 -71
- package/dist/shared/replyBody.js.map +1 -1
- package/dist/templateBody.d.ts +63 -0
- package/dist/{outlookTemplateSections.js → templateBody.js} +72 -72
- package/dist/templateBody.js.map +1 -0
- package/dist/types.d.ts +360 -167
- package/dist/types.js +8 -9
- package/dist/types.js.map +1 -1
- package/dist/windows/accounts.d.ts +7 -12
- package/dist/windows/accounts.js +19 -34
- package/dist/windows/accounts.js.map +1 -1
- package/dist/windows/attachments.d.ts +9 -24
- package/dist/windows/attachments.js +94 -119
- package/dist/windows/attachments.js.map +1 -1
- package/dist/windows/bounces.d.ts +13 -26
- package/dist/windows/bounces.js +168 -200
- package/dist/windows/bounces.js.map +1 -1
- package/dist/windows/cleanup.d.ts +19 -23
- package/dist/windows/cleanup.js +92 -111
- package/dist/windows/cleanup.js.map +1 -1
- package/dist/windows/drafts.d.ts +16 -35
- package/dist/windows/drafts.js +167 -253
- package/dist/windows/drafts.js.map +1 -1
- package/dist/windows/folders.d.ts +8 -13
- package/dist/windows/folders.js +44 -52
- package/dist/windows/folders.js.map +1 -1
- package/dist/windows/index.d.ts +2 -27
- package/dist/windows/index.js +11 -66
- package/dist/windows/index.js.map +1 -1
- package/dist/windows/read.d.ts +24 -60
- package/dist/windows/read.js +261 -306
- package/dist/windows/read.js.map +1 -1
- package/dist/windows/run.d.ts +39 -39
- package/dist/windows/run.js +151 -111
- package/dist/windows/run.js.map +1 -1
- package/dist/windows/scripts.d.ts +65 -55
- package/dist/windows/scripts.js +109 -85
- package/dist/windows/scripts.js.map +1 -1
- package/dist/windows/send.d.ts +6 -11
- package/dist/windows/send.js +68 -88
- package/dist/windows/send.js.map +1 -1
- package/dist/windows/signatures.d.ts +4 -10
- package/dist/windows/signatures.js +30 -37
- package/dist/windows/signatures.js.map +1 -1
- package/dist/windows/templates.d.ts +12 -18
- package/dist/windows/templates.js +122 -149
- package/dist/windows/templates.js.map +1 -1
- package/package.json +4 -3
- package/dist/OutlookService.d.ts +0 -43
- package/dist/OutlookService.js +0 -137
- package/dist/OutlookService.js.map +0 -1
- package/dist/outlookTemplateSections.d.ts +0 -62
- package/dist/outlookTemplateSections.js.map +0 -1
- package/dist/shared/capabilities.d.ts +0 -3
- package/dist/shared/capabilities.js +0 -9
- package/dist/shared/capabilities.js.map +0 -1
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Sections and placeholders in HTML email templates.
|
|
4
4
|
*
|
|
5
|
-
* A template email is
|
|
6
|
-
* greeting, closing and signature
|
|
7
|
-
* reply
|
|
5
|
+
* A template email is an ordinary saved mail its owner edits in Outlook, so the
|
|
6
|
+
* greeting, closing and signature stay theirs. Where one template serves several
|
|
7
|
+
* kinds of reply, the variants live inside it between markers:
|
|
8
8
|
*
|
|
9
9
|
* Hello,
|
|
10
|
-
* [[
|
|
11
|
-
* [[
|
|
12
|
-
* [[CLARIFY]] {{QUESTIONS}}
|
|
13
|
-
* Best regards,
|
|
10
|
+
* [[ACCEPT]] Thank you — we accept... [[/ACCEPT]]
|
|
11
|
+
* [[DECLINE]] Thank you for letting us know... [[/DECLINE]]
|
|
12
|
+
* [[CLARIFY]] {{QUESTIONS}} [[/CLARIFY]]
|
|
13
|
+
* Best regards, {{SIGNATURE}}
|
|
14
14
|
*
|
|
15
|
-
* Composing keeps the requested section, drops the others, and substitutes
|
|
16
|
-
* {{PLACEHOLDER}} — so
|
|
17
|
-
*
|
|
15
|
+
* Composing keeps the requested section, drops the others, and substitutes the
|
|
16
|
+
* `{{PLACEHOLDER}}`s — so shared wording is written once and a caller names a
|
|
17
|
+
* section instead of carrying the whole body.
|
|
18
18
|
*
|
|
19
|
-
* Everything
|
|
20
|
-
* markup carries conditional comments and namespaced tags
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* Everything works on the raw HTML string rather than a parsed tree: Word's
|
|
20
|
+
* markup carries conditional comments and namespaced tags a parser round-trip
|
|
21
|
+
* would rewrite, and the composed body must stay byte-identical to what was
|
|
22
|
+
* written.
|
|
23
23
|
*/
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
25
|
exports.findTemplateMarkers = findTemplateMarkers;
|
|
@@ -29,37 +29,37 @@ exports.findTokens = findTokens;
|
|
|
29
29
|
exports.findUnfilledTokens = findUnfilledTokens;
|
|
30
30
|
exports.composeTemplateBody = composeTemplateBody;
|
|
31
31
|
const errors_1 = require("./errors");
|
|
32
|
-
/** Marker
|
|
32
|
+
/** Marker and placeholder names: what a person can reasonably type in Outlook. */
|
|
33
33
|
const NAME = '[A-Za-z0-9_-]{1,40}';
|
|
34
34
|
/**
|
|
35
|
-
* What Word may inject *between two characters of a marker*: tags (it splits
|
|
36
|
-
* across <span>s freely), comments, non-breaking spaces, and the newlines
|
|
37
|
-
* long lines with — sometimes mid-word. So `[[
|
|
38
|
-
* character with this between, never as a literal string.
|
|
35
|
+
* What Word may inject *between two characters of a marker*: tags (it splits
|
|
36
|
+
* runs across <span>s freely), comments, non-breaking spaces, and the newlines
|
|
37
|
+
* it wraps long lines with — sometimes mid-word. So `[[ACCEPT]]` is matched
|
|
38
|
+
* character by character with this between, never as a literal string.
|
|
39
39
|
*/
|
|
40
40
|
const GAP = '(?:<!--[\\s\\S]*?-->|<[^>]*>| | |\\s)*';
|
|
41
41
|
/** Block elements a marker can sit alone inside; the whole block goes when it does. */
|
|
42
42
|
const BLOCK_TAGS = ['p', 'div', 'li'];
|
|
43
43
|
/**
|
|
44
|
-
* Block elements that make up one *line* of a template, for a
|
|
45
|
-
* empty value takes its whole line out (see `removeTokenLine`).
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
44
|
+
* Block elements that make up one *line* of a template, for a token whose
|
|
45
|
+
* empty value takes its whole line out (see `removeTokenLine`). Outermost
|
|
46
|
+
* first: Word puts a <p> inside every table cell, so a token in a details table
|
|
47
|
+
* has to drop the <tr> — dropping the <p> would leave the row's label beside a
|
|
48
|
+
* blank cell.
|
|
49
49
|
*
|
|
50
50
|
* <div> is deliberately absent: it is rarely a line and often the wrapper around
|
|
51
|
-
* the entire body, so a token
|
|
52
|
-
* with it.
|
|
51
|
+
* the entire body, so a token directly inside one would take the whole email
|
|
52
|
+
* with it. Deleting just the token is the safer miss.
|
|
53
53
|
*/
|
|
54
54
|
const LINE_TAGS = ['tr', 'li', 'p'];
|
|
55
55
|
function escapeRe(s) {
|
|
56
56
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
* Tag-tolerant matcher for one literal
|
|
59
|
+
* Tag-tolerant matcher for one literal token, e.g. `[[/ACCEPT]]` or `{{QUESTIONS}}`.
|
|
60
60
|
*
|
|
61
|
-
* Memoized: the pattern interleaves `GAP` between every character, so a
|
|
62
|
-
* token compiles to
|
|
61
|
+
* Memoized: the pattern interleaves `GAP` between every character, so a short
|
|
62
|
+
* token compiles to hundreds of characters, and the replace loops ask for the
|
|
63
63
|
* same token repeatedly while walking a template.
|
|
64
64
|
*/
|
|
65
65
|
const markerRegexCache = new Map();
|
|
@@ -71,14 +71,14 @@ function markerRegex(token) {
|
|
|
71
71
|
}
|
|
72
72
|
return regex;
|
|
73
73
|
}
|
|
74
|
-
/** HTML → rough plain text, enough to spot markers and
|
|
74
|
+
/** HTML → rough plain text, enough to spot markers and test a block for emptiness. */
|
|
75
75
|
function stripToText(html) {
|
|
76
76
|
return html
|
|
77
77
|
.replace(/<!--[\s\S]*?-->/g, '')
|
|
78
78
|
.replace(/<[^>]*>/g, '')
|
|
79
79
|
.replace(/ | /gi, ' ');
|
|
80
80
|
}
|
|
81
|
-
/** Find the markers in a template body (HTML or the plain
|
|
81
|
+
/** Find the markers in a template body (HTML, or the plain text Outlook derives from it). */
|
|
82
82
|
function findTemplateMarkers(body) {
|
|
83
83
|
const text = stripToText(body || '');
|
|
84
84
|
const opened = [];
|
|
@@ -106,9 +106,9 @@ function findTemplateMarkers(body) {
|
|
|
106
106
|
return { sections: opened.filter(n => closed.has(n)), placeholders };
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
|
-
* Every `[[...]]` / `{{...}}` token still in a body,
|
|
110
|
-
*
|
|
111
|
-
*
|
|
109
|
+
* Every `[[...]]` / `{{...}}` token still in a body, paired or not — the last
|
|
110
|
+
* check before a composed body goes out, so a half-deleted marker stops the
|
|
111
|
+
* send instead of reaching a recipient.
|
|
112
112
|
*/
|
|
113
113
|
function markerTokens(body) {
|
|
114
114
|
const text = stripToText(body || '');
|
|
@@ -125,8 +125,8 @@ function markerTokens(body) {
|
|
|
125
125
|
/**
|
|
126
126
|
* The nearest enclosing block element of [start, end), or null when the marker
|
|
127
127
|
* isn't inside one. The closing tag is the first of the same name after the
|
|
128
|
-
* marker — Word doesn't nest <p>, and where
|
|
129
|
-
* test
|
|
128
|
+
* marker — Word doesn't nest <p>, and where that guess is wrong the emptiness
|
|
129
|
+
* test simply fails and the caller falls back to the marker's own span.
|
|
130
130
|
*/
|
|
131
131
|
function enclosingBlock(html, start, end, tags = BLOCK_TAGS) {
|
|
132
132
|
let best = null;
|
|
@@ -155,9 +155,8 @@ function enclosingBlock(html, start, end, tags = BLOCK_TAGS) {
|
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
157
|
/**
|
|
158
|
-
* What to delete for a marker:
|
|
159
|
-
* (removing only the text would leave a blank line
|
|
160
|
-
* the marker's own characters.
|
|
158
|
+
* What to delete for a marker: its whole paragraph when the marker is alone in
|
|
159
|
+
* it (removing only the text would leave a blank line), else just the marker.
|
|
161
160
|
*/
|
|
162
161
|
function markerSpan(html, start, end) {
|
|
163
162
|
const found = enclosingBlock(html, start, end);
|
|
@@ -170,13 +169,13 @@ function locate(html, token) {
|
|
|
170
169
|
const m = markerRegex(token).exec(html);
|
|
171
170
|
return m ? { start: m.index, end: m.index + m[0].length } : null;
|
|
172
171
|
}
|
|
173
|
-
// ── Single-token fill
|
|
172
|
+
// ── Single-token fill ───────────────────────────────────────────────────
|
|
174
173
|
//
|
|
175
|
-
// The section machinery
|
|
176
|
-
//
|
|
177
|
-
// and
|
|
178
|
-
//
|
|
179
|
-
//
|
|
174
|
+
// The section machinery above is for templates holding several variants of
|
|
175
|
+
// one email. These are for the simpler case: fixed wording with details typed
|
|
176
|
+
// elsewhere and dropped into tokens of the caller's own choosing (`{Name}`,
|
|
177
|
+
// `%NAME%`, …). Same tag-tolerant matching, since these templates are edited in
|
|
178
|
+
// Outlook too.
|
|
180
179
|
/** Replace every occurrence of `token` with `value`, which is never rescanned. */
|
|
181
180
|
function replaceToken(html, token, value) {
|
|
182
181
|
let done = '';
|
|
@@ -190,9 +189,9 @@ function replaceToken(html, token, value) {
|
|
|
190
189
|
}
|
|
191
190
|
}
|
|
192
191
|
/**
|
|
193
|
-
* Remove `token`
|
|
194
|
-
* label included. For an optional detail this is the difference
|
|
195
|
-
* not mentioning it
|
|
192
|
+
* Remove `token` together with the line it sits on — the table row or
|
|
193
|
+
* paragraph, label included. For an optional detail this is the difference
|
|
194
|
+
* between the email not mentioning it and a heading with nothing under it.
|
|
196
195
|
* Falls back to deleting just the token when it isn't inside a line element.
|
|
197
196
|
*/
|
|
198
197
|
function removeTokenLine(html, token) {
|
|
@@ -206,13 +205,13 @@ function removeTokenLine(html, token) {
|
|
|
206
205
|
}
|
|
207
206
|
}
|
|
208
207
|
/**
|
|
209
|
-
* The line to delete for a token at `at`: its enclosing row or paragraph, or
|
|
210
|
-
* token's own span when it isn't in one.
|
|
208
|
+
* The line to delete for a token at `at`: its enclosing row or paragraph, or
|
|
209
|
+
* the token's own span when it isn't in one.
|
|
211
210
|
*
|
|
212
211
|
* `enclosingBlock` returns the *nearest* opening tag before the token, which is
|
|
213
|
-
* only the enclosing one if it hasn't
|
|
212
|
+
* only the enclosing one if it hasn't been closed already — a token in a
|
|
214
213
|
* paragraph after a table has a `<tr>` before it and a `</tr>` after it without
|
|
215
|
-
*
|
|
214
|
+
* being inside a row, and taking that span would delete the table.
|
|
216
215
|
*/
|
|
217
216
|
function lineSpan(html, at) {
|
|
218
217
|
for (const tag of LINE_TAGS) {
|
|
@@ -231,9 +230,10 @@ function findTokens(html, tokens) {
|
|
|
231
230
|
return tokens.filter(token => markerRegex(token).test(text));
|
|
232
231
|
}
|
|
233
232
|
/**
|
|
234
|
-
* `{Token}`-shaped text still in a filled-in body: a placeholder misspelled
|
|
235
|
-
*
|
|
236
|
-
* braces
|
|
233
|
+
* `{Token}`-shaped text still in a filled-in body: a placeholder misspelled
|
|
234
|
+
* while editing, or one nothing supplied. Reads visible text only, so the
|
|
235
|
+
* braces of a <style> block are not mistaken for placeholders. Catches a
|
|
236
|
+
* leftover `{{TOKEN}}` too, reported by its inner `{TOKEN}`.
|
|
237
237
|
*/
|
|
238
238
|
function findUnfilledTokens(html) {
|
|
239
239
|
const text = stripToText((html || '').replace(/<(style|script)\b[^>]*>[\s\S]*?<\/\1>/gi, ''));
|
|
@@ -248,10 +248,10 @@ function findUnfilledTokens(html) {
|
|
|
248
248
|
return found;
|
|
249
249
|
}
|
|
250
250
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
251
|
+
* Reduce a template body to the one email that goes out: keep `section`, drop
|
|
252
|
+
* the rest, fill the placeholders. Throws INVALID_REQUEST rather than guessing —
|
|
253
|
+
* a template edited into an unusable state has to stop the send, not deliver a
|
|
254
|
+
* body with `[[ACCEPT]]` or an unfilled placeholder in it.
|
|
255
255
|
*/
|
|
256
256
|
function composeTemplateBody(html, options = {}) {
|
|
257
257
|
const label = options.label ? `Template '${options.label}'` : 'The template';
|
|
@@ -260,14 +260,14 @@ function composeTemplateBody(html, options = {}) {
|
|
|
260
260
|
if (wanted && !sections.includes(wanted)) {
|
|
261
261
|
throw new errors_1.InvalidRequestError(sections.length
|
|
262
262
|
? `${label} has no [[${wanted}]] section. Sections found: ${sections.join(', ')}.`
|
|
263
|
-
: `${label} has no [[${wanted}]]...[[/${wanted}]] markers — it holds a single body, so
|
|
263
|
+
: `${label} has no [[${wanted}]]...[[/${wanted}]] markers — it holds a single body, so name no section (or add the markers to the template).`);
|
|
264
264
|
}
|
|
265
265
|
if (!wanted && sections.length) {
|
|
266
|
-
throw new errors_1.InvalidRequestError(`${label} is split into sections (${sections.join(', ')})
|
|
267
|
-
+
|
|
266
|
+
throw new errors_1.InvalidRequestError(`${label} is split into sections (${sections.join(', ')}); name the one to send, `
|
|
267
|
+
+ 'otherwise the email would go out with every variant and the markers in it.');
|
|
268
268
|
}
|
|
269
|
-
// Deletions are collected first and applied right
|
|
270
|
-
//
|
|
269
|
+
// Deletions are collected first and applied right to left, so each span's
|
|
270
|
+
// offsets still refer to the string they were measured against.
|
|
271
271
|
const cuts = [];
|
|
272
272
|
for (const name of sections) {
|
|
273
273
|
const open = locate(html, `[[${name}]]`);
|
|
@@ -293,21 +293,21 @@ function composeTemplateBody(html, options = {}) {
|
|
|
293
293
|
if (!available.includes(key)) {
|
|
294
294
|
throw new errors_1.InvalidRequestError(`${label} has no {{${key}}} placeholder`
|
|
295
295
|
+ (available.length ? ` (found: ${available.map(p => `{{${p}}}`).join(', ')}).` : '.')
|
|
296
|
-
+
|
|
296
|
+
+ ' It may have been removed when the template was last edited.');
|
|
297
297
|
}
|
|
298
298
|
const m = markerRegex(`{{${key}}}`).exec(out);
|
|
299
|
-
//
|
|
300
|
-
// section
|
|
299
|
+
// In the template but gone from `out` means it lived in a dropped
|
|
300
|
+
// section: the caller supplying more than one variant needs, not an error.
|
|
301
301
|
if (m)
|
|
302
302
|
out = out.slice(0, m.index) + (value ?? '') + out.slice(m.index + m[0].length);
|
|
303
303
|
}
|
|
304
304
|
const leftover = markerTokens(out);
|
|
305
305
|
if (leftover.length) {
|
|
306
306
|
const hint = leftover.every(t => t.startsWith('{{'))
|
|
307
|
-
?
|
|
308
|
-
:
|
|
309
|
-
throw new errors_1.InvalidRequestError(`${label} still contains unresolved markers after composing: ${leftover.join(', ')}. ${hint}
|
|
307
|
+
? 'Supply a value for each placeholder.'
|
|
308
|
+
: 'Check the template — every [[SECTION]] needs its [[/SECTION]].';
|
|
309
|
+
throw new errors_1.InvalidRequestError(`${label} still contains unresolved markers after composing: ${leftover.join(', ')}. ${hint} Nothing was created.`);
|
|
310
310
|
}
|
|
311
311
|
return out;
|
|
312
312
|
}
|
|
313
|
-
//# sourceMappingURL=
|
|
313
|
+
//# sourceMappingURL=templateBody.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templateBody.js","sourceRoot":"","sources":["../src/templateBody.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Sections and placeholders in HTML email templates.\n *\n * A template email is an ordinary saved mail its owner edits in Outlook, so the\n * greeting, closing and signature stay theirs. Where one template serves several\n * kinds of reply, the variants live inside it between markers:\n *\n * Hello,\n * [[ACCEPT]] Thank you — we accept... [[/ACCEPT]]\n * [[DECLINE]] Thank you for letting us know... [[/DECLINE]]\n * [[CLARIFY]] {{QUESTIONS}} [[/CLARIFY]]\n * Best regards, {{SIGNATURE}}\n *\n * Composing keeps the requested section, drops the others, and substitutes the\n * `{{PLACEHOLDER}}`s — so shared wording is written once and a caller names a\n * section instead of carrying the whole body.\n *\n * Everything works on the raw HTML string rather than a parsed tree: Word's\n * markup carries conditional comments and namespaced tags a parser round-trip\n * would rewrite, and the composed body must stay byte-identical to what was\n * written.\n */\n\nimport { InvalidRequestError } from './errors';\n\n/** Marker and placeholder names: what a person can reasonably type in Outlook. */\nconst NAME = '[A-Za-z0-9_-]{1,40}';\n\n/**\n * What Word may inject *between two characters of a marker*: tags (it splits\n * runs across <span>s freely), comments, non-breaking spaces, and the newlines\n * it wraps long lines with — sometimes mid-word. So `[[ACCEPT]]` is matched\n * character by character with this between, never as a literal string.\n */\nconst GAP = '(?:<!--[\\\\s\\\\S]*?-->|<[^>]*>| | |\\\\s)*';\n\n/** Block elements a marker can sit alone inside; the whole block goes when it does. */\nconst BLOCK_TAGS = ['p', 'div', 'li'];\n\n/**\n * Block elements that make up one *line* of a template, for a token whose\n * empty value takes its whole line out (see `removeTokenLine`). Outermost\n * first: Word puts a <p> inside every table cell, so a token in a details table\n * has to drop the <tr> — dropping the <p> would leave the row's label beside a\n * blank cell.\n *\n * <div> is deliberately absent: it is rarely a line and often the wrapper around\n * the entire body, so a token directly inside one would take the whole email\n * with it. Deleting just the token is the safer miss.\n */\nconst LINE_TAGS = ['tr', 'li', 'p'];\n\nfunction escapeRe(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Tag-tolerant matcher for one literal token, e.g. `[[/ACCEPT]]` or `{{QUESTIONS}}`.\n *\n * Memoized: the pattern interleaves `GAP` between every character, so a short\n * token compiles to hundreds of characters, and the replace loops ask for the\n * same token repeatedly while walking a template.\n */\nconst markerRegexCache = new Map<string, RegExp>();\n\nfunction markerRegex(token: string): RegExp {\n let regex = markerRegexCache.get(token);\n if (!regex) {\n regex = new RegExp(token.split('').map(escapeRe).join(GAP), 'i');\n markerRegexCache.set(token, regex);\n }\n return regex;\n}\n\n/** HTML → rough plain text, enough to spot markers and test a block for emptiness. */\nfunction stripToText(html: string): string {\n return html\n .replace(/<!--[\\s\\S]*?-->/g, '')\n .replace(/<[^>]*>/g, '')\n .replace(/ | /gi, ' ');\n}\n\nexport interface TemplateMarkers {\n /** Section names with both an opening and a closing marker, in document order. */\n sections: string[];\n /** `{{NAME}}` placeholders found anywhere in the template. */\n placeholders: string[];\n}\n\n/** Find the markers in a template body (HTML, or the plain text Outlook derives from it). */\nexport function findTemplateMarkers(body: string): TemplateMarkers {\n const text = stripToText(body || '');\n const opened: string[] = [];\n const closed = new Set<string>();\n const seen = new Set<string>();\n const sectionRe = new RegExp(`\\\\[\\\\[\\\\s*(/?)\\\\s*(${NAME})\\\\s*\\\\]\\\\]`, 'g');\n let m: RegExpExecArray | null;\n while ((m = sectionRe.exec(text)) !== null) {\n const name = m[2].toUpperCase();\n if (m[1]) {\n closed.add(name);\n } else if (!seen.has(name)) {\n seen.add(name);\n opened.push(name);\n }\n }\n const placeholders: string[] = [];\n const placeholderRe = new RegExp(`\\\\{\\\\{\\\\s*(${NAME})\\\\s*\\\\}\\\\}`, 'g');\n while ((m = placeholderRe.exec(text)) !== null) {\n const name = m[1].toUpperCase();\n if (!placeholders.includes(name)) placeholders.push(name);\n }\n return {sections: opened.filter(n => closed.has(n)), placeholders};\n}\n\n/**\n * Every `[[...]]` / `{{...}}` token still in a body, paired or not — the last\n * check before a composed body goes out, so a half-deleted marker stops the\n * send instead of reaching a recipient.\n */\nfunction markerTokens(body: string): string[] {\n const text = stripToText(body || '');\n const re = new RegExp(`\\\\[\\\\[\\\\s*/?\\\\s*${NAME}\\\\s*\\\\]\\\\]|\\\\{\\\\{\\\\s*${NAME}\\\\s*\\\\}\\\\}`, 'g');\n const found: string[] = [];\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n const token = m[0].replace(/\\s+/g, '');\n if (!found.includes(token)) found.push(token);\n }\n return found;\n}\n\ninterface Span {\n start: number;\n end: number;\n}\n\n/**\n * The nearest enclosing block element of [start, end), or null when the marker\n * isn't inside one. The closing tag is the first of the same name after the\n * marker — Word doesn't nest <p>, and where that guess is wrong the emptiness\n * test simply fails and the caller falls back to the marker's own span.\n */\nfunction enclosingBlock(html: string, start: number, end: number, tags: string[] = BLOCK_TAGS): {\n block: Span;\n contentStart: number;\n contentEnd: number;\n} | null {\n let best: { idx: number; contentStart: number; tag: string } | null = null;\n for (const tag of tags) {\n const openRe = new RegExp(`<${tag}(?:\\\\s[^>]*)?>`, 'gi');\n let m: RegExpExecArray | null;\n while ((m = openRe.exec(html)) !== null) {\n if (m.index >= start) break;\n if (!best || m.index > best.idx) {\n best = {idx: m.index, contentStart: m.index + m[0].length, tag};\n }\n }\n }\n if (!best) return null;\n const closeRe = new RegExp(`</${best.tag}\\\\s*>`, 'gi');\n closeRe.lastIndex = end;\n const close = closeRe.exec(html);\n if (!close) return null;\n return {\n block: {start: best.idx, end: close.index + close[0].length},\n contentStart: best.contentStart,\n contentEnd: close.index,\n };\n}\n\n/**\n * What to delete for a marker: its whole paragraph when the marker is alone in\n * it (removing only the text would leave a blank line), else just the marker.\n */\nfunction markerSpan(html: string, start: number, end: number): Span {\n const found = enclosingBlock(html, start, end);\n if (!found || found.contentStart > start || found.contentEnd < end) return {start, end};\n const rest = html.slice(found.contentStart, start) + html.slice(end, found.contentEnd);\n return stripToText(rest).trim() === '' ? found.block : {start, end};\n}\n\nfunction locate(html: string, token: string): Span | null {\n const m = markerRegex(token).exec(html);\n return m ? {start: m.index, end: m.index + m[0].length} : null;\n}\n\n// ── Single-token fill ───────────────────────────────────────────────────\n//\n// The section machinery above is for templates holding several variants of\n// one email. These are for the simpler case: fixed wording with details typed\n// elsewhere and dropped into tokens of the caller's own choosing (`{Name}`,\n// `%NAME%`, …). Same tag-tolerant matching, since these templates are edited in\n// Outlook too.\n\n/** Replace every occurrence of `token` with `value`, which is never rescanned. */\nexport function replaceToken(html: string, token: string, value: string): string {\n let done = '';\n let rest = html;\n for (; ;) {\n const at = locate(rest, token);\n if (!at) return done + rest;\n done += rest.slice(0, at.start) + value;\n rest = rest.slice(at.end);\n }\n}\n\n/**\n * Remove `token` together with the line it sits on — the table row or\n * paragraph, label included. For an optional detail this is the difference\n * between the email not mentioning it and a heading with nothing under it.\n * Falls back to deleting just the token when it isn't inside a line element.\n */\nexport function removeTokenLine(html: string, token: string): string {\n let out = html;\n for (; ;) {\n const at = locate(out, token);\n if (!at) return out;\n const cut = lineSpan(out, at);\n out = out.slice(0, cut.start) + out.slice(cut.end);\n }\n}\n\n/**\n * The line to delete for a token at `at`: its enclosing row or paragraph, or\n * the token's own span when it isn't in one.\n *\n * `enclosingBlock` returns the *nearest* opening tag before the token, which is\n * only the enclosing one if it hasn't been closed already — a token in a\n * paragraph after a table has a `<tr>` before it and a `</tr>` after it without\n * being inside a row, and taking that span would delete the table.\n */\nfunction lineSpan(html: string, at: Span): Span {\n for (const tag of LINE_TAGS) {\n const found = enclosingBlock(html, at.start, at.end, [tag]);\n if (!found) continue;\n const closedBefore = new RegExp(`</${tag}\\\\s*>`, 'i').test(html.slice(found.contentStart, at.start));\n if (!closedBefore) return found.block;\n }\n return at;\n}\n\n/** Which of `tokens` the template actually uses — what the caller has to supply. */\nexport function findTokens(html: string, tokens: readonly string[]): string[] {\n const text = stripToText(html || '');\n return tokens.filter(token => markerRegex(token).test(text));\n}\n\n/**\n * `{Token}`-shaped text still in a filled-in body: a placeholder misspelled\n * while editing, or one nothing supplied. Reads visible text only, so the\n * braces of a <style> block are not mistaken for placeholders. Catches a\n * leftover `{{TOKEN}}` too, reported by its inner `{TOKEN}`.\n */\nexport function findUnfilledTokens(html: string): string[] {\n const text = stripToText((html || '').replace(/<(style|script)\\b[^>]*>[\\s\\S]*?<\\/\\1>/gi, ''));\n const found: string[] = [];\n const re = /\\{\\s*([A-Za-z][A-Za-z0-9 _-]{0,30})\\s*\\}/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n const token = `{${m[1].trim()}}`;\n if (!found.includes(token)) found.push(token);\n }\n return found;\n}\n\nexport interface ComposeOptions {\n /** Section to keep; the other sections are dropped with their markers. */\n section?: string;\n /** `{{NAME}}` → HTML to substitute. Names match case-insensitively. */\n placeholders?: Readonly<Record<string, string>>;\n /** The template's name, for error messages. */\n label?: string;\n}\n\n/**\n * Reduce a template body to the one email that goes out: keep `section`, drop\n * the rest, fill the placeholders. Throws INVALID_REQUEST rather than guessing —\n * a template edited into an unusable state has to stop the send, not deliver a\n * body with `[[ACCEPT]]` or an unfilled placeholder in it.\n */\nexport function composeTemplateBody(html: string, options: ComposeOptions = {}): string {\n const label = options.label ? `Template '${options.label}'` : 'The template';\n const wanted = (options.section || '').trim().toUpperCase();\n const {sections, placeholders: available} = findTemplateMarkers(html);\n\n if (wanted && !sections.includes(wanted)) {\n throw new InvalidRequestError(\n sections.length\n ? `${label} has no [[${wanted}]] section. Sections found: ${sections.join(', ')}.`\n : `${label} has no [[${wanted}]]...[[/${wanted}]] markers — it holds a single body, so name no section (or add the markers to the template).`,\n );\n }\n if (!wanted && sections.length) {\n throw new InvalidRequestError(\n `${label} is split into sections (${sections.join(', ')}); name the one to send, `\n + 'otherwise the email would go out with every variant and the markers in it.',\n );\n }\n\n // Deletions are collected first and applied right to left, so each span's\n // offsets still refer to the string they were measured against.\n const cuts: Span[] = [];\n for (const name of sections) {\n const open = locate(html, `[[${name}]]`);\n const close = locate(html, `[[/${name}]]`);\n if (!open || !close || close.start < open.end) {\n throw new InvalidRequestError(`${label} has a malformed [[${name}]] section — expected [[${name}]] ... [[/${name}]] in that order.`);\n }\n const openSpan = markerSpan(html, open.start, open.end);\n const closeSpan = markerSpan(html, close.start, close.end);\n if (name === wanted) {\n cuts.push(openSpan, closeSpan); // keep the content, shed the markers\n } else {\n cuts.push({start: openSpan.start, end: closeSpan.end});\n }\n }\n let out = html;\n for (const cut of cuts.sort((a, b) => b.start - a.start)) {\n out = out.slice(0, cut.start) + out.slice(cut.end);\n }\n\n for (const [rawKey, value] of Object.entries(options.placeholders || {})) {\n const key = rawKey.trim().toUpperCase();\n if (!available.includes(key)) {\n throw new InvalidRequestError(\n `${label} has no {{${key}}} placeholder`\n + (available.length ? ` (found: ${available.map(p => `{{${p}}}`).join(', ')}).` : '.')\n + ' It may have been removed when the template was last edited.',\n );\n }\n const m = markerRegex(`{{${key}}}`).exec(out);\n // In the template but gone from `out` means it lived in a dropped\n // section: the caller supplying more than one variant needs, not an error.\n if (m) out = out.slice(0, m.index) + (value ?? '') + out.slice(m.index + m[0].length);\n }\n\n const leftover = markerTokens(out);\n if (leftover.length) {\n const hint = leftover.every(t => t.startsWith('{{'))\n ? 'Supply a value for each placeholder.'\n : 'Check the template — every [[SECTION]] needs its [[/SECTION]].';\n throw new InvalidRequestError(\n `${label} still contains unresolved markers after composing: ${leftover.join(', ')}. ${hint} Nothing was created.`,\n );\n }\n return out;\n}\n"]}
|