@gondola-lyrics/kit 0.2.0 → 0.3.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,13 +1,13 @@
1
- import { AgentType, CreditRole, LineType, LyricStatus, Timing, WordType, canonicalizeLyric, deriveLanguageUsages, getWordsText, makeAgent, makeLineAnnotation, makeLineAnnotationRoman, makeLineAnnotationTranslation, makeLineBackground, makeLineContentText, makeLineContentWords, makeLineNormal, makeLyric, makeMeta, makeMetaCredit, makeText, makeTimeRange, makeUnknownEntry, makeWordAnnotation, makeWordAnnotationRuby, makeWordAnnotationToken, makeWordNormal, makeWordSpace, validateLyric } from "@gondola-lyrics/model/runtime";
1
+ import { AgentType, CreditRole, LineType, LyricStatus, Timing, WordType, canonicalizeLyric, deriveLanguageUsages, getWordsText, makeAgent, makeLineAnnotation, makeLineAnnotationRoman, makeLineAnnotationTranslation, makeLineBackground, makeLineContentText, makeLineContentWords, makeLineNormal, makeLyric, makeMeta, makeMetaCredit, makeText, makeTimeRange, makeUnknownEntry, makeWordAnnotation, makeWordAnnotationRuby, makeWordAnnotationToken, makeWordNormal, makeWordSpace, removeOffset, resolveAnnotationItemTime, resolveAnnotationTokenTime, validateLyric } from "@gondola-lyrics/model/runtime";
2
2
  //#region src/utils/annotation.ts
3
3
  /**
4
4
  * An annotation line carries its text in either field, depending on whether the track used word tags.
5
5
  */
6
- var readText = (line) => {
6
+ var readText$1 = (line) => {
7
7
  return (line.content?.text ?? getWordsText(line.content?.words ?? [])).trim();
8
8
  };
9
9
  var isReadable = (line) => {
10
- return line.time?.start !== void 0 && readText(line) !== "";
10
+ return line.time?.start !== void 0 && readText$1(line) !== "";
11
11
  };
12
12
  /**
13
13
  * Assigns each annotation line to a lyric line index.
@@ -79,11 +79,11 @@ var attachLineAnnotations = (lines, sources, options = {}) => {
79
79
  ...line,
80
80
  annotation: makeLineAnnotation({
81
81
  romans: [...line.annotation?.romans ?? [], ...roman === void 0 ? [] : [makeLineAnnotationRoman({
82
- text: readText(roman),
82
+ text: readText$1(roman),
83
83
  language: roman.languages[0]
84
84
  })]],
85
85
  translations: [...line.annotation?.translations ?? [], ...translation === void 0 ? [] : [makeLineAnnotationTranslation({
86
- text: readText(translation),
86
+ text: readText$1(translation),
87
87
  language: translation.languages[0]
88
88
  })]]
89
89
  })
@@ -233,6 +233,32 @@ var createXmlAttribute = (qualifiedName, value) => {
233
233
  return attribute;
234
234
  };
235
235
  /**
236
+ * Creates an element from its full name, with attributes and children in the order given.
237
+ */
238
+ var createXmlElement = (qualifiedName, attributes = [], children = []) => {
239
+ const element = {
240
+ nodeType: XmlNodeType.element,
241
+ qualifiedName,
242
+ namespacePrefix: "",
243
+ localName: qualifiedName,
244
+ attributes: attributes.map(([name, value]) => {
245
+ return createXmlAttribute(name, value);
246
+ }),
247
+ children: [...children]
248
+ };
249
+ applyXmlNameParts(qualifiedName, element);
250
+ return element;
251
+ };
252
+ /**
253
+ * Creates a text node, whose value the writer escapes.
254
+ */
255
+ var createXmlText = (text) => {
256
+ return {
257
+ nodeType: XmlNodeType.text,
258
+ text
259
+ };
260
+ };
261
+ /**
236
262
  * Returns the value of one attribute, matched by its local name so the prefix never has to be known.
237
263
  */
238
264
  var getXmlAttribute = (element, localName) => {
@@ -291,27 +317,13 @@ var findXmlElement = (element, localName) => {
291
317
  }
292
318
  };
293
319
  /**
294
- * Returns every element carrying one local name, the element itself included, in document order.
295
- */
296
- var findXmlElements = (element, localName) => {
297
- const result = [];
298
- const stack = [element];
299
- while (stack.length > 0) {
300
- const node = stack.pop();
301
- if (node === void 0 || node.nodeType !== XmlNodeType.element) continue;
302
- if (node.localName === localName) result.push(node);
303
- for (let index = node.children.length - 1; index >= 0; index -= 1) {
304
- const child = node.children[index];
305
- if (child !== void 0) stack.push(child);
306
- }
307
- }
308
- return result;
309
- };
310
- /**
311
320
  * Joins the text and CDATA content of a node and its descendants, in document order.
312
321
  */
313
322
  var getXmlTextContent = (node) => {
314
323
  if (node.nodeType !== XmlNodeType.element) return node.text;
324
+ if (node.children.length === 0) return "";
325
+ const child = node.children[0];
326
+ if (node.children.length === 1 && child !== void 0 && child.nodeType !== XmlNodeType.element) return child.text;
315
327
  const parts = [];
316
328
  const stack = [node];
317
329
  while (stack.length > 0) {
@@ -330,21 +342,21 @@ var getXmlTextContent = (node) => {
330
342
  };
331
343
  //#endregion
332
344
  //#region src/utils/xml/reader.ts
333
- var LESS_THAN = 60;
334
- var GREATER_THAN = 62;
345
+ var LESS_THAN$1 = 60;
346
+ var GREATER_THAN$1 = 62;
335
347
  var SLASH = 47;
336
348
  var EQUALS = 61;
337
- var DOUBLE_QUOTE = 34;
349
+ var DOUBLE_QUOTE$1 = 34;
338
350
  var SINGLE_QUOTE = 39;
339
351
  var EXCLAMATION = 33;
340
352
  var QUESTION = 63;
341
- var AMPERSAND = 38;
353
+ var AMPERSAND$1 = 38;
342
354
  var HASH = 35;
343
355
  var HYPHEN = 45;
344
356
  var LOWER_X = 120;
345
357
  var UPPER_X = 88;
346
358
  var BYTE_ORDER_MARK = 65279;
347
- var ROOT_NAME = "#document";
359
+ var ROOT_NAME$1 = "#document";
348
360
  var DECIMAL_DIGITS = /^\d+$/;
349
361
  var HEX_DIGITS = /^[0-9a-fA-F]+$/;
350
362
  /**
@@ -375,7 +387,7 @@ var decodeText = (text) => {
375
387
  let start = 0;
376
388
  let cursor = 0;
377
389
  while (cursor < length) {
378
- if (text.charCodeAt(cursor) !== AMPERSAND) {
390
+ if (text.charCodeAt(cursor) !== AMPERSAND$1) {
379
391
  cursor += 1;
380
392
  continue;
381
393
  }
@@ -402,9 +414,9 @@ var parseXml = (source) => {
402
414
  const stack = [];
403
415
  const root = {
404
416
  nodeType: XmlNodeType.element,
405
- qualifiedName: ROOT_NAME,
417
+ qualifiedName: ROOT_NAME$1,
406
418
  namespacePrefix: "",
407
- localName: ROOT_NAME,
419
+ localName: ROOT_NAME$1,
408
420
  attributes: [],
409
421
  children: []
410
422
  };
@@ -432,12 +444,12 @@ var parseXml = (source) => {
432
444
  */
433
445
  const readQuotedValue = () => {
434
446
  const quote = source.charCodeAt(position);
435
- if (quote !== DOUBLE_QUOTE && quote !== SINGLE_QUOTE) {
447
+ if (quote !== DOUBLE_QUOTE$1 && quote !== SINGLE_QUOTE) {
436
448
  valid = false;
437
449
  return "";
438
450
  }
439
451
  position += 1;
440
- const end = source.indexOf(quote === DOUBLE_QUOTE ? "\"" : "'", position);
452
+ const end = source.indexOf(quote === DOUBLE_QUOTE$1 ? "\"" : "'", position);
441
453
  if (end === -1) {
442
454
  valid = false;
443
455
  return "";
@@ -454,7 +466,7 @@ var parseXml = (source) => {
454
466
  const nameStart = cursor;
455
467
  while (cursor < length) {
456
468
  const code = source.charCodeAt(cursor);
457
- if (code <= 32 || code === GREATER_THAN || code === SLASH || code === EQUALS) break;
469
+ if (code <= 32 || code === GREATER_THAN$1 || code === SLASH || code === EQUALS) break;
458
470
  cursor += 1;
459
471
  }
460
472
  if (cursor === nameStart) {
@@ -482,14 +494,14 @@ var parseXml = (source) => {
482
494
  code = source.charCodeAt(cursor);
483
495
  }
484
496
  if (cursor >= length) break;
485
- if (code === GREATER_THAN) {
497
+ if (code === GREATER_THAN$1) {
486
498
  cursor += 1;
487
499
  closed = true;
488
500
  break;
489
501
  }
490
502
  if (code === SLASH) {
491
503
  cursor += 1;
492
- if (cursor < length && source.charCodeAt(cursor) === GREATER_THAN) {
504
+ if (cursor < length && source.charCodeAt(cursor) === GREATER_THAN$1) {
493
505
  cursor += 1;
494
506
  closed = true;
495
507
  selfClosing = true;
@@ -503,7 +515,7 @@ var parseXml = (source) => {
503
515
  cursor += 1;
504
516
  if (cursor >= length) break;
505
517
  code = source.charCodeAt(cursor);
506
- } while (code > 32 && code !== GREATER_THAN && code !== SLASH && code !== EQUALS);
518
+ } while (code > 32 && code !== GREATER_THAN$1 && code !== SLASH && code !== EQUALS);
507
519
  if (cursor === attributeStart) {
508
520
  valid = false;
509
521
  return;
@@ -547,7 +559,7 @@ var parseXml = (source) => {
547
559
  const nameStart = cursor;
548
560
  while (cursor < length) {
549
561
  const code = source.charCodeAt(cursor);
550
- if (code <= 32 || code === GREATER_THAN) break;
562
+ if (code <= 32 || code === GREATER_THAN$1) break;
551
563
  cursor += 1;
552
564
  }
553
565
  const name = source.slice(nameStart, cursor);
@@ -604,8 +616,8 @@ var parseXml = (source) => {
604
616
  let depth = 1;
605
617
  while (cursor < length && depth > 0) {
606
618
  const code = source.charCodeAt(cursor);
607
- if (code === LESS_THAN) depth += 1;
608
- else if (code === GREATER_THAN) depth -= 1;
619
+ if (code === LESS_THAN$1) depth += 1;
620
+ else if (code === GREATER_THAN$1) depth -= 1;
609
621
  cursor += 1;
610
622
  }
611
623
  if (depth > 0) {
@@ -654,6 +666,159 @@ var parseXml = (source) => {
654
666
  return null;
655
667
  };
656
668
  //#endregion
669
+ //#region src/utils/xml/writer.ts
670
+ var AMPERSAND = 38;
671
+ var LESS_THAN = 60;
672
+ var GREATER_THAN = 62;
673
+ var DOUBLE_QUOTE = 34;
674
+ var TAB = 9;
675
+ var LINE_FEED = 10;
676
+ var CARRIAGE_RETURN = 13;
677
+ var ROOT_NAME = "#document";
678
+ var REPLACEMENT_CHARACTER = "�";
679
+ var XML_DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
680
+ var DEFAULT_INDENT = " ";
681
+ /**
682
+ * Reports whether a code point may appear in an XML 1.0 document.
683
+ */
684
+ var isAllowedCodePoint = (code) => {
685
+ return code === TAB || code === LINE_FEED || code === CARRIAGE_RETURN || code >= 32 && code <= 55295 || code >= 57344 && code <= 65533 || code >= 65536 && code <= 1114111;
686
+ };
687
+ /**
688
+ * Escapes text content, replacing characters XML cannot carry so corruption stays visible.
689
+ */
690
+ var escapeXmlText = (text, chunks) => {
691
+ const length = text.length;
692
+ let start = 0;
693
+ for (let index = 0; index < length; index += 1) {
694
+ const code = text.charCodeAt(index);
695
+ let escaped = "";
696
+ if (code === AMPERSAND) escaped = "&amp;";
697
+ else if (code === LESS_THAN) escaped = "&lt;";
698
+ else if (code === GREATER_THAN) escaped = "&gt;";
699
+ else if (code === CARRIAGE_RETURN) escaped = "&#13;";
700
+ else if (code >= 55296 && code <= 56319) {
701
+ const low = index + 1 < length ? text.charCodeAt(index + 1) : 0;
702
+ if (low < 56320 || low > 57343) escaped = REPLACEMENT_CHARACTER;
703
+ else {
704
+ index += 1;
705
+ continue;
706
+ }
707
+ } else if (code >= 56320 && code <= 57343) escaped = REPLACEMENT_CHARACTER;
708
+ else if (!isAllowedCodePoint(code)) escaped = REPLACEMENT_CHARACTER;
709
+ if (escaped === "") continue;
710
+ if (start < index) chunks.push(text.slice(start, index));
711
+ chunks.push(escaped);
712
+ start = index + 1;
713
+ }
714
+ if (start === 0) {
715
+ chunks.push(text);
716
+ return;
717
+ }
718
+ if (start < length) chunks.push(text.slice(start));
719
+ };
720
+ /**
721
+ * Escapes an attribute value, where whitespace also has to survive normalization.
722
+ */
723
+ var escapeXmlAttribute = (value, chunks) => {
724
+ const length = value.length;
725
+ let start = 0;
726
+ for (let index = 0; index < length; index += 1) {
727
+ const code = value.charCodeAt(index);
728
+ let escaped = "";
729
+ if (code === AMPERSAND) escaped = "&amp;";
730
+ else if (code === LESS_THAN) escaped = "&lt;";
731
+ else if (code === GREATER_THAN) escaped = "&gt;";
732
+ else if (code === DOUBLE_QUOTE) escaped = "&quot;";
733
+ else if (code === TAB) escaped = "&#9;";
734
+ else if (code === LINE_FEED) escaped = "&#10;";
735
+ else if (code === CARRIAGE_RETURN) escaped = "&#13;";
736
+ else if (code >= 55296 && code <= 56319) {
737
+ const low = index + 1 < length ? value.charCodeAt(index + 1) : 0;
738
+ if (low < 56320 || low > 57343) escaped = REPLACEMENT_CHARACTER;
739
+ else {
740
+ index += 1;
741
+ continue;
742
+ }
743
+ } else if (code >= 56320 && code <= 57343) escaped = REPLACEMENT_CHARACTER;
744
+ else if (!isAllowedCodePoint(code)) escaped = REPLACEMENT_CHARACTER;
745
+ if (escaped === "") continue;
746
+ if (start < index) chunks.push(value.slice(start, index));
747
+ chunks.push(escaped);
748
+ start = index + 1;
749
+ }
750
+ if (start === 0) {
751
+ chunks.push(value);
752
+ return;
753
+ }
754
+ if (start < length) chunks.push(value.slice(start));
755
+ };
756
+ /**
757
+ * Reports whether every child is an element, which is the only case indentation can add whitespace to.
758
+ *
759
+ * An element holding any character data is mixed content, where the inserted whitespace would join the text.
760
+ */
761
+ var hasOnlyElements = (element) => {
762
+ const children = element.children;
763
+ for (let index = 0; index < children.length; index += 1) {
764
+ const child = children[index];
765
+ if (child !== void 0 && child.nodeType !== XmlNodeType.element) return false;
766
+ }
767
+ return true;
768
+ };
769
+ /**
770
+ * Serializes one node, appending its pieces to `chunks`.
771
+ */
772
+ var writeXmlNode = (node, chunks, depth, indent) => {
773
+ if (node.nodeType === XmlNodeType.text) {
774
+ escapeXmlText(node.text, chunks);
775
+ return;
776
+ }
777
+ if (node.nodeType === XmlNodeType.cdata) {
778
+ chunks.push("<![CDATA[", node.text.replaceAll("]]>", "]]]]><![CDATA[>"), "]]>");
779
+ return;
780
+ }
781
+ if (indent !== void 0 && depth > 0) chunks.push("\n", indent.repeat(depth));
782
+ chunks.push("<", node.qualifiedName);
783
+ for (let index = 0; index < node.attributes.length; index += 1) {
784
+ const attribute = node.attributes[index];
785
+ if (attribute === void 0) continue;
786
+ chunks.push(" ", attribute.qualifiedName, "=\"");
787
+ escapeXmlAttribute(attribute.value, chunks);
788
+ chunks.push("\"");
789
+ }
790
+ const children = node.children;
791
+ if (children.length === 0) {
792
+ chunks.push("/>");
793
+ return;
794
+ }
795
+ chunks.push(">");
796
+ const childIndent = indent !== void 0 && hasOnlyElements(node) ? indent : void 0;
797
+ for (let index = 0; index < children.length; index += 1) {
798
+ const child = children[index];
799
+ if (child !== void 0) writeXmlNode(child, chunks, depth + 1, childIndent);
800
+ }
801
+ if (childIndent !== void 0) chunks.push("\n", childIndent.repeat(depth));
802
+ chunks.push("</", node.qualifiedName, ">");
803
+ };
804
+ /**
805
+ * Writes a tree back to text.
806
+ *
807
+ * A `#document` root, as `parseXml` returns, writes its children; any other root writes itself.
808
+ */
809
+ var serializeXml = (root, options = {}) => {
810
+ const chunks = [];
811
+ if (options.declaration ?? true) chunks.push(XML_DECLARATION);
812
+ const indent = options.indent === true ? options.indentText ?? DEFAULT_INDENT : void 0;
813
+ if (indent !== void 0) chunks.push("\n");
814
+ if (root.qualifiedName === ROOT_NAME) for (let index = 0; index < root.children.length; index += 1) {
815
+ const child = root.children[index];
816
+ if (child !== void 0) writeXmlNode(child, chunks, 0, indent);
817
+ }
818
+ else writeXmlNode(root, chunks, 0, indent);
819
+ return chunks.join("");
820
+ };
821
+ //#endregion
657
822
  //#region src/formats/lrc/common.ts
658
823
  var readLrcContent = (input) => {
659
824
  if (typeof input === "string") return { original: input };
@@ -747,7 +912,7 @@ var detectLrc = (input) => {
747
912
  };
748
913
  //#endregion
749
914
  //#region src/formats/lrc/generator.ts
750
- var resolveOptions = (options = {}) => {
915
+ var resolveOptions$1 = (options = {}) => {
751
916
  return {
752
917
  wordTiming: options.wordTiming ?? "angle",
753
918
  timeFormat: options.timeFormat ?? "mm:ss.SSS",
@@ -830,7 +995,7 @@ var joinBlocks = (header, body) => {
830
995
  * Word tags appear only for a word-timed lyric when `wordTiming` is not `none`; otherwise every line stays whole.
831
996
  */
832
997
  var generateLrc = (lyric, options = {}) => {
833
- const resolved = resolveOptions(options);
998
+ const resolved = resolveOptions$1(options);
834
999
  const header = resolved.includeMeta && lyric.meta ? renderMetaRows(lyric.meta, resolved) : [];
835
1000
  const wordLevel = lyric.lines.some((line) => (line.content?.words.length ?? 0) > 0) && resolved.wordTiming !== "none";
836
1001
  const result = { original: joinBlocks(header, lyric.lines.map((line) => {
@@ -1109,15 +1274,12 @@ var parseLrc = (input, options = {}) => {
1109
1274
  var FORMAT = "ttml";
1110
1275
  var AMLL_HINT_PATTERN = /xmlns:amll|amll:meta/iu;
1111
1276
  var ITUNES_HINT_PATTERN = /xmlns:itunes|iTunesMetadata|itunes:timing=/iu;
1277
+ /**
1278
+ * Accepts the text input shared by TTML detection and parsing.
1279
+ */
1112
1280
  var readTtmlContent = (input) => {
1113
- if (typeof input === "string") return { original: input };
1114
- if (!input || typeof input !== "object") return null;
1115
- const source = input;
1116
- if (typeof source.original !== "string") return null;
1117
- return { original: source.original };
1118
- };
1119
- var readTtmlProfile = (source) => {
1120
- return AMLL_HINT_PATTERN.test(source) ? "amll" : "itunes";
1281
+ if (typeof input !== "string") return null;
1282
+ return input;
1121
1283
  };
1122
1284
  //#endregion
1123
1285
  //#region src/formats/ttml/detect.ts
@@ -1130,35 +1292,344 @@ var readTtmlProfile = (source) => {
1130
1292
  var detectTtml = (input) => {
1131
1293
  const content = readTtmlContent(input);
1132
1294
  if (!content) return false;
1133
- if (AMLL_HINT_PATTERN.test(content.original) || ITUNES_HINT_PATTERN.test(content.original)) return true;
1134
- const root = parseXml(content.original);
1295
+ if (AMLL_HINT_PATTERN.test(content) || ITUNES_HINT_PATTERN.test(content)) return true;
1296
+ const root = parseXml(content);
1135
1297
  if (!root) return false;
1136
1298
  for (const child of root.children) if (child.nodeType === XmlNodeType.element && child.localName === "tt") return true;
1137
1299
  return false;
1138
1300
  };
1301
+ //#endregion
1302
+ //#region src/formats/ttml/utils.ts
1303
+ var ROLE_BACKGROUND = "x-bg";
1304
+ var ROLE_TRANSLATION = "x-translation";
1305
+ var ROLE_ROMAN = "x-roman";
1306
+ var RUBY_CONTAINER = "container";
1307
+ var RUBY_BASE = "base";
1308
+ var RUBY_TEXT_CONTAINER = "textContainer";
1309
+ var RUBY_TEXT = "text";
1139
1310
  var TIMING_WORD = "Word";
1140
1311
  var TIMING_LINE = "Line";
1141
1312
  //#endregion
1142
- //#region src/formats/ttml/parser.ts
1313
+ //#region src/formats/ttml/generator.ts
1314
+ var NAMESPACE_TTML = "http://www.w3.org/ns/ttml";
1315
+ var NAMESPACE_TTML_METADATA = "http://www.w3.org/ns/ttml#metadata";
1316
+ var NAMESPACE_TTML_STYLING = "http://www.w3.org/ns/ttml#styling";
1317
+ var NAMESPACE_ITUNES = "http://itunes.apple.com/lyric-ttml-extensions";
1318
+ var NAMESPACE_ITUNES_METADATA = "http://music.apple.com/lyric-ttml-internal";
1319
+ var NAMESPACE_AMLL = "http://www.example.com/ns/amll";
1320
+ var ANNOTATION_KINDS = ["translations", "romans"];
1321
+ var XML_ID_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
1322
+ var resolveOptions = (lyric, options) => {
1323
+ const profile = options.profile ?? (lyric.format === "ttml" && lyric.extra.profile === "itunes" ? "itunes" : "amll");
1324
+ if (profile !== "amll" && profile !== "itunes") throw new Error("Unsupported TTML profile");
1325
+ return {
1326
+ profile,
1327
+ includeMeta: options.includeMeta ?? true,
1328
+ indent: options.indent ?? false
1329
+ };
1330
+ };
1143
1331
  /**
1144
- * Reads the span time range, which a word needs both ends of to be sung.
1332
+ * Reserves source ids before allocating keys, so unnamed entries cannot take later source ids.
1145
1333
  */
1146
- var readSpanTime = (element) => {
1147
- const begin = getXmlAttribute(element, "begin");
1148
- const end = getXmlAttribute(element, "end");
1149
- if (!begin || !end) return;
1150
- const start = parseTime(begin);
1151
- const finish = parseTime(end);
1152
- if (start === void 0 || finish === void 0) return;
1334
+ var makeIdAllocator = (ids) => {
1335
+ const reserved = new Set(ids.filter((id) => {
1336
+ return XML_ID_PATTERN.test(id);
1337
+ }));
1338
+ const used = /* @__PURE__ */ new Set();
1339
+ const counters = /* @__PURE__ */ new Map();
1340
+ return (preferred, prefix) => {
1341
+ if (XML_ID_PATTERN.test(preferred) && !used.has(preferred)) {
1342
+ used.add(preferred);
1343
+ return preferred;
1344
+ }
1345
+ let count = counters.get(prefix) ?? 0;
1346
+ let id;
1347
+ do {
1348
+ count += 1;
1349
+ id = `${prefix}${count}`;
1350
+ } while (reserved.has(id) || used.has(id));
1351
+ counters.set(prefix, count);
1352
+ used.add(id);
1353
+ return id;
1354
+ };
1355
+ };
1356
+ /**
1357
+ * Shares one mapping between declarations, body lines, and annotation references.
1358
+ */
1359
+ var makeContext = (lyric, lines, profile) => {
1360
+ const ids = lyric.agents.map((agent) => {
1361
+ return agent.id;
1362
+ });
1363
+ for (const line of lines) {
1364
+ ids.push(line.id);
1365
+ for (const background of line.backgrounds) ids.push(background.id);
1366
+ }
1367
+ const allocate = makeIdAllocator(ids);
1368
+ const agents = /* @__PURE__ */ new Map();
1369
+ const keys = /* @__PURE__ */ new Map();
1370
+ for (const agent of lyric.agents) {
1371
+ if (agents.has(agent.id)) throw new Error("TTML agent declarations require unique ids");
1372
+ agents.set(agent.id, allocate(agent.id, "v"));
1373
+ }
1374
+ for (const line of lines) {
1375
+ keys.set(line, allocate(line.id, "L"));
1376
+ for (const background of line.backgrounds) keys.set(background, allocate(background.id, "B"));
1377
+ }
1153
1378
  return {
1154
- start,
1155
- end: finish
1379
+ profile,
1380
+ offset: lyric.meta?.offset ?? 0,
1381
+ keys,
1382
+ agents
1156
1383
  };
1157
1384
  };
1158
1385
  /**
1159
- * Reads the line time range, which may leave its end open for the next line to supply.
1386
+ * Bakes the playback offset into TTML timestamps without clamping negative results.
1160
1387
  */
1161
- var readLineTime = (element) => {
1388
+ var renderTime = (time, offset) => {
1389
+ const shifted = removeOffset(time, offset);
1390
+ if (!Number.isInteger(shifted) || shifted < 0) throw new RangeError("TTML timestamps must be non-negative integer milliseconds after applying the offset");
1391
+ const seconds = Math.floor(shifted / 1e3);
1392
+ const minutes = Math.floor(seconds / 60);
1393
+ const hours = String(Math.floor(minutes / 60)).padStart(2, "0");
1394
+ const milliseconds = String(shifted % 1e3).padStart(3, "0");
1395
+ return `${hours}:${String(minutes % 60).padStart(2, "0")}:${String(seconds % 60).padStart(2, "0")}.${milliseconds}`;
1396
+ };
1397
+ /**
1398
+ * Writes known bounds without inventing an end.
1399
+ */
1400
+ var renderTimeAttributes = (time, offset) => {
1401
+ if (time === void 0) throw new Error("TTML sung lines, words, and ruby tokens require a time range");
1402
+ const attributes = [["begin", renderTime(time.start, offset)]];
1403
+ if (time.end !== void 0) attributes.push(["end", renderTime(time.end, offset)]);
1404
+ return attributes;
1405
+ };
1406
+ /**
1407
+ * Clears inherited language when the model leaves an item's language unspecified.
1408
+ */
1409
+ var renderLanguage = (language, inherited) => {
1410
+ if (language !== void 0) return [["xml:lang", language]];
1411
+ return inherited === void 0 ? [] : [["xml:lang", ""]];
1412
+ };
1413
+ var getLineLanguage = (line) => {
1414
+ return line.languages.length === 1 ? line.languages[0] : void 0;
1415
+ };
1416
+ /**
1417
+ * Writes separators as text and resolves inherited ruby timing through the model.
1418
+ */
1419
+ var renderWord = (word, context, language) => {
1420
+ if (word.type === WordType.SPACE) return createXmlText(word.text);
1421
+ const attributes = [...renderTimeAttributes(word.time, context.offset), ...renderLanguage(word.language, language)];
1422
+ const ruby = context.profile === "amll" ? word.annotation?.rubies[0] : void 0;
1423
+ if (ruby === void 0) return createXmlElement("span", attributes, [createXmlText(word.text)]);
1424
+ const tokens = ruby.tokens.map((token) => {
1425
+ return createXmlElement("span", [["tts:ruby", RUBY_TEXT], ...renderTimeAttributes(resolveAnnotationTokenTime(token, ruby, word), context.offset)], [createXmlText(token.text)]);
1426
+ });
1427
+ return createXmlElement("span", [["tts:ruby", RUBY_CONTAINER], ...attributes], [createXmlElement("span", [["tts:ruby", RUBY_BASE]], [createXmlText(word.text)]), createXmlElement("span", [
1428
+ ["tts:ruby", RUBY_TEXT_CONTAINER],
1429
+ ...renderTimeAttributes(resolveAnnotationItemTime(ruby, word), context.offset),
1430
+ ...renderLanguage(ruby.language, word.language)
1431
+ ], tokens)]);
1432
+ };
1433
+ /**
1434
+ * Retains the model's choice between whole text and timed words.
1435
+ */
1436
+ var renderContent = (line, context) => {
1437
+ const content = line.content;
1438
+ if (content === void 0) throw new Error("TTML sung lines require content");
1439
+ if (content.text !== void 0) return [createXmlText(content.text)];
1440
+ return content.words.map((word) => {
1441
+ return renderWord(word, context, getLineLanguage(line));
1442
+ });
1443
+ };
1444
+ /**
1445
+ * Keeps annotations outside the sung word stream.
1446
+ */
1447
+ var renderInlineAnnotations = (annotation, language) => {
1448
+ const nodes = [];
1449
+ for (const item of annotation?.translations ?? []) nodes.push(createXmlElement("span", [["ttm:role", ROLE_TRANSLATION], ...renderLanguage(item.language, language)], [createXmlText(item.text)]));
1450
+ for (const item of annotation?.romans ?? []) nodes.push(createXmlElement("span", [["ttm:role", ROLE_ROMAN], ...renderLanguage(item.language, language)], [createXmlText(item.text)]));
1451
+ return nodes;
1452
+ };
1453
+ /**
1454
+ * Uses the same allocated keys and agent ids as the document's declarations.
1455
+ */
1456
+ var renderLineAttributes = (line, context) => {
1457
+ const attributes = [
1458
+ ...renderTimeAttributes(line.time, context.offset),
1459
+ ["itunes:key", context.keys.get(line)],
1460
+ ["xml:space", "preserve"],
1461
+ ["xml:lang", getLineLanguage(line) ?? ""]
1462
+ ];
1463
+ const lead = line.agents[0];
1464
+ if (lead !== void 0) {
1465
+ const id = context.agents.get(lead);
1466
+ if (id === void 0) throw new Error("TTML performing agents must have a declaration");
1467
+ attributes.push(["ttm:agent", id]);
1468
+ }
1469
+ return attributes;
1470
+ };
1471
+ /**
1472
+ * An empty text node keeps the XML writer's indentation outside lyric content.
1473
+ */
1474
+ var createLyricElement = (name, attributes, children) => {
1475
+ return createXmlElement(name, attributes, [createXmlText(""), ...children]);
1476
+ };
1477
+ var renderBackground = (line, context) => {
1478
+ const children = renderContent(line, context);
1479
+ if (context.profile === "amll") children.push(...renderInlineAnnotations(line.annotation, getLineLanguage(line)));
1480
+ return createXmlElement("span", [["ttm:role", ROLE_BACKGROUND], ...renderLineAttributes(line, context)], children);
1481
+ };
1482
+ var renderLine = (line, context) => {
1483
+ const before = [];
1484
+ const after = [];
1485
+ for (const background of line.backgrounds) {
1486
+ const node = renderBackground(background, context);
1487
+ if (background.time !== void 0 && line.time !== void 0 && background.time.start < line.time.start) before.push(node);
1488
+ else after.push(node);
1489
+ }
1490
+ const children = [
1491
+ ...before,
1492
+ ...renderContent(line, context),
1493
+ ...after
1494
+ ];
1495
+ if (context.profile === "amll") children.push(...renderInlineAnnotations(line.annotation, getLineLanguage(line)));
1496
+ return createLyricElement("p", renderLineAttributes(line, context), children);
1497
+ };
1498
+ var renderAgents = (lyric, context) => {
1499
+ return lyric.agents.map((agent) => {
1500
+ const type = agent.type === AgentType.PERSON ? "person" : agent.type === AgentType.GROUP ? "group" : agent.raw ?? "other";
1501
+ const names = agent.names.map((name) => {
1502
+ return createXmlElement("ttm:name", [["type", "full"], ...renderLanguage(name.language)], [createXmlText(name.text)]);
1503
+ });
1504
+ return createXmlElement("ttm:agent", [["type", type], ["xml:id", context.agents.get(agent.id)]], names);
1505
+ });
1506
+ };
1507
+ var renderAmllMeta = (meta) => {
1508
+ const nodes = [];
1509
+ const groups = [
1510
+ ["musicName", meta.titles],
1511
+ ["artists", meta.artists],
1512
+ ["album", meta.albums],
1513
+ ["ttmlAuthorGithubLogin", meta.authors]
1514
+ ];
1515
+ for (const [key, items] of groups) for (const item of items) nodes.push(createXmlElement("amll:meta", [
1516
+ ["key", key],
1517
+ ["value", item.text],
1518
+ ...renderLanguage(item.language)
1519
+ ]));
1520
+ for (const value of meta.isrcs) nodes.push(createXmlElement("amll:meta", [["key", "isrc"], ["value", value]]));
1521
+ for (const item of meta.unknowns) nodes.push(createXmlElement("amll:meta", [["key", item.key], ["value", item.value]]));
1522
+ return nodes;
1523
+ };
1524
+ var renderSongwriters = (meta) => {
1525
+ const names = [];
1526
+ for (const credit of meta.credits) {
1527
+ if (credit.role !== CreditRole.LYRICIST) continue;
1528
+ for (const name of credit.names) names.push(createXmlElement("songwriter", renderLanguage(name.language), [createXmlText(name.text)]));
1529
+ }
1530
+ return names.length === 0 ? [] : [createXmlElement("songwriters", [], names)];
1531
+ };
1532
+ var renderAnnotationTracks = (tracks, kind, context) => {
1533
+ if (tracks.size === 0) return [];
1534
+ const translated = kind === "translations";
1535
+ const blocks = Array.from(tracks, ([language, track]) => {
1536
+ const attributes = translated ? [["type", "subtitle"]] : [];
1537
+ attributes.push(...renderLanguage(language));
1538
+ const texts = Array.from(track, ([line, children]) => {
1539
+ return createLyricElement("text", [["for", context.keys.get(line)], ["xml:space", "preserve"]], children);
1540
+ });
1541
+ return createXmlElement(translated ? "translation" : "transliteration", attributes, texts);
1542
+ });
1543
+ return [createXmlElement(translated ? "translations" : "transliterations", [], blocks)];
1544
+ };
1545
+ /**
1546
+ * A background-only annotation still references its parent paragraph, with a second reference naming the background.
1547
+ */
1548
+ var renderHeadAnnotations = (lines, context) => {
1549
+ const tracks = {
1550
+ translations: /* @__PURE__ */ new Map(),
1551
+ romans: /* @__PURE__ */ new Map()
1552
+ };
1553
+ const append = (annotation, line, background) => {
1554
+ if (annotation === void 0) return;
1555
+ for (const kind of ANNOTATION_KINDS) for (const item of annotation[kind]) {
1556
+ const language = item.language?.toLowerCase();
1557
+ let track = tracks[kind].get(language);
1558
+ if (track === void 0) {
1559
+ track = /* @__PURE__ */ new Map();
1560
+ tracks[kind].set(language, track);
1561
+ }
1562
+ let nodes = track.get(line);
1563
+ if (nodes === void 0) {
1564
+ nodes = [];
1565
+ track.set(line, nodes);
1566
+ }
1567
+ nodes.push(background === void 0 ? createXmlText(item.text) : createXmlElement("span", [["ttm:role", ROLE_BACKGROUND], ["for", context.keys.get(background)]], [createXmlText(item.text)]));
1568
+ }
1569
+ };
1570
+ for (const line of lines) {
1571
+ append(line.annotation, line);
1572
+ for (const background of line.backgrounds) append(background.annotation, line, background);
1573
+ }
1574
+ return ANNOTATION_KINDS.flatMap((kind) => {
1575
+ return renderAnnotationTracks(tracks[kind], kind, context);
1576
+ });
1577
+ };
1578
+ var renderMetadata = (lyric, lines, context, includeMeta) => {
1579
+ const metadata = renderAgents(lyric, context);
1580
+ const itunes = [];
1581
+ if (includeMeta && lyric.meta !== void 0) {
1582
+ if (context.profile === "amll") metadata.push(...renderAmllMeta(lyric.meta));
1583
+ itunes.push(...renderSongwriters(lyric.meta));
1584
+ }
1585
+ if (context.profile === "itunes") itunes.push(...renderHeadAnnotations(lines, context));
1586
+ if (itunes.length > 0) metadata.push(createXmlElement("iTunesMetadata", [["xmlns", NAMESPACE_ITUNES_METADATA]], itunes));
1587
+ return createXmlElement("metadata", renderLanguage(void 0, lyric.meta?.language), metadata);
1588
+ };
1589
+ var renderRootAttributes = (lyric, profile) => {
1590
+ const attributes = [
1591
+ ["xmlns", NAMESPACE_TTML],
1592
+ ["xmlns:ttm", NAMESPACE_TTML_METADATA],
1593
+ ["xmlns:tts", NAMESPACE_TTML_STYLING],
1594
+ ["xmlns:itunes", NAMESPACE_ITUNES]
1595
+ ];
1596
+ if (profile === "amll") attributes.push(["xmlns:amll", NAMESPACE_AMLL]);
1597
+ attributes.push(...renderLanguage(lyric.meta?.language), ["itunes:timing", lyric.timing === Timing.WORD ? TIMING_WORD : TIMING_LINE]);
1598
+ return attributes;
1599
+ };
1600
+ /**
1601
+ * Generates one TTML document, retaining the source profile unless explicitly overridden.
1602
+ *
1603
+ * Offsets are baked into timestamps; a negative result or a missing required time throws.
1604
+ * Profile mappings retain the lead agent and first AMLL ruby, omitting unsupported fields and instrumental nodes.
1605
+ */
1606
+ var generateTtml = (lyric, options = {}) => {
1607
+ if (lyric.timing !== Timing.LINE && lyric.timing !== Timing.WORD) throw new Error("TTML generation requires line or word timing");
1608
+ const resolved = resolveOptions(lyric, options);
1609
+ const lines = lyric.lines.filter((line) => {
1610
+ return line.type !== LineType.INSTRUMENTAL;
1611
+ });
1612
+ const context = makeContext(lyric, lines, resolved.profile);
1613
+ const metadata = renderMetadata(lyric, lines, context, resolved.includeMeta);
1614
+ const paragraphs = lines.map((line) => {
1615
+ return renderLine(line, context);
1616
+ });
1617
+ return serializeXml(createXmlElement("tt", renderRootAttributes(lyric, resolved.profile), [createXmlElement("head", [], [metadata]), createXmlElement("body", [], [createXmlElement("div", [], paragraphs)])]), { indent: resolved.indent });
1618
+ };
1619
+ //#endregion
1620
+ //#region src/formats/ttml/parser.ts
1621
+ var METADATA_NAMES = [
1622
+ "agent",
1623
+ "songwriter",
1624
+ "meta",
1625
+ "translation",
1626
+ "transliteration"
1627
+ ];
1628
+ var WORD_SEPARATOR_PATTERN = /\p{White_Space}/u;
1629
+ var readTtmlProfile = (source) => {
1630
+ return AMLL_HINT_PATTERN.test(source) ? "amll" : "itunes";
1631
+ };
1632
+ var readSpanTime = (element) => {
1162
1633
  const begin = getXmlAttribute(element, "begin");
1163
1634
  if (!begin) return;
1164
1635
  const start = parseTime(begin);
@@ -1169,120 +1640,345 @@ var readLineTime = (element) => {
1169
1640
  end: end ? parseTime(end) : void 0
1170
1641
  };
1171
1642
  };
1643
+ var readLineTime = (element) => {
1644
+ return readSpanTime(element);
1645
+ };
1646
+ var readAgentType = (raw) => {
1647
+ switch (raw) {
1648
+ case "person": return {
1649
+ type: AgentType.PERSON,
1650
+ raw: void 0
1651
+ };
1652
+ case "group": return {
1653
+ type: AgentType.GROUP,
1654
+ raw: void 0
1655
+ };
1656
+ default: return {
1657
+ type: AgentType.OTHER,
1658
+ raw: raw ?? "other"
1659
+ };
1660
+ }
1661
+ };
1662
+ var readDeclaredTiming = (root) => {
1663
+ switch (getXmlQualifiedAttribute(root, "itunes:timing")) {
1664
+ case TIMING_WORD: return Timing.WORD;
1665
+ case TIMING_LINE: return Timing.LINE;
1666
+ default: return;
1667
+ }
1668
+ };
1669
+ var readPreserveSpace = (element, inherited) => {
1670
+ switch (getXmlQualifiedAttribute(element, "xml:space")) {
1671
+ case "preserve": return true;
1672
+ case "default": return false;
1673
+ default: return inherited;
1674
+ }
1675
+ };
1676
+ var readLanguage = (element, inherited) => {
1677
+ const language = getXmlQualifiedAttribute(element, "xml:lang") ?? getXmlAttribute(element, "lang");
1678
+ return language === void 0 ? inherited : lowerTag(language);
1679
+ };
1680
+ var readText = (element, inherited) => {
1681
+ const text = getXmlTextContent(element);
1682
+ return readPreserveSpace(element, inherited) ? text : text.trim();
1683
+ };
1172
1684
  /**
1173
- * Turns a run of text into drafts, keeping the separators as space words.
1174
- *
1175
- * A span's own trailing space is part of the word it follows, so leading and trailing runs both survive.
1685
+ * Preserves the parser's whitespace segmentation when turning text into word drafts.
1176
1686
  */
1177
- var splitWords = (text) => {
1178
- const words = [];
1179
- for (const run of text.split(/(\s+)/)) if (run !== "") words.push({
1180
- text: run,
1181
- space: !run.trim(),
1687
+ var splitWords = (text, language) => {
1688
+ if (text === "") return [];
1689
+ if (!WORD_SEPARATOR_PATTERN.test(text)) return [{
1690
+ text,
1691
+ space: false,
1692
+ language,
1182
1693
  rubies: []
1183
- });
1694
+ }];
1695
+ const words = [];
1696
+ for (const run of text.split(/(\p{White_Space}+)/u)) if (run !== "") {
1697
+ const space = /^\p{White_Space}+$/u.test(run);
1698
+ words.push({
1699
+ text: run,
1700
+ space,
1701
+ language: space ? void 0 : language,
1702
+ rubies: []
1703
+ });
1704
+ }
1184
1705
  return words;
1185
1706
  };
1186
1707
  /**
1708
+ * Distinguishes indentation runs from separators using the parser's existing rule.
1709
+ */
1710
+ var isFormattingWhitespace = (text) => {
1711
+ return text.trim() === "" && text.includes("\n");
1712
+ };
1713
+ /**
1714
+ * Builds words from source drafts without deriving missing timestamps.
1715
+ */
1716
+ var buildWords = (draft) => {
1717
+ const words = [];
1718
+ for (const word of draft.words) {
1719
+ if (word.space) {
1720
+ words.push(makeWordSpace({ text: word.text }));
1721
+ continue;
1722
+ }
1723
+ const time = word.start === void 0 ? void 0 : makeTimeRange({
1724
+ start: word.start,
1725
+ end: word.end
1726
+ });
1727
+ if (word.rubies.length === 0) {
1728
+ words.push(makeWordNormal({
1729
+ text: word.text,
1730
+ time,
1731
+ language: word.language
1732
+ }));
1733
+ continue;
1734
+ }
1735
+ const rubies = word.rubies.map((ruby) => {
1736
+ return makeWordAnnotationRuby({
1737
+ time: ruby.start === void 0 ? void 0 : makeTimeRange({
1738
+ start: ruby.start,
1739
+ end: ruby.end
1740
+ }),
1741
+ language: ruby.language,
1742
+ tokens: ruby.tokens.map((token) => {
1743
+ return makeWordAnnotationToken({
1744
+ text: token.text,
1745
+ time: token.start === void 0 ? void 0 : makeTimeRange({
1746
+ start: token.start,
1747
+ end: token.end
1748
+ })
1749
+ });
1750
+ })
1751
+ });
1752
+ });
1753
+ words.push(makeWordNormal({
1754
+ text: word.text,
1755
+ time,
1756
+ language: word.language,
1757
+ annotation: makeWordAnnotation({ rubies })
1758
+ }));
1759
+ }
1760
+ return words;
1761
+ };
1762
+ /**
1763
+ * Builds only annotations explicitly present in the source drafts.
1764
+ */
1765
+ var buildLineAnnotation = (draft) => {
1766
+ if (draft.translations.length === 0 && draft.romans.length === 0) return;
1767
+ return makeLineAnnotation({
1768
+ translations: draft.translations.map((item) => {
1769
+ return makeLineAnnotationTranslation({
1770
+ text: item.text,
1771
+ language: item.language
1772
+ });
1773
+ }),
1774
+ romans: draft.romans.map((item) => {
1775
+ return makeLineAnnotationRoman({
1776
+ text: item.text,
1777
+ language: item.language
1778
+ });
1779
+ })
1780
+ });
1781
+ };
1782
+ /**
1783
+ * Separates head annotation text from its background references.
1784
+ */
1785
+ var splitBackgroundText = (element, preserveSpace, language) => {
1786
+ preserveSpace = readPreserveSpace(element, preserveSpace);
1787
+ language = readLanguage(element, language);
1788
+ let main = "";
1789
+ const backgrounds = [];
1790
+ for (const child of element.children) {
1791
+ if (child.nodeType === XmlNodeType.element && child.localName === "span" && getXmlAttribute(child, "role") === "x-bg") {
1792
+ backgrounds.push({
1793
+ text: readText(child, preserveSpace),
1794
+ key: getXmlAttribute(child, "for"),
1795
+ language: readLanguage(child, language)
1796
+ });
1797
+ continue;
1798
+ }
1799
+ main += getXmlTextContent(child);
1800
+ }
1801
+ return {
1802
+ main: preserveSpace ? main : main.trim(),
1803
+ backgrounds
1804
+ };
1805
+ };
1806
+ /**
1807
+ * Carries language and whitespace declarations through wrappers to the elements they govern.
1808
+ */
1809
+ var groupElements = (root, names, preserveSpace, stopAtMatch = false, language) => {
1810
+ const wanted = new Set(names);
1811
+ const result = /* @__PURE__ */ new Map();
1812
+ const stack = [{
1813
+ element: root,
1814
+ preserveSpace,
1815
+ language
1816
+ }];
1817
+ while (stack.length > 0) {
1818
+ const entry = stack.pop();
1819
+ if (entry === void 0) continue;
1820
+ const element = entry.element;
1821
+ const currentSpace = readPreserveSpace(element, entry.preserveSpace);
1822
+ const currentLanguage = readLanguage(element, entry.language);
1823
+ if (wanted.has(element.localName)) {
1824
+ const group = result.get(element.localName);
1825
+ const context = {
1826
+ element,
1827
+ preserveSpace: currentSpace,
1828
+ language: currentLanguage
1829
+ };
1830
+ if (group === void 0) result.set(element.localName, [context]);
1831
+ else group.push(context);
1832
+ if (stopAtMatch) continue;
1833
+ }
1834
+ const children = element.children;
1835
+ for (let index = children.length - 1; index >= 0; index -= 1) {
1836
+ const child = children[index];
1837
+ if (child && child.nodeType === XmlNodeType.element) stack.push({
1838
+ element: child,
1839
+ preserveSpace: currentSpace,
1840
+ language: currentLanguage
1841
+ });
1842
+ }
1843
+ }
1844
+ return result;
1845
+ };
1846
+ var findElements = (root, name, preserveSpace, stopAtMatch = false, language) => {
1847
+ return groupElements(root, [name], preserveSpace, stopAtMatch, language).get(name) ?? [];
1848
+ };
1849
+ /**
1187
1850
  * Reads an AMLL ruby container into one word plus its per-syllable annotation.
1188
1851
  */
1189
- var readRuby = (container) => {
1852
+ var readRuby = (container, preserveSpace, language) => {
1853
+ preserveSpace = readPreserveSpace(container, preserveSpace);
1854
+ language = readLanguage(container, language);
1190
1855
  let base = "";
1856
+ let baseLanguage = language;
1857
+ let rubyTime;
1858
+ let rubyLanguage = language;
1191
1859
  const tokens = [];
1192
1860
  for (const child of getXmlChildElements(container, "span")) {
1193
1861
  const kind = getXmlAttribute(child, "ruby");
1194
1862
  if (kind === "base") {
1195
- base = getXmlTextContent(child).trim();
1863
+ base = readText(child, preserveSpace);
1864
+ baseLanguage = readLanguage(child, language);
1196
1865
  continue;
1197
1866
  }
1198
1867
  if (kind !== "textContainer") continue;
1868
+ rubyTime ??= readSpanTime(child);
1869
+ rubyLanguage = readLanguage(child, language);
1870
+ const rubySpace = readPreserveSpace(child, preserveSpace);
1199
1871
  for (const text of getXmlChildElements(child, "span")) {
1200
1872
  if (getXmlAttribute(text, "ruby") !== "text") continue;
1201
1873
  const time = readSpanTime(text);
1202
- const content = getXmlTextContent(text).trim();
1203
- if (time === void 0 || !content) continue;
1874
+ const content = readText(text, rubySpace);
1875
+ if (!content) continue;
1204
1876
  tokens.push({
1205
- start: time.start,
1206
- end: time.end,
1877
+ start: time?.start,
1878
+ end: time?.end,
1207
1879
  text: content
1208
1880
  });
1209
1881
  }
1210
1882
  }
1211
- const first = tokens[0];
1212
- const last = tokens[tokens.length - 1];
1213
- if (!base || first === void 0 || last === void 0) return;
1883
+ if (!base) return;
1884
+ const first = tokens.find((token) => {
1885
+ return token.start !== void 0;
1886
+ });
1887
+ const last = tokens.findLast((token) => {
1888
+ return token.start !== void 0;
1889
+ });
1890
+ const time = readSpanTime(container) ?? rubyTime ?? (first === void 0 ? void 0 : {
1891
+ start: first.start,
1892
+ end: last?.end
1893
+ });
1214
1894
  return {
1215
1895
  text: base,
1216
1896
  space: false,
1217
- start: first.start,
1218
- end: last.end,
1219
- rubies: [{
1220
- start: first.start,
1221
- end: last.end,
1897
+ start: time?.start,
1898
+ end: time?.end,
1899
+ language: baseLanguage,
1900
+ rubies: tokens.length === 0 ? [] : [{
1901
+ start: rubyTime?.start,
1902
+ end: rubyTime?.end,
1903
+ language: rubyLanguage,
1222
1904
  tokens
1223
1905
  }]
1224
1906
  };
1225
1907
  };
1226
1908
  /**
1227
- * Reports whether a text run holds nothing but formatting whitespace, which only a pretty-printed source produces.
1228
- */
1229
- var isFormattingWhitespace = (text) => {
1230
- return text.trim() === "" && text.includes("\n");
1231
- };
1232
- /**
1233
1909
  * Collects the words of one span-bearing element.
1234
1910
  *
1235
- * Text between spans carries the separators, and a span without both ends contributes its text untimed.
1911
+ * Text between spans carries the separators, and a span without a readable begin contributes its text untimed.
1236
1912
  * A span carrying a role is annotation rather than a word, so it is skipped here.
1237
- * Indentation between tags is dropped, since the words themselves keep their own trailing spaces.
1913
+ * Whitespace inside a span remains a separator; indentation is dropped only without a preserve declaration.
1238
1914
  */
1239
- var readWords = (element) => {
1915
+ var readWords = (element, preserveSpace, language) => {
1916
+ preserveSpace = readPreserveSpace(element, preserveSpace);
1917
+ language = readLanguage(element, language);
1240
1918
  const words = [];
1241
1919
  let wordLevel = false;
1242
1920
  for (const child of element.children) {
1243
- if (child.nodeType === XmlNodeType.text) {
1244
- if (!isFormattingWhitespace(child.text)) words.push(...splitWords(child.text));
1921
+ if (child.nodeType !== XmlNodeType.element) {
1922
+ if (preserveSpace || !isFormattingWhitespace(child.text)) words.push(...splitWords(child.text, language));
1245
1923
  continue;
1246
1924
  }
1247
- if (child.nodeType !== XmlNodeType.element || child.localName !== "span") continue;
1925
+ if (child.localName !== "span") continue;
1248
1926
  if (getXmlAttribute(child, "role") !== void 0) continue;
1249
1927
  if (getXmlAttribute(child, "ruby") === "container") {
1250
- const ruby = readRuby(child);
1928
+ const ruby = readRuby(child, preserveSpace, language);
1251
1929
  if (ruby !== void 0) {
1252
- words.push(ruby);
1253
- wordLevel = true;
1930
+ for (const part of splitWords(ruby.text)) words.push(part.space ? part : {
1931
+ ...ruby,
1932
+ text: part.text
1933
+ });
1934
+ wordLevel ||= ruby.start !== void 0;
1254
1935
  }
1255
1936
  continue;
1256
1937
  }
1257
- const text = getXmlTextContent(child).trim();
1258
- if (!text) continue;
1938
+ const text = getXmlTextContent(child);
1939
+ const childSpace = readPreserveSpace(child, preserveSpace);
1940
+ if (!text || !childSpace && isFormattingWhitespace(text)) continue;
1259
1941
  const time = readSpanTime(child);
1260
- if (time === void 0) {
1261
- words.push({
1262
- text,
1263
- space: false,
1264
- rubies: []
1265
- });
1266
- continue;
1942
+ for (const part of splitWords(text, readLanguage(child, language))) {
1943
+ if (!part.space && time !== void 0) {
1944
+ part.start = time.start;
1945
+ part.end = time.end;
1946
+ wordLevel = true;
1947
+ }
1948
+ words.push(part);
1267
1949
  }
1268
- wordLevel = true;
1269
- words.push({
1270
- text,
1271
- space: false,
1272
- start: time.start,
1273
- end: time.end,
1274
- rubies: []
1275
- });
1276
1950
  }
1277
1951
  return {
1278
1952
  words,
1279
1953
  wordLevel
1280
1954
  };
1281
1955
  };
1956
+ var readInlineAnnotations = (element, preserveSpace, language) => {
1957
+ const translations = [];
1958
+ const romans = [];
1959
+ for (const span of getXmlChildElements(element, "span")) {
1960
+ const role = getXmlAttribute(span, "role");
1961
+ if (role !== "x-translation" && role !== "x-roman") continue;
1962
+ const text = readText(span, preserveSpace);
1963
+ if (!text) continue;
1964
+ const item = {
1965
+ language: readLanguage(span, language),
1966
+ text
1967
+ };
1968
+ if (role === "x-translation") translations.push(item);
1969
+ else romans.push(item);
1970
+ }
1971
+ return {
1972
+ translations,
1973
+ romans
1974
+ };
1975
+ };
1282
1976
  /**
1283
1977
  * Reads one paragraph, pulling its inline annotations and background lines out of the word stream.
1284
1978
  */
1285
- var readLine = (element) => {
1979
+ var readLine = (element, preserveSpace, language) => {
1980
+ preserveSpace = readPreserveSpace(element, preserveSpace);
1981
+ language = readLanguage(element, language);
1286
1982
  const time = readLineTime(element);
1287
1983
  if (time === void 0) return;
1288
1984
  const draft = {
@@ -1294,33 +1990,25 @@ var readLine = (element) => {
1294
1990
  translations: [],
1295
1991
  romans: [],
1296
1992
  backgrounds: [],
1297
- wordLevel: false
1993
+ wordLevel: false,
1994
+ preserveSpace,
1995
+ language
1298
1996
  };
1299
1997
  if (!hasXmlChildElement(element, "span")) {
1300
- const plain = getXmlTextContent(element).trim();
1998
+ const plain = readText(element, preserveSpace);
1301
1999
  if (!plain) return;
1302
- draft.words = splitWords(plain);
2000
+ draft.words = splitWords(plain, language);
1303
2001
  return draft;
1304
2002
  }
1305
- const read = readWords(element);
2003
+ const read = readWords(element, preserveSpace, language);
1306
2004
  draft.words = read.words;
1307
2005
  draft.wordLevel = read.wordLevel;
1308
- for (const span of getXmlChildElements(element, "span")) {
1309
- const role = getXmlAttribute(span, "role");
1310
- if (!role) continue;
1311
- if (role === "x-bg") {
1312
- const background = readBackground(span);
1313
- if (background) draft.backgrounds.push(background);
1314
- continue;
1315
- }
1316
- const text = getXmlTextContent(span).trim();
1317
- if (!text) continue;
1318
- const item = {
1319
- language: lowerTag(getXmlAttribute(span, "lang")),
1320
- text
1321
- };
1322
- if (role === "x-translation") draft.translations.push(item);
1323
- else if (role === "x-roman") draft.romans.push(item);
2006
+ const annotations = readInlineAnnotations(element, preserveSpace, language);
2007
+ draft.translations = annotations.translations;
2008
+ draft.romans = annotations.romans;
2009
+ for (const span of getXmlChildElements(element, "span")) if (getXmlAttribute(span, "role") === "x-bg") {
2010
+ const background = readBackground(span, preserveSpace, language);
2011
+ if (background) draft.backgrounds.push(background);
1324
2012
  }
1325
2013
  return draft;
1326
2014
  };
@@ -1329,8 +2017,10 @@ var readLine = (element) => {
1329
2017
  *
1330
2018
  * A background line states no time of its own in AMLL files, so its range comes from its words.
1331
2019
  */
1332
- var readBackground = (element) => {
1333
- const read = readWords(element);
2020
+ var readBackground = (element, preserveSpace, language) => {
2021
+ preserveSpace = readPreserveSpace(element, preserveSpace);
2022
+ language = readLanguage(element, language);
2023
+ const read = readWords(element, preserveSpace, language);
1334
2024
  if (read.words.length === 0) return;
1335
2025
  const begin = getXmlAttribute(element, "begin");
1336
2026
  const end = getXmlAttribute(element, "end");
@@ -1348,77 +2038,19 @@ var readBackground = (element) => {
1348
2038
  agentId: getXmlAttribute(element, "agent"),
1349
2039
  key: getXmlAttribute(element, "key"),
1350
2040
  words: read.words,
1351
- translations: [],
1352
- romans: [],
2041
+ ...readInlineAnnotations(element, preserveSpace, language),
1353
2042
  backgrounds: [],
1354
- wordLevel: read.wordLevel
2043
+ wordLevel: read.wordLevel,
2044
+ preserveSpace,
2045
+ language
1355
2046
  };
1356
2047
  };
1357
- /**
1358
- * Builds the model words of one draft line.
1359
- */
1360
- var buildWords = (draft) => {
1361
- const words = [];
1362
- for (const word of draft.words) {
1363
- if (word.space) {
1364
- words.push(makeWordSpace({ text: word.text }));
1365
- continue;
1366
- }
1367
- const time = word.start === void 0 ? void 0 : makeTimeRange({
1368
- start: word.start,
1369
- end: word.end
1370
- });
1371
- if (word.rubies.length === 0) {
1372
- words.push(makeWordNormal({
1373
- text: word.text,
1374
- time
1375
- }));
1376
- continue;
1377
- }
1378
- const rubies = word.rubies.map((ruby) => {
1379
- return makeWordAnnotationRuby({
1380
- time: makeTimeRange({
1381
- start: ruby.start,
1382
- end: ruby.end
1383
- }),
1384
- tokens: ruby.tokens.map((token) => {
1385
- return makeWordAnnotationToken({
1386
- text: token.text,
1387
- time: makeTimeRange({
1388
- start: token.start,
1389
- end: token.end
1390
- })
1391
- });
1392
- })
1393
- });
1394
- });
1395
- words.push(makeWordNormal({
1396
- text: word.text,
1397
- time,
1398
- annotation: makeWordAnnotation({ rubies })
1399
- }));
1400
- }
1401
- return words;
1402
- };
1403
- /**
1404
- * Builds a line's annotation container, which the model only keeps when it holds something.
1405
- */
1406
- var buildLineAnnotation = (draft) => {
1407
- if (draft.translations.length === 0 && draft.romans.length === 0) return;
1408
- return makeLineAnnotation({
1409
- translations: draft.translations.map((item) => {
1410
- return makeLineAnnotationTranslation({
1411
- text: item.text,
1412
- language: item.language
1413
- });
1414
- }),
1415
- romans: draft.romans.map((item) => {
1416
- return makeLineAnnotationRoman({
1417
- text: item.text,
1418
- language: item.language
1419
- });
1420
- })
1421
- });
2048
+ var buildContent = (draft) => {
2049
+ if (draft.wordLevel) return makeLineContentWords(buildWords(draft));
2050
+ const text = draft.words.map((word) => {
2051
+ return word.text;
2052
+ }).join("");
2053
+ return makeLineContentText(draft.preserveSpace ? text : text.trim());
1422
2054
  };
1423
2055
  /**
1424
2056
  * Builds one background line.
@@ -1431,7 +2063,8 @@ var buildBackground = (draft) => {
1431
2063
  end: draft.end
1432
2064
  }),
1433
2065
  agents: draft.agentId === void 0 ? [] : [draft.agentId],
1434
- content: makeLineContentWords(buildWords(draft)),
2066
+ languages: draft.language === void 0 ? [] : [draft.language],
2067
+ content: buildContent(draft),
1435
2068
  annotation: buildLineAnnotation(draft)
1436
2069
  });
1437
2070
  };
@@ -1439,9 +2072,6 @@ var buildBackground = (draft) => {
1439
2072
  * Builds one normal line, keeping plain text whole unless the source timed its words.
1440
2073
  */
1441
2074
  var buildLine = (draft) => {
1442
- const content = draft.wordLevel ? makeLineContentWords(buildWords(draft)) : makeLineContentText(draft.words.map((word) => {
1443
- return word.text;
1444
- }).join("").trim());
1445
2075
  return makeLineNormal({
1446
2076
  id: draft.key,
1447
2077
  time: makeTimeRange({
@@ -1449,54 +2079,34 @@ var buildLine = (draft) => {
1449
2079
  end: draft.end
1450
2080
  }),
1451
2081
  agents: draft.agentId === void 0 ? [] : [draft.agentId],
1452
- content,
2082
+ languages: draft.language === void 0 ? [] : [draft.language],
2083
+ content: buildContent(draft),
1453
2084
  annotation: buildLineAnnotation(draft),
1454
2085
  backgrounds: draft.backgrounds.map(buildBackground)
1455
2086
  });
1456
2087
  };
1457
2088
  /**
1458
- * Maps one iTunes agent type onto the model's.
1459
- *
1460
- * The model requires `raw` on `OTHER`, since it cannot tell whether the source stated that word or the reader gave up.
1461
- */
1462
- var readAgentType = (raw) => {
1463
- switch (raw) {
1464
- case "person": return {
1465
- type: AgentType.PERSON,
1466
- raw: void 0
1467
- };
1468
- case "group": return {
1469
- type: AgentType.GROUP,
1470
- raw: void 0
1471
- };
1472
- default: return {
1473
- type: AgentType.OTHER,
1474
- raw: raw ?? "other"
1475
- };
1476
- }
1477
- };
1478
- /**
1479
2089
  * Reads the head's agents, which lines reference by id.
1480
2090
  */
1481
- var readAgents = (metadata) => {
1482
- if (!metadata) return [];
2091
+ var readAgents = (elements) => {
1483
2092
  const agents = [];
1484
- for (const element of findXmlElements(metadata, "agent")) {
2093
+ for (const { element, preserveSpace, language } of elements) {
1485
2094
  const id = getXmlQualifiedAttribute(element, "xml:id") ?? getXmlAttribute(element, "id");
1486
2095
  if (!id) continue;
1487
2096
  const names = getXmlChildElements(element, "name").map((name) => {
1488
- return getXmlTextContent(name).trim();
2097
+ return makeText({
2098
+ text: readText(name, preserveSpace),
2099
+ language: readLanguage(name, language)
2100
+ });
1489
2101
  }).filter((name) => {
1490
- return name !== "";
2102
+ return name.text !== "";
1491
2103
  });
1492
2104
  const kind = readAgentType(getXmlAttribute(element, "type"));
1493
2105
  agents.push(makeAgent({
1494
2106
  id,
1495
2107
  type: kind.type,
1496
2108
  raw: kind.raw,
1497
- names: names.map((name) => {
1498
- return makeText({ text: name });
1499
- })
2109
+ names
1500
2110
  }));
1501
2111
  }
1502
2112
  return agents;
@@ -1506,43 +2116,56 @@ var readAgents = (metadata) => {
1506
2116
  *
1507
2117
  * AMLL wraps these in `iTunesMetadata`, so the whole head is searched rather than its direct children.
1508
2118
  */
1509
- var readCredits = (metadata, meta) => {
1510
- if (!metadata) return;
1511
- const names = findXmlElements(metadata, "songwriter").map((element) => {
1512
- return getXmlTextContent(element).trim();
2119
+ var readCredits = (elements, meta) => {
2120
+ const names = elements.map(({ element, preserveSpace, language }) => {
2121
+ return makeText({
2122
+ text: readText(element, preserveSpace),
2123
+ language
2124
+ });
1513
2125
  }).filter((name) => {
1514
- return name !== "";
2126
+ return name.text !== "";
1515
2127
  });
1516
2128
  if (names.length === 0) return;
1517
2129
  meta.credits.push(makeMetaCredit({
1518
2130
  role: CreditRole.LYRICIST,
1519
- names: names.map((name) => {
1520
- return makeText({ text: name });
1521
- })
2131
+ names
1522
2132
  }));
1523
2133
  };
1524
2134
  /**
1525
2135
  * Reads one `amll:meta` entry, keeping an unknown key as written so a round trip loses nothing.
1526
2136
  */
1527
- var applyAmllMeta = (meta, element) => {
2137
+ var applyAmllMeta = (meta, element, language) => {
1528
2138
  const key = getXmlAttribute(element, "key");
1529
2139
  const value = getXmlAttribute(element, "value")?.trim();
1530
2140
  if (!key || !value) return;
2141
+ language = readLanguage(element, language);
1531
2142
  switch (key) {
1532
2143
  case "musicName":
1533
- meta.titles.push(makeText({ text: value }));
2144
+ meta.titles.push(makeText({
2145
+ text: value,
2146
+ language
2147
+ }));
1534
2148
  return;
1535
2149
  case "artists":
1536
- meta.artists.push(makeText({ text: value }));
2150
+ meta.artists.push(makeText({
2151
+ text: value,
2152
+ language
2153
+ }));
1537
2154
  return;
1538
2155
  case "album":
1539
- meta.albums.push(makeText({ text: value }));
2156
+ meta.albums.push(makeText({
2157
+ text: value,
2158
+ language
2159
+ }));
1540
2160
  return;
1541
2161
  case "isrc":
1542
2162
  meta.isrcs.push(value);
1543
2163
  return;
1544
2164
  case "ttmlAuthorGithubLogin":
1545
- meta.authors.push(makeText({ text: value }));
2165
+ meta.authors.push(makeText({
2166
+ text: value,
2167
+ language
2168
+ }));
1546
2169
  return;
1547
2170
  default: meta.unknowns.push(makeUnknownEntry({
1548
2171
  key,
@@ -1551,52 +2174,33 @@ var applyAmllMeta = (meta, element) => {
1551
2174
  }
1552
2175
  };
1553
2176
  /**
1554
- * Splits one annotation text into its main part and any background pieces it embeds.
1555
- */
1556
- var splitBackgroundText = (element) => {
1557
- let main = "";
1558
- const backgrounds = [];
1559
- for (const child of element.children) {
1560
- if (child.nodeType === XmlNodeType.element && child.localName === "span" && getXmlAttribute(child, "role") === "x-bg") {
1561
- backgrounds.push({
1562
- text: getXmlTextContent(child).trim(),
1563
- key: getXmlAttribute(child, "for")
1564
- });
1565
- continue;
1566
- }
1567
- main += getXmlTextContent(child);
1568
- }
1569
- return {
1570
- main: main.trim(),
1571
- backgrounds
1572
- };
1573
- };
1574
- /**
1575
2177
  * Attaches head translations and transliterations to the lines they name by `itunes:key`.
1576
2178
  *
1577
2179
  * A replacement translation rewrites the line's text, which is how a source swaps one script for another.
1578
2180
  */
1579
2181
  var attachHeadAnnotations = (metadata, lineMap, backgroundMap) => {
1580
- if (!metadata || lineMap.size === 0) return;
2182
+ if (lineMap.size === 0) return;
1581
2183
  const attach = (blocks, kind) => {
1582
- for (const block of blocks) {
1583
- const language = lowerTag(getXmlAttribute(block, "lang"));
2184
+ for (const { element: block, preserveSpace, language } of blocks) {
1584
2185
  const type = getXmlQualifiedAttribute(block, "type") ?? getXmlAttribute(block, "type");
1585
2186
  for (const text of getXmlChildElements(block, "text")) {
1586
2187
  const key = getXmlAttribute(text, "for");
1587
2188
  const line = key ? lineMap.get(key) : void 0;
1588
2189
  if (!line) continue;
1589
- const { main, backgrounds } = splitBackgroundText(text);
2190
+ const textLanguage = readLanguage(text, language);
2191
+ const { main, backgrounds } = splitBackgroundText(text, preserveSpace, textLanguage);
1590
2192
  if (kind === "translation" && type === "replacement") {
1591
2193
  if (main) {
1592
- line.words = splitWords(main);
2194
+ line.words = splitWords(main, textLanguage);
1593
2195
  line.wordLevel = false;
2196
+ line.preserveSpace = readPreserveSpace(text, preserveSpace);
2197
+ line.language = textLanguage;
1594
2198
  }
1595
2199
  continue;
1596
2200
  }
1597
2201
  if (main) {
1598
2202
  const item = {
1599
- language,
2203
+ language: textLanguage,
1600
2204
  text: main
1601
2205
  };
1602
2206
  if (kind === "translation") line.translations.push(item);
@@ -1607,7 +2211,7 @@ var attachHeadAnnotations = (metadata, lineMap, backgroundMap) => {
1607
2211
  const target = background.key ? backgroundMap.get(background.key) : void 0;
1608
2212
  if (!target) continue;
1609
2213
  const item = {
1610
- language,
2214
+ language: background.language,
1611
2215
  text: background.text
1612
2216
  };
1613
2217
  if (kind === "translation") target.translations.push(item);
@@ -1616,36 +2220,8 @@ var attachHeadAnnotations = (metadata, lineMap, backgroundMap) => {
1616
2220
  }
1617
2221
  }
1618
2222
  };
1619
- attach(findXmlElements(metadata, "translation"), "translation");
1620
- attach(findXmlElements(metadata, "transliteration"), "roman");
1621
- };
1622
- /**
1623
- * Returns the paragraphs of a body, including any nested inside a div.
1624
- */
1625
- var findParagraphs = (body) => {
1626
- const result = [];
1627
- const stack = [body];
1628
- while (stack.length > 0) {
1629
- const element = stack.pop();
1630
- if (!element) continue;
1631
- if (element.localName === "p") {
1632
- result.push(element);
1633
- continue;
1634
- }
1635
- const children = element.children;
1636
- for (let index = children.length - 1; index >= 0; index -= 1) {
1637
- const child = children[index];
1638
- if (child && child.nodeType === XmlNodeType.element) stack.push(child);
1639
- }
1640
- }
1641
- return result;
1642
- };
1643
- var readDeclaredTiming = (root) => {
1644
- switch (getXmlQualifiedAttribute(root, "itunes:timing")) {
1645
- case TIMING_WORD: return Timing.WORD;
1646
- case TIMING_LINE: return Timing.LINE;
1647
- default: return;
1648
- }
2223
+ attach(metadata.get("translation") ?? [], "translation");
2224
+ attach(metadata.get("transliteration") ?? [], "roman");
1649
2225
  };
1650
2226
  /**
1651
2227
  * Reads a TTML document into the model.
@@ -1660,8 +2236,8 @@ var parseTtml = (input, options = {}) => {
1660
2236
  status: LyricStatus.INVALID,
1661
2237
  meta: makeMeta()
1662
2238
  }));
1663
- const extra = { profile: readTtmlProfile(content.original) };
1664
- const document = parseXml(content.original);
2239
+ const extra = { profile: readTtmlProfile(content) };
2240
+ const document = parseXml(content);
1665
2241
  const root = document ? findXmlElement(document, "tt") : void 0;
1666
2242
  if (!root) return finalizeLyric(makeLyric({
1667
2243
  format: FORMAT,
@@ -1669,24 +2245,25 @@ var parseTtml = (input, options = {}) => {
1669
2245
  meta: makeMeta(),
1670
2246
  extra
1671
2247
  }));
1672
- const body = findXmlElement(root, "body");
1673
- const metadata = findXmlElement(root, "metadata");
2248
+ const language = readLanguage(root, lowerTag(options.language));
2249
+ const body = findElements(root, "body", false, true, language)[0];
2250
+ const metadata = findElements(root, "metadata", false, true, language)[0];
2251
+ const metadataElements = metadata === void 0 ? /* @__PURE__ */ new Map() : groupElements(metadata.element, METADATA_NAMES, metadata.preserveSpace, false, metadata.language);
1674
2252
  const meta = makeMeta();
1675
- const language = lowerTag(getXmlQualifiedAttribute(root, "xml:lang")) ?? lowerTag(options.language);
1676
2253
  if (language !== void 0) meta.language = language;
1677
- readCredits(metadata, meta);
1678
- for (const element of metadata ? findXmlElements(metadata, "meta") : []) applyAmllMeta(meta, element);
2254
+ readCredits(metadataElements.get("songwriter") ?? [], meta);
2255
+ for (const { element, language: itemLanguage } of metadataElements.get("meta") ?? []) applyAmllMeta(meta, element, itemLanguage);
1679
2256
  const drafts = [];
1680
2257
  const lineMap = /* @__PURE__ */ new Map();
1681
2258
  const backgroundMap = /* @__PURE__ */ new Map();
1682
- for (const element of body ? findParagraphs(body) : []) {
1683
- const draft = readLine(element);
2259
+ for (const { element, preserveSpace, language: lineLanguage } of body ? findElements(body.element, "p", body.preserveSpace, true, body.language) : []) {
2260
+ const draft = readLine(element, preserveSpace, lineLanguage);
1684
2261
  if (!draft) continue;
1685
2262
  drafts.push(draft);
1686
2263
  if (draft.key) lineMap.set(draft.key, draft);
1687
2264
  for (const background of draft.backgrounds) if (background.key) backgroundMap.set(background.key, background);
1688
2265
  }
1689
- attachHeadAnnotations(metadata, lineMap, backgroundMap);
2266
+ attachHeadAnnotations(metadataElements, lineMap, backgroundMap);
1690
2267
  if (drafts.length === 0) return finalizeLyric(makeLyric({
1691
2268
  format: FORMAT,
1692
2269
  status: LyricStatus.INVALID,
@@ -1695,7 +2272,9 @@ var parseTtml = (input, options = {}) => {
1695
2272
  }));
1696
2273
  const lines = drafts.map(buildLine);
1697
2274
  const wordLevel = drafts.some((draft) => {
1698
- return draft.wordLevel;
2275
+ return draft.wordLevel || draft.backgrounds.some((background) => {
2276
+ return background.wordLevel;
2277
+ });
1699
2278
  });
1700
2279
  return finalizeLyric(makeLyric({
1701
2280
  format: FORMAT,
@@ -1703,7 +2282,7 @@ var parseTtml = (input, options = {}) => {
1703
2282
  timing: readDeclaredTiming(root) ?? (wordLevel ? Timing.WORD : Timing.LINE),
1704
2283
  meta,
1705
2284
  extra,
1706
- agents: readAgents(metadata),
2285
+ agents: readAgents(metadataElements.get("agent") ?? []),
1707
2286
  languages: deriveLanguageUsages(lines),
1708
2287
  lines
1709
2288
  }));
@@ -1731,18 +2310,21 @@ var parse = (input, options = {}) => {
1731
2310
  case "ttml": return parseTtml(input, options);
1732
2311
  }
1733
2312
  };
2313
+ var GENERATORS = {
2314
+ lrc: generateLrc,
2315
+ ttml: generateTtml
2316
+ };
1734
2317
  /**
1735
2318
  * Generates text in the requested format, which need not match `lyric.format`.
1736
2319
  *
1737
- * A format without a writer has no branch here, so adding one to `LyricFormat` makes this fail to compile.
2320
+ * The writer map pairs each format's options with its result and requires a writer for every supported format.
2321
+ * An unknown target returns `null`.
1738
2322
  */
1739
2323
  var generate = (lyric, target, options = {}) => {
1740
- switch (target) {
1741
- case "lrc": return generateLrc(lyric, options);
1742
- case "ttml": throw new Error("ttml generation is not implemented yet");
1743
- }
2324
+ if (!Object.hasOwn(GENERATORS, target)) return null;
2325
+ return GENERATORS[target](lyric, options);
1744
2326
  };
1745
2327
  //#endregion
1746
- export { AMLL_HINT_PATTERN, FORMAT, ITUNES_HINT_PATTERN, detectFormat, detectLrc, detectTtml, generate, generateLrc, parse, parseLrc, parseTtml, readTtmlContent, readTtmlProfile };
2328
+ export { detectFormat, detectLrc, detectTtml, generate, generateLrc, generateTtml, parse, parseLrc, parseTtml };
1747
2329
 
1748
2330
  //# sourceMappingURL=formats.js.map