dirvi-lib 0.8.1 → 0.8.2
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/app/app-api.d.ts +74 -0
- package/dist/app/app-api.d.ts.map +1 -0
- package/dist/app/app-api.js +11 -0
- package/dist/app/effect-to-action.d.ts +6 -0
- package/dist/app/effect-to-action.d.ts.map +1 -0
- package/dist/app/effect-to-action.js +74 -0
- package/dist/app/index.d.ts +6 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +5 -0
- package/dist/app/reducer-action.d.ts +22 -0
- package/dist/app/reducer-action.d.ts.map +1 -0
- package/dist/app/reducer.d.ts +5 -0
- package/dist/app/reducer.d.ts.map +1 -0
- package/dist/app/reducer.js +79 -0
- package/dist/app/view-api.d.ts +11 -0
- package/dist/app/view-api.d.ts.map +1 -0
- package/dist/index.d.ts +4 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -7
- package/dist/input/application/index.d.ts.map +1 -0
- package/dist/input/application/intent-to-effect-no-state.test.d.ts.map +1 -0
- package/dist/input/application/intent-to-effect.d.ts +14 -0
- package/dist/input/application/intent-to-effect.d.ts.map +1 -0
- package/dist/{tree-surfer-ui → input}/application/intent-to-effect.js +36 -17
- package/dist/input/application/parse-command.d.ts +14 -0
- package/dist/input/application/parse-command.d.ts.map +1 -0
- package/dist/{tree-surfer-ui → input}/application/parse-command.js +13 -1
- package/dist/input/application/user-input-to-intent.d.ts.map +1 -0
- package/dist/input/application/user-input-to-intent.test.d.ts.map +1 -0
- package/dist/input/domain/effect.d.ts +55 -0
- package/dist/input/domain/effect.d.ts.map +1 -0
- package/dist/input/domain/index.d.ts.map +1 -0
- package/dist/input/domain/input-mode-state.d.ts.map +1 -0
- package/dist/input/domain/intent.d.ts.map +1 -0
- package/dist/input/domain/user-input.d.ts.map +1 -0
- package/dist/trees/cursor.d.ts.map +1 -0
- package/dist/trees/fold-node/deprecated/fold-node.impl.d.ts +10 -0
- package/dist/trees/fold-node/deprecated/fold-node.impl.d.ts.map +1 -0
- package/dist/trees/fold-node/deprecated/fold-node.impl.js +248 -0
- package/dist/trees/fold-node/deprecated/fold-node.test.d.ts +2 -0
- package/dist/trees/fold-node/deprecated/fold-node.test.d.ts.map +1 -0
- package/dist/trees/fold-node/deprecated/fold-node.test.js +130 -0
- package/dist/{tree-surfer/fold-node → trees/fold-node/deprecated}/fold-node.types.d.ts +15 -76
- package/dist/trees/fold-node/deprecated/fold-node.types.d.ts.map +1 -0
- package/dist/trees/fold-node/fold-node.impl.d.ts +16 -0
- package/dist/trees/fold-node/fold-node.impl.d.ts.map +1 -0
- package/dist/trees/fold-node/fold-node.impl.js +451 -0
- package/dist/trees/fold-node/fold-node.model.d.ts +120 -0
- package/dist/trees/fold-node/fold-node.model.d.ts.map +1 -0
- package/dist/trees/fold-node/fold-node.model.js +80 -0
- package/dist/trees/fold-node/fold-node.types.d.ts +206 -0
- package/dist/trees/fold-node/fold-node.types.d.ts.map +1 -0
- package/dist/trees/fold-node/index.d.ts +4 -0
- package/dist/trees/fold-node/index.d.ts.map +1 -0
- package/dist/trees/fold-node/index.js +3 -0
- package/dist/trees/index.d.ts +9 -0
- package/dist/trees/index.d.ts.map +1 -0
- package/dist/trees/index.js +8 -0
- package/dist/trees/nav-node/nav-node.impl.d.ts +29 -0
- package/dist/trees/nav-node/nav-node.impl.d.ts.map +1 -0
- package/dist/{tree-surfer → trees}/nav-node/nav-node.impl.js +89 -42
- package/dist/trees/nav-node/nav-node.types.d.ts +103 -0
- package/dist/trees/nav-node/nav-node.types.d.ts.map +1 -0
- package/dist/trees/state/state.impl.d.ts +8 -0
- package/dist/trees/state/state.impl.d.ts.map +1 -0
- package/dist/trees/state/state.impl.js +128 -0
- package/dist/trees/state/state.schemas.d.ts +25 -0
- package/dist/trees/state/state.schemas.d.ts.map +1 -0
- package/dist/trees/state/state.schemas.js +17 -0
- package/dist/trees/state/state.types.d.ts +36 -0
- package/dist/trees/state/state.types.d.ts.map +1 -0
- package/dist/trees/state/state.types.js +1 -0
- package/dist/trees/tree-node/deprecated/tree-node.arb.d.ts.map +1 -0
- package/dist/trees/tree-node/deprecated/tree-node.arb.debug.test.d.ts.map +1 -0
- package/dist/{tree-surfer/tree-node → trees/tree-node/deprecated}/tree-node.arb.debug.test.js +1 -1
- package/dist/trees/tree-node/deprecated/tree-node.impl.d.ts.map +1 -0
- package/dist/trees/tree-node/deprecated/tree-node.schemas.d.ts.map +1 -0
- package/dist/trees/tree-node/deprecated/tree-node.test.d.ts.map +1 -0
- package/dist/trees/tree-node/deprecated/tree-node.types.d.ts.map +1 -0
- package/dist/trees/tree-node/deprecated/tree-node.types.js +1 -0
- package/dist/trees/tree-node/index.d.ts +4 -0
- package/dist/trees/tree-node/index.d.ts.map +1 -0
- package/dist/trees/tree-node/index.js +3 -0
- package/dist/trees/tree-node/tree-node.arb.d.ts +104 -0
- package/dist/trees/tree-node/tree-node.arb.d.ts.map +1 -0
- package/dist/trees/tree-node/tree-node.arb.js +191 -0
- package/dist/trees/tree-node/tree-node.impl.d.ts +15 -0
- package/dist/trees/tree-node/tree-node.impl.d.ts.map +1 -0
- package/dist/trees/tree-node/tree-node.impl.js +286 -0
- package/dist/trees/tree-node/tree-node.model.d.ts +78 -0
- package/dist/trees/tree-node/tree-node.model.d.ts.map +1 -0
- package/dist/trees/tree-node/tree-node.model.js +37 -0
- package/dist/trees/tree-node/tree-node.types.d.ts +419 -0
- package/dist/trees/tree-node/tree-node.types.d.ts.map +1 -0
- package/dist/trees/tree-node/tree-node.types.js +1 -0
- package/package.json +1 -1
- package/dist/tree-surfer/cursor.d.ts.map +0 -1
- package/dist/tree-surfer/fold-node/fold-node.arb.d.ts +0 -2
- package/dist/tree-surfer/fold-node/fold-node.arb.d.ts.map +0 -1
- package/dist/tree-surfer/fold-node/fold-node.impl.d.ts +0 -4
- package/dist/tree-surfer/fold-node/fold-node.impl.d.ts.map +0 -1
- package/dist/tree-surfer/fold-node/fold-node.impl.js +0 -149
- package/dist/tree-surfer/fold-node/fold-node.schema.d.ts +0 -5
- package/dist/tree-surfer/fold-node/fold-node.schema.d.ts.map +0 -1
- package/dist/tree-surfer/fold-node/fold-node.schema.js +0 -9
- package/dist/tree-surfer/fold-node/fold-node.types.d.ts.map +0 -1
- package/dist/tree-surfer/fold-node/tree-node.test.d.ts.map +0 -1
- package/dist/tree-surfer/fold-node/tree-node.test.js +0 -90
- package/dist/tree-surfer/index.d.ts +0 -13
- package/dist/tree-surfer/index.d.ts.map +0 -1
- package/dist/tree-surfer/index.js +0 -12
- package/dist/tree-surfer/nav-node/nav-node.impl.d.ts +0 -6
- package/dist/tree-surfer/nav-node/nav-node.impl.d.ts.map +0 -1
- package/dist/tree-surfer/nav-node/nav-node.types.d.ts +0 -70
- package/dist/tree-surfer/nav-node/nav-node.types.d.ts.map +0 -1
- package/dist/tree-surfer/state/state.impl.d.ts +0 -7
- package/dist/tree-surfer/state/state.impl.d.ts.map +0 -1
- package/dist/tree-surfer/state/state.impl.js +0 -89
- package/dist/tree-surfer/state/state.schemas.d.ts +0 -9
- package/dist/tree-surfer/state/state.schemas.d.ts.map +0 -1
- package/dist/tree-surfer/state/state.schemas.js +0 -8
- package/dist/tree-surfer/state/state.types.d.ts +0 -21
- package/dist/tree-surfer/state/state.types.d.ts.map +0 -1
- package/dist/tree-surfer/tree-node/tree-node.arb.d.ts.map +0 -1
- package/dist/tree-surfer/tree-node/tree-node.arb.debug.test.d.ts.map +0 -1
- package/dist/tree-surfer/tree-node/tree-node.impl.d.ts.map +0 -1
- package/dist/tree-surfer/tree-node/tree-node.schemas.d.ts.map +0 -1
- package/dist/tree-surfer/tree-node/tree-node.test.d.ts +0 -2
- package/dist/tree-surfer/tree-node/tree-node.test.d.ts.map +0 -1
- package/dist/tree-surfer/tree-node/tree-node.types.d.ts.map +0 -1
- package/dist/tree-surfer-ui/application/index.d.ts.map +0 -1
- package/dist/tree-surfer-ui/application/intent-to-effect-no-state.test.d.ts.map +0 -1
- package/dist/tree-surfer-ui/application/intent-to-effect.d.ts +0 -10
- package/dist/tree-surfer-ui/application/intent-to-effect.d.ts.map +0 -1
- package/dist/tree-surfer-ui/application/parse-command.d.ts +0 -4
- package/dist/tree-surfer-ui/application/parse-command.d.ts.map +0 -1
- package/dist/tree-surfer-ui/application/user-input-to-intent.d.ts.map +0 -1
- package/dist/tree-surfer-ui/application/user-input-to-intent.test.d.ts.map +0 -1
- package/dist/tree-surfer-ui/domain/effect.d.ts +0 -43
- package/dist/tree-surfer-ui/domain/effect.d.ts.map +0 -1
- package/dist/tree-surfer-ui/domain/index.d.ts.map +0 -1
- package/dist/tree-surfer-ui/domain/input-mode-state.d.ts.map +0 -1
- package/dist/tree-surfer-ui/domain/intent.d.ts.map +0 -1
- package/dist/tree-surfer-ui/domain/user-input.d.ts.map +0 -1
- /package/dist/{tree-surfer-ui/domain/effect.js → app/reducer-action.js} +0 -0
- /package/dist/{tree-surfer-ui/domain/input-mode-state.js → app/view-api.js} +0 -0
- /package/dist/{tree-surfer-ui → input}/application/index.d.ts +0 -0
- /package/dist/{tree-surfer-ui → input}/application/index.js +0 -0
- /package/dist/{tree-surfer-ui → input}/application/intent-to-effect-no-state.test.d.ts +0 -0
- /package/dist/{tree-surfer-ui → input}/application/intent-to-effect-no-state.test.js +0 -0
- /package/dist/{tree-surfer-ui → input}/application/user-input-to-intent.d.ts +0 -0
- /package/dist/{tree-surfer-ui → input}/application/user-input-to-intent.js +0 -0
- /package/dist/{tree-surfer-ui → input}/application/user-input-to-intent.test.d.ts +0 -0
- /package/dist/{tree-surfer-ui → input}/application/user-input-to-intent.test.js +0 -0
- /package/dist/{tree-surfer-ui/domain/intent.js → input/domain/effect.js} +0 -0
- /package/dist/{tree-surfer-ui → input}/domain/index.d.ts +0 -0
- /package/dist/{tree-surfer-ui → input}/domain/index.js +0 -0
- /package/dist/{tree-surfer-ui → input}/domain/input-mode-state.d.ts +0 -0
- /package/dist/{tree-surfer-ui/domain/user-input.js → input/domain/input-mode-state.js} +0 -0
- /package/dist/{tree-surfer-ui → input}/domain/intent.d.ts +0 -0
- /package/dist/{tree-surfer/fold-node/fold-node.arb.js → input/domain/intent.js} +0 -0
- /package/dist/{tree-surfer-ui → input}/domain/user-input.d.ts +0 -0
- /package/dist/{tree-surfer/fold-node/fold-node.types.js → input/domain/user-input.js} +0 -0
- /package/dist/{tree-surfer → trees}/cursor.d.ts +0 -0
- /package/dist/{tree-surfer → trees}/cursor.js +0 -0
- /package/dist/{tree-surfer/nav-node/nav-node.types.js → trees/fold-node/deprecated/fold-node.types.js} +0 -0
- /package/dist/{tree-surfer/state/state.types.js → trees/fold-node/fold-node.types.js} +0 -0
- /package/dist/{tree-surfer/tree-node/tree-node.types.js → trees/nav-node/nav-node.types.js} +0 -0
- /package/dist/{tree-surfer/tree-node → trees/tree-node/deprecated}/tree-node.arb.d.ts +0 -0
- /package/dist/{tree-surfer/tree-node → trees/tree-node/deprecated}/tree-node.arb.debug.test.d.ts +0 -0
- /package/dist/{tree-surfer/tree-node → trees/tree-node/deprecated}/tree-node.arb.js +0 -0
- /package/dist/{tree-surfer/tree-node → trees/tree-node/deprecated}/tree-node.impl.d.ts +0 -0
- /package/dist/{tree-surfer/tree-node → trees/tree-node/deprecated}/tree-node.impl.js +0 -0
- /package/dist/{tree-surfer/tree-node → trees/tree-node/deprecated}/tree-node.schemas.d.ts +0 -0
- /package/dist/{tree-surfer/tree-node → trees/tree-node/deprecated}/tree-node.schemas.js +0 -0
- /package/dist/{tree-surfer/fold-node → trees/tree-node/deprecated}/tree-node.test.d.ts +0 -0
- /package/dist/{tree-surfer/tree-node → trees/tree-node/deprecated}/tree-node.test.js +0 -0
- /package/dist/{tree-surfer/tree-node → trees/tree-node/deprecated}/tree-node.types.d.ts +0 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
function toData(folds) {
|
|
2
|
+
return folds;
|
|
3
|
+
}
|
|
4
|
+
function fromData(data) {
|
|
5
|
+
return data;
|
|
6
|
+
}
|
|
7
|
+
function isValidFoldIndex(index) {
|
|
8
|
+
return Number.isSafeInteger(index) && index >= 0;
|
|
9
|
+
}
|
|
10
|
+
function hasUniqueValues(values) {
|
|
11
|
+
return new Set(values).size === values.length;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Creates operations for an immutable collection of independently active fold
|
|
15
|
+
* trees.
|
|
16
|
+
*
|
|
17
|
+
* Each fold tree preserves its own hidden-entry definition. An entry is hidden
|
|
18
|
+
* by the aggregate when at least one active fold tree hides it.
|
|
19
|
+
*/
|
|
20
|
+
export function createFoldsApi(foldTreeApi) {
|
|
21
|
+
function createEmptyFoldTree(rootId) {
|
|
22
|
+
const tree = foldTreeApi.createLoadedBranch(rootId, [], []);
|
|
23
|
+
/*
|
|
24
|
+
* An empty child collection cannot contain duplicate IDs. Failure would
|
|
25
|
+
* therefore indicate a broken TreeNodeApi implementation or contract.
|
|
26
|
+
*/
|
|
27
|
+
if (tree === undefined) {
|
|
28
|
+
throw new Error('Could not create an empty fold tree');
|
|
29
|
+
}
|
|
30
|
+
return tree;
|
|
31
|
+
}
|
|
32
|
+
function getSlotAtIndex(folds, index) {
|
|
33
|
+
if (!isValidFoldIndex(index)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return toData(folds).slots[index];
|
|
37
|
+
}
|
|
38
|
+
function withSlotAtIndex(folds, index, foldData) {
|
|
39
|
+
if (!isValidFoldIndex(index)) {
|
|
40
|
+
throw new RangeError('Fold index must be a non-negative safe integer');
|
|
41
|
+
}
|
|
42
|
+
const data = toData(folds);
|
|
43
|
+
const slots = [...data.slots];
|
|
44
|
+
while (slots.length <= index) {
|
|
45
|
+
slots.push(undefined);
|
|
46
|
+
}
|
|
47
|
+
slots[index] = foldData;
|
|
48
|
+
/*
|
|
49
|
+
* `setAtIndex` permits replacing the primary slot. Other public operations
|
|
50
|
+
* preserve the invariant that slot zero is always occupied.
|
|
51
|
+
*/
|
|
52
|
+
return fromData({
|
|
53
|
+
slots: slots,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function updateSlotAtIndex(folds, index, update) {
|
|
57
|
+
const foldData = getSlotAtIndex(folds, index);
|
|
58
|
+
if (foldData === undefined) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const updatedFoldData = update(foldData);
|
|
62
|
+
if (updatedFoldData === foldData) {
|
|
63
|
+
return folds;
|
|
64
|
+
}
|
|
65
|
+
return withSlotAtIndex(folds, index, updatedFoldData);
|
|
66
|
+
}
|
|
67
|
+
function removeSlotAtIndex(folds, index) {
|
|
68
|
+
const data = toData(folds);
|
|
69
|
+
const slots = [...data.slots];
|
|
70
|
+
slots[index] = undefined;
|
|
71
|
+
/*
|
|
72
|
+
* Trailing unoccupied slots carry no information, so omit them from the
|
|
73
|
+
* immutable runtime representation.
|
|
74
|
+
*/
|
|
75
|
+
while (slots.length > 1 && slots[slots.length - 1] === undefined) {
|
|
76
|
+
slots.pop();
|
|
77
|
+
}
|
|
78
|
+
return fromData({
|
|
79
|
+
slots: slots,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function getLoadedChildren(tree) {
|
|
83
|
+
const children = foldTreeApi.getLoadedChildren(tree);
|
|
84
|
+
return children === undefined ? undefined : [...children];
|
|
85
|
+
}
|
|
86
|
+
function createEmptyFoldNode(id) {
|
|
87
|
+
const tree = foldTreeApi.createLoadedBranch(id, [], []);
|
|
88
|
+
if (tree === undefined) {
|
|
89
|
+
throw new Error('Could not create an empty fold node');
|
|
90
|
+
}
|
|
91
|
+
return tree;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Creates structural fold-tree nodes for `path` where they do not already
|
|
95
|
+
* exist. A fold tree structurally retains descendants even when an entry is
|
|
96
|
+
* hidden at its parent, so paths may pass through hidden entry IDs.
|
|
97
|
+
*/
|
|
98
|
+
function ensurePath(tree, path) {
|
|
99
|
+
if (path.length === 0) {
|
|
100
|
+
return tree;
|
|
101
|
+
}
|
|
102
|
+
const children = getLoadedChildren(tree);
|
|
103
|
+
if (children === undefined) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const childId = path[0];
|
|
107
|
+
if (childId === undefined) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const childIndex = children.findIndex((child) => foldTreeApi.id(child) === childId);
|
|
111
|
+
const child = childIndex === -1 ? createEmptyFoldNode(childId) : children[childIndex];
|
|
112
|
+
if (child === undefined) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
const updatedChild = ensurePath(child, path.slice(1));
|
|
116
|
+
if (updatedChild === undefined) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
if (childIndex === -1) {
|
|
120
|
+
return foldTreeApi.withLoadedChildren(tree, [...children, updatedChild]);
|
|
121
|
+
}
|
|
122
|
+
if (updatedChild === child) {
|
|
123
|
+
return tree;
|
|
124
|
+
}
|
|
125
|
+
const updatedChildren = [...children];
|
|
126
|
+
updatedChildren[childIndex] = updatedChild;
|
|
127
|
+
return foldTreeApi.withLoadedChildren(tree, updatedChildren);
|
|
128
|
+
}
|
|
129
|
+
function isEmptyFoldNode(tree) {
|
|
130
|
+
const children = getLoadedChildren(tree);
|
|
131
|
+
if (children === undefined) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
return foldTreeApi.value(tree).length === 0 && children.length === 0;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Updates one existing fold-tree node and prunes empty sparse descendants on
|
|
138
|
+
* the path back to the root.
|
|
139
|
+
*
|
|
140
|
+
* The root is always retained, even if it becomes empty.
|
|
141
|
+
*/
|
|
142
|
+
function updateAtPathAndPrune(tree, path, update) {
|
|
143
|
+
if (path.length === 0) {
|
|
144
|
+
return update(tree);
|
|
145
|
+
}
|
|
146
|
+
const children = getLoadedChildren(tree);
|
|
147
|
+
if (children === undefined) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
const childId = path[0];
|
|
151
|
+
if (childId === undefined) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const childIndex = children.findIndex((child) => foldTreeApi.id(child) === childId);
|
|
155
|
+
if (childIndex === -1) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
const child = children[childIndex];
|
|
159
|
+
if (child === undefined) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const updatedChild = updateAtPathAndPrune(child, path.slice(1), update);
|
|
163
|
+
if (updatedChild === undefined) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
if (updatedChild === child) {
|
|
167
|
+
return tree;
|
|
168
|
+
}
|
|
169
|
+
const childIsEmpty = isEmptyFoldNode(updatedChild);
|
|
170
|
+
if (childIsEmpty === undefined) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
const updatedChildren = [...children];
|
|
174
|
+
if (childIsEmpty) {
|
|
175
|
+
updatedChildren.splice(childIndex, 1);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
updatedChildren[childIndex] = updatedChild;
|
|
179
|
+
}
|
|
180
|
+
return foldTreeApi.withLoadedChildren(tree, updatedChildren);
|
|
181
|
+
}
|
|
182
|
+
function foldTreeToModel(tree) {
|
|
183
|
+
return foldTreeApi.match(tree, {
|
|
184
|
+
leaf: () => {
|
|
185
|
+
throw new Error('Fold trees must contain loaded branch nodes');
|
|
186
|
+
},
|
|
187
|
+
unloadedBranch: () => {
|
|
188
|
+
throw new Error('Fold trees must contain loaded branch nodes');
|
|
189
|
+
},
|
|
190
|
+
loadedBranch: ({ id, value, children }) => ({
|
|
191
|
+
id,
|
|
192
|
+
hiddenEntryIds: [...value],
|
|
193
|
+
children: [...children].map(foldTreeToModel),
|
|
194
|
+
}),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
function foldSlotToModel(foldData) {
|
|
198
|
+
return {
|
|
199
|
+
name: foldData.name,
|
|
200
|
+
description: foldData.description,
|
|
201
|
+
active: foldData.active,
|
|
202
|
+
tree: foldTreeToModel(foldData.tree),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function foldTreeFromModel(model) {
|
|
206
|
+
if (!hasUniqueValues(model.hiddenEntryIds)) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
const children = [];
|
|
210
|
+
for (const childModel of model.children) {
|
|
211
|
+
const child = foldTreeFromModel(childModel);
|
|
212
|
+
if (child === undefined) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
children.push(child);
|
|
216
|
+
}
|
|
217
|
+
return foldTreeApi.createLoadedBranch(model.id, [...model.hiddenEntryIds], children);
|
|
218
|
+
}
|
|
219
|
+
const api = {
|
|
220
|
+
create(rootId, initialPrimaryInfo) {
|
|
221
|
+
return fromData({
|
|
222
|
+
slots: [
|
|
223
|
+
{
|
|
224
|
+
name: initialPrimaryInfo.name,
|
|
225
|
+
description: initialPrimaryInfo.description,
|
|
226
|
+
active: true,
|
|
227
|
+
tree: createEmptyFoldTree(rootId),
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
getInfoAtIndex(folds, index) {
|
|
233
|
+
const foldData = getSlotAtIndex(folds, index);
|
|
234
|
+
if (foldData === undefined) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
name: foldData.name,
|
|
239
|
+
description: foldData.description,
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
isActiveAtIndex(folds, index) {
|
|
243
|
+
return getSlotAtIndex(folds, index)?.active;
|
|
244
|
+
},
|
|
245
|
+
hasAtIndex(folds, index) {
|
|
246
|
+
return getSlotAtIndex(folds, index) !== undefined;
|
|
247
|
+
},
|
|
248
|
+
*indexes(folds) {
|
|
249
|
+
const { slots } = toData(folds);
|
|
250
|
+
for (let index = 0; index < slots.length; index += 1) {
|
|
251
|
+
if (slots[index] !== undefined) {
|
|
252
|
+
yield index;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
setAtIndex(folds, index, foldData) {
|
|
257
|
+
if (!isValidFoldIndex(index)) {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
const tree = foldTreeFromModel(foldData.tree);
|
|
261
|
+
if (tree === undefined) {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
return withSlotAtIndex(folds, index, {
|
|
265
|
+
name: foldData.name,
|
|
266
|
+
description: foldData.description,
|
|
267
|
+
active: foldData.active,
|
|
268
|
+
tree,
|
|
269
|
+
});
|
|
270
|
+
},
|
|
271
|
+
setAdditionalFoldAtIndex(folds, index, rootId, info) {
|
|
272
|
+
if (!isValidFoldIndex(index) || index === 0) {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
return withSlotAtIndex(folds, index, {
|
|
276
|
+
name: info.name,
|
|
277
|
+
description: info.description,
|
|
278
|
+
active: true,
|
|
279
|
+
tree: createEmptyFoldTree(rootId),
|
|
280
|
+
});
|
|
281
|
+
},
|
|
282
|
+
removeAdditionalFoldAtIndex(folds, index) {
|
|
283
|
+
if (!isValidFoldIndex(index) || index === 0) {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
if (getSlotAtIndex(folds, index) === undefined) {
|
|
287
|
+
return folds;
|
|
288
|
+
}
|
|
289
|
+
return removeSlotAtIndex(folds, index);
|
|
290
|
+
},
|
|
291
|
+
updateInfoAtIndex(folds, index, update) {
|
|
292
|
+
return updateSlotAtIndex(folds, index, (foldData) => {
|
|
293
|
+
const name = update.name ?? foldData.name;
|
|
294
|
+
const description = update.description === undefined
|
|
295
|
+
? foldData.description
|
|
296
|
+
: update.description;
|
|
297
|
+
if (name === foldData.name && description === foldData.description) {
|
|
298
|
+
return foldData;
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
...foldData,
|
|
302
|
+
name,
|
|
303
|
+
description,
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
activateAtIndex(folds, index) {
|
|
308
|
+
return updateSlotAtIndex(folds, index, (foldData) => foldData.active ? foldData : { ...foldData, active: true });
|
|
309
|
+
},
|
|
310
|
+
deactivateAtIndex(folds, index) {
|
|
311
|
+
return updateSlotAtIndex(folds, index, (foldData) => !foldData.active ? foldData : { ...foldData, active: false });
|
|
312
|
+
},
|
|
313
|
+
toggleActiveAtIndex(folds, index) {
|
|
314
|
+
return updateSlotAtIndex(folds, index, (foldData) => ({
|
|
315
|
+
...foldData,
|
|
316
|
+
active: !foldData.active,
|
|
317
|
+
}));
|
|
318
|
+
},
|
|
319
|
+
isEntryHiddenInFoldAtPath(folds, index, path, entryId) {
|
|
320
|
+
const foldData = getSlotAtIndex(folds, index);
|
|
321
|
+
if (foldData === undefined) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
return (foldTreeApi.selectAtPath(foldData.tree, path, (node) => foldTreeApi.value(node).includes(entryId)) ?? false);
|
|
325
|
+
},
|
|
326
|
+
isEntryHiddenAtPath(folds, path, entryId) {
|
|
327
|
+
for (const index of api.indexes(folds)) {
|
|
328
|
+
if (api.isActiveAtIndex(folds, index) === true &&
|
|
329
|
+
api.isEntryHiddenInFoldAtPath(folds, index, path, entryId)) {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return false;
|
|
334
|
+
},
|
|
335
|
+
hiddenEntryIdsAtPath(folds, index, path) {
|
|
336
|
+
const foldData = getSlotAtIndex(folds, index);
|
|
337
|
+
if (foldData === undefined) {
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
return foldTreeApi.selectAtPath(foldData.tree, path, (node) => [
|
|
341
|
+
...foldTreeApi.value(node),
|
|
342
|
+
]);
|
|
343
|
+
},
|
|
344
|
+
hideEntryAtPath(folds, index, path, entryId) {
|
|
345
|
+
const foldData = getSlotAtIndex(folds, index);
|
|
346
|
+
if (foldData === undefined) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
if (api.isEntryHiddenInFoldAtPath(folds, index, path, entryId)) {
|
|
350
|
+
return folds;
|
|
351
|
+
}
|
|
352
|
+
const treeWithPath = ensurePath(foldData.tree, path);
|
|
353
|
+
if (treeWithPath === undefined) {
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
const updatedTree = updateAtPathAndPrune(treeWithPath, path, (node) => foldTreeApi.withValue(node, [...foldTreeApi.value(node), entryId]));
|
|
357
|
+
if (updatedTree === undefined) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
return withSlotAtIndex(folds, index, {
|
|
361
|
+
...foldData,
|
|
362
|
+
tree: updatedTree,
|
|
363
|
+
});
|
|
364
|
+
},
|
|
365
|
+
showEntryAtPath(folds, index, path, entryId) {
|
|
366
|
+
const foldData = getSlotAtIndex(folds, index);
|
|
367
|
+
if (foldData === undefined) {
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
const updatedTree = updateAtPathAndPrune(foldData.tree, path, (node) => {
|
|
371
|
+
const hiddenEntryIds = foldTreeApi.value(node);
|
|
372
|
+
if (!hiddenEntryIds.includes(entryId)) {
|
|
373
|
+
return node;
|
|
374
|
+
}
|
|
375
|
+
return foldTreeApi.withValue(node, hiddenEntryIds.filter((id) => id !== entryId));
|
|
376
|
+
});
|
|
377
|
+
if (updatedTree === undefined) {
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
if (updatedTree === foldData.tree) {
|
|
381
|
+
return folds;
|
|
382
|
+
}
|
|
383
|
+
return withSlotAtIndex(folds, index, {
|
|
384
|
+
...foldData,
|
|
385
|
+
tree: updatedTree,
|
|
386
|
+
});
|
|
387
|
+
},
|
|
388
|
+
showAllHiddenEntriesAtPath(folds, index, path) {
|
|
389
|
+
const foldData = getSlotAtIndex(folds, index);
|
|
390
|
+
if (foldData === undefined) {
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
const updatedTree = updateAtPathAndPrune(foldData.tree, path, (node) => {
|
|
394
|
+
if (foldTreeApi.value(node).length === 0) {
|
|
395
|
+
return node;
|
|
396
|
+
}
|
|
397
|
+
return foldTreeApi.withValue(node, []);
|
|
398
|
+
});
|
|
399
|
+
if (updatedTree === undefined) {
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
if (updatedTree === foldData.tree) {
|
|
403
|
+
return folds;
|
|
404
|
+
}
|
|
405
|
+
return withSlotAtIndex(folds, index, {
|
|
406
|
+
...foldData,
|
|
407
|
+
tree: updatedTree,
|
|
408
|
+
});
|
|
409
|
+
},
|
|
410
|
+
toModel(folds) {
|
|
411
|
+
const { slots } = toData(folds);
|
|
412
|
+
const modelSlots = [
|
|
413
|
+
foldSlotToModel(slots[0]),
|
|
414
|
+
...slots
|
|
415
|
+
.slice(1)
|
|
416
|
+
.map((foldData) => foldData === undefined ? null : foldSlotToModel(foldData)),
|
|
417
|
+
];
|
|
418
|
+
return {
|
|
419
|
+
slots: modelSlots,
|
|
420
|
+
};
|
|
421
|
+
},
|
|
422
|
+
fromModel(model) {
|
|
423
|
+
const slots = [];
|
|
424
|
+
for (const foldSlotModel of model.slots) {
|
|
425
|
+
if (foldSlotModel === null) {
|
|
426
|
+
slots.push(undefined);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const tree = foldTreeFromModel(foldSlotModel.tree);
|
|
430
|
+
if (tree === undefined) {
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
slots.push({
|
|
434
|
+
name: foldSlotModel.name,
|
|
435
|
+
description: foldSlotModel.description,
|
|
436
|
+
active: foldSlotModel.active,
|
|
437
|
+
tree,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
const primarySlot = slots[0];
|
|
441
|
+
if (primarySlot === undefined) {
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
const foldSlots = [primarySlot, ...slots.slice(1)];
|
|
445
|
+
return fromData({
|
|
446
|
+
slots: foldSlots,
|
|
447
|
+
});
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
return api;
|
|
451
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { SerializableKey } from '../tree-node/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Serializable semantic representation of one fold-state tree node.
|
|
5
|
+
*
|
|
6
|
+
* This is a persistence/interchange model. It is not the runtime
|
|
7
|
+
* representation of the fold tree.
|
|
8
|
+
*
|
|
9
|
+
* A fold node represents one source-tree node. Its `hiddenEntryIds` are IDs of
|
|
10
|
+
* direct source-tree entries hidden at that node. Its `children` are sparse
|
|
11
|
+
* fold-state descendants.
|
|
12
|
+
*
|
|
13
|
+
* `children` does not contain every source-tree child. It contains only
|
|
14
|
+
* children which have fold state at or below them.
|
|
15
|
+
*
|
|
16
|
+
* A child fold-node ID is allowed to also appear in `hiddenEntryIds`. For
|
|
17
|
+
* example:
|
|
18
|
+
*
|
|
19
|
+
* ```text
|
|
20
|
+
* root
|
|
21
|
+
* hiddenEntryIds: ["src"]
|
|
22
|
+
* children:
|
|
23
|
+
* - id: "src"
|
|
24
|
+
* hiddenEntryIds: ["lib"]
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* This means `src` is hidden directly under `root`, while fold state below
|
|
28
|
+
* `src` is retained for when it is shown again.
|
|
29
|
+
*/
|
|
30
|
+
export type FoldNodeModel<Id extends SerializableKey> = Readonly<{
|
|
31
|
+
/**
|
|
32
|
+
* The source-tree node ID represented by this fold-state node.
|
|
33
|
+
*/
|
|
34
|
+
id: Id;
|
|
35
|
+
/**
|
|
36
|
+
* IDs of direct source-tree entries hidden at this node.
|
|
37
|
+
*
|
|
38
|
+
* These IDs are semantically a set: duplicates are invalid and iteration
|
|
39
|
+
* order has no meaning.
|
|
40
|
+
*/
|
|
41
|
+
hiddenEntryIds: readonly Id[];
|
|
42
|
+
/**
|
|
43
|
+
* Sparse child fold-state nodes.
|
|
44
|
+
*
|
|
45
|
+
* This is semantically keyed by child ID. Array order has no meaning.
|
|
46
|
+
*/
|
|
47
|
+
children: readonly FoldNodeModel<Id>[];
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Creates a schema for one serialized fold-state tree.
|
|
51
|
+
*
|
|
52
|
+
* The schema validates fold-tree-local invariants only:
|
|
53
|
+
*
|
|
54
|
+
* - every hidden entry ID is unique within its fold node;
|
|
55
|
+
* - every child fold-node ID is unique within its parent.
|
|
56
|
+
*
|
|
57
|
+
* It intentionally does not validate:
|
|
58
|
+
*
|
|
59
|
+
* - that an ID exists in a particular source tree;
|
|
60
|
+
* - that a child fold node corresponds to a direct source-tree child;
|
|
61
|
+
* - whether the represented source-tree node is a branch;
|
|
62
|
+
* - whether an entry is currently loaded in the source tree.
|
|
63
|
+
*
|
|
64
|
+
* Those rules require access to the source tree and belong in a fold service,
|
|
65
|
+
* not in this serialization schema.
|
|
66
|
+
*/
|
|
67
|
+
export declare function createFoldNodeModelSchema<Id extends SerializableKey>(idSchema: z.ZodType<Id>): z.ZodType<FoldNodeModel<Id>>;
|
|
68
|
+
/**
|
|
69
|
+
* Serializable metadata and fold-state tree for one occupied fold slot.
|
|
70
|
+
*
|
|
71
|
+
* `description: null` means that this fold tree has no description.
|
|
72
|
+
* An empty string is a supplied description with no text.
|
|
73
|
+
*/
|
|
74
|
+
export type FoldSlotModel<Id extends SerializableKey> = Readonly<{
|
|
75
|
+
/**
|
|
76
|
+
* User-facing name of this fold tree.
|
|
77
|
+
*/
|
|
78
|
+
name: string;
|
|
79
|
+
/**
|
|
80
|
+
* User-facing description of this fold tree, or `null` when it has none.
|
|
81
|
+
*/
|
|
82
|
+
description: string | null;
|
|
83
|
+
/**
|
|
84
|
+
* Whether this fold tree currently contributes to hiding entries.
|
|
85
|
+
*
|
|
86
|
+
* An inactive fold tree retains its fold definition but does not affect the
|
|
87
|
+
* visible navigation projection.
|
|
88
|
+
*/
|
|
89
|
+
active: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* The fold-state tree stored in this slot.
|
|
92
|
+
*/
|
|
93
|
+
tree: FoldNodeModel<Id>;
|
|
94
|
+
}>;
|
|
95
|
+
/**
|
|
96
|
+
* Serializable representation of Folds.
|
|
97
|
+
*
|
|
98
|
+
* `null` represents an unoccupied non-primary slot. Do not use `undefined`:
|
|
99
|
+
* JSON serialization converts undefined array entries to null anyway.
|
|
100
|
+
*/
|
|
101
|
+
export type FoldsModel<Id extends SerializableKey> = Readonly<{
|
|
102
|
+
slots: readonly [FoldSlotModel<Id>, ...(FoldSlotModel<Id> | null)[]];
|
|
103
|
+
}>;
|
|
104
|
+
/**
|
|
105
|
+
* Creates a schema for the serializable fold collection.
|
|
106
|
+
*
|
|
107
|
+
* This validates:
|
|
108
|
+
*
|
|
109
|
+
* - there is always a primary slot at index zero;
|
|
110
|
+
* - every occupied slot has a name, a description or `null`, active state,
|
|
111
|
+
* and a valid fold tree;
|
|
112
|
+
* - every non-primary slot is either an occupied fold slot or `null`.
|
|
113
|
+
*
|
|
114
|
+
* A slot's `description: null` means that its fold tree has no description.
|
|
115
|
+
*
|
|
116
|
+
* It does not validate fold-tree names for uniqueness. That is a domain rule
|
|
117
|
+
* only if the fold service decides names must be unique.
|
|
118
|
+
*/
|
|
119
|
+
export declare function createFoldsModelSchema<Id extends SerializableKey>(idSchema: z.ZodType<Id>): z.ZodType<FoldsModel<Id>>;
|
|
120
|
+
//# sourceMappingURL=fold-node.model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fold-node.model.d.ts","sourceRoot":"","sources":["../../../src/trees/fold-node/fold-node.model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAY7D;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,MAAM,aAAa,CAAC,EAAE,SAAS,eAAe,IAAI,QAAQ,CAAC;IAC/D;;OAEG;IACH,EAAE,EAAE,EAAE,CAAC;IAEP;;;;;OAKG;IACH,cAAc,EAAE,SAAS,EAAE,EAAE,CAAC;IAE9B;;;;OAIG;IACH,QAAQ,EAAE,SAAS,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC;CACxC,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CAAC,EAAE,SAAS,eAAe,EAClE,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,GACtB,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CA0B9B;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,CAAC,EAAE,SAAS,eAAe,IAAI,QAAQ,CAAC;IAC/D;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAE3B;;;;;OAKG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;OAEG;IACH,IAAI,EAAE,aAAa,CAAC,EAAE,CAAC,CAAC;CACzB,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,MAAM,UAAU,CAAC,EAAE,SAAS,eAAe,IAAI,QAAQ,CAAC;IAC5D,KAAK,EAAE,SAAS,CAAC,aAAa,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;CACtE,CAAC,CAAC;AAEH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,SAAS,eAAe,EAC/D,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,GACtB,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAmB3B"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Returns whether every value appears at most once.
|
|
4
|
+
*
|
|
5
|
+
* `SerializableKey` values are strings or numbers, so `Set` equality is the
|
|
6
|
+
* appropriate equality relation for fold-node IDs.
|
|
7
|
+
*/
|
|
8
|
+
function hasUniqueValues(values) {
|
|
9
|
+
return new Set(values).size === values.length;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Creates a schema for one serialized fold-state tree.
|
|
13
|
+
*
|
|
14
|
+
* The schema validates fold-tree-local invariants only:
|
|
15
|
+
*
|
|
16
|
+
* - every hidden entry ID is unique within its fold node;
|
|
17
|
+
* - every child fold-node ID is unique within its parent.
|
|
18
|
+
*
|
|
19
|
+
* It intentionally does not validate:
|
|
20
|
+
*
|
|
21
|
+
* - that an ID exists in a particular source tree;
|
|
22
|
+
* - that a child fold node corresponds to a direct source-tree child;
|
|
23
|
+
* - whether the represented source-tree node is a branch;
|
|
24
|
+
* - whether an entry is currently loaded in the source tree.
|
|
25
|
+
*
|
|
26
|
+
* Those rules require access to the source tree and belong in a fold service,
|
|
27
|
+
* not in this serialization schema.
|
|
28
|
+
*/
|
|
29
|
+
export function createFoldNodeModelSchema(idSchema) {
|
|
30
|
+
let nodeSchema;
|
|
31
|
+
const hiddenEntryIdsSchema = z.array(idSchema).refine(hasUniqueValues, {
|
|
32
|
+
message: 'Hidden entry IDs must be unique within a fold node',
|
|
33
|
+
path: ['hiddenEntryIds'],
|
|
34
|
+
});
|
|
35
|
+
nodeSchema = z.lazy(() => z
|
|
36
|
+
.object({
|
|
37
|
+
id: idSchema,
|
|
38
|
+
hiddenEntryIds: hiddenEntryIdsSchema,
|
|
39
|
+
children: z.array(nodeSchema),
|
|
40
|
+
})
|
|
41
|
+
.strict()
|
|
42
|
+
.refine((node) => hasUniqueValues(node.children.map((child) => child.id)), {
|
|
43
|
+
message: 'Child fold-node IDs must be unique within a fold node',
|
|
44
|
+
path: ['children'],
|
|
45
|
+
}));
|
|
46
|
+
return nodeSchema;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Creates a schema for the serializable fold collection.
|
|
50
|
+
*
|
|
51
|
+
* This validates:
|
|
52
|
+
*
|
|
53
|
+
* - there is always a primary slot at index zero;
|
|
54
|
+
* - every occupied slot has a name, a description or `null`, active state,
|
|
55
|
+
* and a valid fold tree;
|
|
56
|
+
* - every non-primary slot is either an occupied fold slot or `null`.
|
|
57
|
+
*
|
|
58
|
+
* A slot's `description: null` means that its fold tree has no description.
|
|
59
|
+
*
|
|
60
|
+
* It does not validate fold-tree names for uniqueness. That is a domain rule
|
|
61
|
+
* only if the fold service decides names must be unique.
|
|
62
|
+
*/
|
|
63
|
+
export function createFoldsModelSchema(idSchema) {
|
|
64
|
+
const foldNodeSchema = createFoldNodeModelSchema(idSchema);
|
|
65
|
+
const foldSlotSchema = z
|
|
66
|
+
.object({
|
|
67
|
+
name: z.string(),
|
|
68
|
+
description: z.string().nullable(),
|
|
69
|
+
active: z.boolean(),
|
|
70
|
+
tree: foldNodeSchema,
|
|
71
|
+
})
|
|
72
|
+
.strict();
|
|
73
|
+
return z
|
|
74
|
+
.object({
|
|
75
|
+
slots: z
|
|
76
|
+
.tuple([foldSlotSchema])
|
|
77
|
+
.rest(z.union([foldSlotSchema, z.null()])),
|
|
78
|
+
})
|
|
79
|
+
.strict();
|
|
80
|
+
}
|