@gondola-lyrics/kit 0.5.0 → 0.6.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/formats.js CHANGED
@@ -1,5 +1,55 @@
1
- import { i as mapShared, n as lowerTag, r as lowerBound } from "./language.js";
1
+ import { a as lowerBound, i as lowerTag, o as mapShared, r as trimRuleWhitespace } from "./rules.js";
2
2
  import { AgentType, CreditRole, LineType, LyricStatus, Timing, WordType, canonicalizeLyric, deriveLanguageUsages, deriveLineRomans, getAnnotationItemText, getContentText, getWordsText, isWordLevelLine, makeAgent, makeLineAnnotation, makeLineAnnotationRoman, makeLineAnnotationTranslation, makeLineBackground, makeLineContentText, makeLineContentWords, makeLineInstrumental, makeLineNormal, makeLyric, makeMeta, makeMetaCredit, makeText, makeTimeRange, makeUnknownEntry, makeWordAnnotation, makeWordAnnotationRoman, makeWordAnnotationRuby, makeWordAnnotationToken, makeWordNormal, makeWordSpace, removeOffset, resolveAnnotationItemTime, resolveAnnotationTokenTime, validateLyric } from "@gondola-lyrics/model/runtime";
3
+ //#region src/formats/krc/common.ts
4
+ var LINE_PATTERN$3 = /^\[(\d+),(\d+)\]/;
5
+ var WORD_PATTERN$3 = /<(\d+,\d+,\d+)>/g;
6
+ /**
7
+ * Reads decrypted plaintext without unwrapping XML or accepting auxiliary tracks.
8
+ *
9
+ * @returns Original content, or null when the input cannot be read as plaintext KRC.
10
+ */
11
+ var readKrcContent = (input) => {
12
+ const source = typeof input === "string" ? { original: input } : input;
13
+ if (!source || typeof source !== "object" || Array.isArray(source)) return null;
14
+ const original = source.original;
15
+ if (typeof original !== "string" || /^\s*</.test(original)) return null;
16
+ return { original };
17
+ };
18
+ //#endregion
19
+ //#region src/formats/krc/detect.ts
20
+ var LANGUAGE_PATTERN = /\[language:\s*ey[A-Za-z0-9+/=]*\]/;
21
+ /**
22
+ * Recognizes native KRC from a duration line carrying a complete three-field word tag, or from the Kugou language container.
23
+ *
24
+ * Both signals live in the original plaintext, so an auxiliary track still cannot decide the format.
25
+ *
26
+ * @returns Whether the original plaintext contains a strong KRC syntax signal.
27
+ */
28
+ var detectKrc = (input) => {
29
+ const source = readKrcContent(input);
30
+ if (!source) return false;
31
+ if (LANGUAGE_PATTERN.test(source.original)) return true;
32
+ return source.original.split(/\r\n?|\n/).some((raw) => {
33
+ const line = raw.trimStart();
34
+ const header = LINE_PATTERN$3.exec(line);
35
+ return !!header && /<\d+,\d+,\d+>/.test(line.slice(header[0].length));
36
+ });
37
+ };
38
+ //#endregion
39
+ //#region src/formats/result.ts
40
+ /**
41
+ * Canonicalizes and validates one parser's output, so every format hands back the same shape.
42
+ *
43
+ * Canonicalization runs first because validation reports paths against the canonical tree.
44
+ */
45
+ var finalizeLyric = (input) => {
46
+ const lyric = canonicalizeLyric(input);
47
+ return {
48
+ lyric,
49
+ diagnostics: validateLyric(lyric)
50
+ };
51
+ };
52
+ //#endregion
3
53
  //#region src/utils/kana.ts
4
54
  var KANA_COMBINATIONS = /* @__PURE__ */ new Map([
5
55
  [12353, "うくぐつふゔ"],
@@ -31,7 +81,7 @@ var combinesKana = (first, second) => {
31
81
  };
32
82
  //#endregion
33
83
  //#region src/utils/annotation.ts
34
- var readText$2 = (line) => {
84
+ var readText$3 = (line) => {
35
85
  return getContentText(line.content).trim();
36
86
  };
37
87
  /**
@@ -267,7 +317,7 @@ var readSources = (lines, options) => {
267
317
  const preserve = options.preservePlaceholders;
268
318
  for (const [index, line] of lines.entries()) {
269
319
  const start = line.time?.start;
270
- const text = readText$2(line);
320
+ const text = readText$3(line);
271
321
  const timing = options.getTiming?.(line);
272
322
  const precision = timing?.precision;
273
323
  if (start !== void 0 && (preserve || text) || options.matchByOrder && text) result.push({
@@ -289,7 +339,7 @@ var createTimeIndex = (lines, options) => {
289
339
  const start = line.time?.start;
290
340
  const valid = start !== void 0 && (!options.preservePlaceholders || Number.isFinite(start));
291
341
  starts.push(valid ? start : void 0);
292
- if (options.matchByOrder && line.type === LineType.NORMAL && readText$2(line)) lyricRows.push(index);
342
+ if (options.matchByOrder && line.type === LineType.NORMAL && readText$3(line)) lyricRows.push(index);
293
343
  if (!valid) continue;
294
344
  const queue = targets.get(start) ?? [];
295
345
  queue.push(index);
@@ -516,6 +566,26 @@ var attachAnnotations = (lines, sources, options = {}) => {
516
566
  return attachTracks(lines, sources, options, Number.isFinite(options.wordTolerance) ? Math.max(0, options.wordTolerance ?? 0) : 0);
517
567
  };
518
568
  //#endregion
569
+ //#region src/utils/encoding.ts
570
+ /**
571
+ * Decodes canonical standard base64 into bytes, identically in browsers and Node.
572
+ * atob decides what is decodable; the round-trip re-encode keeps the result canonical, so slacker forms stay undecoded.
573
+ *
574
+ * @param value Base64 text; whitespace, URL-safe alphabets, and misplaced padding are rejected.
575
+ *
576
+ * @returns The decoded bytes, or null when the input is not canonical base64.
577
+ */
578
+ var decodeBase64 = (value) => {
579
+ if (!value) return null;
580
+ try {
581
+ const binary = atob(value);
582
+ if (btoa(binary).replace(/=+$/, "") !== value.replace(/=+$/, "")) return null;
583
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
584
+ } catch {
585
+ return null;
586
+ }
587
+ };
588
+ //#endregion
519
589
  //#region src/utils/time.ts
520
590
  var CLOCK_PATTERN$1 = /^(?:(?:(\d+):)?(\d+):)?(\d+)(?:\.(\d+))?$/;
521
591
  var OFFSET_PATTERN = /^(\d+(?:\.\d+)?)(ms|h|m|s)$/iu;
@@ -1233,6 +1303,77 @@ var serializeXml = (root, options = {}) => {
1233
1303
  return chunks.join("");
1234
1304
  };
1235
1305
  //#endregion
1306
+ //#region src/formats/qrc/common.ts
1307
+ var LINE_PATTERN$2 = /^\[(\d+),(\d+)\]/;
1308
+ var XML_ROOT_NAMES = [
1309
+ "QrcInfos",
1310
+ "LyricInfo",
1311
+ "Lyric_1"
1312
+ ];
1313
+ var XML_PATTERN = /^\s*</;
1314
+ var TIMED_LINE_PATTERN = /(?:^|[\r\n])[^\S\r\n]*\[\d+,\d+\]/;
1315
+ /**
1316
+ * Unwraps supported QRC XML while keeping plaintext literal.
1317
+ *
1318
+ * XML entities are decoded once by the XML reader.
1319
+ *
1320
+ * @returns The payload, or null when XML is malformed or has no supported lyric content.
1321
+ */
1322
+ var readText$2 = (source) => {
1323
+ if (!XML_PATTERN.test(source)) return source;
1324
+ if (!XML_ROOT_NAMES.some((name) => source.includes(name))) return null;
1325
+ const document = parseXml(source);
1326
+ if (!document) return null;
1327
+ const root = document.children.find((node) => node.nodeType === XmlNodeType.element);
1328
+ if (!root || !XML_ROOT_NAMES.includes(root.localName)) return null;
1329
+ const pending = [root];
1330
+ while (pending.length) {
1331
+ const element = pending.pop();
1332
+ const content = getXmlAttribute(element, "LyricContent");
1333
+ if (content !== void 0) return content;
1334
+ for (let index = element.children.length - 1; index >= 0; index -= 1) {
1335
+ const child = element.children[index];
1336
+ if (child.nodeType === XmlNodeType.element) pending.push(child);
1337
+ }
1338
+ }
1339
+ return null;
1340
+ };
1341
+ /**
1342
+ * Validates the original payload and keeps auxiliary tracks separate so each can use its own format.
1343
+ *
1344
+ * @returns Normalized input, or null when the original content cannot be read.
1345
+ */
1346
+ var readContent = (input) => {
1347
+ const value = typeof input === "string" ? { original: input } : input;
1348
+ if (!value || typeof value !== "object") return null;
1349
+ const source = value;
1350
+ if (typeof source.original !== "string") return null;
1351
+ const original = readText$2(source.original);
1352
+ if (original === null) return null;
1353
+ return {
1354
+ original,
1355
+ translations: typeof source.translations === "string" ? source.translations : void 0,
1356
+ romans: typeof source.romans === "string" ? source.romans : void 0
1357
+ };
1358
+ };
1359
+ /**
1360
+ * Requires a timed QRC row so metadata alone cannot select the format.
1361
+ */
1362
+ var hasTimedLines = (text) => {
1363
+ return TIMED_LINE_PATTERN.test(text);
1364
+ };
1365
+ //#endregion
1366
+ //#region src/formats/qrc/detect.ts
1367
+ /**
1368
+ * Detects QRC from the original track so annotations cannot change format selection.
1369
+ *
1370
+ * @returns Whether plaintext or a supported XML payload contains a timed QRC row.
1371
+ */
1372
+ var detectQrc = (input) => {
1373
+ const content = readContent(input);
1374
+ return !!content && hasTimedLines(content.original);
1375
+ };
1376
+ //#endregion
1236
1377
  //#region src/formats/lrc/common.ts
1237
1378
  /**
1238
1379
  * Accepts plain lyrics or separate tracks while rejecting an unreadable original payload.
@@ -1254,7 +1395,7 @@ var WORD_OFFSET_PATTERN = /^(\d+)(?:,(\d+))?$/;
1254
1395
  var WORD_CLOCK_DURATION_PATTERN = /^([^,]+),(\d+)$/;
1255
1396
  var WORD_TAG_PATTERN = /<([^>]*)>/g;
1256
1397
  var TAG_PATTERN = /\[([^\]]*)\]/g;
1257
- var META_PATTERN$1 = /^([A-Za-z0-9_-]+)\s*:\s*([\s\S]*)$/;
1398
+ var META_PATTERN = /^([A-Za-z0-9_-]+)\s*:\s*([\s\S]*)$/;
1258
1399
  /**
1259
1400
  * Integer tags use `offset[,duration]` in milliseconds relative to the line start.
1260
1401
  * Clock tags are absolute and may append a duration in milliseconds, as in `<00:01.50,500>`.
@@ -1288,36 +1429,42 @@ var parseWordTime = (body, lineStart) => {
1288
1429
  }
1289
1430
  };
1290
1431
  /**
1291
- * Untagged leading text is preserved; empty timed pieces retain explicit segment boundaries.
1292
- * Only recognized time tags split content; other angle-bracket text remains literal.
1432
+ * Splits recognized tags from their following text while preserving literal content and empty tagged segments.
1433
+ *
1434
+ * @param pattern A global pattern whose first capture is the tag body.
1435
+ * @param accepts Optional format-specific tag validation.
1436
+ *
1437
+ * @returns Source pieces in their original order, including an untagged prefix when present.
1293
1438
  */
1294
- var splitWordTags = (content) => {
1439
+ var scanWordTags = (content, pattern, accepts) => {
1295
1440
  const pieces = [];
1296
1441
  let tag;
1297
1442
  let cursor = 0;
1298
- for (const match of content.matchAll(WORD_TAG_PATTERN)) {
1443
+ for (const match of content.matchAll(pattern)) {
1299
1444
  const nextTag = match[1] ?? "";
1300
- if (!parseWordTime(nextTag, 0)) continue;
1301
- const at = match.index;
1302
- const before = content.slice(cursor, at);
1303
- if (before || tag) pieces.push({
1445
+ if (accepts && !accepts(nextTag)) continue;
1446
+ const before = content.slice(cursor, match.index);
1447
+ if (before || tag !== void 0) pieces.push({
1304
1448
  text: before,
1305
1449
  tag
1306
1450
  });
1307
1451
  tag = nextTag;
1308
- cursor = at + match[0].length;
1452
+ cursor = match.index + match[0].length;
1309
1453
  }
1310
1454
  const tail = content.slice(cursor);
1311
- if (tail) pieces.push({
1455
+ if (tail || tag !== void 0) pieces.push({
1312
1456
  text: tail,
1313
1457
  tag
1314
1458
  });
1315
- else if (tag) pieces.push({
1316
- text: "",
1317
- tag
1318
- });
1319
1459
  return pieces;
1320
1460
  };
1461
+ /**
1462
+ * Untagged leading text is preserved; empty timed pieces retain explicit segment boundaries.
1463
+ * Only recognized time tags split content; other angle-bracket text remains literal.
1464
+ */
1465
+ var splitWordTags = (content) => {
1466
+ return scanWordTags(content, WORD_TAG_PATTERN, (tag) => !!parseWordTime(tag, 0));
1467
+ };
1321
1468
  //#endregion
1322
1469
  //#region src/formats/lrc/detect.ts
1323
1470
  /**
@@ -1465,57 +1612,47 @@ var generateLrc = (lyric, options = {}) => {
1465
1612
  return result;
1466
1613
  };
1467
1614
  //#endregion
1468
- //#region src/formats/result.ts
1469
- /**
1470
- * Canonicalizes and validates one parser's output, so every format hands back the same shape.
1471
- *
1472
- * Canonicalization runs first because validation reports paths against the canonical tree.
1473
- */
1474
- var finalizeLyric = (input) => {
1475
- const lyric = canonicalizeLyric(input);
1476
- return {
1477
- lyric,
1478
- diagnostics: validateLyric(lyric)
1479
- };
1480
- };
1481
- //#endregion
1482
- //#region src/formats/lrc/reader.ts
1615
+ //#region src/formats/lrc/parser.ts
1483
1616
  var CLOCK_PATTERN = /^(?:\d+:){0,2}\d+(?:\.(\d+))?$/;
1484
1617
  var readLineTiming = (tag) => {
1485
1618
  const clock = CLOCK_PATTERN.exec(tag.trim());
1486
1619
  return { precision: clock ? 10 ** Math.max(0, 3 - (clock[1]?.length ?? 0)) : 1 };
1487
1620
  };
1488
1621
  var readMeta$1 = (body) => {
1489
- const match = META_PATTERN$1.exec(body);
1622
+ const match = META_PATTERN.exec(body);
1490
1623
  if (!match) return;
1491
1624
  return {
1492
1625
  key: (match[1] ?? "").toLowerCase(),
1493
1626
  value: (match[2] ?? "").trim()
1494
1627
  };
1495
1628
  };
1496
- var readGroup = (tags, text, timed, meta) => {
1629
+ var readGroup = (tags, text, row, timed, meta, options) => {
1497
1630
  for (const tag of tags) {
1498
1631
  const start = isTime(tag) ? parseTime(tag) : void 0;
1499
1632
  if (start !== void 0) {
1500
1633
  timed.push({
1501
1634
  text,
1502
1635
  start,
1503
- tag
1636
+ tag,
1637
+ row
1504
1638
  });
1505
1639
  continue;
1506
1640
  }
1507
1641
  const pair = readMeta$1(tag);
1508
- if (pair) meta.push(pair);
1642
+ if (pair) {
1643
+ meta.push(pair);
1644
+ options.onMeta?.(pair, row);
1645
+ }
1509
1646
  }
1510
1647
  };
1511
1648
  /**
1512
1649
  * Stacked timestamps share the following text, as in `[00:10][00:20]twice`.
1513
1650
  * Text before the first tag is discarded because it has no timing association.
1514
1651
  */
1515
- var readTimedLines = (text) => {
1652
+ var readTimedLines = (text, options) => {
1516
1653
  const timed = [];
1517
1654
  const meta = [];
1518
- for (const raw of text.replace(/\r\n?/g, "\n").split("\n")) {
1655
+ for (const [row, raw] of text.replace(/\r\n?/g, "\n").split("\n").entries()) {
1519
1656
  const line = raw.trim();
1520
1657
  if (!line) continue;
1521
1658
  const matches = [...line.matchAll(TAG_PATTERN)];
@@ -1526,10 +1663,10 @@ var readTimedLines = (text) => {
1526
1663
  const shared = line.slice(at, next);
1527
1664
  group.push(match[1] ?? "");
1528
1665
  if (!shared.trim()) continue;
1529
- readGroup(group, shared, timed, meta);
1666
+ readGroup(group, shared, row, timed, meta, options);
1530
1667
  group = [];
1531
1668
  }
1532
- readGroup(group, "", timed, meta);
1669
+ readGroup(group, "", row, timed, meta, options);
1533
1670
  }
1534
1671
  return {
1535
1672
  timed,
@@ -1592,7 +1729,7 @@ var readWords$2 = (pieces, lineStart) => {
1592
1729
  * Explicit word ranges can extend past the next line's start, so the inferred end covers both.
1593
1730
  * A trailing open segment uses that end when it is known.
1594
1731
  */
1595
- var readLine$1 = (text, start, nextStart) => {
1732
+ var readLine$4 = (text, start, nextStart) => {
1596
1733
  const trimmed = text.trim();
1597
1734
  let end = nextStart !== void 0 && nextStart >= start ? nextStart : void 0;
1598
1735
  if (!trimmed) return makeLineInstrumental({ time: makeTimeRange({
@@ -1639,15 +1776,14 @@ var readLines$1 = (timed, options) => {
1639
1776
  const lines = [];
1640
1777
  for (const [index, item] of timed.entries()) {
1641
1778
  const next = timed[index + 1]?.start;
1642
- const line = readLine$1(item.text, item.start, next);
1779
+ const line = readLine$4(item.text, item.start, next);
1643
1780
  lines.push(line);
1644
- options.onLine?.(line, readLineTiming(item.tag));
1781
+ options.onLine?.(line, readLineTiming(item.tag), item.row);
1645
1782
  }
1646
1783
  return lines;
1647
1784
  };
1648
1785
  /**
1649
- * Reads one LRC track before normalization so consumers retain source order and timing slots.
1650
- * The caller owns metadata mapping, annotation attachment, and final normalization.
1786
+ * Reads one LRC track in source order and performs no other operation: no metadata mapping, annotation attachment, sorting, or normalization.
1651
1787
  *
1652
1788
  * @param text Original lyrics or one auxiliary track in LRC syntax.
1653
1789
  * @param options Optional observer for source timestamp information.
@@ -1655,22 +1791,23 @@ var readLines$1 = (timed, options) => {
1655
1791
  * @returns Model lines in input order and metadata tags with lowercase keys and trimmed values.
1656
1792
  */
1657
1793
  var readLrc = (text, options = {}) => {
1658
- const { timed, meta } = readTimedLines(text);
1794
+ const { timed, meta } = readTimedLines(text, options);
1659
1795
  return {
1660
1796
  lines: readLines$1(timed, options),
1661
1797
  metaTags: meta
1662
1798
  };
1663
1799
  };
1664
- //#endregion
1665
- //#region src/formats/lrc/parser.ts
1666
1800
  var isBareNumber = (value) => {
1667
1801
  return /^\d+$/.test(value.trim());
1668
1802
  };
1669
1803
  /**
1670
- * Unmapped tags and unparseable durations remain in `unknowns` to avoid discarding metadata.
1671
- * Keys are lowercased and values trimmed, so original tag spelling is not preserved.
1804
+ * Maps standard lyric metadata while preserving unmapped tags and unparseable durations in unknowns.
1805
+ *
1806
+ * @param tags Reader tags with lowercase keys and trimmed values.
1807
+ *
1808
+ * @returns Model metadata without interpreting platform-specific embedded annotations.
1672
1809
  */
1673
- var readMetaTags = (tags) => {
1810
+ var readLrcMetaTags = (tags) => {
1674
1811
  const meta = makeMeta();
1675
1812
  for (const tag of tags) switch (tag.key) {
1676
1813
  case "length":
@@ -1714,7 +1851,7 @@ var readMetaTags = (tags) => {
1714
1851
  };
1715
1852
  var parseLrcText = (text) => {
1716
1853
  const { lines, metaTags } = readLrc(text);
1717
- const meta = readMetaTags(metaTags);
1854
+ const meta = readLrcMetaTags(metaTags);
1718
1855
  if (!lines.length) return makeLyric({
1719
1856
  format: "lrc",
1720
1857
  status: LyricStatus.INVALID,
@@ -1759,88 +1896,22 @@ var parseLrc = (input, options = {}) => {
1759
1896
  });
1760
1897
  };
1761
1898
  //#endregion
1762
- //#region src/formats/qrc/common.ts
1763
- var LINE_PATTERN = /^\[(\d+),(\d+)\]/;
1764
- var XML_ROOT_NAMES = [
1765
- "QrcInfos",
1766
- "LyricInfo",
1767
- "Lyric_1"
1768
- ];
1769
- var XML_PATTERN = /^\s*</;
1770
- var TIMED_LINE_PATTERN = /(?:^|[\r\n])[^\S\r\n]*\[\d+,\d+\]/;
1771
- /**
1772
- * Unwraps supported QRC XML while keeping plaintext literal.
1773
- *
1774
- * XML entities are decoded once by the XML reader.
1775
- *
1776
- * @returns The payload, or null when XML is malformed or has no supported lyric content.
1777
- */
1778
- var readText$1 = (source) => {
1779
- if (!XML_PATTERN.test(source)) return source;
1780
- if (!XML_ROOT_NAMES.some((name) => source.includes(name))) return null;
1781
- const document = parseXml(source);
1782
- if (!document) return null;
1783
- const root = document.children.find((node) => node.nodeType === XmlNodeType.element);
1784
- if (!root || !XML_ROOT_NAMES.includes(root.localName)) return null;
1785
- const pending = [root];
1786
- while (pending.length) {
1787
- const element = pending.pop();
1788
- const content = getXmlAttribute(element, "LyricContent");
1789
- if (content !== void 0) return content;
1790
- for (let index = element.children.length - 1; index >= 0; index -= 1) {
1791
- const child = element.children[index];
1792
- if (child.nodeType === XmlNodeType.element) pending.push(child);
1793
- }
1794
- }
1795
- return null;
1796
- };
1797
- /**
1798
- * Validates the original payload and keeps auxiliary tracks separate so each can use its own format.
1799
- *
1800
- * @returns Normalized input, or null when the original content cannot be read.
1801
- */
1802
- var readContent = (input) => {
1803
- const value = typeof input === "string" ? { original: input } : input;
1804
- if (!value || typeof value !== "object") return null;
1805
- const source = value;
1806
- if (typeof source.original !== "string") return null;
1807
- const original = readText$1(source.original);
1808
- if (original === null) return null;
1809
- return {
1810
- original,
1811
- translations: typeof source.translations === "string" ? source.translations : void 0,
1812
- romans: typeof source.romans === "string" ? source.romans : void 0
1813
- };
1814
- };
1815
- /**
1816
- * Requires a timed QRC row so metadata alone cannot select the format.
1817
- */
1818
- var hasTimedLines = (text) => {
1819
- return TIMED_LINE_PATTERN.test(text);
1820
- };
1821
- //#endregion
1822
- //#region src/formats/qrc/detect.ts
1823
- /**
1824
- * Detects QRC from the original track so annotations cannot change format selection.
1825
- *
1826
- * @returns Whether plaintext or a supported XML payload contains a timed QRC row.
1827
- */
1828
- var detectQrc = (input) => {
1829
- const content = readContent(input);
1830
- return !!content && hasTimedLines(content.original);
1831
- };
1832
- //#endregion
1833
1899
  //#region src/formats/qrc/parser.ts
1834
- var WORD_PATTERN = /\((\d+),(\d+)\)/g;
1835
- var META_PATTERN = /^([A-Za-z0-9_-]+)\s*:\s*([\s\S]*)$/;
1900
+ var WORD_PATTERN$2 = /\((\d+),(\d+)\)/g;
1901
+ var META_PATTERN$1 = /^([A-Za-z0-9_-]+)\s*:\s*([\s\S]*)$/;
1836
1902
  var META_TAG_PATTERN = /\[([^\]]*)\]/g;
1837
1903
  var RUBY_PATTERN = /\((\d+),(\d+)\)|(\d+)|([^0-9()]+)/gy;
1838
1904
  var RUBY_WORD_PATTERN = /[\p{Script=Han}\p{Number}]/u;
1839
- var SEPARATORS = /^\p{White_Space}+$/u;
1905
+ var SEPARATORS$3 = /^\p{White_Space}+$/u;
1840
1906
  var NATIVE_ANNOTATION_TIMING = {
1841
1907
  precision: 1,
1842
1908
  rounding: 1
1843
1909
  };
1910
+ /**
1911
+ * Reads coverage markers and optional absolute token times without interpreting timing digits as coverage.
1912
+ *
1913
+ * @returns Complete ruby sources, or undefined when any source cannot be read.
1914
+ */
1844
1915
  var readRubyEntries = (value) => {
1845
1916
  const entries = [];
1846
1917
  let length = 0;
@@ -1965,13 +2036,16 @@ var matchRubies = (words, sources) => {
1965
2036
  /**
1966
2037
  * Attaches whole-word ruby without changing the source text, segmentation, or timing.
1967
2038
  *
1968
- * @param lines Original QRC rows before filtering or sorting.
2039
+ * @param lines Original timed rows before filtering or sorting.
1969
2040
  * @param sources Readings with source coverage lengths and optional token times.
1970
2041
  *
1971
- * @returns Annotated rows, reusing arrays and nodes when they are unchanged.
2042
+ * @returns Annotated rows and matched source indices, preserving unchanged nodes.
1972
2043
  */
1973
2044
  var attachRubies = (lines, sources) => {
1974
- if (!sources.length) return lines;
2045
+ if (!sources.length) return {
2046
+ lines,
2047
+ matched: /* @__PURE__ */ new Set()
2048
+ };
1975
2049
  const words = [];
1976
2050
  for (const line of lines) {
1977
2051
  if (line.type !== LineType.NORMAL) continue;
@@ -1986,33 +2060,39 @@ var attachRubies = (lines, sources) => {
1986
2060
  });
1987
2061
  }
1988
2062
  const matched = matchRubies(words, sources);
1989
- if (!matched.size) return lines;
2063
+ if (!matched.size) return {
2064
+ lines,
2065
+ matched: /* @__PURE__ */ new Set()
2066
+ };
1990
2067
  const annotations = new Map([...matched].map(([source, target]) => [words[target].word, sources[source].ruby]));
1991
- return mapShared(lines, (line) => {
1992
- const content = line.content;
1993
- if (!content) return line;
1994
- const words = mapShared(content.words, (word) => {
1995
- const ruby = annotations.get(word);
1996
- if (!ruby) return word;
1997
- return {
1998
- ...word,
1999
- annotation: makeWordAnnotation({
2000
- ...word.annotation,
2001
- rubies: [...word.annotation?.rubies ?? [], ruby]
2002
- })
2068
+ return {
2069
+ lines: mapShared(lines, (line) => {
2070
+ const content = line.content;
2071
+ if (!content) return line;
2072
+ const words = mapShared(content.words, (word) => {
2073
+ const ruby = annotations.get(word);
2074
+ if (!ruby) return word;
2075
+ return {
2076
+ ...word,
2077
+ annotation: makeWordAnnotation({
2078
+ ...word.annotation,
2079
+ rubies: [...word.annotation?.rubies ?? [], ruby]
2080
+ })
2081
+ };
2082
+ });
2083
+ return words === content.words ? line : {
2084
+ ...line,
2085
+ content: {
2086
+ ...content,
2087
+ words
2088
+ }
2003
2089
  };
2004
- });
2005
- return words === content.words ? line : {
2006
- ...line,
2007
- content: {
2008
- ...content,
2009
- words
2010
- }
2011
- };
2012
- });
2090
+ }),
2091
+ matched: new Set(matched.keys())
2092
+ };
2013
2093
  };
2014
2094
  var readMeta = (body, meta) => {
2015
- const match = META_PATTERN.exec(body);
2095
+ const match = META_PATTERN$1.exec(body);
2016
2096
  if (!match) return;
2017
2097
  const key = match[1].toLowerCase();
2018
2098
  const value = match[2].trim();
@@ -2062,10 +2142,10 @@ var readTimeRange = (startText, durationText) => {
2062
2142
  end: start + Number(durationText)
2063
2143
  });
2064
2144
  };
2065
- var appendWords = (words, text, time) => {
2145
+ var appendWords$2 = (words, text, time) => {
2066
2146
  for (const fragment of text.split(/(\p{White_Space}+)/u)) {
2067
2147
  if (!fragment) continue;
2068
- if (SEPARATORS.test(fragment)) words.push(makeWordSpace({ text: fragment }));
2148
+ if (SEPARATORS$3.test(fragment)) words.push(makeWordSpace({ text: fragment }));
2069
2149
  else words.push(makeWordNormal({
2070
2150
  text: fragment,
2071
2151
  time
@@ -2076,28 +2156,36 @@ var readWords$1 = (text) => {
2076
2156
  const words = [];
2077
2157
  let cursor = 0;
2078
2158
  let tagged = false;
2079
- for (const match of text.matchAll(WORD_PATTERN)) {
2159
+ for (const match of text.matchAll(WORD_PATTERN$2)) {
2080
2160
  tagged = true;
2081
- appendWords(words, text.slice(cursor, match.index), readTimeRange(match[1], match[2]));
2161
+ appendWords$2(words, text.slice(cursor, match.index), readTimeRange(match[1], match[2]));
2082
2162
  cursor = match.index + match[0].length;
2083
2163
  }
2084
2164
  if (!tagged) return;
2085
- appendWords(words, text.slice(cursor));
2165
+ appendWords$2(words, text.slice(cursor));
2086
2166
  return words;
2087
2167
  };
2168
+ /**
2169
+ * Kugou writes `//` where it holds no translation, in QRC source rows and in its language payload alike.
2170
+ *
2171
+ * @returns Whether the trimmed text is that sentinel instead of real content.
2172
+ */
2173
+ var isPlaceholderText = (text) => {
2174
+ return text.trim() === "//";
2175
+ };
2088
2176
  var readLines = (text) => {
2089
2177
  const lines = [];
2090
2178
  const meta = makeMeta();
2091
2179
  const placeholders = /* @__PURE__ */ new Set();
2092
2180
  for (const raw of text.split(/\r\n?|\n/)) {
2093
2181
  const line = raw.trimStart();
2094
- const match = LINE_PATTERN.exec(line);
2182
+ const match = LINE_PATTERN$2.exec(line);
2095
2183
  if (match) {
2096
2184
  const time = readTimeRange(match[1], match[2]);
2097
2185
  const text = line.slice(match[0].length);
2098
2186
  const words = readWords$1(text);
2099
2187
  const contentText = (words ? getWordsText(words) : text).trim();
2100
- const placeholder = contentText === "//";
2188
+ const placeholder = isPlaceholderText(contentText);
2101
2189
  if (placeholder) placeholders.add(lines.length);
2102
2190
  if (!contentText || placeholder) lines.push(makeLineInstrumental({ time }));
2103
2191
  else {
@@ -2122,7 +2210,7 @@ var readLines = (text) => {
2122
2210
  placeholders
2123
2211
  };
2124
2212
  };
2125
- var readAnnotations = (text, timings) => {
2213
+ var readAnnotations$1 = (text, timings) => {
2126
2214
  if (!text) return [];
2127
2215
  const { lines } = readLines(text);
2128
2216
  if (lines.length) {
@@ -2130,7 +2218,7 @@ var readAnnotations = (text, timings) => {
2130
2218
  return lines;
2131
2219
  }
2132
2220
  readLrc(text, { onLine: (line, timing) => {
2133
- const annotation = getContentText(line.content).trim() === "//" ? makeLineInstrumental({ time: line.time }) : line;
2221
+ const annotation = isPlaceholderText(getContentText(line.content)) ? makeLineInstrumental({ time: line.time }) : line;
2134
2222
  timings.set(annotation, timing);
2135
2223
  lines.push(annotation);
2136
2224
  } });
@@ -2160,11 +2248,11 @@ var parseQrc = (input, options = {}) => {
2160
2248
  meta
2161
2249
  }));
2162
2250
  const timings = /* @__PURE__ */ new WeakMap();
2163
- const translationLines = readAnnotations(source.translations && readText$1(source.translations), timings);
2164
- const romanLines = readAnnotations(source.romans && readText$1(source.romans), timings);
2251
+ const translationLines = readAnnotations$1(source.translations && readText$2(source.translations), timings);
2252
+ const romanLines = readAnnotations$1(source.romans && readText$2(source.romans), timings);
2165
2253
  const rounding = romanLines.some((line) => timings.get(line)?.rounding === 1) ? 1 : 0;
2166
2254
  const wordTolerance = Number.isFinite(options.wordAnnotationTolerance) ? options.wordAnnotationTolerance ?? 0 : 0;
2167
- const annotated = attachAnnotations(!rubies ? parsed.lines : attachRubies(parsed.lines, rubies), {
2255
+ const annotated = attachAnnotations(!rubies ? parsed.lines : attachRubies(parsed.lines, rubies).lines, {
2168
2256
  translations: translationLines,
2169
2257
  romans: romanLines
2170
2258
  }, {
@@ -2186,6 +2274,441 @@ var parseQrc = (input, options = {}) => {
2186
2274
  }));
2187
2275
  };
2188
2276
  //#endregion
2277
+ //#region src/formats/krc/parser.ts
2278
+ var SEPARATORS$2 = /^\p{White_Space}+$/u;
2279
+ var appendWords$1 = (words, text, time) => {
2280
+ for (const fragment of text.split(/(\p{White_Space}+)/u)) {
2281
+ if (!fragment) continue;
2282
+ words.push(SEPARATORS$2.test(fragment) ? makeWordSpace({ text: fragment }) : makeWordNormal({
2283
+ text: fragment,
2284
+ time
2285
+ }));
2286
+ }
2287
+ };
2288
+ var readLine$3 = (body, start, duration) => {
2289
+ const time = makeTimeRange({
2290
+ start,
2291
+ end: start + duration
2292
+ });
2293
+ const pieces = scanWordTags(body, WORD_PATTERN$3);
2294
+ if (!pieces.some((piece) => piece.tag !== void 0)) return body.trim() ? makeLineNormal({
2295
+ time,
2296
+ content: makeLineContentText(body)
2297
+ }) : makeLineInstrumental({ time });
2298
+ const words = [];
2299
+ for (const piece of pieces) {
2300
+ if (piece.tag === void 0) {
2301
+ appendWords$1(words, piece.text);
2302
+ continue;
2303
+ }
2304
+ const [offset, wordDuration] = piece.tag.split(",").map(Number);
2305
+ const wordStart = start + offset;
2306
+ appendWords$1(words, piece.text, makeTimeRange({
2307
+ start: wordStart,
2308
+ end: wordStart + wordDuration
2309
+ }));
2310
+ }
2311
+ return getWordsText(words).trim() ? makeLineNormal({
2312
+ time,
2313
+ content: makeLineContentWords(words)
2314
+ }) : makeLineInstrumental({ time });
2315
+ };
2316
+ /**
2317
+ * Reads native KRC rows in source order and performs no other operation: no metadata mapping, annotation attachment, sorting, or normalization.
2318
+ *
2319
+ * Explicit line and word durations survive unchanged; whitespace splitting never reallocates segment time.
2320
+ *
2321
+ * @returns Original-order model lines and metadata tags awaiting interpretation.
2322
+ */
2323
+ var readKrc = (text) => {
2324
+ const lines = [];
2325
+ const metaTags = [];
2326
+ for (const raw of text.replace(/^\uFEFF/, "").split(/\r\n?|\n/)) {
2327
+ const line = raw.trimStart();
2328
+ const header = LINE_PATTERN$3.exec(line);
2329
+ if (header) {
2330
+ lines.push(readLine$3(line.slice(header[0].length), Number(header[1]), Number(header[2])));
2331
+ continue;
2332
+ }
2333
+ if (!line.startsWith("[")) continue;
2334
+ for (const tag of line.matchAll(TAG_PATTERN)) {
2335
+ const meta = META_PATTERN.exec(tag[1]);
2336
+ if (meta) metaTags.push({
2337
+ key: meta[1].toLowerCase(),
2338
+ value: meta[2].trim()
2339
+ });
2340
+ }
2341
+ }
2342
+ return {
2343
+ lines,
2344
+ metaTags
2345
+ };
2346
+ };
2347
+ var attachKana = (lines, meta) => {
2348
+ for (const entry of meta.unknowns) {
2349
+ if (entry.key !== "kana") continue;
2350
+ const sources = readRubyEntries(entry.value);
2351
+ if (!sources) continue;
2352
+ const attached = attachRubies(lines, sources);
2353
+ if (attached.matched.size === sources.length) meta.unknowns = meta.unknowns.filter((item) => item !== entry);
2354
+ return attached.lines;
2355
+ }
2356
+ return lines;
2357
+ };
2358
+ var isObject$1 = (value) => {
2359
+ return !!value && typeof value === "object" && !Array.isArray(value);
2360
+ };
2361
+ var readPayload = (value) => {
2362
+ const bytes = decodeBase64(value);
2363
+ if (!bytes) return;
2364
+ try {
2365
+ const payload = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
2366
+ return isObject$1(payload) && payload.version === 1 && Array.isArray(payload.content) ? payload : void 0;
2367
+ } catch {
2368
+ return;
2369
+ }
2370
+ };
2371
+ var readTrack$1 = (value) => {
2372
+ if (!isObject$1(value) || value.type !== 0 && value.type !== 1 || typeof value.language !== "number" || !Number.isSafeInteger(value.language) || value.language < 0 || !Array.isArray(value.lyricContent) || !value.lyricContent.every((row) => Array.isArray(row) && row.every((cell) => typeof cell === "string"))) return;
2373
+ return value;
2374
+ };
2375
+ var attachTrack = (lines, track) => {
2376
+ let result = lines;
2377
+ let complete = true;
2378
+ for (const [index, cells] of track.lyricContent.entries()) {
2379
+ const text = cells.join("");
2380
+ if (cells.length > 1) complete = false;
2381
+ if (!text.trim() || isPlaceholderText(text)) continue;
2382
+ const line = result[index];
2383
+ if (line.type === LineType.INSTRUMENTAL) {
2384
+ complete = false;
2385
+ continue;
2386
+ }
2387
+ const kind = track.type === 1 ? "translations" : "romans";
2388
+ const existing = line.annotation?.[kind] ?? [];
2389
+ const occupied = existing.find((item) => !item.language);
2390
+ if (occupied) {
2391
+ complete = complete && occupied.text === text;
2392
+ continue;
2393
+ }
2394
+ const create = track.type === 1 ? makeLineAnnotationTranslation : makeLineAnnotationRoman;
2395
+ const annotated = {
2396
+ ...line,
2397
+ annotation: makeLineAnnotation({
2398
+ ...line.annotation,
2399
+ [kind]: [...existing, create({ text })]
2400
+ })
2401
+ };
2402
+ if (result === lines) result = lines.slice();
2403
+ result[index] = annotated;
2404
+ }
2405
+ return {
2406
+ lines: result,
2407
+ complete
2408
+ };
2409
+ };
2410
+ /**
2411
+ * Attaches strictly recognized Kugou language tracks by original timed slot before sorting or filtering.
2412
+ *
2413
+ * Existing annotations take precedence; incomplete or ambiguous payloads remain in metadata.
2414
+ * Numeric language codes other than the verified default are retained without inventing BCP 47 tags.
2415
+ *
2416
+ * @returns Annotated lines, reusing the original array when no visible content changes.
2417
+ */
2418
+ var attachKugouLanguage = (lines, meta) => {
2419
+ if (!lines.length) return lines;
2420
+ let result = lines;
2421
+ const unknowns = [];
2422
+ const selected = /* @__PURE__ */ new Map();
2423
+ for (const entry of meta.unknowns) {
2424
+ const payload = entry.key === "language" ? readPayload(entry.value) : void 0;
2425
+ if (!payload) {
2426
+ unknowns.push(entry);
2427
+ continue;
2428
+ }
2429
+ let complete = Object.keys(payload).every((key) => key === "version" || key === "content" || key === "contentV2");
2430
+ if ("contentV2" in payload && (!Array.isArray(payload.contentV2) || payload.contentV2.length !== 0)) complete = false;
2431
+ for (const value of payload.content) {
2432
+ const track = readTrack$1(value);
2433
+ if (!track || track.language !== 0 || track.lyricContent.length !== lines.length) {
2434
+ complete = false;
2435
+ continue;
2436
+ }
2437
+ if (Object.keys(value).some((key) => key !== "type" && key !== "language" && key !== "lyricContent")) complete = false;
2438
+ const previous = selected.get(track.type);
2439
+ if (previous) {
2440
+ complete = complete && JSON.stringify(previous) === JSON.stringify(track.lyricContent);
2441
+ continue;
2442
+ }
2443
+ selected.set(track.type, track.lyricContent);
2444
+ const attached = attachTrack(result, track);
2445
+ result = attached.lines;
2446
+ complete = complete && attached.complete;
2447
+ }
2448
+ if (!complete) unknowns.push(entry);
2449
+ }
2450
+ meta.unknowns = unknowns;
2451
+ return result;
2452
+ };
2453
+ /**
2454
+ * Parses decrypted Kugou lyrics, retaining explicit durations and attaching embedded annotations by source slot.
2455
+ *
2456
+ * A source carrying no KRC row is read as LRC, so a platform fallback keeps its container annotations.
2457
+ * Placeholder slots carry no annotation, so they never keep a credit line in the stream.
2458
+ * Kana supplies whole-word ruby; type 0 language tracks retain complete line romanization.
2459
+ * Incomplete metadata remains available in unknowns, and source timing violations remain visible in diagnostics.
2460
+ *
2461
+ * @param input Decrypted plaintext or an object containing its original text.
2462
+ *
2463
+ * @returns Canonical lyrics and model diagnostics; no readable timed rows yields INVALID status.
2464
+ */
2465
+ var parseKrc = (input) => {
2466
+ const source = readKrcContent(input);
2467
+ if (!source) return finalizeLyric(makeLyric({
2468
+ format: "krc",
2469
+ status: LyricStatus.INVALID
2470
+ }));
2471
+ const native = readKrc(source.original);
2472
+ const parsed = native.lines.length ? native : readLrc(source.original);
2473
+ const meta = readLrcMetaTags(parsed.metaTags);
2474
+ const lines = attachKugouLanguage(attachKana(parsed.lines, meta), meta);
2475
+ return finalizeLyric(makeLyric({
2476
+ format: "krc",
2477
+ status: lines.length ? LyricStatus.VALID : LyricStatus.INVALID,
2478
+ timing: lines.some(isWordLevelLine) ? Timing.WORD : Timing.LINE,
2479
+ meta,
2480
+ lines,
2481
+ languages: deriveLanguageUsages(lines)
2482
+ }));
2483
+ };
2484
+ //#endregion
2485
+ //#region src/formats/kwrc/common.ts
2486
+ var FORMAT$1 = "kwrc";
2487
+ var LINE_PATTERN$1 = /^\[([^\]]*)\]/;
2488
+ var WORD_PATTERN$1 = /<(-?\d+,-?\d+(?:,-?\d+)?)>/g;
2489
+ var VISIBLE_TEXT = /[^\p{White_Space}]/u;
2490
+ /**
2491
+ * Removes only complete encoded tags so literal angle-bracket text survives.
2492
+ */
2493
+ var readText$1 = (text) => {
2494
+ return text.replace(WORD_PATTERN$1, "");
2495
+ };
2496
+ /**
2497
+ * Accepts decoded plaintext without unwrapping XML or auxiliary tracks.
2498
+ */
2499
+ var readKwrcContent = (input) => {
2500
+ const source = typeof input === "string" ? { original: input } : input;
2501
+ if (!source || typeof source !== "object" || Array.isArray(source)) return null;
2502
+ const original = source.original;
2503
+ if (typeof original !== "string" || /^\s*</.test(original)) return null;
2504
+ return { original };
2505
+ };
2506
+ /**
2507
+ * Retains source slots and literal bodies before word timing or bilingual boundaries are interpreted.
2508
+ */
2509
+ var readKwrcSource = (text) => {
2510
+ const lines = [];
2511
+ const metaTags = [];
2512
+ const rows = text.replace(/^\uFEFF/, "").split(/\r\n?|\n/);
2513
+ for (const [row, raw] of rows.entries()) {
2514
+ const times = [];
2515
+ let body = raw.trimStart();
2516
+ let header;
2517
+ while (header = LINE_PATTERN$1.exec(body)) {
2518
+ const tag = header[1].trim();
2519
+ const start = parseTime(tag);
2520
+ if (start !== void 0) times.push({
2521
+ start,
2522
+ clock: /^[\d:.]+$/.test(tag)
2523
+ });
2524
+ else {
2525
+ const meta = META_PATTERN.exec(tag);
2526
+ if (!meta || times.length) break;
2527
+ metaTags.push({
2528
+ key: meta[1].toLowerCase(),
2529
+ value: meta[2].trim()
2530
+ });
2531
+ }
2532
+ body = body.slice(header[0].length);
2533
+ if (!times.length) body = body.trimStart();
2534
+ }
2535
+ for (const time of times) lines.push({
2536
+ row,
2537
+ ...time,
2538
+ text: body
2539
+ });
2540
+ }
2541
+ return {
2542
+ lines,
2543
+ metaTags
2544
+ };
2545
+ };
2546
+ /**
2547
+ * Requires complete octal values and agreement across all parameter tags, without selecting a default scale.
2548
+ */
2549
+ var readKwrcScales = (tags) => {
2550
+ let value;
2551
+ for (const tag of tags) {
2552
+ if (tag.key !== "kuwo") continue;
2553
+ if (!/^[0-7]+$/.test(tag.value)) return;
2554
+ const next = Number.parseInt(tag.value, 8);
2555
+ if (!Number.isSafeInteger(next) || value !== void 0 && value !== next) return;
2556
+ value = next;
2557
+ }
2558
+ if (value === void 0) return;
2559
+ const start = Math.trunc(value / 10);
2560
+ const duration = value % 10;
2561
+ return start > 0 && duration > 0 ? {
2562
+ start,
2563
+ duration
2564
+ } : void 0;
2565
+ };
2566
+ /**
2567
+ * A repeated timestamp opens the previous original's translation slot before the next original.
2568
+ * The entire sequence must agree so one missing slot cannot shift all later translations.
2569
+ */
2570
+ var hasKwrcTranslations = ({ lines, metaTags }) => {
2571
+ const markers = metaTags.filter((tag) => tag.key === "ml");
2572
+ if (!markers.length || markers.some((tag) => tag.value !== "1.0") || lines.length < 3 || lines.length % 2 !== 1) return false;
2573
+ if (!lines.every((line, index) => {
2574
+ return line.clock && Number.isSafeInteger(line.start) && line.row !== lines[index - 1]?.row;
2575
+ })) return false;
2576
+ let previous = lines[0];
2577
+ for (let index = 1; index < lines.length; index += 2) {
2578
+ const translation = lines[index];
2579
+ const original = lines[index + 1];
2580
+ if (translation.start <= previous.start || translation.start !== original.start) return false;
2581
+ if (!VISIBLE_TEXT.test(readText$1(previous.text)) && VISIBLE_TEXT.test(readText$1(translation.text))) return false;
2582
+ previous = original;
2583
+ }
2584
+ return true;
2585
+ };
2586
+ //#endregion
2587
+ //#region src/formats/kwrc/detect.ts
2588
+ /**
2589
+ * Recognizes octal parameters with encoded clock rows, or a complete marked bilingual sequence.
2590
+ */
2591
+ var detectKwrc = (input) => {
2592
+ const source = readKwrcContent(input);
2593
+ if (!source) return false;
2594
+ const track = readKwrcSource(source.original);
2595
+ if (readKwrcScales(track.metaTags)) {
2596
+ for (const line of track.lines) if (line.clock && Number.isSafeInteger(line.start) && line.text.search(WORD_PATTERN$1) !== -1) return true;
2597
+ }
2598
+ return hasKwrcTranslations(track);
2599
+ };
2600
+ //#endregion
2601
+ //#region src/formats/kwrc/parser.ts
2602
+ var SEPARATORS$1 = /^\p{White_Space}+$/u;
2603
+ var readWordTime = (tag, lineStart, scales) => {
2604
+ const values = tag.split(",").map(Number);
2605
+ if (!Number.isSafeInteger(lineStart) || !values.every(Number.isSafeInteger)) return;
2606
+ const [x, y] = values;
2607
+ const sum = x + y;
2608
+ const difference = x - y;
2609
+ if (!Number.isSafeInteger(sum) || !Number.isSafeInteger(difference) || sum % (2 * scales.start) !== 0 || difference % (2 * scales.duration) !== 0) return;
2610
+ const relativeStart = Math.abs(sum / (2 * scales.start));
2611
+ const duration = Math.abs(difference / (2 * scales.duration));
2612
+ const start = lineStart + relativeStart;
2613
+ const end = start + duration;
2614
+ if (![
2615
+ relativeStart,
2616
+ duration,
2617
+ start,
2618
+ end
2619
+ ].every(Number.isSafeInteger)) return;
2620
+ return makeTimeRange({
2621
+ start,
2622
+ end
2623
+ });
2624
+ };
2625
+ var readLine$2 = ({ text, start }, nextStart, scales) => {
2626
+ const boundary = nextStart !== void 0 && nextStart >= start ? nextStart : void 0;
2627
+ let end = boundary;
2628
+ const pieces = scanWordTags(text, WORD_PATTERN$1);
2629
+ if (!pieces.some((piece) => piece.tag !== void 0)) {
2630
+ if (Number.isSafeInteger(start) && splitWordTags(text).some((piece) => piece.tag !== void 0)) return readLrc(`[${start}ms]${text}${end === void 0 ? "" : `\n[${end}ms]`}`).lines[0];
2631
+ const time = makeTimeRange({
2632
+ start,
2633
+ end
2634
+ });
2635
+ return VISIBLE_TEXT.test(text) ? makeLineNormal({
2636
+ time,
2637
+ content: makeLineContentText(text)
2638
+ }) : makeLineInstrumental({ time });
2639
+ }
2640
+ const words = [];
2641
+ for (const piece of pieces) {
2642
+ const time = piece.tag !== void 0 && scales ? readWordTime(piece.tag, start, scales) : void 0;
2643
+ if (piece.tag !== void 0 && !time) return makeLineNormal({
2644
+ time: makeTimeRange({
2645
+ start,
2646
+ end: boundary
2647
+ }),
2648
+ content: makeLineContentText(text)
2649
+ });
2650
+ if (time?.end !== void 0) end = Math.max(end ?? start, time.end);
2651
+ for (const fragment of piece.text.split(/(\p{White_Space}+)/u)) if (fragment) words.push(SEPARATORS$1.test(fragment) ? makeWordSpace({ text: fragment }) : makeWordNormal({
2652
+ text: fragment,
2653
+ time
2654
+ }));
2655
+ }
2656
+ const time = makeTimeRange({
2657
+ start,
2658
+ end
2659
+ });
2660
+ return VISIBLE_TEXT.test(readText$1(text)) ? makeLineNormal({
2661
+ time,
2662
+ content: makeLineContentWords(words)
2663
+ }) : makeLineInstrumental({ time });
2664
+ };
2665
+ /**
2666
+ * Parses decoded KWRC with explicit word durations and structurally verified embedded translations.
2667
+ *
2668
+ * Missing or conflicting parameters and unsafe word calculations retain the complete row as line-level text.
2669
+ * Plain LRC remains readable; offset is stored as metadata without changing source timestamps.
2670
+ *
2671
+ * @param input Decoded plaintext or an object containing its original text.
2672
+ *
2673
+ * @returns Canonical lyrics and model diagnostics; no readable timed rows yields INVALID status.
2674
+ */
2675
+ var parseKwrc = (input) => {
2676
+ const source = readKwrcContent(input);
2677
+ if (!source) return finalizeLyric(makeLyric({
2678
+ format: FORMAT$1,
2679
+ status: LyricStatus.INVALID
2680
+ }));
2681
+ const track = readKwrcSource(source.original);
2682
+ const bilingual = hasKwrcTranslations(track);
2683
+ const encoded = track.lines.some((line) => !!line.text.match(WORD_PATTERN$1));
2684
+ let lines;
2685
+ let tags = track.metaTags;
2686
+ if (!encoded && !bilingual) {
2687
+ const fallback = readLrc(source.original);
2688
+ lines = fallback.lines;
2689
+ tags = fallback.metaTags;
2690
+ } else {
2691
+ const scales = readKwrcScales(tags);
2692
+ const originals = bilingual ? track.lines.filter((_, index) => index % 2 === 0) : track.lines;
2693
+ lines = originals.map((original, index) => {
2694
+ const line = readLine$2(original, originals[index + 1]?.start, scales);
2695
+ const translation = bilingual ? readText$1(track.lines[index * 2 + 1]?.text ?? "") : "";
2696
+ if (VISIBLE_TEXT.test(translation)) line.annotation = makeLineAnnotation({ translations: [makeLineAnnotationTranslation({ text: translation })] });
2697
+ return line;
2698
+ });
2699
+ }
2700
+ const last = lines.at(-1);
2701
+ if (last?.type === LineType.INSTRUMENTAL && (last.time?.end === void 0 || last.time.end === last.time.start)) lines.pop();
2702
+ return finalizeLyric(makeLyric({
2703
+ format: FORMAT$1,
2704
+ status: lines.length ? LyricStatus.VALID : LyricStatus.INVALID,
2705
+ timing: lines.some(isWordLevelLine) ? Timing.WORD : Timing.LINE,
2706
+ meta: readLrcMetaTags(tags),
2707
+ lines,
2708
+ languages: deriveLanguageUsages(lines)
2709
+ }));
2710
+ };
2711
+ //#endregion
2189
2712
  //#region src/formats/ttml/common.ts
2190
2713
  var FORMAT = "ttml";
2191
2714
  var AMLL_HINT_PATTERN = /xmlns:amll|amll:meta/iu;
@@ -2896,7 +3419,7 @@ var readInlineAnnotations = (element, preserveSpace, language) => {
2896
3419
  /**
2897
3420
  * Reads one paragraph, pulling its inline annotations and background lines out of the word stream.
2898
3421
  */
2899
- var readLine = (element, preserveSpace, language) => {
3422
+ var readLine$1 = (element, preserveSpace, language) => {
2900
3423
  preserveSpace = readPreserveSpace(element, preserveSpace);
2901
3424
  language = readLanguage(element, language);
2902
3425
  const time = readLineTime(element);
@@ -3177,7 +3700,7 @@ var parseTtml = (input, options = {}) => {
3177
3700
  const lineMap = /* @__PURE__ */ new Map();
3178
3701
  const backgroundMap = /* @__PURE__ */ new Map();
3179
3702
  for (const { element, preserveSpace, language: lineLanguage } of body ? findElements(body.element, "p", body.preserveSpace, true, body.language) : []) {
3180
- const draft = readLine(element, preserveSpace, lineLanguage);
3703
+ const draft = readLine$1(element, preserveSpace, lineLanguage);
3181
3704
  if (!draft) continue;
3182
3705
  drafts.push(draft);
3183
3706
  if (draft.key) lineMap.set(draft.key, draft);
@@ -3208,6 +3731,260 @@ var parseTtml = (input, options = {}) => {
3208
3731
  }));
3209
3732
  };
3210
3733
  //#endregion
3734
+ //#region src/formats/yrc/common.ts
3735
+ var LINE_PATTERN = /^\[(\d+),(\d+)\]/;
3736
+ var WORD_PATTERN = /\((\d+,\d+,\d+)\)/g;
3737
+ /**
3738
+ * Accepts plaintext tracks on one timeline without interpreting an API response or an XML container.
3739
+ *
3740
+ * @returns Original and optional auxiliary text, or null when the original payload is unreadable.
3741
+ */
3742
+ var readYrcContent = (input) => {
3743
+ const value = typeof input === "string" ? { original: input } : input;
3744
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
3745
+ const source = value;
3746
+ if (typeof source.original !== "string" || /^\s*</.test(source.original)) return null;
3747
+ return {
3748
+ original: source.original,
3749
+ translations: typeof source.translations === "string" ? source.translations : void 0,
3750
+ romans: typeof source.romans === "string" ? source.romans : void 0
3751
+ };
3752
+ };
3753
+ //#endregion
3754
+ //#region src/formats/yrc/parser.ts
3755
+ var SEPARATORS = /^\p{White_Space}+$/u;
3756
+ var CREDIT_LABEL_PATTERN = /[::]$/u;
3757
+ var isObject = (value) => {
3758
+ return !!value && typeof value === "object" && !Array.isArray(value);
3759
+ };
3760
+ var readCredit = (fragments) => {
3761
+ const label = trimRuleWhitespace(fragments[0] ?? "");
3762
+ const raw = trimRuleWhitespace(label.replace(CREDIT_LABEL_PATTERN, ""));
3763
+ if (!raw) return;
3764
+ const names = fragments.slice(1).join("").split("/").map(trimRuleWhitespace).filter(Boolean);
3765
+ return names.length ? {
3766
+ raw,
3767
+ names
3768
+ } : void 0;
3769
+ };
3770
+ /**
3771
+ * Reads JSON display text and splits the joined credit content at slash separators.
3772
+ *
3773
+ * @returns A readable display line and whether all source fields are represented, or null for other JSON.
3774
+ */
3775
+ var readJsonLine = (text) => {
3776
+ try {
3777
+ const value = JSON.parse(text);
3778
+ if (!isObject(value) || typeof value.t !== "number" || !Number.isSafeInteger(value.t) || value.t < 0 || !Array.isArray(value.c)) return null;
3779
+ const fragments = [];
3780
+ let complete = Object.keys(value).every((key) => key === "t" || key === "c");
3781
+ for (const fragment of value.c) {
3782
+ if (!isObject(fragment) || typeof fragment.tx !== "string") return null;
3783
+ fragments.push(fragment.tx);
3784
+ complete = complete && Object.keys(fragment).every((key) => key === "tx");
3785
+ }
3786
+ const content = fragments.join("");
3787
+ return /[^\p{White_Space}]/u.test(content) ? {
3788
+ start: value.t,
3789
+ text: content,
3790
+ complete,
3791
+ credit: readCredit(fragments)
3792
+ } : null;
3793
+ } catch {
3794
+ return null;
3795
+ }
3796
+ };
3797
+ var appendWords = (words, text, time) => {
3798
+ for (const fragment of text.split(/(\p{White_Space}+)/u)) if (fragment) words.push(SEPARATORS.test(fragment) ? makeWordSpace({ text: fragment }) : makeWordNormal({
3799
+ text: fragment,
3800
+ time
3801
+ }));
3802
+ };
3803
+ var readLine = (body, start, duration) => {
3804
+ const time = makeTimeRange({
3805
+ start,
3806
+ end: start + duration
3807
+ });
3808
+ const pieces = scanWordTags(body, WORD_PATTERN);
3809
+ if (!pieces.some((piece) => piece.tag !== void 0)) return /[^\p{White_Space}]/u.test(body) ? makeLineNormal({
3810
+ time,
3811
+ content: makeLineContentText(body)
3812
+ }) : makeLineInstrumental({ time });
3813
+ const words = [];
3814
+ for (const piece of pieces) {
3815
+ if (piece.tag === void 0) {
3816
+ appendWords(words, piece.text);
3817
+ continue;
3818
+ }
3819
+ const [wordStart, wordDuration] = piece.tag.split(",").map(Number);
3820
+ appendWords(words, piece.text, makeTimeRange({
3821
+ start: wordStart,
3822
+ end: wordStart + wordDuration
3823
+ }));
3824
+ }
3825
+ return words.some((word) => word.type === WordType.NORMAL) ? makeLineNormal({
3826
+ time,
3827
+ content: makeLineContentWords(words)
3828
+ }) : makeLineInstrumental({ time });
3829
+ };
3830
+ var readTrack = (text, timings) => {
3831
+ const tags = [];
3832
+ const native = [];
3833
+ const fallback = [];
3834
+ const displays = [];
3835
+ const rows = /^\s*</.test(text) ? [] : text.replace(/^\uFEFF/, "").split(/\r\n?|\n/);
3836
+ for (const [row, raw] of rows.entries()) {
3837
+ const line = raw.trimStart();
3838
+ const header = LINE_PATTERN.exec(line);
3839
+ if (header) {
3840
+ native.push({
3841
+ row,
3842
+ line: readLine(line.slice(header[0].length), Number(header[1]), Number(header[2]))
3843
+ });
3844
+ fallback.push("");
3845
+ } else if (line.startsWith("{")) {
3846
+ const display = readJsonLine(line);
3847
+ if (display) {
3848
+ displays.push({
3849
+ row,
3850
+ display: true,
3851
+ credit: !!display.credit,
3852
+ line: makeLineNormal({
3853
+ time: makeTimeRange({ start: display.start }),
3854
+ content: makeLineContentText(display.text)
3855
+ })
3856
+ });
3857
+ if (display.credit) {
3858
+ const { raw, names } = display.credit;
3859
+ for (const value of names) tags.push({
3860
+ row,
3861
+ unknown: true,
3862
+ tag: {
3863
+ key: raw,
3864
+ value
3865
+ }
3866
+ });
3867
+ }
3868
+ }
3869
+ if (!display?.complete) tags.push({
3870
+ row,
3871
+ unknown: true,
3872
+ tag: {
3873
+ key: "yrc:json",
3874
+ value: raw
3875
+ }
3876
+ });
3877
+ fallback.push(display ? `[${display.start}ms]` : "");
3878
+ } else fallback.push(raw);
3879
+ }
3880
+ const clock = [];
3881
+ const displayRows = new Set(displays.map((entry) => entry.row));
3882
+ readLrc(fallback.join("\n"), {
3883
+ onLine: (line, timing, row) => {
3884
+ if (displayRows.has(row)) return;
3885
+ clock.push({
3886
+ row,
3887
+ line
3888
+ });
3889
+ timings?.set(line, timing);
3890
+ },
3891
+ onMeta: (tag, row) => {
3892
+ tags.push({
3893
+ row,
3894
+ tag
3895
+ });
3896
+ }
3897
+ });
3898
+ const entries = [...displays, ...native.length ? native : clock].sort((first, second) => first.row - second.row);
3899
+ for (const [index, entry] of entries.entries()) {
3900
+ if (!entry.display) continue;
3901
+ const next = entries[index + 1]?.line.time?.start;
3902
+ if (next !== void 0 && next >= entry.line.time.start) entry.line.time.end = next;
3903
+ }
3904
+ const metaTags = tags.sort((first, second) => first.row - second.row);
3905
+ return {
3906
+ entries: entries.filter((entry) => !entry.credit),
3907
+ metaTags
3908
+ };
3909
+ };
3910
+ var readAnnotations = (text, timings) => {
3911
+ return text ? readTrack(text, timings).entries.filter((entry) => !entry.display).map((entry) => entry.line) : [];
3912
+ };
3913
+ /**
3914
+ * Parses native YRC or LRC fallback, keeping JSON display rows separate from annotation matching.
3915
+ * Explicit line and word durations remain unchanged, including zero durations and source timing violations.
3916
+ * JSON credit content becomes separate plain-text unknown entries sharing the source label and is omitted from lyric lines.
3917
+ *
3918
+ * @param input Plaintext tracks on the same timeline: yrc with ytlrc/yromalrc, or lrc with tlyric/romalrc.
3919
+ * @param options Independent line and word tolerances, with optional local roman recovery.
3920
+ *
3921
+ * @returns Canonical lyrics and model diagnostics; no readable timed rows yields INVALID status.
3922
+ */
3923
+ var parseYrc = (input, options = {}) => {
3924
+ const source = readYrcContent(input);
3925
+ if (!source) return finalizeLyric(makeLyric({
3926
+ format: "yrc",
3927
+ status: LyricStatus.INVALID
3928
+ }));
3929
+ const parsed = readTrack(source.original);
3930
+ const meta = readLrcMetaTags(parsed.metaTags.filter((entry) => !entry.unknown).map((entry) => entry.tag));
3931
+ let unknownIndex = 0;
3932
+ meta.unknowns = parsed.metaTags.flatMap(({ tag, unknown }) => {
3933
+ if (unknown) return [makeUnknownEntry(tag)];
3934
+ const entry = meta.unknowns[unknownIndex];
3935
+ if (entry?.key === tag.key && entry.value === tag.value) {
3936
+ unknownIndex += 1;
3937
+ return [entry];
3938
+ }
3939
+ return [];
3940
+ });
3941
+ const body = parsed.entries.filter((entry) => !entry.display).map((entry) => entry.line);
3942
+ const timings = /* @__PURE__ */ new WeakMap();
3943
+ const annotated = attachAnnotations(body, {
3944
+ translations: readAnnotations(source.translations, timings),
3945
+ romans: readAnnotations(source.romans, timings)
3946
+ }, {
3947
+ preservePlaceholders: true,
3948
+ tolerance: options.annotationTolerance,
3949
+ wordTolerance: options.wordAnnotationTolerance,
3950
+ wordFallback: options.wordAnnotationFallback,
3951
+ getTiming: (line) => timings.get(line)
3952
+ });
3953
+ let index = 0;
3954
+ const lines = parsed.entries.map((entry) => {
3955
+ return entry.display ? entry.line : annotated[index++];
3956
+ });
3957
+ return finalizeLyric(makeLyric({
3958
+ format: "yrc",
3959
+ status: lines.length ? LyricStatus.VALID : LyricStatus.INVALID,
3960
+ timing: lines.some(isWordLevelLine) ? Timing.WORD : Timing.LINE,
3961
+ meta,
3962
+ lines,
3963
+ languages: deriveLanguageUsages(lines)
3964
+ }));
3965
+ };
3966
+ //#endregion
3967
+ //#region src/formats/yrc/detect.ts
3968
+ /**
3969
+ * Recognizes native word tags or a JSON display line accompanied by LRC content in the original track.
3970
+ *
3971
+ * @returns Whether the original plaintext carries a YRC signal stronger than its shared QRC line header.
3972
+ */
3973
+ var detectYrc = (input) => {
3974
+ const source = readYrcContent(input);
3975
+ if (!source) return false;
3976
+ let display = false;
3977
+ const fallback = [];
3978
+ for (const raw of source.original.split(/\r\n?|\n/)) {
3979
+ const line = raw.trimStart();
3980
+ const header = LINE_PATTERN.exec(line);
3981
+ if (header && line.slice(header[0].length).match(WORD_PATTERN)) return true;
3982
+ if (line.startsWith("{")) display = display || !!readJsonLine(line);
3983
+ else fallback.push(raw);
3984
+ }
3985
+ return display && detectLrc(fallback.join("\n"));
3986
+ };
3987
+ //#endregion
3211
3988
  //#region src/formats/index.ts
3212
3989
  /**
3213
3990
  * Recognition reads only the original text; the annotation tracks never decide the format.
@@ -3215,7 +3992,10 @@ var parseTtml = (input, options = {}) => {
3215
3992
  * Unrecognized text returns `undefined`.
3216
3993
  */
3217
3994
  var detectFormat = (input) => {
3995
+ if (detectKrc(input)) return "krc";
3996
+ if (detectYrc(input)) return "yrc";
3218
3997
  if (detectQrc(input)) return "qrc";
3998
+ if (detectKwrc(input)) return "kwrc";
3219
3999
  if (detectLrc(input)) return "lrc";
3220
4000
  if (detectTtml(input)) return "ttml";
3221
4001
  };
@@ -3227,9 +4007,12 @@ var parse = (input, options = {}) => {
3227
4007
  const format = detectFormat(input);
3228
4008
  if (!format) return null;
3229
4009
  switch (format) {
4010
+ case "krc": return parseKrc(input);
4011
+ case "kwrc": return parseKwrc(input);
3230
4012
  case "lrc": return parseLrc(input, options);
3231
4013
  case "qrc": return parseQrc(input, options);
3232
4014
  case "ttml": return parseTtml(input, options);
4015
+ case "yrc": return parseYrc(input, options);
3233
4016
  }
3234
4017
  };
3235
4018
  var GENERATORS = {
@@ -3247,6 +4030,6 @@ var generate = (lyric, target, options = {}) => {
3247
4030
  return GENERATORS[target](lyric, options);
3248
4031
  };
3249
4032
  //#endregion
3250
- export { detectFormat, detectLrc, detectQrc, detectTtml, generate, generateLrc, generateTtml, parse, parseLrc, parseQrc, parseTtml, readLrc };
4033
+ export { META_PATTERN, TAG_PATTERN, attachRubies, detectFormat, detectKrc, detectKwrc, detectLrc, detectQrc, detectTtml, detectYrc, generate, generateLrc, generateTtml, isPlaceholderText, parse, parseKrc, parseKwrc, parseLrc, parseQrc, parseTtml, parseWordTime, parseYrc, readJsonLine, readKrc, readLrc, readLrcMetaTags, readRubyEntries, scanWordTags, splitWordTags };
3251
4034
 
3252
4035
  //# sourceMappingURL=formats.js.map