@clix-so/clix-agent-skills 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -14
- package/dist/bin/cli.js +1 -1
- package/dist/bin/commands/install.js +2 -0
- package/dist/bin/utils/mcp.js +63 -14
- package/llms.txt +18 -0
- package/package.json +1 -1
- package/skills/event-tracking/SKILL.md +1 -1
- package/skills/integration/SKILL.md +4 -3
- package/skills/integration/references/mcp-integration.md +0 -20
- package/skills/integration/scripts/install-mcp.sh +269 -34
- package/skills/personalization/LICENSE.txt +204 -0
- package/skills/personalization/SKILL.md +128 -0
- package/skills/personalization/references/common-patterns.md +69 -0
- package/skills/personalization/references/debugging.md +89 -0
- package/skills/personalization/references/template-syntax.md +113 -0
- package/skills/personalization/scripts/validate-template.sh +134 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clix-personalization
|
|
3
|
+
description:
|
|
4
|
+
Helps developers author and debug Clix personalization templates
|
|
5
|
+
(Liquid-style) for message content, deep links/URLs, and audience targeting.
|
|
6
|
+
Use when the user mentions personalization variables, Liquid, templates,
|
|
7
|
+
conditional logic, loops, filters, deep links, message logs, or when the user
|
|
8
|
+
types `clix-personalization`.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Clix Personalization
|
|
12
|
+
|
|
13
|
+
Use this skill to help developers write **personalized Clix campaigns** using
|
|
14
|
+
template-based variables and light logic in:
|
|
15
|
+
|
|
16
|
+
- **Message content** (title, body, subtitle)
|
|
17
|
+
- **Links** (dynamic URL or deep link params)
|
|
18
|
+
- **Audience targeting** (conditional include/exclude rules)
|
|
19
|
+
|
|
20
|
+
## What the official docs guarantee (high-signal)
|
|
21
|
+
|
|
22
|
+
- **Data namespaces**:
|
|
23
|
+
- `user.*` (user traits/properties)
|
|
24
|
+
- `event.*` (event payload properties)
|
|
25
|
+
- `trigger.*` (custom properties passed via API-trigger)
|
|
26
|
+
- Device/system vars: `device.id`, `device.platform`, `device.locale`,
|
|
27
|
+
`device.language`, `device.timezone`, plus `user.id`, `event.name`
|
|
28
|
+
- **Missing variables** render as an **empty string**.
|
|
29
|
+
- **Output**: print values with `{{ ... }}`.
|
|
30
|
+
- **Conditionals**: `{% if %}`, `{% else %}`, `{% endif %}` with operators
|
|
31
|
+
`== != > < >= <= and or not`. Invalid conditions evaluate to `false`.
|
|
32
|
+
- **Loops**: `{% for x in y %}` / `{% endfor %}` (use guards for empty lists).
|
|
33
|
+
- **Filters**: `upcase`, `downcase`, `capitalize`, `default`, `join`, `split`,
|
|
34
|
+
`escape`, `strip`, `replace` (chain with `|`).
|
|
35
|
+
- **Errors**: template rendering errors show up in **Message Logs**.
|
|
36
|
+
|
|
37
|
+
## MCP-first (source of truth)
|
|
38
|
+
|
|
39
|
+
If the Clix MCP tools are available, treat them as the source of truth:
|
|
40
|
+
|
|
41
|
+
- `clix-mcp-server:search_docs` for personalization behavior and supported
|
|
42
|
+
syntax
|
|
43
|
+
|
|
44
|
+
If MCP tools are not available, use the bundled references in `references/`.
|
|
45
|
+
|
|
46
|
+
## Workflow (copy + check off)
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
Personalization progress:
|
|
50
|
+
- [ ] 1) Identify where the template runs (message / URL / audience rule)
|
|
51
|
+
- [ ] 2) Identify trigger type (event-triggered vs API-triggered)
|
|
52
|
+
- [ ] 3) Confirm available inputs (user.*, event.*, trigger.*, device.*)
|
|
53
|
+
- [ ] 4) Draft templates with guards/defaults
|
|
54
|
+
- [ ] 5) Validate template structure (balance tags, avoid missing vars)
|
|
55
|
+
- [ ] 6) Verify in Clix (preview/test payloads, check Message Logs)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 1) Confirm the minimum inputs
|
|
59
|
+
|
|
60
|
+
Ask only what’s needed:
|
|
61
|
+
|
|
62
|
+
- **Where used**: title/body/subtitle vs URL/deep link vs audience targeting
|
|
63
|
+
rule
|
|
64
|
+
- **Campaign trigger**:
|
|
65
|
+
- Event-triggered → `event.*` is available
|
|
66
|
+
- API-triggered → `trigger.*` is available
|
|
67
|
+
- **Inputs**:
|
|
68
|
+
- Which `user.*` properties are set by the app
|
|
69
|
+
- Which `event.*` / `trigger.*` properties are passed (include example
|
|
70
|
+
payload)
|
|
71
|
+
- **Fallback policy**: what to show when data is missing (default strings,
|
|
72
|
+
guards)
|
|
73
|
+
|
|
74
|
+
## 2) Produce a “Template Plan” (before tweaking lots of templates)
|
|
75
|
+
|
|
76
|
+
Return a compact table the user can approve:
|
|
77
|
+
|
|
78
|
+
- **field** (title/body/url/audience)
|
|
79
|
+
- **template** (Liquid)
|
|
80
|
+
- **required variables** (and their source: user/event/trigger/device)
|
|
81
|
+
- **fallbacks** (default filter and/or conditional guards)
|
|
82
|
+
- **example payload** + **expected rendered output**
|
|
83
|
+
|
|
84
|
+
## 3) Authoring guidelines (what to do by default)
|
|
85
|
+
|
|
86
|
+
- Prefer **simple variables + `default`** over complex branching.
|
|
87
|
+
- Add **guards** for optional data and for arrays (`size > 0`) before looping.
|
|
88
|
+
- Prefer **pre-formatted** properties from your app (e.g., `"7.4 miles"`,
|
|
89
|
+
`"38m 40s"`) instead of formatting inside templates.
|
|
90
|
+
- Keep logic minimal; complex branching belongs in the app or segmentation
|
|
91
|
+
setup.
|
|
92
|
+
|
|
93
|
+
## 4) Validation (fast feedback loop)
|
|
94
|
+
|
|
95
|
+
If you have a template file (recommended for review), run:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
bash <skill-dir>/scripts/validate-template.sh path/to/template.liquid
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This script does lightweight checks (matching `{% if %}`/`{% endif %}`,
|
|
102
|
+
`{% for %}`/`{% endfor %}`, and brace balancing). It can’t guarantee the
|
|
103
|
+
variables exist — you still need a payload + console verification.
|
|
104
|
+
|
|
105
|
+
## 5) Verification checklist
|
|
106
|
+
|
|
107
|
+
- **Missing data**: does the template still read well with empty strings?
|
|
108
|
+
- **Trigger type**: API triggers use `trigger.*` (not `event.*`).
|
|
109
|
+
- **Message Logs**: check rendering errors and fix syntax issues first.
|
|
110
|
+
- **Upstream data**:
|
|
111
|
+
- If `user.*` is missing → fix user property setting (see
|
|
112
|
+
`clix-user-management`)
|
|
113
|
+
- If `event.*` is missing → fix event tracking payload (see
|
|
114
|
+
`clix-event-tracking`)
|
|
115
|
+
|
|
116
|
+
## Progressive Disclosure
|
|
117
|
+
|
|
118
|
+
- **Level 1**: This `SKILL.md` (always loaded)
|
|
119
|
+
- **Level 2**: `references/` (load when writing/debugging templates)
|
|
120
|
+
- **Level 3**: `scripts/` (execute directly, do not load into context)
|
|
121
|
+
|
|
122
|
+
## References
|
|
123
|
+
|
|
124
|
+
- `references/template-syntax.md` - Variables, output, conditionals, loops,
|
|
125
|
+
filters
|
|
126
|
+
- `references/common-patterns.md` - Copy/paste patterns for messages + URLs +
|
|
127
|
+
guards
|
|
128
|
+
- `references/debugging.md` - Troubleshooting missing variables and Message Logs
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Common Personalization Patterns (Reference)
|
|
2
|
+
|
|
3
|
+
Copy/paste starting points for Clix templates. Prefer simple templates with
|
|
4
|
+
guards/defaults.
|
|
5
|
+
|
|
6
|
+
## Welcome / fallback-safe greeting
|
|
7
|
+
|
|
8
|
+
```liquid
|
|
9
|
+
Hi {{ user.username | default: "there" }},
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Dynamic message body (event-triggered)
|
|
13
|
+
|
|
14
|
+
```liquid
|
|
15
|
+
Hi {{ user.username | default: "Runner" }},
|
|
16
|
+
you ran {{ event.distance | default: "?" }} miles in {{ event.elapsedTime | default: "?" }} seconds.
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Dynamic deep link / URL (event-triggered)
|
|
20
|
+
|
|
21
|
+
```liquid
|
|
22
|
+
myapp://run/summary?distance={{ event.distance }}&time={{ event.elapsedTime }}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Tip: If a value may contain spaces or special characters, prefer passing a
|
|
26
|
+
pre-encoded string from your app (or keep the template value simple).
|
|
27
|
+
|
|
28
|
+
## API-triggered promo (trigger.\*)
|
|
29
|
+
|
|
30
|
+
```liquid
|
|
31
|
+
🎉 {{ trigger.promotion | default: "A new promotion" }} is here!
|
|
32
|
+
Get {{ trigger.discount | default: "a discount" }} off now!
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Conditional title by threshold
|
|
36
|
+
|
|
37
|
+
```liquid
|
|
38
|
+
{% if event.distance >= 10 %}
|
|
39
|
+
Long run completed!
|
|
40
|
+
{% else %}
|
|
41
|
+
Good run today!
|
|
42
|
+
{% endif %}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Nested conditions (keep minimal)
|
|
46
|
+
|
|
47
|
+
```liquid
|
|
48
|
+
{% if user.tier == "pro" %}
|
|
49
|
+
Pro stats updated successfully.
|
|
50
|
+
{% else %}
|
|
51
|
+
{% if event.elapsedTime > 3600 %}
|
|
52
|
+
You ran for more than an hour! Great job!
|
|
53
|
+
{% else %}
|
|
54
|
+
New record saved.
|
|
55
|
+
{% endif %}
|
|
56
|
+
{% endif %}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Looping over an array with a guard
|
|
60
|
+
|
|
61
|
+
```liquid
|
|
62
|
+
{% if user.badges and user.badges.size > 0 %}
|
|
63
|
+
{% for badge in user.badges %}
|
|
64
|
+
- {{ badge }}
|
|
65
|
+
{% endfor %}
|
|
66
|
+
{% else %}
|
|
67
|
+
No badges yet.
|
|
68
|
+
{% endif %}
|
|
69
|
+
```
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Debugging Personalization (Reference)
|
|
2
|
+
|
|
3
|
+
This guide helps you troubleshoot when a personalized message renders
|
|
4
|
+
unexpectedly.
|
|
5
|
+
|
|
6
|
+
## First principles (what the renderer does)
|
|
7
|
+
|
|
8
|
+
- Unknown / missing variables render as an **empty string**.
|
|
9
|
+
- Invalid conditions evaluate to `false`.
|
|
10
|
+
- Template rendering errors (syntax issues) surface in **Message Logs**.
|
|
11
|
+
|
|
12
|
+
## Quick checklist
|
|
13
|
+
|
|
14
|
+
- **Are you using the right namespace?**
|
|
15
|
+
- Event-triggered campaign → use `event.*`
|
|
16
|
+
- API-triggered campaign → use `trigger.*`
|
|
17
|
+
- User traits/properties → use `user.*`
|
|
18
|
+
- **Is the variable actually set upstream?**
|
|
19
|
+
- `user.*` comes from your app calling user property APIs.
|
|
20
|
+
- `event.*` comes from the event payload you track.
|
|
21
|
+
- `trigger.*` comes from properties in the API trigger request.
|
|
22
|
+
- **Is the template resilient to missing data?**
|
|
23
|
+
- Add `| default: "..."` for strings.
|
|
24
|
+
- Wrap optional blocks in `{% if ... %}` guards.
|
|
25
|
+
- **Are you looping safely?**
|
|
26
|
+
- Guard with `collection and collection.size > 0` before `{% for %}`.
|
|
27
|
+
|
|
28
|
+
## Common failure modes
|
|
29
|
+
|
|
30
|
+
### 1) Blank values
|
|
31
|
+
|
|
32
|
+
Symptoms:
|
|
33
|
+
|
|
34
|
+
- Output is missing where you expected text.
|
|
35
|
+
|
|
36
|
+
Causes:
|
|
37
|
+
|
|
38
|
+
- Variable doesn’t exist for that user/campaign trigger.
|
|
39
|
+
|
|
40
|
+
Fix:
|
|
41
|
+
|
|
42
|
+
- Use `default` for output values:
|
|
43
|
+
|
|
44
|
+
```liquid
|
|
45
|
+
{{ user.username | default: "Guest" }}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2) Conditional branch never runs
|
|
49
|
+
|
|
50
|
+
Symptoms:
|
|
51
|
+
|
|
52
|
+
- Always hits the `{% else %}` branch.
|
|
53
|
+
|
|
54
|
+
Causes:
|
|
55
|
+
|
|
56
|
+
- `event.*` is missing (or you should be using `trigger.*`).
|
|
57
|
+
- Value is not the type you expect (e.g., string vs number).
|
|
58
|
+
|
|
59
|
+
Fix:
|
|
60
|
+
|
|
61
|
+
- Verify trigger type and payload shape.
|
|
62
|
+
- Keep comparisons simple and ensure the property is sent as a number when you
|
|
63
|
+
compare numerically.
|
|
64
|
+
|
|
65
|
+
### 3) Syntax error / rendering error
|
|
66
|
+
|
|
67
|
+
Symptoms:
|
|
68
|
+
|
|
69
|
+
- Message fails to render or shows an error in logs.
|
|
70
|
+
|
|
71
|
+
Causes:
|
|
72
|
+
|
|
73
|
+
- Missing `{% endif %}` / `{% endfor %}`
|
|
74
|
+
- Unbalanced `{{` / `}}` or `{%` / `%}`
|
|
75
|
+
|
|
76
|
+
Fix:
|
|
77
|
+
|
|
78
|
+
- Check **Message Logs** first.
|
|
79
|
+
- Validate the template structure locally:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
bash <skill-dir>/scripts/validate-template.sh path/to/template.liquid
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## When to escalate (data issue vs template issue)
|
|
86
|
+
|
|
87
|
+
- If **defaults/guards** fix it → template issue.
|
|
88
|
+
- If values are always blank across many users → upstream tracking/user-property
|
|
89
|
+
issue (fix app instrumentation).
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Personalization Template Syntax (Reference)
|
|
2
|
+
|
|
3
|
+
This reference summarizes the personalization syntax supported by Clix
|
|
4
|
+
templates.
|
|
5
|
+
|
|
6
|
+
## Data namespaces
|
|
7
|
+
|
|
8
|
+
- `user.*`: user properties (traits) you capture about a user
|
|
9
|
+
- Examples: `user.username`, `user.tier`
|
|
10
|
+
- `event.*`: properties from the triggering event (event-triggered campaigns)
|
|
11
|
+
- Examples: `event.distance`, `event.elapsedTime`
|
|
12
|
+
- `trigger.*`: custom properties passed via API trigger (API-triggered
|
|
13
|
+
campaigns)
|
|
14
|
+
- Examples: `trigger.promotion`, `trigger.discount`
|
|
15
|
+
|
|
16
|
+
Missing or undefined variables render as an **empty string**.
|
|
17
|
+
|
|
18
|
+
## Device and system variables
|
|
19
|
+
|
|
20
|
+
Clix templates also support device and environment variables:
|
|
21
|
+
|
|
22
|
+
- `device.id`
|
|
23
|
+
- `device.platform` (`IOS` or `ANDROID`)
|
|
24
|
+
- `device.locale`
|
|
25
|
+
- `device.language`
|
|
26
|
+
- `device.timezone`
|
|
27
|
+
- `user.id` (project user id)
|
|
28
|
+
- `event.name` (event name)
|
|
29
|
+
|
|
30
|
+
## Output variables
|
|
31
|
+
|
|
32
|
+
Print a value:
|
|
33
|
+
|
|
34
|
+
```liquid
|
|
35
|
+
{{ user.username }}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Nested / bracket access (useful when the key is not a valid identifier):
|
|
39
|
+
|
|
40
|
+
```liquid
|
|
41
|
+
{{ event["distance"] }}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Conditionals
|
|
45
|
+
|
|
46
|
+
Use `{% if %}`, `{% else %}`, `{% endif %}` blocks.
|
|
47
|
+
|
|
48
|
+
Supported operators:
|
|
49
|
+
|
|
50
|
+
- `==`, `!=`, `>`, `<`, `>=`, `<=`
|
|
51
|
+
- `and`, `or`, `not`
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
|
|
55
|
+
```liquid
|
|
56
|
+
{% if event.distance >= 10 %}
|
|
57
|
+
Amazing! You completed a long run today.
|
|
58
|
+
{% else %}
|
|
59
|
+
Nice work! Keep it up!
|
|
60
|
+
{% endif %}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Notes:
|
|
64
|
+
|
|
65
|
+
- Unknown variables render as empty strings.
|
|
66
|
+
- Invalid conditions evaluate to `false`.
|
|
67
|
+
|
|
68
|
+
## Loops
|
|
69
|
+
|
|
70
|
+
Iterate over arrays with `{% for %}` / `{% endfor %}`:
|
|
71
|
+
|
|
72
|
+
```liquid
|
|
73
|
+
{% for badge in user.badges %}
|
|
74
|
+
- {{ badge }}
|
|
75
|
+
{% endfor %}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Guard empty collections:
|
|
79
|
+
|
|
80
|
+
```liquid
|
|
81
|
+
{% if user.badges and user.badges.size > 0 %}
|
|
82
|
+
{% for badge in user.badges %}
|
|
83
|
+
{{ badge }}
|
|
84
|
+
{% endfor %}
|
|
85
|
+
{% else %}
|
|
86
|
+
No badges yet.
|
|
87
|
+
{% endif %}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Filters
|
|
91
|
+
|
|
92
|
+
Filters transform values inside `{{ ... }}` blocks and can be chained with `|`.
|
|
93
|
+
|
|
94
|
+
Supported filters:
|
|
95
|
+
|
|
96
|
+
- `upcase` / `downcase`
|
|
97
|
+
- `capitalize`
|
|
98
|
+
- `default`
|
|
99
|
+
- `join`
|
|
100
|
+
- `split`
|
|
101
|
+
- `escape`
|
|
102
|
+
- `strip`
|
|
103
|
+
- `replace`
|
|
104
|
+
|
|
105
|
+
Examples:
|
|
106
|
+
|
|
107
|
+
```liquid
|
|
108
|
+
Hi {{ user.username | default: "Guest" }}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```liquid
|
|
112
|
+
{{ user.username | default: "Guest" | upcase }}
|
|
113
|
+
```
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
usage() {
|
|
5
|
+
cat <<'EOF'
|
|
6
|
+
Validate a Clix personalization template (Liquid-style) with lightweight checks.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
validate-template.sh <template-file>
|
|
10
|
+
|
|
11
|
+
Checks:
|
|
12
|
+
- Balanced {{ ... }} and {% ... %} delimiters
|
|
13
|
+
- Properly nested {% if %}/{% endif %} and {% for %}/{% endfor %} blocks
|
|
14
|
+
|
|
15
|
+
Notes:
|
|
16
|
+
- This is a best-effort validator; it cannot guarantee that variables exist.
|
|
17
|
+
- If Clix shows a rendering error, always trust Message Logs as the source of truth.
|
|
18
|
+
EOF
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" || "${1:-}" == "" ]]; then
|
|
22
|
+
usage
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
file="$1"
|
|
27
|
+
|
|
28
|
+
if [[ ! -f "$file" ]]; then
|
|
29
|
+
echo "Error: file not found: $file" >&2
|
|
30
|
+
exit 2
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
errors=0
|
|
34
|
+
|
|
35
|
+
echo "Validating: $file"
|
|
36
|
+
|
|
37
|
+
brace_counts="$(
|
|
38
|
+
awk '
|
|
39
|
+
{
|
|
40
|
+
c_open_curly += gsub(/\{\{/, "&");
|
|
41
|
+
c_close_curly += gsub(/\}\}/, "&");
|
|
42
|
+
c_open_tag += gsub(/\{%/, "&");
|
|
43
|
+
c_close_tag += gsub(/%\}/, "&");
|
|
44
|
+
}
|
|
45
|
+
END {
|
|
46
|
+
printf("%d %d %d %d\n", c_open_curly, c_close_curly, c_open_tag, c_close_tag);
|
|
47
|
+
}
|
|
48
|
+
' "$file"
|
|
49
|
+
)"
|
|
50
|
+
|
|
51
|
+
read -r open_curly close_curly open_tag close_tag <<<"$brace_counts"
|
|
52
|
+
|
|
53
|
+
if [[ "$open_curly" -ne "$close_curly" ]]; then
|
|
54
|
+
echo "Error: unbalanced mustache braces: '{{'=$open_curly vs '}}'=$close_curly" >&2
|
|
55
|
+
errors=$((errors + 1))
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if [[ "$open_tag" -ne "$close_tag" ]]; then
|
|
59
|
+
echo "Error: unbalanced tag braces: '{%'=$open_tag vs '%}'=$close_tag" >&2
|
|
60
|
+
errors=$((errors + 1))
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
awk_exit=0
|
|
64
|
+
awk '
|
|
65
|
+
function trim(s) { sub(/^[ \t\r\n]+/, "", s); sub(/[ \t\r\n]+$/, "", s); return s }
|
|
66
|
+
function push(x) { stack[++sp] = x }
|
|
67
|
+
function top() { return stack[sp] }
|
|
68
|
+
function pop() { if (sp > 0) return stack[sp--]; return "" }
|
|
69
|
+
|
|
70
|
+
BEGIN { sp = 0; errors = 0 }
|
|
71
|
+
|
|
72
|
+
{
|
|
73
|
+
line = $0
|
|
74
|
+
while (match(line, /\{%[ \t]*[^%]*%\}/)) {
|
|
75
|
+
tag = substr(line, RSTART, RLENGTH)
|
|
76
|
+
inner = tag
|
|
77
|
+
gsub(/^\{%[ \t]*/, "", inner)
|
|
78
|
+
gsub(/[ \t]*%\}$/, "", inner)
|
|
79
|
+
inner = trim(inner)
|
|
80
|
+
|
|
81
|
+
# Tag name is first token.
|
|
82
|
+
split(inner, parts, /[ \t]+/)
|
|
83
|
+
name = parts[1]
|
|
84
|
+
|
|
85
|
+
if (name == "if") {
|
|
86
|
+
push("if")
|
|
87
|
+
} else if (name == "for") {
|
|
88
|
+
push("for")
|
|
89
|
+
} else if (name == "endif") {
|
|
90
|
+
if (top() != "if") {
|
|
91
|
+
printf("Error: found endif but top of stack is '%s'\n", top()) > "/dev/stderr"
|
|
92
|
+
errors++
|
|
93
|
+
} else {
|
|
94
|
+
pop()
|
|
95
|
+
}
|
|
96
|
+
} else if (name == "endfor") {
|
|
97
|
+
if (top() != "for") {
|
|
98
|
+
printf("Error: found endfor but top of stack is '%s'\n", top()) > "/dev/stderr"
|
|
99
|
+
errors++
|
|
100
|
+
} else {
|
|
101
|
+
pop()
|
|
102
|
+
}
|
|
103
|
+
} else if (name == "else" || name == "elsif") {
|
|
104
|
+
if (top() != "if") {
|
|
105
|
+
printf("Error: found %s outside of if block\n", name) > "/dev/stderr"
|
|
106
|
+
errors++
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Move forward in line after the matched tag.
|
|
111
|
+
line = substr(line, RSTART + RLENGTH)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
END {
|
|
116
|
+
if (sp > 0) {
|
|
117
|
+
printf("Error: unclosed blocks remaining on stack (count=%d)\n", sp) > "/dev/stderr"
|
|
118
|
+
errors++
|
|
119
|
+
}
|
|
120
|
+
exit(errors > 0 ? 1 : 0)
|
|
121
|
+
}
|
|
122
|
+
' "$file" || awk_exit=$?
|
|
123
|
+
|
|
124
|
+
if [[ "$awk_exit" -ne 0 ]]; then
|
|
125
|
+
errors=$((errors + 1))
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
if [[ "$errors" -ne 0 ]]; then
|
|
129
|
+
echo "❌ Validation failed ($errors issue(s))." >&2
|
|
130
|
+
exit 1
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
echo "✅ Template structure looks OK."
|
|
134
|
+
|