poxk/Falco
Tiny browser engine written from scratch in Rust. Contribute to poxk/Falco development by creating an account on GitHub.
36 stars · 6 forks · 0 watchers
README
Falco
Logo by Sanya — t.me/SanyochekDev
A tiny, fast browser engine written in Rust. Renders HTML, CSS, JavaScript, SVG and images — to a PNG, or to a live interactive window.
Quick start · Interactive mode · Features · Architecture · Contributing
What it is Falco is a real browser engine in roughly 36,000 lines of Rust. It parses HTML, applies CSS, executes JavaScript, loads images, computes layout, and paints to a canvas — either as a PNG file or a live interactive window where you can scroll, click links, fill out forms, and navigate. HTML ──▶ DOM ──▶ Style tree ──▶ Layout tree ──▶ Paint commands ──▶ Canvas ──▶ PNG / Window ▲ ▲ ▲ │ │ │ HTML5 tokenizer CSS cascade Flex / Grid / Table / Float / Absolute + tree builder + Selectors 4 + Inline / Block flow │ JS (custom VM with closures, generators, Promise, BigInt, Symbol) │ Image loader ──▶ HTTP / data: URL / local file
Falco is not a wrapper around WebKit, Gecko, or Chromium. Every module — HTML tokenizer, CSS parser, layout engine, JS VM, font rasterizer, PNG encoder — is written from scratch in Rust. Honest status (what works vs what's a stub) To set expectations clearly — this is v0.1.0, an early release by a single developer. Not everything listed in this README is production-ready. Here's what is actually wired into the render pipeline and what is structurally complete but not yet called: ✅ Actually works (called by render_with_base_url)
HTML parser (html/) — legacy parser, not spec-compliant but functional DOM types (dom/) — minimal node types, no observers/shadow CSS parser (css/) — selectors, properties, cascade, color parsing Style cascade (style/) — UA styles + inheritance + flex/grid props Layout (layout/) — block / inline / flex / CSS Grid / table / float / absolute Painting (paint/) — fonts (ab_glyph), gradients, shadows, alpha compositing SVG renderer (svg/) — paths, basic shapes, gradients, stroke + fill Hand-written PNG encoder (png/) Image loader (image/) — HTTP, data: URLs, local files JS VM (tjs/) — bytecode interpreter + JIT (x86_64, Linux-only) JS-DOM bindings (js_tjs/, js_runner/) — document.getElementById, console.log, alert, onclick Networking (net/) — HTTP/1.1 (ureq), cookies, cache, websocket, redirect Interactive --window mode — scrolling, forms, navigation, history
⚠️ Structurally complete, passes own unit tests, NOT wired into renderer These exist as spec-compliant replacements for the legacy modules. They compile and have their own unit tests, but render_with_base_url does not call into them yet. This is the v0.2.0 milestone.
html::spec — WHATWG §13.2 tokenizer (all 80 states) + tree builder (all 22 insertion modes) + serializer + XML parser + encoding detection dom::spec — spec DOM with MutationObserver, Shadow DOM, custom elements, accessibility tree css::spec — Selectors Level 4 (:has(), :is(), :where(), cascade layers, container queries) tjs_ext/ — Symbol, BigInt, Promise, microtasks, Map/Set, WeakMap/WeakSet, Reflect
⚠️ Implemented algorithms, NOT enforced in renderer
security/ — SOP, multi-process, seccomp sandbox, CSP, TLS cert chain validation, permissions, extensions, DevTools protocol. All algorithms are in place and unit-tested, but the renderer doesn't enforce them yet. web_runtime/ — fetch(), XMLHttpRequest, event loop, Promise. The Promise / event loop integration is real and unit-tested, but fetch and XHR are stubbed (no real network behind them in the JS context). WebGL, video, MSE, EME, NDSD are headless stubs — they implement the API surface but don't actually render WebGL frames, decode H.264 video, or do DRM. They exist as scaffolding for future work. media/ — @media queries are parsed but always applied (no conditional cascade yet)
❌ Known broken / unfinished
real-http2, real-webgl, sandbox Cargo features do not compile with --all-features because their upstream APIs have drifted (h2::Body removed, glow API changed, seccomp pre_exec is Unix-only). They are disabled by default and the CI clippy step does not check them. The JIT (tjs/jit.rs) works on Linux x86_64 but fails on macOS CI runners because mmap(MAP_JIT) requires code signing with the com.apple.security.cs.allow-jit entitlement. JIT tests are marked #[ignore] on macOS. DOM mutation from JS (element.innerHTML = ..., element.style.color = ...) does not trigger re-render. HTML5 spec-compliant tree repair (adoption agency, foster parenting) is in html/spec/tree_builder.rs but the legacy parser is what actually runs.
Bottom line: if you cargo build && ./falco https://example.com --out out.png, you get a real PNG render. The HTML/CSS/layout/paint path works end-to-end. The spec-compliant parsers, security enforcement, and advanced web runtime (WebGL/video/DRM) are scaffolding for future milestones, not working features. Please read the code before claiming otherwise. Quick start
Build (release binary lands in target/release/falco)
cargo build --releaseRender a local HTML file to PNG
./target/release/falco page.html --out page.png --width 1200Render a URL to PNG
./target/release/falco https://example.com --out example.png --width 800Open interactive live window (desktop only — scrolls, hover, link clicks)
./target/release/falco page.html --windowRender with external CSS merged on top of <style> tags
./target/release/falco README.html --css style.css --out readme.png <style> tags inside the HTML are automatically extracted and applied. External CSS via --css is merged on top. Interactive mode (--window) When you pass --window, Falco opens a real browser window with an address bar at the top, the page content in the middle, and a status bar at the bottom. You can: NavigationClick links (<a href>) — Falco fetches the new URL and re-renders Address bar — click it or press F6, type a URL, press Enter Reload with r — re-fetches the current page Back/Forward with Alt+← / Alt+→ — full history navigation
Forms
Click inputs to focus them, then type to enter text (live update) Tab / Shift+Tab — cycle focus between interactive elements Backspace — delete the last character Enter — submit form / click the focused button / follow focused link Supported types: text, email, password (masked), checkbox, submit, button, textarea
Scrolling & visual feedback
Mouse wheel, arrow keys, Page Up/Down, Home/End Focus ring — 2px blue outline around the focused element Blinking caret — 500ms blink in text inputs and the address bar Hover hint — when hovering a link, its href appears in the status bar
Incremental repaint The window only re-paints when state actually changes (scroll, input text, focus, hover). On idle frames, only the address bar / status bar overlays are redrawn — the page canvas is cached. This keeps CPU usage low and scrolling smooth. Headless fallback If no display is available (headless server, no X11/Wayland), Falco automatically falls back to PNG output with a warning. CLI reference falco — a tiny browser engine
USAGE falco <input> [OPTIONS]
INPUT A URL (http://...) or a path to a local .html file.
OPTIONS --css <path> External CSS file (merged with <style> tags in HTML). --out <path> Output PNG path. Default: falco.png --width <px> Viewport width. Default: 1200 --height <px> Viewport height (canvas grows if content is taller). Default: 800 --bg <hex> Background color (0xRRGGBBAA). Default: 0xFFFFFFFF --window Open a live interactive window instead of writing PNG. -h, --help Show this help -V, --version Print version
INTERACTIVE MODE KEYS (when --window is used) q / Esc quit r reload (prints message — restart Falco to actually reload) ↑ / ↓ scroll line PgUp / PgDn scroll page Home / End jump to top / bottom g / G top / bottom (vim-style) mouse wheel scroll mouse click follow `<a href>` link (prints URL to stderr)
Programmatic API use falco::{render_to_png, RenderOptions};
fn main() -> anyhow::Result<()> { let html = std::fs::read_to_string("page.html")?; let css = std::fs::read_to_string("style.css")?; let opts = RenderOptions { width: 1200, height: 800, background: 0xFFFFFFFF, }; render_to_png(&html, &css, opts, "out.png")?; Ok(()) } What it supports HTML
HTML5 tokenizer (WHATWG §13.2.5) — all 80 states, script-data escape/double-escape, attribute parsing with duplicate detection, named + numeric character references with Windows-1252 quirks HTML5 tree builder (WHATWG §13.2.6) — all 22 insertion modes, stack of open elements with scope algorithms, active formatting elements list, reconstruct active formatting, adoption agency algorithm, foster parenting for table content, <template> with separate DocumentFragment contents Entity references — &, A, ©, 50+ named entities Whitespace collapsing (browser-style) <template> element with separate DocumentFragment contents XML/XHTML parser — strict, with namespace bindings, CDATA, PIs Encoding detection — BOM, HTTP Content-Type charset, <meta charset>, <meta http-equiv>, heuristic UTF-8/UTF-16 detection, decoders for UTF-8 / UTF-16LE / UTF-16BE / Windows-1252 Tree builder auto-close — <li>, <p>, <td>, <tr>, <option>, <dt>/<dd> innerHTML / outerHTML serialization — void elements, <template> contents fragment, raw text elements, full attribute value escaping
DOM (spec-compliant, dom::spec)
NodeRef = Rc<RefCell<Node>> with parent, firstChild, lastChild, previousSibling, nextSibling pointers per spec DocumentHandle = Rc<RefCell<Document>> with weak back-ref from each Node Mutation records queued on every append/insert/remove/setAttribute/removeAttribute MutationObserver with observe(), disconnect(), take_records(), MutationObserverInit (childList/attributes/characterData/subtree/attributeOldValue/ characterDataOldValue/attributeFilter), subtree ancestor matching Shadow DOM — attachShadow() with open/closed modes, host validation, named + default slots, fallback content, slot distribution (flatten tree algorithm), assignedSlot lookup Custom elements — customElements.define() with name validation, observedAttributes tracking, lifecycle callbacks (connected / disconnected / adopted / attributeChanged / form-associated), pending upgrades, customized built-in elements (is="...") Accessibility tree — parallel tree with role/name/description/ state/actions, implicit ARIA roles for ~50 HTML tags, honors role="", aria-hidden, hidden, display:none, accessible name computation (aria-label > aria-labelledby > element-specific > title)
CSS (css/ + css::spec)
Selectors Level 4 — type, class, id, universal (*), descendant, child (>), adjacent sibling (+), general sibling (~), attribute ([attr], =, ~=, |=, ^=, $=, *=) Pseudo-classes — :hover, :focus, :focus-visible, :focus-within, :active, :visited, :checked, :disabled, :enabled, :readonly, :readwrite, :required, :optional, :valid, :invalid, :empty, :root, :first-child, :last-child, :only-child, :first-of-type, :last-of-type, :only-of-type, :nth-child(an+b), :nth-last-child, :nth-of-type, :nth-last-of-type, :nth-child(an+b of S), :is(), :where(), :not(), :has(), :lang(), :dir() Cascade & specificity — (a, b, c) tuple, :where() zero, :is() / :not() / :has() most specific arg, CascadeOrigin (UA / User / Author) with reversed order for !important, CascadeLayers (None wins over layered; later wins over earlier) Properties — display, position, color, background (including linear-gradient and radial-gradient), font-*, margin, padding, border, border-radius, width, height, min/max-width, top/right/bottom/left, z-index, overflow, opacity, box-shadow, white-space, box-sizing, gap, flex*, grid*, writing-mode, logical properties (margin-inline-start, etc.) Values — keywords, hex/rgb/rgba/named colors, lengths (px, em, rem, pt, %, vw, vh), percentages, numbers, !important Functions — linear-gradient(), radial-gradient(), url(), var(), calc() (simplified), rgb(), rgba() Shorthands — margin, padding, border, background, flex @-rules — @media (parsed and applied), @keyframes / @animation with cubic-bezier & steps timing functions, @font-face with family/weight lookup, @layer (cascade layers), @container queries with evaluate_container_query() Logical properties — margin-inline-start etc. resolved to physical properties based on writing-mode CSS counters — counter-reset, counter-increment, counter-set Containment — contain: layout/paint/size/style/inline-size/ block-size, strict, content Filters — blur, brightness, contrast, drop-shadow, grayscale, hue-rotate, invert, opacity, saturate, sepia Clip-path — polygon, circle, ellipse, inset, path, url()
Layout (layout/)
Block flow — vertical stacking Inline flow — horizontal text wrapping with proper baseline Flexbox — flex-direction, justify-content, align-items, flex-wrap, gap, flex-grow, flex-shrink, flex-basis CSS Grid — grid-template-columns / grid-template-rows (with fr, auto, minmax(), repeat()), grid-column / grid-row placement, gap / column-gap / row-gap, auto-flow Table layout — <table>, <tr>, <td>, <th>, <thead>, <tbody>, <tfoot>, <caption>, column width distribution, border collapse Float — float: left/right, simple clear Inline-block — inline elements with block-like width/height Box model — margin, border, padding, content with box-sizing: border-box support Position — static, relative, absolute, fixed (parsed, relative + absolute positioning applied) Units — px, em, rem, pt, %, vw, vh Writing modes — horizontal-tb, vertical-rl/lr, sideways-rl/lr
Painting (paint/)
Backgrounds — solid colors and linear / radial gradients Borders — all four sides with custom colors and styles Border-radius — rounded corners (all four corners) Box-shadow — outer shadows with blur Opacity — alpha blending for entire elements Text — TrueType font rasterization via ab_glyph, bold and italic synthesis Alpha compositing — proper RGBA blending SVG — paths, basic shapes (rect, circle, ellipse, line, polyline, polygon), gradients, stroke + fill PNG encoder — hand-written, no flate2 dependency
JavaScript (tjs/ + tjs_ext/) Falco ships its own JavaScript VM (the tjs module) — pure Rust, no V8/SpiderMonkey/boa. It is a bytecode VM with a generational GC, inline caching, hidden classes, and a JIT tier-up.
ES2015+ syntax — let / const, arrow functions, template literals, destructuring (object + array), default + rest params, spread, for...of, for...in, computed property names, shorthand methods/properties, optional chaining, nullish coalescing, exponentiation operator, async/await (parsed) Functions — function foo() {}, closures with proper upvalue capture, generators (function* / yield / yield*), async function / await (parser-level) Types — Symbol with 13 well-known symbols, BigInt with arbitrary precision (u32 limbs, signed), Promise with then/catch/finally + state machine, Iterator protocol, Generator as state machine Built-ins — Math, JSON, Array (push, pop, map, filter, reduce, forEach, find, findIndex, includes, slice, splice, flat, flatMap), String (split, replace, match, padStart, padEnd, trim, trimStart, trimEnd, startsWith, endsWith, includes, repeat), Object (keys, values, entries, assign, freeze, fromEntries), Reflect (get, set, has, deleteProperty, ownKeys), WeakMap, WeakSet, Map, Set, Proxy Microtask queue — Promise reactions drained as microtasks DOM bindings — document.getElementById, document.querySelector / querySelectorAll, console.log, alert, addEventListener (basic) Event loop integration — setTimeout, setInterval, requestAnimationFrame, fetch() (returns Promise<Response>), XMLHttpRequest, all driven by the event loop in web_runtime/event_loop.rs console.log(...) — prints to stderr alert(msg) — shows in the status bar
Networking (net/)
HTTP/1.1 fetch (via ureq) HTTP/2 parser (http2.rs) Cookie jar (cookies.rs) with proper domain/path matching Redirect handling (redirect.rs) with redirect-loop detection Cache (cache.rs) — HTTP cache with conditional requests WebSocket (websocket.rs) — frame parser, masking, ping/pong
Web runtime (web_runtime/)
fetch() (fetch.rs) — Promise-based, integrates with event loop XMLHttpRequest (xhr.rs) — sync + async modes Event loop (event_loop.rs) — task queues, microtasks, RAF Promise (promise.rs) — state machine, then/catch/finally WebGL (webgl.rs) — shader compilation, buffer management, draw calls (headless) Video (video.rs) — <video> element demux + decode stub MSE (mse.rs) — Media Source Extensions EME (eme.rs) — Encrypted Media Extensions NDSD (ndsd.rs) — Native Device Service Discovery
Security (security/) — implemented but not fully wired into renderer
Origin / SOP (origin.rs) — Origin struct, is_same_origin, is_same_site, registrable_domain (with 2-part TLD list), check_cors (with credentials / wildcard handling), check_navigation Multi-process / site isolation (process.rs) — Process kinds (Browser / Renderer / GPU / Utility / Plugin), site-to-process map, ProcessPerSite / ProcessPerTab policies, crash recovery with max-restarts / sad-tab / fatal modes Sandbox (sandbox.rs) — seccomp-bpf filter (Linux), renderer allowlist (~30 syscalls), blocks execve/fork/ptrace/open/ socket/connect/mount, PR_SET_NO_NEW_PRIVS, drop_capabilities CSP (csp.rs) — directive map, default-src fallback, source expression matching ('self', 'none', 'unsafe-inline', 'unsafe-eval', data:, blob:, host, *.wildcard, scheme:), nonce/hash support, allows_inline_script, allows_eval, allows_javascript_url, is_safe_attribute (blocks onclick, onerror, javascript: in href/src), violation reports TLS certificates (cert.rs) — Certificate struct, validity, hostname matching (with wildcards), TrustStore with Mozilla defaults, validate_chain (chain building, signature check, hostname, EKU, path length), OCSP stub, HPKP pinning, Certificate Transparency Permissions (permissions.rs) — 20 permission types (Geolocation, Camera, Microphone, Notifications, ...), per-(origin, permission) state, pluggable prompt handler, iframe allow parsing Extensions (extensions.rs) — Manifest V3, content scripts, match patterns (<all_urls>, ://.host/*), glob matching, permissions, generate extension ID, ChromeApi enum DevTools protocol (devtools.rs) — JSON value type, Request/Response/Event/RpcError, Inspector/Page/Runtime/DOM/Network/ Console methods, event subscribers, console message buffering
Images (image/)
HTTP/HTTPS URLs — fetched via ureq data: URLs — base64-encoded inline images Local files — relative paths resolved against the page URL Formats — PNG, JPEG, GIF (first frame), BMP (via the image crate) Sizing — width / height HTML attributes take precedence, CSS width / height respected, default 300×200px, nearest-neighbor scaling Broken images — grey placeholder box with the alt text Caching — global cache by URL
What it does NOT do (yet)
The html::spec, dom::spec, css::spec, tjs_ext/, security/ modules are structurally complete but not yet wired into the render pipeline. Falco still uses the legacy html / dom / css modules for actual rendering. The new modules exist as the spec-compliant replacements and pass their own unit tests, but the renderer has not been switched over yet. No CSS animations / transitions in the renderer (the data structures exist in css/spec/advanced.rs, but the paint loop does not interpolate them). No @media query value matching in the renderer (queries are parsed; the cascade does not yet apply them conditionally). No real DOM mutation from JS in the renderer (element.innerHTML = ..., element.style.color = ... are stubs). No HTML5 spec-compliant tree repair in the renderer (the algorithm exists in html/spec/tree_builder.rs but the legacy parser is used).
Architecture
Module Lines Description
html::spec ~4,900 WHATWG HTML5 tokenizer + tree builder + serializer + XML parser + encoding
html.rs ~540 Legacy HTML parser (still used