Joy DOM

HTML & CSS converter

Convert between HTML markup and Spec documents with the optional dom-converter-html module.

The optional dom-converter-html module converts a Spec to HTML and parses HTML back into a Spec. Use it to export a document for the web, or to import existing markup as a starting point. Everything goes through a single object, HtmlConverter.

Experimental

The HTML/CSS converter is experimental. Its API and output may change between releases, and it covers only the subset of HTML and CSS the spec maps — review converted output before relying on it in production.

dependencies {
    implementation("com.j0y.joy:dom-converter-html:1.0.0")
}

Spec → HTML

HtmlConverter.toHtml(spec, options) renders a Spec to an HTML string. By default it emits a self-contained document — <!DOCTYPE html> with a <style> block plus inline style="" attributes:

val html = HtmlConverter.toHtml(spec)

HtmlOptions controls the output shape:

data class HtmlOptions(
    val styleMode: StyleMode = StyleMode.STYLE_BLOCK_AND_INLINE,
    val documentMode: DocumentMode = DocumentMode.FULL_DOCUMENT,
    val prettyPrint: Boolean = true,
    val indent: String = "  ",
)
  • documentModeFULL_DOCUMENT wraps the tree in <html><head><body>; FRAGMENT emits just the layout element tree.
  • styleModeSTYLE_BLOCK_AND_INLINE writes the spec's style map as <style> rules and per-node props.style as inline attributes; INLINE_ONLY drops the <style> block and keeps only inline styles (selector rules are lost).
  • prettyPrint / indent — formatting of the emitted HTML.
// Just the element tree, compact:
HtmlConverter.toHtml(spec, HtmlOptions(documentMode = DocumentMode.FRAGMENT, prettyPrint = false))
// <p>hello</p>

The spec's breakpoints become @media rules inside the <style> block; per-node breakpoint overrides ride in a custom @joy-nodes { } at-rule the converter reads back on parse. Custom (non-built-in) node types render as <div data-joy-type="...">, which restores the original type on round-trip. Text is HTML-escaped on emit and decoded on parse (named, decimal, and hex entities).

HTML → Spec

HtmlConverter.fromHtml(html) parses an HTML string into a Spec:

val spec = HtmlConverter.fromHtml(html)

Input can be a full document or a bare fragment. The parser is lenient — it skips comments and the doctype, lowercases tag and attribute names, and accepts quoted or unquoted attribute values. CSS inside <style> blocks lands in spec.style (and @media blocks in spec.breakpoints); inline style="" attributes land in each node's props.style. Malformed input — unterminated tags, missing root, invalid values — throws HtmlParseException.

What's supported

Elements

TagNotes
div, span, p, h1h6Built-in, rendered as the matching HTML tag.
imgBuilt-in; void/self-closing.
any other typeCarried through as a data-joy-type div.

Selectors

SelectorSupported
Element — divYes
Id — #rootYes
Class — .cardYes
Pseudo-class — :hover, :focusNo — silently dropped

CSS properties

Bidirectional across the full spec property set.

GroupProperties
Layout/positioningposition, display, top/left/bottom/right, z-index, overflow, box-sizing
Flexboxflex-*, justify-content, align-items, align-self, gap/row-gap/column-gap, order
Sizingwidth/height + min-/max- variants
Spacingpadding/margin (1–4-value shorthand)
Bordersborder-width/-color/-style/-radius
Typographyfont-*, color, text-*, line-height, letter-spacing, white-space
Background/effectsbackground-color, opacity
Imageobject-fit, object-position

Media queries

FeatureSupported
min-width / max-widthYes
Range syntaxCSS Level 4 — (width > 600px)
orientationYes
Operatorsand, , (OR), not

Unsupported CSS is dropped, not errored

Custom properties, gradients, transforms, transitions, animations, and grid have no spec equivalent — the parser silently ignores them (and any unknown property) for forward compatibility. Unknown HTML attributes are preserved in the node's extras map.

Round-trip behavior

A Spec → HTML → Spec round-trip is structurally lossless: every property fixture under assets/samples/properties/ survives it. Two normalizations are unavoidable because HTML can't represent the distinction — empty children: [] collapses to children: null, and a border-radius corner of 0px normalizes to omitted. Both sides apply these before comparison.

What's next

On this page