@native-sdk/core 0.0.0 → 0.5.1

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/sdk/events.ts ADDED
@@ -0,0 +1,137 @@
1
+ // @native-sdk/core/events — the canonical event record types the host and
2
+ // markup deliver into an app core, an SDK LIBRARY module: pure type
3
+ // declarations in the app-core subset, emitted into your core when imported
4
+ // and absent when not. Under node the same file resolves as-is.
5
+ //
6
+ // Every record here matches, field for field, the structural shape a
7
+ // runtime matcher expects — markup's `on-input`/`on-scroll` mirrors
8
+ // (`declaredTextInputUnion`/`declaredScrollStateRecord`), the generated
9
+ // wiring's channel builders (`frameMsg`/`keyMsg`/`appearanceMsg`/
10
+ // `chromeMsg`), and the audio event arm validator. Matching stays
11
+ // STRUCTURAL either way (type identity cannot cross the emission
12
+ // boundary), so declaring the same shape in your own core remains legal;
13
+ // these exports exist so no core has to re-type the vocabulary and no
14
+ // hand-rolled mirror can drift. One home per type: the text-input family
15
+ // lives with the byte-splice engine in `@native-sdk/core/text` and is
16
+ // re-exported here, so `@native-sdk/core/events` resolves every event
17
+ // record an app binds. (Importing this module pulls `./text.ts` into the
18
+ // emitted core with it; unused engine functions cost source lines, never
19
+ // binary — Zig compiles only what the core references.)
20
+ //
21
+ // Names are unique across a core's whole module graph (NS1038): a core
22
+ // that imports one of these types cannot also declare its own record
23
+ // under the same name — delete the in-file mirror and import it instead.
24
+ //
25
+ // How each type is bound:
26
+ // - `TextInputEvent`, `ScrollState`: Msg-arm PAYLOAD fields —
27
+ // `{ kind: "draft_edit"; edit: TextInputEvent }`,
28
+ // `{ kind: "scrolled"; scroll: ScrollState }`.
29
+ // - `FrameEvent`, `KeyEvent`: the wiring channels' parameter records —
30
+ // `frameMsg(model, frame: FrameEvent)`, `keyMsg(key: KeyEvent)`.
31
+ // - `ColorScheme`, `ChromeInsets`, `ChromeButtons`, `AudioState`: field
32
+ // types INSIDE the arm records the `appearanceMsg`/`chromeMsg`/audio
33
+ // routes name (the arms themselves stay inline unions of `kind` plus
34
+ // the event's fields — the subset has no intersection arms).
35
+ // - `AppearanceEvent`, `ChromeEvent`, `AudioEvent`: the full arm
36
+ // payload shapes, canonical and importable for helper signatures
37
+ // (an arm value is structurally assignable to its event record).
38
+
39
+ export type { TextCaretDirection, TextCaretMove, TextSelection, TextInputEvent } from "./text.ts";
40
+
41
+ /// The scroll-state mirror markup's `on-scroll` matches structurally: a
42
+ /// record of exactly these four numeric fields. Offsets and extents are
43
+ /// canvas points; `velocity` is points per second while a fling decays.
44
+ /// Echo `offset` into the model field bound as the scroll's `value` to
45
+ /// keep the region model-driven (setting that field in update scrolls it).
46
+ export interface ScrollState {
47
+ readonly offset: number;
48
+ readonly velocity: number;
49
+ readonly viewportExtent: number;
50
+ readonly contentExtent: number;
51
+ }
52
+
53
+ /// The presented-frame channel's record (`frameMsg(model, frame)`): the
54
+ /// canvas size in points plus the frame clock in fractional milliseconds.
55
+ /// Return null from `frameMsg` for frames that change nothing — the idle
56
+ /// law holds exactly when an idle app dispatches nothing.
57
+ export interface FrameEvent {
58
+ readonly width: number;
59
+ readonly height: number;
60
+ readonly timestampMs: number;
61
+ readonly intervalMs: number;
62
+ }
63
+
64
+ /// The key-fallback channel's record (`keyMsg(key)`): the key NAME arrives
65
+ /// lowercased ("space", "arrowleft"), plus the four modifier booleans. A
66
+ /// focused widget's own keys and editable text always win first.
67
+ export interface KeyEvent {
68
+ readonly key: string;
69
+ readonly shift: boolean;
70
+ readonly control: boolean;
71
+ readonly alt: boolean;
72
+ readonly super: boolean;
73
+ }
74
+
75
+ /// The appearance vocabulary — a NAMED light/dark alias (the host matches
76
+ /// enum members by name, so this is the `colorScheme` field's type in the
77
+ /// arm `appearanceMsg` names).
78
+ export type ColorScheme = "light" | "dark";
79
+
80
+ /// The appearance arm's payload shape, whole: what the system appearance
81
+ /// channel delivers into the arm `appearanceMsg` names. The arm itself
82
+ /// stays an inline union member carrying exactly these fields plus `kind`.
83
+ export interface AppearanceEvent {
84
+ readonly colorScheme: ColorScheme;
85
+ readonly reduceMotion: boolean;
86
+ readonly highContrast: boolean;
87
+ }
88
+
89
+ /// The chrome arm's insets record: the window-chrome band (hidden-titlebar
90
+ /// geometry) in canvas points.
91
+ export interface ChromeInsets {
92
+ readonly top: number;
93
+ readonly right: number;
94
+ readonly bottom: number;
95
+ readonly left: number;
96
+ }
97
+
98
+ /// The chrome arm's traffic-light record: the window buttons' frame in
99
+ /// canvas points.
100
+ export interface ChromeButtons {
101
+ readonly x: number;
102
+ readonly y: number;
103
+ readonly width: number;
104
+ readonly height: number;
105
+ }
106
+
107
+ /// The chrome arm's payload shape, whole: what the window-chrome channel
108
+ /// delivers into the arm `chromeMsg` names — delivered before the first
109
+ /// view build and again whenever the geometry changes.
110
+ export interface ChromeEvent {
111
+ readonly insets: ChromeInsets;
112
+ readonly buttons: ChromeButtons;
113
+ readonly tabsProjected: boolean;
114
+ }
115
+
116
+ /// The audio event states, mirroring the engine's event vocabulary:
117
+ /// `loaded` acknowledges a successful load with the player's duration
118
+ /// estimate; `position` ticks at the platform's honest cadence (~500ms)
119
+ /// while playing; `completed` fires exactly once at the natural end;
120
+ /// `failed` reports a load/decode/device failure; `rejected` a command the
121
+ /// effects layer refused; `spectrum` carries a band-magnitude analysis
122
+ /// frame from hosts that analyze their playback.
123
+ export type AudioState = "loaded" | "position" | "completed" | "failed" | "rejected" | "spectrum";
124
+
125
+ /// The audio event arm's payload shape, whole — the one SDK-fixed record,
126
+ /// six fields matched by NAME: `positionMs`/`durationMs` are milliseconds,
127
+ /// `playing` is the transport state, `buffering` is true while a streamed
128
+ /// source stalls waiting for bytes, and `bands` is the 32 spectrum band
129
+ /// magnitudes (0..255 each, all zeros outside "spectrum" events).
130
+ export interface AudioEvent {
131
+ readonly state: AudioState;
132
+ readonly positionMs: number;
133
+ readonly durationMs: number;
134
+ readonly playing: boolean;
135
+ readonly buffering: boolean;
136
+ readonly bands: Uint8Array;
137
+ }
package/sdk/text.ts ADDED
@@ -0,0 +1,519 @@
1
+ // @native-sdk/core/text — the blessed byte-splice text engine, an SDK
2
+ // LIBRARY module: ordinary app-core subset TypeScript (unlike sdk/core.ts,
3
+ // which is intrinsic and never emits), transpiled into your core when
4
+ // imported and absent from the binary when not. Under node the same file
5
+ // runs as-is — byte for byte the same results.
6
+ //
7
+ // This is the TS counterpart of the runtime's TextBuffer, extracted from
8
+ // the soundboard-ts and system-monitor-ts ports (which carried identical
9
+ // private copies): UTF-8 byte splicing, caret and word movement, selection,
10
+ // IME composition, capacity-refusal with a clamped-insert recovery, ASCII
11
+ // trimming, and ASCII case-insensitive comparison. A core that binds a
12
+ // markup text control reduces the control's TextInputEvent stream over its
13
+ // draft bytes with `applyTextInputEvent`; everything is immutable — each
14
+ // call returns a new state and never touches its inputs.
15
+ //
16
+ // Offsets are BYTE offsets, always snapped to UTF-8 sequence boundaries
17
+ // before use, so a caret can never land inside a multi-byte character.
18
+ // Capacity is the caller's fixed byte budget (mirror your runtime
19
+ // TextBuffer's): an edit whose result would not fit returns null — the
20
+ // refuse-whole contract — and `clampedInsertEvent` recovers the one case
21
+ // with a partial meaning, an insert cut at a UTF-8 boundary to the bytes
22
+ // that fit.
23
+
24
+ /// A byte range over the text, start <= end after normalization.
25
+ export interface TextRange {
26
+ readonly start: number;
27
+ readonly end: number;
28
+ }
29
+
30
+ /// A selection as anchor/focus byte offsets (focus is the caret; anchor
31
+ /// stays put while shift-extending). anchor === focus is a bare caret.
32
+ export interface TextSelection {
33
+ readonly anchor: number;
34
+ readonly focus: number;
35
+ }
36
+
37
+ export type TextCaretDirection =
38
+ | "previous"
39
+ | "next"
40
+ | "previous_word"
41
+ | "next_word"
42
+ | "start"
43
+ | "end";
44
+
45
+ export interface TextCaretMove {
46
+ readonly direction: TextCaretDirection;
47
+ readonly extend: boolean;
48
+ }
49
+
50
+ /// The runtime text-control event vocabulary, mirrored structurally —
51
+ /// markup's `on-input` translates each control event into this union.
52
+ export type TextInputEvent =
53
+ | { readonly kind: "insert_text"; readonly text: Uint8Array }
54
+ | { readonly kind: "delete_backward" }
55
+ | { readonly kind: "delete_forward" }
56
+ | { readonly kind: "delete_word_backward" }
57
+ | { readonly kind: "delete_word_forward" }
58
+ | { readonly kind: "clear" }
59
+ | { readonly kind: "move_caret"; readonly move: TextCaretMove }
60
+ | { readonly kind: "set_selection"; readonly selection: TextSelection }
61
+ | { readonly kind: "set_composition"; readonly text: Uint8Array; readonly cursor: number | null }
62
+ | { readonly kind: "commit_composition" }
63
+ | { readonly kind: "cancel_composition" };
64
+
65
+ /// One editor state: the text bytes, the selection, and the active IME
66
+ /// composition range (null when none).
67
+ export interface TextEditState {
68
+ readonly text: Uint8Array;
69
+ readonly selection: TextSelection;
70
+ readonly composition: TextRange | null;
71
+ }
72
+
73
+ function rangeNormalized(r: TextRange, textLen: number): TextRange {
74
+ const start = Math.min(r.start, textLen);
75
+ const end = Math.min(r.end, textLen);
76
+ return start <= end ? { start: start, end: end } : { start: end, end: start };
77
+ }
78
+
79
+ function rangeByteLen(r: TextRange, textLen: number): number {
80
+ const n = rangeNormalized(r, textLen);
81
+ return n.end - n.start;
82
+ }
83
+
84
+ function rangeIsCollapsed(r: TextRange, textLen: number): boolean {
85
+ const n = rangeNormalized(r, textLen);
86
+ return n.start === n.end;
87
+ }
88
+
89
+ function selectionRange(s: TextSelection, textLen: number): TextRange {
90
+ return rangeNormalized({ start: s.anchor, end: s.focus }, textLen);
91
+ }
92
+
93
+ function isUtf8ContinuationByte(byte: number): boolean {
94
+ return (byte & 0xc0) === 0x80;
95
+ }
96
+
97
+ function utf8SequenceLength(lead: number): number {
98
+ if ((lead & 0x80) === 0) return 1;
99
+ if ((lead & 0xe0) === 0xc0) return 2;
100
+ if ((lead & 0xf0) === 0xe0) return 3;
101
+ if ((lead & 0xf8) === 0xf0) return 4;
102
+ return 1;
103
+ }
104
+
105
+ function snapTextOffset(text: Uint8Array, offset: number): number {
106
+ let cursor = Math.min(offset, text.length);
107
+ while (cursor > 0 && cursor < text.length && isUtf8ContinuationByte(text[cursor])) {
108
+ cursor -= 1;
109
+ }
110
+ return cursor;
111
+ }
112
+
113
+ function previousTextOffset(text: Uint8Array, offset: number): number {
114
+ let cursor = snapTextOffset(text, offset);
115
+ if (cursor === 0) return 0;
116
+ cursor -= 1;
117
+ while (cursor > 0 && isUtf8ContinuationByte(text[cursor])) {
118
+ cursor -= 1;
119
+ }
120
+ return cursor;
121
+ }
122
+
123
+ function nextTextOffset(text: Uint8Array, offset: number): number {
124
+ const cursor = snapTextOffset(text, offset);
125
+ if (cursor >= text.length) return text.length;
126
+ const next = Math.min(text.length, cursor + utf8SequenceLength(text[cursor]));
127
+ if (next <= offset) return Math.min(text.length, offset + 1);
128
+ return next;
129
+ }
130
+
131
+ function isAsciiAlphanumeric(b: number): boolean {
132
+ return (b >= 0x30 && b <= 0x39) || (b >= 0x41 && b <= 0x5a) || (b >= 0x61 && b <= 0x7a);
133
+ }
134
+
135
+ function isAsciiWhitespace(b: number): boolean {
136
+ return b === 0x20 || b === 0x09 || b === 0x0a || b === 0x0d || b === 0x0b || b === 0x0c;
137
+ }
138
+
139
+ type TextRunClass = 0 | 1 | 2; // word, space, other
140
+
141
+ function textRunClassAt(text: Uint8Array, offset: number): TextRunClass | null {
142
+ const cursor = snapTextOffset(text, offset);
143
+ if (cursor >= text.length) return null;
144
+ const lead = text[cursor];
145
+ if ((lead & 0x80) !== 0) return 0;
146
+ if (isAsciiAlphanumeric(lead) || lead === 0x5f) return 0;
147
+ if (isAsciiWhitespace(lead)) return 1;
148
+ return 2;
149
+ }
150
+
151
+ function textOffsetStartsWord(text: Uint8Array, offset: number): boolean {
152
+ const cls = textRunClassAt(text, offset);
153
+ return cls !== null && cls === 0;
154
+ }
155
+
156
+ function previousTextWordOffset(text: Uint8Array, offset: number): number {
157
+ let cursor = snapTextOffset(text, offset);
158
+ while (cursor > 0) {
159
+ const previous = previousTextOffset(text, cursor);
160
+ if (textOffsetStartsWord(text, previous)) break;
161
+ cursor = previous;
162
+ }
163
+ while (cursor > 0) {
164
+ const previous = previousTextOffset(text, cursor);
165
+ if (!textOffsetStartsWord(text, previous)) break;
166
+ cursor = previous;
167
+ }
168
+ return cursor;
169
+ }
170
+
171
+ function nextTextWordOffset(text: Uint8Array, offset: number): number {
172
+ let cursor = snapTextOffset(text, offset);
173
+ while (cursor < text.length && !textOffsetStartsWord(text, cursor)) {
174
+ cursor = nextTextOffset(text, cursor);
175
+ }
176
+ while (cursor < text.length && textOffsetStartsWord(text, cursor)) {
177
+ cursor = nextTextOffset(text, cursor);
178
+ }
179
+ return cursor;
180
+ }
181
+
182
+ function snapTextSelection(text: Uint8Array, selection: TextSelection): TextSelection {
183
+ return {
184
+ anchor: snapTextOffset(text, selection.anchor),
185
+ focus: snapTextOffset(text, selection.focus),
186
+ };
187
+ }
188
+
189
+ function snapTextRange(text: Uint8Array, range: TextRange): TextRange {
190
+ const normalized = rangeNormalized(range, text.length);
191
+ return rangeNormalized(
192
+ {
193
+ start: snapTextOffset(text, normalized.start),
194
+ end: snapTextOffset(text, normalized.end),
195
+ },
196
+ text.length,
197
+ );
198
+ }
199
+
200
+ interface TextReplaceResult {
201
+ readonly text: Uint8Array;
202
+ readonly insertedStart: number;
203
+ readonly insertedEnd: number;
204
+ }
205
+
206
+ // Null when the result would exceed `capacity` — the over-full outcome the
207
+ // caller clamps or drops, never a truncated write.
208
+ function replaceTextRange(
209
+ source: Uint8Array,
210
+ range: TextRange,
211
+ replacement: Uint8Array,
212
+ capacity: number,
213
+ ): TextReplaceResult | null {
214
+ const snapped = snapTextRange(source, range);
215
+ const prefixLen = snapped.start;
216
+ const suffixStart = prefixLen + replacement.length;
217
+ const nextLen = prefixLen + replacement.length + (source.length - snapped.end);
218
+ if (nextLen > capacity) return null;
219
+ const out = new Uint8Array(nextLen);
220
+ out.set(source.subarray(0, prefixLen), 0);
221
+ out.set(replacement, prefixLen);
222
+ out.set(source.subarray(snapped.end), suffixStart);
223
+ return { text: out, insertedStart: prefixLen, insertedEnd: suffixStart };
224
+ }
225
+
226
+ function normalizeTextEditState(state: TextEditState): TextEditState {
227
+ return {
228
+ text: state.text,
229
+ selection: snapTextSelection(state.text, state.selection),
230
+ composition: state.composition !== null ? snapTextRange(state.text, state.composition) : null,
231
+ };
232
+ }
233
+
234
+ function activeTextReplaceRange(state: TextEditState): TextRange {
235
+ if (state.composition !== null) return snapTextRange(state.text, state.composition);
236
+ return selectionRange(state.selection, state.text.length);
237
+ }
238
+
239
+ function replaceTextEditRange(
240
+ state: TextEditState,
241
+ range: TextRange,
242
+ replacement: Uint8Array,
243
+ capacity: number,
244
+ composition: TextRange | null,
245
+ cursorOffset: number,
246
+ ): TextEditState | null {
247
+ const result = replaceTextRange(state.text, range, replacement, capacity);
248
+ if (result === null) return null;
249
+ const cursor = snapTextOffset(
250
+ result.text,
251
+ result.insertedStart + Math.min(cursorOffset, replacement.length),
252
+ );
253
+ return {
254
+ text: result.text,
255
+ selection: { anchor: cursor, focus: cursor },
256
+ composition: composition,
257
+ };
258
+ }
259
+
260
+ function setTextComposition(
261
+ state: TextEditState,
262
+ text: Uint8Array,
263
+ cursorIn: number | null,
264
+ capacity: number,
265
+ ): TextEditState | null {
266
+ const range = activeTextReplaceRange(state);
267
+ const cursor = snapTextOffset(text, cursorIn === null ? text.length : cursorIn);
268
+ const result = replaceTextRange(state.text, range, text, capacity);
269
+ if (result === null) return null;
270
+ const absoluteCursor = snapTextOffset(result.text, result.insertedStart + cursor);
271
+ return {
272
+ text: result.text,
273
+ selection: { anchor: absoluteCursor, focus: absoluteCursor },
274
+ composition: { start: result.insertedStart, end: result.insertedEnd },
275
+ };
276
+ }
277
+
278
+ function cancelTextComposition(state: TextEditState, capacity: number): TextEditState | null {
279
+ if (state.composition === null) return state;
280
+ const range = snapTextRange(state.text, state.composition);
281
+ const result = replaceTextRange(state.text, range, new Uint8Array(0), capacity);
282
+ if (result === null) return null;
283
+ return {
284
+ text: result.text,
285
+ selection: { anchor: result.insertedStart, focus: result.insertedStart },
286
+ composition: null,
287
+ };
288
+ }
289
+
290
+ function deleteBackwardTextEdit(state: TextEditState, capacity: number): TextEditState | null {
291
+ const range = activeTextReplaceRange(state);
292
+ if (!rangeIsCollapsed(range, state.text.length)) {
293
+ return replaceTextEditRange(state, range, new Uint8Array(0), capacity, null, 0);
294
+ }
295
+ const caret = snapTextOffset(state.text, state.selection.focus);
296
+ if (caret === 0) {
297
+ return { text: state.text, selection: { anchor: 0, focus: 0 }, composition: null };
298
+ }
299
+ return replaceTextEditRange(
300
+ state,
301
+ { start: previousTextOffset(state.text, caret), end: caret },
302
+ new Uint8Array(0),
303
+ capacity,
304
+ null,
305
+ 0,
306
+ );
307
+ }
308
+
309
+ function deleteForwardTextEdit(state: TextEditState, capacity: number): TextEditState | null {
310
+ const range = activeTextReplaceRange(state);
311
+ if (!rangeIsCollapsed(range, state.text.length)) {
312
+ return replaceTextEditRange(state, range, new Uint8Array(0), capacity, null, 0);
313
+ }
314
+ const caret = snapTextOffset(state.text, state.selection.focus);
315
+ if (caret >= state.text.length) {
316
+ const len = state.text.length;
317
+ return { text: state.text, selection: { anchor: len, focus: len }, composition: null };
318
+ }
319
+ return replaceTextEditRange(
320
+ state,
321
+ { start: caret, end: nextTextOffset(state.text, caret) },
322
+ new Uint8Array(0),
323
+ capacity,
324
+ null,
325
+ 0,
326
+ );
327
+ }
328
+
329
+ function deleteWordBackwardTextEdit(state: TextEditState, capacity: number): TextEditState | null {
330
+ const range = activeTextReplaceRange(state);
331
+ if (!rangeIsCollapsed(range, state.text.length)) {
332
+ return replaceTextEditRange(state, range, new Uint8Array(0), capacity, null, 0);
333
+ }
334
+ const caret = snapTextOffset(state.text, state.selection.focus);
335
+ if (caret === 0) {
336
+ return { text: state.text, selection: { anchor: 0, focus: 0 }, composition: null };
337
+ }
338
+ return replaceTextEditRange(
339
+ state,
340
+ { start: previousTextWordOffset(state.text, caret), end: caret },
341
+ new Uint8Array(0),
342
+ capacity,
343
+ null,
344
+ 0,
345
+ );
346
+ }
347
+
348
+ function deleteWordForwardTextEdit(state: TextEditState, capacity: number): TextEditState | null {
349
+ const range = activeTextReplaceRange(state);
350
+ if (!rangeIsCollapsed(range, state.text.length)) {
351
+ return replaceTextEditRange(state, range, new Uint8Array(0), capacity, null, 0);
352
+ }
353
+ const caret = snapTextOffset(state.text, state.selection.focus);
354
+ if (caret >= state.text.length) {
355
+ const len = state.text.length;
356
+ return { text: state.text, selection: { anchor: len, focus: len }, composition: null };
357
+ }
358
+ return replaceTextEditRange(
359
+ state,
360
+ { start: caret, end: nextTextWordOffset(state.text, caret) },
361
+ new Uint8Array(0),
362
+ capacity,
363
+ null,
364
+ 0,
365
+ );
366
+ }
367
+
368
+ function moveTextCaret(state: TextEditState, move: TextCaretMove): TextEditState {
369
+ const range = selectionRange(state.selection, state.text.length);
370
+ const focus = snapTextOffset(state.text, state.selection.focus);
371
+ const collapsed = rangeIsCollapsed(range, state.text.length);
372
+ let target: number;
373
+ if (move.direction === "previous") {
374
+ target = !move.extend && !collapsed ? range.start : previousTextOffset(state.text, focus);
375
+ } else if (move.direction === "next") {
376
+ target = !move.extend && !collapsed ? range.end : nextTextOffset(state.text, focus);
377
+ } else if (move.direction === "previous_word") {
378
+ target = !move.extend && !collapsed ? range.start : previousTextWordOffset(state.text, focus);
379
+ } else if (move.direction === "next_word") {
380
+ target = !move.extend && !collapsed ? range.end : nextTextWordOffset(state.text, focus);
381
+ } else if (move.direction === "start") {
382
+ target = 0;
383
+ } else {
384
+ target = state.text.length;
385
+ }
386
+ const selection: TextSelection = move.extend
387
+ ? { anchor: state.selection.anchor, focus: target }
388
+ : { anchor: target, focus: target };
389
+ return {
390
+ text: state.text,
391
+ selection: snapTextSelection(state.text, selection),
392
+ composition: null,
393
+ };
394
+ }
395
+
396
+ /// Reduce one runtime text-input event over an editor state within a fixed
397
+ /// byte capacity. Null means the edit would not fit (the refuse-whole
398
+ /// contract) — recover inserts with `clampedInsertEvent`, drop the rest.
399
+ export function applyTextInputEvent(
400
+ state: TextEditState,
401
+ event: TextInputEvent,
402
+ capacity: number,
403
+ ): TextEditState | null {
404
+ const normalized = normalizeTextEditState(state);
405
+ switch (event.kind) {
406
+ case "insert_text":
407
+ return replaceTextEditRange(
408
+ normalized,
409
+ activeTextReplaceRange(normalized),
410
+ event.text,
411
+ capacity,
412
+ null,
413
+ event.text.length,
414
+ );
415
+ case "delete_backward":
416
+ return deleteBackwardTextEdit(normalized, capacity);
417
+ case "delete_forward":
418
+ return deleteForwardTextEdit(normalized, capacity);
419
+ case "delete_word_backward":
420
+ return deleteWordBackwardTextEdit(normalized, capacity);
421
+ case "delete_word_forward":
422
+ return deleteWordForwardTextEdit(normalized, capacity);
423
+ case "clear":
424
+ return { text: new Uint8Array(0), selection: { anchor: 0, focus: 0 }, composition: null };
425
+ case "move_caret":
426
+ return moveTextCaret(normalized, event.move);
427
+ case "set_selection":
428
+ return {
429
+ text: normalized.text,
430
+ selection: snapTextSelection(normalized.text, event.selection),
431
+ composition: null,
432
+ };
433
+ case "set_composition":
434
+ return setTextComposition(normalized, event.text, event.cursor, capacity);
435
+ case "commit_composition":
436
+ return {
437
+ text: normalized.text,
438
+ selection: normalized.selection,
439
+ composition: null,
440
+ };
441
+ case "cancel_composition":
442
+ return cancelTextComposition(normalized, capacity);
443
+ }
444
+ }
445
+
446
+ /// For an over-capacity insert, the same event with its payload clamped (at
447
+ /// a UTF-8 boundary) to the bytes that fit; null when nothing fits — the
448
+ /// runtime TextBuffer's clamp-insert / refuse-everything-else contract.
449
+ export function clampedInsertEvent(
450
+ state: TextEditState,
451
+ event: TextInputEvent,
452
+ capacity: number,
453
+ ): TextInputEvent | null {
454
+ if (event.kind !== "insert_text") return null;
455
+ const insertion = event.text;
456
+ const normalized = normalizeTextEditState(state);
457
+ const replaced = rangeByteLen(activeTextReplaceRange(normalized), normalized.text.length);
458
+ const kept = normalized.text.length - replaced;
459
+ if (kept >= capacity) return null;
460
+ const available = capacity - kept;
461
+ if (available >= insertion.length) return null;
462
+ const clampedLen = snapTextOffset(insertion, available);
463
+ if (clampedLen === 0) return null;
464
+ return { kind: "insert_text", text: insertion.subarray(0, clampedLen) };
465
+ }
466
+
467
+ // --------------------------------------------------------- byte text utils
468
+ // The text comparisons every text-bearing app grows: ASCII-only case rules
469
+ // (byte-exact, locale-free — identical under node and native by
470
+ // construction; non-ASCII bytes compare verbatim).
471
+
472
+ function lowerAsciiByte(b: number): number {
473
+ return b >= 0x41 && b <= 0x5a ? b + 32 : b;
474
+ }
475
+
476
+ /// Whether `haystack` contains `needle`, ASCII case-insensitively. An empty
477
+ /// needle matches everything (the search-filter convention).
478
+ export function containsIgnoreCase(haystack: Uint8Array, needle: Uint8Array): boolean {
479
+ if (needle.length === 0) return true;
480
+ if (needle.length > haystack.length) return false;
481
+ for (let start = 0; start + needle.length <= haystack.length; start++) {
482
+ let hit = true;
483
+ for (let i = 0; i < needle.length; i++) {
484
+ if (lowerAsciiByte(haystack[start + i]) !== lowerAsciiByte(needle[i])) {
485
+ hit = false;
486
+ break;
487
+ }
488
+ }
489
+ if (hit) return true;
490
+ }
491
+ return false;
492
+ }
493
+
494
+ /// ASCII case-insensitive lexicographic order as a sign (-1/0/1) — a
495
+ /// ready-made `.toSorted` comparator for name columns.
496
+ export function orderIgnoreCase(a: Uint8Array, b: Uint8Array): number {
497
+ const shorter = Math.min(a.length, b.length);
498
+ for (let i = 0; i < shorter; i++) {
499
+ const av = lowerAsciiByte(a[i]);
500
+ const bv = lowerAsciiByte(b[i]);
501
+ if (av !== bv) return av < bv ? -1 : 1;
502
+ }
503
+ if (a.length === b.length) return 0;
504
+ return a.length < b.length ? -1 : 1;
505
+ }
506
+
507
+ /// The view of `text` with ASCII spaces, tabs, and carriage returns trimmed
508
+ /// from both ends (a subarray — no copy). DISTINCT from the built-in
509
+ /// `.trim()` on bytes: `.trim()` strips the full JS whitespace set (LF and
510
+ /// the Unicode spaces included) and is the everyday form; this helper stays
511
+ /// for line-oriented parsing where the newline is the record separator and
512
+ /// must survive the trim (system-monitor's ps parser is the canonical use).
513
+ export function trimAsciiSpaces(text: Uint8Array): Uint8Array {
514
+ let start = 0;
515
+ let end = text.length;
516
+ while (start < end && (text[start] === 0x20 || text[start] === 0x09 || text[start] === 0x0d)) start += 1;
517
+ while (end > start && (text[end - 1] === 0x20 || text[end - 1] === 0x09 || text[end - 1] === 0x0d)) end -= 1;
518
+ return text.subarray(start, end);
519
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # @native-sdk/core
2
-
3
- Coming soon.
package/index.js DELETED
@@ -1,2 +0,0 @@
1
- // Coming soon
2
- module.exports = {};