@canonical/anatomy-dsl 0.2.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/README.md +145 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/parse.js +72 -0
- package/dist/esm/transform.js +116 -0
- package/dist/esm/types.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/parse.d.ts +2 -0
- package/dist/parse.js +72 -0
- package/dist/transform.d.ts +2 -0
- package/dist/transform.js +114 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/parse.d.ts +2 -0
- package/dist/types/transform.d.ts +2 -0
- package/dist/types/types.d.ts +36 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.js +1 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Anatomy DSL
|
|
2
|
+
|
|
3
|
+
A YAML-based DSL for representing design system component anatomies — platform-agnostic structural primitives that precede implementation.
|
|
4
|
+
|
|
5
|
+
## Quick Example
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
---
|
|
9
|
+
node:
|
|
10
|
+
uri: global.component.button
|
|
11
|
+
styles:
|
|
12
|
+
layout.type: flow
|
|
13
|
+
layout.direction: horizontal
|
|
14
|
+
layout.align: center
|
|
15
|
+
spacing.internal: spacing/medium
|
|
16
|
+
appearance.background: color/surface/button
|
|
17
|
+
appearance.radius: radius/button
|
|
18
|
+
edges:
|
|
19
|
+
- node:
|
|
20
|
+
uri: global.subcomponent.button-icon
|
|
21
|
+
relation:
|
|
22
|
+
cardinality: "0..1"
|
|
23
|
+
slotName: icon
|
|
24
|
+
- node:
|
|
25
|
+
role: label text
|
|
26
|
+
relation:
|
|
27
|
+
cardinality: "1"
|
|
28
|
+
slotName: default
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Style values are **design token paths** (`spacing/medium`, `color/surface/button`) — forward-slash delimited references resolved at runtime against the active theme. Primitives like `flow` and `center` are used for layout semantics that don't vary across themes.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
bun add -D @canonical/anatomy-dsl
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { parse } from "yaml";
|
|
43
|
+
import { parseAnatomyYAML, anatomyToTTL } from "@canonical/anatomy-dsl";
|
|
44
|
+
|
|
45
|
+
const raw = parse(readFileSync("Button.anatomy.yaml", "utf8"));
|
|
46
|
+
const spec = parseAnatomyYAML(raw);
|
|
47
|
+
const ttl = anatomyToTTL(spec);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The button example above produces:
|
|
51
|
+
|
|
52
|
+
```turtle
|
|
53
|
+
@prefix : <http://anatomy-dsl.example.org/ontology#> .
|
|
54
|
+
|
|
55
|
+
[] a :Specification ;
|
|
56
|
+
:rootNode [
|
|
57
|
+
a :NamedNode ;
|
|
58
|
+
:uri "global.component.button" ;
|
|
59
|
+
:hasStyle
|
|
60
|
+
[ :styleKey "layout.type" ; :styleValue "flow" ] ,
|
|
61
|
+
[ :styleKey "layout.direction" ; :styleValue "horizontal" ] ,
|
|
62
|
+
[ :styleKey "layout.align" ; :styleValue "center" ] ,
|
|
63
|
+
[ :styleKey "spacing.internal" ; :styleValue "spacing/medium" ] ,
|
|
64
|
+
[ :styleKey "appearance.background" ; :styleValue "color/surface/button" ] ,
|
|
65
|
+
[ :styleKey "appearance.radius" ; :styleValue "radius/button" ] ;
|
|
66
|
+
:hasEdge [
|
|
67
|
+
a :Edge ;
|
|
68
|
+
:edgeTarget [
|
|
69
|
+
a :NamedNode ;
|
|
70
|
+
:uri "global.subcomponent.button-icon"
|
|
71
|
+
] ;
|
|
72
|
+
:hasRelation [
|
|
73
|
+
a :Relation ;
|
|
74
|
+
:cardinality "0..1" ;
|
|
75
|
+
:slotName "icon"
|
|
76
|
+
]
|
|
77
|
+
] , [
|
|
78
|
+
a :Edge ;
|
|
79
|
+
:edgeTarget [
|
|
80
|
+
a :AnonymousNode ;
|
|
81
|
+
:role "label text"
|
|
82
|
+
] ;
|
|
83
|
+
:hasRelation [
|
|
84
|
+
a :Relation ;
|
|
85
|
+
:cardinality "1" ;
|
|
86
|
+
:slotName "default"
|
|
87
|
+
]
|
|
88
|
+
]
|
|
89
|
+
] .
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## API
|
|
93
|
+
|
|
94
|
+
### `parseAnatomyYAML(raw: unknown): Specification`
|
|
95
|
+
|
|
96
|
+
Converts a parsed YAML object (from any YAML library) into the typed `Specification` structure. Handles field mapping between the YAML format and the TypeScript types.
|
|
97
|
+
|
|
98
|
+
### `anatomyToTTL(spec: Specification): string`
|
|
99
|
+
|
|
100
|
+
Pure function. Takes a `Specification`, returns a Turtle (RDF) string. No I/O, no side effects.
|
|
101
|
+
|
|
102
|
+
### Types
|
|
103
|
+
|
|
104
|
+
All types mirror the [OWL ontology](definitions/ontology.ttl) exactly:
|
|
105
|
+
|
|
106
|
+
| Type | Description |
|
|
107
|
+
|------|-------------|
|
|
108
|
+
| `Specification` | Root — contains exactly one `NamedNode` |
|
|
109
|
+
| `NamedNode` | Design system entity with a `uri` |
|
|
110
|
+
| `AnonymousNode` | Structural element with a `role` |
|
|
111
|
+
| `Node` | `NamedNode \| AnonymousNode` (discriminated on `type`) |
|
|
112
|
+
| `Edge` | Reified parent→child relationship |
|
|
113
|
+
| `Relation` | Cardinality and optional slot name |
|
|
114
|
+
| `Style` | Reified key-value tuple |
|
|
115
|
+
| `Switch` | Polymorphic position (discriminator: `props \| internal \| override`) |
|
|
116
|
+
| `SwitchCase` | One alternative within a switch |
|
|
117
|
+
|
|
118
|
+
## Repository Structure
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
definitions/ Turtle ontology (OWL) + SHACL shapes
|
|
122
|
+
schemas/ JSON Schema for validating .anatomy.yaml files
|
|
123
|
+
docs/ API reference (merged WD404 + WD404.1)
|
|
124
|
+
examples/ Example anatomy files (YAML + Turtle pairs)
|
|
125
|
+
src/ TypeScript types, parser, and transform
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Scope
|
|
129
|
+
|
|
130
|
+
The Anatomy DSL describes **structure only**. It does not handle:
|
|
131
|
+
|
|
132
|
+
- **Prop mapping** — which props a component accepts and how they map to behaviour
|
|
133
|
+
- **State or state machines** — component states, transitions, or interaction logic
|
|
134
|
+
- **Modifier descriptions** — only design token references are supported, not semantic modifier definitions
|
|
135
|
+
|
|
136
|
+
## Design Notes
|
|
137
|
+
|
|
138
|
+
Styles are modelled as reified key-value tuples (`hasStyle [ styleKey "…" ; styleValue "…" ]`). This keeps the ontology open-ended while remaining lossless. Frequently used style keys may be promoted to first-class datatype properties in a future version.
|
|
139
|
+
|
|
140
|
+
## Specification Status
|
|
141
|
+
|
|
142
|
+
| Index | Title | Status |
|
|
143
|
+
|---------|--------------------------|----------------|
|
|
144
|
+
| [WD404](https://docs.google.com/document/d/1eFr-SNsAZyidnZzpWp1Jeegiat_SSM7mOW_G8p3nXo8/edit?tab=t.pndvuecem8cf) | Anatomy DSL | Approved |
|
|
145
|
+
| [WD404.1](https://docs.google.com/document/d/1eFr-SNsAZyidnZzpWp1Jeegiat_SSM7mOW_G8p3nXo8/edit?tab=t.pndvuecem8cf) | Anatomy DSL — Addendum 1 | Pending Review |
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function parseAnatomyYAML(raw) {
|
|
2
|
+
const doc = raw;
|
|
3
|
+
return {
|
|
4
|
+
root: toNamedNode(doc.node),
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
function toNamedNode(raw) {
|
|
8
|
+
return {
|
|
9
|
+
type: "named",
|
|
10
|
+
uri: raw.uri,
|
|
11
|
+
...(raw.styles ? { styles: toStyles(raw.styles) } : {}),
|
|
12
|
+
...(raw.edges ? { edges: raw.edges.map(toEdge) } : {}),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function toNode(raw) {
|
|
16
|
+
if ("uri" in raw && raw.uri) {
|
|
17
|
+
return toNamedNode(raw);
|
|
18
|
+
}
|
|
19
|
+
const anon = raw;
|
|
20
|
+
return {
|
|
21
|
+
type: "anonymous",
|
|
22
|
+
role: anon.role,
|
|
23
|
+
...(anon.styles ? { styles: toStyles(anon.styles) } : {}),
|
|
24
|
+
...(anon.edges ? { edges: anon.edges.map(toEdge) } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function toStyles(raw) {
|
|
28
|
+
return Object.entries(raw).map(([key, value]) => ({
|
|
29
|
+
key,
|
|
30
|
+
value: String(value),
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
function toEdge(raw) {
|
|
34
|
+
let target;
|
|
35
|
+
if (raw.switch) {
|
|
36
|
+
target = toSwitch(raw.switch);
|
|
37
|
+
}
|
|
38
|
+
else if (raw.node) {
|
|
39
|
+
target = toNode(raw.node);
|
|
40
|
+
}
|
|
41
|
+
else if (raw.uri) {
|
|
42
|
+
target = { type: "named", uri: raw.uri };
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
throw new Error("Edge must have node, uri, or switch");
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
target,
|
|
49
|
+
relation: toRelation(raw.relation),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function toSwitch(raw) {
|
|
53
|
+
return {
|
|
54
|
+
discriminator: raw.on,
|
|
55
|
+
cases: raw.cases.map(toSwitchCase),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function toSwitchCase(raw) {
|
|
59
|
+
if (raw.uri !== undefined) {
|
|
60
|
+
const node = { type: "named", uri: raw.uri };
|
|
61
|
+
return { value: raw.uri, node };
|
|
62
|
+
}
|
|
63
|
+
const node = toNode(raw.node);
|
|
64
|
+
const value = node.type === "named" ? node.uri : node.role;
|
|
65
|
+
return { value, node };
|
|
66
|
+
}
|
|
67
|
+
function toRelation(raw) {
|
|
68
|
+
return {
|
|
69
|
+
cardinality: raw.cardinality,
|
|
70
|
+
...(raw.slotName ? { slotName: raw.slotName } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const PREFIX = "@prefix : <http://anatomy-dsl.example.org/ontology#> .";
|
|
2
|
+
const INDENT = " ";
|
|
3
|
+
export function anatomyToTTL(spec) {
|
|
4
|
+
const lines = [PREFIX, ""];
|
|
5
|
+
lines.push("[] a :Specification ;");
|
|
6
|
+
lines.push(`${INDENT}:rootNode [`);
|
|
7
|
+
writeNode(lines, spec.root, 2);
|
|
8
|
+
lines.push(`${INDENT}] .`);
|
|
9
|
+
return `${lines.join("\n")}\n`;
|
|
10
|
+
}
|
|
11
|
+
function writeNode(lines, node, depth) {
|
|
12
|
+
if (node.type === "named") {
|
|
13
|
+
writeNamedNode(lines, node, depth);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
writeAnonymousNode(lines, node, depth);
|
|
17
|
+
}
|
|
18
|
+
const styles = node.styles ?? [];
|
|
19
|
+
const edges = node.edges ?? [];
|
|
20
|
+
if (styles.length > 0) {
|
|
21
|
+
writeStyles(lines, styles, depth, edges.length === 0);
|
|
22
|
+
}
|
|
23
|
+
if (edges.length > 0) {
|
|
24
|
+
writeEdges(lines, edges, depth);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function writeNamedNode(lines, node, depth) {
|
|
28
|
+
const indent = INDENT.repeat(depth);
|
|
29
|
+
lines.push(`${indent}a :NamedNode ;`);
|
|
30
|
+
const hasMore = (node.styles && node.styles.length > 0) ||
|
|
31
|
+
(node.edges && node.edges.length > 0);
|
|
32
|
+
lines.push(`${indent}:uri "${node.uri}"${hasMore ? " ;" : ""}`);
|
|
33
|
+
}
|
|
34
|
+
function writeAnonymousNode(lines, node, depth) {
|
|
35
|
+
const indent = INDENT.repeat(depth);
|
|
36
|
+
lines.push(`${indent}a :AnonymousNode ;`);
|
|
37
|
+
const hasMore = (node.styles && node.styles.length > 0) ||
|
|
38
|
+
(node.edges && node.edges.length > 0);
|
|
39
|
+
lines.push(`${indent}:role "${node.role}"${hasMore ? " ;" : ""}`);
|
|
40
|
+
}
|
|
41
|
+
function writeStyles(lines, styles, depth, isLast) {
|
|
42
|
+
const indent = INDENT.repeat(depth);
|
|
43
|
+
const innerIndent = INDENT.repeat(depth + 1);
|
|
44
|
+
lines.push(`${indent}:hasStyle`);
|
|
45
|
+
for (const [i, style] of styles.entries()) {
|
|
46
|
+
const sep = i < styles.length - 1 ? " ," : isLast ? "" : " ;";
|
|
47
|
+
lines.push(`${innerIndent}[ :styleKey "${style.key}" ; :styleValue "${style.value}" ]${sep}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function writeEdges(lines, edges, depth) {
|
|
51
|
+
const indent = INDENT.repeat(depth);
|
|
52
|
+
for (const [i, edge] of edges.entries()) {
|
|
53
|
+
if (i === 0) {
|
|
54
|
+
lines.push(`${indent}:hasEdge [`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
lines.push(`${indent}] , [`);
|
|
58
|
+
}
|
|
59
|
+
writeEdge(lines, edge, depth + 1);
|
|
60
|
+
}
|
|
61
|
+
lines.push(`${indent}]`);
|
|
62
|
+
}
|
|
63
|
+
function writeEdge(lines, edge, depth) {
|
|
64
|
+
const indent = INDENT.repeat(depth);
|
|
65
|
+
lines.push(`${indent}a :Edge ;`);
|
|
66
|
+
if (isSwitch(edge.target)) {
|
|
67
|
+
lines.push(`${indent}:edgeSwitch [`);
|
|
68
|
+
writeSwitch(lines, edge.target, depth + 1);
|
|
69
|
+
lines.push(`${indent}] ;`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
lines.push(`${indent}:edgeTarget [`);
|
|
73
|
+
writeNode(lines, edge.target, depth + 1);
|
|
74
|
+
lines.push(`${indent}] ;`);
|
|
75
|
+
}
|
|
76
|
+
writeRelation(lines, edge, depth);
|
|
77
|
+
}
|
|
78
|
+
function writeSwitch(lines, sw, depth) {
|
|
79
|
+
const indent = INDENT.repeat(depth);
|
|
80
|
+
lines.push(`${indent}a :Switch ;`);
|
|
81
|
+
lines.push(`${indent}:discriminator "${sw.discriminator}" ;`);
|
|
82
|
+
for (const [i, sc] of sw.cases.entries()) {
|
|
83
|
+
if (i === 0) {
|
|
84
|
+
lines.push(`${indent}:hasCase [`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
lines.push(`${indent}] , [`);
|
|
88
|
+
}
|
|
89
|
+
writeSwitchCase(lines, sc, depth + 1);
|
|
90
|
+
}
|
|
91
|
+
lines.push(`${indent}]`);
|
|
92
|
+
}
|
|
93
|
+
function writeSwitchCase(lines, sc, depth) {
|
|
94
|
+
const indent = INDENT.repeat(depth);
|
|
95
|
+
lines.push(`${indent}a :SwitchCase ;`);
|
|
96
|
+
lines.push(`${indent}:caseNode [`);
|
|
97
|
+
writeNode(lines, sc.node, depth + 1);
|
|
98
|
+
lines.push(`${indent}]`);
|
|
99
|
+
}
|
|
100
|
+
function writeRelation(lines, edge, depth) {
|
|
101
|
+
const indent = INDENT.repeat(depth);
|
|
102
|
+
lines.push(`${indent}:hasRelation [`);
|
|
103
|
+
const inner = INDENT.repeat(depth + 1);
|
|
104
|
+
lines.push(`${inner}a :Relation ;`);
|
|
105
|
+
if (edge.relation.slotName) {
|
|
106
|
+
lines.push(`${inner}:cardinality "${edge.relation.cardinality}" ;`);
|
|
107
|
+
lines.push(`${inner}:slotName "${edge.relation.slotName}"`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
lines.push(`${inner}:cardinality "${edge.relation.cardinality}"`);
|
|
111
|
+
}
|
|
112
|
+
lines.push(`${indent}]`);
|
|
113
|
+
}
|
|
114
|
+
function isSwitch(target) {
|
|
115
|
+
return "discriminator" in target;
|
|
116
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/parse.d.ts
ADDED
package/dist/parse.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function parseAnatomyYAML(raw) {
|
|
2
|
+
const doc = raw;
|
|
3
|
+
return {
|
|
4
|
+
root: toNamedNode(doc.node),
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
function toNamedNode(raw) {
|
|
8
|
+
return {
|
|
9
|
+
type: "named",
|
|
10
|
+
uri: raw.uri,
|
|
11
|
+
...(raw.styles ? { styles: toStyles(raw.styles) } : {}),
|
|
12
|
+
...(raw.edges ? { edges: raw.edges.map(toEdge) } : {}),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function toNode(raw) {
|
|
16
|
+
if ("uri" in raw && raw.uri) {
|
|
17
|
+
return toNamedNode(raw);
|
|
18
|
+
}
|
|
19
|
+
const anon = raw;
|
|
20
|
+
return {
|
|
21
|
+
type: "anonymous",
|
|
22
|
+
role: anon.role,
|
|
23
|
+
...(anon.styles ? { styles: toStyles(anon.styles) } : {}),
|
|
24
|
+
...(anon.edges ? { edges: anon.edges.map(toEdge) } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function toStyles(raw) {
|
|
28
|
+
return Object.entries(raw).map(([key, value]) => ({
|
|
29
|
+
key,
|
|
30
|
+
value: String(value),
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
function toEdge(raw) {
|
|
34
|
+
let target;
|
|
35
|
+
if (raw.switch) {
|
|
36
|
+
target = toSwitch(raw.switch);
|
|
37
|
+
}
|
|
38
|
+
else if (raw.node) {
|
|
39
|
+
target = toNode(raw.node);
|
|
40
|
+
}
|
|
41
|
+
else if (raw.uri) {
|
|
42
|
+
target = { type: "named", uri: raw.uri };
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
throw new Error("Edge must have node, uri, or switch");
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
target,
|
|
49
|
+
relation: toRelation(raw.relation),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function toSwitch(raw) {
|
|
53
|
+
return {
|
|
54
|
+
discriminator: raw.on,
|
|
55
|
+
cases: raw.cases.map(toSwitchCase),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function toSwitchCase(raw) {
|
|
59
|
+
if (raw.uri !== undefined) {
|
|
60
|
+
const node = { type: "named", uri: raw.uri };
|
|
61
|
+
return { value: raw.uri, node };
|
|
62
|
+
}
|
|
63
|
+
const node = toNode(raw.node);
|
|
64
|
+
const value = node.type === "named" ? node.uri : node.role;
|
|
65
|
+
return { value, node };
|
|
66
|
+
}
|
|
67
|
+
function toRelation(raw) {
|
|
68
|
+
return {
|
|
69
|
+
cardinality: raw.cardinality,
|
|
70
|
+
...(raw.slotName ? { slotName: raw.slotName } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const PREFIX = "@prefix : <http://anatomy-dsl.example.org/ontology#> .";
|
|
2
|
+
const INDENT = " ";
|
|
3
|
+
export function anatomyToTTL(spec) {
|
|
4
|
+
const lines = [PREFIX, ""];
|
|
5
|
+
lines.push("[] a :Specification ;");
|
|
6
|
+
lines.push(`${INDENT}:rootNode [`);
|
|
7
|
+
writeNode(lines, spec.root, 2);
|
|
8
|
+
lines.push(`${INDENT}] .`);
|
|
9
|
+
return `${lines.join("\n")}\n`;
|
|
10
|
+
}
|
|
11
|
+
function writeNode(lines, node, depth) {
|
|
12
|
+
if (node.type === "named") {
|
|
13
|
+
writeNamedNode(lines, node, depth);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
writeAnonymousNode(lines, node, depth);
|
|
17
|
+
}
|
|
18
|
+
const styles = node.styles ?? [];
|
|
19
|
+
const edges = node.edges ?? [];
|
|
20
|
+
if (styles.length > 0) {
|
|
21
|
+
writeStyles(lines, styles, depth, edges.length === 0);
|
|
22
|
+
}
|
|
23
|
+
if (edges.length > 0) {
|
|
24
|
+
writeEdges(lines, edges, depth);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function writeNamedNode(lines, node, depth) {
|
|
28
|
+
const indent = INDENT.repeat(depth);
|
|
29
|
+
lines.push(`${indent}a :NamedNode ;`);
|
|
30
|
+
const hasMore = (node.styles && node.styles.length > 0) || (node.edges && node.edges.length > 0);
|
|
31
|
+
lines.push(`${indent}:uri "${node.uri}"${hasMore ? " ;" : ""}`);
|
|
32
|
+
}
|
|
33
|
+
function writeAnonymousNode(lines, node, depth) {
|
|
34
|
+
const indent = INDENT.repeat(depth);
|
|
35
|
+
lines.push(`${indent}a :AnonymousNode ;`);
|
|
36
|
+
const hasMore = (node.styles && node.styles.length > 0) || (node.edges && node.edges.length > 0);
|
|
37
|
+
lines.push(`${indent}:role "${node.role}"${hasMore ? " ;" : ""}`);
|
|
38
|
+
}
|
|
39
|
+
function writeStyles(lines, styles, depth, isLast) {
|
|
40
|
+
const indent = INDENT.repeat(depth);
|
|
41
|
+
const innerIndent = INDENT.repeat(depth + 1);
|
|
42
|
+
lines.push(`${indent}:hasStyle`);
|
|
43
|
+
for (const [i, style] of styles.entries()) {
|
|
44
|
+
const sep = i < styles.length - 1 ? " ," : isLast ? "" : " ;";
|
|
45
|
+
lines.push(`${innerIndent}[ :styleKey "${style.key}" ; :styleValue "${style.value}" ]${sep}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function writeEdges(lines, edges, depth) {
|
|
49
|
+
const indent = INDENT.repeat(depth);
|
|
50
|
+
for (const [i, edge] of edges.entries()) {
|
|
51
|
+
if (i === 0) {
|
|
52
|
+
lines.push(`${indent}:hasEdge [`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
lines.push(`${indent}] , [`);
|
|
56
|
+
}
|
|
57
|
+
writeEdge(lines, edge, depth + 1);
|
|
58
|
+
}
|
|
59
|
+
lines.push(`${indent}]`);
|
|
60
|
+
}
|
|
61
|
+
function writeEdge(lines, edge, depth) {
|
|
62
|
+
const indent = INDENT.repeat(depth);
|
|
63
|
+
lines.push(`${indent}a :Edge ;`);
|
|
64
|
+
if (isSwitch(edge.target)) {
|
|
65
|
+
lines.push(`${indent}:edgeSwitch [`);
|
|
66
|
+
writeSwitch(lines, edge.target, depth + 1);
|
|
67
|
+
lines.push(`${indent}] ;`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
lines.push(`${indent}:edgeTarget [`);
|
|
71
|
+
writeNode(lines, edge.target, depth + 1);
|
|
72
|
+
lines.push(`${indent}] ;`);
|
|
73
|
+
}
|
|
74
|
+
writeRelation(lines, edge, depth);
|
|
75
|
+
}
|
|
76
|
+
function writeSwitch(lines, sw, depth) {
|
|
77
|
+
const indent = INDENT.repeat(depth);
|
|
78
|
+
lines.push(`${indent}a :Switch ;`);
|
|
79
|
+
lines.push(`${indent}:discriminator "${sw.discriminator}" ;`);
|
|
80
|
+
for (const [i, sc] of sw.cases.entries()) {
|
|
81
|
+
if (i === 0) {
|
|
82
|
+
lines.push(`${indent}:hasCase [`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
lines.push(`${indent}] , [`);
|
|
86
|
+
}
|
|
87
|
+
writeSwitchCase(lines, sc, depth + 1);
|
|
88
|
+
}
|
|
89
|
+
lines.push(`${indent}]`);
|
|
90
|
+
}
|
|
91
|
+
function writeSwitchCase(lines, sc, depth) {
|
|
92
|
+
const indent = INDENT.repeat(depth);
|
|
93
|
+
lines.push(`${indent}a :SwitchCase ;`);
|
|
94
|
+
lines.push(`${indent}:caseNode [`);
|
|
95
|
+
writeNode(lines, sc.node, depth + 1);
|
|
96
|
+
lines.push(`${indent}]`);
|
|
97
|
+
}
|
|
98
|
+
function writeRelation(lines, edge, depth) {
|
|
99
|
+
const indent = INDENT.repeat(depth);
|
|
100
|
+
lines.push(`${indent}:hasRelation [`);
|
|
101
|
+
const inner = INDENT.repeat(depth + 1);
|
|
102
|
+
lines.push(`${inner}a :Relation ;`);
|
|
103
|
+
if (edge.relation.slotName) {
|
|
104
|
+
lines.push(`${inner}:cardinality "${edge.relation.cardinality}" ;`);
|
|
105
|
+
lines.push(`${inner}:slotName "${edge.relation.slotName}"`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
lines.push(`${inner}:cardinality "${edge.relation.cardinality}"`);
|
|
109
|
+
}
|
|
110
|
+
lines.push(`${indent}]`);
|
|
111
|
+
}
|
|
112
|
+
function isSwitch(target) {
|
|
113
|
+
return "discriminator" in target;
|
|
114
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface Specification {
|
|
2
|
+
root: NamedNode;
|
|
3
|
+
}
|
|
4
|
+
export interface NamedNode {
|
|
5
|
+
type: "named";
|
|
6
|
+
uri: string;
|
|
7
|
+
styles?: Style[];
|
|
8
|
+
edges?: Edge[];
|
|
9
|
+
}
|
|
10
|
+
export interface AnonymousNode {
|
|
11
|
+
type: "anonymous";
|
|
12
|
+
role: string;
|
|
13
|
+
styles?: Style[];
|
|
14
|
+
edges?: Edge[];
|
|
15
|
+
}
|
|
16
|
+
export type Node = NamedNode | AnonymousNode;
|
|
17
|
+
export interface Edge {
|
|
18
|
+
target: Node | Switch;
|
|
19
|
+
relation: Relation;
|
|
20
|
+
}
|
|
21
|
+
export interface Relation {
|
|
22
|
+
cardinality: string;
|
|
23
|
+
slotName?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface Style {
|
|
26
|
+
key: string;
|
|
27
|
+
value: string;
|
|
28
|
+
}
|
|
29
|
+
export interface Switch {
|
|
30
|
+
discriminator: "props" | "internal" | "override";
|
|
31
|
+
cases: SwitchCase[];
|
|
32
|
+
}
|
|
33
|
+
export interface SwitchCase {
|
|
34
|
+
value: string;
|
|
35
|
+
node: Node;
|
|
36
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface Specification {
|
|
2
|
+
root: NamedNode;
|
|
3
|
+
}
|
|
4
|
+
export interface NamedNode {
|
|
5
|
+
type: "named";
|
|
6
|
+
uri: string;
|
|
7
|
+
styles?: Style[];
|
|
8
|
+
edges?: Edge[];
|
|
9
|
+
}
|
|
10
|
+
export interface AnonymousNode {
|
|
11
|
+
type: "anonymous";
|
|
12
|
+
role: string;
|
|
13
|
+
styles?: Style[];
|
|
14
|
+
edges?: Edge[];
|
|
15
|
+
}
|
|
16
|
+
export type Node = NamedNode | AnonymousNode;
|
|
17
|
+
export interface Edge {
|
|
18
|
+
target: Node | Switch;
|
|
19
|
+
relation: Relation;
|
|
20
|
+
}
|
|
21
|
+
export interface Relation {
|
|
22
|
+
cardinality: string;
|
|
23
|
+
slotName?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface Style {
|
|
26
|
+
key: string;
|
|
27
|
+
value: string;
|
|
28
|
+
}
|
|
29
|
+
export interface Switch {
|
|
30
|
+
discriminator: "props" | "internal" | "override";
|
|
31
|
+
cases: SwitchCase[];
|
|
32
|
+
}
|
|
33
|
+
export interface SwitchCase {
|
|
34
|
+
value: string;
|
|
35
|
+
node: Node;
|
|
36
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@canonical/anatomy-dsl",
|
|
3
|
+
"description": "Anatomy DSL meta-model: TypeScript types mirroring the OWL ontology, and a YAML-to-Turtle transform",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/types/index.d.ts",
|
|
11
|
+
"import": "./dist/esm/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"license": "LGPL-3.0",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.build.json",
|
|
20
|
+
"check": "biome check && tsc --noEmit",
|
|
21
|
+
"check:fix": "biome check --write",
|
|
22
|
+
"check:ts": "tsc --noEmit",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"test:coverage": "vitest run --coverage"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@biomejs/biome": "^2.4.6",
|
|
29
|
+
"@canonical/biome-config": "^0.17.1",
|
|
30
|
+
"@canonical/typescript-config": "^0.17.1",
|
|
31
|
+
"expect-type": "^1.3.0",
|
|
32
|
+
"typescript": "^5.9.3",
|
|
33
|
+
"vitest": "^4.0.18",
|
|
34
|
+
"yaml": "^2.7.1"
|
|
35
|
+
}
|
|
36
|
+
}
|