Skip to content

Latest commit

 

History

History
136 lines (91 loc) · 5.28 KB

File metadata and controls

136 lines (91 loc) · 5.28 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Build Commands

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:check

Quality Gates

All 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

Project Structure

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.

Architecture

Rendering Pipeline (7 phases)

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).

Component Hierarchy

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().

Context Objects

GlobalContext is a slim facade delegating to:

  • MetadataContext — title, preview, breakpoint, containerWidth
  • StyleContext — fonts, styles, mediaQueries (FontDef/MediaQuery records live here)
  • AttributeContext — default/class/html attributes

Attribute Cascade (highest to lowest priority)

inline → mj-class → tag defaults → mj-all → component hardcoded defaults

Key Patterns

  • 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 per MjmlConfiguration (ConcurrentHashMap, max 256 entries)
  • CSS inliner uses its own lightweight HTML parser (not JDK XML) to handle MSO conditionals
  • MjmlConfiguration has equals/hashCode using identity comparison for functional fields

Public API

// 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.

Security

  • sanitizeHref() allowlist: http:, https:, mailto:, tel:, #, /
  • escapeAttr() on all attribute outputs + VML attributes
  • FEATURE_SECURE_PROCESSING on XML parser (entity expansion protection)
  • UrlIncludeResolver blocks loopback/site-local/link-local/multicast IPs
  • Configurable maxInputSize, maxNestingDepth, maxIncludeDepth

Testing

~1,116 tests total (core: ~1,020, resolvers: ~75, spring: ~21).

Golden File Tests

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

JPMS Modules

  • dev.jcputney.mjml — exports: dev.jcputney.mjml, .component, .context, .parser, .css
  • dev.jcputney.mjml.resolver — requires core + java.net.http
  • Spring module uses Automatic-Module-Name header (not full JPMS)

Notable Gotchas

  • mj-hero default mode is fluid-height (not fixed-height)
  • Preprocessor wraps raw content in CDATA; single-pass alternation regex
  • VML origin/position uses different formulas for repeat vs no-repeat backgrounds
  • DocumentBuilder access uses ThreadLocal<DocumentBuilder> with reset() before each parse
  • ColumnWidthCalculator uses boolean[] for auto-width tracking (not 0 sentinel)
  • DEFAULT_CONTAINER_WIDTH = 600 constant in MjmlConfiguration