✨ Reintroduce web response type for full pages
This commit is contained in:
parent
04a365797d
commit
b2b0d8bd3e
40 changed files with 1516 additions and 249 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1659,6 +1659,7 @@ dependencies = [
|
|||
"fluent-templates",
|
||||
"itoa",
|
||||
"nom",
|
||||
"pagetop-build",
|
||||
"pagetop-macros",
|
||||
"paste",
|
||||
"serde",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
} */
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
7
pagetop/build.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
use pagetop_build::StaticFilesBundle;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
StaticFilesBundle::from_dir("../static", None)
|
||||
.with_name("assets")
|
||||
.build()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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 => "/");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ pub trait PackageTrait: AnyBase + Send + Sync {
|
|||
}
|
||||
|
||||
fn description(&self) -> L10n {
|
||||
L10n::none()
|
||||
L10n::default()
|
||||
}
|
||||
|
||||
fn theme(&self) -> Option<ThemeRef> {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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") }
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
53
pagetop/src/html/assets.rs
Normal file
53
pagetop/src/html/assets.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
93
pagetop/src/html/assets/favicon.rs
Normal file
93
pagetop/src/html/assets/favicon.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
111
pagetop/src/html/assets/javascript.rs
Normal file
111
pagetop/src/html/assets/javascript.rs
Normal 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
|
||||
}
|
||||
}
|
||||
95
pagetop/src/html/assets/stylesheet.rs
Normal file
95
pagetop/src/html/assets/stylesheet.rs
Normal 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
|
||||
}
|
||||
}
|
||||
111
pagetop/src/html/opt_classes.rs
Normal file
111
pagetop/src/html/opt_classes.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
29
pagetop/src/html/opt_id.rs
Normal file
29
pagetop/src/html/opt_id.rs
Normal 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
|
||||
}
|
||||
}
|
||||
29
pagetop/src/html/opt_name.rs
Normal file
29
pagetop/src/html/opt_name.rs
Normal 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
|
||||
}
|
||||
}
|
||||
29
pagetop/src/html/opt_string.rs
Normal file
29
pagetop/src/html/opt_string.rs
Normal 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
|
||||
}
|
||||
}
|
||||
30
pagetop/src/html/opt_translated.rs
Normal file
30
pagetop/src/html/opt_translated.rs
Normal 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
56
pagetop/src/html/unit.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
pagetop/src/locale/en-US/languages.ftl
Normal file
5
pagetop/src/locale/en-US/languages.ftl
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
english = English
|
||||
english_british = English (British)
|
||||
english_united_states = English (United States)
|
||||
spanish = Spanish
|
||||
spanish_spain = Spanish (Spain)
|
||||
5
pagetop/src/locale/es-ES/languages.ftl
Normal file
5
pagetop/src/locale/es-ES/languages.ftl
Normal 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)
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
pub use actix_web::ResponseError;
|
||||
|
||||
pub mod page;
|
||||
|
||||
pub mod json;
|
||||
|
||||
pub mod redirect;
|
||||
|
|
|
|||
195
pagetop/src/response/page.rs
Normal file
195
pagetop/src/response/page.rs
Normal 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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
208
pagetop/src/response/page/context.rs
Normal file
208
pagetop/src/response/page/context.rs
Normal 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())
|
||||
}
|
||||
} */
|
||||
}
|
||||
71
pagetop/src/response/page/error.rs
Normal file
71
pagetop/src/response/page/error.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
BIN
static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Loading…
Add table
Add a link
Reference in a new issue