Añade composición de páginas basada en componentes
This commit is contained in:
parent
0e3300dc90
commit
24e773c17b
30 changed files with 895 additions and 31 deletions
|
|
@ -32,10 +32,11 @@ config_rs = { package = "config", version = "0.11.0", features = ["toml"] }
|
||||||
fluent-templates = "0.6.1"
|
fluent-templates = "0.6.1"
|
||||||
unic-langid = "0.9.0"
|
unic-langid = "0.9.0"
|
||||||
|
|
||||||
actix-web = "4.0.0-rc.3"
|
actix-web = "3.3.3"
|
||||||
sycamore = { version = "0.8.0-beta.2", features = ["ssr"] }
|
maud = { version = "0.23.0", features = ["actix-web"] }
|
||||||
|
sycamore = { version = "0.7.1", features = ["ssr"] }
|
||||||
|
downcast-rs = "1.2.0"
|
||||||
|
|
||||||
tokio = { version = "1.16", features = ["macros", "rt-multi-thread"] }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,5 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pagetop = { path = "pagetop" }
|
pagetop = { path = "pagetop" }
|
||||||
tokio = { version = "1.16", features = ["macros", "rt-multi-thread"] }
|
actix-web = "3.3.3"
|
||||||
|
maud = { version = "0.23.0" }
|
||||||
[[bin]]
|
|
||||||
name = "app"
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
[app]
|
[app]
|
||||||
name = "PageTop Essence"
|
name = "PageTop Essence"
|
||||||
|
language = "es-ES"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ name = "PageTop Application"
|
||||||
description = "Developed with the amazing PageTop framework."
|
description = "Developed with the amazing PageTop framework."
|
||||||
# Idioma (localización) predeterminado.
|
# Idioma (localización) predeterminado.
|
||||||
language = "en-US"
|
language = "en-US"
|
||||||
|
# Tema predeterminado.
|
||||||
|
theme = "Minimal"
|
||||||
|
|
||||||
[webserver]
|
[webserver]
|
||||||
# Configuración opcional del servidor web.
|
# Configuración opcional del servidor web.
|
||||||
|
|
|
||||||
77
src/base/component/chunck.rs
Normal file
77
src/base/component/chunck.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub struct Chunck {
|
||||||
|
renderable: fn() -> bool,
|
||||||
|
weight : i8,
|
||||||
|
markup : Vec<Markup>,
|
||||||
|
template : String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PageComponent for Chunck {
|
||||||
|
|
||||||
|
fn prepare() -> Self {
|
||||||
|
Chunck {
|
||||||
|
renderable: always,
|
||||||
|
weight : 0,
|
||||||
|
markup : Vec::new(),
|
||||||
|
template : "default".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_renderable(&self) -> bool {
|
||||||
|
(self.renderable)()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn weight(&self) -> i8 {
|
||||||
|
self.weight
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_render(&self, _: &mut PageAssets) -> Markup {
|
||||||
|
html! {
|
||||||
|
@for markup in self.markup.iter() {
|
||||||
|
(*markup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Chunck {
|
||||||
|
|
||||||
|
// Chunck BUILDER.
|
||||||
|
|
||||||
|
pub fn markup(markup: Markup) -> Self {
|
||||||
|
let mut chunck = Chunck::prepare();
|
||||||
|
chunck.markup.push(markup);
|
||||||
|
chunck
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_renderable(mut self, renderable: fn() -> bool) -> Self {
|
||||||
|
self.renderable = renderable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_weight(mut self, weight: i8) -> Self {
|
||||||
|
self.weight = weight;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_markup(mut self, markup: Markup) -> Self {
|
||||||
|
self.markup.push(markup);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn using_template(mut self, template: &str) -> Self {
|
||||||
|
self.template = template.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunck GETTERS.
|
||||||
|
|
||||||
|
pub fn template(&self) -> &str {
|
||||||
|
self.template.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn always() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
103
src/base/component/container.rs
Normal file
103
src/base/component/container.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
enum ContainerType { Column, Row, Wrapper }
|
||||||
|
|
||||||
|
pub struct Container {
|
||||||
|
renderable: fn() -> bool,
|
||||||
|
weight : i8,
|
||||||
|
id : String,
|
||||||
|
container : ContainerType,
|
||||||
|
components: PageContainer,
|
||||||
|
template : String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PageComponent for Container {
|
||||||
|
|
||||||
|
fn prepare() -> Self {
|
||||||
|
Container {
|
||||||
|
renderable: always,
|
||||||
|
weight : 0,
|
||||||
|
id : "".to_string(),
|
||||||
|
container : ContainerType::Wrapper,
|
||||||
|
components: PageContainer::new(),
|
||||||
|
template : "default".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_renderable(&self) -> bool {
|
||||||
|
(self.renderable)()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn weight(&self) -> i8 {
|
||||||
|
self.weight
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_render(&self, assets: &mut PageAssets) -> Markup {
|
||||||
|
let classes = match self.container {
|
||||||
|
ContainerType::Wrapper => "wrapper",
|
||||||
|
ContainerType::Row => "row",
|
||||||
|
ContainerType::Column => "col",
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
div class=(classes) {
|
||||||
|
(self.components.render(assets))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Container {
|
||||||
|
|
||||||
|
// Container BUILDER.
|
||||||
|
|
||||||
|
pub fn row() -> Self {
|
||||||
|
let mut grid = Container::prepare();
|
||||||
|
grid.container = ContainerType::Row;
|
||||||
|
grid
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn column() -> Self {
|
||||||
|
let mut grid = Container::prepare();
|
||||||
|
grid.container = ContainerType::Column;
|
||||||
|
grid
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_renderable(mut self, renderable: fn() -> bool) -> Self {
|
||||||
|
self.renderable = renderable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_weight(mut self, weight: i8) -> Self {
|
||||||
|
self.weight = weight;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_id(mut self, id: &str) -> Self {
|
||||||
|
self.id = id.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(mut self, component: impl PageComponent) -> Self {
|
||||||
|
self.components.add(component);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn using_template(mut self, template: &str) -> Self {
|
||||||
|
self.template = template.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid GETTERS.
|
||||||
|
|
||||||
|
pub fn id(&self) -> &str {
|
||||||
|
self.id.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn template(&self) -> &str {
|
||||||
|
self.template.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn always() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
2
src/base/component/mod.rs
Normal file
2
src/base/component/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod container;
|
||||||
|
pub mod chunck;
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
//! Temas, Módulos y Componentes base.
|
//! Temas, Módulos y Componentes base.
|
||||||
|
|
||||||
|
pub mod theme;
|
||||||
pub mod module;
|
pub mod module;
|
||||||
|
pub mod component;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
module_name = Default homepage
|
module_name = Default homepage
|
||||||
module_desc = Displays a default homepage when none is configured.
|
module_desc = Displays a default homepage when none is configured.
|
||||||
|
|
||||||
greeting = Hello { $name }!
|
greetings = Hello { $name }!
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
module_name = Página de inicio predeterminada
|
module_name = Página de inicio predeterminada
|
||||||
module_desc = Muestra una página de inicio predeterminada cuando no hay ninguna configurada.
|
module_desc = Muestra una página de inicio predeterminada cuando no hay ninguna configurada.
|
||||||
|
|
||||||
greeting = Hola { $name }!
|
greetings = ¡Hola { $name }!
|
||||||
|
|
|
||||||
|
|
@ -14,26 +14,16 @@ impl Module for HomepageModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configure_module(&self, cfg: &mut server::web::ServiceConfig) {
|
fn configure_module(&self, cfg: &mut server::web::ServiceConfig) {
|
||||||
cfg.service(
|
cfg.service(server::web::resource("/").to(home));
|
||||||
server::web::resource("/")
|
cfg.service(server::web::resource("/{name}").to(home));
|
||||||
.route(server::web::get().to(greet))
|
|
||||||
);
|
|
||||||
cfg.service(
|
|
||||||
server::web::resource("/{name}")
|
|
||||||
.route(server::web::get().to(greet_with_param))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn greet() -> impl server::Responder {
|
async fn home(req: server::HttpRequest) -> server::Result<Markup> {
|
||||||
t("greeting", &args!["name" => config_get!("app.name")])
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn greet_with_param(req: server::HttpRequest) -> server::HttpResponse {
|
|
||||||
let name: String = req.match_info().get("name").unwrap_or("World").into();
|
let name: String = req.match_info().get("name").unwrap_or("World").into();
|
||||||
let args = args!["name" => name];
|
Page::prepare()
|
||||||
server::HttpResponse::Ok()
|
.add_to("content", Chunck::markup(html! {
|
||||||
.body(sycamore::render_to_string(|ctx| sycamore::view! { ctx,
|
h1 { (t("greetings", &args![ "name" => name])) }
|
||||||
p { (t("greeting", &args)) }
|
|
||||||
}))
|
}))
|
||||||
|
.render()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
src/base/theme/minimal/mod.rs
Normal file
9
src/base/theme/minimal/mod.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub struct MinimalTheme;
|
||||||
|
|
||||||
|
impl Theme for MinimalTheme {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"Minimal"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/base/theme/mod.rs
Normal file
1
src/base/theme/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod minimal;
|
||||||
|
|
@ -73,6 +73,7 @@ pub struct App {
|
||||||
pub name : String,
|
pub name : String,
|
||||||
pub description : String,
|
pub description : String,
|
||||||
pub language : String,
|
pub language : String,
|
||||||
|
pub theme : String,
|
||||||
pub run_mode : String,
|
pub run_mode : String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,6 +99,7 @@ Ajustes globales y valores predeterminados para las secciones *\[app\]* y
|
||||||
"app.name" => "PageTop Application",
|
"app.name" => "PageTop Application",
|
||||||
"app.description" => "Developed with the amazing PageTop framework.",
|
"app.description" => "Developed with the amazing PageTop framework.",
|
||||||
"app.language" => "en-US",
|
"app.language" => "en-US",
|
||||||
|
"app.theme" => "Minimal",
|
||||||
|
|
||||||
// [webserver]
|
// [webserver]
|
||||||
"webserver.bind_address" => "localhost",
|
"webserver.bind_address" => "localhost",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
use crate::core::{server, state};
|
use crate::core::{server, state};
|
||||||
|
|
||||||
|
pub fn themes(cfg: &mut server::web::ServiceConfig) {
|
||||||
|
for t in state::THEMES.read().unwrap().iter() {
|
||||||
|
t.configure_theme(cfg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn modules(cfg: &mut server::web::ServiceConfig) {
|
pub fn modules(cfg: &mut server::web::ServiceConfig) {
|
||||||
for m in state::MODULES.read().unwrap().iter() {
|
for m in state::MODULES.read().unwrap().iter() {
|
||||||
m.configure_module(cfg);
|
m.configure_module(cfg);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
pub use actix_web::dev::Server;
|
pub use actix_web::dev::Server;
|
||||||
|
|
||||||
mod state;
|
mod state;
|
||||||
|
pub use state::register_theme;
|
||||||
pub use state::register_module;
|
pub use state::register_module;
|
||||||
|
pub use state::add_component_to;
|
||||||
|
|
||||||
mod all;
|
mod all;
|
||||||
|
|
||||||
|
pub mod theme;
|
||||||
pub mod module;
|
pub mod module;
|
||||||
|
pub mod response;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
|
|
||||||
1
src/core/response/mod.rs
Normal file
1
src/core/response/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod page;
|
||||||
258
src/core/response/page/assets.rs
Normal file
258
src/core/response/page/assets.rs
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
use crate::core::theme::{Markup, PreEscaped, html};
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Favicon.
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub struct Favicon(Vec<String>);
|
||||||
|
|
||||||
|
impl Favicon {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Favicon(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_icon(self, image: &str) -> Self {
|
||||||
|
self.add_item("icon", image, "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_icon_for_sizes(self, image: &str, sizes: &str) -> Self {
|
||||||
|
self.add_item("icon", image, sizes, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_apple_touch_icon(self, image: &str, sizes: &str) -> Self {
|
||||||
|
self.add_item("apple-touch-icon", image, sizes, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_mask_icon(self, image: &str, color: &str) -> Self {
|
||||||
|
self.add_item("mask-icon", image, "", color)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_manifest(self, file: &str) -> Self {
|
||||||
|
self.add_item("manifest", file, "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_theme_color(mut self, color: &str) -> Self {
|
||||||
|
self.0.push(format!(
|
||||||
|
"<meta name=\"theme-color\" content=\"{}\">", color
|
||||||
|
));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_ms_tile_color(mut self, color: &str) -> Self {
|
||||||
|
self.0.push(format!(
|
||||||
|
"<meta name=\"msapplication-TileColor\" content=\"{}\">", color
|
||||||
|
));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_ms_tile_image(mut self, image: &str) -> Self {
|
||||||
|
self.0.push(format!(
|
||||||
|
"<meta name=\"msapplication-TileImage\" content=\"{}\">", image
|
||||||
|
));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_item(
|
||||||
|
mut self,
|
||||||
|
rel : &str,
|
||||||
|
source: &str,
|
||||||
|
sizes : &str,
|
||||||
|
color : &str
|
||||||
|
) -> Self {
|
||||||
|
let mut link: String = format!("<link rel=\"{}\"", rel);
|
||||||
|
if let Some(i) = source.rfind('.') {
|
||||||
|
link = match source[i..].to_string().to_lowercase().as_str() {
|
||||||
|
".gif" => format!("{} type=\"image/gif\"", link),
|
||||||
|
".ico" => format!("{} type=\"image/x-icon\"", link),
|
||||||
|
".jpg" => format!("{} type=\"image/jpg\"", link),
|
||||||
|
".png" => format!("{} type=\"image/png\"", link),
|
||||||
|
".svg" => format!("{} type=\"image/svg+xml\"", link),
|
||||||
|
_ => link
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if !sizes.is_empty() {
|
||||||
|
link = format!("{} sizes=\"{}\"", link, sizes);
|
||||||
|
}
|
||||||
|
if !color.is_empty() {
|
||||||
|
link = format!("{} color=\"{}\"", link, color);
|
||||||
|
}
|
||||||
|
self.0.push(format!("{} href=\"{}\">", link, source));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
html! {
|
||||||
|
@for item in &self.0 {
|
||||||
|
(PreEscaped(item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// StyleSheet.
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub struct StyleSheet {
|
||||||
|
source: &'static str,
|
||||||
|
weight: i8,
|
||||||
|
}
|
||||||
|
impl StyleSheet {
|
||||||
|
pub fn source(s: &'static str) -> Self {
|
||||||
|
StyleSheet {
|
||||||
|
source: s,
|
||||||
|
weight: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_weight(mut self, weight: i8) -> Self {
|
||||||
|
self.weight = weight;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn weight(self) -> i8 {
|
||||||
|
self.weight
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
html! {
|
||||||
|
link rel="stylesheet" href=(self.source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// JavaScript.
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum JSMode { Async, Defer, Normal }
|
||||||
|
|
||||||
|
pub struct JavaScript {
|
||||||
|
source: &'static str,
|
||||||
|
weight: i8,
|
||||||
|
mode : JSMode,
|
||||||
|
}
|
||||||
|
impl JavaScript {
|
||||||
|
pub fn source(s: &'static str) -> Self {
|
||||||
|
JavaScript {
|
||||||
|
source: s,
|
||||||
|
weight: 0,
|
||||||
|
mode : JSMode::Defer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_weight(mut self, weight: i8) -> Self {
|
||||||
|
self.weight = weight;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_mode(mut self, mode: JSMode) -> Self {
|
||||||
|
self.mode = mode;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn weight(self) -> i8 {
|
||||||
|
self.weight
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
html! {
|
||||||
|
script type="text/javascript"
|
||||||
|
src=(self.source)
|
||||||
|
async[self.mode == JSMode::Async]
|
||||||
|
defer[self.mode == JSMode::Defer]
|
||||||
|
{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Page assets.
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub struct Assets {
|
||||||
|
favicon : Option<Favicon>,
|
||||||
|
metadata : Vec<(String, String)>,
|
||||||
|
stylesheets: Vec<StyleSheet>,
|
||||||
|
javascripts: Vec<JavaScript>,
|
||||||
|
seqid_count: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Assets {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Assets {
|
||||||
|
favicon : None,
|
||||||
|
metadata : Vec::new(),
|
||||||
|
stylesheets: Vec::new(),
|
||||||
|
javascripts: Vec::new(),
|
||||||
|
seqid_count: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_favicon(&mut self, favicon: Favicon) -> &mut Self {
|
||||||
|
self.favicon = Some(favicon);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_metadata(&mut self, name: String, content: String) -> &mut Self {
|
||||||
|
self.metadata.push((name, content));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_stylesheet(&mut self, css: StyleSheet) -> &mut Self {
|
||||||
|
match self.stylesheets.iter().position(|x| x.source == css.source) {
|
||||||
|
Some(index) => if self.stylesheets[index].weight > css.weight {
|
||||||
|
self.stylesheets.remove(index);
|
||||||
|
self.stylesheets.push(css);
|
||||||
|
},
|
||||||
|
_ => self.stylesheets.push(css)
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_javascript(&mut self, js: JavaScript) -> &mut Self {
|
||||||
|
match self.javascripts.iter().position(|x| x.source == js.source) {
|
||||||
|
Some(index) => if self.javascripts[index].weight > js.weight {
|
||||||
|
self.javascripts.remove(index);
|
||||||
|
self.javascripts.push(js);
|
||||||
|
},
|
||||||
|
_ => self.javascripts.push(js)
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&mut self) -> Markup {
|
||||||
|
let ordered_css = &mut self.stylesheets;
|
||||||
|
ordered_css.sort_by_key(|o| o.weight);
|
||||||
|
|
||||||
|
let ordered_js = &mut self.javascripts;
|
||||||
|
ordered_js.sort_by_key(|o| o.weight);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
@match &self.favicon {
|
||||||
|
Some(favicon) => (favicon.render()),
|
||||||
|
None => "",
|
||||||
|
}
|
||||||
|
@for (name, content) in &self.metadata {
|
||||||
|
meta name=(name) content=(content) {}
|
||||||
|
}
|
||||||
|
@for css in ordered_css {
|
||||||
|
(css.render())
|
||||||
|
}
|
||||||
|
@for js in ordered_js {
|
||||||
|
(js.render())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assets GETTERS.
|
||||||
|
|
||||||
|
pub fn seqid(&mut self, id: &str) -> String {
|
||||||
|
if id.is_empty() {
|
||||||
|
self.seqid_count += 1;
|
||||||
|
return format!("seqid-{}", self.seqid_count);
|
||||||
|
}
|
||||||
|
id.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/core/response/page/component.rs
Normal file
34
src/core/response/page/component.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
use crate::core::theme::{Markup, html};
|
||||||
|
use crate::core::response::page::PageAssets;
|
||||||
|
|
||||||
|
use downcast_rs::{Downcast, impl_downcast};
|
||||||
|
|
||||||
|
use std::any::type_name;
|
||||||
|
|
||||||
|
pub trait Component: Downcast + Send + Sync {
|
||||||
|
|
||||||
|
fn prepare() -> Self where Self: Sized;
|
||||||
|
|
||||||
|
fn qualified_name(&self) -> &str {
|
||||||
|
type_name::<Self>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_renderable(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn weight(&self) -> i8 {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn default_render(&self, assets: &mut PageAssets) -> Markup {
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_downcast!(Component);
|
||||||
33
src/core/response/page/container.rs
Normal file
33
src/core/response/page/container.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
use crate::core::theme::{Markup, html};
|
||||||
|
use crate::core::response::page::{PageAssets, PageComponent, render_component};
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Container(Vec<Arc<dyn PageComponent>>);
|
||||||
|
|
||||||
|
impl Container {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Container(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with(component: impl PageComponent) -> Self {
|
||||||
|
let mut container = Container::new();
|
||||||
|
container.add(component);
|
||||||
|
container
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, component: impl PageComponent) {
|
||||||
|
self.0.push(Arc::new(component));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self, assets: &mut PageAssets) -> Markup {
|
||||||
|
let mut components = self.0.clone();
|
||||||
|
components.sort_by_key(|c| c.weight());
|
||||||
|
html! {
|
||||||
|
@for c in components.iter() {
|
||||||
|
(render_component(&**c, assets))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/core/response/page/mod.rs
Normal file
12
src/core/response/page/mod.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
pub mod assets;
|
||||||
|
pub use assets::Assets as PageAssets;
|
||||||
|
|
||||||
|
mod component;
|
||||||
|
pub use component::Component as PageComponent;
|
||||||
|
|
||||||
|
mod container;
|
||||||
|
pub use container::Container as PageContainer;
|
||||||
|
|
||||||
|
mod page;
|
||||||
|
pub use page::Page;
|
||||||
|
pub use page::render_component;
|
||||||
172
src/core/response/page/page.rs
Normal file
172
src/core/response/page/page.rs
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
use crate::config::SETTINGS;
|
||||||
|
use crate::core::server;
|
||||||
|
use crate::core::state::{COMPONENTS, THEME};
|
||||||
|
use crate::core::theme::{DOCTYPE, Markup, html};
|
||||||
|
use crate::core::response::page::{PageAssets, PageComponent, PageContainer};
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub enum TextDirection { LeftToRight, RightToLeft, Auto }
|
||||||
|
|
||||||
|
pub struct Page<'a> {
|
||||||
|
language : &'a str,
|
||||||
|
title : &'a str,
|
||||||
|
direction : &'a str,
|
||||||
|
description : &'a str,
|
||||||
|
body_classes: Cow<'a, str>,
|
||||||
|
assets : PageAssets,
|
||||||
|
regions : HashMap<&'a str, PageContainer>,
|
||||||
|
template : &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Page<'a> {
|
||||||
|
|
||||||
|
pub fn prepare() -> Self {
|
||||||
|
Page {
|
||||||
|
language : &SETTINGS.app.language[..2],
|
||||||
|
title : &SETTINGS.app.name,
|
||||||
|
direction : "ltr",
|
||||||
|
description : "",
|
||||||
|
body_classes: "body".into(),
|
||||||
|
assets : PageAssets::new(),
|
||||||
|
regions : COMPONENTS.read().unwrap().clone(),
|
||||||
|
template : "default",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page BUILDER.
|
||||||
|
|
||||||
|
pub fn with_language(&mut self, language: &'a str) -> &mut Self {
|
||||||
|
self.language = language;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_title(&mut self, title: &'a str) -> &mut Self {
|
||||||
|
self.title = title;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_direction(&mut self, dir: TextDirection) -> &mut Self {
|
||||||
|
self.direction = match dir {
|
||||||
|
TextDirection::LeftToRight => "ltr",
|
||||||
|
TextDirection::RightToLeft => "rtl",
|
||||||
|
_ => "auto"
|
||||||
|
};
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_description(&mut self, description: &'a str) -> &mut Self {
|
||||||
|
self.description = description;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_body_classes(&mut self, body_classes: &'a str) -> &mut Self {
|
||||||
|
self.body_classes = body_classes.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_body_classes(&mut self, body_classes: &'a str) -> &mut Self {
|
||||||
|
self.body_classes = String::from(
|
||||||
|
format!("{} {}", self.body_classes, body_classes).trim()
|
||||||
|
).into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_to(
|
||||||
|
&mut self,
|
||||||
|
region: &'a str,
|
||||||
|
component: impl PageComponent
|
||||||
|
) -> &mut Self {
|
||||||
|
if let Some(regions) = self.regions.get_mut(region) {
|
||||||
|
regions.add(component);
|
||||||
|
} else {
|
||||||
|
self.regions.insert(region, PageContainer::new_with(component));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn using_template(&mut self, template: &'a str) -> &mut Self {
|
||||||
|
self.template = template;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page GETTERS.
|
||||||
|
|
||||||
|
pub fn language(&self) -> &str {
|
||||||
|
self.language
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> &str {
|
||||||
|
self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn direction(&self) -> TextDirection {
|
||||||
|
match self.direction {
|
||||||
|
"ltr" => TextDirection::LeftToRight,
|
||||||
|
"rtl" => TextDirection::RightToLeft,
|
||||||
|
_ => TextDirection::Auto
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn description(&self) -> &str {
|
||||||
|
self.description
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn body_classes(&self) -> &str {
|
||||||
|
if self.body_classes.is_empty() {
|
||||||
|
return "body";
|
||||||
|
}
|
||||||
|
&self.body_classes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assets(&mut self) -> &mut PageAssets {
|
||||||
|
&mut self.assets
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn template(&self) -> &str {
|
||||||
|
self.template
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page RENDER.
|
||||||
|
|
||||||
|
pub fn render(&mut self) -> server::Result<Markup> {
|
||||||
|
// Acciones del tema antes de renderizar la página.
|
||||||
|
THEME.before_render_page(self);
|
||||||
|
|
||||||
|
// Primero, renderizar el cuerpo.
|
||||||
|
let body = THEME.render_page_body(self);
|
||||||
|
|
||||||
|
// Luego, renderizar la cabecera.
|
||||||
|
let head = THEME.render_page_head(self);
|
||||||
|
|
||||||
|
// Finalmente, renderizar la página.
|
||||||
|
return Ok(html! {
|
||||||
|
(DOCTYPE)
|
||||||
|
html lang=(self.language) dir=(self.direction) {
|
||||||
|
(head)
|
||||||
|
(body)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_region(&mut self, region: &str) -> Markup {
|
||||||
|
match self.regions.get_mut(region) {
|
||||||
|
Some(components) => components.render(&mut self.assets),
|
||||||
|
None => html! {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_component(
|
||||||
|
component: &dyn PageComponent,
|
||||||
|
assets: &mut PageAssets
|
||||||
|
) -> Markup {
|
||||||
|
match component.is_renderable() {
|
||||||
|
true => match THEME.render_component(component, assets) {
|
||||||
|
Some(markup) => markup,
|
||||||
|
None => component.default_render(assets)
|
||||||
|
},
|
||||||
|
false => html! {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,13 +8,14 @@ pub fn run(bootstrap: Option<fn()>) -> Result<Server, std::io::Error> {
|
||||||
let _ = &(bootstrap.unwrap())();
|
let _ = &(bootstrap.unwrap())();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registra la página de inicio de PageTop como último módulo.
|
// Registra el módulo para la página de inicio de PageTop.
|
||||||
// Así, la función de arranque de la aplicación podría sobrecargarlo.
|
// Al ser el último, puede sobrecargarse en la función de arranque.
|
||||||
register_module(&base::module::homepage::HomepageModule);
|
register_module(&base::module::homepage::HomepageModule);
|
||||||
|
|
||||||
// Inicializa el servidor web.
|
// Inicializa el servidor web.
|
||||||
let server = server::HttpServer::new(|| {
|
let server = server::HttpServer::new(|| {
|
||||||
server::App::new()
|
server::App::new()
|
||||||
|
.configure(&all::themes)
|
||||||
.configure(&all::modules)
|
.configure(&all::modules)
|
||||||
})
|
})
|
||||||
.bind(format!("{}:{}",
|
.bind(format!("{}:{}",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
pub use actix_web::{App, HttpRequest, HttpResponse, HttpServer, Responder, web};
|
pub use actix_web::{
|
||||||
|
App, HttpRequest, HttpResponse, HttpServer, Responder, Result, web
|
||||||
|
};
|
||||||
|
|
||||||
mod main;
|
mod main;
|
||||||
pub use main::run;
|
pub use main::run;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,35 @@
|
||||||
use crate::Lazy;
|
use crate::Lazy;
|
||||||
|
use crate::config::SETTINGS;
|
||||||
|
use crate::core::theme::Theme;
|
||||||
use crate::core::module::Module;
|
use crate::core::module::Module;
|
||||||
|
use crate::core::response::page::{PageComponent, PageContainer};
|
||||||
|
use crate::base;
|
||||||
|
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Temas registrados.
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub static THEMES: Lazy<RwLock<Vec<&dyn Theme>>> = Lazy::new(|| {
|
||||||
|
RwLock::new(vec![
|
||||||
|
&base::theme::minimal::MinimalTheme,
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
pub static THEME: Lazy<&dyn Theme> = Lazy::new(|| {
|
||||||
|
for t in THEMES.read().unwrap().iter() {
|
||||||
|
if t.name().to_lowercase() == SETTINGS.app.theme.to_lowercase() {
|
||||||
|
return *t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&base::theme::minimal::MinimalTheme
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn register_theme(t: &'static (dyn Theme + 'static)) {
|
||||||
|
THEMES.write().unwrap().push(t);
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Módulos registrados.
|
// Módulos registrados.
|
||||||
|
|
@ -14,3 +42,21 @@ pub static MODULES: Lazy<RwLock<Vec<&dyn Module>>> = Lazy::new(|| {
|
||||||
pub fn register_module(m: &'static (dyn Module + 'static)) {
|
pub fn register_module(m: &'static (dyn Module + 'static)) {
|
||||||
MODULES.write().unwrap().push(m);
|
MODULES.write().unwrap().push(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Componentes globales.
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub static COMPONENTS: Lazy<RwLock<HashMap<&str, PageContainer>>> = Lazy::new(
|
||||||
|
|| { RwLock::new(HashMap::new()) }
|
||||||
|
);
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn add_component_to(region: &'static str, component: impl PageComponent) {
|
||||||
|
let mut hmap = COMPONENTS.write().unwrap();
|
||||||
|
if let Some(regions) = hmap.get_mut(region) {
|
||||||
|
regions.add(component);
|
||||||
|
} else {
|
||||||
|
hmap.insert(region, PageContainer::new_with(component));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
84
src/core/theme/api.rs
Normal file
84
src/core/theme/api.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
use crate::core::server;
|
||||||
|
use crate::core::theme::{Markup, html};
|
||||||
|
use crate::core::response::page::{Page, PageAssets, PageComponent};
|
||||||
|
|
||||||
|
/// Los temas deben implementar este "trait".
|
||||||
|
pub trait Theme: Send + Sync {
|
||||||
|
fn name(&self) -> &str;
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn configure_theme(&self, cfg: &mut server::web::ServiceConfig) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn before_render_page(&self, page: &mut Page) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_page_head(&self, page: &mut Page) -> Markup {
|
||||||
|
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
|
||||||
|
let description = page.description();
|
||||||
|
html! {
|
||||||
|
head {
|
||||||
|
meta charset="utf-8";
|
||||||
|
|
||||||
|
meta http-equiv="X-UA-Compatible" content="IE=edge";
|
||||||
|
meta name="viewport" content=(viewport);
|
||||||
|
@if !description.is_empty() {
|
||||||
|
meta name="description" content=(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
title { (page.title()) }
|
||||||
|
|
||||||
|
(page.assets().render())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_page_body(&self, page: &mut Page) -> Markup {
|
||||||
|
html! {
|
||||||
|
body id="body" class=(page.body_classes()) {
|
||||||
|
@match page.template() {
|
||||||
|
"admin" => {
|
||||||
|
@for region in &["top-menu", "side-menu", "content"] {
|
||||||
|
#(region) {
|
||||||
|
(page.render_region(region))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
#content {
|
||||||
|
(page.render_region("content"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn render_component(
|
||||||
|
&self,
|
||||||
|
component: &dyn PageComponent,
|
||||||
|
assets: &mut PageAssets
|
||||||
|
) -> Option<Markup> {
|
||||||
|
None
|
||||||
|
/*
|
||||||
|
Cómo usarlo:
|
||||||
|
|
||||||
|
match component.type_name() {
|
||||||
|
"Block" => {
|
||||||
|
let block = component.downcast_mut::<Block>().unwrap();
|
||||||
|
match block.template() {
|
||||||
|
"default" => Some(block_default(block)),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/core/theme/mod.rs
Normal file
4
src/core/theme/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub use maud::{DOCTYPE, Markup, PreEscaped, html};
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
pub use api::Theme;
|
||||||
|
|
@ -43,5 +43,15 @@ macro_rules! localize {
|
||||||
) -> String {
|
) -> String {
|
||||||
LOCALES.lookup_with_args(&LANGID, key, args)
|
LOCALES.lookup_with_args(&LANGID, key, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn e(
|
||||||
|
key: &str,
|
||||||
|
args: &std::collections::HashMap<String, FluentValue>
|
||||||
|
) -> crate::core::theme::PreEscaped<String> {
|
||||||
|
crate::core::theme::PreEscaped(
|
||||||
|
LOCALES.lookup_with_args(&LANGID, key, args)
|
||||||
|
)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
#[tokio::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
pagetop::core::server::run(None)?.await
|
pagetop::core::server::run(None)?.await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
//! Re-exporta recursos comunes.
|
//! Re-exporta recursos comunes.
|
||||||
|
|
||||||
pub use crate::args;
|
pub use crate::args;
|
||||||
pub use crate::config_get;
|
|
||||||
pub use crate::localize;
|
pub use crate::localize;
|
||||||
|
|
||||||
|
pub use crate::core::theme::*;
|
||||||
|
|
||||||
pub use crate::core::module::*;
|
pub use crate::core::module::*;
|
||||||
|
|
||||||
|
pub use crate::core::response::page::*;
|
||||||
|
pub use crate::core::response::page::assets::*;
|
||||||
|
|
||||||
pub use crate::core::server;
|
pub use crate::core::server;
|
||||||
|
pub use crate::core::register_theme;
|
||||||
pub use crate::core::register_module;
|
pub use crate::core::register_module;
|
||||||
|
pub use crate::core::add_component_to;
|
||||||
|
|
||||||
|
pub use crate::base::component::container::Container;
|
||||||
|
pub use crate::base::component::chunck::Chunck;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue