@utilix-tech/sdk 0.22.0 → 0.24.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.
@@ -12,6 +12,7 @@
12
12
  * - env-parser : parseEnv, envToJson, envToExport, validateEnvKey
13
13
  * - env-diff-checker: diffEnvExample
14
14
  * - ndjson-formatter: validateNdjson, minifyNdjson, formatNdjson, ndjsonToJsonArray, jsonArrayToNdjson
15
+ * - subtitle-converter: parseSubtitleTimestamp, formatSubtitleTimestamp, detectSubtitleFormat, parseSubtitles, serializeSubtitles, convertSubtitles, shiftSubtitles
15
16
  */
16
17
  interface ValidationResult {
17
18
  valid: boolean;
@@ -210,7 +211,69 @@ declare function ndjsonToJsonArray(input: string): string | {
210
211
  declare function jsonArrayToNdjson(input: string): string | {
211
212
  error: string;
212
213
  };
214
+ type SubtitleFormat = "srt" | "vtt";
215
+ interface SubtitleCue {
216
+ index: number;
217
+ /** Cue identifier line, when the source had one (WebVTT only). */
218
+ id?: string;
219
+ /** Start time in seconds. */
220
+ start: number;
221
+ /** End time in seconds. */
222
+ end: number;
223
+ /** WebVTT cue settings that followed the timestamp (e.g. `line:0 align:start`). */
224
+ settings?: string;
225
+ text: string;
226
+ }
227
+ interface ParsedSubtitles {
228
+ format: SubtitleFormat;
229
+ cues: SubtitleCue[];
230
+ cueCount: number;
231
+ /** End time of the last cue, in seconds. */
232
+ duration: number;
233
+ warnings: string[];
234
+ }
235
+ interface ConvertedSubtitles {
236
+ format: SubtitleFormat;
237
+ sourceFormat: SubtitleFormat;
238
+ output: string;
239
+ cueCount: number;
240
+ duration: number;
241
+ warnings: string[];
242
+ }
243
+ /** Parse `HH:MM:SS,mmm`, `HH:MM:SS.mmm`, or WebVTT's `MM:SS.mmm` into seconds. */
244
+ declare function parseSubtitleTimestamp(raw: string): number | null;
245
+ /** Format seconds as `HH:MM:SS,mmm` (SRT) or `HH:MM:SS.mmm` (WebVTT). */
246
+ declare function formatSubtitleTimestamp(totalSeconds: number, format: SubtitleFormat): string;
247
+ /** Auto-detect the source format from the WEBVTT signature or timestamp separator. */
248
+ declare function detectSubtitleFormat(input: string): SubtitleFormat;
249
+ /**
250
+ * Parse an SRT or WebVTT file into normalised cues. The format is auto-detected
251
+ * unless you pass one. Malformed blocks are reported as warnings rather than
252
+ * throwing, so a mostly-good file still converts.
253
+ */
254
+ declare function parseSubtitles(input: string, format?: SubtitleFormat): ParsedSubtitles | {
255
+ error: string;
256
+ };
257
+ /** Serialise parsed cues back into SRT or WebVTT text. */
258
+ declare function serializeSubtitles(cues: SubtitleCue[], format: SubtitleFormat): string;
259
+ /**
260
+ * Convert a subtitle file to the target format. Converting WebVTT to SRT drops
261
+ * cue identifiers, cue settings, and NOTE/STYLE/REGION blocks, since SRT has no
262
+ * equivalent for any of them; each drop is reported as a warning.
263
+ */
264
+ declare function convertSubtitles(input: string, target: SubtitleFormat, sourceFormat?: SubtitleFormat): ConvertedSubtitles | {
265
+ error: string;
266
+ };
267
+ /**
268
+ * Shift every cue by a fixed offset in seconds, keeping the source format
269
+ * unless a target is given. Negative offsets clamp at zero, so a cue that would
270
+ * start before the video does starts at 00:00:00 instead.
271
+ */
272
+ declare function shiftSubtitles(input: string, offsetSeconds: number, target?: SubtitleFormat): ConvertedSubtitles | {
273
+ error: string;
274
+ };
213
275
 
276
+ type data_ConvertedSubtitles = ConvertedSubtitles;
214
277
  type data_CsvParseResult = CsvParseResult;
215
278
  type data_EnvDiffError = EnvDiffError;
216
279
  type data_EnvDiffResult = EnvDiffResult;
@@ -221,16 +284,22 @@ type data_NdjsonLineResult = NdjsonLineResult;
221
284
  type data_NdjsonValidateResult = NdjsonValidateResult;
222
285
  type data_ParseOptions = ParseOptions;
223
286
  type data_ParsedIni = ParsedIni;
287
+ type data_ParsedSubtitles = ParsedSubtitles;
288
+ type data_SubtitleCue = SubtitleCue;
289
+ type data_SubtitleFormat = SubtitleFormat;
224
290
  type data_TomlFormatResult = TomlFormatResult;
225
291
  type data_ValidationResult = ValidationResult;
226
292
  type data_XmlFormatResult = XmlFormatResult;
227
293
  type data_XmlToJsonOptions = XmlToJsonOptions;
294
+ declare const data_convertSubtitles: typeof convertSubtitles;
228
295
  declare const data_csvToJson: typeof csvToJson;
229
296
  declare const data_detectDelimiter: typeof detectDelimiter;
297
+ declare const data_detectSubtitleFormat: typeof detectSubtitleFormat;
230
298
  declare const data_diffEnvExample: typeof diffEnvExample;
231
299
  declare const data_envToExport: typeof envToExport;
232
300
  declare const data_envToJson: typeof envToJson;
233
301
  declare const data_formatNdjson: typeof formatNdjson;
302
+ declare const data_formatSubtitleTimestamp: typeof formatSubtitleTimestamp;
234
303
  declare const data_formatToml: typeof formatToml;
235
304
  declare const data_formatXml: typeof formatXml;
236
305
  declare const data_getSection: typeof getSection;
@@ -250,7 +319,11 @@ declare const data_ndjsonToJsonArray: typeof ndjsonToJsonArray;
250
319
  declare const data_parseCsv: typeof parseCsv;
251
320
  declare const data_parseEnv: typeof parseEnv;
252
321
  declare const data_parseIni: typeof parseIni;
322
+ declare const data_parseSubtitleTimestamp: typeof parseSubtitleTimestamp;
323
+ declare const data_parseSubtitles: typeof parseSubtitles;
253
324
  declare const data_parseXmlToObject: typeof parseXmlToObject;
325
+ declare const data_serializeSubtitles: typeof serializeSubtitles;
326
+ declare const data_shiftSubtitles: typeof shiftSubtitles;
254
327
  declare const data_stringifyIni: typeof stringifyIni;
255
328
  declare const data_toCsvString: typeof toCsvString;
256
329
  declare const data_tomlToJson: typeof tomlToJson;
@@ -262,7 +335,7 @@ declare const data_validateXml: typeof validateXml;
262
335
  declare const data_validateYaml: typeof validateYaml;
263
336
  declare const data_xmlToJson: typeof xmlToJson;
264
337
  declare namespace data {
265
- export { type data_CsvParseResult as CsvParseResult, type data_EnvDiffError as EnvDiffError, type data_EnvDiffResult as EnvDiffResult, type data_EnvEntry as EnvEntry, type data_EnvParseResult as EnvParseResult, type data_IniSection as IniSection, type data_NdjsonLineResult as NdjsonLineResult, type data_NdjsonValidateResult as NdjsonValidateResult, type data_ParseOptions as ParseOptions, type data_ParsedIni as ParsedIni, type data_TomlFormatResult as TomlFormatResult, type data_ValidationResult as ValidationResult, type data_XmlFormatResult as XmlFormatResult, type data_XmlToJsonOptions as XmlToJsonOptions, data_csvToJson as csvToJson, data_detectDelimiter as detectDelimiter, data_diffEnvExample as diffEnvExample, data_envToExport as envToExport, data_envToJson as envToJson, data_formatNdjson as formatNdjson, data_formatToml as formatToml, data_formatXml as formatXml, data_getSection as getSection, data_getSectionNames as getSectionNames, data_getTomlStats as getTomlStats, data_getXmlStats as getXmlStats, data_getYamlStats as getYamlStats, data_iniToJson as iniToJson, data_jsonArrayToNdjson as jsonArrayToNdjson, data_jsonToIni as jsonToIni, data_jsonToToml as jsonToToml, data_jsonToXml as jsonToXml, data_minifyNdjson as minifyNdjson, data_minifyToml as minifyToml, data_minifyXml as minifyXml, data_ndjsonToJsonArray as ndjsonToJsonArray, data_parseCsv as parseCsv, data_parseEnv as parseEnv, data_parseIni as parseIni, data_parseXmlToObject as parseXmlToObject, data_stringifyIni as stringifyIni, data_toCsvString as toCsvString, data_tomlToJson as tomlToJson, data_validateEnvKey as validateEnvKey, data_validateIni as validateIni, data_validateNdjson as validateNdjson, data_validateToml as validateToml, data_validateXml as validateXml, data_validateYaml as validateYaml, data_xmlToJson as xmlToJson };
338
+ export { type data_ConvertedSubtitles as ConvertedSubtitles, type data_CsvParseResult as CsvParseResult, type data_EnvDiffError as EnvDiffError, type data_EnvDiffResult as EnvDiffResult, type data_EnvEntry as EnvEntry, type data_EnvParseResult as EnvParseResult, type data_IniSection as IniSection, type data_NdjsonLineResult as NdjsonLineResult, type data_NdjsonValidateResult as NdjsonValidateResult, type data_ParseOptions as ParseOptions, type data_ParsedIni as ParsedIni, type data_ParsedSubtitles as ParsedSubtitles, type data_SubtitleCue as SubtitleCue, type data_SubtitleFormat as SubtitleFormat, type data_TomlFormatResult as TomlFormatResult, type data_ValidationResult as ValidationResult, type data_XmlFormatResult as XmlFormatResult, type data_XmlToJsonOptions as XmlToJsonOptions, data_convertSubtitles as convertSubtitles, data_csvToJson as csvToJson, data_detectDelimiter as detectDelimiter, data_detectSubtitleFormat as detectSubtitleFormat, data_diffEnvExample as diffEnvExample, data_envToExport as envToExport, data_envToJson as envToJson, data_formatNdjson as formatNdjson, data_formatSubtitleTimestamp as formatSubtitleTimestamp, data_formatToml as formatToml, data_formatXml as formatXml, data_getSection as getSection, data_getSectionNames as getSectionNames, data_getTomlStats as getTomlStats, data_getXmlStats as getXmlStats, data_getYamlStats as getYamlStats, data_iniToJson as iniToJson, data_jsonArrayToNdjson as jsonArrayToNdjson, data_jsonToIni as jsonToIni, data_jsonToToml as jsonToToml, data_jsonToXml as jsonToXml, data_minifyNdjson as minifyNdjson, data_minifyToml as minifyToml, data_minifyXml as minifyXml, data_ndjsonToJsonArray as ndjsonToJsonArray, data_parseCsv as parseCsv, data_parseEnv as parseEnv, data_parseIni as parseIni, data_parseSubtitleTimestamp as parseSubtitleTimestamp, data_parseSubtitles as parseSubtitles, data_parseXmlToObject as parseXmlToObject, data_serializeSubtitles as serializeSubtitles, data_shiftSubtitles as shiftSubtitles, data_stringifyIni as stringifyIni, data_toCsvString as toCsvString, data_tomlToJson as tomlToJson, data_validateEnvKey as validateEnvKey, data_validateIni as validateIni, data_validateNdjson as validateNdjson, data_validateToml as validateToml, data_validateXml as validateXml, data_validateYaml as validateYaml, data_xmlToJson as xmlToJson };
266
339
  }
267
340
 
268
- export { minifyToml as A, minifyXml as B, type CsvParseResult as C, ndjsonToJsonArray as D, type EnvDiffError as E, parseCsv as F, parseEnv as G, parseIni as H, type IniSection as I, parseXmlToObject as J, stringifyIni as K, toCsvString as L, tomlToJson as M, type NdjsonLineResult as N, validateEnvKey as O, type ParseOptions as P, validateIni as Q, validateNdjson as R, validateToml as S, type TomlFormatResult as T, validateXml as U, type ValidationResult as V, validateYaml as W, type XmlFormatResult as X, xmlToJson as Y, type EnvDiffResult as a, type EnvEntry as b, type EnvParseResult as c, data as d, type NdjsonValidateResult as e, type ParsedIni as f, type XmlToJsonOptions as g, csvToJson as h, detectDelimiter as i, diffEnvExample as j, envToExport as k, envToJson as l, formatNdjson as m, formatToml as n, formatXml as o, getSection as p, getSectionNames as q, getTomlStats as r, getXmlStats as s, getYamlStats as t, iniToJson as u, jsonArrayToNdjson as v, jsonToIni as w, jsonToToml as x, jsonToXml as y, minifyNdjson as z };
341
+ export { toCsvString as $, iniToJson as A, jsonArrayToNdjson as B, type ConvertedSubtitles as C, jsonToIni as D, type EnvDiffError as E, jsonToToml as F, jsonToXml as G, minifyNdjson as H, type IniSection as I, minifyToml as J, minifyXml as K, ndjsonToJsonArray as L, parseCsv as M, type NdjsonLineResult as N, parseEnv as O, type ParseOptions as P, parseIni as Q, parseSubtitleTimestamp as R, type SubtitleCue as S, type TomlFormatResult as T, parseSubtitles as U, type ValidationResult as V, parseXmlToObject as W, type XmlFormatResult as X, serializeSubtitles as Y, shiftSubtitles as Z, stringifyIni as _, type CsvParseResult as a, tomlToJson as a0, validateEnvKey as a1, validateIni as a2, validateNdjson as a3, validateToml as a4, validateXml as a5, validateYaml as a6, xmlToJson as a7, type EnvDiffResult as b, type EnvEntry as c, data as d, type EnvParseResult as e, type NdjsonValidateResult as f, type ParsedIni as g, type ParsedSubtitles as h, type SubtitleFormat as i, type XmlToJsonOptions as j, convertSubtitles as k, csvToJson as l, detectDelimiter as m, detectSubtitleFormat as n, diffEnvExample as o, envToExport as p, envToJson as q, formatNdjson as r, formatSubtitleTimestamp as s, formatToml as t, formatXml as u, getSection as v, getSectionNames as w, getTomlStats as x, getXmlStats as y, getYamlStats as z };
package/dist/index.cjs CHANGED
@@ -4467,12 +4467,15 @@ function detectFillerWords(text) {
4467
4467
  // src/tools/data.ts
4468
4468
  var data_exports = {};
4469
4469
  __export(data_exports, {
4470
+ convertSubtitles: () => convertSubtitles,
4470
4471
  csvToJson: () => csvToJson2,
4471
4472
  detectDelimiter: () => detectDelimiter2,
4473
+ detectSubtitleFormat: () => detectSubtitleFormat,
4472
4474
  diffEnvExample: () => diffEnvExample,
4473
4475
  envToExport: () => envToExport,
4474
4476
  envToJson: () => envToJson,
4475
4477
  formatNdjson: () => formatNdjson,
4478
+ formatSubtitleTimestamp: () => formatSubtitleTimestamp,
4476
4479
  formatToml: () => formatToml,
4477
4480
  formatXml: () => formatXml,
4478
4481
  getSection: () => getSection,
@@ -4492,7 +4495,11 @@ __export(data_exports, {
4492
4495
  parseCsv: () => parseCsv,
4493
4496
  parseEnv: () => parseEnv,
4494
4497
  parseIni: () => parseIni,
4498
+ parseSubtitleTimestamp: () => parseSubtitleTimestamp,
4499
+ parseSubtitles: () => parseSubtitles,
4495
4500
  parseXmlToObject: () => parseXmlToObject,
4501
+ serializeSubtitles: () => serializeSubtitles,
4502
+ shiftSubtitles: () => shiftSubtitles,
4496
4503
  stringifyIni: () => stringifyIni,
4497
4504
  toCsvString: () => toCsvString,
4498
4505
  tomlToJson: () => tomlToJson,
@@ -5619,6 +5626,176 @@ function jsonArrayToNdjson(input) {
5619
5626
  if (!Array.isArray(parsed)) return { error: "Input must be a JSON array" };
5620
5627
  return parsed.map((item) => JSON.stringify(item)).join("\n");
5621
5628
  }
5629
+ var _SUBTITLE_TIMESTAMP_RE = /(\d+):(\d{1,2}):(\d{1,2})[.,](\d{1,3})|(\d{1,3}):(\d{1,2})[.,](\d{1,3})/;
5630
+ var _VTT_BLOCK_KEYWORDS = ["NOTE", "STYLE", "REGION"];
5631
+ function parseSubtitleTimestamp(raw) {
5632
+ const m = _SUBTITLE_TIMESTAMP_RE.exec(raw.trim());
5633
+ if (!m || m.index !== 0) return null;
5634
+ let hours;
5635
+ let minutes;
5636
+ let seconds;
5637
+ let millis;
5638
+ if (m[1] !== void 0) {
5639
+ hours = Number(m[1]);
5640
+ minutes = Number(m[2]);
5641
+ seconds = Number(m[3]);
5642
+ millis = m[4];
5643
+ } else {
5644
+ hours = 0;
5645
+ minutes = Number(m[5]);
5646
+ seconds = Number(m[6]);
5647
+ millis = m[7];
5648
+ }
5649
+ if (minutes > 59 || seconds > 59) return null;
5650
+ return hours * 3600 + minutes * 60 + seconds + Number(millis.padEnd(3, "0")) / 1e3;
5651
+ }
5652
+ function formatSubtitleTimestamp(totalSeconds, format) {
5653
+ const totalMs = Math.round(Math.max(0, totalSeconds) * 1e3);
5654
+ const ms = totalMs % 1e3;
5655
+ const totalSecs = Math.floor(totalMs / 1e3);
5656
+ const pad = (n, width = 2) => String(n).padStart(width, "0");
5657
+ return `${pad(Math.floor(totalSecs / 3600))}:${pad(Math.floor(totalSecs / 60) % 60)}:${pad(totalSecs % 60)}${format === "srt" ? "," : "."}${pad(ms, 3)}`;
5658
+ }
5659
+ function _stripBom(input) {
5660
+ return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
5661
+ }
5662
+ function detectSubtitleFormat(input) {
5663
+ const text = _stripBom(input).trimStart();
5664
+ if (/^WEBVTT/.test(text)) return "vtt";
5665
+ const arrow = /(\d{1,3}):(\d{1,2})(?::(\d{1,2}))?([.,])(\d{1,3})\s*-->/.exec(text);
5666
+ if (arrow && arrow[4] === ".") return "vtt";
5667
+ return "srt";
5668
+ }
5669
+ function parseSubtitles(input, format) {
5670
+ if (typeof input !== "string" || input.trim().length === 0) {
5671
+ return { error: "Subtitle input is empty" };
5672
+ }
5673
+ const sourceFormat = format ?? detectSubtitleFormat(input);
5674
+ const normalised = _stripBom(input).replace(/\r\n?/g, "\n");
5675
+ const warnings = [];
5676
+ let body = normalised;
5677
+ if (sourceFormat === "vtt") {
5678
+ const headerMatch = /^WEBVTT[^\n]*\n?/.exec(body.trimStart());
5679
+ if (headerMatch) {
5680
+ body = body.trimStart().slice(headerMatch[0].length);
5681
+ } else {
5682
+ warnings.push('WebVTT input is missing its "WEBVTT" header line');
5683
+ }
5684
+ }
5685
+ const blocks = body.split(/\n[ \t]*\n+/).map((b) => b.trim()).filter((b) => b.length > 0);
5686
+ const cues = [];
5687
+ for (const block of blocks) {
5688
+ const lines = block.split("\n");
5689
+ const arrowIdx = lines.findIndex((l) => l.includes("-->"));
5690
+ if (arrowIdx === -1) {
5691
+ const keyword = _VTT_BLOCK_KEYWORDS.find((k) => lines[0] === k || lines[0].startsWith(`${k} `));
5692
+ if (sourceFormat === "vtt" && keyword) {
5693
+ warnings.push(`Dropped a WebVTT ${keyword} block, which has no SRT equivalent`);
5694
+ } else {
5695
+ warnings.push(`Skipped a block with no "-->" timing line: ${JSON.stringify(lines[0].slice(0, 40))}`);
5696
+ }
5697
+ continue;
5698
+ }
5699
+ const timingLine = lines[arrowIdx];
5700
+ const parts = timingLine.split("-->");
5701
+ const start = parseSubtitleTimestamp(parts[0] ?? "");
5702
+ const rightTrimmed = (parts[1] ?? "").trim();
5703
+ const end = parseSubtitleTimestamp(rightTrimmed);
5704
+ if (start === null || end === null) {
5705
+ warnings.push(`Skipped a cue with an unparseable timing line: ${JSON.stringify(timingLine.slice(0, 60))}`);
5706
+ continue;
5707
+ }
5708
+ const endMatch = _SUBTITLE_TIMESTAMP_RE.exec(rightTrimmed);
5709
+ const settings = endMatch ? rightTrimmed.slice(endMatch[0].length).trim() : "";
5710
+ const idLines = lines.slice(0, arrowIdx).filter((l) => l.trim().length > 0);
5711
+ const text = lines.slice(arrowIdx + 1).join("\n").trim();
5712
+ const cue = { index: cues.length + 1, start, end, text };
5713
+ if (sourceFormat === "vtt" && idLines.length > 0) cue.id = idLines[idLines.length - 1].trim();
5714
+ if (settings.length > 0) cue.settings = settings;
5715
+ if (end < start) warnings.push(`Cue ${cue.index} ends before it starts`);
5716
+ if (text.length === 0) warnings.push(`Cue ${cue.index} has no text`);
5717
+ if (cues.length > 0 && start < cues[cues.length - 1].end) {
5718
+ warnings.push(`Cue ${cue.index} starts before cue ${cue.index - 1} ends (overlapping cues)`);
5719
+ }
5720
+ cues.push(cue);
5721
+ }
5722
+ if (cues.length === 0) {
5723
+ return { error: "No subtitle cues found: check that the input is a valid SRT or WebVTT file" };
5724
+ }
5725
+ return {
5726
+ format: sourceFormat,
5727
+ cues,
5728
+ cueCount: cues.length,
5729
+ duration: Math.round(Math.max(...cues.map((c) => c.end)) * 1e3) / 1e3,
5730
+ warnings
5731
+ };
5732
+ }
5733
+ function serializeSubtitles(cues, format) {
5734
+ const blocks = cues.map((cue, i) => {
5735
+ const time = `${formatSubtitleTimestamp(cue.start, format)} --> ${formatSubtitleTimestamp(cue.end, format)}`;
5736
+ if (format === "srt") return `${i + 1}
5737
+ ${time}
5738
+ ${cue.text}`;
5739
+ const timing = cue.settings ? `${time} ${cue.settings}` : time;
5740
+ return cue.id ? `${cue.id}
5741
+ ${timing}
5742
+ ${cue.text}` : `${timing}
5743
+ ${cue.text}`;
5744
+ });
5745
+ const body = blocks.join("\n\n");
5746
+ return format === "vtt" ? `WEBVTT
5747
+
5748
+ ${body}
5749
+ ` : `${body}
5750
+ `;
5751
+ }
5752
+ function convertSubtitles(input, target, sourceFormat) {
5753
+ if (target !== "srt" && target !== "vtt") return { error: 'target must be "srt" or "vtt"' };
5754
+ const parsed = parseSubtitles(input, sourceFormat);
5755
+ if ("error" in parsed) return parsed;
5756
+ const warnings = [...parsed.warnings];
5757
+ if (target === "srt") {
5758
+ const droppedSettings = parsed.cues.filter((c) => c.settings).length;
5759
+ const droppedIds = parsed.cues.filter((c) => c.id).length;
5760
+ if (droppedSettings > 0) {
5761
+ warnings.push(`Dropped WebVTT cue settings on ${droppedSettings} cue(s): SRT has no equivalent`);
5762
+ }
5763
+ if (droppedIds > 0) {
5764
+ warnings.push(`Dropped ${droppedIds} WebVTT cue identifier(s): SRT numbers cues sequentially instead`);
5765
+ }
5766
+ }
5767
+ return {
5768
+ format: target,
5769
+ sourceFormat: parsed.format,
5770
+ output: serializeSubtitles(parsed.cues, target),
5771
+ cueCount: parsed.cueCount,
5772
+ duration: parsed.duration,
5773
+ warnings
5774
+ };
5775
+ }
5776
+ function shiftSubtitles(input, offsetSeconds, target) {
5777
+ if (!Number.isFinite(offsetSeconds)) return { error: "offsetSeconds must be a finite number" };
5778
+ const parsed = parseSubtitles(input);
5779
+ if ("error" in parsed) return parsed;
5780
+ const warnings = [...parsed.warnings];
5781
+ let clamped = 0;
5782
+ const shifted = parsed.cues.map((cue) => {
5783
+ const start = cue.start + offsetSeconds;
5784
+ const end = cue.end + offsetSeconds;
5785
+ if (start < 0) clamped += 1;
5786
+ return { ...cue, start: Math.max(0, start), end: Math.max(0, end) };
5787
+ });
5788
+ if (clamped > 0) warnings.push(`${clamped} cue(s) would start before zero and were clamped to 00:00:00`);
5789
+ const outFormat = target ?? parsed.format;
5790
+ return {
5791
+ format: outFormat,
5792
+ sourceFormat: parsed.format,
5793
+ output: serializeSubtitles(shifted, outFormat),
5794
+ cueCount: shifted.length,
5795
+ duration: Math.round(Math.max(...shifted.map((c) => c.end)) * 1e3) / 1e3,
5796
+ warnings
5797
+ };
5798
+ }
5622
5799
 
5623
5800
  // src/tools/generators.ts
5624
5801
  var generators_exports = {};
@@ -6924,12 +7101,15 @@ __export(units_exports, {
6924
7101
  COMMON_TAX_RATES: () => COMMON_TAX_RATES,
6925
7102
  COMMON_TIP_PERCENTAGES: () => COMMON_TIP_PERCENTAGES,
6926
7103
  COMMON_VIEWPORTS: () => COMMON_VIEWPORTS,
7104
+ applySuccessiveDiscounts: () => applySuccessiveDiscounts,
6927
7105
  autoFormat: () => autoFormat,
6928
7106
  bumpVersion: () => bumpVersion,
6929
7107
  calcAspectRatio: () => calcAspectRatio,
6930
7108
  calcSubnet: () => calcSubnet,
6931
7109
  calculateCompoundInterest: () => calculateCompoundInterest,
7110
+ calculateDiscount: () => calculateDiscount,
6932
7111
  calculateLoan: () => calculateLoan,
7112
+ calculateMarginMarkup: () => calculateMarginMarkup,
6933
7113
  calculatePercentageChange: () => calculatePercentageChange,
6934
7114
  calculateSimpleInterest: () => calculateSimpleInterest,
6935
7115
  calculateTax: () => calculateTax,
@@ -6962,6 +7142,8 @@ __export(units_exports, {
6962
7142
  isValidCidr: () => isValidCidr,
6963
7143
  isValidIp: () => isValidIp,
6964
7144
  isValidIpv4: () => isValidIpv4,
7145
+ marginToMarkup: () => marginToMarkup,
7146
+ markupToMargin: () => markupToMargin,
6965
7147
  octalToPermissions: () => octalToPermissions,
6966
7148
  parseFileSize: () => parseFileSize,
6967
7149
  parseInput: () => parseInput,
@@ -8304,6 +8486,155 @@ function convertAprApy(rate, compoundsPerYear, direction) {
8304
8486
  difference: round6(apy - apr)
8305
8487
  };
8306
8488
  }
8489
+ var _MARGIN_FIELDS = ["cost", "price", "marginPercent", "markupPercent"];
8490
+ function calculateMarginMarkup(input) {
8491
+ const provided = (v) => typeof v === "number" && Number.isFinite(v);
8492
+ const given = _MARGIN_FIELDS.filter((f) => provided(input[f]));
8493
+ if (given.length !== 2) {
8494
+ return { error: `Provide exactly two of cost, price, marginPercent, markupPercent (got ${given.length})` };
8495
+ }
8496
+ const has = (f) => given.includes(f);
8497
+ const cost0 = input.cost;
8498
+ const price0 = input.price;
8499
+ const margin0 = input.marginPercent;
8500
+ const markup0 = input.markupPercent;
8501
+ if (has("cost") && cost0 <= 0) return { error: "Cost must be greater than zero" };
8502
+ if (has("price") && price0 <= 0) return { error: "Price must be greater than zero" };
8503
+ if (has("marginPercent") && margin0 >= 100) {
8504
+ return { error: "Margin percent must be below 100 (a 100% margin implies a zero cost)" };
8505
+ }
8506
+ if (has("markupPercent") && markup0 <= -100) {
8507
+ return { error: "Markup percent must be greater than -100" };
8508
+ }
8509
+ let cost;
8510
+ let price;
8511
+ if (has("cost") && has("price")) {
8512
+ cost = cost0;
8513
+ price = price0;
8514
+ } else if (has("cost") && has("marginPercent")) {
8515
+ cost = cost0;
8516
+ price = cost0 / (1 - margin0 / 100);
8517
+ } else if (has("cost") && has("markupPercent")) {
8518
+ cost = cost0;
8519
+ price = cost0 * (1 + markup0 / 100);
8520
+ } else if (has("price") && has("marginPercent")) {
8521
+ price = price0;
8522
+ cost = price0 * (1 - margin0 / 100);
8523
+ } else if (has("price") && has("markupPercent")) {
8524
+ price = price0;
8525
+ cost = price0 / (1 + markup0 / 100);
8526
+ } else {
8527
+ return { error: "Margin and markup alone cannot be solved: also provide a cost or a price" };
8528
+ }
8529
+ if (!Number.isFinite(cost) || !Number.isFinite(price)) return { error: "Inputs produce a non-finite result" };
8530
+ if (cost <= 0) return { error: "Those inputs imply a cost of zero or less" };
8531
+ if (price <= 0) return { error: "Those inputs imply a price of zero or less" };
8532
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
8533
+ const profit = price - cost;
8534
+ return {
8535
+ cost: round4(cost),
8536
+ price: round4(price),
8537
+ profit: round4(profit),
8538
+ marginPercent: round4(profit / price * 100),
8539
+ markupPercent: round4(profit / cost * 100),
8540
+ given,
8541
+ solvedFor: _MARGIN_FIELDS.filter((f) => !given.includes(f))
8542
+ };
8543
+ }
8544
+ function marginToMarkup(marginPercent) {
8545
+ if (!Number.isFinite(marginPercent) || marginPercent >= 100) {
8546
+ return { error: "Margin percent must be a finite number below 100" };
8547
+ }
8548
+ return Math.round(marginPercent / (100 - marginPercent) * 100 * 1e4) / 1e4;
8549
+ }
8550
+ function markupToMargin(markupPercent) {
8551
+ if (!Number.isFinite(markupPercent) || markupPercent <= -100) {
8552
+ return { error: "Markup percent must be a finite number greater than -100" };
8553
+ }
8554
+ return Math.round(markupPercent / (100 + markupPercent) * 100 * 1e4) / 1e4;
8555
+ }
8556
+ var _DISCOUNT_FIELDS = ["discountPercent", "discountAmount", "finalPrice"];
8557
+ function calculateDiscount(input) {
8558
+ const provided = (v) => typeof v === "number" && Number.isFinite(v);
8559
+ if (!provided(input.originalPrice)) {
8560
+ return { error: "originalPrice is required and must be a finite number" };
8561
+ }
8562
+ const originalPrice = input.originalPrice;
8563
+ if (originalPrice <= 0) return { error: "Original price must be greater than zero" };
8564
+ const given = _DISCOUNT_FIELDS.filter((f) => provided(input[f]));
8565
+ if (given.length !== 1) {
8566
+ return { error: `Provide exactly one of discountPercent, discountAmount, finalPrice (got ${given.length})` };
8567
+ }
8568
+ const field = given[0];
8569
+ let discountAmount;
8570
+ if (field === "discountPercent") {
8571
+ const pct = input.discountPercent;
8572
+ if (pct < 0) return { error: "Discount percent cannot be negative" };
8573
+ if (pct > 100) return { error: "Discount percent cannot exceed 100" };
8574
+ discountAmount = originalPrice * (pct / 100);
8575
+ } else if (field === "discountAmount") {
8576
+ const amt = input.discountAmount;
8577
+ if (amt < 0) return { error: "Discount amount cannot be negative" };
8578
+ if (amt > originalPrice) return { error: "Discount amount cannot exceed the original price" };
8579
+ discountAmount = amt;
8580
+ } else {
8581
+ const fin = input.finalPrice;
8582
+ if (fin < 0) return { error: "Final price cannot be negative" };
8583
+ if (fin > originalPrice) {
8584
+ return { error: "Final price cannot exceed the original price: that is a price increase, not a discount" };
8585
+ }
8586
+ discountAmount = originalPrice - fin;
8587
+ }
8588
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
8589
+ const finalPrice = originalPrice - discountAmount;
8590
+ return {
8591
+ originalPrice: round4(originalPrice),
8592
+ finalPrice: round4(finalPrice),
8593
+ discountAmount: round4(discountAmount),
8594
+ discountPercent: round4(discountAmount / originalPrice * 100),
8595
+ given: field,
8596
+ solvedFor: _DISCOUNT_FIELDS.filter((f) => f !== field)
8597
+ };
8598
+ }
8599
+ function applySuccessiveDiscounts(originalPrice, discountPercents) {
8600
+ if (!Number.isFinite(originalPrice) || originalPrice <= 0) {
8601
+ return { error: "Original price must be a finite number greater than zero" };
8602
+ }
8603
+ if (!Array.isArray(discountPercents) || discountPercents.length === 0) {
8604
+ return { error: "Provide at least one discount percent" };
8605
+ }
8606
+ if (discountPercents.length > 20) {
8607
+ return { error: "Provide at most 20 successive discounts" };
8608
+ }
8609
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
8610
+ const steps = [];
8611
+ let running = originalPrice;
8612
+ let sumOfDiscountPercents = 0;
8613
+ for (const pct of discountPercents) {
8614
+ if (!Number.isFinite(pct)) return { error: "Every discount percent must be a finite number" };
8615
+ if (pct < 0) return { error: "Discount percent cannot be negative" };
8616
+ if (pct > 100) return { error: "Discount percent cannot exceed 100" };
8617
+ const priceBefore = running;
8618
+ const saved = priceBefore * (pct / 100);
8619
+ running = priceBefore - saved;
8620
+ sumOfDiscountPercents += pct;
8621
+ steps.push({
8622
+ discountPercent: round4(pct),
8623
+ priceBefore: round4(priceBefore),
8624
+ priceAfter: round4(running),
8625
+ saved: round4(saved)
8626
+ });
8627
+ }
8628
+ const totalSaved = originalPrice - running;
8629
+ return {
8630
+ originalPrice: round4(originalPrice),
8631
+ finalPrice: round4(running),
8632
+ totalSaved: round4(totalSaved),
8633
+ effectiveDiscountPercent: round4(totalSaved / originalPrice * 100),
8634
+ sumOfDiscountPercents: round4(sumOfDiscountPercents),
8635
+ steps
8636
+ };
8637
+ }
8307
8638
 
8308
8639
  // src/tools/network.ts
8309
8640
  var network_exports = {};
package/dist/index.d.cts CHANGED
@@ -2,10 +2,10 @@ export { j as json } from './json-DEVFIpsC.cjs';
2
2
  export { e as encoding } from './encoding-Dk3wpmb3.cjs';
3
3
  export { h as hashing } from './hashing-CnetQFD_.cjs';
4
4
  export { t as text } from './text-DS70aMY5.cjs';
5
- export { d as data } from './data-D8XTI7Du.cjs';
5
+ export { d as data } from './data-XRb0D7Zn.cjs';
6
6
  export { g as generators } from './generators-BGtRGpJZ.cjs';
7
7
  export { t as time } from './time-KqJjdqII.cjs';
8
- export { u as units } from './units-KrjRyu0w.cjs';
8
+ export { u as units } from './units-DAoSM2g8.cjs';
9
9
  export { n as network } from './network-BJOjJnmY.cjs';
10
10
  export { a as api } from './api-Xl7OzIgq.cjs';
11
11
  export { c as code } from './code-DaRvff2v.cjs';
package/dist/index.d.ts CHANGED
@@ -2,10 +2,10 @@ export { j as json } from './json-DEVFIpsC.js';
2
2
  export { e as encoding } from './encoding-Dk3wpmb3.js';
3
3
  export { h as hashing } from './hashing-CnetQFD_.js';
4
4
  export { t as text } from './text-DS70aMY5.js';
5
- export { d as data } from './data-D8XTI7Du.js';
5
+ export { d as data } from './data-XRb0D7Zn.js';
6
6
  export { g as generators } from './generators-BGtRGpJZ.js';
7
7
  export { t as time } from './time-KqJjdqII.js';
8
- export { u as units } from './units-KrjRyu0w.js';
8
+ export { u as units } from './units-DAoSM2g8.js';
9
9
  export { n as network } from './network-BJOjJnmY.js';
10
10
  export { a as api } from './api-Xl7OzIgq.js';
11
11
  export { c as code } from './code-DaRvff2v.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { media_exports as media } from './chunk-XMKJYVKB.js';
2
- export { units_exports as units } from './chunk-TLRSQHMP.js';
2
+ export { units_exports as units } from './chunk-HZIS2DMJ.js';
3
3
  export { network_exports as network } from './chunk-Y5OOP6WN.js';
4
4
  export { api_exports as api } from './chunk-Q3B3YWOA.js';
5
5
  export { code_exports as code } from './chunk-LXKIPPDG.js';
@@ -11,7 +11,7 @@ export { json_exports as json } from './chunk-WO3LJMVV.js';
11
11
  export { encoding_exports as encoding } from './chunk-DOWQZUTP.js';
12
12
  export { hashing_exports as hashing } from './chunk-GEFVMGZR.js';
13
13
  export { text_exports as text } from './chunk-2I4O5YCT.js';
14
- export { data_exports as data } from './chunk-TINQFTLO.js';
14
+ export { data_exports as data } from './chunk-66QHV5KW.js';
15
15
  export { generators_exports as generators } from './chunk-2CJSLYWI.js';
16
16
  export { time_exports as time } from './chunk-IEPWZERQ.js';
17
17
  import './chunk-MLKGABMK.js';