@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.
Files changed (100) hide show
  1. package/dist/esm/displayset/BaseDisplaySet.d.ts +3 -4
  2. package/dist/esm/displayset/BaseDisplaySet.js +1 -5
  3. package/dist/esm/displayset/IDisplaySet.d.ts +9 -3
  4. package/dist/esm/displayset/ImageStackDisplaySet.d.ts +1 -3
  5. package/dist/esm/displayset/ImageStackDisplaySet.js +0 -1
  6. package/dist/esm/displayset/__fixtures__/scenarioInstances.d.ts +12 -0
  7. package/dist/esm/displayset/__fixtures__/scenarioInstances.js +210 -0
  8. package/dist/esm/displayset/compileSplitRules.d.ts +3 -0
  9. package/dist/esm/displayset/compileSplitRules.js +197 -0
  10. package/dist/esm/displayset/compiledDisplaySetRules.d.ts +3 -0
  11. package/dist/esm/displayset/compiledDisplaySetRules.js +3 -0
  12. package/dist/esm/displayset/createDisplaySetFromGroup.js +54 -19
  13. package/dist/esm/displayset/displaySetContext.d.ts +150 -0
  14. package/dist/esm/displayset/displaySetContext.js +918 -0
  15. package/dist/esm/displayset/displaySetRules.d.ts +5 -0
  16. package/dist/esm/displayset/displaySetRules.js +142 -0
  17. package/dist/esm/displayset/displaySetRules.test.d.ts +1 -0
  18. package/dist/esm/displayset/displaySetRules.test.js +1306 -0
  19. package/dist/esm/displayset/displaySetRulesTypes.d.ts +34 -0
  20. package/dist/esm/displayset/displaySetRulesTypes.js +0 -0
  21. package/dist/esm/displayset/displayset.test.js +422 -130
  22. package/dist/esm/displayset/groupInstancesBySplitRules.d.ts +8 -2
  23. package/dist/esm/displayset/groupInstancesBySplitRules.js +174 -17
  24. package/dist/esm/displayset/index.d.ts +14 -4
  25. package/dist/esm/displayset/index.js +10 -3
  26. package/dist/esm/displayset/isEcgInstance.d.ts +2 -1
  27. package/dist/esm/displayset/isEcgInstance.js +4 -9
  28. package/dist/esm/displayset/isImageInstance.d.ts +2 -1
  29. package/dist/esm/displayset/isImageInstance.js +4 -44
  30. package/dist/esm/displayset/isReconstructableInstances.d.ts +2 -0
  31. package/dist/esm/displayset/isReconstructableInstances.js +91 -0
  32. package/dist/esm/displayset/isVideoInstance.d.ts +2 -1
  33. package/dist/esm/displayset/isVideoInstance.js +4 -30
  34. package/dist/esm/displayset/isWsiInstance.d.ts +2 -1
  35. package/dist/esm/displayset/isWsiInstance.js +4 -6
  36. package/dist/esm/displayset/metadataContext.d.ts +13 -0
  37. package/dist/esm/displayset/metadataContext.js +42 -0
  38. package/dist/esm/displayset/metadataContext.test.d.ts +1 -0
  39. package/dist/esm/displayset/metadataContext.test.js +62 -0
  40. package/dist/esm/displayset/scenarioFixtures.test.d.ts +1 -0
  41. package/dist/esm/displayset/scenarioFixtures.test.js +206 -0
  42. package/dist/esm/displayset/seriesContext.test.d.ts +1 -0
  43. package/dist/esm/displayset/seriesContext.test.js +238 -0
  44. package/dist/esm/displayset/seriesFunctions.d.ts +29 -0
  45. package/dist/esm/displayset/seriesFunctions.js +327 -0
  46. package/dist/esm/displayset/sopClassUidLists.d.ts +14 -0
  47. package/dist/esm/displayset/sopClassUidLists.js +100 -0
  48. package/dist/esm/displayset/sortInstances.d.ts +10 -0
  49. package/dist/esm/displayset/sortInstances.js +33 -0
  50. package/dist/esm/displayset/sortInstances.test.d.ts +1 -0
  51. package/dist/esm/displayset/sortInstances.test.js +56 -0
  52. package/dist/esm/displayset/splitImageIdsBySplitRules.d.ts +3 -3
  53. package/dist/esm/displayset/splitImageIdsBySplitRules.js +5 -5
  54. package/dist/esm/displayset/splitRuleSchema.d.ts +11 -0
  55. package/dist/esm/displayset/splitRuleSchema.js +168 -0
  56. package/dist/esm/displayset/splitRuleSet.d.ts +12 -0
  57. package/dist/esm/displayset/splitRuleSet.js +47 -0
  58. package/dist/esm/displayset/splitRuleSet.test.d.ts +1 -0
  59. package/dist/esm/displayset/splitRuleSet.test.js +119 -0
  60. package/dist/esm/displayset/types.d.ts +37 -4
  61. package/dist/esm/displayset/types.js +15 -0
  62. package/dist/esm/index.d.ts +4 -2
  63. package/dist/esm/index.js +2 -1
  64. package/dist/esm/safeFunctions/compile.d.ts +31 -0
  65. package/dist/esm/safeFunctions/compile.js +142 -0
  66. package/dist/esm/safeFunctions/compile.test.d.ts +1 -0
  67. package/dist/esm/safeFunctions/compile.test.js +82 -0
  68. package/dist/esm/safeFunctions/expression/compiler.d.ts +23 -0
  69. package/dist/esm/safeFunctions/expression/compiler.js +481 -0
  70. package/dist/esm/safeFunctions/expression/expression.test.d.ts +1 -0
  71. package/dist/esm/safeFunctions/expression/expression.test.js +325 -0
  72. package/dist/esm/safeFunctions/expression/index.d.ts +4 -0
  73. package/dist/esm/safeFunctions/expression/index.js +3 -0
  74. package/dist/esm/safeFunctions/expression/parser.d.ts +56 -0
  75. package/dist/esm/safeFunctions/expression/parser.js +248 -0
  76. package/dist/esm/safeFunctions/expression/tokenizer.d.ts +29 -0
  77. package/dist/esm/safeFunctions/expression/tokenizer.js +198 -0
  78. package/dist/esm/safeFunctions/index.d.ts +5 -0
  79. package/dist/esm/safeFunctions/index.js +4 -0
  80. package/dist/esm/safeFunctions/matchTests.d.ts +16 -0
  81. package/dist/esm/safeFunctions/matchTests.js +203 -0
  82. package/dist/esm/safeFunctions/schema.d.ts +72 -0
  83. package/dist/esm/safeFunctions/schema.js +361 -0
  84. package/dist/esm/safeFunctions/types.d.ts +47 -0
  85. package/dist/esm/safeFunctions/types.js +0 -0
  86. package/dist/esm/safeFunctions/values.d.ts +6 -0
  87. package/dist/esm/safeFunctions/values.js +71 -0
  88. package/dist/esm/utilities/Tags.d.ts +1 -0
  89. package/dist/esm/utilities/Tags.js +14 -2
  90. package/dist/esm/utilities/index.d.ts +1 -1
  91. package/dist/esm/utilities/index.js +1 -1
  92. package/dist/esm/utilities/toNumber.d.ts +1 -1
  93. package/dist/esm/utilities/toNumber.js +1 -1
  94. package/dist/esm/version.d.ts +1 -1
  95. package/dist/esm/version.js +1 -1
  96. package/package.json +2 -2
  97. package/dist/esm/displayset/defaultDisplaySetSplitRules.d.ts +0 -2
  98. package/dist/esm/displayset/defaultDisplaySetSplitRules.js +0 -93
  99. package/dist/esm/displayset/viewportTypes.d.ts +0 -4
  100. package/dist/esm/displayset/viewportTypes.js +0 -13
@@ -0,0 +1,918 @@
1
+ import { asArray, logging } from '@cornerstonejs/utils';
2
+ import { at, collectReads, didYouMean, compileExpressionAt, FORBIDDEN_PROPERTIES, hasNoValue, hasOwn, invalidAt, isUnsafeKey, matchForm, parseExpressionSource, parseTemplate, readOwn, RuleCompileError, } from '../safeFunctions/index.js';
3
+ import { keywordIgnoringCase, resolveHexFromKeyword } from '../utilities/Tags.js';
4
+ import { sortInstancesCopy } from './sortInstances.js';
5
+ import { instanceKey } from './seriesFunctions.js';
6
+ import { bulkAnnotationSopClassUids, chartSopClassUids, ecgSopClassUids, encapsulatedDocumentSopClassUids, imageSopClassUids, longVideoMinFrames, longVideoSopClassUids, parametricMapSopClassUids, rtStructureSetSopClassUids, segmentationSopClassUids, structuredReportSopClassUids, videoSopClassUids, videoTransferSyntaxUids, wholeSlideSopClassUids, } from './sopClassUidLists.js';
7
+ import { ATTRIBUTE_ONLY_NAMES, splitRuleSchema } from './splitRuleSchema.js';
8
+ const log = logging.metadataLog.getLogger('displayset', 'displaySetContext');
9
+ export const DISPLAY_SET_CONTEXT_FACT = 'displaySetContext';
10
+ export const defaultMetadataContext = {
11
+ timeClustersMaxGap: 30,
12
+ timeClusters: {
13
+ function: 'timeClusters',
14
+ args: { maxGap: 'timeClustersMaxGap' },
15
+ },
16
+ longTimeClusters: { function: 'timeClusters', args: { maxGap: 60 } },
17
+ sweepCount: 'timeClusters.clusters',
18
+ sweepStart: 'timeClusters.start',
19
+ acquisitionSeconds: 'timeClusters.time',
20
+ sweepOffset: 'round(sweepStart - timeClusters.first)',
21
+ planeGeometryTolerance: 0.1,
22
+ planeGeometry: {
23
+ function: 'planeGeometry',
24
+ args: { tolerance: 'planeGeometryTolerance' },
25
+ },
26
+ sliceSpacing: 'planeGeometry.spacing',
27
+ sliceIndex: 'planeGeometry.index',
28
+ sliceDistance: 'planeGeometry.distance',
29
+ revisionChains: { function: 'revisionChains' },
30
+ revisionChainUid: 'revisionChains.active',
31
+ isActiveRevision: 'SOPInstanceUID == revisionChainUid',
32
+ seriesInstanceCount: 'series.instances.count()',
33
+ isMultiFrame: 'NumberOfFrames > 1',
34
+ imageId: { attribute: 'imageId' },
35
+ imageSopClassUids,
36
+ videoSopClassUids,
37
+ videoTransferSyntaxUids,
38
+ longVideoSopClassUids,
39
+ longVideoMinFrames,
40
+ ecgSopClassUids,
41
+ wholeSlideSopClassUids,
42
+ segmentationSopClassUids,
43
+ rtStructureSetSopClassUids,
44
+ parametricMapSopClassUids,
45
+ structuredReportSopClassUids,
46
+ encapsulatedDocumentSopClassUids,
47
+ bulkAnnotationSopClassUids,
48
+ chartSopClassUids,
49
+ isImage: 'SOPClassUID in imageSopClassUids',
50
+ transferSyntaxKnown: 'AvailableTransferSyntaxUID != null || TransferSyntaxUID != null',
51
+ isVideo: 'SOPClassUID in videoSopClassUids || ' +
52
+ 'AvailableTransferSyntaxUID in videoTransferSyntaxUids || ' +
53
+ 'TransferSyntaxUID in videoTransferSyntaxUids || ' +
54
+ '(!transferSyntaxKnown && SOPClassUID in longVideoSopClassUids && ' +
55
+ 'NumberOfFrames >= longVideoMinFrames)',
56
+ isEcg: 'SOPClassUID in ecgSopClassUids',
57
+ isWsi: "SOPClassUID in wholeSlideSopClassUids || Modality == 'SM'",
58
+ isSegmentation: 'SOPClassUID in segmentationSopClassUids',
59
+ isRtStructureSet: 'SOPClassUID in rtStructureSetSopClassUids',
60
+ isParametricMap: 'SOPClassUID in parametricMapSopClassUids',
61
+ isStructuredReport: 'SOPClassUID in structuredReportSopClassUids',
62
+ isEncapsulatedDocument: 'SOPClassUID in encapsulatedDocumentSopClassUids',
63
+ isBulkAnnotation: 'SOPClassUID in bulkAnnotationSopClassUids',
64
+ isChart: 'SOPClassUID in chartSopClassUids',
65
+ };
66
+ const CONTEXT_NAME = /^[a-z][A-Za-z0-9_]*$/;
67
+ const RESERVED_LITERALS = new Set([
68
+ 'true',
69
+ 'false',
70
+ 'null',
71
+ 'undefined',
72
+ 'defined',
73
+ ]);
74
+ const HEX_TAG = /^[0-9A-Fa-f]{8}$/;
75
+ export function isTagKeyword(name) {
76
+ return resolveHexFromKeyword(name) !== undefined;
77
+ }
78
+ const DISPLAY_SETS_RESERVED = 'displaySets aggregates are not available yet; see https://github.com/cornerstonejs/cornerstone3D/issues/3000';
79
+ const EXPRESSION_SCOPE = Object.freeze({
80
+ params: Object.freeze(['#values', '#instances']),
81
+ implicitScope: '#values',
82
+ receivers: Object.freeze({ series: '#instances' }),
83
+ unavailableReceivers: Object.freeze({
84
+ displaySet: 'displaySet.instances aggregates are available only in the { "expression" } form of attributes',
85
+ displaySets: DISPLAY_SETS_RESERVED,
86
+ }),
87
+ });
88
+ const ATTRIBUTE_EXPRESSION_SCOPE = Object.freeze({
89
+ params: Object.freeze(['#values', '#instances', '#displaySetInstances']),
90
+ implicitScope: '#values',
91
+ receivers: Object.freeze({
92
+ series: '#instances',
93
+ displaySet: '#displaySetInstances',
94
+ }),
95
+ unavailableReceivers: Object.freeze({ displaySets: DISPLAY_SETS_RESERVED }),
96
+ });
97
+ const RESERVED_RECEIVERS = new Set(['series', 'displaySet', 'displaySets']);
98
+ const DISPLAY_SET_PLACES = new Set(['sortBy', 'attributes']);
99
+ const DISPLAY_SET_PREFIX_ONLY = "the 'displaySet.' prefix is valid only in sortBy and attributes";
100
+ let functionCount = 0;
101
+ const functionIds = new WeakMap();
102
+ function identityOf(raw) {
103
+ return JSON.stringify(raw, (_key, value) => {
104
+ if (typeof value !== 'function') {
105
+ return value;
106
+ }
107
+ let id = functionIds.get(value);
108
+ if (!id) {
109
+ id = `#function${++functionCount}`;
110
+ functionIds.set(value, id);
111
+ }
112
+ return id;
113
+ });
114
+ }
115
+ function unknownName(name, place, candidates) {
116
+ const suggestion = didYouMean(name, [
117
+ keywordIgnoringCase(name),
118
+ ...candidates,
119
+ ]);
120
+ if (suggestion) {
121
+ return `unknown name '${name}'${suggestion}`;
122
+ }
123
+ let hint;
124
+ if (HEX_TAG.test(name)) {
125
+ hint = `a hex tag needs a declaration in the context, e.g. "myValue": { "attribute": "${name}" }`;
126
+ }
127
+ else if (RESERVED_LITERALS.has(name)) {
128
+ hint = `'${name}' is a literal; write X == ${name} in a context entry`;
129
+ }
130
+ else if (/^[a-z]/.test(name)) {
131
+ hint = 'no context entry has this name';
132
+ }
133
+ else {
134
+ hint = `it is not a keyword of the DICOM dictionary; a private, vendor or synthetic attribute needs a declaration, e.g. "myValue": { "attribute": "${name}" }`;
135
+ }
136
+ const fixedText = place === 'attributes'
137
+ ? `; fixed text is { "template": ${JSON.stringify(name)} }`
138
+ : '';
139
+ return `unknown name '${name}': ${hint}${fixedText}`;
140
+ }
141
+ function checkContextName(name, site) {
142
+ if (isUnsafeKey(name) || FORBIDDEN_PROPERTIES.has(name)) {
143
+ invalidAt(site, `the context name "${name}" is not allowed`);
144
+ }
145
+ if (RESERVED_LITERALS.has(name) || RESERVED_RECEIVERS.has(name)) {
146
+ invalidAt(site, `'${name}' is reserved by the expression language, and cannot be a context name`);
147
+ }
148
+ if (isTagKeyword(name)) {
149
+ invalidAt(site, `the context name '${name}' is a DICOM keyword; a context cannot hide a value of the instance`);
150
+ }
151
+ if (!CONTEXT_NAME.test(name)) {
152
+ invalidAt(site, `the context name '${name}' must start with a lowercase letter, and hold only letters, digits and _`);
153
+ }
154
+ if (ATTRIBUTE_ONLY_NAMES.includes(name)) {
155
+ invalidAt(site, `'${name}' is a value of the display set, and cannot be a context name`);
156
+ }
157
+ }
158
+ function parseExpression(source, site, scope = EXPRESSION_SCOPE) {
159
+ const evaluate = compileExpressionAt(source, site, scope);
160
+ const ast = parseExpressionSource(source);
161
+ return {
162
+ evaluate,
163
+ reads: collectReads(ast, { aggregates: true }),
164
+ single: ast.type === 'identifier' ? ast.name : undefined,
165
+ ast,
166
+ };
167
+ }
168
+ function propertyReads(node) {
169
+ const found = [];
170
+ const walk = (current) => {
171
+ switch (current.type) {
172
+ case 'member':
173
+ found.push(current.property);
174
+ walk(current.object);
175
+ return;
176
+ case 'index':
177
+ if (current.index.type !== 'literal') {
178
+ found.push(undefined);
179
+ }
180
+ else if (typeof current.index.value === 'string') {
181
+ found.push(current.index.value);
182
+ }
183
+ walk(current.object);
184
+ walk(current.index);
185
+ return;
186
+ case 'call':
187
+ current.args.forEach(walk);
188
+ return;
189
+ case 'unary':
190
+ walk(current.argument);
191
+ return;
192
+ case 'binary':
193
+ case 'logical':
194
+ walk(current.left);
195
+ walk(current.right);
196
+ return;
197
+ case 'conditional':
198
+ walk(current.test);
199
+ walk(current.consequent);
200
+ walk(current.alternate);
201
+ return;
202
+ case 'array':
203
+ current.elements.forEach(walk);
204
+ return;
205
+ case 'template':
206
+ current.parts.forEach((part) => part.kind === 'expr' && walk(part.node));
207
+ return;
208
+ }
209
+ };
210
+ walk(node);
211
+ return found;
212
+ }
213
+ export function isFixedValue(raw, arrays = true) {
214
+ return (raw === null ||
215
+ typeof raw === 'boolean' ||
216
+ (typeof raw === 'number' && Number.isFinite(raw)) ||
217
+ (arrays && Array.isArray(raw)));
218
+ }
219
+ function copyFixedValue(raw) {
220
+ return Array.isArray(raw) ? [...raw] : raw;
221
+ }
222
+ function loopText(entries) {
223
+ return entries
224
+ .map(({ name, ruleId }) => ruleId === undefined ? name : `${name} (rule '${ruleId}')`)
225
+ .join(' -> ');
226
+ }
227
+ export function withInstances(series, instances) {
228
+ const values = readOwn(series, DISPLAY_SET_CONTEXT_FACT);
229
+ if (!(values instanceof DisplaySetContextValues)) {
230
+ return series;
231
+ }
232
+ const keyOf = (instance) => instanceKey(instance) ?? instance;
233
+ const known = new Set(values.instances.map(keyOf));
234
+ const added = instances.filter((instance) => !known.has(keyOf(instance)));
235
+ if (!added.length) {
236
+ return series;
237
+ }
238
+ return {
239
+ ...series,
240
+ [DISPLAY_SET_CONTEXT_FACT]: new DisplaySetContextValues(sortInstancesCopy([...values.instances, ...added])),
241
+ };
242
+ }
243
+ export class DisplaySetContextValues {
244
+ constructor(instances, seriesInstances = instances, split) {
245
+ this.instances = instances;
246
+ this.seriesInstances = seriesInstances;
247
+ this.split = split;
248
+ this.runtimes = new WeakMap();
249
+ }
250
+ runtime(program) {
251
+ let runtime = this.runtimes.get(program);
252
+ if (!runtime) {
253
+ runtime = new ContextRuntime(this.instances, this.seriesInstances, this.split?.runtime(program));
254
+ this.runtimes.set(program, runtime);
255
+ }
256
+ return runtime;
257
+ }
258
+ }
259
+ export function displaySetContextFor(series, instances) {
260
+ const values = readOwn(series, DISPLAY_SET_CONTEXT_FACT);
261
+ return values instanceof DisplaySetContextValues
262
+ ? new DisplaySetContextValues(instances, values.seriesInstances, values)
263
+ : new DisplaySetContextValues(instances);
264
+ }
265
+ export class ContextRuntime {
266
+ constructor(instances, seriesInstances = instances, split) {
267
+ this.instances = instances;
268
+ this.seriesInstances = seriesInstances;
269
+ this.split = split;
270
+ this.seriesValues = new Map();
271
+ this.instanceValues = new Map();
272
+ this.views = new Map();
273
+ this.envs = new Map();
274
+ this.instanceEnvs = new Map();
275
+ this.inProgress = [];
276
+ }
277
+ get seriesRuntime() {
278
+ return this.split ?? this;
279
+ }
280
+ value(bound, instance) {
281
+ if (bound.scope === 'series') {
282
+ if (this.split && !bound.perDisplaySet) {
283
+ return this.split.value(bound, undefined);
284
+ }
285
+ if (!this.seriesValues.has(bound.key)) {
286
+ this.seriesValues.set(bound.key, this.compute(bound, undefined));
287
+ }
288
+ return this.seriesValues.get(bound.key);
289
+ }
290
+ if (!instance) {
291
+ return undefined;
292
+ }
293
+ let values = this.instanceValues.get(instance);
294
+ if (!values) {
295
+ values = new Map();
296
+ this.instanceValues.set(instance, values);
297
+ }
298
+ if (!values.has(bound.key)) {
299
+ values.set(bound.key, this.compute(bound, instance));
300
+ }
301
+ return values.get(bound.key);
302
+ }
303
+ compute(bound, instance) {
304
+ const start = this.inProgress.findIndex((entry) => entry.bound === bound && entry.instance === instance);
305
+ if (start !== -1) {
306
+ const loop = [
307
+ ...this.inProgress.slice(start).map((entry) => entry.bound),
308
+ bound,
309
+ ];
310
+ throw new RuleCompileError('display set rules', bound.name, `Invalid display set rules: the context entries form a loop: ${loopText(loop)}`);
311
+ }
312
+ this.inProgress.push({ bound, instance });
313
+ try {
314
+ return bound.compute(this, instance);
315
+ }
316
+ finally {
317
+ this.inProgress.pop();
318
+ }
319
+ }
320
+ groupView(bound, instance) {
321
+ const result = this.value(bound, instance);
322
+ const fields = bound.group?.fields;
323
+ if (!instance || !fields || !result || typeof result !== 'object') {
324
+ return result;
325
+ }
326
+ let views = this.views.get(instance);
327
+ if (!views) {
328
+ views = new Map();
329
+ this.views.set(instance, views);
330
+ }
331
+ if (!views.has(bound.key)) {
332
+ const key = instanceKey(instance);
333
+ const view = Object.create(null);
334
+ for (const [field, scope] of Object.entries(fields)) {
335
+ const value = readOwn(result, field);
336
+ view[field] =
337
+ scope === 'instance'
338
+ ? key === undefined
339
+ ? undefined
340
+ : readOwn(value, key)
341
+ : value;
342
+ }
343
+ views.set(bound.key, view);
344
+ }
345
+ return views.get(bound.key);
346
+ }
347
+ read(name, dependency, instance, extras) {
348
+ if (!dependency) {
349
+ return undefined;
350
+ }
351
+ switch (dependency.kind) {
352
+ case 'keyword':
353
+ return instance ? readOwn(instance, name) : undefined;
354
+ case 'attribute':
355
+ return readOwn(extras, name);
356
+ case 'entry':
357
+ return dependency.bound.group
358
+ ? this.groupView(dependency.bound, instance)
359
+ : this.value(dependency.bound, instance);
360
+ case 'series':
361
+ return this.seriesRuntime.read(name, dependency.inner, instance, extras);
362
+ case 'displaySet': {
363
+ const fields = Object.create(null);
364
+ dependency.fields.forEach((field, fieldName) => {
365
+ fields[fieldName] = this.read(fieldName, field, instance, extras);
366
+ });
367
+ return fields;
368
+ }
369
+ }
370
+ }
371
+ env(bound, instance, extras, fresh = false) {
372
+ const cacheable = !extras && !fresh;
373
+ let byInstance = cacheable ? this.envs.get(bound.key) : undefined;
374
+ const cached = byInstance?.get(instance);
375
+ if (cached) {
376
+ return cached;
377
+ }
378
+ const env = new Proxy(Object.create(null), {
379
+ has: () => true,
380
+ get: (_target, name) => {
381
+ if (typeof name !== 'string') {
382
+ return undefined;
383
+ }
384
+ const dependency = bound.deps.get(name) ??
385
+ bound.layer?.dynamicDependency(name, bound.definition);
386
+ return this.read(name, dependency, instance, extras);
387
+ },
388
+ });
389
+ if (cacheable) {
390
+ if (!byInstance) {
391
+ byInstance = new Map();
392
+ this.envs.set(bound.key, byInstance);
393
+ }
394
+ byInstance.set(instance, env);
395
+ }
396
+ return env;
397
+ }
398
+ envsOf(bound, instances = this.seriesInstances, extras) {
399
+ if (extras || instances !== this.seriesInstances) {
400
+ return instances.map((instance) => this.env(bound, instance, extras));
401
+ }
402
+ if (this.split && !bound.perDisplaySet) {
403
+ return this.split.envsOf(bound, this.split.seriesInstances);
404
+ }
405
+ let envs = this.instanceEnvs.get(bound.key);
406
+ if (!envs) {
407
+ envs = instances.map((instance) => this.env(bound, instance));
408
+ this.instanceEnvs.set(bound.key, envs);
409
+ }
410
+ return envs;
411
+ }
412
+ }
413
+ export class ContextLayer {
414
+ constructor(program, ruleId, local) {
415
+ this.program = program;
416
+ this.ruleId = ruleId;
417
+ this.local = local;
418
+ this.memo = new Map();
419
+ this.dynamic = new Map();
420
+ }
421
+ resolve(name, from) {
422
+ if (from && from.ruleId !== undefined && from.name === name) {
423
+ return this.program.global.get(name);
424
+ }
425
+ return this.local.get(name) ?? this.program.global.get(name);
426
+ }
427
+ dynamicDependency(name, from) {
428
+ let byName = this.dynamic.get(from);
429
+ if (!byName) {
430
+ byName = new Map();
431
+ this.dynamic.set(from, byName);
432
+ }
433
+ if (!byName.has(name)) {
434
+ let dependency;
435
+ if (this.program.isDenied(name)) {
436
+ log.warn(`a code entry of the context read '${name}', which is on the deny list of the host; the read gives undefined`);
437
+ }
438
+ else if (isTagKeyword(name)) {
439
+ dependency = { kind: 'keyword' };
440
+ }
441
+ else {
442
+ const definition = this.resolve(name, from);
443
+ if (definition) {
444
+ dependency = {
445
+ kind: 'entry',
446
+ bound: this.program.bind(definition, this, []),
447
+ };
448
+ }
449
+ else if (ATTRIBUTE_ONLY_NAMES.includes(name)) {
450
+ dependency = { kind: 'attribute' };
451
+ }
452
+ }
453
+ byName.set(name, dependency);
454
+ }
455
+ return byName.get(name);
456
+ }
457
+ }
458
+ export class ContextProgram {
459
+ constructor(context, options) {
460
+ this.global = new Map();
461
+ this.bounds = new Map();
462
+ this.fallbackLogged = false;
463
+ this.seriesHook = ({ instances, series }) => hasOwn(series, DISPLAY_SET_CONTEXT_FACT)
464
+ ? {}
465
+ : { [DISPLAY_SET_CONTEXT_FACT]: new DisplaySetContextValues(instances) };
466
+ this.seriesFunctions = options.seriesFunctions;
467
+ this.denied = new Set(options.deniedNames ?? []);
468
+ this.deniedTags = new Set([...this.denied].map(resolveHexFromKeyword).filter(Boolean));
469
+ for (const definition of this.parseLayer(context, options.site, undefined)) {
470
+ this.global.set(definition.name, definition);
471
+ }
472
+ this.empty = new ContextLayer(this, undefined, new Map());
473
+ for (const definition of this.global.values()) {
474
+ this.bind(definition, this.empty, []);
475
+ }
476
+ }
477
+ isDenied(name) {
478
+ return this.denied.has(name);
479
+ }
480
+ parseExpression(source, site, scope) {
481
+ const parsed = parseExpression(source, site, scope);
482
+ if (this.denied.size) {
483
+ for (const property of propertyReads(parsed.ast)) {
484
+ if (property === undefined) {
485
+ invalidAt(site, 'a computed field name is not allowed when the host has a deny list; use a literal index, e.g. ViewCodeSequence[0].CodeValue');
486
+ }
487
+ if (this.denied.has(property)) {
488
+ invalidAt(site, `the name '${property}' is on the deny list of the host, so no rule can read it`);
489
+ }
490
+ }
491
+ }
492
+ return parsed;
493
+ }
494
+ runtimeFrom(context, fallback) {
495
+ const displaySet = readOwn(context, 'displaySet');
496
+ if (displaySet instanceof DisplaySetContextValues) {
497
+ return displaySet.runtime(this);
498
+ }
499
+ const values = readOwn(readOwn(context, 'series'), DISPLAY_SET_CONTEXT_FACT);
500
+ if (values instanceof DisplaySetContextValues) {
501
+ return values.runtime(this);
502
+ }
503
+ if (!this.fallbackLogged) {
504
+ this.fallbackLogged = true;
505
+ log.debug('a split rule hook was called with no split context; its context values cover only the instances of the call');
506
+ }
507
+ return new DisplaySetContextValues(fallback ? asArray(fallback) : []).runtime(this);
508
+ }
509
+ instanceValue(name, instance) {
510
+ const definition = this.global.get(name);
511
+ if (!definition) {
512
+ throw new Error(`the metadata context has no entry '${name}'`);
513
+ }
514
+ const bound = this.bind(definition, this.empty, []);
515
+ if (bound.scope !== 'instance') {
516
+ throw new Error(`the context entry '${name}' has the series scope, so one instance cannot give its value`);
517
+ }
518
+ return new DisplaySetContextValues([instance])
519
+ .runtime(this)
520
+ .read(name, { kind: 'entry', bound }, instance);
521
+ }
522
+ layer(ruleId, context, site) {
523
+ if (context === undefined) {
524
+ return this.empty;
525
+ }
526
+ const local = new Map();
527
+ for (const definition of this.parseLayer(context, site, ruleId)) {
528
+ local.set(definition.name, definition);
529
+ log.debug(this.global.has(definition.name)
530
+ ? `rule '${ruleId}': the context entry '${definition.name}' replaces the global entry for this rule`
531
+ : `rule '${ruleId}': the context entry '${definition.name}' defines a new name for this rule`);
532
+ }
533
+ const layer = new ContextLayer(this, ruleId, local);
534
+ for (const definition of local.values()) {
535
+ this.bind(definition, layer, []);
536
+ }
537
+ return layer;
538
+ }
539
+ readerAt(layer, name, site, options = {}) {
540
+ const place = options.place ?? '';
541
+ let { field } = options;
542
+ let target = name;
543
+ const prefixed = name === 'displaySet' || name.startsWith('displaySet.');
544
+ if (prefixed) {
545
+ if (!DISPLAY_SET_PLACES.has(place)) {
546
+ invalidAt(site, DISPLAY_SET_PREFIX_ONLY);
547
+ }
548
+ const path = name.split('.').slice(1);
549
+ if (field !== undefined) {
550
+ path.push(field);
551
+ }
552
+ if (!path.length || path.length > 2 || path.some((part) => !part)) {
553
+ invalidAt(site, `'${name}' must be displaySet.<name> or displaySet.<name>.<field>, e.g. displaySet.sliceSpacing`);
554
+ }
555
+ [target, field] = path;
556
+ }
557
+ let dependency = this.dependencyOf({ name: target, field, inAggregate: false }, undefined, layer, [], site, place);
558
+ if (!prefixed && DISPLAY_SET_PLACES.has(place)) {
559
+ dependency = { kind: 'series', inner: dependency };
560
+ }
561
+ return (runtime, instance, extras) => {
562
+ const value = runtime.read(target, dependency, instance, extras);
563
+ return field === undefined ? value : readOwn(value, field);
564
+ };
565
+ }
566
+ attributeExpression(layer, source, site) {
567
+ const { evaluate, reads } = this.parseExpression(source, site, ATTRIBUTE_EXPRESSION_SCOPE);
568
+ const deps = new Map();
569
+ const displaySetFields = new Map();
570
+ for (const read of reads) {
571
+ if (read.name === 'displaySet') {
572
+ if (read.field === undefined) {
573
+ invalidAt(site, "'displaySet' needs a name, e.g. displaySet.sliceSpacing");
574
+ }
575
+ displaySetFields.set(read.field, this.dependencyOf({ name: read.field, inAggregate: read.inAggregate }, undefined, layer, [], site, 'attributes'));
576
+ continue;
577
+ }
578
+ const dependency = this.dependencyOf(read, undefined, layer, [], site, 'attributes');
579
+ deps.set(read.name, { kind: 'series', inner: dependency });
580
+ }
581
+ if (displaySetFields.size) {
582
+ deps.set('displaySet', { kind: 'displaySet', fields: displaySetFields });
583
+ }
584
+ const perDisplaySet = displaySetFields.size > 0 ||
585
+ [...deps.values()].some((dependency) => dependency.kind === 'series' && dependency.inner.kind === 'attribute');
586
+ const scope = { key: `#attribute:${site.path}`, deps, perDisplaySet };
587
+ return (runtime, instances, extras) => evaluate(runtime.env(scope, instances[0], extras), perDisplaySet
588
+ ? runtime.envsOf(scope, runtime.seriesInstances, extras)
589
+ : runtime.envsOf(scope), runtime.envsOf(scope, instances, extras));
590
+ }
591
+ bind(definition, layer, stack) {
592
+ const memo = layer.memo.get(definition);
593
+ if (memo) {
594
+ return memo;
595
+ }
596
+ const loopStart = stack.indexOf(definition);
597
+ if (loopStart !== -1) {
598
+ const loop = [...stack.slice(loopStart), definition]
599
+ .map(({ name }) => name)
600
+ .join(' -> ');
601
+ return invalidAt(definition.site, `the context entries form a loop: ${loop}`);
602
+ }
603
+ stack.push(definition);
604
+ try {
605
+ const deps = new Map();
606
+ const closure = new Set();
607
+ if (definition.ruleId !== undefined) {
608
+ closure.add(definition.name);
609
+ }
610
+ if (definition.kind === 'code') {
611
+ layer.local.forEach((_entry, name) => closure.add(name));
612
+ }
613
+ let scope = definition.kind === 'declaration' || definition.kind === 'code'
614
+ ? 'instance'
615
+ : 'series';
616
+ for (const read of definition.reads) {
617
+ const dependency = this.dependencyOf(read, definition, layer, stack, definition.site, 'context');
618
+ deps.set(read.name, dependency);
619
+ if (dependency.kind === 'entry') {
620
+ dependency.bound.closure.forEach((name) => closure.add(name));
621
+ }
622
+ if (!read.inAggregate && readsInstance(read, dependency)) {
623
+ scope = 'instance';
624
+ }
625
+ }
626
+ if (definition.kind === 'group') {
627
+ for (const argument of definition.args) {
628
+ const instanceRead = argument.reads.find((read) => !read.inAggregate && readsInstance(read, deps.get(read.name)));
629
+ if (instanceRead) {
630
+ invalidAt(at(at(definition.site, 'args'), argument.name), `the argument must have one value for the series, but '${instanceRead.name}' has one value for each instance`);
631
+ }
632
+ }
633
+ scope = 'series';
634
+ }
635
+ const replaced = [...closure].sort();
636
+ const level = definition.ruleId === undefined ? '' : '@';
637
+ const key = replaced.length
638
+ ? `${level}${definition.name}{${replaced
639
+ .map((name) => `${name}=${layer.local.get(name)?.identity}`)
640
+ .join(',')}}`
641
+ : `${level}${definition.name}`;
642
+ let bound = this.bounds.get(key);
643
+ if (!bound) {
644
+ bound = this.createBound(definition, layer, key, scope, deps, closure);
645
+ this.bounds.set(key, bound);
646
+ }
647
+ layer.memo.set(definition, bound);
648
+ return bound;
649
+ }
650
+ finally {
651
+ stack.pop();
652
+ }
653
+ }
654
+ dependencyOf(read, from, layer, stack, site, place) {
655
+ const { name, field } = read;
656
+ if (this.denied.has(name)) {
657
+ return invalidAt(site, `the name '${name}' is on the deny list of the host, so no rule can read it`);
658
+ }
659
+ if (name === 'displaySet') {
660
+ return invalidAt(site, DISPLAY_SET_PREFIX_ONLY);
661
+ }
662
+ if (isTagKeyword(name)) {
663
+ if (field !== undefined) {
664
+ return invalidAt(site, `'${name}' is a DICOM keyword and has no field '${field}'; read an item of a sequence with an index, e.g. ${name}[0].${field}`);
665
+ }
666
+ return { kind: 'keyword' };
667
+ }
668
+ const definition = layer.resolve(name, from);
669
+ if (!definition) {
670
+ if (ATTRIBUTE_ONLY_NAMES.includes(name)) {
671
+ if (place === 'attributes') {
672
+ return { kind: 'attribute' };
673
+ }
674
+ return invalidAt(site, `'${name}' is a value of the display set, and only attributes can read it`);
675
+ }
676
+ if (from && from.ruleId !== undefined && from.name === name) {
677
+ return invalidAt(site, `the rule entry '${name}' reads its own name, but the global context has no entry '${name}'`);
678
+ }
679
+ return invalidAt(site, unknownName(name, place, [
680
+ ...layer.local.keys(),
681
+ ...this.global.keys(),
682
+ ...ATTRIBUTE_ONLY_NAMES,
683
+ ]));
684
+ }
685
+ const bound = this.bind(definition, layer, stack);
686
+ if (read.indexed && bound.group) {
687
+ return invalidAt(site, `read a field of the group '${name}' by path, e.g. ${name}.${Object.keys(bound.group.fields ?? {})[0] ?? 'field'}`);
688
+ }
689
+ if (field !== undefined) {
690
+ if (!bound.group) {
691
+ return invalidAt(site, `'${name}' is not a group, so '${name}.${field}' reads nothing`);
692
+ }
693
+ const { fields } = bound.group;
694
+ if (fields && !hasOwn(fields, field)) {
695
+ return invalidAt(site, `the group '${name}' has no field '${field}'${didYouMean(field, Object.keys(fields))}; fields: ${Object.keys(fields).join(', ')}`);
696
+ }
697
+ }
698
+ return { kind: 'entry', bound };
699
+ }
700
+ createBound(definition, layer, key, scope, deps, closure) {
701
+ const bound = {
702
+ key,
703
+ name: definition.name,
704
+ scope,
705
+ deps,
706
+ closure,
707
+ ruleId: definition.ruleId,
708
+ perDisplaySet: definition.kind === 'code' ||
709
+ definition.kind === 'group' ||
710
+ [...deps.values()].some((dependency) => dependency.kind === 'entry' && dependency.bound.perDisplaySet),
711
+ compute: () => undefined,
712
+ };
713
+ switch (definition.kind) {
714
+ case 'fixed': {
715
+ const { value } = definition;
716
+ bound.compute = () => value;
717
+ break;
718
+ }
719
+ case 'declaration': {
720
+ const { attribute } = definition;
721
+ bound.compute = (_runtime, instance) => readOwn(instance, attribute);
722
+ break;
723
+ }
724
+ case 'code': {
725
+ const { fn } = definition;
726
+ bound.layer = layer;
727
+ bound.definition = definition;
728
+ bound.compute = (runtime, instance) => instance
729
+ ? fn(instance, runtime.env(bound, instance, undefined, true))
730
+ : undefined;
731
+ break;
732
+ }
733
+ case 'expression': {
734
+ const target = definition.aliasOf !== undefined
735
+ ? deps.get(definition.aliasOf)
736
+ : undefined;
737
+ if (target?.kind === 'entry' && target.bound.group) {
738
+ const group = target.bound;
739
+ bound.group = group.group;
740
+ bound.scope = 'series';
741
+ bound.compute = (runtime) => runtime.value(group, undefined);
742
+ break;
743
+ }
744
+ const { evaluate } = definition;
745
+ bound.compute = (runtime, instance) => evaluate(runtime.env(bound, instance), runtime.envsOf(bound));
746
+ break;
747
+ }
748
+ case 'template': {
749
+ const parts = definition.segments.map((segment) => 'literal' in segment
750
+ ? () => segment.literal
751
+ : (runtime, instance) => {
752
+ const value = runtime.read(segment.name, deps.get(segment.name), instance);
753
+ return segment.field === undefined
754
+ ? value
755
+ : readOwn(value, segment.field);
756
+ });
757
+ bound.compute = (runtime, instance) => parts
758
+ .map((part) => {
759
+ const value = part(runtime, instance);
760
+ return hasNoValue(value) ? '' : String(value);
761
+ })
762
+ .join('');
763
+ break;
764
+ }
765
+ case 'group': {
766
+ const { compute, args } = definition;
767
+ bound.group = { fields: definition.fields };
768
+ bound.compute = (runtime) => {
769
+ const values = {};
770
+ for (const argument of args) {
771
+ values[argument.name] = argument.evaluate
772
+ ? argument.evaluate(runtime.env(bound, undefined), runtime.envsOf(bound))
773
+ : argument.fixed;
774
+ }
775
+ return compute(runtime.instances, {
776
+ args: Object.freeze(values),
777
+ });
778
+ };
779
+ break;
780
+ }
781
+ }
782
+ return bound;
783
+ }
784
+ parseLayer(context, site, ruleId) {
785
+ if (!context || typeof context !== 'object' || Array.isArray(context)) {
786
+ return invalidAt(site, 'expected an object of context entries, keyed by context name', context);
787
+ }
788
+ return Object.entries(context).map(([name, raw]) => {
789
+ const entrySite = at(site, name);
790
+ checkContextName(name, entrySite);
791
+ if (this.denied.has(name)) {
792
+ invalidAt(entrySite, `the context name '${name}' is on the deny list of the host`);
793
+ }
794
+ return this.parseEntry(name, raw, entrySite, ruleId);
795
+ });
796
+ }
797
+ parseEntry(name, raw, site, ruleId) {
798
+ const base = { name, ruleId, site, identity: identityOf(raw) };
799
+ if (typeof raw === 'function') {
800
+ return {
801
+ ...base,
802
+ kind: 'code',
803
+ reads: [],
804
+ fn: raw,
805
+ };
806
+ }
807
+ if (isFixedValue(raw)) {
808
+ return { ...base, kind: 'fixed', reads: [], value: copyFixedValue(raw) };
809
+ }
810
+ if (typeof raw === 'string') {
811
+ const { evaluate, reads, single } = this.parseExpression(raw, site);
812
+ return { ...base, kind: 'expression', reads, evaluate, aliasOf: single };
813
+ }
814
+ if (!raw || typeof raw !== 'object') {
815
+ return invalidAt(site, 'expected a fixed value (a number, a boolean, null or an array), an expression string, { attribute }, { template }, { function, args? }, or a function', raw);
816
+ }
817
+ const [form] = matchForm(splitRuleSchema.contextEntry, raw, site);
818
+ const fields = raw;
819
+ if (form === 'declaration') {
820
+ if (!fields.attribute) {
821
+ return invalidAt(at(site, 'attribute'), 'expected a non-empty string');
822
+ }
823
+ const attribute = String(fields.attribute);
824
+ if (this.denied.has(attribute) ||
825
+ this.deniedTags.has(attribute.replace(/^x/i, '').toUpperCase())) {
826
+ return invalidAt(at(site, 'attribute'), `the attribute '${attribute}' is on the deny list of the host, so no rule can read it`);
827
+ }
828
+ return {
829
+ ...base,
830
+ kind: 'declaration',
831
+ reads: [],
832
+ attribute: fields.attribute,
833
+ vr: fields.vr,
834
+ vm: fields.vm,
835
+ };
836
+ }
837
+ if (form === 'template') {
838
+ const segments = parseTemplate(fields.template, at(site, 'template'));
839
+ const reads = segments.flatMap((segment) => 'literal' in segment
840
+ ? []
841
+ : [{ name: segment.name, field: segment.field, inAggregate: false }]);
842
+ return { ...base, kind: 'template', reads, segments };
843
+ }
844
+ const definition = this.seriesFunctionOf(fields.function, at(site, 'function'));
845
+ const args = this.parseArguments(fields.args, at(site, 'args'));
846
+ return {
847
+ ...base,
848
+ kind: 'group',
849
+ reads: args.flatMap((argument) => argument.reads),
850
+ compute: definition.compute,
851
+ fields: definition.fields,
852
+ args,
853
+ };
854
+ }
855
+ seriesFunctionOf(value, site) {
856
+ if (typeof value === 'function') {
857
+ return { compute: value };
858
+ }
859
+ if (value &&
860
+ typeof value === 'object' &&
861
+ typeof value.compute === 'function') {
862
+ return value;
863
+ }
864
+ if (typeof value === 'string') {
865
+ const registered = readOwn(this.seriesFunctions, value);
866
+ if (typeof registered === 'function') {
867
+ return { compute: registered };
868
+ }
869
+ if (registered && typeof registered === 'object') {
870
+ return registered;
871
+ }
872
+ }
873
+ return invalidAt(site, `unknown series function ${JSON.stringify(value)}; known: ${Object.keys(this.seriesFunctions).join(', ') || '(none)'}`);
874
+ }
875
+ parseArguments(args, site) {
876
+ if (args === undefined) {
877
+ return [];
878
+ }
879
+ if (!args || typeof args !== 'object' || Array.isArray(args)) {
880
+ return invalidAt(site, 'expected an object of arguments', args);
881
+ }
882
+ return Object.entries(args).map(([name, raw]) => {
883
+ const argumentSite = at(site, name);
884
+ if (isUnsafeKey(name)) {
885
+ invalidAt(argumentSite, `the argument name "${name}" is not allowed`);
886
+ }
887
+ if (isFixedValue(raw)) {
888
+ return { name, fixed: copyFixedValue(raw), reads: [] };
889
+ }
890
+ if (typeof raw === 'string') {
891
+ const { evaluate, reads } = this.parseExpression(raw, argumentSite);
892
+ return { name, evaluate, reads };
893
+ }
894
+ return invalidAt(argumentSite, 'an argument is a fixed value (a number, a boolean, null or an array) or an expression string', raw);
895
+ });
896
+ }
897
+ }
898
+ function readsInstance(read, dependency) {
899
+ if (!dependency) {
900
+ return false;
901
+ }
902
+ switch (dependency.kind) {
903
+ case 'keyword':
904
+ return true;
905
+ case 'attribute':
906
+ case 'displaySet':
907
+ return false;
908
+ case 'series':
909
+ return readsInstance(read, dependency.inner);
910
+ case 'entry': {
911
+ const { bound } = dependency;
912
+ if (read.field !== undefined) {
913
+ return bound.group?.fields?.[read.field] === 'instance';
914
+ }
915
+ return bound.scope === 'instance';
916
+ }
917
+ }
918
+ }