use std::fmt::{Arguments, Write};
use crate::dom::{Attribute, AttributeValue, CssValue, HtmlElement};
use crate::gen::{append, Abi, DomNode, Generator, IntoGenerator, JsArgument, Short};
use crate::itertools::IteratorExt;
use crate::parse::IdentExt;
use crate::tokenize::prelude::*;
pub struct JsElement {
pub tag: String,
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();
gen.set_js_type(element_js_type(&self.name));
let mut el = JsElement {
tag: self.name,
var,
code: String::new(),
args: Vec::new(),
hoisted: self.classes.iter().any(CssValue::is_expression),
};
match self.classes.len() {
0 => (),
1 => match self.classes.remove(0) {
CssValue::Literal(class) => writeln!(el, "{var}.className={class};"),
CssValue::Expression(expr) => {
let class = gen.add_attribute(
el.var,
Abi::Borrowed("&'abi str"),
call("::kobold::attribute::ClassName::from", expr.stream),
);
el.args.push(JsArgument::with_abi(class, "&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});");
}
for class in self.classes {
if let CssValue::Expression(expr) = class {
let class = gen.add_attribute(
el.var,
Abi::Borrowed("&'abi str"),
call("::kobold::attribute::Class::from", expr.stream),
);
el.args.push(JsArgument::with_abi(class, "&str"));
writeln!(el, "{class} && {var}.classList.add({class});");
}
}
}
}
for Attribute { name, value } in self.attributes {
match value {
AttributeValue::Literal(value) => {
writeln!(el, "{var}.setAttribute(\"{name}\",{value});");
}
AttributeValue::Boolean(value) => {
writeln!(el, "{var}.{name}={value};");
}
AttributeValue::Expression(expr) => name.with_str(|attr| {
let arg = if attr.starts_with("on") && attr.len() > 2 {
let event = &attr[2..];
let target = element_js_type(el.tag.as_str());
let event_type = event_js_type(event);
let callback = call(
format_args!(
"::kobold::event::event_handler::<\
::kobold::event::{event_type}<\
::kobold::reexport::web_sys::{target}\
>\
>"
),
expr.stream,
);
let value = gen.add_expression(callback);
writeln!(el, "{var}.addEventListener(\"{event}\",{value});");
JsArgument::new(value)
} else if attr == "checked" {
el.hoisted = true;
let value = gen.add_attribute(
var,
Abi::Owned("bool"),
call("::kobold::attribute::Checked", expr.stream),
);
writeln!(el, "{var}.{attr}={value};");
JsArgument::with_abi(value, "bool")
} else {
let value = gen.add_expression(call(
"::kobold::attribute::AttributeNode::new",
(string(attr), ',', expr.stream),
));
writeln!(el, "{var}.setAttributeNode({value});");
JsArgument::new(value)
};
el.args.push(arg);
}),
};
}
if let Some(children) = self.children {
let append = append(gen, &mut el.code, &mut el.args, children);
writeln!(el, "{var}.{append};");
}
DomNode::Element(el)
}
}
#[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",
}
}