Compare commits

...

108 commits

Author SHA1 Message Date
7432ffd5f9
clickable badge 2024-09-19 15:53:28 +02:00
764a632ae6
/api/v1/websites 2024-09-19 15:50:37 +02:00
396bff909e
update dependencies 2024-09-14 21:01:30 +02:00
fb44c25e47
clippy 2024-09-14 20:17:11 +02:00
a7aec1b94e
add countdown 2024-08-29 23:31:35 +02:00
8cedeb531d
cleanup using https://github.com/actions/checkout/issues/1830#issuecomment-2314758792 answer 2024-08-28 16:43:30 +02:00
912f16e0c3
remove jas 2024-08-13 13:21:25 +02:00
999d68ab60
includes images in releases 2024-08-13 13:01:20 +02:00
2d9fc0d559
only run workflow manually 2024-08-13 11:55:12 +02:00
8c386d5ac6
use forgejo actions (#72)
Some checks failed
Publish latest version / build (push) Has been cancelled
2024-08-11 16:49:06 +02:00
95b92699ed
update dependencies
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-07-23 10:26:19 +02:00
2dc54a6f76
turn out to be weird with blockquote
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-07-15 10:24:51 +02:00
deb54372a2
update comrak
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-07-13 00:29:07 +02:00
485797c64f
update comrak
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-07-12 17:52:38 +02:00
5b43730150
update dependencie "cached"
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-07-10 23:02:49 +02:00
b145510d83
custom selection color
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-07-10 22:58:35 +02:00
847ec0d3c3
sursis
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-07-10 19:56:36 +02:00
e0b59130ee
macron
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-06-29 13:06:59 +02:00
a4b67df515
:)
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-06-21 20:42:35 +02:00
83a010e8f5
fix text shadow
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-06-17 16:24:45 +02:00
b5683be191
make multiple fields public
All checks were successful
ci/woodpecker/manual/publish Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2024-06-17 15:56:57 +02:00
9010c7f975
fix nfp
Some checks failed
ci/woodpecker/push/publish Pipeline failed
2024-06-17 15:20:30 +02:00
a43f5813c0
use color of the nvp
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-06-17 15:15:46 +02:00
8bfa012af9
malice + nvp 2024-06-17 15:08:08 +02:00
975d6de8e6
useless vec
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-06-16 15:32:19 +02:00
c224816807
stagiaire
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-06-05 20:31:12 +02:00
1e19809df4
fix css
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-06-03 00:20:32 +02:00
aa1ba564dc
add missing mail obfuscater injection
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-06-02 20:02:23 +02:00
b9194dd0fa
use new rss endpoint: blog.rss
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-06-02 19:35:44 +02:00
de60dd23e6
trim
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-06-02 19:16:56 +02:00
c9405d0fdb
useless call
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-06-02 19:02:26 +02:00
0c6c88d181
fix minifier
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-06-02 18:58:36 +02:00
a08490f669
fix issue
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-06-02 18:53:40 +02:00
d1d21bc68c
w3c compliance 2024-06-02 18:44:57 +02:00
bf9217ba84
order 2024-06-02 18:44:46 +02:00
bece8ef147
don't lose data when obfuscating mails
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-06-02 18:19:58 +02:00
4146f2ea45
minor tags update
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-06-02 17:16:09 +02:00
cce73c0e09
its official..
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-06-02 17:13:33 +02:00
df3f66a424
add shadow for ukr too
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-05-30 11:24:46 +02:00
0ffb7a06d6
use text-shadow instead of text-stroke
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-05-30 10:13:22 +02:00
afb1e72adf
also text stroke acab
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-05-30 09:58:58 +02:00
2efe4ce47f
fix css flag and add a bordure since black and white are not visible in dark/light theme
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-05-30 09:50:20 +02:00
988f8345aa
add ukraine and palestine
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-05-30 02:25:49 +02:00
8e626a9640
don't underline footnotes
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-05-29 23:30:09 +02:00
2787450c0e
fix space
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-05-29 22:56:35 +02:00
73a235e3e1
printing css
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-05-29 22:46:37 +02:00
beacc5e02d
minor optimisations (can it break?)
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-05-29 21:01:17 +02:00
ed404bacca
mail obfuscater: change link 2024-05-29 20:51:04 +02:00
024fa67682
some are already defaults
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-05-29 12:03:07 +02:00
aba8a501af
more clippy advices
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-05-28 20:58:41 +02:00
0d924de79b
clippy::pendatic
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-05-28 20:26:58 +02:00
d400ef3c5b
update dependencies 2024-05-28 19:44:08 +02:00
6ab37ad04c
use a11y hljs theme
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-05-22 01:05:18 +02:00
2e43c6df12
clippy
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-05-16 18:08:58 +02:00
0f8391660d
better footnote
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-05-16 18:05:00 +02:00
754db13565
humans.txt wording
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-05-16 17:10:31 +02:00
13c19c5f68
clickable h2 (#63)
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-05-16 16:54:35 +02:00
af4c113153
update katex from 0.16.6 to 0.16.10
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-05-16 16:32:14 +02:00
5e223b972c
use html (#64) 2024-05-16 16:32:02 +02:00
c31cfb1295
add TeX lang
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-05-11 15:11:48 +02:00
d352206e29
wip: mail obfuscation (#51)
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-05-03 12:49:50 +02:00
ab5ce11037
update dependencies
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-05-03 11:49:05 +02:00
c42beef801
better footnote area
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-04-15 17:13:53 +02:00
9c8b036c79
update dependencies 2024-04-15 17:05:53 +02:00
d0cc3e584f
update chrono (fixing RUSTSEC-2020-0159)
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-04-01 19:48:31 +02:00
9dfcc1101d
Basic cours support (#44)
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
feat: Basic support for new `/cours` endpoint (not ready for release yet), see commit description for more

- Basic /cours support
- Fix LaTeX support (see #47 / cours+blog)
  - Better detection of when there is LaTeX in document
  - Don't shuffle markdown and LaTeX processing (thanks to comrak)
  - Macros on release
- Local image support (cours+blog)
- PDF support
- Support of markdown files integration in other markdown files
- Very basic exclusion support in toc (need a lot of improvement!!)
- Update multiple dependencies (actix-web, ramhorns, comrak, reqwest, hljs)
- Reformat some code
- ToC in /cours support (very basic, works via building it in rust and processing it in js)
- Remove very old assets (font + jspdf)
- Hide navbar when printing the website
- New tag in index page
- Fix OCaml support for HLJS + add "pseudocode" derived from Julia

Reviewed-on: #44
Co-authored-by: Mylloon <kennel.anri@tutanota.com>
Co-committed-by: Mylloon <kennel.anri@tutanota.com>
2024-04-01 18:11:46 +02:00
51ed97273c
Last element in odd list take all space available
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-03-19 13:01:34 +01:00
943603a330
fix breakpoint 2024-03-19 13:01:03 +01:00
04db320065
EVEN FUNNIER
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-03-06 13:07:39 +01:00
bf35016f45
CHENIL
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-03-05 13:24:07 +01:00
b1d651026b
sry to myself 2024-03-05 13:23:59 +01:00
8ff80d8b34
add pasteque
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2024-03-05 13:20:23 +01:00
116bc311c8
rounded square pfp #55
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-03-03 20:55:00 +01:00
6f8103ac97
update dependencies
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-02-27 16:21:33 +01:00
8e0a1b0bb5
hide contact page for navbar (#57) 2024-02-27 16:17:55 +01:00
619bab58ea
add pic of me
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-02-06 01:25:22 +01:00
3ed9178def
cat
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-01-30 09:07:15 +01:00
9c8718afac
fix scroll and click on chromium based browser
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-01-26 17:17:08 +01:00
058eb3c8c8
mouseup instead of mousedown
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-01-26 17:02:01 +01:00
c380da6380
Support list in portfolio cards
Some checks failed
ci/woodpecker/push/publish Pipeline failed
2024-01-26 16:58:16 +01:00
048ed0c7e1
2024
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2024-01-25 18:38:35 +01:00
f4e01449cc
update cached
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-01-25 18:30:46 +01:00
62eacf879e
update documentation
Some checks failed
ci/woodpecker/push/publish Pipeline failed
2024-01-25 18:28:25 +01:00
d51afcdcf6
contact genericity 2024-01-25 18:28:21 +01:00
9558202a46
blog genericity 2024-01-25 18:23:12 +01:00
c787171d6b
portfolio genericity 2024-01-25 18:23:07 +01:00
bc14e1b393
clean svg
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2024-01-25 17:46:09 +01:00
889573b4e7
adapt webmanifest (#36) 2024-01-25 17:46:00 +01:00
c18c0adf34
add metadata for index file
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-01-24 13:09:20 +01:00
17850ae636
alphabetical order 2024-01-24 12:40:14 +01:00
978cc48a57
index is now a md file, fallback to README, fallback to info about how to setup the index page
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2024-01-24 12:38:37 +01:00
fbde0a07c6
documentation 2024-01-24 12:10:50 +01:00
cb67c85040
add generic metada type for md files + comments 2024-01-24 12:06:59 +01:00
1467740f60
clippy 2024-01-24 12:05:41 +01:00
5ce39b0453
variable for data directory path 2024-01-24 11:52:20 +01:00
a4af983418
typo
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-01-21 00:10:23 +01:00
061d84a5a5
add jas to friends
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2024-01-21 00:09:45 +01:00
db4019ab67
preserve templates
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-01-19 17:04:20 +01:00
b49319d56c
use new woodpecker syntax 2024-01-19 17:00:24 +01:00
86f7d23fa5
update dependencies 2024-01-19 16:57:10 +01:00
07bea92346
prevent the scroll of the middle click
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2024-01-19 12:21:54 +01:00
cedb2e0e4c
fix the propagation of links
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2024-01-19 12:20:18 +01:00
465cc71e9e
background tab also when the user use ctrl or meta 2024-01-19 12:15:44 +01:00
d485946d56
fix links in cards
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2024-01-19 12:10:38 +01:00
05c86564d0
fix: mobile padding (#48)
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2023-12-25 19:50:51 +01:00
df5d47322c
fix: trim descriptions
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2023-12-23 17:39:14 +01:00
bfd8467d14
fix metadata parsing with strange whitespace
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
2023-12-22 15:57:30 +01:00
d6740851ec
pseudocode highlighting
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending
2023-11-16 21:24:32 +01:00
75 changed files with 3541 additions and 1174 deletions

View file

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

View file

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

1927
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -10,9 +10,11 @@
- [Global configuration](#global-configuration) - [Global configuration](#global-configuration)
- [Link shortener for contacts](#link-shortener-for-contacts) - [Link shortener for contacts](#link-shortener-for-contacts)
- [Add content](#add-content) - [Add content](#add-content)
- [Index](#index)
- [Blog](#blog) - [Blog](#blog)
- [Projects](#projects) - [Projects](#projects)
- [Contacts](#contacts) - [Contacts](#contacts)
- [Courses](#courses)
# Installation # Installation
@ -104,6 +106,7 @@ onion = "http://youraddress.onion/"
app_name = "Nickname" # fallback to 'EWP' if none app_name = "Nickname" # fallback to 'EWP' if none
name = "Firstname" name = "Firstname"
fullname = "Fullname" fullname = "Fullname"
exclude_courses = []
``` ```
## Link shortener for contacts ## Link shortener for contacts
@ -138,9 +141,29 @@ option: value
Markdown file Markdown file
``` ```
## Index
Markdown file is stored in `/app/data/index.md`
```
---
name: Option<String>
pronouns: Option<String>
avatar: Option<String>
avatar_caption: Option<String>
avatar_style: Option<String>
---
Index content
```
- If no `name`, the `fullname` used in the configuration will be used
- `avatar` is the link of the avatar
- `avatar_style` is either `round` (default) or `square`
## Blog ## Blog
Markdown files are stored in `/app/data/blog/` Markdown files are stored in `/app/data/blog/posts/`
``` ```
--- ---
@ -157,11 +180,15 @@ Post content
- If no `title`, the filename will be used - If no `title`, the filename will be used
- `date` format is `day-month-year` - `date` format is `day-month-year`
- `publish` is default to false. When false, posts are hidden from index - `publish` is default to false. When false, posts are hidden from index
but accessible, see #30 but accessible, see [#30](https://git.mylloon.fr/Anri/mylloon.fr/issues/30)
### About <!-- omit in toc -->
The file is stored at `/app/data/blog/about.md`.
## Projects ## Projects
Markdown files are stored in `/app/data/projects/` Markdown files are stored in `/app/data/projects/apps/`
``` ```
--- ---
@ -176,7 +203,14 @@ Project description
- If no `link` : the div won't be clickable and will be reported as is to the user - If no `link` : the div won't be clickable and will be reported as is to the user
(no corner-arrow) (no corner-arrow)
- Note that only a handful of [`language`s are supported](./static/css/languages.css). - Note that only a handful of [`language`s are supported](./static/css/languages.css)
You can also put apps in an "Archived" category, in this case, store markdown
files in `archive` subdirectory of `apps`.
### About <!-- omit in toc -->
The file is stored at `/app/data/projects/about.md`.
## Contacts ## Contacts
@ -204,3 +238,19 @@ Custom project description
- `user` is the username used in the platform - `user` is the username used in the platform
- `description` will be rendered as HTML "title" (text will appear when cursor - `description` will be rendered as HTML "title" (text will appear when cursor
is hover the link) is hover the link)
Also, contacts are categorized, here is the list of the available categories:
- `socials`
- `forges`
- `others`
For example, `socials` contact files are stored in `/app/data/contacts/socials/`.
### About <!-- omit in toc -->
The file is stored at `/app/data/contacts/about.md`.
## Courses
Markdown files are stored in `/app/data/cours/`

View file

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

View file

@ -3,7 +3,7 @@
Easy WebPage generator Easy WebPage generator
[![dependency status](https://deps.rs/repo/gitea/git.mylloon.fr/Anri/mylloon.fr/status.svg)](https://deps.rs/repo/gitea/git.mylloon.fr/Anri/mylloon.fr) [![dependency status](https://deps.rs/repo/gitea/git.mylloon.fr/Anri/mylloon.fr/status.svg)](https://deps.rs/repo/gitea/git.mylloon.fr/Anri/mylloon.fr)
[![status-badge](https://ci.mylloon.fr/api/badges/Anri/mylloon.fr/status.svg)](https://ci.mylloon.fr/Anri/mylloon.fr) [![status-badge](https://git.mylloon.fr/Anri/mylloon.fr/badges/workflows/publish.yml/badge.svg)](https://git.mylloon.fr/Anri/mylloon.fr/actions?workflow=publish.yml)
- See [issues](https://git.mylloon.fr/Anri/mylloon.fr/issues) - See [issues](https://git.mylloon.fr/Anri/mylloon.fr/issues)
- See [documentation](./Documentation.md) - See [documentation](https://git.mylloon.fr/Anri/mylloon.fr/src/branch/main/Documentation.md)

View file

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

View file

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

View file

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

View file

@ -1,12 +1,20 @@
use crate::misc::date::Date; use crate::misc::date::Date;
use base64::engine::general_purpose;
use base64::Engine;
use comrak::nodes::{AstNode, NodeValue}; use comrak::nodes::{AstNode, NodeValue};
use comrak::{format_html, parse_document, Arena, ComrakOptions, ListStyleType}; use comrak::{format_html, parse_document, Arena, ComrakOptions, ListStyleType, Options};
use lol_html::{element, rewrite_str, RewriteStrSettings}; use lol_html::html_content::ContentType;
use lol_html::{element, rewrite_str, HtmlRewriter, RewriteStrSettings, Settings};
use ramhorns::Content; use ramhorns::Content;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use std::fmt::Debug;
use std::fs; use std::fs;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
#[derive(Default, Deserialize, Content, Debug)] /// Metadata for blog posts
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadataBlog { pub struct FileMetadataBlog {
pub title: Option<String>, pub title: Option<String>,
pub date: Option<Date>, pub date: Option<Date>,
@ -16,37 +24,7 @@ pub struct FileMetadataBlog {
pub toc: Option<bool>, pub toc: Option<bool>,
} }
#[derive(Default, Deserialize, Content, Debug)] /// A tag, related to post blog
pub struct FileMetadataContact {
pub title: String,
pub custom: Option<bool>,
pub user: Option<String>,
pub link: Option<String>,
pub newtab: Option<bool>,
pub description: Option<String>,
}
#[derive(Default, Deserialize, Content, Debug)]
pub struct FileMetadataPortfolio {
pub title: Option<String>,
pub link: Option<String>,
pub description: Option<String>,
pub language: Option<String>,
}
pub enum TypeFileMetadata {
Blog,
Contact,
Portfolio,
}
#[derive(Default, Deserialize, Content, Debug)]
pub struct FileMetadata {
pub blog: Option<FileMetadataBlog>,
pub contact: Option<FileMetadataContact>,
pub portfolio: Option<FileMetadataPortfolio>,
}
#[derive(Content, Debug, Clone)] #[derive(Content, Debug, Clone)]
pub struct Tag { pub struct Tag {
pub name: String, pub name: String,
@ -58,7 +36,7 @@ impl<'de> Deserialize<'de> for Tag {
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
match <&str>::deserialize(deserializer) { match <&str>::deserialize(deserializer) {
Ok(s) => match serde_yaml::from_str(s) { Ok(s) => match serde_yml::from_str(s) {
Ok(tag) => Ok(Self { name: tag }), Ok(tag) => Ok(Self { name: tag }),
Err(e) => Err(serde::de::Error::custom(e)), Err(e) => Err(serde::de::Error::custom(e)),
}, },
@ -67,14 +45,76 @@ impl<'de> Deserialize<'de> for Tag {
} }
} }
/// Metadata for contact entry
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadataContact {
pub title: String,
pub custom: Option<bool>,
pub user: Option<String>,
pub link: Option<String>,
pub newtab: Option<bool>,
pub description: Option<String>,
}
/// Metadata for index page
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadataIndex {
pub name: Option<String>,
pub pronouns: Option<String>,
pub avatar: Option<String>,
pub avatar_caption: Option<String>,
pub avatar_style: Option<String>,
}
/// Metadata for portfolio cards
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadataPortfolio {
pub title: Option<String>,
pub link: Option<String>,
pub description: Option<String>,
pub language: Option<String>,
}
/// List of available metadata types
pub enum TypeFileMetadata {
Blog,
Contact,
Generic,
Index,
Portfolio,
}
/// Structure who holds all the metadata the file have
/// Usually all fields are None except one
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadata {
pub blog: Option<FileMetadataBlog>,
pub contact: Option<FileMetadataContact>,
pub index: Option<FileMetadataIndex>,
pub portfolio: Option<FileMetadataPortfolio>,
}
#[allow(clippy::struct_excessive_bools)]
/// Global metadata
#[derive(Content, Debug)] #[derive(Content, Debug)]
pub struct Metadata { pub struct Metadata {
pub info: FileMetadata, pub info: FileMetadata,
pub math: bool, pub math: bool,
pub mermaid: bool, pub mermaid: bool,
pub syntax_highlight: bool, pub syntax_highlight: bool,
pub mail_obfsucated: bool,
} }
impl Metadata {
/// Update current metadata boolean fields, keeping true ones
fn merge(&mut self, other: &Self) {
self.math = self.math || other.math;
self.mermaid = self.mermaid || other.mermaid;
self.syntax_highlight = self.syntax_highlight || other.syntax_highlight;
}
}
/// File description
#[derive(Content, Debug)] #[derive(Content, Debug)]
pub struct File { pub struct File {
pub metadata: Metadata, pub metadata: Metadata,
@ -82,7 +122,7 @@ pub struct File {
} }
/// Options used for parser and compiler MD --> HTML /// Options used for parser and compiler MD --> HTML
pub fn get_options() -> ComrakOptions { pub fn get_options<'a>() -> ComrakOptions<'a> {
let mut options = comrak::Options::default(); let mut options = comrak::Options::default();
// Extension // Extension
@ -96,12 +136,21 @@ pub fn get_options() -> ComrakOptions {
options.extension.footnotes = true; options.extension.footnotes = true;
options.extension.description_lists = true; options.extension.description_lists = true;
options.extension.front_matter_delimiter = Some("---".into()); options.extension.front_matter_delimiter = Some("---".into());
options.extension.multiline_block_quotes = true;
options.extension.math_dollars = true;
options.extension.math_code = false;
options.extension.wikilinks_title_after_pipe = false;
options.extension.wikilinks_title_before_pipe = false;
options.extension.underline = true;
options.extension.spoiler = false;
options.extension.greentext = false;
// Parser // Parser
options.parse.smart = true; // could be boring options.parse.smart = true; // could be boring
options.parse.default_info_string = Some("plaintext".into()); options.parse.default_info_string = Some("plaintext".into());
options.parse.relaxed_tasklist_matching = true; options.parse.relaxed_tasklist_matching = true;
options.parse.relaxed_autolinks = true; options.parse.relaxed_autolinks = true;
// options.render.broken_link_callback = ...;
// Renderer // Renderer
options.render.hardbreaks = false; // could be true? change by metadata could be good for compatibility options.render.hardbreaks = false; // could be true? change by metadata could be good for compatibility
@ -112,18 +161,25 @@ pub fn get_options() -> ComrakOptions {
options.render.escape = false; options.render.escape = false;
options.render.list_style = ListStyleType::Dash; options.render.list_style = ListStyleType::Dash;
options.render.sourcepos = false; options.render.sourcepos = false;
options.render.experimental_inline_sourcepos = false;
options.render.escaped_char_spans = false;
options.render.ignore_setext = true;
options.render.ignore_empty_links = true;
options.render.gfm_quirks = true;
options.render.prefer_fenced = false;
options.render.figure_with_caption = false;
options options
} }
/// Resize images if needed /// Resize images if needed
fn custom_img_size(html: String) -> String { fn custom_img_size(html: &str) -> String {
rewrite_str( rewrite_str(
&html, html,
RewriteStrSettings { RewriteStrSettings {
element_content_handlers: vec![element!("img[alt]", |el| { element_content_handlers: vec![element!("img[alt]", |el| {
let alt = el.get_attribute("alt").unwrap(); let alt = el.get_attribute("alt").unwrap();
let possible_piece = alt.split(|c| c == '|').collect::<Vec<&str>>(); let possible_piece = alt.split('|').collect::<Vec<&str>>();
if possible_piece.len() > 1 { if possible_piece.len() > 1 {
let data = possible_piece.last().unwrap().trim(); let data = possible_piece.last().unwrap().trim();
@ -138,7 +194,7 @@ fn custom_img_size(html: String) -> String {
el.set_attribute("width", dimension.0).unwrap(); el.set_attribute("width", dimension.0).unwrap();
el.set_attribute("height", dimension.1).unwrap(); el.set_attribute("height", dimension.1).unwrap();
if new_alt.is_empty() { if new_alt.is_empty() {
el.remove_attribute("alt") el.remove_attribute("alt");
} else { } else {
el.set_attribute("alt", new_alt).unwrap(); el.set_attribute("alt", new_alt).unwrap();
} }
@ -148,7 +204,7 @@ fn custom_img_size(html: String) -> String {
if data.parse::<i32>().is_ok() { if data.parse::<i32>().is_ok() {
el.set_attribute("width", data).unwrap(); el.set_attribute("width", data).unwrap();
if new_alt.is_empty() { if new_alt.is_empty() {
el.remove_attribute("alt") el.remove_attribute("alt");
} else { } else {
el.set_attribute("alt", new_alt).unwrap(); el.set_attribute("alt", new_alt).unwrap();
} }
@ -164,12 +220,69 @@ fn custom_img_size(html: String) -> String {
.unwrap() .unwrap()
} }
/// Fix local images to base64 and integration of markdown files
fn fix_images_and_integration(path: &str, html: &str) -> (String, Metadata) {
let mut metadata = Metadata {
info: FileMetadata::default(),
math: false,
mermaid: false,
syntax_highlight: false,
mail_obfsucated: false,
};
(
rewrite_str(
html,
RewriteStrSettings {
element_content_handlers: vec![element!("img", |el| {
if let Some(src) = el.get_attribute("src") {
let img_src = Path::new(path).parent().unwrap();
let img_path = urlencoding::decode(img_src.join(src).to_str().unwrap())
.unwrap()
.to_string();
if let Ok(file) = fs::read_to_string(&img_path) {
let mime = mime_guess::from_path(&img_path).first_or_octet_stream();
if mime == "text/markdown" {
let mut options = get_options();
options.extension.footnotes = false;
let data = read_md(
&img_path,
&file,
&TypeFileMetadata::Generic,
Some(options),
);
el.replace(&data.content, ContentType::Html);
metadata.merge(&data.metadata);
} else {
let image = general_purpose::STANDARD.encode(file);
el.set_attribute("src", &format!("data:{mime};base64,{image}"))
.unwrap();
}
}
}
Ok(())
})],
..RewriteStrSettings::default()
},
)
.unwrap(),
metadata,
)
}
/// Transform markdown string to File structure /// Transform markdown string to File structure
fn read(raw_text: &str, metadata_type: TypeFileMetadata) -> File { pub fn read_md(
path: &str,
raw_text: &str,
metadata_type: &TypeFileMetadata,
options: Option<Options>,
) -> File {
let arena = Arena::new(); let arena = Arena::new();
let options = get_options(); let opt = options.map_or_else(get_options, |specific_opt| specific_opt);
let root = parse_document(&arena, raw_text, &options); let root = parse_document(&arena, raw_text, &opt);
// Find metadata // Find metadata
let metadata = get_metadata(root, metadata_type); let metadata = get_metadata(root, metadata_type);
@ -179,48 +292,62 @@ fn read(raw_text: &str, metadata_type: TypeFileMetadata) -> File {
// Convert to HTML // Convert to HTML
let mut html = vec![]; let mut html = vec![];
format_html(root, &options, &mut html).unwrap(); format_html(root, &opt, &mut html).unwrap();
let mut html_content = String::from_utf8(html).unwrap(); let mut html_content = String::from_utf8(html).unwrap();
html_content = custom_img_size(html_content); let children_metadata;
let mail_obfsucated;
(html_content, children_metadata) = fix_images_and_integration(path, &html_content);
html_content = custom_img_size(&html_content);
(html_content, mail_obfsucated) = mail_obfuscation(&html_content);
let mut final_metadata = Metadata {
info: metadata,
mermaid: check_mermaid(root, mermaid_name),
syntax_highlight: check_code(root, &[mermaid_name.into()]),
math: check_math(&html_content),
mail_obfsucated,
};
final_metadata.merge(&children_metadata);
File { File {
metadata: Metadata { metadata: final_metadata,
info: metadata,
mermaid: check_mermaid(root, mermaid_name),
syntax_highlight: check_code(root, &[mermaid_name.into()]),
math: check_math(&html_content),
},
content: html_content, content: html_content,
} }
} }
/// Read markdown file
pub fn read_file(filename: &str, expected_file: TypeFileMetadata) -> Option<File> {
match fs::read_to_string(filename) {
Ok(text) => Some(read(&text, expected_file)),
_ => None,
}
}
/// Deserialize metadata based on a type /// Deserialize metadata based on a type
fn deserialize_metadata<T: Default + serde::de::DeserializeOwned>(text: &str) -> T { fn deserialize_metadata<T: Default + serde::de::DeserializeOwned>(text: &str) -> T {
serde_yaml::from_str(text.trim_matches(&['-', '\n'] as &[_])).unwrap_or_default() serde_yml::from_str(text.trim().trim_matches(&['-'] as &[_])).unwrap_or_default()
} }
/// Fetch metadata from AST /// Fetch metadata from AST
pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: TypeFileMetadata) -> FileMetadata { pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: &TypeFileMetadata) -> FileMetadata {
match root root.children()
.children()
.find_map(|node| match &node.data.borrow().value { .find_map(|node| match &node.data.borrow().value {
// Extract metadata from frontmatter
NodeValue::FrontMatter(text) => Some(match mtype { NodeValue::FrontMatter(text) => Some(match mtype {
TypeFileMetadata::Blog => FileMetadata { TypeFileMetadata::Blog => FileMetadata {
blog: Some(deserialize_metadata(text)), blog: Some(deserialize_metadata(text)),
..FileMetadata::default() ..FileMetadata::default()
}, },
TypeFileMetadata::Contact => FileMetadata { TypeFileMetadata::Contact => {
contact: Some(deserialize_metadata(text)), let mut metadata: FileMetadataContact = deserialize_metadata(text);
// Trim descriptions
if let Some(desc) = &mut metadata.description {
desc.clone_from(&desc.trim().into());
}
FileMetadata {
contact: Some(metadata),
..FileMetadata::default()
}
}
TypeFileMetadata::Generic => FileMetadata::default(),
TypeFileMetadata::Index => FileMetadata {
index: Some(deserialize_metadata(text)),
..FileMetadata::default() ..FileMetadata::default()
}, },
TypeFileMetadata::Portfolio => FileMetadata { TypeFileMetadata::Portfolio => FileMetadata {
@ -229,23 +356,29 @@ pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: TypeFileMetadata) -> FileM
}, },
}), }),
_ => None, _ => None,
}) { })
Some(data) => data, .map_or_else(
None => match mtype { || match mtype {
TypeFileMetadata::Blog => FileMetadata { TypeFileMetadata::Blog => FileMetadata {
blog: Some(FileMetadataBlog::default()), blog: Some(FileMetadataBlog::default()),
..FileMetadata::default() ..FileMetadata::default()
},
TypeFileMetadata::Contact => FileMetadata {
contact: Some(FileMetadataContact::default()),
..FileMetadata::default()
},
TypeFileMetadata::Generic => FileMetadata::default(),
TypeFileMetadata::Index => FileMetadata {
index: Some(FileMetadataIndex::default()),
..FileMetadata::default()
},
TypeFileMetadata::Portfolio => FileMetadata {
portfolio: Some(FileMetadataPortfolio::default()),
..FileMetadata::default()
},
}, },
TypeFileMetadata::Contact => FileMetadata { |data| data,
contact: Some(FileMetadataContact::default()), )
..FileMetadata::default()
},
TypeFileMetadata::Portfolio => FileMetadata {
portfolio: Some(FileMetadataPortfolio::default()),
..FileMetadata::default()
},
},
}
} }
/// Check whether mermaid diagrams are in the AST /// Check whether mermaid diagrams are in the AST
@ -271,9 +404,24 @@ fn check_code<'a>(root: &'a AstNode<'a>, blacklist: &[String]) -> bool {
}) })
} }
/// Check if html can contains maths /// Check if html contains maths
fn check_math(html: &str) -> bool { fn check_math(html: &str) -> bool {
html.contains('$') let math_detected = Arc::new(AtomicBool::new(false));
let _ = HtmlRewriter::new(
Settings {
element_content_handlers: vec![element!("span[data-math-style]", |_| {
math_detected.store(true, Ordering::SeqCst);
Ok(())
})],
..Settings::default()
},
|_: &[u8]| {},
)
.write(html.as_bytes());
math_detected.load(Ordering::SeqCst)
} }
/// Change class of languages for hljs detection /// Change class of languages for hljs detection
@ -286,3 +434,69 @@ fn hljs_replace<'a>(root: &'a AstNode<'a>, mermaid_str: &str) {
} }
}); });
} }
/// Obfuscate email if email found
fn mail_obfuscation(html: &str) -> (String, bool) {
let modified = Arc::new(AtomicBool::new(false));
let data_attr = "title";
// Modify HTML for mails
let new_html = rewrite_str(
html,
RewriteStrSettings {
element_content_handlers: vec![element!("a[href^='mailto:']", |el| {
modified.store(true, Ordering::SeqCst);
// Get mail address
let link = el.get_attribute("href").unwrap();
let (uri, mail) = &link.split_at(7);
let (before, after) = mail.split_once('@').unwrap();
// Preserve old data and add obfuscated mail address
el.prepend(&format!("<span {data_attr}='"), ContentType::Html);
let modified_mail = format!("'></span>{before}<span class='at'>(at)</span>{after}");
el.append(&modified_mail, ContentType::Html);
// Change href
Ok(el.set_attribute("href", &format!("{uri}{before} at {after}"))?)
})],
..RewriteStrSettings::default()
},
)
.unwrap();
let is_modified = modified.load(Ordering::SeqCst);
if is_modified {
// Remove old data email if exists
(
rewrite_str(
&new_html,
RewriteStrSettings {
element_content_handlers: vec![element!(
&format!("a[href^='mailto:'] > span[{data_attr}]"),
|el| {
Ok(el.set_attribute(
data_attr,
// Remove mails
el.get_attribute(data_attr)
.unwrap()
.split_whitespace()
.filter(|word| !word.contains('@'))
.collect::<Vec<&str>>()
.join(" ")
.trim(),
)?)
}
)],
..RewriteStrSettings::default()
},
)
.unwrap(),
is_modified,
)
} else {
(new_html, is_modified)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,145 @@
use actix_web::{get, Responder}; use std::path::Path;
use actix_web::{get, web, Responder};
use cached::proc_macro::cached;
use ramhorns::Content;
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::{
config::Config,
misc::{
markdown::{File, TypeFileMetadata},
utils::{make_kw, read_file, Html},
},
template::{InfosPage, NavBar},
};
#[derive(Debug, Deserialize)]
pub struct PathRequest {
q: Option<String>,
}
#[get("/cours")] #[get("/cours")]
async fn page() -> impl Responder { pub async fn page(info: web::Query<PathRequest>, config: web::Data<Config>) -> impl Responder {
// Page de notes de cours Html(build_page(&info, config.get_ref().to_owned()))
// Cf. https://univ.mylloon.fr/ }
// Cf. https://github.com/xy2z/PineDocs
actix_web::web::Redirect::to("/") #[derive(Content, Debug)]
struct CoursTemplate {
navbar: NavBar,
filetree: String,
content: Option<File>,
}
#[derive(Clone, Debug, Serialize)]
struct FileNode {
name: String,
is_dir: bool,
children: Vec<FileNode>,
}
#[cached]
fn compile_patterns(exclusion_list: Vec<String>) -> Vec<Regex> {
exclusion_list
.iter()
.map(|pattern| Regex::new(pattern).unwrap())
.collect()
}
fn get_filetree(dir_path: &str, exclusion_patterns: &Vec<Regex>) -> FileNode {
let children = std::fs::read_dir(dir_path)
.unwrap()
.filter_map(Result::ok)
.filter_map(|entry| {
let entry_path = entry.path();
let entry_name = entry_path.file_name()?.to_string_lossy().to_string();
// Exclude element with the exclusion_list
if exclusion_patterns.iter().any(|re| re.is_match(&entry_name)) {
return None;
}
if entry_path.is_file() {
Some(FileNode {
name: entry_name,
is_dir: false,
children: vec![],
})
} else {
// Exclude empty directories
let children_of_children =
get_filetree(entry_path.to_str().unwrap(), exclusion_patterns);
if children_of_children.is_dir && children_of_children.children.is_empty() {
None
} else {
Some(children_of_children)
}
}
})
.collect();
FileNode {
name: Path::new(dir_path)
.file_name()
.unwrap()
.to_string_lossy()
.to_string(),
is_dir: true,
children,
}
}
/// Get a page content
fn get_content(
cours_dir: &str,
path: &web::Query<PathRequest>,
exclusion_list: &[String],
) -> Option<File> {
let filename = path.q.as_ref().map_or("index.md", |q| q);
// We should support regex?
if exclusion_list
.iter()
.any(|excluded_term| filename.contains(excluded_term.as_str()))
{
return None;
}
read_file(
&format!("{cours_dir}/{filename}"),
&TypeFileMetadata::Generic,
)
}
fn build_page(info: &web::Query<PathRequest>, config: Config) -> String {
let cours_dir = "data/cours";
let exclusion_list = config.fc.exclude_courses.unwrap();
let exclusion_patterns = compile_patterns(exclusion_list.clone());
let filetree = get_filetree(cours_dir, &exclusion_patterns);
config.tmpl.render(
"cours.html",
CoursTemplate {
navbar: NavBar {
cours: true,
..NavBar::default()
},
filetree: serde_json::to_string(&filetree).unwrap(),
content: get_content(cours_dir, info, &exclusion_list),
},
InfosPage {
title: Some("Cours".into()),
desc: Some("Cours à l'univ".into()),
kw: Some(make_kw(&[
"cours",
"études",
"université",
"licence",
"master",
"notes",
"digital garden",
])),
},
)
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

Binary file not shown.

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

Binary file not shown.

View file

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

View file

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

View file

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

View file

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

53
static/css/cours.css Normal file
View file

@ -0,0 +1,53 @@
/* Filetree */
aside {
float: left;
margin-left: 20px;
position: sticky;
top: 0;
}
aside ul {
list-style: none;
padding-left: 0.6em;
}
aside li {
position: relative;
}
/* Element */
aside li:before {
content: "";
position: absolute;
top: -0.2em;
left: -1em;
height: 1em;
}
aside li.collapsed > ul {
display: none;
}
aside li.directory::before {
content: "+";
}
aside li:not(.collapsed).directory::before {
content: "-";
}
aside li.directory {
cursor: pointer;
}
@media print {
aside {
visibility: hidden;
}
}
main img {
max-width: 100%;
display: block;
margin: auto;
}

Binary file not shown.

Binary file not shown.

View file

@ -1,14 +0,0 @@
Ces fontes sont distribuées gratuitement sous Licence publique Creative Commons Attribution 4.0 International :
https://creativecommons.org/licenses/by/4.0/legalcode.fr
These fonts are freely available under Creative Commons Attribution 4.0 International Public License:
https://creativecommons.org/licenses/by/4.0/legalcode
Luciole © Laurent Bourcellier & Jonathan Perez

View file

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

View file

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

View file

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

View file

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

View file

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

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

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

167
static/js/cours.js Normal file
View file

@ -0,0 +1,167 @@
/**
* Build the filetree
* @param {HTMLElement} parent Root element of the filetree
* @param {{name: string, is_dir: boolean, children: any[]}} data FileNode
* @param {string} location Current location, used for links creation
*/
const buildFileTree = (parent, data, location) => {
const ul = document.createElement("ul");
data.forEach((item) => {
const li = document.createElement("li");
li.classList.add(item.is_dir ? "directory" : "file");
if (item.is_dir) {
// Directory
li.textContent = item.name;
li.classList.add("collapsed");
// Toggle collapsing on click
li.addEventListener("click", function (e) {
if (e.target === li) {
li.classList.toggle("collapsed");
}
});
} else {
// File
const url = window.location.href.split("?")[0];
const a = document.createElement("a");
a.text = item.name;
a.href = `${url}?q=${location}${item.name}`;
li.appendChild(a);
}
ul.appendChild(li);
if (item.children && item.children.length > 0) {
buildFileTree(
li,
item.children,
item.is_dir ? location + `${item.name}/` : location
);
}
});
parent.appendChild(ul);
};
/**
* Uncollapse elements from the deepest element
* @param {HTMLLIElement} element Element to uncollapse
*/
const uncollapse = (element) => {
if (element) {
element.classList.remove("collapsed");
uncollapse(element.parentElement.closest("li"));
}
};
/**
* Find the deepest opened directory
* @param {string[]} path Current path we are looking at, init with fullpath
* @param {NodeListOf<ChildNode>} options Options we have, init with list root
* @returns
*/
const deepestNodeOpened = (path, options) => {
// Iterate over possible options
for (let i = 0; i < options.length; ++i) {
// If the directory and the current path match
if (decodeURI(path[0]) === options[i].firstChild.nodeValue) {
if (path.length === 1) {
// We found it
return options[i];
}
// Continue the search
return deepestNodeOpened(
path.slice(1),
options[i].querySelector("ul").childNodes
);
}
}
};
const svgDarkTheme = () => {
for (const item of document.getElementsByTagName("img")) {
if (!item.src.startsWith("data:image/svg+xml;base64,")) {
// Exclude image who aren't SVG and base64 encoded
break;
}
/** Convert to grayscale */
const colorToGrayscale = (color) => {
return 0.3 * color.r + 0.59 * color.g + 0.11 * color.b;
};
/** Extract color using canvas2d */
const extractColors = (image) => {
const canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0);
const imageData = ctx.getImageData(
0,
0,
Math.max(1, canvas.width),
Math.max(1, canvas.height)
);
const pixelData = imageData.data;
const colors = [];
for (let i = 0; i < pixelData.length; i += 4) {
if (pixelData[i + 3] > 0) {
colors.push({
r: pixelData[i],
g: pixelData[i + 1],
b: pixelData[i + 2],
});
}
}
return colors;
};
// Extract colors
const colors = extractColors(item);
// Calculate the average grayscale value
const grayscaleValues = colors.map(colorToGrayscale);
const totalGrayscale = grayscaleValues.reduce((acc, val) => acc + val, 0);
const averageGrayscale = totalGrayscale / grayscaleValues.length;
if (averageGrayscale < 128) {
item.style = "filter: invert(1);";
}
}
};
window.addEventListener("load", () => {
// Build the filetree
const fileTreeElement = document.getElementsByTagName("aside")[0];
const dataElement = fileTreeElement.getElementsByTagName("span")[0];
buildFileTree(
fileTreeElement,
JSON.parse(dataElement.getAttribute("data-json")).children,
""
);
dataElement.remove();
// Open nested openeded directories
const infoURL = window.location.href.split("?");
if (infoURL.length > 1) {
const fullpath = infoURL[1].substring(2);
const path = fullpath.substring(0, fullpath.lastIndexOf("/"));
const last_openeded = deepestNodeOpened(
path.split("/"),
fileTreeElement.querySelector("ul").childNodes
);
uncollapse(last_openeded);
}
// Fix SVG images in dark mode
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
svgDarkTheme();
}
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,17 +10,19 @@ window.addEventListener("load", () => {
la: "leftarrow", la: "leftarrow",
RA: "Rightarrow", RA: "Rightarrow",
LA: "Leftarrow", LA: "Leftarrow",
u: "mu",
}) })
)[Symbol.iterator]()) { )[Symbol.iterator]()) {
macros[`\\${item[0]}`] = `\\${item[1]}`; const bs = "\\";
macros[`${bs}${item[0]}`] = `${bs}${item[1]}`;
} }
renderMathInElement(document.body, { const attribute = "data-math-style";
delimiters: [ for (const element of document.querySelectorAll(`span[${attribute}]`)) {
{ left: "$$", right: "$$", display: true }, katex.render(element.textContent, element, {
{ left: "$", right: "$", display: false }, throwOnError: false,
], displayMode: element.getAttribute(attribute) === "display",
throwOnError: false, macros: macros,
macros, });
}); }
}); });

View file

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

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

Binary file not shown.

21
templates/app.webmanifest Normal file
View file

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

View file

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

View file

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

View file

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

31
templates/cours.html Normal file
View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="fr">
<head dir="ltr">
{{>head.html}}
<link rel="stylesheet" href="/css/cours.css" />
{{#data}} {{#content}} {{#metadata}}
{{#math}}{{>libs/katex_head.html}}{{/math}}
{{#syntax_highlight}}{{>libs/hljs_head.html}}{{/syntax_highlight}}
{{/metadata}} {{/content}}
</head>
<body>
<header>{{>navbar.html}}</header>
<aside>
<span data-json="{{filetree}} "></span>
</aside>
<main>
{{^content}}
<p>Fichier introuvable</p>
{{/content}} {{#content}}
<article>{{&content}}</article>
</main>
{{#metadata}} {{#mermaid}}{{>libs/mermaid_footer.html}}{{/mermaid}}
{{#math}}{{>libs/katex_footer.html}}{{/math}}
{{#syntax_highlight}}{{>libs/hljs_footer.html}}{{/syntax_highlight}}
{{#mail_obfsucated}}{{>libs/mail_obfuscater.html}}{{/mail_obfsucated}}
{{/metadata}} {{/content}} {{/data}}
<script src="/js/cours.js"></script>
</body>
</html>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@
>Portfolio</a >Portfolio</a
></p> ></p>
</li><li> </li><!-- <li>
<p><a <p><a
class="_ {{#contact}}bold{{/contact}}" class="_ {{#contact}}bold{{/contact}}"
@ -32,7 +32,7 @@
>Contact</a >Contact</a
></p> ></p>
</li><li> </li> --><li>
<p><a <p><a
class="_ {{#contrib}}bold{{/contrib}}" class="_ {{#contrib}}bold{{/contrib}}"

View file

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

View file

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

View file

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