@gondola-lyrics/kit 0.2.0 → 0.4.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/README.md +4 -3
- package/dist/formats.d.ts +41 -15
- package/dist/formats.js +925 -337
- package/dist/formats.js.map +1 -1
- package/dist/generator.d.ts +42 -2
- package/dist/generator.js +1 -1
- package/dist/index.d.ts +208 -7
- package/dist/index.js +3 -1
- package/dist/parser.d.ts +169 -6
- package/dist/parser.js +3 -1
- package/dist/transforms.d.ts +170 -1
- package/dist/transforms.js +1048 -0
- package/dist/transforms.js.map +1 -0
- package/package.json +1 -1
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, makeLineInstrumental, 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 = "&";
|
|
697
|
+
else if (code === LESS_THAN) escaped = "<";
|
|
698
|
+
else if (code === GREATER_THAN) escaped = ">";
|
|
699
|
+
else if (code === CARRIAGE_RETURN) escaped = " ";
|
|
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 = "&";
|
|
730
|
+
else if (code === LESS_THAN) escaped = "<";
|
|
731
|
+
else if (code === GREATER_THAN) escaped = ">";
|
|
732
|
+
else if (code === DOUBLE_QUOTE) escaped = """;
|
|
733
|
+
else if (code === TAB) escaped = "	";
|
|
734
|
+
else if (code === LINE_FEED) escaped = " ";
|
|
735
|
+
else if (code === CARRIAGE_RETURN) escaped = " ";
|
|
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) => {
|
|
@@ -1019,14 +1184,20 @@ var readWords$1 = (text, lineStart) => {
|
|
|
1019
1184
|
/**
|
|
1020
1185
|
* A line ends at the next line's start; without one, it ends at its final word's end.
|
|
1021
1186
|
* An open word is closed by the next timed word, or by the line end, so every sung word stays inside the line.
|
|
1187
|
+
* Empty timed rows represent instrumental time rather than sung content.
|
|
1022
1188
|
*/
|
|
1023
1189
|
var readLine$1 = (text, start, nextStart) => {
|
|
1190
|
+
const trimmed = text.trim();
|
|
1191
|
+
let end = nextStart !== void 0 && nextStart >= start ? nextStart : void 0;
|
|
1192
|
+
if (trimmed === "") return makeLineInstrumental({ time: makeTimeRange({
|
|
1193
|
+
start,
|
|
1194
|
+
end
|
|
1195
|
+
}) });
|
|
1024
1196
|
const wordTimed = isWordTimed(text);
|
|
1025
1197
|
const { drafts, closeSegment } = wordTimed ? readWords$1(text, start) : {
|
|
1026
1198
|
drafts: [],
|
|
1027
1199
|
closeSegment: () => {}
|
|
1028
1200
|
};
|
|
1029
|
-
let end = nextStart !== void 0 && nextStart >= start ? nextStart : void 0;
|
|
1030
1201
|
if (end === void 0) end = [...drafts].reverse().find((draft) => !draft.space && draft.start !== void 0)?.end;
|
|
1031
1202
|
closeSegment(end);
|
|
1032
1203
|
const words = drafts.map((draft) => {
|
|
@@ -1044,7 +1215,7 @@ var readLine$1 = (text, start, nextStart) => {
|
|
|
1044
1215
|
start,
|
|
1045
1216
|
end
|
|
1046
1217
|
});
|
|
1047
|
-
const content = wordTimed ? makeLineContentWords(words) : makeLineContentText(
|
|
1218
|
+
const content = wordTimed ? makeLineContentWords(words) : makeLineContentText(trimmed);
|
|
1048
1219
|
return makeLineNormal({
|
|
1049
1220
|
time,
|
|
1050
1221
|
content
|
|
@@ -1052,7 +1223,7 @@ var readLine$1 = (text, start, nextStart) => {
|
|
|
1052
1223
|
};
|
|
1053
1224
|
/**
|
|
1054
1225
|
* Builds one line per timed row, so an annotation track reuses the original text's reading unchanged.
|
|
1055
|
-
*
|
|
1226
|
+
* Empty timed rows become instrumental lines bounded by the next timestamp when one is usable.
|
|
1056
1227
|
*/
|
|
1057
1228
|
var readLines = (timed) => {
|
|
1058
1229
|
const lines = [];
|
|
@@ -1082,7 +1253,7 @@ var parseLrcText = (text) => {
|
|
|
1082
1253
|
});
|
|
1083
1254
|
};
|
|
1084
1255
|
/**
|
|
1085
|
-
*
|
|
1256
|
+
* Empty timed rows become instrumental lines; source order and missing word times remain for model validation.
|
|
1086
1257
|
*
|
|
1087
1258
|
* Annotation tracks are read as lines too and attached by timestamp, and rows that match no line are dropped.
|
|
1088
1259
|
*
|
|
@@ -1109,15 +1280,12 @@ var parseLrc = (input, options = {}) => {
|
|
|
1109
1280
|
var FORMAT = "ttml";
|
|
1110
1281
|
var AMLL_HINT_PATTERN = /xmlns:amll|amll:meta/iu;
|
|
1111
1282
|
var ITUNES_HINT_PATTERN = /xmlns:itunes|iTunesMetadata|itunes:timing=/iu;
|
|
1283
|
+
/**
|
|
1284
|
+
* Accepts the text input shared by TTML detection and parsing.
|
|
1285
|
+
*/
|
|
1112
1286
|
var readTtmlContent = (input) => {
|
|
1113
|
-
if (typeof input
|
|
1114
|
-
|
|
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";
|
|
1287
|
+
if (typeof input !== "string") return null;
|
|
1288
|
+
return input;
|
|
1121
1289
|
};
|
|
1122
1290
|
//#endregion
|
|
1123
1291
|
//#region src/formats/ttml/detect.ts
|
|
@@ -1130,35 +1298,344 @@ var readTtmlProfile = (source) => {
|
|
|
1130
1298
|
var detectTtml = (input) => {
|
|
1131
1299
|
const content = readTtmlContent(input);
|
|
1132
1300
|
if (!content) return false;
|
|
1133
|
-
if (AMLL_HINT_PATTERN.test(content
|
|
1134
|
-
const root = parseXml(content
|
|
1301
|
+
if (AMLL_HINT_PATTERN.test(content) || ITUNES_HINT_PATTERN.test(content)) return true;
|
|
1302
|
+
const root = parseXml(content);
|
|
1135
1303
|
if (!root) return false;
|
|
1136
1304
|
for (const child of root.children) if (child.nodeType === XmlNodeType.element && child.localName === "tt") return true;
|
|
1137
1305
|
return false;
|
|
1138
1306
|
};
|
|
1307
|
+
//#endregion
|
|
1308
|
+
//#region src/formats/ttml/utils.ts
|
|
1309
|
+
var ROLE_BACKGROUND = "x-bg";
|
|
1310
|
+
var ROLE_TRANSLATION = "x-translation";
|
|
1311
|
+
var ROLE_ROMAN = "x-roman";
|
|
1312
|
+
var RUBY_CONTAINER = "container";
|
|
1313
|
+
var RUBY_BASE = "base";
|
|
1314
|
+
var RUBY_TEXT_CONTAINER = "textContainer";
|
|
1315
|
+
var RUBY_TEXT = "text";
|
|
1139
1316
|
var TIMING_WORD = "Word";
|
|
1140
1317
|
var TIMING_LINE = "Line";
|
|
1141
1318
|
//#endregion
|
|
1142
|
-
//#region src/formats/ttml/
|
|
1319
|
+
//#region src/formats/ttml/generator.ts
|
|
1320
|
+
var NAMESPACE_TTML = "http://www.w3.org/ns/ttml";
|
|
1321
|
+
var NAMESPACE_TTML_METADATA = "http://www.w3.org/ns/ttml#metadata";
|
|
1322
|
+
var NAMESPACE_TTML_STYLING = "http://www.w3.org/ns/ttml#styling";
|
|
1323
|
+
var NAMESPACE_ITUNES = "http://itunes.apple.com/lyric-ttml-extensions";
|
|
1324
|
+
var NAMESPACE_ITUNES_METADATA = "http://music.apple.com/lyric-ttml-internal";
|
|
1325
|
+
var NAMESPACE_AMLL = "http://www.example.com/ns/amll";
|
|
1326
|
+
var ANNOTATION_KINDS = ["translations", "romans"];
|
|
1327
|
+
var XML_ID_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
|
|
1328
|
+
var resolveOptions = (lyric, options) => {
|
|
1329
|
+
const profile = options.profile ?? (lyric.format === "ttml" && lyric.extra.profile === "itunes" ? "itunes" : "amll");
|
|
1330
|
+
if (profile !== "amll" && profile !== "itunes") throw new Error("Unsupported TTML profile");
|
|
1331
|
+
return {
|
|
1332
|
+
profile,
|
|
1333
|
+
includeMeta: options.includeMeta ?? true,
|
|
1334
|
+
indent: options.indent ?? false
|
|
1335
|
+
};
|
|
1336
|
+
};
|
|
1143
1337
|
/**
|
|
1144
|
-
*
|
|
1338
|
+
* Reserves source ids before allocating keys, so unnamed entries cannot take later source ids.
|
|
1145
1339
|
*/
|
|
1146
|
-
var
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
const
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1340
|
+
var makeIdAllocator = (ids) => {
|
|
1341
|
+
const reserved = new Set(ids.filter((id) => {
|
|
1342
|
+
return XML_ID_PATTERN.test(id);
|
|
1343
|
+
}));
|
|
1344
|
+
const used = /* @__PURE__ */ new Set();
|
|
1345
|
+
const counters = /* @__PURE__ */ new Map();
|
|
1346
|
+
return (preferred, prefix) => {
|
|
1347
|
+
if (XML_ID_PATTERN.test(preferred) && !used.has(preferred)) {
|
|
1348
|
+
used.add(preferred);
|
|
1349
|
+
return preferred;
|
|
1350
|
+
}
|
|
1351
|
+
let count = counters.get(prefix) ?? 0;
|
|
1352
|
+
let id;
|
|
1353
|
+
do {
|
|
1354
|
+
count += 1;
|
|
1355
|
+
id = `${prefix}${count}`;
|
|
1356
|
+
} while (reserved.has(id) || used.has(id));
|
|
1357
|
+
counters.set(prefix, count);
|
|
1358
|
+
used.add(id);
|
|
1359
|
+
return id;
|
|
1360
|
+
};
|
|
1361
|
+
};
|
|
1362
|
+
/**
|
|
1363
|
+
* Shares one mapping between declarations, body lines, and annotation references.
|
|
1364
|
+
*/
|
|
1365
|
+
var makeContext = (lyric, lines, profile) => {
|
|
1366
|
+
const ids = lyric.agents.map((agent) => {
|
|
1367
|
+
return agent.id;
|
|
1368
|
+
});
|
|
1369
|
+
for (const line of lines) {
|
|
1370
|
+
ids.push(line.id);
|
|
1371
|
+
for (const background of line.backgrounds) ids.push(background.id);
|
|
1372
|
+
}
|
|
1373
|
+
const allocate = makeIdAllocator(ids);
|
|
1374
|
+
const agents = /* @__PURE__ */ new Map();
|
|
1375
|
+
const keys = /* @__PURE__ */ new Map();
|
|
1376
|
+
for (const agent of lyric.agents) {
|
|
1377
|
+
if (agents.has(agent.id)) throw new Error("TTML agent declarations require unique ids");
|
|
1378
|
+
agents.set(agent.id, allocate(agent.id, "v"));
|
|
1379
|
+
}
|
|
1380
|
+
for (const line of lines) {
|
|
1381
|
+
keys.set(line, allocate(line.id, "L"));
|
|
1382
|
+
for (const background of line.backgrounds) keys.set(background, allocate(background.id, "B"));
|
|
1383
|
+
}
|
|
1153
1384
|
return {
|
|
1154
|
-
|
|
1155
|
-
|
|
1385
|
+
profile,
|
|
1386
|
+
offset: lyric.meta?.offset ?? 0,
|
|
1387
|
+
keys,
|
|
1388
|
+
agents
|
|
1156
1389
|
};
|
|
1157
1390
|
};
|
|
1158
1391
|
/**
|
|
1159
|
-
*
|
|
1392
|
+
* Bakes the playback offset into TTML timestamps without clamping negative results.
|
|
1160
1393
|
*/
|
|
1161
|
-
var
|
|
1394
|
+
var renderTime = (time, offset) => {
|
|
1395
|
+
const shifted = removeOffset(time, offset);
|
|
1396
|
+
if (!Number.isInteger(shifted) || shifted < 0) throw new RangeError("TTML timestamps must be non-negative integer milliseconds after applying the offset");
|
|
1397
|
+
const seconds = Math.floor(shifted / 1e3);
|
|
1398
|
+
const minutes = Math.floor(seconds / 60);
|
|
1399
|
+
const hours = String(Math.floor(minutes / 60)).padStart(2, "0");
|
|
1400
|
+
const milliseconds = String(shifted % 1e3).padStart(3, "0");
|
|
1401
|
+
return `${hours}:${String(minutes % 60).padStart(2, "0")}:${String(seconds % 60).padStart(2, "0")}.${milliseconds}`;
|
|
1402
|
+
};
|
|
1403
|
+
/**
|
|
1404
|
+
* Writes known bounds without inventing an end.
|
|
1405
|
+
*/
|
|
1406
|
+
var renderTimeAttributes = (time, offset) => {
|
|
1407
|
+
if (time === void 0) throw new Error("TTML sung lines, words, and ruby tokens require a time range");
|
|
1408
|
+
const attributes = [["begin", renderTime(time.start, offset)]];
|
|
1409
|
+
if (time.end !== void 0) attributes.push(["end", renderTime(time.end, offset)]);
|
|
1410
|
+
return attributes;
|
|
1411
|
+
};
|
|
1412
|
+
/**
|
|
1413
|
+
* Clears inherited language when the model leaves an item's language unspecified.
|
|
1414
|
+
*/
|
|
1415
|
+
var renderLanguage = (language, inherited) => {
|
|
1416
|
+
if (language !== void 0) return [["xml:lang", language]];
|
|
1417
|
+
return inherited === void 0 ? [] : [["xml:lang", ""]];
|
|
1418
|
+
};
|
|
1419
|
+
var getLineLanguage = (line) => {
|
|
1420
|
+
return line.languages.length === 1 ? line.languages[0] : void 0;
|
|
1421
|
+
};
|
|
1422
|
+
/**
|
|
1423
|
+
* Writes separators as text and resolves inherited ruby timing through the model.
|
|
1424
|
+
*/
|
|
1425
|
+
var renderWord = (word, context, language) => {
|
|
1426
|
+
if (word.type === WordType.SPACE) return createXmlText(word.text);
|
|
1427
|
+
const attributes = [...renderTimeAttributes(word.time, context.offset), ...renderLanguage(word.language, language)];
|
|
1428
|
+
const ruby = context.profile === "amll" ? word.annotation?.rubies[0] : void 0;
|
|
1429
|
+
if (ruby === void 0) return createXmlElement("span", attributes, [createXmlText(word.text)]);
|
|
1430
|
+
const tokens = ruby.tokens.map((token) => {
|
|
1431
|
+
return createXmlElement("span", [["tts:ruby", RUBY_TEXT], ...renderTimeAttributes(resolveAnnotationTokenTime(token, ruby, word), context.offset)], [createXmlText(token.text)]);
|
|
1432
|
+
});
|
|
1433
|
+
return createXmlElement("span", [["tts:ruby", RUBY_CONTAINER], ...attributes], [createXmlElement("span", [["tts:ruby", RUBY_BASE]], [createXmlText(word.text)]), createXmlElement("span", [
|
|
1434
|
+
["tts:ruby", RUBY_TEXT_CONTAINER],
|
|
1435
|
+
...renderTimeAttributes(resolveAnnotationItemTime(ruby, word), context.offset),
|
|
1436
|
+
...renderLanguage(ruby.language, word.language)
|
|
1437
|
+
], tokens)]);
|
|
1438
|
+
};
|
|
1439
|
+
/**
|
|
1440
|
+
* Retains the model's choice between whole text and timed words.
|
|
1441
|
+
*/
|
|
1442
|
+
var renderContent = (line, context) => {
|
|
1443
|
+
const content = line.content;
|
|
1444
|
+
if (content === void 0) throw new Error("TTML sung lines require content");
|
|
1445
|
+
if (content.text !== void 0) return [createXmlText(content.text)];
|
|
1446
|
+
return content.words.map((word) => {
|
|
1447
|
+
return renderWord(word, context, getLineLanguage(line));
|
|
1448
|
+
});
|
|
1449
|
+
};
|
|
1450
|
+
/**
|
|
1451
|
+
* Keeps annotations outside the sung word stream.
|
|
1452
|
+
*/
|
|
1453
|
+
var renderInlineAnnotations = (annotation, language) => {
|
|
1454
|
+
const nodes = [];
|
|
1455
|
+
for (const item of annotation?.translations ?? []) nodes.push(createXmlElement("span", [["ttm:role", ROLE_TRANSLATION], ...renderLanguage(item.language, language)], [createXmlText(item.text)]));
|
|
1456
|
+
for (const item of annotation?.romans ?? []) nodes.push(createXmlElement("span", [["ttm:role", ROLE_ROMAN], ...renderLanguage(item.language, language)], [createXmlText(item.text)]));
|
|
1457
|
+
return nodes;
|
|
1458
|
+
};
|
|
1459
|
+
/**
|
|
1460
|
+
* Uses the same allocated keys and agent ids as the document's declarations.
|
|
1461
|
+
*/
|
|
1462
|
+
var renderLineAttributes = (line, context) => {
|
|
1463
|
+
const attributes = [
|
|
1464
|
+
...renderTimeAttributes(line.time, context.offset),
|
|
1465
|
+
["itunes:key", context.keys.get(line)],
|
|
1466
|
+
["xml:space", "preserve"],
|
|
1467
|
+
["xml:lang", getLineLanguage(line) ?? ""]
|
|
1468
|
+
];
|
|
1469
|
+
const lead = line.agents[0];
|
|
1470
|
+
if (lead !== void 0) {
|
|
1471
|
+
const id = context.agents.get(lead);
|
|
1472
|
+
if (id === void 0) throw new Error("TTML performing agents must have a declaration");
|
|
1473
|
+
attributes.push(["ttm:agent", id]);
|
|
1474
|
+
}
|
|
1475
|
+
return attributes;
|
|
1476
|
+
};
|
|
1477
|
+
/**
|
|
1478
|
+
* An empty text node keeps the XML writer's indentation outside lyric content.
|
|
1479
|
+
*/
|
|
1480
|
+
var createLyricElement = (name, attributes, children) => {
|
|
1481
|
+
return createXmlElement(name, attributes, [createXmlText(""), ...children]);
|
|
1482
|
+
};
|
|
1483
|
+
var renderBackground = (line, context) => {
|
|
1484
|
+
const children = renderContent(line, context);
|
|
1485
|
+
if (context.profile === "amll") children.push(...renderInlineAnnotations(line.annotation, getLineLanguage(line)));
|
|
1486
|
+
return createXmlElement("span", [["ttm:role", ROLE_BACKGROUND], ...renderLineAttributes(line, context)], children);
|
|
1487
|
+
};
|
|
1488
|
+
var renderLine = (line, context) => {
|
|
1489
|
+
const before = [];
|
|
1490
|
+
const after = [];
|
|
1491
|
+
for (const background of line.backgrounds) {
|
|
1492
|
+
const node = renderBackground(background, context);
|
|
1493
|
+
if (background.time !== void 0 && line.time !== void 0 && background.time.start < line.time.start) before.push(node);
|
|
1494
|
+
else after.push(node);
|
|
1495
|
+
}
|
|
1496
|
+
const children = [
|
|
1497
|
+
...before,
|
|
1498
|
+
...renderContent(line, context),
|
|
1499
|
+
...after
|
|
1500
|
+
];
|
|
1501
|
+
if (context.profile === "amll") children.push(...renderInlineAnnotations(line.annotation, getLineLanguage(line)));
|
|
1502
|
+
return createLyricElement("p", renderLineAttributes(line, context), children);
|
|
1503
|
+
};
|
|
1504
|
+
var renderAgents = (lyric, context) => {
|
|
1505
|
+
return lyric.agents.map((agent) => {
|
|
1506
|
+
const type = agent.type === AgentType.PERSON ? "person" : agent.type === AgentType.GROUP ? "group" : agent.raw ?? "other";
|
|
1507
|
+
const names = agent.names.map((name) => {
|
|
1508
|
+
return createXmlElement("ttm:name", [["type", "full"], ...renderLanguage(name.language)], [createXmlText(name.text)]);
|
|
1509
|
+
});
|
|
1510
|
+
return createXmlElement("ttm:agent", [["type", type], ["xml:id", context.agents.get(agent.id)]], names);
|
|
1511
|
+
});
|
|
1512
|
+
};
|
|
1513
|
+
var renderAmllMeta = (meta) => {
|
|
1514
|
+
const nodes = [];
|
|
1515
|
+
const groups = [
|
|
1516
|
+
["musicName", meta.titles],
|
|
1517
|
+
["artists", meta.artists],
|
|
1518
|
+
["album", meta.albums],
|
|
1519
|
+
["ttmlAuthorGithubLogin", meta.authors]
|
|
1520
|
+
];
|
|
1521
|
+
for (const [key, items] of groups) for (const item of items) nodes.push(createXmlElement("amll:meta", [
|
|
1522
|
+
["key", key],
|
|
1523
|
+
["value", item.text],
|
|
1524
|
+
...renderLanguage(item.language)
|
|
1525
|
+
]));
|
|
1526
|
+
for (const value of meta.isrcs) nodes.push(createXmlElement("amll:meta", [["key", "isrc"], ["value", value]]));
|
|
1527
|
+
for (const item of meta.unknowns) nodes.push(createXmlElement("amll:meta", [["key", item.key], ["value", item.value]]));
|
|
1528
|
+
return nodes;
|
|
1529
|
+
};
|
|
1530
|
+
var renderSongwriters = (meta) => {
|
|
1531
|
+
const names = [];
|
|
1532
|
+
for (const credit of meta.credits) {
|
|
1533
|
+
if (credit.role !== CreditRole.LYRICIST) continue;
|
|
1534
|
+
for (const name of credit.names) names.push(createXmlElement("songwriter", renderLanguage(name.language), [createXmlText(name.text)]));
|
|
1535
|
+
}
|
|
1536
|
+
return names.length === 0 ? [] : [createXmlElement("songwriters", [], names)];
|
|
1537
|
+
};
|
|
1538
|
+
var renderAnnotationTracks = (tracks, kind, context) => {
|
|
1539
|
+
if (tracks.size === 0) return [];
|
|
1540
|
+
const translated = kind === "translations";
|
|
1541
|
+
const blocks = Array.from(tracks, ([language, track]) => {
|
|
1542
|
+
const attributes = translated ? [["type", "subtitle"]] : [];
|
|
1543
|
+
attributes.push(...renderLanguage(language));
|
|
1544
|
+
const texts = Array.from(track, ([line, children]) => {
|
|
1545
|
+
return createLyricElement("text", [["for", context.keys.get(line)], ["xml:space", "preserve"]], children);
|
|
1546
|
+
});
|
|
1547
|
+
return createXmlElement(translated ? "translation" : "transliteration", attributes, texts);
|
|
1548
|
+
});
|
|
1549
|
+
return [createXmlElement(translated ? "translations" : "transliterations", [], blocks)];
|
|
1550
|
+
};
|
|
1551
|
+
/**
|
|
1552
|
+
* A background-only annotation still references its parent paragraph, with a second reference naming the background.
|
|
1553
|
+
*/
|
|
1554
|
+
var renderHeadAnnotations = (lines, context) => {
|
|
1555
|
+
const tracks = {
|
|
1556
|
+
translations: /* @__PURE__ */ new Map(),
|
|
1557
|
+
romans: /* @__PURE__ */ new Map()
|
|
1558
|
+
};
|
|
1559
|
+
const append = (annotation, line, background) => {
|
|
1560
|
+
if (annotation === void 0) return;
|
|
1561
|
+
for (const kind of ANNOTATION_KINDS) for (const item of annotation[kind]) {
|
|
1562
|
+
const language = item.language?.toLowerCase();
|
|
1563
|
+
let track = tracks[kind].get(language);
|
|
1564
|
+
if (track === void 0) {
|
|
1565
|
+
track = /* @__PURE__ */ new Map();
|
|
1566
|
+
tracks[kind].set(language, track);
|
|
1567
|
+
}
|
|
1568
|
+
let nodes = track.get(line);
|
|
1569
|
+
if (nodes === void 0) {
|
|
1570
|
+
nodes = [];
|
|
1571
|
+
track.set(line, nodes);
|
|
1572
|
+
}
|
|
1573
|
+
nodes.push(background === void 0 ? createXmlText(item.text) : createXmlElement("span", [["ttm:role", ROLE_BACKGROUND], ["for", context.keys.get(background)]], [createXmlText(item.text)]));
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
for (const line of lines) {
|
|
1577
|
+
append(line.annotation, line);
|
|
1578
|
+
for (const background of line.backgrounds) append(background.annotation, line, background);
|
|
1579
|
+
}
|
|
1580
|
+
return ANNOTATION_KINDS.flatMap((kind) => {
|
|
1581
|
+
return renderAnnotationTracks(tracks[kind], kind, context);
|
|
1582
|
+
});
|
|
1583
|
+
};
|
|
1584
|
+
var renderMetadata = (lyric, lines, context, includeMeta) => {
|
|
1585
|
+
const metadata = renderAgents(lyric, context);
|
|
1586
|
+
const itunes = [];
|
|
1587
|
+
if (includeMeta && lyric.meta !== void 0) {
|
|
1588
|
+
if (context.profile === "amll") metadata.push(...renderAmllMeta(lyric.meta));
|
|
1589
|
+
itunes.push(...renderSongwriters(lyric.meta));
|
|
1590
|
+
}
|
|
1591
|
+
if (context.profile === "itunes") itunes.push(...renderHeadAnnotations(lines, context));
|
|
1592
|
+
if (itunes.length > 0) metadata.push(createXmlElement("iTunesMetadata", [["xmlns", NAMESPACE_ITUNES_METADATA]], itunes));
|
|
1593
|
+
return createXmlElement("metadata", renderLanguage(void 0, lyric.meta?.language), metadata);
|
|
1594
|
+
};
|
|
1595
|
+
var renderRootAttributes = (lyric, profile) => {
|
|
1596
|
+
const attributes = [
|
|
1597
|
+
["xmlns", NAMESPACE_TTML],
|
|
1598
|
+
["xmlns:ttm", NAMESPACE_TTML_METADATA],
|
|
1599
|
+
["xmlns:tts", NAMESPACE_TTML_STYLING],
|
|
1600
|
+
["xmlns:itunes", NAMESPACE_ITUNES]
|
|
1601
|
+
];
|
|
1602
|
+
if (profile === "amll") attributes.push(["xmlns:amll", NAMESPACE_AMLL]);
|
|
1603
|
+
attributes.push(...renderLanguage(lyric.meta?.language), ["itunes:timing", lyric.timing === Timing.WORD ? TIMING_WORD : TIMING_LINE]);
|
|
1604
|
+
return attributes;
|
|
1605
|
+
};
|
|
1606
|
+
/**
|
|
1607
|
+
* Generates one TTML document, retaining the source profile unless explicitly overridden.
|
|
1608
|
+
*
|
|
1609
|
+
* Offsets are baked into timestamps; a negative result or a missing required time throws.
|
|
1610
|
+
* Profile mappings retain the lead agent and first AMLL ruby, omitting unsupported fields and instrumental nodes.
|
|
1611
|
+
*/
|
|
1612
|
+
var generateTtml = (lyric, options = {}) => {
|
|
1613
|
+
if (lyric.timing !== Timing.LINE && lyric.timing !== Timing.WORD) throw new Error("TTML generation requires line or word timing");
|
|
1614
|
+
const resolved = resolveOptions(lyric, options);
|
|
1615
|
+
const lines = lyric.lines.filter((line) => {
|
|
1616
|
+
return line.type !== LineType.INSTRUMENTAL;
|
|
1617
|
+
});
|
|
1618
|
+
const context = makeContext(lyric, lines, resolved.profile);
|
|
1619
|
+
const metadata = renderMetadata(lyric, lines, context, resolved.includeMeta);
|
|
1620
|
+
const paragraphs = lines.map((line) => {
|
|
1621
|
+
return renderLine(line, context);
|
|
1622
|
+
});
|
|
1623
|
+
return serializeXml(createXmlElement("tt", renderRootAttributes(lyric, resolved.profile), [createXmlElement("head", [], [metadata]), createXmlElement("body", [], [createXmlElement("div", [], paragraphs)])]), { indent: resolved.indent });
|
|
1624
|
+
};
|
|
1625
|
+
//#endregion
|
|
1626
|
+
//#region src/formats/ttml/parser.ts
|
|
1627
|
+
var METADATA_NAMES = [
|
|
1628
|
+
"agent",
|
|
1629
|
+
"songwriter",
|
|
1630
|
+
"meta",
|
|
1631
|
+
"translation",
|
|
1632
|
+
"transliteration"
|
|
1633
|
+
];
|
|
1634
|
+
var WORD_SEPARATOR_PATTERN = /\p{White_Space}/u;
|
|
1635
|
+
var readTtmlProfile = (source) => {
|
|
1636
|
+
return AMLL_HINT_PATTERN.test(source) ? "amll" : "itunes";
|
|
1637
|
+
};
|
|
1638
|
+
var readSpanTime = (element) => {
|
|
1162
1639
|
const begin = getXmlAttribute(element, "begin");
|
|
1163
1640
|
if (!begin) return;
|
|
1164
1641
|
const start = parseTime(begin);
|
|
@@ -1169,120 +1646,345 @@ var readLineTime = (element) => {
|
|
|
1169
1646
|
end: end ? parseTime(end) : void 0
|
|
1170
1647
|
};
|
|
1171
1648
|
};
|
|
1649
|
+
var readLineTime = (element) => {
|
|
1650
|
+
return readSpanTime(element);
|
|
1651
|
+
};
|
|
1652
|
+
var readAgentType = (raw) => {
|
|
1653
|
+
switch (raw) {
|
|
1654
|
+
case "person": return {
|
|
1655
|
+
type: AgentType.PERSON,
|
|
1656
|
+
raw: void 0
|
|
1657
|
+
};
|
|
1658
|
+
case "group": return {
|
|
1659
|
+
type: AgentType.GROUP,
|
|
1660
|
+
raw: void 0
|
|
1661
|
+
};
|
|
1662
|
+
default: return {
|
|
1663
|
+
type: AgentType.OTHER,
|
|
1664
|
+
raw: raw ?? "other"
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
var readDeclaredTiming = (root) => {
|
|
1669
|
+
switch (getXmlQualifiedAttribute(root, "itunes:timing")) {
|
|
1670
|
+
case TIMING_WORD: return Timing.WORD;
|
|
1671
|
+
case TIMING_LINE: return Timing.LINE;
|
|
1672
|
+
default: return;
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
var readPreserveSpace = (element, inherited) => {
|
|
1676
|
+
switch (getXmlQualifiedAttribute(element, "xml:space")) {
|
|
1677
|
+
case "preserve": return true;
|
|
1678
|
+
case "default": return false;
|
|
1679
|
+
default: return inherited;
|
|
1680
|
+
}
|
|
1681
|
+
};
|
|
1682
|
+
var readLanguage = (element, inherited) => {
|
|
1683
|
+
const language = getXmlQualifiedAttribute(element, "xml:lang") ?? getXmlAttribute(element, "lang");
|
|
1684
|
+
return language === void 0 ? inherited : lowerTag(language);
|
|
1685
|
+
};
|
|
1686
|
+
var readText = (element, inherited) => {
|
|
1687
|
+
const text = getXmlTextContent(element);
|
|
1688
|
+
return readPreserveSpace(element, inherited) ? text : text.trim();
|
|
1689
|
+
};
|
|
1172
1690
|
/**
|
|
1173
|
-
*
|
|
1174
|
-
*
|
|
1175
|
-
* A span's own trailing space is part of the word it follows, so leading and trailing runs both survive.
|
|
1691
|
+
* Preserves the parser's whitespace segmentation when turning text into word drafts.
|
|
1176
1692
|
*/
|
|
1177
|
-
var splitWords = (text) => {
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
text
|
|
1181
|
-
space:
|
|
1693
|
+
var splitWords = (text, language) => {
|
|
1694
|
+
if (text === "") return [];
|
|
1695
|
+
if (!WORD_SEPARATOR_PATTERN.test(text)) return [{
|
|
1696
|
+
text,
|
|
1697
|
+
space: false,
|
|
1698
|
+
language,
|
|
1182
1699
|
rubies: []
|
|
1183
|
-
}
|
|
1700
|
+
}];
|
|
1701
|
+
const words = [];
|
|
1702
|
+
for (const run of text.split(/(\p{White_Space}+)/u)) if (run !== "") {
|
|
1703
|
+
const space = /^\p{White_Space}+$/u.test(run);
|
|
1704
|
+
words.push({
|
|
1705
|
+
text: run,
|
|
1706
|
+
space,
|
|
1707
|
+
language: space ? void 0 : language,
|
|
1708
|
+
rubies: []
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1184
1711
|
return words;
|
|
1185
1712
|
};
|
|
1186
1713
|
/**
|
|
1714
|
+
* Distinguishes indentation runs from separators using the parser's existing rule.
|
|
1715
|
+
*/
|
|
1716
|
+
var isFormattingWhitespace = (text) => {
|
|
1717
|
+
return text.trim() === "" && text.includes("\n");
|
|
1718
|
+
};
|
|
1719
|
+
/**
|
|
1720
|
+
* Builds words from source drafts without deriving missing timestamps.
|
|
1721
|
+
*/
|
|
1722
|
+
var buildWords = (draft) => {
|
|
1723
|
+
const words = [];
|
|
1724
|
+
for (const word of draft.words) {
|
|
1725
|
+
if (word.space) {
|
|
1726
|
+
words.push(makeWordSpace({ text: word.text }));
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
const time = word.start === void 0 ? void 0 : makeTimeRange({
|
|
1730
|
+
start: word.start,
|
|
1731
|
+
end: word.end
|
|
1732
|
+
});
|
|
1733
|
+
if (word.rubies.length === 0) {
|
|
1734
|
+
words.push(makeWordNormal({
|
|
1735
|
+
text: word.text,
|
|
1736
|
+
time,
|
|
1737
|
+
language: word.language
|
|
1738
|
+
}));
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
const rubies = word.rubies.map((ruby) => {
|
|
1742
|
+
return makeWordAnnotationRuby({
|
|
1743
|
+
time: ruby.start === void 0 ? void 0 : makeTimeRange({
|
|
1744
|
+
start: ruby.start,
|
|
1745
|
+
end: ruby.end
|
|
1746
|
+
}),
|
|
1747
|
+
language: ruby.language,
|
|
1748
|
+
tokens: ruby.tokens.map((token) => {
|
|
1749
|
+
return makeWordAnnotationToken({
|
|
1750
|
+
text: token.text,
|
|
1751
|
+
time: token.start === void 0 ? void 0 : makeTimeRange({
|
|
1752
|
+
start: token.start,
|
|
1753
|
+
end: token.end
|
|
1754
|
+
})
|
|
1755
|
+
});
|
|
1756
|
+
})
|
|
1757
|
+
});
|
|
1758
|
+
});
|
|
1759
|
+
words.push(makeWordNormal({
|
|
1760
|
+
text: word.text,
|
|
1761
|
+
time,
|
|
1762
|
+
language: word.language,
|
|
1763
|
+
annotation: makeWordAnnotation({ rubies })
|
|
1764
|
+
}));
|
|
1765
|
+
}
|
|
1766
|
+
return words;
|
|
1767
|
+
};
|
|
1768
|
+
/**
|
|
1769
|
+
* Builds only annotations explicitly present in the source drafts.
|
|
1770
|
+
*/
|
|
1771
|
+
var buildLineAnnotation = (draft) => {
|
|
1772
|
+
if (draft.translations.length === 0 && draft.romans.length === 0) return;
|
|
1773
|
+
return makeLineAnnotation({
|
|
1774
|
+
translations: draft.translations.map((item) => {
|
|
1775
|
+
return makeLineAnnotationTranslation({
|
|
1776
|
+
text: item.text,
|
|
1777
|
+
language: item.language
|
|
1778
|
+
});
|
|
1779
|
+
}),
|
|
1780
|
+
romans: draft.romans.map((item) => {
|
|
1781
|
+
return makeLineAnnotationRoman({
|
|
1782
|
+
text: item.text,
|
|
1783
|
+
language: item.language
|
|
1784
|
+
});
|
|
1785
|
+
})
|
|
1786
|
+
});
|
|
1787
|
+
};
|
|
1788
|
+
/**
|
|
1789
|
+
* Separates head annotation text from its background references.
|
|
1790
|
+
*/
|
|
1791
|
+
var splitBackgroundText = (element, preserveSpace, language) => {
|
|
1792
|
+
preserveSpace = readPreserveSpace(element, preserveSpace);
|
|
1793
|
+
language = readLanguage(element, language);
|
|
1794
|
+
let main = "";
|
|
1795
|
+
const backgrounds = [];
|
|
1796
|
+
for (const child of element.children) {
|
|
1797
|
+
if (child.nodeType === XmlNodeType.element && child.localName === "span" && getXmlAttribute(child, "role") === "x-bg") {
|
|
1798
|
+
backgrounds.push({
|
|
1799
|
+
text: readText(child, preserveSpace),
|
|
1800
|
+
key: getXmlAttribute(child, "for"),
|
|
1801
|
+
language: readLanguage(child, language)
|
|
1802
|
+
});
|
|
1803
|
+
continue;
|
|
1804
|
+
}
|
|
1805
|
+
main += getXmlTextContent(child);
|
|
1806
|
+
}
|
|
1807
|
+
return {
|
|
1808
|
+
main: preserveSpace ? main : main.trim(),
|
|
1809
|
+
backgrounds
|
|
1810
|
+
};
|
|
1811
|
+
};
|
|
1812
|
+
/**
|
|
1813
|
+
* Carries language and whitespace declarations through wrappers to the elements they govern.
|
|
1814
|
+
*/
|
|
1815
|
+
var groupElements = (root, names, preserveSpace, stopAtMatch = false, language) => {
|
|
1816
|
+
const wanted = new Set(names);
|
|
1817
|
+
const result = /* @__PURE__ */ new Map();
|
|
1818
|
+
const stack = [{
|
|
1819
|
+
element: root,
|
|
1820
|
+
preserveSpace,
|
|
1821
|
+
language
|
|
1822
|
+
}];
|
|
1823
|
+
while (stack.length > 0) {
|
|
1824
|
+
const entry = stack.pop();
|
|
1825
|
+
if (entry === void 0) continue;
|
|
1826
|
+
const element = entry.element;
|
|
1827
|
+
const currentSpace = readPreserveSpace(element, entry.preserveSpace);
|
|
1828
|
+
const currentLanguage = readLanguage(element, entry.language);
|
|
1829
|
+
if (wanted.has(element.localName)) {
|
|
1830
|
+
const group = result.get(element.localName);
|
|
1831
|
+
const context = {
|
|
1832
|
+
element,
|
|
1833
|
+
preserveSpace: currentSpace,
|
|
1834
|
+
language: currentLanguage
|
|
1835
|
+
};
|
|
1836
|
+
if (group === void 0) result.set(element.localName, [context]);
|
|
1837
|
+
else group.push(context);
|
|
1838
|
+
if (stopAtMatch) continue;
|
|
1839
|
+
}
|
|
1840
|
+
const children = element.children;
|
|
1841
|
+
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
1842
|
+
const child = children[index];
|
|
1843
|
+
if (child && child.nodeType === XmlNodeType.element) stack.push({
|
|
1844
|
+
element: child,
|
|
1845
|
+
preserveSpace: currentSpace,
|
|
1846
|
+
language: currentLanguage
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
return result;
|
|
1851
|
+
};
|
|
1852
|
+
var findElements = (root, name, preserveSpace, stopAtMatch = false, language) => {
|
|
1853
|
+
return groupElements(root, [name], preserveSpace, stopAtMatch, language).get(name) ?? [];
|
|
1854
|
+
};
|
|
1855
|
+
/**
|
|
1187
1856
|
* Reads an AMLL ruby container into one word plus its per-syllable annotation.
|
|
1188
1857
|
*/
|
|
1189
|
-
var readRuby = (container) => {
|
|
1858
|
+
var readRuby = (container, preserveSpace, language) => {
|
|
1859
|
+
preserveSpace = readPreserveSpace(container, preserveSpace);
|
|
1860
|
+
language = readLanguage(container, language);
|
|
1190
1861
|
let base = "";
|
|
1862
|
+
let baseLanguage = language;
|
|
1863
|
+
let rubyTime;
|
|
1864
|
+
let rubyLanguage = language;
|
|
1191
1865
|
const tokens = [];
|
|
1192
1866
|
for (const child of getXmlChildElements(container, "span")) {
|
|
1193
1867
|
const kind = getXmlAttribute(child, "ruby");
|
|
1194
1868
|
if (kind === "base") {
|
|
1195
|
-
base =
|
|
1869
|
+
base = readText(child, preserveSpace);
|
|
1870
|
+
baseLanguage = readLanguage(child, language);
|
|
1196
1871
|
continue;
|
|
1197
1872
|
}
|
|
1198
1873
|
if (kind !== "textContainer") continue;
|
|
1874
|
+
rubyTime ??= readSpanTime(child);
|
|
1875
|
+
rubyLanguage = readLanguage(child, language);
|
|
1876
|
+
const rubySpace = readPreserveSpace(child, preserveSpace);
|
|
1199
1877
|
for (const text of getXmlChildElements(child, "span")) {
|
|
1200
1878
|
if (getXmlAttribute(text, "ruby") !== "text") continue;
|
|
1201
1879
|
const time = readSpanTime(text);
|
|
1202
|
-
const content =
|
|
1203
|
-
if (
|
|
1880
|
+
const content = readText(text, rubySpace);
|
|
1881
|
+
if (!content) continue;
|
|
1204
1882
|
tokens.push({
|
|
1205
|
-
start: time
|
|
1206
|
-
end: time
|
|
1883
|
+
start: time?.start,
|
|
1884
|
+
end: time?.end,
|
|
1207
1885
|
text: content
|
|
1208
1886
|
});
|
|
1209
1887
|
}
|
|
1210
1888
|
}
|
|
1211
|
-
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1889
|
+
if (!base) return;
|
|
1890
|
+
const first = tokens.find((token) => {
|
|
1891
|
+
return token.start !== void 0;
|
|
1892
|
+
});
|
|
1893
|
+
const last = tokens.findLast((token) => {
|
|
1894
|
+
return token.start !== void 0;
|
|
1895
|
+
});
|
|
1896
|
+
const time = readSpanTime(container) ?? rubyTime ?? (first === void 0 ? void 0 : {
|
|
1897
|
+
start: first.start,
|
|
1898
|
+
end: last?.end
|
|
1899
|
+
});
|
|
1214
1900
|
return {
|
|
1215
1901
|
text: base,
|
|
1216
1902
|
space: false,
|
|
1217
|
-
start:
|
|
1218
|
-
end:
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1903
|
+
start: time?.start,
|
|
1904
|
+
end: time?.end,
|
|
1905
|
+
language: baseLanguage,
|
|
1906
|
+
rubies: tokens.length === 0 ? [] : [{
|
|
1907
|
+
start: rubyTime?.start,
|
|
1908
|
+
end: rubyTime?.end,
|
|
1909
|
+
language: rubyLanguage,
|
|
1222
1910
|
tokens
|
|
1223
1911
|
}]
|
|
1224
1912
|
};
|
|
1225
1913
|
};
|
|
1226
1914
|
/**
|
|
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
1915
|
* Collects the words of one span-bearing element.
|
|
1234
1916
|
*
|
|
1235
|
-
* Text between spans carries the separators, and a span without
|
|
1917
|
+
* Text between spans carries the separators, and a span without a readable begin contributes its text untimed.
|
|
1236
1918
|
* A span carrying a role is annotation rather than a word, so it is skipped here.
|
|
1237
|
-
*
|
|
1919
|
+
* Whitespace inside a span remains a separator; indentation is dropped only without a preserve declaration.
|
|
1238
1920
|
*/
|
|
1239
|
-
var readWords = (element) => {
|
|
1921
|
+
var readWords = (element, preserveSpace, language) => {
|
|
1922
|
+
preserveSpace = readPreserveSpace(element, preserveSpace);
|
|
1923
|
+
language = readLanguage(element, language);
|
|
1240
1924
|
const words = [];
|
|
1241
1925
|
let wordLevel = false;
|
|
1242
1926
|
for (const child of element.children) {
|
|
1243
|
-
if (child.nodeType
|
|
1244
|
-
if (!isFormattingWhitespace(child.text)) words.push(...splitWords(child.text));
|
|
1927
|
+
if (child.nodeType !== XmlNodeType.element) {
|
|
1928
|
+
if (preserveSpace || !isFormattingWhitespace(child.text)) words.push(...splitWords(child.text, language));
|
|
1245
1929
|
continue;
|
|
1246
1930
|
}
|
|
1247
|
-
if (child.
|
|
1931
|
+
if (child.localName !== "span") continue;
|
|
1248
1932
|
if (getXmlAttribute(child, "role") !== void 0) continue;
|
|
1249
1933
|
if (getXmlAttribute(child, "ruby") === "container") {
|
|
1250
|
-
const ruby = readRuby(child);
|
|
1934
|
+
const ruby = readRuby(child, preserveSpace, language);
|
|
1251
1935
|
if (ruby !== void 0) {
|
|
1252
|
-
words.push(
|
|
1253
|
-
|
|
1936
|
+
for (const part of splitWords(ruby.text)) words.push(part.space ? part : {
|
|
1937
|
+
...ruby,
|
|
1938
|
+
text: part.text
|
|
1939
|
+
});
|
|
1940
|
+
wordLevel ||= ruby.start !== void 0;
|
|
1254
1941
|
}
|
|
1255
1942
|
continue;
|
|
1256
1943
|
}
|
|
1257
|
-
const text = getXmlTextContent(child)
|
|
1258
|
-
|
|
1944
|
+
const text = getXmlTextContent(child);
|
|
1945
|
+
const childSpace = readPreserveSpace(child, preserveSpace);
|
|
1946
|
+
if (!text || !childSpace && isFormattingWhitespace(text)) continue;
|
|
1259
1947
|
const time = readSpanTime(child);
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1948
|
+
for (const part of splitWords(text, readLanguage(child, language))) {
|
|
1949
|
+
if (!part.space && time !== void 0) {
|
|
1950
|
+
part.start = time.start;
|
|
1951
|
+
part.end = time.end;
|
|
1952
|
+
wordLevel = true;
|
|
1953
|
+
}
|
|
1954
|
+
words.push(part);
|
|
1267
1955
|
}
|
|
1268
|
-
wordLevel = true;
|
|
1269
|
-
words.push({
|
|
1270
|
-
text,
|
|
1271
|
-
space: false,
|
|
1272
|
-
start: time.start,
|
|
1273
|
-
end: time.end,
|
|
1274
|
-
rubies: []
|
|
1275
|
-
});
|
|
1276
1956
|
}
|
|
1277
1957
|
return {
|
|
1278
1958
|
words,
|
|
1279
1959
|
wordLevel
|
|
1280
1960
|
};
|
|
1281
1961
|
};
|
|
1962
|
+
var readInlineAnnotations = (element, preserveSpace, language) => {
|
|
1963
|
+
const translations = [];
|
|
1964
|
+
const romans = [];
|
|
1965
|
+
for (const span of getXmlChildElements(element, "span")) {
|
|
1966
|
+
const role = getXmlAttribute(span, "role");
|
|
1967
|
+
if (role !== "x-translation" && role !== "x-roman") continue;
|
|
1968
|
+
const text = readText(span, preserveSpace);
|
|
1969
|
+
if (!text) continue;
|
|
1970
|
+
const item = {
|
|
1971
|
+
language: readLanguage(span, language),
|
|
1972
|
+
text
|
|
1973
|
+
};
|
|
1974
|
+
if (role === "x-translation") translations.push(item);
|
|
1975
|
+
else romans.push(item);
|
|
1976
|
+
}
|
|
1977
|
+
return {
|
|
1978
|
+
translations,
|
|
1979
|
+
romans
|
|
1980
|
+
};
|
|
1981
|
+
};
|
|
1282
1982
|
/**
|
|
1283
1983
|
* Reads one paragraph, pulling its inline annotations and background lines out of the word stream.
|
|
1284
1984
|
*/
|
|
1285
|
-
var readLine = (element) => {
|
|
1985
|
+
var readLine = (element, preserveSpace, language) => {
|
|
1986
|
+
preserveSpace = readPreserveSpace(element, preserveSpace);
|
|
1987
|
+
language = readLanguage(element, language);
|
|
1286
1988
|
const time = readLineTime(element);
|
|
1287
1989
|
if (time === void 0) return;
|
|
1288
1990
|
const draft = {
|
|
@@ -1294,33 +1996,25 @@ var readLine = (element) => {
|
|
|
1294
1996
|
translations: [],
|
|
1295
1997
|
romans: [],
|
|
1296
1998
|
backgrounds: [],
|
|
1297
|
-
wordLevel: false
|
|
1999
|
+
wordLevel: false,
|
|
2000
|
+
preserveSpace,
|
|
2001
|
+
language
|
|
1298
2002
|
};
|
|
1299
2003
|
if (!hasXmlChildElement(element, "span")) {
|
|
1300
|
-
const plain =
|
|
2004
|
+
const plain = readText(element, preserveSpace);
|
|
1301
2005
|
if (!plain) return;
|
|
1302
|
-
draft.words = splitWords(plain);
|
|
2006
|
+
draft.words = splitWords(plain, language);
|
|
1303
2007
|
return draft;
|
|
1304
2008
|
}
|
|
1305
|
-
const read = readWords(element);
|
|
2009
|
+
const read = readWords(element, preserveSpace, language);
|
|
1306
2010
|
draft.words = read.words;
|
|
1307
2011
|
draft.wordLevel = read.wordLevel;
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
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);
|
|
2012
|
+
const annotations = readInlineAnnotations(element, preserveSpace, language);
|
|
2013
|
+
draft.translations = annotations.translations;
|
|
2014
|
+
draft.romans = annotations.romans;
|
|
2015
|
+
for (const span of getXmlChildElements(element, "span")) if (getXmlAttribute(span, "role") === "x-bg") {
|
|
2016
|
+
const background = readBackground(span, preserveSpace, language);
|
|
2017
|
+
if (background) draft.backgrounds.push(background);
|
|
1324
2018
|
}
|
|
1325
2019
|
return draft;
|
|
1326
2020
|
};
|
|
@@ -1329,8 +2023,10 @@ var readLine = (element) => {
|
|
|
1329
2023
|
*
|
|
1330
2024
|
* A background line states no time of its own in AMLL files, so its range comes from its words.
|
|
1331
2025
|
*/
|
|
1332
|
-
var readBackground = (element) => {
|
|
1333
|
-
|
|
2026
|
+
var readBackground = (element, preserveSpace, language) => {
|
|
2027
|
+
preserveSpace = readPreserveSpace(element, preserveSpace);
|
|
2028
|
+
language = readLanguage(element, language);
|
|
2029
|
+
const read = readWords(element, preserveSpace, language);
|
|
1334
2030
|
if (read.words.length === 0) return;
|
|
1335
2031
|
const begin = getXmlAttribute(element, "begin");
|
|
1336
2032
|
const end = getXmlAttribute(element, "end");
|
|
@@ -1348,77 +2044,19 @@ var readBackground = (element) => {
|
|
|
1348
2044
|
agentId: getXmlAttribute(element, "agent"),
|
|
1349
2045
|
key: getXmlAttribute(element, "key"),
|
|
1350
2046
|
words: read.words,
|
|
1351
|
-
|
|
1352
|
-
romans: [],
|
|
2047
|
+
...readInlineAnnotations(element, preserveSpace, language),
|
|
1353
2048
|
backgrounds: [],
|
|
1354
|
-
wordLevel: read.wordLevel
|
|
2049
|
+
wordLevel: read.wordLevel,
|
|
2050
|
+
preserveSpace,
|
|
2051
|
+
language
|
|
1355
2052
|
};
|
|
1356
2053
|
};
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
-
});
|
|
2054
|
+
var buildContent = (draft) => {
|
|
2055
|
+
if (draft.wordLevel) return makeLineContentWords(buildWords(draft));
|
|
2056
|
+
const text = draft.words.map((word) => {
|
|
2057
|
+
return word.text;
|
|
2058
|
+
}).join("");
|
|
2059
|
+
return makeLineContentText(draft.preserveSpace ? text : text.trim());
|
|
1422
2060
|
};
|
|
1423
2061
|
/**
|
|
1424
2062
|
* Builds one background line.
|
|
@@ -1431,7 +2069,8 @@ var buildBackground = (draft) => {
|
|
|
1431
2069
|
end: draft.end
|
|
1432
2070
|
}),
|
|
1433
2071
|
agents: draft.agentId === void 0 ? [] : [draft.agentId],
|
|
1434
|
-
|
|
2072
|
+
languages: draft.language === void 0 ? [] : [draft.language],
|
|
2073
|
+
content: buildContent(draft),
|
|
1435
2074
|
annotation: buildLineAnnotation(draft)
|
|
1436
2075
|
});
|
|
1437
2076
|
};
|
|
@@ -1439,9 +2078,6 @@ var buildBackground = (draft) => {
|
|
|
1439
2078
|
* Builds one normal line, keeping plain text whole unless the source timed its words.
|
|
1440
2079
|
*/
|
|
1441
2080
|
var buildLine = (draft) => {
|
|
1442
|
-
const content = draft.wordLevel ? makeLineContentWords(buildWords(draft)) : makeLineContentText(draft.words.map((word) => {
|
|
1443
|
-
return word.text;
|
|
1444
|
-
}).join("").trim());
|
|
1445
2081
|
return makeLineNormal({
|
|
1446
2082
|
id: draft.key,
|
|
1447
2083
|
time: makeTimeRange({
|
|
@@ -1449,54 +2085,34 @@ var buildLine = (draft) => {
|
|
|
1449
2085
|
end: draft.end
|
|
1450
2086
|
}),
|
|
1451
2087
|
agents: draft.agentId === void 0 ? [] : [draft.agentId],
|
|
1452
|
-
|
|
2088
|
+
languages: draft.language === void 0 ? [] : [draft.language],
|
|
2089
|
+
content: buildContent(draft),
|
|
1453
2090
|
annotation: buildLineAnnotation(draft),
|
|
1454
2091
|
backgrounds: draft.backgrounds.map(buildBackground)
|
|
1455
2092
|
});
|
|
1456
2093
|
};
|
|
1457
2094
|
/**
|
|
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
2095
|
* Reads the head's agents, which lines reference by id.
|
|
1480
2096
|
*/
|
|
1481
|
-
var readAgents = (
|
|
1482
|
-
if (!metadata) return [];
|
|
2097
|
+
var readAgents = (elements) => {
|
|
1483
2098
|
const agents = [];
|
|
1484
|
-
for (const element of
|
|
2099
|
+
for (const { element, preserveSpace, language } of elements) {
|
|
1485
2100
|
const id = getXmlQualifiedAttribute(element, "xml:id") ?? getXmlAttribute(element, "id");
|
|
1486
2101
|
if (!id) continue;
|
|
1487
2102
|
const names = getXmlChildElements(element, "name").map((name) => {
|
|
1488
|
-
return
|
|
2103
|
+
return makeText({
|
|
2104
|
+
text: readText(name, preserveSpace),
|
|
2105
|
+
language: readLanguage(name, language)
|
|
2106
|
+
});
|
|
1489
2107
|
}).filter((name) => {
|
|
1490
|
-
return name !== "";
|
|
2108
|
+
return name.text !== "";
|
|
1491
2109
|
});
|
|
1492
2110
|
const kind = readAgentType(getXmlAttribute(element, "type"));
|
|
1493
2111
|
agents.push(makeAgent({
|
|
1494
2112
|
id,
|
|
1495
2113
|
type: kind.type,
|
|
1496
2114
|
raw: kind.raw,
|
|
1497
|
-
names
|
|
1498
|
-
return makeText({ text: name });
|
|
1499
|
-
})
|
|
2115
|
+
names
|
|
1500
2116
|
}));
|
|
1501
2117
|
}
|
|
1502
2118
|
return agents;
|
|
@@ -1506,43 +2122,56 @@ var readAgents = (metadata) => {
|
|
|
1506
2122
|
*
|
|
1507
2123
|
* AMLL wraps these in `iTunesMetadata`, so the whole head is searched rather than its direct children.
|
|
1508
2124
|
*/
|
|
1509
|
-
var readCredits = (
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
2125
|
+
var readCredits = (elements, meta) => {
|
|
2126
|
+
const names = elements.map(({ element, preserveSpace, language }) => {
|
|
2127
|
+
return makeText({
|
|
2128
|
+
text: readText(element, preserveSpace),
|
|
2129
|
+
language
|
|
2130
|
+
});
|
|
1513
2131
|
}).filter((name) => {
|
|
1514
|
-
return name !== "";
|
|
2132
|
+
return name.text !== "";
|
|
1515
2133
|
});
|
|
1516
2134
|
if (names.length === 0) return;
|
|
1517
2135
|
meta.credits.push(makeMetaCredit({
|
|
1518
2136
|
role: CreditRole.LYRICIST,
|
|
1519
|
-
names
|
|
1520
|
-
return makeText({ text: name });
|
|
1521
|
-
})
|
|
2137
|
+
names
|
|
1522
2138
|
}));
|
|
1523
2139
|
};
|
|
1524
2140
|
/**
|
|
1525
2141
|
* Reads one `amll:meta` entry, keeping an unknown key as written so a round trip loses nothing.
|
|
1526
2142
|
*/
|
|
1527
|
-
var applyAmllMeta = (meta, element) => {
|
|
2143
|
+
var applyAmllMeta = (meta, element, language) => {
|
|
1528
2144
|
const key = getXmlAttribute(element, "key");
|
|
1529
2145
|
const value = getXmlAttribute(element, "value")?.trim();
|
|
1530
2146
|
if (!key || !value) return;
|
|
2147
|
+
language = readLanguage(element, language);
|
|
1531
2148
|
switch (key) {
|
|
1532
2149
|
case "musicName":
|
|
1533
|
-
meta.titles.push(makeText({
|
|
2150
|
+
meta.titles.push(makeText({
|
|
2151
|
+
text: value,
|
|
2152
|
+
language
|
|
2153
|
+
}));
|
|
1534
2154
|
return;
|
|
1535
2155
|
case "artists":
|
|
1536
|
-
meta.artists.push(makeText({
|
|
2156
|
+
meta.artists.push(makeText({
|
|
2157
|
+
text: value,
|
|
2158
|
+
language
|
|
2159
|
+
}));
|
|
1537
2160
|
return;
|
|
1538
2161
|
case "album":
|
|
1539
|
-
meta.albums.push(makeText({
|
|
2162
|
+
meta.albums.push(makeText({
|
|
2163
|
+
text: value,
|
|
2164
|
+
language
|
|
2165
|
+
}));
|
|
1540
2166
|
return;
|
|
1541
2167
|
case "isrc":
|
|
1542
2168
|
meta.isrcs.push(value);
|
|
1543
2169
|
return;
|
|
1544
2170
|
case "ttmlAuthorGithubLogin":
|
|
1545
|
-
meta.authors.push(makeText({
|
|
2171
|
+
meta.authors.push(makeText({
|
|
2172
|
+
text: value,
|
|
2173
|
+
language
|
|
2174
|
+
}));
|
|
1546
2175
|
return;
|
|
1547
2176
|
default: meta.unknowns.push(makeUnknownEntry({
|
|
1548
2177
|
key,
|
|
@@ -1551,52 +2180,33 @@ var applyAmllMeta = (meta, element) => {
|
|
|
1551
2180
|
}
|
|
1552
2181
|
};
|
|
1553
2182
|
/**
|
|
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
2183
|
* Attaches head translations and transliterations to the lines they name by `itunes:key`.
|
|
1576
2184
|
*
|
|
1577
2185
|
* A replacement translation rewrites the line's text, which is how a source swaps one script for another.
|
|
1578
2186
|
*/
|
|
1579
2187
|
var attachHeadAnnotations = (metadata, lineMap, backgroundMap) => {
|
|
1580
|
-
if (
|
|
2188
|
+
if (lineMap.size === 0) return;
|
|
1581
2189
|
const attach = (blocks, kind) => {
|
|
1582
|
-
for (const block of blocks) {
|
|
1583
|
-
const language = lowerTag(getXmlAttribute(block, "lang"));
|
|
2190
|
+
for (const { element: block, preserveSpace, language } of blocks) {
|
|
1584
2191
|
const type = getXmlQualifiedAttribute(block, "type") ?? getXmlAttribute(block, "type");
|
|
1585
2192
|
for (const text of getXmlChildElements(block, "text")) {
|
|
1586
2193
|
const key = getXmlAttribute(text, "for");
|
|
1587
2194
|
const line = key ? lineMap.get(key) : void 0;
|
|
1588
2195
|
if (!line) continue;
|
|
1589
|
-
const
|
|
2196
|
+
const textLanguage = readLanguage(text, language);
|
|
2197
|
+
const { main, backgrounds } = splitBackgroundText(text, preserveSpace, textLanguage);
|
|
1590
2198
|
if (kind === "translation" && type === "replacement") {
|
|
1591
2199
|
if (main) {
|
|
1592
|
-
line.words = splitWords(main);
|
|
2200
|
+
line.words = splitWords(main, textLanguage);
|
|
1593
2201
|
line.wordLevel = false;
|
|
2202
|
+
line.preserveSpace = readPreserveSpace(text, preserveSpace);
|
|
2203
|
+
line.language = textLanguage;
|
|
1594
2204
|
}
|
|
1595
2205
|
continue;
|
|
1596
2206
|
}
|
|
1597
2207
|
if (main) {
|
|
1598
2208
|
const item = {
|
|
1599
|
-
language,
|
|
2209
|
+
language: textLanguage,
|
|
1600
2210
|
text: main
|
|
1601
2211
|
};
|
|
1602
2212
|
if (kind === "translation") line.translations.push(item);
|
|
@@ -1607,7 +2217,7 @@ var attachHeadAnnotations = (metadata, lineMap, backgroundMap) => {
|
|
|
1607
2217
|
const target = background.key ? backgroundMap.get(background.key) : void 0;
|
|
1608
2218
|
if (!target) continue;
|
|
1609
2219
|
const item = {
|
|
1610
|
-
language,
|
|
2220
|
+
language: background.language,
|
|
1611
2221
|
text: background.text
|
|
1612
2222
|
};
|
|
1613
2223
|
if (kind === "translation") target.translations.push(item);
|
|
@@ -1616,36 +2226,8 @@ var attachHeadAnnotations = (metadata, lineMap, backgroundMap) => {
|
|
|
1616
2226
|
}
|
|
1617
2227
|
}
|
|
1618
2228
|
};
|
|
1619
|
-
attach(
|
|
1620
|
-
attach(
|
|
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
|
-
}
|
|
2229
|
+
attach(metadata.get("translation") ?? [], "translation");
|
|
2230
|
+
attach(metadata.get("transliteration") ?? [], "roman");
|
|
1649
2231
|
};
|
|
1650
2232
|
/**
|
|
1651
2233
|
* Reads a TTML document into the model.
|
|
@@ -1660,8 +2242,8 @@ var parseTtml = (input, options = {}) => {
|
|
|
1660
2242
|
status: LyricStatus.INVALID,
|
|
1661
2243
|
meta: makeMeta()
|
|
1662
2244
|
}));
|
|
1663
|
-
const extra = { profile: readTtmlProfile(content
|
|
1664
|
-
const document = parseXml(content
|
|
2245
|
+
const extra = { profile: readTtmlProfile(content) };
|
|
2246
|
+
const document = parseXml(content);
|
|
1665
2247
|
const root = document ? findXmlElement(document, "tt") : void 0;
|
|
1666
2248
|
if (!root) return finalizeLyric(makeLyric({
|
|
1667
2249
|
format: FORMAT,
|
|
@@ -1669,24 +2251,25 @@ var parseTtml = (input, options = {}) => {
|
|
|
1669
2251
|
meta: makeMeta(),
|
|
1670
2252
|
extra
|
|
1671
2253
|
}));
|
|
1672
|
-
const
|
|
1673
|
-
const
|
|
2254
|
+
const language = readLanguage(root, lowerTag(options.language));
|
|
2255
|
+
const body = findElements(root, "body", false, true, language)[0];
|
|
2256
|
+
const metadata = findElements(root, "metadata", false, true, language)[0];
|
|
2257
|
+
const metadataElements = metadata === void 0 ? /* @__PURE__ */ new Map() : groupElements(metadata.element, METADATA_NAMES, metadata.preserveSpace, false, metadata.language);
|
|
1674
2258
|
const meta = makeMeta();
|
|
1675
|
-
const language = lowerTag(getXmlQualifiedAttribute(root, "xml:lang")) ?? lowerTag(options.language);
|
|
1676
2259
|
if (language !== void 0) meta.language = language;
|
|
1677
|
-
readCredits(
|
|
1678
|
-
for (const element
|
|
2260
|
+
readCredits(metadataElements.get("songwriter") ?? [], meta);
|
|
2261
|
+
for (const { element, language: itemLanguage } of metadataElements.get("meta") ?? []) applyAmllMeta(meta, element, itemLanguage);
|
|
1679
2262
|
const drafts = [];
|
|
1680
2263
|
const lineMap = /* @__PURE__ */ new Map();
|
|
1681
2264
|
const backgroundMap = /* @__PURE__ */ new Map();
|
|
1682
|
-
for (const element of body ?
|
|
1683
|
-
const draft = readLine(element);
|
|
2265
|
+
for (const { element, preserveSpace, language: lineLanguage } of body ? findElements(body.element, "p", body.preserveSpace, true, body.language) : []) {
|
|
2266
|
+
const draft = readLine(element, preserveSpace, lineLanguage);
|
|
1684
2267
|
if (!draft) continue;
|
|
1685
2268
|
drafts.push(draft);
|
|
1686
2269
|
if (draft.key) lineMap.set(draft.key, draft);
|
|
1687
2270
|
for (const background of draft.backgrounds) if (background.key) backgroundMap.set(background.key, background);
|
|
1688
2271
|
}
|
|
1689
|
-
attachHeadAnnotations(
|
|
2272
|
+
attachHeadAnnotations(metadataElements, lineMap, backgroundMap);
|
|
1690
2273
|
if (drafts.length === 0) return finalizeLyric(makeLyric({
|
|
1691
2274
|
format: FORMAT,
|
|
1692
2275
|
status: LyricStatus.INVALID,
|
|
@@ -1695,7 +2278,9 @@ var parseTtml = (input, options = {}) => {
|
|
|
1695
2278
|
}));
|
|
1696
2279
|
const lines = drafts.map(buildLine);
|
|
1697
2280
|
const wordLevel = drafts.some((draft) => {
|
|
1698
|
-
return draft.wordLevel
|
|
2281
|
+
return draft.wordLevel || draft.backgrounds.some((background) => {
|
|
2282
|
+
return background.wordLevel;
|
|
2283
|
+
});
|
|
1699
2284
|
});
|
|
1700
2285
|
return finalizeLyric(makeLyric({
|
|
1701
2286
|
format: FORMAT,
|
|
@@ -1703,7 +2288,7 @@ var parseTtml = (input, options = {}) => {
|
|
|
1703
2288
|
timing: readDeclaredTiming(root) ?? (wordLevel ? Timing.WORD : Timing.LINE),
|
|
1704
2289
|
meta,
|
|
1705
2290
|
extra,
|
|
1706
|
-
agents: readAgents(
|
|
2291
|
+
agents: readAgents(metadataElements.get("agent") ?? []),
|
|
1707
2292
|
languages: deriveLanguageUsages(lines),
|
|
1708
2293
|
lines
|
|
1709
2294
|
}));
|
|
@@ -1731,18 +2316,21 @@ var parse = (input, options = {}) => {
|
|
|
1731
2316
|
case "ttml": return parseTtml(input, options);
|
|
1732
2317
|
}
|
|
1733
2318
|
};
|
|
2319
|
+
var GENERATORS = {
|
|
2320
|
+
lrc: generateLrc,
|
|
2321
|
+
ttml: generateTtml
|
|
2322
|
+
};
|
|
1734
2323
|
/**
|
|
1735
2324
|
* Generates text in the requested format, which need not match `lyric.format`.
|
|
1736
2325
|
*
|
|
1737
|
-
*
|
|
2326
|
+
* The writer map pairs each format's options with its result and requires a writer for every supported format.
|
|
2327
|
+
* An unknown target returns `null`.
|
|
1738
2328
|
*/
|
|
1739
2329
|
var generate = (lyric, target, options = {}) => {
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
case "ttml": throw new Error("ttml generation is not implemented yet");
|
|
1743
|
-
}
|
|
2330
|
+
if (!Object.hasOwn(GENERATORS, target)) return null;
|
|
2331
|
+
return GENERATORS[target](lyric, options);
|
|
1744
2332
|
};
|
|
1745
2333
|
//#endregion
|
|
1746
|
-
export {
|
|
2334
|
+
export { detectFormat, detectLrc, detectTtml, generate, generateLrc, generateTtml, parse, parseLrc, parseTtml };
|
|
1747
2335
|
|
|
1748
2336
|
//# sourceMappingURL=formats.js.map
|