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

tagparse

v2.0.1

Published

AST-first templating library for Discord and chat bots. Parse {variable}, {if:cond|then|else}, {each:items|template} placeholder syntax with safe rendering helpers.

Downloads

397

Readme

tagparse

A fast, AST-first templating library for Discord bots and chat applications. Parse {variable} and {tag:arg|arg} placeholder syntax with conditionals, loops, and built-in Discord-safe rendering.

npm types license

tagparse is a small (~10kB gzipped, zero deps), fully-typed templating engine designed for bot message templates — the kind of placeholder syntax users configure in Discord, Slack, and chat-bot dashboards. It parses templates into an AST, gives you a friendly Template.compile().render() API, and ships with batteries-included Discord helpers for mentions, embeds, timestamps, and markdown-safe output.

Why use tagparse?

  • Tiny and fast. Linear-time parser, no dependencies, ESM + CJS. Parses 500KB templates in ~200ms.
  • Compile-once, render-many. Parse a template once, render against different data thousands of times per second. Half a million renders/sec on a laptop.
  • Conditionals and loops built in. {if:cond|then|else}, {each:items|template}, {eq}, {unless}, {not}, {default} — no plugins required.
  • Discord-safe by default. First-class helpers for <@user>, <#channel>, <:emoji:>, <t:timestamp:R>, plus markdown and mention escaping that survives @everyone injection attempts.
  • Custom tag handlers. Register your own tags as plain functions. Structural tags get raw AST access for lazy evaluation.
  • Async resolvers. Fetch user data from a DB or API mid-render with renderAsync().
  • Real diagnostics. Errors with line/column spans and hints, not generic exceptions.
  • Strict TypeScript. Every public type is exported; no any in the public API.

Comparison

| Feature | tagparse | Mustache | Handlebars | EJS | | ----------------------------- | :------: | :------: | :--------: | :-: | | Discord-style {tag:arg} | ✅ | ❌ | ❌ | ❌ | | Built-in mention escaping | ✅ | ❌ | ❌ | ❌ | | Custom delimiters | ✅ | ✅ | ❌ | ❌ | | AST inspection / visitors | ✅ | ❌ | ✅ | ❌ | | Async resolvers | ✅ | ❌ | ❌ | ✅ | | Zero dependencies | ✅ | ✅ | ❌ | ❌ | | TypeScript-native | ✅ | ⚠️ | ⚠️ | ⚠️ |

Install

npm install tagparse

Requires Node 18+. Pure ES2022. ESM and CommonJS both supported.

Quick start

import { Template, builtinTags } from "tagparse";
import { discordTags } from "tagparse/discord";

const tpl = Template.compile(
    "Welcome {mention:{userId}}! You have {if:{premium}|⭐ premium access|a basic plan}.",
);

const message = tpl.render({
    variables: {
        userId: "123456789012345678",
        premium: "true",
    },
    tags: { ...builtinTags, ...discordTags },
});

// → "Welcome <@123456789012345678>! You have ⭐ premium access."

Nested data

For dotted paths like {member.proper} or {guild.id}, use pathResolver:

import { Template, pathResolver } from "tagparse";

const tpl = Template.compile("Hi {member.proper}, you're in {guild.name}!");

tpl.render({
    variables: pathResolver({
        member: { proper: "Alice#1234" },
        guild:  { name: "My Server" },
    }),
});
// → "Hi Alice#1234, you're in My Server!"

pathResolver walks the data object segment by segment. Array indexing works too ({items.0.name}). Zero-arg functions are auto-invoked, so lazy values like { user: () => fetchUser() } are supported. Prototype-pollution paths (__proto__, constructor, prototype) are refused.

Syntax

| Form | Meaning | | -------------------------- | ----------------------------------------------------- | | {name} | Variable substitution | | {tag:arg} | Tag with one argument | | {tag:arg1\|arg2\|arg3} | Tag with multiple pipe-separated arguments | | {outer:{inner}} | Nested tags — inner evaluates first | | \{ \} \| \\ | Escape a delimiter (write a literal {, }, etc.) |

Built-in tags

import { Template, builtinTags } from "tagparse";

Template.compile("{if:{age}|adult|minor}").render({ /* ... */ });
Template.compile("{unless:{banned}|welcome|access denied}");
Template.compile("{each:apple,banana,cherry|<{it}>|, }");
Template.compile("{eq:{role}|admin}");
Template.compile("{ne:{a}|{b}}");
Template.compile("{gt:{score}|100}");
Template.compile("{lt:{score}|10}");
Template.compile("{not:{flag}}");
Template.compile("{upper:{name}}");
Template.compile("{lower:{name}}");
Template.compile("{trim:{input}}");
Template.compile("{length:{name}}");
Template.compile("{replace:{text}|old|new}");
Template.compile("{default:{nickname}|stranger}");

{each} locals

Inside an {each} template, these locals are available:

  • {it} — the current item
  • {idx} — 0-based index
  • {idx1} — 1-based index
  • {first} / {last} — booleans
Template.compile("{each:a,b,c|{idx1}. {it}{if:{last}||\\n}}");
// 1. a
// 2. b
// 3. c

Discord helpers

Import from the dedicated tagparse/discord subpath to keep the core small.

import { Template, builtinTags } from "tagparse";
import { discordTags, escapeDiscord } from "tagparse/discord";

const tpl = Template.compile(
    "Hi {mention:{userId}}, your role is {role:{roleId}}, " +
    "scheduled at {timestamp:{when}|R}. " +
    "Note: {escape:{userInput}}",
);

tpl.render({
    variables: {
        userId: "123456789012345678",
        roleId: "987654321098765432",
        when: "1700000000",
        userInput: "**@everyone — fake announcement**",
    },
    tags: { ...builtinTags, ...discordTags },
});

The {escape:...} tag neutralizes markdown chars (* _ ~ \``) **and** mention syntax (@everyone, <@id>, <@&id>, <#id>`) by inserting zero-width spaces. Use it on every piece of user-controlled text spliced into a template.

| Tag | Output | | ------------------------------------ | ----------------------- | | {mention:id} | <@id> | | {channel:id} | <#id> | | {role:id} | <@&id> | | {emoji:name\|id} | <:name:id> | | {animEmoji:name\|id} | <a:name:id> | | {timestamp:unix\|style} | <t:unix:style> | | {bold:text} / {italic:text} | **text** / *text* | | {underline:text} / {strike:text} | __text__ / ~~text~~ | | {code:text} / {spoiler:text} | `text` / \|\|text\|\| | | {codeblock:lang\|text} | Fenced code block | | {escape:text} | Markdown- and mention-safe |

Custom tags

Plain functions are tags:

const tpl = Template.compile("Total: {sum:{a}|{b}|{c}}");
tpl.render({
    variables: { a: "10", b: "20", c: "30" },
    tags: {
        sum: (args) => args.reduce((acc, v) => acc + Number(v), 0),
    },
});
// → "Total: 60"

For tags that conditionally evaluate args (like {if}), use defineStructuralTag:

import { defineStructuralTag } from "tagparse";

const switchTag = defineStructuralTag((args, ctx, render) => {
    const value = render(args[0]);
    for (let i = 1; i < args.length - 1; i += 2) {
        if (render(args[i]) === value) return render(args[i + 1]);
    }
    return args.length % 2 === 0 ? render(args[args.length - 1]) : "";
});

Template.compile("{switch:{role}|admin|🛡|mod|🔧|user|👤|❓}").render({ /* ... */ });

Async data

import { Template, builtinTags } from "tagparse";

const tpl = Template.compile("Hi {profile:{userId}}!");

await tpl.renderAsync({
    variables: { userId: "123" },
    tags: {
        ...builtinTags,
        profile: async ([id]) => {
            const user = await db.users.findOne({ id });
            return user.displayName;
        },
    },
});

Diagnostics

Template.compile() collects errors and warnings instead of throwing on every issue. This keeps user-authored templates from blowing up bots in production.

const tpl = Template.compile("Hello {unclosed");
console.log(tpl.diagnostics);
// [{ severity: "error", message: "Unclosed tag", span: { start: { line: 1, column: 7, ... }, ... } }]

console.log(tpl.hasErrors); // true

For strict parsing (throw on first error), pass { strict: true } to compile.

Custom delimiters

const tpl = Template.compile("Hi <%= name %>!", {
    tagStart: "<%=",
    tagEnd: "%>",
});

AST inspection

Pre-flight validate templates against a schema:

const tpl = Template.compile(userProvidedTemplate);

// Reject templates referencing variables you don't expose
const allowed = new Set(["user", "channel", "balance"]);
const used = tpl.variableNames;
const unknown = [...used].filter((v) => !allowed.has(v));
if (unknown.length) throw new Error(`Unknown variables: ${unknown.join(", ")}`);

API surface

import {
    // Main facade
    Template,

    // Core functions
    parse,        // (input, options) => { template, diagnostics }
    render,       // (template, options) => string
    renderAsync,  // (template, options) => Promise<string>

    // Tags
    builtinTags,
    defineStructuralTag,

    // Resolvers
    pathResolver,

    // AST utilities
    walk, findNodes, collectVariableNames, collectTagNames,

    // Errors
    TagParseError, StrictModeError, RenderError, AggregateParseError,

    // Lower-level
    Lexer, Stream,
} from "tagparse";

import {
    discordTags,
    escapeDiscord,
    truncate,
    DISCORD_LIMITS,
} from "tagparse/discord";

Performance

| Input size | Parse time | Render time | | ----------- | ---------: | ----------: | | 1 KB | < 1 ms | < 1 ms | | 10 KB | ~3 ms | < 1 ms | | 100 KB | ~25 ms | ~5 ms | | 500 KB | ~150 ms | ~50 ms |

Compile-once / render-many: ~500,000 renders/sec on a typical Discord bot template.

Migrating from v1

v2 is a complete rewrite. The Parser class is gone; use Template.compile() or parse(). The evaluateTags mode is replaced by registering tag handlers in render(). See the CHANGELOG for the full list of changes.

Inspired by

ikigai — the original Discord-bot tag parser this library descends from.

License

MIT