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> | <!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> | <html> | ||||||
| <head> | <head> | ||||||
| <meta charset="UTF-8"> | <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>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> | <div>go to <a href="/">index without</a> or <a href="/dbg">with debugging information</a></div> | ||||||
| 
 | 
 | ||||||
|  | <h3>index</h3> | ||||||
|  | 
 | ||||||
| <h4>links</h4> | <h4>links</h4> | ||||||
| In the "running" section on the website, the ids may or may not be links. | 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, | 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> | 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. | 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> | <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. | This entry will have the id <code>*</code> and the file's contents will be shown after that. | ||||||
| 
 | 
 | ||||||
| <h4>/srv/tomatenmhark-dystatus/*</h4> | <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 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> | 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. | 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> | To redirect to port <code>8000</code> using <code>https</code>, use <code>https://%DOMAIN%:8000</code> | ||||||
| 
 | 
 | ||||||
| </body> | </body> | ||||||
							
								
								
									
										16
									
								
								src/data.rs
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								src/data.rs
									
									
									
									
									
								
							| @ -2,22 +2,32 @@ use std::time::Instant; | |||||||
| 
 | 
 | ||||||
| use tokio::sync::{Mutex, MutexGuard}; | use tokio::sync::{Mutex, MutexGuard}; | ||||||
| 
 | 
 | ||||||
| use crate::status::Status; | use crate::{status::Status, template::Template}; | ||||||
| 
 | 
 | ||||||
| pub struct Data { | pub struct Data { | ||||||
|  |     pub int_html: String, | ||||||
|  |     pub globals: Vec<String>, | ||||||
|  |     pub index: Template, | ||||||
|     pub status: Mutex<Status>, |     pub status: Mutex<Status>, | ||||||
|     pub status_updated: Mutex<(Instant, bool)>, |     pub status_updated: Mutex<(Instant, bool)>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[allow(dead_code)] | ||||||
| impl Data { | impl Data { | ||||||
|     pub fn new_sync() -> Self { |     pub fn new_sync(int_html: String, index: Template, globals: Vec<String>) -> Self { | ||||||
|         Self { |         Self { | ||||||
|  |             int_html, | ||||||
|  |             index, | ||||||
|  |             globals, | ||||||
|             status: Mutex::new(Status::query_sync(false)), |             status: Mutex::new(Status::query_sync(false)), | ||||||
|             status_updated: Mutex::new((Instant::now(), 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 { |         Self { | ||||||
|  |             int_html, | ||||||
|  |             index, | ||||||
|  |             globals, | ||||||
|             status: Mutex::new(Status::query_async(false).await), |             status: Mutex::new(Status::query_async(false).await), | ||||||
|             status_updated: Mutex::new((Instant::now(), false)), |             status_updated: Mutex::new((Instant::now(), false)), | ||||||
|         } |         } | ||||||
|  | |||||||
							
								
								
									
										185
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										185
									
								
								src/main.rs
									
									
									
									
									
								
							| @ -1,8 +1,7 @@ | |||||||
| mod data; | mod data; | ||||||
| mod redirecter; | mod redirecter; | ||||||
| mod status; | mod status; | ||||||
| 
 | mod template; | ||||||
| use std::time::Duration; |  | ||||||
| 
 | 
 | ||||||
| use data::Data; | use data::Data; | ||||||
| use redirecter::Redirecter; | use redirecter::Redirecter; | ||||||
| @ -12,7 +11,8 @@ use rocket::{ | |||||||
|     response::content::RawHtml, |     response::content::RawHtml, | ||||||
|     routes, State, |     routes, State, | ||||||
| }; | }; | ||||||
| use tokio::time::Instant; | use std::process::exit; | ||||||
|  | use template::Template; | ||||||
| 
 | 
 | ||||||
| #[get("/")] | #[get("/")] | ||||||
| async fn index(data: &State<Data>) -> RawHtml<String> { | 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> { | 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 status = data.status_async(dbg).await; | ||||||
|     let time_queried = Instant::now(); |     return RawHtml(data.index.gen(&status, dbg, data.globals.clone())); | ||||||
|     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) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[get("/int")] | #[get("/int")] | ||||||
| async fn int() -> RawHtml<&'static str> { | async fn int(data: &State<Data>) -> RawHtml<&str> { | ||||||
|     RawHtml(include_str!("int.html")) |     RawHtml(&data.int_html) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[rocket::launch] | #[rocket::launch] | ||||||
| fn rocket() -> _ { | 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() |     rocket::build() | ||||||
|         .manage(data) |         .manage(data) | ||||||
|         .mount("/", routes![index, index_dbg, int]) |         .mount("/", routes![index, index_dbg, int]) | ||||||
|  | |||||||
| @ -8,11 +8,12 @@ const REDIRECT: &'static str = "/srv/tomatenmhark-redirect/"; | |||||||
| pub struct Status( | pub struct Status( | ||||||
|     pub BTreeMap<String, (bool, bool, String, Option<String>, Option<Duration>)>, |     pub BTreeMap<String, (bool, bool, String, Option<String>, Option<Duration>)>, | ||||||
|     pub BTreeMap<String, (bool, bool, String, Option<String>)>, |     pub BTreeMap<String, (bool, bool, String, Option<String>)>, | ||||||
|     pub Duration, |  | ||||||
| ); | ); | ||||||
| impl Status { | impl Status { | ||||||
|  |     pub fn empty() -> Self { | ||||||
|  |         Self(Default::default(), Default::default()) | ||||||
|  |     } | ||||||
|     pub fn query_sync(dbg: bool) -> Self { |     pub fn query_sync(dbg: bool) -> Self { | ||||||
|         let start = Instant::now(); |  | ||||||
|         let mut map = BTreeMap::new(); |         let mut map = BTreeMap::new(); | ||||||
|         query_status_sync( |         query_status_sync( | ||||||
|             |k, e, r, v, dur| { |             |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 { |     pub async fn query_async(dbg: bool) -> Self { | ||||||
|         let start = Instant::now(); |  | ||||||
|         let mut map = BTreeMap::new(); |         let mut map = BTreeMap::new(); | ||||||
|         query_status_async( |         query_status_async( | ||||||
|             |k, e, r, v, dur| { |             |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