@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.
@@ -1,439 +0,0 @@
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
-
package/src/index.d.mts DELETED
@@ -1,64 +0,0 @@
1
- // index.d.mts
2
- // path: src/index.d.mts
3
- // Types for src/index.mjs. TypeScript does not read JSDoc from a package under
4
- // node_modules, so a site's astro.config.mjs needs these to type-check.
5
-
6
- import type { StarlightPlugin } from '@astrojs/starlight/types';
7
- import type { LikeC4VitePlugin } from 'likec4/vite-plugin';
8
-
9
- /**
10
- * LikeC4 syntax highlighting, derived from the Langium grammar in
11
- * `likec4/likec4` and registered with the site's Expressive Code.
12
- */
13
- export interface LikeC4HighlightingOptions {
14
- /**
15
- * Branch, tag or commit SHA to read `like-c4.langium` from.
16
- * Defaults to `main`; pin a tag (`v1.59.4`) or a SHA for reproducible CI.
17
- * Also read from `ASTROC4_REF`.
18
- */
19
- ref?: string;
20
- /** Repository to read from. Defaults to `likec4/likec4`. */
21
- repo?: string;
22
- /** Use the bundled snapshot and never touch the network. Also `ASTROC4_OFFLINE=1`. */
23
- offline?: boolean;
24
- /**
25
- * What to do when the grammar cannot be fetched. Defaults to `fail`, so
26
- * `astro build` and `astro check` fail like any other build error.
27
- * A `ref` that does not exist always fails.
28
- */
29
- onError?: 'fail' | 'warn' | 'silent';
30
- /** Seconds a cached fetch stays fresh. Defaults to an hour. */
31
- cacheTtl?: number;
32
- /** Ignore the cache and refetch. */
33
- refresh?: boolean;
34
- /** Where to cache. Defaults to `node_modules/.cache/astroc4`. */
35
- cacheDir?: string;
36
- /** Raw host to read from, for mirrors. Defaults to raw.githubusercontent.com. */
37
- baseUrl?: string;
38
- /** Request timeout in milliseconds. Defaults to 15000. */
39
- timeout?: number;
40
- }
41
-
42
- export interface StarlightLikeC4Options {
43
- /**
44
- * Passed through to `LikeC4VitePlugin` — e.g. `{ workspace: 'diagrams' }` to
45
- * look for .c4 files somewhere other than the site root.
46
- */
47
- vitePlugin?: Parameters<typeof LikeC4VitePlugin>[0];
48
- /** Render ```likec4 fences in .mdx pages as diagrams. Defaults to `true`. */
49
- fences?: boolean;
50
- /**
51
- * Highlight ```c4 / ```likec4 source. Defaults to `true`; `false` leaves
52
- * Expressive Code entirely to the site.
53
- */
54
- highlighting?: boolean | LikeC4HighlightingOptions;
55
- }
56
-
57
- export default function starlightLikeC4(options?: StarlightLikeC4Options): StarlightPlugin;
58
-
59
- /** Adds the LikeC4 language to a site's Expressive Code options, preserving the rest. */
60
- export declare function withLikeC4Language(
61
- expressiveCode: unknown,
62
- registration: Record<string, unknown>,
63
- ): Record<string, unknown> & { shiki: { langs: unknown[] } };
64
-