@likec4/language-server 1.13.0 → 1.15.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 (48) 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.80ITEDo5.cjs} +272 -64
  17. package/dist/shared/{language-server.C2ebP2pZ.cjs → language-server.BUtiWTKg.cjs} +383 -32
  18. package/dist/shared/{language-server.DFLaUdYu.mjs → language-server.DXC9g4_f.mjs} +274 -66
  19. package/dist/shared/{language-server.ryB8CivX.d.mts → language-server.DfMwkd2l.d.mts} +81 -15
  20. package/dist/shared/{language-server.Cnq_hgfm.d.cts → language-server.U2piOAVt.d.cts} +81 -15
  21. package/dist/shared/{language-server.eY70DuKx.d.ts → language-server.j-ShR6as.d.ts} +81 -15
  22. package/dist/shared/{language-server.CrE0nFSB.mjs → language-server.zY53FGJE.mjs} +385 -34
  23. package/package.json +12 -12
  24. package/src/ast.ts +11 -0
  25. package/src/generated/ast.ts +177 -17
  26. package/src/generated/grammar.ts +1 -1
  27. package/src/generated-lib/icons.ts +0 -1
  28. package/src/like-c4.langium +50 -4
  29. package/src/lsp/CompletionProvider.ts +70 -2
  30. package/src/model/model-builder.ts +25 -3
  31. package/src/model/model-parser.ts +150 -33
  32. package/src/model-graph/compute-view/__test__/fixture.ts +45 -4
  33. package/src/model-graph/compute-view/compute.ts +223 -40
  34. package/src/model-graph/compute-view/predicates.ts +12 -6
  35. package/src/model-graph/utils/applyCustomElementProperties.ts +18 -4
  36. package/src/model-graph/utils/applyCustomRelationProperties.ts +2 -1
  37. package/src/model-graph/utils/applyViewRuleStyles.ts +30 -25
  38. package/src/model-graph/utils/buildComputeNodes.ts +69 -17
  39. package/src/model-graph/utils/elementExpressionToPredicate.ts +3 -1
  40. package/src/model-graph/utils/sortNodes.ts +11 -7
  41. package/src/references/scope-computation.ts +24 -1
  42. package/src/validation/index.ts +4 -0
  43. package/src/validation/specification.ts +30 -0
  44. package/src/view-utils/index.ts +1 -0
  45. package/src/view-utils/resolve-global-rules.ts +72 -0
  46. package/dist/shared/language-server.B-9_mDoo.d.mts +0 -1238
  47. package/dist/shared/language-server.C3oS5yhF.d.cts +0 -1238
  48. package/dist/shared/language-server.r5AXAWzc.d.ts +0 -1238
@@ -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
@@ -5,6 +5,7 @@ entry LikeC4Grammar:
5
5
  specifications+=SpecificationRule |
6
6
  models+=Model |
7
7
  views+=ModelViews |
8
+ globals+=Globals |
8
9
  likec4lib+=LikeC4Lib
9
10
  )*
10
11
  ;
@@ -196,7 +197,7 @@ MetadataAttribute:
196
197
  ModelViews:
197
198
  name='views' '{' (
198
199
  views+=LikeC4ViewRule |
199
- styles+=ViewRuleStyle
200
+ styles+=ViewRuleStyleOrGlobalRef
200
201
  )*
201
202
  '}';
202
203
 
@@ -266,13 +267,27 @@ ViewLayoutDirection returns string:
266
267
 
267
268
  ViewRule:
268
269
  ViewRulePredicate |
269
- ViewRuleStyle |
270
+ ViewRuleGroup |
271
+ ViewRuleStyleOrGlobalRef |
270
272
  ViewRuleAutoLayout
271
273
  ;
272
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
+
273
288
  DynamicViewRule:
274
289
  DynamicViewIncludePredicate |
275
- ViewRuleStyle |
290
+ ViewRuleStyleOrGlobalRef |
276
291
  ViewRuleAutoLayout
277
292
  ;
278
293
 
@@ -440,6 +455,12 @@ ViewRuleStyle:
440
455
  )*
441
456
  '}';
442
457
 
458
+ ViewRuleGlobalStyle:
459
+ 'global' 'style' style=[GlobalStyleId];
460
+
461
+ ViewRuleStyleOrGlobalRef:
462
+ ViewRuleStyle | ViewRuleGlobalStyle;
463
+
443
464
  ViewRuleAutoLayout:
444
465
  'autoLayout' direction=ViewLayoutDirection (
445
466
  rankSep=Number (
@@ -480,6 +501,31 @@ NavigateToProperty:
480
501
  RelationNavigateToProperty:
481
502
  key='navigateTo' value=DynamicViewRef;
482
503
 
504
+ // Global -------------------------------------
505
+
506
+ Globals:
507
+ name='global' '{'
508
+ (
509
+ styles+=(GlobalStyle | GlobalStyleGroup)*
510
+ )*
511
+ '}';
512
+
513
+ GlobalStyleId:
514
+ name=IdTerminal;
515
+
516
+ GlobalStyle:
517
+ 'style' id=GlobalStyleId target=ElementExpressionsIterator '{'
518
+ props+=(
519
+ StyleProperty |
520
+ NotationProperty
521
+ )*
522
+ '}';
523
+
524
+ GlobalStyleGroup:
525
+ 'styleGroup' id=GlobalStyleId '{'
526
+ styles+=ViewRuleStyle*
527
+ '}';
528
+
483
529
  // Common properties -------------------------------------
484
530
 
485
531
  LinkProperty:
@@ -570,7 +616,7 @@ CustomColorId returns string:
570
616
  IdTerminal | ElementShape | ArrowType | LineOptions | 'element' | 'model';
571
617
 
572
618
  Id returns string:
573
- IdTerminal | ElementShape | ThemeColor | ArrowType | LineOptions | 'element' | 'model';
619
+ IdTerminal | ElementShape | ThemeColor | ArrowType | LineOptions | 'element' | 'model' | 'group';
574
620
 
575
621
  fragment EqOperator:
576
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),
@@ -13,6 +13,7 @@ import { deepEqual as eq } from 'fast-equals'
13
13
  import type { Cancellation, LangiumDocument, LangiumDocuments, URI, WorkspaceCache } from 'langium'
14
14
  import { Disposable, DocumentState, interruptAndCheck } from 'langium'
15
15
  import {
16
+ entries,
16
17
  filter,
17
18
  flatMap,
18
19
  forEach,
@@ -45,8 +46,7 @@ import { isParsedLikeC4LangiumDocument } from '../ast'
45
46
  import { logError, logger, logWarnError } from '../logger'
46
47
  import { computeDynamicView, computeView, LikeC4ModelGraph } from '../model-graph'
47
48
  import type { LikeC4Services } from '../module'
48
- import { printDocs } from '../utils/printDocs'
49
- import { assignNavigateTo, resolveRelativePaths, resolveRulesExtendedViews } from '../view-utils'
49
+ import { assignNavigateTo, resolveGlobalRules, resolveRelativePaths, resolveRulesExtendedViews } from '../view-utils'
50
50
 
51
51
  function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[]): c4.ParsedLikeC4Model {
52
52
  const c4Specification: ParsedAstSpecification = {
@@ -211,6 +211,23 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
211
211
  indexBy(prop('id'))
212
212
  )
213
213
 
214
+ const parsedGlobals: {
215
+ styles: Record<c4.GlobalStyleID, c4.ViewRuleStyle[]>
216
+ } = {
217
+ styles: {}
218
+ }
219
+ Object.assign(parsedGlobals.styles, ...docs.map(d => d.c4Globals.styles))
220
+
221
+ const globals: {
222
+ styles: Record<c4.GlobalStyleID, c4.GlobalStyle>
223
+ } = {
224
+ styles: pipe(
225
+ entries(parsedGlobals.styles),
226
+ map(([id, styles]) => ({ id, styles })),
227
+ indexBy(prop('id'))
228
+ )
229
+ }
230
+
214
231
  function toC4View(doc: LangiumDocument) {
215
232
  const docUri = doc.uri.toString()
216
233
  return (parsedAstView: ParsedAstView): c4.LikeC4View => {
@@ -287,6 +304,7 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
287
304
  },
288
305
  elements,
289
306
  relations,
307
+ globals,
290
308
  views
291
309
  }
292
310
  }
@@ -396,6 +414,7 @@ export class LikeC4ModelBuilder {
396
414
  specification: model.specification,
397
415
  elements: model.elements,
398
416
  relations: model.relations,
417
+ globals: model.globals,
399
418
  views
400
419
  }
401
420
  })
@@ -441,7 +460,10 @@ export class LikeC4ModelBuilder {
441
460
  return null
442
461
  }
443
462
  const index = new LikeC4ModelGraph(model)
444
- const result = isElementView(view) ? computeView(view, index) : computeDynamicView(view, index)
463
+ const resolvedView = resolveGlobalRules(view, model.globals.styles)
464
+ const result = isElementView(resolvedView)
465
+ ? computeView(resolvedView, index)
466
+ : computeDynamicView(resolvedView, index)
445
467
  if (!result.isSuccess) {
446
468
  logError(result.error)
447
469
  return null
@@ -30,15 +30,13 @@ import {
30
30
  ViewOps
31
31
  } from '../ast'
32
32
  import { elementRef, getFqnElementRef } from '../elementRef'
33
+ import { isGlobalStyle, isGlobalStyleGroup, type NotationProperty } from '../generated/ast'
33
34
  import { logError, logger, logWarnError } from '../logger'
34
35
  import type { LikeC4Services } from '../module'
35
36
  import { stringHash } from '../utils'
36
37
  import { deserializeFromComment, hasManualLayout } from '../view-utils/manual-layout'
37
38
  import type { FqnIndex } from './fqn-index'
38
39
  import { parseWhereClause } from './model-parser-where'
39
- import {
40
- type NotationProperty
41
- } from '../generated/ast'
42
40
 
43
41
  const { getDocument } = AstUtils
44
42
 
@@ -83,6 +81,7 @@ export class LikeC4ModelParser {
83
81
  const { isValid } = checksFromDiagnostics(doc)
84
82
  this.parseSpecification(doc, isValid)
85
83
  this.parseModel(doc, isValid)
84
+ this.parseGlobal(doc, isValid)
86
85
  this.parseViews(doc, isValid)
87
86
  return doc
88
87
  }
@@ -285,11 +284,57 @@ export class LikeC4ModelParser {
285
284
  }
286
285
  }
287
286
 
287
+ private parseGlobal(doc: ParsedLikeC4LangiumDocument, isValid: IsValidFn) {
288
+ const { parseResult, c4Globals } = doc
289
+
290
+ const globals = parseResult.value.globals.filter(isValid)
291
+ const styles = globals.flatMap(r => r.styles.filter(isValid))
292
+ for (const style of styles) {
293
+ try {
294
+ const globalStyleId = style.id.name as c4.GlobalStyleID
295
+ if (!isTruthy(globalStyleId)) {
296
+ continue
297
+ }
298
+ if (globalStyleId in c4Globals.styles) {
299
+ logger.warn(`Global style named "${globalStyleId}" is already defined`)
300
+ continue
301
+ }
302
+
303
+ const styles = this.parseGlobalStyleOrGroup(style, isValid)
304
+ if (styles.length > 0) {
305
+ c4Globals.styles[globalStyleId] = styles as c4.NonEmptyArray<c4.ViewRuleStyle>
306
+ }
307
+ } catch (e) {
308
+ logWarnError(e)
309
+ }
310
+ }
311
+ }
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
+
326
+ private parseGlobalStyle(astRule: ast.GlobalStyle, isValid: IsValidFn): c4.ViewRuleStyle {
327
+ const styleProps = astRule.props.filter(ast.isStyleProperty)
328
+ const targets = astRule.target
329
+ const notation = astRule.props.find(ast.isNotationProperty)
330
+ return this.parseRuleStyle(styleProps, targets, isValid, notation)
331
+ }
332
+
288
333
  private parseViews(doc: ParsedLikeC4LangiumDocument, isValid: IsValidFn) {
289
334
  const viewBlocks = doc.parseResult.value.views.filter(v => isValid(v))
290
335
  for (const viewBlock of viewBlocks) {
291
336
  const localStyles = viewBlock.styles
292
- .flatMap(s => this.parseViewRuleStyle(s, isValid))
337
+ .flatMap(s => this.parseViewRuleStyleOrGlobalRef(s, isValid))
293
338
  const stylesToApply = localStyles
294
339
 
295
340
  for (const view of viewBlock.views) {
@@ -298,7 +343,9 @@ export class LikeC4ModelParser {
298
343
  continue
299
344
  }
300
345
  doc.c4Views.push(
301
- ast.isElementView(view) ? this.parseElementView(view, stylesToApply, isValid) : this.parseDynamicElementView(view, stylesToApply, isValid)
346
+ ast.isElementView(view)
347
+ ? this.parseElementView(view, stylesToApply, isValid)
348
+ : this.parseDynamicElementView(view, stylesToApply, isValid)
302
349
  )
303
350
  } catch (e) {
304
351
  logWarnError(e)
@@ -498,8 +545,11 @@ export class LikeC4ModelParser {
498
545
 
499
546
  private parseRelationPredicate(astNode: ast.RelationPredicate, _isValid: IsValidFn): c4.RelationPredicateExpression {
500
547
  if (ast.isRelationPredicateWith(astNode)) {
501
- const subject = ast.isRelationPredicateWhere(astNode.subject) ? astNode.subject.subject : astNode.subject
502
- 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)
503
553
  }
504
554
  if (ast.isRelationPredicateWhere(astNode)) {
505
555
  return this.parseRelationPredicateWhere(astNode)
@@ -526,9 +576,8 @@ export class LikeC4ModelParser {
526
576
 
527
577
  private parseRelationPredicateWith(
528
578
  astNode: ast.RelationPredicateWith,
529
- subject: ast.RelationExpression
579
+ relation: c4.RelationExpression | c4.RelationWhereExpr
530
580
  ): c4.CustomRelationExpr {
531
- const relation = this.parseRelationExpr(subject)
532
581
  const props = astNode.custom?.props ?? []
533
582
  return props.reduce(
534
583
  (acc, prop) => {
@@ -604,12 +653,25 @@ export class LikeC4ModelParser {
604
653
  if (ast.isViewRulePredicate(astRule)) {
605
654
  return this.parseViewRulePredicate(astRule, isValid)
606
655
  }
607
- if (ast.isViewRuleStyle(astRule)) {
608
- return this.parseViewRuleStyle(astRule, isValid)
656
+ if (ast.isViewRuleStyleOrGlobalRef(astRule)) {
657
+ return this.parseViewRuleStyleOrGlobalRef(astRule, isValid)
609
658
  }
610
659
  if (ast.isViewRuleAutoLayout(astRule)) {
611
660
  return toAutoLayout(astRule)
612
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
+ }
672
+ if (ast.isViewRuleGlobalStyle(astRule)) {
673
+ return this.parseViewRuleGlobalStyle(astRule, isValid)
674
+ }
613
675
  nonexhaustive(astRule)
614
676
  }
615
677
 
@@ -620,7 +682,39 @@ export class LikeC4ModelParser {
620
682
  return this.parseRuleStyle(styleProps, targets, isValid, notation)
621
683
  }
622
684
 
623
- private parseRuleStyle(styleProperties: ast.StyleProperty[], elementExpressionsIterator: ast.ElementExpressionsIterator, isValid: IsValidFn, notationProperty?: NotationProperty): c4.ViewRuleStyle {
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
+
712
+ private parseRuleStyle(
713
+ styleProperties: ast.StyleProperty[],
714
+ elementExpressionsIterator: ast.ElementExpressionsIterator,
715
+ isValid: IsValidFn,
716
+ notationProperty?: NotationProperty
717
+ ): c4.ViewRuleStyle {
624
718
  const styleProps = toElementStyle(styleProperties, isValid)
625
719
  const notation = removeIndent(notationProperty?.value)
626
720
  const targets = this.parseElementExpressionsIterator(elementExpressionsIterator)
@@ -633,6 +727,12 @@ export class LikeC4ModelParser {
633
727
  }
634
728
  }
635
729
 
730
+ private parseViewRuleGlobalStyle(astRule: ast.ViewRuleGlobalStyle, _isValid: IsValidFn): c4.ViewRuleGlobalStyle {
731
+ return {
732
+ styleId: astRule.style.$refText as c4.GlobalStyleID
733
+ }
734
+ }
735
+
636
736
  private parseViewManualLaout(node: ast.DynamicView | ast.ElementView): c4.ViewManualLayout | undefined {
637
737
  const commentNode = CstUtils.findCommentNode(node.$cstNode, ['BLOCK_COMMENT'])
638
738
  if (!commentNode || !hasManualLayout(commentNode.text)) {
@@ -729,7 +829,11 @@ export class LikeC4ModelParser {
729
829
  return step
730
830
  }
731
831
 
732
- private parseElementView(astNode: ast.ElementView, additionalStyles: c4.ViewRuleStyle[], isValid: IsValidFn): ParsedAstElementView {
832
+ private parseElementView(
833
+ astNode: ast.ElementView,
834
+ additionalStyles: c4.ViewRuleStyleOrGlobalRef[],
835
+ isValid: IsValidFn
836
+ ): ParsedAstElementView {
733
837
  const body = astNode.body
734
838
  invariant(body, 'ElementView body is not defined')
735
839
  const astPath = this.getAstNodePath(astNode)
@@ -770,14 +874,17 @@ export class LikeC4ModelParser {
770
874
  description,
771
875
  tags,
772
876
  links: isNonEmptyArray(links) ? links : null,
773
- rules: [...additionalStyles, ...body.rules.flatMap(n => {
774
- try {
775
- return isValid(n) ? this.parseViewRule(n, isValid) : []
776
- } catch (e) {
777
- logWarnError(e)
778
- return []
779
- }
780
- })],
877
+ rules: [
878
+ ...additionalStyles,
879
+ ...body.rules.flatMap(n => {
880
+ try {
881
+ return isValid(n) ? this.parseViewRule(n, isValid) : []
882
+ } catch (e) {
883
+ logWarnError(e)
884
+ return []
885
+ }
886
+ })
887
+ ],
781
888
  ...(viewOf && { viewOf }),
782
889
  ...(manualLayout && { manualLayout })
783
890
  }
@@ -794,7 +901,11 @@ export class LikeC4ModelParser {
794
901
  return view
795
902
  }
796
903
 
797
- private parseDynamicElementView(astNode: ast.DynamicView, additionalStyles: c4.ViewRuleStyle[], isValid: IsValidFn): ParsedAstDynamicView {
904
+ private parseDynamicElementView(
905
+ astNode: ast.DynamicView,
906
+ additionalStyles: c4.ViewRuleStyleOrGlobalRef[],
907
+ isValid: IsValidFn
908
+ ): ParsedAstDynamicView {
798
909
  const body = astNode.body
799
910
  invariant(body, 'DynamicElementView body is not defined')
800
911
  // only valid props
@@ -827,14 +938,17 @@ export class LikeC4ModelParser {
827
938
  description,
828
939
  tags,
829
940
  links: isNonEmptyArray(links) ? links : null,
830
- rules: [...additionalStyles, ...body.rules.flatMap(n => {
831
- try {
832
- return isValid(n) ? this.parseDynamicViewRule(n, isValid) : []
833
- } catch (e) {
834
- logWarnError(e)
835
- return []
836
- }
837
- }, [] as Array<c4.DynamicViewRule>)],
941
+ rules: [
942
+ ...additionalStyles,
943
+ ...body.rules.flatMap(n => {
944
+ try {
945
+ return isValid(n) ? this.parseDynamicViewRule(n, isValid) : []
946
+ } catch (e) {
947
+ logWarnError(e)
948
+ return []
949
+ }
950
+ }, [] as Array<c4.DynamicViewRule>)
951
+ ],
838
952
  steps: body.steps.reduce((acc, n) => {
839
953
  try {
840
954
  if (isValid(n)) {
@@ -857,8 +971,8 @@ export class LikeC4ModelParser {
857
971
  if (ast.isDynamicViewIncludePredicate(astRule)) {
858
972
  return this.parseDynamicViewIncludePredicate(astRule, isValid)
859
973
  }
860
- if (ast.isViewRuleStyle(astRule)) {
861
- return this.parseViewRuleStyle(astRule, isValid)
974
+ if (ast.isViewRuleStyleOrGlobalRef(astRule)) {
975
+ return this.parseViewRuleStyleOrGlobalRef(astRule, isValid)
862
976
  }
863
977
  if (ast.isViewRuleAutoLayout(astRule)) {
864
978
  return toAutoLayout(astRule)
@@ -866,7 +980,10 @@ export class LikeC4ModelParser {
866
980
  nonexhaustive(astRule)
867
981
  }
868
982
 
869
- private parseDynamicViewIncludePredicate(astRule: ast.DynamicViewIncludePredicate, isValid: IsValidFn): c4.DynamicViewIncludeRule {
983
+ private parseDynamicViewIncludePredicate(
984
+ astRule: ast.DynamicViewIncludePredicate,
985
+ isValid: IsValidFn
986
+ ): c4.DynamicViewIncludeRule {
870
987
  const include = [] as c4.ElementPredicateExpression[]
871
988
  let iter: ast.DynamicViewPredicateIterator | undefined = astRule.predicates
872
989
  while (iter) {
@@ -11,6 +11,10 @@ import {
11
11
  type ElementWhereExpr,
12
12
  type Expression as C4Expression,
13
13
  type Fqn,
14
+ isElementRef,
15
+ isElementWhere,
16
+ isRelationExpression,
17
+ isRelationWhere,
14
18
  type NonEmptyArray,
15
19
  type Relation,
16
20
  type RelationID,
@@ -20,6 +24,7 @@ import {
20
24
  type Tag,
21
25
  type ViewID,
22
26
  type ViewRule,
27
+ type ViewRuleGroup,
23
28
  type ViewRulePredicate,
24
29
  type ViewRuleStyle,
25
30
  type WhereOperator
@@ -362,8 +367,6 @@ export type Expression =
362
367
  | IncomingExpr
363
368
  | OutgoingExpr
364
369
  | RelationExpr
365
- | ElementWhereExpr
366
- | RelationWhereExpr
367
370
 
368
371
  export function $custom(
369
372
  expr: ElementRefExpr,
@@ -465,9 +468,41 @@ export function $expr(expr: Expression | C4Expression): C4Expression {
465
468
  }
466
469
  }
467
470
 
468
- export function $include(expr: Expression | C4Expression): ViewRulePredicate {
471
+ type CustomProps = {
472
+ where?: WhereOperator<TestTag, string>
473
+ with?: {
474
+ title?: string
475
+ description?: string
476
+ technology?: string
477
+ shape?: ElementShape
478
+ color?: Color
479
+ border?: BorderStyle
480
+ icon?: string
481
+ opacity?: number
482
+ navigateTo?: string
483
+ } & Omit<C4CustomRelationExpr['customRelation'], 'relation' | 'navigateTo'>
484
+ }
485
+ export function $include(expr: Expression | C4Expression, props?: CustomProps): ViewRulePredicate {
486
+ let _expr = props?.where ? $where(expr, props.where) : $expr(expr)
487
+ if (props?.with) {
488
+ if (isRelationExpression(_expr) || isRelationWhere(_expr)) {
489
+ _expr = {
490
+ customRelation: {
491
+ relation: _expr,
492
+ ...props.with as any
493
+ }
494
+ }
495
+ } else if (isElementRef(_expr) || isElementWhere(_expr)) {
496
+ _expr = {
497
+ custom: {
498
+ expr: _expr,
499
+ ...props.with as any
500
+ }
501
+ }
502
+ }
503
+ }
469
504
  return {
470
- include: [$expr(expr)]
505
+ include: [_expr]
471
506
  }
472
507
  }
473
508
  export function $exclude(expr: Expression | C4Expression): ViewRulePredicate {
@@ -475,6 +510,12 @@ export function $exclude(expr: Expression | C4Expression): ViewRulePredicate {
475
510
  exclude: [$expr(expr)]
476
511
  }
477
512
  }
513
+ export function $group(groupRules: ViewRuleGroup['groupRules']): ViewRuleGroup {
514
+ return {
515
+ title: null,
516
+ groupRules
517
+ }
518
+ }
478
519
 
479
520
  export function $style(element: ElementRefExpr, style: ViewRuleStyle['style']): ViewRuleStyle {
480
521
  return {