@utilix-tech/sdk 0.23.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.
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,11 +7101,13 @@ __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,
6933
7112
  calculateMarginMarkup: () => calculateMarginMarkup,
6934
7113
  calculatePercentageChange: () => calculatePercentageChange,
@@ -8374,6 +8553,88 @@ function markupToMargin(markupPercent) {
8374
8553
  }
8375
8554
  return Math.round(markupPercent / (100 + markupPercent) * 100 * 1e4) / 1e4;
8376
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
+ }
8377
8638
 
8378
8639
  // src/tools/network.ts
8379
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-h4rHQGp5.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-h4rHQGp5.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-AIKSKJ77.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';
@@ -1139,13 +1139,186 @@ function jsonArrayToNdjson(input) {
1139
1139
  if (!Array.isArray(parsed)) return { error: "Input must be a JSON array" };
1140
1140
  return parsed.map((item) => JSON.stringify(item)).join("\n");
1141
1141
  }
1142
+ var _SUBTITLE_TIMESTAMP_RE = /(\d+):(\d{1,2}):(\d{1,2})[.,](\d{1,3})|(\d{1,3}):(\d{1,2})[.,](\d{1,3})/;
1143
+ var _VTT_BLOCK_KEYWORDS = ["NOTE", "STYLE", "REGION"];
1144
+ function parseSubtitleTimestamp(raw) {
1145
+ const m = _SUBTITLE_TIMESTAMP_RE.exec(raw.trim());
1146
+ if (!m || m.index !== 0) return null;
1147
+ let hours;
1148
+ let minutes;
1149
+ let seconds;
1150
+ let millis;
1151
+ if (m[1] !== void 0) {
1152
+ hours = Number(m[1]);
1153
+ minutes = Number(m[2]);
1154
+ seconds = Number(m[3]);
1155
+ millis = m[4];
1156
+ } else {
1157
+ hours = 0;
1158
+ minutes = Number(m[5]);
1159
+ seconds = Number(m[6]);
1160
+ millis = m[7];
1161
+ }
1162
+ if (minutes > 59 || seconds > 59) return null;
1163
+ return hours * 3600 + minutes * 60 + seconds + Number(millis.padEnd(3, "0")) / 1e3;
1164
+ }
1165
+ function formatSubtitleTimestamp(totalSeconds, format) {
1166
+ const totalMs = Math.round(Math.max(0, totalSeconds) * 1e3);
1167
+ const ms = totalMs % 1e3;
1168
+ const totalSecs = Math.floor(totalMs / 1e3);
1169
+ const pad = (n, width = 2) => String(n).padStart(width, "0");
1170
+ return `${pad(Math.floor(totalSecs / 3600))}:${pad(Math.floor(totalSecs / 60) % 60)}:${pad(totalSecs % 60)}${format === "srt" ? "," : "."}${pad(ms, 3)}`;
1171
+ }
1172
+ function _stripBom(input) {
1173
+ return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
1174
+ }
1175
+ function detectSubtitleFormat(input) {
1176
+ const text = _stripBom(input).trimStart();
1177
+ if (/^WEBVTT/.test(text)) return "vtt";
1178
+ const arrow = /(\d{1,3}):(\d{1,2})(?::(\d{1,2}))?([.,])(\d{1,3})\s*-->/.exec(text);
1179
+ if (arrow && arrow[4] === ".") return "vtt";
1180
+ return "srt";
1181
+ }
1182
+ function parseSubtitles(input, format) {
1183
+ if (typeof input !== "string" || input.trim().length === 0) {
1184
+ return { error: "Subtitle input is empty" };
1185
+ }
1186
+ const sourceFormat = format ?? detectSubtitleFormat(input);
1187
+ const normalised = _stripBom(input).replace(/\r\n?/g, "\n");
1188
+ const warnings = [];
1189
+ let body = normalised;
1190
+ if (sourceFormat === "vtt") {
1191
+ const headerMatch = /^WEBVTT[^\n]*\n?/.exec(body.trimStart());
1192
+ if (headerMatch) {
1193
+ body = body.trimStart().slice(headerMatch[0].length);
1194
+ } else {
1195
+ warnings.push('WebVTT input is missing its "WEBVTT" header line');
1196
+ }
1197
+ }
1198
+ const blocks = body.split(/\n[ \t]*\n+/).map((b) => b.trim()).filter((b) => b.length > 0);
1199
+ const cues = [];
1200
+ for (const block of blocks) {
1201
+ const lines = block.split("\n");
1202
+ const arrowIdx = lines.findIndex((l) => l.includes("-->"));
1203
+ if (arrowIdx === -1) {
1204
+ const keyword = _VTT_BLOCK_KEYWORDS.find((k) => lines[0] === k || lines[0].startsWith(`${k} `));
1205
+ if (sourceFormat === "vtt" && keyword) {
1206
+ warnings.push(`Dropped a WebVTT ${keyword} block, which has no SRT equivalent`);
1207
+ } else {
1208
+ warnings.push(`Skipped a block with no "-->" timing line: ${JSON.stringify(lines[0].slice(0, 40))}`);
1209
+ }
1210
+ continue;
1211
+ }
1212
+ const timingLine = lines[arrowIdx];
1213
+ const parts = timingLine.split("-->");
1214
+ const start = parseSubtitleTimestamp(parts[0] ?? "");
1215
+ const rightTrimmed = (parts[1] ?? "").trim();
1216
+ const end = parseSubtitleTimestamp(rightTrimmed);
1217
+ if (start === null || end === null) {
1218
+ warnings.push(`Skipped a cue with an unparseable timing line: ${JSON.stringify(timingLine.slice(0, 60))}`);
1219
+ continue;
1220
+ }
1221
+ const endMatch = _SUBTITLE_TIMESTAMP_RE.exec(rightTrimmed);
1222
+ const settings = endMatch ? rightTrimmed.slice(endMatch[0].length).trim() : "";
1223
+ const idLines = lines.slice(0, arrowIdx).filter((l) => l.trim().length > 0);
1224
+ const text = lines.slice(arrowIdx + 1).join("\n").trim();
1225
+ const cue = { index: cues.length + 1, start, end, text };
1226
+ if (sourceFormat === "vtt" && idLines.length > 0) cue.id = idLines[idLines.length - 1].trim();
1227
+ if (settings.length > 0) cue.settings = settings;
1228
+ if (end < start) warnings.push(`Cue ${cue.index} ends before it starts`);
1229
+ if (text.length === 0) warnings.push(`Cue ${cue.index} has no text`);
1230
+ if (cues.length > 0 && start < cues[cues.length - 1].end) {
1231
+ warnings.push(`Cue ${cue.index} starts before cue ${cue.index - 1} ends (overlapping cues)`);
1232
+ }
1233
+ cues.push(cue);
1234
+ }
1235
+ if (cues.length === 0) {
1236
+ return { error: "No subtitle cues found: check that the input is a valid SRT or WebVTT file" };
1237
+ }
1238
+ return {
1239
+ format: sourceFormat,
1240
+ cues,
1241
+ cueCount: cues.length,
1242
+ duration: Math.round(Math.max(...cues.map((c) => c.end)) * 1e3) / 1e3,
1243
+ warnings
1244
+ };
1245
+ }
1246
+ function serializeSubtitles(cues, format) {
1247
+ const blocks = cues.map((cue, i) => {
1248
+ const time = `${formatSubtitleTimestamp(cue.start, format)} --> ${formatSubtitleTimestamp(cue.end, format)}`;
1249
+ if (format === "srt") return `${i + 1}
1250
+ ${time}
1251
+ ${cue.text}`;
1252
+ const timing = cue.settings ? `${time} ${cue.settings}` : time;
1253
+ return cue.id ? `${cue.id}
1254
+ ${timing}
1255
+ ${cue.text}` : `${timing}
1256
+ ${cue.text}`;
1257
+ });
1258
+ const body = blocks.join("\n\n");
1259
+ return format === "vtt" ? `WEBVTT
1260
+
1261
+ ${body}
1262
+ ` : `${body}
1263
+ `;
1264
+ }
1265
+ function convertSubtitles(input, target, sourceFormat) {
1266
+ if (target !== "srt" && target !== "vtt") return { error: 'target must be "srt" or "vtt"' };
1267
+ const parsed = parseSubtitles(input, sourceFormat);
1268
+ if ("error" in parsed) return parsed;
1269
+ const warnings = [...parsed.warnings];
1270
+ if (target === "srt") {
1271
+ const droppedSettings = parsed.cues.filter((c) => c.settings).length;
1272
+ const droppedIds = parsed.cues.filter((c) => c.id).length;
1273
+ if (droppedSettings > 0) {
1274
+ warnings.push(`Dropped WebVTT cue settings on ${droppedSettings} cue(s): SRT has no equivalent`);
1275
+ }
1276
+ if (droppedIds > 0) {
1277
+ warnings.push(`Dropped ${droppedIds} WebVTT cue identifier(s): SRT numbers cues sequentially instead`);
1278
+ }
1279
+ }
1280
+ return {
1281
+ format: target,
1282
+ sourceFormat: parsed.format,
1283
+ output: serializeSubtitles(parsed.cues, target),
1284
+ cueCount: parsed.cueCount,
1285
+ duration: parsed.duration,
1286
+ warnings
1287
+ };
1288
+ }
1289
+ function shiftSubtitles(input, offsetSeconds, target) {
1290
+ if (!Number.isFinite(offsetSeconds)) return { error: "offsetSeconds must be a finite number" };
1291
+ const parsed = parseSubtitles(input);
1292
+ if ("error" in parsed) return parsed;
1293
+ const warnings = [...parsed.warnings];
1294
+ let clamped = 0;
1295
+ const shifted = parsed.cues.map((cue) => {
1296
+ const start = cue.start + offsetSeconds;
1297
+ const end = cue.end + offsetSeconds;
1298
+ if (start < 0) clamped += 1;
1299
+ return { ...cue, start: Math.max(0, start), end: Math.max(0, end) };
1300
+ });
1301
+ if (clamped > 0) warnings.push(`${clamped} cue(s) would start before zero and were clamped to 00:00:00`);
1302
+ const outFormat = target ?? parsed.format;
1303
+ return {
1304
+ format: outFormat,
1305
+ sourceFormat: parsed.format,
1306
+ output: serializeSubtitles(shifted, outFormat),
1307
+ cueCount: shifted.length,
1308
+ duration: Math.round(Math.max(...shifted.map((c) => c.end)) * 1e3) / 1e3,
1309
+ warnings
1310
+ };
1311
+ }
1142
1312
 
1313
+ exports.convertSubtitles = convertSubtitles;
1143
1314
  exports.csvToJson = csvToJson;
1144
1315
  exports.detectDelimiter = detectDelimiter;
1316
+ exports.detectSubtitleFormat = detectSubtitleFormat;
1145
1317
  exports.diffEnvExample = diffEnvExample;
1146
1318
  exports.envToExport = envToExport;
1147
1319
  exports.envToJson = envToJson;
1148
1320
  exports.formatNdjson = formatNdjson;
1321
+ exports.formatSubtitleTimestamp = formatSubtitleTimestamp;
1149
1322
  exports.formatToml = formatToml;
1150
1323
  exports.formatXml = formatXml;
1151
1324
  exports.getSection = getSection;
@@ -1165,7 +1338,11 @@ exports.ndjsonToJsonArray = ndjsonToJsonArray;
1165
1338
  exports.parseCsv = parseCsv;
1166
1339
  exports.parseEnv = parseEnv;
1167
1340
  exports.parseIni = parseIni;
1341
+ exports.parseSubtitleTimestamp = parseSubtitleTimestamp;
1342
+ exports.parseSubtitles = parseSubtitles;
1168
1343
  exports.parseXmlToObject = parseXmlToObject;
1344
+ exports.serializeSubtitles = serializeSubtitles;
1345
+ exports.shiftSubtitles = shiftSubtitles;
1169
1346
  exports.stringifyIni = stringifyIni;
1170
1347
  exports.toCsvString = toCsvString;
1171
1348
  exports.tomlToJson = tomlToJson;
@@ -1 +1 @@
1
- export { C as CsvParseResult, E as EnvDiffError, a as EnvDiffResult, b as EnvEntry, c as EnvParseResult, I as IniSection, N as NdjsonLineResult, e as NdjsonValidateResult, P as ParseOptions, f as ParsedIni, T as TomlFormatResult, V as ValidationResult, X as XmlFormatResult, g as XmlToJsonOptions, h as csvToJson, i as detectDelimiter, j as diffEnvExample, k as envToExport, l as envToJson, m as formatNdjson, n as formatToml, o as formatXml, p as getSection, q as getSectionNames, r as getTomlStats, s as getXmlStats, t as getYamlStats, u as iniToJson, v as jsonArrayToNdjson, w as jsonToIni, x as jsonToToml, y as jsonToXml, z as minifyNdjson, A as minifyToml, B as minifyXml, D as ndjsonToJsonArray, F as parseCsv, G as parseEnv, H as parseIni, J as parseXmlToObject, K as stringifyIni, L as toCsvString, M as tomlToJson, O as validateEnvKey, Q as validateIni, R as validateNdjson, S as validateToml, U as validateXml, W as validateYaml, Y as xmlToJson } from '../data-D8XTI7Du.cjs';
1
+ export { C as ConvertedSubtitles, a as CsvParseResult, E as EnvDiffError, b as EnvDiffResult, c as EnvEntry, e as EnvParseResult, I as IniSection, N as NdjsonLineResult, f as NdjsonValidateResult, P as ParseOptions, g as ParsedIni, h as ParsedSubtitles, S as SubtitleCue, i as SubtitleFormat, T as TomlFormatResult, V as ValidationResult, X as XmlFormatResult, j as XmlToJsonOptions, k as convertSubtitles, l as csvToJson, m as detectDelimiter, n as detectSubtitleFormat, o as diffEnvExample, p as envToExport, q as envToJson, r as formatNdjson, s as formatSubtitleTimestamp, t as formatToml, u as formatXml, v as getSection, w as getSectionNames, x as getTomlStats, y as getXmlStats, z as getYamlStats, A as iniToJson, B as jsonArrayToNdjson, D as jsonToIni, F as jsonToToml, G as jsonToXml, H as minifyNdjson, J as minifyToml, K as minifyXml, L as ndjsonToJsonArray, M as parseCsv, O as parseEnv, Q as parseIni, R as parseSubtitleTimestamp, U as parseSubtitles, W as parseXmlToObject, Y as serializeSubtitles, Z as shiftSubtitles, _ as stringifyIni, $ as toCsvString, a0 as tomlToJson, a1 as validateEnvKey, a2 as validateIni, a3 as validateNdjson, a4 as validateToml, a5 as validateXml, a6 as validateYaml, a7 as xmlToJson } from '../data-XRb0D7Zn.cjs';
@@ -1 +1 @@
1
- export { C as CsvParseResult, E as EnvDiffError, a as EnvDiffResult, b as EnvEntry, c as EnvParseResult, I as IniSection, N as NdjsonLineResult, e as NdjsonValidateResult, P as ParseOptions, f as ParsedIni, T as TomlFormatResult, V as ValidationResult, X as XmlFormatResult, g as XmlToJsonOptions, h as csvToJson, i as detectDelimiter, j as diffEnvExample, k as envToExport, l as envToJson, m as formatNdjson, n as formatToml, o as formatXml, p as getSection, q as getSectionNames, r as getTomlStats, s as getXmlStats, t as getYamlStats, u as iniToJson, v as jsonArrayToNdjson, w as jsonToIni, x as jsonToToml, y as jsonToXml, z as minifyNdjson, A as minifyToml, B as minifyXml, D as ndjsonToJsonArray, F as parseCsv, G as parseEnv, H as parseIni, J as parseXmlToObject, K as stringifyIni, L as toCsvString, M as tomlToJson, O as validateEnvKey, Q as validateIni, R as validateNdjson, S as validateToml, U as validateXml, W as validateYaml, Y as xmlToJson } from '../data-D8XTI7Du.js';
1
+ export { C as ConvertedSubtitles, a as CsvParseResult, E as EnvDiffError, b as EnvDiffResult, c as EnvEntry, e as EnvParseResult, I as IniSection, N as NdjsonLineResult, f as NdjsonValidateResult, P as ParseOptions, g as ParsedIni, h as ParsedSubtitles, S as SubtitleCue, i as SubtitleFormat, T as TomlFormatResult, V as ValidationResult, X as XmlFormatResult, j as XmlToJsonOptions, k as convertSubtitles, l as csvToJson, m as detectDelimiter, n as detectSubtitleFormat, o as diffEnvExample, p as envToExport, q as envToJson, r as formatNdjson, s as formatSubtitleTimestamp, t as formatToml, u as formatXml, v as getSection, w as getSectionNames, x as getTomlStats, y as getXmlStats, z as getYamlStats, A as iniToJson, B as jsonArrayToNdjson, D as jsonToIni, F as jsonToToml, G as jsonToXml, H as minifyNdjson, J as minifyToml, K as minifyXml, L as ndjsonToJsonArray, M as parseCsv, O as parseEnv, Q as parseIni, R as parseSubtitleTimestamp, U as parseSubtitles, W as parseXmlToObject, Y as serializeSubtitles, Z as shiftSubtitles, _ as stringifyIni, $ as toCsvString, a0 as tomlToJson, a1 as validateEnvKey, a2 as validateIni, a3 as validateNdjson, a4 as validateToml, a5 as validateXml, a6 as validateYaml, a7 as xmlToJson } from '../data-XRb0D7Zn.js';
@@ -1,2 +1,2 @@
1
- export { csvToJson, detectDelimiter, diffEnvExample, envToExport, envToJson, formatNdjson, formatToml, formatXml, getSection, getSectionNames, getTomlStats, getXmlStats, getYamlStats, iniToJson, jsonArrayToNdjson, jsonToIni, jsonToToml, jsonToXml, minifyNdjson, minifyToml, minifyXml, ndjsonToJsonArray, parseCsv, parseEnv, parseIni, parseXmlToObject, stringifyIni, toCsvString, tomlToJson, validateEnvKey, validateIni, validateNdjson, validateToml, validateXml, validateYaml, xmlToJson } from '../chunk-TINQFTLO.js';
1
+ export { convertSubtitles, csvToJson, detectDelimiter, detectSubtitleFormat, diffEnvExample, envToExport, envToJson, formatNdjson, formatSubtitleTimestamp, formatToml, formatXml, getSection, getSectionNames, getTomlStats, getXmlStats, getYamlStats, iniToJson, jsonArrayToNdjson, jsonToIni, jsonToToml, jsonToXml, minifyNdjson, minifyToml, minifyXml, ndjsonToJsonArray, parseCsv, parseEnv, parseIni, parseSubtitleTimestamp, parseSubtitles, parseXmlToObject, serializeSubtitles, shiftSubtitles, stringifyIni, toCsvString, tomlToJson, validateEnvKey, validateIni, validateNdjson, validateToml, validateXml, validateYaml, xmlToJson } from '../chunk-66QHV5KW.js';
2
2
  import '../chunk-MLKGABMK.js';