@likec4/language-server 1.14.0 → 1.15.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.
Files changed (46) hide show
  1. package/contrib/likec4.tmLanguage.json +1 -1
  2. package/dist/browser.cjs +1 -1
  3. package/dist/browser.d.cts +2 -2
  4. package/dist/browser.d.mts +2 -2
  5. package/dist/browser.d.ts +2 -2
  6. package/dist/browser.mjs +2 -2
  7. package/dist/index.cjs +1 -1
  8. package/dist/index.d.cts +2 -2
  9. package/dist/index.d.mts +2 -2
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.mjs +2 -2
  12. package/dist/likec4lib.cjs +0 -1
  13. package/dist/likec4lib.mjs +0 -1
  14. package/dist/model-graph/index.cjs +1 -1
  15. package/dist/model-graph/index.mjs +1 -1
  16. package/dist/shared/{language-server.CbDa016p.cjs → language-server.B8s2wfT_.cjs} +279 -68
  17. package/dist/shared/{language-server.DFLaUdYu.mjs → language-server.BT4WTbFI.mjs} +281 -70
  18. package/dist/shared/{language-server.DZYziEuF.mjs → language-server.BuChFlda.mjs} +231 -44
  19. package/dist/shared/{language-server.CO6aEDQm.d.mts → language-server.DfMwkd2l.d.mts} +46 -15
  20. package/dist/shared/{language-server.CITj0XN3.cjs → language-server.DfjkvknB.cjs} +230 -43
  21. package/dist/shared/{language-server.Cqyh6-9S.d.cts → language-server.U2piOAVt.d.cts} +46 -15
  22. package/dist/shared/{language-server.BI99piRy.d.ts → language-server.j-ShR6as.d.ts} +46 -15
  23. package/package.json +10 -10
  24. package/src/ast.ts +1 -0
  25. package/src/formatting/LikeC4Formatter.ts +44 -4
  26. package/src/generated/ast.ts +103 -19
  27. package/src/generated/grammar.ts +1 -1
  28. package/src/generated-lib/icons.ts +0 -1
  29. package/src/like-c4.langium +32 -9
  30. package/src/lsp/CompletionProvider.ts +70 -2
  31. package/src/lsp/SemanticTokenProvider.ts +3 -1
  32. package/src/model/model-builder.ts +13 -7
  33. package/src/model/model-parser.ts +71 -20
  34. package/src/model-graph/compute-view/__test__/fixture.ts +96 -29
  35. package/src/model-graph/compute-view/compute.ts +223 -40
  36. package/src/model-graph/compute-view/predicates.ts +12 -6
  37. package/src/model-graph/utils/applyCustomElementProperties.ts +18 -4
  38. package/src/model-graph/utils/applyCustomRelationProperties.ts +9 -39
  39. package/src/model-graph/utils/applyViewRuleStyles.ts +30 -25
  40. package/src/model-graph/utils/buildComputeNodes.ts +69 -17
  41. package/src/model-graph/utils/elementExpressionToPredicate.ts +3 -1
  42. package/src/model-graph/utils/relationExpressionToPredicates.ts +43 -0
  43. package/src/model-graph/utils/sortNodes.ts +11 -7
  44. package/src/references/scope-computation.ts +3 -2
  45. package/src/validation/index.ts +2 -2
  46. package/src/validation/specification.ts +4 -4
@@ -342,7 +342,6 @@ export const LibIcons: string = `likec4lib { icons {
342
342
  azure:arc-data-services
343
343
  azure:arc-kubernetes
344
344
  azure:arc-machines
345
- azure:arc-postgre-sql
346
345
  azure:arc-sql-managed-instance
347
346
  azure:arc-sql-server
348
347
  azure:atm-multistack
@@ -197,7 +197,7 @@ MetadataAttribute:
197
197
  ModelViews:
198
198
  name='views' '{' (
199
199
  views+=LikeC4ViewRule |
200
- styles+=ViewRuleStyle
200
+ styles+=ViewRuleStyleOrGlobalRef
201
201
  )*
202
202
  '}';
203
203
 
@@ -267,15 +267,27 @@ ViewLayoutDirection returns string:
267
267
 
268
268
  ViewRule:
269
269
  ViewRulePredicate |
270
- ViewRuleStyle |
271
- ViewRuleGlobalStyle |
270
+ ViewRuleGroup |
271
+ ViewRuleStyleOrGlobalRef |
272
272
  ViewRuleAutoLayout
273
273
  ;
274
274
 
275
+ ViewRuleGroup:
276
+ 'group' title=String? '{'
277
+ props+=(
278
+ ColorProperty |
279
+ BorderProperty |
280
+ OpacityProperty
281
+ )*
282
+ groupRules+=(
283
+ ViewRulePredicate |
284
+ ViewRuleGroup
285
+ )*
286
+ '}';
287
+
275
288
  DynamicViewRule:
276
289
  DynamicViewIncludePredicate |
277
- ViewRuleStyle |
278
- ViewRuleGlobalStyle |
290
+ ViewRuleStyleOrGlobalRef |
279
291
  ViewRuleAutoLayout
280
292
  ;
281
293
 
@@ -444,7 +456,10 @@ ViewRuleStyle:
444
456
  '}';
445
457
 
446
458
  ViewRuleGlobalStyle:
447
- 'global' 'style' style=[GlobalStyle];
459
+ 'global' 'style' style=[GlobalStyleId];
460
+
461
+ ViewRuleStyleOrGlobalRef:
462
+ ViewRuleStyle | ViewRuleGlobalStyle;
448
463
 
449
464
  ViewRuleAutoLayout:
450
465
  'autoLayout' direction=ViewLayoutDirection (
@@ -491,18 +506,26 @@ RelationNavigateToProperty:
491
506
  Globals:
492
507
  name='global' '{'
493
508
  (
494
- styles+=GlobalStyle*
509
+ styles+=(GlobalStyle | GlobalStyleGroup)*
495
510
  )*
496
511
  '}';
497
512
 
513
+ GlobalStyleId:
514
+ name=IdTerminal;
515
+
498
516
  GlobalStyle:
499
- 'style' name=IdTerminal target=ElementExpressionsIterator '{'
517
+ 'style' id=GlobalStyleId target=ElementExpressionsIterator '{'
500
518
  props+=(
501
519
  StyleProperty |
502
520
  NotationProperty
503
521
  )*
504
522
  '}';
505
523
 
524
+ GlobalStyleGroup:
525
+ 'styleGroup' id=GlobalStyleId '{'
526
+ styles+=ViewRuleStyle*
527
+ '}';
528
+
506
529
  // Common properties -------------------------------------
507
530
 
508
531
  LinkProperty:
@@ -593,7 +616,7 @@ CustomColorId returns string:
593
616
  IdTerminal | ElementShape | ArrowType | LineOptions | 'element' | 'model';
594
617
 
595
618
  Id returns string:
596
- IdTerminal | ElementShape | ThemeColor | ArrowType | LineOptions | 'element' | 'model';
619
+ IdTerminal | ElementShape | ThemeColor | ArrowType | LineOptions | 'element' | 'model' | 'group';
597
620
 
598
621
  fragment EqOperator:
599
622
  (
@@ -1,11 +1,13 @@
1
- import { type GrammarAST, type MaybePromise } from 'langium'
1
+ import { AstUtils, type GrammarAST, type MaybePromise } from 'langium'
2
2
  import {
3
3
  type CompletionAcceptor,
4
4
  type CompletionContext,
5
5
  type CompletionProviderOptions,
6
6
  DefaultCompletionProvider
7
7
  } from 'langium/lsp'
8
+ import { anyPass } from 'remeda'
8
9
  import { CompletionItemKind, InsertTextFormat } from 'vscode-languageserver-types'
10
+ import { ast } from '../ast'
9
11
 
10
12
  export class LikeC4CompletionProvider extends DefaultCompletionProvider {
11
13
  override readonly completionOptions = {
@@ -20,7 +22,15 @@ export class LikeC4CompletionProvider extends DefaultCompletionProvider {
20
22
  if (!this.filterKeyword(context, keyword)) {
21
23
  return
22
24
  }
23
- if (['views', 'specification', 'model'].includes(keyword.value)) {
25
+ if (['title', 'description', 'technology'].includes(keyword.value)) {
26
+ return acceptor(context, {
27
+ label: keyword.value,
28
+ kind: CompletionItemKind.Property,
29
+ insertTextFormat: InsertTextFormat.Snippet,
30
+ insertText: `${keyword.value} '\${0}'`
31
+ })
32
+ }
33
+ if (['views', 'specification', 'model', 'with'].includes(keyword.value)) {
24
34
  return acceptor(context, {
25
35
  label: keyword.value,
26
36
  detail: `Insert ${keyword.value} block`,
@@ -29,6 +39,19 @@ export class LikeC4CompletionProvider extends DefaultCompletionProvider {
29
39
  insertText: `${keyword.value} {\n\t$0\n}`
30
40
  })
31
41
  }
42
+ if (keyword.value === 'group') {
43
+ return acceptor(context, {
44
+ label: keyword.value,
45
+ detail: `Insert group block`,
46
+ kind: CompletionItemKind.Class,
47
+ insertTextFormat: InsertTextFormat.Snippet,
48
+ insertText: [
49
+ 'group \'${1:Title}\' {',
50
+ '\t$0',
51
+ '}'
52
+ ].join('\n')
53
+ })
54
+ }
32
55
  if (keyword.value === 'dynamic') {
33
56
  return acceptor(context, {
34
57
  label: keyword.value,
@@ -44,6 +67,51 @@ export class LikeC4CompletionProvider extends DefaultCompletionProvider {
44
67
  ].join('\n')
45
68
  })
46
69
  }
70
+ if (keyword.value === 'style' && context.node) {
71
+ if (AstUtils.hasContainerOfType(context.node, ast.isGlobalStyle)) {
72
+ return acceptor(context, {
73
+ label: keyword.value,
74
+ detail: `Insert ${keyword.value} block`,
75
+ kind: CompletionItemKind.Module,
76
+ insertTextFormat: InsertTextFormat.Snippet,
77
+ insertText: `${keyword.value} \${1:name} \${2:*} {\n\t\${3|color,shape,border,opacity,icon|} \$0\n}`
78
+ })
79
+ }
80
+ if (AstUtils.hasContainerOfType(context.node, anyPass([ast.isModelViews, ast.isGlobalStyleGroup]))) {
81
+ return acceptor(context, {
82
+ label: keyword.value,
83
+ detail: `Insert ${keyword.value} block`,
84
+ kind: CompletionItemKind.Module,
85
+ insertTextFormat: InsertTextFormat.Snippet,
86
+ insertText: `${keyword.value} \${1:*} {\n\t\${2|color,shape,border,opacity,icon|} \$0\n}`
87
+ })
88
+ }
89
+ return acceptor(context, {
90
+ label: keyword.value,
91
+ detail: `Insert ${keyword.value} block`,
92
+ kind: CompletionItemKind.Module,
93
+ insertTextFormat: InsertTextFormat.Snippet,
94
+ insertText: `${keyword.value} {\n\t\${1|color,shape,border,opacity,icon|} \$0\n}`
95
+ })
96
+ }
97
+ if (keyword.value === 'extend') {
98
+ return acceptor(context, {
99
+ label: keyword.value,
100
+ detail: `Extend another view`,
101
+ kind: CompletionItemKind.Class,
102
+ insertTextFormat: InsertTextFormat.Snippet,
103
+ insertText: 'extend ${1:element} {\n\t$0\n}'
104
+ })
105
+ }
106
+
107
+ if (keyword.value === 'autoLayout') {
108
+ return acceptor(context, {
109
+ label: keyword.value,
110
+ kind: CompletionItemKind.Class,
111
+ insertTextFormat: InsertTextFormat.Snippet,
112
+ insertText: 'autoLayout ${1|TopBottom,BottomTop,LeftRight,RightLeft|}$0'
113
+ })
114
+ }
47
115
  acceptor(context, {
48
116
  label: keyword.value,
49
117
  kind: this.getKeywordCompletionItemKind(keyword),
@@ -68,7 +68,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
68
68
  SemanticTokenModifiers.readonly
69
69
  ]
70
70
  })
71
- return
71
+ return 'prune'
72
72
  }
73
73
  if (ast.isWhereRelationKind(node) && isTruthy(node.value)) {
74
74
  acceptor({
@@ -97,6 +97,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
97
97
  type: SemanticTokenTypes.type,
98
98
  modifier: [SemanticTokenModifiers.definition]
99
99
  })
100
+ return 'prune'
100
101
  }
101
102
  if (ast.isElementTagExpression(node) && isTruthy(node.tag)) {
102
103
  acceptor({
@@ -105,6 +106,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
105
106
  type: SemanticTokenTypes.type,
106
107
  modifier: [SemanticTokenModifiers.definition]
107
108
  })
109
+ return 'prune'
108
110
  }
109
111
  if (ast.isElementRef(node) || ast.isFqnElementRef(node)) {
110
112
  acceptor({
@@ -46,7 +46,6 @@ import { isParsedLikeC4LangiumDocument } from '../ast'
46
46
  import { logError, logger, logWarnError } from '../logger'
47
47
  import { computeDynamicView, computeView, LikeC4ModelGraph } from '../model-graph'
48
48
  import type { LikeC4Services } from '../module'
49
- import { printDocs } from '../utils/printDocs'
50
49
  import { assignNavigateTo, resolveGlobalRules, resolveRelativePaths, resolveRulesExtendedViews } from '../view-utils'
51
50
 
52
51
  function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[]): c4.ParsedLikeC4Model {
@@ -256,18 +255,23 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
256
255
 
257
256
  return {
258
257
  ...model,
258
+ customColorDefinitions,
259
259
  tags,
260
260
  links,
261
261
  docUri,
262
262
  description,
263
263
  title,
264
- id,
265
- customColorDefinitions
264
+ id
266
265
  }
267
266
  }
268
267
  }
269
268
 
270
- const parsedViews = docs.flatMap(d => map(d.c4Views, toC4View(d)))
269
+ const parsedViews = pipe(
270
+ docs,
271
+ flatMap(d => map(d.c4Views, toC4View(d))),
272
+ // Resolve relative paths and sort by
273
+ resolveRelativePaths
274
+ )
271
275
  // Add index view if not present
272
276
  if (!parsedViews.some(v => v.id === 'index')) {
273
277
  parsedViews.unshift({
@@ -292,7 +296,6 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
292
296
 
293
297
  const views = pipe(
294
298
  parsedViews,
295
- resolveRelativePaths,
296
299
  indexBy(prop('id')),
297
300
  resolveRulesExtendedViews
298
301
  )
@@ -363,7 +366,7 @@ export class LikeC4ModelBuilder {
363
366
  logger.debug('[ModelBuilder] No documents to build model from')
364
367
  return null
365
368
  }
366
- logger.debug(`[ModelBuilder] onValidated (${docs.length} docs)`)
369
+ logger.debug(`[ModelBuilder] buildModel (${docs.length} docs)`)
367
370
  return buildModel(this.services, docs)
368
371
  })
369
372
  }
@@ -396,7 +399,10 @@ export class LikeC4ModelBuilder {
396
399
 
397
400
  const allViews = [] as c4.ComputedView[]
398
401
  for (const view of values(model.views)) {
399
- const result = isElementView(view) ? computeView(view, index) : computeDynamicView(view, index)
402
+ const resolvedView = resolveGlobalRules(view, model.globals.styles)
403
+ const result = isElementView(resolvedView)
404
+ ? computeView(resolvedView, index)
405
+ : computeDynamicView(resolvedView, index)
400
406
  if (!result.isSuccess) {
401
407
  logWarnError(result.error)
402
408
  continue
@@ -30,7 +30,7 @@ import {
30
30
  ViewOps
31
31
  } from '../ast'
32
32
  import { elementRef, getFqnElementRef } from '../elementRef'
33
- import { type NotationProperty } from '../generated/ast'
33
+ import { isGlobalStyle, isGlobalStyleGroup, type NotationProperty } from '../generated/ast'
34
34
  import { logError, logger, logWarnError } from '../logger'
35
35
  import type { LikeC4Services } from '../module'
36
36
  import { stringHash } from '../utils'
@@ -291,7 +291,7 @@ export class LikeC4ModelParser {
291
291
  const styles = globals.flatMap(r => r.styles.filter(isValid))
292
292
  for (const style of styles) {
293
293
  try {
294
- const globalStyleId = style.name as c4.GlobalStyleID
294
+ const globalStyleId = style.id.name as c4.GlobalStyleID
295
295
  if (!isTruthy(globalStyleId)) {
296
296
  continue
297
297
  }
@@ -299,15 +299,30 @@ export class LikeC4ModelParser {
299
299
  logger.warn(`Global style named "${globalStyleId}" is already defined`)
300
300
  continue
301
301
  }
302
- c4Globals.styles[globalStyleId] = [
303
- this.parseGlobalStyle(style, isValid)
304
- ]
302
+
303
+ const styles = this.parseGlobalStyleOrGroup(style, isValid)
304
+ if (styles.length > 0) {
305
+ c4Globals.styles[globalStyleId] = styles as c4.NonEmptyArray<c4.ViewRuleStyle>
306
+ }
305
307
  } catch (e) {
306
308
  logWarnError(e)
307
309
  }
308
310
  }
309
311
  }
310
312
 
313
+ private parseGlobalStyleOrGroup(
314
+ astRule: ast.GlobalStyle | ast.GlobalStyleGroup,
315
+ isValid: IsValidFn
316
+ ): c4.ViewRuleStyle[] {
317
+ if (ast.isGlobalStyle(astRule)) {
318
+ return [this.parseGlobalStyle(astRule, isValid)]
319
+ }
320
+ if (ast.isGlobalStyleGroup(astRule)) {
321
+ return astRule.styles.map(s => this.parseViewRuleStyle(s, isValid))
322
+ }
323
+ nonexhaustive(astRule)
324
+ }
325
+
311
326
  private parseGlobalStyle(astRule: ast.GlobalStyle, isValid: IsValidFn): c4.ViewRuleStyle {
312
327
  const styleProps = astRule.props.filter(ast.isStyleProperty)
313
328
  const targets = astRule.target
@@ -319,7 +334,7 @@ export class LikeC4ModelParser {
319
334
  const viewBlocks = doc.parseResult.value.views.filter(v => isValid(v))
320
335
  for (const viewBlock of viewBlocks) {
321
336
  const localStyles = viewBlock.styles
322
- .flatMap(s => this.parseViewRuleStyle(s, isValid))
337
+ .flatMap(s => this.parseViewRuleStyleOrGlobalRef(s, isValid))
323
338
  const stylesToApply = localStyles
324
339
 
325
340
  for (const view of viewBlock.views) {
@@ -530,8 +545,11 @@ export class LikeC4ModelParser {
530
545
 
531
546
  private parseRelationPredicate(astNode: ast.RelationPredicate, _isValid: IsValidFn): c4.RelationPredicateExpression {
532
547
  if (ast.isRelationPredicateWith(astNode)) {
533
- const subject = ast.isRelationPredicateWhere(astNode.subject) ? astNode.subject.subject : astNode.subject
534
- return this.parseRelationPredicateWith(astNode, subject)
548
+ let relation = ast.isRelationPredicateWhere(astNode.subject)
549
+ ? this.parseRelationPredicateWhere(astNode.subject)
550
+ : this.parseRelationExpr(astNode.subject)
551
+
552
+ return this.parseRelationPredicateWith(astNode, relation)
535
553
  }
536
554
  if (ast.isRelationPredicateWhere(astNode)) {
537
555
  return this.parseRelationPredicateWhere(astNode)
@@ -558,9 +576,8 @@ export class LikeC4ModelParser {
558
576
 
559
577
  private parseRelationPredicateWith(
560
578
  astNode: ast.RelationPredicateWith,
561
- subject: ast.RelationExpression
579
+ relation: c4.RelationExpression | c4.RelationWhereExpr
562
580
  ): c4.CustomRelationExpr {
563
- const relation = this.parseRelationExpr(subject)
564
581
  const props = astNode.custom?.props ?? []
565
582
  return props.reduce(
566
583
  (acc, prop) => {
@@ -636,12 +653,22 @@ export class LikeC4ModelParser {
636
653
  if (ast.isViewRulePredicate(astRule)) {
637
654
  return this.parseViewRulePredicate(astRule, isValid)
638
655
  }
639
- if (ast.isViewRuleStyle(astRule)) {
640
- return this.parseViewRuleStyle(astRule, isValid)
656
+ if (ast.isViewRuleStyleOrGlobalRef(astRule)) {
657
+ return this.parseViewRuleStyleOrGlobalRef(astRule, isValid)
641
658
  }
642
659
  if (ast.isViewRuleAutoLayout(astRule)) {
643
660
  return toAutoLayout(astRule)
644
661
  }
662
+ if (ast.isViewRuleGroup(astRule)) {
663
+ return this.parseViewRuleGroup(astRule, isValid)
664
+ }
665
+ nonexhaustive(astRule)
666
+ }
667
+
668
+ private parseViewRuleStyleOrGlobalRef(astRule: ast.ViewRuleStyleOrGlobalRef, isValid: IsValidFn): c4.ViewRuleStyleOrGlobalRef {
669
+ if (ast.isViewRuleStyle(astRule)) {
670
+ return this.parseViewRuleStyle(astRule, isValid)
671
+ }
645
672
  if (ast.isViewRuleGlobalStyle(astRule)) {
646
673
  return this.parseViewRuleGlobalStyle(astRule, isValid)
647
674
  }
@@ -655,6 +682,33 @@ export class LikeC4ModelParser {
655
682
  return this.parseRuleStyle(styleProps, targets, isValid, notation)
656
683
  }
657
684
 
685
+ private parseViewRuleGroup(astNode: ast.ViewRuleGroup, _isValid: IsValidFn): c4.ViewRuleGroup {
686
+ const groupRules = [] as c4.ViewRuleGroup['groupRules']
687
+ for (const rule of astNode.groupRules) {
688
+ try {
689
+ if (!_isValid(rule)) {
690
+ continue
691
+ }
692
+ if (ast.isViewRulePredicate(rule)) {
693
+ groupRules.push(this.parseViewRulePredicate(rule, _isValid))
694
+ continue
695
+ }
696
+ if (ast.isViewRuleGroup(rule)) {
697
+ groupRules.push(this.parseViewRuleGroup(rule, _isValid))
698
+ continue
699
+ }
700
+ nonexhaustive(rule)
701
+ } catch (e) {
702
+ logWarnError(e)
703
+ }
704
+ }
705
+ return {
706
+ title: toSingleLine(astNode.title) ?? null,
707
+ groupRules,
708
+ ...toElementStyle(astNode.props, _isValid)
709
+ }
710
+ }
711
+
658
712
  private parseRuleStyle(
659
713
  styleProperties: ast.StyleProperty[],
660
714
  elementExpressionsIterator: ast.ElementExpressionsIterator,
@@ -673,7 +727,7 @@ export class LikeC4ModelParser {
673
727
  }
674
728
  }
675
729
 
676
- private parseViewRuleGlobalStyle(astRule: ast.ViewRuleGlobalStyle, isValid: IsValidFn): c4.ViewRuleGlobalStyle {
730
+ private parseViewRuleGlobalStyle(astRule: ast.ViewRuleGlobalStyle, _isValid: IsValidFn): c4.ViewRuleGlobalStyle {
677
731
  return {
678
732
  styleId: astRule.style.$refText as c4.GlobalStyleID
679
733
  }
@@ -777,7 +831,7 @@ export class LikeC4ModelParser {
777
831
 
778
832
  private parseElementView(
779
833
  astNode: ast.ElementView,
780
- additionalStyles: c4.ViewRuleStyle[],
834
+ additionalStyles: c4.ViewRuleStyleOrGlobalRef[],
781
835
  isValid: IsValidFn
782
836
  ): ParsedAstElementView {
783
837
  const body = astNode.body
@@ -849,7 +903,7 @@ export class LikeC4ModelParser {
849
903
 
850
904
  private parseDynamicElementView(
851
905
  astNode: ast.DynamicView,
852
- additionalStyles: c4.ViewRuleStyle[],
906
+ additionalStyles: c4.ViewRuleStyleOrGlobalRef[],
853
907
  isValid: IsValidFn
854
908
  ): ParsedAstDynamicView {
855
909
  const body = astNode.body
@@ -917,15 +971,12 @@ export class LikeC4ModelParser {
917
971
  if (ast.isDynamicViewIncludePredicate(astRule)) {
918
972
  return this.parseDynamicViewIncludePredicate(astRule, isValid)
919
973
  }
920
- if (ast.isViewRuleStyle(astRule)) {
921
- return this.parseViewRuleStyle(astRule, isValid)
974
+ if (ast.isViewRuleStyleOrGlobalRef(astRule)) {
975
+ return this.parseViewRuleStyleOrGlobalRef(astRule, isValid)
922
976
  }
923
977
  if (ast.isViewRuleAutoLayout(astRule)) {
924
978
  return toAutoLayout(astRule)
925
979
  }
926
- if (ast.isViewRuleGlobalStyle(astRule)) {
927
- return this.parseViewRuleGlobalStyle(astRule, isValid)
928
- }
929
980
  nonexhaustive(astRule)
930
981
  }
931
982
 
@@ -11,8 +11,16 @@ import {
11
11
  type ElementWhereExpr,
12
12
  type Expression as C4Expression,
13
13
  type Fqn,
14
+ type IncomingExpr as C4IncomingExpr,
15
+ type InOutExpr as C4InOutExpr,
16
+ isElementRef,
17
+ isElementWhere,
18
+ isRelationExpression,
19
+ isRelationWhere,
14
20
  type NonEmptyArray,
21
+ type OutgoingExpr as C4OutgoingExpr,
15
22
  type Relation,
23
+ type RelationExpr as C4RelationExpr,
16
24
  type RelationID,
17
25
  type RelationshipArrowType,
18
26
  type RelationshipLineType,
@@ -20,6 +28,7 @@ import {
20
28
  type Tag,
21
29
  type ViewID,
22
30
  type ViewRule,
31
+ type ViewRuleGroup,
23
32
  type ViewRulePredicate,
24
33
  type ViewRuleStyle,
25
34
  type WhereOperator
@@ -362,8 +371,6 @@ export type Expression =
362
371
  | IncomingExpr
363
372
  | OutgoingExpr
364
373
  | RelationExpr
365
- | ElementWhereExpr
366
- | RelationWhereExpr
367
374
 
368
375
  export function $custom(
369
376
  expr: ElementRefExpr,
@@ -411,6 +418,49 @@ export function $where(
411
418
  }
412
419
  }
413
420
 
421
+ export function $inout(
422
+ expr: InOutExpr | C4ElementExpression
423
+ ): C4InOutExpr {
424
+ const innerExpression = !isString(expr)
425
+ ? expr as C4Expression
426
+ : $expr(expr.replace(/->/g, '').trim() as ElementRefExpr) as any
427
+
428
+ return { inout: innerExpression }
429
+ }
430
+
431
+ export function $incoming(
432
+ expr: IncomingExpr | C4ElementExpression
433
+ ): C4IncomingExpr {
434
+ const innerExpression = !isString(expr)
435
+ ? expr as C4Expression
436
+ : $expr(expr.replace('-> ', '') as ElementRefExpr) as any
437
+
438
+ return { incoming: innerExpression }
439
+ }
440
+
441
+ export function $outgoing(
442
+ expr: OutgoingExpr | C4ElementExpression
443
+ ): C4OutgoingExpr {
444
+ const innerExpression = !isString(expr)
445
+ ? expr as C4Expression
446
+ : $expr(expr.replace(' ->', '') as ElementRefExpr) as any
447
+
448
+ return { outgoing: innerExpression }
449
+ }
450
+
451
+ export function $relation(
452
+ expr: RelationExpr
453
+ ): C4RelationExpr {
454
+ const [source, target] = expr.split(/ -> | <-> /)
455
+ const isBidirectional = expr.includes(' <-> ')
456
+
457
+ return {
458
+ source: $expr(source as ElementRefExpr) as any,
459
+ target: $expr(target as ElementRefExpr) as any,
460
+ ...(isBidirectional && { isBidirectional })
461
+ }
462
+ }
463
+
414
464
  export function $expr(expr: Expression | C4Expression): C4Expression {
415
465
  if (!isString(expr)) {
416
466
  return expr as C4Expression
@@ -419,34 +469,13 @@ export function $expr(expr: Expression | C4Expression): C4Expression {
419
469
  return { wildcard: true }
420
470
  }
421
471
  if (expr.startsWith('->')) {
422
- if (expr.endsWith('->')) {
423
- return {
424
- inout: $expr(expr.replace(/->/g, '').trim() as ElementRefExpr) as any
425
- }
426
- }
427
- return {
428
- incoming: $expr(expr.replace('-> ', '') as ElementRefExpr) as any
429
- }
472
+ return expr.endsWith('->') ? $inout(expr as InOutExpr) : $incoming(expr as IncomingExpr)
430
473
  }
431
474
  if (expr.endsWith(' ->')) {
432
- return {
433
- outgoing: $expr(expr.replace(' ->', '') as ElementRefExpr) as any
434
- }
435
- }
436
- if (expr.includes(' <-> ')) {
437
- const [source, target] = expr.split(' <-> ')
438
- return {
439
- source: $expr(source as ElementRefExpr) as any,
440
- target: $expr(target as ElementRefExpr) as any,
441
- isBidirectional: true
442
- }
475
+ return $outgoing(expr as OutgoingExpr)
443
476
  }
444
- if (expr.includes(' -> ')) {
445
- const [source, target] = expr.split(' -> ')
446
- return {
447
- source: $expr(source as ElementRefExpr) as any,
448
- target: $expr(target as ElementRefExpr) as any
449
- }
477
+ if (expr.includes(' -> ') || expr.includes(' <-> ')) {
478
+ return $relation(expr as RelationExpr)
450
479
  }
451
480
  if (expr.endsWith('._')) {
452
481
  return {
@@ -465,9 +494,41 @@ export function $expr(expr: Expression | C4Expression): C4Expression {
465
494
  }
466
495
  }
467
496
 
468
- export function $include(expr: Expression | C4Expression): ViewRulePredicate {
497
+ type CustomProps = {
498
+ where?: WhereOperator<TestTag, string>
499
+ with?: {
500
+ title?: string
501
+ description?: string
502
+ technology?: string
503
+ shape?: ElementShape
504
+ color?: Color
505
+ border?: BorderStyle
506
+ icon?: string
507
+ opacity?: number
508
+ navigateTo?: string
509
+ } & Omit<C4CustomRelationExpr['customRelation'], 'relation' | 'navigateTo'>
510
+ }
511
+ export function $include(expr: Expression | C4Expression, props?: CustomProps): ViewRulePredicate {
512
+ let _expr = props?.where ? $where(expr, props.where) : $expr(expr)
513
+ if (props?.with) {
514
+ if (isRelationExpression(_expr) || isRelationWhere(_expr)) {
515
+ _expr = {
516
+ customRelation: {
517
+ relation: _expr,
518
+ ...props.with as any
519
+ }
520
+ }
521
+ } else if (isElementRef(_expr) || isElementWhere(_expr)) {
522
+ _expr = {
523
+ custom: {
524
+ expr: _expr,
525
+ ...props.with as any
526
+ }
527
+ }
528
+ }
529
+ }
469
530
  return {
470
- include: [$expr(expr)]
531
+ include: [_expr]
471
532
  }
472
533
  }
473
534
  export function $exclude(expr: Expression | C4Expression): ViewRulePredicate {
@@ -475,6 +536,12 @@ export function $exclude(expr: Expression | C4Expression): ViewRulePredicate {
475
536
  exclude: [$expr(expr)]
476
537
  }
477
538
  }
539
+ export function $group(groupRules: ViewRuleGroup['groupRules']): ViewRuleGroup {
540
+ return {
541
+ title: null,
542
+ groupRules
543
+ }
544
+ }
478
545
 
479
546
  export function $style(element: ElementRefExpr, style: ViewRuleStyle['style']): ViewRuleStyle {
480
547
  return {