Compare commits

..

2 commits
main ... toc

Author SHA1 Message Date
ba4b5b6b54 Respect toc attribute in metadata
Some checks are pending
ci/woodpecker/push/publish Pipeline is pending approval
2024-06-21 20:43:42 +02:00
2400d33f56 wip: quick and dumb implementation of toc 2024-06-21 20:43:42 +02:00
15 changed files with 377 additions and 511 deletions

View file

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

15
.woodpecker/publish.yml Normal file
View file

@ -0,0 +1,15 @@
steps:
publish:
image: woodpeckerci/plugin-docker-buildx:2
settings:
labels:
platform: 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

582
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

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

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://git.mylloon.fr/Anri/mylloon.fr/badges/workflows/publish.yml/badge.svg)](https://git.mylloon.fr/Anri/mylloon.fr/actions?workflow=publish.yml) [![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 [issues](https://git.mylloon.fr/Anri/mylloon.fr/issues)
- See [documentation](https://git.mylloon.fr/Anri/mylloon.fr/src/branch/main/Documentation.md) - See [documentation](https://git.mylloon.fr/Anri/mylloon.fr/src/branch/main/Documentation.md)

View file

@ -42,14 +42,7 @@ 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( .service(web::scope("/api").service(web::scope("v1").service(api_v1::love)))
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)

View file

@ -1,8 +1,10 @@
use crate::misc::date::Date; use crate::misc::date::Date;
use base64::engine::general_purpose; use base64::engine::general_purpose;
use base64::Engine; use base64::Engine;
use comrak::nodes::{AstNode, NodeValue}; use comrak::nodes::{AstNode, NodeCode, NodeMath, NodeValue};
use comrak::{format_html, parse_document, Arena, ComrakOptions, ListStyleType, Options}; use comrak::{
format_html, parse_document, Anchorizer, Arena, ComrakOptions, ListStyleType, Options,
};
use lol_html::html_content::ContentType; use lol_html::html_content::ContentType;
use lol_html::{element, rewrite_str, HtmlRewriter, RewriteStrSettings, Settings}; use lol_html::{element, rewrite_str, HtmlRewriter, RewriteStrSettings, Settings};
use ramhorns::Content; use ramhorns::Content;
@ -119,10 +121,11 @@ impl Metadata {
pub struct File { pub struct File {
pub metadata: Metadata, pub metadata: Metadata,
pub content: String, pub content: String,
pub toc_data: String,
} }
/// Options used for parser and compiler MD --> HTML /// Options used for parser and compiler MD --> HTML
pub fn get_options<'a>() -> ComrakOptions<'a> { pub fn get_options() -> ComrakOptions {
let mut options = comrak::Options::default(); let mut options = comrak::Options::default();
// Extension // Extension
@ -139,18 +142,12 @@ pub fn get_options<'a>() -> ComrakOptions<'a> {
options.extension.multiline_block_quotes = true; options.extension.multiline_block_quotes = true;
options.extension.math_dollars = true; options.extension.math_dollars = true;
options.extension.math_code = false; 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
@ -161,13 +158,7 @@ pub fn get_options<'a>() -> ComrakOptions<'a> {
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.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
} }
@ -179,7 +170,7 @@ fn custom_img_size(html: &str) -> String {
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('|').collect::<Vec<&str>>(); let possible_piece = alt.split(|c| c == '|').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();
@ -302,6 +293,8 @@ pub fn read_md(
html_content = custom_img_size(&html_content); html_content = custom_img_size(&html_content);
(html_content, mail_obfsucated) = mail_obfuscation(&html_content); (html_content, mail_obfsucated) = mail_obfuscation(&html_content);
let toc = toc_to_html(&generate_toc(root));
let mut final_metadata = Metadata { let mut final_metadata = Metadata {
info: metadata, info: metadata,
mermaid: check_mermaid(root, mermaid_name), mermaid: check_mermaid(root, mermaid_name),
@ -314,6 +307,7 @@ pub fn read_md(
File { File {
metadata: final_metadata, metadata: final_metadata,
content: html_content, content: html_content,
toc_data: toc,
} }
} }
@ -500,3 +494,87 @@ fn mail_obfuscation(html: &str) -> (String, bool) {
(new_html, is_modified) (new_html, is_modified)
} }
} }
#[derive(Debug)]
struct TOCEntry {
id: String,
title: String,
depth: u8,
}
fn generate_toc<'a>(root: &'a AstNode<'a>) -> Vec<TOCEntry> {
/// See <https://github.com/kivikakk/comrak/blob/b67d406d3b101b93539c37a1ca75bff81ff8c149/src/html.rs#L446>
fn collect_text<'a>(node: &'a AstNode<'a>, output: &mut String) {
match node.data.borrow().value {
NodeValue::Text(ref literal)
| NodeValue::Code(NodeCode { ref literal, .. })
| NodeValue::Math(NodeMath { ref literal, .. }) => {
*output = literal.to_string();
}
_ => {
for n in node.children() {
if !output.is_empty() {
break;
}
collect_text(n, output);
}
}
}
}
let mut toc = vec![];
let mut anchorizer = Anchorizer::new();
// Collect headings first to avoid mutable borrow conflicts
let headings: Vec<_> = root
.children()
.filter_map(|node| {
if let NodeValue::Heading(ref nch) = &node.data.borrow().value {
Some((*nch, node))
} else {
None
}
})
.collect();
// Now process each heading
for (nch, node) in headings {
let mut title = String::with_capacity(20);
collect_text(node, &mut title);
toc.push(TOCEntry {
id: anchorizer.anchorize(title.clone()),
title,
depth: nch.level,
});
}
toc
}
fn toc_to_html(toc: &[TOCEntry]) -> String {
if toc.is_empty() {
return String::new();
}
let mut html = Vec::with_capacity(20 + 20 * toc.len());
html.extend_from_slice(b"<ul>");
for entry in toc {
// TODO: Use depth
html.extend_from_slice(
format!(
"<li><a href=\"{}\">{} (dbg/depth/{})</a></li>",
entry.id, entry.title, entry.depth
)
.as_bytes(),
);
}
html.extend_from_slice(b"</ul>");
String::from_utf8(html).unwrap()
}

View file

@ -81,5 +81,6 @@ fn read_pdf(data: Vec<u8>) -> File {
style="width: 100%; height: 79vh"; style="width: 100%; height: 79vh";
>"# >"#
), ),
toc_data: String::new(),
} }
} }

View file

@ -1,53 +1,15 @@
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 /// Response
#[derive(Serialize)] #[derive(Serialize)]
struct InfoLove { struct Info {
unix_epoch: u32, unix_epoch: u32,
} }
#[get("/love")] #[get("/love")]
pub async fn love() -> impl Responder { pub async fn love() -> impl Responder {
HttpResponse::Ok().json(InfoLove { HttpResponse::Ok().json(Info {
unix_epoch: 1_605_576_600, 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

@ -186,7 +186,6 @@ fn get_posts(location: &str) -> Vec<Post> {
struct BlogPostTemplate { struct BlogPostTemplate {
navbar: NavBar, navbar: NavBar,
post: Option<File>, post: Option<File>,
toc: String,
} }
#[get("/blog/p/{id}")] #[get("/blog/p/{id}")]
@ -199,7 +198,7 @@ pub async fn page(path: web::Path<(String,)>, config: web::Data<Config>) -> impl
fn build_post(file: &str, config: Config) -> String { fn build_post(file: &str, config: Config) -> String {
let mut post = None; let mut post = None;
let (infos, toc) = get_post( let infos = get_post(
&mut post, &mut post,
file, file,
&config.fc.name.unwrap_or_default(), &config.fc.name.unwrap_or_default(),
@ -214,18 +213,12 @@ fn build_post(file: &str, config: Config) -> String {
..NavBar::default() ..NavBar::default()
}, },
post, post,
toc,
}, },
infos, infos,
) )
} }
fn get_post( fn get_post(post: &mut Option<File>, filename: &str, name: &str, data_dir: &str) -> InfosPage {
post: &mut Option<File>,
filename: &str,
name: &str,
data_dir: &str,
) -> (InfosPage, String) {
let blog_dir = format!("{data_dir}/{BLOG_DIR}/{POST_DIR}"); let blog_dir = format!("{data_dir}/{BLOG_DIR}/{POST_DIR}");
let ext = ".md"; let ext = ".md";
@ -234,13 +227,8 @@ fn get_post(
&TypeFileMetadata::Blog, &TypeFileMetadata::Blog,
); );
let default = ( let default = (filename, &format!("Blog d'{name}"), Vec::new());
filename, let (title, desc, tags) = match post {
&format!("Blog d'{name}"),
Vec::new(),
String::new(),
);
let (title, desc, tags, toc) = match post {
Some(data) => ( Some(data) => (
match &data.metadata.info.blog.as_ref().unwrap().title { match &data.metadata.info.blog.as_ref().unwrap().title {
Some(text) => text, Some(text) => text,
@ -254,16 +242,10 @@ fn get_post(
Some(tags) => tags.clone(), Some(tags) => tags.clone(),
None => default.2, None => default.2,
}, },
match &data.metadata.info.blog.as_ref().unwrap().toc {
// TODO: Generate TOC
Some(true) => String::new(),
_ => default.3,
},
), ),
None => default, None => default,
}; };
(
InfosPage { InfosPage {
title: Some(format!("Post: {title}")), title: Some(format!("Post: {title}")),
desc: Some(desc.clone()), desc: Some(desc.clone()),
@ -273,9 +255,7 @@ fn get_post(
.chain(tags.iter().map(|t| t.name.as_str())) .chain(tags.iter().map(|t| t.name.as_str()))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
)), )),
}, }
toc,
)
} }
#[routes] #[routes]

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

Binary file not shown.

View file

@ -12,7 +12,6 @@
--background: #f1f1f1; --background: #f1f1f1;
--font-color: #18181b; --font-color: #18181b;
--link-color: #df5a9c; --link-color: #df5a9c;
--selection-color: #c5c5c560;
} }
} }
@ -22,6 +21,5 @@
--background: #171e26; --background: #171e26;
--font-color: #bcbcc5; --font-color: #bcbcc5;
--link-color: #ff80bf; --link-color: #ff80bf;
--selection-color: #c5c5c530;
} }
} }

View file

@ -4,10 +4,6 @@ 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);

View file

@ -12,13 +12,12 @@ window.addEventListener("load", () => {
-webkit-background-clip: text; /* Chromium fix */ -webkit-background-clip: text; /* Chromium fix */
color: transparent; color: transparent;
`; `;
const mono = "font-family: monospace";
const tags = [ const tags = [
new Tag("Comment fonctionne un PC 😵‍💫"), new Tag("Comment fonctionne un PC 😵‍💫"),
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("A rater son master 🎊"),
new Tag("Peak D2 sur Valo 🤡"), new Tag("Peak D2 sur Valo 🤡"),
new Tag( new Tag(
"0x520", "0x520",
@ -62,7 +61,6 @@ window.addEventListener("load", () => {
text-shadow: 0px 0px 20px light-dark(var(--font-color), transparent); 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

@ -28,7 +28,9 @@
<main> <main>
{{^post}} {{^post}}
<p>This post doesn't exist... sorry</p> <p>This post doesn't exist... sorry</p>
{{/post}} {{#post}} {{&toc}} {{/post}} {{#post}} {{#metadata}} {{#info}} {{#blog}} {{#toc}}
<aside>{{&toc_data}}</aside>
{{/toc}} {{/blog}} {{/info}} {{/metadata}}
<article>{{&content}}</article> <article>{{&content}}</article>
{{/post}} {{/post}}
</main> </main>