@likec4/core 1.32.2 → 1.33.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.
- package/dist/builder/index.d.mts +6 -4
- package/dist/builder/index.mjs +45 -13
- package/dist/compute-view/index.d.mts +6 -4
- package/dist/compute-view/index.mjs +33 -10
- package/dist/compute-view/relationships-view/index.d.mts +6 -4
- package/dist/compute-view/relationships-view/index.mjs +6 -3
- package/dist/index.d.mts +9 -138
- package/dist/index.mjs +31 -3712
- package/dist/model/connection/deployment/index.d.mts +10 -0
- package/dist/model/connection/deployment/index.mjs +18 -0
- package/dist/model/connection/index.d.mts +70 -0
- package/dist/model/connection/index.mjs +23 -0
- package/dist/model/connection/model/index.d.mts +10 -0
- package/dist/model/connection/model/index.mjs +11 -0
- package/dist/model/index.d.mts +14 -256
- package/dist/model/index.mjs +27 -5
- package/dist/shared/{core.Bdz6fBF1.d.mts → core.1pNeDdRk.d.mts} +92 -81
- package/dist/shared/{core.BuO4ncfY.d.mts → core.668kXpZq.d.mts} +42 -52
- package/dist/shared/core.8KM0D6va.mjs +33 -0
- package/dist/shared/core.B0oEIXqd.mjs +47 -0
- package/dist/shared/core.BH9nN-_I.mjs +22 -0
- package/dist/shared/core.BMCb0wJI.mjs +177 -0
- package/dist/shared/core.BTnf14gD.mjs +331 -0
- package/dist/shared/core.BgvIuGGU.mjs +3 -0
- package/dist/shared/core.BoyJPEen.mjs +21 -0
- package/dist/shared/{core.CUYDgv2-.mjs → core.Br7Fab9l.mjs} +12 -15
- package/dist/shared/{core.BrCnZFTn.mjs → core.Btl47GOn.mjs} +3 -3
- package/dist/shared/core.ByrmCl9I.mjs +5 -0
- package/dist/shared/core.C80Z37Eu.mjs +29 -0
- package/dist/shared/{core.Pf5I9o0e.d.mts → core.CF82MWyh.d.mts} +1 -1
- package/dist/shared/{core.rmvsy0n3.mjs → core.C_C-3c1S.mjs} +5 -198
- package/dist/shared/core.CeQbbp7K.d.mts +41 -0
- package/dist/shared/{core.DoK86JEe.mjs → core.CpwrZi3D.mjs} +2 -73
- package/dist/shared/{core.CpXlRyE5.mjs → core.CtKXPqN_.mjs} +5 -36
- package/dist/shared/core.CxGKeL-L.mjs +144 -0
- package/dist/shared/core.CzdwsPmY.d.mts +69 -0
- package/dist/shared/{core.Cy9smucx.mjs → core.D4npX2q8.mjs} +1 -3
- package/dist/shared/core.D56nCp7e.mjs +168 -0
- package/dist/shared/core.D5qVzkws.mjs +724 -0
- package/dist/shared/core.DDAPzd3T.d.mts +87 -0
- package/dist/shared/{core.CBpKAlhj.mjs → core.DDynPqbh.mjs} +96 -56
- package/dist/shared/{core.Bk7wEZFG.mjs → core.DXRAwwDB.mjs} +3 -2
- package/dist/shared/core.DcS-0zys.mjs +10 -0
- package/dist/shared/{core.CBxTtXMi.d.mts → core.De___iBS.d.mts} +3 -2
- package/dist/shared/core.DsMrjY2q.mjs +34 -0
- package/dist/shared/core.DuSLYTG6.d.mts +57 -0
- package/dist/shared/core.Dy-F3xRP.d.mts +83 -0
- package/dist/shared/{core.DaAyklhe.d.mts → core.OLInHaJx.d.mts} +30 -22
- package/dist/shared/core.XptQjhMA.mjs +1295 -0
- package/dist/shared/{core.cbA9VZ02.mjs → core.hoq9W4rW.mjs} +28 -1073
- package/dist/shared/core.zLlww-07.mjs +3 -0
- package/dist/theme/index.d.mts +134 -0
- package/dist/theme/index.mjs +3709 -0
- package/dist/types/aux.d.mts +3 -2
- package/dist/types/index.d.mts +9 -5
- package/dist/types/index.mjs +17 -146
- package/dist/utils/index.d.mts +15 -100
- package/dist/utils/index.mjs +25 -1315
- package/dist/utils/iterable/index.d.mts +100 -0
- package/dist/utils/iterable/index.mjs +24 -0
- package/package.json +23 -7
- package/src/builder/Builder.ts +13 -4
- package/src/builder/__snapshots__/Builder-style1.spec.ts.snap +0 -13
- package/src/builder/__snapshots__/Builder-style2.compute-model.json5 +2 -6
- package/src/builder/__snapshots__/Builder-style2.spec.ts.snap +0 -7
- package/src/compute-view/deployment-view/__test__/fixture.ts +15 -5
- package/src/compute-view/deployment-view/utils.ts +13 -13
- package/src/compute-view/dynamic-view/compute.ts +2 -1
- package/src/compute-view/element-view/__test__/__snapshots__/legacy.spec.ts.snap +2 -2
- package/src/compute-view/relationships-view/layout.ts +1 -1
- package/src/compute-view/utils/applyCustomElementProperties.ts +8 -0
- package/src/compute-view/utils/applyCustomRelationProperties.ts +2 -0
- package/src/compute-view/utils/buildComputedNodes.ts +4 -3
- package/src/compute-view/utils/merge-props-from-relationships.ts +6 -5
- package/src/compute-view/utils/view-hash.ts +24 -3
- package/src/model/DeploymentElementModel.ts +20 -16
- package/src/model/ElementModel.ts +9 -3
- package/src/model/RelationModel.ts +19 -16
- package/src/model/index.ts +2 -0
- package/src/model/types.ts +1 -0
- package/src/model/view/EdgeModel.ts +4 -3
- package/src/model/view/LikeC4ViewModel.ts +5 -0
- package/src/model/view/NodeModel.ts +4 -2
- package/src/theme/index.ts +12 -5
- package/src/types/RichText.ts +200 -0
- package/src/types/index.ts +2 -0
- package/src/types/model-deployment.ts +4 -3
- package/src/types/model-logical.ts +3 -3
- package/src/types/scalar.ts +47 -2
- package/src/types/view-common.ts +1 -1
- package/src/types/view-computed.ts +2 -4
- package/src/types/view-layouted.ts +9 -0
- package/src/utils/index.ts +4 -9
- package/src/utils/markdown/index.ts +2 -0
- package/src/utils/markdown/to-html.ts +32 -0
- package/src/utils/markdown/to-text.ts +14 -0
|
@@ -47,6 +47,7 @@ export function applyCustomElementProperties<A extends AnyAux>(
|
|
|
47
47
|
padding,
|
|
48
48
|
size,
|
|
49
49
|
textSize,
|
|
50
|
+
description,
|
|
50
51
|
...rest
|
|
51
52
|
} = omitBy(props, isNullish)
|
|
52
53
|
const notEmpty = !isEmpty(rest)
|
|
@@ -62,6 +63,13 @@ export function applyCustomElementProperties<A extends AnyAux>(
|
|
|
62
63
|
...rest,
|
|
63
64
|
}
|
|
64
65
|
}
|
|
66
|
+
if (description !== undefined) {
|
|
67
|
+
node = {
|
|
68
|
+
...node,
|
|
69
|
+
isCustomized: true,
|
|
70
|
+
description: { txt: description },
|
|
71
|
+
}
|
|
72
|
+
}
|
|
65
73
|
|
|
66
74
|
let styleOverride: ComputedNode['style'] | undefined
|
|
67
75
|
if (border !== undefined) {
|
|
@@ -18,6 +18,7 @@ export function applyCustomRelationProperties<A extends AnyAux>(
|
|
|
18
18
|
customRelation: {
|
|
19
19
|
expr,
|
|
20
20
|
title,
|
|
21
|
+
description,
|
|
21
22
|
...customprops
|
|
22
23
|
},
|
|
23
24
|
} of rules
|
|
@@ -44,6 +45,7 @@ export function applyCustomRelationProperties<A extends AnyAux>(
|
|
|
44
45
|
edges[i] = {
|
|
45
46
|
...edge,
|
|
46
47
|
...props,
|
|
48
|
+
...description && { description: { txt: description } },
|
|
47
49
|
label: title ?? edge.label,
|
|
48
50
|
isCustomized: true,
|
|
49
51
|
}
|
|
@@ -29,8 +29,8 @@ function updateDepthOfAncestors(node: ComputedNode, nodes: ReadonlyMap<string, C
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export type ComputedNodeSource<A extends AnyAux = Unknown> = Simplify<
|
|
32
|
-
& Pick<ComputedNode<A>, 'id' | 'title' | 'kind' | 'deploymentRef' | 'modelRef'>
|
|
33
|
-
& Partial<Omit<Element<A>, 'id' | 'title' | 'kind'>>
|
|
32
|
+
& Pick<ComputedNode<A>, 'id' | 'title' | 'description' | 'kind' | 'deploymentRef' | 'modelRef'>
|
|
33
|
+
& Partial<Omit<Element<A>, 'id' | 'title' | 'description' | 'kind'>>
|
|
34
34
|
>
|
|
35
35
|
|
|
36
36
|
export function elementModelToNodeSource<A extends AnyAux>(el: ElementModel<A>): ComputedNodeSource<A> {
|
|
@@ -86,7 +86,7 @@ export function buildComputedNodes<A extends AnyAux>(
|
|
|
86
86
|
// Ensure that parent nodes are created before child nodes
|
|
87
87
|
Array.from(elements)
|
|
88
88
|
.sort(compareByFqnHierarchically)
|
|
89
|
-
.forEach(({ id, style, kind, title, color, shape, tags, ...el }) => {
|
|
89
|
+
.forEach(({ id, style, kind, title, color, shape, tags, notation, ...el }) => {
|
|
90
90
|
let parent = parentFqn(id)
|
|
91
91
|
let level = 0
|
|
92
92
|
let parentNd: ComputedNode<A> | undefined
|
|
@@ -130,6 +130,7 @@ export function buildComputedNodes<A extends AnyAux>(
|
|
|
130
130
|
children: [],
|
|
131
131
|
inEdges: [],
|
|
132
132
|
outEdges: [],
|
|
133
|
+
...notation && { notation },
|
|
133
134
|
...el,
|
|
134
135
|
style: {
|
|
135
136
|
...style,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isTruthy, only, pickBy, pipe, reduce, unique } from 'remeda'
|
|
1
|
+
import { isDeepEqual, isTruthy, only, pickBy, pipe, reduce, unique } from 'remeda'
|
|
2
2
|
import type {
|
|
3
3
|
AnyAux,
|
|
4
4
|
aux,
|
|
@@ -9,12 +9,13 @@ import type {
|
|
|
9
9
|
Relationship,
|
|
10
10
|
RelationshipArrowType,
|
|
11
11
|
RelationshipLineType,
|
|
12
|
+
scalar,
|
|
12
13
|
} from '../../types'
|
|
13
14
|
import { isNonEmptyArray } from '../../utils'
|
|
14
15
|
|
|
15
16
|
function pickRelationshipProps<A extends AnyAux>(relation: Relationship<A> | DeploymentRelationship<A>): {
|
|
16
17
|
title?: string
|
|
17
|
-
description?:
|
|
18
|
+
description?: scalar.MarkdownOrString | null
|
|
18
19
|
technology: string | null
|
|
19
20
|
kind: aux.RelationKind<A> | null
|
|
20
21
|
color: Color | null
|
|
@@ -52,7 +53,7 @@ function pickRelationshipProps<A extends AnyAux>(relation: Relationship<A> | Dep
|
|
|
52
53
|
|
|
53
54
|
export type MergedRelationshipProps<A extends AnyAux> = {
|
|
54
55
|
title?: string | null
|
|
55
|
-
description?:
|
|
56
|
+
description?: scalar.MarkdownOrString | null
|
|
56
57
|
technology?: string
|
|
57
58
|
kind?: aux.RelationKind<A>
|
|
58
59
|
color?: Color
|
|
@@ -80,7 +81,7 @@ export function mergePropsFromRelationships<A extends AnyAux>(
|
|
|
80
81
|
if (isTruthy(r.title) && !acc.title.includes(r.title)) {
|
|
81
82
|
acc.title.push(r.title)
|
|
82
83
|
}
|
|
83
|
-
if (isTruthy(r.description) && !acc.description.
|
|
84
|
+
if (isTruthy(r.description) && !acc.description.some(isDeepEqual(r.description))) {
|
|
84
85
|
acc.description.push(r.description)
|
|
85
86
|
}
|
|
86
87
|
if (isTruthy(r.technology) && !acc.technology.includes(r.technology)) {
|
|
@@ -114,7 +115,7 @@ export function mergePropsFromRelationships<A extends AnyAux>(
|
|
|
114
115
|
},
|
|
115
116
|
{
|
|
116
117
|
title: [] as string[],
|
|
117
|
-
description: [] as
|
|
118
|
+
description: [] as scalar.MarkdownOrString[],
|
|
118
119
|
technology: [] as string[],
|
|
119
120
|
kind: [] as aux.RelationKind<A>[],
|
|
120
121
|
head: [] as RelationshipArrowType[],
|
|
@@ -10,12 +10,33 @@ export function calcViewLayoutHash<V extends ComputedView>(view: SetOptional<V,
|
|
|
10
10
|
autoLayout: view.autoLayout,
|
|
11
11
|
nodes: pipe(
|
|
12
12
|
view.nodes,
|
|
13
|
-
map(
|
|
14
|
-
|
|
13
|
+
map(n => ({
|
|
14
|
+
id: n.id,
|
|
15
|
+
icon: isTruthy(n.icon) ? 'Y' : 'N',
|
|
16
|
+
title: n.title,
|
|
17
|
+
description: n.description?.md ?? n.description?.txt ?? null,
|
|
18
|
+
technology: n.technology ?? null,
|
|
19
|
+
shape: n.shape,
|
|
20
|
+
size: n.style.size ?? null,
|
|
21
|
+
textSize: n.style.textSize ?? null,
|
|
22
|
+
padding: n.style.padding ?? null,
|
|
23
|
+
children: n.children,
|
|
24
|
+
})),
|
|
25
|
+
mapToObj(({ id, ...node }) => [id, node]),
|
|
15
26
|
),
|
|
16
27
|
edges: pipe(
|
|
17
28
|
view.edges,
|
|
18
|
-
map(
|
|
29
|
+
map(e => ({
|
|
30
|
+
source: e.source,
|
|
31
|
+
target: e.target,
|
|
32
|
+
label: e.label,
|
|
33
|
+
description: e.description?.md ?? e.description?.txt ?? null,
|
|
34
|
+
technology: e.technology ?? null,
|
|
35
|
+
dir: e.dir,
|
|
36
|
+
head: e.head,
|
|
37
|
+
tail: e.tail,
|
|
38
|
+
line: e.line,
|
|
39
|
+
})),
|
|
19
40
|
mapToObj(({ source, target, ...edge }) => [`${source}:${target}`, edge]),
|
|
20
41
|
),
|
|
21
42
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isTruthy, only, unique } from 'remeda'
|
|
1
|
+
import { isEmpty, isTruthy, only, unique } from 'remeda'
|
|
2
2
|
import type { SetRequired } from 'type-fest'
|
|
3
3
|
import {
|
|
4
4
|
type Any,
|
|
@@ -12,11 +12,13 @@ import {
|
|
|
12
12
|
type IteratorLike,
|
|
13
13
|
type Link,
|
|
14
14
|
type RelationshipLineType,
|
|
15
|
+
type RichTextOrEmpty,
|
|
15
16
|
type scalar,
|
|
16
17
|
DefaultElementShape,
|
|
17
18
|
DefaultLineStyle,
|
|
18
19
|
DefaultShapeSize,
|
|
19
20
|
DefaultThemeColor,
|
|
21
|
+
RichText,
|
|
20
22
|
} from '../types'
|
|
21
23
|
import * as aux from '../types/aux'
|
|
22
24
|
import { commonAncestor, hierarchyLevel, memoizeProp, nonNullable } from '../utils'
|
|
@@ -61,8 +63,8 @@ abstract class AbstractDeploymentElementModel<A extends Any> implements WithTags
|
|
|
61
63
|
return this.$node.style?.color as Color ?? DefaultThemeColor
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
get description():
|
|
65
|
-
return this.$node.description
|
|
66
|
+
get description(): RichTextOrEmpty {
|
|
67
|
+
return RichText.memoize(this, this.$node.description)
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
get technology(): string | null {
|
|
@@ -199,6 +201,10 @@ abstract class AbstractDeploymentElementModel<A extends Any> implements WithTags
|
|
|
199
201
|
))
|
|
200
202
|
}
|
|
201
203
|
|
|
204
|
+
public hasMetadata(): boolean {
|
|
205
|
+
return !!this.$node.metadata && !isEmpty(this.$node.metadata)
|
|
206
|
+
}
|
|
207
|
+
|
|
202
208
|
public getMetadata(): aux.Metadata<A>
|
|
203
209
|
public getMetadata(field: aux.MetadataKey<A>): string | undefined
|
|
204
210
|
public getMetadata(field?: aux.MetadataKey<A>) {
|
|
@@ -402,12 +408,12 @@ export class DeployedInstanceModel<A extends Any = Any> extends AbstractDeployme
|
|
|
402
408
|
return this.element.kind
|
|
403
409
|
}
|
|
404
410
|
|
|
405
|
-
override get description():
|
|
406
|
-
return this.$instance.description ?? this.element.description
|
|
411
|
+
override get description(): RichTextOrEmpty {
|
|
412
|
+
return RichText.memoize(this, this.$instance.description ?? this.element.$element.description)
|
|
407
413
|
}
|
|
408
414
|
|
|
409
415
|
override get technology(): string | null {
|
|
410
|
-
return this.$instance.technology ?? this.element.technology
|
|
416
|
+
return this.$instance.technology ?? this.element.technology ?? null
|
|
411
417
|
}
|
|
412
418
|
|
|
413
419
|
override get links(): ReadonlyArray<Link> {
|
|
@@ -486,7 +492,7 @@ export class NestedElementOfDeployedInstanceModel<A extends Any = Any> {
|
|
|
486
492
|
return this.element.title
|
|
487
493
|
}
|
|
488
494
|
|
|
489
|
-
get description():
|
|
495
|
+
get description(): RichTextOrEmpty {
|
|
490
496
|
return this.element.description
|
|
491
497
|
}
|
|
492
498
|
|
|
@@ -536,17 +542,11 @@ export class DeploymentRelationModel<A extends Any = Any> implements AnyRelation
|
|
|
536
542
|
}
|
|
537
543
|
|
|
538
544
|
get technology(): string | null {
|
|
539
|
-
|
|
540
|
-
return null
|
|
541
|
-
}
|
|
542
|
-
return this.$relationship.technology
|
|
545
|
+
return this.$relationship.technology ?? null
|
|
543
546
|
}
|
|
544
547
|
|
|
545
|
-
get description():
|
|
546
|
-
|
|
547
|
-
return null
|
|
548
|
-
}
|
|
549
|
-
return this.$relationship.description
|
|
548
|
+
get description(): RichTextOrEmpty {
|
|
549
|
+
return RichText.memoize(this, this.$relationship.description)
|
|
550
550
|
}
|
|
551
551
|
|
|
552
552
|
get tags(): aux.Tags<A> {
|
|
@@ -590,6 +590,10 @@ export class DeploymentRelationModel<A extends Any = Any> implements AnyRelation
|
|
|
590
590
|
return false
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
+
public hasMetadata(): boolean {
|
|
594
|
+
return !!this.$relationship.metadata && !isEmpty(this.$relationship.metadata)
|
|
595
|
+
}
|
|
596
|
+
|
|
593
597
|
public getMetadata(): aux.Metadata<A>
|
|
594
598
|
public getMetadata(field: aux.MetadataKey<A>): string | undefined
|
|
595
599
|
public getMetadata(field?: aux.MetadataKey<A>) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isTruthy, unique } from 'remeda'
|
|
1
|
+
import { isEmpty, isTruthy, unique } from 'remeda'
|
|
2
2
|
import type { SetRequired } from 'type-fest'
|
|
3
3
|
import type { Any, AnyAux, Color, IteratorLike } from '../types'
|
|
4
4
|
import {
|
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
type IconUrl,
|
|
9
9
|
type Link,
|
|
10
10
|
type ProjectId,
|
|
11
|
+
type RichTextOrEmpty,
|
|
11
12
|
DefaultElementShape,
|
|
12
13
|
DefaultShapeSize,
|
|
13
14
|
DefaultThemeColor,
|
|
15
|
+
RichText,
|
|
14
16
|
splitGlobalFqn,
|
|
15
17
|
} from '../types'
|
|
16
18
|
import * as aux from '../types/aux'
|
|
@@ -89,8 +91,8 @@ export class ElementModel<A extends AnyAux = Any> implements WithTags<A>, WithMe
|
|
|
89
91
|
return this.$element.title
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
get description():
|
|
93
|
-
return this.$element.description
|
|
94
|
+
get description(): RichTextOrEmpty {
|
|
95
|
+
return RichText.memoize(this, this.$element.description)
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
get technology(): string | null {
|
|
@@ -265,6 +267,10 @@ export class ElementModel<A extends AnyAux = Any> implements WithTags<A>, WithMe
|
|
|
265
267
|
return this.$model.deployment.instancesOf(this)
|
|
266
268
|
}
|
|
267
269
|
|
|
270
|
+
public hasMetadata(): boolean {
|
|
271
|
+
return !!this.$element.metadata && !isEmpty(this.$element.metadata)
|
|
272
|
+
}
|
|
273
|
+
|
|
268
274
|
public getMetadata(): aux.Metadata<A>
|
|
269
275
|
public getMetadata(field: aux.MetadataKey<A>): string | undefined
|
|
270
276
|
public getMetadata(field?: aux.MetadataKey<A>) {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { isTruthy } from 'remeda'
|
|
1
|
+
import { isEmpty, isTruthy } from 'remeda'
|
|
2
2
|
import type { AnyAux, Color, IteratorLike, Link, scalar } from '../types'
|
|
3
3
|
import {
|
|
4
4
|
type Relationship,
|
|
5
5
|
type RelationshipLineType,
|
|
6
|
+
type RichTextOrEmpty,
|
|
6
7
|
DefaultLineStyle,
|
|
7
8
|
DefaultRelationshipColor,
|
|
8
9
|
FqnRef,
|
|
10
|
+
RichText,
|
|
9
11
|
} from '../types'
|
|
10
12
|
import type * as aux from '../types/aux'
|
|
11
13
|
import { commonAncestor } from '../utils/fqn'
|
|
@@ -23,16 +25,16 @@ export type RelationshipsIterator<A extends AnyAux> = IteratorLike<RelationshipM
|
|
|
23
25
|
* use {@link isDeploymentRelationModel} guard to check if the relationship is a deployment relationship
|
|
24
26
|
*/
|
|
25
27
|
export interface AnyRelationshipModel<A extends AnyAux> extends WithTags<A>, WithMetadata<A> {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
readonly id: scalar.RelationId
|
|
29
|
+
readonly expression: string
|
|
30
|
+
readonly title: string | null
|
|
31
|
+
readonly technology: string | null
|
|
32
|
+
readonly description: RichTextOrEmpty
|
|
33
|
+
readonly navigateTo: LikeC4ViewModel<A> | null
|
|
34
|
+
readonly kind: aux.RelationKind<A> | null
|
|
35
|
+
readonly links: ReadonlyArray<Link>
|
|
36
|
+
readonly color: Color
|
|
37
|
+
readonly line: RelationshipLineType
|
|
36
38
|
isDeploymentRelation(): this is DeploymentRelationModel<A>
|
|
37
39
|
isModelRelation(): this is RelationshipModel<A>
|
|
38
40
|
views(): ViewsIterator<A>
|
|
@@ -80,11 +82,8 @@ export class RelationshipModel<A extends AnyAux = AnyAux> implements AnyRelation
|
|
|
80
82
|
return this.$relationship.technology
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
get description():
|
|
84
|
-
|
|
85
|
-
return null
|
|
86
|
-
}
|
|
87
|
-
return this.$relationship.description
|
|
85
|
+
get description(): RichTextOrEmpty {
|
|
86
|
+
return RichText.memoize(this, this.$relationship.description)
|
|
88
87
|
}
|
|
89
88
|
|
|
90
89
|
get navigateTo(): LikeC4ViewModel<A> | null {
|
|
@@ -131,6 +130,10 @@ export class RelationshipModel<A extends AnyAux = AnyAux> implements AnyRelation
|
|
|
131
130
|
return true
|
|
132
131
|
}
|
|
133
132
|
|
|
133
|
+
public hasMetadata(): boolean {
|
|
134
|
+
return !!this.$relationship.metadata && !isEmpty(this.$relationship.metadata)
|
|
135
|
+
}
|
|
136
|
+
|
|
134
137
|
public getMetadata(): aux.Metadata<A>
|
|
135
138
|
public getMetadata(field: aux.MetadataKey<A>): string | undefined
|
|
136
139
|
public getMetadata(field?: aux.MetadataKey<A>) {
|
package/src/model/index.ts
CHANGED
package/src/model/types.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
extractStep,
|
|
12
12
|
isStepEdgeId,
|
|
13
13
|
} from '../../types'
|
|
14
|
+
import { type RichTextOrEmpty, RichText } from '../../types'
|
|
14
15
|
import type { DeploymentRelationModel } from '../DeploymentElementModel'
|
|
15
16
|
import type { RelationshipModel } from '../RelationModel'
|
|
16
17
|
import type { $View, WithTags } from '../types'
|
|
@@ -39,11 +40,11 @@ export class EdgeModel<A extends Any = Any, View extends $View<A> = $View<A>> im
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
get label(): string | null {
|
|
42
|
-
return this.#edge.label
|
|
43
|
+
return this.#edge.label ?? null
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
get description():
|
|
46
|
-
return this.#edge.description
|
|
46
|
+
get description(): RichTextOrEmpty {
|
|
47
|
+
return RichText.memoize(this, this.#edge.description)
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
get technology(): string | null {
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ViewWithType,
|
|
9
9
|
} from '../../types'
|
|
10
10
|
import { _stage, _type } from '../../types'
|
|
11
|
+
import { type RichTextOrEmpty, RichText } from '../../types'
|
|
11
12
|
import type * as aux from '../../types/aux'
|
|
12
13
|
import { DefaultMap, ifind, nonNullable } from '../../utils'
|
|
13
14
|
import type { ElementModel } from '../ElementModel'
|
|
@@ -92,6 +93,10 @@ export class LikeC4ViewModel<A extends Any = Any, V extends $View<A> = $View<A>>
|
|
|
92
93
|
return this.$view.title
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
get description(): RichTextOrEmpty {
|
|
97
|
+
return RichText.memoize(this, this.$view.description)
|
|
98
|
+
}
|
|
99
|
+
|
|
95
100
|
get tags(): aux.Tags<A> {
|
|
96
101
|
return this.$view.tags ?? []
|
|
97
102
|
}
|
|
@@ -10,9 +10,11 @@ import {
|
|
|
10
10
|
type IconUrl,
|
|
11
11
|
type IteratorLike,
|
|
12
12
|
type Link,
|
|
13
|
+
type RichTextOrEmpty,
|
|
13
14
|
type scalar,
|
|
14
15
|
GroupElementKind,
|
|
15
16
|
isGroupElementKind,
|
|
17
|
+
RichText,
|
|
16
18
|
} from '../../types'
|
|
17
19
|
import type * as aux from '../../types/aux'
|
|
18
20
|
import { memoizeProp } from '../../utils'
|
|
@@ -46,8 +48,8 @@ export class NodeModel<A extends Any = Any, V extends $View<A> = $View<A>> imple
|
|
|
46
48
|
return this.#node.kind as any
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
get description():
|
|
50
|
-
return this.#node.description
|
|
51
|
+
get description(): RichTextOrEmpty {
|
|
52
|
+
return RichText.memoize(this, this.#node.description)
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
get technology(): string | null {
|
package/src/theme/index.ts
CHANGED
|
@@ -37,12 +37,19 @@ export const defaultTheme: LikeC4Theme = {
|
|
|
37
37
|
lg: 24, // 1.5rem = 16px + 8px
|
|
38
38
|
xl: 32, // 2rem
|
|
39
39
|
},
|
|
40
|
+
/**
|
|
41
|
+
* Text sizes for titles
|
|
42
|
+
* https://typescale.com/
|
|
43
|
+
*
|
|
44
|
+
* Scale: 1.2
|
|
45
|
+
* Base: 16px
|
|
46
|
+
*/
|
|
40
47
|
textSizes: {
|
|
41
|
-
xs:
|
|
42
|
-
sm:
|
|
43
|
-
md: 19,
|
|
44
|
-
lg:
|
|
45
|
-
xl:
|
|
48
|
+
xs: 13.33,
|
|
49
|
+
sm: 16,
|
|
50
|
+
md: 19.2,
|
|
51
|
+
lg: 23.04,
|
|
52
|
+
xl: 27.65,
|
|
46
53
|
},
|
|
47
54
|
}
|
|
48
55
|
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { MarkdownOrString } from '../types'
|
|
2
|
+
import { markdownToHtml, markdownToText, memoizeProp, nonexhaustive } from '../utils'
|
|
3
|
+
|
|
4
|
+
const richtxt = Symbol.for('richtxt')
|
|
5
|
+
const symb_text = Symbol.for('text')
|
|
6
|
+
const symb_html = Symbol.for('html')
|
|
7
|
+
|
|
8
|
+
const emptyTxt = ''
|
|
9
|
+
|
|
10
|
+
export interface RichTextEmpty {
|
|
11
|
+
readonly isEmpty: true
|
|
12
|
+
readonly isMarkdown: false
|
|
13
|
+
readonly nonEmpty: false
|
|
14
|
+
readonly $source: null
|
|
15
|
+
readonly text: null
|
|
16
|
+
readonly md: null
|
|
17
|
+
readonly html: null
|
|
18
|
+
equals(other: unknown): boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RichTextNonEmpty {
|
|
22
|
+
readonly isEmpty: false
|
|
23
|
+
readonly nonEmpty: true
|
|
24
|
+
readonly isMarkdown: boolean
|
|
25
|
+
readonly $source: MarkdownOrString
|
|
26
|
+
readonly text: string
|
|
27
|
+
readonly md: string
|
|
28
|
+
readonly html: string
|
|
29
|
+
equals(other: unknown): boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type RichTextOrEmpty = RichTextNonEmpty | RichTextEmpty
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* RichText is a class that represents a potentially markdown string.
|
|
36
|
+
* It can be either a plain text or a markdown.
|
|
37
|
+
* It is used to represent the content of a node or a link.
|
|
38
|
+
*/
|
|
39
|
+
export class RichText {
|
|
40
|
+
private static _cache = new WeakMap<MarkdownOrString, RichTextOrEmpty>()
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates and memoizes a RichText instance.
|
|
44
|
+
* @see ElementModel.description
|
|
45
|
+
* @example
|
|
46
|
+
*
|
|
47
|
+
* get description(): RichTextOrEmpty {
|
|
48
|
+
* return RichText.memoize(this, this.$element.description)
|
|
49
|
+
* }
|
|
50
|
+
*/
|
|
51
|
+
static memoize(obj: object, source: MarkdownOrString | null | undefined): RichTextOrEmpty {
|
|
52
|
+
return memoizeProp(obj, richtxt, () => RichText.from(source))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Creates a RichText instance from a source.
|
|
57
|
+
*/
|
|
58
|
+
static from(source: RichTextOrEmpty | MarkdownOrString | string | null | undefined): RichTextOrEmpty {
|
|
59
|
+
if (source === null || source === undefined || source === RichText.EMPTY) {
|
|
60
|
+
return RichText.EMPTY
|
|
61
|
+
}
|
|
62
|
+
if (source instanceof RichText) {
|
|
63
|
+
return source as RichTextOrEmpty
|
|
64
|
+
}
|
|
65
|
+
if (typeof source === 'string') {
|
|
66
|
+
return RichText.from({ txt: source })
|
|
67
|
+
}
|
|
68
|
+
if ('isEmpty' in source && source.isEmpty) {
|
|
69
|
+
return RichText.EMPTY
|
|
70
|
+
}
|
|
71
|
+
if ('md' in source && source.md.trim() === emptyTxt) {
|
|
72
|
+
return RichText.EMPTY
|
|
73
|
+
}
|
|
74
|
+
if ('txt' in source && source.txt.trim() === emptyTxt) {
|
|
75
|
+
return RichText.EMPTY
|
|
76
|
+
}
|
|
77
|
+
const _source = source as MarkdownOrString
|
|
78
|
+
const cached = RichText._cache.get(_source)
|
|
79
|
+
if (cached) {
|
|
80
|
+
return cached
|
|
81
|
+
}
|
|
82
|
+
const rt = new RichText(_source) as RichTextOrEmpty
|
|
83
|
+
RichText._cache.set(_source, rt)
|
|
84
|
+
return rt
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* This is a workaround for the fact that we need instance of RichText for `instanceof` checks
|
|
89
|
+
* It is invalid inheritance (returning `null` from getters), and we cast to @see RichTextEmpty
|
|
90
|
+
*/
|
|
91
|
+
static EMPTY: RichTextEmpty = new class extends RichText {
|
|
92
|
+
public override readonly isEmpty: true = true
|
|
93
|
+
public override readonly nonEmpty: false = false
|
|
94
|
+
public override readonly isMarkdown: false = false
|
|
95
|
+
public override readonly $source: null = null
|
|
96
|
+
constructor() {
|
|
97
|
+
super({ txt: emptyTxt })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
override get text(): any {
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override get md(): any {
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
override get html(): any {
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
}()
|
|
112
|
+
|
|
113
|
+
public readonly $source: Readonly<MarkdownOrString> | null
|
|
114
|
+
|
|
115
|
+
public readonly isEmpty: boolean
|
|
116
|
+
public readonly nonEmpty: boolean
|
|
117
|
+
public readonly isMarkdown: boolean
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Private constructor to prevent direct instantiation.
|
|
121
|
+
* Use {@link RichText.from} or {@link RichText.memoize} instead.
|
|
122
|
+
*/
|
|
123
|
+
private constructor(source: MarkdownOrString | string) {
|
|
124
|
+
this.isMarkdown = false
|
|
125
|
+
if (typeof source === 'string') {
|
|
126
|
+
this.$source = { txt: source }
|
|
127
|
+
this.isEmpty = source.trim() === emptyTxt
|
|
128
|
+
} else {
|
|
129
|
+
this.$source = source
|
|
130
|
+
this.isEmpty = true
|
|
131
|
+
if ('md' in source) {
|
|
132
|
+
this.isEmpty = source.md === emptyTxt
|
|
133
|
+
this.isMarkdown = true
|
|
134
|
+
} else {
|
|
135
|
+
this.isEmpty = source.txt === emptyTxt
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
this.nonEmpty = !this.isEmpty
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Returns the text content of the rich text.
|
|
143
|
+
* If the source is a string, it returns the string.
|
|
144
|
+
* If the source is a markdown, it returns the markdown.
|
|
145
|
+
*/
|
|
146
|
+
get text(): string {
|
|
147
|
+
if (this.isEmpty || this.$source === null) {
|
|
148
|
+
return emptyTxt
|
|
149
|
+
}
|
|
150
|
+
const source = this.$source
|
|
151
|
+
if ('txt' in source) {
|
|
152
|
+
return source.txt
|
|
153
|
+
}
|
|
154
|
+
return memoizeProp(this, symb_text, () => markdownToText(source.md!))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Returns the markdown content of the rich text.
|
|
159
|
+
* If the source is a string, it returns the string.
|
|
160
|
+
* If the source is a markdown, it returns the markdown.
|
|
161
|
+
*/
|
|
162
|
+
get md(): string {
|
|
163
|
+
if (this.isEmpty || this.$source === null) {
|
|
164
|
+
return emptyTxt
|
|
165
|
+
}
|
|
166
|
+
const source = this.$source
|
|
167
|
+
if ('md' in source) {
|
|
168
|
+
return source.md
|
|
169
|
+
}
|
|
170
|
+
if ('txt' in source) {
|
|
171
|
+
return source.txt
|
|
172
|
+
}
|
|
173
|
+
nonexhaustive(source)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Returns the html content of the rich text.
|
|
178
|
+
* If the source is a string, it returns the string.
|
|
179
|
+
* If the source is a markdown, it returns the HTML.
|
|
180
|
+
*/
|
|
181
|
+
get html(): string {
|
|
182
|
+
if (this.isEmpty || this.$source === null) {
|
|
183
|
+
return emptyTxt
|
|
184
|
+
}
|
|
185
|
+
const markdown = this.$source.md ?? this.$source.txt
|
|
186
|
+
return memoizeProp(this, symb_html, () => markdownToHtml(markdown))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
equals(other: unknown): boolean {
|
|
190
|
+
if (this === other) return true
|
|
191
|
+
if (!(other instanceof RichText)) return false
|
|
192
|
+
if (this.isEmpty && other.isEmpty) return true
|
|
193
|
+
if (this.isEmpty !== other.isEmpty || this.isMarkdown !== other.isMarkdown) return false
|
|
194
|
+
|
|
195
|
+
if (this.isMarkdown) {
|
|
196
|
+
return this.$source?.md === other.$source?.md
|
|
197
|
+
}
|
|
198
|
+
return this.$source?.txt === other.$source?.txt
|
|
199
|
+
}
|
|
200
|
+
}
|