Files
skills/docs/foundation/data/templates/src/plugins/dynamic/CounterPlugin.md
2026-05-29 08:20:56 +01:00

3.6 KiB

CounterPlugin

Purpose

Provides stateful auto-incrementing counters within a single template rendering pass. Counters are scoped to the ParseState object created for each parse() call, meaning counter values always start fresh on every render. Multiple independent named counters can exist simultaneously, and each counter can be configured with a custom starting value and optional zero-padding, making the plugin suitable for generating numbered lists, pagination markers, or padded identifiers.

Features

  • {_counter} — default counter, increments from 1.
  • {_counter:10} — start counting from a custom initial value.
  • {_counter:1:3} — start at 1 and zero-pad output to 3 digits (001, 002, …).
  • {_counter:name} — independent named counter starting at 1.
  • {_counter:name:5} — named counter starting at 5.
  • {_counter:name:1:4} — named counter, start 1, pad to 4 digits.
  • Multiple counters in the same template are each incremented in order of appearance.
  • Counters reset to their initial value on every new parse() call.

Usage: Basic Example

import { TemplateEngine, createCounterPlugin } from "@suaveplan/templates";

const engine = new TemplateEngine().use(createCounterPlugin());

engine.parse("{_counter}. {_counter}. {_counter}.", {});
// → "1. 2. 3."

// Zero-padded counter
engine.parse("{_counter:1:3} {_counter:1:3} {_counter:1:3}", {});
// → "001 002 003"

Usage: Advanced Example

import { TemplateEngine, createCounterPlugin, createDotNotationPlugin } from "@suaveplan/templates";

const engine = new TemplateEngine()
    .use(createCounterPlugin())
    .use(createDotNotationPlugin());

// Two independent named counters in the same template
const tpl = "Row {_counter:row}: Col {_counter:col} | Row {_counter:row}: Col {_counter:col}";
engine.parse(tpl, {});
// → "Row 1: Col 1 | Row 2: Col 2"

// Counters reset on every parse call
engine.parse("{_counter}", {});  // "1"
engine.parse("{_counter}", {});  // "1" (fresh state)

API Reference

createCounterPlugin<TContext>(): Plugin<TContext>

Returns a Plugin with:

  • name: "counter"
  • priority: PluginPriority.HIGH (80)
  • pattern: /\{_counter(?::([^:}]+)(?::(\d+)(?::(\d+))?)?)?\}/g

The pattern captures up to three optional groups:

  1. Name-or-start: either a counter name (non-numeric string) or a numeric start value
  2. Start-or-padding: a numeric start value (when group 1 is a name) or a pad length
  3. Padding: pad length (only when group 1 is a name and group 2 is a start value)

transform(match, _context, state): Parses the three optional groups to determine counter name, start value, and pad length. Calls getCounter(state, name, start) to advance the counter and get its current value. Returns pad(value, padLength) when pad length > 0, otherwise String(value).

Implementation Notes

  • getCounter from ParseState initialises a counter at initialValue - 1 on first access so that the first call returns initialValue without a special branch. This detail is encapsulated in ParseState; the plugin only sees the returned current value.
  • The disambiguation between a numeric first argument and a named counter is done with /^\d+$/.test(nameOrStart). A counter named "42" is therefore interpreted as a start value, not a name — name strings must contain at least one non-digit character.
  • Padding uses the pad(value, length) utility from format.ts, which zero-pads from the left by default.
  • Runs at HIGH priority (80) so counters resolve before MEDIUM-priority transforms but after CRITICAL control-flow plugins. This ensures loop bodies can contain counter tokens that are resolved during recursive rendering.