or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

analysis.mdbuilt-in-tags.mdconfiguration.mdcontext-and-scoping.mdcore-engine.mdextensions.mdfilesystem.mdfilters.mdindex.md
tile.json

context-and-scoping.mddocs/

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

```