edfcore 0.1.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/LICENSE +21 -0
- package/README.md +319 -0
- package/dist/bytes/latin1.d.ts +43 -0
- package/dist/bytes/latin1.d.ts.map +1 -0
- package/dist/bytes/latin1.js +84 -0
- package/dist/bytes/latin1.js.map +1 -0
- package/dist/bytes/numbers.d.ts +45 -0
- package/dist/bytes/numbers.d.ts.map +1 -0
- package/dist/bytes/numbers.js +87 -0
- package/dist/bytes/numbers.js.map +1 -0
- package/dist/bytes/view.d.ts +27 -0
- package/dist/bytes/view.d.ts.map +1 -0
- package/dist/bytes/view.js +53 -0
- package/dist/bytes/view.js.map +1 -0
- package/dist/constants.d.ts +113 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +83 -0
- package/dist/constants.js.map +1 -0
- package/dist/decode/digital.d.ts +49 -0
- package/dist/decode/digital.d.ts.map +1 -0
- package/dist/decode/digital.js +180 -0
- package/dist/decode/digital.js.map +1 -0
- package/dist/decode/physical.d.ts +37 -0
- package/dist/decode/physical.d.ts.map +1 -0
- package/dist/decode/physical.js +178 -0
- package/dist/decode/physical.js.map +1 -0
- package/dist/diagnostics/codes.d.ts +38 -0
- package/dist/diagnostics/codes.d.ts.map +1 -0
- package/dist/diagnostics/codes.js +134 -0
- package/dist/diagnostics/codes.js.map +1 -0
- package/dist/diagnostics/collector.d.ts +76 -0
- package/dist/diagnostics/collector.d.ts.map +1 -0
- package/dist/diagnostics/collector.js +95 -0
- package/dist/diagnostics/collector.js.map +1 -0
- package/dist/diagnostics/format.d.ts +21 -0
- package/dist/diagnostics/format.d.ts.map +1 -0
- package/dist/diagnostics/format.js +125 -0
- package/dist/diagnostics/format.js.map +1 -0
- package/dist/errors.d.ts +143 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +135 -0
- package/dist/errors.js.map +1 -0
- package/dist/header/dates.d.ts +101 -0
- package/dist/header/dates.d.ts.map +1 -0
- package/dist/header/dates.js +378 -0
- package/dist/header/dates.js.map +1 -0
- package/dist/header/fields.d.ts +105 -0
- package/dist/header/fields.d.ts.map +1 -0
- package/dist/header/fields.js +363 -0
- package/dist/header/fields.js.map +1 -0
- package/dist/header/identification.d.ts +47 -0
- package/dist/header/identification.d.ts.map +1 -0
- package/dist/header/identification.js +156 -0
- package/dist/header/identification.js.map +1 -0
- package/dist/header/lookup.d.ts +33 -0
- package/dist/header/lookup.d.ts.map +1 -0
- package/dist/header/lookup.js +67 -0
- package/dist/header/lookup.js.map +1 -0
- package/dist/header/parse.d.ts +32 -0
- package/dist/header/parse.d.ts.map +1 -0
- package/dist/header/parse.js +319 -0
- package/dist/header/parse.js.map +1 -0
- package/dist/header/scale.d.ts +68 -0
- package/dist/header/scale.d.ts.map +1 -0
- package/dist/header/scale.js +154 -0
- package/dist/header/scale.js.map +1 -0
- package/dist/header/signals.d.ts +64 -0
- package/dist/header/signals.d.ts.map +1 -0
- package/dist/header/signals.js +374 -0
- package/dist/header/signals.js.map +1 -0
- package/dist/header/variant.d.ts +67 -0
- package/dist/header/variant.d.ts.map +1 -0
- package/dist/header/variant.js +180 -0
- package/dist/header/variant.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect.d.ts +31 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +165 -0
- package/dist/inspect.js.map +1 -0
- package/dist/io/blob.d.ts +14 -0
- package/dist/io/blob.d.ts.map +1 -0
- package/dist/io/blob.js +29 -0
- package/dist/io/blob.js.map +1 -0
- package/dist/io/bytes.d.ts +11 -0
- package/dist/io/bytes.d.ts.map +1 -0
- package/dist/io/bytes.js +26 -0
- package/dist/io/bytes.js.map +1 -0
- package/dist/io/cached.d.ts +19 -0
- package/dist/io/cached.d.ts.map +1 -0
- package/dist/io/cached.js +129 -0
- package/dist/io/cached.js.map +1 -0
- package/dist/io/http.d.ts +20 -0
- package/dist/io/http.d.ts.map +1 -0
- package/dist/io/http.js +225 -0
- package/dist/io/http.js.map +1 -0
- package/dist/io/read.d.ts +42 -0
- package/dist/io/read.d.ts.map +1 -0
- package/dist/io/read.js +119 -0
- package/dist/io/read.js.map +1 -0
- package/dist/io/source.d.ts +33 -0
- package/dist/io/source.d.ts.map +1 -0
- package/dist/io/source.js +72 -0
- package/dist/io/source.js.map +1 -0
- package/dist/node.d.ts +70 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +109 -0
- package/dist/node.js.map +1 -0
- package/dist/record-index.d.ts +58 -0
- package/dist/record-index.d.ts.map +1 -0
- package/dist/record-index.js +290 -0
- package/dist/record-index.js.map +1 -0
- package/dist/recording.d.ts +57 -0
- package/dist/recording.d.ts.map +1 -0
- package/dist/recording.js +178 -0
- package/dist/recording.js.map +1 -0
- package/dist/tal/annotations.d.ts +36 -0
- package/dist/tal/annotations.d.ts.map +1 -0
- package/dist/tal/annotations.js +437 -0
- package/dist/tal/annotations.js.map +1 -0
- package/dist/tal/grammar.d.ts +111 -0
- package/dist/tal/grammar.d.ts.map +1 -0
- package/dist/tal/grammar.js +324 -0
- package/dist/tal/grammar.js.map +1 -0
- package/dist/tal/ticks.d.ts +69 -0
- package/dist/tal/ticks.d.ts.map +1 -0
- package/dist/tal/ticks.js +137 -0
- package/dist/tal/ticks.js.map +1 -0
- package/dist/time/segments.d.ts +33 -0
- package/dist/time/segments.d.ts.map +1 -0
- package/dist/time/segments.js +87 -0
- package/dist/time/segments.js.map +1 -0
- package/dist/time/timeline.d.ts +62 -0
- package/dist/time/timeline.d.ts.map +1 -0
- package/dist/time/timeline.js +227 -0
- package/dist/time/timeline.js.map +1 -0
- package/dist/time/window.d.ts +54 -0
- package/dist/time/window.d.ts.map +1 -0
- package/dist/time/window.js +232 -0
- package/dist/time/window.js.map +1 -0
- package/dist/types.d.ts +409 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +53 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +483 -0
- package/dist/validate.js.map +1 -0
- package/package.json +77 -0
- package/src/bytes/latin1.ts +87 -0
- package/src/bytes/numbers.ts +115 -0
- package/src/bytes/view.ts +59 -0
- package/src/constants.ts +96 -0
- package/src/decode/digital.ts +258 -0
- package/src/decode/physical.ts +235 -0
- package/src/diagnostics/codes.ts +209 -0
- package/src/diagnostics/collector.ts +129 -0
- package/src/diagnostics/format.ts +143 -0
- package/src/errors.ts +208 -0
- package/src/header/dates.ts +453 -0
- package/src/header/fields.ts +455 -0
- package/src/header/identification.ts +196 -0
- package/src/header/lookup.ts +81 -0
- package/src/header/parse.ts +398 -0
- package/src/header/scale.ts +268 -0
- package/src/header/signals.ts +550 -0
- package/src/header/variant.ts +239 -0
- package/src/index.ts +151 -0
- package/src/inspect.ts +181 -0
- package/src/io/blob.ts +31 -0
- package/src/io/bytes.ts +29 -0
- package/src/io/cached.ts +136 -0
- package/src/io/http.ts +299 -0
- package/src/io/read.ts +152 -0
- package/src/io/source.ts +87 -0
- package/src/node.ts +138 -0
- package/src/record-index.ts +389 -0
- package/src/recording.ts +234 -0
- package/src/tal/annotations.ts +574 -0
- package/src/tal/grammar.ts +540 -0
- package/src/tal/ticks.ts +172 -0
- package/src/time/segments.ts +109 -0
- package/src/time/timeline.ts +290 -0
- package/src/time/window.ts +304 -0
- package/src/types.ts +490 -0
- package/src/validate.ts +610 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `decodeAnnotations`: TALs plus EDF+ semantics.
|
|
3
|
+
*
|
|
4
|
+
* Layer 3. Sole owner of timekeeping extraction and of the annotation sort. The byte grammar
|
|
5
|
+
* lives in `grammar.ts`; everything here is about which TAL means what.
|
|
6
|
+
*
|
|
7
|
+
* Three rules the rest of the library depends on:
|
|
8
|
+
*
|
|
9
|
+
* 1. The FIRST TAL of the FIRST annotation signal of the file is that record's timekeeping TAL.
|
|
10
|
+
* "First" is a position, not "the first one that parsed", and "first annotation signal" is
|
|
11
|
+
* `header.annotationSignalIndices[0]` — not the first signal this call was asked for. An
|
|
12
|
+
* additional annotation signal carries NO timekeeping TAL, and stripping its first TAL would
|
|
13
|
+
* silently delete a real event.
|
|
14
|
+
* 2. `recordOnsetTicks` has one entry for EVERY record in the decoded range, always. A record
|
|
15
|
+
* whose timekeeping TAL is missing gets the derived onset `start + recordIndex * duration`
|
|
16
|
+
* rather than a hole or a sentinel, and `TIMEKEEPING_TAL_MISSING` carries the record index so
|
|
17
|
+
* the derivation is never invisible. Every timeline in edfcore is built from this array.
|
|
18
|
+
* 3. Onsets are exposed under both conventions as separately named fields, never as an option:
|
|
19
|
+
* `onsetSecondsFromHeaderStart` is the verbatim on-disk value and
|
|
20
|
+
* `onsetSecondsFromFirstRecord` is rebased to record 0's true start. `onsetTicks` is exact
|
|
21
|
+
* and is the only one worth comparing.
|
|
22
|
+
*
|
|
23
|
+
* Diagnostic volume is bounded on purpose, by one test: does another occurrence of this code
|
|
24
|
+
* carry information available nowhere else? `TIMEKEEPING_TAL_MISSING` does — it names a record
|
|
25
|
+
* whose onset was derived — so it is reported per record. `NEGATIVE_ANNOTATION_ONSET` and
|
|
26
|
+
* `TIMEKEEPING_TAL_NONCONFORMANT` do not: the onsets are in the result and the shorthand is a
|
|
27
|
+
* property of the writer, so they are reported once per call. The grammar's own defects are
|
|
28
|
+
* deduplicated per region and carry an occurrence count.
|
|
29
|
+
*
|
|
30
|
+
* Record-onset spacing and monotonicity are deliberately NOT checked here. This module produces
|
|
31
|
+
* `recordOnsetTicks`; `time/timeline.ts` owns enforcing what a valid timeline is, and splitting
|
|
32
|
+
* that would give one rule two owners.
|
|
33
|
+
*/
|
|
34
|
+
import { sliceBytes } from '../bytes/view.js';
|
|
35
|
+
import { TICKS_PER_SECOND } from '../constants.js';
|
|
36
|
+
import { DiagnosticSink } from '../diagnostics/collector.js';
|
|
37
|
+
import { EdfRangeError } from '../errors.js';
|
|
38
|
+
import { parseTalRegion, previewBytes, splitChannelLabel, TAL_PREVIEW_MAX_BYTES, } from './grammar.js';
|
|
39
|
+
import { ticksToSeconds } from './ticks.js';
|
|
40
|
+
const ANNOTATIONS_SPEC = "EDF+ specification 2.2 (the 'EDF Annotations' signal)";
|
|
41
|
+
const TIMEKEEPING_SPEC = 'EDF+ specification 2.2.1 (time keeping of data records)';
|
|
42
|
+
const ONSET_SPEC = 'EDF+ specification 2.2.4 (onsets are relative to the startdate/starttime)';
|
|
43
|
+
const ISSUE_RULES = {
|
|
44
|
+
TAL_MALFORMED: 'a TAL is Onset [ 0x15 Duration ] 0x14 *( Text 0x14 ) 0x00, the onset carries a mandatory ' +
|
|
45
|
+
'sign and the duration never does',
|
|
46
|
+
TAL_TRUNCATED_AT_REGION_END: 'a TAL is contained in its own annotation region; the bytes past the region are the next ' +
|
|
47
|
+
"signal's samples",
|
|
48
|
+
TAL_REGION_NOT_NUL_TERMINATED: 'the bytes after the last TAL of a region are all 0x00',
|
|
49
|
+
ANNOTATION_TEXT_NOT_UTF8: 'annotation text is UTF-8',
|
|
50
|
+
};
|
|
51
|
+
const ISSUE_NEXT_STEPS = {
|
|
52
|
+
TAL_MALFORMED: 'the rest of the region and the rest of the file were kept; hexdump the file at the byte ' +
|
|
53
|
+
'offset above to see what the writer emitted',
|
|
54
|
+
TAL_TRUNCATED_AT_REGION_END: "check the writer's samplesPerRecord for this annotation signal — a region too small for " +
|
|
55
|
+
'the TALs written into it is the usual cause',
|
|
56
|
+
TAL_REGION_NOT_NUL_TERMINATED: 'hexdump the file at the byte offset above; trailing bytes from an earlier, longer record ' +
|
|
57
|
+
'mean the writer reused a buffer without clearing it',
|
|
58
|
+
ANNOTATION_TEXT_NOT_UTF8: 'the affected annotations report textEncoding "latin-1-fallback", so you can re-decode the ' +
|
|
59
|
+
'bytes yourself if the writer used another code page',
|
|
60
|
+
};
|
|
61
|
+
const ISSUE_SPEC_REFERENCES = {
|
|
62
|
+
TAL_MALFORMED: ANNOTATIONS_SPEC,
|
|
63
|
+
TAL_TRUNCATED_AT_REGION_END: ANNOTATIONS_SPEC,
|
|
64
|
+
TAL_REGION_NOT_NUL_TERMINATED: ANNOTATIONS_SPEC,
|
|
65
|
+
ANNOTATION_TEXT_NOT_UTF8: ANNOTATIONS_SPEC,
|
|
66
|
+
};
|
|
67
|
+
/** Enough of a region to show what went wrong, without copying a 60 KB region per diagnostic. */
|
|
68
|
+
const RAW_EVIDENCE_MAX_BYTES = TAL_PREVIEW_MAX_BYTES;
|
|
69
|
+
/**
|
|
70
|
+
* A `BigInt64Array` wraps silently on assignment, so a derived onset is saturated rather than
|
|
71
|
+
* wrapped. Reaching either bound needs a declared geometry that is already impossible (~29,000
|
|
72
|
+
* years of records); saturating keeps the array non-decreasing where wrapping would invert it.
|
|
73
|
+
*/
|
|
74
|
+
const INT64_MIN = -(2n ** 63n);
|
|
75
|
+
const INT64_MAX = 2n ** 63n - 1n;
|
|
76
|
+
function saturateToInt64(ticks) {
|
|
77
|
+
if (ticks > INT64_MAX)
|
|
78
|
+
return INT64_MAX;
|
|
79
|
+
if (ticks < INT64_MIN)
|
|
80
|
+
return INT64_MIN;
|
|
81
|
+
return ticks;
|
|
82
|
+
}
|
|
83
|
+
function describeRange(range) {
|
|
84
|
+
return `{ start: ${range.start}, count: ${range.count} }`;
|
|
85
|
+
}
|
|
86
|
+
function assertRecordRange(header, recordBytes, records) {
|
|
87
|
+
const available = { start: 0, count: header.recordCount };
|
|
88
|
+
const validIndices = Number.isSafeInteger(records.start) &&
|
|
89
|
+
records.start >= 0 &&
|
|
90
|
+
Number.isSafeInteger(records.count) &&
|
|
91
|
+
records.count >= 0;
|
|
92
|
+
if (!validIndices || records.start + records.count > header.recordCount) {
|
|
93
|
+
throw new EdfRangeError(`decodeAnnotations(): records ${describeRange(records)} is not inside the ` +
|
|
94
|
+
`${header.recordCount} records this file has. ` +
|
|
95
|
+
`Next: clamp the range to [0, ${header.recordCount}).`, { requested: records, available });
|
|
96
|
+
}
|
|
97
|
+
const expected = records.count * header.recordByteLength;
|
|
98
|
+
if (recordBytes.length !== expected) {
|
|
99
|
+
const whole = header.recordByteLength > 0 ? Math.floor(recordBytes.length / header.recordByteLength) : 0;
|
|
100
|
+
throw new EdfRangeError(`decodeAnnotations(): recordBytes is ${recordBytes.length} bytes, but records ` +
|
|
101
|
+
`${describeRange(records)} of this file is exactly ${expected} bytes ` +
|
|
102
|
+
`(${records.count} x ${header.recordByteLength}). ` +
|
|
103
|
+
'Next: pass the buffer readRecordBytes() returned for this exact range, unsliced.', { requested: records, available: { start: records.start, count: whole } });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The annotation signals to read, ascending and deduplicated.
|
|
108
|
+
*
|
|
109
|
+
* A non-annotation index is refused with a plain `RangeError`, not an `EdfError`: parsing a data
|
|
110
|
+
* signal's samples as text is exactly the garbage this module exists to prevent, and it can only
|
|
111
|
+
* happen through a caller's mistake, never through a file's.
|
|
112
|
+
*/
|
|
113
|
+
function resolveSignals(header, requested) {
|
|
114
|
+
const indices = requested ?? header.annotationSignalIndices;
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
const signals = [];
|
|
117
|
+
for (const index of indices) {
|
|
118
|
+
if (seen.has(index))
|
|
119
|
+
continue;
|
|
120
|
+
seen.add(index);
|
|
121
|
+
const signal = header.signals[index];
|
|
122
|
+
if (signal === undefined || signal.kind !== 'annotations') {
|
|
123
|
+
throw new RangeError(`decodeAnnotations(): signal ${index} is not an annotation signal. This file's ` +
|
|
124
|
+
`annotation signals are [${header.annotationSignalIndices.join(', ')}]. ` +
|
|
125
|
+
'Next: pass one of those, or omit signalIndices to read them all.');
|
|
126
|
+
}
|
|
127
|
+
signals.push(signal);
|
|
128
|
+
}
|
|
129
|
+
return signals.sort((a, b) => a.index - b.index);
|
|
130
|
+
}
|
|
131
|
+
function reportIssue(sink, context, issue) {
|
|
132
|
+
const repeats = issue.occurrences > 1
|
|
133
|
+
? ` (${issue.occurrences} occurrences in this region; the first is described)`
|
|
134
|
+
: '';
|
|
135
|
+
const evidenceLength = Math.min(issue.byteLength, RAW_EVIDENCE_MAX_BYTES);
|
|
136
|
+
sink.report({
|
|
137
|
+
code: issue.code,
|
|
138
|
+
message: `annotation region of signal ${context.signal.index} ("${context.signal.label}") in ` +
|
|
139
|
+
`record ${context.recordIndex}: ${issue.detail}${repeats}. ` +
|
|
140
|
+
`Bytes at that offset: "${issue.raw}". Rule: ${ISSUE_RULES[issue.code]}. ` +
|
|
141
|
+
`Next: ${ISSUE_NEXT_STEPS[issue.code]}.`,
|
|
142
|
+
field: 'annotation region',
|
|
143
|
+
byteOffset: context.fileOffset + issue.byteOffsetInRegion,
|
|
144
|
+
byteLength: issue.byteLength,
|
|
145
|
+
rawBytes: sliceBytes(context.bytes, context.offset + issue.byteOffsetInRegion, evidenceLength),
|
|
146
|
+
raw: issue.raw,
|
|
147
|
+
signalIndex: context.signal.index,
|
|
148
|
+
recordIndex: context.recordIndex,
|
|
149
|
+
specReference: ISSUE_SPEC_REFERENCES[issue.code],
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function reportTimekeepingMissing(sink, context) {
|
|
153
|
+
const shown = Math.min(context.signal.recordByteLength, RAW_EVIDENCE_MAX_BYTES);
|
|
154
|
+
sink.report({
|
|
155
|
+
code: 'TIMEKEEPING_TAL_MISSING',
|
|
156
|
+
message: `record ${context.recordIndex} has no timekeeping TAL: the first TAL slot of annotation ` +
|
|
157
|
+
`signal ${context.signal.index} ("${context.signal.label}") is empty or did not parse. ` +
|
|
158
|
+
`Region starts with: "${previewBytes(context.bytes, context.offset, shown)}". ` +
|
|
159
|
+
'Rule: the first TAL of the first annotation signal of every data record gives that ' +
|
|
160
|
+
"record's start relative to the file start, and carries no text. " +
|
|
161
|
+
`Next: this record's onset was derived as start + ${context.recordIndex} x ` +
|
|
162
|
+
'recordDuration and is in recordOnsetTicks; treat times inside it as nominal, and run ' +
|
|
163
|
+
'validateRecording() to see how many records are affected.',
|
|
164
|
+
field: 'timekeeping TAL',
|
|
165
|
+
byteOffset: context.fileOffset,
|
|
166
|
+
byteLength: context.signal.recordByteLength,
|
|
167
|
+
rawBytes: sliceBytes(context.bytes, context.offset, shown),
|
|
168
|
+
signalIndex: context.signal.index,
|
|
169
|
+
recordIndex: context.recordIndex,
|
|
170
|
+
specReference: TIMEKEEPING_SPEC,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* What is non-conformant about a timekeeping TAL, or `undefined` when it is exactly
|
|
175
|
+
* `+onset 0x14 0x14 0x00`.
|
|
176
|
+
*
|
|
177
|
+
* Zero texts is the widespread `+t 0x14 0x00` shorthand. EDFlib rejects that file outright; we
|
|
178
|
+
* treat "zero texts" and "one empty text" as the same thing and keep going, because the onset is
|
|
179
|
+
* unambiguous either way and refusing the file would refuse a large part of the real corpus.
|
|
180
|
+
*/
|
|
181
|
+
function timekeepingDefect(tal) {
|
|
182
|
+
if (tal.durationRaw !== undefined) {
|
|
183
|
+
return `carries the duration "${tal.durationRaw}", which a timekeeping TAL never has`;
|
|
184
|
+
}
|
|
185
|
+
if (tal.texts.length === 0) {
|
|
186
|
+
return ('omits the mandatory empty text and is written "+onset 0x14 0x00" — the widespread ' +
|
|
187
|
+
'shorthand, which EDFlib rejects outright');
|
|
188
|
+
}
|
|
189
|
+
const texts = tal.texts.map((run) => run.text).filter((text) => text.length > 0);
|
|
190
|
+
if (texts.length > 0) {
|
|
191
|
+
return (`carries the text ${texts.map((text) => `"${text}"`).join(', ')}, which is dropped: the ` +
|
|
192
|
+
'timekeeping TAL is not an annotation');
|
|
193
|
+
}
|
|
194
|
+
if (tal.texts.length > 1) {
|
|
195
|
+
return `carries ${tal.texts.length} empty texts, where the grammar allows exactly one`;
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
function reportTimekeepingDefect(sink, context, tal, defect) {
|
|
200
|
+
sink.report({
|
|
201
|
+
code: 'TIMEKEEPING_TAL_NONCONFORMANT',
|
|
202
|
+
message: `the timekeeping TAL of record ${context.recordIndex} (annotation signal ` +
|
|
203
|
+
`${context.signal.index}, onset "${tal.onsetRaw}") ${defect}. ` +
|
|
204
|
+
'Rule: a timekeeping TAL is written "+onset 0x14 0x14 0x00" — one signed onset, no ' +
|
|
205
|
+
'duration, one empty text. ' +
|
|
206
|
+
"Next: the onset was used as this record's start and the file was kept; this is " +
|
|
207
|
+
'reported once per decodeAnnotations() call, so later records are not re-reported.',
|
|
208
|
+
field: 'timekeeping TAL',
|
|
209
|
+
byteOffset: context.fileOffset + tal.byteOffsetInRegion,
|
|
210
|
+
byteLength: tal.byteLength,
|
|
211
|
+
raw: tal.onsetRaw,
|
|
212
|
+
signalIndex: context.signal.index,
|
|
213
|
+
recordIndex: context.recordIndex,
|
|
214
|
+
specReference: TIMEKEEPING_SPEC,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
function reportNegativeOnset(sink, context, tal) {
|
|
218
|
+
sink.report({
|
|
219
|
+
code: 'NEGATIVE_ANNOTATION_ONSET',
|
|
220
|
+
message: `annotation onset "${tal.onsetRaw}" in record ${context.recordIndex} (signal ` +
|
|
221
|
+
`${context.signal.index}) is negative: ${ticksToSeconds(tal.onsetTicks)} s before ` +
|
|
222
|
+
'the file start. ' +
|
|
223
|
+
'Rule: EDF+ allows a negative onset and it is how a pre-stimulus event is written, so ' +
|
|
224
|
+
'this is information, not a warning. ' +
|
|
225
|
+
'Next: nothing to fix — compare event times with onsetTicks, which is exact and signed. ' +
|
|
226
|
+
'Reported once per decodeAnnotations() call.',
|
|
227
|
+
field: 'annotation onset',
|
|
228
|
+
byteOffset: context.fileOffset + tal.byteOffsetInRegion,
|
|
229
|
+
byteLength: tal.byteLength,
|
|
230
|
+
raw: tal.onsetRaw,
|
|
231
|
+
signalIndex: context.signal.index,
|
|
232
|
+
recordIndex: context.recordIndex,
|
|
233
|
+
specReference: ONSET_SPEC,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The pinned total order: onset, then signal, then byte offset in the record, then insertion
|
|
238
|
+
* order. The last key is what makes it total — two annotations in different records can share
|
|
239
|
+
* all three of the others — and it is spelled out rather than left to `Array.prototype.sort`
|
|
240
|
+
* being stable, because a test pins this order.
|
|
241
|
+
*/
|
|
242
|
+
function compareAnnotations(a, b) {
|
|
243
|
+
if (a.onsetTicks !== b.onsetTicks)
|
|
244
|
+
return a.onsetTicks < b.onsetTicks ? -1 : 1;
|
|
245
|
+
if (a.signalIndex !== b.signalIndex)
|
|
246
|
+
return a.signalIndex - b.signalIndex;
|
|
247
|
+
if (a.byteOffsetInRecord !== b.byteOffsetInRecord) {
|
|
248
|
+
return a.byteOffsetInRecord - b.byteOffsetInRecord;
|
|
249
|
+
}
|
|
250
|
+
return a.order - b.order;
|
|
251
|
+
}
|
|
252
|
+
export function decodeAnnotations(header, recordBytes, records, options) {
|
|
253
|
+
assertRecordRange(header, recordBytes, records);
|
|
254
|
+
const sink = new DiagnosticSink(options);
|
|
255
|
+
const signals = resolveSignals(header, options?.signalIndices);
|
|
256
|
+
// Timekeeping belongs to the file's first annotation signal, not to the first one this call
|
|
257
|
+
// was asked for: an additional annotation signal's first TAL is an ordinary annotation. A call
|
|
258
|
+
// that leaves that signal out reads no timekeeping at all, and every recordOnsetTicks entry is
|
|
259
|
+
// then the nominal grid — silence the caller asked for, not a missing TAL.
|
|
260
|
+
const timekeepingSignalIndex = header.annotationSignalIndices[0];
|
|
261
|
+
const durationTicks = header.recordDurationTicks;
|
|
262
|
+
const observedOnsets = new Array(records.count).fill(undefined);
|
|
263
|
+
const pending = [];
|
|
264
|
+
let firstObserved;
|
|
265
|
+
let negativeOnsetReported = false;
|
|
266
|
+
let timekeepingDefectReported = false;
|
|
267
|
+
for (let position = 0; position < records.count; position += 1) {
|
|
268
|
+
const recordIndex = records.start + position;
|
|
269
|
+
const recordOffset = position * header.recordByteLength;
|
|
270
|
+
for (const signal of signals) {
|
|
271
|
+
const context = {
|
|
272
|
+
signal,
|
|
273
|
+
recordIndex,
|
|
274
|
+
bytes: recordBytes,
|
|
275
|
+
offset: recordOffset + signal.recordByteOffset,
|
|
276
|
+
fileOffset: header.headerByteLength + recordIndex * header.recordByteLength + signal.recordByteOffset,
|
|
277
|
+
};
|
|
278
|
+
const parse = parseTalRegion(recordBytes, context.offset, signal.recordByteLength);
|
|
279
|
+
for (const issue of parse.issues)
|
|
280
|
+
reportIssue(sink, context, issue);
|
|
281
|
+
const first = parse.tals[0];
|
|
282
|
+
const timekeeping = signal.index === timekeepingSignalIndex && first !== undefined && first.ordinal === 0
|
|
283
|
+
? first
|
|
284
|
+
: undefined;
|
|
285
|
+
if (signal.index === timekeepingSignalIndex) {
|
|
286
|
+
if (timekeeping === undefined) {
|
|
287
|
+
reportTimekeepingMissing(sink, context);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
observedOnsets[position] = timekeeping.onsetTicks;
|
|
291
|
+
firstObserved ??= {
|
|
292
|
+
recordIndex,
|
|
293
|
+
ticks: timekeeping.onsetTicks,
|
|
294
|
+
raw: timekeeping.onsetRaw,
|
|
295
|
+
};
|
|
296
|
+
const defect = timekeepingDefect(timekeeping);
|
|
297
|
+
if (defect !== undefined && !timekeepingDefectReported) {
|
|
298
|
+
timekeepingDefectReported = true;
|
|
299
|
+
reportTimekeepingDefect(sink, context, timekeeping, defect);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
for (const tal of parse.tals) {
|
|
304
|
+
if (tal === timekeeping)
|
|
305
|
+
continue;
|
|
306
|
+
for (const run of tal.texts) {
|
|
307
|
+
// An empty run is structure, not an event: it is how the grammar terminates a
|
|
308
|
+
// timestamp, and every record would otherwise carry a phantom annotation.
|
|
309
|
+
if (run.text.length === 0)
|
|
310
|
+
continue;
|
|
311
|
+
const parts = splitChannelLabel(run.text);
|
|
312
|
+
const annotation = {
|
|
313
|
+
onsetTicks: tal.onsetTicks,
|
|
314
|
+
onsetRaw: tal.onsetRaw,
|
|
315
|
+
durationTicks: tal.durationTicks,
|
|
316
|
+
durationRaw: tal.durationRaw,
|
|
317
|
+
text: parts.text,
|
|
318
|
+
channelLabel: parts.channelLabel,
|
|
319
|
+
signalIndex: signal.index,
|
|
320
|
+
recordIndex,
|
|
321
|
+
byteOffsetInRecord: signal.recordByteOffset + run.byteOffsetInRegion,
|
|
322
|
+
textEncoding: run.encoding,
|
|
323
|
+
order: pending.length,
|
|
324
|
+
};
|
|
325
|
+
pending.push(annotation);
|
|
326
|
+
if (annotation.onsetTicks < 0n && !negativeOnsetReported) {
|
|
327
|
+
negativeOnsetReported = true;
|
|
328
|
+
reportNegativeOnset(sink, context, tal);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Record 0's onset, observed when it was decoded and derived from the first record that was
|
|
335
|
+
// otherwise. For a continuous file the derivation is exact; see the rebasing note below.
|
|
336
|
+
const baseTicks = firstObserved === undefined
|
|
337
|
+
? 0n
|
|
338
|
+
: firstObserved.ticks - BigInt(firstObserved.recordIndex) * durationTicks;
|
|
339
|
+
const recordOnsetTicks = new BigInt64Array(records.count);
|
|
340
|
+
for (let position = 0; position < records.count; position += 1) {
|
|
341
|
+
const observed = observedOnsets[position];
|
|
342
|
+
recordOnsetTicks[position] =
|
|
343
|
+
observed ?? saturateToInt64(baseTicks + BigInt(records.start + position) * durationTicks);
|
|
344
|
+
}
|
|
345
|
+
const startOffsetTicks = resolveStartOffsetTicks(sink, header, records, recordOnsetTicks, baseTicks, firstObserved);
|
|
346
|
+
pending.sort(compareAnnotations);
|
|
347
|
+
const annotations = pending.map((item) => ({
|
|
348
|
+
onsetSecondsFromHeaderStart: ticksToSeconds(item.onsetTicks),
|
|
349
|
+
onsetSecondsFromFirstRecord: ticksToSeconds(item.onsetTicks - startOffsetTicks),
|
|
350
|
+
onsetTicks: item.onsetTicks,
|
|
351
|
+
onsetRaw: item.onsetRaw,
|
|
352
|
+
durationSeconds: item.durationTicks === undefined ? undefined : ticksToSeconds(item.durationTicks),
|
|
353
|
+
durationTicks: item.durationTicks,
|
|
354
|
+
durationRaw: item.durationRaw,
|
|
355
|
+
text: item.text,
|
|
356
|
+
channelLabel: item.channelLabel,
|
|
357
|
+
signalIndex: item.signalIndex,
|
|
358
|
+
recordIndex: item.recordIndex,
|
|
359
|
+
byteOffsetInRecord: item.byteOffsetInRecord,
|
|
360
|
+
textEncoding: item.textEncoding,
|
|
361
|
+
}));
|
|
362
|
+
return {
|
|
363
|
+
annotations: Object.freeze(annotations),
|
|
364
|
+
recordOnsetTicks,
|
|
365
|
+
diagnostics: sink.diagnostics,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Record 0's true start, which is what `onsetSecondsFromFirstRecord` is rebased by.
|
|
370
|
+
*
|
|
371
|
+
* When the range starts at record 0 the value is read from the file and used as written, even if
|
|
372
|
+
* it is outside the [0, 1) second the spec allows — it is still that record's start.
|
|
373
|
+
*
|
|
374
|
+
* When the range starts later, record 0's onset is not in the bytes we were given, so it is
|
|
375
|
+
* derived as `onset(first decoded record) - recordIndex * recordDuration`. That is exact for a
|
|
376
|
+
* continuous file and wrong by the elapsed gaps for an EDF+D one, so the derived value is used
|
|
377
|
+
* only when it lands inside [0, 1) — where a gap cannot hide, unless it is itself shorter than a
|
|
378
|
+
* second. Otherwise rebasing is switched off and the two onset conventions become equal.
|
|
379
|
+
*
|
|
380
|
+
* A derivation that lands outside [0, 1) is only *reported* for a file that claims to be
|
|
381
|
+
* continuous, where `onset(r) = start + r * duration` must hold and a violation is the file's
|
|
382
|
+
* fault. On an EDF+D file it is the expected outcome of not decoding from record 0, and a
|
|
383
|
+
* diagnostic there would make `strict` reject every conformant discontinuous recording — the
|
|
384
|
+
* record-index probes read exactly one late record.
|
|
385
|
+
*/
|
|
386
|
+
function resolveStartOffsetTicks(sink, header, records, recordOnsetTicks, baseTicks, firstObserved) {
|
|
387
|
+
if (records.count === 0)
|
|
388
|
+
return 0n;
|
|
389
|
+
const origin = firstObserved === undefined
|
|
390
|
+
? 'no timekeeping TAL was found in the decoded range'
|
|
391
|
+
: firstObserved.recordIndex === 0
|
|
392
|
+
? `raw timekeeping onset "${firstObserved.raw}"`
|
|
393
|
+
: `derived from record ${firstObserved.recordIndex}, timekeeping onset ` +
|
|
394
|
+
`"${firstObserved.raw}"`;
|
|
395
|
+
if (records.start === 0) {
|
|
396
|
+
const onset = recordOnsetTicks[0] ?? 0n;
|
|
397
|
+
if (onset < 0n || onset >= TICKS_PER_SECOND) {
|
|
398
|
+
sink.report({
|
|
399
|
+
code: 'START_OFFSET_OUT_OF_RANGE',
|
|
400
|
+
message: `record 0 starts ${ticksToSeconds(onset)} s after the header start time, but a ` +
|
|
401
|
+
'sub-second start offset must be in [0, 1). ' +
|
|
402
|
+
`Origin: ${origin}. ` +
|
|
403
|
+
"Rule: the onset of the first record's timekeeping TAL is the recording's sub-second " +
|
|
404
|
+
'start offset, and the whole-second part of the start time lives in the header. ' +
|
|
405
|
+
'Next: the value was used as written; compare it with the header starttime field, ' +
|
|
406
|
+
'because a writer that encodes the start time twice produces exactly this.',
|
|
407
|
+
field: 'timekeeping TAL',
|
|
408
|
+
raw: firstObserved?.raw ?? '',
|
|
409
|
+
recordIndex: 0,
|
|
410
|
+
specReference: TIMEKEEPING_SPEC,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return onset;
|
|
414
|
+
}
|
|
415
|
+
if (baseTicks >= 0n && baseTicks < TICKS_PER_SECOND)
|
|
416
|
+
return baseTicks;
|
|
417
|
+
if (header.continuity === 'continuous') {
|
|
418
|
+
sink.report({
|
|
419
|
+
code: 'START_OFFSET_OUT_OF_RANGE',
|
|
420
|
+
message: `the decoded range starts at record ${records.start}, so record 0's start offset had ` +
|
|
421
|
+
`to be derived (${origin}); the derived value ${ticksToSeconds(baseTicks)} s is outside ` +
|
|
422
|
+
'[0, 1), which a continuous file cannot produce — its record onsets are start + ' +
|
|
423
|
+
'recordIndex * recordDuration by definition. ' +
|
|
424
|
+
"Rule: the onset of record 0's timekeeping TAL is the recording's sub-second start " +
|
|
425
|
+
'offset, and it is the only correct rebasing origin. ' +
|
|
426
|
+
'Next: onsetSecondsFromFirstRecord equals onsetSecondsFromHeaderStart for this call; ' +
|
|
427
|
+
'the file is either discontinuous while claiming EDF+C, or its record onsets drift — ' +
|
|
428
|
+
'buildRecordIndex() will say which.',
|
|
429
|
+
field: 'timekeeping TAL',
|
|
430
|
+
raw: firstObserved?.raw ?? '',
|
|
431
|
+
recordIndex: records.start,
|
|
432
|
+
specReference: TIMEKEEPING_SPEC,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return 0n;
|
|
436
|
+
}
|
|
437
|
+
//# sourceMappingURL=annotations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"annotations.js","sourceRoot":"","sources":["../../src/tal/annotations.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The byte-level TAL state machine.
|
|
3
|
+
*
|
|
4
|
+
* Layer 3. Sole owner of turning ONE annotation region into time-stamped annotation lists.
|
|
5
|
+
* `annotations.ts` adds provenance and EDF+ semantics on top; nothing here knows about records,
|
|
6
|
+
* signals, or timekeeping.
|
|
7
|
+
*
|
|
8
|
+
* Two rules define the module, and both are where other EDF readers go wrong:
|
|
9
|
+
*
|
|
10
|
+
* 1. Parsing is HARD-BOUNDED to `[regionStart, regionStart + regionBytes)`. A TAL with no
|
|
11
|
+
* terminating 0x00 inside the region is discarded, never continued past the bound — the bytes
|
|
12
|
+
* after it are the next signal's samples, and parsing those is how a reader invents
|
|
13
|
+
* annotations that are not in the file.
|
|
14
|
+
* 2. The region is split on the STRUCTURAL bytes (0x00, 0x14, 0x15) FIRST and each text run is
|
|
15
|
+
* decoded as UTF-8 LAST. That order is safe in exactly one direction: every byte of a
|
|
16
|
+
* multi-byte UTF-8 sequence is >= 0x80 and can never collide with a structural byte, while a
|
|
17
|
+
* string that has already been decoded can no longer be split on bytes at all.
|
|
18
|
+
*
|
|
19
|
+
* `src/tal/` is the only part of edfcore allowed to use `TextDecoder`, and only for annotation
|
|
20
|
+
* text. Header bytes go through `decodeHeaderLatin1`, for the cross-runtime reason documented in
|
|
21
|
+
* `src/bytes/latin1.ts`.
|
|
22
|
+
*
|
|
23
|
+
* Each defect is reported at most once per region, with an occurrence count: a corrupt region
|
|
24
|
+
* can hold thousands of malformed TALs, and one diagnostic per TAL would be an unbounded
|
|
25
|
+
* allocation dressed up as diligence. Nothing is hidden — `occurrences` says how many there were.
|
|
26
|
+
*/
|
|
27
|
+
export type TalTextEncoding = 'utf-8' | 'latin-1-fallback';
|
|
28
|
+
/** One text run of a TAL: the bytes between two 0x14 separators, decoded. */
|
|
29
|
+
export interface TalText {
|
|
30
|
+
/** Verbatim. Never trimmed, never case-folded, and a BOM is kept as a character. */
|
|
31
|
+
readonly text: string;
|
|
32
|
+
readonly encoding: TalTextEncoding;
|
|
33
|
+
readonly byteOffsetInRegion: number;
|
|
34
|
+
readonly byteLength: number;
|
|
35
|
+
}
|
|
36
|
+
/** The subset of the diagnostic vocabulary this module can observe. */
|
|
37
|
+
export type TalIssueCode = 'TAL_MALFORMED' | 'TAL_TRUNCATED_AT_REGION_END' | 'TAL_REGION_NOT_NUL_TERMINATED' | 'ANNOTATION_TEXT_NOT_UTF8';
|
|
38
|
+
/**
|
|
39
|
+
* One defect, described once per region.
|
|
40
|
+
*
|
|
41
|
+
* `detail` states what was wrong AND what was done about it, because the disposition differs
|
|
42
|
+
* per defect: a missing onset sign keeps the annotation, a missing duration discards it.
|
|
43
|
+
*/
|
|
44
|
+
export interface TalIssue {
|
|
45
|
+
readonly code: TalIssueCode;
|
|
46
|
+
/** Of the first occurrence. Relative to the region start. */
|
|
47
|
+
readonly byteOffsetInRegion: number;
|
|
48
|
+
readonly byteLength: number;
|
|
49
|
+
/** Occurrences in this region. Only the first is described. */
|
|
50
|
+
readonly occurrences: number;
|
|
51
|
+
readonly detail: string;
|
|
52
|
+
/** The first occurrence's bytes, escaped and truncated, for the diagnostic message. */
|
|
53
|
+
readonly raw: string;
|
|
54
|
+
}
|
|
55
|
+
/** One TAL that survived parsing. Times are exact ticks; the digits they came from are kept. */
|
|
56
|
+
export interface ParsedTal {
|
|
57
|
+
/**
|
|
58
|
+
* Position of this TAL in the region, counting the ones that were skipped. Timekeeping is a
|
|
59
|
+
* property of TAL slot 0, so a record whose first TAL was malformed must not have its second
|
|
60
|
+
* TAL promoted into the timekeeping role.
|
|
61
|
+
*/
|
|
62
|
+
readonly ordinal: number;
|
|
63
|
+
readonly byteOffsetInRegion: number;
|
|
64
|
+
/** Includes the terminating 0x00. */
|
|
65
|
+
readonly byteLength: number;
|
|
66
|
+
readonly onsetRaw: string;
|
|
67
|
+
readonly onsetTicks: bigint;
|
|
68
|
+
readonly durationRaw: string | undefined;
|
|
69
|
+
readonly durationTicks: bigint | undefined;
|
|
70
|
+
readonly texts: readonly TalText[];
|
|
71
|
+
}
|
|
72
|
+
export interface TalRegionParse {
|
|
73
|
+
readonly tals: readonly ParsedTal[];
|
|
74
|
+
readonly issues: readonly TalIssue[];
|
|
75
|
+
}
|
|
76
|
+
/** The two halves of the EDF+ `description@@channel` convention. */
|
|
77
|
+
export interface TalTextParts {
|
|
78
|
+
/** The description exactly as written, minus a trailing `@@channel` suffix. */
|
|
79
|
+
readonly text: string;
|
|
80
|
+
readonly channelLabel: string | undefined;
|
|
81
|
+
}
|
|
82
|
+
/** Caps the bytes copied into a diagnostic. The full region is one hexdump from `byteOffset`. */
|
|
83
|
+
export declare const TAL_PREVIEW_MAX_BYTES: number;
|
|
84
|
+
/**
|
|
85
|
+
* Bytes as a short, escaped, single-line string for a diagnostic message.
|
|
86
|
+
*
|
|
87
|
+
* Latin-1 and not UTF-8 on purpose: this is evidence about bytes, so every byte must map to
|
|
88
|
+
* exactly one visible character even when the run is the invalid UTF-8 being complained about.
|
|
89
|
+
*/
|
|
90
|
+
export declare function previewBytes(bytes: Uint8Array, offset: number, length: number): string;
|
|
91
|
+
/**
|
|
92
|
+
* Every TAL in `[regionStart, regionStart + regionBytes)`, and nothing outside it.
|
|
93
|
+
*
|
|
94
|
+
* `region = *TAL *%x00`, so a 0x00 where a TAL would start means the padding has begun. Content
|
|
95
|
+
* after that point is `TAL_REGION_NOT_NUL_TERMINATED`; parsing then RESUMES at those bytes
|
|
96
|
+
* rather than stopping, because they are still inside this signal's own region and a writer
|
|
97
|
+
* that pads between TALs would otherwise lose every annotation after the first pad. Recovery is
|
|
98
|
+
* bounded: each attempt consumes at least one byte, and a slot that does not parse is skipped
|
|
99
|
+
* to just past its 0x00.
|
|
100
|
+
*/
|
|
101
|
+
export declare function parseTalRegion(bytes: Uint8Array, regionStart: number, regionBytes: number): TalRegionParse;
|
|
102
|
+
/**
|
|
103
|
+
* `description@@channel` split at the LAST `@@`, because the channel label is the suffix and a
|
|
104
|
+
* description is free to contain anything.
|
|
105
|
+
*
|
|
106
|
+
* A trailing `@@` with nothing after it is not a channel label: the text keeps it verbatim.
|
|
107
|
+
* `@@Fp1` with nothing before it yields an empty description and the channel — the run itself
|
|
108
|
+
* is not empty, so it is still a real annotation.
|
|
109
|
+
*/
|
|
110
|
+
export declare function splitChannelLabel(run: string): TalTextParts;
|
|
111
|
+
//# sourceMappingURL=grammar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../../src/tal/grammar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAWH,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,kBAAkB,CAAC;AAE3D,6EAA6E;AAC7E,MAAM,WAAW,OAAO;IACtB,oFAAoF;IACpF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,uEAAuE;AACvE,MAAM,MAAM,YAAY,GACpB,eAAe,GACf,6BAA6B,GAC7B,+BAA+B,GAC/B,0BAA0B,CAAC;AAE/B;;;;;GAKG;AACH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,6DAA6D;IAC7D,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,+DAA+D;IAC/D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,uFAAuF;IACvF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,gGAAgG;AAChG,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,qCAAqC;IACrC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,QAAQ,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,SAAS,SAAS,EAAE,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,CAAC;CACtC;AAED,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3C;AAED,iGAAiG;AACjG,eAAO,MAAM,qBAAqB,EAAE,MAAW,CAAC;AAiGhD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAItF;AAyQD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,UAAU,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,cAAc,CAkChB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAM3D"}
|