diff --git a/TriasDev.Templify.Tests/Conditionals/CollectBareOperandVariablesTests.cs b/TriasDev.Templify.Tests/Conditionals/CollectBareOperandVariablesTests.cs
new file mode 100644
index 0000000..e3c79d5
--- /dev/null
+++ b/TriasDev.Templify.Tests/Conditionals/CollectBareOperandVariablesTests.cs
@@ -0,0 +1,43 @@
+// Copyright (c) 2026 TriasDev GmbH & Co. KG
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+
+using TriasDev.Templify.Conditionals;
+
+namespace TriasDev.Templify.Tests.Conditionals;
+
+///
+/// finds the variables evaluated for truthiness on
+/// their own.
+///
+public sealed class CollectBareOperandVariablesTests
+{
+ [Theory]
+ [InlineData("A", "A")]
+ [InlineData("not A", "A")]
+ [InlineData("A and B", "A,B")]
+ [InlineData("A or not B", "A,B")]
+ [InlineData("(A or B) and not (C and D)", "A,B,C,D")]
+ [InlineData("A and Status = \"x\"", "A")]
+ [InlineData("Customer.Name", "Customer.Name")]
+ [InlineData("Items[0].Flag or [Exists]", "Items[0].Flag,Exists")]
+ [InlineData("(A and B) = true", "A,B")]
+ [InlineData("not (A = B)", "")]
+ [InlineData("A exists", "")]
+ [InlineData("A is empty or B is not empty", "")]
+ [InlineData("Status = Active", "")]
+ [InlineData("A != B", "")]
+ [InlineData("A > 1", "")]
+ [InlineData("A in (B, C)", "")]
+ [InlineData("A contains B", "")]
+ [InlineData("A startswith B or C endswith D", "")]
+ [InlineData("true and not null", "")]
+ [InlineData("\"text\"", "")]
+ public void CollectsBareOperands(string expression, string expected)
+ {
+ IEnumerable paths = ConditionalEvaluator
+ .CollectBareOperandVariables(ConditionalEvaluator.Parse(expression))
+ .Select(v => v.Path);
+
+ Assert.Equal(expected, string.Join(",", paths));
+ }
+}
diff --git a/TriasDev.Templify.Tests/Integration/ValidationMissingConditionVariableTests.cs b/TriasDev.Templify.Tests/Integration/ValidationMissingConditionVariableTests.cs
new file mode 100644
index 0000000..62f4906
--- /dev/null
+++ b/TriasDev.Templify.Tests/Integration/ValidationMissingConditionVariableTests.cs
@@ -0,0 +1,256 @@
+// Copyright (c) 2026 TriasDev GmbH & Co. KG
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+
+using TriasDev.Templify.Core;
+using TriasDev.Templify.Tests.Helpers;
+
+namespace TriasDev.Templify.Tests.Integration;
+
+///
+/// ValidateTemplate(template, data) warns ( ) when a
+/// condition evaluates a variable that is missing from the data for truthiness on its own. Every case is validated as
+/// a Word and as an OpenDocument template, directly and through the facade.
+///
+public sealed class ValidationMissingConditionVariableTests
+{
+ private static Dictionary Data() => new Dictionary
+ {
+ ["A"] = true,
+ ["Status"] = "Active",
+ ["Role"] = "Admin",
+ ["Global"] = "g",
+ ["Items"] = new List>
+ {
+ new() { ["Title"] = "i1", ["Active"] = true },
+ new() { ["Title"] = "i2" },
+ },
+ ["Empty"] = new List(),
+ };
+
+ /// Validates the paragraphs in both formats and both entry points; returns the Word result.
+ private static ValidationResult Validate(Dictionary? data, params string[] paragraphs)
+ {
+ DocumentBuilder docx = new DocumentBuilder();
+ OdtDocumentBuilder odt = new OdtDocumentBuilder();
+ foreach (string paragraph in paragraphs)
+ {
+ docx.AddParagraph(paragraph);
+ odt.AddParagraph(paragraph);
+ }
+
+ return ValidateAll(docx.ToStream().ToArray(), odt.ToBytes(), data);
+ }
+
+ private static ValidationResult ValidateAll(byte[] docxBytes, byte[] odtBytes, Dictionary? data)
+ {
+ ValidationResult word = data == null
+ ? new DocumentTemplateProcessor().ValidateTemplate(new MemoryStream(docxBytes))
+ : new DocumentTemplateProcessor().ValidateTemplate(new MemoryStream(docxBytes), data);
+ ValidationResult[] others = data == null
+ ? new[]
+ {
+ new OdtTemplateProcessor().ValidateTemplate(new MemoryStream(odtBytes)),
+ new TemplateProcessor().ValidateTemplate(new MemoryStream(docxBytes)),
+ new TemplateProcessor().ValidateTemplate(new MemoryStream(odtBytes)),
+ }
+ : new[]
+ {
+ new OdtTemplateProcessor().ValidateTemplate(new MemoryStream(odtBytes), data),
+ new TemplateProcessor().ValidateTemplate(new MemoryStream(docxBytes), data),
+ new TemplateProcessor().ValidateTemplate(new MemoryStream(odtBytes), data),
+ };
+
+ foreach (ValidationResult other in others)
+ {
+ Assert.Equal(Describe(word.Warnings), Describe(other.Warnings));
+ Assert.Equal(word.Errors.Select(e => e.Message).Order(), other.Errors.Select(e => e.Message).Order());
+ Assert.Equal(word.MissingVariables, other.MissingVariables);
+ }
+
+ return word;
+ }
+
+ private static List Describe(IEnumerable warnings) =>
+ warnings.Select(w => $"{w.Type}: {w.Message} @ {w.Location}").ToList();
+
+ private static List ConditionWarnings(ValidationResult result) =>
+ result.Warnings.Where(w => w.Type == ValidationWarningType.MissingConditionVariable).ToList();
+
+ public static TheoryData WarningCases => new TheoryData
+ {
+ { "Missing", new[] { "{{#if Missing}}", "x", "{{/if}}" }, "{{#if Missing}}" },
+ { "Missing", new[] { "{{#if not Missing}}", "x", "{{/if}}" }, "{{#if not Missing}}" },
+ { "Missing", new[] { "{{#if A and Missing}}", "x", "{{/if}}" }, "{{#if A and Missing}}" },
+ { "Missing", new[] { "{{#if Missing or Status = \"Active\"}}", "x", "{{/if}}" }, "{{#if Missing or Status = \"Active\"}}" },
+ { "Missing", new[] { "{{#if (A or Missing) and not A}}", "x", "{{/if}}" }, "{{#if (A or Missing) and not A}}" },
+ { "Missing", new[] { "{{#if A}}", "a", "{{#elseif Missing}}", "b", "{{/if}}" }, "{{#elseif Missing}}" },
+ { "Missing", new[] { "Text {{#if Missing}}x{{/if}} end" }, "{{#if Missing}}" },
+ { "Customer.Name", new[] { "{{#if Customer.Name}}", "x", "{{/if}}" }, "{{#if Customer.Name}}" },
+ };
+
+ [Theory]
+ [MemberData(nameof(WarningCases))]
+ public void BareMissingOperand_Warns(string variable, string[] paragraphs, string location)
+ {
+ ValidationResult result = Validate(Data(), paragraphs);
+
+ ValidationWarning warning = Assert.Single(ConditionWarnings(result));
+ Assert.Contains($"'{variable}'", warning.Message);
+ Assert.Contains($"Condition '{location[(location.IndexOf(' ') + 1)..^2]}'", warning.Message);
+ Assert.Equal(location, warning.Location);
+
+ // Warning only: no error, no missing variable, still valid.
+ Assert.True(result.IsValid, string.Join("; ", result.Errors.Select(e => e.Message)));
+ Assert.Empty(result.MissingVariables);
+ }
+
+ public static TheoryData NoWarningConditions => new TheoryData
+ {
+ "A",
+ "not A",
+ "A and Status = \"Active\"",
+ "Missing exists",
+ "not (Missing exists)",
+ "Missing is empty",
+ "Missing is not empty",
+ "A and Missing is empty",
+ "Status = Active",
+ "Missing = \"x\"",
+ "Missing != Other",
+ "Role in (\"Admin\", \"Owner\")",
+ "Missing in (\"Admin\", Other)",
+ "Missing contains \"x\"",
+ "Status startswith Missing",
+ "true",
+ "not null",
+ "\"text\"",
+ "1",
+ };
+
+ [Theory]
+ [MemberData(nameof(NoWarningConditions))]
+ public void NonBareOrResolvedOperand_DoesNotWarn(string condition)
+ {
+ ValidationResult result = Validate(Data(), $"{{{{#if {condition}}}}}", "x", "{{/if}}");
+
+ Assert.Empty(ConditionWarnings(result));
+ }
+
+ [Fact]
+ public void WithoutData_DoesNotWarn()
+ {
+ ValidationResult result = Validate(null, "{{#if Missing}}", "x", "{{/if}}");
+
+ Assert.Empty(result.Warnings);
+ Assert.True(result.IsValid);
+ }
+
+ [Fact]
+ public void InvalidCondition_IsAnErrorOnly()
+ {
+ ValidationResult result = Validate(Data(), "{{#if Missing and}}", "x", "{{/if}}");
+
+ Assert.Empty(ConditionWarnings(result));
+ Assert.Contains(result.Errors, e => e.Type == ValidationErrorType.InvalidConditionalExpression);
+ }
+
+ [Fact]
+ public void SameConditionTwice_WarnsOnce()
+ {
+ ValidationResult result = Validate(
+ Data(),
+ "{{#if Missing}}", "x", "{{/if}}",
+ "{{#if Missing}}", "y", "{{/if}}");
+
+ Assert.Single(ConditionWarnings(result));
+ }
+
+ [Fact]
+ public void SeveralMissingOperands_WarnEach()
+ {
+ ValidationResult result = Validate(Data(), "{{#if First or not Second}}", "x", "{{/if}}");
+
+ Assert.Equal(
+ new[] { "First", "Second" },
+ ConditionWarnings(result).Select(w => w.Message.Contains("'First'") ? "First" : "Second").Order());
+ }
+
+ [Fact]
+ public void InLoop_ItemPropertiesNamedVariablesMetadataAndGlobals_Resolve()
+ {
+ ValidationResult result = Validate(
+ Data(),
+ "{{#foreach item in Items}}",
+ "{{#if item.Active}}a{{/if}} {{#if Active}}b{{/if}} {{#if item}}c{{/if}} {{#if Title and Global}}d{{/if}}",
+ "{{#if @first}}e{{/if}} {{#if not @last and @index}}f{{/if}} {{#if this}}g{{/if}}",
+ "{{/foreach}}");
+
+ Assert.Empty(ConditionWarnings(result));
+ Assert.True(result.IsValid, string.Join("; ", result.Errors.Select(e => e.Message)));
+ }
+
+ [Fact]
+ public void InLoop_MissingItemPropertyOrVariable_Warns()
+ {
+ ValidationResult result = Validate(
+ Data(),
+ "{{#foreach item in Items}}",
+ "{{#if item.Nope}}", "a", "{{/if}}",
+ "{{#if Nope}}", "b", "{{/if}}",
+ "{{/foreach}}");
+
+ Assert.Equal(
+ new[] { "Condition 'Nope' uses variable 'Nope'", "Condition 'item.Nope' uses variable 'item.Nope'" },
+ ConditionWarnings(result).Select(w => w.Message[..w.Message.IndexOf(", which", StringComparison.Ordinal)]).Order(StringComparer.Ordinal));
+ }
+
+ [Fact]
+ public void ItemPropertyOutsideTheLoop_Warns()
+ {
+ ValidationResult result = Validate(
+ Data(),
+ "{{#foreach Items}}", "{{#if Active}}a{{/if}}", "{{/foreach}}",
+ "{{#if Active}}", "b", "{{/if}}");
+
+ Assert.Single(ConditionWarnings(result));
+ }
+
+ [Fact]
+ public void InEmptyLoop_IsNotChecked()
+ {
+ ValidationResult result = Validate(Data(), "{{#foreach Empty}}", "{{#if Nope}}x{{/if}}", "{{/foreach}}");
+
+ Assert.Empty(ConditionWarnings(result));
+ }
+
+ [Fact]
+ public void InlineExpression_IsNotChecked()
+ {
+ ValidationResult result = Validate(Data(), "{{(Missing and A)}} {{(not Missing):yesno}}");
+
+ Assert.Empty(ConditionWarnings(result));
+ }
+
+ [Fact]
+ public void TableRowConditionalAndHeader_Warn()
+ {
+ DocumentBuilder docx = new DocumentBuilder();
+ docx.AddTable(3, 1, (row, _) => row switch
+ {
+ 0 => "{{#if RowFlag}}",
+ 1 => "x",
+ _ => "{{/if}}"
+ });
+ docx.AddHeader("{{#if HeaderFlag}}h{{/if}}");
+
+ OdtDocumentBuilder odt = new OdtDocumentBuilder();
+ odt.AddTable(new[] { "{{#if RowFlag}}" }, new[] { "x" }, new[] { "{{/if}}" });
+ odt.AddHeaderParagraph("{{#if HeaderFlag}}h{{/if}}");
+
+ ValidationResult result = ValidateAll(docx.ToStream().ToArray(), odt.ToBytes(), Data());
+
+ Assert.Equal(2, ConditionWarnings(result).Count);
+ Assert.Contains(result.Warnings, w => w.Message.Contains("'RowFlag'"));
+ Assert.Contains(result.Warnings, w => w.Message.Contains("'HeaderFlag'"));
+ }
+}
diff --git a/TriasDev.Templify.Tests/Odt/OdtValidationParityTests.cs b/TriasDev.Templify.Tests/Odt/OdtValidationParityTests.cs
index 540a61d..1773864 100644
--- a/TriasDev.Templify.Tests/Odt/OdtValidationParityTests.cs
+++ b/TriasDev.Templify.Tests/Odt/OdtValidationParityTests.cs
@@ -44,6 +44,15 @@ public sealed class OdtValidationParityTests
{ "inline conditional", new[] { "{{Name}}{{#if Flag}} (flag {{Title}}){{/if}}" } },
{ "nested invalid iteration variable", new[] { "{{#foreach Items}}", "{{#foreach in in Tags}}", "x", "{{/foreach}}", "{{/foreach}}" } },
{ "unmatched loop with body", new[] { "{{Name}} {{Missing}}", "{{#foreach Items}}", "{{Title}} {{Nope}}" } },
+ {
+ "missing bare condition operands",
+ new[]
+ {
+ "{{#if Nope}}", "a", "{{#elseif not Other and A}}", "b", "{{/if}}",
+ "x {{#if Absent or Missing exists}}y{{/if}} {{#if Nope is empty or Status = Nope}}z{{/if}}",
+ "{{#foreach item in Items}}", "{{#if item.Active and not item.Nope and Title and @first}}", "c", "{{/if}}", "{{/foreach}}",
+ }
+ },
{ "nested unmatched conditional in loop", new[] { "{{#foreach Items}}", "{{#if Active}}", "{{Title}}", "{{/foreach}}" } },
};
diff --git a/TriasDev.Templify/ARCHITECTURE.md b/TriasDev.Templify/ARCHITECTURE.md
index 7f70847..94c9dfb 100644
--- a/TriasDev.Templify/ARCHITECTURE.md
+++ b/TriasDev.Templify/ARCHITECTURE.md
@@ -209,7 +209,9 @@ expression string
- **`Core/TemplateValidator.cs`** (`ValidateTemplate`) walks body, headers/footers and notes: it reports unmatched
markers and invalid conditions (`ValidationError`), collects `AllPlaceholders` (condition variables are taken from the
AST), and with data reports `MissingVariables` plus warnings (`EmptyLoopCollection` when
- `WarnOnEmptyLoopCollections` is on, `ReservedWordAsVariable`). Missing variables are checked by
+ `WarnOnEmptyLoopCollections` is on, `ReservedWordAsVariable`, and `MissingConditionVariable` for bare condition
+ operands missing from the data, found in the AST by `ConditionalEvaluator.CollectBareOperandVariables` and checked
+ in loop scope by `Core/MissingConditionVariableCheck.cs`). Missing variables are checked by
`Core/ScopedVariableValidator.cs`, which traverses containers like the walker (body loops, table row loops per table
and per loop row list, cells, nested tables, text boxes, content controls) and resolves names inside a loop against
the items of that loop and its enclosing loops (implicit properties, named iteration variables, metadata).
diff --git a/TriasDev.Templify/Conditionals/ConditionalEvaluator.cs b/TriasDev.Templify/Conditionals/ConditionalEvaluator.cs
index d17fa7e..bfb20a7 100644
--- a/TriasDev.Templify/Conditionals/ConditionalEvaluator.cs
+++ b/TriasDev.Templify/Conditionals/ConditionalEvaluator.cs
@@ -312,6 +312,44 @@ internal static Engine.ConditionNode Parse(string expression)
}
}
+ ///
+ /// Collects the variables of a parsed expression that are evaluated for truthiness on their own: the whole
+ /// expression (Flag ) and the operands of and , or and not .
+ ///
+ ///
+ /// Operands of comparisons (Status = Active , where a missing bareword falls back to a string literal),
+ /// of exists / is empty / is not empty (designed for missing values), of in and of
+ /// the string operators are not bare operands. Nested operator nodes are always descended into, so the
+ /// operands of a logical operator nested in a comparison ((A and B) = true ) are still found.
+ ///
+ internal static IEnumerable CollectBareOperandVariables(Engine.ConditionNode node)
+ {
+ return Collect(node, isBooleanContext: true);
+
+ static IEnumerable Collect(Engine.ConditionNode node, bool isBooleanContext)
+ {
+ switch (node)
+ {
+ case Engine.VariableNode variable when isBooleanContext:
+ yield return variable;
+ break;
+ case Engine.OperatorNode op:
+ bool isLogical = op.Operator is Engine.Operators.AndOperator
+ or Engine.Operators.OrOperator
+ or Engine.Operators.NotOperator;
+ foreach (Engine.ConditionNode operand in op.Operands)
+ {
+ foreach (Engine.VariableNode v in Collect(operand, isLogical))
+ {
+ yield return v;
+ }
+ }
+
+ break;
+ }
+ }
+ }
+
///
/// Evaluates a conditional expression (backward compatibility bridge).
///
diff --git a/TriasDev.Templify/Core/MissingConditionVariableCheck.cs b/TriasDev.Templify/Core/MissingConditionVariableCheck.cs
new file mode 100644
index 0000000..d5c20c8
--- /dev/null
+++ b/TriasDev.Templify/Core/MissingConditionVariableCheck.cs
@@ -0,0 +1,66 @@
+// Copyright (c) 2026 TriasDev GmbH & Co. KG
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+
+using System.Text.RegularExpressions;
+using TriasDev.Templify.Conditionals;
+
+namespace TriasDev.Templify.Core;
+
+///
+/// Reports the variables that a condition evaluates for truthiness on their own (bare operands, e.g.
+/// {{#if Missing}} or {{#if A and not Missing}} ) and that cannot be resolved in the data.
+/// Shared by the Word and the OpenDocument validators, which supply the loop-scoped resolution.
+///
+///
+/// A missing bare operand silently evaluates to , which usually hides a typo or a data
+/// problem, so validation with data reports a
+/// warning. The warning is not a error and does not affect
+/// or .
+///
+internal static class MissingConditionVariableCheck
+{
+ ///
+ /// Checks the {{#if}} /{{#elseif}} conditions in (block, inline and
+ /// table-row markers) and adds one warning per condition and missing variable.
+ ///
+ /// The text of a paragraph.
+ /// Resolves a variable path in the current (loop) scope.
+ /// The warnings; a warning that is already present is not added again.
+ public static void Check(string text, Func canResolve, List warnings)
+ {
+ if (text.IndexOf("{{#", StringComparison.Ordinal) < 0)
+ {
+ return;
+ }
+
+ ConditionalEvaluator evaluator = new ConditionalEvaluator();
+ foreach (Match match in ConditionalPatterns.IfStart.Matches(text).Concat(ConditionalPatterns.ElseIf.Matches(text)))
+ {
+ string expression = match.Groups[1].Value.Trim();
+
+ // Invalid conditions are reported as errors by the syntax validation.
+ if (!evaluator.Validate(expression).IsValid)
+ {
+ continue;
+ }
+
+ foreach (Conditionals.Engine.VariableNode variable in ConditionalEvaluator.CollectBareOperandVariables(ConditionalEvaluator.Parse(expression)))
+ {
+ string path = variable.Path;
+
+ // Loop metadata and the current item are resolved by the loop, not by the data.
+ if (path.StartsWith('@') || path.StartsWith('.') || path == "this" || canResolve(path))
+ {
+ continue;
+ }
+
+ string message = $"Condition '{expression}' uses variable '{path}', which is not provided in the data. " +
+ "It is treated as false.";
+ if (!warnings.Any(w => w.Type == ValidationWarningType.MissingConditionVariable && w.Message == message))
+ {
+ warnings.Add(ValidationWarning.Create(ValidationWarningType.MissingConditionVariable, message, match.Value));
+ }
+ }
+ }
+ }
+}
diff --git a/TriasDev.Templify/Core/ScopedVariableValidator.cs b/TriasDev.Templify/Core/ScopedVariableValidator.cs
index abe899d..453cc98 100644
--- a/TriasDev.Templify/Core/ScopedVariableValidator.cs
+++ b/TriasDev.Templify/Core/ScopedVariableValidator.cs
@@ -219,6 +219,8 @@ private void ValidateText(string text)
$"Variable '{placeholder}' is referenced in the template but not provided in the data.");
}
}
+
+ MissingConditionVariableCheck.Check(text, CanResolveInScope, _warnings);
}
private void ReportMissing(string variable, string message)
diff --git a/TriasDev.Templify/Core/ValidationResult.cs b/TriasDev.Templify/Core/ValidationResult.cs
index d63554c..4ca6b4b 100644
--- a/TriasDev.Templify/Core/ValidationResult.cs
+++ b/TriasDev.Templify/Core/ValidationResult.cs
@@ -231,5 +231,18 @@ public enum ValidationWarningType
/// variable with that name. It is evaluated as the variable, but the bracketed form ([Exists] ) is
/// recommended because the word is also an operator.
///
- ReservedWordAsVariable
+ ReservedWordAsVariable,
+
+ ///
+ /// A condition evaluates a variable for truthiness on its own (a bare operand, e.g. {{#if Missing}} ,
+ /// {{#if not Missing}} or {{#if A and Missing}} ) and the variable is not provided in the data,
+ /// so it is treated as . Reported only when the template is validated with data.
+ ///
+ ///
+ /// Operands of exists , is empty and is not empty (designed for missing values) and
+ /// comparison operands (Status = Active , where an unknown bareword is a string literal) are not
+ /// reported. The variable is not added to and the warning does
+ /// not affect .
+ ///
+ MissingConditionVariable = 2
}
diff --git a/TriasDev.Templify/Examples.md b/TriasDev.Templify/Examples.md
index 2f5543b..f07649d 100644
--- a/TriasDev.Templify/Examples.md
+++ b/TriasDev.Templify/Examples.md
@@ -1592,7 +1592,7 @@ Console.WriteLine($"Missing: {string.Join(", ", validation.MissingVariables)}");
foreach (ValidationWarning warning in validation.Warnings)
{
- // e.g. EmptyLoopCollection (see WarnOnEmptyLoopCollections), ReservedWordAsVariable
+ // e.g. EmptyLoopCollection (see WarnOnEmptyLoopCollections), ReservedWordAsVariable, MissingConditionVariable
Console.WriteLine($"{warning.Type}: {warning.Message}");
}
```
diff --git a/TriasDev.Templify/OpenDocument/OdtTemplateValidator.cs b/TriasDev.Templify/OpenDocument/OdtTemplateValidator.cs
index 4e2fb80..cf5ec4c 100644
--- a/TriasDev.Templify/OpenDocument/OdtTemplateValidator.cs
+++ b/TriasDev.Templify/OpenDocument/OdtTemplateValidator.cs
@@ -486,6 +486,8 @@ private void ValidateText(string text)
$"Variable '{placeholder}' is referenced in the template but not provided in the data.");
}
}
+
+ MissingConditionVariableCheck.Check(text, CanResolveInScope, _warnings);
}
private void ReportMissing(string variable, string message)
diff --git a/TriasDev.Templify/PublicAPI.Unshipped.txt b/TriasDev.Templify/PublicAPI.Unshipped.txt
index 266dc17..3f667fc 100644
--- a/TriasDev.Templify/PublicAPI.Unshipped.txt
+++ b/TriasDev.Templify/PublicAPI.Unshipped.txt
@@ -33,3 +33,4 @@ TriasDev.Templify.Core.TemplateProcessor.ValidateTemplate(System.IO.Stream! temp
TriasDev.Templify.Core.TemplateProcessor.ValidateTemplate(System.IO.Stream! templateStream, System.Collections.Generic.IReadOnlyDictionary! data) -> TriasDev.Templify.Core.ValidationResult!
static TriasDev.Templify.Core.TemplateProcessor.DetectFormat(System.IO.Stream! templateStream) -> TriasDev.Templify.Core.TemplateFormat
TriasDev.Templify.Core.ValidationErrorType.InvalidDocument = 7 -> TriasDev.Templify.Core.ValidationErrorType
+TriasDev.Templify.Core.ValidationWarningType.MissingConditionVariable = 2 -> TriasDev.Templify.Core.ValidationWarningType
diff --git a/TriasDev.Templify/README.md b/TriasDev.Templify/README.md
index c17509b..ca27585 100644
--- a/TriasDev.Templify/README.md
+++ b/TriasDev.Templify/README.md
@@ -1356,7 +1356,7 @@ Result of template processing operation.
- `IsValid` (no errors), `Errors` (`ValidationError` with `Type`, `Message`, `Location`); when data is passed, each missing variable is an error of type `MissingVariable`; a template that cannot be read (unsupported format, corrupted package) passed to `TemplateProcessor` or `OdtTemplateProcessor` is an error of type `InvalidDocument`
- `AllPlaceholders`, `MissingVariables`
-- `Warnings` (`ValidationWarning` of type `EmptyLoopCollection` or `ReservedWordAsVariable`)
+- `Warnings` (`ValidationWarning` of type `EmptyLoopCollection`, `ReservedWordAsVariable` or `MissingConditionVariable`: a condition tests a variable missing from the data on its own, e.g. `{{#if Missing}}`; not added to `MissingVariables`, `IsValid` is unchanged)
### TextTemplateProcessor
diff --git a/docs/for-developers/condition-evaluation.md b/docs/for-developers/condition-evaluation.md
index 0323a36..3466a71 100644
--- a/docs/for-developers/condition-evaluation.md
+++ b/docs/for-developers/condition-evaluation.md
@@ -223,7 +223,9 @@ evaluator.Evaluate("[Empty] = \"yes\" and not [Not]", data);
evaluator.Evaluate("[Exists].Count > 0", data);
```
-`ValidateTemplate(template, data)` reports a `ReservedWordAsVariable` warning when a condition uses a bare keyword as a variable that exists in the data, recommending the bracketed form.
+`ValidateTemplate(template, data)` reports a `ReservedWordAsVariable` warning when a condition uses a bare keyword as a variable that exists in the data, recommending the bracketed form. It reports a `MissingConditionVariable` warning when a condition tests a variable
+that is not in the data for truthiness on its own (`{{#if Missing}}`, `{{#if A and not Missing}}`); operands of
+comparisons, `exists` and `is empty` are not reported.
**Quote string literals** that could collide with a keyword (`= "empty"` rather than `= empty`).
diff --git a/docs/for-developers/quick-start.md b/docs/for-developers/quick-start.md
index b211a2b..9ac09d4 100644
--- a/docs/for-developers/quick-start.md
+++ b/docs/for-developers/quick-start.md
@@ -222,7 +222,26 @@ Console.WriteLine(string.Join(", ", validation.MissingVariables));
Each missing variable or loop collection is reported as an error of type `MissingVariable` (so `IsValid` is
`false` for that data) and listed in `MissingVariables`; call `ValidateTemplate(template)` without data to check
the syntax only. `validation.Warnings` reports loops over empty collections (`EmptyLoopCollection`, controlled by
-`WarnOnEmptyLoopCollections`) and condition keywords used as variable names (`ReservedWordAsVariable`).
+`WarnOnEmptyLoopCollections`), condition keywords used as variable names (`ReservedWordAsVariable`) and conditions
+that test a variable missing from the data on its own (`MissingConditionVariable`, see below). Warnings never make
+`IsValid` false.
+
+`ValidationWarning.Type` is one of these `ValidationWarningType` values:
+
+| Type | Meaning |
+|------|---------|
+| `EmptyLoopCollection` | A loop collection is empty, so the loop content could not be checked (only with `WarnOnEmptyLoopCollections`) |
+| `ReservedWordAsVariable` | A condition uses a bare keyword (`Exists`, `Empty`, ...) as a variable that is in the data; write `[Exists]` |
+| `MissingConditionVariable` | A condition tests a variable that is not in the data for truthiness on its own, so it is treated as false |
+
+`MissingConditionVariable` covers variables used as bare operands: the whole condition (`{{#if Missing}}`,
+`{{#elseif Missing}}`, inline `{{#if Missing}}...{{/if}}`) and the operands of `and`, `or` and `not`
+(`{{#if not Missing}}`, `{{#if A and Missing}}`). Inside loops, item properties, named iteration variables and loop
+metadata (`@first`, ...) are resolved as in processing. It is not reported for the operands of `exists`, `is empty`
+and `is not empty` (they are meant for missing values), for comparison operands (`Status = Active` compares with the
+text `Active` when there is no such variable) or for the other operators, and not for inline `{{(...)}}` expressions.
+The variable is not added to `MissingVariables` and is not a `MissingVariable` error, so existing validation code
+keeps its result. The warning is the same for Word and OpenDocument templates.
`ValidationError.Type` is one of these `ValidationErrorType` values:
diff --git a/docs/for-template-authors/conditionals.md b/docs/for-template-authors/conditionals.md
index 2b90619..e7c36b9 100644
--- a/docs/for-template-authors/conditionals.md
+++ b/docs/for-template-authors/conditionals.md
@@ -18,6 +18,8 @@ Conditionals let you show or hide content in your document based on data values.
**Falsy values** (content is hidden): missing/`null`, `false`, empty or whitespace text, the text `"false"` or `"0"` (any casing, e.g. `"False"`), **any numeric zero** (`0`, `0.0`, `0.00`, JSON `0.0`, and zero of every numeric type such as `decimal` or `long`), `NaN`, and empty lists. Everything else is truthy, including any other text (`"no"` is truthy!).
+> **Misspelled or missing variables** are silently false: `{{#if IsActiv}}` never shows its content. When a developer validates the template with data (`ValidateTemplate`), a condition that tests a missing variable on its own (`{{#if Missing}}`, `{{#if not Missing}}`, `{{#if A and Missing}}`, also in `{{#elseif}}`) produces a `MissingConditionVariable` warning. Use `{{#if Missing exists}}` or `{{#if Missing is empty}}` when a variable is meant to be optional: those checks are not reported.
+
> **Markers in their own paragraphs:** in the block form, each marker (`{{#if}}`, `{{#elseif}}`, `{{#else}}`, `{{/if}}`) should be alone in its paragraph. A marker paragraph is removed completely, so any other text in it is lost. To change only part of a paragraph, put all markers in that paragraph ([inline conditionals](#inline-conditionals)).
>
> Marker keywords are case-insensitive (`{{#IF}}` works), but variable names are not.