More refactoring + MD switch
+ Switched to Markdown + Removed RST module and tests + Converted RST pages to MD + Updated a few pages + Clippy + Fmt
This commit is contained in:
		
							parent
							
								
									33a6056445
								
							
						
					
					
						commit
						813c16a00c
					
				
							
								
								
									
										142
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										142
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -41,6 +41,15 @@ dependencies = [ | |||||||
|  "libc", |  "libc", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "ansi_term" | ||||||
|  | version = "0.12.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" | ||||||
|  | dependencies = [ | ||||||
|  |  "winapi", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "async-compression" | name = "async-compression" | ||||||
| version = "0.3.15" | version = "0.3.15" | ||||||
| @ -66,6 +75,17 @@ dependencies = [ | |||||||
|  "syn", |  "syn", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "atty" | ||||||
|  | version = "0.2.14" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" | ||||||
|  | dependencies = [ | ||||||
|  |  "hermit-abi", | ||||||
|  |  "libc", | ||||||
|  |  "winapi", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "autocfg" | name = "autocfg" | ||||||
| version = "1.1.0" | version = "1.1.0" | ||||||
| @ -250,6 +270,21 @@ dependencies = [ | |||||||
|  "phf_codegen", |  "phf_codegen", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "clap" | ||||||
|  | version = "2.34.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" | ||||||
|  | dependencies = [ | ||||||
|  |  "ansi_term", | ||||||
|  |  "atty", | ||||||
|  |  "bitflags", | ||||||
|  |  "strsim", | ||||||
|  |  "textwrap", | ||||||
|  |  "unicode-width", | ||||||
|  |  "vec_map", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "codespan-reporting" | name = "codespan-reporting" | ||||||
| version = "0.11.1" | version = "0.11.1" | ||||||
| @ -440,6 +475,15 @@ dependencies = [ | |||||||
|  "version_check", |  "version_check", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "getopts" | ||||||
|  | version = "0.2.21" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" | ||||||
|  | dependencies = [ | ||||||
|  |  "unicode-width", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "getrandom" | name = "getrandom" | ||||||
| version = "0.2.3" | version = "0.2.3" | ||||||
| @ -475,6 +519,15 @@ dependencies = [ | |||||||
|  "walkdir", |  "walkdir", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "heck" | ||||||
|  | version = "0.3.3" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" | ||||||
|  | dependencies = [ | ||||||
|  |  "unicode-segmentation", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "hermit-abi" | name = "hermit-abi" | ||||||
| version = "0.1.15" | version = "0.1.15" | ||||||
| @ -622,9 +675,11 @@ version = "0.1.0" | |||||||
| dependencies = [ | dependencies = [ | ||||||
|  "axum", |  "axum", | ||||||
|  "axum-extra", |  "axum-extra", | ||||||
|  |  "pulldown-cmark", | ||||||
|  "regex", |  "regex", | ||||||
|  "serde", |  "serde", | ||||||
|  "serde_json", |  "serde_json", | ||||||
|  |  "structopt", | ||||||
|  "tera", |  "tera", | ||||||
|  "tokio", |  "tokio", | ||||||
|  "tower", |  "tower", | ||||||
| @ -921,6 +976,30 @@ version = "0.2.9" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" | checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "proc-macro-error" | ||||||
|  | version = "1.0.4" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" | ||||||
|  | dependencies = [ | ||||||
|  |  "proc-macro-error-attr", | ||||||
|  |  "proc-macro2", | ||||||
|  |  "quote", | ||||||
|  |  "syn", | ||||||
|  |  "version_check", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "proc-macro-error-attr" | ||||||
|  | version = "1.0.4" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" | ||||||
|  | dependencies = [ | ||||||
|  |  "proc-macro2", | ||||||
|  |  "quote", | ||||||
|  |  "version_check", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "proc-macro2" | name = "proc-macro2" | ||||||
| version = "1.0.49" | version = "1.0.49" | ||||||
| @ -930,6 +1009,18 @@ dependencies = [ | |||||||
|  "unicode-ident", |  "unicode-ident", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "pulldown-cmark" | ||||||
|  | version = "0.9.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63" | ||||||
|  | dependencies = [ | ||||||
|  |  "bitflags", | ||||||
|  |  "getopts", | ||||||
|  |  "memchr", | ||||||
|  |  "unicase", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "quote" | name = "quote" | ||||||
| version = "1.0.7" | version = "1.0.7" | ||||||
| @ -1142,6 +1233,36 @@ dependencies = [ | |||||||
|  "winapi", |  "winapi", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "strsim" | ||||||
|  | version = "0.8.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "structopt" | ||||||
|  | version = "0.3.26" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" | ||||||
|  | dependencies = [ | ||||||
|  |  "clap", | ||||||
|  |  "lazy_static", | ||||||
|  |  "structopt-derive", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "structopt-derive" | ||||||
|  | version = "0.4.18" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" | ||||||
|  | dependencies = [ | ||||||
|  |  "heck", | ||||||
|  |  "proc-macro-error", | ||||||
|  |  "proc-macro2", | ||||||
|  |  "quote", | ||||||
|  |  "syn", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "syn" | name = "syn" | ||||||
| version = "1.0.107" | version = "1.0.107" | ||||||
| @ -1190,6 +1311,15 @@ dependencies = [ | |||||||
|  "winapi-util", |  "winapi-util", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "textwrap" | ||||||
|  | version = "0.11.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" | ||||||
|  | dependencies = [ | ||||||
|  |  "unicode-width", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "thiserror" | name = "thiserror" | ||||||
| version = "1.0.38" | version = "1.0.38" | ||||||
| @ -1432,12 +1562,24 @@ version = "1.0.6" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "unicode-segmentation" | ||||||
|  | version = "1.10.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "unicode-width" | name = "unicode-width" | ||||||
| version = "0.1.10" | version = "0.1.10" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "vec_map" | ||||||
|  | version = "0.8.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "version_check" | name = "version_check" | ||||||
| version = "0.9.2" | version = "0.9.2" | ||||||
|  | |||||||
| @ -22,3 +22,5 @@ tower-http = { version = "0.3.0", features = [ | |||||||
| ] } | ] } | ||||||
| tower-layer = "0.3.2" | tower-layer = "0.3.2" | ||||||
| axum-extra = { version = "0.4.2", features = ["spa"]} | axum-extra = { version = "0.4.2", features = ["spa"]} | ||||||
|  | structopt = "0.3.26" | ||||||
|  | pulldown-cmark = "0.9.2" | ||||||
|  | |||||||
							
								
								
									
										7
									
								
								src/config.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/config.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | use std::net::SocketAddr; | ||||||
|  | use structopt::StructOpt; | ||||||
|  | 
 | ||||||
|  | #[derive(StructOpt)] | ||||||
|  | pub struct SiteArgs { | ||||||
|  |     pub serve_at: SocketAddr, | ||||||
|  | } | ||||||
| @ -44,3 +44,5 @@ impl Display for JSiteError { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | pub type PageResult<T> = Result<T, JSiteError>; | ||||||
|  | |||||||
							
								
								
									
										55
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										55
									
								
								src/main.rs
									
									
									
									
									
								
							| @ -1,8 +1,7 @@ | |||||||
|  | mod config; | ||||||
| mod error; | mod error; | ||||||
| mod rst_parser; |  | ||||||
| mod tests; | mod tests; | ||||||
| 
 | 
 | ||||||
| use crate::rst_parser::parse_images; |  | ||||||
| use axum::error_handling::HandleErrorLayer; | use axum::error_handling::HandleErrorLayer; | ||||||
| use axum::extract::{Path, State}; | use axum::extract::{Path, State}; | ||||||
| use axum::http::StatusCode; | use axum::http::StatusCode; | ||||||
| @ -10,20 +9,21 @@ use axum::response::{Html, IntoResponse}; | |||||||
| use axum::routing::get; | use axum::routing::get; | ||||||
| use axum::{BoxError, Router}; | use axum::{BoxError, Router}; | ||||||
| use axum_extra::routing::SpaRouter; | use axum_extra::routing::SpaRouter; | ||||||
| use error::JSiteError; | use error::{JSiteError, PageResult}; | ||||||
|  | use pulldown_cmark::html::push_html; | ||||||
|  | use pulldown_cmark::{Options, Parser}; | ||||||
| use regex::Regex; | use regex::Regex; | ||||||
| use rst_parser::parse_links; |  | ||||||
| use serde::Serialize; | use serde::Serialize; | ||||||
| use std::borrow::Cow; | use std::borrow::Cow; | ||||||
| use std::net::SocketAddr; |  | ||||||
| use std::path::PathBuf; | use std::path::PathBuf; | ||||||
| use std::sync::Arc; | use std::sync::Arc; | ||||||
| use std::time::Duration; | use std::time::Duration; | ||||||
|  | use structopt::StructOpt; | ||||||
| use tera::{Context, Tera}; | use tera::{Context, Tera}; | ||||||
| use tower::ServiceBuilder; | use tower::ServiceBuilder; | ||||||
| use tower_http::trace::TraceLayer; | use tower_http::trace::TraceLayer; | ||||||
| 
 | 
 | ||||||
| type PageResult<T> = Result<T, JSiteError>; | use crate::config::SiteArgs; | ||||||
| 
 | 
 | ||||||
| #[derive(Serialize)] | #[derive(Serialize)] | ||||||
| struct SiteFile { | struct SiteFile { | ||||||
| @ -39,20 +39,20 @@ struct PageData { | |||||||
|     links: Vec<SiteFile>, |     links: Vec<SiteFile>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Returns the rendered template of the index page of the website. This includes links and rst
 | /// Returns the rendered template of the index page of the website. This includes links and md
 | ||||||
| /// pages included in `static/raw_rst`
 | /// pages included in `static/raw_md`
 | ||||||
| async fn index(State(state): State<Arc<Tera>>) -> PageResult<impl IntoResponse> { | async fn index(State(state): State<Arc<Tera>>) -> PageResult<impl IntoResponse> { | ||||||
|     let mut ctx = Context::new(); |     let mut ctx = Context::new(); | ||||||
|     let mut links: Vec<SiteFile> = Vec::new(); |     let mut links: Vec<SiteFile> = Vec::new(); | ||||||
| 
 | 
 | ||||||
|     // Get the links to display on the main page
 |     // Get the links to display on the main page
 | ||||||
|     get_pages("static/raw_rst", &mut links)?; |     get_pages("static/raw_md", &mut links)?; | ||||||
| 
 | 
 | ||||||
|     ctx.insert("links", &links); |     ctx.insert("links", &links); | ||||||
|     Ok(Html(state.render("index.html.tera", &ctx)?)) |     Ok(Html(state.render("index.html.tera", &ctx)?)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Gets all the raw rst pages contained in a directory
 | /// Gets all the raw md pages contained in a directory
 | ||||||
| ///
 | ///
 | ||||||
| /// The order of the vector is determined by OS. Ordering can be set by prepending the file name
 | /// The order of the vector is determined by OS. Ordering can be set by prepending the file name
 | ||||||
| /// with a number. Files that start with lower numbers are placed earlier in the list.
 | /// with a number. Files that start with lower numbers are placed earlier in the list.
 | ||||||
| @ -86,7 +86,7 @@ fn get_pages(path: &str, pages: &mut Vec<SiteFile>) -> PageResult<()> { | |||||||
|             } else { |             } else { | ||||||
|                 match rank.parse() { |                 match rank.parse() { | ||||||
|                     Ok(r) => r, |                     Ok(r) => r, | ||||||
|                     Err(_) => std::u32::MAX, |                     Err(_) => u32::MAX, | ||||||
|                 } |                 } | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
| @ -149,12 +149,12 @@ fn get_page(path: &std::path::Path) -> PageResult<SiteFile> { | |||||||
|     Err(JSiteError::PageNotFound(path.to_path_buf())) |     Err(JSiteError::PageNotFound(path.to_path_buf())) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Returns a rendered template of a raw rst page if it exists
 | /// Returns a rendered template of a raw md page if it exists
 | ||||||
| ///
 | ///
 | ||||||
| /// # Arguments
 | /// # Arguments
 | ||||||
| /// * `page` - path to page
 | /// * `page` - path to page
 | ||||||
| async fn rst_page(tera: State<Arc<Tera>>, Path(page): Path<PathBuf>) -> PageResult<Html<String>> { | async fn md_page(tera: State<Arc<Tera>>, Path(page): Path<PathBuf>) -> PageResult<Html<String>> { | ||||||
|     let mut path = PathBuf::from("static/raw_rst"); |     let mut path = PathBuf::from("static/raw_md"); | ||||||
|     path.push(page); |     path.push(page); | ||||||
| 
 | 
 | ||||||
|     // Try and get the page
 |     // Try and get the page
 | ||||||
| @ -182,35 +182,35 @@ async fn rst_page(tera: State<Arc<Tera>>, Path(page): Path<PathBuf>) -> PageResu | |||||||
|         map.insert("page_data", &page_data); |         map.insert("page_data", &page_data); | ||||||
|         tera.render("listing.html.tera", &map)? |         tera.render("listing.html.tera", &map)? | ||||||
|     } else { |     } else { | ||||||
|         // Else, render the RST page
 |         // Else, render the MD page
 | ||||||
|         let mut map = Context::new(); |         let mut map = Context::new(); | ||||||
|         let contents = match std::fs::read_to_string(site_page.path.clone()) { |         let contents = match std::fs::read_to_string(site_page.path.clone()) { | ||||||
|             Ok(contents) => contents, |             Ok(contents) => contents, | ||||||
|             Err(_) => return error_page(&tera, site_page.path.to_str().unwrap()).await, |             Err(_) => return error_page(&tera, site_page.path.to_str().unwrap()).await, | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         // Render links
 |         let options = Options::all(); | ||||||
|         let mut contents = parse_links(&contents).unwrap(); |         let parser = Parser::new_ext(&contents, options); | ||||||
|         contents = parse_images(contents.as_str()).unwrap(); |  | ||||||
| 
 | 
 | ||||||
|         // Ensure render will look good
 |         let mut html_output = String::new(); | ||||||
|         contents = contents.replace('\n', "<br>"); |         push_html(&mut html_output, parser); | ||||||
|         contents = contents.replace("  ", "  "); |  | ||||||
| 
 | 
 | ||||||
|         map.insert("page", &site_page.link_name); |         map.insert("page", &site_page.link_name); | ||||||
|         map.insert("content", &contents); |         map.insert("content", &html_output); | ||||||
|         tera.render("rst_page.html.tera", &map)? |         tera.render("md_page.html.tera", &map)? | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     Ok(Html(page)) |     Ok(Html(page)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// Build error page
 | ||||||
| async fn error_page(tera: &Tera, page: &str) -> PageResult<Html<String>> { | async fn error_page(tera: &Tera, page: &str) -> PageResult<Html<String>> { | ||||||
|     let mut map = Context::new(); |     let mut map = Context::new(); | ||||||
|     map.insert("error_page", page); |     map.insert("error_page", page); | ||||||
|     Ok(Html(tera.render("404.html.tera", &map)?)) |     Ok(Html(tera.render("404.html.tera", &map)?)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// Handle server errors
 | ||||||
| async fn handle_error(error: BoxError) -> impl IntoResponse { | async fn handle_error(error: BoxError) -> impl IntoResponse { | ||||||
|     if error.is::<tower::timeout::error::Elapsed>() { |     if error.is::<tower::timeout::error::Elapsed>() { | ||||||
|         return (StatusCode::REQUEST_TIMEOUT, Cow::from("request timed out")); |         return (StatusCode::REQUEST_TIMEOUT, Cow::from("request timed out")); | ||||||
| @ -232,6 +232,8 @@ async fn handle_error(error: BoxError) -> impl IntoResponse { | |||||||
| /// Launches website
 | /// Launches website
 | ||||||
| #[tokio::main] | #[tokio::main] | ||||||
| async fn main() { | async fn main() { | ||||||
|  |     let args = SiteArgs::from_args(); | ||||||
|  | 
 | ||||||
|     // Use globbing
 |     // Use globbing
 | ||||||
|     let tera = match Tera::new("templates/*.tera") { |     let tera = match Tera::new("templates/*.tera") { | ||||||
|         Ok(t) => Arc::new(t), |         Ok(t) => Arc::new(t), | ||||||
| @ -243,7 +245,7 @@ async fn main() { | |||||||
| 
 | 
 | ||||||
|     let app = Router::new() |     let app = Router::new() | ||||||
|         .route("/", get(index)) |         .route("/", get(index)) | ||||||
|         .route("/about/*path", get(rst_page)) |         .route("/about/*path", get(md_page)) | ||||||
|         .merge(SpaRouter::new("/static", "static")) |         .merge(SpaRouter::new("/static", "static")) | ||||||
|         .layer( |         .layer( | ||||||
|             ServiceBuilder::new() |             ServiceBuilder::new() | ||||||
| @ -256,9 +258,8 @@ async fn main() { | |||||||
|         ) |         ) | ||||||
|         .with_state(tera); |         .with_state(tera); | ||||||
| 
 | 
 | ||||||
|     let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); |     println!("listening on {}", args.serve_at); | ||||||
|     println!("listening on {}", addr); |     axum::Server::bind(&args.serve_at) | ||||||
|     axum::Server::bind(&addr) |  | ||||||
|         .serve(app.into_make_service()) |         .serve(app.into_make_service()) | ||||||
|         .await |         .await | ||||||
|         .unwrap(); |         .unwrap(); | ||||||
|  | |||||||
| @ -1,113 +0,0 @@ | |||||||
| pub mod rst_error; |  | ||||||
| 
 |  | ||||||
| use regex::{Captures, Regex}; |  | ||||||
| use rst_error::RSTError; |  | ||||||
| use std::collections::HashMap; |  | ||||||
| use std::convert::TryFrom; |  | ||||||
| 
 |  | ||||||
| /// RST Image
 |  | ||||||
| struct RSTImage { |  | ||||||
|     /// File location
 |  | ||||||
|     image_file: String, |  | ||||||
|     /// Height
 |  | ||||||
|     height: Option<String>, |  | ||||||
|     /// Width
 |  | ||||||
|     width: Option<String>, |  | ||||||
|     /// Alt Text
 |  | ||||||
|     alt: Option<String>, |  | ||||||
|     /// Align (not implemented)
 |  | ||||||
|     #[allow(dead_code)] |  | ||||||
|     align: Option<String>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl RSTImage { |  | ||||||
|     /// Convert to HTML img tag
 |  | ||||||
|     fn to_html(&self) -> String { |  | ||||||
|         let mut html = format!("<img src=\"{}\"", self.image_file); |  | ||||||
|         let mut style = "".to_string(); |  | ||||||
| 
 |  | ||||||
|         if let Some(alt) = &self.alt { |  | ||||||
|             html = format!("{} alt=\"{}\"", html, alt); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if let Some(height) = &self.height { |  | ||||||
|             style = format!("{}height;{};", style, height); |  | ||||||
|         } |  | ||||||
|         if let Some(width) = &self.width { |  | ||||||
|             style = format!("{}width:{};", style, width); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         html = format!("{} style=\"{}\">", html, style); |  | ||||||
| 
 |  | ||||||
|         html |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl TryFrom<&Captures<'_>> for RSTImage { |  | ||||||
|     type Error = RSTError; |  | ||||||
|     /// Convert from a regex capture to a RSTImage
 |  | ||||||
|     ///
 |  | ||||||
|     /// # Arguments
 |  | ||||||
|     ///
 |  | ||||||
|     /// * cap - regex capture
 |  | ||||||
|     fn try_from(cap: &Captures<'_>) -> Result<Self, Self::Error> { |  | ||||||
|         let image = Self { |  | ||||||
|             image_file: cap |  | ||||||
|                 .name("file_name") |  | ||||||
|                 .map(|m| m.as_str().to_string()) |  | ||||||
|                 .ok_or(RSTError::NotFound("file_name"))?, |  | ||||||
|             height: cap.name("width").map(|m| m.as_str().to_string()), |  | ||||||
|             width: cap.name("width").map(|m| m.as_str().to_string()), |  | ||||||
|             alt: cap.name("alt").map(|m| m.as_str().to_string()), |  | ||||||
|             align: cap.name("align").map(|m| m.as_str().to_string()), |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         Ok(image) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// Renders RST links as HTML links
 |  | ||||||
| ///
 |  | ||||||
| /// # Arguments
 |  | ||||||
| ///
 |  | ||||||
| /// * `string` - input RST string
 |  | ||||||
| pub fn parse_links(string: &str) -> Result<String, RSTError> { |  | ||||||
|     let re_link_ref = Regex::new(r"\n?.. _(.*): (.*)\n")?; |  | ||||||
|     let mut link_map: HashMap<String, String> = HashMap::new(); |  | ||||||
| 
 |  | ||||||
|     for cap in re_link_ref.captures_iter(string) { |  | ||||||
|         link_map.insert(String::from(&cap[1]), String::from(&cap[2])); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     let text: String = re_link_ref.replace_all(string, "").to_string(); |  | ||||||
| 
 |  | ||||||
|     let re_link = Regex::new(r"`(.*)`_")?; |  | ||||||
| 
 |  | ||||||
|     let output = re_link.replace_all(&text, |cap: &Captures| { |  | ||||||
|         let link = match link_map.get(&cap[1]) { |  | ||||||
|             None => String::from(""), |  | ||||||
|             Some(link) => link.to_owned(), |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         format!("<a class=\"link\" href=\"{}\">{}</a>", link, &cap[1]) |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     Ok(output.to_string()) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// Renders RST images as HTML embedded images
 |  | ||||||
| ///
 |  | ||||||
| /// # Arguments
 |  | ||||||
| ///
 |  | ||||||
| /// * `string` - input rst string
 |  | ||||||
| pub fn parse_images(string: &str) -> Result<String, RSTError> { |  | ||||||
|     let re_image = Regex::new( |  | ||||||
|         r".. image:: (?P<file_name>.*)\n( *((:height: (?P<height>.*))|(:width: (?P<width>.*))|(:scale: (?P<scale>.*%))|(:alt: (?P<alt>.*))|(:align: (?P<align>.*)))\n)*", |  | ||||||
|     )?; |  | ||||||
| 
 |  | ||||||
|     Ok(re_image |  | ||||||
|         .replace_all(string, |cap: &Captures| { |  | ||||||
|             RSTImage::try_from(cap).unwrap().to_html() |  | ||||||
|         }) |  | ||||||
|         .to_string()) |  | ||||||
| } |  | ||||||
| @ -1,28 +0,0 @@ | |||||||
| use regex::Error; |  | ||||||
| 
 |  | ||||||
| #[derive(Debug)] |  | ||||||
| pub enum RSTError { |  | ||||||
|     /// Regex compile error
 |  | ||||||
|     RegexError(regex::Error), |  | ||||||
|     /// Required element not found
 |  | ||||||
|     NotFound(&'static str), |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl std::error::Error for RSTError {} |  | ||||||
| 
 |  | ||||||
| impl From<regex::Error> for RSTError { |  | ||||||
|     /// From regex error
 |  | ||||||
|     fn from(e: Error) -> Self { |  | ||||||
|         Self::RegexError(e) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl std::fmt::Display for RSTError { |  | ||||||
|     /// fmt error
 |  | ||||||
|     fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { |  | ||||||
|         match self { |  | ||||||
|             RSTError::NotFound(s) => write!(f, "required element missing: `{}`", s), |  | ||||||
|             RSTError::RegexError(e) => write!(f, "regex compile error: `{:?}`", e), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,28 +0,0 @@ | |||||||
| #[cfg(test)] |  | ||||||
| use super::rst_parser::{parse_images, parse_links}; |  | ||||||
| 
 |  | ||||||
| #[test] |  | ||||||
| fn test_link_parser() { |  | ||||||
|     let mut input = String::from( |  | ||||||
|         "This is a paragraph that contains `a link`_.
 |  | ||||||
| 
 |  | ||||||
|     .. _a link: https://domain.invalida\n",
 |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     let output = parse_links(&mut input).unwrap(); |  | ||||||
| 
 |  | ||||||
|     assert_eq!(output.trim_end(), "This is a paragraph that contains <a class=\"link\" href=\"https://domain.invalida\">a link</a>.") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[test] |  | ||||||
| fn test_image_parser() { |  | ||||||
|     let input = ".. image:: cool/image/123.png
 |  | ||||||
|             :width: 60% |  | ||||||
|             :height: auto |  | ||||||
|             :alt: this is the alt text |  | ||||||
|         ";
 |  | ||||||
| 
 |  | ||||||
|     let output = parse_images(input).unwrap(); |  | ||||||
| 
 |  | ||||||
|     assert_eq!(output.trim_end(), "<img src=\"cool/image/123.png\" alt=\"this is the alt text\" style=\"height;60%;width:60%;\">") |  | ||||||
| } |  | ||||||
| @ -1,24 +1,22 @@ | |||||||
| Joey Hines - joey@ahines.net - Westminster, CO | # Joey Hines - joey@ahines.net - Westminster, CO | ||||||
| ============================================== |  | ||||||
| 
 | 
 | ||||||
| Education | ## Education | ||||||
| --------- |  | ||||||
| * Masters of Science in Electrical and Computer Engineering | * Masters of Science in Electrical and Computer Engineering | ||||||
|   * Auburn University, Auburn, AL. GPA: 3.9. May 2021 |   * Auburn University, Auburn, AL. GPA: 3.9. May 2021 | ||||||
| * Bachelors of Science in Computer Engineering | * Bachelors of Science in Computer Engineering | ||||||
|   * The University of Texas at Dallas, Richardson, TX. GPA: 3.59. May 2019 |   * The University of Texas at Dallas, Richardson, TX. GPA: 3.59. May 2019 | ||||||
| 
 | 
 | ||||||
| Technical Skills | ## Technical Skills | ||||||
| ---------------- |  | ||||||
| * Programing Languages: C, Python, Rust, Java | * Programing Languages: C, Python, Rust, Java | ||||||
| * Operating Systems: Linux, Windows | * Operating Systems: Linux, Windows | ||||||
| * Version Control: Git, SVN | * Version Control: Git, SVN | ||||||
| 
 | 
 | ||||||
| Work Experience | ## Work Experience | ||||||
| --------------- |  | ||||||
| Blue Canyon Technologies - Flight Software Engineer - May 2021 to Present | Blue Canyon Technologies - Flight Software Engineer - May 2021 to Present | ||||||
| * Developed flight software for small satellite missions | * Developed flight software for multiple small satellite missions across the BCT product line range | ||||||
| * Collaborated with mission operations to improve ground station capabilities | * Collaborated with mission operations to improve ground station capabilities | ||||||
|  | * Improved test infrastructure and scripting | ||||||
|  | * Supported the summer internship program as a mentor | ||||||
| 
 | 
 | ||||||
| Auburn University - Graduate Research and Teaching Assistant - August 2019 to May 2021 | Auburn University - Graduate Research and Teaching Assistant - August 2019 to May 2021 | ||||||
| * Command and Data Handling (CDHS) team lead for a cube satellite project | * Command and Data Handling (CDHS) team lead for a cube satellite project | ||||||
							
								
								
									
										21
									
								
								static/raw_md/projects/motion_detector.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								static/raw_md/projects/motion_detector.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | # Motion Detector | ||||||
|  | A project made for Auburn ELEC 7450: Digtal Image Processing. The source code can be found on my | ||||||
|  | [GitHub](https://github.com/joeyahines/motion_detector) | ||||||
|  | 
 | ||||||
|  | ## Goal | ||||||
|  | The goal of this project was to detect motion on a video stream from a [VideoForLinux](https://en.wikipedia.org/wiki/Video4Linux) source.  | ||||||
|  | The algorithm can also be tested by loading in individual frames. | ||||||
|  | 
 | ||||||
|  | ## Implementation | ||||||
|  | The project was written in C and VideoForLinux for grabbing image data and [SDL](https://www.libsdl.org/) | ||||||
|  | for rendering the video  output. A background model is built by implementing a moving average of the image.  | ||||||
|  | In addition to this,  a motion mask is implemented to desensitise the algorithm from background motion objects. | ||||||
|  | 
 | ||||||
|  | Webcam output w/ motion highlighted: | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | 
 | ||||||
|  | Motion detection layer: | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | 
 | ||||||
| @ -1,5 +1,34 @@ | |||||||
| This Website | # This Website (V3) | ||||||
| ============ | 
 | ||||||
|  | This is the 3rd iteration of this website. The 1st iteration was a simple HTML page built with bootstrap | ||||||
|  | that looked similar to the current design. The 2nd iteration moved to Rust and the Rocket framework and | ||||||
|  | was more dynamic allowing for RST pages to be rendered to html. This latest iteration uses Axum and Markdown | ||||||
|  | rendered to HTML for the dynamic content. | ||||||
|  | 
 | ||||||
|  | ## Moving away from Rocket | ||||||
|  | Rocket is a great framework and will probably be the Rust goto. However, it is very complex and comes | ||||||
|  | with a lot of bells and whistles I don't need. Its also in active development which meant it became very | ||||||
|  | hard to update the site. Everytime I wanted to add a new feature I would go to update Rocket and have something | ||||||
|  | break.  | ||||||
|  | 
 | ||||||
|  | I originally tried Warp, which I have used for some other projects. While I like it mostly, I find it very | ||||||
|  | hard to move past the basic examples. And when you run into issues with it, the compiler loves to spit out | ||||||
|  | very esoteric errors that are a pain to debug.  | ||||||
|  | 
 | ||||||
|  | I'm also a low level developer. I like working at lower levels of interfaces. Axum is pretty high level | ||||||
|  | in the grand scheme of things, but it doesn't seem overly opinionated in its design. Axum has its own issues | ||||||
|  | and I doubt this will be the last time I switch the framework... | ||||||
|  | 
 | ||||||
|  | ## Moving away from RST | ||||||
|  | I lied, in the last updated for this site I said how much I loved RST. After using more Markdown for note-taking | ||||||
|  | and for some documentation, I realized I like it more. I always decided to not write my own parse for it. | ||||||
|  | Rust has some good libraries that render MD to HTML.  | ||||||
|  | 
 | ||||||
|  | # This Website (V2) | ||||||
|  | > NOTE: This is the old version of this page for the second version of this site. It's here for archival purposes. | ||||||
|  | This was originally written in RST and has been converted to MD for v3.  | ||||||
|  | 
 | ||||||
|  | ## This Website | ||||||
| This is the 2nd version of my personal website, the first version was very similar to this but written in plain HTML | This is the 2nd version of my personal website, the first version was very similar to this but written in plain HTML | ||||||
| using Bootstrap. I was never very happy with how it looked and it being raw HTML limited what I could do with it. To | using Bootstrap. I was never very happy with how it looked and it being raw HTML limited what I could do with it. To | ||||||
| improve it, I set out with a few goals: | improve it, I set out with a few goals: | ||||||
| @ -10,19 +39,15 @@ improve it, I set out with a few goals: | |||||||
| * Improve aesthetics | * Improve aesthetics | ||||||
| * Ensure mobile version also looks good | * Ensure mobile version also looks good | ||||||
| 
 | 
 | ||||||
| Picking Rust and Rocket | ### Picking Rust and Rocket | ||||||
| ----------------------- | As I am not much of a UI person, the website update was put on the back burner for sometime. The old site was "good | ||||||
| As I am not much of a UI person, the website update was put on the back burner for sometime. The old site was "good  |  | ||||||
| enough." I originally considered doing it in Python and using Django. But that fails the criteria of being easily | enough." I originally considered doing it in Python and using Django. But that fails the criteria of being easily | ||||||
| deployable. In early 2020, I began learning Rust out of curiosity and the need to use it for research.  | deployable. In early 2020, I began learning Rust out of curiosity and the need to use it for research. | ||||||
| I decided to tackle the website backend in Rust as a learning exercise. I went with the `Rocket`_ framework. It | I decided to tackle the website backend in Rust as a learning exercise. I went with the Rocket framework. It | ||||||
| seemed to suit my needs and supported Tera templates. Tera templates are very similar to Django's templates that I | seemed to suit my needs and supported Tera templates. Tera templates are very similar to Django's templates that I | ||||||
| had already had experience in. | had already had experience in. | ||||||
| 
 | 
 | ||||||
| .. _Rocket: https://rocket.rs/ | ### Easily Editable | ||||||
| 
 |  | ||||||
| Easily Editable |  | ||||||
| --------------- |  | ||||||
| As the style of the site is a simplistic linux terminal, I decided it would be nice to have the longer pages be in | As the style of the site is a simplistic linux terminal, I decided it would be nice to have the longer pages be in | ||||||
| raw reStructuredText. This fits the aesthetic of the site well and unlike HTML files, they are raw text and are easy to | raw reStructuredText. This fits the aesthetic of the site well and unlike HTML files, they are raw text and are easy to | ||||||
| write and read while editing them. RST files can also easily be built into PDFs, which is great for generating a resume. | write and read while editing them. RST files can also easily be built into PDFs, which is great for generating a resume. | ||||||
| @ -32,8 +57,7 @@ the Rust backend looks for all pages and lists them. To control ordering, I impl | |||||||
| character of a file name is its rank. For example, my resume is in a file called 1resume.rst. This gives it rank of | character of a file name is its rank. For example, my resume is in a file called 1resume.rst. This gives it rank of | ||||||
| 1 so it shows up on the main page before other links. | 1 so it shows up on the main page before other links. | ||||||
| 
 | 
 | ||||||
| Improving Aesthetics | ### Improving Aesthetics | ||||||
| -------------------- |  | ||||||
| Like stated earlier, I am no UI designer. The terminal style meant I could put very little effort into the design of the | Like stated earlier, I am no UI designer. The terminal style meant I could put very little effort into the design of the | ||||||
| actual website. Somehow in version one, I still managed to mess that up. This was mainly due to hacking together | actual website. Somehow in version one, I still managed to mess that up. This was mainly due to hacking together | ||||||
| bootstrap to give me something that looked like a terminal. This time, I dumped Bootstrap and wrote my own CSS. I should | bootstrap to give me something that looked like a terminal. This time, I dumped Bootstrap and wrote my own CSS. I should | ||||||
| @ -1,33 +0,0 @@ | |||||||
| Motion Detector |  | ||||||
| =============== |  | ||||||
| A project made for Auburn ELEC 7450: Digtal Image Processing. The source code can be found on my `Github`_ |  | ||||||
| 
 |  | ||||||
| .. _GitHub: https://github.com/joeyahines/motion_detector |  | ||||||
| 
 |  | ||||||
| Goal |  | ||||||
| ++++ |  | ||||||
| The goal of this project was to detect motion on a video stream from a `VideoForLinux`_ source. The algorithm |  | ||||||
| can also be tested by loading in individual frames. |  | ||||||
| 
 |  | ||||||
| .. _VideoForLinux: https://en.wikipedia.org/wiki/Video4Linux |  | ||||||
| 
 |  | ||||||
| Implementation |  | ||||||
| ++++++++++++++ |  | ||||||
| The project was written in C and VideoForLinux for grabbing image data and `SDL`_ for rendering the video |  | ||||||
| output. A background model is built by implementing a moving average of the image. In addition to this, |  | ||||||
| a motion mask is implemented to desensitise the algorithm from background motion objects. |  | ||||||
| 
 |  | ||||||
| .. _SDL: https://www.libsdl.org/ |  | ||||||
| 
 |  | ||||||
| Webcam output w/ motion highlighted: |  | ||||||
| 
 |  | ||||||
| .. image:: https://github.com/joeyahines/motion_detector/blob/master/docs/final_report/motion.png?&raw=true |  | ||||||
|     :width: 60% |  | ||||||
|     :height: auto |  | ||||||
| 
 |  | ||||||
| Motion detection layer: |  | ||||||
| 
 |  | ||||||
| .. image:: https://github.com/joeyahines/motion_detector/blob/master/docs/final_report/motion_image.png?raw=true |  | ||||||
|     :width: 60% |  | ||||||
|     :height: auto |  | ||||||
| 
 |  | ||||||
| @ -6,7 +6,7 @@ | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .text { | .text { | ||||||
|     font-family: "Ubuntu Mono", monospace; |     font-family: "Hack", monospace; | ||||||
|     color: whitesmoke; |     color: whitesmoke; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -15,7 +15,7 @@ | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .heading { | .heading { | ||||||
|     font-family: "Ubuntu Mono", monospace; |     font-family: "Hack", monospace; | ||||||
|     color: limegreen; |     color: limegreen; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ | |||||||
|     <link rel="stylesheet" type="text/css" href="/static/style.css" xmlns="http://www.w3.org/1999/html"> |     <link rel="stylesheet" type="text/css" href="/static/style.css" xmlns="http://www.w3.org/1999/html"> | ||||||
|     <meta charset="UTF-8"> |     <meta charset="UTF-8"> | ||||||
|     <title>Joey Hines.</title> |     <title>Joey Hines.</title> | ||||||
|     <link href="https://fonts.googleapis.com/css?family=Ubuntu+Mono&display=swap" rel="stylesheet"> |     <link href="https://cdnjs.cloudflare.com/ajax/libs/hack-font/3.3.0/web/hack.min.css" rel="stylesheet"> | ||||||
|     <link rel='shortcut icon' href='/static/logo.png'/> |     <link rel='shortcut icon' href='/static/logo.png'/> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> |     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
| </head> | </head> | ||||||
| @ -14,12 +14,12 @@ | |||||||
| <div class="center"> | <div class="center"> | ||||||
|     <a class="no_underline" href="/"> <h1 class="heading"> Joey Hines.</h1></a> |     <a class="no_underline" href="/"> <h1 class="heading"> Joey Hines.</h1></a> | ||||||
|         <div class="terminal"> |         <div class="terminal"> | ||||||
|             <p class="text body"> |             <div class="text body"> | ||||||
|                 <span class="prompt">joey@ahines:~$</span> {% block command %}{% endblock command %} <br/> |                 <span class="prompt">joey@ahines:~$</span> {% block command %}{% endblock command %} <br/> | ||||||
|                 {% block content %}{% endblock content %} |                 {% block content %}{% endblock content %} | ||||||
|                 <br/> |                 <br/> | ||||||
|                 <span class="prompt">joey@ahines:~$</span> <span class="blinking">  ▊ </span> |             </div> | ||||||
|             </p> |             <span class="prompt text body">joey@ahines:~$</span> <span class="blinking">  ▊ </span> | ||||||
|         </div> |         </div> | ||||||
|     <h6 class="text"> © <script type="text/javascript">document.write( new Date().getFullYear().toString());</script> Joey Hines</h6> |     <h6 class="text"> © <script type="text/javascript">document.write( new Date().getFullYear().toString());</script> Joey Hines</h6> | ||||||
| </div> | </div> | ||||||
|  | |||||||
| @ -1,11 +1,12 @@ | |||||||
| {% extends "base.html.tera" %} | {% extends "base.html.tera" %} | ||||||
| 
 | 
 | ||||||
| {% block command %} | {% block command %} | ||||||
| cat {{ page }}.rst | cat {{ page }}.md | ||||||
| {% endblock command %} | {% endblock command %} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | <div class="text"> | ||||||
| {% block content %} | {% block content %} | ||||||
| <br/> | <br/> | ||||||
| {{ content | safe }} | {{ content | safe }} | ||||||
| {% endblock content %} | {% endblock content %} | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user