Compare commits

..

20 commits

Author SHA1 Message Date
ecbbe85844
closest i can get with stupid regex, currently not working since the regex is eating a character after a match, also its not a proprer parsing so any dollar inside a formula will probably break the whole thing
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-12-10 23:23:27 +01:00
2cb1e664fe
add mermaid and katex support on cours pages
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-12-10 19:26:29 +01:00
5520952d07
add mu 2023-12-10 19:26:20 +01:00
6eef32b6d9
follow scroll
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-12-10 19:19:48 +01:00
7fc3f95dd5
fix minor issue in navbar
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-12-10 19:11:09 +01:00
61015a2536
print!
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-11-02 19:35:08 +01:00
2a44f1240f
WIP: Exclusion support
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-11-01 17:03:03 +01:00
a738d492d2
uncollapse when necessary
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-11-01 16:54:29 +01:00
fe9a0c750b
comments
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-11-01 15:40:25 +01:00
fcc146842c
clickable filetree
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-11-01 13:28:19 +01:00
23079f4418
remove fonts
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-11-01 13:22:14 +01:00
a9f48a79a4
keep location
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-11-01 13:12:33 +01:00
b3cdcff067
span
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-11-01 13:02:35 +01:00
1c19d23f36
hihi
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-11-01 13:00:48 +01:00
bbd86393eb
filetree building
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-11-01 12:59:40 +01:00
9bf1bc807c
sort derivation
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-11-01 12:43:07 +01:00
d025981f0e
filetree preparation
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-11-01 03:36:03 +01:00
fc44816a04
work?
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-10-31 22:46:05 +01:00
4ccd2c8709
yes, ill squash dw
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-10-31 21:02:15 +01:00
21cb50e5fd
cours page
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-10-31 20:55:06 +01:00
70 changed files with 1223 additions and 3252 deletions

View file

@ -1,47 +0,0 @@
name: Publish latest version
on:
workflow_dispatch:
jobs:
build:
container:
image: ghcr.io/catthehacker/ubuntu:act-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Checkout LFS
run: |
# Replace double auth header, see https://github.com/actions/checkout/issues/1830
AUTH=$(git config --local http.${{ github.server_url }}/.extraheader)
git config --local --unset http.${{ github.server_url }}/.extraheader
git config --local http.${{ github.server_url }}/${{ github.repository }}.git/info/lfs/objects/batch.extraheader "$AUTH"
# Get files
git lfs fetch
git lfs checkout
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Sanitize metadata
id: meta
uses: docker/metadata-action@v5
with:
tags: latest
images: git.mylloon.fr/${{ github.repository }}
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ${{ github.server_url }}
username: ${{ github.actor }}
password: ${{ secrets.TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}

14
.woodpecker/publish.yml Normal file
View file

@ -0,0 +1,14 @@
pipeline:
publish:
image: woodpeckerci/plugin-docker-buildx:2
settings:
platforms: linux/amd64
repo: git.mylloon.fr/${CI_REPO,,}
auto_tag: true
registry: git.mylloon.fr
username: ${CI_REPO_OWNER}
password:
from_secret: cb_token
when:
event: push
branch: main

1891
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -10,28 +10,21 @@ publish = false
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
[dependencies] [dependencies]
actix-web = { version = "4.9", default-features = false, features = ["macros", "compress-brotli"] } actix-web = { version = "4.4", 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.46", features = ["async"] }
ramhorns = "1.0" ramhorns = "0.14"
toml = "0.8" toml = "0.8"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_yml = "0.0" serde_yaml = "0.9"
serde_json = "1.0" serde_json = "1.0"
minify-html = "0.15" minify-html = "0.11"
minify-js = "0.6" minify-js = "0.5"
glob = "0.3" glob = "0.3"
comrak = "0.28" comrak = "0.19"
reqwest = { version = "0.12", features = ["json"] } reqwest = { version = "0.11", features = ["json"] }
chrono = { version = "0.4.38", default-features = false, features = ["clock"]} chrono = { version = "0.4.30", default-features = false, features = ["clock"]}
chrono-tz = "0.10" chrono-tz = "0.8"
rss = { version = "2.0", features = ["atom"] } rss = { version = "2.0", features = ["atom"] }
lol_html = "1.2" lol_html = "1.2"
base64 = "0.22"
mime_guess = "2.0"
urlencoding = "2.1"
regex = "1.10" regex = "1.10"
cyborgtime = "2.1.1"
[lints.clippy]
pedantic = "warn"

View file

@ -18,7 +18,6 @@ WORKDIR /app
COPY --from=builder /usr/local/cargo/bin/ewp /app/ewp COPY --from=builder /usr/local/cargo/bin/ewp /app/ewp
COPY --from=builder /usr/src/ewp/LICENSE /app/LICENSE COPY --from=builder /usr/src/ewp/LICENSE /app/LICENSE
COPY --from=builder /usr/src/ewp/README.md /app/README.md
COPY --from=builder /usr/src/ewp/static /app/static COPY --from=builder /usr/src/ewp/static /app/static
COPY --from=builder /usr/src/ewp/templates /app/templates COPY --from=builder /usr/src/ewp/templates /app/templates

View file

@ -10,7 +10,6 @@
- [Global configuration](#global-configuration) - [Global configuration](#global-configuration)
- [Link shortener for contacts](#link-shortener-for-contacts) - [Link shortener for contacts](#link-shortener-for-contacts)
- [Add content](#add-content) - [Add content](#add-content)
- [Index](#index)
- [Blog](#blog) - [Blog](#blog)
- [Projects](#projects) - [Projects](#projects)
- [Contacts](#contacts) - [Contacts](#contacts)
@ -106,7 +105,6 @@ onion = "http://youraddress.onion/"
app_name = "Nickname" # fallback to 'EWP' if none app_name = "Nickname" # fallback to 'EWP' if none
name = "Firstname" name = "Firstname"
fullname = "Fullname" fullname = "Fullname"
exclude_courses = []
``` ```
## Link shortener for contacts ## Link shortener for contacts
@ -141,29 +139,9 @@ option: value
Markdown file Markdown file
``` ```
## Index
Markdown file is stored in `/app/data/index.md`
```
---
name: Option<String>
pronouns: Option<String>
avatar: Option<String>
avatar_caption: Option<String>
avatar_style: Option<String>
---
Index content
```
- If no `name`, the `fullname` used in the configuration will be used
- `avatar` is the link of the avatar
- `avatar_style` is either `round` (default) or `square`
## Blog ## Blog
Markdown files are stored in `/app/data/blog/posts/` Markdown files are stored in `/app/data/blog/`
``` ```
--- ---
@ -180,15 +158,11 @@ 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, see #30
### About <!-- omit in toc -->
The file is stored at `/app/data/blog/about.md`.
## Projects ## Projects
Markdown files are stored in `/app/data/projects/apps/` Markdown files are stored in `/app/data/projects/`
``` ```
--- ---
@ -203,14 +177,7 @@ Project description
- If no `link` : the div won't be clickable and will be reported as is to the user - If no `link` : the div won't be clickable and will be reported as is to the user
(no corner-arrow) (no corner-arrow)
- Note that only a handful of [`language`s are supported](./static/css/languages.css) - Note that only a handful of [`language`s are supported](./static/css/languages.css).
You can also put apps in an "Archived" category, in this case, store markdown
files in `archive` subdirectory of `apps`.
### About <!-- omit in toc -->
The file is stored at `/app/data/projects/about.md`.
## Contacts ## Contacts
@ -239,18 +206,6 @@ Custom project description
- `description` will be rendered as HTML "title" (text will appear when cursor - `description` will be rendered as HTML "title" (text will appear when cursor
is hover the link) is hover the link)
Also, contacts are categorized, here is the list of the available categories:
- `socials`
- `forges`
- `others`
For example, `socials` contact files are stored in `/app/data/contacts/socials/`.
### About <!-- omit in toc -->
The file is stored at `/app/data/contacts/about.md`.
## Courses ## Courses
Markdown files are stored in `/app/data/cours/` Markdown files are stored in `/app/data/cours/`

View file

@ -629,7 +629,7 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found. the "copyright" line and a pointer to where the full notice is found.
Copyright (C) 2023-2024 Mylloon Copyright (C) 2023 Mylloon
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published it under the terms of the GNU Affero General Public License as published

View file

@ -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)](https://git.mylloon.fr/Anri/mylloon.fr/actions?workflow=publish.yml) [![status-badge](https://ci.mylloon.fr/api/badges/Anri/mylloon.fr/status.svg)](https://ci.mylloon.fr/Anri/mylloon.fr)
- 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](./Documentation.md)

View file

@ -8,7 +8,7 @@ use crate::template::Template;
/// Store the configuration of config/config.toml /// Store the configuration of config/config.toml
#[derive(Clone, Debug, Default, Deserialize)] #[derive(Clone, Debug, Default, Deserialize)]
pub struct FileConfiguration { pub struct FileConfig {
/// http/https /// http/https
pub scheme: Option<String>, pub scheme: Option<String>,
/// Domain name "sub.domain.tld" /// Domain name "sub.domain.tld"
@ -27,11 +27,9 @@ pub struct FileConfiguration {
pub name: Option<String>, pub name: Option<String>,
/// Fullname of website owner /// Fullname of website owner
pub fullname: Option<String>, pub fullname: Option<String>,
/// List exclusion for courses
pub exclude_courses: Option<Vec<String>>,
} }
impl FileConfiguration { impl FileConfig {
/// Initialize with default values /// Initialize with default values
fn new() -> Self { fn new() -> Self {
Self { Self {
@ -39,17 +37,15 @@ impl FileConfiguration {
domain: Some("localhost".into()), domain: Some("localhost".into()),
port: Some(8080), port: Some(8080),
app_name: Some("EWP".into()), app_name: Some("EWP".into()),
exclude_courses: Some([].into()), ..FileConfig::default()
..Self::default()
} }
} }
/// Complete default structure with an existing one /// Complete default structure with an existing one
fn complete(a: Self) -> Self { fn complete(a: Self) -> Self {
// Default config // Default config
let d = Self::new(); let d = FileConfig::new();
#[allow(clippy::items_after_statements)]
/// Return the default value if nothing is value is none /// Return the default value if nothing is value is none
fn test<T>(val: Option<T>, default: Option<T>) -> Option<T> { fn test<T>(val: Option<T>, default: Option<T>) -> Option<T> {
if val.is_some() { if val.is_some() {
@ -69,58 +65,54 @@ impl FileConfiguration {
app_name: test(a.app_name, d.app_name), app_name: test(a.app_name, d.app_name),
name: test(a.name, d.name), name: test(a.name, d.name),
fullname: test(a.fullname, d.fullname), fullname: test(a.fullname, d.fullname),
exclude_courses: test(a.exclude_courses, d.exclude_courses),
} }
} }
} }
// Paths where files are stored
#[derive(Clone, Debug)]
pub struct Locations {
pub static_dir: String,
pub data_dir: String,
}
/// Configuration used internally in the app /// Configuration used internally in the app
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Config { pub struct Config {
/// Information given in the config file /// Information given in the config file
pub fc: FileConfiguration, pub fc: FileConfig,
/// Location where the static files are stored /// Location where the static files are stored
pub locations: Locations, pub static_location: String,
/// Informations about templates /// Informations about templates
pub tmpl: Template, pub tmpl: Template,
} }
/// Load the config file /// Load the config file
fn get_file_config(file_path: &str) -> FileConfiguration { fn get_file_config(file_path: &str) -> FileConfig {
fs::read_to_string(file_path).map_or_else( match fs::read_to_string(file_path) {
|_| FileConfiguration::new(), Ok(file) => match toml::from_str(&file) {
|file| match toml::from_str(&file) { Ok(stored_config) => FileConfig::complete(stored_config),
Ok(stored_config) => FileConfiguration::complete(stored_config),
Err(file_error) => { Err(file_error) => {
panic!("Error in config file: {file_error}"); panic!("Error in config file: {file_error}");
} }
}, },
) Err(_) => {
// No config file
FileConfig::new()
}
}
} }
/// Build the configuration /// Build the configuration
pub fn get_configuration(file_path: &str) -> Config { pub fn get_config(file_path: &str) -> Config {
let internal_config = get_file_config(file_path); let internal_config = get_file_config(file_path);
let static_dir = "static"; let static_dir = "static".to_owned();
let templates_dir = "templates"; let templates_dir = "templates".to_owned();
let files_root = init("dist".into(), static_dir, templates_dir); let files_root = init(
"dist".into(),
static_dir.to_owned(),
templates_dir.to_owned(),
);
Config { Config {
fc: internal_config.clone(), fc: internal_config.to_owned(),
locations: Locations { static_location: format!("{}/{}", files_root, static_dir),
static_dir: format!("{files_root}/{static_dir}"),
data_dir: String::from("data"),
},
tmpl: Template { tmpl: Template {
directory: format!("{files_root}/{templates_dir}"), directory: format!("{}/{}", files_root, templates_dir),
app_name: internal_config.app_name.unwrap(), app_name: internal_config.app_name.unwrap(),
url: internal_config.domain.unwrap(), url: internal_config.domain.unwrap(),
name: internal_config.name, name: internal_config.name,
@ -129,16 +121,16 @@ pub fn get_configuration(file_path: &str) -> Config {
} }
/// Preparation before running the http server /// Preparation before running the http server
fn init(dist_dir: String, static_dir: &str, templates_dir: &str) -> String { fn init(dist_dir: String, static_dir: String, templates_dir: String) -> String {
// The static folder is minimized only in release mode // The static folder is minimized only in release mode
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
".".into() ".".into()
} else { } else {
let cfg = minify_html::Cfg { let cfg = minify_html::Cfg {
keep_closing_tags: true, keep_closing_tags: true,
preserve_brace_template_syntax: true,
minify_css: true, minify_css: true,
minify_js: true, minify_js: true,
remove_bangs: false,
..minify_html::Cfg::spec_compliant() ..minify_html::Cfg::spec_compliant()
}; };
@ -147,7 +139,7 @@ fn init(dist_dir: String, static_dir: &str, templates_dir: &str) -> String {
let path = entry.unwrap(); let path = entry.unwrap();
let path_with_dist = path let path_with_dist = path
.to_string_lossy() .to_string_lossy()
.replace(static_dir, &format!("{dist_dir}/{static_dir}")); .replace(&static_dir, &format!("{dist_dir}/{static_dir}"));
minify_and_copy(&cfg, path, path_with_dist); minify_and_copy(&cfg, path, path_with_dist);
} }
@ -157,7 +149,7 @@ fn init(dist_dir: String, static_dir: &str, templates_dir: &str) -> String {
let path = entry.unwrap(); let path = entry.unwrap();
let path_with_dist = path let path_with_dist = path
.to_string_lossy() .to_string_lossy()
.replace(templates_dir, &format!("{dist_dir}/{templates_dir}")); .replace(&templates_dir, &format!("{dist_dir}/{templates_dir}"));
minify_and_copy(&cfg, path, path_with_dist); minify_and_copy(&cfg, path, path_with_dist);
} }

View file

@ -18,20 +18,20 @@ mod routes;
#[actix_web::main] #[actix_web::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let config = config::get_configuration("./config/config.toml"); let config = config::get_config("./config/config.toml");
let addr = ("0.0.0.0", config.fc.port.unwrap()); let addr = ("0.0.0.0", config.fc.port.unwrap());
println!( println!(
"Listening to {}://{}:{}", "Listening to {}://{}:{}",
config.clone().fc.scheme.unwrap(), config.to_owned().fc.scheme.unwrap(),
addr.0, addr.0,
addr.1 addr.1
); );
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.app_data(web::Data::new(config.clone())) .app_data(web::Data::new(config.to_owned()))
.wrap(Compress::default()) .wrap(Compress::default())
.wrap( .wrap(
DefaultHeaders::new() DefaultHeaders::new()
@ -42,19 +42,12 @@ async fn main() -> Result<()> {
.add(("Server", format!("ewp/{}", env!("CARGO_PKG_VERSION")))) .add(("Server", format!("ewp/{}", env!("CARGO_PKG_VERSION"))))
.add(("Permissions-Policy", "interest-cohort=()")), .add(("Permissions-Policy", "interest-cohort=()")),
) )
.service( .service(web::scope("/api").service(web::scope("v1").service(api_v1::love)))
web::scope("/api").service(
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)
.service(agreements::humans) .service(agreements::humans)
.service(agreements::robots) .service(agreements::robots)
.service(agreements::webmanifest) .service(agreements::sitemap)
.service(blog::index) .service(blog::index)
.service(blog::rss) .service(blog::rss)
.service(blog::page) .service(blog::page)
@ -67,7 +60,7 @@ async fn main() -> Result<()> {
.service(portfolio::page) .service(portfolio::page)
.service(setup::page) .service(setup::page)
.service(web3::page) .service(web3::page)
.service(Files::new("/", config.locations.static_dir.clone())) .service(Files::new("/", config.static_location.to_owned()))
.default_service(web::to(not_found::page)) .default_service(web::to(not_found::page))
}) })
.bind(addr)? .bind(addr)?

View file

@ -1,3 +1,5 @@
use core::panic;
use reqwest::{header::ACCEPT, Error}; use reqwest::{header::ACCEPT, Error};
use serde::Deserialize; use serde::Deserialize;
@ -30,23 +32,21 @@ pub enum ProjectState {
Merged = 2, Merged = 2,
} }
impl TryFrom<u8> for ProjectState { impl From<u8> for ProjectState {
type Error = (); fn from(orig: u8) -> Self {
fn try_from(orig: u8) -> Result<Self, Self::Error> {
match orig { match orig {
0 => Ok(Self::Closed), 0 => Self::Closed,
1 => Ok(Self::Open), 1 => Self::Open,
2 => Ok(Self::Merged), 2 => Self::Merged,
_ => Err(()), _ => panic!(),
} }
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Project { pub struct Project {
pub name: String, pub project: String,
pub url: String, pub project_url: String,
pub status: ProjectState, pub status: ProjectState,
pub title: String, pub title: String,
pub id: u32, pub id: u32,
@ -68,8 +68,8 @@ pub async fn fetch_pr() -> Result<Vec<Project>, Error> {
let mut list = vec![]; let mut list = vec![];
resp.items.iter().for_each(|p| { resp.items.iter().for_each(|p| {
list.push(Project { list.push(Project {
name: p.repository_url.split('/').last().unwrap().into(), project: p.repository_url.split('/').last().unwrap().into(),
url: p.repository_url.clone(), project_url: p.repository_url.to_owned(),
status: if p.pull_request.merged_at.is_none() { status: if p.pull_request.merged_at.is_none() {
if p.state == "closed" { if p.state == "closed" {
ProjectState::Closed ProjectState::Closed
@ -79,9 +79,9 @@ pub async fn fetch_pr() -> Result<Vec<Project>, Error> {
} else { } else {
ProjectState::Merged ProjectState::Merged
}, },
title: p.title.clone(), title: p.title.to_owned(),
id: p.number, id: p.number,
contrib_url: p.pull_request.html_url.clone(), contrib_url: p.pull_request.html_url.to_owned(),
}); });
}); });

View file

@ -1,19 +1,16 @@
use crate::misc::date::Date; use crate::misc::date::Date;
use base64::engine::general_purpose;
use base64::Engine;
use comrak::nodes::{AstNode, NodeValue}; use comrak::nodes::{AstNode, NodeValue};
use comrak::{format_html, parse_document, Arena, ComrakOptions, ListStyleType, Options}; use comrak::{format_html, parse_document, Arena, ComrakOptions, ListStyleType};
use lol_html::html_content::ContentType; use lol_html::{element, rewrite_str, RewriteStrSettings};
use lol_html::{element, rewrite_str, HtmlRewriter, RewriteStrSettings, Settings};
use ramhorns::Content; use ramhorns::Content;
use regex::{Captures, Regex};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use std::fmt::Debug;
use std::fs; use std::fs;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
/// Metadata for blog posts /// Regular markdown files, no metadata
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileNoMetadata {}
#[derive(Content, Debug, Default, Deserialize)] #[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadataBlog { pub struct FileMetadataBlog {
pub title: Option<String>, pub title: Option<String>,
@ -24,7 +21,39 @@ pub struct FileMetadataBlog {
pub toc: Option<bool>, pub toc: Option<bool>,
} }
/// A tag, related to post blog #[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>,
}
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadataPortfolio {
pub title: Option<String>,
pub link: Option<String>,
pub description: Option<String>,
pub language: Option<String>,
}
pub enum TypeFileMetadata {
Blog,
Contact,
Portfolio,
Cours,
}
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadata {
pub blog: Option<FileMetadataBlog>,
pub contact: Option<FileMetadataContact>,
pub portfolio: Option<FileMetadataPortfolio>,
pub cours: Option<FileNoMetadata>,
}
#[derive(Content, Debug, Clone)] #[derive(Content, Debug, Clone)]
pub struct Tag { pub struct Tag {
pub name: String, pub name: String,
@ -36,7 +65,7 @@ impl<'de> Deserialize<'de> for Tag {
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
match <&str>::deserialize(deserializer) { match <&str>::deserialize(deserializer) {
Ok(s) => match serde_yml::from_str(s) { Ok(s) => match serde_yaml::from_str(s) {
Ok(tag) => Ok(Self { name: tag }), Ok(tag) => Ok(Self { name: tag }),
Err(e) => Err(serde::de::Error::custom(e)), Err(e) => Err(serde::de::Error::custom(e)),
}, },
@ -45,76 +74,14 @@ impl<'de> Deserialize<'de> for Tag {
} }
} }
/// 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)] #[derive(Content, Debug)]
pub struct Metadata { pub struct Metadata {
pub info: FileMetadata, pub info: FileMetadata,
pub math: bool, pub math: bool,
pub mermaid: bool, pub mermaid: bool,
pub syntax_highlight: 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
#[derive(Content, Debug)] #[derive(Content, Debug)]
pub struct File { pub struct File {
pub metadata: Metadata, pub metadata: Metadata,
@ -122,7 +89,7 @@ pub struct File {
} }
/// Options used for parser and compiler MD --> HTML /// Options used for parser and compiler MD --> HTML
pub fn get_options<'a>() -> ComrakOptions<'a> { pub fn get_options() -> ComrakOptions {
let mut options = comrak::Options::default(); let mut options = comrak::Options::default();
// Extension // Extension
@ -136,21 +103,12 @@ pub fn get_options<'a>() -> ComrakOptions<'a> {
options.extension.footnotes = true; options.extension.footnotes = true;
options.extension.description_lists = true; options.extension.description_lists = true;
options.extension.front_matter_delimiter = Some("---".into()); options.extension.front_matter_delimiter = Some("---".into());
options.extension.multiline_block_quotes = true;
options.extension.math_dollars = true;
options.extension.math_code = false;
options.extension.wikilinks_title_after_pipe = false;
options.extension.wikilinks_title_before_pipe = false;
options.extension.underline = true;
options.extension.spoiler = false;
options.extension.greentext = false;
// Parser // Parser
options.parse.smart = true; // could be boring options.parse.smart = true; // could be boring
options.parse.default_info_string = Some("plaintext".into()); options.parse.default_info_string = Some("plaintext".into());
options.parse.relaxed_tasklist_matching = true; options.parse.relaxed_tasklist_matching = true;
options.parse.relaxed_autolinks = true; options.parse.relaxed_autolinks = true;
// 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; // could be true? change by metadata could be good for compatibility
@ -161,25 +119,18 @@ pub fn get_options<'a>() -> ComrakOptions<'a> {
options.render.escape = false; options.render.escape = false;
options.render.list_style = ListStyleType::Dash; options.render.list_style = ListStyleType::Dash;
options.render.sourcepos = false; options.render.sourcepos = false;
options.render.experimental_inline_sourcepos = false;
options.render.escaped_char_spans = false;
options.render.ignore_setext = true;
options.render.ignore_empty_links = true;
options.render.gfm_quirks = true;
options.render.prefer_fenced = false;
options.render.figure_with_caption = false;
options options
} }
/// Resize images if needed /// Resize images if needed
fn custom_img_size(html: &str) -> String { fn custom_img_size(html: String) -> String {
rewrite_str( rewrite_str(
html, &html,
RewriteStrSettings { RewriteStrSettings {
element_content_handlers: vec![element!("img[alt]", |el| { element_content_handlers: vec![element!("img[alt]", |el| {
let alt = el.get_attribute("alt").unwrap(); let alt = el.get_attribute("alt").unwrap();
let possible_piece = alt.split('|').collect::<Vec<&str>>(); let possible_piece = alt.split(|c| c == '|').collect::<Vec<&str>>();
if possible_piece.len() > 1 { if possible_piece.len() > 1 {
let data = possible_piece.last().unwrap().trim(); let data = possible_piece.last().unwrap().trim();
@ -194,7 +145,7 @@ fn custom_img_size(html: &str) -> String {
el.set_attribute("width", dimension.0).unwrap(); el.set_attribute("width", dimension.0).unwrap();
el.set_attribute("height", dimension.1).unwrap(); el.set_attribute("height", dimension.1).unwrap();
if new_alt.is_empty() { if new_alt.is_empty() {
el.remove_attribute("alt"); el.remove_attribute("alt")
} else { } else {
el.set_attribute("alt", new_alt).unwrap(); el.set_attribute("alt", new_alt).unwrap();
} }
@ -204,7 +155,7 @@ fn custom_img_size(html: &str) -> String {
if data.parse::<i32>().is_ok() { if data.parse::<i32>().is_ok() {
el.set_attribute("width", data).unwrap(); el.set_attribute("width", data).unwrap();
if new_alt.is_empty() { if new_alt.is_empty() {
el.remove_attribute("alt"); el.remove_attribute("alt")
} else { } else {
el.set_attribute("alt", new_alt).unwrap(); el.set_attribute("alt", new_alt).unwrap();
} }
@ -220,69 +171,35 @@ fn custom_img_size(html: &str) -> String {
.unwrap() .unwrap()
} }
/// Fix local images to base64 and integration of markdown files fn math_processing(regex: &str, source: &str) -> String {
fn fix_images_and_integration(path: &str, html: &str) -> (String, Metadata) { Regex::new(regex)
let mut metadata = Metadata {
info: FileMetadata::default(),
math: false,
mermaid: false,
syntax_highlight: false,
mail_obfsucated: false,
};
(
rewrite_str(
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() .unwrap()
.to_string(); .replace_all(source, |captures: &Captures| {
if let Ok(file) = fs::read_to_string(&img_path) { captures
let mime = mime_guess::from_path(&img_path).first_or_octet_stream(); .iter()
if mime == "text/markdown" { .skip(1)
let mut options = get_options(); .filter_map(|capture| {
options.extension.footnotes = false; capture.map(|m| format!("<span data-katex=\"{}\"></span>", m.as_str()))
let data = read_md( })
&img_path, .collect::<Vec<String>>()
&file, .join("")
&TypeFileMetadata::Generic, })
Some(options), .to_string()
);
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}"))
.unwrap();
}
}
}
Ok(())
})],
..RewriteStrSettings::default()
},
)
.unwrap(),
metadata,
)
} }
/// Transform markdown string to File structure /// Transform markdown string to File structure
pub fn read_md( fn read(raw_text: &str, metadata_type: TypeFileMetadata) -> 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); // Transform LaTeX formulas
let root = parse_document(&arena, raw_text, &opt); let text = math_processing(
r"(?U)(\$[^\$|\n]+\$)[^\$]",
&math_processing(r"(?U)(\$\$[^\$|\n]+\$\$)", raw_text),
);
println!("{}", text);
let options = get_options();
let root = parse_document(&arena, &text, &options);
// Find metadata // Find metadata
let metadata = get_metadata(root, metadata_type); let metadata = get_metadata(root, metadata_type);
@ -292,73 +209,64 @@ pub fn read_md(
// Convert to HTML // Convert to HTML
let mut html = vec![]; let mut html = vec![];
format_html(root, &opt, &mut html).unwrap(); format_html(root, &options, &mut html).unwrap();
let mut html_content = String::from_utf8(html).unwrap(); let mut html_content = String::from_utf8(html).unwrap();
/* println!("{}", html_content); */
let children_metadata; html_content = custom_img_size(html_content);
let mail_obfsucated;
(html_content, children_metadata) = fix_images_and_integration(path, &html_content);
html_content = custom_img_size(&html_content);
(html_content, mail_obfsucated) = mail_obfuscation(&html_content);
let mut final_metadata = Metadata { File {
metadata: Metadata {
info: metadata, info: metadata,
mermaid: check_mermaid(root, mermaid_name), mermaid: check_mermaid(root, mermaid_name),
syntax_highlight: check_code(root, &[mermaid_name.into()]), syntax_highlight: check_code(root, &[mermaid_name.into()]),
math: check_math(&html_content), math: check_math(&html_content),
mail_obfsucated, },
};
final_metadata.merge(&children_metadata);
File {
metadata: final_metadata,
content: html_content, content: html_content,
} }
} }
/// Read markdown file
pub fn read_file(filename: &str, expected_file: TypeFileMetadata) -> Option<File> {
match fs::read_to_string(filename) {
Ok(text) => Some(read(&text, expected_file)),
_ => None,
}
}
/// Deserialize metadata based on a type /// Deserialize metadata based on a type
fn deserialize_metadata<T: Default + serde::de::DeserializeOwned>(text: &str) -> T { fn deserialize_metadata<T: Default + serde::de::DeserializeOwned>(text: &str) -> T {
serde_yml::from_str(text.trim().trim_matches(&['-'] as &[_])).unwrap_or_default() serde_yaml::from_str(text.trim_matches(&['-', '\n'] as &[_])).unwrap_or_default()
} }
/// Fetch metadata from AST /// Fetch metadata from AST
pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: &TypeFileMetadata) -> FileMetadata { pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: TypeFileMetadata) -> FileMetadata {
root.children() match root
.children()
.find_map(|node| match &node.data.borrow().value { .find_map(|node| match &node.data.borrow().value {
// Extract metadata from frontmatter
NodeValue::FrontMatter(text) => Some(match mtype { NodeValue::FrontMatter(text) => Some(match mtype {
TypeFileMetadata::Blog => FileMetadata { TypeFileMetadata::Blog => FileMetadata {
blog: Some(deserialize_metadata(text)), blog: Some(deserialize_metadata(text)),
..FileMetadata::default() ..FileMetadata::default()
}, },
TypeFileMetadata::Contact => { TypeFileMetadata::Contact => FileMetadata {
let mut metadata: FileMetadataContact = deserialize_metadata(text); contact: Some(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() ..FileMetadata::default()
}, },
TypeFileMetadata::Portfolio => FileMetadata { TypeFileMetadata::Portfolio => FileMetadata {
portfolio: Some(deserialize_metadata(text)), portfolio: Some(deserialize_metadata(text)),
..FileMetadata::default() ..FileMetadata::default()
}, },
TypeFileMetadata::Cours => FileMetadata {
cours: Some(deserialize_metadata(text)),
..FileMetadata::default()
},
}), }),
_ => None, _ => None,
}) }) {
.map_or_else( Some(data) => data,
|| match mtype { None => match mtype {
TypeFileMetadata::Blog => FileMetadata { TypeFileMetadata::Blog => FileMetadata {
blog: Some(FileMetadataBlog::default()), blog: Some(FileMetadataBlog::default()),
..FileMetadata::default() ..FileMetadata::default()
@ -367,18 +275,16 @@ pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: &TypeFileMetadata) -> File
contact: Some(FileMetadataContact::default()), contact: Some(FileMetadataContact::default()),
..FileMetadata::default() ..FileMetadata::default()
}, },
TypeFileMetadata::Generic => FileMetadata::default(),
TypeFileMetadata::Index => FileMetadata {
index: Some(FileMetadataIndex::default()),
..FileMetadata::default()
},
TypeFileMetadata::Portfolio => FileMetadata { TypeFileMetadata::Portfolio => FileMetadata {
portfolio: Some(FileMetadataPortfolio::default()), portfolio: Some(FileMetadataPortfolio::default()),
..FileMetadata::default() ..FileMetadata::default()
}, },
TypeFileMetadata::Cours => FileMetadata {
cours: Some(FileNoMetadata::default()),
..FileMetadata::default()
}, },
|data| data, },
) }
} }
/// Check whether mermaid diagrams are in the AST /// Check whether mermaid diagrams are in the AST
@ -404,24 +310,9 @@ fn check_code<'a>(root: &'a AstNode<'a>, blacklist: &[String]) -> bool {
}) })
} }
/// Check if html contains maths /// Check if html can contains maths
fn check_math(html: &str) -> bool { fn check_math(html: &str) -> bool {
let math_detected = Arc::new(AtomicBool::new(false)); html.contains('$')
let _ = HtmlRewriter::new(
Settings {
element_content_handlers: vec![element!("span[data-math-style]", |_| {
math_detected.store(true, Ordering::SeqCst);
Ok(())
})],
..Settings::default()
},
|_: &[u8]| {},
)
.write(html.as_bytes());
math_detected.load(Ordering::SeqCst)
} }
/// Change class of languages for hljs detection /// Change class of languages for hljs detection
@ -434,69 +325,3 @@ fn hljs_replace<'a>(root: &'a AstNode<'a>, mermaid_str: &str) {
} }
}); });
} }
/// Obfuscate email if email found
fn mail_obfuscation(html: &str) -> (String, bool) {
let modified = Arc::new(AtomicBool::new(false));
let data_attr = "title";
// Modify HTML for mails
let new_html = rewrite_str(
html,
RewriteStrSettings {
element_content_handlers: vec![element!("a[href^='mailto:']", |el| {
modified.store(true, Ordering::SeqCst);
// Get mail address
let link = el.get_attribute("href").unwrap();
let (uri, mail) = &link.split_at(7);
let (before, after) = mail.split_once('@').unwrap();
// Preserve old data and add obfuscated mail address
el.prepend(&format!("<span {data_attr}='"), ContentType::Html);
let modified_mail = format!("'></span>{before}<span class='at'>(at)</span>{after}");
el.append(&modified_mail, ContentType::Html);
// Change href
Ok(el.set_attribute("href", &format!("{uri}{before} at {after}"))?)
})],
..RewriteStrSettings::default()
},
)
.unwrap();
let is_modified = modified.load(Ordering::SeqCst);
if is_modified {
// Remove old data email if exists
(
rewrite_str(
&new_html,
RewriteStrSettings {
element_content_handlers: vec![element!(
&format!("a[href^='mailto:'] > span[{data_attr}]"),
|el| {
Ok(el.set_attribute(
data_attr,
// Remove mails
el.get_attribute(data_attr)
.unwrap()
.split_whitespace()
.filter(|word| !word.contains('@'))
.collect::<Vec<&str>>()
.join(" ")
.trim(),
)?)
}
)],
..RewriteStrSettings::default()
},
)
.unwrap(),
is_modified,
)
} else {
(new_html, is_modified)
}
}

View file

@ -1,17 +1,11 @@
use std::{fs, path::Path};
use actix_web::{ use actix_web::{
http::header::{self, ContentType, TryIntoHeaderValue}, http::header::{self, ContentType, TryIntoHeaderValue},
http::StatusCode,
HttpRequest, HttpResponse, Responder, HttpRequest, HttpResponse, Responder,
}; };
use base64::{engine::general_purpose, Engine};
use cached::proc_macro::cached; use cached::proc_macro::cached;
use reqwest::Client; use reqwest::{Client, StatusCode};
use crate::config::FileConfiguration; use crate::config::FileConfig;
use super::markdown::{read_md, File, FileMetadata, Metadata, TypeFileMetadata};
#[cached] #[cached]
pub fn get_reqwest_client() -> Client { pub fn get_reqwest_client() -> Client {
@ -22,7 +16,7 @@ pub fn get_reqwest_client() -> Client {
} }
/// Get URL of the app /// Get URL of the app
pub fn get_url(fc: FileConfiguration) -> String { pub fn get_url(fc: FileConfig) -> String {
/* let port = match fc.scheme.as_deref() { /* let port = match fc.scheme.as_deref() {
Some("https") if fc.port == Some(443) => String::new(), Some("https") if fc.port == Some(443) => String::new(),
Some("http") if fc.port == Some(80) => String::new(), Some("http") if fc.port == Some(80) => String::new(),
@ -33,8 +27,8 @@ pub fn get_url(fc: FileConfiguration) -> String {
} }
/// Make a list of keywords /// Make a list of keywords
pub fn make_kw(list: &[&str]) -> String { pub fn make_kw(list: &[&str]) -> Option<String> {
list.join(", ") Some(list.join(", "))
} }
/// Send HTML file /// Send HTML file
@ -51,35 +45,3 @@ impl Responder for Html {
res res
} }
} }
/// Read a file
pub fn read_file(filename: &str, expected_file: &TypeFileMetadata) -> Option<File> {
Path::new(filename)
.extension()
.and_then(|ext| match ext.to_str().unwrap() {
"pdf" => fs::read(filename).map_or(None, |bytes| Some(read_pdf(bytes))),
_ => fs::read_to_string(filename).map_or(None, |text| {
Some(read_md(filename, &text, expected_file, None))
}),
})
}
fn read_pdf(data: Vec<u8>) -> File {
let pdf = general_purpose::STANDARD.encode(data);
File {
metadata: Metadata {
info: FileMetadata::default(),
mermaid: false,
syntax_highlight: false,
math: false,
mail_obfsucated: false,
},
content: format!(
r#"<embed
src="data:application/pdf;base64,{pdf}"
style="width: 100%; height: 79vh";
>"#
),
}
}

View file

@ -1,4 +1,4 @@
use crate::{config::Config, misc::utils::get_url, template::InfosPage}; use crate::{config::Config, misc::utils::get_url, template::Infos};
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;
@ -6,7 +6,7 @@ use ramhorns::Content;
#[routes] #[routes]
#[get("/.well-known/security.txt")] #[get("/.well-known/security.txt")]
#[get("/security.txt")] #[get("/security.txt")]
pub async fn security(config: web::Data<Config>) -> impl Responder { async fn security(config: web::Data<Config>) -> impl Responder {
HttpResponse::Ok() HttpResponse::Ok()
.content_type(ContentType::plaintext()) .content_type(ContentType::plaintext())
.body(build_securitytxt(config.get_ref().to_owned())) .body(build_securitytxt(config.get_ref().to_owned()))
@ -28,12 +28,12 @@ fn build_securitytxt(config: Config) -> String {
contact: config.fc.mail.unwrap_or_default(), contact: config.fc.mail.unwrap_or_default(),
pref_lang: config.fc.lang.unwrap_or_default(), pref_lang: config.fc.lang.unwrap_or_default(),
}, },
InfosPage::default(), Infos::default(),
) )
} }
#[get("/humans.txt")] #[get("/humans.txt")]
pub async fn humans(config: web::Data<Config>) -> impl Responder { async fn humans(config: web::Data<Config>) -> impl Responder {
HttpResponse::Ok() HttpResponse::Ok()
.content_type(ContentType::plaintext()) .content_type(ContentType::plaintext())
.body(build_humanstxt(config.get_ref().to_owned())) .body(build_humanstxt(config.get_ref().to_owned()))
@ -55,12 +55,12 @@ fn build_humanstxt(config: Config) -> String {
lang: config.fc.lang.unwrap_or_default(), lang: config.fc.lang.unwrap_or_default(),
name: config.fc.fullname.unwrap_or_default(), name: config.fc.fullname.unwrap_or_default(),
}, },
InfosPage::default(), Infos::default(),
) )
} }
#[get("/robots.txt")] #[get("/robots.txt")]
pub async fn robots() -> impl Responder { async fn robots() -> impl Responder {
HttpResponse::Ok() HttpResponse::Ok()
.content_type(ContentType::plaintext()) .content_type(ContentType::plaintext())
.body(build_robotstxt()) .body(build_robotstxt())
@ -71,29 +71,8 @@ fn build_robotstxt() -> String {
"User-agent: * Allow: /".into() "User-agent: * Allow: /".into()
} }
#[get("/app.webmanifest")] #[get("/sitemap.xml")]
pub async fn webmanifest(config: web::Data<Config>) -> impl Responder { async fn sitemap() -> impl Responder {
HttpResponse::Ok() // TODO
.content_type(ContentType("application/manifest+json".parse().unwrap())) actix_web::web::Redirect::to("/")
.body(build_webmanifest(config.get_ref().to_owned()))
}
#[derive(Content, Debug)]
struct WebManifestTemplate {
name: String,
description: String,
url: String,
}
#[once(time = 60)]
fn build_webmanifest(config: Config) -> String {
config.tmpl.render(
"app.webmanifest",
WebManifestTemplate {
name: config.fc.clone().app_name.unwrap(),
description: "Easy WebPage generator".to_owned(),
url: get_url(config.fc),
},
InfosPage::default(),
)
} }

View file

@ -1,53 +1,14 @@
use std::time::Duration;
use actix_web::{get, HttpResponse, Responder}; use actix_web::{get, HttpResponse, Responder};
use chrono::Utc;
use cyborgtime::format_duration;
use serde::Serialize; use serde::Serialize;
/// Response for /love
#[derive(Serialize)] #[derive(Serialize)]
struct InfoLove { struct Info {
unix_epoch: u32, unix_epoch: u32,
} }
#[get("/love")] #[get("/love")]
pub async fn love() -> impl Responder { async fn love() -> impl Responder {
HttpResponse::Ok().json(InfoLove { HttpResponse::Ok().json(Info {
unix_epoch: 1_605_576_600, unix_epoch: 1605576600,
}) })
} }
/// Response for /backtofrance
#[derive(Serialize)]
struct InfoBTF {
unix_epoch: u64,
countdown: String,
}
#[get("/backtofrance")]
pub async fn btf() -> impl Responder {
let target = 1_736_618_100;
let current_time: u64 = Utc::now().timestamp().try_into().unwrap();
let info = InfoBTF {
unix_epoch: target,
countdown: if current_time > target {
"Already happened".to_owned()
} else {
let duration_epoch = target - current_time;
let duration = Duration::from_secs(duration_epoch);
format_duration(duration).to_string()
},
};
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/",
))
}

View file

@ -7,7 +7,7 @@ use ::rss::{
extension::atom::{AtomExtension, Link}, extension::atom::{AtomExtension, Link},
Category, Channel, Guid, Image, Item, Category, Channel, Guid, Image, Item,
}; };
use actix_web::{get, http::header::ContentType, routes, web, HttpResponse, Responder}; use actix_web::{get, web, HttpResponse, Responder};
use cached::proc_macro::once; use cached::proc_macro::once;
use chrono::{DateTime, Datelike, Local, NaiveDateTime, Utc}; use chrono::{DateTime, Datelike, Local, NaiveDateTime, Utc};
use chrono_tz::Europe; use chrono_tz::Europe;
@ -18,37 +18,31 @@ use crate::{
config::Config, config::Config,
misc::{ misc::{
date::Date, date::Date,
markdown::{get_metadata, get_options, File, FileMetadataBlog, TypeFileMetadata}, markdown::{
utils::{get_url, make_kw, read_file, Html}, get_metadata, get_options, read_file, File, FileMetadataBlog, TypeFileMetadata,
}, },
template::{InfosPage, NavBar}, utils::{get_url, make_kw, Html},
},
template::{Infos, NavBar},
}; };
const MIME_TYPE_RSS: &str = "application/rss+xml"; 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 { 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()))
} }
#[derive(Content, Debug)] #[derive(Content, Debug)]
struct BlogIndexTemplate { struct BlogIndexTemplate {
navbar: NavBar, navbar: NavBar,
about: Option<File>,
posts: Vec<Post>, posts: Vec<Post>,
no_posts: bool, no_posts: bool,
} }
#[once(time = 60)] #[once(time = 60)]
fn build_index(config: Config) -> String { fn build_index(config: Config) -> String {
let blog_dir = format!("{}/{}", config.locations.data_dir, BLOG_DIR); let mut posts = get_posts("data/blog");
let mut posts = get_posts(&format!("{blog_dir}/{POST_DIR}"));
// Get about
let about: Option<File> =
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));
@ -61,17 +55,16 @@ fn build_index(config: Config) -> String {
blog: true, blog: true,
..NavBar::default() ..NavBar::default()
}, },
about,
no_posts: posts.is_empty(), no_posts: posts.is_empty(),
posts, posts,
}, },
InfosPage { Infos {
title: Some("Blog".into()), page_title: Some("Blog".into()),
desc: Some(format!( page_desc: Some(format!(
"Liste des posts d'{}", "Liste des posts d'{}",
config.fc.name.unwrap_or_default() config.fc.name.unwrap_or_default()
)), )),
kw: Some(make_kw(&["blog", "blogging"])), page_kw: make_kw(&["blog", "blogging"]),
}, },
) )
} }
@ -88,13 +81,13 @@ struct Post {
impl Post { impl Post {
// Fetch the file content // Fetch the file content
fn fetch_content(&mut self, data_dir: &str) { fn fetch_content(&mut self) {
let blog_dir = format!("{data_dir}/{BLOG_DIR}/{POST_DIR}"); let blog_dir = "data/blog";
let ext = ".md"; let ext = ".md";
if let Some(file) = read_file( if let Some(file) = read_file(
&format!("{blog_dir}/{}{ext}", self.url), &format!("{blog_dir}/{}{ext}", self.url),
&TypeFileMetadata::Blog, TypeFileMetadata::Blog,
) { ) {
self.content = Some(file.content); self.content = Some(file.content);
} }
@ -104,59 +97,60 @@ impl Post {
impl Hash for Post { impl Hash for Post {
fn hash<H: Hasher>(&self, state: &mut H) { fn hash<H: Hasher>(&self, state: &mut H) {
if let Some(content) = &self.content { if let Some(content) = &self.content {
content.hash(state); content.hash(state)
} }
} }
} }
fn get_posts(location: &str) -> Vec<Post> { fn get_posts(location: &str) -> Vec<Post> {
let entries = std::fs::read_dir(location).map_or_else( let entries = match std::fs::read_dir(location) {
|_| vec![], Ok(res) => res
|res| { .flatten()
res.flatten() .filter(|f| match f.path().extension() {
.filter(|f| f.path().extension().map_or(false, |ext| ext == "md")) Some(ext) => ext == "md",
.collect::<Vec<std::fs::DirEntry>>() None => false,
}, })
); .collect::<Vec<std::fs::DirEntry>>(),
Err(_) => vec![],
};
entries entries
.iter() .iter()
.filter_map(|f| { .filter_map(|f| {
let fname = f.file_name(); let _filename = f.file_name();
let filename = fname.to_string_lossy(); let filename = _filename.to_string_lossy();
let file_without_ext = filename.split_at(filename.len() - 3).0; let file_without_ext = filename.split_at(filename.len() - 3).0;
let file_metadata = std::fs::read_to_string(format!("{location}/{filename}")) let file_metadata = match std::fs::read_to_string(format!("{location}/{filename}")) {
.map_or_else( Ok(text) => {
|_| FileMetadataBlog {
title: Some(file_without_ext.into()),
..FileMetadataBlog::default()
},
|text| {
let arena = Arena::new(); let arena = Arena::new();
let options = get_options(); let options = get_options();
let root = parse_document(&arena, &text, &options); let root = parse_document(&arena, &text, &options);
let mut metadata = let mut metadata = get_metadata(root, TypeFileMetadata::Blog).blog.unwrap();
get_metadata(root, &TypeFileMetadata::Blog).blog.unwrap();
// Always have a title // Always have a title
metadata.title = metadata metadata.title = match metadata.title {
.title Some(title) => Some(title),
.map_or_else(|| Some(file_without_ext.into()), Some); None => Some(file_without_ext.into()),
};
metadata metadata
}
Err(_) => FileMetadataBlog {
title: Some(file_without_ext.into()),
..FileMetadataBlog::default()
}, },
); };
if file_metadata.publish == Some(true) { if let Some(true) = file_metadata.publish {
Some(Post { Some(Post {
url: file_without_ext.into(), url: file_without_ext.into(),
title: file_metadata.title.unwrap(), title: file_metadata.title.unwrap(),
date: file_metadata.date.unwrap_or({ date: file_metadata.date.unwrap_or({
let m = f.metadata().unwrap(); let m = f.metadata().unwrap();
let date = std::convert::Into::<DateTime<Utc>>::into( let date = std::convert::Into::<DateTime<Utc>>::into(
m.modified().unwrap_or_else(|_| m.created().unwrap()), m.modified().unwrap_or(m.created().unwrap()),
) )
.date_naive(); .date_naive();
@ -172,7 +166,7 @@ fn get_posts(location: &str) -> Vec<Post> {
.tags .tags
.unwrap_or_default() .unwrap_or_default()
.iter() .iter()
.map(|t| t.name.clone()) .map(|t| t.name.to_owned())
.collect(), .collect(),
}) })
} else { } else {
@ -190,21 +184,13 @@ struct BlogPostTemplate {
} }
#[get("/blog/p/{id}")] #[get("/blog/p/{id}")]
pub async fn page(path: web::Path<(String,)>, config: web::Data<Config>) -> impl Responder { async fn page(path: web::Path<(String,)>, config: web::Data<Config>) -> impl Responder {
Html(build_post( Html(build_post(path.into_inner().0, config.get_ref().to_owned()))
&path.into_inner().0,
config.get_ref().to_owned(),
))
} }
fn build_post(file: &str, config: Config) -> String { fn build_post(file: String, config: Config) -> String {
let mut post = None; let mut post = None;
let (infos, toc) = get_post( let (infos, toc) = get_post(&mut post, file, config.fc.name.unwrap_or_default());
&mut post,
file,
&config.fc.name.unwrap_or_default(),
&config.locations.data_dir,
);
config.tmpl.render( config.tmpl.render(
"blog/post.html", "blog/post.html",
@ -220,22 +206,17 @@ fn build_post(file: &str, config: Config) -> String {
) )
} }
fn get_post( fn get_post(post: &mut Option<File>, filename: String, name: String) -> (Infos, String) {
post: &mut Option<File>, let blog_dir = "data/blog";
filename: &str,
name: &str,
data_dir: &str,
) -> (InfosPage, String) {
let blog_dir = format!("{data_dir}/{BLOG_DIR}/{POST_DIR}");
let ext = ".md"; let ext = ".md";
*post = read_file( *post = read_file(
&format!("{blog_dir}/{filename}{ext}"), &format!("{blog_dir}/{filename}{ext}"),
&TypeFileMetadata::Blog, TypeFileMetadata::Blog,
); );
let default = ( let default = (
filename, &filename,
&format!("Blog d'{name}"), &format!("Blog d'{name}"),
Vec::new(), Vec::new(),
String::new(), String::new(),
@ -264,35 +245,30 @@ fn get_post(
}; };
( (
InfosPage { Infos {
title: Some(format!("Post: {title}")), page_title: Some(format!("Post: {}", title)),
desc: Some(desc.clone()), page_desc: Some(desc.clone()),
kw: Some(make_kw( page_kw: make_kw(
&["blog", "blogging", "write", "writing"] &["blog", "blogging", "write", "writing"]
.into_iter() .into_iter()
.chain(tags.iter().map(|t| t.name.as_str())) .chain(tags.iter().map(|t| t.name.as_str()))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
)), ),
}, },
toc, toc,
) )
} }
#[routes]
#[get("/blog/blog.rss")]
#[get("/blog/rss")] #[get("/blog/rss")]
pub async fn rss(config: web::Data<Config>) -> impl Responder { async fn rss(config: web::Data<Config>) -> impl Responder {
HttpResponse::Ok() HttpResponse::Ok()
.content_type(ContentType(MIME_TYPE_RSS.parse().unwrap())) .append_header(("content-type", MIME_TYPE_RSS))
.body(build_rss(config.get_ref().to_owned())) .body(build_rss(config.get_ref().to_owned()))
} }
#[once(time = 10800)] // 3h #[once(time = 10800)] // 3h
fn build_rss(config: Config) -> String { fn build_rss(config: Config) -> String {
let mut posts = get_posts(&format!( let mut posts = get_posts("data/blog");
"{}/{}/{}",
config.locations.data_dir, BLOG_DIR, POST_DIR
));
// 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));
@ -305,7 +281,7 @@ fn build_rss(config: Config) -> String {
} }
let link_to_site = get_url(config.fc.clone()); let link_to_site = get_url(config.fc.clone());
let author = if let (Some(mail), Some(name)) = (config.fc.mail, config.fc.fullname.clone()) { let author = if let (Some(mail), Some(name)) = (config.fc.mail, config.fc.fullname.to_owned()) {
Some(format!("{mail} ({name})")) Some(format!("{mail} ({name})"))
} else { } else {
None None
@ -313,11 +289,11 @@ fn build_rss(config: Config) -> String {
let title = format!("Blog d'{}", config.fc.name.unwrap_or_default()); let title = format!("Blog d'{}", config.fc.name.unwrap_or_default());
let lang = "fr"; let lang = "fr";
let channel = Channel { let channel = Channel {
title: title.clone(), title: title.to_owned(),
link: link_to_site.clone(), link: link_to_site.to_owned(),
description: "Un fil qui parle d'informatique notamment".into(), description: "Un fil qui parle d'informatique notamment".into(),
language: Some(lang.into()), language: Some(lang.into()),
managing_editor: author.clone(), managing_editor: author.to_owned(),
webmaster: author, webmaster: author,
pub_date: Some(Local::now().to_rfc2822()), pub_date: Some(Local::now().to_rfc2822()),
categories: ["blog", "blogging", "write", "writing"] categories: ["blog", "blogging", "write", "writing"]
@ -330,22 +306,22 @@ fn build_rss(config: Config) -> String {
generator: Some("ewp with rss crate".into()), generator: Some("ewp with rss crate".into()),
docs: Some("https://www.rssboard.org/rss-specification".into()), docs: Some("https://www.rssboard.org/rss-specification".into()),
image: Some(Image { image: Some(Image {
url: format!("{link_to_site}/icons/favicon-32x32.png"), url: format!("{}/icons/favicon-32x32.png", link_to_site),
title: title.clone(), title: title.to_owned(),
link: link_to_site.clone(), link: link_to_site.to_owned(),
..Image::default() ..Image::default()
}), }),
items: posts items: posts
.iter_mut() .iter_mut()
.map(|p| { .map(|p| {
// Get post data // Get post data
p.fetch_content(&config.locations.data_dir); p.fetch_content();
// Build item // Build item
Item { Item {
title: Some(p.title.clone()), title: Some(p.title.to_owned()),
link: Some(format!("{}/blog/p/{}", link_to_site, p.url)), link: Some(format!("{}/blog/p/{}", link_to_site, p.url)),
description: p.content.clone(), description: p.content.to_owned(),
categories: p categories: p
.tags .tags
.iter() .iter()
@ -378,7 +354,7 @@ fn build_rss(config: Config) -> String {
.collect(), .collect(),
atom_ext: Some(AtomExtension { atom_ext: Some(AtomExtension {
links: vec![Link { links: vec![Link {
href: format!("{link_to_site}/blog/rss"), href: format!("{}/blog/rss", link_to_site),
rel: "self".into(), rel: "self".into(),
hreflang: Some(lang.into()), hreflang: Some(lang.into()),
mime_type: Some(MIME_TYPE_RSS.into()), mime_type: Some(MIME_TYPE_RSS.into()),

View file

@ -7,14 +7,12 @@ use std::fs::read_to_string;
use crate::{ use crate::{
config::Config, config::Config,
misc::{ misc::{
markdown::{File, TypeFileMetadata}, markdown::{read_file, File, TypeFileMetadata},
utils::{make_kw, read_file, Html}, utils::{make_kw, Html},
}, },
template::{InfosPage, NavBar}, template::{Infos, NavBar},
}; };
const CONTACT_DIR: &str = "contacts";
pub fn pages(cfg: &mut web::ServiceConfig) { pub fn pages(cfg: &mut web::ServiceConfig) {
// Here define the services used // Here define the services used
let routes = |route_path| { let routes = |route_path| {
@ -32,7 +30,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)] #[derive(Clone, Debug)]
struct ContactLink { struct ContactLink {
service: String, service: String,
@ -41,12 +38,13 @@ struct ContactLink {
} }
#[once(time = 60)] #[once(time = 60)]
fn find_links(directory: String) -> Vec<ContactLink> { fn find_links() -> Vec<ContactLink> {
// TOML filename // TOML file location
let contacts_dir = "data/contacts";
let toml_file = "links.toml"; let toml_file = "links.toml";
// Read the TOML file and parse it // Read the TOML file and parse it
let toml_str = read_to_string(format!("{directory}/{toml_file}")).unwrap_or_default(); let toml_str = read_to_string(format!("{contacts_dir}/{toml_file}")).unwrap_or_default();
let mut redirections = vec![]; let mut redirections = vec![];
match toml::de::from_str::<toml::Value>(&toml_str) { match toml::de::from_str::<toml::Value>(&toml_str) {
@ -76,14 +74,14 @@ fn find_links(directory: String) -> Vec<ContactLink> {
#[routes] #[routes]
#[get("/{service}")] #[get("/{service}")]
#[get("/{service}/{scope}")] #[get("/{service}/{scope}")]
async fn service_redirection(config: web::Data<Config>, req: HttpRequest) -> impl Responder { async fn service_redirection(req: HttpRequest) -> impl Responder {
let info = req.match_info(); let info = req.match_info();
let link = find_links(format!("{}/{}", config.locations.data_dir, CONTACT_DIR)) let link = find_links()
.iter() .iter()
// Find requested service // Find requested service
.filter(|&x| x.service == *info.query("service")) .filter(|&x| x.service == *info.query("service"))
// Search for a potential scope // Search for a potential scope
.filter(|&x| match (info.get("scope"), x.scope.clone()) { .filter(|&x| match (info.get("scope"), x.scope.to_owned()) {
// The right scope is accepted // The right scope is accepted
(Some(str_value), Some(string_value)) if str_value == string_value.as_str() => true, (Some(str_value), Some(string_value)) if str_value == string_value.as_str() => true,
// No scope provided is accepted // No scope provided is accepted
@ -107,7 +105,6 @@ async fn service_redirection(config: web::Data<Config>, req: HttpRequest) -> imp
#[derive(Content, Debug)] #[derive(Content, Debug)]
struct NetworksTemplate { struct NetworksTemplate {
navbar: NavBar, navbar: NavBar,
about: Option<File>,
socials_exists: bool, socials_exists: bool,
socials: Vec<File>, socials: Vec<File>,
@ -126,31 +123,25 @@ fn remove_paragraphs(list: &mut [File]) {
#[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 = "data/contacts";
let ext = ".md"; let ext = ".md";
// Get about
let about = read_file(
&format!("{contacts_dir}/about.md"),
&TypeFileMetadata::Generic,
);
let socials_dir = "socials"; let socials_dir = "socials";
let mut socials = glob(&format!("{contacts_dir}/{socials_dir}/*{ext}")) let mut socials = glob(&format!("{contacts_dir}/{socials_dir}/*{ext}"))
.unwrap() .unwrap()
.map(|e| read_file(&e.unwrap().to_string_lossy(), &TypeFileMetadata::Contact).unwrap()) .map(|e| read_file(&e.unwrap().to_string_lossy(), TypeFileMetadata::Contact).unwrap())
.collect::<Vec<File>>(); .collect::<Vec<File>>();
let forges_dir = "forges"; let forges_dir = "forges";
let mut forges = glob(&format!("{contacts_dir}/{forges_dir}/*{ext}")) let mut forges = glob(&format!("{contacts_dir}/{forges_dir}/*{ext}"))
.unwrap() .unwrap()
.map(|e| read_file(&e.unwrap().to_string_lossy(), &TypeFileMetadata::Contact).unwrap()) .map(|e| read_file(&e.unwrap().to_string_lossy(), TypeFileMetadata::Contact).unwrap())
.collect::<Vec<File>>(); .collect::<Vec<File>>();
let others_dir = "others"; let others_dir = "others";
let mut others = glob(&format!("{contacts_dir}/{others_dir}/*{ext}")) let mut others = glob(&format!("{contacts_dir}/{others_dir}/*{ext}"))
.unwrap() .unwrap()
.map(|e| read_file(&e.unwrap().to_string_lossy(), &TypeFileMetadata::Contact).unwrap()) .map(|e| read_file(&e.unwrap().to_string_lossy(), TypeFileMetadata::Contact).unwrap())
.collect::<Vec<File>>(); .collect::<Vec<File>>();
// Remove paragraphs in custom statements // Remove paragraphs in custom statements
@ -165,8 +156,6 @@ fn build_page(config: Config) -> String {
contact: true, contact: true,
..NavBar::default() ..NavBar::default()
}, },
about,
socials_exists: !socials.is_empty(), socials_exists: !socials.is_empty(),
socials, socials,
@ -176,15 +165,10 @@ fn build_page(config: Config) -> String {
others_exists: !others.is_empty(), others_exists: !others.is_empty(),
others, others,
}, },
InfosPage { Infos {
title: Some("Contacts".into()), page_title: Some("Contacts".into()),
desc: Some(format!("Réseaux d'{}", config.fc.name.unwrap_or_default())), page_desc: Some(format!("Réseaux d'{}", config.fc.name.unwrap_or_default())),
kw: Some(make_kw(&[ page_kw: make_kw(&["réseaux sociaux", "email", "contact", "linktree"]),
"réseaux sociaux",
"email",
"contact",
"linktree",
])),
}, },
) )
} }

View file

@ -6,14 +6,14 @@ use crate::{
github::{fetch_pr, ProjectState}, github::{fetch_pr, ProjectState},
utils::{make_kw, Html}, utils::{make_kw, Html},
}, },
template::{InfosPage, NavBar}, template::{Infos, NavBar},
}; };
use actix_web::{get, web, Responder}; use actix_web::{get, web, Responder};
use cached::proc_macro::once; use cached::proc_macro::once;
use ramhorns::Content; use ramhorns::Content;
#[get("/contrib")] #[get("/contrib")]
pub async fn page(config: web::Data<Config>) -> impl Responder { async fn page(config: web::Data<Config>) -> impl Responder {
Html(build_page(config.get_ref().to_owned()).await) Html(build_page(config.get_ref().to_owned()).await)
} }
@ -58,33 +58,33 @@ async fn build_page(config: Config) -> String {
// Grouping PRs by projects // Grouping PRs by projects
let mut map: HashMap<&str, Vec<Pull>> = HashMap::new(); let mut map: HashMap<&str, Vec<Pull>> = HashMap::new();
for p in &projects { projects.iter().for_each(|p| {
let project = Pull { let project = Pull {
url: p.contrib_url.clone(), url: p.contrib_url.to_owned(),
id: p.id, id: p.id,
name_repo: p.name.clone(), name_repo: p.project.to_owned(),
title: p.title.clone(), title: p.title.to_owned(),
state: p.status as u8, state: p.status as u8,
}; };
let project_name = p.name.as_str(); let project_name = p.project.as_str();
if map.contains_key(project_name) { if map.contains_key(project_name) {
map.entry(project_name).and_modify(|v| v.push(project)); map.entry(project_name).and_modify(|v| v.push(project));
} else { } else {
data.push(Project { data.push(Project {
name: project_name.into(), name: project_name.into(),
url: p.url.clone(), url: p.project_url.to_owned(),
pulls_merged: Vec::new(), pulls_merged: Vec::new(),
pulls_closed: Vec::new(), pulls_closed: Vec::new(),
pulls_open: Vec::new(), pulls_open: Vec::new(),
}); });
map.insert(project_name, vec![project]); map.insert(project_name, vec![project]);
} }
} });
// Distributes each PR in the right vector // Distributes each PR in the right vector
for d in &mut data { data.iter_mut().for_each(|d| {
map.get(d.name.as_str()).unwrap().iter().for_each(|p| { map.get(d.name.as_str()).unwrap().iter().for_each(|p| {
let state = p.state.try_into().unwrap(); let state = p.state.into();
match state { match state {
ProjectState::Closed => d.pulls_closed.push(p.to_owned()), ProjectState::Closed => d.pulls_closed.push(p.to_owned()),
ProjectState::Merged => d.pulls_merged.push(p.to_owned()), ProjectState::Merged => d.pulls_merged.push(p.to_owned()),
@ -94,14 +94,14 @@ async fn build_page(config: Config) -> String {
let mut name: Vec<char> = d.name.replace('-', " ").chars().collect(); let mut name: Vec<char> = d.name.replace('-', " ").chars().collect();
name[0] = name[0].to_uppercase().next().unwrap(); name[0] = name[0].to_uppercase().next().unwrap();
d.name = name.into_iter().collect(); d.name = name.into_iter().collect();
} });
// Ascending order by pulls IDs // Ascending order by pulls IDs
for d in &mut data { data.iter_mut().for_each(|d| {
d.pulls_closed.reverse(); d.pulls_closed.reverse();
d.pulls_merged.reverse(); d.pulls_merged.reverse();
d.pulls_open.reverse(); d.pulls_open.reverse();
} });
// Ascending order by number of pulls // Ascending order by number of pulls
data.sort_by(|a, b| { data.sort_by(|a, b| {
@ -116,26 +116,26 @@ async fn build_page(config: Config) -> String {
error: false, error: false,
projects: Some( projects: Some(
data.iter() data.iter()
.filter(|&p| !p.pulls_merged.is_empty())
.cloned() .cloned()
.filter(|p| !p.pulls_merged.is_empty())
.collect(), .collect(),
), ),
waiting: Some( waiting: Some(
data.iter() data.iter()
.filter(|&p| !p.pulls_open.is_empty())
.cloned() .cloned()
.filter(|p| !p.pulls_open.is_empty())
.collect(), .collect(),
), ),
closed: Some( closed: Some(
data.iter() data.iter()
.filter(|&p| !p.pulls_closed.is_empty())
.cloned() .cloned()
.filter(|p| !p.pulls_closed.is_empty())
.collect(), .collect(),
), ),
} }
} }
Err(e) => { Err(e) => {
eprintln!("{e}"); eprintln!("{}", e);
PortfolioTemplate { PortfolioTemplate {
navbar, navbar,
@ -150,13 +150,13 @@ async fn build_page(config: Config) -> String {
config.tmpl.render( config.tmpl.render(
"contrib.html", "contrib.html",
data, data,
InfosPage { Infos {
title: Some("Mes contributions".into()), page_title: Some("Mes contributions".into()),
desc: Some(format!( page_desc: Some(format!(
"Contributions d'{} à GitHub", "Contributions d'{} à GitHub",
config.fc.name.unwrap_or_default() config.fc.name.unwrap_or_default()
)), )),
kw: Some(make_kw(&[ page_kw: make_kw(&[
"github", "github",
"contributions", "contributions",
"open source", "open source",
@ -164,7 +164,7 @@ async fn build_page(config: Config) -> String {
"portfolio", "portfolio",
"projets", "projets",
"code", "code",
])), ]),
}, },
) )
} }

View file

@ -1,18 +1,16 @@
use std::path::Path; use std::path::Path;
use actix_web::{get, web, Responder}; use actix_web::{get, web, Responder};
use cached::proc_macro::cached;
use ramhorns::Content; use ramhorns::Content;
use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
config::Config, config::Config,
misc::{ misc::{
markdown::{File, TypeFileMetadata}, markdown::{read_file, File, TypeFileMetadata},
utils::{make_kw, read_file, Html}, utils::{make_kw, Html},
}, },
template::{InfosPage, NavBar}, template::{Infos, NavBar},
}; };
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -21,8 +19,8 @@ pub struct PathRequest {
} }
#[get("/cours")] #[get("/cours")]
pub async fn page(info: web::Query<PathRequest>, config: web::Data<Config>) -> impl Responder { async fn page(info: web::Query<PathRequest>, config: web::Data<Config>) -> impl Responder {
Html(build_page(&info, config.get_ref().to_owned())) Html(build_page(info, config.get_ref().to_owned()))
} }
#[derive(Content, Debug)] #[derive(Content, Debug)]
@ -32,52 +30,36 @@ struct CoursTemplate {
content: Option<File>, content: Option<File>,
} }
#[derive(Clone, Debug, Serialize)] #[derive(Debug, Serialize)]
struct FileNode { struct FileNode {
name: String, name: String,
is_dir: bool, is_dir: bool,
children: Vec<FileNode>, children: Vec<FileNode>,
} }
#[cached] /// Build the filetree
fn compile_patterns(exclusion_list: Vec<String>) -> Vec<Regex> { fn get_filetree(dir_path: &str, exclusion_list: &[&str]) -> FileNode {
exclusion_list let entries = std::fs::read_dir(dir_path).unwrap();
.iter()
.map(|pattern| Regex::new(pattern).unwrap())
.collect()
}
fn get_filetree(dir_path: &str, exclusion_patterns: &Vec<Regex>) -> FileNode { let mut children = Vec::new();
let children = std::fs::read_dir(dir_path) for entry in entries.filter_map(Result::ok) {
.unwrap()
.filter_map(Result::ok)
.filter_map(|entry| {
let entry_path = entry.path(); let entry_path = entry.path();
let entry_name = entry_path.file_name()?.to_string_lossy().to_string(); let entry_name = entry_path.file_name().and_then(|n| n.to_str()).unwrap();
// Exclude element with the exclusion_list
if exclusion_patterns.iter().any(|re| re.is_match(&entry_name)) {
return None;
}
// We should support regex?
if !exclusion_list.contains(&entry_name) {
let filename = entry_name.to_string();
if entry_path.is_file() { if entry_path.is_file() {
Some(FileNode { children.push(FileNode {
name: entry_name, name: filename,
is_dir: false, is_dir: false,
children: vec![], children: vec![],
}) });
} else { } else {
// Exclude empty directories children.push(get_filetree(entry_path.to_str().unwrap(), exclusion_list));
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 { FileNode {
name: Path::new(dir_path) name: Path::new(dir_path)
@ -94,29 +76,30 @@ fn get_filetree(dir_path: &str, exclusion_patterns: &Vec<Regex>) -> FileNode {
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: &[&str],
) -> Option<File> { ) -> Option<File> {
let filename = path.q.as_ref().map_or("index.md", |q| q); let filename = match &path.q {
Some(q) => q,
None => "index.md",
};
// We should support regex? // We should support regex?
if exclusion_list if exclusion_list
.iter() .iter()
.any(|excluded_term| filename.contains(excluded_term.as_str())) .any(|&excluded_term| filename.contains(excluded_term))
{ {
return None; return None;
} }
read_file( read_file(&format!("{cours_dir}/{filename}"), TypeFileMetadata::Cours)
&format!("{cours_dir}/{filename}"),
&TypeFileMetadata::Generic,
)
} }
fn build_page(info: &web::Query<PathRequest>, config: Config) -> String { // #[once(time = 60)]
// TODO: Uncomment before release
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_list = [];
let exclusion_patterns = compile_patterns(exclusion_list.clone()); let filetree = get_filetree(cours_dir, &exclusion_list);
let filetree = get_filetree(cours_dir, &exclusion_patterns);
config.tmpl.render( config.tmpl.render(
"cours.html", "cours.html",
@ -126,12 +109,12 @@ 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),
}, },
InfosPage { Infos {
title: Some("Cours".into()), page_title: Some("Cours".into()),
desc: Some("Cours à l'univ".into()), page_desc: Some("Cours à l'univ".into()),
kw: Some(make_kw(&[ page_kw: make_kw(&[
"cours", "cours",
"études", "études",
"université", "université",
@ -139,7 +122,7 @@ fn build_page(info: &web::Query<PathRequest>, config: Config) -> String {
"master", "master",
"notes", "notes",
"digital garden", "digital garden",
])), ]),
}, },
) )
} }

View file

@ -1,7 +1,7 @@
use actix_web::{get, Responder}; use actix_web::{get, Responder};
#[get("/cv")] #[get("/cv")]
pub async fn page() -> impl Responder { async fn page() -> impl Responder {
// Génération du CV depuis un fichier externe TOML ? // Génération du CV depuis un fichier externe TOML ?
// Cf. https://github.com/sinaatalay/rendercv // Cf. https://github.com/sinaatalay/rendercv
// Faudrait une version HTML, et une version PDF // Faudrait une version HTML, et une version PDF

View file

@ -1,7 +1,7 @@
use actix_web::{get, Responder}; use actix_web::{get, Responder};
#[get("/gaming")] #[get("/gaming")]
pub async fn page() -> impl Responder { async fn page() -> impl Responder {
// Liste de mes comptes gaming, de mon setup, de mes configs de jeu, etc. // Liste de mes comptes gaming, de mon setup, de mes configs de jeu, etc.
actix_web::web::Redirect::to("/") actix_web::web::Redirect::to("/")
} }

View file

@ -4,72 +4,23 @@ use ramhorns::Content;
use crate::{ use crate::{
config::Config, config::Config,
misc::{ misc::utils::{make_kw, Html},
markdown::{File, TypeFileMetadata}, template::{Infos, NavBar},
utils::{make_kw, read_file, Html},
},
template::{InfosPage, NavBar},
}; };
#[get("/")] #[get("/")]
pub async fn page(config: web::Data<Config>) -> impl Responder { 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()))
} }
#[derive(Content, Debug)] #[derive(Content, Debug)]
struct IndexTemplate { struct IndexTemplate {
navbar: NavBar, navbar: NavBar,
name: String, fullname: String,
pronouns: Option<String>,
file: Option<File>,
avatar: String,
avatar_caption: String,
avatar_style: StyleAvatar,
}
#[derive(Content, Debug, Default)]
struct StyleAvatar {
round: bool,
square: bool,
} }
#[once(time = 60)] #[once(time = 60)]
fn build_page(config: Config) -> String { fn build_page(config: Config) -> String {
let mut file = read_file(
&format!("{}/index.md", config.locations.data_dir),
&TypeFileMetadata::Index,
);
// Default values
let mut name = config.fc.fullname.clone().unwrap_or_default();
let mut pronouns = None;
let mut avatar = "/icons/apple-touch-icon.png".to_owned();
let mut avatar_caption = "EWP avatar".to_owned();
let mut avatar_style = StyleAvatar {
round: true,
square: false,
};
if let Some(f) = &file {
if let Some(m) = &f.metadata.info.index {
name = m.name.clone().unwrap_or(name);
avatar = m.avatar.clone().unwrap_or(avatar);
m.pronouns.clone_into(&mut pronouns);
avatar_caption = m.avatar_caption.clone().unwrap_or(avatar_caption);
if let Some(style) = m.avatar_style.clone() {
if style.trim() == "square" {
avatar_style = StyleAvatar {
square: true,
..StyleAvatar::default()
}
}
}
}
} else {
file = read_file("README.md", &TypeFileMetadata::Generic);
}
config.tmpl.render( config.tmpl.render(
"index.html", "index.html",
IndexTemplate { IndexTemplate {
@ -77,17 +28,16 @@ fn build_page(config: Config) -> String {
index: true, index: true,
..NavBar::default() ..NavBar::default()
}, },
file, fullname: config
name, .fc
pronouns, .fullname
avatar, .to_owned()
avatar_caption, .unwrap_or("Fullname".to_owned()),
avatar_style,
}, },
InfosPage { Infos {
title: config.fc.fullname, page_title: config.fc.fullname,
desc: Some("Page principale".into()), page_desc: Some("Page principale".into()),
kw: Some(make_kw(&["index", "étudiant", "accueil"])), page_kw: make_kw(&["index", "étudiant"]),
}, },
) )
} }

View file

@ -1,7 +1,7 @@
use actix_web::{get, Responder}; use actix_web::{get, Responder};
#[get("/memorial")] #[get("/memorial")]
pub async fn page() -> impl Responder { async fn page() -> impl Responder {
// Memorial? J'espere ne jamais faire cette page lol // Memorial? J'espere ne jamais faire cette page lol
actix_web::web::Redirect::to("/") actix_web::web::Redirect::to("/")
} }

View file

@ -5,7 +5,7 @@ use ramhorns::Content;
use crate::{ use crate::{
config::Config, config::Config,
misc::utils::{get_url, Html}, misc::utils::{get_url, Html},
template::{InfosPage, NavBar}, template::{Infos, NavBar},
}; };
pub async fn page(config: web::Data<Config>) -> impl Responder { pub async fn page(config: web::Data<Config>) -> impl Responder {
@ -28,9 +28,9 @@ fn build_page(config: Config) -> String {
www: get_url(config.fc.clone()), www: get_url(config.fc.clone()),
onion: config.fc.onion, onion: config.fc.onion,
}, },
InfosPage { Infos {
desc: Some("Une page perdu du web".into()), page_desc: Some("Une page perdu du web".into()),
..InfosPage::default() ..Infos::default()
}, },
) )
} }

View file

@ -6,21 +6,20 @@ use ramhorns::Content;
use crate::{ use crate::{
config::Config, config::Config,
misc::{ misc::{
markdown::{File, TypeFileMetadata}, markdown::{read_file, File, TypeFileMetadata},
utils::{make_kw, read_file, Html}, utils::{make_kw, Html},
}, },
template::{InfosPage, NavBar}, template::{Infos, NavBar},
}; };
#[get("/portfolio")] #[get("/portfolio")]
pub async fn page(config: web::Data<Config>) -> impl Responder { 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()))
} }
#[derive(Content, Debug)] #[derive(Content, Debug)]
struct PortfolioTemplate<'a> { struct PortfolioTemplate<'a> {
navbar: NavBar, navbar: NavBar,
about: Option<File>,
location_apps: Option<&'a str>, location_apps: Option<&'a str>,
apps: Option<Vec<File>>, apps: Option<Vec<File>>,
archived_apps: Option<Vec<File>>, archived_apps: Option<Vec<File>>,
@ -30,32 +29,25 @@ struct PortfolioTemplate<'a> {
#[once(time = 60)] #[once(time = 60)]
fn build_page(config: Config) -> String { fn build_page(config: Config) -> String {
let projects_dir = format!("{}/projects", config.locations.data_dir); let projects_dir = "data/projects";
let apps_dir = format!("{projects_dir}/apps");
let ext = ".md"; let ext = ".md";
// Get about
let about = read_file(
&format!("{projects_dir}/about.md"),
&TypeFileMetadata::Generic,
);
// Get apps // Get apps
let apps = glob(&format!("{apps_dir}/*{ext}")) let apps = glob(&format!("{projects_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(), TypeFileMetadata::Portfolio).unwrap())
.collect::<Vec<File>>(); .collect::<Vec<File>>();
let appdata = if apps.is_empty() { let appdata = if apps.is_empty() {
(None, Some(apps_dir.as_str())) (None, Some(projects_dir))
} else { } else {
(Some(apps), None) (Some(apps), None)
}; };
// Get archived apps // Get archived apps
let archived_apps = glob(&format!("{apps_dir}/archive/*{ext}")) let archived_apps = glob(&format!("{projects_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(), TypeFileMetadata::Portfolio).unwrap())
.collect::<Vec<File>>(); .collect::<Vec<File>>();
let archived_appdata = if archived_apps.is_empty() { let archived_appdata = if archived_apps.is_empty() {
@ -71,27 +63,26 @@ fn build_page(config: Config) -> String {
portfolio: true, portfolio: true,
..NavBar::default() ..NavBar::default()
}, },
about,
apps: appdata.0, apps: appdata.0,
location_apps: appdata.1, location_apps: appdata.1,
archived_apps: archived_appdata.0, archived_apps: archived_appdata.0,
archived_apps_exists: archived_appdata.1, archived_apps_exists: archived_appdata.1,
err_msg: "is empty", err_msg: "is empty",
}, },
InfosPage { Infos {
title: Some("Portfolio".into()), page_title: Some("Portfolio".into()),
desc: Some(format!( page_desc: Some(format!(
"Portfolio d'{}", "Portfolio d'{}",
config.fc.name.unwrap_or_default() config.fc.name.unwrap_or_default()
)), )),
kw: Some(make_kw(&[ page_kw: make_kw(&[
"développeur", "développeur",
"portfolio", "portfolio",
"projets", "projets",
"programmation", "programmation",
"applications", "applications",
"code", "code",
])), ]),
}, },
) )
} }

View file

@ -1,7 +1,7 @@
use actix_web::{get, Responder}; use actix_web::{get, Responder};
#[get("/setup")] #[get("/setup")]
pub async fn page() -> impl Responder { async fn page() -> impl Responder {
// Explication de l'histoire de par exemple wiki/cat et le follow up // Explication de l'histoire de par exemple wiki/cat et le follow up
// avec les futures video youtube probablement un shortcut // avec les futures video youtube probablement un shortcut
// vers un billet de blog // vers un billet de blog

View file

@ -4,11 +4,11 @@ use cached::proc_macro::once;
use crate::{ use crate::{
config::Config, config::Config,
misc::utils::{make_kw, Html}, misc::utils::{make_kw, Html},
template::InfosPage, template::Infos,
}; };
#[get("/web3")] #[get("/web3")]
pub async fn page(config: web::Data<Config>) -> impl Responder { 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()))
} }
@ -17,10 +17,10 @@ fn build_page(config: Config) -> String {
config.tmpl.render( config.tmpl.render(
"web3.html", "web3.html",
(), (),
InfosPage { Infos {
title: Some("Mylloon".into()), page_title: Some("Mylloon".into()),
desc: Some("Coin reculé de l'internet".into()), page_desc: Some("Coin reculé de l'internet".into()),
kw: Some(make_kw(&["web3", "blockchain", "nft", "ai"])), page_kw: make_kw(&["web3", "blockchain", "nft", "ai"]),
}, },
) )
} }

View file

@ -15,17 +15,15 @@ pub struct Template {
/// Structure used by /routes/*.rs /// Structure used by /routes/*.rs
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct InfosPage { pub struct Infos {
/// Title /// Title
pub title: Option<String>, pub page_title: Option<String>,
/// Description /// Description
pub desc: Option<String>, pub page_desc: Option<String>,
/// Keywords /// Keywords
pub kw: Option<String>, pub page_kw: Option<String>,
} }
#[allow(clippy::struct_excessive_bools)]
/// Information on what page the user is currently
#[derive(Content, Debug, Default)] #[derive(Content, Debug, Default)]
pub struct NavBar { pub struct NavBar {
pub index: bool, pub index: bool,
@ -38,7 +36,7 @@ pub struct NavBar {
/// Final structure given to template /// Final structure given to template
#[derive(Content, Debug)] #[derive(Content, Debug)]
struct DataPage<T> { struct Data<T> {
/// App name /// App name
app_name: String, app_name: String,
/// App URL /// App URL
@ -56,16 +54,16 @@ struct DataPage<T> {
} }
impl Template { impl Template {
pub fn render<C: Content>(&self, template: &str, data: C, info: InfosPage) -> String { pub fn render<C: Content>(&self, template: &str, data: C, info: Infos) -> String {
let mut templates: Ramhorns = Ramhorns::lazy(&self.directory).unwrap(); let mut templates: Ramhorns = Ramhorns::lazy(&self.directory).unwrap();
let tplt = templates.from_file(template).unwrap(); let tplt = templates.from_file(template).unwrap();
tplt.render(&DataPage { tplt.render(&Data {
app_name: self.app_name.clone(), app_name: self.app_name.to_owned(),
url: self.url.clone(), url: self.url.to_owned(),
page_title: info.title, page_title: info.page_title,
page_desc: info.desc, page_desc: info.page_desc,
page_kw: info.kw, page_kw: info.page_kw,
page_author: self.name.clone(), page_author: self.name.clone(),
data, data,
}) })

BIN
static/badges/cat.gif (Stored with Git LFS)

Binary file not shown.

BIN
static/badges/palestine.png (Stored with Git LFS)

Binary file not shown.

View file

@ -21,7 +21,7 @@
} }
/* Title */ /* Title */
main h1 { h1 {
margin-bottom: 0; margin-bottom: 0;
} }
@ -60,34 +60,20 @@ main span {
} }
/* Card text */ /* Card text */
main li h2, li h2,
main li p { li p {
margin: 0px; margin: 0px;
padding-top: 5px; padding-top: 5px;
} }
/* Card titles */ /* Card titles */
main li h2, li h2 {
main li h2 a {
color: var(--title-color); color: var(--title-color);
font-size: var(--font-size); font-size: var(--font-size);
} }
main li h2 a {
text-decoration: none;
}
main li h2 a {
text-decoration: none;
}
main li h2 a:hover {
opacity: initial;
text-decoration: underline;
}
/* Card descriptions */ /* Card descriptions */
main li p { li p {
font-size: calc(var(--font-size) - 2px); font-size: calc(var(--font-size) - 2px);
} }

View file

@ -66,7 +66,7 @@ header > ul:last-of-type li {
/* Post */ /* Post */
main { main {
margin: 0; margin: 0;
padding-block: 0; padding: 0;
max-width: 100%; max-width: 100%;
} }
@ -198,34 +198,12 @@ table.hljs-ln {
font-size: calc(var(--font-size) * 0.8); font-size: calc(var(--font-size) * 0.8);
} }
/* Footnote */ /* Footnotes */
section.footnotes * { .footnotes a {
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; font-family: "Segoe UI", "Segoe UI Symbol", system-ui;
text-decoration: underline dotted; text-decoration: underline dotted;
} }
/* Footnotes block separation from article */
section.footnotes {
margin: 3px;
border-top: 2px dotted var(--separator-color);
}
/* Mermaid diagrams */ /* Mermaid diagrams */
pre:has(code.language-mermaid) { pre:has(code.language-mermaid) {
text-align: center; text-align: center;
@ -246,36 +224,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;
}
}

View file

@ -1,18 +1,9 @@
:root {
color-scheme: light dark;
/* Global parameters */
--font-size: 1.15rem;
--font-family: "Segoe UI", "Segoe UI Emoji", "Segoe UI Symbol", system-ui;
}
/* Parameters light */ /* Parameters light */
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
:root { :root {
--background: #f1f1f1; --background: #f1f1f1;
--font-color: #18181b; --font-color: #18181b;
--link-color: #df5a9c; --link-color: #df5a9c;
--selection-color: #c5c5c560;
} }
} }
@ -22,6 +13,11 @@
--background: #171e26; --background: #171e26;
--font-color: #bcbcc5; --font-color: #bcbcc5;
--link-color: #ff80bf; --link-color: #ff80bf;
--selection-color: #c5c5c530;
} }
} }
/* Global parameters */
:root {
--font-size: 1.15rem;
--font-family: "Segoe UI", "Segoe UI Emoji", "Segoe UI Symbol", system-ui;
}

View file

@ -1,4 +1,4 @@
main h2 { h2 {
padding-left: 1rem; padding-left: 1rem;
} }
@ -8,7 +8,7 @@ main li {
} }
main h1, main h1,
main h2 { h2 {
font-weight: 800; font-weight: 800;
} }
@ -22,7 +22,7 @@ main a:hover {
text-decoration: underline; text-decoration: underline;
} }
main p { p {
margin: 0; margin: 0;
} }

View file

@ -45,9 +45,3 @@ aside li.directory {
visibility: hidden; visibility: hidden;
} }
} }
main img {
max-width: 100%;
display: block;
margin: auto;
}

View file

@ -38,6 +38,7 @@
#avatar { #avatar {
width: calc(var(--font-size) * 5); width: calc(var(--font-size) * 5);
border-radius: 50%;
float: right; float: right;
} }
@ -63,8 +64,8 @@ h1 {
opacity: 1; opacity: 1;
} }
#friends a:not(h1 > a) { #friends a {
padding-right: 5px; padding-right: 10px;
} }
#friends h1 { #friends h1 {

View file

@ -69,11 +69,3 @@ p[data-lang="gdscript"]::before {
p[data-lang="gdscript"]::after { p[data-lang="gdscript"]::after {
content: "GDScript"; content: "GDScript";
} }
p[data-lang="tex"]::before {
background-color: #3d6117;
}
p[data-lang="tex"]::after {
content: "TeX";
}

View file

@ -20,7 +20,7 @@ main {
} }
/* List */ /* List */
main ul:not(ul ul) { main ul {
padding: 0; padding: 0;
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
@ -28,17 +28,13 @@ main ul:not(ul ul) {
/* breakpoint */ /* breakpoint */
@media only screen and (max-width: 740px) { @media only screen and (max-width: 740px) {
main ul:not(ul ul) { main ul {
grid-template-columns: none; grid-template-columns: none;
} }
main li:not(ul ul > li) {
grid-column: inherit !important;
}
} }
/* Card */ /* Card */
main li:not(ul ul > li) { main li {
display: flex; display: flex;
border-radius: 8px; border-radius: 8px;
@ -54,16 +50,12 @@ main li:not(ul ul > li) {
margin-inline: 5px; margin-inline: 5px;
} }
main li:not(ul ul > li):nth-child(odd):last-child { main li:hover {
grid-column: span 2;
}
main li:hover:not(ul ul > li) {
background: color-mix(in srgb, var(--background) 40%, var(--extreme)); background: color-mix(in srgb, var(--background) 40%, var(--extreme));
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.3); box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.3);
} }
main li[role="button"]:hover:not(ul ul > li) { main li[role="button"]:hover {
cursor: pointer; cursor: pointer;
} }
@ -99,17 +91,10 @@ span {
/* Element text */ /* Element text */
div p, div p,
div a, div a {
ul ul li {
font-size: var(--font-size-card); font-size: var(--font-size-card);
} }
/* Element list */
ul ul {
list-style: initial;
padding: 1em;
}
/* Element language */ /* Element language */
p[data-lang] { p[data-lang] {
margin: 0; margin: 0;

View file

@ -4,10 +4,6 @@ html {
font-family: var(--font-family); font-family: var(--font-family);
} }
::selection {
background-color: var(--selection-color);
}
body, body,
a { a {
color: var(--font-color); color: var(--font-color);
@ -71,26 +67,7 @@ header nav a:hover {
} }
@media print { @media print {
/* Hide navigation header */
header nav { header nav {
display: none; visibility: hidden;
}
/* Better colors for paper */
html {
color: black;
background-color: white;
}
/* Add links */
a:not(:where([href^="#"], [href^="/"])):not(:has(img))::after {
content: " (" attr(href) ")";
display: inline-block;
white-space: pre;
color: mediumblue;
}
a {
text-underline-position: under;
} }
} }

View file

@ -1,3 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240" version="1.0" viewBox="0 0 180 180"> <?xml version="1.0" standalone="no"?>
<path d="M38 2c0 4.7 5.6 11.7 8.5 10.7 1.9-.8 1.9-4.7.1-9.2C45.3.4 44.7 0 41.6 0c-3 0-3.6.4-3.6 2zM122 6c-3.4 3.4-1.7 12.7 2.6 14.4 2.7 1 3.9-1 3.6-6.5-.3-8.9-2.5-11.6-6.2-7.9zM78.9 7.1C76.7 11.2 80.1 19 84 19c2.5 0 3.2-2.6 1.9-7.4-1.8-6.8-4.7-8.7-7-4.5zM0 11.7c0 5.5.3 6.4 2.6 8.2 3.3 2.6 4.1 2.6 5.4 0 1.5-2.8-.1-6.6-4.6-11L0 5.6v6.1zM156.3 11.4c-2 4.3-1.1 8.6 1.8 8.6 2.5 0 5.3-6.2 4.5-9.6-.9-3.6-4.3-3.1-6.3 1zM34.3 40.5c-4.7 3.3-3.7 11.6 1.9 15.8 3.6 2.7 7.4 1.6 9.5-2.7 4.1-8.7-4.2-18.2-11.4-13.1zM131.3 41.8c-1.2.9-2.7 3.2-3.4 4.9-2.3 6.9 5.2 15.3 13.8 15.3 8.4 0 11.9-7 7.6-15.3-3.1-6-13.1-8.7-18-4.9zM86.2 80.2c-1.9 1.9-1.4 4.6 1.6 9.1 1.6 2.3 3.2 5 3.5 6 .4.9 1.8 2.2 3.2 2.9 2.2 1 3 .8 5.3-.9 1.5-1.1 2.8-3.2 3-4.7.9-7.3-12-17-16.6-12.4zM142.8 102.1c-2.9 1.6-2.1 6.3 2.7 15.9 8.2 16.4 6.5 18.9-17.3 24.7-19.2 4.7-43.4 2.9-64.6-4.8-10.8-3.9-16-8.3-18.1-15.2-1.6-5.4-2.7-6.7-6.1-6.7-3.2 0-5.4 3.1-5.4 7.6 0 6.3 5 13.8 12.2 18.4 17.8 11.6 54.6 17.8 78.4 13.4 3-.6 3.4-.2 8.8 7.5 6.1 8.6 8.5 10.6 11.6 9.6 4.3-1.4 3.3-7.3-2.8-15.2-1.3-1.8-2.2-3.9-1.9-4.6.3-.8 3.3-2.4 6.8-3.7 11.1-4.2 15.9-11.1 14.6-21.1-.7-5.4-9.1-22.3-12.3-24.8-3-2.3-4.1-2.5-6.6-1z"/> <svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="180.000000pt" height="180.000000pt" viewBox="0 0 180.000000 180.000000" preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,180.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none">
<path d="M380 1780 c0 -47 56 -117 85 -107 19 8 19 47 1 92 -13 31 -19 35 -50 35 -30 0 -36 -4 -36 -20z"/>
<path d="M1220 1740 c-34 -34 -17 -127 26 -144 27 -10 39 10 36 65 -3 89 -25 116 -62 79z"/>
<path d="M789 1729 c-22 -41 12 -119 51 -119 25 0 32 26 19 74 -18 68 -47 87 -70 45z"/>
<path d="M0 1683 c0 -55 3 -64 26 -82 33 -26 41 -26 54 0 15 28 -1 66 -46 110 l-34 33 0 -61z"/>
<path d="M1563 1686 c-20 -43 -11 -86 18 -86 25 0 53 62 45 96 -9 36 -43 31 -63 -10z"/>
<path d="M343 1395 c-47 -33 -37 -116 19 -158 36 -27 74 -16 95 27 41 87 -42 182 -114 131z"/>
<path d="M1313 1382 c-12 -9 -27 -32 -34 -49 -23 -69 52 -153 138 -153 84 0 119 70 76 153 -31 60 -131 87 -180 49z"/>
<path d="M862 998 c-19 -19 -14 -46 16 -91 16 -23 32 -50 35 -60 4 -9 18 -22 32 -29 22 -10 30 -8 53 9 15 11 28 32 30 47 9 73 -120 170 -166 124z"/>
<path d="M1428 779 c-29 -16 -21 -63 27 -159 82 -164 65 -189 -173 -247 -192 -47 -434 -29 -646 48 -108 39 -160 83 -181 152 -16 54 -27 67 -61 67 -32 0 -54 -31 -54 -76 0 -63 50 -138 122 -184 178 -116 546 -178 784 -134 30 6 34 2 88 -75 61 -86 85 -106 116 -96 43 14 33 73 -28 152 -13 18 -22 39 -19 46 3 8 33 24 68 37 111 42 159 111 146 211 -7 54 -91 223 -123 248 -30 23 -41 25 -66 10z"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,15 @@
{
"name": "Site Anri K.",
"short_name": "Site Anri K.",
"icons": [
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
],
"theme_color": "#2a2424",
"background_color": "#2a2424",
"start_url": "https://www.mylloon.fr/",
"display": "standalone"
}

View file

@ -80,61 +80,6 @@ const deepestNodeOpened = (path, options) => {
} }
}; };
const svgDarkTheme = () => {
for (const item of document.getElementsByTagName("img")) {
if (!item.src.startsWith("data:image/svg+xml;base64,")) {
// Exclude image who aren't SVG and base64 encoded
break;
}
/** Convert to grayscale */
const colorToGrayscale = (color) => {
return 0.3 * color.r + 0.59 * color.g + 0.11 * color.b;
};
/** Extract color using canvas2d */
const extractColors = (image) => {
const canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0);
const imageData = ctx.getImageData(
0,
0,
Math.max(1, canvas.width),
Math.max(1, canvas.height)
);
const pixelData = imageData.data;
const colors = [];
for (let i = 0; i < pixelData.length; i += 4) {
if (pixelData[i + 3] > 0) {
colors.push({
r: pixelData[i],
g: pixelData[i + 1],
b: pixelData[i + 2],
});
}
}
return colors;
};
// Extract colors
const colors = extractColors(item);
// Calculate the average grayscale value
const grayscaleValues = colors.map(colorToGrayscale);
const totalGrayscale = grayscaleValues.reduce((acc, val) => acc + val, 0);
const averageGrayscale = totalGrayscale / grayscaleValues.length;
if (averageGrayscale < 128) {
item.style = "filter: invert(1);";
}
}
};
window.addEventListener("load", () => { window.addEventListener("load", () => {
// Build the filetree // Build the filetree
const fileTreeElement = document.getElementsByTagName("aside")[0]; const fileTreeElement = document.getElementsByTagName("aside")[0];
@ -148,9 +93,7 @@ window.addEventListener("load", () => {
dataElement.remove(); dataElement.remove();
// Open nested openeded directories // Open nested openeded directories
const infoURL = window.location.href.split("?"); const fullpath = window.location.href.split("?")[1].substring(2);
if (infoURL.length > 1) {
const fullpath = infoURL[1].substring(2);
const path = fullpath.substring(0, fullpath.lastIndexOf("/")); const path = fullpath.substring(0, fullpath.lastIndexOf("/"));
const last_openeded = deepestNodeOpened( const last_openeded = deepestNodeOpened(
path.split("/"), path.split("/"),
@ -158,10 +101,4 @@ window.addEventListener("load", () => {
); );
uncollapse(last_openeded); uncollapse(last_openeded);
}
// Fix SVG images in dark mode
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
svgDarkTheme();
}
}); });

View file

@ -6,63 +6,24 @@ class Tag {
} }
window.addEventListener("load", () => { window.addEventListener("load", () => {
const clipping_text = ` const tags = [
new Tag("Comment ça marche un PC 😵‍💫"),
new Tag("Idiot certifié"),
new Tag("undefined", "font-family: monospace"),
new Tag("/api/v1/love", "font-family: monospace"),
new Tag("Étudiant qui va rater son master"),
new Tag("Peak D2 sur Valo 🤡"),
new Tag(
"1312",
`
display: inline; display: inline;
background: linear-gradient(to bottom right, red 0%, red 50%, black 50%);
background-clip: text; background-clip: text;
-webkit-background-clip: text; /* Chromium fix */ -webkit-background-clip: text; /* Chromium fix */
color: transparent; color: transparent;
`;
const mono = "font-family: monospace";
const tags = [
new Tag("Comment fonctionne un PC 😵‍💫"),
new Tag("undefined", mono),
new Tag("/api/v1/love", mono),
new Tag("/api/v1/websites", mono),
new Tag("Peak D2 sur Valo 🤡"),
new Tag(
"0x520",
`
background: linear-gradient(to bottom right, red 0%, red 50%, black 50%);
${clipping_text}
text-shadow: 0px 0px 20px light-dark(transparent, var(--font-color));
` `
), ),
new Tag("Nul en CSS", "font-family: 'Comic Sans MS', TSCu_Comic, cursive"), new Tag("Nul en CSS", "font-family: 'Comic Sans MS', cursive"),
new Tag("anri k... caterpillar 🐛☝️"),
new Tag(
"Free Ukraine",
`
background: linear-gradient(to bottom, DodgerBlue 57%, gold 43%);
${clipping_text}
text-shadow: 0px 0px 20px light-dark(var(--font-color), transparent);
`
),
new Tag(
"Free Palestine",
`
background: conic-gradient(at 30% 60%, transparent 230deg, red 0, red 310deg, transparent 0),
linear-gradient(to bottom, black 45%, white 45%, white 67%, DarkGreen 67%);
${clipping_text}
text-shadow: 0px 0px 20px var(--font-color);
`
),
new Tag("School hater"),
new Tag("Stagiaire"),
new Tag("Rempli de malice"),
new Tag(
"#NouveauFrontPopulaire ✊",
`
background: linear-gradient(to bottom, #4fb26b 0%, #4fb26b 36%, \
#e62e35 36%, #e62e35 50%, \
#feeb25 50%, #feeb25 62%, \
#724e99 62%, #724e99 77%, \
#e73160 77%);
${clipping_text}
text-shadow: 0px 0px 20px light-dark(var(--font-color), transparent);
`
),
new Tag("s/centre/droite/g", mono),
]; ];
const random = Math.round(Math.random() * (tags.length - 1)); const random = Math.round(Math.random() * (tags.length - 1));

View file

@ -1,452 +0,0 @@
/*! `julia` grammar compiled for Highlight.js 11.9.0 */
(function(){
var hljsGrammar = (function () {
'use strict';
/*
Language: Julia
Description: Julia is a high-level, high-performance, dynamic programming language.
Author: Kenta Sato <bicycle1885@gmail.com>
Contributors: Alex Arslan <ararslan@comcast.net>, Fredrik Ekre <ekrefredrik@gmail.com>
Website: https://julialang.org
Category: scientific
*/
function julia(hljs) {
// Since there are numerous special names in Julia, it is too much trouble
// to maintain them by hand. Hence these names (i.e. keywords, literals and
// built-ins) are automatically generated from Julia 1.5.2 itself through
// the following scripts for each.
// ref: https://docs.julialang.org/en/v1/manual/variables/#Allowed-Variable-Names
const VARIABLE_NAME_RE = '[A-Za-z_\\u00A1-\\uFFFF][A-Za-z_0-9\\u00A1-\\uFFFF]*';
// # keyword generator, multi-word keywords handled manually below (Julia 1.5.2)
// import REPL.REPLCompletions
// res = String["in", "isa", "where"]
// for kw in collect(x.keyword for x in REPLCompletions.complete_keyword(""))
// if !(contains(kw, " ") || kw == "struct")
// push!(res, kw)
// end
// end
// sort!(unique!(res))
// foreach(x -> println("\'", x, "\',"), res)
const KEYWORD_LIST = [
'baremodule',
'begin',
'break',
'catch',
'ccall',
'const',
'continue',
'do',
'else',
'elseif',
'end',
'export',
'false',
'finally',
'for',
'function',
'global',
'if',
'import',
'in',
'isa',
'let',
'local',
'macro',
'module',
'quote',
'return',
'true',
'try',
'using',
'where',
'while',
];
// # literal generator (Julia 1.5.2)
// import REPL.REPLCompletions
// res = String["true", "false"]
// for compl in filter!(x -> isa(x, REPLCompletions.ModuleCompletion) && (x.parent === Base || x.parent === Core),
// REPLCompletions.completions("", 0)[1])
// try
// v = eval(Symbol(compl.mod))
// if !(v isa Function || v isa Type || v isa TypeVar || v isa Module || v isa Colon)
// push!(res, compl.mod)
// end
// catch e
// end
// end
// sort!(unique!(res))
// foreach(x -> println("\'", x, "\',"), res)
const LITERAL_LIST = [
'ARGS',
'C_NULL',
'DEPOT_PATH',
'ENDIAN_BOM',
'ENV',
'Inf',
'Inf16',
'Inf32',
'Inf64',
'InsertionSort',
'LOAD_PATH',
'MergeSort',
'NaN',
'NaN16',
'NaN32',
'NaN64',
'PROGRAM_FILE',
'QuickSort',
'RoundDown',
'RoundFromZero',
'RoundNearest',
'RoundNearestTiesAway',
'RoundNearestTiesUp',
'RoundToZero',
'RoundUp',
'VERSION|0',
'devnull',
'false',
'im',
'missing',
'nothing',
'pi',
'stderr',
'stdin',
'stdout',
'true',
'undef',
'π',
'',
];
// # built_in generator (Julia 1.5.2)
// import REPL.REPLCompletions
// res = String[]
// for compl in filter!(x -> isa(x, REPLCompletions.ModuleCompletion) && (x.parent === Base || x.parent === Core),
// REPLCompletions.completions("", 0)[1])
// try
// v = eval(Symbol(compl.mod))
// if (v isa Type || v isa TypeVar) && (compl.mod != "=>")
// push!(res, compl.mod)
// end
// catch e
// end
// end
// sort!(unique!(res))
// foreach(x -> println("\'", x, "\',"), res)
const BUILT_IN_LIST = [
'AbstractArray',
'AbstractChannel',
'AbstractChar',
'AbstractDict',
'AbstractDisplay',
'AbstractFloat',
'AbstractIrrational',
'AbstractMatrix',
'AbstractRange',
'AbstractSet',
'AbstractString',
'AbstractUnitRange',
'AbstractVecOrMat',
'AbstractVector',
'Any',
'ArgumentError',
'Array',
'AssertionError',
'BigFloat',
'BigInt',
'BitArray',
'BitMatrix',
'BitSet',
'BitVector',
'Bool',
'BoundsError',
'CapturedException',
'CartesianIndex',
'CartesianIndices',
'Cchar',
'Cdouble',
'Cfloat',
'Channel',
'Char',
'Cint',
'Cintmax_t',
'Clong',
'Clonglong',
'Cmd',
'Colon',
'Complex',
'ComplexF16',
'ComplexF32',
'ComplexF64',
'CompositeException',
'Condition',
'Cptrdiff_t',
'Cshort',
'Csize_t',
'Cssize_t',
'Cstring',
'Cuchar',
'Cuint',
'Cuintmax_t',
'Culong',
'Culonglong',
'Cushort',
'Cvoid',
'Cwchar_t',
'Cwstring',
'DataType',
'DenseArray',
'DenseMatrix',
'DenseVecOrMat',
'DenseVector',
'Dict',
'DimensionMismatch',
'Dims',
'DivideError',
'DomainError',
'EOFError',
'Enum',
'ErrorException',
'Exception',
'ExponentialBackOff',
'Expr',
'Float16',
'Float32',
'Float64',
'Function',
'GlobalRef',
'HTML',
'IO',
'IOBuffer',
'IOContext',
'IOStream',
'IdDict',
'IndexCartesian',
'IndexLinear',
'IndexStyle',
'InexactError',
'InitError',
'Int',
'Int128',
'Int16',
'Int32',
'Int64',
'Int8',
'Integer',
'InterruptException',
'InvalidStateException',
'Irrational',
'KeyError',
'LinRange',
'LineNumberNode',
'LinearIndices',
'LoadError',
'MIME',
'Matrix',
'Method',
'MethodError',
'Missing',
'MissingException',
'Module',
'NTuple',
'NamedTuple',
'Nothing',
'Number',
'OrdinalRange',
'OutOfMemoryError',
'OverflowError',
'Pair',
'PartialQuickSort',
'PermutedDimsArray',
'Pipe',
'ProcessFailedException',
'Ptr',
'QuoteNode',
'Rational',
'RawFD',
'ReadOnlyMemoryError',
'Real',
'ReentrantLock',
'Ref',
'Regex',
'RegexMatch',
'RoundingMode',
'SegmentationFault',
'Set',
'Signed',
'Some',
'StackOverflowError',
'StepRange',
'StepRangeLen',
'StridedArray',
'StridedMatrix',
'StridedVecOrMat',
'StridedVector',
'String',
'StringIndexError',
'SubArray',
'SubString',
'SubstitutionString',
'Symbol',
'SystemError',
'Task',
'TaskFailedException',
'Text',
'TextDisplay',
'Timer',
'Tuple',
'Type',
'TypeError',
'TypeVar',
'UInt',
'UInt128',
'UInt16',
'UInt32',
'UInt64',
'UInt8',
'UndefInitializer',
'UndefKeywordError',
'UndefRefError',
'UndefVarError',
'Union',
'UnionAll',
'UnitRange',
'Unsigned',
'Val',
'Vararg',
'VecElement',
'VecOrMat',
'Vector',
'VersionNumber',
'WeakKeyDict',
'WeakRef',
];
const KEYWORDS = {
$pattern: VARIABLE_NAME_RE,
keyword: KEYWORD_LIST,
literal: LITERAL_LIST,
built_in: BUILT_IN_LIST,
};
// placeholder for recursive self-reference
const DEFAULT = {
keywords: KEYWORDS,
illegal: /<\//
};
// ref: https://docs.julialang.org/en/v1/manual/integers-and-floating-point-numbers/
const NUMBER = {
className: 'number',
// supported numeric literals:
// * binary literal (e.g. 0x10)
// * octal literal (e.g. 0o76543210)
// * hexadecimal literal (e.g. 0xfedcba876543210)
// * hexadecimal floating point literal (e.g. 0x1p0, 0x1.2p2)
// * decimal literal (e.g. 9876543210, 100_000_000)
// * floating pointe literal (e.g. 1.2, 1.2f, .2, 1., 1.2e10, 1.2e-10)
begin: /(\b0x[\d_]*(\.[\d_]*)?|0x\.\d[\d_]*)p[-+]?\d+|\b0[box][a-fA-F0-9][a-fA-F0-9_]*|(\b\d[\d_]*(\.[\d_]*)?|\.\d[\d_]*)([eEfF][-+]?\d+)?/,
relevance: 0
};
const CHAR = {
className: 'string',
begin: /'(.|\\[xXuU][a-zA-Z0-9]+)'/
};
const INTERPOLATION = {
className: 'subst',
begin: /\$\(/,
end: /\)/,
keywords: KEYWORDS
};
const INTERPOLATED_VARIABLE = {
className: 'variable',
begin: '\\$' + VARIABLE_NAME_RE
};
// TODO: neatly escape normal code in string literal
const STRING = {
className: 'string',
contains: [
hljs.BACKSLASH_ESCAPE,
INTERPOLATION,
INTERPOLATED_VARIABLE
],
variants: [
{
begin: /\w*"""/,
end: /"""\w*/,
relevance: 10
},
{
begin: /\w*"/,
end: /"\w*/
}
]
};
const COMMAND = {
className: 'string',
contains: [
hljs.BACKSLASH_ESCAPE,
INTERPOLATION,
INTERPOLATED_VARIABLE
],
begin: '`',
end: '`'
};
const MACROCALL = {
className: 'meta',
begin: '@' + VARIABLE_NAME_RE
};
const COMMENT = {
className: 'comment',
variants: [
{
begin: '#=',
end: '=#',
relevance: 10
},
{
begin: '#',
end: '$'
}
]
};
DEFAULT.name = 'Julia';
DEFAULT.contains = [
NUMBER,
CHAR,
STRING,
COMMAND,
MACROCALL,
COMMENT,
hljs.HASH_COMMENT_MODE,
{
className: 'keyword',
begin:
'\\b(((abstract|primitive)\\s+)type|(mutable\\s+)?struct)\\b'
},
{ begin: /<:/ } // relevance booster
];
INTERPOLATION.contains = DEFAULT.contains;
return DEFAULT;
}
return julia;
})();
hljs.registerLanguage('julia', hljsGrammar);
})();

View file

@ -1,93 +0,0 @@
/*! `ocaml` grammar compiled for Highlight.js 11.9.0 */
(function(){
var hljsGrammar = (function () {
'use strict';
/*
Language: OCaml
Author: Mehdi Dogguy <mehdi@dogguy.org>
Contributors: Nicolas Braud-Santoni <nicolas.braud-santoni@ens-cachan.fr>, Mickael Delahaye <mickael.delahaye@gmail.com>
Description: OCaml language definition.
Website: https://ocaml.org
Category: functional
*/
function ocaml(hljs) {
/* missing support for heredoc-like string (OCaml 4.0.2+) */
return {
name: 'OCaml',
aliases: [ 'ml' ],
keywords: {
$pattern: '[a-z_]\\w*!?',
keyword:
'and as assert asr begin class constraint do done downto else end '
+ 'exception external for fun function functor if in include '
+ 'inherit! inherit initializer land lazy let lor lsl lsr lxor match method!|10 method '
+ 'mod module mutable new object of open! open or private rec sig struct '
+ 'then to try type val! val virtual when while with '
/* camlp4 */
+ 'parser value',
built_in:
/* built-in types */
'array bool bytes char exn|5 float int int32 int64 list lazy_t|5 nativeint|5 string unit '
/* (some) types in Pervasives */
+ 'in_channel out_channel ref',
literal:
'true false'
},
illegal: /\/\/|>>/,
contains: [
{
className: 'literal',
begin: '\\[(\\|\\|)?\\]|\\(\\)',
relevance: 0
},
hljs.COMMENT(
'\\(\\*',
'\\*\\)',
{ contains: [ 'self' ] }
),
{ /* type variable */
className: 'symbol',
begin: '\'[A-Za-z_](?!\')[\\w\']*'
/* the grammar is ambiguous on how 'a'b should be interpreted but not the compiler */
},
{ /* polymorphic variant */
className: 'type',
begin: '`[A-Z][\\w\']*'
},
{ /* module or constructor */
className: 'type',
begin: '\\b[A-Z][\\w\']*',
relevance: 0
},
{ /* don't color identifiers, but safely catch all identifiers with ' */
begin: '[a-z_]\\w*\'[\\w\']*',
relevance: 0
},
hljs.inherit(hljs.APOS_STRING_MODE, {
className: 'string',
relevance: 0
}),
hljs.inherit(hljs.QUOTE_STRING_MODE, { illegal: null }),
{
className: 'number',
begin:
'\\b(0[xX][a-fA-F0-9_]+[Lln]?|'
+ '0[oO][0-7_]+[Lln]?|'
+ '0[bB][01_]+[Lln]?|'
+ '[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)',
relevance: 0
},
{ begin: /->/ // relevance booster
}
]
};
}
return ocaml;
})();
hljs.registerLanguage('ocaml', hljsGrammar);
})();

View file

@ -9,7 +9,6 @@ window.addEventListener("load", () => {
/* Aliases of langs */ /* Aliases of langs */
const aliases = { const aliases = {
bash: ["fish"], bash: ["fish"],
julia: ["pseudocode"],
}; };
for (const lang in aliases) { for (const lang in aliases) {
hljs.registerAliases(aliases[lang], { languageName: lang }); hljs.registerAliases(aliases[lang], { languageName: lang });

14
static/js/libs/jspdf.js Normal file
View file

@ -0,0 +1,14 @@
window.addEventListener("load", () => {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
doc.html(document.body, {
width: doc.internal.pageSize.getWidth() - 20,
windowWidth: 800,
margin: [15, 10, 10, 10],
callback: function (doc) {
doc.save(`${document.title}.pdf`);
},
});
});

View file

@ -13,16 +13,23 @@ window.addEventListener("load", () => {
u: "mu", u: "mu",
}) })
)[Symbol.iterator]()) { )[Symbol.iterator]()) {
const bs = "\\"; macros[`\\${item[0]}`] = `\\${item[1]}`;
macros[`${bs}${item[0]}`] = `${bs}${item[1]}`;
} }
const attribute = "data-math-style"; document.querySelectorAll("span[data-katex]").forEach((element) => {
for (const element of document.querySelectorAll(`span[${attribute}]`)) { const rawLaTeXFormula = element.getAttribute("data-katex");
katex.render(element.textContent, element, { const displayMode = rawLaTeXFormula.startsWith("$$");
const strip = displayMode ? 2 : 1;
katex.render(
rawLaTeXFormula.slice(strip, rawLaTeXFormula.length - strip),
element,
{
throwOnError: false, throwOnError: false,
displayMode: element.getAttribute(attribute) === "display", macros,
macros: macros, displayMode,
}); output: "mathml",
} }
);
});
}); });

View file

@ -1,17 +0,0 @@
window.addEventListener("load", () => {
Array.from(document.getElementsByClassName("at")).forEach((elem) => {
const a = elem.parentElement;
const span = elem.previousElementSibling;
// Replace (at) by @
elem.outerHTML = "@";
// Correct text
const data = span.getAttribute("title");
data.length > 0 ? (a.innerHTML = data) : (a.style = "hyphens: none;");
// Change link
const href = a.getAttribute("href");
a.setAttribute("href", href.replace(" at ", "@"));
});
});

BIN
static/pics/me.png (Stored with Git LFS)

Binary file not shown.

View file

@ -1,21 +0,0 @@
{
"name": "{{#data}}{{name}}{{/data}}",
"start_url": "{{#data}}{{url}}{{/data}}",
"display": "standalone",
"background_color": "#2a2424",
"description": "{{#data}}{{description}}{{/data}}",
"icons": [
{
"src": "/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
],
"theme_color": "#2a2424",
"related_applications": [
{
"platform": "source",
"url": "https://git.mylloon.fr/Anri/mylloon.fr"
}
]
}

View file

@ -16,8 +16,8 @@
{{#data}} {{#data}}
<h1>Blog</h1> <h1>Blog</h1>
{{#about}} {{&content}} {{/about}} <p>Blog perso, je dis peut-être n'importe quoi 🫶</p>
<a id="rss" href="/blog/blog.rss">Lien vers le flux RSS</a> <a id="rss" href="/blog/rss">Lien vers le flux RSS</a>
{{#no_posts}} {{#no_posts}}
<h2>Aucun posts</h2> <h2>Aucun posts</h2>
@ -27,18 +27,14 @@
{{#posts}} {{#posts}}
<li role="button" onclick="window.open('/blog/p/{{url}}', '_parent');"> <li role="button" onclick="window.open('/blog/p/{{url}}', '_parent');">
{{>blog/date.html}} {{>blog/date.html}}
<h2><a href="/blog/p/{{url}} ">{{title}}</a></h2> <h2>{{title}}</h2>
{{#desc}} {{#desc}}
<p>{{desc}}</p> <p>{{desc}}</p>
{{/desc}} {{/desc}}
</li> </li>
{{/posts}} {{/posts}}
</ul> </ul>
{{/no_posts}} {{/no_posts}} {{/data}}
</main> </main>
{{#about}} {{#metadata}}
{{#mail_obfsucated}}{{>libs/mail_obfuscater.html}}{{/mail_obfsucated}}
{{/metadata}} {{/about}} {{/data}}
</body> </body>
</html> </html>

View file

@ -36,7 +36,6 @@
{{#mermaid}}{{>libs/mermaid_footer.html}}{{/mermaid}} {{#mermaid}}{{>libs/mermaid_footer.html}}{{/mermaid}}
{{#math}}{{>libs/katex_footer.html}}{{/math}} {{#math}}{{>libs/katex_footer.html}}{{/math}}
{{#syntax_highlight}}{{>libs/hljs_footer.html}}{{/syntax_highlight}} {{#syntax_highlight}}{{>libs/hljs_footer.html}}{{/syntax_highlight}}
{{#mail_obfsucated}}{{>libs/mail_obfuscater.html}}{{/mail_obfsucated}}
{{/metadata}} {{/post}} {{/data}} {{/metadata}} {{/post}} {{/data}}
</body> </body>
</html> </html>

View file

@ -8,8 +8,9 @@
<header>{{>navbar.html}}</header> <header>{{>navbar.html}}</header>
<main> <main>
<h1>Contact</h1> <h1>Contact</h1>
{{#data}}{{#about}} {{&content}} {{/about}} {{#socials_exists}} <p>Je suis présent relativement partout sur internet 😸</p>
{{#data}} {{#socials_exists}}
<h2>Réseaux sociaux</h2> <h2>Réseaux sociaux</h2>
<ul> <ul>
{{#socials}} {{>contact/element.html}} {{/socials}} {{#socials}} {{>contact/element.html}} {{/socials}}
@ -24,11 +25,7 @@
<ul> <ul>
{{#others}} {{>contact/element.html}} {{/others}} {{#others}} {{>contact/element.html}} {{/others}}
</ul> </ul>
{{/others_exists}} {{/others_exists}} {{/data}}
</main> </main>
{{#about}} {{#metadata}}
{{#mail_obfsucated}}{{>libs/mail_obfuscater.html}}{{/mail_obfsucated}}
{{/metadata}} {{/about}} {{/data}}
</body> </body>
</html> </html>

View file

@ -3,19 +3,15 @@
<head dir="ltr"> <head dir="ltr">
{{>head.html}} {{>head.html}}
<link rel="stylesheet" href="/css/cours.css" /> <link rel="stylesheet" href="/css/cours.css" />
{{#data}} {{#content}} {{#metadata}}
{{#math}}{{>libs/katex_head.html}}{{/math}}
{{#syntax_highlight}}{{>libs/hljs_head.html}}{{/syntax_highlight}}
{{/metadata}} {{/content}}
</head> </head>
<body> <body>
<header>{{>navbar.html}}</header> <header>{{>navbar.html}}</header>
<aside> <aside>
<span data-json="{{filetree}} "></span> <span data-json="{{#data}}{{filetree}}{{/data}} "></span>
</aside> </aside>
<main> <main>
{{^content}} {{#data}} {{^content}}
<p>Fichier introuvable</p> <p>Fichier introuvable</p>
{{/content}} {{#content}} {{/content}} {{#content}}
<article>{{&content}}</article> <article>{{&content}}</article>
@ -24,7 +20,6 @@
{{#metadata}} {{#mermaid}}{{>libs/mermaid_footer.html}}{{/mermaid}} {{#metadata}} {{#mermaid}}{{>libs/mermaid_footer.html}}{{/mermaid}}
{{#math}}{{>libs/katex_footer.html}}{{/math}} {{#math}}{{>libs/katex_footer.html}}{{/math}}
{{#syntax_highlight}}{{>libs/hljs_footer.html}}{{/syntax_highlight}} {{#syntax_highlight}}{{>libs/hljs_footer.html}}{{/syntax_highlight}}
{{#mail_obfsucated}}{{>libs/mail_obfuscater.html}}{{/mail_obfsucated}}
{{/metadata}} {{/content}} {{/data}} {{/metadata}} {{/content}} {{/data}}
<script src="/js/cours.js"></script> <script src="/js/cours.js"></script>
</body> </body>

View file

@ -3,7 +3,6 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="author" href="/humans.txt" /> <link rel="author" href="/humans.txt" />
<link rel="manifest" href="/app.webmanifest" />
{{>icons.html}} {{>metadata.html}} {{>icons.html}} {{>metadata.html}}

View file

@ -1,15 +1,17 @@
/* TEAM */{{#data}} /* TEAM */{{#data}}
{{name}} {{name}}
Contact: {{contact}} Contact: {{contact}}
Lang: {{lang}}{{/data}} Lang: {{lang}}
/* THANKS */ /* THANKS */
The dependencies of EWP are available in the Cargo.toml file, see: All the dependencies I use for building ewp
https://git.mylloon.fr/Anri/mylloon.fr/src/branch/main/Cargo.toml They are listed here: https://git.mylloon.fr/Anri/mylloon.fr/src/branch/main/Cargo.toml
/* SITE: EWP */ /* SITE */
Author: Anri (Mylloon) Kennel Authored by Anri Kennel (Mylloon)
Standards: HTML5, CSS3, mustache templates, Docker, TOML, Markdown, RSS, YAML Standards: HTML5, CSS3, mustache templates, Docker, TOML, Markdown, XML
Components: Rust, JavaScript, Internet Components: Rust, JavaScript, Internet
Software: Visual Studio Code and VSCodium Software: Visual Studio Code
{{/data}}

View file

@ -16,6 +16,7 @@
sizes="16x16" sizes="16x16"
href="/icons/favicon-16x16.png" href="/icons/favicon-16x16.png"
/> />
<link rel="manifest" href="/icons/site.webmanifest" />
<link rel="mask-icon" href="/icons/safari-pinned-tab.svg" color="#5bbad5" /> <link rel="mask-icon" href="/icons/safari-pinned-tab.svg" color="#5bbad5" />
<link rel="shortcut icon" href="/icons/favicon.ico" /> <link rel="shortcut icon" href="/icons/favicon.ico" />
<meta name="msapplication-TileColor" content="#ffffff" /> <meta name="msapplication-TileColor" content="#ffffff" />

View file

@ -3,46 +3,78 @@
<head dir="ltr"> <head dir="ltr">
{{>head.html}} {{>head.html}}
<link rel="stylesheet" href="/css/index.css" /> <link rel="stylesheet" href="/css/index.css" />
{{#data}} {{#avatar_style}} {{#round}}
<style>
#avatar {
border-radius: 50%;
}
</style>
{{/round}} {{#square}}
<style>
#avatar {
border-radius: 10%;
}
</style>
{{/square}} {{/avatar_style}}
</head> </head>
<body> <body>
<header>{{>navbar.html}}</header> <header>{{>navbar.html}}</header>
<main> <main>
{{#data}}
<div> <div>
<span id="name">{{name}}</span> <span id="name">{{fullname}}</span>
{{#pronouns}}<span id="pronouns">{{pronouns}}</span>{{/pronouns}} <span id="pronouns">(il/lui, he/him)</span>
<img <img
id="avatar" id="avatar"
src="{{avatar}} " src="/icons/apple-touch-icon.png"
alt="Avatar" alt="Avatar"
title="{{avatar_caption}} " title="Mon avatar, dessiné un jour super rapidement sur Gimp."
loading="lazy" loading="lazy"
/> />
</div> </div>
<p id="subname"></p> <p id="subname"></p>
{{#file}} {{&content}} {{/file}} {{^file}} <article>
<h1>Qui suis-je ?</h1>
<p>Je m'appelle <b>Anri</b>, mon pseudo est <b>Mylloon</b>.</p>
<p> <p>
<b>Welcome to EWP</b>, create a <code>index.md</code> file inside your J'aime beaucoup l'informatique depuis très petit, ce site est écrit de
<code>data/</code> directory to get started. A à Z par moi-même (modulo la quantité astronomique de librairie
utilisé) en Rust. J'adore le monde de l'open source, l'immense
majorité de mes projets sont sous licence
<a
href="https://en.wikipedia.org/wiki/GNU_Affero_General_Public_License"
target="_blank"
rel="noreferrer"
>AGPLv3</a
>.
</p> </p>
{{/file}} <p>
En ce moment, je suis en master d'informatique à Paris Cité
(anciennement Paris 7), c'est marrant on fait de l'OCaml 🤓☝️.
</p>
</article>
<article id="friends">
<h1>Personnes incroyables</h1>
<a
href="https://github.com/2-1-1-2"
title="GitHub de 21_12"
target="_blank"
rel="noreferrer"
><img src="/badges/friends/21_12.webp" alt="21_12" loading="lazy"
/></a>
<a
href="https://twitter.com/azazouille_"
title="Twitter de Azazouille"
target="_blank"
rel="noreferrer"
><img
src="/badges/friends/azazouille.webp"
alt="Azazouille"
loading="lazy"
/></a>
<a
href="https://102jjwy.carrd.co/"
title="Carrd de 102jjwy"
target="_blank"
rel="noreferrer"
><img src="/badges/friends/102jjwy.webp" alt="102jjwy" loading="lazy"
/></a>
</article>
{{/data}}
</main> </main>
<script src="/js/index.js"></script> <script src="/js/index.js"></script>
{{#file}} {{#metadata}}
{{#mail_obfsucated}}{{>libs/mail_obfuscater.html}}{{/mail_obfsucated}}
{{/metadata}} {{/file}} {{/data}}
</body> </body>
</html> </html>

View file

@ -1,6 +1,4 @@
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
<script src="//unpkg.com/highlightjs-copy/dist/highlightjs-copy.min.js"></script> <script src="//unpkg.com/highlightjs-copy/dist/highlightjs-copy.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlightjs-line-numbers.js/2.8.0/highlightjs-line-numbers.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/highlightjs-line-numbers.js/2.8.0/highlightjs-line-numbers.min.js"></script>
<script src="/js/libs/hljs-languages/julia.js"></script>
<script src="/js/libs/hljs-languages/ocaml.js"></script>
<script src="/js/libs/hljs.js"></script> <script src="/js/libs/hljs.js"></script>

View file

@ -1,12 +1,12 @@
<link <link
id="hljs-light-theme" id="hljs-light-theme"
rel="stylesheet" rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/a11y-light.min.css" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/base16/solarized-light.min.css"
/> />
<link <link
id="hljs-dark-theme" id="hljs-dark-theme"
rel="stylesheet" rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/a11y-dark.min.css" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/base16/dracula.min.css"
/> />
<link <link
rel="stylesheet" rel="stylesheet"

View file

@ -0,0 +1,8 @@
<script src="//html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
<script
src="//cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"
integrity="sha512-qZvrmS2ekKPF2mSznTQsxqPgnpkI4DNTlrdUmTzrDgektczlKNRRhy5X5AAOnx5S09ydFYWWNSfcEqDTTHgtNA=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<script src="/js/libs/jspdf.js"></script>

View file

@ -1,13 +1,7 @@
<script <script
defer defer
src="//cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js" src="//cdn.jsdelivr.net/npm/katex@0.16.6/dist/katex.min.js"
integrity="sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd" integrity="sha384-j/ZricySXBnNMJy9meJCtyXTKMhIJ42heyr7oAdxTDBy/CYA9hzpMo+YTNV5C+1X"
crossorigin="anonymous"
></script>
<script
defer
src="//cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js"
integrity="sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk"
crossorigin="anonymous" crossorigin="anonymous"
></script> ></script>
<script src="/js/libs/katex.js"></script> <script src="/js/libs/katex.js"></script>

View file

@ -1,6 +1,6 @@
<link <link
rel="stylesheet" rel="stylesheet"
href="//cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" href="//cdn.jsdelivr.net/npm/katex@0.16.6/dist/katex.min.css"
integrity="sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww" integrity="sha384-mXD7x5S50Ko38scHSnD4egvoExgMPbrseZorkbE49evAfv9nNcbrXJ8LLNsDgh9d"
crossorigin="anonymous" crossorigin="anonymous"
/> />

View file

@ -1 +0,0 @@
<script defer src="/js/mail_obfuscation.js"></script>

View file

@ -1 +1 @@
<script async type="module" src="/js/libs/mermaid.js"></script> <script type="module" src="/js/libs/mermaid.js"></script>

View file

@ -23,7 +23,7 @@
>Portfolio</a >Portfolio</a
></p> ></p>
</li><!-- <li> </li><li>
<p><a <p><a
class="_ {{#contact}}bold{{/contact}}" class="_ {{#contact}}bold{{/contact}}"
@ -32,7 +32,7 @@
>Contact</a >Contact</a
></p> ></p>
</li> --><li> </li><li>
<p><a <p><a
class="_ {{#contrib}}bold{{/contrib}}" class="_ {{#contrib}}bold{{/contrib}}"
@ -45,9 +45,8 @@
<p><a <p><a
class="_ {{#cours}}bold{{/cours}}" class="_ {{#cours}}bold{{/cours}}"
href="https://univ.mylloon.fr" href="/cours"
title="Page des notes de cours" title="Page des notes de cours"
target="_blank"
>Cours</a >Cours</a
></p> ></p>

View file

@ -1,9 +1,5 @@
{{#metadata}} {{#info}} {{#portfolio}} {{#link}} {{#metadata}} {{#info}} {{#portfolio}} {{#link}}
<li <li role="button" onclick="window.open('{{link}}', '_blank', 'noreferrer');">
role="button"
onmousedown="disableScroll()"
onmouseup="openLink('{{link}}');"
>
{{>portfolio/project.html}} {{>portfolio/project.html}}
</li> </li>
{{/link}} {{^link}} {{/link}} {{^link}}

View file

@ -10,10 +10,14 @@
<main> <main>
{{#data}} {{#data}}
<h1>Portfolio</h1> <h1>Portfolio</h1>
{{#about}} {{&content}} {{/about}} <p>
Je programme depuis 2018 et j'ai appris une multitude de langages
depuis. Étant passionné de logiciels libres depuis que je m'y intéresse,
je publie tout sur des forges publiques.
</p>
<!-- Error message -->
{{#location_apps}} {{#location_apps}}
<!-- Error message -->
<p>{{location_apps}} {{err_msg}}</p> <p>{{location_apps}} {{err_msg}}</p>
{{/location_apps}} {{^location_apps}} {{/location_apps}} {{^location_apps}}
@ -27,62 +31,20 @@
<ul> <ul>
{{#archived_apps}} {{>portfolio/card.html}} {{/archived_apps}} {{#archived_apps}} {{>portfolio/card.html}} {{/archived_apps}}
</ul> </ul>
{{/archived_apps_exists}} {{/location_apps}} {{/archived_apps_exists}} {{/location_apps}} {{/data}}
</main> </main>
{{#about}} {{#metadata}}
{{#mail_obfsucated}}{{>libs/mail_obfuscater.html}}{{/mail_obfsucated}}
{{/metadata}} {{/about}} {{/data}}
<script> <script>
/* Fix links in list */ /* Fix links in list */
window.addEventListener("load", () => window.addEventListener("load", () =>
document.querySelectorAll("main a").forEach(function (link) { document.querySelectorAll("main a").forEach(function (link) {
link.setAttribute("target", "_blank"); link.setAttribute("target", "_blank");
link.setAttribute("rel", "noreferrer"); link.setAttribute("rel", "noreferrer");
link.addEventListener("mouseup", function (event) { link.addEventListener("click", function (event) {
event.stopPropagation(); event.stopPropagation();
}); });
}) })
); );
/* Middle click */
const disableScroll = () => {
if (event.button === 1) {
event.preventDefault();
}
};
/* Open cards link */
const openLink = (url) => {
const backgroundtab = () =>
Object.assign(document.createElement("a"), {
href: url,
target: "_blank",
rel: "noreferrer",
}).dispatchEvent(
new MouseEvent("click", { ctrlKey: true, metaKey: true })
);
switch (event.button) {
case 0:
/* Left click */
if (event.ctrlKey || event.metaKey) {
backgroundtab();
} else {
window.open(url, "_blank", "noreferrer");
}
break;
case 1:
/* Middle click */
backgroundtab();
break;
default:
break;
}
};
</script> </script>
</body> </body>
</html> </html>

View file

@ -263,22 +263,6 @@
alt="humans.txt" alt="humans.txt"
title="We are humans" title="We are humans"
/></a> /></a>
<a target="_blank" href="https://decolonizepalestine.com"
><img
src="/badges/palestine.png"
alt="Stand with palestine"
title="a genocide is happening"
/></a>
<a
target="_blank"
href="https://pbs.twimg.com/media/GEShpIXXAAAKQ_1?format=jpg"
><img
src="/badges/cat.gif"
alt="No time spent with a cat is wasted."
title="meow"
/></a>
</div> </div>
<br /> <br />