@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.
Files changed (142) hide show
  1. package/README.md +152 -183
  2. package/dist/backend.d.ts +151 -0
  3. package/dist/backend.js +3 -0
  4. package/dist/backend.js.map +1 -0
  5. package/dist/bridge.d.ts +56 -0
  6. package/dist/bridge.js +507 -0
  7. package/dist/bridge.js.map +1 -0
  8. package/dist/errors.d.ts +79 -43
  9. package/dist/errors.js +93 -52
  10. package/dist/errors.js.map +1 -1
  11. package/dist/index.d.ts +10 -10
  12. package/dist/index.js +25 -52
  13. package/dist/index.js.map +1 -1
  14. package/dist/mac/accounts.d.ts +7 -2
  15. package/dist/mac/accounts.js +23 -6
  16. package/dist/mac/accounts.js.map +1 -1
  17. package/dist/mac/attachments.d.ts +5 -25
  18. package/dist/mac/attachments.js +40 -99
  19. package/dist/mac/attachments.js.map +1 -1
  20. package/dist/mac/bounces.d.ts +12 -27
  21. package/dist/mac/bounces.js +96 -117
  22. package/dist/mac/bounces.js.map +1 -1
  23. package/dist/mac/cleanup.d.ts +11 -25
  24. package/dist/mac/cleanup.js +44 -107
  25. package/dist/mac/cleanup.js.map +1 -1
  26. package/dist/mac/drafts.d.ts +15 -34
  27. package/dist/mac/drafts.js +113 -186
  28. package/dist/mac/drafts.js.map +1 -1
  29. package/dist/mac/folders.d.ts +12 -21
  30. package/dist/mac/folders.js +44 -59
  31. package/dist/mac/folders.js.map +1 -1
  32. package/dist/mac/index.d.ts +2 -22
  33. package/dist/mac/index.js +19 -81
  34. package/dist/mac/index.js.map +1 -1
  35. package/dist/mac/profile.d.ts +13 -0
  36. package/dist/mac/profile.js +149 -0
  37. package/dist/mac/profile.js.map +1 -0
  38. package/dist/mac/read.d.ts +21 -37
  39. package/dist/mac/read.js +198 -216
  40. package/dist/mac/read.js.map +1 -1
  41. package/dist/mac/run.d.ts +51 -38
  42. package/dist/mac/run.js +138 -103
  43. package/dist/mac/run.js.map +1 -1
  44. package/dist/mac/scripts.d.ts +90 -91
  45. package/dist/mac/scripts.js +167 -126
  46. package/dist/mac/scripts.js.map +1 -1
  47. package/dist/mac/send.d.ts +10 -20
  48. package/dist/mac/send.js +99 -89
  49. package/dist/mac/send.js.map +1 -1
  50. package/dist/mac/signatures.d.ts +2 -2
  51. package/dist/mac/signatures.js +9 -11
  52. package/dist/mac/signatures.js.map +1 -1
  53. package/dist/mac/templates.d.ts +14 -26
  54. package/dist/mac/templates.js +79 -120
  55. package/dist/mac/templates.js.map +1 -1
  56. package/dist/mail.d.ts +80 -46
  57. package/dist/mail.js +179 -73
  58. package/dist/mail.js.map +1 -1
  59. package/dist/runtime.d.ts +76 -43
  60. package/dist/runtime.js +103 -48
  61. package/dist/runtime.js.map +1 -1
  62. package/dist/shared/args.d.ts +48 -0
  63. package/dist/shared/args.js +216 -0
  64. package/dist/shared/args.js.map +1 -0
  65. package/dist/shared/attachmentMatch.d.ts +26 -20
  66. package/dist/shared/attachmentMatch.js +60 -23
  67. package/dist/shared/attachmentMatch.js.map +1 -1
  68. package/dist/shared/bounceReport.d.ts +11 -0
  69. package/dist/shared/bounceReport.js +45 -0
  70. package/dist/shared/bounceReport.js.map +1 -0
  71. package/dist/shared/bounceRules.d.ts +38 -10
  72. package/dist/shared/bounceRules.js +50 -32
  73. package/dist/shared/bounceRules.js.map +1 -1
  74. package/dist/shared/exec.d.ts +21 -0
  75. package/dist/shared/exec.js +47 -0
  76. package/dist/shared/exec.js.map +1 -0
  77. package/dist/shared/json.d.ts +17 -6
  78. package/dist/shared/json.js +35 -24
  79. package/dist/shared/json.js.map +1 -1
  80. package/dist/shared/locate.d.ts +31 -0
  81. package/dist/shared/locate.js +128 -0
  82. package/dist/shared/locate.js.map +1 -0
  83. package/dist/shared/replyBody.d.ts +31 -36
  84. package/dist/shared/replyBody.js +83 -72
  85. package/dist/shared/replyBody.js.map +1 -1
  86. package/dist/shared/templateLimits.d.ts +14 -0
  87. package/dist/shared/templateLimits.js +21 -0
  88. package/dist/shared/templateLimits.js.map +1 -0
  89. package/dist/templateBody.d.ts +69 -0
  90. package/dist/{outlookTemplateSections.js → templateBody.js} +80 -73
  91. package/dist/templateBody.js.map +1 -0
  92. package/dist/types.d.ts +381 -168
  93. package/dist/types.js +8 -9
  94. package/dist/types.js.map +1 -1
  95. package/dist/windows/accounts.d.ts +7 -12
  96. package/dist/windows/accounts.js +19 -34
  97. package/dist/windows/accounts.js.map +1 -1
  98. package/dist/windows/attachments.d.ts +9 -24
  99. package/dist/windows/attachments.js +94 -119
  100. package/dist/windows/attachments.js.map +1 -1
  101. package/dist/windows/bounces.d.ts +13 -26
  102. package/dist/windows/bounces.js +168 -200
  103. package/dist/windows/bounces.js.map +1 -1
  104. package/dist/windows/cleanup.d.ts +23 -23
  105. package/dist/windows/cleanup.js +99 -113
  106. package/dist/windows/cleanup.js.map +1 -1
  107. package/dist/windows/drafts.d.ts +16 -35
  108. package/dist/windows/drafts.js +156 -253
  109. package/dist/windows/drafts.js.map +1 -1
  110. package/dist/windows/folders.d.ts +8 -13
  111. package/dist/windows/folders.js +44 -52
  112. package/dist/windows/folders.js.map +1 -1
  113. package/dist/windows/index.d.ts +2 -27
  114. package/dist/windows/index.js +11 -66
  115. package/dist/windows/index.js.map +1 -1
  116. package/dist/windows/read.d.ts +24 -60
  117. package/dist/windows/read.js +265 -306
  118. package/dist/windows/read.js.map +1 -1
  119. package/dist/windows/run.d.ts +38 -40
  120. package/dist/windows/run.js +151 -112
  121. package/dist/windows/run.js.map +1 -1
  122. package/dist/windows/scripts.d.ts +65 -55
  123. package/dist/windows/scripts.js +109 -85
  124. package/dist/windows/scripts.js.map +1 -1
  125. package/dist/windows/send.d.ts +9 -9
  126. package/dist/windows/send.js +76 -88
  127. package/dist/windows/send.js.map +1 -1
  128. package/dist/windows/signatures.d.ts +4 -10
  129. package/dist/windows/signatures.js +30 -37
  130. package/dist/windows/signatures.js.map +1 -1
  131. package/dist/windows/templates.d.ts +12 -18
  132. package/dist/windows/templates.js +117 -150
  133. package/dist/windows/templates.js.map +1 -1
  134. package/package.json +5 -4
  135. package/dist/OutlookService.d.ts +0 -43
  136. package/dist/OutlookService.js +0 -137
  137. package/dist/OutlookService.js.map +0 -1
  138. package/dist/outlookTemplateSections.d.ts +0 -62
  139. package/dist/outlookTemplateSections.js.map +0 -1
  140. package/dist/shared/capabilities.d.ts +0 -3
  141. package/dist/shared/capabilities.js +0 -9
  142. 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
- * Section + placeholder composition for the operator's Outlook template emails.
3
+ * Sections and placeholders in HTML email templates.
4
4
  *
5
- * A template email is a normal saved mail the operator edits in Outlook, so the
6
- * greeting, closing and signature are theirs. Where one template serves several
7
- * reply types, the variants live inside it between markers:
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
- * [[QUOTE]] Thank you for your quote... [[/QUOTE]]
11
- * [[NOQUOTE]] Thank you for letting us know... [[/NOQUOTE]]
12
- * [[CLARIFY]] {{QUESTIONS}} [[/CLARIFY]]
13
- * Best regards, <signature>
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 any
16
- * {{PLACEHOLDER}} — so the shared wording is written once and the caller only
17
- * names a section instead of carrying (and editing) 40-50k of Word HTML.
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 here works on the raw HTML string rather than a parsed tree: Word's
20
- * markup carries conditional comments and namespaced tags that a round-trip
21
- * through a parser would rewrite, and the reply body must stay byte-identical to
22
- * what the operator wrote.
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/placeholder names: what an operator can reasonably type in Outlook. */
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 runs
36
- * across <span>s freely), comments, non-breaking spaces, and the newlines it wraps
37
- * long lines with — sometimes mid-word. So `[[QUOTE]]` is matched character by
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]*?-->|<[^>]*>|&nbsp;|&#160;|\\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 placeholder whose
45
- * empty value takes its whole line out (see `removeTokenLine`). Ordered
46
- * outermost-first: Word puts a <p> inside every table cell, so a token in a
47
- * details table has to drop the <tr> — dropping the <p> would leave the row's
48
- * label cell sitting beside a blank one.
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 sitting directly in one would take the whole email
52
- * with it. Falling back to deleting just the token is the safer miss.
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 marker, e.g. `[[/QUOTE]]` or `{{QUESTIONS}}`.
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 15-character
62
- * token compiles to several hundred characters, and the replace loops ask for the
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 to test a block for emptiness. */
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(/&nbsp;|&#160;/gi, ' ');
80
81
  }
81
- /** Find the markers in a template body (HTML or the plain-text body Outlook derives). */
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
- * Every `[[...]]` / `{{...}}` token still in a body, matched pairs or not — the
110
- * last check before a composed reply is handed to Outlook, so a marker the
111
- * operator half-deleted stops the run instead of going out to a carrier.
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(`\\[\\[\\s*/?\\s*${NAME}\\s*\\]\\]|\\{\\{\\s*${NAME}\\s*\\}\\}`, 'g');
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 the guess is wrong the emptiness
129
- * test below simply fails and the caller falls back to the marker's own span.
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: the whole paragraph when the marker is alone in it
159
- * (removing only the text would leave a blank line in the reply), otherwise just
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 below is for templates that hold several variants of one
176
- // reply. These three are for the simpler case: a template whose wording is fixed
177
- // and whose details — a lane, a cargo weight — are typed somewhere else and dropped
178
- // into `{Token}` placeholders. Same tag-tolerant matching, since the operator
179
- // edits these templates in Outlook too.
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` along with the line it sits on — the table row or paragraph,
194
- * label included. For an optional detail this is the difference between the email
195
- * not mentioning it at all and it carrying a heading with nothing under 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 the
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 already been closed — a token in a
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
- * ever being inside a row, and taking that span would delete the table.
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 in
235
- * Outlook, or one the app has no field for. Reads the visible text only, so the
236
- * braces in a Word <style> block are not mistaken for placeholders.
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
- * Resolve a template body down to the one reply that goes out: keep `section`,
252
- * drop the rest, fill the placeholders. Throws rather than guessing — a template
253
- * the operator edited into an unusable state must stop the run, not quietly send
254
- * a reply with `[[QUOTE]]` or an empty question list in it.
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 drop templateSection (or add the markers in Outlook).`);
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(', ')}) — name one with templateSection, `
267
- + `otherwise the reply would go out with the markers and every variant in it.`);
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-to-left, so each span's
270
- // indices still refer to the string it was measured against.
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
- + ` It may have been edited out in Outlook — ask the user rather than guessing where the text goes.`);
303
+ + ' It may have been removed when the template was last edited.');
297
304
  }
298
305
  const m = markerRegex(`{{${key}}}`).exec(out);
299
- // Absent from `out` but present in the template = it lived in a dropped
300
- // section, which is the caller over-supplying, not an error.
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
- ? `Pass a value for each in templatePlaceholders.`
308
- : `Check the template in Outlook — every [[SECTION]] needs its [[/SECTION]].`;
309
- throw new errors_1.InvalidRequestError(`${label} still contains unresolved markers after composing: ${leftover.join(', ')}. ${hint} The reply was not created.`);
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=outlookTemplateSections.js.map
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]*?-->|<[^>]*>|&nbsp;|&#160;|\\\\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(/&nbsp;|&#160;/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"]}