@lp-labs/subtitle-timing 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +155 -2
- package/dist/cjs/index.d.ts +57 -0
- package/dist/cjs/index.js +229 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/index.d.ts +57 -0
- package/dist/esm/index.js +218 -0
- package/package.json +61 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Libin Prasanth
|
|
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
CHANGED
|
@@ -1,3 +1,156 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lp-labs/subtitle-timing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shifts, scales, splits, clips and de-overlaps subtitle cues expressed as integer millisecond intervals. It does not parse or write SRT/VTT. Catalog identity 489; standalone package version 1.0.0.
|
|
4
|
+
No runtime, peer, or sibling dependencies. Node >=18, ESM and CommonJS with declarations.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install @lp-labs/subtitle-timing
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quickstart (runnable JavaScript)
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import {
|
|
17
|
+
mergeIntervals,
|
|
18
|
+
resolveOverlaps,
|
|
19
|
+
scaleCues,
|
|
20
|
+
shiftCues,
|
|
21
|
+
splitCue,
|
|
22
|
+
} from '@lp-labs/subtitle-timing';
|
|
23
|
+
const cues = [
|
|
24
|
+
{ id: 'a', startMs: 1000, endMs: 3000, text: 'Hello' },
|
|
25
|
+
{ id: 'b', startMs: 2500, endMs: 4000, text: 'World' },
|
|
26
|
+
];
|
|
27
|
+
assert.deepEqual(
|
|
28
|
+
shiftCues(cues, -1000, { negative: 'error' }).map((c) => c.startMs),
|
|
29
|
+
[0, 1500],
|
|
30
|
+
);
|
|
31
|
+
assert.deepEqual(
|
|
32
|
+
scaleCues(cues, 2).map((c) => c.endMs),
|
|
33
|
+
[6000, 8000],
|
|
34
|
+
);
|
|
35
|
+
assert.deepEqual(
|
|
36
|
+
resolveOverlaps(cues, 'trim-previous').map((c) => [c.startMs, c.endMs]),
|
|
37
|
+
[
|
|
38
|
+
[1000, 2500],
|
|
39
|
+
[2500, 4000],
|
|
40
|
+
],
|
|
41
|
+
);
|
|
42
|
+
assert.deepEqual(
|
|
43
|
+
splitCue(cues[0], [2000]).map((c) => c.id),
|
|
44
|
+
['a#1', 'a#2'],
|
|
45
|
+
);
|
|
46
|
+
assert.deepEqual(mergeIntervals(cues), [{ startMs: 1000, endMs: 4000 }]);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Configuration and errors
|
|
50
|
+
|
|
51
|
+
A `Cue` is `{ id?: string, startMs, endMs, text }` with integer milliseconds, `startMs >= 0` and `endMs > startMs` (half-open intervals). All functions return new cue objects and never mutate input.
|
|
52
|
+
|
|
53
|
+
- `shiftCues(cues, deltaMs, { negative })` and `scaleCues(cues, factor, { anchorMs, rounding, negative })` (time `t` becomes `anchor + (t - anchor) * factor`, rounded with `round` (default), `floor` or `ceil`). Negative-time policy for a cue that would start before 0: `error` (default, RangeError), `clip` (start moves to 0; cues that end at or before 0 are removed) or `drop` (any such cue is removed). A scaled cue that collapses to zero length is a RangeError.
|
|
54
|
+
- `splitCue(cue, atMs, texts?)`: strictly increasing interior times; pieces keep the text unless one string per piece is given; ids become `id#1`, `id#2`...
|
|
55
|
+
- `clipCues(cues, windowStartMs, windowEndMs, { rebase })`: intersect with a window, optionally subtracting the window start.
|
|
56
|
+
- `sortCues`, `findOverlaps(cues, { maxPairs })` (positive-duration overlaps only, indices into the input, touching cues do not overlap), `mergeIntervals` (union; touching intervals merge) and `resolveOverlaps(cues, 'trim-previous' | 'push-next')`.
|
|
57
|
+
- `maxCues` (default 100000) and `maxPairs` (default 100000) are budgets.
|
|
58
|
+
|
|
59
|
+
Errors: `TypeError` for wrong container types, missing text/non-string ids or unknown policies; `RangeError` for non-integer or out-of-range times, results beyond safe integers, collapsed or invalid ranges and exceeded budgets.
|
|
60
|
+
|
|
61
|
+
## Complete public declarations
|
|
62
|
+
|
|
63
|
+
Every runtime export and public type is shown below. This is the complete emitted declaration surface.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
export interface Cue {
|
|
67
|
+
id?: string;
|
|
68
|
+
startMs: number;
|
|
69
|
+
endMs: number;
|
|
70
|
+
text: string;
|
|
71
|
+
}
|
|
72
|
+
export type NegativePolicy = 'error' | 'clip' | 'drop';
|
|
73
|
+
export type TimeRounding = 'round' | 'floor' | 'ceil';
|
|
74
|
+
export type OverlapPolicy = 'trim-previous' | 'push-next';
|
|
75
|
+
export interface ShiftOptions {
|
|
76
|
+
negative?: NegativePolicy;
|
|
77
|
+
}
|
|
78
|
+
export interface ScaleOptions extends ShiftOptions {
|
|
79
|
+
anchorMs?: number;
|
|
80
|
+
rounding?: TimeRounding;
|
|
81
|
+
}
|
|
82
|
+
export interface Interval {
|
|
83
|
+
startMs: number;
|
|
84
|
+
endMs: number;
|
|
85
|
+
}
|
|
86
|
+
export interface Overlap extends Interval {
|
|
87
|
+
first: number;
|
|
88
|
+
second: number;
|
|
89
|
+
}
|
|
90
|
+
export interface LimitOptions {
|
|
91
|
+
maxCues?: number;
|
|
92
|
+
}
|
|
93
|
+
export interface OverlapSearchOptions extends LimitOptions {
|
|
94
|
+
maxPairs?: number;
|
|
95
|
+
}
|
|
96
|
+
export interface ClipOptions extends LimitOptions {
|
|
97
|
+
rebase?: boolean;
|
|
98
|
+
}
|
|
99
|
+
export declare const DEFAULT_MAX_CUES = 100000;
|
|
100
|
+
export declare const DEFAULT_MAX_PAIRS = 100000;
|
|
101
|
+
export declare function shiftCues(
|
|
102
|
+
cues: readonly Cue[],
|
|
103
|
+
deltaMs: number,
|
|
104
|
+
options?: ShiftOptions,
|
|
105
|
+
): Cue[];
|
|
106
|
+
export declare function scaleCues(
|
|
107
|
+
cues: readonly Cue[],
|
|
108
|
+
factor: number,
|
|
109
|
+
options?: ScaleOptions,
|
|
110
|
+
): Cue[];
|
|
111
|
+
export declare function splitCue(
|
|
112
|
+
cue: Cue,
|
|
113
|
+
atMs: readonly number[],
|
|
114
|
+
texts?: readonly string[],
|
|
115
|
+
): Cue[];
|
|
116
|
+
export declare function clipCues(
|
|
117
|
+
cues: readonly Cue[],
|
|
118
|
+
windowStartMs: number,
|
|
119
|
+
windowEndMs: number,
|
|
120
|
+
options?: ClipOptions,
|
|
121
|
+
): Cue[];
|
|
122
|
+
export declare function sortCues(cues: readonly Cue[]): Cue[];
|
|
123
|
+
export declare function findOverlaps(
|
|
124
|
+
cues: readonly Cue[],
|
|
125
|
+
options?: OverlapSearchOptions,
|
|
126
|
+
): Overlap[];
|
|
127
|
+
export declare function resolveOverlaps(cues: readonly Cue[], overlapPolicy: OverlapPolicy): Cue[];
|
|
128
|
+
export declare function mergeIntervals(cues: readonly Cue[], options?: LimitOptions): Interval[];
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Performance, boundaries and limitations
|
|
132
|
+
|
|
133
|
+
Only times are manipulated: there is no SRT/VTT/ASS parsing or serialisation, no text re-wrapping, reading-speed or frame-rate logic. Splitting never guesses where text should break. `resolveOverlaps` sorts by start, end then original order and compares each cue only with the most recently kept cue: `trim-previous` shortens that cue (removing it if nothing remains) and `push-next` delays the new cue to its end (removing it if nothing remains), so some text time can be lost by design. Rounding is applied independently to start and end. Overlap discovery is O(n log n + pairs); every other operation is O(n log n) at most. Times are limited to safe integers.
|
|
134
|
+
|
|
135
|
+
## Development
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
npm ci --offline
|
|
139
|
+
npm run build
|
|
140
|
+
npm run typecheck
|
|
141
|
+
npm test
|
|
142
|
+
npm run format:check
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The offline install requires the locked development dependencies already in the npm cache.
|
|
146
|
+
Build emits separate ESM/CJS and declarations; type checks include bare-package .mts/.cts consumers with negative cases.
|
|
147
|
+
Runtime tests import only the single-quoted standalone dist entry and also run against installed offline tarballs.
|
|
148
|
+
No build artifacts or development dependencies are required at runtime.
|
|
149
|
+
|
|
150
|
+
## Support
|
|
151
|
+
|
|
152
|
+
If this library is useful to you, consider supporting its maintenance with a [donation via PayPal](https://www.paypal.me/LibinPrasanth).
|
|
153
|
+
|
|
154
|
+
## Author
|
|
155
|
+
|
|
156
|
+
Created by [Libin Prasanth](https://www.linkedin.com/in/libinprasanth/). Connect on LinkedIn for updates and discussion.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface Cue {
|
|
2
|
+
id?: string;
|
|
3
|
+
/** Inclusive start in integer milliseconds (>= 0). */
|
|
4
|
+
startMs: number;
|
|
5
|
+
/** Exclusive end in integer milliseconds (> startMs). */
|
|
6
|
+
endMs: number;
|
|
7
|
+
text: string;
|
|
8
|
+
}
|
|
9
|
+
export type NegativePolicy = 'error' | 'clip' | 'drop';
|
|
10
|
+
export type TimeRounding = 'round' | 'floor' | 'ceil';
|
|
11
|
+
export type OverlapPolicy = 'trim-previous' | 'push-next';
|
|
12
|
+
export interface ShiftOptions {
|
|
13
|
+
/** What to do when a shifted cue starts before 0. Default 'error'. */
|
|
14
|
+
negative?: NegativePolicy;
|
|
15
|
+
}
|
|
16
|
+
export interface ScaleOptions extends ShiftOptions {
|
|
17
|
+
/** Time that stays fixed while scaling. Default 0. */
|
|
18
|
+
anchorMs?: number;
|
|
19
|
+
rounding?: TimeRounding;
|
|
20
|
+
}
|
|
21
|
+
export interface Interval {
|
|
22
|
+
startMs: number;
|
|
23
|
+
endMs: number;
|
|
24
|
+
}
|
|
25
|
+
export interface Overlap extends Interval {
|
|
26
|
+
/** Indices into the supplied array, first < second. */
|
|
27
|
+
first: number;
|
|
28
|
+
second: number;
|
|
29
|
+
}
|
|
30
|
+
export interface LimitOptions {
|
|
31
|
+
maxCues?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface OverlapSearchOptions extends LimitOptions {
|
|
34
|
+
maxPairs?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface ClipOptions extends LimitOptions {
|
|
37
|
+
/** Subtract windowStartMs from kept cues. Default false. */
|
|
38
|
+
rebase?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare const DEFAULT_MAX_CUES = 100000;
|
|
41
|
+
export declare const DEFAULT_MAX_PAIRS = 100000;
|
|
42
|
+
/** Adds deltaMs to every cue; the input array and cues are not modified. */
|
|
43
|
+
export declare function shiftCues(cues: readonly Cue[], deltaMs: number, options?: ShiftOptions): Cue[];
|
|
44
|
+
/** Maps t to anchor + (t - anchor) * factor, rounded; a cue that collapses to zero length is an error. */
|
|
45
|
+
export declare function scaleCues(cues: readonly Cue[], factor: number, options?: ScaleOptions): Cue[];
|
|
46
|
+
/** Splits at strictly increasing interior times. Pieces get ids `${id}#1`, `${id}#2`, ... when the cue has an id. */
|
|
47
|
+
export declare function splitCue(cue: Cue, atMs: readonly number[], texts?: readonly string[]): Cue[];
|
|
48
|
+
/** Intersects cues with [windowStartMs, windowEndMs); cues with no remaining duration are removed. */
|
|
49
|
+
export declare function clipCues(cues: readonly Cue[], windowStartMs: number, windowEndMs: number, options?: ClipOptions): Cue[];
|
|
50
|
+
/** Stable sort by start then end; returns new cue objects. */
|
|
51
|
+
export declare function sortCues(cues: readonly Cue[]): Cue[];
|
|
52
|
+
/** All pairs that share positive duration (touching cues do not overlap), ordered by (first, second). */
|
|
53
|
+
export declare function findOverlaps(cues: readonly Cue[], options?: OverlapSearchOptions): Overlap[];
|
|
54
|
+
/** Sorts by start and removes overlaps against the previously kept cue; empty results are dropped. */
|
|
55
|
+
export declare function resolveOverlaps(cues: readonly Cue[], overlapPolicy: OverlapPolicy): Cue[];
|
|
56
|
+
/** Union of cue time as sorted disjoint intervals; touching intervals are merged. */
|
|
57
|
+
export declare function mergeIntervals(cues: readonly Cue[], options?: LimitOptions): Interval[];
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_MAX_PAIRS = exports.DEFAULT_MAX_CUES = void 0;
|
|
4
|
+
exports.shiftCues = shiftCues;
|
|
5
|
+
exports.scaleCues = scaleCues;
|
|
6
|
+
exports.splitCue = splitCue;
|
|
7
|
+
exports.clipCues = clipCues;
|
|
8
|
+
exports.sortCues = sortCues;
|
|
9
|
+
exports.findOverlaps = findOverlaps;
|
|
10
|
+
exports.resolveOverlaps = resolveOverlaps;
|
|
11
|
+
exports.mergeIntervals = mergeIntervals;
|
|
12
|
+
exports.DEFAULT_MAX_CUES = 100_000;
|
|
13
|
+
exports.DEFAULT_MAX_PAIRS = 100_000;
|
|
14
|
+
function time(name, value, min) {
|
|
15
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < min) {
|
|
16
|
+
throw new RangeError(`${name} must be a safe integer >= ${min}`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function limit(name, value, fallback) {
|
|
21
|
+
return value === undefined ? fallback : time(name, value, 0);
|
|
22
|
+
}
|
|
23
|
+
function readCues(cues, maxCues) {
|
|
24
|
+
if (!Array.isArray(cues))
|
|
25
|
+
throw new TypeError('cues must be an array');
|
|
26
|
+
if (cues.length > maxCues)
|
|
27
|
+
throw new RangeError('cue count exceeds maxCues');
|
|
28
|
+
for (const [i, cue] of cues.entries()) {
|
|
29
|
+
if (typeof cue !== 'object' || cue === null)
|
|
30
|
+
throw new TypeError(`cues[${i}] must be an object`);
|
|
31
|
+
const c = cue;
|
|
32
|
+
const start = time(`cues[${i}].startMs`, c.startMs, 0);
|
|
33
|
+
const end = time(`cues[${i}].endMs`, c.endMs, 0);
|
|
34
|
+
if (end <= start)
|
|
35
|
+
throw new RangeError(`cues[${i}] must end after it starts`);
|
|
36
|
+
if (typeof c.text !== 'string')
|
|
37
|
+
throw new TypeError(`cues[${i}].text must be a string`);
|
|
38
|
+
if (c.id !== undefined && typeof c.id !== 'string')
|
|
39
|
+
throw new TypeError(`cues[${i}].id must be a string`);
|
|
40
|
+
}
|
|
41
|
+
return cues;
|
|
42
|
+
}
|
|
43
|
+
function make(cue, startMs, endMs, id) {
|
|
44
|
+
const resultId = id ?? cue.id;
|
|
45
|
+
return resultId === undefined
|
|
46
|
+
? { startMs, endMs, text: cue.text }
|
|
47
|
+
: { id: resultId, startMs, endMs, text: cue.text };
|
|
48
|
+
}
|
|
49
|
+
function policy(value) {
|
|
50
|
+
const chosen = value === undefined ? 'error' : value;
|
|
51
|
+
if (chosen !== 'error' && chosen !== 'clip' && chosen !== 'drop')
|
|
52
|
+
throw new TypeError('unknown negative policy');
|
|
53
|
+
return chosen;
|
|
54
|
+
}
|
|
55
|
+
function applyNegative(cue, start, end, negative) {
|
|
56
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end))
|
|
57
|
+
throw new RangeError('time is not a safe integer');
|
|
58
|
+
if (start >= 0)
|
|
59
|
+
return make(cue, start, end);
|
|
60
|
+
if (negative === 'error')
|
|
61
|
+
throw new RangeError('cue would start before 0');
|
|
62
|
+
if (negative === 'drop' || end <= 0)
|
|
63
|
+
return undefined;
|
|
64
|
+
return make(cue, 0, end);
|
|
65
|
+
}
|
|
66
|
+
/** Adds deltaMs to every cue; the input array and cues are not modified. */
|
|
67
|
+
function shiftCues(cues, deltaMs, options = {}) {
|
|
68
|
+
const input = readCues(cues, exports.DEFAULT_MAX_CUES);
|
|
69
|
+
if (typeof deltaMs !== 'number' || !Number.isSafeInteger(deltaMs))
|
|
70
|
+
throw new RangeError('deltaMs must be a safe integer');
|
|
71
|
+
const negative = policy(options.negative);
|
|
72
|
+
const result = [];
|
|
73
|
+
for (const cue of input) {
|
|
74
|
+
const shifted = applyNegative(cue, cue.startMs + deltaMs, cue.endMs + deltaMs, negative);
|
|
75
|
+
if (shifted)
|
|
76
|
+
result.push(shifted);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/** Maps t to anchor + (t - anchor) * factor, rounded; a cue that collapses to zero length is an error. */
|
|
81
|
+
function scaleCues(cues, factor, options = {}) {
|
|
82
|
+
const input = readCues(cues, exports.DEFAULT_MAX_CUES);
|
|
83
|
+
if (typeof factor !== 'number' || !Number.isFinite(factor) || factor <= 0) {
|
|
84
|
+
throw new RangeError('factor must be a positive finite number');
|
|
85
|
+
}
|
|
86
|
+
const anchor = options.anchorMs === undefined ? 0 : time('anchorMs', options.anchorMs, 0);
|
|
87
|
+
const rounding = options.rounding ?? 'round';
|
|
88
|
+
const round = rounding === 'round'
|
|
89
|
+
? Math.round
|
|
90
|
+
: rounding === 'floor'
|
|
91
|
+
? Math.floor
|
|
92
|
+
: rounding === 'ceil'
|
|
93
|
+
? Math.ceil
|
|
94
|
+
: undefined;
|
|
95
|
+
if (!round)
|
|
96
|
+
throw new TypeError('unknown rounding');
|
|
97
|
+
const negative = policy(options.negative);
|
|
98
|
+
const map = (t) => round(anchor + (t - anchor) * factor);
|
|
99
|
+
const result = [];
|
|
100
|
+
for (const cue of input) {
|
|
101
|
+
const start = map(cue.startMs);
|
|
102
|
+
const end = map(cue.endMs);
|
|
103
|
+
if (end <= start && Number.isSafeInteger(start) && Number.isSafeInteger(end)) {
|
|
104
|
+
throw new RangeError('scaled cue has no positive duration');
|
|
105
|
+
}
|
|
106
|
+
const scaled = applyNegative(cue, start, end, negative);
|
|
107
|
+
if (scaled)
|
|
108
|
+
result.push(scaled);
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
/** Splits at strictly increasing interior times. Pieces get ids `${id}#1`, `${id}#2`, ... when the cue has an id. */
|
|
113
|
+
function splitCue(cue, atMs, texts) {
|
|
114
|
+
const [checked] = readCues([cue], 1);
|
|
115
|
+
if (!Array.isArray(atMs) || atMs.length === 0)
|
|
116
|
+
throw new TypeError('atMs must be a non-empty array');
|
|
117
|
+
let previous = checked.startMs;
|
|
118
|
+
for (const [i, at] of atMs.entries()) {
|
|
119
|
+
time(`atMs[${i}]`, at, 0);
|
|
120
|
+
if (at <= previous || at >= checked.endMs)
|
|
121
|
+
throw new RangeError(`atMs[${i}] must be increasing and inside the cue`);
|
|
122
|
+
previous = at;
|
|
123
|
+
}
|
|
124
|
+
if (texts !== undefined) {
|
|
125
|
+
if (!Array.isArray(texts) ||
|
|
126
|
+
texts.length !== atMs.length + 1 ||
|
|
127
|
+
texts.some((t) => typeof t !== 'string')) {
|
|
128
|
+
throw new TypeError('texts must contain one string per piece');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const bounds = [checked.startMs, ...atMs, checked.endMs];
|
|
132
|
+
return bounds.slice(0, -1).map((start, i) => {
|
|
133
|
+
const piece = make(checked, start, bounds[i + 1], checked.id === undefined ? undefined : `${checked.id}#${i + 1}`);
|
|
134
|
+
return texts ? { ...piece, text: texts[i] } : piece;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/** Intersects cues with [windowStartMs, windowEndMs); cues with no remaining duration are removed. */
|
|
138
|
+
function clipCues(cues, windowStartMs, windowEndMs, options = {}) {
|
|
139
|
+
const input = readCues(cues, limit('maxCues', options.maxCues, exports.DEFAULT_MAX_CUES));
|
|
140
|
+
const from = time('windowStartMs', windowStartMs, 0);
|
|
141
|
+
const to = time('windowEndMs', windowEndMs, 0);
|
|
142
|
+
if (to <= from)
|
|
143
|
+
throw new RangeError('window must have positive duration');
|
|
144
|
+
const offset = options.rebase === true ? from : 0;
|
|
145
|
+
const result = [];
|
|
146
|
+
for (const cue of input) {
|
|
147
|
+
const start = Math.max(cue.startMs, from);
|
|
148
|
+
const end = Math.min(cue.endMs, to);
|
|
149
|
+
if (end > start)
|
|
150
|
+
result.push(make(cue, start - offset, end - offset));
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
/** Stable sort by start then end; returns new cue objects. */
|
|
155
|
+
function sortCues(cues) {
|
|
156
|
+
return readCues(cues, exports.DEFAULT_MAX_CUES)
|
|
157
|
+
.map((cue, index) => ({ cue, index }))
|
|
158
|
+
.sort((a, b) => a.cue.startMs - b.cue.startMs || a.cue.endMs - b.cue.endMs || a.index - b.index)
|
|
159
|
+
.map(({ cue }) => make(cue, cue.startMs, cue.endMs));
|
|
160
|
+
}
|
|
161
|
+
/** All pairs that share positive duration (touching cues do not overlap), ordered by (first, second). */
|
|
162
|
+
function findOverlaps(cues, options = {}) {
|
|
163
|
+
const input = readCues(cues, limit('maxCues', options.maxCues, exports.DEFAULT_MAX_CUES));
|
|
164
|
+
const maxPairs = limit('maxPairs', options.maxPairs, exports.DEFAULT_MAX_PAIRS);
|
|
165
|
+
const order = input
|
|
166
|
+
.map((_, i) => i)
|
|
167
|
+
.sort((a, b) => input[a].startMs - input[b].startMs || a - b);
|
|
168
|
+
const active = [];
|
|
169
|
+
const found = [];
|
|
170
|
+
for (const index of order) {
|
|
171
|
+
const cue = input[index];
|
|
172
|
+
for (let k = active.length - 1; k >= 0; k--) {
|
|
173
|
+
if (input[active[k]].endMs <= cue.startMs)
|
|
174
|
+
active.splice(k, 1);
|
|
175
|
+
}
|
|
176
|
+
for (const other of active) {
|
|
177
|
+
if (found.length >= maxPairs)
|
|
178
|
+
throw new RangeError('overlap pairs exceed maxPairs');
|
|
179
|
+
const o = input[other];
|
|
180
|
+
found.push({
|
|
181
|
+
first: Math.min(index, other),
|
|
182
|
+
second: Math.max(index, other),
|
|
183
|
+
startMs: Math.max(o.startMs, cue.startMs),
|
|
184
|
+
endMs: Math.min(o.endMs, cue.endMs),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
active.push(index);
|
|
188
|
+
}
|
|
189
|
+
return found.sort((a, b) => a.first - b.first || a.second - b.second);
|
|
190
|
+
}
|
|
191
|
+
/** Sorts by start and removes overlaps against the previously kept cue; empty results are dropped. */
|
|
192
|
+
function resolveOverlaps(cues, overlapPolicy) {
|
|
193
|
+
if (overlapPolicy !== 'trim-previous' && overlapPolicy !== 'push-next')
|
|
194
|
+
throw new TypeError('unknown overlap policy');
|
|
195
|
+
const kept = [];
|
|
196
|
+
for (const cue of sortCues(cues)) {
|
|
197
|
+
const previous = kept[kept.length - 1];
|
|
198
|
+
if (previous && cue.startMs < previous.endMs) {
|
|
199
|
+
if (overlapPolicy === 'trim-previous') {
|
|
200
|
+
previous.endMs = cue.startMs;
|
|
201
|
+
if (previous.endMs <= previous.startMs)
|
|
202
|
+
kept.pop();
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
if (previous.endMs >= cue.endMs)
|
|
206
|
+
continue;
|
|
207
|
+
cue.startMs = previous.endMs;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
kept.push(cue);
|
|
211
|
+
}
|
|
212
|
+
return kept;
|
|
213
|
+
}
|
|
214
|
+
/** Union of cue time as sorted disjoint intervals; touching intervals are merged. */
|
|
215
|
+
function mergeIntervals(cues, options = {}) {
|
|
216
|
+
const input = readCues(cues, limit('maxCues', options.maxCues, exports.DEFAULT_MAX_CUES));
|
|
217
|
+
const sorted = input
|
|
218
|
+
.map((c) => ({ startMs: c.startMs, endMs: c.endMs }))
|
|
219
|
+
.sort((a, b) => a.startMs - b.startMs);
|
|
220
|
+
const merged = [];
|
|
221
|
+
for (const interval of sorted) {
|
|
222
|
+
const last = merged[merged.length - 1];
|
|
223
|
+
if (last && interval.startMs <= last.endMs)
|
|
224
|
+
last.endMs = Math.max(last.endMs, interval.endMs);
|
|
225
|
+
else
|
|
226
|
+
merged.push(interval);
|
|
227
|
+
}
|
|
228
|
+
return merged;
|
|
229
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface Cue {
|
|
2
|
+
id?: string;
|
|
3
|
+
/** Inclusive start in integer milliseconds (>= 0). */
|
|
4
|
+
startMs: number;
|
|
5
|
+
/** Exclusive end in integer milliseconds (> startMs). */
|
|
6
|
+
endMs: number;
|
|
7
|
+
text: string;
|
|
8
|
+
}
|
|
9
|
+
export type NegativePolicy = 'error' | 'clip' | 'drop';
|
|
10
|
+
export type TimeRounding = 'round' | 'floor' | 'ceil';
|
|
11
|
+
export type OverlapPolicy = 'trim-previous' | 'push-next';
|
|
12
|
+
export interface ShiftOptions {
|
|
13
|
+
/** What to do when a shifted cue starts before 0. Default 'error'. */
|
|
14
|
+
negative?: NegativePolicy;
|
|
15
|
+
}
|
|
16
|
+
export interface ScaleOptions extends ShiftOptions {
|
|
17
|
+
/** Time that stays fixed while scaling. Default 0. */
|
|
18
|
+
anchorMs?: number;
|
|
19
|
+
rounding?: TimeRounding;
|
|
20
|
+
}
|
|
21
|
+
export interface Interval {
|
|
22
|
+
startMs: number;
|
|
23
|
+
endMs: number;
|
|
24
|
+
}
|
|
25
|
+
export interface Overlap extends Interval {
|
|
26
|
+
/** Indices into the supplied array, first < second. */
|
|
27
|
+
first: number;
|
|
28
|
+
second: number;
|
|
29
|
+
}
|
|
30
|
+
export interface LimitOptions {
|
|
31
|
+
maxCues?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface OverlapSearchOptions extends LimitOptions {
|
|
34
|
+
maxPairs?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface ClipOptions extends LimitOptions {
|
|
37
|
+
/** Subtract windowStartMs from kept cues. Default false. */
|
|
38
|
+
rebase?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare const DEFAULT_MAX_CUES = 100000;
|
|
41
|
+
export declare const DEFAULT_MAX_PAIRS = 100000;
|
|
42
|
+
/** Adds deltaMs to every cue; the input array and cues are not modified. */
|
|
43
|
+
export declare function shiftCues(cues: readonly Cue[], deltaMs: number, options?: ShiftOptions): Cue[];
|
|
44
|
+
/** Maps t to anchor + (t - anchor) * factor, rounded; a cue that collapses to zero length is an error. */
|
|
45
|
+
export declare function scaleCues(cues: readonly Cue[], factor: number, options?: ScaleOptions): Cue[];
|
|
46
|
+
/** Splits at strictly increasing interior times. Pieces get ids `${id}#1`, `${id}#2`, ... when the cue has an id. */
|
|
47
|
+
export declare function splitCue(cue: Cue, atMs: readonly number[], texts?: readonly string[]): Cue[];
|
|
48
|
+
/** Intersects cues with [windowStartMs, windowEndMs); cues with no remaining duration are removed. */
|
|
49
|
+
export declare function clipCues(cues: readonly Cue[], windowStartMs: number, windowEndMs: number, options?: ClipOptions): Cue[];
|
|
50
|
+
/** Stable sort by start then end; returns new cue objects. */
|
|
51
|
+
export declare function sortCues(cues: readonly Cue[]): Cue[];
|
|
52
|
+
/** All pairs that share positive duration (touching cues do not overlap), ordered by (first, second). */
|
|
53
|
+
export declare function findOverlaps(cues: readonly Cue[], options?: OverlapSearchOptions): Overlap[];
|
|
54
|
+
/** Sorts by start and removes overlaps against the previously kept cue; empty results are dropped. */
|
|
55
|
+
export declare function resolveOverlaps(cues: readonly Cue[], overlapPolicy: OverlapPolicy): Cue[];
|
|
56
|
+
/** Union of cue time as sorted disjoint intervals; touching intervals are merged. */
|
|
57
|
+
export declare function mergeIntervals(cues: readonly Cue[], options?: LimitOptions): Interval[];
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
export const DEFAULT_MAX_CUES = 100_000;
|
|
2
|
+
export const DEFAULT_MAX_PAIRS = 100_000;
|
|
3
|
+
function time(name, value, min) {
|
|
4
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < min) {
|
|
5
|
+
throw new RangeError(`${name} must be a safe integer >= ${min}`);
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function limit(name, value, fallback) {
|
|
10
|
+
return value === undefined ? fallback : time(name, value, 0);
|
|
11
|
+
}
|
|
12
|
+
function readCues(cues, maxCues) {
|
|
13
|
+
if (!Array.isArray(cues))
|
|
14
|
+
throw new TypeError('cues must be an array');
|
|
15
|
+
if (cues.length > maxCues)
|
|
16
|
+
throw new RangeError('cue count exceeds maxCues');
|
|
17
|
+
for (const [i, cue] of cues.entries()) {
|
|
18
|
+
if (typeof cue !== 'object' || cue === null)
|
|
19
|
+
throw new TypeError(`cues[${i}] must be an object`);
|
|
20
|
+
const c = cue;
|
|
21
|
+
const start = time(`cues[${i}].startMs`, c.startMs, 0);
|
|
22
|
+
const end = time(`cues[${i}].endMs`, c.endMs, 0);
|
|
23
|
+
if (end <= start)
|
|
24
|
+
throw new RangeError(`cues[${i}] must end after it starts`);
|
|
25
|
+
if (typeof c.text !== 'string')
|
|
26
|
+
throw new TypeError(`cues[${i}].text must be a string`);
|
|
27
|
+
if (c.id !== undefined && typeof c.id !== 'string')
|
|
28
|
+
throw new TypeError(`cues[${i}].id must be a string`);
|
|
29
|
+
}
|
|
30
|
+
return cues;
|
|
31
|
+
}
|
|
32
|
+
function make(cue, startMs, endMs, id) {
|
|
33
|
+
const resultId = id ?? cue.id;
|
|
34
|
+
return resultId === undefined
|
|
35
|
+
? { startMs, endMs, text: cue.text }
|
|
36
|
+
: { id: resultId, startMs, endMs, text: cue.text };
|
|
37
|
+
}
|
|
38
|
+
function policy(value) {
|
|
39
|
+
const chosen = value === undefined ? 'error' : value;
|
|
40
|
+
if (chosen !== 'error' && chosen !== 'clip' && chosen !== 'drop')
|
|
41
|
+
throw new TypeError('unknown negative policy');
|
|
42
|
+
return chosen;
|
|
43
|
+
}
|
|
44
|
+
function applyNegative(cue, start, end, negative) {
|
|
45
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end))
|
|
46
|
+
throw new RangeError('time is not a safe integer');
|
|
47
|
+
if (start >= 0)
|
|
48
|
+
return make(cue, start, end);
|
|
49
|
+
if (negative === 'error')
|
|
50
|
+
throw new RangeError('cue would start before 0');
|
|
51
|
+
if (negative === 'drop' || end <= 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
return make(cue, 0, end);
|
|
54
|
+
}
|
|
55
|
+
/** Adds deltaMs to every cue; the input array and cues are not modified. */
|
|
56
|
+
export function shiftCues(cues, deltaMs, options = {}) {
|
|
57
|
+
const input = readCues(cues, DEFAULT_MAX_CUES);
|
|
58
|
+
if (typeof deltaMs !== 'number' || !Number.isSafeInteger(deltaMs))
|
|
59
|
+
throw new RangeError('deltaMs must be a safe integer');
|
|
60
|
+
const negative = policy(options.negative);
|
|
61
|
+
const result = [];
|
|
62
|
+
for (const cue of input) {
|
|
63
|
+
const shifted = applyNegative(cue, cue.startMs + deltaMs, cue.endMs + deltaMs, negative);
|
|
64
|
+
if (shifted)
|
|
65
|
+
result.push(shifted);
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
/** Maps t to anchor + (t - anchor) * factor, rounded; a cue that collapses to zero length is an error. */
|
|
70
|
+
export function scaleCues(cues, factor, options = {}) {
|
|
71
|
+
const input = readCues(cues, DEFAULT_MAX_CUES);
|
|
72
|
+
if (typeof factor !== 'number' || !Number.isFinite(factor) || factor <= 0) {
|
|
73
|
+
throw new RangeError('factor must be a positive finite number');
|
|
74
|
+
}
|
|
75
|
+
const anchor = options.anchorMs === undefined ? 0 : time('anchorMs', options.anchorMs, 0);
|
|
76
|
+
const rounding = options.rounding ?? 'round';
|
|
77
|
+
const round = rounding === 'round'
|
|
78
|
+
? Math.round
|
|
79
|
+
: rounding === 'floor'
|
|
80
|
+
? Math.floor
|
|
81
|
+
: rounding === 'ceil'
|
|
82
|
+
? Math.ceil
|
|
83
|
+
: undefined;
|
|
84
|
+
if (!round)
|
|
85
|
+
throw new TypeError('unknown rounding');
|
|
86
|
+
const negative = policy(options.negative);
|
|
87
|
+
const map = (t) => round(anchor + (t - anchor) * factor);
|
|
88
|
+
const result = [];
|
|
89
|
+
for (const cue of input) {
|
|
90
|
+
const start = map(cue.startMs);
|
|
91
|
+
const end = map(cue.endMs);
|
|
92
|
+
if (end <= start && Number.isSafeInteger(start) && Number.isSafeInteger(end)) {
|
|
93
|
+
throw new RangeError('scaled cue has no positive duration');
|
|
94
|
+
}
|
|
95
|
+
const scaled = applyNegative(cue, start, end, negative);
|
|
96
|
+
if (scaled)
|
|
97
|
+
result.push(scaled);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
/** Splits at strictly increasing interior times. Pieces get ids `${id}#1`, `${id}#2`, ... when the cue has an id. */
|
|
102
|
+
export function splitCue(cue, atMs, texts) {
|
|
103
|
+
const [checked] = readCues([cue], 1);
|
|
104
|
+
if (!Array.isArray(atMs) || atMs.length === 0)
|
|
105
|
+
throw new TypeError('atMs must be a non-empty array');
|
|
106
|
+
let previous = checked.startMs;
|
|
107
|
+
for (const [i, at] of atMs.entries()) {
|
|
108
|
+
time(`atMs[${i}]`, at, 0);
|
|
109
|
+
if (at <= previous || at >= checked.endMs)
|
|
110
|
+
throw new RangeError(`atMs[${i}] must be increasing and inside the cue`);
|
|
111
|
+
previous = at;
|
|
112
|
+
}
|
|
113
|
+
if (texts !== undefined) {
|
|
114
|
+
if (!Array.isArray(texts) ||
|
|
115
|
+
texts.length !== atMs.length + 1 ||
|
|
116
|
+
texts.some((t) => typeof t !== 'string')) {
|
|
117
|
+
throw new TypeError('texts must contain one string per piece');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const bounds = [checked.startMs, ...atMs, checked.endMs];
|
|
121
|
+
return bounds.slice(0, -1).map((start, i) => {
|
|
122
|
+
const piece = make(checked, start, bounds[i + 1], checked.id === undefined ? undefined : `${checked.id}#${i + 1}`);
|
|
123
|
+
return texts ? { ...piece, text: texts[i] } : piece;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/** Intersects cues with [windowStartMs, windowEndMs); cues with no remaining duration are removed. */
|
|
127
|
+
export function clipCues(cues, windowStartMs, windowEndMs, options = {}) {
|
|
128
|
+
const input = readCues(cues, limit('maxCues', options.maxCues, DEFAULT_MAX_CUES));
|
|
129
|
+
const from = time('windowStartMs', windowStartMs, 0);
|
|
130
|
+
const to = time('windowEndMs', windowEndMs, 0);
|
|
131
|
+
if (to <= from)
|
|
132
|
+
throw new RangeError('window must have positive duration');
|
|
133
|
+
const offset = options.rebase === true ? from : 0;
|
|
134
|
+
const result = [];
|
|
135
|
+
for (const cue of input) {
|
|
136
|
+
const start = Math.max(cue.startMs, from);
|
|
137
|
+
const end = Math.min(cue.endMs, to);
|
|
138
|
+
if (end > start)
|
|
139
|
+
result.push(make(cue, start - offset, end - offset));
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
/** Stable sort by start then end; returns new cue objects. */
|
|
144
|
+
export function sortCues(cues) {
|
|
145
|
+
return readCues(cues, DEFAULT_MAX_CUES)
|
|
146
|
+
.map((cue, index) => ({ cue, index }))
|
|
147
|
+
.sort((a, b) => a.cue.startMs - b.cue.startMs || a.cue.endMs - b.cue.endMs || a.index - b.index)
|
|
148
|
+
.map(({ cue }) => make(cue, cue.startMs, cue.endMs));
|
|
149
|
+
}
|
|
150
|
+
/** All pairs that share positive duration (touching cues do not overlap), ordered by (first, second). */
|
|
151
|
+
export function findOverlaps(cues, options = {}) {
|
|
152
|
+
const input = readCues(cues, limit('maxCues', options.maxCues, DEFAULT_MAX_CUES));
|
|
153
|
+
const maxPairs = limit('maxPairs', options.maxPairs, DEFAULT_MAX_PAIRS);
|
|
154
|
+
const order = input
|
|
155
|
+
.map((_, i) => i)
|
|
156
|
+
.sort((a, b) => input[a].startMs - input[b].startMs || a - b);
|
|
157
|
+
const active = [];
|
|
158
|
+
const found = [];
|
|
159
|
+
for (const index of order) {
|
|
160
|
+
const cue = input[index];
|
|
161
|
+
for (let k = active.length - 1; k >= 0; k--) {
|
|
162
|
+
if (input[active[k]].endMs <= cue.startMs)
|
|
163
|
+
active.splice(k, 1);
|
|
164
|
+
}
|
|
165
|
+
for (const other of active) {
|
|
166
|
+
if (found.length >= maxPairs)
|
|
167
|
+
throw new RangeError('overlap pairs exceed maxPairs');
|
|
168
|
+
const o = input[other];
|
|
169
|
+
found.push({
|
|
170
|
+
first: Math.min(index, other),
|
|
171
|
+
second: Math.max(index, other),
|
|
172
|
+
startMs: Math.max(o.startMs, cue.startMs),
|
|
173
|
+
endMs: Math.min(o.endMs, cue.endMs),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
active.push(index);
|
|
177
|
+
}
|
|
178
|
+
return found.sort((a, b) => a.first - b.first || a.second - b.second);
|
|
179
|
+
}
|
|
180
|
+
/** Sorts by start and removes overlaps against the previously kept cue; empty results are dropped. */
|
|
181
|
+
export function resolveOverlaps(cues, overlapPolicy) {
|
|
182
|
+
if (overlapPolicy !== 'trim-previous' && overlapPolicy !== 'push-next')
|
|
183
|
+
throw new TypeError('unknown overlap policy');
|
|
184
|
+
const kept = [];
|
|
185
|
+
for (const cue of sortCues(cues)) {
|
|
186
|
+
const previous = kept[kept.length - 1];
|
|
187
|
+
if (previous && cue.startMs < previous.endMs) {
|
|
188
|
+
if (overlapPolicy === 'trim-previous') {
|
|
189
|
+
previous.endMs = cue.startMs;
|
|
190
|
+
if (previous.endMs <= previous.startMs)
|
|
191
|
+
kept.pop();
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
if (previous.endMs >= cue.endMs)
|
|
195
|
+
continue;
|
|
196
|
+
cue.startMs = previous.endMs;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
kept.push(cue);
|
|
200
|
+
}
|
|
201
|
+
return kept;
|
|
202
|
+
}
|
|
203
|
+
/** Union of cue time as sorted disjoint intervals; touching intervals are merged. */
|
|
204
|
+
export function mergeIntervals(cues, options = {}) {
|
|
205
|
+
const input = readCues(cues, limit('maxCues', options.maxCues, DEFAULT_MAX_CUES));
|
|
206
|
+
const sorted = input
|
|
207
|
+
.map((c) => ({ startMs: c.startMs, endMs: c.endMs }))
|
|
208
|
+
.sort((a, b) => a.startMs - b.startMs);
|
|
209
|
+
const merged = [];
|
|
210
|
+
for (const interval of sorted) {
|
|
211
|
+
const last = merged[merged.length - 1];
|
|
212
|
+
if (last && interval.startMs <= last.endMs)
|
|
213
|
+
last.endMs = Math.max(last.endMs, interval.endMs);
|
|
214
|
+
else
|
|
215
|
+
merged.push(interval);
|
|
216
|
+
}
|
|
217
|
+
return merged;
|
|
218
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lp-labs/subtitle-timing",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Subtitle cue shift, scale, split, clip, overlap and interval operations on millisecond times.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/cjs/index.js",
|
|
8
|
+
"module": "./dist/esm/index.js",
|
|
9
|
+
"types": "./dist/esm/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./dist/esm/index.d.ts",
|
|
14
|
+
"default": "./dist/esm/index.js"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./dist/cjs/index.d.ts",
|
|
18
|
+
"default": "./dist/cjs/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "node scripts/build.mjs",
|
|
34
|
+
"typecheck": "npm run build && node scripts/check-types.mjs",
|
|
35
|
+
"test": "npm run build && node --test tests/*.test.mjs",
|
|
36
|
+
"format": "prettier --write src scripts tests *.json README.md",
|
|
37
|
+
"format:check": "prettier --check src scripts tests *.json README.md",
|
|
38
|
+
"prepack": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"prettier": "^3.6.2"
|
|
43
|
+
},
|
|
44
|
+
"prettier": {
|
|
45
|
+
"singleQuote": true,
|
|
46
|
+
"semi": true,
|
|
47
|
+
"trailingComma": "all",
|
|
48
|
+
"printWidth": 100,
|
|
49
|
+
"endOfLine": "lf"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public",
|
|
53
|
+
"registry": "https://registry.npmjs.org/"
|
|
54
|
+
},
|
|
55
|
+
"author": {
|
|
56
|
+
"name": "Libin Prasanth",
|
|
57
|
+
"url": "https://www.linkedin.com/in/libinprasanth/"
|
|
58
|
+
},
|
|
59
|
+
"funding": {
|
|
60
|
+
"type": "paypal",
|
|
61
|
+
"url": "https://www.paypal.me/LibinPrasanth"
|
|
62
|
+
}
|
|
63
|
+
}
|