mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-05 03:12:14 +00:00
feat(ui): add syntax highlighting to finding groups remediation code (#10698)
This commit is contained in:
@@ -7,6 +7,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
### 🚀 Added
|
||||
|
||||
- Resources side drawer with redesigned detail panel [(#10673)](https://github.com/prowler-cloud/prowler/pull/10673)
|
||||
- Syntax highlighting for remediation code blocks in finding groups drawer with provider-aware auto-detection (Shell, HCL, YAML, Bicep) [(#10698)](https://github.com/prowler-cloud/prowler/pull/10698)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
|
||||
+43
-1
@@ -178,16 +178,30 @@ vi.mock("@/components/findings/markdown-container", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/components/shared/query-code-editor", () => ({
|
||||
QUERY_EDITOR_LANGUAGE: {
|
||||
OPEN_CYPHER: "openCypher",
|
||||
PLAIN_TEXT: "plainText",
|
||||
SHELL: "shell",
|
||||
HCL: "hcl",
|
||||
BICEP: "bicep",
|
||||
YAML: "yaml",
|
||||
},
|
||||
QueryCodeEditor: ({
|
||||
ariaLabel,
|
||||
language,
|
||||
value,
|
||||
copyValue,
|
||||
}: {
|
||||
ariaLabel: string;
|
||||
language?: string;
|
||||
value: string;
|
||||
copyValue?: string;
|
||||
}) => (
|
||||
<div data-testid="query-code-editor" data-aria-label={ariaLabel}>
|
||||
<div
|
||||
data-testid="query-code-editor"
|
||||
data-aria-label={ariaLabel}
|
||||
data-language={language}
|
||||
>
|
||||
<span>{ariaLabel}</span>
|
||||
<span>{value}</span>
|
||||
<button
|
||||
@@ -512,6 +526,34 @@ describe("ResourceDetailDrawerContent — Fix 2: Remediation heading labels", ()
|
||||
expect(mockClipboardWriteText).toHaveBeenCalledWith("aws s3 ...");
|
||||
expect(screen.getByText("$ aws s3 ...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should pass syntax highlighting languages to each remediation editor", () => {
|
||||
// Given
|
||||
render(
|
||||
<ResourceDetailDrawerContent
|
||||
isLoading={false}
|
||||
isNavigating={false}
|
||||
checkMeta={checkMetaWithCommands}
|
||||
currentIndex={0}
|
||||
totalResources={1}
|
||||
currentFinding={mockFinding}
|
||||
otherFindings={[]}
|
||||
onNavigatePrev={vi.fn()}
|
||||
onNavigateNext={vi.fn()}
|
||||
onMuteComplete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
// When
|
||||
const editors = screen.getAllByTestId("query-code-editor");
|
||||
|
||||
// Then
|
||||
expect(editors[0]).toHaveAttribute("data-language", "shell");
|
||||
expect(editors[1]).toHaveAttribute("data-language", "hcl");
|
||||
expect(editors[2]).toHaveAttribute("data-language", "yaml");
|
||||
expect(editors[0]).toHaveAttribute("data-aria-label", "CLI Command");
|
||||
expect(editors[2]).toHaveAttribute("data-aria-label", "CloudFormation");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+42
-4
@@ -44,7 +44,11 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import { EventsTimeline } from "@/components/shared/events-timeline/events-timeline";
|
||||
import { QueryCodeEditor } from "@/components/shared/query-code-editor";
|
||||
import {
|
||||
QUERY_EDITOR_LANGUAGE,
|
||||
QueryCodeEditor,
|
||||
type QueryEditorLanguage,
|
||||
} from "@/components/shared/query-code-editor";
|
||||
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { DateWithTime } from "@/components/ui/entities/date-with-time";
|
||||
@@ -81,19 +85,49 @@ function stripCodeFences(code: string): string {
|
||||
.trim();
|
||||
}
|
||||
|
||||
function resolveNativeIacConfig(providerType: string | undefined): {
|
||||
label: string;
|
||||
language: QueryEditorLanguage;
|
||||
} {
|
||||
switch (providerType) {
|
||||
case "aws":
|
||||
return {
|
||||
label: "CloudFormation",
|
||||
language: QUERY_EDITOR_LANGUAGE.YAML,
|
||||
};
|
||||
case "azure":
|
||||
return {
|
||||
label: "Bicep",
|
||||
language: QUERY_EDITOR_LANGUAGE.BICEP,
|
||||
};
|
||||
case "kubernetes":
|
||||
return {
|
||||
label: "Kubernetes Manifest",
|
||||
language: QUERY_EDITOR_LANGUAGE.YAML,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: "Native IaC",
|
||||
language: QUERY_EDITOR_LANGUAGE.PLAIN_TEXT,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function renderRemediationCodeBlock({
|
||||
label,
|
||||
value,
|
||||
copyValue,
|
||||
language = QUERY_EDITOR_LANGUAGE.PLAIN_TEXT,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
copyValue?: string;
|
||||
language?: QueryEditorLanguage;
|
||||
}) {
|
||||
return (
|
||||
<QueryCodeEditor
|
||||
ariaLabel={label}
|
||||
language="plainText"
|
||||
language={language}
|
||||
value={value}
|
||||
copyValue={copyValue}
|
||||
editable={false}
|
||||
@@ -351,6 +385,7 @@ export function ResourceDetailDrawerContent({
|
||||
? (f?.scan?.id ?? null)
|
||||
: null;
|
||||
const regionFilter = searchParams.get("filter[region__in]");
|
||||
const nativeIacConfig = resolveNativeIacConfig(f?.providerType);
|
||||
|
||||
const handleOpenCompliance = async (framework: string) => {
|
||||
if (!complianceScanId || resolvingFramework) {
|
||||
@@ -741,6 +776,7 @@ export function ResourceDetailDrawerContent({
|
||||
<div className="flex flex-col gap-1">
|
||||
{renderRemediationCodeBlock({
|
||||
label: "CLI Command",
|
||||
language: QUERY_EDITOR_LANGUAGE.SHELL,
|
||||
value: `$ ${stripCodeFences(checkMeta.remediation.code.cli)}`,
|
||||
copyValue: stripCodeFences(
|
||||
checkMeta.remediation.code.cli,
|
||||
@@ -753,6 +789,7 @@ export function ResourceDetailDrawerContent({
|
||||
<div className="flex flex-col gap-1">
|
||||
{renderRemediationCodeBlock({
|
||||
label: "Terraform",
|
||||
language: QUERY_EDITOR_LANGUAGE.HCL,
|
||||
value: stripCodeFences(
|
||||
checkMeta.remediation.code.terraform,
|
||||
),
|
||||
@@ -760,10 +797,11 @@ export function ResourceDetailDrawerContent({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{checkMeta.remediation.code.nativeiac && (
|
||||
{checkMeta.remediation.code.nativeiac && f && (
|
||||
<div className="flex flex-col gap-1">
|
||||
{renderRemediationCodeBlock({
|
||||
label: "CloudFormation",
|
||||
label: nativeIacConfig.label,
|
||||
language: nativeIacConfig.language,
|
||||
value: stripCodeFences(
|
||||
checkMeta.remediation.code.nativeiac,
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import {
|
||||
HighlightStyle,
|
||||
StreamLanguage,
|
||||
type StringStream,
|
||||
syntaxHighlighting,
|
||||
} from "@codemirror/language";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
@@ -20,12 +21,16 @@ import { type HTMLAttributes, useState } from "react";
|
||||
import { Badge } from "@/components/shadcn";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const QUERY_EDITOR_LANGUAGE = {
|
||||
export const QUERY_EDITOR_LANGUAGE = {
|
||||
OPEN_CYPHER: "openCypher",
|
||||
PLAIN_TEXT: "plainText",
|
||||
SHELL: "shell",
|
||||
HCL: "hcl",
|
||||
BICEP: "bicep",
|
||||
YAML: "yaml",
|
||||
} as const;
|
||||
|
||||
type QueryEditorLanguage =
|
||||
export type QueryEditorLanguage =
|
||||
(typeof QUERY_EDITOR_LANGUAGE)[keyof typeof QUERY_EDITOR_LANGUAGE];
|
||||
|
||||
const OPEN_CYPHER_KEYWORDS = new Set([
|
||||
@@ -98,11 +103,204 @@ const OPEN_CYPHER_FUNCTIONS = new Set([
|
||||
"type",
|
||||
]);
|
||||
|
||||
const SHELL_KEYWORDS = new Set([
|
||||
"if",
|
||||
"then",
|
||||
"else",
|
||||
"elif",
|
||||
"fi",
|
||||
"for",
|
||||
"in",
|
||||
"do",
|
||||
"done",
|
||||
"while",
|
||||
"until",
|
||||
"case",
|
||||
"esac",
|
||||
"function",
|
||||
"return",
|
||||
"export",
|
||||
"local",
|
||||
"readonly",
|
||||
"declare",
|
||||
"typeset",
|
||||
"unset",
|
||||
"shift",
|
||||
"break",
|
||||
"continue",
|
||||
"select",
|
||||
"time",
|
||||
"trap",
|
||||
]);
|
||||
|
||||
const SHELL_COMMANDS = new Set([
|
||||
"aws",
|
||||
"az",
|
||||
"gcloud",
|
||||
"kubectl",
|
||||
"terraform",
|
||||
"echo",
|
||||
"grep",
|
||||
"sed",
|
||||
"awk",
|
||||
"curl",
|
||||
"wget",
|
||||
"chmod",
|
||||
"chown",
|
||||
"mkdir",
|
||||
"rm",
|
||||
"cp",
|
||||
"mv",
|
||||
"cat",
|
||||
"ls",
|
||||
]);
|
||||
|
||||
const HCL_KEYWORDS = new Set([
|
||||
"resource",
|
||||
"data",
|
||||
"variable",
|
||||
"output",
|
||||
"locals",
|
||||
"module",
|
||||
"provider",
|
||||
"terraform",
|
||||
"backend",
|
||||
"required_providers",
|
||||
"dynamic",
|
||||
"for_each",
|
||||
"count",
|
||||
"depends_on",
|
||||
"lifecycle",
|
||||
"provisioner",
|
||||
"connection",
|
||||
]);
|
||||
|
||||
const HCL_FUNCTIONS = new Set([
|
||||
"lookup",
|
||||
"merge",
|
||||
"join",
|
||||
"split",
|
||||
"length",
|
||||
"element",
|
||||
"concat",
|
||||
"format",
|
||||
"replace",
|
||||
"regex",
|
||||
"tolist",
|
||||
"tomap",
|
||||
"toset",
|
||||
"try",
|
||||
"can",
|
||||
"file",
|
||||
"templatefile",
|
||||
"jsonencode",
|
||||
"jsondecode",
|
||||
"yamlencode",
|
||||
"yamldecode",
|
||||
"base64encode",
|
||||
"base64decode",
|
||||
"md5",
|
||||
"sha256",
|
||||
"cidrsubnet",
|
||||
"cidrhost",
|
||||
]);
|
||||
|
||||
const BICEP_KEYWORDS = new Set([
|
||||
"resource",
|
||||
"module",
|
||||
"param",
|
||||
"var",
|
||||
"output",
|
||||
"type",
|
||||
"metadata",
|
||||
"import",
|
||||
"using",
|
||||
"extension",
|
||||
"targetScope",
|
||||
"existing",
|
||||
"if",
|
||||
"for",
|
||||
"in",
|
||||
"true",
|
||||
"false",
|
||||
"null",
|
||||
]);
|
||||
|
||||
const BICEP_DECORATORS = new Set([
|
||||
"description",
|
||||
"secure",
|
||||
"minLength",
|
||||
"maxLength",
|
||||
"minValue",
|
||||
"maxValue",
|
||||
"allowed",
|
||||
"metadata",
|
||||
"batchSize",
|
||||
"sys",
|
||||
]);
|
||||
|
||||
const BICEP_FUNCTIONS = new Set([
|
||||
"concat",
|
||||
"format",
|
||||
"toLower",
|
||||
"toUpper",
|
||||
"substring",
|
||||
"replace",
|
||||
"split",
|
||||
"join",
|
||||
"length",
|
||||
"contains",
|
||||
"empty",
|
||||
"first",
|
||||
"last",
|
||||
"indexOf",
|
||||
"array",
|
||||
"union",
|
||||
"intersection",
|
||||
"resourceId",
|
||||
"subscriptionResourceId",
|
||||
"tenantResourceId",
|
||||
"reference",
|
||||
"listKeys",
|
||||
"listAccountSas",
|
||||
"uniqueString",
|
||||
"guid",
|
||||
"base64",
|
||||
"uri",
|
||||
"environment",
|
||||
"subscription",
|
||||
"resourceGroup",
|
||||
"tenant",
|
||||
]);
|
||||
|
||||
interface OpenCypherParserState {
|
||||
inBlockComment: boolean;
|
||||
inString: "'" | '"' | null;
|
||||
}
|
||||
|
||||
interface ShellParserState {
|
||||
inString: "'" | '"' | null;
|
||||
}
|
||||
|
||||
interface HclParserState {
|
||||
inBlockComment: boolean;
|
||||
inString: '"' | null;
|
||||
expectBlockType: boolean;
|
||||
heredocTerminator: string | null;
|
||||
}
|
||||
|
||||
interface BicepParserState {
|
||||
inBlockComment: boolean;
|
||||
inString: "'" | null;
|
||||
expectResourceType: boolean;
|
||||
}
|
||||
|
||||
interface YamlParserState {
|
||||
inString: "'" | '"' | null;
|
||||
inBlockScalar: boolean;
|
||||
blockScalarIndent: number;
|
||||
}
|
||||
|
||||
const openCypherLanguage = StreamLanguage.define<OpenCypherParserState>({
|
||||
startState() {
|
||||
return {
|
||||
@@ -216,6 +414,595 @@ const openCypherLanguage = StreamLanguage.define<OpenCypherParserState>({
|
||||
},
|
||||
});
|
||||
|
||||
const shellLanguage = StreamLanguage.define<ShellParserState>({
|
||||
startState() {
|
||||
return {
|
||||
inString: null,
|
||||
};
|
||||
},
|
||||
token(stream, state) {
|
||||
if (state.inString) {
|
||||
let escaped = false;
|
||||
|
||||
while (!stream.eol()) {
|
||||
const next = stream.next();
|
||||
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (next === "\\" && state.inString === '"') {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (next === state.inString) {
|
||||
state.inString = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (stream.eol()) {
|
||||
state.inString = null;
|
||||
}
|
||||
|
||||
return "string";
|
||||
}
|
||||
|
||||
if (stream.eatSpace()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (stream.peek() === "#") {
|
||||
stream.skipToEnd();
|
||||
return "comment";
|
||||
}
|
||||
|
||||
if (stream.match(/\$\([^)]+\)/)) {
|
||||
return "variableName";
|
||||
}
|
||||
|
||||
if (stream.match(/\$\{[A-Za-z_][\w]*\}/)) {
|
||||
return "variableName";
|
||||
}
|
||||
|
||||
if (stream.match(/\$[A-Za-z_][\w]*/)) {
|
||||
return "variableName";
|
||||
}
|
||||
|
||||
const quote = stream.peek();
|
||||
if (quote === "'" || quote === '"') {
|
||||
state.inString = quote;
|
||||
stream.next();
|
||||
return "string";
|
||||
}
|
||||
|
||||
if (stream.match(/--[A-Za-z0-9][\w-]*/)) {
|
||||
return "operator";
|
||||
}
|
||||
|
||||
if (stream.match(/-[A-Za-z0-9]+/)) {
|
||||
return "operator";
|
||||
}
|
||||
|
||||
if (stream.match(/\|\||&&|>>|[|><;]/)) {
|
||||
return "operator";
|
||||
}
|
||||
|
||||
if (stream.match(/\d+(?:\.\d+)?/)) {
|
||||
return "number";
|
||||
}
|
||||
|
||||
if (stream.match(/[A-Za-z_][\w-]*/)) {
|
||||
const currentValue = stream.current();
|
||||
const normalizedValue = currentValue.toLowerCase();
|
||||
|
||||
if (SHELL_KEYWORDS.has(normalizedValue)) {
|
||||
return "keyword";
|
||||
}
|
||||
|
||||
if (SHELL_COMMANDS.has(normalizedValue)) {
|
||||
return "function";
|
||||
}
|
||||
|
||||
return "variableName";
|
||||
}
|
||||
|
||||
stream.next();
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
const hclLanguage = StreamLanguage.define<HclParserState>({
|
||||
startState() {
|
||||
return {
|
||||
inBlockComment: false,
|
||||
inString: null,
|
||||
expectBlockType: false,
|
||||
heredocTerminator: null,
|
||||
};
|
||||
},
|
||||
token(stream, state) {
|
||||
if (state.heredocTerminator) {
|
||||
// Match the closing terminator on its own line, including indented <<-EOF forms.
|
||||
if (
|
||||
stream.sol() &&
|
||||
stream.match(new RegExp(`^\\s*${state.heredocTerminator}\\s*$`))
|
||||
) {
|
||||
state.heredocTerminator = null;
|
||||
return "keyword";
|
||||
}
|
||||
|
||||
stream.skipToEnd();
|
||||
return "string";
|
||||
}
|
||||
|
||||
if (state.inBlockComment) {
|
||||
while (!stream.eol()) {
|
||||
if (stream.match("*/")) {
|
||||
state.inBlockComment = false;
|
||||
break;
|
||||
}
|
||||
|
||||
stream.next();
|
||||
}
|
||||
|
||||
return "comment";
|
||||
}
|
||||
|
||||
if (state.inString) {
|
||||
let escaped = false;
|
||||
|
||||
while (!stream.eol()) {
|
||||
const next = stream.next();
|
||||
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (next === "\\") {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (next === state.inString) {
|
||||
state.inString = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (stream.eol()) {
|
||||
state.inString = null;
|
||||
}
|
||||
|
||||
return "string";
|
||||
}
|
||||
|
||||
if (stream.eatSpace()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (stream.match("#") || stream.match("//")) {
|
||||
stream.skipToEnd();
|
||||
return "comment";
|
||||
}
|
||||
|
||||
if (stream.match("/*")) {
|
||||
state.inBlockComment = true;
|
||||
return "comment";
|
||||
}
|
||||
|
||||
if (stream.match(/\$\{[^}]+\}/)) {
|
||||
return "variableName";
|
||||
}
|
||||
|
||||
if (stream.peek() === '"') {
|
||||
if (state.expectBlockType) {
|
||||
state.expectBlockType = false;
|
||||
stream.next(); // opening "
|
||||
while (!stream.eol()) {
|
||||
const ch = stream.next();
|
||||
if (ch === "\\") {
|
||||
stream.next(); // skip escaped char
|
||||
} else if (ch === '"') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return "typeName";
|
||||
}
|
||||
|
||||
state.inString = '"';
|
||||
stream.next();
|
||||
return "string";
|
||||
}
|
||||
|
||||
if (stream.match(/[{}\[\]()]/)) {
|
||||
return "punctuation";
|
||||
}
|
||||
|
||||
// Heredoc (<<EOF, <<-EOF) — must be before generic operator matcher
|
||||
if (stream.match(/<<-?([A-Za-z_][\w]*)/)) {
|
||||
state.heredocTerminator = stream.current().replace(/^<<-?/, "");
|
||||
return "keyword";
|
||||
}
|
||||
|
||||
if (stream.match(/=>|\.\.\.|==|!=|>=|<=|[=><?:]/)) {
|
||||
return "operator";
|
||||
}
|
||||
|
||||
if (stream.match(/\b(?:true|false)\b/)) {
|
||||
return "keyword";
|
||||
}
|
||||
|
||||
if (stream.match(/\d+(?:\.\d+)?/)) {
|
||||
return "number";
|
||||
}
|
||||
|
||||
if (stream.match(/[A-Za-z_][\w-]*/)) {
|
||||
const currentValue = stream.current();
|
||||
const normalizedValue = currentValue.toLowerCase();
|
||||
|
||||
if (HCL_KEYWORDS.has(normalizedValue)) {
|
||||
state.expectBlockType =
|
||||
normalizedValue === "resource" || normalizedValue === "data";
|
||||
return "keyword";
|
||||
}
|
||||
|
||||
if (
|
||||
HCL_FUNCTIONS.has(normalizedValue) &&
|
||||
stream.match(/\s*(?=\()/, false)
|
||||
) {
|
||||
return "function";
|
||||
}
|
||||
|
||||
if (state.expectBlockType) {
|
||||
state.expectBlockType = false;
|
||||
return "typeName";
|
||||
}
|
||||
|
||||
if (stream.match(/\s*(?==)/, false)) {
|
||||
return "propertyName";
|
||||
}
|
||||
|
||||
return "variableName";
|
||||
}
|
||||
|
||||
stream.next();
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
const yamlLanguage = StreamLanguage.define<YamlParserState>({
|
||||
startState() {
|
||||
return {
|
||||
inString: null,
|
||||
inBlockScalar: false,
|
||||
blockScalarIndent: 0,
|
||||
};
|
||||
},
|
||||
token(stream, state) {
|
||||
// Block scalar continuation (| or > multiline strings)
|
||||
if (state.inBlockScalar) {
|
||||
// Blank lines are always part of a block scalar.
|
||||
if (stream.match(/^\s*$/)) {
|
||||
stream.skipToEnd();
|
||||
return "string";
|
||||
}
|
||||
|
||||
const indent = stream.indentation();
|
||||
|
||||
if (indent > state.blockScalarIndent) {
|
||||
stream.skipToEnd();
|
||||
return "string";
|
||||
}
|
||||
|
||||
state.inBlockScalar = false;
|
||||
state.blockScalarIndent = 0;
|
||||
}
|
||||
|
||||
// Continue quoted strings across tokens
|
||||
if (state.inString) {
|
||||
let escaped = false;
|
||||
|
||||
while (!stream.eol()) {
|
||||
const next = stream.next();
|
||||
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (next === "\\" && state.inString === '"') {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (next === state.inString) {
|
||||
state.inString = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return "string";
|
||||
}
|
||||
|
||||
if (stream.eatSpace()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Comments
|
||||
if (stream.peek() === "#") {
|
||||
stream.skipToEnd();
|
||||
return "comment";
|
||||
}
|
||||
|
||||
// Document markers
|
||||
if (stream.sol() && (stream.match("---") || stream.match("..."))) {
|
||||
return "keyword";
|
||||
}
|
||||
|
||||
// Anchors & aliases
|
||||
if (stream.match(/[&*][A-Za-z_][\w]*/)) {
|
||||
return "variableName";
|
||||
}
|
||||
|
||||
// CloudFormation intrinsic tags (!Ref, !Sub, !GetAtt, etc.)
|
||||
if (stream.match(/![A-Za-z][A-Za-z0-9]*/)) {
|
||||
return "typeName";
|
||||
}
|
||||
|
||||
// Tags (!!str, !!map, etc.)
|
||||
if (stream.match(/!![A-Za-z]+/)) {
|
||||
return "typeName";
|
||||
}
|
||||
|
||||
// Quoted strings
|
||||
const quote = stream.peek();
|
||||
if (quote === "'" || quote === '"') {
|
||||
state.inString = quote;
|
||||
stream.next();
|
||||
return "string";
|
||||
}
|
||||
|
||||
// Block scalar indicators (| or >)
|
||||
if (
|
||||
stream.sol() === false &&
|
||||
(stream.peek() === "|" || stream.peek() === ">")
|
||||
) {
|
||||
const prevChar = stream.string.charAt(stream.pos - 1);
|
||||
|
||||
if (prevChar === " " || prevChar === ":") {
|
||||
stream.next();
|
||||
// Eat optional modifiers like |-, |+, |2
|
||||
stream.match(/[-+]?\d?/);
|
||||
state.inBlockScalar = true;
|
||||
state.blockScalarIndent = stream.indentation();
|
||||
return "operator";
|
||||
}
|
||||
}
|
||||
|
||||
// List item marker
|
||||
if (stream.match(/^-(?=\s)/)) {
|
||||
return "punctuation";
|
||||
}
|
||||
|
||||
// Key: value pattern — supports CloudFormation long-form intrinsics (Fn::Sub:)
|
||||
if (stream.match(/[A-Za-z_][\w./-]*(?:::[A-Za-z_][\w]*)*(?=\s*:(?!:))/)) {
|
||||
return "propertyName";
|
||||
}
|
||||
|
||||
// Booleans & null (YAML spec values)
|
||||
if (stream.match(/\b(?:true|false|yes|no|on|off|null)\b/i)) {
|
||||
return "keyword";
|
||||
}
|
||||
|
||||
// Numbers (integers, floats, hex, octal)
|
||||
if (
|
||||
stream.match(
|
||||
/^[-+]?(?:0x[0-9a-fA-F]+|0o[0-7]+|0b[01]+|\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)/,
|
||||
)
|
||||
) {
|
||||
return "number";
|
||||
}
|
||||
|
||||
// Colon separator
|
||||
if (stream.match(":")) {
|
||||
return "punctuation";
|
||||
}
|
||||
|
||||
// Braces/brackets (flow style)
|
||||
if (stream.match(/[{}\[\],]/)) {
|
||||
return "punctuation";
|
||||
}
|
||||
|
||||
// Tilde (null alias)
|
||||
if (stream.match("~")) {
|
||||
return "keyword";
|
||||
}
|
||||
|
||||
// Unquoted strings / values — consume word
|
||||
if (stream.match(/[^\s#:,\[\]{}]+/)) {
|
||||
return "string";
|
||||
}
|
||||
|
||||
stream.next();
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
function readBicepStringSegment(
|
||||
stream: StringStream,
|
||||
includeOpeningQuote = false,
|
||||
) {
|
||||
if (includeOpeningQuote) {
|
||||
stream.next();
|
||||
}
|
||||
|
||||
while (!stream.eol()) {
|
||||
if (stream.match("${")) {
|
||||
stream.backUp(2);
|
||||
break;
|
||||
}
|
||||
|
||||
const next = stream.next();
|
||||
|
||||
if (next === "'" && stream.peek() === "'") {
|
||||
stream.next();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (next === "'") {
|
||||
stream.backUp(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bicepLanguage = StreamLanguage.define<BicepParserState>({
|
||||
startState() {
|
||||
return {
|
||||
inBlockComment: false,
|
||||
inString: null,
|
||||
expectResourceType: false,
|
||||
};
|
||||
},
|
||||
token(stream, state) {
|
||||
if (state.inBlockComment) {
|
||||
while (!stream.eol()) {
|
||||
if (stream.match("*/")) {
|
||||
state.inBlockComment = false;
|
||||
break;
|
||||
}
|
||||
|
||||
stream.next();
|
||||
}
|
||||
|
||||
return "comment";
|
||||
}
|
||||
|
||||
if (state.inString) {
|
||||
if (stream.match("${")) {
|
||||
let depth = 1;
|
||||
|
||||
while (!stream.eol() && depth > 0) {
|
||||
const next = stream.next();
|
||||
|
||||
if (next === "{") {
|
||||
depth += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (next === "}") {
|
||||
depth -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
return "variableName";
|
||||
}
|
||||
|
||||
readBicepStringSegment(stream);
|
||||
|
||||
if (stream.peek() !== "'") {
|
||||
return "string";
|
||||
}
|
||||
|
||||
if (stream.peek() === "'") {
|
||||
state.inString = null;
|
||||
stream.next();
|
||||
}
|
||||
|
||||
return "string";
|
||||
}
|
||||
|
||||
if (stream.eatSpace()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (stream.match("//")) {
|
||||
stream.skipToEnd();
|
||||
return "comment";
|
||||
}
|
||||
|
||||
if (stream.match("/*")) {
|
||||
state.inBlockComment = true;
|
||||
return "comment";
|
||||
}
|
||||
|
||||
if (stream.match(/@[A-Za-z_][\w]*/)) {
|
||||
const decorator = stream.current().slice(1);
|
||||
|
||||
if (BICEP_DECORATORS.has(decorator)) {
|
||||
return "keyword";
|
||||
}
|
||||
|
||||
return "keyword";
|
||||
}
|
||||
|
||||
if (stream.peek() === "'") {
|
||||
if (state.expectResourceType) {
|
||||
state.expectResourceType = false;
|
||||
// Consume the full quoted resource type including both quotes
|
||||
stream.next(); // opening '
|
||||
while (!stream.eol()) {
|
||||
const ch = stream.next();
|
||||
if (ch === "'" && stream.peek() === "'") {
|
||||
stream.next(); // escaped ''
|
||||
continue;
|
||||
}
|
||||
if (ch === "'") {
|
||||
break; // closing '
|
||||
}
|
||||
}
|
||||
return "typeName";
|
||||
}
|
||||
|
||||
stream.next(); // consume opening '
|
||||
state.inString = "'";
|
||||
return "string";
|
||||
}
|
||||
|
||||
if (stream.match(/[A-Za-z_][\w-]*(?=\s*:)/)) {
|
||||
return "propertyName";
|
||||
}
|
||||
|
||||
if (stream.match(/\?\?|==|!=|>=|<=|=|>|<|\?|:|!/)) {
|
||||
return "operator";
|
||||
}
|
||||
|
||||
if (stream.match(/[{}\[\]()]/)) {
|
||||
return "punctuation";
|
||||
}
|
||||
|
||||
if (stream.match(/-?\d+(?:\.\d+)?/)) {
|
||||
return "number";
|
||||
}
|
||||
|
||||
if (stream.match(/[A-Za-z_][\w]*/)) {
|
||||
const currentValue = stream.current();
|
||||
|
||||
if (BICEP_KEYWORDS.has(currentValue)) {
|
||||
state.expectResourceType =
|
||||
currentValue === "resource" || currentValue === "module";
|
||||
return "keyword";
|
||||
}
|
||||
|
||||
if (
|
||||
BICEP_FUNCTIONS.has(currentValue) &&
|
||||
stream.match(/\s*(?=\()/, false)
|
||||
) {
|
||||
return "function";
|
||||
}
|
||||
|
||||
return "variableName";
|
||||
}
|
||||
|
||||
stream.next();
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
const lightHighlightStyle = HighlightStyle.define([
|
||||
{ tag: tags.keyword, color: "#0550ae", fontWeight: "600" },
|
||||
{ tag: tags.string, color: "#0a3069" },
|
||||
@@ -380,6 +1167,14 @@ export const QueryCodeEditor = ({
|
||||
openCypherLanguage,
|
||||
syntaxHighlighting(editorHighlightStyle),
|
||||
);
|
||||
} else if (language === QUERY_EDITOR_LANGUAGE.SHELL) {
|
||||
extensions.push(shellLanguage, syntaxHighlighting(editorHighlightStyle));
|
||||
} else if (language === QUERY_EDITOR_LANGUAGE.HCL) {
|
||||
extensions.push(hclLanguage, syntaxHighlighting(editorHighlightStyle));
|
||||
} else if (language === QUERY_EDITOR_LANGUAGE.BICEP) {
|
||||
extensions.push(bicepLanguage, syntaxHighlighting(editorHighlightStyle));
|
||||
} else if (language === QUERY_EDITOR_LANGUAGE.YAML) {
|
||||
extensions.push(yamlLanguage, syntaxHighlighting(editorHighlightStyle));
|
||||
}
|
||||
|
||||
const handleCopy = async () => {
|
||||
|
||||
Reference in New Issue
Block a user