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