use std::fmt::{Arguments, Write};
use tokens::{Literal, TokenStream};
use crate::dom::{Attribute, AttributeValue, CssValue, ElementTag, HtmlElement};
use crate::gen::{append, DomNode, Generator, IntoGenerator, JsArgument, Short};
use crate::itertools::IteratorExt as _;
use crate::parse::{IdentExt, IteratorExt as _};
use crate::tokenize::prelude::*;
pub struct JsElement {
pub tag: ElementTag,
pub typ: &'static str,
pub var: Short,
pub code: String,
pub args: Vec<JsArgument>,
pub hoisted: bool,
}
impl JsElement {
fn write_fmt(&mut self, args: Arguments) {
let _ = self.code.write_fmt(args);
}
}
impl IntoGenerator for HtmlElement {
fn into_gen(mut self, gen: &mut Generator) -> DomNode {
let var = gen.names.next_el();
let typ = element_js_type(&self.name);
let mut el = JsElement {
tag: self.name,
typ,
var,
code: String::new(),
args: Vec::new(),
hoisted: false, };
match self.classes.len() {
0 => (),
1 => match self.classes.remove(0) {
CssValue::Literal(class) => writeln!(el, "{var}.className={class};"),
CssValue::Expression(expr) => {
el.hoisted = true;
let attr = Attr {
name: "ClassName",
abi: Some(InlineAbi::Str),
};
let class = gen
.add_field(expr.stream)
.attr(el.var, attr, attr.prop())
.name;
el.args.push(JsArgument::with_abi(class, InlineAbi::Str));
writeln!(el, "{var}.className={class};");
}
},
_ => {
let lit_count = self.classes.iter().map(CssValue::is_literal).count();
if lit_count > 0 {
let classes = self
.classes
.iter()
.filter_map(CssValue::as_literal)
.join(",");
writeln!(el, "{var}.classList.add({classes});");
}
let attr = Attr {
name: "Class",
abi: Some(InlineAbi::Str),
};
for class in self.classes {
if let CssValue::Expression(expr) = class {
el.hoisted = true;
let class = gen
.add_field(expr.stream)
.attr(el.var, attr, attr.prop())
.name;
el.args.push(JsArgument::with_abi(class, InlineAbi::Str));
writeln!(el, "{class} && {var}.classList.add({class});");
}
}
}
}
for Attribute { name, value } in self.attributes {
let attr_type = name.with_str(attribute_type);
match value {
AttributeValue::Literal(value) => {
writeln!(el, "{var}.setAttribute(\"{name}\",{value});");
}
AttributeValue::Boolean(value) => {
writeln!(el, "{var}.{name}={value};");
}
AttributeValue::Expression(mut expr) => match &attr_type {
AttributeType::Event(event) => {
let target = el.typ;
let coerce = if is_inline_closure(&mut expr.stream) {
call(
format_args!(
"::kobold::internal::fn_type_hint::<\
::kobold::event::{event}<\
::kobold::reexport::web_sys::{target}\
>,\
_,\
>"
),
expr.stream,
)
} else {
expr.stream
};
let value = gen.add_field(coerce).event(event, el.typ).name;
name.with_str(|name| {
writeln!(el, "{var}.addEventListener(\"{}\",{value});", &name[2..])
});
el.args.push(JsArgument::new(value))
}
AttributeType::Provided(attr) => {
el.hoisted = true;
let value = gen
.add_field(expr.stream)
.attr(var, *attr, attr.prop())
.name;
if let Some(abi) = attr.abi {
writeln!(el, "{var}.{name}={value};");
el.args.push(JsArgument::with_abi(value, abi))
}
}
AttributeType::Unknown => {
el.hoisted = true;
let prop = (name.with_str(Literal::string), ".into()").tokenize();
let attr = Attr::new("&AttributeName");
gen.add_field(expr.stream).attr(var, attr, prop);
}
},
};
match attr_type {
AttributeType::Event(event) => {
let target = el.typ;
gen.add_hint(
name.clone(),
format_args!(
"impl Fn(\
::kobold::event::{event}<\
::kobold::reexport::web_sys::{target}\
>\
) + 'static"
),
);
}
AttributeType::Provided(attr) => {
gen.add_attr_hint(name, "", attr.name);
}
AttributeType::Unknown => {
gen.add_attr_hint(name, "&'static", "AttributeName");
}
}
}
if let Some(children) = self.children {
let append = append(gen, &mut el.code, &mut el.args, children);
writeln!(el, "{var}.{append};");
}
DomNode::Element(el)
}
}
#[derive(Clone, Copy, Debug)]
pub enum InlineAbi {
Bool,
Str,
}
impl InlineAbi {
pub fn abi(self) -> &'static str {
match self {
InlineAbi::Bool => "bool",
InlineAbi::Str => "&str",
}
}
pub fn method(self) -> &'static str {
match self {
InlineAbi::Bool => ".into()",
InlineAbi::Str => ".as_ref()",
}
}
pub fn bound(self) -> &'static str {
match self {
InlineAbi::Bool => "+ Into<bool> + Copy",
InlineAbi::Str => "+ AsRef<str>",
}
}
}
#[derive(Clone, Copy)]
enum AttributeType {
Provided(Attr),
Event(&'static str),
Unknown,
}
#[derive(Debug, Clone, Copy)]
pub struct Attr {
pub name: &'static str,
pub abi: Option<InlineAbi>,
}
impl Attr {
const fn new(name: &'static str) -> Self {
Attr { name, abi: None }
}
pub fn as_parts(&self) -> (&str, &str) {
if self.name.starts_with('&') {
("&'static ", &self.name[1..])
} else {
("", self.name)
}
}
fn prop(&self) -> TokenStream {
format_args!("::kobold::attribute::{}", self.name).tokenize()
}
}
fn is_inline_closure(out: &mut TokenStream) -> bool {
let mut is_closure = false;
let mut stream = std::mem::replace(out, TokenStream::new()).parse_stream();
if let Some(tt) = stream.allow_consume("move") {
out.write(tt);
}
if let Some(tt) = stream.allow_consume('|') {
is_closure = true;
out.write(tt);
}
out.extend(stream);
is_closure
}
fn attribute_type(attr: &str) -> AttributeType {
if attr.starts_with("on") && attr.len() > 2 {
return AttributeType::Event(event_js_type(&attr[2..]));
}
let attr = match attr {
"checked" => Attr {
name: "Checked",
abi: Some(InlineAbi::Bool),
},
"href" => Attr {
name: "Href",
abi: Some(InlineAbi::Str),
},
"style" => Attr {
name: "Style",
abi: Some(InlineAbi::Str),
},
"value" => Attr {
name: "Value",
abi: None,
},
_ => return AttributeType::Unknown,
};
AttributeType::Provided(attr)
}
#[rustfmt::skip]
fn event_js_type(event: &str) -> &'static str {
match event {
"click"
| "dblclick"
| "mousedown"
| "mouseup"
| "mouseover"
| "mousemove"
| "mouseout"
| "mouseenter"
| "mouseleave" => "MouseEvent",
"keydown"
| "keyup"
| "keypress" => "KeyboardEvent",
_ => "Event",
}
}
fn element_js_type(tag: &str) -> &'static str {
match tag {
"a" => "HtmlLinkElement",
"canvas" => "HtmlCanvasElement",
"form" => "HtmlFormElement",
"img" => "HtmlImageElement",
"input" => "HtmlInputElement",
"option" => "HtmlOptionElement",
"select" => "HtmlSelectElement",
"textarea" => "HtmlTextAreaElement",
_ => "HtmlElement",
}
}