Hi everyone! I’m working on a modified JSON grammar that supports parsing Javascript between handlebars brackets. By following the mixed-language parsing example and referencing a number of other discussions, I’ve gotten something to work pretty well. The gist is that I’m using parseMixed
to parse the content of HandlebarsContent
nodes with the Javascript parser and mount that tree in place of the original node. Here’s a quick reference to the grammar, external tokenizer, and language definition, as well as a codesandbox link with a working example:
syntax.grammar
@top JsonText { value }
value { True | False | Null | Number | String | Object | Array | directive }
directive {
Handlebars
}
Handlebars { HandlebarsOpen HandlebarsContent HandlebarsClose }
HandlebarsOpen {
"{{"
}
HandlebarsContent { handlebarsText* }
@external tokens handlebarsTokens from "./tokens" {
handlebarsText,
HandlebarsClose
}
String { string }
Object { "{" list<Property>? "}" }
Array { "[" list<value>? "]" }
Property { PropertyName ":" value }
PropertyName { string | Handlebars }
@tokens {
True { "true" }
False { "false" }
Null { "null" }
Number { '-'? int frac? exp? }
int { '0' | $[1-9] @digit* }
frac { '.' @digit+ }
exp { $[eE] $[+\-]? @digit+ }
string { '"' char* '"' }
char { $[\u{20}\u{21}\u{23}-\u{5b}\u{5d}-\u{10ffff}] | "\\" esc }
esc { $["\\\/bfnrt] | "u" hex hex hex hex }
hex { $[0-9a-fA-F] }
whitespace { @whitespace+ }
@precedence {whitespace}
"{" "}" "[" "]"
"{{" "}}"
}
@skip { whitespace }
list<item> { item ("," item)* }
@external propSource jsonHighlighting from "./highlight"
@detectDelim
tokenizer
import { ExternalTokenizer } from "@lezer/lr";
import { handlebarsText, HandlebarsClose } from "./syntax.grammar.terms";
const closeTemplate = 125;
function expressionTokenizer() {
return new ExternalTokenizer((input) => {
let i = 0;
let state = 0;
let contentLength = 0;
while (true) {
if (input.next < 0) {
if (i) input.acceptToken(handlebarsText);
break;
}
// first close template
if (state == 0 && input.next == closeTemplate) {
state++;
}
// second close template
else if (state == 1 && input.next == closeTemplate) {
// if we have contentLength then accept that token
if (contentLength) {
input.acceptToken(handlebarsText, -contentLength);
} else {
input.acceptToken(HandlebarsClose, 1);
}
break;
} else {
// reset
contentLength++;
state = 0;
}
input.advance();
}
});
}
export const handlebarsTokens = expressionTokenizer();
language
export const JsonHandlebarsLanguage = LRLanguage.define({
name: "json",
parser: jsonParser.configure({
wrap: parseMixed((node) => {
return node.name === "HandlebarsContent"
? {
parser: javascriptLanguage.parser.configure({
top: "SingleExpression",
}),
}
: null;
}),
props: [
indentNodeProp.add({
Object: continuedIndent({ except: /^\s*\}/ }),
Array: continuedIndent({ except: /^\s*\]/ }),
}),
foldNodeProp.add({
"Object Array": foldInside,
}),
],
}),
languageData: {
closeBrackets: { brackets: ["[", "{", '"'] },
indentOnInput: /^\s*[\}\]]$/,
},
});
As an example, here’s the resulting tree for a simple expression like ‘{{4}}’:
JsonText(
Handlebars(
HandlebarsOpen,
SingleExpression(Number),
HandlebarsClose
)
)
I’ve verified that completion and syntax highlighting work as expected in most cases. However, there’s an issue I could use some help with. Ideally, if I start autocompletion from anywhere within the handlebars brackets, it should show Javascript completions. But when I add an empty Handlebars expression ‘{{}}’, and try to explicitly start autocompletion when the cursor is between the brackets, it appears the active language at that point in the document is still the parent language (not the wrapped language), so I don’t get shown completions for Javascript. If I print out the tree structure, I can see the following:
JsonText(
Handlebars(
HandlebarsOpen,
HandlebarsContent,
HandlebarsClose
)
)
Based on my understanding of parseMixed
, I would expect HandlebarsContent
to be replaced with SingleExpression
, even if the content of SingleExpression
was empty. I would also expect the active language at this location to be Javascript, so I get shown Javascript completions. Are these assumptions correct? If not, is there anything I can do to achieve the desired behavior?
Thanks for your help in advance, and for this amazing library!