@gondola-lyrics/kit 0.4.0 → 0.5.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.d.ts +109 -4
- package/dist/formats.js +1180 -264
- package/dist/formats.js.map +1 -1
- package/dist/generator.d.ts +7 -3
- package/dist/index.d.ts +154 -4
- package/dist/index.js +3 -3
- package/dist/language.js +117 -0
- package/dist/language.js.map +1 -0
- package/dist/parser.d.ts +148 -2
- package/dist/parser.js +3 -3
- package/dist/transforms.d.ts +87 -0
- package/dist/transforms.js +2291 -32
- package/dist/transforms.js.map +1 -1
- package/package.json +1 -1
package/dist/formats.js
CHANGED
|
@@ -1,109 +1,523 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { i as mapShared, n as lowerTag, r as lowerBound } from "./language.js";
|
|
2
|
+
import { AgentType, CreditRole, LineType, LyricStatus, Timing, WordType, canonicalizeLyric, deriveLanguageUsages, deriveLineRomans, getAnnotationItemText, getContentText, getWordsText, isWordLevelLine, makeAgent, makeLineAnnotation, makeLineAnnotationRoman, makeLineAnnotationTranslation, makeLineBackground, makeLineContentText, makeLineContentWords, makeLineInstrumental, makeLineNormal, makeLyric, makeMeta, makeMetaCredit, makeText, makeTimeRange, makeUnknownEntry, makeWordAnnotation, makeWordAnnotationRoman, makeWordAnnotationRuby, makeWordAnnotationToken, makeWordNormal, makeWordSpace, removeOffset, resolveAnnotationItemTime, resolveAnnotationTokenTime, validateLyric } from "@gondola-lyrics/model/runtime";
|
|
3
|
+
//#region src/utils/kana.ts
|
|
4
|
+
var KANA_COMBINATIONS = /* @__PURE__ */ new Map([
|
|
5
|
+
[12353, "うくぐつふゔ"],
|
|
6
|
+
[12355, "うくぐすずつてでふゔ"],
|
|
7
|
+
[12357, "とど"],
|
|
8
|
+
[12359, "いうくぐしじちぢつにひびぴみりふゔ"],
|
|
9
|
+
[12361, "うくぐつふゔ"],
|
|
10
|
+
[12419, "きぎしじちぢにひびぴみり"],
|
|
11
|
+
[12421, "きぎしじちぢにひびぴみりてでふゔ"],
|
|
12
|
+
[12423, "きぎしじちぢにひびぴみり"],
|
|
13
|
+
[12430, "くぐ"]
|
|
14
|
+
]);
|
|
15
|
+
var readKana = (text) => {
|
|
16
|
+
const kana = text.normalize("NFKC");
|
|
17
|
+
if (kana.length !== 1) return "";
|
|
18
|
+
const code = kana.charCodeAt(0);
|
|
19
|
+
return code >= 12449 && code <= 12534 ? String.fromCharCode(code - 96) : kana;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Reports whether the second kana joins the first into one mora, so a single roman fragment may cover both.
|
|
23
|
+
*
|
|
24
|
+
* Katakana and compatibility forms normalize to hiragana first.
|
|
25
|
+
* Doubled consonants, long-vowel marks and three-kana runs are not combinations.
|
|
5
26
|
*/
|
|
6
|
-
var
|
|
7
|
-
|
|
27
|
+
var combinesKana = (first, second) => {
|
|
28
|
+
const base = readKana(first);
|
|
29
|
+
const small = readKana(second);
|
|
30
|
+
return !!base && !!small && !!KANA_COMBINATIONS.get(small.charCodeAt(0))?.includes(base);
|
|
8
31
|
};
|
|
9
|
-
|
|
10
|
-
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/utils/annotation.ts
|
|
34
|
+
var readText$2 = (line) => {
|
|
35
|
+
return getContentText(line.content).trim();
|
|
11
36
|
};
|
|
12
37
|
/**
|
|
13
|
-
*
|
|
38
|
+
* Combines explicit roman tracks with display-only word aggregations, preferring explicit entries for the same language.
|
|
14
39
|
*
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
40
|
+
* Derived entries must not be stored back on the line.
|
|
41
|
+
*/
|
|
42
|
+
var getLineRomans = (line) => {
|
|
43
|
+
const explicit = line.annotation?.romans ?? [];
|
|
44
|
+
const languages = new Set(explicit.map((item) => lowerTag(item.language)));
|
|
45
|
+
const derived = deriveLineRomans(line.content).filter((item) => !languages.has(lowerTag(item.language)));
|
|
46
|
+
return derived.length ? [...explicit, ...derived] : explicit;
|
|
47
|
+
};
|
|
48
|
+
var readWordRange = (word) => {
|
|
49
|
+
const time = word.time;
|
|
50
|
+
if (!time || !Number.isSafeInteger(time.start) || time.start < 0 || time.end !== void 0 && (!Number.isSafeInteger(time.end) || time.end < time.start)) return;
|
|
51
|
+
return time;
|
|
52
|
+
};
|
|
53
|
+
var containsRomanTime = (source, target, tolerance) => {
|
|
54
|
+
const end = target.end ?? Number.POSITIVE_INFINITY;
|
|
55
|
+
if (source.start < target.start - tolerance || source.end !== void 0 && source.end > end + tolerance) return false;
|
|
56
|
+
if (source.end === void 0 || source.start === source.end) return target.start === target.end ? source.start <= end + tolerance : source.start < end + tolerance;
|
|
57
|
+
return true;
|
|
58
|
+
};
|
|
59
|
+
var matchRomanTarget = (time, targets, tolerance) => {
|
|
60
|
+
let matched;
|
|
61
|
+
let priority = -1;
|
|
62
|
+
for (const target of targets) {
|
|
63
|
+
if (!containsRomanTime(time, target.time, tolerance)) continue;
|
|
64
|
+
const exact = time.end !== void 0 && time.start === target.time.start && time.end === target.time.end;
|
|
65
|
+
const point = time.start === time.end && target.time.start === target.time.end;
|
|
66
|
+
const rank = exact ? 3 : point ? 2 : containsRomanTime(time, target.time, 0) ? 1 : 0;
|
|
67
|
+
if (rank > priority) {
|
|
68
|
+
matched = target;
|
|
69
|
+
priority = rank;
|
|
70
|
+
} else if (rank === priority) matched = void 0;
|
|
71
|
+
}
|
|
72
|
+
return matched ?? (priority === -1 ? void 0 : null);
|
|
73
|
+
};
|
|
74
|
+
var overlapLength = (time, target) => {
|
|
75
|
+
const finish = time.end ?? time.start;
|
|
76
|
+
return Math.min(finish, target.time.end ?? Number.POSITIVE_INFINITY) - Math.max(time.start, target.time.start);
|
|
77
|
+
};
|
|
78
|
+
var sharesLanguage = (targets) => {
|
|
79
|
+
return new Set(targets.flatMap((target) => target ? [lowerTag(target.word.language)] : []).filter(Boolean)).size <= 1;
|
|
80
|
+
};
|
|
81
|
+
var isOrderedByTime = (targets) => {
|
|
82
|
+
return targets.every((target, index) => {
|
|
83
|
+
return target.time.start >= (targets[index - 1]?.time.start ?? 0);
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
var canExtendToFollowing = (target, following) => {
|
|
87
|
+
const first = lowerTag(target.word.language);
|
|
88
|
+
const second = lowerTag(following.word.language);
|
|
89
|
+
return (!first || !second || first === second) && combinesKana(target.word.text, following.word.text);
|
|
90
|
+
};
|
|
91
|
+
var recoverRomanTargets = (sources, targets, tolerance, rounding, language) => {
|
|
92
|
+
const slack = Math.max(tolerance, rounding);
|
|
93
|
+
const used = new Set(sources.flatMap((source) => source.target ? [source.target] : []));
|
|
94
|
+
const reserved = /* @__PURE__ */ new Set();
|
|
95
|
+
const conflict = (target) => {
|
|
96
|
+
return target.word.annotation?.romans.some((item) => lowerTag(item.language) === language);
|
|
97
|
+
};
|
|
98
|
+
for (const [index, source] of sources.entries()) {
|
|
99
|
+
const time = source.time;
|
|
100
|
+
if (source.target || !time || time.end === void 0) continue;
|
|
101
|
+
const exact = targets.filter((target) => target.time.start === time.start);
|
|
102
|
+
const candidates = exact.length ? exact : targets.filter((target) => Math.abs(target.time.start - time.start) <= rounding);
|
|
103
|
+
if (candidates.length !== 1) continue;
|
|
104
|
+
const target = candidates[0];
|
|
105
|
+
const following = targets[targets.indexOf(target) + 1];
|
|
106
|
+
if (!following || following.index !== target.index + 1 || !canExtendToFollowing(target, following)) continue;
|
|
107
|
+
if (used.has(target) || used.has(following) || reserved.has(target) || reserved.has(following)) continue;
|
|
108
|
+
if (conflict(target) || following.word.annotation?.romans.length) continue;
|
|
109
|
+
const previous = sources.slice(0, index).findLast((item) => item.target)?.target;
|
|
110
|
+
const next = sources.slice(index + 1).find((item) => item.target)?.target;
|
|
111
|
+
if (target.index <= (previous?.index ?? -1) || following.index >= (next?.index ?? Number.POSITIVE_INFINITY)) continue;
|
|
112
|
+
if (target.time.start < (previous?.time.start ?? 0) || following.time.start > (next?.time.start ?? Number.POSITIVE_INFINITY)) continue;
|
|
113
|
+
if (!sharesLanguage([
|
|
114
|
+
previous,
|
|
115
|
+
target,
|
|
116
|
+
following,
|
|
117
|
+
next
|
|
118
|
+
])) continue;
|
|
119
|
+
if (target.time.end === void 0 || following.time.end === void 0 || Math.abs(following.time.start - target.time.end) > rounding) continue;
|
|
120
|
+
if (time.end <= target.time.end || time.end > following.time.end + slack) continue;
|
|
121
|
+
if (targets.some((candidate) => candidate !== target && candidate !== following && overlapLength(time, candidate) > slack)) continue;
|
|
122
|
+
source.target = target;
|
|
123
|
+
used.add(target);
|
|
124
|
+
reserved.add(following);
|
|
125
|
+
}
|
|
126
|
+
for (let start = 0; start < sources.length; start += 1) {
|
|
127
|
+
if (sources[start].target) continue;
|
|
128
|
+
let end = start + 1;
|
|
129
|
+
while (end < sources.length && !sources[end].target) end += 1;
|
|
130
|
+
const previous = sources[start - 1]?.target;
|
|
131
|
+
const next = sources[end]?.target;
|
|
132
|
+
const pending = targets.filter((target) => {
|
|
133
|
+
return target.index > (previous?.index ?? -1) && target.index < (next?.index ?? Number.POSITIVE_INFINITY) && !reserved.has(target);
|
|
134
|
+
});
|
|
135
|
+
if (pending.length !== end - start) return false;
|
|
136
|
+
if (sources.slice(start, end).some((source) => !source.time) && pending.length > 1 && (!previous || !next)) return false;
|
|
137
|
+
const boundaries = [
|
|
138
|
+
previous,
|
|
139
|
+
...pending,
|
|
140
|
+
next
|
|
141
|
+
].filter((target) => target !== void 0);
|
|
142
|
+
if (!sharesLanguage(boundaries) || !isOrderedByTime(boundaries)) return false;
|
|
143
|
+
for (const [offset, target] of pending.entries()) {
|
|
144
|
+
const source = sources[start + offset];
|
|
145
|
+
if (used.has(target) || conflict(target)) return false;
|
|
146
|
+
const time = source.time;
|
|
147
|
+
if (time && (overlapLength(time, target) <= 0 || targets.some((candidate) => candidate !== target && overlapLength(time, candidate) > slack))) return false;
|
|
148
|
+
source.target = target;
|
|
149
|
+
used.add(target);
|
|
150
|
+
}
|
|
151
|
+
start = end - 1;
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
};
|
|
155
|
+
var readRomanLanguage = (source) => {
|
|
156
|
+
const languages = new Set(source.languages.map(lowerTag).filter(Boolean));
|
|
157
|
+
for (const word of source.content?.words ?? []) {
|
|
158
|
+
const language = word.type === WordType.NORMAL ? lowerTag(word.language) : void 0;
|
|
159
|
+
if (language) languages.add(language);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
language: languages.size === 1 ? languages.values().next().value : lowerTag(source.languages[0]),
|
|
163
|
+
consistent: languages.size <= 1
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
var attachWordRomans = (line, source, tolerance, language, fallback) => {
|
|
167
|
+
const content = line.content;
|
|
168
|
+
const sourceContent = source.line.content;
|
|
169
|
+
if (!content?.words.length || content.text !== void 0 || !sourceContent?.words.length || sourceContent.text !== void 0) return;
|
|
170
|
+
const targets = [];
|
|
171
|
+
let spaces = 0;
|
|
172
|
+
for (const [index, word] of content.words.entries()) {
|
|
173
|
+
if (word.type === WordType.SPACE) {
|
|
174
|
+
spaces += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const time = readWordRange(word);
|
|
178
|
+
if (word.type !== WordType.NORMAL || !time) return;
|
|
179
|
+
targets.push({
|
|
180
|
+
word,
|
|
181
|
+
index,
|
|
182
|
+
time,
|
|
183
|
+
spaces,
|
|
184
|
+
tokens: []
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const sources = [];
|
|
188
|
+
let separator = "";
|
|
189
|
+
for (const word of sourceContent.words) {
|
|
190
|
+
if (word.type === WordType.SPACE) {
|
|
191
|
+
separator += word.text;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const time = readWordRange(word);
|
|
195
|
+
if (word.type !== WordType.NORMAL || !word.text.trim() || word.time && !time || !time && !fallback) return;
|
|
196
|
+
const target = time ? matchRomanTarget(time, targets, tolerance) : void 0;
|
|
197
|
+
if (target === null || !target && !fallback) return;
|
|
198
|
+
sources.push({
|
|
199
|
+
word,
|
|
200
|
+
time,
|
|
201
|
+
separator,
|
|
202
|
+
target
|
|
203
|
+
});
|
|
204
|
+
separator = "";
|
|
205
|
+
}
|
|
206
|
+
const anchors = sources.flatMap((source) => source.target ? [source.target] : []);
|
|
207
|
+
if (anchors.some((target, index) => target.index < (anchors[index - 1]?.index ?? -1))) return;
|
|
208
|
+
const recovered = fallback && sources.some((item) => !item.target);
|
|
209
|
+
if (recovered && !recoverRomanTargets(sources, targets, tolerance, source.rounding, language)) return;
|
|
210
|
+
let previous;
|
|
211
|
+
let previousStart = 0;
|
|
212
|
+
for (const { word, time, separator, target } of sources) {
|
|
213
|
+
if (!target || target.index < (previous?.index ?? -1) || target.word.annotation?.romans.some((item) => lowerTag(item.language) === language)) return;
|
|
214
|
+
const bound = target.time.end ?? Number.POSITIVE_INFINITY;
|
|
215
|
+
const start = time ? Math.min(bound, Math.max(target.time.start, time.start)) : target.time.start;
|
|
216
|
+
const end = time?.end === void 0 ? target.time.end : Math.min(bound, Math.max(target.time.start, time.end));
|
|
217
|
+
if (recovered && start < previousStart) return;
|
|
218
|
+
previousStart = start;
|
|
219
|
+
const token = makeWordAnnotationToken({
|
|
220
|
+
text: previous ? word.text : word.text.trimStart(),
|
|
221
|
+
time: {
|
|
222
|
+
start,
|
|
223
|
+
end
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
if (previous) {
|
|
227
|
+
const last = previous.tokens.at(-1);
|
|
228
|
+
if (target.spaces > previous.spaces) {
|
|
229
|
+
const space = separator.indexOf(" ");
|
|
230
|
+
if (space === -1) return;
|
|
231
|
+
last.text += separator.slice(0, space);
|
|
232
|
+
token.text = separator.slice(space + 1) + token.text;
|
|
233
|
+
} else last.text += separator;
|
|
234
|
+
}
|
|
235
|
+
target.tokens.push(token);
|
|
236
|
+
previous = target;
|
|
237
|
+
}
|
|
238
|
+
if (!previous) return;
|
|
239
|
+
const last = previous.tokens.at(-1);
|
|
240
|
+
last.text = last.text.trimEnd();
|
|
241
|
+
const words = content.words.slice();
|
|
242
|
+
for (const { word, index, tokens } of targets) {
|
|
243
|
+
if (!tokens.length) continue;
|
|
244
|
+
const roman = makeWordAnnotationRoman({
|
|
245
|
+
language,
|
|
246
|
+
tokens
|
|
247
|
+
});
|
|
248
|
+
words[index] = {
|
|
249
|
+
...word,
|
|
250
|
+
annotation: makeWordAnnotation({
|
|
251
|
+
...word.annotation,
|
|
252
|
+
romans: [...word.annotation?.romans ?? [], roman]
|
|
253
|
+
})
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const result = {
|
|
257
|
+
...line,
|
|
258
|
+
content: {
|
|
259
|
+
...content,
|
|
260
|
+
words
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
return deriveLineRomans(result.content).find((item) => lowerTag(item.language) === language)?.text === source.text ? result : void 0;
|
|
264
|
+
};
|
|
265
|
+
var readSources = (lines, options) => {
|
|
266
|
+
const result = [];
|
|
267
|
+
const preserve = options.preservePlaceholders;
|
|
268
|
+
for (const [index, line] of lines.entries()) {
|
|
269
|
+
const start = line.time?.start;
|
|
270
|
+
const text = readText$2(line);
|
|
271
|
+
const timing = options.getTiming?.(line);
|
|
272
|
+
const precision = timing?.precision;
|
|
273
|
+
if (start !== void 0 && (preserve || text) || options.matchByOrder && text) result.push({
|
|
274
|
+
line,
|
|
275
|
+
index,
|
|
276
|
+
text,
|
|
277
|
+
start,
|
|
278
|
+
precision: precision && Number.isFinite(precision) && precision > 0 ? precision : 1,
|
|
279
|
+
rounding: timing?.rounding === 1 ? 1 : 0
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
};
|
|
284
|
+
var createTimeIndex = (lines, options) => {
|
|
285
|
+
const starts = [];
|
|
286
|
+
const lyricRows = [];
|
|
287
|
+
const targets = /* @__PURE__ */ new Map();
|
|
288
|
+
for (const [index, line] of lines.entries()) {
|
|
289
|
+
const start = line.time?.start;
|
|
290
|
+
const valid = start !== void 0 && (!options.preservePlaceholders || Number.isFinite(start));
|
|
291
|
+
starts.push(valid ? start : void 0);
|
|
292
|
+
if (options.matchByOrder && line.type === LineType.NORMAL && readText$2(line)) lyricRows.push(index);
|
|
293
|
+
if (!valid) continue;
|
|
294
|
+
const queue = targets.get(start) ?? [];
|
|
295
|
+
queue.push(index);
|
|
296
|
+
targets.set(start, queue);
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
starts,
|
|
300
|
+
lyricRows,
|
|
301
|
+
targets,
|
|
302
|
+
buckets: /* @__PURE__ */ new Map()
|
|
303
|
+
};
|
|
304
|
+
};
|
|
305
|
+
var getTimeBuckets = (index, precision) => {
|
|
306
|
+
const cached = index.buckets.get(precision);
|
|
307
|
+
if (cached) return cached;
|
|
308
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
309
|
+
for (const [target, start] of index.starts.entries()) {
|
|
310
|
+
if (start === void 0 || !Number.isFinite(start)) continue;
|
|
311
|
+
const key = Math.floor(start / precision);
|
|
312
|
+
let bucket = buckets.get(key);
|
|
313
|
+
if (!bucket) {
|
|
314
|
+
bucket = {
|
|
315
|
+
start,
|
|
316
|
+
indices: [],
|
|
317
|
+
mixed: false
|
|
318
|
+
};
|
|
319
|
+
buckets.set(key, bucket);
|
|
320
|
+
}
|
|
321
|
+
bucket.indices.push(target);
|
|
322
|
+
bucket.mixed ||= bucket.start !== start;
|
|
323
|
+
}
|
|
324
|
+
index.buckets.set(precision, buckets);
|
|
325
|
+
return buckets;
|
|
326
|
+
};
|
|
327
|
+
var matchDuplicates = (start, count, index, groups) => {
|
|
328
|
+
const result = [];
|
|
329
|
+
const collect = (precision, key, roundingOnly) => {
|
|
330
|
+
const queue = groups.get(precision)?.get(key);
|
|
331
|
+
if (!queue) return true;
|
|
332
|
+
const bucket = getTimeBuckets(index, precision).get(key);
|
|
333
|
+
if (bucket && (bucket.mixed || bucket.start !== start)) return roundingOnly && queue.every((source) => source.rounding !== 1);
|
|
334
|
+
for (const source of queue) {
|
|
335
|
+
const sourceStart = source.start;
|
|
336
|
+
const rounded = source.rounding === 1;
|
|
337
|
+
if (roundingOnly && !rounded) continue;
|
|
338
|
+
if (rounded && (sourceStart - 1 !== start && index.targets.has(sourceStart - 1) || sourceStart + 1 !== start && index.targets.has(sourceStart + 1))) return false;
|
|
339
|
+
result.push(source);
|
|
340
|
+
if (result.length > count) return false;
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
};
|
|
344
|
+
for (const precision of groups.keys()) if (!collect(precision, Math.floor(start / precision), false)) return;
|
|
345
|
+
for (const neighbor of [start - 1, start + 1]) if (neighbor !== start && !collect(1, Math.floor(neighbor), true)) return;
|
|
346
|
+
return result;
|
|
347
|
+
};
|
|
348
|
+
var matchAnnotations = (index, sources, options) => {
|
|
19
349
|
const matched = /* @__PURE__ */ new Map();
|
|
20
|
-
if (
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
for (const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
350
|
+
if (!sources.length) return matched;
|
|
351
|
+
const preserve = options.preservePlaceholders;
|
|
352
|
+
const byStart = /* @__PURE__ */ new Map();
|
|
353
|
+
for (const source of sources) {
|
|
354
|
+
const start = source.start;
|
|
355
|
+
if (start === void 0 || preserve && !Number.isFinite(start)) continue;
|
|
356
|
+
const queue = byStart.get(start) ?? [];
|
|
357
|
+
queue.push(source);
|
|
358
|
+
byStart.set(start, queue);
|
|
28
359
|
}
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
360
|
+
const usedSources = /* @__PURE__ */ new Set();
|
|
361
|
+
const ambiguous = /* @__PURE__ */ new Set();
|
|
362
|
+
const assign = (target, source) => {
|
|
363
|
+
matched.set(target, source);
|
|
364
|
+
usedSources.add(source);
|
|
365
|
+
};
|
|
366
|
+
for (const [start, queue] of index.targets) {
|
|
367
|
+
const candidates = byStart.get(start);
|
|
368
|
+
if (!candidates) continue;
|
|
369
|
+
if (queue.length > 1) {
|
|
370
|
+
if (!preserve || candidates.length === queue.length) {
|
|
371
|
+
const count = Math.min(queue.length, candidates.length);
|
|
372
|
+
for (let position = 0; position < count; position += 1) assign(queue[position], candidates[position]);
|
|
373
|
+
}
|
|
37
374
|
continue;
|
|
38
375
|
}
|
|
39
|
-
|
|
40
|
-
|
|
376
|
+
let source = candidates[0];
|
|
377
|
+
for (const candidate of candidates) if (candidate.precision < source.precision) source = candidate;
|
|
378
|
+
assign(queue[0], source);
|
|
41
379
|
}
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
380
|
+
if (usedSources.size === sources.length) return matched;
|
|
381
|
+
const readable = options.matchByOrder ? sources.filter((source) => source.text) : [];
|
|
382
|
+
if (readable.length && readable.length === index.lyricRows.length) {
|
|
383
|
+
if (readable.every((source, position) => {
|
|
384
|
+
const target = index.lyricRows[position];
|
|
385
|
+
return !matched.has(target) && !usedSources.has(source) || matched.get(target) === source;
|
|
386
|
+
})) {
|
|
387
|
+
for (const [position, source] of readable.entries()) if (!usedSources.has(source)) assign(index.lyricRows[position], source);
|
|
388
|
+
return matched;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (preserve || options.getTiming) {
|
|
392
|
+
const groups = /* @__PURE__ */ new Map();
|
|
393
|
+
for (const source of sources) {
|
|
394
|
+
const start = source.start;
|
|
395
|
+
if (usedSources.has(source) || start === void 0 || !Number.isFinite(start)) continue;
|
|
396
|
+
const precision = source.precision;
|
|
397
|
+
let group = groups.get(precision);
|
|
398
|
+
if (!group) {
|
|
399
|
+
group = /* @__PURE__ */ new Map();
|
|
400
|
+
groups.set(precision, group);
|
|
401
|
+
}
|
|
402
|
+
const key = Math.floor(start / precision);
|
|
403
|
+
const queue = group.get(key) ?? [];
|
|
404
|
+
queue.push(source);
|
|
405
|
+
group.set(key, queue);
|
|
406
|
+
}
|
|
407
|
+
for (const [start, queue] of index.targets) {
|
|
408
|
+
if (queue.length < 2 || matched.has(queue[0])) continue;
|
|
409
|
+
const candidates = matchDuplicates(start, queue.length, index, groups);
|
|
410
|
+
if (!candidates || candidates.length !== queue.length) continue;
|
|
411
|
+
candidates.sort((first, second) => first.index - second.index);
|
|
412
|
+
for (const [position, source] of candidates.entries()) assign(queue[position], source);
|
|
413
|
+
}
|
|
414
|
+
for (const precision of [...groups.keys()].sort((first, second) => first - second)) {
|
|
415
|
+
const candidates = getTimeBuckets(index, precision);
|
|
416
|
+
for (const [key, entries] of groups.get(precision)) {
|
|
417
|
+
const bucket = candidates.get(key);
|
|
418
|
+
if (!bucket) continue;
|
|
419
|
+
const sourceQueue = entries.filter((source) => !usedSources.has(source));
|
|
420
|
+
const targetQueue = bucket.indices.filter((target) => !matched.has(target));
|
|
421
|
+
if (!sourceQueue.length || !targetQueue.length) continue;
|
|
422
|
+
if ((precision > 1 || targetQueue.length > 1) && sourceQueue.length !== targetQueue.length) {
|
|
423
|
+
for (const source of sourceQueue) ambiguous.add(source);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const count = Math.min(sourceQueue.length, targetQueue.length);
|
|
427
|
+
for (let position = 0; position < count; position += 1) assign(targetQueue[position], sourceQueue[position]);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const rounding = /* @__PURE__ */ new Map();
|
|
431
|
+
const claims = /* @__PURE__ */ new Map();
|
|
432
|
+
for (const source of sources) {
|
|
433
|
+
const start = source.start;
|
|
434
|
+
if (source.rounding !== 1 || usedSources.has(source) || ambiguous.has(source) || start === void 0 || !Number.isFinite(start)) continue;
|
|
435
|
+
let target = -1;
|
|
436
|
+
let count = 0;
|
|
437
|
+
for (const neighbor of [start - 1, start + 1]) for (const candidate of index.targets.get(neighbor) ?? []) {
|
|
438
|
+
if (matched.has(candidate)) continue;
|
|
439
|
+
target = candidate;
|
|
440
|
+
count += 1;
|
|
441
|
+
claims.set(candidate, (claims.get(candidate) ?? 0) + 1);
|
|
442
|
+
}
|
|
443
|
+
if (count === 1) rounding.set(source, target);
|
|
444
|
+
else if (count > 1) ambiguous.add(source);
|
|
445
|
+
}
|
|
446
|
+
for (const [source, target] of rounding) if (claims.get(target) === 1) assign(target, source);
|
|
447
|
+
else ambiguous.add(source);
|
|
448
|
+
}
|
|
449
|
+
const tolerance = options.tolerance ?? 0;
|
|
450
|
+
if (!Number.isFinite(tolerance) || tolerance <= 0) return matched;
|
|
451
|
+
const ordered = index.ordered ??= [...index.targets.keys()].filter(Number.isFinite).sort((first, second) => first - second);
|
|
452
|
+
const cursors = /* @__PURE__ */ new Map();
|
|
453
|
+
for (const source of sources) {
|
|
454
|
+
const start = source.start;
|
|
455
|
+
if (usedSources.has(source) || ambiguous.has(source) || start === void 0 || !Number.isFinite(start)) continue;
|
|
46
456
|
let target = -1;
|
|
47
457
|
let distance = Number.POSITIVE_INFINITY;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
458
|
+
const end = start + tolerance;
|
|
459
|
+
for (let position = lowerBound(ordered, start - tolerance); position < ordered.length && ordered[position] <= end; position += 1) {
|
|
460
|
+
const targetStart = ordered[position];
|
|
461
|
+
const current = Math.abs(targetStart - start);
|
|
462
|
+
if (current > tolerance || current > distance) continue;
|
|
463
|
+
const queue = index.targets.get(targetStart);
|
|
464
|
+
let cursor = cursors.get(targetStart) ?? 0;
|
|
465
|
+
while (cursor < queue.length && matched.has(queue[cursor])) cursor += 1;
|
|
466
|
+
cursors.set(targetStart, cursor);
|
|
467
|
+
const candidate = queue[cursor];
|
|
468
|
+
if (candidate !== void 0 && (current < distance || current === distance && candidate < target)) {
|
|
469
|
+
target = candidate;
|
|
53
470
|
distance = current;
|
|
54
471
|
}
|
|
55
472
|
}
|
|
56
|
-
if (target !== -1)
|
|
57
|
-
matched.set(target, annotation);
|
|
58
|
-
used.add(target);
|
|
59
|
-
}
|
|
473
|
+
if (target !== -1) assign(target, source);
|
|
60
474
|
}
|
|
61
475
|
return matched;
|
|
62
476
|
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
477
|
+
var attachTracks = (lines, sources, options, wordTolerance) => {
|
|
478
|
+
if (!lines.length || !sources.translations?.length && !sources.romans?.length) return lines;
|
|
479
|
+
const index = createTimeIndex(lines, options);
|
|
480
|
+
let result = lines;
|
|
481
|
+
for (const kind of ["translations", "romans"]) {
|
|
482
|
+
const matches = matchAnnotations(index, readSources(sources[kind] ?? [], options), options);
|
|
483
|
+
for (const [target, source] of matches) {
|
|
484
|
+
const line = result[target];
|
|
485
|
+
if (line.type === LineType.INSTRUMENTAL || !source.text) continue;
|
|
486
|
+
const romanLanguage = kind === "romans" && wordTolerance !== void 0 ? readRomanLanguage(source.line) : void 0;
|
|
487
|
+
let annotated = romanLanguage?.consistent && wordTolerance !== void 0 ? attachWordRomans(line, source, wordTolerance, romanLanguage.language, options.wordFallback === true) : void 0;
|
|
488
|
+
if (!annotated) {
|
|
489
|
+
const language = romanLanguage ? romanLanguage.language : source.line.languages[0];
|
|
490
|
+
const create = kind === "romans" ? makeLineAnnotationRoman : makeLineAnnotationTranslation;
|
|
491
|
+
const annotation = makeLineAnnotation({
|
|
492
|
+
...line.annotation,
|
|
493
|
+
[kind]: [...line.annotation?.[kind] ?? [], create({
|
|
494
|
+
text: source.text,
|
|
495
|
+
language
|
|
496
|
+
})]
|
|
497
|
+
});
|
|
498
|
+
annotated = {
|
|
499
|
+
...line,
|
|
500
|
+
annotation
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
if (result === lines) result = lines.slice();
|
|
504
|
+
result[target] = annotated;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return result;
|
|
92
508
|
};
|
|
93
|
-
//#endregion
|
|
94
|
-
//#region src/utils/language.ts
|
|
95
509
|
/**
|
|
96
|
-
*
|
|
510
|
+
* Attaches complete roman tracks to words using timing and optional local recovery, retaining the source line when matching is incomplete.
|
|
511
|
+
* Translations remain line-level, and successful word tracks are not duplicated on the line.
|
|
97
512
|
*
|
|
98
|
-
*
|
|
99
|
-
* Comparing tags stays with the consumer, since BCP 47 matches by subtag prefix rather than by string equality.
|
|
513
|
+
* Match before final normalization; original text and word timing are preserved.
|
|
100
514
|
*/
|
|
101
|
-
var
|
|
102
|
-
return
|
|
515
|
+
var attachAnnotations = (lines, sources, options = {}) => {
|
|
516
|
+
return attachTracks(lines, sources, options, Number.isFinite(options.wordTolerance) ? Math.max(0, options.wordTolerance ?? 0) : 0);
|
|
103
517
|
};
|
|
104
518
|
//#endregion
|
|
105
519
|
//#region src/utils/time.ts
|
|
106
|
-
var CLOCK_PATTERN = /^(?:(?:(\d+):)?(\d+):)?(\d+)(?:\.(\d+))?$/;
|
|
520
|
+
var CLOCK_PATTERN$1 = /^(?:(?:(\d+):)?(\d+):)?(\d+)(?:\.(\d+))?$/;
|
|
107
521
|
var OFFSET_PATTERN = /^(\d+(?:\.\d+)?)(ms|h|m|s)$/iu;
|
|
108
522
|
/**
|
|
109
523
|
* Reads a clock value, where the fraction is padded or truncated to millisecond precision.
|
|
@@ -112,7 +526,7 @@ var OFFSET_PATTERN = /^(\d+(?:\.\d+)?)(ms|h|m|s)$/iu;
|
|
|
112
526
|
*/
|
|
113
527
|
var parseClock = (value) => {
|
|
114
528
|
if (!value.includes(":") && !value.includes(".")) return;
|
|
115
|
-
const match = CLOCK_PATTERN.exec(value);
|
|
529
|
+
const match = CLOCK_PATTERN$1.exec(value);
|
|
116
530
|
if (!match) return;
|
|
117
531
|
const hour = Number(match[1] ?? 0);
|
|
118
532
|
const minute = Number(match[2] ?? 0);
|
|
@@ -194,7 +608,7 @@ var prefixedNames = /* @__PURE__ */ new Map();
|
|
|
194
608
|
*/
|
|
195
609
|
var splitXmlName = (name) => {
|
|
196
610
|
const cached = prefixedNames.get(name);
|
|
197
|
-
if (cached
|
|
611
|
+
if (cached) return cached;
|
|
198
612
|
const colon = name.indexOf(":");
|
|
199
613
|
const split = colon === -1 ? {
|
|
200
614
|
namespacePrefix: "",
|
|
@@ -203,7 +617,7 @@ var splitXmlName = (name) => {
|
|
|
203
617
|
namespacePrefix: name.slice(0, colon),
|
|
204
618
|
localName: name.slice(colon + 1)
|
|
205
619
|
};
|
|
206
|
-
if (split.namespacePrefix
|
|
620
|
+
if (split.namespacePrefix && prefixedNames.size < 1024) prefixedNames.set(name, split);
|
|
207
621
|
return split;
|
|
208
622
|
};
|
|
209
623
|
/**
|
|
@@ -265,7 +679,7 @@ var getXmlAttribute = (element, localName) => {
|
|
|
265
679
|
const attributes = element.attributes;
|
|
266
680
|
for (let index = 0; index < attributes.length; index += 1) {
|
|
267
681
|
const attribute = attributes[index];
|
|
268
|
-
if (attribute
|
|
682
|
+
if (attribute && attribute.localName === localName) return attribute.value;
|
|
269
683
|
}
|
|
270
684
|
};
|
|
271
685
|
/**
|
|
@@ -275,7 +689,7 @@ var getXmlQualifiedAttribute = (element, qualifiedName) => {
|
|
|
275
689
|
const attributes = element.attributes;
|
|
276
690
|
for (let index = 0; index < attributes.length; index += 1) {
|
|
277
691
|
const attribute = attributes[index];
|
|
278
|
-
if (attribute
|
|
692
|
+
if (attribute && attribute.qualifiedName === qualifiedName) return attribute.value;
|
|
279
693
|
}
|
|
280
694
|
};
|
|
281
695
|
/**
|
|
@@ -286,7 +700,7 @@ var getXmlChildElements = (element, localName) => {
|
|
|
286
700
|
const children = element.children;
|
|
287
701
|
for (let index = 0; index < children.length; index += 1) {
|
|
288
702
|
const child = children[index];
|
|
289
|
-
if (child
|
|
703
|
+
if (child && child.nodeType === XmlNodeType.element && child.localName === localName) result.push(child);
|
|
290
704
|
}
|
|
291
705
|
return result;
|
|
292
706
|
};
|
|
@@ -297,7 +711,7 @@ var hasXmlChildElement = (element, localName) => {
|
|
|
297
711
|
const children = element.children;
|
|
298
712
|
for (let index = 0; index < children.length; index += 1) {
|
|
299
713
|
const child = children[index];
|
|
300
|
-
if (child
|
|
714
|
+
if (child && child.nodeType === XmlNodeType.element && child.localName === localName) return true;
|
|
301
715
|
}
|
|
302
716
|
return false;
|
|
303
717
|
};
|
|
@@ -306,13 +720,13 @@ var hasXmlChildElement = (element, localName) => {
|
|
|
306
720
|
*/
|
|
307
721
|
var findXmlElement = (element, localName) => {
|
|
308
722
|
const stack = [element];
|
|
309
|
-
while (stack.length
|
|
723
|
+
while (stack.length) {
|
|
310
724
|
const node = stack.pop();
|
|
311
|
-
if (node
|
|
725
|
+
if (!node || node.nodeType !== XmlNodeType.element) continue;
|
|
312
726
|
if (node.localName === localName) return node;
|
|
313
727
|
for (let index = node.children.length - 1; index >= 0; index -= 1) {
|
|
314
728
|
const child = node.children[index];
|
|
315
|
-
if (child
|
|
729
|
+
if (child) stack.push(child);
|
|
316
730
|
}
|
|
317
731
|
}
|
|
318
732
|
};
|
|
@@ -321,18 +735,18 @@ var findXmlElement = (element, localName) => {
|
|
|
321
735
|
*/
|
|
322
736
|
var getXmlTextContent = (node) => {
|
|
323
737
|
if (node.nodeType !== XmlNodeType.element) return node.text;
|
|
324
|
-
if (node.children.length
|
|
738
|
+
if (!node.children.length) return "";
|
|
325
739
|
const child = node.children[0];
|
|
326
|
-
if (node.children.length === 1 && child
|
|
740
|
+
if (node.children.length === 1 && child && child.nodeType !== XmlNodeType.element) return child.text;
|
|
327
741
|
const parts = [];
|
|
328
742
|
const stack = [node];
|
|
329
|
-
while (stack.length
|
|
743
|
+
while (stack.length) {
|
|
330
744
|
const current = stack.pop();
|
|
331
|
-
if (current
|
|
745
|
+
if (!current) continue;
|
|
332
746
|
if (current.nodeType === XmlNodeType.element) {
|
|
333
747
|
for (let index = current.children.length - 1; index >= 0; index -= 1) {
|
|
334
748
|
const child = current.children[index];
|
|
335
|
-
if (child
|
|
749
|
+
if (child) stack.push(child);
|
|
336
750
|
}
|
|
337
751
|
continue;
|
|
338
752
|
}
|
|
@@ -400,7 +814,7 @@ var decodeText = (text) => {
|
|
|
400
814
|
cursor = end + 1;
|
|
401
815
|
start = cursor;
|
|
402
816
|
}
|
|
403
|
-
if (parts.length
|
|
817
|
+
if (!parts.length) return text;
|
|
404
818
|
parts.push(text.slice(start));
|
|
405
819
|
return parts.join("");
|
|
406
820
|
};
|
|
@@ -409,7 +823,7 @@ var decodeText = (text) => {
|
|
|
409
823
|
*/
|
|
410
824
|
var parseXml = (source) => {
|
|
411
825
|
const length = source.length;
|
|
412
|
-
let position = length
|
|
826
|
+
let position = length && source.charCodeAt(0) === BYTE_ORDER_MARK ? 1 : 0;
|
|
413
827
|
let valid = true;
|
|
414
828
|
const stack = [];
|
|
415
829
|
const root = {
|
|
@@ -429,7 +843,7 @@ var parseXml = (source) => {
|
|
|
429
843
|
position = end;
|
|
430
844
|
if (start === end) return;
|
|
431
845
|
const parent = stack[stack.length - 1];
|
|
432
|
-
if (parent
|
|
846
|
+
if (!parent) {
|
|
433
847
|
valid = false;
|
|
434
848
|
return;
|
|
435
849
|
}
|
|
@@ -544,7 +958,7 @@ var parseXml = (source) => {
|
|
|
544
958
|
}
|
|
545
959
|
position = cursor;
|
|
546
960
|
const parent = stack[stack.length - 1];
|
|
547
|
-
if (parent
|
|
961
|
+
if (!parent) {
|
|
548
962
|
valid = false;
|
|
549
963
|
return;
|
|
550
964
|
}
|
|
@@ -574,7 +988,7 @@ var parseXml = (source) => {
|
|
|
574
988
|
return;
|
|
575
989
|
}
|
|
576
990
|
const top = stack[stack.length - 1];
|
|
577
|
-
if (top
|
|
991
|
+
if (!top || top.qualifiedName !== name) {
|
|
578
992
|
valid = false;
|
|
579
993
|
return;
|
|
580
994
|
}
|
|
@@ -601,7 +1015,7 @@ var parseXml = (source) => {
|
|
|
601
1015
|
return;
|
|
602
1016
|
}
|
|
603
1017
|
const parent = stack[stack.length - 1];
|
|
604
|
-
if (parent
|
|
1018
|
+
if (!parent) {
|
|
605
1019
|
valid = false;
|
|
606
1020
|
return;
|
|
607
1021
|
}
|
|
@@ -706,7 +1120,7 @@ var escapeXmlText = (text, chunks) => {
|
|
|
706
1120
|
}
|
|
707
1121
|
} else if (code >= 56320 && code <= 57343) escaped = REPLACEMENT_CHARACTER;
|
|
708
1122
|
else if (!isAllowedCodePoint(code)) escaped = REPLACEMENT_CHARACTER;
|
|
709
|
-
if (escaped
|
|
1123
|
+
if (!escaped) continue;
|
|
710
1124
|
if (start < index) chunks.push(text.slice(start, index));
|
|
711
1125
|
chunks.push(escaped);
|
|
712
1126
|
start = index + 1;
|
|
@@ -742,7 +1156,7 @@ var escapeXmlAttribute = (value, chunks) => {
|
|
|
742
1156
|
}
|
|
743
1157
|
} else if (code >= 56320 && code <= 57343) escaped = REPLACEMENT_CHARACTER;
|
|
744
1158
|
else if (!isAllowedCodePoint(code)) escaped = REPLACEMENT_CHARACTER;
|
|
745
|
-
if (escaped
|
|
1159
|
+
if (!escaped) continue;
|
|
746
1160
|
if (start < index) chunks.push(value.slice(start, index));
|
|
747
1161
|
chunks.push(escaped);
|
|
748
1162
|
start = index + 1;
|
|
@@ -762,7 +1176,7 @@ var hasOnlyElements = (element) => {
|
|
|
762
1176
|
const children = element.children;
|
|
763
1177
|
for (let index = 0; index < children.length; index += 1) {
|
|
764
1178
|
const child = children[index];
|
|
765
|
-
if (child
|
|
1179
|
+
if (child && child.nodeType !== XmlNodeType.element) return false;
|
|
766
1180
|
}
|
|
767
1181
|
return true;
|
|
768
1182
|
};
|
|
@@ -782,13 +1196,13 @@ var writeXmlNode = (node, chunks, depth, indent) => {
|
|
|
782
1196
|
chunks.push("<", node.qualifiedName);
|
|
783
1197
|
for (let index = 0; index < node.attributes.length; index += 1) {
|
|
784
1198
|
const attribute = node.attributes[index];
|
|
785
|
-
if (attribute
|
|
1199
|
+
if (!attribute) continue;
|
|
786
1200
|
chunks.push(" ", attribute.qualifiedName, "=\"");
|
|
787
1201
|
escapeXmlAttribute(attribute.value, chunks);
|
|
788
1202
|
chunks.push("\"");
|
|
789
1203
|
}
|
|
790
1204
|
const children = node.children;
|
|
791
|
-
if (children.length
|
|
1205
|
+
if (!children.length) {
|
|
792
1206
|
chunks.push("/>");
|
|
793
1207
|
return;
|
|
794
1208
|
}
|
|
@@ -796,7 +1210,7 @@ var writeXmlNode = (node, chunks, depth, indent) => {
|
|
|
796
1210
|
const childIndent = indent !== void 0 && hasOnlyElements(node) ? indent : void 0;
|
|
797
1211
|
for (let index = 0; index < children.length; index += 1) {
|
|
798
1212
|
const child = children[index];
|
|
799
|
-
if (child
|
|
1213
|
+
if (child) writeXmlNode(child, chunks, depth + 1, childIndent);
|
|
800
1214
|
}
|
|
801
1215
|
if (childIndent !== void 0) chunks.push("\n", childIndent.repeat(depth));
|
|
802
1216
|
chunks.push("</", node.qualifiedName, ">");
|
|
@@ -809,20 +1223,23 @@ var writeXmlNode = (node, chunks, depth, indent) => {
|
|
|
809
1223
|
var serializeXml = (root, options = {}) => {
|
|
810
1224
|
const chunks = [];
|
|
811
1225
|
if (options.declaration ?? true) chunks.push(XML_DECLARATION);
|
|
812
|
-
const indent = options.indent
|
|
1226
|
+
const indent = options.indent ? options.indentText ?? DEFAULT_INDENT : void 0;
|
|
813
1227
|
if (indent !== void 0) chunks.push("\n");
|
|
814
1228
|
if (root.qualifiedName === ROOT_NAME) for (let index = 0; index < root.children.length; index += 1) {
|
|
815
1229
|
const child = root.children[index];
|
|
816
|
-
if (child
|
|
1230
|
+
if (child) writeXmlNode(child, chunks, 0, indent);
|
|
817
1231
|
}
|
|
818
1232
|
else writeXmlNode(root, chunks, 0, indent);
|
|
819
1233
|
return chunks.join("");
|
|
820
1234
|
};
|
|
821
1235
|
//#endregion
|
|
822
1236
|
//#region src/formats/lrc/common.ts
|
|
1237
|
+
/**
|
|
1238
|
+
* Accepts plain lyrics or separate tracks while rejecting an unreadable original payload.
|
|
1239
|
+
*/
|
|
823
1240
|
var readLrcContent = (input) => {
|
|
824
1241
|
if (typeof input === "string") return { original: input };
|
|
825
|
-
if (typeof input !== "object"
|
|
1242
|
+
if (!input || typeof input !== "object") return null;
|
|
826
1243
|
const source = input;
|
|
827
1244
|
if (typeof source.original !== "string") return null;
|
|
828
1245
|
return {
|
|
@@ -837,7 +1254,7 @@ var WORD_OFFSET_PATTERN = /^(\d+)(?:,(\d+))?$/;
|
|
|
837
1254
|
var WORD_CLOCK_DURATION_PATTERN = /^([^,]+),(\d+)$/;
|
|
838
1255
|
var WORD_TAG_PATTERN = /<([^>]*)>/g;
|
|
839
1256
|
var TAG_PATTERN = /\[([^\]]*)\]/g;
|
|
840
|
-
var META_PATTERN = /^([A-Za-z0-9_-]+)\s*:\s*([\s\S]*)$/;
|
|
1257
|
+
var META_PATTERN$1 = /^([A-Za-z0-9_-]+)\s*:\s*([\s\S]*)$/;
|
|
841
1258
|
/**
|
|
842
1259
|
* Integer tags use `offset[,duration]` in milliseconds relative to the line start.
|
|
843
1260
|
* Clock tags are absolute and may append a duration in milliseconds, as in `<00:01.50,500>`.
|
|
@@ -871,21 +1288,23 @@ var parseWordTime = (body, lineStart) => {
|
|
|
871
1288
|
}
|
|
872
1289
|
};
|
|
873
1290
|
/**
|
|
874
|
-
* Untagged leading text is preserved;
|
|
875
|
-
*
|
|
1291
|
+
* Untagged leading text is preserved; empty timed pieces retain explicit segment boundaries.
|
|
1292
|
+
* Only recognized time tags split content; other angle-bracket text remains literal.
|
|
876
1293
|
*/
|
|
877
1294
|
var splitWordTags = (content) => {
|
|
878
1295
|
const pieces = [];
|
|
879
1296
|
let tag;
|
|
880
1297
|
let cursor = 0;
|
|
881
1298
|
for (const match of content.matchAll(WORD_TAG_PATTERN)) {
|
|
1299
|
+
const nextTag = match[1] ?? "";
|
|
1300
|
+
if (!parseWordTime(nextTag, 0)) continue;
|
|
882
1301
|
const at = match.index;
|
|
883
1302
|
const before = content.slice(cursor, at);
|
|
884
|
-
if (before) pieces.push({
|
|
1303
|
+
if (before || tag) pieces.push({
|
|
885
1304
|
text: before,
|
|
886
1305
|
tag
|
|
887
1306
|
});
|
|
888
|
-
tag =
|
|
1307
|
+
tag = nextTag;
|
|
889
1308
|
cursor = at + match[0].length;
|
|
890
1309
|
}
|
|
891
1310
|
const tail = content.slice(cursor);
|
|
@@ -893,7 +1312,7 @@ var splitWordTags = (content) => {
|
|
|
893
1312
|
text: tail,
|
|
894
1313
|
tag
|
|
895
1314
|
});
|
|
896
|
-
else if (tag
|
|
1315
|
+
else if (tag) pieces.push({
|
|
897
1316
|
text: "",
|
|
898
1317
|
tag
|
|
899
1318
|
});
|
|
@@ -932,25 +1351,48 @@ var renderWordTag = (start, end, options) => {
|
|
|
932
1351
|
return `${open}${formatTime(start, options.timeFormat)}${duration}${close}`;
|
|
933
1352
|
};
|
|
934
1353
|
/**
|
|
935
|
-
* Plain text joins words as written, including the separators stored as space words.
|
|
936
|
-
*/
|
|
937
|
-
var renderPlainText = (line) => {
|
|
938
|
-
const content = line.content;
|
|
939
|
-
if (!content) return "";
|
|
940
|
-
if (content.text !== void 0) return content.text;
|
|
941
|
-
return content.words.map((word) => word.text).join("");
|
|
942
|
-
};
|
|
943
|
-
/**
|
|
944
1354
|
* Word-level lines interleave tags with their text; untimed words stay untagged.
|
|
945
1355
|
*/
|
|
946
1356
|
var renderSyllableText = (line, options) => {
|
|
947
1357
|
const content = line.content;
|
|
948
|
-
if (!content?.words.length) return
|
|
1358
|
+
if (!content?.words.length) return getContentText(content);
|
|
949
1359
|
return content.words.map((word) => {
|
|
950
1360
|
if (word.type === WordType.SPACE) return word.text;
|
|
951
1361
|
const start = word.time?.start;
|
|
952
1362
|
if (start === void 0) return word.text;
|
|
953
|
-
|
|
1363
|
+
const end = word.time?.end;
|
|
1364
|
+
const closing = options.wordTiming === "angle" && options.includeWordEnd && end === start ? renderWordTag(end, void 0, options) : "";
|
|
1365
|
+
return `${renderWordTag(start, end, options)}${word.text}${closing}`;
|
|
1366
|
+
}).join("");
|
|
1367
|
+
};
|
|
1368
|
+
var renderWordRomans = (line, language, options) => {
|
|
1369
|
+
const pieces = [];
|
|
1370
|
+
let spaced = false;
|
|
1371
|
+
for (const word of line.content?.words ?? []) {
|
|
1372
|
+
if (word.type === WordType.SPACE) {
|
|
1373
|
+
spaced = true;
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
const roman = word.annotation?.romans.find((item) => lowerTag(item.language) === language);
|
|
1377
|
+
if (!roman || !getAnnotationItemText(roman)) continue;
|
|
1378
|
+
let prefix = pieces.length && spaced ? " " : "";
|
|
1379
|
+
spaced = false;
|
|
1380
|
+
for (const token of roman.tokens) {
|
|
1381
|
+
if (!token.text) continue;
|
|
1382
|
+
pieces.push({
|
|
1383
|
+
text: prefix + token.text,
|
|
1384
|
+
time: resolveAnnotationTokenTime(token, roman, word)
|
|
1385
|
+
});
|
|
1386
|
+
prefix = "";
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return pieces.map((piece, index) => {
|
|
1390
|
+
const start = piece.time?.start;
|
|
1391
|
+
if (start === void 0) return piece.text;
|
|
1392
|
+
const end = piece.time?.end;
|
|
1393
|
+
const nextStart = pieces[index + 1]?.time?.start;
|
|
1394
|
+
const closing = end !== void 0 && end !== nextStart && (!options.includeWordEnd || end === start) ? renderWordTag(end, void 0, options) : "";
|
|
1395
|
+
return `${renderWordTag(start, end, options)}${piece.text}${closing}`;
|
|
954
1396
|
}).join("");
|
|
955
1397
|
};
|
|
956
1398
|
var renderMetaRows = (meta, options) => {
|
|
@@ -972,7 +1414,7 @@ var renderLineRow = (line, options, wordLevel) => {
|
|
|
972
1414
|
if (line.type === LineType.INSTRUMENTAL && !options.includeInstrumental) return;
|
|
973
1415
|
const time = renderTimeTag(line.time?.start ?? 0, options);
|
|
974
1416
|
if (line.type === LineType.INSTRUMENTAL) return time;
|
|
975
|
-
return `${time}${wordLevel && line.content?.words.length ? renderSyllableText(line, options) :
|
|
1417
|
+
return `${time}${wordLevel && line.content?.words.length ? renderSyllableText(line, options) : getContentText(line.content)}`;
|
|
976
1418
|
};
|
|
977
1419
|
var renderAnnotationRows = (lines, options, pick) => {
|
|
978
1420
|
const rows = [];
|
|
@@ -983,6 +1425,20 @@ var renderAnnotationRows = (lines, options, pick) => {
|
|
|
983
1425
|
}
|
|
984
1426
|
return rows;
|
|
985
1427
|
};
|
|
1428
|
+
var renderRomanRows = (lines, options) => {
|
|
1429
|
+
const rows = [];
|
|
1430
|
+
for (const line of lines) {
|
|
1431
|
+
const start = line.time?.start;
|
|
1432
|
+
if (start === void 0) continue;
|
|
1433
|
+
const explicit = new Set((line.annotation?.romans ?? []).map((item) => lowerTag(item.language)));
|
|
1434
|
+
for (const item of getLineRomans(line)) {
|
|
1435
|
+
const language = lowerTag(item.language);
|
|
1436
|
+
const text = options.wordTiming === "none" || explicit.has(language) ? item.text : renderWordRomans(line, language, options);
|
|
1437
|
+
rows.push(`${renderTimeTag(start, options)}${text}`);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return rows;
|
|
1441
|
+
};
|
|
986
1442
|
var joinBlocks = (header, body) => {
|
|
987
1443
|
const blocks = [];
|
|
988
1444
|
if (header.length) blocks.push(header.join("\n"));
|
|
@@ -993,17 +1449,18 @@ var joinBlocks = (header, body) => {
|
|
|
993
1449
|
* Generates the LRC text plus the annotations a single file cannot hold.
|
|
994
1450
|
*
|
|
995
1451
|
* Word tags appear only for a word-timed lyric when `wordTiming` is not `none`; otherwise every line stays whole.
|
|
1452
|
+
* Word romanizations retain token boundaries; explicit line romanizations take precedence for the same language.
|
|
996
1453
|
*/
|
|
997
1454
|
var generateLrc = (lyric, options = {}) => {
|
|
998
1455
|
const resolved = resolveOptions$1(options);
|
|
999
1456
|
const header = resolved.includeMeta && lyric.meta ? renderMetaRows(lyric.meta, resolved) : [];
|
|
1000
|
-
const wordLevel = lyric.lines.some((line) =>
|
|
1457
|
+
const wordLevel = lyric.lines.some((line) => !!line.content?.words.length) && resolved.wordTiming !== "none";
|
|
1001
1458
|
const result = { original: joinBlocks(header, lyric.lines.map((line) => {
|
|
1002
1459
|
return renderLineRow(line, resolved, wordLevel);
|
|
1003
1460
|
}).filter((row) => row !== void 0)) };
|
|
1004
1461
|
const translations = renderAnnotationRows(lyric.lines, resolved, (line) => line.annotation?.translations ?? []);
|
|
1005
1462
|
if (translations.length) result.translations = joinBlocks(header, translations);
|
|
1006
|
-
const romans =
|
|
1463
|
+
const romans = renderRomanRows(lyric.lines, resolved);
|
|
1007
1464
|
if (romans.length) result.romans = joinBlocks(header, romans);
|
|
1008
1465
|
return result;
|
|
1009
1466
|
};
|
|
@@ -1022,19 +1479,14 @@ var finalizeLyric = (input) => {
|
|
|
1022
1479
|
};
|
|
1023
1480
|
};
|
|
1024
1481
|
//#endregion
|
|
1025
|
-
//#region src/formats/lrc/
|
|
1026
|
-
var
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
var isWordTimed = (text) => {
|
|
1034
|
-
return splitWordTags(text).some((piece) => piece.tag !== void 0 && parseWordTime(piece.tag, 0) !== void 0);
|
|
1035
|
-
};
|
|
1036
|
-
var readMeta = (body) => {
|
|
1037
|
-
const match = META_PATTERN.exec(body);
|
|
1482
|
+
//#region src/formats/lrc/reader.ts
|
|
1483
|
+
var CLOCK_PATTERN = /^(?:\d+:){0,2}\d+(?:\.(\d+))?$/;
|
|
1484
|
+
var readLineTiming = (tag) => {
|
|
1485
|
+
const clock = CLOCK_PATTERN.exec(tag.trim());
|
|
1486
|
+
return { precision: clock ? 10 ** Math.max(0, 3 - (clock[1]?.length ?? 0)) : 1 };
|
|
1487
|
+
};
|
|
1488
|
+
var readMeta$1 = (body) => {
|
|
1489
|
+
const match = META_PATTERN$1.exec(body);
|
|
1038
1490
|
if (!match) return;
|
|
1039
1491
|
return {
|
|
1040
1492
|
key: (match[1] ?? "").toLowerCase(),
|
|
@@ -1047,11 +1499,12 @@ var readGroup = (tags, text, timed, meta) => {
|
|
|
1047
1499
|
if (start !== void 0) {
|
|
1048
1500
|
timed.push({
|
|
1049
1501
|
text,
|
|
1050
|
-
start
|
|
1502
|
+
start,
|
|
1503
|
+
tag
|
|
1051
1504
|
});
|
|
1052
1505
|
continue;
|
|
1053
1506
|
}
|
|
1054
|
-
const pair = readMeta(tag);
|
|
1507
|
+
const pair = readMeta$1(tag);
|
|
1055
1508
|
if (pair) meta.push(pair);
|
|
1056
1509
|
}
|
|
1057
1510
|
};
|
|
@@ -1084,57 +1537,11 @@ var readTimedLines = (text) => {
|
|
|
1084
1537
|
};
|
|
1085
1538
|
};
|
|
1086
1539
|
/**
|
|
1087
|
-
* Unmapped tags and unparseable durations remain in `unknowns` to avoid discarding metadata.
|
|
1088
|
-
* Keys are lowercased and values trimmed, so original tag spelling is not preserved.
|
|
1089
|
-
*/
|
|
1090
|
-
var readMetaTags = (tags) => {
|
|
1091
|
-
const meta = makeMeta();
|
|
1092
|
-
for (const tag of tags) switch (tag.key) {
|
|
1093
|
-
case "length":
|
|
1094
|
-
case "duration": {
|
|
1095
|
-
const duration = parseTime(tag.value) ?? (isBareNumber(tag.value) ? Number(tag.value) : NaN);
|
|
1096
|
-
if (Number.isFinite(duration)) meta.duration = Math.trunc(duration);
|
|
1097
|
-
else meta.unknowns.push(makeUnknownEntry({
|
|
1098
|
-
key: tag.key,
|
|
1099
|
-
value: tag.value
|
|
1100
|
-
}));
|
|
1101
|
-
break;
|
|
1102
|
-
}
|
|
1103
|
-
case "offset":
|
|
1104
|
-
meta.offset = Math.trunc(Number(tag.value)) || 0;
|
|
1105
|
-
break;
|
|
1106
|
-
case "ti":
|
|
1107
|
-
case "title":
|
|
1108
|
-
meta.titles.push(makeText({ text: tag.value }));
|
|
1109
|
-
break;
|
|
1110
|
-
case "ar":
|
|
1111
|
-
case "artist":
|
|
1112
|
-
meta.artists.push(makeText({ text: tag.value }));
|
|
1113
|
-
break;
|
|
1114
|
-
case "al":
|
|
1115
|
-
case "album":
|
|
1116
|
-
meta.albums.push(makeText({ text: tag.value }));
|
|
1117
|
-
break;
|
|
1118
|
-
case "by":
|
|
1119
|
-
case "author":
|
|
1120
|
-
meta.authors.push(makeText({ text: tag.value }));
|
|
1121
|
-
break;
|
|
1122
|
-
case "isrc":
|
|
1123
|
-
meta.isrcs.push(tag.value);
|
|
1124
|
-
break;
|
|
1125
|
-
default: meta.unknowns.push(makeUnknownEntry({
|
|
1126
|
-
key: tag.key,
|
|
1127
|
-
value: tag.value
|
|
1128
|
-
}));
|
|
1129
|
-
}
|
|
1130
|
-
return meta;
|
|
1131
|
-
};
|
|
1132
|
-
/**
|
|
1133
1540
|
* Whitespace becomes a space word preserving the source run, as in `lo World`.
|
|
1134
1541
|
* A tag opens a segment; untagged words continue from the previous end so the timeline has no holes.
|
|
1135
1542
|
* Explicit word ends are kept as written, so duration tags survive unchanged.
|
|
1136
1543
|
*/
|
|
1137
|
-
var readWords$
|
|
1544
|
+
var readWords$2 = (pieces, lineStart) => {
|
|
1138
1545
|
const drafts = [];
|
|
1139
1546
|
const open = /* @__PURE__ */ new Set();
|
|
1140
1547
|
const closeSegment = (boundary) => {
|
|
@@ -1144,9 +1551,9 @@ var readWords$1 = (text, lineStart) => {
|
|
|
1144
1551
|
};
|
|
1145
1552
|
let segmentStart = lineStart;
|
|
1146
1553
|
let cursor = lineStart;
|
|
1147
|
-
for (const piece of
|
|
1148
|
-
const time = piece.tag
|
|
1149
|
-
if (time
|
|
1554
|
+
for (const piece of pieces) {
|
|
1555
|
+
const time = piece.tag ? parseWordTime(piece.tag, lineStart) : void 0;
|
|
1556
|
+
if (time) {
|
|
1150
1557
|
closeSegment(time.start);
|
|
1151
1558
|
segmentStart = time.start;
|
|
1152
1559
|
cursor = time.start;
|
|
@@ -1162,7 +1569,7 @@ var readWords$1 = (text, lineStart) => {
|
|
|
1162
1569
|
});
|
|
1163
1570
|
continue;
|
|
1164
1571
|
}
|
|
1165
|
-
const tagged = !applied && time
|
|
1572
|
+
const tagged = !applied && !!time;
|
|
1166
1573
|
applied = true;
|
|
1167
1574
|
const start = tagged ? segmentStart : cursor;
|
|
1168
1575
|
const end = tagged ? time?.end : void 0;
|
|
@@ -1182,23 +1589,26 @@ var readWords$1 = (text, lineStart) => {
|
|
|
1182
1589
|
};
|
|
1183
1590
|
};
|
|
1184
1591
|
/**
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1187
|
-
* Empty timed rows represent instrumental time rather than sung content.
|
|
1592
|
+
* Explicit word ranges can extend past the next line's start, so the inferred end covers both.
|
|
1593
|
+
* A trailing open segment uses that end when it is known.
|
|
1188
1594
|
*/
|
|
1189
1595
|
var readLine$1 = (text, start, nextStart) => {
|
|
1190
1596
|
const trimmed = text.trim();
|
|
1191
1597
|
let end = nextStart !== void 0 && nextStart >= start ? nextStart : void 0;
|
|
1192
|
-
if (trimmed
|
|
1598
|
+
if (!trimmed) return makeLineInstrumental({ time: makeTimeRange({
|
|
1193
1599
|
start,
|
|
1194
1600
|
end
|
|
1195
1601
|
}) });
|
|
1196
|
-
const
|
|
1197
|
-
const
|
|
1602
|
+
const pieces = splitWordTags(text);
|
|
1603
|
+
const wordTimed = pieces.some((piece) => !!piece.tag);
|
|
1604
|
+
const { drafts, closeSegment } = wordTimed ? readWords$2(pieces, start) : {
|
|
1198
1605
|
drafts: [],
|
|
1199
1606
|
closeSegment: () => {}
|
|
1200
1607
|
};
|
|
1201
1608
|
if (end === void 0) end = [...drafts].reverse().find((draft) => !draft.space && draft.start !== void 0)?.end;
|
|
1609
|
+
if (end !== void 0) {
|
|
1610
|
+
for (const draft of drafts) if (draft.end !== void 0) end = Math.max(end, draft.end);
|
|
1611
|
+
}
|
|
1202
1612
|
closeSegment(end);
|
|
1203
1613
|
const words = drafts.map((draft) => {
|
|
1204
1614
|
if (draft.space) return makeWordSpace({ text: draft.text });
|
|
@@ -1225,29 +1635,97 @@ var readLine$1 = (text, start, nextStart) => {
|
|
|
1225
1635
|
* Builds one line per timed row, so an annotation track reuses the original text's reading unchanged.
|
|
1226
1636
|
* Empty timed rows become instrumental lines bounded by the next timestamp when one is usable.
|
|
1227
1637
|
*/
|
|
1228
|
-
var readLines = (timed) => {
|
|
1638
|
+
var readLines$1 = (timed, options) => {
|
|
1229
1639
|
const lines = [];
|
|
1230
1640
|
for (const [index, item] of timed.entries()) {
|
|
1231
1641
|
const next = timed[index + 1]?.start;
|
|
1232
|
-
|
|
1642
|
+
const line = readLine$1(item.text, item.start, next);
|
|
1643
|
+
lines.push(line);
|
|
1644
|
+
options.onLine?.(line, readLineTiming(item.tag));
|
|
1233
1645
|
}
|
|
1234
1646
|
return lines;
|
|
1235
1647
|
};
|
|
1236
|
-
|
|
1648
|
+
/**
|
|
1649
|
+
* Reads one LRC track before normalization so consumers retain source order and timing slots.
|
|
1650
|
+
* The caller owns metadata mapping, annotation attachment, and final normalization.
|
|
1651
|
+
*
|
|
1652
|
+
* @param text Original lyrics or one auxiliary track in LRC syntax.
|
|
1653
|
+
* @param options Optional observer for source timestamp information.
|
|
1654
|
+
*
|
|
1655
|
+
* @returns Model lines in input order and metadata tags with lowercase keys and trimmed values.
|
|
1656
|
+
*/
|
|
1657
|
+
var readLrc = (text, options = {}) => {
|
|
1237
1658
|
const { timed, meta } = readTimedLines(text);
|
|
1238
|
-
|
|
1239
|
-
|
|
1659
|
+
return {
|
|
1660
|
+
lines: readLines$1(timed, options),
|
|
1661
|
+
metaTags: meta
|
|
1662
|
+
};
|
|
1663
|
+
};
|
|
1664
|
+
//#endregion
|
|
1665
|
+
//#region src/formats/lrc/parser.ts
|
|
1666
|
+
var isBareNumber = (value) => {
|
|
1667
|
+
return /^\d+$/.test(value.trim());
|
|
1668
|
+
};
|
|
1669
|
+
/**
|
|
1670
|
+
* Unmapped tags and unparseable durations remain in `unknowns` to avoid discarding metadata.
|
|
1671
|
+
* Keys are lowercased and values trimmed, so original tag spelling is not preserved.
|
|
1672
|
+
*/
|
|
1673
|
+
var readMetaTags = (tags) => {
|
|
1674
|
+
const meta = makeMeta();
|
|
1675
|
+
for (const tag of tags) switch (tag.key) {
|
|
1676
|
+
case "length":
|
|
1677
|
+
case "duration": {
|
|
1678
|
+
const duration = parseTime(tag.value) ?? (isBareNumber(tag.value) ? Number(tag.value) : NaN);
|
|
1679
|
+
if (Number.isFinite(duration)) meta.duration = Math.trunc(duration);
|
|
1680
|
+
else meta.unknowns.push(makeUnknownEntry({
|
|
1681
|
+
key: tag.key,
|
|
1682
|
+
value: tag.value
|
|
1683
|
+
}));
|
|
1684
|
+
break;
|
|
1685
|
+
}
|
|
1686
|
+
case "offset":
|
|
1687
|
+
meta.offset = Math.trunc(Number(tag.value)) || 0;
|
|
1688
|
+
break;
|
|
1689
|
+
case "ti":
|
|
1690
|
+
case "title":
|
|
1691
|
+
meta.titles.push(makeText({ text: tag.value }));
|
|
1692
|
+
break;
|
|
1693
|
+
case "ar":
|
|
1694
|
+
case "artist":
|
|
1695
|
+
meta.artists.push(makeText({ text: tag.value }));
|
|
1696
|
+
break;
|
|
1697
|
+
case "al":
|
|
1698
|
+
case "album":
|
|
1699
|
+
meta.albums.push(makeText({ text: tag.value }));
|
|
1700
|
+
break;
|
|
1701
|
+
case "by":
|
|
1702
|
+
case "author":
|
|
1703
|
+
meta.authors.push(makeText({ text: tag.value }));
|
|
1704
|
+
break;
|
|
1705
|
+
case "isrc":
|
|
1706
|
+
meta.isrcs.push(tag.value);
|
|
1707
|
+
break;
|
|
1708
|
+
default: meta.unknowns.push(makeUnknownEntry({
|
|
1709
|
+
key: tag.key,
|
|
1710
|
+
value: tag.value
|
|
1711
|
+
}));
|
|
1712
|
+
}
|
|
1713
|
+
return meta;
|
|
1714
|
+
};
|
|
1715
|
+
var parseLrcText = (text) => {
|
|
1716
|
+
const { lines, metaTags } = readLrc(text);
|
|
1717
|
+
const meta = readMetaTags(metaTags);
|
|
1718
|
+
if (!lines.length) return makeLyric({
|
|
1240
1719
|
format: "lrc",
|
|
1241
1720
|
status: LyricStatus.INVALID,
|
|
1242
|
-
meta
|
|
1721
|
+
meta
|
|
1243
1722
|
});
|
|
1244
|
-
const lines = readLines(timed);
|
|
1245
1723
|
const wordTimed = lines.some((line) => (line.content?.words.length ?? 0) > 0);
|
|
1246
1724
|
return makeLyric({
|
|
1247
1725
|
format: "lrc",
|
|
1248
1726
|
status: LyricStatus.VALID,
|
|
1249
1727
|
timing: wordTimed ? Timing.WORD : Timing.LINE,
|
|
1250
|
-
meta
|
|
1728
|
+
meta,
|
|
1251
1729
|
languages: deriveLanguageUsages(lines),
|
|
1252
1730
|
lines
|
|
1253
1731
|
});
|
|
@@ -1256,6 +1734,7 @@ var parseLrcText = (text) => {
|
|
|
1256
1734
|
* Empty timed rows become instrumental lines; source order and missing word times remain for model validation.
|
|
1257
1735
|
*
|
|
1258
1736
|
* Annotation tracks are read as lines too and attached by timestamp, and rows that match no line are dropped.
|
|
1737
|
+
* Complete roman tracks attach to words by timing and optional local recovery; other tracks retain their source line text.
|
|
1259
1738
|
*
|
|
1260
1739
|
* `INVALID` means no readable timed lines; the returned diagnostics carry whatever the canonical tree breaks.
|
|
1261
1740
|
*/
|
|
@@ -1263,19 +1742,450 @@ var parseLrc = (input, options = {}) => {
|
|
|
1263
1742
|
const source = readLrcContent(input) ?? { original: "" };
|
|
1264
1743
|
const lyric = parseLrcText(source.original);
|
|
1265
1744
|
if (lyric.status === LyricStatus.INVALID) return finalizeLyric(lyric);
|
|
1266
|
-
const translations = source.translations
|
|
1267
|
-
const romans = source.romans
|
|
1268
|
-
if (translations.length
|
|
1269
|
-
const lines =
|
|
1745
|
+
const translations = source.translations ? readLrc(source.translations).lines : [];
|
|
1746
|
+
const romans = source.romans ? readLrc(source.romans).lines : [];
|
|
1747
|
+
if (!translations.length && !romans.length) return finalizeLyric(lyric);
|
|
1748
|
+
const lines = attachAnnotations(lyric.lines, {
|
|
1270
1749
|
translations,
|
|
1271
1750
|
romans
|
|
1272
|
-
}, {
|
|
1751
|
+
}, {
|
|
1752
|
+
tolerance: options.annotationTolerance,
|
|
1753
|
+
wordTolerance: options.wordAnnotationTolerance,
|
|
1754
|
+
wordFallback: options.wordAnnotationFallback
|
|
1755
|
+
});
|
|
1273
1756
|
return finalizeLyric({
|
|
1274
1757
|
...lyric,
|
|
1275
1758
|
lines
|
|
1276
1759
|
});
|
|
1277
1760
|
};
|
|
1278
1761
|
//#endregion
|
|
1762
|
+
//#region src/formats/qrc/common.ts
|
|
1763
|
+
var LINE_PATTERN = /^\[(\d+),(\d+)\]/;
|
|
1764
|
+
var XML_ROOT_NAMES = [
|
|
1765
|
+
"QrcInfos",
|
|
1766
|
+
"LyricInfo",
|
|
1767
|
+
"Lyric_1"
|
|
1768
|
+
];
|
|
1769
|
+
var XML_PATTERN = /^\s*</;
|
|
1770
|
+
var TIMED_LINE_PATTERN = /(?:^|[\r\n])[^\S\r\n]*\[\d+,\d+\]/;
|
|
1771
|
+
/**
|
|
1772
|
+
* Unwraps supported QRC XML while keeping plaintext literal.
|
|
1773
|
+
*
|
|
1774
|
+
* XML entities are decoded once by the XML reader.
|
|
1775
|
+
*
|
|
1776
|
+
* @returns The payload, or null when XML is malformed or has no supported lyric content.
|
|
1777
|
+
*/
|
|
1778
|
+
var readText$1 = (source) => {
|
|
1779
|
+
if (!XML_PATTERN.test(source)) return source;
|
|
1780
|
+
if (!XML_ROOT_NAMES.some((name) => source.includes(name))) return null;
|
|
1781
|
+
const document = parseXml(source);
|
|
1782
|
+
if (!document) return null;
|
|
1783
|
+
const root = document.children.find((node) => node.nodeType === XmlNodeType.element);
|
|
1784
|
+
if (!root || !XML_ROOT_NAMES.includes(root.localName)) return null;
|
|
1785
|
+
const pending = [root];
|
|
1786
|
+
while (pending.length) {
|
|
1787
|
+
const element = pending.pop();
|
|
1788
|
+
const content = getXmlAttribute(element, "LyricContent");
|
|
1789
|
+
if (content !== void 0) return content;
|
|
1790
|
+
for (let index = element.children.length - 1; index >= 0; index -= 1) {
|
|
1791
|
+
const child = element.children[index];
|
|
1792
|
+
if (child.nodeType === XmlNodeType.element) pending.push(child);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
return null;
|
|
1796
|
+
};
|
|
1797
|
+
/**
|
|
1798
|
+
* Validates the original payload and keeps auxiliary tracks separate so each can use its own format.
|
|
1799
|
+
*
|
|
1800
|
+
* @returns Normalized input, or null when the original content cannot be read.
|
|
1801
|
+
*/
|
|
1802
|
+
var readContent = (input) => {
|
|
1803
|
+
const value = typeof input === "string" ? { original: input } : input;
|
|
1804
|
+
if (!value || typeof value !== "object") return null;
|
|
1805
|
+
const source = value;
|
|
1806
|
+
if (typeof source.original !== "string") return null;
|
|
1807
|
+
const original = readText$1(source.original);
|
|
1808
|
+
if (original === null) return null;
|
|
1809
|
+
return {
|
|
1810
|
+
original,
|
|
1811
|
+
translations: typeof source.translations === "string" ? source.translations : void 0,
|
|
1812
|
+
romans: typeof source.romans === "string" ? source.romans : void 0
|
|
1813
|
+
};
|
|
1814
|
+
};
|
|
1815
|
+
/**
|
|
1816
|
+
* Requires a timed QRC row so metadata alone cannot select the format.
|
|
1817
|
+
*/
|
|
1818
|
+
var hasTimedLines = (text) => {
|
|
1819
|
+
return TIMED_LINE_PATTERN.test(text);
|
|
1820
|
+
};
|
|
1821
|
+
//#endregion
|
|
1822
|
+
//#region src/formats/qrc/detect.ts
|
|
1823
|
+
/**
|
|
1824
|
+
* Detects QRC from the original track so annotations cannot change format selection.
|
|
1825
|
+
*
|
|
1826
|
+
* @returns Whether plaintext or a supported XML payload contains a timed QRC row.
|
|
1827
|
+
*/
|
|
1828
|
+
var detectQrc = (input) => {
|
|
1829
|
+
const content = readContent(input);
|
|
1830
|
+
return !!content && hasTimedLines(content.original);
|
|
1831
|
+
};
|
|
1832
|
+
//#endregion
|
|
1833
|
+
//#region src/formats/qrc/parser.ts
|
|
1834
|
+
var WORD_PATTERN = /\((\d+),(\d+)\)/g;
|
|
1835
|
+
var META_PATTERN = /^([A-Za-z0-9_-]+)\s*:\s*([\s\S]*)$/;
|
|
1836
|
+
var META_TAG_PATTERN = /\[([^\]]*)\]/g;
|
|
1837
|
+
var RUBY_PATTERN = /\((\d+),(\d+)\)|(\d+)|([^0-9()]+)/gy;
|
|
1838
|
+
var RUBY_WORD_PATTERN = /[\p{Script=Han}\p{Number}]/u;
|
|
1839
|
+
var SEPARATORS = /^\p{White_Space}+$/u;
|
|
1840
|
+
var NATIVE_ANNOTATION_TIMING = {
|
|
1841
|
+
precision: 1,
|
|
1842
|
+
rounding: 1
|
|
1843
|
+
};
|
|
1844
|
+
var readRubyEntries = (value) => {
|
|
1845
|
+
const entries = [];
|
|
1846
|
+
let length = 0;
|
|
1847
|
+
let tokens = [];
|
|
1848
|
+
let text = "";
|
|
1849
|
+
let cursor = 0;
|
|
1850
|
+
const finish = () => {
|
|
1851
|
+
if (text.trim()) tokens.push(makeWordAnnotationToken({ text }));
|
|
1852
|
+
if (!length || !tokens.length) return false;
|
|
1853
|
+
entries.push({
|
|
1854
|
+
length,
|
|
1855
|
+
ruby: makeWordAnnotationRuby({ tokens })
|
|
1856
|
+
});
|
|
1857
|
+
tokens = [];
|
|
1858
|
+
text = "";
|
|
1859
|
+
return true;
|
|
1860
|
+
};
|
|
1861
|
+
for (const match of value.matchAll(RUBY_PATTERN)) {
|
|
1862
|
+
cursor = match.index + match[0].length;
|
|
1863
|
+
if (match[3]) {
|
|
1864
|
+
if (length && !finish()) return;
|
|
1865
|
+
length = Number(match[3]);
|
|
1866
|
+
if (!Number.isSafeInteger(length) || length <= 0) return;
|
|
1867
|
+
} else if (match[1]) {
|
|
1868
|
+
const start = Number(match[1]);
|
|
1869
|
+
const end = start + Number(match[2]);
|
|
1870
|
+
if (!length || !text.trim() || !Number.isSafeInteger(start) || !Number.isSafeInteger(end)) return;
|
|
1871
|
+
tokens.push(makeWordAnnotationToken({
|
|
1872
|
+
text,
|
|
1873
|
+
time: {
|
|
1874
|
+
start,
|
|
1875
|
+
end
|
|
1876
|
+
}
|
|
1877
|
+
}));
|
|
1878
|
+
text = "";
|
|
1879
|
+
} else {
|
|
1880
|
+
if (!length) return;
|
|
1881
|
+
text += match[4];
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return cursor === value.length && finish() ? entries : void 0;
|
|
1885
|
+
};
|
|
1886
|
+
var readRubies = (meta) => {
|
|
1887
|
+
let entries;
|
|
1888
|
+
const unknowns = [];
|
|
1889
|
+
for (const entry of meta.unknowns) if (entry.key === "kana") entries ??= readRubyEntries(entry.value);
|
|
1890
|
+
else unknowns.push(entry);
|
|
1891
|
+
meta.unknowns = unknowns;
|
|
1892
|
+
return entries;
|
|
1893
|
+
};
|
|
1894
|
+
var matchRubies = (words, sources) => {
|
|
1895
|
+
const starts = /* @__PURE__ */ new Map();
|
|
1896
|
+
for (const [index, candidate] of words.entries()) {
|
|
1897
|
+
const start = candidate?.word.time?.start;
|
|
1898
|
+
if (start !== void 0 && Number.isSafeInteger(start) && start >= 0) {
|
|
1899
|
+
const queue = starts.get(start) ?? [];
|
|
1900
|
+
queue.push(index);
|
|
1901
|
+
starts.set(start, queue);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
const timed = /* @__PURE__ */ new Set();
|
|
1905
|
+
const claims = /* @__PURE__ */ new Map();
|
|
1906
|
+
for (const [index, source] of sources.entries()) {
|
|
1907
|
+
const times = source.ruby.tokens.flatMap((token) => token.time ? [token.time] : []);
|
|
1908
|
+
if (!times.length) continue;
|
|
1909
|
+
timed.add(index);
|
|
1910
|
+
const start = times[0].start;
|
|
1911
|
+
const end = times.at(-1).end;
|
|
1912
|
+
if (start === void 0) continue;
|
|
1913
|
+
let candidates = (starts.get(start) ?? []).filter((target) => {
|
|
1914
|
+
const candidate = words[target];
|
|
1915
|
+
const finish = candidate.word.time?.end;
|
|
1916
|
+
return candidate.length === source.length && finish !== void 0 && Number.isSafeInteger(finish) && times.every((time) => {
|
|
1917
|
+
return time.start !== void 0 && time.end !== void 0 && time.start >= start && time.end >= time.start && time.end <= finish;
|
|
1918
|
+
});
|
|
1919
|
+
});
|
|
1920
|
+
if (times.length === source.ruby.tokens.length) {
|
|
1921
|
+
const exact = candidates.filter((target) => words[target].word.time.end === end);
|
|
1922
|
+
if (exact.length) candidates = exact;
|
|
1923
|
+
}
|
|
1924
|
+
if (candidates.length !== 1) continue;
|
|
1925
|
+
const target = candidates[0];
|
|
1926
|
+
const queue = claims.get(target) ?? [];
|
|
1927
|
+
queue.push(index);
|
|
1928
|
+
claims.set(target, queue);
|
|
1929
|
+
}
|
|
1930
|
+
const matched = /* @__PURE__ */ new Map();
|
|
1931
|
+
for (const [target, queue] of claims) if (queue.length === 1) matched.set(queue[0], target);
|
|
1932
|
+
const anchors = [
|
|
1933
|
+
[-1, -1],
|
|
1934
|
+
...[...matched].sort((first, second) => first[0] - second[0]),
|
|
1935
|
+
[sources.length, words.length]
|
|
1936
|
+
];
|
|
1937
|
+
for (let index = 1; index < anchors.length; index += 1) if (anchors[index][1] <= anchors[index - 1][1]) return matched;
|
|
1938
|
+
for (let index = 1; index < anchors.length; index += 1) {
|
|
1939
|
+
const [firstSource, firstWord] = anchors[index - 1];
|
|
1940
|
+
const [lastSource, lastWord] = anchors[index];
|
|
1941
|
+
const pending = [];
|
|
1942
|
+
let compatible = true;
|
|
1943
|
+
for (let source = firstSource + 1; source < lastSource; source += 1) {
|
|
1944
|
+
if (timed.has(source)) {
|
|
1945
|
+
compatible = false;
|
|
1946
|
+
break;
|
|
1947
|
+
}
|
|
1948
|
+
pending.push(source);
|
|
1949
|
+
}
|
|
1950
|
+
if (!compatible || !pending.length) continue;
|
|
1951
|
+
const candidates = [];
|
|
1952
|
+
for (let target = firstWord + 1; target < lastWord; target += 1) {
|
|
1953
|
+
const candidate = words[target];
|
|
1954
|
+
if (!candidate) {
|
|
1955
|
+
compatible = false;
|
|
1956
|
+
break;
|
|
1957
|
+
}
|
|
1958
|
+
if (candidate.byOrder) candidates.push(target);
|
|
1959
|
+
}
|
|
1960
|
+
if (!compatible || candidates.length !== pending.length || pending.some((source, position) => sources[source].length !== words[candidates[position]].length)) continue;
|
|
1961
|
+
for (const [position, source] of pending.entries()) matched.set(source, candidates[position]);
|
|
1962
|
+
}
|
|
1963
|
+
return matched;
|
|
1964
|
+
};
|
|
1965
|
+
/**
|
|
1966
|
+
* Attaches whole-word ruby without changing the source text, segmentation, or timing.
|
|
1967
|
+
*
|
|
1968
|
+
* @param lines Original QRC rows before filtering or sorting.
|
|
1969
|
+
* @param sources Readings with source coverage lengths and optional token times.
|
|
1970
|
+
*
|
|
1971
|
+
* @returns Annotated rows, reusing arrays and nodes when they are unchanged.
|
|
1972
|
+
*/
|
|
1973
|
+
var attachRubies = (lines, sources) => {
|
|
1974
|
+
if (!sources.length) return lines;
|
|
1975
|
+
const words = [];
|
|
1976
|
+
for (const line of lines) {
|
|
1977
|
+
if (line.type !== LineType.NORMAL) continue;
|
|
1978
|
+
if (line.content?.text && RUBY_WORD_PATTERN.test(line.content.text)) {
|
|
1979
|
+
words.push(void 0);
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
for (const word of line.content?.words ?? []) if (word.type === WordType.NORMAL) words.push({
|
|
1983
|
+
word,
|
|
1984
|
+
length: [...word.text].length,
|
|
1985
|
+
byOrder: RUBY_WORD_PATTERN.test(word.text)
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
const matched = matchRubies(words, sources);
|
|
1989
|
+
if (!matched.size) return lines;
|
|
1990
|
+
const annotations = new Map([...matched].map(([source, target]) => [words[target].word, sources[source].ruby]));
|
|
1991
|
+
return mapShared(lines, (line) => {
|
|
1992
|
+
const content = line.content;
|
|
1993
|
+
if (!content) return line;
|
|
1994
|
+
const words = mapShared(content.words, (word) => {
|
|
1995
|
+
const ruby = annotations.get(word);
|
|
1996
|
+
if (!ruby) return word;
|
|
1997
|
+
return {
|
|
1998
|
+
...word,
|
|
1999
|
+
annotation: makeWordAnnotation({
|
|
2000
|
+
...word.annotation,
|
|
2001
|
+
rubies: [...word.annotation?.rubies ?? [], ruby]
|
|
2002
|
+
})
|
|
2003
|
+
};
|
|
2004
|
+
});
|
|
2005
|
+
return words === content.words ? line : {
|
|
2006
|
+
...line,
|
|
2007
|
+
content: {
|
|
2008
|
+
...content,
|
|
2009
|
+
words
|
|
2010
|
+
}
|
|
2011
|
+
};
|
|
2012
|
+
});
|
|
2013
|
+
};
|
|
2014
|
+
var readMeta = (body, meta) => {
|
|
2015
|
+
const match = META_PATTERN.exec(body);
|
|
2016
|
+
if (!match) return;
|
|
2017
|
+
const key = match[1].toLowerCase();
|
|
2018
|
+
const value = match[2].trim();
|
|
2019
|
+
switch (key) {
|
|
2020
|
+
case "length":
|
|
2021
|
+
case "duration": {
|
|
2022
|
+
const duration = parseTime(value) ?? (/^\d+$/.test(value) ? Number(value) : NaN);
|
|
2023
|
+
if (Number.isFinite(duration)) meta.duration = Math.trunc(duration);
|
|
2024
|
+
else meta.unknowns.push(makeUnknownEntry({
|
|
2025
|
+
key,
|
|
2026
|
+
value
|
|
2027
|
+
}));
|
|
2028
|
+
break;
|
|
2029
|
+
}
|
|
2030
|
+
case "offset":
|
|
2031
|
+
meta.offset = Math.trunc(Number(value)) || 0;
|
|
2032
|
+
break;
|
|
2033
|
+
case "ti":
|
|
2034
|
+
case "title":
|
|
2035
|
+
meta.titles.push(makeText({ text: value }));
|
|
2036
|
+
break;
|
|
2037
|
+
case "ar":
|
|
2038
|
+
case "artist":
|
|
2039
|
+
meta.artists.push(makeText({ text: value }));
|
|
2040
|
+
break;
|
|
2041
|
+
case "al":
|
|
2042
|
+
case "album":
|
|
2043
|
+
meta.albums.push(makeText({ text: value }));
|
|
2044
|
+
break;
|
|
2045
|
+
case "by":
|
|
2046
|
+
case "author":
|
|
2047
|
+
meta.authors.push(makeText({ text: value }));
|
|
2048
|
+
break;
|
|
2049
|
+
case "isrc":
|
|
2050
|
+
meta.isrcs.push(value);
|
|
2051
|
+
break;
|
|
2052
|
+
default: meta.unknowns.push(makeUnknownEntry({
|
|
2053
|
+
key,
|
|
2054
|
+
value
|
|
2055
|
+
}));
|
|
2056
|
+
}
|
|
2057
|
+
};
|
|
2058
|
+
var readTimeRange = (startText, durationText) => {
|
|
2059
|
+
const start = Number(startText);
|
|
2060
|
+
return makeTimeRange({
|
|
2061
|
+
start,
|
|
2062
|
+
end: start + Number(durationText)
|
|
2063
|
+
});
|
|
2064
|
+
};
|
|
2065
|
+
var appendWords = (words, text, time) => {
|
|
2066
|
+
for (const fragment of text.split(/(\p{White_Space}+)/u)) {
|
|
2067
|
+
if (!fragment) continue;
|
|
2068
|
+
if (SEPARATORS.test(fragment)) words.push(makeWordSpace({ text: fragment }));
|
|
2069
|
+
else words.push(makeWordNormal({
|
|
2070
|
+
text: fragment,
|
|
2071
|
+
time
|
|
2072
|
+
}));
|
|
2073
|
+
}
|
|
2074
|
+
};
|
|
2075
|
+
var readWords$1 = (text) => {
|
|
2076
|
+
const words = [];
|
|
2077
|
+
let cursor = 0;
|
|
2078
|
+
let tagged = false;
|
|
2079
|
+
for (const match of text.matchAll(WORD_PATTERN)) {
|
|
2080
|
+
tagged = true;
|
|
2081
|
+
appendWords(words, text.slice(cursor, match.index), readTimeRange(match[1], match[2]));
|
|
2082
|
+
cursor = match.index + match[0].length;
|
|
2083
|
+
}
|
|
2084
|
+
if (!tagged) return;
|
|
2085
|
+
appendWords(words, text.slice(cursor));
|
|
2086
|
+
return words;
|
|
2087
|
+
};
|
|
2088
|
+
var readLines = (text) => {
|
|
2089
|
+
const lines = [];
|
|
2090
|
+
const meta = makeMeta();
|
|
2091
|
+
const placeholders = /* @__PURE__ */ new Set();
|
|
2092
|
+
for (const raw of text.split(/\r\n?|\n/)) {
|
|
2093
|
+
const line = raw.trimStart();
|
|
2094
|
+
const match = LINE_PATTERN.exec(line);
|
|
2095
|
+
if (match) {
|
|
2096
|
+
const time = readTimeRange(match[1], match[2]);
|
|
2097
|
+
const text = line.slice(match[0].length);
|
|
2098
|
+
const words = readWords$1(text);
|
|
2099
|
+
const contentText = (words ? getWordsText(words) : text).trim();
|
|
2100
|
+
const placeholder = contentText === "//";
|
|
2101
|
+
if (placeholder) placeholders.add(lines.length);
|
|
2102
|
+
if (!contentText || placeholder) lines.push(makeLineInstrumental({ time }));
|
|
2103
|
+
else {
|
|
2104
|
+
const content = words ? makeLineContentWords(words) : makeLineContentText(text);
|
|
2105
|
+
lines.push(makeLineNormal({
|
|
2106
|
+
time,
|
|
2107
|
+
content
|
|
2108
|
+
}));
|
|
2109
|
+
}
|
|
2110
|
+
continue;
|
|
2111
|
+
}
|
|
2112
|
+
if (!line.startsWith("[")) continue;
|
|
2113
|
+
for (const tag of line.matchAll(META_TAG_PATTERN)) {
|
|
2114
|
+
const body = tag[1];
|
|
2115
|
+
if (isTime(body)) continue;
|
|
2116
|
+
readMeta(body, meta);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
return {
|
|
2120
|
+
lines,
|
|
2121
|
+
meta,
|
|
2122
|
+
placeholders
|
|
2123
|
+
};
|
|
2124
|
+
};
|
|
2125
|
+
var readAnnotations = (text, timings) => {
|
|
2126
|
+
if (!text) return [];
|
|
2127
|
+
const { lines } = readLines(text);
|
|
2128
|
+
if (lines.length) {
|
|
2129
|
+
for (const line of lines) timings.set(line, NATIVE_ANNOTATION_TIMING);
|
|
2130
|
+
return lines;
|
|
2131
|
+
}
|
|
2132
|
+
readLrc(text, { onLine: (line, timing) => {
|
|
2133
|
+
const annotation = getContentText(line.content).trim() === "//" ? makeLineInstrumental({ time: line.time }) : line;
|
|
2134
|
+
timings.set(annotation, timing);
|
|
2135
|
+
lines.push(annotation);
|
|
2136
|
+
} });
|
|
2137
|
+
return lines;
|
|
2138
|
+
};
|
|
2139
|
+
/**
|
|
2140
|
+
* Parses plaintext or XML QRC, retaining explicit timing, word romanization, and ruby from kana metadata.
|
|
2141
|
+
* Original-track kana supplies ruby and is removed from the returned metadata.
|
|
2142
|
+
*
|
|
2143
|
+
* @param input Original lyrics with optional translation and romanization tracks.
|
|
2144
|
+
* @param options Independent line and word tolerances, with optional local roman recovery.
|
|
2145
|
+
*
|
|
2146
|
+
* @returns Canonical lyrics and source diagnostics; an empty result has INVALID status.
|
|
2147
|
+
*/
|
|
2148
|
+
var parseQrc = (input, options = {}) => {
|
|
2149
|
+
const source = readContent(input);
|
|
2150
|
+
if (!source) return finalizeLyric(makeLyric({
|
|
2151
|
+
format: "qrc",
|
|
2152
|
+
status: LyricStatus.INVALID
|
|
2153
|
+
}));
|
|
2154
|
+
const parsed = readLines(source.original);
|
|
2155
|
+
const meta = parsed.meta;
|
|
2156
|
+
const rubies = readRubies(meta);
|
|
2157
|
+
if (!parsed.lines.length) return finalizeLyric(makeLyric({
|
|
2158
|
+
format: "qrc",
|
|
2159
|
+
status: LyricStatus.INVALID,
|
|
2160
|
+
meta
|
|
2161
|
+
}));
|
|
2162
|
+
const timings = /* @__PURE__ */ new WeakMap();
|
|
2163
|
+
const translationLines = readAnnotations(source.translations && readText$1(source.translations), timings);
|
|
2164
|
+
const romanLines = readAnnotations(source.romans && readText$1(source.romans), timings);
|
|
2165
|
+
const rounding = romanLines.some((line) => timings.get(line)?.rounding === 1) ? 1 : 0;
|
|
2166
|
+
const wordTolerance = Number.isFinite(options.wordAnnotationTolerance) ? options.wordAnnotationTolerance ?? 0 : 0;
|
|
2167
|
+
const annotated = attachAnnotations(!rubies ? parsed.lines : attachRubies(parsed.lines, rubies), {
|
|
2168
|
+
translations: translationLines,
|
|
2169
|
+
romans: romanLines
|
|
2170
|
+
}, {
|
|
2171
|
+
matchByOrder: true,
|
|
2172
|
+
preservePlaceholders: true,
|
|
2173
|
+
tolerance: options.annotationTolerance,
|
|
2174
|
+
wordTolerance: Math.max(wordTolerance, rounding),
|
|
2175
|
+
wordFallback: options.wordAnnotationFallback,
|
|
2176
|
+
getTiming: (line) => timings.get(line)
|
|
2177
|
+
});
|
|
2178
|
+
const lines = parsed.placeholders.size ? annotated.filter((_, index) => !parsed.placeholders.has(index)) : annotated;
|
|
2179
|
+
return finalizeLyric(makeLyric({
|
|
2180
|
+
format: "qrc",
|
|
2181
|
+
status: lines.length ? LyricStatus.VALID : LyricStatus.INVALID,
|
|
2182
|
+
timing: lines.some(isWordLevelLine) ? Timing.WORD : Timing.LINE,
|
|
2183
|
+
meta,
|
|
2184
|
+
lines,
|
|
2185
|
+
languages: deriveLanguageUsages(lines)
|
|
2186
|
+
}));
|
|
2187
|
+
};
|
|
2188
|
+
//#endregion
|
|
1279
2189
|
//#region src/formats/ttml/common.ts
|
|
1280
2190
|
var FORMAT = "ttml";
|
|
1281
2191
|
var AMLL_HINT_PATTERN = /xmlns:amll|amll:meta/iu;
|
|
@@ -1404,7 +2314,7 @@ var renderTime = (time, offset) => {
|
|
|
1404
2314
|
* Writes known bounds without inventing an end.
|
|
1405
2315
|
*/
|
|
1406
2316
|
var renderTimeAttributes = (time, offset) => {
|
|
1407
|
-
if (time
|
|
2317
|
+
if (!time) throw new Error("TTML sung lines, words, and ruby tokens require a time range");
|
|
1408
2318
|
const attributes = [["begin", renderTime(time.start, offset)]];
|
|
1409
2319
|
if (time.end !== void 0) attributes.push(["end", renderTime(time.end, offset)]);
|
|
1410
2320
|
return attributes;
|
|
@@ -1426,7 +2336,7 @@ var renderWord = (word, context, language) => {
|
|
|
1426
2336
|
if (word.type === WordType.SPACE) return createXmlText(word.text);
|
|
1427
2337
|
const attributes = [...renderTimeAttributes(word.time, context.offset), ...renderLanguage(word.language, language)];
|
|
1428
2338
|
const ruby = context.profile === "amll" ? word.annotation?.rubies[0] : void 0;
|
|
1429
|
-
if (ruby
|
|
2339
|
+
if (!ruby) return createXmlElement("span", attributes, [createXmlText(word.text)]);
|
|
1430
2340
|
const tokens = ruby.tokens.map((token) => {
|
|
1431
2341
|
return createXmlElement("span", [["tts:ruby", RUBY_TEXT], ...renderTimeAttributes(resolveAnnotationTokenTime(token, ruby, word), context.offset)], [createXmlText(token.text)]);
|
|
1432
2342
|
});
|
|
@@ -1441,7 +2351,7 @@ var renderWord = (word, context, language) => {
|
|
|
1441
2351
|
*/
|
|
1442
2352
|
var renderContent = (line, context) => {
|
|
1443
2353
|
const content = line.content;
|
|
1444
|
-
if (content
|
|
2354
|
+
if (!content) throw new Error("TTML sung lines require content");
|
|
1445
2355
|
if (content.text !== void 0) return [createXmlText(content.text)];
|
|
1446
2356
|
return content.words.map((word) => {
|
|
1447
2357
|
return renderWord(word, context, getLineLanguage(line));
|
|
@@ -1450,10 +2360,10 @@ var renderContent = (line, context) => {
|
|
|
1450
2360
|
/**
|
|
1451
2361
|
* Keeps annotations outside the sung word stream.
|
|
1452
2362
|
*/
|
|
1453
|
-
var renderInlineAnnotations = (
|
|
2363
|
+
var renderInlineAnnotations = (line, language) => {
|
|
1454
2364
|
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
|
|
2365
|
+
for (const item of line.annotation?.translations ?? []) nodes.push(createXmlElement("span", [["ttm:role", ROLE_TRANSLATION], ...renderLanguage(item.language, language)], [createXmlText(item.text)]));
|
|
2366
|
+
for (const item of getLineRomans(line)) nodes.push(createXmlElement("span", [["ttm:role", ROLE_ROMAN], ...renderLanguage(item.language, language)], [createXmlText(item.text)]));
|
|
1457
2367
|
return nodes;
|
|
1458
2368
|
};
|
|
1459
2369
|
/**
|
|
@@ -1469,7 +2379,7 @@ var renderLineAttributes = (line, context) => {
|
|
|
1469
2379
|
const lead = line.agents[0];
|
|
1470
2380
|
if (lead !== void 0) {
|
|
1471
2381
|
const id = context.agents.get(lead);
|
|
1472
|
-
if (id
|
|
2382
|
+
if (!id) throw new Error("TTML performing agents must have a declaration");
|
|
1473
2383
|
attributes.push(["ttm:agent", id]);
|
|
1474
2384
|
}
|
|
1475
2385
|
return attributes;
|
|
@@ -1482,7 +2392,7 @@ var createLyricElement = (name, attributes, children) => {
|
|
|
1482
2392
|
};
|
|
1483
2393
|
var renderBackground = (line, context) => {
|
|
1484
2394
|
const children = renderContent(line, context);
|
|
1485
|
-
if (context.profile === "amll") children.push(...renderInlineAnnotations(line
|
|
2395
|
+
if (context.profile === "amll") children.push(...renderInlineAnnotations(line, getLineLanguage(line)));
|
|
1486
2396
|
return createXmlElement("span", [["ttm:role", ROLE_BACKGROUND], ...renderLineAttributes(line, context)], children);
|
|
1487
2397
|
};
|
|
1488
2398
|
var renderLine = (line, context) => {
|
|
@@ -1490,7 +2400,7 @@ var renderLine = (line, context) => {
|
|
|
1490
2400
|
const after = [];
|
|
1491
2401
|
for (const background of line.backgrounds) {
|
|
1492
2402
|
const node = renderBackground(background, context);
|
|
1493
|
-
if (background.time
|
|
2403
|
+
if (background.time && line.time && background.time.start < line.time.start) before.push(node);
|
|
1494
2404
|
else after.push(node);
|
|
1495
2405
|
}
|
|
1496
2406
|
const children = [
|
|
@@ -1498,7 +2408,7 @@ var renderLine = (line, context) => {
|
|
|
1498
2408
|
...renderContent(line, context),
|
|
1499
2409
|
...after
|
|
1500
2410
|
];
|
|
1501
|
-
if (context.profile === "amll") children.push(...renderInlineAnnotations(line
|
|
2411
|
+
if (context.profile === "amll") children.push(...renderInlineAnnotations(line, getLineLanguage(line)));
|
|
1502
2412
|
return createLyricElement("p", renderLineAttributes(line, context), children);
|
|
1503
2413
|
};
|
|
1504
2414
|
var renderAgents = (lyric, context) => {
|
|
@@ -1533,10 +2443,10 @@ var renderSongwriters = (meta) => {
|
|
|
1533
2443
|
if (credit.role !== CreditRole.LYRICIST) continue;
|
|
1534
2444
|
for (const name of credit.names) names.push(createXmlElement("songwriter", renderLanguage(name.language), [createXmlText(name.text)]));
|
|
1535
2445
|
}
|
|
1536
|
-
return names.length
|
|
2446
|
+
return names.length ? [createXmlElement("songwriters", [], names)] : [];
|
|
1537
2447
|
};
|
|
1538
2448
|
var renderAnnotationTracks = (tracks, kind, context) => {
|
|
1539
|
-
if (tracks.size
|
|
2449
|
+
if (!tracks.size) return [];
|
|
1540
2450
|
const translated = kind === "translations";
|
|
1541
2451
|
const blocks = Array.from(tracks, ([language, track]) => {
|
|
1542
2452
|
const attributes = translated ? [["type", "subtitle"]] : [];
|
|
@@ -1556,26 +2466,29 @@ var renderHeadAnnotations = (lines, context) => {
|
|
|
1556
2466
|
translations: /* @__PURE__ */ new Map(),
|
|
1557
2467
|
romans: /* @__PURE__ */ new Map()
|
|
1558
2468
|
};
|
|
1559
|
-
const append = (
|
|
1560
|
-
|
|
2469
|
+
const append = (source, line, background) => {
|
|
2470
|
+
const annotation = {
|
|
2471
|
+
translations: source.annotation?.translations ?? [],
|
|
2472
|
+
romans: getLineRomans(source)
|
|
2473
|
+
};
|
|
1561
2474
|
for (const kind of ANNOTATION_KINDS) for (const item of annotation[kind]) {
|
|
1562
2475
|
const language = item.language?.toLowerCase();
|
|
1563
2476
|
let track = tracks[kind].get(language);
|
|
1564
|
-
if (track
|
|
2477
|
+
if (!track) {
|
|
1565
2478
|
track = /* @__PURE__ */ new Map();
|
|
1566
2479
|
tracks[kind].set(language, track);
|
|
1567
2480
|
}
|
|
1568
2481
|
let nodes = track.get(line);
|
|
1569
|
-
if (nodes
|
|
2482
|
+
if (!nodes) {
|
|
1570
2483
|
nodes = [];
|
|
1571
2484
|
track.set(line, nodes);
|
|
1572
2485
|
}
|
|
1573
|
-
nodes.push(background
|
|
2486
|
+
nodes.push(!background ? createXmlText(item.text) : createXmlElement("span", [["ttm:role", ROLE_BACKGROUND], ["for", context.keys.get(background)]], [createXmlText(item.text)]));
|
|
1574
2487
|
}
|
|
1575
2488
|
};
|
|
1576
2489
|
for (const line of lines) {
|
|
1577
|
-
append(line
|
|
1578
|
-
for (const background of line.backgrounds) append(background
|
|
2490
|
+
append(line, line);
|
|
2491
|
+
for (const background of line.backgrounds) append(background, line, background);
|
|
1579
2492
|
}
|
|
1580
2493
|
return ANNOTATION_KINDS.flatMap((kind) => {
|
|
1581
2494
|
return renderAnnotationTracks(tracks[kind], kind, context);
|
|
@@ -1584,12 +2497,12 @@ var renderHeadAnnotations = (lines, context) => {
|
|
|
1584
2497
|
var renderMetadata = (lyric, lines, context, includeMeta) => {
|
|
1585
2498
|
const metadata = renderAgents(lyric, context);
|
|
1586
2499
|
const itunes = [];
|
|
1587
|
-
if (includeMeta && lyric.meta
|
|
2500
|
+
if (includeMeta && lyric.meta) {
|
|
1588
2501
|
if (context.profile === "amll") metadata.push(...renderAmllMeta(lyric.meta));
|
|
1589
2502
|
itunes.push(...renderSongwriters(lyric.meta));
|
|
1590
2503
|
}
|
|
1591
2504
|
if (context.profile === "itunes") itunes.push(...renderHeadAnnotations(lines, context));
|
|
1592
|
-
if (itunes.length
|
|
2505
|
+
if (itunes.length) metadata.push(createXmlElement("iTunesMetadata", [["xmlns", NAMESPACE_ITUNES_METADATA]], itunes));
|
|
1593
2506
|
return createXmlElement("metadata", renderLanguage(void 0, lyric.meta?.language), metadata);
|
|
1594
2507
|
};
|
|
1595
2508
|
var renderRootAttributes = (lyric, profile) => {
|
|
@@ -1608,6 +2521,7 @@ var renderRootAttributes = (lyric, profile) => {
|
|
|
1608
2521
|
*
|
|
1609
2522
|
* Offsets are baked into timestamps; a negative result or a missing required time throws.
|
|
1610
2523
|
* Profile mappings retain the lead agent and first AMLL ruby, omitting unsupported fields and instrumental nodes.
|
|
2524
|
+
* Word romanizations are emitted as line text without their token timing.
|
|
1611
2525
|
*/
|
|
1612
2526
|
var generateTtml = (lyric, options = {}) => {
|
|
1613
2527
|
if (lyric.timing !== Timing.LINE && lyric.timing !== Timing.WORD) throw new Error("TTML generation requires line or word timing");
|
|
@@ -1691,7 +2605,7 @@ var readText = (element, inherited) => {
|
|
|
1691
2605
|
* Preserves the parser's whitespace segmentation when turning text into word drafts.
|
|
1692
2606
|
*/
|
|
1693
2607
|
var splitWords = (text, language) => {
|
|
1694
|
-
if (text
|
|
2608
|
+
if (!text) return [];
|
|
1695
2609
|
if (!WORD_SEPARATOR_PATTERN.test(text)) return [{
|
|
1696
2610
|
text,
|
|
1697
2611
|
space: false,
|
|
@@ -1699,7 +2613,7 @@ var splitWords = (text, language) => {
|
|
|
1699
2613
|
rubies: []
|
|
1700
2614
|
}];
|
|
1701
2615
|
const words = [];
|
|
1702
|
-
for (const run of text.split(/(\p{White_Space}+)/u)) if (run
|
|
2616
|
+
for (const run of text.split(/(\p{White_Space}+)/u)) if (run) {
|
|
1703
2617
|
const space = /^\p{White_Space}+$/u.test(run);
|
|
1704
2618
|
words.push({
|
|
1705
2619
|
text: run,
|
|
@@ -1714,7 +2628,7 @@ var splitWords = (text, language) => {
|
|
|
1714
2628
|
* Distinguishes indentation runs from separators using the parser's existing rule.
|
|
1715
2629
|
*/
|
|
1716
2630
|
var isFormattingWhitespace = (text) => {
|
|
1717
|
-
return text.trim()
|
|
2631
|
+
return !text.trim() && text.includes("\n");
|
|
1718
2632
|
};
|
|
1719
2633
|
/**
|
|
1720
2634
|
* Builds words from source drafts without deriving missing timestamps.
|
|
@@ -1730,7 +2644,7 @@ var buildWords = (draft) => {
|
|
|
1730
2644
|
start: word.start,
|
|
1731
2645
|
end: word.end
|
|
1732
2646
|
});
|
|
1733
|
-
if (word.rubies.length
|
|
2647
|
+
if (!word.rubies.length) {
|
|
1734
2648
|
words.push(makeWordNormal({
|
|
1735
2649
|
text: word.text,
|
|
1736
2650
|
time,
|
|
@@ -1769,7 +2683,7 @@ var buildWords = (draft) => {
|
|
|
1769
2683
|
* Builds only annotations explicitly present in the source drafts.
|
|
1770
2684
|
*/
|
|
1771
2685
|
var buildLineAnnotation = (draft) => {
|
|
1772
|
-
if (draft.translations.length
|
|
2686
|
+
if (!draft.translations.length && !draft.romans.length) return;
|
|
1773
2687
|
return makeLineAnnotation({
|
|
1774
2688
|
translations: draft.translations.map((item) => {
|
|
1775
2689
|
return makeLineAnnotationTranslation({
|
|
@@ -1820,9 +2734,9 @@ var groupElements = (root, names, preserveSpace, stopAtMatch = false, language)
|
|
|
1820
2734
|
preserveSpace,
|
|
1821
2735
|
language
|
|
1822
2736
|
}];
|
|
1823
|
-
while (stack.length
|
|
2737
|
+
while (stack.length) {
|
|
1824
2738
|
const entry = stack.pop();
|
|
1825
|
-
if (entry
|
|
2739
|
+
if (!entry) continue;
|
|
1826
2740
|
const element = entry.element;
|
|
1827
2741
|
const currentSpace = readPreserveSpace(element, entry.preserveSpace);
|
|
1828
2742
|
const currentLanguage = readLanguage(element, entry.language);
|
|
@@ -1833,7 +2747,7 @@ var groupElements = (root, names, preserveSpace, stopAtMatch = false, language)
|
|
|
1833
2747
|
preserveSpace: currentSpace,
|
|
1834
2748
|
language: currentLanguage
|
|
1835
2749
|
};
|
|
1836
|
-
if (group
|
|
2750
|
+
if (!group) result.set(element.localName, [context]);
|
|
1837
2751
|
else group.push(context);
|
|
1838
2752
|
if (stopAtMatch) continue;
|
|
1839
2753
|
}
|
|
@@ -1893,22 +2807,22 @@ var readRuby = (container, preserveSpace, language) => {
|
|
|
1893
2807
|
const last = tokens.findLast((token) => {
|
|
1894
2808
|
return token.start !== void 0;
|
|
1895
2809
|
});
|
|
1896
|
-
const time = readSpanTime(container) ?? rubyTime ?? (first
|
|
2810
|
+
const time = readSpanTime(container) ?? rubyTime ?? (first ? {
|
|
1897
2811
|
start: first.start,
|
|
1898
2812
|
end: last?.end
|
|
1899
|
-
});
|
|
2813
|
+
} : void 0);
|
|
1900
2814
|
return {
|
|
1901
2815
|
text: base,
|
|
1902
2816
|
space: false,
|
|
1903
2817
|
start: time?.start,
|
|
1904
2818
|
end: time?.end,
|
|
1905
2819
|
language: baseLanguage,
|
|
1906
|
-
rubies: tokens.length
|
|
2820
|
+
rubies: tokens.length ? [{
|
|
1907
2821
|
start: rubyTime?.start,
|
|
1908
2822
|
end: rubyTime?.end,
|
|
1909
2823
|
language: rubyLanguage,
|
|
1910
2824
|
tokens
|
|
1911
|
-
}]
|
|
2825
|
+
}] : []
|
|
1912
2826
|
};
|
|
1913
2827
|
};
|
|
1914
2828
|
/**
|
|
@@ -1932,7 +2846,7 @@ var readWords = (element, preserveSpace, language) => {
|
|
|
1932
2846
|
if (getXmlAttribute(child, "role") !== void 0) continue;
|
|
1933
2847
|
if (getXmlAttribute(child, "ruby") === "container") {
|
|
1934
2848
|
const ruby = readRuby(child, preserveSpace, language);
|
|
1935
|
-
if (ruby
|
|
2849
|
+
if (ruby) {
|
|
1936
2850
|
for (const part of splitWords(ruby.text)) words.push(part.space ? part : {
|
|
1937
2851
|
...ruby,
|
|
1938
2852
|
text: part.text
|
|
@@ -1946,7 +2860,7 @@ var readWords = (element, preserveSpace, language) => {
|
|
|
1946
2860
|
if (!text || !childSpace && isFormattingWhitespace(text)) continue;
|
|
1947
2861
|
const time = readSpanTime(child);
|
|
1948
2862
|
for (const part of splitWords(text, readLanguage(child, language))) {
|
|
1949
|
-
if (!part.space && time
|
|
2863
|
+
if (!part.space && time) {
|
|
1950
2864
|
part.start = time.start;
|
|
1951
2865
|
part.end = time.end;
|
|
1952
2866
|
wordLevel = true;
|
|
@@ -1986,7 +2900,7 @@ var readLine = (element, preserveSpace, language) => {
|
|
|
1986
2900
|
preserveSpace = readPreserveSpace(element, preserveSpace);
|
|
1987
2901
|
language = readLanguage(element, language);
|
|
1988
2902
|
const time = readLineTime(element);
|
|
1989
|
-
if (time
|
|
2903
|
+
if (!time) return;
|
|
1990
2904
|
const draft = {
|
|
1991
2905
|
start: time.start,
|
|
1992
2906
|
end: time.end,
|
|
@@ -2027,7 +2941,7 @@ var readBackground = (element, preserveSpace, language) => {
|
|
|
2027
2941
|
preserveSpace = readPreserveSpace(element, preserveSpace);
|
|
2028
2942
|
language = readLanguage(element, language);
|
|
2029
2943
|
const read = readWords(element, preserveSpace, language);
|
|
2030
|
-
if (read.words.length
|
|
2944
|
+
if (!read.words.length) return;
|
|
2031
2945
|
const begin = getXmlAttribute(element, "begin");
|
|
2032
2946
|
const end = getXmlAttribute(element, "end");
|
|
2033
2947
|
const first = read.words.find((word) => {
|
|
@@ -2069,7 +2983,7 @@ var buildBackground = (draft) => {
|
|
|
2069
2983
|
end: draft.end
|
|
2070
2984
|
}),
|
|
2071
2985
|
agents: draft.agentId === void 0 ? [] : [draft.agentId],
|
|
2072
|
-
languages: draft.language
|
|
2986
|
+
languages: draft.language ? [draft.language] : [],
|
|
2073
2987
|
content: buildContent(draft),
|
|
2074
2988
|
annotation: buildLineAnnotation(draft)
|
|
2075
2989
|
});
|
|
@@ -2085,7 +2999,7 @@ var buildLine = (draft) => {
|
|
|
2085
2999
|
end: draft.end
|
|
2086
3000
|
}),
|
|
2087
3001
|
agents: draft.agentId === void 0 ? [] : [draft.agentId],
|
|
2088
|
-
languages: draft.language
|
|
3002
|
+
languages: draft.language ? [draft.language] : [],
|
|
2089
3003
|
content: buildContent(draft),
|
|
2090
3004
|
annotation: buildLineAnnotation(draft),
|
|
2091
3005
|
backgrounds: draft.backgrounds.map(buildBackground)
|
|
@@ -2105,7 +3019,7 @@ var readAgents = (elements) => {
|
|
|
2105
3019
|
language: readLanguage(name, language)
|
|
2106
3020
|
});
|
|
2107
3021
|
}).filter((name) => {
|
|
2108
|
-
return name.text
|
|
3022
|
+
return !!name.text;
|
|
2109
3023
|
});
|
|
2110
3024
|
const kind = readAgentType(getXmlAttribute(element, "type"));
|
|
2111
3025
|
agents.push(makeAgent({
|
|
@@ -2129,9 +3043,9 @@ var readCredits = (elements, meta) => {
|
|
|
2129
3043
|
language
|
|
2130
3044
|
});
|
|
2131
3045
|
}).filter((name) => {
|
|
2132
|
-
return name.text
|
|
3046
|
+
return !!name.text;
|
|
2133
3047
|
});
|
|
2134
|
-
if (names.length
|
|
3048
|
+
if (!names.length) return;
|
|
2135
3049
|
meta.credits.push(makeMetaCredit({
|
|
2136
3050
|
role: CreditRole.LYRICIST,
|
|
2137
3051
|
names
|
|
@@ -2185,7 +3099,7 @@ var applyAmllMeta = (meta, element, language) => {
|
|
|
2185
3099
|
* A replacement translation rewrites the line's text, which is how a source swaps one script for another.
|
|
2186
3100
|
*/
|
|
2187
3101
|
var attachHeadAnnotations = (metadata, lineMap, backgroundMap) => {
|
|
2188
|
-
if (lineMap.size
|
|
3102
|
+
if (!lineMap.size) return;
|
|
2189
3103
|
const attach = (blocks, kind) => {
|
|
2190
3104
|
for (const { element: block, preserveSpace, language } of blocks) {
|
|
2191
3105
|
const type = getXmlQualifiedAttribute(block, "type") ?? getXmlAttribute(block, "type");
|
|
@@ -2254,9 +3168,9 @@ var parseTtml = (input, options = {}) => {
|
|
|
2254
3168
|
const language = readLanguage(root, lowerTag(options.language));
|
|
2255
3169
|
const body = findElements(root, "body", false, true, language)[0];
|
|
2256
3170
|
const metadata = findElements(root, "metadata", false, true, language)[0];
|
|
2257
|
-
const metadataElements = metadata
|
|
3171
|
+
const metadataElements = metadata ? groupElements(metadata.element, METADATA_NAMES, metadata.preserveSpace, false, metadata.language) : /* @__PURE__ */ new Map();
|
|
2258
3172
|
const meta = makeMeta();
|
|
2259
|
-
if (language
|
|
3173
|
+
if (language) meta.language = language;
|
|
2260
3174
|
readCredits(metadataElements.get("songwriter") ?? [], meta);
|
|
2261
3175
|
for (const { element, language: itemLanguage } of metadataElements.get("meta") ?? []) applyAmllMeta(meta, element, itemLanguage);
|
|
2262
3176
|
const drafts = [];
|
|
@@ -2270,7 +3184,7 @@ var parseTtml = (input, options = {}) => {
|
|
|
2270
3184
|
for (const background of draft.backgrounds) if (background.key) backgroundMap.set(background.key, background);
|
|
2271
3185
|
}
|
|
2272
3186
|
attachHeadAnnotations(metadataElements, lineMap, backgroundMap);
|
|
2273
|
-
if (drafts.length
|
|
3187
|
+
if (!drafts.length) return finalizeLyric(makeLyric({
|
|
2274
3188
|
format: FORMAT,
|
|
2275
3189
|
status: LyricStatus.INVALID,
|
|
2276
3190
|
meta,
|
|
@@ -2301,6 +3215,7 @@ var parseTtml = (input, options = {}) => {
|
|
|
2301
3215
|
* Unrecognized text returns `undefined`.
|
|
2302
3216
|
*/
|
|
2303
3217
|
var detectFormat = (input) => {
|
|
3218
|
+
if (detectQrc(input)) return "qrc";
|
|
2304
3219
|
if (detectLrc(input)) return "lrc";
|
|
2305
3220
|
if (detectTtml(input)) return "ttml";
|
|
2306
3221
|
};
|
|
@@ -2313,6 +3228,7 @@ var parse = (input, options = {}) => {
|
|
|
2313
3228
|
if (!format) return null;
|
|
2314
3229
|
switch (format) {
|
|
2315
3230
|
case "lrc": return parseLrc(input, options);
|
|
3231
|
+
case "qrc": return parseQrc(input, options);
|
|
2316
3232
|
case "ttml": return parseTtml(input, options);
|
|
2317
3233
|
}
|
|
2318
3234
|
};
|
|
@@ -2323,7 +3239,7 @@ var GENERATORS = {
|
|
|
2323
3239
|
/**
|
|
2324
3240
|
* Generates text in the requested format, which need not match `lyric.format`.
|
|
2325
3241
|
*
|
|
2326
|
-
* The writer map pairs
|
|
3242
|
+
* The writer map pairs options with results only for formats that have an implemented writer.
|
|
2327
3243
|
* An unknown target returns `null`.
|
|
2328
3244
|
*/
|
|
2329
3245
|
var generate = (lyric, target, options = {}) => {
|
|
@@ -2331,6 +3247,6 @@ var generate = (lyric, target, options = {}) => {
|
|
|
2331
3247
|
return GENERATORS[target](lyric, options);
|
|
2332
3248
|
};
|
|
2333
3249
|
//#endregion
|
|
2334
|
-
export { detectFormat, detectLrc, detectTtml, generate, generateLrc, generateTtml, parse, parseLrc, parseTtml };
|
|
3250
|
+
export { detectFormat, detectLrc, detectQrc, detectTtml, generate, generateLrc, generateTtml, parse, parseLrc, parseQrc, parseTtml, readLrc };
|
|
2335
3251
|
|
|
2336
3252
|
//# sourceMappingURL=formats.js.map
|