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,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VALIDATION STRATEGY AND IMPLEMENTATION
|
|
3
|
+
* Enforces data integrity and template compliance
|
|
4
|
+
*/
|
|
5
|
+
import type { EmailDocument, ValidationError, ValidationContext } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Validation rule interface
|
|
8
|
+
* Each rule checks a specific constraint
|
|
9
|
+
*/
|
|
10
|
+
interface ValidationRule {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
validate: (context: ValidationContext) => ValidationError[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* RULE 1: Block type availability
|
|
17
|
+
* Ensures only allowed block types for this template are used
|
|
18
|
+
*/
|
|
19
|
+
export declare const blockTypeAllowedRule: ValidationRule;
|
|
20
|
+
/**
|
|
21
|
+
* RULE 2: Block count constraints
|
|
22
|
+
* Enforces min/max count per block type
|
|
23
|
+
*/
|
|
24
|
+
export declare const blockCountConstraintRule: ValidationRule;
|
|
25
|
+
/**
|
|
26
|
+
* RULE 3: Total block count
|
|
27
|
+
* Enforces maximum total blocks in body
|
|
28
|
+
*/
|
|
29
|
+
export declare const totalBlockCountRule: ValidationRule;
|
|
30
|
+
/**
|
|
31
|
+
* RULE 4: Mandatory blocks
|
|
32
|
+
* Ensures all required block types are present
|
|
33
|
+
*/
|
|
34
|
+
export declare const mandatoryBlocksRule: ValidationRule;
|
|
35
|
+
/**
|
|
36
|
+
* RULE 5: Block order consistency
|
|
37
|
+
* If reordering is disabled, checks that blocks follow required order
|
|
38
|
+
*/
|
|
39
|
+
export declare const blockOrderRule: ValidationRule;
|
|
40
|
+
/**
|
|
41
|
+
* RULE 6: Fixed sections presence
|
|
42
|
+
* Ensures help and compliance sections are present if required
|
|
43
|
+
*/
|
|
44
|
+
export declare const fixedSectionsRule: ValidationRule;
|
|
45
|
+
/**
|
|
46
|
+
* RULE 7: Block IDs uniqueness
|
|
47
|
+
* Ensures all block IDs are unique
|
|
48
|
+
*/
|
|
49
|
+
export declare const blockIdUniquenessRule: ValidationRule;
|
|
50
|
+
/**
|
|
51
|
+
* RULE 8: Block content validation
|
|
52
|
+
* Type-specific content rules
|
|
53
|
+
*/
|
|
54
|
+
export declare const blockContentValidationRule: ValidationRule;
|
|
55
|
+
/**
|
|
56
|
+
* RULE 9: Color format validation
|
|
57
|
+
* Ensures all color values are valid hex colors
|
|
58
|
+
*/
|
|
59
|
+
export declare const colorFormatRule: ValidationRule;
|
|
60
|
+
/**
|
|
61
|
+
* MAIN VALIDATION FUNCTION
|
|
62
|
+
*
|
|
63
|
+
* @param email - The email document to validate
|
|
64
|
+
* @param strict - If true, warnings become errors
|
|
65
|
+
* @returns ValidationError array
|
|
66
|
+
*/
|
|
67
|
+
export declare function validateEmailDocument(email: EmailDocument, strict?: boolean): ValidationError[];
|
|
68
|
+
/**
|
|
69
|
+
* Check if email document is valid
|
|
70
|
+
* @returns true if no errors, false otherwise
|
|
71
|
+
*/
|
|
72
|
+
export declare function isEmailDocumentValid(email: EmailDocument, strict?: boolean): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Get validation summary
|
|
75
|
+
* Useful for reporting
|
|
76
|
+
*/
|
|
77
|
+
export interface ValidationSummary {
|
|
78
|
+
isValid: boolean;
|
|
79
|
+
errorCount: number;
|
|
80
|
+
warningCount: number;
|
|
81
|
+
errors: ValidationError[];
|
|
82
|
+
warnings: ValidationError[];
|
|
83
|
+
}
|
|
84
|
+
export declare function getValidationSummary(email: EmailDocument, strict?: boolean): ValidationSummary;
|
|
85
|
+
export {};
|
|
86
|
+
//# sourceMappingURL=validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../src/validator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAGV,aAAa,EACb,eAAe,EACf,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAOpB;;;GAGG;AACH,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,eAAe,EAAE,CAAC;CAC7D;AAMD;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,cAqBlC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,EAAE,cA0CtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,cAiBjC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,cAsBjC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,cA0C5B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,cAyB/B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,cAsBnC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,EAAE,cA0HxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,cA+C7B,CAAC;AAqBF;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,aAAa,EACpB,MAAM,GAAE,OAAe,GACtB,eAAe,EAAE,CAiBnB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,aAAa,EACpB,MAAM,GAAE,OAAe,GACtB,OAAO,CAGT;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,aAAa,EACpB,MAAM,GAAE,OAAe,GACtB,iBAAiB,CAanB"}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VALIDATION STRATEGY AND IMPLEMENTATION
|
|
3
|
+
* Enforces data integrity and template compliance
|
|
4
|
+
*/
|
|
5
|
+
import { getTemplateConfig } from './template-config.js';
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// CORE VALIDATORS
|
|
8
|
+
// ============================================================================
|
|
9
|
+
/**
|
|
10
|
+
* RULE 1: Block type availability
|
|
11
|
+
* Ensures only allowed block types for this template are used
|
|
12
|
+
*/
|
|
13
|
+
export const blockTypeAllowedRule = {
|
|
14
|
+
name: 'BLOCK_TYPE_ALLOWED',
|
|
15
|
+
description: 'Block type is allowed for this template',
|
|
16
|
+
validate: (context) => {
|
|
17
|
+
const { email, templateConfig } = context;
|
|
18
|
+
const errors = [];
|
|
19
|
+
email.body.blocks.forEach((block) => {
|
|
20
|
+
if (!templateConfig.allowedBlockTypes.includes(block.type)) {
|
|
21
|
+
errors.push({
|
|
22
|
+
code: 'BLOCK_TYPE_NOT_ALLOWED',
|
|
23
|
+
message: `Block type "${block.type}" is not allowed in ${templateConfig.templateType} template`,
|
|
24
|
+
blockId: block.id,
|
|
25
|
+
blockType: block.type,
|
|
26
|
+
severity: 'error',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return errors;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* RULE 2: Block count constraints
|
|
35
|
+
* Enforces min/max count per block type
|
|
36
|
+
*/
|
|
37
|
+
export const blockCountConstraintRule = {
|
|
38
|
+
name: 'BLOCK_COUNT_CONSTRAINT',
|
|
39
|
+
description: 'Block count respects per-type min/max constraints',
|
|
40
|
+
validate: (context) => {
|
|
41
|
+
const { email, templateConfig } = context;
|
|
42
|
+
const errors = [];
|
|
43
|
+
// Count blocks by type
|
|
44
|
+
const blockCounts = new Map();
|
|
45
|
+
email.body.blocks.forEach((block) => {
|
|
46
|
+
blockCounts.set(block.type, (blockCounts.get(block.type) ?? 0) + 1);
|
|
47
|
+
});
|
|
48
|
+
// Check each constraint
|
|
49
|
+
Object.entries(templateConfig.blockConstraints).forEach(([blockType, constraint]) => {
|
|
50
|
+
if (!constraint)
|
|
51
|
+
return;
|
|
52
|
+
const count = blockCounts.get(blockType) ?? 0;
|
|
53
|
+
// Check minimum
|
|
54
|
+
if (count < constraint.min) {
|
|
55
|
+
errors.push({
|
|
56
|
+
code: 'MIN_BLOCKS_NOT_MET',
|
|
57
|
+
message: `Minimum ${constraint.min} ${blockType} block(s) required. Found: ${count}`,
|
|
58
|
+
blockType: blockType,
|
|
59
|
+
severity: constraint.required ? 'error' : 'warning',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// Check maximum
|
|
63
|
+
if (count > constraint.max) {
|
|
64
|
+
errors.push({
|
|
65
|
+
code: 'MAX_BLOCKS_EXCEEDED',
|
|
66
|
+
message: `Maximum ${constraint.max} ${blockType} block(s) allowed. Found: ${count}`,
|
|
67
|
+
blockType: blockType,
|
|
68
|
+
severity: 'error',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return errors;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* RULE 3: Total block count
|
|
77
|
+
* Enforces maximum total blocks in body
|
|
78
|
+
*/
|
|
79
|
+
export const totalBlockCountRule = {
|
|
80
|
+
name: 'TOTAL_BLOCK_COUNT',
|
|
81
|
+
description: 'Total block count does not exceed template maximum',
|
|
82
|
+
validate: (context) => {
|
|
83
|
+
const { email, templateConfig } = context;
|
|
84
|
+
const errors = [];
|
|
85
|
+
if (email.body.blocks.length > templateConfig.maxTotalBlocks) {
|
|
86
|
+
errors.push({
|
|
87
|
+
code: 'MAX_TOTAL_BLOCKS_EXCEEDED',
|
|
88
|
+
message: `Maximum ${templateConfig.maxTotalBlocks} total blocks allowed. Found: ${email.body.blocks.length}`,
|
|
89
|
+
severity: 'error',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return errors;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* RULE 4: Mandatory blocks
|
|
97
|
+
* Ensures all required block types are present
|
|
98
|
+
*/
|
|
99
|
+
export const mandatoryBlocksRule = {
|
|
100
|
+
name: 'MANDATORY_BLOCKS',
|
|
101
|
+
description: 'All mandatory block types are present',
|
|
102
|
+
validate: (context) => {
|
|
103
|
+
const { email, templateConfig } = context;
|
|
104
|
+
const errors = [];
|
|
105
|
+
const presentBlockTypes = new Set(email.body.blocks.map((b) => b.type));
|
|
106
|
+
templateConfig.mandatoryBlocks.forEach((blockType) => {
|
|
107
|
+
if (!presentBlockTypes.has(blockType)) {
|
|
108
|
+
errors.push({
|
|
109
|
+
code: 'MANDATORY_BLOCK_MISSING',
|
|
110
|
+
message: `Mandatory block type "${blockType}" is missing`,
|
|
111
|
+
blockType,
|
|
112
|
+
severity: 'error',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
return errors;
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* RULE 5: Block order consistency
|
|
121
|
+
* If reordering is disabled, checks that blocks follow required order
|
|
122
|
+
*/
|
|
123
|
+
export const blockOrderRule = {
|
|
124
|
+
name: 'BLOCK_ORDER',
|
|
125
|
+
description: 'Blocks are in the recommended order',
|
|
126
|
+
validate: (context) => {
|
|
127
|
+
const { email, templateConfig, strict } = context;
|
|
128
|
+
const errors = [];
|
|
129
|
+
// Skip if reordering is allowed or no order requirement
|
|
130
|
+
if (templateConfig.allowReordering || !templateConfig.requireBlockOrder) {
|
|
131
|
+
return errors;
|
|
132
|
+
}
|
|
133
|
+
// Check if blocks follow required order
|
|
134
|
+
let lastOrderIndex = -1;
|
|
135
|
+
for (const block of email.body.blocks) {
|
|
136
|
+
const requireOrder = templateConfig.requireBlockOrder;
|
|
137
|
+
let blockOrderIndex = -1;
|
|
138
|
+
for (let i = 0; i < requireOrder.length; i++) {
|
|
139
|
+
if (requireOrder[i] === 'any' || requireOrder[i] === block.type) {
|
|
140
|
+
blockOrderIndex = i;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (blockOrderIndex < lastOrderIndex) {
|
|
145
|
+
errors.push({
|
|
146
|
+
code: 'BLOCK_ORDER_VIOLATION',
|
|
147
|
+
message: `Block "${block.type}" appears out of order. Expected order: ${requireOrder.join(' → ')}`,
|
|
148
|
+
blockId: block.id,
|
|
149
|
+
blockType: block.type,
|
|
150
|
+
severity: strict ? 'error' : 'warning',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (blockOrderIndex >= 0) {
|
|
154
|
+
lastOrderIndex = blockOrderIndex;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return errors;
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* RULE 6: Fixed sections presence
|
|
162
|
+
* Ensures help and compliance sections are present if required
|
|
163
|
+
*/
|
|
164
|
+
export const fixedSectionsRule = {
|
|
165
|
+
name: 'FIXED_SECTIONS',
|
|
166
|
+
description: 'Required fixed sections are present',
|
|
167
|
+
validate: (context) => {
|
|
168
|
+
const { email, templateConfig } = context;
|
|
169
|
+
const errors = [];
|
|
170
|
+
if (templateConfig.helpSectionRequired && !email.helpSection) {
|
|
171
|
+
errors.push({
|
|
172
|
+
code: 'HELP_SECTION_MISSING',
|
|
173
|
+
message: 'Help section is required for this template',
|
|
174
|
+
severity: 'error',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (templateConfig.complianceSectionRequired && !email.complianceSection) {
|
|
178
|
+
errors.push({
|
|
179
|
+
code: 'COMPLIANCE_SECTION_MISSING',
|
|
180
|
+
message: 'Compliance section is required for this template',
|
|
181
|
+
severity: 'error',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return errors;
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* RULE 7: Block IDs uniqueness
|
|
189
|
+
* Ensures all block IDs are unique
|
|
190
|
+
*/
|
|
191
|
+
export const blockIdUniquenessRule = {
|
|
192
|
+
name: 'BLOCK_ID_UNIQUENESS',
|
|
193
|
+
description: 'All block IDs are unique',
|
|
194
|
+
validate: (context) => {
|
|
195
|
+
const { email } = context;
|
|
196
|
+
const errors = [];
|
|
197
|
+
const idSet = new Set();
|
|
198
|
+
email.body.blocks.forEach((block) => {
|
|
199
|
+
if (idSet.has(block.id)) {
|
|
200
|
+
errors.push({
|
|
201
|
+
code: 'DUPLICATE_BLOCK_ID',
|
|
202
|
+
message: `Duplicate block ID: ${block.id}`,
|
|
203
|
+
blockId: block.id,
|
|
204
|
+
severity: 'error',
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
idSet.add(block.id);
|
|
208
|
+
});
|
|
209
|
+
return errors;
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* RULE 8: Block content validation
|
|
214
|
+
* Type-specific content rules
|
|
215
|
+
*/
|
|
216
|
+
export const blockContentValidationRule = {
|
|
217
|
+
name: 'BLOCK_CONTENT',
|
|
218
|
+
description: 'Block content is valid per block type',
|
|
219
|
+
validate: (context) => {
|
|
220
|
+
const { email } = context;
|
|
221
|
+
const errors = [];
|
|
222
|
+
email.body.blocks.forEach((block) => {
|
|
223
|
+
// Title blocks
|
|
224
|
+
if (block.type === 'title') {
|
|
225
|
+
if (!block.content || block.content.trim().length === 0) {
|
|
226
|
+
errors.push({
|
|
227
|
+
code: 'EMPTY_TITLE',
|
|
228
|
+
message: 'Title block cannot be empty',
|
|
229
|
+
blockId: block.id,
|
|
230
|
+
blockType: 'title',
|
|
231
|
+
severity: 'error',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Paragraph blocks
|
|
236
|
+
if (block.type === 'paragraph') {
|
|
237
|
+
if (!block.content || block.content.trim().length === 0) {
|
|
238
|
+
errors.push({
|
|
239
|
+
code: 'EMPTY_PARAGRAPH',
|
|
240
|
+
message: 'Paragraph block cannot be empty',
|
|
241
|
+
blockId: block.id,
|
|
242
|
+
blockType: 'paragraph',
|
|
243
|
+
severity: 'error',
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Image blocks
|
|
248
|
+
if (block.type === 'image') {
|
|
249
|
+
if (!block.src || !block.src.trim()) {
|
|
250
|
+
errors.push({
|
|
251
|
+
code: 'MISSING_IMAGE_SRC',
|
|
252
|
+
message: 'Image block must have a source URL',
|
|
253
|
+
blockId: block.id,
|
|
254
|
+
blockType: 'image',
|
|
255
|
+
severity: 'error',
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
else if (!block.src.startsWith('https://')) {
|
|
259
|
+
errors.push({
|
|
260
|
+
code: 'INVALID_IMAGE_PROTOCOL',
|
|
261
|
+
message: 'Image URL must use HTTPS protocol',
|
|
262
|
+
blockId: block.id,
|
|
263
|
+
blockType: 'image',
|
|
264
|
+
severity: 'error',
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (!block.alt || block.alt.trim().length === 0) {
|
|
268
|
+
errors.push({
|
|
269
|
+
code: 'MISSING_IMAGE_ALT',
|
|
270
|
+
message: 'Image block must have alt text for accessibility',
|
|
271
|
+
blockId: block.id,
|
|
272
|
+
blockType: 'image',
|
|
273
|
+
severity: 'error',
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Button blocks
|
|
278
|
+
if (block.type === 'button') {
|
|
279
|
+
if (!block.label || block.label.trim().length === 0) {
|
|
280
|
+
errors.push({
|
|
281
|
+
code: 'EMPTY_BUTTON_LABEL',
|
|
282
|
+
message: 'Button block must have a label',
|
|
283
|
+
blockId: block.id,
|
|
284
|
+
blockType: 'button',
|
|
285
|
+
severity: 'error',
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (!block.href || !block.href.trim()) {
|
|
289
|
+
errors.push({
|
|
290
|
+
code: 'MISSING_BUTTON_HREF',
|
|
291
|
+
message: 'Button block must have a URL',
|
|
292
|
+
blockId: block.id,
|
|
293
|
+
blockType: 'button',
|
|
294
|
+
severity: 'error',
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
else if (!block.href.startsWith('https://') && !block.href.startsWith('http://')) {
|
|
298
|
+
errors.push({
|
|
299
|
+
code: 'INVALID_BUTTON_PROTOCOL',
|
|
300
|
+
message: 'Button URL must use HTTP or HTTPS protocol',
|
|
301
|
+
blockId: block.id,
|
|
302
|
+
blockType: 'button',
|
|
303
|
+
severity: 'error',
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Highlight box blocks
|
|
308
|
+
if (block.type === 'highlight-box') {
|
|
309
|
+
if (!block.content || block.content.trim().length === 0) {
|
|
310
|
+
errors.push({
|
|
311
|
+
code: 'EMPTY_HIGHLIGHT_BOX',
|
|
312
|
+
message: 'Highlight box cannot be empty',
|
|
313
|
+
blockId: block.id,
|
|
314
|
+
blockType: 'highlight-box',
|
|
315
|
+
severity: 'error',
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (!block.backgroundColor) {
|
|
319
|
+
errors.push({
|
|
320
|
+
code: 'MISSING_HIGHLIGHT_COLOR',
|
|
321
|
+
message: 'Highlight box must have a background color',
|
|
322
|
+
blockId: block.id,
|
|
323
|
+
blockType: 'highlight-box',
|
|
324
|
+
severity: 'warning',
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
return errors;
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
/**
|
|
333
|
+
* RULE 9: Color format validation
|
|
334
|
+
* Ensures all color values are valid hex colors
|
|
335
|
+
*/
|
|
336
|
+
export const colorFormatRule = {
|
|
337
|
+
name: 'COLOR_FORMAT',
|
|
338
|
+
description: 'All color values are valid hex colors',
|
|
339
|
+
validate: (context) => {
|
|
340
|
+
const { email } = context;
|
|
341
|
+
const errors = [];
|
|
342
|
+
const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
|
|
343
|
+
email.body.blocks.forEach((block) => {
|
|
344
|
+
if ('color' in block && block.color && !hexColorRegex.test(block.color)) {
|
|
345
|
+
errors.push({
|
|
346
|
+
code: 'INVALID_COLOR_FORMAT',
|
|
347
|
+
message: `Invalid color format: ${block.color}. Use hex format like #FF5733`,
|
|
348
|
+
blockId: block.id,
|
|
349
|
+
blockType: block.type,
|
|
350
|
+
severity: 'error',
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if ('backgroundColor' in block &&
|
|
354
|
+
block.backgroundColor &&
|
|
355
|
+
!hexColorRegex.test(block.backgroundColor)) {
|
|
356
|
+
errors.push({
|
|
357
|
+
code: 'INVALID_COLOR_FORMAT',
|
|
358
|
+
message: `Invalid background color format: ${block.backgroundColor}. Use hex format like #FF5733`,
|
|
359
|
+
blockId: block.id,
|
|
360
|
+
blockType: block.type,
|
|
361
|
+
severity: 'error',
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if ('borderColor' in block && block.borderColor && !hexColorRegex.test(block.borderColor)) {
|
|
365
|
+
errors.push({
|
|
366
|
+
code: 'INVALID_COLOR_FORMAT',
|
|
367
|
+
message: `Invalid border color format: ${block.borderColor}. Use hex format like #FF5733`,
|
|
368
|
+
blockId: block.id,
|
|
369
|
+
blockType: block.type,
|
|
370
|
+
severity: 'error',
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
return errors;
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
// ============================================================================
|
|
378
|
+
// VALIDATION ENGINE
|
|
379
|
+
// ============================================================================
|
|
380
|
+
/**
|
|
381
|
+
* All validation rules in execution order
|
|
382
|
+
*/
|
|
383
|
+
const VALIDATION_RULES = [
|
|
384
|
+
blockTypeAllowedRule,
|
|
385
|
+
blockCountConstraintRule,
|
|
386
|
+
totalBlockCountRule,
|
|
387
|
+
mandatoryBlocksRule,
|
|
388
|
+
blockIdUniquenessRule,
|
|
389
|
+
blockContentValidationRule,
|
|
390
|
+
colorFormatRule,
|
|
391
|
+
blockOrderRule, // Order check is last since other errors might matter more
|
|
392
|
+
fixedSectionsRule,
|
|
393
|
+
];
|
|
394
|
+
/**
|
|
395
|
+
* MAIN VALIDATION FUNCTION
|
|
396
|
+
*
|
|
397
|
+
* @param email - The email document to validate
|
|
398
|
+
* @param strict - If true, warnings become errors
|
|
399
|
+
* @returns ValidationError array
|
|
400
|
+
*/
|
|
401
|
+
export function validateEmailDocument(email, strict = false) {
|
|
402
|
+
const templateConfig = getTemplateConfig(email.templateType);
|
|
403
|
+
const context = {
|
|
404
|
+
templateConfig,
|
|
405
|
+
email,
|
|
406
|
+
strict,
|
|
407
|
+
};
|
|
408
|
+
// Run all validation rules
|
|
409
|
+
const allErrors = VALIDATION_RULES.flatMap((rule) => rule.validate(context));
|
|
410
|
+
// Filter if not strict mode
|
|
411
|
+
if (!strict) {
|
|
412
|
+
return allErrors.filter((err) => err.severity === 'error');
|
|
413
|
+
}
|
|
414
|
+
return allErrors;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Check if email document is valid
|
|
418
|
+
* @returns true if no errors, false otherwise
|
|
419
|
+
*/
|
|
420
|
+
export function isEmailDocumentValid(email, strict = false) {
|
|
421
|
+
const errors = validateEmailDocument(email, strict);
|
|
422
|
+
return errors.length === 0;
|
|
423
|
+
}
|
|
424
|
+
export function getValidationSummary(email, strict = false) {
|
|
425
|
+
const allErrors = validateEmailDocument(email, true); // Always get both errors and warnings
|
|
426
|
+
const errors = allErrors.filter((e) => e.severity === 'error');
|
|
427
|
+
const warnings = allErrors.filter((e) => e.severity === 'warning');
|
|
428
|
+
return {
|
|
429
|
+
isValid: errors.length === 0,
|
|
430
|
+
errorCount: errors.length,
|
|
431
|
+
warningCount: strict ? warnings.length : 0,
|
|
432
|
+
errors,
|
|
433
|
+
warnings,
|
|
434
|
+
};
|
|
435
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "email-editor-core",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc"
|
|
15
|
+
},
|
|
16
|
+
"files": ["dist"]
|
|
17
|
+
}
|