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};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttributeAction {
Keep,
Skip,
SkipAndCountBinding,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParserPluginArtifacts {
None,
ComponentTemplates(Vec<ComponentTemplateArtifact>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StateSurface {
None,
Keys(Vec<String>),
All,
}
impl StateSurface {
#[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)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ComponentTemplateArtifact {
pub tag_name: String,
pub template: String,
pub template_json: String,
pub template_functions: String,
pub hydration: StateSurface,
pub navigation: StateSurface,
pub template_roots: Vec<String>,
pub is_scripted: bool,
pub uses_shadow_dom: bool,
}
impl ComponentTemplateArtifact {
#[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,
}
}
#[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,
}
}
#[must_use]
pub fn with_hydration(mut self, hydration: StateSurface) -> Self {
self.hydration = hydration;
self
}
#[must_use]
pub fn with_navigation(mut self, navigation: StateSurface) -> Self {
self.navigation = navigation;
self
}
#[must_use]
pub fn with_template_roots(mut self, roots: Vec<String>) -> Self {
self.template_roots = roots;
self
}
#[must_use]
pub fn scripted(mut self, is_scripted: bool) -> Self {
self.is_scripted = is_scripted;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComponentStyleDelivery<'a> {
Link {
href: &'a str,
},
Inline {
css: &'a str,
},
Adopted {
specifier: &'a str,
},
}
#[derive(Debug, Clone, Copy)]
pub struct ComponentBuildContext<'a> {
pub component: &'a Component,
pub template: &'a str,
pub uses_shadow_dom: bool,
pub style: Option<ComponentStyleDelivery<'a>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FragmentContext<'a> {
pub id: &'a str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AttributeContext<'a> {
pub name: &'a str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ElementStartContext<'a> {
pub tag_name: &'a str,
pub binding_count: u32,
}
#[derive(Debug, Clone, Copy)]
pub struct ComponentSource<'a> {
pub tag_name: &'a str,
pub html_content: &'a str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransformedComponentSource {
pub tag_name: String,
pub parser_content: String,
pub artifact_content: Option<String>,
}
pub type ComponentSourceTransform =
for<'a> fn(ComponentSource<'a>) -> Result<Option<TransformedComponentSource>>;
#[derive(Debug, Clone, Copy, Default)]
pub struct ComponentProcessing {
pub source_transform: Option<ComponentSourceTransform>,
pub process_root_template_attributes: bool,
pub inline_styles_after_content: bool,
}
pub trait ParserPlugin {
fn configure_parser(&mut self, _options: &ParserOptions) {}
fn component_processing(&self) -> ComponentProcessing {
ComponentProcessing::default()
}
fn begin_fragment(&mut self, _context: FragmentContext<'_>) {}
fn component_built(&mut self, _context: ComponentBuildContext<'_>) -> Result<()> {
Ok(())
}
fn process_attribute(&mut self, _context: AttributeContext<'_>) -> AttributeAction {
AttributeAction::Keep
}
fn finish_opening_tag(&mut self, _context: ElementStartContext<'_>) -> Option<Vec<u8>> {
None
}
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() {
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);
}
}