Skip to content

Releases: PrairieLearn/tree-sitter-htmlmustache

v1.3.0

Choose a tag to compare

@reteps reteps released this 15 May 04:10
bf91dbf

What's Changed

  • Add recursive parent-owned child schemas for customTags.
  • Support strict and loose direct-child validation with Mustache sections treated as transparent.
  • Allow child-only tags to stay scoped to declaring parents unless listed top-level.
  • Document child schema configuration and diagnostics.

v1.2.0

Choose a tag to compare

@reteps reteps released this 14 May 22:07
c789ff0

API changes

  • Added tag validator ergonomics for project-level custom validation.
  • Updated validator docs and examples for the new API shape.

Parser fixes

  • Improved literal < handling in text so less-than characters before real tags parse cleanly.
  • Added corpus and linter coverage for the less-than parsing cases.

Release maintenance

  • Bumped the parser package and VS Code extension to 1.2.0.
  • Refreshed package-lock.json so the npm lock metadata matches the current package API surface.

Checks

  • pnpm run lint
  • pnpm run format:check
  • pnpm run typecheck
  • pnpm run test:js
  • pnpm run build:js
  • pnpm --prefix lsp run typecheck
  • pnpm --prefix lsp run test
  • pnpm --prefix lsp run build:production
  • pnpm test

v1.1.0

Choose a tag to compare

@reteps reteps released this 14 May 20:57
5943d61

Custom tag validators

Custom tag validation now separates attribute schemas from richer project rules:

  • Tag schemas are draft-06 JSON Schemas over the flat attribute object only.
  • pluginModule replaces ajvModule and can export formats and synchronous validators.
  • Validator ids behave like lint rule ids, including severity overrides and inline disables.
  • Validators receive one-level child facades with mustache-section flattening and optional raw innerHtml.

Breaking validation changes

  • Removed draft 2020-12 schema support.
  • Removed the element-envelope schema shape (tag, attributes, children, text, innerHtml).
  • Removed custom AJV keyword registration and htmlGlobalAttributes expansion.
  • Schema deprecation diagnostics now apply to flat attribute schemas only.

Cleanup

  • Removed unused aria-attributes and html-element-attributes dependencies.

Checks

  • pnpm run lint
  • pnpm run format:check
  • pnpm run typecheck
  • pnpm run test:js
  • pnpm run build:js
  • pnpm --prefix lsp run typecheck
  • pnpm --prefix lsp run test
  • pnpm --prefix lsp run build:production
  • pnpm test

v1.0.4

Choose a tag to compare

@reteps reteps released this 13 May 23:57
3353cda

customTagDeprecations lint rule

JSON Schema's deprecated: true is an annotation, not a validator — AJV silently ignores it. A new lint rule (default severity warning) walks each custom-tag element + its registered schema in parallel and surfaces deprecations at four levels, folding any sibling description into the diagnostic as the reason:

  • Tag<pl-legacy> is deprecated. Use <pl-question-panel> instead.
  • AttributeAttribute "old-name" on <pl-card> is deprecated. Renamed to "answers-name".
  • Attribute valueValue "legacy" for attribute "kind" on <pl-card> is deprecated. Use "new". (matched via oneOf/anyOf const branches; skipped for mustache-interpolated values)
  • Child tag in context<pl-answer-old> as a child of <pl-choice> is deprecated. (matched via properties.children.items branches keyed by properties.tag.const)

Structural deprecation (tag, attribute) walks only unconditional composition (root + allOf) — branches inside anyOf/oneOf/if describe alternate value validations and don't structurally deprecate the attribute they live on.

Disable per-element via the existing infrastructure: <!-- htmlmustache-disable customTagDeprecations -->.

Schema diagnostics: collapse composition-wrapper noise

With AJV's allErrors: true, a single bad attribute like correct="trsue" against an anyOf: [boolean, string-with-format-pl-boolean] schema used to produce three diagnostics — the boolean-branch type failure plus two raw AJV strings without attribute context. They now collapse to one:

Attribute "correct" on <pl-answer> child of <pl-multiple-choice> must be boolean or match format "pl-boolean".

Errors are grouped by instancePath and dispatched on the wrapper keyword: anyOf/oneOf join their translated branch phrases with "or"; if/allOf wrappers are dropped (pure bookkeeping over their branches); not translates generically; an ajv-errors errorMessage at any path overrides the merged result.

format, pattern, minLength, maxLength, exclusiveMinimum, and exclusiveMaximum are now translated with attribute context too.

LSP: schema-aware completion

Inside any custom-tag element the language server now offers, driven by the tag's registered schema:

  • Attribute names — every property the schema allows (across allOf/anyOf/oneOf and if/then branches), excluding attributes already present on the element. Required attributes are flagged.
  • Attribute values — concrete candidates from enum, const, examples, default, and type: "boolean" declarations across all branches. When no concrete values are available, surfaces the declared format name as a hint.

No new config surface — drives directly off the existing customTags[*].schema.

v1.0.2

Choose a tag to compare

@reteps reteps released this 13 May 21:11
8d25bc6

Schema validator extensions

New envelope fields (text, innerHtml)

The per-element value validated against custom-tag schemas now exposes two new fields, on both the top-level element and each child:

  • text — concatenated descendant text. HTML tags stripped (<b>foo</b> contributes "foo"); mustache interpolations preserved verbatim ({{name}} stays in the string). Surrounding whitespace trimmed; interior whitespace preserved.
  • innerHtml — raw source between the open and close tags, byte-for-byte. Empty for self-closing or void elements.

This makes content-shaped rules expressible directly in the schema — non-empty content via minLength, no duplicate inner text across children via uniqueItems, regex/length checks over the raw markup — without dropping back to selector-based custom rules or consumer-side post-processing.

createLinter({ keywords })

Register consumer-defined ajv keywords on the schema validator. Thin pass-through to ajv.addKeyword — the key becomes the keyword name. The diagnostic rewriter detects custom-keyword errors and passes the consumer's message through unchanged (same path as ajv-errors); when no message is set, falls back to a generic <tag>: validation <keyword> failed on <path>.

Config-file surface: formatsModuleajvModule

Breaking change. The .htmlmustache.jsonc formatsModule field is renamed to ajvModule and now reads two named exports: formats and keywords. The v1.0.1 default-export-as-formats shim is gone — modules must use named exports:

// before (v1.0.1)
export default { 'pl-boolean': /* ... */ };

// after (v1.0.2)
export const formats = { 'pl-boolean': /* ... */ };
export const keywords = { 'unique-child-text': /* ... */ };

SchemaKeyword and SchemaFormat are re-exported from ./linter so consumers can type their definitions without reaching into ajv internals.

v1.0.1

Choose a tag to compare

@reteps reteps released this 13 May 20:34
01327bf

Schema formats

Two new ways to register ajv formats so custom-tag schemas can use { "format": "..." } value rules:

  • createLinter({ formats }) — programmatic hook, passed through to loadSchemaRegistry and ajv.addFormat.
  • formatsModule in .htmlmustache.jsonc — relative path to a JS/TS module whose default export is Record<string, SchemaFormat>. The CLI and the language server dynamically import it once per process. Load failures surface as customTagSchema diagnostics.

loadConfigFile and loadConfigFileForPath are now async (the dynamic import happens during config load).

Documentation

  • Worked example in the README showing how to express parent-conditional child-attribute constraints in plain JSON Schema 2020-12 (no DSL extensions). Solves the recurring "when parent has attr X, child of tag Y must not have attr Z" pattern.
  • Notes on the trust boundary formatsModule introduces (the CLI / extension runs that file in-process).

Internal

  • ADR directory removed.

v1.0.0

Choose a tag to compare

@reteps reteps released this 13 May 20:01
6b7cd49

First stable release. Headline changes since v0.9.3:

Breaking — package layout

The JS source moved to js/ and the package now exposes three dedicated
entry points instead of a single browser export:

Old New
@reteps/tree-sitter-htmlmustache/browser @reteps/tree-sitter-htmlmustache/parser, /linter, /formatter
createLinter().format(...) createFormatter().format(...)
cli/out/main.js dist/cli/main.js (bin shim unaffected)

Rationale and migration notes: docs/adr/0001-restructure-js-entry-points.md.

New — custom-tag attribute validation

Custom tags can now declare attributes and types via JSON Schema. The
linter surfaces unknown/missing/typed-mismatch attributes as HTML-shaped
diagnostics (unknown-attribute, missing-required-attribute, etc.),
with optional per-rule custom messages. See the linter docs for the
customTags config key.

Improved — global attribute list

The list of HTML global attributes is now sourced from
html-element-attributes + aria-attributes instead of a hand-maintained
list, so ARIA + standard global attributes stay current with upstream.

Other

  • Docs corrected: htmlGlobalAttributes example, customTags key, pnpm
    in the LSP README.
  • Repo-wide prettier + eslint pass over the new js/ layout.

🤖 Generated with Claude Code

v0.9.3

Choose a tag to compare

@reteps reteps released this 23 Apr 14:39
17b86c4

Fix: multi-segment :not() in custom lint rules

:not(...) with a combinator inside it was silently dropped by the selector engine. Given a rule like:

{
  "id": "require-img-class-or-style",
  "selector": "img:not([style]):not([class]):not(pl-overlay img)",
  "message": "Annotate <img> or keep it under <pl-overlay>."
}

the third negation (:not(pl-overlay img)) was ignored because checkSelfNegations bailed on any alternative with more than one segment. Only simple compound selectors on the element itself worked inside :not().

What works now

:not(X) accepts any multi-segment selector, tested against the node's ancestor/sibling context (matching Element.matches semantics):

  • img:not(pl-overlay img) — descendant
  • td:not(thead > td) — direct child
  • p:not(h2 + p) — adjacent sibling
  • button:not({{#admin}} button) — crosses into a Mustache section
  • section:has(img:not(pl-overlay img)) — composes inside :has()

Simple forms (:not(div), :not([attr]), :not(.cls), :not(:has(...))) are unchanged.

Scope

Affects the selector matcher shared by the CLI (htmlmustache check), the LSP/VS Code extension, and the browser API.

v0.9.2

Choose a tag to compare

@reteps reteps released this 23 Apr 04:01
33a2062

Per-rule include/exclude for custom lint rules

Custom rules now accept optional `include` and `exclude` glob arrays, applied as an intersection on top of the top-level `include`/`exclude`. A rule fires for a file only when both the top-level filter admits it AND the per-rule patterns match. `exclude` takes precedence over `include`.

```jsonc
{
"include": ["/*.mustache"],
"customRules": [
{
"id": "pl-input-in-panel",
"include": ["questions/
"],
"exclude": ["/legacy/"],
"selector": ":is(pl-question-panel, pl-answer-panel) :is(pl-string-input, pl-integer-input)",
"message": "Move input elements outside panel tags."
}
]
}
```

Paths are matched relative to the `.htmlmustache.jsonc` directory, with separators normalized to forward slashes for cross-platform pattern portability.

Scope

  • CLI (`htmlmustache check`): filter applied per file.
  • LSP / VS Code extension: filter applied per document URI.
  • Browser API (`createLinter().lint`): `include`/`exclude` are stripped from the browser `CustomRule` type because the browser has no filesystem path context. TypeScript will reject the fields at the browser API boundary — strip them before passing a shared config.

v0.9.1

Choose a tag to compare

@reteps reteps released this 23 Apr 03:42
e5742e6

New selector features for custom lint rules

:is(a, b, ...) grouping

Expanded at parse time into the Cartesian product of alternatives. Cuts selector boilerplate dramatically:

":is(pl-question-panel, pl-answer-panel, pl-submission-panel) :is(pl-big-o-input, pl-integer-input, pl-string-input)"

Sibling combinators + and ~

Work across HTML and Mustache constructs, skipping whitespace/text per CSS semantics:

  • label + input — adjacent HTML siblings
  • {{foo}} + p — Mustache interpolation followed by an element
  • {{#items}} + {{#other}} — adjacent Mustache sections
  • h2 ~ {{foo}} — any later sibling of <h2> that is {{foo}}

:not(...) accepts Mustache literals and type selectors

Enables whole-node negation against the current segment:

{{*}}:not({{internal.*}})  — any interpolation except internal.*
:not(div)                  — any element that isn't a div

Attribute / class / id / :has inside :not(...) still take the existing fast path.