Reintroduce web response type for full pages

This commit is contained in:
Manuel Cillero 2024-11-24 09:23:24 +01:00
parent 04a365797d
commit b2b0d8bd3e
40 changed files with 1516 additions and 249 deletions

1
Cargo.lock generated
View file

@ -1659,6 +1659,7 @@ dependencies = [
"fluent-templates",
"itoa",
"nom",
"pagetop-build",
"pagetop-macros",
"paste",
"serde",

View file

@ -43,7 +43,7 @@ impl PackageTrait for HelloWorld {
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request)
.with_component(Html::with(html! { h1 { "Hello World!" } }))
.with_body(PrepareMarkup::With(html! { h1 { "Hello World!" } }))
.render()
}

View file

@ -1,5 +1,4 @@
//! **`StaticFilesBundle`** uses [static_files](https://docs.rs/static-files/latest/static_files/)
//! to provide an easy way to embed static files or compiled SCSS files into your binary at compile
//! Provide an easy way to embed static files or compiled SCSS files into your binary at compile
//! time.
//!
//! ## Adding to your project
@ -24,9 +23,9 @@
//! use pagetop_build::StaticFilesBundle;
//!
//! fn main() -> std::io::Result<()> {
//! StaticFilesBundle::from_dir("./static", None) // Include all files.
//! .with_name("guides") // Name the generated module.
//! .build() // Build the bundle.
//! StaticFilesBundle::from_dir("./static", None)
//! .with_name("guides")
//! .build()
//! }
//! ```
//!
@ -67,7 +66,7 @@
//!
//! ## Generated module
//!
//! `StaticFilesBundle` generates a file in the standard directory
//! [`StaticFilesBundle`] generates a file in the standard directory
//! [OUT_DIR](https://doc.rust-lang.org/cargo/reference/environment-variables.html) where all
//! intermediate and output artifacts are placed during compilation. For example, if you use
//! `with_name("guides")`, it generates a file named `guides.rs`:
@ -78,15 +77,15 @@
//! ```rust#ignore
//! use pagetop::prelude::*;
//!
//! static_files!(guides);
//! include_files!(guides);
//! ```
//!
//! Or, access the entire bundle as a static `HashMap`:
//! Or, access the entire bundle as a global static `HashMap`:
//!
//! ```rust#ignore
//! use pagetop::prelude::*;
//!
//! static_files!(guides => BUNDLE_GUIDES);
//! include_files!(guides => BUNDLE_GUIDES);
//! ```
//!
//! You can build more than one resources file to compile with your project.
@ -98,6 +97,8 @@ use std::fs::{create_dir_all, remove_dir_all, File};
use std::io::Write;
use std::path::Path;
/// Generates the resources to embed at compile time using
/// [static_files](https://docs.rs/static-files/latest/static_files/).
pub struct StaticFilesBundle {
resource_dir: ResourceDir,
}

View file

@ -3,8 +3,110 @@ mod smart_default;
use proc_macro::TokenStream;
use proc_macro_error::proc_macro_error;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
use quote::{quote, quote_spanned, ToTokens};
use syn::{parse_macro_input, parse_str, DeriveInput, ItemFn};
/// Macro attribute to generate builder methods from `set_` methods.
///
/// This macro takes a method with the `set_` prefix and generates a corresponding method with the
/// `with_` prefix to use in the builder pattern.
///
/// # Panics
///
/// This function will panic if a parameter identifier is not found in the argument list.
///
/// # Examples
///
/// ```
/// #[fn_builder]
/// pub fn set_example(&mut self) -> &mut Self {
/// // implementation
/// }
/// ```
///
/// Will generate:
///
/// ```
/// pub fn with_example(mut self) -> Self {
/// self.set_example();
/// self
/// }
/// ```
#[proc_macro_attribute]
pub fn fn_builder(_: TokenStream, item: TokenStream) -> TokenStream {
let fn_set = parse_macro_input!(item as ItemFn);
let fn_set_name = fn_set.sig.ident.to_string();
if !fn_set_name.starts_with("set_") {
let expanded = quote_spanned! {
fn_set.sig.ident.span() =>
compile_error!("expected a \"pub fn set_...() -> &mut Self\" method");
};
return expanded.into();
}
let fn_with_name = fn_set_name.replace("set_", "with_");
let fn_with_generics = if fn_set.sig.generics.params.is_empty() {
fn_with_name.clone()
} else {
let g = &fn_set.sig.generics;
format!("{fn_with_name}{}", quote! { #g }.to_string())
};
let where_clause = fn_set
.sig
.generics
.where_clause
.as_ref()
.map_or(String::new(), |where_clause| {
format!("{} ", quote! { #where_clause }.to_string())
});
let args: Vec<String> = fn_set
.sig
.inputs
.iter()
.skip(1)
.map(|arg| arg.to_token_stream().to_string())
.collect();
let params: Vec<String> = args
.iter()
.map(|arg| {
arg.split_whitespace()
.next()
.unwrap()
.trim_end_matches(':')
.to_string()
})
.collect();
#[rustfmt::skip]
let fn_with = parse_str::<ItemFn>(format!(r#"
pub fn {fn_with_generics}(mut self, {}) -> Self {where_clause} {{
self.{fn_set_name}({});
self
}}
"#, args.join(", "), params.join(", ")
).as_str()).unwrap();
#[rustfmt::skip]
let fn_set_doc = format!(r##"
<p id="method.{fn_with_name}" style="margin-bottom: 12px;">Use
<code class="code-header">pub fn <span class="fn" href="#method.{fn_with_name}">{fn_with_name}</span>(self, ) -> Self</code>
for the <a href="#method.new">builder pattern</a>.
</p>
"##);
let expanded = quote! {
#[doc(hidden)]
#fn_with
#[inline]
#[doc = #fn_set_doc]
#fn_set
};
expanded.into()
}
#[proc_macro]
#[proc_macro_error]

View file

@ -5,9 +5,9 @@ use tera::Tera;
use std::sync::LazyLock;
static_locales!(LOCALES_ALINER);
include_locales!(LOCALES_ALINER);
static_files!(aliner);
include_files!(aliner);
// ALINER THEME ************************************************************************************
@ -50,7 +50,7 @@ impl PackageTrait for Aliner {
}
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
static_files_service!(scfg, aliner => "/aliner");
include_files_service!(scfg, aliner => "/aliner");
}
}

View file

@ -1,8 +1,8 @@
use pagetop::prelude::*;
static_locales!(LOCALES_BOOTSIER);
include_locales!(LOCALES_BOOTSIER);
//static_files!(bootsier);
//include_files!(bootsier);
pub struct Bootsier;
@ -26,7 +26,7 @@ impl PackageTrait for Bootsier {
}
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
static_files_service!(scfg, bootsier => "/bootsier");
include_files_service!(scfg, bootsier => "/bootsier");
} */
}

View file

@ -19,15 +19,15 @@ license = { workspace = true }
name = "pagetop"
[dependencies]
colored = "2.1.0"
colored = "2.1.0"
concat-string = "1.0.1"
figlet-rs = "0.1.5"
itoa = "1.0.11"
nom = "7.1.3"
paste = "1.0.15"
substring = "1.4.5"
figlet-rs = "0.1.5"
itoa = "1.0.11"
nom = "7.1.3"
paste = "1.0.15"
substring = "1.4.5"
terminal_size = "0.4.0"
toml = "0.8.19"
toml = "0.8.19"
tracing = "0.1.40"
tracing-appender = "0.2.3"
@ -47,3 +47,6 @@ serde.workspace = true
static-files.workspace = true
pagetop-macros.workspace = true
[build-dependencies]
pagetop-build.workspace = true

7
pagetop/build.rs Normal file
View file

@ -0,0 +1,7 @@
use pagetop_build::StaticFilesBundle;
fn main() -> std::io::Result<()> {
StaticFilesBundle::from_dir("../static", None)
.with_name("assets")
.build()
}

View file

@ -15,7 +15,7 @@ async fn hello_name(
) -> ResultPage<Markup, ErrorPage> {
let name = path.into_inner();
Page::new(request)
.with_component(Html::with(html! { h1 { "Hello " (name) "!" } }))
.with_body(PrepareMarkup::With(html! { h1 { "Hello " (name) "!" } }))
.render()
}

View file

@ -10,7 +10,7 @@ impl PackageTrait for HelloWorld {
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request)
.with_component(Html::with(html! { h1 { "Hello World!" } }))
.with_body(PrepareMarkup::With(html! { h1 { "Hello World!" } }))
.render()
}

View file

@ -3,6 +3,8 @@
mod figfont;
use crate::core::{package, package::PackageRef};
use crate::html::Markup;
use crate::response::page::{ErrorPage, ResultPage};
use crate::{global, locale, service, trace};
use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle};
@ -154,12 +156,12 @@ impl Application {
InitError = (),
>,
> {
service::App::new().configure(package::all::configure_services)
// .default_service(service::web::route().to(service_not_found))
service::App::new()
.configure(package::all::configure_services)
.default_service(service::web::route().to(service_not_found))
}
}
/*
async fn service_not_found(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
async fn service_not_found(request: service::HttpRequest) -> ResultPage<Markup, ErrorPage> {
Err(ErrorPage::NotFound(request))
}
*/

View file

@ -1,7 +1,7 @@
use crate::core::action::add_action;
use crate::core::package::{welcome, PackageRef};
use crate::core::theme::all::THEMES;
use crate::{service, trace};
use crate::{include_files, include_files_service, service, trace};
use std::sync::{LazyLock, RwLock};
@ -23,8 +23,6 @@ pub fn register_packages(root_package: Option<PackageRef>) {
if let Some(package) = root_package {
add_to_enabled(&mut enabled_list, package);
}
// Reverse the order to ensure packages are sorted from none to most dependencies.
enabled_list.reverse();
// Save the final list of enabled packages.
ENABLED_PACKAGES.write().unwrap().append(&mut enabled_list);
@ -41,16 +39,14 @@ pub fn register_packages(root_package: Option<PackageRef>) {
fn add_to_enabled(list: &mut Vec<PackageRef>, package: PackageRef) {
// Check if the package is not already in the enabled list to avoid duplicates.
if !list.iter().any(|p| p.type_id() == package.type_id()) {
// Add the package to the enabled list.
list.push(package);
// Reverse dependencies to add them in correct order (dependencies first).
let mut dependencies = package.dependencies();
dependencies.reverse();
for d in &dependencies {
// Add the package dependencies in reverse order first.
for d in package.dependencies().iter().rev() {
add_to_enabled(list, *d);
}
// Add the package itself to the enabled list.
list.push(package);
// Check if the package has an associated theme to register.
if let Some(theme) = package.theme() {
let mut registered_themes = THEMES.write().unwrap();
@ -119,10 +115,14 @@ pub fn init_packages() {
// CONFIGURE SERVICES ******************************************************************************
include_files!(assets);
pub fn configure_services(scfg: &mut service::web::ServiceConfig) {
for m in ENABLED_PACKAGES.read().unwrap().iter() {
m.configure_service(scfg);
}
// Default welcome homepage.
scfg.route("/", service::web::get().to(welcome::homepage));
// Default assets.
include_files_service!(scfg, assets => "/");
}

View file

@ -13,7 +13,7 @@ pub trait PackageTrait: AnyBase + Send + Sync {
}
fn description(&self) -> L10n {
L10n::none()
L10n::default()
}
fn theme(&self) -> Option<ThemeRef> {

View file

@ -1,67 +1,63 @@
use crate::html::{html, Markup};
use crate::html::{html, Markup, PrepareMarkup, StyleSheet};
use crate::locale::L10n;
use crate::{concat_string, global};
use crate::response::page::{AssetsOp, ErrorPage, Page, ResultPage};
use crate::{global, service};
pub async fn homepage() -> Markup {
html! {
head {
meta charset="UTF-8" {}
meta name="viewport" content="width=device-width, initial-scale=1" {}
title { (concat_string!(
&global::SETTINGS.app.name, " | ", L10n::l("welcome_page").to_string()
)) }
style { r#"
body {
background-color: #f3d060;
font-size: 20px;
}
.wrapper {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 0;
}
.container {
padding: 0 16px;
}
.title {
font-size: clamp(3rem, 10vw, 10rem);
letter-spacing: -0.05em;
line-height: 1.2;
margin: 0;
}
.subtitle {
font-size: clamp(1.8rem, 2vw, 3rem);
letter-spacing: -0.02em;
line-height: 1.2;
margin: 0;
}
.powered {
margin: .5em 0 1em;
}
.box-container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: stretch;
gap: 1.5em;
}
.box {
flex: 1 1 280px;
border: 3px solid #25282a;
box-shadow: 5px 5px 0px #25282a;
box-sizing: border-box;
padding: 0 16px;
}
footer {
margin-top: 5em;
font-size: 14px;
font-weight: 500;
color: #a5282c;
}
"# }
}
body style="font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;" {
pub async fn homepage(request: service::HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request)
.with_title(L10n::l("welcome_page"))
.with_assets(AssetsOp::AddStyleSheet(StyleSheet::inline("styles", r#"
body {
background-color: #f3d060;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 20px;
}
.wrapper {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 0;
}
.container {
padding: 0 16px;
}
.title {
font-size: clamp(3rem, 10vw, 10rem);
letter-spacing: -0.05em;
line-height: 1.2;
margin: 0;
}
.subtitle {
font-size: clamp(1.8rem, 2vw, 3rem);
letter-spacing: -0.02em;
line-height: 1.2;
margin: 0;
}
.powered {
margin: .5em 0 1em;
}
.box-container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: stretch;
gap: 1.5em;
}
.box {
flex: 1 1 280px;
border: 3px solid #25282a;
box-shadow: 5px 5px 0px #25282a;
box-sizing: border-box;
padding: 0 16px;
}
footer {
margin-top: 5em;
font-size: 14px;
font-weight: 500;
color: #a5282c;
}
"#)))
.with_body(PrepareMarkup::With(html! {
div class="wrapper" {
div class="container" {
h1 class="title" { (L10n::l("welcome_title").markup()) }
@ -115,6 +111,6 @@ pub async fn homepage() -> Markup {
footer { "[ " (L10n::l("welcome_have_fun").markup()) " ]" }
}
}
}
}
}))
.render()
}

View file

@ -1,4 +1,6 @@
use crate::core::theme::ThemeRef;
use crate::core::package::PackageTrait;
use crate::core::theme::{ThemeRef, ThemeTrait};
use crate::global;
use std::sync::{LazyLock, RwLock};
@ -6,7 +8,7 @@ use std::sync::{LazyLock, RwLock};
pub static THEMES: LazyLock<RwLock<Vec<ThemeRef>>> = LazyLock::new(|| RwLock::new(Vec::new()));
/* DEFAULT THEME ***********************************************************************************
// DEFAULT THEME ***********************************************************************************
pub struct NoTheme;
@ -16,8 +18,7 @@ impl PackageTrait for NoTheme {
}
}
impl ThemeTrait for NoTheme {
}
impl ThemeTrait for NoTheme {}
pub static DEFAULT_THEME: LazyLock<ThemeRef> =
LazyLock::new(|| match theme_by_short_name(&global::SETTINGS.app.theme) {
@ -39,4 +40,3 @@ pub fn theme_by_short_name(short_name: &str) -> Option<ThemeRef> {
_ => None,
}
}
*/

View file

@ -1,4 +1,8 @@
use crate::core::package::PackageTrait;
use crate::html::{html, PrepareMarkup};
use crate::locale::L10n;
use crate::response::page::Page;
use crate::{global, service};
pub type ThemeRef = &'static dyn ThemeTrait;
@ -15,69 +19,16 @@ pub trait ThemeTrait: PackageTrait + Send + Sync {
("sidebar_right", L10n::l("sidebar_right")),
("footer", L10n::l("footer")),
]
}
} */
#[allow(unused_variables)]
fn before_prepare_body(&self, page: &mut Page) {}
fn prepare_body(&self, page: &mut Page) -> PrepareMarkup {
let skip_to_id = page.body_skip_to().get().unwrap_or("content".to_owned());
PrepareMarkup::With(html! {
body id=[page.body_id().get()] class=[page.body_classes().get()] {
@if let Some(skip) = L10n::l("skip_to_content").using(page.context().langid()) {
div class="skip__to_content" {
a href=(concat_string!("#", skip_to_id)) { (skip) }
}
}
(flex::Container::new()
.with_id("body__wrapper")
.with_direction(flex::Direction::Column(BreakPoint::None))
.with_align(flex::Align::Center)
.add_item(flex::Item::region().with_id("header"))
.add_item(flex::Item::region().with_id("pagetop"))
.add_item(
flex::Item::with(
flex::Container::new()
.with_direction(flex::Direction::Row(BreakPoint::None))
.add_item(
flex::Item::region()
.with_id("sidebar_left")
.with_grow(flex::Grow::Is1),
)
.add_item(
flex::Item::region()
.with_id("content")
.with_grow(flex::Grow::Is3),
)
.add_item(
flex::Item::region()
.with_id("sidebar_right")
.with_grow(flex::Grow::Is1),
),
)
.with_id("flex__wrapper"),
)
.add_item(flex::Item::region().with_id("footer"))
.render(page.context()))
}
})
}
fn after_prepare_body(&self, page: &mut Page) {
page.set_assets(AssetsOp::SetFaviconIfNone(
Favicon::new().with_icon("/base/favicon.ico"),
));
}
fn prepare_head(&self, page: &mut Page) -> PrepareMarkup {
fn prepare_page_head(&self, page: &mut Page) -> PrepareMarkup {
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
PrepareMarkup::With(html! {
head {
meta charset="utf-8";
@if let Some(title) = page.title() {
title { (global::SETTINGS.app.name) (" - ") (title) }
title { (global::SETTINGS.app.name) (" | ") (title) }
} @else {
title { (global::SETTINGS.app.name) }
}
@ -100,5 +51,32 @@ pub trait ThemeTrait: PackageTrait + Send + Sync {
}
})
}
*/
fn prepare_page_body(&self, page: &mut Page) -> PrepareMarkup {
PrepareMarkup::With(html! {
body id=[page.body_id().get()] class=[page.body_classes().get()] {
(page.body_content().render())
}
})
}
fn error_403(&self, request: service::HttpRequest) -> Page {
Page::new(request)
.with_title(L10n::n("Error FORBIDDEN"))
.with_body(PrepareMarkup::With(html! {
div {
h1 { ("FORBIDDEN ACCESS") }
}
}))
}
fn error_404(&self, request: service::HttpRequest) -> Page {
Page::new(request)
.with_title(L10n::n("Error RESOURCE NOT FOUND"))
.with_body(PrepareMarkup::With(html! {
div {
h1 { ("RESOURCE NOT FOUND") }
}
}))
}
}

View file

@ -1,10 +1,10 @@
//! Global settings.
use crate::static_config;
use crate::include_config;
use serde::Deserialize;
static_config!(SETTINGS: Settings => [
include_config!(SETTINGS: Settings => [
// [app]
"app.name" => "My App",
"app.description" => "Developed with the amazing PageTop framework.",

View file

@ -2,3 +2,46 @@
mod maud;
pub use maud::{html, html_private, Markup, PreEscaped, DOCTYPE};
mod assets;
pub use assets::favicon::Favicon;
pub use assets::javascript::JavaScript;
pub use assets::stylesheet::{StyleSheet, TargetMedia};
pub(crate) use assets::Assets;
mod opt_id;
pub use opt_id::OptionId;
mod opt_name;
pub use opt_name::OptionName;
mod opt_string;
pub use opt_string::OptionString;
mod opt_translated;
pub use opt_translated::OptionTranslated;
mod opt_classes;
pub use opt_classes::{ClassesOp, OptionClasses};
pub mod unit;
use crate::AutoDefault;
#[derive(AutoDefault)]
pub enum PrepareMarkup {
#[default]
None,
Escaped(String),
With(Markup),
}
impl PrepareMarkup {
pub fn render(&self) -> Markup {
match self {
PrepareMarkup::None => html! {},
PrepareMarkup::Escaped(string) => html! { (PreEscaped(string)) },
PrepareMarkup::With(markup) => html! { (markup) },
}
}
}

View file

@ -0,0 +1,53 @@
pub mod favicon;
pub mod javascript;
pub mod stylesheet;
use crate::html::{html, Markup};
use crate::{AutoDefault, Weight};
pub trait AssetsTrait {
fn name(&self) -> &String;
fn weight(&self) -> Weight;
fn prepare(&self) -> Markup;
}
#[derive(AutoDefault)]
pub(crate) struct Assets<T>(Vec<T>);
impl<T: AssetsTrait> Assets<T> {
pub fn new() -> Self {
Assets::<T>(Vec::<T>::new())
}
pub fn add(&mut self, asset: T) -> &mut Self {
match self.0.iter().position(|x| x.name() == asset.name()) {
Some(index) => {
if self.0[index].weight() > asset.weight() {
self.0.remove(index);
self.0.push(asset);
}
}
_ => self.0.push(asset),
};
self
}
pub fn remove(&mut self, name: &'static str) -> &mut Self {
if let Some(index) = self.0.iter().position(|x| x.name() == name) {
self.0.remove(index);
};
self
}
pub fn prepare(&mut self) -> Markup {
let assets = &mut self.0;
assets.sort_by_key(AssetsTrait::weight);
html! {
@for a in assets {
(a.prepare())
}
}
}
}

View file

@ -0,0 +1,93 @@
use crate::html::{html, Markup};
use crate::AutoDefault;
#[derive(AutoDefault)]
pub struct Favicon(Vec<Markup>);
impl Favicon {
pub fn new() -> Self {
Favicon::default()
}
// Favicon BUILDER.
pub fn with_icon(self, image: &str) -> Self {
self.add_icon_item("icon", image, None, None)
}
pub fn with_icon_for_sizes(self, image: &str, sizes: &str) -> Self {
self.add_icon_item("icon", image, Some(sizes), None)
}
pub fn with_apple_touch_icon(self, image: &str, sizes: &str) -> Self {
self.add_icon_item("apple-touch-icon", image, Some(sizes), None)
}
pub fn with_mask_icon(self, image: &str, color: &str) -> Self {
self.add_icon_item("mask-icon", image, None, Some(color))
}
pub fn with_manifest(self, file: &str) -> Self {
self.add_icon_item("manifest", file, None, None)
}
pub fn with_theme_color(mut self, color: &str) -> Self {
self.0.push(html! {
meta name="theme-color" content=(color);
});
self
}
pub fn with_ms_tile_color(mut self, color: &str) -> Self {
self.0.push(html! {
meta name="msapplication-TileColor" content=(color);
});
self
}
pub fn with_ms_tile_image(mut self, image: &str) -> Self {
self.0.push(html! {
meta name="msapplication-TileImage" content=(image);
});
self
}
fn add_icon_item(
mut self,
icon_rel: &str,
icon_source: &str,
icon_sizes: Option<&str>,
icon_color: Option<&str>,
) -> Self {
let icon_type = match icon_source.rfind('.') {
Some(i) => match icon_source[i..].to_owned().to_lowercase().as_str() {
".gif" => Some("image/gif"),
".ico" => Some("image/x-icon"),
".jpg" => Some("image/jpg"),
".png" => Some("image/png"),
".svg" => Some("image/svg+xml"),
_ => None,
},
_ => None,
};
self.0.push(html! {
link
rel=(icon_rel)
type=[(icon_type)]
sizes=[(icon_sizes)]
color=[(icon_color)]
href=(icon_source);
});
self
}
// Favicon PREPARE.
pub(crate) fn prepare(&self) -> Markup {
html! {
@for item in &self.0 {
(item)
}
}
}
}

View file

@ -0,0 +1,111 @@
use crate::html::assets::AssetsTrait;
use crate::html::{html, Markup};
use crate::{concat_string, AutoDefault, Weight};
#[derive(AutoDefault)]
enum Source {
#[default]
From(String),
Defer(String),
Async(String),
Inline(String, String),
OnLoad(String, String),
}
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct JavaScript {
source : Source,
prefix : &'static str,
version: &'static str,
weight : Weight,
}
impl AssetsTrait for JavaScript {
fn name(&self) -> &String {
match &self.source {
Source::From(path) => path,
Source::Defer(path) => path,
Source::Async(path) => path,
Source::Inline(name, _) => name,
Source::OnLoad(name, _) => name,
}
}
fn weight(&self) -> Weight {
self.weight
}
fn prepare(&self) -> Markup {
match &self.source {
Source::From(path) => html! {
script src=(concat_string!(path, self.prefix, self.version)) {};
},
Source::Defer(path) => html! {
script src=(concat_string!(path, self.prefix, self.version)) defer {};
},
Source::Async(path) => html! {
script src=(concat_string!(path, self.prefix, self.version)) async {};
},
Source::Inline(_, code) => html! {
script { (code) };
},
Source::OnLoad(_, code) => html! { (concat_string!(
"document.addEventListener('DOMContentLoaded',function(){",
code,
"});"
)) },
}
}
}
impl JavaScript {
pub fn from(path: impl Into<String>) -> Self {
JavaScript {
source: Source::From(path.into()),
..Default::default()
}
}
pub fn defer(path: impl Into<String>) -> Self {
JavaScript {
source: Source::Defer(path.into()),
..Default::default()
}
}
pub fn asynchronous(path: impl Into<String>) -> Self {
JavaScript {
source: Source::Async(path.into()),
..Default::default()
}
}
pub fn inline(name: impl Into<String>, script: impl Into<String>) -> Self {
JavaScript {
source: Source::Inline(name.into(), script.into()),
..Default::default()
}
}
pub fn on_load(name: impl Into<String>, script: impl Into<String>) -> Self {
JavaScript {
source: Source::OnLoad(name.into(), script.into()),
..Default::default()
}
}
pub fn with_version(mut self, version: &'static str) -> Self {
(self.prefix, self.version) = if version.is_empty() {
("", "")
} else {
("?v=", version)
};
self
}
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
}

View file

@ -0,0 +1,95 @@
use crate::html::assets::AssetsTrait;
use crate::html::{html, Markup, PreEscaped};
use crate::{concat_string, AutoDefault, Weight};
#[derive(AutoDefault)]
enum Source {
#[default]
From(String),
Inline(String, String),
}
pub enum TargetMedia {
Default,
Print,
Screen,
Speech,
}
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct StyleSheet {
source : Source,
prefix : &'static str,
version: &'static str,
media : Option<&'static str>,
weight : Weight,
}
impl AssetsTrait for StyleSheet {
fn name(&self) -> &String {
match &self.source {
Source::From(path) => path,
Source::Inline(name, _) => name,
}
}
fn weight(&self) -> Weight {
self.weight
}
fn prepare(&self) -> Markup {
match &self.source {
Source::From(path) => html! {
link
rel="stylesheet"
href=(concat_string!(path, self.prefix, self.version))
media=[self.media];
},
Source::Inline(_, code) => html! {
style { (PreEscaped(code)) };
},
}
}
}
impl StyleSheet {
pub fn from(path: impl Into<String>) -> Self {
StyleSheet {
source: Source::From(path.into()),
..Default::default()
}
}
pub fn inline(name: impl Into<String>, styles: impl Into<String>) -> Self {
StyleSheet {
source: Source::Inline(name.into(), styles.into()),
..Default::default()
}
}
pub fn with_version(mut self, version: &'static str) -> Self {
(self.prefix, self.version) = if version.is_empty() {
("", "")
} else {
("?v=", version)
};
self
}
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
#[rustfmt::skip]
pub fn for_media(mut self, media: &TargetMedia) -> Self {
self.media = match media {
TargetMedia::Default => None,
TargetMedia::Print => Some("print"),
TargetMedia::Screen => Some("screen"),
TargetMedia::Speech => Some("speech"),
};
self
}
}

View file

@ -0,0 +1,111 @@
//! **OptionClasses** implements a *helper* for dynamically adding class names to components.
//!
//! This *helper* differentiates between default classes (generally associated with styles provided
//! by the theme) and user classes (for customizing components based on application styles).
//!
//! Classes can be added using [Add]. Operations to [Remove], [Replace] or [Toggle] a class, as well
//! as [Clear] all classes, are also provided.
//!
//! **OptionClasses** assumes that the order of the classes is irrelevant
//! (<https://stackoverflow.com/a/1321712>), and duplicate classes will not be allowed.
use crate::{fn_builder, AutoDefault};
pub enum ClassesOp {
Add,
Prepend,
Remove,
Replace(String),
Toggle,
Set,
}
#[derive(AutoDefault)]
pub struct OptionClasses(Vec<String>);
impl OptionClasses {
pub fn new(classes: impl Into<String>) -> Self {
OptionClasses::default().with_value(ClassesOp::Prepend, classes)
}
// OptionClasses BUILDER.
#[fn_builder]
pub fn set_value(&mut self, op: ClassesOp, classes: impl Into<String>) -> &mut Self {
let classes: String = classes.into();
let classes: Vec<&str> = classes.split_ascii_whitespace().collect();
if classes.is_empty() {
return self;
}
match op {
ClassesOp::Add => {
self.add(&classes, self.0.len());
}
ClassesOp::Prepend => {
self.add(&classes, 0);
}
ClassesOp::Remove => {
for class in classes {
self.0.retain(|c| c.ne(&class.to_string()));
}
}
ClassesOp::Replace(classes_to_replace) => {
let mut pos = self.0.len();
let replace: Vec<&str> = classes_to_replace.split_ascii_whitespace().collect();
for class in replace {
if let Some(replace_pos) = self.0.iter().position(|c| c.eq(class)) {
self.0.remove(replace_pos);
if pos > replace_pos {
pos = replace_pos;
}
}
}
self.add(&classes, pos);
}
ClassesOp::Toggle => {
for class in classes {
if !class.is_empty() {
if let Some(pos) = self.0.iter().position(|c| c.eq(class)) {
self.0.remove(pos);
} else {
self.0.push(class.to_string());
}
}
}
}
ClassesOp::Set => {
self.0.clear();
self.add(&classes, 0);
}
}
self
}
#[inline]
fn add(&mut self, classes: &[&str], mut pos: usize) {
for &class in classes {
if !class.is_empty() && !self.0.iter().any(|c| c == class) {
self.0.insert(pos, class.to_string());
pos += 1;
}
}
}
// OptionClasses GETTERS.
pub fn get(&self) -> Option<String> {
if self.0.is_empty() {
None
} else {
Some(self.0.join(" "))
}
}
pub fn contains(&self, class: impl Into<String>) -> bool {
let class: String = class.into();
self.0.iter().any(|c| c.eq(&class))
}
}

View file

@ -0,0 +1,29 @@
use crate::{fn_builder, AutoDefault};
#[derive(AutoDefault)]
pub struct OptionId(Option<String>);
impl OptionId {
pub fn new(value: impl Into<String>) -> Self {
OptionId::default().with_value(value)
}
// OptionId BUILDER.
#[fn_builder]
pub fn set_value(&mut self, value: impl Into<String>) -> &mut Self {
self.0 = Some(value.into().trim().replace(' ', "_"));
self
}
// OptionId GETTERS.
pub fn get(&self) -> Option<String> {
if let Some(value) = &self.0 {
if !value.is_empty() {
return Some(value.to_owned());
}
}
None
}
}

View file

@ -0,0 +1,29 @@
use crate::{fn_builder, AutoDefault};
#[derive(AutoDefault)]
pub struct OptionName(Option<String>);
impl OptionName {
pub fn new(value: impl Into<String>) -> Self {
OptionName::default().with_value(value)
}
// OptionName BUILDER.
#[fn_builder]
pub fn set_value(&mut self, value: impl Into<String>) -> &mut Self {
self.0 = Some(value.into().trim().replace(' ', "_"));
self
}
// OptionName GETTERS.
pub fn get(&self) -> Option<String> {
if let Some(value) = &self.0 {
if !value.is_empty() {
return Some(value.to_owned());
}
}
None
}
}

View file

@ -0,0 +1,29 @@
use crate::{fn_builder, AutoDefault};
#[derive(AutoDefault)]
pub struct OptionString(Option<String>);
impl OptionString {
pub fn new(value: impl Into<String>) -> Self {
OptionString::default().with_value(value)
}
// OptionString BUILDER.
#[fn_builder]
pub fn set_value(&mut self, value: impl Into<String>) -> &mut Self {
self.0 = Some(value.into().trim().to_owned());
self
}
// OptionString GETTERS.
pub fn get(&self) -> Option<String> {
if let Some(value) = &self.0 {
if !value.is_empty() {
return Some(value.to_owned());
}
}
None
}
}

View file

@ -0,0 +1,30 @@
use crate::html::Markup;
use crate::locale::{L10n, LanguageIdentifier};
use crate::{fn_builder, AutoDefault};
#[derive(AutoDefault)]
pub struct OptionTranslated(L10n);
impl OptionTranslated {
pub fn new(value: L10n) -> Self {
OptionTranslated(value)
}
// OptionTranslated BUILDER.
#[fn_builder]
pub fn set_value(&mut self, value: L10n) -> &mut Self {
self.0 = value;
self
}
// OptionTranslated GETTERS.
pub fn using(&self, langid: &LanguageIdentifier) -> Option<String> {
self.0.using(langid)
}
pub fn escaped(&self, langid: &LanguageIdentifier) -> Markup {
self.0.escaped(langid)
}
}

56
pagetop/src/html/unit.rs Normal file
View file

@ -0,0 +1,56 @@
use crate::AutoDefault;
use std::fmt;
// About pixels: Pixels (px) are relative to the viewing device. For low-dpi devices, 1px is one
// device pixel (dot) of the display. For printers and high resolution screens 1px implies multiple
// device pixels.
// About em: 2em means 2 times the size of the current font. The em and rem units are practical in
// creating perfectly scalable layout!
// About viewport: If the browser window size is 50cm wide, 1vw = 0.5cm.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub enum Value {
#[default]
None,
Auto,
Cm(isize), // Centimeters.
In(isize), // Inches (1in = 96px = 2.54cm).
Mm(isize), // Millimeters.
Pc(isize), // Picas (1pc = 12pt).
Pt(isize), // Points (1pt = 1/72 of 1in).
Px(isize), // Pixels (1px = 1/96th of 1in).
RelEm(f32), // Relative to the font-size of the element.
RelPct(f32), // Percentage relative to the parent element.
RelRem(f32), // Relative to font-size of the root element.
RelVh(f32), // Relative to 1% of the height of the viewport.
RelVw(f32), // Relative to 1% of the value of the viewport.
}
#[rustfmt::skip]
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::None => write!(f, ""),
Value::Auto => write!(f, "auto"),
// Absolute value.
Value::Cm(av) => write!(f, "{av}cm"),
Value::In(av) => write!(f, "{av}in"),
Value::Mm(av) => write!(f, "{av}mm"),
Value::Pc(av) => write!(f, "{av}pc"),
Value::Pt(av) => write!(f, "{av}pt"),
Value::Px(av) => write!(f, "{av}px"),
// Relative value.
Value::RelEm(rv) => write!(f, "{rv}em"),
Value::RelPct(rv) => write!(f, "{rv}%"),
Value::RelRem(rv) => write!(f, "{rv}rem"),
Value::RelVh(rv) => write!(f, "{rv}vh"),
Value::RelVw(rv) => write!(f, "{rv}vw"),
}
}
}

View file

@ -42,7 +42,7 @@
//!
//! async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
//! Page::new(request)
//! .with_component(Html::with(html! { h1 { "Hello World!" } }))
//! .with_body(PrepareMarkup::With(html! { h1 { "Hello World!" } }))
//! .render()
//! }
//!
@ -79,7 +79,7 @@ pub use concat_string::concat_string;
/// Enables flexible identifier concatenation in macros, allowing new items with pasted identifiers.
pub use paste::paste;
pub use pagetop_macros::{html, main, test, AutoDefault};
pub use pagetop_macros::{fn_builder, html, main, test, AutoDefault};
pub type StaticResources = std::collections::HashMap<&'static str, static_files::Resource>;

View file

@ -67,13 +67,13 @@
//! # How to apply localization in your code
//!
//! Once you have created your FTL resource directory, use the
//! [`static_locales!`](crate::static_locales) macro to integrate them into your module or
//! [`include_locales!`](crate::include_locales) macro to integrate them into your module or
//! application. If your resources are located in the `"src/locale"` directory, simply declare:
//!
//! ```
//! use pagetop::prelude::*;
//!
//! static_locales!(LOCALES_SAMPLE);
//! include_locales!(LOCALES_SAMPLE);
//! ```
//!
//! But if they are in another directory, then you can use:
@ -81,7 +81,7 @@
//! ```
//! use pagetop::prelude::*;
//!
//! static_locales!(LOCALES_SAMPLE in "path/to/locale");
//! include_locales!(LOCALES_SAMPLE from "path/to/locale");
//! ```
use crate::html::{Markup, PreEscaped};
@ -89,55 +89,75 @@ use crate::{global, kv, AutoDefault};
pub use fluent_bundle::FluentValue;
pub use fluent_templates;
pub use unic_langid::LanguageIdentifier;
pub use unic_langid::{CharacterDirection, LanguageIdentifier};
use fluent_templates::Loader;
use fluent_templates::StaticLoader as Locales;
use unic_langid::langid;
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::LazyLock;
use std::fmt;
const LANGUAGE_SET_FAILURE: &str = "language_set_failure";
/// A mapping between language codes (e.g., "en-US") and their corresponding [`LanguageIdentifier`]
/// and human-readable names.
/// and locale key names.
static LANGUAGES: LazyLock<HashMap<String, (LanguageIdentifier, &str)>> = LazyLock::new(|| {
kv![
"en" => (langid!("en-US"), "English"),
"en-GB" => (langid!("en-GB"), "English (British)"),
"en-US" => (langid!("en-US"), "English (United States)"),
"es" => (langid!("es-ES"), "Spanish"),
"es-ES" => (langid!("es-ES"), "Spanish (Spain)"),
"en" => ( langid!("en-US"), "english" ),
"en-GB" => ( langid!("en-GB"), "english_british" ),
"en-US" => ( langid!("en-US"), "english_united_states" ),
"es" => ( langid!("es-ES"), "spanish" ),
"es-ES" => ( langid!("es-ES"), "spanish_spain" ),
]
});
pub static LANGID_FALLBACK: LazyLock<LanguageIdentifier> = LazyLock::new(|| langid!("en-US"));
static FALLBACK: LazyLock<LanguageIdentifier> = LazyLock::new(|| langid!("en-US"));
/// Sets the application's default
/// [Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier)
/// through `SETTINGS.app.language`.
pub static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> =
LazyLock::new(|| langid_for(&global::SETTINGS.app.language).unwrap_or(&LANGID_FALLBACK));
LazyLock::new(|| langid_for(&global::SETTINGS.app.language).unwrap_or(&FALLBACK));
pub fn langid_for(language: impl Into<String>) -> Result<&'static LanguageIdentifier, String> {
pub enum LangError {
EmptyLang,
UnknownLang(String),
}
impl fmt::Display for LangError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LangError::EmptyLang => write!(f, "The language identifier is empty."),
LangError::UnknownLang(lang) => write!(f, "Unknown language identifier: {lang}"),
}
}
}
pub fn langid_for(language: impl Into<String>) -> Result<&'static LanguageIdentifier, LangError> {
let language = language.into();
if language.is_empty() {
return Ok(&LANGID_FALLBACK);
return Err(LangError::EmptyLang);
}
LANGUAGES
.get(&language)
.map(|(langid, _)| langid)
.ok_or_else(|| format!("No langid for Unicode Language Identifier \"{language}\"."))
// Attempt to match the full language code (e.g., "es-MX").
if let Some(langid) = LANGUAGES.get(&language).map(|(langid, _)| langid) {
return Ok(langid);
}
// Fallback to the base language if no sublocale is found (e.g., "es").
if let Some((base_lang, _)) = language.split_once('-') {
if let Some(langid) = LANGUAGES.get(base_lang).map(|(langid, _)| langid) {
return Ok(langid);
}
}
Err(LangError::UnknownLang(language))
}
#[macro_export]
/// Defines a set of localization elements and local translation texts, removing Unicode isolating
/// marks around arguments to improve readability and compatibility in certain rendering contexts.
macro_rules! static_locales {
macro_rules! include_locales {
( $LOCALES:ident $(, $core_locales:literal)? ) => {
$crate::locale::fluent_templates::static_loader! {
static $LOCALES = {
@ -149,7 +169,7 @@ macro_rules! static_locales {
};
}
};
( $LOCALES:ident in $dir_locales:literal $(, $core_locales:literal)? ) => {
( $LOCALES:ident from $dir_locales:literal $(, $core_locales:literal)? ) => {
$crate::locale::fluent_templates::static_loader! {
static $LOCALES = {
locales: $dir_locales,
@ -162,7 +182,7 @@ macro_rules! static_locales {
};
}
static_locales!(LOCALES_PAGETOP);
include_locales!(LOCALES_PAGETOP);
#[derive(AutoDefault)]
enum L10nOp {
@ -180,13 +200,9 @@ pub struct L10n {
}
impl L10n {
pub fn none() -> Self {
L10n::default()
}
pub fn n(text: impl Into<String>) -> Self {
pub fn n<S: Into<Cow<'static, str>>>(text: S) -> Self {
L10n {
op: L10nOp::Text(text.into()),
op: L10nOp::Text(text.into().to_string()),
..Default::default()
}
}
@ -208,11 +224,32 @@ impl L10n {
}
pub fn with_arg(mut self, arg: impl Into<String>, value: impl Into<String>) -> Self {
self.args
.insert(arg.into(), FluentValue::from(value.into()));
let value = FluentValue::from(value.into());
self.args.insert(arg.into(), value);
self
}
pub fn add_args(mut self, args: HashMap<String, String>) -> Self {
for (k, v) in args {
self.args.insert(k, FluentValue::from(v));
}
self
}
pub fn with_count(mut self, key: impl Into<String>, count: usize) -> Self {
self.args.insert(key.into(), FluentValue::from(count));
self
}
pub fn with_date(mut self, key: impl Into<String>, date: impl Into<String>) -> Self {
self.args.insert(key.into(), FluentValue::from(date.into()));
self
}
pub fn get(&self) -> Option<String> {
self.using(&DEFAULT_LANGID)
}
pub fn using(&self, langid: &LanguageIdentifier) -> Option<String> {
match &self.op {
L10nOp::None => None,
@ -230,13 +267,13 @@ impl L10n {
}
}
/// Escapes the content using the default language identifier.
/// Escapes translated text using the default language identifier.
pub fn markup(&self) -> Markup {
let content = self.using(&DEFAULT_LANGID).unwrap_or_default();
let content = self.get().unwrap_or_default();
PreEscaped(content)
}
/// Escapes the content using the specified language identifier.
/// Escapes translated text using the specified language identifier.
pub fn escaped(&self, langid: &LanguageIdentifier) -> Markup {
let content = self.using(langid).unwrap_or_default();
PreEscaped(content)
@ -245,37 +282,11 @@ impl L10n {
impl fmt::Display for L10n {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.op {
L10nOp::None => write!(f, ""),
L10nOp::Text(text) => write!(f, "{text}"),
L10nOp::Translate(key) => {
if let Some(locales) = self.locales {
write!(
f,
"{}",
if self.args.is_empty() {
locales.lookup(
match key.as_str() {
LANGUAGE_SET_FAILURE => &LANGID_FALLBACK,
_ => &DEFAULT_LANGID,
},
key,
)
} else {
locales.lookup_with_args(
match key.as_str() {
LANGUAGE_SET_FAILURE => &LANGID_FALLBACK,
_ => &DEFAULT_LANGID,
},
key,
&self.args,
)
}
)
} else {
write!(f, "Unknown localization {key}")
}
}
}
let content = match &self.op {
L10nOp::None => "".to_string(),
L10nOp::Text(text) => text.clone(),
L10nOp::Translate(key) => self.get().unwrap_or_else(|| format!("No <{}>", key)),
};
write!(f, "{}", content)
}
}

View file

@ -0,0 +1,5 @@
english = English
english_british = English (British)
english_united_states = English (United States)
spanish = Spanish
spanish_spain = Spanish (Spain)

View file

@ -0,0 +1,5 @@
english = Inglés
english_british = Inglés (Gran Bretaña)
english_united_states = Inglés (Estados Unidos)
spanish = Español
spanish_spain = Español (España)

View file

@ -2,18 +2,18 @@
// RE-EXPORTED.
pub use crate::{concat_string, html, main, paste, test};
pub use crate::{concat_string, fn_builder, html, main, paste, test};
pub use crate::{AutoDefault, StaticResources, TypeId, Weight};
// MACROS.
// crate::util
pub use crate::{kv, static_config};
pub use crate::{include_config, kv};
// crate::locale
pub use crate::static_locales;
pub use crate::include_locales;
// crate::service
pub use crate::{static_files, static_files_service};
pub use crate::{include_files, include_files_service};
// crate::core::action
pub use crate::actions;
@ -36,7 +36,7 @@ pub use crate::core::action::*;
pub use crate::core::package::*;
pub use crate::core::theme::*;
pub use crate::response::{json::*, redirect::*, ResponseError};
pub use crate::response::{json::*, page::*, redirect::*, ResponseError};
pub use crate::global;

View file

@ -2,6 +2,8 @@
pub use actix_web::ResponseError;
pub mod page;
pub mod json;
pub mod redirect;

View file

@ -0,0 +1,195 @@
mod error;
pub use error::ErrorPage;
mod context;
pub use context::{AssetsOp, ContextPage /*, ParamError*/};
/*
pub type FnContextualPath = fn(cx: &Context) -> &str;
*/
use crate::fn_builder;
use crate::html::{html, Markup, PrepareMarkup, DOCTYPE};
use crate::html::{ClassesOp, OptionClasses, OptionId, OptionTranslated};
use crate::locale::L10n;
use crate::service::HttpRequest;
pub use actix_web::Result as ResultPage;
use unic_langid::CharacterDirection;
#[rustfmt::skip]
pub struct Page {
title : OptionTranslated,
description : OptionTranslated,
metadata : Vec<(&'static str, &'static str)>,
properties : Vec<(&'static str, &'static str)>,
context : ContextPage,
body_id : OptionId,
body_classes: OptionClasses,
body_content: PrepareMarkup,
}
impl Page {
#[rustfmt::skip]
pub fn new(request: HttpRequest) -> Self {
Page {
title : OptionTranslated::default(),
description : OptionTranslated::default(),
metadata : Vec::default(),
properties : Vec::default(),
context : ContextPage::new(request),
body_id : OptionId::default(),
body_classes: OptionClasses::default(),
body_content: PrepareMarkup::default(),
}
}
// Page BUILDER.
#[fn_builder]
pub fn set_title(&mut self, title: L10n) -> &mut Self {
self.title.set_value(title);
self
}
#[fn_builder]
pub fn set_description(&mut self, description: L10n) -> &mut Self {
self.description.set_value(description);
self
}
#[fn_builder]
pub fn set_metadata(&mut self, name: &'static str, content: &'static str) -> &mut Self {
self.metadata.push((name, content));
self
}
#[fn_builder]
pub fn set_property(&mut self, property: &'static str, content: &'static str) -> &mut Self {
self.metadata.push((property, content));
self
}
#[fn_builder]
pub fn set_assets(&mut self, op: AssetsOp) -> &mut Self {
self.context.set_assets(op);
self
}
#[fn_builder]
pub fn set_body_id(&mut self, id: impl Into<String>) -> &mut Self {
self.body_id.set_value(id);
self
}
#[fn_builder]
pub fn set_body_classes(&mut self, op: ClassesOp, classes: impl Into<String>) -> &mut Self {
self.body_classes.set_value(op, classes);
self
}
#[fn_builder]
pub fn set_body(&mut self, content: PrepareMarkup) -> &mut Self {
self.body_content = content;
self
}
/*
#[fn_builder]
pub fn set_layout(&mut self, layout: &'static str) -> &mut Self {
self.context.set_assets(AssetsOp::Layout(layout));
self
}
#[fn_builder]
pub fn set_regions(&mut self, region: &'static str, op: AnyOp) -> &mut Self {
self.context.set_regions(region, op);
self
}
pub fn with_component(mut self, component: impl ComponentTrait) -> Self {
self.context
.set_regions("content", AnyOp::Add(AnyComponent::with(component)));
self
}
pub fn with_component_in(
mut self,
region: &'static str,
component: impl ComponentTrait,
) -> Self {
self.context
.set_regions(region, AnyOp::Add(AnyComponent::with(component)));
self
}
*/
// Page GETTERS.
pub fn title(&mut self) -> Option<String> {
self.title.using(self.context.langid())
}
pub fn description(&mut self) -> Option<String> {
self.description.using(self.context.langid())
}
pub fn metadata(&self) -> &Vec<(&str, &str)> {
&self.metadata
}
pub fn properties(&self) -> &Vec<(&str, &str)> {
&self.properties
}
pub fn context(&mut self) -> &mut ContextPage {
&mut self.context
}
pub fn body_id(&self) -> &OptionId {
&self.body_id
}
pub fn body_classes(&self) -> &OptionClasses {
&self.body_classes
}
pub fn body_content(&self) -> &PrepareMarkup {
&self.body_content
}
// Page RENDER.
pub fn render(&mut self) -> ResultPage<Markup, ErrorPage> {
// Theme operations before preparing the page body.
//self.context.theme().before_prepare_body(self);
// Packages actions before preparing the page body.
//action::page::BeforePrepareBody::dispatch(self);
// Prepare page body.
let body = self.context.theme().prepare_page_body(self);
// Theme operations after preparing the page body.
//self.context.theme().after_prepare_body(self);
// Packages actions after preparing the page body.
//action::page::AfterPrepareBody::dispatch(self);
// Prepare page head.
let head = self.context.theme().prepare_page_head(self);
// Render the page.
let lang = self.context.langid().language.as_str();
let dir = match self.context.langid().character_direction() {
CharacterDirection::LTR => "ltr",
CharacterDirection::RTL => "rtl",
CharacterDirection::TTB => "auto",
};
Ok(html! {
(DOCTYPE)
html lang=(lang) dir=(dir) {
(head.render())
(body.render())
}
})
}
}

View file

@ -0,0 +1,208 @@
/*
use crate::base::component::add_base_assets;
use crate::concat_string;
use crate::core::component::AnyOp; */
use crate::core::theme::all::{theme_by_short_name, DEFAULT_THEME};
use crate::core::theme::{/*ComponentsInRegions,*/ ThemeRef};
/* use crate::global::TypeInfo; */
use crate::html::{html, Markup};
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
use crate::locale::{LanguageIdentifier, DEFAULT_LANGID};
use crate::service::HttpRequest;
/*
use std::collections::HashMap;
use std::error::Error;
use std::str::FromStr;
use std::fmt;
*/
pub enum AssetsOp {
LangId(&'static LanguageIdentifier),
Theme(&'static str),
//Layout(&'static str),
// Favicon.
SetFavicon(Option<Favicon>),
SetFaviconIfNone(Favicon),
// Stylesheets.
AddStyleSheet(StyleSheet),
RemoveStyleSheet(&'static str),
// JavaScripts.
AddJavaScript(JavaScript),
RemoveJavaScript(&'static str),
// Add assets to properly use base components.
//AddBaseAssets,
}
/*
#[derive(Debug)]
pub enum ParamError {
NotFound,
ParseError(String),
}
impl fmt::Display for ParamError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParamError::NotFound => write!(f, "Parameter not found"),
ParamError::ParseError(e) => write!(f, "Parse error: {e}"),
}
}
}
impl Error for ParamError {}
*/
#[rustfmt::skip]
pub struct ContextPage {
request : HttpRequest,
langid : &'static LanguageIdentifier,
theme : ThemeRef, /*
layout : &'static str, */
favicon : Option<Favicon>,
stylesheet: Assets<StyleSheet>,
javascript: Assets<JavaScript>, /*
regions : ComponentsInRegions,
params : HashMap<&'static str, String>,
id_counter: usize, */
}
impl ContextPage {
#[rustfmt::skip]
pub(crate) fn new(request: HttpRequest) -> Self {
ContextPage {
request,
langid : &DEFAULT_LANGID,
theme : *DEFAULT_THEME, /*
layout : "default", */
favicon : None,
stylesheet: Assets::<StyleSheet>::new(),
javascript: Assets::<JavaScript>::new(), /*
regions : ComponentsInRegions::default(),
params : HashMap::<&str, String>::new(),
id_counter: 0,*/
}
}
pub fn set_assets(&mut self, op: AssetsOp) -> &mut Self {
match op {
AssetsOp::LangId(langid) => {
self.langid = langid;
}
AssetsOp::Theme(theme_name) => {
self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME);
} /*
AssetsOp::Layout(layout) => {
self.layout = layout;
} */
// Favicon.
AssetsOp::SetFavicon(favicon) => {
self.favicon = favicon;
}
AssetsOp::SetFaviconIfNone(icon) => {
if self.favicon.is_none() {
self.favicon = Some(icon);
}
}
// Stylesheets.
AssetsOp::AddStyleSheet(css) => {
self.stylesheet.add(css);
}
AssetsOp::RemoveStyleSheet(path) => {
self.stylesheet.remove(path);
}
// JavaScripts.
AssetsOp::AddJavaScript(js) => {
self.javascript.add(js);
}
AssetsOp::RemoveJavaScript(path) => {
self.javascript.remove(path);
} /*
// Add assets to properly use base components.
AssetsOp::AddBaseAssets => {
add_base_assets(self);
} */
}
self
}
/*
pub fn set_regions(&mut self, region: &'static str, op: AnyOp) -> &mut Self {
self.regions.set_components(region, op);
self
}
pub fn set_param<T: FromStr + ToString>(&mut self, key: &'static str, value: &T) -> &mut Self {
self.params.insert(key, value.to_string());
self
}
*/
// Context GETTERS.
pub fn request(&self) -> &HttpRequest {
&self.request
}
pub fn langid(&self) -> &LanguageIdentifier {
self.langid
}
pub fn theme(&self) -> ThemeRef {
self.theme
}
/*
pub fn layout(&self) -> &str {
self.layout
}
pub fn regions(&self) -> &ComponentsInRegions {
&self.regions
}
pub fn get_param<T: FromStr + ToString>(&self, key: &'static str) -> Result<T, ParamError> {
self.params
.get(key)
.ok_or(ParamError::NotFound)
.and_then(|v| T::from_str(v).map_err(|_| ParamError::ParseError(v.clone())))
}
*/
// Context PREPARE.
pub(crate) fn prepare_assets(&mut self) -> Markup {
html! {
@if let Some(favicon) = &self.favicon {
(favicon.prepare())
}
(self.stylesheet.prepare())
(self.javascript.prepare())
}
}
/*
pub(crate) fn prepare_region(&mut self, region: impl Into<String>) -> Markup {
self.regions
.all_components(self.theme, region.into().as_str())
.render(self)
}
// Context EXTRAS.
pub fn remove_param(&mut self, key: &'static str) -> bool {
self.params.remove(key).is_some()
}
pub fn required_id<T>(&mut self, id: Option<String>) -> String {
if let Some(id) = id {
id
} else {
let prefix = TypeInfo::ShortName
.of::<T>()
.trim()
.replace(' ', "_")
.to_lowercase();
let prefix = if prefix.is_empty() {
"prefix".to_owned()
} else {
prefix
};
self.id_counter += 1;
concat_string!(prefix, "-", self.id_counter.to_string())
}
} */
}

View file

@ -0,0 +1,71 @@
use crate::core::theme::all::DEFAULT_THEME;
use crate::response::ResponseError;
use crate::service::http::{header::ContentType, StatusCode};
use crate::service::{HttpRequest, HttpResponse};
use std::fmt;
#[derive(Debug)]
pub enum ErrorPage {
NotModified(HttpRequest),
BadRequest(HttpRequest),
AccessDenied(HttpRequest),
NotFound(HttpRequest),
PreconditionFailed(HttpRequest),
InternalError(HttpRequest),
Timeout(HttpRequest),
}
impl fmt::Display for ErrorPage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
// Error 304.
ErrorPage::NotModified(_) => write!(f, "Not Modified"),
// Error 400.
ErrorPage::BadRequest(_) => write!(f, "Bad Client Data"),
// Error 403.
ErrorPage::AccessDenied(request) => {
if let Ok(page) = DEFAULT_THEME.error_403(request.clone()).render() {
write!(f, "{}", page.into_string())
} else {
write!(f, "Access Denied")
}
}
// Error 404.
ErrorPage::NotFound(request) => {
if let Ok(page) = DEFAULT_THEME.error_404(request.clone()).render() {
write!(f, "{}", page.into_string())
} else {
write!(f, "Not Found")
}
}
// Error 412.
ErrorPage::PreconditionFailed(_) => write!(f, "Precondition Failed"),
// Error 500.
ErrorPage::InternalError(_) => write!(f, "Internal Error"),
// Error 504.
ErrorPage::Timeout(_) => write!(f, "Timeout"),
}
}
}
impl ResponseError for ErrorPage {
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code())
.insert_header(ContentType::html())
.body(self.to_string())
}
#[rustfmt::skip]
fn status_code(&self) -> StatusCode {
match self {
ErrorPage::NotModified(_) => StatusCode::NOT_MODIFIED,
ErrorPage::BadRequest(_) => StatusCode::BAD_REQUEST,
ErrorPage::AccessDenied(_) => StatusCode::FORBIDDEN,
ErrorPage::NotFound(_) => StatusCode::NOT_FOUND,
ErrorPage::PreconditionFailed(_) => StatusCode::PRECONDITION_FAILED,
ErrorPage::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
ErrorPage::Timeout(_) => StatusCode::GATEWAY_TIMEOUT,
}
}
}

View file

@ -13,7 +13,7 @@ pub use actix_web_files::Files as ActixFiles;
pub use actix_web_static_files::ResourceFiles;
#[macro_export]
macro_rules! static_files {
macro_rules! include_files {
( $bundle:ident ) => {
$crate::paste! {
mod [<static_files_ $bundle>] {
@ -34,11 +34,12 @@ macro_rules! static_files {
}
#[macro_export]
macro_rules! static_files_service {
macro_rules! include_files_service {
( $scfg:ident, $bundle:ident => $path:expr $(, [$root:expr, $relative:expr])? ) => {{
$crate::paste! {
let span = $crate::trace::debug_span!("Configuring static files ", path = $path);
let _ = span.in_scope(|| {
#[allow(unused_mut)]
let mut serve_embedded:bool = true;
$(
if !$root.is_empty() && !$relative.is_empty() {

View file

@ -218,7 +218,7 @@ macro_rules! kv {
/// serde = { version = "1.0", features = ["derive"] }
/// ```
///
/// Y luego inicializa con la macro [`static_config!`](crate::static_config) tus ajustes, usando
/// Y luego inicializa con la macro [`include_config!`](crate::include_config) tus ajustes, usando
/// tipos seguros y asignando los valores predefinidos para la estructura asociada:
///
/// ```
@ -238,7 +238,7 @@ macro_rules! kv {
/// pub height: u16,
/// }
///
/// static_config!(SETTINGS: Settings => [
/// include_config!(SETTINGS: Settings from [
/// // [myapp]
/// "myapp.name" => "Value Name",
/// "myapp.width" => 900,
@ -278,7 +278,7 @@ macro_rules! kv {
/// println!("{}", &config::SETTINGS.myapp.width);
/// }
/// ```
macro_rules! static_config {
macro_rules! include_config {
( $SETTINGS:ident: $Settings:ty => [ $($key:literal => $value:literal),* $(,)? ] ) => {
#[doc = concat!(
"Assigned or predefined values for configuration settings associated to the ",

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB