-
Pierre Jarriges authoredPierre Jarriges authored
page.rs 14.46 KiB
use super::css::StyleSheet;
use super::html::{
replace_placeholders, HtmlDoc, HtmlElement, CSS_LINK_FRAGMENT, FAVICON_LINK_FRAGMENT,
IMAGE_LINKS_FRAGMENT, SCRIPT_FRAGMENT,
};
use crate::StaticFilesManager;
use serde::{
ser::{SerializeStruct, Serializer},
Deserialize, Serialize,
};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct PageId(usize);
impl Default for PageId {
fn default() -> Self {
PageId(0)
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct Page {
#[serde(default)]
id: PageId,
pub template_name: String,
pub body: PageBody,
pub full_body: Option<PageBody>,
pub metadata: PageMetadata,
#[serde(default = "Vec::new")]
pub sub_pages: Vec<Page>,
#[serde(default = "HtmlDoc::new")]
pub html: HtmlDoc,
pub template: Option<PageTemplate>,
}
impl Serialize for Page {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("Page", 5)?;
state.serialize_field("id", &self.id.0)?;
state.serialize_field("template_name", &self.template_name)?;
state.serialize_field("body", &self.body)?;
state.serialize_field("metadata", &self.metadata)?;
state.serialize_field("sub_pages", &self.sub_pages)?;
state.end()
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PageTemplate {
pub layout: StyleSheet,
pub name: String,
pub contents: Vec<HtmlElement>,
}
impl PageTemplate {
pub fn get_page_body_placeholder<'a>(
elements: &'a mut Vec<HtmlElement>,
) -> Result<&'a mut HtmlElement, ()> {
for el in elements.iter_mut() {
if el.attrs.get("id").unwrap_or(&String::new()).eq("page-body") {
return Ok(el);
} else if let Some(children) = &mut el.contents {
if let Ok(found) = Self::get_page_body_placeholder(children) {
return Ok(found);
}
}
}
return Err(());
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PageBody(Vec<HtmlElement>);
impl std::fmt::Display for PageBody {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
&self
.0
.iter()
.map(|i| i.to_string())
.collect::<Vec<String>>()
.join("")
)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PageMetadata {
pub title: String,
pub lang: String,
pub description: String,
pub url_slug: String,
#[serde(default = "CSSLinks::new")]
pub css: CSSLinks,
#[serde(default = "JSLinks::new")]
pub js: JSLinks,
#[serde(default = "FaviconLink::new")]
pub favicon: FaviconLink,
#[serde(default = "String::new")]
pub author: String,
#[serde(default = "ImageLinks::new")]
pub image: ImageLinks,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FaviconLink(String);
impl FaviconLink {
pub fn new() -> Self {
FaviconLink(String::from("/assets/default/favicon.ico"))
}
}
impl std::fmt::Display for FaviconLink {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
replace_placeholders(FAVICON_LINK_FRAGMENT, {
let mut map = HashMap::new();
map.insert("url".to_owned(), self.0.to_owned());
map
})
)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CSSLinks(Vec<String>);
impl CSSLinks {
pub fn new() -> Self {
CSSLinks(vec!["/assets/default/style.css".to_owned()])
}
}
impl std::fmt::Display for CSSLinks {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
&self
.0
.iter()
.map(|url| replace_placeholders(CSS_LINK_FRAGMENT, {
let mut map = HashMap::new();
map.insert("url".to_string(), url.to_owned());
map
}))
.collect::<Vec<String>>()
.join("\n")
)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JSLinks(Vec<String>);
impl JSLinks {
pub fn new() -> Self {
JSLinks(vec!["/assets/default/script.js".to_owned()])
}
}
impl std::fmt::Display for JSLinks {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
&self
.0
.iter()
.map(|url| replace_placeholders(SCRIPT_FRAGMENT, {
let mut map = HashMap::new();
map.insert("url".to_string(), url.to_owned());
map
}))
.collect::<Vec<String>>()
.join("\n")
)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ImageLinks(Vec<String>);
impl ImageLinks {
pub fn new() -> Self {
ImageLinks(vec![])
}
}
impl std::fmt::Display for ImageLinks {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
&self
.0
.iter()
.map(|url| replace_placeholders(IMAGE_LINKS_FRAGMENT, {
let mut map = HashMap::new();
map.insert("url".to_string(), url.to_owned());
map
}))
.collect::<Vec<String>>()
.join("\n")
)
}
}
impl Page {
pub fn build(
&mut self,
templates: &Vec<PageTemplate>,
parent_scope: PathBuf,
static_files_manager: &StaticFilesManager,
) -> Result<(), String> {
self.set_template(self.template(templates).clone());
self.build_full_body();
self.build_html();
let url = self.url(parent_scope);
if let Err(err) = self.write_to_static_file(&url, static_files_manager) {
return Err(format!(
"Error writing html static file for page {} - {}",
self.metadata.title, err
));
}
for p in self.sub_pages.iter_mut() {
if let Err(err) = p.build(templates, url.clone(), static_files_manager) {
return Err(format!(
"Error building page {} - {}",
p.metadata.title, err
));
}
}
Ok(())
}
pub fn set_template(&mut self, template: PageTemplate) {
self.template = Some(template);
}
pub fn get_id(&self) -> usize {
self.id.0
}
pub fn set_id(&mut self, id: usize) {
self.id.0 = id
}
fn build_html(&mut self) {
self.html = HtmlDoc::from_page(self);
}
fn build_full_body(&mut self) {
let mut full_body = self.template.clone().expect("Page template is not set");
// insert the page body elements in the template page-body container.
PageTemplate::get_page_body_placeholder(&mut full_body.contents)
.expect("Couldn't find page body container in template")
.set_contents(self.body.0.clone());
self.full_body = Some(PageBody(full_body.contents));
}
fn url(&self, parent_scope: PathBuf) -> PathBuf {
parent_scope.join(&self.metadata.url_slug)
}
fn template<'a>(&self, templates: &'a Vec<PageTemplate>) -> &'a PageTemplate {
templates
.iter()
.find(|t| t.name == self.template_name)
.expect("Page template not found")
}
fn write_to_static_file(
&self,
url: &PathBuf,
static_files_manager: &StaticFilesManager,
) -> std::io::Result<()> {
static_files_manager.write_html_page(&url, self)
}
pub fn to_map(&self) -> HashMap<String, String> {
let mut map = HashMap::new();
map.insert("title".to_string(), self.metadata.title.to_owned());
map.insert("lang".to_string(), self.metadata.lang.to_owned());
map.insert(
"description".to_string(),
self.metadata.description.to_owned(),
);
map.insert("slug".to_string(), self.metadata.url_slug.to_owned());
map.insert(
"body".to_string(),
self.full_body
.as_ref()
.expect("Page full body is not built")
.to_string(),
);
map.insert("css".to_string(), self.metadata.css.to_string());
map.insert("js".to_string(), self.metadata.js.to_string());
map.insert("author".to_string(), self.metadata.author.to_string());
map.insert("favicon".to_string(), self.metadata.favicon.to_string());
map.insert("image".to_string(), self.metadata.image.to_string());
map
}
pub fn add_sub_page(&mut self, page: Page) {
self.sub_pages.push(page);
}
}
#[cfg(test)]
mod test_pages {
use super::*;
use crate::website::html::{HtmlAttributes, HtmlElement};
fn test_page_metadata() -> PageMetadata {
PageMetadata {
title: String::from("Test"),
lang: String::from("en"),
description: String::from("test descr"),
url_slug: String::from("test-page"),
css: CSSLinks(vec![
"/assets/source_code/mystyle.css".to_string(),
"/assets/source_code/mystyle2.css".to_string(),
]),
js: JSLinks(vec![
"/assets/source_code/myscript.js".to_string(),
"/assets/source_code/myscript2.js".to_string(),
]),
favicon: FaviconLink(String::from("/assets/images/testicon.ico")),
author: String::from("test author"),
image: ImageLinks(vec![
"/assets/images/testimage.png".to_string(),
"/assets/images/testimage2.png".to_string(),
]),
}
}
fn test_page_template() -> PageTemplate {
PageTemplate {
layout: StyleSheet(HashMap::new()),
name: String::from("test template"),
contents: vec![HtmlElement {
tag: String::from("div"),
attrs: {
let mut attrs = HtmlAttributes::new();
attrs
.0
.insert(String::from("id"), String::from("test-template"));
attrs
},
contents: Some(vec![
HtmlElement {
tag: String::from("nav"),
attrs: HtmlAttributes::new(),
contents: None,
text: Some(String::from("NAV")),
},
HtmlElement {
tag: String::from("div"),
attrs: {
let mut attrs = HtmlAttributes::new();
attrs
.0
.insert(String::from("id"), String::from("page-body"));
attrs
},
contents: None,
text: None,
},
HtmlElement {
tag: String::from("footer"),
attrs: HtmlAttributes::new(),
contents: None,
text: Some(String::from("FOOTER")),
},
]),
text: None,
}],
}
}
fn test_page() -> Page {
Page {
id: PageId(1),
template_name: String::from("test template"),
body: PageBody(vec![HtmlElement {
tag: "span".to_string(),
text: Some("TEST".to_string()),
contents: None,
attrs: HtmlAttributes::new(),
}]),
full_body: None,
metadata: test_page_metadata(),
sub_pages: Vec::new(),
html: HtmlDoc::new(),
template: None,
}
}
#[test]
fn page_body_to_string() {
let body = PageBody(vec![
HtmlElement {
tag: "span".to_string(),
text: Some("Hello ".to_string()),
contents: None,
attrs: HtmlAttributes::new(),
},
HtmlElement {
tag: "span".to_string(),
text: Some("World!".to_string()),
contents: None,
attrs: HtmlAttributes::new(),
},
]);
assert_eq!(body.to_string(), "<span>Hello </span><span>World!</span>");
}
#[test]
fn favicon_link_to_string() {
let pmd = test_page_metadata();
assert_eq!(
pmd.favicon.to_string(),
"<link rel='icon' type='image/*' href='/assets/images/testicon.ico'/>"
)
}
#[test]
fn css_links_to_string() {
let pmd = test_page_metadata();
assert_eq!(
pmd.css.to_string(),
"<link rel='stylesheet' href='/assets/source_code/mystyle.css'>
<link rel='stylesheet' href='/assets/source_code/mystyle2.css'>"
)
}
#[test]
fn js_links_to_string() {
let pmd = test_page_metadata();
assert_eq!(
pmd.js.to_string(),
"<script src='/assets/source_code/myscript.js'></script>
<script src='/assets/source_code/myscript2.js'></script>"
)
}
#[test]
fn images_links_to_string() {
let pmd = test_page_metadata();
assert_eq!(
pmd.image.to_string(),
"<meta name='image' content='/assets/images/testimage.png'/>
<meta property='og:image' content='/assets/images/testimage.png'/>
<meta property='twitter:image' content='/assets/images/testimage.png'/>
<meta name='image' content='/assets/images/testimage2.png'/>
<meta property='og:image' content='/assets/images/testimage2.png'/>
<meta property='twitter:image' content='/assets/images/testimage2.png'/>"
)
}
#[test]
fn build_body_with_template() {
let mut page = test_page();
page.set_template(test_page_template());
page.build_full_body();
assert_eq!(
page.full_body.unwrap().to_string(),
"<div id=\"test-template\"><nav>NAV</nav><div id=\"page-body\"><span>TEST</span></div><footer>FOOTER</footer></div>"
)
}
}