Skip to content

Latest commit

 

History

History
1157 lines (969 loc) · 55.8 KB

File metadata and controls

1157 lines (969 loc) · 55.8 KB

WebUI Framework Technical Specification

Overview

WebUI Framework is a high-performance server-side rendering framework that operates without JavaScript runtimes. It separates static and dynamic content at build time, creating an efficient protocol that enables fast server-side rendering in any language (Rust, Go, C#, PHP, Ruby, etc.). On the client, Web Components hydrate as interactive islands — only components that need interactivity ship JavaScript.

Core Architecture

The framework consists of four primary modules:

  • Protocol: Defines the structural representation of UI components
  • Parser: Processes HTML/CSS templates into protocol structures
  • Expression: Evaluation*: Handles conditional logic evaluation
  • Handler: Renders protocol with state data into final HTML output

Performance Principles

  • No recursion (all algorithms must be iterative)
  • No regular expressions
  • Minimal runtime computation
  • Protocol Buffers for compact binary serialization
  • Buffer consolidation for reduced allocations
  • Strict context isolation during processing
  • Proactive error handling with actionable messages

Protocol Specification (webui-protocol)

The protocol defines the serializable structure representing UI templates. At runtime, the protocol uses protobuf for efficient binary serialization. Types are generated directly from proto/webui.proto using prost — there is no separate domain type layer.

Data Types

/// The root protocol structure representing a complete webpage configuration.
/// Generated from protobuf `message WebUIProtocol`.
pub struct WebUIProtocol {
    /// Map of fragment identifiers to their associated fragment lists.
    pub fragments: HashMap<String, FragmentList>,
    /// Sorted, deduplicated CSS custom property names used across all
    /// components and entry page styles (without `--` prefix).
    pub tokens: Vec<String>,
    /// Per-component data keyed by tag name (client template + CSS).
    pub components: HashMap<String, ComponentData>,
}

/// Per-component metadata populated by the active parser plugin at build time.
/// Framework-neutral: each plugin populates the fields it needs.
/// Generated from protobuf `message ComponentData`.
pub struct ComponentData {
    /// Client-side template string for hydration.
    /// Populated by the active parser plugin (e.g., f-template HTML for FAST,
    /// compiled template JS for WebUI Framework).
    pub template: String,
    /// Component CSS content for the Module strategy.
    pub css: String,
    /// External stylesheet href for the Link CSS strategy (e.g. "/my-card.css").
    /// Empty for Style/Module strategies and for components without CSS.
    /// The handler emits a `<link>` tag in `<head>` only when this is non-empty.
    pub css_href: String,
}

/// A list of fragments (needed because protobuf maps cannot have repeated values directly).
pub struct FragmentList {
    pub fragments: Vec<WebUIFragment>,
}

/// A mapping of unique fragment identifiers to their corresponding fragment lists.
pub type WebUIFragmentRecords = HashMap<String, FragmentList>;

/// A single fragment — one of several types.
/// Generated from protobuf `message WebUIFragment` with a `oneof fragment` field.
pub struct WebUIFragment {
    pub fragment: Option<web_ui_fragment::Fragment>,
}

/// The fragment oneof variants.
pub enum Fragment {
    Raw(WebUIFragmentRaw),
    Component(WebUIFragmentComponent),
    ForLoop(WebUIFragmentFor),
    Signal(WebUIFragmentSignal),
    IfCond(WebUIFragmentIf),
    Attribute(WebUIFragmentAttribute),
    Plugin(WebUIFragmentPlugin),
    Route(WebUIFragmentRoute),
    Outlet(WebUIFragmentOutlet),
}

Fragment Types

Raw Fragment

pub struct WebUIFragmentRaw {
    /// The content to render.
    pub value: String,
}

Component Fragment

pub struct WebUIFragmentComponent {
    /// The identifier for the associated fragment record.
    pub fragment_id: String,
}

For Loop Fragment

pub struct WebUIFragmentFor {
    /// The name representing a singular item (e.g., "person").
    pub item: String,
    /// The collection name (e.g., "people").
    pub collection: String,
    /// The identifier for the fragment to render for each item.
    pub fragment_id: String,
}

Signal Fragment

pub struct WebUIFragmentSignal {
    /// The value or identifier of the signal.
    pub value: String,
    /// Determines if the value should be rendered as raw content.
    pub raw: bool,
}

Conditional Fragment

pub struct WebUIFragmentIf {
    /// The condition expression to evaluate.
    pub condition: Option<ConditionExpr>,
    /// The identifier for the fragment record to render if true.
    pub fragment_id: String,
}

Attribute Fragment

Attribute fragments represent dynamic HTML attributes with various binding types:

pub struct WebUIFragmentAttribute {
    /// The attribute name (may include `:` prefix for complex attributes).
    pub name: String,
    /// For simple dynamic attributes, the signal name.
    pub value: String,
    /// For mixed (template) attributes, the sub-stream fragment ID.
    pub template: String,
    /// True for `:`-prefixed complex attributes.
    pub complex: bool,
    /// True for the first dynamic attribute on a component element.
    pub attr_start: bool,
    /// True for skipped attributes (class, style, role, data-*, aria-*).
    pub attr_skip: bool,
    /// True for static attribute values on components.
    pub raw_value: bool,
    /// For `?`-prefixed boolean attributes, the condition tree.
    pub condition_tree: Option<ConditionExpr>,
}

Plugin Fragment

Plugin fragments carry opaque data from parser plugins to handler plugins. WebUI does not interpret this data — each parser/handler plugin pair defines its own binary contract.

pub struct WebUIFragmentPlugin {
    /// Opaque plugin-specific binary data.
    pub data: Vec<u8>,
}

Route Fragment

Route fragments define declarative URL-based routes linking path templates to fragment bodies. The parser emits these from <route> elements; the handler uses them for server-side route matching.

pub struct WebUIFragmentRoute {
    pub path: String,                          // URL path template (e.g., "sections/:id")
    pub fragment_id: String,                   // Fragment containing the route body
    pub exact: bool,                           // Require exact path match
    pub children: Vec<WebUIFragmentRoute>,     // Nested child routes
}

There is no global route registry or route tree — routes are inline in the fragment graph via WebUIFragmentRoute nesting.

Outlet Fragment

Outlet fragments mark where matched child route content renders inside a parent route component. The parser emits these from <outlet /> elements.

pub struct WebUIFragmentOutlet {}

Components use <outlet /> in their templates to declare insertion points:

<template shadowrootmode="open">
  <h1>Title</h1>
  <main><outlet /></main>
</template>

Route declaration: Routes are declared as nested <route> elements in the entry HTML. Child paths are relative to their parent (no leading /). The HTML nesting IS the route tree:

<route path="/" component="app-shell">
  <route path="sections/:sectionId" component="section-page">
    <route path="topics/:topicId" component="topic-page">
      <route path="lessons/:lessonId" component="lesson-page" exact />
    </route>
  </route>
</route>

Route matching: The handler uses an iterative path template matcher (no regex). Segments are compared left-to-right: :param binds a value, *splat captures remaining segments, ? marks optional parameters. Exact matches (most literal segments) take precedence over parameterized ones.

Server-side rendering: When the handler encounters Fragment::Route:

  1. Pre-scan siblings, pick the best match by specificity.
  2. Matched route: emit <webui-route path="..." component="..." active>, render component, recurse into children.
  3. Non-matched routes: emit <webui-route ... style="display:none">.

For the WebUI framework path, matched route components do not receive route state as scalar attributes or data-state. Initial SSR state comes from the rendered DOM plus hydration markers, and client-side navigations apply fresh state through the partial-response setInitialState(...) path.

When the handler encounters Fragment::Outlet:

  1. Take children from the currently active route.
  2. Match children against the request path (relative to route base).
  3. Emit <webui-outlet> containing matched child <webui-route> with component, and hidden stubs for siblings.

The handler also emits <meta name="webui-inventory"> in <head> with the initial component inventory bitmask, so the client router can tell the server which templates it already has on the first client-side navigation.

Key elements:

  • <webui-route> — light DOM custom element, structural routing wrapper with no shadow DOM
  • <webui-outlet> — light DOM custom element, marks insertion point for child route content

Client-side navigation:

  1. On initial load, the router builds the active chain from SSR'd <webui-route active> elements.
  2. On navigation, fetches a JSON partial (Accept: application/json) from the server.
  3. The server returns the matched route chain — the client does NOT perform route matching.
  4. Reconciles old vs new chain — finds first changed level.
  5. Mounts components at changed levels, creates <webui-route> stubs at outlet positions.
  6. Parent components and their state are preserved.

Partial response: The server returns { state, templates, inventory, path, chain }:

  • state: route params from all nesting levels injected into API state
  • templates: client template strings the client doesn't already have (filtered by inventory bitmask)
  • inventory: updated hex bitmask of loaded templates
  • chain: matched route chain array — each entry has component, path, optional params and exact

The chain field is produced by render_partial() in the handler, which walks the fragment graph and matches routes at each nesting level. Host servers call this once per partial response. Available via FFI as webui_render_partial() for C/.NET/Node hosts.

Partial-template selection: During client navigation, servers derive template names from the normal render fragment graph starting at the persistent entry fragment. The traversal is route-aware but state-agnostic:

  • follow component, if, for, and attribute-template edges conservatively without evaluating request-time state
  • when a fragment list contains sibling <route> fragments, follow only the single best match for the current request path using the same specificity rules as SSR
  • recurse through nested matched route groups so the active route chain is included
  • skip unvisited sibling route branches entirely; later navigations will request those templates if needed
  • filter the discovered component set against the client's inventory bitmask before returning templates

This intentionally over-ships inactive conditional and loop-driven templates inside the active route chain rather than trying to mirror a transient server-side state snapshot.

Attribute types:

  • Simple dynamic: href="{{url}}"{ name: "href", value: "url" }
  • Boolean (? prefix): ?disabled={{isDisabled}}{ name: "disabled", condition_tree: identifier("isDisabled") } — rendered only if condition is truthy; silently dropped if value is not a pure handlebars expression.
  • Pass-through / property (: prefix): :config="{{settings}}" or :value="{{searchQuery}}"{ name: ":config", value: "settings", complex: true } — reserved for direct pass-through/property bindings, including live form-control values.
  • Mixed/template: value="hello {{world}}"{ name: "value", template: "attr-1" } with sub-stream attr-1: [raw("hello "), signal("world")]

Condition Expressions

Condition expressions are protobuf messages with a oneof expr field:

/// A condition expression tree (protobuf message with oneof).
pub struct ConditionExpr {
    pub expr: Option<condition_expr::Expr>,
}

pub enum Expr {
    Predicate(Predicate),
    Not(Box<NotCondition>),
    Compound(Box<CompoundCondition>),
    Identifier(IdentifierCondition),
}

pub struct NotCondition {
    pub condition: Option<Box<ConditionExpr>>,
}

pub struct CompoundCondition {
    pub left: Option<Box<ConditionExpr>>,
    pub op: i32, // LogicalOperator enum value
    pub right: Option<Box<ConditionExpr>>,
}

pub struct IdentifierCondition {
    pub value: String,
}

Operators

/// Logical operators for compound conditions (protobuf enum, i32 repr).
pub enum LogicalOperator {
    Unspecified = 0,
    And = 1,
    Or = 2,
}

/// Comparison operators for predicates (protobuf enum, i32 repr).
pub enum ComparisonOperator {
    Unspecified = 0,
    GreaterThan = 1,     // >
    LessThan = 2,        // <
    Equal = 3,           // ==
    NotEqual = 4,        // !=
    GreaterThanOrEqual = 5, // >=
    LessThanOrEqual = 6,   // <=
}

Predicates

pub struct Predicate {
    /// The left-hand side value.
    pub left: String,
    /// The operator used in comparison (ComparisonOperator as i32).
    pub operator: i32,
    /// The right-hand side value.
    pub right: String,
}

Serialization Requirements

  • Protobuf binary serialization/deserialization as the primary format, using prost for direct encode/decode with no conversion layer
  • Types are generated from proto/webui.proto at build time via prost-build
  • JSON output supported via webui inspect for debugging only (using serde derives on generated types)
  • Support for custom error types and propagation
  • Validation of protocol structure during deserialization
  • Performance optimizations for large protocol structures
  • Support for fragment reference validation
  • Attribute names starting with '?' are treated as boolean attributes using the Attribute fragment type with a condition_tree. The attribute is rendered only if the condition evaluates to true.

State Management (webui-state)

Path Resolution

The find_value_by_dotted_path function provides a high-performance way to query JSON state:

pub fn find_value_by_dotted_path(path: &str, state: &Value) -> Result<Value, StateError>

Requirements

  • Dot notation support (e.g., user.profile.name)
  • Array indexing support (e.g., users.0.name)
  • Special length property support for arrays (e.g., users.length)
  • Nullable path handling
  • Missing path error reporting

Expression Evaluation (webui-expressions)

Core Function

pub fn evaluate(condition: &ConditionExpr, state: &Value) -> Result<bool, ExpressionError>

Evaluation Requirements

  • No recursion: All evaluation must be iterative
  • No parentheses: Expression grouping is handled by the ConditionExpr structure
  • Logical operators: Support for && (AND) and || (OR) only
  • Comparison operators: Support for >, <, ==, !=, >=, <= only
  • Negation: Support for ! operator
  • No mixed operators: Cannot mix AND and OR in the same expression level
  • Operator limit: Maximum of 5 logical operators per expression
  • Error handling: Clear, actionable error messages for invalid expressions

Error Types

pub enum ExpressionError {
    MixedOperators,
    TooManyOperators(usize),
    ValueNotFound(String),
    TypeMismatch { expected: String, found: String },
    InvalidComparison(String),
    // Other error types...
}

Handler Implementation (webui-handler)

Core API

pub struct WebUIHandler {
    plugin: Option<Box<dyn HandlerPlugin>>,
}

/// Options controlling how the handler renders a protocol.
pub struct RenderOptions<'a> {
    /// The fragment ID to start rendering from (e.g., `"index.html"`).
    pub entry_id: &'a str,
    /// The URL path to match routes against (e.g., `"/contacts/42"`).
    pub request_path: &'a str,
}

impl<'a> RenderOptions<'a> {
    pub fn new(entry_id: &'a str, request_path: &'a str) -> Self;
}

impl WebUIHandler {
    pub fn new() -> Self;
    pub fn with_plugin(plugin: Box<dyn HandlerPlugin>) -> Self;

    pub fn handle(
        &mut self,
        protocol: &WebUIProtocol,
        state: &Value,
        options: &RenderOptions<'_>,
        writer: &mut dyn ResponseWriter,
    ) -> Result<()>;
}

Route-aware rendering: The handler performs server-side route matching during rendering. When processing Fragment::Route, the handler matches the route's path template against options.request_path:

  • Matched route: rendered visible (active attribute) with component content.
  • Non-matched routes: rendered hidden (style="display:none") and empty.

When processing Fragment::Outlet, the handler takes children from the active route, matches them against the request path relative to the current route base, and emits <webui-outlet> containing the matched child and hidden stubs for siblings.

This eliminates the need for post-render HTML pruning — the handler produces correct route output in a single pass.

Writer Interface

pub trait ResponseWriter {
    /// Write content to the output
    fn write(&mut self, content: &str) -> Result<()>;
    /// Finalize the output
    fn end(&mut self) -> Result<()>;
}

Handler Plugin System

The handler supports framework-specific hydration plugins. Plugins receive lifecycle callbacks during rendering and write marker formats for their framework, while shared completion work such as rendered-component template emission stays in handler core.

pub trait HandlerPlugin {
    fn push_scope(&mut self);
    fn pop_scope(&mut self);
    fn on_binding_start(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>;
    fn on_binding_end(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>;
    fn on_repeat_item_start(&mut self, index: usize, writer: &mut dyn ResponseWriter) -> Result<()>;
    fn on_repeat_item_end(&mut self, index: usize, writer: &mut dyn ResponseWriter) -> Result<()>;
    fn on_element_data(&mut self, data: &[u8], writer: &mut dyn ResponseWriter) -> Result<()>;
    fn write_route_component_state(
        &self,
        state: &serde_json::Value,
        writer: &mut dyn ResponseWriter,
    ) -> Result<()>;
}

Hook invocation points:

  • Signal: on_binding_start before, on_binding_end after (same scope)
  • For loop: on_binding_start/end around entire loop; on_repeat_item_start/end + push_scope/pop_scope per item
  • If condition: on_binding_start/end around condition; push_scope/pop_scope if condition is true
  • Component: push_scope/pop_scope around component body
  • Plugin fragment: on_element_data with parser-produced hydration bytes from protocol
  • Matched route component: write_route_component_state before the opening tag closes

Built-in plugin: FastHydrationPlugin Injects FAST-HTML hydration comment markers for client-side re-hydration:

  • Binding: <!--fe-b$$start$$INDEX$$NAME$$fe-b--> / <!--fe-b$$end$$INDEX$$NAME$$fe-b-->
  • Repeat: <!--fe-repeat$$start$$INDEX$$fe-repeat--> / <!--fe-repeat$$end$$INDEX$$fe-repeat-->
  • Attribute (single): data-fe-b-INDEX
  • Attribute (multi): data-fe-c-INDEX-COUNT

FAST template authoring and runtime usage are documented in the canonical FAST HTML README. DESIGN.md only specifies the parser/handler integration contracts.

Built-in plugin: WebUIHydrationPlugin Injects WebUI Framework SSR hydration markers and attributes:

  • Binding: <!--w-b:start:INDEX:NAME--> / <!--w-b:end:INDEX:NAME-->
  • Repeat: <!--w-r:start:INDEX--> / <!--w-r:end:INDEX-->
  • Attribute (single): data-w-b-INDEX
  • Attribute (multi): data-w-c-INDEX-COUNT
  • Event: data-ev="COUNT" once per element with event handlers

Markers are emitted only in active child scopes; the root page scope stays marker-free. During hydration the runtime walks data-ev elements in DOM order, consumes COUNT consecutive entries from metadata e[], then installs delegated shadow-root listeners using runtime data-eh-* attributes on the target elements. See WebUI Framework Plugin for the protocol details, and packages/webui-framework/README.md for the public framework API and authoring model.

Usage:

// FAST plugin
let handler = WebUIHandler::with_plugin(|| Box::new(FastHydrationPlugin::new()));
// WebUI Framework plugin
let handler = WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new()));
handler.handle(&protocol, &state, &options, &mut writer)?;

Fragment Processing

  • Raw fragments: Write value directly to output
  • Signal fragments:
    • Resolve value from state using find_value_by_dotted_path
    • Escape value if raw is false, otherwise write as-is
  • Attribute fragments:
    • Boolean (with condition_tree): Evaluate condition; if truthy, render attribute name only. If false, omit entirely.
    • Simple dynamic (with value): Resolve signal from state, render as name="resolved_value".
    • Template (with template): Render name=", process referenced sub-stream, render closing ".
    • Pass-through / property (with complex: true): Same as simple dynamic on the SSR output, but reserved for : prefixed direct pass-through/property bindings.
  • If fragments:
    • Evaluate condition using evaluate
    • If true, process referenced fragment
    • Track false conditions for template generation
    • When the If fragments are enclosed in one or more For fragments it can access the states of those For fragments' current item thorugh their corresponding item monikers. It can also access global state.
    • If fragment conditions can have tokens from different state objects i.e. local states from enclosing For fragment items and/or the global state mixed in the condition expression.
  • For fragments:
    • Iterate over collection from state
    • Process referenced fragment for each item with current item's state accessible thorugh a moniker and the global state as a fallback.
  • Component fragments: Process referenced fragment directly. Component fragments enclosed in a For fragment has access to the fields of the current item being looped over and the global state. The Component fragment doesn't need to use the For fragment item moniker and can access the fields without the qualification. If the Component fragment is nested in multiple For fragments only the closest enclosing For fragment item's state is accessible to it.
  • Plugin fragments: Pass opaque data bytes to the handler plugin's on_element_data hook. Skipped silently when no plugin is configured.

State Management

  • Global state refers to the global application state that is available to all fragments at all times.
  • Local state refers to the state corresponding to the current item being looped over in a For fragment.
  • When nested For fragments are present local state of the current item being looped over for any of the For fragment in the hierarchy can be accessed through the corresponding item moniker with an exception for Component fragments.
  • For Component fragments only the closest enclosing For fragment's current item state is available and can be accessed directly without the item moniker qualification. Component fragments also have access to the global application state.

Error Handling

  • Report missing fragment references
  • Handle state resolution failures
  • Propagate writer errors
  • Validate protocol before processing
  • Maximum recursion depth protection

Parser Modules (webui-parser)

Component Registry

pub struct ComponentRegistry {
    components: HashMap<String, Component>,
}

pub struct Component {
    pub name: String,
    pub html_content: String,
    pub css_content: Option<String>,
    /// Sorted, deduplicated CSS token names extracted from css_content.
    pub css_tokens: Vec<String>,
}

Registration Methods

pub fn register_component(&mut self, name: &str, html: &str, css: Option<&str>) -> Result<(), ParserError>
pub fn register_directory(&mut self, directory: &Path) -> Result<(), ParserError>
pub fn contains(&self, name: &str) -> bool
pub fn get(&self, name: &str) -> Option<&Component>

Requirements

  • Validate component names (must contain hyphen)
  • Lazy loading of component content
  • Directory scanning with file matching
  • Cache optimization for repeated lookups

External Component Discovery (webui-discovery)

The webui-discovery crate discovers components from external sources. It has no dependency on webui-parser — it returns plain data structs that callers register into their component registry. This makes it reusable by CLI, FFI, and other host integrations.

Source Classification

enum ComponentSource {
    /// npm package: starts with `@` (scoped) or is a bare identifier
    NpmPackage(String),
    /// Local filesystem path: starts with `.`, `/`, `\`, or drive letter
    Path(PathBuf),
}

Public API

/// Discover components from a single source.
pub fn discover_source(source: &str, search_dir: &Path) -> Result<DiscoveryResult>

/// Collect resolved local paths for file watching.
pub fn collect_watch_paths(sources: &[String], search_dir: &Path) -> Vec<PathBuf>

/// A discovered component (tag name, HTML template, optional CSS).
pub struct DiscoveredComponent {
    pub tag_name: String,
    pub html_content: String,
    pub css_content: Option<String>,
    pub source: String,
}

npm Package Resolution

  1. Walk up from the search directory to find node_modules/ (Node.js-style resolution)
  2. For scoped packages (@scope), enumerate all sub-directories
  3. For each package, read package.json:
    • exports["./template-webui.html"] → template HTML path
    • exports["./styles.css"] → styles CSS path (optional)
    • customElements → path to Custom Elements Manifest
  4. Parse the Custom Elements Manifest for modules[].declarations[].tagName
  5. Return DiscoveredComponent structs (callers handle registration)

Conditional exports are resolved with deterministic priority: defaultimportrequire.

Security

  • Path traversal: Export paths are validated — absolute paths and .. components are rejected
  • Symlink escape: Resolved package paths must remain within node_modules/ after fs::canonicalize()
  • File size limits: Manifests and templates are capped at 10 MB to prevent denial-of-service

Discovery Cache

  • Location: ~/.webui/cache/components/
  • Cache key: hash of source identifier + resolved path
  • Invalidation: hash of package.json content (re-discover on change)
  • Atomic writes: temp file + rename to prevent corruption from concurrent builds
  • Corrupt cache files are silently ignored (graceful fallback)

Local Path Resolution

Local paths perform a recursive WalkDir scan for HTML files with hyphenated names, pairing matching CSS files — the same convention used by the parser's ComponentRegistry.

HTML Parser

pub struct HtmlParser {
    component_registry: ComponentRegistry,
    css_parser: CssParser,
    condition_parser: ConditionParser,
    handlebars_parser: HandlebarsParser,
    css_strategy: CssStrategy,
    // Other fields...
}

CSS Strategy

/// Strategy for how component CSS is delivered in rendered output.
pub enum CssStrategy {
    /// Emit `<link rel="stylesheet" href="./component.css">` tags for
    /// components that actually have discovered CSS (default).
    Link,
    /// Embed CSS content inline in `<style>` tags within the shadow DOM template.
    Style,
    /// Emit a `<style type="module" specifier="component">` definition once per
    /// page and reference it via `shadowrootadoptedstylesheets` on each shadow
    /// root `<template>`.
    Module,
}
  • Link (default): Emits <link> tags referencing external .css files only for components whose discovery/registration data included CSS. Used by the CLI for production builds where CSS files are served separately.
  • Style: Embeds the full CSS content in <style> tags inside the shadow DOM template. Used when all files are needed in-memory.
  • Module: Uses the Declarative CSS Module Scripts proposal. Emits a <style type="module" specifier="component-name"> definition in each component's light DOM (once per component) and adds shadowrootadoptedstylesheets="component-name" to the <template> tag. The browser registers the CSS module globally and shares a single CSSStyleSheet across all shadow roots that adopt it. No external CSS files are produced. For partial rendering, CSS module definitions are prepended to client template payloads so the client inserts the definition before the companion template/script runs; WebUI Framework compiled metadata also carries the adopted stylesheet specifier so client-created components can reuse the registered stylesheet.

Set via parser.set_css_strategy(CssStrategy::Style).

Primary Method

pub fn parse(&mut self, fragment_id: &str, html_content: &str) -> Result<(), ParserError>
pub fn into_fragment_records(self) -> WebUIFragmentRecords

Parser Plugin System

The parser supports a framework-aware plugin system. Plugins classify framework-owned attributes, capture finalized component templates, and emit per-element hydration metadata without requiring the build layer to downcast concrete plugin types.

pub trait ParserPlugin {
    fn start_fragment(&mut self, fragment_id: &str) {}
    fn register_component_template(
        &mut self,
        tag_name: &str,
        component: &Component,
        processed_template: &str,
    ) -> Result<()>;
    fn classify_attribute(&mut self, attr_name: &str) -> AttributeAction;
    fn finish_element(&mut self, binding_attribute_count: u32) -> Option<Vec<u8>>;
    fn into_artifacts(self: Box<Self>) -> ParserPluginArtifacts;
}

Hook invocation points:

  • Fragment start: start_fragment runs before each HtmlParser::parse(...) call so plugins can reset fragment-local counters
  • Attribute loop: classify_attribute decides whether framework-owned attrs are kept, skipped, or skipped-and-counted as bindings
  • Element completion: finish_element runs with the final binding count after all attrs are processed; returned bytes are emitted as a Plugin fragment
  • Component registration: register_component_template receives the final processed component template HTML
  • Artifact extraction: into_artifacts returns post-parse outputs such as client component templates without Any downcasts

Built-in plugin: FastParserPlugin

  • Marks FAST-specific runtime attributes (@click, f-ref, f-slotted, f-children) as skipped but still counted bindings
  • Emits Plugin fragments with u32 LE attribute binding counts
  • Tracks components and returns <f-template> artifacts after parsing
  • Converts syntax to FAST syntax: <if><f-when>, <for><f-repeat>, {{expr}}{expr} in :attr values
  • FAST authoring details live in the canonical FAST HTML README

Built-in plugin: WebUIParserPlugin

  • Skips WebUI Framework runtime attributes (@click, @keydown, etc.) without counting them as attribute bindings
  • Tracks per-element event count; emits 12-byte WebUIElementData Plugin fragments encoding [binding_count, event_start, event_count]
  • Tracks components and compiles templates into raw JS IIFE strings registered in window.__webui_templates. During SSR the handler wraps them in a single <script> tag; during SPA navigation the router evaluates them directly.
  • Public framework authoring, decorators, and package entrypoints live in packages/webui-framework/README.md

Usage:

// FAST plugin
let mut parser = HtmlParser::with_plugin(Box::new(FastParserPlugin::new()));
// WebUI Framework plugin
let mut parser = HtmlParser::with_plugin(Box::new(WebUIParserPlugin::new()));
parser.parse("index.html", &html)?;

CLI integration:

# FAST plugin
webui build ./templates --out ./dist --plugin=fast
webui serve ./templates --state ./data/state.json --plugin=fast

# WebUI Framework plugin
webui build ./templates --out ./dist --plugin=webui
webui serve ./templates --state ./data/state.json --plugin=webui

webui serve performs a preflight bind check on its configured HTTP port and fails before the initial build if that port is already in use, returning an actionable message so stale dev processes can be stopped explicitly.

Content Processing

Raw Content
  • Buffer content until directive or signal encountered
  • Consolidate adjacent raw content
  • Flush buffer when transitioning to non-raw content
Directive Processing
  • : Extract item/collection pair and process children into separate fragment. Empty <for> bodies (no children) are silently skipped.
  • : Extract and parse condition, process children into separate fragment
  • : Injects body_start and body_end raw signals around the body content
  • Components: Check component registry, process as component if found
Element Processing
  • Maintain proper tag structure
  • Process children recursively (iterative implementation)
  • Handle attributes and special elements
  • Omit closing tags when the HTML parser produces no end tag (void elements, etc.)
  • Handle self-closing tags (/> syntax) for SVG and other elements

Buffer Management

  • Buffer Isolation: Isolate directive content from parent context
  • Buffer Swapping: Save/restore parent buffer during directive processing
  • Final Flush: Ensure all content is captured

Handlebars Parser

pub struct HandlebarsParser;

impl HandlebarsParser {
    pub fn parse(&self, text: &str) -> Result<Vec<WebUIFragment>, ParserError>
}

Requirements

  • Parse {{variable}} as escaped signal
  • Parse {{{variable}}} as raw (unescaped) signal
  • No support for nested handlebars expressions
  • Iterative implementation (no recursion)
  • Proper error handling for malformed expressions

Condition Parser

pub struct ConditionParser;

impl ConditionParser {
    pub fn parse(&self, input: &str) -> Result<ConditionExpr, ParserError>
}

Expression Types

  • Identifiers: Simple variable names (e.g., isAdmin)
  • Predicates: Comparisons (e.g., age > 18)
  • Not Expressions: Negations (e.g., !isBlocked)
  • Compound Expressions: Combined with logical operators (e.g., isAdmin && isActive)

Requirements

-- No parentheses support -- String literal support with both single and double quotes -- Proper operator precedence -- No recursion in implementation -- Comprehensive error messages

CSS Parser

pub struct CssParser {
    parser: Parser,
}

impl CssParser {
    pub fn parse(&mut self, css_content: &str) -> Result<WebUIFragmentRecords, ParserError>
    pub fn process_css(&mut self, css_content: &str, fragments: &mut WebUIFragmentRecords) -> Result<(), ParserError>
    pub fn parse_inline_css(&mut self, style_content: &str) -> Result<String, ParserError>
    pub fn extract_tokens(&mut self, css_content: &str) -> Result<HashSet<String>, ParserError>
}

Requirements

  • Process CSS variables
  • Extract dynamic variables with --webui- prefix
  • Convert dynamic variables to signals
  • Handle nested variable references
  • Process inline and external CSS

CSS Token Hoisting

CSS Token Hoisting extracts the set of CSS custom properties (tokens) that are used across all components and entry page styles at build time. The sorted, deduplicated list is included in the protocol's tokens field, enabling host runtimes to resolve only the design tokens the application actually needs.

Token Extraction (CssParser::extract_tokens)

The extract_tokens method uses tree-sitter-css to iteratively walk the CSS AST and extract custom property usages from var() calls, while excluding locally-defined custom properties.

Extracted (hoisted):

  • var(--colorPrimary) → token "colorPrimary"
  • var(--a, var(--b, var(--c))) → tokens "a", "b", "c" (nested fallbacks)
  • var(--size, 16px) → token "size" (literal fallbacks ignored)

Excluded (not hoisted):

  • --bar: 12px — local custom property definitions
  • var(--bar) when --bar is defined in the same CSS file

The iterative walker visits each call_expression node independently, so nested var() fallbacks (which are separate call_expression nodes in the tree-sitter AST) are naturally handled.

Token Collection During Parsing

The HtmlParser maintains a token_store: HashSet<String> that accumulates tokens from two sources:

  1. Component CSS — when a component is first encountered during parsing, its pre-extracted css_tokens (stored in the Component struct at registration time) are merged into the token store.
  2. Inline <style> tags — when the parser processes a style_element node, it calls extract_tokens on the CSS content and merges the result.

After parsing completes, HtmlParser::take_tokens() returns the sorted, deduplicated token list for inclusion in the protocol.

Comment-Based Signal Bindings

HTML comments containing handlebars expressions are parsed as signal fragments:

<!--{{tokens}}-->        → Signal { value: "tokens", raw: false }
<!--{{{tokens}}}-->      → Signal { value: "tokens", raw: true }
<!--{{tokens.light}}-->  → Signal { value: "tokens.light", raw: false }
<!-- regular comment -->  → Raw (preserved as-is)

This mechanism is general-purpose (not limited to tokens) and enables comment-based placeholders for runtime value injection in HTML files. The existing handlebars parser is reused for expression parsing within comment delimiters.

Design Token Resolution (webui-tokens)

The webui-tokens crate provides serve-time resolution of design token values. While the parser extracts token names into the protocol at build time, the token resolver loads token values from a theme file and generates CSS declarations for injection into state.

Theme File Format

A multi-theme JSON file maps theme names to flat token-name → CSS-value objects:

{
  "themes": {
    "light": { "surface-page": "#ffffff", "text-primary": "#111827" },
    "dark":  { "surface-page": "#171717", "text-primary": "#fafafa" }
  }
}

Token names omit the -- prefix (matching the protocol.tokens format). Flat single-theme files (without the "themes" wrapper) are also supported.

Resolution Pipeline

load_token_file(path) → TokenFile
    ↓
resolve_tokens(protocol.tokens, token_file) → ResolvedTokens { css, warnings }
    ↓
inject_token_css(state, css) → state["tokens"]["light"] = "..."
  1. Filter: Only tokens in protocol.tokens are kept.
  2. Dependency closure: Token values referencing other tokens via var(--x) trigger transitive inclusion. Uses an iterative BFS expansion followed by DFS cycle detection.
  3. CSS generation: Sorted --name: value; declarations. Output is deterministic.
  4. State injection: Per-theme CSS strings are set on state.tokens.<theme>, where /*{{{tokens.<theme>}}}*/ signals resolve them during rendering.

Package Resolution (resolve_theme_path)

The CLI --theme flag accepts a file path or an npm package name:

webui serve ./src --theme=@microsoft/webui-examples-theme
webui serve ./src --theme=./my-theme.json

Package names are resolved by walking up from search_root looking for node_modules/<pkg>/tokens.json. Scoped packages (@scope/name) and explicit subpaths (@scope/name/custom.json) are supported.

Error Handling

#[derive(Debug, Error)]
pub enum ParserError {
    #[error("HTML parsing error: {0}")]
    Html(String),

    #[error("CSS parsing error: {0}")]
    Css(String),

    #[error("Directive parsing error: {0}")]
    Directive(String),

    #[error("Expression parsing error: {0}")]
    Parse(String),

    #[error("Component error: {0}")]
    Component(String),

    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

WebUI Framework Plugin

This section specifies only the cross-crate wire contract for --plugin=webui: the metadata emitted by webui-parser, the SSR markers emitted by webui-handler, and the hydration/runtime expectations consumed by @microsoft/webui-framework.

It intentionally does not duplicate package tutorials or framework API docs. Use the canonical sources instead:

Metadata object format

Each component's compiled template is registered in window.__webui_templates[tagName] as a marker-free metadata object consumed by the browser runtime:

Field Type Description
h string Marker-free static HTML for client-created DOM, including baked-in <link> / <style> nodes for link/style CSS strategies
tx [slot, parts][] Client text runs inserted at precompiled slots
a CompiledAttrMeta[] Attribute binding metadata
ag [elementPath, start, count][] Attribute-target groups for a[]
c [ConditionExpr, blockIndex][] Conditional blocks
cl SlotPath[] Conditional anchor slots aligned to c[]
r [collection, itemVar, blockIndex][] Repeat blocks
rl SlotPath[] Repeat anchor slots aligned to r[]
e [event, handler, needsEvent][] Body events
el NodePath[] Event target element paths aligned to e[]
b TemplateBlockMeta[] Nested compiled block table referenced by c / r
sa string Optional module-mode adopted stylesheet specifier copied from shadowrootadoptedstylesheets
re [event, handler, needsEvent][] Root events, attached to the host element

All arrays are optional — omitted from the output when empty to minimize payload.

ConditionExpr in compiled framework metadata reuses the protocol condition AST in a compact tuple form:

  • [0, path] — identifier / truthy path lookup
  • [1, left, operator, right] — predicate comparison
  • [2, condition] — logical NOT
  • [3, left, operator, right] — logical compound (AND / OR)

Comparison operators match the protocol enum values:

  • 1 = GREATER_THAN
  • 2 = LESS_THAN
  • 3 = EQUAL
  • 4 = NOT_EQUAL
  • 5 = GREATER_THAN_OR_EQUAL
  • 6 = LESS_THAN_OR_EQUAL

Logical operators also match the protocol enum values:

  • 1 = AND
  • 2 = OR

a[] uses compact tuple forms to avoid runtime parsing:

  • [name, 0, path] — simple attribute binding, e.g. href="{{url}}"
  • [name, 1, path] — pass-through/property binding, e.g. :config="{{settings}}" or :value="{{searchQuery}}"
  • [name, 2, ConditionExpr] — boolean attribute binding, e.g. ?disabled="{{expr}}"
  • [name, 3, parts] — mixed/template attribute binding, e.g. class="item {{state}}"

Compilation rules

The Rust compiler (generate_compiled_template in webui-parser/src/plugin/webui.rs) transforms the HTML template in a single forward pass, then finalizes it into marker-free client HTML plus locator metadata:

Source syntax Metadata field(s) Client h result
{{expr}}, {{{expr}}}, mixed text tx[] dynamic text run removed
href="{{url}}" a[] + ag[] element kept marker-free
class="item {{state}}" a[] + ag[] element kept marker-free
?disabled="{{expr}}" a[] + ag[] element kept marker-free
:config="{{settings}}", :value="{{searchQuery}}" a[] + ag[] element kept marker-free
<if condition="expr">body</if> c[] + cl[] + b[] block removed; anchor slot stored
<for each="v in coll">body</for> r[] + rl[] + b[] block removed; anchor slot stored
@event="{handler(e)}" e[] + el[] element kept marker-free
@event on <template> wrapper re[N] (stripped)
w-ref="name" (stays) (unchanged)
<outlet /> (stays) <outlet></outlet>

tx[] stores text runs as [slot, parts], where parts reuse the compact attribute-part encoding (string for static text, [path] for dynamic text). Client-created DOM inserts one runtime Text node per run instead of scanning compiled marker comments.

Attribute bindings are recorded in a[], while ag[] points at the owning element and the contiguous [start, count) range inside a[]. The compiled client HTML never embeds data-w-* markers; those remain SSR-only handler markers.

Nested <if> / <for> blocks are recursively compiled into the shared b[] block table. The client runtime instantiates compiled child blocks directly and evaluates precompiled condition AST tuples — it does not parse raw template syntax or condition strings from repeat or conditional body content.

The private workspace package packages/webui-test-support (@microsoft/webui-test-support) exists to build this metadata shape in JS-side tests without duplicating tuple encodings or fixture infrastructure across webui-framework and webui-router. It centralizes fixture builders such as buildTemplate, registerCompiledTemplate, and the condition AST helpers, and it also provides shared Node-side fixture bundling/server helpers so browser fixture apps and Playwright servers stay aligned with the runtime/compiler contract as that contract evolves.

Plugin data and SSR hydration markers

The current WebUI parser emits a 12-byte Plugin fragment (WebUIElementData) for each element that has attribute bindings or @event handlers:

Bytes 0–3:  binding_count   (u32 LE)  — number of dynamic attribute bindings
Bytes 4–7:  event_start_idx (u32 LE)  — global index into metadata `e[]`
Bytes 8–11: event_count     (u32 LE)  — number of @event attrs on this element

The handler decodes this in on_element_data and emits SSR-only markers:

  • data-w-b-N for one bound attribute, or data-w-c-START-COUNT for multiple a[] entries on the same element
  • data-ev="COUNT" once per element, where COUNT is the number of consecutive entries in the metadata e[] array that belong to that element

For compatibility during mixed parser/handler rollouts, the handler also accepts the legacy 4-byte binding-only payload and upgrades it to event_count = 0.

WebUI SSR marker formats are:

Marker Format Notes
Repeat block start <!--wr--> Opens a <for> loop region
Repeat block end <!--/wr--> Closes the <for> loop region
Repeat item <!--wi--> Marks each iteration boundary inside a repeat
Conditional start <!--wc--> Opens an <if> block
Conditional end <!--/wc--> Closes the <if> block

The WebUI handler plugin emits only these five comment markers. Text bindings, attribute bindings, and event handlers are resolved from compiled metadata path indices at hydration time - no DOM attribute markers are needed. The handler only emits markers in active child scopes; the root page scope remains marker-free. During hydration the framework keeps <!--wr--> and <!--wc--> as runtime anchors and removes <!--/wr-->, <!--/wc-->, and <!--wi--> markers.

Runtime contract

@microsoft/webui-framework consumes the metadata object above plus the SSR markers emitted by WebUIHydrationPlugin. This follows an Islands Architecture approach: the server delivers fully-rendered HTML, and only interactive Web Components hydrate on the client — leaving static content untouched.

  • SSR hydration uses one DOM walk to discover <!--wr-->, <!--wi-->, and <!--wc--> comment markers, wire the relevant bindings using compiled metadata path indices, then remove SSR-only markers.
  • Client-created DOM never reparses template syntax; it clones marker-free h and resolves tx, ag, cl, rl, and el locators directly.
  • Events are resolved from compiled e[] and el[] metadata entries using path indices. The runtime installs one delegated listener per event type on the shadow root. Root events from re[] attach directly to the host element.
  • The full package entrypoint supports repeat metadata (r[] / rl[]). The additive @microsoft/webui-framework/element-no-repeat entrypoint preserves the same public WebUIElement API but must reject compiled templates that contain repeat metadata.

Detailed component examples, decorators, and package entrypoint guidance live in packages/webui-framework/README.md rather than being duplicated in this design spec.

Integration and Testing

Test Suite Requirements

  • Unit tests for each module
  • Integration tests for complete pipeline
  • Performance benchmarks
  • Error case coverage

Project Structure

webui/
├── crates/
│   ├── webui/                # Programmatic library API (build, inspect, re-exports)
│   ├── webui-cli/            # CLI build tool (binary: "webui")
│   ├── webui-discovery/      # External component discovery (npm, paths)
│   ├── webui-expressions/    # Expression evaluation engine
│   ├── webui-ffi/            # C-compatible FFI bindings
│   ├── webui-handler/        # Protocol handler implementation
│   ├── webui-node/           # Node.js native addon (napi-rs)
│   ├── webui-parser/         # HTML/CSS/template parser
│   ├── webui-protocol/       # Protocol definition
│   ├── webui-state/          # State management
│   ├── webui-test-utils/     # Testing utilities
│   └── webui-wasm/           # WebAssembly bindings
├── packages/
│   ├── @microsoft/
│   │   ├── webui/            # npm package (CLI + programmatic JS API)
│   │   ├── webui-darwin-arm64/   # Platform binary (macOS ARM64)
│   │   ├── webui-darwin-x64/     # Platform binary (macOS x64)
│   │   ├── webui-linux-x64/      # Platform binary (Linux x64)
│   │   ├── webui-linux-arm64/    # Platform binary (Linux ARM64)
│   │   ├── webui-win32-x64/      # Platform binary (Windows x64)
│   │   └── webui-win32-arm64/    # Platform binary (Windows ARM64)
│   ├── webui-framework/      # WebUI Framework client runtime (@microsoft/webui-framework)
│   ├── webui-router/         # SPA router for WebUI Framework (@microsoft/webui-router)
│   └── webui-test-support/   # Private shared JS test metadata helpers (@microsoft/webui-test-support)
├── examples/                 # Example applications (todo-fast, todo-webui, routes, …)
├── docs/                     # Documentation (VitePress)
├── tests/                    # Integration tests
└── benchmarks/               # Performance benchmarks

Crate Dependency Graph

webui-cli ──────► webui (library) ◄────── webui-node
                    │                        │
                    ├── webui-parser          ├── webui-handler
                    ├── webui-handler         ├── webui-protocol
                    ├── webui-protocol        └── serde_json
                    └── webui-discovery

webui-ffi ──────► webui-handler ◄────── webui-wasm
                  webui-parser              webui-parser
                  webui-protocol            webui-protocol

The webui library crate is the primary API surface for programmatic use. It re-exports WebUIHandler, ResponseWriter, and WebUIProtocol from their respective crates and provides build(), build_to_disk(), and inspect() functions with BuildStats (duration, fragment/component/CSS counts, protocol size).

npm Distribution

The @microsoft/webui npm package follows the esbuild single-package model:

  • bin: { "webui": "bin/webui" } — CLI binary via platform-specific optionalDependencies
  • main: "lib/main.js" — Programmatic API that loads the .node native addon directly
  • WASM fallback for render when native addon is unavailable (one-time warning logged)

Documentation Guidelines

  • Using vitepress in docs/
  • API documentation for all public interfaces
  • Technical explanations of algorithms
  • Performance considerations
  • Error handling guidelines
  • Examples for all major features

FFI Bindings (webui-ffi)

The FFI crate exposes WebUI to host languages via a C-compatible ABI. The generated header is at crates/webui-ffi/include/webui_ffi.h.

Functions

Function Description
webui_render(html, data_json) Parse + render in one call. Returns heap-allocated string (caller frees with webui_free).
webui_handler_create() Create a reusable handler (no plugin).
webui_handler_create_with_plugin(plugin_id) Create a handler with a named plugin (e.g. "fast"). Returns NULL on error.
webui_handler_render(handler, data, len, json, entry_id, request_path) Render a pre-compiled protocol with route matching. request_path controls which route is active. Returns heap-allocated string.
webui_render_partial(protocol_data, len, state_json, entry_id, request_path, inventory_hex) Produce a complete JSON partial response (state, templates, inventory, path, and matched route chain) in a single call. Returns heap-allocated JSON string.
webui_handler_destroy(handler) Destroy a handler. NULL is a safe no-op.
webui_free(ptr) Free a string returned by any render function. NULL is a safe no-op.
webui_last_error() Return per-thread error message. Caller must not free.

Error Model

Thread-local error storage following the POSIX dlerror() pattern. After any function returns NULL, call webui_last_error() for a human-readable diagnostic.

CLI Tool (webui-cli)

The CLI specification and usage details are maintained in crates/webui-cli/README.md.

Example Workflow

Examples and end-to-end walkthroughs are maintained in examples/README.md