Skip to main content

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

Project description

HTML Mustache Logo

HTML Mustache

Full language support for HTML with Mustache/Handlebars templates

Lint LSP Open VSX npm PyPI


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 @prairielearn/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

Python API

The grammar is also published to PyPI as tree-sitter-htmlmustache for use with py-tree-sitter.

pip install tree-sitter tree-sitter-htmlmustache
from tree_sitter import Language, Parser
import tree_sitter_htmlmustache

parser = Parser(Language(tree_sitter_htmlmustache.language()))
tree = parser.parse(b"<div>{{#items}}<li>{{name}}</li>{{/items}}</div>")
print(tree.root_node)

JavaScript API

The package ships three independent subpath exports. Pick the one you need; each pulls in only its own deps.

Subpath What you get Deps
./parser createParser({ locateWasm }) → typed JSON AST + walk helper web-tree-sitter
./linter createLinter({ locateWasm })lint(source, config) → Diagnostic[] web-tree-sitter, ajv
./formatter createFormatter({ locateWasm, prettier })format(source, config) → string web-tree-sitter, peer prettier
./wasm Direct URL of tree-sitter-htmlmustache.wasm

Parser — typed JSON AST

import {
  createParser,
  walk,
} from '@prairielearn/tree-sitter-htmlmustache/parser';

const parser = await createParser({
  locateWasm: '/static/tree-sitter-htmlmustache.wasm',
});

const { rootNode, hasError } = parser.parse('<p>{{name}}</p>');

walk(rootNode, (node, parents) => {
  switch (node.type) {
    case 'html_element':
      // node.children is narrowed to HtmlStartTagNode | HtmlEndTagNode | ...
      console.log('element:', node.text);
      break;
    case 'mustache_interpolation':
      // node.children: MustacheIdentifierNode | MustachePathExpressionNode
      console.log('interpolation:', node.text);
      break;
  }
});

The SyntaxNode discriminated union is generated from the grammar's node-types.json — switching on node.type narrows node.children to the exact set of allowed children. Build your own validator on top of walk with full autocomplete and exhaustiveness checking.

Linter

import {
  createLinter,
  DEFAULT_CONFIG,
} from '@prairielearn/tree-sitter-htmlmustache/linter';

const linter = await createLinter({
  locateWasm: '/static/tree-sitter-htmlmustache.wasm',
});
const diagnostics = linter.lint('<a href={{url}}></a>', DEFAULT_CONFIG);
// → [{ line, column, severity: 'error', ruleName: 'unquotedMustacheAttributes', message, ... }]

Custom rules and per-tag JSON-Schema validation are passed through the config argument — see the Configuration section for the shape.

Formatter

import { createFormatter } from '@prairielearn/tree-sitter-htmlmustache/formatter';
import prettier from 'prettier';

const formatter = await createFormatter({
  locateWasm: '/static/tree-sitter-htmlmustache.wasm',
  prettier, // optional — for embedded <script> / <style>
});

const formatted = await formatter.format('<div><p>hi</p></div>', {
  indentSize: 2,
});

locateWasm

Both a string (URL of the grammar wasm) and a callback are supported:

locateWasm: (name) => {
  if (name === 'tree-sitter-htmlmustache.wasm')
    return '/static/htmlmustache.wasm';
  return `/static/${name}`; // web-tree-sitter resolves tree-sitter.wasm itself
};

In Node, pass absolute file paths. In the browser, pass URLs.

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).

{
  "$schema": "https://raw.githubusercontent.com/prairielearn/tree-sitter-htmlmustache/main/schemas/htmlmustache-config.schema.json",

  // 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>), or bind
  // a JSON Schema for attribute/child validation — see Tag Schemas below.
  "customTags": [
    {
      "name": "x-code",
      "languageDefault": "javascript",
    },
  ],
}

The schema URL above tracks main. For release-pinned validation, replace main with a tag such as v1.4.0.

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
elementContentTooLong off Flags configured elements whose inner content exceeds a byte-length threshold
customTagSchema error Validates configured custom tags against their JSON Schema contracts
customTagDeprecations warning Surfaces deprecated: true annotations in custom tag schemas
pluginModule error Reports plugin module load and export-shape failures

Tag Schemas

Custom tags can declare a JSON Schema draft-06 contract. The schema may be inline or a path resolved relative to .htmlmustache.jsonc:

{
  "customTags": [
    {
      "name": "pl-multiple-choice",
      "schema": "elements/pl-multiple-choice/pl-multiple-choice.schema.json",
    },
  ],
  "rules": {
    "customTagSchema": "error",
  },
}

Schemas validate the tag's flat attribute object. The tag name is implicit from customTags[].name:

{
  "answers-name": "q1",
  "weight": "2",
  "disabled": true,
}

Attribute values are coerced by JSON Schema ("2" can satisfy an integer, boolean attributes become true). Attribute values containing mustache are treated as unknown runtime values, so value-dependent schema errors are waived while presence and unknown-attribute checks still run.

HTML boolean attributes are allowed on custom tags by default, so <pl-answer correct> validates as { "correct": true }. To require explicit values for custom tag attributes by default, set customTagDefaults.allowBooleanAttributes to false; individual custom tag entries can opt back in:

{
  "customTagDefaults": { "allowBooleanAttributes": false },
  "customTags": [{ "name": "pl-answer", "allowBooleanAttributes": true }],
}

When boolean attributes are disabled for a custom tag, <pl-answer correct> reports Attribute "correct" on <pl-answer> must have a value. Ordinary HTML boolean attributes such as <input disabled> are not affected by this custom tag setting.

Custom tags can also declare parent-owned child schemas. Child schemas validate a direct child tag's flat attribute object only in the context of that parent:

{
  "customTags": [
    {
      "name": "pl-multiple-choice",
      "schema": "elements/pl-multiple-choice/pl-multiple-choice.schema.json",
      "children": [
        {
          "name": "pl-answer",
          "schema": "elements/pl-multiple-choice/pl-answer.schema.json",
        },
      ],
    },
    { "name": "pl-answer" },
  ],
}

By default, the example above allows only direct <pl-answer> HTML elements under <pl-multiple-choice>. Set "allowAdditionalChildren": true on the parent tag to keep unlisted direct child elements allowed while still schema-validating listed child tags when they appear. Mustache sections are transparent for this check: <pl-answer> inside {{#cond}}...{{/cond}} still counts as a direct child of the surrounding parent element.

Parent-owned child schemas do not create a global schema for the child tag. A bare <pl-answer> outside <pl-multiple-choice> is recognized by { "name": "pl-answer" }, but it does not use the multiple-choice-specific child schema.

If a child tag is declared only inside children and is not listed as a top-level customTags entry, it is recognized only as a child-owned tag and may appear only as a direct child of the parent tags that declared it. Listing the same tag at the top level means it can also appear in any other context.

children can be nested recursively. Each level still validates only direct children, so this keeps <pl-answer> scoped to <pl-multiple-choice> while giving <pl-answer> its own allowed direct child tags:

{
  "customTags": [
    {
      "name": "pl-multiple-choice",
      "children": [
        {
          "name": "pl-answer",
          "schema": "elements/pl-multiple-choice/pl-answer.schema.json",
          "children": [{ "name": "pl-answer-feedback" }],
        },
      ],
    },
  ],
}

Same-name child entries are self-references. This child-only recursive tag allows <pl-node> to nest under itself at any depth while still rejecting other direct children:

{
  "customTags": [
    {
      "name": "pl-tree",
      "children": [
        {
          "name": "pl-node",
          "children": [{ "name": "pl-node" }],
        },
      ],
    },
  ],
}

Example schema:

{
  "$schema": "http://json-schema.org/draft-06/schema#",
  "type": "object",
  "properties": {
    "answers-name": { "type": "string" },
    "weight": { "type": "number", "minimum": 0 },
    "disabled": { "type": "boolean" },
  },
  "required": ["answers-name"],
  "additionalProperties": false,
}

Schemas must declare draft-06 using http://json-schema.org/draft-06/schema# (the https form and missing trailing # are also accepted). htmlGlobalAttributes, custom AJV keywords, and schema access to tag, text, innerHtml, or children are intentionally not supported; use tag validators for content or relationship checks.

Diagnostics

Schema diagnostics are phrased in HTML/element terms rather than JSON-Schema vocabulary, so template authors aren't asked to translate instancePath and additionalProperty back into the markup they wrote. Examples:

Schema constraint Diagnostic
required: ["answers-name"] <pl-multiple-choice> is missing required attribute "answers-name".
additionalProperties: false Unknown attribute "extra" on <pl-multiple-choice>.
strict unlisted child <pl-multiple-choice> only allows these child elements: <pl-answer>.
child-only tag outside its parent <pl-answer> may only appear as a direct child of these parent elements: <pl-multiple-choice>.
child additionalProperties Unknown attribute "ranking" on <pl-answer> inside <pl-multiple-choice>.
custom tag boolean attribute Attribute "correct" on <pl-answer> must have a value.
properties.display.enum Attribute "display" on <pl-multiple-choice> must be one of: "block", "inline".
properties.size.type: "integer" Attribute "size" on <pl-multiple-choice> must be integer.
properties.weight.minimum: 0 Attribute "weight" on <pl-multiple-choice> must be >= 0.

Constraints without a rewriter fall through to ajv's localized text. Every diagnostic carries ruleName: "customTagSchema" and points at the element or attribute — see Disabling Lint Rules to silence them per-region.

Custom error messages

Override individual diagnostics with JSON Schema's errorMessage keyword (enabled via ajv-errors). Authored strings flow through unchanged — the HTML-shaped rewriter only kicks in when the schema doesn't provide one.

{
  "type": "object",
  "required": ["answers-name"],
  "properties": {
    "size": { "type": "integer", "minimum": 1 },
    "display": { "enum": ["block", "inline", "dropdown"] },
  },
  "errorMessage": {
    "required": {
      // Per-property override for `required` failures
      "answers-name": "Add `answers-name=\"...\"` so this question can be graded.",
    },
    "properties": {
      // Per-property override for *value-level* failures (type, minimum, enum, ...)
      "size": "`size` must be a positive whole number — got ${0}.",
      "display": "`display` must be one of: block, inline, dropdown.",
    },
    // Catch-all for the whole object
    "_": "<pl-multiple-choice> failed its schema.",
  },
}

ajv-errors substitutes the original ajv error with one whose message is your string; the rewriter sees keyword: "errorMessage", doesn't recognise it, and passes the string through. Use this when the default phrasing doesn't say enough about your domain (e.g. linking authors to a runbook URL, naming a specific config the attribute drives).

Tag validators

Use synchronous JavaScript validators for checks that need children, content, ordering, or cross-element relationships. Validators run only for tags that are also declared in customTags[]; the plugin does not make undeclared tags known by itself.

// scripts/htmlmustache-plugin.mjs
import {
  attr,
  defineTagValidators,
  validateAttributes,
  validateElement,
} from '@prairielearn/tree-sitter-htmlmustache/linter';

export const validators = defineTagValidators('pl-order-blocks', {
  'pl/order-blocks-children'(element, context) {
    for (const child of element.childrenWithoutTag('pl-answer')) {
      context.reportElement(
        child,
        '<pl-order-blocks> only allows <pl-answer> children.',
      );
    }
  },
  'pl/order-blocks-attributes'(element, context) {
    const gradingMethod =
      attr(element, 'grading-method').literalMap((value) =>
        typeof value === 'string' ? value : undefined,
      ) ?? 'ordered';
    if (!['ordered', 'unordered'].includes(gradingMethod)) {
      context.reportAttribute(
        element,
        'grading-method',
        'grading-method must be ordered or unordered.',
      );
    }

    validateElement(context, element, {
      reportAttribute: 'code-language',
      invalid(target) {
        return (
          attr(target, 'code-language').present() &&
          attr(target, 'format').literal() !== 'code'
        );
      },
      message: 'code-language is only valid when format is code.',
    });

    validateAttributes(context, element, ['min-incorrect'], {
      invalid(_target, attribute) {
        return (
          attribute.present() &&
          attribute.literal() !== undefined &&
          attribute.literalMap((value) => {
            if (typeof value !== 'string') return undefined;
            const trimmed = value.trim();
            if (!/^[+-]?\d+$/.test(trimmed)) return undefined;
            return Number(trimmed);
          }) === undefined
        );
      },
      message(_target, attribute) {
        return `${attribute.name} must be an integer.`;
      },
    });
  },
});

element.children contains direct child HTML elements. Mustache sections are transparent, so children inside {{#section}}...{{/section}} are included. Child facades are populated recursively, so validators can inspect nested direct children from each child facade. element.innerHtml is present only when the validator opts into options.includeInnerHtml.

defineTagValidators(tagOrTags, rules) is optional sugar for plugin authors. It lowercases the target tag or tags and expands each rule-map entry into an independent validator; rule-map keys are the exact rule ids used by rules config and inline disable comments. A rule can be a bare validation function or an object with validate, severity, and options.

Attribute helper names are case-insensitive. Use attr(element, name).present() for presence and attr(element, name).literal() when dynamic Mustache values should be treated as unknown. Use attr(element, name).literalMap(mapper) to convert literal values into project-specific enums, booleans, numbers, or other validated values. literalMap(mapper) runs only for literal values and returns undefined when the attribute is missing, dynamic, or rejected by the mapper.

Use validateElement(context, element, options) and validateAttributes(context, element, names, options) for common report-if-invalid checks. validateElement reports on the element unless options.reportAttribute is set. validateAttributes passes an attribute helper to invalid(element, attribute) and reports against the matching attribute name.

Use childrenWithTag(tag) and childrenWithoutTag(tag) for direct child tag checks. ValidatorContext also exposes reportElement(element, message) and reportAttribute(element, name, message) as shorthand for report(...).

Validator ids are rule ids. Configure severity and inline disables the same way as built-in rules:

{
  "rules": {
    "pl/order-blocks-children": "warning",
  },
}

Custom formats

Schemas can use the format keyword for value rules that don't fit into enum/pattern/type — for example, the case-insensitive truthy-string set PrairieLearn accepts for boolean attributes (true/t/1/yes/y/on and their negatives, any case). Register the format implementation programmatically when constructing the linter:

import {
  createLinter,
  type SchemaFormat,
} from '@prairielearn/tree-sitter-htmlmustache/linter';

const BOOLEAN_STRINGS = new Set([
  'true',
  't',
  '1',
  'yes',
  'y',
  'on',
  'false',
  'f',
  '0',
  'no',
  'n',
  'off',
]);
const plBoolean: SchemaFormat = (value) =>
  typeof value === 'string' && BOOLEAN_STRINGS.has(value.toLowerCase());

const linter = await createLinter({
  locateWasm,
  formats: { 'pl-boolean': plBoolean },
});

A schema then references it like any built-in format:

{
  "$schema": "http://json-schema.org/draft-06/schema#",
  "type": "object",
  "properties": {
    "fixed-order": { "type": "string", "format": "pl-boolean" },
  },
}

Formats are functions (or RegExp, or ajv FormatDefinition objects), so they can't live directly in .htmlmustache.jsonc. For the CLI and the VS Code extension (which both load .htmlmustache.jsonc), supply them through a pluginModule field pointing at a relative JS/TS file that exports a formats record:

// .htmlmustache.jsonc
{
  "customTags": [
    {
      "name": "pl-multiple-choice",
      "schema": "elements/pl-multiple-choice/pl-multiple-choice.schema.json",
    },
  ],
  "pluginModule": "./scripts/htmlmustache-plugin.mjs",
  "rules": { "customTagSchema": "error" },
}
// scripts/htmlmustache-plugin.mjs
const BOOLEAN_STRINGS = new Set([
  'true',
  't',
  '1',
  'yes',
  'y',
  'on',
  'false',
  'f',
  '0',
  'no',
  'n',
  'off',
]);

export const formats = {
  'pl-boolean': (value) =>
    typeof value === 'string' && BOOLEAN_STRINGS.has(value.toLowerCase()),
};

The CLI and the language server dynamically import this file once per process (cached by absolute path) and register the exports before linting. Note the trust implication: pointing pluginModule at a path means running that code in-process when the CLI lints or the VS Code extension opens a document — treat it like a build script in your repo.

If the file can't be loaded, or its export shape is wrong, you'll see a pluginModule-rule diagnostic naming the path in the failure message; tag schemas that reference an unregistered format will then fail at compile time with the standard ajv "unknown format" error.

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tree_sitter_htmlmustache-1.4.3.tar.gz (72.6 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

tree_sitter_htmlmustache-1.4.3-cp310-abi3-win_amd64.whl (36.9 kB view details)

Uploaded CPython 3.10+Windows x86-64

tree_sitter_htmlmustache-1.4.3-cp310-abi3-musllinux_1_2_x86_64.whl (69.0 kB view details)

Uploaded CPython 3.10+musllinux: musl 1.2+ x86-64

tree_sitter_htmlmustache-1.4.3-cp310-abi3-musllinux_1_2_aarch64.whl (68.8 kB view details)

Uploaded CPython 3.10+musllinux: musl 1.2+ ARM64

tree_sitter_htmlmustache-1.4.3-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (70.9 kB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ ARM64manylinux: glibc 2.28+ ARM64

tree_sitter_htmlmustache-1.4.3-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl (70.0 kB view details)

Uploaded CPython 3.10+manylinux: glibc 2.28+ x86-64manylinux: glibc 2.5+ x86-64

tree_sitter_htmlmustache-1.4.3-cp310-abi3-macosx_11_0_arm64.whl (35.6 kB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

File details

Details for the file tree_sitter_htmlmustache-1.4.3.tar.gz.

File metadata

  • Download URL: tree_sitter_htmlmustache-1.4.3.tar.gz
  • Upload date:
  • Size: 72.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tree_sitter_htmlmustache-1.4.3.tar.gz
Algorithm Hash digest
SHA256 f1009838e49622c68dec35ad884cfc13358662a67003158fa4c4208ef186c0e0
MD5 55fd89584308f412d3e2a047b741d0eb
BLAKE2b-256 668a87cfe51ddc4ca9bd8df36802d7cfe20e1647b97bc500fc6b0b95e45c19a1

See more details on using hashes here.

Provenance

The following attestation bundles were made for tree_sitter_htmlmustache-1.4.3.tar.gz:

Publisher: publish-pypi.yml on PrairieLearn/tree-sitter-htmlmustache

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tree_sitter_htmlmustache-1.4.3-cp310-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for tree_sitter_htmlmustache-1.4.3-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 a674138f630a7ba0b212ac133ab8eb6bb16033d805ba709fba1f75e38e04ea97
MD5 daa4258ac9456f168f93df4b29b3e742
BLAKE2b-256 102f143055304b42264f2e084eb73628c27206ff29f75741a9519edb083128ad

See more details on using hashes here.

Provenance

The following attestation bundles were made for tree_sitter_htmlmustache-1.4.3-cp310-abi3-win_amd64.whl:

Publisher: publish-pypi.yml on PrairieLearn/tree-sitter-htmlmustache

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tree_sitter_htmlmustache-1.4.3-cp310-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for tree_sitter_htmlmustache-1.4.3-cp310-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 a1bb32cdfaf6aa70496712bdbdeeaaa904811d4bfb72369096cda23ced212f41
MD5 a9da0a3ff3559dfe7c0e6d2b62a93153
BLAKE2b-256 e750f96dd36a42de2cffbbbaf47966d400663c60db8602e20cf4d8686c09f276

See more details on using hashes here.

Provenance

The following attestation bundles were made for tree_sitter_htmlmustache-1.4.3-cp310-abi3-musllinux_1_2_x86_64.whl:

Publisher: publish-pypi.yml on PrairieLearn/tree-sitter-htmlmustache

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tree_sitter_htmlmustache-1.4.3-cp310-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for tree_sitter_htmlmustache-1.4.3-cp310-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 54a7368b296e8c3af7250324bd60a8c6d174a30a0ef81b130ba5e24adaf16405
MD5 444921020c7e59b371381d4e77fc8f72
BLAKE2b-256 b6052d031a19e4c1a13e8b0b6ba21c911b0bdc05c2f53f9e1a9a1e8eb85bb456

See more details on using hashes here.

Provenance

The following attestation bundles were made for tree_sitter_htmlmustache-1.4.3-cp310-abi3-musllinux_1_2_aarch64.whl:

Publisher: publish-pypi.yml on PrairieLearn/tree-sitter-htmlmustache

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tree_sitter_htmlmustache-1.4.3-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for tree_sitter_htmlmustache-1.4.3-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 d4013f69408c8707d55961784ca132ec9b39c0076e86953149d8ada284b55179
MD5 12b1da4c41b189111f316c445e302850
BLAKE2b-256 3959d9412e0ff35631b236455aadb60766f9b5b5111fe621383fc8c535de91ea

See more details on using hashes here.

Provenance

The following attestation bundles were made for tree_sitter_htmlmustache-1.4.3-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl:

Publisher: publish-pypi.yml on PrairieLearn/tree-sitter-htmlmustache

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tree_sitter_htmlmustache-1.4.3-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl.

File metadata

File hashes

Hashes for tree_sitter_htmlmustache-1.4.3-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl
Algorithm Hash digest
SHA256 322b03df8639058c48568e21f9a457edfcee725707e99509253936d814b1d579
MD5 c014e63b265132d569a3d749eb6b4895
BLAKE2b-256 44b4e20e5a6e817eba33e17da122fa87bfe1040352a910d2cc35982fef76faa6

See more details on using hashes here.

Provenance

The following attestation bundles were made for tree_sitter_htmlmustache-1.4.3-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl:

Publisher: publish-pypi.yml on PrairieLearn/tree-sitter-htmlmustache

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tree_sitter_htmlmustache-1.4.3-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for tree_sitter_htmlmustache-1.4.3-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 70f85d2da1bd75e893dbb14d1c167849f05143a6ddee2bd190030bcf31024903
MD5 b5603d6fc464c1d3d595552433c2dcd1
BLAKE2b-256 5ae1e51369d8def9a98fee7335b961ada51ced76e3bba5e45fd736f3b3a7c6f4

See more details on using hashes here.

Provenance

The following attestation bundles were made for tree_sitter_htmlmustache-1.4.3-cp310-abi3-macosx_11_0_arm64.whl:

Publisher: publish-pypi.yml on PrairieLearn/tree-sitter-htmlmustache

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page