Metadata and contacts (#38)
All checks were successful
ci/woodpecker/push/publish Pipeline was successful

- Rework metadata, now each type of file based on markdown have his own metadata struct (blog/portfolio/contacts)
- Contact is now generated by markdown files

Reviewed-on: #38
Co-authored-by: Mylloon <kennel.anri@tutanota.com>
Co-committed-by: Mylloon <kennel.anri@tutanota.com>
This commit is contained in:
Mylloon 2023-10-20 18:21:11 +02:00 committed by Anri Kennel
parent 34c1720cdc
commit f84a37829c
Signed by: Forgejo
GPG key ID: E72245C752A07631
10 changed files with 200 additions and 252 deletions

View file

@ -6,7 +6,7 @@ use serde::{Deserialize, Deserializer};
use std::fs;
#[derive(Default, Deserialize, Content, Debug)]
pub struct FileMetadata {
pub struct FileMetadataBlog {
pub title: Option<String>,
pub link: Option<String>,
pub date: Option<Date>,
@ -17,6 +17,37 @@ pub struct FileMetadata {
pub language: Option<String>,
}
#[derive(Default, Deserialize, Content, Debug)]
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)]
pub struct Tag {
pub name: String,
@ -87,14 +118,14 @@ pub fn get_options() -> ComrakOptions {
}
/// Transform markdown string to File structure
fn read(raw_text: &str) -> File {
fn read(raw_text: &str, metadata_type: TypeFileMetadata) -> File {
let arena = Arena::new();
let options = get_options();
let root = parse_document(&arena, raw_text, &options);
// Find metadata
let metadata = get_metadata(root);
let metadata = get_metadata(root, metadata_type);
let mermaid_name = "mermaid";
hljs_replace(root, mermaid_name);
@ -117,26 +148,54 @@ fn read(raw_text: &str) -> File {
}
/// Read markdown file
pub fn read_file(filename: &str) -> Option<File> {
pub fn read_file(filename: &str, expected_file: TypeFileMetadata) -> Option<File> {
match fs::read_to_string(filename) {
Ok(text) => Some(read(&text)),
Ok(text) => Some(read(&text, expected_file)),
_ => None,
}
}
/// Deserialize metadata based on a type
fn deserialize_metadata<T: Default + serde::de::DeserializeOwned>(text: &str) -> T {
serde_yaml::from_str(text.trim_matches(&['-', '\n'] as &[_])).unwrap_or_default()
}
/// Fetch metadata from AST
pub fn get_metadata<'a>(root: &'a AstNode<'a>) -> FileMetadata {
pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: TypeFileMetadata) -> FileMetadata {
match root
.children()
.find_map(|node| match &node.data.borrow().value {
NodeValue::FrontMatter(text) => {
// '-' correspond to `front_matter_delimiter`
serde_yaml::from_str(text.trim_matches(&['-', '\n'] as &[_])).unwrap_or_default()
}
NodeValue::FrontMatter(text) => Some(match mtype {
TypeFileMetadata::Blog => FileMetadata {
blog: Some(deserialize_metadata(text)),
..FileMetadata::default()
},
TypeFileMetadata::Contact => FileMetadata {
contact: Some(deserialize_metadata(text)),
..FileMetadata::default()
},
TypeFileMetadata::Portfolio => FileMetadata {
portfolio: Some(deserialize_metadata(text)),
..FileMetadata::default()
},
}),
_ => None,
}) {
Some(data) => data,
None => FileMetadata::default(),
None => match mtype {
TypeFileMetadata::Blog => FileMetadata {
blog: Some(FileMetadataBlog::default()),
..FileMetadata::default()
},
TypeFileMetadata::Contact => FileMetadata {
contact: Some(FileMetadataContact::default()),
..FileMetadata::default()
},
TypeFileMetadata::Portfolio => FileMetadata {
portfolio: Some(FileMetadataPortfolio::default()),
..FileMetadata::default()
},
},
}
}

View file

@ -18,7 +18,9 @@ use crate::{
config::Config,
misc::{
date::Date,
markdown::{get_metadata, get_options, read_file, File, FileMetadata},
markdown::{
get_metadata, get_options, read_file, File, FileMetadataBlog, TypeFileMetadata,
},
utils::get_url,
},
template::{Infos, NavBar},
@ -87,7 +89,10 @@ impl Post {
let blog_dir = "data/blog";
let ext = ".md";
if let Some(file) = read_file(&format!("{blog_dir}/{}{ext}", self.url)) {
if let Some(file) = read_file(
&format!("{blog_dir}/{}{ext}", self.url),
TypeFileMetadata::Blog,
) {
self.content = Some(file.content);
}
}
@ -126,7 +131,7 @@ fn get_posts(location: &str) -> Vec<Post> {
let options = get_options();
let root = parse_document(&arena, &text, &options);
let mut metadata = get_metadata(root);
let mut metadata = get_metadata(root, TypeFileMetadata::Blog).blog.unwrap();
// Always have a title
metadata.title = match metadata.title {
@ -136,9 +141,9 @@ fn get_posts(location: &str) -> Vec<Post> {
metadata
}
Err(_) => FileMetadata {
Err(_) => FileMetadataBlog {
title: Some(file_without_ext.into()),
..FileMetadata::default()
..FileMetadataBlog::default()
},
};
@ -222,20 +227,23 @@ fn get_post(
let blog_dir = "data/blog";
let ext = ".md";
*post = read_file(&format!("{blog_dir}/{filename}{ext}"));
*post = read_file(
&format!("{blog_dir}/{filename}{ext}"),
TypeFileMetadata::Blog,
);
let default = (&filename, Vec::new(), String::new());
let (title, tags, toc) = match post {
Some(data) => (
match &data.metadata.info.title {
match &data.metadata.info.blog.as_ref().unwrap().title {
Some(text) => text,
None => default.0,
},
match &data.metadata.info.tags {
match &data.metadata.info.blog.as_ref().unwrap().tags {
Some(tags) => tags.clone(),
None => default.1,
},
match &data.metadata.info.toc {
match &data.metadata.info.blog.as_ref().unwrap().toc {
// TODO: Generate TOC
Some(true) => String::new(),
_ => default.2,

View file

@ -1,10 +1,14 @@
use actix_web::{get, routes, web, HttpRequest, HttpResponse, Responder};
use cached::proc_macro::once;
use glob::glob;
use ramhorns::Content;
use crate::{
config::Config,
misc::utils::get_url,
misc::{
markdown::{read_file, File, TypeFileMetadata},
utils::get_url,
},
template::{Infos, NavBar},
};
@ -63,17 +67,65 @@ async fn service_redirection(req: HttpRequest) -> impl Responder {
#[derive(Content, Debug)]
struct NetworksTemplate {
navbar: NavBar,
socials_exists: bool,
socials: Vec<File>,
forges_exists: bool,
forges: Vec<File>,
others_exists: bool,
others: Vec<File>,
}
fn remove_paragraphs(list: &mut [File]) {
list.iter_mut()
.for_each(|file| file.content = file.content.replace("<p>", "").replace("</p>", ""));
}
#[once(time = 60)]
fn build_page(config: Config, url: String) -> String {
let contacts_dir = "data/contacts";
let ext = ".md";
let socials_dir = "socials";
let mut socials = glob(&format!("{contacts_dir}/{socials_dir}/*{ext}"))
.unwrap()
.map(|e| read_file(&e.unwrap().to_string_lossy(), TypeFileMetadata::Contact).unwrap())
.collect::<Vec<File>>();
let forges_dir = "forges";
let mut forges = glob(&format!("{contacts_dir}/{forges_dir}/*{ext}"))
.unwrap()
.map(|e| read_file(&e.unwrap().to_string_lossy(), TypeFileMetadata::Contact).unwrap())
.collect::<Vec<File>>();
let others_dir = "others";
let mut others = glob(&format!("{contacts_dir}/{others_dir}/*{ext}"))
.unwrap()
.map(|e| read_file(&e.unwrap().to_string_lossy(), TypeFileMetadata::Contact).unwrap())
.collect::<Vec<File>>();
// Remove paragraphs in custom statements
[&mut socials, &mut forges, &mut others]
.iter_mut()
.for_each(|it| remove_paragraphs(it));
config.tmpl.render(
"contact.html",
"contact/index.html",
NetworksTemplate {
navbar: NavBar {
contact: true,
..NavBar::default()
},
socials_exists: !socials.is_empty(),
socials,
forges_exists: !forges.is_empty(),
forges,
others_exists: !others.is_empty(),
others,
},
Infos {
page_title: Some("Contacts".into()),

View file

@ -6,7 +6,7 @@ use ramhorns::Content;
use crate::{
config::Config,
misc::{
markdown::{read_file, File},
markdown::{read_file, File, TypeFileMetadata},
utils::get_url,
},
template::{Infos, NavBar},
@ -38,7 +38,7 @@ fn build_page(config: Config, url: String) -> String {
// Get apps
let apps = glob(&format!("{projects_dir}/*{ext}"))
.unwrap()
.map(|e| read_file(&e.unwrap().to_string_lossy()).unwrap())
.map(|e| read_file(&e.unwrap().to_string_lossy(), TypeFileMetadata::Portfolio).unwrap())
.collect::<Vec<File>>();
let appdata = if apps.is_empty() {
@ -50,7 +50,7 @@ fn build_page(config: Config, url: String) -> String {
// Get archived apps
let archived_apps = glob(&format!("{projects_dir}/archive/*{ext}"))
.unwrap()
.map(|e| read_file(&e.unwrap().to_string_lossy()).unwrap())
.map(|e| read_file(&e.unwrap().to_string_lossy(), TypeFileMetadata::Portfolio).unwrap())
.collect::<Vec<File>>();
let archived_appdata = if archived_apps.is_empty() {

View file

@ -15,6 +15,10 @@ main ul {
column-gap: 5em;
}
main li {
break-inside: avoid-column;
}
main li > p {
margin: 0;
padding: 3%;

View file

@ -15,7 +15,7 @@
</head>
<body>
<header>
{{>navbar.html}} {{#info}}
{{>navbar.html}} {{#info}} {{#blog}}
<h1>{{title}}</h1>
{{#date}} {{>blog/date.html}} {{/date}}
<ul>
@ -23,7 +23,7 @@
<li>{{name}}</li>
{{/tags}}
</ul>
{{/info}} {{/metadata}} {{/post}}
{{/blog}} {{/info}} {{/metadata}} {{/post}}
</header>
<main>
{{^post}}

View file

@ -1,223 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head dir="ltr">
{{>head.html}}
<link rel="stylesheet" href="/css/contact.css" />
</head>
<body>
<header>{{>navbar.html}}</header>
<main>
<h1>Contact</h1>
<p>Je suis présent relativement partout sur internet 😸</p>
<h2>Réseaux sociaux</h2>
<ul>
<li>
<p>
Twitter :
<a
href="/contact/twitter"
target="_blank"
rel="noreferrer me"
title="Compte Twitter, pour le shitposting"
>@Mylloon</a
>
</p>
</li>
<li>
<p>
Mastodon :
<a
href="/contact/mastodon"
target="_blank"
rel="noreferrer me"
title="Compte Mastodon, alternative à Twitter, principalement pour l'IT"
>Mylloon@piaille.fr</a
>
</p>
</li>
<li>
<p>
Bluesky :
<a
href="/contact/bluesky"
target="_blank"
rel="noreferrer me"
title="Compte Bluesky, alternative à Twitter, quand Elon aura rendu Twitter payant je serais principalement sur Bluesky"
>mylloon.fr</a
>
</p>
</li>
<li>
<p>
Discord :
<a
href="/contact/discord/user"
target="_blank"
rel="noreferrer me"
title="Compte Discord perso"
>mylloon</a
>
et
<a
href="/contact/discord/guild"
target="_blank"
rel="noreferrer"
title="Serveur Discord accessible à tous, venez !"
>mon serveur</a
>
</p>
</li>
<li>
<p>
Reddit :
<a
href="/contact/reddit"
target="_blank"
rel="noreferrer me"
title="Compte Reddit, sert à rien"
>mylloon</a
>
</p>
</li>
<li>
<p>
Instagram :
<a
href="/contact/instagram"
target="_blank"
rel="noreferrer me"
title="Compte Instagram, sert à rien"
>mylloon</a
>
</p>
</li>
<li>
<p>
Kitsu :
<a
href="/contact/kitsu"
target="_blank"
rel="noreferrer me"
title="Compte Kitsu, pour suivre les anime/manga/webtoon que je lis/regarde"
>Mylloon</a
>
</p>
</li>
<li>
<p>
Steam :
<a
href="/contact/steam"
target="_blank"
rel="noreferrer me"
title="Compte Steam pour les jeux-vidéos"
>Mylloon</a
>
</p>
</li>
<li>
<p>
Youtube :
<a
href="/contact/youtube"
target="_blank"
rel="noreferrer me"
title="Compte YouTube, parfois je poste des vidéos JV ou IT"
>Mylloon</a
>
</p>
</li>
<li>
<p>
Twitch :
<a
href="/contact/twitch"
target="_blank"
rel="noreferrer me"
title="Compte Twitch, parfois je stream soit des JV soit du dev"
>mylloon</a
>
</p>
</li>
</ul>
<h2>Forges</h2>
<ul>
<li>
<p>
Github :
<a
href="/contact/github"
target="_blank"
rel="noreferrer me"
title="Compte GitHub, principalement pour les contributions"
>Mylloon</a
>
</p>
</li>
<li>
<p>
Gitlab :
<a
href="/contact/gitlab"
target="_blank"
rel="noreferrer me"
title="Compte Gitlab, sert à rien"
>Mylloon</a
>
</p>
</li>
<li>
<p>
Codeberg :
<a
href="/contact/codeberg"
target="_blank"
rel="noreferrer me"
title="Compte Codeberg, pas utilisé mais j'adore Codeberg !"
>Mylloon</a
>
</p>
</li>
<li>
<p>
Forgejo (mon instance) :
<a
href="/contact/forgejo"
target="_blank"
rel="noreferrer me"
title="Compte Forgejo, là où il y a tout mes projets"
>Anri</a
>
</p>
</li>
</ul>
<h2>Autre</h2>
<ul>
<li>
<p>
Mail :
<a
href="mailto:kennel.anri%20at%20tutanota.com"
title="kennel.anri at tutanota.com"
>kennel.anri at tutanota.com</a
>
</p>
</li>
<li>
<p>
Keyoxide :
<a
href="/contact/keyoxide"
target="_blank"
rel="noreferrer me"
title="Page Keyoxide, vérifie l'appartenance de la majorité des comptes mentionné ici via GPG"
>Mylloon</a
>
</p>
</li>
</ul>
</main>
</body>
</html>

View file

@ -0,0 +1,17 @@
{{#metadata}} {{#info}} {{#contact}}
<li>
<p>
{{title}} : {{^custom}} {{#newtab}}
<a
href="{{link}} "
target="_blank"
rel="noreferrer me"
title="{{description}} "
>{{user}}</a
>
{{/newtab}} {{^newtab}}
<a href="{{link}} " title="{{description}} ">{{user}}</a>
{{/newtab}} {{/custom}} {{#custom}} {{&content}} {{/custom}}
</p>
</li>
{{/contact}} {{/info}} {{/metadata}}

View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="fr">
<head dir="ltr">
{{>head.html}}
<link rel="stylesheet" href="/css/contact.css" />
</head>
<body>
<header>{{>navbar.html}}</header>
<main>
<h1>Contact</h1>
<p>Je suis présent relativement partout sur internet 😸</p>
{{#data}} {{#socials_exists}}
<h2>Réseaux sociaux</h2>
<ul>
{{#socials}} {{>contact/element.html}} {{/socials}}
</ul>
{{/socials_exists}} {{#forges_exists}}
<h2>Forges</h2>
<ul>
{{#forges}} {{>contact/element.html}} {{/forges}}
</ul>
{{/forges_exists}} {{#others_exists}}
<h2>Autre</h2>
<ul>
{{#others}} {{>contact/element.html}} {{/others}}
</ul>
{{/others_exists}} {{/data}}
</main>
</body>
</html>

View file

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