use std::fmt::{self, Debug, Display, Write};
use std::ops::Deref;
use arrayvec::ArrayString;
use proc_macro::{Literal, TokenStream};
use crate::gen::Short;
use crate::itertools::IteratorExt;
use crate::tokenize::prelude::*;
pub type JsFnName = ArrayString<24>;
#[derive(Default, Debug)]
pub struct Transient {
pub js: JsModule,
pub fields: Vec<Field>,
pub els: Vec<Short>,
}
impl Tokenize for Transient {
fn tokenize(mut self) -> TokenStream {
if self.els.is_empty() {
return match self.fields.remove(0) {
Field::Html { value, .. } | Field::Attribute { value, .. } => value,
};
}
let abi_lifetime = if self.fields.iter().any(Field::borrows) {
"'abi,"
} else {
""
};
let mut generics = String::new();
let mut generics_product = String::new();
let mut bounds = String::new();
let mut build = String::new();
let mut update = String::new();
let mut declare = String::new();
let mut vars = String::new();
for field in self.fields.iter() {
let (name, _) = field.name_value();
let typ = field.make_type();
let _ = write!(generics, "{typ},");
let _ = write!(generics_product, "{typ}::Product,");
field.bounds(&mut bounds);
field.build(&mut build);
field.update(&mut update);
field.declare(&mut declare);
let _ = write!(vars, "{name},");
}
let mut declare_els = String::new();
for (jsfn, el) in self.js.functions.iter().zip(self.els) {
let JsFunction { name, constructor, args } = jsfn;
let _ = write!(declare_els, "{el}: ::kobold::dom::Element,");
let args = args
.iter()
.map(|a| {
let mut temp = ArrayString::<8>::new();
let _ = write!(temp, "{}.js()", a.name);
temp
})
.join(",");
let _ = write!(
build,
"let {el} = ::kobold::dom::{constructor}({name}({args}));"
);
let _ = write!(vars, "{el},");
}
block((
"\
use ::kobold::{Mountable as _};\
use ::kobold::attribute::{AttributeProduct as _};\
use ::kobold::reexport::wasm_bindgen;\
",
self.js,
format_args!(
"\
struct Transient <{generics}> {{\
{declare}\
}}\
\
struct TransientProduct <{generics}> {{\
{declare}\
{declare_els}\
}}\
\
impl<{abi_lifetime}{generics}> ::kobold::Html for Transient<{generics}>\
where \
{bounds}\
{{\
type Product = TransientProduct<{generics_product}>;\
\
fn build(self) -> Self::Product {{\
{build}\
\
TransientProduct {{\
{vars}\
}}\
}}\
\
fn update(self, p: &mut Self::Product) {{\
{update}\
}}\
}}\
\
impl<{generics}> ::kobold::Mountable for TransientProduct<{generics}>\
where \
Self: 'static,\
{{\
fn el(&self) -> &::kobold::dom::Element {{\
&self.e0\
}}\
}}\
\
Transient\
"
),
block(each(self.fields.iter().map(Field::invoke))),
))
.tokenize()
}
}
#[derive(Default)]
pub struct JsModule {
pub functions: Vec<JsFunction>,
pub code: String,
}
impl Tokenize for JsModule {
fn tokenize(self) -> TokenStream {
if self.functions.is_empty() {
return TokenStream::new();
}
(
'#',
group(
'[',
(
"wasm_bindgen::prelude::wasm_bindgen",
group('(', ("inline_js = ", string(&self.code))),
),
),
"extern \"C\"",
block(each(self.functions)),
)
.tokenize()
}
}
impl Debug for JsModule {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Debug::fmt(&self.functions, f)?;
write!(f, "\ncode: ({})", self.code)
}
}
#[derive(Debug)]
pub struct JsFunction {
pub name: JsFnName,
pub constructor: &'static str,
pub args: Vec<JsArgument>,
}
impl Tokenize for JsFunction {
fn tokenize(self) -> TokenStream {
let name = self.name;
(
call(format_args!("fn {name}"), each(self.args)),
"-> ::kobold::reexport::web_sys::Node;",
)
.tokenize()
}
}
#[derive(Debug)]
pub struct JsArgument {
pub name: Short,
pub abi: &'static str,
}
impl JsArgument {
pub fn new(name: Short) -> Self {
JsArgument {
name,
abi: "&wasm_bindgen::JsValue",
}
}
pub fn with_abi(name: Short, abi: &'static str) -> Self {
JsArgument { name, abi }
}
}
impl Tokenize for JsArgument {
fn tokenize(self) -> TokenStream {
(ident(&self.name), ':', self.abi, ',').tokenize()
}
fn tokenize_in(self, stream: &mut TokenStream) {
(ident(&self.name), ':', self.abi, ',').tokenize_in(stream)
}
}
pub enum Field {
Html {
name: Short,
value: TokenStream,
},
Attribute {
name: Short,
el: Short,
abi: Abi,
value: TokenStream,
},
}
pub enum Abi {
Owned(&'static str),
Borrowed(&'static str),
}
impl Deref for Abi {
type Target = str;
fn deref(&self) -> &str {
match self {
Abi::Owned(abi) => abi,
Abi::Borrowed(abi) => abi,
}
}
}
impl Debug for Field {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Field::Html { name, value } => {
write!(f, "{name} <Html>: {value}")
}
Field::Attribute {
name,
el,
abi,
value,
} => {
let abi = abi.deref();
write!(f, "{name} <Attribute({abi} -> {el})>: {value}")
}
}
}
}
impl Field {
fn borrows(&self) -> bool {
matches!(
self,
Field::Attribute {
abi: Abi::Borrowed(_),
..
}
)
}
fn name_value(&self) -> (&Short, &TokenStream) {
match self {
Field::Html { name, value } | Field::Attribute { name, value, .. } => (name, value),
}
}
fn make_type(&self) -> Short {
let (name, _) = self.name_value();
let mut typ = *name;
typ.make_ascii_uppercase();
typ
}
fn bounds(&self, buf: &mut String) {
match self {
Field::Html { name, .. } => {
let mut typ = *name;
typ.make_ascii_uppercase();
let _ = write!(buf, "{typ}: ::kobold::Html,");
}
Field::Attribute { name, abi, .. } => {
let mut typ = *name;
typ.make_ascii_uppercase();
let abi = abi.deref();
let _ = write!(
buf,
"{typ}: ::kobold::attribute::Attribute,\
{typ}::Product: ::kobold::attribute::AttributeProduct<Abi = {abi}>,"
);
}
}
}
fn declare(&self, buf: &mut String) {
let (name, _) = self.name_value();
let typ = self.make_type();
let _ = write!(buf, "{name}: {typ},");
}
fn build(&self, buf: &mut String) {
let (name, _) = self.name_value();
let _ = write!(buf, "let {name} = self.{name}.build();");
}
fn update(&self, buf: &mut String) {
match self {
Field::Html { name, .. } => {
let _ = write!(buf, "self.{name}.update(&mut p.{name});");
}
Field::Attribute { name, el, .. } => {
let _ = write!(buf, "self.{name}.update(&mut p.{name}, &p.{el});");
}
}
}
fn invoke(&self) -> impl Tokenize {
let (name, value) = self.name_value();
(ident(name), ':', value.clone(), ',')
}
}
pub struct JsString(pub Literal);
impl Display for JsString {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let stringified = self.0.to_string();
match stringified.chars().next() {
Some('"' | '\'') => f.write_str(&stringified),
_ => write!(f, "\"{stringified}\""),
}
}
}