@octanejs/email 0.0.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.
- package/LICENSE.react-email +21 -0
- package/README.md +32 -0
- package/package.json +60 -0
- package/src/button-utils.ts +73 -0
- package/src/components.tsrx +368 -0
- package/src/index.ts +6 -0
- package/src/markdown.ts +194 -0
- package/src/render.ts +49 -0
- package/src/rich-content.tsrx +208 -0
- package/src/tailwind/context.ts +33 -0
- package/src/tailwind/index.ts +3 -0
- package/src/tailwind/preset.ts +62 -0
- package/src/tailwind/tailwind.tsrx +15 -0
- package/src/tailwind/transform.ts +485 -0
- package/src/types.ts +30 -0
- package/src/utils.ts +76 -0
package/src/markdown.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { marked, Renderer } from 'marked';
|
|
2
|
+
import type { EmailStyle } from './types.ts';
|
|
3
|
+
|
|
4
|
+
export type MarkdownStyles = Readonly<{
|
|
5
|
+
h1?: EmailStyle;
|
|
6
|
+
h2?: EmailStyle;
|
|
7
|
+
h3?: EmailStyle;
|
|
8
|
+
h4?: EmailStyle;
|
|
9
|
+
h5?: EmailStyle;
|
|
10
|
+
h6?: EmailStyle;
|
|
11
|
+
blockQuote?: EmailStyle;
|
|
12
|
+
bold?: EmailStyle;
|
|
13
|
+
italic?: EmailStyle;
|
|
14
|
+
link?: EmailStyle;
|
|
15
|
+
codeBlock?: EmailStyle;
|
|
16
|
+
codeInline?: EmailStyle;
|
|
17
|
+
p?: EmailStyle;
|
|
18
|
+
li?: EmailStyle;
|
|
19
|
+
ul?: EmailStyle;
|
|
20
|
+
ol?: EmailStyle;
|
|
21
|
+
image?: EmailStyle;
|
|
22
|
+
br?: EmailStyle;
|
|
23
|
+
hr?: EmailStyle;
|
|
24
|
+
table?: EmailStyle;
|
|
25
|
+
thead?: EmailStyle;
|
|
26
|
+
tbody?: EmailStyle;
|
|
27
|
+
tr?: EmailStyle;
|
|
28
|
+
th?: EmailStyle;
|
|
29
|
+
td?: EmailStyle;
|
|
30
|
+
strikethrough?: EmailStyle;
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
const heading = { fontWeight: '500', paddingTop: 20 };
|
|
34
|
+
export const markdownStyles: MarkdownStyles = {
|
|
35
|
+
h1: { ...heading, fontSize: '2.5rem' },
|
|
36
|
+
h2: { ...heading, fontSize: '2rem' },
|
|
37
|
+
h3: { ...heading, fontSize: '1.75rem' },
|
|
38
|
+
h4: { ...heading, fontSize: '1.5rem' },
|
|
39
|
+
h5: { ...heading, fontSize: '1.25rem' },
|
|
40
|
+
h6: { ...heading, fontSize: '1rem' },
|
|
41
|
+
blockQuote: {
|
|
42
|
+
background: '#f9f9f9',
|
|
43
|
+
borderLeft: '10px solid #ccc',
|
|
44
|
+
margin: '1.5em 10px',
|
|
45
|
+
padding: '1em 10px',
|
|
46
|
+
},
|
|
47
|
+
bold: { fontWeight: 'bold' },
|
|
48
|
+
italic: { fontStyle: 'italic' },
|
|
49
|
+
link: { color: '#007bff', textDecoration: 'underline', backgroundColor: 'transparent' },
|
|
50
|
+
codeBlock: {
|
|
51
|
+
color: '#212529',
|
|
52
|
+
fontSize: '87.5%',
|
|
53
|
+
display: 'block',
|
|
54
|
+
background: ' #f8f8f8',
|
|
55
|
+
fontFamily: 'SFMono-Regular,Menlo,Monaco,Consolas,monospace',
|
|
56
|
+
paddingTop: 10,
|
|
57
|
+
paddingRight: 10,
|
|
58
|
+
paddingLeft: 10,
|
|
59
|
+
paddingBottom: 1,
|
|
60
|
+
marginBottom: 20,
|
|
61
|
+
wordWrap: 'break-word',
|
|
62
|
+
},
|
|
63
|
+
codeInline: {
|
|
64
|
+
color: '#212529',
|
|
65
|
+
fontSize: '87.5%',
|
|
66
|
+
display: 'inline',
|
|
67
|
+
background: ' #f8f8f8',
|
|
68
|
+
fontFamily: 'SFMono-Regular,Menlo,Monaco,Consolas,monospace',
|
|
69
|
+
wordWrap: 'break-word',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const pixelProperties = new Set([
|
|
74
|
+
'width',
|
|
75
|
+
'height',
|
|
76
|
+
'margin',
|
|
77
|
+
'marginTop',
|
|
78
|
+
'marginRight',
|
|
79
|
+
'marginBottom',
|
|
80
|
+
'marginLeft',
|
|
81
|
+
'padding',
|
|
82
|
+
'paddingTop',
|
|
83
|
+
'paddingRight',
|
|
84
|
+
'paddingBottom',
|
|
85
|
+
'paddingLeft',
|
|
86
|
+
'borderWidth',
|
|
87
|
+
'borderTopWidth',
|
|
88
|
+
'borderRightWidth',
|
|
89
|
+
'borderBottomWidth',
|
|
90
|
+
'borderLeftWidth',
|
|
91
|
+
'outlineWidth',
|
|
92
|
+
'top',
|
|
93
|
+
'right',
|
|
94
|
+
'bottom',
|
|
95
|
+
'left',
|
|
96
|
+
'fontSize',
|
|
97
|
+
'letterSpacing',
|
|
98
|
+
'wordSpacing',
|
|
99
|
+
'maxWidth',
|
|
100
|
+
'minWidth',
|
|
101
|
+
'maxHeight',
|
|
102
|
+
'minHeight',
|
|
103
|
+
'borderRadius',
|
|
104
|
+
'borderTopLeftRadius',
|
|
105
|
+
'borderTopRightRadius',
|
|
106
|
+
'borderBottomLeftRadius',
|
|
107
|
+
'borderBottomRightRadius',
|
|
108
|
+
'textIndent',
|
|
109
|
+
'gridColumnGap',
|
|
110
|
+
'gridRowGap',
|
|
111
|
+
'gridGap',
|
|
112
|
+
'translateX',
|
|
113
|
+
'translateY',
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
function inlineStyle(style: EmailStyle | undefined): string {
|
|
117
|
+
if (!style) return '';
|
|
118
|
+
return Object.entries(style)
|
|
119
|
+
.map(([property, value]) => {
|
|
120
|
+
const name = property.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
121
|
+
const serialized =
|
|
122
|
+
typeof value === 'number' && pixelProperties.has(property)
|
|
123
|
+
? `${value}px`
|
|
124
|
+
: String(value).replaceAll('"', '"');
|
|
125
|
+
return `${name}:${serialized}`;
|
|
126
|
+
})
|
|
127
|
+
.join(';');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const styleAttribute = (style: EmailStyle | undefined) => {
|
|
131
|
+
const css = inlineStyle(style);
|
|
132
|
+
return css ? ` style="${css}"` : '';
|
|
133
|
+
};
|
|
134
|
+
const styled = (tag: string, style: EmailStyle | undefined) => `${tag}${styleAttribute(style)}`;
|
|
135
|
+
const escapeAttribute = (value: string) => escapeHtml(value);
|
|
136
|
+
|
|
137
|
+
function escapeHtml(value: string): string {
|
|
138
|
+
return value
|
|
139
|
+
.replaceAll('&', '&')
|
|
140
|
+
.replaceAll('<', '<')
|
|
141
|
+
.replaceAll('>', '>')
|
|
142
|
+
.replaceAll('"', '"')
|
|
143
|
+
.replaceAll("'", ''');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function renderMarkdown(source: string, customStyles?: MarkdownStyles): string {
|
|
147
|
+
const finalStyles = { ...markdownStyles, ...customStyles };
|
|
148
|
+
const renderer = new Renderer();
|
|
149
|
+
renderer.blockquote = ({ tokens }) =>
|
|
150
|
+
`<${styled('blockquote', finalStyles.blockQuote)}>\n${renderer.parser.parse(tokens)}</blockquote>\n`;
|
|
151
|
+
renderer.br = () => `<${styled('br', finalStyles.br)} />`;
|
|
152
|
+
renderer.code = ({ text }) =>
|
|
153
|
+
`<${styled('pre', finalStyles.codeBlock)}><code>${escapeHtml(text.replace(/\n$/, ''))}\n</code></pre>\n`;
|
|
154
|
+
renderer.codespan = ({ text }) =>
|
|
155
|
+
`<${styled('code', finalStyles.codeInline)}>${escapeHtml(text)}</code>`;
|
|
156
|
+
renderer.del = ({ tokens }) =>
|
|
157
|
+
`<${styled('del', finalStyles.strikethrough)}>${renderer.parser.parseInline(tokens)}</del>`;
|
|
158
|
+
renderer.em = ({ tokens }) =>
|
|
159
|
+
`<${styled('em', finalStyles.italic)}>${renderer.parser.parseInline(tokens)}</em>`;
|
|
160
|
+
renderer.heading = ({ tokens, depth }) =>
|
|
161
|
+
`<${styled(`h${depth}`, finalStyles[`h${depth}` as keyof MarkdownStyles])}>${renderer.parser.parseInline(tokens)}</h${depth}>`;
|
|
162
|
+
renderer.hr = () => `<${styled('hr', finalStyles.hr)} />\n`;
|
|
163
|
+
renderer.image = ({ href, text, title }) =>
|
|
164
|
+
`<img src="${escapeAttribute(href)}" alt="${escapeAttribute(text)}"${title ? ` title="${escapeAttribute(title)}"` : ''}${styleAttribute(finalStyles.image)}>`;
|
|
165
|
+
renderer.link = ({ href, title, tokens }) =>
|
|
166
|
+
`<a href="${escapeAttribute(href)}" target="_blank"${title ? ` title="${escapeAttribute(title)}"` : ''}${styleAttribute(finalStyles.link)}>${renderer.parser.parseInline(tokens)}</a>`;
|
|
167
|
+
renderer.listitem = ({ tokens, loose }) =>
|
|
168
|
+
`<${styled('li', finalStyles.li)}>${loose || tokens.some((token) => token.type === 'list') ? renderer.parser.parse(tokens) : renderer.parser.parseInline(tokens)}</li>\n`;
|
|
169
|
+
renderer.list = ({ items, ordered, start }) => {
|
|
170
|
+
const tag = ordered ? 'ol' : 'ul';
|
|
171
|
+
return `<${styled(tag + (ordered && start !== 1 ? ` start="${start}"` : ''), finalStyles[tag as 'ol' | 'ul'])}>\n${items.map((item) => renderer.listitem(item)).join('')}</${tag}>\n`;
|
|
172
|
+
};
|
|
173
|
+
renderer.paragraph = ({ tokens }) =>
|
|
174
|
+
`<${styled('p', finalStyles.p)}>${renderer.parser.parseInline(tokens)}</p>\n`;
|
|
175
|
+
renderer.strong = ({ tokens }) =>
|
|
176
|
+
`<${styled('strong', finalStyles.bold)}>${renderer.parser.parseInline(tokens)}</strong>`;
|
|
177
|
+
renderer.table = ({ header, rows }) => {
|
|
178
|
+
const headRow = renderer.tablerow({
|
|
179
|
+
text: header.map((cell) => renderer.tablecell(cell)).join(''),
|
|
180
|
+
});
|
|
181
|
+
const bodyRows = rows
|
|
182
|
+
.map((row) =>
|
|
183
|
+
renderer.tablerow({ text: row.map((cell) => renderer.tablecell(cell)).join('') }),
|
|
184
|
+
)
|
|
185
|
+
.join('');
|
|
186
|
+
return `<${styled('table role="presentation"', finalStyles.table)}>\n<${styled('thead', finalStyles.thead)}>\n${headRow}</thead>\n<${styled('tbody', finalStyles.tbody)}>${bodyRows}</tbody></table>\n`;
|
|
187
|
+
};
|
|
188
|
+
renderer.tablecell = ({ tokens, align, header }) => {
|
|
189
|
+
const tag = header ? 'th' : 'td';
|
|
190
|
+
return `<${styled(`${tag}${align ? ` align="${align}"` : ''}`, finalStyles[tag])}>${renderer.parser.parseInline(tokens)}</${tag}>\n`;
|
|
191
|
+
};
|
|
192
|
+
renderer.tablerow = ({ text }) => `<${styled('tr', finalStyles.tr)}>\n${text}</tr>\n`;
|
|
193
|
+
return marked.parse(source, { renderer, async: false });
|
|
194
|
+
}
|
package/src/render.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'octane/server';
|
|
2
|
+
import { renderWithTailwind } from './tailwind/index.ts';
|
|
3
|
+
|
|
4
|
+
export interface RenderOptions {
|
|
5
|
+
pretty?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type EmailComponent<Props> = (props: Props) => unknown;
|
|
9
|
+
|
|
10
|
+
const DOCTYPE =
|
|
11
|
+
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
|
|
12
|
+
|
|
13
|
+
const HEAD_BOUNDARY = /<body[^>]*data-octane-email-head=["']head["'][^>]*>([\s\S]*?)<\/body>/gi;
|
|
14
|
+
|
|
15
|
+
/** Render an Octane email component to non-hydratable email HTML. */
|
|
16
|
+
export async function render<Props>(
|
|
17
|
+
component: EmailComponent<Props>,
|
|
18
|
+
props?: Props,
|
|
19
|
+
options?: RenderOptions,
|
|
20
|
+
): Promise<string> {
|
|
21
|
+
const document = await renderWithTailwind(() => {
|
|
22
|
+
const { html, css } = renderToStaticMarkup(component, props);
|
|
23
|
+
const cleanHtml = html.replace(/<!DOCTYPE.*?>/i, '');
|
|
24
|
+
let authoredHead = '';
|
|
25
|
+
const bodyHtml = cleanHtml.replace(HEAD_BOUNDARY, (_boundary, content: string) => {
|
|
26
|
+
authoredHead += content;
|
|
27
|
+
return '';
|
|
28
|
+
});
|
|
29
|
+
const htmlStart = bodyHtml.indexOf('<html');
|
|
30
|
+
let documentHtml = bodyHtml;
|
|
31
|
+
if (htmlStart >= 0) {
|
|
32
|
+
const hoistedHead = bodyHtml.slice(0, htmlStart);
|
|
33
|
+
const root = bodyHtml.slice(htmlStart);
|
|
34
|
+
const head = `${hoistedHead}${authoredHead}${css}`;
|
|
35
|
+
if (head.length > 0) {
|
|
36
|
+
const openingEnd = root.indexOf('>') + 1;
|
|
37
|
+
documentHtml = `${root.slice(0, openingEnd)}<head>${head}</head>${root.slice(openingEnd)}`;
|
|
38
|
+
}
|
|
39
|
+
} else if (authoredHead.length > 0 || css.length > 0) {
|
|
40
|
+
documentHtml = bodyHtml.replace('<head>', `<head>${authoredHead}${css}`);
|
|
41
|
+
}
|
|
42
|
+
return `${DOCTYPE}${documentHtml}`;
|
|
43
|
+
});
|
|
44
|
+
return options?.pretty ? formatEmailHtml(document) : document;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatEmailHtml(html: string): string {
|
|
48
|
+
return html.replace(/></g, '>\n<');
|
|
49
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { isChildrenBlock, type OctaneNode } from 'octane';
|
|
2
|
+
import Prism from 'prismjs';
|
|
3
|
+
import loadLanguages from 'prismjs/components/index.js';
|
|
4
|
+
import { renderMarkdown, type MarkdownStyles } from './markdown.ts';
|
|
5
|
+
import type { EmailElementProps, EmailStyle } from './types.ts';
|
|
6
|
+
|
|
7
|
+
loadLanguages();
|
|
8
|
+
|
|
9
|
+
function resolveTextChild(child: unknown): string {
|
|
10
|
+
let value = child;
|
|
11
|
+
if (typeof value === 'function' && !isChildrenBlock(value)) {
|
|
12
|
+
value = (value as () => unknown)();
|
|
13
|
+
}
|
|
14
|
+
if (typeof value === 'string') return value;
|
|
15
|
+
if (isChildrenBlock(child)) {
|
|
16
|
+
throw new Error('Markdown children must be a string prop: use <Markdown children={"…"} />. ' +
|
|
17
|
+
'JSX children compile to a children block, which is not a string.');
|
|
18
|
+
}
|
|
19
|
+
throw new Error('Markdown children must resolve to a string');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type MarkdownProps =
|
|
23
|
+
Readonly<Omit<EmailElementProps<HTMLDivElement>, 'children' | 'style'>> & Readonly<{
|
|
24
|
+
children: string;
|
|
25
|
+
markdownCustomStyles?: MarkdownStyles;
|
|
26
|
+
markdownContainerStyles?: EmailStyle;
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
export function Markdown(props: MarkdownProps) @{
|
|
30
|
+
const {
|
|
31
|
+
children: _children,
|
|
32
|
+
markdownContainerStyles,
|
|
33
|
+
markdownCustomStyles,
|
|
34
|
+
ref,
|
|
35
|
+
...rest
|
|
36
|
+
} = props;
|
|
37
|
+
<div
|
|
38
|
+
{...rest}
|
|
39
|
+
{...(ref === undefined ? {} : { ref })}
|
|
40
|
+
data-id="react-email-markdown"
|
|
41
|
+
style={markdownContainerStyles}
|
|
42
|
+
dangerouslySetInnerHTML={{
|
|
43
|
+
__html: renderMarkdown(resolveTextChild(props.children), markdownCustomStyles),
|
|
44
|
+
}}
|
|
45
|
+
/>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type PrismLanguage = keyof typeof Prism.languages;
|
|
49
|
+
export type Theme = Record<string, EmailStyle | undefined> & { base?: EmailStyle };
|
|
50
|
+
|
|
51
|
+
export interface CodeBlockProps extends EmailElementProps<HTMLPreElement> {
|
|
52
|
+
lineNumbers?: boolean;
|
|
53
|
+
fontFamily?: string;
|
|
54
|
+
theme: Theme;
|
|
55
|
+
language: PrismLanguage;
|
|
56
|
+
code: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type Token = string | Prism.Token;
|
|
60
|
+
|
|
61
|
+
function tokenStyle(token: Prism.Token, theme: Theme): EmailStyle {
|
|
62
|
+
let style = { ...theme[token.type] };
|
|
63
|
+
const aliases = Array.isArray(token.alias) ? token.alias : [token.alias];
|
|
64
|
+
for (const alias of aliases) if (alias) style = { ...style, ...theme[alias] };
|
|
65
|
+
return style;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function CodeToken({
|
|
69
|
+
token,
|
|
70
|
+
theme,
|
|
71
|
+
inheritedStyle,
|
|
72
|
+
}: {
|
|
73
|
+
token: Token;
|
|
74
|
+
theme: Theme;
|
|
75
|
+
inheritedStyle?: EmailStyle;
|
|
76
|
+
}): OctaneNode {
|
|
77
|
+
if (typeof token === 'string') return <span style={inheritedStyle}>{token.replaceAll(
|
|
78
|
+
' ',
|
|
79
|
+
' ',
|
|
80
|
+
)}</span>;
|
|
81
|
+
const style = { ...inheritedStyle, ...tokenStyle(token, theme) };
|
|
82
|
+
if (Array.isArray(token.content)) return <>
|
|
83
|
+
@for (const child of token.content; index childIndex; key childIndex) {
|
|
84
|
+
<CodeToken token={child} theme={theme} inheritedStyle={style} />
|
|
85
|
+
}
|
|
86
|
+
</>;
|
|
87
|
+
return <span style={style}>
|
|
88
|
+
{typeof token.content === 'string'
|
|
89
|
+
? token.content
|
|
90
|
+
: <CodeToken token={token.content} theme={theme} inheritedStyle={style} />}
|
|
91
|
+
</span>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function CodeLine({
|
|
95
|
+
tokens,
|
|
96
|
+
lineNumber,
|
|
97
|
+
lineNumbers,
|
|
98
|
+
fontFamily,
|
|
99
|
+
theme,
|
|
100
|
+
}: {
|
|
101
|
+
tokens: Token[];
|
|
102
|
+
lineNumber: number;
|
|
103
|
+
lineNumbers: boolean;
|
|
104
|
+
fontFamily?: string;
|
|
105
|
+
theme: Theme;
|
|
106
|
+
}) @{
|
|
107
|
+
<>
|
|
108
|
+
@if (lineNumbers) {
|
|
109
|
+
<span
|
|
110
|
+
style={{ width: '2em', height: '1em', display: 'inline-block', fontFamily }}
|
|
111
|
+
>{lineNumber}</span>
|
|
112
|
+
}
|
|
113
|
+
@for (const [tokenIndex, token] of tokens.entries(); key tokenIndex) {
|
|
114
|
+
<CodeToken token={token} theme={theme} inheritedStyle={{ fontFamily }} />
|
|
115
|
+
}
|
|
116
|
+
<br />
|
|
117
|
+
</>
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function CodeBlock({
|
|
121
|
+
code,
|
|
122
|
+
fontFamily,
|
|
123
|
+
lineNumbers = false,
|
|
124
|
+
theme,
|
|
125
|
+
language,
|
|
126
|
+
style,
|
|
127
|
+
ref,
|
|
128
|
+
...props
|
|
129
|
+
}: CodeBlockProps) @{
|
|
130
|
+
const grammar = Prism.languages[language];
|
|
131
|
+
if (
|
|
132
|
+
grammar === undefined
|
|
133
|
+
) throw new Error(`CodeBlock: There is no language defined on Prism called ${String(language)}`);
|
|
134
|
+
const lines = code.split(/\r\n|\r|\n/gm).map((line) => Prism.tokenize(line, grammar));
|
|
135
|
+
<pre
|
|
136
|
+
{...props}
|
|
137
|
+
{...(ref === undefined ? {} : { ref })}
|
|
138
|
+
style={{ ...theme.base, width: '100%', ...style }}
|
|
139
|
+
>
|
|
140
|
+
<code>
|
|
141
|
+
@for (const [lineIndex, tokens] of lines.entries(); key lineIndex) {
|
|
142
|
+
<CodeLine
|
|
143
|
+
tokens={tokens}
|
|
144
|
+
lineNumber={lineIndex + 1}
|
|
145
|
+
lineNumbers={lineNumbers}
|
|
146
|
+
fontFamily={fontFamily}
|
|
147
|
+
theme={theme}
|
|
148
|
+
/>
|
|
149
|
+
}
|
|
150
|
+
</code>
|
|
151
|
+
</pre>
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const vscDarkPlus: Theme = {
|
|
155
|
+
base: {
|
|
156
|
+
color: '#d4d4d4',
|
|
157
|
+
fontSize: '13px',
|
|
158
|
+
textShadow: 'none',
|
|
159
|
+
fontFamily: 'Menlo, Monaco, Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace',
|
|
160
|
+
direction: 'ltr',
|
|
161
|
+
textAlign: 'left',
|
|
162
|
+
whiteSpace: 'pre',
|
|
163
|
+
wordSpacing: 'normal',
|
|
164
|
+
wordBreak: 'normal',
|
|
165
|
+
lineHeight: '1.5',
|
|
166
|
+
tabSize: '4',
|
|
167
|
+
hyphens: 'none',
|
|
168
|
+
padding: '1em',
|
|
169
|
+
margin: '.5em 0',
|
|
170
|
+
overflow: 'auto',
|
|
171
|
+
background: '#1e1e1e',
|
|
172
|
+
},
|
|
173
|
+
comment: { color: '#6a9955' },
|
|
174
|
+
keyword: { color: '#569cd6' },
|
|
175
|
+
boolean: { color: '#569cd6' },
|
|
176
|
+
number: { color: '#b5cea8' },
|
|
177
|
+
string: { color: '#ce9178' },
|
|
178
|
+
operator: { color: '#d4d4d4' },
|
|
179
|
+
punctuation: { color: '#d4d4d4' },
|
|
180
|
+
function: { color: '#dcdcaa' },
|
|
181
|
+
'class-name': { color: '#4ec9b0' },
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export const xonokai: Theme = {
|
|
185
|
+
base: {
|
|
186
|
+
tabSize: '2',
|
|
187
|
+
hyphens: 'none',
|
|
188
|
+
whiteSpace: 'pre-wrap',
|
|
189
|
+
wordWrap: 'normal',
|
|
190
|
+
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
|
191
|
+
fontSize: '14px',
|
|
192
|
+
color: '#76d9e6',
|
|
193
|
+
textShadow: 'none',
|
|
194
|
+
background: '#2a2a2a',
|
|
195
|
+
padding: '15px',
|
|
196
|
+
borderRadius: '4px',
|
|
197
|
+
border: '1px solid #e1e1e8',
|
|
198
|
+
overflow: 'auto',
|
|
199
|
+
position: 'relative',
|
|
200
|
+
},
|
|
201
|
+
comment: { color: '#6f705e' },
|
|
202
|
+
operator: { color: '#a77afe' },
|
|
203
|
+
boolean: { color: '#a77afe' },
|
|
204
|
+
number: { color: '#a77afe' },
|
|
205
|
+
string: { color: '#e6d06c' },
|
|
206
|
+
keyword: { color: '#ef3b7d' },
|
|
207
|
+
punctuation: { color: '#bebec5' },
|
|
208
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import type { TailwindConfig } from './preset.ts';
|
|
3
|
+
|
|
4
|
+
export interface TailwindBoundaryOptions {
|
|
5
|
+
config?: TailwindConfig;
|
|
6
|
+
theme?: string;
|
|
7
|
+
utility?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface TailwindCollection {
|
|
11
|
+
nextId: number;
|
|
12
|
+
boundaries: Map<string, TailwindBoundaryOptions>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const collections = new AsyncLocalStorage<TailwindCollection>();
|
|
16
|
+
|
|
17
|
+
export function registerTailwindBoundary(options: TailwindBoundaryOptions): string {
|
|
18
|
+
const collection = collections.getStore();
|
|
19
|
+
if (!collection) {
|
|
20
|
+
throw new Error('Tailwind must be rendered through the react-email render() pipeline.');
|
|
21
|
+
}
|
|
22
|
+
const id = `tw-${collection.nextId++}`;
|
|
23
|
+
collection.boundaries.set(id, options);
|
|
24
|
+
return id;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function collectTailwindBoundaries<T>(
|
|
28
|
+
render: () => T | Promise<T>,
|
|
29
|
+
): Promise<{ value: T; boundaries: Map<string, TailwindBoundaryOptions> }> {
|
|
30
|
+
const collection: TailwindCollection = { nextId: 0, boundaries: new Map() };
|
|
31
|
+
const value = await collections.run(collection, render);
|
|
32
|
+
return { value, boundaries: collection.boundaries };
|
|
33
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Config } from 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
export type TailwindConfig = Omit<Config, 'content'>;
|
|
4
|
+
|
|
5
|
+
export const pixelBasedPreset: TailwindConfig = {
|
|
6
|
+
theme: {
|
|
7
|
+
extend: {
|
|
8
|
+
fontSize: {
|
|
9
|
+
xs: ['12px', { lineHeight: '16px' }],
|
|
10
|
+
sm: ['14px', { lineHeight: '20px' }],
|
|
11
|
+
base: ['16px', { lineHeight: '24px' }],
|
|
12
|
+
lg: ['18px', { lineHeight: '28px' }],
|
|
13
|
+
xl: ['20px', { lineHeight: '28px' }],
|
|
14
|
+
'2xl': ['24px', { lineHeight: '32px' }],
|
|
15
|
+
'3xl': ['30px', { lineHeight: '36px' }],
|
|
16
|
+
'4xl': ['36px', { lineHeight: '36px' }],
|
|
17
|
+
'5xl': ['48px', { lineHeight: '1' }],
|
|
18
|
+
'6xl': ['60px', { lineHeight: '1' }],
|
|
19
|
+
'7xl': ['72px', { lineHeight: '1' }],
|
|
20
|
+
'8xl': ['96px', { lineHeight: '1' }],
|
|
21
|
+
'9xl': ['144px', { lineHeight: '1' }],
|
|
22
|
+
},
|
|
23
|
+
spacing: {
|
|
24
|
+
px: '1px',
|
|
25
|
+
0: '0',
|
|
26
|
+
0.5: '2px',
|
|
27
|
+
1: '4px',
|
|
28
|
+
1.5: '6px',
|
|
29
|
+
2: '8px',
|
|
30
|
+
2.5: '10px',
|
|
31
|
+
3: '12px',
|
|
32
|
+
3.5: '14px',
|
|
33
|
+
4: '16px',
|
|
34
|
+
5: '20px',
|
|
35
|
+
6: '24px',
|
|
36
|
+
7: '28px',
|
|
37
|
+
8: '32px',
|
|
38
|
+
9: '36px',
|
|
39
|
+
10: '40px',
|
|
40
|
+
11: '44px',
|
|
41
|
+
12: '48px',
|
|
42
|
+
14: '56px',
|
|
43
|
+
16: '64px',
|
|
44
|
+
20: '80px',
|
|
45
|
+
24: '96px',
|
|
46
|
+
28: '112px',
|
|
47
|
+
32: '128px',
|
|
48
|
+
36: '144px',
|
|
49
|
+
40: '160px',
|
|
50
|
+
44: '176px',
|
|
51
|
+
48: '192px',
|
|
52
|
+
52: '208px',
|
|
53
|
+
56: '224px',
|
|
54
|
+
60: '240px',
|
|
55
|
+
64: '256px',
|
|
56
|
+
72: '288px',
|
|
57
|
+
80: '320px',
|
|
58
|
+
96: '384px',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { OctaneNode } from 'octane';
|
|
2
|
+
import { registerTailwindBoundary, type TailwindBoundaryOptions } from './context.ts';
|
|
3
|
+
|
|
4
|
+
export interface TailwindProps extends TailwindBoundaryOptions {
|
|
5
|
+
children: OctaneNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Tailwind({ children, config, theme, utility }: TailwindProps) @{
|
|
9
|
+
const id = registerTailwindBoundary({ config, theme, utility });
|
|
10
|
+
<>
|
|
11
|
+
<template data-octane-email-tailwind-start={id} />
|
|
12
|
+
{children}
|
|
13
|
+
<template data-octane-email-tailwind-end={id} />
|
|
14
|
+
</>
|
|
15
|
+
}
|