@likec4/language-server 1.14.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 (43) 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.CITj0XN3.cjs → language-server.BUtiWTKg.cjs} +188 -30
  18. package/dist/shared/{language-server.DFLaUdYu.mjs → language-server.DXC9g4_f.mjs} +274 -66
  19. package/dist/shared/{language-server.CO6aEDQm.d.mts → language-server.DfMwkd2l.d.mts} +46 -15
  20. package/dist/shared/{language-server.Cqyh6-9S.d.cts → language-server.U2piOAVt.d.cts} +46 -15
  21. package/dist/shared/{language-server.BI99piRy.d.ts → language-server.j-ShR6as.d.ts} +46 -15
  22. package/dist/shared/{language-server.DZYziEuF.mjs → language-server.zY53FGJE.mjs} +189 -31
  23. package/package.json +9 -9
  24. package/src/ast.ts +1 -0
  25. package/src/generated/ast.ts +103 -19
  26. package/src/generated/grammar.ts +1 -1
  27. package/src/generated-lib/icons.ts +0 -1
  28. package/src/like-c4.langium +32 -9
  29. package/src/lsp/CompletionProvider.ts +70 -2
  30. package/src/model/model-builder.ts +0 -1
  31. package/src/model/model-parser.ts +71 -20
  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 +3 -2
  42. package/src/validation/index.ts +2 -2
  43. 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),
@@ -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 {
@@ -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,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 {