@bkazemi/verily 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { Refused } from '../core/index.js';
2
2
  import { emailLogo } from './email-logo.js';
3
- import { escape } from './escape.js';
3
+ import { html, raw } from './html.js';
4
4
  /** The logotype as the message carries it. */
5
5
  const logo = {
6
6
  contentId: 'verily-logo',
@@ -71,51 +71,139 @@ export function emailProvider(options) {
71
71
  * otherwise push the code that far off centre.
72
72
  */
73
73
  function letter(parts) {
74
- const sans = "system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif";
75
- const mono = "ui-monospace,SFMono-Regular,Menlo,Consolas,'Liberation Mono',monospace";
76
- return `<!doctype html>
77
- <html lang="en">
78
- <head>
79
- <meta charset="utf-8">
80
- <meta name="viewport" content="width=device-width,initial-scale=1">
81
- <meta name="color-scheme" content="light dark">
82
- <meta name="supported-color-schemes" content="light dark">
83
- <title>Verily</title>
84
- <style>
85
- @media (prefers-color-scheme: dark) {
86
- .page { background: #161a18 !important; }
87
- .ink { color: #dde5e0 !important; }
88
- .muted { color: #96a39b !important; }
89
- .button { background: #7fc4a2 !important; }
90
- .button a { color: #10231a !important; }
91
- .code { background: #1d2220 !important; border-color: #2f3b35 !important; color: #dde5e0 !important; }
92
- .rule { border-color: #2f3b35 !important; }
93
- }
94
- </style>
95
- </head>
96
- <body class="page" style="margin:0;padding:0;background:#ffffff;">
97
- <div style="display:none;max-height:0;overflow:hidden;opacity:0;">${escape(parts.ask)} ${escape(parts.lasts)}</div>
98
- <table role="presentation" class="page" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#ffffff;">
99
- <tr><td align="center" style="padding:40px 16px 48px;">
100
- <table role="presentation" align="center" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:440px;margin:0 auto;">
101
- <tr><td align="center" style="padding:0 0 28px;">
102
- <img src="cid:${logo.contentId}" width="94" height="40" alt="Verily" style="display:block;margin:0 auto;border:0;outline:none;font:700 20px/40px ${sans};color:#23312b;">
103
- </td></tr>
104
- <tr><td class="ink" align="center" style="padding:0 0 10px;font:650 22px/1.3 ${sans};color:#23312b;text-align:center;">Confirm your address</td></tr>
105
- <tr><td class="ink" align="center" style="padding:0 0 24px;font:16px/1.6 ${sans};color:#23312b;text-align:center;">${escape(parts.ask)}</td></tr>
106
- <tr><td align="center" style="padding:0 0 28px;">
107
- <table role="presentation" align="center" cellpadding="0" cellspacing="0" border="0" style="margin:0 auto;"><tr><td class="button" align="center" style="background:#245f43;border-radius:10px;"><a href="${escape(parts.link)}" style="display:inline-block;padding:13px 26px;font:600 16px/1.2 ${sans};color:#ffffff;text-decoration:none;">${escape(parts.button)}</a></td></tr></table>
108
- </td></tr>
109
- <tr><td class="muted" align="center" style="padding:0 0 10px;font:14px/1.5 ${sans};color:#6b786f;text-align:center;">${escape(parts.enter)}</td></tr>
110
- <tr><td class="code" align="center" style="padding:14px 12px 14px 15px;background:#f7f9f7;border:1px solid #dce2de;border-radius:10px;font:600 22px/1.2 ${mono};letter-spacing:.14em;color:#23312b;text-align:center;">${escape(parts.code)}</td></tr>
111
- <tr><td class="muted" align="center" style="padding:12px 0 28px;font:14px/1.5 ${sans};color:#6b786f;text-align:center;">${escape(parts.lasts)}</td></tr>
112
- <tr><td class="rule" style="border-top:1px solid #dce2de;font-size:0;line-height:0;">&nbsp;</td></tr>
113
- <tr><td class="muted" align="center" style="padding:18px 0 0;font:13px/1.6 ${sans};color:#6b786f;text-align:center;">${escape(parts.ignore)}</td></tr>
114
- </table>
115
- </td></tr>
116
- </table>
117
- </body>
118
- </html>`;
74
+ // Constants, let in as written so the quotes in them stay quotes.
75
+ const sans = raw("system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif");
76
+ const mono = raw("ui-monospace,SFMono-Regular,Menlo,Consolas,'Liberation Mono',monospace");
77
+ return html `
78
+ <!doctype html>
79
+ <html lang="en">
80
+ <head>
81
+ <meta charset="utf-8">
82
+ <meta name="viewport" content="width=device-width,initial-scale=1">
83
+ <meta name="color-scheme" content="light dark">
84
+ <meta name="supported-color-schemes" content="light dark">
85
+ <title>Verily</title>
86
+ <style>
87
+ @media (prefers-color-scheme: dark) {
88
+ .page { background: #161a18 !important; }
89
+ .ink { color: #dde5e0 !important; }
90
+ .muted { color: #96a39b !important; }
91
+ .button { background: #7fc4a2 !important; }
92
+ .button a { color: #10231a !important; }
93
+ .code { background: #1d2220 !important; border-color: #2f3b35 !important; color: #dde5e0 !important; }
94
+ .rule { border-color: #2f3b35 !important; }
95
+ }
96
+ </style>
97
+ </head>
98
+ <body class="page" style="margin:0;padding:0;background:#ffffff;">
99
+ <div style="display:none;max-height:0;overflow:hidden;opacity:0;">${parts.ask} ${parts.lasts}</div>
100
+ <table
101
+ role="presentation"
102
+ class="page"
103
+ width="100%"
104
+ cellpadding="0"
105
+ cellspacing="0"
106
+ border="0"
107
+ style="background:#ffffff;">
108
+ <tr>
109
+ <td align="center" style="padding:40px 16px 48px;">
110
+ <table
111
+ role="presentation"
112
+ align="center"
113
+ width="100%"
114
+ cellpadding="0"
115
+ cellspacing="0"
116
+ border="0"
117
+ style="max-width:440px;margin:0 auto;">
118
+ <tr>
119
+ <td align="center" style="padding:0 0 28px;">
120
+ <img
121
+ src="cid:${logo.contentId}"
122
+ width="94"
123
+ height="40"
124
+ alt="Verily"
125
+ style="display:block;margin:0 auto;border:0;outline:none;font:700 20px/40px ${sans};color:#23312b;">
126
+ </td>
127
+ </tr>
128
+ <tr>
129
+ <td
130
+ class="ink"
131
+ align="center"
132
+ style="padding:0 0 10px;font:650 22px/1.3 ${sans};color:#23312b;text-align:center;">
133
+ Confirm your address
134
+ </td>
135
+ </tr>
136
+ <tr>
137
+ <td
138
+ class="ink"
139
+ align="center"
140
+ style="padding:0 0 24px;font:16px/1.6 ${sans};color:#23312b;text-align:center;">
141
+ ${parts.ask}
142
+ </td>
143
+ </tr>
144
+ <tr>
145
+ <td align="center" style="padding:0 0 28px;">
146
+ <table
147
+ role="presentation"
148
+ align="center"
149
+ cellpadding="0"
150
+ cellspacing="0"
151
+ border="0"
152
+ style="margin:0 auto;">
153
+ <tr>
154
+ <td class="button" align="center" style="background:#245f43;border-radius:10px;">
155
+ <a
156
+ href="${parts.link}"
157
+ style="display:inline-block;padding:13px 26px;font:600 16px/1.2 ${sans};color:#ffffff;text-decoration:none;">
158
+ ${parts.button}
159
+ </a>
160
+ </td>
161
+ </tr>
162
+ </table>
163
+ </td>
164
+ </tr>
165
+ <tr>
166
+ <td
167
+ class="muted"
168
+ align="center"
169
+ style="padding:0 0 10px;font:14px/1.5 ${sans};color:#6b786f;text-align:center;">
170
+ ${parts.enter}
171
+ </td>
172
+ </tr>
173
+ <tr>
174
+ <td
175
+ class="code"
176
+ align="center"
177
+ style="padding:14px 12px 14px 15px;background:#f7f9f7;border:1px solid #dce2de;border-radius:10px;font:600 22px/1.2 ${mono};letter-spacing:.14em;color:#23312b;text-align:center;">
178
+ ${parts.code}
179
+ </td>
180
+ </tr>
181
+ <tr>
182
+ <td
183
+ class="muted"
184
+ align="center"
185
+ style="padding:12px 0 28px;font:14px/1.5 ${sans};color:#6b786f;text-align:center;">
186
+ ${parts.lasts}
187
+ </td>
188
+ </tr>
189
+ <tr>
190
+ <td class="rule" style="border-top:1px solid #dce2de;font-size:0;line-height:0;">&nbsp;</td>
191
+ </tr>
192
+ <tr>
193
+ <td
194
+ class="muted"
195
+ align="center"
196
+ style="padding:18px 0 0;font:13px/1.6 ${sans};color:#6b786f;text-align:center;">
197
+ ${parts.ignore}
198
+ </td>
199
+ </tr>
200
+ </table>
201
+ </td>
202
+ </tr>
203
+ </table>
204
+ </body>
205
+ </html>
206
+ `.markup;
119
207
  }
120
208
  /**
121
209
  * The mailbox an address names. Lowercased throughout: the part before the @ may be case
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Markup that is safe to put in a page as it is: built by `html`, or let through by
3
+ * `raw()`. Everything else that goes into a page is text, and is escaped.
4
+ */
5
+ export declare class Html {
6
+ readonly markup: string;
7
+ constructor(markup: string);
8
+ toString(): string;
9
+ }
10
+ /**
11
+ * What can go inside `html`. Text and numbers are escaped. `Html` goes in as it is, and a
12
+ * list goes in as each of its items in turn. Nothing, false and null leave nothing, so a
13
+ * part that is only there sometimes can be written as `${shown && html`...`}`.
14
+ */
15
+ export type Content = Html | string | number | false | null | undefined | readonly Content[];
16
+ /**
17
+ * Markup, or nothing: what a parameter takes that must not be given text. An empty string
18
+ * is allowed, since it holds nothing that could be read as markup.
19
+ */
20
+ export type OptionalHtml = Html | '' | false | null | undefined;
21
+ /**
22
+ * Builds markup from a template, escaping every value put into it that is not markup
23
+ * already. This covers element text and quoted attributes. It does not make a URL safe to
24
+ * follow: pass an address through `safeUrl()` before it goes into `href`.
25
+ *
26
+ * The template can be written indented across lines: see `unindent` for how its line
27
+ * breaks are taken out. Values are never changed.
28
+ */
29
+ export declare function html(template: TemplateStringsArray, ...values: Content[]): Html;
30
+ /**
31
+ * Markup let into a page unescaped. Only for markup this code wrote itself, such as the
32
+ * logo, and never for anything a request or a record supplied.
33
+ */
34
+ export declare const raw: (markup: string) => Html;
@@ -0,0 +1,67 @@
1
+ import { escape } from './escape.js';
2
+ /**
3
+ * Markup that is safe to put in a page as it is: built by `html`, or let through by
4
+ * `raw()`. Everything else that goes into a page is text, and is escaped.
5
+ */
6
+ export class Html {
7
+ markup;
8
+ constructor(markup) {
9
+ this.markup = markup;
10
+ }
11
+ toString() {
12
+ return this.markup;
13
+ }
14
+ }
15
+ const write = (content) => content instanceof Html
16
+ ? content.markup
17
+ : Array.isArray(content)
18
+ ? content.map(write).join('')
19
+ : content === undefined || content === null || content === false
20
+ ? ''
21
+ : escape(content);
22
+ /**
23
+ * One piece of a template's own text, without the line breaks and indentation it was
24
+ * written with. Whitespace that holds a line break is dropped where it touches a tag or
25
+ * either end of the template, or lies between two values. Anywhere else, between words or
26
+ * between a word and a value, it becomes one space.
27
+ *
28
+ * `first` and `last` say whether the piece starts or ends the template; otherwise a value
29
+ * comes before or after it.
30
+ */
31
+ function unindent(text, first, last) {
32
+ return text.replace(/\s*\n\s*/g, (run, at) => {
33
+ const before = at === 0 ? (first ? '' : 'value') : text[at - 1];
34
+ const after = at + run.length === text.length ? (last ? '' : 'value') : text[at + run.length];
35
+ return before === '>' || after === '<' || !before || !after
36
+ ? ''
37
+ : before === 'value' && after === 'value'
38
+ ? ''
39
+ : ' ';
40
+ });
41
+ }
42
+ /** Each template's text, unindented once. The same template is the same array every call. */
43
+ const unindented = new WeakMap();
44
+ /**
45
+ * Builds markup from a template, escaping every value put into it that is not markup
46
+ * already. This covers element text and quoted attributes. It does not make a URL safe to
47
+ * follow: pass an address through `safeUrl()` before it goes into `href`.
48
+ *
49
+ * The template can be written indented across lines: see `unindent` for how its line
50
+ * breaks are taken out. Values are never changed.
51
+ */
52
+ export function html(template, ...values) {
53
+ let strings = unindented.get(template);
54
+ if (!strings) {
55
+ strings = template.map((text, i) => unindent(text, i === 0, i === template.length - 1));
56
+ unindented.set(template, strings);
57
+ }
58
+ let markup = strings[0];
59
+ for (const [index, value] of values.entries())
60
+ markup += write(value) + strings[index + 1];
61
+ return new Html(markup);
62
+ }
63
+ /**
64
+ * Markup let into a page unescaped. Only for markup this code wrote itself, such as the
65
+ * logo, and never for anything a request or a record supplied.
66
+ */
67
+ export const raw = (markup) => new Html(markup);