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

@reteps/tree-sitter-htmlmustache

v0.9.3

Published

HTML with Mustache/Handlebars template syntax grammar for tree-sitter

Readme


Features

  • Syntax Highlighting — Full semantic highlighting for HTML and Mustache, plus embedded JS/TS in <script> and CSS in <style>
  • Document Formatting — Auto-format with EditorConfig and config file support
  • CLI Linter & Formatter — Check and format templates from the command line
  • Document Symbols — Outline view and breadcrumb navigation
  • Folding — Collapse HTML elements and Mustache sections
  • Hover Information — Tag and attribute documentation

Supported Mustache Syntax

| Syntax | Description | | ------------------------- | ---------------------- | | {{name}} | Variable interpolation | | {{{html}}} | Unescaped HTML | | {{#items}}...{{/items}} | Sections | | {{^items}}...{{/items}} | Inverted sections | | {{! comment }} | Comments | | {{> partial}} | Partials |

VS Code Extension

Install from the VS Code Marketplace or search for "HTML Mustache" in the Extensions view.

What you get out of the box:

  • Syntax highlighting (including embedded JS/TS and CSS)
  • Document formatting (format on save, format selection)
  • Error diagnostics (parse errors, mismatched tags)
  • Document outline and breadcrumbs
  • Hover information for HTML tags and attributes
  • Code folding for HTML elements and Mustache sections

Using with .html Files

By default, the extension activates for .mustache, .hbs, and .handlebars files. To use it with .html files, add this to your VS Code settings:

{
  "files.associations": {
    "*.html": "htmlmustache"
  }
}

You can also change the language mode for a single file by clicking the language indicator in the status bar and selecting "HTML Mustache".

CLI

Install globally or run via npx:

npm install -g @reteps/tree-sitter-htmlmustache

htmlmustache check

Check templates for parse errors:

htmlmustache check '**/*.mustache' '**/*.hbs'

If include is configured in .htmlmustache.jsonc, patterns are optional:

htmlmustache check
file.mustache:3:3 error: Mismatched mustache section: {{/wrong}}
  |
1 | {{#items}}
2 |   <li>{{name}}
3 |   {{/wrong}}
  |   ^^^^^^^^^^ Mismatched mustache section: {{/wrong}}

1 error in 1 file (5 files checked)

Detects parse errors, mismatched Mustache sections, mismatched HTML end tags, and missing tokens.

htmlmustache format

Format templates:

htmlmustache format --write '**/*.mustache'

If include is configured in .htmlmustache.jsonc, patterns are optional:

htmlmustache format --write

Check formatting in CI (exits 1 if any files would change):

htmlmustache format --check 'templates/**/*.hbs'

Read from stdin:

echo '<div><p>hi</p></div>' | htmlmustache format --stdin

Options:

| Flag | Description | | ------------------- | ------------------------------------------------ | | --write | Modify files in-place (default: print to stdout) | | --check | Exit 1 if any files would change (for CI) | | --stdin | Read from stdin, write to stdout | | --indent-size N | Spaces per indent level (default: 2) | | --print-width N | Max line width (default: 80) | | --mustache-spaces | Add spaces inside mustache delimiters |

Format Ignore

Skip formatting for specific regions using ignore directives. Both HTML and Mustache comment forms are supported.

Ignore Next Node

Place a comment immediately before the element to preserve its original formatting:

<!-- htmlmustache-ignore -->
<div class="a" id="b">manually formatted</div>
{{! htmlmustache-ignore }}
<table>
  <tr>
    <td>compact</td>
    <td>table</td>
  </tr>
</table>

Only the immediately following sibling node is ignored. Subsequent nodes are formatted normally.

Ignore Region

Wrap a region in start/end comments to preserve everything between them:

<!-- htmlmustache-ignore-start -->
<div class="a">content</div>
<p>kept as-is</p>
<!-- htmlmustache-ignore-end -->
{{! htmlmustache-ignore-start }} {{#items}}
<li>{{name}}</li>
{{/items}} {{! htmlmustache-ignore-end }}

If ignore-start has no matching ignore-end, all remaining siblings in the current scope are preserved as raw text.

Configuration

.htmlmustache.jsonc

Create a .htmlmustache.jsonc file in your project root to configure formatting options. Both the VS Code extension and CLI will pick it up automatically (the file is found by walking up from the formatted file).

{
  // File patterns for CLI commands (used when no patterns are passed as arguments)
  "include": ["**/*.mustache", "**/*.hbs"],

  // Patterns to always exclude (node_modules and .git are excluded by default)
  "exclude": ["**/vendor/**"],

  // Max line width before wrapping (default: 80)
  "printWidth": 100,

  // Spaces per indent level (default: 2)
  "indentSize": 4,

  // Add spaces inside mustache delimiters: {{ foo }} vs {{foo}} (default: false)
  "mustacheSpaces": true,

  // Treat custom tags as raw code blocks (like <script>/<style>)
  "customCodeTags": [
    {
      "name": "x-code",
      "languageDefault": "javascript",
    },
  ],
}

Lint Rules

The following checks are always enabled and report as errors:

  • Syntax errors — invalid or unparseable template syntax
  • Missing tokens — e.g. a missing closing >
  • Mismatched mustache sections{{/wrong}} closing a different section than was opened
  • Mismatched HTML tags — closing tags that don't match their opening tag, including across mustache branches
  • Unclosed HTML tags — non-void elements that are never closed

Additionally, the following rules are configurable. Set their severities ("error", "warning", or "off") in the rules object:

{
  "rules": {
    "consecutiveDuplicateSections": "off",
    "preferMustacheComments": "warning",
  },
}

| Rule | Default | Description | | ------------------------------ | --------- | ---------------------------------------------------------------------------- | | nestedDuplicateSections | error | Flags {{#name}} nested inside another {{#name}} with the same name | | unquotedMustacheAttributes | error | Requires quotes around mustache expressions used as attribute values | | consecutiveDuplicateSections | warning | Warns when adjacent same-name sections can be merged | | selfClosingNonVoidTags | error | Disallows self-closing syntax on non-void HTML elements (e.g. <div/>) | | duplicateAttributes | error | Detects duplicate HTML attributes on the same element | | unescapedEntities | warning | Flags unescaped & and > characters in text content | | preferMustacheComments | off | Suggests replacing HTML comments with mustache comments | | unrecognizedHtmlTags | error | Flags HTML tags that are not standard HTML elements or valid custom elements |

Custom Rules

Define project-specific lint rules using CSS-like selectors. Mustache constructs are written literally — {{foo}}, {{{foo}}}, {{#section}}, {{^inverted}}, {{!comment}}, {{>partial}}:

{
  "customRules": [
    {
      "id": "no-font",
      "selector": "font",
      "message": "The <font> tag is deprecated. Use CSS instead.",
    },
    {
      "id": "images-need-alt",
      "selector": "img:not([alt])",
      "message": "Images must have alt text for accessibility",
    },
    {
      "id": "no-hidden-inputs-in-list",
      "selector": "{{#items}} > input[type=hidden]",
      "message": "Hidden inputs inside {{#items}} sections are usually a mistake",
    },
    {
      "id": "no-relative-client-files-question",
      "selector": "[src^=\"clientFilesQuestion/\"], [href^=\"clientFilesQuestion/\"]",
      "message": "Use {{options.client_files_question_url}}/... instead of a relative clientFilesQuestion/... path.",
      "severity": "warning",
    },
    {
      "id": "no-deprecated-param",
      "selector": "{{data.deprecated_param}}",
      "message": "data.deprecated_param was removed.",
    },
    {
      "id": "no-raw-user-input",
      "selector": "{{{user_input}}}",
      "message": "Never emit user text unescaped.",
    },
  ],
}

Each custom rule requires an id, selector, and message. The severity defaults to "error" but can be set to "warning" or "off".

Selector syntax:

| Selector | Matches | | ----------------------------------------- | ------------------------------------------ | | div | HTML elements by tag name | | * | Any HTML element | | #main | ID (shorthand for [id="main"]) | | .panel | Class (shorthand for [class~="panel"]) | | div span | Descendant (span anywhere inside div) | | div > span | Direct child | | [style] | Attribute presence | | input[type=hidden] | Attribute value (exact) | | [src^="prefix/"] | Attribute starts with | | [href*="substring"] | Attribute contains | | [src$=".png"] | Attribute ends with | | [class~="warning"] | Attribute contains whitespace-token | | img:not([alt]) | Negated attribute / class / id | | {{foo}} | Escaped variable {{foo}} | | {{data.foo}} | Variable with a dotted path | | {{{foo}}} | Triple / unescaped variable | | {{options.*}} | Variable path prefix match | | {{*.deprecated}} | Variable path suffix match | | {{*}} | Any escaped variable | | {{{*}}} | Any triple | | {{#items}} | Section {{#items}}...{{/items}} | | {{^items}} | Inverted section {{^items}}...{{/items}} | | {{#items}} > li | Direct child inside a section | | {{!TODO}} | Comment with exact content | | {{!*TODO*}} | Comment containing "TODO" | | {{>header}} | Partial invocation | | {{>legacy_*}} | Partial name prefix | | pl-multiple-choice:has({{foo}}) | Element containing a given variable | | pl-multiple-choice:not(:has(pl-answer)) | Element missing a required descendant | | div, span | Comma-separated alternatives | | :root | The document root (the whole parse tree) | | :root:has(pl-answer-panel) | Document contains a descendant anywhere | | :root:not(:has(pl-answer-panel)) | Document is missing a descendant anywhere | | :root > section | Top-level element (direct child of root) |

The > (child) combinator is kind-transparent: div > span matches even if a Mustache section sits between them (e.g. <div>{{#show}}<span>{{/show}}</div>), and {{#a}} > {{#b}} matches across intervening HTML elements. {{#foo}} matches only positive sections — to target inverted sections use {{^foo}}.

Document-scoped conditional rules. Use :root with :has(...) / :not(:has(...)) to express rules that depend on the overall document. Chained :has(...) acts as AND, so you can combine "contains X" and "missing Y" in one selector:

{
  "id": "question-needs-answer-panel",
  "selector": ":root:has(pl-question-panel):not(:has(pl-answer-panel))",
  "message": "A question-panel document must also declare a pl-answer-panel.",
}

When :root matches, the diagnostic is reported at the start of the document (row 0, column 0) so the squiggle doesn't span the whole file. :root here is the tree-sitter fragment root, so it works on partial templates and fragments — unlike browser CSS, which anchors :root on <html>. Inside :has(...), :root refers to the element being has-checked, not the document.

Disabling Lint Rules

Disable a lint rule for an entire file with an inline comment:

<!-- htmlmustache-disable preferMustacheComments -->
{{! htmlmustache-disable selfClosingNonVoidTags }}

The comment can appear anywhere in the file. Both built-in and custom rules can be disabled by name/id. Use multiple comments to disable multiple rules.

EditorConfig

Both the CLI and VS Code extension respect your .editorconfig file for indentation settings (indent_style, indent_size). EditorConfig values override .htmlmustache.jsonc for indentation, and CLI flags override everything.

Priority order: defaults < .htmlmustache.jsonc < .editorconfig (indent only) < CLI flags

Acknowledgments

This project is based on tree-sitter-html by Max Brunsfeld and Amaan Qureshi.