@purview-dev/astroc4 1.0.0-prerelease.2 → 1.0.0-prerelease.4
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/README.md +2 -2
- package/dist/expressive-code.d.ts +14 -0
- package/dist/expressive-code.js +1171 -0
- package/dist/expressive-code.js.map +13 -0
- package/dist/fences.d.ts +32 -0
- package/dist/grammar/langium.d.ts +42 -0
- package/dist/grammar/source.d.ts +90 -0
- package/dist/grammar/textmate.d.ts +51 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +1125 -0
- package/dist/index.js.map +15 -0
- package/{src/grammar → dist}/likec4.tmLanguage.json +3 -3
- package/dist/translations.d.ts +21 -0
- package/package.json +23 -14
- package/src/components/labels.ts +5 -1
- package/src/expressive-code.d.mts +0 -28
- package/src/expressive-code.mjs +0 -46
- package/src/fences.mjs +0 -288
- package/src/grammar/langium.mjs +0 -559
- package/src/grammar/source.mjs +0 -410
- package/src/grammar/textmate.mjs +0 -439
- package/src/index.d.mts +0 -64
- package/src/index.mjs +0 -228
- package/src/types.d.ts +0 -12
- /package/{src/grammar → dist}/like-c4.langium +0 -0
- /package/src/{translations.mjs → translations.ts} +0 -0
package/src/grammar/langium.mjs
DELETED
|
@@ -1,559 +0,0 @@
|
|
|
1
|
-
// langium.mjs
|
|
2
|
-
// path: src/grammar/langium.mjs
|
|
3
|
-
// Reduces the LikeC4 Langium grammar to the vocabulary a TextMate grammar
|
|
4
|
-
// needs: which words open a block, which are plain keywords, which are
|
|
5
|
-
// property names, which belong to a closed value domain (shapes, colours,
|
|
6
|
-
// arrow heads…) and which tokens have to be matched structurally (comments,
|
|
7
|
-
// strings, icon ids).
|
|
8
|
-
//
|
|
9
|
-
// The grammar is read from likec4/likec4 at a ref the site chooses:
|
|
10
|
-
// `packages/language-server/src/like-c4.langium`. Deriving from it is what
|
|
11
|
-
// keeps highlighting in step with the language — `ElementShape` gained `agent`
|
|
12
|
-
// after v1.59.4, and the TextMate grammar upstream maintains by hand still
|
|
13
|
-
// does not know about it.
|
|
14
|
-
//
|
|
15
|
-
// Deliberately no Langium dependency: this is a tolerant scanner over one
|
|
16
|
-
// file, not a parser for the grammar language, and it has to survive upstream
|
|
17
|
-
// reformatting. Anything it cannot classify is dropped rather than guessed.
|
|
18
|
-
|
|
19
|
-
/** Rules that describe a closed set of values are named after what they hold. */
|
|
20
|
-
const NOT_VALUE_DOMAINS = /Id$/;
|
|
21
|
-
|
|
22
|
-
/** Property names the grammar matches as `key='…'` assignments. */
|
|
23
|
-
const PROPERTY_SUPPLEMENTS = ['kind', 'tag'];
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @typedef {object} Terminal
|
|
27
|
-
* @property {boolean} hidden
|
|
28
|
-
* @property {'regex' | 'literal'} kind
|
|
29
|
-
* @property {string} value Raw regex source (without slashes) or literal.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @typedef {object} Vocabulary
|
|
34
|
-
* @property {string[]} blocks Keywords that open a `{ … }` block.
|
|
35
|
-
* @property {string[]} keywords Every remaining word-like literal.
|
|
36
|
-
* @property {string[]} properties Property names from `key='…'` assignments.
|
|
37
|
-
* @property {string[]} arrows Relationship connectors containing `->`.
|
|
38
|
-
* @property {Record<string, string[]>} enums `returns string` value domains.
|
|
39
|
-
* @property {string[]} icons Icon packs from the `LIB_ICON` terminal.
|
|
40
|
-
* @property {string[]} booleans Literals from the `BOOLEAN` terminal.
|
|
41
|
-
* @property {number} ruleCount Parser rules seen — used for sanity checks.
|
|
42
|
-
*/
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* @param {string} source
|
|
46
|
-
* @returns {Vocabulary}
|
|
47
|
-
*/
|
|
48
|
-
export function parseLangium(source) {
|
|
49
|
-
// `grammar LikeC4` is a header, not a rule, and it has no `;`: leaving it in
|
|
50
|
-
// would glue it to the first real rule and hide that rule from the scanner.
|
|
51
|
-
const statements = splitStatements(
|
|
52
|
-
stripComments(source).replace(/^[ \t]*grammar[ \t]+[A-Za-z_]\w*[ \t]*$/gm, ''),
|
|
53
|
-
);
|
|
54
|
-
const rules = statements.map(readStatement).filter((rule) => rule !== undefined);
|
|
55
|
-
|
|
56
|
-
const parserRules = rules.filter((rule) => rule.kind === 'rule' || rule.kind === 'fragment');
|
|
57
|
-
|
|
58
|
-
if (parserRules.length === 0) {
|
|
59
|
-
throw new Error(
|
|
60
|
-
'the file contains no Langium parser rules — is this still `packages/language-server/src/like-c4.langium`?',
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const enums = resolveEnums(parserRules);
|
|
65
|
-
const blocks = findBlocks(parserRules);
|
|
66
|
-
const properties = unique([...findProperties(parserRules), ...PROPERTY_SUPPLEMENTS]);
|
|
67
|
-
const arrows = findArrows(parserRules);
|
|
68
|
-
const literals = unique(
|
|
69
|
-
parserRules.flatMap((rule) => rule.tokens.filter((t) => t.type === 'literal').map((t) => t.value)),
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const classified = new Set([...blocks, ...properties, ...Object.values(enums).flat()]);
|
|
73
|
-
const keywords = literals.filter(
|
|
74
|
-
(value) => /^[A-Za-z_]\w*$/.test(value) && !classified.has(value),
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
const terminals = readTerminals(rules);
|
|
78
|
-
|
|
79
|
-
// A grammar with parser rules but no structure at all is not a LikeC4
|
|
80
|
-
// grammar — most likely the path now holds something else. A small grammar
|
|
81
|
-
// can legitimately have no "leftover" keywords, so blocks and value domains
|
|
82
|
-
// count too.
|
|
83
|
-
if (keywords.length === 0 && blocks.length === 0 && Object.keys(enums).length === 0) {
|
|
84
|
-
throw new Error('the file contains no LikeC4 keywords — the Langium grammar could not be read');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
blocks,
|
|
89
|
-
keywords,
|
|
90
|
-
properties,
|
|
91
|
-
arrows,
|
|
92
|
-
enums,
|
|
93
|
-
icons: terminalAlternatives(terminals.LIB_ICON),
|
|
94
|
-
booleans: terminalAlternatives(terminals.BOOLEAN),
|
|
95
|
-
ruleCount: parserRules.length,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// region scanning
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Blank out `//` and block comments. String and regex literals are copied
|
|
103
|
-
* verbatim, because a `//` inside `/https?:\/\//` is not a comment.
|
|
104
|
-
* @param {string} source
|
|
105
|
-
*/
|
|
106
|
-
function stripComments(source) {
|
|
107
|
-
let out = '';
|
|
108
|
-
let i = 0;
|
|
109
|
-
|
|
110
|
-
while (i < source.length) {
|
|
111
|
-
const char = source[i];
|
|
112
|
-
|
|
113
|
-
if (char === '/' && source[i + 1] === '/') {
|
|
114
|
-
while (i < source.length && source[i] !== '\n') i++;
|
|
115
|
-
out += ' ';
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (char === '/' && source[i + 1] === '*') {
|
|
120
|
-
i += 2;
|
|
121
|
-
while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) i++;
|
|
122
|
-
i += 2;
|
|
123
|
-
out += ' ';
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (char === "'" || char === '"') {
|
|
128
|
-
const end = endOfString(source, i);
|
|
129
|
-
out += source.slice(i, end);
|
|
130
|
-
i = end;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (char === '/') {
|
|
135
|
-
const end = endOfRegex(source, i);
|
|
136
|
-
out += source.slice(i, end);
|
|
137
|
-
i = end;
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
out += char;
|
|
142
|
-
i++;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return out;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** Index just past the quoted literal that opens at `start`. */
|
|
149
|
-
function endOfString(source, start) {
|
|
150
|
-
const quote = source[start];
|
|
151
|
-
let i = start + 1;
|
|
152
|
-
|
|
153
|
-
while (i < source.length) {
|
|
154
|
-
if (source[i] === '\\') {
|
|
155
|
-
i += 2;
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
if (source[i] === quote) return i + 1;
|
|
159
|
-
i++;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return source.length;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Index just past the regex literal that opens at `start`. */
|
|
166
|
-
function endOfRegex(source, start) {
|
|
167
|
-
let i = start + 1;
|
|
168
|
-
let inClass = false;
|
|
169
|
-
|
|
170
|
-
while (i < source.length) {
|
|
171
|
-
const char = source[i];
|
|
172
|
-
|
|
173
|
-
if (char === '\\') {
|
|
174
|
-
i += 2;
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
if (char === '[') inClass = true;
|
|
178
|
-
else if (char === ']') inClass = false;
|
|
179
|
-
else if (char === '/' && !inClass) return i + 1;
|
|
180
|
-
|
|
181
|
-
i++;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return source.length;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Split on the `;` that ends each rule, ignoring the ones inside strings,
|
|
189
|
-
* regexes and `{ … }` — interfaces have fields, and rules have `{infer X}`
|
|
190
|
-
* actions, both of which can contain semicolons.
|
|
191
|
-
* @param {string} source
|
|
192
|
-
*/
|
|
193
|
-
function splitStatements(source) {
|
|
194
|
-
const statements = [];
|
|
195
|
-
let depth = 0;
|
|
196
|
-
let start = 0;
|
|
197
|
-
let i = 0;
|
|
198
|
-
|
|
199
|
-
while (i < source.length) {
|
|
200
|
-
const char = source[i];
|
|
201
|
-
|
|
202
|
-
if (char === "'" || char === '"') {
|
|
203
|
-
i = endOfString(source, i);
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (char === '/') {
|
|
208
|
-
i = endOfRegex(source, i);
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (char === '{') depth++;
|
|
213
|
-
else if (char === '}') depth = Math.max(0, depth - 1);
|
|
214
|
-
else if (char === ';' && depth === 0) {
|
|
215
|
-
statements.push(source.slice(start, i));
|
|
216
|
-
start = i + 1;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
i++;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const tail = source.slice(start).trim();
|
|
223
|
-
if (tail.length > 0) statements.push(tail);
|
|
224
|
-
|
|
225
|
-
return statements.filter((statement) => statement.trim().length > 0);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const HEADERS = [
|
|
229
|
-
// `hidden terminal WS: /[ \t]+/`
|
|
230
|
-
{ kind: 'terminal', hidden: true, pattern: /^\s*hidden\s+terminal\s+([A-Za-z_]\w*)\s*(?:<[^>]*>)?\s*:/ },
|
|
231
|
-
// `terminal LIB_ICON: /…/`, `terminal fragment HASH: '#'`
|
|
232
|
-
{
|
|
233
|
-
kind: 'terminal',
|
|
234
|
-
hidden: false,
|
|
235
|
-
pattern:
|
|
236
|
-
/^\s*terminal\s+(?:fragment\s+)?([A-Za-z_]\w*)\s*(?:<[^>]*>)?\s*(?:returns\s+[A-Za-z_]\w*\s*)?:/,
|
|
237
|
-
},
|
|
238
|
-
// `fragment RelationConnector:`
|
|
239
|
-
{ kind: 'fragment', hidden: false, pattern: /^\s*fragment\s+([A-Za-z_]\w*)\s*:/ },
|
|
240
|
-
// `Model:`, `ModelViews:`, `entry LikeC4Grammar:`, `Relation<isExplicit>:`
|
|
241
|
-
{
|
|
242
|
-
kind: 'rule',
|
|
243
|
-
hidden: false,
|
|
244
|
-
pattern: /^\s*(?:entry\s+)?([A-Za-z_]\w*)\s*(?:<[^>]*>)?\s*(?:returns\s+([A-Za-z_]\w*)\s*)?:/,
|
|
245
|
-
},
|
|
246
|
-
];
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* @param {string} statement
|
|
250
|
-
* @returns {{ name: string, kind: 'rule' | 'fragment' | 'terminal', hidden: boolean, returns?: string, tokens: Token[] } | undefined}
|
|
251
|
-
*/
|
|
252
|
-
function readStatement(statement) {
|
|
253
|
-
for (const header of HEADERS) {
|
|
254
|
-
const match = header.pattern.exec(statement);
|
|
255
|
-
if (!match) continue;
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
name: match[1],
|
|
259
|
-
kind: header.kind,
|
|
260
|
-
hidden: header.hidden,
|
|
261
|
-
returns: header.kind === 'rule' ? match[2] : undefined,
|
|
262
|
-
tokens: tokenize(statement.slice(match[0].length)),
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return undefined;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* @typedef {'literal' | 'regex' | 'ident' | 'punct'} TokenType
|
|
271
|
-
* @typedef {{ type: TokenType, value: string }} Token
|
|
272
|
-
*/
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* @param {string} body
|
|
276
|
-
* @returns {Token[]}
|
|
277
|
-
*/
|
|
278
|
-
function tokenize(body) {
|
|
279
|
-
/** @type {Token[]} */
|
|
280
|
-
const tokens = [];
|
|
281
|
-
let i = 0;
|
|
282
|
-
|
|
283
|
-
while (i < body.length) {
|
|
284
|
-
const char = body[i];
|
|
285
|
-
|
|
286
|
-
if (/\s/.test(char)) {
|
|
287
|
-
i++;
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (char === "'" || char === '"') {
|
|
292
|
-
const end = endOfString(body, i);
|
|
293
|
-
tokens.push({ type: 'literal', value: unquote(body.slice(i, end)) });
|
|
294
|
-
i = end;
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (char === '/') {
|
|
299
|
-
const end = endOfRegex(body, i);
|
|
300
|
-
tokens.push({ type: 'regex', value: body.slice(i + 1, end - 1) });
|
|
301
|
-
i = end;
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const identifier = /^[A-Za-z_]\w*/.exec(body.slice(i));
|
|
306
|
-
if (identifier) {
|
|
307
|
-
tokens.push({ type: 'ident', value: identifier[0] });
|
|
308
|
-
i += identifier[0].length;
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Multi-character punctuation (`+=`, `?=`) is irrelevant: only `=`, `|`,
|
|
313
|
-
// `(` and `)` are inspected, and per-character tokens give all four.
|
|
314
|
-
tokens.push({ type: 'punct', value: char });
|
|
315
|
-
i++;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return tokens;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/** Strip the quotes and resolve the escapes Langium allows in literals. */
|
|
322
|
-
function unquote(quoted) {
|
|
323
|
-
const inner = quoted.slice(1, -1);
|
|
324
|
-
let out = '';
|
|
325
|
-
|
|
326
|
-
for (let i = 0; i < inner.length; i++) {
|
|
327
|
-
if (inner[i] !== '\\') {
|
|
328
|
-
out += inner[i];
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const escaped = inner[++i];
|
|
333
|
-
if (escaped === 'n') out += '\n';
|
|
334
|
-
else if (escaped === 't') out += '\t';
|
|
335
|
-
else if (escaped === 'r') out += '\r';
|
|
336
|
-
else out += escaped ?? '';
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return out;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// endregion
|
|
343
|
-
|
|
344
|
-
// region vocabulary
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Rules of the shape `'specification' '{' …` or `name='model' '{' …` open a
|
|
348
|
-
* block. The first literal of the rule is the keyword, and it only counts when
|
|
349
|
-
* the rule reaches a `{` without branching first: `'element' kind=ElementKind
|
|
350
|
-
* ('{' …)` has an optional body, so `element` stays a keyword.
|
|
351
|
-
* @param {{ tokens: Token[] }[]} rules
|
|
352
|
-
*/
|
|
353
|
-
function findBlocks(rules) {
|
|
354
|
-
return unique(
|
|
355
|
-
rules.flatMap((rule) => {
|
|
356
|
-
const tokens = rule.tokens;
|
|
357
|
-
const first = tokens.findIndex((token) => token.type === 'literal');
|
|
358
|
-
const keyword = first === -1 ? undefined : tokens[first].value;
|
|
359
|
-
|
|
360
|
-
if (!keyword || keyword === '{' || !/^[A-Za-z_]\w*$/.test(keyword)) return [];
|
|
361
|
-
|
|
362
|
-
for (let i = first + 1; i < tokens.length; i++) {
|
|
363
|
-
if (tokens[i].value === '(' || tokens[i].value === '|') return [];
|
|
364
|
-
if (tokens[i].type === 'literal' && tokens[i].value === '{') return [keyword];
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return [];
|
|
368
|
-
}),
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* `key='title'` and `key=('head' | 'tail')` name a property. Positional
|
|
374
|
-
* arguments (`props+=String`) and `name=` (block headers) are ignored.
|
|
375
|
-
* @param {{ tokens: Token[] }[]} rules
|
|
376
|
-
*/
|
|
377
|
-
function findProperties(rules) {
|
|
378
|
-
/** @type {string[]} */
|
|
379
|
-
const properties = [];
|
|
380
|
-
|
|
381
|
-
for (const rule of rules) {
|
|
382
|
-
const tokens = rule.tokens;
|
|
383
|
-
|
|
384
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
385
|
-
if (tokens[i].type !== 'ident' || tokens[i].value !== 'key') continue;
|
|
386
|
-
|
|
387
|
-
const equals = equalsIndex(tokens, i);
|
|
388
|
-
if (equals === -1) continue;
|
|
389
|
-
|
|
390
|
-
let j = equals + 1;
|
|
391
|
-
|
|
392
|
-
if (tokens[j]?.value === '(') {
|
|
393
|
-
for (j++; j < tokens.length && tokens[j].value !== ')'; j++) {
|
|
394
|
-
if (tokens[j].type === 'literal') properties.push(tokens[j].value);
|
|
395
|
-
}
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (tokens[j]?.type === 'literal') properties.push(tokens[j].value);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return properties.filter((value) => /^[A-Za-z_]\w*$/.test(value));
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Index of the `=` that assigns `tokens[i]` (`key=`, `key?=`, `key+=`), or `-1`
|
|
408
|
-
* when the identifier is not being assigned.
|
|
409
|
-
* @param {Token[]} tokens
|
|
410
|
-
* @param {number} i
|
|
411
|
-
*/
|
|
412
|
-
function equalsIndex(tokens, i) {
|
|
413
|
-
if (tokens[i]?.type !== 'ident') return -1;
|
|
414
|
-
|
|
415
|
-
const next = tokens[i + 1]?.value;
|
|
416
|
-
if (next === '=') return i + 1;
|
|
417
|
-
if ((next === '?' || next === '+') && tokens[i + 2]?.value === '=') return i + 2;
|
|
418
|
-
|
|
419
|
-
return -1;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* The connectors live in one fragment:
|
|
424
|
-
* dotKind=RelationKindDotRef | '-[' kind=[RelationshipKind] (… '->') | … | '->'
|
|
425
|
-
* @param {{ name: string, tokens: Token[] }[]} rules
|
|
426
|
-
*/
|
|
427
|
-
function findArrows(rules) {
|
|
428
|
-
const connector = rules.find((rule) => rule.name === 'RelationConnector');
|
|
429
|
-
const fromConnector = (connector?.tokens ?? [])
|
|
430
|
-
.filter((token) => token.type === 'literal' && token.value.includes('->'))
|
|
431
|
-
.map((token) => token.value);
|
|
432
|
-
|
|
433
|
-
// Longest first: TextMate alternation is first-match, so `]->` has to be
|
|
434
|
-
// tried before `->` at the same position.
|
|
435
|
-
return unique([...fromConnector, '->', '<->']).sort((a, b) => b.length - a.length || a.localeCompare(b));
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* A `returns string` rule whose body is only literals, `|` and references to
|
|
440
|
-
* other such rules is a closed value domain — `BorderStyleValue: LineOptions |
|
|
441
|
-
* 'none'` inherits from `LineOptions` that way. Anything referencing a rule
|
|
442
|
-
* that is not itself a value domain (e.g. `Id returns string`) resolves to
|
|
443
|
-
* nothing and is dropped.
|
|
444
|
-
* @param {{ name: string, returns?: string, tokens: Token[] }[]} rules
|
|
445
|
-
*/
|
|
446
|
-
function resolveEnums(rules) {
|
|
447
|
-
/** @type {Map<string, string[]>} */
|
|
448
|
-
const candidates = new Map();
|
|
449
|
-
|
|
450
|
-
for (const rule of rules) {
|
|
451
|
-
if (rule.returns !== 'string' || NOT_VALUE_DOMAINS.test(rule.name)) continue;
|
|
452
|
-
|
|
453
|
-
const onlyValuesAndChoices = rule.tokens.every(
|
|
454
|
-
(token) =>
|
|
455
|
-
token.type === 'literal' ||
|
|
456
|
-
token.type === 'ident' ||
|
|
457
|
-
(token.type === 'punct' && (token.value === '|' || token.value === ';')),
|
|
458
|
-
);
|
|
459
|
-
|
|
460
|
-
if (onlyValuesAndChoices) candidates.set(rule.name, []);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Fixpoint, because `BorderStyleValue` can only resolve once `LineOptions`
|
|
464
|
-
// has. One pass per rule is enough for any acyclic grammar.
|
|
465
|
-
for (let pass = 0; pass < rules.length; pass++) {
|
|
466
|
-
let changed = false;
|
|
467
|
-
|
|
468
|
-
for (const rule of rules) {
|
|
469
|
-
if (!candidates.has(rule.name)) continue;
|
|
470
|
-
|
|
471
|
-
/** @type {string[]} */
|
|
472
|
-
const resolved = [];
|
|
473
|
-
let complete = true;
|
|
474
|
-
|
|
475
|
-
for (const token of rule.tokens) {
|
|
476
|
-
if (token.type === 'literal') {
|
|
477
|
-
resolved.push(token.value);
|
|
478
|
-
continue;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (token.type !== 'ident') continue;
|
|
482
|
-
|
|
483
|
-
const inherited = candidates.get(token.value);
|
|
484
|
-
if (inherited === undefined) {
|
|
485
|
-
complete = false;
|
|
486
|
-
break;
|
|
487
|
-
}
|
|
488
|
-
resolved.push(...inherited);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const next = unique(resolved);
|
|
492
|
-
if (!complete || next.length === 0) continue;
|
|
493
|
-
|
|
494
|
-
if ((candidates.get(rule.name) ?? []).length !== next.length) {
|
|
495
|
-
candidates.set(rule.name, next);
|
|
496
|
-
changed = true;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
if (!changed) break;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/** @type {Record<string, string[]>} */
|
|
504
|
-
const enums = {};
|
|
505
|
-
for (const [name, values] of [...candidates].sort(([a], [b]) => a.localeCompare(b))) {
|
|
506
|
-
if (values.length > 0) enums[name] = values;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
return enums;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* @param {{ name: string, kind: string, hidden: boolean, tokens: Token[] }[]} rules
|
|
514
|
-
* @returns {Record<string, Terminal>}
|
|
515
|
-
*/
|
|
516
|
-
function readTerminals(rules) {
|
|
517
|
-
/** @type {Record<string, Terminal>} */
|
|
518
|
-
const terminals = {};
|
|
519
|
-
|
|
520
|
-
for (const rule of rules) {
|
|
521
|
-
if (rule.kind !== 'terminal') continue;
|
|
522
|
-
|
|
523
|
-
const token = rule.tokens.find((candidate) => candidate.type === 'regex' || candidate.type === 'literal');
|
|
524
|
-
if (!token) continue;
|
|
525
|
-
|
|
526
|
-
terminals[rule.name] = {
|
|
527
|
-
hidden: rule.hidden,
|
|
528
|
-
kind: token.type === 'regex' ? 'regex' : 'literal',
|
|
529
|
-
value: token.value,
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
return terminals;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* The alternatives of the first group in a terminal's regex:
|
|
538
|
-
* `(aws|azure|bootstrap|gcp|md|tech):[-\w]+` -> the icon packs.
|
|
539
|
-
* @param {Terminal | undefined} terminal
|
|
540
|
-
*/
|
|
541
|
-
function terminalAlternatives(terminal) {
|
|
542
|
-
if (!terminal || terminal.kind !== 'regex') return [];
|
|
543
|
-
|
|
544
|
-
const group = /\(([^()]*)\)/.exec(terminal.value);
|
|
545
|
-
if (!group) return [];
|
|
546
|
-
|
|
547
|
-
return unique(group[1].split('|').map((value) => value.trim())).filter((value) =>
|
|
548
|
-
/^[A-Za-z][\w-]*$/.test(value),
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
function unique(values) {
|
|
553
|
-
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// endregion
|
|
557
|
-
|
|
558
|
-
export default parseLangium;
|
|
559
|
-
|