Refactored poem selection so that a static hashmap and vector are ceated as a prop that is passed to each component in order to grab poem data more efficiently and cleanly.

This commit is contained in:
Ada Werefox 2023-04-18 19:33:44 -05:00
parent db2d86c5ee
commit e593f46a35
12 changed files with 257 additions and 172 deletions

View File

@ -16,7 +16,8 @@ dioxus = "0.3.2"
markdown = "1.0.0-alpha.7"
dioxus-ssr = "0.3.0"
rust-embed = { version = "6.6.1" }
once_cell = "1.17.1"
[dependencies]
void-be = { path = "./void-be" }
rocket = { workspace = true }
rocket = { workspace = true }

File diff suppressed because one or more lines are too long

View File

@ -7,6 +7,7 @@ edition = "2021"
[dependencies]
void-fe = { path = "../void-fe" }
dioxus-ssr = { workspace = true }
once_cell = { workspace = true }
# If you're unsure about why we're depending on a release candidate,
# check the Rocket documentation:

View File

@ -8,15 +8,17 @@ extern crate rocket;
/// A module that handles the backend for the site.
pub mod web_app_backend {
use once_cell::sync::OnceCell;
use rocket::fs::FileServer;
use rocket::http::{Cookie, CookieJar};
use rocket::response::Redirect;
use rocket::{Build, Rocket};
use rocket_dyn_templates::{context, Template};
use void_fe::utils::prop_structs::PoemRequest;
use void_fe::utils::prop_structs::{PoemDatabase, VoidProps};
use void_fe::utils::user_prefs::*;
use void_fe::void_app::{self, VirtualDom};
use void_fe::utils::user_prefs::*;
static POEM_DATABASE: OnceCell<PoemDatabase> = OnceCell::new();
async fn get_user_prefs(cookies: &CookieJar<'_>) -> UserPrefs {
let user_theme = match cookies.get("theme") {
@ -74,7 +76,15 @@ pub mod web_app_backend {
#[get("/")]
async fn index(cookies: &CookieJar<'_>) -> Template {
let user_prefs = get_user_prefs(cookies).await;
let mut vdom = VirtualDom::new_with_props(void_app::HomePage, user_prefs);
let void_props = VoidProps {
slug: None,
poem_database: POEM_DATABASE
.get()
.expect("Poem database is not initialized")
.clone(),
user_prefs: user_prefs,
};
let mut vdom = VirtualDom::new_with_props(void_app::HomePage, void_props);
let _ = vdom.rebuild();
let output = dioxus_ssr::render(&vdom);
Template::render(
@ -120,7 +130,15 @@ pub mod web_app_backend {
#[get("/")]
async fn poem_list(cookies: &CookieJar<'_>) -> Template {
let user_prefs = get_user_prefs(cookies).await;
let mut vdom = VirtualDom::new_with_props(void_app::PoemListPage, user_prefs);
let void_props = VoidProps {
slug: None,
poem_database: POEM_DATABASE
.get()
.expect("Poem database is not initialized")
.clone(),
user_prefs: user_prefs,
};
let mut vdom = VirtualDom::new_with_props(void_app::PoemListPage, void_props);
let _ = vdom.rebuild();
let output = dioxus_ssr::render(&vdom);
Template::render(
@ -137,13 +155,15 @@ pub mod web_app_backend {
#[get("/<entry>")]
async fn poem(cookies: &CookieJar<'_>, entry: &str) -> Template {
let user_prefs = get_user_prefs(cookies).await;
let mut vdom = VirtualDom::new_with_props(
void_app::PoemPage,
PoemRequest {
slug: format!("{entry}"),
user_prefs,
},
);
let void_props = VoidProps {
slug: Some(entry.to_string()),
poem_database: POEM_DATABASE
.get()
.expect("Poem database is not initialized")
.clone(),
user_prefs: user_prefs,
};
let mut vdom = VirtualDom::new_with_props(void_app::PoemPage, void_props);
let _ = vdom.rebuild();
let output = dioxus_ssr::render(&vdom);
Template::render(
@ -159,6 +179,9 @@ pub mod web_app_backend {
/// This runs `rocket::build()` with the needed mounts and routes.
pub async fn build_rocket() -> Rocket<Build> {
let mut poem_database = PoemDatabase::new();
poem_database.build_poem_database().await;
POEM_DATABASE.set(poem_database).expect("Could not initialize poem database.");
rocket::build()
.mount("/images", FileServer::from("public/images"))
.mount("/styles", FileServer::from("public/styles"))

View File

@ -1,5 +1,5 @@
# im on repeat
[Audio reading](https://cloud.werefox.dev/s/dq5ccm5QqmMF4GB)
*Author's note: Unfortunately, I've lost the audio reading for this.*
Death is like a memory. Ceaseless, and comforting\
Knowing things will end has always helped me feel like I'm free\

View File

@ -1,5 +1,5 @@
# Iced Coffee
[Listen to the song on Soundcloud](https://soundcloud.com/alexis-werefox/iced-coffee/s-5SgBwPrLwyR)
<a href="https://soundcloud.com/alexis-werefox/iced-coffee/s-5SgBwPrLwyR">Listen to the song on Soundcloud</a>
*Iced coffee on a cold Winter's day* \
*Iced coffee bittersweet at the taste* \

View File

@ -1,5 +1,5 @@
use dioxus::prelude::*;
use crate::{components::void_buttons::NavigationButton, utils::user_prefs::{UserPrefs, ThemedComponent}};
use crate::{components::void_buttons::{ButtonGroup, NavigationButton}, utils::user_prefs::{UserPrefs, ThemedComponent}};
pub fn Footer(cx: Scope<UserPrefs>) -> Element {
cx.render(rsx! {
@ -13,7 +13,12 @@ fn MutantStandardFooter(cx: Scope<UserPrefs>) -> Element {
let user_font = cx.props.get_font_class();
cx.render(rsx!{
div { class: "p-4 flex flex-col space-y-4 mx-auto max-w-full justify-center ring-4 {user_theme} {user_font}",
NavigationButton { title: "⚙️ Settings".to_string(), slug: "/settings".to_string(), user_prefs: user_prefs }
ButtonGroup{
NavigationButton { title: "⚙️ Settings".to_string(), slug: "/settings".to_string(), user_prefs: user_prefs.clone() }
span { class: "font-nerd contents",
NavigationButton { title: " /src".to_string(), slug: "https://gitea.werefox.cafe/ada/void-werefox-cafe".to_string(), user_prefs: user_prefs }
}
}
div { class: "flex mx-auto max-w-full justify-center text-md text-center",
"This site uses Mutant Standard emoji, which are licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License"
}

View File

@ -2,11 +2,10 @@ use crate::utils::prop_structs::PageChildren;
use dioxus::prelude::*;
pub fn PageBase<'a>(cx: Scope<'a, PageChildren<'a>>) -> Element {
cx.render(rsx!{
cx.render(rsx! {
a { rel: "me", href: "https://yiff.life/@werefox", hidden: true, "Mastodon" }
div { class: "min-h-screen",
div { class: "container space-y-4 mx-auto p-4",
&cx.props.children
}
div { class: "container space-y-4 mx-auto p-4", &cx.props.children }
}
})
}

View File

@ -1,17 +1,17 @@
use crate::components::void_buttons::*;
use crate::components::void_title::*;
use crate::utils::{helpers, prop_structs::{PoemChildren, PoemData}, user_prefs::{UserPrefs, ThemedComponent} };
use crate::utils::prop_structs::VoidProps;
use crate::utils::{prop_structs::PoemChildren, user_prefs::ThemedComponent};
use dioxus::prelude::*;
pub fn PoemList(cx: Scope<UserPrefs>) -> Element {
let poem_list = helpers::get_poem_list();
pub fn PoemList(cx: Scope<VoidProps>) -> Element {
let poem_list = cx.props.poem_database.get_poem_list();
cx.render(rsx! {
ul { class: "flex flex-col space-y-4",
poem_list.into_iter().map(|p| {
let user_prefs = cx.props.clone();
let slug = format!("/poems/{}", p.1);
let slug = format!("/poems/{}", p.0);
rsx!{
NavigationButton { title: p.0, slug: slug, user_prefs: user_prefs.clone() }
NavigationButton { title: p.1, slug: slug, user_prefs: cx.props.user_prefs.clone() }
}
})
}
@ -26,25 +26,41 @@ pub fn MakePoem<'a>(cx: Scope<'a, PoemChildren<'a>>) -> Element {
})
}
pub fn GetPoem(cx: Scope<PoemData>) -> Element {
let slug = String::from(cx.props.slug.clone().expect("No slug specified."));
let (title, content, creation_date) = helpers::get_poem(slug.clone());
pub fn GetPoem(cx: Scope<VoidProps>) -> Element {
let poem_database = &cx.props.poem_database;
let slug = String::from(&cx.props.slug.clone().expect("No slug specified."));
let poem_struct = poem_database.get_poem(slug.clone());
cx.render(rsx! {
Title { title: title, is_html: true, user_prefs: cx.props.user_prefs.clone() }
Title { title: poem_struct.title.clone(), is_html: true, user_prefs: cx.props.user_prefs.clone() }
MakePoem{
PoemContent { content: content, creation_date: creation_date, user_prefs: cx.props.user_prefs.clone() }
PoemContent { slug: slug, poem_database: poem_database.clone(), user_prefs: cx.props.user_prefs.clone() }
}
})
}
pub fn PoemContent(cx: Scope<PoemData>) -> Element {
pub fn PoemContent(cx: Scope<VoidProps>) -> Element {
let (user_theme, user_font) = cx.props.user_prefs.get_pref_classes(ThemedComponent::Card);
let content = cx.props.content.clone().expect("No content specified.");
let creation_date = cx
let slug = cx
.props
.slug
.as_ref()
.expect("Received slug for poem selection.");
let content = &cx
.props
.poem_database
.poem_hashmap
.get(slug)
.expect("Grabbed poem stuct from databse.")
.content
.clone();
let creation_date = &cx
.props
.poem_database
.poem_hashmap
.get(slug)
.expect("Grabbed poem struct from database.")
.creation_date
.clone()
.expect("No creation date specified.");
.clone();
#[cfg(any(target_family = "unix", target_family = "windows"))]
return cx.render(rsx! {
div { class: "flex p-2 mx-auto max-w-full justify-center",

View File

@ -62,19 +62,20 @@ pub mod void_app {
}
/// Renders the app and returns the rendered Element.
pub fn HomePage(cx: Scope<UserPrefs>) -> Element {
let user_prefs = cx.props.clone();
pub fn HomePage(cx: Scope<VoidProps>) -> Element {
let poem_database = &cx.props.poem_database;
let user_prefs = cx.props.user_prefs.clone();
let (user_theme, user_font) = user_prefs.get_pref_classes(ThemedComponent::Page);
let title = "A Letter to the Void".to_string();
let page_title = "A Letter to the Void".to_string();
cx.render(rsx!{
div { class: "{user_theme} {user_font}",
PageBase {
Title { title: title, is_html: false, user_prefs: user_prefs.clone() }
Title { title: page_title, is_html: false, user_prefs: user_prefs.clone() }
RenderContent { content: helpers::get_homepage_paragraph(), user_prefs: user_prefs.clone() }
ButtonGroup {
NavigationButton { title: "See Latest Entry".to_string(), slug: helpers::get_latest_entry("".to_string()), user_prefs: user_prefs.clone() }
NavigationButton { title: "See Oldest Entry".to_string(), slug: helpers::get_oldest_entry("".to_string()), user_prefs: user_prefs.clone() }
NavigationButton { title: "See A Random Entry".to_string(), slug: helpers::get_random_entry(), user_prefs: user_prefs.clone() }
NavigationButton { title: "See Latest Entry".to_string(), slug: poem_database.get_latest_entry("".to_string()), user_prefs: user_prefs.clone() }
NavigationButton { title: "See Oldest Entry".to_string(), slug: poem_database.get_oldest_entry("".to_string()), user_prefs: user_prefs.clone() }
NavigationButton { title: "See A Random Entry".to_string(), slug: poem_database.get_random_entry(), user_prefs: user_prefs.clone() }
NavigationButton { title: "See All Entries".to_string(), slug: "/poems".to_string(), user_prefs: user_prefs.clone() }
}
Footer { theme: user_prefs.clone().get_theme(), font: user_prefs.get_font() }
@ -84,15 +85,16 @@ pub mod void_app {
}
/// Renders the app and returns the rendered Element.
pub fn PoemListPage(cx: Scope<UserPrefs>) -> Element {
let user_prefs = cx.props.clone();
pub fn PoemListPage(cx: Scope<VoidProps>) -> Element {
let poem_database = &cx.props.poem_database;
let user_prefs = cx.props.user_prefs.clone();
let (user_theme, user_font) = user_prefs.get_pref_classes(ThemedComponent::Page);
cx.render(rsx! {
div { class: "{user_theme} {user_font}",
PageBase {
Title { title: "A Letter to the Void".to_string(), is_html: false, user_prefs: user_prefs.clone() }
BackToHomePage { theme: user_prefs.clone().get_theme(), font: user_prefs.clone().get_font() }
PoemList { theme: user_prefs.clone().get_theme(), font: user_prefs.clone().get_font() }
PoemList { poem_database: poem_database.clone(), user_prefs: user_prefs.clone() }
BackToHomePage { theme: user_prefs.clone().get_theme(), font: user_prefs.clone().get_font() }
Footer { theme: user_prefs.clone().get_theme(), font: user_prefs.get_font() }
}
@ -100,11 +102,12 @@ pub mod void_app {
})
}
pub fn PoemPage(cx: Scope<PoemRequest>) -> Element {
pub fn PoemPage(cx: Scope<VoidProps>) -> Element {
let poem_database = &cx.props.poem_database;
let user_prefs = cx.props.user_prefs.clone();
let (user_theme, user_font) = user_prefs.get_pref_classes(ThemedComponent::Page);
#[cfg(any(target_family = "unix", target_family = "windows"))]
let slug = cx.props.slug.clone();
let slug = &cx.props.slug.as_ref().expect("A slug was given in the pops.").clone();
#[cfg(target_family = "wasm")]
let slug = String::from(
dioxus_router::use_route(cx)
@ -115,13 +118,13 @@ pub mod void_app {
div { class: "{user_theme} {user_font}",
PageBase {
BackToHomePage { theme: user_prefs.clone().get_theme(), font: user_prefs.clone().get_font() }
GetPoem { slug: slug.clone(), user_prefs: user_prefs.clone() }
GetPoem { slug: slug.clone(), poem_database: poem_database.clone(), user_prefs: user_prefs.clone() }
ButtonGroup {
NavigationButton { title: "Oldest".to_string(), slug: helpers::get_oldest_entry(slug.clone()), user_prefs: user_prefs.clone() }
NavigationButton { title: "Previous".to_string(), slug: helpers::get_previous_entry(slug.clone()), user_prefs: user_prefs.clone() }
NavigationButton { title: "Random".to_string(), slug: helpers::get_random_entry(), user_prefs: user_prefs.clone() }
NavigationButton { title: "Next".to_string(), slug: helpers::get_next_entry(slug.clone()), user_prefs: user_prefs.clone() }
NavigationButton { title: "Latest".to_string(), slug: helpers::get_latest_entry(slug.clone()), user_prefs: user_prefs.clone() }
NavigationButton { title: "Oldest".to_string(), slug: poem_database.get_oldest_entry(slug.clone()), user_prefs: user_prefs.clone() }
NavigationButton { title: "Previous".to_string(), slug: poem_database.get_previous_entry(slug.clone()), user_prefs: user_prefs.clone() }
NavigationButton { title: "Random".to_string(), slug: poem_database.get_random_entry(), user_prefs: user_prefs.clone() }
NavigationButton { title: "Next".to_string(), slug: poem_database.get_next_entry(slug.clone()), user_prefs: user_prefs.clone() }
NavigationButton { title: "Latest".to_string(), slug: poem_database.get_latest_entry(slug.clone()), user_prefs: user_prefs.clone() }
}
BackToHomePage { theme: user_prefs.clone().get_theme(), font: user_prefs.clone().get_font() }
Footer { theme: user_prefs.clone().get_theme(), font: user_prefs.get_font() }

View File

@ -1,15 +1,150 @@
use markdown::Options;
use rand::seq::SliceRandom;
use rust_embed::RustEmbed;
use std::collections::VecDeque;
use std::collections::{HashMap, VecDeque};
use super::prop_structs::{PoemDatabase, PoemStruct};
#[derive(RustEmbed)]
#[folder = "data/other"]
struct OtherData;
#[derive(RustEmbed)]
#[folder = "data/poems"]
struct Poems;
pub struct Poems;
impl PoemDatabase {
pub fn new() -> PoemDatabase {
PoemDatabase {
poem_list: Vec::<(String, String)>::new(),
poem_hashmap: HashMap::<String, PoemStruct>::new(),
}
}
// There's no need to actually make a database yet, but maybe in the future...
pub async fn build_poem_database(&mut self) {
for p in Poems::iter() {
let filename = p.to_string();
let poem_content = Poems::get(&filename).expect("Found poem {filename:?}");
let mut poem_to_str = std::str::from_utf8(poem_content.data.as_ref())
.expect("Poem is valid UT8.")
.lines();
let poem_title = poem_to_str.next().expect("No title specified.");
let poem_content = poem_to_str.into_iter().collect::<Vec<&str>>().join("\n");
let poem_title_to_html_string =
markdown::to_html_with_options(poem_title, &Options::gfm()).unwrap();
let poem_content_to_html_string =
markdown::to_html_with_options(poem_content.as_str(), &Options::gfm()).unwrap();
let mut split_filename = filename.trim_end_matches(".md").split("_");
let creation_date = split_filename.next().expect("Obtained creation date");
let slug = split_filename.next().expect("Obtained slug");
self.poem_list
.push((creation_date.to_string(), slug.to_string()));
let poem_struct = PoemStruct {
title: poem_title_to_html_string.to_string(),
content: poem_content_to_html_string,
creation_date: creation_date.to_string(),
};
self.poem_hashmap.insert(slug.to_string(), poem_struct);
}
self.poem_list.sort_by_key(|k| k.0.clone())
}
pub fn get_poem(&self, slug: String) -> PoemStruct {
self.poem_hashmap
.get(slug.as_str())
.expect("Grabbed poem from database")
.clone()
}
pub fn get_poem_list(&self) -> Vec<(String, String)> {
self.poem_list
.iter()
.map(|s| {
(
s.1.clone(),
self.poem_hashmap
.get(&s.1)
.expect("Got poemstruct from database")
.title
.clone(),
)
})
.collect::<Vec<(String, String)>>()
.clone()
}
pub fn get_oldest_entry(&self, current: String) -> String {
let mut poem_list = VecDeque::from_iter(self.poem_list.iter());
let oldest = poem_list
.pop_front()
.expect("There is an entry in this list of poems.")
.1
.clone();
if current == oldest {
return format!("/poems/{current}#");
}
format!("/poems/{oldest}")
}
pub fn get_latest_entry(&self, current: String) -> String {
let mut poem_list = self.poem_list.clone();
let latest = poem_list
.pop()
.expect("There is an entry in this list of poems.")
.1
.clone();
if current == latest {
return format!("/poems/{current}#");
}
format!("/poems/{latest}")
}
pub fn get_previous_entry(&self, current: String) -> String {
let poem_list = self.poem_list.clone();
match poem_list.iter().enumerate().find_map(|(index, p)| {
if p.1 == current {
if index != 0 {
Some(poem_list[index - 1].1.clone())
} else {
None
}
} else {
None
}
}) {
Some(entry) => format!("/poems/{entry}"),
None => format!("/poems/{current}"),
}
}
pub fn get_next_entry(&self, current: String) -> String {
let poem_list = self.poem_list.clone();
match poem_list.iter().enumerate().find_map(|(index, p)| {
if p.1 == current {
if index != poem_list.len() - 1 {
Some(poem_list[index + 1].1.clone())
} else {
None
}
} else {
None
}
}) {
Some(entry) => format!("/poems/{entry}"),
None => format!("/poems/{current}"),
}
}
pub fn get_random_entry(&self) -> String {
let poem_list = self.poem_list.clone();
let mut rng = rand::thread_rng();
let random_entry = poem_list
.choose(&mut rng)
.expect("Got a valid entry")
.1
.clone();
format!("/poems/{random_entry}")
}
}
pub fn get_homepage_paragraph() -> String {
let homepage_paragraph_content =
@ -21,112 +156,3 @@ pub fn get_homepage_paragraph() -> String {
markdown::to_html_with_options(homepage_paragraph_to_string, &Options::gfm()).unwrap();
test
}
pub fn get_poem(slug: String) -> (String, String, String) {
let filename = String::from(String::from(slug.clone()) + ".md");
let creation_date =
String::from(String::from("<br>Written on: ") + filename.split("_").next().unwrap());
let poem_content = Poems::get(&filename).expect("Found poem {filename:?}");
let mut poem_to_str = std::str::from_utf8(poem_content.data.as_ref())
.expect("Title is valid UT8.")
.lines();
let poem_title = poem_to_str.next().unwrap();
let poem_content = poem_to_str.into_iter().collect::<Vec<&str>>().join("\n");
let poem_title_to_html_string =
markdown::to_html_with_options(poem_title, &Options::gfm()).unwrap();
let poem_content_to_html_string =
markdown::to_html_with_options(poem_content.as_str(), &Options::gfm()).unwrap();
(
poem_title_to_html_string,
poem_content_to_html_string,
creation_date,
)
}
pub fn get_poem_list() -> Vec<(String, String)> {
let mut poem_list = Vec::new();
for p in Poems::iter() {
let filename = p.to_string();
let poem_content = Poems::get(&filename).expect("Found poem {filename:?}");
let mut poem_to_str = std::str::from_utf8(poem_content.data.as_ref())
.expect("Title is valid UT8.")
.lines();
let title_markdown = poem_to_str.next().expect("No title specified.");
let title = markdown::to_html_with_options(title_markdown, &Options::gfm()).unwrap();
let slug = String::from(filename.trim_end_matches(".md"));
poem_list.push((title.clone(), slug.clone()));
}
log::trace!("{poem_list:?}");
poem_list
}
pub fn get_oldest_entry(current: String) -> String {
let mut poem_list = VecDeque::from(get_poem_list());
let oldest = poem_list
.pop_front()
.expect("There is an entry in this list of poems.")
.1;
if current == oldest {
return format!("/poems/{current}#");
}
format!("/poems/{oldest}")
}
pub fn get_latest_entry(current: String) -> String {
let mut poem_list = get_poem_list();
let latest = poem_list
.pop()
.expect("There is an entry in this list of poems.")
.1;
if current == latest {
return format!("/poems/{current}#");
}
format!("/poems/{latest}")
}
pub fn get_previous_entry(current: String) -> String {
let poem_list = get_poem_list();
match poem_list.iter().enumerate().find_map(|(index, p)| {
if p.1 == current {
if index != 0 {
Some(poem_list[index - 1].1.clone())
} else {
None
}
} else {
None
}
}) {
Some(entry) => format!("/poems/{entry}"),
None => format!("/poems/{current}"),
}
}
pub fn get_next_entry(current: String) -> String {
let poem_list = get_poem_list();
match poem_list.iter().enumerate().find_map(|(index, p)| {
if p.1 == current {
if index != poem_list.len() - 1 {
Some(poem_list[index + 1].1.clone())
} else {
None
}
} else {
None
}
}) {
Some(entry) => format!("/poems/{entry}"),
None => format!("/poems/{current}"),
}
}
pub fn get_random_entry() -> String {
let poem_list = get_poem_list();
let mut rng = rand::thread_rng();
let random_entry = poem_list
.choose(&mut rng)
.expect("Got a valid entry")
.1
.clone();
format!("/poems/{random_entry}")
}

View File

@ -1,5 +1,5 @@
use std::collections::HashMap;
use crate::void_app::{Element, Props};
use super::user_prefs::UserPrefs;
#[derive(PartialEq, Props)]
@ -29,14 +29,25 @@ pub struct ButtonProps {
}
#[derive(PartialEq, Props)]
pub struct PoemData {
pub title: Option<String>,
pub content: Option<String>,
pub creation_date: Option<String>,
pub struct VoidProps {
pub slug: Option<String>,
pub poem_database: PoemDatabase,
pub user_prefs: UserPrefs,
}
#[derive(PartialEq, Props, Clone, Debug)]
pub struct PoemDatabase {
pub poem_list: Vec<(String, String)>,
pub poem_hashmap: HashMap<String, PoemStruct>,
}
#[derive(PartialEq, Clone, Debug)]
pub struct PoemStruct {
pub title: String,
pub content: String,
pub creation_date: String,
}
// These next three should all just be one prop.
#[derive(Props)]
pub struct PoemChildren<'a> {
@ -51,4 +62,4 @@ pub struct PageChildren<'a> {
#[derive(Props)]
pub struct ContentChildren<'a> {
pub children: Element<'a>,
}
}