use indexmap::IndexMap;
pub use simple_rsx_macros::{component, either, rsx};
use std::fmt::Display;
pub trait Attribute {
fn value(&self) -> String;
}
pub trait OptionAttribute {
fn value(&self) -> String;
}
impl<T: ToString> Attribute for T {
fn value(&self) -> String {
self.to_string()
}
}
impl<T: ToString> OptionAttribute for Option<T> {
fn value(&self) -> String {
match self {
Some(t) => t.to_string(),
None => String::new(),
}
}
}
#[derive(Clone)]
pub struct Element {
tag: String,
attributes: IndexMap<String, String>,
children: Vec<Node>,
}
impl Element {
pub fn new(tag: &str) -> Node {
Node::Element(Element {
tag: tag.to_string(),
attributes: IndexMap::new(),
children: Vec::new(),
})
}
pub fn set_attribute(&mut self, name: &str, value: impl Attribute) {
self.attributes.insert(name.to_string(), value.value());
}
pub fn append_child(&mut self, node: Node) {
self.children.push(node);
}
}
impl Node {
pub fn as_element_mut(&mut self) -> Option<&mut Element> {
match self {
Node::Element(el) => Some(el),
_ => None,
}
}
pub fn append_child(&mut self, node: Node) {
if let Node::Element(el) = self {
el.children.push(node);
}
}
}
pub trait Component {
type Props;
fn render(props: Self::Props) -> Node;
}
#[derive(Clone)]
pub enum Node {
Element(Element),
Text(String),
Fragment(Vec<Node>),
Comment(String),
}
impl From<String> for Node {
fn from(value: String) -> Self {
Node::Text(value)
}
}
impl From<&String> for Node {
fn from(value: &String) -> Self {
Node::Text(value.to_string())
}
}
impl From<&str> for Node {
fn from(value: &str) -> Self {
Node::Text(value.to_string())
}
}
impl From<&&str> for Node {
fn from(value: &&str) -> Self {
Node::Text(value.to_string())
}
}
impl<T: ToString> From<Vec<T>> for Node {
fn from(value: Vec<T>) -> Self {
Node::Fragment(
value
.into_iter()
.map(|t| Node::Text(t.to_string()))
.collect(),
)
}
}
impl<T: ToString> From<Option<T>> for Node {
fn from(value: Option<T>) -> Self {
match value {
Some(t) => Node::Text(t.to_string()),
_ => Node::Text("".to_string()),
}
}
}
impl From<&Vec<String>> for Node {
fn from(value: &Vec<String>) -> Self {
Node::Fragment(
value
.iter()
.map(|item| Node::Text(item.to_string()))
.collect(),
)
}
}
impl From<i32> for Node {
fn from(value: i32) -> Self {
Node::Text(value.to_string())
}
}
impl From<u32> for Node {
fn from(value: u32) -> Self {
Node::Text(value.to_string())
}
}
impl From<u64> for Node {
fn from(value: u64) -> Self {
Node::Text(value.to_string())
}
}
impl FromIterator<u32> for Node {
fn from_iter<T: IntoIterator<Item = u32>>(iter: T) -> Self {
let mut result = Vec::new();
for item in iter {
result.push(Node::Text(item.to_string()));
}
Node::Fragment(result)
}
}
impl FromIterator<u64> for Node {
fn from_iter<T: IntoIterator<Item = u64>>(iter: T) -> Self {
let mut result = Vec::new();
for item in iter {
result.push(Node::Text(item.to_string()));
}
Node::Fragment(result)
}
}
impl FromIterator<i32> for Node {
fn from_iter<T: IntoIterator<Item = i32>>(iter: T) -> Self {
let mut result = Vec::new();
for item in iter {
result.push(Node::Text(item.to_string()));
}
Node::Fragment(result)
}
}
impl From<f32> for Node {
fn from(value: f32) -> Self {
Node::Text(value.to_string())
}
}
impl From<bool> for Node {
fn from(value: bool) -> Self {
Node::Text(value.to_string())
}
}
impl<I, F, R> From<std::iter::Map<I, F>> for Node
where
I: Iterator,
F: FnMut(I::Item) -> R,
R: Into<Node>,
Vec<Node>: FromIterator<R>,
{
fn from(iter: std::iter::Map<I, F>) -> Self {
let nodes: Vec<Node> = iter.collect();
Node::from(nodes)
}
}
impl Display for Node {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Node::Element(el) => {
write!(f, "<{}", el.tag)?;
for (key, value) in &el.attributes {
write!(f, " {}=\"{}\"", key, value)?;
}
write!(f, ">")?;
for child in &el.children {
write!(f, "{}", child)?;
}
write!(f, "</{}>", el.tag)?;
Ok(())
}
Node::Text(text) => {
write!(f, "{}", text)?;
Ok(())
}
Node::Fragment(nodes) => {
for node in nodes {
write!(f, "{}", node)?;
}
Ok(())
}
Node::Comment(comment) => {
write!(f, "<!--{}-->", comment)?;
Ok(())
}
}
}
}
macro_rules! derive_elements {
(
$(
$(#[$tag_meta:meta])*
$tag:ident {
$(
$(#[$attr_meta:meta])*
$attr_name:ident : $attr_value:ty
),* $(,)?
}
)*
) => {
$(
#[allow(non_camel_case_types)]
$(#[$tag_meta])*
pub struct $tag;
paste::paste! {
#[derive(Default)]
#[allow(non_snake_case)]
pub struct [<HTML $tag:camel Element Props>] {
pub children: Vec<Node>,
pub id: String,
pub key: String,
pub class: String,
pub style: String,
pub title: Option<String>,
pub width: Option<String>,
pub height: Option<String>,
pub draggable: bool,
pub hidden: bool,
pub accesskey: String,
pub contenteditable: bool,
pub dir: String,
pub tabindex: Option<i32>,
pub spellcheck: bool,
pub lang: String,
pub translate: bool,
pub autocapitalize: String,
pub role: String,
pub r#data: std::collections::HashMap<String, String>,
pub aria_current: String,
pub aria_label: Option<String>,
pub aria_labelledby: Option<String>,
pub aria_describedby: Option<String>,
pub aria_expanded: bool,
pub aria_selected: bool,
pub aria_checked: String,
pub aria_hidden: bool,
pub aria_haspopup: String,
pub aria_role: String,
$(
pub $attr_name: $attr_value,
)*
}
impl [<HTML $tag:camel Element Props>] {
fn to_attributes(&self) -> IndexMap<String, String> {
#[allow(unused_mut)]
let mut attributes = IndexMap::new();
$(
if !self.$attr_name.value().is_empty() {
let mut key = stringify!($attr_name);
if let Some(last_char) = key.chars().last() {
if last_char == '_' {
key = &key[..key.len() - 1];
}
}
attributes.insert(key.replace('_', "-"), self.$attr_name.value());
}
)*
if !self.id.value().is_empty() {
attributes.insert("id".to_string(), self.id.value());
}
if !self.class.value().is_empty() {
attributes.insert("class".to_string(), self.class.value());
}
if !self.style.value().is_empty() {
attributes.insert("style".to_string(), self.style.value());
}
if !self.title.value().is_empty() {
attributes.insert("title".to_string(), self.title.value());
}
if self.draggable {
attributes.insert("draggable".to_string(), "true".to_string());
}
if self.hidden {
attributes.insert("hidden".to_string(), "true".to_string());
}
if !self.accesskey.value().is_empty() {
attributes.insert("accesskey".to_string(), self.accesskey.value());
}
if self.contenteditable {
attributes.insert("contenteditable".to_string(), "true".to_string());
}
if !self.dir.value().is_empty() {
attributes.insert("dir".to_string(), self.dir.value());
}
if let Some(tabindex) = self.tabindex {
attributes.insert("tabindex".to_string(), tabindex.to_string());
}
if self.spellcheck {
attributes.insert("spellcheck".to_string(), "true".to_string());
}
if !self.lang.value().is_empty() {
attributes.insert("lang".to_string(), self.lang.value());
}
if self.translate {
attributes.insert("translate".to_string(), "true".to_string());
}
if !self.autocapitalize.value().is_empty() {
attributes.insert("autocapitalize".to_string(), self.autocapitalize.value());
}
if !self.role.value().is_empty() {
attributes.insert("role".to_string(), self.role.value());
}
if !self.aria_current.value().is_empty() {
attributes.insert("aria-current".to_string(), self.aria_current.value());
}
if !self.aria_label.value().is_empty() {
attributes.insert("aria-label".to_string(), self.aria_label.value());
}
if !self.aria_labelledby.value().is_empty() {
attributes.insert("aria-labelledby".to_string(), self.aria_labelledby.value());
}
if !self.aria_describedby.value().is_empty() {
attributes.insert("aria-describedby".to_string(), self.aria_describedby.value());
}
if self.aria_expanded {
attributes.insert("aria-expanded".to_string(), "true".to_string());
}
if self.aria_selected {
attributes.insert("aria-selected".to_string(), "true".to_string());
}
if !self.aria_checked.value().is_empty() {
attributes.insert("aria-checked".to_string(), self.aria_checked.value());
}
if self.aria_hidden {
attributes.insert("aria-hidden".to_string(), "true".to_string());
}
if !self.aria_haspopup.value().is_empty() {
attributes.insert("aria-haspopup".to_string(), self.aria_haspopup.value());
}
if !self.aria_role.value().is_empty() {
attributes.insert("aria-role".to_string(), self.aria_role.value());
}
for (key, value) in &self.r#data {
if key.starts_with("data_") {
attributes.insert(key.replace("_", "-"), value.clone());
} else {
attributes.insert(format!("data-{}", key), value.clone());
}
}
attributes
}
}
impl Component for $tag {
type Props = [<HTML $tag:camel Element Props>];
fn render(props: Self::Props) -> Node {
Node::Element(Element {
tag: stringify!($tag).to_string(),
attributes: props.to_attributes(),
children: props.children,
})
}
}
}
)*
};
}
pub mod elements {
use super::*;
derive_elements! {
html {
}
body {
}
head {
}
title {
}
meta {
charset: String,
http_equiv: String,
content: String,
name: String,
property: String,
}
style {
}
script {
src: String,
type_: String,
language: String,
charset: String,
defer: bool,
async_: bool,
}
link {
rel: String,
href: String,
type_: String,
charset: String,
crossorigin: String,
referrerpolicy: String,
}
div {
}
p {
}
span {
}
a {
href: String,
target: String,
rel: String,
download: String,
hreflang: String,
type_: String,
media: String,
referrerpolicy: String,
ping: String,
}
h1 {
}
h2 {
}
h3 {
}
h4 {
}
h5 {
}
h6 {
}
img {
src: String,
alt: String,
loading: String,
}
br {}
hr {
}
ul {
type_: String,
}
li {
value: Option<i32>,
}
ol {
type_: String,
start: i32,
reversed: bool,
}
table {
border: i32,
cellpadding: i32,
cellspacing: i32,
}
tr {
}
td {
colspan: i32,
rowspan: i32,
headers: String,
scope: String,
}
th {
colspan: i32,
rowspan: i32,
headers: String,
scope: String,
}
tbody {
}
thead {
}
tfoot {
}
form {
action: String,
method: String,
target: String,
enctype: String,
novalidate: bool,
autocomplete: String,
accept: String,
name: String,
}
input {
type_: String,
placeholder: String,
required: bool,
value: String,
name: String,
disabled: bool,
readonly: bool,
min: String,
max: String,
pattern: String,
autocomplete: String,
}
textarea {
placeholder: String,
required: bool,
value: String,
rows: i32,
cols: i32,
name: String,
disabled: bool,
readonly: bool,
maxlength: i32,
}
button {
type_: String,
value: String,
disabled: bool,
name: String,
formaction: String,
formmethod: String,
}
select {
multiple: bool,
disabled: bool,
value: String,
name: String,
size: i32,
required: bool,
}
option {
value: String,
selected: bool,
disabled: bool,
}
label {
for_: String,
}
iframe {
src: String,
frameborder: String,
allow: String,
allowfullscreen: bool,
sandbox: String,
}
video {
src: String,
controls: bool,
autoplay: bool,
loop_: bool,
poster: String,
muted: bool,
preload: String,
playsinline: bool,
}
audio {
src: String,
controls: bool,
autoplay: bool,
loop_: bool,
muted: bool,
preload: String,
}
source {
src: String,
type_: String,
media: String,
}
canvas {
}
svg {
viewBox: String,
preserve_aspect_ratio: String,
xmlns: String,
fill: String,
stroke: String,
stroke_width: String,
stroke_linecap: String,
stroke_linejoin: String,
stroke_miterlimit: String,
stroke_dasharray: String,
stroke_dashoffset: String,
stroke_opacity: String,
fill_opacity: String,
}
path {
d: String,
fill: String,
stroke: String,
stroke_width: String,
stroke_linecap: String,
stroke_linejoin: String,
stroke_miterlimit: String,
stroke_dasharray: String,
stroke_dashoffset: String,
stroke_opacity: String,
fill_opacity: String,
}
rect {
x: String,
y: String,
rx: String,
ry: String,
fill: String,
stroke: String,
stroke_width: String,
}
circle {
cx: String,
cy: String,
r: String,
fill: String,
stroke: String,
stroke_width: String,
}
ellipse {
cx: String,
cy: String,
rx: String,
ry: String,
fill: String,
stroke: String,
stroke_width: String,
}
line {
x1: String,
y1: String,
x2: String,
y2: String,
stroke: String,
stroke_width: String,
stroke_linecap: String,
stroke_dasharray: String,
}
polyline {
points: String,
fill: String,
stroke: String,
stroke_width: String,
stroke_linejoin: String,
}
polygon {
points: String,
fill: String,
stroke: String,
stroke_width: String,
fill_rule: String,
}
g {
transform: String,
fill: String,
stroke: String,
}
r#use {
href: String,
x: String,
y: String,
}
foreignObject {
x: String,
y: String,
}
defs {
}
linearGradient {
x1: String,
y1: String,
x2: String,
y2: String,
gradientUnits: String,
spreadMethod: String,
}
stop {
offset: String,
stop_color: String,
stop_opacity: String,
}
radialGradient {
cx: String,
cy: String,
r: String,
fx: String,
fy: String,
fr: String,
gradientUnits: String,
spreadMethod: String,
}
mask {
mask_units: String,
mask_content_units: String,
x: String,
y: String,
}
article {
}
aside {
}
details {
}
figcaption {
}
figure {
}
footer {
}
header {
}
main {
}
mark {
}
nav {
}
section {
}
summary {
}
time {
datetime: String,
pubdate: String,
}
wbr {
}
address {
}
bdi {
}
bdo {
}
cite {
}
dfn {
}
em {
}
i {
}
kbd {
}
meter {
value: String,
min: String,
max: String,
low: String,
high: String,
optimum: String,
}
output {
}
progress {
value: String,
max: String,
}
q {
}
rp {
}
rt {
}
ruby {
}
s {
}
samp {
}
small {
}
strong {
}
sub {
}
sup {
}
var {
}
template {
}
u {
}
noscript {
}
legend {
}
optgroup {
label: String,
}
dialog {
open: String,
}
blockquote {
}
dd {
}
dl {
}
dt {
}
base {
href: String,
target: String,
}
}
}