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.
Files changed (188) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +319 -0
  3. package/dist/bytes/latin1.d.ts +43 -0
  4. package/dist/bytes/latin1.d.ts.map +1 -0
  5. package/dist/bytes/latin1.js +84 -0
  6. package/dist/bytes/latin1.js.map +1 -0
  7. package/dist/bytes/numbers.d.ts +45 -0
  8. package/dist/bytes/numbers.d.ts.map +1 -0
  9. package/dist/bytes/numbers.js +87 -0
  10. package/dist/bytes/numbers.js.map +1 -0
  11. package/dist/bytes/view.d.ts +27 -0
  12. package/dist/bytes/view.d.ts.map +1 -0
  13. package/dist/bytes/view.js +53 -0
  14. package/dist/bytes/view.js.map +1 -0
  15. package/dist/constants.d.ts +113 -0
  16. package/dist/constants.d.ts.map +1 -0
  17. package/dist/constants.js +83 -0
  18. package/dist/constants.js.map +1 -0
  19. package/dist/decode/digital.d.ts +49 -0
  20. package/dist/decode/digital.d.ts.map +1 -0
  21. package/dist/decode/digital.js +180 -0
  22. package/dist/decode/digital.js.map +1 -0
  23. package/dist/decode/physical.d.ts +37 -0
  24. package/dist/decode/physical.d.ts.map +1 -0
  25. package/dist/decode/physical.js +178 -0
  26. package/dist/decode/physical.js.map +1 -0
  27. package/dist/diagnostics/codes.d.ts +38 -0
  28. package/dist/diagnostics/codes.d.ts.map +1 -0
  29. package/dist/diagnostics/codes.js +134 -0
  30. package/dist/diagnostics/codes.js.map +1 -0
  31. package/dist/diagnostics/collector.d.ts +76 -0
  32. package/dist/diagnostics/collector.d.ts.map +1 -0
  33. package/dist/diagnostics/collector.js +95 -0
  34. package/dist/diagnostics/collector.js.map +1 -0
  35. package/dist/diagnostics/format.d.ts +21 -0
  36. package/dist/diagnostics/format.d.ts.map +1 -0
  37. package/dist/diagnostics/format.js +125 -0
  38. package/dist/diagnostics/format.js.map +1 -0
  39. package/dist/errors.d.ts +143 -0
  40. package/dist/errors.d.ts.map +1 -0
  41. package/dist/errors.js +135 -0
  42. package/dist/errors.js.map +1 -0
  43. package/dist/header/dates.d.ts +101 -0
  44. package/dist/header/dates.d.ts.map +1 -0
  45. package/dist/header/dates.js +378 -0
  46. package/dist/header/dates.js.map +1 -0
  47. package/dist/header/fields.d.ts +105 -0
  48. package/dist/header/fields.d.ts.map +1 -0
  49. package/dist/header/fields.js +363 -0
  50. package/dist/header/fields.js.map +1 -0
  51. package/dist/header/identification.d.ts +47 -0
  52. package/dist/header/identification.d.ts.map +1 -0
  53. package/dist/header/identification.js +156 -0
  54. package/dist/header/identification.js.map +1 -0
  55. package/dist/header/lookup.d.ts +33 -0
  56. package/dist/header/lookup.d.ts.map +1 -0
  57. package/dist/header/lookup.js +67 -0
  58. package/dist/header/lookup.js.map +1 -0
  59. package/dist/header/parse.d.ts +32 -0
  60. package/dist/header/parse.d.ts.map +1 -0
  61. package/dist/header/parse.js +319 -0
  62. package/dist/header/parse.js.map +1 -0
  63. package/dist/header/scale.d.ts +68 -0
  64. package/dist/header/scale.d.ts.map +1 -0
  65. package/dist/header/scale.js +154 -0
  66. package/dist/header/scale.js.map +1 -0
  67. package/dist/header/signals.d.ts +64 -0
  68. package/dist/header/signals.d.ts.map +1 -0
  69. package/dist/header/signals.js +374 -0
  70. package/dist/header/signals.js.map +1 -0
  71. package/dist/header/variant.d.ts +67 -0
  72. package/dist/header/variant.d.ts.map +1 -0
  73. package/dist/header/variant.js +180 -0
  74. package/dist/header/variant.js.map +1 -0
  75. package/dist/index.d.ts +46 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +59 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/inspect.d.ts +31 -0
  80. package/dist/inspect.d.ts.map +1 -0
  81. package/dist/inspect.js +165 -0
  82. package/dist/inspect.js.map +1 -0
  83. package/dist/io/blob.d.ts +14 -0
  84. package/dist/io/blob.d.ts.map +1 -0
  85. package/dist/io/blob.js +29 -0
  86. package/dist/io/blob.js.map +1 -0
  87. package/dist/io/bytes.d.ts +11 -0
  88. package/dist/io/bytes.d.ts.map +1 -0
  89. package/dist/io/bytes.js +26 -0
  90. package/dist/io/bytes.js.map +1 -0
  91. package/dist/io/cached.d.ts +19 -0
  92. package/dist/io/cached.d.ts.map +1 -0
  93. package/dist/io/cached.js +129 -0
  94. package/dist/io/cached.js.map +1 -0
  95. package/dist/io/http.d.ts +20 -0
  96. package/dist/io/http.d.ts.map +1 -0
  97. package/dist/io/http.js +225 -0
  98. package/dist/io/http.js.map +1 -0
  99. package/dist/io/read.d.ts +42 -0
  100. package/dist/io/read.d.ts.map +1 -0
  101. package/dist/io/read.js +119 -0
  102. package/dist/io/read.js.map +1 -0
  103. package/dist/io/source.d.ts +33 -0
  104. package/dist/io/source.d.ts.map +1 -0
  105. package/dist/io/source.js +72 -0
  106. package/dist/io/source.js.map +1 -0
  107. package/dist/node.d.ts +70 -0
  108. package/dist/node.d.ts.map +1 -0
  109. package/dist/node.js +109 -0
  110. package/dist/node.js.map +1 -0
  111. package/dist/record-index.d.ts +58 -0
  112. package/dist/record-index.d.ts.map +1 -0
  113. package/dist/record-index.js +290 -0
  114. package/dist/record-index.js.map +1 -0
  115. package/dist/recording.d.ts +57 -0
  116. package/dist/recording.d.ts.map +1 -0
  117. package/dist/recording.js +178 -0
  118. package/dist/recording.js.map +1 -0
  119. package/dist/tal/annotations.d.ts +36 -0
  120. package/dist/tal/annotations.d.ts.map +1 -0
  121. package/dist/tal/annotations.js +437 -0
  122. package/dist/tal/annotations.js.map +1 -0
  123. package/dist/tal/grammar.d.ts +111 -0
  124. package/dist/tal/grammar.d.ts.map +1 -0
  125. package/dist/tal/grammar.js +324 -0
  126. package/dist/tal/grammar.js.map +1 -0
  127. package/dist/tal/ticks.d.ts +69 -0
  128. package/dist/tal/ticks.d.ts.map +1 -0
  129. package/dist/tal/ticks.js +137 -0
  130. package/dist/tal/ticks.js.map +1 -0
  131. package/dist/time/segments.d.ts +33 -0
  132. package/dist/time/segments.d.ts.map +1 -0
  133. package/dist/time/segments.js +87 -0
  134. package/dist/time/segments.js.map +1 -0
  135. package/dist/time/timeline.d.ts +62 -0
  136. package/dist/time/timeline.d.ts.map +1 -0
  137. package/dist/time/timeline.js +227 -0
  138. package/dist/time/timeline.js.map +1 -0
  139. package/dist/time/window.d.ts +54 -0
  140. package/dist/time/window.d.ts.map +1 -0
  141. package/dist/time/window.js +232 -0
  142. package/dist/time/window.js.map +1 -0
  143. package/dist/types.d.ts +409 -0
  144. package/dist/types.d.ts.map +1 -0
  145. package/dist/types.js +16 -0
  146. package/dist/types.js.map +1 -0
  147. package/dist/validate.d.ts +53 -0
  148. package/dist/validate.d.ts.map +1 -0
  149. package/dist/validate.js +483 -0
  150. package/dist/validate.js.map +1 -0
  151. package/package.json +77 -0
  152. package/src/bytes/latin1.ts +87 -0
  153. package/src/bytes/numbers.ts +115 -0
  154. package/src/bytes/view.ts +59 -0
  155. package/src/constants.ts +96 -0
  156. package/src/decode/digital.ts +258 -0
  157. package/src/decode/physical.ts +235 -0
  158. package/src/diagnostics/codes.ts +209 -0
  159. package/src/diagnostics/collector.ts +129 -0
  160. package/src/diagnostics/format.ts +143 -0
  161. package/src/errors.ts +208 -0
  162. package/src/header/dates.ts +453 -0
  163. package/src/header/fields.ts +455 -0
  164. package/src/header/identification.ts +196 -0
  165. package/src/header/lookup.ts +81 -0
  166. package/src/header/parse.ts +398 -0
  167. package/src/header/scale.ts +268 -0
  168. package/src/header/signals.ts +550 -0
  169. package/src/header/variant.ts +239 -0
  170. package/src/index.ts +151 -0
  171. package/src/inspect.ts +181 -0
  172. package/src/io/blob.ts +31 -0
  173. package/src/io/bytes.ts +29 -0
  174. package/src/io/cached.ts +136 -0
  175. package/src/io/http.ts +299 -0
  176. package/src/io/read.ts +152 -0
  177. package/src/io/source.ts +87 -0
  178. package/src/node.ts +138 -0
  179. package/src/record-index.ts +389 -0
  180. package/src/recording.ts +234 -0
  181. package/src/tal/annotations.ts +574 -0
  182. package/src/tal/grammar.ts +540 -0
  183. package/src/tal/ticks.ts +172 -0
  184. package/src/time/segments.ts +109 -0
  185. package/src/time/timeline.ts +290 -0
  186. package/src/time/window.ts +304 -0
  187. package/src/types.ts +490 -0
  188. package/src/validate.ts +610 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sarthak Tayal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,319 @@
1
+ # edfcore
2
+
3
+ edfcore reads EDF, EDF+, BDF and BDF+ biosignal files in TypeScript, in the browser and in
4
+ Node. Real random access, no dependencies.
5
+
6
+ ```bash
7
+ npm install edfcore
8
+ ```
9
+
10
+ ## How do I read an EDF file in JavaScript?
11
+
12
+ Open a source, pick a signal, read a time window, convert it to physical units. In the browser,
13
+ a `File` from an `<input type="file">` works directly:
14
+
15
+ ```ts
16
+ import { openEdf, blobSource, getSignal, readWindow, toPhysical } from 'edfcore';
17
+
18
+ const recording = await openEdf(blobSource(file));
19
+ const fp1 = getSignal(recording.header, 'Fp1');
20
+
21
+ const [chunk] = await readWindow(recording, {
22
+ signalIndices: [fp1.index],
23
+ startSeconds: 30,
24
+ durationSeconds: 10,
25
+ });
26
+
27
+ const microvolts = toPhysical(fp1, chunk.signals[0].digital); // Float64Array
28
+ ```
29
+
30
+ In Node, swap the source and nothing else changes:
31
+
32
+ ```ts
33
+ import { openEdf, getSignal, readWindow, toPhysical } from 'edfcore';
34
+ import { fileSource } from 'edfcore/node';
35
+
36
+ const recording = await openEdf(await fileSource('./overnight.edf'));
37
+
38
+ console.log(recording.header.variant); // 'EDF+C'
39
+ console.log(recording.header.signals.map((s) => s.label));
40
+ console.log(recording.timeline.spanSeconds);
41
+ ```
42
+
43
+ Reading those ten seconds out of a twelve-hour recording reads roughly ten seconds' worth of
44
+ bytes. Nothing loads the whole file.
45
+
46
+ Events come from the same recording, with exact times:
47
+
48
+ ```ts
49
+ import { readAnnotations } from 'edfcore';
50
+
51
+ const { annotations } = await readAnnotations(recording, {
52
+ start: 0,
53
+ count: recording.header.recordCount,
54
+ });
55
+
56
+ for (const event of annotations) {
57
+ console.log(event.onsetSecondsFromFirstRecord, event.durationSeconds, event.text);
58
+ }
59
+ ```
60
+
61
+ > **Status: 0.1.0, early.** edfcore runs 1,100+ tests on generated fixtures, and it's checked
62
+ > against public corpora it didn't author: the EDF, EDF+ and 24-bit BDF+ test files from
63
+ > teuniz.net, and PhysioNet's sleep-edfx (a real 22-hour polysomnography recording and its
64
+ > sleep-staging file). Those checks are numeric. Channels labelled `sine 8.5 Hz` decode to
65
+ > 8.5 Hz at their stated amplitude, the 24-bit and 16-bit paths agree on identical signals,
66
+ > and the rectal temperature channel of a real recording reads 37 °C. Run them with
67
+ > `npm run corpus:fetch && npm test`.
68
+ >
69
+ > One thing is missing: a golden-value harness comparing edfcore's float64 output against
70
+ > pyEDFlib and MNE element by element. Until it exists, edfcore claims no numeric parity with
71
+ > those readers. The API can still move. See [Roadmap](#roadmap).
72
+
73
+ ---
74
+
75
+ ## When to use edfcore, and when not to
76
+
77
+ **Use it when** you need to read EDF, EDF+, BDF or BDF+ in JavaScript or TypeScript. That
78
+ covers browser-based EEG and sleep viewers, Electron desktop tools, Node data pipelines, and
79
+ research upload portals that must not send recordings to a server.
80
+
81
+ **Use something else when:**
82
+
83
+ - **You are already in Python.** pyEDFlib and MNE are mature, validated against enormous
84
+ amounts of real data, and have years of clinical use behind them. Don't port a working Python
85
+ pipeline to JavaScript for the sake of it.
86
+ - **You want a whole viewer stack.** [`@epicurrents/edf-reader`](https://github.com/epicurrents/edf-reader)
87
+ is strong prior art and arrives with an EEG application framework attached.
88
+ - **You need analysis, not file access.** edfcore does not filter, resample, re-reference,
89
+ build montages, run ICA, reject artifacts, or detect events. Those are permanent non-goals.
90
+ - **You need to *write* EDF.** edfcore is read-only through 1.0.
91
+
92
+ ---
93
+
94
+ ## Why this exists
95
+
96
+ EDF is the standard container for EEG, sleep studies, ECG and EMG. Python has excellent tooling
97
+ for it. JavaScript didn't. This is every EDF package on npm, surveyed in mid-2026:
98
+
99
+ | | Before edfcore |
100
+ |---|---|
101
+ | TypeScript types | No standalone reader ships them |
102
+ | Random access | Nothing published does byte-range reads; a 24 h study is loaded whole or not at all |
103
+ | BDF (24-bit) | No published package can read it. BioSemi hardware has no JS support |
104
+ | EDF+D (gaps) | Unsupported or, worse, decoded as if contiguous, producing a silently wrong timeline |
105
+ | Errors | `console.warn` and `null`, or bare thrown strings; nothing typed or located |
106
+ | Header validation | Essentially none. No size checks, no degenerate-range checks |
107
+
108
+ So people building EEG viewers don't reach for an EDF package. They hand-roll a parser and
109
+ reproduce the same bugs.
110
+
111
+ The strongest existing implementation is
112
+ [`@epicurrents/edf-reader`](https://github.com/epicurrents/edf-reader): real TAL parsing, real
113
+ partial reads, BDF support. It's a plugin inside an EEG application framework, so it arrives
114
+ with that framework attached. Use it if you want a whole viewer stack. edfcore is a small,
115
+ standalone, dependency-free file-format library you can drop into any project.
116
+
117
+ ---
118
+
119
+ ## Design in one page
120
+
121
+ **A bad file either throws or reports itself.** One rule decides which. If edfcore can't
122
+ continue without inventing something, it throws. If it can continue on what the file says, it
123
+ records a diagnostic on the result. There's no third option, and nothing is written to the
124
+ console.
125
+
126
+ ```ts
127
+ const recording = await openEdf(source);
128
+ for (const d of recording.header.diagnostics) {
129
+ console.log(d.code, d.message, d.byteOffset); // your logger, your call
130
+ }
131
+ ```
132
+
133
+ Every diagnostic carries the field, the byte offset, the raw bytes as written, the spec clause it
134
+ violates, and what to do next. Pass `{ strict: true }` and the first one throws instead.
135
+
136
+ **Digital and physical are two functions.** `chunk.signals[i].digital` is an `Int32Array` of
137
+ the values as stored. `toPhysical(signal, digital)` returns a `Float64Array` in the signal's own
138
+ units. There's no `{ physical: true }` option, because it would change the return type, and no
139
+ `Float32` output (it costs about a quarter of a quantisation step on 24-bit BDF).
140
+
141
+ **Scaling can be refused.** A header that declares `digitalMinimum === digitalMaximum` defines
142
+ no scale: the gain is a division by zero. edfcore sets `signal.scale` to `undefined`, so
143
+ `toPhysical` throws and `strictNullChecks` catches the call you didn't guard. `decodeDigital`
144
+ keeps working. EDFlib substitutes a gain of 1 here and returns ADC counts labelled as microvolts.
145
+
146
+ **Sample rates stay per-signal.** An EDF file can hold EEG at 256 Hz, ECG at 512 Hz and
147
+ temperature at 1 Hz. There's no recording-wide rate. Sample indexing uses `samplesPerRecord`
148
+ rather than a floating-point rate, and `sampleRateHz` is `undefined` when the record duration is
149
+ zero (which is legal EDF).
150
+
151
+ **Event times are exact.** Annotation onsets are parsed digit by digit into `bigint`
152
+ hundred-nanosecond ticks, not through `parseFloat`. Compare `onsetTicks`, not the float.
153
+
154
+ **Gaps are structural.** `readWindow` always returns an array of chunks, one per contiguous
155
+ run, including for continuous files. A window that falls inside a gap returns `[]`. There's no
156
+ gap-filling and no option to enable it.
157
+
158
+ **No `Date`.** EDF stores local time at the patient with no timezone, and a `Date` would apply
159
+ the reader's zone instead. You get `EdfCalendarDate` and `EdfClockTime`, plus
160
+ `formatStartTimeNaive()` for a zone-free ISO-like string.
161
+
162
+ ---
163
+
164
+ ## Reading data
165
+
166
+ The unit of I/O is the **record range**, not the channel range. EDF interleaves every channel
167
+ inside each data record, so there's no cheap single-channel read: asking for one channel over a
168
+ window still reads the records containing it. `chunk.byteLength` reports the bytes actually
169
+ read.
170
+
171
+ ```ts
172
+ import { openEdf, readWindow, resolveTimeWindow } from 'edfcore';
173
+ import { fileSource } from 'edfcore/node';
174
+
175
+ const recording = await openEdf(await fileSource('./overnight.edf'));
176
+
177
+ // resolveTimeWindow is pure and does no I/O, so you can audit the cost first.
178
+ const ranges = resolveTimeWindow(recording.timeline, recording.index, 3600, 30);
179
+
180
+ const chunks = await readWindow(recording, {
181
+ signalIndices: [0, 1, 2],
182
+ startSeconds: 3600,
183
+ durationSeconds: 30,
184
+ });
185
+ ```
186
+
187
+ Chunks are record-aligned and may be slightly wider than requested. `trimToWindow()` narrows
188
+ them to a sample-exact window using integer arithmetic on `(record, sampleWithinRecord)` rather
189
+ than `round(t * rate)`.
190
+
191
+ ### Sources
192
+
193
+ One interface, four adapters, all universal:
194
+
195
+ ```ts
196
+ byteSource(arrayBufferOrUint8Array)
197
+ blobSource(fileOrBlob) // structural, no DOM types required
198
+ httpSource(url, { headers }) // HTTP Range requests
199
+ cachedSource(inner, { blockBytes }) // the only caching, opt-in
200
+ ```
201
+
202
+ and from `edfcore/node`:
203
+
204
+ ```ts
205
+ fileSource('./recording.edf')
206
+ fileHandleSource(handle, byteLength)
207
+ ```
208
+
209
+ Bring your own by implementing `ByteSource`, which has three members. The contract is *exactly
210
+ `length` bytes or reject*, and edfcore verifies it on every call, including on sources you
211
+ supplied.
212
+
213
+ ### Discontinuous recordings
214
+
215
+ Opening a file never scans it. For EDF+D the index starts out `'probed'`, meaning record 0 and
216
+ the last record only. It throws rather than map a timestamp to a record across an unmapped gap:
217
+
218
+ ```ts
219
+ const index = await buildRecordIndex(recording, { onProgress: (done, total) => … });
220
+ const located = { ...recording, index };
221
+
222
+ index.coverage; // 'complete'
223
+ index.segments; // EdfSegment[] — only defined once coverage is complete
224
+ index.gaps; // EdfGap[]
225
+ ```
226
+
227
+ `segments` is `undefined` while coverage is `'probed'`, so no property reads as
228
+ "continuous" before anything has checked.
229
+
230
+ ### Triage
231
+
232
+ `inspectEdf()` reads at most 128 KiB and never throws on malformed content. It is the right first
233
+ call for an unfamiliar file:
234
+
235
+ ```ts
236
+ const { ok, variant, header, diagnostics } = await inspectEdf(source);
237
+ console.log(formatDiagnostics(diagnostics));
238
+ ```
239
+
240
+ ---
241
+
242
+ ## What it deliberately does not do
243
+
244
+ edfcore is a file-format library. It is not an EEG analysis framework. These are permanent non-goals: filtering, resampling,
245
+ re-referencing, montages, ICA, artifact rejection, bad-channel detection, spectral analysis,
246
+ event detection, channel-type inference from labels, unit normalisation to SI volts, and
247
+ anything involving AI or a network service.
248
+
249
+ Writing EDF is out of scope through 1.0. Producing a *correct* file is a much larger normative
250
+ commitment than tolerating an incorrect one, and a subtly non-conformant writer would undermine
251
+ the reason to trust the reader.
252
+
253
+ ---
254
+
255
+ ## Compatibility
256
+
257
+ - **Node** ≥ 22.12.0 · **Chrome/Edge** 94+ · **Firefox** 93+ · **Safari** 15.4+
258
+ - ESM only. `require()` works on Node ≥ 22.12 (there is no top-level `await` anywhere in the
259
+ module graph, which is what makes that safe).
260
+ - Zero runtime dependencies, permanently. `edfcore/node` imports `node:fs/promises` and nothing
261
+ else.
262
+ - Three entry points, no environment conditions in the exports map: `edfcore`, `edfcore/node`,
263
+ `edfcore/validate`.
264
+
265
+ ---
266
+
267
+ ## Roadmap
268
+
269
+ **0.1 — reads everything, lies about nothing.** All six variants, the primitive/I/O/convenience
270
+ layers, `inspectEdf`, `edfcore/validate`, six source adapters, the full diagnostic vocabulary.
271
+
272
+ **Before 1.0.** Validation against public corpora (sleep-edfx, CHB-MIT, the BioSemi and
273
+ edfplus.info test files) and a golden-value harness cross-checking physical values against
274
+ pyEDFlib and MNE. Until that harness exists this README makes no numerical-interop claim, and it
275
+ will not make one that a test did not produce.
276
+
277
+ **Later, additive.** Min/max envelope decimation as its own type. The `edffloat` logarithmic
278
+ inverse transform, opt-in (currently detected and rejected). BioSemi
279
+ Status-byte helpers. JSR publication.
280
+
281
+ ---
282
+
283
+ ## Documentation
284
+
285
+ The full documentation site lives in [`website/`](website/), an Astro build with twenty pages.
286
+ It includes a **local inspector** that opens an EDF file and shows its header, channels, events
287
+ and waveforms in your browser, with nothing uploaded.
288
+
289
+ ```bash
290
+ npm install --prefix website
291
+ npm run dev --prefix website
292
+ ```
293
+
294
+ Start with **Concepts**, which is the mental model the rest of the API follows from. Then the guides (reading signals, physical values, annotations, discontinuous recordings,
295
+ diagnostics, data sources, large files, validation), the API reference, and the background
296
+ pages. Those include a standalone primer on the EDF format itself.
297
+
298
+ ### Deploying the site
299
+
300
+ [`vercel.json`](vercel.json) is set up so linking this repository to Vercel works as is.
301
+ **Leave the Root Directory as the repository root** and don't pick a framework preset. The
302
+ build compiles the library first (the site imports `edfcore` from the parent package), then
303
+ builds the site into `website/dist`.
304
+
305
+ Nothing needs configuring for URLs: Astro reads Vercel's own production-domain variable, so the
306
+ sitemap and canonical links are correct on the first deploy. If you later point a custom domain
307
+ at it, set `SITE_URL` to that origin in the project's environment variables.
308
+
309
+ **Design decisions** — why the API is shaped the way it is, including the choices that look
310
+ like bugs and are not (the pinned scaling expression, `readWindow` always returning an array,
311
+ `scale` being `undefined` rather than fabricated) are documented on the site under
312
+ *Background → Design decisions*.
313
+
314
+ - [`tests/README.md`](tests/README.md) covers how the suite builds every fixture in memory,
315
+ and why no binaries are committed.
316
+
317
+ ## License
318
+
319
+ MIT © Sarthak Tayal
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Header text decoding.
3
+ *
4
+ * Layer 0. Imports nothing. Sole owner of turning header bytes into strings.
5
+ *
6
+ * ISO-8859-1 is the identity map onto U+0000..U+00FF, so the whole decoder is
7
+ * `String.fromCharCode`. `TextDecoder` is banned here and everywhere outside `src/tal/`:
8
+ * verified on Node v24.4.0, `TextDecoder('latin1' | 'iso-8859-1' | 'ascii' | 'windows-1252')`
9
+ * all report `encoding === 'windows-1252'` yet decode 0x80 as U+0080, while the WHATWG
10
+ * Encoding Standard mandates U+20AC for those labels. The same header bytes would therefore
11
+ * produce different strings in Node and in a spec-compliant browser.
12
+ */
13
+ /**
14
+ * Decode header bytes as ISO-8859-1: byte `b` becomes code point U+00`b`, always.
15
+ *
16
+ * Never `TextDecoder` — see the module comment.
17
+ */
18
+ export declare function decodeHeaderLatin1(bytes: Uint8Array): string;
19
+ /**
20
+ * True when the range holds any byte outside printable ASCII 32..126 — the alphabet the EDF
21
+ * spec allows in a header field. Drives `NON_ASCII_HEADER_FIELD`, which is a warning: the
22
+ * field still decodes truthfully as Latin-1, and real equipment writes accented names and a
23
+ * bare 0xB5 for micro.
24
+ *
25
+ * Trailing NUL padding counts, deliberately: it is outside the alphabet too, and the caller
26
+ * that wants to tolerate it can check the padding itself.
27
+ */
28
+ export declare function hasNonPrintableAscii(bytes: Uint8Array): boolean;
29
+ /**
30
+ * Strip EDF field padding: spaces and NULs, from both ends.
31
+ *
32
+ * EDF pads on the RIGHT with spaces, so trailing padding is the expected case. Leading
33
+ * padding is stripped as well, and that choice is deliberate: a label written `' Fp1'`
34
+ * names the same electrode as `'Fp1 '`, and text fields are compared, not measured. No
35
+ * evidence is lost by it — every field is also exposed raw and untrimmed, and for numeric
36
+ * fields the non-conformance is reported separately by `parseEdfInteger`/`parseEdfNumber`,
37
+ * which read the untrimmed string.
38
+ *
39
+ * Only 0x20 and 0x00 are stripped. A trailing TAB or CR is not padding, it is content the
40
+ * file should not contain, and hiding it here would hide it from `NON_ASCII_HEADER_FIELD`.
41
+ */
42
+ export declare function trimEdfField(text: string): string;
43
+ //# sourceMappingURL=latin1.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"latin1.d.ts","sourceRoot":"","sources":["../../src/bytes/latin1.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAsBH;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAO5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAK/D;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMjD"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Header text decoding.
3
+ *
4
+ * Layer 0. Imports nothing. Sole owner of turning header bytes into strings.
5
+ *
6
+ * ISO-8859-1 is the identity map onto U+0000..U+00FF, so the whole decoder is
7
+ * `String.fromCharCode`. `TextDecoder` is banned here and everywhere outside `src/tal/`:
8
+ * verified on Node v24.4.0, `TextDecoder('latin1' | 'iso-8859-1' | 'ascii' | 'windows-1252')`
9
+ * all report `encoding === 'windows-1252'` yet decode 0x80 as U+0080, while the WHATWG
10
+ * Encoding Standard mandates U+20AC for those labels. The same header bytes would therefore
11
+ * produce different strings in Node and in a spec-compliant browser.
12
+ */
13
+ /** Padding bytes an EDF field may carry. Space is the spec's; NUL is what writers emit. */
14
+ const CHAR_NUL = 0x00;
15
+ const CHAR_SPACE = 0x20;
16
+ /** The spec's legal header alphabet. Anything outside it is Latin-1 in practice. */
17
+ const PRINTABLE_ASCII_MIN = 0x20;
18
+ const PRINTABLE_ASCII_MAX = 0x7e;
19
+ /**
20
+ * Well under every engine's argument-count limit (Safari's is the lowest, ~65k), so a
21
+ * maximal 2.56 MB header costs 625 calls rather than one that throws.
22
+ */
23
+ const CHUNK_BYTES = 4096;
24
+ function fromCharCodes(bytes) {
25
+ // A Uint8Array is an array-like at runtime, which is all `apply` needs; the strict
26
+ // `Function.prototype.apply` signature only admits `number[]`, hence the cast.
27
+ return String.fromCharCode.apply(null, bytes);
28
+ }
29
+ /**
30
+ * Decode header bytes as ISO-8859-1: byte `b` becomes code point U+00`b`, always.
31
+ *
32
+ * Never `TextDecoder` — see the module comment.
33
+ */
34
+ export function decodeHeaderLatin1(bytes) {
35
+ if (bytes.length <= CHUNK_BYTES)
36
+ return fromCharCodes(bytes);
37
+ const parts = [];
38
+ for (let start = 0; start < bytes.length; start += CHUNK_BYTES) {
39
+ parts.push(fromCharCodes(bytes.subarray(start, Math.min(start + CHUNK_BYTES, bytes.length))));
40
+ }
41
+ return parts.join('');
42
+ }
43
+ /**
44
+ * True when the range holds any byte outside printable ASCII 32..126 — the alphabet the EDF
45
+ * spec allows in a header field. Drives `NON_ASCII_HEADER_FIELD`, which is a warning: the
46
+ * field still decodes truthfully as Latin-1, and real equipment writes accented names and a
47
+ * bare 0xB5 for micro.
48
+ *
49
+ * Trailing NUL padding counts, deliberately: it is outside the alphabet too, and the caller
50
+ * that wants to tolerate it can check the padding itself.
51
+ */
52
+ export function hasNonPrintableAscii(bytes) {
53
+ for (const byte of bytes) {
54
+ if (byte < PRINTABLE_ASCII_MIN || byte > PRINTABLE_ASCII_MAX)
55
+ return true;
56
+ }
57
+ return false;
58
+ }
59
+ function isPadding(code) {
60
+ return code === CHAR_SPACE || code === CHAR_NUL;
61
+ }
62
+ /**
63
+ * Strip EDF field padding: spaces and NULs, from both ends.
64
+ *
65
+ * EDF pads on the RIGHT with spaces, so trailing padding is the expected case. Leading
66
+ * padding is stripped as well, and that choice is deliberate: a label written `' Fp1'`
67
+ * names the same electrode as `'Fp1 '`, and text fields are compared, not measured. No
68
+ * evidence is lost by it — every field is also exposed raw and untrimmed, and for numeric
69
+ * fields the non-conformance is reported separately by `parseEdfInteger`/`parseEdfNumber`,
70
+ * which read the untrimmed string.
71
+ *
72
+ * Only 0x20 and 0x00 are stripped. A trailing TAB or CR is not padding, it is content the
73
+ * file should not contain, and hiding it here would hide it from `NON_ASCII_HEADER_FIELD`.
74
+ */
75
+ export function trimEdfField(text) {
76
+ let start = 0;
77
+ let end = text.length;
78
+ while (end > start && isPadding(text.charCodeAt(end - 1)))
79
+ end--;
80
+ while (start < end && isPadding(text.charCodeAt(start)))
81
+ start++;
82
+ return text.slice(start, end);
83
+ }
84
+ //# sourceMappingURL=latin1.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"latin1.js","sourceRoot":"","sources":["../../src/bytes/latin1.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,2FAA2F;AAC3F,MAAM,QAAQ,GAAG,IAAI,CAAC;AACtB,MAAM,UAAU,GAAG,IAAI,CAAC;AAExB,oFAAoF;AACpF,MAAM,mBAAmB,GAAG,IAAI,CAAC;AACjC,MAAM,mBAAmB,GAAG,IAAI,CAAC;AAEjC;;;GAGG;AACH,MAAM,WAAW,GAAG,IAAI,CAAC;AAEzB,SAAS,aAAa,CAAC,KAAiB;IACtC,mFAAmF;IACnF,+EAA+E;IAC/E,OAAO,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,KAA4B,CAAC,CAAC;AACvE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAiB;IAClD,IAAI,KAAK,CAAC,MAAM,IAAI,WAAW;QAAE,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;IAC7D,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,WAAW,EAAE,CAAC;QAC/D,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAChG,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACxB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAiB;IACpD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,GAAG,mBAAmB,IAAI,IAAI,GAAG,mBAAmB;YAAE,OAAO,IAAI,CAAC;IAC5E,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,IAAI,KAAK,UAAU,IAAI,IAAI,KAAK,QAAQ,CAAC;AAClD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC;IACtB,OAAO,GAAG,GAAG,KAAK,IAAI,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;QAAE,GAAG,EAAE,CAAC;IACjE,OAAO,KAAK,GAAG,GAAG,IAAI,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAAE,KAAK,EAAE,CAAC;IACjE,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAChC,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The two EDF numeric grammars.
3
+ *
4
+ * Layer 0. Imports nothing. Sole owner of turning an 8-byte ASCII field into a number.
5
+ *
6
+ * The spec says these fields are ASCII, left-justified and space-padded. Real files break
7
+ * that in every way imaginable, so parsing reports *why* it failed rather than returning NaN
8
+ * and leaving the caller to guess which diagnostic the bytes deserve: a comma decimal is
9
+ * fatal, a right-justified field is a warning, and `'20 48'` is neither of those.
10
+ */
11
+ /**
12
+ * The outcome of parsing one numeric field.
13
+ *
14
+ * `ok === false` means `value` is meaningless — it is NaN, so a caller that ignores this flag
15
+ * fails loudly instead of quietly recording a plausible 0.
16
+ *
17
+ * `problem` is single-valued and ordered by how much it matters: a field that is both
18
+ * right-justified and comma-separated reports `'comma-decimal'`, because that is the one the
19
+ * caller must refuse.
20
+ */
21
+ export interface EdfNumberParse {
22
+ readonly ok: boolean;
23
+ readonly value: number;
24
+ /** The field exactly as it was read, padding included. */
25
+ readonly raw: string;
26
+ readonly problem: 'none' | 'empty' | 'comma-decimal' | 'not-left-justified' | 'malformed';
27
+ }
28
+ /**
29
+ * Parse a field that must be a whole number: signal count, samples per record, record count,
30
+ * digital minimum and maximum.
31
+ *
32
+ * Exponent forms are deliberately rejected. Every field parsed by this function sizes the
33
+ * file geometry, and `'1E3'` in one of them is far likelier to be corruption than a writer's
34
+ * idea of 1000 — accepting it would turn unreadable bytes into a confidently wrong offset.
35
+ * `'256.0'` is rejected for the same reason.
36
+ */
37
+ export declare function parseEdfInteger(raw: string): EdfNumberParse;
38
+ /**
39
+ * Parse a field that may be fractional: physical minimum and maximum, record duration.
40
+ *
41
+ * Accepts a leading sign, a bare leading or trailing point, and an exponent, because writers
42
+ * in the wild emit `'+22'`, `'.5'`, `'1E3'` and `'-1.23E-4'`.
43
+ */
44
+ export declare function parseEdfNumber(raw: string): EdfNumberParse;
45
+ //# sourceMappingURL=numbers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"numbers.d.ts","sourceRoot":"","sources":["../../src/bytes/numbers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;;;;GASG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,0DAA0D;IAC1D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,eAAe,GAAG,oBAAoB,GAAG,WAAW,CAAC;CAC3F;AAkED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAE3D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAE1D"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The two EDF numeric grammars.
3
+ *
4
+ * Layer 0. Imports nothing. Sole owner of turning an 8-byte ASCII field into a number.
5
+ *
6
+ * The spec says these fields are ASCII, left-justified and space-padded. Real files break
7
+ * that in every way imaginable, so parsing reports *why* it failed rather than returning NaN
8
+ * and leaving the caller to guess which diagnostic the bytes deserve: a comma decimal is
9
+ * fatal, a right-justified field is a warning, and `'20 48'` is neither of those.
10
+ */
11
+ const CHAR_NUL = 0x00;
12
+ const CHAR_SPACE = 0x20;
13
+ /** No decimal point and no exponent — see `parseEdfInteger`. */
14
+ const INTEGER_GRAMMAR = /^[+-]?[0-9]+$/;
15
+ /** `'.5'`, `'+22'`, `'1E3'` and `'-1.23E-4'` are all emitted by real writers. */
16
+ const NUMBER_GRAMMAR = /^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/;
17
+ function isPadding(code) {
18
+ return code === CHAR_SPACE || code === CHAR_NUL;
19
+ }
20
+ function failure(raw, problem) {
21
+ return { ok: false, value: Number.NaN, raw, problem };
22
+ }
23
+ function containsNul(text) {
24
+ for (let i = 0; i < text.length; i++) {
25
+ if (text.charCodeAt(i) === CHAR_NUL)
26
+ return true;
27
+ }
28
+ return false;
29
+ }
30
+ function parseField(raw, grammar, integral) {
31
+ let start = 0;
32
+ let end = raw.length;
33
+ while (start < end && isPadding(raw.charCodeAt(start)))
34
+ start++;
35
+ while (end > start && isPadding(raw.charCodeAt(end - 1)))
36
+ end--;
37
+ const core = raw.slice(start, end);
38
+ // Distinct from malformed: an all-space field is a writer omitting a value, not corrupting
39
+ // one, and several fields are legally blank.
40
+ if (core.length === 0)
41
+ return failure(raw, 'empty');
42
+ if (core.includes(',')) {
43
+ // '0,5' (half) and '1,024' (a thousand and twenty-four) are indistinguishable, and
44
+ // substituting '.' in the second silently turns 1024 into 1.024. Fatal, never guessed.
45
+ // Reported as comma-decimal only when the field is otherwise numeric; 'a,b' is just junk.
46
+ const substituted = core.replaceAll(',', '.');
47
+ return failure(raw, NUMBER_GRAMMAR.test(substituted) ? 'comma-decimal' : 'malformed');
48
+ }
49
+ // Embedded whitespace inside the digits reaches here intact: '20 48' fails the grammar and
50
+ // is malformed, never 2048 and never 20.
51
+ if (!grammar.test(core))
52
+ return failure(raw, 'malformed');
53
+ const value = Number(core);
54
+ // Guards a digit string too long for exact float64 (integral) or one like '1e999' that
55
+ // overflows to Infinity. Neither can come from an 8-byte field, but this function takes a
56
+ // string, not a field.
57
+ if (integral ? !Number.isSafeInteger(value) : !Number.isFinite(value)) {
58
+ return failure(raw, 'malformed');
59
+ }
60
+ // The value is trustworthy; the layout is not. Leading padding means right-justified, and a
61
+ // NUL anywhere in the padding means the writer used the wrong pad byte. Both parse, and
62
+ // both are worth a NUMERIC_FIELD_NOT_LEFT_JUSTIFIED warning.
63
+ const leftJustified = start === 0 && !containsNul(raw.slice(end));
64
+ return { ok: true, value, raw, problem: leftJustified ? 'none' : 'not-left-justified' };
65
+ }
66
+ /**
67
+ * Parse a field that must be a whole number: signal count, samples per record, record count,
68
+ * digital minimum and maximum.
69
+ *
70
+ * Exponent forms are deliberately rejected. Every field parsed by this function sizes the
71
+ * file geometry, and `'1E3'` in one of them is far likelier to be corruption than a writer's
72
+ * idea of 1000 — accepting it would turn unreadable bytes into a confidently wrong offset.
73
+ * `'256.0'` is rejected for the same reason.
74
+ */
75
+ export function parseEdfInteger(raw) {
76
+ return parseField(raw, INTEGER_GRAMMAR, true);
77
+ }
78
+ /**
79
+ * Parse a field that may be fractional: physical minimum and maximum, record duration.
80
+ *
81
+ * Accepts a leading sign, a bare leading or trailing point, and an exponent, because writers
82
+ * in the wild emit `'+22'`, `'.5'`, `'1E3'` and `'-1.23E-4'`.
83
+ */
84
+ export function parseEdfNumber(raw) {
85
+ return parseField(raw, NUMBER_GRAMMAR, false);
86
+ }
87
+ //# sourceMappingURL=numbers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"numbers.js","sourceRoot":"","sources":["../../src/bytes/numbers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Bounds-checked slicing.
3
+ *
4
+ * Layer 0. Every offset here is a plain JS number — exact to 2^53 — and is never touched by
5
+ * `|0`, `<<`, `>>` or `>>>`. A data offset in a multi-gigabyte BDF routinely exceeds 2^31,
6
+ * where every bitwise operator silently wraps it to a negative number. (Bitwise ops are
7
+ * correct and required for SAMPLE decoding, which operates on 16- and 24-bit values.)
8
+ */
9
+ /**
10
+ * A VIEW of `length` bytes at `offset`, sharing memory with `bytes` — no copy is made.
11
+ *
12
+ * Nothing in edfcore mutates a source buffer, so a view is safe to read for as long as the
13
+ * buffer lives. A caller that pools or reuses buffers must not retain the view past that
14
+ * reuse: use `copyBytes` when the bytes have to outlive the read that produced them.
15
+ */
16
+ export declare function sliceBytes(bytes: Uint8Array, offset: number, length: number): Uint8Array;
17
+ /** `length` bytes at `offset`, copied into a buffer of their own. */
18
+ export declare function copyBytes(bytes: Uint8Array, offset: number, length: number): Uint8Array;
19
+ /**
20
+ * The RAW, untrimmed text of a fixed-width header field, decoded as ISO-8859-1.
21
+ *
22
+ * Untrimmed on purpose: `raw.*` on the header exposes what the file actually wrote, and the
23
+ * numeric grammars need the padding to tell a right-justified field from a conformant one.
24
+ * Callers that want the value trim it themselves with `trimEdfField`.
25
+ */
26
+ export declare function readAsciiField(bytes: Uint8Array, offset: number, length: number): string;
27
+ //# sourceMappingURL=view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../src/bytes/view.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAwBH;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAGxF;AAED,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAGvF;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAExF"}