@likec4/language-server 1.8.0 → 1.9.0

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.
Files changed (83) hide show
  1. package/contrib/likec4.tmLanguage.json +1 -1
  2. package/dist/browser.cjs +21 -0
  3. package/dist/browser.d.cts +22 -0
  4. package/dist/browser.d.mts +22 -0
  5. package/dist/browser.d.ts +22 -0
  6. package/dist/browser.mjs +19 -0
  7. package/dist/index.cjs +10 -0
  8. package/dist/index.d.cts +18 -0
  9. package/dist/index.d.mts +18 -0
  10. package/dist/index.d.ts +18 -0
  11. package/dist/index.mjs +1 -0
  12. package/dist/likec4lib.cjs +961 -0
  13. package/dist/likec4lib.d.cts +6 -0
  14. package/dist/likec4lib.d.mts +6 -0
  15. package/dist/likec4lib.d.ts +6 -0
  16. package/dist/likec4lib.mjs +957 -0
  17. package/dist/model-graph/index.cjs +10 -0
  18. package/dist/model-graph/index.d.cts +79 -0
  19. package/dist/model-graph/index.d.mts +79 -0
  20. package/dist/model-graph/index.d.ts +79 -0
  21. package/dist/model-graph/index.mjs +1 -0
  22. package/dist/node.cjs +18 -0
  23. package/dist/node.d.cts +20 -0
  24. package/dist/node.d.mts +20 -0
  25. package/dist/node.d.ts +20 -0
  26. package/dist/node.mjs +16 -0
  27. package/dist/protocol.cjs +25 -0
  28. package/dist/protocol.d.cts +43 -0
  29. package/dist/protocol.d.mts +43 -0
  30. package/dist/protocol.d.ts +43 -0
  31. package/dist/protocol.mjs +17 -0
  32. package/dist/shared/language-server.86lmJ8ZN.d.cts +1194 -0
  33. package/dist/shared/language-server.B1TZgyoH.cjs +5371 -0
  34. package/dist/shared/language-server.CCB4ESN5.mjs +1606 -0
  35. package/dist/shared/language-server.CFTY6j4e.d.mts +1194 -0
  36. package/dist/shared/language-server.D0bOlrCi.cjs +1619 -0
  37. package/dist/shared/language-server.Q-wtPShM.mjs +5360 -0
  38. package/dist/shared/language-server.RjhrBZS0.d.ts +1194 -0
  39. package/package.json +35 -20
  40. package/src/ast.ts +44 -32
  41. package/src/browser.ts +0 -3
  42. package/src/elementRef.ts +1 -1
  43. package/src/generated/ast.ts +105 -86
  44. package/src/generated/grammar.ts +1 -1
  45. package/src/generated-lib/icons.ts +1 -1
  46. package/src/like-c4.langium +30 -18
  47. package/src/likec4lib.ts +2 -3
  48. package/src/logger.ts +9 -1
  49. package/src/lsp/RenameProvider.ts +8 -0
  50. package/src/lsp/SemanticTokenProvider.ts +19 -1
  51. package/src/lsp/index.ts +1 -0
  52. package/src/model/fqn-computation.ts +33 -23
  53. package/src/model/fqn-index.ts +5 -20
  54. package/src/model/model-builder.ts +147 -90
  55. package/src/model/model-locator.ts +1 -1
  56. package/src/model/model-parser-where.ts +3 -2
  57. package/src/model/model-parser.ts +57 -19
  58. package/src/model-graph/LikeC4ModelGraph.ts +42 -21
  59. package/src/model-graph/compute-view/__test__/fixture.ts +16 -14
  60. package/src/model-graph/compute-view/compute.ts +9 -6
  61. package/src/model-graph/compute-view/predicates.ts +3 -3
  62. package/src/model-graph/dynamic-view/__test__/fixture.ts +1 -0
  63. package/src/model-graph/dynamic-view/compute.ts +2 -1
  64. package/src/model-graph/utils/elementExpressionToPredicate.ts +1 -1
  65. package/src/model-graph/utils/sortNodes.ts +2 -6
  66. package/src/module.ts +23 -3
  67. package/src/protocol.ts +4 -5
  68. package/src/references/scope-computation.ts +10 -1
  69. package/src/references/scope-provider.ts +22 -9
  70. package/src/shared/NodeKindProvider.ts +73 -34
  71. package/src/test/setup.ts +3 -8
  72. package/src/utils/graphlib.ts +11 -0
  73. package/src/validation/_shared.ts +24 -0
  74. package/src/validation/element.ts +9 -9
  75. package/src/validation/index.ts +2 -1
  76. package/src/validation/relation.ts +45 -39
  77. package/src/validation/specification.ts +15 -2
  78. package/src/validation/view.ts +7 -0
  79. package/src/view-utils/manual-layout.ts +1 -1
  80. package/src/view-utils/resolve-extended-views.ts +19 -10
  81. package/src/view-utils/resolve-relative-paths.ts +5 -7
  82. package/src/view-utils/view-hash.ts +1 -1
  83. package/src/reset.d.ts +0 -2
@@ -1,12 +1,15 @@
1
1
  import {
2
- type c4,
3
2
  compareByFqnHierarchically,
3
+ compareRelations,
4
+ computeColorValues,
5
+ type CustomColorDefinitions,
4
6
  isElementView,
5
7
  isScopedElementView,
6
8
  parentFqn,
7
9
  type ScopedElementView,
8
10
  type ViewID
9
11
  } from '@likec4/core'
12
+ import type * as c4 from '@likec4/core'
10
13
  import { deepEqual as eq } from 'fast-equals'
11
14
  import type { Cancellation, LangiumDocument, LangiumDocuments, URI, WorkspaceCache } from 'langium'
12
15
  import { Disposable, DocumentState, interruptAndCheck } from 'langium'
@@ -15,14 +18,18 @@ import {
15
18
  find,
16
19
  flatMap,
17
20
  forEach,
21
+ indexBy,
22
+ isNonNullish,
18
23
  isNullish,
19
24
  isNumber,
20
25
  isTruthy,
21
26
  map,
22
27
  mapToObj,
28
+ mapValues,
23
29
  pipe,
24
30
  prop,
25
31
  reduce,
32
+ reverse,
26
33
  sort,
27
34
  values
28
35
  } from 'remeda'
@@ -40,26 +47,40 @@ import type { LikeC4Services } from '../module'
40
47
  import { printDocs } from '../utils/printDocs'
41
48
  import { assignNavigateTo, resolveRelativePaths, resolveRulesExtendedViews } from '../view-utils'
42
49
 
43
- function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[]): c4.LikeC4Model {
50
+ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[]): c4.ParsedLikeC4Model {
44
51
  const c4Specification: ParsedAstSpecification = {
45
- kinds: {},
46
- relationships: {}
52
+ tags: new Set(),
53
+ elements: {},
54
+ relationships: {},
55
+ colors: {}
47
56
  }
48
57
  forEach(map(docs, prop('c4Specification')), spec => {
49
- Object.assign(c4Specification.kinds, spec.kinds)
58
+ spec.tags.forEach(t => c4Specification.tags.add(t))
59
+ Object.assign(c4Specification.elements, spec.elements)
50
60
  Object.assign(c4Specification.relationships, spec.relationships)
61
+ Object.assign(c4Specification.colors, spec.colors)
51
62
  })
52
63
  const resolveLinks = (doc: LangiumDocument, links: c4.NonEmptyArray<c4.Link>) => {
53
- return links.map(l => ({
54
- url: services.lsp.DocumentLinkProvider.resolveLink(doc, l.url),
55
- ...(l.title && { title: l.title })
56
- })) as c4.NonEmptyArray<c4.Link>
64
+ try {
65
+ return links.map(l => ({
66
+ ...l,
67
+ url: services.lsp.DocumentLinkProvider.resolveLink(doc, l.url)
68
+ })) as c4.NonEmptyArray<c4.Link>
69
+ } catch (e) {
70
+ logWarnError(e)
71
+ return null
72
+ }
57
73
  }
58
74
 
75
+ const customColorDefinitions: CustomColorDefinitions = mapValues(
76
+ c4Specification.colors,
77
+ c => computeColorValues(c.color)
78
+ )
79
+
59
80
  const toModelElement = (doc: LangiumDocument) => {
60
81
  return ({
61
82
  tags,
62
- links,
83
+ links: unresolvedLinks,
63
84
  style: {
64
85
  color,
65
86
  shape,
@@ -75,11 +96,12 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
75
96
  metadata
76
97
  }: ParsedAstElement): c4.Element | null => {
77
98
  try {
78
- const __kind = c4Specification.kinds[kind]
99
+ const __kind = c4Specification.elements[kind]
79
100
  if (!__kind) {
80
101
  logger.warn(`No kind '${kind}' found for ${id}`)
81
102
  return null
82
103
  }
104
+ const links = unresolvedLinks ? resolveLinks(doc, unresolvedLinks) : null
83
105
  color ??= __kind.style.color
84
106
  shape ??= __kind.style.shape
85
107
  icon ??= __kind.style.icon
@@ -96,7 +118,7 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
96
118
  ...(border && { border }),
97
119
  ...(isNumber(opacity) && { opacity })
98
120
  },
99
- links: links ? resolveLinks(doc, links) : null,
121
+ links,
100
122
  tags: tags ?? null,
101
123
  technology: technology ?? null,
102
124
  description: description ?? null,
@@ -115,6 +137,7 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
115
137
  docs,
116
138
  flatMap(d => map(d.c4Elements, toModelElement(d))),
117
139
  filter(isTruthy),
140
+ // sort from root elements to nested, so that parent is always present
118
141
  sort(compareByFqnHierarchically),
119
142
  reduce(
120
143
  (acc, el) => {
@@ -123,15 +146,10 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
123
146
  logWarnError(`No parent found for ${el.id}`)
124
147
  return acc
125
148
  }
126
- if (el.id in acc) {
127
- // should not happen, as validated
128
- logWarnError(`Duplicate element id: ${el.id}`)
129
- return acc
130
- }
131
149
  acc[el.id] = el
132
150
  return acc
133
151
  },
134
- {} as c4.LikeC4Model['elements']
152
+ {} as c4.ParsedLikeC4Model['elements']
135
153
  )
136
154
  )
137
155
 
@@ -141,7 +159,7 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
141
159
  source,
142
160
  target,
143
161
  kind,
144
- links,
162
+ links: unresolvedLinks,
145
163
  id,
146
164
  ...model
147
165
  }: ParsedAstRelation): c4.Relation | null => {
@@ -151,12 +169,13 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
151
169
  )
152
170
  return null
153
171
  }
172
+ const links = unresolvedLinks ? resolveLinks(doc, unresolvedLinks) : null
154
173
 
155
- if (!isNullish(kind) && kind in c4Specification.relationships) {
174
+ if (isNonNullish(kind) && kind in c4Specification.relationships) {
156
175
  return {
157
- ...(links && { links: resolveLinks(doc, links) }),
158
176
  ...c4Specification.relationships[kind],
159
177
  ...model,
178
+ ...(links && { links }),
160
179
  source,
161
180
  target,
162
181
  kind,
@@ -164,7 +183,7 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
164
183
  }
165
184
  }
166
185
  return {
167
- ...(links && { links: resolveLinks(doc, links) }),
186
+ ...(links && { links }),
168
187
  ...model,
169
188
  source,
170
189
  target,
@@ -177,7 +196,9 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
177
196
  docs,
178
197
  flatMap(d => map(d.c4Relations, toModelRelation(d))),
179
198
  filter(isTruthy),
180
- mapToObj(r => [r.id, r])
199
+ sort(compareRelations),
200
+ reverse(),
201
+ indexBy(prop('id'))
181
202
  )
182
203
 
183
204
  const toC4View = (doc: LangiumDocument) => {
@@ -188,7 +209,7 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
188
209
  title,
189
210
  description,
190
211
  tags,
191
- links,
212
+ links: unresolvedLinks,
192
213
 
193
214
  // ignore this property
194
215
  astPath: _ignore,
@@ -205,34 +226,32 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
205
226
  title = 'Landscape view'
206
227
  }
207
228
 
229
+ const links = unresolvedLinks ? resolveLinks(doc, unresolvedLinks) : null
230
+
208
231
  return {
209
- id,
210
- title,
211
- description,
232
+ ...model,
212
233
  tags,
213
- links: links ? resolveLinks(doc, links) : null,
234
+ links,
214
235
  docUri,
215
- ...model
236
+ description,
237
+ title,
238
+ id,
239
+ customColorDefinitions
216
240
  }
217
241
  }
218
242
  }
219
243
 
220
- const views = pipe(
221
- docs,
222
- flatMap(d => map(d.c4Views, toC4View(d))),
223
- resolveRelativePaths,
224
- mapToObj(v => [v.id, v]),
225
- resolveRulesExtendedViews
226
- )
227
- // add index view if not present
228
- if (!('index' in views)) {
229
- views['index' as ViewID] = {
244
+ const parsedViews = docs.flatMap(d => map(d.c4Views, toC4View(d)))
245
+ // Add index view if not present
246
+ if (!parsedViews.some(v => v.id === 'index')) {
247
+ parsedViews.unshift({
230
248
  __: 'element',
231
249
  id: 'index' as ViewID,
232
- title: 'Landscape',
250
+ title: 'Landscape view',
233
251
  description: null,
234
252
  tags: null,
235
253
  links: null,
254
+ customColorDefinitions: customColorDefinitions,
236
255
  rules: [
237
256
  {
238
257
  include: [
@@ -242,17 +261,30 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
242
261
  ]
243
262
  }
244
263
  ]
245
- }
264
+ })
246
265
  }
247
266
 
267
+ const views = pipe(
268
+ parsedViews,
269
+ resolveRelativePaths,
270
+ indexBy(prop('id')),
271
+ resolveRulesExtendedViews
272
+ )
273
+
248
274
  return {
275
+ specification: {
276
+ tags: Array.from(c4Specification.tags),
277
+ elements: c4Specification.elements,
278
+ relationships: c4Specification.relationships
279
+ },
249
280
  elements,
250
281
  relations,
251
282
  views
252
283
  }
253
284
  }
254
- const RAW_MODEL_CACHE = 'LikeC4RawModel'
255
- const MODEL_CACHE = 'LikeC4Model'
285
+
286
+ const CACHE_KEY_PARSED_MODEL = 'ParsedLikeC4Model'
287
+ const CACHE_KEY_COMPUTED_MODEL = 'ComputedLikeC4Model'
256
288
 
257
289
  type ModelParsedListener = (docs: URI[]) => void
258
290
 
@@ -274,9 +306,13 @@ export class LikeC4ModelBuilder {
274
306
  DocumentState.Validated,
275
307
  async (docs, _cancelToken) => {
276
308
  let parsed = [] as URI[]
277
- logger.debug(`[ModelBuilder] onValidated (${docs.length} docs)\n${printDocs(docs)}`)
278
- for (const doc of parser.parse(docs)) {
279
- parsed.push(doc.uri)
309
+ try {
310
+ logger.debug(`[ModelBuilder] onValidated (${docs.length} docs)\n${printDocs(docs)}`)
311
+ for (const doc of parser.parse(docs)) {
312
+ parsed.push(doc.uri)
313
+ }
314
+ } catch (e) {
315
+ logWarnError(e)
280
316
  }
281
317
  if (parsed.length > 0) {
282
318
  this.notifyListeners(parsed)
@@ -287,35 +323,82 @@ export class LikeC4ModelBuilder {
287
323
  logger.debug(`[ModelBuilder] Created`)
288
324
  }
289
325
 
290
- public async buildModel(cancelToken?: Cancellation.CancellationToken): Promise<c4.LikeC4Model | null> {
291
- const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.LikeC4Model | null>
292
- if (cache.has(RAW_MODEL_CACHE)) {
293
- return cache.get(RAW_MODEL_CACHE)!
326
+ /**
327
+ * WARNING:
328
+ * This method is internal and should to be called only when all documents are known to be parsed.
329
+ * Otherwise, the model may be incomplete.
330
+ */
331
+ public unsafeSyncBuildModel(): c4.ParsedLikeC4Model | null {
332
+ const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.ParsedLikeC4Model | null>
333
+ return cache.get(CACHE_KEY_PARSED_MODEL, () => {
334
+ const docs = this.documents()
335
+ if (docs.length === 0) {
336
+ logger.debug('[ModelBuilder] No documents to build model from')
337
+ return null
338
+ }
339
+ logger.debug(`[ModelBuilder] buildModel from ${docs.length} docs:\n${printDocs(docs)}`)
340
+ return buildModel(this.services, docs)
341
+ })
342
+ }
343
+
344
+ public async buildModel(cancelToken?: Cancellation.CancellationToken): Promise<c4.ParsedLikeC4Model | null> {
345
+ const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.ParsedLikeC4Model | null>
346
+ if (cache.has(CACHE_KEY_PARSED_MODEL)) {
347
+ return cache.get(CACHE_KEY_PARSED_MODEL)!
294
348
  }
295
349
  return await this.services.shared.workspace.WorkspaceLock.read(async () => {
296
350
  if (cancelToken) {
297
351
  await interruptAndCheck(cancelToken)
298
352
  }
299
- return cache.get(RAW_MODEL_CACHE, () => {
300
- const docs = this.documents()
301
- if (docs.length === 0) {
302
- logger.debug('[ModelBuilder] No documents to build model from')
303
- return null
304
- }
305
- logger.debug(`[ModelBuilder] buildModel from ${docs.length} docs:\n${printDocs(docs)}`)
306
- return buildModel(this.services, docs)
307
- })
353
+ return this.unsafeSyncBuildModel()
308
354
  })
309
355
  }
310
356
 
311
357
  private previousViews: Record<ViewID, c4.ComputedView> = {}
312
358
 
359
+ /**
360
+ * WARNING:
361
+ * This method is internal and should to be called only when all documents are known to be parsed.
362
+ * Otherwise, the model may be incomplete.
363
+ */
364
+ public unsafeSyncBuildComputedModel(model: c4.ParsedLikeC4Model): c4.ComputedLikeC4Model {
365
+ const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.ComputedLikeC4Model>
366
+ const viewsCache = this.services.WorkspaceCache as WorkspaceCache<string, c4.ComputedView | null>
367
+ return cache.get(CACHE_KEY_COMPUTED_MODEL, () => {
368
+ const index = new LikeC4ModelGraph(model)
369
+
370
+ const allViews = [] as c4.ComputedView[]
371
+ for (const view of values(model.views)) {
372
+ const result = isElementView(view) ? computeView(view, index) : computeDynamicView(view, index)
373
+ if (!result.isSuccess) {
374
+ logWarnError(result.error)
375
+ continue
376
+ }
377
+ allViews.push(result.view)
378
+ }
379
+ assignNavigateTo(allViews)
380
+ const views = mapToObj(allViews, v => {
381
+ const previous = this.previousViews[v.id]
382
+ const view = previous && eq(v, previous) ? previous : v
383
+ viewsCache.set(computedViewKey(v.id), view)
384
+ return [v.id, view] as const
385
+ })
386
+ this.previousViews = { ...views }
387
+ return {
388
+ specification: model.specification,
389
+ elements: model.elements,
390
+ relations: model.relations,
391
+ views
392
+ }
393
+ })
394
+ }
395
+
313
396
  public async buildComputedModel(
314
397
  cancelToken?: Cancellation.CancellationToken
315
- ): Promise<c4.LikeC4ComputedModel | null> {
316
- const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.LikeC4ComputedModel | null>
317
- if (cache.has(MODEL_CACHE)) {
318
- return cache.get(MODEL_CACHE)!
398
+ ): Promise<c4.ComputedLikeC4Model | null> {
399
+ const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.ComputedLikeC4Model | null>
400
+ if (cache.has(CACHE_KEY_COMPUTED_MODEL)) {
401
+ return cache.get(CACHE_KEY_COMPUTED_MODEL)!
319
402
  }
320
403
  const model = await this.buildModel(cancelToken)
321
404
  if (!model) {
@@ -325,33 +408,7 @@ export class LikeC4ModelBuilder {
325
408
  if (cancelToken) {
326
409
  await interruptAndCheck(cancelToken)
327
410
  }
328
- const viewsCache = this.services.WorkspaceCache as WorkspaceCache<string, c4.ComputedView | null>
329
- return cache.get(MODEL_CACHE, () => {
330
- const index = new LikeC4ModelGraph(model)
331
-
332
- const allViews = [] as c4.ComputedView[]
333
- for (const view of values(model.views)) {
334
- const result = isElementView(view) ? computeView(view, index) : computeDynamicView(view, index)
335
- if (!result.isSuccess) {
336
- logWarnError(result.error)
337
- continue
338
- }
339
- allViews.push(result.view)
340
- }
341
- assignNavigateTo(allViews)
342
- const views = mapToObj(allViews, v => {
343
- const previous = this.previousViews[v.id]
344
- const view = previous && eq(v, previous) ? previous : v
345
- viewsCache.set(computedViewKey(v.id), view)
346
- return [v.id, view] as const
347
- })
348
- this.previousViews = { ...views }
349
- return {
350
- elements: model.elements,
351
- relations: model.relations,
352
- views
353
- }
354
- })
411
+ return this.unsafeSyncBuildComputedModel(model)
355
412
  })
356
413
  }
357
414
 
@@ -1,4 +1,4 @@
1
- import type { likec4 as c4 } from '@likec4/core'
1
+ import type * as c4 from '@likec4/core'
2
2
  import type { LangiumDocuments } from 'langium'
3
3
  import { AstUtils, GrammarUtils } from 'langium'
4
4
  import type { Location } from 'vscode-languageserver-types'
@@ -1,5 +1,6 @@
1
- import { type c4, invariant, isNonEmptyArray, nonexhaustive } from '@likec4/core'
2
- import { isAndOperator, isOrOperator } from '@likec4/core/types'
1
+ import { invariant, isNonEmptyArray, nonexhaustive } from '@likec4/core'
2
+ import { isAndOperator, isOrOperator } from '@likec4/core'
3
+ import type * as c4 from '@likec4/core'
3
4
  import { ast } from '../ast'
4
5
 
5
6
  const parseEquals = (
@@ -1,4 +1,5 @@
1
- import { type c4, InvalidModelError, invariant, isNonEmptyArray, nonexhaustive } from '@likec4/core'
1
+ import { type HexColorLiteral, invariant, isNonEmptyArray, nonexhaustive } from '@likec4/core'
2
+ import type * as c4 from '@likec4/core'
2
3
  import type { AstNode, LangiumDocument } from 'langium'
3
4
  import { AstUtils, CstUtils } from 'langium'
4
5
  import { filter, flatMap, isDefined, isNonNullish, isTruthy, mapToObj, pipe } from 'remeda'
@@ -23,6 +24,7 @@ import {
23
24
  resolveRelationPoints,
24
25
  streamModel,
25
26
  toAutoLayout,
27
+ toColor,
26
28
  toElementStyle,
27
29
  toRelationshipStyleExcludeDefaults,
28
30
  ViewOps
@@ -67,7 +69,7 @@ export class LikeC4ModelParser {
67
69
  try {
68
70
  result.push(this.parseLikeC4Document(doc))
69
71
  } catch (cause) {
70
- logError(new InvalidModelError(`Error parsing document ${doc.uri.toString()}`, { cause }))
72
+ logError(new Error(`Error parsing document ${doc.uri.toString()}`, { cause }))
71
73
  }
72
74
  }
73
75
  return result
@@ -89,20 +91,23 @@ export class LikeC4ModelParser {
89
91
  const element_specs = specifications.flatMap(s => s.elements.filter(isValid))
90
92
  for (const { kind, props } of element_specs) {
91
93
  try {
92
- const style = props.find(ast.isElementStyleProperty)
93
94
  const kindName = kind.name as c4.ElementKind
94
- if (kindName in c4Specification.kinds) {
95
+ if (!isTruthy(kindName)) {
96
+ continue
97
+ }
98
+ if (kindName in c4Specification.elements) {
95
99
  logger.warn(`Element kind "${kindName}" is already defined`)
96
100
  continue
97
101
  }
102
+ const style = props.find(ast.isElementStyleProperty)
98
103
  const bodyProps = mapToObj(
99
104
  props.filter(ast.isSpecificationElementStringProperty).filter(p => isNonNullish(p.value)) ?? [],
100
- p => [p.key, removeIndent(p.value)]
105
+ p => [p.key, removeIndent(p.value)] as const
101
106
  )
102
- c4Specification.kinds[kindName] = {
107
+ c4Specification.elements[kindName] = {
103
108
  ...bodyProps,
104
109
  style: {
105
- ...toElementStyle(style?.props)
110
+ ...toElementStyle(style?.props, isValid)
106
111
  }
107
112
  }
108
113
  } catch (e) {
@@ -114,13 +119,16 @@ export class LikeC4ModelParser {
114
119
  for (const { kind, props } of relations_specs) {
115
120
  try {
116
121
  const kindName = kind.name as c4.RelationshipKind
122
+ if (!isTruthy(kindName)) {
123
+ continue
124
+ }
117
125
  if (kindName in c4Specification.relationships) {
118
126
  logger.warn(`Relationship kind "${kindName}" is already defined`)
119
127
  continue
120
128
  }
121
129
  const bodyProps = mapToObj(
122
130
  props.filter(ast.isSpecificationRelationshipStringProperty).filter(p => isNonNullish(p.value)) ?? [],
123
- p => [p.key, p.value]
131
+ p => [p.key, removeIndent(p.value)]
124
132
  )
125
133
  c4Specification.relationships[kindName] = {
126
134
  ...bodyProps,
@@ -130,13 +138,38 @@ export class LikeC4ModelParser {
130
138
  logWarnError(e)
131
139
  }
132
140
  }
141
+
142
+ const tags_specs = specifications.flatMap(s => s.tags.filter(isValid))
143
+ for (const tagSpec of tags_specs) {
144
+ const tag = tagSpec.tag.name as c4.Tag
145
+ if (isTruthy(tag)) {
146
+ c4Specification.tags.add(tag)
147
+ }
148
+ }
149
+
150
+ const colors_specs = specifications.flatMap(s => s.colors.filter(isValid))
151
+ for (const { name, color } of colors_specs) {
152
+ try {
153
+ const colorName = name.name as c4.CustomColor
154
+ if (colorName in c4Specification.colors) {
155
+ logger.warn(`Custom color "${colorName}" is already defined`)
156
+ continue
157
+ }
158
+
159
+ c4Specification.colors[colorName] = {
160
+ color: color as HexColorLiteral
161
+ }
162
+ } catch (e) {
163
+ logWarnError(e)
164
+ }
165
+ }
133
166
  }
134
167
 
135
168
  private parseModel(doc: ParsedLikeC4LangiumDocument, isValid: IsValidFn) {
136
169
  for (const el of streamModel(doc, isValid)) {
137
170
  if (ast.isElement(el)) {
138
171
  try {
139
- doc.c4Elements.push(this.parseElement(el))
172
+ doc.c4Elements.push(this.parseElement(el, isValid))
140
173
  } catch (e) {
141
174
  logWarnError(e)
142
175
  }
@@ -154,12 +187,12 @@ export class LikeC4ModelParser {
154
187
  }
155
188
  }
156
189
 
157
- private parseElement(astNode: ast.Element): ParsedAstElement {
190
+ private parseElement(astNode: ast.Element, isValid: IsValidFn): ParsedAstElement {
158
191
  const id = this.resolveFqn(astNode)
159
192
  const kind = astNode.kind.$refText as c4.ElementKind
160
193
  const tags = this.convertTags(astNode.body)
161
194
  const stylePropsAst = astNode.body?.props.find(ast.isElementStyleProperty)?.props
162
- const style = toElementStyle(stylePropsAst)
195
+ const style = toElementStyle(stylePropsAst, isValid)
163
196
  const metadata = this.getMetadata(astNode.body?.props.find(ast.isMetadataProperty))
164
197
  const astPath = this.getAstNodePath(astNode)
165
198
 
@@ -392,8 +425,9 @@ export class LikeC4ModelParser {
392
425
  return acc
393
426
  }
394
427
  if (ast.isColorProperty(prop)) {
395
- if (isDefined(prop.value)) {
396
- acc.custom[prop.key] = prop.value
428
+ const value = toColor(prop)
429
+ if (isDefined(value)) {
430
+ acc.custom[prop.key] = value
397
431
  }
398
432
  return acc
399
433
  }
@@ -493,8 +527,9 @@ export class LikeC4ModelParser {
493
527
  return acc
494
528
  }
495
529
  if (ast.isColorProperty(prop)) {
496
- if (isTruthy(prop.value)) {
497
- acc.customRelation[prop.key] = prop.value
530
+ const value = toColor(prop)
531
+ if (isTruthy(value)) {
532
+ acc.customRelation[prop.key] = value
498
533
  }
499
534
  return acc
500
535
  }
@@ -551,7 +586,7 @@ export class LikeC4ModelParser {
551
586
  return this.parseViewRulePredicate(astRule, isValid)
552
587
  }
553
588
  if (ast.isViewRuleStyle(astRule)) {
554
- const styleProps = toElementStyle(astRule.props.filter(ast.isStyleProperty))
589
+ const styleProps = toElementStyle(astRule.props.filter(ast.isStyleProperty), isValid)
555
590
  const notation = removeIndent(astRule.props.find(ast.isNotationProperty)?.value)
556
591
  const targets = this.parseElementExpressionsIterator(astRule.target)
557
592
  return {
@@ -632,7 +667,10 @@ export class LikeC4ModelParser {
632
667
  continue
633
668
  }
634
669
  if (ast.isColorProperty(prop)) {
635
- step[prop.key] = prop.value
670
+ const value = toColor(prop)
671
+ if (isTruthy(value)) {
672
+ step[prop.key] = value
673
+ }
636
674
  continue
637
675
  }
638
676
  if (ast.isLineProperty(prop)) {
@@ -778,7 +816,7 @@ export class LikeC4ModelParser {
778
816
  return acc
779
817
  }
780
818
  if (ast.isViewRuleStyle(n)) {
781
- const styleProps = toElementStyle(n.props.filter(ast.isStyleProperty))
819
+ const styleProps = toElementStyle(n.props.filter(ast.isStyleProperty), isValid)
782
820
  const notation = removeIndent(n.props.find(ast.isNotationProperty)?.value)
783
821
  const targets = this.parseElementExpressionsIterator(n.target)
784
822
  if (targets.length > 0) {
@@ -845,7 +883,7 @@ export class LikeC4ModelParser {
845
883
  const tags = [] as c4.Tag[]
846
884
  while (iter) {
847
885
  try {
848
- const values = iter.values.map(t => t.ref?.name).filter(Boolean) as c4.Tag[]
886
+ const values = iter.values.map(t => t.ref?.name).filter(isTruthy) as c4.Tag[]
849
887
  if (values.length > 0) {
850
888
  tags.unshift(...values)
851
889
  }