Skip to content

Render MJML from the AST#8

Open
preslavrachev wants to merge 6 commits intomainfrom
feat/render-mjml-from-ast
Open

Render MJML from the AST#8
preslavrachev wants to merge 6 commits intomainfrom
feat/render-mjml-from-ast

Conversation

@preslavrachev
Copy link
Copy Markdown
Owner

@preslavrachev preslavrachev commented Aug 6, 2025

This PR extends the MJML rendering system to support dual output formats, enabling AST-to-MJML rendering alongside the existing AST-to-HTML functionality. This allows for MJML round-trip validation and opens possibilities for generating Go code from MJML templates.

  • Introduced OutputFormat enum with HTML and MJML options
  • Added RenderMJML method to all component interfaces
  • Renamed existing Render methods to RenderHTML for clarity

Motivation

This addition extends the existing implementation with back-to-back rendering of MJML=>AST=>HTML/MJML.

Why do we want the AST to render MJML back? Isn't the MJML content sort of the input that we are given anyway?

In most cases, yes, but imagine the following: having the MJML AST in Go code opens up new possibilities, one of which is generating Go code out of some MJML, and using the Go code as the one and only way we render email templates. This would be great, but until now, it wouldn't be able to verify and validate the MJML code out of the Go code. We either had to store both versions (leads to duplication and possible inconsistencies) or rely on never changing the generated Go code. If you did want to change it, however (e.g., include some loops, conditional logic, etc.), you would need a way to see the MJML this new Go code would generate - it's just way easier to debug MJML code than having to debug its generated HTML output.

@preslavrachev preslavrachev requested a review from Copilot August 6, 2025 12:40

This comment was marked as outdated.

@preslavrachev preslavrachev requested a review from Copilot August 6, 2025 19:15

This comment was marked as outdated.

@preslavrachev preslavrachev requested a review from Copilot August 6, 2025 19:48
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR extends the MJML rendering system to support dual output formats, enabling AST-to-MJML rendering alongside existing AST-to-HTML functionality. This allows for MJML round-trip validation and supports generating Go code from MJML templates.

  • Added OutputFormat enum and RenderMJML interface methods for MJML output
  • Renamed existing Render methods to RenderHTML for clarity
  • Introduced indentation configuration and XML attribute escaping for MJML output

Reviewed Changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
mjml/options/options.go Added OutputFormat enum and IndentationConfig for MJML formatting
mjml/render.go Introduced dual output support with OutputFormat option and RenderMJML methods
mjml/component.go Added depth tracking and RenderMJML interface methods to component creation
mjml/components/base.go Extended Component interface with RenderMJML and depth tracking methods
mjml/html/escape.go Added XML attribute escaping function for security
Multiple component files Implemented RenderMJML methods and renamed Render to RenderHTML

Comment on lines +77 to +81
indent := ""
for i := 0; i < totalLevel; i++ {
indent += ic.Unit
}
return indent
Copy link

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using strings.Repeat instead of a loop for better performance when generating indentation strings. Replace the loop with: return strings.Repeat(ic.Unit, totalLevel)

Suggested change
indent := ""
for i := 0; i < totalLevel; i++ {
indent += ic.Unit
}
return indent
return strings.Repeat(ic.Unit, totalLevel)

Copilot uses AI. Check for mistakes.

func (c *MJTextComponent) RenderMJML(w io.StringWriter) error {
// Write opening tag with indentation
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(4) + "<mj-text"); err != nil {
Copy link

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded indentation level of 4 should be derived from the component's depth instead of being fixed. Use c.GetDepth() to maintain consistent indentation based on the actual component hierarchy.

Suggested change
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(4) + "<mj-text"); err != nil {
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(c.GetDepth()) + "<mj-text"); err != nil {

Copilot uses AI. Check for mistakes.

func (c *MJSectionComponent) RenderMJML(w io.StringWriter) error {
// Write opening tag with indentation
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(2) + "<mj-section"); err != nil {
Copy link

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded indentation level of 2 should be derived from the component's depth instead of being fixed. Use c.GetDepth() to maintain consistent indentation based on the actual component hierarchy.

Suggested change
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(2) + "<mj-section"); err != nil {
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(c.GetDepth()) + "<mj-section"); err != nil {

Copilot uses AI. Check for mistakes.

func (c *MJColumnComponent) RenderMJML(w io.StringWriter) error {
// Write opening tag with indentation
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(3) + "<mj-column"); err != nil {
Copy link

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded indentation level of 3 should be derived from the component's depth instead of being fixed. Use c.GetDepth() to maintain consistent indentation based on the actual component hierarchy.

Suggested change
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(3) + "<mj-column"); err != nil {
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(c.GetDepth()) + "<mj-column"); err != nil {

Copilot uses AI. Check for mistakes.

func (c *MJBodyComponent) RenderMJML(w io.StringWriter) error {
// Write opening tag with indentation
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(1) + "<mj-body"); err != nil {
Copy link

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded indentation level of 1 should be derived from the component's depth instead of being fixed. Use c.GetDepth() to maintain consistent indentation based on the actual component hierarchy.

Suggested change
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(1) + "<mj-body"); err != nil {
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(c.GetDepth()) + "<mj-body"); err != nil {

Copilot uses AI. Check for mistakes.

func (c *MJHeadComponent) RenderMJML(w io.StringWriter) error {
// Opening tag with newline and indentation
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(1) + "<mj-head"); err != nil {
Copy link

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded indentation level of 1 should be derived from the component's depth instead of being fixed. Use c.GetDepth() to maintain consistent indentation based on the actual component hierarchy.

Suggested change
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(1) + "<mj-head"); err != nil {
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(c.GetDepth()) + "<mj-head"); err != nil {

Copilot uses AI. Check for mistakes.

func (c *MJTitleComponent) RenderMJML(w io.StringWriter) error {
// Opening tag with newline and indentation
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(2) + "<mj-title"); err != nil {
Copy link

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded indentation level of 2 should be derived from the component's depth instead of being fixed. Use c.GetDepth() to maintain consistent indentation based on the actual component hierarchy.

Suggested change
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(2) + "<mj-title"); err != nil {
if _, err := w.WriteString("\n" + c.RenderOpts.Indentation.GetIndent(c.GetDepth()) + "<mj-title"); err != nil {

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants