Compare commits

..

20 commits

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

View file

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

14
.woodpecker/publish.yml Normal file
View file

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

1891
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -10,28 +10,21 @@ publish = false
license = "AGPL-3.0-or-later"
[dependencies]
actix-web = { version = "4.9", default-features = false, features = ["macros", "compress-brotli"] }
actix-web = { version = "4.4", default-features = false, features = ["macros", "compress-brotli"] }
actix-files = "0.6"
cached = { version = "0.53", features = ["async", "ahash"] }
ramhorns = "1.0"
cached = { version = "0.46", features = ["async"] }
ramhorns = "0.14"
toml = "0.8"
serde = { version = "1.0", features = ["derive"] }
serde_yml = "0.0"
serde_yaml = "0.9"
serde_json = "1.0"
minify-html = "0.15"
minify-js = "0.6"
minify-html = "0.11"
minify-js = "0.5"
glob = "0.3"
comrak = "0.28"
reqwest = { version = "0.12", features = ["json"] }
chrono = { version = "0.4.38", default-features = false, features = ["clock"]}
chrono-tz = "0.10"
comrak = "0.19"
reqwest = { version = "0.11", features = ["json"] }
chrono = { version = "0.4.30", default-features = false, features = ["clock"]}
chrono-tz = "0.8"
rss = { version = "2.0", features = ["atom"] }
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"

View file

@ -18,7 +18,6 @@ WORKDIR /app
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/README.md /app/README.md
COPY --from=builder /usr/src/ewp/static /app/static
COPY --from=builder /usr/src/ewp/templates /app/templates

View file

@ -10,7 +10,6 @@
- [Global configuration](#global-configuration)
- [Link shortener for contacts](#link-shortener-for-contacts)
- [Add content](#add-content)
- [Index](#index)
- [Blog](#blog)
- [Projects](#projects)
- [Contacts](#contacts)
@ -106,7 +105,6 @@ onion = "http://youraddress.onion/"
app_name = "Nickname" # fallback to 'EWP' if none
name = "Firstname"
fullname = "Fullname"
exclude_courses = []
```
## Link shortener for contacts
@ -141,29 +139,9 @@ option: value
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
Markdown files are stored in `/app/data/blog/posts/`
Markdown files are stored in `/app/data/blog/`
```
---
@ -180,15 +158,11 @@ Post content
- If no `title`, the filename will be used
- `date` format is `day-month-year`
- `publish` is default to false. When false, posts are hidden from index
but accessible, see [#30](https://git.mylloon.fr/Anri/mylloon.fr/issues/30)
### About <!-- omit in toc -->
The file is stored at `/app/data/blog/about.md`.
but accessible, see #30
## Projects
Markdown files are stored in `/app/data/projects/apps/`
Markdown files are stored in `/app/data/projects/`
```
---
@ -203,14 +177,7 @@ Project description
- If no `link` : the div won't be clickable and will be reported as is to the user
(no corner-arrow)
- 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`.
- Note that only a handful of [`language`s are supported](./static/css/languages.css).
## Contacts
@ -239,18 +206,6 @@ Custom project description
- `description` will be rendered as HTML "title" (text will appear when cursor
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/`

View file

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

View file

@ -3,7 +3,7 @@
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)
![status-badge](https://git.mylloon.fr/Anri/mylloon.fr/badges/workflows/publish.yml/badge.svg)
[![status-badge](https://ci.mylloon.fr/api/badges/Anri/mylloon.fr/status.svg)](https://ci.mylloon.fr/Anri/mylloon.fr)
- See [issues](https://git.mylloon.fr/Anri/mylloon.fr/issues)
- See [documentation](https://git.mylloon.fr/Anri/mylloon.fr/src/branch/main/Documentation.md)
- See [documentation](./Documentation.md)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
use crate::{config::Config, misc::utils::get_url, template::InfosPage};
use crate::{config::Config, misc::utils::get_url, template::Infos};
use actix_web::{get, http::header::ContentType, routes, web, HttpResponse, Responder};
use cached::proc_macro::once;
use ramhorns::Content;
@ -6,7 +6,7 @@ use ramhorns::Content;
#[routes]
#[get("/.well-known/security.txt")]
#[get("/security.txt")]
pub async fn security(config: web::Data<Config>) -> impl Responder {
async fn security(config: web::Data<Config>) -> impl Responder {
HttpResponse::Ok()
.content_type(ContentType::plaintext())
.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(),
pref_lang: config.fc.lang.unwrap_or_default(),
},
InfosPage::default(),
Infos::default(),
)
}
#[get("/humans.txt")]
pub async fn humans(config: web::Data<Config>) -> impl Responder {
async fn humans(config: web::Data<Config>) -> impl Responder {
HttpResponse::Ok()
.content_type(ContentType::plaintext())
.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(),
name: config.fc.fullname.unwrap_or_default(),
},
InfosPage::default(),
Infos::default(),
)
}
#[get("/robots.txt")]
pub async fn robots() -> impl Responder {
async fn robots() -> impl Responder {
HttpResponse::Ok()
.content_type(ContentType::plaintext())
.body(build_robotstxt())
@ -71,29 +71,8 @@ fn build_robotstxt() -> String {
"User-agent: * Allow: /".into()
}
#[get("/app.webmanifest")]
pub async fn webmanifest(config: web::Data<Config>) -> impl Responder {
HttpResponse::Ok()
.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(),
)
#[get("/sitemap.xml")]
async fn sitemap() -> impl Responder {
// TODO
actix_web::web::Redirect::to("/")
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,72 +4,23 @@ use ramhorns::Content;
use crate::{
config::Config,
misc::{
markdown::{File, TypeFileMetadata},
utils::{make_kw, read_file, Html},
},
template::{InfosPage, NavBar},
misc::utils::{make_kw, Html},
template::{Infos, NavBar},
};
#[get("/")]
pub async fn page(config: web::Data<Config>) -> impl Responder {
async fn page(config: web::Data<Config>) -> impl Responder {
Html(build_page(config.get_ref().to_owned()))
}
#[derive(Content, Debug)]
struct IndexTemplate {
navbar: NavBar,
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,
fullname: String,
}
#[once(time = 60)]
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(
"index.html",
IndexTemplate {
@ -77,17 +28,16 @@ fn build_page(config: Config) -> String {
index: true,
..NavBar::default()
},
file,
name,
pronouns,
avatar,
avatar_caption,
avatar_style,
fullname: config
.fc
.fullname
.to_owned()
.unwrap_or("Fullname".to_owned()),
},
InfosPage {
title: config.fc.fullname,
desc: Some("Page principale".into()),
kw: Some(make_kw(&["index", "étudiant", "accueil"])),
Infos {
page_title: config.fc.fullname,
page_desc: Some("Page principale".into()),
page_kw: make_kw(&["index", "étudiant"]),
},
)
}

View file

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

View file

@ -5,7 +5,7 @@ use ramhorns::Content;
use crate::{
config::Config,
misc::utils::{get_url, Html},
template::{InfosPage, NavBar},
template::{Infos, NavBar},
};
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()),
onion: config.fc.onion,
},
InfosPage {
desc: Some("Une page perdu du web".into()),
..InfosPage::default()
Infos {
page_desc: Some("Une page perdu du web".into()),
..Infos::default()
},
)
}

View file

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

View file

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

View file

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

View file

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

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

Binary file not shown.

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

Binary file not shown.

View file

@ -21,7 +21,7 @@
}
/* Title */
main h1 {
h1 {
margin-bottom: 0;
}
@ -60,34 +60,20 @@ main span {
}
/* Card text */
main li h2,
main li p {
li h2,
li p {
margin: 0px;
padding-top: 5px;
}
/* Card titles */
main li h2,
main li h2 a {
li h2 {
color: var(--title-color);
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 */
main li p {
li p {
font-size: calc(var(--font-size) - 2px);
}

View file

@ -66,7 +66,7 @@ header > ul:last-of-type li {
/* Post */
main {
margin: 0;
padding-block: 0;
padding: 0;
max-width: 100%;
}
@ -198,34 +198,12 @@ table.hljs-ln {
font-size: calc(var(--font-size) * 0.8);
}
/* Footnote */
section.footnotes * {
font-size: calc(var(--font-size) * 0.8);
}
/* When multiple ref */
a.footnote-backref sup {
font-size: calc(var(--font-size) * 0.6);
}
a.footnote-backref sup::before {
content: "(";
}
a.footnote-backref sup::after {
content: ")";
}
/* Footnotes links */
a.footnote-backref {
/* Footnotes */
.footnotes a {
font-family: "Segoe UI", "Segoe UI Symbol", system-ui;
text-decoration: underline dotted;
}
/* Footnotes block separation from article */
section.footnotes {
margin: 3px;
border-top: 2px dotted var(--separator-color);
}
/* Mermaid diagrams */
pre:has(code.language-mermaid) {
text-align: center;
@ -246,36 +224,3 @@ nav#toc {
visibility: hidden;
}
}
@media print {
/* Better colors for paper */
blockquote {
border-color: black;
background: var(--background);
}
.hljs {
background: var(--background);
}
/* Force line numbering to be on top */
td.hljs-ln-line {
vertical-align: top;
}
/* Break code */
code.hljs {
white-space: break-spaces;
hyphens: none;
}
/* Hide arrows of backref */
a.footnote-backref {
visibility: hidden;
}
/* No underline for footnotes */
.footnote-ref > a {
text-decoration: none;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,10 +4,6 @@ html {
font-family: var(--font-family);
}
::selection {
background-color: var(--selection-color);
}
body,
a {
color: var(--font-color);
@ -71,26 +67,7 @@ header nav a:hover {
}
@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;
visibility: hidden;
}
}

View file

@ -1,3 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240" version="1.0" viewBox="0 0 180 180">
<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"/>
<?xml version="1.0" standalone="no"?>
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="180.000000pt" height="180.000000pt" viewBox="0 0 180.000000 180.000000" preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,180.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none">
<path d="M380 1780 c0 -47 56 -117 85 -107 19 8 19 47 1 92 -13 31 -19 35 -50 35 -30 0 -36 -4 -36 -20z"/>
<path d="M1220 1740 c-34 -34 -17 -127 26 -144 27 -10 39 10 36 65 -3 89 -25 116 -62 79z"/>
<path d="M789 1729 c-22 -41 12 -119 51 -119 25 0 32 26 19 74 -18 68 -47 87 -70 45z"/>
<path d="M0 1683 c0 -55 3 -64 26 -82 33 -26 41 -26 54 0 15 28 -1 66 -46 110 l-34 33 0 -61z"/>
<path d="M1563 1686 c-20 -43 -11 -86 18 -86 25 0 53 62 45 96 -9 36 -43 31 -63 -10z"/>
<path d="M343 1395 c-47 -33 -37 -116 19 -158 36 -27 74 -16 95 27 41 87 -42 182 -114 131z"/>
<path d="M1313 1382 c-12 -9 -27 -32 -34 -49 -23 -69 52 -153 138 -153 84 0 119 70 76 153 -31 60 -131 87 -180 49z"/>
<path d="M862 998 c-19 -19 -14 -46 16 -91 16 -23 32 -50 35 -60 4 -9 18 -22 32 -29 22 -10 30 -8 53 9 15 11 28 32 30 47 9 73 -120 170 -166 124z"/>
<path d="M1428 779 c-29 -16 -21 -63 27 -159 82 -164 65 -189 -173 -247 -192 -47 -434 -29 -646 48 -108 39 -160 83 -181 152 -16 54 -27 67 -61 67 -32 0 -54 -31 -54 -76 0 -63 50 -138 122 -184 178 -116 546 -178 784 -134 30 6 34 2 88 -75 61 -86 85 -106 116 -96 43 14 33 73 -28 152 -13 18 -22 39 -19 46 3 8 33 24 68 37 111 42 159 111 146 211 -7 54 -91 223 -123 248 -30 23 -41 25 -66 10z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

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

View file

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

View file

@ -6,62 +6,24 @@ class Tag {
}
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 = [
new Tag("Comment fonctionne un PC 😵‍💫"),
new Tag("undefined", mono),
new Tag("/api/v1/love", mono),
new Tag("Comment ça marche un PC 😵‍💫"),
new Tag("Idiot certifié"),
new Tag("undefined", "font-family: monospace"),
new Tag("/api/v1/love", "font-family: monospace"),
new Tag("Étudiant qui va rater son master"),
new Tag("Peak D2 sur Valo 🤡"),
new Tag(
"0x520",
"1312",
`
display: inline;
background: linear-gradient(to bottom right, red 0%, red 50%, black 50%);
${clipping_text}
text-shadow: 0px 0px 20px light-dark(transparent, var(--font-color));
background-clip: text;
-webkit-background-clip: text; /* Chromium fix */
color: transparent;
`
),
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),
new Tag("Nul en CSS", "font-family: 'Comic Sans MS', cursive"),
];
const random = Math.round(Math.random() * (tags.length - 1));

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

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

Binary file not shown.

View file

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

View file

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

View file

@ -36,7 +36,6 @@
{{#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}} {{/post}} {{/data}}
</body>
</html>

View file

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

View file

@ -3,19 +3,15 @@
<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>
<span data-json="{{#data}}{{filetree}}{{/data}} "></span>
</aside>
<main>
{{^content}}
{{#data}} {{^content}}
<p>Fichier introuvable</p>
{{/content}} {{#content}}
<article>{{&content}}</article>
@ -24,7 +20,6 @@
{{#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>

View file

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

View file

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

View file

@ -16,6 +16,7 @@
sizes="16x16"
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="shortcut icon" href="/icons/favicon.ico" />
<meta name="msapplication-TileColor" content="#ffffff" />

View file

@ -3,46 +3,78 @@
<head dir="ltr">
{{>head.html}}
<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>
<body>
<header>{{>navbar.html}}</header>
<main>
{{#data}}
<div>
<span id="name">{{name}}</span>
{{#pronouns}}<span id="pronouns">{{pronouns}}</span>{{/pronouns}}
<span id="name">{{fullname}}</span>
<span id="pronouns">(il/lui, he/him)</span>
<img
id="avatar"
src="{{avatar}} "
src="/icons/apple-touch-icon.png"
alt="Avatar"
title="{{avatar_caption}} "
title="Mon avatar, dessiné un jour super rapidement sur Gimp."
loading="lazy"
/>
</div>
<p id="subname"></p>
{{#file}} {{&content}} {{/file}} {{^file}}
<p>
<b>Welcome to EWP</b>, create a <code>index.md</code> file inside your
<code>data/</code> directory to get started.
</p>
{{/file}}
<article>
<h1>Qui suis-je ?</h1>
<p>Je m'appelle <b>Anri</b>, mon pseudo est <b>Mylloon</b>.</p>
<p>
J'aime beaucoup l'informatique depuis très petit, ce site est écrit de
A à Z par moi-même (modulo la quantité astronomique de librairie
utilisé) en Rust. J'adore le monde de l'open source, l'immense
majorité de mes projets sont sous licence
<a
href="https://en.wikipedia.org/wiki/GNU_Affero_General_Public_License"
target="_blank"
rel="noreferrer"
>AGPLv3</a
>.
</p>
<p>
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>
<script src="/js/index.js"></script>
{{#file}} {{#metadata}}
{{#mail_obfsucated}}{{>libs/mail_obfuscater.html}}{{/mail_obfsucated}}
{{/metadata}} {{/file}} {{/data}}
</body>
</html>

View file

@ -1,6 +1,4 @@
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
<script src="//unpkg.com/highlightjs-copy/dist/highlightjs-copy.min.js"></script>
<script src="//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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,10 +10,14 @@
<main>
{{#data}}
<h1>Portfolio</h1>
{{#about}} {{&content}} {{/about}}
<p>
Je programme depuis 2018 et j'ai appris une multitude de langages
depuis. Étant passionné de logiciels libres depuis que je m'y intéresse,
je publie tout sur des forges publiques.
</p>
<!-- Error message -->
{{#location_apps}}
<!-- Error message -->
<p>{{location_apps}} {{err_msg}}</p>
{{/location_apps}} {{^location_apps}}
@ -27,62 +31,20 @@
<ul>
{{#archived_apps}} {{>portfolio/card.html}} {{/archived_apps}}
</ul>
{{/archived_apps_exists}} {{/location_apps}}
{{/archived_apps_exists}} {{/location_apps}} {{/data}}
</main>
{{#about}} {{#metadata}}
{{#mail_obfsucated}}{{>libs/mail_obfuscater.html}}{{/mail_obfsucated}}
{{/metadata}} {{/about}} {{/data}}
<script>
/* Fix links in list */
window.addEventListener("load", () =>
document.querySelectorAll("main a").forEach(function (link) {
link.setAttribute("target", "_blank");
link.setAttribute("rel", "noreferrer");
link.addEventListener("mouseup", function (event) {
link.addEventListener("click", function (event) {
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>
</body>
</html>

View file

@ -263,22 +263,6 @@
alt="humans.txt"
title="We are humans"
/></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>
<br />