Skip to content

Commit 4f56a37

Browse files
zamoorealex-ju
andauthored
Hds::CodeEditor - Support Sentinel language (#2660)
Co-authored-by: Alex <alex-ju@users.noreply.github.com>
1 parent 96b94b6 commit 4f56a37

File tree

5 files changed

+232
-0
lines changed

5 files changed

+232
-0
lines changed

packages/components/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@
329329
"./modifiers/hds-clipboard.js": "./dist/_app_/modifiers/hds-clipboard.js",
330330
"./modifiers/hds-code-editor.js": "./dist/_app_/modifiers/hds-code-editor.js",
331331
"./modifiers/hds-code-editor/highlight-styles/hds-dark-highlight-style.js": "./dist/_app_/modifiers/hds-code-editor/highlight-styles/hds-dark-highlight-style.js",
332+
"./modifiers/hds-code-editor/languages/sentinel.js": "./dist/_app_/modifiers/hds-code-editor/languages/sentinel.js",
332333
"./modifiers/hds-code-editor/palettes/hds-dark-palette.js": "./dist/_app_/modifiers/hds-code-editor/palettes/hds-dark-palette.js",
333334
"./modifiers/hds-code-editor/themes/hds-dark-theme.js": "./dist/_app_/modifiers/hds-code-editor/themes/hds-dark-theme.js",
334335
"./modifiers/hds-code-editor/types.js": "./dist/_app_/modifiers/hds-code-editor/types.js",

packages/components/src/modifiers/hds-code-editor.ts

+8
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ const LANGUAGES: Record<
5656
return defineStreamLanguage(ruby);
5757
},
5858
},
59+
sentinel: {
60+
load: async () => {
61+
const { sentinel } = await import(
62+
'./hds-code-editor/languages/sentinel.ts'
63+
);
64+
return defineStreamLanguage(sentinel);
65+
},
66+
},
5967
shell: {
6068
load: async () => {
6169
const { shell } = await import('@codemirror/legacy-modes/mode/shell');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import type { StringStream } from '@codemirror/language';
2+
3+
type Quote = '"' | "'";
4+
5+
interface ParserConfig {
6+
delimiters?: RegExp;
7+
operators?: RegExp;
8+
}
9+
10+
interface SentinelState {
11+
tokenize: (stream: StringStream, state: SentinelState) => string | null;
12+
}
13+
14+
function wordRegexp(words: string[]): RegExp {
15+
return new RegExp('^((' + words.join(')|(') + '))\\b');
16+
}
17+
18+
// logical operators
19+
const wordOperators = wordRegexp([
20+
'and',
21+
'contains',
22+
'else',
23+
'in',
24+
'is',
25+
'matches',
26+
'not',
27+
'or',
28+
'xor',
29+
]);
30+
31+
// keywords
32+
const sentinelKeywords = [
33+
'all',
34+
'any',
35+
'as',
36+
'break',
37+
'case',
38+
'continue',
39+
'default',
40+
'else',
41+
'empty',
42+
'filter',
43+
'for',
44+
'func',
45+
'if',
46+
'import',
47+
'map',
48+
'param',
49+
'return',
50+
'rule',
51+
'when',
52+
];
53+
54+
// built-ins / constants
55+
const sentinelBuiltins = [
56+
'true',
57+
'false',
58+
'null',
59+
'undefined',
60+
'length',
61+
'append',
62+
'delete',
63+
'keys',
64+
'values',
65+
'range',
66+
'print',
67+
'int',
68+
'float',
69+
'string',
70+
'bool',
71+
];
72+
73+
const keywords = wordRegexp(sentinelKeywords);
74+
const builtins = wordRegexp(sentinelBuiltins);
75+
76+
export function mkSentinel(parserConf: ParserConfig) {
77+
const ERRORCLASS = 'error';
78+
79+
// delimiters, operators, etc.
80+
const delimiters = parserConf.delimiters ?? /^[()[\]{},:;=.]/;
81+
const operators = [
82+
parserConf.operators ?? /^(\+|-|\*|\/|%|<=|>=|<|>|==|!=|!|&&|\|\|)/,
83+
];
84+
85+
// tokenizer
86+
function tokenBase(
87+
stream: StringStream,
88+
state: SentinelState
89+
): string | null {
90+
return tokenBaseInner(stream, state);
91+
}
92+
93+
function tokenBaseInner(
94+
stream: StringStream,
95+
state: SentinelState
96+
): string | null {
97+
if (stream.eatSpace()) {
98+
return null;
99+
}
100+
101+
// comments
102+
// single-line `//`
103+
if (stream.match('//')) {
104+
stream.skipToEnd();
105+
return 'comment';
106+
}
107+
// multi-line `/* ... */`
108+
if (stream.match('/*')) {
109+
state.tokenize = tokenComment;
110+
return state.tokenize(stream, state);
111+
}
112+
113+
// strings
114+
if (stream.match(/"/) || stream.match(/'/)) {
115+
// We’ve just consumed either " or '
116+
const quote = stream.current();
117+
state.tokenize = tokenString(quote as Quote);
118+
return state.tokenize(stream, state);
119+
}
120+
121+
// numbers
122+
if (stream.match(/^[0-9]+(\.[0-9]+)?/)) {
123+
return 'number';
124+
}
125+
126+
// operators
127+
for (let i = 0; i < operators.length; i++) {
128+
if (stream.match(operators[i]!)) {
129+
return 'operator';
130+
}
131+
}
132+
133+
// delimiters/punctuation
134+
if (stream.match(delimiters)) {
135+
return 'punctuation';
136+
}
137+
138+
// keywords, operators, builtins
139+
if (stream.match(keywords) || stream.match(wordOperators)) {
140+
return 'keyword';
141+
}
142+
if (stream.match(builtins)) {
143+
return 'builtin';
144+
}
145+
146+
// identifiers (variables, function names, etc.)
147+
if (stream.match(/^[_A-Za-z][_A-Za-z0-9]*/)) {
148+
return 'variable';
149+
}
150+
151+
// if nothing matched, consume one character and mark it as error
152+
stream.next();
153+
154+
return ERRORCLASS;
155+
}
156+
157+
// multi-line comment tokenizer
158+
function tokenComment(stream: StringStream, state: SentinelState): string {
159+
while (!stream.eol()) {
160+
if (stream.match('*/')) {
161+
state.tokenize = tokenBase;
162+
break;
163+
}
164+
stream.next();
165+
}
166+
return 'comment';
167+
}
168+
169+
// string tokenizer factory
170+
function tokenString(
171+
quote: Quote
172+
): (stream: StringStream, state: SentinelState) => string {
173+
return function (stream: StringStream, state: SentinelState) {
174+
let escaped = false;
175+
let ch = null;
176+
177+
while ((ch = stream.next()) != null) {
178+
if (ch === quote && !escaped) {
179+
// end of string
180+
state.tokenize = tokenBase;
181+
break;
182+
}
183+
escaped = !escaped && ch === '\\';
184+
}
185+
186+
return 'string';
187+
};
188+
}
189+
190+
// CodeMirror API
191+
return {
192+
name: 'sentinel',
193+
194+
startState: function (): SentinelState {
195+
return {
196+
tokenize: tokenBase,
197+
};
198+
},
199+
200+
token: function (
201+
stream: StringStream,
202+
state: SentinelState
203+
): string | null {
204+
return state.tokenize(stream, state);
205+
},
206+
207+
languageData: {
208+
commentTokens: { line: '//', block: { open: '/*', close: '*/' } },
209+
closeBrackets: { brackets: ['(', '[', '{', '"', "'"] },
210+
},
211+
};
212+
}
213+
214+
export const sentinel = mkSentinel({});

packages/components/src/modifiers/hds-code-editor/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export enum HdsCodeEditorLanguageValues {
99
Go = 'go',
1010
Hcl = 'hcl',
1111
Json = 'json',
12+
Sentinel = 'sentinel',
1213
Sql = 'sql',
1314
Yaml = 'yaml',
1415
}

showcase/app/controllers/components/code-editor.js

+8
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ func main() {
6868
"status": "success",
6969
"data": null
7070
}`,
71+
},
72+
{
73+
value: 'sentinel',
74+
label: 'Sentinel',
75+
code: `param allowed_regions = ["us-east-1", "us-west-2"]
76+
77+
main = rule { all tfplan.resources[*].instances as r { r.attributes.region in allowed_regions } }
78+
`,
7179
},
7280
{
7381
value: 'sql',

0 commit comments

Comments
 (0)