@likec4/language-server 1.3.0 → 1.5.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 (34) hide show
  1. package/README.md +1 -1
  2. package/contrib/likec4.tmLanguage.json +1 -1
  3. package/package.json +13 -13
  4. package/src/Rpc.ts +23 -1
  5. package/src/ast.ts +11 -12
  6. package/src/generated/ast.ts +101 -31
  7. package/src/generated/grammar.ts +1 -1
  8. package/src/like-c4.langium +58 -33
  9. package/src/lsp/SemanticTokenProvider.ts +12 -14
  10. package/src/model/fqn-computation.ts +29 -7
  11. package/src/model/fqn-index.ts +18 -31
  12. package/src/model/model-builder.ts +13 -9
  13. package/src/model/model-locator.ts +7 -7
  14. package/src/model/model-parser.ts +63 -18
  15. package/src/model-change/changeElementStyle.ts +2 -2
  16. package/src/model-graph/compute-view/__test__/fixture.ts +51 -23
  17. package/src/model-graph/compute-view/compute.ts +47 -16
  18. package/src/model-graph/compute-view/predicates.ts +6 -1
  19. package/src/model-graph/dynamic-view/compute.ts +2 -2
  20. package/src/model-graph/utils/{applyElementCustomProperties.ts → applyCustomElementProperties.ts} +5 -3
  21. package/src/model-graph/utils/applyCustomRelationProperties.ts +50 -0
  22. package/src/model-graph/utils/applyViewRuleStyles.ts +11 -34
  23. package/src/model-graph/utils/elementExpressionToPredicate.ts +32 -0
  24. package/src/references/scope-provider.ts +3 -23
  25. package/src/validation/dynamic-view-rule.ts +5 -7
  26. package/src/validation/element.ts +8 -4
  27. package/src/validation/index.ts +2 -0
  28. package/src/validation/view-predicates/custom-element-expr.ts +17 -6
  29. package/src/validation/view-predicates/custom-relation-expr.ts +15 -0
  30. package/src/validation/view-predicates/index.ts +1 -0
  31. package/src/view-utils/assignNavigateTo.ts +2 -2
  32. package/src/view-utils/manual-layout.ts +4 -2
  33. package/src/view-utils/resolve-extended-views.ts +2 -2
  34. package/src/view-utils/resolve-relative-paths.ts +3 -3
@@ -23,16 +23,15 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
23
23
  type: SemanticTokenTypes.function
24
24
  })
25
25
  }
26
- if (ast.isElementViewRef(node)) {
27
- return acceptor({
26
+ if (ast.isNavigateToProperty(node)) {
27
+ acceptor({
28
28
  node,
29
- property: 'view',
30
- type: SemanticTokenTypes.variable
29
+ property: 'key',
30
+ type: SemanticTokenTypes.property
31
31
  })
32
- }
33
- if (ast.isDescedantsExpr(node) && node.$cstNode) {
34
32
  acceptor({
35
- cst: node.$cstNode,
33
+ node,
34
+ property: 'value',
36
35
  type: SemanticTokenTypes.variable,
37
36
  modifier: [
38
37
  SemanticTokenModifiers.definition,
@@ -41,7 +40,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
41
40
  })
42
41
  return 'prune'
43
42
  }
44
- if (ast.isWildcardExpr(node) && node.$cstNode) {
43
+ if ((ast.isDescedantsExpr(node) || ast.isWildcardExpr(node)) && node.$cstNode) {
45
44
  acceptor({
46
45
  cst: node.$cstNode,
47
46
  type: SemanticTokenTypes.variable,
@@ -52,7 +51,6 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
52
51
  })
53
52
  return 'prune'
54
53
  }
55
-
56
54
  if (ast.isElementKindExpr(node)) {
57
55
  acceptor({
58
56
  node,
@@ -98,6 +96,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
98
96
  property: 'value',
99
97
  type: SemanticTokenTypes.interface
100
98
  })
99
+ return
101
100
  }
102
101
  if (ast.isTag(node)) {
103
102
  return acceptor({
@@ -134,6 +133,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
134
133
  property: 'value',
135
134
  type: SemanticTokenTypes.enum
136
135
  })
136
+ return 'prune'
137
137
  }
138
138
  if (ast.isOpacityProperty(node)) {
139
139
  acceptor({
@@ -146,14 +146,12 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
146
146
  property: 'value',
147
147
  type: SemanticTokenTypes.number
148
148
  })
149
- return
149
+ return 'prune'
150
150
  }
151
151
  if (
152
152
  ast.isLinkProperty(node)
153
153
  || ast.isIconProperty(node)
154
- || ast.isElementStringProperty(node)
155
- || ast.isRelationStringProperty(node)
156
- || ast.isViewStringProperty(node)
154
+ || ast.isStringProperty(node)
157
155
  ) {
158
156
  acceptor({
159
157
  node,
@@ -165,7 +163,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
165
163
  property: 'value',
166
164
  type: SemanticTokenTypes.string
167
165
  })
168
- return
166
+ return 'prune'
169
167
  }
170
168
  if (ast.isElement(node)) {
171
169
  return this.highlightAstElement(node, acceptor)
@@ -1,14 +1,38 @@
1
1
  import { AsFqn, type c4, nonexhaustive } from '@likec4/core'
2
- import { MultiMap } from 'langium'
2
+ import { type AstNodeDescription, type AstNodeLocator, AstUtils, CstUtils, GrammarUtils, MultiMap } from 'langium'
3
3
  import { isEmpty, isNullish as isNil } from 'remeda'
4
4
  import { ast, ElementOps, type LikeC4LangiumDocument } from '../ast'
5
5
  import { getFqnElementRef } from '../elementRef'
6
6
  import type { LikeC4Services } from '../module'
7
7
 
8
+ const { findNodeForProperty } = GrammarUtils
9
+ const { toDocumentSegment } = CstUtils
10
+ const { getDocument } = AstUtils
11
+
8
12
  type TraversePair = [el: ast.Element | ast.ExtendElement | ast.Relation, parent: c4.Fqn | null]
9
13
 
14
+ function toAstNodeDescription(
15
+ locator: AstNodeLocator,
16
+ entry: ast.Element,
17
+ doc: LikeC4LangiumDocument
18
+ ): AstNodeDescription {
19
+ const $cstNode = findNodeForProperty(entry.$cstNode, 'name')
20
+ return {
21
+ documentUri: doc.uri,
22
+ name: entry.name,
23
+ ...(entry.$cstNode && {
24
+ selectionSegment: toDocumentSegment(entry.$cstNode)
25
+ }),
26
+ ...($cstNode && {
27
+ nameSegment: toDocumentSegment($cstNode)
28
+ }),
29
+ path: locator.getAstNodePath(entry),
30
+ type: ast.Element
31
+ }
32
+ }
33
+
10
34
  export function computeDocumentFqn(document: LikeC4LangiumDocument, services: LikeC4Services) {
11
- const c4fqns = (document.c4fqns = new MultiMap())
35
+ const c4fqnIndex = (document.c4fqnIndex = new MultiMap())
12
36
  const elements = document.parseResult.value.models.flatMap(m => m.elements)
13
37
  if (elements.length === 0) {
14
38
  return
@@ -30,11 +54,9 @@ export function computeDocumentFqn(document: LikeC4LangiumDocument, services: Li
30
54
  }
31
55
  if (ast.isElement(el)) {
32
56
  const fqn = AsFqn(el.name, parent)
33
- const path = locator.getAstNodePath(el)
34
- c4fqns.add(fqn, {
35
- el: new WeakRef(el),
36
- path,
37
- name: el.name
57
+ c4fqnIndex.add(fqn, {
58
+ ...toAstNodeDescription(locator, el, document),
59
+ fqn
38
60
  })
39
61
  ElementOps.writeId(el, fqn)
40
62
  if (!isNil(el.body) && !isEmpty(el.body.elements)) {
@@ -1,8 +1,8 @@
1
1
  import type { Fqn } from '@likec4/core'
2
2
  import { nameFromFqn, parentFqn } from '@likec4/core'
3
- import type { LangiumDocuments, Stream } from 'langium'
3
+ import type { AstNodeDescription, LangiumDocuments, Stream } from 'langium'
4
4
  import { DocumentState, DONE_RESULT, MultiMap, stream, StreamImpl } from 'langium'
5
- import type { ast, FqnIndexedDocument } from '../ast'
5
+ import type { ast, DocFqnIndexAstNodeDescription, FqnIndexedDocument } from '../ast'
6
6
  import { ElementOps, isFqnIndexedDocument, isLikeC4LangiumDocument } from '../ast'
7
7
  import { logError, logger } from '../logger'
8
8
  import type { LikeC4Services } from '../module'
@@ -17,8 +17,6 @@ export interface FqnIndexEntry {
17
17
  path: string
18
18
  }
19
19
 
20
- const True = () => true
21
-
22
20
  export class FqnIndex {
23
21
  protected langiumDocuments: LangiumDocuments
24
22
 
@@ -31,7 +29,7 @@ export class FqnIndex {
31
29
  logger.debug(`[FqnIndex] onIndexedContent ${docs.length}:\n` + printDocs(docs))
32
30
  for (const doc of docs) {
33
31
  if (isLikeC4LangiumDocument(doc)) {
34
- delete doc.c4fqns
32
+ delete doc.c4fqnIndex
35
33
  delete doc.c4Elements
36
34
  delete doc.c4Specification
37
35
  delete doc.c4Relations
@@ -53,18 +51,13 @@ export class FqnIndex {
53
51
  return this.langiumDocuments.all.filter(isFqnIndexedDocument)
54
52
  }
55
53
 
56
- private entries(filterByFqn: (fqn: Fqn) => boolean = True): Stream<FqnIndexEntry> {
57
- return this.documents.flatMap(doc =>
58
- doc.c4fqns.entries().flatMap(([fqn, entry]): FqnIndexEntry | FqnIndexEntry[] => {
59
- if (filterByFqn(fqn)) {
60
- const el = entry.el.deref()
61
- if (el) {
62
- return { ...entry, fqn, el, doc }
63
- }
64
- }
65
- return []
66
- })
67
- )
54
+ private entries(filterByFqn?: (fqn: Fqn) => boolean): Stream<DocFqnIndexAstNodeDescription> {
55
+ return this.documents.flatMap(doc => {
56
+ if (filterByFqn) {
57
+ return doc.c4fqnIndex.keys().filter(filterByFqn).flatMap(fqn => doc.c4fqnIndex.get(fqn))
58
+ }
59
+ return doc.c4fqnIndex.values()
60
+ })
68
61
  }
69
62
 
70
63
  public getFqn(el: ast.Element): Fqn | null {
@@ -83,22 +76,16 @@ export class FqnIndex {
83
76
  // return fqn
84
77
  }
85
78
 
86
- public byFqn(fqn: Fqn): Stream<FqnIndexEntry> {
79
+ public byFqn(fqn: Fqn): Stream<AstNodeDescription> {
87
80
  return this.documents.flatMap(doc => {
88
- return doc.c4fqns.get(fqn).flatMap(entry => {
89
- const el = entry.el.deref()
90
- if (el) {
91
- return { fqn, el, doc, path: entry.path, name: entry.name }
92
- }
93
- return []
94
- })
81
+ return doc.c4fqnIndex.get(fqn)
95
82
  })
96
83
  }
97
84
 
98
- public directChildrenOf(parent: Fqn): Stream<FqnIndexEntry> {
85
+ public directChildrenOf(parent: Fqn): Stream<AstNodeDescription> {
99
86
  return stream([parent]).flatMap(_parent => {
100
87
  const children = this.entries(fqn => parentFqn(fqn) === _parent)
101
- .map((entry): [string, FqnIndexEntry] => [entry.name, entry])
88
+ .map((entry) => [entry.name, entry] as [string, AstNodeDescription])
102
89
  .toArray()
103
90
  if (children.length === 0) {
104
91
  return []
@@ -113,15 +100,15 @@ export class FqnIndex {
113
100
  /**
114
101
  * Returns descedant elements with unique names in the scope
115
102
  */
116
- public uniqueDescedants(parent: Fqn): Stream<FqnIndexEntry> {
103
+ public uniqueDescedants(parent: Fqn): Stream<AstNodeDescription> {
117
104
  return new StreamImpl(
118
105
  () => {
119
106
  const prefix = `${parent}.`
120
107
 
121
108
  const childrenNames = new Set<string>()
122
- const descedants = [] as FqnIndexEntry[]
109
+ const descedants = [] as AstNodeDescription[]
123
110
 
124
- const nested = new MultiMap<string, FqnIndexEntry>()
111
+ const nested = new MultiMap<string, AstNodeDescription>()
125
112
 
126
113
  this.entries(f => f.startsWith(prefix)).forEach(e => {
127
114
  const name = nameFromFqn(e.fqn)
@@ -154,7 +141,7 @@ export class FqnIndex {
154
141
  if (iterator) {
155
142
  return iterator.next()
156
143
  }
157
- return DONE_RESULT as IteratorResult<FqnIndexEntry>
144
+ return DONE_RESULT as IteratorResult<AstNodeDescription>
158
145
  }
159
146
  )
160
147
  }
@@ -2,9 +2,9 @@ import {
2
2
  type c4,
3
3
  compareByFqnHierarchically,
4
4
  isElementView,
5
- isStrictElementView,
5
+ isScopedElementView,
6
6
  parentFqn,
7
- type StrictElementView,
7
+ type ScopedElementView,
8
8
  type ViewID
9
9
  } from '@likec4/core'
10
10
  import { deepEqual as eq } from 'fast-equals'
@@ -26,7 +26,7 @@ import {
26
26
  sort,
27
27
  values
28
28
  } from 'remeda'
29
- import { type CancellationToken, Disposable } from 'vscode-languageserver'
29
+ import { type CancellationToken, Disposable } from 'vscode-languageserver-protocol'
30
30
  import type {
31
31
  ParsedAstElement,
32
32
  ParsedAstRelation,
@@ -107,7 +107,7 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
107
107
 
108
108
  const elements = pipe(
109
109
  docs,
110
- flatMap(d => d.c4Elements.map(toModelElement(d))),
110
+ flatMap(d => map(d.c4Elements, toModelElement(d))),
111
111
  filter(isTruthy),
112
112
  sort(compareByFqnHierarchically),
113
113
  reduce(
@@ -140,10 +140,13 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
140
140
  ...model
141
141
  }: ParsedAstRelation): c4.Relation | null => {
142
142
  if (isNullish(elements[source]) || isNullish(elements[target])) {
143
+ logger.warn(
144
+ `Invalid relation ${id}, source: ${source}(${!!elements[source]}), target: ${target}(${!!elements[target]})`
145
+ )
143
146
  return null
144
147
  }
145
148
 
146
- if (!!kind && kind in c4Specification.relationships) {
149
+ if (!isNullish(kind) && kind in c4Specification.relationships) {
147
150
  return {
148
151
  ...(links && { links: resolveLinks(doc, links) }),
149
152
  ...c4Specification.relationships[kind],
@@ -165,14 +168,15 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
165
168
  }
166
169
 
167
170
  const relations = pipe(
168
- flatMap(docs, d => map(d.c4Relations, toModelRelation(d))),
171
+ docs,
172
+ flatMap(d => map(d.c4Relations, toModelRelation(d))),
169
173
  filter(isTruthy),
170
174
  mapToObj(r => [r.id, r])
171
175
  )
172
176
 
173
177
  const toC4View = (doc: LangiumDocument) => {
174
178
  const docUri = doc.uri.toString()
175
- return (parsedAstView: ParsedAstView): c4.View => {
179
+ return (parsedAstView: ParsedAstView): c4.LikeC4View => {
176
180
  let {
177
181
  id,
178
182
  title,
@@ -188,7 +192,7 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
188
192
  } = parsedAstView
189
193
 
190
194
  if (parsedAstView.__ === 'element' && isNullish(title) && 'viewOf' in parsedAstView) {
191
- title ??= elements[parsedAstView.viewOf]?.title ?? null
195
+ title = elements[parsedAstView.viewOf]?.title ?? null
192
196
  }
193
197
 
194
198
  if (isNullish(title) && id === 'index') {
@@ -359,7 +363,7 @@ export class LikeC4ModelBuilder {
359
363
  }
360
364
 
361
365
  const allElementViews = values(model.views).filter(
362
- (v): v is StrictElementView => isStrictElementView(v) && v.id !== viewId
366
+ (v): v is ScopedElementView => isScopedElementView(v) && v.id !== viewId
363
367
  )
364
368
 
365
369
  let computedView = result.view
@@ -33,18 +33,18 @@ export class LikeC4ModelLocator {
33
33
  return doc.c4Elements.find(e => e.id === fqn) ?? null
34
34
  }
35
35
 
36
- public locateElement(fqn: c4.Fqn, property = 'name'): Location | null {
36
+ public locateElement(fqn: c4.Fqn, _property = 'name'): Location | null {
37
37
  const entry = this.fqnIndex.byFqn(fqn).head()
38
38
  if (!entry) {
39
39
  return null
40
40
  }
41
- const propertyNode = findNodeForProperty(entry.el.$cstNode, property) ?? entry.el.$cstNode
42
- if (!propertyNode) {
43
- return null
44
- }
41
+ // const propertyNode = findNodeForProperty(entry.el.$cstNode, property) ?? entry.el.$cstNode
42
+ // if (!propertyNode) {
43
+ // return null
44
+ // }
45
45
  return {
46
- uri: entry.doc.uri.toString(),
47
- range: propertyNode.range
46
+ uri: entry.documentUri.toString(),
47
+ range: entry.nameSegment?.range!
48
48
  }
49
49
  }
50
50
 
@@ -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 { isTruthy } from 'remeda'
4
+ import { isTruthy, mapToObj } from 'remeda'
5
5
  import stripIndent from 'strip-indent'
6
6
  import type {
7
7
  ChecksFromDiagnostics,
@@ -37,14 +37,14 @@ const { getDocument } = AstUtils
37
37
  export type ModelParsedListener = () => void
38
38
 
39
39
  function toSingleLine<T extends string | undefined>(str: T): T {
40
- return (str ? removeIndent(str).split('\n').join(' ') : undefined) as T
40
+ return (isTruthy(str) ? removeIndent(str).split('\n').join(' ') : undefined) as T
41
41
  }
42
42
 
43
43
  function removeIndent<T extends string | undefined>(str: T): T {
44
- return (str ? stripIndent(str).trim() : undefined) as T
44
+ return (isTruthy(str) ? stripIndent(str).trim() : undefined) as T
45
45
  }
46
46
 
47
- type IsValidFn = ChecksFromDiagnostics['isValid']
47
+ export type IsValidFn = ChecksFromDiagnostics['isValid']
48
48
 
49
49
  export class LikeC4ModelParser {
50
50
  private fqnIndex: FqnIndex
@@ -142,11 +142,11 @@ export class LikeC4ModelParser {
142
142
 
143
143
  let [title, description, technology] = astNode.props ?? []
144
144
 
145
- const bodyProps = astNode.body?.props.filter(ast.isElementStringProperty) ?? []
145
+ const bodyProps = mapToObj(astNode.body?.props.filter(ast.isElementStringProperty) ?? [], p => [p.key, p.value])
146
146
 
147
- title = toSingleLine(title ?? bodyProps.find(p => p.key === 'title')?.value)
148
- description = removeIndent(description ?? bodyProps.find(p => p.key === 'description')?.value)
149
- technology = toSingleLine(technology ?? bodyProps.find(p => p.key === 'technology')?.value)
147
+ title = toSingleLine(title ?? bodyProps.title)
148
+ description = removeIndent(description ?? bodyProps.description)
149
+ technology = toSingleLine(technology ?? bodyProps.technology)
150
150
 
151
151
  const links = astNode.body?.props.filter(ast.isLinkProperty).map(p => p.value)
152
152
 
@@ -171,7 +171,7 @@ export class LikeC4ModelParser {
171
171
  const links = astNode.body?.props.filter(ast.isLinkProperty).map(p => p.value)
172
172
  const kind = astNode.kind?.ref?.name as c4.RelationshipKind
173
173
  const astPath = this.getAstNodePath(astNode)
174
- const title = toSingleLine(
174
+ const title = removeIndent(
175
175
  astNode.title ?? astNode.body?.props.find((p): p is ast.RelationStringProperty => p.key === 'title')?.value
176
176
  ) ?? ''
177
177
  const styleProp = astNode.body?.props.find(ast.isRelationStyleProperty)
@@ -321,12 +321,11 @@ export class LikeC4ModelParser {
321
321
  }
322
322
 
323
323
  private parsePredicateExpr(astNode: ast.ViewRulePredicateExpr): c4.Expression {
324
+ if (ast.isCustomRelationExpr(astNode)) {
325
+ return this.parseCustomRelationExpr(astNode)
326
+ }
324
327
  if (ast.isRelationExpr(astNode)) {
325
- return {
326
- source: this.parseElementExpr(astNode.source),
327
- target: this.parseElementExpr(astNode.target),
328
- isBidirectional: astNode.isBidirectional
329
- }
328
+ return this.parseRelationExpr(astNode)
330
329
  }
331
330
  if (ast.isInOutExpr(astNode)) {
332
331
  return {
@@ -352,6 +351,48 @@ export class LikeC4ModelParser {
352
351
  nonexhaustive(astNode)
353
352
  }
354
353
 
354
+ private parseCustomRelationExpr(astNode: ast.CustomRelationExpr): c4.CustomRelationExpr {
355
+ const relation = this.parseRelationExpr(astNode.relation)
356
+ const props = astNode.body?.props ?? []
357
+ return props.reduce(
358
+ (acc, prop) => {
359
+ if (ast.isRelationStringProperty(prop)) {
360
+ const value = removeIndent(prop.value)
361
+ if (isTruthy(value)) {
362
+ acc.customRelation['title'] = value
363
+ }
364
+ return acc
365
+ }
366
+ if (ast.isArrowProperty(prop)) {
367
+ acc.customRelation[prop.key] = prop.value
368
+ return acc
369
+ }
370
+ if (ast.isColorProperty(prop)) {
371
+ acc.customRelation[prop.key] = prop.value
372
+ return acc
373
+ }
374
+ if (ast.isLineProperty(prop)) {
375
+ acc.customRelation[prop.key] = prop.value
376
+ return acc
377
+ }
378
+ nonexhaustive(prop)
379
+ },
380
+ {
381
+ customRelation: {
382
+ relation
383
+ }
384
+ } as c4.CustomRelationExpr
385
+ )
386
+ }
387
+
388
+ private parseRelationExpr(astNode: ast.RelationExpr): c4.RelationExpr {
389
+ return {
390
+ source: this.parseElementExpr(astNode.source),
391
+ target: this.parseElementExpr(astNode.target),
392
+ isBidirectional: astNode.isBidirectional
393
+ }
394
+ }
395
+
355
396
  private parseViewRule(astRule: ast.ViewRule, isValid: IsValidFn): c4.ViewRule {
356
397
  if (ast.isIncludePredicate(astRule) || ast.isExcludePredicate(astRule)) {
357
398
  const exprs = astRule.expressions.flatMap(n => {
@@ -398,18 +439,22 @@ export class LikeC4ModelParser {
398
439
  if (!targetEl) {
399
440
  throw new Error('Invalid reference to target')
400
441
  }
442
+ const title = removeIndent(node.title) ?? null
401
443
  let source = this.resolveFqn(sourceEl)
402
444
  let target = this.resolveFqn(targetEl)
403
445
  if (node.isBackward) {
404
- ;[source, target] = [target, source]
446
+ return {
447
+ source: target,
448
+ target: source,
449
+ title,
450
+ isBackward: true
451
+ }
405
452
  }
406
453
 
407
- const title = toSingleLine(node.title) ?? null
408
454
  return {
409
455
  source,
410
456
  target,
411
- title,
412
- isBackward: node.isBackward
457
+ title
413
458
  }
414
459
  }
415
460
 
@@ -12,7 +12,7 @@ const asViewStyleRule = (target: string, style: ViewChanges.ChangeElementStyle['
12
12
  const indentStr = indent > 0 ? ' '.repeat(indent) : ''
13
13
  return [
14
14
  indentStr + `style ${target} {`,
15
- ...entries.strict(style).map(([key, value]) =>
15
+ ...entries(style).map(([key, value]) =>
16
16
  indentStr + ` ${key} ${key === 'opacity' ? value.toString() + '%' : value}`
17
17
  ),
18
18
  indentStr + `}`
@@ -125,7 +125,7 @@ export function changeElementStyle(services: LikeC4Services, {
125
125
  for (const { rule } of existing) {
126
126
  const ruleCstNode = rule.$cstNode
127
127
  invariant(ruleCstNode, 'RuleCstNode not found')
128
- for (const [key, _value] of entries.strict(style)) {
128
+ for (const [key, _value] of entries(style)) {
129
129
  const value = key === 'opacity' ? _value.toString() + '%' : _value
130
130
  const ruleProp = rule.styleprops.find(p => p.key === key)
131
131
  // replace existing property