Merge branch 'main' into cours
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending

This commit is contained in:
Mylloon 2024-01-24 13:17:30 +01:00
commit 349b822361
Signed by: Anri
GPG key ID: A82D63DFF8D1317F
16 changed files with 173 additions and 138 deletions

View file

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

View file

@ -6,4 +6,4 @@ Easy WebPage generator
[![status-badge](https://ci.mylloon.fr/api/badges/Anri/mylloon.fr/status.svg)](https://ci.mylloon.fr/Anri/mylloon.fr)
- See [issues](https://git.mylloon.fr/Anri/mylloon.fr/issues)
- See [documentation](./Documentation.md)
- See [documentation](https://git.mylloon.fr/Anri/mylloon.fr/src/branch/main/Documentation.md)

View file

@ -69,13 +69,20 @@ impl FileConfig {
}
}
// Paths where files are stored
#[derive(Clone, Debug)]
pub struct Locations {
pub static_dir: String,
pub data_dir: String,
}
/// Configuration used internally in the app
#[derive(Clone, Debug)]
pub struct Config {
/// Information given in the config file
pub fc: FileConfig,
/// Location where the static files are stored
pub static_location: String,
pub locations: Locations,
/// Informations about templates
pub tmpl: Template,
}
@ -110,7 +117,10 @@ pub fn get_config(file_path: &str) -> Config {
Config {
fc: internal_config.to_owned(),
static_location: format!("{}/{}", files_root, static_dir),
locations: Locations {
static_dir: format!("{}/{}", files_root, static_dir),
data_dir: String::from("data"),
},
tmpl: Template {
directory: format!("{}/{}", files_root, templates_dir),
app_name: internal_config.app_name.unwrap(),

View file

@ -60,7 +60,7 @@ async fn main() -> Result<()> {
.service(portfolio::page)
.service(setup::page)
.service(web3::page)
.service(Files::new("/", config.static_location.to_owned()))
.service(Files::new("/", config.locations.static_dir.to_owned()))
.default_service(web::to(not_found::page))
})
.bind(addr)?

View file

@ -6,10 +6,7 @@ use ramhorns::Content;
use serde::{Deserialize, Deserializer};
use std::fs;
/// Regular markdown files, no metadata
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileNoMetadata {}
/// Metadata for blog posts
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadataBlog {
pub title: Option<String>,
@ -20,39 +17,7 @@ pub struct FileMetadataBlog {
pub toc: Option<bool>,
}
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadataContact {
pub title: String,
pub custom: Option<bool>,
pub user: Option<String>,
pub link: Option<String>,
pub newtab: Option<bool>,
pub description: Option<String>,
}
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadataPortfolio {
pub title: Option<String>,
pub link: Option<String>,
pub description: Option<String>,
pub language: Option<String>,
}
pub enum TypeFileMetadata {
Blog,
Contact,
Portfolio,
Cours,
}
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadata {
pub blog: Option<FileMetadataBlog>,
pub contact: Option<FileMetadataContact>,
pub portfolio: Option<FileMetadataPortfolio>,
pub cours: Option<FileNoMetadata>,
}
/// A tag, related to post blog
#[derive(Content, Debug, Clone)]
pub struct Tag {
pub name: String,
@ -73,6 +38,55 @@ 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>,
}
/// 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>,
}
/// Global metadata
#[derive(Content, Debug)]
pub struct Metadata {
pub info: FileMetadata,
@ -81,6 +95,7 @@ pub struct Metadata {
pub syntax_highlight: bool,
}
/// File description
#[derive(Content, Debug)]
pub struct File {
pub metadata: Metadata,
@ -220,6 +235,7 @@ pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: TypeFileMetadata) -> FileM
match root
.children()
.find_map(|node| match &node.data.borrow().value {
// Extract metadata from frontmatter
NodeValue::FrontMatter(text) => Some(match mtype {
TypeFileMetadata::Blog => FileMetadata {
blog: Some(deserialize_metadata(text)),
@ -238,18 +254,22 @@ pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: TypeFileMetadata) -> FileM
..FileMetadata::default()
}
}
TypeFileMetadata::Portfolio => FileMetadata {
portfolio: Some(deserialize_metadata(text)),
TypeFileMetadata::Generic => FileMetadata {
..FileMetadata::default()
},
TypeFileMetadata::Cours => FileMetadata {
cours: Some(deserialize_metadata(text)),
TypeFileMetadata::Index => FileMetadata {
index: Some(deserialize_metadata(text)),
..FileMetadata::default()
},
TypeFileMetadata::Portfolio => FileMetadata {
portfolio: Some(deserialize_metadata(text)),
..FileMetadata::default()
},
}),
_ => None,
}) {
Some(data) => data,
// No metadata
None => match mtype {
TypeFileMetadata::Blog => FileMetadata {
blog: Some(FileMetadataBlog::default()),
@ -259,12 +279,15 @@ pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: TypeFileMetadata) -> FileM
contact: Some(FileMetadataContact::default()),
..FileMetadata::default()
},
TypeFileMetadata::Portfolio => FileMetadata {
portfolio: Some(FileMetadataPortfolio::default()),
TypeFileMetadata::Generic => FileMetadata {
..FileMetadata::default()
},
TypeFileMetadata::Cours => FileMetadata {
cours: Some(FileNoMetadata::default()),
TypeFileMetadata::Index => FileMetadata {
index: Some(FileMetadataIndex::default()),
..FileMetadata::default()
},
TypeFileMetadata::Portfolio => FileMetadata {
portfolio: Some(FileMetadataPortfolio::default()),
..FileMetadata::default()
},
},

View file

@ -1,6 +1,7 @@
use actix_web::{get, HttpResponse, Responder};
use serde::Serialize;
/// Response
#[derive(Serialize)]
struct Info {
unix_epoch: u32,

View file

@ -42,7 +42,7 @@ struct BlogIndexTemplate {
#[once(time = 60)]
fn build_index(config: Config) -> String {
let mut posts = get_posts("data/blog");
let mut posts = get_posts(format!("{}/blog", config.locations.data_dir));
// Sort from newest to oldest
posts.sort_by_cached_key(|p| (p.date.year, p.date.month, p.date.day));
@ -81,8 +81,8 @@ struct Post {
impl Post {
// Fetch the file content
fn fetch_content(&mut self) {
let blog_dir = "data/blog";
fn fetch_content(&mut self, data_dir: &str) {
let blog_dir = format!("{}/blog", data_dir);
let ext = ".md";
if let Some(file) = read_file(
@ -102,8 +102,8 @@ impl Hash for Post {
}
}
fn get_posts(location: &str) -> Vec<Post> {
let entries = match std::fs::read_dir(location) {
fn get_posts(location: String) -> Vec<Post> {
let entries = match std::fs::read_dir(&location) {
Ok(res) => res
.flatten()
.filter(|f| match f.path().extension() {
@ -190,7 +190,12 @@ async fn page(path: web::Path<(String,)>, config: web::Data<Config>) -> impl Res
fn build_post(file: String, config: Config) -> String {
let mut post = None;
let (infos, toc) = get_post(&mut post, file, config.fc.name.unwrap_or_default());
let (infos, toc) = get_post(
&mut post,
file,
config.fc.name.unwrap_or_default(),
config.locations.data_dir,
);
config.tmpl.render(
"blog/post.html",
@ -206,8 +211,13 @@ fn build_post(file: String, config: Config) -> String {
)
}
fn get_post(post: &mut Option<File>, filename: String, name: String) -> (Infos, String) {
let blog_dir = "data/blog";
fn get_post(
post: &mut Option<File>,
filename: String,
name: String,
data_dir: String,
) -> (Infos, String) {
let blog_dir = format!("{}/blog", data_dir);
let ext = ".md";
*post = read_file(
@ -268,7 +278,7 @@ async fn rss(config: web::Data<Config>) -> impl Responder {
#[once(time = 10800)] // 3h
fn build_rss(config: Config) -> String {
let mut posts = get_posts("data/blog");
let mut posts = get_posts(format!("{}/blog", config.locations.data_dir));
// Sort from newest to oldest
posts.sort_by_cached_key(|p| (p.date.year, p.date.month, p.date.day));
@ -315,7 +325,7 @@ fn build_rss(config: Config) -> String {
.iter_mut()
.map(|p| {
// Get post data
p.fetch_content();
p.fetch_content(&config.locations.data_dir);
// Build item
Item {

View file

@ -30,6 +30,7 @@ async fn page(config: web::Data<Config>) -> impl Responder {
Html(build_page(config.get_ref().to_owned()))
}
/// Contact node
#[derive(Clone, Debug)]
struct ContactLink {
service: String,
@ -38,13 +39,12 @@ struct ContactLink {
}
#[once(time = 60)]
fn find_links() -> Vec<ContactLink> {
// TOML file location
let contacts_dir = "data/contacts";
fn find_links(directory: String) -> Vec<ContactLink> {
// TOML filename
let toml_file = "links.toml";
// Read the TOML file and parse it
let toml_str = read_to_string(format!("{contacts_dir}/{toml_file}")).unwrap_or_default();
let toml_str = read_to_string(format!("{directory}/{toml_file}")).unwrap_or_default();
let mut redirections = vec![];
match toml::de::from_str::<toml::Value>(&toml_str) {
@ -74,9 +74,9 @@ fn find_links() -> Vec<ContactLink> {
#[routes]
#[get("/{service}")]
#[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 link = find_links()
let link = find_links(format!("{}/contacts", config.locations.data_dir))
.iter()
// Find requested service
.filter(|&x| x.service == *info.query("service"))
@ -123,7 +123,7 @@ fn remove_paragraphs(list: &mut [File]) {
#[once(time = 60)]
fn build_page(config: Config) -> String {
let contacts_dir = "data/contacts";
let contacts_dir = format!("{}/contacts", config.locations.data_dir);
let ext = ".md";
let socials_dir = "socials";

View file

@ -116,20 +116,20 @@ async fn build_page(config: Config) -> String {
error: false,
projects: Some(
data.iter()
.filter(|&p| !p.pulls_merged.is_empty())
.cloned()
.filter(|p| !p.pulls_merged.is_empty())
.collect(),
),
waiting: Some(
data.iter()
.filter(|&p| !p.pulls_open.is_empty())
.cloned()
.filter(|p| !p.pulls_open.is_empty())
.collect(),
),
closed: Some(
data.iter()
.filter(|&p| !p.pulls_closed.is_empty())
.cloned()
.filter(|p| !p.pulls_closed.is_empty())
.collect(),
),
}

View file

@ -91,7 +91,10 @@ fn get_content(
return None;
}
read_file(&format!("{cours_dir}/{filename}"), TypeFileMetadata::Cours)
read_file(
&format!("{cours_dir}/{filename}"),
TypeFileMetadata::Generic,
)
}
// #[once(time = 60)]

View file

@ -4,7 +4,10 @@ use ramhorns::Content;
use crate::{
config::Config,
misc::utils::{make_kw, Html},
misc::{
markdown::{read_file, File, TypeFileMetadata},
utils::{make_kw, Html},
},
template::{Infos, NavBar},
};
@ -16,11 +19,37 @@ async fn page(config: web::Data<Config>) -> impl Responder {
#[derive(Content, Debug)]
struct IndexTemplate {
navbar: NavBar,
fullname: String,
name: String,
pronouns: Option<String>,
content: Option<File>,
avatar: String,
avatar_caption: String,
}
#[once(time = 60)]
fn build_page(config: Config) -> String {
let mut file = read_file(
&format!("{}/index.md", config.locations.data_dir),
TypeFileMetadata::Index,
);
// Default values
let mut name = config.fc.fullname.to_owned().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();
if let Some(f) = &file {
if let Some(m) = &f.metadata.info.index {
name = m.name.to_owned().unwrap_or(name);
avatar = m.avatar.to_owned().unwrap_or(avatar);
pronouns = m.pronouns.to_owned();
avatar_caption = m.avatar_caption.to_owned().unwrap_or(avatar_caption);
}
} else {
file = read_file("README.md", TypeFileMetadata::Generic);
}
config.tmpl.render(
"index.html",
IndexTemplate {
@ -28,16 +57,16 @@ fn build_page(config: Config) -> String {
index: true,
..NavBar::default()
},
fullname: config
.fc
.fullname
.to_owned()
.unwrap_or("Fullname".to_owned()),
content: file,
name,
pronouns,
avatar,
avatar_caption,
},
Infos {
page_title: config.fc.fullname,
page_desc: Some("Page principale".into()),
page_kw: make_kw(&["index", "étudiant"]),
page_kw: make_kw(&["index", "étudiant", "accueil"]),
},
)
}

View file

@ -29,7 +29,7 @@ struct PortfolioTemplate<'a> {
#[once(time = 60)]
fn build_page(config: Config) -> String {
let projects_dir = "data/projects";
let projects_dir = format!("{}/projects", config.locations.data_dir);
let ext = ".md";
// Get apps
@ -39,7 +39,7 @@ fn build_page(config: Config) -> String {
.collect::<Vec<File>>();
let appdata = if apps.is_empty() {
(None, Some(projects_dir))
(None, Some(projects_dir.as_str()))
} else {
(Some(apps), None)
};

View file

@ -24,6 +24,7 @@ pub struct Infos {
pub page_kw: Option<String>,
}
/// Information on what page the user is currently
#[derive(Content, Debug, Default)]
pub struct NavBar {
pub index: bool,

BIN
static/badges/friends/jas.webp (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -64,8 +64,8 @@ h1 {
opacity: 1;
}
#friends a {
padding-right: 10px;
#friends a:not(h1 > a) {
padding-right: 5px;
}
#friends h1 {

View file

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