use template

This commit is contained in:
Mark 2025-07-21 13:55:21 +02:00
parent 80a86e9f8a
commit 19f51c5732
6 changed files with 632 additions and 160 deletions

173
index.html Normal file
View 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>

View File

@ -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>&lt;!-- t: ... --&gt;</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>

View File

@ -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)),
} }

View File

@ -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])

View File

@ -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
View 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);
}
}
}
}
}
}
}
}