use template
This commit is contained in:
		
							parent
							
								
									80a86e9f8a
								
							
						
					
					
						commit
						19f51c5732
					
				
							
								
								
									
										173
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,173 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
| <meta charset="UTF-8"> | ||||
| <meta name="color-scheme" content="light dark"> | ||||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
| <title> | ||||
| <!-- t: safe sitename --> | ||||
| </title> | ||||
| <style> | ||||
| a:link { | ||||
|   color: DarkCyan; | ||||
|   text-decoration: none; | ||||
|   font-weight: normal; | ||||
| } | ||||
| a:hover { | ||||
|   color: DarkCyan; | ||||
|   text-decoration: none; | ||||
|   font-weight: bold; | ||||
| } | ||||
| a:visited { | ||||
|   color: DarkCyan; | ||||
|   text-decoration: underline; | ||||
|   font-weight: normal; | ||||
| } | ||||
| span { | ||||
|   color: SeaGreen; | ||||
|   font-weight: normal; | ||||
| } | ||||
| .ohs { | ||||
|   display: none; | ||||
| } | ||||
| .oht:hover + .ohs, .ohs:hover { | ||||
|   display: block; | ||||
| } | ||||
| </style> | ||||
| </head> | ||||
| <body> | ||||
| 
 | ||||
| <!-- t: if domain --> | ||||
| 
 | ||||
| <!-- t: if sitedecoration --> | ||||
| <h2><!-- t: safe sitedecoration --> <!-- t: safe sitename --> <!-- t: safe sitedecoration --></h2> | ||||
| <!-- t: else --> | ||||
| <h2><!-- t: safe sitename --></h2> | ||||
| <!-- t: end --> | ||||
| 
 | ||||
| 
 | ||||
| <!-- t: if running --> | ||||
| 
 | ||||
| <h3>things running on the server</h3> | ||||
| <ul> | ||||
| 
 | ||||
| <!-- t: for running --> | ||||
| <li> | ||||
| <!-- t: if info --> | ||||
| <a href="/info/<!-- t: raw info -->"> | ||||
| <!-- t: else --> | ||||
| <!-- t: if redirect --> | ||||
| <a href="/<!-- t: raw redirect -->"> | ||||
| <!-- t: else --> | ||||
| <span> | ||||
| <!-- t: end --> | ||||
| <!-- t: end --> | ||||
| <!-- t: safe id --> | ||||
| <!-- t: if info --> | ||||
| </a> | ||||
| <!-- t: else --> | ||||
| <!-- t: if redirect --> | ||||
| </a> | ||||
| <!-- t: else --> | ||||
| </span> | ||||
| <!-- t: end --> | ||||
| <!-- t: end --> | ||||
| : <!-- t: // comment to keep editors from removing the trailing space --> | ||||
| <!-- t: if additional --> | ||||
| <em class="oht"> | ||||
| <!-- t: end --> | ||||
| <!-- t: safe status --> | ||||
| <!-- t: if additional --> | ||||
| </em><div class="ohs"> | ||||
| <!-- t: set linebreak --> | ||||
| <!-- t: for line additional --> | ||||
| <!-- t: raw linebreak --> | ||||
| <!-- t: set linebreak <br> | ||||
|  --> | ||||
| <!-- t: safe line --> | ||||
| <!-- t: end --> | ||||
| </div> | ||||
| <!-- t: end --> | ||||
| <!-- t: if debug --> | ||||
| <!-- t: if duration --> | ||||
|  | <!-- t: safe duration -->ms | ||||
| <!-- t: end --> | ||||
| <!-- t: end --> | ||||
| </li> | ||||
| 
 | ||||
| <!-- t: end --> | ||||
| </ul> | ||||
| 
 | ||||
| <!-- t: end --> | ||||
| 
 | ||||
| <!-- t: if static --> | ||||
| 
 | ||||
| <h3>everything else</h3> | ||||
| <ul> | ||||
| 
 | ||||
| <!-- t: for static --> | ||||
| <li> | ||||
| <!-- t: if info --> | ||||
| <a href="/info/<!-- t: raw info -->"> | ||||
| <!-- t: else --> | ||||
| <!-- t: if redirect --> | ||||
| <a href="/<!-- t: raw redirect -->"> | ||||
| <!-- t: else --> | ||||
| <span> | ||||
| <!-- t: end --> | ||||
| <!-- t: end --> | ||||
| <!-- t: safe id --> | ||||
| <!-- t: if info --> | ||||
| </a> | ||||
| <!-- t: else --> | ||||
| <!-- t: if redirect --> | ||||
| </a> | ||||
| <!-- t: else --> | ||||
| </span> | ||||
| <!-- t: end --> | ||||
| <!-- t: end --> | ||||
| : <!-- t: // comment to keep editors from removing the trailing space --> | ||||
| <!-- t: if additional --> | ||||
| <em class="oht"> | ||||
| <!-- t: end --> | ||||
| <!-- t: safe status --> | ||||
| <!-- t: if additional --> | ||||
| </em><div class="ohs"> | ||||
| <!-- t: set linebreak --> | ||||
| <!-- t: for line additional --> | ||||
| <!-- t: raw linebreak --> | ||||
| <!-- t: set linebreak <br> | ||||
|  --> | ||||
| <!-- t: safe line --> | ||||
| <!-- t: end --> | ||||
| </div> | ||||
| <!-- t: end --> | ||||
| <!-- t: if debug --> | ||||
| <!-- t: if duration --> | ||||
|  | <!-- t: safe duration -->ms | ||||
| <!-- t: end --> | ||||
| <!-- t: end --> | ||||
| </li> | ||||
| 
 | ||||
| <!-- t: end --> | ||||
| </ul> | ||||
| 
 | ||||
| <!-- t: end --> | ||||
| 
 | ||||
| <!-- t: if docslink --> | ||||
| 
 | ||||
| <br><small><small><a href="/int">tomatenmharksite documentation</a></small></small> | ||||
| 
 | ||||
| <!-- t: end --> | ||||
| 
 | ||||
| <br><br><br><br><br><br> | ||||
| <footer><p><!-- t: safe domain --> and subdomains: <a href="<!-- t: raw ownerpage -->"><!-- t: safe ownername --></a></p></footer> | ||||
| 
 | ||||
| <!-- t: else --> | ||||
| 
 | ||||
| <h1>unconfigured tomatenmharksite server</h1> | ||||
| please fill out the required values in <code>int.html</code> | ||||
| and restart the tomatenmharksite server | ||||
| 
 | ||||
| <!-- t: end --> | ||||
| </body></html> | ||||
| @ -1,4 +1,14 @@ | ||||
| <!DOCTYPE html> | ||||
| 
 | ||||
| <!-- t: // fill out the following information --> | ||||
| <!-- t: set domain --> | ||||
| <!-- t: set sitename unconfigured tomatenmharksite --> | ||||
| <!-- t: set sitedecoration :) --> | ||||
| <!-- t: set ownername CHANGEME --> | ||||
| <!-- t: set ownerpage /info/me/ --> | ||||
| 
 | ||||
| <!-- t: // remove this to disable the "tomatenmharksite documentation" link --> | ||||
| <!-- t: set docslink true --> | ||||
| <html> | ||||
| <head> | ||||
| <meta charset="UTF-8"> | ||||
| @ -11,6 +21,8 @@ | ||||
| <div>documentation of how the tomatenmharksite server works for people who care or people who keep forgetting (me)</div> | ||||
| <div>go to <a href="/">index without</a> or <a href="/dbg">with debugging information</a></div> | ||||
| 
 | ||||
| <h3>index</h3> | ||||
| 
 | ||||
| <h4>links</h4> | ||||
| In the "running" section on the website, the ids may or may not be links. | ||||
| If a <code>/srv/tomatenmhark-slashinfo/*/index.html</code> entry exists, the link will point to that info site, | ||||
| @ -18,8 +30,22 @@ If only a <code>/srv/tomatenmhark-redirect/*</code> entry exists, the link will | ||||
| and if neither exist, there will be no link.<br> | ||||
| In the "everything else" section, every entry links to its info page, if there is one. | ||||
| 
 | ||||
| <h4>texts</h4> | ||||
| Each id has a status text. This can be a file's content or dynamic output, and will be cached for 3 seconds. | ||||
| If a status consists of multiple lines, the all but the first one are additional information. In this case, | ||||
| the first line will be <em>emphasized</em> and the additional lines will be displayed on hover. | ||||
| 
 | ||||
| <h4>customizing</h4> | ||||
| 
 | ||||
| The contents of the index are specified in a template file, <code>index.html</code>. | ||||
| This file is served as-is, but comments like <code><!-- t: ... --></code> are | ||||
| given special meaning. | ||||
| See <code>index.html</code> and <code>int.html</code> for usage examples. | ||||
| 
 | ||||
| <h3>files</h3> | ||||
| 
 | ||||
| <h4>/tmp/tomatenmhark-status-*</h4> | ||||
| For each file <code>/tmp/tomatenmhark-status-*</code>, an entry will be created on <a href="/">tomatenmhark.org</a>. | ||||
| For each file <code>/tmp/tomatenmhark-status-*</code>, an entry will be created on <a href="/"><!-- t: safe domain --></a>. | ||||
| This entry will have the id <code>*</code> and the file's contents will be shown after that. | ||||
| 
 | ||||
| <h4>/srv/tomatenmhark-dystatus/*</h4> | ||||
| @ -37,7 +63,7 @@ Each file in <code>/srv/tomatenmhark-redirect/</code> contains a port number or | ||||
| If the file contains a port number, <code>/filename/...</code> will be redirected to <code>samedomain:portnumber/...</code>.<br> | ||||
| If the file contains anything else, <code>/filename/...</code> will be redirected to <code>filecontent/...</code>.<br> | ||||
| In the port number case, <code>http</code> is used. Otherwise, the protocol must be included in the file. | ||||
| <code>%DOMAIN%</code> will be replaced with <code>tomatenmhark.org</code> or whatever domain was used in the request. | ||||
| <code>%DOMAIN%</code> will be replaced with <code><!-- t: safe domain --></code> or whatever domain was used in the request. | ||||
| To redirect to port <code>8000</code> using <code>https</code>, use <code>https://%DOMAIN%:8000</code> | ||||
| 
 | ||||
| </body> | ||||
							
								
								
									
										16
									
								
								src/data.rs
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								src/data.rs
									
									
									
									
									
								
							| @ -2,22 +2,32 @@ use std::time::Instant; | ||||
| 
 | ||||
| use tokio::sync::{Mutex, MutexGuard}; | ||||
| 
 | ||||
| use crate::status::Status; | ||||
| use crate::{status::Status, template::Template}; | ||||
| 
 | ||||
| pub struct Data { | ||||
|     pub int_html: String, | ||||
|     pub globals: Vec<String>, | ||||
|     pub index: Template, | ||||
|     pub status: Mutex<Status>, | ||||
|     pub status_updated: Mutex<(Instant, bool)>, | ||||
| } | ||||
| 
 | ||||
| #[allow(dead_code)] | ||||
| impl Data { | ||||
|     pub fn new_sync() -> Self { | ||||
|     pub fn new_sync(int_html: String, index: Template, globals: Vec<String>) -> Self { | ||||
|         Self { | ||||
|             int_html, | ||||
|             index, | ||||
|             globals, | ||||
|             status: Mutex::new(Status::query_sync(false)), | ||||
|             status_updated: Mutex::new((Instant::now(), false)), | ||||
|         } | ||||
|     } | ||||
|     pub async fn new_async() -> Self { | ||||
|     pub async fn new_async(int_html: String, index: Template, globals: Vec<String>) -> Self { | ||||
|         Self { | ||||
|             int_html, | ||||
|             index, | ||||
|             globals, | ||||
|             status: Mutex::new(Status::query_async(false).await), | ||||
|             status_updated: Mutex::new((Instant::now(), false)), | ||||
|         } | ||||
|  | ||||
							
								
								
									
										185
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										185
									
								
								src/main.rs
									
									
									
									
									
								
							| @ -1,8 +1,7 @@ | ||||
| mod data; | ||||
| mod redirecter; | ||||
| mod status; | ||||
| 
 | ||||
| use std::time::Duration; | ||||
| mod template; | ||||
| 
 | ||||
| use data::Data; | ||||
| use redirecter::Redirecter; | ||||
| @ -12,7 +11,8 @@ use rocket::{ | ||||
|     response::content::RawHtml, | ||||
|     routes, State, | ||||
| }; | ||||
| use tokio::time::Instant; | ||||
| use std::process::exit; | ||||
| use template::Template; | ||||
| 
 | ||||
| #[get("/")] | ||||
| async fn index(data: &State<Data>) -> RawHtml<String> { | ||||
| @ -25,161 +25,46 @@ async fn index_dbg(data: &State<Data>) -> RawHtml<String> { | ||||
| } | ||||
| 
 | ||||
| async fn index_gen(data: &State<Data>, dbg: bool) -> RawHtml<String> { | ||||
|     let mut o = "<!DOCTYPE html><html><head>".to_owned(); | ||||
|     o.push_str(r#"<meta charset="UTF-8">"#); | ||||
|     o.push_str(r#"<meta name="color-scheme" content="light dark">"#); | ||||
|     o.push_str(r#"<meta name="viewport" content="width=device-width, initial-scale=1">"#); | ||||
|     o.push_str("<title>tomatenmhark</title>"); | ||||
|     o.push_str( | ||||
|         "<style>
 | ||||
| a:link { | ||||
|   color: DarkCyan; | ||||
|   text-decoration: none; | ||||
|   font-weight: normal; | ||||
| } | ||||
| a:hover { | ||||
|   color: DarkCyan; | ||||
|   text-decoration: none; | ||||
|   font-weight: bold; | ||||
| } | ||||
| a:visited { | ||||
|   color: DarkCyan; | ||||
|   text-decoration: underline; | ||||
|   font-weight: normal; | ||||
| } | ||||
| span { | ||||
|   color: SeaGreen; | ||||
|   font-weight: normal; | ||||
| } | ||||
| .ohs { | ||||
|   display: none; | ||||
| } | ||||
| .oht:hover + .ohs, .ohs:hover { | ||||
|   display: block; | ||||
| } | ||||
| </style> | ||||
| ",
 | ||||
|     ); | ||||
|     o.push_str("</head><body>"); | ||||
|     o.push_str("<h2>:) tomatenmhark :)</h2>"); | ||||
|     let status = data.status_async(dbg).await; | ||||
|     let time_queried = Instant::now(); | ||||
|     fn push_elem_to_html( | ||||
|         id: &str, | ||||
|         info: bool, | ||||
|         redirect: bool, | ||||
|         status: &str, | ||||
|         additional: Option<&str>, | ||||
|         dur: Option<Duration>, | ||||
|         o: &mut String, | ||||
|         dbg: bool, | ||||
|     ) { | ||||
|         o.push_str("<li>"); | ||||
|         if info { | ||||
|             o.push_str(r#"<a href="/info/"#); | ||||
|             o.push_str(&html_escape::encode_double_quoted_attribute(&id)); | ||||
|             o.push_str(r#"">"#); | ||||
|         } else if redirect { | ||||
|             o.push_str(r#"<a href="/"#); | ||||
|             o.push_str(&html_escape::encode_double_quoted_attribute(&id)); | ||||
|             o.push_str(r#"">"#); | ||||
|         } else { | ||||
|             o.push_str("<span>"); | ||||
|         } | ||||
|         o.push_str(&html_escape::encode_text(&id)); | ||||
|         if info || redirect { | ||||
|             o.push_str("</a>"); | ||||
|         } else { | ||||
|             o.push_str("</span>"); | ||||
|         } | ||||
|         o.push_str(": "); | ||||
|         if additional.is_some() { | ||||
|             o.push_str(r#"<em class="oht">"#); | ||||
|         } | ||||
|         o.push_str(&html_escape::encode_text(&status)); | ||||
|         if let Some(additional) = additional { | ||||
|             o.push_str(r#"</em><div class="ohs">"#); | ||||
|             o.push_str( | ||||
|                 &html_escape::encode_text(additional) | ||||
|                     .replace('\r', "") | ||||
|                     .replace('\n', "<br>"), | ||||
|             ); | ||||
|             o.push_str("</div>"); | ||||
|         } | ||||
|         if dbg { | ||||
|             if let Some(dur) = dur { | ||||
|                 o.push_str(&format!(" | {}ms", dur.as_millis())); | ||||
|             } | ||||
|         } | ||||
|         o.push_str("</li>"); | ||||
|     } | ||||
|     if !status.0.is_empty() { | ||||
|         o.push_str("<h3>things running on the server</h3>"); | ||||
|         o.push_str("<ul>"); | ||||
|         for (id, (info, redirect, status, additional, dur)) in status.0.iter() { | ||||
|             push_elem_to_html( | ||||
|                 id, | ||||
|                 *info, | ||||
|                 *redirect, | ||||
|                 status, | ||||
|                 additional.as_ref().map(|v| v.as_str()), | ||||
|                 *dur, | ||||
|                 &mut o, | ||||
|                 dbg, | ||||
|             ); | ||||
|         } | ||||
|         o.push_str("</ul>"); | ||||
|     } | ||||
|     if !status.1.is_empty() { | ||||
|         o.push_str("<h3>everything else</h3>"); | ||||
|         o.push_str("<ul>"); | ||||
|         for (id, (info, redirect, status, additional)) in status.1.iter() { | ||||
|             push_elem_to_html( | ||||
|                 id, | ||||
|                 *info, | ||||
|                 *redirect, | ||||
|                 status, | ||||
|                 additional.as_ref().map(|v| v.as_str()), | ||||
|                 None, | ||||
|                 &mut o, | ||||
|                 dbg, | ||||
|             ); | ||||
|         } | ||||
|         o.push_str("</ul>"); | ||||
|     } | ||||
|     o.push_str( | ||||
|         r#"<br><small><small><a href="/int">tomatenmharksite documentation</a></small></small>"#, | ||||
|     ); | ||||
|     o.push_str(r#"<br><br><br><br><br><br><footer><p>tomatenmhark.org und subdomains: <a href="/info/me">Mark</a>"#); | ||||
|     if dbg { | ||||
|         o.push_str(r#"<br><small><small>"#); | ||||
|         let time_pagegen = Instant::now(); | ||||
|         o.push_str(&format!( | ||||
|             "{}/{}ms querying + {:.2}ms pagegen", | ||||
|             status | ||||
|                 .0 | ||||
|                 .values() | ||||
|                 .filter_map(|v| v.4) | ||||
|                 .sum::<Duration>() | ||||
|                 .as_millis(), | ||||
|             status.2.as_millis(), | ||||
|             (time_pagegen - time_queried).as_micros() as f32 / 1000.0, | ||||
|         )); | ||||
|         o.push_str(r#"</small></small>"#); | ||||
|     } | ||||
|     o.push_str(r#"</p></footer>"#); | ||||
|     o.push_str("</body></html>"); | ||||
|     RawHtml(o) | ||||
|     return RawHtml(data.index.gen(&status, dbg, data.globals.clone())); | ||||
| } | ||||
| 
 | ||||
| #[get("/int")] | ||||
| async fn int() -> RawHtml<&'static str> { | ||||
|     RawHtml(include_str!("int.html")) | ||||
| async fn int(data: &State<Data>) -> RawHtml<&str> { | ||||
|     RawHtml(&data.int_html) | ||||
| } | ||||
| 
 | ||||
| #[rocket::launch] | ||||
| fn rocket() -> _ { | ||||
|     let data = Data::new_sync(); | ||||
|     let int_file = match std::fs::read_to_string("int.html") { | ||||
|         Ok(v) => v, | ||||
|         Err(e) => { | ||||
|             eprintln!("Could not read int.html: {e}"); | ||||
|             exit(1); | ||||
|         } | ||||
|     }; | ||||
|     let index_file = match std::fs::read_to_string("index.html") { | ||||
|         Ok(v) => v, | ||||
|         Err(e) => { | ||||
|             eprintln!("Could not read index.html: {e}"); | ||||
|             exit(1); | ||||
|         } | ||||
|     }; | ||||
|     let (int_html, variables, globals) = match Template::parse(&int_file, None) { | ||||
|         Ok(int_template) => int_template.gen_int(), | ||||
|         Err(e) => { | ||||
|             eprintln!("Could not parse int.html: {e}"); | ||||
|             exit(1); | ||||
|         } | ||||
|     }; | ||||
|     let index = match Template::parse(&index_file, Some(variables)) { | ||||
|         Ok(index_template) => index_template, | ||||
|         Err(e) => { | ||||
|             eprintln!("Could not parse index.html: {e}"); | ||||
|             exit(1); | ||||
|         } | ||||
|     }; | ||||
|     let data = Data::new_sync(int_html, index, globals); | ||||
|     rocket::build() | ||||
|         .manage(data) | ||||
|         .mount("/", routes![index, index_dbg, int]) | ||||
|  | ||||
| @ -8,11 +8,12 @@ const REDIRECT: &'static str = "/srv/tomatenmhark-redirect/"; | ||||
| pub struct Status( | ||||
|     pub BTreeMap<String, (bool, bool, String, Option<String>, Option<Duration>)>, | ||||
|     pub BTreeMap<String, (bool, bool, String, Option<String>)>, | ||||
|     pub Duration, | ||||
| ); | ||||
| impl Status { | ||||
|     pub fn empty() -> Self { | ||||
|         Self(Default::default(), Default::default()) | ||||
|     } | ||||
|     pub fn query_sync(dbg: bool) -> Self { | ||||
|         let start = Instant::now(); | ||||
|         let mut map = BTreeMap::new(); | ||||
|         query_status_sync( | ||||
|             |k, e, r, v, dur| { | ||||
| @ -75,10 +76,9 @@ impl Status { | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         Self(map, rest, start.elapsed()) | ||||
|         Self(map, rest) | ||||
|     } | ||||
|     pub async fn query_async(dbg: bool) -> Self { | ||||
|         let start = Instant::now(); | ||||
|         let mut map = BTreeMap::new(); | ||||
|         query_status_async( | ||||
|             |k, e, r, v, dur| { | ||||
| @ -137,7 +137,7 @@ impl Status { | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         Self(map, rest, start.elapsed()) | ||||
|         Self(map, rest) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										378
									
								
								src/template.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										378
									
								
								src/template.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,378 @@ | ||||
| use std::{collections::HashMap, fmt::Display}; | ||||
| 
 | ||||
| use crate::status::Status; | ||||
| 
 | ||||
| pub struct Template(Templates, TemplateInfo); | ||||
| 
 | ||||
| pub struct Templates(Vec<TemplateType>); | ||||
| 
 | ||||
| pub enum TemplateType { | ||||
|     Html(String), | ||||
|     Raw(usize), | ||||
|     Safe(usize), | ||||
|     Set(usize, String), | ||||
|     Concat(usize, usize), | ||||
|     If(usize, Templates, Option<Templates>), | ||||
|     For(Vec<TemplateLoop>, Templates), | ||||
| } | ||||
| 
 | ||||
| pub enum TemplateLoop { | ||||
|     Running, | ||||
|     Static, | ||||
|     Lines(usize), | ||||
| } | ||||
| 
 | ||||
| const TEMPLATE_FALSE: (usize, &str) = (0, "false"); | ||||
| const TEMPLATE_TRUE: (usize, &str) = (1, "true"); | ||||
| const TEMPLATE_DEBUG: (usize, &str) = (2, "debug"); | ||||
| const TEMPLATE_RUNNING: (usize, &str) = (3, "running"); | ||||
| const TEMPLATE_STATIC: (usize, &str) = (4, "static"); | ||||
| const TEMPLATE_LOOP_ELEM_ID: (usize, &str) = (5, "id"); | ||||
| const TEMPLATE_LOOP_ELEM_STATUS: (usize, &str) = (6, "status"); | ||||
| const TEMPLATE_LOOP_ELEM_ADDITIONAL: (usize, &str) = (7, "additional"); | ||||
| const TEMPLATE_LOOP_ELEM_INFO: (usize, &str) = (8, "info"); | ||||
| const TEMPLATE_LOOP_ELEM_REDIRECT: (usize, &str) = (9, "redirect"); | ||||
| const TEMPLATE_LOOP_ELEM_DURATION: (usize, &str) = (10, "duration"); | ||||
| const TEMPLATE_LINE: (usize, &str) = (11, "line"); | ||||
| const TEMPLATE_DEFAULT_VARS: [(usize, &str); 12] = [ | ||||
|     TEMPLATE_FALSE, | ||||
|     TEMPLATE_TRUE, | ||||
|     TEMPLATE_DEBUG, | ||||
|     TEMPLATE_RUNNING, | ||||
|     TEMPLATE_STATIC, | ||||
|     TEMPLATE_LOOP_ELEM_ID, | ||||
|     TEMPLATE_LOOP_ELEM_STATUS, | ||||
|     TEMPLATE_LOOP_ELEM_ADDITIONAL, | ||||
|     TEMPLATE_LOOP_ELEM_INFO, | ||||
|     TEMPLATE_LOOP_ELEM_REDIRECT, | ||||
|     TEMPLATE_LOOP_ELEM_DURATION, | ||||
|     TEMPLATE_LINE, | ||||
| ]; | ||||
| 
 | ||||
| impl Template { | ||||
|     pub fn gen_int(self) -> (String, HashMap<String, usize>, Vec<String>) { | ||||
|         let mut vars = Vec::with_capacity(self.1.variables.len()); | ||||
|         // make space for all variables
 | ||||
|         vars.resize_with(self.1.variables.len(), String::new); | ||||
|         vars[TEMPLATE_TRUE.0] = "true".to_owned(); | ||||
| 
 | ||||
|         // generate html based on the template
 | ||||
|         let mut out = String::new(); | ||||
|         self.0.gen(&Status::empty(), false, &mut vars, &mut out); | ||||
|         (out, self.1.variables, vars) | ||||
|     } | ||||
|     pub fn gen(&self, status: &Status, debug: bool, mut vars: Vec<String>) -> String { | ||||
|         // setup variables
 | ||||
|         if self.1.variables.len() > vars.len() { | ||||
|             vars.resize_with(self.1.variables.len(), String::new); | ||||
|         } | ||||
|         vars[TEMPLATE_DEBUG.0] = if debug { | ||||
|             "dbg".to_owned() | ||||
|         } else { | ||||
|             String::new() | ||||
|         }; | ||||
|         vars[TEMPLATE_RUNNING.0] = if status.0.is_empty() { | ||||
|             String::new() | ||||
|         } else { | ||||
|             status.0.len().to_string() | ||||
|         }; | ||||
|         vars[TEMPLATE_STATIC.0] = if status.1.is_empty() { | ||||
|             String::new() | ||||
|         } else { | ||||
|             status.1.len().to_string() | ||||
|         }; | ||||
| 
 | ||||
|         // generate html based on the template
 | ||||
|         let mut out = String::new(); | ||||
|         self.0.gen(status, debug, &mut vars, &mut out); | ||||
|         out | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub struct TemplateInfo { | ||||
|     variables: HashMap<String, usize>, | ||||
|     last_end_was_else: Option<bool>, | ||||
| } | ||||
| 
 | ||||
| const COMMENT_START: &'static str = "<!-- t: "; | ||||
| const COMMENT_END: &'static str = "-->"; | ||||
| impl Template { | ||||
|     pub fn parse( | ||||
|         mut src: &str, | ||||
|         variables: Option<HashMap<String, usize>>, | ||||
|     ) -> Result<Self, TemplateParseError> { | ||||
|         let mut info = TemplateInfo { | ||||
|             variables: variables.unwrap_or_else(|| { | ||||
|                 TEMPLATE_DEFAULT_VARS | ||||
|                     .iter() | ||||
|                     .map(|(i, name)| ((*name).to_owned(), *i)) | ||||
|                     .collect() | ||||
|             }), | ||||
|             last_end_was_else: None, | ||||
|         }; | ||||
|         let templates = Templates::parse(&mut src, &mut info)?; | ||||
|         if !src.trim().is_empty() { | ||||
|             return Err(TemplateParseError::DidNotReachEOF(src.trim().to_owned())); | ||||
|         } | ||||
|         Ok(Self(templates, info)) | ||||
|     } | ||||
| } | ||||
| impl Templates { | ||||
|     fn parse(src: &mut &str, info: &mut TemplateInfo) -> Result<Self, TemplateParseError> { | ||||
|         let mut out = vec![]; | ||||
|         'parsing: loop { | ||||
|             let comment_start = src.find(COMMENT_START); | ||||
|             let mut pre_comment = comment_start.map(|i| &src[..i]).unwrap_or(src); | ||||
|             if comment_start.is_some() { | ||||
|                 pre_comment = pre_comment | ||||
|                     .strip_suffix('\n') | ||||
|                     .unwrap_or(pre_comment) | ||||
|                     .trim_end_matches('\r'); | ||||
|             } | ||||
|             out.push(TemplateType::Html(pre_comment.to_owned())); | ||||
|             *src = &src[pre_comment.len()..]; | ||||
| 
 | ||||
|             if comment_start.is_some() { | ||||
|                 *src = &src[COMMENT_START.len()..]; | ||||
| 
 | ||||
|                 if let Some(comment_end) = src.find(COMMENT_END) { | ||||
|                     // extract comment content
 | ||||
|                     let comment_og = src[..comment_end].trim(); | ||||
|                     *src = &src[comment_end + COMMENT_END.len()..].trim_start_matches('\r'); | ||||
|                     *src = src.strip_prefix('\n').unwrap_or(src); | ||||
|                     // do template things
 | ||||
|                     let comment = comment_og.to_lowercase(); | ||||
|                     let mut comment = comment.split(char::is_whitespace); | ||||
|                     match comment.next().unwrap_or("") { | ||||
|                         "//" | "#" => {} | ||||
|                         "end" => { | ||||
|                             break 'parsing; | ||||
|                         } | ||||
|                         "else" if info.last_end_was_else == Some(false) => { | ||||
|                             info.last_end_was_else = Some(true); | ||||
|                             break 'parsing; | ||||
|                         } | ||||
|                         "raw" => out.push(TemplateType::Raw( | ||||
|                             info.var( | ||||
|                                 comment | ||||
|                                     .next() | ||||
|                                     .ok_or(TemplateParseError::MissingVariable("raw"))?, | ||||
|                             ), | ||||
|                         )), | ||||
|                         "safe" => out.push(TemplateType::Safe( | ||||
|                             info.var( | ||||
|                                 comment | ||||
|                                     .next() | ||||
|                                     .ok_or(TemplateParseError::MissingVariable("safe"))?, | ||||
|                             ), | ||||
|                         )), | ||||
|                         "set" => { | ||||
|                             let var_name = comment | ||||
|                                 .next() | ||||
|                                 .ok_or(TemplateParseError::MissingVariable("set"))?; | ||||
|                             let value = comment_og[3..].trim_start()[var_name.len()..].trim_start(); | ||||
|                             out.push(TemplateType::Set(info.var(var_name), value.to_owned())); | ||||
|                         } | ||||
|                         "concat" => { | ||||
|                             out.push(TemplateType::Concat( | ||||
|                                 info.var( | ||||
|                                     comment | ||||
|                                         .next() | ||||
|                                         .ok_or(TemplateParseError::MissingVariable("concat"))?, | ||||
|                                 ), | ||||
|                                 info.var( | ||||
|                                     comment | ||||
|                                         .next() | ||||
|                                         .ok_or(TemplateParseError::MissingVariable("concat"))?, | ||||
|                                 ), | ||||
|                             )); | ||||
|                         } | ||||
|                         "if" => { | ||||
|                             let prev_lewe = info.last_end_was_else; | ||||
|                             info.last_end_was_else = Some(false); | ||||
|                             out.push(TemplateType::If( | ||||
|                                 info.var( | ||||
|                                     comment | ||||
|                                         .next() | ||||
|                                         .ok_or(TemplateParseError::MissingVariable("if"))?, | ||||
|                                 ), | ||||
|                                 Templates::parse(src, info)?, | ||||
|                                 if info.last_end_was_else.is_some_and(|v| v) { | ||||
|                                     Some(Templates::parse(src, info)?) | ||||
|                                 } else { | ||||
|                                     None | ||||
|                                 }, | ||||
|                             )); | ||||
|                             info.last_end_was_else = prev_lewe; | ||||
|                         } | ||||
|                         "for" => { | ||||
|                             let mut sources = Vec::new(); | ||||
|                             while let Some(source_name) = comment.next() { | ||||
|                                 sources.push(match source_name { | ||||
|                                     "running" => TemplateLoop::Running, | ||||
|                                     "static" => TemplateLoop::Static, | ||||
|                                     "line" => { | ||||
|                                         TemplateLoop::Lines(info.var(comment.next().ok_or( | ||||
|                                             TemplateParseError::MissingVariable("for line"), | ||||
|                                         )?)) | ||||
|                                     } | ||||
|                                     s => { | ||||
|                                         return Err(TemplateParseError::UnknownForSource( | ||||
|                                             s.to_owned(), | ||||
|                                         )) | ||||
|                                     } | ||||
|                                 }); | ||||
|                             } | ||||
|                             if sources.is_empty() { | ||||
|                                 return Err(TemplateParseError::UnknownForSource(String::new())); | ||||
|                             } | ||||
|                             out.push(TemplateType::For(sources, Self::parse(src, info)?)); | ||||
|                         } | ||||
|                         unknown => { | ||||
|                             return Err(TemplateParseError::InvalidTemplateKeyword( | ||||
|                                 unknown.to_owned(), | ||||
|                             )) | ||||
|                         } | ||||
|                     } | ||||
|                 } else { | ||||
|                     return Err(TemplateParseError::UnclosedComment); | ||||
|                 } | ||||
|             } else { | ||||
|                 break 'parsing; | ||||
|             } | ||||
|         } | ||||
|         Ok(Self(out)) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub enum TemplateParseError { | ||||
|     UnclosedComment, | ||||
|     InvalidTemplateKeyword(String), | ||||
|     UnknownForSource(String), | ||||
|     DidNotReachEOF(String), | ||||
|     MissingVariable(&'static str), | ||||
| } | ||||
| impl Display for TemplateParseError { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         match self { | ||||
|             Self::UnclosedComment => write!(f, "There is an unclosed comment. Make sure all `<!-- t: ` comments are closed by a `-->`"), | ||||
|             Self::InvalidTemplateKeyword(kw) => write!(f, "Invalid keyword `{kw}` after `<!-- t: `"), | ||||
|             Self::UnknownForSource(s) => write!(f, "For loop can only be used with one or more of `running`, `static`, or `line varname`, not `{s}`"), | ||||
|             Self::DidNotReachEOF(s) => write!(f, "Stopped parsing before the end of the file. Remaining, unparsed text: {s}"), | ||||
|             Self::MissingVariable(t) => write!(f, "Missing a variable name for `<!-- t: {t}`."), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl TemplateInfo { | ||||
|     fn var(&mut self, v: &str) -> usize { | ||||
|         if let Some(i) = self.variables.get(v) { | ||||
|             *i | ||||
|         } else { | ||||
|             let i = self.variables.len(); | ||||
|             self.variables.insert(v.to_owned(), i); | ||||
|             i | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Templates { | ||||
|     pub fn gen(&self, status: &Status, debug: bool, vars: &mut Vec<String>, out: &mut String) { | ||||
|         for template in &self.0 { | ||||
|             template.gen(status, debug, vars, out); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| impl TemplateType { | ||||
|     pub fn gen(&self, status: &Status, debug: bool, vars: &mut Vec<String>, out: &mut String) { | ||||
|         match self { | ||||
|             Self::Html(html) => out.push_str(html), | ||||
|             Self::Raw(v) => out.push_str(&vars[*v]), | ||||
|             Self::Safe(v) => { | ||||
|                 html_escape::encode_safe_to_string(&vars[*v], out); | ||||
|             } | ||||
|             Self::Set(v, val) => vars[*v] = val.clone(), | ||||
|             Self::Concat(v, a) => { | ||||
|                 if *v == *a { | ||||
|                     let a = &mut vars[*v]; | ||||
|                     a.reserve(a.len()); | ||||
|                     let mut i = 0; | ||||
|                     while let Some(ch) = a[i..].chars().next() { | ||||
|                         i += ch.len_utf8(); | ||||
|                         a.push(ch); | ||||
|                     } | ||||
|                 } else { | ||||
|                     let a_temp = std::mem::replace(&mut vars[*a], String::new()); | ||||
|                     vars[*v].push_str(&a_temp); | ||||
|                     vars[*a] = a_temp; | ||||
|                 } | ||||
|             } | ||||
|             Self::If(v, template, else_template) => { | ||||
|                 if !vars[*v].trim().is_empty() { | ||||
|                     template.gen(status, debug, vars, out); | ||||
|                 } else if let Some(else_template) = else_template { | ||||
|                     else_template.gen(status, debug, vars, out); | ||||
|                 } | ||||
|             } | ||||
|             Self::For(src, template) => { | ||||
|                 for src in src { | ||||
|                     match src { | ||||
|                         TemplateLoop::Running => { | ||||
|                             for (id, (info, redirect, main_status, additional, dur)) in | ||||
|                                 status.0.iter() | ||||
|                             { | ||||
|                                 vars[TEMPLATE_LOOP_ELEM_ID.0] = id.clone(); | ||||
|                                 vars[TEMPLATE_LOOP_ELEM_STATUS.0] = main_status.clone(); | ||||
|                                 vars[TEMPLATE_LOOP_ELEM_ADDITIONAL.0] = | ||||
|                                     additional.clone().unwrap_or_else(String::new); | ||||
|                                 vars[TEMPLATE_LOOP_ELEM_DURATION.0] = if let Some(dur) = dur { | ||||
|                                     format!("{}", dur.as_millis()) | ||||
|                                 } else { | ||||
|                                     String::new() | ||||
|                                 }; | ||||
|                                 vars[TEMPLATE_LOOP_ELEM_INFO.0] = if *info { | ||||
|                                     html_escape::encode_double_quoted_attribute(&id).into_owned() | ||||
|                                 } else { | ||||
|                                     String::new() | ||||
|                                 }; | ||||
|                                 vars[TEMPLATE_LOOP_ELEM_REDIRECT.0] = if *redirect { | ||||
|                                     html_escape::encode_double_quoted_attribute(&id).into_owned() | ||||
|                                 } else { | ||||
|                                     String::new() | ||||
|                                 }; | ||||
|                                 template.gen(status, debug, vars, out); | ||||
|                             } | ||||
|                         } | ||||
|                         TemplateLoop::Static => { | ||||
|                             for (id, (info, redirect, main_status, additional)) in status.1.iter() { | ||||
|                                 vars[TEMPLATE_LOOP_ELEM_ID.0] = id.clone(); | ||||
|                                 vars[TEMPLATE_LOOP_ELEM_STATUS.0] = main_status.clone(); | ||||
|                                 vars[TEMPLATE_LOOP_ELEM_ADDITIONAL.0] = | ||||
|                                     additional.clone().unwrap_or_else(String::new); | ||||
|                                 vars[TEMPLATE_LOOP_ELEM_DURATION.0] = String::new(); | ||||
|                                 vars[TEMPLATE_LOOP_ELEM_INFO.0] = if *info { | ||||
|                                     html_escape::encode_double_quoted_attribute(&id).into_owned() | ||||
|                                 } else { | ||||
|                                     String::new() | ||||
|                                 }; | ||||
|                                 vars[TEMPLATE_LOOP_ELEM_REDIRECT.0] = if *redirect { | ||||
|                                     html_escape::encode_double_quoted_attribute(&id).into_owned() | ||||
|                                 } else { | ||||
|                                     String::new() | ||||
|                                 }; | ||||
|                                 template.gen(status, debug, vars, out); | ||||
|                             } | ||||
|                         } | ||||
|                         TemplateLoop::Lines(v) => { | ||||
|                             for line in vars[*v].lines().map(|v| v.to_owned()).collect::<Vec<_>>() { | ||||
|                                 vars[TEMPLATE_LINE.0] = line; | ||||
|                                 template.gen(status, debug, vars, out); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Mark
						Mark