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 = " ",
)documentMode—FULL_DOCUMENTwraps the tree in<html><head><body>;FRAGMENTemits just the layout element tree.styleMode—STYLE_BLOCK_AND_INLINEwrites the spec'sstylemap as<style>rules and per-nodeprops.styleas inline attributes;INLINE_ONLYdrops 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
| Tag | Notes |
|---|---|
div, span, p, h1–h6 | Built-in, rendered as the matching HTML tag. |
img | Built-in; void/self-closing. |
any other type | Carried through as a data-joy-type div. |
Selectors
| Selector | Supported |
|---|---|
Element — div | Yes |
Id — #root | Yes |
Class — .card | Yes |
Pseudo-class — :hover, :focus | No — silently dropped |
CSS properties
Bidirectional across the full spec property set.
| Group | Properties |
|---|---|
| Layout/positioning | position, display, top/left/bottom/right, z-index, overflow, box-sizing |
| Flexbox | flex-*, justify-content, align-items, align-self, gap/row-gap/column-gap, order |
| Sizing | width/height + min-/max- variants |
| Spacing | padding/margin (1–4-value shorthand) |
| Borders | border-width/-color/-style/-radius |
| Typography | font-*, color, text-*, line-height, letter-spacing, white-space |
| Background/effects | background-color, opacity |
| Image | object-fit, object-position |
Media queries
| Feature | Supported |
|---|---|
min-width / max-width | Yes |
| Range syntax | CSS Level 4 — (width > 600px) |
orientation | Yes |
| Operators | and, , (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
- Kotlin DSL — build a
Specin type-safe Kotlin instead of parsing HTML. - Specification — the properties and selectors the converter maps.
- API reference (Dokka) ↗ — the full
HtmlConverterandHtmlOptionssurface.