Compare commits

...

20 commits

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

2
Cargo.lock generated
View file

@ -927,9 +927,11 @@ dependencies = [
"minify-html",
"minify-js 0.5.6",
"ramhorns",
"regex",
"reqwest",
"rss",
"serde",
"serde_json",
"serde_yaml",
"toml",
]

View file

@ -17,6 +17,7 @@ ramhorns = "0.14"
toml = "0.8"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"
serde_json = "1.0"
minify-html = "0.11"
minify-js = "0.5"
glob = "0.3"
@ -26,3 +27,4 @@ chrono = { version = "0.4.30", default-features = false, features = ["clock"]}
chrono-tz = "0.8"
rss = { version = "2.0", features = ["atom"] }
lol_html = "1.2"
regex = "1.10"

View file

@ -13,6 +13,7 @@
- [Blog](#blog)
- [Projects](#projects)
- [Contacts](#contacts)
- [Courses](#courses)
# Installation
@ -204,3 +205,7 @@ Custom project description
- `user` is the username used in the platform
- `description` will be rendered as HTML "title" (text will appear when cursor
is hover the link)
## Courses
Markdown files are stored in `/app/data/cours/`

View file

@ -7,7 +7,7 @@ use std::{fs::File, io::Write, path::Path};
use crate::template::Template;
/// Store the configuration of config/config.toml
#[derive(Deserialize, Clone, Default, Debug)]
#[derive(Clone, Debug, Default, Deserialize)]
pub struct FileConfig {
/// http/https
pub scheme: Option<String>,

View file

@ -5,12 +5,12 @@ use serde::Deserialize;
use crate::misc::utils::get_reqwest_client;
#[derive(Deserialize, Debug)]
#[derive(Debug, Deserialize)]
struct GithubResponse {
items: Vec<GithubProject>,
}
#[derive(Deserialize, Debug)]
#[derive(Debug, Deserialize)]
struct GithubProject {
repository_url: String,
number: u32,
@ -19,7 +19,7 @@ struct GithubProject {
pull_request: GithubPullRequest,
}
#[derive(Deserialize, Debug)]
#[derive(Debug, Deserialize)]
struct GithubPullRequest {
html_url: String,
merged_at: Option<String>,

View file

@ -3,10 +3,15 @@ use comrak::nodes::{AstNode, NodeValue};
use comrak::{format_html, parse_document, Arena, ComrakOptions, ListStyleType};
use lol_html::{element, rewrite_str, RewriteStrSettings};
use ramhorns::Content;
use regex::{Captures, Regex};
use serde::{Deserialize, Deserializer};
use std::fs;
#[derive(Default, Deserialize, Content, Debug)]
/// Regular markdown files, no metadata
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileNoMetadata {}
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadataBlog {
pub title: Option<String>,
pub date: Option<Date>,
@ -16,7 +21,7 @@ pub struct FileMetadataBlog {
pub toc: Option<bool>,
}
#[derive(Default, Deserialize, Content, Debug)]
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadataContact {
pub title: String,
pub custom: Option<bool>,
@ -26,7 +31,7 @@ pub struct FileMetadataContact {
pub description: Option<String>,
}
#[derive(Default, Deserialize, Content, Debug)]
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadataPortfolio {
pub title: Option<String>,
pub link: Option<String>,
@ -38,13 +43,15 @@ pub enum TypeFileMetadata {
Blog,
Contact,
Portfolio,
Cours,
}
#[derive(Default, Deserialize, Content, Debug)]
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadata {
pub blog: Option<FileMetadataBlog>,
pub contact: Option<FileMetadataContact>,
pub portfolio: Option<FileMetadataPortfolio>,
pub cours: Option<FileNoMetadata>,
}
#[derive(Content, Debug, Clone)]
@ -164,12 +171,35 @@ fn custom_img_size(html: String) -> String {
.unwrap()
}
fn math_processing(regex: &str, source: &str) -> String {
Regex::new(regex)
.unwrap()
.replace_all(source, |captures: &Captures| {
captures
.iter()
.skip(1)
.filter_map(|capture| {
capture.map(|m| format!("<span data-katex=\"{}\"></span>", m.as_str()))
})
.collect::<Vec<String>>()
.join("")
})
.to_string()
}
/// Transform markdown string to File structure
fn read(raw_text: &str, metadata_type: TypeFileMetadata) -> File {
let arena = Arena::new();
// Transform LaTeX formulas
let text = math_processing(
r"(?U)(\$[^\$|\n]+\$)[^\$]",
&math_processing(r"(?U)(\$\$[^\$|\n]+\$\$)", raw_text),
);
println!("{}", text);
let options = get_options();
let root = parse_document(&arena, raw_text, &options);
let root = parse_document(&arena, &text, &options);
// Find metadata
let metadata = get_metadata(root, metadata_type);
@ -182,6 +212,7 @@ fn read(raw_text: &str, metadata_type: TypeFileMetadata) -> File {
format_html(root, &options, &mut html).unwrap();
let mut html_content = String::from_utf8(html).unwrap();
/* println!("{}", html_content); */
html_content = custom_img_size(html_content);
@ -227,6 +258,10 @@ pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: TypeFileMetadata) -> FileM
portfolio: Some(deserialize_metadata(text)),
..FileMetadata::default()
},
TypeFileMetadata::Cours => FileMetadata {
cours: Some(deserialize_metadata(text)),
..FileMetadata::default()
},
}),
_ => None,
}) {
@ -244,6 +279,10 @@ pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: TypeFileMetadata) -> FileM
portfolio: Some(FileMetadataPortfolio::default()),
..FileMetadata::default()
},
TypeFileMetadata::Cours => FileMetadata {
cours: Some(FileNoMetadata::default()),
..FileMetadata::default()
},
},
}
}

View file

@ -26,7 +26,7 @@ struct PortfolioTemplate {
closed: Option<Vec<Project>>,
}
#[derive(Content, Clone, Debug)]
#[derive(Clone, Content, Debug)]
struct Project {
name: String,
url: String,
@ -35,7 +35,7 @@ struct Project {
pulls_closed: Vec<Pull>,
}
#[derive(Content, Clone, Debug)]
#[derive(Clone, Content, Debug)]
struct Pull {
url: String,
id: u32,

View file

@ -1,9 +1,128 @@
use actix_web::{get, Responder};
use std::path::Path;
use actix_web::{get, web, Responder};
use ramhorns::Content;
use serde::{Deserialize, Serialize};
use crate::{
config::Config,
misc::{
markdown::{read_file, File, TypeFileMetadata},
utils::{make_kw, Html},
},
template::{Infos, NavBar},
};
#[derive(Debug, Deserialize)]
pub struct PathRequest {
q: Option<String>,
}
#[get("/cours")]
async fn page() -> impl Responder {
// Page de notes de cours
// Cf. https://univ.mylloon.fr/
// Cf. https://github.com/xy2z/PineDocs
actix_web::web::Redirect::to("/")
async fn page(info: web::Query<PathRequest>, config: web::Data<Config>) -> impl Responder {
Html(build_page(info, config.get_ref().to_owned()))
}
#[derive(Content, Debug)]
struct CoursTemplate {
navbar: NavBar,
filetree: String,
content: Option<File>,
}
#[derive(Debug, Serialize)]
struct FileNode {
name: String,
is_dir: bool,
children: Vec<FileNode>,
}
/// Build the filetree
fn get_filetree(dir_path: &str, exclusion_list: &[&str]) -> FileNode {
let entries = std::fs::read_dir(dir_path).unwrap();
let mut children = Vec::new();
for entry in entries.filter_map(Result::ok) {
let entry_path = entry.path();
let entry_name = entry_path.file_name().and_then(|n| n.to_str()).unwrap();
// We should support regex?
if !exclusion_list.contains(&entry_name) {
let filename = entry_name.to_string();
if entry_path.is_file() {
children.push(FileNode {
name: filename,
is_dir: false,
children: vec![],
});
} else {
children.push(get_filetree(entry_path.to_str().unwrap(), exclusion_list));
}
}
}
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: &[&str],
) -> Option<File> {
let filename = match &path.q {
Some(q) => q,
None => "index.md",
};
// We should support regex?
if exclusion_list
.iter()
.any(|&excluded_term| filename.contains(excluded_term))
{
return None;
}
read_file(&format!("{cours_dir}/{filename}"), TypeFileMetadata::Cours)
}
// #[once(time = 60)]
// TODO: Uncomment before release
fn build_page(info: web::Query<PathRequest>, config: Config) -> String {
let cours_dir = "data/cours";
let exclusion_list = [];
let filetree = get_filetree(cours_dir, &exclusion_list);
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),
},
Infos {
page_title: Some("Cours".into()),
page_desc: Some("Cours à l'univ".into()),
page_kw: make_kw(&[
"cours",
"études",
"université",
"licence",
"master",
"notes",
"digital garden",
]),
},
)
}

View file

@ -14,7 +14,7 @@ pub struct Template {
}
/// Structure used by /routes/*.rs
#[derive(Default, Debug)]
#[derive(Debug, Default)]
pub struct Infos {
/// Title
pub page_title: Option<String>,

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

@ -0,0 +1,47 @@
/* 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;
}
}

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

@ -65,3 +65,9 @@ header nav a:hover {
.bold {
font-weight: bold;
}
@media print {
header nav {
visibility: hidden;
}
}

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

@ -0,0 +1,104 @@
/**
* 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
);
}
}
};
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 fullpath = window.location.href.split("?")[1].substring(2);
const path = fullpath.substring(0, fullpath.lastIndexOf("/"));
const last_openeded = deepestNodeOpened(
path.split("/"),
fileTreeElement.querySelector("ul").childNodes
);
uncollapse(last_openeded);
});

View file

@ -10,17 +10,26 @@ window.addEventListener("load", () => {
la: "leftarrow",
RA: "Rightarrow",
LA: "Leftarrow",
u: "mu",
})
)[Symbol.iterator]()) {
macros[`\\${item[0]}`] = `\\${item[1]}`;
}
renderMathInElement(document.body, {
delimiters: [
{ left: "$$", right: "$$", display: true },
{ left: "$", right: "$", display: false },
],
throwOnError: false,
macros,
document.querySelectorAll("span[data-katex]").forEach((element) => {
const rawLaTeXFormula = element.getAttribute("data-katex");
const displayMode = rawLaTeXFormula.startsWith("$$");
const strip = displayMode ? 2 : 1;
katex.render(
rawLaTeXFormula.slice(strip, rawLaTeXFormula.length - strip),
element,
{
throwOnError: false,
macros,
displayMode,
output: "mathml",
}
);
});
});

26
templates/cours.html Normal file
View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="fr">
<head dir="ltr">
{{>head.html}}
<link rel="stylesheet" href="/css/cours.css" />
</head>
<body>
<header>{{>navbar.html}}</header>
<aside>
<span data-json="{{#data}}{{filetree}}{{/data}} "></span>
</aside>
<main>
{{#data}} {{^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}}
{{/metadata}} {{/content}} {{/data}}
<script src="/js/cours.js"></script>
</body>
</html>

View file

@ -4,10 +4,4 @@
integrity="sha384-j/ZricySXBnNMJy9meJCtyXTKMhIJ42heyr7oAdxTDBy/CYA9hzpMo+YTNV5C+1X"
crossorigin="anonymous"
></script>
<script
defer
src="//cdn.jsdelivr.net/npm/katex@0.16.6/dist/contrib/auto-render.min.js"
integrity="sha384-+VBxd3r6XgURycqtZ117nYw44OOcIax56Z4dCRWbxyPt0Koah1uHoK0o4+/RRE05"
crossorigin="anonymous"
></script>
<script src="/js/libs/katex.js"></script>

View file

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