Text JSON (TJSON) for VS Code / VSCodium
Syntax highlighting, error checking, and JSON ↔ TJSON conversion for .tjson files.
https://github.com/rfanth/tjson-highlight
Learn more about TJSON at textjson.com.
Install - VSCode, VSCodium
This has full coverage for everything in TJSON within the TextMate grammar file.
Copy this folder to your extensions directory:
VSCodium:
~/.vscode-oss/extensions/tjson-highlight/
VS Code:
~/.vscode/extensions/tjson-highlight/
Then reload the window: Ctrl+Shift+P → Developer: Reload Window.
Disabling and reenabling this extension also seems to have a similar effect if you don't want to reload everything.
Converting between JSON and TJSON
Three commands, on the editor title bar and in the command palette:
| command |
on |
gives |
| Preview as TJSON |
a .json file |
that JSON rendered as TJSON |
| Preview as JSON |
a .tjson file |
the data as plain JSON |
| Preview Reformatted with My Render Settings |
a .tjson file |
the same document re-rendered your way |
A preview opens beside the source, is read-only, and re-renders as you edit the
original. It is a real editor pane rather than a webview, so it is highlighted by
this extension's own grammar in your own theme, and it is checked by this
extension's own error reporting — if a preview ever shows a squiggle, the
renderer produced something the parser will not accept.
Open Preview as an Editable File turns the pane you are looking at into an
ordinary untitled document you can edit and save.
Reformatting goes through JSON, and JSON has no comments, so comments do not
survive it. The command warns first when the file actually has any.
Render settings
Every option the renderer takes is available as tjson.render.* — wrap width,
multiline style, whether to use tables and the thresholds for them, string and
key quoting, fold styles, and the rest.
Anything you do not set is left alone. Unset settings are not sent to the
renderer at all, so the library's own default applies — including after an
upgrade that changes one. This extension never writes a default into your
settings, and never passes one on your behalf, which is why every setting shows
null until you pick something.
The settings are generated from the bundled library's own type definitions, so
their names, their dropdown values and their documentation always describe the
parser actually shipped rather than the one that shipped when this was written.
Customizing Colors
This extension only adds the grammar — it does not override your color theme.
Most themes will highlight TJSON reasonably well out of the box.
If you want to fine-tune the colors (or reproduce the palette from
textjson.com), paste the block below into your
settings.json (Ctrl+Shift+P → Preferences: Open User Settings (JSON)).
The [tjson] wrapper limits every rule to .tjson files only, so your
theme is untouched everywhere else.
"[tjson]": {
"workbench.colorCustomizations": {
"editor.background": "#11131d"
},
"editor.tokenColorCustomizations": {
"textMateRules": [
// ── Comments ──────────────────────────────────────────────────────
{
"scope": "comment.line.double-slash.tjson",
"settings": { "foreground": "#45475a", "fontStyle": "italic" }
},
// ── Keys ──────────────────────────────────────────────────────────
{
"scope": "entity.other.attribute-name.bare.tjson",
"settings": { "foreground": "#89b4fa" }
},
{
"scope": "entity.other.attribute-name.quoted.tjson",
"settings": { "foreground": "#89b4fa" }
},
// ── String values ─────────────────────────────────────────────────
{
"scope": "string.unquoted.bare.tjson",
"settings": { "foreground": "#a6e3a1" }
},
// The single space that opens a bare string — a one-sided quote.
// It is the entire type marker: `k:true` is the boolean, `k: true`
// is the string "true". Dim by default like any other delimiter;
// see "Making the bare string marker more visible" below to light it up.
{
"scope": "punctuation.definition.string.begin.bare.tjson",
"settings": { "foreground": "#6c7086" }
},
{
"scope": "string.unquoted.fold-continuation.tjson",
"settings": { "foreground": "#a6e3a1" }
},
{
"scope": "string.quoted.double.tjson",
"settings": { "foreground": "#f9e2af" }
},
{
"scope": "markup.raw.block.tjson",
"settings": { "foreground": "#f9e2af" }
},
// ── Numbers ───────────────────────────────────────────────────────
{
"scope": "constant.numeric.tjson",
"settings": { "foreground": "#fab387" }
},
// ── Booleans & null ───────────────────────────────────────────────
{
"scope": "constant.language.boolean.tjson",
"settings": { "foreground": "#cba6f7" }
},
{
"scope": "constant.language.null.tjson",
"settings": { "foreground": "#9399b2" }
},
// ── Escape sequences ──────────────────────────────────────────────
{
"scope": "constant.character.escape.tjson",
"settings": { "foreground": "#89dceb" }
},
// ── Structural markers ([ , { , /< , /> , / fold) ───────────────
{
"scope": "keyword.operator.array-marker.tjson",
"settings": { "foreground": "#74c7ec" }
},
{
"scope": "keyword.operator.object-marker.tjson",
"settings": { "foreground": "#74c7ec" }
},
{
"scope": "keyword.control.indent-adjustment.tjson",
"settings": { "foreground": "#74c7ec" }
},
{
"scope": "punctuation.definition.fold-marker.tjson",
"settings": { "foreground": "#74c7ec" }
},
{
"scope": "punctuation.definition.string.multiline-margin.tjson",
"settings": { "foreground": "#74c7ec" }
},
// ── Punctuation ───────────────────────────────────────────────────
{
"scope": "punctuation.separator.key-value.tjson",
"settings": { "foreground": "#6c7086" }
},
{
"scope": "punctuation.definition.string.begin.tjson",
"settings": { "foreground": "#6c7086" }
},
{
"scope": "punctuation.definition.string.end.tjson",
"settings": { "foreground": "#6c7086" }
},
{
"scope": "punctuation.definition.array.begin.tjson",
"settings": { "foreground": "#6c7086" }
},
{
"scope": "punctuation.definition.array.end.tjson",
"settings": { "foreground": "#6c7086" }
},
{
"scope": "punctuation.definition.dictionary.begin.tjson",
"settings": { "foreground": "#6c7086" }
},
{
"scope": "punctuation.definition.dictionary.end.tjson",
"settings": { "foreground": "#6c7086" }
},
{
"scope": "punctuation.section.embedded.tjson",
"settings": { "foreground": "#6c7086" }
},
{
"scope": "punctuation.whitespace.indent.tjson",
"settings": { "foreground": "#6c7086" }
},
// ── Separators ────────────────────────────────────────────────────
// The gap between inline-packed pairs.
{
"scope": "punctuation.separator.inline.tjson",
"settings": { "foreground": "#6c7086" }
},
// Between elements of a packed array.
{
"scope": "punctuation.separator.array.tjson",
"settings": { "foreground": "#6c7086" }
},
// Commas inside MINIMAL JSON. Named for what is known: whether one
// separates array elements or object pairs depends on the enclosing
// bracket, which the grammar rule cannot see from where it matches.
{
"scope": "punctuation.separator.embedded.tjson",
"settings": { "foreground": "#6c7086" }
},
// ── Table pipes ───────────────────────────────────────────────────
{
"scope": "punctuation.separator.table.tjson",
"settings": { "foreground": "#6c7086" }
}
]
}
}
These colors are based on the textjson.com demo
palette (a dark Catppuccin Mocha-inspired scheme). Swap any hex value to
taste — all rules are scoped to .tjson so nothing else changes.
Making the bare string marker more visible
In TJSON, location is king. It shows you where something is in the data, and
what type it is. There are indent markers and bare space markers that you can
print or not, but the space tells you the meaning, not the thing that may or
may not be printed there. If you don't understand this, it reads fine as text,
but if you do, you can see the type information too without being overwhelmed
by visual noise. The space is never invisible, you can always see the gap, and
that's the point. That being said, sometimes making the space more obvious is
useful.
In TJSON an extra leading space distinguishes between a bare string and other
values — active:true is the boolean, active: true is the string "true".
That distinction is load-bearing and easy to spot with the naked eye once you
know what to look for. However, sometimes it's nice to see the space as a
character, either to help you understand what the spaces mean, or because you
are doing something that makes types extra important and justifies the
visual noise. Because space comes first, when we put something in the space
to mark it, we simply replace the space, we never actually move any text.
This can be done on the screen by using the highlighter, or in the text by
using the format itself.
Using the highlighter
If you would rather not change the document, but still want to make the marker
jump off the page, the grammar scopes the opener as
punctuation.definition.string.begin.bare.tjson, so you can make it show up in
your editor alone. Give it a background instead of a foreground:
{
"scope": "punctuation.definition.string.begin.bare.tjson",
"settings": { "background": "#a6e3a133" }
}
Every bare string in the file now begins with a faint tinted column, and a
value that looks like true but isn't reads as a string at a glance. This is
off by default because it is a preference, not a correction — the highlighting
is right either way. It can help you see the indent however, and you might
want to give it a shot.
Note there is deliberately no matching ...string.end.bare scope. A bare
string ends with two spaces or an eol. In an underpadded table, it can end
with a |, but this is not something a generator will ever do on its own —
the spec forbids a pipe inside a bare string in a table precisely so that an
underpadded table still parses deterministically.
Using the generator
If you control the generator, tjson --bare-strings marked writes that opening
space as _ — active:_true — which puts the distinction in the file itself
for every reader, editor or not. The two openers are interchangeable, occupy
the same column, and can be mixed freely; the grammar treats them identically.
Install - nano
Note: The nano highlighter isn't nearly as good as the VSCode/VSCodium one
The textmate grammar file can cover just about anything TJSON can spit out, but
nano's highlighting rules are not that sophisticated, so it is quite possible
that for certain things it simply cannot highlight TJSON correctly. That does
not mean that this highlighter could not be improved, I'm sure it can be to
a great extent. It's not nearly as good as the textmate grammar at the moment,
but it's much better than nothing.
Copy editors/nano/tjson.nanorc to your nano config directory:
cp editors/nano/tjson.nanorc ~/.config/nano/tjson.nanorc
Then include it in your nano config. Add this line to ~/.config/nano/nanorc
(or ~/.nanorc if you use the old location):
include "~/.config/nano/tjson.nanorc"
Or install system-wide so all users get it:
sudo cp editors/nano/tjson.nanorc /usr/share/nano/tjson.nanorc
System-wide files in /usr/share/nano/ are loaded automatically — no include line needed.
How to use the highlighter on your own web page
The grammar is an ordinary TextMate grammar, so the engine VS Code runs it with
also runs in a browser. You need no TJSON-specific library: tjson.tmLanguage.json
from this repo, plus vscode-textmate and vscode-oniguruma.
docs/web-highlighting.md has the scope-to-CSS
mapping, the palette textjson.com uses, and where to run the tokenizer.
Most pages are showing something they already hold as JSON, and for those the
TJSON view is one call — with the reader downloading none of the machinery:
const html = await renderJson(JSON.stringify(data)); // -> highlighted spans
The two forms carry the same data and convert both ways, so whichever one is
your source of truth, the other is always one conversion away. Rendering on
demand also means your render settings live in one place and every page picks
them up at once.
Where the TJSON is the artifact — an example in documentation, a snippet in a
spec — scripts/render-html.mjs highlights the text
itself just as well:
node scripts/render-html.mjs sample.tjson --fragment > sample.html
Either way a highlighted block is around 750 bytes gzipped, against roughly 181K
for the tokenizer, grammar and regex engine needed to produce the same pixels in
the browser — which is the whole page-load budget on a phone.
If the reader is typing TJSON, the engine has to be client-side. The guide
shows how to load it lazily, behind a dynamic import triggered by an
IntersectionObserver, so a page pays only when something is about to be coloured
and never during load. Parse-error reporting in the browser works the same way,
and is a separate download only pages with an editor need.
The mapping lives in docs/scope-classes.json rather
than in prose so it can be checked: tests/scopes.js asserts that every scope
the grammar emits resolves to a class and that no rule in it is dead. Copy it
as-is.