Compare commits
46 commits
french-quo
...
main
Author | SHA1 | Date | |
---|---|---|---|
adfb769df6 | |||
aaf228c32d | |||
d9f3d64c55 | |||
e1448bd773 | |||
fa4d0ba7e8 | |||
b1c4bbdb27 | |||
c7f1f912f0 | |||
eb55d13c01 | |||
182b17c47f | |||
a5240fea57 | |||
754e717a58 | |||
77970da8b3 | |||
b9bc57c1e0 | |||
744857d685 | |||
1209b0eb36 | |||
40cd5bdca5 | |||
9dde91f8ed | |||
47570bf9e3 | |||
8e1b036386 | |||
8b5c128bfd | |||
7b1fb7dae3 | |||
58c1b8a21a | |||
e54cd44714 | |||
a3161d822d | |||
0f6f2f1fc4 | |||
2a4ae9f273 | |||
aed4fa2bff | |||
856770c2ae | |||
29cf3e3e00 | |||
b02f715c5a | |||
3e5ac643a7 | |||
1097ee5194 | |||
61170953fe | |||
15f8397c6c | |||
87d0fa3c11 | |||
7f3434b7c1 | |||
13d7b54c27 | |||
984ecb6b69 | |||
5a15945439 | |||
e9441dba46 | |||
3f3efe4afa | |||
37b51bcbee | |||
3cc69f3d4f | |||
7432ffd5f9 | |||
764a632ae6 | |||
396bff909e |
36 changed files with 1720 additions and 1499 deletions
23
.forgejo/workflows/pr-check.yml
Normal file
23
.forgejo/workflows/pr-check.yml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
name: PR Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-and-format:
|
||||||
|
container:
|
||||||
|
image: cimg/rust:1.81-node
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build
|
||||||
|
|
||||||
|
- name: Run format check
|
||||||
|
run: cargo fmt --check
|
||||||
|
|
||||||
|
- name: Run Clippy
|
||||||
|
run: cargo clippy
|
13
.gitignore
vendored
13
.gitignore
vendored
|
@ -5,4 +5,15 @@
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
|
||||||
/.vscode
|
/.vscode
|
||||||
/data
|
|
||||||
|
# Data
|
||||||
|
data/index.md
|
||||||
|
|
||||||
|
data/contacts/*
|
||||||
|
data/cours/*
|
||||||
|
data/projects/*
|
||||||
|
|
||||||
|
# Blog
|
||||||
|
data/blog/*.md
|
||||||
|
data/blog/posts/*
|
||||||
|
!data/blog/posts/Makefile
|
||||||
|
|
896
Cargo.lock
generated
896
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
14
Cargo.toml
14
Cargo.toml
|
@ -10,9 +10,9 @@ publish = false
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = { version = "4.6", default-features = false, features = ["macros", "compress-brotli"] }
|
actix-web = { version = "4.9", default-features = false, features = ["macros", "compress-brotli"] }
|
||||||
actix-files = "0.6"
|
actix-files = "0.6"
|
||||||
cached = { version = "0.53", features = ["async", "ahash"] }
|
cached = { version = "0.54", features = ["async", "ahash"] }
|
||||||
ramhorns = "1.0"
|
ramhorns = "1.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
@ -21,12 +21,12 @@ serde_json = "1.0"
|
||||||
minify-html = "0.15"
|
minify-html = "0.15"
|
||||||
minify-js = "0.6"
|
minify-js = "0.6"
|
||||||
glob = "0.3"
|
glob = "0.3"
|
||||||
comrak = "0.26"
|
comrak = "0.29"
|
||||||
reqwest = { version = "0.12", features = ["json"] }
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
chrono = { version = "0.4.38", default-features = false, features = ["clock"]}
|
chrono = { version = "0.4.38", default-features = false, features = ["clock"]}
|
||||||
chrono-tz = "0.9"
|
chrono-tz = "0.10"
|
||||||
rss = { version = "2.0", features = ["atom"] }
|
rss = { version = "2.0", features = ["atom"] }
|
||||||
lol_html = "1.2"
|
lol_html = "2.0"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
mime_guess = "2.0"
|
mime_guess = "2.0"
|
||||||
urlencoding = "2.1"
|
urlencoding = "2.1"
|
||||||
|
@ -35,3 +35,7 @@ cyborgtime = "2.1.1"
|
||||||
|
|
||||||
[lints.clippy]
|
[lints.clippy]
|
||||||
pedantic = "warn"
|
pedantic = "warn"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
strip = "symbols"
|
||||||
|
lto = "thin"
|
||||||
|
|
|
@ -171,6 +171,7 @@ title: Option<String>
|
||||||
date: Option<Date>
|
date: Option<Date>
|
||||||
description: Option<String>
|
description: Option<String>
|
||||||
publish: Option<bool>
|
publish: Option<bool>
|
||||||
|
draft: Option<bool>
|
||||||
tags: Option<Vec<Tag>>
|
tags: Option<Vec<Tag>>
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -180,7 +181,8 @@ Post content
|
||||||
- If no `title`, the filename will be used
|
- If no `title`, the filename will be used
|
||||||
- `date` format is `day-month-year`
|
- `date` format is `day-month-year`
|
||||||
- `publish` is default to false. When false, posts are hidden from index
|
- `publish` is default to false. When false, posts are hidden from index
|
||||||
but accessible, see [#30](https://git.mylloon.fr/Anri/mylloon.fr/issues/30)
|
but accessible.
|
||||||
|
- `draft` is default to false. When true, posts are hidden and unaccessible.
|
||||||
|
|
||||||
### About <!-- omit in toc -->
|
### About <!-- omit in toc -->
|
||||||
|
|
||||||
|
@ -223,6 +225,7 @@ custom: Option<bool>
|
||||||
user: "Option<String>"
|
user: "Option<String>"
|
||||||
link: Option<String>
|
link: Option<String>
|
||||||
newtab: Option<bool>
|
newtab: Option<bool>
|
||||||
|
hide: Option<bool>
|
||||||
description: >
|
description: >
|
||||||
Option<String>
|
Option<String>
|
||||||
---
|
---
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
Easy WebPage generator
|
Easy WebPage generator
|
||||||
|
|
||||||
[![dependency status](https://deps.rs/repo/gitea/git.mylloon.fr/Anri/mylloon.fr/status.svg)](https://deps.rs/repo/gitea/git.mylloon.fr/Anri/mylloon.fr)
|
[![dependency status](https://deps.rs/repo/gitea/git.mylloon.fr/Anri/mylloon.fr/status.svg)](https://deps.rs/repo/gitea/git.mylloon.fr/Anri/mylloon.fr)
|
||||||
![status-badge](https://git.mylloon.fr/Anri/mylloon.fr/badges/workflows/publish.yml/badge.svg)
|
[![status-badge](https://git.mylloon.fr/Anri/mylloon.fr/badges/workflows/publish.yml/badge.svg)](https://git.mylloon.fr/Anri/mylloon.fr/actions?workflow=publish.yml)
|
||||||
|
|
||||||
- See [issues](https://git.mylloon.fr/Anri/mylloon.fr/issues)
|
- See [issues](https://git.mylloon.fr/Anri/mylloon.fr/issues)
|
||||||
- See [documentation](https://git.mylloon.fr/Anri/mylloon.fr/src/branch/main/Documentation.md)
|
- See [documentation](https://git.mylloon.fr/Anri/mylloon.fr/src/branch/main/Documentation.md)
|
||||||
|
|
20
data/blog/posts/Makefile
Normal file
20
data/blog/posts/Makefile
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
MKDIR = mkdir -p
|
||||||
|
TOUCH = touch
|
||||||
|
PRINT = echo
|
||||||
|
|
||||||
|
DATE = $(shell date '+%d-%m-%Y')
|
||||||
|
|
||||||
|
DIR = $(shell date '+%Y/%m')
|
||||||
|
FI := new
|
||||||
|
|
||||||
|
new:
|
||||||
|
$(MKDIR) $(DIR) 2> /dev/null
|
||||||
|
$(TOUCH) $(DIR)/$(FI).md
|
||||||
|
$(PRINT) "---" > $(DIR)/$(FI).md
|
||||||
|
$(PRINT) "publish: false" >> $(DIR)/$(FI).md
|
||||||
|
$(PRINT) "date: $(DATE)" >> $(DIR)/$(FI).md
|
||||||
|
$(PRINT) "draft: true" >> $(DIR)/$(FI).md
|
||||||
|
$(PRINT) "---" >> $(DIR)/$(FI).md
|
||||||
|
|
||||||
|
help:
|
||||||
|
$(PRINT) "make FI=new"
|
10
src/main.rs
10
src/main.rs
|
@ -13,7 +13,7 @@ use crate::routes::{
|
||||||
mod config;
|
mod config;
|
||||||
mod template;
|
mod template;
|
||||||
|
|
||||||
mod misc;
|
mod utils;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
|
@ -43,8 +43,12 @@ async fn main() -> Result<()> {
|
||||||
.add(("Permissions-Policy", "interest-cohort=()")),
|
.add(("Permissions-Policy", "interest-cohort=()")),
|
||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
web::scope("/api")
|
web::scope("/api").service(
|
||||||
.service(web::scope("v1").service(api_v1::love).service(api_v1::btf)),
|
web::scope("v1")
|
||||||
|
.service(api_v1::love)
|
||||||
|
.service(api_v1::btf)
|
||||||
|
.service(api_v1::websites),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.service(index::page)
|
.service(index::page)
|
||||||
.service(agreements::security)
|
.service(agreements::security)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{config::Config, misc::utils::get_url, template::InfosPage};
|
use crate::{config::Config, utils::misc::get_url, template::InfosPage};
|
||||||
use actix_web::{get, http::header::ContentType, routes, web, HttpResponse, Responder};
|
use actix_web::{get, http::header::ContentType, routes, web, HttpResponse, Responder};
|
||||||
use cached::proc_macro::once;
|
use cached::proc_macro::once;
|
||||||
use ramhorns::Content;
|
use ramhorns::Content;
|
||||||
|
|
|
@ -43,3 +43,11 @@ pub async fn btf() -> impl Responder {
|
||||||
|
|
||||||
HttpResponse::Ok().json(info)
|
HttpResponse::Ok().json(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/websites")]
|
||||||
|
pub async fn websites() -> impl Responder {
|
||||||
|
HttpResponse::Ok().json((
|
||||||
|
"http://www.bocal.cs.univ-paris8.fr/~akennel/",
|
||||||
|
"https://anri.up8.site/",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
|
@ -1,33 +1,18 @@
|
||||||
use std::{
|
|
||||||
collections::hash_map::DefaultHasher,
|
|
||||||
hash::{Hash, Hasher},
|
|
||||||
};
|
|
||||||
|
|
||||||
use ::rss::{
|
|
||||||
extension::atom::{AtomExtension, Link},
|
|
||||||
Category, Channel, Guid, Image, Item,
|
|
||||||
};
|
|
||||||
use actix_web::{get, http::header::ContentType, routes, web, HttpResponse, Responder};
|
use actix_web::{get, http::header::ContentType, routes, web, HttpResponse, Responder};
|
||||||
use cached::proc_macro::once;
|
use cached::proc_macro::once;
|
||||||
use chrono::{DateTime, Datelike, Local, NaiveDateTime, Utc};
|
|
||||||
use chrono_tz::Europe;
|
|
||||||
use comrak::{parse_document, Arena};
|
|
||||||
use ramhorns::Content;
|
use ramhorns::Content;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
misc::{
|
|
||||||
date::Date,
|
|
||||||
markdown::{get_metadata, get_options, File, FileMetadataBlog, TypeFileMetadata},
|
|
||||||
utils::{get_url, make_kw, read_file, Html},
|
|
||||||
},
|
|
||||||
template::{InfosPage, NavBar},
|
template::{InfosPage, NavBar},
|
||||||
|
utils::{
|
||||||
|
markdown::File,
|
||||||
|
metadata::MType,
|
||||||
|
misc::{make_kw, read_file, Html},
|
||||||
|
routes::blog::{build_rss, get_post, get_posts, Post, BLOG_DIR, MIME_TYPE_RSS, POST_DIR},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MIME_TYPE_RSS: &str = "application/rss+xml";
|
|
||||||
const BLOG_DIR: &str = "blog";
|
|
||||||
const POST_DIR: &str = "posts";
|
|
||||||
|
|
||||||
#[get("/blog")]
|
#[get("/blog")]
|
||||||
pub async fn index(config: web::Data<Config>) -> impl Responder {
|
pub async fn index(config: web::Data<Config>) -> impl Responder {
|
||||||
Html(build_index(config.get_ref().to_owned()))
|
Html(build_index(config.get_ref().to_owned()))
|
||||||
|
@ -47,8 +32,7 @@ fn build_index(config: Config) -> String {
|
||||||
let mut posts = get_posts(&format!("{blog_dir}/{POST_DIR}"));
|
let mut posts = get_posts(&format!("{blog_dir}/{POST_DIR}"));
|
||||||
|
|
||||||
// Get about
|
// Get about
|
||||||
let about: Option<File> =
|
let about: Option<File> = read_file(format!("{blog_dir}/about.md"), MType::Generic);
|
||||||
read_file(&format!("{blog_dir}/about.md"), &TypeFileMetadata::Generic);
|
|
||||||
|
|
||||||
// Sort from newest to oldest
|
// Sort from newest to oldest
|
||||||
posts.sort_by_cached_key(|p| (p.date.year, p.date.month, p.date.day));
|
posts.sort_by_cached_key(|p| (p.date.year, p.date.month, p.date.day));
|
||||||
|
@ -76,112 +60,6 @@ fn build_index(config: Config) -> String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Content, Debug)]
|
|
||||||
struct Post {
|
|
||||||
title: String,
|
|
||||||
date: Date,
|
|
||||||
url: String,
|
|
||||||
desc: Option<String>,
|
|
||||||
content: Option<String>,
|
|
||||||
tags: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Post {
|
|
||||||
// Fetch the file content
|
|
||||||
fn fetch_content(&mut self, data_dir: &str) {
|
|
||||||
let blog_dir = format!("{data_dir}/{BLOG_DIR}/{POST_DIR}");
|
|
||||||
let ext = ".md";
|
|
||||||
|
|
||||||
if let Some(file) = read_file(
|
|
||||||
&format!("{blog_dir}/{}{ext}", self.url),
|
|
||||||
&TypeFileMetadata::Blog,
|
|
||||||
) {
|
|
||||||
self.content = Some(file.content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Hash for Post {
|
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
||||||
if let Some(content) = &self.content {
|
|
||||||
content.hash(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_posts(location: &str) -> Vec<Post> {
|
|
||||||
let entries = std::fs::read_dir(location).map_or_else(
|
|
||||||
|_| vec![],
|
|
||||||
|res| {
|
|
||||||
res.flatten()
|
|
||||||
.filter(|f| f.path().extension().map_or(false, |ext| ext == "md"))
|
|
||||||
.collect::<Vec<std::fs::DirEntry>>()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
entries
|
|
||||||
.iter()
|
|
||||||
.filter_map(|f| {
|
|
||||||
let fname = f.file_name();
|
|
||||||
let filename = fname.to_string_lossy();
|
|
||||||
let file_without_ext = filename.split_at(filename.len() - 3).0;
|
|
||||||
|
|
||||||
let file_metadata = std::fs::read_to_string(format!("{location}/{filename}"))
|
|
||||||
.map_or_else(
|
|
||||||
|_| FileMetadataBlog {
|
|
||||||
title: Some(file_without_ext.into()),
|
|
||||||
..FileMetadataBlog::default()
|
|
||||||
},
|
|
||||||
|text| {
|
|
||||||
let arena = Arena::new();
|
|
||||||
|
|
||||||
let options = get_options();
|
|
||||||
let root = parse_document(&arena, &text, &options);
|
|
||||||
let mut metadata =
|
|
||||||
get_metadata(root, &TypeFileMetadata::Blog).blog.unwrap();
|
|
||||||
|
|
||||||
// Always have a title
|
|
||||||
metadata.title = metadata
|
|
||||||
.title
|
|
||||||
.map_or_else(|| Some(file_without_ext.into()), Some);
|
|
||||||
|
|
||||||
metadata
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if file_metadata.publish == Some(true) {
|
|
||||||
Some(Post {
|
|
||||||
url: file_without_ext.into(),
|
|
||||||
title: file_metadata.title.unwrap(),
|
|
||||||
date: file_metadata.date.unwrap_or({
|
|
||||||
let m = f.metadata().unwrap();
|
|
||||||
let date = std::convert::Into::<DateTime<Utc>>::into(
|
|
||||||
m.modified().unwrap_or_else(|_| m.created().unwrap()),
|
|
||||||
)
|
|
||||||
.date_naive();
|
|
||||||
|
|
||||||
Date {
|
|
||||||
day: date.day(),
|
|
||||||
month: date.month(),
|
|
||||||
year: date.year(),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
desc: file_metadata.description,
|
|
||||||
content: None,
|
|
||||||
tags: file_metadata
|
|
||||||
.tags
|
|
||||||
.unwrap_or_default()
|
|
||||||
.iter()
|
|
||||||
.map(|t| t.name.clone())
|
|
||||||
.collect(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<Post>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Content, Debug)]
|
#[derive(Content, Debug)]
|
||||||
struct BlogPostTemplate {
|
struct BlogPostTemplate {
|
||||||
navbar: NavBar,
|
navbar: NavBar,
|
||||||
|
@ -220,64 +98,6 @@ fn build_post(file: &str, config: Config) -> String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_post(
|
|
||||||
post: &mut Option<File>,
|
|
||||||
filename: &str,
|
|
||||||
name: &str,
|
|
||||||
data_dir: &str,
|
|
||||||
) -> (InfosPage, String) {
|
|
||||||
let blog_dir = format!("{data_dir}/{BLOG_DIR}/{POST_DIR}");
|
|
||||||
let ext = ".md";
|
|
||||||
|
|
||||||
*post = read_file(
|
|
||||||
&format!("{blog_dir}/{filename}{ext}"),
|
|
||||||
&TypeFileMetadata::Blog,
|
|
||||||
);
|
|
||||||
|
|
||||||
let default = (
|
|
||||||
filename,
|
|
||||||
&format!("Blog d'{name}"),
|
|
||||||
Vec::new(),
|
|
||||||
String::new(),
|
|
||||||
);
|
|
||||||
let (title, desc, tags, toc) = match post {
|
|
||||||
Some(data) => (
|
|
||||||
match &data.metadata.info.blog.as_ref().unwrap().title {
|
|
||||||
Some(text) => text,
|
|
||||||
None => default.0,
|
|
||||||
},
|
|
||||||
match &data.metadata.info.blog.as_ref().unwrap().description {
|
|
||||||
Some(desc) => desc,
|
|
||||||
None => default.1,
|
|
||||||
},
|
|
||||||
match &data.metadata.info.blog.as_ref().unwrap().tags {
|
|
||||||
Some(tags) => tags.clone(),
|
|
||||||
None => default.2,
|
|
||||||
},
|
|
||||||
match &data.metadata.info.blog.as_ref().unwrap().toc {
|
|
||||||
// TODO: Generate TOC
|
|
||||||
Some(true) => String::new(),
|
|
||||||
_ => default.3,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
None => default,
|
|
||||||
};
|
|
||||||
|
|
||||||
(
|
|
||||||
InfosPage {
|
|
||||||
title: Some(format!("Post: {title}")),
|
|
||||||
desc: Some(desc.clone()),
|
|
||||||
kw: Some(make_kw(
|
|
||||||
&["blog", "blogging", "write", "writing"]
|
|
||||||
.into_iter()
|
|
||||||
.chain(tags.iter().map(|t| t.name.as_str()))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
toc,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[routes]
|
#[routes]
|
||||||
#[get("/blog/blog.rss")]
|
#[get("/blog/blog.rss")]
|
||||||
#[get("/blog/rss")]
|
#[get("/blog/rss")]
|
||||||
|
@ -286,110 +106,3 @@ pub async fn rss(config: web::Data<Config>) -> impl Responder {
|
||||||
.content_type(ContentType(MIME_TYPE_RSS.parse().unwrap()))
|
.content_type(ContentType(MIME_TYPE_RSS.parse().unwrap()))
|
||||||
.body(build_rss(config.get_ref().to_owned()))
|
.body(build_rss(config.get_ref().to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[once(time = 10800)] // 3h
|
|
||||||
fn build_rss(config: Config) -> String {
|
|
||||||
let mut posts = get_posts(&format!(
|
|
||||||
"{}/{}/{}",
|
|
||||||
config.locations.data_dir, BLOG_DIR, POST_DIR
|
|
||||||
));
|
|
||||||
|
|
||||||
// Sort from newest to oldest
|
|
||||||
posts.sort_by_cached_key(|p| (p.date.year, p.date.month, p.date.day));
|
|
||||||
posts.reverse();
|
|
||||||
|
|
||||||
// Only the 20 newest
|
|
||||||
let max = 20;
|
|
||||||
if posts.len() > max {
|
|
||||||
posts.drain(max..);
|
|
||||||
}
|
|
||||||
|
|
||||||
let link_to_site = get_url(config.fc.clone());
|
|
||||||
let author = if let (Some(mail), Some(name)) = (config.fc.mail, config.fc.fullname.clone()) {
|
|
||||||
Some(format!("{mail} ({name})"))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let title = format!("Blog d'{}", config.fc.name.unwrap_or_default());
|
|
||||||
let lang = "fr";
|
|
||||||
let channel = Channel {
|
|
||||||
title: title.clone(),
|
|
||||||
link: link_to_site.clone(),
|
|
||||||
description: "Un fil qui parle d'informatique notamment".into(),
|
|
||||||
language: Some(lang.into()),
|
|
||||||
managing_editor: author.clone(),
|
|
||||||
webmaster: author,
|
|
||||||
pub_date: Some(Local::now().to_rfc2822()),
|
|
||||||
categories: ["blog", "blogging", "write", "writing"]
|
|
||||||
.iter()
|
|
||||||
.map(|&c| Category {
|
|
||||||
name: c.into(),
|
|
||||||
..Category::default()
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
generator: Some("ewp with rss crate".into()),
|
|
||||||
docs: Some("https://www.rssboard.org/rss-specification".into()),
|
|
||||||
image: Some(Image {
|
|
||||||
url: format!("{link_to_site}/icons/favicon-32x32.png"),
|
|
||||||
title: title.clone(),
|
|
||||||
link: link_to_site.clone(),
|
|
||||||
..Image::default()
|
|
||||||
}),
|
|
||||||
items: posts
|
|
||||||
.iter_mut()
|
|
||||||
.map(|p| {
|
|
||||||
// Get post data
|
|
||||||
p.fetch_content(&config.locations.data_dir);
|
|
||||||
|
|
||||||
// Build item
|
|
||||||
Item {
|
|
||||||
title: Some(p.title.clone()),
|
|
||||||
link: Some(format!("{}/blog/p/{}", link_to_site, p.url)),
|
|
||||||
description: p.content.clone(),
|
|
||||||
categories: p
|
|
||||||
.tags
|
|
||||||
.iter()
|
|
||||||
.map(|c| Category {
|
|
||||||
name: c.to_owned(),
|
|
||||||
..Category::default()
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
guid: Some(Guid {
|
|
||||||
value: format!("urn:hash:{}", {
|
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
p.hash(&mut hasher);
|
|
||||||
hasher.finish()
|
|
||||||
}),
|
|
||||||
permalink: false,
|
|
||||||
}),
|
|
||||||
pub_date: Some(
|
|
||||||
NaiveDateTime::parse_from_str(
|
|
||||||
&format!("{}-{}-{} 13:12:00", p.date.day, p.date.month, p.date.year),
|
|
||||||
"%d-%m-%Y %H:%M:%S",
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.and_local_timezone(Europe::Paris)
|
|
||||||
.unwrap()
|
|
||||||
.to_rfc2822(),
|
|
||||||
),
|
|
||||||
..Item::default()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
atom_ext: Some(AtomExtension {
|
|
||||||
links: vec![Link {
|
|
||||||
href: format!("{link_to_site}/blog/rss"),
|
|
||||||
rel: "self".into(),
|
|
||||||
hreflang: Some(lang.into()),
|
|
||||||
mime_type: Some(MIME_TYPE_RSS.into()),
|
|
||||||
title: Some(title),
|
|
||||||
length: None,
|
|
||||||
}],
|
|
||||||
}),
|
|
||||||
..Channel::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
std::str::from_utf8(&channel.write_to(Vec::new()).unwrap())
|
|
||||||
.unwrap()
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
use actix_web::{get, routes, web, HttpRequest, Responder};
|
use actix_web::{get, routes, web, HttpRequest, Responder};
|
||||||
use cached::proc_macro::once;
|
use cached::proc_macro::once;
|
||||||
use glob::glob;
|
|
||||||
use ramhorns::Content;
|
use ramhorns::Content;
|
||||||
use std::fs::read_to_string;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
misc::{
|
|
||||||
markdown::{File, TypeFileMetadata},
|
|
||||||
utils::{make_kw, read_file, Html},
|
|
||||||
},
|
|
||||||
template::{InfosPage, NavBar},
|
template::{InfosPage, NavBar},
|
||||||
|
utils::{
|
||||||
|
markdown::File,
|
||||||
|
metadata::MType,
|
||||||
|
misc::{make_kw, read_file, Html},
|
||||||
|
routes::contact::{find_links, read, remove_paragraphs},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const CONTACT_DIR: &str = "contacts";
|
const CONTACT_DIR: &str = "contacts";
|
||||||
|
@ -32,47 +32,6 @@ async fn page(config: web::Data<Config>) -> impl Responder {
|
||||||
Html(build_page(config.get_ref().to_owned()))
|
Html(build_page(config.get_ref().to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contact node
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
struct ContactLink {
|
|
||||||
service: String,
|
|
||||||
scope: Option<String>,
|
|
||||||
link: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[once(time = 60)]
|
|
||||||
fn find_links(directory: String) -> Vec<ContactLink> {
|
|
||||||
// TOML filename
|
|
||||||
let toml_file = "links.toml";
|
|
||||||
|
|
||||||
// Read the TOML file and parse it
|
|
||||||
let toml_str = read_to_string(format!("{directory}/{toml_file}")).unwrap_or_default();
|
|
||||||
|
|
||||||
let mut redirections = vec![];
|
|
||||||
match toml::de::from_str::<toml::Value>(&toml_str) {
|
|
||||||
Ok(data) => {
|
|
||||||
if let Some(section) = data.as_table() {
|
|
||||||
section.iter().for_each(|(key, value)| {
|
|
||||||
// Scopes are delimited with `/`
|
|
||||||
let (service, scope) = match key.split_once('/') {
|
|
||||||
Some((service, scope)) => (service.to_owned(), Some(scope.to_owned())),
|
|
||||||
None => (key.to_owned(), None),
|
|
||||||
};
|
|
||||||
|
|
||||||
redirections.push(ContactLink {
|
|
||||||
service,
|
|
||||||
scope,
|
|
||||||
link: value.as_str().unwrap().to_owned(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => return vec![],
|
|
||||||
}
|
|
||||||
|
|
||||||
redirections
|
|
||||||
}
|
|
||||||
|
|
||||||
#[routes]
|
#[routes]
|
||||||
#[get("/{service}")]
|
#[get("/{service}")]
|
||||||
#[get("/{service}/{scope}")]
|
#[get("/{service}/{scope}")]
|
||||||
|
@ -92,7 +51,7 @@ async fn service_redirection(config: web::Data<Config>, req: HttpRequest) -> imp
|
||||||
_ => false,
|
_ => false,
|
||||||
})
|
})
|
||||||
// Returns the link
|
// Returns the link
|
||||||
.map(|data| data.link.clone())
|
.map(|data| data.url.clone())
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
// This shouldn't be more than one link here
|
// This shouldn't be more than one link here
|
||||||
|
@ -119,39 +78,17 @@ struct NetworksTemplate {
|
||||||
others: Vec<File>,
|
others: Vec<File>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_paragraphs(list: &mut [File]) {
|
|
||||||
list.iter_mut()
|
|
||||||
.for_each(|file| file.content = file.content.replace("<p>", "").replace("</p>", ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[once(time = 60)]
|
#[once(time = 60)]
|
||||||
fn build_page(config: Config) -> String {
|
fn build_page(config: Config) -> String {
|
||||||
let contacts_dir = format!("{}/{}", config.locations.data_dir, CONTACT_DIR);
|
let contacts_dir = format!("{}/{}", config.locations.data_dir, CONTACT_DIR);
|
||||||
let ext = ".md";
|
let ext = ".md";
|
||||||
|
|
||||||
// Get about
|
// Get about
|
||||||
let about = read_file(
|
let about = read_file(format!("{contacts_dir}/about.md"), MType::Generic);
|
||||||
&format!("{contacts_dir}/about.md"),
|
|
||||||
&TypeFileMetadata::Generic,
|
|
||||||
);
|
|
||||||
|
|
||||||
let socials_dir = "socials";
|
let mut socials = read(&format!("{contacts_dir}/socials/*{ext}"));
|
||||||
let mut socials = glob(&format!("{contacts_dir}/{socials_dir}/*{ext}"))
|
let mut forges = read(&format!("{contacts_dir}/forges/*{ext}"));
|
||||||
.unwrap()
|
let mut others = read(&format!("{contacts_dir}/others/*{ext}"));
|
||||||
.map(|e| read_file(&e.unwrap().to_string_lossy(), &TypeFileMetadata::Contact).unwrap())
|
|
||||||
.collect::<Vec<File>>();
|
|
||||||
|
|
||||||
let forges_dir = "forges";
|
|
||||||
let mut forges = glob(&format!("{contacts_dir}/{forges_dir}/*{ext}"))
|
|
||||||
.unwrap()
|
|
||||||
.map(|e| read_file(&e.unwrap().to_string_lossy(), &TypeFileMetadata::Contact).unwrap())
|
|
||||||
.collect::<Vec<File>>();
|
|
||||||
|
|
||||||
let others_dir = "others";
|
|
||||||
let mut others = glob(&format!("{contacts_dir}/{others_dir}/*{ext}"))
|
|
||||||
.unwrap()
|
|
||||||
.map(|e| read_file(&e.unwrap().to_string_lossy(), &TypeFileMetadata::Contact).unwrap())
|
|
||||||
.collect::<Vec<File>>();
|
|
||||||
|
|
||||||
// Remove paragraphs in custom statements
|
// Remove paragraphs in custom statements
|
||||||
[&mut socials, &mut forges, &mut others]
|
[&mut socials, &mut forges, &mut others]
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
misc::{
|
|
||||||
github::{fetch_pr, ProjectState},
|
|
||||||
utils::{make_kw, Html},
|
|
||||||
},
|
|
||||||
template::{InfosPage, NavBar},
|
template::{InfosPage, NavBar},
|
||||||
|
utils::{
|
||||||
|
misc::{make_kw, Html},
|
||||||
|
routes::contrib::{fetch, Project},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use actix_web::{get, web, Responder};
|
use actix_web::{get, web, Responder};
|
||||||
use cached::proc_macro::once;
|
use cached::proc_macro::once;
|
||||||
|
@ -26,24 +24,6 @@ struct PortfolioTemplate {
|
||||||
closed: Option<Vec<Project>>,
|
closed: Option<Vec<Project>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Content, Debug)]
|
|
||||||
struct Project {
|
|
||||||
name: String,
|
|
||||||
url: String,
|
|
||||||
pulls_merged: Vec<Pull>,
|
|
||||||
pulls_open: Vec<Pull>,
|
|
||||||
pulls_closed: Vec<Pull>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Content, Debug)]
|
|
||||||
struct Pull {
|
|
||||||
url: String,
|
|
||||||
id: u32,
|
|
||||||
name_repo: String,
|
|
||||||
title: String,
|
|
||||||
state: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[once(time = 600)] // 10min
|
#[once(time = 600)] // 10min
|
||||||
async fn build_page(config: Config) -> String {
|
async fn build_page(config: Config) -> String {
|
||||||
let navbar = NavBar {
|
let navbar = NavBar {
|
||||||
|
@ -52,88 +32,29 @@ async fn build_page(config: Config) -> String {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch latest data from github
|
// Fetch latest data from github
|
||||||
let data = match fetch_pr().await {
|
let data = match fetch().await {
|
||||||
Ok(projects) => {
|
Ok(data) => PortfolioTemplate {
|
||||||
let mut data: Vec<Project> = Vec::new();
|
navbar,
|
||||||
|
error: false,
|
||||||
// Grouping PRs by projects
|
projects: Some(
|
||||||
let mut map: HashMap<&str, Vec<Pull>> = HashMap::new();
|
data.iter()
|
||||||
for p in &projects {
|
.filter(|&p| !p.pulls_merged.is_empty())
|
||||||
let project = Pull {
|
.cloned()
|
||||||
url: p.contrib_url.clone(),
|
.collect(),
|
||||||
id: p.id,
|
),
|
||||||
name_repo: p.name.clone(),
|
waiting: Some(
|
||||||
title: p.title.clone(),
|
data.iter()
|
||||||
state: p.status as u8,
|
.filter(|&p| !p.pulls_open.is_empty())
|
||||||
};
|
.cloned()
|
||||||
let project_name = p.name.as_str();
|
.collect(),
|
||||||
if map.contains_key(project_name) {
|
),
|
||||||
map.entry(project_name).and_modify(|v| v.push(project));
|
closed: Some(
|
||||||
} else {
|
data.iter()
|
||||||
data.push(Project {
|
.filter(|&p| !p.pulls_closed.is_empty())
|
||||||
name: project_name.into(),
|
.cloned()
|
||||||
url: p.url.clone(),
|
.collect(),
|
||||||
pulls_merged: Vec::new(),
|
),
|
||||||
pulls_closed: Vec::new(),
|
},
|
||||||
pulls_open: Vec::new(),
|
|
||||||
});
|
|
||||||
map.insert(project_name, vec![project]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Distributes each PR in the right vector
|
|
||||||
for d in &mut data {
|
|
||||||
map.get(d.name.as_str()).unwrap().iter().for_each(|p| {
|
|
||||||
let state = p.state.try_into().unwrap();
|
|
||||||
match state {
|
|
||||||
ProjectState::Closed => d.pulls_closed.push(p.to_owned()),
|
|
||||||
ProjectState::Merged => d.pulls_merged.push(p.to_owned()),
|
|
||||||
ProjectState::Open => d.pulls_open.push(p.to_owned()),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let mut name: Vec<char> = d.name.replace('-', " ").chars().collect();
|
|
||||||
name[0] = name[0].to_uppercase().next().unwrap();
|
|
||||||
d.name = name.into_iter().collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ascending order by pulls IDs
|
|
||||||
for d in &mut data {
|
|
||||||
d.pulls_closed.reverse();
|
|
||||||
d.pulls_merged.reverse();
|
|
||||||
d.pulls_open.reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ascending order by number of pulls
|
|
||||||
data.sort_by(|a, b| {
|
|
||||||
b.pulls_merged
|
|
||||||
.len()
|
|
||||||
.partial_cmp(&a.pulls_merged.len())
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
PortfolioTemplate {
|
|
||||||
navbar,
|
|
||||||
error: false,
|
|
||||||
projects: Some(
|
|
||||||
data.iter()
|
|
||||||
.filter(|&p| !p.pulls_merged.is_empty())
|
|
||||||
.cloned()
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
waiting: Some(
|
|
||||||
data.iter()
|
|
||||||
.filter(|&p| !p.pulls_open.is_empty())
|
|
||||||
.cloned()
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
closed: Some(
|
|
||||||
data.iter()
|
|
||||||
.filter(|&p| !p.pulls_closed.is_empty())
|
|
||||||
.cloned()
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("{e}");
|
eprintln!("{e}");
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use actix_web::{get, web, Responder};
|
use actix_web::{get, web, Responder};
|
||||||
use cached::proc_macro::cached;
|
use cached::proc_macro::cached;
|
||||||
use ramhorns::Content;
|
use ramhorns::Content;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
misc::{
|
|
||||||
markdown::{File, TypeFileMetadata},
|
|
||||||
utils::{make_kw, read_file, Html},
|
|
||||||
},
|
|
||||||
template::{InfosPage, NavBar},
|
template::{InfosPage, NavBar},
|
||||||
|
utils::{
|
||||||
|
markdown::File,
|
||||||
|
metadata::MType,
|
||||||
|
misc::{make_kw, read_file, Html},
|
||||||
|
routes::cours::{excluded, get_filetree},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
@ -32,13 +32,6 @@ struct CoursTemplate {
|
||||||
content: Option<File>,
|
content: Option<File>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
|
||||||
struct FileNode {
|
|
||||||
name: String,
|
|
||||||
is_dir: bool,
|
|
||||||
children: Vec<FileNode>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cached]
|
#[cached]
|
||||||
fn compile_patterns(exclusion_list: Vec<String>) -> Vec<Regex> {
|
fn compile_patterns(exclusion_list: Vec<String>) -> Vec<Regex> {
|
||||||
exclusion_list
|
exclusion_list
|
||||||
|
@ -47,76 +40,42 @@ fn compile_patterns(exclusion_list: Vec<String>) -> Vec<Regex> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_filetree(dir_path: &str, exclusion_patterns: &Vec<Regex>) -> FileNode {
|
|
||||||
let children = std::fs::read_dir(dir_path)
|
|
||||||
.unwrap()
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.filter_map(|entry| {
|
|
||||||
let entry_path = entry.path();
|
|
||||||
let entry_name = entry_path.file_name()?.to_string_lossy().to_string();
|
|
||||||
|
|
||||||
// Exclude element with the exclusion_list
|
|
||||||
if exclusion_patterns.iter().any(|re| re.is_match(&entry_name)) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry_path.is_file() {
|
|
||||||
Some(FileNode {
|
|
||||||
name: entry_name,
|
|
||||||
is_dir: false,
|
|
||||||
children: vec![],
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Exclude empty directories
|
|
||||||
let children_of_children =
|
|
||||||
get_filetree(entry_path.to_str().unwrap(), exclusion_patterns);
|
|
||||||
if children_of_children.is_dir && children_of_children.children.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(children_of_children)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
FileNode {
|
|
||||||
name: Path::new(dir_path)
|
|
||||||
.file_name()
|
|
||||||
.unwrap()
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string(),
|
|
||||||
is_dir: true,
|
|
||||||
children,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a page content
|
/// Get a page content
|
||||||
fn get_content(
|
fn get_content(
|
||||||
cours_dir: &str,
|
cours_dir: &str,
|
||||||
path: &web::Query<PathRequest>,
|
path: &web::Query<PathRequest>,
|
||||||
exclusion_list: &[String],
|
exclusion_list: &[String],
|
||||||
|
exclusion_patterns: &[Regex],
|
||||||
) -> Option<File> {
|
) -> Option<File> {
|
||||||
let filename = path.q.as_ref().map_or("index.md", |q| q);
|
let filename = path.q.as_ref().map_or("index.md", |q| q);
|
||||||
|
|
||||||
// We should support regex?
|
// Exclusion checks
|
||||||
if exclusion_list
|
if excluded(filename, exclusion_list, exclusion_patterns) {
|
||||||
.iter()
|
|
||||||
.any(|excluded_term| filename.contains(excluded_term.as_str()))
|
|
||||||
{
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
read_file(
|
read_file(format!("{cours_dir}/{filename}"), MType::Generic)
|
||||||
&format!("{cours_dir}/{filename}"),
|
|
||||||
&TypeFileMetadata::Generic,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_page(info: &web::Query<PathRequest>, config: Config) -> String {
|
fn build_page(info: &web::Query<PathRequest>, config: Config) -> String {
|
||||||
let cours_dir = "data/cours";
|
let cours_dir = "data/cours";
|
||||||
let exclusion_list = config.fc.exclude_courses.unwrap();
|
|
||||||
let exclusion_patterns = compile_patterns(exclusion_list.clone());
|
let (ep, el): (_, Vec<String>) = config
|
||||||
let filetree = get_filetree(cours_dir, &exclusion_patterns);
|
.fc
|
||||||
|
.exclude_courses
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.partition(|item| item.starts_with('/'));
|
||||||
|
|
||||||
|
let exclusion_list = {
|
||||||
|
let mut base = vec!["../".to_owned()];
|
||||||
|
base.extend(el);
|
||||||
|
base
|
||||||
|
};
|
||||||
|
let exclusion_patterns: Vec<Regex> =
|
||||||
|
compile_patterns(ep.iter().map(|r| r[1..r.len() - 1].to_owned()).collect());
|
||||||
|
|
||||||
|
let filetree = get_filetree(cours_dir, &exclusion_list, &exclusion_patterns);
|
||||||
|
|
||||||
config.tmpl.render(
|
config.tmpl.render(
|
||||||
"cours.html",
|
"cours.html",
|
||||||
|
@ -126,7 +85,7 @@ fn build_page(info: &web::Query<PathRequest>, config: Config) -> String {
|
||||||
..NavBar::default()
|
..NavBar::default()
|
||||||
},
|
},
|
||||||
filetree: serde_json::to_string(&filetree).unwrap(),
|
filetree: serde_json::to_string(&filetree).unwrap(),
|
||||||
content: get_content(cours_dir, info, &exclusion_list),
|
content: get_content(cours_dir, info, &exclusion_list, &exclusion_patterns),
|
||||||
},
|
},
|
||||||
InfosPage {
|
InfosPage {
|
||||||
title: Some("Cours".into()),
|
title: Some("Cours".into()),
|
||||||
|
|
|
@ -4,11 +4,12 @@ use ramhorns::Content;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
misc::{
|
|
||||||
markdown::{File, TypeFileMetadata},
|
|
||||||
utils::{make_kw, read_file, Html},
|
|
||||||
},
|
|
||||||
template::{InfosPage, NavBar},
|
template::{InfosPage, NavBar},
|
||||||
|
utils::{
|
||||||
|
markdown::File,
|
||||||
|
metadata::MType,
|
||||||
|
misc::{make_kw, read_file, Html},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
|
@ -36,8 +37,8 @@ struct StyleAvatar {
|
||||||
#[once(time = 60)]
|
#[once(time = 60)]
|
||||||
fn build_page(config: Config) -> String {
|
fn build_page(config: Config) -> String {
|
||||||
let mut file = read_file(
|
let mut file = read_file(
|
||||||
&format!("{}/index.md", config.locations.data_dir),
|
format!("{}/index.md", config.locations.data_dir),
|
||||||
&TypeFileMetadata::Index,
|
MType::Index,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Default values
|
// Default values
|
||||||
|
@ -67,7 +68,7 @@ fn build_page(config: Config) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
file = read_file("README.md", &TypeFileMetadata::Generic);
|
file = read_file("README.md".to_string(), MType::Generic);
|
||||||
}
|
}
|
||||||
|
|
||||||
config.tmpl.render(
|
config.tmpl.render(
|
||||||
|
|
|
@ -4,7 +4,7 @@ use ramhorns::Content;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
misc::utils::{get_url, Html},
|
utils::misc::{get_url, Html},
|
||||||
template::{InfosPage, NavBar},
|
template::{InfosPage, NavBar},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,12 @@ use ramhorns::Content;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
misc::{
|
|
||||||
markdown::{File, TypeFileMetadata},
|
|
||||||
utils::{make_kw, read_file, Html},
|
|
||||||
},
|
|
||||||
template::{InfosPage, NavBar},
|
template::{InfosPage, NavBar},
|
||||||
|
utils::{
|
||||||
|
markdown::File,
|
||||||
|
metadata::MType,
|
||||||
|
misc::{make_kw, read_file, Html},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[get("/portfolio")]
|
#[get("/portfolio")]
|
||||||
|
@ -36,14 +37,20 @@ fn build_page(config: Config) -> String {
|
||||||
|
|
||||||
// Get about
|
// Get about
|
||||||
let about = read_file(
|
let about = read_file(
|
||||||
&format!("{projects_dir}/about.md"),
|
format!("{projects_dir}/about.md"),
|
||||||
&TypeFileMetadata::Generic,
|
MType::Generic,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get apps
|
// Get apps
|
||||||
let apps = glob(&format!("{apps_dir}/*{ext}"))
|
let apps = glob(&format!("{apps_dir}/*{ext}"))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map(|e| read_file(&e.unwrap().to_string_lossy(), &TypeFileMetadata::Portfolio).unwrap())
|
.map(|e| {
|
||||||
|
read_file(
|
||||||
|
e.unwrap().to_string_lossy().to_string(),
|
||||||
|
MType::Portfolio,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
.collect::<Vec<File>>();
|
.collect::<Vec<File>>();
|
||||||
|
|
||||||
let appdata = if apps.is_empty() {
|
let appdata = if apps.is_empty() {
|
||||||
|
@ -55,7 +62,13 @@ fn build_page(config: Config) -> String {
|
||||||
// Get archived apps
|
// Get archived apps
|
||||||
let archived_apps = glob(&format!("{apps_dir}/archive/*{ext}"))
|
let archived_apps = glob(&format!("{apps_dir}/archive/*{ext}"))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map(|e| read_file(&e.unwrap().to_string_lossy(), &TypeFileMetadata::Portfolio).unwrap())
|
.map(|e| {
|
||||||
|
read_file(
|
||||||
|
e.unwrap().to_string_lossy().to_string(),
|
||||||
|
MType::Portfolio,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
.collect::<Vec<File>>();
|
.collect::<Vec<File>>();
|
||||||
|
|
||||||
let archived_appdata = if archived_apps.is_empty() {
|
let archived_appdata = if archived_apps.is_empty() {
|
||||||
|
|
|
@ -3,7 +3,7 @@ use cached::proc_macro::once;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
misc::utils::{make_kw, Html},
|
utils::misc::{make_kw, Html},
|
||||||
template::InfosPage,
|
template::InfosPage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ use chrono::{Datelike, NaiveDate};
|
||||||
use ramhorns::Content;
|
use ramhorns::Content;
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
|
|
||||||
#[derive(Content, Default, Debug)]
|
#[derive(Content, Clone, Default, Debug)]
|
||||||
pub struct Date {
|
pub struct Date {
|
||||||
pub day: u32,
|
pub day: u32,
|
||||||
pub month: u32,
|
pub month: u32,
|
|
@ -1,7 +1,7 @@
|
||||||
use reqwest::{header::ACCEPT, Error};
|
use reqwest::{header::ACCEPT, Error};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::misc::utils::get_reqwest_client;
|
use crate::utils::misc::get_reqwest_client;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct GithubResponse {
|
struct GithubResponse {
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::misc::date::Date;
|
|
||||||
use base64::engine::general_purpose;
|
use base64::engine::general_purpose;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use comrak::nodes::{AstNode, NodeValue};
|
use comrak::nodes::{AstNode, NodeValue};
|
||||||
|
@ -6,116 +5,16 @@ use comrak::{format_html, parse_document, Arena, ComrakOptions, ListStyleType, O
|
||||||
use lol_html::html_content::ContentType;
|
use lol_html::html_content::ContentType;
|
||||||
use lol_html::{element, rewrite_str, HtmlRewriter, RewriteStrSettings, Settings};
|
use lol_html::{element, rewrite_str, HtmlRewriter, RewriteStrSettings, Settings};
|
||||||
use ramhorns::Content;
|
use ramhorns::Content;
|
||||||
use serde::{Deserialize, Deserializer};
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
/// Metadata for blog posts
|
use crate::utils::metadata::MType;
|
||||||
#[derive(Content, Debug, Default, Deserialize)]
|
|
||||||
pub struct FileMetadataBlog {
|
|
||||||
pub title: Option<String>,
|
|
||||||
pub date: Option<Date>,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub publish: Option<bool>,
|
|
||||||
pub tags: Option<Vec<Tag>>,
|
|
||||||
pub toc: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A tag, related to post blog
|
use super::metadata::{get, MFile, Metadata};
|
||||||
#[derive(Content, Debug, Clone)]
|
|
||||||
pub struct Tag {
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Tag {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
match <&str>::deserialize(deserializer) {
|
|
||||||
Ok(s) => match serde_yml::from_str(s) {
|
|
||||||
Ok(tag) => Ok(Self { name: tag }),
|
|
||||||
Err(e) => Err(serde::de::Error::custom(e)),
|
|
||||||
},
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Metadata for contact entry
|
|
||||||
#[derive(Content, Debug, Default, Deserialize)]
|
|
||||||
pub struct FileMetadataContact {
|
|
||||||
pub title: String,
|
|
||||||
pub custom: Option<bool>,
|
|
||||||
pub user: Option<String>,
|
|
||||||
pub link: Option<String>,
|
|
||||||
pub newtab: Option<bool>,
|
|
||||||
pub description: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Metadata for index page
|
|
||||||
#[derive(Content, Debug, Default, Deserialize)]
|
|
||||||
pub struct FileMetadataIndex {
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub pronouns: Option<String>,
|
|
||||||
pub avatar: Option<String>,
|
|
||||||
pub avatar_caption: Option<String>,
|
|
||||||
pub avatar_style: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Metadata for portfolio cards
|
|
||||||
#[derive(Content, Debug, Default, Deserialize)]
|
|
||||||
pub struct FileMetadataPortfolio {
|
|
||||||
pub title: Option<String>,
|
|
||||||
pub link: Option<String>,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub language: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List of available metadata types
|
|
||||||
pub enum TypeFileMetadata {
|
|
||||||
Blog,
|
|
||||||
Contact,
|
|
||||||
Generic,
|
|
||||||
Index,
|
|
||||||
Portfolio,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Structure who holds all the metadata the file have
|
|
||||||
/// Usually all fields are None except one
|
|
||||||
#[derive(Content, Debug, Default, Deserialize)]
|
|
||||||
pub struct FileMetadata {
|
|
||||||
pub blog: Option<FileMetadataBlog>,
|
|
||||||
pub contact: Option<FileMetadataContact>,
|
|
||||||
pub index: Option<FileMetadataIndex>,
|
|
||||||
pub portfolio: Option<FileMetadataPortfolio>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
|
||||||
/// Global metadata
|
|
||||||
#[derive(Content, Debug)]
|
|
||||||
pub struct Metadata {
|
|
||||||
pub info: FileMetadata,
|
|
||||||
pub math: bool,
|
|
||||||
pub mermaid: bool,
|
|
||||||
pub syntax_highlight: bool,
|
|
||||||
pub mail_obfsucated: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Metadata {
|
|
||||||
/// Update current metadata boolean fields, keeping true ones
|
|
||||||
fn merge(&mut self, other: &Self) {
|
|
||||||
self.math = self.math || other.math;
|
|
||||||
self.mermaid = self.mermaid || other.mermaid;
|
|
||||||
self.syntax_highlight = self.syntax_highlight || other.syntax_highlight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// File description
|
/// File description
|
||||||
#[derive(Content, Debug)]
|
#[derive(Content, Debug, Clone)]
|
||||||
pub struct File {
|
pub struct File {
|
||||||
pub metadata: Metadata,
|
pub metadata: Metadata,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
@ -153,7 +52,7 @@ pub fn get_options<'a>() -> ComrakOptions<'a> {
|
||||||
// options.render.broken_link_callback = ...;
|
// options.render.broken_link_callback = ...;
|
||||||
|
|
||||||
// Renderer
|
// Renderer
|
||||||
options.render.hardbreaks = false; // could be true? change by metadata could be good for compatibility
|
options.render.hardbreaks = false;
|
||||||
options.render.github_pre_lang = false;
|
options.render.github_pre_lang = false;
|
||||||
options.render.full_info_string = true;
|
options.render.full_info_string = true;
|
||||||
options.render.width = 0; // 0 mean disabled?
|
options.render.width = 0; // 0 mean disabled?
|
||||||
|
@ -167,6 +66,7 @@ pub fn get_options<'a>() -> ComrakOptions<'a> {
|
||||||
options.render.ignore_empty_links = true;
|
options.render.ignore_empty_links = true;
|
||||||
options.render.gfm_quirks = true;
|
options.render.gfm_quirks = true;
|
||||||
options.render.prefer_fenced = false;
|
options.render.prefer_fenced = false;
|
||||||
|
options.render.figure_with_caption = false;
|
||||||
|
|
||||||
options
|
options
|
||||||
}
|
}
|
||||||
|
@ -222,69 +122,69 @@ fn custom_img_size(html: &str) -> String {
|
||||||
/// Fix local images to base64 and integration of markdown files
|
/// Fix local images to base64 and integration of markdown files
|
||||||
fn fix_images_and_integration(path: &str, html: &str) -> (String, Metadata) {
|
fn fix_images_and_integration(path: &str, html: &str) -> (String, Metadata) {
|
||||||
let mut metadata = Metadata {
|
let mut metadata = Metadata {
|
||||||
info: FileMetadata::default(),
|
info: MFile::default(),
|
||||||
math: false,
|
math: false,
|
||||||
mermaid: false,
|
mermaid: false,
|
||||||
syntax_highlight: false,
|
syntax_highlight: false,
|
||||||
mail_obfsucated: false,
|
mail_obfsucated: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
(
|
// Collection of any additional metadata
|
||||||
rewrite_str(
|
let mut additional_metadata = Vec::new();
|
||||||
html,
|
|
||||||
RewriteStrSettings {
|
|
||||||
element_content_handlers: vec![element!("img", |el| {
|
|
||||||
if let Some(src) = el.get_attribute("src") {
|
|
||||||
let img_src = Path::new(path).parent().unwrap();
|
|
||||||
let img_path = urlencoding::decode(img_src.join(src).to_str().unwrap())
|
|
||||||
.unwrap()
|
|
||||||
.to_string();
|
|
||||||
if let Ok(file) = fs::read_to_string(&img_path) {
|
|
||||||
let mime = mime_guess::from_path(&img_path).first_or_octet_stream();
|
|
||||||
if mime == "text/markdown" {
|
|
||||||
let mut options = get_options();
|
|
||||||
options.extension.footnotes = false;
|
|
||||||
let data = read_md(
|
|
||||||
&img_path,
|
|
||||||
&file,
|
|
||||||
&TypeFileMetadata::Generic,
|
|
||||||
Some(options),
|
|
||||||
);
|
|
||||||
el.replace(&data.content, ContentType::Html);
|
|
||||||
metadata.merge(&data.metadata);
|
|
||||||
} else {
|
|
||||||
let image = general_purpose::STANDARD.encode(file);
|
|
||||||
|
|
||||||
el.set_attribute("src", &format!("data:{mime};base64,{image}"))
|
let result = rewrite_str(
|
||||||
.unwrap();
|
html,
|
||||||
}
|
RewriteStrSettings {
|
||||||
|
element_content_handlers: vec![element!("img", |el| {
|
||||||
|
if let Some(src) = el.get_attribute("src") {
|
||||||
|
let img_src = Path::new(path).parent().unwrap();
|
||||||
|
let img_path = urlencoding::decode(img_src.join(src).to_str().unwrap())
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
if let Ok(file) = fs::read_to_string(&img_path) {
|
||||||
|
let mime = mime_guess::from_path(&img_path).first_or_octet_stream();
|
||||||
|
if mime == "text/markdown" {
|
||||||
|
let mut options = get_options();
|
||||||
|
options.extension.footnotes = false;
|
||||||
|
let data = read_md(&img_path, &file, MType::Generic, Some(options));
|
||||||
|
el.replace(&data.content, ContentType::Html);
|
||||||
|
|
||||||
|
// Store the metadata for later merging
|
||||||
|
additional_metadata.push(data.metadata);
|
||||||
|
} else {
|
||||||
|
let image = general_purpose::STANDARD.encode(file);
|
||||||
|
el.set_attribute("src", &format!("data:{mime};base64,{image}"))
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})],
|
})],
|
||||||
..RewriteStrSettings::default()
|
..RewriteStrSettings::default()
|
||||||
},
|
},
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
metadata,
|
|
||||||
)
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Merge all collected metadata
|
||||||
|
for additional in additional_metadata {
|
||||||
|
metadata.merge(&additional);
|
||||||
|
}
|
||||||
|
|
||||||
|
(result, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transform markdown string to File structure
|
/// Transform markdown string to File structure
|
||||||
pub fn read_md(
|
pub fn read_md(path: &str, raw_text: &str, metadata_type: MType, options: Option<Options>) -> File {
|
||||||
path: &str,
|
|
||||||
raw_text: &str,
|
|
||||||
metadata_type: &TypeFileMetadata,
|
|
||||||
options: Option<Options>,
|
|
||||||
) -> File {
|
|
||||||
let arena = Arena::new();
|
let arena = Arena::new();
|
||||||
|
|
||||||
let opt = options.map_or_else(get_options, |specific_opt| specific_opt);
|
let mut opt = options.map_or_else(get_options, |specific_opt| specific_opt);
|
||||||
let root = parse_document(&arena, raw_text, &opt);
|
let root = parse_document(&arena, raw_text, &opt);
|
||||||
|
|
||||||
// Find metadata
|
// Find metadata
|
||||||
let metadata = get_metadata(root, metadata_type);
|
let metadata = get(root, metadata_type);
|
||||||
|
|
||||||
|
// Update comrak render properties
|
||||||
|
opt.render.hardbreaks = metadata.hardbreaks;
|
||||||
|
|
||||||
let mermaid_name = "mermaid";
|
let mermaid_name = "mermaid";
|
||||||
hljs_replace(root, mermaid_name);
|
hljs_replace(root, mermaid_name);
|
||||||
|
@ -316,70 +216,6 @@ pub fn read_md(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deserialize metadata based on a type
|
|
||||||
fn deserialize_metadata<T: Default + serde::de::DeserializeOwned>(text: &str) -> T {
|
|
||||||
serde_yml::from_str(text.trim().trim_matches(&['-'] as &[_])).unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch metadata from AST
|
|
||||||
pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: &TypeFileMetadata) -> FileMetadata {
|
|
||||||
root.children()
|
|
||||||
.find_map(|node| match &node.data.borrow().value {
|
|
||||||
// Extract metadata from frontmatter
|
|
||||||
NodeValue::FrontMatter(text) => Some(match mtype {
|
|
||||||
TypeFileMetadata::Blog => FileMetadata {
|
|
||||||
blog: Some(deserialize_metadata(text)),
|
|
||||||
..FileMetadata::default()
|
|
||||||
},
|
|
||||||
TypeFileMetadata::Contact => {
|
|
||||||
let mut metadata: FileMetadataContact = deserialize_metadata(text);
|
|
||||||
|
|
||||||
// Trim descriptions
|
|
||||||
if let Some(desc) = &mut metadata.description {
|
|
||||||
desc.clone_from(&desc.trim().into());
|
|
||||||
}
|
|
||||||
|
|
||||||
FileMetadata {
|
|
||||||
contact: Some(metadata),
|
|
||||||
..FileMetadata::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TypeFileMetadata::Generic => FileMetadata::default(),
|
|
||||||
TypeFileMetadata::Index => FileMetadata {
|
|
||||||
index: Some(deserialize_metadata(text)),
|
|
||||||
..FileMetadata::default()
|
|
||||||
},
|
|
||||||
TypeFileMetadata::Portfolio => FileMetadata {
|
|
||||||
portfolio: Some(deserialize_metadata(text)),
|
|
||||||
..FileMetadata::default()
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.map_or_else(
|
|
||||||
|| match mtype {
|
|
||||||
TypeFileMetadata::Blog => FileMetadata {
|
|
||||||
blog: Some(FileMetadataBlog::default()),
|
|
||||||
..FileMetadata::default()
|
|
||||||
},
|
|
||||||
TypeFileMetadata::Contact => FileMetadata {
|
|
||||||
contact: Some(FileMetadataContact::default()),
|
|
||||||
..FileMetadata::default()
|
|
||||||
},
|
|
||||||
TypeFileMetadata::Generic => FileMetadata::default(),
|
|
||||||
TypeFileMetadata::Index => FileMetadata {
|
|
||||||
index: Some(FileMetadataIndex::default()),
|
|
||||||
..FileMetadata::default()
|
|
||||||
},
|
|
||||||
TypeFileMetadata::Portfolio => FileMetadata {
|
|
||||||
portfolio: Some(FileMetadataPortfolio::default()),
|
|
||||||
..FileMetadata::default()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|data| data,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether mermaid diagrams are in the AST
|
/// Check whether mermaid diagrams are in the AST
|
||||||
fn check_mermaid<'a>(root: &'a AstNode<'a>, mermaid_str: &str) -> bool {
|
fn check_mermaid<'a>(root: &'a AstNode<'a>, mermaid_str: &str) -> bool {
|
||||||
root.children().any(|node| match &node.data.borrow().value {
|
root.children().any(|node| match &node.data.borrow().value {
|
||||||
|
@ -405,12 +241,12 @@ fn check_code<'a>(root: &'a AstNode<'a>, blacklist: &[String]) -> bool {
|
||||||
|
|
||||||
/// Check if html contains maths
|
/// Check if html contains maths
|
||||||
fn check_math(html: &str) -> bool {
|
fn check_math(html: &str) -> bool {
|
||||||
let math_detected = Arc::new(AtomicBool::new(false));
|
let mut math_detected = false;
|
||||||
|
|
||||||
let _ = HtmlRewriter::new(
|
let _ = HtmlRewriter::new(
|
||||||
Settings {
|
Settings {
|
||||||
element_content_handlers: vec![element!("span[data-math-style]", |_| {
|
element_content_handlers: vec![element!("span[data-math-style]", |_| {
|
||||||
math_detected.store(true, Ordering::SeqCst);
|
math_detected = true;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})],
|
})],
|
||||||
|
@ -420,7 +256,7 @@ fn check_math(html: &str) -> bool {
|
||||||
)
|
)
|
||||||
.write(html.as_bytes());
|
.write(html.as_bytes());
|
||||||
|
|
||||||
math_detected.load(Ordering::SeqCst)
|
math_detected
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change class of languages for hljs detection
|
/// Change class of languages for hljs detection
|
||||||
|
@ -436,7 +272,7 @@ fn hljs_replace<'a>(root: &'a AstNode<'a>, mermaid_str: &str) {
|
||||||
|
|
||||||
/// Obfuscate email if email found
|
/// Obfuscate email if email found
|
||||||
fn mail_obfuscation(html: &str) -> (String, bool) {
|
fn mail_obfuscation(html: &str) -> (String, bool) {
|
||||||
let modified = Arc::new(AtomicBool::new(false));
|
let mut modified = false;
|
||||||
|
|
||||||
let data_attr = "title";
|
let data_attr = "title";
|
||||||
|
|
||||||
|
@ -445,7 +281,7 @@ fn mail_obfuscation(html: &str) -> (String, bool) {
|
||||||
html,
|
html,
|
||||||
RewriteStrSettings {
|
RewriteStrSettings {
|
||||||
element_content_handlers: vec![element!("a[href^='mailto:']", |el| {
|
element_content_handlers: vec![element!("a[href^='mailto:']", |el| {
|
||||||
modified.store(true, Ordering::SeqCst);
|
modified = true;
|
||||||
|
|
||||||
// Get mail address
|
// Get mail address
|
||||||
let link = el.get_attribute("href").unwrap();
|
let link = el.get_attribute("href").unwrap();
|
||||||
|
@ -465,9 +301,7 @@ fn mail_obfuscation(html: &str) -> (String, bool) {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let is_modified = modified.load(Ordering::SeqCst);
|
if modified {
|
||||||
|
|
||||||
if is_modified {
|
|
||||||
// Remove old data email if exists
|
// Remove old data email if exists
|
||||||
(
|
(
|
||||||
rewrite_str(
|
rewrite_str(
|
||||||
|
@ -493,9 +327,9 @@ fn mail_obfuscation(html: &str) -> (String, bool) {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
is_modified,
|
modified,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(new_html, is_modified)
|
(new_html, modified)
|
||||||
}
|
}
|
||||||
}
|
}
|
185
src/utils/metadata.rs
Normal file
185
src/utils/metadata.rs
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
use crate::utils::date::Date;
|
||||||
|
use comrak::nodes::{AstNode, NodeValue};
|
||||||
|
use ramhorns::Content;
|
||||||
|
use serde::{Deserialize, Deserializer};
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
/// Metadata for blog posts
|
||||||
|
#[derive(Content, Clone, Debug, Default, Deserialize)]
|
||||||
|
pub struct FileMetadataBlog {
|
||||||
|
pub hardbreaks: Option<bool>,
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub date: Option<Date>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub publish: Option<bool>,
|
||||||
|
pub draft: Option<bool>,
|
||||||
|
pub tags: Option<Vec<Tag>>,
|
||||||
|
pub toc: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A tag, related to post blog
|
||||||
|
#[derive(Content, Debug, Clone)]
|
||||||
|
pub struct Tag {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Deserialize<'a> for Tag {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'a>,
|
||||||
|
{
|
||||||
|
match <&str>::deserialize(deserializer) {
|
||||||
|
Ok(s) => match serde_yml::from_str(s) {
|
||||||
|
Ok(tag) => Ok(Self { name: tag }),
|
||||||
|
Err(e) => Err(serde::de::Error::custom(e)),
|
||||||
|
},
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Metadata for contact entry
|
||||||
|
#[derive(Content, Debug, Default, Deserialize, Clone)]
|
||||||
|
pub struct FileMetadataContact {
|
||||||
|
pub title: String,
|
||||||
|
pub custom: Option<bool>,
|
||||||
|
pub user: Option<String>,
|
||||||
|
pub link: Option<String>,
|
||||||
|
pub newtab: Option<bool>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub hide: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Metadata for index page
|
||||||
|
#[derive(Content, Debug, Default, Deserialize, Clone)]
|
||||||
|
pub struct FileMetadataIndex {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub pronouns: Option<String>,
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub avatar_caption: Option<String>,
|
||||||
|
pub avatar_style: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Metadata for portfolio cards
|
||||||
|
#[derive(Content, Debug, Default, Deserialize, Clone)]
|
||||||
|
pub struct FileMetadataPortfolio {
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub link: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub language: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List of available metadata types
|
||||||
|
#[derive(Hash, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum MType {
|
||||||
|
Blog,
|
||||||
|
Contact,
|
||||||
|
Generic,
|
||||||
|
Index,
|
||||||
|
Portfolio,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Structure who holds all the metadata the file have
|
||||||
|
/// Usually all fields are None except one
|
||||||
|
#[derive(Content, Debug, Default, Deserialize, Clone)]
|
||||||
|
pub struct MFile {
|
||||||
|
pub hardbreaks: bool,
|
||||||
|
pub blog: Option<FileMetadataBlog>,
|
||||||
|
pub contact: Option<FileMetadataContact>,
|
||||||
|
pub index: Option<FileMetadataIndex>,
|
||||||
|
pub portfolio: Option<FileMetadataPortfolio>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
/// Global metadata
|
||||||
|
#[derive(Content, Debug, Clone)]
|
||||||
|
pub struct Metadata {
|
||||||
|
pub info: MFile,
|
||||||
|
pub math: bool,
|
||||||
|
pub mermaid: bool,
|
||||||
|
pub syntax_highlight: bool,
|
||||||
|
pub mail_obfsucated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Metadata {
|
||||||
|
/// Update current metadata boolean fields, keeping true ones
|
||||||
|
pub fn merge(&mut self, other: &Self) {
|
||||||
|
self.math = self.math || other.math;
|
||||||
|
self.mermaid = self.mermaid || other.mermaid;
|
||||||
|
self.syntax_highlight = self.syntax_highlight || other.syntax_highlight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize metadata based on a type
|
||||||
|
fn deserialize_metadata<T: Default + serde::de::DeserializeOwned>(text: &str) -> T {
|
||||||
|
serde_yml::from_str(text.trim().trim_matches(&['-'] as &[_])).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch metadata from AST
|
||||||
|
pub fn get<'a>(root: &'a AstNode<'a>, mtype: MType) -> MFile {
|
||||||
|
root.children()
|
||||||
|
.map(|node| {
|
||||||
|
let generic = MFile {
|
||||||
|
hardbreaks: true,
|
||||||
|
..MFile::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match &node.data.borrow().value {
|
||||||
|
// Extract metadata from frontmatter
|
||||||
|
NodeValue::FrontMatter(text) => match mtype {
|
||||||
|
MType::Blog => {
|
||||||
|
let metadata: FileMetadataBlog = deserialize_metadata(text);
|
||||||
|
MFile {
|
||||||
|
blog: Some(metadata.clone()),
|
||||||
|
hardbreaks: metadata.hardbreaks.unwrap_or_default(),
|
||||||
|
..MFile::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MType::Contact => {
|
||||||
|
let mut metadata: FileMetadataContact = deserialize_metadata(text);
|
||||||
|
// Trim descriptions
|
||||||
|
if let Some(desc) = &mut metadata.description {
|
||||||
|
desc.clone_from(&desc.trim().into());
|
||||||
|
}
|
||||||
|
MFile {
|
||||||
|
contact: Some(metadata),
|
||||||
|
..MFile::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MType::Generic => generic,
|
||||||
|
MType::Index => MFile {
|
||||||
|
index: Some(deserialize_metadata(text)),
|
||||||
|
..MFile::default()
|
||||||
|
},
|
||||||
|
MType::Portfolio => MFile {
|
||||||
|
portfolio: Some(deserialize_metadata(text)),
|
||||||
|
..MFile::default()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_ => generic,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
.map_or_else(
|
||||||
|
|| match mtype {
|
||||||
|
MType::Blog => MFile {
|
||||||
|
blog: Some(FileMetadataBlog::default()),
|
||||||
|
..MFile::default()
|
||||||
|
},
|
||||||
|
MType::Contact => MFile {
|
||||||
|
contact: Some(FileMetadataContact::default()),
|
||||||
|
..MFile::default()
|
||||||
|
},
|
||||||
|
MType::Generic => MFile::default(),
|
||||||
|
MType::Index => MFile {
|
||||||
|
index: Some(FileMetadataIndex::default()),
|
||||||
|
..MFile::default()
|
||||||
|
},
|
||||||
|
MType::Portfolio => MFile {
|
||||||
|
portfolio: Some(FileMetadataPortfolio::default()),
|
||||||
|
..MFile::default()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|data| data,
|
||||||
|
)
|
||||||
|
}
|
|
@ -11,7 +11,10 @@ use reqwest::Client;
|
||||||
|
|
||||||
use crate::config::FileConfiguration;
|
use crate::config::FileConfiguration;
|
||||||
|
|
||||||
use super::markdown::{read_md, File, FileMetadata, Metadata, TypeFileMetadata};
|
use super::{
|
||||||
|
markdown::{read_md, File},
|
||||||
|
metadata::{MFile, MType, Metadata},
|
||||||
|
};
|
||||||
|
|
||||||
#[cached]
|
#[cached]
|
||||||
pub fn get_reqwest_client() -> Client {
|
pub fn get_reqwest_client() -> Client {
|
||||||
|
@ -53,13 +56,14 @@ impl Responder for Html {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read a file
|
/// Read a file
|
||||||
pub fn read_file(filename: &str, expected_file: &TypeFileMetadata) -> Option<File> {
|
#[cached]
|
||||||
Path::new(filename)
|
pub fn read_file(filename: String, expected_file: MType) -> Option<File> {
|
||||||
|
Path::new(&filename.clone())
|
||||||
.extension()
|
.extension()
|
||||||
.and_then(|ext| match ext.to_str().unwrap() {
|
.and_then(|ext| match ext.to_str().unwrap() {
|
||||||
"pdf" => fs::read(filename).map_or(None, |bytes| Some(read_pdf(bytes))),
|
"pdf" => fs::read(filename).map_or(None, |bytes| Some(read_pdf(bytes))),
|
||||||
_ => fs::read_to_string(filename).map_or(None, |text| {
|
_ => fs::read_to_string(&filename).map_or(None, |text| {
|
||||||
Some(read_md(filename, &text, expected_file, None))
|
Some(read_md(&filename, &text, expected_file, None))
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -69,7 +73,7 @@ fn read_pdf(data: Vec<u8>) -> File {
|
||||||
|
|
||||||
File {
|
File {
|
||||||
metadata: Metadata {
|
metadata: Metadata {
|
||||||
info: FileMetadata::default(),
|
info: MFile::default(),
|
||||||
mermaid: false,
|
mermaid: false,
|
||||||
syntax_highlight: false,
|
syntax_highlight: false,
|
||||||
math: false,
|
math: false,
|
|
@ -1,4 +1,6 @@
|
||||||
pub mod date;
|
pub mod date;
|
||||||
pub mod github;
|
pub mod github;
|
||||||
pub mod markdown;
|
pub mod markdown;
|
||||||
pub mod utils;
|
pub mod metadata;
|
||||||
|
pub mod misc;
|
||||||
|
pub mod routes;
|
292
src/utils/routes/blog.rs
Normal file
292
src/utils/routes/blog.rs
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
use std::{
|
||||||
|
collections::hash_map::DefaultHasher,
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
};
|
||||||
|
|
||||||
|
use ::rss::{
|
||||||
|
extension::atom::{AtomExtension, Link},
|
||||||
|
Category, Channel, Guid, Image, Item,
|
||||||
|
};
|
||||||
|
use cached::proc_macro::once;
|
||||||
|
use chrono::{DateTime, Datelike, Local, NaiveDateTime, Utc};
|
||||||
|
use chrono_tz::Europe;
|
||||||
|
use comrak::{parse_document, Arena};
|
||||||
|
use ramhorns::Content;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
template::InfosPage,
|
||||||
|
utils::{
|
||||||
|
date::Date,
|
||||||
|
markdown::{get_options, File},
|
||||||
|
metadata::{get, FileMetadataBlog, MType},
|
||||||
|
misc::{get_url, make_kw, read_file},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const MIME_TYPE_RSS: &str = "application/rss+xml";
|
||||||
|
pub const BLOG_DIR: &str = "blog";
|
||||||
|
pub const POST_DIR: &str = "posts";
|
||||||
|
|
||||||
|
#[derive(Content, Debug)]
|
||||||
|
pub struct Post {
|
||||||
|
title: String,
|
||||||
|
pub date: Date,
|
||||||
|
pub url: String,
|
||||||
|
desc: Option<String>,
|
||||||
|
content: Option<String>,
|
||||||
|
tags: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Post {
|
||||||
|
// Fetch the file content
|
||||||
|
fn fetch_content(&mut self, data_dir: &str) {
|
||||||
|
let blog_dir = format!("{data_dir}/{BLOG_DIR}/{POST_DIR}");
|
||||||
|
let ext = ".md";
|
||||||
|
|
||||||
|
if let Some(file) = read_file(format!("{blog_dir}/{}{ext}", self.url), MType::Blog) {
|
||||||
|
self.content = Some(file.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for Post {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
if let Some(content) = &self.content {
|
||||||
|
content.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_posts(location: &str) -> Vec<Post> {
|
||||||
|
std::fs::read_dir(location)
|
||||||
|
.map_or_else(
|
||||||
|
|_| vec![],
|
||||||
|
|res| {
|
||||||
|
res.flatten()
|
||||||
|
.filter(|f| f.path().extension().map_or(false, |ext| ext == "md"))
|
||||||
|
.collect::<Vec<std::fs::DirEntry>>()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.iter()
|
||||||
|
.filter_map(|f| {
|
||||||
|
let fname = f.file_name();
|
||||||
|
let filename = fname.to_string_lossy();
|
||||||
|
let file_without_ext = filename.split_at(filename.len() - 3).0;
|
||||||
|
|
||||||
|
let file_metadata = std::fs::read_to_string(format!("{location}/{filename}"))
|
||||||
|
.map_or_else(
|
||||||
|
|_| FileMetadataBlog {
|
||||||
|
title: Some(file_without_ext.into()),
|
||||||
|
..FileMetadataBlog::default()
|
||||||
|
},
|
||||||
|
|text| {
|
||||||
|
let arena = Arena::new();
|
||||||
|
|
||||||
|
let options = get_options();
|
||||||
|
let root = parse_document(&arena, &text, &options);
|
||||||
|
let mut metadata = get(root, MType::Blog).blog.unwrap();
|
||||||
|
|
||||||
|
// Always have a title
|
||||||
|
metadata.title = metadata
|
||||||
|
.title
|
||||||
|
.map_or_else(|| Some(file_without_ext.into()), Some);
|
||||||
|
|
||||||
|
metadata
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if file_metadata.publish == Some(true) && file_metadata.draft != Some(true) {
|
||||||
|
Some(Post {
|
||||||
|
url: file_without_ext.into(),
|
||||||
|
title: file_metadata.title.unwrap(),
|
||||||
|
date: file_metadata.date.unwrap_or({
|
||||||
|
let m = f.metadata().unwrap();
|
||||||
|
let date = std::convert::Into::<DateTime<Utc>>::into(
|
||||||
|
m.modified().unwrap_or_else(|_| m.created().unwrap()),
|
||||||
|
)
|
||||||
|
.date_naive();
|
||||||
|
|
||||||
|
Date {
|
||||||
|
day: date.day(),
|
||||||
|
month: date.month(),
|
||||||
|
year: date.year(),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
desc: file_metadata.description,
|
||||||
|
content: None,
|
||||||
|
tags: file_metadata
|
||||||
|
.tags
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.map(|t| t.name.clone())
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<Post>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_post(
|
||||||
|
post: &mut Option<File>,
|
||||||
|
filename: &str,
|
||||||
|
name: &str,
|
||||||
|
data_dir: &str,
|
||||||
|
) -> (InfosPage, String) {
|
||||||
|
let blog_dir = format!("{data_dir}/{BLOG_DIR}/{POST_DIR}");
|
||||||
|
let ext = ".md";
|
||||||
|
|
||||||
|
*post = read_file(format!("{blog_dir}/{filename}{ext}"), MType::Blog);
|
||||||
|
|
||||||
|
let default = (
|
||||||
|
filename,
|
||||||
|
&format!("Blog d'{name}"),
|
||||||
|
Vec::new(),
|
||||||
|
String::new(),
|
||||||
|
);
|
||||||
|
let (title, desc, tags, toc) = match post {
|
||||||
|
Some(data) => (
|
||||||
|
match &data.metadata.info.blog.as_ref().unwrap().title {
|
||||||
|
Some(text) => text,
|
||||||
|
None => default.0,
|
||||||
|
},
|
||||||
|
match &data.metadata.info.blog.as_ref().unwrap().description {
|
||||||
|
Some(desc) => desc,
|
||||||
|
None => default.1,
|
||||||
|
},
|
||||||
|
match &data.metadata.info.blog.as_ref().unwrap().tags {
|
||||||
|
Some(tags) => tags.clone(),
|
||||||
|
None => default.2,
|
||||||
|
},
|
||||||
|
match &data.metadata.info.blog.as_ref().unwrap().toc {
|
||||||
|
// TODO: Generate TOC
|
||||||
|
Some(true) => String::new(),
|
||||||
|
_ => default.3,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
None => default,
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
InfosPage {
|
||||||
|
title: Some(format!("Post: {title}")),
|
||||||
|
desc: Some(desc.clone()),
|
||||||
|
kw: Some(make_kw(
|
||||||
|
&["blog", "blogging", "write", "writing"]
|
||||||
|
.into_iter()
|
||||||
|
.chain(tags.iter().map(|t| t.name.as_str()))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
toc,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[once(time = 10800)] // 3h
|
||||||
|
pub fn build_rss(config: Config) -> String {
|
||||||
|
let mut posts = get_posts(&format!(
|
||||||
|
"{}/{}/{}",
|
||||||
|
config.locations.data_dir, BLOG_DIR, POST_DIR
|
||||||
|
));
|
||||||
|
|
||||||
|
// Sort from newest to oldest
|
||||||
|
posts.sort_by_cached_key(|p| (p.date.year, p.date.month, p.date.day));
|
||||||
|
posts.reverse();
|
||||||
|
|
||||||
|
// Only the 20 newest
|
||||||
|
let max = 20;
|
||||||
|
if posts.len() > max {
|
||||||
|
posts.drain(max..);
|
||||||
|
}
|
||||||
|
|
||||||
|
let link_to_site = get_url(config.fc.clone());
|
||||||
|
let author = if let (Some(mail), Some(name)) = (config.fc.mail, config.fc.fullname.clone()) {
|
||||||
|
Some(format!("{mail} ({name})"))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let title = format!("Blog d'{}", config.fc.name.unwrap_or_default());
|
||||||
|
let lang = "fr";
|
||||||
|
let channel = Channel {
|
||||||
|
title: title.clone(),
|
||||||
|
link: link_to_site.clone(),
|
||||||
|
description: "Un fil qui parle d'informatique notamment".into(),
|
||||||
|
language: Some(lang.into()),
|
||||||
|
managing_editor: author.clone(),
|
||||||
|
webmaster: author,
|
||||||
|
pub_date: Some(Local::now().to_rfc2822()),
|
||||||
|
categories: ["blog", "blogging", "write", "writing"]
|
||||||
|
.iter()
|
||||||
|
.map(|&c| Category {
|
||||||
|
name: c.into(),
|
||||||
|
..Category::default()
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
generator: Some("ewp with rss crate".into()),
|
||||||
|
docs: Some("https://www.rssboard.org/rss-specification".into()),
|
||||||
|
image: Some(Image {
|
||||||
|
url: format!("{link_to_site}/icons/favicon-32x32.png"),
|
||||||
|
title: title.clone(),
|
||||||
|
link: link_to_site.clone(),
|
||||||
|
..Image::default()
|
||||||
|
}),
|
||||||
|
items: posts
|
||||||
|
.iter_mut()
|
||||||
|
.map(|p| {
|
||||||
|
// Get post data
|
||||||
|
p.fetch_content(&config.locations.data_dir);
|
||||||
|
|
||||||
|
// Build item
|
||||||
|
Item {
|
||||||
|
title: Some(p.title.clone()),
|
||||||
|
link: Some(format!("{}/blog/p/{}", link_to_site, p.url)),
|
||||||
|
description: p.content.clone(),
|
||||||
|
categories: p
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.map(|c| Category {
|
||||||
|
name: c.to_owned(),
|
||||||
|
..Category::default()
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
guid: Some(Guid {
|
||||||
|
value: format!("urn:hash:{}", {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
p.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}),
|
||||||
|
permalink: false,
|
||||||
|
}),
|
||||||
|
pub_date: Some(
|
||||||
|
NaiveDateTime::parse_from_str(
|
||||||
|
&format!("{}-{}-{} 13:12:00", p.date.day, p.date.month, p.date.year),
|
||||||
|
"%d-%m-%Y %H:%M:%S",
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.and_local_timezone(Europe::Paris)
|
||||||
|
.unwrap()
|
||||||
|
.to_rfc2822(),
|
||||||
|
),
|
||||||
|
..Item::default()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
atom_ext: Some(AtomExtension {
|
||||||
|
links: vec![Link {
|
||||||
|
href: format!("{link_to_site}/blog/rss"),
|
||||||
|
rel: "self".into(),
|
||||||
|
hreflang: Some(lang.into()),
|
||||||
|
mime_type: Some(MIME_TYPE_RSS.into()),
|
||||||
|
title: Some(title),
|
||||||
|
length: None,
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
..Channel::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
std::str::from_utf8(&channel.write_to(Vec::new()).unwrap())
|
||||||
|
.unwrap()
|
||||||
|
.into()
|
||||||
|
}
|
67
src/utils/routes/contact.rs
Normal file
67
src/utils/routes/contact.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
use cached::proc_macro::once;
|
||||||
|
use glob::glob;
|
||||||
|
use std::fs::read_to_string;
|
||||||
|
|
||||||
|
use crate::utils::{markdown::File, metadata::MType, misc::read_file};
|
||||||
|
|
||||||
|
/// Contact node
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Link {
|
||||||
|
pub service: String,
|
||||||
|
pub scope: Option<String>,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[once(time = 60)]
|
||||||
|
pub fn find_links(directory: String) -> Vec<Link> {
|
||||||
|
// TOML filename
|
||||||
|
let toml_file = "links.toml";
|
||||||
|
|
||||||
|
// Read the TOML file and parse it
|
||||||
|
let toml_str = read_to_string(format!("{directory}/{toml_file}")).unwrap_or_default();
|
||||||
|
|
||||||
|
let mut redirections = vec![];
|
||||||
|
match toml::de::from_str::<toml::Value>(&toml_str) {
|
||||||
|
Ok(data) => {
|
||||||
|
if let Some(section) = data.as_table() {
|
||||||
|
section.iter().for_each(|(key, value)| {
|
||||||
|
// Scopes are delimited with `/`
|
||||||
|
let (service, scope) = match key.split_once('/') {
|
||||||
|
Some((service, scope)) => (service.to_owned(), Some(scope.to_owned())),
|
||||||
|
None => (key.to_owned(), None),
|
||||||
|
};
|
||||||
|
|
||||||
|
redirections.push(Link {
|
||||||
|
service,
|
||||||
|
scope,
|
||||||
|
url: value.as_str().unwrap().to_owned(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => return vec![],
|
||||||
|
}
|
||||||
|
|
||||||
|
redirections
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_paragraphs(list: &mut [File]) {
|
||||||
|
list.iter_mut()
|
||||||
|
.for_each(|file| file.content = file.content.replace("<p>", "").replace("</p>", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(path: &str) -> Vec<File> {
|
||||||
|
glob(path)
|
||||||
|
.unwrap()
|
||||||
|
.map(|e| read_file(e.unwrap().to_string_lossy().to_string(), MType::Contact).unwrap())
|
||||||
|
.filter(|f| {
|
||||||
|
!f.metadata
|
||||||
|
.info
|
||||||
|
.contact
|
||||||
|
.clone()
|
||||||
|
.unwrap()
|
||||||
|
.hide
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
.collect::<Vec<File>>()
|
||||||
|
}
|
90
src/utils/routes/contrib.rs
Normal file
90
src/utils/routes/contrib.rs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use ramhorns::Content;
|
||||||
|
use reqwest::Error;
|
||||||
|
|
||||||
|
use crate::utils::github::{fetch_pr, ProjectState};
|
||||||
|
|
||||||
|
#[derive(Clone, Content, Debug)]
|
||||||
|
pub struct Project {
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
pub pulls_merged: Vec<Pull>,
|
||||||
|
pub pulls_open: Vec<Pull>,
|
||||||
|
pub pulls_closed: Vec<Pull>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Content, Debug)]
|
||||||
|
pub struct Pull {
|
||||||
|
url: String,
|
||||||
|
id: u32,
|
||||||
|
name_repo: String,
|
||||||
|
title: String,
|
||||||
|
state: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch() -> Result<Vec<Project>, Error> {
|
||||||
|
match fetch_pr().await {
|
||||||
|
Ok(projects) => {
|
||||||
|
let mut data: Vec<Project> = Vec::new();
|
||||||
|
|
||||||
|
// Grouping PRs by projects
|
||||||
|
let mut map: HashMap<&str, Vec<Pull>> = HashMap::new();
|
||||||
|
for p in &projects {
|
||||||
|
let project = Pull {
|
||||||
|
url: p.contrib_url.clone(),
|
||||||
|
id: p.id,
|
||||||
|
name_repo: p.name.clone(),
|
||||||
|
title: p.title.clone(),
|
||||||
|
state: p.status as u8,
|
||||||
|
};
|
||||||
|
let project_name = p.name.as_str();
|
||||||
|
if map.contains_key(project_name) {
|
||||||
|
map.entry(project_name).and_modify(|v| v.push(project));
|
||||||
|
} else {
|
||||||
|
data.push(Project {
|
||||||
|
name: project_name.into(),
|
||||||
|
url: p.url.clone(),
|
||||||
|
pulls_merged: Vec::new(),
|
||||||
|
pulls_closed: Vec::new(),
|
||||||
|
pulls_open: Vec::new(),
|
||||||
|
});
|
||||||
|
map.insert(project_name, vec![project]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distributes each PR in the right vector
|
||||||
|
for d in &mut data {
|
||||||
|
map.get(d.name.as_str()).unwrap().iter().for_each(|p| {
|
||||||
|
let state = p.state.try_into().unwrap();
|
||||||
|
match state {
|
||||||
|
ProjectState::Closed => d.pulls_closed.push(p.to_owned()),
|
||||||
|
ProjectState::Merged => d.pulls_merged.push(p.to_owned()),
|
||||||
|
ProjectState::Open => d.pulls_open.push(p.to_owned()),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut name: Vec<char> = d.name.replace('-', " ").chars().collect();
|
||||||
|
name[0] = name[0].to_uppercase().next().unwrap();
|
||||||
|
d.name = name.into_iter().collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ascending order by pulls IDs
|
||||||
|
for d in &mut data {
|
||||||
|
d.pulls_closed.reverse();
|
||||||
|
d.pulls_merged.reverse();
|
||||||
|
d.pulls_open.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ascending order by number of pulls
|
||||||
|
data.sort_by(|a, b| {
|
||||||
|
b.pulls_merged
|
||||||
|
.len()
|
||||||
|
.partial_cmp(&a.pulls_merged.len())
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
106
src/utils/routes/cours.rs
Normal file
106
src/utils/routes/cours.rs
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
use std::{cmp::Ordering, path::Path};
|
||||||
|
|
||||||
|
use cached::proc_macro::once;
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct FileNode {
|
||||||
|
name: String,
|
||||||
|
is_dir: bool,
|
||||||
|
children: Vec<FileNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for FileNode {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
match (self.is_dir, other.is_dir) {
|
||||||
|
// If both are directories or both are files, compare names
|
||||||
|
(true, true) | (false, false) => self.name.cmp(&other.name),
|
||||||
|
// If self is directory and other is file, self comes first
|
||||||
|
(true, false) => Ordering::Less,
|
||||||
|
// If self is file and other is directory, other comes first
|
||||||
|
(false, true) => Ordering::Greater,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for FileNode {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[once(time = 120)]
|
||||||
|
pub fn get_filetree(
|
||||||
|
initial_dir: &str,
|
||||||
|
exclusion_list: &[String],
|
||||||
|
exclusion_patterns: &[Regex],
|
||||||
|
) -> FileNode {
|
||||||
|
gen_filetree(initial_dir, exclusion_list, exclusion_patterns)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gen_filetree(
|
||||||
|
dir_path: &str,
|
||||||
|
exclusion_list: &[String],
|
||||||
|
exclusion_patterns: &[Regex],
|
||||||
|
) -> FileNode {
|
||||||
|
let mut children: Vec<FileNode> = std::fs::read_dir(dir_path)
|
||||||
|
.unwrap()
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter_map(|entry| {
|
||||||
|
let entry_path = entry.path();
|
||||||
|
let entry_name = entry_path.file_name()?.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
// Exclusion checks
|
||||||
|
if excluded(&entry_name, exclusion_list, exclusion_patterns) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry_path.is_file() {
|
||||||
|
Some(FileNode {
|
||||||
|
name: entry_name,
|
||||||
|
is_dir: false,
|
||||||
|
children: vec![],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Exclude empty directories
|
||||||
|
let children_of_children = gen_filetree(
|
||||||
|
entry_path.to_str().unwrap(),
|
||||||
|
exclusion_list,
|
||||||
|
exclusion_patterns,
|
||||||
|
);
|
||||||
|
if children_of_children.is_dir && children_of_children.children.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(children_of_children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
children.sort();
|
||||||
|
|
||||||
|
FileNode {
|
||||||
|
name: Path::new(dir_path)
|
||||||
|
.file_name()
|
||||||
|
.unwrap()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
is_dir: true,
|
||||||
|
children,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn excluded(element: &str, exclusion_list: &[String], exclusion_patterns: &[Regex]) -> bool {
|
||||||
|
if exclusion_list
|
||||||
|
.iter()
|
||||||
|
.any(|excluded_term| element.contains(excluded_term))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if exclusion_patterns.iter().any(|re| re.is_match(element)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
4
src/utils/routes/mod.rs
Normal file
4
src/utils/routes/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod blog;
|
||||||
|
pub mod contact;
|
||||||
|
pub mod contrib;
|
||||||
|
pub mod cours;
|
|
@ -1,21 +1,13 @@
|
||||||
|
@import "../markdown.css";
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
:root {
|
:root {
|
||||||
--code-font-color: #333333;
|
|
||||||
--code-bg-color: #eeeeee;
|
|
||||||
--quote-border-color: #9852fa;
|
|
||||||
--quote-bg-color: #d8d6d6;
|
|
||||||
--separator-color: #cccccc;
|
|
||||||
--tag-bg-color: #d2e0f0;
|
--tag-bg-color: #d2e0f0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--code-font-color: #eeeeee;
|
|
||||||
--code-bg-color: #333333;
|
|
||||||
--quote-border-color: #bd93f9;
|
|
||||||
--quote-bg-color: #273341;
|
|
||||||
--separator-color: #414558;
|
|
||||||
--tag-bg-color: #242e38;
|
--tag-bg-color: #242e38;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,11 +16,6 @@
|
||||||
--max-width: 750px;
|
--max-width: 750px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Page */
|
|
||||||
html {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
max-width: var(--max-width);
|
max-width: var(--max-width);
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
@ -70,49 +57,8 @@ main {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Anchors */
|
|
||||||
:is(h1, h2, h3, h4, h5, h6):hover a.anchor::before {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.anchor::before {
|
|
||||||
content: "#";
|
|
||||||
visibility: hidden;
|
|
||||||
padding-right: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.anchor {
|
|
||||||
text-decoration: none;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Links in headers */
|
|
||||||
:is(h1, h2, h3, h4, h5, h6) a {
|
|
||||||
font-size: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Separators */
|
|
||||||
hr {
|
|
||||||
border: 0;
|
|
||||||
height: 1px;
|
|
||||||
background: var(--separator-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Quotes */
|
|
||||||
blockquote {
|
|
||||||
margin: 1em 0;
|
|
||||||
padding: 0.1em 10px;
|
|
||||||
border-left: 6px solid;
|
|
||||||
border-color: var(--quote-border-color);
|
|
||||||
background: var(--quote-bg-color);
|
|
||||||
border-top-right-radius: 5px;
|
|
||||||
border-bottom-right-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Images */
|
/* Images */
|
||||||
img {
|
img {
|
||||||
display: block;
|
|
||||||
margin: auto;
|
|
||||||
max-width: var(--max-width);
|
max-width: var(--max-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,115 +68,6 @@ code {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Little snippet of code (not blocks) */
|
|
||||||
kbd,
|
|
||||||
code:not(.hljs):not(:has(svg)) {
|
|
||||||
background: var(--code-bg-color);
|
|
||||||
border-radius: 3px;
|
|
||||||
color: var(--code-font-color);
|
|
||||||
box-shadow: 0 1px 1px black;
|
|
||||||
font-size: calc(var(--font-size) * 0.8);
|
|
||||||
padding: 2px 4px;
|
|
||||||
vertical-align: 1.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code blocks */
|
|
||||||
.hljs {
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs::-webkit-scrollbar {
|
|
||||||
width: 7px;
|
|
||||||
height: 9px;
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs::-webkit-scrollbar-thumb {
|
|
||||||
background-color: var(--font-color);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Marge for numbers */
|
|
||||||
.hljs-ln-n {
|
|
||||||
margin-right: 0.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Numbers in codeblocks */
|
|
||||||
.hljs-ln-numbers {
|
|
||||||
text-align: right;
|
|
||||||
color: var(--font-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix scroll in codeblocks with line numbering */
|
|
||||||
table.hljs-ln {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Background for copy code button */
|
|
||||||
.hljs-copy-button {
|
|
||||||
background-color: var(--background) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Light theme for the copy code button */
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
.hljs-copy-button {
|
|
||||||
background-color: var(--font-color) !important;
|
|
||||||
filter: invert(100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide last line in codeblocks if empty */
|
|
||||||
.hljs-ln
|
|
||||||
> tbody
|
|
||||||
> tr:last-child:has(td:last-child > span::-moz-only-whitespace) {
|
|
||||||
visibility: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Temporary fix for layout.css.has-selector.enabled available only on
|
|
||||||
* Firefox under certain circumstances */
|
|
||||||
.hljs-ln > tbody > tr:last-child {
|
|
||||||
visibility: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reference to footnotes */
|
|
||||||
.footnote-ref a {
|
|
||||||
text-decoration: underline dotted;
|
|
||||||
font-size: calc(var(--font-size) * 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footnote */
|
|
||||||
section.footnotes * {
|
|
||||||
font-size: calc(var(--font-size) * 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* When multiple ref */
|
|
||||||
a.footnote-backref sup {
|
|
||||||
font-size: calc(var(--font-size) * 0.6);
|
|
||||||
}
|
|
||||||
a.footnote-backref sup::before {
|
|
||||||
content: "(";
|
|
||||||
}
|
|
||||||
a.footnote-backref sup::after {
|
|
||||||
content: ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footnotes links */
|
|
||||||
a.footnote-backref {
|
|
||||||
font-family: "Segoe UI", "Segoe UI Symbol", system-ui;
|
|
||||||
text-decoration: underline dotted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footnotes block separation from article */
|
|
||||||
section.footnotes {
|
|
||||||
margin: 3px;
|
|
||||||
border-top: 2px dotted var(--separator-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mermaid diagrams */
|
|
||||||
pre:has(code.language-mermaid) {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table of content */
|
/* Table of content */
|
||||||
nav#toc {
|
nav#toc {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -246,36 +83,3 @@ nav#toc {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media print {
|
|
||||||
/* Better colors for paper */
|
|
||||||
blockquote {
|
|
||||||
border-color: black;
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs {
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force line numbering to be on top */
|
|
||||||
td.hljs-ln-line {
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Break code */
|
|
||||||
code.hljs {
|
|
||||||
white-space: break-spaces;
|
|
||||||
hyphens: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide arrows of backref */
|
|
||||||
a.footnote-backref {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* No underline for footnotes */
|
|
||||||
.footnote-ref > a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +1,13 @@
|
||||||
|
@import "markdown.css";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--max-width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: var(--max-width);
|
||||||
|
}
|
||||||
|
|
||||||
/* Filetree */
|
/* Filetree */
|
||||||
aside {
|
aside {
|
||||||
float: left;
|
float: left;
|
||||||
|
@ -42,12 +52,10 @@ aside li.directory {
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
aside {
|
aside {
|
||||||
visibility: hidden;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main img {
|
main img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
display: block;
|
|
||||||
margin: auto;
|
|
||||||
}
|
}
|
||||||
|
|
248
static/css/markdown.css
Normal file
248
static/css/markdown.css
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--code-font-color: #333333;
|
||||||
|
--code-bg-color: #eeeeee;
|
||||||
|
--quote-border-color: #9852fa;
|
||||||
|
--quote-bg-color: #d8d6d6;
|
||||||
|
--separator-color: #cccccc;
|
||||||
|
--tag-bg-color: #d2e0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--code-font-color: #eeeeee;
|
||||||
|
--code-bg-color: #333333;
|
||||||
|
--quote-border-color: #bd93f9;
|
||||||
|
--quote-bg-color: #273341;
|
||||||
|
--separator-color: #414558;
|
||||||
|
--tag-bg-color: #242e38;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page */
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Anchors */
|
||||||
|
main :is(h1, h2, h3, h4, h5, h6):hover a.anchor::before {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
main a.anchor::before {
|
||||||
|
content: "#";
|
||||||
|
visibility: hidden;
|
||||||
|
padding-right: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
main a.anchor {
|
||||||
|
text-decoration: none;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links in headers */
|
||||||
|
:is(h1, h2, h3, h4, h5, h6) a {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Images */
|
||||||
|
main img {
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Separators */
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--separator-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quotes */
|
||||||
|
blockquote {
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 0.1em 10px;
|
||||||
|
border-left: 6px solid;
|
||||||
|
border-color: var(--quote-border-color);
|
||||||
|
background: var(--quote-bg-color);
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Little snippet of code (not blocks) */
|
||||||
|
kbd,
|
||||||
|
code:not(.hljs):not(:has(svg)) {
|
||||||
|
background: var(--code-bg-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
color: var(--code-font-color);
|
||||||
|
box-shadow: 0 1px 1px black;
|
||||||
|
font-size: calc(var(--font-size) * 0.8);
|
||||||
|
padding: 2px 4px;
|
||||||
|
vertical-align: 1.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
.hljs {
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs::-webkit-scrollbar {
|
||||||
|
width: 7px;
|
||||||
|
height: 9px;
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--font-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Marge for numbers */
|
||||||
|
.hljs-ln-n {
|
||||||
|
margin-right: 0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Numbers in codeblocks */
|
||||||
|
.hljs-ln-numbers {
|
||||||
|
text-align: right;
|
||||||
|
color: var(--font-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix scroll in codeblocks with line numbering */
|
||||||
|
table.hljs-ln {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Background for copy code button */
|
||||||
|
.hljs-copy-button {
|
||||||
|
background-color: var(--background) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light theme for the copy code button */
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
.hljs-copy-button {
|
||||||
|
background-color: var(--font-color) !important;
|
||||||
|
filter: invert(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide last line in codeblocks if empty */
|
||||||
|
.hljs-ln
|
||||||
|
> tbody
|
||||||
|
> tr:last-child:has(td:last-child > span::-moz-only-whitespace) {
|
||||||
|
visibility: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Temporary fix for layout.css.has-selector.enabled available only on
|
||||||
|
* Firefox under certain circumstances */
|
||||||
|
.hljs-ln > tbody > tr:last-child {
|
||||||
|
visibility: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reference to footnotes */
|
||||||
|
.footnote-ref a {
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
font-size: calc(var(--font-size) * 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footnote */
|
||||||
|
section.footnotes * {
|
||||||
|
font-size: calc(var(--font-size) * 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When multiple ref */
|
||||||
|
a.footnote-backref sup {
|
||||||
|
font-size: calc(var(--font-size) * 0.6);
|
||||||
|
}
|
||||||
|
a.footnote-backref sup::before {
|
||||||
|
content: "(";
|
||||||
|
}
|
||||||
|
a.footnote-backref sup::after {
|
||||||
|
content: ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footnotes links */
|
||||||
|
a.footnote-backref {
|
||||||
|
font-family: "Segoe UI", "Segoe UI Symbol", system-ui;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footnotes block separation from content */
|
||||||
|
section.footnotes {
|
||||||
|
margin: 3px;
|
||||||
|
border-top: 2px dotted var(--separator-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mermaid diagrams */
|
||||||
|
pre:has(code.language-mermaid) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables */
|
||||||
|
table:not(.hljs-ln) {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table:not(.hljs-ln) th,
|
||||||
|
table:not(.hljs-ln) td {
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid var(--separator-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
table:not(.hljs-ln)th {
|
||||||
|
border-bottom: 2px solid var(--separator-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No borders on the outer edges of the table */
|
||||||
|
table:not(.hljs-ln) tr:last-child td {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table:not(.hljs-ln) tr:first-child th {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table:not(.hljs-ln) tr td:first-child,
|
||||||
|
table:not(.hljs-ln) tr th:first-child {
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table:not(.hljs-ln) tr td:last-child,
|
||||||
|
table:not(.hljs-ln) tr th:last-child {
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
/* Better colors for paper */
|
||||||
|
blockquote {
|
||||||
|
border-color: black;
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force line numbering to be on top */
|
||||||
|
td.hljs-ln-line {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Break code */
|
||||||
|
code.hljs {
|
||||||
|
white-space: break-spaces;
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide arrows of backref */
|
||||||
|
a.footnote-backref {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No underline for footnotes */
|
||||||
|
.footnote-ref > a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -80,11 +80,16 @@ const deepestNodeOpened = (path, options) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const svgDarkTheme = () => {
|
const Mode = {
|
||||||
|
Light: 1,
|
||||||
|
Dark: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const svgChangeTheme = (mode) => {
|
||||||
for (const item of document.getElementsByTagName("img")) {
|
for (const item of document.getElementsByTagName("img")) {
|
||||||
if (!item.src.startsWith("data:image/svg+xml;base64,")) {
|
if (!item.src.startsWith("data:image/svg+xml;base64,")) {
|
||||||
// Exclude image who aren't SVG and base64 encoded
|
// Exclude image who aren't SVG and base64 encoded
|
||||||
break;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convert to grayscale */
|
/** Convert to grayscale */
|
||||||
|
@ -129,9 +134,19 @@ const svgDarkTheme = () => {
|
||||||
const totalGrayscale = grayscaleValues.reduce((acc, val) => acc + val, 0);
|
const totalGrayscale = grayscaleValues.reduce((acc, val) => acc + val, 0);
|
||||||
const averageGrayscale = totalGrayscale / grayscaleValues.length;
|
const averageGrayscale = totalGrayscale / grayscaleValues.length;
|
||||||
|
|
||||||
if (averageGrayscale < 128) {
|
const treshold = 128;
|
||||||
|
|
||||||
|
if (averageGrayscale < treshold && mode === Mode.Dark) {
|
||||||
item.style = "filter: invert(1);";
|
item.style = "filter: invert(1);";
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (averageGrayscale > treshold && mode === Mode.Light) {
|
||||||
|
item.style = "filter: invert(1);";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.style = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -160,8 +175,16 @@ window.addEventListener("load", () => {
|
||||||
uncollapse(last_openeded);
|
uncollapse(last_openeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix SVG images in dark mode
|
// Fix SVG images
|
||||||
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
svgChangeTheme(
|
||||||
svgDarkTheme();
|
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
}
|
? Mode.Dark
|
||||||
|
: Mode.Light
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window
|
||||||
|
.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
.addEventListener("change", (event) =>
|
||||||
|
svgChangeTheme(event.matches ? Mode.Dark : Mode.Light)
|
||||||
|
);
|
||||||
|
|
|
@ -18,6 +18,7 @@ window.addEventListener("load", () => {
|
||||||
new Tag("Comment fonctionne un PC 😵💫"),
|
new Tag("Comment fonctionne un PC 😵💫"),
|
||||||
new Tag("undefined", mono),
|
new Tag("undefined", mono),
|
||||||
new Tag("/api/v1/love", mono),
|
new Tag("/api/v1/love", mono),
|
||||||
|
new Tag("/api/v1/websites", mono),
|
||||||
new Tag("Peak D2 sur Valo 🤡"),
|
new Tag("Peak D2 sur Valo 🤡"),
|
||||||
new Tag(
|
new Tag(
|
||||||
"0x520",
|
"0x520",
|
||||||
|
@ -47,7 +48,7 @@ window.addEventListener("load", () => {
|
||||||
`
|
`
|
||||||
),
|
),
|
||||||
new Tag("School hater"),
|
new Tag("School hater"),
|
||||||
new Tag("Stagiaire"),
|
new Tag("Étudiant"),
|
||||||
new Tag("Rempli de malice"),
|
new Tag("Rempli de malice"),
|
||||||
new Tag(
|
new Tag(
|
||||||
"#NouveauFrontPopulaire ✊",
|
"#NouveauFrontPopulaire ✊",
|
||||||
|
|
|
@ -2,15 +2,19 @@ window.addEventListener("load", () => {
|
||||||
const macros = {};
|
const macros = {};
|
||||||
for (const item of new Map(
|
for (const item of new Map(
|
||||||
Object.entries({
|
Object.entries({
|
||||||
|
B: "mathbb{B}",
|
||||||
N: "mathbb{N}",
|
N: "mathbb{N}",
|
||||||
R: "mathbb{R}",
|
R: "mathbb{R}",
|
||||||
Z: "mathbb{Z}",
|
Z: "mathbb{Z}",
|
||||||
O: "Theta",
|
O: "Theta",
|
||||||
|
Tau: "mathrm{T}",
|
||||||
|
u: "mu",
|
||||||
ra: "rightarrow",
|
ra: "rightarrow",
|
||||||
la: "leftarrow",
|
la: "leftarrow",
|
||||||
RA: "Rightarrow",
|
RA: "Rightarrow",
|
||||||
LA: "Leftarrow",
|
LA: "Leftarrow",
|
||||||
u: "mu",
|
lb: "llbracket",
|
||||||
|
rb: "rrbracket",
|
||||||
})
|
})
|
||||||
)[Symbol.iterator]()) {
|
)[Symbol.iterator]()) {
|
||||||
const bs = "\\";
|
const bs = "\\";
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</aside>
|
</aside>
|
||||||
<main>
|
<main>
|
||||||
{{^content}}
|
{{^content}}
|
||||||
<p>Fichier introuvable</p>
|
<p>Fichier introuvable ou invalide.</p>
|
||||||
{{/content}} {{#content}}
|
{{/content}} {{#content}}
|
||||||
<article>{{&content}}</article>
|
<article>{{&content}}</article>
|
||||||
</main>
|
</main>
|
||||||
|
|
Loading…
Reference in a new issue