@capillarytech/creatives-library 8.0.286 → 8.0.287-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/constants/unified.js +0 -1
- package/initialState.js +0 -2
- package/package.json +1 -1
- package/utils/common.js +5 -8
- package/utils/commonUtils.js +2 -83
- package/utils/tagValidations.js +84 -222
- package/utils/tests/commonUtil.test.js +147 -118
- package/utils/tests/tagValidations.test.js +280 -358
- package/v2Components/ErrorInfoNote/index.js +2 -5
- package/v2Components/FormBuilder/index.js +68 -160
- package/v2Components/FormBuilder/messages.js +0 -8
- package/v2Components/HtmlEditor/HTMLEditor.js +0 -5
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +0 -1
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +0 -15
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +1 -2
- package/v2Containers/Cap/mockData.js +0 -14
- package/v2Containers/Cap/reducer.js +3 -55
- package/v2Containers/Cap/tests/reducer.test.js +0 -102
- package/v2Containers/CreativesContainer/index.js +4 -4
- package/v2Containers/Email/index.js +1 -5
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +10 -62
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +12 -115
- package/v2Containers/FTP/index.js +2 -51
- package/v2Containers/FTP/messages.js +0 -4
- package/v2Containers/InApp/index.js +1 -96
- package/v2Containers/InApp/tests/index.test.js +17 -6
- package/v2Containers/InappAdvance/index.js +2 -103
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -24
- package/v2Containers/Line/Container/Text/index.js +0 -1
- package/v2Containers/MobilePushNew/index.js +2 -33
- package/v2Containers/Rcs/index.js +12 -37
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +4 -18
- package/v2Containers/Sms/Create/index.js +30 -2
- package/v2Containers/Sms/Create/messages.js +4 -0
- package/v2Containers/Sms/Edit/index.js +28 -2
- package/v2Containers/Sms/commonMethods.js +7 -2
- package/v2Containers/SmsTrai/Create/index.scss +1 -1
- package/v2Containers/SmsTrai/Edit/index.js +6 -47
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
- package/v2Containers/Viber/index.js +0 -1
- package/v2Containers/Viber/index.scss +1 -1
- package/v2Containers/WebPush/Create/hooks/useTagManagement.js +1 -3
- package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +0 -7
- package/v2Containers/WebPush/Create/index.js +2 -2
- package/v2Containers/WebPush/Create/utils/validation.js +18 -9
- package/v2Containers/WebPush/Create/utils/validation.test.js +0 -24
- package/v2Containers/Whatsapp/index.js +9 -17
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +248 -624
- package/v2Containers/Zalo/index.js +3 -11
package/constants/unified.js
CHANGED
|
@@ -43,7 +43,6 @@ export const HOSPITALITY_BASED_SCOPE = 'HOSPITALITY_BASED_SCOPE';
|
|
|
43
43
|
export const REGISTRATION_CUSTOM_FIELD = 'Registration custom fields';
|
|
44
44
|
export const GIFT_CARDS = 'GIFT_CARDS';
|
|
45
45
|
export const PROMO_ENGINE = 'PROMO_ENGINE';
|
|
46
|
-
export const LIQUID_SUPPORT = 'ENABLE_LIQUID_SUPPORT';
|
|
47
46
|
export const ENABLE_NEW_MPUSH = 'ENABLE_NEW_MPUSH';
|
|
48
47
|
export const SUPPORT_CK_EDITOR = 'SUPPORT_CK_EDITOR';
|
|
49
48
|
export const CUSTOM_TAG = 'CustomTagMessage';
|
package/initialState.js
CHANGED
package/package.json
CHANGED
package/utils/common.js
CHANGED
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
BADGES_ISSUE,
|
|
23
23
|
ENABLE_WECHAT,
|
|
24
24
|
ENABLE_WEBPUSH,
|
|
25
|
-
LIQUID_SUPPORT,
|
|
26
25
|
SUPPORT_CK_EDITOR,
|
|
27
26
|
ENABLE_NEW_MPUSH
|
|
28
27
|
} from '../constants/unified';
|
|
@@ -91,12 +90,6 @@ export const hasPromoFeature = Auth.hasFeatureAccess.bind(
|
|
|
91
90
|
null,
|
|
92
91
|
PROMO_ENGINE,
|
|
93
92
|
);
|
|
94
|
-
|
|
95
|
-
export const hasLiquidSupportFeature = Auth.hasFeatureAccess.bind(
|
|
96
|
-
null,
|
|
97
|
-
LIQUID_SUPPORT,
|
|
98
|
-
);
|
|
99
|
-
|
|
100
93
|
export const hasSupportCKEditor = Auth.hasFeatureAccess.bind(
|
|
101
94
|
null,
|
|
102
95
|
SUPPORT_CK_EDITOR,
|
|
@@ -132,7 +125,11 @@ export const hasCustomerBarcodeFeatureEnabled = Auth.hasFeatureAccess.bind(
|
|
|
132
125
|
ENABLE_CUSTOMER_BARCODE_TAG,
|
|
133
126
|
);
|
|
134
127
|
|
|
135
|
-
|
|
128
|
+
// Note: The "EMAIL_UNSUBSCRIBE_TAG_MANDATORY" feature flag determines if the Unsubscribe tag in email is optional.
|
|
129
|
+
// When this flag is enabled for an org, the Unsubscribe tag is NOT mandatory in the email flow.
|
|
130
|
+
// This is as per the requirement in the tech doc:
|
|
131
|
+
// https://capillarytech.atlassian.net/wiki/spaces/CAM/pages/3941662838/Remove+mandate+for+Unsubscribe+tag+in+email+flow
|
|
132
|
+
export const isEmailUnsubscribeTagOptional = Auth.hasFeatureAccess.bind(
|
|
136
133
|
null,
|
|
137
134
|
EMAIL_UNSUBSCRIBE_TAG_MANDATORY,
|
|
138
135
|
);
|
package/utils/commonUtils.js
CHANGED
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
IOS,
|
|
17
17
|
} from "../v2Containers/CreativesContainer/constants";
|
|
18
18
|
import { GLOBAL_CONVERT_OPTIONS } from "../v2Components/FormBuilder/constants";
|
|
19
|
-
import { checkSupport, extractNames, skipTags as defaultSkipTags, isInsideLiquidBlock } from "./tagValidations";
|
|
20
19
|
import { SMS_TRAI_VAR } from '../v2Containers/SmsTrai/Edit/constants';
|
|
21
20
|
export const apiMessageFormatHandler = (id, fallback) => (
|
|
22
21
|
<FormattedMessage id={id} defaultMessage={fallback} />
|
|
@@ -141,12 +140,7 @@ export const validateLiquidTemplateContent = async (
|
|
|
141
140
|
messages,
|
|
142
141
|
onError = () => {},
|
|
143
142
|
onSuccess = () => {},
|
|
144
|
-
tagLookupMap,
|
|
145
|
-
eventContextTags,
|
|
146
|
-
isLiquidFlow,
|
|
147
|
-
forwardedTags = {},
|
|
148
143
|
tabType,
|
|
149
|
-
skipTags = defaultSkipTags,
|
|
150
144
|
} = options;
|
|
151
145
|
const emptyBodyError = formatMessage(messages?.emailBodyEmptyError);
|
|
152
146
|
const somethingWrongMsg = formatMessage(messages?.somethingWentWrong);
|
|
@@ -204,81 +198,6 @@ export const validateLiquidTemplateContent = async (
|
|
|
204
198
|
});
|
|
205
199
|
return false;
|
|
206
200
|
}
|
|
207
|
-
// Extract and validate tags
|
|
208
|
-
const extractedLiquidTags = extractNames(result?.data || []);
|
|
209
|
-
// Get supported tags
|
|
210
|
-
const supportedLiquidTags = checkSupport(
|
|
211
|
-
result,
|
|
212
|
-
tagLookupMap,
|
|
213
|
-
eventContextTags,
|
|
214
|
-
isLiquidFlow,
|
|
215
|
-
forwardedTags
|
|
216
|
-
);
|
|
217
|
-
// Helper function to check if a tag appears only inside {% %} blocks
|
|
218
|
-
const isTagOnlyInsideLiquidBlocks = (tagName) => {
|
|
219
|
-
// Escape special regex characters in tag name, including dots
|
|
220
|
-
// Dots need to be escaped to match literally (item.name should match item.name, not item or name)
|
|
221
|
-
const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
222
|
-
|
|
223
|
-
// First, check if tag appears in {% %} syntax itself (like "order.items" in "{% for item in order.items %}")
|
|
224
|
-
// This is a tag used in Liquid logic, not output, so it should always be skipped
|
|
225
|
-
// Match the tag name as a whole (with dots escaped), optionally surrounded by word boundaries or non-word chars
|
|
226
|
-
// For tags with dots, we match them directly; for simple tags, we use word boundaries
|
|
227
|
-
const hasDot = tagName.includes('.');
|
|
228
|
-
const liquidSyntaxPattern = hasDot
|
|
229
|
-
? `{%[^%]*${escapedTagName}[^%]*%}`
|
|
230
|
-
: `{%[^%]*\\b${escapedTagName}\\b[^%]*%}`;
|
|
231
|
-
const liquidSyntaxRegex = new RegExp(liquidSyntaxPattern, 'g');
|
|
232
|
-
const liquidSyntaxMatches = Array.from(content.matchAll(liquidSyntaxRegex));
|
|
233
|
-
|
|
234
|
-
// Find all occurrences of {{tagName}} in the content (output tags)
|
|
235
|
-
// Match patterns like: {{tagName}}, {{ tagName }}, {{tagName }}, {{ tagName}}
|
|
236
|
-
// Use non-word-boundary approach for tags with dots (item.name should match item.name, not item or name separately)
|
|
237
|
-
const outputTagRegex = new RegExp(`\\{\\{\\s*${escapedTagName}\\s*\\}\\}`, 'g');
|
|
238
|
-
const outputTagMatches = Array.from(content.matchAll(outputTagRegex));
|
|
239
|
-
const outputTagPositions = outputTagMatches.map((match) => match.index);
|
|
240
|
-
|
|
241
|
-
// If tag appears in {% %} syntax, skip validation
|
|
242
|
-
if (liquidSyntaxMatches.length > 0) {
|
|
243
|
-
return true;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// If no output tag matches found, don't skip validation
|
|
247
|
-
// The tag was extracted by the API, so it exists somewhere and should be validated
|
|
248
|
-
if (outputTagPositions.length === 0) {
|
|
249
|
-
return false;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Check if all output tag occurrences are inside {% %} blocks
|
|
253
|
-
// We check the position of {{ to see if it's inside a block
|
|
254
|
-
// Only skip validation if ALL occurrences are inside blocks
|
|
255
|
-
return outputTagPositions.every((tagIndex) => {
|
|
256
|
-
if (tagIndex === undefined || tagIndex === null) {
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
return isInsideLiquidBlock(content, tagIndex);
|
|
260
|
-
});
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
// Find unsupported tags, excluding those that are only inside {% %} blocks
|
|
264
|
-
const unsupportedLiquidTags = extractedLiquidTags?.filter(
|
|
265
|
-
(tag) => !supportedLiquidTags?.includes(tag)
|
|
266
|
-
&& !skipTags(tag)
|
|
267
|
-
&& !isTagOnlyInsideLiquidBlocks(tag)
|
|
268
|
-
);
|
|
269
|
-
// Handle unsupported tags
|
|
270
|
-
if (unsupportedLiquidTags?.length > 0) {
|
|
271
|
-
const errorMsg = formatMessage(messages.unsupportedTagsValidationError, {
|
|
272
|
-
unsupportedTags: unsupportedLiquidTags.join(", "),
|
|
273
|
-
});
|
|
274
|
-
onError({
|
|
275
|
-
standardErrors: [],
|
|
276
|
-
liquidErrors: [errorMsg],
|
|
277
|
-
tabType,
|
|
278
|
-
});
|
|
279
|
-
return false;
|
|
280
|
-
}
|
|
281
|
-
// All validations passed
|
|
282
201
|
onSuccess(content, tabType);
|
|
283
202
|
return true;
|
|
284
203
|
};
|
|
@@ -365,8 +284,8 @@ export const _validatePlatformSpecificContent = async (
|
|
|
365
284
|
/**
|
|
366
285
|
* Validate Mobile Push content for both Android and iOS tabs
|
|
367
286
|
* @param {object} formData - Form data containing Android and iOS content
|
|
368
|
-
* @param {object} options - Options for validation
|
|
369
|
-
* @returns {Promise} - Promise that resolves when validation
|
|
287
|
+
* @param {object} options - Options for validation (currentTab, onError, onSuccess, getLiquidTags, formatMessage, messages, singleTab).
|
|
288
|
+
* @returns {Promise<boolean>} - Promise that resolves to true when validation succeeds, false otherwise
|
|
370
289
|
*/
|
|
371
290
|
export const validateMobilePushContent = async (formData, options) => {
|
|
372
291
|
const {
|
package/utils/tagValidations.js
CHANGED
|
@@ -13,259 +13,121 @@ import { ENTRY_TRIGGER_TAG_REGEX, SKIP_TAGS_REGEX_GROUPS } from '../constants/un
|
|
|
13
13
|
const DEFAULT = 'default';
|
|
14
14
|
const SUBTAGS = 'subtags';
|
|
15
15
|
|
|
16
|
+
/** Whitespace-tolerant check for {{ unsubscribe }}-style tag in content. */
|
|
17
|
+
const UNSUBSCRIBE_TAG_REGEX = /\{\{\s*unsubscribe\s*\}\}/;
|
|
18
|
+
export const hasUnsubscribeTag = (content) =>
|
|
19
|
+
typeof content === 'string' && UNSUBSCRIBE_TAG_REGEX.test(content);
|
|
20
|
+
|
|
16
21
|
/**
|
|
17
|
-
*
|
|
18
|
-
* @param {Object}
|
|
19
|
-
* @param {
|
|
22
|
+
* Shared core for tag validation. Used by validateTags (tagValidations) and FormBuilder.validateTags.
|
|
23
|
+
* @param {Object} params
|
|
24
|
+
* @param {string} params.contentForBraceCheck - String to run validateIfTagClosed on.
|
|
25
|
+
* @param {string} params.contentForUnsubscribeScan - String to scan for {{...}} unsubscribe variants.
|
|
26
|
+
* @param {Array} [params.tags] - Tag definitions (for definition-based missing tags).
|
|
27
|
+
* @param {string} [params.currentModule] - Module context (e.g. 'default', 'outbound').
|
|
28
|
+
* @param {boolean} [params.isFullMode] - If true, skip unsubscribe checks.
|
|
29
|
+
* @param {Array} [params.initialMissingTags=null] - If set, use instead of computing from definitions.
|
|
30
|
+
* @param {function} [params.skipTagsFn=skipTags] - skipTags implementation.
|
|
31
|
+
* @param {boolean} [params.includeIsContentEmpty=false] - If true, response includes isContentEmpty: false.
|
|
32
|
+
* @returns {{ valid: boolean, missingTags: string[], isBraceError: boolean }}
|
|
20
33
|
*/
|
|
21
|
-
export const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
if (!tagObject?.[parentTag] && !mappedForwardedTags.includes(parentTag)) return false;
|
|
39
|
-
let updatedChildName = childName;
|
|
40
|
-
let updatedWithoutDotChildName = childName;
|
|
41
|
-
if (childName?.includes(".")) {
|
|
42
|
-
updatedChildName = `.${childName?.split(".")?.[1]}`;
|
|
43
|
-
updatedWithoutDotChildName = childName?.split(".")?.[1];
|
|
44
|
-
}
|
|
45
|
-
if (tagObject?.[parentTag]) {
|
|
46
|
-
const subTags = tagObject?.[parentTag]?.definition?.subtags;
|
|
47
|
-
return subTags?.includes(updatedChildName);
|
|
48
|
-
}
|
|
49
|
-
return mappedForwardedTags.includes(updatedChildName) || mappedForwardedTags.includes(updatedWithoutDotChildName) || mappedForwardedTags.includes(childName);
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
//Recursively checks if the childTag is actually a Sub-tag of the ParentTag
|
|
53
|
-
const processChildren = (parentTag = "", children = []) => {
|
|
54
|
-
for (const child of children) {
|
|
55
|
-
if (checkSubtags(parentTag, child?.name)) {
|
|
56
|
-
supportedList.push(child?.name);
|
|
57
|
-
}
|
|
58
|
-
if (child?.children?.length) {
|
|
59
|
-
processChildren(child?.name, child?.children);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
34
|
+
export const validateTagsCore = ({
|
|
35
|
+
contentForBraceCheck,
|
|
36
|
+
contentForUnsubscribeScan,
|
|
37
|
+
tags,
|
|
38
|
+
currentModule,
|
|
39
|
+
isFullMode,
|
|
40
|
+
initialMissingTags = null,
|
|
41
|
+
skipTagsFn = skipTags,
|
|
42
|
+
includeIsContentEmpty = false,
|
|
43
|
+
}) => {
|
|
44
|
+
const response = {
|
|
45
|
+
valid: true,
|
|
46
|
+
missingTags: [],
|
|
47
|
+
isBraceError: false,
|
|
48
|
+
...(includeIsContentEmpty && { isContentEmpty: false }),
|
|
62
49
|
};
|
|
63
50
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
result.push(key); // Add the main key to the result array
|
|
83
|
-
|
|
84
|
-
// Check if there are subtags for the current key
|
|
85
|
-
if (forwardedTags[key].subtags) {
|
|
86
|
-
// If subtags exist, add all subtag keys to the result array
|
|
87
|
-
Object.keys(forwardedTags[key].subtags).forEach((subkey) => {
|
|
88
|
-
result.push(subkey);
|
|
51
|
+
if (tags && tags.length && !isFullMode) {
|
|
52
|
+
if (initialMissingTags == null) {
|
|
53
|
+
// Definition-based: same as original tagValidations (when caller does not pass initialMissingTags)
|
|
54
|
+
lodashForEach(tags, ({
|
|
55
|
+
definition: {
|
|
56
|
+
supportedModules,
|
|
57
|
+
value,
|
|
58
|
+
},
|
|
59
|
+
}) => {
|
|
60
|
+
if (value === 'unsubscribe') {
|
|
61
|
+
lodashForEach(supportedModules, (module) => {
|
|
62
|
+
if (module.mandatory && (currentModule === module.context)) {
|
|
63
|
+
if (!hasUnsubscribeTag(contentForUnsubscribeScan)) {
|
|
64
|
+
response.missingTags.push(value);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
89
69
|
});
|
|
70
|
+
} else {
|
|
71
|
+
response.missingTags = [...initialMissingTags];
|
|
90
72
|
}
|
|
91
|
-
});
|
|
92
|
-
return result;
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Extracts the names from the given data.
|
|
97
|
-
* @param {Array} data - The data to extract names from.
|
|
98
|
-
* @returns {Array} - The extracted names.
|
|
99
|
-
*/
|
|
100
|
-
export function extractNames(data) {
|
|
101
|
-
const names = [];
|
|
102
73
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
74
|
+
const regex = /{{([^}]+)}}/g;
|
|
75
|
+
let match = regex.exec(contentForUnsubscribeScan);
|
|
76
|
+
while (match !== null) {
|
|
77
|
+
const tagValue = match[1].trim();
|
|
78
|
+
const ifSkipped = skipTagsFn(tagValue);
|
|
79
|
+
if (ifSkipped && tagValue.indexOf('unsubscribe') !== -1) {
|
|
80
|
+
const missingTagIndex = response.missingTags.indexOf('unsubscribe');
|
|
81
|
+
if (missingTagIndex !== -1) {
|
|
82
|
+
response.missingTags.splice(missingTagIndex, 1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
match = regex.exec(contentForUnsubscribeScan);
|
|
106
86
|
}
|
|
107
87
|
|
|
108
|
-
if (
|
|
109
|
-
|
|
88
|
+
if (response.missingTags.length > 0) {
|
|
89
|
+
response.valid = false;
|
|
110
90
|
}
|
|
111
91
|
}
|
|
112
92
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Helper to check if a tag is inside a {% ... %} block
|
|
119
|
-
// Handles all Liquid block types: {% %}, {%- -%}, {%- %}, {% -%}
|
|
120
|
-
// Content inside {% %} blocks can contain any dynamic tags and should not be validated
|
|
121
|
-
export const isInsideLiquidBlock = (content, tagIndex) => {
|
|
122
|
-
if (!content || tagIndex < 0 || tagIndex >= content.length) {
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Check if tagIndex is at the start of a {% block
|
|
127
|
-
// Need to check a few characters ahead to catch {% patterns
|
|
128
|
-
const checkWindow = content.substring(Math.max(0, tagIndex - 1), Math.min(content.length, tagIndex + 3));
|
|
129
|
-
if (/{%-?/.test(checkWindow) && content.substring(tagIndex, tagIndex + 2) === '{%') {
|
|
130
|
-
return true;
|
|
93
|
+
response.isBraceError = !validateIfTagClosed(contentForBraceCheck);
|
|
94
|
+
if (response.isBraceError) {
|
|
95
|
+
response.valid = false;
|
|
96
|
+
} else if (response.missingTags.length > 0) {
|
|
97
|
+
response.valid = false;
|
|
131
98
|
}
|
|
132
|
-
|
|
133
|
-
// Check content up to tagIndex to see if we're inside any {% %} block
|
|
134
|
-
// We need to properly handle Liquid tags:
|
|
135
|
-
// - Block opening tags: {% for %}, {% if %}, {% unless %}, {% case %}, {% capture %}, etc.
|
|
136
|
-
// These OPEN a block that needs to be closed with an "end" tag
|
|
137
|
-
// - Block closing tags: {% endfor %}, {% endif %}, {% endunless %}, etc.
|
|
138
|
-
// These CLOSE a block opened by a corresponding opening tag
|
|
139
|
-
// - Self-contained tags: {% assign %}, {% comment %}, etc.
|
|
140
|
-
// These are blocks themselves (from {% to %})
|
|
141
|
-
const contentBeforeTag = content.substring(0, tagIndex);
|
|
142
|
-
|
|
143
|
-
// Regex to match Liquid block opening tags (for, if, unless, case, capture, tablerow, raw, comment)
|
|
144
|
-
// These tags OPEN a block that needs to be closed with an "end" tag
|
|
145
|
-
const blockOpeningTags = ['for', 'if', 'unless', 'case', 'capture', 'tablerow', 'raw', 'comment'];
|
|
146
|
-
const blockOpeningPattern = blockOpeningTags.map(tag => `\\b${tag}\\b`).join('|');
|
|
147
|
-
const blockOpeningRegex = new RegExp(`{%-?\\s*(${blockOpeningPattern})[^%]*%}`, 'gi');
|
|
148
|
-
|
|
149
|
-
// Regex to match Liquid block closing tags (endfor, endif, endunless, endcase, endcapture, etc.)
|
|
150
|
-
const blockClosingTags = ['endfor', 'endif', 'endunless', 'endcase', 'endcapture', 'endtablerow', 'endraw', 'endcomment'];
|
|
151
|
-
const blockClosingPattern = blockClosingTags.map(tag => `\\b${tag}\\b`).join('|');
|
|
152
|
-
const blockClosingRegex = new RegExp(`{%-?\\s*(${blockClosingPattern})[^%]*%}`, 'gi');
|
|
153
|
-
|
|
154
|
-
// Find all block opening tags before tagIndex
|
|
155
|
-
const blockOpenings = Array.from(contentBeforeTag.matchAll(blockOpeningRegex), (match) => match.index);
|
|
156
|
-
|
|
157
|
-
// Find all block closing tags before tagIndex
|
|
158
|
-
const blockClosings = Array.from(contentBeforeTag.matchAll(blockClosingRegex), (match) => match.index);
|
|
159
|
-
|
|
160
|
-
// Count unmatched opening blocks (for, if, etc.)
|
|
161
|
-
const openBlockCount = blockOpenings.length - blockClosings.length;
|
|
162
|
-
|
|
163
|
-
// Also check for self-contained tags (assign, etc.) that create blocks from {% to %}
|
|
164
|
-
// These are any {% %} tags that are not block-opening or block-closing tags
|
|
165
|
-
const allBlockStarts = /{%-?/g;
|
|
166
|
-
const allBlockEnds = /-?%}/g;
|
|
167
|
-
const allStarts = Array.from(contentBeforeTag.matchAll(allBlockStarts), (match) => match.index);
|
|
168
|
-
const allEnds = Array.from(contentBeforeTag.matchAll(allBlockEnds), (match) => match.index);
|
|
169
|
-
|
|
170
|
-
// Check if we're inside a self-contained tag (more {% than %} before tagIndex, excluding block tags)
|
|
171
|
-
const selfContainedCount = allStarts.length - allEnds.length;
|
|
172
|
-
|
|
173
|
-
// We're inside a block if:
|
|
174
|
-
// 1. We're inside an unclosed block-opening tag (for, if, etc.), OR
|
|
175
|
-
// 2. We're inside a self-contained tag (assign, etc.)
|
|
176
|
-
return openBlockCount > 0 || selfContainedCount > 0;
|
|
99
|
+
return response;
|
|
177
100
|
};
|
|
178
101
|
|
|
179
102
|
/**
|
|
180
103
|
* Validates the tags based on the provided parameters.
|
|
181
104
|
* @param {Object} params - The parameters for tag validation.
|
|
105
|
+
* @param {string} params.content - Content to validate.
|
|
106
|
+
* @param {Array} [params.tagsParam] - Tag definitions.
|
|
107
|
+
* @param {Object} [params.location] - Location with query.module.
|
|
108
|
+
* @param {string} [params.tagModule] - Override for current module context.
|
|
109
|
+
* @param {boolean} [params.isFullMode] - If true, skip unsubscribe checks.
|
|
110
|
+
* @returns {{ valid: boolean, missingTags: string[], isBraceError: boolean }}
|
|
182
111
|
*/
|
|
183
112
|
export const validateTags = ({
|
|
184
113
|
content,
|
|
185
114
|
tagsParam,
|
|
186
|
-
injectedTagsParams,
|
|
187
115
|
location,
|
|
188
116
|
tagModule,
|
|
189
|
-
eventContextTags,
|
|
190
117
|
isFullMode,
|
|
191
118
|
}) => {
|
|
192
119
|
const tags = tagsParam;
|
|
193
|
-
const injectedTags = transformInjectedTags(injectedTagsParams);
|
|
194
120
|
let currentModule = location?.query?.module ? location?.query?.module : DEFAULT;
|
|
195
121
|
if (tagModule) {
|
|
196
122
|
currentModule = tagModule;
|
|
197
123
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (tags && tags.length && !isFullMode) {
|
|
206
|
-
lodashForEach(tags, ({
|
|
207
|
-
definition: {
|
|
208
|
-
supportedModules,
|
|
209
|
-
value,
|
|
210
|
-
},
|
|
211
|
-
}) => {
|
|
212
|
-
lodashForEach(supportedModules, (module) => {
|
|
213
|
-
if (module.mandatory && (currentModule === module.context)) {
|
|
214
|
-
if (content.indexOf(`{{${value}}}`) === -1) {
|
|
215
|
-
response.valid = false;
|
|
216
|
-
response.missingTags.push(value);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
// In library mode, always scan content for {{...}} and flag unsupported tags (even when tags list is empty)
|
|
223
|
-
if (!isFullMode && content) {
|
|
224
|
-
const regex = /{{[(A-Z\w+(\s\w+)*$\(\)@!#$%^&*~.,/\\]+}}/g;
|
|
225
|
-
let match = regex.exec(content);
|
|
226
|
-
while (match !== null) {
|
|
227
|
-
const tagValue = match[0].substring(indexOfEnd(match[0], '{{'), match[0].indexOf('}}'));
|
|
228
|
-
const tagIndex = match?.index;
|
|
229
|
-
match = regex.exec(content);
|
|
230
|
-
let ifSupported = false;
|
|
231
|
-
lodashForEach(tags || [], (tag) => {
|
|
232
|
-
if (tag?.definition?.value === tagValue) {
|
|
233
|
-
ifSupported = true;
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
const ifSkipped = skipTags(tagValue);
|
|
237
|
-
if (ifSkipped) {
|
|
238
|
-
ifSupported = true;
|
|
239
|
-
const isUnsubscribeSkipped = tagValue.indexOf("unsubscribe") !== -1;
|
|
240
|
-
if (isUnsubscribeSkipped) {
|
|
241
|
-
const missingTagIndex = response.missingTags.indexOf("unsubscribe");
|
|
242
|
-
if (missingTagIndex !== -1) {
|
|
243
|
-
response.missingTags.splice(missingTagIndex, 1);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
// Journey Event Context Tags support
|
|
248
|
-
eventContextTags?.forEach((tag) => {
|
|
249
|
-
if (tagValue === tag?.tagName) {
|
|
250
|
-
ifSupported = true;
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
ifSupported = ifSupported || checkIfSupportedTag(tagValue, injectedTags);
|
|
254
|
-
// Only add to unsupportedTags if not inside a {% ... %} block and does not contain a dot
|
|
255
|
-
if (!ifSupported && !isInsideLiquidBlock(content, tagIndex) && tagValue?.indexOf('.') === -1) {
|
|
256
|
-
response.unsupportedTags.push(tagValue);
|
|
257
|
-
response.valid = false;
|
|
258
|
-
}
|
|
259
|
-
if (response.unsupportedTags.length === 0 && response.missingTags.length === 0) {
|
|
260
|
-
response.valid = true;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
response.isBraceError = !validateIfTagClosed(content);
|
|
265
|
-
// response should not be valid if there is unbalanced bracket error, as
|
|
266
|
-
// validations (eg button) are handled on valid property coming from the response.
|
|
267
|
-
response.isBraceError ? response.valid = false : response.valid = true;
|
|
268
|
-
return response;
|
|
124
|
+
return validateTagsCore({
|
|
125
|
+
contentForBraceCheck: content,
|
|
126
|
+
contentForUnsubscribeScan: content,
|
|
127
|
+
tags,
|
|
128
|
+
currentModule,
|
|
129
|
+
isFullMode,
|
|
130
|
+
});
|
|
269
131
|
};
|
|
270
132
|
|
|
271
133
|
/**
|