Refactor
Some checks failed
CI / Check links (push) Failing after 31s
CI / Check typos (push) Successful in 52s
CI / Clippy (push) Successful in 1m11s
CI / Build and test (push) Failing after 1m12s
CI / Build container (push) Has been skipped
CI / Deploy on waypoint (push) Has been skipped
Some checks failed
CI / Check links (push) Failing after 31s
CI / Check typos (push) Successful in 52s
CI / Clippy (push) Successful in 1m11s
CI / Build and test (push) Failing after 1m12s
CI / Build container (push) Has been skipped
CI / Deploy on waypoint (push) Has been skipped
This commit is contained in:
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -1536,6 +1536,21 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "page"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"chrono",
|
||||||
|
"libservice",
|
||||||
|
"lru",
|
||||||
|
"maud",
|
||||||
|
"parking_lot",
|
||||||
|
"serde",
|
||||||
|
"tower-http",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -2214,11 +2229,11 @@ dependencies = [
|
|||||||
"emojis",
|
"emojis",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libservice",
|
"libservice",
|
||||||
"lru",
|
|
||||||
"macro-assets",
|
"macro-assets",
|
||||||
"macro-sass",
|
"macro-sass",
|
||||||
"markdown-it",
|
"markdown-it",
|
||||||
"maud",
|
"maud",
|
||||||
|
"page",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ macro-sass = { path = "crates/macro/macro-sass" }
|
|||||||
assetserver = { path = "crates/lib/assetserver" }
|
assetserver = { path = "crates/lib/assetserver" }
|
||||||
libservice = { path = "crates/lib/libservice" }
|
libservice = { path = "crates/lib/libservice" }
|
||||||
toolbox = { path = "crates/lib/toolbox" }
|
toolbox = { path = "crates/lib/toolbox" }
|
||||||
|
page = { path = "crates/lib/page" }
|
||||||
|
|
||||||
service-webpage = { path = "crates/service/service-webpage" }
|
service-webpage = { path = "crates/service/service-webpage" }
|
||||||
|
|
||||||
|
|||||||
20
crates/lib/page/Cargo.toml
Normal file
20
crates/lib/page/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "page"
|
||||||
|
version = { workspace = true }
|
||||||
|
rust-version = { workspace = true }
|
||||||
|
edition = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libservice = { workspace = true }
|
||||||
|
|
||||||
|
axum = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
maud = { workspace = true }
|
||||||
|
chrono = { workspace = true }
|
||||||
|
parking_lot = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
lru = { workspace = true }
|
||||||
|
tower-http = { workspace = true }
|
||||||
8
crates/lib/page/src/lib.rs
Normal file
8
crates/lib/page/src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
mod page;
|
||||||
|
pub use page::*;
|
||||||
|
|
||||||
|
mod requestcontext;
|
||||||
|
pub use requestcontext::*;
|
||||||
|
|
||||||
|
mod server;
|
||||||
|
pub use server::*;
|
||||||
105
crates/lib/page/src/page.rs
Normal file
105
crates/lib/page/src/page.rs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
use chrono::TimeDelta;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::pin::Pin;
|
||||||
|
|
||||||
|
use crate::RequestContext;
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: metadata
|
||||||
|
//
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize)]
|
||||||
|
pub struct PageMetadata {
|
||||||
|
pub title: String,
|
||||||
|
pub author: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub image: Option<String>,
|
||||||
|
pub slug: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PageMetadata {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
title: "Untitled page".into(),
|
||||||
|
author: None,
|
||||||
|
description: None,
|
||||||
|
image: None,
|
||||||
|
slug: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for PageMetadata {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
let empty = String::new();
|
||||||
|
let title = &self.title;
|
||||||
|
let author = &self.author.as_ref().unwrap_or(&empty);
|
||||||
|
let description = &self.description.as_ref().unwrap_or(&empty);
|
||||||
|
let image = &self.image.as_ref().unwrap_or(&empty);
|
||||||
|
|
||||||
|
html !(
|
||||||
|
meta property="og:site_name" content=(title) {}
|
||||||
|
meta name="title" content=(title) {}
|
||||||
|
meta property="og:title" content=(title) {}
|
||||||
|
meta property="twitter:title" content=(title) {}
|
||||||
|
|
||||||
|
meta name="author" content=(author) {}
|
||||||
|
|
||||||
|
meta name="description" content=(description) {}
|
||||||
|
meta property="og:description" content=(description) {}
|
||||||
|
meta property="twitter:description" content=(description) {}
|
||||||
|
|
||||||
|
meta content=(image) property="og:image" {}
|
||||||
|
link rel="shortcut icon" href=(image) type="image/x-icon" {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: page
|
||||||
|
//
|
||||||
|
|
||||||
|
// Some HTML
|
||||||
|
pub struct Page {
|
||||||
|
pub meta: PageMetadata,
|
||||||
|
|
||||||
|
/// How long this page's html may be cached.
|
||||||
|
/// This controls the maximum age of a page shown to the user.
|
||||||
|
///
|
||||||
|
/// If `None`, this page is always rendered from scratch.
|
||||||
|
pub html_ttl: Option<TimeDelta>,
|
||||||
|
|
||||||
|
/// A function that generates this page's html.
|
||||||
|
///
|
||||||
|
/// This should return the contents of this page's <body> tag,
|
||||||
|
/// or the contents of a wrapper element (defined in the page server struct).
|
||||||
|
///
|
||||||
|
/// This closure must never return `<html>` or `<head>`.
|
||||||
|
pub generate_html: Box<
|
||||||
|
dyn Send
|
||||||
|
+ Sync
|
||||||
|
+ for<'a> Fn(
|
||||||
|
&'a Page,
|
||||||
|
&'a RequestContext,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>>,
|
||||||
|
>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Page {
|
||||||
|
fn default() -> Self {
|
||||||
|
Page {
|
||||||
|
meta: Default::default(),
|
||||||
|
html_ttl: Some(TimeDelta::seconds(60 * 24 * 30)),
|
||||||
|
//css_ttl: Duration::from_secs(60 * 24 * 30),
|
||||||
|
//generate_css: None,
|
||||||
|
generate_html: Box::new(|_, _| Box::pin(async { html!() })),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Page {
|
||||||
|
pub async fn generate_html(&self, req_info: &RequestContext) -> Markup {
|
||||||
|
(self.generate_html)(self, req_info).await
|
||||||
|
}
|
||||||
|
}
|
||||||
60
crates/lib/page/src/requestcontext.rs
Normal file
60
crates/lib/page/src/requestcontext.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
use axum::http::HeaderMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct RequestContext {
|
||||||
|
pub client_info: ClientInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum DeviceType {
|
||||||
|
Mobile,
|
||||||
|
Desktop,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DeviceType {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Desktop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: clientinfo
|
||||||
|
//
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ClientInfo {
|
||||||
|
/// This is an estimate, but it's probably good enough.
|
||||||
|
pub device_type: DeviceType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientInfo {
|
||||||
|
pub fn from_headers(headers: &HeaderMap) -> Self {
|
||||||
|
let ua = headers
|
||||||
|
.get("user-agent")
|
||||||
|
.and_then(|x| x.to_str().ok())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
let ch_mobile = headers
|
||||||
|
.get("Sec-CH-UA-Mobile")
|
||||||
|
.and_then(|x| x.to_str().ok())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
let mut device_type = None;
|
||||||
|
|
||||||
|
if device_type.is_none() && ch_mobile.contains("1") {
|
||||||
|
device_type = Some(DeviceType::Mobile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if device_type.is_none() && ua.contains("Mobile") {
|
||||||
|
device_type = Some(DeviceType::Mobile);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
device_type: device_type.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
//
|
|
||||||
// MARK: metadata
|
|
||||||
//
|
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
extract::{ConnectInfo, Path, State},
|
extract::{ConnectInfo, Path, State},
|
||||||
@@ -9,185 +5,17 @@ use axum::{
|
|||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
routing::get,
|
routing::get,
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, TimeDelta, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use libservice::ServiceConnectInfo;
|
use libservice::ServiceConnectInfo;
|
||||||
use lru::LruCache;
|
use lru::LruCache;
|
||||||
use markdown_it::Node;
|
use maud::Markup;
|
||||||
use maud::{Markup, PreEscaped, Render, html};
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use serde::Deserialize;
|
|
||||||
use std::{collections::HashMap, num::NonZero, pin::Pin, sync::Arc, time::Instant};
|
use std::{collections::HashMap, num::NonZero, pin::Pin, sync::Arc, time::Instant};
|
||||||
use tower_http::compression::{CompressionLayer, DefaultPredicate};
|
use tower_http::compression::{CompressionLayer, DefaultPredicate};
|
||||||
use tracing::{trace, warn};
|
use tracing::{trace, warn};
|
||||||
|
|
||||||
use crate::components::{
|
use crate::{ClientInfo, RequestContext, page::Page};
|
||||||
md::{FrontMatter, Markdown},
|
|
||||||
misc::Backlinks,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize)]
|
|
||||||
pub struct PageMetadata {
|
|
||||||
pub title: String,
|
|
||||||
pub author: Option<String>,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub image: Option<String>,
|
|
||||||
pub slug: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PageMetadata {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
title: "Untitled page".into(),
|
|
||||||
author: None,
|
|
||||||
description: None,
|
|
||||||
image: None,
|
|
||||||
slug: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for PageMetadata {
|
|
||||||
fn render(&self) -> Markup {
|
|
||||||
let empty = String::new();
|
|
||||||
let title = &self.title;
|
|
||||||
let author = &self.author.as_ref().unwrap_or(&empty);
|
|
||||||
let description = &self.description.as_ref().unwrap_or(&empty);
|
|
||||||
let image = &self.image.as_ref().unwrap_or(&empty);
|
|
||||||
|
|
||||||
html !(
|
|
||||||
meta property="og:site_name" content=(title) {}
|
|
||||||
meta name="title" content=(title) {}
|
|
||||||
meta property="og:title" content=(title) {}
|
|
||||||
meta property="twitter:title" content=(title) {}
|
|
||||||
|
|
||||||
meta name="author" content=(author) {}
|
|
||||||
|
|
||||||
meta name="description" content=(description) {}
|
|
||||||
meta property="og:description" content=(description) {}
|
|
||||||
meta property="twitter:description" content=(description) {}
|
|
||||||
|
|
||||||
meta content=(image) property="og:image" {}
|
|
||||||
link rel="shortcut icon" href=(image) type="image/x-icon" {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PageMetadata {
|
|
||||||
/// Try to read page metadata from a markdown file's frontmatter.
|
|
||||||
/// - returns `none` if there is no frontmatter
|
|
||||||
/// - returns an error if we fail to parse frontmatter
|
|
||||||
pub fn from_markdown_frontmatter(
|
|
||||||
root_node: &Node,
|
|
||||||
) -> Result<Option<PageMetadata>, serde_yaml::Error> {
|
|
||||||
root_node
|
|
||||||
.children
|
|
||||||
.first()
|
|
||||||
.and_then(|x| x.cast::<FrontMatter>())
|
|
||||||
.map(|x| serde_yaml::from_str::<PageMetadata>(&x.content))
|
|
||||||
.map_or(Ok(None), |v| v.map(Some))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// MARK: page
|
|
||||||
//
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub struct RequestContext {
|
|
||||||
pub client_info: ClientInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some HTML
|
|
||||||
pub struct Page {
|
|
||||||
pub meta: PageMetadata,
|
|
||||||
|
|
||||||
/// How long this page's html may be cached.
|
|
||||||
/// This controls the maximum age of a page shown to the user.
|
|
||||||
///
|
|
||||||
/// If `None`, this page is always rendered from scratch.
|
|
||||||
pub html_ttl: Option<TimeDelta>,
|
|
||||||
|
|
||||||
/// A function that generates this page's html.
|
|
||||||
///
|
|
||||||
/// This should return the contents of this page's <body> tag,
|
|
||||||
/// or the contents of a wrapper element (defined in the page server struct).
|
|
||||||
///
|
|
||||||
/// This closure must never return `<html>` or `<head>`.
|
|
||||||
pub generate_html: Box<
|
|
||||||
dyn Send
|
|
||||||
+ Sync
|
|
||||||
+ for<'a> Fn(
|
|
||||||
&'a Page,
|
|
||||||
&'a RequestContext,
|
|
||||||
) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>>,
|
|
||||||
>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Page {
|
|
||||||
fn default() -> Self {
|
|
||||||
Page {
|
|
||||||
meta: Default::default(),
|
|
||||||
html_ttl: Some(TimeDelta::seconds(60 * 24 * 30)),
|
|
||||||
//css_ttl: Duration::from_secs(60 * 24 * 30),
|
|
||||||
//generate_css: None,
|
|
||||||
generate_html: Box::new(|_, _| Box::pin(async { html!() })),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Page {
|
|
||||||
pub async fn generate_html(&self, req_info: &RequestContext) -> Markup {
|
|
||||||
(self.generate_html)(self, req_info).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_markdown(md: impl Into<String>, default_image: Option<String>) -> Self {
|
|
||||||
let md: String = md.into();
|
|
||||||
let md = Markdown::parse(&md);
|
|
||||||
|
|
||||||
let mut meta = PageMetadata::from_markdown_frontmatter(&md)
|
|
||||||
.unwrap_or(Some(PageMetadata {
|
|
||||||
title: "Invalid frontmatter!".into(),
|
|
||||||
..Default::default()
|
|
||||||
}))
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
if meta.image.is_none() {
|
|
||||||
meta.image = default_image
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = PreEscaped(md.render());
|
|
||||||
|
|
||||||
Page {
|
|
||||||
meta,
|
|
||||||
generate_html: Box::new(move |page, _| {
|
|
||||||
let html = html.clone();
|
|
||||||
Box::pin(async move {
|
|
||||||
html! {
|
|
||||||
@if let Some(slug) = &page.meta.slug {
|
|
||||||
(Backlinks(&[("/", "home")], slug))
|
|
||||||
}
|
|
||||||
|
|
||||||
(html)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// MARK: server
|
|
||||||
//
|
|
||||||
|
|
||||||
// Rerender considerations:
|
|
||||||
// - rerendering often in the background is wasteful. Maybe we should fall asleep?
|
|
||||||
// - rerendering on request is slow
|
|
||||||
// - rerendering in the background after a request could be a good idea. Maybe implement?
|
|
||||||
//
|
|
||||||
// - cached pages only make sense for static assets.
|
|
||||||
// - user pages can't be pre-rendered!
|
|
||||||
pub struct PageServer {
|
pub struct PageServer {
|
||||||
/// If true, expired pages will be rerendered before being sent to the user.
|
/// If true, expired pages will be rerendered before being sent to the user.
|
||||||
/// If false, requests never trigger rerenders. We rely on the rerender task.
|
/// If false, requests never trigger rerenders. We rely on the rerender task.
|
||||||
@@ -316,7 +144,7 @@ impl PageServer {
|
|||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let mut html_expires = None;
|
let mut html_expires = None;
|
||||||
|
|
||||||
// Get from cache, if availablee
|
// Get from cache, if available
|
||||||
if let Some((html, expires)) = state.html_cache.lock().get(&cache_key)
|
if let Some((html, expires)) = state.html_cache.lock().get(&cache_key)
|
||||||
&& (*expires > now || state.never_rerender_on_request)
|
&& (*expires > now || state.never_rerender_on_request)
|
||||||
{
|
{
|
||||||
@@ -376,53 +204,3 @@ impl PageServer {
|
|||||||
.with_state(self)
|
.with_state(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// MARK: UserAgent
|
|
||||||
//
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub enum DeviceType {
|
|
||||||
Mobile,
|
|
||||||
Desktop,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for DeviceType {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Desktop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ClientInfo {
|
|
||||||
/// This is an estimate, but it's probably good enough.
|
|
||||||
pub device_type: DeviceType,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClientInfo {
|
|
||||||
pub fn from_headers(headers: &HeaderMap) -> Self {
|
|
||||||
let ua = headers
|
|
||||||
.get("user-agent")
|
|
||||||
.and_then(|x| x.to_str().ok())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
let ch_mobile = headers
|
|
||||||
.get("Sec-CH-UA-Mobile")
|
|
||||||
.and_then(|x| x.to_str().ok())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
let mut device_type = None;
|
|
||||||
|
|
||||||
if device_type.is_none() && ch_mobile.contains("1") {
|
|
||||||
device_type = Some(DeviceType::Mobile);
|
|
||||||
}
|
|
||||||
|
|
||||||
if device_type.is_none() && ua.contains("Mobile") {
|
|
||||||
device_type = Some(DeviceType::Mobile);
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
device_type: device_type.unwrap_or_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,7 @@ libservice = { workspace = true }
|
|||||||
macro-assets = { workspace = true }
|
macro-assets = { workspace = true }
|
||||||
macro-sass = { workspace = true }
|
macro-sass = { workspace = true }
|
||||||
assetserver = { workspace = true }
|
assetserver = { workspace = true }
|
||||||
|
page = { workspace = true }
|
||||||
|
|
||||||
axum = { workspace = true }
|
axum = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
@@ -25,6 +26,5 @@ lazy_static = { workspace = true }
|
|||||||
serde_yaml = { workspace = true }
|
serde_yaml = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
lru = { workspace = true }
|
|
||||||
tower-http = { workspace = true }
|
tower-http = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ use markdown_it::parser::block::{BlockRule, BlockState};
|
|||||||
use markdown_it::parser::core::Root;
|
use markdown_it::parser::core::Root;
|
||||||
use markdown_it::parser::inline::{InlineRule, InlineState};
|
use markdown_it::parser::inline::{InlineRule, InlineState};
|
||||||
use markdown_it::{MarkdownIt, Node, NodeValue, Renderer};
|
use markdown_it::{MarkdownIt, Node, NodeValue, Renderer};
|
||||||
use maud::{Markup, PreEscaped, Render};
|
use maud::{Markup, PreEscaped, Render, html};
|
||||||
|
use page::{Page, PageMetadata};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::components::fa::FAIcon;
|
use crate::components::fa::FAIcon;
|
||||||
use crate::components::mangle::{MangledBetaEmail, MangledGoogleEmail};
|
use crate::components::mangle::{MangledBetaEmail, MangledGoogleEmail};
|
||||||
|
use crate::components::misc::Backlinks;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref MdParser: MarkdownIt = {
|
static ref MdParser: MarkdownIt = {
|
||||||
@@ -38,6 +40,58 @@ impl Markdown<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: helpers
|
||||||
|
//
|
||||||
|
|
||||||
|
/// Try to read page metadata from a markdown file's frontmatter.
|
||||||
|
/// - returns `none` if there is no frontmatter
|
||||||
|
/// - returns an error if we fail to parse frontmatter
|
||||||
|
pub fn meta_from_markdown(root_node: &Node) -> Result<Option<PageMetadata>, serde_yaml::Error> {
|
||||||
|
root_node
|
||||||
|
.children
|
||||||
|
.first()
|
||||||
|
.and_then(|x| x.cast::<FrontMatter>())
|
||||||
|
.map(|x| serde_yaml::from_str::<PageMetadata>(&x.content))
|
||||||
|
.map_or(Ok(None), |v| v.map(Some))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn page_from_markdown(md: impl Into<String>, default_image: Option<String>) -> Page {
|
||||||
|
let md: String = md.into();
|
||||||
|
let md = Markdown::parse(&md);
|
||||||
|
|
||||||
|
let mut meta = meta_from_markdown(&md)
|
||||||
|
.unwrap_or(Some(PageMetadata {
|
||||||
|
title: "Invalid frontmatter!".into(),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if meta.image.is_none() {
|
||||||
|
meta.image = default_image
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = PreEscaped(md.render());
|
||||||
|
|
||||||
|
Page {
|
||||||
|
meta,
|
||||||
|
generate_html: Box::new(move |page, _| {
|
||||||
|
let html = html.clone();
|
||||||
|
Box::pin(async move {
|
||||||
|
html! {
|
||||||
|
@if let Some(slug) = &page.meta.slug {
|
||||||
|
(Backlinks(&[("/", "home")], slug))
|
||||||
|
}
|
||||||
|
|
||||||
|
(html)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// MARK: extensions
|
// MARK: extensions
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use axum::Router;
|
|||||||
use libservice::ToService;
|
use libservice::ToService;
|
||||||
|
|
||||||
mod components;
|
mod components;
|
||||||
mod page;
|
|
||||||
mod pages;
|
mod pages;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ use std::{
|
|||||||
use assetserver::Asset;
|
use assetserver::Asset;
|
||||||
use chrono::{DateTime, TimeDelta, Utc};
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
use maud::{Markup, PreEscaped, html};
|
use maud::{Markup, PreEscaped, html};
|
||||||
|
use page::{DeviceType, Page, RequestContext};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{
|
components::{
|
||||||
md::Markdown,
|
md::{Markdown, meta_from_markdown},
|
||||||
misc::{Backlinks, FarLink},
|
misc::{Backlinks, FarLink},
|
||||||
},
|
},
|
||||||
page::{DeviceType, Page, PageMetadata, RequestContext},
|
|
||||||
routes::assets::Image_Icon,
|
routes::assets::Image_Icon,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -190,9 +190,7 @@ pub fn handouts() -> Page {
|
|||||||
tokio::spawn(index.clone().autoget(Duration::from_secs(60 * 20)));
|
tokio::spawn(index.clone().autoget(Duration::from_secs(60 * 20)));
|
||||||
|
|
||||||
#[expect(clippy::unwrap_used)]
|
#[expect(clippy::unwrap_used)]
|
||||||
let mut meta = PageMetadata::from_markdown_frontmatter(&md)
|
let mut meta = meta_from_markdown(&md).unwrap().unwrap();
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if meta.image.is_none() {
|
if meta.image.is_none() {
|
||||||
meta.image = Some(Image_Icon::URL.to_owned());
|
meta.image = Some(Image_Icon::URL.to_owned());
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use assetserver::Asset;
|
use assetserver::Asset;
|
||||||
use maud::html;
|
use maud::html;
|
||||||
|
use page::{Page, PageMetadata};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{
|
components::{
|
||||||
@@ -8,7 +9,6 @@ use crate::{
|
|||||||
md::Markdown,
|
md::Markdown,
|
||||||
misc::FarLink,
|
misc::FarLink,
|
||||||
},
|
},
|
||||||
page::{Page, PageMetadata},
|
|
||||||
routes::assets::{Image_Cover, Image_Icon},
|
routes::assets::{Image_Cover, Image_Icon},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use assetserver::Asset;
|
use assetserver::Asset;
|
||||||
|
use page::Page;
|
||||||
|
|
||||||
use crate::{page::Page, routes::assets::Image_Icon};
|
use crate::{components::md::page_from_markdown, routes::assets::Image_Icon};
|
||||||
|
|
||||||
mod handouts;
|
mod handouts;
|
||||||
mod index;
|
mod index;
|
||||||
@@ -16,11 +17,11 @@ pub fn links() -> Page {
|
|||||||
http://www.3dprintmath.com/
|
http://www.3dprintmath.com/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Page::from_markdown(include_str!("links.md"), Some(Image_Icon::URL.to_owned()))
|
page_from_markdown(include_str!("links.md"), Some(Image_Icon::URL.to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn betalupi() -> Page {
|
pub fn betalupi() -> Page {
|
||||||
Page::from_markdown(
|
page_from_markdown(
|
||||||
include_str!("betalupi.md"),
|
include_str!("betalupi.md"),
|
||||||
Some(Image_Icon::URL.to_owned()),
|
Some(Image_Icon::URL.to_owned()),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,14 +3,10 @@ use std::{pin::Pin, sync::Arc};
|
|||||||
use assetserver::Asset;
|
use assetserver::Asset;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use maud::{DOCTYPE, Markup, PreEscaped, html};
|
use maud::{DOCTYPE, Markup, PreEscaped, html};
|
||||||
|
use page::{Page, PageServer, RequestContext};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::{
|
use crate::{components::misc::FarLink, pages, routes::assets::Styles_Main};
|
||||||
components::misc::FarLink,
|
|
||||||
page::{Page, PageServer, RequestContext},
|
|
||||||
pages,
|
|
||||||
routes::assets::Styles_Main,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user