@@ -16,11 +16,26 @@ func ConvertJSONToMJML(tree EmailBlock) string {
1616
1717// ConvertJSONToMJMLWithData converts an EmailBlock JSON tree to MJML string with template data
1818func ConvertJSONToMJMLWithData (tree EmailBlock , templateData string ) (string , error ) {
19- return convertBlockToMJMLWithError (tree , 0 , templateData )
19+ // Parse template data once at the beginning
20+ parsedData , parseErr := parseTemplateDataString (templateData )
21+ if parseErr != nil {
22+ return "" , fmt .Errorf ("template data parsing failed: %v" , parseErr )
23+ }
24+ return convertBlockToMJMLWithErrorAndParsedData (tree , 0 , templateData , parsedData )
2025}
2126
2227// convertBlockToMJMLWithError recursively converts a single EmailBlock to MJML string with error handling
2328func convertBlockToMJMLWithError (block EmailBlock , indentLevel int , templateData string ) (string , error ) {
29+ // Parse template data once at the beginning
30+ parsedData , parseErr := parseTemplateDataString (templateData )
31+ if parseErr != nil {
32+ return "" , fmt .Errorf ("template data parsing failed: %v" , parseErr )
33+ }
34+ return convertBlockToMJMLWithErrorAndParsedData (block , indentLevel , templateData , parsedData )
35+ }
36+
37+ // convertBlockToMJMLWithErrorAndParsedData recursively converts a single EmailBlock to MJML string with error handling and pre-parsed data
38+ func convertBlockToMJMLWithErrorAndParsedData (block EmailBlock , indentLevel int , templateData string , parsedData map [string ]interface {}) (string , error ) {
2439 indent := strings .Repeat (" " , indentLevel )
2540 tagName := string (block .GetType ())
2641 children := block .GetChildren ()
@@ -34,10 +49,6 @@ func convertBlockToMJMLWithError(block EmailBlock, indentLevel int, templateData
3449 // Process Liquid templating for mj-text, mj-button, mj-title, mj-preview, and mj-raw blocks
3550 blockType := block .GetType ()
3651 if blockType == MJMLComponentMjText || blockType == MJMLComponentMjButton || blockType == MJMLComponentMjTitle || blockType == MJMLComponentMjPreview || blockType == MJMLComponentMjRaw {
37- parsedData , parseErr := parseTemplateDataString (templateData )
38- if parseErr != nil {
39- return "" , fmt .Errorf ("template data parsing failed for block %s: %v" , block .GetID (), parseErr )
40- }
4152 processedContent , err := processLiquidContent (content , parsedData , block .GetID ())
4253 if err != nil {
4354 // Return error instead of just logging
@@ -48,15 +59,15 @@ func convertBlockToMJMLWithError(block EmailBlock, indentLevel int, templateData
4859 }
4960
5061 // Block with content - don't escape for mj-raw, mj-text, and mj-button (they can contain HTML)
51- attributeString := formatAttributes (block .GetAttributes ())
62+ attributeString := formatAttributesWithLiquid (block .GetAttributes (), parsedData , block . GetID ())
5263 if blockType == MJMLComponentMjRaw || blockType == MJMLComponentMjText || blockType == MJMLComponentMjButton {
5364 return fmt .Sprintf ("%s<%s%s>%s</%s>" , indent , tagName , attributeString , content , tagName ), nil
5465 } else {
5566 return fmt .Sprintf ("%s<%s%s>%s</%s>" , indent , tagName , attributeString , escapeContent (content ), tagName ), nil
5667 }
5768 } else {
5869 // Self-closing block or empty block
59- attributeString := formatAttributes (block .GetAttributes ())
70+ attributeString := formatAttributesWithLiquid (block .GetAttributes (), parsedData , block . GetID ())
6071 if attributeString != "" {
6172 return fmt .Sprintf ("%s<%s%s />" , indent , tagName , attributeString ), nil
6273 } else {
@@ -66,15 +77,15 @@ func convertBlockToMJMLWithError(block EmailBlock, indentLevel int, templateData
6677 }
6778
6879 // Block with children
69- attributeString := formatAttributes (block .GetAttributes ())
80+ attributeString := formatAttributesWithLiquid (block .GetAttributes (), parsedData , block . GetID ())
7081 openTag := fmt .Sprintf ("%s<%s%s>" , indent , tagName , attributeString )
7182 closeTag := fmt .Sprintf ("%s</%s>" , indent , tagName )
7283
7384 // Process children
7485 var childrenMJML []string
7586 for _ , child := range children {
7687 if child != nil {
77- childMJML , err := convertBlockToMJMLWithError (child , indentLevel + 1 , templateData )
88+ childMJML , err := convertBlockToMJMLWithErrorAndParsedData (child , indentLevel + 1 , templateData , parsedData )
7889 if err != nil {
7990 return "" , err
8091 }
@@ -87,6 +98,16 @@ func convertBlockToMJMLWithError(block EmailBlock, indentLevel int, templateData
8798
8899// convertBlockToMJML recursively converts a single EmailBlock to MJML string
89100func convertBlockToMJML (block EmailBlock , indentLevel int , templateData string ) string {
101+ // Parse template data once at the beginning
102+ parsedData , parseErr := parseTemplateDataString (templateData )
103+ if parseErr != nil {
104+ parsedData = nil // Continue with nil data if parsing fails
105+ }
106+ return convertBlockToMJMLWithParsedData (block , indentLevel , templateData , parsedData )
107+ }
108+
109+ // convertBlockToMJMLWithParsedData recursively converts a single EmailBlock to MJML string with pre-parsed data
110+ func convertBlockToMJMLWithParsedData (block EmailBlock , indentLevel int , templateData string , parsedData map [string ]interface {}) string {
90111 indent := strings .Repeat (" " , indentLevel )
91112 tagName := string (block .GetType ())
92113 children := block .GetChildren ()
@@ -100,10 +121,7 @@ func convertBlockToMJML(block EmailBlock, indentLevel int, templateData string)
100121 // Process Liquid templating for mj-text, mj-button, mj-title, mj-preview, and mj-raw blocks
101122 blockType := block .GetType ()
102123 if blockType == MJMLComponentMjText || blockType == MJMLComponentMjButton || blockType == MJMLComponentMjTitle || blockType == MJMLComponentMjPreview || blockType == MJMLComponentMjRaw {
103- parsedData , parseErr := parseTemplateDataString (templateData )
104- if parseErr != nil {
105- fmt .Printf ("Warning: Template data parsing failed for block %s: %v\n " , block .GetID (), parseErr )
106- } else {
124+ if parsedData != nil {
107125 processedContent , err := processLiquidContent (content , parsedData , block .GetID ())
108126 if err != nil {
109127 // Log error but continue with original content
@@ -115,15 +133,15 @@ func convertBlockToMJML(block EmailBlock, indentLevel int, templateData string)
115133 }
116134
117135 // Block with content - don't escape for mj-raw, mj-text, and mj-button (they can contain HTML)
118- attributeString := formatAttributes (block .GetAttributes ())
136+ attributeString := formatAttributesWithLiquid (block .GetAttributes (), parsedData , block . GetID ())
119137 if blockType == MJMLComponentMjRaw || blockType == MJMLComponentMjText || blockType == MJMLComponentMjButton {
120138 return fmt .Sprintf ("%s<%s%s>%s</%s>" , indent , tagName , attributeString , content , tagName )
121139 } else {
122140 return fmt .Sprintf ("%s<%s%s>%s</%s>" , indent , tagName , attributeString , escapeContent (content ), tagName )
123141 }
124142 } else {
125143 // Self-closing block or empty block
126- attributeString := formatAttributes (block .GetAttributes ())
144+ attributeString := formatAttributesWithLiquid (block .GetAttributes (), parsedData , block . GetID ())
127145 if attributeString != "" {
128146 return fmt .Sprintf ("%s<%s%s />" , indent , tagName , attributeString )
129147 } else {
@@ -133,15 +151,15 @@ func convertBlockToMJML(block EmailBlock, indentLevel int, templateData string)
133151 }
134152
135153 // Block with children
136- attributeString := formatAttributes (block .GetAttributes ())
154+ attributeString := formatAttributesWithLiquid (block .GetAttributes (), parsedData , block . GetID ())
137155 openTag := fmt .Sprintf ("%s<%s%s>" , indent , tagName , attributeString )
138156 closeTag := fmt .Sprintf ("%s</%s>" , indent , tagName )
139157
140158 // Process children
141159 var childrenMJML []string
142160 for _ , child := range children {
143161 if child != nil {
144- childrenMJML = append (childrenMJML , convertBlockToMJML (child , indentLevel + 1 , templateData ))
162+ childrenMJML = append (childrenMJML , convertBlockToMJMLWithParsedData (child , indentLevel + 1 , templateData , parsedData ))
145163 }
146164 }
147165
@@ -231,14 +249,19 @@ func getBlockContent(block EmailBlock) string {
231249
232250// formatAttributes formats attributes object into MJML attribute string
233251func formatAttributes (attributes map [string ]interface {}) string {
252+ return formatAttributesWithLiquid (attributes , nil , "" )
253+ }
254+
255+ // formatAttributesWithLiquid formats attributes object into MJML attribute string with liquid processing
256+ func formatAttributesWithLiquid (attributes map [string ]interface {}, templateData map [string ]interface {}, blockID string ) string {
234257 if len (attributes ) == 0 {
235258 return ""
236259 }
237260
238261 var attrPairs []string
239262 for key , value := range attributes {
240263 if shouldIncludeAttribute (value ) {
241- if attr := formatSingleAttribute (key , value ); attr != "" {
264+ if attr := formatSingleAttributeWithLiquid (key , value , templateData , blockID ); attr != "" {
242265 attrPairs = append (attrPairs , attr )
243266 }
244267 }
@@ -271,6 +294,11 @@ func shouldIncludeAttribute(value interface{}) bool {
271294
272295// formatSingleAttribute formats a single attribute key-value pair
273296func formatSingleAttribute (key string , value interface {}) string {
297+ return formatSingleAttributeWithLiquid (key , value , nil , "" )
298+ }
299+
300+ // formatSingleAttributeWithLiquid formats a single attribute key-value pair with liquid processing
301+ func formatSingleAttributeWithLiquid (key string , value interface {}, templateData map [string ]interface {}, blockID string ) string {
274302 // Convert camelCase to kebab-case for MJML attributes
275303 kebabKey := camelToKebab (key )
276304
@@ -290,25 +318,50 @@ func formatSingleAttribute(key string, value interface{}) string {
290318 if v == "" {
291319 return ""
292320 }
293- escapedValue := escapeAttributeValue (v , kebabKey )
321+ processedValue := processAttributeValue (v , kebabKey , templateData , blockID )
322+ escapedValue := escapeAttributeValue (processedValue , kebabKey )
294323 return fmt .Sprintf (` %s="%s"` , kebabKey , escapedValue )
295324 case * string :
296325 if v == nil || * v == "" {
297326 return ""
298327 }
299- escapedValue := escapeAttributeValue (* v , kebabKey )
328+ processedValue := processAttributeValue (* v , kebabKey , templateData , blockID )
329+ escapedValue := escapeAttributeValue (processedValue , kebabKey )
300330 return fmt .Sprintf (` %s="%s"` , kebabKey , escapedValue )
301331 default :
302332 // Handle other types (int, float, etc.) by converting to string
303333 strValue := fmt .Sprintf ("%v" , value )
304334 if strValue == "" {
305335 return ""
306336 }
307- escapedValue := escapeAttributeValue (strValue , kebabKey )
337+ processedValue := processAttributeValue (strValue , kebabKey , templateData , blockID )
338+ escapedValue := escapeAttributeValue (processedValue , kebabKey )
308339 return fmt .Sprintf (` %s="%s"` , kebabKey , escapedValue )
309340 }
310341}
311342
343+ // processAttributeValue processes attribute values through liquid templating if applicable
344+ func processAttributeValue (value , attributeKey string , templateData map [string ]interface {}, blockID string ) string {
345+ // Only process liquid templates for URL-related attributes that might contain dynamic content
346+ isURLAttribute := attributeKey == "href" || attributeKey == "src" || attributeKey == "action" ||
347+ attributeKey == "background-url" || strings .HasSuffix (attributeKey , "-url" )
348+
349+ // If templateData is nil or this isn't a URL attribute, return as-is
350+ if templateData == nil || ! isURLAttribute {
351+ return value
352+ }
353+
354+ // Process liquid content for URL attributes
355+ processedValue , err := processLiquidContent (value , templateData , fmt .Sprintf ("%s.%s" , blockID , attributeKey ))
356+ if err != nil {
357+ // If liquid processing fails, return original value and log warning
358+ fmt .Printf ("Warning: Liquid processing failed for attribute %s in block %s: %v\n " , attributeKey , blockID , err )
359+ return value
360+ }
361+
362+ return processedValue
363+ }
364+
312365// camelToKebab converts camelCase to kebab-case
313366func camelToKebab (str string ) string {
314367 // Use regex to find capital letters and replace them with hyphen + lowercase
0 commit comments