email-editor-core 0.0.4
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 +438 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +260 -0
- package/dist/renderer/blocks/button.d.ts +18 -0
- package/dist/renderer/blocks/button.d.ts.map +1 -0
- package/dist/renderer/blocks/button.js +57 -0
- package/dist/renderer/blocks/divider.d.ts +18 -0
- package/dist/renderer/blocks/divider.d.ts.map +1 -0
- package/dist/renderer/blocks/divider.js +42 -0
- package/dist/renderer/blocks/highlight.d.ts +18 -0
- package/dist/renderer/blocks/highlight.d.ts.map +1 -0
- package/dist/renderer/blocks/highlight.js +49 -0
- package/dist/renderer/blocks/image.d.ts +18 -0
- package/dist/renderer/blocks/image.d.ts.map +1 -0
- package/dist/renderer/blocks/image.js +59 -0
- package/dist/renderer/blocks/paragraph.d.ts +18 -0
- package/dist/renderer/blocks/paragraph.d.ts.map +1 -0
- package/dist/renderer/blocks/paragraph.js +41 -0
- package/dist/renderer/blocks/title.d.ts +18 -0
- package/dist/renderer/blocks/title.d.ts.map +1 -0
- package/dist/renderer/blocks/title.js +49 -0
- package/dist/renderer/parseInlineFormatting.d.ts +14 -0
- package/dist/renderer/parseInlineFormatting.d.ts.map +1 -0
- package/dist/renderer/parseInlineFormatting.js +178 -0
- package/dist/renderer/renderBlock.d.ts +21 -0
- package/dist/renderer/renderBlock.d.ts.map +1 -0
- package/dist/renderer/renderBlock.js +44 -0
- package/dist/renderer/renderEmail.d.ts +26 -0
- package/dist/renderer/renderEmail.d.ts.map +1 -0
- package/dist/renderer/renderEmail.js +275 -0
- package/dist/sanitizer.d.ts +147 -0
- package/dist/sanitizer.d.ts.map +1 -0
- package/dist/sanitizer.js +533 -0
- package/dist/template-config.d.ts +38 -0
- package/dist/template-config.d.ts.map +1 -0
- package/dist/template-config.js +196 -0
- package/dist/test-formatting.d.ts +6 -0
- package/dist/test-formatting.d.ts.map +1 -0
- package/dist/test-formatting.js +132 -0
- package/dist/types.d.ts +243 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/validator.d.ts +86 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +435 -0
- package/package.json +17 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEMPLATE-SPECIFIC RULES AND CONFIGURATION
|
|
3
|
+
* Enforces constraints per template type
|
|
4
|
+
*/
|
|
5
|
+
import type { TemplateConfiguration, BlockType } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* OPEN-FUND TEMPLATE RULES
|
|
8
|
+
* Purpose: Marketing email for new fund launch
|
|
9
|
+
* Structure: Hero image → Title + Paragraphs + CTA → Highlight box
|
|
10
|
+
* Marketing-focused, needs strong CTA
|
|
11
|
+
*/
|
|
12
|
+
export declare const OPEN_FUND_CONFIG: TemplateConfiguration;
|
|
13
|
+
/**
|
|
14
|
+
* CLOSE-FUND TEMPLATE RULES
|
|
15
|
+
* Purpose: Notification that fund is closed, transition to next phase
|
|
16
|
+
* Structure: Announcement → Details → Next steps → Highlight
|
|
17
|
+
* More formal, fewer blocks than open-fund
|
|
18
|
+
*/
|
|
19
|
+
export declare const CLOSE_FUND_CONFIG: TemplateConfiguration;
|
|
20
|
+
/**
|
|
21
|
+
* NEWSLETTER TEMPLATE RULES
|
|
22
|
+
* Purpose: Information and updates about fund performance
|
|
23
|
+
* Structure: Article → Performance details → CTA → Help
|
|
24
|
+
* Education-focused, longer form content allowed
|
|
25
|
+
*/
|
|
26
|
+
export declare const NEWSLETTER_CONFIG: TemplateConfiguration;
|
|
27
|
+
/**
|
|
28
|
+
* Registry of all template configurations
|
|
29
|
+
* Used by validator to look up rules
|
|
30
|
+
*/
|
|
31
|
+
export declare const TEMPLATE_CONFIG_REGISTRY: Record<string, TemplateConfiguration>;
|
|
32
|
+
/**
|
|
33
|
+
* Get template configuration by type
|
|
34
|
+
* @throws Error if template type not found
|
|
35
|
+
*/
|
|
36
|
+
export declare function getTemplateConfig(templateType: string): TemplateConfiguration;
|
|
37
|
+
export declare const BLOCK_CONSTRAINT_MESSAGES: Record<BlockType, string>;
|
|
38
|
+
//# sourceMappingURL=template-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-config.d.ts","sourceRoot":"","sources":["../src/template-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAMnE;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,EAAE,qBA4D9B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,qBAgD/B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,qBA+C/B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAI1E,CAAC;AAEF;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,qBAAqB,CAM7E;AAMD,eAAO,MAAM,yBAAyB,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAO/D,CAAC"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEMPLATE-SPECIFIC RULES AND CONFIGURATION
|
|
3
|
+
* Enforces constraints per template type
|
|
4
|
+
*/
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// TEMPLATE RULES
|
|
7
|
+
// ============================================================================
|
|
8
|
+
/**
|
|
9
|
+
* OPEN-FUND TEMPLATE RULES
|
|
10
|
+
* Purpose: Marketing email for new fund launch
|
|
11
|
+
* Structure: Hero image → Title + Paragraphs + CTA → Highlight box
|
|
12
|
+
* Marketing-focused, needs strong CTA
|
|
13
|
+
*/
|
|
14
|
+
export const OPEN_FUND_CONFIG = {
|
|
15
|
+
templateType: 'open-fund',
|
|
16
|
+
// Which block types are allowed
|
|
17
|
+
allowedBlockTypes: ['title', 'paragraph', 'image', 'button', 'divider', 'highlight-box'],
|
|
18
|
+
// Per-block constraints
|
|
19
|
+
blockConstraints: {
|
|
20
|
+
title: {
|
|
21
|
+
min: 1,
|
|
22
|
+
max: 2,
|
|
23
|
+
required: true,
|
|
24
|
+
},
|
|
25
|
+
paragraph: {
|
|
26
|
+
min: 2,
|
|
27
|
+
max: 5,
|
|
28
|
+
required: true,
|
|
29
|
+
},
|
|
30
|
+
image: {
|
|
31
|
+
min: 0,
|
|
32
|
+
max: 3,
|
|
33
|
+
required: false,
|
|
34
|
+
},
|
|
35
|
+
button: {
|
|
36
|
+
min: 1,
|
|
37
|
+
max: 2,
|
|
38
|
+
required: true, // Must have CTA
|
|
39
|
+
},
|
|
40
|
+
divider: {
|
|
41
|
+
min: 0,
|
|
42
|
+
max: 2,
|
|
43
|
+
required: false,
|
|
44
|
+
},
|
|
45
|
+
'highlight-box': {
|
|
46
|
+
min: 0,
|
|
47
|
+
max: 1,
|
|
48
|
+
required: false,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
// Global constraints
|
|
52
|
+
maxTotalBlocks: 15,
|
|
53
|
+
allowReordering: true,
|
|
54
|
+
// Recommended order (but not enforced if allowReordering: true)
|
|
55
|
+
requireBlockOrder: [
|
|
56
|
+
'title',
|
|
57
|
+
'paragraph',
|
|
58
|
+
'image',
|
|
59
|
+
'paragraph',
|
|
60
|
+
'button',
|
|
61
|
+
'highlight-box',
|
|
62
|
+
],
|
|
63
|
+
// Must always exist
|
|
64
|
+
mandatoryBlocks: ['title', 'paragraph', 'button'],
|
|
65
|
+
// Fixed sections
|
|
66
|
+
helpSectionRequired: true,
|
|
67
|
+
complianceSectionRequired: true,
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* CLOSE-FUND TEMPLATE RULES
|
|
71
|
+
* Purpose: Notification that fund is closed, transition to next phase
|
|
72
|
+
* Structure: Announcement → Details → Next steps → Highlight
|
|
73
|
+
* More formal, fewer blocks than open-fund
|
|
74
|
+
*/
|
|
75
|
+
export const CLOSE_FUND_CONFIG = {
|
|
76
|
+
templateType: 'close-fund',
|
|
77
|
+
allowedBlockTypes: ['title', 'paragraph', 'image', 'button', 'divider', 'highlight-box'],
|
|
78
|
+
blockConstraints: {
|
|
79
|
+
title: {
|
|
80
|
+
min: 0,
|
|
81
|
+
max: 1,
|
|
82
|
+
required: false,
|
|
83
|
+
},
|
|
84
|
+
paragraph: {
|
|
85
|
+
min: 2,
|
|
86
|
+
max: 4,
|
|
87
|
+
required: true,
|
|
88
|
+
},
|
|
89
|
+
image: {
|
|
90
|
+
min: 0,
|
|
91
|
+
max: 2,
|
|
92
|
+
required: false,
|
|
93
|
+
},
|
|
94
|
+
button: {
|
|
95
|
+
min: 0,
|
|
96
|
+
max: 1,
|
|
97
|
+
required: false, // No hard CTA for close
|
|
98
|
+
},
|
|
99
|
+
divider: {
|
|
100
|
+
min: 0,
|
|
101
|
+
max: 1,
|
|
102
|
+
required: false,
|
|
103
|
+
},
|
|
104
|
+
'highlight-box': {
|
|
105
|
+
min: 0,
|
|
106
|
+
max: 1,
|
|
107
|
+
required: false,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
maxTotalBlocks: 12,
|
|
111
|
+
allowReordering: true,
|
|
112
|
+
// Suggested structure but not required
|
|
113
|
+
requireBlockOrder: ['paragraph', 'divider', 'paragraph', 'highlight-box'],
|
|
114
|
+
mandatoryBlocks: ['paragraph'],
|
|
115
|
+
helpSectionRequired: true,
|
|
116
|
+
complianceSectionRequired: true,
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* NEWSLETTER TEMPLATE RULES
|
|
120
|
+
* Purpose: Information and updates about fund performance
|
|
121
|
+
* Structure: Article → Performance details → CTA → Help
|
|
122
|
+
* Education-focused, longer form content allowed
|
|
123
|
+
*/
|
|
124
|
+
export const NEWSLETTER_CONFIG = {
|
|
125
|
+
templateType: 'newsletter',
|
|
126
|
+
allowedBlockTypes: ['title', 'paragraph', 'image', 'button', 'divider', 'highlight-box'],
|
|
127
|
+
blockConstraints: {
|
|
128
|
+
title: {
|
|
129
|
+
min: 1,
|
|
130
|
+
max: 2,
|
|
131
|
+
required: true,
|
|
132
|
+
},
|
|
133
|
+
paragraph: {
|
|
134
|
+
min: 3,
|
|
135
|
+
max: 8,
|
|
136
|
+
required: true, // Longer articles
|
|
137
|
+
},
|
|
138
|
+
image: {
|
|
139
|
+
min: 1,
|
|
140
|
+
max: 4,
|
|
141
|
+
required: true, // Always include performance charts
|
|
142
|
+
},
|
|
143
|
+
button: {
|
|
144
|
+
min: 0,
|
|
145
|
+
max: 2,
|
|
146
|
+
required: false,
|
|
147
|
+
},
|
|
148
|
+
divider: {
|
|
149
|
+
min: 0,
|
|
150
|
+
max: 3,
|
|
151
|
+
required: false,
|
|
152
|
+
},
|
|
153
|
+
'highlight-box': {
|
|
154
|
+
min: 0,
|
|
155
|
+
max: 2,
|
|
156
|
+
required: false,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
maxTotalBlocks: 20, // Most flexible
|
|
160
|
+
allowReordering: true,
|
|
161
|
+
requireBlockOrder: ['title', 'image', 'paragraph', 'divider', 'paragraph', 'button'],
|
|
162
|
+
mandatoryBlocks: ['title', 'paragraph', 'image'],
|
|
163
|
+
helpSectionRequired: true,
|
|
164
|
+
complianceSectionRequired: true,
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Registry of all template configurations
|
|
168
|
+
* Used by validator to look up rules
|
|
169
|
+
*/
|
|
170
|
+
export const TEMPLATE_CONFIG_REGISTRY = {
|
|
171
|
+
'open-fund': OPEN_FUND_CONFIG,
|
|
172
|
+
'close-fund': CLOSE_FUND_CONFIG,
|
|
173
|
+
'newsletter': NEWSLETTER_CONFIG,
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Get template configuration by type
|
|
177
|
+
* @throws Error if template type not found
|
|
178
|
+
*/
|
|
179
|
+
export function getTemplateConfig(templateType) {
|
|
180
|
+
const config = TEMPLATE_CONFIG_REGISTRY[templateType];
|
|
181
|
+
if (!config) {
|
|
182
|
+
throw new Error(`Unknown template type: ${templateType}`);
|
|
183
|
+
}
|
|
184
|
+
return config;
|
|
185
|
+
}
|
|
186
|
+
// ============================================================================
|
|
187
|
+
// CONSTRAINT DESCRIPTIONS (for error messages)
|
|
188
|
+
// ============================================================================
|
|
189
|
+
export const BLOCK_CONSTRAINT_MESSAGES = {
|
|
190
|
+
title: 'Section heading (appears once or twice)',
|
|
191
|
+
paragraph: 'Text content with optional inline formatting',
|
|
192
|
+
image: 'Responsive image with alt text',
|
|
193
|
+
button: 'Call-to-action button',
|
|
194
|
+
divider: 'Visual separator line',
|
|
195
|
+
'highlight-box': 'Featured callout box',
|
|
196
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-formatting.d.ts","sourceRoot":"","sources":["../src/test-formatting.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test parseInlineFormatting function
|
|
3
|
+
* Run with: npx ts-node test-formatting.ts
|
|
4
|
+
*/
|
|
5
|
+
import { parseInlineFormatting } from './renderer/parseInlineFormatting.js';
|
|
6
|
+
// Test cases
|
|
7
|
+
const testCases = [
|
|
8
|
+
{
|
|
9
|
+
input: 'NOBI Dana Kripto: Solusi Investasi Kripto yang {{bold:#008867}}#SemudahItu{{/bold}}',
|
|
10
|
+
expected: 'NOBI Dana Kripto: Solusi Investasi Kripto yang <span style="color:#008867;font-weight:700;">#SemudahItu</span>',
|
|
11
|
+
description: 'Basic formatting with accent color',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
input: 'Start {{bold:#FF0000}}red text{{/bold}} middle {{bold:#0000FF}}blue text{{/bold}} end',
|
|
15
|
+
expected: 'Start <span style="color:#FF0000;font-weight:700;">red text</span> middle <span style="color:#0000FF;font-weight:700;">blue text</span> end',
|
|
16
|
+
description: 'Multiple formatting tokens',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
input: 'No formatting here',
|
|
20
|
+
expected: 'No formatting here',
|
|
21
|
+
description: 'No tokens present',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
input: 'Invalid {{bold:NOTACOLOR}}text{{/bold}} should stay as is',
|
|
25
|
+
expected: 'Invalid {{bold:NOTACOLOR}}text{{/bold}} should stay as is',
|
|
26
|
+
description: 'Invalid color format ignored',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
input: 'Valid 3-char hex {{bold:#F0F}}text{{/bold}} works',
|
|
30
|
+
expected: 'Valid 3-char hex <span style="color:#F0F;font-weight:700;">text</span> works',
|
|
31
|
+
description: '3-character hex color supported',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
input: 'Visit {{link:https://example.com}}our website{{/link}} for more',
|
|
35
|
+
expected: 'Visit <a href="https://example.com" style="color:#008867;text-decoration:underline;font-weight:600;" target="_blank" rel="noopener noreferrer">our website</a> for more',
|
|
36
|
+
description: 'Basic link with https',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
input: 'Email us {{link:mailto:hello@example.com}}here{{/link}} anytime',
|
|
40
|
+
expected: 'Email us <a href="mailto:hello@example.com" style="color:#008867;text-decoration:underline;font-weight:600;" target="_blank" rel="noopener noreferrer">here</a> anytime',
|
|
41
|
+
description: 'Email link with mailto',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
input: 'Chat {{link:https://wa.me/1234567890}}on WhatsApp{{/link}} now',
|
|
45
|
+
expected: 'Chat <a href="https://wa.me/1234567890" style="color:#008867;text-decoration:underline;font-weight:600;" target="_blank" rel="noopener noreferrer">on WhatsApp</a> now',
|
|
46
|
+
description: 'WhatsApp link with wa.me',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
input: 'Invalid {{link:javascript:alert(1)}}attack{{/link}} blocked',
|
|
50
|
+
expected: 'Invalid attack blocked',
|
|
51
|
+
description: 'JavaScript URLs rejected',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
input: 'Invalid {{link:data:text/html}}data{{/link}} blocked',
|
|
55
|
+
expected: 'Invalid data blocked',
|
|
56
|
+
description: 'Data URLs rejected',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
input: 'Both {{bold:#008867}}bold{{/bold}} and {{link:https://example.com}}link{{/link}} work',
|
|
60
|
+
expected: 'Both <span style="color:#008867;font-weight:700;">bold</span> and <a href="https://example.com" style="color:#008867;text-decoration:underline;font-weight:600;" target="_blank" rel="noopener noreferrer">link</a> work',
|
|
61
|
+
description: 'Both bold and link tokens in same text',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
input: 'Combined {{bold:#FF0000}}bold{{/bold}} {{link:https://example.com}}link{{/link}} test',
|
|
65
|
+
expected: 'Combined <span style="color:#FF0000;font-weight:700;">bold</span> <a href="https://example.com" style="color:#008867;text-decoration:underline;font-weight:600;" target="_blank" rel="noopener noreferrer">link</a> test',
|
|
66
|
+
description: 'Adjacent bold and link tokens',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
input: 'Combined {{link:https://example.com|bold|color:#FF0000}}bold red link{{/link}} test',
|
|
70
|
+
expected: 'Combined <a href="https://example.com" style="color:#FF0000;text-decoration:underline;font-weight:700;" target="_blank" rel="noopener noreferrer">bold red link</a> test',
|
|
71
|
+
description: 'Link with bold and custom color',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
input: 'Just {{link:https://example.com|bold}}bold link{{/link}} no color',
|
|
75
|
+
expected: 'Just <a href="https://example.com" style="color:#008867;text-decoration:underline;font-weight:700;" target="_blank" rel="noopener noreferrer">bold link</a> no color',
|
|
76
|
+
description: 'Link with bold only',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
input: 'Just {{link:https://example.com|color:#0000FF}}colored link{{/link}} no bold',
|
|
80
|
+
expected: 'Just <a href="https://example.com" style="color:#0000FF;text-decoration:underline;font-weight:600;" target="_blank" rel="noopener noreferrer">colored link</a> no bold',
|
|
81
|
+
description: 'Link with color only',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
input: 'Invalid {{link:https://example.com|invalid}}modifier{{/link}} ignored',
|
|
85
|
+
expected: 'Invalid <a href="https://example.com" style="color:#008867;text-decoration:underline;font-weight:600;" target="_blank" rel="noopener noreferrer">modifier</a> ignored',
|
|
86
|
+
description: 'Invalid modifier ignored gracefully',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
input: 'Text with {{style:bold}}strong emphasis{{/style}} word',
|
|
90
|
+
expected: 'Text with <span style="font-weight:700;">strong emphasis</span> word',
|
|
91
|
+
description: 'Style token with bold weight only',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
input: 'Text with {{style:color:#FF0000}}red text{{/style}} here',
|
|
95
|
+
expected: 'Text with <span style="color:#FF0000;">red text</span> here',
|
|
96
|
+
description: 'Style token with color only',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
input: 'Text with {{style:semibold|color:#008867}}styled{{/style}} word',
|
|
100
|
+
expected: 'Text with <span style="color:#008867;font-weight:600;">styled</span> word',
|
|
101
|
+
description: 'Style token with semibold and color',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
input: 'Text with {{style:bold|color:#0000FF}}bold blue{{/style}} text',
|
|
105
|
+
expected: 'Text with <span style="color:#0000FF;font-weight:700;">bold blue</span> text',
|
|
106
|
+
description: 'Style token with bold and custom color',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
input: 'Invalid {{style:invalid}}token{{/style}} ignored',
|
|
110
|
+
expected: 'Invalid token ignored',
|
|
111
|
+
description: 'Invalid style token (no valid options) renders plain text',
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
console.log('Testing parseInlineFormatting function...\n');
|
|
115
|
+
let passed = 0;
|
|
116
|
+
let failed = 0;
|
|
117
|
+
testCases.forEach((testCase, index) => {
|
|
118
|
+
const result = parseInlineFormatting(testCase.input);
|
|
119
|
+
const isPass = result === testCase.expected;
|
|
120
|
+
if (isPass) {
|
|
121
|
+
passed++;
|
|
122
|
+
console.log(`✓ Test ${index + 1}: ${testCase.description}`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
failed++;
|
|
126
|
+
console.log(`✗ Test ${index + 1}: ${testCase.description}`);
|
|
127
|
+
console.log(` Input: ${testCase.input}`);
|
|
128
|
+
console.log(` Expected: ${testCase.expected}`);
|
|
129
|
+
console.log(` Got: ${result}`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
console.log(`\n${passed} passed, ${failed} failed out of ${testCases.length} tests`);
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORE DATA MODEL FOR BLOCK-BASED HTML EMAIL GENERATOR
|
|
3
|
+
* Rock-solid foundation for email template management
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Supported email template types
|
|
7
|
+
* Each has distinct allowed blocks and layout constraints
|
|
8
|
+
*/
|
|
9
|
+
export type TemplateType = "open-fund" | "close-fund" | "newsletter";
|
|
10
|
+
/**
|
|
11
|
+
* All supported block types in the email body
|
|
12
|
+
* Restricted set to maintain email client compatibility
|
|
13
|
+
*/
|
|
14
|
+
export type BlockType = "title" | "paragraph" | "image" | "button" | "divider" | "highlight-box";
|
|
15
|
+
/**
|
|
16
|
+
* Allowed inline HTML tags for text content
|
|
17
|
+
* Restricted to ensure email client compatibility and prevent XSS
|
|
18
|
+
*/
|
|
19
|
+
export type AllowedInlineTag = "strong" | "b" | "em" | "i" | "u" | "a" | "br";
|
|
20
|
+
/**
|
|
21
|
+
* Text sanitization configuration
|
|
22
|
+
* Specifies which inline tags are allowed per block type
|
|
23
|
+
*/
|
|
24
|
+
export interface TextSanitizationConfig {
|
|
25
|
+
stripAllHTMLExcept: AllowedInlineTag[];
|
|
26
|
+
stripAllStyles: boolean;
|
|
27
|
+
escapeUnsafeCharacters: boolean;
|
|
28
|
+
tagRestrictions: {
|
|
29
|
+
a: {
|
|
30
|
+
allowedAttributes: ["href"];
|
|
31
|
+
requireHttpProtocol: boolean;
|
|
32
|
+
};
|
|
33
|
+
img: {
|
|
34
|
+
allowedAttributes: ["src", "alt", "width", "height"];
|
|
35
|
+
requireHttpProtocol: boolean;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Title block: Single heading for sections
|
|
41
|
+
* Max 2 allowed per template typically
|
|
42
|
+
*/
|
|
43
|
+
export interface TitleBlock {
|
|
44
|
+
type: "title";
|
|
45
|
+
id: string;
|
|
46
|
+
content: string;
|
|
47
|
+
level: "h1" | "h2" | "h3";
|
|
48
|
+
color?: string;
|
|
49
|
+
paddingBottom?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Paragraph block: Text content with optional inline formatting
|
|
53
|
+
* Most flexible block type
|
|
54
|
+
*/
|
|
55
|
+
export interface ParagraphBlock {
|
|
56
|
+
type: "paragraph";
|
|
57
|
+
id: string;
|
|
58
|
+
content: string;
|
|
59
|
+
color?: string;
|
|
60
|
+
lineHeight?: number;
|
|
61
|
+
paddingBottom?: number;
|
|
62
|
+
textAlign?: "left" | "center" | "right";
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Image block: Responsive image with alt text
|
|
66
|
+
* Must have accessible alt text
|
|
67
|
+
*/
|
|
68
|
+
export interface ImageBlock {
|
|
69
|
+
type: "image";
|
|
70
|
+
id: string;
|
|
71
|
+
src: string;
|
|
72
|
+
alt: string;
|
|
73
|
+
width?: number;
|
|
74
|
+
height?: number;
|
|
75
|
+
maxWidth?: number;
|
|
76
|
+
borderRadius?: number;
|
|
77
|
+
paddingBottom?: number;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Button block: Call-to-action button
|
|
81
|
+
* Limited to 1-2 per template
|
|
82
|
+
*/
|
|
83
|
+
export interface ButtonBlock {
|
|
84
|
+
type: "button";
|
|
85
|
+
id: string;
|
|
86
|
+
label: string;
|
|
87
|
+
href: string;
|
|
88
|
+
backgroundColor?: string;
|
|
89
|
+
textColor?: string;
|
|
90
|
+
padding?: string;
|
|
91
|
+
borderRadius?: number;
|
|
92
|
+
marginTop?: number;
|
|
93
|
+
paddingBottom?: number;
|
|
94
|
+
align?: "left" | "center" | "right";
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Divider block: Visual separator
|
|
98
|
+
* Horizontal line
|
|
99
|
+
*/
|
|
100
|
+
export interface DividerBlock {
|
|
101
|
+
type: "divider";
|
|
102
|
+
id: string;
|
|
103
|
+
color?: string;
|
|
104
|
+
height?: number;
|
|
105
|
+
margin?: number;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Highlight box: Featured content area
|
|
109
|
+
* Used for callouts, warnings, feature highlights
|
|
110
|
+
*/
|
|
111
|
+
export interface HighlightBoxBlock {
|
|
112
|
+
type: "highlight-box";
|
|
113
|
+
id: string;
|
|
114
|
+
content: string;
|
|
115
|
+
backgroundColor: string;
|
|
116
|
+
borderColor?: string;
|
|
117
|
+
borderLeft?: boolean;
|
|
118
|
+
padding?: string;
|
|
119
|
+
paddingBottom?: number;
|
|
120
|
+
borderRadius?: number;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Union type for all possible blocks
|
|
124
|
+
*/
|
|
125
|
+
export type Block = TitleBlock | ParagraphBlock | ImageBlock | ButtonBlock | DividerBlock | HighlightBoxBlock;
|
|
126
|
+
/**
|
|
127
|
+
* Fixed section: Template logo/header
|
|
128
|
+
* Consistent across all templates
|
|
129
|
+
*/
|
|
130
|
+
export interface EmailHeader {
|
|
131
|
+
logoUrl: string;
|
|
132
|
+
logoHeight?: number;
|
|
133
|
+
showDarkModeVariant?: boolean;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Fixed section: Contact and support info
|
|
137
|
+
* Consistent across all templates
|
|
138
|
+
*/
|
|
139
|
+
export interface HelpSection {
|
|
140
|
+
title: string;
|
|
141
|
+
description: string;
|
|
142
|
+
contactItems: {
|
|
143
|
+
type: "email" | "phone" | "whatsapp";
|
|
144
|
+
label: string;
|
|
145
|
+
value: string;
|
|
146
|
+
href: string;
|
|
147
|
+
}[];
|
|
148
|
+
imageUrl?: string;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Fixed section: Compliance and legal
|
|
152
|
+
* Varies per template
|
|
153
|
+
*/
|
|
154
|
+
export interface ComplianceSection {
|
|
155
|
+
text: string;
|
|
156
|
+
sandboxNumber?: string;
|
|
157
|
+
backgroundColor?: string;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Fixed section: Footer
|
|
161
|
+
* Company information
|
|
162
|
+
*/
|
|
163
|
+
export interface EmailFooter {
|
|
164
|
+
logoUrl: string;
|
|
165
|
+
companyName: string;
|
|
166
|
+
address: string;
|
|
167
|
+
socialLinks?: {
|
|
168
|
+
platform: "instagram" | "twitter" | "linkedin" | "facebook" | "whatsapp" | "email";
|
|
169
|
+
url: string;
|
|
170
|
+
}[];
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Email document: Complete structure
|
|
174
|
+
* Represents a fully-formed email with template, body blocks, and fixed sections
|
|
175
|
+
*/
|
|
176
|
+
export interface EmailDocument {
|
|
177
|
+
id: string;
|
|
178
|
+
templateType: TemplateType;
|
|
179
|
+
version: number;
|
|
180
|
+
createdAt: Date;
|
|
181
|
+
updatedAt: Date;
|
|
182
|
+
blocks: Block[];
|
|
183
|
+
header: EmailHeader;
|
|
184
|
+
body: {
|
|
185
|
+
blocks: Block[];
|
|
186
|
+
};
|
|
187
|
+
helpSection: HelpSection;
|
|
188
|
+
complianceSection: ComplianceSection;
|
|
189
|
+
footer: EmailFooter;
|
|
190
|
+
personalizationVariables?: {
|
|
191
|
+
[key: string]: string;
|
|
192
|
+
};
|
|
193
|
+
isValid?: boolean;
|
|
194
|
+
validationErrors?: ValidationError[];
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Validation error details
|
|
198
|
+
*/
|
|
199
|
+
export interface ValidationError {
|
|
200
|
+
code: string;
|
|
201
|
+
message: string;
|
|
202
|
+
blockId?: string;
|
|
203
|
+
blockType?: BlockType;
|
|
204
|
+
severity: "error" | "warning";
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Template configuration: Rules per template
|
|
208
|
+
* Enforced by validator
|
|
209
|
+
*/
|
|
210
|
+
export interface TemplateConfiguration {
|
|
211
|
+
templateType: TemplateType;
|
|
212
|
+
allowedBlockTypes: BlockType[];
|
|
213
|
+
blockConstraints: {
|
|
214
|
+
[K in BlockType]?: {
|
|
215
|
+
min: number;
|
|
216
|
+
max: number;
|
|
217
|
+
required: boolean;
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
maxTotalBlocks: number;
|
|
221
|
+
allowReordering: boolean;
|
|
222
|
+
requireBlockOrder?: (BlockType | "any")[];
|
|
223
|
+
mandatoryBlocks: BlockType[];
|
|
224
|
+
helpSectionRequired: boolean;
|
|
225
|
+
complianceSectionRequired: boolean;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Context for validation operations
|
|
229
|
+
* Passed to validator to enforce rules
|
|
230
|
+
*/
|
|
231
|
+
export interface ValidationContext {
|
|
232
|
+
templateConfig: TemplateConfiguration;
|
|
233
|
+
email: EmailDocument;
|
|
234
|
+
strict: boolean;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Context for text sanitization
|
|
238
|
+
*/
|
|
239
|
+
export interface SanitizationContext {
|
|
240
|
+
blockType: BlockType;
|
|
241
|
+
sanitizationConfig: TextSanitizationConfig;
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|