Merge branch 'main' into svg-selectable
All checks were successful
PR Check / lint-and-format (pull_request) Successful in 11m46s

This commit is contained in:
Mylloon 2025-02-10 18:28:13 +01:00
commit e64010ac41
Signed by: Forgejo
GPG key ID: E72245C752A07631
43 changed files with 815 additions and 444 deletions

4
.gitignore vendored
View file

@ -7,13 +7,13 @@ docker-compose.yml
/.vscode
# Data
data/index.md
data/index*.md
data/contacts/*
data/cours/*
data/projects/*
# Blog
data/blog/*.md
data/blog/about*.md
data/blog/posts/*
!data/blog/posts/Makefile

656
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -21,18 +21,19 @@ serde_json = "1.0"
minify-html = "0.15"
minify-js = "0.6"
glob = "0.3"
comrak = "0.31"
comrak = "0.35"
reqwest = { version = "0.12", features = ["json"] }
chrono = { version = "0.4.39", default-features = false, features = ["clock"]}
chrono-tz = "0.10"
rss = { version = "2.0", features = ["atom"] }
lol_html = "2.1"
lol_html = "2.2"
base64 = "0.22"
mime_guess = "2.0"
urlencoding = "2.1"
regex = "1.11"
cyborgtime = "2.1.1"
walkdir = "2.5"
env_logger = "0.11"
[lints.clippy]
pedantic = "warn"

View file

@ -145,6 +145,8 @@ Markdown file
Markdown file is stored in `/app/data/index.md`
> For french clients, `/app/data/index-fr.md` will be read instead.
```
---
name: Option<String>
@ -188,6 +190,8 @@ Post content
The file is stored at `/app/data/blog/about.md`.
> For french clients, `/app/data/blog/about-fr.md` will be read instead.
## Projects
Markdown files are stored in `/app/data/projects/apps/`
@ -214,6 +218,8 @@ files in `archive` subdirectory of `apps`.
The file is stored at `/app/data/projects/about.md`.
> For french clients, `/app/data/projects/about-fr.md` will be read instead.
## Contacts
Markdown files are stored in `/app/data/contacts/`
@ -254,6 +260,8 @@ For example, `socials` contact files are stored in `/app/data/contacts/socials/`
The file is stored at `/app/data/contacts/about.md`.
> For french clients, `/app/data/contacts/about-fr.md` will be read instead.
## Courses
Markdown files are stored in `/app/data/cours/`

View file

@ -7,7 +7,7 @@ use std::{fs::File, io::Write, path::Path};
use crate::template::Template;
/// Store the configuration of config/config.toml
#[derive(Clone, Debug, Default, Deserialize)]
#[derive(Clone, Debug, Default, Deserialize, Hash, PartialEq, Eq)]
pub struct FileConfiguration {
/// http/https
pub scheme: Option<String>,
@ -75,14 +75,14 @@ impl FileConfiguration {
}
// Paths where files are stored
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Locations {
pub static_dir: String,
pub data_dir: String,
}
/// Configuration used internally in the app
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Config {
/// Information given in the config file
pub fc: FileConfiguration,

1
src/logic/api/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod v1;

View file

@ -0,0 +1,56 @@
use std::time::Duration;
use chrono::{DateTime, Utc};
use chrono_tz::Europe;
use cyborgtime::format_duration;
use serde::Serialize;
#[derive(Serialize)]
pub struct Info {
unix_epoch: i64,
departure: String,
arrival: String,
countdown: String,
since: String,
}
pub fn json() -> Info {
let target = 1_736_616_600;
let start = 1_724_832_000;
let current_time = Utc::now().timestamp();
let departure = DateTime::from_timestamp(start, 0)
.unwrap()
.with_timezone(&Europe::Paris)
.to_rfc2822();
let arrival = DateTime::from_timestamp(target, 0)
.unwrap()
.with_timezone(&Europe::Paris)
.to_rfc2822();
if current_time > target {
Info {
unix_epoch: target,
departure,
arrival,
countdown: "Already happened".to_owned(),
since: "Not relevant anymore".to_owned(),
}
} else {
Info {
unix_epoch: target,
departure,
arrival,
countdown: {
let duration_epoch = target - current_time;
let duration = Duration::from_secs(duration_epoch.try_into().unwrap());
format_duration(duration).to_string()
},
since: {
let duration_epoch = current_time - start;
let duration = Duration::from_secs(duration_epoch.try_into().unwrap());
format_duration(duration).to_string()
},
}
}
}

33
src/logic/api/v1/love.rs Normal file
View file

@ -0,0 +1,33 @@
use std::time::Duration;
use chrono::{DateTime, Utc};
use chrono_tz::Europe;
use cyborgtime::format_duration;
use serde::Serialize;
#[derive(Serialize)]
pub struct Info {
unix_epoch: i64,
date: String,
since: String,
}
pub fn json() -> Info {
let target = 1_605_576_600;
let current_time = Utc::now().timestamp();
let date = DateTime::from_timestamp(target, 0)
.unwrap()
.with_timezone(&Europe::Paris)
.to_rfc2822();
Info {
unix_epoch: target,
date,
since: {
let duration_epoch = current_time - target;
let duration = Duration::from_secs(duration_epoch.try_into().unwrap());
format_duration(duration).to_string()
},
}
}

3
src/logic/api/v1/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod backtofrance;
pub mod love;
pub mod websites;

View file

@ -0,0 +1,6 @@
pub fn tuple() -> (&'static str, &'static str) {
(
"http://www.bocal.cs.univ-paris8.fr/~akennel/",
"https://anri.up8.site/",
)
}

View file

@ -51,6 +51,7 @@ impl Post {
path: format!("{}{ext}", self.url),
},
MType::Blog,
None,
) {
self.content = Some(file.content);
}
@ -149,6 +150,7 @@ pub fn get_post(
path: format!("{filename}{ext}"),
},
MType::Blog,
None,
);
let default = (

View file

@ -57,7 +57,14 @@ pub fn remove_paragraphs(list: &mut [File]) {
pub fn read(path: &FilePath) -> Vec<File> {
glob(&path.to_string())
.unwrap()
.map(|e| read_file(path.from(&e.unwrap().to_string_lossy()), MType::Contact).unwrap())
.map(|e| {
read_file(
path.from(&e.unwrap().to_string_lossy()),
MType::Contact,
None,
)
.unwrap()
})
.filter(|f| {
!f.metadata
.info

View file

@ -1,3 +1,4 @@
pub mod api;
pub mod blog;
pub mod contact;
pub mod contrib;

View file

@ -1,6 +1,6 @@
use actix_files::Files;
use actix_web::{
middleware::{Compress, DefaultHeaders},
middleware::{Compress, DefaultHeaders, Logger},
web, App, HttpServer,
};
use std::io::Result;
@ -13,6 +13,7 @@ use crate::routes::{
mod config;
mod template;
mod logic;
mod routes;
mod utils;
@ -29,16 +30,12 @@ async fn main() -> Result<()> {
config.fc.port.unwrap(),
);
println!(
"Listening to {}://{}:{}",
config.clone().fc.scheme.unwrap(),
addr.0,
addr.1
);
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(config.clone()))
.wrap(Logger::default().log_target(config.fc.app_name.clone().unwrap_or_default()))
.wrap(Compress::default())
.wrap(
DefaultHeaders::new()

View file

@ -29,6 +29,7 @@ fn build_securitytxt(config: Config) -> String {
pref_lang: config.fc.lang.unwrap_or_default(),
},
InfosPage::default(),
None,
)
}
@ -56,6 +57,7 @@ fn build_humanstxt(config: Config) -> String {
name: config.fc.fullname.unwrap_or_default(),
},
InfosPage::default(),
None,
)
}
@ -95,5 +97,6 @@ fn build_webmanifest(config: Config) -> String {
url: get_url(config.fc),
},
InfosPage::default(),
None,
)
}

View file

@ -1,73 +1,18 @@
use std::time::Duration;
use actix_web::{get, HttpResponse, Responder};
use chrono::Utc;
use cyborgtime::format_duration;
use serde::Serialize;
/// Response for /love
#[derive(Serialize)]
struct InfoLove {
unix_epoch: u64,
since: String,
}
use crate::logic::api::v1;
#[get("/love")]
pub async fn love() -> impl Responder {
let target = 1_605_576_600;
let current_time: u64 = Utc::now().timestamp().try_into().unwrap();
HttpResponse::Ok().json(InfoLove {
unix_epoch: target,
since: {
let duration_epoch = current_time - target;
let duration = Duration::from_secs(duration_epoch);
format_duration(duration).to_string()
},
})
}
/// Response for /backtofrance
#[derive(Serialize)]
struct InfoBTF {
unix_epoch: u64,
countdown: String,
since: String,
HttpResponse::Ok().json(v1::love::json())
}
#[get("/backtofrance")]
pub async fn btf() -> impl Responder {
let target = 1_736_618_100;
let start = 1_724_832_000;
let current_time: u64 = Utc::now().timestamp().try_into().unwrap();
if current_time > target {
HttpResponse::Ok().json(InfoBTF {
unix_epoch: target,
countdown: "Already happened".to_owned(),
since: "Not relevant anymore".to_owned(),
})
} else {
HttpResponse::Ok().json(InfoBTF {
unix_epoch: target,
countdown: {
let duration_epoch = target - current_time;
let duration = Duration::from_secs(duration_epoch);
format_duration(duration).to_string()
},
since: {
let duration_epoch = current_time - start;
let duration = Duration::from_secs(duration_epoch);
format_duration(duration).to_string()
},
})
}
HttpResponse::Ok().json(v1::backtofrance::json())
}
#[get("/websites")]
pub async fn websites() -> impl Responder {
HttpResponse::Ok().json((
"http://www.bocal.cs.univ-paris8.fr/~akennel/",
"https://anri.up8.site/",
))
HttpResponse::Ok().json(v1::websites::tuple())
}

View file

@ -1,21 +1,26 @@
use actix_web::{get, http::header::ContentType, routes, web, HttpResponse, Responder};
use cached::proc_macro::once;
use actix_web::{
get, http::header::ContentType, routes, web, HttpRequest, HttpResponse, Responder,
};
use cached::proc_macro::cached;
use ramhorns::Content;
use crate::{
config::Config,
logic::blog::{build_rss, get_post, get_posts, Post, BLOG_DIR, MIME_TYPE_RSS, POST_DIR},
template::{InfosPage, NavBar},
utils::{
markdown::{File, FilePath},
metadata::MType,
misc::{make_kw, read_file, Html},
routes::blog::{build_rss, get_post, get_posts, Post, BLOG_DIR, MIME_TYPE_RSS, POST_DIR},
misc::{lang, make_kw, read_file_fallback, Html, Lang},
},
};
#[get("/blog")]
pub async fn index(config: web::Data<Config>) -> impl Responder {
Html(build_index(config.get_ref().to_owned()))
pub async fn index(req: HttpRequest, config: web::Data<Config>) -> impl Responder {
Html(build_index(
config.get_ref().to_owned(),
lang(req.headers()),
))
}
#[derive(Content, Debug)]
@ -26,18 +31,19 @@ struct BlogIndexTemplate {
no_posts: bool,
}
#[once(time = 60)]
fn build_index(config: Config) -> String {
#[cached(time = 60)]
fn build_index(config: Config, lang: Lang) -> String {
let blog_dir = format!("{}/{}", config.locations.data_dir, BLOG_DIR);
let mut posts = get_posts(&format!("{blog_dir}/{POST_DIR}"));
// Get about
let about: Option<File> = read_file(
let (about, html_lang) = read_file_fallback(
FilePath {
base: blog_dir,
path: "about.md".to_owned(),
},
MType::Generic,
&lang,
);
// Sort from newest to oldest
@ -63,6 +69,7 @@ fn build_index(config: Config) -> String {
)),
kw: Some(make_kw(&["blog", "blogging"])),
},
Some(html_lang),
)
}
@ -101,6 +108,7 @@ fn build_post(file: &str, config: Config) -> String {
toc,
},
infos,
None,
)
}

View file

@ -1,15 +1,15 @@
use actix_web::{get, routes, web, HttpRequest, Responder};
use cached::proc_macro::once;
use cached::proc_macro::cached;
use ramhorns::Content;
use crate::{
config::Config,
logic::contact::{find_links, read, remove_paragraphs},
template::{InfosPage, NavBar},
utils::{
markdown::{File, FilePath},
metadata::MType,
misc::{make_kw, read_file, Html},
routes::contact::{find_links, read, remove_paragraphs},
misc::{lang, make_kw, read_file_fallback, Html, Lang},
},
};
@ -28,8 +28,8 @@ pub fn pages(cfg: &mut web::ServiceConfig) {
}
#[get("")]
async fn page(config: web::Data<Config>) -> impl Responder {
Html(build_page(config.get_ref().to_owned()))
async fn page(req: HttpRequest, config: web::Data<Config>) -> impl Responder {
Html(build_page(config.get_ref().to_owned(), lang(req.headers())))
}
#[routes]
@ -78,18 +78,19 @@ struct NetworksTemplate {
others: Vec<File>,
}
#[once(time = 60)]
fn build_page(config: Config) -> String {
#[cached(time = 60)]
fn build_page(config: Config, lang: Lang) -> String {
let contacts_dir = format!("{}/{}", config.locations.data_dir, CONTACT_DIR);
let ext = ".md";
// Get about
let about = read_file(
let (about, html_lang) = read_file_fallback(
FilePath {
base: contacts_dir.clone(),
path: "about.md".to_owned(),
},
MType::Generic,
&lang,
);
let mut socials = read(&FilePath {
@ -138,5 +139,6 @@ fn build_page(config: Config) -> String {
"linktree",
])),
},
Some(html_lang),
)
}

View file

@ -1,10 +1,8 @@
use crate::{
config::Config,
logic::contrib::{fetch, Project},
template::{InfosPage, NavBar},
utils::{
misc::{make_kw, Html},
routes::contrib::{fetch, Project},
},
utils::misc::{make_kw, Html},
};
use actix_web::{get, web, Responder};
use cached::proc_macro::once;
@ -87,5 +85,6 @@ async fn build_page(config: Config) -> String {
"code",
])),
},
None,
)
}

View file

@ -6,12 +6,12 @@ use serde::Deserialize;
use crate::{
config::Config,
logic::cours::{excluded, get_filetree},
template::{InfosPage, NavBar},
utils::{
markdown::{File, FilePath},
metadata::MType,
misc::{make_kw, read_file, Html},
routes::cours::{excluded, get_filetree},
},
};
@ -60,13 +60,14 @@ fn get_content(
path: filename.to_owned(),
},
MType::Cours,
None,
)
}
fn build_page(info: &web::Query<PathRequest>, config: Config) -> String {
let cours_dir = "data/cours";
let (ep, el): (_, Vec<String>) = config
let (ep, el): (_, Vec<_>) = config
.fc
.exclude_courses
.unwrap()
@ -106,5 +107,6 @@ fn build_page(info: &web::Query<PathRequest>, config: Config) -> String {
"digital garden",
])),
},
None,
)
}

View file

@ -1,5 +1,5 @@
use actix_web::{get, web, Responder};
use cached::proc_macro::once;
use actix_web::{get, web, HttpRequest, Responder};
use cached::proc_macro::cached;
use ramhorns::Content;
use crate::{
@ -8,13 +8,13 @@ use crate::{
utils::{
markdown::{File, FilePath},
metadata::MType,
misc::{make_kw, read_file, Html},
misc::{lang, make_kw, read_file, read_file_fallback, Html, Lang},
},
};
#[get("/")]
pub async fn page(config: web::Data<Config>) -> impl Responder {
Html(build_page(config.get_ref().to_owned()))
pub async fn page(req: HttpRequest, config: web::Data<Config>) -> impl Responder {
Html(build_page(config.get_ref().to_owned(), lang(req.headers())))
}
#[derive(Content, Debug)]
@ -34,14 +34,15 @@ struct StyleAvatar {
square: bool,
}
#[once(time = 60)]
fn build_page(config: Config) -> String {
let mut file = read_file(
#[cached(time = 60)]
fn build_page(config: Config, lang: Lang) -> String {
let (mut file, html_lang) = read_file_fallback(
FilePath {
base: config.locations.data_dir,
base: config.locations.data_dir.clone(),
path: "index.md".to_owned(),
},
MType::Index,
&lang,
);
// Default values
@ -77,6 +78,7 @@ fn build_page(config: Config) -> String {
path: "README.md".to_owned(),
},
MType::Generic,
None,
);
}
@ -99,5 +101,6 @@ fn build_page(config: Config) -> String {
desc: Some("Page principale".into()),
kw: Some(make_kw(&["index", "étudiant", "accueil"])),
},
Some(html_lang),
)
}

View file

@ -32,5 +32,6 @@ fn build_page(config: Config) -> String {
desc: Some("Une page perdu du web".into()),
..InfosPage::default()
},
None,
)
}

View file

@ -1,5 +1,5 @@
use actix_web::{get, web, Responder};
use cached::proc_macro::once;
use actix_web::{get, web, HttpRequest, Responder};
use cached::proc_macro::cached;
use glob::glob;
use ramhorns::Content;
@ -9,13 +9,13 @@ use crate::{
utils::{
markdown::{File, FilePath},
metadata::MType,
misc::{make_kw, read_file, Html},
misc::{lang, make_kw, read_file, read_file_fallback, Html, Lang},
},
};
#[get("/portfolio")]
pub async fn page(config: web::Data<Config>) -> impl Responder {
Html(build_page(config.get_ref().to_owned()))
pub async fn page(req: HttpRequest, config: web::Data<Config>) -> impl Responder {
Html(build_page(config.get_ref().to_owned(), lang(req.headers())))
}
#[derive(Content, Debug)]
@ -29,8 +29,8 @@ struct PortfolioTemplate<'a> {
err_msg: &'a str,
}
#[once(time = 60)]
fn build_page(config: Config) -> String {
#[cached(time = 60)]
fn build_page(config: Config, lang: Lang) -> String {
let projects_dir = format!("{}/projects", config.locations.data_dir);
let apps_dir = FilePath {
base: format!("{projects_dir}/apps"),
@ -39,12 +39,13 @@ fn build_page(config: Config) -> String {
let ext = ".md";
// Get about
let about = read_file(
let (about, html_lang) = read_file_fallback(
FilePath {
base: projects_dir,
path: "about.md".to_owned(),
},
MType::Generic,
&lang,
);
// Get apps
@ -54,6 +55,7 @@ fn build_page(config: Config) -> String {
read_file(
apps_dir.from(&e.unwrap().to_string_lossy()),
MType::Portfolio,
None,
)
.unwrap()
})
@ -72,6 +74,7 @@ fn build_page(config: Config) -> String {
read_file(
apps_dir.from(&e.unwrap().to_string_lossy()),
MType::Portfolio,
None,
)
.unwrap()
})
@ -112,5 +115,6 @@ fn build_page(config: Config) -> String {
"code",
])),
},
Some(html_lang),
)
}

View file

@ -22,5 +22,6 @@ fn build_page(config: Config) -> String {
desc: Some("Coin reculé de l'internet".into()),
kw: Some(make_kw(&["web3", "blockchain", "nft", "ai"])),
},
None,
)
}

View file

@ -1,7 +1,9 @@
use ramhorns::{Content, Ramhorns};
use crate::utils::misc::Lang;
/// Structure used in the config variable of the app
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Template {
/// Root directory where templates are stored
pub directory: String,
@ -51,12 +53,20 @@ struct DataPage<T> {
page_kw: Option<String>,
/// Author's name
page_author: Option<String>,
/// Language used
lang: String,
/// Data needed to render the page
data: T,
}
impl Template {
pub fn render<C: Content>(&self, template: &str, data: C, info: InfosPage) -> String {
pub fn render<C: Content>(
&self,
template: &str,
data: C,
info: InfosPage,
lang: Option<String>,
) -> String {
let mut templates: Ramhorns = Ramhorns::lazy(&self.directory).unwrap();
let tplt = templates.from_file(template).unwrap();
@ -67,6 +77,7 @@ impl Template {
page_desc: info.desc,
page_kw: info.kw,
page_author: self.name.clone(),
lang: lang.unwrap_or(Lang::default()),
data,
})
}

View file

@ -52,7 +52,7 @@ impl FilePath {
}
/// Options used for parser and compiler MD --> HTML
pub fn get_options(path: Option<FilePath>, metadata_type: MType) -> ComrakOptions {
pub fn get_options(path: Option<FilePath>, metadata_type: MType) -> ComrakOptions<'static> {
comrak::Options {
extension: comrak::ExtensionOptions::builder()
.strikethrough(true)

View file

@ -1,12 +1,15 @@
use std::{fs, path::Path};
use std::{fs, os::unix::fs::MetadataExt, path::Path};
use actix_web::{
http::header::{self, ContentType, TryIntoHeaderValue},
http::StatusCode,
http::{
header::{self, ContentType, HeaderMap, TryIntoHeaderValue},
StatusCode,
},
HttpRequest, HttpResponse, Responder,
};
use base64::{engine::general_purpose, Engine};
use cached::proc_macro::cached;
use mime_guess::mime;
use reqwest::Client;
use crate::config::FileConfiguration;
@ -55,22 +58,60 @@ impl Responder for Html {
}
}
/// Read a file localized, fallback to default file if localized file isn't found
pub fn read_file_fallback(
filename: FilePath,
expected_file: MType,
lang: &Lang,
) -> (Option<File>, String) {
match read_file(filename.clone(), expected_file, Some(lang.clone())) {
None => (
read_file(filename, expected_file, None),
Lang::English.to_string(),
),
data => (data, lang.to_string()),
}
}
/// Read a file
pub fn read_file(filename: FilePath, expected_file: MType) -> Option<File> {
reader(filename, expected_file)
pub fn read_file(filename: FilePath, expected_file: MType, lang: Option<Lang>) -> Option<File> {
reader(filename, expected_file, lang.unwrap_or(Lang::English))
}
#[cached(time = 600)]
fn reader(filename: FilePath, expected_file: MType) -> Option<File> {
let as_str = filename.to_string();
Path::new(&as_str)
.extension()
.and_then(|ext| match ext.to_str().unwrap() {
"pdf" => fs::read(&as_str).map_or(None, |bytes| Some(read_pdf(bytes))),
_ => fs::read_to_string(&as_str).map_or(None, |text| {
fn reader(filename: FilePath, expected_file: MType, lang: Lang) -> Option<File> {
let as_str = match lang {
Lang::French => {
let str = filename.to_string();
let mut parts = str.split('.').collect::<Vec<_>>();
let extension = parts.pop().unwrap_or("");
let filename = parts.join(".");
&format!("{filename}-fr.{extension}")
}
Lang::English => &filename.to_string(),
};
let path = Path::new(as_str);
if let Ok(metadata) = path.metadata() {
// Taille maximale : 30M
if metadata.size() > 30 * 1000 * 1000 {
return None;
}
}
path.extension().and_then(|ext| {
match mime_guess::from_ext(ext.to_str().unwrap_or_default()).first_or_text_plain() {
mime if mime == mime::APPLICATION_PDF => {
fs::read(as_str).map_or(None, |bytes| Some(read_pdf(bytes)))
}
mime if mime.type_() == mime::IMAGE => {
fs::read(as_str).map_or(None, |bytes| Some(read_img(bytes, &mime)))
}
_ => fs::read_to_string(as_str).map_or(None, |text| {
Some(read_md(&filename, &text, expected_file, None, true))
}),
})
}
})
}
fn read_pdf(data: Vec<u8>) -> File {
@ -80,14 +121,59 @@ fn read_pdf(data: Vec<u8>) -> File {
metadata: Metadata::default(),
content: format!(
r#"<embed
src="data:application/pdf;base64,{pdf}"
src="data:{};base64,{pdf}"
style="width: 100%; height: 79vh";
>"#
>"#,
mime::APPLICATION_PDF
),
}
}
fn read_img(data: Vec<u8>, mime: &mime::Mime) -> File {
let image = general_purpose::STANDARD.encode(data);
File {
metadata: Metadata::default(),
content: format!("<img src='data:{mime};base64,{image}'>"),
}
}
/// Remove the first character of a string
pub fn remove_first_letter(s: &str) -> &str {
s.chars().next().map(|c| &s[c.len_utf8()..]).unwrap()
}
#[derive(Hash, PartialEq, Eq, Clone)]
pub enum Lang {
French,
English,
}
impl Lang {
pub fn default() -> String {
Lang::French.to_string()
}
}
impl std::fmt::Display for Lang {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Lang::French => write!(f, "fr"),
Lang::English => write!(f, "en"),
}
}
}
/// Get the browser language
pub fn lang(headers: &HeaderMap) -> Lang {
headers
.get("Accept-Language")
.and_then(|lang| lang.to_str().ok())
.and_then(|lang| {
["fr", "fr-FR"]
.into_iter()
.any(|i| lang.contains(i))
.then_some(Lang::French)
})
.unwrap_or(Lang::English)
}

View file

@ -3,4 +3,3 @@ pub mod github;
pub mod markdown;
pub mod metadata;
pub mod misc;
pub mod routes;

View file

@ -12,7 +12,7 @@ h1 + p {
/* List */
main ul {
column-count: 2;
column-gap: 5em;
column-gap: 1em;
}
main li {

View file

@ -65,6 +65,13 @@ aside li.directory {
cursor: pointer;
}
aside li.file::before {
content: "📄";
margin-left: var(--shift-icon-filetree);
font-size: var(--shift-icon-filetree-size);
line-height: var(--shift-icon-filetree-height);
}
aside a {
text-decoration: none;
}

View file

@ -14,20 +14,13 @@ window.addEventListener("load", () => {
`;
const mono = "font-family: monospace";
const tags = [
let tags = [
new Tag("Comment fonctionne un PC 😵‍💫"),
new Tag("undefined", mono),
new Tag("[object Object]", mono),
new Tag("/api/v1/love", mono),
new Tag("/api/v1/websites", mono),
new Tag("Peak D2 sur Valo 🤡"),
new Tag(
"0x520",
`
background: linear-gradient(to bottom right, red 0%, red 50%, black 50%);
${clipping_text}
text-shadow: 0px 0px 20px light-dark(transparent, var(--font-color));
`
),
new Tag("Nul en CSS", "font-family: 'Comic Sans MS', TSCu_Comic, cursive"),
new Tag("anri k... caterpillar 🐛☝️"),
new Tag(
@ -38,34 +31,51 @@ window.addEventListener("load", () => {
text-shadow: 0px 0px 20px light-dark(var(--font-color), transparent);
`
),
new Tag(
"Free Palestine",
`
background: conic-gradient(at 30% 60%, transparent 230deg, red 0, red 310deg, transparent 0),
linear-gradient(to bottom, black 45%, white 45%, white 67%, DarkGreen 67%);
${clipping_text}
text-shadow: 0px 0px 20px var(--font-color);
`
),
new Tag("School hater"),
new Tag("Étudiant"),
new Tag("Rempli de malice"),
new Tag(
"#NouveauFrontPopulaire ✊",
`
background: linear-gradient(to bottom, #4fb26b 0%, #4fb26b 36%, \
#e62e35 36%, #e62e35 50%, \
#feeb25 50%, #feeb25 62%, \
#724e99 62%, #724e99 77%, \
#e73160 77%);
${clipping_text}
text-shadow: 0px 0px 20px light-dark(var(--font-color), transparent);
`
),
new Tag("s/centre/droite/g", mono),
new Tag("anri.exe", mono),
new Tag("C'est... l'électricien"),
new Tag("Mellow"),
new Tag("Million"),
];
const hour = new Date().getHours();
if (hour <= 8 || hour >= 18) {
tags = tags.concat([
new Tag(
"0x520",
`
background: linear-gradient(to bottom right, red 0%, red 50%, black 50%);
${clipping_text}
text-shadow: 0px 0px 20px light-dark(transparent, var(--font-color));
`
),
new Tag("School hater"),
new Tag(
"Free Palestine",
`
background: conic-gradient(at 30% 60%, transparent 230deg, red 0, red 310deg, transparent 0),
linear-gradient(to bottom, black 45%, white 45%, white 67%, DarkGreen 67%);
${clipping_text}
text-shadow: 0px 0px 20px var(--font-color);
`
),
new Tag(
"#NouveauFrontPopulaire ✊",
`
background: linear-gradient(to bottom, #4fb26b 0%, #4fb26b 36%, \
#e62e35 36%, #e62e35 50%, \
#feeb25 50%, #feeb25 62%, \
#724e99 62%, #724e99 77%, \
#e73160 77%);
${clipping_text}
text-shadow: 0px 0px 20px light-dark(var(--font-color), transparent);
`
),
new Tag("s/centre/droite/g", mono),
]);
}
const random = Math.round(Math.random() * (tags.length - 1));
const element = document.getElementById("subname");
element.textContent = tags[random].variant;

View file

@ -148,6 +148,63 @@ const applyFilter = (mode, item, colors) => {
item.style = "";
};
/**
* Replace base64 urls of embeded PDFs into blob
*/
const blobifyPdfs = () => {
const pdfContentType = "application/pdf";
for (const item of document.getElementsByTagName("embed")) {
if (!item.src.startsWith(`data:${pdfContentType};base64,`)) {
// Exclude embed who are not PDFs encoded via base64
continue;
}
/**
* Convert Base64 data to a blob
* @param {String} b64Data Encoded data
* @param {Number} sliceSize Size of the slices
* @returns Blob representing the data
*/
const base64ToBlob = async (b64Data, sliceSize = 512) => {
const byteCharacters = atob(b64Data);
const byteArrays = [];
for (
let offset = 0;
offset < byteCharacters.length;
offset += sliceSize
) {
const slice = byteCharacters.slice(offset, offset + sliceSize);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
return new Blob(byteArrays, { type: pdfContentType });
};
base64ToBlob(item.src.split(",")[1]).then((blob) => {
const newUrl = URL.createObjectURL(blob);
item.src = newUrl;
const link = document.createElement("a");
link.href = newUrl;
link.target = "_blank";
link.textContent = "Ouvrir le PDF dans un nouvel onglet";
item.insertAdjacentElement("afterend", link);
});
}
};
window.addEventListener("DOMContentLoaded", () => {
// Turn Base64 PDFs into blobs
blobifyPdfs();
});
window.addEventListener("load", () => {
// Fix SVG images
changeTheme(

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="fr">
<html lang="{{lang}}">
<head dir="ltr">
{{>head.html}}
</head>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="fr">
<html lang="{{lang}}">
<head dir="ltr">
{{>head.html}}
<link

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="fr">
<html lang="{{lang}}">
<head dir="ltr">
{{>head.html}}
<link

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="fr">
<html lang="{{lang}}">
<head dir="ltr">
{{>head.html}}
<link rel="stylesheet" href="/css/contact.css" />

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="fr">
<html lang="{{lang}}">
<head dir="ltr">
{{>head.html}}
<link rel="stylesheet" href="/css/contrib.css" />

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="fr">
<html lang="{{lang}}">
<head dir="ltr">
{{>head.html}}
<link rel="stylesheet" href="/css/cours.css" />

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="fr">
<html lang="{{lang}}">
<head dir="ltr">
{{>head.html}}
<link rel="stylesheet" href="/css/index.css" />

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="fr">
<html lang="{{lang}}">
<head dir="ltr">
{{>head.html}}
<link rel="stylesheet" href="/css/portfolio.css" />

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html class="index" lang="fr">
<html class="index" lang="{{lang}}">
<head dir="ltr">
<title>{{page_title}}{{#page_title}} - {{/page_title}}{{app_name}}</title>
<meta charset="UTF-8" />