HTML with Mustache/Handlebars template syntax grammar for tree-sitter
Project description
HTML Mustache
Full language support for HTML with Mustache/Handlebars templates
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 |
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 '@reteps/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 '@reteps/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 '@reteps/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/reteps/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 '@reteps/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 '@reteps/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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tree_sitter_htmlmustache-1.4.1.tar.gz.
File metadata
- Download URL: tree_sitter_htmlmustache-1.4.1.tar.gz
- Upload date:
- Size: 72.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3ab96a2a76bf69d8d13877489d5e13271a4d11f6837983e6e9564fe6f540c11e
|
|
| MD5 |
e0c24d55780854174cfa6d20a575f64e
|
|
| BLAKE2b-256 |
5c7d563ccc9676ebf1bf6ef38b56096d2106cd1f9b0a7e9433435bc2c77080e2
|
Provenance
The following attestation bundles were made for tree_sitter_htmlmustache-1.4.1.tar.gz:
Publisher:
publish-pypi.yml on reteps/tree-sitter-htmlmustache
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tree_sitter_htmlmustache-1.4.1.tar.gz -
Subject digest:
3ab96a2a76bf69d8d13877489d5e13271a4d11f6837983e6e9564fe6f540c11e - Sigstore transparency entry: 1624482718
- Sigstore integration time:
-
Permalink:
reteps/tree-sitter-htmlmustache@79f31b2d59bd168cf165db7bb575e37e22c0b152 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/reteps
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@79f31b2d59bd168cf165db7bb575e37e22c0b152 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file tree_sitter_htmlmustache-1.4.1-cp310-abi3-win_amd64.whl.
File metadata
- Download URL: tree_sitter_htmlmustache-1.4.1-cp310-abi3-win_amd64.whl
- Upload date:
- Size: 37.9 kB
- Tags: CPython 3.10+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ffcd0bb85d9b5cb9960b35e0389d56691255f8a4205f1a789722e5450c36c5cb
|
|
| MD5 |
53a7ba65c6bf9599a9f37e61b3a977a7
|
|
| BLAKE2b-256 |
f36b5abd58af3d5106cb6cf009660ebf868ffdf18832b29389c46d3fbdc2f246
|
Provenance
The following attestation bundles were made for tree_sitter_htmlmustache-1.4.1-cp310-abi3-win_amd64.whl:
Publisher:
publish-pypi.yml on reteps/tree-sitter-htmlmustache
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tree_sitter_htmlmustache-1.4.1-cp310-abi3-win_amd64.whl -
Subject digest:
ffcd0bb85d9b5cb9960b35e0389d56691255f8a4205f1a789722e5450c36c5cb - Sigstore transparency entry: 1624482877
- Sigstore integration time:
-
Permalink:
reteps/tree-sitter-htmlmustache@79f31b2d59bd168cf165db7bb575e37e22c0b152 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/reteps
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@79f31b2d59bd168cf165db7bb575e37e22c0b152 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file tree_sitter_htmlmustache-1.4.1-cp310-abi3-musllinux_1_2_x86_64.whl.
File metadata
- Download URL: tree_sitter_htmlmustache-1.4.1-cp310-abi3-musllinux_1_2_x86_64.whl
- Upload date:
- Size: 69.0 kB
- Tags: CPython 3.10+, musllinux: musl 1.2+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
60d5b387c7a0d929817a97bfb35b09ab5d1a1dfd52b237e6a3357195ba177f3b
|
|
| MD5 |
430183d76d724c586c37874a4e40693f
|
|
| BLAKE2b-256 |
95e52806d5f0e48a2390b5fc8897549be2df65022568acb6e0d4ec52fcf47d3b
|
Provenance
The following attestation bundles were made for tree_sitter_htmlmustache-1.4.1-cp310-abi3-musllinux_1_2_x86_64.whl:
Publisher:
publish-pypi.yml on reteps/tree-sitter-htmlmustache
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tree_sitter_htmlmustache-1.4.1-cp310-abi3-musllinux_1_2_x86_64.whl -
Subject digest:
60d5b387c7a0d929817a97bfb35b09ab5d1a1dfd52b237e6a3357195ba177f3b - Sigstore transparency entry: 1624482954
- Sigstore integration time:
-
Permalink:
reteps/tree-sitter-htmlmustache@79f31b2d59bd168cf165db7bb575e37e22c0b152 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/reteps
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@79f31b2d59bd168cf165db7bb575e37e22c0b152 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file tree_sitter_htmlmustache-1.4.1-cp310-abi3-musllinux_1_2_aarch64.whl.
File metadata
- Download URL: tree_sitter_htmlmustache-1.4.1-cp310-abi3-musllinux_1_2_aarch64.whl
- Upload date:
- Size: 68.8 kB
- Tags: CPython 3.10+, musllinux: musl 1.2+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
86fba2497d2b07cc965d7666a511d3ec195e0be7280c9562e2dea21a0c42670f
|
|
| MD5 |
ffee8282f84168c1de813c77d72dc6b1
|
|
| BLAKE2b-256 |
28e3ba4f772d6c1e2717c0d38de93c00981ae6e6c84660cc26f46568c5a6e0c5
|
Provenance
The following attestation bundles were made for tree_sitter_htmlmustache-1.4.1-cp310-abi3-musllinux_1_2_aarch64.whl:
Publisher:
publish-pypi.yml on reteps/tree-sitter-htmlmustache
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tree_sitter_htmlmustache-1.4.1-cp310-abi3-musllinux_1_2_aarch64.whl -
Subject digest:
86fba2497d2b07cc965d7666a511d3ec195e0be7280c9562e2dea21a0c42670f - Sigstore transparency entry: 1624482839
- Sigstore integration time:
-
Permalink:
reteps/tree-sitter-htmlmustache@79f31b2d59bd168cf165db7bb575e37e22c0b152 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/reteps
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@79f31b2d59bd168cf165db7bb575e37e22c0b152 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file tree_sitter_htmlmustache-1.4.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.
File metadata
- Download URL: tree_sitter_htmlmustache-1.4.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
- Upload date:
- Size: 70.8 kB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ ARM64, manylinux: glibc 2.28+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9821af209649bc25c6d00a317fa2c5590e5fc56e2f42a27376fd1f7a4bb8a2ca
|
|
| MD5 |
1ccf6b83115838775bd6964564f3b64f
|
|
| BLAKE2b-256 |
d33c34caa631d1391df53bb57997170f0056f99e1accb7f1ace13f22278830f8
|
Provenance
The following attestation bundles were made for tree_sitter_htmlmustache-1.4.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl:
Publisher:
publish-pypi.yml on reteps/tree-sitter-htmlmustache
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tree_sitter_htmlmustache-1.4.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl -
Subject digest:
9821af209649bc25c6d00a317fa2c5590e5fc56e2f42a27376fd1f7a4bb8a2ca - Sigstore transparency entry: 1624482805
- Sigstore integration time:
-
Permalink:
reteps/tree-sitter-htmlmustache@79f31b2d59bd168cf165db7bb575e37e22c0b152 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/reteps
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@79f31b2d59bd168cf165db7bb575e37e22c0b152 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file tree_sitter_htmlmustache-1.4.1-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl.
File metadata
- Download URL: tree_sitter_htmlmustache-1.4.1-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl
- Upload date:
- Size: 69.9 kB
- Tags: CPython 3.10+, manylinux: glibc 2.28+ x86-64, manylinux: glibc 2.5+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee6864992c44b665a4d28b685abf7e387ae5e449b724f9c89deee90bd35f5632
|
|
| MD5 |
ec19fe3b5f9207b525cdc697da958588
|
|
| BLAKE2b-256 |
41d1609db097da660cb621d6e6bfcaff9a4654f31beb8a477d97f9e329f37e68
|
Provenance
The following attestation bundles were made for tree_sitter_htmlmustache-1.4.1-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl:
Publisher:
publish-pypi.yml on reteps/tree-sitter-htmlmustache
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tree_sitter_htmlmustache-1.4.1-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl -
Subject digest:
ee6864992c44b665a4d28b685abf7e387ae5e449b724f9c89deee90bd35f5632 - Sigstore transparency entry: 1624482772
- Sigstore integration time:
-
Permalink:
reteps/tree-sitter-htmlmustache@79f31b2d59bd168cf165db7bb575e37e22c0b152 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/reteps
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@79f31b2d59bd168cf165db7bb575e37e22c0b152 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file tree_sitter_htmlmustache-1.4.1-cp310-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: tree_sitter_htmlmustache-1.4.1-cp310-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 35.5 kB
- Tags: CPython 3.10+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8f42f724d150d784d08605769735d0012917883c23074ec69f9faeaf0ff3359
|
|
| MD5 |
606a227d807f4e8bad302a763fbba142
|
|
| BLAKE2b-256 |
c22478fd40545cc76a168e5da40ec52207ff29269cdcbb5797ef07d08fba42a8
|
Provenance
The following attestation bundles were made for tree_sitter_htmlmustache-1.4.1-cp310-abi3-macosx_11_0_arm64.whl:
Publisher:
publish-pypi.yml on reteps/tree-sitter-htmlmustache
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tree_sitter_htmlmustache-1.4.1-cp310-abi3-macosx_11_0_arm64.whl -
Subject digest:
c8f42f724d150d784d08605769735d0012917883c23074ec69f9faeaf0ff3359 - Sigstore transparency entry: 1624482916
- Sigstore integration time:
-
Permalink:
reteps/tree-sitter-htmlmustache@79f31b2d59bd168cf165db7bb575e37e22c0b152 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/reteps
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@79f31b2d59bd168cf165db7bb575e37e22c0b152 -
Trigger Event:
workflow_dispatch
-
Statement type: