A simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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.
The main class responsible for managing template execution context and variable resolution.
/**
* Template execution context managing variables and scopes
*/
class Context {
/** Create new context with environment and options */
constructor(env?: object, opts?: NormalizedFullOptions, renderOptions?: RenderOptions);
/** Normalized Liquid options */
readonly opts: NormalizedFullOptions;
/** User-provided scope environment */
readonly environments: Scope;
/** Global scope used as fallback */
readonly globals: Scope;
/** Sync/async execution mode */
readonly sync: boolean;
/** Strict variable validation */
readonly strictVariables: boolean;
/** Only check own properties */
readonly ownPropertyOnly: boolean;
/** Memory usage limiter */
readonly memoryLimit: Limiter;
/** Render time limiter */
readonly renderLimit: Limiter;
}
interface Scope extends Record<string, any> {
/** Convert object to liquid-compatible representation */
toLiquid?(): any;
}Usage Examples:
import { Context, Liquid } from "liquidjs";
// Basic context creation
const data = { user: { name: 'Alice', age: 30 } };
const ctx = new Context(data);
// Context with options
const engine = new Liquid({ strictVariables: true });
const ctxWithOptions = new Context(data, engine.options);
// Context with render options
const ctxWithRenderOpts = new Context(data, engine.options, {
globals: { siteName: 'My Site' },
strictVariables: false
});Hierarchical variable lookup with multiple scope levels.
/**
* Get variable value synchronously
* @param paths - Array of property keys to traverse
* @returns Variable value or undefined
*/
getSync(paths: PropertyKey[]): unknown;
/**
* Get all variables from all scopes merged
* @returns Combined object with all variables
*/
getAll(): object;
/**
* Find which scope contains a variable
* Resolution order: scopes (newest first) -> environments -> globals
*/
private findScope(key: string | number): Scope;Usage Examples:
import { Context } from "liquidjs";
const ctx = new Context({
user: { name: 'Alice', profile: { bio: 'Developer' } },
products: [{ name: 'Laptop' }, { name: 'Mouse' }]
});
// Simple property access
const userName = ctx.getSync(['user', 'name']);
console.log(userName); // "Alice"
// Nested property access
const bio = ctx.getSync(['user', 'profile', 'bio']);
console.log(bio); // "Developer"
// Array access
const firstProduct = ctx.getSync(['products', 0, 'name']);
console.log(firstProduct); // "Laptop"
// Get all variables
const allVars = ctx.getAll();
console.log(allVars); // { user: {...}, products: [...], ...globals }Push and pop scopes to create nested variable environments.
/**
* Push new scope onto scope stack
* Variables in new scope shadow outer scopes
* @param ctx - Object to add as new scope
* @returns New scope stack length
*/
push(ctx: object): number;
/**
* Pop current scope from stack
* @returns Removed scope object
*/
pop(): object;
/**
* Get bottom (first) scope for variable assignment
* @returns Bottom scope object
*/
bottom(): object;
/**
* Create child context with new environment
* Inherits options and limits from parent
* @param scope - New environment scope
* @returns New Context instance
*/
spawn(scope?: object): Context;Usage Examples:
import { Context } from "liquidjs";
const ctx = new Context({ global_var: 'global' });
// Push new scope
ctx.push({ local_var: 'local', global_var: 'shadowed' });
console.log(ctx.getSync(['global_var'])); // "shadowed" (from new scope)
console.log(ctx.getSync(['local_var'])); // "local"
// Pop scope
ctx.pop();
console.log(ctx.getSync(['global_var'])); // "global" (original value)
console.log(ctx.getSync(['local_var'])); // undefined (scope removed)
// Bottom scope for assignments
ctx.bottom()['new_var'] = 'assigned';
console.log(ctx.getSync(['new_var'])); // "assigned"
// Child context
const childCtx = ctx.spawn({ child_var: 'child' });
console.log(childCtx.getSync(['global_var'])); // "global" (inherited)
console.log(childCtx.getSync(['child_var'])); // "child"Store and retrieve arbitrary data in the context that persists across template rendering.
/**
* Get register by key (creates empty object if not exists)
* @param key - Register key
* @returns Register object
*/
getRegister(key: string): any;
/**
* Set register value
* @param key - Register key
* @param value - Value to store
* @returns Stored value
*/
setRegister(key: string, value: any): any;
/**
* Save current state of multiple registers
* @param keys - Register keys to save
* @returns Array of key-value pairs
*/
saveRegister(...keys: string[]): [string, any][];
/**
* Restore register state from saved values
* @param keyValues - Array of key-value pairs to restore
*/
restoreRegister(keyValues: [string, any][]): void;Usage Examples:
import { Context } from "liquidjs";
const ctx = new Context();
// Set register data
ctx.setRegister('counters', { page: 1, section: 0 });
ctx.setRegister('cache', new Map());
// Get register (creates if not exists)
const counters = ctx.getRegister('counters');
counters.page += 1;
// Save and restore register state
const saved = ctx.saveRegister('counters', 'cache');
// ... modify registers ...
ctx.restoreRegister(saved); // Restore previous stateVariable resolution follows a specific hierarchy:
push()// Resolution order example
const ctx = new Context(
{ env_var: 'environment' }, // Environment scope
engine.options,
{ globals: { global_var: 'global' } } // Global scope
);
ctx.push({ local_var: 'local', env_var: 'overridden' });
// Resolution:
ctx.getSync(['local_var']); // 'local' (from local scope)
ctx.getSync(['env_var']); // 'overridden' (local shadows environment)
ctx.getSync(['global_var']); // 'global' (from global scope)The Context system provides special property access features:
/**
* Read property from object with special handling
* @param obj - Source object
* @param key - Property key
* @param ownPropertyOnly - Only check own properties
* @returns Property value
*/
function readProperty(obj: Scope, key: PropertyKey, ownPropertyOnly: boolean): any;Special Properties:
size: Returns length for arrays/strings, key count for objectsfirst: Returns first element of array or obj.firstlast: Returns last element of array or obj.lastarr[-1] gets last elementobj as thisliquidMethodMissing for undefined propertiesUsage Examples:
const ctx = new Context({
items: ['a', 'b', 'c'],
user: { name: 'Alice', getName() { return this.name.toUpperCase(); } },
data: { key1: 'value1', key2: 'value2' }
});
// Special size property
console.log(ctx.getSync(['items', 'size'])); // 3
console.log(ctx.getSync(['data', 'size'])); // 2
// First and last
console.log(ctx.getSync(['items', 'first'])); // 'a'
console.log(ctx.getSync(['items', 'last'])); // 'c'
// Negative indices
console.log(ctx.getSync(['items', -1])); // 'c' (last element)
console.log(ctx.getSync(['items', -2])); // 'b' (second to last)
// Function calls
console.log(ctx.getSync(['user', 'getName'])); // 'ALICE'Custom objects that implement special liquid behavior.
/**
* Base class for liquid drop objects
*/
abstract class Drop {
/**
* Handle access to undefined properties
* @param key - Property key that was accessed
* @returns Value for the property or Promise<value>
*/
liquidMethodMissing(key: string | number): Promise<any> | any;
}
/**
* Scope type - either regular object or Drop
*/
type Scope = ScopeObject | Drop;
interface ScopeObject extends Record<string, any> {
/** Convert object to liquid representation */
toLiquid?(): any;
}Usage Examples:
import { Drop, Context } from "liquidjs";
// Custom Drop implementation
class UserDrop extends Drop {
constructor(private userData: any) {
super();
}
get name() {
return this.userData.name;
}
liquidMethodMissing(key: string) {
// Handle dynamic properties
if (key.startsWith('is_')) {
const role = key.slice(3);
return this.userData.roles?.includes(role) || false;
}
return undefined;
}
}
// Use Drop in context
const userDrop = new UserDrop({
name: 'Alice',
roles: ['admin', 'editor']
});
const ctx = new Context({ user: userDrop });
console.log(ctx.getSync(['user', 'name'])); // 'Alice'
console.log(ctx.getSync(['user', 'is_admin'])); // true
console.log(ctx.getSync(['user', 'is_guest'])); // falseControl how undefined variables are handled.
interface StrictVariableOptions {
/** Throw error when accessing undefined variables */
strictVariables?: boolean;
/** Only allow access to own properties (not inherited) */
ownPropertyOnly?: boolean;
}Usage Examples:
import { Context, Liquid } from "liquidjs";
// Strict mode - throws on undefined
const strictEngine = new Liquid({ strictVariables: true });
const strictCtx = new Context({ user: 'Alice' }, strictEngine.options);
try {
strictCtx.getSync(['missing_var']); // Throws UndefinedVariableError
} catch (error) {
console.log('Variable not found!');
}
// Lenient mode - returns undefined
const lenientEngine = new Liquid({ strictVariables: false });
const lenientCtx = new Context({ user: 'Alice' }, lenientEngine.options);
console.log(lenientCtx.getSync(['missing_var'])); // undefined (no error)
// Own property only
const ownPropCtx = new Context(
Object.create({ inherited: 'value' }),
{ ...lenientEngine.options, ownPropertyOnly: true }
);
console.log(ownPropCtx.getSync(['inherited'])); // undefined (ignored)Context includes built-in protection against DoS attacks.
interface ContextLimits {
/** Memory usage limiter */
memoryLimit: Limiter;
/** Render time limiter */
renderLimit: Limiter;
}
class Limiter {
constructor(name: string, limit: number);
/** Track resource usage */
use(amount: number): void;
}Usage Examples:
import { Context, Liquid } from "liquidjs";
// Configure limits
const engine = new Liquid({
memoryLimit: 1024 * 1024, // 1MB
renderLimit: 5000 // 5 seconds
});
const ctx = new Context(data, engine.options);
// Limits are enforced automatically during rendering
// Memory usage tracked for string operations, array operations, etc.
// Render time tracked during template executionContext is used throughout template rendering:
<!-- Variable access uses context resolution -->
{{ user.name }} <!-- ctx.getSync(['user', 'name']) -->
{{ items.size }} <!-- ctx.getSync(['items', 'size']) -->
{{ products.first.name }} <!-- ctx.getSync(['products', 'first', 'name']) -->
<!-- Tags create new scopes -->
{% for item in items %}
{{ item }} <!-- 'item' pushed to local scope -->
{% endfor %}
{% assign temp = 'value' %} <!-- Added to bottom scope -->
{{ temp }} <!-- Available after assignment -->
<!-- Captures create variables -->
{% capture content %}
<p>{{ user.name }}</p>
{% endcapture %}
{{ content }} <!-- Available in context -->