binja 0.9.0 → 0.9.1
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 +223 -17
- package/dist/ai/index.d.ts +24 -0
- package/dist/ai/index.js +113 -0
- package/dist/ai/lint.d.ts +26 -0
- package/dist/ai/prompt.d.ts +10 -0
- package/dist/ai/providers/anthropic.d.ts +6 -0
- package/dist/ai/providers/groq.d.ts +6 -0
- package/dist/ai/providers/index.d.ts +22 -0
- package/dist/ai/providers/ollama.d.ts +6 -0
- package/dist/ai/providers/openai.d.ts +6 -0
- package/dist/ai/types.d.ts +42 -0
- package/dist/cli.js +183 -30
- package/package.json +22 -2
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<a href="#installation">Installation</a> •
|
|
9
9
|
<a href="#quick-start">Quick Start</a> •
|
|
10
|
-
<a href="#
|
|
11
|
-
<a href="#
|
|
12
|
-
<a href="#filters">Filters</a>
|
|
10
|
+
<a href="#framework-adapters">Hono/Elysia</a> •
|
|
11
|
+
<a href="#multi-engine-support">Multi-Engine</a> •
|
|
12
|
+
<a href="#filters-84-built-in">Filters</a>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
|---------|-----------|------------------|
|
|
28
28
|
| **Runtime Performance** | ✅ 2-4x faster | ❌ |
|
|
29
29
|
| **AOT Compilation** | ✅ 160x faster | ❌ |
|
|
30
|
-
| **Multi-Engine** | ✅ Jinja2, Handlebars, Liquid | ❌ |
|
|
30
|
+
| **Multi-Engine** | ✅ Jinja2, Handlebars, Liquid, Twig | ❌ |
|
|
31
|
+
| **Framework Adapters** | ✅ Hono, Elysia | ❌ |
|
|
31
32
|
| Django DTL Compatible | ✅ 100% | ❌ Partial |
|
|
32
33
|
| Jinja2 Compatible | ✅ Full | ⚠️ Limited |
|
|
33
34
|
| Template Inheritance | ✅ | ⚠️ |
|
|
@@ -430,6 +431,7 @@ Binja supports multiple template engines through a unified API. All engines pars
|
|
|
430
431
|
| **Jinja2/DTL** | `{{ var }}` `{% if %}` | Default, Python/Django compatibility |
|
|
431
432
|
| **Handlebars** | `{{var}}` `{{#if}}` | JavaScript ecosystem, Ember.js |
|
|
432
433
|
| **Liquid** | `{{ var }}` `{% if %}` | Shopify, Jekyll, static sites |
|
|
434
|
+
| **Twig** | `{{ var }}` `{% if %}` | PHP/Symfony, Drupal, Craft CMS |
|
|
433
435
|
|
|
434
436
|
### Usage
|
|
435
437
|
|
|
@@ -437,6 +439,7 @@ Binja supports multiple template engines through a unified API. All engines pars
|
|
|
437
439
|
// Direct engine imports
|
|
438
440
|
import * as handlebars from 'binja/engines/handlebars'
|
|
439
441
|
import * as liquid from 'binja/engines/liquid'
|
|
442
|
+
import * as twig from 'binja/engines/twig'
|
|
440
443
|
|
|
441
444
|
// Handlebars
|
|
442
445
|
await handlebars.render('Hello {{name}}!', { name: 'World' })
|
|
@@ -447,6 +450,11 @@ await handlebars.render('{{{html}}}', { html: '<b>unescaped</b>' })
|
|
|
447
450
|
await liquid.render('Hello {{ name }}!', { name: 'World' })
|
|
448
451
|
await liquid.render('{% for item in items %}{{ item }}{% endfor %}', { items: ['a', 'b'] })
|
|
449
452
|
await liquid.render('{% assign x = "value" %}{{ x }}', {})
|
|
453
|
+
|
|
454
|
+
// Twig (Symfony)
|
|
455
|
+
await twig.render('Hello {{ name }}!', { name: 'World' })
|
|
456
|
+
await twig.render('{% for item in items %}{{ item }}{% endfor %}', { items: ['a', 'b'] })
|
|
457
|
+
await twig.render('{{ name|upper }}', { name: 'world' })
|
|
450
458
|
```
|
|
451
459
|
|
|
452
460
|
### MultiEngine API
|
|
@@ -459,26 +467,117 @@ const engine = new MultiEngine()
|
|
|
459
467
|
// Render with any engine
|
|
460
468
|
await engine.render('Hello {{name}}!', { name: 'World' }, 'handlebars')
|
|
461
469
|
await engine.render('Hello {{ name }}!', { name: 'World' }, 'liquid')
|
|
470
|
+
await engine.render('Hello {{ name }}!', { name: 'World' }, 'twig')
|
|
462
471
|
await engine.render('Hello {{ name }}!', { name: 'World' }, 'jinja2')
|
|
463
472
|
|
|
464
473
|
// Auto-detect from file extension
|
|
465
474
|
import { detectEngine } from 'binja/engines'
|
|
466
|
-
const eng = detectEngine('template.hbs')
|
|
467
|
-
const eng2 = detectEngine('page.liquid')
|
|
475
|
+
const eng = detectEngine('template.hbs') // Returns Handlebars engine
|
|
476
|
+
const eng2 = detectEngine('page.liquid') // Returns Liquid engine
|
|
477
|
+
const eng3 = detectEngine('page.twig') // Returns Twig engine
|
|
468
478
|
```
|
|
469
479
|
|
|
470
480
|
### Engine Feature Matrix
|
|
471
481
|
|
|
472
|
-
| Feature | Jinja2 | Handlebars | Liquid |
|
|
473
|
-
|
|
474
|
-
| Variables | `{{ x }}` | `{{x}}` | `{{ x }}` |
|
|
475
|
-
| Conditionals | `{% if %}` | `{{#if}}` | `{% if %}` |
|
|
476
|
-
| Loops | `{% for %}` | `{{#each}}` | `{% for %}` |
|
|
477
|
-
| Filters | `{{ x\|filter }}` | `{{ x }}` | `{{ x \| filter }}` |
|
|
478
|
-
| Raw output | `{% raw %}` | - | `{% raw %}` |
|
|
479
|
-
| Comments | `{# #}` | `{{! }}` | `{% comment %}` |
|
|
480
|
-
| Assignment | `{% set %}` | - | `{% assign %}` |
|
|
481
|
-
| Unescaped | `{{ x\|safe }}` | `{{{x}}}` | - |
|
|
482
|
+
| Feature | Jinja2 | Handlebars | Liquid | Twig |
|
|
483
|
+
|---------|--------|------------|--------|------|
|
|
484
|
+
| Variables | `{{ x }}` | `{{x}}` | `{{ x }}` | `{{ x }}` |
|
|
485
|
+
| Conditionals | `{% if %}` | `{{#if}}` | `{% if %}` | `{% if %}` |
|
|
486
|
+
| Loops | `{% for %}` | `{{#each}}` | `{% for %}` | `{% for %}` |
|
|
487
|
+
| Filters | `{{ x\|filter }}` | `{{ x }}` | `{{ x \| filter }}` | `{{ x\|filter }}` |
|
|
488
|
+
| Raw output | `{% raw %}` | - | `{% raw %}` | `{% raw %}` |
|
|
489
|
+
| Comments | `{# #}` | `{{! }}` | `{% comment %}` | `{# #}` |
|
|
490
|
+
| Assignment | `{% set %}` | - | `{% assign %}` | `{% set %}` |
|
|
491
|
+
| Unescaped | `{{ x\|safe }}` | `{{{x}}}` | - | `{{ x\|raw }}` |
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## Framework Adapters
|
|
496
|
+
|
|
497
|
+
Binja provides first-class integration with Bun's most popular web frameworks.
|
|
498
|
+
|
|
499
|
+
### Hono
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
import { Hono } from 'hono'
|
|
503
|
+
import { binja } from 'binja/hono'
|
|
504
|
+
|
|
505
|
+
const app = new Hono()
|
|
506
|
+
|
|
507
|
+
// Add binja middleware
|
|
508
|
+
app.use(binja({
|
|
509
|
+
root: './views', // Template directory
|
|
510
|
+
extension: '.html', // Default extension
|
|
511
|
+
engine: 'jinja2', // jinja2 | handlebars | liquid | twig
|
|
512
|
+
cache: true, // Cache compiled templates
|
|
513
|
+
globals: { siteName: 'My App' }, // Global context
|
|
514
|
+
layout: 'layouts/base', // Optional layout template
|
|
515
|
+
}))
|
|
516
|
+
|
|
517
|
+
// Render templates with c.render()
|
|
518
|
+
app.get('/', (c) => c.render('index', { title: 'Home' }))
|
|
519
|
+
app.get('/users/:id', async (c) => {
|
|
520
|
+
const user = await getUser(c.req.param('id'))
|
|
521
|
+
return c.render('users/profile', { user })
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
export default app
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Elysia
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
import { Elysia } from 'elysia'
|
|
531
|
+
import { binja } from 'binja/elysia'
|
|
532
|
+
|
|
533
|
+
const app = new Elysia()
|
|
534
|
+
// Add binja plugin
|
|
535
|
+
.use(binja({
|
|
536
|
+
root: './views',
|
|
537
|
+
extension: '.html',
|
|
538
|
+
engine: 'jinja2',
|
|
539
|
+
cache: true,
|
|
540
|
+
globals: { siteName: 'My App' },
|
|
541
|
+
layout: 'layouts/base',
|
|
542
|
+
}))
|
|
543
|
+
// Render templates with render()
|
|
544
|
+
.get('/', ({ render }) => render('index', { title: 'Home' }))
|
|
545
|
+
.get('/users/:id', async ({ render, params }) => {
|
|
546
|
+
const user = await getUser(params.id)
|
|
547
|
+
return render('users/profile', { user })
|
|
548
|
+
})
|
|
549
|
+
.listen(3000)
|
|
550
|
+
|
|
551
|
+
console.log('Server running at http://localhost:3000')
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Adapter Options
|
|
555
|
+
|
|
556
|
+
| Option | Type | Default | Description |
|
|
557
|
+
|--------|------|---------|-------------|
|
|
558
|
+
| `root` | `string` | `./views` | Template directory |
|
|
559
|
+
| `extension` | `string` | `.html` | Default file extension |
|
|
560
|
+
| `engine` | `string` | `jinja2` | Template engine (`jinja2`, `handlebars`, `liquid`, `twig`) |
|
|
561
|
+
| `cache` | `boolean` | `true` (prod) | Cache compiled templates |
|
|
562
|
+
| `debug` | `boolean` | `false` | Show error details |
|
|
563
|
+
| `globals` | `object` | `{}` | Global context variables |
|
|
564
|
+
| `layout` | `string` | - | Layout template path |
|
|
565
|
+
| `contentVar` | `string` | `content` | Content variable name in layout |
|
|
566
|
+
|
|
567
|
+
### Cache Management
|
|
568
|
+
|
|
569
|
+
```typescript
|
|
570
|
+
import { clearCache, getCacheStats } from 'binja/hono'
|
|
571
|
+
// or
|
|
572
|
+
import { clearCache, getCacheStats } from 'binja/elysia'
|
|
573
|
+
|
|
574
|
+
// Clear all cached templates
|
|
575
|
+
clearCache()
|
|
576
|
+
|
|
577
|
+
// Get cache statistics
|
|
578
|
+
const stats = getCacheStats()
|
|
579
|
+
console.log(stats) // { size: 10, keys: ['jinja2:./views/index.html', ...] }
|
|
580
|
+
```
|
|
482
581
|
|
|
483
582
|
---
|
|
484
583
|
|
|
@@ -609,7 +708,7 @@ debugOptions: {
|
|
|
609
708
|
|
|
610
709
|
## CLI Tool
|
|
611
710
|
|
|
612
|
-
Binja includes a CLI for template pre-compilation:
|
|
711
|
+
Binja includes a CLI for template pre-compilation and linting:
|
|
613
712
|
|
|
614
713
|
```bash
|
|
615
714
|
# Compile all templates to JavaScript
|
|
@@ -620,6 +719,15 @@ binja check ./templates
|
|
|
620
719
|
|
|
621
720
|
# Watch mode for development
|
|
622
721
|
binja watch ./templates -o ./dist
|
|
722
|
+
|
|
723
|
+
# Lint templates (syntax check)
|
|
724
|
+
binja lint ./templates
|
|
725
|
+
|
|
726
|
+
# Lint with AI analysis (requires API key)
|
|
727
|
+
binja lint ./templates --ai
|
|
728
|
+
|
|
729
|
+
# Lint with specific AI provider
|
|
730
|
+
binja lint ./templates --ai=ollama
|
|
623
731
|
```
|
|
624
732
|
|
|
625
733
|
### Pre-compiled Templates
|
|
@@ -633,6 +741,104 @@ const html = render({ title: 'Home', items: [...] })
|
|
|
633
741
|
|
|
634
742
|
---
|
|
635
743
|
|
|
744
|
+
## AI-Powered Linting (Optional)
|
|
745
|
+
|
|
746
|
+
Binja includes an optional AI-powered linting module that detects security issues, performance problems, accessibility concerns, and best practice violations.
|
|
747
|
+
|
|
748
|
+
### Installation
|
|
749
|
+
|
|
750
|
+
The AI module is opt-in. Install the SDK for your preferred provider:
|
|
751
|
+
|
|
752
|
+
```bash
|
|
753
|
+
# For Claude (Anthropic)
|
|
754
|
+
bun add @anthropic-ai/sdk
|
|
755
|
+
|
|
756
|
+
# For OpenAI
|
|
757
|
+
bun add openai
|
|
758
|
+
|
|
759
|
+
# For Ollama (local) - no package needed
|
|
760
|
+
# For Groq - no package needed
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
### Configuration
|
|
764
|
+
|
|
765
|
+
Set the API key for your provider:
|
|
766
|
+
|
|
767
|
+
```bash
|
|
768
|
+
# Anthropic
|
|
769
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
770
|
+
|
|
771
|
+
# OpenAI
|
|
772
|
+
export OPENAI_API_KEY=sk-...
|
|
773
|
+
|
|
774
|
+
# Groq (free tier available)
|
|
775
|
+
export GROQ_API_KEY=gsk_...
|
|
776
|
+
|
|
777
|
+
# Ollama - no key needed, just run: ollama serve
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
### Usage
|
|
781
|
+
|
|
782
|
+
#### CLI
|
|
783
|
+
|
|
784
|
+
```bash
|
|
785
|
+
# Lint with AI (auto-detect provider)
|
|
786
|
+
binja lint ./templates --ai
|
|
787
|
+
|
|
788
|
+
# Use specific provider
|
|
789
|
+
binja lint ./templates --ai=anthropic
|
|
790
|
+
binja lint ./templates --ai=openai
|
|
791
|
+
binja lint ./templates --ai=ollama
|
|
792
|
+
binja lint ./templates --ai=groq
|
|
793
|
+
|
|
794
|
+
# JSON output for CI/CD
|
|
795
|
+
binja lint ./templates --ai --format=json
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
#### Programmatic
|
|
799
|
+
|
|
800
|
+
```typescript
|
|
801
|
+
import { lint } from 'binja/ai'
|
|
802
|
+
|
|
803
|
+
// Auto-detect provider from environment
|
|
804
|
+
const result = await lint(template)
|
|
805
|
+
|
|
806
|
+
// Specify provider and API key directly
|
|
807
|
+
const result = await lint(template, {
|
|
808
|
+
provider: 'anthropic',
|
|
809
|
+
apiKey: 'sk-ant-...',
|
|
810
|
+
model: 'claude-sonnet-4-20250514'
|
|
811
|
+
})
|
|
812
|
+
|
|
813
|
+
// Check results
|
|
814
|
+
console.log(result.errors) // Syntax errors
|
|
815
|
+
console.log(result.warnings) // Security, performance issues
|
|
816
|
+
console.log(result.suggestions) // Best practice recommendations
|
|
817
|
+
console.log(result.provider) // Which AI was used
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
### What It Detects
|
|
821
|
+
|
|
822
|
+
| Category | Examples |
|
|
823
|
+
|----------|----------|
|
|
824
|
+
| **Security** | XSS vulnerabilities, `\|safe` on user input, sensitive data exposure |
|
|
825
|
+
| **Performance** | Heavy filters in loops, repeated calculations |
|
|
826
|
+
| **Accessibility** | Missing alt text, forms without labels |
|
|
827
|
+
| **Best Practices** | `{% for %}` without `{% empty %}`, deep nesting |
|
|
828
|
+
|
|
829
|
+
### Provider Comparison
|
|
830
|
+
|
|
831
|
+
| Provider | API Key | Speed | Cost |
|
|
832
|
+
|----------|---------|-------|------|
|
|
833
|
+
| **Anthropic** | `ANTHROPIC_API_KEY` | Fast | Paid |
|
|
834
|
+
| **OpenAI** | `OPENAI_API_KEY` | Fast | Paid |
|
|
835
|
+
| **Groq** | `GROQ_API_KEY` | Very Fast | Free tier |
|
|
836
|
+
| **Ollama** | None (local) | Varies | Free |
|
|
837
|
+
|
|
838
|
+
Auto-detect priority: Anthropic → OpenAI → Groq → Ollama
|
|
839
|
+
|
|
840
|
+
---
|
|
841
|
+
|
|
636
842
|
## Raw/Verbatim Tag
|
|
637
843
|
|
|
638
844
|
Output template syntax without processing:
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* binja AI Module - Optional AI-powered template linting
|
|
3
|
+
*
|
|
4
|
+
* This module is opt-in and requires an AI provider:
|
|
5
|
+
* - Anthropic Claude: bun add @anthropic-ai/sdk + ANTHROPIC_API_KEY
|
|
6
|
+
* - OpenAI GPT-4: bun add openai + OPENAI_API_KEY
|
|
7
|
+
* - Groq (free): GROQ_API_KEY
|
|
8
|
+
* - Ollama (local): ollama running on localhost:11434
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { lint } from 'binja/ai'
|
|
13
|
+
*
|
|
14
|
+
* const result = await lint(template)
|
|
15
|
+
* console.log(result.warnings) // AI-detected issues
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @module binja/ai
|
|
19
|
+
*/
|
|
20
|
+
export { lint, syntaxCheck } from './lint';
|
|
21
|
+
export { resolveProvider, detectProvider, getProvider } from './providers';
|
|
22
|
+
export { createAnthropicProvider, createOpenAIProvider, createOllamaProvider, createGroqProvider, } from './providers';
|
|
23
|
+
export type { Issue, IssueType, IssueSeverity, LintResult, LintOptions, AIProvider, } from './types';
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/ai/index.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var j=Object.create;var{getPrototypeOf:b,defineProperty:X,getOwnPropertyNames:v}=Object;var f=Object.prototype.hasOwnProperty;var $=(A,E,O)=>{O=A!=null?j(b(A)):{};let I=E||!A||!A.__esModule?X(O,"default",{value:A,enumerable:!0}):O;for(let P of v(A))if(!f.call(I,P))X(I,P,{get:()=>A[P],enumerable:!0});return I};var F=import.meta.require;var J={and:"AND",or:"OR",not:"NOT",true:"NAME",false:"NAME",True:"NAME",False:"NAME",None:"NAME",none:"NAME",is:"NAME",in:"NAME"};var h={red:"\x1B[31m",yellow:"\x1B[33m",cyan:"\x1B[36m",gray:"\x1B[90m",bold:"\x1B[1m",dim:"\x1B[2m",reset:"\x1B[0m"},x=process.stdout?.isTTY!==!1;function B(A,E){return x?`${h[A]}${E}${h.reset}`:E}class _ extends Error{line;column;source;templateName;suggestion;constructor(A,E){let O=g("TemplateSyntaxError",A,E);super(O);this.name="TemplateSyntaxError",this.line=E.line,this.column=E.column,this.source=E.source,this.templateName=E.templateName,this.suggestion=E.suggestion}}function g(A,E,O){let I=[],P=O.templateName?`${O.templateName}:${O.line}:${O.column}`:`line ${O.line}, column ${O.column}`;if(I.push(`${B("red",B("bold",A))}: ${E} at ${B("cyan",P)}`),O.source)I.push(""),I.push(u(O.source,O.line,O.column));if(O.suggestion)I.push(""),I.push(`${B("yellow","Did you mean")}: ${B("cyan",O.suggestion)}?`);if(O.availableOptions&&O.availableOptions.length>0){I.push("");let R=O.availableOptions.slice(0,8),M=O.availableOptions.length>8?` ${B("gray",`... and ${O.availableOptions.length-8} more`)}`:"";I.push(`${B("gray","Available")}: ${R.join(", ")}${M}`)}return I.join(`
|
|
3
|
+
`)}function u(A,E,O){let I=A.split(`
|
|
4
|
+
`),P=[],R=Math.max(1,E-2),M=Math.min(I.length,E+1),L=String(M).length;for(let N=R;N<=M;N++){let C=I[N-1]||"",K=String(N).padStart(L," ");if(N===E){P.push(`${B("red"," \u2192")} ${B("gray",K)} ${B("dim","\u2502")} ${C}`);let W=" ".repeat(L+4+Math.max(0,O-1)),V=B("red","^");P.push(`${W}${V}`)}else P.push(` ${B("gray",K)} ${B("dim","\u2502")} ${B("gray",C)}`)}return P.join(`
|
|
5
|
+
`)}class D{state;variableStart;variableEnd;blockStart;blockEnd;commentStart;commentEnd;constructor(A,E={}){this.state={source:A,pos:0,line:1,column:1,tokens:[]},this.variableStart=E.variableStart??"{{",this.variableEnd=E.variableEnd??"}}",this.blockStart=E.blockStart??"{%",this.blockEnd=E.blockEnd??"%}",this.commentStart=E.commentStart??"{#",this.commentEnd=E.commentEnd??"#}"}tokenize(){while(!this.isAtEnd())this.scanToken();return this.addToken("EOF",""),this.state.tokens}scanToken(){if(this.match(this.variableStart)){this.addToken("VARIABLE_START",this.variableStart),this.scanExpression(this.variableEnd,"VARIABLE_END");return}if(this.match(this.blockStart)){let A=this.peek()==="-";if(A)this.advance();let E=this.state.pos;if(this.skipWhitespace(),this.checkWord("raw")||this.checkWord("verbatim")){let O=this.checkWord("raw")?"raw":"verbatim";this.scanRawBlock(O,A);return}this.state.pos=E,this.addToken("BLOCK_START",this.blockStart+(A?"-":"")),this.scanExpression(this.blockEnd,"BLOCK_END");return}if(this.match(this.commentStart)){this.scanComment();return}this.scanText()}checkWord(A){let E=this.state.pos;for(let I=0;I<A.length;I++)if(this.state.source[E+I]?.toLowerCase()!==A[I])return!1;let O=this.state.source[E+A.length];return!O||!this.isAlphaNumeric(O)}scanRawBlock(A,E){let O=this.state.line,I=this.state.column;for(let M=0;M<A.length;M++)this.advance();if(this.skipWhitespace(),this.peek()==="-")this.advance();if(!this.match(this.blockEnd))throw new _(`Expected '${this.blockEnd}' after '${A}'`,{line:this.state.line,column:this.state.column,source:this.state.source});let P=`end${A}`,R=this.state.pos;while(!this.isAtEnd()){if(this.check(this.blockStart)){let M=this.state.pos,L=this.state.line,N=this.state.column;if(this.match(this.blockStart),this.peek()==="-")this.advance();if(this.skipWhitespace(),this.checkWord(P)){let C=this.state.source.slice(R,M);if(C.length>0)this.state.tokens.push({type:"TEXT",value:C,line:O,column:I});for(let K=0;K<P.length;K++)this.advance();if(this.skipWhitespace(),this.peek()==="-")this.advance();if(!this.match(this.blockEnd))throw new _(`Expected '${this.blockEnd}' after '${P}'`,{line:this.state.line,column:this.state.column,source:this.state.source});return}this.state.pos=M,this.state.line=L,this.state.column=N}if(this.peek()===`
|
|
6
|
+
`)this.state.line++,this.state.column=0;this.advance()}throw new _(`Unclosed '${A}' block`,{line:O,column:I,source:this.state.source,suggestion:`Add {% end${A} %} to close the block`})}scanText(){let A=this.state.pos,E=this.state.line,O=this.state.column;while(!this.isAtEnd()){if(this.check(this.variableStart)||this.check(this.blockStart)||this.check(this.commentStart))break;if(this.peek()===`
|
|
7
|
+
`)this.state.line++,this.state.column=0;this.advance()}if(this.state.pos>A){let I=this.state.source.slice(A,this.state.pos);this.state.tokens.push({type:"TEXT",value:I,line:E,column:O})}}scanExpression(A,E){this.skipWhitespace();while(!this.isAtEnd()){if(this.skipWhitespace(),this.peek()==="-"&&this.check(A,1))this.advance();if(this.match(A)){this.addToken(E,A);return}this.scanExpressionToken()}throw new _("Unclosed template tag",{line:this.state.line,column:this.state.column,source:this.state.source,suggestion:`Add closing delimiter '${A}'`})}scanExpressionToken(){if(this.skipWhitespace(),this.isAtEnd())return;let A=this.peek();if(A==='"'||A==="'"){this.scanString(A);return}if(this.isDigit(A)){this.scanNumber();return}if(this.isAlpha(A)||A==="_"){this.scanIdentifier();return}this.scanOperator()}scanString(A){this.advance();let E=this.state.pos;while(!this.isAtEnd()&&this.peek()!==A){if(this.peek()==="\\"&&this.peekNext()===A)this.advance();if(this.peek()===`
|
|
8
|
+
`)this.state.line++,this.state.column=0;this.advance()}if(this.isAtEnd())throw new _("Unterminated string literal",{line:this.state.line,column:this.state.column,source:this.state.source,suggestion:`Add closing quote '${A}'`});let O=this.state.source.slice(E,this.state.pos);this.advance(),this.addToken("STRING",O)}scanNumber(){let A=this.state.pos;while(this.isDigit(this.peek()))this.advance();if(this.peek()==="."&&this.isDigit(this.peekNext())){this.advance();while(this.isDigit(this.peek()))this.advance()}let E=this.state.source.slice(A,this.state.pos);this.addToken("NUMBER",E)}scanIdentifier(){let A=this.state.pos;while(this.isAlphaNumeric(this.peek())||this.peek()==="_")this.advance();let E=this.state.source.slice(A,this.state.pos),O=J[E]??"NAME";this.addToken(O,E)}scanOperator(){let A=this.advance();switch(A){case".":this.addToken("DOT",A);break;case",":this.addToken("COMMA",A);break;case":":this.addToken("COLON",A);break;case"|":this.addToken("PIPE",A);break;case"(":this.addToken("LPAREN",A);break;case")":this.addToken("RPAREN",A);break;case"[":this.addToken("LBRACKET",A);break;case"]":this.addToken("RBRACKET",A);break;case"{":this.addToken("LBRACE",A);break;case"}":this.addToken("RBRACE",A);break;case"+":this.addToken("ADD",A);break;case"-":this.addToken("SUB",A);break;case"*":this.addToken("MUL",A);break;case"/":this.addToken("DIV",A);break;case"%":this.addToken("MOD",A);break;case"~":this.addToken("TILDE",A);break;case"=":if(this.match("="))this.addToken("EQ","==");else this.addToken("ASSIGN","=");break;case"!":if(this.match("="))this.addToken("NE","!=");else throw new _("Unexpected character '!'",{line:this.state.line,column:this.state.column-1,source:this.state.source,suggestion:"Use '!=' for not-equal comparison or 'not' for negation"});break;case"<":if(this.match("="))this.addToken("LE","<=");else this.addToken("LT","<");break;case">":if(this.match("="))this.addToken("GE",">=");else this.addToken("GT",">");break;case"?":if(this.match("?"))this.addToken("NULLCOALESCE","??");else this.addToken("QUESTION","?");break;default:if(!this.isWhitespace(A))throw new _(`Unexpected character '${A}'`,{line:this.state.line,column:this.state.column-1,source:this.state.source})}}scanComment(){while(!this.isAtEnd()&&!this.check(this.commentEnd)){if(this.peek()===`
|
|
9
|
+
`)this.state.line++,this.state.column=0;this.advance()}if(!this.isAtEnd())this.match(this.commentEnd)}isAtEnd(){return this.state.pos>=this.state.source.length}peek(){if(this.isAtEnd())return"\x00";return this.state.source[this.state.pos]}peekNext(){if(this.state.pos+1>=this.state.source.length)return"\x00";return this.state.source[this.state.pos+1]}advance(){let A=this.state.source[this.state.pos];return this.state.pos++,this.state.column++,A}match(A,E=0){let O=this.state.source,I=this.state.pos+E,P=A.length;if(I+P>O.length)return!1;for(let R=0;R<P;R++)if(O[I+R]!==A[R])return!1;if(E===0)this.state.pos+=P,this.state.column+=P;return!0}check(A,E=0){let O=this.state.source,I=this.state.pos+E,P=A.length;if(I+P>O.length)return!1;for(let R=0;R<P;R++)if(O[I+R]!==A[R])return!1;return!0}skipWhitespace(){while(!this.isAtEnd()&&this.isWhitespace(this.peek())){if(this.peek()===`
|
|
10
|
+
`)this.state.line++,this.state.column=0;this.advance()}}isWhitespace(A){return A===" "||A==="\t"||A===`
|
|
11
|
+
`||A==="\r"}isDigit(A){let E=A.charCodeAt(0);return E>=48&&E<=57}isAlpha(A){let E=A.charCodeAt(0);return E>=97&&E<=122||E>=65&&E<=90}isAlphaNumeric(A){let E=A.charCodeAt(0);return E>=48&&E<=57||E>=97&&E<=122||E>=65&&E<=90}addToken(A,E){this.state.tokens.push({type:A,value:E,line:this.state.line,column:this.state.column-E.length})}}class Q{tokens;current=0;source;constructor(A,E){this.tokens=A,this.source=E}parse(){let A=[];while(!this.isAtEnd()){let E=this.parseStatement();if(E)A.push(E)}return{type:"Template",body:A,line:1,column:1}}parseStatement(){switch(this.peek().type){case"TEXT":return this.parseText();case"VARIABLE_START":return this.parseOutput();case"BLOCK_START":return this.parseBlock();case"EOF":return null;default:return this.advance(),null}}parseText(){let A=this.advance();return{type:"Text",value:A.value,line:A.line,column:A.column}}parseOutput(){let A=this.advance(),E=this.parseExpression();return this.expect("VARIABLE_END"),{type:"Output",expression:E,line:A.line,column:A.column}}parseBlock(){let A=this.advance(),E=this.expect("NAME");switch(E.value){case"if":return this.parseIf(A);case"for":return this.parseFor(A);case"block":return this.parseBlockTag(A);case"extends":return this.parseExtends(A);case"include":return this.parseInclude(A);case"set":return this.parseSet(A);case"with":return this.parseWith(A);case"load":return this.parseLoad(A);case"url":return this.parseUrl(A);case"static":return this.parseStatic(A);case"now":return this.parseNow(A);case"comment":return this.parseComment(A);case"spaceless":case"autoescape":case"verbatim":return this.parseSimpleBlock(A,E.value);case"cycle":return this.parseCycle(A);case"firstof":return this.parseFirstof(A);case"ifchanged":return this.parseIfchanged(A);case"regroup":return this.parseRegroup(A);case"widthratio":return this.parseWidthratio(A);case"lorem":return this.parseLorem(A);case"csrf_token":return this.parseCsrfToken(A);case"debug":return this.parseDebug(A);case"templatetag":return this.parseTemplatetag(A);case"ifequal":return this.parseIfequal(A,!1);case"ifnotequal":return this.parseIfequal(A,!0);default:return this.skipToBlockEnd(),null}}parseIf(A){let E=this.parseExpression();this.expect("BLOCK_END");let O=[],I=[],P=[];while(!this.isAtEnd()){if(this.checkBlockTag("elif")||this.checkBlockTag("else")||this.checkBlockTag("endif"))break;let R=this.parseStatement();if(R)O.push(R)}while(this.checkBlockTag("elif")){this.advance(),this.advance();let R=this.parseExpression();this.expect("BLOCK_END");let M=[];while(!this.isAtEnd()){if(this.checkBlockTag("elif")||this.checkBlockTag("else")||this.checkBlockTag("endif"))break;let L=this.parseStatement();if(L)M.push(L)}I.push({test:R,body:M})}if(this.checkBlockTag("else")){this.advance(),this.advance(),this.expect("BLOCK_END");while(!this.isAtEnd()){if(this.checkBlockTag("endif"))break;let R=this.parseStatement();if(R)P.push(R)}}return this.expectBlockTag("endif"),{type:"If",test:E,body:O,elifs:I,else_:P,line:A.line,column:A.column}}parseFor(A){let E,O=this.expect("NAME").value;if(this.check("COMMA")){let N=[O];while(this.match("COMMA"))N.push(this.expect("NAME").value);E=N}else E=O;let I=this.expect("NAME");if(I.value!=="in")throw this.error(`Expected 'in' in for loop, got '${I.value}'`);let P=this.parseExpression(),R=this.check("NAME")&&this.peek().value==="recursive";if(R)this.advance();this.expect("BLOCK_END");let M=[],L=[];while(!this.isAtEnd()){if(this.checkBlockTag("empty")||this.checkBlockTag("else")||this.checkBlockTag("endfor"))break;let N=this.parseStatement();if(N)M.push(N)}if(this.checkBlockTag("empty")||this.checkBlockTag("else")){this.advance(),this.advance(),this.expect("BLOCK_END");while(!this.isAtEnd()){if(this.checkBlockTag("endfor"))break;let N=this.parseStatement();if(N)L.push(N)}}return this.expectBlockTag("endfor"),{type:"For",target:E,iter:P,body:M,else_:L,recursive:R,line:A.line,column:A.column}}parseBlockTag(A){let E=this.expect("NAME").value,O=this.check("NAME")&&this.peek().value==="scoped";if(O)this.advance();this.expect("BLOCK_END");let I=[];while(!this.isAtEnd()){if(this.checkBlockTag("endblock"))break;let P=this.parseStatement();if(P)I.push(P)}if(this.advance(),this.advance(),this.check("NAME"))this.advance();return this.expect("BLOCK_END"),{type:"Block",name:E,body:I,scoped:O,line:A.line,column:A.column}}parseExtends(A){let E=this.parseExpression();return this.expect("BLOCK_END"),{type:"Extends",template:E,line:A.line,column:A.column}}parseInclude(A){let E=this.parseExpression(),O=null,I=!1,P=!1;while(this.check("NAME")){let R=this.peek().value;if(R==="ignore"&&this.peekNext()?.value==="missing")this.advance(),this.advance(),P=!0;else if(R==="with")this.advance(),O=this.parseKeywordArgs();else if(R==="only")this.advance(),I=!0;else if(R==="without"){if(this.advance(),this.check("NAME")&&this.peek().value==="context")this.advance(),I=!0}else break}return this.expect("BLOCK_END"),{type:"Include",template:E,context:O,only:I,ignoreMissing:P,line:A.line,column:A.column}}parseSet(A){let E=this.expect("NAME").value;this.expect("ASSIGN");let O=this.parseExpression();return this.expect("BLOCK_END"),{type:"Set",target:E,value:O,line:A.line,column:A.column}}parseWith(A){let E=[];do{let I=this.expect("NAME").value;this.expect("ASSIGN");let P=this.parseExpression();E.push({target:I,value:P})}while(this.match("COMMA")||this.check("NAME")&&this.peekNext()?.type==="ASSIGN");this.expect("BLOCK_END");let O=[];while(!this.isAtEnd()){if(this.checkBlockTag("endwith"))break;let I=this.parseStatement();if(I)O.push(I)}return this.expectBlockTag("endwith"),{type:"With",assignments:E,body:O,line:A.line,column:A.column}}parseLoad(A){let E=[];while(this.check("NAME"))E.push(this.advance().value);return this.expect("BLOCK_END"),{type:"Load",names:E,line:A.line,column:A.column}}parseUrl(A){let E=this.parseExpression(),O=[],I={},P=null;while(!this.check("BLOCK_END")){if(this.check("NAME")&&this.peek().value==="as"){this.advance(),P=this.expect("NAME").value;break}if(this.check("NAME")&&this.peekNext()?.type==="ASSIGN"){let R=this.advance().value;this.advance(),I[R]=this.parseExpression()}else O.push(this.parseExpression())}return this.expect("BLOCK_END"),{type:"Url",name:E,args:O,kwargs:I,asVar:P,line:A.line,column:A.column}}parseStatic(A){let E=this.parseExpression(),O=null;if(this.check("NAME")&&this.peek().value==="as")this.advance(),O=this.expect("NAME").value;return this.expect("BLOCK_END"),{type:"Static",path:E,asVar:O,line:A.line,column:A.column}}parseNow(A){let E=this.parseExpression(),O=null;if(this.check("NAME")&&this.peek().value==="as")this.advance(),O=this.expect("NAME").value;return this.expect("BLOCK_END"),{type:"Now",format:E,asVar:O,line:A.line,column:A.column}}parseComment(A){this.expect("BLOCK_END");while(!this.isAtEnd()){if(this.checkBlockTag("endcomment"))break;this.advance()}return this.expectBlockTag("endcomment"),null}parseSimpleBlock(A,E){this.skipToBlockEnd();let O=`end${E}`;while(!this.isAtEnd()){if(this.checkBlockTag(O))break;this.advance()}if(this.checkBlockTag(O))this.advance(),this.advance(),this.expect("BLOCK_END");return null}parseCycle(A){let E=[],O=null,I=!1;while(!this.check("BLOCK_END")){if(this.check("NAME")&&this.peek().value==="as"){if(this.advance(),O=this.expect("NAME").value,this.check("NAME")&&this.peek().value==="silent")this.advance(),I=!0;break}E.push(this.parseExpression())}return this.expect("BLOCK_END"),{type:"Cycle",values:E,asVar:O,silent:I,line:A.line,column:A.column}}parseFirstof(A){let E=[],O=null,I=null;while(!this.check("BLOCK_END")){if(this.check("NAME")&&this.peek().value==="as"){this.advance(),I=this.expect("NAME").value;break}E.push(this.parseExpression())}if(E.length>0){let P=E[E.length-1];if(P.type==="Literal"&&typeof P.value==="string")O=E.pop()}return this.expect("BLOCK_END"),{type:"Firstof",values:E,fallback:O,asVar:I,line:A.line,column:A.column}}parseIfchanged(A){let E=[];while(!this.check("BLOCK_END"))E.push(this.parseExpression());this.expect("BLOCK_END");let O=[],I=[];while(!this.isAtEnd()){if(this.checkBlockTag("else")||this.checkBlockTag("endifchanged"))break;let P=this.parseStatement();if(P)O.push(P)}if(this.checkBlockTag("else")){this.advance(),this.advance(),this.expect("BLOCK_END");while(!this.isAtEnd()){if(this.checkBlockTag("endifchanged"))break;let P=this.parseStatement();if(P)I.push(P)}}return this.expectBlockTag("endifchanged"),{type:"Ifchanged",values:E,body:O,else_:I,line:A.line,column:A.column}}parseRegroup(A){let E=this.parseExpression();this.expectName("by");let O=this.expect("NAME").value;this.expectName("as");let I=this.expect("NAME").value;return this.expect("BLOCK_END"),{type:"Regroup",target:E,key:O,asVar:I,line:A.line,column:A.column}}parseWidthratio(A){let E=this.parseExpression(),O=this.parseExpression(),I=this.parseExpression(),P=null;if(this.check("NAME")&&this.peek().value==="as")this.advance(),P=this.expect("NAME").value;return this.expect("BLOCK_END"),{type:"Widthratio",value:E,maxValue:O,maxWidth:I,asVar:P,line:A.line,column:A.column}}parseLorem(A){let E=null,O="p",I=!1;if(this.check("NUMBER"))E={type:"Literal",value:parseInt(this.advance().value,10),line:A.line,column:A.column};if(this.check("NAME")){let P=this.peek().value.toLowerCase();if(P==="w"||P==="p"||P==="b")O=P,this.advance()}if(this.check("NAME")&&this.peek().value==="random")I=!0,this.advance();return this.expect("BLOCK_END"),{type:"Lorem",count:E,method:O,random:I,line:A.line,column:A.column}}parseCsrfToken(A){return this.expect("BLOCK_END"),{type:"CsrfToken",line:A.line,column:A.column}}parseDebug(A){return this.expect("BLOCK_END"),{type:"Debug",line:A.line,column:A.column}}parseTemplatetag(A){let E=this.expect("NAME").value;return this.expect("BLOCK_END"),{type:"Templatetag",tagType:E,line:A.line,column:A.column}}parseIfequal(A,E){let O=this.parseExpression(),I=this.parseExpression();this.expect("BLOCK_END");let P={type:"Compare",left:O,ops:[{operator:E?"!=":"==",right:I}],line:A.line,column:A.column},R=[],M=[],L=E?"endifnotequal":"endifequal";while(!this.isAtEnd()){if(this.checkBlockTag("else")||this.checkBlockTag(L))break;let N=this.parseStatement();if(N)R.push(N)}if(this.checkBlockTag("else")){this.advance(),this.advance(),this.expect("BLOCK_END");while(!this.isAtEnd()){if(this.checkBlockTag(L))break;let N=this.parseStatement();if(N)M.push(N)}}return this.expectBlockTag(L),{type:"If",test:P,body:R,elifs:[],else_:M,line:A.line,column:A.column}}parseExpression(){return this.parseConditional()}parseConditional(){let A=this.parseOr();if(this.check("NAME")&&this.peek().value==="if"){this.advance();let E=this.parseOr();this.expectName("else");let O=this.parseConditional();A={type:"Conditional",test:E,trueExpr:A,falseExpr:O,line:A.line,column:A.column}}return A}parseOr(){let A=this.parseAnd();while(this.check("OR")||this.check("NAME")&&this.peek().value==="or"){this.advance();let E=this.parseAnd();A={type:"BinaryOp",operator:"or",left:A,right:E,line:A.line,column:A.column}}return A}parseAnd(){let A=this.parseNot();while(this.check("AND")||this.check("NAME")&&this.peek().value==="and"){this.advance();let E=this.parseNot();A={type:"BinaryOp",operator:"and",left:A,right:E,line:A.line,column:A.column}}return A}parseNot(){if(this.check("NOT")||this.check("NAME")&&this.peek().value==="not"){let A=this.advance();return{type:"UnaryOp",operator:"not",operand:this.parseNot(),line:A.line,column:A.column}}return this.parseCompare()}parseCompare(){let A=this.parseAddSub(),E=[];while(!0){let O=null;if(this.match("EQ"))O="==";else if(this.match("NE"))O="!=";else if(this.match("LT"))O="<";else if(this.match("GT"))O=">";else if(this.match("LE"))O="<=";else if(this.match("GE"))O=">=";else if(this.check("NAME")){let P=this.peek().value;if(P==="in")this.advance(),O="in";else if(P==="not"&&this.peekNext()?.value==="in")this.advance(),this.advance(),O="not in";else if(P==="is"){this.advance();let R=this.check("NOT");if(R)this.advance();let L=this.expect("NAME").value,N=[];if(this.match("LPAREN")){while(!this.check("RPAREN"))if(N.push(this.parseExpression()),!this.check("RPAREN"))this.expect("COMMA");this.expect("RPAREN")}A={type:"TestExpr",node:A,test:L,args:N,negated:R,line:A.line,column:A.column};continue}}if(!O)break;let I=this.parseAddSub();E.push({operator:O,right:I})}if(E.length===0)return A;return{type:"Compare",left:A,ops:E,line:A.line,column:A.column}}parseAddSub(){let A=this.parseMulDiv();while(this.check("ADD")||this.check("SUB")||this.check("TILDE")){let E=this.advance(),O=this.parseMulDiv();A={type:"BinaryOp",operator:E.value,left:A,right:O,line:A.line,column:A.column}}return A}parseMulDiv(){let A=this.parseUnary();while(this.check("MUL")||this.check("DIV")||this.check("MOD")){let E=this.advance(),O=this.parseUnary();A={type:"BinaryOp",operator:E.value,left:A,right:O,line:A.line,column:A.column}}return A}parseUnary(){if(this.check("SUB")||this.check("ADD")){let A=this.advance(),E=this.parseUnary();return{type:"UnaryOp",operator:A.value,operand:E,line:A.line,column:A.column}}return this.parseFilter()}parseFilter(){let A=this.parsePostfix();while(this.match("PIPE")){let E=this.expect("NAME").value,O=[],I={};if(this.match("COLON"))if(this.check("SUB")||this.check("ADD")){let P=this.advance(),R=this.parsePostfix();O.push({type:"UnaryOp",operator:P.value,operand:R,line:P.line,column:P.column})}else O.push(this.parsePostfix());else if(this.match("LPAREN")){while(!this.check("RPAREN")){if(this.check("NAME")&&this.peekNext()?.type==="ASSIGN"){let P=this.advance().value;this.advance(),I[P]=this.parseExpression()}else O.push(this.parseExpression());if(!this.check("RPAREN"))this.expect("COMMA")}this.expect("RPAREN")}A={type:"FilterExpr",node:A,filter:E,args:O,kwargs:I,line:A.line,column:A.column}}return A}parsePostfix(){let A=this.parsePrimary();while(!0)if(this.match("DOT")){let E;if(this.check("NUMBER"))E=this.advance().value;else E=this.expect("NAME").value;A={type:"GetAttr",object:A,attribute:E,line:A.line,column:A.column}}else if(this.match("LBRACKET")){let E=this.parseExpression();this.expect("RBRACKET"),A={type:"GetItem",object:A,index:E,line:A.line,column:A.column}}else if(this.match("LPAREN")){let E=[],O={};while(!this.check("RPAREN")){if(this.check("NAME")&&this.peekNext()?.type==="ASSIGN"){let I=this.advance().value;this.advance(),O[I]=this.parseExpression()}else E.push(this.parseExpression());if(!this.check("RPAREN"))this.expect("COMMA")}this.expect("RPAREN"),A={type:"FunctionCall",callee:A,args:E,kwargs:O,line:A.line,column:A.column}}else break;return A}parsePrimary(){let A=this.peek();if(this.match("STRING"))return{type:"Literal",value:A.value,line:A.line,column:A.column};if(this.match("NUMBER"))return{type:"Literal",value:A.value.includes(".")?parseFloat(A.value):parseInt(A.value,10),line:A.line,column:A.column};if(this.check("NAME")){let E=this.advance().value;if(E==="true"||E==="True")return{type:"Literal",value:!0,line:A.line,column:A.column};if(E==="false"||E==="False")return{type:"Literal",value:!1,line:A.line,column:A.column};if(E==="none"||E==="None"||E==="null")return{type:"Literal",value:null,line:A.line,column:A.column};return{type:"Name",name:E,line:A.line,column:A.column}}if(this.match("LPAREN")){let E=this.parseExpression();return this.expect("RPAREN"),E}if(this.match("LBRACKET")){let E=[];while(!this.check("RBRACKET"))if(E.push(this.parseExpression()),!this.check("RBRACKET"))this.expect("COMMA");return this.expect("RBRACKET"),{type:"Array",elements:E,line:A.line,column:A.column}}if(this.match("LBRACE")){let E=[];while(!this.check("RBRACE")){let O=this.parseExpression();this.expect("COLON");let I=this.parseExpression();if(E.push({key:O,value:I}),!this.check("RBRACE"))this.expect("COMMA")}return this.expect("RBRACE"),{type:"Object",pairs:E,line:A.line,column:A.column}}throw this.error(`Unexpected token: ${A.type} (${A.value})`)}parseKeywordArgs(){let A={};while(this.check("NAME")&&this.peekNext()?.type==="ASSIGN"){let E=this.advance().value;this.advance(),A[E]=this.parseExpression()}return A}checkBlockTag(A){if(this.peek().type!=="BLOCK_START")return!1;let E=this.current+1;if(E>=this.tokens.length)return!1;let O=this.tokens[E];return O.type==="NAME"&&O.value===A}expectBlockTag(A){this.advance();let E=this.expect("NAME");if(E.value!==A)throw this.error(`Expected '${A}', got '${E.value}'`);this.expect("BLOCK_END")}expectName(A){let E=this.expect("NAME");if(E.value!==A)throw this.error(`Expected '${A}', got '${E.value}'`)}skipToBlockEnd(){while(!this.isAtEnd()&&!this.check("BLOCK_END"))this.advance();if(this.check("BLOCK_END"))this.advance()}isAtEnd(){return this.peek().type==="EOF"}peek(){return this.tokens[this.current]}peekNext(){if(this.current+1>=this.tokens.length)return null;return this.tokens[this.current+1]}advance(){if(!this.isAtEnd())this.current++;return this.tokens[this.current-1]}check(A){if(this.isAtEnd())return!1;return this.peek().type===A}match(A){if(this.check(A))return this.advance(),!0;return!1}expect(A){if(this.check(A))return this.advance();let E=this.peek();throw this.error(`Expected ${A}, got ${E.type} (${E.value})`)}error(A){let E=this.peek();return new _(A,{line:E.line,column:E.column,source:this.source})}}function G(A,E){let O=E||process.env.ANTHROPIC_API_KEY;return{name:"anthropic",async available(){return!!O},async analyze(I,P){let N=(await new(await import("@anthropic-ai/sdk")).default({apiKey:O}).messages.create({model:A||"claude-sonnet-4-20250514",max_tokens:1500,messages:[{role:"user",content:P.replace("{{TEMPLATE}}",I)}]})).content[0];if(N.type==="text")return N.text;throw Error("Unexpected response type from Anthropic")}}}function U(A,E){let O=E||process.env.OPENAI_API_KEY;return{name:"openai",async available(){return!!O},async analyze(I,P){return(await new(await import("openai")).default({apiKey:O}).chat.completions.create({model:A||"gpt-4o-mini",max_tokens:1500,messages:[{role:"user",content:P.replace("{{TEMPLATE}}",I)}]})).choices[0]?.message?.content||""}}}function S(A,E){let O=E||"http://localhost:11434";return{name:"ollama",async available(){try{return(await fetch(`${O}/api/tags`,{signal:AbortSignal.timeout(2000)})).ok}catch{return!1}},async analyze(I,P){let R=await fetch(`${O}/api/generate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:A||"llama3.1",prompt:P.replace("{{TEMPLATE}}",I),stream:!1})});if(!R.ok)throw Error(`Ollama error: ${R.statusText}`);return(await R.json()).response}}}function Y(A,E){let O=E||process.env.GROQ_API_KEY;return{name:"groq",async available(){return!!O},async analyze(I,P){if(!O)throw Error("Groq API key not provided");let R=await fetch("https://api.groq.com/openai/v1/chat/completions",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${O}`},body:JSON.stringify({model:A||"llama-3.1-70b-versatile",max_tokens:1500,messages:[{role:"user",content:P.replace("{{TEMPLATE}}",I)}]})});if(!R.ok){let L=await R.text();throw Error(`Groq error: ${L}`)}return(await R.json()).choices[0]?.message?.content||""}}}function z(A,E={}){switch(A){case"anthropic":return G(E.model,E.apiKey);case"openai":return U(E.model,E.apiKey);case"ollama":return S(E.model,E.ollamaUrl);case"groq":return Y(E.model,E.apiKey);default:throw Error(`Unknown provider: ${A}`)}}async function Z(A={}){let E=[{name:"anthropic",create:()=>G(A.model,A.apiKey)},{name:"openai",create:()=>U(A.model,A.apiKey)},{name:"groq",create:()=>Y(A.model,A.apiKey)},{name:"ollama",create:()=>S(A.model,A.ollamaUrl)}];for(let{name:O,create:I}of E){let P=I();if(await P.available())return P}throw Error(`No AI provider available.
|
|
12
|
+
|
|
13
|
+
Configure one of the following:
|
|
14
|
+
- ANTHROPIC_API_KEY (Claude)
|
|
15
|
+
- OPENAI_API_KEY (GPT-4)
|
|
16
|
+
- GROQ_API_KEY (Llama, free tier)
|
|
17
|
+
- Ollama running locally (http://localhost:11434)
|
|
18
|
+
|
|
19
|
+
Install SDK if needed:
|
|
20
|
+
bun add @anthropic-ai/sdk # for Claude
|
|
21
|
+
bun add openai # for OpenAI`)}async function w(A={}){let E=A.provider||"auto";if(E==="auto")return Z(A);let O=z(E,A);if(!await O.available())throw Error(`Provider '${E}' is not available. Check your API key or configuration.`);return O}function q(A){if(!A||A.length===0)return`Analyze this Jinja2/Django template for issues.
|
|
22
|
+
|
|
23
|
+
TEMPLATE:
|
|
24
|
+
\`\`\`jinja
|
|
25
|
+
{{TEMPLATE}}
|
|
26
|
+
\`\`\`
|
|
27
|
+
|
|
28
|
+
Check for:
|
|
29
|
+
|
|
30
|
+
1. SECURITY
|
|
31
|
+
- XSS vulnerabilities (unescaped user input, |safe on untrusted data)
|
|
32
|
+
- Variables in onclick/onerror/javascript: without escapejs
|
|
33
|
+
- Sensitive data exposure (passwords, tokens, API keys)
|
|
34
|
+
- SQL/command injection patterns
|
|
35
|
+
|
|
36
|
+
2. PERFORMANCE
|
|
37
|
+
- Heavy filters inside loops (date, filesizeformat)
|
|
38
|
+
- Repeated filter calls on same value (use {% with %})
|
|
39
|
+
- N+1 query patterns (accessing relations in loops)
|
|
40
|
+
|
|
41
|
+
3. ACCESSIBILITY
|
|
42
|
+
- Images without alt text
|
|
43
|
+
- Forms without labels
|
|
44
|
+
- Missing ARIA attributes on interactive elements
|
|
45
|
+
- Poor heading hierarchy
|
|
46
|
+
|
|
47
|
+
4. BEST PRACTICES
|
|
48
|
+
- {% for %} without {% empty %}
|
|
49
|
+
- Deeply nested conditionals (>3 levels)
|
|
50
|
+
- Magic numbers/strings (should be variables)
|
|
51
|
+
- Deprecated filter usage
|
|
52
|
+
|
|
53
|
+
Respond ONLY with valid JSON (no markdown, no explanation):
|
|
54
|
+
{
|
|
55
|
+
"issues": [
|
|
56
|
+
{
|
|
57
|
+
"line": 1,
|
|
58
|
+
"type": "security|performance|accessibility|best-practice",
|
|
59
|
+
"severity": "error|warning|suggestion",
|
|
60
|
+
"message": "Brief description",
|
|
61
|
+
"suggestion": "How to fix"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
If no issues found, return: {"issues": []}`;let E={syntax:"SYNTAX",security:"SECURITY",performance:"PERFORMANCE",accessibility:"ACCESSIBILITY","best-practice":"BEST PRACTICES",deprecated:"DEPRECATED"},O=A.map((I)=>E[I]).filter(Boolean);return`Analyze this Jinja2/Django template for issues.
|
|
67
|
+
|
|
68
|
+
TEMPLATE:
|
|
69
|
+
\`\`\`jinja
|
|
70
|
+
{{TEMPLATE}}
|
|
71
|
+
\`\`\`
|
|
72
|
+
|
|
73
|
+
Check for:
|
|
74
|
+
|
|
75
|
+
1. SECURITY
|
|
76
|
+
- XSS vulnerabilities (unescaped user input, |safe on untrusted data)
|
|
77
|
+
- Variables in onclick/onerror/javascript: without escapejs
|
|
78
|
+
- Sensitive data exposure (passwords, tokens, API keys)
|
|
79
|
+
- SQL/command injection patterns
|
|
80
|
+
|
|
81
|
+
2. PERFORMANCE
|
|
82
|
+
- Heavy filters inside loops (date, filesizeformat)
|
|
83
|
+
- Repeated filter calls on same value (use {% with %})
|
|
84
|
+
- N+1 query patterns (accessing relations in loops)
|
|
85
|
+
|
|
86
|
+
3. ACCESSIBILITY
|
|
87
|
+
- Images without alt text
|
|
88
|
+
- Forms without labels
|
|
89
|
+
- Missing ARIA attributes on interactive elements
|
|
90
|
+
- Poor heading hierarchy
|
|
91
|
+
|
|
92
|
+
4. BEST PRACTICES
|
|
93
|
+
- {% for %} without {% empty %}
|
|
94
|
+
- Deeply nested conditionals (>3 levels)
|
|
95
|
+
- Magic numbers/strings (should be variables)
|
|
96
|
+
- Deprecated filter usage
|
|
97
|
+
|
|
98
|
+
Respond ONLY with valid JSON (no markdown, no explanation):
|
|
99
|
+
{
|
|
100
|
+
"issues": [
|
|
101
|
+
{
|
|
102
|
+
"line": 1,
|
|
103
|
+
"type": "security|performance|accessibility|best-practice",
|
|
104
|
+
"severity": "error|warning|suggestion",
|
|
105
|
+
"message": "Brief description",
|
|
106
|
+
"suggestion": "How to fix"
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
If no issues found, return: {"issues": []}`.replace(/Check for:[\s\S]*?Respond ONLY/,`Check ONLY for: ${O.join(", ")}
|
|
112
|
+
|
|
113
|
+
Respond ONLY`)}async function c(A,E={}){let O=Date.now(),I={valid:!0,errors:[],warnings:[],suggestions:[]};try{let R=new D(A).tokenize();new Q(R,A).parse()}catch(P){return I.valid=!1,I.errors.push({line:P.line||1,type:"syntax",severity:"error",message:P.message}),I.duration=Date.now()-O,I}try{let P=await w(E);I.provider=P.name;let R=q(E.categories),M=await P.analyze(A,R),L=m(M);for(let N of L){if(E.maxIssues&&I.errors.length+I.warnings.length+I.suggestions.length>=E.maxIssues)break;switch(N.severity){case"error":I.errors.push(N);break;case"warning":I.warnings.push(N);break;case"suggestion":I.suggestions.push(N);break}}I.valid=I.errors.length===0}catch(P){I.warnings.push({line:0,type:"best-practice",severity:"warning",message:`AI analysis failed: ${P.message}`})}return I.duration=Date.now()-O,I}function m(A){try{let E=A.trim(),O=E.match(/```(?:json)?\s*([\s\S]*?)```/);if(O)E=O[1].trim();let I=E.match(/\{[\s\S]*\}/);if(I)E=I[0];let P=JSON.parse(E);if(!P.issues||!Array.isArray(P.issues))return[];return P.issues.filter((R)=>R&&typeof R==="object").map((R)=>({line:typeof R.line==="number"?R.line:1,type:l(R.type),severity:d(R.severity),message:String(R.message||"Unknown issue"),suggestion:R.suggestion?String(R.suggestion):void 0}))}catch{return[]}}function l(A){return{security:"security",performance:"performance",accessibility:"accessibility",a11y:"accessibility","best-practice":"best-practice","best-practices":"best-practice",bestpractice:"best-practice",deprecated:"deprecated",syntax:"syntax"}[String(A).toLowerCase()]||"best-practice"}function d(A){return{error:"error",warning:"warning",warn:"warning",suggestion:"suggestion",info:"suggestion",hint:"suggestion"}[String(A).toLowerCase()]||"warning"}function i(A){let E={valid:!0,errors:[],warnings:[],suggestions:[]};try{let I=new D(A).tokenize();new Q(I,A).parse()}catch(O){E.valid=!1,E.errors.push({line:O.line||1,type:"syntax",severity:"error",message:O.message})}return E}export{i as syntaxCheck,w as resolveProvider,c as lint,z as getProvider,Z as detectProvider,U as createOpenAIProvider,S as createOllamaProvider,Y as createGroqProvider,G as createAnthropicProvider};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI-powered Template Linting
|
|
3
|
+
*/
|
|
4
|
+
import type { LintResult, LintOptions } from './types';
|
|
5
|
+
/**
|
|
6
|
+
* Lint a template using AI analysis
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { lint } from 'binja/ai'
|
|
11
|
+
*
|
|
12
|
+
* const result = await lint(`
|
|
13
|
+
* {% for p in products %}
|
|
14
|
+
* <div onclick="buy({{ p.id }})">{{ p.name }}</div>
|
|
15
|
+
* {% endfor %}
|
|
16
|
+
* `)
|
|
17
|
+
*
|
|
18
|
+
* // result.warnings = [{ type: 'security', message: 'XSS: unescaped in onclick' }]
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function lint(template: string, options?: LintOptions): Promise<LintResult>;
|
|
22
|
+
/**
|
|
23
|
+
* Quick syntax check without AI (fast, sync)
|
|
24
|
+
*/
|
|
25
|
+
export declare function syntaxCheck(template: string): LintResult;
|
|
26
|
+
//# sourceMappingURL=lint.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Lint Prompt Engineering
|
|
3
|
+
*/
|
|
4
|
+
import type { IssueType } from './types';
|
|
5
|
+
export declare const LINT_PROMPT = "Analyze this Jinja2/Django template for issues.\n\nTEMPLATE:\n```jinja\n{{TEMPLATE}}\n```\n\nCheck for:\n\n1. SECURITY\n - XSS vulnerabilities (unescaped user input, |safe on untrusted data)\n - Variables in onclick/onerror/javascript: without escapejs\n - Sensitive data exposure (passwords, tokens, API keys)\n - SQL/command injection patterns\n\n2. PERFORMANCE\n - Heavy filters inside loops (date, filesizeformat)\n - Repeated filter calls on same value (use {% with %})\n - N+1 query patterns (accessing relations in loops)\n\n3. ACCESSIBILITY\n - Images without alt text\n - Forms without labels\n - Missing ARIA attributes on interactive elements\n - Poor heading hierarchy\n\n4. BEST PRACTICES\n - {% for %} without {% empty %}\n - Deeply nested conditionals (>3 levels)\n - Magic numbers/strings (should be variables)\n - Deprecated filter usage\n\nRespond ONLY with valid JSON (no markdown, no explanation):\n{\n \"issues\": [\n {\n \"line\": 1,\n \"type\": \"security|performance|accessibility|best-practice\",\n \"severity\": \"error|warning|suggestion\",\n \"message\": \"Brief description\",\n \"suggestion\": \"How to fix\"\n }\n ]\n}\n\nIf no issues found, return: {\"issues\": []}";
|
|
6
|
+
/**
|
|
7
|
+
* Build prompt with optional category filter
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildPrompt(categories?: IssueType[]): string;
|
|
10
|
+
//# sourceMappingURL=prompt.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Provider Manager - Auto-detect and create providers
|
|
3
|
+
*/
|
|
4
|
+
import type { AIProvider, LintOptions } from '../types';
|
|
5
|
+
/**
|
|
6
|
+
* Get a specific provider by name
|
|
7
|
+
*/
|
|
8
|
+
export declare function getProvider(name: 'anthropic' | 'openai' | 'ollama' | 'groq', options?: LintOptions): AIProvider;
|
|
9
|
+
/**
|
|
10
|
+
* Auto-detect available provider
|
|
11
|
+
* Priority: Anthropic > OpenAI > Groq > Ollama
|
|
12
|
+
*/
|
|
13
|
+
export declare function detectProvider(options?: LintOptions): Promise<AIProvider>;
|
|
14
|
+
/**
|
|
15
|
+
* Get provider based on options (auto or specific)
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveProvider(options?: LintOptions): Promise<AIProvider>;
|
|
18
|
+
export { createAnthropicProvider } from './anthropic';
|
|
19
|
+
export { createOpenAIProvider } from './openai';
|
|
20
|
+
export { createOllamaProvider } from './ollama';
|
|
21
|
+
export { createGroqProvider } from './groq';
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Linting Types
|
|
3
|
+
*/
|
|
4
|
+
export type IssueSeverity = 'error' | 'warning' | 'suggestion';
|
|
5
|
+
export type IssueType = 'syntax' | 'security' | 'performance' | 'accessibility' | 'best-practice' | 'deprecated';
|
|
6
|
+
export interface Issue {
|
|
7
|
+
line: number;
|
|
8
|
+
column?: number;
|
|
9
|
+
type: IssueType;
|
|
10
|
+
severity: IssueSeverity;
|
|
11
|
+
message: string;
|
|
12
|
+
suggestion?: string;
|
|
13
|
+
code?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface LintResult {
|
|
16
|
+
valid: boolean;
|
|
17
|
+
errors: Issue[];
|
|
18
|
+
warnings: Issue[];
|
|
19
|
+
suggestions: Issue[];
|
|
20
|
+
provider?: string;
|
|
21
|
+
duration?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface LintOptions {
|
|
24
|
+
/** AI provider to use: 'auto', 'anthropic', 'openai', 'ollama', 'groq' */
|
|
25
|
+
provider?: 'auto' | 'anthropic' | 'openai' | 'ollama' | 'groq';
|
|
26
|
+
/** Model to use (provider-specific) */
|
|
27
|
+
model?: string;
|
|
28
|
+
/** API key (alternative to environment variable) */
|
|
29
|
+
apiKey?: string;
|
|
30
|
+
/** Ollama server URL (default: http://localhost:11434) */
|
|
31
|
+
ollamaUrl?: string;
|
|
32
|
+
/** Categories to check */
|
|
33
|
+
categories?: IssueType[];
|
|
34
|
+
/** Maximum issues to return */
|
|
35
|
+
maxIssues?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface AIProvider {
|
|
38
|
+
name: string;
|
|
39
|
+
available: () => Promise<boolean>;
|
|
40
|
+
analyze: (template: string, prompt: string) => Promise<string>;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=types.d.ts.map
|