A miniature browser rendering engine written in Java. It takes an HTML file, parses it into a DOM tree, applies CSS, computes the cascade, lays the page out into boxes, and paints the result to a PNG image — plus a self-contained, interactive parse-tree report. Think of it as the essential skeleton of the Chrome/Firefox pipeline in a few thousand lines of code.
- Hand-written HTML parser — single-pass, tolerant of comments, declarations, CDATA and ~200 named entities
- CSS parser & cascade — complex selectors, combinators, pseudo-classes, specificity triples and
!important - Style resolution — browser defaults + inheritance + rules + inline
styleattributes - Layout engine — block and inline layout, box model, margin collapsing, text wrapping with real font metrics
- PNG rendering — paints the page with Java AWT, with an optional DevTools-style box-model debug overlay
- Parse-tree report — a self-contained
report.htmlwith an SVG diagram, tree-algorithm outputs, specificity and computed-style tables
The engine reproduces the real browser rendering pipeline in six stages. Each stage consumes the output of the previous one:
HTML file
│ SimpleHtmlParser.parse() build DOM tree
▼
DOM tree (DomNode)
│ extractCss() pull CSS out of <style> elements
▼
CSS string
│ SimpleCssParser.parse() turn into a list of CssRule
▼
List<CssRule>
│ StyleResolver.resolveTree() walk the DOM, compute final styles
▼
DOM tree with computedStyle
│ LayoutTreeBuilder.build() styled DOM → layout box tree
▼
Layout tree (LayoutBox)
│ LayoutEngine.layout() block + inline layout (box model, margins)
▼
Positioned boxes + text lines
│ Renderer.render() paint to a PNG (optional box-model overlay)
▼
out/render.png
The same DOM tree feeds a parallel analysis pipeline that produces the interactive report:
DOM tree
│ DomTreeBuilder.build() → generic Tree<TreeNode<DomNode>>
▼
Tree<TreeNode<DomNode>>
│ Tree.preOrder()/postOrder()/levels() run the tree algorithms
│ ReportGenerator.generate() → SVG diagram + specificity + computed styles
▼
out/report.html
| Stage | Component | What happens |
|---|---|---|
| 1. Parse | html/SimpleHtmlParser |
Walks the source char-by-char and builds the DOM tree, stripping whitespace-only text between elements. Handles comments, <!DOCTYPE> declarations, CDATA sections and character references. |
| 2. Extract CSS | Main.extractCss() |
Collects the text content of every <style> element into one CSS string. An optional external CSS file is appended after it (so external rules win on ties). |
| 3. Parse CSS | css/SimpleCssParser |
Splits the CSS into CssRule objects: selectors + declarations, each carrying an !important flag. |
| 4. Resolve styles | style/StyleResolver |
Computes each element's final style by layering browser defaults → inherited properties → matching rules (sorted by specificity) → inline style="" attributes. |
| 5. Layout | layout/* |
LayoutTreeBuilder turns the styled DOM into a box tree (block/inline/anonymous-block). LayoutEngine.layout() walks it with a viewport size, computing dimensions and stacking boxes and text lines (including vertical margin collapsing). |
| 6. Render | layout/Renderer |
Paints the box tree onto a BufferedImage and writes it as a PNG via ImageIO. |
The engine writes two kinds of images, both to the out/ folder:
Renderer.render() builds a BufferedImage the size of the viewport and walks the layout tree recursively:
- Background — fills the canvas white, then paints each box's
background-colorover its padding-box rectangle. - Borders — for each side with a non-zero
border-*-width, fills a thin rect along that edge. - Text — for every
TextFragmentin everyLineBox, sets the font (via real AWT font metrics fromTextMetrics.fontFor()), picks the color from the computed style, and draws the string at its measured position withg.drawString(). - Children — recurses into child boxes in document order, so later (deeper) elements paint on top.
The output is written with javax.imageio.ImageIO.write(img, "png", ...). Text is anti-aliased.
Pass the --boxmodel flag to paint the same page again with a DevTools-style overlay on top:
| Color | Region |
|---|---|
| blue | content |
| green | padding |
| brown | border |
| orange | margin |
Each region is drawn as translucent fills between the box's content, padding, border and margin rectangles, with a color legend in the top-left corner.
The images under
docs/in this repo are generated by the engine itself (e.g.boxmodel.htmlwith--boxmodel) and committed so they survive the per-runout/cleanup.
src/main/java/org/example/browser/
├── Main.java entry point: loads files, runs the pipeline, prints results
│
├── html/
│ └── SimpleHtmlParser.java hand-written HTML parser → DOM tree
│
├── dom/
│ ├── DomNode.java common node API (name, attributes, children, text)
│ ├── ElementNode.java a tag node (div, span, …) + its computed style
│ └── TextNode.java a text leaf node
│
├── css/
│ ├── CssRule.java a rule: selectors + declarations (!important flags)
│ ├── CssSelector.java chain of compound selectors joined by combinators
│ ├── SimpleSelector.java one compound selector: tag / id / class / pseudo-classes
│ ├── CssDeclaration.java a property declaration (value + !important flag)
│ └── SimpleCssParser.java CSS string → List<CssRule>
│
├── style/
│ └── StyleResolver.java computes final styles (defaults + inheritance + rules + inline)
│
├── layout/ the box tree + layout + rendering
│ ├── LayoutTreeBuilder.java styled DOM → layout box tree (block/inline + anonymous blocks)
│ ├── LayoutEngine.java entry point: lays out a tree in a viewport; prints the box tree
│ ├── LayoutBox.java one box: type, element, style spec, dimensions, children, text lines
│ ├── BoxType.java BlockNode / InlineNode / AnonymousBlock
│ ├── BoxSpec.java parsed style: lengths, colors, font info
│ ├── Length.java a length: px / percent / auto
│ ├── BoxDimensions.java content / padding / border / margin rects
│ ├── Rect.java rectangle
│ ├── EdgesSizes.java per-edge sizes
│ ├── BlockLayout.java block layout: widths, auto margins, child stacking, heights
│ ├── InlineLayout.java wraps inline text into lines (measure + word wrap)
│ ├── LineBox.java a line of text
│ ├── TextFragment.java one word in a line
│ ├── TextMetrics.java real font metrics via java.awt.Font
│ ├── MarginCollapser.java vertical margin collapsing
│ └── Renderer.java paints the page to a PNG (optional box-model debug overlay)
│
├── tree/ generic tree data structure + algorithms
│ ├── TreeNode.java node with value, parent pointer, list of children
│ └── Tree.java preOrder(), postOrder(), levels() (BFS), height, nodeCount, leafCount
│
├── report/ the parse-tree report generator
│ ├── DomTreeBuilder.java DOM tree → generic Tree<TreeNode<DomNode>> (wraps under <root>)
│ ├── ReportGenerator.java builds out/report.html: stats, highlighted source, SVG diagram,
│ │ tree-algorithm outputs, rule specificity, computed-style tables
│ └── HtmlUtil.java HTML escaping for the generated report
Sample files:
src/main/resources/ bundled defaults used when no arguments are given
├── index.html demo markup with an embedded <style> block
└── style.css demo external stylesheet
project root extra demo pages (resolved from the filesystem)
├── article.html / article.css a long-form blog page with sections, lists, quotes
├── boxmodel.html / boxmodel.css a box-model demo, best run with the --boxmodel flag
└── demo.html / demo.css another demo page
- Java 17 or newer
- Maven
Build and run with the bundled defaults (index.html + style.css):
mvn compile
mvn exec:javaThis prints the extracted CSS, the number of parsed rules, the DOM tree with computed styles, the layout tree, and writes out/render.png plus out/report.html.
| Position | Meaning | Default |
|---|---|---|
args[0] |
HTML file (filesystem path or bundled resource name) | index.html |
args[1] |
External CSS file (merged after the embedded <style> CSS) |
style.css |
args[2] |
Viewport width in px | 800 |
args[3] |
Viewport height in px | 600 |
args[4] |
--boxmodel → also write out/render-boxmodel.png with the debug overlay |
— |
args[5] |
Custom output path for the content image (box-model image gets a -boxmodel suffix) |
out/render.png |
HTML only (CSS comes from the <style> block inside the HTML):
mvn exec:java "-Dexec.args=C:\path\to\page.html"HTML + external CSS (merged after the embedded <style> CSS):
mvn exec:java "-Dexec.args=C:\path\to\page.html C:\path\to\style.css"Files inside src/main/resources (pass the filename only — the loader falls back to the classpath):
mvn exec:java "-Dexec.args=index.html style.css"Box-model demo with the debug overlay:
mvn exec:java "-Dexec.args=boxmodel.html boxmodel.css 900 500 --boxmodel"Article/blog demo (long page — good for the parse-tree report):
mvn exec:java "-Dexec.args=article.html article.css 900 900"Extra demo page:
mvn exec:java "-Dexec.args=demo.html demo.css"PowerShell note: wrap the whole
-Dexec.args=...argument in quotes. On CMD you can write-Dexec.args="C:\path\page.html"instead.
mvn compile
java -cp target/classes org.example.browser.Main C:\path\to\page.html C:\path\to\style.cssMain.loadText() checks the filesystem first; if the path does not exist, it falls back to a bundled resource with the same name.
| Command | HTML source | CSS source |
|---|---|---|
mvn exec:java |
bundled index.html |
<style> + bundled style.css |
mvn exec:java "-Dexec.args=page.html" |
your page.html |
<style> + bundled style.css |
mvn exec:java "-Dexec.args=page.html style.css" |
your file | <style> + your style.css |
The out/ folder is cleared on every run, so it always holds only the latest pair:
| File | Contents |
|---|---|
out/render.png |
the plain page (content) image |
out/render-boxmodel.png |
with the box-model debug overlay (only if --boxmodel is passed) |
out/report.html |
the self-contained parse-tree report (written on every run) |
SimpleHtmlParser is a hand-written single-pass parser. It walks the source character by character and builds the DOM tree while collapsing whitespace-only text between elements. Beyond plain <tag> / </tag> / attr="value" handling, it understands:
- Comments —
<!-- ... -->are skipped entirely. - Declarations —
<!DOCTYPE html>and any other<!...>declaration are consumed and ignored. - CDATA sections —
<![CDATA[...]]>is kept as a raw text node; its content is not entity-decoded. - Character references — decoded while parsing:
- decimal
&→& - hexadecimal
&→& - named entities (
&,<,>,", ,—,…, …) from a table of ~200 common HTML entities. - decoding is single-pass:
&lt;becomes<and is left alone (no double decoding).
- decimal
StyleResolver builds each element's final style by layering sources in order of increasing priority:
- Browser defaults — a tiny user-agent stylesheet (display, margins, padding, color, font-size, …)
- Inherited properties from the parent (
color,font-size,font-family) - All matching CSS rules, sorted by specificity (
#id= 100,.class= 10,tag= 1) - Inline
style="..."attributes — highest priority
External CSS is appended after embedded <style> CSS, so on ties the external rules win (source order).
Selectors are parsed into a chain of compound parts joined by combinators and matched right-to-left against the DOM tree (nodes carry a parent pointer for ancestor/sibling traversal).
Combinators:
| Selector | Meaning |
|---|---|
div p |
descendant — any p inside a div |
div > p |
child — direct child only |
h2 + p |
adjacent sibling — p immediately after h2 |
h2 ~ p |
general sibling — any p after h2 |
* |
universal — matches any element |
Pseudo-classes:
:first-child,:last-child,:only-child:nth-child(An+B)— alsoodd,even, and plain numbers (e.g.2n+1,-n+3,3):first-of-type,:last-of-type,:nth-of-type(An+B):not(selector)— accepts one compound selector as its argument- Interaction pseudo-classes (
:hover,:active,:focus,:visited,:link) parse but never match, since the engine has no interaction state
Cascade & specificity:
- Specificity is a true
(ids, classes, types)triple summed over every compound part (including:not()arguments), compared lexicographically — not just a flat number. - A rule's cascade weight comes from the highest-specificity selector that actually matched.
- Ties are broken by source order (the stable sort keeps the later rule winning).
!importantdeclarations win over all normal ones; among important declarations, specificity and source order still apply.- Priority:
normal rules → normal inline → !important rules → !important inline.
Example:
ul li { color: green; } /* descendant: all <li> inside <ul> */
div > p { color: purple; } /* child only */
li:nth-child(2n+1) { background: yellow; } /* odd rows */
p.note { color: red !important; } /* beats any normal rule, even #wrap p */Every run writes out/report.html — a single self-contained HTML page (no external assets) that shows how the HTML was parsed and how the CSS was resolved:
- Stats cards — DOM node count, tree height, leaf count, number of CSS rules.
- Input HTML — syntax-highlighted, with the input CSS in a collapsible
<details>block. - DOM parse tree — an SVG diagram of the whole tree:
- nodes are colored by kind (structural
html/body/head= slate, block elements = blue, inline elements = teal, headings = purple, text nodes = white pills); - node size adapts to its label/attributes;
- parent→child edges are cubic Bézier curves with arrowheads;
- dashed horizontal guide lines mark each tree level;
- a legend explains the color scheme;
- a "Show traversal indices" checkbox (default off) reveals each node's pre-order, post-order, BFS level and BFS index badges.
- nodes are colored by kind (structural
- Tree algorithms — the pre-order (DFS discovery), post-order (DFS finish) and BFS level-order lists produced by the
treepackage. - Parsed CSS rules — every rule with its selectors, computed specificity
(ids, classes, types)and declarations. - Computed styles — the final cascade output for each element (with expand/collapse-all buttons).
The SVG layout uses a bottom-up tidy algorithm: each node is centered over its children, and sibling subtrees are spaced so their bounding envelopes never overlap (a GAP between contours at every depth). Node widths are measured from the actual label/attribute text, so the diagram stays compact.
All images below are generated by the engine itself and committed under docs/ so they survive the out/ cleanup.
The rendered article page (mvn exec:java "-Dexec.args=article.html article.css 900 900"):
The box-model demo page, plain and with the debug overlay (mvn exec:java "-Dexec.args=boxmodel.html boxmodel.css 900 500 --boxmodel"):
The DOM parse-tree diagram from out/report.html (click to view at full size):
- The HTML parser assumes well-formed input; it does not validate that closing tags match.
- The named-entity table covers ~200 common entities, not the full HTML5 list; unknown references are left as-is. CDATA is kept as raw text, but there is no special handling of raw-text elements (
<script>,<style>,<textarea>) beyond what the parser does today. - No attribute selectors (
[type="text"]) or pseudo-elements (::before) yet. :not()accepts only a single compound selector (no comma lists or combinators).- Structural pseudo-classes count only element siblings (whitespace-only text nodes are already stripped by the HTML parser).
- No cascade tie-breaking beyond source order, no
!importantfor inline styles beyond what is listed above. - The layout engine handles block and inline boxes only (no floats, positioning, flex/grid); only
background-color(plus thebackgroundshorthand) andborder-*-widthare painted; inline elements get no box (their width is measured but they don't produce boxes or wrap on their own line). TextMetricsuses Java AWT font metrics, so results vary with the platform's fonts.- The SVG diagram approximates text width with a fixed per-character estimate, and edges are drawn as a single straight Bézier per parent→child pair, so very wide trees can still look dense.
The engine ships with unit tests for the core pipeline, written in JUnit 4 under src/test/java/org/example/browser/:
html/SimpleHtmlParserTest— DOM parsing, attributes, comments, entities, whitespacecss/SimpleCssParserTest— selectors, combinators, pseudo-classes,!important, specificity, shorthandsstyle/StyleResolverTest— cascade priority, inheritance, inline styleslayout/LengthTest— length parsing and resolution
Run them with:
mvn testIdeas for extending the project:
- inline element boxes (width/height on
span/aetc.), line-height, andvertical-align - floats,
position, flexbox / grid layout - attribute selectors (
[type="text"]), pseudo-elements (::before), richer:not()lists - the full HTML5 named-entity table, raw-text element handling, and parser error recovery
- multi-cascade layering (user agent < user < author < !important)
- navigation and page loading over HTTP



