Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,7 @@ pub trait ParserPlugin {
component: &Component,
processed_template: &str,
) -> Result<()>;
fn component_source_transform(&self) -> Option<ComponentSourceTransform> { None }
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>) -> Result<ParserPluginArtifacts>;
Expand All @@ -1237,6 +1238,7 @@ pub trait ParserPlugin {
- **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 plugin-facing component template HTML after HTML/CSS comment stripping. Authored root `<template>` attributes are preserved for plugins; the SSR/internal parse view may strip runtime-only attributes so rendered HTML stays clean. The component's client-ownership marker distinguishes authored from scriptless templates; Rust does not inspect JavaScript/TypeScript semantics.
- **Component-source transform**: `component_source_transform` returns an optional stateless function pointer, `for<'a> fn(ComponentSource<'a>) -> Result<ComponentSourceResult>`. The component registry calls it once per component — after reading the authored HTML but before name validation, duplicate checking, CSS processing, or insertion. `Unchanged` stores the filename-derived tag and HTML verbatim; `Transformed` may replace the registry key and supplies the HTML the WebUI parser consumes, plus an optional distinct source retained for the plugin's client artifact. The default returns `None`, so a component's filename-derived tag and HTML are stored unchanged and any plugin-specific markup in the source is inert. This is the sole extension point for a plugin that owns an alternate authored-template dialect; see "Built-in FAST parser plugins" below for the concrete FAST implementation.
- **Artifact extraction**: `into_artifacts` returns post-parse outputs such as client component templates without `Any` downcasts. It is **fallible**: template-authoring mistakes found while compiling component templates (an invalid `@event` handler or a non-braced `w-ref`) surface as `ParserError::Template` instead of panicking, so every host (CLI, Node, FFI, WASM) can handle them.

**Selecting parser plugins**
Expand All @@ -1250,6 +1252,65 @@ documentation for the current list. Each plugin defines:
- Any post-parse artifacts (e.g., client component templates) it injects at `</body>`
- Any template-syntax conversions it performs inside component templates

**Built-in FAST parser plugins**

The `fast`, `fast_v2`, and `fast_v3` parser implementations (selected as
`fast`, `fast-v2`, and `fast-v3` by CLI and host string APIs) share one
`component_source_transform` implementation. Only when one of these plugins is
selected does the component registry run that transform for each component,
after reading the authored HTML but before name validation, duplicate
checking, CSS processing, or insertion. With no plugin, or with any other
plugin (including `webui`) that returns `None` from
`component_source_transform`, the registry never scans for or interprets
`<f-template>` syntax — an `<f-template>`-shaped source passes through
unchanged, exactly like any other component.

The shared FAST transform scans the authored source for an `<f-template>`. A
source that has one must contain exactly one `<f-template>` with exactly one
inner `<template>`. A present, non-empty `name` becomes the registered
component tag and overrides the filename-derived tag. If `name` is absent or
trims to empty, registration keeps the filename-derived tag. Multiple
`<f-template>` elements return `unsupported-multiple-f-templates`; multiple
inner `<template>` elements are also invalid. Sources without an `<f-template>`
return `ComponentSourceResult::Unchanged` and follow the normal component
template path.

For build-time SSR parsing, WebUI internally adapts supported FAST declarative
constructs into the WebUI parser view. The adapted inner template becomes the
parser view returned as `TransformedComponentSource::parser_content`:

- `<f-repeat value="{{item in items}}">` converts to
`<for each="item in items">`.
- `<f-when value="{{condition}}">` converts to
`<if condition="condition">`.
- The adaptation unwraps the `value` expression for those directives. Text
`{{expression}}` bindings and `?boolean` bindings remain available to the
WebUI parser; ordinary attributes are not treated as additional FAST
declarative syntax.
- Unsupported `f-*` elements or attributes and malformed directive expressions
return structured authoring diagnostics. WebUI claims support only for the
FAST constructs described here. Stable codes are
`unsupported-multiple-f-templates` for multiple wrappers,
`invalid-fast-template` for unsupported or malformed FAST declarative syntax,
and the shared `unclosed-html-tag` for unclosed markup.
- The FAST plugins' `classify_attribute` skips `@event`, `:property`, `f-ref`,
`f-slotted`, and `f-children` and counts each as a binding, so they are
absent from the SSR view while the hydration binding count still reflects
them. No parser-core marker or FAST-named branch is involved.

The transform separately returns the authored `<f-template>` body as
`TransformedComponentSource::artifact_content`, including its inner template
and client-only bindings, rather than deriving it from the converted parser
view. The FAST plugin wraps that retained source in the resolved
`<f-template name="...">` for insertion. The artifact is normalized rather than
preserved byte-for-byte: it passes through the same generic component-template
processing as any other component, including wrapper normalization, selected
CSS-strategy injection, module stylesheet adoption where applicable,
legal-comment handling, and plugin artifact normalization. The deprecated
`fast` selector aliases the FAST 2 implementation, while `fast_v2` and `fast_v3`
use their respective hydration marker formats; all three share this transform,
conversion, and artifact-retention behavior.

WebUI itself does not interpret plugin-emitted bytes; each parser plugin pairs with
a matching handler plugin that consumes them at render time. See [packages/webui-framework/README.md](packages/webui-framework/README.md)
for the WebUI Framework's public authoring model.
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,31 @@
- **Server-side logic:** Conditions, loops, and expressions are evaluated on the server.
- **Plugin-ready:** Parser and handler plugins support framework-specific hydration and directives.

## FAST plugin authored templates

WebUI's parser is framework-neutral by default: with no plugin selected, or
with the `webui` plugin, `<f-template>` markup is not scanned or interpreted
and passes through like any other HTML. Selecting the `fast`, `fast_v2`, or
`fast_v3` integration (CLI names `fast`, `fast-v2`, and `fast-v3`) installs a
shared plugin hook that detects component HTML authored as a single
`<f-template>`. A non-empty `name` replaces the component tag derived from the
filename; an absent or whitespace-only name keeps the filename-derived tag. The
wrapper must contain exactly one inner `<template>`. Unsupported FAST syntax and
multiple `<f-template>` blocks fail the build with an authoring diagnostic.

For build-time SSR, WebUI internally adapts supported FAST declarative
constructs into its parser view. It converts `<f-repeat>` and `<f-when>`
directives to WebUI `<for>` and `<if>` directives while preserving text
interpolation and boolean bindings. An absent or empty authored name continues
to use the filename-derived component tag. WebUI then removes client-only
`@event`, `:property`, `f-ref`, `f-slotted`, and `f-children` attributes from the
SSR view while retaining their binding counts for FAST hydration. The authored
`<f-template>` body, including its inner template and client bindings, is
retained for the emitted `<f-template>` instead of being regenerated from the
SSR conversion. It still receives normal wrapper normalization, legal comment
processing, and CSS injection for the selected strategy. Component HTML without
an `<f-template>` continues through the normal WebUI template path.

## Install

```bash
Expand Down
83 changes: 82 additions & 1 deletion crates/webui-parser/benches/parser_bench.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
use std::hint::black_box;
use webui_parser::{plugin::fast_v2::FastV2ParserPlugin, CssStrategy, HtmlParser};

Expand Down Expand Up @@ -316,6 +316,38 @@ fn build_nested_article_template(depth: usize) -> String {
html
}

/// Authored component source with no FAST syntax: the FAST source transform
/// must reject it on the byte precheck without walking the elements.
fn build_ordinary_component_source(depth: usize) -> String {
let mut html = String::with_capacity(depth * 33 + 64);
html.push_str("<template>");
for _ in 0..depth {
html.push_str("<section class=\"level\">");
}
html.push_str("<span>{{title}}</span>");
for _ in 0..depth {
html.push_str("</section>");
}
html.push_str("</template>");
html
}

/// Authored FAST component source: the transform must walk it, resolve the
/// `<f-template name>`, and run the internal FAST-to-WebUI conversion.
fn build_fast_component_source(depth: usize) -> String {
let mut html = String::with_capacity(depth * 63 + 128);
html.push_str("<f-template name=\"x-registration-bench\"><template>");
for _ in 0..depth {
html.push_str("<f-when value=\"{{visible}}\"><section>");
}
html.push_str("<span>{{title}}</span>");
for _ in 0..depth {
html.push_str("</section></f-when>");
}
html.push_str("</template></f-template>");
html
}

fn parser_with_bench_components() -> HtmlParser {
let mut parser = HtmlParser::new();
register_bench_components(&mut parser);
Expand Down Expand Up @@ -599,11 +631,60 @@ fn parser_adversarial_bench(c: &mut Criterion) {
group.finish();
}

/// Measure `ComponentRegistry::register_component` with the FAST plugin's
/// component-source transform installed, for both a FAST-free source (byte
/// precheck, no walk) and an authored `<f-template>` source (walk plus
/// conversion). Each iteration registers into a freshly built parser created
/// outside the timed closure, so parser construction and source generation are
/// excluded and no duplicate-registration error can occur.
fn component_registration_fast_source_transform_bench(c: &mut Criterion) {
let mut group = c.benchmark_group("component_registration_fast_source_transform");
let scenarios = [
("ordinary", 8, build_ordinary_component_source(8)),
("ordinary", 64, build_ordinary_component_source(64)),
("f_template", 8, build_fast_component_source(8)),
("f_template", 64, build_fast_component_source(64)),
];

for (source_kind, depth, source) in scenarios {
group.throughput(Throughput::Bytes(source.len() as u64));
group.bench_with_input(
BenchmarkId::new(source_kind, depth),
&source,
|b, source| {
b.iter_batched(
|| HtmlParser::with_plugin(Box::new(FastV2ParserPlugin::new())),
|mut parser| {
parser
.component_registry_mut()
.register_component(webui_parser::ComponentRegistration::new(
"x-registration-bench",
black_box(source.as_str()),
None,
true,
))
.unwrap_or_else(|error| {
panic!(
"registration failed for {source_kind} depth {depth}: {error}"
)
});
black_box(parser)
},
BatchSize::SmallInput,
);
},
);
}

group.finish();
}

criterion_group!(
benches,
parser_parse_reuse_bench,
parser_parse_fresh_vs_reuse,
parser_plugin_bench,
component_registration_fast_source_transform_bench,
parser_css_strategy_bench,
parser_size_sweep_bench,
parser_realistic_bench,
Expand Down
Loading
Loading