@purview-dev/astroc4 1.0.0-prerelease.3 → 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/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/dist/like-c4.langium +1357 -0
- package/dist/likec4.tmLanguage.json +327 -0
- package/dist/translations.d.ts +21 -0
- package/package.json +2 -1
package/dist/index.js
ADDED
|
@@ -0,0 +1,1125 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3
|
+
import { LikeC4VitePlugin } from "likec4/vite-plugin";
|
|
4
|
+
|
|
5
|
+
// src/fences.ts
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
var require2 = createRequire(import.meta.url);
|
|
10
|
+
var likec4ApiPromise;
|
|
11
|
+
function loadLikeC4() {
|
|
12
|
+
likec4ApiPromise ??= Promise.resolve(require2("likec4").LikeC4);
|
|
13
|
+
return likec4ApiPromise;
|
|
14
|
+
}
|
|
15
|
+
function preloadLikeC4() {
|
|
16
|
+
return loadLikeC4();
|
|
17
|
+
}
|
|
18
|
+
var FENCE_LANG = "likec4";
|
|
19
|
+
var INLINE_COMPONENT = "__LikeC4Fence";
|
|
20
|
+
var PROJECT_COMPONENT = "__LikeC4View";
|
|
21
|
+
var PACKAGE_NAME = "@purview-dev/astroc4";
|
|
22
|
+
var IMPORTS = {
|
|
23
|
+
[INLINE_COMPONENT]: `${PACKAGE_NAME}/components/LikeC4Fence.astro`,
|
|
24
|
+
[PROJECT_COMPONENT]: `${PACKAGE_NAME}/components/LikeC4View.astro`
|
|
25
|
+
};
|
|
26
|
+
var layoutCache = new Map;
|
|
27
|
+
function parseFenceMeta(meta) {
|
|
28
|
+
const options = {};
|
|
29
|
+
if (!meta)
|
|
30
|
+
return options;
|
|
31
|
+
const pattern = /(\w+)=(?:"([^"]*)"|'([^']*)'|(\S+))/g;
|
|
32
|
+
let match;
|
|
33
|
+
while ((match = pattern.exec(meta)) !== null) {
|
|
34
|
+
const key = match[1];
|
|
35
|
+
if (!key)
|
|
36
|
+
continue;
|
|
37
|
+
options[key] = match[2] ?? match[3] ?? match[4] ?? "";
|
|
38
|
+
}
|
|
39
|
+
return options;
|
|
40
|
+
}
|
|
41
|
+
async function layoutSnippet(source) {
|
|
42
|
+
const key = createHash("sha256").update(source).digest("hex");
|
|
43
|
+
const cached = layoutCache.get(key);
|
|
44
|
+
if (cached)
|
|
45
|
+
return cached;
|
|
46
|
+
const LikeC4 = await loadLikeC4();
|
|
47
|
+
const likec4 = await LikeC4.fromSource(source);
|
|
48
|
+
if (likec4.hasErrors()) {
|
|
49
|
+
const detail = likec4.getErrors().map((error) => String(error.message).split(`
|
|
50
|
+
`)[0]?.trim() ?? "").slice(0, 3).join("; ");
|
|
51
|
+
throw new Error(detail || "the snippet could not be parsed");
|
|
52
|
+
}
|
|
53
|
+
const model = await likec4.layoutedModel();
|
|
54
|
+
const dump = JSON.parse(JSON.stringify(model.$data));
|
|
55
|
+
const result = { dump, viewIds: Object.keys(dump.views ?? {}) };
|
|
56
|
+
layoutCache.set(key, result);
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
function importNode(local, source) {
|
|
60
|
+
return {
|
|
61
|
+
type: "mdxjsEsm",
|
|
62
|
+
value: `import ${local} from ${JSON.stringify(source)};`,
|
|
63
|
+
data: {
|
|
64
|
+
estree: {
|
|
65
|
+
type: "Program",
|
|
66
|
+
sourceType: "module",
|
|
67
|
+
body: [
|
|
68
|
+
{
|
|
69
|
+
type: "ImportDeclaration",
|
|
70
|
+
specifiers: [
|
|
71
|
+
{ type: "ImportDefaultSpecifier", local: { type: "Identifier", name: local } }
|
|
72
|
+
],
|
|
73
|
+
source: { type: "Literal", value: source, raw: JSON.stringify(source) },
|
|
74
|
+
attributes: []
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function attributes(pairs) {
|
|
82
|
+
return Object.entries(pairs).filter(([, value]) => value !== undefined && value !== null && value !== "").map(([name, value]) => ({ type: "mdxJsxAttribute", name, value: String(value) }));
|
|
83
|
+
}
|
|
84
|
+
function describe(fileURL) {
|
|
85
|
+
if (!fileURL)
|
|
86
|
+
return "a content file";
|
|
87
|
+
try {
|
|
88
|
+
return fileURLToPath(fileURL);
|
|
89
|
+
} catch {
|
|
90
|
+
return String(fileURL);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function likec4Fences() {
|
|
94
|
+
return (factory) => {
|
|
95
|
+
if (factory.sourceFormat !== "mdx") {
|
|
96
|
+
if (!factory.source.includes("```" + FENCE_LANG))
|
|
97
|
+
return null;
|
|
98
|
+
return {
|
|
99
|
+
name: "likec4-fences:warn",
|
|
100
|
+
code(node, ctx) {
|
|
101
|
+
if (node.lang !== FENCE_LANG)
|
|
102
|
+
return;
|
|
103
|
+
console.warn(`[likec4-fences] ${describe(ctx.fileURL)} has a \`\`\`${FENCE_LANG} fence, but fences only ` + "render as diagrams in .mdx files. Rename the page to .mdx, or use <LikeC4View /> directly.");
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (!factory.source.includes("```" + FENCE_LANG))
|
|
108
|
+
return null;
|
|
109
|
+
const used = new Set;
|
|
110
|
+
return {
|
|
111
|
+
name: "likec4-fences",
|
|
112
|
+
async code(node, ctx) {
|
|
113
|
+
if (node.lang !== FENCE_LANG)
|
|
114
|
+
return;
|
|
115
|
+
const where = `${describe(ctx.fileURL)}: \`\`\`${FENCE_LANG} fence`;
|
|
116
|
+
const options = parseFenceMeta(node.meta);
|
|
117
|
+
const source = (node.value ?? "").trim();
|
|
118
|
+
const common = {
|
|
119
|
+
title: options.title,
|
|
120
|
+
description: options.description,
|
|
121
|
+
height: options.height,
|
|
122
|
+
collapsible: options.collapsible,
|
|
123
|
+
defaultOpen: options.defaultOpen
|
|
124
|
+
};
|
|
125
|
+
if (source.length === 0) {
|
|
126
|
+
if (!options.view) {
|
|
127
|
+
throw new Error(`${where} is empty, so it needs \`view="<id>"\` to name a view in the project model.`);
|
|
128
|
+
}
|
|
129
|
+
used.add(PROJECT_COMPONENT);
|
|
130
|
+
ctx.replaceNode(node, {
|
|
131
|
+
type: "mdxJsxFlowElement",
|
|
132
|
+
name: PROJECT_COMPONENT,
|
|
133
|
+
attributes: attributes({ ...common, viewId: options.view }),
|
|
134
|
+
children: []
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
let layout;
|
|
139
|
+
try {
|
|
140
|
+
layout = await layoutSnippet(source);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw new Error(`${where} is not valid LikeC4 — ${error instanceof Error ? error.message : String(error)}`);
|
|
143
|
+
}
|
|
144
|
+
const viewId = options.view ?? layout.viewIds[0];
|
|
145
|
+
if (!viewId || !layout.viewIds.includes(viewId)) {
|
|
146
|
+
throw new Error(`${where} has no view "${viewId ?? "(none)"}". Declared: ${layout.viewIds.join(", ") || "(none)"}.`);
|
|
147
|
+
}
|
|
148
|
+
used.add(INLINE_COMPONENT);
|
|
149
|
+
ctx.replaceNode(node, {
|
|
150
|
+
type: "mdxJsxFlowElement",
|
|
151
|
+
name: INLINE_COMPONENT,
|
|
152
|
+
attributes: attributes({ ...common, viewId, model: JSON.stringify(layout.dump) }),
|
|
153
|
+
children: []
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
after(root, ctx) {
|
|
157
|
+
if (used.size === 0)
|
|
158
|
+
return;
|
|
159
|
+
ctx.insertChildAt(root, 0, [...used].map((local) => importNode(local, IMPORTS[local])));
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/grammar/source.ts
|
|
166
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
167
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
168
|
+
import { join } from "node:path";
|
|
169
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
170
|
+
|
|
171
|
+
// src/grammar/langium.ts
|
|
172
|
+
var NOT_VALUE_DOMAINS = /Id$/;
|
|
173
|
+
var PROPERTY_SUPPLEMENTS = ["kind", "tag"];
|
|
174
|
+
function parseLangium(source) {
|
|
175
|
+
const statements = splitStatements(stripComments(source).replace(/^[ \t]*grammar[ \t]+[A-Za-z_]\w*[ \t]*$/gm, ""));
|
|
176
|
+
const rules = statements.map(readStatement).filter((rule) => rule !== undefined);
|
|
177
|
+
const parserRules = rules.filter((rule) => rule.kind === "rule" || rule.kind === "fragment");
|
|
178
|
+
if (parserRules.length === 0) {
|
|
179
|
+
throw new Error("the file contains no Langium parser rules — is this still `packages/language-server/src/like-c4.langium`?");
|
|
180
|
+
}
|
|
181
|
+
const enums = resolveEnums(parserRules);
|
|
182
|
+
const blocks = findBlocks(parserRules);
|
|
183
|
+
const properties = unique([...findProperties(parserRules), ...PROPERTY_SUPPLEMENTS]);
|
|
184
|
+
const arrows = findArrows(parserRules);
|
|
185
|
+
const literals = unique(parserRules.flatMap((rule) => rule.tokens.filter((t) => t.type === "literal").map((t) => t.value)));
|
|
186
|
+
const classified = new Set([...blocks, ...properties, ...Object.values(enums).flat()]);
|
|
187
|
+
const keywords = literals.filter((value) => /^[A-Za-z_]\w*$/.test(value) && !classified.has(value));
|
|
188
|
+
const terminals = readTerminals(rules);
|
|
189
|
+
if (keywords.length === 0 && blocks.length === 0 && Object.keys(enums).length === 0) {
|
|
190
|
+
throw new Error("the file contains no LikeC4 keywords — the Langium grammar could not be read");
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
blocks,
|
|
194
|
+
keywords,
|
|
195
|
+
properties,
|
|
196
|
+
arrows,
|
|
197
|
+
enums,
|
|
198
|
+
icons: terminalAlternatives(terminals.LIB_ICON),
|
|
199
|
+
booleans: terminalAlternatives(terminals.BOOLEAN),
|
|
200
|
+
ruleCount: parserRules.length
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function stripComments(source) {
|
|
204
|
+
let out = "";
|
|
205
|
+
let i = 0;
|
|
206
|
+
while (i < source.length) {
|
|
207
|
+
const char = source[i];
|
|
208
|
+
if (char === "/" && source[i + 1] === "/") {
|
|
209
|
+
while (i < source.length && source[i] !== `
|
|
210
|
+
`)
|
|
211
|
+
i++;
|
|
212
|
+
out += " ";
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (char === "/" && source[i + 1] === "*") {
|
|
216
|
+
i += 2;
|
|
217
|
+
while (i < source.length && !(source[i] === "*" && source[i + 1] === "/"))
|
|
218
|
+
i++;
|
|
219
|
+
i += 2;
|
|
220
|
+
out += " ";
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (char === "'" || char === '"') {
|
|
224
|
+
const end = endOfString(source, i);
|
|
225
|
+
out += source.slice(i, end);
|
|
226
|
+
i = end;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (char === "/") {
|
|
230
|
+
const end = endOfRegex(source, i);
|
|
231
|
+
out += source.slice(i, end);
|
|
232
|
+
i = end;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
out += char;
|
|
236
|
+
i++;
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
function endOfString(source, start) {
|
|
241
|
+
const quote = source[start];
|
|
242
|
+
let i = start + 1;
|
|
243
|
+
while (i < source.length) {
|
|
244
|
+
if (source[i] === "\\") {
|
|
245
|
+
i += 2;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (source[i] === quote)
|
|
249
|
+
return i + 1;
|
|
250
|
+
i++;
|
|
251
|
+
}
|
|
252
|
+
return source.length;
|
|
253
|
+
}
|
|
254
|
+
function endOfRegex(source, start) {
|
|
255
|
+
let i = start + 1;
|
|
256
|
+
let inClass = false;
|
|
257
|
+
while (i < source.length) {
|
|
258
|
+
const char = source[i];
|
|
259
|
+
if (char === "\\") {
|
|
260
|
+
i += 2;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (char === "[")
|
|
264
|
+
inClass = true;
|
|
265
|
+
else if (char === "]")
|
|
266
|
+
inClass = false;
|
|
267
|
+
else if (char === "/" && !inClass)
|
|
268
|
+
return i + 1;
|
|
269
|
+
i++;
|
|
270
|
+
}
|
|
271
|
+
return source.length;
|
|
272
|
+
}
|
|
273
|
+
function splitStatements(source) {
|
|
274
|
+
const statements = [];
|
|
275
|
+
let depth = 0;
|
|
276
|
+
let start = 0;
|
|
277
|
+
let i = 0;
|
|
278
|
+
while (i < source.length) {
|
|
279
|
+
const char = source[i];
|
|
280
|
+
if (char === "'" || char === '"') {
|
|
281
|
+
i = endOfString(source, i);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (char === "/") {
|
|
285
|
+
i = endOfRegex(source, i);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (char === "{")
|
|
289
|
+
depth++;
|
|
290
|
+
else if (char === "}")
|
|
291
|
+
depth = Math.max(0, depth - 1);
|
|
292
|
+
else if (char === ";" && depth === 0) {
|
|
293
|
+
statements.push(source.slice(start, i));
|
|
294
|
+
start = i + 1;
|
|
295
|
+
}
|
|
296
|
+
i++;
|
|
297
|
+
}
|
|
298
|
+
const tail = source.slice(start).trim();
|
|
299
|
+
if (tail.length > 0)
|
|
300
|
+
statements.push(tail);
|
|
301
|
+
return statements.filter((statement) => statement.trim().length > 0);
|
|
302
|
+
}
|
|
303
|
+
var HEADERS = [
|
|
304
|
+
{ kind: "terminal", hidden: true, pattern: /^\s*hidden\s+terminal\s+([A-Za-z_]\w*)\s*(?:<[^>]*>)?\s*:/ },
|
|
305
|
+
{
|
|
306
|
+
kind: "terminal",
|
|
307
|
+
hidden: false,
|
|
308
|
+
pattern: /^\s*terminal\s+(?:fragment\s+)?([A-Za-z_]\w*)\s*(?:<[^>]*>)?\s*(?:returns\s+[A-Za-z_]\w*\s*)?:/
|
|
309
|
+
},
|
|
310
|
+
{ kind: "fragment", hidden: false, pattern: /^\s*fragment\s+([A-Za-z_]\w*)\s*:/ },
|
|
311
|
+
{
|
|
312
|
+
kind: "rule",
|
|
313
|
+
hidden: false,
|
|
314
|
+
pattern: /^\s*(?:entry\s+)?([A-Za-z_]\w*)\s*(?:<[^>]*>)?\s*(?:returns\s+([A-Za-z_]\w*)\s*)?:/
|
|
315
|
+
}
|
|
316
|
+
];
|
|
317
|
+
function readStatement(statement) {
|
|
318
|
+
for (const header of HEADERS) {
|
|
319
|
+
const match = header.pattern.exec(statement);
|
|
320
|
+
const name = match?.[1];
|
|
321
|
+
if (!match || !name)
|
|
322
|
+
continue;
|
|
323
|
+
return {
|
|
324
|
+
name,
|
|
325
|
+
kind: header.kind,
|
|
326
|
+
hidden: header.hidden,
|
|
327
|
+
returns: header.kind === "rule" ? match[2] : undefined,
|
|
328
|
+
tokens: tokenize(statement.slice(match[0].length))
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
function tokenize(body) {
|
|
334
|
+
const tokens = [];
|
|
335
|
+
let i = 0;
|
|
336
|
+
while (i < body.length) {
|
|
337
|
+
const char = body.charAt(i);
|
|
338
|
+
if (/\s/.test(char)) {
|
|
339
|
+
i++;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (char === "'" || char === '"') {
|
|
343
|
+
const end = endOfString(body, i);
|
|
344
|
+
tokens.push({ type: "literal", value: unquote(body.slice(i, end)) });
|
|
345
|
+
i = end;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (char === "/") {
|
|
349
|
+
const end = endOfRegex(body, i);
|
|
350
|
+
tokens.push({ type: "regex", value: body.slice(i + 1, end - 1) });
|
|
351
|
+
i = end;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const identifier = /^[A-Za-z_]\w*/.exec(body.slice(i));
|
|
355
|
+
if (identifier) {
|
|
356
|
+
tokens.push({ type: "ident", value: identifier[0] });
|
|
357
|
+
i += identifier[0].length;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
tokens.push({ type: "punct", value: char });
|
|
361
|
+
i++;
|
|
362
|
+
}
|
|
363
|
+
return tokens;
|
|
364
|
+
}
|
|
365
|
+
function unquote(quoted) {
|
|
366
|
+
const inner = quoted.slice(1, -1);
|
|
367
|
+
let out = "";
|
|
368
|
+
for (let i = 0;i < inner.length; i++) {
|
|
369
|
+
if (inner[i] !== "\\") {
|
|
370
|
+
out += inner[i];
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const escaped = inner[++i];
|
|
374
|
+
if (escaped === "n")
|
|
375
|
+
out += `
|
|
376
|
+
`;
|
|
377
|
+
else if (escaped === "t")
|
|
378
|
+
out += "\t";
|
|
379
|
+
else if (escaped === "r")
|
|
380
|
+
out += "\r";
|
|
381
|
+
else
|
|
382
|
+
out += escaped ?? "";
|
|
383
|
+
}
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
386
|
+
function findBlocks(rules) {
|
|
387
|
+
return unique(rules.flatMap((rule) => {
|
|
388
|
+
const tokens = rule.tokens;
|
|
389
|
+
const first = tokens.findIndex((token) => token.type === "literal");
|
|
390
|
+
const keyword = first === -1 ? undefined : tokens[first]?.value;
|
|
391
|
+
if (!keyword || keyword === "{" || !/^[A-Za-z_]\w*$/.test(keyword))
|
|
392
|
+
return [];
|
|
393
|
+
for (let i = first + 1;i < tokens.length; i++) {
|
|
394
|
+
const token = tokens[i];
|
|
395
|
+
if (!token)
|
|
396
|
+
break;
|
|
397
|
+
if (token.value === "(" || token.value === "|")
|
|
398
|
+
return [];
|
|
399
|
+
if (token.type === "literal" && token.value === "{")
|
|
400
|
+
return [keyword];
|
|
401
|
+
}
|
|
402
|
+
return [];
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
405
|
+
function findProperties(rules) {
|
|
406
|
+
const properties = [];
|
|
407
|
+
for (const rule of rules) {
|
|
408
|
+
const tokens = rule.tokens;
|
|
409
|
+
for (let i = 0;i < tokens.length; i++) {
|
|
410
|
+
const token = tokens[i];
|
|
411
|
+
if (!token || token.type !== "ident" || token.value !== "key")
|
|
412
|
+
continue;
|
|
413
|
+
const equals = equalsIndex(tokens, i);
|
|
414
|
+
if (equals === -1)
|
|
415
|
+
continue;
|
|
416
|
+
let j = equals + 1;
|
|
417
|
+
if (tokens[j]?.value === "(") {
|
|
418
|
+
for (j++;j < tokens.length && tokens[j]?.value !== ")"; j++) {
|
|
419
|
+
const inner = tokens[j];
|
|
420
|
+
if (inner?.type === "literal")
|
|
421
|
+
properties.push(inner.value);
|
|
422
|
+
}
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const value = tokens[j];
|
|
426
|
+
if (value?.type === "literal")
|
|
427
|
+
properties.push(value.value);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return properties.filter((value) => /^[A-Za-z_]\w*$/.test(value));
|
|
431
|
+
}
|
|
432
|
+
function equalsIndex(tokens, i) {
|
|
433
|
+
if (tokens[i]?.type !== "ident")
|
|
434
|
+
return -1;
|
|
435
|
+
const next = tokens[i + 1]?.value;
|
|
436
|
+
if (next === "=")
|
|
437
|
+
return i + 1;
|
|
438
|
+
if ((next === "?" || next === "+") && tokens[i + 2]?.value === "=")
|
|
439
|
+
return i + 2;
|
|
440
|
+
return -1;
|
|
441
|
+
}
|
|
442
|
+
function findArrows(rules) {
|
|
443
|
+
const connector = rules.find((rule) => rule.name === "RelationConnector");
|
|
444
|
+
const fromConnector = (connector?.tokens ?? []).filter((token) => token.type === "literal" && token.value.includes("->")).map((token) => token.value);
|
|
445
|
+
return unique([...fromConnector, "->", "<->"]).sort((a, b) => b.length - a.length || a.localeCompare(b));
|
|
446
|
+
}
|
|
447
|
+
function resolveEnums(rules) {
|
|
448
|
+
const candidates = new Map;
|
|
449
|
+
for (const rule of rules) {
|
|
450
|
+
if (rule.returns !== "string" || NOT_VALUE_DOMAINS.test(rule.name))
|
|
451
|
+
continue;
|
|
452
|
+
const onlyValuesAndChoices = rule.tokens.every((token) => token.type === "literal" || token.type === "ident" || token.type === "punct" && (token.value === "|" || token.value === ";"));
|
|
453
|
+
if (onlyValuesAndChoices)
|
|
454
|
+
candidates.set(rule.name, []);
|
|
455
|
+
}
|
|
456
|
+
for (let pass = 0;pass < rules.length; pass++) {
|
|
457
|
+
let changed = false;
|
|
458
|
+
for (const rule of rules) {
|
|
459
|
+
if (!candidates.has(rule.name))
|
|
460
|
+
continue;
|
|
461
|
+
const resolved = [];
|
|
462
|
+
let complete = true;
|
|
463
|
+
for (const token of rule.tokens) {
|
|
464
|
+
if (token.type === "literal") {
|
|
465
|
+
resolved.push(token.value);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (token.type !== "ident")
|
|
469
|
+
continue;
|
|
470
|
+
const inherited = candidates.get(token.value);
|
|
471
|
+
if (inherited === undefined) {
|
|
472
|
+
complete = false;
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
resolved.push(...inherited);
|
|
476
|
+
}
|
|
477
|
+
const next = unique(resolved);
|
|
478
|
+
if (!complete || next.length === 0)
|
|
479
|
+
continue;
|
|
480
|
+
if ((candidates.get(rule.name) ?? []).length !== next.length) {
|
|
481
|
+
candidates.set(rule.name, next);
|
|
482
|
+
changed = true;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (!changed)
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
const enums = {};
|
|
489
|
+
for (const [name, values] of [...candidates].sort(([a], [b]) => a.localeCompare(b))) {
|
|
490
|
+
if (values.length > 0)
|
|
491
|
+
enums[name] = values;
|
|
492
|
+
}
|
|
493
|
+
return enums;
|
|
494
|
+
}
|
|
495
|
+
function readTerminals(rules) {
|
|
496
|
+
const terminals = {};
|
|
497
|
+
for (const rule of rules) {
|
|
498
|
+
if (rule.kind !== "terminal")
|
|
499
|
+
continue;
|
|
500
|
+
const token = rule.tokens.find((candidate) => candidate.type === "regex" || candidate.type === "literal");
|
|
501
|
+
if (!token)
|
|
502
|
+
continue;
|
|
503
|
+
terminals[rule.name] = {
|
|
504
|
+
hidden: rule.hidden,
|
|
505
|
+
kind: token.type === "regex" ? "regex" : "literal",
|
|
506
|
+
value: token.value
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
return terminals;
|
|
510
|
+
}
|
|
511
|
+
function terminalAlternatives(terminal) {
|
|
512
|
+
if (!terminal || terminal.kind !== "regex")
|
|
513
|
+
return [];
|
|
514
|
+
const group = /\(([^()]*)\)/.exec(terminal.value);
|
|
515
|
+
if (!group)
|
|
516
|
+
return [];
|
|
517
|
+
return unique((group[1] ?? "").split("|").map((value) => value.trim())).filter((value) => /^[A-Za-z][\w-]*$/.test(value));
|
|
518
|
+
}
|
|
519
|
+
function unique(values) {
|
|
520
|
+
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/grammar/textmate.ts
|
|
524
|
+
var SCOPE_NAME = "source.likec4";
|
|
525
|
+
var LANGUAGE_NAME = "likec4";
|
|
526
|
+
var LANGUAGE_ALIASES = ["c4"];
|
|
527
|
+
var TMLANGUAGE_SCHEMA = "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json";
|
|
528
|
+
var VALUE_DOMAINS = [
|
|
529
|
+
"BorderStyleValue",
|
|
530
|
+
"DynamicViewDisplayVariantValue",
|
|
531
|
+
"EdgeRoutingValue",
|
|
532
|
+
"ElementShape",
|
|
533
|
+
"IconPositionValue",
|
|
534
|
+
"LineOptions",
|
|
535
|
+
"ArrowType",
|
|
536
|
+
"RankValue",
|
|
537
|
+
"SizeValue",
|
|
538
|
+
"SizingValue",
|
|
539
|
+
"ThemeColor",
|
|
540
|
+
"ViewLayoutDirection"
|
|
541
|
+
];
|
|
542
|
+
var KEYWORD_DOMAINS = ["DynamicViewFlowKeyword"];
|
|
543
|
+
var VARIABLE_DOMAINS = ["Participant"];
|
|
544
|
+
var OPERATOR_WORDS = ["and", "or", "not", "is"];
|
|
545
|
+
var FALLBACK_ICON_PACKS = ["aws", "azure", "bootstrap", "gcp", "tech"];
|
|
546
|
+
function buildTmLanguage(vocabulary, meta = {}) {
|
|
547
|
+
const control = uniqueDomain(vocabulary, KEYWORD_DOMAINS);
|
|
548
|
+
const variables = uniqueDomain(vocabulary, VARIABLE_DOMAINS);
|
|
549
|
+
const icons = vocabulary.icons.length > 0 ? vocabulary.icons : FALLBACK_ICON_PACKS;
|
|
550
|
+
const reserved = alternationOf([...vocabulary.keywords, ...vocabulary.blocks, ...control]);
|
|
551
|
+
const grammar = {
|
|
552
|
+
$schema: TMLANGUAGE_SCHEMA,
|
|
553
|
+
...meta.repo && meta.ref ? {
|
|
554
|
+
$generated: {
|
|
555
|
+
repo: meta.repo,
|
|
556
|
+
ref: meta.ref,
|
|
557
|
+
...meta.sha ? { sha: meta.sha } : {},
|
|
558
|
+
by: "astroc4/scripts/sync-grammar.ts"
|
|
559
|
+
}
|
|
560
|
+
} : {},
|
|
561
|
+
name: LANGUAGE_NAME,
|
|
562
|
+
displayName: "LikeC4",
|
|
563
|
+
scopeName: SCOPE_NAME,
|
|
564
|
+
fileTypes: ["c4", "likec4"],
|
|
565
|
+
patterns: [{ include: "#everything" }],
|
|
566
|
+
repository: {
|
|
567
|
+
everything: {
|
|
568
|
+
patterns: [
|
|
569
|
+
"comments",
|
|
570
|
+
"strings",
|
|
571
|
+
"urls",
|
|
572
|
+
"icons",
|
|
573
|
+
"hex-colors",
|
|
574
|
+
"tags",
|
|
575
|
+
"typed-arrow",
|
|
576
|
+
"declarations",
|
|
577
|
+
"keywords",
|
|
578
|
+
"numbers",
|
|
579
|
+
"operators",
|
|
580
|
+
"punctuation"
|
|
581
|
+
].map((name) => ({ include: `#${name}` }))
|
|
582
|
+
},
|
|
583
|
+
comments: comments(),
|
|
584
|
+
strings: strings(),
|
|
585
|
+
urls: urls(),
|
|
586
|
+
icons: iconIds(icons),
|
|
587
|
+
"hex-colors": hexColours(),
|
|
588
|
+
tags: tags(),
|
|
589
|
+
"typed-arrow": typedArrow(),
|
|
590
|
+
declarations: declarations(reserved),
|
|
591
|
+
keywords: keywords({ ...vocabulary, control, variables }),
|
|
592
|
+
numbers: numbers(),
|
|
593
|
+
operators: operators(vocabulary.arrows),
|
|
594
|
+
punctuation: punctuation()
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
if (meta.repo && meta.ref) {
|
|
598
|
+
grammar.$generated = {
|
|
599
|
+
repo: meta.repo,
|
|
600
|
+
ref: meta.ref,
|
|
601
|
+
...meta.sha ? { sha: meta.sha } : {},
|
|
602
|
+
by: "astroc4/scripts/sync-grammar.ts"
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
return grammar;
|
|
606
|
+
}
|
|
607
|
+
function comments() {
|
|
608
|
+
return {
|
|
609
|
+
patterns: [
|
|
610
|
+
{
|
|
611
|
+
name: "comment.block.likec4",
|
|
612
|
+
begin: "/\\*",
|
|
613
|
+
end: "\\*/",
|
|
614
|
+
captures: { 0: { name: "punctuation.definition.comment.likec4" } }
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
name: "comment.line.double-slash.likec4",
|
|
618
|
+
match: "//.*$",
|
|
619
|
+
captures: { 0: { name: "punctuation.definition.comment.likec4" } }
|
|
620
|
+
}
|
|
621
|
+
]
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function strings() {
|
|
625
|
+
const triple = (quote) => ({
|
|
626
|
+
name: "string.quoted.triple.likec4",
|
|
627
|
+
begin: quote.repeat(3),
|
|
628
|
+
end: quote.repeat(3),
|
|
629
|
+
beginCaptures: { 0: { name: "punctuation.definition.string.begin.likec4" } },
|
|
630
|
+
endCaptures: { 0: { name: "punctuation.definition.string.end.likec4" } }
|
|
631
|
+
});
|
|
632
|
+
const single = (quote, name) => ({
|
|
633
|
+
name,
|
|
634
|
+
begin: quote,
|
|
635
|
+
end: `${quote}|$`,
|
|
636
|
+
beginCaptures: { 0: { name: "punctuation.definition.string.begin.likec4" } },
|
|
637
|
+
endCaptures: { 0: { name: "punctuation.definition.string.end.likec4" } },
|
|
638
|
+
patterns: [{ name: "constant.character.escape.likec4", match: "\\\\." }]
|
|
639
|
+
});
|
|
640
|
+
return {
|
|
641
|
+
patterns: [
|
|
642
|
+
triple("'"),
|
|
643
|
+
triple('"'),
|
|
644
|
+
single("'", "string.quoted.single.likec4"),
|
|
645
|
+
single('"', "string.quoted.double.likec4")
|
|
646
|
+
]
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function urls() {
|
|
650
|
+
return { name: "markup.underline.link.likec4", match: `\\bhttps?://[^\\s'"]+` };
|
|
651
|
+
}
|
|
652
|
+
function iconIds(packs) {
|
|
653
|
+
return {
|
|
654
|
+
comment: "Bundled icon ids: `tech:nginx`, `aws:…`, `gcp:…`, …",
|
|
655
|
+
match: `\\b(${alternationOf(packs)})(:)([\\w-]+)`,
|
|
656
|
+
captures: {
|
|
657
|
+
1: { name: "support.constant.icon-pack.likec4" },
|
|
658
|
+
2: { name: "punctuation.separator.likec4" },
|
|
659
|
+
3: { name: "support.constant.icon.likec4" }
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function hexColours() {
|
|
664
|
+
return {
|
|
665
|
+
name: "constant.other.color.rgb-value.hex.likec4",
|
|
666
|
+
match: "#(?:[0-9A-Fa-f]{8}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})(?![\\w-])"
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
function tags() {
|
|
670
|
+
return { name: "entity.name.tag.likec4", match: "#[A-Za-z_][\\w-]*" };
|
|
671
|
+
}
|
|
672
|
+
function typedArrow() {
|
|
673
|
+
return {
|
|
674
|
+
comment: "`a -[https]-> b`",
|
|
675
|
+
match: "(-\\[)\\s*([A-Za-z_][\\w-]*)\\s*(\\]->)",
|
|
676
|
+
captures: {
|
|
677
|
+
1: { name: "keyword.operator.arrow.likec4" },
|
|
678
|
+
2: { name: "entity.name.type.relationship.likec4" },
|
|
679
|
+
3: { name: "keyword.operator.arrow.likec4" }
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function declarations(reserved) {
|
|
684
|
+
const notReserved = reserved ? `(?!(?:${reserved})\\b)` : "";
|
|
685
|
+
return {
|
|
686
|
+
patterns: [
|
|
687
|
+
{
|
|
688
|
+
comment: "`element system`, `deploymentNode region`, `tag tls`, `relationship https`. Not `where tag is #x`.",
|
|
689
|
+
match: "\\b(element|deploymentNode|tag|relationship)\\s+(?!is\\b)([A-Za-z_][\\w-]*)(?=\\s*(?:\\{|$|//))",
|
|
690
|
+
captures: {
|
|
691
|
+
1: { name: "storage.type.likec4" },
|
|
692
|
+
2: { name: "entity.name.type.likec4" }
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
comment: "`color testingBlue #0046BE` declares; `color testingBlue` inside a style block only refers.",
|
|
697
|
+
match: "\\b(color)\\s+([A-Za-z_][\\w-]*)(?=\\s+#)",
|
|
698
|
+
captures: {
|
|
699
|
+
1: { name: "storage.type.likec4" },
|
|
700
|
+
2: { name: "entity.name.type.likec4" }
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
comment: "`view index`, `dynamic view firstRequest`, `deployment view prod`, `view internals of site`",
|
|
705
|
+
match: "\\b(?:(dynamic|deployment)\\s+)?(view)\\s+([A-Za-z_]\\w*)",
|
|
706
|
+
captures: {
|
|
707
|
+
1: { name: "storage.modifier.likec4" },
|
|
708
|
+
2: { name: "storage.type.view.likec4" },
|
|
709
|
+
3: { name: "entity.name.function.view.likec4" }
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
comment: "`reader = actor 'Engineer'`",
|
|
714
|
+
match: `\\b([A-Za-z_]\\w*)\\s*(=)(?!=)\\s*${notReserved}([A-Za-z_]\\w*)?`,
|
|
715
|
+
captures: {
|
|
716
|
+
1: { name: "variable.other.definition.likec4" },
|
|
717
|
+
2: { name: "keyword.operator.assignment.likec4" },
|
|
718
|
+
3: { name: "entity.name.type.likec4" }
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
]
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function keywords(vocabulary) {
|
|
725
|
+
const patterns = [];
|
|
726
|
+
const blocks = alternationOf(vocabulary.blocks);
|
|
727
|
+
if (blocks) {
|
|
728
|
+
patterns.push({
|
|
729
|
+
comment: "Keywords that open a block: `model { … }`.",
|
|
730
|
+
name: "keyword.control.block.likec4",
|
|
731
|
+
match: `\\b(?:${blocks})\\b(?=\\s*\\{)`
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
const control = alternationOf([...vocabulary.keywords, ...vocabulary.control, ...vocabulary.blocks]);
|
|
735
|
+
patterns.push({
|
|
736
|
+
name: "keyword.operator.word.likec4",
|
|
737
|
+
match: `\\b(?:${alternationOf(OPERATOR_WORDS)})\\b`
|
|
738
|
+
});
|
|
739
|
+
if (control) {
|
|
740
|
+
patterns.push({ name: "keyword.control.likec4", match: `\\b(?:${control})\\b` });
|
|
741
|
+
}
|
|
742
|
+
const variables = alternationOf(vocabulary.variables);
|
|
743
|
+
if (variables) {
|
|
744
|
+
patterns.push({ name: "variable.language.likec4", match: `\\b(?:${variables})\\b` });
|
|
745
|
+
}
|
|
746
|
+
const properties = alternationOf(vocabulary.properties);
|
|
747
|
+
if (properties) {
|
|
748
|
+
patterns.push({ name: "support.type.property-name.likec4", match: `\\b(?:${properties})\\b` });
|
|
749
|
+
}
|
|
750
|
+
const values = alternationOf(uniqueDomain(vocabulary, VALUE_DOMAINS));
|
|
751
|
+
if (values) {
|
|
752
|
+
patterns.push({ name: "constant.language.likec4", match: `\\b(?:${values})\\b` });
|
|
753
|
+
}
|
|
754
|
+
return { patterns };
|
|
755
|
+
}
|
|
756
|
+
function numbers() {
|
|
757
|
+
return { name: "constant.numeric.likec4", match: "\\b\\d+(?:\\.\\d+)?%?" };
|
|
758
|
+
}
|
|
759
|
+
function operators(arrows) {
|
|
760
|
+
const patterns = [];
|
|
761
|
+
const connectors = alternationOf(arrows);
|
|
762
|
+
if (connectors) {
|
|
763
|
+
patterns.push({ name: "keyword.operator.arrow.likec4", match: connectors });
|
|
764
|
+
}
|
|
765
|
+
patterns.push({ name: "keyword.operator.wildcard.likec4", match: "\\.\\*\\*|\\.\\*|\\*\\*|\\*" });
|
|
766
|
+
return { patterns };
|
|
767
|
+
}
|
|
768
|
+
function punctuation() {
|
|
769
|
+
return {
|
|
770
|
+
patterns: [
|
|
771
|
+
{ name: "punctuation.section.block.begin.likec4", match: "\\{" },
|
|
772
|
+
{ name: "punctuation.section.block.end.likec4", match: "\\}" },
|
|
773
|
+
{ name: "punctuation.separator.comma.likec4", match: "," },
|
|
774
|
+
{ name: "punctuation.accessor.likec4", match: "\\." }
|
|
775
|
+
]
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
function uniqueDomain(vocabulary, names) {
|
|
779
|
+
const values = names.flatMap((name) => vocabulary.enums[name] ?? []);
|
|
780
|
+
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
781
|
+
}
|
|
782
|
+
function alternationOf(values) {
|
|
783
|
+
return values.map(escapeRegExp).join("|");
|
|
784
|
+
}
|
|
785
|
+
function escapeRegExp(value) {
|
|
786
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/grammar/source.ts
|
|
790
|
+
var DEFAULT_REPO = "likec4/likec4";
|
|
791
|
+
var DEFAULT_REF = "main";
|
|
792
|
+
var LANGIUM_PATH = "packages/language-server/src/like-c4.langium";
|
|
793
|
+
var DEFAULT_BASE_URL = "https://raw.githubusercontent.com";
|
|
794
|
+
var CACHE_VERSION = 1;
|
|
795
|
+
var BUNDLED_GRAMMAR = new URL("./likec4.tmLanguage.json", import.meta.url);
|
|
796
|
+
var BUNDLED_LANGIUM = new URL("./like-c4.langium", import.meta.url);
|
|
797
|
+
var REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
798
|
+
|
|
799
|
+
class GrammarRefError extends Error {
|
|
800
|
+
name = "GrammarRefError";
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
class GrammarFetchError extends Error {
|
|
804
|
+
name = "GrammarFetchError";
|
|
805
|
+
}
|
|
806
|
+
async function resolveLikeC4Grammar(options = {}) {
|
|
807
|
+
const env = options.env ?? process.env;
|
|
808
|
+
const repo = options.repo ?? DEFAULT_REPO;
|
|
809
|
+
const ref = env.ASTROC4_REF || options.ref || DEFAULT_REF;
|
|
810
|
+
const offline = options.offline ?? isTruthy(env.ASTROC4_OFFLINE);
|
|
811
|
+
const onError = options.onError ?? "fail";
|
|
812
|
+
const logger = options.logger;
|
|
813
|
+
validateRef(ref);
|
|
814
|
+
if (offline) {
|
|
815
|
+
return bundled("offline: true", options);
|
|
816
|
+
}
|
|
817
|
+
const cache = await readCache(options, { repo, ref });
|
|
818
|
+
if (cache?.fresh && !options.refresh) {
|
|
819
|
+
return derive(cache.langium, { repo, ref, source: "cache", fetchedAt: cache.fetchedAt, etag: cache.etag });
|
|
820
|
+
}
|
|
821
|
+
let fetched;
|
|
822
|
+
try {
|
|
823
|
+
fetched = await fetchLangiumSource({ ...options, repo, ref, etag: cache?.etag });
|
|
824
|
+
} catch (error) {
|
|
825
|
+
if (!(error instanceof Error))
|
|
826
|
+
throw error;
|
|
827
|
+
if (error instanceof GrammarRefError)
|
|
828
|
+
throw error;
|
|
829
|
+
if (onError === "fail")
|
|
830
|
+
throw error;
|
|
831
|
+
if (onError === "warn")
|
|
832
|
+
logger?.warn?.(`[astroc4] ${error.message}`);
|
|
833
|
+
if (cache) {
|
|
834
|
+
if (onError === "warn") {
|
|
835
|
+
logger?.warn?.(`[astroc4] using the grammar cached at ${new Date(cache.fetchedAt).toISOString()}`);
|
|
836
|
+
}
|
|
837
|
+
return derive(cache.langium, {
|
|
838
|
+
repo,
|
|
839
|
+
ref,
|
|
840
|
+
source: "cache",
|
|
841
|
+
fetchedAt: cache.fetchedAt,
|
|
842
|
+
etag: cache.etag
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
return bundled(error.message, options);
|
|
846
|
+
}
|
|
847
|
+
if ("notModified" in fetched) {
|
|
848
|
+
if (!cache) {
|
|
849
|
+
throw new GrammarFetchError("the grammar host answered 304 for a grammar that is not cached — retry with `refresh: true`");
|
|
850
|
+
}
|
|
851
|
+
await writeCache(options, { repo, ref }, cache);
|
|
852
|
+
return derive(cache.langium, { repo, ref, source: "cache", fetchedAt: cache.fetchedAt, etag: cache.etag });
|
|
853
|
+
}
|
|
854
|
+
const result = derive(fetched.text, { repo, ref, source: "network", etag: fetched.etag });
|
|
855
|
+
await writeCache(options, { repo, ref }, { langium: fetched.text, etag: fetched.etag });
|
|
856
|
+
return result;
|
|
857
|
+
}
|
|
858
|
+
function derive(langium, info) {
|
|
859
|
+
let vocabulary;
|
|
860
|
+
try {
|
|
861
|
+
vocabulary = parseLangium(langium);
|
|
862
|
+
} catch (cause) {
|
|
863
|
+
throw new GrammarFetchError(`${info.repo}@${info.ref}: the grammar could not be read — ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
864
|
+
}
|
|
865
|
+
const sha = grammarSha(langium);
|
|
866
|
+
return {
|
|
867
|
+
grammar: buildTmLanguage(vocabulary, { repo: info.repo, ref: info.ref, sha }),
|
|
868
|
+
langium,
|
|
869
|
+
meta: {
|
|
870
|
+
repo: info.repo,
|
|
871
|
+
ref: info.ref,
|
|
872
|
+
source: info.source,
|
|
873
|
+
fetchedAt: info.fetchedAt ?? Date.now(),
|
|
874
|
+
etag: info.etag,
|
|
875
|
+
sha
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
async function bundled(reason, options) {
|
|
880
|
+
const path = options.bundledLangiumPath ?? fileURLToPath2(BUNDLED_LANGIUM);
|
|
881
|
+
const snapshot = await bundledProvenance();
|
|
882
|
+
const source = derive(await readFile(path, "utf8"), {
|
|
883
|
+
repo: snapshot.repo ?? DEFAULT_REPO,
|
|
884
|
+
ref: snapshot.ref ?? DEFAULT_REF,
|
|
885
|
+
source: "bundled"
|
|
886
|
+
});
|
|
887
|
+
source.meta.reason = reason;
|
|
888
|
+
return source;
|
|
889
|
+
}
|
|
890
|
+
async function bundledProvenance() {
|
|
891
|
+
try {
|
|
892
|
+
const { $generated } = JSON.parse(await readFile(fileURLToPath2(BUNDLED_GRAMMAR), "utf8"));
|
|
893
|
+
return { repo: $generated?.repo, ref: $generated?.ref };
|
|
894
|
+
} catch {
|
|
895
|
+
return {};
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
function grammarSha(source) {
|
|
899
|
+
return createHash2("sha256").update(source).digest("hex").slice(0, 12);
|
|
900
|
+
}
|
|
901
|
+
function validateRef(ref) {
|
|
902
|
+
if (typeof ref !== "string" || ref.length === 0) {
|
|
903
|
+
throw new GrammarRefError("the LikeC4 grammar `ref` must be a branch, tag or commit SHA");
|
|
904
|
+
}
|
|
905
|
+
if (!REF_PATTERN.test(ref) || ref.includes("..") || ref.endsWith("/") || ref.includes("//")) {
|
|
906
|
+
throw new GrammarRefError(`"${ref}" is not a usable LikeC4 grammar ref — expected a branch, tag or commit SHA such as "main" or "v1.59.4"`);
|
|
907
|
+
}
|
|
908
|
+
return ref;
|
|
909
|
+
}
|
|
910
|
+
async function fetchLangiumSource(options) {
|
|
911
|
+
const repo = options.repo;
|
|
912
|
+
const ref = options.ref;
|
|
913
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
914
|
+
const url = `${baseUrl}/${repo}/${ref}/${LANGIUM_PATH}`;
|
|
915
|
+
const timeout = options.timeout ?? 15000;
|
|
916
|
+
const request = options.fetchImpl ?? fetch;
|
|
917
|
+
const headers = { "user-agent": "astroc4", accept: "text/plain" };
|
|
918
|
+
if (options.etag)
|
|
919
|
+
headers["if-none-match"] = options.etag;
|
|
920
|
+
let response;
|
|
921
|
+
try {
|
|
922
|
+
response = await request(url, { headers, redirect: "follow", signal: AbortSignal.timeout(timeout) });
|
|
923
|
+
} catch (cause) {
|
|
924
|
+
throw new GrammarFetchError(`could not fetch the LikeC4 grammar from ${url} — ${cause instanceof Error ? cause.message : String(cause)}. ` + "Builds are strict by default: pass `highlighting: { offline: true }` to use the bundled grammar instead.", { cause });
|
|
925
|
+
}
|
|
926
|
+
if (response.status === 304)
|
|
927
|
+
return { notModified: true };
|
|
928
|
+
if (response.status === 404) {
|
|
929
|
+
throw new GrammarRefError(`${repo} has no "${ref}", or it no longer has ${LANGIUM_PATH} (${url}). ` + 'Use a tag such as "v1.59.4", a commit SHA, or "main".');
|
|
930
|
+
}
|
|
931
|
+
if (!response.ok) {
|
|
932
|
+
throw new GrammarFetchError(`${url} responded ${response.status} ${response.statusText} — set ` + '`highlighting: { onError: "warn" }` to fall back to the bundled grammar.');
|
|
933
|
+
}
|
|
934
|
+
const text = await response.text();
|
|
935
|
+
if (text.trim().length === 0) {
|
|
936
|
+
throw new GrammarFetchError(`${url} returned an empty file — the grammar is not usable`);
|
|
937
|
+
}
|
|
938
|
+
return { text, etag: response.headers.get("etag") ?? undefined };
|
|
939
|
+
}
|
|
940
|
+
function cachePath(options, identity) {
|
|
941
|
+
const dir = options.cacheDir ?? join(options.root ?? process.cwd(), "node_modules", ".cache", "astroc4");
|
|
942
|
+
const key = createHash2("sha256").update(`${identity.repo}@${identity.ref}`).digest("hex").slice(0, 16);
|
|
943
|
+
return join(dir, `grammar-${key}.json`);
|
|
944
|
+
}
|
|
945
|
+
async function readCache(options, identity) {
|
|
946
|
+
let raw;
|
|
947
|
+
try {
|
|
948
|
+
raw = await readFile(cachePath(options, identity), "utf8");
|
|
949
|
+
} catch {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
try {
|
|
953
|
+
const entry = JSON.parse(raw);
|
|
954
|
+
if (entry?.version !== CACHE_VERSION || entry.repo !== identity.repo || entry.ref !== identity.ref || typeof entry.langium !== "string" || typeof entry.fetchedAt !== "number") {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const ttl = options.cacheTtl ?? 3600;
|
|
958
|
+
return { ...entry, fresh: Date.now() - entry.fetchedAt < ttl * 1000 };
|
|
959
|
+
} catch {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
async function writeCache(options, identity, entry) {
|
|
964
|
+
const path = cachePath(options, identity);
|
|
965
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
966
|
+
try {
|
|
967
|
+
await mkdir(join(path, ".."), { recursive: true });
|
|
968
|
+
await writeFile(temporary, JSON.stringify({
|
|
969
|
+
version: CACHE_VERSION,
|
|
970
|
+
repo: identity.repo,
|
|
971
|
+
ref: identity.ref,
|
|
972
|
+
etag: entry.etag,
|
|
973
|
+
fetchedAt: Date.now(),
|
|
974
|
+
langium: entry.langium
|
|
975
|
+
}), "utf8");
|
|
976
|
+
await rename(temporary, path);
|
|
977
|
+
} catch (error) {
|
|
978
|
+
options.logger?.warn?.(`[astroc4] could not cache the LikeC4 grammar in ${path} — ${error instanceof Error ? error.message : String(error)}`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function isTruthy(value) {
|
|
982
|
+
return typeof value === "string" && /^(1|true|yes|on)$/i.test(value.trim());
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// src/translations.ts
|
|
986
|
+
var translations = {
|
|
987
|
+
en: {
|
|
988
|
+
"likec4.view.title": "Architecture diagram",
|
|
989
|
+
"likec4.view.description": "Interactive LikeC4 diagram for this section.",
|
|
990
|
+
"likec4.view.hint": "Click a node in the diagram to open its details.",
|
|
991
|
+
"likec4.view.badge": "Click any node to open the modal",
|
|
992
|
+
"likec4.view.loading": "Loading architecture diagram…",
|
|
993
|
+
"likec4.fence.title": "Diagram",
|
|
994
|
+
"likec4.fence.hint": "Click any node to open its details.",
|
|
995
|
+
"likec4.fence.loading": "Loading diagram…",
|
|
996
|
+
"likec4.error.title": "We couldn’t load this diagram.",
|
|
997
|
+
"likec4.error.body": "Please retry, or refresh the page if the issue persists.",
|
|
998
|
+
"likec4.error.retry": "Retry",
|
|
999
|
+
"likec4.error.viewIdRequired": "A LikeC4 viewId is required to render the diagram.",
|
|
1000
|
+
"likec4.error.noProjects": "No LikeC4 projects are available in this workspace.",
|
|
1001
|
+
"likec4.error.unknownProject": 'Unknown LikeC4 projectId "{projectId}". Available projects: {available}.',
|
|
1002
|
+
"likec4.error.unresolvedProject": "Unable to resolve a LikeC4 project.",
|
|
1003
|
+
"likec4.error.loadFailed": 'Unable to load LikeC4 project "{projectId}".'
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
// src/index.ts
|
|
1008
|
+
var PACKAGE_NAME2 = "@purview-dev/astroc4";
|
|
1009
|
+
function starlightLikeC4(options = {}) {
|
|
1010
|
+
return {
|
|
1011
|
+
name: PACKAGE_NAME2,
|
|
1012
|
+
hooks: {
|
|
1013
|
+
"i18n:setup"({ injectTranslations }) {
|
|
1014
|
+
injectTranslations(translations);
|
|
1015
|
+
},
|
|
1016
|
+
async "config:setup"({ config, astroConfig, updateConfig, addIntegration, logger }) {
|
|
1017
|
+
addIntegration(likec4Integration(options));
|
|
1018
|
+
if (options.highlighting === false)
|
|
1019
|
+
return;
|
|
1020
|
+
await injectHighlighting({
|
|
1021
|
+
root: fileURLToPath3(astroConfig.root),
|
|
1022
|
+
expressiveCode: config.expressiveCode,
|
|
1023
|
+
updateConfig,
|
|
1024
|
+
logger,
|
|
1025
|
+
options: typeof options.highlighting === "object" ? options.highlighting : {}
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
async function injectHighlighting({
|
|
1032
|
+
root,
|
|
1033
|
+
expressiveCode,
|
|
1034
|
+
updateConfig,
|
|
1035
|
+
logger,
|
|
1036
|
+
options
|
|
1037
|
+
}) {
|
|
1038
|
+
if (expressiveCode === false) {
|
|
1039
|
+
logger.warn("LikeC4 syntax highlighting is skipped: `expressiveCode: false` disables the code blocks it " + "would highlight. Diagrams are unaffected.");
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
const source = await resolveLikeC4Grammar({ root, ...options, logger });
|
|
1043
|
+
const registration = {
|
|
1044
|
+
...source.grammar,
|
|
1045
|
+
name: LANGUAGE_NAME,
|
|
1046
|
+
aliases: [...LANGUAGE_ALIASES]
|
|
1047
|
+
};
|
|
1048
|
+
updateConfig({
|
|
1049
|
+
expressiveCode: withLikeC4Language(expressiveCode, registration)
|
|
1050
|
+
});
|
|
1051
|
+
const { repo, ref, sha, source: from } = source.meta;
|
|
1052
|
+
logger.info(`LikeC4 grammar ${repo}@${ref} (${sha})${from === "network" ? "" : ` from ${from}`} — ` + "highlighting ```c4 and ```likec4 fences");
|
|
1053
|
+
}
|
|
1054
|
+
function withLikeC4Language(expressiveCode, registration) {
|
|
1055
|
+
const current = isObject(expressiveCode) ? expressiveCode : {};
|
|
1056
|
+
const shiki = isObject(current.shiki) ? current.shiki : {};
|
|
1057
|
+
const langs = Array.isArray(shiki.langs) ? [...shiki.langs] : [];
|
|
1058
|
+
const existing = langs.findIndex(isLikeC4Language);
|
|
1059
|
+
if (existing === -1)
|
|
1060
|
+
langs.push(registration);
|
|
1061
|
+
else
|
|
1062
|
+
langs[existing] = registration;
|
|
1063
|
+
return { ...current, shiki: { ...shiki, langs } };
|
|
1064
|
+
}
|
|
1065
|
+
function isLikeC4Language(entry) {
|
|
1066
|
+
if (typeof entry === "string")
|
|
1067
|
+
return entry === LANGUAGE_NAME || LANGUAGE_ALIASES.includes(entry);
|
|
1068
|
+
if (!isObject(entry))
|
|
1069
|
+
return false;
|
|
1070
|
+
if (entry.name === LANGUAGE_NAME || entry.scopeName === "source.likec4")
|
|
1071
|
+
return true;
|
|
1072
|
+
return Array.isArray(entry.aliases) && entry.aliases.some((alias) => LANGUAGE_ALIASES.includes(alias));
|
|
1073
|
+
}
|
|
1074
|
+
function isObject(value) {
|
|
1075
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1076
|
+
}
|
|
1077
|
+
function likec4Integration({ vitePlugin = {}, fences = true }) {
|
|
1078
|
+
return {
|
|
1079
|
+
name: `${PACKAGE_NAME2}/integration`,
|
|
1080
|
+
hooks: {
|
|
1081
|
+
"astro:config:setup": async ({ command, config, updateConfig, logger }) => {
|
|
1082
|
+
updateConfig({
|
|
1083
|
+
vite: {
|
|
1084
|
+
plugins: [LikeC4VitePlugin(vitePlugin)],
|
|
1085
|
+
ssr: { noExternal: [PACKAGE_NAME2] }
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
if (fences) {
|
|
1089
|
+
await registerFences(config, updateConfig, logger);
|
|
1090
|
+
}
|
|
1091
|
+
if (command === "build") {
|
|
1092
|
+
await preloadLikeC4();
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
async function registerFences(config, updateConfig, logger) {
|
|
1099
|
+
const { satteri, isSatteriProcessor } = await import("@astrojs/markdown-satteri");
|
|
1100
|
+
const existing = config.markdown?.processor;
|
|
1101
|
+
if (existing === undefined) {
|
|
1102
|
+
updateConfig({ markdown: { processor: satteri({ mdastPlugins: [likec4Fences()] }) } });
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (!isSatteriProcessor(existing)) {
|
|
1106
|
+
logger.warn("```likec4 fences are disabled: the site is not using the Sätteri Markdown processor. " + "<LikeC4View /> still works.");
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
const existingOptions = existing.options ?? {};
|
|
1110
|
+
updateConfig({
|
|
1111
|
+
markdown: {
|
|
1112
|
+
processor: satteri({
|
|
1113
|
+
...existingOptions,
|
|
1114
|
+
mdastPlugins: [...existingOptions.mdastPlugins ?? [], likec4Fences()]
|
|
1115
|
+
})
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
export {
|
|
1120
|
+
starlightLikeC4 as default,
|
|
1121
|
+
withLikeC4Language
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
//# debugId=BA2D1DBED70A424964756E2164756E21
|
|
1125
|
+
//# sourceMappingURL=index.js.map
|