notify: switch to file-based templating system
Instead of passing the template strings for subject and body when constructing a notification, we pass only the name of a template. When rendering the template, the name of the template is used to find corresponding template files. For PVE, they are located at /usr/share/proxmox-ve/templates/default. The `default` part is the 'template namespace', which is a preparation for user-customizable and/or translatable notifications. Previously, the same template string was used to render HTML and plaintext notifications. This was achieved by providing some template helpers that 'abstract away' HTML/plaintext formatting. However, in hindsight this turned out to be pretty finicky. Since the current changes lay the foundations for user-customizable notification templates, I ripped these abstractions out. Now there are simply two templates, one for plaintext, one for HTML. Signed-off-by: Lukas Wagner <l.wagner@proxmox.com> Tested-by: Folke Gleumes <f.gleumes@proxmox.com> Reviewed-by: Fiona Ebner <f.ebner@proxmox.com>
This commit is contained in:
parent
42fb9ed26b
commit
1516cc26d2
@ -1,63 +0,0 @@
|
||||
use proxmox_notify::renderer::{render_template, TemplateRenderer};
|
||||
use proxmox_notify::Error;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
const TEMPLATE: &str = r#"
|
||||
{{ heading-1 "Backup Report"}}
|
||||
A backup job on host {{host}} was run.
|
||||
|
||||
{{ heading-2 "Guests"}}
|
||||
{{ table table }}
|
||||
The total size of all backups is {{human-bytes total-size}}.
|
||||
|
||||
The backup job took {{duration total-time}}.
|
||||
|
||||
{{ heading-2 "Logs"}}
|
||||
{{ verbatim-monospaced logs}}
|
||||
|
||||
{{ heading-2 "Objects"}}
|
||||
{{ object table }}
|
||||
"#;
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
let properties = json!({
|
||||
"host": "pali",
|
||||
"logs": "100: starting backup\n100: backup failed",
|
||||
"total-size": 1024 * 1024 + 2048 * 1024,
|
||||
"total-time": 100,
|
||||
"table": {
|
||||
"schema": {
|
||||
"columns": [
|
||||
{
|
||||
"label": "VMID",
|
||||
"id": "vmid"
|
||||
},
|
||||
{
|
||||
"label": "Size",
|
||||
"id": "size",
|
||||
"renderer": "human-bytes"
|
||||
}
|
||||
],
|
||||
},
|
||||
"data" : [
|
||||
{
|
||||
"vmid": 1001,
|
||||
"size": "1048576"
|
||||
},
|
||||
{
|
||||
"vmid": 1002,
|
||||
"size": 2048 * 1024,
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
let output = render_template(TemplateRenderer::Html, TEMPLATE, &properties)?;
|
||||
println!("{output}");
|
||||
|
||||
let output = render_template(TemplateRenderer::Plaintext, TEMPLATE, &properties)?;
|
||||
println!("{output}");
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
#[cfg(any(feature = "pve-context", feature = "pbs-context"))]
|
||||
pub mod common;
|
||||
#[cfg(feature = "pbs-context")]
|
||||
@ -20,8 +22,14 @@ pub trait Context: Send + Sync + Debug {
|
||||
fn default_sendmail_from(&self) -> String;
|
||||
/// Proxy configuration for the current node
|
||||
fn http_proxy_config(&self) -> Option<String>;
|
||||
// Return default config for built-in targets/matchers.
|
||||
/// Return default config for built-in targets/matchers.
|
||||
fn default_config(&self) -> &'static str;
|
||||
/// Lookup a template in a certain (optional) namespace
|
||||
fn lookup_template(
|
||||
&self,
|
||||
filename: &str,
|
||||
namespace: Option<&str>,
|
||||
) -> Result<Option<String>, Error>;
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
|
@ -1,9 +1,11 @@
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
|
||||
use proxmox_schema::{ObjectSchema, Schema, StringSchema};
|
||||
use proxmox_section_config::{SectionConfig, SectionConfigPlugin};
|
||||
|
||||
use crate::context::{common, Context};
|
||||
use crate::Error;
|
||||
|
||||
const PBS_USER_CFG_FILENAME: &str = "/etc/proxmox-backup/user.cfg";
|
||||
const PBS_NODE_CFG_FILENAME: &str = "/etc/proxmox-backup/node.cfg";
|
||||
@ -98,6 +100,20 @@ impl Context for PBSContext {
|
||||
fn default_config(&self) -> &'static str {
|
||||
return DEFAULT_CONFIG;
|
||||
}
|
||||
|
||||
fn lookup_template(
|
||||
&self,
|
||||
filename: &str,
|
||||
namespace: Option<&str>,
|
||||
) -> Result<Option<String>, Error> {
|
||||
let path = Path::new("/usr/share/proxmox-backup/templates")
|
||||
.join(namespace.unwrap_or("default"))
|
||||
.join(filename);
|
||||
|
||||
let template_string = proxmox_sys::fs::file_read_optional_string(path)
|
||||
.map_err(|err| Error::Generic(format!("could not load template: {err}")))?;
|
||||
Ok(template_string)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -1,4 +1,6 @@
|
||||
use crate::context::{common, Context};
|
||||
use crate::Error;
|
||||
use std::path::Path;
|
||||
|
||||
fn lookup_mail_address(content: &str, user: &str) -> Option<String> {
|
||||
common::normalize_for_return(content.lines().find_map(|line| {
|
||||
@ -51,6 +53,19 @@ impl Context for PVEContext {
|
||||
fn default_config(&self) -> &'static str {
|
||||
return DEFAULT_CONFIG;
|
||||
}
|
||||
|
||||
fn lookup_template(
|
||||
&self,
|
||||
filename: &str,
|
||||
namespace: Option<&str>,
|
||||
) -> Result<Option<String>, Error> {
|
||||
let path = Path::new("/usr/share/pve-manager/templates")
|
||||
.join(namespace.unwrap_or("default"))
|
||||
.join(filename);
|
||||
let template_string = proxmox_sys::fs::file_read_optional_string(path)
|
||||
.map_err(|err| Error::Generic(format!("could not load template: {err}")))?;
|
||||
Ok(template_string)
|
||||
}
|
||||
}
|
||||
|
||||
pub static PVE_CONTEXT: PVEContext = PVEContext;
|
||||
|
@ -1,4 +1,5 @@
|
||||
use crate::context::Context;
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TestContext;
|
||||
@ -23,4 +24,12 @@ impl Context for TestContext {
|
||||
fn default_config(&self) -> &'static str {
|
||||
""
|
||||
}
|
||||
|
||||
fn lookup_template(
|
||||
&self,
|
||||
_filename: &str,
|
||||
_namespace: Option<&str>,
|
||||
) -> Result<Option<String>, Error> {
|
||||
Ok(Some(String::new()))
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ use proxmox_schema::api_types::COMMENT_SCHEMA;
|
||||
use proxmox_schema::{api, Updater};
|
||||
|
||||
use crate::context::context;
|
||||
use crate::renderer::TemplateRenderer;
|
||||
use crate::renderer::TemplateType;
|
||||
use crate::schema::ENTITY_NAME_SCHEMA;
|
||||
use crate::{renderer, Content, Endpoint, Error, Notification, Origin, Severity};
|
||||
|
||||
@ -92,14 +92,13 @@ impl Endpoint for GotifyEndpoint {
|
||||
fn send(&self, notification: &Notification) -> Result<(), Error> {
|
||||
let (title, message) = match ¬ification.content {
|
||||
Content::Template {
|
||||
title_template,
|
||||
body_template,
|
||||
template_name,
|
||||
data,
|
||||
} => {
|
||||
let rendered_title =
|
||||
renderer::render_template(TemplateRenderer::Plaintext, title_template, data)?;
|
||||
renderer::render_template(TemplateType::Subject, template_name, data)?;
|
||||
let rendered_message =
|
||||
renderer::render_template(TemplateRenderer::Plaintext, body_template, data)?;
|
||||
renderer::render_template(TemplateType::PlaintextBody, template_name, data)?;
|
||||
|
||||
(rendered_title, rendered_message)
|
||||
}
|
||||
|
@ -3,9 +3,9 @@ use serde::{Deserialize, Serialize};
|
||||
use proxmox_schema::api_types::COMMENT_SCHEMA;
|
||||
use proxmox_schema::{api, Updater};
|
||||
|
||||
use crate::context::context;
|
||||
use crate::context;
|
||||
use crate::endpoints::common::mail;
|
||||
use crate::renderer::TemplateRenderer;
|
||||
use crate::renderer::TemplateType;
|
||||
use crate::schema::{EMAIL_SCHEMA, ENTITY_NAME_SCHEMA, USER_SCHEMA};
|
||||
use crate::{renderer, Content, Endpoint, Error, Notification, Origin};
|
||||
|
||||
@ -103,16 +103,15 @@ impl Endpoint for SendmailEndpoint {
|
||||
|
||||
match ¬ification.content {
|
||||
Content::Template {
|
||||
title_template,
|
||||
body_template,
|
||||
template_name,
|
||||
data,
|
||||
} => {
|
||||
let subject =
|
||||
renderer::render_template(TemplateRenderer::Plaintext, title_template, data)?;
|
||||
renderer::render_template(TemplateType::Subject, template_name, data)?;
|
||||
let html_part =
|
||||
renderer::render_template(TemplateRenderer::Html, body_template, data)?;
|
||||
renderer::render_template(TemplateType::HtmlBody, template_name, data)?;
|
||||
let text_part =
|
||||
renderer::render_template(TemplateRenderer::Plaintext, body_template, data)?;
|
||||
renderer::render_template(TemplateType::PlaintextBody, template_name, data)?;
|
||||
|
||||
let author = self
|
||||
.config
|
||||
|
@ -11,7 +11,7 @@ use proxmox_schema::{api, Updater};
|
||||
|
||||
use crate::context::context;
|
||||
use crate::endpoints::common::mail;
|
||||
use crate::renderer::TemplateRenderer;
|
||||
use crate::renderer::TemplateType;
|
||||
use crate::schema::{EMAIL_SCHEMA, ENTITY_NAME_SCHEMA, USER_SCHEMA};
|
||||
use crate::{renderer, Content, Endpoint, Error, Notification, Origin};
|
||||
|
||||
@ -202,16 +202,15 @@ impl Endpoint for SmtpEndpoint {
|
||||
|
||||
let mut email = match ¬ification.content {
|
||||
Content::Template {
|
||||
title_template,
|
||||
body_template,
|
||||
template_name,
|
||||
data,
|
||||
} => {
|
||||
let subject =
|
||||
renderer::render_template(TemplateRenderer::Plaintext, title_template, data)?;
|
||||
renderer::render_template(TemplateType::Subject, template_name, data)?;
|
||||
let html_part =
|
||||
renderer::render_template(TemplateRenderer::Html, body_template, data)?;
|
||||
renderer::render_template(TemplateType::HtmlBody, template_name, data)?;
|
||||
let text_part =
|
||||
renderer::render_template(TemplateRenderer::Plaintext, body_template, data)?;
|
||||
renderer::render_template(TemplateType::PlaintextBody, template_name, data)?;
|
||||
|
||||
email_builder = email_builder.subject(subject);
|
||||
|
||||
|
@ -162,10 +162,8 @@ pub trait Endpoint {
|
||||
pub enum Content {
|
||||
/// Title and body will be rendered as a template
|
||||
Template {
|
||||
/// Template for the notification title.
|
||||
title_template: String,
|
||||
/// Template for the notification body.
|
||||
body_template: String,
|
||||
/// Name of the used template
|
||||
template_name: String,
|
||||
/// Data that can be used for template rendering.
|
||||
data: Value,
|
||||
},
|
||||
@ -203,10 +201,9 @@ pub struct Notification {
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
pub fn new_templated<S: AsRef<str>>(
|
||||
pub fn from_template<S: AsRef<str>>(
|
||||
severity: Severity,
|
||||
title: S,
|
||||
body: S,
|
||||
template_name: S,
|
||||
template_data: Value,
|
||||
fields: HashMap<String, String>,
|
||||
) -> Self {
|
||||
@ -217,8 +214,7 @@ impl Notification {
|
||||
timestamp: proxmox_time::epoch_i64(),
|
||||
},
|
||||
content: Content::Template {
|
||||
title_template: title.as_ref().to_string(),
|
||||
body_template: body.as_ref().to_string(),
|
||||
template_name: template_name.as_ref().to_string(),
|
||||
data: template_data,
|
||||
},
|
||||
}
|
||||
@ -549,8 +545,7 @@ impl Bus {
|
||||
timestamp: proxmox_time::epoch_i64(),
|
||||
},
|
||||
content: Content::Template {
|
||||
title_template: "Test notification".into(),
|
||||
body_template: "This is a test of the notification target '{{ target }}'".into(),
|
||||
template_name: "test".to_string(),
|
||||
data: json!({ "target": target }),
|
||||
},
|
||||
};
|
||||
@ -623,10 +618,9 @@ mod tests {
|
||||
bus.add_matcher(matcher);
|
||||
|
||||
// Send directly to endpoint
|
||||
bus.send(&Notification::new_templated(
|
||||
bus.send(&Notification::from_template(
|
||||
Severity::Info,
|
||||
"Title",
|
||||
"Body",
|
||||
"test",
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
));
|
||||
@ -661,10 +655,9 @@ mod tests {
|
||||
});
|
||||
|
||||
let send_with_severity = |severity| {
|
||||
let notification = Notification::new_templated(
|
||||
let notification = Notification::from_template(
|
||||
severity,
|
||||
"Title",
|
||||
"Body",
|
||||
"test",
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
);
|
||||
|
@ -456,7 +456,7 @@ mod tests {
|
||||
fields.insert("foo".into(), "bar".into());
|
||||
|
||||
let notification =
|
||||
Notification::new_templated(Severity::Notice, "test", "test", Value::Null, fields);
|
||||
Notification::from_template(Severity::Notice, "test", Value::Null, fields);
|
||||
|
||||
let matcher: FieldMatcher = "exact:foo=bar".parse().unwrap();
|
||||
assert!(matcher.matches(¬ification).unwrap());
|
||||
@ -474,14 +474,14 @@ mod tests {
|
||||
fields.insert("foo".into(), "test".into());
|
||||
|
||||
let notification =
|
||||
Notification::new_templated(Severity::Notice, "test", "test", Value::Null, fields);
|
||||
Notification::from_template(Severity::Notice, "test", Value::Null, fields);
|
||||
assert!(matcher.matches(¬ification).unwrap());
|
||||
|
||||
let mut fields = HashMap::new();
|
||||
fields.insert("foo".into(), "notthere".into());
|
||||
|
||||
let notification =
|
||||
Notification::new_templated(Severity::Notice, "test", "test", Value::Null, fields);
|
||||
Notification::from_template(Severity::Notice, "test", Value::Null, fields);
|
||||
assert!(!matcher.matches(¬ification).unwrap());
|
||||
|
||||
assert!("regex:'3=b.*".parse::<FieldMatcher>().is_err());
|
||||
@ -489,13 +489,8 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn test_severities() {
|
||||
let notification = Notification::new_templated(
|
||||
Severity::Notice,
|
||||
"test",
|
||||
"test",
|
||||
Value::Null,
|
||||
Default::default(),
|
||||
);
|
||||
let notification =
|
||||
Notification::from_template(Severity::Notice, "test", Value::Null, Default::default());
|
||||
|
||||
let matcher: SeverityMatcher = "info,notice,warning,error".parse().unwrap();
|
||||
assert!(matcher.matches(¬ification).unwrap());
|
||||
@ -503,13 +498,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_empty_matcher_matches_always() {
|
||||
let notification = Notification::new_templated(
|
||||
Severity::Notice,
|
||||
"test",
|
||||
"test",
|
||||
Value::Null,
|
||||
Default::default(),
|
||||
);
|
||||
let notification =
|
||||
Notification::from_template(Severity::Notice, "test", Value::Null, Default::default());
|
||||
|
||||
for mode in [MatchModeOperator::All, MatchModeOperator::Any] {
|
||||
let config = MatcherConfig {
|
||||
|
@ -5,7 +5,6 @@ use handlebars::{
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{table::Table, value_to_string};
|
||||
use crate::define_helper_with_prefix_and_postfix;
|
||||
use crate::renderer::BlockRenderFunctions;
|
||||
|
||||
fn render_html_table(
|
||||
@ -79,22 +78,9 @@ fn render_object(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
define_helper_with_prefix_and_postfix!(verbatim_monospaced, "<pre>", "</pre>");
|
||||
define_helper_with_prefix_and_postfix!(heading_1, "<h1 style=\"font-size: 1.2em\">", "</h1>");
|
||||
define_helper_with_prefix_and_postfix!(heading_2, "<h2 style=\"font-size: 1em\">", "</h2>");
|
||||
define_helper_with_prefix_and_postfix!(
|
||||
verbatim,
|
||||
"<pre style=\"font-family: sans-serif\">",
|
||||
"</pre>"
|
||||
);
|
||||
|
||||
pub(super) fn block_render_functions() -> BlockRenderFunctions {
|
||||
BlockRenderFunctions {
|
||||
table: Box::new(render_html_table),
|
||||
verbatim_monospaced: Box::new(verbatim_monospaced),
|
||||
object: Box::new(render_object),
|
||||
heading_1: Box::new(heading_1),
|
||||
heading_2: Box::new(heading_2),
|
||||
verbatim: Box::new(verbatim),
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ use serde_json::Value;
|
||||
use proxmox_human_byte::HumanByte;
|
||||
use proxmox_time::TimeSpan;
|
||||
|
||||
use crate::Error;
|
||||
use crate::{context, Error};
|
||||
|
||||
mod html;
|
||||
mod plaintext;
|
||||
@ -165,41 +165,47 @@ impl ValueRenderFunction {
|
||||
}
|
||||
}
|
||||
|
||||
/// Available renderers for notification templates.
|
||||
/// Available template types
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum TemplateRenderer {
|
||||
/// Render to HTML code
|
||||
Html,
|
||||
/// Render to plain text
|
||||
Plaintext,
|
||||
pub enum TemplateType {
|
||||
/// HTML body template
|
||||
HtmlBody,
|
||||
/// Plaintext body template
|
||||
PlaintextBody,
|
||||
/// Plaintext body template
|
||||
Subject,
|
||||
}
|
||||
|
||||
impl TemplateRenderer {
|
||||
fn prefix(&self) -> &str {
|
||||
impl TemplateType {
|
||||
fn file_suffix(&self) -> &'static str {
|
||||
match self {
|
||||
TemplateRenderer::Html => "<html>\n<body>\n",
|
||||
TemplateRenderer::Plaintext => "",
|
||||
TemplateType::HtmlBody => "body.html.hbs",
|
||||
TemplateType::PlaintextBody => "body.txt.hbs",
|
||||
TemplateType::Subject => "subject.txt.hbs",
|
||||
}
|
||||
}
|
||||
|
||||
fn postfix(&self) -> &str {
|
||||
match self {
|
||||
TemplateRenderer::Html => "\n</body>\n</html>",
|
||||
TemplateRenderer::Plaintext => "",
|
||||
fn postprocess(&self, mut rendered: String) -> String {
|
||||
if let Self::Subject = self {
|
||||
rendered = rendered.replace('\n', " ");
|
||||
}
|
||||
|
||||
rendered
|
||||
}
|
||||
|
||||
fn block_render_fns(&self) -> BlockRenderFunctions {
|
||||
match self {
|
||||
TemplateRenderer::Html => html::block_render_functions(),
|
||||
TemplateRenderer::Plaintext => plaintext::block_render_functions(),
|
||||
TemplateType::HtmlBody => html::block_render_functions(),
|
||||
TemplateType::Subject => plaintext::block_render_functions(),
|
||||
TemplateType::PlaintextBody => plaintext::block_render_functions(),
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_fn(&self) -> fn(&str) -> String {
|
||||
match self {
|
||||
TemplateRenderer::Html => handlebars::html_escape,
|
||||
TemplateRenderer::Plaintext => handlebars::no_escape,
|
||||
TemplateType::PlaintextBody => handlebars::no_escape,
|
||||
TemplateType::Subject => handlebars::no_escape,
|
||||
TemplateType::HtmlBody => handlebars::html_escape,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -208,28 +214,20 @@ type HelperFn = dyn HelperDef + Send + Sync;
|
||||
|
||||
struct BlockRenderFunctions {
|
||||
table: Box<HelperFn>,
|
||||
verbatim_monospaced: Box<HelperFn>,
|
||||
object: Box<HelperFn>,
|
||||
heading_1: Box<HelperFn>,
|
||||
heading_2: Box<HelperFn>,
|
||||
verbatim: Box<HelperFn>,
|
||||
}
|
||||
|
||||
impl BlockRenderFunctions {
|
||||
fn register_helpers(self, handlebars: &mut Handlebars) {
|
||||
handlebars.register_helper("table", self.table);
|
||||
handlebars.register_helper("verbatim", self.verbatim);
|
||||
handlebars.register_helper("verbatim-monospaced", self.verbatim_monospaced);
|
||||
handlebars.register_helper("object", self.object);
|
||||
handlebars.register_helper("heading-1", self.heading_1);
|
||||
handlebars.register_helper("heading-2", self.heading_2);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_template_impl(
|
||||
template: &str,
|
||||
data: &Value,
|
||||
renderer: TemplateRenderer,
|
||||
renderer: TemplateType,
|
||||
) -> Result<String, Error> {
|
||||
let mut handlebars = Handlebars::new();
|
||||
handlebars.register_escape_fn(renderer.escape_fn());
|
||||
@ -248,61 +246,45 @@ fn render_template_impl(
|
||||
|
||||
/// Render a template string.
|
||||
///
|
||||
/// The output format can be chosen via the `renderer` parameter (see [TemplateRenderer]
|
||||
/// The output format can be chosen via the `renderer` parameter (see [TemplateType]
|
||||
/// for available options).
|
||||
pub fn render_template(
|
||||
renderer: TemplateRenderer,
|
||||
mut ty: TemplateType,
|
||||
template: &str,
|
||||
data: &Value,
|
||||
) -> Result<String, Error> {
|
||||
let mut rendered_template = String::from(renderer.prefix());
|
||||
let filename = format!("{template}-{suffix}", suffix = ty.file_suffix());
|
||||
|
||||
rendered_template.push_str(&render_template_impl(template, data, renderer)?);
|
||||
rendered_template.push_str(renderer.postfix());
|
||||
let template_string = context::context().lookup_template(&filename, None)?;
|
||||
|
||||
Ok(rendered_template)
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! define_helper_with_prefix_and_postfix {
|
||||
($name:ident, $pre:expr, $post:expr) => {
|
||||
fn $name<'reg, 'rc>(
|
||||
h: &Helper<'reg, 'rc>,
|
||||
handlebars: &'reg Handlebars,
|
||||
context: &'rc Context,
|
||||
render_context: &mut RenderContext<'reg, 'rc>,
|
||||
out: &mut dyn Output,
|
||||
) -> HelperResult {
|
||||
use handlebars::Renderable;
|
||||
|
||||
let block_text = h.template();
|
||||
let param = h.param(0);
|
||||
|
||||
out.write($pre)?;
|
||||
match (param, block_text) {
|
||||
(None, Some(block_text)) => {
|
||||
block_text.render(handlebars, context, render_context, out)
|
||||
}
|
||||
(Some(param), None) => {
|
||||
let value = param.value();
|
||||
let text = value.as_str().ok_or_else(|| {
|
||||
HandlebarsRenderError::new(format!("value {value} is not a string"))
|
||||
})?;
|
||||
|
||||
out.write(text)?;
|
||||
Ok(())
|
||||
}
|
||||
(Some(_), Some(_)) => Err(HandlebarsRenderError::new(
|
||||
"Cannot use parameter and template at the same time",
|
||||
)),
|
||||
(None, None) => Err(HandlebarsRenderError::new(
|
||||
"Neither parameter nor template was provided",
|
||||
)),
|
||||
}?;
|
||||
out.write($post)?;
|
||||
Ok(())
|
||||
let (template_string, fallback) = match (template_string, ty) {
|
||||
(None, TemplateType::HtmlBody) => {
|
||||
ty = TemplateType::PlaintextBody;
|
||||
let plaintext_filename = format!("{template}-{suffix}", suffix = ty.file_suffix());
|
||||
log::info!("html template '{filename}' not found, falling back to plain text template '{plaintext_filename}'");
|
||||
(
|
||||
context::context().lookup_template(&plaintext_filename, None)?,
|
||||
true,
|
||||
)
|
||||
}
|
||||
(template_string, _) => (template_string, false),
|
||||
};
|
||||
|
||||
let template_string = template_string.ok_or(Error::Generic(format!(
|
||||
"could not load template '{template}'"
|
||||
)))?;
|
||||
|
||||
let mut rendered = render_template_impl(&template_string, data, ty)?;
|
||||
rendered = ty.postprocess(rendered);
|
||||
|
||||
if fallback {
|
||||
rendered = format!(
|
||||
"<html><body><pre>{}</pre></body></html>",
|
||||
handlebars::html_escape(&rendered)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(rendered)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -310,73 +292,6 @@ mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_render_template() -> Result<(), Error> {
|
||||
let data = json!({
|
||||
"dur": 12345,
|
||||
"size": 1024 * 15,
|
||||
|
||||
"table": {
|
||||
"schema": {
|
||||
"columns": [
|
||||
{
|
||||
"id": "col1",
|
||||
"label": "Column 1"
|
||||
},
|
||||
{
|
||||
"id": "col2",
|
||||
"label": "Column 2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"data": [
|
||||
{
|
||||
"col1": "val1",
|
||||
"col2": "val2"
|
||||
},
|
||||
{
|
||||
"col1": "val3",
|
||||
"col2": "val4"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
let template = r#"
|
||||
{{heading-1 "Hello World"}}
|
||||
|
||||
{{heading-2 "Hello World"}}
|
||||
|
||||
{{human-bytes size}}
|
||||
{{duration dur}}
|
||||
|
||||
{{table table}}"#;
|
||||
|
||||
let expected_plaintext = r#"
|
||||
Hello World
|
||||
===========
|
||||
|
||||
Hello World
|
||||
-----------
|
||||
|
||||
15 KiB
|
||||
3h 25min 45s
|
||||
|
||||
Column 1 Column 2
|
||||
val1 val2
|
||||
val3 val4
|
||||
"#;
|
||||
|
||||
let rendered_plaintext = render_template(TemplateRenderer::Plaintext, template, &data)?;
|
||||
|
||||
// Let's not bother about testing the HTML output, too fragile.
|
||||
|
||||
assert_eq!(rendered_plaintext, expected_plaintext);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_helpers() {
|
||||
assert_eq!(value_to_byte_size(&json!(1024)), Some("1 KiB".to_string()));
|
||||
|
@ -7,7 +7,6 @@ use handlebars::{
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{table::Table, value_to_string};
|
||||
use crate::define_helper_with_prefix_and_postfix;
|
||||
use crate::renderer::BlockRenderFunctions;
|
||||
|
||||
fn optimal_column_widths(table: &Table) -> HashMap<&str, usize> {
|
||||
@ -76,40 +75,6 @@ fn render_plaintext_table(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
macro_rules! define_underlining_heading_fn {
|
||||
($name:ident, $underline:expr) => {
|
||||
fn $name<'reg, 'rc>(
|
||||
h: &Helper<'reg, 'rc>,
|
||||
_handlebars: &'reg Handlebars,
|
||||
_context: &'rc Context,
|
||||
_render_context: &mut RenderContext<'reg, 'rc>,
|
||||
out: &mut dyn Output,
|
||||
) -> HelperResult {
|
||||
let param = h
|
||||
.param(0)
|
||||
.ok_or_else(|| HandlebarsRenderError::new("No parameter provided"))?;
|
||||
|
||||
let value = param.value();
|
||||
let text = value.as_str().ok_or_else(|| {
|
||||
HandlebarsRenderError::new(format!("value {value} is not a string"))
|
||||
})?;
|
||||
|
||||
out.write(text)?;
|
||||
out.write("\n")?;
|
||||
|
||||
for _ in 0..text.len() {
|
||||
out.write($underline)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
define_helper_with_prefix_and_postfix!(verbatim_monospaced, "", "");
|
||||
define_underlining_heading_fn!(heading_1, "=");
|
||||
define_underlining_heading_fn!(heading_2, "-");
|
||||
define_helper_with_prefix_and_postfix!(verbatim, "", "");
|
||||
|
||||
fn render_object(
|
||||
h: &Helper,
|
||||
_: &Handlebars,
|
||||
@ -133,10 +98,6 @@ fn render_object(
|
||||
pub(super) fn block_render_functions() -> BlockRenderFunctions {
|
||||
BlockRenderFunctions {
|
||||
table: Box::new(render_plaintext_table),
|
||||
verbatim_monospaced: Box::new(verbatim_monospaced),
|
||||
verbatim: Box::new(verbatim),
|
||||
object: Box::new(render_object),
|
||||
heading_1: Box::new(heading_1),
|
||||
heading_2: Box::new(heading_2),
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user