npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@optima-crm/shortcodes

v0.1.2

Published

WordPress-compatible [shortcode] parser for Node.js with an optional NestJS integration. Node.js equivalent of the PHP library tpoxa/shortcodes

Downloads

443

Readme

@optima-crm/shortcodes

A [shortcode] parser for Node.js, with an optional NestJS integration.

It is the Node.js equivalent of the PHP library tpoxa/shortcodes (tpoxa\shortcodes\Shortcode), which ports the WordPress shortcode parser. It uses the same syntax and parsing rules, so templates written for the PHP library render the same way here:

Hello [user attribute=name],
today is [date format="d/m/Y"].
[box]Enclosed [b]content[/b][/box]   [[escaped]]
  • Framework-agnostic core: no dependencies.
  • Async handlers: load data from a database or an API while rendering.
  • Explicit context: pass the ids of the records being rendered to parse().
  • Per-render cache (memo): many tags that need the same record load it only once.
  • NestJS module: every shortcode can be an injectable class with its own dependencies.
  • PHP date() formats, including a ready-made [date] shortcode.
  • Written in TypeScript, with type definitions included.

Installation

npm install @optima-crm/shortcodes

Node.js 18 or newer is required.

The NestJS integration (@optima-crm/shortcodes/nestjs) additionally needs @nestjs/common, @nestjs/core (version 10 or newer) and reflect-metadata. A NestJS project has these already. They are optional peer dependencies, so the core works without them.

Quick start

import { ShortcodeEngine } from '@optima-crm/shortcodes';

const shortcodes = new ShortcodeEngine();

shortcodes.register('name', ({ context }) => context.name);
shortcodes.register('upper', ({ content }) => (content ?? '').toUpperCase());

const html = await shortcodes.parse('Hello [name], [upper]welcome[/upper]!', { name: 'John' });
// → "Hello John, WELCOME!"

Tags that are not registered are left in the text unchanged.

Syntax

| Form | Example | Handler receives | |---|---|---| | Self-closing | [price], [price /] | content: null | | With attributes | [property attribute="title" lang=es] | attrs: { attribute: 'title', lang: 'es' } | | Positional attributes | [gallery 5 "large"] | attrs: { '0': '5', '1': 'large' } | | Enclosing | [box]inner text[/box] | content: 'inner text' | | Escaped | [[price]] | not called; prints [price] |

Attribute values can be in double quotes, single quotes or unquoted. Attribute names are lower-cased, and tag names are case-sensitive.

Handlers

A handler is a function, sync or async, that receives one object:

| Field | Description | |---|---| | tag | The tag name, e.g. property | | attrs | Attributes as written in the tag, or null if there are none | | options | The handler's defaults with attrs applied on top | | content | Inner text of [tag]…[/tag], or null for a self-closing tag | | context | The object passed to parse() | | memo(key, factory) | Runs factory once per parse() call for this key and shares the result | | parse(text) | Parses nested text with the same context and cache |

It returns a string, a number, a boolean or null/undefined (or a promise of one). null, undefined and false render as an empty string, and true renders as 1.

Defaults and options

shortcodes.register(
  'property',
  async ({ options, context, memo }) => {
    const property = await memo(`property:${context.propertyId}`, () => db.properties.findById(context.propertyId));
    return property?.[options.attribute as string];
  },
  { defaults: { attribute: 'reference', lang: 'en' } },
);

await shortcodes.parse('[property] / [property attribute=city]', { propertyId: '42' });
// the property is loaded once for both tags

Registering several handlers

shortcodes.registerAll([
  { tag: 'first', handler: () => '1' },
  { tag: 'second', handler: () => '2', defaults: { size: 'small' } },
]);

shortcodes.has('first'); // true
shortcodes.tags; // ['first', 'second']
shortcodes.unregister('first');

Registering a tag that already exists throws DuplicateShortcodeError. Pass { override: true } to replace it.

Typed context

interface MailContext {
  userId: string;
  lang?: string;
}

const shortcodes = new ShortcodeEngine<MailContext>();
shortcodes.register('lang', ({ context }) => context.lang ?? 'en'); // context is typed

Engine options

new ShortcodeEngine({
  onError: 'throw',
  errorListener: (error, tag, source) => console.error(tag, error),
  strictAttributes: false,
});

| Option | Default | Description | |---|---|---| | onError | 'throw' | What to do when a handler throws: 'throw' rejects the whole parse(), 'remove' renders the tag as empty text, 'keep' leaves the original tag in place | | errorListener | – | Called for every handler error, e.g. for logging | | strictAttributes | false | Reject attributes that are not in the handler's defaults (UnknownShortcodeAttributeError) |

Errors: ShortcodeError (base class), DuplicateShortcodeError, InvalidShortcodeTagError, ShortcodeHandlerError (wraps the handler's error in cause), UnknownShortcodeAttributeError.

Built-in [date] shortcode

import { dateShortcode } from '@optima-crm/shortcodes';

shortcodes.registerAll([dateShortcode({ timeZone: 'UTC' })]);

await shortcodes.parse('[date] | [date format="l jS F Y, H:i"]');
// → "2024-02-29 | Thursday 29th February 2024, 13:05"

format uses PHP date() format characters and defaults to Y-m-d. Without timeZone, the Node.js process timezone is used.

The formatter is also exported on its own:

import { formatPhpDate } from '@optima-crm/shortcodes';

formatPhpDate('d/m/Y H:i', new Date(), 'UTC');

All PHP format characters are supported. The one difference is T (timezone abbreviation): Node.js only knows abbreviations such as UTC or EST, so for other zones it prints the offset (+02) where PHP prints e.g. CEST.

NestJS

// app.module.ts
import { Module } from '@nestjs/common';
import { dateShortcode } from '@optima-crm/shortcodes';
import { ShortcodesModule } from '@optima-crm/shortcodes/nestjs';

@Module({
  imports: [
    ShortcodesModule.forRoot({
      handlers: [dateShortcode({ timeZone: 'UTC' })],
      onError: 'throw',
    }),
  ],
})
export class AppModule {}

Or configure it from other providers with forRootAsync:

ShortcodesModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    handlers: [dateShortcode({ timeZone: config.get('TIMEZONE') })],
  }),
});

Class-based shortcodes

Decorate an injectable class with @Shortcode() and list it in the providers of any module. The module finds and registers it on startup, so it can inject models and services:

import { Injectable } from '@nestjs/common';
import { ShortcodeInvocation } from '@optima-crm/shortcodes';
import { Shortcode, ShortcodeHandler } from '@optima-crm/shortcodes/nestjs';
import { UsersService } from './users.service'; // any service or model of your app

@Injectable()
@Shortcode('user', { defaults: { attribute: 'name' } })
export class UserShortcode implements ShortcodeHandler<{ userId: string }> {
  constructor(private readonly users: UsersService) {}

  async render({ options, context, memo }: ShortcodeInvocation<{ userId: string }>) {
    const user = await memo(`user:${context.userId}`, () => this.users.findById(context.userId));
    return user?.[options.attribute as string];
  }
}

One class can handle several tags: @Shortcode(['tag_one', 'tag_two']).

Rendering

ShortcodesService is global by default (turn this off with isGlobal: false):

import { Injectable } from '@nestjs/common';
import { ShortcodesService } from '@optima-crm/shortcodes/nestjs';

@Injectable()
export class MailService {
  constructor(private readonly shortcodes: ShortcodesService) {}

  render(template: string, userId: string) {
    return this.shortcodes.parse(template, { userId });
  }
}

Handlers must be singletons. A request-scoped handler is rejected at startup; pass per-request data through the parse() context instead.

Parsing rules

The parser is a port of tpoxa\shortcodes\Shortcode and follows its rules (the WordPress rules) exactly, including their edge cases, so existing templates render the same. It is tested against output recorded from the PHP library itself. Worth knowing:

  • Unknown tags, and text such as array[0], are left unchanged.
  • A tag name needs at least two characters.
  • [price-old] and [price_x] are not treated as [price].
  • [price/] (no space before /) is only recognised when a plain [price] also appears in the same text. Write [price /] or [price].
  • A newline or non-breaking space directly after the tag name stops the tag from being recognised. Between attributes, both are fine.
  • Attribute values are unescaped like PHP stripcslashes() (\t, \x41, …).
  • An unquoted value ending in / loses it: in [link url=https://a.com/], /] closes the tag. Quote such values: [link url="https://a.com/"].
  • Tags are rendered one tag name at a time, in the order the names first appear. When a handler's output contains a tag whose name first appears later in the text, that tag is rendered too.

Differences from tpoxa\shortcodes\Shortcode:

  • Handlers can be async, and the data they need is passed in the context (not in a global variable).
  • Registering the same tag twice throws, instead of silently keeping the last one.
  • Unknown attributes are ignored unless strictAttributes is on.
  • content is null for self-closing tags (PHP passed ''), and parse(null) returns ''.

Other exports

For advanced use, the low-level functions are exported too: getShortcodeList(text), getShortcodeRegexp(tag), parseAttributes(text) and stripcslashes(text).

License

UNLICENSED (all rights reserved).