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*3and 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_modulesor PATH, and respects the project's.prettierrc. Use@prettier/plugin-ejsfor full EJS-aware formatting. -
Include navigation
File-path completions inside
include()list.ejsand.htmlfiles relative to the current file with directory traversal. Everyinclude()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 aninclude()path does not exist on disk. All of it is scoped to the code region inside an EJS tag, soinclude()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-outinclude()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 throwsCould not find matching close tagrather 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 inlanguage-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, andwhileconstructs spanning multiple blocks are accepted correctly. -
Folding
Multi-line EJS blocks fold as a unit. Matching
if,for, andwhileopen-blocks ({) fold to their corresponding}close-block. -
Completions
EJS tag snippets trigger on
<or<%, coveringif,if/else,for,forEach, andincludeblock 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.
| Setting | Default |
|---|---|
ejsColorizer.diagnostics.jsSyntax.severity | error |
ejsColorizer.diagnostics.missingInclude.severity | warning |
ejsColorizer.diagnostics.brokenComment.severity | warning |
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-languageserviceis 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.