@likec4/language-server 1.7.4 → 1.8.1

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.
@@ -42,25 +42,37 @@ SpecificationRule:
42
42
 
43
43
  SpecificationElementKind:
44
44
  'element' kind=ElementKind ('{'
45
- style=StyleProperties?
45
+ props+=(
46
+ SpecificationElementStringProperty |
47
+ ElementStyleProperty
48
+ )*
46
49
  '}')?;
47
50
 
51
+ SpecificationElementStringProperty:
52
+ key=('technology' | 'notation') ':'? value=String ';'?;
53
+
48
54
  SpecificationTag:
49
55
  'tag' tag=Tag;
50
56
 
51
57
  SpecificationRelationshipKind:
52
58
  'relationship' kind=RelationshipKind ('{'
53
- props+=RelationshipStyleProperty*
59
+ props+=(
60
+ RelationshipStyleProperty |
61
+ SpecificationRelationshipStringProperty
62
+ )*
54
63
  '}')?
55
64
  ;
56
65
 
66
+ SpecificationRelationshipStringProperty:
67
+ key=('technology' | 'notation') ':'? value=String ';'?;
68
+
57
69
  // Model -------------------------------------
58
70
 
59
71
  Model:
60
72
  name='model' '{'
61
73
  elements+=(
62
74
  ExtendElement |
63
- ExplicitRelation |
75
+ Relation<true> |
64
76
  Element
65
77
  )*
66
78
  '}'
@@ -85,14 +97,14 @@ ElementBody: '{'
85
97
  tags=Tags?
86
98
  props+=ElementProperty*
87
99
  elements+=(
88
- Relation |
100
+ Relation<false> |
89
101
  Element
90
102
  )*
91
103
  '}'
92
104
  ;
93
105
 
94
106
  ElementProperty:
95
- ElementStringProperty | StyleProperties | LinkProperty | IconProperty;
107
+ ElementStringProperty | ElementStyleProperty | LinkProperty | IconProperty | MetadataProperty;
96
108
 
97
109
  ElementStringProperty:
98
110
  key=('title' | 'technology' | 'description') ':'? value=String ';'?;
@@ -103,7 +115,7 @@ ExtendElement:
103
115
 
104
116
  ExtendElementBody: '{'
105
117
  elements+=(
106
- ExplicitRelation |
118
+ Relation<true> |
107
119
  Element
108
120
  )*
109
121
  '}'
@@ -120,19 +132,18 @@ Tags:
120
132
  (values+=[Tag:TagId])+ ({infer Tags.prev=current} ',' (values+=[Tag:TagId])*)* ';'?
121
133
  ;
122
134
 
123
- Relation:
124
- ExplicitRelation | ImplicitRelation;
125
-
126
- ExplicitRelation:
127
- source=ElementRef RelationFragment;
128
-
129
- ImplicitRelation:
130
- ('this' | 'it')? RelationFragment;
131
-
132
- fragment RelationFragment:
133
- ('->' | '-[' kind=[RelationshipKind] ']->' | kind=[RelationshipKind:DotId] )
135
+ Relation<isExplicit>:
136
+ (<isExplicit> source=ElementRef | <!isExplicit> source=ElementRef?)
137
+ (
138
+ kind=[RelationshipKind:DotId] |
139
+ '-[' kind=[RelationshipKind] ']->' |
140
+ '->'
141
+ )
134
142
  target=ElementRef
135
- title=String?
143
+ (
144
+ title=String
145
+ technology=String?
146
+ )?
136
147
  tags=Tags?
137
148
  body=RelationBody?
138
149
  ;
@@ -144,7 +155,7 @@ RelationBody: '{'
144
155
  ;
145
156
 
146
157
  RelationProperty:
147
- RelationStringProperty | RelationStyleProperty | LinkProperty;
158
+ RelationStringProperty | RelationStyleProperty | LinkProperty | MetadataProperty;
148
159
 
149
160
  RelationStringProperty:
150
161
  key=('title' | 'technology' | 'description') ':'? value=String ';'?;
@@ -155,6 +166,18 @@ RelationStyleProperty:
155
166
  '}'
156
167
  ;
157
168
 
169
+ MetadataProperty:
170
+ 'metadata' MetadataBody
171
+ ;
172
+
173
+ MetadataBody: '{'
174
+ props+=(MetadataAttribute)*
175
+ '}'
176
+ ;
177
+
178
+ MetadataAttribute:
179
+ key=IdTerminal value=String
180
+ ;
158
181
 
159
182
  // Views -------------------------------------
160
183
 
@@ -200,7 +223,15 @@ DynamicViewBody: '{'
200
223
  ;
201
224
 
202
225
 
203
- type StringProperty = ElementStringProperty | ViewStringProperty | RelationStringProperty;
226
+ type StringProperty =
227
+ ElementStringProperty |
228
+ ViewStringProperty |
229
+ RelationStringProperty |
230
+ MetadataAttribute |
231
+ SpecificationElementStringProperty |
232
+ SpecificationRelationshipStringProperty |
233
+ NotationProperty
234
+ ;
204
235
 
205
236
  ViewProperty:
206
237
  ViewStringProperty | LinkProperty
@@ -376,16 +407,24 @@ DynamicViewPredicateIterator:
376
407
 
377
408
  ViewRuleStyle:
378
409
  'style' target=ElementExpressionsIterator '{'
379
- props+=StyleProperty*
410
+ props+=(
411
+ StyleProperty |
412
+ NotationProperty
413
+ )*
380
414
  '}';
381
415
 
382
416
  ViewRuleAutoLayout:
383
417
  'autoLayout' direction=ViewLayoutDirection;
384
418
 
419
+ NotationProperty:
420
+ key='notation' ':'? value=String ';'?
421
+ ;
422
+
385
423
  CustomElementProperties: '{'
386
424
  props+=(
387
425
  NavigateToProperty |
388
426
  ElementStringProperty |
427
+ NotationProperty |
389
428
  StyleProperty
390
429
  )*
391
430
  '}'
@@ -394,6 +433,7 @@ CustomElementProperties: '{'
394
433
  CustomRelationProperties: '{'
395
434
  props+=(
396
435
  RelationStringProperty |
436
+ NotationProperty |
397
437
  RelationshipStyleProperty
398
438
  )*
399
439
  '}'
@@ -405,7 +445,7 @@ NavigateToProperty:
405
445
  // Common properties -------------------------------------
406
446
 
407
447
  LinkProperty:
408
- key='link' ':'? value=Uri ';'?;
448
+ key='link' ':'? value=Uri title=String? ';'?;
409
449
  ColorProperty:
410
450
  key='color' ':'? value=ThemeColor ';'?;
411
451
 
@@ -433,7 +473,7 @@ StyleProperty:
433
473
  OpacityProperty |
434
474
  IconProperty;
435
475
 
436
- StyleProperties:
476
+ ElementStyleProperty:
437
477
  key='style' '{'
438
478
  props+=StyleProperty*
439
479
  '}';
@@ -146,7 +146,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
146
146
  }
147
147
  if (
148
148
  ast.isRelationStyleProperty(node)
149
- || (ast.isStyleProperties(node) && ast.isElementBody(node.$container))
149
+ || (ast.isElementStyleProperty(node) && ast.isElementBody(node.$container))
150
150
  ) {
151
151
  acceptor({
152
152
  node,
@@ -177,7 +177,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
177
177
  property: 'key',
178
178
  type: SemanticTokenTypes.property
179
179
  })
180
- if ('value' in node) {
180
+ if ('value' in node && node.value) {
181
181
  acceptor({
182
182
  node,
183
183
  property: 'value',
@@ -40,7 +40,7 @@ import type { LikeC4Services } from '../module'
40
40
  import { printDocs } from '../utils/printDocs'
41
41
  import { assignNavigateTo, resolveRelativePaths, resolveRulesExtendedViews } from '../view-utils'
42
42
 
43
- function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[]) {
43
+ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[]): c4.LikeC4Model {
44
44
  const c4Specification: ParsedAstSpecification = {
45
45
  kinds: {},
46
46
  relationships: {}
@@ -49,8 +49,11 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
49
49
  Object.assign(c4Specification.kinds, spec.kinds)
50
50
  Object.assign(c4Specification.relationships, spec.relationships)
51
51
  })
52
- const resolveLinks = (doc: LangiumDocument, links: c4.NonEmptyArray<string>) => {
53
- return links.map(l => services.lsp.DocumentLinkProvider.resolveLink(doc, l)) as c4.NonEmptyArray<string>
52
+ 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>
54
57
  }
55
58
 
56
59
  const toModelElement = (doc: LangiumDocument) => {
@@ -68,7 +71,8 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
68
71
  kind,
69
72
  title,
70
73
  description,
71
- technology
74
+ technology,
75
+ metadata
72
76
  }: ParsedAstElement): c4.Element | null => {
73
77
  try {
74
78
  const __kind = c4Specification.kinds[kind]
@@ -76,15 +80,18 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
76
80
  logger.warn(`No kind '${kind}' found for ${id}`)
77
81
  return null
78
82
  }
79
- color ??= __kind.color
80
- shape ??= __kind.shape
81
- icon ??= __kind.icon
82
- opacity ??= __kind.opacity
83
- border ??= __kind.border
83
+ color ??= __kind.style.color
84
+ shape ??= __kind.style.shape
85
+ icon ??= __kind.style.icon
86
+ opacity ??= __kind.style.opacity
87
+ border ??= __kind.style.border
88
+ technology ??= __kind.technology
84
89
  return {
85
90
  ...(color && { color }),
86
91
  ...(shape && { shape }),
87
92
  ...(icon && { icon }),
93
+ ...(metadata && { metadata }),
94
+ ...(__kind.notation && { notation: __kind.notation }),
88
95
  style: {
89
96
  ...(border && { border }),
90
97
  ...(isNumber(opacity) && { opacity })
@@ -281,11 +288,14 @@ export class LikeC4ModelBuilder {
281
288
  }
282
289
 
283
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)!
294
+ }
284
295
  return await this.services.shared.workspace.WorkspaceLock.read(async () => {
285
296
  if (cancelToken) {
286
297
  await interruptAndCheck(cancelToken)
287
298
  }
288
- const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.LikeC4Model | null>
289
299
  return cache.get(RAW_MODEL_CACHE, () => {
290
300
  const docs = this.documents()
291
301
  if (docs.length === 0) {
@@ -303,6 +313,10 @@ export class LikeC4ModelBuilder {
303
313
  public async buildComputedModel(
304
314
  cancelToken?: Cancellation.CancellationToken
305
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)!
319
+ }
306
320
  const model = await this.buildModel(cancelToken)
307
321
  if (!model) {
308
322
  return null
@@ -311,7 +325,6 @@ export class LikeC4ModelBuilder {
311
325
  if (cancelToken) {
312
326
  await interruptAndCheck(cancelToken)
313
327
  }
314
- const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.LikeC4ComputedModel | null>
315
328
  const viewsCache = this.services.WorkspaceCache as WorkspaceCache<string, c4.ComputedView | null>
316
329
  return cache.get(MODEL_CACHE, () => {
317
330
  const index = new LikeC4ModelGraph(model)
@@ -346,6 +359,11 @@ export class LikeC4ModelBuilder {
346
359
  viewId: ViewID,
347
360
  cancelToken?: Cancellation.CancellationToken
348
361
  ): Promise<c4.ComputedView | null> {
362
+ const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.ComputedView | null>
363
+ const cacheKey = computedViewKey(viewId)
364
+ if (cache.has(cacheKey)) {
365
+ return cache.get(cacheKey)!
366
+ }
349
367
  const model = await this.buildModel(cancelToken)
350
368
  const view = model?.views[viewId]
351
369
  if (!view) {
@@ -356,8 +374,7 @@ export class LikeC4ModelBuilder {
356
374
  if (cancelToken) {
357
375
  await interruptAndCheck(cancelToken)
358
376
  }
359
- const cache = this.services.WorkspaceCache as WorkspaceCache<string, c4.ComputedView | null>
360
- return cache.get(computedViewKey(viewId), () => {
377
+ return cache.get(cacheKey, () => {
361
378
  const index = new LikeC4ModelGraph(model)
362
379
  const result = isElementView(view) ? computeView(view, index) : computeDynamicView(view, index)
363
380
  if (!result.isSuccess) {
@@ -1,7 +1,7 @@
1
1
  import { type c4, InvalidModelError, invariant, isNonEmptyArray, nonexhaustive } from '@likec4/core'
2
2
  import type { AstNode, LangiumDocument } from 'langium'
3
3
  import { AstUtils, CstUtils } from 'langium'
4
- import { isDefined, isTruthy, mapToObj } from 'remeda'
4
+ import { filter, flatMap, isDefined, isNonNullish, isTruthy, mapToObj, pipe } from 'remeda'
5
5
  import stripIndent from 'strip-indent'
6
6
  import type { Writable } from 'type-fest'
7
7
  import type {
@@ -11,7 +11,8 @@ import type {
11
11
  ParsedAstElement,
12
12
  ParsedAstElementView,
13
13
  ParsedAstRelation,
14
- ParsedLikeC4LangiumDocument
14
+ ParsedLikeC4LangiumDocument,
15
+ ParsedLink
15
16
  } from '../ast'
16
17
  import {
17
18
  ast,
@@ -38,12 +39,12 @@ const { getDocument } = AstUtils
38
39
 
39
40
  export type ModelParsedListener = () => void
40
41
 
41
- function toSingleLine<T extends string | undefined>(str: T): T {
42
- return (isTruthy(str) ? removeIndent(str).split('\n').join(' ') : undefined) as T
42
+ function toSingleLine<T extends string | undefined | null>(str: T): T {
43
+ return (isNonNullish(str) ? removeIndent(str).split('\n').join(' ') : undefined) as T
43
44
  }
44
45
 
45
- function removeIndent<T extends string | undefined>(str: T): T {
46
- return (isTruthy(str) ? stripIndent(str).trim() : undefined) as T
46
+ function removeIndent<T extends string | undefined | null>(str: T): T {
47
+ return (isNonNullish(str) ? stripIndent(str).trim() : undefined) as T
47
48
  }
48
49
 
49
50
  export type IsValidFn = ChecksFromDiagnostics['isValid']
@@ -86,12 +87,23 @@ export class LikeC4ModelParser {
86
87
 
87
88
  const specifications = parseResult.value.specifications.filter(isValid)
88
89
  const element_specs = specifications.flatMap(s => s.elements.filter(isValid))
89
- for (const { kind, style } of element_specs) {
90
+ for (const { kind, props } of element_specs) {
90
91
  try {
92
+ const style = props.find(ast.isElementStyleProperty)
91
93
  const kindName = kind.name as c4.ElementKind
94
+ if (kindName in c4Specification.kinds) {
95
+ logger.warn(`Element kind "${kindName}" is already defined`)
96
+ continue
97
+ }
98
+ const bodyProps = mapToObj(
99
+ props.filter(ast.isSpecificationElementStringProperty).filter(p => isNonNullish(p.value)) ?? [],
100
+ p => [p.key, removeIndent(p.value)]
101
+ )
92
102
  c4Specification.kinds[kindName] = {
93
- ...c4Specification.kinds[kindName],
94
- ...toElementStyle(style?.props)
103
+ ...bodyProps,
104
+ style: {
105
+ ...toElementStyle(style?.props)
106
+ }
95
107
  }
96
108
  } catch (e) {
97
109
  logWarnError(e)
@@ -102,8 +114,16 @@ export class LikeC4ModelParser {
102
114
  for (const { kind, props } of relations_specs) {
103
115
  try {
104
116
  const kindName = kind.name as c4.RelationshipKind
117
+ if (kindName in c4Specification.relationships) {
118
+ logger.warn(`Relationship kind "${kindName}" is already defined`)
119
+ continue
120
+ }
121
+ const bodyProps = mapToObj(
122
+ props.filter(ast.isSpecificationRelationshipStringProperty).filter(p => isNonNullish(p.value)) ?? [],
123
+ p => [p.key, p.value]
124
+ )
105
125
  c4Specification.relationships[kindName] = {
106
- ...c4Specification.relationships[kindName],
126
+ ...bodyProps,
107
127
  ...toRelationshipStyleExcludeDefaults(props)
108
128
  }
109
129
  } catch (e) {
@@ -138,8 +158,9 @@ export class LikeC4ModelParser {
138
158
  const id = this.resolveFqn(astNode)
139
159
  const kind = astNode.kind.$refText as c4.ElementKind
140
160
  const tags = this.convertTags(astNode.body)
141
- const stylePropsAst = astNode.body?.props.find(ast.isStyleProperties)?.props
161
+ const stylePropsAst = astNode.body?.props.find(ast.isElementStyleProperty)?.props
142
162
  const style = toElementStyle(stylePropsAst)
163
+ const metadata = this.getMetadata(astNode.body?.props.find(ast.isMetadataProperty))
143
164
  const astPath = this.getAstNodePath(astNode)
144
165
 
145
166
  let [title, description, technology] = astNode.props ?? []
@@ -153,7 +174,7 @@ export class LikeC4ModelParser {
153
174
  description = removeIndent(bodyProps.description ?? description)
154
175
  technology = toSingleLine(bodyProps.technology ?? technology)
155
176
 
156
- const links = astNode.body?.props.filter(ast.isLinkProperty).map(p => p.value)
177
+ const links = this.convertLinks(astNode.body)
157
178
 
158
179
  // Property has higher priority than from style
159
180
  const iconProp = astNode.body?.props.find(ast.isIconProperty)
@@ -169,6 +190,7 @@ export class LikeC4ModelParser {
169
190
  kind,
170
191
  astPath,
171
192
  title: title ?? astNode.name,
193
+ ...(metadata && { metadata }),
172
194
  ...(tags && { tags }),
173
195
  ...(links && isNonEmptyArray(links) && { links }),
174
196
  ...(isTruthy(technology) && { technology }),
@@ -182,18 +204,19 @@ export class LikeC4ModelParser {
182
204
  const target = this.resolveFqn(coupling.target)
183
205
  const source = this.resolveFqn(coupling.source)
184
206
  const tags = this.convertTags(astNode) ?? this.convertTags(astNode.body)
185
- const links = astNode.body?.props.filter(ast.isLinkProperty).map(p => p.value)
207
+ const links = this.convertLinks(astNode.body)
186
208
  const kind = astNode.kind?.ref?.name as (c4.RelationshipKind | undefined)
209
+ const metadata = this.getMetadata(astNode.body?.props.find(ast.isMetadataProperty))
187
210
  const astPath = this.getAstNodePath(astNode)
188
211
 
189
212
  const bodyProps = mapToObj(
190
- astNode.body?.props.filter(ast.isRelationStringProperty) ?? [],
191
- p => [p.key, p.value || undefined]
213
+ astNode.body?.props.filter(ast.isRelationStringProperty).filter(p => isNonNullish(p.value)) ?? [],
214
+ p => [p.key, p.value]
192
215
  )
193
216
 
194
217
  const title = removeIndent(astNode.title ?? bodyProps.title) ?? ''
195
218
  const description = removeIndent(bodyProps.description)
196
- const technology = toSingleLine(bodyProps.technology)
219
+ const technology = removeIndent(astNode.technology) ?? toSingleLine(bodyProps.technology)
197
220
 
198
221
  const styleProp = astNode.body?.props.find(ast.isRelationStyleProperty)
199
222
  const id = stringHash(
@@ -207,6 +230,7 @@ export class LikeC4ModelParser {
207
230
  source,
208
231
  target,
209
232
  title,
233
+ ...(metadata && { metadata }),
210
234
  ...(isTruthy(technology) && { technology }),
211
235
  ...(isTruthy(description) && { description }),
212
236
  ...(kind && { kind }),
@@ -391,7 +415,12 @@ export class LikeC4ModelParser {
391
415
  }
392
416
  return acc
393
417
  }
394
-
418
+ if (ast.isNotationProperty(prop)) {
419
+ if (isTruthy(prop.value)) {
420
+ acc.custom[prop.key] = removeIndent(prop.value)
421
+ }
422
+ return acc
423
+ }
395
424
  nonexhaustive(prop)
396
425
  },
397
426
  {
@@ -475,6 +504,12 @@ export class LikeC4ModelParser {
475
504
  }
476
505
  return acc
477
506
  }
507
+ if (ast.isNotationProperty(prop)) {
508
+ if (isTruthy(prop.value)) {
509
+ acc.customRelation[prop.key] = removeIndent(prop.value)
510
+ }
511
+ return acc
512
+ }
478
513
  nonexhaustive(prop)
479
514
  },
480
515
  {
@@ -516,9 +551,12 @@ export class LikeC4ModelParser {
516
551
  return this.parseViewRulePredicate(astRule, isValid)
517
552
  }
518
553
  if (ast.isViewRuleStyle(astRule)) {
519
- const styleProps = toElementStyle(astRule.props)
554
+ const styleProps = toElementStyle(astRule.props.filter(ast.isStyleProperty))
555
+ const notation = removeIndent(astRule.props.find(ast.isNotationProperty)?.value)
556
+ const targets = this.parseElementExpressionsIterator(astRule.target)
520
557
  return {
521
- targets: this.parseElementExpressionsIterator(astRule.target),
558
+ targets,
559
+ ...(notation && { notation }),
522
560
  style: {
523
561
  ...styleProps
524
562
  }
@@ -601,6 +639,12 @@ export class LikeC4ModelParser {
601
639
  step[prop.key] = prop.value
602
640
  continue
603
641
  }
642
+ if (ast.isNotationProperty(prop)) {
643
+ if (isTruthy(prop.value)) {
644
+ step[prop.key] = prop.value
645
+ }
646
+ continue
647
+ }
604
648
  nonexhaustive(prop)
605
649
  }
606
650
  catch (e) {
@@ -640,7 +684,7 @@ export class LikeC4ModelParser {
640
684
  const description = removeIndent(body.props.find(p => p.key === 'description')?.value) ?? null
641
685
 
642
686
  const tags = this.convertTags(body)
643
- const links = body.props.filter(ast.isLinkProperty).map(p => p.value)
687
+ const links = this.convertLinks(body)
644
688
 
645
689
  const manualLayout = this.parseViewManualLaout(astNode)
646
690
 
@@ -695,7 +739,7 @@ export class LikeC4ModelParser {
695
739
  const description = removeIndent(props.find(p => p.key === 'description')?.value) ?? null
696
740
 
697
741
  const tags = this.convertTags(body)
698
- const links = props.filter(ast.isLinkProperty).map(p => p.value)
742
+ const links = this.convertLinks(body)
699
743
 
700
744
  ViewOps.writeId(astNode, id as c4.ViewID)
701
745
 
@@ -734,11 +778,13 @@ export class LikeC4ModelParser {
734
778
  return acc
735
779
  }
736
780
  if (ast.isViewRuleStyle(n)) {
737
- const styleProps = toElementStyle(n.props)
781
+ const styleProps = toElementStyle(n.props.filter(ast.isStyleProperty))
782
+ const notation = removeIndent(n.props.find(ast.isNotationProperty)?.value)
738
783
  const targets = this.parseElementExpressionsIterator(n.target)
739
784
  if (targets.length > 0) {
740
785
  acc.push({
741
786
  targets,
787
+ ...(notation && { notation }),
742
788
  style: {
743
789
  ...styleProps
744
790
  }
@@ -785,6 +831,12 @@ export class LikeC4ModelParser {
785
831
  return this.services.workspace.AstNodeLocator.getAstNodePath(node)
786
832
  }
787
833
 
834
+ private getMetadata(metadataAstNode: ast.MetadataProperty | undefined): { [key: string]: string } | undefined {
835
+ return metadataAstNode?.props != null
836
+ ? mapToObj(metadataAstNode.props, (p) => [p.key, removeIndent(p.value)])
837
+ : undefined
838
+ }
839
+
788
840
  private convertTags<E extends { tags?: ast.Tags }>(withTags?: E) {
789
841
  let iter = withTags?.tags
790
842
  if (!iter) {
@@ -804,4 +856,22 @@ export class LikeC4ModelParser {
804
856
  }
805
857
  return isNonEmptyArray(tags) ? tags : null
806
858
  }
859
+
860
+ private convertLinks(source?: ast.LinkProperty['$container']): ParsedLink[] | undefined {
861
+ if (!source?.props || source.props.length === 0) {
862
+ return undefined
863
+ }
864
+ return pipe(
865
+ source.props,
866
+ filter(ast.isLinkProperty),
867
+ flatMap(p => {
868
+ const url = p.value
869
+ if (isTruthy(url)) {
870
+ const title = isTruthy(p.title) ? toSingleLine(p.title) : undefined
871
+ return title ? { url, title } : { url }
872
+ }
873
+ return []
874
+ })
875
+ )
876
+ }
807
877
  }
@@ -29,13 +29,14 @@ import {
29
29
  parentFqn,
30
30
  whereOperatorAsPredicate
31
31
  } from '@likec4/core'
32
- import { first, flatMap, hasAtLeast, isTruthy, unique } from 'remeda'
32
+ import { first, flatMap, hasAtLeast, isTruthy, map, omit, unique } from 'remeda'
33
33
  import { calcViewLayoutHash } from '../../view-utils/view-hash'
34
34
  import type { LikeC4ModelGraph } from '../LikeC4ModelGraph'
35
35
  import { applyCustomElementProperties } from '../utils/applyCustomElementProperties'
36
36
  import { applyCustomRelationProperties } from '../utils/applyCustomRelationProperties'
37
37
  import { applyViewRuleStyles } from '../utils/applyViewRuleStyles'
38
38
  import { buildComputeNodes } from '../utils/buildComputeNodes'
39
+ import { buildElementNotations } from '../utils/buildElementNotations'
39
40
  import { sortNodes } from '../utils/sortNodes'
40
41
  import {
41
42
  type ElementPredicateFn,
@@ -161,18 +162,25 @@ export class ComputeCtx {
161
162
  })
162
163
  )
163
164
  )
164
-
165
165
  const sortedEdges = new Set([
166
166
  ...nodes.flatMap(n => n.children.length === 0 ? n.outEdges.flatMap(id => edgesMap.get(id) ?? []) : []),
167
167
  ...edges
168
168
  ])
169
169
 
170
170
  const autoLayoutRule = this.view.rules.findLast(isViewRuleAutoLayout)
171
+
172
+ const elementNotations = buildElementNotations(nodes)
173
+
171
174
  return calcViewLayoutHash({
172
175
  ...view,
173
176
  autoLayout: autoLayoutRule?.autoLayout ?? 'TB',
174
- nodes,
175
- edges: applyCustomRelationProperties(rules, nodes, sortedEdges)
177
+ nodes: map(nodes, omit(['notation'])),
178
+ edges: applyCustomRelationProperties(rules, nodes, sortedEdges),
179
+ ...(elementNotations.length > 0 && {
180
+ notation: {
181
+ elements: elementNotations
182
+ }
183
+ })
176
184
  })
177
185
  }
178
186
 
@@ -263,7 +271,7 @@ export class ComputeCtx {
263
271
 
264
272
  return Object.assign(
265
273
  edge,
266
- isTruthy(relation.title) && { label: relation.title },
274
+ this.getEdgeLabel(relation),
267
275
  isTruthy(relation.description) && { description: relation.description },
268
276
  isTruthy(relation.technology) && { description: relation.technology },
269
277
  isTruthy(relation.kind) && { kind: relation.kind },
@@ -529,5 +537,19 @@ export class ComputeCtx {
529
537
  return this
530
538
  }
531
539
  nonexhaustive(expr)
540
+ }
541
+
542
+ protected getEdgeLabel(relation: { title: String | undefined, technology?: String | undefined }): { label: String } | false {
543
+ const labelParts: String[] = []
544
+
545
+ if(isTruthy(relation.title)) {
546
+ labelParts.push(relation.title)
547
+ }
548
+
549
+ if(isTruthy(relation.technology)) {
550
+ labelParts.push(`[${relation.technology}]`)
551
+ }
552
+
553
+ return labelParts.length > 0 && { label: labelParts.join('\n') }
532
554
  }
533
555
  }
@@ -22,12 +22,13 @@ import {
22
22
  parentFqn,
23
23
  StepEdgeId
24
24
  } from '@likec4/core'
25
- import { hasAtLeast, isTruthy, map, unique } from 'remeda'
25
+ import { hasAtLeast, isTruthy, map, omit, unique } from 'remeda'
26
26
  import { calcViewLayoutHash } from '../../view-utils/view-hash'
27
27
  import type { LikeC4ModelGraph } from '../LikeC4ModelGraph'
28
28
  import { applyCustomElementProperties } from '../utils/applyCustomElementProperties'
29
29
  import { applyViewRuleStyles } from '../utils/applyViewRuleStyles'
30
30
  import { buildComputeNodes } from '../utils/buildComputeNodes'
31
+ import { buildElementNotations } from '../utils/buildElementNotations'
31
32
  import { elementExprToPredicate } from '../utils/elementExpressionToPredicate'
32
33
 
33
34
  export namespace DynamicViewComputeCtx {
@@ -162,11 +163,18 @@ export class DynamicViewComputeCtx {
162
163
 
163
164
  const autoLayoutRule = rules.findLast(isViewRuleAutoLayout)
164
165
 
166
+ const elementNotations = buildElementNotations(nodes)
167
+
165
168
  return calcViewLayoutHash({
166
169
  ...view,
167
170
  autoLayout: autoLayoutRule?.autoLayout ?? 'LR',
168
- nodes,
169
- edges
171
+ nodes: map(nodes, omit(['notation'])),
172
+ edges,
173
+ ...(elementNotations.length > 0 && {
174
+ notation: {
175
+ elements: elementNotations
176
+ }
177
+ })
170
178
  })
171
179
  }
172
180