ranuts 0.3.0-alpha.1 → 0.4.0-alpha.2

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.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Paginate plain text into fixed-size boxes — the layout a reader, a teleprompter or a
3
+ * printable preview needs, where the same text must be cut into pages of a known size.
4
+ *
5
+ * The whole thing is **pure arithmetic**: it takes the box dimensions and the type metrics as
6
+ * numbers and never touches the DOM. That is what lets it run inside a Worker, on the server,
7
+ * or in a test — measure the container once on the main thread, then paginate anywhere.
8
+ *
9
+ * It assumes a **monospaced grid**: every character advances either one cell (CJK, full-width)
10
+ * or `narrowRatio` of one (ASCII). That is exactly true for a monospaced font and close enough
11
+ * for the CJK-dominant body text this was built for; it is *not* a substitute for real shaping
12
+ * on proportional Latin text.
13
+ */
14
+ /** The box each page must fit into, in px. */
15
+ export interface TextBox {
16
+ width: number;
17
+ height: number;
18
+ }
19
+ export interface TextGridMetrics {
20
+ /** Advance width of one full-width character, px (font size + letter spacing). */
21
+ charWidth: number;
22
+ /** Distance between baselines, px. */
23
+ lineHeight: number;
24
+ /**
25
+ * Advance of a narrow (ASCII) character as a fraction of `charWidth`. Default `0.5625`
26
+ * (9/16), the usual half-width-plus-spacing ratio.
27
+ */
28
+ narrowRatio?: number;
29
+ }
30
+ export interface PaginateOptions {
31
+ /**
32
+ * Below this, in either dimension, the box is treated as not yet laid out and pagination
33
+ * returns nothing. Default `30`. Guards against paginating during first paint, when a
34
+ * container still measures 0 and a naive `floor(0 / charWidth)` would loop forever.
35
+ */
36
+ minBox?: number;
37
+ }
38
+ export interface TextPage {
39
+ text: string;
40
+ /** Offset of this page's first character in the source text. */
41
+ start: number;
42
+ /** Offset one past this page's last character. */
43
+ end: number;
44
+ index: number;
45
+ }
46
+ export interface PaginateResult {
47
+ pages: TextPage[];
48
+ /** Length of the normalized source text. */
49
+ total: number;
50
+ charsPerLine: number;
51
+ linesPerPage: number;
52
+ /** `charsPerLine * linesPerPage` — the budget one page spends. */
53
+ charsPerPage: number;
54
+ }
55
+ /**
56
+ * @description: Cut text into pages that fit `box`, given the type metrics.
57
+ *
58
+ * ASCII words are kept whole: a page never ends mid-word unless the word is longer than a
59
+ * line. Newlines start a new line and are charged one character.
60
+ *
61
+ * @param {string} text source text; `\r\n` / `\r` are normalized to `\n` first
62
+ * @param {TextBox} box page box in px — measure the container once, then call this anywhere
63
+ * @param {TextGridMetrics} metrics character advance and line height, px
64
+ * @param {PaginateOptions} options
65
+ * @return {PaginateResult} pages plus the grid it derived
66
+ * @example
67
+ * ```ts
68
+ * const { width, height } = container.getBoundingClientRect();
69
+ * const result = paginateText(book, { width, height }, { charWidth: 18.4, lineHeight: 40 });
70
+ * render(result.pages[0].text);
71
+ * ```
72
+ */
73
+ export declare const paginateText: (text: string, box: TextBox, metrics: TextGridMetrics, options?: PaginateOptions) => PaginateResult;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Speech recognition (dictation) over the Web Speech API.
3
+ *
4
+ * This is the counterpart to `AudioRecorder`, which captures audio *bytes*; this one asks the
5
+ * platform to turn speech into *text*. The browser API is still prefixed on WebKit, is absent
6
+ * from `lib.dom`, and reports several routine non-events (a silent pause, a programmatic stop)
7
+ * through the same error channel as a denied microphone — so it is worth wrapping once.
8
+ */
9
+ /**
10
+ * `denied` means the user or the browser refused the microphone — worth surfacing.
11
+ * `noSpeech` and `aborted` are routine (a silent pause, a programmatic `stop()`) and are
12
+ * usually not worth showing anyone; they are reported rather than swallowed so the caller
13
+ * decides. Everything else is `failed`.
14
+ */
15
+ export type SpeechErrorKind = 'denied' | 'noSpeech' | 'aborted' | 'failed';
16
+ export interface SpeechError {
17
+ kind: SpeechErrorKind;
18
+ /** The raw `error` string from the platform event. */
19
+ detail: string;
20
+ }
21
+ export interface SpeechRecognizerOptions {
22
+ /**
23
+ * BCP 47 tag for the language being spoken, e.g. `'en-US'`, `'zh-CN'`. Pass a function to
24
+ * have it read at the start of each capture, which is what an app whose UI language can
25
+ * change mid-session wants.
26
+ */
27
+ lang?: string | (() => string);
28
+ /** Keep listening across pauses instead of stopping at the first one. Default `true`. */
29
+ continuous?: boolean;
30
+ /** Emit partial results as the speaker talks. Default `true`. */
31
+ interimResults?: boolean;
32
+ /**
33
+ * Called as text arrives, with the transcript of the **whole capture so far** — not just
34
+ * the newest fragment — because interim results are revised as recognition firms up.
35
+ */
36
+ onResult?: (transcript: string, isFinal: boolean) => void;
37
+ onError?: (error: SpeechError) => void;
38
+ onStart?: () => void;
39
+ /** Fires once per capture, however it ended: stopped, timed out, or errored. */
40
+ onEnd?: () => void;
41
+ }
42
+ export interface SpeechRecognizer {
43
+ /** `false` when the platform has no speech recognition; every method is then a no-op. */
44
+ readonly supported: boolean;
45
+ /** Whether a capture is currently running. */
46
+ readonly active: boolean;
47
+ /** Begin a capture. Ignored if one is already running. */
48
+ start(): void;
49
+ /** End the current capture; results already recognized are kept. `onEnd` follows. */
50
+ stop(): void;
51
+ /** End the current capture and discard pending results. */
52
+ abort(): void;
53
+ /** Start if idle, stop if running — the behaviour a single microphone button wants. */
54
+ toggle(): void;
55
+ }
56
+ /**
57
+ * @description: Whether this runtime can recognize speech. Checked at call time, so it is safe
58
+ * to import this module during server-side rendering.
59
+ * @return {boolean}
60
+ */
61
+ export declare const isSpeechRecognitionSupported: () => boolean;
62
+ /**
63
+ * @description: Create a dictation session over the Web Speech API.
64
+ *
65
+ * A recognizer is reusable: `start` builds a fresh native instance each time, so options read
66
+ * through a function (notably `lang`) are re-read per capture.
67
+ *
68
+ * On a platform without speech recognition this returns an inert recognizer with
69
+ * `supported === false` rather than throwing — check that field to hide your microphone button.
70
+ *
71
+ * @param {SpeechRecognizerOptions} options
72
+ * @return {SpeechRecognizer}
73
+ * @example
74
+ * ```ts
75
+ * const mic = createSpeechRecognizer({
76
+ * lang: () => currentLocale(),
77
+ * onResult: (text) => { input.value = text; },
78
+ * onError: (e) => { if (e.kind === 'denied') toast('Microphone access was refused'); },
79
+ * onEnd: () => button.classList.remove('recording'),
80
+ * onStart: () => button.classList.add('recording'),
81
+ * });
82
+ * if (!mic.supported) button.style.display = 'none';
83
+ * button.addEventListener('click', () => mic.toggle());
84
+ * ```
85
+ */
86
+ export declare const createSpeechRecognizer: (options?: SpeechRecognizerOptions) => SpeechRecognizer;
@@ -64,6 +64,41 @@ export declare const toHalfWidth: (value: string) => string;
64
64
  * @return {string}
65
65
  */
66
66
  export declare const toFullWidth: (value: string) => string;
67
+ /** Which end of the string gets dropped when it is too long. */
68
+ export type TruncatePosition = 'end' | 'start' | 'middle';
69
+ export interface TruncateOptions {
70
+ /** Ceiling for the returned string, ellipsis included. */
71
+ length: number;
72
+ /** Default `'end'`. */
73
+ position?: TruncatePosition;
74
+ /** Default `'…'`. */
75
+ ellipsis?: string;
76
+ }
77
+ /**
78
+ * @description: Shorten a string to a maximum length, marking the cut with an ellipsis.
79
+ *
80
+ * `position` decides which end survives, and that choice carries real information:
81
+ *
82
+ * - `'end'` (default) keeps the beginning — right for prose and titles.
83
+ * - `'start'` keeps the **tail**, which is what a file path wants. `/Users/someone/work/…`
84
+ * are the bytes a reader already knows; `…/src/utils/str.ts` is the part they need.
85
+ * - `'middle'` keeps both ends, for identifiers whose head *and* tail are meaningful, such as
86
+ * a hash or an account number.
87
+ *
88
+ * The result never exceeds `length`, so a `length` shorter than the ellipsis returns a
89
+ * truncated ellipsis rather than overflowing.
90
+ *
91
+ * @param {string} value
92
+ * @param {TruncateOptions | number} options a bare number is shorthand for `{ length }`
93
+ * @return {string}
94
+ * @example
95
+ * ```ts
96
+ * truncate('the quick brown fox', 12); // 'the quick b…'
97
+ * truncate('/Users/me/code/app/src/index.ts', { length: 20, position: 'start' }); // '…de/app/src/index.ts'
98
+ * truncate('0xabcdef0123456789', { length: 11, position: 'middle' }); // '0xabc…56789'
99
+ * ```
100
+ */
101
+ export declare const truncate: (value: string, options: TruncateOptions | number) => string;
67
102
  export interface TransformText {
68
103
  encoding: string;
69
104
  content: string;
@@ -1,3 +1,5 @@
1
+ /** Accepted everywhere a moment in time is taken; `undefined` means "now". */
2
+ export type DateInput = number | string | Date;
1
3
  /**
2
4
  * @description: Format a date with a token pattern. Accepts a timestamp, a date string, a
3
5
  * `Date`, or nothing (meaning now). Wrap literal text in `[]` to keep it out of the substitution.
@@ -23,10 +25,10 @@
23
25
  * ```ts
24
26
  * formatDate(); // '2026-07-25 14:30:00'
25
27
  * formatDate(1753425000000, 'YYYY/MM/DD'); // '2026/07/25'
26
- * formatDate(new Date(), 'YYYY[年]MM[月]DD[日] hh:mm a'); // '2026年07月25日 02:30 pm' (escaped literals)
28
+ * formatDate(new Date(), 'YYYY[年]MM[月]DD[日] hh:mm a'); // '2026 年 07 月 25 日 02:30 pm' (escaped literals)
27
29
  * ```
28
30
  */
29
- export declare const formatDate: (value?: number | string | Date, pattern?: string) => string;
31
+ export declare const formatDate: (value?: DateInput, pattern?: string) => string;
30
32
  /**
31
33
  * @description: Turn a timestamp into a `Date` carrying a `format` method.
32
34
  * @deprecated Use [`formatDate`](#formatdate). This returns a `Date` with an extra method
@@ -39,12 +41,80 @@ export declare const formatDate: (value?: number | string | Date, pattern?: stri
39
41
  export declare function timestampToTime(timestamp?: number | string): Date & {
40
42
  format?: Function;
41
43
  };
44
+ /**
45
+ * @description: Format an elapsed number of **seconds** as a colon-separated clock duration,
46
+ * the shape media players use for a playhead: `mm:ss`, widening to `hh:mm:ss` past an hour.
47
+ *
48
+ * This is a length of time, not a point in time — for the latter see
49
+ * [`formatDate`](#formatdate) (absolute) and [`formatRelative`](#formatrelative) (relative).
50
+ *
51
+ * @param {number} seconds elapsed seconds; negatives clamp to `0`
52
+ * @return {string} the duration, or `''` when the input is not a finite number — a player
53
+ * asks for `video.duration` before metadata loads and gets `NaN`, and an empty label reads
54
+ * better there than `NaN:NaN`
55
+ * @example
56
+ * ```ts
57
+ * formatDuration(0); // '00:00'
58
+ * formatDuration(65); // '01:05'
59
+ * formatDuration(3661); // '01:01:01'
60
+ * formatDuration(NaN); // ''
61
+ * ```
62
+ */
63
+ export declare const formatDuration: (seconds: number) => string;
42
64
  /**
43
65
  * @description: Format a number of seconds as a colon-separated duration
66
+ * @deprecated Renamed to [`formatDuration`](#formatduration) — `timeFormat` said nothing about
67
+ * *which* of the three time formats it was, and read backwards from its `formatDate` sibling.
68
+ * This alias stays for compatibility and behaves identically.
44
69
  * @param {number} time
45
- * @return {*}
70
+ * @return {string}
46
71
  */
47
72
  export declare const timeFormat: (time: number) => string;
73
+ /** `'compact'` is ours; the other three are `Intl.RelativeTimeFormat` styles. */
74
+ export type RelativeStyle = 'long' | 'short' | 'narrow' | 'compact';
75
+ export interface FormatRelativeOptions {
76
+ /** What to measure against. Defaults to the current time. */
77
+ now?: DateInput;
78
+ /** BCP 47 tag(s); defaults to the runtime's locale. Ignored by the `compact` style. */
79
+ locale?: string | string[];
80
+ /** Defaults to `'long'` (`'3 days ago'`). */
81
+ style?: RelativeStyle;
82
+ /**
83
+ * `'auto'` (the default here) swaps in idioms where a language has them — `yesterday`
84
+ * rather than `1 day ago`, `now` rather than `in 0 seconds`. `'always'` keeps the number.
85
+ */
86
+ numeric?: 'always' | 'auto';
87
+ }
88
+ /**
89
+ * @description: Format a point in time relative to another — "3 days ago", "in 2 hours".
90
+ *
91
+ * Localization is delegated to the platform's `Intl.RelativeTimeFormat`, which has been
92
+ * available in every major browser since 2020 and knows each language's plural and
93
+ * inflection rules. This function supplies only the part `Intl` deliberately leaves out:
94
+ * picking *which* unit to express the gap in.
95
+ *
96
+ * Like `Intl` itself it reports a **single** unit — a gap of 3 days and 6 hours is
97
+ * "3 days ago", never "3 days and 6 hours ago".
98
+ *
99
+ * The extra `compact` style is the dense badge form seen next to list items (`5m`, `3h`,
100
+ * `2d`). It is a magnitude only: it carries **no direction**, so a future timestamp reads
101
+ * the same as a past one. Use it for feeds of past events, and any other style when the
102
+ * reader has to tell past from future.
103
+ *
104
+ * @param {DateInput} value the moment to describe
105
+ * @param {FormatRelativeOptions} options
106
+ * @return {string} the description, or `''` when either end cannot be parsed
107
+ * @example
108
+ * ```ts
109
+ * const twoHoursAgo = Date.now() - 2 * 3600_000;
110
+ * formatRelative(twoHoursAgo); // '2 hours ago'
111
+ * formatRelative(twoHoursAgo, { style: 'short' }); // '2 hr. ago'
112
+ * formatRelative(twoHoursAgo, { style: 'compact' }); // '2h'
113
+ * formatRelative(twoHoursAgo, { locale: 'zh-CN' }); // '2 小时前'
114
+ * formatRelative(Date.now() + 60_000); // 'in 1 minute'
115
+ * ```
116
+ */
117
+ export declare const formatRelative: (value: DateInput, options?: FormatRelativeOptions) => string;
48
118
  /**
49
119
  * @description: Current timestamp
50
120
  * @return {*}
@@ -74,3 +74,52 @@ export declare class WorkerClient<Req extends object, Res extends WorkerResponse
74
74
  private handleMessage;
75
75
  private rejectAll;
76
76
  }
77
+ /** A request always carries the id the client stamped on it */
78
+ export interface WorkerRequestBase {
79
+ operationId: number;
80
+ type?: string;
81
+ }
82
+ /** Handed to the handler so it can stream progress for the request it is currently serving */
83
+ export interface WorkerHandlerContext<Progress> {
84
+ /** Post a progress update. Does not settle the request; may be called any number of times. */
85
+ progress: (payload: Progress) => void;
86
+ }
87
+ export interface ServeWorkerOptions {
88
+ /**
89
+ * Where to listen. Defaults to `self` — override for a `MessagePort`, a nested worker, or
90
+ * a test double.
91
+ */
92
+ scope?: {
93
+ addEventListener: (type: 'message', listener: (event: MessageEvent) => void) => void;
94
+ removeEventListener: (type: 'message', listener: (event: MessageEvent) => void) => void;
95
+ postMessage: (message: unknown, transfer?: Transferable[]) => void;
96
+ };
97
+ /** Response `type` for a successful reply that the handler returned bare (non-object). Default `'result'`. */
98
+ resultType?: string;
99
+ }
100
+ /**
101
+ * @description: Serve requests inside a Web Worker, mirroring {@link WorkerClient} on the
102
+ * other end. Reads `operationId` off each request, awaits your handler, and posts the reply
103
+ * back with that same id.
104
+ *
105
+ * Failures become `{ operationId, type: 'error', message }` — the envelope `WorkerClient`
106
+ * rejects on by default. **Both synchronous throws and rejected promises are caught**: a sync
107
+ * throw inside `onmessage` would otherwise escape to the worker's error handler, which carries
108
+ * no id, so the client could only fail *every* in-flight request instead of the one that broke.
109
+ *
110
+ * @param {Function} handler receives the request and a context whose `progress()` streams updates
111
+ * @param {ServeWorkerOptions} options
112
+ * @return {Function} stop — removes the listener
113
+ * @example
114
+ * ```ts
115
+ * // my.worker.ts
116
+ * serveWorker<MyRequest, MyResponse, ModelProgress>(async (request, { progress }) => {
117
+ * if (request.type === 'load') {
118
+ * const device = await load(request.modelId, (p) => progress(p));
119
+ * return { type: 'loaded', device };
120
+ * }
121
+ * return { type: 'result', scores: await classify(request.lines) };
122
+ * });
123
+ * ```
124
+ */
125
+ export declare const serveWorker: <Req extends WorkerRequestBase, Res extends object = object, Progress = unknown>(handler: (request: Req, context: WorkerHandlerContext<Progress>) => Promise<Res> | Res, options?: ServeWorkerOptions) => (() => void);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};