// -*- coding: utf-8 -*- // // Simple CMS // // Copyright (C) 2011-2024 Michael Büsch // // Licensed under the Apache License version 2.0 // or the MIT license, at your option. // SPDX-License-Identifier: Apache-2.0 OR MIT use crate::{ anchor::Anchor, args::CmsGetArgs, config::CmsConfig, navtree::{NavElem, NavTree}, reply::CmsReply, resolver::Resolver, }; use anyhow::{self as ah, format_err as err}; use chrono::prelude::*; use cms_ident::{CheckedIdent, UrlComp}; use std::{fmt::Write as _, sync::Arc, write as wr, writeln as ln}; const DEFAULT_HTML_ALLOC: usize = 1024 * 64; const DEFAULT_INDEX_HTML_ALLOC: usize = 1024 * 4; const MAX_INDENT: usize = 1024; #[inline] fn make_indent(indent: usize) -> &'static str { const TEMPLATE: &str = " "; &TEMPLATE[..(indent * 4).min(TEMPLATE.len())] } pub struct PageGen<'a> { get: &'a CmsGetArgs, config: Arc, } impl<'a> PageGen<'a> { pub fn new(get: &'a CmsGetArgs, config: Arc) -> Self { Self { get, config } } #[allow(clippy::comparison_chain)] pub fn generate_index(&self, anchors: &[Anchor], resolver: &Resolver) -> ah::Result { let mut html = String::with_capacity(DEFAULT_INDEX_HTML_ALLOC); ln!(html, r#"{}
    "#, make_indent(1))?; let mut indent = 0; for anchor in anchors { if anchor.no_index() || anchor.text().is_empty() { continue; } if let Some(aindent) = anchor.indent() { // Adjust indent. if aindent > indent { if aindent > MAX_INDENT { return Err(err!("Anchor indent too big")); } for _ in 0..(aindent - indent) { indent += 1; ln!(html, r#"{}
      "#, make_indent(indent + 1))?; } } else if aindent < indent { for _ in 0..(indent - aindent) { ln!(html, r#"{}
    "#, make_indent(indent + 1))?; indent -= 1; } } } // Anchor data. ln!( html, r#"{}
  • {}
  • "#, make_indent(indent + 2), anchor.make_html(resolver, false)?, )?; } for _ in 0..(indent + 1) { ln!(html, r#"{}
"#, make_indent(indent + 1))?; } Ok(html) } #[rustfmt::skip] #[allow(clippy::only_used_in_recursion)] pub fn generate_navelem( &self, b: &mut String, navelems: &[NavElem], indent: usize, ) -> ah::Result<()> { if navelems.is_empty() { return Ok(()); } let c = &self.config; let ii = make_indent(indent + 1); if indent > 0 { ln!(b, r#"{ii}"#)?; // navelems } Ok(()) } #[rustfmt::skip] fn generate_nav( &self, b: &mut String, navtree: &NavTree, homestr: &str, ) -> ah::Result<()> { let c = &self.config; let nav_home_href = CheckedIdent::ROOT.url(UrlComp { protocol: None, domain: None, base: Some(c.url_base()), }); let nav_home_text = homestr.trim(); ln!(b, r#""#)?; // navbar Ok(()) } #[rustfmt::skip] #[allow(clippy::too_many_arguments)] fn generate_body( &self, b: &mut String, path: Option<&CheckedIdent>, title: &str, page_content: &str, stamp: &DateTime, navtree: &NavTree, homestr: &str, ) -> ah::Result<()> { let c = &self.config; let page_stamp = stamp.format("%A %d %B %Y %H:%M"); ln!(b, r#"
"#)?; ln!(b, r#" "#)?; ln!(b, r#"
{title}
"#)?; ln!(b, r#"
"#)?; self.generate_nav(b, navtree, homestr)?; ln!(b, r#"
"#)?; ln!(b)?; ln!(b, r#""#)?; ln!(b, r#"{page_content}"#)?; ln!(b, r#""#)?; ln!(b)?; ln!(b, r#"
"#)?; ln!(b, r#" Updated: {page_stamp} (UTC)"#)?; ln!(b, r#"
"#)?; ln!(b)?; if let Some(path) = path { let url = path.url(UrlComp { protocol: Some("https"), domain: Some(self.config.domain()), base: Some(self.config.url_base()), }); let mut url_enc = String::with_capacity(url.len() * 4); let url = url_escape::encode_component_to_string(url, &mut url_enc); ln!(b, r#"
"#)?; wr!(b, r#" xhtml"#)?; ln!(b, r#" /"#)?; wr!(b, r#" css"#)?; ln!(b, r#"
"#)?; } ln!(b)?; ln!(b, r#"
"#)?; Ok(()) } #[rustfmt::skip] #[allow(clippy::too_many_arguments)] pub fn generate_html( &self, path: Option<&CheckedIdent>, title: &str, headers: &str, data: &str, now: &DateTime, stamp: &DateTime, navtree: &NavTree, homestr: &str, ) -> ah::Result { let c = &self.config; let mut b = String::with_capacity(DEFAULT_HTML_ALLOC); let title = title.trim(); let now = now.to_rfc3339_opts(SecondsFormat::Secs, true); let headers = headers .lines() .fold( String::with_capacity(headers.len() * 2), |mut buf, line| { let _ = ln!(buf, r#" {line}"#); buf } ); ln!(b, r#""#)?; ln!(b, r#""#)?; ln!(b, r#""#)?; ln!(b, r#""#)?; ln!(b, r#" "#)?; ln!(b, r#" "#)?; ln!(b, r#" "#)?; ln!(b, r#" "#)?; ln!(b, r#" {title}"#)?; ln!(b, r#" "#, c.url_base())?; ln!(b, r#" "#, c.url_base())?; ln!(b, r#" "#)?; ln!(b, r#"{headers}"#)?; ln!(b, r#""#)?; ln!(b, r#""#)?; self.generate_body(&mut b, path, title, data, stamp, navtree, homestr)?; ln!(b, r#""#)?; ln!(b, r#""#)?; Ok(b) } #[allow(clippy::too_many_arguments)] pub fn generate( &self, path: Option<&CheckedIdent>, title: &str, headers: &str, data: &str, now: &DateTime, stamp: &DateTime, navtree: &NavTree, homestr: &str, ) -> CmsReply { if let Ok(b) = self.generate_html(path, title, headers, data, now, stamp, navtree, homestr) { CmsReply::ok(b.into_bytes(), "application/xhtml+xml; charset=UTF-8") } else { CmsReply::internal_error("PageGen failed") } } } // vim: ts=4 sw=4 expandtab