use {
crate::*,
serde::{
Deserialize,
de,
},
std::fmt,
termimad::crossterm::style::Stylize,
};
#[derive(Debug, Clone, Default)]
pub struct ElementList {
pub children: Vec<Element>,
}
impl ElementList {
pub fn child(
&self,
index: usize,
) -> Option<&Element> {
self.children.get(index)
}
pub fn len(&self) -> usize {
self.children.len()
}
pub fn is_empty(&self) -> bool {
self.children.is_empty()
}
pub fn visit<F>(
&self,
mut f: F,
) where
F: FnMut(&Element),
{
for child in &self.children {
child.visit(&mut f);
}
}
pub fn has<F>(
&self,
mut f: F,
) -> bool
where
F: FnMut(&Element) -> bool,
{
for child in &self.children {
if child.has(&mut f) {
return true;
}
}
false
}
pub fn has_href(
&self,
href: &str,
) -> bool {
self.has(|element: &Element| {
if let ElementContent::Link(link) = &element.content {
if link.href.as_deref() == Some(href) {
return true;
}
}
false
})
}
pub fn merge(
&mut self,
other: &Self,
) {
for other_element in &other.children {
let mut merged = false;
let other_selector = other_element.selector();
for element in &mut self.children {
if element.selector() == other_selector {
if element.try_merge(other_element) {
merged = true;
break;
}
}
}
if !merged {
self.children.push(other_element.clone());
}
}
}
}
pub struct ElementListDeserializer {}
impl<'de> de::Visitor<'de> for ElementListDeserializer {
type Value = ElementList;
fn expecting(
&self,
formatter: &mut fmt::Formatter,
) -> fmt::Result {
formatter.write_str("a composite element")
}
fn visit_map<M>(
self,
mut access: M,
) -> Result<Self::Value, M::Error>
where
M: serde::de::MapAccess<'de>,
{
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum DeserContent {
Composite(ElementList),
Attributes(Attributes),
}
let mut children = Vec::new();
while let Some((key, value)) = access.next_entry::<ElementKey, DeserContent>()? {
let ElementKey { etype, classes: _ } = key;
let content = match (etype, value) {
(ElementType::HtmlTag(tag), DeserContent::Composite(comp)) => {
ElementContent::DomTree {
tag,
children: comp.children,
}
}
(ElementType::HtmlTag(tag), DeserContent::Attributes(mut attrs)) => {
let text = attrs.shift_remove("text").map(Text::from);
let raw_html = attrs.shift_remove("html").map(|v| v.to_string());
ElementContent::DomLeaf {
tag,
text,
raw_html,
attributes: attrs,
}
}
(ElementType::Link, DeserContent::Attributes(attrs)) => {
let nav_link: NavLink = attrs.into();
ElementContent::Link(nav_link)
}
(ElementType::Menu, DeserContent::Attributes(attrs)) => {
let menu_insert: Menu = attrs.into();
ElementContent::Menu(menu_insert)
}
(ElementType::Toc, DeserContent::Attributes(attrs)) => {
let toc: Toc = attrs.into();
ElementContent::Toc(toc)
}
(ElementType::Menu, _) => ElementContent::Menu(Menu::default()),
(ElementType::Toc, _) => ElementContent::Toc(Toc::default()),
(ElementType::Main, _) => ElementContent::Main,
(ElementType::PageTitle, _) => ElementContent::PageTitle,
(etype, value) => {
eprintln!(
"{}: invalid element type {} for value {:?}",
"error".red(),
etype.to_string().yellow(),
value,
);
return Err(de::Error::custom(format!(
"invalid element type {:?} for value {:?}",
etype, value
)));
}
};
children.push(Element {
classes: key.classes,
content,
});
}
Ok(Self::Value { children })
}
}
impl<'de> de::Deserialize<'de> for ElementList {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
deserializer.deserialize_map(ElementListDeserializer {})
}
}
#[test]
fn test_composite_element_deserialization() {
let hjson = r#"
{
header: {
nav.before-menu: {
ddoc-link: {
img: img/dystroy-rust-white.svg
href: https://dystroy.org
alt: dystroy.org homepage
class: external-nav-link
}
ddoc-link: {
url: /index.md
alt: ddoc homepage
label: ddoc
class: home-link
}
}
ddoc-menu: {
hamburger-checkbox: true
}
nav.after-menu: {
ddoc-link: {
img: img/ddoc-left-arrow.svg
href: --previous
class: previous-page-link
alt: Previous Page
}
ddoc-link: {
img: img/ddoc-search.svg
href: --search
class: search-opener
alt: Search
}
ddoc-link: {
img: img/ddoc-right-arrow.svg
href: --next
class: next-page-link
alt: Next Page
}
ddoc-link: {
img: img/github-mark-white.svg
class: external-nav-link
alt: GitHub
href: https://github.com/Canop/ddoc
}
}
}
article: {
aside.page-nav: {
ddoc-toc: {}
}
ddoc-main: {}
}
footer: {
nav.made-with-ddoc: {
ddoc-link: {
label: made with
}
ddoc-link: {
label: ddoc
href: https://dystroy.org/ddoc
class: link-to-ddoc
}
}
}
}
"#;
let composite: ElementList = deser_hjson::from_str(hjson).unwrap();
assert_eq!(composite.children.len(), 3);
let header = &composite.children[0];
assert!(matches!(
header.children().unwrap()[1].content,
ElementContent::Menu(_)
));
let article = &composite.children[1];
let toc = &article.children().unwrap()[0].children().unwrap()[0];
assert!(matches!(toc.content, ElementContent::Toc(_)));
let footer = &composite.children[2];
assert_eq!(footer.tag(), "footer");
assert!(composite.has_href("--next"));
assert!(!composite.has_href("--top"));
}