Compare commits
108 commits
math-rewor
...
main
Author | SHA1 | Date | |
---|---|---|---|
7432ffd5f9 | |||
764a632ae6 | |||
396bff909e | |||
fb44c25e47 | |||
a7aec1b94e | |||
8cedeb531d | |||
912f16e0c3 | |||
999d68ab60 | |||
2d9fc0d559 | |||
8c386d5ac6 | |||
95b92699ed | |||
2dc54a6f76 | |||
deb54372a2 | |||
485797c64f | |||
5b43730150 | |||
b145510d83 | |||
847ec0d3c3 | |||
e0b59130ee | |||
a4b67df515 | |||
83a010e8f5 | |||
b5683be191 | |||
9010c7f975 | |||
a43f5813c0 | |||
8bfa012af9 | |||
975d6de8e6 | |||
c224816807 | |||
1e19809df4 | |||
aa1ba564dc | |||
b9194dd0fa | |||
de60dd23e6 | |||
c9405d0fdb | |||
0c6c88d181 | |||
a08490f669 | |||
d1d21bc68c | |||
bf9217ba84 | |||
bece8ef147 | |||
4146f2ea45 | |||
cce73c0e09 | |||
df3f66a424 | |||
0ffb7a06d6 | |||
afb1e72adf | |||
2efe4ce47f | |||
988f8345aa | |||
8e626a9640 | |||
2787450c0e | |||
73a235e3e1 | |||
beacc5e02d | |||
ed404bacca | |||
024fa67682 | |||
aba8a501af | |||
0d924de79b | |||
d400ef3c5b | |||
6ab37ad04c | |||
2e43c6df12 | |||
0f8391660d | |||
754db13565 | |||
13c19c5f68 | |||
af4c113153 | |||
5e223b972c | |||
c31cfb1295 | |||
d352206e29 | |||
ab5ce11037 | |||
c42beef801 | |||
9c8b036c79 | |||
d0cc3e584f | |||
9dfcc1101d | |||
51ed97273c | |||
943603a330 | |||
04db320065 | |||
bf35016f45 | |||
b1d651026b | |||
8ff80d8b34 | |||
116bc311c8 | |||
6f8103ac97 | |||
8e0a1b0bb5 | |||
619bab58ea | |||
3ed9178def | |||
9c8718afac | |||
058eb3c8c8 | |||
c380da6380 | |||
048ed0c7e1 | |||
f4e01449cc | |||
62eacf879e | |||
d51afcdcf6 | |||
9558202a46 | |||
c787171d6b | |||
bc14e1b393 | |||
889573b4e7 | |||
c18c0adf34 | |||
17850ae636 | |||
978cc48a57 | |||
fbde0a07c6 | |||
cb67c85040 | |||
1467740f60 | |||
5ce39b0453 | |||
a4af983418 | |||
061d84a5a5 | |||
db4019ab67 | |||
b49319d56c | |||
86f7d23fa5 | |||
07bea92346 | |||
cedb2e0e4c | |||
465cc71e9e | |||
d485946d56 | |||
05c86564d0 | |||
df5d47322c | |||
bfd8467d14 | |||
d6740851ec |
75 changed files with 3541 additions and 1174 deletions
47
.forgejo/workflows/publish.yml
Normal file
47
.forgejo/workflows/publish.yml
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
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 }}
|
|
@ -1,14 +0,0 @@
|
||||||
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
|
|
1927
Cargo.lock
generated
1927
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
29
Cargo.toml
29
Cargo.toml
|
@ -10,19 +10,28 @@ publish = false
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = { version = "4.4", default-features = false, features = ["macros", "compress-brotli"] }
|
actix-web = { version = "4.9", default-features = false, features = ["macros", "compress-brotli"] }
|
||||||
actix-files = "0.6"
|
actix-files = "0.6"
|
||||||
cached = { version = "0.46", features = ["async"] }
|
cached = { version = "0.53", features = ["async", "ahash"] }
|
||||||
ramhorns = "0.14"
|
ramhorns = "1.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_yaml = "0.9"
|
serde_yml = "0.0"
|
||||||
minify-html = "0.11"
|
serde_json = "1.0"
|
||||||
minify-js = "0.5"
|
minify-html = "0.15"
|
||||||
|
minify-js = "0.6"
|
||||||
glob = "0.3"
|
glob = "0.3"
|
||||||
comrak = "0.19"
|
comrak = "0.28"
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
chrono = { version = "0.4.30", default-features = false, features = ["clock"]}
|
chrono = { version = "0.4.38", default-features = false, features = ["clock"]}
|
||||||
chrono-tz = "0.8"
|
chrono-tz = "0.10"
|
||||||
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"
|
||||||
|
cyborgtime = "2.1.1"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
pedantic = "warn"
|
||||||
|
|
|
@ -18,6 +18,7 @@ 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
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,11 @@
|
||||||
- [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)
|
||||||
|
- [Courses](#courses)
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
|
@ -104,6 +106,7 @@ 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
|
||||||
|
@ -138,9 +141,29 @@ 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/`
|
Markdown files are stored in `/app/data/blog/posts/`
|
||||||
|
|
||||||
```
|
```
|
||||||
---
|
---
|
||||||
|
@ -157,11 +180,15 @@ 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
|
but accessible, see [#30](https://git.mylloon.fr/Anri/mylloon.fr/issues/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/`
|
Markdown files are stored in `/app/data/projects/apps/`
|
||||||
|
|
||||||
```
|
```
|
||||||
---
|
---
|
||||||
|
@ -176,7 +203,14 @@ 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
|
||||||
|
|
||||||
|
@ -204,3 +238,19 @@ Custom project description
|
||||||
- `user` is the username used in the platform
|
- `user` is the username used in the platform
|
||||||
- `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
|
||||||
|
|
||||||
|
Markdown files are stored in `/app/data/cours/`
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -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 Mylloon
|
Copyright (C) 2023-2024 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
|
||||||
|
|
|
@ -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://ci.mylloon.fr/api/badges/Anri/mylloon.fr/status.svg)](https://ci.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)
|
||||||
|
|
||||||
- See [issues](https://git.mylloon.fr/Anri/mylloon.fr/issues)
|
- See [issues](https://git.mylloon.fr/Anri/mylloon.fr/issues)
|
||||||
- See [documentation](./Documentation.md)
|
- See [documentation](https://git.mylloon.fr/Anri/mylloon.fr/src/branch/main/Documentation.md)
|
||||||
|
|
|
@ -7,8 +7,8 @@ use std::{fs::File, io::Write, path::Path};
|
||||||
use crate::template::Template;
|
use crate::template::Template;
|
||||||
|
|
||||||
/// Store the configuration of config/config.toml
|
/// Store the configuration of config/config.toml
|
||||||
#[derive(Deserialize, Clone, Default, Debug)]
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
pub struct FileConfig {
|
pub struct FileConfiguration {
|
||||||
/// http/https
|
/// http/https
|
||||||
pub scheme: Option<String>,
|
pub scheme: Option<String>,
|
||||||
/// Domain name "sub.domain.tld"
|
/// Domain name "sub.domain.tld"
|
||||||
|
@ -27,9 +27,11 @@ pub struct FileConfig {
|
||||||
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 FileConfig {
|
impl FileConfiguration {
|
||||||
/// Initialize with default values
|
/// Initialize with default values
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -37,15 +39,17 @@ impl FileConfig {
|
||||||
domain: Some("localhost".into()),
|
domain: Some("localhost".into()),
|
||||||
port: Some(8080),
|
port: Some(8080),
|
||||||
app_name: Some("EWP".into()),
|
app_name: Some("EWP".into()),
|
||||||
..FileConfig::default()
|
exclude_courses: Some([].into()),
|
||||||
|
..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 = FileConfig::new();
|
let d = Self::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() {
|
||||||
|
@ -65,54 +69,58 @@ impl FileConfig {
|
||||||
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: FileConfig,
|
pub fc: FileConfiguration,
|
||||||
/// Location where the static files are stored
|
/// Location where the static files are stored
|
||||||
pub static_location: String,
|
pub locations: Locations,
|
||||||
/// 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) -> FileConfig {
|
fn get_file_config(file_path: &str) -> FileConfiguration {
|
||||||
match fs::read_to_string(file_path) {
|
fs::read_to_string(file_path).map_or_else(
|
||||||
Ok(file) => match toml::from_str(&file) {
|
|_| FileConfiguration::new(),
|
||||||
Ok(stored_config) => FileConfig::complete(stored_config),
|
|file| match toml::from_str(&file) {
|
||||||
|
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_config(file_path: &str) -> Config {
|
pub fn get_configuration(file_path: &str) -> Config {
|
||||||
let internal_config = get_file_config(file_path);
|
let internal_config = get_file_config(file_path);
|
||||||
|
|
||||||
let static_dir = "static".to_owned();
|
let static_dir = "static";
|
||||||
let templates_dir = "templates".to_owned();
|
let templates_dir = "templates";
|
||||||
let files_root = init(
|
let files_root = init("dist".into(), static_dir, templates_dir);
|
||||||
"dist".into(),
|
|
||||||
static_dir.to_owned(),
|
|
||||||
templates_dir.to_owned(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Config {
|
Config {
|
||||||
fc: internal_config.to_owned(),
|
fc: internal_config.clone(),
|
||||||
static_location: format!("{}/{}", files_root, static_dir),
|
locations: Locations {
|
||||||
|
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,
|
||||||
|
@ -121,16 +129,16 @@ pub fn get_config(file_path: &str) -> Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Preparation before running the http server
|
/// Preparation before running the http server
|
||||||
fn init(dist_dir: String, static_dir: String, templates_dir: String) -> String {
|
fn init(dist_dir: String, static_dir: &str, templates_dir: &str) -> 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()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -139,7 +147,7 @@ fn init(dist_dir: String, static_dir: String, templates_dir: String) -> 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);
|
||||||
}
|
}
|
||||||
|
@ -149,7 +157,7 @@ fn init(dist_dir: String, static_dir: String, templates_dir: String) -> 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);
|
||||||
}
|
}
|
||||||
|
|
19
src/main.rs
19
src/main.rs
|
@ -18,20 +18,20 @@ mod routes;
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let config = config::get_config("./config/config.toml");
|
let config = config::get_configuration("./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.to_owned().fc.scheme.unwrap(),
|
config.clone().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.to_owned()))
|
.app_data(web::Data::new(config.clone()))
|
||||||
.wrap(Compress::default())
|
.wrap(Compress::default())
|
||||||
.wrap(
|
.wrap(
|
||||||
DefaultHeaders::new()
|
DefaultHeaders::new()
|
||||||
|
@ -42,12 +42,19 @@ 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(web::scope("/api").service(web::scope("v1").service(api_v1::love)))
|
.service(
|
||||||
|
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::sitemap)
|
.service(agreements::webmanifest)
|
||||||
.service(blog::index)
|
.service(blog::index)
|
||||||
.service(blog::rss)
|
.service(blog::rss)
|
||||||
.service(blog::page)
|
.service(blog::page)
|
||||||
|
@ -60,7 +67,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.static_location.to_owned()))
|
.service(Files::new("/", config.locations.static_dir.clone()))
|
||||||
.default_service(web::to(not_found::page))
|
.default_service(web::to(not_found::page))
|
||||||
})
|
})
|
||||||
.bind(addr)?
|
.bind(addr)?
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
use core::panic;
|
|
||||||
|
|
||||||
use reqwest::{header::ACCEPT, Error};
|
use reqwest::{header::ACCEPT, Error};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::misc::utils::get_reqwest_client;
|
use crate::misc::utils::get_reqwest_client;
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct GithubResponse {
|
struct GithubResponse {
|
||||||
items: Vec<GithubProject>,
|
items: Vec<GithubProject>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct GithubProject {
|
struct GithubProject {
|
||||||
repository_url: String,
|
repository_url: String,
|
||||||
number: u32,
|
number: u32,
|
||||||
|
@ -19,7 +17,7 @@ struct GithubProject {
|
||||||
pull_request: GithubPullRequest,
|
pull_request: GithubPullRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct GithubPullRequest {
|
struct GithubPullRequest {
|
||||||
html_url: String,
|
html_url: String,
|
||||||
merged_at: Option<String>,
|
merged_at: Option<String>,
|
||||||
|
@ -32,21 +30,23 @@ pub enum ProjectState {
|
||||||
Merged = 2,
|
Merged = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<u8> for ProjectState {
|
impl TryFrom<u8> for ProjectState {
|
||||||
fn from(orig: u8) -> Self {
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(orig: u8) -> Result<Self, Self::Error> {
|
||||||
match orig {
|
match orig {
|
||||||
0 => Self::Closed,
|
0 => Ok(Self::Closed),
|
||||||
1 => Self::Open,
|
1 => Ok(Self::Open),
|
||||||
2 => Self::Merged,
|
2 => Ok(Self::Merged),
|
||||||
_ => panic!(),
|
_ => Err(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
pub project: String,
|
pub name: String,
|
||||||
pub project_url: String,
|
pub 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 {
|
||||||
project: p.repository_url.split('/').last().unwrap().into(),
|
name: p.repository_url.split('/').last().unwrap().into(),
|
||||||
project_url: p.repository_url.to_owned(),
|
url: p.repository_url.clone(),
|
||||||
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.to_owned(),
|
title: p.title.clone(),
|
||||||
id: p.number,
|
id: p.number,
|
||||||
contrib_url: p.pull_request.html_url.to_owned(),
|
contrib_url: p.pull_request.html_url.clone(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
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};
|
use comrak::{format_html, parse_document, Arena, ComrakOptions, ListStyleType, Options};
|
||||||
use lol_html::{element, rewrite_str, RewriteStrSettings};
|
use lol_html::html_content::ContentType;
|
||||||
|
use lol_html::{element, rewrite_str, HtmlRewriter, RewriteStrSettings, Settings};
|
||||||
use ramhorns::Content;
|
use ramhorns::Content;
|
||||||
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;
|
||||||
|
|
||||||
#[derive(Default, Deserialize, Content, Debug)]
|
/// Metadata for blog posts
|
||||||
|
#[derive(Content, Debug, Default, Deserialize)]
|
||||||
pub struct FileMetadataBlog {
|
pub struct FileMetadataBlog {
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
pub date: Option<Date>,
|
pub date: Option<Date>,
|
||||||
|
@ -16,37 +24,7 @@ pub struct FileMetadataBlog {
|
||||||
pub toc: Option<bool>,
|
pub toc: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Deserialize, Content, Debug)]
|
/// A tag, related to post blog
|
||||||
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(Default, Deserialize, Content, Debug)]
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Deserialize, Content, Debug)]
|
|
||||||
pub struct FileMetadata {
|
|
||||||
pub blog: Option<FileMetadataBlog>,
|
|
||||||
pub contact: Option<FileMetadataContact>,
|
|
||||||
pub portfolio: Option<FileMetadataPortfolio>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Content, Debug, Clone)]
|
#[derive(Content, Debug, Clone)]
|
||||||
pub struct Tag {
|
pub struct Tag {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -58,7 +36,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_yaml::from_str(s) {
|
Ok(s) => match serde_yml::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)),
|
||||||
},
|
},
|
||||||
|
@ -67,14 +45,76 @@ 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,
|
||||||
|
@ -82,7 +122,7 @@ pub struct File {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Options used for parser and compiler MD --> HTML
|
/// Options used for parser and compiler MD --> HTML
|
||||||
pub fn get_options() -> ComrakOptions {
|
pub fn get_options<'a>() -> ComrakOptions<'a> {
|
||||||
let mut options = comrak::Options::default();
|
let mut options = comrak::Options::default();
|
||||||
|
|
||||||
// Extension
|
// Extension
|
||||||
|
@ -96,12 +136,21 @@ pub fn get_options() -> ComrakOptions {
|
||||||
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
|
||||||
|
@ -112,18 +161,25 @@ pub fn get_options() -> ComrakOptions {
|
||||||
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: String) -> String {
|
fn custom_img_size(html: &str) -> 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(|c| c == '|').collect::<Vec<&str>>();
|
let possible_piece = alt.split('|').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();
|
||||||
|
@ -138,7 +194,7 @@ fn custom_img_size(html: String) -> 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();
|
||||||
}
|
}
|
||||||
|
@ -148,7 +204,7 @@ fn custom_img_size(html: String) -> 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();
|
||||||
}
|
}
|
||||||
|
@ -164,12 +220,69 @@ fn custom_img_size(html: String) -> String {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fix local images to base64 and integration of markdown files
|
||||||
|
fn fix_images_and_integration(path: &str, html: &str) -> (String, Metadata) {
|
||||||
|
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()
|
||||||
|
.to_string();
|
||||||
|
if let Ok(file) = fs::read_to_string(&img_path) {
|
||||||
|
let mime = mime_guess::from_path(&img_path).first_or_octet_stream();
|
||||||
|
if mime == "text/markdown" {
|
||||||
|
let mut options = get_options();
|
||||||
|
options.extension.footnotes = false;
|
||||||
|
let data = read_md(
|
||||||
|
&img_path,
|
||||||
|
&file,
|
||||||
|
&TypeFileMetadata::Generic,
|
||||||
|
Some(options),
|
||||||
|
);
|
||||||
|
el.replace(&data.content, ContentType::Html);
|
||||||
|
metadata.merge(&data.metadata);
|
||||||
|
} else {
|
||||||
|
let image = general_purpose::STANDARD.encode(file);
|
||||||
|
|
||||||
|
el.set_attribute("src", &format!("data:{mime};base64,{image}"))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})],
|
||||||
|
..RewriteStrSettings::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Transform markdown string to File structure
|
/// Transform markdown string to File structure
|
||||||
fn read(raw_text: &str, metadata_type: TypeFileMetadata) -> File {
|
pub fn read_md(
|
||||||
|
path: &str,
|
||||||
|
raw_text: &str,
|
||||||
|
metadata_type: &TypeFileMetadata,
|
||||||
|
options: Option<Options>,
|
||||||
|
) -> File {
|
||||||
let arena = Arena::new();
|
let arena = Arena::new();
|
||||||
|
|
||||||
let options = get_options();
|
let opt = options.map_or_else(get_options, |specific_opt| specific_opt);
|
||||||
let root = parse_document(&arena, raw_text, &options);
|
let root = parse_document(&arena, raw_text, &opt);
|
||||||
|
|
||||||
// Find metadata
|
// Find metadata
|
||||||
let metadata = get_metadata(root, metadata_type);
|
let metadata = get_metadata(root, metadata_type);
|
||||||
|
@ -179,48 +292,62 @@ fn read(raw_text: &str, metadata_type: TypeFileMetadata) -> File {
|
||||||
|
|
||||||
// Convert to HTML
|
// Convert to HTML
|
||||||
let mut html = vec![];
|
let mut html = vec![];
|
||||||
format_html(root, &options, &mut html).unwrap();
|
format_html(root, &opt, &mut html).unwrap();
|
||||||
|
|
||||||
let mut html_content = String::from_utf8(html).unwrap();
|
let mut html_content = String::from_utf8(html).unwrap();
|
||||||
|
|
||||||
html_content = custom_img_size(html_content);
|
let children_metadata;
|
||||||
|
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 {
|
||||||
|
info: metadata,
|
||||||
|
mermaid: check_mermaid(root, mermaid_name),
|
||||||
|
syntax_highlight: check_code(root, &[mermaid_name.into()]),
|
||||||
|
math: check_math(&html_content),
|
||||||
|
mail_obfsucated,
|
||||||
|
};
|
||||||
|
final_metadata.merge(&children_metadata);
|
||||||
|
|
||||||
File {
|
File {
|
||||||
metadata: Metadata {
|
metadata: final_metadata,
|
||||||
info: metadata,
|
|
||||||
mermaid: check_mermaid(root, mermaid_name),
|
|
||||||
syntax_highlight: check_code(root, &[mermaid_name.into()]),
|
|
||||||
math: check_math(&html_content),
|
|
||||||
},
|
|
||||||
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_yaml::from_str(text.trim_matches(&['-', '\n'] as &[_])).unwrap_or_default()
|
serde_yml::from_str(text.trim().trim_matches(&['-'] 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 {
|
||||||
match root
|
root.children()
|
||||||
.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 => FileMetadata {
|
TypeFileMetadata::Contact => {
|
||||||
contact: Some(deserialize_metadata(text)),
|
let mut metadata: FileMetadataContact = deserialize_metadata(text);
|
||||||
|
|
||||||
|
// Trim descriptions
|
||||||
|
if let Some(desc) = &mut metadata.description {
|
||||||
|
desc.clone_from(&desc.trim().into());
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMetadata {
|
||||||
|
contact: Some(metadata),
|
||||||
|
..FileMetadata::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TypeFileMetadata::Generic => FileMetadata::default(),
|
||||||
|
TypeFileMetadata::Index => FileMetadata {
|
||||||
|
index: Some(deserialize_metadata(text)),
|
||||||
..FileMetadata::default()
|
..FileMetadata::default()
|
||||||
},
|
},
|
||||||
TypeFileMetadata::Portfolio => FileMetadata {
|
TypeFileMetadata::Portfolio => FileMetadata {
|
||||||
|
@ -229,23 +356,29 @@ pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: TypeFileMetadata) -> FileM
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
_ => None,
|
_ => None,
|
||||||
}) {
|
})
|
||||||
Some(data) => data,
|
.map_or_else(
|
||||||
None => match mtype {
|
|| match mtype {
|
||||||
TypeFileMetadata::Blog => FileMetadata {
|
TypeFileMetadata::Blog => FileMetadata {
|
||||||
blog: Some(FileMetadataBlog::default()),
|
blog: Some(FileMetadataBlog::default()),
|
||||||
..FileMetadata::default()
|
..FileMetadata::default()
|
||||||
|
},
|
||||||
|
TypeFileMetadata::Contact => FileMetadata {
|
||||||
|
contact: Some(FileMetadataContact::default()),
|
||||||
|
..FileMetadata::default()
|
||||||
|
},
|
||||||
|
TypeFileMetadata::Generic => FileMetadata::default(),
|
||||||
|
TypeFileMetadata::Index => FileMetadata {
|
||||||
|
index: Some(FileMetadataIndex::default()),
|
||||||
|
..FileMetadata::default()
|
||||||
|
},
|
||||||
|
TypeFileMetadata::Portfolio => FileMetadata {
|
||||||
|
portfolio: Some(FileMetadataPortfolio::default()),
|
||||||
|
..FileMetadata::default()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
TypeFileMetadata::Contact => FileMetadata {
|
|data| data,
|
||||||
contact: Some(FileMetadataContact::default()),
|
)
|
||||||
..FileMetadata::default()
|
|
||||||
},
|
|
||||||
TypeFileMetadata::Portfolio => FileMetadata {
|
|
||||||
portfolio: Some(FileMetadataPortfolio::default()),
|
|
||||||
..FileMetadata::default()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check whether mermaid diagrams are in the AST
|
/// Check whether mermaid diagrams are in the AST
|
||||||
|
@ -271,9 +404,24 @@ fn check_code<'a>(root: &'a AstNode<'a>, blacklist: &[String]) -> bool {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if html can contains maths
|
/// Check if html contains maths
|
||||||
fn check_math(html: &str) -> bool {
|
fn check_math(html: &str) -> bool {
|
||||||
html.contains('$')
|
let math_detected = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
|
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
|
||||||
|
@ -286,3 +434,69 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
|
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, StatusCode};
|
use reqwest::Client;
|
||||||
|
|
||||||
use crate::config::FileConfig;
|
use crate::config::FileConfiguration;
|
||||||
|
|
||||||
|
use super::markdown::{read_md, File, FileMetadata, Metadata, TypeFileMetadata};
|
||||||
|
|
||||||
#[cached]
|
#[cached]
|
||||||
pub fn get_reqwest_client() -> Client {
|
pub fn get_reqwest_client() -> Client {
|
||||||
|
@ -16,7 +22,7 @@ pub fn get_reqwest_client() -> Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get URL of the app
|
/// Get URL of the app
|
||||||
pub fn get_url(fc: FileConfig) -> String {
|
pub fn get_url(fc: FileConfiguration) -> 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(),
|
||||||
|
@ -27,8 +33,8 @@ pub fn get_url(fc: FileConfig) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make a list of keywords
|
/// Make a list of keywords
|
||||||
pub fn make_kw(list: &[&str]) -> Option<String> {
|
pub fn make_kw(list: &[&str]) -> String {
|
||||||
Some(list.join(", "))
|
list.join(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send HTML file
|
/// Send HTML file
|
||||||
|
@ -45,3 +51,35 @@ 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";
|
||||||
|
>"#
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{config::Config, misc::utils::get_url, template::Infos};
|
use crate::{config::Config, misc::utils::get_url, template::InfosPage};
|
||||||
use actix_web::{get, http::header::ContentType, routes, web, HttpResponse, Responder};
|
use actix_web::{get, http::header::ContentType, routes, web, HttpResponse, Responder};
|
||||||
use cached::proc_macro::once;
|
use cached::proc_macro::once;
|
||||||
use ramhorns::Content;
|
use ramhorns::Content;
|
||||||
|
@ -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")]
|
||||||
async fn security(config: web::Data<Config>) -> impl Responder {
|
pub 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(),
|
||||||
},
|
},
|
||||||
Infos::default(),
|
InfosPage::default(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/humans.txt")]
|
#[get("/humans.txt")]
|
||||||
async fn humans(config: web::Data<Config>) -> impl Responder {
|
pub 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(),
|
||||||
},
|
},
|
||||||
Infos::default(),
|
InfosPage::default(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/robots.txt")]
|
#[get("/robots.txt")]
|
||||||
async fn robots() -> impl Responder {
|
pub async fn robots() -> impl Responder {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.content_type(ContentType::plaintext())
|
.content_type(ContentType::plaintext())
|
||||||
.body(build_robotstxt())
|
.body(build_robotstxt())
|
||||||
|
@ -71,8 +71,29 @@ fn build_robotstxt() -> String {
|
||||||
"User-agent: * Allow: /".into()
|
"User-agent: * Allow: /".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/sitemap.xml")]
|
#[get("/app.webmanifest")]
|
||||||
async fn sitemap() -> impl Responder {
|
pub async fn webmanifest(config: web::Data<Config>) -> impl Responder {
|
||||||
// TODO
|
HttpResponse::Ok()
|
||||||
actix_web::web::Redirect::to("/")
|
.content_type(ContentType("application/manifest+json".parse().unwrap()))
|
||||||
|
.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(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,53 @@
|
||||||
|
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 Info {
|
struct InfoLove {
|
||||||
unix_epoch: u32,
|
unix_epoch: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/love")]
|
#[get("/love")]
|
||||||
async fn love() -> impl Responder {
|
pub async fn love() -> impl Responder {
|
||||||
HttpResponse::Ok().json(Info {
|
HttpResponse::Ok().json(InfoLove {
|
||||||
unix_epoch: 1605576600,
|
unix_epoch: 1_605_576_600,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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/",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
|
@ -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, web, HttpResponse, Responder};
|
use actix_web::{get, http::header::ContentType, routes, web, HttpResponse, Responder};
|
||||||
use cached::proc_macro::once;
|
use cached::proc_macro::once;
|
||||||
use chrono::{DateTime, Datelike, Local, NaiveDateTime, Utc};
|
use chrono::{DateTime, Datelike, Local, NaiveDateTime, Utc};
|
||||||
use chrono_tz::Europe;
|
use chrono_tz::Europe;
|
||||||
|
@ -18,31 +18,37 @@ use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
misc::{
|
misc::{
|
||||||
date::Date,
|
date::Date,
|
||||||
markdown::{
|
markdown::{get_metadata, get_options, File, FileMetadataBlog, TypeFileMetadata},
|
||||||
get_metadata, get_options, read_file, File, FileMetadataBlog, TypeFileMetadata,
|
utils::{get_url, make_kw, read_file, Html},
|
||||||
},
|
|
||||||
utils::{get_url, make_kw, Html},
|
|
||||||
},
|
},
|
||||||
template::{Infos, NavBar},
|
template::{InfosPage, 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")]
|
||||||
async fn index(config: web::Data<Config>) -> impl Responder {
|
pub async fn index(config: web::Data<Config>) -> impl Responder {
|
||||||
Html(build_index(config.get_ref().to_owned()))
|
Html(build_index(config.get_ref().to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 mut posts = get_posts("data/blog");
|
let blog_dir = format!("{}/{}", config.locations.data_dir, BLOG_DIR);
|
||||||
|
let mut posts = get_posts(&format!("{blog_dir}/{POST_DIR}"));
|
||||||
|
|
||||||
|
// Get about
|
||||||
|
let about: Option<File> =
|
||||||
|
read_file(&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));
|
||||||
|
@ -55,16 +61,17 @@ 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,
|
||||||
},
|
},
|
||||||
Infos {
|
InfosPage {
|
||||||
page_title: Some("Blog".into()),
|
title: Some("Blog".into()),
|
||||||
page_desc: Some(format!(
|
desc: Some(format!(
|
||||||
"Liste des posts d'{}",
|
"Liste des posts d'{}",
|
||||||
config.fc.name.unwrap_or_default()
|
config.fc.name.unwrap_or_default()
|
||||||
)),
|
)),
|
||||||
page_kw: make_kw(&["blog", "blogging"]),
|
kw: Some(make_kw(&["blog", "blogging"])),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -81,13 +88,13 @@ struct Post {
|
||||||
|
|
||||||
impl Post {
|
impl Post {
|
||||||
// Fetch the file content
|
// Fetch the file content
|
||||||
fn fetch_content(&mut self) {
|
fn fetch_content(&mut self, data_dir: &str) {
|
||||||
let blog_dir = "data/blog";
|
let blog_dir = format!("{data_dir}/{BLOG_DIR}/{POST_DIR}");
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
@ -97,60 +104,59 @@ 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 = match std::fs::read_dir(location) {
|
let entries = std::fs::read_dir(location).map_or_else(
|
||||||
Ok(res) => res
|
|_| vec![],
|
||||||
.flatten()
|
|res| {
|
||||||
.filter(|f| match f.path().extension() {
|
res.flatten()
|
||||||
Some(ext) => ext == "md",
|
.filter(|f| f.path().extension().map_or(false, |ext| ext == "md"))
|
||||||
None => false,
|
.collect::<Vec<std::fs::DirEntry>>()
|
||||||
})
|
},
|
||||||
.collect::<Vec<std::fs::DirEntry>>(),
|
);
|
||||||
Err(_) => vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
entries
|
entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|f| {
|
.filter_map(|f| {
|
||||||
let _filename = f.file_name();
|
let fname = f.file_name();
|
||||||
let filename = _filename.to_string_lossy();
|
let filename = fname.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 = match std::fs::read_to_string(format!("{location}/{filename}")) {
|
let file_metadata = std::fs::read_to_string(format!("{location}/{filename}"))
|
||||||
Ok(text) => {
|
.map_or_else(
|
||||||
let arena = Arena::new();
|
|_| FileMetadataBlog {
|
||||||
|
title: Some(file_without_ext.into()),
|
||||||
|
..FileMetadataBlog::default()
|
||||||
|
},
|
||||||
|
|text| {
|
||||||
|
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 = get_metadata(root, TypeFileMetadata::Blog).blog.unwrap();
|
let mut metadata =
|
||||||
|
get_metadata(root, &TypeFileMetadata::Blog).blog.unwrap();
|
||||||
|
|
||||||
// Always have a title
|
// Always have a title
|
||||||
metadata.title = match metadata.title {
|
metadata.title = metadata
|
||||||
Some(title) => Some(title),
|
.title
|
||||||
None => Some(file_without_ext.into()),
|
.map_or_else(|| Some(file_without_ext.into()), Some);
|
||||||
};
|
|
||||||
|
|
||||||
metadata
|
metadata
|
||||||
}
|
},
|
||||||
Err(_) => FileMetadataBlog {
|
);
|
||||||
title: Some(file_without_ext.into()),
|
|
||||||
..FileMetadataBlog::default()
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(true) = file_metadata.publish {
|
if file_metadata.publish == Some(true) {
|
||||||
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(m.created().unwrap()),
|
m.modified().unwrap_or_else(|_| m.created().unwrap()),
|
||||||
)
|
)
|
||||||
.date_naive();
|
.date_naive();
|
||||||
|
|
||||||
|
@ -166,7 +172,7 @@ fn get_posts(location: &str) -> Vec<Post> {
|
||||||
.tags
|
.tags
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|t| t.name.to_owned())
|
.map(|t| t.name.clone())
|
||||||
.collect(),
|
.collect(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -184,13 +190,21 @@ struct BlogPostTemplate {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/blog/p/{id}")]
|
#[get("/blog/p/{id}")]
|
||||||
async fn page(path: web::Path<(String,)>, config: web::Data<Config>) -> impl Responder {
|
pub async fn page(path: web::Path<(String,)>, config: web::Data<Config>) -> impl Responder {
|
||||||
Html(build_post(path.into_inner().0, config.get_ref().to_owned()))
|
Html(build_post(
|
||||||
|
&path.into_inner().0,
|
||||||
|
config.get_ref().to_owned(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_post(file: String, config: Config) -> String {
|
fn build_post(file: &str, config: Config) -> String {
|
||||||
let mut post = None;
|
let mut post = None;
|
||||||
let (infos, toc) = get_post(&mut post, file, config.fc.name.unwrap_or_default());
|
let (infos, toc) = get_post(
|
||||||
|
&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",
|
||||||
|
@ -206,17 +220,22 @@ fn build_post(file: String, config: Config) -> String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_post(post: &mut Option<File>, filename: String, name: String) -> (Infos, String) {
|
fn get_post(
|
||||||
let blog_dir = "data/blog";
|
post: &mut Option<File>,
|
||||||
|
filename: &str,
|
||||||
|
name: &str,
|
||||||
|
data_dir: &str,
|
||||||
|
) -> (InfosPage, String) {
|
||||||
|
let blog_dir = format!("{data_dir}/{BLOG_DIR}/{POST_DIR}");
|
||||||
let ext = ".md";
|
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(),
|
||||||
|
@ -245,30 +264,35 @@ fn get_post(post: &mut Option<File>, filename: String, name: String) -> (Infos,
|
||||||
};
|
};
|
||||||
|
|
||||||
(
|
(
|
||||||
Infos {
|
InfosPage {
|
||||||
page_title: Some(format!("Post: {}", title)),
|
title: Some(format!("Post: {title}")),
|
||||||
page_desc: Some(desc.clone()),
|
desc: Some(desc.clone()),
|
||||||
page_kw: make_kw(
|
kw: Some(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")]
|
||||||
async fn rss(config: web::Data<Config>) -> impl Responder {
|
pub async fn rss(config: web::Data<Config>) -> impl Responder {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.append_header(("content-type", MIME_TYPE_RSS))
|
.content_type(ContentType(MIME_TYPE_RSS.parse().unwrap()))
|
||||||
.body(build_rss(config.get_ref().to_owned()))
|
.body(build_rss(config.get_ref().to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[once(time = 10800)] // 3h
|
#[once(time = 10800)] // 3h
|
||||||
fn build_rss(config: Config) -> String {
|
fn build_rss(config: Config) -> String {
|
||||||
let mut posts = get_posts("data/blog");
|
let mut posts = get_posts(&format!(
|
||||||
|
"{}/{}/{}",
|
||||||
|
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));
|
||||||
|
@ -281,7 +305,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.to_owned()) {
|
let author = if let (Some(mail), Some(name)) = (config.fc.mail, config.fc.fullname.clone()) {
|
||||||
Some(format!("{mail} ({name})"))
|
Some(format!("{mail} ({name})"))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -289,11 +313,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.to_owned(),
|
title: title.clone(),
|
||||||
link: link_to_site.to_owned(),
|
link: link_to_site.clone(),
|
||||||
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.to_owned(),
|
managing_editor: author.clone(),
|
||||||
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"]
|
||||||
|
@ -306,22 +330,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!("{}/icons/favicon-32x32.png", link_to_site),
|
url: format!("{link_to_site}/icons/favicon-32x32.png"),
|
||||||
title: title.to_owned(),
|
title: title.clone(),
|
||||||
link: link_to_site.to_owned(),
|
link: link_to_site.clone(),
|
||||||
..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();
|
p.fetch_content(&config.locations.data_dir);
|
||||||
|
|
||||||
// Build item
|
// Build item
|
||||||
Item {
|
Item {
|
||||||
title: Some(p.title.to_owned()),
|
title: Some(p.title.clone()),
|
||||||
link: Some(format!("{}/blog/p/{}", link_to_site, p.url)),
|
link: Some(format!("{}/blog/p/{}", link_to_site, p.url)),
|
||||||
description: p.content.to_owned(),
|
description: p.content.clone(),
|
||||||
categories: p
|
categories: p
|
||||||
.tags
|
.tags
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -354,7 +378,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!("{}/blog/rss", link_to_site),
|
href: format!("{link_to_site}/blog/rss"),
|
||||||
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()),
|
||||||
|
|
|
@ -7,12 +7,14 @@ use std::fs::read_to_string;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
misc::{
|
misc::{
|
||||||
markdown::{read_file, File, TypeFileMetadata},
|
markdown::{File, TypeFileMetadata},
|
||||||
utils::{make_kw, Html},
|
utils::{make_kw, read_file, Html},
|
||||||
},
|
},
|
||||||
template::{Infos, NavBar},
|
template::{InfosPage, 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| {
|
||||||
|
@ -30,6 +32,7 @@ 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,
|
||||||
|
@ -38,13 +41,12 @@ struct ContactLink {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[once(time = 60)]
|
#[once(time = 60)]
|
||||||
fn find_links() -> Vec<ContactLink> {
|
fn find_links(directory: String) -> Vec<ContactLink> {
|
||||||
// TOML file location
|
// TOML filename
|
||||||
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!("{contacts_dir}/{toml_file}")).unwrap_or_default();
|
let toml_str = read_to_string(format!("{directory}/{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) {
|
||||||
|
@ -74,14 +76,14 @@ fn find_links() -> Vec<ContactLink> {
|
||||||
#[routes]
|
#[routes]
|
||||||
#[get("/{service}")]
|
#[get("/{service}")]
|
||||||
#[get("/{service}/{scope}")]
|
#[get("/{service}/{scope}")]
|
||||||
async fn service_redirection(req: HttpRequest) -> impl Responder {
|
async fn service_redirection(config: web::Data<Config>, req: HttpRequest) -> impl Responder {
|
||||||
let info = req.match_info();
|
let info = req.match_info();
|
||||||
let link = find_links()
|
let link = find_links(format!("{}/{}", config.locations.data_dir, CONTACT_DIR))
|
||||||
.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.to_owned()) {
|
.filter(|&x| match (info.get("scope"), x.scope.clone()) {
|
||||||
// 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
|
||||||
|
@ -105,6 +107,7 @@ async fn service_redirection(req: HttpRequest) -> impl Responder {
|
||||||
#[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>,
|
||||||
|
@ -123,25 +126,31 @@ 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 = "data/contacts";
|
let contacts_dir = format!("{}/{}", config.locations.data_dir, CONTACT_DIR);
|
||||||
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
|
||||||
|
@ -156,6 +165,8 @@ 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,
|
||||||
|
|
||||||
|
@ -165,10 +176,15 @@ fn build_page(config: Config) -> String {
|
||||||
others_exists: !others.is_empty(),
|
others_exists: !others.is_empty(),
|
||||||
others,
|
others,
|
||||||
},
|
},
|
||||||
Infos {
|
InfosPage {
|
||||||
page_title: Some("Contacts".into()),
|
title: Some("Contacts".into()),
|
||||||
page_desc: Some(format!("Réseaux d'{}", config.fc.name.unwrap_or_default())),
|
desc: Some(format!("Réseaux d'{}", config.fc.name.unwrap_or_default())),
|
||||||
page_kw: make_kw(&["réseaux sociaux", "email", "contact", "linktree"]),
|
kw: Some(make_kw(&[
|
||||||
|
"réseaux sociaux",
|
||||||
|
"email",
|
||||||
|
"contact",
|
||||||
|
"linktree",
|
||||||
|
])),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,14 @@ use crate::{
|
||||||
github::{fetch_pr, ProjectState},
|
github::{fetch_pr, ProjectState},
|
||||||
utils::{make_kw, Html},
|
utils::{make_kw, Html},
|
||||||
},
|
},
|
||||||
template::{Infos, NavBar},
|
template::{InfosPage, 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")]
|
||||||
async fn page(config: web::Data<Config>) -> impl Responder {
|
pub 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ struct PortfolioTemplate {
|
||||||
closed: Option<Vec<Project>>,
|
closed: Option<Vec<Project>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Content, Clone, Debug)]
|
#[derive(Clone, Content, Debug)]
|
||||||
struct Project {
|
struct Project {
|
||||||
name: String,
|
name: String,
|
||||||
url: String,
|
url: String,
|
||||||
|
@ -35,7 +35,7 @@ struct Project {
|
||||||
pulls_closed: Vec<Pull>,
|
pulls_closed: Vec<Pull>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Content, Clone, Debug)]
|
#[derive(Clone, Content, Debug)]
|
||||||
struct Pull {
|
struct Pull {
|
||||||
url: String,
|
url: String,
|
||||||
id: u32,
|
id: u32,
|
||||||
|
@ -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();
|
||||||
projects.iter().for_each(|p| {
|
for p in &projects {
|
||||||
let project = Pull {
|
let project = Pull {
|
||||||
url: p.contrib_url.to_owned(),
|
url: p.contrib_url.clone(),
|
||||||
id: p.id,
|
id: p.id,
|
||||||
name_repo: p.project.to_owned(),
|
name_repo: p.name.clone(),
|
||||||
title: p.title.to_owned(),
|
title: p.title.clone(),
|
||||||
state: p.status as u8,
|
state: p.status as u8,
|
||||||
};
|
};
|
||||||
let project_name = p.project.as_str();
|
let project_name = p.name.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.project_url.to_owned(),
|
url: p.url.clone(),
|
||||||
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
|
||||||
data.iter_mut().for_each(|d| {
|
for d in &mut data {
|
||||||
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.into();
|
let state = p.state.try_into().unwrap();
|
||||||
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
|
||||||
data.iter_mut().for_each(|d| {
|
for d in &mut data {
|
||||||
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,
|
||||||
Infos {
|
InfosPage {
|
||||||
page_title: Some("Mes contributions".into()),
|
title: Some("Mes contributions".into()),
|
||||||
page_desc: Some(format!(
|
desc: Some(format!(
|
||||||
"Contributions d'{} à GitHub",
|
"Contributions d'{} à GitHub",
|
||||||
config.fc.name.unwrap_or_default()
|
config.fc.name.unwrap_or_default()
|
||||||
)),
|
)),
|
||||||
page_kw: make_kw(&[
|
kw: Some(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",
|
||||||
]),
|
])),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,145 @@
|
||||||
use actix_web::{get, Responder};
|
use std::path::Path;
|
||||||
|
|
||||||
|
use actix_web::{get, web, Responder};
|
||||||
|
use cached::proc_macro::cached;
|
||||||
|
use ramhorns::Content;
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
misc::{
|
||||||
|
markdown::{File, TypeFileMetadata},
|
||||||
|
utils::{make_kw, read_file, Html},
|
||||||
|
},
|
||||||
|
template::{InfosPage, NavBar},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct PathRequest {
|
||||||
|
q: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/cours")]
|
#[get("/cours")]
|
||||||
async fn page() -> impl Responder {
|
pub async fn page(info: web::Query<PathRequest>, config: web::Data<Config>) -> impl Responder {
|
||||||
// Page de notes de cours
|
Html(build_page(&info, config.get_ref().to_owned()))
|
||||||
// Cf. https://univ.mylloon.fr/
|
}
|
||||||
// Cf. https://github.com/xy2z/PineDocs
|
|
||||||
actix_web::web::Redirect::to("/")
|
#[derive(Content, Debug)]
|
||||||
|
struct CoursTemplate {
|
||||||
|
navbar: NavBar,
|
||||||
|
filetree: String,
|
||||||
|
content: Option<File>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
struct FileNode {
|
||||||
|
name: String,
|
||||||
|
is_dir: bool,
|
||||||
|
children: Vec<FileNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cached]
|
||||||
|
fn compile_patterns(exclusion_list: Vec<String>) -> Vec<Regex> {
|
||||||
|
exclusion_list
|
||||||
|
.iter()
|
||||||
|
.map(|pattern| Regex::new(pattern).unwrap())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_filetree(dir_path: &str, exclusion_patterns: &Vec<Regex>) -> FileNode {
|
||||||
|
let children = std::fs::read_dir(dir_path)
|
||||||
|
.unwrap()
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter_map(|entry| {
|
||||||
|
let entry_path = entry.path();
|
||||||
|
let entry_name = entry_path.file_name()?.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
// Exclude element with the exclusion_list
|
||||||
|
if exclusion_patterns.iter().any(|re| re.is_match(&entry_name)) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry_path.is_file() {
|
||||||
|
Some(FileNode {
|
||||||
|
name: entry_name,
|
||||||
|
is_dir: false,
|
||||||
|
children: vec![],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Exclude empty directories
|
||||||
|
let children_of_children =
|
||||||
|
get_filetree(entry_path.to_str().unwrap(), exclusion_patterns);
|
||||||
|
if children_of_children.is_dir && children_of_children.children.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(children_of_children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
FileNode {
|
||||||
|
name: Path::new(dir_path)
|
||||||
|
.file_name()
|
||||||
|
.unwrap()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
is_dir: true,
|
||||||
|
children,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a page content
|
||||||
|
fn get_content(
|
||||||
|
cours_dir: &str,
|
||||||
|
path: &web::Query<PathRequest>,
|
||||||
|
exclusion_list: &[String],
|
||||||
|
) -> Option<File> {
|
||||||
|
let filename = path.q.as_ref().map_or("index.md", |q| q);
|
||||||
|
|
||||||
|
// We should support regex?
|
||||||
|
if exclusion_list
|
||||||
|
.iter()
|
||||||
|
.any(|excluded_term| filename.contains(excluded_term.as_str()))
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
read_file(
|
||||||
|
&format!("{cours_dir}/{filename}"),
|
||||||
|
&TypeFileMetadata::Generic,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_page(info: &web::Query<PathRequest>, config: Config) -> String {
|
||||||
|
let cours_dir = "data/cours";
|
||||||
|
let exclusion_list = config.fc.exclude_courses.unwrap();
|
||||||
|
let exclusion_patterns = compile_patterns(exclusion_list.clone());
|
||||||
|
let filetree = get_filetree(cours_dir, &exclusion_patterns);
|
||||||
|
|
||||||
|
config.tmpl.render(
|
||||||
|
"cours.html",
|
||||||
|
CoursTemplate {
|
||||||
|
navbar: NavBar {
|
||||||
|
cours: true,
|
||||||
|
..NavBar::default()
|
||||||
|
},
|
||||||
|
filetree: serde_json::to_string(&filetree).unwrap(),
|
||||||
|
content: get_content(cours_dir, info, &exclusion_list),
|
||||||
|
},
|
||||||
|
InfosPage {
|
||||||
|
title: Some("Cours".into()),
|
||||||
|
desc: Some("Cours à l'univ".into()),
|
||||||
|
kw: Some(make_kw(&[
|
||||||
|
"cours",
|
||||||
|
"études",
|
||||||
|
"université",
|
||||||
|
"licence",
|
||||||
|
"master",
|
||||||
|
"notes",
|
||||||
|
"digital garden",
|
||||||
|
])),
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use actix_web::{get, Responder};
|
use actix_web::{get, Responder};
|
||||||
|
|
||||||
#[get("/cv")]
|
#[get("/cv")]
|
||||||
async fn page() -> impl Responder {
|
pub 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
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use actix_web::{get, Responder};
|
use actix_web::{get, Responder};
|
||||||
|
|
||||||
#[get("/gaming")]
|
#[get("/gaming")]
|
||||||
async fn page() -> impl Responder {
|
pub 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("/")
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,23 +4,72 @@ use ramhorns::Content;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
misc::utils::{make_kw, Html},
|
misc::{
|
||||||
template::{Infos, NavBar},
|
markdown::{File, TypeFileMetadata},
|
||||||
|
utils::{make_kw, read_file, Html},
|
||||||
|
},
|
||||||
|
template::{InfosPage, NavBar},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn page(config: web::Data<Config>) -> impl Responder {
|
pub 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,
|
||||||
fullname: String,
|
name: 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 {
|
||||||
|
@ -28,16 +77,17 @@ fn build_page(config: Config) -> String {
|
||||||
index: true,
|
index: true,
|
||||||
..NavBar::default()
|
..NavBar::default()
|
||||||
},
|
},
|
||||||
fullname: config
|
file,
|
||||||
.fc
|
name,
|
||||||
.fullname
|
pronouns,
|
||||||
.to_owned()
|
avatar,
|
||||||
.unwrap_or("Fullname".to_owned()),
|
avatar_caption,
|
||||||
|
avatar_style,
|
||||||
},
|
},
|
||||||
Infos {
|
InfosPage {
|
||||||
page_title: config.fc.fullname,
|
title: config.fc.fullname,
|
||||||
page_desc: Some("Page principale".into()),
|
desc: Some("Page principale".into()),
|
||||||
page_kw: make_kw(&["index", "étudiant"]),
|
kw: Some(make_kw(&["index", "étudiant", "accueil"])),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use actix_web::{get, Responder};
|
use actix_web::{get, Responder};
|
||||||
|
|
||||||
#[get("/memorial")]
|
#[get("/memorial")]
|
||||||
async fn page() -> impl Responder {
|
pub 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("/")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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::{Infos, NavBar},
|
template::{InfosPage, 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,
|
||||||
},
|
},
|
||||||
Infos {
|
InfosPage {
|
||||||
page_desc: Some("Une page perdu du web".into()),
|
desc: Some("Une page perdu du web".into()),
|
||||||
..Infos::default()
|
..InfosPage::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,20 +6,21 @@ use ramhorns::Content;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
misc::{
|
misc::{
|
||||||
markdown::{read_file, File, TypeFileMetadata},
|
markdown::{File, TypeFileMetadata},
|
||||||
utils::{make_kw, Html},
|
utils::{make_kw, read_file, Html},
|
||||||
},
|
},
|
||||||
template::{Infos, NavBar},
|
template::{InfosPage, NavBar},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[get("/portfolio")]
|
#[get("/portfolio")]
|
||||||
async fn page(config: web::Data<Config>) -> impl Responder {
|
pub 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>>,
|
||||||
|
@ -29,25 +30,32 @@ struct PortfolioTemplate<'a> {
|
||||||
|
|
||||||
#[once(time = 60)]
|
#[once(time = 60)]
|
||||||
fn build_page(config: Config) -> String {
|
fn build_page(config: Config) -> String {
|
||||||
let projects_dir = "data/projects";
|
let projects_dir = format!("{}/projects", config.locations.data_dir);
|
||||||
|
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!("{projects_dir}/*{ext}"))
|
let apps = glob(&format!("{apps_dir}/*{ext}"))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map(|e| read_file(&e.unwrap().to_string_lossy(), TypeFileMetadata::Portfolio).unwrap())
|
.map(|e| read_file(&e.unwrap().to_string_lossy(), &TypeFileMetadata::Portfolio).unwrap())
|
||||||
.collect::<Vec<File>>();
|
.collect::<Vec<File>>();
|
||||||
|
|
||||||
let appdata = if apps.is_empty() {
|
let appdata = if apps.is_empty() {
|
||||||
(None, Some(projects_dir))
|
(None, Some(apps_dir.as_str()))
|
||||||
} else {
|
} else {
|
||||||
(Some(apps), None)
|
(Some(apps), None)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get archived apps
|
// Get archived apps
|
||||||
let archived_apps = glob(&format!("{projects_dir}/archive/*{ext}"))
|
let archived_apps = glob(&format!("{apps_dir}/archive/*{ext}"))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map(|e| read_file(&e.unwrap().to_string_lossy(), TypeFileMetadata::Portfolio).unwrap())
|
.map(|e| read_file(&e.unwrap().to_string_lossy(), &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() {
|
||||||
|
@ -63,26 +71,27 @@ 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",
|
||||||
},
|
},
|
||||||
Infos {
|
InfosPage {
|
||||||
page_title: Some("Portfolio".into()),
|
title: Some("Portfolio".into()),
|
||||||
page_desc: Some(format!(
|
desc: Some(format!(
|
||||||
"Portfolio d'{}",
|
"Portfolio d'{}",
|
||||||
config.fc.name.unwrap_or_default()
|
config.fc.name.unwrap_or_default()
|
||||||
)),
|
)),
|
||||||
page_kw: make_kw(&[
|
kw: Some(make_kw(&[
|
||||||
"développeur",
|
"développeur",
|
||||||
"portfolio",
|
"portfolio",
|
||||||
"projets",
|
"projets",
|
||||||
"programmation",
|
"programmation",
|
||||||
"applications",
|
"applications",
|
||||||
"code",
|
"code",
|
||||||
]),
|
])),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use actix_web::{get, Responder};
|
use actix_web::{get, Responder};
|
||||||
|
|
||||||
#[get("/setup")]
|
#[get("/setup")]
|
||||||
async fn page() -> impl Responder {
|
pub 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
|
||||||
|
|
|
@ -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::Infos,
|
template::InfosPage,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[get("/web3")]
|
#[get("/web3")]
|
||||||
async fn page(config: web::Data<Config>) -> impl Responder {
|
pub 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",
|
||||||
(),
|
(),
|
||||||
Infos {
|
InfosPage {
|
||||||
page_title: Some("Mylloon".into()),
|
title: Some("Mylloon".into()),
|
||||||
page_desc: Some("Coin reculé de l'internet".into()),
|
desc: Some("Coin reculé de l'internet".into()),
|
||||||
page_kw: make_kw(&["web3", "blockchain", "nft", "ai"]),
|
kw: Some(make_kw(&["web3", "blockchain", "nft", "ai"])),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,16 +14,18 @@ pub struct Template {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Structure used by /routes/*.rs
|
/// Structure used by /routes/*.rs
|
||||||
#[derive(Default, Debug)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Infos {
|
pub struct InfosPage {
|
||||||
/// Title
|
/// Title
|
||||||
pub page_title: Option<String>,
|
pub title: Option<String>,
|
||||||
/// Description
|
/// Description
|
||||||
pub page_desc: Option<String>,
|
pub desc: Option<String>,
|
||||||
/// Keywords
|
/// Keywords
|
||||||
pub page_kw: Option<String>,
|
pub 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,
|
||||||
|
@ -36,7 +38,7 @@ pub struct NavBar {
|
||||||
|
|
||||||
/// Final structure given to template
|
/// Final structure given to template
|
||||||
#[derive(Content, Debug)]
|
#[derive(Content, Debug)]
|
||||||
struct Data<T> {
|
struct DataPage<T> {
|
||||||
/// App name
|
/// App name
|
||||||
app_name: String,
|
app_name: String,
|
||||||
/// App URL
|
/// App URL
|
||||||
|
@ -54,16 +56,16 @@ struct Data<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Template {
|
impl Template {
|
||||||
pub fn render<C: Content>(&self, template: &str, data: C, info: Infos) -> String {
|
pub fn render<C: Content>(&self, template: &str, data: C, info: InfosPage) -> 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(&Data {
|
tplt.render(&DataPage {
|
||||||
app_name: self.app_name.to_owned(),
|
app_name: self.app_name.clone(),
|
||||||
url: self.url.to_owned(),
|
url: self.url.clone(),
|
||||||
page_title: info.page_title,
|
page_title: info.title,
|
||||||
page_desc: info.page_desc,
|
page_desc: info.desc,
|
||||||
page_kw: info.page_kw,
|
page_kw: info.kw,
|
||||||
page_author: self.name.clone(),
|
page_author: self.name.clone(),
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
|
|
BIN
static/badges/cat.gif
(Stored with Git LFS)
Normal file
BIN
static/badges/cat.gif
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
static/badges/palestine.png
(Stored with Git LFS)
Normal file
BIN
static/badges/palestine.png
(Stored with Git LFS)
Normal file
Binary file not shown.
|
@ -21,7 +21,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Title */
|
/* Title */
|
||||||
h1 {
|
main h1 {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,20 +60,34 @@ main span {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card text */
|
/* Card text */
|
||||||
li h2,
|
main li h2,
|
||||||
li p {
|
main li p {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card titles */
|
/* Card titles */
|
||||||
li h2 {
|
main 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 */
|
||||||
li p {
|
main li p {
|
||||||
font-size: calc(var(--font-size) - 2px);
|
font-size: calc(var(--font-size) - 2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ header > ul:last-of-type li {
|
||||||
/* Post */
|
/* Post */
|
||||||
main {
|
main {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding-block: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,12 +198,34 @@ table.hljs-ln {
|
||||||
font-size: calc(var(--font-size) * 0.8);
|
font-size: calc(var(--font-size) * 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Footnotes */
|
/* Footnote */
|
||||||
.footnotes a {
|
section.footnotes * {
|
||||||
|
font-size: calc(var(--font-size) * 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When multiple ref */
|
||||||
|
a.footnote-backref sup {
|
||||||
|
font-size: calc(var(--font-size) * 0.6);
|
||||||
|
}
|
||||||
|
a.footnote-backref sup::before {
|
||||||
|
content: "(";
|
||||||
|
}
|
||||||
|
a.footnote-backref sup::after {
|
||||||
|
content: ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footnotes links */
|
||||||
|
a.footnote-backref {
|
||||||
font-family: "Segoe UI", "Segoe UI Symbol", system-ui;
|
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;
|
||||||
|
@ -224,3 +246,36 @@ nav#toc {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
/* Better colors for paper */
|
||||||
|
blockquote {
|
||||||
|
border-color: black;
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force line numbering to be on top */
|
||||||
|
td.hljs-ln-line {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Break code */
|
||||||
|
code.hljs {
|
||||||
|
white-space: break-spaces;
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide arrows of backref */
|
||||||
|
a.footnote-backref {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No underline for footnotes */
|
||||||
|
.footnote-ref > a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,18 @@
|
||||||
|
: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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,11 +22,6 @@
|
||||||
--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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
h2 {
|
main h2 {
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ main li {
|
||||||
}
|
}
|
||||||
|
|
||||||
main h1,
|
main h1,
|
||||||
h2 {
|
main h2 {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ main a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
main p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
53
static/css/cours.css
Normal file
53
static/css/cours.css
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/* Filetree */
|
||||||
|
aside {
|
||||||
|
float: left;
|
||||||
|
margin-left: 20px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside li {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element */
|
||||||
|
aside li:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -0.2em;
|
||||||
|
left: -1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside li.collapsed > ul {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside li.directory::before {
|
||||||
|
content: "+";
|
||||||
|
}
|
||||||
|
|
||||||
|
aside li:not(.collapsed).directory::before {
|
||||||
|
content: "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
aside li.directory {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
aside {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main img {
|
||||||
|
max-width: 100%;
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,14 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Ces fontes sont distribuées gratuitement sous Licence publique Creative Commons Attribution 4.0 International :
|
|
||||||
https://creativecommons.org/licenses/by/4.0/legalcode.fr
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
These fonts are freely available under Creative Commons Attribution 4.0 International Public License:
|
|
||||||
https://creativecommons.org/licenses/by/4.0/legalcode
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Luciole © Laurent Bourcellier & Jonathan Perez
|
|
|
@ -38,7 +38,6 @@
|
||||||
|
|
||||||
#avatar {
|
#avatar {
|
||||||
width: calc(var(--font-size) * 5);
|
width: calc(var(--font-size) * 5);
|
||||||
border-radius: 50%;
|
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,8 +63,8 @@ h1 {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#friends a {
|
#friends a:not(h1 > a) {
|
||||||
padding-right: 10px;
|
padding-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#friends h1 {
|
#friends h1 {
|
||||||
|
|
|
@ -69,3 +69,11 @@ 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";
|
||||||
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ main {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* List */
|
/* List */
|
||||||
main ul {
|
main ul:not(ul ul) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
@ -28,13 +28,17 @@ main ul {
|
||||||
|
|
||||||
/* breakpoint */
|
/* breakpoint */
|
||||||
@media only screen and (max-width: 740px) {
|
@media only screen and (max-width: 740px) {
|
||||||
main ul {
|
main ul:not(ul ul) {
|
||||||
grid-template-columns: none;
|
grid-template-columns: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main li:not(ul ul > li) {
|
||||||
|
grid-column: inherit !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card */
|
/* Card */
|
||||||
main li {
|
main li:not(ul ul > li) {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
@ -50,12 +54,16 @@ main li {
|
||||||
margin-inline: 5px;
|
margin-inline: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
main li:hover {
|
main li:not(ul ul > li):nth-child(odd):last-child {
|
||||||
|
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 {
|
main li[role="button"]:hover:not(ul ul > li) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,10 +99,17 @@ 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;
|
||||||
|
|
|
@ -4,6 +4,10 @@ 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);
|
||||||
|
@ -65,3 +69,28 @@ header nav a:hover {
|
||||||
.bold {
|
.bold {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
/* Hide navigation header */
|
||||||
|
header nav {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +1,3 @@
|
||||||
<?xml version="1.0" standalone="no"?>
|
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240" version="1.0" viewBox="0 0 180 180">
|
||||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
<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"/>
|
||||||
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.6 KiB After Width: | Height: | Size: 1.2 KiB |
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
167
static/js/cours.js
Normal file
167
static/js/cours.js
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
/**
|
||||||
|
* Build the filetree
|
||||||
|
* @param {HTMLElement} parent Root element of the filetree
|
||||||
|
* @param {{name: string, is_dir: boolean, children: any[]}} data FileNode
|
||||||
|
* @param {string} location Current location, used for links creation
|
||||||
|
*/
|
||||||
|
const buildFileTree = (parent, data, location) => {
|
||||||
|
const ul = document.createElement("ul");
|
||||||
|
data.forEach((item) => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.classList.add(item.is_dir ? "directory" : "file");
|
||||||
|
|
||||||
|
if (item.is_dir) {
|
||||||
|
// Directory
|
||||||
|
li.textContent = item.name;
|
||||||
|
li.classList.add("collapsed");
|
||||||
|
|
||||||
|
// Toggle collapsing on click
|
||||||
|
li.addEventListener("click", function (e) {
|
||||||
|
if (e.target === li) {
|
||||||
|
li.classList.toggle("collapsed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// File
|
||||||
|
const url = window.location.href.split("?")[0];
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.text = item.name;
|
||||||
|
a.href = `${url}?q=${location}${item.name}`;
|
||||||
|
li.appendChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.appendChild(li);
|
||||||
|
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
buildFileTree(
|
||||||
|
li,
|
||||||
|
item.children,
|
||||||
|
item.is_dir ? location + `${item.name}/` : location
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
parent.appendChild(ul);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uncollapse elements from the deepest element
|
||||||
|
* @param {HTMLLIElement} element Element to uncollapse
|
||||||
|
*/
|
||||||
|
const uncollapse = (element) => {
|
||||||
|
if (element) {
|
||||||
|
element.classList.remove("collapsed");
|
||||||
|
uncollapse(element.parentElement.closest("li"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the deepest opened directory
|
||||||
|
* @param {string[]} path Current path we are looking at, init with fullpath
|
||||||
|
* @param {NodeListOf<ChildNode>} options Options we have, init with list root
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const deepestNodeOpened = (path, options) => {
|
||||||
|
// Iterate over possible options
|
||||||
|
for (let i = 0; i < options.length; ++i) {
|
||||||
|
// If the directory and the current path match
|
||||||
|
if (decodeURI(path[0]) === options[i].firstChild.nodeValue) {
|
||||||
|
if (path.length === 1) {
|
||||||
|
// We found it
|
||||||
|
return options[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue the search
|
||||||
|
return deepestNodeOpened(
|
||||||
|
path.slice(1),
|
||||||
|
options[i].querySelector("ul").childNodes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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", () => {
|
||||||
|
// Build the filetree
|
||||||
|
const fileTreeElement = document.getElementsByTagName("aside")[0];
|
||||||
|
const dataElement = fileTreeElement.getElementsByTagName("span")[0];
|
||||||
|
|
||||||
|
buildFileTree(
|
||||||
|
fileTreeElement,
|
||||||
|
JSON.parse(dataElement.getAttribute("data-json")).children,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
dataElement.remove();
|
||||||
|
|
||||||
|
// Open nested openeded directories
|
||||||
|
const infoURL = window.location.href.split("?");
|
||||||
|
if (infoURL.length > 1) {
|
||||||
|
const fullpath = infoURL[1].substring(2);
|
||||||
|
const path = fullpath.substring(0, fullpath.lastIndexOf("/"));
|
||||||
|
const last_openeded = deepestNodeOpened(
|
||||||
|
path.split("/"),
|
||||||
|
fileTreeElement.querySelector("ul").childNodes
|
||||||
|
);
|
||||||
|
|
||||||
|
uncollapse(last_openeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix SVG images in dark mode
|
||||||
|
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
|
svgDarkTheme();
|
||||||
|
}
|
||||||
|
});
|
|
@ -6,24 +6,63 @@ class Tag {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
|
const clipping_text = `
|
||||||
|
display: inline;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text; /* Chromium fix */
|
||||||
|
color: transparent;
|
||||||
|
`;
|
||||||
|
const mono = "font-family: monospace";
|
||||||
|
|
||||||
const tags = [
|
const tags = [
|
||||||
new Tag("Comment ça marche un PC 😵💫"),
|
new Tag("Comment fonctionne un PC 😵💫"),
|
||||||
new Tag("Idiot certifié"),
|
new Tag("undefined", mono),
|
||||||
new Tag("undefined", "font-family: monospace"),
|
new Tag("/api/v1/love", mono),
|
||||||
new Tag("/api/v1/love", "font-family: monospace"),
|
new Tag("/api/v1/websites", mono),
|
||||||
new Tag("Étudiant qui va rater son master"),
|
|
||||||
new Tag("Peak D2 sur Valo 🤡"),
|
new Tag("Peak D2 sur Valo 🤡"),
|
||||||
new Tag(
|
new Tag(
|
||||||
"1312",
|
"0x520",
|
||||||
`
|
`
|
||||||
display: inline;
|
|
||||||
background: linear-gradient(to bottom right, red 0%, red 50%, black 50%);
|
background: linear-gradient(to bottom right, red 0%, red 50%, black 50%);
|
||||||
background-clip: text;
|
${clipping_text}
|
||||||
-webkit-background-clip: text; /* Chromium fix */
|
text-shadow: 0px 0px 20px light-dark(transparent, var(--font-color));
|
||||||
color: transparent;
|
|
||||||
`
|
`
|
||||||
),
|
),
|
||||||
new Tag("Nul en CSS", "font-family: 'Comic Sans MS', cursive"),
|
new Tag("Nul en CSS", "font-family: 'Comic Sans MS', TSCu_Comic, 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));
|
||||||
|
|
452
static/js/libs/hljs-languages/julia.js
Normal file
452
static/js/libs/hljs-languages/julia.js
Normal file
|
@ -0,0 +1,452 @@
|
||||||
|
/*! `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);
|
||||||
|
})();
|
93
static/js/libs/hljs-languages/ocaml.js
Normal file
93
static/js/libs/hljs-languages/ocaml.js
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
/*! `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);
|
||||||
|
})();
|
|
@ -9,6 +9,7 @@ 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 });
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
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`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -10,17 +10,19 @@ window.addEventListener("load", () => {
|
||||||
la: "leftarrow",
|
la: "leftarrow",
|
||||||
RA: "Rightarrow",
|
RA: "Rightarrow",
|
||||||
LA: "Leftarrow",
|
LA: "Leftarrow",
|
||||||
|
u: "mu",
|
||||||
})
|
})
|
||||||
)[Symbol.iterator]()) {
|
)[Symbol.iterator]()) {
|
||||||
macros[`\\${item[0]}`] = `\\${item[1]}`;
|
const bs = "\\";
|
||||||
|
macros[`${bs}${item[0]}`] = `${bs}${item[1]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderMathInElement(document.body, {
|
const attribute = "data-math-style";
|
||||||
delimiters: [
|
for (const element of document.querySelectorAll(`span[${attribute}]`)) {
|
||||||
{ left: "$$", right: "$$", display: true },
|
katex.render(element.textContent, element, {
|
||||||
{ left: "$", right: "$", display: false },
|
throwOnError: false,
|
||||||
],
|
displayMode: element.getAttribute(attribute) === "display",
|
||||||
throwOnError: false,
|
macros: macros,
|
||||||
macros,
|
});
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
17
static/js/mail_obfuscation.js
Normal file
17
static/js/mail_obfuscation.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
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)
Normal file
BIN
static/pics/me.png
(Stored with Git LFS)
Normal file
Binary file not shown.
21
templates/app.webmanifest
Normal file
21
templates/app.webmanifest
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -16,8 +16,8 @@
|
||||||
{{#data}}
|
{{#data}}
|
||||||
|
|
||||||
<h1>Blog</h1>
|
<h1>Blog</h1>
|
||||||
<p>Blog perso, je dis peut-être n'importe quoi 🫶</p>
|
{{#about}} {{&content}} {{/about}}
|
||||||
<a id="rss" href="/blog/rss">Lien vers le flux RSS</a>
|
<a id="rss" href="/blog/blog.rss">Lien vers le flux RSS</a>
|
||||||
|
|
||||||
{{#no_posts}}
|
{{#no_posts}}
|
||||||
<h2>Aucun posts</h2>
|
<h2>Aucun posts</h2>
|
||||||
|
@ -27,14 +27,18 @@
|
||||||
{{#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>{{title}}</h2>
|
<h2><a href="/blog/p/{{url}} ">{{title}}</a></h2>
|
||||||
{{#desc}}
|
{{#desc}}
|
||||||
<p>{{desc}}</p>
|
<p>{{desc}}</p>
|
||||||
{{/desc}}
|
{{/desc}}
|
||||||
</li>
|
</li>
|
||||||
{{/posts}}
|
{{/posts}}
|
||||||
</ul>
|
</ul>
|
||||||
{{/no_posts}} {{/data}}
|
{{/no_posts}}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{{#about}} {{#metadata}}
|
||||||
|
{{#mail_obfsucated}}{{>libs/mail_obfuscater.html}}{{/mail_obfsucated}}
|
||||||
|
{{/metadata}} {{/about}} {{/data}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
{{#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>
|
||||||
|
|
|
@ -8,9 +8,8 @@
|
||||||
<header>{{>navbar.html}}</header>
|
<header>{{>navbar.html}}</header>
|
||||||
<main>
|
<main>
|
||||||
<h1>Contact</h1>
|
<h1>Contact</h1>
|
||||||
<p>Je suis présent relativement partout sur internet 😸</p>
|
{{#data}}{{#about}} {{&content}} {{/about}} {{#socials_exists}}
|
||||||
|
|
||||||
{{#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}}
|
||||||
|
@ -25,7 +24,11 @@
|
||||||
<ul>
|
<ul>
|
||||||
{{#others}} {{>contact/element.html}} {{/others}}
|
{{#others}} {{>contact/element.html}} {{/others}}
|
||||||
</ul>
|
</ul>
|
||||||
{{/others_exists}} {{/data}}
|
{{/others_exists}}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{{#about}} {{#metadata}}
|
||||||
|
{{#mail_obfsucated}}{{>libs/mail_obfuscater.html}}{{/mail_obfsucated}}
|
||||||
|
{{/metadata}} {{/about}} {{/data}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
31
templates/cours.html
Normal file
31
templates/cours.html
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head dir="ltr">
|
||||||
|
{{>head.html}}
|
||||||
|
<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>
|
||||||
|
<body>
|
||||||
|
<header>{{>navbar.html}}</header>
|
||||||
|
|
||||||
|
<aside>
|
||||||
|
<span data-json="{{filetree}} "></span>
|
||||||
|
</aside>
|
||||||
|
<main>
|
||||||
|
{{^content}}
|
||||||
|
<p>Fichier introuvable</p>
|
||||||
|
{{/content}} {{#content}}
|
||||||
|
<article>{{&content}}</article>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{{#metadata}} {{#mermaid}}{{>libs/mermaid_footer.html}}{{/mermaid}}
|
||||||
|
{{#math}}{{>libs/katex_footer.html}}{{/math}}
|
||||||
|
{{#syntax_highlight}}{{>libs/hljs_footer.html}}{{/syntax_highlight}}
|
||||||
|
{{#mail_obfsucated}}{{>libs/mail_obfuscater.html}}{{/mail_obfsucated}}
|
||||||
|
{{/metadata}} {{/content}} {{/data}}
|
||||||
|
<script src="/js/cours.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -3,6 +3,7 @@
|
||||||
<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}}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
/* TEAM */{{#data}}
|
/* TEAM */{{#data}}
|
||||||
{{name}}
|
{{name}}
|
||||||
Contact: {{contact}}
|
Contact: {{contact}}
|
||||||
Lang: {{lang}}
|
Lang: {{lang}}{{/data}}
|
||||||
|
|
||||||
/* THANKS */
|
/* THANKS */
|
||||||
All the dependencies I use for building ewp
|
The dependencies of EWP are available in the Cargo.toml file, see:
|
||||||
They are listed here: https://git.mylloon.fr/Anri/mylloon.fr/src/branch/main/Cargo.toml
|
https://git.mylloon.fr/Anri/mylloon.fr/src/branch/main/Cargo.toml
|
||||||
|
|
||||||
|
|
||||||
/* SITE */
|
/* SITE: EWP */
|
||||||
Authored by Anri Kennel (Mylloon)
|
Author: Anri (Mylloon) Kennel
|
||||||
Standards: HTML5, CSS3, mustache templates, Docker, TOML, Markdown, XML
|
Standards: HTML5, CSS3, mustache templates, Docker, TOML, Markdown, RSS, YAML
|
||||||
Components: Rust, JavaScript, Internet
|
Components: Rust, JavaScript, Internet
|
||||||
Software: Visual Studio Code
|
Software: Visual Studio Code and VSCodium
|
||||||
|
|
||||||
{{/data}}
|
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
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" />
|
||||||
|
|
|
@ -3,78 +3,46 @@
|
||||||
<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">{{fullname}}</span>
|
<span id="name">{{name}}</span>
|
||||||
<span id="pronouns">(il/lui, he/him)</span>
|
{{#pronouns}}<span id="pronouns">{{pronouns}}</span>{{/pronouns}}
|
||||||
<img
|
<img
|
||||||
id="avatar"
|
id="avatar"
|
||||||
src="/icons/apple-touch-icon.png"
|
src="{{avatar}} "
|
||||||
alt="Avatar"
|
alt="Avatar"
|
||||||
title="Mon avatar, dessiné un jour super rapidement sur Gimp."
|
title="{{avatar_caption}} "
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p id="subname"></p>
|
<p id="subname"></p>
|
||||||
|
|
||||||
<article>
|
{{#file}} {{&content}} {{/file}} {{^file}}
|
||||||
<h1>Qui suis-je ?</h1>
|
<p>
|
||||||
<p>Je m'appelle <b>Anri</b>, mon pseudo est <b>Mylloon</b>.</p>
|
<b>Welcome to EWP</b>, create a <code>index.md</code> file inside your
|
||||||
<p>
|
<code>data/</code> directory to get started.
|
||||||
J'aime beaucoup l'informatique depuis très petit, ce site est écrit de
|
</p>
|
||||||
A à Z par moi-même (modulo la quantité astronomique de librairie
|
{{/file}}
|
||||||
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>
|
|
||||||
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>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.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>
|
||||||
|
|
|
@ -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.7.0/styles/base16/solarized-light.min.css"
|
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/a11y-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.7.0/styles/base16/dracula.min.css"
|
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/a11y-dark.min.css"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,13 +1,13 @@
|
||||||
<script
|
<script
|
||||||
defer
|
defer
|
||||||
src="//cdn.jsdelivr.net/npm/katex@0.16.6/dist/katex.min.js"
|
src="//cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js"
|
||||||
integrity="sha384-j/ZricySXBnNMJy9meJCtyXTKMhIJ42heyr7oAdxTDBy/CYA9hzpMo+YTNV5C+1X"
|
integrity="sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
></script>
|
></script>
|
||||||
<script
|
<script
|
||||||
defer
|
defer
|
||||||
src="//cdn.jsdelivr.net/npm/katex@0.16.6/dist/contrib/auto-render.min.js"
|
src="//cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js"
|
||||||
integrity="sha384-+VBxd3r6XgURycqtZ117nYw44OOcIax56Z4dCRWbxyPt0Koah1uHoK0o4+/RRE05"
|
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>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="//cdn.jsdelivr.net/npm/katex@0.16.6/dist/katex.min.css"
|
href="//cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css"
|
||||||
integrity="sha384-mXD7x5S50Ko38scHSnD4egvoExgMPbrseZorkbE49evAfv9nNcbrXJ8LLNsDgh9d"
|
integrity="sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
/>
|
/>
|
||||||
|
|
1
templates/libs/mail_obfuscater.html
Normal file
1
templates/libs/mail_obfuscater.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<script defer src="/js/mail_obfuscation.js"></script>
|
|
@ -1 +1 @@
|
||||||
<script type="module" src="/js/libs/mermaid.js"></script>
|
<script async type="module" src="/js/libs/mermaid.js"></script>
|
||||||
|
|
|
@ -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}}"
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
{{#metadata}} {{#info}} {{#portfolio}} {{#link}}
|
{{#metadata}} {{#info}} {{#portfolio}} {{#link}}
|
||||||
<li role="button" onclick="window.open('{{link}}', '_blank', 'noreferrer');">
|
<li
|
||||||
|
role="button"
|
||||||
|
onmousedown="disableScroll()"
|
||||||
|
onmouseup="openLink('{{link}}');"
|
||||||
|
>
|
||||||
{{>portfolio/project.html}}
|
{{>portfolio/project.html}}
|
||||||
</li>
|
</li>
|
||||||
{{/link}} {{^link}}
|
{{/link}} {{^link}}
|
||||||
|
|
|
@ -10,14 +10,10 @@
|
||||||
<main>
|
<main>
|
||||||
{{#data}}
|
{{#data}}
|
||||||
<h1>Portfolio</h1>
|
<h1>Portfolio</h1>
|
||||||
<p>
|
{{#about}} {{&content}} {{/about}}
|
||||||
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>
|
|
||||||
|
|
||||||
{{#location_apps}}
|
|
||||||
<!-- Error message -->
|
<!-- Error message -->
|
||||||
|
{{#location_apps}}
|
||||||
<p>{{location_apps}} {{err_msg}}</p>
|
<p>{{location_apps}} {{err_msg}}</p>
|
||||||
{{/location_apps}} {{^location_apps}}
|
{{/location_apps}} {{^location_apps}}
|
||||||
|
|
||||||
|
@ -31,20 +27,62 @@
|
||||||
<ul>
|
<ul>
|
||||||
{{#archived_apps}} {{>portfolio/card.html}} {{/archived_apps}}
|
{{#archived_apps}} {{>portfolio/card.html}} {{/archived_apps}}
|
||||||
</ul>
|
</ul>
|
||||||
{{/archived_apps_exists}} {{/location_apps}} {{/data}}
|
{{/archived_apps_exists}} {{/location_apps}}
|
||||||
</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("click", function (event) {
|
link.addEventListener("mouseup", 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>
|
||||||
|
|
|
@ -263,6 +263,22 @@
|
||||||
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 />
|
||||||
|
|
Loading…
Reference in a new issue