@travetto/email-inky 8.0.0-alpha.21 → 8.0.0-alpha.23
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/README.md +20 -16
- package/__index__.ts +4 -4
- package/package.json +6 -6
- package/src/components.ts +31 -12
- package/src/render/common.ts +9 -4
- package/src/render/context.ts +4 -5
- package/src/render/html.ts +57 -24
- package/src/render/markdown.ts +22 -10
- package/src/render/renderer.ts +9 -7
- package/src/render/subject.ts +39 -11
- package/src/types.ts +4 -5
- package/src/wrapper.ts +4 -4
package/README.md
CHANGED
|
@@ -19,21 +19,25 @@ This module provides [inky](https://github.com/zurb/inky) support via [JSX](http
|
|
|
19
19
|
```typescript
|
|
20
20
|
/** @jsxImportSource @travetto/email-inky/support */
|
|
21
21
|
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
export default
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
</
|
|
22
|
+
import { Button, Column, Container, If, InkyTemplate, Row, Summary, Title, Value } from '@travetto/email-inky';
|
|
23
|
+
|
|
24
|
+
export default (
|
|
25
|
+
<InkyTemplate>
|
|
26
|
+
<Title>Test Email</Title>
|
|
27
|
+
<Summary>Email Summary</Summary>
|
|
28
|
+
<Container>
|
|
29
|
+
<If attr="person">
|
|
30
|
+
<Row>
|
|
31
|
+
<Column small={5}>
|
|
32
|
+
<Button href="https://google.com/{{query}}">
|
|
33
|
+
Hello <Value attr="name" />
|
|
34
|
+
</Button>
|
|
35
|
+
</Column>
|
|
36
|
+
</Row>
|
|
37
|
+
</If>
|
|
38
|
+
</Container>
|
|
39
|
+
</InkyTemplate>
|
|
40
|
+
);
|
|
37
41
|
```
|
|
38
42
|
|
|
39
43
|
Please see the [inky](https://github.com/zurb/inky) documentation for more information on the [component specifications](#https-get-foundation-emails-docs-inky-html)
|
|
@@ -43,7 +47,7 @@ The underlying [Email](https://github.com/travetto/travetto/tree/main/module/ema
|
|
|
43
47
|
|
|
44
48
|
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
49
|
|
|
46
|
-
To that end, the module introduces additional components ([If Component](https://github.com/travetto/travetto/tree/main/module/email-inky/src/components.ts#
|
|
50
|
+
To that end, the module introduces additional components ([If Component](https://github.com/travetto/travetto/tree/main/module/email-inky/src/components.ts#L32), [Unless Component](https://github.com/travetto/travetto/tree/main/module/email-inky/src/components.ts#L34), and [For Component](https://github.com/travetto/travetto/tree/main/module/email-inky/src/components.ts#L35)) to assist with control flow logic. When it comes to variable substitution, and a desire to intermingle seamlessly with component properties, `{{value}}` can be used within a string value. To leverage substitutions outside of string contexts, a more formal version can be found in the [Value Component](https://github.com/travetto/travetto/tree/main/module/email-inky/src/components.ts#L33) component, but this cannot be integrated into properties (e.g. an href).
|
|
47
51
|
|
|
48
52
|
## Template Extension Points
|
|
49
53
|
The template extension points are defined at:
|
package/__index__.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export * from './src/components.ts';
|
|
2
|
-
export * from './src/
|
|
3
|
-
export * from './src/render/
|
|
2
|
+
export * from './src/render/common.ts';
|
|
3
|
+
export * from './src/render/context.ts';
|
|
4
4
|
export * from './src/render/html.ts';
|
|
5
5
|
export * from './src/render/markdown.ts';
|
|
6
|
+
export * from './src/render/renderer.ts';
|
|
6
7
|
export * from './src/render/subject.ts';
|
|
7
|
-
export * from './src/
|
|
8
|
-
export * from './src/render/common.ts';
|
|
8
|
+
export * from './src/wrapper.ts';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/email-inky",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Email Inky templating module",
|
|
6
6
|
"keywords": [
|
|
@@ -28,14 +28,14 @@
|
|
|
28
28
|
"directory": "module/email-inky"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@travetto/config": "^8.0.0-alpha.
|
|
32
|
-
"@travetto/di": "^8.0.0-alpha.
|
|
33
|
-
"@travetto/email": "^8.0.0-alpha.
|
|
34
|
-
"@travetto/runtime": "^8.0.0-alpha.
|
|
31
|
+
"@travetto/config": "^8.0.0-alpha.22",
|
|
32
|
+
"@travetto/di": "^8.0.0-alpha.20",
|
|
33
|
+
"@travetto/email": "^8.0.0-alpha.22",
|
|
34
|
+
"@travetto/runtime": "^8.0.0-alpha.20",
|
|
35
35
|
"foundation-emails": "^2.5.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@travetto/email-compiler": "^8.0.0-alpha.
|
|
38
|
+
"@travetto/email-compiler": "^8.0.0-alpha.23"
|
|
39
39
|
},
|
|
40
40
|
"peerDependenciesMeta": {
|
|
41
41
|
"@travetto/cli": {
|
package/src/components.ts
CHANGED
|
@@ -1,41 +1,60 @@
|
|
|
1
1
|
import { TypedObject } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { JSXComponentFunction as CompFn, JSXElement } from '../support/jsx-runtime.ts';
|
|
4
4
|
|
|
5
5
|
const EMPTY: JSXElement = { type: '', key: '', props: {} };
|
|
6
6
|
|
|
7
7
|
export const Column: CompFn<{
|
|
8
8
|
valign?: string;
|
|
9
9
|
align?: string;
|
|
10
|
-
small?: number;
|
|
11
|
-
|
|
10
|
+
small?: number;
|
|
11
|
+
smallOffset?: number;
|
|
12
|
+
hideSmall?: boolean;
|
|
13
|
+
large?: number;
|
|
14
|
+
largeOffset?: number;
|
|
15
|
+
hideLarge?: boolean;
|
|
12
16
|
noExpander?: boolean;
|
|
13
17
|
}> = () => EMPTY;
|
|
14
18
|
export const Title: CompFn<{}> = () => EMPTY;
|
|
15
19
|
export const Summary: CompFn<{}> = () => EMPTY;
|
|
16
20
|
export const HLine: CompFn<{}> = () => EMPTY;
|
|
17
21
|
export const Row: CompFn<{}> = () => EMPTY;
|
|
18
|
-
export const Button: CompFn<{ href: string
|
|
22
|
+
export const Button: CompFn<{ href: string; target?: string; expanded?: boolean }> = () => EMPTY;
|
|
19
23
|
export const Container: CompFn<{}> = () => EMPTY;
|
|
20
24
|
export const BlockGrid: CompFn<{ up?: number }> = () => EMPTY;
|
|
21
25
|
export const Menu: CompFn<{}> = () => EMPTY;
|
|
22
|
-
export const Item: CompFn<{ href: string
|
|
26
|
+
export const Item: CompFn<{ href: string; target?: string }> = () => EMPTY;
|
|
23
27
|
export const Center: CompFn<{}> = () => EMPTY;
|
|
24
28
|
export const Callout: CompFn<{}> = () => EMPTY;
|
|
25
|
-
export const Spacer: CompFn<{ small?: number
|
|
29
|
+
export const Spacer: CompFn<{ small?: number; large?: number; size?: number }> = () => EMPTY;
|
|
26
30
|
export const Wrapper: CompFn<{}> = () => EMPTY;
|
|
27
31
|
|
|
28
32
|
export const If: CompFn<{ attr: string }> = () => EMPTY;
|
|
29
|
-
export const Value: CompFn<{ attr: string
|
|
33
|
+
export const Value: CompFn<{ attr: string; raw?: boolean }> = () => EMPTY;
|
|
30
34
|
export const Unless: CompFn<{ attr: string }> = () => EMPTY;
|
|
31
35
|
export const For: CompFn<{ attr: string }> = () => EMPTY;
|
|
32
36
|
export const InkyTemplate: CompFn<{}> = () => EMPTY;
|
|
33
37
|
|
|
34
38
|
export const c = {
|
|
35
|
-
Wrapper,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
Wrapper,
|
|
40
|
+
Container,
|
|
41
|
+
InkyTemplate,
|
|
42
|
+
Column,
|
|
43
|
+
Title,
|
|
44
|
+
Summary,
|
|
45
|
+
HLine,
|
|
46
|
+
Row,
|
|
47
|
+
Button,
|
|
48
|
+
BlockGrid,
|
|
49
|
+
Menu,
|
|
50
|
+
Item,
|
|
51
|
+
Center,
|
|
52
|
+
Callout,
|
|
53
|
+
Spacer,
|
|
54
|
+
If,
|
|
55
|
+
Unless,
|
|
56
|
+
For,
|
|
57
|
+
Value
|
|
39
58
|
} as const;
|
|
40
59
|
|
|
41
60
|
for (const key of TypedObject.keys(c)) {
|
|
@@ -46,7 +65,7 @@ type C = typeof c;
|
|
|
46
65
|
|
|
47
66
|
// @ts-expect-error
|
|
48
67
|
export type JSXElementByFn<K extends keyof C> = JSXElement<C[K], Parameters<C[K]>[0]>;
|
|
49
|
-
export type JSXElements = { [K in keyof C]: JSXElementByFn<K
|
|
68
|
+
export type JSXElements = { [K in keyof C]: JSXElementByFn<K> };
|
|
50
69
|
|
|
51
70
|
export const EMPTY_ELEMENT = EMPTY;
|
|
52
71
|
|
package/src/render/common.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type JSXElement
|
|
1
|
+
import { isJSXElement, type JSXElement } from '../../support/jsx-runtime.ts';
|
|
2
2
|
|
|
3
3
|
export const getChildren = (node: JSXElement): JSXElement[] => {
|
|
4
4
|
const kids = node?.props?.children;
|
|
@@ -42,12 +42,17 @@ export const classString = (existing: string | undefined, ...toAdd: string[]): s
|
|
|
42
42
|
return out.join(' ');
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
export const combinePropsToString = (
|
|
45
|
+
export const combinePropsToString = (
|
|
46
|
+
allowedProps: Set<string>,
|
|
47
|
+
props: { className?: string } & Record<string, unknown>,
|
|
48
|
+
addClasses: string[] = []
|
|
49
|
+
): string => {
|
|
46
50
|
const out = { ...props, className: classString(props.className, ...addClasses) };
|
|
47
51
|
return Object.entries(out)
|
|
48
52
|
.filter(([key, value]) => allowedProps.has(key) && value !== undefined && value !== null && value !== '')
|
|
49
53
|
.map(([key, value]) => [key === 'className' ? 'class' : key, value])
|
|
50
|
-
.map(([key, value]) => `${key}="${value}"`)
|
|
54
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
55
|
+
.join(' ');
|
|
51
56
|
};
|
|
52
57
|
|
|
53
|
-
export const isOfType = (node: JSXElement, type: string): boolean => typeof node.type === 'function' && node.type.name === type;
|
|
58
|
+
export const isOfType = (node: JSXElement, type: string): boolean => typeof node.type === 'function' && node.type.name === type;
|
package/src/render/context.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
+
import { EmailResourceLoader, type EmailTemplateLocation } from '@travetto/email';
|
|
1
2
|
import { castTo, type FileLoader } from '@travetto/runtime';
|
|
2
|
-
import { type EmailTemplateLocation, EmailResourceLoader } from '@travetto/email';
|
|
3
3
|
|
|
4
|
-
import { type JSXElementByFn, c } from '../components.ts';
|
|
5
4
|
import { createElement } from '../../support/jsx-runtime.ts';
|
|
5
|
+
import { c, type JSXElementByFn } from '../components.ts';
|
|
6
6
|
|
|
7
|
-
export type RenderContextInit = EmailTemplateLocation & { loader?: FileLoader
|
|
7
|
+
export type RenderContextInit = EmailTemplateLocation & { loader?: FileLoader; columnCount?: number };
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Render Context
|
|
11
11
|
*/
|
|
12
12
|
export class RenderContext implements RenderContextInit {
|
|
13
|
-
|
|
14
13
|
columnCount: number = 12;
|
|
15
14
|
file: string;
|
|
16
15
|
module: string;
|
|
@@ -27,4 +26,4 @@ export class RenderContext implements RenderContextInit {
|
|
|
27
26
|
createElement<K extends keyof typeof c>(name: K, props: JSXElementByFn<K>['props']): JSXElementByFn<K> {
|
|
28
27
|
return castTo(createElement(castTo<Record<string, string>>(c)[name], props));
|
|
29
28
|
}
|
|
30
|
-
}
|
|
29
|
+
}
|
package/src/render/html.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { JSXElement } from '../../support/jsx-runtime.ts';
|
|
2
2
|
import type { RenderProvider, RenderState } from '../types.ts';
|
|
3
|
-
import type { RenderContext } from './context.ts';
|
|
4
3
|
import { classString, combinePropsToString, getChildren, isOfType, visit } from './common.ts';
|
|
4
|
+
import type { RenderContext } from './context.ts';
|
|
5
5
|
|
|
6
6
|
export const SUMMARY_STYLE = Object.entries({
|
|
7
7
|
display: 'none',
|
|
@@ -12,12 +12,25 @@ export const SUMMARY_STYLE = Object.entries({
|
|
|
12
12
|
'max-width': '0px',
|
|
13
13
|
opacity: '0',
|
|
14
14
|
overflow: 'hidden'
|
|
15
|
-
})
|
|
15
|
+
})
|
|
16
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
17
|
+
.join('; ');
|
|
16
18
|
|
|
17
19
|
const allowedProps = new Set([
|
|
18
|
-
'className',
|
|
19
|
-
'
|
|
20
|
-
'
|
|
20
|
+
'className',
|
|
21
|
+
'id',
|
|
22
|
+
'dir',
|
|
23
|
+
'name',
|
|
24
|
+
'src',
|
|
25
|
+
'alt',
|
|
26
|
+
'href',
|
|
27
|
+
'title',
|
|
28
|
+
'height',
|
|
29
|
+
'target',
|
|
30
|
+
'width',
|
|
31
|
+
'style',
|
|
32
|
+
'align',
|
|
33
|
+
'valign'
|
|
21
34
|
]);
|
|
22
35
|
|
|
23
36
|
const propsToString = combinePropsToString.bind(null, allowedProps);
|
|
@@ -44,15 +57,21 @@ export const Html: RenderProvider<RenderContext> = {
|
|
|
44
57
|
// Force summary to top, and title to head
|
|
45
58
|
const final = wrapper
|
|
46
59
|
.replace('<!-- BODY -->', html)
|
|
47
|
-
.replace(/<title>.*?<\/title>/, a => {
|
|
48
|
-
|
|
60
|
+
.replace(/<title>.*?<\/title>/, a => {
|
|
61
|
+
headerTop.push(a);
|
|
62
|
+
return '';
|
|
63
|
+
})
|
|
64
|
+
.replace(/<span[^>]+id="summary"[^>]*>(.*?)<\/span>/ms, a => {
|
|
65
|
+
bodyTop.push(a);
|
|
66
|
+
return '';
|
|
67
|
+
})
|
|
49
68
|
.replace(/<head( [^>]*)?>/, tag => `${tag}\n${headerTop.join('\n')}`)
|
|
50
69
|
.replace(/<body[^>]*>/, tag => `${tag}\n${bodyTop.join('\n')}`);
|
|
51
70
|
|
|
52
71
|
// Allow tag suffixes/prefixes via comments
|
|
53
72
|
html = final
|
|
54
73
|
.replace(/\s*<!--\s*[$]:([^ -]+)\s*-->\s*(<\/[^>]+>)/g, (_, suffix, tag) => `${tag}${suffix}`)
|
|
55
|
-
.replace(/(<[
|
|
74
|
+
.replace(/(<[^/][^>]+>)\s*<!--\s*[#]:([^ ]+)\s*-->\s*/g, (_, tag, prefix) => `${prefix}${tag}`);
|
|
56
75
|
}
|
|
57
76
|
|
|
58
77
|
return html;
|
|
@@ -61,16 +80,32 @@ export const Html: RenderProvider<RenderContext> = {
|
|
|
61
80
|
For: async ({ recurse, props }) => `{{#${props.attr}}}${await recurse()}{{/${props.attr}}}`,
|
|
62
81
|
If: async ({ recurse, props }) => `{{#${props.attr}}}${await recurse()}{{/${props.attr}}}`,
|
|
63
82
|
Unless: async ({ recurse, props }) => `{{^${props.attr}}}${await recurse()}{{/${props.attr}}}`,
|
|
64
|
-
Value: async ({ props }) => props.raw ? `{{{${props.attr}}}}` : `{{${props.attr}}}
|
|
83
|
+
Value: async ({ props }) => (props.raw ? `{{{${props.attr}}}}` : `{{${props.attr}}}`),
|
|
65
84
|
|
|
66
85
|
br: async () => '<br>\n',
|
|
67
|
-
hr: async
|
|
68
|
-
strong: standardInline,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
86
|
+
hr: async node => `<table ${propsToString(node.props)}><th></th></table>`,
|
|
87
|
+
strong: standardInline,
|
|
88
|
+
em: standardInline,
|
|
89
|
+
p: standardFull,
|
|
90
|
+
h1: standardFull,
|
|
91
|
+
h2: standardFull,
|
|
92
|
+
h3: standardFull,
|
|
93
|
+
h4: standardFull,
|
|
94
|
+
li: standard,
|
|
95
|
+
ol: standardFull,
|
|
96
|
+
ul: standardFull,
|
|
97
|
+
table: standardFull,
|
|
98
|
+
thead: standard,
|
|
99
|
+
tr: standard,
|
|
100
|
+
td: standard,
|
|
101
|
+
th: standard,
|
|
102
|
+
tbody: standard,
|
|
103
|
+
center: standard,
|
|
104
|
+
img: standardInline,
|
|
72
105
|
title: standard,
|
|
73
|
-
div: standard,
|
|
106
|
+
div: standard,
|
|
107
|
+
span: standardInline,
|
|
108
|
+
small: standardInline,
|
|
74
109
|
a: async ({ recurse, props }) => `<a ${propsToString(props)}>${await recurse()}</a>`,
|
|
75
110
|
|
|
76
111
|
InkyTemplate: ctx => ctx.recurse(),
|
|
@@ -78,7 +113,6 @@ export const Html: RenderProvider<RenderContext> = {
|
|
|
78
113
|
Summary: async ({ recurse }) => `<span id="summary" style="${SUMMARY_STYLE}">${await recurse()}</span>`,
|
|
79
114
|
|
|
80
115
|
Column: async ({ props, recurse, stack, node, context }): Promise<string> => {
|
|
81
|
-
|
|
82
116
|
recurse();
|
|
83
117
|
|
|
84
118
|
let expander = '';
|
|
@@ -88,7 +122,7 @@ export const Html: RenderProvider<RenderContext> = {
|
|
|
88
122
|
const colCount = siblings.length || 1;
|
|
89
123
|
|
|
90
124
|
if (parent) {
|
|
91
|
-
const parentNode:
|
|
125
|
+
const parentNode: typeof parent & { columnVisited?: boolean } = parent;
|
|
92
126
|
if (!parentNode.columnVisited) {
|
|
93
127
|
parentNode.columnVisited = true;
|
|
94
128
|
if (siblings.length) {
|
|
@@ -107,9 +141,9 @@ export const Html: RenderProvider<RenderContext> = {
|
|
|
107
141
|
// If the column contains a nested row, the .expander class should not be used
|
|
108
142
|
if (largeSize === context.columnCount && !props.noExpander) {
|
|
109
143
|
let hasRow = false;
|
|
110
|
-
visit(node,
|
|
144
|
+
visit(node, child => {
|
|
111
145
|
if (isOfType(child, 'Row')) {
|
|
112
|
-
return hasRow = true;
|
|
146
|
+
return (hasRow = true);
|
|
113
147
|
}
|
|
114
148
|
});
|
|
115
149
|
if (!hasRow) {
|
|
@@ -217,11 +251,11 @@ export const Html: RenderProvider<RenderContext> = {
|
|
|
217
251
|
|
|
218
252
|
Menu: async ({ recurse, node, props }): Promise<string> => {
|
|
219
253
|
let hasItem = false;
|
|
220
|
-
visit(node,
|
|
254
|
+
visit(node, child => {
|
|
221
255
|
if (isOfType(child, 'Item')) {
|
|
222
|
-
return hasItem = true;
|
|
256
|
+
return (hasItem = true);
|
|
223
257
|
} else if ((child.type === 'td' || child.type === 'th') && child.props.className?.includes('menu-item')) {
|
|
224
|
-
return hasItem = true;
|
|
258
|
+
return (hasItem = true);
|
|
225
259
|
}
|
|
226
260
|
});
|
|
227
261
|
|
|
@@ -280,7 +314,6 @@ export const Html: RenderProvider<RenderContext> = {
|
|
|
280
314
|
},
|
|
281
315
|
|
|
282
316
|
Callout: async ({ recurse, props }): Promise<string> => {
|
|
283
|
-
|
|
284
317
|
const innerProps: JSXElement['props'] = { className: props.className };
|
|
285
318
|
delete props.className;
|
|
286
319
|
|
|
@@ -337,4 +370,4 @@ export const Html: RenderProvider<RenderContext> = {
|
|
|
337
370
|
</tr>
|
|
338
371
|
</tbody>
|
|
339
372
|
</table>`
|
|
340
|
-
};
|
|
373
|
+
};
|
package/src/render/markdown.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { castTo } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
+
import type { JSXElement } from '../../support/jsx-runtime.ts';
|
|
3
4
|
import type { RenderProvider, RenderState } from '../types.ts';
|
|
4
5
|
import type { RenderContext } from './context.ts';
|
|
5
|
-
import type { JSXElement } from '../../support/jsx-runtime.ts';
|
|
6
6
|
|
|
7
7
|
const visit = ({ recurse }: RenderState<JSXElement, RenderContext>): Promise<string> => recurse();
|
|
8
8
|
const ignore = async (_: RenderState<JSXElement, RenderContext>): Promise<string> => '';
|
|
9
9
|
|
|
10
10
|
export const Markdown: RenderProvider<RenderContext> = {
|
|
11
|
-
finalize:
|
|
11
|
+
finalize: text => {
|
|
12
12
|
text = text
|
|
13
|
-
.replace(/(\[[^\]]{1,100}\]\([^)]{1,1000}\))([A-Za-z0-9$]{1,100})/g, (all, link, value) => value === 's' ? all : `${link} ${value}`)
|
|
13
|
+
.replace(/(\[[^\]]{1,100}\]\([^)]{1,1000}\))([A-Za-z0-9$]{1,100})/g, (all, link, value) => (value === 's' ? all : `${link} ${value}`))
|
|
14
14
|
.replace(/(\S)\n(#)/g, (_, left, right) => `${left}\n\n${right}`);
|
|
15
15
|
return text;
|
|
16
16
|
},
|
|
@@ -18,7 +18,7 @@ export const Markdown: RenderProvider<RenderContext> = {
|
|
|
18
18
|
For: async ({ recurse, props }) => `{{#${props.attr}}}${await recurse()}{{/${props.attr}}}`,
|
|
19
19
|
If: async ({ recurse, props }) => `{{#${props.attr}}}${await recurse()}{{/${props.attr}}}`,
|
|
20
20
|
Unless: async ({ recurse, props }) => `{{^${props.attr}}}${await recurse()}{{/${props.attr}}}`,
|
|
21
|
-
Value: async ({ props }) => props.raw ? `{{{${props.attr}}}}` : `{{${props.attr}}}
|
|
21
|
+
Value: async ({ props }) => (props.raw ? `{{{${props.attr}}}}` : `{{${props.attr}}}`),
|
|
22
22
|
|
|
23
23
|
strong: async ({ recurse }) => `**${await recurse()}**`,
|
|
24
24
|
hr: async () => '\n------------------\n',
|
|
@@ -31,7 +31,7 @@ export const Markdown: RenderProvider<RenderContext> = {
|
|
|
31
31
|
li: async ({ recurse, stack }) => {
|
|
32
32
|
const parent = stack.toReversed().find(node => node.type === 'ol' || node.type === 'ul');
|
|
33
33
|
const depth = stack.filter(node => node.type === 'ol' || node.type === 'ul').length;
|
|
34
|
-
return `${' '.repeat(depth)}${
|
|
34
|
+
return `${' '.repeat(depth)}${parent && parent.type === 'ol' ? '1.' : '* '} ${await recurse()}\n`;
|
|
35
35
|
},
|
|
36
36
|
th: async ({ recurse }) => `|${await recurse()}`,
|
|
37
37
|
td: async ({ recurse }) => `|${await recurse()}`,
|
|
@@ -44,12 +44,17 @@ export const Markdown: RenderProvider<RenderContext> = {
|
|
|
44
44
|
h2: async ({ recurse }) => `\n## ${await recurse()}\n\n`,
|
|
45
45
|
h3: async ({ recurse }) => `\n### ${await recurse()}\n\n`,
|
|
46
46
|
h4: async ({ recurse }) => `\n#### ${await recurse()}\n\n`,
|
|
47
|
-
a: async ({ recurse, props }) => `\n[${await recurse()}](${
|
|
47
|
+
a: async ({ recurse, props }) => `\n[${await recurse()}](${castTo<{ href: string }>(props).href})\n`,
|
|
48
48
|
Button: async ({ recurse, props }) => `\n[${await recurse()}](${props.href})\n`,
|
|
49
49
|
|
|
50
50
|
InkyTemplate: visit,
|
|
51
|
-
Callout: visit,
|
|
52
|
-
|
|
51
|
+
Callout: visit,
|
|
52
|
+
Center: visit,
|
|
53
|
+
Container: visit,
|
|
54
|
+
Column: visit,
|
|
55
|
+
Wrapper: visit,
|
|
56
|
+
Row: visit,
|
|
57
|
+
BlockGrid: visit,
|
|
53
58
|
|
|
54
59
|
Menu: async ({ recurse }) => `\n${await recurse()}`,
|
|
55
60
|
Item: async ({ recurse, stack, props }) => {
|
|
@@ -58,7 +63,14 @@ export const Markdown: RenderProvider<RenderContext> = {
|
|
|
58
63
|
},
|
|
59
64
|
Spacer: async () => '\n\n',
|
|
60
65
|
|
|
61
|
-
Summary: ignore,
|
|
66
|
+
Summary: ignore,
|
|
67
|
+
Title: ignore,
|
|
62
68
|
img: ignore,
|
|
63
|
-
div: visit,
|
|
69
|
+
div: visit,
|
|
70
|
+
title: visit,
|
|
71
|
+
span: visit,
|
|
72
|
+
center: visit,
|
|
73
|
+
table: visit,
|
|
74
|
+
tbody: visit,
|
|
75
|
+
small: visit
|
|
64
76
|
};
|
package/src/render/renderer.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { castTo } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { createFragment, isJSXElement, type JSXChild, type JSXElement, JSXFragmentType } from '../../support/jsx-runtime.ts';
|
|
4
|
+
import { type c, EMPTY_ELEMENT, getComponentName, type JSXElementByFn } from '../components.ts';
|
|
4
5
|
import type { RenderProvider, RenderState } from '../types.ts';
|
|
5
6
|
import { RenderContext, type RenderContextInit } from './context.ts';
|
|
6
|
-
import { isJSXElement, type JSXElement, createFragment, JSXFragmentType, type JSXChild } from '../../support/jsx-runtime.ts';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Inky Renderer
|
|
10
10
|
*/
|
|
11
11
|
export class InkyRenderer {
|
|
12
|
-
|
|
13
12
|
static async #render(
|
|
14
13
|
ctx: RenderContext,
|
|
15
14
|
renderer: RenderProvider<RenderContext>,
|
|
@@ -46,9 +45,12 @@ export class InkyRenderer {
|
|
|
46
45
|
const recurse = (): Promise<string> => this.#render(ctx, renderer, final.props.children ?? [], [...stack, final]);
|
|
47
46
|
// @ts-expect-error
|
|
48
47
|
const state: RenderState<JSXElement, RenderContext> = {
|
|
49
|
-
node: final,
|
|
48
|
+
node: final,
|
|
49
|
+
props: final.props,
|
|
50
|
+
recurse,
|
|
51
|
+
stack,
|
|
52
|
+
context: ctx
|
|
50
53
|
};
|
|
51
|
-
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
52
54
|
state.createState = (key, props) => this.createState(ctx, renderer, state, key, props);
|
|
53
55
|
// @ts-expect-error
|
|
54
56
|
return renderer[name](state);
|
|
@@ -66,7 +68,7 @@ export class InkyRenderer {
|
|
|
66
68
|
renderer: RenderProvider<RenderContext>,
|
|
67
69
|
state: RenderState<JSXElement, RenderContext>,
|
|
68
70
|
key: K,
|
|
69
|
-
props: JSXElementByFn<K>['props']
|
|
71
|
+
props: JSXElementByFn<K>['props']
|
|
70
72
|
// @ts-expect-error
|
|
71
73
|
): RenderState<JSXElementByFn<K>, RenderContext> {
|
|
72
74
|
const node = ctx.createElement(key, props);
|
|
@@ -89,4 +91,4 @@ export class InkyRenderer {
|
|
|
89
91
|
const text = await this.#render(ctx, provider, par, []);
|
|
90
92
|
return provider.finalize(text, ctx, isRoot);
|
|
91
93
|
}
|
|
92
|
-
}
|
|
94
|
+
}
|
package/src/render/subject.ts
CHANGED
|
@@ -6,7 +6,7 @@ const empty = async (): Promise<string> => '';
|
|
|
6
6
|
const visit = ({ recurse }: RenderState<JSXElement, RenderContext>): Promise<string> => recurse();
|
|
7
7
|
|
|
8
8
|
export const Subject: RenderProvider<RenderContext> = {
|
|
9
|
-
finalize:
|
|
9
|
+
finalize: text => text,
|
|
10
10
|
|
|
11
11
|
For: async ({ recurse, props }) => `{{#${props.attr}}}${await recurse()}{{/${props.attr}}}`,
|
|
12
12
|
If: async ({ recurse, props }) => `{{#${props.attr}}}${await recurse()}{{/${props.attr}}}`,
|
|
@@ -15,16 +15,44 @@ export const Subject: RenderProvider<RenderContext> = {
|
|
|
15
15
|
Title: visit,
|
|
16
16
|
InkyTemplate: visit,
|
|
17
17
|
|
|
18
|
-
title: visit,
|
|
18
|
+
title: visit,
|
|
19
|
+
span: visit,
|
|
20
|
+
strong: visit,
|
|
21
|
+
center: visit,
|
|
22
|
+
em: visit,
|
|
23
|
+
p: visit,
|
|
24
|
+
small: visit,
|
|
19
25
|
|
|
20
|
-
Summary: empty,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
Summary: empty,
|
|
27
|
+
Button: empty,
|
|
28
|
+
Callout: empty,
|
|
29
|
+
Center: empty,
|
|
30
|
+
HLine: empty,
|
|
31
|
+
Menu: empty,
|
|
32
|
+
Item: empty,
|
|
33
|
+
Column: empty,
|
|
34
|
+
Row: empty,
|
|
35
|
+
BlockGrid: empty,
|
|
36
|
+
Spacer: empty,
|
|
37
|
+
Wrapper: empty,
|
|
38
|
+
Container: empty,
|
|
25
39
|
|
|
26
|
-
div: empty,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
div: empty,
|
|
41
|
+
hr: empty,
|
|
42
|
+
br: empty,
|
|
43
|
+
a: empty,
|
|
44
|
+
img: empty,
|
|
45
|
+
ul: empty,
|
|
46
|
+
ol: empty,
|
|
47
|
+
li: empty,
|
|
48
|
+
table: empty,
|
|
49
|
+
tbody: empty,
|
|
50
|
+
td: empty,
|
|
51
|
+
th: empty,
|
|
52
|
+
tr: empty,
|
|
53
|
+
thead: empty,
|
|
54
|
+
h1: empty,
|
|
55
|
+
h2: empty,
|
|
56
|
+
h3: empty,
|
|
57
|
+
h4: empty
|
|
30
58
|
};
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JSXElement, ValidHtmlTags } from '../support/jsx-runtime.ts';
|
|
2
|
-
import type {
|
|
2
|
+
import type { c, JSXElementByFn } from './components.ts';
|
|
3
3
|
|
|
4
4
|
export type Wrapper = Record<string, (count: string) => string>;
|
|
5
5
|
|
|
@@ -16,8 +16,7 @@ export type RenderState<T extends JSXElement, C> = {
|
|
|
16
16
|
/**
|
|
17
17
|
* Renderer
|
|
18
18
|
*/
|
|
19
|
-
export type RenderProvider<C> =
|
|
20
|
-
|
|
21
|
-
{ [K in ValidHtmlTags]: (state: RenderState<JSXElement<K>, C>) => Promise<string>; } &
|
|
19
|
+
export type RenderProvider<C> = { finalize: (text: string, ctx: C, isRoot?: boolean) => string | Promise<string> } & {
|
|
20
|
+
[K in ValidHtmlTags]: (state: RenderState<JSXElement<K>, C>) => Promise<string>;
|
|
22
21
|
// @ts-expect-error - This is a bit of a hack to allow for custom components,
|
|
23
|
-
|
|
22
|
+
} & { [K in keyof typeof c]: (state: RenderState<JSXElementByFn<K>, C>) => Promise<string> };
|
package/src/wrapper.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { EmailResourceLoader, type EmailTemplateLocation, type EmailTemplateModule } from '@travetto/email';
|
|
4
4
|
import { PackageUtil } from '@travetto/manifest';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import type { JSXElement } from '../support/jsx-runtime.ts';
|
|
7
7
|
import { Html } from './render/html.ts';
|
|
8
8
|
import { Markdown } from './render/markdown.ts';
|
|
9
|
+
import { InkyRenderer } from './render/renderer.ts';
|
|
9
10
|
import { Subject } from './render/subject.ts';
|
|
10
|
-
import type { JSXElement } from '../support/jsx-runtime.ts';
|
|
11
11
|
|
|
12
12
|
export async function prepare(node: JSXElement, location: EmailTemplateLocation): Promise<EmailTemplateModule> {
|
|
13
13
|
const ctx = {
|
|
@@ -23,6 +23,6 @@ export async function prepare(node: JSXElement, location: EmailTemplateLocation)
|
|
|
23
23
|
`,
|
|
24
24
|
html: () => InkyRenderer.render(node, Html, ctx),
|
|
25
25
|
text: () => InkyRenderer.render(node, Markdown, ctx),
|
|
26
|
-
subject: () => InkyRenderer.render(node, Subject, ctx)
|
|
26
|
+
subject: () => InkyRenderer.render(node, Subject, ctx)
|
|
27
27
|
};
|
|
28
28
|
}
|