Basic cours support #44
32 changed files with 1767 additions and 258 deletions
748
Cargo.lock
generated
748
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
19
Cargo.toml
19
Cargo.toml
|
@ -10,19 +10,24 @@ 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.5", default-features = false, features = ["macros", "compress-brotli"] }
|
||||||
actix-files = "0.6"
|
actix-files = "0.6"
|
||||||
cached = { version = "0.49", features = ["async"] }
|
cached = { version = "0.49", 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.2"
|
||||||
|
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.21"
|
comrak = "0.22"
|
||||||
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", default-features = false, features = ["clock"]}
|
||||||
chrono-tz = "0.8"
|
chrono-tz = "0.8"
|
||||||
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"
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
- [Blog](#blog)
|
- [Blog](#blog)
|
||||||
- [Projects](#projects)
|
- [Projects](#projects)
|
||||||
- [Contacts](#contacts)
|
- [Contacts](#contacts)
|
||||||
|
- [Courses](#courses)
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
|
@ -105,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
|
||||||
|
@ -248,3 +250,7 @@ For example, `socials` contact files are stored in `/app/data/contacts/socials/`
|
||||||
### About <!-- omit in toc -->
|
### About <!-- omit in toc -->
|
||||||
|
|
||||||
The file is stored at `/app/data/contacts/about.md`.
|
The file is stored at `/app/data/contacts/about.md`.
|
||||||
|
|
||||||
|
## Courses
|
||||||
|
|
||||||
|
Markdown files are stored in `/app/data/cours/`
|
||||||
|
|
|
@ -7,7 +7,7 @@ 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 FileConfig {
|
||||||
/// http/https
|
/// http/https
|
||||||
pub scheme: Option<String>,
|
pub scheme: Option<String>,
|
||||||
|
@ -27,6 +27,8 @@ 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 FileConfig {
|
||||||
|
@ -37,6 +39,7 @@ 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()),
|
||||||
|
exclude_courses: Some([].into()),
|
||||||
..FileConfig::default()
|
..FileConfig::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,6 +68,7 @@ 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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,12 @@ 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 +19,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>,
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
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::fs;
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Metadata for blog posts
|
/// Metadata for blog posts
|
||||||
#[derive(Default, Deserialize, Content, Debug)]
|
#[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>,
|
||||||
|
@ -29,7 +35,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)),
|
||||||
},
|
},
|
||||||
|
@ -39,7 +45,7 @@ impl<'de> Deserialize<'de> for Tag {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Metadata for contact entry
|
/// Metadata for contact entry
|
||||||
#[derive(Default, Deserialize, Content, Debug)]
|
#[derive(Content, Debug, Default, Deserialize)]
|
||||||
pub struct FileMetadataContact {
|
pub struct FileMetadataContact {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub custom: Option<bool>,
|
pub custom: Option<bool>,
|
||||||
|
@ -50,7 +56,7 @@ pub struct FileMetadataContact {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Metadata for index page
|
/// Metadata for index page
|
||||||
#[derive(Default, Deserialize, Content, Debug)]
|
#[derive(Content, Debug, Default, Deserialize)]
|
||||||
pub struct FileMetadataIndex {
|
pub struct FileMetadataIndex {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub pronouns: Option<String>,
|
pub pronouns: Option<String>,
|
||||||
|
@ -60,7 +66,7 @@ pub struct FileMetadataIndex {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Metadata for portfolio cards
|
/// Metadata for portfolio cards
|
||||||
#[derive(Default, Deserialize, Content, Debug)]
|
#[derive(Content, Debug, Default, Deserialize)]
|
||||||
pub struct FileMetadataPortfolio {
|
pub struct FileMetadataPortfolio {
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
pub link: Option<String>,
|
pub link: Option<String>,
|
||||||
|
@ -79,7 +85,7 @@ pub enum TypeFileMetadata {
|
||||||
|
|
||||||
/// Structure who holds all the metadata the file have
|
/// Structure who holds all the metadata the file have
|
||||||
/// Usually all fields are None except one
|
/// Usually all fields are None except one
|
||||||
#[derive(Default, Deserialize, Content, Debug)]
|
#[derive(Content, Debug, Default, Deserialize)]
|
||||||
pub struct FileMetadata {
|
pub struct FileMetadata {
|
||||||
pub blog: Option<FileMetadataBlog>,
|
pub blog: Option<FileMetadataBlog>,
|
||||||
pub contact: Option<FileMetadataContact>,
|
pub contact: Option<FileMetadataContact>,
|
||||||
|
@ -96,6 +102,15 @@ pub struct Metadata {
|
||||||
pub syntax_highlight: bool,
|
pub syntax_highlight: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Metadata {
|
||||||
|
/// Update current metadata boolean fields, keeping true ones
|
||||||
|
fn merge(&mut self, other: Metadata) {
|
||||||
|
self.math = self.math || other.math;
|
||||||
|
self.mermaid = self.mermaid || other.mermaid;
|
||||||
|
self.syntax_highlight = self.syntax_highlight || other.syntax_highlight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// File description
|
/// File description
|
||||||
#[derive(Content, Debug)]
|
#[derive(Content, Debug)]
|
||||||
pub struct File {
|
pub struct File {
|
||||||
|
@ -118,6 +133,9 @@ 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;
|
||||||
|
|
||||||
// Parser
|
// Parser
|
||||||
options.parse.smart = true; // could be boring
|
options.parse.smart = true; // could be boring
|
||||||
|
@ -134,6 +152,7 @@ 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.escaped_char_spans = false;
|
||||||
|
|
||||||
options
|
options
|
||||||
}
|
}
|
||||||
|
@ -186,12 +205,71 @@ 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: String) -> (String, Metadata) {
|
||||||
|
let mut metadata = Metadata {
|
||||||
|
info: FileMetadata::default(),
|
||||||
|
math: false,
|
||||||
|
mermaid: false,
|
||||||
|
syntax_highlight: 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:{};base64,{}", mime, 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 = match options {
|
||||||
let root = parse_document(&arena, raw_text, &options);
|
Some(specific_opt) => specific_opt,
|
||||||
|
None => get_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);
|
||||||
|
@ -201,34 +279,31 @@ 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();
|
||||||
|
|
||||||
|
let children_metadata;
|
||||||
|
(html_content, children_metadata) = fix_images_and_integration(path, html_content);
|
||||||
html_content = custom_img_size(html_content);
|
html_content = custom_img_size(html_content);
|
||||||
|
|
||||||
File {
|
let mut final_metadata = Metadata {
|
||||||
metadata: Metadata {
|
info: metadata,
|
||||||
info: metadata,
|
mermaid: check_mermaid(root, mermaid_name),
|
||||||
mermaid: check_mermaid(root, mermaid_name),
|
syntax_highlight: check_code(root, &[mermaid_name.into()]),
|
||||||
syntax_highlight: check_code(root, &[mermaid_name.into()]),
|
math: check_math(&html_content),
|
||||||
math: check_math(&html_content),
|
};
|
||||||
},
|
final_metadata.merge(children_metadata);
|
||||||
content: html_content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read markdown file
|
File {
|
||||||
pub fn read_file(filename: &str, expected_file: TypeFileMetadata) -> Option<File> {
|
metadata: final_metadata,
|
||||||
match fs::read_to_string(filename) {
|
content: html_content,
|
||||||
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().trim_matches(&['-'] as &[_])).unwrap_or_default()
|
serde_yml::from_str(text.trim().trim_matches(&['-'] as &[_])).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch metadata from AST
|
/// Fetch metadata from AST
|
||||||
|
@ -318,9 +393,25 @@ 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 mut output = vec![];
|
||||||
|
let _ = HtmlRewriter::new(
|
||||||
|
Settings {
|
||||||
|
element_content_handlers: vec![element!("span[data-math-style]", |_| {
|
||||||
|
math_detected.store(true, Ordering::SeqCst);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})],
|
||||||
|
..Settings::default()
|
||||||
|
},
|
||||||
|
|c: &[u8]| output.extend_from_slice(c),
|
||||||
|
)
|
||||||
|
.write(html.as_bytes());
|
||||||
|
|
||||||
|
math_detected.load(Ordering::SeqCst)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change class of languages for hljs detection
|
/// Change class of languages for hljs detection
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
|
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::FileConfig;
|
||||||
|
|
||||||
|
use super::markdown::{read_md, File, FileMetadata, Metadata, TypeFileMetadata};
|
||||||
|
|
||||||
#[cached]
|
#[cached]
|
||||||
pub fn get_reqwest_client() -> Client {
|
pub fn get_reqwest_client() -> Client {
|
||||||
Client::builder()
|
Client::builder()
|
||||||
|
@ -45,3 +51,40 @@ impl Responder for Html {
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read a file
|
||||||
|
pub fn read_file(filename: &str, expected_file: TypeFileMetadata) -> Option<File> {
|
||||||
|
match Path::new(filename).extension() {
|
||||||
|
Some(ext) => match ext.to_str().unwrap() {
|
||||||
|
"pdf" => match fs::read(filename) {
|
||||||
|
Ok(bytes) => Some(read_pdf(bytes)),
|
||||||
|
Err(_) => None,
|
||||||
|
},
|
||||||
|
_ => match fs::read_to_string(filename) {
|
||||||
|
Ok(text) => Some(read_md(filename, &text, expected_file, None)),
|
||||||
|
Err(_) => None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
None => 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,
|
||||||
|
},
|
||||||
|
content: format!(
|
||||||
|
r#"<embed
|
||||||
|
src="data:application/pdf;base64,{}"
|
||||||
|
style="width: 100%; height: 79vh";
|
||||||
|
>"#,
|
||||||
|
pdf
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,10 +18,8 @@ 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::{Infos, NavBar},
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,8 +7,8 @@ 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::{Infos, NavBar},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -1,9 +1,148 @@
|
||||||
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::{Infos, NavBar},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct PathRequest {
|
||||||
|
q: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/cours")]
|
#[get("/cours")]
|
||||||
async fn page() -> impl Responder {
|
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: Vec<String>,
|
||||||
|
) -> 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.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.to_owned());
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,8 @@ 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::{Infos, NavBar},
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,8 +6,8 @@ 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::{Infos, NavBar},
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,7 +14,7 @@ pub struct Template {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Structure used by /routes/*.rs
|
/// Structure used by /routes/*.rs
|
||||||
#[derive(Default, Debug)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Infos {
|
pub struct Infos {
|
||||||
/// Title
|
/// Title
|
||||||
pub page_title: Option<String>,
|
pub page_title: Option<String>,
|
||||||
|
|
53
static/css/cours.css
Normal file
53
static/css/cours.css
Normal 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.
Binary file not shown.
Binary file not shown.
|
@ -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
|
|
|
@ -65,3 +65,9 @@ header nav a:hover {
|
||||||
.bold {
|
.bold {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
header nav {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
167
static/js/cours.js
Normal file
167
static/js/cours.js
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
|
@ -24,6 +24,7 @@ window.addEventListener("load", () => {
|
||||||
`
|
`
|
||||||
),
|
),
|
||||||
new Tag("Nul en CSS", "font-family: 'Comic Sans MS', cursive"),
|
new Tag("Nul en CSS", "font-family: 'Comic Sans MS', cursive"),
|
||||||
|
new Tag("Mention poufiasse"),
|
||||||
new Tag("anri k... caterpillar 🐛☝️"),
|
new Tag("anri k... caterpillar 🐛☝️"),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
452
static/js/libs/hljs-languages/julia.js
Normal file
452
static/js/libs/hljs-languages/julia.js
Normal 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);
|
||||||
|
})();
|
93
static/js/libs/hljs-languages/ocaml.js
Normal file
93
static/js/libs/hljs-languages/ocaml.js
Normal 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);
|
||||||
|
})();
|
|
@ -9,7 +9,7 @@ window.addEventListener("load", () => {
|
||||||
/* Aliases of langs */
|
/* Aliases of langs */
|
||||||
const aliases = {
|
const aliases = {
|
||||||
bash: ["fish"],
|
bash: ["fish"],
|
||||||
pascal: ["pseudocode"],
|
julia: ["pseudocode"],
|
||||||
};
|
};
|
||||||
for (const lang in aliases) {
|
for (const lang in aliases) {
|
||||||
hljs.registerAliases(aliases[lang], { languageName: lang });
|
hljs.registerAliases(aliases[lang], { languageName: lang });
|
||||||
|
|
|
@ -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`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -10,17 +10,20 @@ 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,
|
output: "mathml",
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
30
templates/cours.html
Normal file
30
templates/cours.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<!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}}
|
||||||
|
{{/metadata}} {{/content}} {{/data}}
|
||||||
|
<script src="/js/cours.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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>
|
||||||
|
|
|
@ -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/base16/solarized-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/base16/dracula.min.css"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
|
|
|
@ -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>
|
|
Loading…
Reference in a new issue