@travetto/email-inky 3.1.17
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 +21 -0
- package/README.md +52 -0
- package/__index__.ts +2 -0
- package/package.json +52 -0
- package/resources/email/inky.wrapper.html +19 -0
- package/src/components.ts +55 -0
- package/src/render/context.ts +20 -0
- package/src/render/html.ts +389 -0
- package/src/render/markdown.ts +58 -0
- package/src/render/renderer.ts +93 -0
- package/src/render/subject.ts +30 -0
- package/src/types.ts +31 -0
- package/src/wrapper.ts +26 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 ArcSine Technologies
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<!-- This file was generated by @travetto/doc and should not be modified directly -->
|
|
2
|
+
<!-- Please modify https://github.com/travetto/travetto/tree/main/module/email-inky/DOC.tsx and execute "npx trv doc" to rebuild -->
|
|
3
|
+
# Email Inky Templates
|
|
4
|
+
|
|
5
|
+
## Email Inky templating module
|
|
6
|
+
|
|
7
|
+
**Install: @travetto/email-inky**
|
|
8
|
+
```bash
|
|
9
|
+
npm install @travetto/email-inky
|
|
10
|
+
|
|
11
|
+
# or
|
|
12
|
+
|
|
13
|
+
yarn add @travetto/email-inky
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This module provides [inky](https://github.com/zurb/inky) support via [JSX](https://en.wikipedia.org/wiki/JSX_(JavaScript)) tags for integration with the [Email Templating](https://github.com/travetto/travetto/tree/main/module/email-template#readme "Email templating module") engine.
|
|
17
|
+
|
|
18
|
+
**Code: Sample Inky Template**
|
|
19
|
+
```typescript
|
|
20
|
+
/** @jsxImportSource @travetto/email-inky */
|
|
21
|
+
|
|
22
|
+
import { Title, Container, Summary, Row, Column, If, inkyTpl, Button, Value } from '@travetto/email-inky';
|
|
23
|
+
|
|
24
|
+
export default inkyTpl(<>
|
|
25
|
+
<Title>Test Email</Title>
|
|
26
|
+
<Summary>Email Summary</Summary>
|
|
27
|
+
<Container>
|
|
28
|
+
<If key='person'>
|
|
29
|
+
<Row>
|
|
30
|
+
<Column small={5}>
|
|
31
|
+
<Button href='https://google.com/[[query]]'>Hello <Value key='name' /></Button>
|
|
32
|
+
</Column>
|
|
33
|
+
</Row>
|
|
34
|
+
</If>
|
|
35
|
+
</Container>
|
|
36
|
+
</>);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Please see the [inky](https://github.com/zurb/inky) documentation for more information on the [component specifications](#https-get-foundation-emails-docs-inky-html)
|
|
40
|
+
|
|
41
|
+
## Conditionals and Substitutions
|
|
42
|
+
The underlying [Email](https://github.com/travetto/travetto/tree/main/module/email#readme "Email transmission module.") module is built on [mustache](https://github.com/janl/mustache.js/), which provides enough flexibility to enable sufficient power with minimal complexity and overhead.
|
|
43
|
+
|
|
44
|
+
This means this module, while showing [inky](https://github.com/zurb/inky) components, will ultimately produce HTML/markdown that is [mustache](https://github.com/janl/mustache.js/) compatible. The syntax used by [mustache](https://github.com/janl/mustache.js/) and the syntax used by [JSX](https://en.wikipedia.org/wiki/JSX_(JavaScript)) are in conflict due to both of the tools relying on the uniqueness of `{}` brackets.
|
|
45
|
+
|
|
46
|
+
To that end, the module introduces additional components ([If Component](https://github.com/travetto/travetto/tree/main/module/email-inky/src/components.ts#L27), [Unless Component](https://github.com/travetto/travetto/tree/main/module/email-inky/src/components.ts#L29), and [For Component](https://github.com/travetto/travetto/tree/main/module/email-inky/src/components.ts#L30)) to assist with control flow logic. When it comes to variable substitution, and a desire to intermingle seamlessly with component properties, `[]` are used to represent variable expressions. A more formal version can be found in the [Value Component](https://github.com/travetto/travetto/tree/main/module/email-inky/src/components.ts#L28) component, but this cannot be integrated into properties (e.g. an href)
|
|
47
|
+
|
|
48
|
+
## Template Extension Points
|
|
49
|
+
The template extension points are defined at:
|
|
50
|
+
1. `email/main.scss` - The entry point for adding, and overriding any [sass](https://github.com/sass/dart-sass)
|
|
51
|
+
1. `email/inky.wrapper.html` - Provides direct access to override the entire base HTML document for all HTML emails.
|
|
52
|
+
In addition to the overrides, you can find the list of available settings at [Github](https://github.com/foundation/foundation-emails/blob/develop/scss/settings/_settings.scss)
|
package/__index__.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@travetto/email-inky",
|
|
3
|
+
"version": "3.1.17",
|
|
4
|
+
"description": "Email Inky templating module",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"email",
|
|
7
|
+
"inky",
|
|
8
|
+
"templating",
|
|
9
|
+
"travetto",
|
|
10
|
+
"typescript"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://travetto.io",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": {
|
|
15
|
+
"email": "travetto.framework@gmail.com",
|
|
16
|
+
"name": "Travetto Framework"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"__index__.ts",
|
|
20
|
+
"resources/email/inky.wrapper.html",
|
|
21
|
+
"src"
|
|
22
|
+
],
|
|
23
|
+
"main": "__index__.ts",
|
|
24
|
+
"repository": {
|
|
25
|
+
"url": "https://github.com/travetto/travetto.git",
|
|
26
|
+
"directory": "module/email-inky"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@travetto/base": "^3.1.2",
|
|
30
|
+
"@travetto/config": "^3.1.6",
|
|
31
|
+
"@travetto/di": "^3.1.3",
|
|
32
|
+
"@travetto/email": "^3.1.12",
|
|
33
|
+
"foundation-emails": "^2.4.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@travetto/email-template": "^3.1.17"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@travetto/cli": "^3.1.6"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"@travetto/cli": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"travetto": {
|
|
47
|
+
"displayName": "Email Inky Templates"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width" />
|
|
7
|
+
</head>
|
|
8
|
+
|
|
9
|
+
<body>
|
|
10
|
+
<table class="body">
|
|
11
|
+
<tr>
|
|
12
|
+
<td class="float-center" align="center" valign="top">
|
|
13
|
+
<!-- BODY -->
|
|
14
|
+
</td>
|
|
15
|
+
</tr>
|
|
16
|
+
</table>
|
|
17
|
+
</body>
|
|
18
|
+
|
|
19
|
+
</html>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { JSXElement, JSXComponentFunction as CompFn } from '@travetto/email-inky/jsx-runtime';
|
|
2
|
+
import { TypedObject } from '@travetto/base';
|
|
3
|
+
|
|
4
|
+
const EMPTY: JSXElement = { type: '', key: '', props: {} };
|
|
5
|
+
|
|
6
|
+
export const Column: CompFn<{
|
|
7
|
+
valign?: string;
|
|
8
|
+
align?: string;
|
|
9
|
+
small?: number; smallOffset?: number; hideSmall?: boolean;
|
|
10
|
+
large?: number; largeOffset?: number; hideLarge?: boolean;
|
|
11
|
+
noExpander?: boolean;
|
|
12
|
+
}> = () => EMPTY;
|
|
13
|
+
export const Title: CompFn<{}> = () => EMPTY;
|
|
14
|
+
export const Summary: CompFn<{}> = () => EMPTY;
|
|
15
|
+
export const HLine: CompFn<{}> = () => EMPTY;
|
|
16
|
+
export const Row: CompFn<{}> = () => EMPTY;
|
|
17
|
+
export const Button: CompFn<{ href: string, target?: string, expanded?: boolean }> = () => EMPTY;
|
|
18
|
+
export const Container: CompFn<{}> = () => EMPTY;
|
|
19
|
+
export const BlockGrid: CompFn<{ up?: number }> = () => EMPTY;
|
|
20
|
+
export const Menu: CompFn<{}> = () => EMPTY;
|
|
21
|
+
export const Item: CompFn<{ href: string, target?: string }> = () => EMPTY;
|
|
22
|
+
export const Center: CompFn<{}> = () => EMPTY;
|
|
23
|
+
export const Callout: CompFn<{}> = () => EMPTY;
|
|
24
|
+
export const Spacer: CompFn<{ small?: number, large?: number, size?: number }> = () => EMPTY;
|
|
25
|
+
export const Wrapper: CompFn<{}> = () => EMPTY;
|
|
26
|
+
|
|
27
|
+
export const If: CompFn<{ attr: string }> = () => EMPTY;
|
|
28
|
+
export const Value: CompFn<{ attr: string }> = () => EMPTY;
|
|
29
|
+
export const Unless: CompFn<{ attr: string }> = () => EMPTY;
|
|
30
|
+
export const For: CompFn<{ attr: string }> = () => EMPTY;
|
|
31
|
+
|
|
32
|
+
export const c = {
|
|
33
|
+
Wrapper, Container,
|
|
34
|
+
Column, Title, Summary, HLine, Row, Button,
|
|
35
|
+
BlockGrid, Menu, Item, Center, Callout, Spacer,
|
|
36
|
+
If, Unless, For, Value
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
type C = typeof c;
|
|
40
|
+
|
|
41
|
+
// @ts-expect-error
|
|
42
|
+
export type JSXElementByFn<K extends keyof C> = JSXElement<C[K], Parameters<C[K]>[0]>;
|
|
43
|
+
export type JSXElements = { [K in keyof C]: JSXElementByFn<K>; };
|
|
44
|
+
|
|
45
|
+
export const EMPTY_ELEMENT = EMPTY;
|
|
46
|
+
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
48
|
+
const invertedC = new Map<Function, string>(TypedObject.entries(c).map(p => [p[1], p[0]] as [CompFn, string]));
|
|
49
|
+
|
|
50
|
+
export function getComponentName(fn: Function | string): string {
|
|
51
|
+
if (typeof fn === 'string') {
|
|
52
|
+
return fn;
|
|
53
|
+
}
|
|
54
|
+
return invertedC.get(fn) ?? fn.name;
|
|
55
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createElement } from '@travetto/doc/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
import { JSXElementByFn, c } from '../components';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Render Context
|
|
7
|
+
*/
|
|
8
|
+
export class RenderContext {
|
|
9
|
+
|
|
10
|
+
columnCount: number = 12;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a new element from a given JSX factory
|
|
14
|
+
*/
|
|
15
|
+
createElement<K extends keyof typeof c>(name: K, props: JSXElementByFn<K>['props']): JSXElementByFn<K> {
|
|
16
|
+
// @ts-expect-error
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
18
|
+
return createElement(c[name], props) as JSXElementByFn<K>;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { JSXElement, isJSXElement } from '@travetto/email-inky/jsx-runtime';
|
|
2
|
+
import { EmailResource } from '@travetto/email';
|
|
3
|
+
|
|
4
|
+
import { RenderProvider, RenderState } from '../types';
|
|
5
|
+
import { RenderContext } from './context';
|
|
6
|
+
|
|
7
|
+
const isOfType = (el: JSXElement, type: string): boolean => typeof el.type === 'function' && el.type.name === type;
|
|
8
|
+
|
|
9
|
+
export const SUMMARY_STYLE = Object.entries({
|
|
10
|
+
display: 'none',
|
|
11
|
+
'font-size': '1px',
|
|
12
|
+
color: '#333333',
|
|
13
|
+
'line-height': '1px',
|
|
14
|
+
'max-height': '0px',
|
|
15
|
+
'max-width': '0px',
|
|
16
|
+
opacity: '0',
|
|
17
|
+
overflow: 'hidden'
|
|
18
|
+
}).map(([k, v]) => `${k}: ${v}`).join('; ');
|
|
19
|
+
|
|
20
|
+
const classStr = (existing: string | undefined, ...toAdd: string[]): string => {
|
|
21
|
+
const out = [];
|
|
22
|
+
const seen = new Set<string>();
|
|
23
|
+
for (const item of existing?.split(/\s+/) ?? []) {
|
|
24
|
+
if (item && !seen.has(item)) {
|
|
25
|
+
out.push(item);
|
|
26
|
+
seen.add(item);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const item of toAdd) {
|
|
30
|
+
if (item && !seen.has(item)) {
|
|
31
|
+
out.push(item);
|
|
32
|
+
seen.add(item);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out.join(' ');
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
const allowedProps = new Set([
|
|
40
|
+
'class', 'id', 'dir', 'name', 'src',
|
|
41
|
+
'alt', 'href', 'title', 'height', 'target',
|
|
42
|
+
'width', 'style', 'align', 'valign'
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const propsToStr = (props: Record<string, unknown>, ...addClasses: string[]): string => {
|
|
46
|
+
const out = { ...props, class: classStr(props.class as string, ...addClasses) };
|
|
47
|
+
return Object.entries(out)
|
|
48
|
+
.filter(([k, v]) => allowedProps.has(k) && v !== undefined && v !== null && v !== '')
|
|
49
|
+
.map(([k, v]) => `${k}="${v}"`).join(' ');
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const stdInline = async ({ recurse, el }: RenderState<JSXElement, RenderContext>): Promise<string> =>
|
|
53
|
+
`<${el.type} ${propsToStr(el.props)}>${await recurse()}</${el.type}>`;
|
|
54
|
+
|
|
55
|
+
const std = async (state: RenderState<JSXElement, RenderContext>): Promise<string> => `${await stdInline(state)}\n`;
|
|
56
|
+
const stdFull = async (state: RenderState<JSXElement, RenderContext>): Promise<string> => `\n${await stdInline(state)}\n`;
|
|
57
|
+
|
|
58
|
+
const getKids = (el: JSXElement): JSXElement[] => {
|
|
59
|
+
const kids = el?.props?.children;
|
|
60
|
+
let result: unknown[] = [];
|
|
61
|
+
if (kids) {
|
|
62
|
+
result = !Array.isArray(kids) ? [kids] : kids;
|
|
63
|
+
}
|
|
64
|
+
return result.filter(isJSXElement);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const visit = (el: JSXElement, onVisit: (fn: JSXElement) => boolean | undefined | void, depth = 0): boolean | undefined => {
|
|
68
|
+
if (depth > 0) {
|
|
69
|
+
const res = onVisit(el);
|
|
70
|
+
if (res === true) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const item of getKids(el)) {
|
|
75
|
+
const res = visit(item, onVisit, depth + 1);
|
|
76
|
+
if (res) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const Html: RenderProvider<RenderContext> = {
|
|
83
|
+
finalize: (html, context) => html
|
|
84
|
+
.replace(/(<[/](?:a)>)([A-Za-z0-9$])/g, (_, tag, v) => `${tag} ${v}`)
|
|
85
|
+
.replace(/(<[uo]l>)(<li>)/g, (_, a, b) => `${a} ${b}`)
|
|
86
|
+
.replace(/[\[]{2}([^\]]+)[\]]{2}/gm, (_, t) => `{{${t}}}`),
|
|
87
|
+
|
|
88
|
+
For: async ({ recurse, props }) => `{{#${props.attr}}}${await recurse()}{{/${props.attr}}}`,
|
|
89
|
+
If: async ({ recurse, props }) => `{{#${props.attr}}}${await recurse()}{{/${props.attr}}}`,
|
|
90
|
+
Unless: async ({ recurse, props }) => `{{^${props.attr}}}${await recurse()}{{/${props.attr}}}`,
|
|
91
|
+
Value: async ({ props }) => `{{${props.attr}}}`,
|
|
92
|
+
|
|
93
|
+
br: async () => '<br>\n',
|
|
94
|
+
hr: async (el) => `<table ${propsToStr(el.props)}><th></th></table>`,
|
|
95
|
+
strong: stdInline, em: stdInline,
|
|
96
|
+
h1: stdFull, h2: stdFull, h3: stdFull, h4: stdFull,
|
|
97
|
+
li: std, ol: stdFull, ul: stdFull,
|
|
98
|
+
table: stdFull, thead: std, tr: std, td: std, th: std, tbody: std, center: std, img: stdInline,
|
|
99
|
+
title: std,
|
|
100
|
+
div: std, span: stdInline,
|
|
101
|
+
a: async ({ recurse, props }) => `<a ${propsToStr(props)}>${await recurse()}</a>`,
|
|
102
|
+
|
|
103
|
+
Title: async ({ recurse, el }) => `<title>${await recurse()}</title>`,
|
|
104
|
+
Summary: async ({ recurse, el }) => `<span id="summary" style="${SUMMARY_STYLE}">${await recurse()}</span>`,
|
|
105
|
+
|
|
106
|
+
Column: async ({ props, recurse, stack, el, context }): Promise<string> => {
|
|
107
|
+
|
|
108
|
+
recurse();
|
|
109
|
+
|
|
110
|
+
let expander = '';
|
|
111
|
+
|
|
112
|
+
const kids = getKids(el);
|
|
113
|
+
const colCount = kids.length || 1;
|
|
114
|
+
|
|
115
|
+
const parent = stack[stack.length - 1];
|
|
116
|
+
const pProps = parent.props as { columnVisited: boolean };
|
|
117
|
+
if (!pProps.columnVisited) {
|
|
118
|
+
pProps.columnVisited = true;
|
|
119
|
+
const sibs = getKids(parent).filter(x => isOfType(x, 'Column'));
|
|
120
|
+
if (sibs.length) {
|
|
121
|
+
sibs[0].props.class = classStr(sibs[0].props.class ?? '', 'first');
|
|
122
|
+
sibs[sibs.length - 1].props.class = classStr(sibs[sibs.length - 1].props.class ?? '', 'last');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check for sizes. If no attribute is provided, default to small-12. Divide evenly for large columns
|
|
128
|
+
const smallSize = el.props.small ?? context.columnCount;
|
|
129
|
+
const largeSize = el.props.large ?? el.props.small ?? Math.trunc(context.columnCount / colCount);
|
|
130
|
+
|
|
131
|
+
// If the column contains a nested row, the .expander class should not be used
|
|
132
|
+
if (largeSize === context.columnCount && !props.noExpander) {
|
|
133
|
+
let hasRow = false;
|
|
134
|
+
visit(el, (node) => {
|
|
135
|
+
if (isOfType(node, 'Row')) {
|
|
136
|
+
return hasRow = true;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
if (!hasRow) {
|
|
140
|
+
expander = '\n<th class="expander"></th>';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const classes: string[] = [`small-${smallSize}`, `large-${largeSize}`, 'columns'];
|
|
145
|
+
if (props.smallOffset) {
|
|
146
|
+
classes.push(`small-offset-${props.smallOffset}`);
|
|
147
|
+
}
|
|
148
|
+
if (props.hideSmall) {
|
|
149
|
+
classes.push('hide-for-small');
|
|
150
|
+
}
|
|
151
|
+
if (props.largeOffset) {
|
|
152
|
+
classes.push(`large-offset-${props.smallOffset}`);
|
|
153
|
+
}
|
|
154
|
+
if (props.hideLarge) {
|
|
155
|
+
classes.push('hide-for-large');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Final HTML output
|
|
159
|
+
return `
|
|
160
|
+
<th ${propsToStr(el.props, ...classes)}>
|
|
161
|
+
<table>
|
|
162
|
+
<tbody>
|
|
163
|
+
<tr>
|
|
164
|
+
<th>${await recurse()}</th>${expander}
|
|
165
|
+
</tr>
|
|
166
|
+
</tbody>
|
|
167
|
+
</table>
|
|
168
|
+
</th>`;
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
HLine: async ({ props }) => `
|
|
172
|
+
<table ${propsToStr(props, 'h-line')}>
|
|
173
|
+
<tbody>
|
|
174
|
+
<tr><th> </th></tr>
|
|
175
|
+
</tbody>
|
|
176
|
+
</table>`,
|
|
177
|
+
|
|
178
|
+
Row: async ({ recurse, el }): Promise<string> => `
|
|
179
|
+
<table ${propsToStr(el.props, 'row')}>
|
|
180
|
+
<tbody>
|
|
181
|
+
<tr>${await recurse()}</tr>
|
|
182
|
+
</tbody>
|
|
183
|
+
<!-- $:‍ --></table>`,
|
|
184
|
+
|
|
185
|
+
Button: async ({ recurse, el, props, createState }): Promise<string> => {
|
|
186
|
+
const { href, target, ...rest } = props;
|
|
187
|
+
let inner = await recurse();
|
|
188
|
+
let expander = '';
|
|
189
|
+
|
|
190
|
+
// If we have the href attribute we can create an anchor for the inner of the button;
|
|
191
|
+
if (href) {
|
|
192
|
+
const linkProps = { href, target };
|
|
193
|
+
if (props.expanded) {
|
|
194
|
+
Object.assign(linkProps, { align: 'center', class: 'float-center' });
|
|
195
|
+
}
|
|
196
|
+
inner = `<a ${propsToStr(linkProps)}>${inner}</a>`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// If the button is expanded, it needs a <center> tag around the content
|
|
200
|
+
if (props.expanded) {
|
|
201
|
+
inner = await Html.Center(createState('Center', { children: [inner] }));
|
|
202
|
+
rest.class = classStr(rest.class ?? '', 'expand');
|
|
203
|
+
expander = '\n<td class="expander"></td>';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// The .button class is always there, along with any others on the <button> element
|
|
207
|
+
return `
|
|
208
|
+
<table ${propsToStr(rest, 'button')}>
|
|
209
|
+
<tbody>
|
|
210
|
+
<tr>
|
|
211
|
+
<td>
|
|
212
|
+
<table>
|
|
213
|
+
<tbody>
|
|
214
|
+
<tr>
|
|
215
|
+
<td>
|
|
216
|
+
${inner}
|
|
217
|
+
</td>
|
|
218
|
+
</tr>
|
|
219
|
+
</tbody>
|
|
220
|
+
</table>
|
|
221
|
+
</td>${expander}
|
|
222
|
+
</tr>
|
|
223
|
+
</tbody>
|
|
224
|
+
</table>
|
|
225
|
+
${await Html.Spacer(createState('Spacer', { size: 16 }))}`;
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
Container: async ({ recurse, props }): Promise<string> => `
|
|
229
|
+
<table align="center" ${propsToStr(props, 'container')}>
|
|
230
|
+
<tbody>
|
|
231
|
+
<tr><td>${await recurse()}</td></tr>
|
|
232
|
+
</tbody>
|
|
233
|
+
</table>`,
|
|
234
|
+
|
|
235
|
+
BlockGrid: async ({ recurse, props }): Promise<string> => `
|
|
236
|
+
<table ${propsToStr(props, 'block-grid', props.up ? `up-${props.up}` : '')}>
|
|
237
|
+
<tbody>
|
|
238
|
+
<tr>${await recurse()}</tr>
|
|
239
|
+
</tbody>
|
|
240
|
+
</table>`,
|
|
241
|
+
|
|
242
|
+
Menu: async ({ recurse, el, props }): Promise<string> => {
|
|
243
|
+
let hasItem = false;
|
|
244
|
+
visit(el, (child) => {
|
|
245
|
+
if (isOfType(child, 'Item')) {
|
|
246
|
+
return hasItem = true;
|
|
247
|
+
} else if ((child.type === 'td' || child.type === 'th') && child.props.class?.includes('menu-item')) {
|
|
248
|
+
return hasItem = true;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
let inner = await recurse();
|
|
253
|
+
|
|
254
|
+
if (!hasItem && inner) {
|
|
255
|
+
inner = `<th class="menu-item">${inner}</th>`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return `
|
|
259
|
+
<table ${propsToStr(props, 'menu')}>
|
|
260
|
+
<tbody>
|
|
261
|
+
<tr>
|
|
262
|
+
<td>
|
|
263
|
+
<table>
|
|
264
|
+
<tbody>
|
|
265
|
+
<tr>
|
|
266
|
+
${inner}
|
|
267
|
+
</tr>
|
|
268
|
+
</tbody>
|
|
269
|
+
</table>
|
|
270
|
+
</td>
|
|
271
|
+
</tr>
|
|
272
|
+
</tbody>
|
|
273
|
+
</table>`;
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
Item: async ({ recurse, props }): Promise<string> => {
|
|
277
|
+
const { href, target, ...parentAttrs } = props;
|
|
278
|
+
return `
|
|
279
|
+
<th ${propsToStr(parentAttrs, 'menu-item')}>
|
|
280
|
+
<a ${propsToStr({ href, target })}>${await recurse()}</a>
|
|
281
|
+
</th>`;
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
Center: async ({ props, recurse, el }): Promise<string> => {
|
|
285
|
+
for (const kid of getKids(el)) {
|
|
286
|
+
Object.assign(kid.props, {
|
|
287
|
+
align: 'center',
|
|
288
|
+
class: classStr(kid.props.class, 'float-center')
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
visit(el, child => {
|
|
293
|
+
if (isOfType(child, 'Item')) {
|
|
294
|
+
child.props.class = classStr(child.props.class, 'float-center');
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
return `
|
|
300
|
+
<center ${propsToStr(props)}>
|
|
301
|
+
${await recurse()}
|
|
302
|
+
</center>
|
|
303
|
+
`;
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
Callout: async ({ recurse, el, props }): Promise<string> => {
|
|
307
|
+
|
|
308
|
+
const innerProps: JSXElement['props'] = { class: props.class };
|
|
309
|
+
delete props.class;
|
|
310
|
+
|
|
311
|
+
return `
|
|
312
|
+
<table ${propsToStr(props, 'callout')}>
|
|
313
|
+
<tbody>
|
|
314
|
+
<tr>
|
|
315
|
+
<th ${propsToStr(innerProps, 'callout-inner')}>
|
|
316
|
+
${await recurse()}
|
|
317
|
+
</th>
|
|
318
|
+
<th class="expander"></th>
|
|
319
|
+
</tr>
|
|
320
|
+
</tbody>
|
|
321
|
+
</table>`;
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
Spacer: async ({ props }): Promise<string> => {
|
|
325
|
+
const html: string[] = [];
|
|
326
|
+
const buildSpacer = (size: number | string, extraClass: string = ''): string =>
|
|
327
|
+
`
|
|
328
|
+
<table ${propsToStr(props, 'spacer', extraClass)}>
|
|
329
|
+
<tbody>
|
|
330
|
+
<tr>
|
|
331
|
+
<td height="${size}px" style="font-size:${size}px;line-height:${size}px;"> </td>
|
|
332
|
+
</tr>
|
|
333
|
+
</tbody>
|
|
334
|
+
</table>
|
|
335
|
+
`;
|
|
336
|
+
|
|
337
|
+
const sm = props.small ?? undefined;
|
|
338
|
+
const lg = props.large ?? undefined;
|
|
339
|
+
|
|
340
|
+
if (sm || lg) {
|
|
341
|
+
if (sm) {
|
|
342
|
+
html.push(buildSpacer(sm, 'hide-for-large'));
|
|
343
|
+
}
|
|
344
|
+
if (lg) {
|
|
345
|
+
html.push(buildSpacer(lg, 'show-for-large'));
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
html.push(buildSpacer(props.size || 16));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return html.join('\n');
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
Wrapper: async ({ recurse, el }) => `
|
|
355
|
+
<table align="center" ${propsToStr(el.props, 'wrapper')}>
|
|
356
|
+
<tbody>
|
|
357
|
+
<tr>
|
|
358
|
+
<td class="wrapper-inner">
|
|
359
|
+
${await recurse()}
|
|
360
|
+
</td>
|
|
361
|
+
</tr>
|
|
362
|
+
</tbody>
|
|
363
|
+
</table>`
|
|
364
|
+
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
export const HtmlWrap = async (content: string): Promise<string> => {
|
|
368
|
+
const wrapper = await new EmailResource(['@', '@travetto/email-inky/resources'])
|
|
369
|
+
.read('/email/inky.wrapper.html');
|
|
370
|
+
|
|
371
|
+
// Get Subject
|
|
372
|
+
const headerTop: string[] = [];
|
|
373
|
+
const bodyTop: string[] = [];
|
|
374
|
+
|
|
375
|
+
// Force summary to top, and title to head
|
|
376
|
+
let final = wrapper
|
|
377
|
+
.replace('<!-- BODY -->', content)
|
|
378
|
+
.replace(/<title>.*?<\/title>/, a => { headerTop.push(a); return ''; })
|
|
379
|
+
.replace(/<span[^>]+id="summary"[^>]*>(.*?)<\/span>/sm, a => { bodyTop.push(a); return ''; })
|
|
380
|
+
.replace(/<head( [^>]*)?>/, t => `${t}\n${headerTop.join('\n')}`)
|
|
381
|
+
.replace(/<body[^>]*>/, t => `${t}\n${bodyTop.join('\n')}`);
|
|
382
|
+
|
|
383
|
+
// Allow tag suffixes/prefixes via comments
|
|
384
|
+
final = final
|
|
385
|
+
.replace(/\s*<!--\s*[$]:([^ -]+)\s*-->\s*(<\/[^>]+>)/g, (_, suf, tag) => `${tag}${suf}`)
|
|
386
|
+
.replace(/(<[^\/][^>]+>)\s*<!--\s*[#]:([^ ]+)\s*-->\s*/g, (_, tag, pre) => `${pre}${tag}`);
|
|
387
|
+
|
|
388
|
+
return final;
|
|
389
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { JSXElement } from '@travetto/email-inky/jsx-runtime';
|
|
2
|
+
import { RenderProvider, RenderState } from '../types';
|
|
3
|
+
import { RenderContext } from './context';
|
|
4
|
+
|
|
5
|
+
const visit = ({ recurse }: RenderState<JSXElement, RenderContext>): Promise<string> => recurse();
|
|
6
|
+
const ignore = async ({ recurse: _ }: RenderState<JSXElement, RenderContext>): Promise<string> => '';
|
|
7
|
+
|
|
8
|
+
export const Markdown: RenderProvider<RenderContext> = {
|
|
9
|
+
finalize: (text, context) => text
|
|
10
|
+
.replace(/(\[[^\]]+\]\([^)]+\))([A-Za-z0-9$]+)/g, (_, link, v) => v === 's' ? _ : `${link} ${v}`)
|
|
11
|
+
.replace(/(\S)\n(#)/g, (_, l, r) => `${l}\n\n${r}`)
|
|
12
|
+
.replace(/[\[]{2}([^\]]+)[\]]{2}/gm, (_, t) => `{{${t}}}`),
|
|
13
|
+
|
|
14
|
+
For: async ({ recurse, props }) => `{{#${props.attr}}}${await recurse()}{{/${props.attr}}}`,
|
|
15
|
+
If: async ({ recurse, props }) => `{{#${props.attr}}}${await recurse()}{{/${props.attr}}}`,
|
|
16
|
+
Unless: async ({ recurse, props }) => `{{^${props.attr}}}${await recurse()}{{/${props.attr}}}`,
|
|
17
|
+
Value: async ({ props }) => `{{${props.attr}}}`,
|
|
18
|
+
|
|
19
|
+
strong: async ({ recurse }) => `**${await recurse()}**`,
|
|
20
|
+
hr: async () => '\n------------------\n',
|
|
21
|
+
HLine: async () => '\n------------------\n',
|
|
22
|
+
br: async () => '\n\n',
|
|
23
|
+
em: async ({ recurse }) => `*${await recurse()}*`,
|
|
24
|
+
ul: async ({ recurse }) => `\n${await recurse()}`,
|
|
25
|
+
ol: async ({ recurse }) => `\n${await recurse()}`,
|
|
26
|
+
li: async ({ recurse, stack }) => {
|
|
27
|
+
const parent = stack.reverse().find(x => x.type === 'ol' || x.type === 'ul');
|
|
28
|
+
const depth = stack.filter(x => x.type === 'ol' || x.type === 'ul').length;
|
|
29
|
+
return `${' '.repeat(depth)}${(parent && parent.type === 'ol') ? '1.' : '* '} ${await recurse()}\n`;
|
|
30
|
+
},
|
|
31
|
+
th: async ({ recurse }) => `|${await recurse()}`,
|
|
32
|
+
td: async ({ recurse }) => `|${await recurse()}`,
|
|
33
|
+
tr: async ({ recurse }) => `${await recurse()}|\n`,
|
|
34
|
+
thead: async ({ recurse }) => {
|
|
35
|
+
const row = await recurse();
|
|
36
|
+
return `${row}${row.replace(/[^|\n]/g, '-')}`;
|
|
37
|
+
},
|
|
38
|
+
h1: async ({ recurse }) => `\n# ${await recurse()}\n\n`,
|
|
39
|
+
h2: async ({ recurse }) => `\n## ${await recurse()}\n\n`,
|
|
40
|
+
h3: async ({ recurse }) => `\n### ${await recurse()}\n\n`,
|
|
41
|
+
h4: async ({ recurse }) => `\n#### ${await recurse()}\n\n`,
|
|
42
|
+
a: async ({ recurse, props }) => `\n[${await recurse()}](${(props as { href: string }).href})\n`,
|
|
43
|
+
Button: async ({ recurse, props }) => `\n[${await recurse()}](${props.href})\n`,
|
|
44
|
+
|
|
45
|
+
Callout: visit, Center: visit, Container: visit,
|
|
46
|
+
Column: visit, Wrapper: visit, Row: visit, BlockGrid: visit,
|
|
47
|
+
|
|
48
|
+
Menu: async ({ recurse }) => `\n${await recurse()}`,
|
|
49
|
+
Item: async ({ recurse, stack, props }) => {
|
|
50
|
+
const depth = stack.filter(x => x.type === 'Menu').length;
|
|
51
|
+
return `${' '.repeat(depth)}* [${await recurse()}](${props.href})\n`;
|
|
52
|
+
},
|
|
53
|
+
Spacer: async () => '\n\n',
|
|
54
|
+
|
|
55
|
+
Summary: ignore, Title: ignore,
|
|
56
|
+
img: ignore,
|
|
57
|
+
div: visit, title: visit, span: visit, center: visit, table: visit, tbody: visit,
|
|
58
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { isJSXElement, JSXElement, createFragment, JSXFragmentType } from '@travetto/email-inky/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
import { EMPTY_ELEMENT, getComponentName, JSXElementByFn, c } from '../components';
|
|
4
|
+
import { DocumentShape, RenderProvider, RenderState } from '../types';
|
|
5
|
+
import { RenderContext } from './context';
|
|
6
|
+
import { JSXChild } from '../../jsx-runtime';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Inky Renderer
|
|
10
|
+
*/
|
|
11
|
+
export class InkyRenderer {
|
|
12
|
+
|
|
13
|
+
static async #render(
|
|
14
|
+
ctx: RenderContext,
|
|
15
|
+
renderer: RenderProvider<RenderContext>,
|
|
16
|
+
node: JSXChild[] | JSXChild | null | undefined,
|
|
17
|
+
stack: JSXElement[] = []
|
|
18
|
+
): Promise<string> {
|
|
19
|
+
if (node === null || node === undefined) {
|
|
20
|
+
return '';
|
|
21
|
+
} else if (Array.isArray(node)) {
|
|
22
|
+
const out: string[] = [];
|
|
23
|
+
const nextStack = [...stack, { key: '', props: { children: node }, type: 'Fragment' }];
|
|
24
|
+
for (const el of node) {
|
|
25
|
+
out.push(await this.#render(ctx, renderer, el, nextStack));
|
|
26
|
+
}
|
|
27
|
+
return out.join('');
|
|
28
|
+
} else if (isJSXElement(node)) {
|
|
29
|
+
let final: JSXElement = node;
|
|
30
|
+
// Render simple element if needed
|
|
31
|
+
if (typeof node.type === 'function') {
|
|
32
|
+
// @ts-expect-error
|
|
33
|
+
const out = node.type(node.props);
|
|
34
|
+
final = out !== EMPTY_ELEMENT ? out : final;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (final.type === createFragment || final.type === JSXFragmentType) {
|
|
38
|
+
return this.#render(ctx, renderer, final.props.children ?? [], stack);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(final)) {
|
|
42
|
+
return this.#render(ctx, renderer, final, stack);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const name = getComponentName(final.type);
|
|
46
|
+
if (name in renderer) {
|
|
47
|
+
const recurse = (): Promise<string> => this.#render(ctx, renderer, final.props.children ?? [], [...stack, final]);
|
|
48
|
+
// @ts-expect-error
|
|
49
|
+
const state: RenderState<JSXElement, RenderContext> = {
|
|
50
|
+
el: final, props: final.props, recurse, stack, context: ctx
|
|
51
|
+
};
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
53
|
+
state.createState = (key, props) => this.createState(ctx, renderer, state, key, props);
|
|
54
|
+
// @ts-expect-error
|
|
55
|
+
return renderer[name](state);
|
|
56
|
+
} else {
|
|
57
|
+
console.log(final);
|
|
58
|
+
throw new Error(`Unknown element: ${final.type}`);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
return `${node}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static createState<K extends keyof typeof c>(
|
|
66
|
+
ctx: RenderContext,
|
|
67
|
+
renderer: RenderProvider<RenderContext>,
|
|
68
|
+
state: RenderState<JSXElement, RenderContext>,
|
|
69
|
+
key: K,
|
|
70
|
+
props: JSXElementByFn<K>['props'],
|
|
71
|
+
// @ts-expect-error
|
|
72
|
+
): RenderState<JSXElementByFn<K>, RenderContext> {
|
|
73
|
+
const el = ctx.createElement(key, props);
|
|
74
|
+
const newStack = [...state.stack, el] as JSXElement[];
|
|
75
|
+
return { ...state, el, props: el.props, recurse: () => this.#render(ctx, renderer, el.props.children ?? [], newStack) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Render a context given a specific renderer
|
|
80
|
+
* @param renderer
|
|
81
|
+
*/
|
|
82
|
+
static async render(root: DocumentShape, provider: RenderProvider<RenderContext>): Promise<string> {
|
|
83
|
+
const ctx = new RenderContext();
|
|
84
|
+
const par = { props: { children: Array.isArray(root.text) ? root.text : [root.text] }, type: '', key: '' };
|
|
85
|
+
const text = await this.#render(ctx, provider, root.text, [par]);
|
|
86
|
+
|
|
87
|
+
let cleaned = `${text.replace(/\n{3,100}/msg, '\n\n').trim()}\n`;
|
|
88
|
+
if (root.wrap) {
|
|
89
|
+
cleaned = await root.wrap?.(cleaned);
|
|
90
|
+
}
|
|
91
|
+
return provider.finalize(cleaned, ctx);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { JSXElement } from '@travetto/email-inky/jsx-runtime';
|
|
2
|
+
import { RenderProvider, RenderState } from '../types';
|
|
3
|
+
import { RenderContext } from './context';
|
|
4
|
+
|
|
5
|
+
const empty = async (): Promise<string> => '';
|
|
6
|
+
const visit = ({ recurse }: RenderState<JSXElement, RenderContext>): Promise<string> => recurse();
|
|
7
|
+
|
|
8
|
+
export const Subject: RenderProvider<RenderContext> = {
|
|
9
|
+
finalize: text => text
|
|
10
|
+
.replace(/[\[]{2}([^\]]+)[\]]{2}/gm, (_, t) => `{{${t}}}`),
|
|
11
|
+
|
|
12
|
+
For: async ({ recurse, props }) => `{{#${props.attr}}}${await recurse()}{{/${props.attr}}}`,
|
|
13
|
+
If: async ({ recurse, props }) => `{{#${props.attr}}}${await recurse()}{{/${props.attr}}}`,
|
|
14
|
+
Unless: async ({ recurse, props }) => `{{^${props.attr}}}${await recurse()}{{/${props.attr}}}`,
|
|
15
|
+
Value: async ({ props }) => `{{${props.attr}}}`,
|
|
16
|
+
Title: visit,
|
|
17
|
+
|
|
18
|
+
title: visit, span: visit, strong: visit, center: visit, em: visit,
|
|
19
|
+
|
|
20
|
+
Summary: empty, Button: empty,
|
|
21
|
+
Callout: empty, Center: empty, HLine: empty,
|
|
22
|
+
Menu: empty, Item: empty,
|
|
23
|
+
Column: empty, Row: empty, BlockGrid: empty, Spacer: empty,
|
|
24
|
+
Wrapper: empty, Container: empty,
|
|
25
|
+
|
|
26
|
+
div: empty, hr: empty, br: empty, a: empty, img: empty,
|
|
27
|
+
ul: empty, ol: empty, li: empty,
|
|
28
|
+
table: empty, tbody: empty, td: empty, th: empty, tr: empty, thead: empty,
|
|
29
|
+
h1: empty, h2: empty, h3: empty, h4: empty,
|
|
30
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { JSXChild, JSXElement, ValidHtmlTags } from '../jsx-runtime';
|
|
2
|
+
import { JSXElementByFn, c } from './components';
|
|
3
|
+
|
|
4
|
+
export type Wrapper = Record<string, (cnt: string) => string>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Document file shape
|
|
8
|
+
*/
|
|
9
|
+
export interface DocumentShape {
|
|
10
|
+
text: JSXChild | JSXChild[] | undefined | null;
|
|
11
|
+
wrap?: (content: string) => string | Promise<string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type RenderState<T extends JSXElement, C> = {
|
|
15
|
+
el: T;
|
|
16
|
+
props: T['props'];
|
|
17
|
+
recurse: () => Promise<string>;
|
|
18
|
+
stack: JSXElement[];
|
|
19
|
+
// @ts-expect-error
|
|
20
|
+
createState: <K extends keyof typeof c>(key: K, props: JSXElementByFn<K>['props']) => RenderState<JSXElementByFn<K>, C>;
|
|
21
|
+
context: C;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Renderer
|
|
26
|
+
*/
|
|
27
|
+
export type RenderProvider<C> =
|
|
28
|
+
{ finalize: (text: string, ctx: C) => string } &
|
|
29
|
+
{ [K in ValidHtmlTags]: (state: RenderState<JSXElement<K>, C>) => Promise<string>; } &
|
|
30
|
+
// @ts-expect-error
|
|
31
|
+
{ [K in keyof typeof c]: (state: RenderState<JSXElementByFn<K>, C>) => Promise<string>; };
|
package/src/wrapper.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
|
|
3
|
+
import { MessageCompilationSource } from '@travetto/email';
|
|
4
|
+
import { JSXElement } from '@travetto/email-inky/jsx-runtime';
|
|
5
|
+
import { RootIndex, path } from '@travetto/manifest';
|
|
6
|
+
|
|
7
|
+
import { InkyRenderer } from './render/renderer';
|
|
8
|
+
import { Html, HtmlWrap } from './render/html';
|
|
9
|
+
import { Markdown } from './render/markdown';
|
|
10
|
+
import { Subject } from './render/subject';
|
|
11
|
+
|
|
12
|
+
const req = createRequire(`${RootIndex.manifest.workspacePath}/node_modules`);
|
|
13
|
+
|
|
14
|
+
export const inkyTpl = (content: JSXElement | JSXElement[]): MessageCompilationSource => ({
|
|
15
|
+
file: '',
|
|
16
|
+
html: InkyRenderer.render.bind(InkyRenderer, { text: content, wrap: HtmlWrap }, Html),
|
|
17
|
+
text: InkyRenderer.render.bind(InkyRenderer, { text: content }, Markdown),
|
|
18
|
+
subject: InkyRenderer.render.bind(InkyRenderer, { text: content }, Subject),
|
|
19
|
+
styles: {
|
|
20
|
+
search: [path.dirname(req.resolve('foundation-emails/scss/_global.scss'))],
|
|
21
|
+
global: `
|
|
22
|
+
@import 'settings/_settings.scss';
|
|
23
|
+
@import 'foundation-emails.scss';
|
|
24
|
+
`
|
|
25
|
+
}
|
|
26
|
+
});
|