subcraft 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Core normalized representation of a single subtitle cue.
3
+ * Timestamps are strictly stored in integer milliseconds.
4
+ */
5
+ interface SubtitleCue {
6
+ /** Unique sequential identifier (e.g. "1", "2") */
7
+ id: string;
8
+ /** Start time in integer milliseconds */
9
+ start: number;
10
+ /** End time in integer milliseconds */
11
+ end: number;
12
+ /** Raw subtitle text content (may contain newlines) */
13
+ text: string;
14
+ }
15
+ /**
16
+ * Calculates the duration of a cue in milliseconds.
17
+ * Derived helper function to keep the core data model minimal and immutable.
18
+ */
19
+ declare const getCueDuration: (cue: SubtitleCue) => number;
20
+ /** Supported subtitle file formats */
21
+ type TimecodeFormat = 'srt' | 'vtt';
22
+ /**
23
+ * Detailed error reported when a cue block fails parsing syntax rules.
24
+ */
25
+ interface ParseError {
26
+ index?: number;
27
+ rawBlock: string;
28
+ message: string;
29
+ }
30
+ /**
31
+ * Resilient parser result returning successfully parsed cues
32
+ * alongside any syntax errors encountered without throwing.
33
+ */
34
+ interface ParseResult {
35
+ cues: SubtitleCue[];
36
+ errors: ParseError[];
37
+ }
38
+ /** Severity levels for lint diagnostics */
39
+ type LintSeverity = 'error' | 'warning';
40
+ /** Quality rules enforced by the linter */
41
+ type LintRule = 'out-of-order' | 'overlap' | 'zero-or-negative-duration' | 'min-duration' | 'max-duration' | 'max-cpl' | 'max-cps';
42
+ /**
43
+ * Diagnostic report produced when a cue violates a quality rule.
44
+ */
45
+ interface LintDiagnostic {
46
+ cueId: string;
47
+ rule: LintRule;
48
+ severity: LintSeverity;
49
+ message: string;
50
+ expected?: number | string;
51
+ actual?: number | string;
52
+ }
53
+ /**
54
+ * Configuration options for locale-aware subtitle validation.
55
+ */
56
+ interface LintProfile {
57
+ name: string;
58
+ locale?: string;
59
+ maxCps: number;
60
+ maxCpl: number;
61
+ minDurationMs: number;
62
+ maxDurationMs: number;
63
+ includeSpaces?: boolean;
64
+ }
65
+
66
+ /**
67
+ * Converts a formatted timecode string into integer milliseconds.
68
+ *
69
+ * Supported formats:
70
+ * - "01:23:45,678" (SRT standard)
71
+ * - "01:23:45.678" (WebVTT standard)
72
+ * - "23:45.678" (WebVTT short form without hours)
73
+ *
74
+ * @throws {Error} if timecode format is invalid
75
+ */
76
+ declare function timecodeToMs(timecode: string): number;
77
+ /**
78
+ * Converts integer milliseconds into a standard formatted timecode string.
79
+ *
80
+ * @param ms - Non-negative integer milliseconds (negative values clamped to 0)
81
+ * @param format - 'srt' (uses comma delimiter) or 'vtt' (uses dot delimiter)
82
+ *
83
+ * @example
84
+ * msToTimecode(83456, 'srt') // "00:01:23,456"
85
+ * msToTimecode(83456, 'vtt') // "00:01:23.456"
86
+ */
87
+ declare function msToTimecode(ms: number, format?: TimecodeFormat): string;
88
+ /**
89
+ * Linearly shifts timecodes by a constant millisecond offset.
90
+ * Used to correct audio-sync delay or advance.
91
+ * Clamps negative results to 0.
92
+ *
93
+ * @param cues - Array of SubtitleCue objects
94
+ * @param offsetMs - Offset in milliseconds (positive advances, negative delays)
95
+ * @returns A new immutable array of shifted cues
96
+ */
97
+ declare function shiftTimecodes(cues: readonly SubtitleCue[], offsetMs: number): SubtitleCue[];
98
+ /**
99
+ * Scales all cue timecodes by a multiplicative ratio.
100
+ * Used for frame-rate drift compensation between video standards.
101
+ * All results are strictly rounded to integer milliseconds with Math.round.
102
+ *
103
+ * Direction definition:
104
+ * targetTimestamp = Math.round(sourceTimestamp * ratio)
105
+ *
106
+ * @example
107
+ * // 24 fps to 25 fps (PAL speed-up): video is ~4% faster, timecodes compress
108
+ * const palCues = retime(filmCues, 24 / 25); // ratio = 0.96
109
+ *
110
+ * @example
111
+ * // 25 fps to 24 fps (Slow-down): video is ~4% slower, timecodes stretch
112
+ * const filmCues = retime(palCues, 25 / 24); // ratio = 1.04167
113
+ *
114
+ * @param cues - Array of SubtitleCue objects
115
+ * @param ratio - Positive scaling multiplier (e.g., 24/25 or 25/24)
116
+ * @returns A new immutable array of retimed cues
117
+ */
118
+ declare function retime(cues: readonly SubtitleCue[], ratio: number): SubtitleCue[];
119
+
120
+ /**
121
+ * Parses raw SRT subtitle text into structured SubtitleCue objects.
122
+ * Resilient parser that collects syntax errors without throwing unhandled exceptions.
123
+ *
124
+ * Handles:
125
+ * - UTF-8 Byte Order Marks (BOM `\uFEFF`)
126
+ * - Windows (CRLF), Unix (LF), and legacy Mac (CR) line endings
127
+ * - Optional or missing cue numeric index headers
128
+ * - Multi-line subtitle texts
129
+ * - Trailing and irregular blank lines
130
+ *
131
+ * @param rawText - Raw SRT file content as a string
132
+ * @returns {ParseResult} Object containing successfully parsed cues and any block errors
133
+ */
134
+ declare function parseSRT(rawText: string): ParseResult;
135
+
136
+ /**
137
+ * Serializes an array of SubtitleCue objects into standard SRT file format.
138
+ *
139
+ * Output format:
140
+ * 1
141
+ * 00:00:20,000 --> 00:00:24,000
142
+ * Subtitle text
143
+ *
144
+ * @param cues - Array of SubtitleCue objects to format
145
+ * @returns Standard formatted SRT string
146
+ */
147
+ declare function stringifySRT(cues: readonly SubtitleCue[]): string;
148
+
149
+ /** Default quality profiles for subtitle verification */
150
+ declare const DEFAULT_PROFILES: Record<string, LintProfile>;
151
+ /**
152
+ * Strips HTML formatting tags from subtitle text before measuring character limits.
153
+ * Handles <i>, <b>, <u>, <font color="...">, etc.
154
+ */
155
+ declare function stripFormattingTags(text: string): string;
156
+ /**
157
+ * Counts user-perceived grapheme clusters using native ECMAScript Intl.Segmenter.
158
+ * Accurately measures multi-byte conjuncts and vowel modifiers in Indic scripts (Kannada, Hindi),
159
+ * as well as emojis and complex Unicode characters without UTF-16 code unit inflation.
160
+ */
161
+ declare function countGraphemes(text: string, locale?: string): number;
162
+ /**
163
+ * Inspects an array of SubtitleCues against timing, structural, and reading speed rules.
164
+ *
165
+ * Rules verified:
166
+ * - out-of-order: Cues not in ascending chronological order
167
+ * - zero-or-negative-duration: Cues where end <= start
168
+ * - overlap: Cues overlapping chronologically with preceding cues
169
+ * - min-duration: Cues flashing too fast for comprehension (< 800ms)
170
+ * - max-duration: Cues remaining on screen too long (> 7000ms)
171
+ * - max-cpl: Any line exceeding characters-per-line limit
172
+ * - max-cps: Reading speed exceeding characters-per-second limit
173
+ *
174
+ * @param cues - Array of SubtitleCue objects to inspect
175
+ * @param customProfile - Optional custom thresholds or language profile
176
+ * @returns Array of LintDiagnostic reports
177
+ */
178
+ declare function lintCues(cues: readonly SubtitleCue[], customProfile?: Partial<LintProfile>): LintDiagnostic[];
179
+
180
+ export { DEFAULT_PROFILES, type LintDiagnostic, type LintProfile, type LintRule, type LintSeverity, type ParseError, type ParseResult, type SubtitleCue, type TimecodeFormat, countGraphemes, getCueDuration, lintCues, msToTimecode, parseSRT, retime, shiftTimecodes, stringifySRT, stripFormattingTags, timecodeToMs };
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Core normalized representation of a single subtitle cue.
3
+ * Timestamps are strictly stored in integer milliseconds.
4
+ */
5
+ interface SubtitleCue {
6
+ /** Unique sequential identifier (e.g. "1", "2") */
7
+ id: string;
8
+ /** Start time in integer milliseconds */
9
+ start: number;
10
+ /** End time in integer milliseconds */
11
+ end: number;
12
+ /** Raw subtitle text content (may contain newlines) */
13
+ text: string;
14
+ }
15
+ /**
16
+ * Calculates the duration of a cue in milliseconds.
17
+ * Derived helper function to keep the core data model minimal and immutable.
18
+ */
19
+ declare const getCueDuration: (cue: SubtitleCue) => number;
20
+ /** Supported subtitle file formats */
21
+ type TimecodeFormat = 'srt' | 'vtt';
22
+ /**
23
+ * Detailed error reported when a cue block fails parsing syntax rules.
24
+ */
25
+ interface ParseError {
26
+ index?: number;
27
+ rawBlock: string;
28
+ message: string;
29
+ }
30
+ /**
31
+ * Resilient parser result returning successfully parsed cues
32
+ * alongside any syntax errors encountered without throwing.
33
+ */
34
+ interface ParseResult {
35
+ cues: SubtitleCue[];
36
+ errors: ParseError[];
37
+ }
38
+ /** Severity levels for lint diagnostics */
39
+ type LintSeverity = 'error' | 'warning';
40
+ /** Quality rules enforced by the linter */
41
+ type LintRule = 'out-of-order' | 'overlap' | 'zero-or-negative-duration' | 'min-duration' | 'max-duration' | 'max-cpl' | 'max-cps';
42
+ /**
43
+ * Diagnostic report produced when a cue violates a quality rule.
44
+ */
45
+ interface LintDiagnostic {
46
+ cueId: string;
47
+ rule: LintRule;
48
+ severity: LintSeverity;
49
+ message: string;
50
+ expected?: number | string;
51
+ actual?: number | string;
52
+ }
53
+ /**
54
+ * Configuration options for locale-aware subtitle validation.
55
+ */
56
+ interface LintProfile {
57
+ name: string;
58
+ locale?: string;
59
+ maxCps: number;
60
+ maxCpl: number;
61
+ minDurationMs: number;
62
+ maxDurationMs: number;
63
+ includeSpaces?: boolean;
64
+ }
65
+
66
+ /**
67
+ * Converts a formatted timecode string into integer milliseconds.
68
+ *
69
+ * Supported formats:
70
+ * - "01:23:45,678" (SRT standard)
71
+ * - "01:23:45.678" (WebVTT standard)
72
+ * - "23:45.678" (WebVTT short form without hours)
73
+ *
74
+ * @throws {Error} if timecode format is invalid
75
+ */
76
+ declare function timecodeToMs(timecode: string): number;
77
+ /**
78
+ * Converts integer milliseconds into a standard formatted timecode string.
79
+ *
80
+ * @param ms - Non-negative integer milliseconds (negative values clamped to 0)
81
+ * @param format - 'srt' (uses comma delimiter) or 'vtt' (uses dot delimiter)
82
+ *
83
+ * @example
84
+ * msToTimecode(83456, 'srt') // "00:01:23,456"
85
+ * msToTimecode(83456, 'vtt') // "00:01:23.456"
86
+ */
87
+ declare function msToTimecode(ms: number, format?: TimecodeFormat): string;
88
+ /**
89
+ * Linearly shifts timecodes by a constant millisecond offset.
90
+ * Used to correct audio-sync delay or advance.
91
+ * Clamps negative results to 0.
92
+ *
93
+ * @param cues - Array of SubtitleCue objects
94
+ * @param offsetMs - Offset in milliseconds (positive advances, negative delays)
95
+ * @returns A new immutable array of shifted cues
96
+ */
97
+ declare function shiftTimecodes(cues: readonly SubtitleCue[], offsetMs: number): SubtitleCue[];
98
+ /**
99
+ * Scales all cue timecodes by a multiplicative ratio.
100
+ * Used for frame-rate drift compensation between video standards.
101
+ * All results are strictly rounded to integer milliseconds with Math.round.
102
+ *
103
+ * Direction definition:
104
+ * targetTimestamp = Math.round(sourceTimestamp * ratio)
105
+ *
106
+ * @example
107
+ * // 24 fps to 25 fps (PAL speed-up): video is ~4% faster, timecodes compress
108
+ * const palCues = retime(filmCues, 24 / 25); // ratio = 0.96
109
+ *
110
+ * @example
111
+ * // 25 fps to 24 fps (Slow-down): video is ~4% slower, timecodes stretch
112
+ * const filmCues = retime(palCues, 25 / 24); // ratio = 1.04167
113
+ *
114
+ * @param cues - Array of SubtitleCue objects
115
+ * @param ratio - Positive scaling multiplier (e.g., 24/25 or 25/24)
116
+ * @returns A new immutable array of retimed cues
117
+ */
118
+ declare function retime(cues: readonly SubtitleCue[], ratio: number): SubtitleCue[];
119
+
120
+ /**
121
+ * Parses raw SRT subtitle text into structured SubtitleCue objects.
122
+ * Resilient parser that collects syntax errors without throwing unhandled exceptions.
123
+ *
124
+ * Handles:
125
+ * - UTF-8 Byte Order Marks (BOM `\uFEFF`)
126
+ * - Windows (CRLF), Unix (LF), and legacy Mac (CR) line endings
127
+ * - Optional or missing cue numeric index headers
128
+ * - Multi-line subtitle texts
129
+ * - Trailing and irregular blank lines
130
+ *
131
+ * @param rawText - Raw SRT file content as a string
132
+ * @returns {ParseResult} Object containing successfully parsed cues and any block errors
133
+ */
134
+ declare function parseSRT(rawText: string): ParseResult;
135
+
136
+ /**
137
+ * Serializes an array of SubtitleCue objects into standard SRT file format.
138
+ *
139
+ * Output format:
140
+ * 1
141
+ * 00:00:20,000 --> 00:00:24,000
142
+ * Subtitle text
143
+ *
144
+ * @param cues - Array of SubtitleCue objects to format
145
+ * @returns Standard formatted SRT string
146
+ */
147
+ declare function stringifySRT(cues: readonly SubtitleCue[]): string;
148
+
149
+ /** Default quality profiles for subtitle verification */
150
+ declare const DEFAULT_PROFILES: Record<string, LintProfile>;
151
+ /**
152
+ * Strips HTML formatting tags from subtitle text before measuring character limits.
153
+ * Handles <i>, <b>, <u>, <font color="...">, etc.
154
+ */
155
+ declare function stripFormattingTags(text: string): string;
156
+ /**
157
+ * Counts user-perceived grapheme clusters using native ECMAScript Intl.Segmenter.
158
+ * Accurately measures multi-byte conjuncts and vowel modifiers in Indic scripts (Kannada, Hindi),
159
+ * as well as emojis and complex Unicode characters without UTF-16 code unit inflation.
160
+ */
161
+ declare function countGraphemes(text: string, locale?: string): number;
162
+ /**
163
+ * Inspects an array of SubtitleCues against timing, structural, and reading speed rules.
164
+ *
165
+ * Rules verified:
166
+ * - out-of-order: Cues not in ascending chronological order
167
+ * - zero-or-negative-duration: Cues where end <= start
168
+ * - overlap: Cues overlapping chronologically with preceding cues
169
+ * - min-duration: Cues flashing too fast for comprehension (< 800ms)
170
+ * - max-duration: Cues remaining on screen too long (> 7000ms)
171
+ * - max-cpl: Any line exceeding characters-per-line limit
172
+ * - max-cps: Reading speed exceeding characters-per-second limit
173
+ *
174
+ * @param cues - Array of SubtitleCue objects to inspect
175
+ * @param customProfile - Optional custom thresholds or language profile
176
+ * @returns Array of LintDiagnostic reports
177
+ */
178
+ declare function lintCues(cues: readonly SubtitleCue[], customProfile?: Partial<LintProfile>): LintDiagnostic[];
179
+
180
+ export { DEFAULT_PROFILES, type LintDiagnostic, type LintProfile, type LintRule, type LintSeverity, type ParseError, type ParseResult, type SubtitleCue, type TimecodeFormat, countGraphemes, getCueDuration, lintCues, msToTimecode, parseSRT, retime, shiftTimecodes, stringifySRT, stripFormattingTags, timecodeToMs };
package/dist/index.js ADDED
@@ -0,0 +1,280 @@
1
+ // src/types.ts
2
+ var getCueDuration = (cue) => cue.end - cue.start;
3
+
4
+ // src/timecode.ts
5
+ var TIMECODE_REGEX = /^(?:(?:(\d{1,2}):)?(\d{2}):)?(\d{2})[,.](\d{3})$/;
6
+ function timecodeToMs(timecode) {
7
+ const trimmed = timecode.trim();
8
+ const match = trimmed.match(TIMECODE_REGEX);
9
+ if (!match) {
10
+ throw new Error(
11
+ `Invalid timecode format: "${timecode}". Expected "HH:MM:SS,mmm" or "HH:MM:SS.mmm".`
12
+ );
13
+ }
14
+ const hours = match[1] ? parseInt(match[1], 10) : 0;
15
+ const minutes = match[2] ? parseInt(match[2], 10) : 0;
16
+ const seconds = parseInt(match[3] ?? "0", 10);
17
+ const milliseconds = parseInt(match[4] ?? "0", 10);
18
+ return hours * 36e5 + minutes * 6e4 + seconds * 1e3 + milliseconds;
19
+ }
20
+ function msToTimecode(ms, format = "srt") {
21
+ const safeMs = Math.max(0, Math.floor(ms));
22
+ const hours = Math.floor(safeMs / 36e5);
23
+ const minutes = Math.floor(safeMs % 36e5 / 6e4);
24
+ const seconds = Math.floor(safeMs % 6e4 / 1e3);
25
+ const remainderMs = safeMs % 1e3;
26
+ const hh = String(hours).padStart(2, "0");
27
+ const mm = String(minutes).padStart(2, "0");
28
+ const ss = String(seconds).padStart(2, "0");
29
+ const mmm = String(remainderMs).padStart(3, "0");
30
+ const delimiter = format === "vtt" ? "." : ",";
31
+ return `${hh}:${mm}:${ss}${delimiter}${mmm}`;
32
+ }
33
+ function shiftTimecodes(cues, offsetMs) {
34
+ return cues.map((cue) => ({
35
+ ...cue,
36
+ start: Math.max(0, cue.start + offsetMs),
37
+ end: Math.max(0, cue.end + offsetMs)
38
+ }));
39
+ }
40
+ function retime(cues, ratio) {
41
+ if (ratio <= 0) {
42
+ throw new Error(`Retime ratio must be greater than 0. Received: ${ratio}`);
43
+ }
44
+ return cues.map((cue) => ({
45
+ ...cue,
46
+ start: Math.round(cue.start * ratio),
47
+ end: Math.round(cue.end * ratio)
48
+ }));
49
+ }
50
+
51
+ // src/parser.ts
52
+ var ARROW_REGEX = /((?:\d{1,2}:)?\d{2}:\d{2}[,.]\d{3})\s*-->\s*((?:\d{1,2}:)?\d{2}:\d{2}[,.]\d{3})/;
53
+ function parseSRT(rawText) {
54
+ const cues = [];
55
+ const errors = [];
56
+ if (!rawText || !rawText.trim()) {
57
+ return { cues, errors };
58
+ }
59
+ const cleanText = rawText.replace(/^\uFEFF/, "");
60
+ const normalized = cleanText.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
61
+ const rawBlocks = normalized.trim().split(/\n\s*\n+/);
62
+ let fallbackIndex = 1;
63
+ for (let bIndex = 0; bIndex < rawBlocks.length; bIndex++) {
64
+ const rawBlock = rawBlocks[bIndex]?.trim();
65
+ if (!rawBlock) continue;
66
+ const lines = rawBlock.split("\n");
67
+ let id;
68
+ let timecodeLine;
69
+ let textLines;
70
+ if (lines[0] && ARROW_REGEX.test(lines[0])) {
71
+ id = String(fallbackIndex);
72
+ timecodeLine = lines[0];
73
+ textLines = lines.slice(1);
74
+ } else if (lines[1] && ARROW_REGEX.test(lines[1])) {
75
+ id = lines[0]?.trim() || String(fallbackIndex);
76
+ timecodeLine = lines[1];
77
+ textLines = lines.slice(2);
78
+ } else {
79
+ errors.push({
80
+ index: fallbackIndex,
81
+ rawBlock,
82
+ message: 'Invalid block format. Expected timecode arrow "-->" on line 1 or 2.'
83
+ });
84
+ continue;
85
+ }
86
+ const match = timecodeLine.match(ARROW_REGEX);
87
+ if (!match || !match[1] || !match[2]) {
88
+ errors.push({
89
+ index: fallbackIndex,
90
+ rawBlock,
91
+ message: `Failed to extract start and end timestamps from: "${timecodeLine}".`
92
+ });
93
+ continue;
94
+ }
95
+ try {
96
+ const start = timecodeToMs(match[1]);
97
+ const end = timecodeToMs(match[2]);
98
+ const text = textLines.join("\n").trim();
99
+ cues.push({
100
+ id,
101
+ start,
102
+ end,
103
+ text
104
+ });
105
+ fallbackIndex++;
106
+ } catch (err) {
107
+ errors.push({
108
+ index: fallbackIndex,
109
+ rawBlock,
110
+ message: err instanceof Error ? err.message : String(err)
111
+ });
112
+ }
113
+ }
114
+ return { cues, errors };
115
+ }
116
+
117
+ // src/serializer.ts
118
+ function stringifySRT(cues) {
119
+ if (!cues || cues.length === 0) {
120
+ return "";
121
+ }
122
+ const blocks = cues.map((cue, idx) => {
123
+ const id = cue.id ? String(cue.id) : String(idx + 1);
124
+ const startStr = msToTimecode(cue.start, "srt");
125
+ const endStr = msToTimecode(cue.end, "srt");
126
+ const text = cue.text ?? "";
127
+ return `${id}
128
+ ${startStr} --> ${endStr}
129
+ ${text}`;
130
+ });
131
+ return blocks.join("\n\n") + "\n";
132
+ }
133
+
134
+ // src/linter.ts
135
+ var DEFAULT_PROFILES = {
136
+ standardLatin: {
137
+ name: "Standard Latin (Netflix/BBC Style Guide)",
138
+ locale: "en",
139
+ maxCps: 20,
140
+ maxCpl: 42,
141
+ minDurationMs: 800,
142
+ maxDurationMs: 7e3,
143
+ includeSpaces: true
144
+ },
145
+ indicKannada: {
146
+ name: "Indic - Kannada Locale",
147
+ locale: "kn",
148
+ maxCps: 15,
149
+ maxCpl: 35,
150
+ minDurationMs: 800,
151
+ maxDurationMs: 7e3,
152
+ includeSpaces: true
153
+ }
154
+ };
155
+ function stripFormattingTags(text) {
156
+ return text.replace(/<[^>]*>/g, "");
157
+ }
158
+ function countGraphemes(text, locale = "en") {
159
+ if (!text) return 0;
160
+ if (typeof Intl !== "undefined" && "Segmenter" in Intl) {
161
+ const segmenter = new Intl.Segmenter(locale, { granularity: "grapheme" });
162
+ let count = 0;
163
+ for (const _ of segmenter.segment(text)) {
164
+ count++;
165
+ }
166
+ return count;
167
+ }
168
+ return [...text].length;
169
+ }
170
+ function lintCues(cues, customProfile) {
171
+ const diagnostics = [];
172
+ if (!cues || cues.length === 0) return diagnostics;
173
+ const profile = {
174
+ ...DEFAULT_PROFILES["standardLatin"],
175
+ ...customProfile
176
+ };
177
+ const {
178
+ locale = "en",
179
+ maxCps,
180
+ maxCpl,
181
+ minDurationMs,
182
+ maxDurationMs,
183
+ includeSpaces = true
184
+ } = profile;
185
+ for (let i = 0; i < cues.length; i++) {
186
+ const cue = cues[i];
187
+ if (!cue) continue;
188
+ const prevCue = i > 0 ? cues[i - 1] : void 0;
189
+ const duration = cue.end - cue.start;
190
+ if (prevCue && cue.start < prevCue.start) {
191
+ diagnostics.push({
192
+ cueId: cue.id,
193
+ rule: "out-of-order",
194
+ severity: "error",
195
+ message: `Cue starts at ${cue.start}ms before previous cue start of ${prevCue.start}ms.`,
196
+ expected: `start >= ${prevCue.start}`,
197
+ actual: `start = ${cue.start}`
198
+ });
199
+ }
200
+ if (cue.end <= cue.start) {
201
+ diagnostics.push({
202
+ cueId: cue.id,
203
+ rule: "zero-or-negative-duration",
204
+ severity: "error",
205
+ message: `Cue has invalid duration (${duration}ms). End timestamp must be greater than start timestamp.`,
206
+ expected: `end > ${cue.start}`,
207
+ actual: `end = ${cue.end}`
208
+ });
209
+ }
210
+ if (prevCue && cue.start < prevCue.end && cue.start >= prevCue.start) {
211
+ const overlapMs = prevCue.end - cue.start;
212
+ diagnostics.push({
213
+ cueId: cue.id,
214
+ rule: "overlap",
215
+ severity: "error",
216
+ message: `Cue overlaps with previous cue by ${overlapMs}ms.`,
217
+ expected: `start >= ${prevCue.end}`,
218
+ actual: `start = ${cue.start}`
219
+ });
220
+ }
221
+ if (duration > 0 && duration < minDurationMs) {
222
+ diagnostics.push({
223
+ cueId: cue.id,
224
+ rule: "min-duration",
225
+ severity: "warning",
226
+ message: `Cue duration of ${duration}ms is below the minimum threshold of ${minDurationMs}ms.`,
227
+ expected: minDurationMs,
228
+ actual: duration
229
+ });
230
+ }
231
+ if (duration > maxDurationMs) {
232
+ diagnostics.push({
233
+ cueId: cue.id,
234
+ rule: "max-duration",
235
+ severity: "warning",
236
+ message: `Cue duration of ${duration}ms exceeds the maximum threshold of ${maxDurationMs}ms.`,
237
+ expected: maxDurationMs,
238
+ actual: duration
239
+ });
240
+ }
241
+ const rawText = cue.text || "";
242
+ const cleanText = stripFormattingTags(rawText);
243
+ const lines = cleanText.split("\n");
244
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
245
+ const line = lines[lineIndex];
246
+ if (!line) continue;
247
+ const cpl = countGraphemes(line, locale);
248
+ if (cpl > maxCpl) {
249
+ diagnostics.push({
250
+ cueId: cue.id,
251
+ rule: "max-cpl",
252
+ severity: "warning",
253
+ message: `Line ${lineIndex + 1} has ${cpl} characters, exceeding the maximum limit of ${maxCpl} CPL.`,
254
+ expected: maxCpl,
255
+ actual: cpl
256
+ });
257
+ }
258
+ }
259
+ if (duration > 0) {
260
+ const textToMeasure = includeSpaces ? cleanText : cleanText.replace(/\s+/g, "");
261
+ const totalGraphemes = countGraphemes(textToMeasure, locale);
262
+ const cps = Number((totalGraphemes / duration * 1e3).toFixed(1));
263
+ if (cps > maxCps) {
264
+ diagnostics.push({
265
+ cueId: cue.id,
266
+ rule: "max-cps",
267
+ severity: "warning",
268
+ message: `Reading speed of ${cps} CPS exceeds maximum limit of ${maxCps} CPS.`,
269
+ expected: maxCps,
270
+ actual: cps
271
+ });
272
+ }
273
+ }
274
+ }
275
+ return diagnostics;
276
+ }
277
+
278
+ export { DEFAULT_PROFILES, countGraphemes, getCueDuration, lintCues, msToTimecode, parseSRT, retime, shiftTimecodes, stringifySRT, stripFormattingTags, timecodeToMs };
279
+ //# sourceMappingURL=index.js.map
280
+ //# sourceMappingURL=index.js.map