A Neovim plugin that lets you edit Meteor BlazeJS HTML templates using a concise, readable syntax called not_html_blaze. When you open an .html file, it is automatically decompiled into not_html_blaze syntax -- including Spacebars expressions like {{#if}}, {{> template}}, and {{helper}}. When you save, it is transparently compiled back to HTML with Spacebars. The underlying .html file is never changed to a different format -- not_html_blaze is purely a visual editing layer.
HTML with Spacebars on disk:
<template name="taskList">
{{! Task list component}}
{{#if hasPermission}}
<h2>{{title}}</h2>
{{#each task in tasks}}
<div class="task {{#if task.done}}completed{{/if}}">
<span>{{task.name}}</span>
{{> taskActions taskId=task._id canEdit=true}}
</div>
{{else}}
<p>No tasks found.</p>
{{/each}}
{{else}}
<p>Access denied.</p>
{{/if}}
</template>What you see and edit in Neovim:
~template(name: taskList) {
$! { Task list component }
#if (hasPermission) {
~h2 { ${title} }
#each (task in tasks) {
~div(class: task ${#if (task.done) { completed }}) {
~span { ${task.name} }
~>taskActions (taskId: task._id canEdit: true)
}
} #else {
~p { No tasks found. }
}
} #else {
~p { Access denied. }
}
}
- Transparent roundtrip -- open HTML with Spacebars, edit as
not_html_blaze, save back. No data loss. - Full BlazeJS support -- helpers, block helpers (
#if,#each,#with,#let), template inclusions, dynamic attributes, and Spacebars comments. - Preserves structure -- DOCTYPE declarations, HTML comments, Spacebars comments, blank lines, and line breaks all survive the roundtrip.
- Syntax highlighting -- elements, attributes, Blaze helpers, block helpers, template inclusions, and escapes are all highlighted.
- Zero dependencies -- pure Lua, no external tools or binaries required.
- Toggle on/off -- disable the plugin with
:NotHtmlBlazeToggleto edit raw HTML when needed.
{
"tymnim/not-html-blazejs.vim",
config = function()
require("not_html_blaze").setup()
end,
}Plug 'tymnim/not-html-blazejs.vim'Then in your init.lua:
require("not_html_blaze").setup()Or in your init.vim:
lua require("not_html_blaze").setup()use {
"tymnim/not-html-blazejs.vim",
config = function()
require("not_html_blaze").setup()
end,
}Clone the repo and add it to your runtime path:
vim.opt.runtimepath:prepend("~/path/to/not-html-blazejs.vim")
require("not_html_blaze").setup()Call setup() to activate the plugin. It hooks into BufReadCmd and BufWriteCmd for *.html and *.htm files.
require("not_html_blaze").setup({
enabled = true, -- set to false to start disabled
})| Command | Description |
|---|---|
:NotHtmlBlazeToggle |
Toggle the plugin on/off. When off, HTML files open and save as raw HTML. |
:NotHtmlBlazeCompile |
Convert the current buffer from not_html_blaze to HTML (in-place). |
:NotHtmlBlazeDecompile |
Convert the current buffer from HTML to not_html_blaze (in-place). |
Every HTML element becomes ~tagname. Attributes go in (), children go in {}.
~div → <div></div>
~img(src: photo.jpg) → <img src="photo.jpg"/>
~p { Hello } → <p>Hello</p>
~a(href: /, class: nav-link) { Home } → <a href="/" class="nav-link">Home</a>
Key-value pairs separated by :, multiple attributes separated by ,. Values are unquoted and run until the next , or ).
~meta(charset: utf-8)
~div(class: container main, id: app)
Boolean attributes are bare names:
~script(defer, src: app.js)
~input(disabled)
Spacebars {{expression}} becomes ${expression}. Keyword args use : instead of =.
${username} → {{username}}
${formatDate createdAt format: short} → {{formatDate createdAt format=short}}
Helpers can appear in text and attribute values, including concatenated directly with text (no space):
~p { Hello ${username} } → <p>Hello {{username}}</p>
~div(class: item ${activeClass}) → <div class="item {{activeClass}}">
~div(id: tabs-${randomId}) → <div id="tabs-{{randomId}}">
~p { prefix-${name}-suffix } → <p>prefix-{{name}}-suffix</p>
Spacebars {{{expression}}} (unescaped HTML) becomes ~${expression}:
~${rawContent} → {{{rawContent}}}
~${toHtmlString (helper arg)} → {{{toHtmlString (helper arg)}}}
Spacebars block helpers use # prefix. Args in (), body in {}.
#if / #elseif / #else:
#if (isAdmin) {
~p { Admin panel }
} #elseif (isMod) {
~p { Moderator view }
} #else {
~p { Standard view }
}
#each:
#each (item in items) {
~li { ${item.name} }
} #else {
~li { No items found }
}
#with:
#with (currentUser) {
~span { ${name} }
}
#let:
#let (name: person.bio.firstName age: person.bio.age) {
~p { ${name} is ${age} years old }
}
Generic blocks:
#myBlock (args) {
content
}
Blocks in attribute values (including concatenated with text):
~a(class: #if (active) { on } #else { off })
→ <a class="{{#if active}}on{{else}}off{{/if}}">
~div(class: container-#if (show) { visible } #else { hidden })
→ <div class="container-{{#if show}}visible{{else}}hidden{{/if}}">
Spacebars {{> template args}} becomes ~>template (args):
~>myTemplate → {{> myTemplate}}
~>Template.contentBlock → {{> Template.contentBlock}}
~>userCard (name: (nameHelper person) timestamp: time)
→ {{> userCard name=(nameHelper person) timestamp=time}}
Spacebars dynamic attributes <div {{attrs}}> become $-prefixed boolean attrs:
~div($attrs) → <div {{attrs}}>
~div(class: container, $dynamicAttrs) → <div class="container" {{dynamicAttrs}}>
HTML comments use ~!, Spacebars comments use $!:
~! { TODO\: fix this } → <!-- TODO: fix this -->
$! { Template-only note } → {{! Template-only note }}
~!doctype(html) → <!DOCTYPE html>
Use \ to escape breaking characters (~ # { } ( ) : ,) and special sequences (${, $!) in text or attribute values:
~div(style: height\: 100px)
~p { Price is \~5 }
~p { \#not a block helper }
~p { \${not a helper} }
nvim -l test/test_lua.luaSee spec.md for the complete language specification, grammar, processing pipeline, and decompilation rules.