@purview-dev/astroc4 1.0.0-prerelease.1 → 1.0.0-prerelease.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +9 -9
- package/package.json +1 -1
- package/src/grammar/langium.mjs +559 -559
- package/src/grammar/source.mjs +410 -410
- package/src/grammar/textmate.mjs +439 -439
package/src/grammar/textmate.mjs
CHANGED
|
@@ -1,439 +1,439 @@
|
|
|
1
|
-
// textmate.mjs
|
|
2
|
-
// path: src/grammar/textmate.mjs
|
|
3
|
-
// Builds the Shiki / TextMate grammar for LikeC4 out of the vocabulary read
|
|
4
|
-
// from the live Langium grammar (./langium.mjs).
|
|
5
|
-
//
|
|
6
|
-
// Two layers:
|
|
7
|
-
// - generated — every word list: blocks, keywords, property names and closed
|
|
8
|
-
// value domains. These are exactly the lists that go stale when the
|
|
9
|
-
// language grows (`agent` was added to `ElementShape` after v1.59.4).
|
|
10
|
-
// - structural — how LikeC4 source is *shaped* rather than which words it
|
|
11
|
-
// uses: comments, quoted and triple-quoted strings, `#tag` vs `#hex`,
|
|
12
|
-
// `tech:astro` icons, `-[https]->` arrows, `element system` declarations.
|
|
13
|
-
// These are curated here because a Langium rule does not say how a token
|
|
14
|
-
// should look, only that it is one.
|
|
15
|
-
//
|
|
16
|
-
// The result must stay plain JSON: Expressive Code JSON-round-trips integration
|
|
17
|
-
// options before the `<Code>` component renders them, and throws on functions
|
|
18
|
-
// or RegExp instances.
|
|
19
|
-
|
|
20
|
-
/** Scope name of the grammar, as upstream's VS Code extension uses it. */
|
|
21
|
-
export const SCOPE_NAME = 'source.likec4';
|
|
22
|
-
|
|
23
|
-
/** Shiki language id, plus the short alias used by ` ```c4 ` fences. */
|
|
24
|
-
export const LANGUAGE_NAME = 'likec4';
|
|
25
|
-
export const LANGUAGE_ALIASES = ['c4'];
|
|
26
|
-
|
|
27
|
-
const TMLANGUAGE_SCHEMA =
|
|
28
|
-
'https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json';
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Closed value domains: they colour as values, not as keywords.
|
|
32
|
-
* Anything the grammar adds under these names is picked up automatically.
|
|
33
|
-
*/
|
|
34
|
-
const VALUE_DOMAINS = [
|
|
35
|
-
'BorderStyleValue',
|
|
36
|
-
'DynamicViewDisplayVariantValue',
|
|
37
|
-
'EdgeRoutingValue',
|
|
38
|
-
'ElementShape',
|
|
39
|
-
'IconPositionValue',
|
|
40
|
-
'LineOptions',
|
|
41
|
-
'ArrowType',
|
|
42
|
-
'RankValue',
|
|
43
|
-
'SizeValue',
|
|
44
|
-
'SizingValue',
|
|
45
|
-
'ThemeColor',
|
|
46
|
-
'ViewLayoutDirection',
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
/** Control-flow keywords read as keywords, not as a value domain. */
|
|
50
|
-
const KEYWORD_DOMAINS = ['DynamicViewFlowKeyword'];
|
|
51
|
-
|
|
52
|
-
/** `source` / `target` are the only value-like variables. */
|
|
53
|
-
const VARIABLE_DOMAINS = ['Participant'];
|
|
54
|
-
|
|
55
|
-
/** Operators whose scope is a word, kept curated: they are not all literals. */
|
|
56
|
-
const OPERATOR_WORDS = ['and', 'or', 'not', 'is'];
|
|
57
|
-
|
|
58
|
-
/** Used when the grammar cannot be read — never expected, never silent. */
|
|
59
|
-
const FALLBACK_ICON_PACKS = ['aws', 'azure', 'bootstrap', 'gcp', 'tech'];
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* @typedef {object} TmPattern
|
|
63
|
-
* @property {string} [name]
|
|
64
|
-
* @property {string} [comment]
|
|
65
|
-
* @property {string} [include]
|
|
66
|
-
* @property {string} [match]
|
|
67
|
-
* @property {string} [begin]
|
|
68
|
-
* @property {string} [end]
|
|
69
|
-
* @property {string} [contentName]
|
|
70
|
-
* @property {Record<string, { name: string }>} [captures]
|
|
71
|
-
* @property {Record<string, { name: string }>} [beginCaptures]
|
|
72
|
-
* @property {Record<string, { name: string }>} [endCaptures]
|
|
73
|
-
* @property {TmPattern[]} [patterns]
|
|
74
|
-
*/
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* A TextMate grammar: JSON all the way down, never a `RegExp`.
|
|
78
|
-
* @typedef {object} TmGrammar
|
|
79
|
-
* @property {string} $schema
|
|
80
|
-
* @property {string} name
|
|
81
|
-
* @property {string} displayName
|
|
82
|
-
* @property {string} scopeName
|
|
83
|
-
* @property {string[]} fileTypes
|
|
84
|
-
* @property {TmPattern[]} patterns
|
|
85
|
-
* @property {Record<string, any>} repository
|
|
86
|
-
* @property {{ repo: string, ref: string, sha?: string, by: string }} [$generated]
|
|
87
|
-
*/
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* @param {import('./langium.mjs').Vocabulary} vocabulary
|
|
91
|
-
* @param {{ repo?: string, ref?: string, sha?: string }} [meta] Provenance.
|
|
92
|
-
* @returns {TmGrammar} A TextMate grammar, JSON-serialisable.
|
|
93
|
-
*/
|
|
94
|
-
export function buildTmLanguage(vocabulary, meta = {}) {
|
|
95
|
-
const control = uniqueDomain(vocabulary, KEYWORD_DOMAINS);
|
|
96
|
-
const variables = uniqueDomain(vocabulary, VARIABLE_DOMAINS);
|
|
97
|
-
const icons = vocabulary.icons.length > 0 ? vocabulary.icons : FALLBACK_ICON_PACKS;
|
|
98
|
-
|
|
99
|
-
// Words that are always syntax, never the name of a kind: `deployed =
|
|
100
|
-
// instanceOf a` must not paint `instanceOf` as a type.
|
|
101
|
-
const reserved = alternationOf([...vocabulary.keywords, ...vocabulary.blocks, ...control]);
|
|
102
|
-
|
|
103
|
-
/** @type {TmGrammar} */
|
|
104
|
-
const grammar = {
|
|
105
|
-
$schema: TMLANGUAGE_SCHEMA,
|
|
106
|
-
...(meta.repo && meta.ref
|
|
107
|
-
? {
|
|
108
|
-
$generated: {
|
|
109
|
-
repo: meta.repo,
|
|
110
|
-
ref: meta.ref,
|
|
111
|
-
...(meta.sha ? { sha: meta.sha } : {}),
|
|
112
|
-
by: 'astroc4/scripts/sync-grammar.mjs',
|
|
113
|
-
},
|
|
114
|
-
}
|
|
115
|
-
: {}),
|
|
116
|
-
name: LANGUAGE_NAME,
|
|
117
|
-
displayName: 'LikeC4',
|
|
118
|
-
scopeName: SCOPE_NAME,
|
|
119
|
-
fileTypes: ['c4', 'likec4'],
|
|
120
|
-
patterns: [{ include: '#everything' }],
|
|
121
|
-
repository: {
|
|
122
|
-
everything: {
|
|
123
|
-
patterns: [
|
|
124
|
-
'comments',
|
|
125
|
-
'strings',
|
|
126
|
-
'urls',
|
|
127
|
-
'icons',
|
|
128
|
-
'hex-colors',
|
|
129
|
-
'tags',
|
|
130
|
-
'typed-arrow',
|
|
131
|
-
'declarations',
|
|
132
|
-
'keywords',
|
|
133
|
-
'numbers',
|
|
134
|
-
'operators',
|
|
135
|
-
'punctuation',
|
|
136
|
-
].map((name) => ({ include: `#${name}` })),
|
|
137
|
-
},
|
|
138
|
-
comments: comments(),
|
|
139
|
-
strings: strings(),
|
|
140
|
-
urls: urls(),
|
|
141
|
-
icons: iconIds(icons),
|
|
142
|
-
'hex-colors': hexColours(),
|
|
143
|
-
tags: tags(),
|
|
144
|
-
'typed-arrow': typedArrow(),
|
|
145
|
-
declarations: declarations(reserved),
|
|
146
|
-
keywords: keywords({ ...vocabulary, control, variables }),
|
|
147
|
-
numbers: numbers(),
|
|
148
|
-
operators: operators(vocabulary.arrows),
|
|
149
|
-
punctuation: punctuation(),
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
if (meta.repo && meta.ref) {
|
|
154
|
-
grammar.$generated = {
|
|
155
|
-
repo: meta.repo,
|
|
156
|
-
ref: meta.ref,
|
|
157
|
-
...(meta.sha ? { sha: meta.sha } : {}),
|
|
158
|
-
by: 'astroc4/scripts/sync-grammar.mjs',
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return grammar;
|
|
163
|
-
}
|
|
164
|
-
// region structural patterns
|
|
165
|
-
|
|
166
|
-
function comments() {
|
|
167
|
-
return {
|
|
168
|
-
patterns: [
|
|
169
|
-
{
|
|
170
|
-
name: 'comment.block.likec4',
|
|
171
|
-
begin: '/\\*',
|
|
172
|
-
end: '\\*/',
|
|
173
|
-
captures: { 0: { name: 'punctuation.definition.comment.likec4' } },
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
name: 'comment.line.double-slash.likec4',
|
|
177
|
-
match: '//.*$',
|
|
178
|
-
captures: { 0: { name: 'punctuation.definition.comment.likec4' } },
|
|
179
|
-
},
|
|
180
|
-
],
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* `'…'`, `"…"`, and the markdown strings `''' … '''` / `""" … """` that
|
|
186
|
-
* LikeC4 uses for descriptions.
|
|
187
|
-
*/
|
|
188
|
-
function strings() {
|
|
189
|
-
const triple = (quote) => ({
|
|
190
|
-
name: 'string.quoted.triple.likec4',
|
|
191
|
-
begin: quote.repeat(3),
|
|
192
|
-
end: quote.repeat(3),
|
|
193
|
-
beginCaptures: { 0: { name: 'punctuation.definition.string.begin.likec4' } },
|
|
194
|
-
endCaptures: { 0: { name: 'punctuation.definition.string.end.likec4' } },
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
const single = (quote, name) => ({
|
|
198
|
-
name,
|
|
199
|
-
begin: quote,
|
|
200
|
-
end: `${quote}|$`,
|
|
201
|
-
beginCaptures: { 0: { name: 'punctuation.definition.string.begin.likec4' } },
|
|
202
|
-
endCaptures: { 0: { name: 'punctuation.definition.string.end.likec4' } },
|
|
203
|
-
patterns: [{ name: 'constant.character.escape.likec4', match: '\\\\.' }],
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
patterns: [
|
|
208
|
-
triple("'"),
|
|
209
|
-
triple('"'),
|
|
210
|
-
single("'", 'string.quoted.single.likec4'),
|
|
211
|
-
single('"', 'string.quoted.double.likec4'),
|
|
212
|
-
],
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function urls() {
|
|
217
|
-
return { name: 'markup.underline.link.likec4', match: "\\bhttps?://[^\\s'\"]+" };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Icon ids (`tech:astro`, `aws:lambda`). The packs come from the `LIB_ICON`
|
|
222
|
-
* terminal, so a pack added upstream highlights without touching this package.
|
|
223
|
-
* @param {string[]} packs
|
|
224
|
-
*/
|
|
225
|
-
function iconIds(packs) {
|
|
226
|
-
return {
|
|
227
|
-
comment: 'Bundled icon ids: `tech:nginx`, `aws:…`, `gcp:…`, …',
|
|
228
|
-
match: `\\b(${alternationOf(packs)})(:)([\\w-]+)`,
|
|
229
|
-
captures: {
|
|
230
|
-
1: { name: 'support.constant.icon-pack.likec4' },
|
|
231
|
-
2: { name: 'punctuation.separator.likec4' },
|
|
232
|
-
3: { name: 'support.constant.icon.likec4' },
|
|
233
|
-
},
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** `#0046BE` is a colour, `#compute` is a tag — hex first, so it wins. */
|
|
238
|
-
function hexColours() {
|
|
239
|
-
return {
|
|
240
|
-
name: 'constant.other.color.rgb-value.hex.likec4',
|
|
241
|
-
match: '#(?:[0-9A-Fa-f]{8}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})(?![\\w-])',
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function tags() {
|
|
246
|
-
return { name: 'entity.name.tag.likec4', match: '#[A-Za-z_][\\w-]*' };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** `a -[https]-> b` — the relationship kind inside the connector. */
|
|
250
|
-
function typedArrow() {
|
|
251
|
-
return {
|
|
252
|
-
comment: '`a -[https]-> b`',
|
|
253
|
-
match: '(-\\[)\\s*([A-Za-z_][\\w-]*)\\s*(\\]->)',
|
|
254
|
-
captures: {
|
|
255
|
-
1: { name: 'keyword.operator.arrow.likec4' },
|
|
256
|
-
2: { name: 'entity.name.type.relationship.likec4' },
|
|
257
|
-
3: { name: 'keyword.operator.arrow.likec4' },
|
|
258
|
-
},
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Declarations: the name being introduced is scoped, the name only being
|
|
264
|
-
* referred to is not. Runs before the keyword patterns so `element system`
|
|
265
|
-
* reads as a declaration rather than as two keywords.
|
|
266
|
-
*
|
|
267
|
-
* @param {string} reserved Alternation of syntax words (`instanceOf`, `extend`,
|
|
268
|
-
* …), so `deployed = instanceOf a` scopes `instanceOf` as a keyword rather
|
|
269
|
-
* than as the element kind the name is being assigned.
|
|
270
|
-
*/
|
|
271
|
-
function declarations(reserved) {
|
|
272
|
-
const notReserved = reserved ? `(?!(?:${reserved})\\b)` : '';
|
|
273
|
-
|
|
274
|
-
return {
|
|
275
|
-
patterns: [
|
|
276
|
-
{
|
|
277
|
-
comment:
|
|
278
|
-
'`element system`, `deploymentNode region`, `tag tls`, `relationship https`. Not `where tag is #x`.',
|
|
279
|
-
match: '\\b(element|deploymentNode|tag|relationship)\\s+(?!is\\b)([A-Za-z_][\\w-]*)(?=\\s*(?:\\{|$|//))',
|
|
280
|
-
captures: {
|
|
281
|
-
1: { name: 'storage.type.likec4' },
|
|
282
|
-
2: { name: 'entity.name.type.likec4' },
|
|
283
|
-
},
|
|
284
|
-
},
|
|
285
|
-
{
|
|
286
|
-
comment:
|
|
287
|
-
'`color testingBlue #0046BE` declares; `color testingBlue` inside a style block only refers.',
|
|
288
|
-
match: '\\b(color)\\s+([A-Za-z_][\\w-]*)(?=\\s+#)',
|
|
289
|
-
captures: {
|
|
290
|
-
1: { name: 'storage.type.likec4' },
|
|
291
|
-
2: { name: 'entity.name.type.likec4' },
|
|
292
|
-
},
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
comment: '`view index`, `dynamic view firstRequest`, `deployment view prod`, `view internals of site`',
|
|
296
|
-
match: '\\b(?:(dynamic|deployment)\\s+)?(view)\\s+([A-Za-z_]\\w*)',
|
|
297
|
-
captures: {
|
|
298
|
-
1: { name: 'storage.modifier.likec4' },
|
|
299
|
-
2: { name: 'storage.type.view.likec4' },
|
|
300
|
-
3: { name: 'entity.name.function.view.likec4' },
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
{
|
|
304
|
-
comment: "`reader = actor 'Engineer'`",
|
|
305
|
-
match: `\\b([A-Za-z_]\\w*)\\s*(=)(?!=)\\s*${notReserved}([A-Za-z_]\\w*)?`,
|
|
306
|
-
captures: {
|
|
307
|
-
1: { name: 'variable.other.definition.likec4' },
|
|
308
|
-
2: { name: 'keyword.operator.assignment.likec4' },
|
|
309
|
-
3: { name: 'entity.name.type.likec4' },
|
|
310
|
-
},
|
|
311
|
-
},
|
|
312
|
-
],
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// endregion
|
|
317
|
-
|
|
318
|
-
// region generated keyword patterns
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Order matters here: blocks before plain keywords (so `model {` is a block),
|
|
322
|
-
* operator words before keywords (`is`, `not`), variables before value domains
|
|
323
|
-
* (so `source`/`target` stay variables even though `RankValue` reuses `source`).
|
|
324
|
-
*
|
|
325
|
-
* @param {import('./langium.mjs').Vocabulary & { control: string[], variables: string[] }} vocabulary
|
|
326
|
-
*/
|
|
327
|
-
function keywords(vocabulary) {
|
|
328
|
-
/** @type {Record<string, unknown>[]} */
|
|
329
|
-
const patterns = [];
|
|
330
|
-
|
|
331
|
-
const blocks = alternationOf(vocabulary.blocks);
|
|
332
|
-
if (blocks) {
|
|
333
|
-
patterns.push({
|
|
334
|
-
comment: 'Keywords that open a block: `model { … }`.',
|
|
335
|
-
name: 'keyword.control.block.likec4',
|
|
336
|
-
match: `\\b(?:${blocks})\\b(?=\\s*\\{)`,
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Blocks repeat in the keyword list on purpose: `predicateGroup mine { … }`
|
|
341
|
-
// is not followed by `{`, and still has to colour as a keyword. The block
|
|
342
|
-
// pattern above wins wherever the brace is there.
|
|
343
|
-
const control = alternationOf([...vocabulary.keywords, ...vocabulary.control, ...vocabulary.blocks]);
|
|
344
|
-
|
|
345
|
-
// Word operators (`is`, `not`) come before the keyword list, which also
|
|
346
|
-
// contains them: `where tag is #x` reads as an operator, not a keyword.
|
|
347
|
-
patterns.push({
|
|
348
|
-
name: 'keyword.operator.word.likec4',
|
|
349
|
-
match: `\\b(?:${alternationOf(OPERATOR_WORDS)})\\b`,
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
if (control) {
|
|
353
|
-
patterns.push({ name: 'keyword.control.likec4', match: `\\b(?:${control})\\b` });
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const variables = alternationOf(vocabulary.variables);
|
|
357
|
-
if (variables) {
|
|
358
|
-
patterns.push({ name: 'variable.language.likec4', match: `\\b(?:${variables})\\b` });
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const properties = alternationOf(vocabulary.properties);
|
|
362
|
-
if (properties) {
|
|
363
|
-
patterns.push({ name: 'support.type.property-name.likec4', match: `\\b(?:${properties})\\b` });
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const values = alternationOf(uniqueDomain(vocabulary, VALUE_DOMAINS));
|
|
367
|
-
if (values) {
|
|
368
|
-
patterns.push({ name: 'constant.language.likec4', match: `\\b(?:${values})\\b` });
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return { patterns };
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function numbers() {
|
|
375
|
-
return { name: 'constant.numeric.likec4', match: '\\b\\d+(?:\\.\\d+)?%?' };
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Relationship connectors, longest first so `]->` wins over `->`.
|
|
380
|
-
* @param {string[]} arrows
|
|
381
|
-
*/
|
|
382
|
-
function operators(arrows) {
|
|
383
|
-
const patterns = [];
|
|
384
|
-
|
|
385
|
-
const connectors = alternationOf(arrows);
|
|
386
|
-
if (connectors) {
|
|
387
|
-
patterns.push({ name: 'keyword.operator.arrow.likec4', match: connectors });
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
patterns.push({ name: 'keyword.operator.wildcard.likec4', match: '\\.\\*\\*|\\.\\*|\\*\\*|\\*' });
|
|
391
|
-
|
|
392
|
-
return { patterns };
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function punctuation() {
|
|
396
|
-
return {
|
|
397
|
-
patterns: [
|
|
398
|
-
{ name: 'punctuation.section.block.begin.likec4', match: '\\{' },
|
|
399
|
-
{ name: 'punctuation.section.block.end.likec4', match: '\\}' },
|
|
400
|
-
{ name: 'punctuation.separator.comma.likec4', match: ',' },
|
|
401
|
-
{ name: 'punctuation.accessor.likec4', match: '\\.' },
|
|
402
|
-
],
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// endregion
|
|
407
|
-
|
|
408
|
-
// region helpers
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Every value of the named domains, deduplicated and sorted. Missing domains
|
|
412
|
-
* are skipped rather than failing: upstream is free to rename a rule, and a
|
|
413
|
-
* renamed domain still reaches the grammar through `keywords`.
|
|
414
|
-
* @param {import('./langium.mjs').Vocabulary} vocabulary
|
|
415
|
-
* @param {string[]} names
|
|
416
|
-
*/
|
|
417
|
-
function uniqueDomain(vocabulary, names) {
|
|
418
|
-
const values = names.flatMap((name) => vocabulary.enums[name] ?? []);
|
|
419
|
-
|
|
420
|
-
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* `['a', 'b']` -> `a|b`, escaped for a TextMate regex. Returns '' for an empty
|
|
425
|
-
* list so callers can skip the pattern instead of emitting `\b(?:)\b`.
|
|
426
|
-
* @param {string[]} values
|
|
427
|
-
*/
|
|
428
|
-
function alternationOf(values) {
|
|
429
|
-
return values.map(escapeRegExp).join('|');
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function escapeRegExp(value) {
|
|
433
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// endregion
|
|
437
|
-
|
|
438
|
-
export default buildTmLanguage;
|
|
439
|
-
|
|
1
|
+
// textmate.mjs
|
|
2
|
+
// path: src/grammar/textmate.mjs
|
|
3
|
+
// Builds the Shiki / TextMate grammar for LikeC4 out of the vocabulary read
|
|
4
|
+
// from the live Langium grammar (./langium.mjs).
|
|
5
|
+
//
|
|
6
|
+
// Two layers:
|
|
7
|
+
// - generated — every word list: blocks, keywords, property names and closed
|
|
8
|
+
// value domains. These are exactly the lists that go stale when the
|
|
9
|
+
// language grows (`agent` was added to `ElementShape` after v1.59.4).
|
|
10
|
+
// - structural — how LikeC4 source is *shaped* rather than which words it
|
|
11
|
+
// uses: comments, quoted and triple-quoted strings, `#tag` vs `#hex`,
|
|
12
|
+
// `tech:astro` icons, `-[https]->` arrows, `element system` declarations.
|
|
13
|
+
// These are curated here because a Langium rule does not say how a token
|
|
14
|
+
// should look, only that it is one.
|
|
15
|
+
//
|
|
16
|
+
// The result must stay plain JSON: Expressive Code JSON-round-trips integration
|
|
17
|
+
// options before the `<Code>` component renders them, and throws on functions
|
|
18
|
+
// or RegExp instances.
|
|
19
|
+
|
|
20
|
+
/** Scope name of the grammar, as upstream's VS Code extension uses it. */
|
|
21
|
+
export const SCOPE_NAME = 'source.likec4';
|
|
22
|
+
|
|
23
|
+
/** Shiki language id, plus the short alias used by ` ```c4 ` fences. */
|
|
24
|
+
export const LANGUAGE_NAME = 'likec4';
|
|
25
|
+
export const LANGUAGE_ALIASES = ['c4'];
|
|
26
|
+
|
|
27
|
+
const TMLANGUAGE_SCHEMA =
|
|
28
|
+
'https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Closed value domains: they colour as values, not as keywords.
|
|
32
|
+
* Anything the grammar adds under these names is picked up automatically.
|
|
33
|
+
*/
|
|
34
|
+
const VALUE_DOMAINS = [
|
|
35
|
+
'BorderStyleValue',
|
|
36
|
+
'DynamicViewDisplayVariantValue',
|
|
37
|
+
'EdgeRoutingValue',
|
|
38
|
+
'ElementShape',
|
|
39
|
+
'IconPositionValue',
|
|
40
|
+
'LineOptions',
|
|
41
|
+
'ArrowType',
|
|
42
|
+
'RankValue',
|
|
43
|
+
'SizeValue',
|
|
44
|
+
'SizingValue',
|
|
45
|
+
'ThemeColor',
|
|
46
|
+
'ViewLayoutDirection',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** Control-flow keywords read as keywords, not as a value domain. */
|
|
50
|
+
const KEYWORD_DOMAINS = ['DynamicViewFlowKeyword'];
|
|
51
|
+
|
|
52
|
+
/** `source` / `target` are the only value-like variables. */
|
|
53
|
+
const VARIABLE_DOMAINS = ['Participant'];
|
|
54
|
+
|
|
55
|
+
/** Operators whose scope is a word, kept curated: they are not all literals. */
|
|
56
|
+
const OPERATOR_WORDS = ['and', 'or', 'not', 'is'];
|
|
57
|
+
|
|
58
|
+
/** Used when the grammar cannot be read — never expected, never silent. */
|
|
59
|
+
const FALLBACK_ICON_PACKS = ['aws', 'azure', 'bootstrap', 'gcp', 'tech'];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @typedef {object} TmPattern
|
|
63
|
+
* @property {string} [name]
|
|
64
|
+
* @property {string} [comment]
|
|
65
|
+
* @property {string} [include]
|
|
66
|
+
* @property {string} [match]
|
|
67
|
+
* @property {string} [begin]
|
|
68
|
+
* @property {string} [end]
|
|
69
|
+
* @property {string} [contentName]
|
|
70
|
+
* @property {Record<string, { name: string }>} [captures]
|
|
71
|
+
* @property {Record<string, { name: string }>} [beginCaptures]
|
|
72
|
+
* @property {Record<string, { name: string }>} [endCaptures]
|
|
73
|
+
* @property {TmPattern[]} [patterns]
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A TextMate grammar: JSON all the way down, never a `RegExp`.
|
|
78
|
+
* @typedef {object} TmGrammar
|
|
79
|
+
* @property {string} $schema
|
|
80
|
+
* @property {string} name
|
|
81
|
+
* @property {string} displayName
|
|
82
|
+
* @property {string} scopeName
|
|
83
|
+
* @property {string[]} fileTypes
|
|
84
|
+
* @property {TmPattern[]} patterns
|
|
85
|
+
* @property {Record<string, any>} repository
|
|
86
|
+
* @property {{ repo: string, ref: string, sha?: string, by: string }} [$generated]
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {import('./langium.mjs').Vocabulary} vocabulary
|
|
91
|
+
* @param {{ repo?: string, ref?: string, sha?: string }} [meta] Provenance.
|
|
92
|
+
* @returns {TmGrammar} A TextMate grammar, JSON-serialisable.
|
|
93
|
+
*/
|
|
94
|
+
export function buildTmLanguage(vocabulary, meta = {}) {
|
|
95
|
+
const control = uniqueDomain(vocabulary, KEYWORD_DOMAINS);
|
|
96
|
+
const variables = uniqueDomain(vocabulary, VARIABLE_DOMAINS);
|
|
97
|
+
const icons = vocabulary.icons.length > 0 ? vocabulary.icons : FALLBACK_ICON_PACKS;
|
|
98
|
+
|
|
99
|
+
// Words that are always syntax, never the name of a kind: `deployed =
|
|
100
|
+
// instanceOf a` must not paint `instanceOf` as a type.
|
|
101
|
+
const reserved = alternationOf([...vocabulary.keywords, ...vocabulary.blocks, ...control]);
|
|
102
|
+
|
|
103
|
+
/** @type {TmGrammar} */
|
|
104
|
+
const grammar = {
|
|
105
|
+
$schema: TMLANGUAGE_SCHEMA,
|
|
106
|
+
...(meta.repo && meta.ref
|
|
107
|
+
? {
|
|
108
|
+
$generated: {
|
|
109
|
+
repo: meta.repo,
|
|
110
|
+
ref: meta.ref,
|
|
111
|
+
...(meta.sha ? { sha: meta.sha } : {}),
|
|
112
|
+
by: 'astroc4/scripts/sync-grammar.mjs',
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
: {}),
|
|
116
|
+
name: LANGUAGE_NAME,
|
|
117
|
+
displayName: 'LikeC4',
|
|
118
|
+
scopeName: SCOPE_NAME,
|
|
119
|
+
fileTypes: ['c4', 'likec4'],
|
|
120
|
+
patterns: [{ include: '#everything' }],
|
|
121
|
+
repository: {
|
|
122
|
+
everything: {
|
|
123
|
+
patterns: [
|
|
124
|
+
'comments',
|
|
125
|
+
'strings',
|
|
126
|
+
'urls',
|
|
127
|
+
'icons',
|
|
128
|
+
'hex-colors',
|
|
129
|
+
'tags',
|
|
130
|
+
'typed-arrow',
|
|
131
|
+
'declarations',
|
|
132
|
+
'keywords',
|
|
133
|
+
'numbers',
|
|
134
|
+
'operators',
|
|
135
|
+
'punctuation',
|
|
136
|
+
].map((name) => ({ include: `#${name}` })),
|
|
137
|
+
},
|
|
138
|
+
comments: comments(),
|
|
139
|
+
strings: strings(),
|
|
140
|
+
urls: urls(),
|
|
141
|
+
icons: iconIds(icons),
|
|
142
|
+
'hex-colors': hexColours(),
|
|
143
|
+
tags: tags(),
|
|
144
|
+
'typed-arrow': typedArrow(),
|
|
145
|
+
declarations: declarations(reserved),
|
|
146
|
+
keywords: keywords({ ...vocabulary, control, variables }),
|
|
147
|
+
numbers: numbers(),
|
|
148
|
+
operators: operators(vocabulary.arrows),
|
|
149
|
+
punctuation: punctuation(),
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if (meta.repo && meta.ref) {
|
|
154
|
+
grammar.$generated = {
|
|
155
|
+
repo: meta.repo,
|
|
156
|
+
ref: meta.ref,
|
|
157
|
+
...(meta.sha ? { sha: meta.sha } : {}),
|
|
158
|
+
by: 'astroc4/scripts/sync-grammar.mjs',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return grammar;
|
|
163
|
+
}
|
|
164
|
+
// region structural patterns
|
|
165
|
+
|
|
166
|
+
function comments() {
|
|
167
|
+
return {
|
|
168
|
+
patterns: [
|
|
169
|
+
{
|
|
170
|
+
name: 'comment.block.likec4',
|
|
171
|
+
begin: '/\\*',
|
|
172
|
+
end: '\\*/',
|
|
173
|
+
captures: { 0: { name: 'punctuation.definition.comment.likec4' } },
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'comment.line.double-slash.likec4',
|
|
177
|
+
match: '//.*$',
|
|
178
|
+
captures: { 0: { name: 'punctuation.definition.comment.likec4' } },
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* `'…'`, `"…"`, and the markdown strings `''' … '''` / `""" … """` that
|
|
186
|
+
* LikeC4 uses for descriptions.
|
|
187
|
+
*/
|
|
188
|
+
function strings() {
|
|
189
|
+
const triple = (quote) => ({
|
|
190
|
+
name: 'string.quoted.triple.likec4',
|
|
191
|
+
begin: quote.repeat(3),
|
|
192
|
+
end: quote.repeat(3),
|
|
193
|
+
beginCaptures: { 0: { name: 'punctuation.definition.string.begin.likec4' } },
|
|
194
|
+
endCaptures: { 0: { name: 'punctuation.definition.string.end.likec4' } },
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const single = (quote, name) => ({
|
|
198
|
+
name,
|
|
199
|
+
begin: quote,
|
|
200
|
+
end: `${quote}|$`,
|
|
201
|
+
beginCaptures: { 0: { name: 'punctuation.definition.string.begin.likec4' } },
|
|
202
|
+
endCaptures: { 0: { name: 'punctuation.definition.string.end.likec4' } },
|
|
203
|
+
patterns: [{ name: 'constant.character.escape.likec4', match: '\\\\.' }],
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
patterns: [
|
|
208
|
+
triple("'"),
|
|
209
|
+
triple('"'),
|
|
210
|
+
single("'", 'string.quoted.single.likec4'),
|
|
211
|
+
single('"', 'string.quoted.double.likec4'),
|
|
212
|
+
],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function urls() {
|
|
217
|
+
return { name: 'markup.underline.link.likec4', match: "\\bhttps?://[^\\s'\"]+" };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Icon ids (`tech:astro`, `aws:lambda`). The packs come from the `LIB_ICON`
|
|
222
|
+
* terminal, so a pack added upstream highlights without touching this package.
|
|
223
|
+
* @param {string[]} packs
|
|
224
|
+
*/
|
|
225
|
+
function iconIds(packs) {
|
|
226
|
+
return {
|
|
227
|
+
comment: 'Bundled icon ids: `tech:nginx`, `aws:…`, `gcp:…`, …',
|
|
228
|
+
match: `\\b(${alternationOf(packs)})(:)([\\w-]+)`,
|
|
229
|
+
captures: {
|
|
230
|
+
1: { name: 'support.constant.icon-pack.likec4' },
|
|
231
|
+
2: { name: 'punctuation.separator.likec4' },
|
|
232
|
+
3: { name: 'support.constant.icon.likec4' },
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** `#0046BE` is a colour, `#compute` is a tag — hex first, so it wins. */
|
|
238
|
+
function hexColours() {
|
|
239
|
+
return {
|
|
240
|
+
name: 'constant.other.color.rgb-value.hex.likec4',
|
|
241
|
+
match: '#(?:[0-9A-Fa-f]{8}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})(?![\\w-])',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function tags() {
|
|
246
|
+
return { name: 'entity.name.tag.likec4', match: '#[A-Za-z_][\\w-]*' };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** `a -[https]-> b` — the relationship kind inside the connector. */
|
|
250
|
+
function typedArrow() {
|
|
251
|
+
return {
|
|
252
|
+
comment: '`a -[https]-> b`',
|
|
253
|
+
match: '(-\\[)\\s*([A-Za-z_][\\w-]*)\\s*(\\]->)',
|
|
254
|
+
captures: {
|
|
255
|
+
1: { name: 'keyword.operator.arrow.likec4' },
|
|
256
|
+
2: { name: 'entity.name.type.relationship.likec4' },
|
|
257
|
+
3: { name: 'keyword.operator.arrow.likec4' },
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Declarations: the name being introduced is scoped, the name only being
|
|
264
|
+
* referred to is not. Runs before the keyword patterns so `element system`
|
|
265
|
+
* reads as a declaration rather than as two keywords.
|
|
266
|
+
*
|
|
267
|
+
* @param {string} reserved Alternation of syntax words (`instanceOf`, `extend`,
|
|
268
|
+
* …), so `deployed = instanceOf a` scopes `instanceOf` as a keyword rather
|
|
269
|
+
* than as the element kind the name is being assigned.
|
|
270
|
+
*/
|
|
271
|
+
function declarations(reserved) {
|
|
272
|
+
const notReserved = reserved ? `(?!(?:${reserved})\\b)` : '';
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
patterns: [
|
|
276
|
+
{
|
|
277
|
+
comment:
|
|
278
|
+
'`element system`, `deploymentNode region`, `tag tls`, `relationship https`. Not `where tag is #x`.',
|
|
279
|
+
match: '\\b(element|deploymentNode|tag|relationship)\\s+(?!is\\b)([A-Za-z_][\\w-]*)(?=\\s*(?:\\{|$|//))',
|
|
280
|
+
captures: {
|
|
281
|
+
1: { name: 'storage.type.likec4' },
|
|
282
|
+
2: { name: 'entity.name.type.likec4' },
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
comment:
|
|
287
|
+
'`color testingBlue #0046BE` declares; `color testingBlue` inside a style block only refers.',
|
|
288
|
+
match: '\\b(color)\\s+([A-Za-z_][\\w-]*)(?=\\s+#)',
|
|
289
|
+
captures: {
|
|
290
|
+
1: { name: 'storage.type.likec4' },
|
|
291
|
+
2: { name: 'entity.name.type.likec4' },
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
comment: '`view index`, `dynamic view firstRequest`, `deployment view prod`, `view internals of site`',
|
|
296
|
+
match: '\\b(?:(dynamic|deployment)\\s+)?(view)\\s+([A-Za-z_]\\w*)',
|
|
297
|
+
captures: {
|
|
298
|
+
1: { name: 'storage.modifier.likec4' },
|
|
299
|
+
2: { name: 'storage.type.view.likec4' },
|
|
300
|
+
3: { name: 'entity.name.function.view.likec4' },
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
comment: "`reader = actor 'Engineer'`",
|
|
305
|
+
match: `\\b([A-Za-z_]\\w*)\\s*(=)(?!=)\\s*${notReserved}([A-Za-z_]\\w*)?`,
|
|
306
|
+
captures: {
|
|
307
|
+
1: { name: 'variable.other.definition.likec4' },
|
|
308
|
+
2: { name: 'keyword.operator.assignment.likec4' },
|
|
309
|
+
3: { name: 'entity.name.type.likec4' },
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// endregion
|
|
317
|
+
|
|
318
|
+
// region generated keyword patterns
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Order matters here: blocks before plain keywords (so `model {` is a block),
|
|
322
|
+
* operator words before keywords (`is`, `not`), variables before value domains
|
|
323
|
+
* (so `source`/`target` stay variables even though `RankValue` reuses `source`).
|
|
324
|
+
*
|
|
325
|
+
* @param {import('./langium.mjs').Vocabulary & { control: string[], variables: string[] }} vocabulary
|
|
326
|
+
*/
|
|
327
|
+
function keywords(vocabulary) {
|
|
328
|
+
/** @type {Record<string, unknown>[]} */
|
|
329
|
+
const patterns = [];
|
|
330
|
+
|
|
331
|
+
const blocks = alternationOf(vocabulary.blocks);
|
|
332
|
+
if (blocks) {
|
|
333
|
+
patterns.push({
|
|
334
|
+
comment: 'Keywords that open a block: `model { … }`.',
|
|
335
|
+
name: 'keyword.control.block.likec4',
|
|
336
|
+
match: `\\b(?:${blocks})\\b(?=\\s*\\{)`,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Blocks repeat in the keyword list on purpose: `predicateGroup mine { … }`
|
|
341
|
+
// is not followed by `{`, and still has to colour as a keyword. The block
|
|
342
|
+
// pattern above wins wherever the brace is there.
|
|
343
|
+
const control = alternationOf([...vocabulary.keywords, ...vocabulary.control, ...vocabulary.blocks]);
|
|
344
|
+
|
|
345
|
+
// Word operators (`is`, `not`) come before the keyword list, which also
|
|
346
|
+
// contains them: `where tag is #x` reads as an operator, not a keyword.
|
|
347
|
+
patterns.push({
|
|
348
|
+
name: 'keyword.operator.word.likec4',
|
|
349
|
+
match: `\\b(?:${alternationOf(OPERATOR_WORDS)})\\b`,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
if (control) {
|
|
353
|
+
patterns.push({ name: 'keyword.control.likec4', match: `\\b(?:${control})\\b` });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const variables = alternationOf(vocabulary.variables);
|
|
357
|
+
if (variables) {
|
|
358
|
+
patterns.push({ name: 'variable.language.likec4', match: `\\b(?:${variables})\\b` });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const properties = alternationOf(vocabulary.properties);
|
|
362
|
+
if (properties) {
|
|
363
|
+
patterns.push({ name: 'support.type.property-name.likec4', match: `\\b(?:${properties})\\b` });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const values = alternationOf(uniqueDomain(vocabulary, VALUE_DOMAINS));
|
|
367
|
+
if (values) {
|
|
368
|
+
patterns.push({ name: 'constant.language.likec4', match: `\\b(?:${values})\\b` });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return { patterns };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function numbers() {
|
|
375
|
+
return { name: 'constant.numeric.likec4', match: '\\b\\d+(?:\\.\\d+)?%?' };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Relationship connectors, longest first so `]->` wins over `->`.
|
|
380
|
+
* @param {string[]} arrows
|
|
381
|
+
*/
|
|
382
|
+
function operators(arrows) {
|
|
383
|
+
const patterns = [];
|
|
384
|
+
|
|
385
|
+
const connectors = alternationOf(arrows);
|
|
386
|
+
if (connectors) {
|
|
387
|
+
patterns.push({ name: 'keyword.operator.arrow.likec4', match: connectors });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
patterns.push({ name: 'keyword.operator.wildcard.likec4', match: '\\.\\*\\*|\\.\\*|\\*\\*|\\*' });
|
|
391
|
+
|
|
392
|
+
return { patterns };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function punctuation() {
|
|
396
|
+
return {
|
|
397
|
+
patterns: [
|
|
398
|
+
{ name: 'punctuation.section.block.begin.likec4', match: '\\{' },
|
|
399
|
+
{ name: 'punctuation.section.block.end.likec4', match: '\\}' },
|
|
400
|
+
{ name: 'punctuation.separator.comma.likec4', match: ',' },
|
|
401
|
+
{ name: 'punctuation.accessor.likec4', match: '\\.' },
|
|
402
|
+
],
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// endregion
|
|
407
|
+
|
|
408
|
+
// region helpers
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Every value of the named domains, deduplicated and sorted. Missing domains
|
|
412
|
+
* are skipped rather than failing: upstream is free to rename a rule, and a
|
|
413
|
+
* renamed domain still reaches the grammar through `keywords`.
|
|
414
|
+
* @param {import('./langium.mjs').Vocabulary} vocabulary
|
|
415
|
+
* @param {string[]} names
|
|
416
|
+
*/
|
|
417
|
+
function uniqueDomain(vocabulary, names) {
|
|
418
|
+
const values = names.flatMap((name) => vocabulary.enums[name] ?? []);
|
|
419
|
+
|
|
420
|
+
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* `['a', 'b']` -> `a|b`, escaped for a TextMate regex. Returns '' for an empty
|
|
425
|
+
* list so callers can skip the pattern instead of emitting `\b(?:)\b`.
|
|
426
|
+
* @param {string[]} values
|
|
427
|
+
*/
|
|
428
|
+
function alternationOf(values) {
|
|
429
|
+
return values.map(escapeRegExp).join('|');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function escapeRegExp(value) {
|
|
433
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// endregion
|
|
437
|
+
|
|
438
|
+
export default buildTmLanguage;
|
|
439
|
+
|