@likec4/language-server 1.7.3 → 1.8.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.
@@ -42,18 +42,30 @@ 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:
@@ -92,7 +104,7 @@ ElementBody: '{'
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 ';'?;
@@ -133,6 +145,7 @@ fragment RelationFragment:
133
145
  ('->' | '-[' kind=[RelationshipKind] ']->' | kind=[RelationshipKind:DotId] )
134
146
  target=ElementRef
135
147
  title=String?
148
+ technology=String?
136
149
  tags=Tags?
137
150
  body=RelationBody?
138
151
  ;
@@ -144,7 +157,7 @@ RelationBody: '{'
144
157
  ;
145
158
 
146
159
  RelationProperty:
147
- RelationStringProperty | RelationStyleProperty | LinkProperty;
160
+ RelationStringProperty | RelationStyleProperty | LinkProperty | MetadataProperty;
148
161
 
149
162
  RelationStringProperty:
150
163
  key=('title' | 'technology' | 'description') ':'? value=String ';'?;
@@ -155,6 +168,18 @@ RelationStyleProperty:
155
168
  '}'
156
169
  ;
157
170
 
171
+ MetadataProperty:
172
+ 'metadata' MetadataBody
173
+ ;
174
+
175
+ MetadataBody: '{'
176
+ props+=(MetadataAttribute)*
177
+ '}'
178
+ ;
179
+
180
+ MetadataAttribute:
181
+ key=IdTerminal value=String
182
+ ;
158
183
 
159
184
  // Views -------------------------------------
160
185
 
@@ -200,7 +225,15 @@ DynamicViewBody: '{'
200
225
  ;
201
226
 
202
227
 
203
- type StringProperty = ElementStringProperty | ViewStringProperty | RelationStringProperty;
228
+ type StringProperty =
229
+ ElementStringProperty |
230
+ ViewStringProperty |
231
+ RelationStringProperty |
232
+ MetadataAttribute |
233
+ SpecificationElementStringProperty |
234
+ SpecificationRelationshipStringProperty |
235
+ NotationProperty
236
+ ;
204
237
 
205
238
  ViewProperty:
206
239
  ViewStringProperty | LinkProperty
@@ -376,16 +409,24 @@ DynamicViewPredicateIterator:
376
409
 
377
410
  ViewRuleStyle:
378
411
  'style' target=ElementExpressionsIterator '{'
379
- props+=StyleProperty*
412
+ props+=(
413
+ StyleProperty |
414
+ NotationProperty
415
+ )*
380
416
  '}';
381
417
 
382
418
  ViewRuleAutoLayout:
383
419
  'autoLayout' direction=ViewLayoutDirection;
384
420
 
421
+ NotationProperty:
422
+ key='notation' ':'? value=String ';'?
423
+ ;
424
+
385
425
  CustomElementProperties: '{'
386
426
  props+=(
387
427
  NavigateToProperty |
388
428
  ElementStringProperty |
429
+ NotationProperty |
389
430
  StyleProperty
390
431
  )*
391
432
  '}'
@@ -394,6 +435,7 @@ CustomElementProperties: '{'
394
435
  CustomRelationProperties: '{'
395
436
  props+=(
396
437
  RelationStringProperty |
438
+ NotationProperty |
397
439
  RelationshipStyleProperty
398
440
  )*
399
441
  '}'
@@ -405,7 +447,7 @@ NavigateToProperty:
405
447
  // Common properties -------------------------------------
406
448
 
407
449
  LinkProperty:
408
- key='link' ':'? value=Uri ';'?;
450
+ key='link' ':'? value=Uri title=String? ';'?;
409
451
  ColorProperty:
410
452
  key='color' ':'? value=ThemeColor ';'?;
411
453
 
@@ -433,7 +475,7 @@ StyleProperty:
433
475
  OpacityProperty |
434
476
  IconProperty;
435
477
 
436
- StyleProperties:
478
+ ElementStyleProperty:
437
479
  key='style' '{'
438
480
  props+=StyleProperty*
439
481
  '}';
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  import type { ComputedNode, ViewRule } from '@likec4/core'
2
- import { Expr, nonNullable } from '@likec4/core'
3
- import { isEmpty, isNonNullish, isNullish, omitBy, pickBy } from 'remeda'
2
+ import { Expr } from '@likec4/core'
3
+ import { isEmpty, isNullish, omitBy } from 'remeda'
4
4
  import { elementExprToPredicate } from './elementExpressionToPredicate'
5
5
 
6
6
  export function applyCustomElementProperties(_rules: ViewRule[], _nodes: ComputedNode[]) {
@@ -15,12 +15,13 @@ export function applyCustomElementProperties(_rules: ViewRule[], _nodes: Compute
15
15
  } of rules
16
16
  ) {
17
17
  const { border, opacity, ...rest } = omitBy(props, isNullish)
18
+ const notEmpty = !isEmpty(rest)
18
19
  const satisfies = elementExprToPredicate(expr)
19
20
  nodes.forEach((node, i) => {
20
21
  if (!satisfies(node)) {
21
22
  return
22
23
  }
23
- if (!isEmpty(rest)) {
24
+ if (notEmpty) {
24
25
  node = {
25
26
  ...node,
26
27
  isCustomized: true,
@@ -25,6 +25,9 @@ export function applyViewRuleStyles(_rules: ViewRule[], nodes: ComputedNode[]) {
25
25
  if (isDefined(rule.style.icon)) {
26
26
  n.icon = rule.style.icon
27
27
  }
28
+ if (isDefined(rule.notation)) {
29
+ n.notation = rule.notation
30
+ }
28
31
  let styleOverride: ComputedNode['style'] | undefined
29
32
  if (isDefined(rule.style.border)) {
30
33
  styleOverride = { border: rule.style.border }