@dbx-tools/email 0.3.43 → 0.4.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/lib/index.d.ts +26 -0
- package/lib/index.js +24 -0
- package/lib/src/brand.d.ts +47 -0
- package/lib/src/brand.js +44 -0
- package/lib/src/config.d.ts +143 -0
- package/lib/src/config.js +212 -0
- package/lib/src/defaults.d.ts +60 -0
- package/lib/src/defaults.js +64 -0
- package/lib/src/email-html.d.ts +51 -0
- package/lib/src/email-html.js +146 -0
- package/lib/src/markdown.d.ts +22 -0
- package/lib/src/markdown.js +81 -0
- package/lib/src/outbox.d.ts +22 -0
- package/lib/src/outbox.js +60 -0
- package/lib/src/plugin.d.ts +165 -0
- package/lib/src/plugin.js +294 -0
- package/lib/src/sender.d.ts +71 -0
- package/lib/src/sender.js +152 -0
- package/lib/src/tool.d.ts +68 -0
- package/lib/src/tool.js +82 -0
- package/lib/src/transport.d.ts +103 -0
- package/lib/src/transport.js +303 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +11 -7
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interceptor defaults and hard payload caps for the email plugin.
|
|
3
|
+
*
|
|
4
|
+
* The execution settings are what the runtime's executor hands
|
|
5
|
+
* `Plugin.execute()`, kept here rather than at the call sites so the
|
|
6
|
+
* caching / retry / timeout posture of every outbound operation is
|
|
7
|
+
* reviewable in one place. The caps bound the one unbounded input the
|
|
8
|
+
* plugin accepts: a model-drafted message with attachments.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/** Ceiling on how long a single SMTP conversation may take. */
|
|
13
|
+
export const SEND_TIMEOUT_MS = 30_000;
|
|
14
|
+
/** Ceiling on the SMTP handshake performed at plugin setup. */
|
|
15
|
+
export const VERIFY_TIMEOUT_MS = 15_000;
|
|
16
|
+
/** Attempts allowed for the setup-time SMTP handshake, including the first. */
|
|
17
|
+
export const VERIFY_ATTEMPTS = 3;
|
|
18
|
+
/** Largest single attachment accepted, in decoded bytes (10 MiB). */
|
|
19
|
+
export const MAX_ATTACHMENT_BYTES = 10_485_760;
|
|
20
|
+
/** Largest combined attachment payload accepted, in decoded bytes (20 MiB). */
|
|
21
|
+
export const MAX_ATTACHMENTS_TOTAL_BYTES = 20_971_520;
|
|
22
|
+
/** Largest number of attachments accepted on one message. */
|
|
23
|
+
export const MAX_ATTACHMENT_COUNT = 20;
|
|
24
|
+
/** Largest markdown body accepted, in characters. */
|
|
25
|
+
export const MAX_BODY_CHARS = 200_000;
|
|
26
|
+
/** Execution settings for a send (SMTP dispatch or an outbox write). */
|
|
27
|
+
export const EMAIL_SEND_SETTINGS = {
|
|
28
|
+
default: {
|
|
29
|
+
// Cache disabled: a send is a side effect, not a value. Replaying a
|
|
30
|
+
// cached result would report success for a message never handed to SMTP.
|
|
31
|
+
cache: { enabled: false },
|
|
32
|
+
// Retry disabled: SMTP delivery is not idempotent. A `sendMail` that
|
|
33
|
+
// times out may already have queued the message, so a second attempt
|
|
34
|
+
// risks delivering the mail twice.
|
|
35
|
+
retry: { enabled: false },
|
|
36
|
+
timeout: SEND_TIMEOUT_MS,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
/** Execution settings for the setup-time SMTP connectivity check. */
|
|
40
|
+
export const EMAIL_VERIFY_SETTINGS = {
|
|
41
|
+
default: {
|
|
42
|
+
// Cache disabled: connectivity is a point-in-time fact about the server,
|
|
43
|
+
// and this runs once per boot, so there is nothing to reuse.
|
|
44
|
+
cache: { enabled: false },
|
|
45
|
+
// Retry enabled: the handshake has no side effect, and a cold SMTP relay
|
|
46
|
+
// or a slow DNS answer at boot is exactly the transient failure that
|
|
47
|
+
// should not take the app down.
|
|
48
|
+
retry: { enabled: true, attempts: VERIFY_ATTEMPTS },
|
|
49
|
+
timeout: VERIFY_TIMEOUT_MS,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
/** Execution settings for the sender-options lookup. */
|
|
53
|
+
export const EMAIL_SENDERS_SETTINGS = {
|
|
54
|
+
default: {
|
|
55
|
+
// Cache disabled: the options are computed from already-resolved config
|
|
56
|
+
// and the caller's own address, so a cache would add a cross-identity
|
|
57
|
+
// leak risk for no measurable saving.
|
|
58
|
+
cache: { enabled: false },
|
|
59
|
+
// Retry disabled: the lookup performs no I/O, so a failure is
|
|
60
|
+
// deterministic and a second attempt would fail identically.
|
|
61
|
+
retry: { enabled: false },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVmYXVsdHMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvZGVmYXVsdHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7R0FVRztBQXdCSCwrREFBK0Q7QUFDL0QsTUFBTSxDQUFDLE1BQU0sZUFBZSxHQUFHLE1BQU0sQ0FBQztBQUV0QywrREFBK0Q7QUFDL0QsTUFBTSxDQUFDLE1BQU0saUJBQWlCLEdBQUcsTUFBTSxDQUFDO0FBRXhDLCtFQUErRTtBQUMvRSxNQUFNLENBQUMsTUFBTSxlQUFlLEdBQUcsQ0FBQyxDQUFDO0FBRWpDLHFFQUFxRTtBQUNyRSxNQUFNLENBQUMsTUFBTSxvQkFBb0IsR0FBRyxVQUFVLENBQUM7QUFFL0MsK0VBQStFO0FBQy9FLE1BQU0sQ0FBQyxNQUFNLDJCQUEyQixHQUFHLFVBQVUsQ0FBQztBQUV0RCw2REFBNkQ7QUFDN0QsTUFBTSxDQUFDLE1BQU0sb0JBQW9CLEdBQUcsRUFBRSxDQUFDO0FBRXZDLHFEQUFxRDtBQUNyRCxNQUFNLENBQUMsTUFBTSxjQUFjLEdBQUcsT0FBTyxDQUFDO0FBRXRDLHdFQUF3RTtBQUN4RSxNQUFNLENBQUMsTUFBTSxtQkFBbUIsR0FBMkI7SUFDekQsT0FBTyxFQUFFO1FBQ1Asb0VBQW9FO1FBQ3BFLHlFQUF5RTtRQUN6RSxLQUFLLEVBQUUsRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFO1FBQ3pCLHFFQUFxRTtRQUNyRSxxRUFBcUU7UUFDckUsbUNBQW1DO1FBQ25DLEtBQUssRUFBRSxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUU7UUFDekIsT0FBTyxFQUFFLGVBQWU7S0FDekI7Q0FDRixDQUFDO0FBRUYscUVBQXFFO0FBQ3JFLE1BQU0sQ0FBQyxNQUFNLHFCQUFxQixHQUEyQjtJQUMzRCxPQUFPLEVBQUU7UUFDUCx5RUFBeUU7UUFDekUsNkRBQTZEO1FBQzdELEtBQUssRUFBRSxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUU7UUFDekIseUVBQXlFO1FBQ3pFLHFFQUFxRTtRQUNyRSxnQ0FBZ0M7UUFDaEMsS0FBSyxFQUFFLEVBQUUsT0FBTyxFQUFFLElBQUksRUFBRSxRQUFRLEVBQUUsZUFBZSxFQUFFO1FBQ25ELE9BQU8sRUFBRSxpQkFBaUI7S0FDM0I7Q0FDRixDQUFDO0FBRUYsd0RBQXdEO0FBQ3hELE1BQU0sQ0FBQyxNQUFNLHNCQUFzQixHQUEyQjtJQUM1RCxPQUFPLEVBQUU7UUFDUCx3RUFBd0U7UUFDeEUsc0VBQXNFO1FBQ3RFLHNDQUFzQztRQUN0QyxLQUFLLEVBQUUsRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFO1FBQ3pCLDhEQUE4RDtRQUM5RCw2REFBNkQ7UUFDN0QsS0FBSyxFQUFFLEVBQUUsT0FBTyxFQUFFLEtBQUssRUFBRTtLQUMxQjtDQUNGLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIEludGVyY2VwdG9yIGRlZmF1bHRzIGFuZCBoYXJkIHBheWxvYWQgY2FwcyBmb3IgdGhlIGVtYWlsIHBsdWdpbi5cbiAqXG4gKiBUaGUgZXhlY3V0aW9uIHNldHRpbmdzIGFyZSB3aGF0IHRoZSBydW50aW1lJ3MgZXhlY3V0b3IgaGFuZHNcbiAqIGBQbHVnaW4uZXhlY3V0ZSgpYCwga2VwdCBoZXJlIHJhdGhlciB0aGFuIGF0IHRoZSBjYWxsIHNpdGVzIHNvIHRoZVxuICogY2FjaGluZyAvIHJldHJ5IC8gdGltZW91dCBwb3N0dXJlIG9mIGV2ZXJ5IG91dGJvdW5kIG9wZXJhdGlvbiBpc1xuICogcmV2aWV3YWJsZSBpbiBvbmUgcGxhY2UuIFRoZSBjYXBzIGJvdW5kIHRoZSBvbmUgdW5ib3VuZGVkIGlucHV0IHRoZVxuICogcGx1Z2luIGFjY2VwdHM6IGEgbW9kZWwtZHJhZnRlZCBtZXNzYWdlIHdpdGggYXR0YWNobWVudHMuXG4gKlxuICogQG1vZHVsZVxuICovXG5cbi8qKlxuICogVGhlIGBQbHVnaW5FeGVjdXRlQ29uZmlnYCBzbGljZSB0aGlzIHBhY2thZ2Ugc2V0cy4gTWlycm9yZWQgc3RydWN0dXJhbGx5XG4gKiBiZWNhdXNlIEFwcEtpdCdzIGBQbHVnaW5FeGVjdXRlQ29uZmlnYCBsaXZlcyBiZWhpbmQgYSBzdWJwYXRoIGl0cyBgZXhwb3J0c2BcbiAqIG1hcCBkb2VzIG5vdCBwdWJsaXNoLCBzbyB0aGUgbm9taW5hbCB0eXBlIGNhbm5vdCBiZSBpbXBvcnRlZC4gV3JpdHRlbiBhcyBhXG4gKiB0eXBlIGFsaWFzIHJhdGhlciB0aGFuIGFuIGludGVyZmFjZSBzbyBpdCBzdGF5cyBhc3NpZ25hYmxlIHRvIHRoZSBub21pbmFsXG4gKiB0eXBlJ3MgaW5kZXggc2lnbmF0dXJlLlxuICovXG5leHBvcnQgdHlwZSBFbWFpbEV4ZWN1dGVDb25maWcgPSB7XG4gIGNhY2hlPzogeyBlbmFibGVkPzogYm9vbGVhbjsgdHRsPzogbnVtYmVyOyBjYWNoZUtleT86IChzdHJpbmcgfCBudW1iZXIgfCBvYmplY3QpW10gfTtcbiAgcmV0cnk/OiB7IGVuYWJsZWQ/OiBib29sZWFuOyBhdHRlbXB0cz86IG51bWJlcjsgaW5pdGlhbERlbGF5PzogbnVtYmVyOyBtYXhEZWxheT86IG51bWJlciB9O1xuICB0aW1lb3V0PzogbnVtYmVyO1xufTtcblxuLyoqXG4gKiBUaGUgYFBsdWdpbkV4ZWN1dGlvblNldHRpbmdzYCBzaGFwZSBhY2NlcHRlZCBieSBBcHBLaXQncyBgUGx1Z2luLmV4ZWN1dGUoKWAuXG4gKiBNaXJyb3JlZCBzdHJ1Y3R1cmFsbHkgZm9yIHRoZSBzYW1lIHJlYXNvbiBhcyB7QGxpbmsgRW1haWxFeGVjdXRlQ29uZmlnfS5cbiAqL1xuZXhwb3J0IHR5cGUgRW1haWxFeGVjdXRpb25TZXR0aW5ncyA9IHtcbiAgZGVmYXVsdDogRW1haWxFeGVjdXRlQ29uZmlnO1xuICB1c2VyPzogRW1haWxFeGVjdXRlQ29uZmlnO1xufTtcblxuLyoqIENlaWxpbmcgb24gaG93IGxvbmcgYSBzaW5nbGUgU01UUCBjb252ZXJzYXRpb24gbWF5IHRha2UuICovXG5leHBvcnQgY29uc3QgU0VORF9USU1FT1VUX01TID0gMzBfMDAwO1xuXG4vKiogQ2VpbGluZyBvbiB0aGUgU01UUCBoYW5kc2hha2UgcGVyZm9ybWVkIGF0IHBsdWdpbiBzZXR1cC4gKi9cbmV4cG9ydCBjb25zdCBWRVJJRllfVElNRU9VVF9NUyA9IDE1XzAwMDtcblxuLyoqIEF0dGVtcHRzIGFsbG93ZWQgZm9yIHRoZSBzZXR1cC10aW1lIFNNVFAgaGFuZHNoYWtlLCBpbmNsdWRpbmcgdGhlIGZpcnN0LiAqL1xuZXhwb3J0IGNvbnN0IFZFUklGWV9BVFRFTVBUUyA9IDM7XG5cbi8qKiBMYXJnZXN0IHNpbmdsZSBhdHRhY2htZW50IGFjY2VwdGVkLCBpbiBkZWNvZGVkIGJ5dGVzICgxMCBNaUIpLiAqL1xuZXhwb3J0IGNvbnN0IE1BWF9BVFRBQ0hNRU5UX0JZVEVTID0gMTBfNDg1Xzc2MDtcblxuLyoqIExhcmdlc3QgY29tYmluZWQgYXR0YWNobWVudCBwYXlsb2FkIGFjY2VwdGVkLCBpbiBkZWNvZGVkIGJ5dGVzICgyMCBNaUIpLiAqL1xuZXhwb3J0IGNvbnN0IE1BWF9BVFRBQ0hNRU5UU19UT1RBTF9CWVRFUyA9IDIwXzk3MV81MjA7XG5cbi8qKiBMYXJnZXN0IG51bWJlciBvZiBhdHRhY2htZW50cyBhY2NlcHRlZCBvbiBvbmUgbWVzc2FnZS4gKi9cbmV4cG9ydCBjb25zdCBNQVhfQVRUQUNITUVOVF9DT1VOVCA9IDIwO1xuXG4vKiogTGFyZ2VzdCBtYXJrZG93biBib2R5IGFjY2VwdGVkLCBpbiBjaGFyYWN0ZXJzLiAqL1xuZXhwb3J0IGNvbnN0IE1BWF9CT0RZX0NIQVJTID0gMjAwXzAwMDtcblxuLyoqIEV4ZWN1dGlvbiBzZXR0aW5ncyBmb3IgYSBzZW5kIChTTVRQIGRpc3BhdGNoIG9yIGFuIG91dGJveCB3cml0ZSkuICovXG5leHBvcnQgY29uc3QgRU1BSUxfU0VORF9TRVRUSU5HUzogRW1haWxFeGVjdXRpb25TZXR0aW5ncyA9IHtcbiAgZGVmYXVsdDoge1xuICAgIC8vIENhY2hlIGRpc2FibGVkOiBhIHNlbmQgaXMgYSBzaWRlIGVmZmVjdCwgbm90IGEgdmFsdWUuIFJlcGxheWluZyBhXG4gICAgLy8gY2FjaGVkIHJlc3VsdCB3b3VsZCByZXBvcnQgc3VjY2VzcyBmb3IgYSBtZXNzYWdlIG5ldmVyIGhhbmRlZCB0byBTTVRQLlxuICAgIGNhY2hlOiB7IGVuYWJsZWQ6IGZhbHNlIH0sXG4gICAgLy8gUmV0cnkgZGlzYWJsZWQ6IFNNVFAgZGVsaXZlcnkgaXMgbm90IGlkZW1wb3RlbnQuIEEgYHNlbmRNYWlsYCB0aGF0XG4gICAgLy8gdGltZXMgb3V0IG1heSBhbHJlYWR5IGhhdmUgcXVldWVkIHRoZSBtZXNzYWdlLCBzbyBhIHNlY29uZCBhdHRlbXB0XG4gICAgLy8gcmlza3MgZGVsaXZlcmluZyB0aGUgbWFpbCB0d2ljZS5cbiAgICByZXRyeTogeyBlbmFibGVkOiBmYWxzZSB9LFxuICAgIHRpbWVvdXQ6IFNFTkRfVElNRU9VVF9NUyxcbiAgfSxcbn07XG5cbi8qKiBFeGVjdXRpb24gc2V0dGluZ3MgZm9yIHRoZSBzZXR1cC10aW1lIFNNVFAgY29ubmVjdGl2aXR5IGNoZWNrLiAqL1xuZXhwb3J0IGNvbnN0IEVNQUlMX1ZFUklGWV9TRVRUSU5HUzogRW1haWxFeGVjdXRpb25TZXR0aW5ncyA9IHtcbiAgZGVmYXVsdDoge1xuICAgIC8vIENhY2hlIGRpc2FibGVkOiBjb25uZWN0aXZpdHkgaXMgYSBwb2ludC1pbi10aW1lIGZhY3QgYWJvdXQgdGhlIHNlcnZlcixcbiAgICAvLyBhbmQgdGhpcyBydW5zIG9uY2UgcGVyIGJvb3QsIHNvIHRoZXJlIGlzIG5vdGhpbmcgdG8gcmV1c2UuXG4gICAgY2FjaGU6IHsgZW5hYmxlZDogZmFsc2UgfSxcbiAgICAvLyBSZXRyeSBlbmFibGVkOiB0aGUgaGFuZHNoYWtlIGhhcyBubyBzaWRlIGVmZmVjdCwgYW5kIGEgY29sZCBTTVRQIHJlbGF5XG4gICAgLy8gb3IgYSBzbG93IEROUyBhbnN3ZXIgYXQgYm9vdCBpcyBleGFjdGx5IHRoZSB0cmFuc2llbnQgZmFpbHVyZSB0aGF0XG4gICAgLy8gc2hvdWxkIG5vdCB0YWtlIHRoZSBhcHAgZG93bi5cbiAgICByZXRyeTogeyBlbmFibGVkOiB0cnVlLCBhdHRlbXB0czogVkVSSUZZX0FUVEVNUFRTIH0sXG4gICAgdGltZW91dDogVkVSSUZZX1RJTUVPVVRfTVMsXG4gIH0sXG59O1xuXG4vKiogRXhlY3V0aW9uIHNldHRpbmdzIGZvciB0aGUgc2VuZGVyLW9wdGlvbnMgbG9va3VwLiAqL1xuZXhwb3J0IGNvbnN0IEVNQUlMX1NFTkRFUlNfU0VUVElOR1M6IEVtYWlsRXhlY3V0aW9uU2V0dGluZ3MgPSB7XG4gIGRlZmF1bHQ6IHtcbiAgICAvLyBDYWNoZSBkaXNhYmxlZDogdGhlIG9wdGlvbnMgYXJlIGNvbXB1dGVkIGZyb20gYWxyZWFkeS1yZXNvbHZlZCBjb25maWdcbiAgICAvLyBhbmQgdGhlIGNhbGxlcidzIG93biBhZGRyZXNzLCBzbyBhIGNhY2hlIHdvdWxkIGFkZCBhIGNyb3NzLWlkZW50aXR5XG4gICAgLy8gbGVhayByaXNrIGZvciBubyBtZWFzdXJhYmxlIHNhdmluZy5cbiAgICBjYWNoZTogeyBlbmFibGVkOiBmYWxzZSB9LFxuICAgIC8vIFJldHJ5IGRpc2FibGVkOiB0aGUgbG9va3VwIHBlcmZvcm1zIG5vIEkvTywgc28gYSBmYWlsdXJlIGlzXG4gICAgLy8gZGV0ZXJtaW5pc3RpYyBhbmQgYSBzZWNvbmQgYXR0ZW1wdCB3b3VsZCBmYWlsIGlkZW50aWNhbGx5LlxuICAgIHJldHJ5OiB7IGVuYWJsZWQ6IGZhbHNlIH0sXG4gIH0sXG59O1xuIl19
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email HTML assembly: render a markdown body into a branded, responsive
|
|
3
|
+
* email layout and inline the stylesheet with `juice`.
|
|
4
|
+
*
|
|
5
|
+
* The layout is the classic email-safe pattern (a centered 600px
|
|
6
|
+
* table-based container with a header band, content card, and optional
|
|
7
|
+
* footer) - the same shape MJML emits, hand-built here because MJML's
|
|
8
|
+
* toolchain pulls fast-moving browser-data deps (caniuse-lite,
|
|
9
|
+
* baseline-browser-mapping) that are awkward to install behind a
|
|
10
|
+
* locked-down registry. Inlining matters because real clients (Gmail,
|
|
11
|
+
* Outlook) strip `<style>` blocks and ignore class selectors; the same
|
|
12
|
+
* renderer feeds both the local outbox preview and the SMTP HTML part,
|
|
13
|
+
* so a browser and an inbox show the same thing.
|
|
14
|
+
*
|
|
15
|
+
* Brand styling (accent color, font, header logo) is optional: pass an
|
|
16
|
+
* {@link EmailBrand} to color the layout, or omit it for the neutral
|
|
17
|
+
* default. Branding is inlined here because the browser UI's `[data-brand]`
|
|
18
|
+
* CSS bridge can't reach an inbox (see `./brand`).
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import { string } from "@dbx-tools/shared-core";
|
|
23
|
+
import type { EmailBrand } from "./brand.js";
|
|
24
|
+
/** Escape HTML-significant characters (re-exported from `@dbx-tools/shared-core`). */
|
|
25
|
+
export declare const escapeHtml: typeof string.escapeHtml;
|
|
26
|
+
/** Options for {@link renderEmailHtml}. */
|
|
27
|
+
export interface EmailHtmlOptions {
|
|
28
|
+
/** Markdown body. Rendered to HTML, then wrapped in the layout. */
|
|
29
|
+
body: string;
|
|
30
|
+
/** Header-band title and document `<title>`. Defaults to "Message". */
|
|
31
|
+
subject?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Optional `[label, value]` envelope rows shown above the body (used
|
|
34
|
+
* by the outbox preview; omitted for SMTP sends, where the mail client
|
|
35
|
+
* shows the envelope itself).
|
|
36
|
+
*/
|
|
37
|
+
headers?: ReadonlyArray<readonly [string, string]>;
|
|
38
|
+
/** Optional small-print footer line. Omitted when unset. */
|
|
39
|
+
footer?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Optional brand styling for the layout (accent, font, header logo).
|
|
42
|
+
* Omit for the neutral default palette.
|
|
43
|
+
*/
|
|
44
|
+
brand?: EmailBrand;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Render `body` (markdown) into a complete, style-inlined email document
|
|
48
|
+
* using the responsive layout. When `opts.brand` is set its accent, font,
|
|
49
|
+
* and logo style the layout; otherwise a neutral default palette is used.
|
|
50
|
+
*/
|
|
51
|
+
export declare function renderEmailHtml(opts: EmailHtmlOptions): string;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email HTML assembly: render a markdown body into a branded, responsive
|
|
3
|
+
* email layout and inline the stylesheet with `juice`.
|
|
4
|
+
*
|
|
5
|
+
* The layout is the classic email-safe pattern (a centered 600px
|
|
6
|
+
* table-based container with a header band, content card, and optional
|
|
7
|
+
* footer) - the same shape MJML emits, hand-built here because MJML's
|
|
8
|
+
* toolchain pulls fast-moving browser-data deps (caniuse-lite,
|
|
9
|
+
* baseline-browser-mapping) that are awkward to install behind a
|
|
10
|
+
* locked-down registry. Inlining matters because real clients (Gmail,
|
|
11
|
+
* Outlook) strip `<style>` blocks and ignore class selectors; the same
|
|
12
|
+
* renderer feeds both the local outbox preview and the SMTP HTML part,
|
|
13
|
+
* so a browser and an inbox show the same thing.
|
|
14
|
+
*
|
|
15
|
+
* Brand styling (accent color, font, header logo) is optional: pass an
|
|
16
|
+
* {@link EmailBrand} to color the layout, or omit it for the neutral
|
|
17
|
+
* default. Branding is inlined here because the browser UI's `[data-brand]`
|
|
18
|
+
* CSS bridge can't reach an inbox (see `./brand`).
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import { string } from "@dbx-tools/shared-core";
|
|
23
|
+
import juice from "juice";
|
|
24
|
+
import { markdownToHtml } from "./markdown.js";
|
|
25
|
+
/**
|
|
26
|
+
* Rendered height of the header logo. Fixed rather than intrinsic because
|
|
27
|
+
* mail clients ignore CSS sizing on an image without an `height` attribute.
|
|
28
|
+
*/
|
|
29
|
+
const LOGO_HEIGHT_PX = 28;
|
|
30
|
+
/** Neutral fallback styling when no brand is supplied. */
|
|
31
|
+
const DEFAULT_BRAND = {
|
|
32
|
+
accent: "#0b6bcb",
|
|
33
|
+
onAccent: "#ffffff",
|
|
34
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif",
|
|
35
|
+
};
|
|
36
|
+
/** Escape HTML-significant characters (re-exported from `@dbx-tools/shared-core`). */
|
|
37
|
+
export const escapeHtml = string.escapeHtml;
|
|
38
|
+
/**
|
|
39
|
+
* Content stylesheet inlined onto the markdown body (juice maps these
|
|
40
|
+
* onto elements). Outer layout styling is written inline directly so it
|
|
41
|
+
* survives even if inlining is skipped; the `@media` rule is preserved
|
|
42
|
+
* by juice for clients that honor it. Parameterized by the resolved accent
|
|
43
|
+
* so body links match the brand.
|
|
44
|
+
*/
|
|
45
|
+
function contentCss(accent) {
|
|
46
|
+
return `
|
|
47
|
+
.email-body { font-size: 15px; line-height: 1.55; color: #1a1a1a; }
|
|
48
|
+
.email-body p { margin: 0 0 1rem; }
|
|
49
|
+
.email-body a { color: ${accent}; }
|
|
50
|
+
.email-body h1, .email-body h2, .email-body h3 { margin: 1.4rem 0 0.6rem; line-height: 1.25; }
|
|
51
|
+
.email-body ul, .email-body ol { margin: 0 0 1rem; padding-left: 1.4rem; }
|
|
52
|
+
.email-body table { border-collapse: collapse; margin: 1rem 0; width: 100%; font-size: 14px; }
|
|
53
|
+
.email-body th, .email-body td { border: 1px solid #d0d7de; padding: 6px 10px; text-align: left; }
|
|
54
|
+
.email-body th { background: #f6f8fa; font-weight: 600; }
|
|
55
|
+
.email-body code { background: #f1f3f5; padding: 2px 5px; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; }
|
|
56
|
+
.email-body pre { background: #f1f3f5; padding: 12px; border-radius: 6px; overflow-x: auto; }
|
|
57
|
+
.email-body pre code { background: none; padding: 0; }
|
|
58
|
+
.email-body blockquote { margin: 1rem 0; padding: 0 1rem; color: #57606a; border-left: 3px solid #d0d7de; }
|
|
59
|
+
.email-body img { max-width: 100%; height: auto; }
|
|
60
|
+
.meta { border-collapse: collapse; font-size: 13px; margin-bottom: 4px; }
|
|
61
|
+
.meta th { text-align: left; padding: 2px 12px 2px 0; vertical-align: top; color: #6b7280; font-weight: 600; white-space: nowrap; }
|
|
62
|
+
.meta td { padding: 2px 0; color: #374151; }
|
|
63
|
+
@media only screen and (max-width: 620px) {
|
|
64
|
+
.container { width: 100% !important; }
|
|
65
|
+
.gutter { padding-left: 20px !important; padding-right: 20px !important; }
|
|
66
|
+
}`;
|
|
67
|
+
}
|
|
68
|
+
/** Render the optional envelope-header table block. */
|
|
69
|
+
function metaBlock(headers) {
|
|
70
|
+
if (!headers || headers.length === 0)
|
|
71
|
+
return "";
|
|
72
|
+
const rows = headers
|
|
73
|
+
.map(([label, value]) => `<tr><th>${escapeHtml(label)}</th><td>${escapeHtml(value)}</td></tr>`)
|
|
74
|
+
.join("");
|
|
75
|
+
return `<table role="presentation" class="meta"><tbody>${rows}</tbody></table>`;
|
|
76
|
+
}
|
|
77
|
+
/** Render the optional footer row. */
|
|
78
|
+
function footerRow(footer) {
|
|
79
|
+
if (!footer)
|
|
80
|
+
return "";
|
|
81
|
+
return `
|
|
82
|
+
<tr>
|
|
83
|
+
<td class="gutter" style="padding: 16px 32px; border-top: 1px solid #eaecef; color: #9aa0a6; font-size: 12px; line-height: 1.5;">
|
|
84
|
+
${escapeHtml(footer)}
|
|
85
|
+
</td>
|
|
86
|
+
</tr>`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Render the header-band content: the brand logo (when the brand supplies a
|
|
90
|
+
* renderable image) above the title, or just the title. The logo is capped
|
|
91
|
+
* at {@link LOGO_HEIGHT_PX} and tinted implicitly by its own artwork; the
|
|
92
|
+
* title always shows so the band is never empty.
|
|
93
|
+
*/
|
|
94
|
+
function headerBand(title, brand, onAccent) {
|
|
95
|
+
const logo = brand.logoUrl
|
|
96
|
+
? `<img src="${escapeHtml(brand.logoUrl)}" alt="${escapeHtml(brand.name ?? title)}" height="${LOGO_HEIGHT_PX}" style="height: ${LOGO_HEIGHT_PX}px; width: auto; display: block; margin-bottom: 8px;" />`
|
|
97
|
+
: "";
|
|
98
|
+
return `${logo}<span style="color: ${onAccent}; font-size: 18px; font-weight: 700; line-height: 1.3;">${escapeHtml(title)}</span>`;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Render `body` (markdown) into a complete, style-inlined email document
|
|
102
|
+
* using the responsive layout. When `opts.brand` is set its accent, font,
|
|
103
|
+
* and logo style the layout; otherwise a neutral default palette is used.
|
|
104
|
+
*/
|
|
105
|
+
export function renderEmailHtml(opts) {
|
|
106
|
+
const title = opts.subject?.trim() || "Message";
|
|
107
|
+
const accent = opts.brand?.accent ?? DEFAULT_BRAND.accent;
|
|
108
|
+
const onAccent = opts.brand?.onAccent ?? DEFAULT_BRAND.onAccent;
|
|
109
|
+
const fontFamily = opts.brand?.fontFamily ?? DEFAULT_BRAND.fontFamily;
|
|
110
|
+
const brand = opts.brand ?? { accent, onAccent, fontFamily };
|
|
111
|
+
const doc = `<!doctype html>
|
|
112
|
+
<html lang="en">
|
|
113
|
+
<head>
|
|
114
|
+
<meta charset="utf-8" />
|
|
115
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
116
|
+
<meta name="color-scheme" content="light only" />
|
|
117
|
+
<title>${escapeHtml(title)}</title>
|
|
118
|
+
<style>${contentCss(accent)}
|
|
119
|
+
</style>
|
|
120
|
+
</head>
|
|
121
|
+
<body style="margin: 0; padding: 0; background-color: #f4f5f7; -webkit-text-size-adjust: 100%;">
|
|
122
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f5f7;">
|
|
123
|
+
<tr>
|
|
124
|
+
<td align="center" style="padding: 24px 12px;">
|
|
125
|
+
<table role="presentation" class="container" width="600" cellpadding="0" cellspacing="0" style="width: 600px; max-width: 100%; background-color: #ffffff; border-radius: 10px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.08); font-family: ${fontFamily};">
|
|
126
|
+
<tr>
|
|
127
|
+
<td class="gutter" style="padding: 20px 32px; background-color: ${accent};">
|
|
128
|
+
${headerBand(title, brand, onAccent)}
|
|
129
|
+
</td>
|
|
130
|
+
</tr>
|
|
131
|
+
<tr>
|
|
132
|
+
<td class="gutter" style="padding: 24px 32px 8px;">
|
|
133
|
+
${metaBlock(opts.headers)}
|
|
134
|
+
<div class="email-body">${markdownToHtml(opts.body)}</div>
|
|
135
|
+
</td>
|
|
136
|
+
</tr>${footerRow(opts.footer)}
|
|
137
|
+
</table>
|
|
138
|
+
</td>
|
|
139
|
+
</tr>
|
|
140
|
+
</table>
|
|
141
|
+
</body>
|
|
142
|
+
</html>
|
|
143
|
+
`;
|
|
144
|
+
return juice(doc);
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZW1haWwtaHRtbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9lbWFpbC1odG1sLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQW9CRztBQUVILE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUNoRCxPQUFPLEtBQUssTUFBTSxPQUFPLENBQUM7QUFFMUIsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLFlBQVksQ0FBQztBQUU1Qzs7O0dBR0c7QUFDSCxNQUFNLGNBQWMsR0FBRyxFQUFFLENBQUM7QUFFMUIsMERBQTBEO0FBQzFELE1BQU0sYUFBYSxHQUFxRTtJQUN0RixNQUFNLEVBQUUsU0FBUztJQUNqQixRQUFRLEVBQUUsU0FBUztJQUNuQixVQUFVLEVBQUUsNkVBQTZFO0NBQzFGLENBQUM7QUFFRixzRkFBc0Y7QUFDdEYsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHLE1BQU0sQ0FBQyxVQUFVLENBQUM7QUFFNUM7Ozs7OztHQU1HO0FBQ0gsU0FBUyxVQUFVLENBQUMsTUFBYztJQUNoQyxPQUFPOzs7NkJBR29CLE1BQU07Ozs7Ozs7Ozs7Ozs7Ozs7O01BaUI3QixDQUFDO0FBQ1AsQ0FBQztBQXVCRCx1REFBdUQ7QUFDdkQsU0FBUyxTQUFTLENBQUMsT0FBb0M7SUFDckQsSUFBSSxDQUFDLE9BQU8sSUFBSSxPQUFPLENBQUMsTUFBTSxLQUFLLENBQUM7UUFBRSxPQUFPLEVBQUUsQ0FBQztJQUNoRCxNQUFNLElBQUksR0FBRyxPQUFPO1NBQ2pCLEdBQUcsQ0FBQyxDQUFDLENBQUMsS0FBSyxFQUFFLEtBQUssQ0FBQyxFQUFFLEVBQUUsQ0FBQyxXQUFXLFVBQVUsQ0FBQyxLQUFLLENBQUMsWUFBWSxVQUFVLENBQUMsS0FBSyxDQUFDLFlBQVksQ0FBQztTQUM5RixJQUFJLENBQUMsRUFBRSxDQUFDLENBQUM7SUFDWixPQUFPLGtEQUFrRCxJQUFJLGtCQUFrQixDQUFDO0FBQ2xGLENBQUM7QUFFRCxzQ0FBc0M7QUFDdEMsU0FBUyxTQUFTLENBQUMsTUFBMEI7SUFDM0MsSUFBSSxDQUFDLE1BQU07UUFBRSxPQUFPLEVBQUUsQ0FBQztJQUN2QixPQUFPOzs7Z0JBR08sVUFBVSxDQUFDLE1BQU0sQ0FBQzs7Z0JBRWxCLENBQUM7QUFDakIsQ0FBQztBQUVEOzs7OztHQUtHO0FBQ0gsU0FBUyxVQUFVLENBQUMsS0FBYSxFQUFFLEtBQWlCLEVBQUUsUUFBZ0I7SUFDcEUsTUFBTSxJQUFJLEdBQUcsS0FBSyxDQUFDLE9BQU87UUFDeEIsQ0FBQyxDQUFDLGFBQWEsVUFBVSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsVUFBVSxVQUFVLENBQUMsS0FBSyxDQUFDLElBQUksSUFBSSxLQUFLLENBQUMsYUFBYSxjQUFjLG9CQUFvQixjQUFjLDBEQUEwRDtRQUN4TSxDQUFDLENBQUMsRUFBRSxDQUFDO0lBQ1AsT0FBTyxHQUFHLElBQUksdUJBQXVCLFFBQVEsMkRBQTJELFVBQVUsQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDO0FBQ3JJLENBQUM7QUFFRDs7OztHQUlHO0FBQ0gsTUFBTSxVQUFVLGVBQWUsQ0FBQyxJQUFzQjtJQUNwRCxNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsT0FBTyxFQUFFLElBQUksRUFBRSxJQUFJLFNBQVMsQ0FBQztJQUNoRCxNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsS0FBSyxFQUFFLE1BQU0sSUFBSSxhQUFhLENBQUMsTUFBTSxDQUFDO0lBQzFELE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxLQUFLLEVBQUUsUUFBUSxJQUFJLGFBQWEsQ0FBQyxRQUFRLENBQUM7SUFDaEUsTUFBTSxVQUFVLEdBQUcsSUFBSSxDQUFDLEtBQUssRUFBRSxVQUFVLElBQUksYUFBYSxDQUFDLFVBQVUsQ0FBQztJQUN0RSxNQUFNLEtBQUssR0FBZSxJQUFJLENBQUMsS0FBSyxJQUFJLEVBQUUsTUFBTSxFQUFFLFFBQVEsRUFBRSxVQUFVLEVBQUUsQ0FBQztJQUN6RSxNQUFNLEdBQUcsR0FBRzs7Ozs7O2FBTUQsVUFBVSxDQUFDLEtBQUssQ0FBQzthQUNqQixVQUFVLENBQUMsTUFBTSxDQUFDOzs7Ozs7O2tRQU9tTyxVQUFVOztnRkFFNUwsTUFBTTtrQkFDcEUsVUFBVSxDQUFDLEtBQUssRUFBRSxLQUFLLEVBQUUsUUFBUSxDQUFDOzs7OztrQkFLbEMsU0FBUyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUM7MENBQ0MsY0FBYyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUM7O21CQUVoRCxTQUFTLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQzs7Ozs7OztDQU94QyxDQUFDO0lBQ0EsT0FBTyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7QUFDcEIsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogRW1haWwgSFRNTCBhc3NlbWJseTogcmVuZGVyIGEgbWFya2Rvd24gYm9keSBpbnRvIGEgYnJhbmRlZCwgcmVzcG9uc2l2ZVxuICogZW1haWwgbGF5b3V0IGFuZCBpbmxpbmUgdGhlIHN0eWxlc2hlZXQgd2l0aCBganVpY2VgLlxuICpcbiAqIFRoZSBsYXlvdXQgaXMgdGhlIGNsYXNzaWMgZW1haWwtc2FmZSBwYXR0ZXJuIChhIGNlbnRlcmVkIDYwMHB4XG4gKiB0YWJsZS1iYXNlZCBjb250YWluZXIgd2l0aCBhIGhlYWRlciBiYW5kLCBjb250ZW50IGNhcmQsIGFuZCBvcHRpb25hbFxuICogZm9vdGVyKSAtIHRoZSBzYW1lIHNoYXBlIE1KTUwgZW1pdHMsIGhhbmQtYnVpbHQgaGVyZSBiZWNhdXNlIE1KTUwnc1xuICogdG9vbGNoYWluIHB1bGxzIGZhc3QtbW92aW5nIGJyb3dzZXItZGF0YSBkZXBzIChjYW5pdXNlLWxpdGUsXG4gKiBiYXNlbGluZS1icm93c2VyLW1hcHBpbmcpIHRoYXQgYXJlIGF3a3dhcmQgdG8gaW5zdGFsbCBiZWhpbmQgYVxuICogbG9ja2VkLWRvd24gcmVnaXN0cnkuIElubGluaW5nIG1hdHRlcnMgYmVjYXVzZSByZWFsIGNsaWVudHMgKEdtYWlsLFxuICogT3V0bG9vaykgc3RyaXAgYDxzdHlsZT5gIGJsb2NrcyBhbmQgaWdub3JlIGNsYXNzIHNlbGVjdG9yczsgdGhlIHNhbWVcbiAqIHJlbmRlcmVyIGZlZWRzIGJvdGggdGhlIGxvY2FsIG91dGJveCBwcmV2aWV3IGFuZCB0aGUgU01UUCBIVE1MIHBhcnQsXG4gKiBzbyBhIGJyb3dzZXIgYW5kIGFuIGluYm94IHNob3cgdGhlIHNhbWUgdGhpbmcuXG4gKlxuICogQnJhbmQgc3R5bGluZyAoYWNjZW50IGNvbG9yLCBmb250LCBoZWFkZXIgbG9nbykgaXMgb3B0aW9uYWw6IHBhc3MgYW5cbiAqIHtAbGluayBFbWFpbEJyYW5kfSB0byBjb2xvciB0aGUgbGF5b3V0LCBvciBvbWl0IGl0IGZvciB0aGUgbmV1dHJhbFxuICogZGVmYXVsdC4gQnJhbmRpbmcgaXMgaW5saW5lZCBoZXJlIGJlY2F1c2UgdGhlIGJyb3dzZXIgVUkncyBgW2RhdGEtYnJhbmRdYFxuICogQ1NTIGJyaWRnZSBjYW4ndCByZWFjaCBhbiBpbmJveCAoc2VlIGAuL2JyYW5kYCkuXG4gKlxuICogQG1vZHVsZVxuICovXG5cbmltcG9ydCB7IHN0cmluZyB9IGZyb20gXCJAZGJ4LXRvb2xzL3NoYXJlZC1jb3JlXCI7XG5pbXBvcnQganVpY2UgZnJvbSBcImp1aWNlXCI7XG5pbXBvcnQgdHlwZSB7IEVtYWlsQnJhbmQgfSBmcm9tIFwiLi9icmFuZFwiO1xuaW1wb3J0IHsgbWFya2Rvd25Ub0h0bWwgfSBmcm9tIFwiLi9tYXJrZG93blwiO1xuXG4vKipcbiAqIFJlbmRlcmVkIGhlaWdodCBvZiB0aGUgaGVhZGVyIGxvZ28uIEZpeGVkIHJhdGhlciB0aGFuIGludHJpbnNpYyBiZWNhdXNlXG4gKiBtYWlsIGNsaWVudHMgaWdub3JlIENTUyBzaXppbmcgb24gYW4gaW1hZ2Ugd2l0aG91dCBhbiBgaGVpZ2h0YCBhdHRyaWJ1dGUuXG4gKi9cbmNvbnN0IExPR09fSEVJR0hUX1BYID0gMjg7XG5cbi8qKiBOZXV0cmFsIGZhbGxiYWNrIHN0eWxpbmcgd2hlbiBubyBicmFuZCBpcyBzdXBwbGllZC4gKi9cbmNvbnN0IERFRkFVTFRfQlJBTkQ6IFJlcXVpcmVkPFBpY2s8RW1haWxCcmFuZCwgXCJhY2NlbnRcIiB8IFwib25BY2NlbnRcIiB8IFwiZm9udEZhbWlseVwiPj4gPSB7XG4gIGFjY2VudDogXCIjMGI2YmNiXCIsXG4gIG9uQWNjZW50OiBcIiNmZmZmZmZcIixcbiAgZm9udEZhbWlseTogXCItYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICdTZWdvZSBVSScsIEhlbHZldGljYSwgQXJpYWwsIHNhbnMtc2VyaWZcIixcbn07XG5cbi8qKiBFc2NhcGUgSFRNTC1zaWduaWZpY2FudCBjaGFyYWN0ZXJzIChyZS1leHBvcnRlZCBmcm9tIGBAZGJ4LXRvb2xzL3NoYXJlZC1jb3JlYCkuICovXG5leHBvcnQgY29uc3QgZXNjYXBlSHRtbCA9IHN0cmluZy5lc2NhcGVIdG1sO1xuXG4vKipcbiAqIENvbnRlbnQgc3R5bGVzaGVldCBpbmxpbmVkIG9udG8gdGhlIG1hcmtkb3duIGJvZHkgKGp1aWNlIG1hcHMgdGhlc2VcbiAqIG9udG8gZWxlbWVudHMpLiBPdXRlciBsYXlvdXQgc3R5bGluZyBpcyB3cml0dGVuIGlubGluZSBkaXJlY3RseSBzbyBpdFxuICogc3Vydml2ZXMgZXZlbiBpZiBpbmxpbmluZyBpcyBza2lwcGVkOyB0aGUgYEBtZWRpYWAgcnVsZSBpcyBwcmVzZXJ2ZWRcbiAqIGJ5IGp1aWNlIGZvciBjbGllbnRzIHRoYXQgaG9ub3IgaXQuIFBhcmFtZXRlcml6ZWQgYnkgdGhlIHJlc29sdmVkIGFjY2VudFxuICogc28gYm9keSBsaW5rcyBtYXRjaCB0aGUgYnJhbmQuXG4gKi9cbmZ1bmN0aW9uIGNvbnRlbnRDc3MoYWNjZW50OiBzdHJpbmcpOiBzdHJpbmcge1xuICByZXR1cm4gYFxuICAgIC5lbWFpbC1ib2R5IHsgZm9udC1zaXplOiAxNXB4OyBsaW5lLWhlaWdodDogMS41NTsgY29sb3I6ICMxYTFhMWE7IH1cbiAgICAuZW1haWwtYm9keSBwIHsgbWFyZ2luOiAwIDAgMXJlbTsgfVxuICAgIC5lbWFpbC1ib2R5IGEgeyBjb2xvcjogJHthY2NlbnR9OyB9XG4gICAgLmVtYWlsLWJvZHkgaDEsIC5lbWFpbC1ib2R5IGgyLCAuZW1haWwtYm9keSBoMyB7IG1hcmdpbjogMS40cmVtIDAgMC42cmVtOyBsaW5lLWhlaWdodDogMS4yNTsgfVxuICAgIC5lbWFpbC1ib2R5IHVsLCAuZW1haWwtYm9keSBvbCB7IG1hcmdpbjogMCAwIDFyZW07IHBhZGRpbmctbGVmdDogMS40cmVtOyB9XG4gICAgLmVtYWlsLWJvZHkgdGFibGUgeyBib3JkZXItY29sbGFwc2U6IGNvbGxhcHNlOyBtYXJnaW46IDFyZW0gMDsgd2lkdGg6IDEwMCU7IGZvbnQtc2l6ZTogMTRweDsgfVxuICAgIC5lbWFpbC1ib2R5IHRoLCAuZW1haWwtYm9keSB0ZCB7IGJvcmRlcjogMXB4IHNvbGlkICNkMGQ3ZGU7IHBhZGRpbmc6IDZweCAxMHB4OyB0ZXh0LWFsaWduOiBsZWZ0OyB9XG4gICAgLmVtYWlsLWJvZHkgdGggeyBiYWNrZ3JvdW5kOiAjZjZmOGZhOyBmb250LXdlaWdodDogNjAwOyB9XG4gICAgLmVtYWlsLWJvZHkgY29kZSB7IGJhY2tncm91bmQ6ICNmMWYzZjU7IHBhZGRpbmc6IDJweCA1cHg7IGJvcmRlci1yYWRpdXM6IDRweDsgZm9udC1mYW1pbHk6IHVpLW1vbm9zcGFjZSwgU0ZNb25vLVJlZ3VsYXIsIE1lbmxvLCBtb25vc3BhY2U7IGZvbnQtc2l6ZTogMC45ZW07IH1cbiAgICAuZW1haWwtYm9keSBwcmUgeyBiYWNrZ3JvdW5kOiAjZjFmM2Y1OyBwYWRkaW5nOiAxMnB4OyBib3JkZXItcmFkaXVzOiA2cHg7IG92ZXJmbG93LXg6IGF1dG87IH1cbiAgICAuZW1haWwtYm9keSBwcmUgY29kZSB7IGJhY2tncm91bmQ6IG5vbmU7IHBhZGRpbmc6IDA7IH1cbiAgICAuZW1haWwtYm9keSBibG9ja3F1b3RlIHsgbWFyZ2luOiAxcmVtIDA7IHBhZGRpbmc6IDAgMXJlbTsgY29sb3I6ICM1NzYwNmE7IGJvcmRlci1sZWZ0OiAzcHggc29saWQgI2QwZDdkZTsgfVxuICAgIC5lbWFpbC1ib2R5IGltZyB7IG1heC13aWR0aDogMTAwJTsgaGVpZ2h0OiBhdXRvOyB9XG4gICAgLm1ldGEgeyBib3JkZXItY29sbGFwc2U6IGNvbGxhcHNlOyBmb250LXNpemU6IDEzcHg7IG1hcmdpbi1ib3R0b206IDRweDsgfVxuICAgIC5tZXRhIHRoIHsgdGV4dC1hbGlnbjogbGVmdDsgcGFkZGluZzogMnB4IDEycHggMnB4IDA7IHZlcnRpY2FsLWFsaWduOiB0b3A7IGNvbG9yOiAjNmI3MjgwOyBmb250LXdlaWdodDogNjAwOyB3aGl0ZS1zcGFjZTogbm93cmFwOyB9XG4gICAgLm1ldGEgdGQgeyBwYWRkaW5nOiAycHggMDsgY29sb3I6ICMzNzQxNTE7IH1cbiAgICBAbWVkaWEgb25seSBzY3JlZW4gYW5kIChtYXgtd2lkdGg6IDYyMHB4KSB7XG4gICAgICAuY29udGFpbmVyIHsgd2lkdGg6IDEwMCUgIWltcG9ydGFudDsgfVxuICAgICAgLmd1dHRlciB7IHBhZGRpbmctbGVmdDogMjBweCAhaW1wb3J0YW50OyBwYWRkaW5nLXJpZ2h0OiAyMHB4ICFpbXBvcnRhbnQ7IH1cbiAgICB9YDtcbn1cblxuLyoqIE9wdGlvbnMgZm9yIHtAbGluayByZW5kZXJFbWFpbEh0bWx9LiAqL1xuZXhwb3J0IGludGVyZmFjZSBFbWFpbEh0bWxPcHRpb25zIHtcbiAgLyoqIE1hcmtkb3duIGJvZHkuIFJlbmRlcmVkIHRvIEhUTUwsIHRoZW4gd3JhcHBlZCBpbiB0aGUgbGF5b3V0LiAqL1xuICBib2R5OiBzdHJpbmc7XG4gIC8qKiBIZWFkZXItYmFuZCB0aXRsZSBhbmQgZG9jdW1lbnQgYDx0aXRsZT5gLiBEZWZhdWx0cyB0byBcIk1lc3NhZ2VcIi4gKi9cbiAgc3ViamVjdD86IHN0cmluZztcbiAgLyoqXG4gICAqIE9wdGlvbmFsIGBbbGFiZWwsIHZhbHVlXWAgZW52ZWxvcGUgcm93cyBzaG93biBhYm92ZSB0aGUgYm9keSAodXNlZFxuICAgKiBieSB0aGUgb3V0Ym94IHByZXZpZXc7IG9taXR0ZWQgZm9yIFNNVFAgc2VuZHMsIHdoZXJlIHRoZSBtYWlsIGNsaWVudFxuICAgKiBzaG93cyB0aGUgZW52ZWxvcGUgaXRzZWxmKS5cbiAgICovXG4gIGhlYWRlcnM/OiBSZWFkb25seUFycmF5PHJlYWRvbmx5IFtzdHJpbmcsIHN0cmluZ10+O1xuICAvKiogT3B0aW9uYWwgc21hbGwtcHJpbnQgZm9vdGVyIGxpbmUuIE9taXR0ZWQgd2hlbiB1bnNldC4gKi9cbiAgZm9vdGVyPzogc3RyaW5nO1xuICAvKipcbiAgICogT3B0aW9uYWwgYnJhbmQgc3R5bGluZyBmb3IgdGhlIGxheW91dCAoYWNjZW50LCBmb250LCBoZWFkZXIgbG9nbykuXG4gICAqIE9taXQgZm9yIHRoZSBuZXV0cmFsIGRlZmF1bHQgcGFsZXR0ZS5cbiAgICovXG4gIGJyYW5kPzogRW1haWxCcmFuZDtcbn1cblxuLyoqIFJlbmRlciB0aGUgb3B0aW9uYWwgZW52ZWxvcGUtaGVhZGVyIHRhYmxlIGJsb2NrLiAqL1xuZnVuY3Rpb24gbWV0YUJsb2NrKGhlYWRlcnM6IEVtYWlsSHRtbE9wdGlvbnNbXCJoZWFkZXJzXCJdKTogc3RyaW5nIHtcbiAgaWYgKCFoZWFkZXJzIHx8IGhlYWRlcnMubGVuZ3RoID09PSAwKSByZXR1cm4gXCJcIjtcbiAgY29uc3Qgcm93cyA9IGhlYWRlcnNcbiAgICAubWFwKChbbGFiZWwsIHZhbHVlXSkgPT4gYDx0cj48dGg+JHtlc2NhcGVIdG1sKGxhYmVsKX08L3RoPjx0ZD4ke2VzY2FwZUh0bWwodmFsdWUpfTwvdGQ+PC90cj5gKVxuICAgIC5qb2luKFwiXCIpO1xuICByZXR1cm4gYDx0YWJsZSByb2xlPVwicHJlc2VudGF0aW9uXCIgY2xhc3M9XCJtZXRhXCI+PHRib2R5PiR7cm93c308L3Rib2R5PjwvdGFibGU+YDtcbn1cblxuLyoqIFJlbmRlciB0aGUgb3B0aW9uYWwgZm9vdGVyIHJvdy4gKi9cbmZ1bmN0aW9uIGZvb3RlclJvdyhmb290ZXI6IHN0cmluZyB8IHVuZGVmaW5lZCk6IHN0cmluZyB7XG4gIGlmICghZm9vdGVyKSByZXR1cm4gXCJcIjtcbiAgcmV0dXJuIGBcbiAgICAgICAgICA8dHI+XG4gICAgICAgICAgICA8dGQgY2xhc3M9XCJndXR0ZXJcIiBzdHlsZT1cInBhZGRpbmc6IDE2cHggMzJweDsgYm9yZGVyLXRvcDogMXB4IHNvbGlkICNlYWVjZWY7IGNvbG9yOiAjOWFhMGE2OyBmb250LXNpemU6IDEycHg7IGxpbmUtaGVpZ2h0OiAxLjU7XCI+XG4gICAgICAgICAgICAgICR7ZXNjYXBlSHRtbChmb290ZXIpfVxuICAgICAgICAgICAgPC90ZD5cbiAgICAgICAgICA8L3RyPmA7XG59XG5cbi8qKlxuICogUmVuZGVyIHRoZSBoZWFkZXItYmFuZCBjb250ZW50OiB0aGUgYnJhbmQgbG9nbyAod2hlbiB0aGUgYnJhbmQgc3VwcGxpZXMgYVxuICogcmVuZGVyYWJsZSBpbWFnZSkgYWJvdmUgdGhlIHRpdGxlLCBvciBqdXN0IHRoZSB0aXRsZS4gVGhlIGxvZ28gaXMgY2FwcGVkXG4gKiBhdCB7QGxpbmsgTE9HT19IRUlHSFRfUFh9IGFuZCB0aW50ZWQgaW1wbGljaXRseSBieSBpdHMgb3duIGFydHdvcms7IHRoZVxuICogdGl0bGUgYWx3YXlzIHNob3dzIHNvIHRoZSBiYW5kIGlzIG5ldmVyIGVtcHR5LlxuICovXG5mdW5jdGlvbiBoZWFkZXJCYW5kKHRpdGxlOiBzdHJpbmcsIGJyYW5kOiBFbWFpbEJyYW5kLCBvbkFjY2VudDogc3RyaW5nKTogc3RyaW5nIHtcbiAgY29uc3QgbG9nbyA9IGJyYW5kLmxvZ29VcmxcbiAgICA/IGA8aW1nIHNyYz1cIiR7ZXNjYXBlSHRtbChicmFuZC5sb2dvVXJsKX1cIiBhbHQ9XCIke2VzY2FwZUh0bWwoYnJhbmQubmFtZSA/PyB0aXRsZSl9XCIgaGVpZ2h0PVwiJHtMT0dPX0hFSUdIVF9QWH1cIiBzdHlsZT1cImhlaWdodDogJHtMT0dPX0hFSUdIVF9QWH1weDsgd2lkdGg6IGF1dG87IGRpc3BsYXk6IGJsb2NrOyBtYXJnaW4tYm90dG9tOiA4cHg7XCIgLz5gXG4gICAgOiBcIlwiO1xuICByZXR1cm4gYCR7bG9nb308c3BhbiBzdHlsZT1cImNvbG9yOiAke29uQWNjZW50fTsgZm9udC1zaXplOiAxOHB4OyBmb250LXdlaWdodDogNzAwOyBsaW5lLWhlaWdodDogMS4zO1wiPiR7ZXNjYXBlSHRtbCh0aXRsZSl9PC9zcGFuPmA7XG59XG5cbi8qKlxuICogUmVuZGVyIGBib2R5YCAobWFya2Rvd24pIGludG8gYSBjb21wbGV0ZSwgc3R5bGUtaW5saW5lZCBlbWFpbCBkb2N1bWVudFxuICogdXNpbmcgdGhlIHJlc3BvbnNpdmUgbGF5b3V0LiBXaGVuIGBvcHRzLmJyYW5kYCBpcyBzZXQgaXRzIGFjY2VudCwgZm9udCxcbiAqIGFuZCBsb2dvIHN0eWxlIHRoZSBsYXlvdXQ7IG90aGVyd2lzZSBhIG5ldXRyYWwgZGVmYXVsdCBwYWxldHRlIGlzIHVzZWQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiByZW5kZXJFbWFpbEh0bWwob3B0czogRW1haWxIdG1sT3B0aW9ucyk6IHN0cmluZyB7XG4gIGNvbnN0IHRpdGxlID0gb3B0cy5zdWJqZWN0Py50cmltKCkgfHwgXCJNZXNzYWdlXCI7XG4gIGNvbnN0IGFjY2VudCA9IG9wdHMuYnJhbmQ/LmFjY2VudCA/PyBERUZBVUxUX0JSQU5ELmFjY2VudDtcbiAgY29uc3Qgb25BY2NlbnQgPSBvcHRzLmJyYW5kPy5vbkFjY2VudCA/PyBERUZBVUxUX0JSQU5ELm9uQWNjZW50O1xuICBjb25zdCBmb250RmFtaWx5ID0gb3B0cy5icmFuZD8uZm9udEZhbWlseSA/PyBERUZBVUxUX0JSQU5ELmZvbnRGYW1pbHk7XG4gIGNvbnN0IGJyYW5kOiBFbWFpbEJyYW5kID0gb3B0cy5icmFuZCA/PyB7IGFjY2VudCwgb25BY2NlbnQsIGZvbnRGYW1pbHkgfTtcbiAgY29uc3QgZG9jID0gYDwhZG9jdHlwZSBodG1sPlxuPGh0bWwgbGFuZz1cImVuXCI+XG4gIDxoZWFkPlxuICAgIDxtZXRhIGNoYXJzZXQ9XCJ1dGYtOFwiIC8+XG4gICAgPG1ldGEgbmFtZT1cInZpZXdwb3J0XCIgY29udGVudD1cIndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xXCIgLz5cbiAgICA8bWV0YSBuYW1lPVwiY29sb3Itc2NoZW1lXCIgY29udGVudD1cImxpZ2h0IG9ubHlcIiAvPlxuICAgIDx0aXRsZT4ke2VzY2FwZUh0bWwodGl0bGUpfTwvdGl0bGU+XG4gICAgPHN0eWxlPiR7Y29udGVudENzcyhhY2NlbnQpfVxuICAgIDwvc3R5bGU+XG4gIDwvaGVhZD5cbiAgPGJvZHkgc3R5bGU9XCJtYXJnaW46IDA7IHBhZGRpbmc6IDA7IGJhY2tncm91bmQtY29sb3I6ICNmNGY1Zjc7IC13ZWJraXQtdGV4dC1zaXplLWFkanVzdDogMTAwJTtcIj5cbiAgICA8dGFibGUgcm9sZT1cInByZXNlbnRhdGlvblwiIHdpZHRoPVwiMTAwJVwiIGNlbGxwYWRkaW5nPVwiMFwiIGNlbGxzcGFjaW5nPVwiMFwiIHN0eWxlPVwiYmFja2dyb3VuZC1jb2xvcjogI2Y0ZjVmNztcIj5cbiAgICAgIDx0cj5cbiAgICAgICAgPHRkIGFsaWduPVwiY2VudGVyXCIgc3R5bGU9XCJwYWRkaW5nOiAyNHB4IDEycHg7XCI+XG4gICAgICAgICAgPHRhYmxlIHJvbGU9XCJwcmVzZW50YXRpb25cIiBjbGFzcz1cImNvbnRhaW5lclwiIHdpZHRoPVwiNjAwXCIgY2VsbHBhZGRpbmc9XCIwXCIgY2VsbHNwYWNpbmc9XCIwXCIgc3R5bGU9XCJ3aWR0aDogNjAwcHg7IG1heC13aWR0aDogMTAwJTsgYmFja2dyb3VuZC1jb2xvcjogI2ZmZmZmZjsgYm9yZGVyLXJhZGl1czogMTBweDsgb3ZlcmZsb3c6IGhpZGRlbjsgYm94LXNoYWRvdzogMCAxcHggM3B4IHJnYmEoMCwwLDAsMC4wOCk7IGZvbnQtZmFtaWx5OiAke2ZvbnRGYW1pbHl9O1wiPlxuICAgICAgICAgICAgPHRyPlxuICAgICAgICAgICAgICA8dGQgY2xhc3M9XCJndXR0ZXJcIiBzdHlsZT1cInBhZGRpbmc6IDIwcHggMzJweDsgYmFja2dyb3VuZC1jb2xvcjogJHthY2NlbnR9O1wiPlxuICAgICAgICAgICAgICAgICR7aGVhZGVyQmFuZCh0aXRsZSwgYnJhbmQsIG9uQWNjZW50KX1cbiAgICAgICAgICAgICAgPC90ZD5cbiAgICAgICAgICAgIDwvdHI+XG4gICAgICAgICAgICA8dHI+XG4gICAgICAgICAgICAgIDx0ZCBjbGFzcz1cImd1dHRlclwiIHN0eWxlPVwicGFkZGluZzogMjRweCAzMnB4IDhweDtcIj5cbiAgICAgICAgICAgICAgICAke21ldGFCbG9jayhvcHRzLmhlYWRlcnMpfVxuICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9XCJlbWFpbC1ib2R5XCI+JHttYXJrZG93blRvSHRtbChvcHRzLmJvZHkpfTwvZGl2PlxuICAgICAgICAgICAgICA8L3RkPlxuICAgICAgICAgICAgPC90cj4ke2Zvb3RlclJvdyhvcHRzLmZvb3Rlcil9XG4gICAgICAgICAgPC90YWJsZT5cbiAgICAgICAgPC90ZD5cbiAgICAgIDwvdHI+XG4gICAgPC90YWJsZT5cbiAgPC9ib2R5PlxuPC9odG1sPlxuYDtcbiAgcmV0dXJuIGp1aWNlKGRvYyk7XG59XG4iXX0=
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side markdown -> HTML rendering for email bodies. The model
|
|
3
|
+
* drafts bodies in markdown; this turns them into real HTML (GFM tables,
|
|
4
|
+
* lists, code, links). {@link normalizeMarkdown} first repairs the two
|
|
5
|
+
* structures LLMs most often emit as plain text instead of markdown -
|
|
6
|
+
* `=====` divider rules and pipe tables missing their `| --- |`
|
|
7
|
+
* separator row - so they render as a `<hr>` / `<table>` rather than
|
|
8
|
+
* literal text. The prompt steers the model away from ASCII art; this is
|
|
9
|
+
* the belt-and-suspenders fallback.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Repair common LLM "looks-like-markdown-but-isn't" output: convert
|
|
15
|
+
* standalone `=====` / `_____` rules to a `---` thematic break (leaving
|
|
16
|
+
* genuine setext-heading underlines intact), and insert a `| --- |`
|
|
17
|
+
* separator after the first row of a pipe block that lacks one so GFM
|
|
18
|
+
* renders it as a table.
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizeMarkdown(src: string): string;
|
|
21
|
+
/** Render a markdown body to an HTML fragment (GFM tables enabled). */
|
|
22
|
+
export declare function markdownToHtml(body: string): string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side markdown -> HTML rendering for email bodies. The model
|
|
3
|
+
* drafts bodies in markdown; this turns them into real HTML (GFM tables,
|
|
4
|
+
* lists, code, links). {@link normalizeMarkdown} first repairs the two
|
|
5
|
+
* structures LLMs most often emit as plain text instead of markdown -
|
|
6
|
+
* `=====` divider rules and pipe tables missing their `| --- |`
|
|
7
|
+
* separator row - so they render as a `<hr>` / `<table>` rather than
|
|
8
|
+
* literal text. The prompt steers the model away from ASCII art; this is
|
|
9
|
+
* the belt-and-suspenders fallback.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import { marked } from "marked";
|
|
14
|
+
/** A line of only `=` or `_` (length >= 3): an ASCII divider rule. */
|
|
15
|
+
function isAsciiRule(line) {
|
|
16
|
+
return /^[ \t]*[=_]{3,}[ \t]*$/.test(line);
|
|
17
|
+
}
|
|
18
|
+
/** A line that participates in a markdown pipe table (has a `|`). */
|
|
19
|
+
function isPipeRow(line) {
|
|
20
|
+
return line.includes("|") && line.trim().length > 0;
|
|
21
|
+
}
|
|
22
|
+
/** A markdown table separator row, e.g. `| --- | :--: |`. */
|
|
23
|
+
function isSeparatorRow(line) {
|
|
24
|
+
return /^[ \t]*\|?[ \t]*:?-{2,}:?[ \t]*(\|[ \t]*:?-{2,}:?[ \t]*)+\|?[ \t]*$/.test(line);
|
|
25
|
+
}
|
|
26
|
+
/** Column count of a pipe row (outer pipes optional). */
|
|
27
|
+
function pipeColumns(line) {
|
|
28
|
+
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").length;
|
|
29
|
+
}
|
|
30
|
+
/** A GFM separator row with `columns` cells. */
|
|
31
|
+
function separatorRow(columns) {
|
|
32
|
+
return `| ${Array.from({ length: columns }, () => "---").join(" | ")} |`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Repair common LLM "looks-like-markdown-but-isn't" output: convert
|
|
36
|
+
* standalone `=====` / `_____` rules to a `---` thematic break (leaving
|
|
37
|
+
* genuine setext-heading underlines intact), and insert a `| --- |`
|
|
38
|
+
* separator after the first row of a pipe block that lacks one so GFM
|
|
39
|
+
* renders it as a table.
|
|
40
|
+
*/
|
|
41
|
+
export function normalizeMarkdown(src) {
|
|
42
|
+
const lines = src.split("\n");
|
|
43
|
+
const out = [];
|
|
44
|
+
for (let i = 0; i < lines.length; i++) {
|
|
45
|
+
const line = lines[i];
|
|
46
|
+
const prev = i > 0 ? lines[i - 1] : "";
|
|
47
|
+
const next = i + 1 < lines.length ? lines[i + 1] : "";
|
|
48
|
+
if (isAsciiRule(line)) {
|
|
49
|
+
// A run of `=` directly under a non-blank text line is a setext H1
|
|
50
|
+
// underline - keep it. Anything else is a decorative divider.
|
|
51
|
+
const isSetextUnderline = /^[ \t]*={3,}[ \t]*$/.test(line) && prev.trim() !== "" && !isPipeRow(prev);
|
|
52
|
+
out.push(isSetextUnderline ? line : "---");
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// First row of a pipe block (prev is not itself a pipe row) followed
|
|
56
|
+
// by another aligned pipe row, with no separator and no `###` bar-
|
|
57
|
+
// chart fill: treat as a table header and inject the separator.
|
|
58
|
+
const startsPipeBlock = isPipeRow(line) && !isPipeRow(prev);
|
|
59
|
+
if (startsPipeBlock &&
|
|
60
|
+
isPipeRow(next) &&
|
|
61
|
+
!isSeparatorRow(next) &&
|
|
62
|
+
!/#{3,}/.test(line) &&
|
|
63
|
+
pipeColumns(line) >= 2 &&
|
|
64
|
+
pipeColumns(line) === pipeColumns(next)) {
|
|
65
|
+
out.push(line);
|
|
66
|
+
out.push(separatorRow(pipeColumns(line)));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
out.push(line);
|
|
70
|
+
}
|
|
71
|
+
return out.join("\n");
|
|
72
|
+
}
|
|
73
|
+
/** Render a markdown body to an HTML fragment (GFM tables enabled). */
|
|
74
|
+
export function markdownToHtml(body) {
|
|
75
|
+
return marked.parse(normalizeMarkdown(body), {
|
|
76
|
+
async: false,
|
|
77
|
+
gfm: true,
|
|
78
|
+
breaks: true,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFya2Rvd24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvbWFya2Rvd24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7O0dBV0c7QUFFSCxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sUUFBUSxDQUFDO0FBRWhDLHNFQUFzRTtBQUN0RSxTQUFTLFdBQVcsQ0FBQyxJQUFZO0lBQy9CLE9BQU8sd0JBQXdCLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO0FBQzdDLENBQUM7QUFFRCxxRUFBcUU7QUFDckUsU0FBUyxTQUFTLENBQUMsSUFBWTtJQUM3QixPQUFPLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLElBQUksSUFBSSxDQUFDLElBQUksRUFBRSxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUM7QUFDdEQsQ0FBQztBQUVELDZEQUE2RDtBQUM3RCxTQUFTLGNBQWMsQ0FBQyxJQUFZO0lBQ2xDLE9BQU8scUVBQXFFLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO0FBQzFGLENBQUM7QUFFRCx5REFBeUQ7QUFDekQsU0FBUyxXQUFXLENBQUMsSUFBWTtJQUMvQixPQUFPLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxPQUFPLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE1BQU0sQ0FBQztBQUM3RSxDQUFDO0FBRUQsZ0RBQWdEO0FBQ2hELFNBQVMsWUFBWSxDQUFDLE9BQWU7SUFDbkMsT0FBTyxLQUFLLEtBQUssQ0FBQyxJQUFJLENBQUMsRUFBRSxNQUFNLEVBQUUsT0FBTyxFQUFFLEVBQUUsR0FBRyxFQUFFLENBQUMsS0FBSyxDQUFDLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUM7QUFDM0UsQ0FBQztBQUVEOzs7Ozs7R0FNRztBQUNILE1BQU0sVUFBVSxpQkFBaUIsQ0FBQyxHQUFXO0lBQzNDLE1BQU0sS0FBSyxHQUFHLEdBQUcsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDOUIsTUFBTSxHQUFHLEdBQWEsRUFBRSxDQUFDO0lBQ3pCLEtBQUssSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsR0FBRyxLQUFLLENBQUMsTUFBTSxFQUFFLENBQUMsRUFBRSxFQUFFLENBQUM7UUFDdEMsTUFBTSxJQUFJLEdBQUcsS0FBSyxDQUFDLENBQUMsQ0FBRSxDQUFDO1FBQ3ZCLE1BQU0sSUFBSSxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztRQUN4QyxNQUFNLElBQUksR0FBRyxDQUFDLEdBQUcsQ0FBQyxHQUFHLEtBQUssQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztRQUV2RCxJQUFJLFdBQVcsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDO1lBQ3RCLG1FQUFtRTtZQUNuRSw4REFBOEQ7WUFDOUQsTUFBTSxpQkFBaUIsR0FDckIscUJBQXFCLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLElBQUksQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDN0UsR0FBRyxDQUFDLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQztZQUMzQyxTQUFTO1FBQ1gsQ0FBQztRQUVELHFFQUFxRTtRQUNyRSxtRUFBbUU7UUFDbkUsZ0VBQWdFO1FBQ2hFLE1BQU0sZUFBZSxHQUFHLFNBQVMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUM1RCxJQUNFLGVBQWU7WUFDZixTQUFTLENBQUMsSUFBSSxDQUFDO1lBQ2YsQ0FBQyxjQUFjLENBQUMsSUFBSSxDQUFDO1lBQ3JCLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUM7WUFDbkIsV0FBVyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUM7WUFDdEIsV0FBVyxDQUFDLElBQUksQ0FBQyxLQUFLLFdBQVcsQ0FBQyxJQUFJLENBQUMsRUFDdkMsQ0FBQztZQUNELEdBQUcsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDZixHQUFHLENBQUMsSUFBSSxDQUFDLFlBQVksQ0FBQyxXQUFXLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQzFDLFNBQVM7UUFDWCxDQUFDO1FBRUQsR0FBRyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUNqQixDQUFDO0lBQ0QsT0FBTyxHQUFHLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO0FBQ3hCLENBQUM7QUFFRCx1RUFBdUU7QUFDdkUsTUFBTSxVQUFVLGNBQWMsQ0FBQyxJQUFZO0lBQ3pDLE9BQU8sTUFBTSxDQUFDLEtBQUssQ0FBQyxpQkFBaUIsQ0FBQyxJQUFJLENBQUMsRUFBRTtRQUMzQyxLQUFLLEVBQUUsS0FBSztRQUNaLEdBQUcsRUFBRSxJQUFJO1FBQ1QsTUFBTSxFQUFFLElBQUk7S0FDYixDQUFDLENBQUM7QUFDTCxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBTZXJ2ZXItc2lkZSBtYXJrZG93biAtPiBIVE1MIHJlbmRlcmluZyBmb3IgZW1haWwgYm9kaWVzLiBUaGUgbW9kZWxcbiAqIGRyYWZ0cyBib2RpZXMgaW4gbWFya2Rvd247IHRoaXMgdHVybnMgdGhlbSBpbnRvIHJlYWwgSFRNTCAoR0ZNIHRhYmxlcyxcbiAqIGxpc3RzLCBjb2RlLCBsaW5rcykuIHtAbGluayBub3JtYWxpemVNYXJrZG93bn0gZmlyc3QgcmVwYWlycyB0aGUgdHdvXG4gKiBzdHJ1Y3R1cmVzIExMTXMgbW9zdCBvZnRlbiBlbWl0IGFzIHBsYWluIHRleHQgaW5zdGVhZCBvZiBtYXJrZG93biAtXG4gKiBgPT09PT1gIGRpdmlkZXIgcnVsZXMgYW5kIHBpcGUgdGFibGVzIG1pc3NpbmcgdGhlaXIgYHwgLS0tIHxgXG4gKiBzZXBhcmF0b3Igcm93IC0gc28gdGhleSByZW5kZXIgYXMgYSBgPGhyPmAgLyBgPHRhYmxlPmAgcmF0aGVyIHRoYW5cbiAqIGxpdGVyYWwgdGV4dC4gVGhlIHByb21wdCBzdGVlcnMgdGhlIG1vZGVsIGF3YXkgZnJvbSBBU0NJSSBhcnQ7IHRoaXMgaXNcbiAqIHRoZSBiZWx0LWFuZC1zdXNwZW5kZXJzIGZhbGxiYWNrLlxuICpcbiAqIEBtb2R1bGVcbiAqL1xuXG5pbXBvcnQgeyBtYXJrZWQgfSBmcm9tIFwibWFya2VkXCI7XG5cbi8qKiBBIGxpbmUgb2Ygb25seSBgPWAgb3IgYF9gIChsZW5ndGggPj0gMyk6IGFuIEFTQ0lJIGRpdmlkZXIgcnVsZS4gKi9cbmZ1bmN0aW9uIGlzQXNjaWlSdWxlKGxpbmU6IHN0cmluZyk6IGJvb2xlYW4ge1xuICByZXR1cm4gL15bIFxcdF0qWz1fXXszLH1bIFxcdF0qJC8udGVzdChsaW5lKTtcbn1cblxuLyoqIEEgbGluZSB0aGF0IHBhcnRpY2lwYXRlcyBpbiBhIG1hcmtkb3duIHBpcGUgdGFibGUgKGhhcyBhIGB8YCkuICovXG5mdW5jdGlvbiBpc1BpcGVSb3cobGluZTogc3RyaW5nKTogYm9vbGVhbiB7XG4gIHJldHVybiBsaW5lLmluY2x1ZGVzKFwifFwiKSAmJiBsaW5lLnRyaW0oKS5sZW5ndGggPiAwO1xufVxuXG4vKiogQSBtYXJrZG93biB0YWJsZSBzZXBhcmF0b3Igcm93LCBlLmcuIGB8IC0tLSB8IDotLTogfGAuICovXG5mdW5jdGlvbiBpc1NlcGFyYXRvclJvdyhsaW5lOiBzdHJpbmcpOiBib29sZWFuIHtcbiAgcmV0dXJuIC9eWyBcXHRdKlxcfD9bIFxcdF0qOj8tezIsfTo/WyBcXHRdKihcXHxbIFxcdF0qOj8tezIsfTo/WyBcXHRdKikrXFx8P1sgXFx0XSokLy50ZXN0KGxpbmUpO1xufVxuXG4vKiogQ29sdW1uIGNvdW50IG9mIGEgcGlwZSByb3cgKG91dGVyIHBpcGVzIG9wdGlvbmFsKS4gKi9cbmZ1bmN0aW9uIHBpcGVDb2x1bW5zKGxpbmU6IHN0cmluZyk6IG51bWJlciB7XG4gIHJldHVybiBsaW5lLnRyaW0oKS5yZXBsYWNlKC9eXFx8LywgXCJcIikucmVwbGFjZSgvXFx8JC8sIFwiXCIpLnNwbGl0KFwifFwiKS5sZW5ndGg7XG59XG5cbi8qKiBBIEdGTSBzZXBhcmF0b3Igcm93IHdpdGggYGNvbHVtbnNgIGNlbGxzLiAqL1xuZnVuY3Rpb24gc2VwYXJhdG9yUm93KGNvbHVtbnM6IG51bWJlcik6IHN0cmluZyB7XG4gIHJldHVybiBgfCAke0FycmF5LmZyb20oeyBsZW5ndGg6IGNvbHVtbnMgfSwgKCkgPT4gXCItLS1cIikuam9pbihcIiB8IFwiKX0gfGA7XG59XG5cbi8qKlxuICogUmVwYWlyIGNvbW1vbiBMTE0gXCJsb29rcy1saWtlLW1hcmtkb3duLWJ1dC1pc24ndFwiIG91dHB1dDogY29udmVydFxuICogc3RhbmRhbG9uZSBgPT09PT1gIC8gYF9fX19fYCBydWxlcyB0byBhIGAtLS1gIHRoZW1hdGljIGJyZWFrIChsZWF2aW5nXG4gKiBnZW51aW5lIHNldGV4dC1oZWFkaW5nIHVuZGVybGluZXMgaW50YWN0KSwgYW5kIGluc2VydCBhIGB8IC0tLSB8YFxuICogc2VwYXJhdG9yIGFmdGVyIHRoZSBmaXJzdCByb3cgb2YgYSBwaXBlIGJsb2NrIHRoYXQgbGFja3Mgb25lIHNvIEdGTVxuICogcmVuZGVycyBpdCBhcyBhIHRhYmxlLlxuICovXG5leHBvcnQgZnVuY3Rpb24gbm9ybWFsaXplTWFya2Rvd24oc3JjOiBzdHJpbmcpOiBzdHJpbmcge1xuICBjb25zdCBsaW5lcyA9IHNyYy5zcGxpdChcIlxcblwiKTtcbiAgY29uc3Qgb3V0OiBzdHJpbmdbXSA9IFtdO1xuICBmb3IgKGxldCBpID0gMDsgaSA8IGxpbmVzLmxlbmd0aDsgaSsrKSB7XG4gICAgY29uc3QgbGluZSA9IGxpbmVzW2ldITtcbiAgICBjb25zdCBwcmV2ID0gaSA+IDAgPyBsaW5lc1tpIC0gMV0hIDogXCJcIjtcbiAgICBjb25zdCBuZXh0ID0gaSArIDEgPCBsaW5lcy5sZW5ndGggPyBsaW5lc1tpICsgMV0hIDogXCJcIjtcblxuICAgIGlmIChpc0FzY2lpUnVsZShsaW5lKSkge1xuICAgICAgLy8gQSBydW4gb2YgYD1gIGRpcmVjdGx5IHVuZGVyIGEgbm9uLWJsYW5rIHRleHQgbGluZSBpcyBhIHNldGV4dCBIMVxuICAgICAgLy8gdW5kZXJsaW5lIC0ga2VlcCBpdC4gQW55dGhpbmcgZWxzZSBpcyBhIGRlY29yYXRpdmUgZGl2aWRlci5cbiAgICAgIGNvbnN0IGlzU2V0ZXh0VW5kZXJsaW5lID1cbiAgICAgICAgL15bIFxcdF0qPXszLH1bIFxcdF0qJC8udGVzdChsaW5lKSAmJiBwcmV2LnRyaW0oKSAhPT0gXCJcIiAmJiAhaXNQaXBlUm93KHByZXYpO1xuICAgICAgb3V0LnB1c2goaXNTZXRleHRVbmRlcmxpbmUgPyBsaW5lIDogXCItLS1cIik7XG4gICAgICBjb250aW51ZTtcbiAgICB9XG5cbiAgICAvLyBGaXJzdCByb3cgb2YgYSBwaXBlIGJsb2NrIChwcmV2IGlzIG5vdCBpdHNlbGYgYSBwaXBlIHJvdykgZm9sbG93ZWRcbiAgICAvLyBieSBhbm90aGVyIGFsaWduZWQgcGlwZSByb3csIHdpdGggbm8gc2VwYXJhdG9yIGFuZCBubyBgIyMjYCBiYXItXG4gICAgLy8gY2hhcnQgZmlsbDogdHJlYXQgYXMgYSB0YWJsZSBoZWFkZXIgYW5kIGluamVjdCB0aGUgc2VwYXJhdG9yLlxuICAgIGNvbnN0IHN0YXJ0c1BpcGVCbG9jayA9IGlzUGlwZVJvdyhsaW5lKSAmJiAhaXNQaXBlUm93KHByZXYpO1xuICAgIGlmIChcbiAgICAgIHN0YXJ0c1BpcGVCbG9jayAmJlxuICAgICAgaXNQaXBlUm93KG5leHQpICYmXG4gICAgICAhaXNTZXBhcmF0b3JSb3cobmV4dCkgJiZcbiAgICAgICEvI3szLH0vLnRlc3QobGluZSkgJiZcbiAgICAgIHBpcGVDb2x1bW5zKGxpbmUpID49IDIgJiZcbiAgICAgIHBpcGVDb2x1bW5zKGxpbmUpID09PSBwaXBlQ29sdW1ucyhuZXh0KVxuICAgICkge1xuICAgICAgb3V0LnB1c2gobGluZSk7XG4gICAgICBvdXQucHVzaChzZXBhcmF0b3JSb3cocGlwZUNvbHVtbnMobGluZSkpKTtcbiAgICAgIGNvbnRpbnVlO1xuICAgIH1cblxuICAgIG91dC5wdXNoKGxpbmUpO1xuICB9XG4gIHJldHVybiBvdXQuam9pbihcIlxcblwiKTtcbn1cblxuLyoqIFJlbmRlciBhIG1hcmtkb3duIGJvZHkgdG8gYW4gSFRNTCBmcmFnbWVudCAoR0ZNIHRhYmxlcyBlbmFibGVkKS4gKi9cbmV4cG9ydCBmdW5jdGlvbiBtYXJrZG93blRvSHRtbChib2R5OiBzdHJpbmcpOiBzdHJpbmcge1xuICByZXR1cm4gbWFya2VkLnBhcnNlKG5vcm1hbGl6ZU1hcmtkb3duKGJvZHkpLCB7XG4gICAgYXN5bmM6IGZhbHNlLFxuICAgIGdmbTogdHJ1ZSxcbiAgICBicmVha3M6IHRydWUsXG4gIH0pO1xufVxuIl19
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem outbox for the no-SMTP fallback: {@link writeOutboxEmail}
|
|
3
|
+
* persists one drafted message as a standalone HTML file under
|
|
4
|
+
* `<dir>/<from>/<timestamp>-<subject-slug>.html` instead of sending it.
|
|
5
|
+
* This lets the approval flow and agents be exercised end-to-end with
|
|
6
|
+
* zero email configuration - open the file in a browser to see exactly
|
|
7
|
+
* what would have gone out (the HTML matches the SMTP send, headers
|
|
8
|
+
* table aside). The body is rendered + style-inlined by
|
|
9
|
+
* {@link renderEmailHtml}. Attachments are listed by filename in the
|
|
10
|
+
* header table but not written to disk - the outbox previews the
|
|
11
|
+
* envelope, it doesn't reproduce the wire payload.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import type { EmailMessage } from "@dbx-tools/shared-email";
|
|
16
|
+
import type { EmailBrand } from "./brand.js";
|
|
17
|
+
/**
|
|
18
|
+
* Write one message as HTML under `<dir>/<from>/` and return the
|
|
19
|
+
* absolute path. The per-sender folder mirrors the SMTP `From`, so test
|
|
20
|
+
* output groups by who the message would have been sent as.
|
|
21
|
+
*/
|
|
22
|
+
export declare function writeOutboxEmail(message: EmailMessage, from: string, dir: string, brand?: EmailBrand): Promise<string>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem outbox for the no-SMTP fallback: {@link writeOutboxEmail}
|
|
3
|
+
* persists one drafted message as a standalone HTML file under
|
|
4
|
+
* `<dir>/<from>/<timestamp>-<subject-slug>.html` instead of sending it.
|
|
5
|
+
* This lets the approval flow and agents be exercised end-to-end with
|
|
6
|
+
* zero email configuration - open the file in a browser to see exactly
|
|
7
|
+
* what would have gone out (the HTML matches the SMTP send, headers
|
|
8
|
+
* table aside). The body is rendered + style-inlined by
|
|
9
|
+
* {@link renderEmailHtml}. Attachments are listed by filename in the
|
|
10
|
+
* header table but not written to disk - the outbox previews the
|
|
11
|
+
* envelope, it doesn't reproduce the wire payload.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
17
|
+
import { string } from "@dbx-tools/shared-core";
|
|
18
|
+
import { renderEmailHtml } from "./email-html.js";
|
|
19
|
+
/**
|
|
20
|
+
* Longest subject slug kept in an outbox file name, leaving room for the
|
|
21
|
+
* timestamp prefix and extension inside common filesystem name limits.
|
|
22
|
+
*/
|
|
23
|
+
const SUBJECT_SLUG_MAX_LENGTH = 48;
|
|
24
|
+
/** Filesystem-safe slug of the subject for the file name. */
|
|
25
|
+
function subjectSlug(subject) {
|
|
26
|
+
return string.toSlugWithOptions({ maxLength: SUBJECT_SLUG_MAX_LENGTH }, subject) || "email";
|
|
27
|
+
}
|
|
28
|
+
/** The envelope rows shown above the body in the preview file. */
|
|
29
|
+
function headerRows(message, from) {
|
|
30
|
+
const rows = [
|
|
31
|
+
["From", from],
|
|
32
|
+
["To", message.to.join(", ")],
|
|
33
|
+
["Cc", message.cc?.join(", ")],
|
|
34
|
+
["Bcc", message.bcc?.join(", ")],
|
|
35
|
+
["Subject", message.subject],
|
|
36
|
+
["Attachments", message.attachments?.map((att) => att.filename).join(", ")],
|
|
37
|
+
["Date", new Date().toISOString()],
|
|
38
|
+
];
|
|
39
|
+
return rows.filter((row) => Boolean(row[1]));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Write one message as HTML under `<dir>/<from>/` and return the
|
|
43
|
+
* absolute path. The per-sender folder mirrors the SMTP `From`, so test
|
|
44
|
+
* output groups by who the message would have been sent as.
|
|
45
|
+
*/
|
|
46
|
+
export async function writeOutboxEmail(message, from, dir, brand) {
|
|
47
|
+
const folder = resolve(dir, from);
|
|
48
|
+
await mkdir(folder, { recursive: true });
|
|
49
|
+
const path = join(folder, `${Date.now()}-${subjectSlug(message.subject)}.html`);
|
|
50
|
+
const html = renderEmailHtml({
|
|
51
|
+
subject: message.subject,
|
|
52
|
+
headers: headerRows(message, from),
|
|
53
|
+
body: message.body,
|
|
54
|
+
footer: "Local outbox preview - written to disk, not sent (no SMTP credentials configured).",
|
|
55
|
+
...(brand ? { brand } : {}),
|
|
56
|
+
});
|
|
57
|
+
await writeFile(path, html, "utf8");
|
|
58
|
+
return path;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3V0Ym94LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL291dGJveC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7OztHQWFHO0FBRUgsT0FBTyxFQUFFLEtBQUssRUFBRSxTQUFTLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUNwRCxPQUFPLEVBQUUsSUFBSSxFQUFFLE9BQU8sRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUMxQyxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFHaEQsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLGNBQWMsQ0FBQztBQUUvQzs7O0dBR0c7QUFDSCxNQUFNLHVCQUF1QixHQUFHLEVBQUUsQ0FBQztBQUVuQyw2REFBNkQ7QUFDN0QsU0FBUyxXQUFXLENBQUMsT0FBZTtJQUNsQyxPQUFPLE1BQU0sQ0FBQyxpQkFBaUIsQ0FBQyxFQUFFLFNBQVMsRUFBRSx1QkFBdUIsRUFBRSxFQUFFLE9BQU8sQ0FBQyxJQUFJLE9BQU8sQ0FBQztBQUM5RixDQUFDO0FBRUQsa0VBQWtFO0FBQ2xFLFNBQVMsVUFBVSxDQUFDLE9BQXFCLEVBQUUsSUFBWTtJQUNyRCxNQUFNLElBQUksR0FBaUQ7UUFDekQsQ0FBQyxNQUFNLEVBQUUsSUFBSSxDQUFDO1FBQ2QsQ0FBQyxJQUFJLEVBQUUsT0FBTyxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDN0IsQ0FBQyxJQUFJLEVBQUUsT0FBTyxDQUFDLEVBQUUsRUFBRSxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDOUIsQ0FBQyxLQUFLLEVBQUUsT0FBTyxDQUFDLEdBQUcsRUFBRSxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDaEMsQ0FBQyxTQUFTLEVBQUUsT0FBTyxDQUFDLE9BQU8sQ0FBQztRQUM1QixDQUFDLGFBQWEsRUFBRSxPQUFPLENBQUMsV0FBVyxFQUFFLEdBQUcsQ0FBQyxDQUFDLEdBQUcsRUFBRSxFQUFFLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUMzRSxDQUFDLE1BQU0sRUFBRSxJQUFJLElBQUksRUFBRSxDQUFDLFdBQVcsRUFBRSxDQUFDO0tBQ25DLENBQUM7SUFDRixPQUFPLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQyxHQUFHLEVBQTJCLEVBQUUsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUN4RSxDQUFDO0FBRUQ7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUsZ0JBQWdCLENBQ3BDLE9BQXFCLEVBQ3JCLElBQVksRUFDWixHQUFXLEVBQ1gsS0FBa0I7SUFFbEIsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLEdBQUcsRUFBRSxJQUFJLENBQUMsQ0FBQztJQUNsQyxNQUFNLEtBQUssQ0FBQyxNQUFNLEVBQUUsRUFBRSxTQUFTLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUN6QyxNQUFNLElBQUksR0FBRyxJQUFJLENBQUMsTUFBTSxFQUFFLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxJQUFJLFdBQVcsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQ2hGLE1BQU0sSUFBSSxHQUFHLGVBQWUsQ0FBQztRQUMzQixPQUFPLEVBQUUsT0FBTyxDQUFDLE9BQU87UUFDeEIsT0FBTyxFQUFFLFVBQVUsQ0FBQyxPQUFPLEVBQUUsSUFBSSxDQUFDO1FBQ2xDLElBQUksRUFBRSxPQUFPLENBQUMsSUFBSTtRQUNsQixNQUFNLEVBQUUsb0ZBQW9GO1FBQzVGLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztLQUM1QixDQUFDLENBQUM7SUFDSCxNQUFNLFNBQVMsQ0FBQyxJQUFJLEVBQUUsSUFBSSxFQUFFLE1BQU0sQ0FBQyxDQUFDO0lBQ3BDLE9BQU8sSUFBSSxDQUFDO0FBQ2QsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogRmlsZXN5c3RlbSBvdXRib3ggZm9yIHRoZSBuby1TTVRQIGZhbGxiYWNrOiB7QGxpbmsgd3JpdGVPdXRib3hFbWFpbH1cbiAqIHBlcnNpc3RzIG9uZSBkcmFmdGVkIG1lc3NhZ2UgYXMgYSBzdGFuZGFsb25lIEhUTUwgZmlsZSB1bmRlclxuICogYDxkaXI+Lzxmcm9tPi88dGltZXN0YW1wPi08c3ViamVjdC1zbHVnPi5odG1sYCBpbnN0ZWFkIG9mIHNlbmRpbmcgaXQuXG4gKiBUaGlzIGxldHMgdGhlIGFwcHJvdmFsIGZsb3cgYW5kIGFnZW50cyBiZSBleGVyY2lzZWQgZW5kLXRvLWVuZCB3aXRoXG4gKiB6ZXJvIGVtYWlsIGNvbmZpZ3VyYXRpb24gLSBvcGVuIHRoZSBmaWxlIGluIGEgYnJvd3NlciB0byBzZWUgZXhhY3RseVxuICogd2hhdCB3b3VsZCBoYXZlIGdvbmUgb3V0ICh0aGUgSFRNTCBtYXRjaGVzIHRoZSBTTVRQIHNlbmQsIGhlYWRlcnNcbiAqIHRhYmxlIGFzaWRlKS4gVGhlIGJvZHkgaXMgcmVuZGVyZWQgKyBzdHlsZS1pbmxpbmVkIGJ5XG4gKiB7QGxpbmsgcmVuZGVyRW1haWxIdG1sfS4gQXR0YWNobWVudHMgYXJlIGxpc3RlZCBieSBmaWxlbmFtZSBpbiB0aGVcbiAqIGhlYWRlciB0YWJsZSBidXQgbm90IHdyaXR0ZW4gdG8gZGlzayAtIHRoZSBvdXRib3ggcHJldmlld3MgdGhlXG4gKiBlbnZlbG9wZSwgaXQgZG9lc24ndCByZXByb2R1Y2UgdGhlIHdpcmUgcGF5bG9hZC5cbiAqXG4gKiBAbW9kdWxlXG4gKi9cblxuaW1wb3J0IHsgbWtkaXIsIHdyaXRlRmlsZSB9IGZyb20gXCJub2RlOmZzL3Byb21pc2VzXCI7XG5pbXBvcnQgeyBqb2luLCByZXNvbHZlIH0gZnJvbSBcIm5vZGU6cGF0aFwiO1xuaW1wb3J0IHsgc3RyaW5nIH0gZnJvbSBcIkBkYngtdG9vbHMvc2hhcmVkLWNvcmVcIjtcbmltcG9ydCB0eXBlIHsgRW1haWxNZXNzYWdlIH0gZnJvbSBcIkBkYngtdG9vbHMvc2hhcmVkLWVtYWlsXCI7XG5pbXBvcnQgdHlwZSB7IEVtYWlsQnJhbmQgfSBmcm9tIFwiLi9icmFuZFwiO1xuaW1wb3J0IHsgcmVuZGVyRW1haWxIdG1sIH0gZnJvbSBcIi4vZW1haWwtaHRtbFwiO1xuXG4vKipcbiAqIExvbmdlc3Qgc3ViamVjdCBzbHVnIGtlcHQgaW4gYW4gb3V0Ym94IGZpbGUgbmFtZSwgbGVhdmluZyByb29tIGZvciB0aGVcbiAqIHRpbWVzdGFtcCBwcmVmaXggYW5kIGV4dGVuc2lvbiBpbnNpZGUgY29tbW9uIGZpbGVzeXN0ZW0gbmFtZSBsaW1pdHMuXG4gKi9cbmNvbnN0IFNVQkpFQ1RfU0xVR19NQVhfTEVOR1RIID0gNDg7XG5cbi8qKiBGaWxlc3lzdGVtLXNhZmUgc2x1ZyBvZiB0aGUgc3ViamVjdCBmb3IgdGhlIGZpbGUgbmFtZS4gKi9cbmZ1bmN0aW9uIHN1YmplY3RTbHVnKHN1YmplY3Q6IHN0cmluZyk6IHN0cmluZyB7XG4gIHJldHVybiBzdHJpbmcudG9TbHVnV2l0aE9wdGlvbnMoeyBtYXhMZW5ndGg6IFNVQkpFQ1RfU0xVR19NQVhfTEVOR1RIIH0sIHN1YmplY3QpIHx8IFwiZW1haWxcIjtcbn1cblxuLyoqIFRoZSBlbnZlbG9wZSByb3dzIHNob3duIGFib3ZlIHRoZSBib2R5IGluIHRoZSBwcmV2aWV3IGZpbGUuICovXG5mdW5jdGlvbiBoZWFkZXJSb3dzKG1lc3NhZ2U6IEVtYWlsTWVzc2FnZSwgZnJvbTogc3RyaW5nKTogQXJyYXk8cmVhZG9ubHkgW3N0cmluZywgc3RyaW5nXT4ge1xuICBjb25zdCByb3dzOiBBcnJheTxyZWFkb25seSBbc3RyaW5nLCBzdHJpbmcgfCB1bmRlZmluZWRdPiA9IFtcbiAgICBbXCJGcm9tXCIsIGZyb21dLFxuICAgIFtcIlRvXCIsIG1lc3NhZ2UudG8uam9pbihcIiwgXCIpXSxcbiAgICBbXCJDY1wiLCBtZXNzYWdlLmNjPy5qb2luKFwiLCBcIildLFxuICAgIFtcIkJjY1wiLCBtZXNzYWdlLmJjYz8uam9pbihcIiwgXCIpXSxcbiAgICBbXCJTdWJqZWN0XCIsIG1lc3NhZ2Uuc3ViamVjdF0sXG4gICAgW1wiQXR0YWNobWVudHNcIiwgbWVzc2FnZS5hdHRhY2htZW50cz8ubWFwKChhdHQpID0+IGF0dC5maWxlbmFtZSkuam9pbihcIiwgXCIpXSxcbiAgICBbXCJEYXRlXCIsIG5ldyBEYXRlKCkudG9JU09TdHJpbmcoKV0sXG4gIF07XG4gIHJldHVybiByb3dzLmZpbHRlcigocm93KTogcm93IGlzIFtzdHJpbmcsIHN0cmluZ10gPT4gQm9vbGVhbihyb3dbMV0pKTtcbn1cblxuLyoqXG4gKiBXcml0ZSBvbmUgbWVzc2FnZSBhcyBIVE1MIHVuZGVyIGA8ZGlyPi88ZnJvbT4vYCBhbmQgcmV0dXJuIHRoZVxuICogYWJzb2x1dGUgcGF0aC4gVGhlIHBlci1zZW5kZXIgZm9sZGVyIG1pcnJvcnMgdGhlIFNNVFAgYEZyb21gLCBzbyB0ZXN0XG4gKiBvdXRwdXQgZ3JvdXBzIGJ5IHdobyB0aGUgbWVzc2FnZSB3b3VsZCBoYXZlIGJlZW4gc2VudCBhcy5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHdyaXRlT3V0Ym94RW1haWwoXG4gIG1lc3NhZ2U6IEVtYWlsTWVzc2FnZSxcbiAgZnJvbTogc3RyaW5nLFxuICBkaXI6IHN0cmluZyxcbiAgYnJhbmQ/OiBFbWFpbEJyYW5kLFxuKTogUHJvbWlzZTxzdHJpbmc+IHtcbiAgY29uc3QgZm9sZGVyID0gcmVzb2x2ZShkaXIsIGZyb20pO1xuICBhd2FpdCBta2Rpcihmb2xkZXIsIHsgcmVjdXJzaXZlOiB0cnVlIH0pO1xuICBjb25zdCBwYXRoID0gam9pbihmb2xkZXIsIGAke0RhdGUubm93KCl9LSR7c3ViamVjdFNsdWcobWVzc2FnZS5zdWJqZWN0KX0uaHRtbGApO1xuICBjb25zdCBodG1sID0gcmVuZGVyRW1haWxIdG1sKHtcbiAgICBzdWJqZWN0OiBtZXNzYWdlLnN1YmplY3QsXG4gICAgaGVhZGVyczogaGVhZGVyUm93cyhtZXNzYWdlLCBmcm9tKSxcbiAgICBib2R5OiBtZXNzYWdlLmJvZHksXG4gICAgZm9vdGVyOiBcIkxvY2FsIG91dGJveCBwcmV2aWV3IC0gd3JpdHRlbiB0byBkaXNrLCBub3Qgc2VudCAobm8gU01UUCBjcmVkZW50aWFscyBjb25maWd1cmVkKS5cIixcbiAgICAuLi4oYnJhbmQgPyB7IGJyYW5kIH0gOiB7fSksXG4gIH0pO1xuICBhd2FpdCB3cml0ZUZpbGUocGF0aCwgaHRtbCwgXCJ1dGY4XCIpO1xuICByZXR1cm4gcGF0aDtcbn1cbiJdfQ==
|