diff --git a/src/backend/external_plugin_cache.rs b/src/backend/external_plugin_cache.rs index a381334c66..3cdf5be2c9 100644 --- a/src/backend/external_plugin_cache.rs +++ b/src/backend/external_plugin_cache.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::env; use crate::env_diff::EnvMap; use crate::hash::hash_to_str; -use crate::tera::{BASE_CONTEXT, get_tera}; +use crate::tera::{BASE_CONTEXT, contains_template_syntax, get_tera, render_str}; use crate::toolset::{ToolRequest, ToolVersion}; use dashmap::DashMap; use eyre::{WrapErr, eyre}; @@ -107,16 +107,19 @@ fn render_cache_key(config: &Config, tv: &ToolVersion, cache_key: &[String]) -> } fn parse_template(config: &Config, tv: &ToolVersion, tmpl: &str) -> eyre::Result { + if !contains_template_syntax(tmpl) { + return Ok(tmpl.to_string()); + } + let mut ctx = BASE_CONTEXT.clone(); ctx.insert("project_root", &config.project_root); ctx.insert("opts", &tv.request.options().opts_as_strings()); - get_tera( + let mut tera = get_tera( config .project_root .as_ref() .or(env::current_dir().as_ref().ok()) .map(|p| p.as_path()), - ) - .render_str(tmpl, &ctx) - .wrap_err_with(|| eyre!("failed to parse template: {tmpl}")) + ); + render_str(&mut tera, tmpl, &ctx).wrap_err_with(|| eyre!("failed to parse template: {tmpl}")) } diff --git a/src/backend/github.rs b/src/backend/github.rs index 1369668627..0eb3c1fd81 100644 --- a/src/backend/github.rs +++ b/src/backend/github.rs @@ -1833,6 +1833,10 @@ fn template_string_for_target(template: &str, tv: &ToolVersion, target: &Platfor .replace("{gnu_arch}", gnu_arch); } + if !crate::tera::contains_template_syntax(template) { + return template.to_string(); + } + // Use Tera rendering for templates let mut ctx = crate::tera::BASE_CONTEXT.clone(); ctx.insert("version", version); @@ -1858,7 +1862,7 @@ fn template_string_for_target(template: &str, tv: &ToolVersion, target: &Platfor tera.register_function("os", make_remapping_fn(os.to_string())); tera.register_function("arch", make_remapping_fn(arch.to_string())); - match tera.render_str(template, &ctx) { + match crate::tera::render_str(&mut tera, template, &ctx) { Ok(rendered) => rendered, Err(e) => { warn!("Failed to render template '{}': {}", template, e); diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 29fa5545a1..813d9eff05 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -28,7 +28,7 @@ use crate::plugins::core::CORE_PLUGINS; use crate::plugins::{PEP440_PRERELEASE_REGEX, PluginType, VERSION_REGEX}; use crate::registry::{REGISTRY, full_to_url, normalize_remote, tool_enabled}; use crate::runtime_symlinks::is_runtime_symlink; -use crate::tera::get_tera; +use crate::tera::{contains_template_syntax, get_tera, render_str}; use crate::toolset::outdated_info::OutdatedInfo; use crate::toolset::{ ResolveOptions, ToolOptionSource, ToolRequest, ToolVersion, Toolset, install_state, @@ -1820,10 +1820,14 @@ pub trait Backend: Debug + Send + Sync { } // Render tera template variables (e.g. {{tools.ripgrep.path}}) - let tera_ctx = ctx.ts.tera_ctx(&ctx.config).await?; - let dir = tv.request.source().path().and_then(|p| p.parent()); - let mut tera = get_tera(dir); - let rendered_script = tera.render_str(script, tera_ctx)?; + let rendered_script = if contains_template_syntax(script) { + let tera_ctx = ctx.ts.tera_ctx(&ctx.config).await?; + let dir = tv.request.source().path().and_then(|p| p.parent()); + let mut tera = get_tera(dir); + render_str(&mut tera, script, tera_ctx)? + } else { + script.to_string() + }; let mut runner = CmdLineRunner::new(&*env::SHELL) .env(&*env::PATH_KEY, path_env.join()) diff --git a/src/backend/static_helpers.rs b/src/backend/static_helpers.rs index a000c62ea0..3eff63a56c 100644 --- a/src/backend/static_helpers.rs +++ b/src/backend/static_helpers.rs @@ -357,12 +357,17 @@ pub fn template_string(template: &str, tv: &ToolVersion) -> String { return template.replace("{version}", &tv.version); } + if !crate::tera::contains_template_syntax(template) { + return template.to_string(); + } + // Use Tera rendering for templates // Supports {{ version }}, {{ os() }}, {{ arch() }}, etc. let mut ctx = crate::tera::BASE_CONTEXT.clone(); ctx.insert("version", &tv.version); - match crate::tera::get_tera(None).render_str(template, &ctx) { + let mut tera = crate::tera::get_tera(None); + match crate::tera::render_str(&mut tera, template, &ctx) { Ok(rendered) => rendered, Err(e) => { warn!("Failed to render template '{}': {}", template, e); diff --git a/src/cli/tasks/validate.rs b/src/cli/tasks/validate.rs index b293daff1c..938ddc2db4 100644 --- a/src/cli/tasks/validate.rs +++ b/src/cli/tasks/validate.rs @@ -7,6 +7,7 @@ use crate::duration; use crate::file; use crate::task::Task; use crate::task::task_fetcher::TaskFetcher; +use crate::tera::contains_template_syntax; use crate::ui::style; use console::style as console_style; use eyre::{Result, eyre}; @@ -414,7 +415,7 @@ impl TasksValidate { if let Some(ref dir) = task.dir { // Try to render the directory template - if dir.contains("{{") || dir.contains("{%") { + if contains_template_syntax(dir) { // Contains template syntax - try to render it match task.dir(config).await { Ok(rendered_dir) => { diff --git a/src/cli/test_tool.rs b/src/cli/test_tool.rs index 05c5278399..eb8ce1a595 100644 --- a/src/cli/test_tool.rs +++ b/src/cli/test_tool.rs @@ -3,7 +3,7 @@ use crate::cmd::cmd; use crate::config::Config; use crate::file::display_path; use crate::registry::{REGISTRY, RegistryTool}; -use crate::tera::get_tera; +use crate::tera::{contains_template_syntax, get_tera, render_str}; use crate::toolset::{InstallOptions, ToolsetBuilder}; use crate::ui::time; use crate::{dirs, env, file}; @@ -446,10 +446,14 @@ impl TestTool { } None => return Err(eyre!("command failed: terminated by signal")), } - let mut ctx = config.tera_ctx.clone(); - ctx.insert("version", &tv.version); - let mut tera = get_tera(dirs::CWD.as_ref().map(|d| d.as_path())); - let expected = tera.render_str(expected, &ctx)?; + let expected = if contains_template_syntax(expected) { + let mut ctx = config.tera_ctx.clone(); + ctx.insert("version", &tv.version); + let mut tera = get_tera(dirs::CWD.as_ref().map(|d| d.as_path())); + render_str(&mut tera, expected, &ctx)? + } else { + expected.to_string() + }; let stdout = String::from_utf8(res.stdout)?; let clean_stdout = console::strip_ansi_codes(&stdout); if !clean_stdout.contains(&expected) { diff --git a/src/config/config_file/mise_toml.rs b/src/config/config_file/mise_toml.rs index c26fa99aa7..0eefe3175b 100644 --- a/src/config/config_file/mise_toml.rs +++ b/src/config/config_file/mise_toml.rs @@ -31,7 +31,7 @@ use crate::oci::OciConfig; use crate::redactions::Redactions; use crate::registry::REGISTRY; use crate::task::{Task, TaskTemplate}; -use crate::tera::{BASE_CONTEXT, get_tera}; +use crate::tera::{BASE_CONTEXT, contains_template_syntax, get_tera, render_str}; use crate::toolset::{ToolRequest, ToolRequestSet, ToolSource, ToolVersionOptions}; use crate::watch_files::WatchFile; use crate::{env, file}; @@ -249,10 +249,6 @@ impl MiseToml { } Ok(()) } - fn contains_template_syntax(input: &str) -> bool { - input.contains("{{") || input.contains("{%") || input.contains("{#") - } - pub fn init(path: &Path) -> Self { let mut context = BASE_CONTEXT.clone(); context.insert( @@ -575,11 +571,12 @@ impl MiseToml { context: &TeraContext, input: &str, ) -> eyre::Result { - if !Self::contains_template_syntax(input) { + if !contains_template_syntax(input) { return Ok(input.to_string()); } let dir = self.path.parent(); - let output = get_tera(dir).render_str(input, context).wrap_err_with(|| { + let mut tera = get_tera(dir); + let output = render_str(&mut tera, input, context).wrap_err_with(|| { let p = display_path(&self.path); eyre!("failed to parse template {input} in {p}") })?; diff --git a/src/config/config_file/tool_versions.rs b/src/config/config_file/tool_versions.rs index e82fe9be5f..ae0d921513 100644 --- a/src/config/config_file/tool_versions.rs +++ b/src/config/config_file/tool_versions.rs @@ -14,7 +14,7 @@ use crate::cli::args::BackendArg; use crate::config::config_file::{ConfigFile, trust_check}; use crate::file; use crate::file::display_path; -use crate::tera::{BASE_CONTEXT, get_tera}; +use crate::tera::{BASE_CONTEXT, contains_template_syntax, get_tera, render_str}; use crate::toolset::{ToolRequest, ToolRequestSet, ToolSource}; use super::ConfigFileType; @@ -60,9 +60,10 @@ impl ToolVersions { pub fn parse_str(s: &str, path: PathBuf) -> Result { let mut cf = Self::init(&path); let dir = path.parent(); - let s = if s.contains("{{") || s.contains("{%") || s.contains("{#") { + let s = if contains_template_syntax(s) { trust_check(&path)?; - get_tera(dir).render_str(s, &cf.context)? + let mut tera = get_tera(dir); + render_str(&mut tera, s, &cf.context)? } else { s.to_string() }; diff --git a/src/config/env_directive/file.rs b/src/config/env_directive/file.rs index 3447ef0bb3..7b790fbe73 100644 --- a/src/config/env_directive/file.rs +++ b/src/config/env_directive/file.rs @@ -1,4 +1,5 @@ use crate::config::{Config, env_directive::EnvResults}; +use crate::env_diff::EnvMap as TeraEnvMap; use crate::file::display_path; use crate::{Result, file, sops}; use eyre::{WrapErr, bail, eyre}; @@ -25,18 +26,19 @@ impl EnvResults { pub async fn file( config: &Arc, ctx: &mut tera::Context, - tera: &mut tera::Tera, + tera: &mut Option, r: &mut EnvResults, normalize_path: fn(&Path, PathBuf) -> PathBuf, source: &Path, + exec_env: &TeraEnvMap, config_root: &Path, input: String, ) -> Result> { let mut out = IndexMap::new(); - let s = r.parse_template(ctx, tera, source, &input)?; + let s = r.parse_template(ctx, tera, source, exec_env, &input)?; for p in xx::file::glob(normalize_path(config_root, s.into())).unwrap_or_default() { let env = out.entry(p.clone()).or_insert_with(IndexMap::new); - let parse_template = |s: String| r.parse_template(ctx, tera, source, &s); + let parse_template = |s: String| r.parse_template(ctx, tera, source, exec_env, &s); let ext = p .extension() .map(|e| e.to_string_lossy().to_string()) diff --git a/src/config/env_directive/mod.rs b/src/config/env_directive/mod.rs index 046e36e5a4..76bdde7bb6 100644 --- a/src/config/env_directive/mod.rs +++ b/src/config/env_directive/mod.rs @@ -4,7 +4,7 @@ use crate::env; use crate::env_diff::EnvMap; use crate::file::display_path; use crate::path_env::PathEnv; -use crate::tera::{get_tera, tera_exec}; +use crate::tera::{contains_template_syntax, get_tera, render_str, tera_exec}; use eyre::{Context, eyre}; use indexmap::IndexMap; use itertools::Itertools; @@ -317,16 +317,7 @@ impl EnvResults { let filtered_input_for_validation = filtered_input.clone(); for (directive, source) in filtered_input { - let mut tera = get_tera(source.parent()); - tera.register_function( - "exec", - tera_exec( - source.parent().map(|d| d.to_path_buf()), - env.iter() - .map(|(k, (v, _))| (k.clone(), v.clone())) - .collect(), - ), - ); + let mut tera = None; // trace!( // "resolve: directive: {:?}, source: {:?}", // &directive, @@ -357,7 +348,7 @@ impl EnvResults { // trace!("resolve: ctx.get('env'): {:#?}", &ctx.get("env")); match directive { EnvDirective::Val(k, v, _opts) => { - let v = r.parse_template(&ctx, &mut tera, &source, &v)?; + let v = r.parse_template(&ctx, &mut tera, &source, &env_vars, &v)?; if resolve_opts.vars { r.vars.insert(k, (v, source.clone())); @@ -388,7 +379,7 @@ impl EnvResults { let decrypted_v = match res { Ok(decrypted_v) => { // Parse as template after decryption - r.parse_template(&ctx, &mut tera, &source, &decrypted_v)? + r.parse_template(&ctx, &mut tera, &source, &env_vars, &decrypted_v)? } Err(e) if Settings::get().age.strict => { return Err(e) @@ -438,7 +429,9 @@ impl EnvResults { } } EnvDirective::Path(input_str, _opts) => { - let path = Self::path(&mut ctx, &mut tera, &mut r, &source, input_str).await?; + let path = + Self::path(&mut ctx, &mut tera, &mut r, &source, &env_vars, input_str) + .await?; paths.push((path.clone(), source.clone())); // Don't modify PATH in env - just add to env_paths // This allows consumers to control PATH ordering @@ -451,6 +444,7 @@ impl EnvResults { &mut r, normalize_path, &source, + &env_vars, &config_root, input, ) @@ -477,6 +471,7 @@ impl EnvResults { &mut r, normalize_path, &source, + &env_vars, &config_root, &env_vars, input, @@ -511,8 +506,9 @@ impl EnvResults { &mut r, normalize_path, &source, + &env_vars, &config_root, - env_vars, + env_vars.clone(), path, create, python, @@ -673,17 +669,25 @@ impl EnvResults { fn parse_template( &self, ctx: &tera::Context, - tera: &mut tera::Tera, + tera: &mut Option, path: &Path, + exec_env: &EnvMap, input: &str, ) -> eyre::Result { let mut output = input.to_string(); // Step 1: Tera template expansion - if input.contains("{{") || input.contains("{%") || input.contains("{#") { + if contains_template_syntax(input) { trust_check(path)?; - output = tera - .render_str(input, ctx) + let tera = tera.get_or_insert_with(|| { + let mut tera = get_tera(path.parent()); + tera.register_function( + "exec", + tera_exec(path.parent().map(|d| d.to_path_buf()), exec_env.clone()), + ); + tera + }); + output = render_str(tera, input, ctx) .wrap_err_with(|| eyre!("failed to parse template: '{input}'"))?; } diff --git a/src/config/env_directive/path.rs b/src/config/env_directive/path.rs index 318ea482ff..5eca9c8d37 100644 --- a/src/config/env_directive/path.rs +++ b/src/config/env_directive/path.rs @@ -1,16 +1,18 @@ use crate::config::env_directive::EnvResults; +use crate::env_diff::EnvMap; use crate::result; use std::path::{Path, PathBuf}; impl EnvResults { pub async fn path( ctx: &mut tera::Context, - tera: &mut tera::Tera, + tera: &mut Option, r: &mut EnvResults, source: &Path, + exec_env: &EnvMap, input: String, ) -> result::Result { - r.parse_template(ctx, tera, source, &input) + r.parse_template(ctx, tera, source, exec_env, &input) .map(PathBuf::from) } } diff --git a/src/config/env_directive/source.rs b/src/config/env_directive/source.rs index d6b1a57408..b642d93d2b 100644 --- a/src/config/env_directive/source.rs +++ b/src/config/env_directive/source.rs @@ -9,17 +9,18 @@ impl EnvResults { #[allow(clippy::too_many_arguments)] pub fn source( ctx: &mut tera::Context, - tera: &mut tera::Tera, + tera: &mut Option, paths: &mut Vec<(PathBuf, PathBuf)>, r: &mut EnvResults, normalize_path: fn(&Path, PathBuf) -> PathBuf, source: &Path, + exec_env: &EnvMap, config_root: &Path, env_vars: &EnvMap, input: String, ) -> Result>> { let mut out = IndexMap::new(); - let s = r.parse_template(ctx, tera, source, &input)?; + let s = r.parse_template(ctx, tera, source, exec_env, &input)?; let orig_path = env_vars.get(&*env::PATH_KEY).cloned().unwrap_or_default(); let mut env_diff_opts = EnvDiffOptions::default(); env_diff_opts.ignore_keys.shift_remove(&*env::PATH_KEY); // allow modifying PATH diff --git a/src/config/env_directive/venv.rs b/src/config/env_directive/venv.rs index ddddd2ff7b..977a437f68 100644 --- a/src/config/env_directive/venv.rs +++ b/src/config/env_directive/venv.rs @@ -173,11 +173,12 @@ impl EnvResults { pub async fn venv( config: &Arc, ctx: &mut tera::Context, - tera: &mut tera::Tera, + tera: &mut Option, env: &mut IndexMap)>, r: &mut EnvResults, normalize_path: fn(&Path, PathBuf) -> PathBuf, source: &Path, + exec_env: &EnvMap, config_root: &Path, env_vars: EnvMap, path: String, @@ -188,7 +189,7 @@ impl EnvResults { ) -> Result<()> { trace!("python venv: {} create={create}", display_path(&path)); trust_check(source)?; - let venv = r.parse_template(ctx, tera, source, &path)?; + let venv = r.parse_template(ctx, tera, source, exec_env, &path)?; let venv = normalize_path(config_root, venv.into()); let venv_lock = LockFile::new(&venv).lock()?; if !venv.exists() && create { diff --git a/src/config/miserc.rs b/src/config/miserc.rs index dee9a9e8b7..378b72e2b6 100644 --- a/src/config/miserc.rs +++ b/src/config/miserc.rs @@ -16,7 +16,9 @@ use crate::config::settings::MisercSettings; use crate::dirs; use crate::env; use crate::file; -use crate::tera::{get_miserc_tera, take_tera_accessed_files}; +use crate::tera::{ + contains_template_syntax, get_miserc_tera, render_str, take_tera_accessed_files, +}; static MISERC: OnceLock = OnceLock::new(); @@ -84,7 +86,7 @@ fn render_miserc_template( content: &str, config_root: &Path, ) -> String { - if !content.contains("{{") && !content.contains("{%") && !content.contains("{#") { + if !contains_template_syntax(content) { return content.to_string(); } // Lazily initialize the Tera instance — only pay the clone cost if at least one file @@ -103,7 +105,7 @@ fn render_miserc_template( context.insert("xdg_config_home", &*env::XDG_CONFIG_HOME); context.insert("xdg_data_home", &*env::XDG_DATA_HOME); context.insert("xdg_state_home", &*env::XDG_STATE_HOME); - match tera.render_str(content, &context) { + match render_str(tera, content, &context) { Ok(rendered) => rendered, Err(e) => { warn!("Failed to render template in miserc: {e}"); diff --git a/src/config/mod.rs b/src/config/mod.rs index d5b36db90c..c29a7989ee 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -28,7 +28,7 @@ use crate::file::display_path; use crate::shorthands::{Shorthands, get_shorthands}; use crate::task::task_file_providers::TaskFileProvidersBuilder; use crate::task::{Task, TaskTemplate}; -use crate::tera::take_tera_accessed_files; +use crate::tera::{contains_template_syntax, render_str, take_tera_accessed_files}; use crate::toolset::env_cache::{CachedNonToolEnv, compute_settings_hash, get_file_mtime}; use crate::toolset::{ ResolvedToolOptions, ToolOptionSource, ToolOptions, ToolRequestSet, ToolRequestSetBuilder, @@ -2312,9 +2312,13 @@ async fn load_tasks_includes( if task.dir.is_none() && let Some(ref dir) = *task_config_dir { - let mut tera = crate::tera::get_tera(Some(config_root.as_ref())); - let tera_ctx = task.tera_ctx(&config).await?; - task.dir = Some(tera.render_str(dir, &tera_ctx)?); + task.dir = Some(if contains_template_syntax(dir) { + let mut tera = crate::tera::get_tera(Some(config_root.as_ref())); + let tera_ctx = task.tera_ctx(&config).await?; + render_str(&mut tera, dir, &tera_ctx)? + } else { + dir.clone() + }); } tasks.push(task); } diff --git a/src/deps/engine.rs b/src/deps/engine.rs index dc7cc214a8..27501c7fa0 100644 --- a/src/deps/engine.rs +++ b/src/deps/engine.rs @@ -9,7 +9,7 @@ use tokio::task::JoinSet; use crate::cmd::CmdLineRunner; use crate::config::config_file::ConfigFile; use crate::config::{Config, Settings}; -use crate::tera::{BASE_CONTEXT, get_tera}; +use crate::tera::{BASE_CONTEXT, contains_template_syntax, get_tera, render_str}; use crate::ui::multi_progress_report::MultiProgressReport; use crate::ui::progress_report::SingleReport; use crate::ui::style; @@ -783,16 +783,24 @@ impl DepsEngine { // Apply command-specific environment (can override toolset env) // Render tera templates in env values (e.g., "{{env.baz}}") - let mut tera_ctx = BASE_CONTEXT.clone(); - // Merge toolset env (which includes [env] directives) into tera context - // so templates like "{{env.MY_VAR}}" can resolve config-defined vars - let mut env_map = crate::env::PRISTINE_ENV.clone(); - env_map.extend(toolset_env.iter().map(|(k, v)| (k.clone(), v.clone()))); - tera_ctx.insert("env", &env_map); - let mut tera = get_tera(cmd.cwd.as_deref()); + let has_template_env = cmd.env.values().any(|v| contains_template_syntax(v)); + let mut tera_state = if has_template_env { + let mut tera_ctx = BASE_CONTEXT.clone(); + // Merge toolset env (which includes [env] directives) into tera context + // so templates like "{{env.MY_VAR}}" can resolve config-defined vars + let mut env_map = crate::env::PRISTINE_ENV.clone(); + env_map.extend(toolset_env.iter().map(|(k, v)| (k.clone(), v.clone()))); + tera_ctx.insert("env", &env_map); + Some((get_tera(cmd.cwd.as_deref()), tera_ctx)) + } else { + None + }; for (k, v) in &cmd.env { - let rendered = if v.contains("{{") || v.contains("{%") || v.contains("{#") { - tera.render_str(v, &tera_ctx).unwrap_or_else(|e| { + let rendered = if contains_template_syntax(v) { + let (tera, tera_ctx) = tera_state + .as_mut() + .expect("tera state should exist for template env values"); + render_str(tera, v, tera_ctx).unwrap_or_else(|e| { warn!("failed to render template for deps env {k}: {e}"); v.clone() }) diff --git a/src/hooks.rs b/src/hooks.rs index 25b370de39..5d9dd051e8 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -1,7 +1,7 @@ use crate::cmd::cmd; use crate::config::{Config, Settings, config_file}; use crate::shell::Shell; -use crate::tera::get_tera; +use crate::tera::{contains_template_syntax, get_tera, render_str}; use crate::toolset::{ToolVersion, Toolset}; use crate::{dirs, hook_env}; use eyre::Result; @@ -434,16 +434,29 @@ async fn execute( // providing those env vars aren't installed yet (fixes #6162) let (tera_ctx, mut env) = if hook.hook == Hooks::Preinstall { let env = ts.full_env_without_tools(config).await?; - let mut ctx = config.tera_ctx.clone(); - ctx.insert("env", &env); + let ctx = if contains_template_syntax(run) { + let mut ctx = config.tera_ctx.clone(); + ctx.insert("env", &env); + Some(ctx) + } else { + None + }; (ctx, env) } else { - let ctx = ts.tera_ctx(config).await?.clone(); let env = ts.full_env(config).await?; + let ctx = if contains_template_syntax(run) { + Some(ts.tera_ctx(config).await?.clone()) + } else { + None + }; (ctx, env) }; - let mut tera = get_tera(Some(root)); - let rendered_script = tera.render_str(run, &tera_ctx)?; + let rendered_script = if let Some(tera_ctx) = tera_ctx { + let mut tera = get_tera(Some(root)); + render_str(&mut tera, run, &tera_ctx)? + } else { + run.to_string() + }; let args = shell .iter() diff --git a/src/redactions.rs b/src/redactions.rs index 09d99d9f2b..27dfe250ec 100644 --- a/src/redactions.rs +++ b/src/redactions.rs @@ -2,6 +2,8 @@ use aho_corasick::AhoCorasick; use indexmap::IndexSet; use std::sync::Arc; +use crate::tera::render_str_if_template; + #[derive(Default, Clone, Debug, serde::Deserialize)] pub struct Redactions(pub IndexSet); @@ -12,7 +14,7 @@ impl Redactions { pub fn render(&mut self, tera: &mut tera::Tera, ctx: &tera::Context) -> eyre::Result<()> { for r in self.0.clone().drain(..) { - self.0.insert(tera.render_str(&r, ctx)?); + self.0.insert(render_str_if_template(tera, &r, ctx)?); } Ok(()) } diff --git a/src/task/mod.rs b/src/task/mod.rs index 4f7ea79f88..a071e40744 100644 --- a/src/task/mod.rs +++ b/src/task/mod.rs @@ -4,7 +4,7 @@ use crate::config::env_directive::{EnvDirective, EnvResolveOptions, EnvResults, use crate::config::{self, Config}; use crate::path_env::PathEnv; use crate::task::task_script_parser::TaskScriptParser; -use crate::tera::get_tera; +use crate::tera::{contains_template_syntax, get_tera, render_str}; use crate::ui::tree::TreeItem; use crate::{dirs, env, file}; use console::{measure_text_width, truncate_str}; @@ -159,21 +159,46 @@ impl RunEntry { match self { RunEntry::Script(s) => Ok(RunEntry::Script(s.clone())), RunEntry::SingleTask { task, args, env } => { - let task = tera.render_str(task, tera_ctx)?; + let task = if contains_template_syntax(task) { + render_str(tera, task, tera_ctx)? + } else { + task.clone() + }; let args = args .iter() - .map(|a| tera.render_str(a, tera_ctx)) + .map(|a| { + if contains_template_syntax(a) { + render_str(tera, a, tera_ctx) + } else { + Ok(a.clone()) + } + }) .collect::, _>>()?; let env = env .iter() - .map(|(k, v)| Ok((k.clone(), tera.render_str(v, tera_ctx)?))) + .map(|(k, v)| { + Ok(( + k.clone(), + if contains_template_syntax(v) { + render_str(tera, v, tera_ctx)? + } else { + v.clone() + }, + )) + }) .collect::, tera::Error>>()?; Ok(RunEntry::SingleTask { task, args, env }) } RunEntry::TaskGroup { tasks } => { let tasks = tasks .iter() - .map(|t| tera.render_str(t, tera_ctx)) + .map(|t| { + if contains_template_syntax(t) { + render_str(tera, t, tera_ctx) + } else { + Ok(t.clone()) + } + }) .collect::, _>>()?; Ok(RunEntry::TaskGroup { tasks }) } @@ -181,13 +206,14 @@ impl RunEntry { } pub fn has_tera_template(&self) -> bool { - let has_ref = |s: &str| s.contains("{{") || s.contains("{%") || s.contains("{#"); match self { RunEntry::Script(_) => false, RunEntry::SingleTask { task, args, env } => { - has_ref(task) || args.iter().any(|a| has_ref(a)) || env.values().any(|v| has_ref(v)) + contains_template_syntax(task) + || args.iter().any(|a| contains_template_syntax(a)) + || env.values().any(|v| contains_template_syntax(v)) } - RunEntry::TaskGroup { tasks } => tasks.iter().any(|t| has_ref(t)), + RunEntry::TaskGroup { tasks } => tasks.iter().any(|t| contains_template_syntax(t)), } } } @@ -1035,10 +1061,14 @@ impl Task { .as_ref() .and_then(|cf| cf.task_config().dir.clone()) }) { - let config_root = self.config_root.clone().unwrap_or_default(); - let mut tera = get_tera(Some(&config_root)); - let tera_ctx = self.tera_ctx(config).await?; - let dir = tera.render_str(&dir, &tera_ctx)?; + let dir = if contains_template_syntax(&dir) { + let config_root = self.config_root.clone().unwrap_or_default(); + let mut tera = get_tera(Some(&config_root)); + let tera_ctx = self.tera_ctx(config).await?; + render_str(&mut tera, &dir, &tera_ctx)? + } else { + dir + }; let dir = file::replace_path(&dir); if dir.is_absolute() { Ok(Some(dir.to_path_buf())) @@ -1055,10 +1085,14 @@ impl Task { pub async fn file_path(&self, config: &Arc) -> Result> { if let Some(file) = &self.file { let file_str = file.to_string_lossy().to_string(); - let config_root = self.config_root.clone().unwrap_or_default(); - let mut tera = get_tera(Some(&config_root)); - let tera_ctx = self.tera_ctx(config).await?; - let rendered = tera.render_str(&file_str, &tera_ctx)?; + let rendered = if contains_template_syntax(&file_str) { + let config_root = self.config_root.clone().unwrap_or_default(); + let mut tera = get_tera(Some(&config_root)); + let tera_ctx = self.tera_ctx(config).await?; + render_str(&mut tera, &file_str, &tera_ctx)? + } else { + file_str + }; let rendered_path = file::replace_path(&rendered); if rendered_path.is_absolute() { Ok(Some(rendered_path)) @@ -1329,26 +1363,96 @@ impl Task { self.allow_env.extend(other.allow_env); } - pub async fn render(&mut self, config: &Arc, config_root: &Path) -> Result<()> { - let mut tera = get_tera(Some(config_root)); - let tera_ctx = self.tera_ctx(config).await?; - for a in &mut self.aliases { - *a = tera.render_str(a, &tera_ctx)?; - } + fn has_render_templates(&self) -> bool { + let deps_have_template = |deps: &[TaskDep]| { + deps.iter().any(|dep| { + contains_template_syntax(&dep.task) + || dep.args.iter().any(|arg| contains_template_syntax(arg)) + || dep + .env + .values() + .any(|value| contains_template_syntax(value)) + }) + }; + let tools_have_template = self.tools.values().any(|tool| match tool { + TaskToolValue::String(s) => contains_template_syntax(s), + TaskToolValue::Map(map) => { + contains_template_syntax(&map.version) + || map + .opts + .values() + .any(|value| value.as_str().is_some_and(contains_template_syntax)) + } + }); - self.description = tera.render_str(&self.description, &tera_ctx)?; - for s in &mut self.sources { - *s = tera.render_str(s, &tera_ctx)?; - } + self.aliases.iter().any(|s| contains_template_syntax(s)) + || contains_template_syntax(&self.description) + || self.sources.iter().any(|s| contains_template_syntax(s)) + || self.outputs.has_tera_template() + || deps_have_template(&self.depends) + || deps_have_template(&self.depends_post) + || deps_have_template(&self.wait_for) + || self + .dir + .as_ref() + .is_some_and(|s| contains_template_syntax(s)) + || self + .shell + .as_ref() + .is_some_and(|s| contains_template_syntax(s)) + || tools_have_template + } + + fn store_raw_render_inputs(&mut self) { if !self.sources.is_empty() && self.outputs.is_empty() { self.outputs = TaskOutputs::Auto; } - self.raw_outputs = self.outputs.render(&mut tera, &tera_ctx)?; + self.raw_outputs = self.outputs.raw_templates_without_env(); // Save unrendered dependency templates so they can be re-rendered later // with parent task args available (for passing args to dependencies). self.depends_raw = Some(self.depends.clone()); self.depends_post_raw = Some(self.depends_post.clone()); self.wait_for_raw = Some(self.wait_for.clone()); + } + + fn parse_plain_depends(&mut self) -> Result<()> { + for d in &mut self.depends { + d.parse_shell_style_env()?; + } + for d in &mut self.depends_post { + d.parse_shell_style_env()?; + } + for d in &mut self.wait_for { + d.parse_shell_style_env()?; + } + Ok(()) + } + + pub async fn render(&mut self, config: &Arc, config_root: &Path) -> Result<()> { + if !self.has_render_templates() { + self.store_raw_render_inputs(); + self.parse_plain_depends()?; + return Ok(()); + } + + let mut tera = get_tera(Some(config_root)); + let tera_ctx = self.tera_ctx(config).await?; + for a in &mut self.aliases { + if contains_template_syntax(a) { + *a = render_str(&mut tera, a, &tera_ctx)?; + } + } + + if contains_template_syntax(&self.description) { + self.description = render_str(&mut tera, &self.description, &tera_ctx)?; + } + for s in &mut self.sources { + if contains_template_syntax(s) { + *s = render_str(&mut tera, s, &tera_ctx)?; + } + } + self.store_raw_render_inputs(); + self.raw_outputs = self.outputs.render(&mut tera, &tera_ctx)?; // Render deps that don't contain {{usage.*}} references. Deps with usage // references are deferred until render_depends_with_usage() is called with // the actual arg values from CLI or parent dependency. @@ -1367,22 +1471,32 @@ impl Task { d.render(&mut tera, &tera_ctx)?; } } - if let Some(dir) = &mut self.dir { - *dir = tera.render_str(dir, &tera_ctx)?; + if let Some(dir) = &mut self.dir + && contains_template_syntax(dir) + { + *dir = render_str(&mut tera, dir, &tera_ctx)?; } - if let Some(shell) = &mut self.shell { - *shell = tera.render_str(shell, &tera_ctx)?; + if let Some(shell) = &mut self.shell + && contains_template_syntax(shell) + { + *shell = render_str(&mut tera, shell, &tera_ctx)?; } for (_, v) in &mut self.tools { match v { TaskToolValue::String(s) => { - *v = TaskToolValue::String(tera.render_str(s, &tera_ctx)?); + if contains_template_syntax(s) { + *v = TaskToolValue::String(render_str(&mut tera, s, &tera_ctx)?); + } } TaskToolValue::Map(map) => { - map.version = tera.render_str(&map.version, &tera_ctx)?; + if contains_template_syntax(&map.version) { + map.version = render_str(&mut tera, &map.version, &tera_ctx)?; + } for (_ok, ov) in &mut map.opts { - if let toml::Value::String(s) = ov { - *ov = toml::Value::String(tera.render_str(s, &tera_ctx)?); + if let toml::Value::String(s) = ov + && contains_template_syntax(s) + { + *ov = toml::Value::String(render_str(&mut tera, s, &tera_ctx)?); } } } @@ -1402,6 +1516,16 @@ impl Task { if usage_values.is_empty() { return Ok(()); } + let has_usage_deps = |raw: &Option>| { + raw.as_ref() + .is_some_and(|deps| deps.iter().any(dep_has_usage_ref)) + }; + if !has_usage_deps(&self.depends_raw) + && !has_usage_deps(&self.depends_post_raw) + && !has_usage_deps(&self.wait_for_raw) + { + return Ok(()); + } let config_root = self.config_root.clone().unwrap_or_default(); let mut tera = get_tera(Some(&config_root)); let mut tera_ctx = self.tera_ctx(config).await?; diff --git a/src/task/task_dep.rs b/src/task/task_dep.rs index 555d164bb8..292a1eddd4 100644 --- a/src/task/task_dep.rs +++ b/src/task/task_dep.rs @@ -6,6 +6,8 @@ use std::fmt; use std::fmt::{Display, Formatter}; use std::str::FromStr; +use crate::tera::{contains_template_syntax, render_str}; + #[derive(Debug, Clone, Eq, PartialEq)] pub struct TaskDep { pub task: String, @@ -19,14 +21,25 @@ impl TaskDep { tera: &mut tera::Tera, tera_ctx: &tera::Context, ) -> crate::Result<&mut Self> { - self.task = tera.render_str(&self.task, tera_ctx)?; + if contains_template_syntax(&self.task) { + self.task = render_str(tera, &self.task, tera_ctx)?; + } for a in &mut self.args { - *a = tera.render_str(a, tera_ctx)?; + if contains_template_syntax(a) { + *a = render_str(tera, a, tera_ctx)?; + } } // Render env values through Tera for v in self.env.values_mut() { - *v = tera.render_str(v, tera_ctx)?; + if contains_template_syntax(v) { + *v = render_str(tera, v, tera_ctx)?; + } } + self.parse_shell_style_env()?; + Ok(self) + } + + pub fn parse_shell_style_env(&mut self) -> crate::Result<&mut Self> { // Parse shell-style "FOO=bar BAZ=qux taskname arg1 arg2" if args/env not already set if self.args.is_empty() && self.env.is_empty() { let s = self.task.clone(); diff --git a/src/task/task_executor.rs b/src/task/task_executor.rs index f9d521adf3..2851ba20e3 100644 --- a/src/task/task_executor.rs +++ b/src/task/task_executor.rs @@ -13,6 +13,7 @@ use crate::task::task_output_handler::OutputHandler; use crate::task::task_script_parser::subcommand_name_from_parse; use crate::task::task_source_checker::{save_checksum, sources_are_fresh, task_cwd}; use crate::task::{Deps, FailedTasks, GetMatchingExt, Task}; +use crate::tera::{contains_template_syntax, render_str}; use crate::toolset::env_cache::CachedEnv; use crate::ui::{style, time}; use duct::IntoExecutablePath; @@ -1128,20 +1129,23 @@ impl TaskExecutor { if let Some(confirm) = &task.confirm && !Settings::get().yes { - let config_root = task.config_root.clone().unwrap_or_default(); - let mut tera = crate::tera::get_tera(Some(&config_root)); - let mut tera_ctx = task.tera_ctx(config).await?; - - // Add usage values from parsed environment - let mut usage_ctx = std::collections::HashMap::new(); - for (key, value) in env { - if let Some(usage_key) = key.strip_prefix("usage_") { - usage_ctx.insert(usage_key.to_string(), tera::Value::String(value.clone())); + let message = if contains_template_syntax(confirm.message()) { + let config_root = task.config_root.clone().unwrap_or_default(); + let mut tera = crate::tera::get_tera(Some(&config_root)); + let mut tera_ctx = task.tera_ctx(config).await?; + + // Add usage values from parsed environment + let mut usage_ctx = std::collections::HashMap::new(); + for (key, value) in env { + if let Some(usage_key) = key.strip_prefix("usage_") { + usage_ctx.insert(usage_key.to_string(), tera::Value::String(value.clone())); + } } - } - tera_ctx.insert("usage", &usage_ctx); - - let message = tera.render_str(confirm.message(), &tera_ctx)?; + tera_ctx.insert("usage", &usage_ctx); + render_str(&mut tera, confirm.message(), &tera_ctx)? + } else { + confirm.message().to_string() + }; let default_yes = match confirm.default_value() { Some(default) => Self::parse_confirm_default(default)?, None => true, // keep backwards compatible default of yes if not specified diff --git a/src/task/task_script_parser.rs b/src/task/task_script_parser.rs index 38eaa436ff..14414a546f 100644 --- a/src/task/task_script_parser.rs +++ b/src/task/task_script_parser.rs @@ -3,7 +3,7 @@ use crate::env_diff::EnvMap; use crate::exit::exit; use crate::shell::ShellType; use crate::task::Task; -use crate::tera::get_tera; +use crate::tera::{contains_template_syntax, get_tera, render_str}; use eyre::{Context, Result}; use heck::ToSnakeCase; use indexmap::IndexMap; @@ -64,7 +64,7 @@ impl TaskScriptParser { script: &str, ctx: &tera::Context, ) -> Result { - tera.render_str(script.trim(), ctx) + render_str(tera, script.trim(), ctx) .with_context(|| format!("Failed to render task script: {}", script)) } @@ -73,7 +73,7 @@ impl TaskScriptParser { usage: &str, ctx: &tera::Context, ) -> Result { - tera.render_str(usage.trim(), ctx) + render_str(tera, usage.trim(), ctx) .with_context(|| format!("Failed to render task usage: {}", usage)) } @@ -524,11 +524,7 @@ impl TaskScriptParser { let mut resolved = Vec::with_capacity(glob_patterns.len()); for pattern in glob_patterns.iter() { - // pattern is considered a tera template string if it contains opening tags: - // - "{#" for comments - // - "{{" for expressions - // - "{%" for statements - if pattern.contains("{#") || pattern.contains("{{") || pattern.contains("{%") { + if contains_template_syntax(pattern) { trace!( "tera::render::resolve_task_sources including tera template string in resolved task sources: {pattern}" ); @@ -605,10 +601,24 @@ impl TaskScriptParser { task: &Task, scripts: &[String], ) -> Result { + let usage_has_template = contains_template_syntax(&task.usage); + let scripts_have_template = scripts + .iter() + .any(|script| contains_template_syntax(script)); + if !usage_has_template + && (!scripts_have_template || Settings::get().task.disable_spec_from_run_scripts) + { + return task.usage.trim().parse().map_err(Into::into); + } + let (mut tera, arg_order, input_args, input_flags) = self.setup_tera_for_spec_parsing(task); let mut tera_ctx = task.tera_ctx(config).await?; // First render the usage field to collect the spec - let rendered_usage = Self::render_usage_with_context(&mut tera, &task.usage, &tera_ctx)?; + let rendered_usage = if usage_has_template { + Self::render_usage_with_context(&mut tera, &task.usage, &tera_ctx)? + } else { + task.usage.trim().to_string() + }; let spec_from_field: usage::Spec = rendered_usage.parse()?; if Settings::get().task.disable_spec_from_run_scripts { @@ -624,8 +634,12 @@ impl TaskScriptParser { // Render scripts to trigger spec collection via Tera template functions // (arg/option/flag), but discard the results. Ignore rendering errors since we only // care about collecting arg/flag definitions from the deprecated Tera syntax. - for script in scripts { - let _ = Self::render_script_with_context(&mut tera, script, &tera_ctx); + if scripts_have_template { + for script in scripts { + if contains_template_syntax(script) { + let _ = Self::render_script_with_context(&mut tera, script, &tera_ctx); + } + } } let mut cmd = usage::SpecCommand::default(); // TODO: ensure no gaps in args, e.g.: 1,2,3,4,5 @@ -660,6 +674,15 @@ impl TaskScriptParser { scripts: &[String], env: &EnvMap, ) -> Result<(Vec, usage::Spec)> { + let usage_has_template = contains_template_syntax(&task.usage); + let scripts_have_template = scripts + .iter() + .any(|script| contains_template_syntax(script)); + if !usage_has_template && !scripts_have_template { + let scripts = scripts.iter().map(|s| s.trim().to_string()).collect(); + return Ok((scripts, task.usage.trim().parse()?)); + } + let (mut tera, arg_order, input_args, input_flags) = self.setup_tera_for_spec_parsing(task); let mut tera_ctx = task.tera_ctx(config).await?; self.inject_extra_vars(&mut tera_ctx); @@ -667,15 +690,29 @@ impl TaskScriptParser { // First render the usage field to collect the spec and build a default // usage map, so that `{{ usage.* }}` references in run scripts do not // fail during this initial parsing phase (e.g. for inline tasks). - let rendered_usage = Self::render_usage_with_context(&mut tera, &task.usage, &tera_ctx)?; + let rendered_usage = if usage_has_template { + Self::render_usage_with_context(&mut tera, &task.usage, &tera_ctx)? + } else { + task.usage.trim().to_string() + }; let spec_from_field: usage::Spec = rendered_usage.parse()?; let usage_ctx = Self::make_usage_ctx_from_spec_defaults(&spec_from_field); tera_ctx.insert("usage", &usage_ctx); - let scripts = scripts - .iter() - .map(|s| Self::render_script_with_context(&mut tera, s, &tera_ctx)) - .collect::>>()?; + let scripts = if scripts_have_template { + scripts + .iter() + .map(|s| { + if contains_template_syntax(s) { + Self::render_script_with_context(&mut tera, s, &tera_ctx) + } else { + Ok(s.trim().to_string()) + } + }) + .collect::>>()? + } else { + scripts.iter().map(|s| s.trim().to_string()).collect() + }; let mut cmd = usage::SpecCommand::default(); // TODO: ensure no gaps in args, e.g.: 1,2,3,4,5 let arg_order = arg_order.lock().unwrap(); @@ -730,6 +767,10 @@ impl TaskScriptParser { let mut out: Vec = vec![]; for script in scripts { + if !contains_template_syntax(script) { + out.push(script.trim().to_string()); + continue; + } let shell_type = shell_from_shebang(script) .or(task.shell()) .unwrap_or(Settings::get().default_inline_shell()?)[0] diff --git a/src/task/task_sources.rs b/src/task/task_sources.rs index 17435472a7..712db16cb9 100644 --- a/src/task/task_sources.rs +++ b/src/task/task_sources.rs @@ -1,5 +1,6 @@ use crate::dirs; use crate::task::Task; +use crate::tera::{contains_template_syntax, render_str}; use serde::ser::{SerializeMap, SerializeSeq}; use serde::{Deserialize, Deserializer, Serialize}; use std::hash::{DefaultHasher, Hash, Hasher}; @@ -47,6 +48,23 @@ impl TaskOutputs { } } + pub fn has_tera_template(&self) -> bool { + match self { + TaskOutputs::Files(files) => files.iter().any(|file| contains_template_syntax(file)), + TaskOutputs::Auto => false, + } + } + + pub fn raw_templates_without_env(&self) -> RawOutputTemplates { + match self { + TaskOutputs::Files(files) => RawOutputTemplates { + templates: Some(files.clone()), + original_env: None, + }, + TaskOutputs::Auto => RawOutputTemplates::default(), + } + } + fn auto_path(&self, task: &Task, root: &Path) -> String { let mut hasher = DefaultHasher::new(); task.hash(&mut hasher); @@ -72,7 +90,9 @@ impl TaskOutputs { .get("env") .and_then(|v| serde_json::from_value(v.clone()).ok()); for file in files.iter_mut() { - *file = tera.render_str(file, ctx)?; + if contains_template_syntax(file) { + *file = render_str(tera, file, ctx)?; + } } Ok(RawOutputTemplates { templates: Some(raw), @@ -94,19 +114,32 @@ impl TaskOutputs { if let TaskOutputs::Files(files) = self && let Some(raw_templates) = raw.templates.as_ref() { - let mut tera = crate::tera::get_tera(Some(config_root)); - let mut ctx = tera::Context::new(); - // Start with original env from initial render, then overlay dependency env - let mut env_map = raw.original_env.clone().unwrap_or_default(); - for (k, v) in env { - env_map.insert(k.clone(), v.clone()); - } - ctx.insert("env", &env_map); - ctx.insert("config_root", &config_root.to_string_lossy().to_string()); - *files = raw_templates + if raw_templates .iter() - .map(|tmpl| tera.render_str(tmpl, &ctx)) - .collect::, _>>()?; + .any(|tmpl| contains_template_syntax(tmpl)) + { + let mut tera = crate::tera::get_tera(Some(config_root)); + let mut ctx = tera::Context::new(); + // Start with original env from initial render, then overlay dependency env + let mut env_map = raw.original_env.clone().unwrap_or_default(); + for (k, v) in env { + env_map.insert(k.clone(), v.clone()); + } + ctx.insert("env", &env_map); + ctx.insert("config_root", &config_root.to_string_lossy().to_string()); + *files = raw_templates + .iter() + .map(|tmpl| { + if contains_template_syntax(tmpl) { + render_str(&mut tera, tmpl, &ctx) + } else { + Ok(tmpl.clone()) + } + }) + .collect::, _>>()?; + } else { + *files = raw_templates.clone(); + } } Ok(()) } diff --git a/src/tera.rs b/src/tera.rs index 8a1cec282c..04ebdb1b51 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -42,6 +42,31 @@ pub fn take_tera_accessed_files() -> Vec { files } +/// Fast marker check for Tera 1.x syntax. +/// +/// Tera 1.20.1's grammar starts every variable, tag, and comment block with +/// `{{`, `{%`, or `{#` respectively, including whitespace-trimmed forms like +/// `{{-`, `{%-`, and `{#-`. +pub fn contains_template_syntax(input: &str) -> bool { + input.contains("{{") || input.contains("{%") || input.contains("{#") +} + +pub fn render_str_if_template( + tera: &mut Tera, + input: &str, + context: &Context, +) -> tera::Result { + if contains_template_syntax(input) { + render_str(tera, input, context) + } else { + Ok(input.to_string()) + } +} + +pub fn render_str(tera: &mut Tera, input: &str, context: &Context) -> tera::Result { + tera.render_str(input, context) +} + pub static BASE_CONTEXT: Lazy = Lazy::new(|| { let mut context = Context::new(); context.insert("env", &*env::PRISTINE_ENV); @@ -818,6 +843,38 @@ mod tests { assert_eq!(s.trim(), "ok"); } + #[test] + fn test_contains_template_syntax() { + assert!(contains_template_syntax("{{ foo }}")); + assert!(contains_template_syntax("{{- foo -}}")); + assert!(contains_template_syntax("{% if foo %}bar{% endif %}")); + assert!(contains_template_syntax("{%- if foo -%}bar{%- endif -%}")); + assert!(contains_template_syntax("{# comment #}")); + assert!(contains_template_syntax("{#- comment -#}")); + assert!(!contains_template_syntax("plain text")); + } + + #[test] + fn test_render_str_if_template_skips_plain_text() { + let mut tera = Tera::default(); + let ctx = Context::new(); + assert_eq!( + render_str_if_template(&mut tera, "plain text", &ctx).unwrap(), + "plain text" + ); + } + + #[test] + fn test_render_str_if_template_renders_template() { + let mut tera = Tera::default(); + let mut ctx = Context::new(); + ctx.insert("name", "world"); + assert_eq!( + render_str_if_template(&mut tera, "hello {{ name }}", &ctx).unwrap(), + "hello world" + ); + } + #[tokio::test] #[cfg(unix)] async fn test_read_file() { @@ -837,15 +894,17 @@ mod tests { tera_ctx.insert("cwd", temp_dir.path().to_str().unwrap()); let mut tera = get_tera(Some(temp_dir.path())); - let s = tera - .render_str(r#"{{ read_file(path="test.txt") }}"#, &tera_ctx) + let s = render_str_if_template(&mut tera, r#"{{ read_file(path="test.txt") }}"#, &tera_ctx) .unwrap(); assert_eq!(s, "test content\nwith multiple lines"); // Test with trim filter - let s = tera - .render_str(r#"{{ read_file(path="test.txt") | trim }}"#, &tera_ctx) - .unwrap(); + let s = render_str_if_template( + &mut tera, + r#"{{ read_file(path="test.txt") | trim }}"#, + &tera_ctx, + ) + .unwrap(); assert_eq!(s, "test content\nwith multiple lines"); } @@ -855,6 +914,6 @@ mod tests { tera_ctx.insert("config_root", &config_root); tera_ctx.insert("cwd", "/"); let mut tera = get_tera(Option::from(config_root)); - tera.render_str(s, &tera_ctx).unwrap() + render_str_if_template(&mut tera, s, &tera_ctx).unwrap() } }