mirror of
https://github.com/zaphar/wasm-web-components.git
synced 2025-07-22 19:50:07 -04:00
263 lines
8.8 KiB
Rust
263 lines
8.8 KiB
Rust
// Copyright 2022 Jeremy Wall (Jeremy@marzhilsltudios.com)
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
use inflector::Inflector;
|
|
use proc_macro::TokenStream;
|
|
use proc_macro2::{Literal, Span};
|
|
use proc_macro_crate::{crate_name, FoundCrate};
|
|
use quote::quote;
|
|
use syn::{
|
|
parse_macro_input, parse_quote, AttributeArgs, Ident, ItemStruct, Lit, LitStr, Meta,
|
|
NestedMeta, Path,
|
|
};
|
|
|
|
fn expand_crate_ref(name: &str, path: Path) -> syn::Path {
|
|
let found_crate = crate_name(name).expect(&format!("{} is present in `Cargo.toml`", name));
|
|
|
|
match found_crate {
|
|
FoundCrate::Itself => parse_quote!( crate::#path ),
|
|
FoundCrate::Name(name) => {
|
|
let ident = Ident::new(&name, Span::call_site());
|
|
parse_quote!( #ident::#path )
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_class_and_element_names(
|
|
args: Vec<NestedMeta>,
|
|
struct_name: &Ident,
|
|
) -> (Literal, Literal, Literal) {
|
|
let mut class_name = None;
|
|
let mut element_name = None;
|
|
let mut observed_attributes = None;
|
|
for arg in args {
|
|
if let NestedMeta::Meta(Meta::NameValue(nv)) = arg {
|
|
if nv.path.is_ident("class_name") {
|
|
if let Lit::Str(nm) = nv.lit {
|
|
class_name = Some(nm);
|
|
}
|
|
} else if nv.path.is_ident("element_name") {
|
|
if let Lit::Str(nm) = nv.lit {
|
|
element_name = Some(nm);
|
|
}
|
|
} else if nv.path.is_ident("observed_attrs") {
|
|
if let Lit::Str(nm) = nv.lit {
|
|
observed_attributes = Some(nm);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let class_name = class_name.map(|n| n.token()).unwrap_or_else(|| {
|
|
LitStr::new(struct_name.to_string().as_ref(), Span::call_site()).token()
|
|
});
|
|
|
|
let element_name = match element_name.map(|n| n.token()) {
|
|
Some(n) => n,
|
|
None => {
|
|
let class_kebab = class_name.to_string().to_kebab_case();
|
|
LitStr::new(&class_kebab, Span::call_site()).token()
|
|
}
|
|
};
|
|
|
|
let observed_attributes = observed_attributes
|
|
.map(|n| n.token())
|
|
.unwrap_or_else(|| LitStr::new("[]", Span::call_site()).token());
|
|
(class_name, element_name, observed_attributes)
|
|
}
|
|
|
|
fn expand_component_def(
|
|
struct_name: &Ident,
|
|
class_name: &Literal,
|
|
element_name: &Literal,
|
|
) -> syn::ItemImpl {
|
|
let trait_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponentDef));
|
|
parse_quote! {
|
|
impl #trait_path for #struct_name {
|
|
fn element_name() -> &'static str {
|
|
#element_name
|
|
}
|
|
|
|
fn class_name() -> &'static str {
|
|
#class_name
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
fn expand_struct_trait_shim(struct_name: &Ident, observed_attrs: Literal) -> syn::ItemImpl {
|
|
let trait_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponentDef));
|
|
let handle_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponentHandle));
|
|
parse_quote! {
|
|
impl #struct_name {
|
|
pub fn element_name() -> &'static str {
|
|
<Self as #trait_path>::element_name()
|
|
}
|
|
|
|
pub fn class_name() -> &'static str {
|
|
<Self as #trait_path>::class_name()
|
|
}
|
|
|
|
pub fn define() -> std::result::Result<#handle_path<#struct_name>, JsValue> {
|
|
use wasm_bindgen::JsCast;
|
|
use web_sys::{window, Element, HtmlElement};
|
|
let registry = web_sys::window().unwrap().custom_elements();
|
|
let maybe_element = registry.get(Self::element_name());
|
|
if maybe_element.is_truthy() {
|
|
return Err("Custom Element has already been defined".into());
|
|
}
|
|
let body = format!(
|
|
"class {name} extends HTMLElement {{
|
|
constructor() {{
|
|
super();
|
|
this._impl = impl();
|
|
}}
|
|
|
|
connectedCallback() {{
|
|
this._impl.connected_impl(this);
|
|
}}
|
|
|
|
disconnectedCallback() {{
|
|
this._impl.disconnected_impl(this);
|
|
}}
|
|
|
|
static get observedAttributes() {{
|
|
return {observed_attributes};
|
|
}}
|
|
|
|
adoptedCallback() {{
|
|
this._impl.adopted_impl(this);
|
|
}}
|
|
|
|
attributeChangedCallback(name, oldValue, newValue) {{
|
|
this._impl.attribute_changed_impl(this, name, oldValue, newValue);
|
|
}}
|
|
}}
|
|
customElements.define(\"{element_name}\", {name});
|
|
var element = customElements.get(\"{element_name}\");
|
|
return element;",
|
|
name = Self::class_name(),
|
|
element_name = Self::element_name(),
|
|
observed_attributes = #observed_attrs,
|
|
);
|
|
let fun = Function::new_with_args("impl", &body);
|
|
let f: Box<dyn FnMut() -> Self> = Box::new(|| {
|
|
let obj = Self::new();
|
|
obj
|
|
});
|
|
let constructor_handle = Closure::wrap(f);
|
|
let element = fun
|
|
.call1(
|
|
&window().unwrap(),
|
|
constructor_handle.as_ref().unchecked_ref::<Function>(),
|
|
)?
|
|
.dyn_into()?;
|
|
Ok(WebComponentHandle {
|
|
element_constructor: element,
|
|
impl_handle: constructor_handle,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn expand_wasm_shim(struct_name: &Ident) -> syn::ItemImpl {
|
|
let trait_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponentBinding));
|
|
parse_quote! {
|
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
|
impl #struct_name {
|
|
#[wasm_bindgen::prelude::wasm_bindgen(constructor)]
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
|
pub fn connected_impl(&self, element: &web_sys::HtmlElement) {
|
|
use #trait_path;
|
|
self.connected(element);
|
|
}
|
|
|
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
|
pub fn disconnected_impl(&self, element: &web_sys::HtmlElement) {
|
|
use #trait_path;
|
|
self.disconnected(element);
|
|
}
|
|
|
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
|
pub fn adopted_impl(&self, element: &web_sys::HtmlElement) {
|
|
use #trait_path;
|
|
self.adopted(element);
|
|
}
|
|
|
|
|
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
|
pub fn attribute_changed_impl(
|
|
&self,
|
|
element: &web_sys::HtmlElement,
|
|
name: wasm_bindgen::JsValue,
|
|
old_value: wasm_bindgen::JsValue,
|
|
new_value: wasm_bindgen::JsValue,
|
|
) {
|
|
use #trait_path;
|
|
self.attribute_changed(element, name, old_value, new_value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn expand_binding(struct_name: &Ident) -> syn::ItemImpl {
|
|
let trait_path = expand_crate_ref("wasm-web-component", parse_quote!(WebComponent));
|
|
parse_quote!(
|
|
impl #trait_path for #struct_name {}
|
|
)
|
|
}
|
|
|
|
fn expand_struct(
|
|
item_struct: ItemStruct,
|
|
class_name: Literal,
|
|
element_name: Literal,
|
|
observed_attributes: Literal,
|
|
) -> TokenStream {
|
|
let struct_name = item_struct.ident.clone();
|
|
let component_def = expand_component_def(&struct_name, &class_name, &element_name);
|
|
let non_wasm_impl = expand_struct_trait_shim(&struct_name, observed_attributes);
|
|
let wasm_shim = expand_wasm_shim(&struct_name);
|
|
let binding_trait = expand_binding(&struct_name);
|
|
let expanded = quote! {
|
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
|
#[derive(Default, Debug)]
|
|
#item_struct
|
|
#component_def
|
|
#non_wasm_impl
|
|
#binding_trait
|
|
#wasm_shim
|
|
};
|
|
|
|
TokenStream::from(expanded)
|
|
}
|
|
|
|
/// Creates the necessary Rust and Javascript shims for a Web Component.
|
|
#[proc_macro_attribute]
|
|
pub fn web_component(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
// TODO(jwall): Attrs for class name and element name
|
|
// Gather our attributes
|
|
let args = parse_macro_input!(attr as AttributeArgs);
|
|
let item_struct = parse_macro_input!(item as ItemStruct);
|
|
|
|
let (class_name, element_name, observed_attributes) =
|
|
get_class_and_element_names(args, &item_struct.ident);
|
|
|
|
expand_struct(item_struct, class_name, element_name, observed_attributes)
|
|
}
|