@@ -30,6 +30,58 @@ import { Tour } from 'antd'
3030import { ImportExportButton } from './ImportExportButton'
3131import { useAuth } from '../../contexts/AuthContext'
3232
33+ /**
34+ * Validates liquid template tags in a string to ensure they are properly closed
35+ * @param text - The text to validate
36+ * @returns Object with isValid boolean and error message if invalid
37+ */
38+ const validateLiquidTags = ( text : string ) : { isValid : boolean ; error ?: string } => {
39+ if ( ! text ) return { isValid : true }
40+
41+ // Find all opening double curly braces
42+ const openingTags = text . match ( / \{ \{ / g) || [ ]
43+ // Find all closing double curly braces
44+ const closingTags = text . match ( / \} \} / g) || [ ]
45+
46+ if ( openingTags . length !== closingTags . length ) {
47+ return {
48+ isValid : false ,
49+ error : `Unclosed liquid tag detected. Found ${ openingTags . length } opening tags ({{) but ${ closingTags . length } closing tags (}})`
50+ }
51+ }
52+
53+ // Check for proper nesting by tracking position
54+ let openCount = 0
55+ let i = 0
56+
57+ while ( i < text . length - 1 ) {
58+ if ( text . slice ( i , i + 2 ) === '{{' ) {
59+ openCount ++
60+ i += 2
61+ } else if ( text . slice ( i , i + 2 ) === '}}' ) {
62+ if ( openCount === 0 ) {
63+ return {
64+ isValid : false ,
65+ error : 'Found closing liquid tag (}}) without matching opening tag ({{)'
66+ }
67+ }
68+ openCount --
69+ i += 2
70+ } else {
71+ i ++
72+ }
73+ }
74+
75+ if ( openCount > 0 ) {
76+ return {
77+ isValid : false ,
78+ error : `${ openCount } liquid tag(s) not properly closed. Make sure each {{ has a matching }}`
79+ }
80+ }
81+
82+ return { isValid : true }
83+ }
84+
3385interface CreateTemplateDrawerProps {
3486 workspace : Workspace
3587 template ?: Template
@@ -572,14 +624,36 @@ export function CreateTemplateDrawer({
572624 < Form . Item
573625 name = { [ 'email' , 'subject' ] }
574626 label = "Email subject"
575- rules = { [ { required : true , type : 'string' } ] }
627+ rules = { [
628+ { required : true , type : 'string' } ,
629+ {
630+ validator : ( _ , value ) => {
631+ const validation = validateLiquidTags ( value )
632+ if ( ! validation . isValid ) {
633+ return Promise . reject ( new Error ( validation . error ) )
634+ }
635+ return Promise . resolve ( )
636+ }
637+ }
638+ ] }
576639 >
577640 < Input placeholder = "Templating markup allowed" />
578641 </ Form . Item >
579642 < Form . Item
580643 name = { [ 'email' , 'subject_preview' ] }
581644 label = "Subject preview"
582- rules = { [ { required : true , type : 'string' } ] }
645+ rules = { [
646+ { required : true , type : 'string' } ,
647+ {
648+ validator : ( _ , value ) => {
649+ const validation = validateLiquidTags ( value )
650+ if ( ! validation . isValid ) {
651+ return Promise . reject ( new Error ( validation . error ) )
652+ }
653+ return Promise . resolve ( )
654+ }
655+ }
656+ ] }
583657 >
584658 < Input placeholder = "Templating markup allowed" />
585659 </ Form . Item >
0 commit comments