0
# Context and Scoping
1
2
The Context system in LiquidJS manages variable scopes, registers, and the template execution environment. It provides a hierarchical variable resolution system with support for nested scopes, global variables, and custom Drop objects.
3
4
## Capabilities
5
6
### Context Class
7
8
The main class responsible for managing template execution context and variable resolution.
9
10
```typescript { .api }
11
/**
12
* Template execution context managing variables and scopes
13
*/
14
class Context {
15
/** Create new context with environment and options */
16
constructor(env?: object, opts?: NormalizedFullOptions, renderOptions?: RenderOptions);
17
18
/** Normalized Liquid options */
19
readonly opts: NormalizedFullOptions;
20
/** User-provided scope environment */
21
readonly environments: Scope;
22
/** Global scope used as fallback */
23
readonly globals: Scope;
24
/** Sync/async execution mode */
25
readonly sync: boolean;
26
/** Strict variable validation */
27
readonly strictVariables: boolean;
28
/** Only check own properties */
29
readonly ownPropertyOnly: boolean;
30
/** Memory usage limiter */
31
readonly memoryLimit: Limiter;
32
/** Render time limiter */
33
readonly renderLimit: Limiter;
34
}
35
36
interface Scope extends Record<string, any> {
37
/** Convert object to liquid-compatible representation */
38
toLiquid?(): any;
39
}
40
```
41
42
**Usage Examples:**
43
44
```typescript
45
import { Context, Liquid } from "liquidjs";
46
47
// Basic context creation
48
const data = { user: { name: 'Alice', age: 30 } };
49
const ctx = new Context(data);
50
51
// Context with options
52
const engine = new Liquid({ strictVariables: true });
53
const ctxWithOptions = new Context(data, engine.options);
54
55
// Context with render options
56
const ctxWithRenderOpts = new Context(data, engine.options, {
57
globals: { siteName: 'My Site' },
58
strictVariables: false
59
});
60
```
61
62
### Variable Resolution
63
64
Hierarchical variable lookup with multiple scope levels.
65
66
```typescript { .api }
67
/**
68
* Get variable value synchronously
69
* @param paths - Array of property keys to traverse
70
* @returns Variable value or undefined
71
*/
72
getSync(paths: PropertyKey[]): unknown;
73
74
/**
75
* Get all variables from all scopes merged
76
* @returns Combined object with all variables
77
*/
78
getAll(): object;
79
80
/**
81
* Find which scope contains a variable
82
* Resolution order: scopes (newest first) -> environments -> globals
83
*/
84
private findScope(key: string | number): Scope;
85
```
86
87
**Usage Examples:**
88
89
```typescript
90
import { Context } from "liquidjs";
91
92
const ctx = new Context({
93
user: { name: 'Alice', profile: { bio: 'Developer' } },
94
products: [{ name: 'Laptop' }, { name: 'Mouse' }]
95
});
96
97
// Simple property access
98
const userName = ctx.getSync(['user', 'name']);
99
console.log(userName); // "Alice"
100
101
// Nested property access
102
const bio = ctx.getSync(['user', 'profile', 'bio']);
103
console.log(bio); // "Developer"
104
105
// Array access
106
const firstProduct = ctx.getSync(['products', 0, 'name']);
107
console.log(firstProduct); // "Laptop"
108
109
// Get all variables
110
const allVars = ctx.getAll();
111
console.log(allVars); // { user: {...}, products: [...], ...globals }
112
```
113
114
### Scope Management
115
116
Push and pop scopes to create nested variable environments.
117
118
```typescript { .api }
119
/**
120
* Push new scope onto scope stack
121
* Variables in new scope shadow outer scopes
122
* @param ctx - Object to add as new scope
123
* @returns New scope stack length
124
*/
125
push(ctx: object): number;
126
127
/**
128
* Pop current scope from stack
129
* @returns Removed scope object
130
*/
131
pop(): object;
132
133
/**
134
* Get bottom (first) scope for variable assignment
135
* @returns Bottom scope object
136
*/
137
bottom(): object;
138
139
/**
140
* Create child context with new environment
141
* Inherits options and limits from parent
142
* @param scope - New environment scope
143
* @returns New Context instance
144
*/
145
spawn(scope?: object): Context;
146
```
147
148
**Usage Examples:**
149
150
```typescript
151
import { Context } from "liquidjs";
152
153
const ctx = new Context({ global_var: 'global' });
154
155
// Push new scope
156
ctx.push({ local_var: 'local', global_var: 'shadowed' });
157
158
console.log(ctx.getSync(['global_var'])); // "shadowed" (from new scope)
159
console.log(ctx.getSync(['local_var'])); // "local"
160
161
// Pop scope
162
ctx.pop();
163
164
console.log(ctx.getSync(['global_var'])); // "global" (original value)
165
console.log(ctx.getSync(['local_var'])); // undefined (scope removed)
166
167
// Bottom scope for assignments
168
ctx.bottom()['new_var'] = 'assigned';
169
console.log(ctx.getSync(['new_var'])); // "assigned"
170
171
// Child context
172
const childCtx = ctx.spawn({ child_var: 'child' });
173
console.log(childCtx.getSync(['global_var'])); // "global" (inherited)
174
console.log(childCtx.getSync(['child_var'])); // "child"
175
```
176
177
### Register System
178
179
Store and retrieve arbitrary data in the context that persists across template rendering.
180
181
```typescript { .api }
182
/**
183
* Get register by key (creates empty object if not exists)
184
* @param key - Register key
185
* @returns Register object
186
*/
187
getRegister(key: string): any;
188
189
/**
190
* Set register value
191
* @param key - Register key
192
* @param value - Value to store
193
* @returns Stored value
194
*/
195
setRegister(key: string, value: any): any;
196
197
/**
198
* Save current state of multiple registers
199
* @param keys - Register keys to save
200
* @returns Array of key-value pairs
201
*/
202
saveRegister(...keys: string[]): [string, any][];
203
204
/**
205
* Restore register state from saved values
206
* @param keyValues - Array of key-value pairs to restore
207
*/
208
restoreRegister(keyValues: [string, any][]): void;
209
```
210
211
**Usage Examples:**
212
213
```typescript
214
import { Context } from "liquidjs";
215
216
const ctx = new Context();
217
218
// Set register data
219
ctx.setRegister('counters', { page: 1, section: 0 });
220
ctx.setRegister('cache', new Map());
221
222
// Get register (creates if not exists)
223
const counters = ctx.getRegister('counters');
224
counters.page += 1;
225
226
// Save and restore register state
227
const saved = ctx.saveRegister('counters', 'cache');
228
// ... modify registers ...
229
ctx.restoreRegister(saved); // Restore previous state
230
```
231
232
### Scope Resolution Order
233
234
Variable resolution follows a specific hierarchy:
235
236
1. **Local Scopes** (newest to oldest) - Variables from `push()`
237
2. **Environment Scope** - User-provided data
238
3. **Global Scope** - Global variables from options
239
240
```typescript
241
// Resolution order example
242
const ctx = new Context(
243
{ env_var: 'environment' }, // Environment scope
244
engine.options,
245
{ globals: { global_var: 'global' } } // Global scope
246
);
247
248
ctx.push({ local_var: 'local', env_var: 'overridden' });
249
250
// Resolution:
251
ctx.getSync(['local_var']); // 'local' (from local scope)
252
ctx.getSync(['env_var']); // 'overridden' (local shadows environment)
253
ctx.getSync(['global_var']); // 'global' (from global scope)
254
```
255
256
### Property Access Features
257
258
The Context system provides special property access features:
259
260
```typescript { .api }
261
/**
262
* Read property from object with special handling
263
* @param obj - Source object
264
* @param key - Property key
265
* @param ownPropertyOnly - Only check own properties
266
* @returns Property value
267
*/
268
function readProperty(obj: Scope, key: PropertyKey, ownPropertyOnly: boolean): any;
269
```
270
271
**Special Properties:**
272
273
- **`size`**: Returns length for arrays/strings, key count for objects
274
- **`first`**: Returns first element of array or `obj.first`
275
- **`last`**: Returns last element of array or `obj.last`
276
- **Negative array indices**: `arr[-1]` gets last element
277
- **Function calls**: Functions are automatically called with `obj` as `this`
278
- **Drop method missing**: Calls `liquidMethodMissing` for undefined properties
279
280
**Usage Examples:**
281
282
```typescript
283
const ctx = new Context({
284
items: ['a', 'b', 'c'],
285
user: { name: 'Alice', getName() { return this.name.toUpperCase(); } },
286
data: { key1: 'value1', key2: 'value2' }
287
});
288
289
// Special size property
290
console.log(ctx.getSync(['items', 'size'])); // 3
291
console.log(ctx.getSync(['data', 'size'])); // 2
292
293
// First and last
294
console.log(ctx.getSync(['items', 'first'])); // 'a'
295
console.log(ctx.getSync(['items', 'last'])); // 'c'
296
297
// Negative indices
298
console.log(ctx.getSync(['items', -1])); // 'c' (last element)
299
console.log(ctx.getSync(['items', -2])); // 'b' (second to last)
300
301
// Function calls
302
console.log(ctx.getSync(['user', 'getName'])); // 'ALICE'
303
```
304
305
### Drop Objects
306
307
Custom objects that implement special liquid behavior.
308
309
```typescript { .api }
310
/**
311
* Base class for liquid drop objects
312
*/
313
abstract class Drop {
314
/**
315
* Handle access to undefined properties
316
* @param key - Property key that was accessed
317
* @returns Value for the property or Promise<value>
318
*/
319
liquidMethodMissing(key: string | number): Promise<any> | any;
320
}
321
322
/**
323
* Scope type - either regular object or Drop
324
*/
325
type Scope = ScopeObject | Drop;
326
327
interface ScopeObject extends Record<string, any> {
328
/** Convert object to liquid representation */
329
toLiquid?(): any;
330
}
331
```
332
333
**Usage Examples:**
334
335
```typescript
336
import { Drop, Context } from "liquidjs";
337
338
// Custom Drop implementation
339
class UserDrop extends Drop {
340
constructor(private userData: any) {
341
super();
342
}
343
344
get name() {
345
return this.userData.name;
346
}
347
348
liquidMethodMissing(key: string) {
349
// Handle dynamic properties
350
if (key.startsWith('is_')) {
351
const role = key.slice(3);
352
return this.userData.roles?.includes(role) || false;
353
}
354
return undefined;
355
}
356
}
357
358
// Use Drop in context
359
const userDrop = new UserDrop({
360
name: 'Alice',
361
roles: ['admin', 'editor']
362
});
363
364
const ctx = new Context({ user: userDrop });
365
366
console.log(ctx.getSync(['user', 'name'])); // 'Alice'
367
console.log(ctx.getSync(['user', 'is_admin'])); // true
368
console.log(ctx.getSync(['user', 'is_guest'])); // false
369
```
370
371
### Strict Variables
372
373
Control how undefined variables are handled.
374
375
```typescript { .api }
376
interface StrictVariableOptions {
377
/** Throw error when accessing undefined variables */
378
strictVariables?: boolean;
379
/** Only allow access to own properties (not inherited) */
380
ownPropertyOnly?: boolean;
381
}
382
```
383
384
**Usage Examples:**
385
386
```typescript
387
import { Context, Liquid } from "liquidjs";
388
389
// Strict mode - throws on undefined
390
const strictEngine = new Liquid({ strictVariables: true });
391
const strictCtx = new Context({ user: 'Alice' }, strictEngine.options);
392
393
try {
394
strictCtx.getSync(['missing_var']); // Throws UndefinedVariableError
395
} catch (error) {
396
console.log('Variable not found!');
397
}
398
399
// Lenient mode - returns undefined
400
const lenientEngine = new Liquid({ strictVariables: false });
401
const lenientCtx = new Context({ user: 'Alice' }, lenientEngine.options);
402
403
console.log(lenientCtx.getSync(['missing_var'])); // undefined (no error)
404
405
// Own property only
406
const ownPropCtx = new Context(
407
Object.create({ inherited: 'value' }),
408
{ ...lenientEngine.options, ownPropertyOnly: true }
409
);
410
411
console.log(ownPropCtx.getSync(['inherited'])); // undefined (ignored)
412
```
413
414
### Performance and Limits
415
416
Context includes built-in protection against DoS attacks.
417
418
```typescript { .api }
419
interface ContextLimits {
420
/** Memory usage limiter */
421
memoryLimit: Limiter;
422
/** Render time limiter */
423
renderLimit: Limiter;
424
}
425
426
class Limiter {
427
constructor(name: string, limit: number);
428
/** Track resource usage */
429
use(amount: number): void;
430
}
431
```
432
433
**Usage Examples:**
434
435
```typescript
436
import { Context, Liquid } from "liquidjs";
437
438
// Configure limits
439
const engine = new Liquid({
440
memoryLimit: 1024 * 1024, // 1MB
441
renderLimit: 5000 // 5 seconds
442
});
443
444
const ctx = new Context(data, engine.options);
445
446
// Limits are enforced automatically during rendering
447
// Memory usage tracked for string operations, array operations, etc.
448
// Render time tracked during template execution
449
```
450
451
### Context in Template Execution
452
453
Context is used throughout template rendering:
454
455
```liquid
456
<!-- Variable access uses context resolution -->
457
{{ user.name }} <!-- ctx.getSync(['user', 'name']) -->
458
{{ items.size }} <!-- ctx.getSync(['items', 'size']) -->
459
{{ products.first.name }} <!-- ctx.getSync(['products', 'first', 'name']) -->
460
461
<!-- Tags create new scopes -->
462
{% for item in items %}
463
{{ item }} <!-- 'item' pushed to local scope -->
464
{% endfor %}
465
466
{% assign temp = 'value' %} <!-- Added to bottom scope -->
467
{{ temp }} <!-- Available after assignment -->
468
469
<!-- Captures create variables -->
470
{% capture content %}
471
<p>{{ user.name }}</p>
472
{% endcapture %}
473
{{ content }} <!-- Available in context -->
474
```