Initial commit
This commit is contained in:
939
skills/selmer/SKILL.md
Normal file
939
skills/selmer/SKILL.md
Normal file
@@ -0,0 +1,939 @@
|
||||
---
|
||||
name: selmer
|
||||
description: Django-inspired HTML templating system for Clojure with filters, tags, and template inheritance
|
||||
---
|
||||
|
||||
# Selmer
|
||||
|
||||
Django-inspired HTML templating system for Clojure providing a fast, productive templating experience with filters, tags, template inheritance, and extensive customization options.
|
||||
|
||||
## Overview
|
||||
|
||||
Selmer is a pure Clojure template engine inspired by Django's template syntax. It compiles templates at runtime, supports template inheritance and includes, provides extensive built-in filters and tags, and allows custom extensions. Designed for server-side rendering in web applications, email generation, reports, and any text-based output.
|
||||
|
||||
**Key Features:**
|
||||
- Django-compatible template syntax
|
||||
- Variable interpolation with nested data access
|
||||
- 50+ built-in filters for data transformation
|
||||
- Control flow tags (if, for, with)
|
||||
- Template inheritance (extends, block, include)
|
||||
- Custom filter and tag creation
|
||||
- Template caching for performance
|
||||
- Auto-escaping with override control
|
||||
- Validation and error reporting
|
||||
- Middleware support
|
||||
|
||||
**Installation:**
|
||||
|
||||
Leiningen:
|
||||
```clojure
|
||||
[selmer "1.12.65"]
|
||||
```
|
||||
|
||||
deps.edn:
|
||||
```clojure
|
||||
{selmer/selmer {:mvn/version "1.12.65"}}
|
||||
```
|
||||
|
||||
**Quick Start:**
|
||||
```clojure
|
||||
(require '[selmer.parser :refer [render render-file]])
|
||||
|
||||
;; Render string
|
||||
(render "Hello {{name}}!" {:name "World"})
|
||||
;=> "Hello World!"
|
||||
|
||||
;; Render file
|
||||
(render-file "templates/home.html" {:user "Alice"})
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Variables
|
||||
|
||||
Variables use `{{variable}}` syntax and are replaced with values from the context map.
|
||||
|
||||
```clojure
|
||||
(render "{{greeting}} {{name}}" {:greeting "Hello" :name "Bob"})
|
||||
;=> "Hello Bob"
|
||||
```
|
||||
|
||||
**Nested Access:**
|
||||
```clojure
|
||||
(render "{{person.name}}" {:person {:name "Alice"}})
|
||||
;=> "Alice"
|
||||
|
||||
(render "{{items.0.title}}" {:items [{:title "First"}]})
|
||||
;=> "First"
|
||||
```
|
||||
|
||||
**Missing Values:**
|
||||
```clojure
|
||||
(render "{{missing}}" {})
|
||||
;=> "" (empty string by default)
|
||||
```
|
||||
|
||||
### Filters
|
||||
|
||||
Filters transform variable values using the pipe `|` operator.
|
||||
|
||||
```clojure
|
||||
(render "{{name|upper}}" {:name "alice"})
|
||||
;=> "ALICE"
|
||||
|
||||
;; Chain filters
|
||||
(render "{{text|upper|take:5}}" {:text "hello world"})
|
||||
;=> "HELLO"
|
||||
```
|
||||
|
||||
### Tags
|
||||
|
||||
Tags use `{% tag %}` syntax for control flow and template structure.
|
||||
|
||||
```clojure
|
||||
{% if user %}
|
||||
Welcome {{user}}!
|
||||
{% else %}
|
||||
Please log in.
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### Template Inheritance
|
||||
|
||||
Parent template (`base.html`):
|
||||
```html
|
||||
<html>
|
||||
<head>{% block head %}Default Title{% endblock %}</head>
|
||||
<body>{% block content %}{% endblock %}</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Child template:
|
||||
```html
|
||||
{% extends "base.html" %}
|
||||
{% block head %}Custom Title{% endblock %}
|
||||
{% block content %}<h1>Hello!</h1>{% endblock %}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Rendering Functions
|
||||
|
||||
#### `render`
|
||||
Render a template string with context.
|
||||
|
||||
```clojure
|
||||
(render template-string context-map)
|
||||
(render template-string context-map options)
|
||||
|
||||
;; Examples
|
||||
(render "{{x}}" {:x 42})
|
||||
;=> "42"
|
||||
|
||||
(render "[% x %]" {:x 42}
|
||||
{:tag-open "[%" :tag-close "%]"})
|
||||
;=> "42"
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `template-string` - Template as string
|
||||
- `context-map` - Data map for template
|
||||
- `options` - Optional map with `:tag-open`, `:tag-close`, `:filter-open`, `:filter-close`
|
||||
|
||||
#### `render-file`
|
||||
Render a template file from classpath or resource path.
|
||||
|
||||
```clojure
|
||||
(render-file filename context-map)
|
||||
(render-file filename context-map options)
|
||||
|
||||
;; Examples
|
||||
(render-file "templates/email.html" {:name "Alice"})
|
||||
|
||||
(render-file "custom.tpl" {:x 1}
|
||||
{:tag-open "<%%" :tag-close "%%>"})
|
||||
```
|
||||
|
||||
**File Resolution:**
|
||||
1. Checks configured resource path
|
||||
2. Falls back to classpath
|
||||
3. Caches compiled template
|
||||
|
||||
### Caching
|
||||
|
||||
#### `cache-on!`
|
||||
Enable template caching (default).
|
||||
|
||||
```clojure
|
||||
(require '[selmer.parser :refer [cache-on!]])
|
||||
|
||||
(cache-on!)
|
||||
```
|
||||
|
||||
Templates are compiled once and cached. Use in production.
|
||||
|
||||
#### `cache-off!`
|
||||
Disable template caching for development.
|
||||
|
||||
```clojure
|
||||
(require '[selmer.parser :refer [cache-off!]])
|
||||
|
||||
(cache-off!)
|
||||
```
|
||||
|
||||
Templates recompile on each render. Use during development.
|
||||
|
||||
### Configuration
|
||||
|
||||
#### `set-resource-path!`
|
||||
Configure base path for template files.
|
||||
|
||||
```clojure
|
||||
(require '[selmer.parser :refer [set-resource-path!]])
|
||||
|
||||
(set-resource-path! "/var/html/templates/")
|
||||
(set-resource-path! nil) ; Reset to classpath
|
||||
```
|
||||
|
||||
#### `set-missing-value-formatter!`
|
||||
Configure how missing values are rendered.
|
||||
|
||||
```clojure
|
||||
(require '[selmer.parser :refer [set-missing-value-formatter!]])
|
||||
|
||||
(set-missing-value-formatter!
|
||||
(fn [tag context-map]
|
||||
(str "MISSING: " tag)))
|
||||
|
||||
(render "{{missing}}" {})
|
||||
;=> "MISSING: missing"
|
||||
```
|
||||
|
||||
### Introspection
|
||||
|
||||
#### `known-variables`
|
||||
Extract all variables from a template.
|
||||
|
||||
```clojure
|
||||
(require '[selmer.parser :refer [known-variables]])
|
||||
|
||||
(known-variables "{{x}} {{y.z}}")
|
||||
;=> #{:x :y.z}
|
||||
```
|
||||
|
||||
Useful for validation and documentation.
|
||||
|
||||
### Validation
|
||||
|
||||
#### `validate-on!` / `validate-off!`
|
||||
Control template validation.
|
||||
|
||||
```clojure
|
||||
(require '[selmer.validator :refer [validate-on! validate-off!]])
|
||||
|
||||
(validate-on!) ; Default - validates templates
|
||||
(validate-off!) ; Skip validation for performance
|
||||
```
|
||||
|
||||
Validation catches undefined filters, malformed tags, and syntax errors.
|
||||
|
||||
### Custom Filters
|
||||
|
||||
#### `add-filter!`
|
||||
Register a custom filter.
|
||||
|
||||
```clojure
|
||||
(require '[selmer.filters :refer [add-filter!]])
|
||||
|
||||
(add-filter! :shout
|
||||
(fn [s] (str (clojure.string/upper-case s) "!!!")))
|
||||
|
||||
(render "{{msg|shout}}" {:msg "hello"})
|
||||
;=> "HELLO!!!"
|
||||
|
||||
;; With arguments
|
||||
(add-filter! :repeat
|
||||
(fn [s n] (apply str (repeat (Integer/parseInt n) s))))
|
||||
|
||||
(render "{{x|repeat:3}}" {:x "ha"})
|
||||
;=> "hahaha"
|
||||
```
|
||||
|
||||
#### `remove-filter!`
|
||||
Remove a filter.
|
||||
|
||||
```clojure
|
||||
(require '[selmer.filters :refer [remove-filter!]])
|
||||
|
||||
(remove-filter! :shout)
|
||||
```
|
||||
|
||||
### Custom Tags
|
||||
|
||||
#### `add-tag!`
|
||||
Register a custom tag.
|
||||
|
||||
```clojure
|
||||
(require '[selmer.parser :refer [add-tag!]])
|
||||
|
||||
(add-tag! :uppercase
|
||||
(fn [args context-map]
|
||||
(clojure.string/upper-case (first args))))
|
||||
|
||||
;; In template: {% uppercase "hello" %}
|
||||
```
|
||||
|
||||
**Block Tags:**
|
||||
```clojure
|
||||
(add-tag! :bold
|
||||
(fn [args context-map content]
|
||||
(str "<b>" (get-in content [:bold :content]) "</b>"))
|
||||
:bold :endbold)
|
||||
|
||||
;; In template:
|
||||
;; {% bold %}text here{% endbold %}
|
||||
```
|
||||
|
||||
#### `remove-tag!`
|
||||
Remove a tag.
|
||||
|
||||
```clojure
|
||||
(require '[selmer.parser :refer [remove-tag!]])
|
||||
|
||||
(remove-tag! :uppercase)
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
#### `wrap-error-page`
|
||||
Middleware to display template errors with context.
|
||||
|
||||
```clojure
|
||||
(require '[selmer.middleware :refer [wrap-error-page]])
|
||||
|
||||
(def app
|
||||
(wrap-error-page handler))
|
||||
```
|
||||
|
||||
Shows error message, line number, and template snippet.
|
||||
|
||||
### Escaping Control
|
||||
|
||||
#### `without-escaping`
|
||||
Render template without HTML escaping.
|
||||
|
||||
```clojure
|
||||
(require '[selmer.util :refer [without-escaping]])
|
||||
|
||||
(render "{{html}}" {:html "<b>Bold</b>"})
|
||||
;=> "<b>Bold</b>"
|
||||
|
||||
(without-escaping
|
||||
(render "{{html}}" {:html "<b>Bold</b>"}))
|
||||
;=> "<b>Bold</b>"
|
||||
```
|
||||
|
||||
## Built-in Filters
|
||||
|
||||
### String Filters
|
||||
|
||||
**upper** - Convert to uppercase
|
||||
```clojure
|
||||
{{name|upper}} ; "alice" → "ALICE"
|
||||
```
|
||||
|
||||
**lower** - Convert to lowercase
|
||||
```clojure
|
||||
{{NAME|lower}} ; "ALICE" → "alice"
|
||||
```
|
||||
|
||||
**capitalize** - Capitalize first letter
|
||||
```clojure
|
||||
{{word|capitalize}} ; "hello" → "Hello"
|
||||
```
|
||||
|
||||
**title** - Title case
|
||||
```clojure
|
||||
{{phrase|title}} ; "hello world" → "Hello World"
|
||||
```
|
||||
|
||||
**addslashes** - Escape quotes
|
||||
```clojure
|
||||
{{text|addslashes}} ; "I'm" → "I\'m"
|
||||
```
|
||||
|
||||
**remove-tags** - Strip HTML tags
|
||||
```clojure
|
||||
{{html|remove-tags}} ; "<b>text</b>" → "text"
|
||||
```
|
||||
|
||||
**safe** - Mark as safe (no escaping)
|
||||
```clojure
|
||||
{{html|safe}} ; Renders HTML without escaping
|
||||
```
|
||||
|
||||
**replace** - Replace substring
|
||||
```clojure
|
||||
{{text|replace:"old":"new"}}
|
||||
```
|
||||
|
||||
**subs** - Substring
|
||||
```clojure
|
||||
{{text|subs:0:5}} ; First 5 characters
|
||||
```
|
||||
|
||||
**abbreviate** - Truncate with ellipsis
|
||||
```clojure
|
||||
{{text|abbreviate:10}} ; "Long text..." (max 10 chars)
|
||||
```
|
||||
|
||||
### Formatting Filters
|
||||
|
||||
**date** - Format date
|
||||
```clojure
|
||||
{{timestamp|date:"yyyy-MM-dd"}}
|
||||
{{timestamp|date:"MMM dd, yyyy"}}
|
||||
```
|
||||
|
||||
**currency-format** - Format currency
|
||||
```clojure
|
||||
{{amount|currency-format}} ; 1234.5 → "$1,234.50"
|
||||
```
|
||||
|
||||
**double-format** - Format decimal
|
||||
```clojure
|
||||
{{number|double-format:"%.2f"}} ; 3.14159 → "3.14"
|
||||
```
|
||||
|
||||
**pluralize** - Pluralize noun
|
||||
```clojure
|
||||
{{count}} item{{count|pluralize}}
|
||||
; 1 item, 2 items
|
||||
|
||||
{{count}} box{{count|pluralize:"es"}}
|
||||
; 1 box, 2 boxes
|
||||
```
|
||||
|
||||
### Collection Filters
|
||||
|
||||
**count** - Get collection size
|
||||
```clojure
|
||||
{{items|count}} ; [1 2 3] → "3"
|
||||
```
|
||||
|
||||
**first** - First element
|
||||
```clojure
|
||||
{{items|first}} ; [1 2 3] → "1"
|
||||
```
|
||||
|
||||
**last** - Last element
|
||||
```clojure
|
||||
{{items|last}} ; [1 2 3] → "3"
|
||||
```
|
||||
|
||||
**join** - Join with separator
|
||||
```clojure
|
||||
{{items|join:", "}} ; [1 2 3] → "1, 2, 3"
|
||||
```
|
||||
|
||||
**sort** - Sort collection
|
||||
```clojure
|
||||
{{items|sort}} ; [3 1 2] → [1 2 3]
|
||||
```
|
||||
|
||||
**sort-by** - Sort by key
|
||||
```clojure
|
||||
{{people|sort-by:"age"}}
|
||||
```
|
||||
|
||||
**reverse** - Reverse collection
|
||||
```clojure
|
||||
{{items|reverse}} ; [1 2 3] → [3 2 1]
|
||||
```
|
||||
|
||||
**take** - Take first N
|
||||
```clojure
|
||||
{{items|take:2}} ; [1 2 3] → [1 2]
|
||||
```
|
||||
|
||||
**drop** - Drop first N
|
||||
```clojure
|
||||
{{items|drop:1}} ; [1 2 3] → [2 3]
|
||||
```
|
||||
|
||||
### Utility Filters
|
||||
|
||||
**default** - Default if falsy
|
||||
```clojure
|
||||
{{value|default:"N/A"}}
|
||||
```
|
||||
|
||||
**default-if-empty** - Default if empty
|
||||
```clojure
|
||||
{{text|default-if-empty:"None"}}
|
||||
```
|
||||
|
||||
**hash** - Compute hash
|
||||
```clojure
|
||||
{{text|hash:"md5"}}
|
||||
{{text|hash:"sha256"}}
|
||||
```
|
||||
|
||||
**json** - Convert to JSON
|
||||
```clojure
|
||||
{{data|json}} ; {:x 1} → "{\"x\":1}"
|
||||
```
|
||||
|
||||
**length** - String/collection length
|
||||
```clojure
|
||||
{{text|length}} ; "hello" → "5"
|
||||
```
|
||||
|
||||
## Built-in Tags
|
||||
|
||||
### Control Flow
|
||||
|
||||
#### `if` / `else` / `endif`
|
||||
Conditional rendering.
|
||||
|
||||
```clojure
|
||||
{% if user %}
|
||||
Hello {{user}}!
|
||||
{% else %}
|
||||
Please log in.
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
**With operators:**
|
||||
```clojure
|
||||
{% if count > 10 %}
|
||||
Many items
|
||||
{% elif count > 0 %}
|
||||
Few items
|
||||
{% else %}
|
||||
No items
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
**Operators:** `=`, `!=`, `<`, `>`, `<=`, `>=`, `and`, `or`, `not`
|
||||
|
||||
#### `ifequal` / `ifunequal`
|
||||
Compare two values.
|
||||
|
||||
```clojure
|
||||
{% ifequal user.role "admin" %}
|
||||
Admin panel
|
||||
{% endifequal %}
|
||||
|
||||
{% ifunequal status "active" %}
|
||||
Inactive
|
||||
{% endifunequal %}
|
||||
```
|
||||
|
||||
#### `firstof`
|
||||
Render first truthy value.
|
||||
|
||||
```clojure
|
||||
{% firstof user.nickname user.name "Guest" %}
|
||||
```
|
||||
|
||||
### Loops
|
||||
|
||||
#### `for`
|
||||
Iterate over collections.
|
||||
|
||||
```clojure
|
||||
{% for item in items %}
|
||||
{{forloop.counter}}. {{item}}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
**Loop Variables:**
|
||||
- `forloop.counter` - 1-indexed position
|
||||
- `forloop.counter0` - 0-indexed position
|
||||
- `forloop.first` - True on first iteration
|
||||
- `forloop.last` - True on last iteration
|
||||
- `forloop.length` - Total items
|
||||
|
||||
**With empty:**
|
||||
```clojure
|
||||
{% for item in items %}
|
||||
{{item}}
|
||||
{% empty %}
|
||||
No items found
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
**Destructuring:**
|
||||
```clojure
|
||||
{% for [k v] in pairs %}
|
||||
{{k}}: {{v}}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
#### `cycle`
|
||||
Cycle through values in a loop.
|
||||
|
||||
```clojure
|
||||
{% for item in items %}
|
||||
<tr class="{% cycle 'odd' 'even' %}">{{item}}</tr>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### Template Structure
|
||||
|
||||
#### `extends`
|
||||
Inherit from parent template.
|
||||
|
||||
```clojure
|
||||
{% extends "base.html" %}
|
||||
```
|
||||
|
||||
Must be first tag in template.
|
||||
|
||||
#### `block`
|
||||
Define overridable section.
|
||||
|
||||
Parent template:
|
||||
```html
|
||||
{% block content %}Default content{% endblock %}
|
||||
```
|
||||
|
||||
Child template:
|
||||
```html
|
||||
{% block content %}Custom content{% endblock %}
|
||||
```
|
||||
|
||||
**Block super:**
|
||||
```html
|
||||
{% block content %}
|
||||
{{block.super}} Additional content
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
#### `include`
|
||||
Insert another template.
|
||||
|
||||
```clojure
|
||||
{% include "header.html" %}
|
||||
```
|
||||
|
||||
**With context:**
|
||||
```clojure
|
||||
{% include "item.html" with item=product %}
|
||||
```
|
||||
|
||||
### Other Tags
|
||||
|
||||
#### `comment`
|
||||
Template comments (not rendered).
|
||||
|
||||
```clojure
|
||||
{% comment %}
|
||||
This won't appear in output
|
||||
{% endcomment %}
|
||||
```
|
||||
|
||||
#### `now`
|
||||
Render current timestamp.
|
||||
|
||||
```clojure
|
||||
{% now "yyyy-MM-dd HH:mm" %}
|
||||
```
|
||||
|
||||
#### `with`
|
||||
Create local variables.
|
||||
|
||||
```clojure
|
||||
{% with total=items|count %}
|
||||
Total: {{total}}
|
||||
{% endwith %}
|
||||
```
|
||||
|
||||
#### `verbatim`
|
||||
Render content without processing.
|
||||
|
||||
```clojure
|
||||
{% verbatim %}
|
||||
{{this}} won't be processed
|
||||
{% endverbatim %}
|
||||
```
|
||||
|
||||
Useful for client-side templates.
|
||||
|
||||
#### `script` / `style`
|
||||
Include script/style blocks without escaping.
|
||||
|
||||
```clojure
|
||||
{% script %}
|
||||
var x = {{data|json}};
|
||||
{% endscript %}
|
||||
|
||||
{% style %}
|
||||
.class { color: {{color}}; }
|
||||
{% endstyle %}
|
||||
```
|
||||
|
||||
#### `debug`
|
||||
Output context map for debugging.
|
||||
|
||||
```clojure
|
||||
{% debug %}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Email Templates
|
||||
|
||||
```clojure
|
||||
(defn send-welcome-email [user]
|
||||
(let [html (render-file "emails/welcome.html"
|
||||
{:name (:name user)
|
||||
:activation-link (generate-link user)})]
|
||||
(send-email {:to (:email user)
|
||||
:subject "Welcome!"
|
||||
:body html})))
|
||||
```
|
||||
|
||||
### Web Page Rendering
|
||||
|
||||
```clojure
|
||||
(defn home-handler [request]
|
||||
{:status 200
|
||||
:headers {"Content-Type" "text/html"}
|
||||
:body (render-file "pages/home.html"
|
||||
{:user (:user request)
|
||||
:posts (fetch-recent-posts)})})
|
||||
```
|
||||
|
||||
### Template Fragments
|
||||
|
||||
```clojure
|
||||
;; Reusable components
|
||||
(render-file "components/button.html"
|
||||
{:text "Click me"
|
||||
:action "/submit"})
|
||||
```
|
||||
|
||||
### Dynamic Form Generation
|
||||
|
||||
```clojure
|
||||
(render-file "forms/user-form.html"
|
||||
{:fields [{:name "username" :type "text"}
|
||||
{:name "email" :type "email"}
|
||||
{:name "password" :type "password"}]
|
||||
:action "/register"})
|
||||
```
|
||||
|
||||
### Report Generation
|
||||
|
||||
```clojure
|
||||
(defn generate-report [data]
|
||||
(render-file "reports/monthly.html"
|
||||
{:period (format-period)
|
||||
:totals (calculate-totals data)
|
||||
:items data
|
||||
:generated-at (java.time.LocalDateTime/now)}))
|
||||
```
|
||||
|
||||
### Template Composition
|
||||
|
||||
```clojure
|
||||
;; Base layout
|
||||
{% extends "layouts/main.html" %}
|
||||
|
||||
;; Page-specific
|
||||
{% block title %}Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "components/stats.html" %}
|
||||
{% include "components/chart.html" %}
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### Custom Marker Syntax
|
||||
|
||||
```clojure
|
||||
;; Compatible with client-side frameworks
|
||||
(render-file "spa.html" data
|
||||
{:tag-open "[%"
|
||||
:tag-close "%]"
|
||||
:filter-open "[["
|
||||
:filter-close "]]"})
|
||||
```
|
||||
|
||||
### Validation and Error Handling
|
||||
|
||||
```clojure
|
||||
(require '[selmer.parser :refer [render-file known-variables]])
|
||||
(require '[selmer.validator :refer [validate-on!]])
|
||||
|
||||
(validate-on!)
|
||||
|
||||
(defn safe-render [template-name data]
|
||||
(try
|
||||
(let [required (known-variables
|
||||
(slurp (io/resource template-name)))]
|
||||
(when-not (every? #(contains? data %) required)
|
||||
(throw (ex-info "Missing template variables"
|
||||
{:required required :provided (keys data)})))
|
||||
(render-file template-name data))
|
||||
(catch Exception e
|
||||
(log/error e "Template rendering failed")
|
||||
"Error rendering template")))
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Errors
|
||||
|
||||
**Missing Template:**
|
||||
```clojure
|
||||
(render-file "nonexistent.html" {})
|
||||
;=> Exception: resource nonexistent.html not found
|
||||
```
|
||||
|
||||
**Solution:** Verify file exists in classpath or resource path.
|
||||
|
||||
**Undefined Filter:**
|
||||
```clojure
|
||||
(render "{{x|badfilter}}" {:x 1})
|
||||
;=> Exception: filter badfilter not found
|
||||
```
|
||||
|
||||
**Solution:** Check filter name or define custom filter.
|
||||
|
||||
**Malformed Tag:**
|
||||
```clojure
|
||||
(render "{% if %}" {})
|
||||
;=> Exception: malformed if tag
|
||||
```
|
||||
|
||||
**Solution:** Ensure tag syntax is correct.
|
||||
|
||||
### Error Middleware
|
||||
|
||||
```clojure
|
||||
(require '[selmer.middleware :refer [wrap-error-page]])
|
||||
|
||||
(def app
|
||||
(-> handler
|
||||
wrap-error-page
|
||||
wrap-other-middleware))
|
||||
```
|
||||
|
||||
Displays detailed error page with:
|
||||
- Error message
|
||||
- Line number
|
||||
- Template excerpt
|
||||
- Context data
|
||||
|
||||
### Validation
|
||||
|
||||
```clojure
|
||||
(require '[selmer.validator :refer [validate-on!]])
|
||||
|
||||
(validate-on!)
|
||||
|
||||
(render "{% unknown-tag %}" {})
|
||||
;=> Validation error with details
|
||||
```
|
||||
|
||||
Catches errors at compile time rather than runtime.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Template Caching
|
||||
|
||||
**Enable in production:**
|
||||
```clojure
|
||||
(cache-on!)
|
||||
```
|
||||
|
||||
Templates compile once, cache compiled version. Significant performance improvement.
|
||||
|
||||
**Disable in development:**
|
||||
```clojure
|
||||
(cache-off!)
|
||||
```
|
||||
|
||||
Recompiles on each render. See changes immediately.
|
||||
|
||||
### Resource Path Configuration
|
||||
|
||||
```clojure
|
||||
(set-resource-path! "/var/templates/")
|
||||
```
|
||||
|
||||
Reduces classpath scanning overhead.
|
||||
|
||||
### Filter Performance
|
||||
|
||||
**Expensive operations:**
|
||||
```clojure
|
||||
;; Avoid in loops
|
||||
{% for item in items %}
|
||||
{{item.data|json|hash:"sha256"}}
|
||||
{% endfor %}
|
||||
|
||||
;; Better: preprocess in Clojure
|
||||
(render-file "template.html"
|
||||
{:items (map #(assoc % :hash (compute-hash %))
|
||||
items)})
|
||||
```
|
||||
|
||||
### Validation Overhead
|
||||
|
||||
```clojure
|
||||
;; Development
|
||||
(validate-on!)
|
||||
|
||||
;; Production (after testing)
|
||||
(validate-off!)
|
||||
```
|
||||
|
||||
Validation adds minimal overhead but can be disabled if templates are thoroughly tested.
|
||||
|
||||
### Template Inheritance
|
||||
|
||||
Shallow inheritance trees perform better than deep nesting.
|
||||
|
||||
**Good:**
|
||||
```
|
||||
base.html → page.html
|
||||
```
|
||||
|
||||
**Slower:**
|
||||
```
|
||||
base.html → layout.html → section.html → page.html
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use template caching in production**
|
||||
2. **Keep templates in dedicated directory** (`resources/templates/`)
|
||||
3. **Validate templates in development**
|
||||
4. **Preprocess complex data in Clojure** rather than in templates
|
||||
5. **Use includes for reusable components**
|
||||
6. **Leverage template inheritance** for consistent layouts
|
||||
7. **Escape user content** (default behavior) unless explicitly safe
|
||||
8. **Name templates descriptively** (`user-profile.html`, not `page1.html`)
|
||||
9. **Document custom filters and tags**
|
||||
10. **Test templates with various data** to catch edge cases
|
||||
|
||||
## Platform Notes
|
||||
|
||||
**Clojure:** Full support, production-ready.
|
||||
|
||||
**ClojureScript:** Not supported. Selmer is JVM-only due to template compilation requiring Java classes.
|
||||
|
||||
**Babashka:** Not supported. Selmer requires classes and compilation not available in Babashka.
|
||||
|
||||
**Alternatives for ClojureScript:**
|
||||
- Reagent (Hiccup-style)
|
||||
- Rum
|
||||
- UIx
|
||||
|
||||
**Alternatives for Babashka:**
|
||||
- Hiccup
|
||||
- String templates with `format`
|
||||
285
skills/selmer/examples.clj
Normal file
285
skills/selmer/examples.clj
Normal file
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env clojure
|
||||
|
||||
;; Selmer Examples
|
||||
;; NOTE: Selmer requires Clojure (JVM) and is NOT compatible with Babashka
|
||||
;; Run with: clojure -M examples.clj
|
||||
;; Or add deps.edn with selmer dependency
|
||||
|
||||
(comment
|
||||
"To run these examples, create a deps.edn file:"
|
||||
{:deps {selmer/selmer {:mvn/version "1.12.65"}}}
|
||||
"Then run: clojure -M examples.clj")
|
||||
|
||||
(require '[selmer.parser :as parser])
|
||||
(require '[selmer.filters :as filters])
|
||||
|
||||
(println "=== Selmer Templating Examples ===\n")
|
||||
|
||||
;; ============================================================================
|
||||
;; Basic Rendering
|
||||
;; ============================================================================
|
||||
|
||||
(println "1. Basic Variable Rendering")
|
||||
(println (parser/render "Hello {{name}}!" {:name "World"}))
|
||||
(println (parser/render "{{greeting}}, {{name}}!"
|
||||
{:greeting "Hi" :name "Alice"}))
|
||||
(println)
|
||||
|
||||
(println "2. Nested Data Access")
|
||||
(println (parser/render "{{person.name}} is {{person.age}} years old"
|
||||
{:person {:name "Bob" :age 30}}))
|
||||
(println (parser/render "First item: {{items.0}}"
|
||||
{:items ["apple" "banana" "cherry"]}))
|
||||
(println)
|
||||
|
||||
;; ============================================================================
|
||||
;; Filters
|
||||
;; ============================================================================
|
||||
|
||||
(println "3. String Filters")
|
||||
(println (parser/render "{{name|upper}}" {:name "alice"}))
|
||||
(println (parser/render "{{name|lower}}" {:name "ALICE"}))
|
||||
(println (parser/render "{{text|capitalize}}" {:text "hello world"}))
|
||||
(println (parser/render "{{text|title}}" {:text "hello world"}))
|
||||
(println)
|
||||
|
||||
(println "4. Filter Chaining")
|
||||
(println (parser/render "{{text|upper|take:5}}" {:text "hello world"}))
|
||||
(println (parser/render "{{items|join:\", \"|upper}}"
|
||||
{:items ["a" "b" "c"]}))
|
||||
(println)
|
||||
|
||||
(println "5. Collection Filters")
|
||||
(println (parser/render "Count: {{items|count}}"
|
||||
{:items [1 2 3 4 5]}))
|
||||
(println (parser/render "First: {{items|first}}, Last: {{items|last}}"
|
||||
{:items [10 20 30]}))
|
||||
(println (parser/render "Joined: {{items|join:\", \"}}"
|
||||
{:items ["red" "green" "blue"]}))
|
||||
(println (parser/render "Reversed: {{items|reverse|join:\"-\"}}"
|
||||
{:items [1 2 3]}))
|
||||
(println)
|
||||
|
||||
(println "6. Utility Filters")
|
||||
(println (parser/render "Value: {{val|default:\"N/A\"}}" {:val nil}))
|
||||
(println (parser/render "Value: {{val|default:\"N/A\"}}" {:val "Present"}))
|
||||
(println (parser/render "Length: {{text|length}}" {:text "hello"}))
|
||||
(println)
|
||||
|
||||
;; ============================================================================
|
||||
;; Control Flow
|
||||
;; ============================================================================
|
||||
|
||||
(println "7. If/Else Tags")
|
||||
(println (parser/render "{% if user %}Hello {{user}}!{% else %}Please log in{% endif %}"
|
||||
{:user "Alice"}))
|
||||
(println (parser/render "{% if user %}Hello {{user}}!{% else %}Please log in{% endif %}"
|
||||
{}))
|
||||
(println)
|
||||
|
||||
(println "8. If with Comparisons")
|
||||
(println (parser/render "{% if count > 10 %}Many{% elif count > 0 %}Few{% else %}None{% endif %}"
|
||||
{:count 15}))
|
||||
(println (parser/render "{% if count > 10 %}Many{% elif count > 0 %}Few{% else %}None{% endif %}"
|
||||
{:count 5}))
|
||||
(println (parser/render "{% if count > 10 %}Many{% elif count > 0 %}Few{% else %}None{% endif %}"
|
||||
{:count 0}))
|
||||
(println)
|
||||
|
||||
(println "9. Ifequal/Ifunequal")
|
||||
(println (parser/render "{% ifequal role \"admin\" %}Admin panel{% endifequal %}"
|
||||
{:role "admin"}))
|
||||
(println (parser/render "{% ifunequal status \"active\" %}Inactive{% endifunequal %}"
|
||||
{:status "pending"}))
|
||||
(println)
|
||||
|
||||
;; ============================================================================
|
||||
;; Loops
|
||||
;; ============================================================================
|
||||
|
||||
(println "10. For Loops")
|
||||
(def for-template
|
||||
"{% for item in items %}{{forloop.counter}}. {{item}}\n{% endfor %}")
|
||||
(println (parser/render for-template {:items ["apple" "banana" "cherry"]}))
|
||||
|
||||
(println "11. For Loop Variables")
|
||||
(def loop-vars-template
|
||||
"{% for item in items %}{{item}}{% if not forloop.last %}, {% endif %}{% endfor %}")
|
||||
(println (parser/render loop-vars-template {:items [1 2 3 4 5]}))
|
||||
(println)
|
||||
|
||||
(println "12. For with Empty")
|
||||
(def for-empty-template
|
||||
"{% for item in items %}{{item}}\n{% empty %}No items found{% endfor %}")
|
||||
(println (parser/render for-empty-template {:items []}))
|
||||
(println (parser/render for-empty-template {:items ["present"]}))
|
||||
(println)
|
||||
|
||||
(println "13. Destructuring in For Loops")
|
||||
(def destructure-template
|
||||
"{% for [k v] in pairs %}{{k}}: {{v}}\n{% endfor %}")
|
||||
(println (parser/render destructure-template
|
||||
{:pairs [["name" "Alice"]
|
||||
["age" "30"]
|
||||
["city" "NYC"]]}))
|
||||
|
||||
;; ============================================================================
|
||||
;; Template Includes (Inline)
|
||||
;; ============================================================================
|
||||
|
||||
(println "14. Template Composition with Render")
|
||||
(def header-template "=== {{title}} ===")
|
||||
(def content-template "{{header}}\n{{body}}")
|
||||
(let [header (parser/render header-template {:title "Welcome"})
|
||||
result (parser/render content-template
|
||||
{:header header
|
||||
:body "This is the content"})]
|
||||
(println result))
|
||||
(println)
|
||||
|
||||
;; ============================================================================
|
||||
;; Custom Filters
|
||||
;; ============================================================================
|
||||
|
||||
(println "15. Custom Filters")
|
||||
(filters/add-filter! :shout
|
||||
(fn [s] (str (clojure.string/upper-case s) "!!!")))
|
||||
|
||||
(println (parser/render "{{msg|shout}}" {:msg "hello"}))
|
||||
|
||||
(filters/add-filter! :repeat
|
||||
(fn [s n] (apply str (repeat (Integer/parseInt n) s))))
|
||||
|
||||
(println (parser/render "{{x|repeat:3}}" {:x "ha"}))
|
||||
(println)
|
||||
|
||||
;; ============================================================================
|
||||
;; Custom Tags
|
||||
;; ============================================================================
|
||||
|
||||
(println "16. Custom Tags")
|
||||
(parser/add-tag! :uppercase
|
||||
(fn [args context-map]
|
||||
(clojure.string/upper-case (first args))))
|
||||
|
||||
(println (parser/render "{% uppercase \"hello world\" %}" {}))
|
||||
(println)
|
||||
|
||||
;; ============================================================================
|
||||
;; Escaping
|
||||
;; ============================================================================
|
||||
|
||||
(println "17. HTML Escaping (Default)")
|
||||
(println (parser/render "{{html}}" {:html "<b>Bold</b>"}))
|
||||
|
||||
(println "\n18. Safe Filter (No Escaping)")
|
||||
(println (parser/render "{{html|safe}}" {:html "<b>Bold</b>"}))
|
||||
(println)
|
||||
|
||||
;; ============================================================================
|
||||
;; Variable Introspection
|
||||
;; ============================================================================
|
||||
|
||||
(println "19. Known Variables")
|
||||
(println "Variables in template '{{x}} {{y.z}}':")
|
||||
(println (parser/known-variables "{{x}} {{y.z}}"))
|
||||
(println)
|
||||
|
||||
;; ============================================================================
|
||||
;; Practical Examples
|
||||
;; ============================================================================
|
||||
|
||||
(println "20. Email Template Example")
|
||||
(def email-template
|
||||
"Dear {{name}},
|
||||
|
||||
Thank you for registering! Your account details:
|
||||
|
||||
Username: {{username}}
|
||||
Email: {{email}}
|
||||
Registered: {% now \"yyyy-MM-dd HH:mm\" %}
|
||||
|
||||
{% if premium %}
|
||||
Premium features are now available!
|
||||
{% else %}
|
||||
Upgrade to premium for additional features.
|
||||
{% endif %}
|
||||
|
||||
Best regards,
|
||||
The Team")
|
||||
|
||||
(println (parser/render email-template
|
||||
{:name "Alice Johnson"
|
||||
:username "alice"
|
||||
:email "alice@example.com"
|
||||
:premium true}))
|
||||
(println)
|
||||
|
||||
(println "21. HTML List Example")
|
||||
(def list-template
|
||||
"<ul>
|
||||
{% for item in items %}
|
||||
<li class=\"{% cycle 'odd' 'even' %}\">
|
||||
{{item.name}} - ${{item.price|double-format:\"%.2f\"}}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>")
|
||||
|
||||
(println (parser/render list-template
|
||||
{:items [{:name "Widget" :price 19.99}
|
||||
{:name "Gadget" :price 29.50}
|
||||
{:name "Doohickey" :price 9.99}]}))
|
||||
(println)
|
||||
|
||||
(println "22. Conditional Navigation")
|
||||
(def nav-template
|
||||
"<nav>
|
||||
{% if user %}
|
||||
Welcome, {{user.name}}!
|
||||
{% ifequal user.role \"admin\" %}
|
||||
<a href=\"/admin\">Admin Panel</a>
|
||||
{% endifequal %}
|
||||
<a href=\"/logout\">Logout</a>
|
||||
{% else %}
|
||||
<a href=\"/login\">Login</a>
|
||||
<a href=\"/register\">Register</a>
|
||||
{% endif %}
|
||||
</nav>")
|
||||
|
||||
(println "Admin user:")
|
||||
(println (parser/render nav-template
|
||||
{:user {:name "Admin" :role "admin"}}))
|
||||
|
||||
(println "\nRegular user:")
|
||||
(println (parser/render nav-template
|
||||
{:user {:name "Bob" :role "user"}}))
|
||||
|
||||
(println "\nNot logged in:")
|
||||
(println (parser/render nav-template {}))
|
||||
(println)
|
||||
|
||||
(println "23. Data Table Example")
|
||||
(def table-template
|
||||
"<table>
|
||||
<thead>
|
||||
<tr><th>#</th><th>Name</th><th>Status</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{forloop.counter}}</td>
|
||||
<td>{{user.name|title}}</td>
|
||||
<td>{% if user.active %}Active{% else %}Inactive{% endif %}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan=\"3\">No users found</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>")
|
||||
|
||||
(println (parser/render table-template
|
||||
{:users [{:name "alice" :active true}
|
||||
{:name "bob" :active false}
|
||||
{:name "charlie" :active true}]}))
|
||||
|
||||
(println "\n=== Examples Complete ===")
|
||||
52
skills/selmer/metadata.edn
Normal file
52
skills/selmer/metadata.edn
Normal file
@@ -0,0 +1,52 @@
|
||||
{:name "selmer"
|
||||
:version "1.12.65"
|
||||
:description "Django-inspired HTML templating system for Clojure with filters, tags, and template inheritance"
|
||||
:library {:name "selmer/selmer"
|
||||
:version "1.12.65"
|
||||
:url "https://github.com/yogthos/Selmer"
|
||||
:license "EPL-1.0"}
|
||||
:tags [:templating :html :web :django :markup :rendering :filters :template-inheritance]
|
||||
:use-cases [:web-applications
|
||||
:html-generation
|
||||
:email-templates
|
||||
:report-generation
|
||||
:static-site-generation
|
||||
:dynamic-content-rendering
|
||||
:template-based-output
|
||||
:server-side-rendering]
|
||||
:features [:variable-interpolation
|
||||
:template-filters
|
||||
:control-flow-tags
|
||||
:template-inheritance
|
||||
:template-includes
|
||||
:custom-filters
|
||||
:custom-tags
|
||||
:template-caching
|
||||
:auto-escaping
|
||||
:nested-data-access
|
||||
:filter-chaining
|
||||
:django-compatible-syntax
|
||||
:error-reporting
|
||||
:validation
|
||||
:custom-markers
|
||||
:middleware-support]
|
||||
:file-structure {:SKILL.md "Comprehensive documentation with API reference and examples"
|
||||
:metadata.edn "Skill metadata"
|
||||
:examples.clj "Runnable examples"}
|
||||
:learning-path [{:level :beginner
|
||||
:topics [:render :render-file :variables :basic-filters :if-tags :for-loops]}
|
||||
{:level :intermediate
|
||||
:topics [:template-inheritance :includes :blocks :filter-chaining :resource-paths :caching]}
|
||||
{:level :advanced
|
||||
:topics [:custom-filters :custom-tags :validation :error-handling :middleware :escaping-control]}]
|
||||
:platform-support {:babashka false
|
||||
:clojure true
|
||||
:clojurescript false}
|
||||
:api-coverage {:rendering [:render :render-file]
|
||||
:caching [:cache-on! :cache-off!]
|
||||
:configuration [:set-resource-path! :set-missing-value-formatter!]
|
||||
:introspection [:known-variables]
|
||||
:validation [:validate-on! :validate-off!]
|
||||
:customization [:add-filter! :add-tag! :remove-filter! :remove-tag!]
|
||||
:error-handling [:wrap-error-page]
|
||||
:utilities [:without-escaping]}}
|
||||
Reference in New Issue
Block a user