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:
- Reads an
.mjmlfile frommjml-java-core/src/test/resources/golden/ - Renders it to HTML using
MjmlRenderer.render() - Compares the output against the corresponding
.htmlfile in the same directory
Adding a New Golden Test
- Create your MJML template and save it as
mjml-java-core/src/test/resources/golden/my-test.mjml - 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 - The test runner automatically picks up new
.mjmlfiles 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)
- Create a new class in the appropriate package (
body/,content/, orinteractive/) - Extend
BodyComponent - 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>";
}
}
- Register the component in
RenderPipeline.createRegistry():reg.register("mj-custom", MjCustom::new);
Head Component (processes metadata)
- Extend
HeadComponent - Implement
process()to update theGlobalContext:
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
switchon sealed types is not available in Java 17; useif-else instanceofchains instead - Zero external runtime dependencies -- only JDK standard library
- Test dependencies: JUnit Jupiter 6
Submitting Changes
- Fork the repository
- Create a feature branch from
main - Make your changes with tests
- Run the full test suite:
mvn clean verify - 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.