microsoft-webui-parser 3.0.3

Core parser for WebUI framework
Documentation
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

//! Parser plugin trait and built-in plugin implementations.
//!
//! The plugin system is framework-aware but phase-local — parser plugins
//! classify framework-owned attributes, capture plugin-facing component templates,
//! and emit per-element hydration metadata for the handler.

pub mod fast;
pub use fast::v2 as fast_v2;
pub use fast::v3 as fast_v3;
pub mod webui;

use crate::component_registry::Component;
use crate::{ParserOptions, Result};

/// Parser-owned decision about how an attribute should be handled.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttributeAction {
    /// Let the parser process the attribute normally.
    Keep,
    /// Skip the attribute without incrementing the element binding count.
    Skip,
    /// Skip the attribute and count it as a dynamic binding.
    SkipAndCountBinding,
}

/// Build artifacts extracted from a parser plugin after parsing completes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParserPluginArtifacts {
    /// The plugin did not produce any post-parse artifacts.
    None,
    /// Client component templates keyed by tag name.
    ComponentTemplates(Vec<ComponentTemplateArtifact>),
}

/// State surface a component consumes through one hydration channel.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StateSurface {
    /// The component is known not to consume state through this channel.
    None,
    /// The component consumes only these sorted, deduplicated keys.
    Keys(Vec<String>),
    /// The complete surface is unknown, so callers must preserve full state.
    All,
}

impl StateSurface {
    /// Create an exact keyed surface, collapsing an empty list to [`Self::None`].
    #[must_use]
    pub fn keys(mut keys: Vec<String>) -> Self {
        keys.sort_unstable();
        keys.dedup();
        if keys.is_empty() {
            Self::None
        } else {
            Self::Keys(keys)
        }
    }
}

/// Client template payload generated by a parser plugin for one component.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ComponentTemplateArtifact {
    /// Component custom-element tag name.
    pub tag_name: String,
    /// Plugin-owned client template payload.
    pub template: String,
    /// WebUI JSON-safe template metadata.
    pub template_json: String,
    /// WebUI component-local JavaScript condition closure array.
    pub template_functions: String,
    /// Initial hydration state surface derived by the producing plugin.
    pub hydration: StateSurface,
    /// Partial-navigation state surface derived by the producing plugin.
    pub navigation: StateSurface,
    /// Compiled template roots — the reactive property names referenced
    /// directly by the template (e.g. `{{name}}`) independent of any
    /// authored client script. Always populated (possibly empty), even when
    /// `hydration`/`navigation` collapse to [`StateSurface::All`], so a
    /// bundler-neutral manifest consumer can union exact client keys with
    /// these Rust-derived roots without re-deriving them.
    pub template_roots: Vec<String>,
    /// Whether this component has an authored client module (sibling script
    /// or an externally client-owned package), as opposed to a scriptless
    /// compiled template. Bundler manifest coverage validation only requires
    /// an entry for scripted components; scriptless ones are always
    /// Rust-derived and never require a manifest entry.
    pub is_scripted: bool,
    /// Whether this component effectively uses Shadow DOM.
    pub uses_shadow_dom: bool,
}

impl ComponentTemplateArtifact {
    /// Create a non-WebUI template payload.
    #[must_use]
    pub fn template(tag_name: String, template: String, uses_shadow_dom: bool) -> Self {
        Self {
            tag_name,
            template,
            template_json: String::new(),
            template_functions: String::new(),
            hydration: StateSurface::None,
            navigation: StateSurface::None,
            template_roots: Vec::new(),
            is_scripted: false,
            uses_shadow_dom,
        }
    }

    /// Create a WebUI split metadata/function payload.
    ///
    /// The hydration surface starts empty; the producing plugin attaches it with
    /// [`Self::with_hydration`] once it has derived the surface.
    #[must_use]
    pub fn webui(
        tag_name: String,
        template_json: String,
        template_functions: String,
        uses_shadow_dom: bool,
    ) -> Self {
        Self {
            tag_name,
            template: String::new(),
            template_json,
            template_functions,
            hydration: StateSurface::None,
            navigation: StateSurface::None,
            template_roots: Vec::new(),
            is_scripted: false,
            uses_shadow_dom,
        }
    }

    /// Attach the initial hydration surface derived by the plugin.
    #[must_use]
    pub fn with_hydration(mut self, hydration: StateSurface) -> Self {
        self.hydration = hydration;
        self
    }

    /// Attach the partial-navigation surface derived by the plugin.
    #[must_use]
    pub fn with_navigation(mut self, navigation: StateSurface) -> Self {
        self.navigation = navigation;
        self
    }

    /// Attach the compiled template roots (see [`Self::template_roots`]).
    #[must_use]
    pub fn with_template_roots(mut self, roots: Vec<String>) -> Self {
        self.template_roots = roots;
        self
    }

    /// Mark whether this component has an authored client module.
    #[must_use]
    pub fn scripted(mut self, is_scripted: bool) -> Self {
        self.is_scripted = is_scripted;
        self
    }
}

/// Build-resolved CSS delivery for a component.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComponentStyleDelivery<'a> {
    /// An external stylesheet served at `href`.
    Link {
        /// Resolved stylesheet URL.
        href: &'a str,
    },
    /// The compiled CSS text itself.
    Inline {
        /// Compiled CSS source.
        css: &'a str,
    },
    /// A constructed stylesheet the root adopts by `specifier`, already
    /// recorded on the template's `shadowrootadoptedstylesheets`.
    Adopted {
        /// Module specifier for the constructed stylesheet.
        specifier: &'a str,
    },
}

/// A component template after the parser has resolved its build behavior.
#[derive(Debug, Clone, Copy)]
pub struct ComponentBuildContext<'a> {
    /// Registered component definition.
    pub component: &'a Component,
    /// Final template view intended for the plugin runtime.
    pub template: &'a str,
    /// Whether this component effectively uses Shadow DOM.
    pub uses_shadow_dom: bool,
    /// CSS delivery for effective Shadow DOM components.
    pub style: Option<ComponentStyleDelivery<'a>>,
}

/// A fragment about to be parsed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FragmentContext<'a> {
    /// Stable fragment identifier.
    pub id: &'a str,
}

/// An attribute encountered on an opening tag.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AttributeContext<'a> {
    /// Authored attribute name.
    pub name: &'a str,
}

/// An opening tag after all attributes have been processed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ElementStartContext<'a> {
    /// Authored element tag name.
    pub tag_name: &'a str,
    /// Number of dynamic attribute bindings on the element.
    pub binding_count: u32,
}

/// Borrowed authored component source presented to a parser plugin before the
/// component registry stores it.
#[derive(Debug, Clone, Copy)]
pub struct ComponentSource<'a> {
    /// Filename-derived custom-element tag name.
    pub tag_name: &'a str,
    /// Authored HTML template for the component.
    pub html_content: &'a str,
}

/// Owned replacement views produced by a plugin's component-source transform.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransformedComponentSource {
    /// Resolved registry key. May differ from the filename-derived tag when the
    /// authored source names the component itself.
    pub tag_name: String,
    /// Content the WebUI SSR parser consumes for this component.
    pub parser_content: String,
    /// Optional distinct authored source retained for the plugin's client
    /// artifact. `None` reuses `parser_content` for the artifact.
    pub artifact_content: Option<String>,
}

/// Stateless source transform installed into a [`ComponentRegistry`].
///
/// It may resolve the registry key and provide separate SSR and client views.
/// Returning `Ok(None)` preserves the authored source without allocation.
///
/// # Errors
///
/// Returns a [`crate::Diagnostic`]-carrying error when the authored source is
/// invalid.
///
/// [`ComponentRegistry`]: crate::component_registry::ComponentRegistry
pub type ComponentSourceTransform =
    for<'a> fn(ComponentSource<'a>) -> Result<Option<TransformedComponentSource>>;

/// Static component-processing behavior for a parser plugin.
///
/// The parser reads this once during construction and keeps it on the direct
/// path, avoiding repeated virtual calls while compiling component templates.
#[derive(Debug, Clone, Copy, Default)]
pub struct ComponentProcessing {
    /// Optional build-time source transformation.
    pub source_transform: Option<ComponentSourceTransform>,
    /// Keep root-template runtime attributes for [`ParserPlugin::process_attribute`].
    pub process_root_template_attributes: bool,
    /// Place inline component styles after template content.
    pub inline_styles_after_content: bool,
}

/// A parser plugin that can customize template parsing behavior.
///
/// Plugins receive callbacks at explicit lifecycle points: parser setup,
/// fragment start, component build, attribute processing, opening-tag
/// completion, and build completion. Every callback has a no-op default.
///
/// WebUI calls these hooks during parsing; plugins decide what (if anything) to do.
pub trait ParserPlugin {
    /// Configure the plugin before parsing or component registration begins.
    fn configure_parser(&mut self, _options: &ParserOptions) {}

    /// Return static component-processing behavior.
    fn component_processing(&self) -> ComponentProcessing {
        ComponentProcessing::default()
    }

    /// Called before parsing begins for a fragment.
    ///
    /// Plugins can use this to reset fragment-local counters while preserving
    /// global build-level state such as tracked component templates.
    fn begin_fragment(&mut self, _context: FragmentContext<'_>) {}

    /// Called after a component template is fully built.
    ///
    /// # Errors
    ///
    /// Returns an error when the plugin cannot compile or retain the component.
    fn component_built(&mut self, _context: ComponentBuildContext<'_>) -> Result<()> {
        Ok(())
    }

    /// Process one authored attribute.
    fn process_attribute(&mut self, _context: AttributeContext<'_>) -> AttributeAction {
        AttributeAction::Keep
    }

    /// Called after all attributes on an opening tag have been processed.
    /// Returns optional opaque bytes to emit as a `Plugin` protocol fragment.
    fn finish_opening_tag(&mut self, _context: ElementStartContext<'_>) -> Option<Vec<u8>> {
        None
    }

    /// Complete the build and return any captured artifacts.
    ///
    /// # Errors
    ///
    /// Returns an error if the plugin encountered an invalid template construct
    /// while producing its artifacts (e.g. an invalid `@event` handler).
    fn finish(self: Box<Self>) -> Result<ParserPluginArtifacts> {
        Ok(ParserPluginArtifacts::None)
    }
}

#[cfg(test)]
mod artifact_tests {
    use super::{ComponentTemplateArtifact, StateSurface};

    #[test]
    fn template_constructor_starts_with_no_hydration_surface() {
        let artifact = ComponentTemplateArtifact::template("x-a".into(), "<p></p>".into(), false);
        assert_eq!(artifact.tag_name, "x-a");
        assert_eq!(artifact.template, "<p></p>");
        assert!(artifact.template_json.is_empty());
        assert!(artifact.template_functions.is_empty());
        assert_eq!(artifact.hydration, StateSurface::None);
        assert_eq!(artifact.navigation, StateSurface::None);
    }

    #[test]
    fn webui_constructor_starts_with_no_hydration_surface() {
        let artifact =
            ComponentTemplateArtifact::webui("x-b".into(), "{\"j\":1}".into(), "[]".into(), false);
        assert_eq!(artifact.tag_name, "x-b");
        assert!(artifact.template.is_empty());
        assert_eq!(artifact.template_json, "{\"j\":1}");
        assert_eq!(artifact.template_functions, "[]");
        assert_eq!(artifact.hydration, StateSurface::None);
        assert_eq!(artifact.navigation, StateSurface::None);
    }

    #[test]
    fn with_hydration_attaches_custom_surface() {
        let artifact =
            ComponentTemplateArtifact::webui("x-c".into(), "{}".into(), "[]".into(), false)
                .with_hydration(StateSurface::keys(vec!["count".into(), "name".into()]));
        assert_eq!(
            artifact.hydration,
            StateSurface::Keys(vec!["count".into(), "name".into()])
        );
    }

    #[test]
    fn with_hydration_is_plugin_agnostic_over_template_payloads() {
        // A non-WebUI plugin payload can carry its own hydration surface too.
        let artifact =
            ComponentTemplateArtifact::template("x-d".into(), "<f-t></f-t>".into(), false)
                .with_hydration(StateSurface::keys(vec!["value".into()]));
        assert_eq!(artifact.template, "<f-t></f-t>");
        assert_eq!(artifact.hydration, StateSurface::Keys(vec!["value".into()]));
    }

    #[test]
    fn with_navigation_attaches_partial_state_surface() {
        let artifact =
            ComponentTemplateArtifact::webui("x-e".into(), "{}".into(), "[]".into(), false)
                .with_navigation(StateSurface::keys(vec!["items".into(), "title".into()]));
        assert_eq!(
            artifact.navigation,
            StateSurface::Keys(vec!["items".into(), "title".into()])
        );
        assert_eq!(artifact.hydration, StateSurface::None);
    }
}