CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-liquidjs

A simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript.

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

context-and-scoping.mddocs/

Context and Scoping

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.

Capabilities

Context Class

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
});

Variable Resolution

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 }

Scope Management

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"

Register System

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 state

Scope Resolution Order

Variable resolution follows a specific hierarchy:

  1. Local Scopes (newest to oldest) - Variables from push()
  2. Environment Scope - User-provided data
  3. Global Scope - Global variables from options
// 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)

Property Access Features

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 objects
  • first: Returns first element of array or obj.first
  • last: Returns last element of array or obj.last
  • Negative array indices: arr[-1] gets last element
  • Function calls: Functions are automatically called with obj as this
  • Drop method missing: Calls liquidMethodMissing for undefined properties

Usage 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'

Drop Objects

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'])); // false

Strict Variables

Control 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)

Performance and Limits

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 execution

Context in Template Execution

Context 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 -->

docs

analysis.md

built-in-tags.md

configuration.md

context-and-scoping.md

core-engine.md

extensions.md

filesystem.md

filters.md

index.md

tile.json