diff --git a/packages/app/src/components/highlighted-code-block.tsx b/packages/app/src/components/highlighted-code-block.tsx index 292ec53618..9b7939daf1 100644 --- a/packages/app/src/components/highlighted-code-block.tsx +++ b/packages/app/src/components/highlighted-code-block.tsx @@ -46,6 +46,7 @@ const LANGUAGE_ALIASES: Record = { "objective-c": "m", markdown: "md", elixir: "ex", + terraform: "tf", }; function fenceLanguageToExtension(info: string | null | undefined): string | null { diff --git a/packages/highlight/package.json b/packages/highlight/package.json index 009e1677d5..21a683f3e8 100644 --- a/packages/highlight/package.json +++ b/packages/highlight/package.json @@ -5,7 +5,8 @@ "files": [ "dist", "!dist/**/*.map", - "src/astro/LICENSE" + "src/astro/LICENSE", + "src/hcl/LICENSE" ], "type": "module", "main": "./dist/index.js", diff --git a/packages/highlight/src/__tests__/highlighter.test.ts b/packages/highlight/src/__tests__/highlighter.test.ts index 095f9689a8..7c5e710504 100644 --- a/packages/highlight/src/__tests__/highlighter.test.ts +++ b/packages/highlight/src/__tests__/highlighter.test.ts @@ -3,6 +3,7 @@ import { setImmediate } from "node:timers/promises"; import { highlightCodeWithBudget, HIGHLIGHT_MAX_SOURCE_BYTES } from "../budgeted-highlighter.js"; import type { HighlightToken } from "../types.js"; import { highlightCode, highlightCodeInSteps, highlightLine } from "../highlighter.js"; +import { getParserForFile } from "../parsers.js"; describe("highlightCode", () => { it("highlights JavaScript code with correct token styles", () => { @@ -344,6 +345,121 @@ const title = "Hello"; }); }); +describe("Terraform", () => { + function styleOf(line: HighlightToken[], text: string) { + return line.find((token) => token.text === text)?.style; + } + + it("highlights blocks, labels, attributes, and values", () => { + const code = [ + "# The web server", + 'resource "aws_instance" "web" {', + " count = 2", + " enabled = true", + ' tags = merge(var.labels, { Name = "web" })', + "}", + ].join("\n"); + const result = highlightCode(code, "main.tf"); + + expect(result[0][0]).toEqual({ text: "# The web server", style: "comment" }); + expect(styleOf(result[1], "resource")).toBe("type"); + expect(styleOf(result[1], '"aws_instance"')).toBe("string"); + expect(styleOf(result[1], '"web"')).toBe("string"); + expect(styleOf(result[2], "count")).toBe("definition"); + expect(styleOf(result[2], "2")).toBe("number"); + expect(styleOf(result[3], "true")).toBe("literal"); + expect(styleOf(result[4], "merge")).toBe("function"); + expect(styleOf(result[4], "var")).toBe("variable"); + expect(styleOf(result[4], "labels")).toBe("property"); + }); + + it("highlights expressions interpolated inside strings", () => { + const result = highlightCode('name = "app-${var.env}-web"', "main.tf"); + + expect(result[0].map((token) => [token.text, token.style])).toEqual([ + ["name", "definition"], + [" ", null], + ["=", "operator"], + [" ", null], + ['"app-', "string"], + ["${", "punctuation"], + ["var", "variable"], + [".", "operator"], + ["env", "property"], + ["}", "punctuation"], + ['-web"', "string"], + ]); + }); + + it("highlights heredoc bodies as strings and resumes after the terminator", () => { + const code = ["policy = <<-EOT", ' { "Version": "2012" }', " EOT", "retries = 3"].join("\n"); + const result = highlightCode(code, "main.tf"); + + expect(styleOf(result[0], "EOT")).toBe("keyword"); + expect(result[1].map((token) => token.style)).toEqual(["string"]); + expect(styleOf(result[3], "retries")).toBe("definition"); + expect(styleOf(result[3], "3")).toBe("number"); + }); + + it("parses a real configuration without error nodes", () => { + const code = [ + "terraform {", + ' required_version = ">= 1.5"', + ' backend "s3" {}', + "}", + "", + 'variable "subnets" {', + " type = map(object({ cidr = string }))", + " default = {}", + "}", + "", + "locals {", + " names = [for key, subnet in var.subnets : upper(key) if subnet.cidr != null]", + ' first = length(local.names) > 0 ? local.names[0] : "none"', + " arns = aws_instance.web[*].arn", + " /* block comment */", + " path = provider::time::rfc3339_parse(timestamp()) // trailing", + "}", + "", + 'data "aws_iam_policy_document" "assume" {', + ' dynamic "statement" {', + " for_each = var.subnets", + " content {", + ' sid = "${statement.key}-%{ if statement.value.cidr != "" }set%{ else }unset%{ endif }"', + " }", + " }", + "}", + "", + 'output "script" {', + " value = < code.slice(from, from + 30))).toEqual([]); + }); + + it("uses the same grammar for tfvars and hcl files", () => { + expect(highlightCode('region = "eu-west-1"', "prod.tfvars")).toEqual( + highlightCode('region = "eu-west-1"', "terragrunt.hcl"), + ); + expect(styleOf(highlightLine('region = "eu-west-1"', "prod.tfvars"), "region")).toBe( + "definition", + ); + }); +}); + describe("highlightCodeInSteps", () => { it("splits a large file into many bounded steps with the same result", () => { const code = Array.from( diff --git a/packages/highlight/src/__tests__/parsers.test.ts b/packages/highlight/src/__tests__/parsers.test.ts index b24872eb94..b546d889c8 100644 --- a/packages/highlight/src/__tests__/parsers.test.ts +++ b/packages/highlight/src/__tests__/parsers.test.ts @@ -22,6 +22,9 @@ describe("isLanguageSupported", () => { expect(isLanguageSupported("test.dart")).toBe(true); expect(isLanguageSupported("test.cs")).toBe(true); expect(isLanguageSupported("test.nix")).toBe(true); + expect(isLanguageSupported("main.tf")).toBe(true); + expect(isLanguageSupported("prod.tfvars")).toBe(true); + expect(isLanguageSupported("terragrunt.hcl")).toBe(true); expect(isLanguageSupported("test.ex")).toBe(true); expect(isLanguageSupported("Counter.svelte")).toBe(true); expect(isLanguageSupported("Page.astro")).toBe(true); @@ -65,6 +68,9 @@ describe("getSupportedExtensions", () => { expect(extensions).toContain("dart"); expect(extensions).toContain("cs"); expect(extensions).toContain("nix"); + expect(extensions).toContain("tf"); + expect(extensions).toContain("tfvars"); + expect(extensions).toContain("hcl"); expect(extensions).toContain("json"); expect(extensions).toContain("svelte"); expect(extensions).toContain("astro"); diff --git a/packages/highlight/src/hcl/LICENSE b/packages/highlight/src/hcl/LICENSE new file mode 100644 index 0000000000..57650a9daf --- /dev/null +++ b/packages/highlight/src/hcl/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (C) 2021 by Marijn Haverbeke and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/highlight/src/hcl/highlight.ts b/packages/highlight/src/hcl/highlight.ts new file mode 100644 index 0000000000..9a73b6b7b5 --- /dev/null +++ b/packages/highlight/src/hcl/highlight.ts @@ -0,0 +1,41 @@ +import { styleTags, tags as t } from "@lezer/highlight"; + +export const hclHighlighting = styleTags({ + "for in if else endfor endif": t.controlKeyword, + "Attribute/Identifier": t.definition(t.propertyName), + "ObjectKey/Expression/VariableExpr/Identifier": t.definition(t.propertyName), + "ObjectKey/Expression/LiteralValue/StringLit!": t.definition(t.propertyName), + "ForIntro/Identifier TemplateForStart/Identifier": t.definition(t.variableName), + "BlockType!": t.typeName, + // Paseo themes have no role for labelName; labels render as the strings they are. + "BlockLabel!": t.string, + "FunctionNamespace/Identifier": t.namespace, + "FunctionName/Identifier": t.function(t.variableName), + "VariableExpr/Identifier": t.variableName, + "GetAttr/Identifier": t.propertyName, + Identifier: t.name, + "QuotedTemplateStart QuotedTemplateEnd TemplateLiteral": t.string, + NumericLit: t.number, + "true false": t.bool, + null: t.null, + LineComment: t.lineComment, + BlockComment: t.blockComment, + "( )": t.paren, + "{ }": t.brace, + "[ ]": t.squareBracket, + ArithOp: t.arithmeticOperator, + LogicOp: t.logicOperator, + CompareOp: t.compareOperator, + ControlOp: t.controlOperator, + "ControlOp/Colon": t.controlOperator, + AssignOp: t.definitionOperator, + DerefOp: t.derefOperator, + HeredocIdentifier: t.special(t.controlKeyword), + HeredocStart: t.operator, + "TemplateInterpolationStart TemplateInterpolationEnd": t.special(t.brace), + "TemplateDirectiveStart TemplateDirectiveEnd": t.special(t.brace), + StripMarker: t.special(t.brace), + "AttrSplat FullSplat LegacyIndex": t.derefOperator, + "Arrow Ellipsis": t.punctuation, + "ObjectElem/Colon ForIntro/Colon Comma NamespaceSeparator": t.separator, +}); diff --git a/packages/highlight/src/hcl/language.ts b/packages/highlight/src/hcl/language.ts new file mode 100644 index 0000000000..77722490b8 --- /dev/null +++ b/packages/highlight/src/hcl/language.ts @@ -0,0 +1,61 @@ +import { + delimitedIndent, + foldInside, + foldNodeProp, + indentNodeProp, + LRLanguage, +} from "@codemirror/language"; +import type { SyntaxNode } from "@lezer/common"; +import { parser } from "./parser.js"; + +export const hclLanguage = LRLanguage.define({ + name: "HCL", + parser: parser.configure({ + props: [ + indentNodeProp.add({ + Block: delimitedIndent({ closing: "}", align: false }), + }), + foldNodeProp.add({ + "Object Tuple TemplateFor TemplateIf": foldInside, + BlockComment: (tree: SyntaxNode) => { + return { from: tree.from + 2, to: tree.to - 2 }; + }, + HeredocTemplate(tree: SyntaxNode) { + const start = tree.getChild("HeredocIdentifier"); + if (!start) { + return null; + } + return { + from: start.to, + to: tree.to, + }; + }, + FunctionCall(tree: SyntaxNode) { + const start = tree.getChild("("); + const end = tree.getChild(")"); + if (!start || !end) { + return null; + } + return { + from: start.from + 1, + to: end.to - 1, + }; + }, + Block(tree: SyntaxNode) { + const start = tree.getChild("{"); + const end = tree.getChild("}"); + if (!start || !end) { + return null; + } + return { + from: start.from + 1, + to: end.to - 1, + }; + }, + }), + ], + }), + languageData: { + commentTokens: { line: "#", block: { open: "/*", close: "*/" } }, + }, +}); diff --git a/packages/highlight/src/hcl/parser.ts b/packages/highlight/src/hcl/parser.ts new file mode 100644 index 0000000000..fce1a5ffdb --- /dev/null +++ b/packages/highlight/src/hcl/parser.ts @@ -0,0 +1,64 @@ +// Generated by lezer-generator, taken from codemirror-lang-hcl@0.2.0 (MIT, see LICENSE). +// Vendored for the same reason as packages/highlight/src/nix: only Lezer primitives load here. +import { LRParser } from "@lezer/lr"; +import { hclHighlighting } from "./highlight.js"; +import { + scanQuotedTemplateStart, + scanQuotedTemplateEnd, + scanTemplateInterpolationStart, + scanTemplateInterpolationEnd, + scanTemplateDirectiveStart, + scanTemplateDirectiveEnd, + scanHeredocIdentifier, + scanTemplateLiteralChunk, + trackTemplate, +} from "./tokens.js"; + +const spec_Identifier = new Map([ + ["true", 41], + ["false", 43], + ["null", 47], + ["for", 66], + ["in", 70], + ["endfor", 74], + ["if", 80], + ["else", 84], + ["endif", 88], +]); +export const parser = LRParser.deserialize({ + version: 14, + states: + "GxQ]Q&jOOPeO&jOOOmQ&kO'#DcO!kQ&kO'#E_O!yQ&kO'#E^OOQO'#Ea'#EaO#UQ&jO'#ChQOQ&jOOPOOO'#Cd'#CdPOOO)C@`)C@`OOQy'#Co'#CoOOQy'#Cr'#CrO#aQ)vO'#CwOOQy'#Cm'#CmOOQ!b'#D['#D[O#oQ'|O'#DZOOQy'#Cv'#CvO$rQ&kO'#D`O$yQ&jO'#DeO%UQ&jO'#DdO&XQ&kO'#EqO&cQ&kO'#DcOOQy,59},59}O&jQ&jO,59}OOQy'#D]'#D]O(YQ'dO'#DgOOQO'#Ee'#EeO)PQ&jO'#DiO)UQ&jO'#DiOOQy'#Dp'#DpOOQP'#Dx'#DxOOQP'#Dy'#DyO%^Q&kO'#DwO*nQ'dO'#ClOOQy'#Dv'#DvOOQy'#El'#ElO%^Q&kO'#ElOOQy'#Cl'#ClO%^Q&kO,59TO#jQ)`O'#CtOOQP'#E`'#E`OOQP'#Eg'#EgO+eQ&kO,5:xO+pQ&jO,5:xOOQO-E8_-E8_O+xQ+YO'#CuO,ZQ)vO,59`O,iQ&sO'#CxO,sQ)tO'#C{O-OQ)tO'#DSOOQ%n'#Cz'#CzOOQ%n'#Eb'#EbO-ZQ)vO,59cOOQy,59c,59cO-iQ&jO'#DTO-tQ+WO,59uO.SQ&jO'#EpOOQy,59z,59zO._Q&jO,59zO%^Q&kO,5:]O.dQ&jO'#DrOOQP'#E['#E[O%^Q&kO,5:uO.iQ&kO,5:OOOQP'#Ed'#EdO/dQ&kO,5;]O/kQ&kO,5;]O/uQ&kO,5:`OOQy1G/i1G/iOOQO'#Dk'#DkOOQO,5:U,5:UO0pQ&jO'#DlOOQO-E8c-E8cO0xQ&jO,5:TO0}Q&kO,5:TOOQy,5:c,5:cOOQP'#D{'#D{OOQP'#D|'#D|OOQP'#D}'#D}OOQP'#EO'#EOOOQP'#EP'#EPOOQP'#EQ'#EQO%^Q&kO,5:fO%^Q&kO,5:fO%^Q&kO,5:fO%^Q&kO,5:fO%^Q&kO,5:fO%^Q&kO,5:fO%^Q&kO'#ESOOQy'#ER'#ERO1UQ&jO'#EUOOQy'#EX'#EXOOQy'#EY'#EYOOQy'#EW'#EWOOQy,5;W,5;WO1ZQ&jO,5;WO1cQ&jO1G.oO1qQ&lO,59`OOQP-E8e-E8eO1vQ&jO1G0dOOQO1G0d1G0dO1yQ&jO1G0dO+xQ+YO'#EmOOQ%n,59a,59aOOQy1G.z1G.zOOQ%n,59d,59dO2OQ&sO,59dO2YQ&rO,59dOOQ%n,59g,59gO,sQ)tO,59gO2eQ&jO'#DQOOQ%n,59n,59nO2sQ)tO,59nO-OQ)tO,59nO3OQ&jO'#DVOOQ%n-E8`-E8`OOQy1G.}1G.}O3aQ&jO,59hO3fQ&jO,59oO%^Q&kO,59oO3nQ+WO1G/aOOQy1G/a1G/aO3|Q&kO,5;[O4TQ&jO,5;[OOQy1G/f1G/fO4]Q&jO1G/wO4hQ&jO,5:^O4pQ&jO1G0aO4xQ&kO1G/jOOQP,5;O,5;OO5|Q&kO1G0wOOQP-E8b-E8bO7eQ,UO'#DgO7oQ,UO1G/zO9RQ,UO'#ClP9]Q&jO'#DjO9bQ&kO1G/oO9iQ&jO'#DnOOQy1G/o1G/oO9wQ&jO1G/oOOQy1G0Q1G0QO;|Q'dO1G0QObQ'dO1G0QO>lQ'dO1G0QO@^Q'dO1G0QOAQQ&jO,5:nOOQy,5:p,5:pPOQy'#Ef'#EfPOQy,5:s,5:sPOQy,5:t,5:tOOQy1G0r1G0rOOQO7+&O7+&OOAYQ&jO7+&OOOQ%n,5;X,5;XOOQ%n1G/O1G/OOA_Q&rO1G/OOAdQ&rO1G/OOOQ%n1G/R1G/ROAoQ&jO,59lOAzQ'[O,59lOOQ%n1G/Y1G/YO2sQ)tO1G/YOBSQ&jO'#DXO2sQ)tO1G/YOBbQ&jO,59qOBpQ'[O,59qOBxQ'[O,59sOCQQ&jO1G/SOCYQ&jO1G/SO%^Q&kO1G/ZOC_Q'[O1G/ZOOQy7+${7+${OCjQ&jO,5:}OC{Q&kO1G0vOOQO-E8a-E8aOOQy7+%c7+%cODSQ&jO7+%cO%^Q&kO'#DsO%^Q&kO1G/xODXQ&jO1G/xOOQP'#E]'#E]O%^Q&kO7+%{P%^Q&kO'#EdO%^Q&kO7+%fOOQy7+%Z7+%ZOD^Q&jO7+%ZODcQ&kO,5:YODjQ&jO,5:YOOQO,5:Y,5:YOOQy1G0Y1G0YPOQy-E8d-E8dOOQO<lAN>lONbQ&jOAN>lONgQ&jOAN>lOOQ%n<jOOQyG24WG24WO! ^Q&jOG24WOOQ$YG23`G23`O! cQ'[OG23`O! nQ'[OG23`O%^Q&kOG23`OOQPG24UG24UOOQyLD)rLD)rOOQ$YLD(zLD(zO! sQ'[OLD(zO! xQ'[OLD(zOOQ$Y!$'Lf!$'LfO!!TQ'[O!$'LfOOQ$Y!)9BQ!)9BQO!!YQ&kO'#DgO.iQ&kO'#DwO/uQ&kO'#DwO!!|Q&kO'#ClO.iQ&kO,5:fO/uQ&kO,5:fO.iQ&kO,5:fO/uQ&kO,5:fO.iQ&kO,5:fO/uQ&kO,5:fO.iQ&kO,5:fO/uQ&kO,5:fO.iQ&kO,5:fO/uQ&kO,5:fO.iQ&kO,5:fO/uQ&kO,5:fO!#sQ&kO1G0QO!$pQ&kO1G0QO!%mQ&kO1G0QO!(mQ&kO1G0QO!(tQ&kO1G0QO.iQ&kO7+%{O/uQ&kO7+%{O4pQ&jO1G0aO4pQ&jO1G0aO%^Q&kO,5:uO%^Q&kO,5:u", + stateData: + "!)T~O#_OSXPQYPQ~O^RO!UQO~OXWOYWO~OP[O^iOb]OdYOeYOgZO!RaO!TfO!UeO!atO#b^O#c^O#gnO#hoO~O_vOP#RX^#RX!U#RX~OPwO^xO!U{O~O^RO#[[X!T[X~OQ!VOR!POT!WO#]}O~OV!XO~OP[O^iOb]OdYOeYOgZOq!^O!RaO!UeO!atO#b^O#c^O#gnO#hoO~O!Q!ZO~P#tO#w!_O_!XX!Y!XX~O_!aO!Y!aO~OP[O^iOb]OdYOeYOgZO!RaO!UeO!atO#b^O#c^O#gnO#hoO~Or!cO!T#eX~P%^Oq!^O~PmO!T!fO~O#f!gO!R!ZX!w!ZX!y!ZX#g!ZX#i!ZX#j!ZX#k!ZX#l!ZX#m!ZX#n!ZX#o!ZX#p!ZX#q!ZX#r!ZX#s!ZX#t!ZX#u!ZX#v!ZX#w!ZXr!ZX^!ZX!T!ZX~O_!ZX!Y!ZX!a!`X!Q!ZX![!ZX#[!ZXS!ZXm!ZXx!ZX!c!ZXU!ZX~P&oO^!iO~O!a!lO~O!w!{O!y!|O#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO#q!qO#r!qO#s!rO#t!sO#u!}O#v#OO#w`Xr`X^`X!T`X~O!R!zO_`X!Y`X!Q`X![`X#[`XS`Xm`Xx`X!c`XU`X~P)ZOPwO^xO!U#VO~O^RO!T#WO~O#]#YOQ#aPR#aPT#aPV#aP~OQ#[OR#UXT#UX#]#UX~OS#]Om#^O~P%^OR!POT#bO#]}O~OR!POT#fO#]}O~OQ#hOR!POT!WO#]}O~Om#jOq#iOx#kO~OR!POT!WOV#mO#]}O~Or#nO#w!_O!Q#dX~O!Q#pO~O^#rO~OP[O^&yOb]OdYOeYOgZO!RaO!UeO!atO#b^O#c^O#gnO#hoO~O!T#ea~P%^Or#vO!T#ea~P%^OP[O^#xOb]OdYOeYOgZO!RaO!UeO!atO#b^O#c^O#gnO#hoO~O#f!gO!a!`X~O!a#|O~O![$OO~P%^O^$XO~O![$]O#w!_O~O#w!_O^]i#[]i!T]i~OQ#[O~O^RO!T$^O~OS$aOm$bO~P%^OS$aOm$bO#w!_O~Om$eOq#iOu$fOx#kO~OR!POT$iO#]}O~Om$kOq#iOx#kOz$lO|$mO~O^$nO~Oq$oOx$pO~OR!POT!WOV$rO#]}O~O!Q#da~P%^Or$tO!Q#da~Ox$xO!Q$vO#w!_O~Or$zOs$yO~O!Y${O#w!_O~O#w!_OP!Wi^!Wib!Wid!Wie!Wig!Wir!Wi!R!Wi!T!Wi!U!Wi!a!Wi#b!Wi#c!Wi#g!Wi#h!Wi~O!T#ei~P%^O#f!gO!R!ZX!w!ZX!y!ZX#g!ZX#i!ZX#j!ZX#k!ZX#l!ZX#m!ZX#n!ZX#o!ZX#p!ZX#q!ZX#r!ZX#s!ZX#t!ZX#u!ZX#v!ZX#w!ZX~O!a!`X!i!ZX~P6TO!i%OO#w!_O~O!w!{O!y!|O#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO#q!qO#r!qO#s!rO#t!sO#u!}O#v#OO#w`X~O!R!zO!i`X~P7wO#f!gO~O![%PO~P%^Or%RO!c%TO#w!_O![!bX~O![%PO~O#i!nO#j!nO#k!nO_!ni!R!ni!Y!ni!w!ni!y!ni#m!ni#n!ni#o!ni#p!ni#q!ni#r!ni#s!ni#t!ni#u!ni#v!ni#w!nir!ni!Q!ni![!ni^!ni#[!niS!nim!nix!ni!c!niU!ni!T!ni~O#g!ni#l!ni~P9|O#g!oO#l!oO~P9|O#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO_!ni!R!ni!Y!ni!w!ni!y!ni#s!ni#t!ni#u!ni#v!ni#w!nir!ni!Q!ni![!ni^!ni#[!niS!nim!nix!ni!c!niU!ni!T!ni~O#q!ni#r!ni~PvO!Q%UO#w!_O~O!T%WO~OS%XO~OS%XOm%YO#w!_O~Oq$oOu%ZOx$pO~OU%[Om%]O~Om%_Oq#iOx#kO|$mO~Oq$oOx$pOz%aO|%bO~OU%cOm%dO~OU%eOm%fO~Or%hOs%gO~O^%iO~OU%kOm%lO#w!_O~O#w!_Or#Va!Q#Va![#Va!c#Va~O!Q#di~P%^O!Q%nO~O^%qO~O![%yO~O![!ba~P%^Or%zO!c%{O![!ba~OS%|O~OU%}Om&OO~OU%}O~Oq$oOx$pO|%bO~OU&QOm&RO~OU&SOm&TO~OU&QO~OU&SO~O^&VO~Or&XOs&WO~OU&YOm&ZO#w!_O~OU&YO~O#w!_O!Q!ga!T!ga~O!Y&[O#w!_O~Os&]O~Ox$xO!T&^O!c&`O#w!_O~O#i!nO#j!nO#k!nO!R!ni!w!ni!y!ni#g!ni#l!ni#m!ni#n!ni#o!ni#p!ni#q!ni#r!ni#s!ni#t!ni#u!ni#v!ni#w!ni~O!i!ni~PGeO#g!oO#i!nO#j!nO#k!nO#l!oO!R!ni!w!ni!y!ni#m!ni#n!ni#o!ni#p!ni#q!ni#r!ni#s!ni#t!ni#u!ni#v!ni#w!ni~O!i!ni~PHyO#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO!R!ni!w!ni!y!ni#q!ni#r!ni#s!ni#t!ni#u!ni#v!ni#w!ni~O!i!ni~PJ_O#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO#q!qO#r!qO!R!ni!i!ni!w!ni!y!ni#t!ni#u!ni#v!ni#w!ni~O#s!ni~PKsO#s!rO~PKsO![!bi~P%^OU&aO~OU&bO~OU&cO~OU&dOm&eO#w!_O~Os&fO~O^&hO~OU&iO~O!T&kO~Ox$xO!T&kO~OU&mO~OU&mOm&oO#w!_O~Os&pO~O!Y&qO#w!_O~O!T&rO~OU&sOm&tO#w!_O~OU&sO~OU&vO~OU&vOm&wO#w!_O~OU&xO~OP!ZXb!ZXd!ZXe!ZXg!ZX!U!ZX!a!ZX#b!ZX#c!ZX#h!ZX~P&oOP`Xb`Xd`Xe`Xg`X!R`X!U`X!a`X#b`X#c`X#h`X~P)ZOP!ni^!nib!nid!nie!nig!nir!ni!T!ni!U!ni!a!ni#b!ni#c!ni#h!ni~PGeOP!ni^!nib!nid!nie!nig!nir!ni!T!ni!U!ni!a!ni#b!ni#c!ni#h!ni~PHyOP!ni^!nib!nid!nie!nig!nir!ni!T!ni!U!ni!a!ni#b!ni#c!ni#h!ni~PJ_O#g!oO#i!nO#j!nO#k!nO#l!oO#m!pO#n!pO#o!pO#p!pO#q!qO#r!qOP!ni^!nib!nid!nie!nig!nir!ni!R!ni!T!ni!U!ni!a!ni!w!ni!y!ni#b!ni#c!ni#h!ni#t!ni#u!ni#v!ni#w!ni~O#s!ni~P!&jO#s!rO~P!&jOb!w!y^X#j^~", + goto: "CW#fPPPPPPPP#gPPP#j#sPP#y%xP'YPP'YP(j*O%x*c+sP+s,R,aPPP,oP,R,uP-TP-ZP*c-i%xPP.yPP0Z1n1{P%xP%x2U3g3nP5RP%x5X6i6o5XP%x6x8Y8Y6x9n:W:m;P;`;l;u;zP;uP;uO>S>[>v?Q?WP@kPPPP@qByPPCPCSRXPQVOQ#X{R$_#VXTOU{#V^bQde!c!d#v$}Q!YaQ#RtQ#SvQ#_!PQ#q!]Q#s!`Q#t!aQ#y!eS#}!l#|Q$W!zQ$c#^Q$q#kY$s#n$t%R%m%zQ%j$pQ%o$xQ%p$yU%r$|'`'aQ%s%OQ&U%gQ&g&WQ&j&]Q&n&fQ&u&pQ'b'dR'c'e#msQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'e#m]Qadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'e#l]Qadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eTxSzQ!O[f!T!Q!R!U!X#a#d#e#l$h$j%`R#Tw#m`Qadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'ei!T[!Q!R!U!X#a#d#e#l$h$j%`i!S[!Q!R!U!X#a#d#e#l$h$j%`i!Q[!Q!R!U!X#a#d#e#l$h$j%`Q#`!QR$d#ai!R[!Q!R!U!X#a#d#e#l$h$j%`Q#d!RR$j#eQ#c!RS$g#d#eS%^$h$jR&P%`#m_Qadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'e#mhQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eQVO#mhQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eSdQeS!bd!dV#u!c#v$}_cQde!c!d#v$}#ojQadekptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eZ!hi!i#x#{&y#llQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eR!kkQ$P!lR%Q#|#mmQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eQ!]aR!eeQ$w#qQ&_%sR&l&`#mrQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'e!vpQadeptv!P!]!`!c!d!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p'd'eb&z!a&z&}'P'R'T'V'X'`c&{!e&{'O'Q'S'U'W'Y'a[!tq$R$S$T$U$V[&}&|'Z'[']'^'_]'O#z%t%u%v%w%xY!uq$S$T$U$VY'P&|'[']'^'_Z'Q#z%u%v%w%xW!vq$T$U$VW'R&|']'^'_X'S#z%v%w%xU!wq$U$VU'T&|'^'_V'U#z%w%xS!xq$VS'V&|'_T'W#z%xQ!yqQ'X&|R'Y#zV#Qq#z&|T!{q#zV#Pq#z&|!quQadetv!P!]!`!a!c!d!e!l!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p'`'a'd'e!O!`b!Y#R#S#_#q#s#}$W$c$q$s%j%o%p%s&U&g&j&n&u'b'cQ'd#tR'e#yQ$|#sQ'`'bR'a'cXSOU{#VTySzUUO{#VR|UQ!U[Q#a!QQ#e!R[#g!U#a#e#l$h%`Q#l!XQ$h#dR%`$jQ#o!YS$u#o%SR%S#}Q!ddR#w!d#lkQadeptv!P!]!`!a!c!d!e!l!t!u!v!w!x!y!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p&z&{&}'O'P'Q'R'S'T'U'V'W'X'Y'`'a'd'eR!jkQzSR#Uz!hqQadetv!P!]!`!c!d!l!z#^#k#n#v#|$p$t$x$y$|$}%O%R%g%m%z&W&]&f&p'd'eU!mp&z&{S#z!e'aU$Q!t&}'OQ$R!uQ$S!vQ$T!wQ$U!xQ$V!yQ%t'QQ%u'SQ%v'UQ%w'WQ%x'YS&|!a'`Q'Z'PQ'['RQ']'TQ'^'VR'_'XQ#Z}R$`#YR![aTgQe", + nodeNames: + "⚠ QuotedTemplateStart QuotedTemplateEnd TemplateInterpolationStart TemplateInterpolationEnd TemplateDirectiveStart TemplateDirectiveEnd HeredocIdentifier Comment LineComment BlockComment ConfigFile Body Attribute Identifier AssignOp Expression LiteralValue NumericLit BoolLit true false NullLit null StringLit TemplateLiteral TemplateExpr QuotedTemplate TemplateInterpolation StripMarker TemplateDirective TemplateFor TemplateForStart for Comma in TemplateForEnd endfor TemplateIf TemplateIfIntro if TemplateElseIntro else TemplateIfEnd endif HeredocTemplate HeredocStart CollectionValue ] [ Tuple } { Object ObjectElem ObjectKey Colon VariableExpr ) FunctionCall FunctionNamespace NamespaceSeparator FunctionName ( FunctionArguments Ellipsis ForExpr ForTupleExpr ForIntro ForCond ForObjectExpr Arrow Operation UnaryOperation ArithOp LogicOp BinaryOperation ArithOp ArithOp CompareOp CompareOp LogicOp LogicOp Index NewIndex LegacyIndex GetAttr DerefOp Splat AttrSplat FullSplat Conditional ControlOp ControlOp Block BlockType BlockLabel", + maxTerm: 131, + context: trackTemplate, + nodeProps: [ + ["openedBy", 48, "[", 51, "{"], + ["closedBy", 49, "]", 52, "}"], + ], + skippedNodes: [0, 8, 9, 10], + repeatNodeCount: 7, + tokenData: + "-p~RvX^#ipq#iqr$^st$kuv%Svw%Xxy%dyz%iz{%n{|%s|}%x}!O%}!O!P&S!P!Q&z!Q!R(^!R![)l![!]*c!^!_*p!_!`+_!`!a+t!a!b,R!c!},W!}#O,l#P#Q-P#R#S,W#T#o,W#o#p-U#p#q-Z#q#r-f#r#s-k#y#z#i$f$g#i#BY#BZ#i$IS$I_#i$I|$JO#i$JT$JU#i$KV$KW#i&FU&FV#i~#nY#_~X^#ipq#i#y#z#i$f$g#i#BY#BZ#i$IS$I_#i$I|$JO#i$JT$JU#i$KV$KW#i&FU&FV#i~$cP#h~!_!`$f~$kO#r~~$pSX~OY$kZ;'S$k;'S;=`$|<%lO$k~%PP;=`<%l$k~%XO#k~~%[Pvw%_~%dO#s~~%iO!a~~%nO![~~%sO#i~~%xO#l~~%}Or~~&SO#g~~&XR!y~z{&b!O!P&g!Q![&r~&gO#u~~&jP!O!P&m~&rO!c~~&wP!w~!Q![&r~'PQ#j~z{'V!P!Q$k~'YTOz'Vz{'i{;'S'V;'S;=`(W<%lO'V~'lVOz'Vz{'i{!P'V!P!Q(R!Q;'S'V;'S;=`(W<%lO'V~(WOY~~(ZP;=`<%l'V~(cRb~!O!P(l!Q![)l#l#m)w~(oP!Q![(r~(wRb~!Q![(r!g!h)Q#X#Y)Q~)TR{|)^}!O)^!Q![)d~)aP!Q![)d~)iPb~!Q![)d~)qQb~!O!P(l!Q![)l~)zR!Q![*T!c!i*T#T#Z*T~*YRb~!Q![*T!c!i*T#T#Z*T~*hP!Y~![!]*k~*pO#f~~*uQ#o~!^!_*{!_!`+Y~+QP#b~}!O+T~+YO#c~~+_O#p~~+dQ_P!_!`+j!`!a+o~+oO#q~Q+tO!iQ~+yP#m~!_!`+|~,RO#n~~,WO#w~~,]T^~}!O,W!Q![,W!c!},W#R#S,W#T#o,W~,qP!R~z{,t~,wP#P#Q,z~-PO#v~~-UO!Q~~-ZO!U~~-^P#p#q-a~-fO#t~~-kO!T~~-pOm~", + tokenizers: [ + scanQuotedTemplateStart, + scanQuotedTemplateEnd, + scanTemplateInterpolationStart, + scanTemplateInterpolationEnd, + scanTemplateDirectiveStart, + scanTemplateDirectiveEnd, + scanHeredocIdentifier, + scanTemplateLiteralChunk, + 0, + 1, + ], + topRules: { ConfigFile: [0, 11] }, + dynamicPrecedences: { "19": 1, "22": 1 }, + specialized: [{ term: 14, get: (value: string) => spec_Identifier.get(value) ?? -1 }], + propSources: [hclHighlighting], + tokenPrec: 2481, +}); diff --git a/packages/highlight/src/hcl/terms.ts b/packages/highlight/src/hcl/terms.ts new file mode 100644 index 0000000000..e4fc0621f7 --- /dev/null +++ b/packages/highlight/src/hcl/terms.ts @@ -0,0 +1,10 @@ +// Generated by lezer-generator, taken from codemirror-lang-hcl@0.2.0 (MIT, see LICENSE). +// Vendored for the same reason as packages/highlight/src/nix: only Lezer primitives load here. +export const QuotedTemplateStart = 1; +export const QuotedTemplateEnd = 2; +export const TemplateInterpolationStart = 3; +export const TemplateInterpolationEnd = 4; +export const TemplateDirectiveStart = 5; +export const TemplateDirectiveEnd = 6; +export const HeredocIdentifier = 7; +export const templateLiteralChunk = 105; diff --git a/packages/highlight/src/hcl/tokens.ts b/packages/highlight/src/hcl/tokens.ts new file mode 100644 index 0000000000..3d4ca15685 --- /dev/null +++ b/packages/highlight/src/hcl/tokens.ts @@ -0,0 +1,334 @@ +// Taken from codemirror-lang-hcl@0.2.0 (MIT, see LICENSE). +// Vendored for the same reason as packages/highlight/src/nix: only Lezer primitives load here. +import { ContextTracker, ExternalTokenizer, type InputStream } from "@lezer/lr"; +import { + QuotedTemplateStart, + QuotedTemplateEnd, + TemplateInterpolationStart, + TemplateInterpolationEnd, + TemplateDirectiveStart, + TemplateDirectiveEnd, + HeredocIdentifier, + templateLiteralChunk, +} from "./terms.js"; + +const ContextType = { + QUOTED_TEMPLATE: 1, + TEMPLATE_INTERPOLATION: 2, + TEMPLATE_DIRECTIVE: 4, + HEREDOC_TEMPLATE: 8, +}; + +const hashStr = function (str: string) { + var hash = 0, + i, + chr; + if (str.length === 0) return hash; + for (i = 0; i < str.length; i++) { + chr = str.charCodeAt(i); + hash = (hash << 5) - hash + chr; + hash |= 0; // Convert to 32bit integer + } + return hash; +}; + +class Context { + readonly parent: Context | null; + readonly type: number; + readonly heredoc_identifier: string; + readonly hash: number; + + constructor(parent: Context | null, type: number, heredoc_identifier: string) { + this.parent = parent; + this.type = type; + this.heredoc_identifier = heredoc_identifier; + this.hash = + (parent ? (parent.hash + parent.hash) << 8 : 0) + + (type << 4) + + (this.heredoc_identifier === "" ? 0 : hashStr(this.heredoc_identifier)); + } +} + +function readTokenText(input: InputStream, end: number): string { + let text = ""; + for (let offset = 0; offset < end - input.pos; offset++) { + text += String.fromCharCode(input.peek(offset)); + } + return text; +} + +const topCtx = new Context(null, 0, ""); + +export const trackTemplate = new ContextTracker({ + start: topCtx, + shift(context, term, stack, input) { + if (term === QuotedTemplateStart) { + return new Context(context, ContextType.QUOTED_TEMPLATE, ""); + } + if (term === TemplateInterpolationStart) { + return new Context(context, ContextType.TEMPLATE_INTERPOLATION, ""); + } + if (term === TemplateDirectiveStart) { + return new Context(context, ContextType.TEMPLATE_DIRECTIVE, ""); + } + if (term === HeredocIdentifier && context.type !== ContextType.HEREDOC_TEMPLATE) { + const ctx = new Context( + context, + ContextType.HEREDOC_TEMPLATE, + readTokenText(input, stack.pos), + ); + return ctx; + } + + if (term === HeredocIdentifier && context.type === ContextType.HEREDOC_TEMPLATE) { + return context.parent ?? context; + } + switch (term) { + case QuotedTemplateEnd: + case TemplateInterpolationEnd: + case TemplateDirectiveEnd: + return context.parent ?? context; + } + + return context; + }, + hash(context) { + return context.hash; + }, +}); + +const HEX_DIGIT = /[0-9a-fA-F]/; + +function scanEscapeSequence(input: InputStream): void { + input.advance(); + const kind = String.fromCharCode(input.next); + if (kind === "u" || kind === "U") { + const digits = kind === "u" ? 4 : 8; + for (let i = 0; i < digits; i++) { + input.advance(); + if (!HEX_DIGIT.test(String.fromCharCode(input.next))) return; + } + } else if (!'"nrt\\'.includes(kind)) { + return; + } + input.advance(); + input.acceptToken(templateLiteralChunk); +} + +function scanHeredocBody(input: InputStream, expected: string): void { + let has_leading_whitespace_with_newline = false; + let has_leading_whitespace = false; + let next = String.fromCharCode(input.next); + while (/\s/.test(next)) { + has_leading_whitespace = true; + if (next === "\n") { + has_leading_whitespace_with_newline = true; + } + input.advance(); + next = String.fromCharCode(input.next); + } + if (has_leading_whitespace) { + input.acceptToken(templateLiteralChunk); + } + + if (!has_leading_whitespace_with_newline) { + return; + } + + let advance = 0; + for (let i = 0; i < expected.length; i++) { + if (String.fromCharCode(input.next) === expected[i]) { + input.advance(); + advance++; + } else { + if (advance) { + input.acceptToken(templateLiteralChunk); + } + return; + } + } + + // check if the identifier is on a line of its own + let i = 0; + while (true) { + const ch = input.peek(i); + if (ch == -1 || String.fromCharCode(ch) === "\n") { + return input.acceptToken(HeredocIdentifier); + } + + if (!/\s/.test(String.fromCharCode(ch))) { + return input.acceptToken(templateLiteralChunk); + } + + i++; + } +} + +export const scanQuotedTemplateStart = new ExternalTokenizer( + (input, stack) => { + if ( + stack.canShift(QuotedTemplateStart) && + !(stack.context && stack.context.type === ContextType.QUOTED_TEMPLATE) && + String.fromCharCode(input.next) === '"' + ) { + input.advance(); + return input.acceptToken(QuotedTemplateStart); + } + }, + { contextual: true, fallback: true }, +); +export const scanQuotedTemplateEnd = new ExternalTokenizer( + (input, stack) => { + if ( + stack.canShift(QuotedTemplateEnd) && + stack.context && + stack.context.type === ContextType.QUOTED_TEMPLATE && + String.fromCharCode(input.next) === '"' + ) { + input.advance(); + return input.acceptToken(QuotedTemplateEnd); + } + }, + { contextual: true, fallback: true }, +); + +export const scanTemplateLiteralChunk = new ExternalTokenizer( + (input, stack) => { + if ( + stack.canShift(templateLiteralChunk) && + stack.context && + stack.context.type === ContextType.QUOTED_TEMPLATE + ) { + if (String.fromCharCode(input.next) === "\\") { + return scanEscapeSequence(input); + } + } + + if ( + stack.canShift(templateLiteralChunk) && + stack.context && + (stack.context.type === ContextType.QUOTED_TEMPLATE || + stack.context.type === ContextType.HEREDOC_TEMPLATE) && + input.next !== -1 + ) { + input.advance(); + return input.acceptToken(templateLiteralChunk); + } + }, + { contextual: true, fallback: true }, +); +export const scanTemplateInterpolationStart = new ExternalTokenizer( + (input, stack) => { + if ( + stack.canShift(TemplateInterpolationStart) && + stack.canShift(templateLiteralChunk) && + !(stack.context && stack.context.type === ContextType.TEMPLATE_INTERPOLATION) && + String.fromCharCode(input.next) === "$" + ) { + input.advance(); + if (String.fromCharCode(input.next) === "{") { + input.advance(); + return input.acceptToken(TemplateInterpolationStart); + } + + // escape sequence + if (String.fromCharCode(input.next) === "$") { + input.advance(); + if (String.fromCharCode(input.next) === "{") { + // $${ + input.advance(); + return input.acceptToken(templateLiteralChunk); + } + } + + return input.acceptToken(templateLiteralChunk); + } + }, + { contextual: true, fallback: true }, +); +export const scanTemplateInterpolationEnd = new ExternalTokenizer( + (input, stack) => { + if ( + stack.canShift(TemplateInterpolationEnd) && + stack.context && + stack.context.type === ContextType.TEMPLATE_INTERPOLATION && + String.fromCharCode(input.next) === "}" + ) { + input.advance(); + return input.acceptToken(TemplateInterpolationEnd); + } + }, + { contextual: true, fallback: true }, +); +export const scanTemplateDirectiveStart = new ExternalTokenizer( + (input, stack) => { + if ( + stack.canShift(TemplateDirectiveStart) && + stack.canShift(templateLiteralChunk) && + !(stack.context && stack.context.type === ContextType.TEMPLATE_DIRECTIVE) && + String.fromCharCode(input.next) === "%" + ) { + input.advance(); + if (String.fromCharCode(input.next) === "{") { + input.advance(); + return input.acceptToken(TemplateDirectiveStart); + } + + // escape sequence + if (String.fromCharCode(input.next) === "%") { + input.advance(); + if (String.fromCharCode(input.next) === "{") { + // $${ + input.advance(); + return input.acceptToken(templateLiteralChunk); + } + } + + return input.acceptToken(templateLiteralChunk); + } + }, + { contextual: true, fallback: true }, +); +export const scanTemplateDirectiveEnd = new ExternalTokenizer( + (input, stack) => { + if ( + stack.canShift(TemplateDirectiveEnd) && + stack.context && + stack.context.type === ContextType.TEMPLATE_DIRECTIVE && + String.fromCharCode(input.next) === "}" + ) { + input.advance(); + return input.acceptToken(TemplateDirectiveEnd); + } + }, + { contextual: true, fallback: true }, +); + +export const scanHeredocIdentifier = new ExternalTokenizer( + (input, stack) => { + if ( + stack.canShift(HeredocIdentifier) && + !(stack.context && stack.context.type === ContextType.HEREDOC_TEMPLATE) && + input.next !== -1 + ) { + let next = String.fromCharCode(input.next); + while (/[0-9a-zA-Z]/.test(next) || next === "_" || next === "-") { + input.advance(); + next = String.fromCharCode(input.next); + } + + return input.acceptToken(HeredocIdentifier); + } + + if ( + stack.canShift(HeredocIdentifier) && + stack.canShift(templateLiteralChunk) && + stack.context && + stack.context.type === ContextType.HEREDOC_TEMPLATE && + input.next != -1 + ) { + return scanHeredocBody(input, stack.context.heredoc_identifier); + } + }, + { contextual: true, fallback: true }, +); diff --git a/packages/highlight/src/parsers.ts b/packages/highlight/src/parsers.ts index 06c3129733..1f1d2b037d 100644 --- a/packages/highlight/src/parsers.ts +++ b/packages/highlight/src/parsers.ts @@ -20,6 +20,7 @@ import { csharpLanguage } from "./csharp/language.js"; import { astroParser } from "./astro/parser.js"; import { vueParser } from "./vue/parser.js"; import { nixLanguage } from "./nix/language.js"; +import { hclLanguage } from "./hcl/language.js"; import { parser as svelteBaseParser } from "./svelte/parser.js"; import { configureNesting, defaultNesting } from "./svelte/nesting.js"; @@ -82,6 +83,10 @@ const languagesByExtension: Record = { cs: csharpLanguage, // Nix nix: nixLanguage, + // Terraform / HCL + tf: hclLanguage, + tfvars: hclLanguage, + hcl: hclLanguage, // Elixir ex: language(elixirParser), exs: language(elixirParser),