/** * The byte-level TAL state machine. * * Layer 3. Sole owner of turning ONE annotation region into time-stamped annotation lists. * `annotations.ts` adds provenance and EDF+ semantics on top; nothing here knows about records, * signals, or timekeeping. * * Two rules define the module, and both are where other EDF readers go wrong: * * 1. Parsing is HARD-BOUNDED to `[regionStart, regionStart + regionBytes)`. A TAL with no * terminating 0x00 inside the region is discarded, never continued past the bound — the bytes * after it are the next signal's samples, and parsing those is how a reader invents * annotations that are not in the file. * 2. The region is split on the STRUCTURAL bytes (0x00, 0x14, 0x15) FIRST and each text run is * decoded as UTF-8 LAST. That order is safe in exactly one direction: every byte of a * multi-byte UTF-8 sequence is >= 0x80 and can never collide with a structural byte, while a * string that has already been decoded can no longer be split on bytes at all. * * `src/tal/` is the only part of edfcore allowed to use `TextDecoder`, and only for annotation * text. Header bytes go through `decodeHeaderLatin1`, for the cross-runtime reason documented in * `src/bytes/latin1.ts`. * * Each defect is reported at most once per region, with an occurrence count: a corrupt region * can hold thousands of malformed TALs, and one diagnostic per TAL would be an unbounded * allocation dressed up as diligence. Nothing is hidden — `occurrences` says how many there were. */ /** * How a TAL text run decoded. `latin-1-fallback` means the bytes were not valid UTF-8 and were * read byte-per-character instead — recorded rather than assumed, because this is the one region * of the file where a real `TextDecoder` is allowed and its verdict is worth reporting. */ export type TalTextEncoding = 'utf-8' | 'latin-1-fallback'; /** One text run of a TAL: the bytes between two 0x14 separators, decoded. */ export interface TalText { /** Verbatim. Never trimmed, never case-folded, and a BOM is kept as a character. */ readonly text: string; readonly encoding: TalTextEncoding; readonly byteOffsetInRegion: number; readonly byteLength: number; } /** The subset of the diagnostic vocabulary this module can observe. */ export type TalIssueCode = 'TAL_MALFORMED' | 'TAL_TRUNCATED_AT_REGION_END' | 'TAL_REGION_NOT_NUL_TERMINATED' | 'ANNOTATION_TEXT_NOT_UTF8'; /** * One defect, described once per region. * * `detail` states what was wrong AND what was done about it, because the disposition differs * per defect: a missing onset sign keeps the annotation, a missing duration discards it. */ export interface TalIssue { readonly code: TalIssueCode; /** Of the first occurrence. Relative to the region start. */ readonly byteOffsetInRegion: number; readonly byteLength: number; /** Occurrences in this region. Only the first is described. */ readonly occurrences: number; readonly detail: string; /** The first occurrence's bytes, escaped and truncated, for the diagnostic message. */ readonly raw: string; /** The same bytes UNESCAPED, for `EdfDiagnostic.raw`, which `formatDiagnostics` quotes itself. */ readonly rawText: string; } /** One TAL that survived parsing. Times are exact ticks; the digits they came from are kept. */ export interface ParsedTal { /** * Position of this TAL in the region, counting the ones that were skipped. Timekeeping is a * property of TAL slot 0, so a record whose first TAL was malformed must not have its second * TAL promoted into the timekeeping role. */ readonly ordinal: number; readonly byteOffsetInRegion: number; /** Includes the terminating 0x00. */ readonly byteLength: number; readonly onsetRaw: string; readonly onsetTicks: bigint; readonly durationRaw: string | undefined; readonly durationTicks: bigint | undefined; readonly texts: readonly TalText[]; } /** * What one annotation region yielded, and what was wrong with it. Both, always: a malformed TAL * does not stop the region, so events after it are still returned — and the issues alongside * them are how the caller learns the list is not everything the bytes contained. */ export interface TalRegionParse { readonly tals: readonly ParsedTal[]; readonly issues: readonly TalIssue[]; } /** The two halves of the EDF+ `description@@channel` convention. */ export interface TalTextParts { /** The description exactly as written, minus a trailing `@@channel` suffix. */ readonly text: string; readonly channelLabel: string | undefined; } /** Caps the bytes copied into a diagnostic. The full region is one hexdump from `byteOffset`. */ export declare const TAL_PREVIEW_MAX_BYTES: number; /** * Escapes every byte that would otherwise be invisible or would break the line. * * C1 is in scope, not just C0 and DEL. Under Latin-1 the bytes 0x80-0x9F decode to the Unicode C1 * controls, which render as nothing — and in cp1252, the encoding that produces them, that block is * the smart quotes, the en and em dash and the ellipsis. It is the single commonest source of an * invalid-UTF-8 annotation, which is to say the main case `ANNOTATION_TEXT_NOT_UTF8` exists for. * Passing it through meant the message's "Bytes at that offset: ..." hid the very byte it was * complaining about: `Wach<0x96>Beginn` printed as `WachBeginn` (fixed in 0.3.66). * * 0xA0-0xFF stay literal. Those are printable in Latin-1 and é must remain readable — that is the * reason the preview decodes as Latin-1 in the first place. The rule below 0xA0 now matches * `quote()` in `diagnostics/format.ts`, which escapes anything non-printable. * * Exported because a message can carry decoded text as well as raw bytes: the timekeeping-TAL * defect quotes `annotation.text`, which is decoded rather than sliced and so cannot go through * `previewBytes` (fixed in 0.3.104). */ export declare function escapeControls(text: string): string; /** * Bytes as a short, escaped, single-line string for a diagnostic message. * * Latin-1 and not UTF-8 on purpose: this is evidence about bytes, so every byte must map to * exactly one visible character even when the run is the invalid UTF-8 being complained about. */ export declare function previewBytes(bytes: Uint8Array, offset: number, length: number): string; /** * The same bounded run, decoded and NOT escaped. * * `previewBytes` is for a MESSAGE, which is one line of prose, so it escapes. `EdfDiagnostic.raw` * is a data field documented as "those bytes as text, exactly as written including padding", and * `formatDiagnostics` escapes it itself with `quote()`. Putting the escaped preview there made the * public field a 13-character string `\\x01\\x0a\\x1bA` for four bytes, and the rendered detail * line escaped the backslashes a second time (fixed in 0.3.68). * * Still bounded and still Latin-1: a diagnostic must not carry an unbounded copy of a record, and * every byte must map to exactly one character even when the run is the invalid UTF-8 being * complained about. */ export declare function rawBytesText(bytes: Uint8Array, offset: number, length: number): string; /** * Every TAL in `[regionStart, regionStart + regionBytes)`, and nothing outside it. * * `region = *TAL *%x00`, so a 0x00 where a TAL would start means the padding has begun. Content * after that point is `TAL_REGION_NOT_NUL_TERMINATED`; parsing then RESUMES at those bytes * rather than stopping, because they are still inside this signal's own region and a writer * that pads between TALs would otherwise lose every annotation after the first pad. Recovery is * bounded: each attempt consumes at least one byte, and a slot that does not parse is skipped * to just past its 0x00. */ export declare function parseTalRegion(bytes: Uint8Array, regionStart: number, regionBytes: number): TalRegionParse; /** * `description@@channel` split at the LAST `@@`, because the channel label is the suffix and a * description is free to contain anything. * * A trailing `@@` with nothing after it is not a channel label: the text keeps it verbatim. * `@@Fp1` with nothing before it yields an empty description and the channel — the run itself * is not empty, so it is still a real annotation. */ export declare function splitChannelLabel(run: string): TalTextParts; //# sourceMappingURL=grammar.d.ts.map