Scriban Templates in LCDPossible
This skill covers Scriban template usage in C# interpolated strings for the LCDPossible panel system.
CRITICAL RULES - Read First
Rule 1: Count Your Braces
This is the #1 source of errors. Before writing ANY template:
| What You Want | Brace Count in C# | Example |
|---|---|---|
| C# interpolation | 1 | {myVar} |
| CSS literal brace | 2 | {{ and }} |
| Scriban expression | 4 | {{{{ and }}}} |
Rule 2: Scriban is NOT Liquid/Jinja
These features DO NOT exist in Scriban:
- •
| rawfilter - Scriban doesn't HTML-encode by default - •
| escapefilter - Different syntax - •
{% %}blocks - Scriban uses{{ }}
Rule 3: The Golden Pattern
private string MyTemplate => $@"
<style>
.class {{ /* CSS: 2 braces for literal {{ */
padding: {Padding}px; /* C#: 1 brace for interpolation */
}}
</style>
<body>
{{{{ for x in data }}}} /* Scriban: 4 braces for {{ */
<div>{{{{ x.name }}}}</div>
{{{{ end }}}}
</body>";
Pre-Flight Checklist
Before writing a template, verify:
- • CSS blocks use 2 braces:
body {{notbody {{{{ - • Scriban uses 4 braces:
{{{{ for }}}}not{{ for }} - • C# vars use 1 brace:
{PanelPadding}not{{PanelPadding}} - • No
| rawfilter: Just{{{{ widget.html }}}}not{{{{ widget.html | raw }}}} - • Closing braces match: Count opening and closing braces
DO vs DON'T Examples
CSS Blocks
// ✅ DO - CSS needs literal { in output, so use 2 braces
$@"body {{
margin: 0;
}}"
// ❌ DON'T - 4 braces produces {{ which Scriban tries to parse
$@"body {{{{
margin: 0;
}}}}"
Scriban Expressions
// ✅ DO - Scriban expressions need {{ }} in output, so use 4 braces
$@"{{{{ for item in data.items }}}}"
// ❌ DON'T - 2 braces produces { which is just a literal
$@"{{ for item in data.items }}"
// ❌ DON'T - 6 braces produces {{{ which is invalid
$@"{{{{{{ for item in data.items }}}}}}"
Output Without Encoding
// ✅ DO - Scriban outputs raw strings by default
$@"{{{{ widget.html }}}}"
// ❌ DON'T - There is no raw filter in Scriban
$@"{{{{ widget.html | raw }}}}"
Mixed C# and Scriban
// ✅ DO - C# interpolation with 1 brace, Scriban with 4
$@"<style>
.grid {{
gap: {GridGap}px; /* C# interpolation */
columns: {{{{ data.cols }}}}; /* Scriban expression */
}}
</style>"
// ❌ DON'T - Mixing up brace counts
$@"<style>
.grid {{{{
gap: {{GridGap}}px; /* WRONG: GridGap is C#, not Scriban */
}}}}
</style>"
Decision Tree: How Many Braces?
What are you outputting?
│
├─ CSS/JavaScript literal brace { or }
│ └─ Use 2 braces: {{ or }}
│
├─ Scriban expression {{ something }}
│ └─ Use 4 braces: {{{{ something }}}}
│
├─ C# variable from the class
│ └─ Use 1 brace: {MyProperty}
│
└─ Literal text with no braces
└─ Just write it: <div>text</div>
Quick Reference: Brace Escaping
When using Scriban templates inside C# interpolated strings ($"..."):
| In C# String | Output | Use For |
|---|---|---|
{variable} | interpolated value | C# variable interpolation |
{{ | { | CSS blocks, literal braces |
}} | } | CSS blocks, literal braces |
{{{{ | {{ | Scriban expression start |
}}}} | }} | Scriban expression end |
Common Patterns
// CSS block (need single braces in output)
$@"body {{
margin: 0;
padding: {PanelPadding}px; // C# interpolation
}}"
// Output: body { margin: 0; padding: 8px; }
// Scriban expression (need double braces in output)
$@"{{{{ for item in data.items }}}}"
// Output: {{ for item in data.items }}
// Scriban variable
$@"<div>{{{{ item.name }}}}</div>"
// Output: <div>{{ item.name }}</div>
Template Context Variables
When using HtmlPanel or WidgetPanel, these variables are automatically available:
| Variable | Type | Description |
|---|---|---|
data | object | The data model from GetDataModelAsync() |
target_width | int | Target render width in pixels |
target_height | int | Target render height in pixels |
assets_path | string | File URI to html_assets folder |
assets | string | Alias for assets_path |
colors | object | Color scheme values (see below) |
colors_css | string | Pre-rendered CSS variables |
Color Scheme Variables
Access via colors.property_name:
colors.background colors.text_primary colors.background_secondary colors.text_secondary colors.bar_background colors.text_muted colors.bar_border colors.accent colors.usage_low colors.usage_medium colors.usage_high colors.usage_critical
WidgetPanel Data Model
For WidgetPanel, the data object contains:
data = {
panel_data: { /* from GetPanelDataAsync() */ },
items: [ /* from GetItemsAsync() */ ],
widgets: [
{
component: "lcd-stat-card",
col_span: 6,
row_span: 2,
props: { /* widget properties */ },
props_json: "{ ... }",
html: "<lcd-stat-card props='...'></lcd-stat-card>"
}
]
}
Scriban Syntax Reference
Loops
{{ for item in data.items }}
<div>{{ item.name }}</div>
{{ end }}
{{ for widget in data.widgets }}
<div style="grid-column: span {{ widget.col_span }}">
{{ widget.html }}
</div>
{{ end }}
Conditionals
{{ if item.value > 90 }}
<span class="critical">{{ item.value }}</span>
{{ else if item.value > 70 }}
<span class="warning">{{ item.value }}</span>
{{ else }}
<span class="normal">{{ item.value }}</span>
{{ end }}
Variable Access
{{ data.panel_data.cpu_name }}
{{ data.items[0].value }}
{{ colors.accent }}
Important: No raw Filter
Scriban does NOT have a | raw filter (that's Liquid/Jinja). Scriban outputs strings without HTML encoding by default, so just use:
{{ widget.html }}
NOT:
{{ widget.html | raw }} ❌ ERROR: function 'raw' was not found
Common Errors and Fixes
Error: "Expecting an expression to the right of the operator"
Cause: CSS braces being interpreted as Scriban expressions.
Wrong:
$@"body {{{{
margin: 0;
}}}}"
Correct:
$@"body {{
margin: 0;
}}"
Error: "function 'raw' was not found"
Cause: Using Liquid/Jinja syntax instead of Scriban.
Wrong: {{ widget.html | raw }}
Correct: {{ widget.html }}
Error: "Unexpected token"
Cause: Missing closing braces or incorrect escaping.
Check that:
- •Every
{{{{has a matching}}}} - •CSS blocks use
{{and}}(2 braces each) - •Scriban expressions use
{{{{and}}}}(4 braces each)
Complete Template Example
private string GridTemplate => $@"<!DOCTYPE html>
<html lang=""en"">
<head>
<meta charset=""UTF-8"">
<meta name=""viewport"" content=""width={{{{ target_width }}}}, height={{{{ target_height }}}}"">
<link rel=""stylesheet"" href=""file://{{{{ assets_path }}}}/css/theme.css"">
<script src=""file://{{{{ assets_path }}}}/js/components.js""></script>
<style>
{{{{ colors_css }}}}
body {{
margin: 0;
padding: {PanelPadding}px;
background: var(--color-background);
}}
.grid {{
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: {GridGap}px;
}}
</style>
</head>
<body>
<div class=""grid"">
{{{{ for widget in data.widgets }}}}
<div style=""grid-column: span {{{{ widget.col_span }}}};"">
{{{{ widget.html }}}}
</div>
{{{{ end }}}}
</div>
</body>
</html>";
Adding Context Variables
To add new variables to the template context, edit HtmlPanel.RenderTemplate():
protected string RenderTemplate(object dataModel)
{
var context = new TemplateContext();
var scriptObject = new ScriptObject();
// Existing variables
scriptObject["data"] = dataModel;
scriptObject["target_width"] = TargetWidth;
scriptObject["target_height"] = TargetHeight;
scriptObject["assets_path"] = AssetsUri;
scriptObject["colors_css"] = GenerateColorsCss();
// Add your new variable
scriptObject["my_variable"] = "value";
context.PushGlobal(scriptObject);
return _compiledTemplate.Render(context);
}
Testing Templates
Use the test verb to verify templates render correctly:
# Render and check output ./start-app.ps1 test cpu-widget --debug # Check for errors in output # [DEBUG] Written: C:\Users\...\cpu-widget.jpg (10211 bytes) ✓ # Error: Template compilation failed: ... ✗
Web Components Available
These components are defined in html_assets/js/components.js:
| Component | Props | Description |
|---|---|---|
<lcd-usage-bar> | value, max, label, color, orientation | Progress bar |
<lcd-stat-card> | title, value, unit, subtitle, status, size | Value display |
<lcd-temp-gauge> | value, max, label | Temperature gauge |
<lcd-info-list> | items (JSON array) | Label/value pairs |
<lcd-sparkline> | values (JSON array), color, label | Time-series chart |
<lcd-status-dot> | status, label | Colored indicator |
<lcd-donut> | value, max, label, color | Circular percentage |
Component Usage
// In DefineWidgets()
yield return new WidgetDefinition(
Component: "lcd-stat-card",
ColSpan: 6,
RowSpan: 1,
Props: new {
title = "CPU",
value = metrics.Usage.ToString("F1"),
unit = "%",
status = metrics.Usage > 90 ? "critical" : "success"
}
);
Files Reference
| File | Purpose |
|---|---|
src/LCDPossible.Sdk/HtmlPanel.cs | Base class, template compilation |
src/LCDPossible.Sdk/WidgetPanel.cs | Grid layout, widget definitions |
src/LCDPossible.Sdk/html_assets/css/theme.css | CSS variables, responsive styles |
src/LCDPossible.Sdk/html_assets/js/components.js | Web component definitions |