Skip to main content

Contributing

Thank you for your interest in contributing to mjml-java! This guide covers how to set up your development environment, run tests, and submit changes.

Prerequisites

  • Java 17 or later (the project targets Java 17)
  • Maven 3.8+ (used for building and testing)
  • Git for version control

Building the Project

Clone the repository and build:

git clone https://github.com/jcputney/mjml-java.git
cd mjml-java
mvn clean verify

This compiles the source, runs all tests, and generates a JaCoCo code coverage report. Per-module reports are generated under:

  • mjml-java-core/target/site/jacoco/
  • mjml-java-resolvers/target/site/jacoco/
  • mjml-java-spring/target/site/jacoco/

To build without running tests:

mvn clean package -DskipTests

Running Tests

The project has over 1,000 tests across all modules. To check current counts in your checkout:

rg -n "@Test" mjml-java-core/src/test mjml-java-resolvers/src/test mjml-java-spring/src/test | wc -l

Run all tests across all modules:

mvn clean verify

Run tests for a specific module:

mvn test -pl mjml-java-core

Golden File Tests

These tests render MJML templates and compare the output against known-good HTML files generated by the official MJML v4 Node.js toolchain. They ensure compatibility with the MJML specification.

Golden test resources are located flat in a single directory:

mjml-java-core/src/test/resources/golden/
├── basic-layout.mjml # MJML source
├── basic-layout.html # Expected HTML output
├── social-component.mjml
├── social-component.html
└── ...

Each golden test:

  1. Reads an .mjml file from mjml-java-core/src/test/resources/golden/
  2. Renders it to HTML using MjmlRenderer.render()
  3. Compares the output against the corresponding .html file in the same directory

Adding a New Golden Test

  1. Create your MJML template and save it as mjml-java-core/src/test/resources/golden/my-test.mjml
  2. Generate the expected HTML using the official MJML CLI:
    npx mjml mjml-java-core/src/test/resources/golden/my-test.mjml -o mjml-java-core/src/test/resources/golden/my-test.html
  3. The test runner automatically picks up new .mjml files in the directory

Project Structure

mjml-java is a multi-module Maven project:

mjml-java-parent/                # Parent POM
├── mjml-java-core/ # Core renderer (zero dependencies)
│ └── src/main/java/dev/jcputney/mjml/
│ ├── MjmlRenderer.java # Public API entry point
│ ├── MjmlConfiguration.java # Immutable configuration
│ ├── MjmlRenderResult.java # Render result record
│ ├── MjmlException.java # Base exception
│ ├── MjmlRenderException.java # Render-phase exception
│ ├── IncludeResolver.java # Include resolution interface
│ ├── ResolverContext.java # Include chain metadata
│ ├── ContentSanitizer.java # Content sanitization hook
│ ├── Direction.java # LTR/RTL/AUTO enum
│ ├── FileSystemIncludeResolver.java
│ ├── ClasspathIncludeResolver.java
│ ├── component/ # Component hierarchy
│ ├── context/ # Rendering context
│ ├── css/ # CSS inlining engine
│ ├── parser/ # MJML parser
│ ├── render/ # Rendering pipeline
│ └── util/ # Utilities
├── mjml-java-resolvers/ # Additional resolvers (zero dependencies)
│ └── src/main/java/dev/jcputney/mjml/resolver/
│ ├── MapIncludeResolver.java
│ ├── CompositeIncludeResolver.java
│ ├── CachingIncludeResolver.java
│ ├── UrlIncludeResolver.java
│ └── PrefixRoutingIncludeResolver.java
├── mjml-java-spring/ # Spring Boot integration
│ └── src/main/java/dev/jcputney/mjml/spring/
│ ├── MjmlProperties.java
│ ├── MjmlService.java
│ ├── ThymeleafMjmlService.java
│ ├── SpringResourceIncludeResolver.java
│ └── autoconfigure/
└── mjml-java-bom/ # Bill of Materials

Adding a New Component

Body Component (renders HTML)

  1. Create a new class in the appropriate package (body/, content/, or interactive/)
  2. Extend BodyComponent
  3. Implement the required methods:
public class MjCustom extends BodyComponent {

public MjCustom(MjmlNode node, GlobalContext globalContext, RenderContext renderContext) {
super(node, globalContext, renderContext);
}

@Override
public String getTagName() {
return "mj-custom";
}

@Override
public Map<String, String> getDefaultAttributes() {
return Map.of(
"padding", "10px 25px",
"color", "#000000"
);
}

@Override
public String render() {
String padding = getAttribute("padding", "10px 25px");
String color = getAttribute("color", "#000000");
return "<div style=\"padding:" + padding + ";color:" + color + ";\">"
+ "Custom content"
+ "</div>";
}
}
  1. Register the component in RenderPipeline.createRegistry():
    reg.register("mj-custom", MjCustom::new);

Head Component (processes metadata)

  1. Extend HeadComponent
  2. Implement process() to update the GlobalContext:
public class MjCustomHead extends HeadComponent {

public MjCustomHead(MjmlNode node, GlobalContext globalContext, RenderContext renderContext) {
super(node, globalContext, renderContext);
}

@Override
public String getTagName() {
return "mj-custom-head";
}

@Override
public void process() {
// Update globalContext with metadata from this component's attributes
}
}

Code Style

  • Google Java Format via Spotless (enforced on mvn verify)
  • Java 17 features are available (records, sealed classes, text blocks, etc.)
  • Note: Pattern-matching switch on sealed types is not available in Java 17; use if-else instanceof chains instead
  • Zero external runtime dependencies -- only JDK standard library
  • Test dependencies: JUnit Jupiter 6

Submitting Changes

  1. Fork the repository
  2. Create a feature branch from main
  3. Make your changes with tests
  4. Run the full test suite: mvn clean verify
  5. Submit a pull request with a clear description of the changes

All pull requests should include appropriate test coverage. For new components, include at least a golden file test demonstrating the rendered output.