EJS Colorizer icon

EJS Colorizer

VS Code Extension for EJS Syntax Highlighting

EJS Colorizer is a free, open source Visual Studio Code extension that delivers accurate EJS syntax highlighting using the Semantic Tokens API. Where earlier TextMate-based implementations corrupt the HTML grammar's internal state when EJS tags appear inside elements, EJS Colorizer uses a stateful HTML scanner and a purpose-built JavaScript lexer to place every token at its exact document offset. </p> is always the right color.

The extension requires no configuration and works with any VS Code color theme. It registers .ejs as the host language with a named, stable scope (text.html.ejs), making it fully composable with other extensions such as Alpine.js Tools.

Why v2 Uses the Semantic Tokens API

TextMate grammars are stateless regex engines. When EJS tags appear inside HTML elements, the HTML grammar's internal state machine gets corrupted and downstream tokens such as closing tags and attribute values end up the wrong color. The only reliable fix is to step outside the TextMate system entirely.

Version 2 replaces the TextMate grammar with a full semantic token provider. A stateful HTML scanner runs over a placeholder version of the document (EJS blocks replaced with equal-length spaces so character offsets are preserved), then a purpose-built JS lexer handles the content inside each EJS block. The result is correct colorization at every nesting level, including EJS inside attribute values and EJS inside <script> elements.

Features

  • Accurate syntax highlighting

    All EJS tag types, HTML (tag names, attribute names, attribute values, comments, DOCTYPE), and JavaScript inside EJS blocks (keywords, strings, numbers, comments, operators) are colored correctly at every nesting level.

  • Outline and Document Symbols

    The Outline panel and breadcrumbs show all EJS blocks as navigable symbols: control-flow blocks, include() calls, variable declarations, and output expressions.

  • Emmet abbreviations

    Emmet expansion works inside HTML regions of EJS files. Type div.container>ul>li*3 and expand it into a full HTML structure without switching file types.

  • Prettier formatting

    Format Document (Shift+Alt+F) uses the Prettier CLI from the project's node_modules or PATH, and respects the project's .prettierrc. Use @prettier/plugin-ejs for full EJS-aware formatting.

  • Include navigation

    File-path completions inside include() list .ejs and .html files relative to the current file with directory traversal. Every include() path is a clickable document link (Ctrl/Cmd+click). Go to Definition (F12) jumps to the first line of the included file. A warning diagnostic fires when an include() path does not exist on disk. All of it is scoped to the code region inside an EJS tag, so include() written in prose, in an HTML comment, or in a <script> body is left alone — including path completion, which additionally treats a tag you are still typing as a tag, so completions appear mid-edit before the %> exists. A commented-out include() is the one deliberate disagreement: it raises no warning, but its link and F12 still work, since those only answer where the pointer already is.

  • Region-aware comment toggle

    Cmd+/ and Shift+Alt+A classify each line or span by what it actually is, because EJS has five line shapes and only one of them comments with <%# ... %>. Markup is wrapped; a whole EJS tag takes a # marker on the tag itself (<%#if (a) { %>); a markup line carrying a tag becomes a one-line dead branch (<% if (false) { %>...<% } %>), the only construct EJS offers that suppresses a line containing a tag; a line inside a scriptlet body gets a // prefix; and a line that is only a delimiter is left alone.

    The distinction matters more than it sounds. Wrapping <p><%= x %></p> in <%# ... %> produces a comment that closes at the inner %>, and under EJS 6 that throws Could not find matching close tag rather than merely leaking text — the page does not render at all. Every toggle result is now verified by compiling it with the real EJS engine, across every contiguous selection of eight representative templates, asserting the output compiles, leaks no raw delimiters, and round-trips byte for byte.

    Cmd+/ and Shift+Alt+A are bound to the region-aware commands. Routes that cannot be intercepted — the Command Palette's comment entries, and third-party keymaps such as Vim's gc, which drive VS Code's built-in command — fall back to the static tokens in language-configuration.json, which are now the dead branch. That compiles on any markup selection, where the old <%# tokens compiled only when no tag was present; but static tokens cannot tell scriptlet JavaScript from markup, so inside a <% %> body those routes still produce a dead branch rather than //.

  • JavaScript syntax diagnostics

    All scriptlet and output block contents are joined into a single synthetic program before syntax checking, matching EJS runtime behavior. Split if, for, and while constructs spanning multiple blocks are accepted correctly.

  • Folding

    Multi-line EJS blocks fold as a unit. Matching if, for, and while open-blocks ({) fold to their corresponding } close-block.

  • Completions

    EJS tag snippets trigger on < or <%, covering if, if/else, for, forEach, and include block templates.

  • Hover documentation

    Hover over any EJS delimiter to see an explanation of what it does.

EJS Tag Reference

Tag Description
<% %> Scriptlet: executes JavaScript, produces no output
<%= %> Output, HTML-escaped
<%- %> Output, unescaped (trusted content only)
<%# %> Server-side comment: not executed, not sent to browser
<%_ %> Whitespace-slurping scriptlet
-%> Trims the trailing newline
_%> Trims all trailing whitespace

Settings

The extension is zero-config: each setting defaults to what that check already reported at, so an install that never opens settings.json behaves identically to one that does.

SettingDefault
ejsColorizer.diagnostics.jsSyntax.severityerror
ejsColorizer.diagnostics.missingInclude.severitywarning
ejsColorizer.diagnostics.brokenComment.severitywarning

Only the diagnostics are configurable, and deliberately so: highlighting, folding, outline, hover, completions, and include navigation are either cursor-triggered or purely presentational, and none of them accumulates anywhere you have to dismiss it. Diagnostics land in the Problems panel and stay there.

Severities are enums rather than on/off switches, because the value worth having is hint — the underline stays in the editor while the entry stays out of the Problems panel, which is the middle ground between living with a false positive and losing the check entirely. The three checks are configured separately because they are wrong in unrelated ways: the JavaScript syntax check is the only one reporting at Error severity and its false positives are structural, since a template whose blocks legitimately don't form a standalone parse gets a squiggle no amount of correct EJS will clear. Include navigation is unaffected by missingInclude.severity — document links and F12 keep working with it set to off.

Compatibility with Alpine.js Tools

EJS Colorizer registers text.html.ejs as a named, stable TextMate host scope. Alpine.js Tools targets that scope name for its JavaScript injection grammar, so both extensions can be active simultaneously in the same .ejs file with no tokenization conflicts. Alpine directive values receive full JavaScript highlighting from Alpine.js Tools on top of the EJS structure provided by EJS Colorizer.

Requirements

  • VS Code 1.116.0 or later
  • No other dependencies. vscode-html-languageservice is bundled.
  • No configuration required

Installation

Search for EJS Colorizer in the VS Code Extensions panel, or install directly from the Marketplace. Source code is available on GitHub under the MIT license.

Related Extensions