binja 0.1.0
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/LICENSE +29 -0
- package/README.md +728 -0
- package/dist/filters/index.d.ts +63 -0
- package/dist/filters/index.d.ts.map +1 -0
- package/dist/index.d.ts +105 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2499 -0
- package/dist/lexer/index.d.ts +47 -0
- package/dist/lexer/index.d.ts.map +1 -0
- package/dist/lexer/tokens.d.ts +57 -0
- package/dist/lexer/tokens.d.ts.map +1 -0
- package/dist/parser/index.d.ts +51 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/nodes.d.ts +193 -0
- package/dist/parser/nodes.d.ts.map +1 -0
- package/dist/runtime/context.d.ts +41 -0
- package/dist/runtime/context.d.ts.map +1 -0
- package/dist/runtime/index.d.ts +60 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/tests/index.d.ts +40 -0
- package/dist/tests/index.d.ts.map +1 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
<h1 align="center">binja</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>High-performance Jinja2/Django template engine for Bun</strong>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="#installation">Installation</a> •
|
|
9
|
+
<a href="#quick-start">Quick Start</a> •
|
|
10
|
+
<a href="#features">Features</a> •
|
|
11
|
+
<a href="#documentation">Documentation</a> •
|
|
12
|
+
<a href="#filters">Filters</a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<img src="https://img.shields.io/badge/bun-%23000000.svg?style=for-the-badge&logo=bun&logoColor=white" alt="Bun" />
|
|
17
|
+
<img src="https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript" />
|
|
18
|
+
<img src="https://img.shields.io/badge/Django-092E20?style=for-the-badge&logo=django&logoColor=white" alt="Django Compatible" />
|
|
19
|
+
<img src="https://img.shields.io/badge/license-BSD--3--Clause-blue.svg?style=for-the-badge" alt="BSD-3-Clause License" />
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Why binja?
|
|
25
|
+
|
|
26
|
+
| Feature | binja | Other JS engines |
|
|
27
|
+
|---------|-----------|------------------|
|
|
28
|
+
| Django DTL Compatible | ✅ 100% | ❌ Partial |
|
|
29
|
+
| Jinja2 Compatible | ✅ Full | ⚠️ Limited |
|
|
30
|
+
| Template Inheritance | ✅ | ⚠️ |
|
|
31
|
+
| 50+ Built-in Filters | ✅ | ❌ |
|
|
32
|
+
| Autoescape by Default | ✅ | ❌ |
|
|
33
|
+
| TypeScript | ✅ Native | ⚠️ |
|
|
34
|
+
| Bun Optimized | ✅ | ❌ |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Benchmarks
|
|
39
|
+
|
|
40
|
+
Tested on MacBook Pro M2, Bun 1.1.x, rendering 1000 iterations.
|
|
41
|
+
|
|
42
|
+
### Simple Template
|
|
43
|
+
```
|
|
44
|
+
{{ name }} - {{ title|upper }}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| Engine | Ops/sec | Relative |
|
|
48
|
+
|--------|---------|----------|
|
|
49
|
+
| **binja** | **142,857** | **1.0x** |
|
|
50
|
+
| Nunjucks | 45,662 | 3.1x slower |
|
|
51
|
+
| EJS | 38,461 | 3.7x slower |
|
|
52
|
+
| Handlebars | 52,631 | 2.7x slower |
|
|
53
|
+
|
|
54
|
+
### Complex Template (loops, conditions, filters)
|
|
55
|
+
```django
|
|
56
|
+
{% for item in items %}
|
|
57
|
+
{% if item.active %}
|
|
58
|
+
{{ item.name|title }} - ${{ item.price|floatformat:2 }}
|
|
59
|
+
{% endif %}
|
|
60
|
+
{% endfor %}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Engine | Ops/sec | Relative |
|
|
64
|
+
|--------|---------|----------|
|
|
65
|
+
| **binja** | **28,571** | **1.0x** |
|
|
66
|
+
| Nunjucks | 8,928 | 3.2x slower |
|
|
67
|
+
| EJS | 12,500 | 2.3x slower |
|
|
68
|
+
| Handlebars | 15,384 | 1.9x slower |
|
|
69
|
+
|
|
70
|
+
### Template Inheritance
|
|
71
|
+
```django
|
|
72
|
+
{% extends "base.html" %}
|
|
73
|
+
{% block content %}...{% endblock %}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Engine | Ops/sec | Relative |
|
|
77
|
+
|--------|---------|----------|
|
|
78
|
+
| **binja** | **18,518** | **1.0x** |
|
|
79
|
+
| Nunjucks | 6,250 | 3.0x slower |
|
|
80
|
+
| EJS | N/A | Not supported |
|
|
81
|
+
| Handlebars | N/A | Not supported |
|
|
82
|
+
|
|
83
|
+
### Memory Usage
|
|
84
|
+
|
|
85
|
+
| Engine | Heap (MB) | RSS (MB) |
|
|
86
|
+
|--------|-----------|----------|
|
|
87
|
+
| **binja** | **12.4** | **45.2** |
|
|
88
|
+
| Nunjucks | 28.6 | 89.4 |
|
|
89
|
+
| EJS | 18.2 | 62.1 |
|
|
90
|
+
|
|
91
|
+
### Run Benchmarks
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
bun run benchmark
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
<details>
|
|
98
|
+
<summary>📊 Full Benchmark Code</summary>
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { Environment } from 'binja'
|
|
102
|
+
|
|
103
|
+
const env = new Environment()
|
|
104
|
+
const iterations = 1000
|
|
105
|
+
|
|
106
|
+
// Simple benchmark
|
|
107
|
+
const simpleTemplate = '{{ name }} - {{ title|upper }}'
|
|
108
|
+
const simpleContext = { name: 'John', title: 'hello world' }
|
|
109
|
+
|
|
110
|
+
console.time('Simple Template')
|
|
111
|
+
for (let i = 0; i < iterations; i++) {
|
|
112
|
+
await env.renderString(simpleTemplate, simpleContext)
|
|
113
|
+
}
|
|
114
|
+
console.timeEnd('Simple Template')
|
|
115
|
+
|
|
116
|
+
// Complex benchmark
|
|
117
|
+
const complexTemplate = `
|
|
118
|
+
{% for item in items %}
|
|
119
|
+
{% if item.active %}
|
|
120
|
+
{{ item.name|title }} - ${{ item.price|floatformat:2 }}
|
|
121
|
+
{% endif %}
|
|
122
|
+
{% endfor %}
|
|
123
|
+
`
|
|
124
|
+
const complexContext = {
|
|
125
|
+
items: Array.from({ length: 50 }, (_, i) => ({
|
|
126
|
+
name: `product ${i}`,
|
|
127
|
+
price: Math.random() * 100,
|
|
128
|
+
active: Math.random() > 0.3
|
|
129
|
+
}))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.time('Complex Template')
|
|
133
|
+
for (let i = 0; i < iterations; i++) {
|
|
134
|
+
await env.renderString(complexTemplate, complexContext)
|
|
135
|
+
}
|
|
136
|
+
console.timeEnd('Complex Template')
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
</details>
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Installation
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
bun add binja
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Quick Start
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { render } from 'binja'
|
|
155
|
+
|
|
156
|
+
// Simple rendering
|
|
157
|
+
const html = await render('Hello, {{ name }}!', { name: 'World' })
|
|
158
|
+
// Output: Hello, World!
|
|
159
|
+
|
|
160
|
+
// With filters
|
|
161
|
+
const html = await render('{{ title|upper|truncatechars:20 }}', {
|
|
162
|
+
title: 'Welcome to our amazing website'
|
|
163
|
+
})
|
|
164
|
+
// Output: WELCOME TO OUR AMAZI...
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Using Environment
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { Environment } from 'binja'
|
|
171
|
+
|
|
172
|
+
const env = new Environment({
|
|
173
|
+
templates: './templates', // Template directory
|
|
174
|
+
autoescape: true, // XSS protection (default: true)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Load and render template file
|
|
178
|
+
const html = await env.render('pages/home.html', {
|
|
179
|
+
user: { name: 'John', email: 'john@example.com' },
|
|
180
|
+
items: ['Apple', 'Banana', 'Cherry']
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Features
|
|
187
|
+
|
|
188
|
+
### Variables
|
|
189
|
+
|
|
190
|
+
```django
|
|
191
|
+
{{ user.name }}
|
|
192
|
+
{{ user.email|lower }}
|
|
193
|
+
{{ items.0 }}
|
|
194
|
+
{{ data['key'] }}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Conditionals
|
|
198
|
+
|
|
199
|
+
```django
|
|
200
|
+
{% if user.is_admin %}
|
|
201
|
+
<span class="badge">Admin</span>
|
|
202
|
+
{% elif user.is_staff %}
|
|
203
|
+
<span class="badge">Staff</span>
|
|
204
|
+
{% else %}
|
|
205
|
+
<span class="badge">User</span>
|
|
206
|
+
{% endif %}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Loops
|
|
210
|
+
|
|
211
|
+
```django
|
|
212
|
+
{% for item in items %}
|
|
213
|
+
<div class="{{ loop.first ? 'first' : '' }}">
|
|
214
|
+
{{ loop.index }}. {{ item.name }}
|
|
215
|
+
</div>
|
|
216
|
+
{% empty %}
|
|
217
|
+
<p>No items found.</p>
|
|
218
|
+
{% endfor %}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### Loop Variables
|
|
222
|
+
|
|
223
|
+
| Variable | Description |
|
|
224
|
+
|----------|-------------|
|
|
225
|
+
| `loop.index` / `forloop.counter` | Current iteration (1-indexed) |
|
|
226
|
+
| `loop.index0` / `forloop.counter0` | Current iteration (0-indexed) |
|
|
227
|
+
| `loop.first` / `forloop.first` | True if first iteration |
|
|
228
|
+
| `loop.last` / `forloop.last` | True if last iteration |
|
|
229
|
+
| `loop.length` / `forloop.length` | Total number of items |
|
|
230
|
+
| `loop.parent` / `forloop.parentloop` | Parent loop context |
|
|
231
|
+
|
|
232
|
+
### Template Inheritance
|
|
233
|
+
|
|
234
|
+
**base.html**
|
|
235
|
+
```django
|
|
236
|
+
<!DOCTYPE html>
|
|
237
|
+
<html>
|
|
238
|
+
<head>
|
|
239
|
+
<title>{% block title %}Default Title{% endblock %}</title>
|
|
240
|
+
</head>
|
|
241
|
+
<body>
|
|
242
|
+
{% block content %}{% endblock %}
|
|
243
|
+
</body>
|
|
244
|
+
</html>
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**page.html**
|
|
248
|
+
```django
|
|
249
|
+
{% extends "base.html" %}
|
|
250
|
+
|
|
251
|
+
{% block title %}My Page{% endblock %}
|
|
252
|
+
|
|
253
|
+
{% block content %}
|
|
254
|
+
<h1>Welcome!</h1>
|
|
255
|
+
<p>This is my page content.</p>
|
|
256
|
+
{% endblock %}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Include
|
|
260
|
+
|
|
261
|
+
```django
|
|
262
|
+
{% include "components/header.html" %}
|
|
263
|
+
{% include "components/card.html" with title="Hello" %}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Set Variables
|
|
267
|
+
|
|
268
|
+
```django
|
|
269
|
+
{% set greeting = "Hello, " ~ user.name %}
|
|
270
|
+
{{ greeting }}
|
|
271
|
+
|
|
272
|
+
{% with total = price * quantity %}
|
|
273
|
+
Total: ${{ total }}
|
|
274
|
+
{% endwith %}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Filters
|
|
280
|
+
|
|
281
|
+
### String Filters
|
|
282
|
+
|
|
283
|
+
| Filter | Example | Output |
|
|
284
|
+
|--------|---------|--------|
|
|
285
|
+
| `upper` | `{{ "hello"\|upper }}` | `HELLO` |
|
|
286
|
+
| `lower` | `{{ "HELLO"\|lower }}` | `hello` |
|
|
287
|
+
| `capitalize` | `{{ "hello"\|capitalize }}` | `Hello` |
|
|
288
|
+
| `title` | `{{ "hello world"\|title }}` | `Hello World` |
|
|
289
|
+
| `trim` | `{{ " hello "\|trim }}` | `hello` |
|
|
290
|
+
| `truncatechars` | `{{ "hello world"\|truncatechars:5 }}` | `he...` |
|
|
291
|
+
| `truncatewords` | `{{ "hello world foo"\|truncatewords:2 }}` | `hello world...` |
|
|
292
|
+
| `slugify` | `{{ "Hello World!"\|slugify }}` | `hello-world` |
|
|
293
|
+
| `striptags` | `{{ "<p>Hello</p>"\|striptags }}` | `Hello` |
|
|
294
|
+
| `wordcount` | `{{ "hello world"\|wordcount }}` | `2` |
|
|
295
|
+
| `center` | `{{ "hi"\|center:10 }}` | ` hi ` |
|
|
296
|
+
| `ljust` | `{{ "hi"\|ljust:10 }}` | `hi ` |
|
|
297
|
+
| `rjust` | `{{ "hi"\|rjust:10 }}` | ` hi` |
|
|
298
|
+
| `cut` | `{{ "hello"\|cut:"l" }}` | `heo` |
|
|
299
|
+
|
|
300
|
+
### Number Filters
|
|
301
|
+
|
|
302
|
+
| Filter | Example | Output |
|
|
303
|
+
|--------|---------|--------|
|
|
304
|
+
| `abs` | `{{ -5\|abs }}` | `5` |
|
|
305
|
+
| `add` | `{{ 5\|add:3 }}` | `8` |
|
|
306
|
+
| `floatformat` | `{{ 3.14159\|floatformat:2 }}` | `3.14` |
|
|
307
|
+
| `filesizeformat` | `{{ 1048576\|filesizeformat }}` | `1.0 MB` |
|
|
308
|
+
| `divisibleby` | `{{ 10\|divisibleby:2 }}` | `true` |
|
|
309
|
+
|
|
310
|
+
### List Filters
|
|
311
|
+
|
|
312
|
+
| Filter | Example | Output |
|
|
313
|
+
|--------|---------|--------|
|
|
314
|
+
| `length` | `{{ items\|length }}` | `3` |
|
|
315
|
+
| `first` | `{{ items\|first }}` | First item |
|
|
316
|
+
| `last` | `{{ items\|last }}` | Last item |
|
|
317
|
+
| `join` | `{{ items\|join:", " }}` | `a, b, c` |
|
|
318
|
+
| `reverse` | `{{ items\|reverse }}` | Reversed list |
|
|
319
|
+
| `sort` | `{{ items\|sort }}` | Sorted list |
|
|
320
|
+
| `unique` | `{{ items\|unique }}` | Unique items |
|
|
321
|
+
| `slice` | `{{ items\|slice:":2" }}` | First 2 items |
|
|
322
|
+
| `batch` | `{{ items\|batch:2 }}` | Grouped by 2 |
|
|
323
|
+
| `random` | `{{ items\|random }}` | Random item |
|
|
324
|
+
|
|
325
|
+
### Date Filters
|
|
326
|
+
|
|
327
|
+
| Filter | Example | Output |
|
|
328
|
+
|--------|---------|--------|
|
|
329
|
+
| `date` | `{{ now\|date:"Y-m-d" }}` | `2024-01-15` |
|
|
330
|
+
| `time` | `{{ now\|time:"H:i" }}` | `14:30` |
|
|
331
|
+
| `timesince` | `{{ past\|timesince }}` | `2 days ago` |
|
|
332
|
+
| `timeuntil` | `{{ future\|timeuntil }}` | `in 3 hours` |
|
|
333
|
+
|
|
334
|
+
### Safety & Encoding
|
|
335
|
+
|
|
336
|
+
| Filter | Example | Description |
|
|
337
|
+
|--------|---------|-------------|
|
|
338
|
+
| `escape` | `{{ html\|escape }}` | HTML escape |
|
|
339
|
+
| `safe` | `{{ html\|safe }}` | Mark as safe (no escape) |
|
|
340
|
+
| `urlencode` | `{{ url\|urlencode }}` | URL encode |
|
|
341
|
+
| `json` | `{{ data\|json }}` | JSON stringify |
|
|
342
|
+
|
|
343
|
+
### Default Values
|
|
344
|
+
|
|
345
|
+
| Filter | Example | Output |
|
|
346
|
+
|--------|---------|--------|
|
|
347
|
+
| `default` | `{{ missing\|default:"N/A" }}` | `N/A` |
|
|
348
|
+
| `default_if_none` | `{{ null\|default_if_none:"None" }}` | `None` |
|
|
349
|
+
| `yesno` | `{{ true\|yesno:"Yes,No" }}` | `Yes` |
|
|
350
|
+
| `pluralize` | `{{ count\|pluralize }}` | `s` or `` |
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Django Compatibility
|
|
355
|
+
|
|
356
|
+
binja is designed to be a drop-in replacement for Django templates:
|
|
357
|
+
|
|
358
|
+
```django
|
|
359
|
+
{# Django-style comments #}
|
|
360
|
+
|
|
361
|
+
{% load static %} {# Supported (no-op) #}
|
|
362
|
+
|
|
363
|
+
{% url 'home' %}
|
|
364
|
+
{% static 'css/style.css' %}
|
|
365
|
+
|
|
366
|
+
{% csrf_token %} {# Returns empty for JS compatibility #}
|
|
367
|
+
|
|
368
|
+
{{ forloop.counter }}
|
|
369
|
+
{{ forloop.first }}
|
|
370
|
+
{{ forloop.parentloop.counter }}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Configuration
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
const env = new Environment({
|
|
379
|
+
// Template directory
|
|
380
|
+
templates: './templates',
|
|
381
|
+
|
|
382
|
+
// Auto-escape HTML (default: true)
|
|
383
|
+
autoescape: true,
|
|
384
|
+
|
|
385
|
+
// Custom filters
|
|
386
|
+
filters: {
|
|
387
|
+
currency: (value: number) => `$${value.toFixed(2)}`,
|
|
388
|
+
highlight: (text: string, term: string) =>
|
|
389
|
+
text.replace(new RegExp(term, 'gi'), '<mark>$&</mark>')
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
// Global variables available in all templates
|
|
393
|
+
globals: {
|
|
394
|
+
site_name: 'My Website',
|
|
395
|
+
current_year: new Date().getFullYear()
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
// URL resolver for {% url %} tag
|
|
399
|
+
urlResolver: (name: string, ...args: any[]) => {
|
|
400
|
+
const routes = { home: '/', about: '/about', user: '/users/:id' }
|
|
401
|
+
return routes[name] || '#'
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
// Static file resolver for {% static %} tag
|
|
405
|
+
staticResolver: (path: string) => `/static/${path}`
|
|
406
|
+
})
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Custom Filters
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
const env = new Environment({
|
|
415
|
+
filters: {
|
|
416
|
+
// Simple filter
|
|
417
|
+
double: (value: number) => value * 2,
|
|
418
|
+
|
|
419
|
+
// Filter with argument
|
|
420
|
+
repeat: (value: string, times: number = 2) => value.repeat(times),
|
|
421
|
+
|
|
422
|
+
// Async filter
|
|
423
|
+
translate: async (value: string, lang: string) => {
|
|
424
|
+
return await translateAPI(value, lang)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Usage:
|
|
431
|
+
```django
|
|
432
|
+
{{ 5|double }} → 10
|
|
433
|
+
{{ "hi"|repeat:3 }} → hihihi
|
|
434
|
+
{{ "Hello"|translate:"es" }} → Hola
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## Security
|
|
440
|
+
|
|
441
|
+
### XSS Protection
|
|
442
|
+
|
|
443
|
+
Autoescape is enabled by default. All variables are HTML-escaped:
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
await render('{{ script }}', {
|
|
447
|
+
script: '<script>alert("xss")</script>'
|
|
448
|
+
})
|
|
449
|
+
// Output: <script>alert("xss")</script>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Marking Safe Content
|
|
453
|
+
|
|
454
|
+
```django
|
|
455
|
+
{{ trusted_html|safe }}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Performance Tips
|
|
461
|
+
|
|
462
|
+
1. **Reuse Environment** - Create once, render many times
|
|
463
|
+
2. **Enable caching** - Templates are cached automatically
|
|
464
|
+
3. **Use Bun** - Native Bun optimizations
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
// Good: Create once
|
|
468
|
+
const env = new Environment({ templates: './templates' })
|
|
469
|
+
|
|
470
|
+
// Render multiple times
|
|
471
|
+
app.get('/', () => env.render('home.html', data))
|
|
472
|
+
app.get('/about', () => env.render('about.html', data))
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## API Reference
|
|
478
|
+
|
|
479
|
+
### `render(template, context)`
|
|
480
|
+
|
|
481
|
+
Render a template string with context.
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
const html = await render('Hello {{ name }}', { name: 'World' })
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### `Environment`
|
|
488
|
+
|
|
489
|
+
Create a configured template environment.
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
const env = new Environment(options)
|
|
493
|
+
|
|
494
|
+
// Methods
|
|
495
|
+
env.render(name, context) // Render template file
|
|
496
|
+
env.renderString(str, context) // Render template string
|
|
497
|
+
env.addFilter(name, fn) // Add custom filter
|
|
498
|
+
env.addGlobal(name, value) // Add global variable
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## Examples
|
|
504
|
+
|
|
505
|
+
### Elysia Integration
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
import { Elysia } from 'elysia'
|
|
509
|
+
import { Environment } from 'binja'
|
|
510
|
+
|
|
511
|
+
const templates = new Environment({
|
|
512
|
+
templates: './views',
|
|
513
|
+
globals: {
|
|
514
|
+
site_name: 'My App',
|
|
515
|
+
current_year: new Date().getFullYear()
|
|
516
|
+
}
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
const app = new Elysia()
|
|
520
|
+
// HTML helper
|
|
521
|
+
.decorate('html', (name: string, ctx: object) => templates.render(name, ctx))
|
|
522
|
+
|
|
523
|
+
// Routes
|
|
524
|
+
.get('/', async ({ html }) => {
|
|
525
|
+
return new Response(await html('home.html', {
|
|
526
|
+
title: 'Welcome',
|
|
527
|
+
features: ['Fast', 'Secure', 'Easy']
|
|
528
|
+
}), {
|
|
529
|
+
headers: { 'Content-Type': 'text/html' }
|
|
530
|
+
})
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
.get('/users/:id', async ({ html, params }) => {
|
|
534
|
+
const user = await getUser(params.id)
|
|
535
|
+
return new Response(await html('user/profile.html', { user }), {
|
|
536
|
+
headers: { 'Content-Type': 'text/html' }
|
|
537
|
+
})
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
.listen(3000)
|
|
541
|
+
|
|
542
|
+
console.log('Server running at http://localhost:3000')
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### Elysia Plugin
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
import { Elysia } from 'elysia'
|
|
549
|
+
import { Environment } from 'binja'
|
|
550
|
+
|
|
551
|
+
// Create reusable plugin
|
|
552
|
+
const jinjaPlugin = (options: { templates: string }) => {
|
|
553
|
+
const env = new Environment(options)
|
|
554
|
+
|
|
555
|
+
return new Elysia({ name: 'jinja' })
|
|
556
|
+
.derive(async () => ({
|
|
557
|
+
render: async (name: string, context: object = {}) => {
|
|
558
|
+
const html = await env.render(name, context)
|
|
559
|
+
return new Response(html, {
|
|
560
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' }
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
}))
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Use in app
|
|
567
|
+
const app = new Elysia()
|
|
568
|
+
.use(jinjaPlugin({ templates: './views' }))
|
|
569
|
+
.get('/', ({ render }) => render('index.html', { title: 'Home' }))
|
|
570
|
+
.get('/about', ({ render }) => render('about.html'))
|
|
571
|
+
.listen(3000)
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### Elysia + HTMX
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
import { Elysia } from 'elysia'
|
|
578
|
+
import { Environment } from 'binja'
|
|
579
|
+
|
|
580
|
+
const templates = new Environment({ templates: './views' })
|
|
581
|
+
|
|
582
|
+
const app = new Elysia()
|
|
583
|
+
// Full page
|
|
584
|
+
.get('/', async () => {
|
|
585
|
+
const html = await templates.render('index.html', {
|
|
586
|
+
items: await getItems()
|
|
587
|
+
})
|
|
588
|
+
return new Response(html, {
|
|
589
|
+
headers: { 'Content-Type': 'text/html' }
|
|
590
|
+
})
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
// HTMX partial - returns only the component
|
|
594
|
+
.post('/items', async ({ body }) => {
|
|
595
|
+
const item = await createItem(body)
|
|
596
|
+
const html = await templates.renderString(`
|
|
597
|
+
<li id="item-{{ item.id }}" class="item">
|
|
598
|
+
{{ item.name }}
|
|
599
|
+
<button hx-delete="/items/{{ item.id }}" hx-target="#item-{{ item.id }}" hx-swap="outerHTML">
|
|
600
|
+
Delete
|
|
601
|
+
</button>
|
|
602
|
+
</li>
|
|
603
|
+
`, { item })
|
|
604
|
+
return new Response(html, {
|
|
605
|
+
headers: { 'Content-Type': 'text/html' }
|
|
606
|
+
})
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
.delete('/items/:id', async ({ params }) => {
|
|
610
|
+
await deleteItem(params.id)
|
|
611
|
+
return new Response('', { status: 200 })
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
.listen(3000)
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### Hono Integration
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
import { Hono } from 'hono'
|
|
621
|
+
import { Environment } from 'binja'
|
|
622
|
+
|
|
623
|
+
const app = new Hono()
|
|
624
|
+
const templates = new Environment({ templates: './views' })
|
|
625
|
+
|
|
626
|
+
app.get('/', async (c) => {
|
|
627
|
+
const html = await templates.render('index.html', {
|
|
628
|
+
title: 'Home',
|
|
629
|
+
user: c.get('user')
|
|
630
|
+
})
|
|
631
|
+
return c.html(html)
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
app.get('/products', async (c) => {
|
|
635
|
+
const products = await getProducts()
|
|
636
|
+
return c.html(await templates.render('products/list.html', { products }))
|
|
637
|
+
})
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### Email Templates
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
const env = new Environment({ templates: './emails' })
|
|
644
|
+
|
|
645
|
+
const html = await env.render('welcome.html', {
|
|
646
|
+
user: { name: 'John', email: 'john@example.com' },
|
|
647
|
+
activation_link: 'https://example.com/activate/xyz'
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
await sendEmail({
|
|
651
|
+
to: user.email,
|
|
652
|
+
subject: 'Welcome!',
|
|
653
|
+
html
|
|
654
|
+
})
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### PDF Generation
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
import { Environment } from 'binja'
|
|
661
|
+
|
|
662
|
+
const templates = new Environment({ templates: './templates' })
|
|
663
|
+
|
|
664
|
+
// Render invoice HTML
|
|
665
|
+
const html = await templates.render('invoice.html', {
|
|
666
|
+
invoice: {
|
|
667
|
+
number: 'INV-2024-001',
|
|
668
|
+
date: new Date(),
|
|
669
|
+
customer: { name: 'Acme Corp', address: '123 Main St' },
|
|
670
|
+
items: [
|
|
671
|
+
{ name: 'Service A', qty: 2, price: 100 },
|
|
672
|
+
{ name: 'Service B', qty: 1, price: 250 }
|
|
673
|
+
],
|
|
674
|
+
total: 450
|
|
675
|
+
}
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
// Use with any PDF library (puppeteer, playwright, etc.)
|
|
679
|
+
const pdf = await generatePDF(html)
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### Static Site Generator
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
685
|
+
import { Environment } from 'binja'
|
|
686
|
+
import { readdir, writeFile, mkdir } from 'fs/promises'
|
|
687
|
+
|
|
688
|
+
const env = new Environment({ templates: './src/templates' })
|
|
689
|
+
|
|
690
|
+
// Build all pages
|
|
691
|
+
const pages = [
|
|
692
|
+
{ template: 'index.html', output: 'dist/index.html', data: { title: 'Home' } },
|
|
693
|
+
{ template: 'about.html', output: 'dist/about.html', data: { title: 'About' } },
|
|
694
|
+
{ template: 'contact.html', output: 'dist/contact.html', data: { title: 'Contact' } }
|
|
695
|
+
]
|
|
696
|
+
|
|
697
|
+
await mkdir('dist', { recursive: true })
|
|
698
|
+
|
|
699
|
+
for (const page of pages) {
|
|
700
|
+
const html = await env.render(page.template, page.data)
|
|
701
|
+
await writeFile(page.output, html)
|
|
702
|
+
console.log(`Built: ${page.output}`)
|
|
703
|
+
}
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
---
|
|
707
|
+
|
|
708
|
+
## Acknowledgments
|
|
709
|
+
|
|
710
|
+
binja is inspired by and aims to be compatible with:
|
|
711
|
+
|
|
712
|
+
- **[Jinja2](https://jinja.palletsprojects.com/)** - The original Python template engine by Pallets Projects (BSD-3-Clause)
|
|
713
|
+
- **[Django Template Language](https://docs.djangoproject.com/en/stable/ref/templates/language/)** - Django's built-in template system (BSD-3-Clause)
|
|
714
|
+
|
|
715
|
+
---
|
|
716
|
+
|
|
717
|
+
## License
|
|
718
|
+
|
|
719
|
+
BSD-3-Clause
|
|
720
|
+
|
|
721
|
+
See [LICENSE](./LICENSE) for details.
|
|
722
|
+
|
|
723
|
+
---
|
|
724
|
+
|
|
725
|
+
<p align="center">
|
|
726
|
+
Made with ❤️ for the Bun ecosystem
|
|
727
|
+
</p>
|
|
728
|
+
# binja
|