@cornerstonejs/metadata 5.13.2 → 5.14.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/esm/displayset/BaseDisplaySet.d.ts +3 -4
- package/dist/esm/displayset/BaseDisplaySet.js +1 -5
- package/dist/esm/displayset/IDisplaySet.d.ts +9 -3
- package/dist/esm/displayset/ImageStackDisplaySet.d.ts +1 -3
- package/dist/esm/displayset/ImageStackDisplaySet.js +0 -1
- package/dist/esm/displayset/__fixtures__/scenarioInstances.d.ts +12 -0
- package/dist/esm/displayset/__fixtures__/scenarioInstances.js +210 -0
- package/dist/esm/displayset/compileSplitRules.d.ts +3 -0
- package/dist/esm/displayset/compileSplitRules.js +197 -0
- package/dist/esm/displayset/compiledDisplaySetRules.d.ts +3 -0
- package/dist/esm/displayset/compiledDisplaySetRules.js +3 -0
- package/dist/esm/displayset/createDisplaySetFromGroup.js +54 -19
- package/dist/esm/displayset/displaySetContext.d.ts +150 -0
- package/dist/esm/displayset/displaySetContext.js +918 -0
- package/dist/esm/displayset/displaySetRules.d.ts +5 -0
- package/dist/esm/displayset/displaySetRules.js +142 -0
- package/dist/esm/displayset/displaySetRules.test.d.ts +1 -0
- package/dist/esm/displayset/displaySetRules.test.js +1306 -0
- package/dist/esm/displayset/displaySetRulesTypes.d.ts +34 -0
- package/dist/esm/displayset/displaySetRulesTypes.js +0 -0
- package/dist/esm/displayset/displayset.test.js +422 -130
- package/dist/esm/displayset/groupInstancesBySplitRules.d.ts +8 -2
- package/dist/esm/displayset/groupInstancesBySplitRules.js +174 -17
- package/dist/esm/displayset/index.d.ts +14 -4
- package/dist/esm/displayset/index.js +10 -3
- package/dist/esm/displayset/isEcgInstance.d.ts +2 -1
- package/dist/esm/displayset/isEcgInstance.js +4 -9
- package/dist/esm/displayset/isImageInstance.d.ts +2 -1
- package/dist/esm/displayset/isImageInstance.js +4 -44
- package/dist/esm/displayset/isReconstructableInstances.d.ts +2 -0
- package/dist/esm/displayset/isReconstructableInstances.js +91 -0
- package/dist/esm/displayset/isVideoInstance.d.ts +2 -1
- package/dist/esm/displayset/isVideoInstance.js +4 -30
- package/dist/esm/displayset/isWsiInstance.d.ts +2 -1
- package/dist/esm/displayset/isWsiInstance.js +4 -6
- package/dist/esm/displayset/metadataContext.d.ts +13 -0
- package/dist/esm/displayset/metadataContext.js +42 -0
- package/dist/esm/displayset/metadataContext.test.d.ts +1 -0
- package/dist/esm/displayset/metadataContext.test.js +62 -0
- package/dist/esm/displayset/scenarioFixtures.test.d.ts +1 -0
- package/dist/esm/displayset/scenarioFixtures.test.js +206 -0
- package/dist/esm/displayset/seriesContext.test.d.ts +1 -0
- package/dist/esm/displayset/seriesContext.test.js +238 -0
- package/dist/esm/displayset/seriesFunctions.d.ts +29 -0
- package/dist/esm/displayset/seriesFunctions.js +327 -0
- package/dist/esm/displayset/sopClassUidLists.d.ts +14 -0
- package/dist/esm/displayset/sopClassUidLists.js +100 -0
- package/dist/esm/displayset/sortInstances.d.ts +10 -0
- package/dist/esm/displayset/sortInstances.js +33 -0
- package/dist/esm/displayset/sortInstances.test.d.ts +1 -0
- package/dist/esm/displayset/sortInstances.test.js +56 -0
- package/dist/esm/displayset/splitImageIdsBySplitRules.d.ts +3 -3
- package/dist/esm/displayset/splitImageIdsBySplitRules.js +5 -5
- package/dist/esm/displayset/splitRuleSchema.d.ts +11 -0
- package/dist/esm/displayset/splitRuleSchema.js +168 -0
- package/dist/esm/displayset/splitRuleSet.d.ts +12 -0
- package/dist/esm/displayset/splitRuleSet.js +47 -0
- package/dist/esm/displayset/splitRuleSet.test.d.ts +1 -0
- package/dist/esm/displayset/splitRuleSet.test.js +119 -0
- package/dist/esm/displayset/types.d.ts +37 -4
- package/dist/esm/displayset/types.js +15 -0
- package/dist/esm/index.d.ts +4 -2
- package/dist/esm/index.js +2 -1
- package/dist/esm/safeFunctions/compile.d.ts +31 -0
- package/dist/esm/safeFunctions/compile.js +142 -0
- package/dist/esm/safeFunctions/compile.test.d.ts +1 -0
- package/dist/esm/safeFunctions/compile.test.js +82 -0
- package/dist/esm/safeFunctions/expression/compiler.d.ts +23 -0
- package/dist/esm/safeFunctions/expression/compiler.js +481 -0
- package/dist/esm/safeFunctions/expression/expression.test.d.ts +1 -0
- package/dist/esm/safeFunctions/expression/expression.test.js +325 -0
- package/dist/esm/safeFunctions/expression/index.d.ts +4 -0
- package/dist/esm/safeFunctions/expression/index.js +3 -0
- package/dist/esm/safeFunctions/expression/parser.d.ts +56 -0
- package/dist/esm/safeFunctions/expression/parser.js +248 -0
- package/dist/esm/safeFunctions/expression/tokenizer.d.ts +29 -0
- package/dist/esm/safeFunctions/expression/tokenizer.js +198 -0
- package/dist/esm/safeFunctions/index.d.ts +5 -0
- package/dist/esm/safeFunctions/index.js +4 -0
- package/dist/esm/safeFunctions/matchTests.d.ts +16 -0
- package/dist/esm/safeFunctions/matchTests.js +203 -0
- package/dist/esm/safeFunctions/schema.d.ts +72 -0
- package/dist/esm/safeFunctions/schema.js +361 -0
- package/dist/esm/safeFunctions/types.d.ts +47 -0
- package/dist/esm/safeFunctions/types.js +0 -0
- package/dist/esm/safeFunctions/values.d.ts +6 -0
- package/dist/esm/safeFunctions/values.js +71 -0
- package/dist/esm/utilities/Tags.d.ts +1 -0
- package/dist/esm/utilities/Tags.js +14 -2
- package/dist/esm/utilities/index.d.ts +1 -1
- package/dist/esm/utilities/index.js +1 -1
- package/dist/esm/utilities/toNumber.d.ts +1 -1
- package/dist/esm/utilities/toNumber.js +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +2 -2
- package/dist/esm/displayset/defaultDisplaySetSplitRules.d.ts +0 -2
- package/dist/esm/displayset/defaultDisplaySetSplitRules.js +0 -93
- package/dist/esm/displayset/viewportTypes.d.ts +0 -4
- package/dist/esm/displayset/viewportTypes.js +0 -13
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { conditionShape, matchTestShape, valueShape } from '../safeFunctions/index.js';
|
|
2
|
+
export const ATTRIBUTE_ONLY_NAMES = Object.freeze([
|
|
3
|
+
'splitNumber',
|
|
4
|
+
'sopClassUids',
|
|
5
|
+
]);
|
|
6
|
+
const ruleShape = {
|
|
7
|
+
name: 'rule',
|
|
8
|
+
keyNoun: 'field',
|
|
9
|
+
forms: {
|
|
10
|
+
rule: {
|
|
11
|
+
keys: {
|
|
12
|
+
id: {
|
|
13
|
+
kind: 'string',
|
|
14
|
+
optional: true,
|
|
15
|
+
description: 'The rule id. Optional: the key of the rule in the rule set is the id. When present it must equal that key.',
|
|
16
|
+
},
|
|
17
|
+
groupId: {
|
|
18
|
+
kind: 'string',
|
|
19
|
+
optional: true,
|
|
20
|
+
description: 'The group of rules this rule belongs to, for a reader that must recognize display sets from several related rules (for example every mammography rule). Defaults to the rule id.',
|
|
21
|
+
},
|
|
22
|
+
priority: {
|
|
23
|
+
kind: 'priority',
|
|
24
|
+
description: 'Evaluation order: ascending, first match wins. null excludes the rule.',
|
|
25
|
+
},
|
|
26
|
+
description: {
|
|
27
|
+
kind: 'string',
|
|
28
|
+
optional: true,
|
|
29
|
+
description: 'Human-readable explanation. Not compiled.',
|
|
30
|
+
},
|
|
31
|
+
viewportTypes: {
|
|
32
|
+
kind: 'string',
|
|
33
|
+
list: true,
|
|
34
|
+
optional: true,
|
|
35
|
+
description: 'Viewport types for the host. CS3D copies them to the display set unchanged, and does not read them.',
|
|
36
|
+
},
|
|
37
|
+
handler: {
|
|
38
|
+
kind: 'string',
|
|
39
|
+
optional: true,
|
|
40
|
+
description: 'The generic handler name of the display sets, e.g. segmentation. Any string; the host decides what an unknown name does.',
|
|
41
|
+
},
|
|
42
|
+
context: {
|
|
43
|
+
kind: 'contextEntry',
|
|
44
|
+
record: true,
|
|
45
|
+
optional: true,
|
|
46
|
+
description: 'Context entries for this rule only. An entry replaces the global entry of the same name, also inside the global entries that the rule reads, or defines a new name.',
|
|
47
|
+
},
|
|
48
|
+
matches: {
|
|
49
|
+
kind: 'condition',
|
|
50
|
+
optional: true,
|
|
51
|
+
function: true,
|
|
52
|
+
call: '(instance, context) => boolean',
|
|
53
|
+
description: 'Which instances this rule claims: a map of name to test, and every entry must hold. Omit for a catch-all rule.',
|
|
54
|
+
},
|
|
55
|
+
groupBy: {
|
|
56
|
+
kind: 'name',
|
|
57
|
+
list: true,
|
|
58
|
+
optional: true,
|
|
59
|
+
call: '(instance, context) => value',
|
|
60
|
+
description: 'Names (or functions) whose values must all be equal for two instances to form one group. Defaults to SeriesInstanceUID.',
|
|
61
|
+
},
|
|
62
|
+
sortBy: {
|
|
63
|
+
kind: 'sortKey',
|
|
64
|
+
list: true,
|
|
65
|
+
optional: true,
|
|
66
|
+
call: '(a, b, context) => number',
|
|
67
|
+
description: 'The order of the instances in a display set: names in ascending order, named sort functions, or functions. A result of 0 or NaN is "no opinion": the next entry decides.',
|
|
68
|
+
},
|
|
69
|
+
attributes: {
|
|
70
|
+
kind: 'attributeValue',
|
|
71
|
+
record: true,
|
|
72
|
+
optional: true,
|
|
73
|
+
function: true,
|
|
74
|
+
call: '(attributes, options) => Record<string, unknown>',
|
|
75
|
+
description: 'Attributes of the produced display sets, keyed by attribute name.',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
const contextEntryShape = {
|
|
82
|
+
name: 'context entry',
|
|
83
|
+
string: 'expression',
|
|
84
|
+
function: true,
|
|
85
|
+
forms: {
|
|
86
|
+
declaration: {
|
|
87
|
+
selectBy: 'attribute',
|
|
88
|
+
keys: {
|
|
89
|
+
attribute: { kind: 'string' },
|
|
90
|
+
vr: { kind: 'string', optional: true },
|
|
91
|
+
vm: { kind: 'string', optional: true },
|
|
92
|
+
},
|
|
93
|
+
description: 'Reads one attribute of the instance that the DICOM dictionary does not hold: a private tag as 8-digit hex, a vendor attribute, or a synthetic attribute such as imageId. vr and vm are recorded only.',
|
|
94
|
+
},
|
|
95
|
+
template: {
|
|
96
|
+
selectBy: 'template',
|
|
97
|
+
keys: { template: { kind: 'string' } },
|
|
98
|
+
description: 'Text: each {name} gives the value of a name or a path.',
|
|
99
|
+
},
|
|
100
|
+
group: {
|
|
101
|
+
selectBy: 'function',
|
|
102
|
+
keys: {
|
|
103
|
+
function: {
|
|
104
|
+
kind: 'literal',
|
|
105
|
+
description: 'The name of a series function (planeGeometry, timeClusters, or one of options.seriesFunctions), or in code a function.',
|
|
106
|
+
},
|
|
107
|
+
args: {
|
|
108
|
+
kind: 'literal',
|
|
109
|
+
optional: true,
|
|
110
|
+
description: 'The arguments of the series function. Each value is a fixed value, or an expression with one value for the series.',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
description: 'One call of a series function over the series. A rule reads a result field by path, e.g. timeClusters.start. In sortBy and attributes, displaySet.<name> runs it over the display set.',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
const sortKeyShape = {
|
|
118
|
+
name: 'sort entry',
|
|
119
|
+
string: 'attribute',
|
|
120
|
+
function: true,
|
|
121
|
+
forms: {
|
|
122
|
+
sortFunction: {
|
|
123
|
+
selectBy: 'sortFunction',
|
|
124
|
+
keys: {
|
|
125
|
+
sortFunction: {
|
|
126
|
+
kind: 'string',
|
|
127
|
+
description: 'The name of a sort function in options.sortFunctions.',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
description: 'A named sort function, called (a, b, context) => number.',
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
const attributeValueShape = {
|
|
135
|
+
...valueShape,
|
|
136
|
+
name: 'attribute value',
|
|
137
|
+
forms: {
|
|
138
|
+
template: valueShape.forms.template,
|
|
139
|
+
expression: {
|
|
140
|
+
...valueShape.forms.expression,
|
|
141
|
+
description: 'An expression over the display set: displaySet.instances.max(x) (and min, sum, count) runs over its instances, series.instances.max(x) over the series, and a DICOM keyword reads its first instance.',
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
const nameShape = {
|
|
146
|
+
name: 'name',
|
|
147
|
+
string: 'attribute',
|
|
148
|
+
function: true,
|
|
149
|
+
forms: {},
|
|
150
|
+
};
|
|
151
|
+
function deepFreeze(value) {
|
|
152
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
153
|
+
Object.freeze(value);
|
|
154
|
+
for (const entry of Object.values(value)) {
|
|
155
|
+
deepFreeze(entry);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
export const splitRuleSchema = deepFreeze({
|
|
161
|
+
rule: ruleShape,
|
|
162
|
+
contextEntry: contextEntryShape,
|
|
163
|
+
condition: conditionShape,
|
|
164
|
+
matchTest: matchTestShape,
|
|
165
|
+
name: nameShape,
|
|
166
|
+
sortKey: sortKeyShape,
|
|
167
|
+
attributeValue: attributeValueShape,
|
|
168
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SplitRule, SplitRuleSet } from './types.js';
|
|
2
|
+
export declare function validateSplitRuleSetEntry(id: string, entry: {
|
|
3
|
+
id?: string;
|
|
4
|
+
priority?: number | null;
|
|
5
|
+
} | null | undefined): boolean;
|
|
6
|
+
export declare function orderSplitRuleSetEntries<Entry extends {
|
|
7
|
+
id?: string;
|
|
8
|
+
priority?: number | null;
|
|
9
|
+
}>(ruleSet: Record<string, Entry>): (Omit<Entry, 'priority'> & {
|
|
10
|
+
id: string;
|
|
11
|
+
})[];
|
|
12
|
+
export declare function resolveSplitRuleSet(ruleSet: SplitRuleSet): SplitRule[];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { isUnsafeKey } from '../safeFunctions/schema.js';
|
|
2
|
+
function invalidEntry(id, message) {
|
|
3
|
+
throw new Error(`Invalid split rule set entry "${id}": ${message}`);
|
|
4
|
+
}
|
|
5
|
+
export function validateSplitRuleSetEntry(id, entry) {
|
|
6
|
+
if (isUnsafeKey(id)) {
|
|
7
|
+
invalidEntry(id, 'the id is not allowed; give the rule another id');
|
|
8
|
+
}
|
|
9
|
+
if (!entry || typeof entry !== 'object') {
|
|
10
|
+
invalidEntry(id, 'the entry must be an object');
|
|
11
|
+
}
|
|
12
|
+
if (entry.id !== undefined && entry.id !== id) {
|
|
13
|
+
invalidEntry(id, `the entry states id "${entry.id}", but its key is the id. Remove the id, or make it equal to the key.`);
|
|
14
|
+
}
|
|
15
|
+
const { priority } = entry;
|
|
16
|
+
if (priority === null) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (typeof priority !== 'number' || !Number.isFinite(priority)) {
|
|
20
|
+
invalidEntry(id, `priority must be a finite number, or null to exclude the rule; got ${JSON.stringify(priority)}`);
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
export function orderSplitRuleSetEntries(ruleSet) {
|
|
25
|
+
if (!ruleSet || typeof ruleSet !== 'object' || Array.isArray(ruleSet)) {
|
|
26
|
+
throw new Error(`A split rule set must be an object keyed by rule id: ${JSON.stringify(ruleSet)}`);
|
|
27
|
+
}
|
|
28
|
+
const included = [];
|
|
29
|
+
for (const [id, entry] of Object.entries(ruleSet)) {
|
|
30
|
+
if (validateSplitRuleSetEntry(id, entry)) {
|
|
31
|
+
included.push({ id, priority: entry.priority, entry });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
included.sort((a, b) => {
|
|
35
|
+
if (a.priority !== b.priority) {
|
|
36
|
+
return a.priority - b.priority;
|
|
37
|
+
}
|
|
38
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
39
|
+
});
|
|
40
|
+
return included.map(({ id, entry }) => {
|
|
41
|
+
const { priority, ...rule } = entry;
|
|
42
|
+
return { ...rule, id };
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export function resolveSplitRuleSet(ruleSet) {
|
|
46
|
+
return orderSplitRuleSetEntries(ruleSet);
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals';
|
|
2
|
+
import { groupInstancesBySplitRules } from './groupInstancesBySplitRules.js';
|
|
3
|
+
import { createDisplaySetSplitRules } from './displaySetRules.js';
|
|
4
|
+
import { resolveSplitRuleSet, validateSplitRuleSetEntry } from './splitRuleSet.js';
|
|
5
|
+
const ct = (imageId, InstanceNumber) => ({
|
|
6
|
+
imageId,
|
|
7
|
+
Modality: 'CT',
|
|
8
|
+
SeriesInstanceUID: 'series-1',
|
|
9
|
+
SOPInstanceUID: `sop-${imageId}`,
|
|
10
|
+
InstanceNumber,
|
|
11
|
+
});
|
|
12
|
+
describe('resolveSplitRuleSet', () => {
|
|
13
|
+
it('orders rules by ascending priority, and takes the id from the key', () => {
|
|
14
|
+
const rules = resolveSplitRuleSet({
|
|
15
|
+
late: { priority: 20000 },
|
|
16
|
+
defaultA: { priority: 1 },
|
|
17
|
+
early: { priority: -1 },
|
|
18
|
+
defaultB: { priority: 2 },
|
|
19
|
+
});
|
|
20
|
+
expect(rules.map((rule) => rule.id)).toEqual([
|
|
21
|
+
'early',
|
|
22
|
+
'defaultA',
|
|
23
|
+
'defaultB',
|
|
24
|
+
'late',
|
|
25
|
+
]);
|
|
26
|
+
expect(rules.every((rule) => !('priority' in rule))).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
it('orders equal priorities by id, whatever order the keys were inserted in', () => {
|
|
29
|
+
const forward = resolveSplitRuleSet({
|
|
30
|
+
b: { priority: 5 },
|
|
31
|
+
a: { priority: 5 },
|
|
32
|
+
});
|
|
33
|
+
const reverse = resolveSplitRuleSet({
|
|
34
|
+
a: { priority: 5 },
|
|
35
|
+
b: { priority: 5 },
|
|
36
|
+
});
|
|
37
|
+
expect(forward.map((rule) => rule.id)).toEqual(['a', 'b']);
|
|
38
|
+
expect(reverse.map((rule) => rule.id)).toEqual(['a', 'b']);
|
|
39
|
+
});
|
|
40
|
+
it('accepts an entry that states an id equal to its key', () => {
|
|
41
|
+
const rules = resolveSplitRuleSet({ same: { id: 'same', priority: 1 } });
|
|
42
|
+
expect(rules.map((rule) => rule.id)).toEqual(['same']);
|
|
43
|
+
});
|
|
44
|
+
it('rejects an array', () => {
|
|
45
|
+
expect(() => resolveSplitRuleSet([{ id: 'a' }])).toThrow(/must be an object keyed by rule id/);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('validateSplitRuleSetEntry', () => {
|
|
49
|
+
it('returns true for a numeric priority, and false for a null priority', () => {
|
|
50
|
+
expect(validateSplitRuleSetEntry('a', { priority: 0 })).toBe(true);
|
|
51
|
+
expect(validateSplitRuleSetEntry('a', { id: 'a', priority: -2.5 })).toBe(true);
|
|
52
|
+
expect(validateSplitRuleSetEntry('a', { priority: null })).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
it('throws for an id that differs from the key', () => {
|
|
55
|
+
expect(() => validateSplitRuleSetEntry('a', { id: 'b', priority: 1 })).toThrow(/"a".*"b"/);
|
|
56
|
+
});
|
|
57
|
+
it('throws for a missing or non-numeric priority', () => {
|
|
58
|
+
for (const priority of [undefined, '1', Number.NaN, Infinity]) {
|
|
59
|
+
expect(() => validateSplitRuleSetEntry('a', { priority })).toThrow(/"a".*priority/);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
it('throws for an entry that is not an object', () => {
|
|
63
|
+
expect(() => validateSplitRuleSetEntry('a', null)).toThrow(/"a".*must be an object/);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('groupInstancesBySplitRules with a rule set', () => {
|
|
67
|
+
it('gives one key for every naturalized form of a value', () => {
|
|
68
|
+
const keyOf = (EchoNumbers) => groupInstancesBySplitRules([{ ...ct('a', 1), EchoNumbers }], {
|
|
69
|
+
byEcho: { priority: 1, groupBy: ['SeriesInstanceUID', 'EchoNumbers'] },
|
|
70
|
+
})[0].splitKey;
|
|
71
|
+
expect(new Set([5, '5', [5], ['5']].map(keyOf)).size).toBe(1);
|
|
72
|
+
expect(new Set([undefined, null, '', []].map(keyOf)).size).toBe(1);
|
|
73
|
+
expect(keyOf('05')).not.toBe(keyOf(5));
|
|
74
|
+
});
|
|
75
|
+
it('evaluates the rules in priority order, first match wins', () => {
|
|
76
|
+
const ruleSet = {
|
|
77
|
+
everything: { priority: 1, groupBy: ['SeriesInstanceUID'] },
|
|
78
|
+
first: {
|
|
79
|
+
priority: -1,
|
|
80
|
+
matches: (instance) => instance.InstanceNumber === 1,
|
|
81
|
+
groupBy: ['SOPInstanceUID'],
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const groups = groupInstancesBySplitRules([ct('a', 1), ct('b', 2), ct('c', 3)], ruleSet);
|
|
85
|
+
expect(groups.map((group) => [
|
|
86
|
+
group.matchedRule.id,
|
|
87
|
+
group.instances.map((i) => i.imageId),
|
|
88
|
+
])).toEqual([
|
|
89
|
+
['first', ['a']],
|
|
90
|
+
['everything', ['b', 'c']],
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
it('lets a layer replace a rule by its key', () => {
|
|
94
|
+
const defaults = {
|
|
95
|
+
volume3d: { priority: 1, groupBy: ['SeriesInstanceUID'] },
|
|
96
|
+
};
|
|
97
|
+
const layered = {
|
|
98
|
+
...defaults,
|
|
99
|
+
volume3d: { priority: -1, groupBy: ['SOPInstanceUID'] },
|
|
100
|
+
};
|
|
101
|
+
const groups = groupInstancesBySplitRules([ct('a', 1), ct('b', 2)], layered);
|
|
102
|
+
expect(groups).toHaveLength(2);
|
|
103
|
+
expect(groups.every((group) => group.matchedRule.id === 'volume3d')).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe('createDisplaySetSplitRules with a rule set as data', () => {
|
|
107
|
+
it('compiles to a keyed rule set that resolves in priority order', () => {
|
|
108
|
+
const rules = createDisplaySetSplitRules({
|
|
109
|
+
catchAll: { priority: 2 },
|
|
110
|
+
ctRule: { priority: 1, matches: { Modality: 'CT' } },
|
|
111
|
+
off: { priority: null, matches: { Modality: 'MR' } },
|
|
112
|
+
});
|
|
113
|
+
expect(resolveSplitRuleSet(rules).map((rule) => rule.id)).toEqual([
|
|
114
|
+
'ctRule',
|
|
115
|
+
'catchAll',
|
|
116
|
+
]);
|
|
117
|
+
expect(rules.ctRule.matches?.(ct('a', 1), { series: {} })).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DisplaySetContextValues } from './displaySetContext.js';
|
|
1
2
|
export type NaturalizedInstance = {
|
|
2
3
|
imageId?: string;
|
|
3
4
|
Modality?: string;
|
|
@@ -13,7 +14,7 @@ export type NaturalizedInstance = {
|
|
|
13
14
|
AvailableTransferSyntaxUID?: string;
|
|
14
15
|
[key: string]: unknown;
|
|
15
16
|
};
|
|
16
|
-
export
|
|
17
|
+
export declare const DISPLAY_SET_HANDLER_NAMES: readonly ["image", "video", "wholeSlide", "ecg", "segmentation", "rtStructureSet", "parametricMap", "structuredReport", "encapsulatedDocument", "bulkAnnotation", "chart", "unsupported"];
|
|
17
18
|
export type SeriesInfo = {
|
|
18
19
|
NumberOfSeriesRelatedInstances: number;
|
|
19
20
|
numberOfFrames: number;
|
|
@@ -25,35 +26,67 @@ export type SeriesInfo = {
|
|
|
25
26
|
export type SeriesFacts = Record<string, unknown>;
|
|
26
27
|
export type SeriesContext = {
|
|
27
28
|
instances: NaturalizedInstance[];
|
|
29
|
+
series?: Readonly<SeriesFacts>;
|
|
30
|
+
};
|
|
31
|
+
export type SeriesFieldScope = 'series' | 'instance';
|
|
32
|
+
export type SeriesFunction = (instances: NaturalizedInstance[], context: {
|
|
33
|
+
args: Readonly<Record<string, unknown>>;
|
|
34
|
+
}) => unknown;
|
|
35
|
+
export type SeriesFunctionDefinition = {
|
|
36
|
+
compute: SeriesFunction;
|
|
37
|
+
fields?: Readonly<Record<string, SeriesFieldScope>>;
|
|
28
38
|
};
|
|
29
39
|
export type RuleContext = {
|
|
30
40
|
series: SeriesFacts;
|
|
41
|
+
displaySet?: DisplaySetContextValues;
|
|
31
42
|
};
|
|
32
43
|
export type SplitRuleCustomAttributesContext = {
|
|
33
44
|
instance: NaturalizedInstance;
|
|
34
45
|
isMultiFrame?: boolean;
|
|
35
46
|
sopClassUids?: string[];
|
|
36
|
-
viewportTypes?: readonly ViewportTypeHint[];
|
|
37
47
|
[key: string]: unknown;
|
|
38
48
|
};
|
|
39
49
|
export type SplitRuleOptions = {
|
|
40
50
|
instances: NaturalizedInstance[];
|
|
41
51
|
splitNumber?: number;
|
|
42
52
|
descriptionName?: string;
|
|
53
|
+
series?: SeriesFacts;
|
|
43
54
|
};
|
|
44
55
|
export type SplitRule = {
|
|
45
|
-
id
|
|
46
|
-
|
|
56
|
+
id: string;
|
|
57
|
+
groupId?: string;
|
|
58
|
+
handler?: string;
|
|
59
|
+
viewportTypes?: readonly string[];
|
|
47
60
|
series?: (context: SeriesContext) => SeriesFacts;
|
|
48
61
|
matches?: (instance: NaturalizedInstance, context: RuleContext) => boolean;
|
|
49
62
|
groupBy?: (string | ((instance: NaturalizedInstance, context: RuleContext) => unknown))[];
|
|
63
|
+
compareInstances?: (a: NaturalizedInstance, b: NaturalizedInstance, context: RuleContext) => number;
|
|
50
64
|
customAttributes?: (attributes: SplitRuleCustomAttributesContext, options: SplitRuleOptions) => Record<string, unknown>;
|
|
51
65
|
};
|
|
66
|
+
export type SplitRuleSetEntry = Omit<SplitRule, 'id'> & {
|
|
67
|
+
id?: string;
|
|
68
|
+
priority: number | null;
|
|
69
|
+
};
|
|
70
|
+
export type SplitRuleSet = Record<string, SplitRuleSetEntry>;
|
|
71
|
+
export declare const DEFAULT_SPLIT_RULE_PRIORITY_LIMIT = 10000;
|
|
72
|
+
export type InstanceOrderContext = {
|
|
73
|
+
matchedRule: SplitRule;
|
|
74
|
+
series: SeriesFacts;
|
|
75
|
+
};
|
|
76
|
+
export type SortInstances = (instances: NaturalizedInstance[], context: InstanceOrderContext) => NaturalizedInstance[];
|
|
77
|
+
export type GroupInstancesOptions = {
|
|
78
|
+
sortInstances?: SortInstances;
|
|
79
|
+
compareInstances?: SplitRule['compareInstances'];
|
|
80
|
+
};
|
|
52
81
|
export type SplitContext = {
|
|
53
82
|
getNaturalizedInstance: (imageId: string) => NaturalizedInstance | undefined;
|
|
54
83
|
};
|
|
84
|
+
export type OrderInstancesOptions = GroupInstancesOptions & {
|
|
85
|
+
series?: SeriesFacts;
|
|
86
|
+
};
|
|
55
87
|
export type InstanceGroup = {
|
|
56
88
|
instances: NaturalizedInstance[];
|
|
57
89
|
matchedRule: SplitRule;
|
|
90
|
+
series?: SeriesFacts;
|
|
58
91
|
splitKey?: string;
|
|
59
92
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const DISPLAY_SET_HANDLER_NAMES = [
|
|
2
|
+
'image',
|
|
3
|
+
'video',
|
|
4
|
+
'wholeSlide',
|
|
5
|
+
'ecg',
|
|
6
|
+
'segmentation',
|
|
7
|
+
'rtStructureSet',
|
|
8
|
+
'parametricMap',
|
|
9
|
+
'structuredReport',
|
|
10
|
+
'encapsulatedDocument',
|
|
11
|
+
'bulkAnnotation',
|
|
12
|
+
'chart',
|
|
13
|
+
'unsupported',
|
|
14
|
+
];
|
|
15
|
+
export const DEFAULT_SPLIT_RULE_PRIORITY_LIMIT = 10000;
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -4,8 +4,10 @@ export { version } from './version.js';
|
|
|
4
4
|
export * as metaData from './metaData.js';
|
|
5
5
|
export * as utilities from './utilities/index.js';
|
|
6
6
|
export * as displaySet from './displayset/index.js';
|
|
7
|
-
export
|
|
8
|
-
export {
|
|
7
|
+
export { collectIdentifiers, collectReads, compileCondition, compileExpression, compileTemplate, compileValue, ExpressionSyntaxError, hasNoValue, isAbsent, isEmpty, looseEquals, MATCH_OPERATOR_NAMES, parseExpressionSource, RuleCompileError, } from './safeFunctions/index.js';
|
|
8
|
+
export type { CompiledExpression, CompileExpressionOptions, CompileOptions, CompiledPredicate, CompiledValue, ExpressionNode, ExpressionRead, ExpressionScope, InlinePredicate, InlineValue, NameReader, NameReaderFunction, RawMatchTest, RawScalar, RawValue, SafeFunctionSubject, SchemaSite, } from './safeFunctions/index.js';
|
|
9
|
+
export type { IDisplaySet, BaseDisplaySetOptions, ImageStackDisplaySetOptions, ResolveInstancesOptions, SplitImageIdsBySplitRulesOptions, RegisterDisplaySetMetadataOptions, NaturalizedInstance, SeriesInfo, SeriesFacts, SeriesContext, SeriesFunction, SeriesFunctionDefinition, SeriesFieldScope, PlaneGeometry, TimeClusters, RuleContext, SplitRule, SplitContext, SplitRuleOptions, SplitRuleCustomAttributesContext, InstanceGroup, InstanceOrderContext, SortInstances, GroupInstancesOptions, SplitRuleSet, SplitRuleSetEntry, OrderInstancesOptions, ContextEntry, ContextGroupEntry, ContextValues, DisplaySetContext, InstanceContextFunction, InstanceHook, RawAttributeValue, RawCondition, RawName, RawSortKey, RawSplitRule, DisplaySetRules, CreateDisplaySetSplitRulesOptions, } from './displayset/index.js';
|
|
10
|
+
export { BaseDisplaySet, ImageStackDisplaySet, resolveInstances, buildSeriesInfo, groupInstancesBySplitRules, splitImageIdsBySplitRules, registerDisplaySetMetadata, registerDisplaySetProviders, compiledDisplaySetRules, defaultDisplaySetSplitRules, orderInstancesForRule, resolveSplitRuleSet, compare, compareInstanceNumber, compareSOPInstanceUID, sortByInstanceNumber, instancesSortCriteria, sortInstancesCopy, displaySetRules, createDisplaySetSplitRules, defaultMetadataContext, setMetadataContext, getMetadataContext, getContextValue, imageSopClassUids, videoSopClassUids, videoTransferSyntaxUids, longVideoSopClassUids, longVideoMinFrames, ecgSopClassUids, wholeSlideSopClassUids, segmentationSopClassUids, rtStructureSetSopClassUids, parametricMapSopClassUids, structuredReportSopClassUids, encapsulatedDocumentSopClassUids, bulkAnnotationSopClassUids, chartSopClassUids, createDisplaySetFromGroup, isImageInstance, isVideoInstance, isEcgInstance, isWsiInstance, isReconstructableInstances, DEFAULT_SPLIT_RULE_PRIORITY_LIMIT, DISPLAY_SET_HANDLER_NAMES, } from './displayset/index.js';
|
|
9
11
|
export type { CreateDisplaySetFromGroupOptions } from './displayset/index.js';
|
|
10
12
|
export * as logging from './utilities/logging.js';
|
|
11
13
|
export { registerDefaultProviders } from './registerDefaultProviders.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -4,7 +4,8 @@ export { version } from './version.js';
|
|
|
4
4
|
export * as metaData from './metaData.js';
|
|
5
5
|
export * as utilities from './utilities/index.js';
|
|
6
6
|
export * as displaySet from './displayset/index.js';
|
|
7
|
-
export {
|
|
7
|
+
export { collectIdentifiers, collectReads, compileCondition, compileExpression, compileTemplate, compileValue, ExpressionSyntaxError, hasNoValue, isAbsent, isEmpty, looseEquals, MATCH_OPERATOR_NAMES, parseExpressionSource, RuleCompileError, } from './safeFunctions/index.js';
|
|
8
|
+
export { BaseDisplaySet, ImageStackDisplaySet, resolveInstances, buildSeriesInfo, groupInstancesBySplitRules, splitImageIdsBySplitRules, registerDisplaySetMetadata, registerDisplaySetProviders, compiledDisplaySetRules, defaultDisplaySetSplitRules, orderInstancesForRule, resolveSplitRuleSet, compare, compareInstanceNumber, compareSOPInstanceUID, sortByInstanceNumber, instancesSortCriteria, sortInstancesCopy, displaySetRules, createDisplaySetSplitRules, defaultMetadataContext, setMetadataContext, getMetadataContext, getContextValue, imageSopClassUids, videoSopClassUids, videoTransferSyntaxUids, longVideoSopClassUids, longVideoMinFrames, ecgSopClassUids, wholeSlideSopClassUids, segmentationSopClassUids, rtStructureSetSopClassUids, parametricMapSopClassUids, structuredReportSopClassUids, encapsulatedDocumentSopClassUids, bulkAnnotationSopClassUids, chartSopClassUids, createDisplaySetFromGroup, isImageInstance, isVideoInstance, isEcgInstance, isWsiInstance, isReconstructableInstances, DEFAULT_SPLIT_RULE_PRIORITY_LIMIT, DISPLAY_SET_HANDLER_NAMES, } from './displayset/index.js';
|
|
8
9
|
export * as logging from './utilities/logging.js';
|
|
9
10
|
export { registerDefaultProviders } from './registerDefaultProviders.js';
|
|
10
11
|
const { calculateSUVScalingFactors } = calculateSUV;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ExpressionScope, NameReader, NameReaderFunction, Schema, SchemaSite, ShapeCompiler } from './schema.js';
|
|
2
|
+
import type { CompiledPredicate, CompiledValue, RawCondition, RawValue, SafeFunctionSubject } from './types.js';
|
|
3
|
+
export { compareFinite, hasNoValue, isAbsent, isEmpty, isTrue, looseEquals, } from './values.js';
|
|
4
|
+
export type CompileOptions = {
|
|
5
|
+
path?: string;
|
|
6
|
+
definition?: string;
|
|
7
|
+
expression?: ExpressionScope;
|
|
8
|
+
call?: string;
|
|
9
|
+
readName?: NameReader;
|
|
10
|
+
};
|
|
11
|
+
export declare function invalid(message: string, fragment: unknown): never;
|
|
12
|
+
export declare const readOwnName: NameReader;
|
|
13
|
+
export declare const safeFunctionCompilers: Record<string, ShapeCompiler>;
|
|
14
|
+
export declare function createSite(options?: CompileOptions, schema?: Schema, compilers?: Record<string, ShapeCompiler>): SchemaSite;
|
|
15
|
+
export declare function readNameAt(name: string, site: SchemaSite, field?: string): NameReaderFunction;
|
|
16
|
+
export declare function compileExpressionAt(source: string, site: SchemaSite, scope?: ExpressionScope & {
|
|
17
|
+
receivers?: Readonly<Record<string, string>>;
|
|
18
|
+
unavailableReceivers?: Readonly<Record<string, string>>;
|
|
19
|
+
}): import("./expression/index.js").CompiledExpression;
|
|
20
|
+
export declare function compileConditionAt<Subject extends SafeFunctionSubject>(condition: unknown, site: SchemaSite): CompiledPredicate<Subject>;
|
|
21
|
+
export declare function compileCondition<Subject extends SafeFunctionSubject>(condition: RawCondition, options?: CompileOptions): CompiledPredicate<Subject>;
|
|
22
|
+
export type TemplateSegment = {
|
|
23
|
+
literal: string;
|
|
24
|
+
} | {
|
|
25
|
+
name: string;
|
|
26
|
+
field?: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function parseTemplate(template: string, site?: Pick<SchemaSite, 'path' | 'definition'>): TemplateSegment[];
|
|
29
|
+
export declare function compileTemplate<Subject extends SafeFunctionSubject>(template: string, site?: SchemaSite): CompiledValue<Subject>;
|
|
30
|
+
export declare function compileValueAt<Subject extends SafeFunctionSubject>(value: unknown, site: SchemaSite): CompiledValue<Subject>;
|
|
31
|
+
export declare function compileValue<Subject extends SafeFunctionSubject>(value: RawValue, options?: CompileOptions): CompiledValue<Subject>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { compileExpression } from './expression/index.js';
|
|
2
|
+
import { compileMatchTest } from './matchTests.js';
|
|
3
|
+
import { at, conditionShape, INSTANCE_EXPRESSION_SCOPE, invalidAt, isUnsafeKey, matchForm, readOwn, safeFunctionSchema, unrecognized, valueShape, } from './schema.js';
|
|
4
|
+
import { hasNoValue } from './values.js';
|
|
5
|
+
export { compareFinite, hasNoValue, isAbsent, isEmpty, isTrue, looseEquals, } from './values.js';
|
|
6
|
+
const DEFAULT_DEFINITION = 'safe function definition';
|
|
7
|
+
export function invalid(message, fragment) {
|
|
8
|
+
return invalidAt({ path: '', definition: DEFAULT_DEFINITION }, message, fragment);
|
|
9
|
+
}
|
|
10
|
+
export const readOwnName = (name, _site, field) => field === undefined
|
|
11
|
+
? (subject) => readOwn(subject, name)
|
|
12
|
+
: (subject) => readOwn(readOwn(subject, name), field);
|
|
13
|
+
export const safeFunctionCompilers = {
|
|
14
|
+
condition: (fragment, site) => compileConditionAt(fragment, site),
|
|
15
|
+
matchTest: (fragment, site) => compileMatchTest(fragment, site),
|
|
16
|
+
value: (fragment, site) => compileValueAt(fragment, site),
|
|
17
|
+
};
|
|
18
|
+
export function createSite(options = {}, schema = safeFunctionSchema, compilers = safeFunctionCompilers) {
|
|
19
|
+
return {
|
|
20
|
+
path: options.path ?? '',
|
|
21
|
+
definition: options.definition ?? DEFAULT_DEFINITION,
|
|
22
|
+
schema,
|
|
23
|
+
compilers,
|
|
24
|
+
readName: options.readName ?? readOwnName,
|
|
25
|
+
expression: options.expression ?? INSTANCE_EXPRESSION_SCOPE,
|
|
26
|
+
call: options.call,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function readNameAt(name, site, field) {
|
|
30
|
+
return (site.readName ?? readOwnName)(name, site, field);
|
|
31
|
+
}
|
|
32
|
+
export function compileExpressionAt(source, site, scope = site.expression ?? INSTANCE_EXPRESSION_SCOPE) {
|
|
33
|
+
try {
|
|
34
|
+
return compileExpression(source, {
|
|
35
|
+
params: scope.params,
|
|
36
|
+
implicitScope: scope.implicitScope,
|
|
37
|
+
receivers: scope.receivers,
|
|
38
|
+
unavailableReceivers: scope.unavailableReceivers,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
return invalidAt(site, error.message);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function compileConditionAt(condition, site) {
|
|
46
|
+
if (typeof condition === 'function') {
|
|
47
|
+
return condition;
|
|
48
|
+
}
|
|
49
|
+
if (!condition || typeof condition !== 'object' || Array.isArray(condition)) {
|
|
50
|
+
return unrecognized(conditionShape, condition, site);
|
|
51
|
+
}
|
|
52
|
+
const entries = Object.entries(condition).map(([name, test]) => {
|
|
53
|
+
const entrySite = at(site, name);
|
|
54
|
+
if (isUnsafeKey(name)) {
|
|
55
|
+
invalidAt(entrySite, `the name "${name}" is not allowed`, condition);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
read: readNameAt(name, entrySite),
|
|
59
|
+
test: compileMatchTest(test, entrySite),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
return (subject, context) => entries.every(({ read, test }) => test(read(subject, context), subject, context));
|
|
63
|
+
}
|
|
64
|
+
export function compileCondition(condition, options = {}) {
|
|
65
|
+
return compileConditionAt(condition, createSite(options));
|
|
66
|
+
}
|
|
67
|
+
export function parseTemplate(template, site = {
|
|
68
|
+
path: '',
|
|
69
|
+
definition: DEFAULT_DEFINITION,
|
|
70
|
+
}) {
|
|
71
|
+
if (typeof template !== 'string') {
|
|
72
|
+
invalidAt(site, 'template must be a string', template);
|
|
73
|
+
}
|
|
74
|
+
const segments = [];
|
|
75
|
+
let literal = '';
|
|
76
|
+
for (let i = 0; i < template.length; i++) {
|
|
77
|
+
const char = template[i];
|
|
78
|
+
if (char === '\\' && (template[i + 1] === '{' || template[i + 1] === '}')) {
|
|
79
|
+
literal += template[i + 1];
|
|
80
|
+
i++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (char !== '{') {
|
|
84
|
+
literal += char;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const end = template.indexOf('}', i + 1);
|
|
88
|
+
if (end === -1) {
|
|
89
|
+
invalidAt(site, 'template has an unclosed "{"', template);
|
|
90
|
+
}
|
|
91
|
+
const placeholder = template.slice(i + 1, end).trim();
|
|
92
|
+
if (!placeholder) {
|
|
93
|
+
invalidAt(site, 'template has an empty "{}" placeholder', template);
|
|
94
|
+
}
|
|
95
|
+
const [name, field, ...rest] = placeholder.split('.');
|
|
96
|
+
if (!name || field === '' || rest.length) {
|
|
97
|
+
invalidAt(site, `template placeholder "{${placeholder}}" must be a name, or a name and one field`, template);
|
|
98
|
+
}
|
|
99
|
+
if (literal) {
|
|
100
|
+
segments.push({ literal });
|
|
101
|
+
literal = '';
|
|
102
|
+
}
|
|
103
|
+
segments.push(field === undefined ? { name } : { name, field });
|
|
104
|
+
i = end;
|
|
105
|
+
}
|
|
106
|
+
if (literal) {
|
|
107
|
+
segments.push({ literal });
|
|
108
|
+
}
|
|
109
|
+
return segments;
|
|
110
|
+
}
|
|
111
|
+
export function compileTemplate(template, site = createSite()) {
|
|
112
|
+
const parts = parseTemplate(template, site).map((segment) => 'literal' in segment
|
|
113
|
+
? () => segment.literal
|
|
114
|
+
: readNameAt(segment.name, site, segment.field));
|
|
115
|
+
return (subject, context) => parts
|
|
116
|
+
.map((part) => {
|
|
117
|
+
const value = part(subject, context);
|
|
118
|
+
return hasNoValue(value) ? '' : String(value);
|
|
119
|
+
})
|
|
120
|
+
.join('');
|
|
121
|
+
}
|
|
122
|
+
export function compileValueAt(value, site) {
|
|
123
|
+
if (typeof value === 'function') {
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
if (typeof value === 'string') {
|
|
127
|
+
return readNameAt(value, site);
|
|
128
|
+
}
|
|
129
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
130
|
+
return unrecognized(valueShape, value, site);
|
|
131
|
+
}
|
|
132
|
+
const [form] = matchForm(valueShape, value, site);
|
|
133
|
+
const fields = value;
|
|
134
|
+
if (form === 'template') {
|
|
135
|
+
return compileTemplate(fields.template, at(site, 'template'));
|
|
136
|
+
}
|
|
137
|
+
const evaluate = compileExpressionAt(fields.expression, at(site, 'expression'));
|
|
138
|
+
return (subject, context) => evaluate(subject, context);
|
|
139
|
+
}
|
|
140
|
+
export function compileValue(value, options = {}) {
|
|
141
|
+
return compileValueAt(value, createSite(options));
|
|
142
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|