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

@gulomov/modx-tmlanguage

v2.1.0

Published

TextMate grammar for MODX Revolution tag syntax, for VS Code, Sublime Text and other editors that read TextMate grammars

Readme

@gulomov/modx-tmlanguage

CI npm license

Previously published as modx-tmlanguage; that name is deprecated and no longer updated.

A TextMate grammar for the MODX Revolution tag syntax — snippets, chunks, resource fields, placeholders, system settings, links and lexicon entries, with their property lists and output modifiers.

It reads as one grammar over a template file: the surrounding HTML is highlighted by the editor's own HTML grammar, and MODX tags are recognised inside it, including within attribute values and <style>.

Any editor that reads TextMate grammars can use it — Visual Studio Code, Sublime Text, and others.

What it looks like

The sample is docs/preview.tpl, rendered with GitHub's own light and dark themes. Both images are generated — npm run preview:update rewrites them — and a test fails if they drift from what the grammar currently produces. That check earns its place: a scope can be spelled correctly and documented and still be coloured by nothing at all, because no theme targets it, and only rendering through a real theme shows that.

Installation

npm install @gulomov/modx-tmlanguage

Usage

The package entry point resolves to the absolute path of the grammar file, so an editor integration can hand it straight to whatever loads TextMate grammars:

const grammarPath = require('@gulomov/modx-tmlanguage');
// or: import grammarPath from '@gulomov/modx-tmlanguage';

The grammar itself is also exported, for when the parsed object is what you need rather than a path:

const grammar = require('@gulomov/modx-tmlanguage/modx.tmLanguage.json');
grammar.scopeName; // "text.html.modx"

The package also ships a language configuration — the half of editor support that is not colour. It pairs [[ with ]] for bracket matching and selection, tells the comment command to write [[- … ]], closes backticks and quotes as you type, and indents the properties of a snippet call written over several lines:

const config = require('@gulomov/modx-tmlanguage/language-configuration.json');

In a VS Code extension the two are registered together:

{
  "contributes": {
    "languages": [
      { "id": "modx", "extensions": [".tpl", ".chunk"], "configuration": "./language-configuration.json" }
    ],
    "grammars": [
      { "language": "modx", "scopeName": "text.html.modx", "path": "./modx.tmLanguage.json" }
    ]
  }
}

File types

The grammar claims .tpl, .html and .htm. MODX itself puts no constraint on how template and chunk files are named, so a project that stores elements under other extensions needs to say so in the editor rather than wait for the grammar to guess. In VS Code that is a files.associations entry:

{
  "files.associations": {
    "*.chunk": "modx",
    "*.modx": "modx"
  }
}

The value is the language id the grammar is registered under by whatever extension packages it — modx above is an example, not a promise.

Scopes

The grammar's own scope is text.html.modx. It includes text.html.basic for the surrounding markup and injects itself into that markup, so MODX tags are recognised wherever they appear — except inside MODX comments, where the injection is deliberately switched off.

Every scope below ends in .modx, so a theme can target the whole language with one selector, or any individual construct with a longer one. Scopes are listed with the element or character they apply to.

Element tags

Each element type carries its own scope, so a theme can colour a chunk differently from a resource field. The token characters follow switch ($token) in MODX's own modParser.

| Tag | Whole tag | Token character | Name | |---|---|---|---| | [[Snippet]] | meta.tag.snippet.modx | — | entity.name.function.modx | | [[$chunk]] | meta.tag.chunk.modx | support.type.chunk.modx | entity.name.type.chunk.modx | | [[*pagetitle]] | meta.tag.field.modx | support.type.field.modx | variable.other.resource.modx | | [[+placeholder]] | meta.tag.placeholder.modx | support.type.placeholder.modx | variable.other.placeholder.modx | | [[++site_name]] | meta.tag.setting.modx | support.type.setting.modx | variable.other.setting.modx | | [[~12]] | meta.tag.link.modx | support.type.link.modx | constant.other.link.modx | | [[%lexicon.key]] | meta.tag.lexicon.modx | support.type.lexicon.modx | variable.other.lexicon.modx |

A snippet has no token character, which is why that cell is empty — it is the fallback, matching MODX's own default branch.

Common to every tag:

| Part | Scope | |---|---| | [[ | punctuation.definition.tag.begin.modx | | ]] | punctuation.definition.tag.end.modx | | ! (uncached) | keyword.control.uncached.modx |

The # in [[*#pagetitle]] is part of the token character and shares support.type.field.modx, mirroring the parser, which strips it from the name.

Inside a tag

| Part | Scope | |---|---| | @ in @propertySet | punctuation.definition.propertyset.modx | | property set name | entity.name.type.propertyset.modx | | ? before properties | punctuation.separator.properties.modx | | & (and an amp; prefix) | punctuation.definition.parameter.modx | | property name | variable.parameter.modx | | = | keyword.operator.assignment.modx | | value in backticks | string.other.modx | | opening backtick | punctuation.definition.string.begin.modx | | closing backtick | punctuation.definition.string.end.modx | | `` inside a value (an escaped backtick) | constant.character.escape.modx | | value written without backticks | string.unquoted.modx | | : before an output modifier | punctuation.separator.modifier.modx | | output modifier name | support.function.modifier.modx | | number | constant.numeric.modx |

Numbers are scoped only inside tags. A digit in ordinary markup is left alone.

Values written without backticks get a scope of their own rather than being flagged as an error: MODX strips backticks only when they are present, so &tpl=row parses fine, even though wrapping values in backticks is the convention.

Comments and timing tags

| Part | Scope | |---|---| | [[- comment ]] | comment.block.modx | | [[- | punctuation.definition.comment.begin.modx | | closing ]] | punctuation.definition.comment.end.modx | | [^t^] | meta.tag.timing.modx | | the letters between [^ and ^] | constant.other.timing.modx |

A tag written inside a comment stays comment-coloured rather than looking active, and nested tags do not end the comment early.

Tags inside embedded languages

MODX tags are recognised inside HTML attribute values and inside <style>:

<a href="[[~12]]" class="[[+cssClass]]">link</a>
<style>.box { color: [[++brand_color]]; }</style>

Inside <script> they are recognised within string literals, which is where they almost always appear:

<script>var id = "[[*id]]";</script>

A bare tag in JavaScript code is not highlighted. In var n = [[+count]]; the JavaScript grammar reads [[ as the start of a nested array literal and wins over this grammar's injection. MODX substitutes the value there perfectly well — only the colouring is missing. Adding an injection targeted at source.js does not change it, and the behaviour predates the current test suite rather than being introduced by it.

The cases above are pinned by tests, including the limitation, so that a future change in either direction is visible.

Development

Install dependencies and run the test suite:

npm install
npm test

The suite tokenizes fixtures from test/fixtures/ with the same engine VS Code uses (vscode-textmate + vscode-oniguruma) and compares the result against committed snapshots in test/snapshots/. Alongside the snapshots it asserts a few specific behaviours — numbers highlighting only inside tags, comments closing at the first ]], and every scope name starting with a root that editor themes recognise.

One of those checks compares the grammar against the Scopes section above: a scope the grammar emits but the table does not mention fails the suite. Adding a scope therefore means documenting it in the same change.

Contributors should read CONTRIBUTING.md — in particular the rule that MODX's own parser, not documentation or community advice, settles what the grammar should accept.

Six further suites cover what snapshots cannot:

  • Language configuration — the comment marker the editor would insert is put through the grammar, because a marker the editor writes and the grammar does not recognise is worse than none.
  • Indentation — a template is stripped of its indentation and re-indented by the rules in language-configuration.json, and has to come back exactly as it was. The rules are line patterns, so the only way to know what they do to a real template is to run them over one.
  • Previews — the images above are re-rendered through real themes and compared with the committed SVGs. This is the only check that can see a scope no theme colours.
  • Performance — deliberately awkward input under a time budget. These patterns run on every keystroke, and one that backtracks catastrophically stops the editor rather than colouring anything wrongly.
  • Fuzz — templates generated from a fixed seed, held to three rules: markup carrying no tag stays with the host grammar, a closed tag does not colour what follows it, and nothing unterminated survives a blank line. The last one found a real gap: a backticked value had no terminator and coloured the rest of the file.
  • Line endings — the package is built with npm pack and every file in the archive is checked for CR. It reads the archive rather than the working tree, because the working tree is what it is guarding: npm pack packs it verbatim, so a checkout that hands out CRLF publishes CRLF. On Linux this cannot fail, which is the point of running it before every publish — the machine cutting the release is the only one where it can.

After an intentional grammar change, regenerate the snapshots and the previews, and review both diffs before committing:

npm run test:update
npm run preview:update

Most tests run without the surrounding HTML grammar, so snapshots describe this grammar's own rules and do not shift when a third-party grammar changes. test/embedded.test.js is the exception: it loads the HTML, CSS and JavaScript grammars from Shiki — the same ones VS Code uses — to cover tags inside embedded languages.