This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Maven is installed via sdkman. Always use the absolute path:
MVN=~/.sdkman/candidates/maven/current/bin/mvn
# Full build with tests, SpotBugs, JaCoCo, Spotless, and Javadoc
$MVN verify
# Tests only
$MVN test
# Single module
$MVN test -pl mjml-java-core
# Single test class
$MVN test -pl mjml-java-core -Dtest=MjmlRendererTest
# Single test method
$MVN test -pl mjml-java-core -Dtest=MjmlRendererTest#rendersSimpleTextTemplate
# Format code (Google Java Format)
$MVN spotless:apply
# Check formatting without fixing
$MVN spotless:checkAll enforced on mvn verify and will fail the build:
- Spotless — Google Java Format 1.34.1
- SpotBugs — Max effort, High threshold, failOnError=true
- JaCoCo — Line coverage minimums: core 80%, resolvers 80%, spring 70%
- Javadoc — failOnError=true
Four Maven modules under mjml-java-parent:
| Module | Purpose | External deps |
|---|---|---|
mjml-java-core |
MJML-to-HTML renderer, 31 components | None (JDK only) |
mjml-java-resolvers |
URL/caching include resolvers | None (java.net.http) |
mjml-java-spring |
Spring Boot auto-configuration | Spring Boot 4.0.2 |
mjml-java-bom |
Bill of Materials POM | N/A |
Java 17 target. Cannot use pattern-matching switch on sealed types — use if-else instanceof chains.
RenderPipeline orchestrates: preprocess (entity/CDATA handling) → parse (XML to MjmlDocument) → includes (<mj-include> expansion) → head (fonts/styles/attributes extraction) → attributes (5-level cascade resolution) → render (top-down component HTML) → skeleton (full HTML document + CSS inlining).
sealed BaseComponent
├── non-sealed BodyComponent → render() returns HTML string
└── non-sealed HeadComponent → process() updates context sub-objects
31 registered components in RenderPipeline.createRegistry(). New components: extend BodyComponent or HeadComponent, implement getTagName()/getDefaultAttributes()/render() or process(), register in createRegistry().
GlobalContext is a slim facade delegating to:
MetadataContext— title, preview, breakpoint, containerWidthStyleContext— fonts, styles, mediaQueries (FontDef/MediaQueryrecords live here)AttributeContext— default/class/html attributes
inline → mj-class → tag defaults → mj-all → component hardcoded defaults
- Head components call
globalContext.metadata(),.styles(),.attributes()to register data - Body components build HTML strings using
getAttribute()for cascaded values MsoHelper— shared MSO conditional HTML (used by MjSection, MjWrapper, MjHero)ComponentRegistry— cached perMjmlConfiguration(ConcurrentHashMap, max 256 entries)- CSS inliner uses its own lightweight HTML parser (not JDK XML) to handle MSO conditionals
MjmlConfigurationhasequals/hashCodeusing identity comparison for functional fields
// Static convenience (new pipeline each call)
MjmlRenderResult result = MjmlRenderer.render(mjmlString);
// Instance API (preferred — reuses pipeline & registry cache)
MjmlRenderer renderer = MjmlRenderer.create(config);
MjmlRenderResult result = renderer.renderTemplate(mjmlString);All render()/renderTemplate() overloads return MjmlRenderResult.
sanitizeHref()allowlist:http:,https:,mailto:,tel:,#,/escapeAttr()on all attribute outputs + VML attributesFEATURE_SECURE_PROCESSINGon XML parser (entity expansion protection)UrlIncludeResolverblocks loopback/site-local/link-local/multicast IPs- Configurable
maxInputSize,maxNestingDepth,maxIncludeDepth
~1,116 tests total (core: ~1,020, resolvers: ~75, spring: ~21).
40 MJML files in mjml-java-core/src/test/resources/golden/ with matching .html expected output. GoldenFileTest uses @TestFactory dynamic tests. On failure, writes .actual.html alongside expected for diffing.
NEVER manually edit the .html golden files. They are generated by the official MJML library and serve as the source of truth for verifying our rendering matches the reference implementation. To regenerate: npx mjml <file>.mjml -o <file>.html
dev.jcputney.mjml— exports:dev.jcputney.mjml,.component,.context,.parser,.cssdev.jcputney.mjml.resolver— requires core +java.net.http- Spring module uses
Automatic-Module-Nameheader (not full JPMS)
mj-herodefault mode isfluid-height(notfixed-height)- Preprocessor wraps raw content in CDATA; single-pass alternation regex
- VML origin/position uses different formulas for repeat vs no-repeat backgrounds
DocumentBuilderaccess usesThreadLocal<DocumentBuilder>withreset()before each parseColumnWidthCalculatorusesboolean[]for auto-width tracking (not 0 sentinel)DEFAULT_CONTAINER_WIDTH = 600constant inMjmlConfiguration