@gondola-lyrics/kit 0.3.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/README.md +4 -3
- package/dist/formats.d.ts +110 -5
- package/dist/formats.js +1188 -266
- package/dist/formats.js.map +1 -1
- package/dist/generator.d.ts +7 -3
- package/dist/index.d.ts +320 -5
- package/dist/index.js +3 -1
- package/dist/language.js +117 -0
- package/dist/language.js.map +1 -0
- package/dist/parser.d.ts +314 -3
- package/dist/parser.js +3 -1
- package/dist/transforms.d.ts +257 -1
- package/dist/transforms.js +3307 -0
- package/dist/transforms.js.map +1 -0
- 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,17 +1589,26 @@ var readWords$1 = (text, lineStart) => {
|
|
|
1182
1589
|
};
|
|
1183
1590
|
};
|
|
1184
1591
|
/**
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
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.
|
|
1187
1594
|
*/
|
|
1188
1595
|
var readLine$1 = (text, start, nextStart) => {
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1596
|
+
const trimmed = text.trim();
|
|
1597
|
+
let end = nextStart !== void 0 && nextStart >= start ? nextStart : void 0;
|
|
1598
|
+
if (!trimmed) return makeLineInstrumental({ time: makeTimeRange({
|
|
1599
|
+
start,
|
|
1600
|
+
end
|
|
1601
|
+
}) });
|
|
1602
|
+
const pieces = splitWordTags(text);
|
|
1603
|
+
const wordTimed = pieces.some((piece) => !!piece.tag);
|
|
1604
|
+
const { drafts, closeSegment } = wordTimed ? readWords$2(pieces, start) : {
|
|
1191
1605
|
drafts: [],
|
|
1192
1606
|
closeSegment: () => {}
|
|
1193
1607
|
};
|
|
1194
|
-
let end = nextStart !== void 0 && nextStart >= start ? nextStart : void 0;
|
|
1195
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
|
+
}
|
|
1196
1612
|
closeSegment(end);
|
|
1197
1613
|
const words = drafts.map((draft) => {
|
|
1198
1614
|
if (draft.space) return makeWordSpace({ text: draft.text });
|
|
@@ -1209,7 +1625,7 @@ var readLine$1 = (text, start, nextStart) => {
|
|
|
1209
1625
|
start,
|
|
1210
1626
|
end
|
|
1211
1627
|
});
|
|
1212
|
-
const content = wordTimed ? makeLineContentWords(words) : makeLineContentText(
|
|
1628
|
+
const content = wordTimed ? makeLineContentWords(words) : makeLineContentText(trimmed);
|
|
1213
1629
|
return makeLineNormal({
|
|
1214
1630
|
time,
|
|
1215
1631
|
content
|
|
@@ -1217,39 +1633,108 @@ var readLine$1 = (text, start, nextStart) => {
|
|
|
1217
1633
|
};
|
|
1218
1634
|
/**
|
|
1219
1635
|
* Builds one line per timed row, so an annotation track reuses the original text's reading unchanged.
|
|
1220
|
-
*
|
|
1636
|
+
* Empty timed rows become instrumental lines bounded by the next timestamp when one is usable.
|
|
1221
1637
|
*/
|
|
1222
|
-
var readLines = (timed) => {
|
|
1638
|
+
var readLines$1 = (timed, options) => {
|
|
1223
1639
|
const lines = [];
|
|
1224
1640
|
for (const [index, item] of timed.entries()) {
|
|
1225
1641
|
const next = timed[index + 1]?.start;
|
|
1226
|
-
|
|
1642
|
+
const line = readLine$1(item.text, item.start, next);
|
|
1643
|
+
lines.push(line);
|
|
1644
|
+
options.onLine?.(line, readLineTiming(item.tag));
|
|
1227
1645
|
}
|
|
1228
1646
|
return lines;
|
|
1229
1647
|
};
|
|
1230
|
-
|
|
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 = {}) => {
|
|
1231
1658
|
const { timed, meta } = readTimedLines(text);
|
|
1232
|
-
|
|
1233
|
-
|
|
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({
|
|
1234
1719
|
format: "lrc",
|
|
1235
1720
|
status: LyricStatus.INVALID,
|
|
1236
|
-
meta
|
|
1721
|
+
meta
|
|
1237
1722
|
});
|
|
1238
|
-
const lines = readLines(timed);
|
|
1239
1723
|
const wordTimed = lines.some((line) => (line.content?.words.length ?? 0) > 0);
|
|
1240
1724
|
return makeLyric({
|
|
1241
1725
|
format: "lrc",
|
|
1242
1726
|
status: LyricStatus.VALID,
|
|
1243
1727
|
timing: wordTimed ? Timing.WORD : Timing.LINE,
|
|
1244
|
-
meta
|
|
1728
|
+
meta,
|
|
1245
1729
|
languages: deriveLanguageUsages(lines),
|
|
1246
1730
|
lines
|
|
1247
1731
|
});
|
|
1248
1732
|
};
|
|
1249
1733
|
/**
|
|
1250
|
-
*
|
|
1734
|
+
* Empty timed rows become instrumental lines; source order and missing word times remain for model validation.
|
|
1251
1735
|
*
|
|
1252
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.
|
|
1253
1738
|
*
|
|
1254
1739
|
* `INVALID` means no readable timed lines; the returned diagnostics carry whatever the canonical tree breaks.
|
|
1255
1740
|
*/
|
|
@@ -1257,19 +1742,450 @@ var parseLrc = (input, options = {}) => {
|
|
|
1257
1742
|
const source = readLrcContent(input) ?? { original: "" };
|
|
1258
1743
|
const lyric = parseLrcText(source.original);
|
|
1259
1744
|
if (lyric.status === LyricStatus.INVALID) return finalizeLyric(lyric);
|
|
1260
|
-
const translations = source.translations
|
|
1261
|
-
const romans = source.romans
|
|
1262
|
-
if (translations.length
|
|
1263
|
-
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, {
|
|
1264
1749
|
translations,
|
|
1265
1750
|
romans
|
|
1266
|
-
}, {
|
|
1751
|
+
}, {
|
|
1752
|
+
tolerance: options.annotationTolerance,
|
|
1753
|
+
wordTolerance: options.wordAnnotationTolerance,
|
|
1754
|
+
wordFallback: options.wordAnnotationFallback
|
|
1755
|
+
});
|
|
1267
1756
|
return finalizeLyric({
|
|
1268
1757
|
...lyric,
|
|
1269
1758
|
lines
|
|
1270
1759
|
});
|
|
1271
1760
|
};
|
|
1272
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
|
|
1273
2189
|
//#region src/formats/ttml/common.ts
|
|
1274
2190
|
var FORMAT = "ttml";
|
|
1275
2191
|
var AMLL_HINT_PATTERN = /xmlns:amll|amll:meta/iu;
|
|
@@ -1398,7 +2314,7 @@ var renderTime = (time, offset) => {
|
|
|
1398
2314
|
* Writes known bounds without inventing an end.
|
|
1399
2315
|
*/
|
|
1400
2316
|
var renderTimeAttributes = (time, offset) => {
|
|
1401
|
-
if (time
|
|
2317
|
+
if (!time) throw new Error("TTML sung lines, words, and ruby tokens require a time range");
|
|
1402
2318
|
const attributes = [["begin", renderTime(time.start, offset)]];
|
|
1403
2319
|
if (time.end !== void 0) attributes.push(["end", renderTime(time.end, offset)]);
|
|
1404
2320
|
return attributes;
|
|
@@ -1420,7 +2336,7 @@ var renderWord = (word, context, language) => {
|
|
|
1420
2336
|
if (word.type === WordType.SPACE) return createXmlText(word.text);
|
|
1421
2337
|
const attributes = [...renderTimeAttributes(word.time, context.offset), ...renderLanguage(word.language, language)];
|
|
1422
2338
|
const ruby = context.profile === "amll" ? word.annotation?.rubies[0] : void 0;
|
|
1423
|
-
if (ruby
|
|
2339
|
+
if (!ruby) return createXmlElement("span", attributes, [createXmlText(word.text)]);
|
|
1424
2340
|
const tokens = ruby.tokens.map((token) => {
|
|
1425
2341
|
return createXmlElement("span", [["tts:ruby", RUBY_TEXT], ...renderTimeAttributes(resolveAnnotationTokenTime(token, ruby, word), context.offset)], [createXmlText(token.text)]);
|
|
1426
2342
|
});
|
|
@@ -1435,7 +2351,7 @@ var renderWord = (word, context, language) => {
|
|
|
1435
2351
|
*/
|
|
1436
2352
|
var renderContent = (line, context) => {
|
|
1437
2353
|
const content = line.content;
|
|
1438
|
-
if (content
|
|
2354
|
+
if (!content) throw new Error("TTML sung lines require content");
|
|
1439
2355
|
if (content.text !== void 0) return [createXmlText(content.text)];
|
|
1440
2356
|
return content.words.map((word) => {
|
|
1441
2357
|
return renderWord(word, context, getLineLanguage(line));
|
|
@@ -1444,10 +2360,10 @@ var renderContent = (line, context) => {
|
|
|
1444
2360
|
/**
|
|
1445
2361
|
* Keeps annotations outside the sung word stream.
|
|
1446
2362
|
*/
|
|
1447
|
-
var renderInlineAnnotations = (
|
|
2363
|
+
var renderInlineAnnotations = (line, language) => {
|
|
1448
2364
|
const nodes = [];
|
|
1449
|
-
for (const item of annotation?.translations ?? []) nodes.push(createXmlElement("span", [["ttm:role", ROLE_TRANSLATION], ...renderLanguage(item.language, language)], [createXmlText(item.text)]));
|
|
1450
|
-
for (const item of
|
|
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)]));
|
|
1451
2367
|
return nodes;
|
|
1452
2368
|
};
|
|
1453
2369
|
/**
|
|
@@ -1463,7 +2379,7 @@ var renderLineAttributes = (line, context) => {
|
|
|
1463
2379
|
const lead = line.agents[0];
|
|
1464
2380
|
if (lead !== void 0) {
|
|
1465
2381
|
const id = context.agents.get(lead);
|
|
1466
|
-
if (id
|
|
2382
|
+
if (!id) throw new Error("TTML performing agents must have a declaration");
|
|
1467
2383
|
attributes.push(["ttm:agent", id]);
|
|
1468
2384
|
}
|
|
1469
2385
|
return attributes;
|
|
@@ -1476,7 +2392,7 @@ var createLyricElement = (name, attributes, children) => {
|
|
|
1476
2392
|
};
|
|
1477
2393
|
var renderBackground = (line, context) => {
|
|
1478
2394
|
const children = renderContent(line, context);
|
|
1479
|
-
if (context.profile === "amll") children.push(...renderInlineAnnotations(line
|
|
2395
|
+
if (context.profile === "amll") children.push(...renderInlineAnnotations(line, getLineLanguage(line)));
|
|
1480
2396
|
return createXmlElement("span", [["ttm:role", ROLE_BACKGROUND], ...renderLineAttributes(line, context)], children);
|
|
1481
2397
|
};
|
|
1482
2398
|
var renderLine = (line, context) => {
|
|
@@ -1484,7 +2400,7 @@ var renderLine = (line, context) => {
|
|
|
1484
2400
|
const after = [];
|
|
1485
2401
|
for (const background of line.backgrounds) {
|
|
1486
2402
|
const node = renderBackground(background, context);
|
|
1487
|
-
if (background.time
|
|
2403
|
+
if (background.time && line.time && background.time.start < line.time.start) before.push(node);
|
|
1488
2404
|
else after.push(node);
|
|
1489
2405
|
}
|
|
1490
2406
|
const children = [
|
|
@@ -1492,7 +2408,7 @@ var renderLine = (line, context) => {
|
|
|
1492
2408
|
...renderContent(line, context),
|
|
1493
2409
|
...after
|
|
1494
2410
|
];
|
|
1495
|
-
if (context.profile === "amll") children.push(...renderInlineAnnotations(line
|
|
2411
|
+
if (context.profile === "amll") children.push(...renderInlineAnnotations(line, getLineLanguage(line)));
|
|
1496
2412
|
return createLyricElement("p", renderLineAttributes(line, context), children);
|
|
1497
2413
|
};
|
|
1498
2414
|
var renderAgents = (lyric, context) => {
|
|
@@ -1527,10 +2443,10 @@ var renderSongwriters = (meta) => {
|
|
|
1527
2443
|
if (credit.role !== CreditRole.LYRICIST) continue;
|
|
1528
2444
|
for (const name of credit.names) names.push(createXmlElement("songwriter", renderLanguage(name.language), [createXmlText(name.text)]));
|
|
1529
2445
|
}
|
|
1530
|
-
return names.length
|
|
2446
|
+
return names.length ? [createXmlElement("songwriters", [], names)] : [];
|
|
1531
2447
|
};
|
|
1532
2448
|
var renderAnnotationTracks = (tracks, kind, context) => {
|
|
1533
|
-
if (tracks.size
|
|
2449
|
+
if (!tracks.size) return [];
|
|
1534
2450
|
const translated = kind === "translations";
|
|
1535
2451
|
const blocks = Array.from(tracks, ([language, track]) => {
|
|
1536
2452
|
const attributes = translated ? [["type", "subtitle"]] : [];
|
|
@@ -1550,26 +2466,29 @@ var renderHeadAnnotations = (lines, context) => {
|
|
|
1550
2466
|
translations: /* @__PURE__ */ new Map(),
|
|
1551
2467
|
romans: /* @__PURE__ */ new Map()
|
|
1552
2468
|
};
|
|
1553
|
-
const append = (
|
|
1554
|
-
|
|
2469
|
+
const append = (source, line, background) => {
|
|
2470
|
+
const annotation = {
|
|
2471
|
+
translations: source.annotation?.translations ?? [],
|
|
2472
|
+
romans: getLineRomans(source)
|
|
2473
|
+
};
|
|
1555
2474
|
for (const kind of ANNOTATION_KINDS) for (const item of annotation[kind]) {
|
|
1556
2475
|
const language = item.language?.toLowerCase();
|
|
1557
2476
|
let track = tracks[kind].get(language);
|
|
1558
|
-
if (track
|
|
2477
|
+
if (!track) {
|
|
1559
2478
|
track = /* @__PURE__ */ new Map();
|
|
1560
2479
|
tracks[kind].set(language, track);
|
|
1561
2480
|
}
|
|
1562
2481
|
let nodes = track.get(line);
|
|
1563
|
-
if (nodes
|
|
2482
|
+
if (!nodes) {
|
|
1564
2483
|
nodes = [];
|
|
1565
2484
|
track.set(line, nodes);
|
|
1566
2485
|
}
|
|
1567
|
-
nodes.push(background
|
|
2486
|
+
nodes.push(!background ? createXmlText(item.text) : createXmlElement("span", [["ttm:role", ROLE_BACKGROUND], ["for", context.keys.get(background)]], [createXmlText(item.text)]));
|
|
1568
2487
|
}
|
|
1569
2488
|
};
|
|
1570
2489
|
for (const line of lines) {
|
|
1571
|
-
append(line
|
|
1572
|
-
for (const background of line.backgrounds) append(background
|
|
2490
|
+
append(line, line);
|
|
2491
|
+
for (const background of line.backgrounds) append(background, line, background);
|
|
1573
2492
|
}
|
|
1574
2493
|
return ANNOTATION_KINDS.flatMap((kind) => {
|
|
1575
2494
|
return renderAnnotationTracks(tracks[kind], kind, context);
|
|
@@ -1578,12 +2497,12 @@ var renderHeadAnnotations = (lines, context) => {
|
|
|
1578
2497
|
var renderMetadata = (lyric, lines, context, includeMeta) => {
|
|
1579
2498
|
const metadata = renderAgents(lyric, context);
|
|
1580
2499
|
const itunes = [];
|
|
1581
|
-
if (includeMeta && lyric.meta
|
|
2500
|
+
if (includeMeta && lyric.meta) {
|
|
1582
2501
|
if (context.profile === "amll") metadata.push(...renderAmllMeta(lyric.meta));
|
|
1583
2502
|
itunes.push(...renderSongwriters(lyric.meta));
|
|
1584
2503
|
}
|
|
1585
2504
|
if (context.profile === "itunes") itunes.push(...renderHeadAnnotations(lines, context));
|
|
1586
|
-
if (itunes.length
|
|
2505
|
+
if (itunes.length) metadata.push(createXmlElement("iTunesMetadata", [["xmlns", NAMESPACE_ITUNES_METADATA]], itunes));
|
|
1587
2506
|
return createXmlElement("metadata", renderLanguage(void 0, lyric.meta?.language), metadata);
|
|
1588
2507
|
};
|
|
1589
2508
|
var renderRootAttributes = (lyric, profile) => {
|
|
@@ -1602,6 +2521,7 @@ var renderRootAttributes = (lyric, profile) => {
|
|
|
1602
2521
|
*
|
|
1603
2522
|
* Offsets are baked into timestamps; a negative result or a missing required time throws.
|
|
1604
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.
|
|
1605
2525
|
*/
|
|
1606
2526
|
var generateTtml = (lyric, options = {}) => {
|
|
1607
2527
|
if (lyric.timing !== Timing.LINE && lyric.timing !== Timing.WORD) throw new Error("TTML generation requires line or word timing");
|
|
@@ -1685,7 +2605,7 @@ var readText = (element, inherited) => {
|
|
|
1685
2605
|
* Preserves the parser's whitespace segmentation when turning text into word drafts.
|
|
1686
2606
|
*/
|
|
1687
2607
|
var splitWords = (text, language) => {
|
|
1688
|
-
if (text
|
|
2608
|
+
if (!text) return [];
|
|
1689
2609
|
if (!WORD_SEPARATOR_PATTERN.test(text)) return [{
|
|
1690
2610
|
text,
|
|
1691
2611
|
space: false,
|
|
@@ -1693,7 +2613,7 @@ var splitWords = (text, language) => {
|
|
|
1693
2613
|
rubies: []
|
|
1694
2614
|
}];
|
|
1695
2615
|
const words = [];
|
|
1696
|
-
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) {
|
|
1697
2617
|
const space = /^\p{White_Space}+$/u.test(run);
|
|
1698
2618
|
words.push({
|
|
1699
2619
|
text: run,
|
|
@@ -1708,7 +2628,7 @@ var splitWords = (text, language) => {
|
|
|
1708
2628
|
* Distinguishes indentation runs from separators using the parser's existing rule.
|
|
1709
2629
|
*/
|
|
1710
2630
|
var isFormattingWhitespace = (text) => {
|
|
1711
|
-
return text.trim()
|
|
2631
|
+
return !text.trim() && text.includes("\n");
|
|
1712
2632
|
};
|
|
1713
2633
|
/**
|
|
1714
2634
|
* Builds words from source drafts without deriving missing timestamps.
|
|
@@ -1724,7 +2644,7 @@ var buildWords = (draft) => {
|
|
|
1724
2644
|
start: word.start,
|
|
1725
2645
|
end: word.end
|
|
1726
2646
|
});
|
|
1727
|
-
if (word.rubies.length
|
|
2647
|
+
if (!word.rubies.length) {
|
|
1728
2648
|
words.push(makeWordNormal({
|
|
1729
2649
|
text: word.text,
|
|
1730
2650
|
time,
|
|
@@ -1763,7 +2683,7 @@ var buildWords = (draft) => {
|
|
|
1763
2683
|
* Builds only annotations explicitly present in the source drafts.
|
|
1764
2684
|
*/
|
|
1765
2685
|
var buildLineAnnotation = (draft) => {
|
|
1766
|
-
if (draft.translations.length
|
|
2686
|
+
if (!draft.translations.length && !draft.romans.length) return;
|
|
1767
2687
|
return makeLineAnnotation({
|
|
1768
2688
|
translations: draft.translations.map((item) => {
|
|
1769
2689
|
return makeLineAnnotationTranslation({
|
|
@@ -1814,9 +2734,9 @@ var groupElements = (root, names, preserveSpace, stopAtMatch = false, language)
|
|
|
1814
2734
|
preserveSpace,
|
|
1815
2735
|
language
|
|
1816
2736
|
}];
|
|
1817
|
-
while (stack.length
|
|
2737
|
+
while (stack.length) {
|
|
1818
2738
|
const entry = stack.pop();
|
|
1819
|
-
if (entry
|
|
2739
|
+
if (!entry) continue;
|
|
1820
2740
|
const element = entry.element;
|
|
1821
2741
|
const currentSpace = readPreserveSpace(element, entry.preserveSpace);
|
|
1822
2742
|
const currentLanguage = readLanguage(element, entry.language);
|
|
@@ -1827,7 +2747,7 @@ var groupElements = (root, names, preserveSpace, stopAtMatch = false, language)
|
|
|
1827
2747
|
preserveSpace: currentSpace,
|
|
1828
2748
|
language: currentLanguage
|
|
1829
2749
|
};
|
|
1830
|
-
if (group
|
|
2750
|
+
if (!group) result.set(element.localName, [context]);
|
|
1831
2751
|
else group.push(context);
|
|
1832
2752
|
if (stopAtMatch) continue;
|
|
1833
2753
|
}
|
|
@@ -1887,22 +2807,22 @@ var readRuby = (container, preserveSpace, language) => {
|
|
|
1887
2807
|
const last = tokens.findLast((token) => {
|
|
1888
2808
|
return token.start !== void 0;
|
|
1889
2809
|
});
|
|
1890
|
-
const time = readSpanTime(container) ?? rubyTime ?? (first
|
|
2810
|
+
const time = readSpanTime(container) ?? rubyTime ?? (first ? {
|
|
1891
2811
|
start: first.start,
|
|
1892
2812
|
end: last?.end
|
|
1893
|
-
});
|
|
2813
|
+
} : void 0);
|
|
1894
2814
|
return {
|
|
1895
2815
|
text: base,
|
|
1896
2816
|
space: false,
|
|
1897
2817
|
start: time?.start,
|
|
1898
2818
|
end: time?.end,
|
|
1899
2819
|
language: baseLanguage,
|
|
1900
|
-
rubies: tokens.length
|
|
2820
|
+
rubies: tokens.length ? [{
|
|
1901
2821
|
start: rubyTime?.start,
|
|
1902
2822
|
end: rubyTime?.end,
|
|
1903
2823
|
language: rubyLanguage,
|
|
1904
2824
|
tokens
|
|
1905
|
-
}]
|
|
2825
|
+
}] : []
|
|
1906
2826
|
};
|
|
1907
2827
|
};
|
|
1908
2828
|
/**
|
|
@@ -1926,7 +2846,7 @@ var readWords = (element, preserveSpace, language) => {
|
|
|
1926
2846
|
if (getXmlAttribute(child, "role") !== void 0) continue;
|
|
1927
2847
|
if (getXmlAttribute(child, "ruby") === "container") {
|
|
1928
2848
|
const ruby = readRuby(child, preserveSpace, language);
|
|
1929
|
-
if (ruby
|
|
2849
|
+
if (ruby) {
|
|
1930
2850
|
for (const part of splitWords(ruby.text)) words.push(part.space ? part : {
|
|
1931
2851
|
...ruby,
|
|
1932
2852
|
text: part.text
|
|
@@ -1940,7 +2860,7 @@ var readWords = (element, preserveSpace, language) => {
|
|
|
1940
2860
|
if (!text || !childSpace && isFormattingWhitespace(text)) continue;
|
|
1941
2861
|
const time = readSpanTime(child);
|
|
1942
2862
|
for (const part of splitWords(text, readLanguage(child, language))) {
|
|
1943
|
-
if (!part.space && time
|
|
2863
|
+
if (!part.space && time) {
|
|
1944
2864
|
part.start = time.start;
|
|
1945
2865
|
part.end = time.end;
|
|
1946
2866
|
wordLevel = true;
|
|
@@ -1980,7 +2900,7 @@ var readLine = (element, preserveSpace, language) => {
|
|
|
1980
2900
|
preserveSpace = readPreserveSpace(element, preserveSpace);
|
|
1981
2901
|
language = readLanguage(element, language);
|
|
1982
2902
|
const time = readLineTime(element);
|
|
1983
|
-
if (time
|
|
2903
|
+
if (!time) return;
|
|
1984
2904
|
const draft = {
|
|
1985
2905
|
start: time.start,
|
|
1986
2906
|
end: time.end,
|
|
@@ -2021,7 +2941,7 @@ var readBackground = (element, preserveSpace, language) => {
|
|
|
2021
2941
|
preserveSpace = readPreserveSpace(element, preserveSpace);
|
|
2022
2942
|
language = readLanguage(element, language);
|
|
2023
2943
|
const read = readWords(element, preserveSpace, language);
|
|
2024
|
-
if (read.words.length
|
|
2944
|
+
if (!read.words.length) return;
|
|
2025
2945
|
const begin = getXmlAttribute(element, "begin");
|
|
2026
2946
|
const end = getXmlAttribute(element, "end");
|
|
2027
2947
|
const first = read.words.find((word) => {
|
|
@@ -2063,7 +2983,7 @@ var buildBackground = (draft) => {
|
|
|
2063
2983
|
end: draft.end
|
|
2064
2984
|
}),
|
|
2065
2985
|
agents: draft.agentId === void 0 ? [] : [draft.agentId],
|
|
2066
|
-
languages: draft.language
|
|
2986
|
+
languages: draft.language ? [draft.language] : [],
|
|
2067
2987
|
content: buildContent(draft),
|
|
2068
2988
|
annotation: buildLineAnnotation(draft)
|
|
2069
2989
|
});
|
|
@@ -2079,7 +2999,7 @@ var buildLine = (draft) => {
|
|
|
2079
2999
|
end: draft.end
|
|
2080
3000
|
}),
|
|
2081
3001
|
agents: draft.agentId === void 0 ? [] : [draft.agentId],
|
|
2082
|
-
languages: draft.language
|
|
3002
|
+
languages: draft.language ? [draft.language] : [],
|
|
2083
3003
|
content: buildContent(draft),
|
|
2084
3004
|
annotation: buildLineAnnotation(draft),
|
|
2085
3005
|
backgrounds: draft.backgrounds.map(buildBackground)
|
|
@@ -2099,7 +3019,7 @@ var readAgents = (elements) => {
|
|
|
2099
3019
|
language: readLanguage(name, language)
|
|
2100
3020
|
});
|
|
2101
3021
|
}).filter((name) => {
|
|
2102
|
-
return name.text
|
|
3022
|
+
return !!name.text;
|
|
2103
3023
|
});
|
|
2104
3024
|
const kind = readAgentType(getXmlAttribute(element, "type"));
|
|
2105
3025
|
agents.push(makeAgent({
|
|
@@ -2123,9 +3043,9 @@ var readCredits = (elements, meta) => {
|
|
|
2123
3043
|
language
|
|
2124
3044
|
});
|
|
2125
3045
|
}).filter((name) => {
|
|
2126
|
-
return name.text
|
|
3046
|
+
return !!name.text;
|
|
2127
3047
|
});
|
|
2128
|
-
if (names.length
|
|
3048
|
+
if (!names.length) return;
|
|
2129
3049
|
meta.credits.push(makeMetaCredit({
|
|
2130
3050
|
role: CreditRole.LYRICIST,
|
|
2131
3051
|
names
|
|
@@ -2179,7 +3099,7 @@ var applyAmllMeta = (meta, element, language) => {
|
|
|
2179
3099
|
* A replacement translation rewrites the line's text, which is how a source swaps one script for another.
|
|
2180
3100
|
*/
|
|
2181
3101
|
var attachHeadAnnotations = (metadata, lineMap, backgroundMap) => {
|
|
2182
|
-
if (lineMap.size
|
|
3102
|
+
if (!lineMap.size) return;
|
|
2183
3103
|
const attach = (blocks, kind) => {
|
|
2184
3104
|
for (const { element: block, preserveSpace, language } of blocks) {
|
|
2185
3105
|
const type = getXmlQualifiedAttribute(block, "type") ?? getXmlAttribute(block, "type");
|
|
@@ -2248,9 +3168,9 @@ var parseTtml = (input, options = {}) => {
|
|
|
2248
3168
|
const language = readLanguage(root, lowerTag(options.language));
|
|
2249
3169
|
const body = findElements(root, "body", false, true, language)[0];
|
|
2250
3170
|
const metadata = findElements(root, "metadata", false, true, language)[0];
|
|
2251
|
-
const metadataElements = metadata
|
|
3171
|
+
const metadataElements = metadata ? groupElements(metadata.element, METADATA_NAMES, metadata.preserveSpace, false, metadata.language) : /* @__PURE__ */ new Map();
|
|
2252
3172
|
const meta = makeMeta();
|
|
2253
|
-
if (language
|
|
3173
|
+
if (language) meta.language = language;
|
|
2254
3174
|
readCredits(metadataElements.get("songwriter") ?? [], meta);
|
|
2255
3175
|
for (const { element, language: itemLanguage } of metadataElements.get("meta") ?? []) applyAmllMeta(meta, element, itemLanguage);
|
|
2256
3176
|
const drafts = [];
|
|
@@ -2264,7 +3184,7 @@ var parseTtml = (input, options = {}) => {
|
|
|
2264
3184
|
for (const background of draft.backgrounds) if (background.key) backgroundMap.set(background.key, background);
|
|
2265
3185
|
}
|
|
2266
3186
|
attachHeadAnnotations(metadataElements, lineMap, backgroundMap);
|
|
2267
|
-
if (drafts.length
|
|
3187
|
+
if (!drafts.length) return finalizeLyric(makeLyric({
|
|
2268
3188
|
format: FORMAT,
|
|
2269
3189
|
status: LyricStatus.INVALID,
|
|
2270
3190
|
meta,
|
|
@@ -2295,6 +3215,7 @@ var parseTtml = (input, options = {}) => {
|
|
|
2295
3215
|
* Unrecognized text returns `undefined`.
|
|
2296
3216
|
*/
|
|
2297
3217
|
var detectFormat = (input) => {
|
|
3218
|
+
if (detectQrc(input)) return "qrc";
|
|
2298
3219
|
if (detectLrc(input)) return "lrc";
|
|
2299
3220
|
if (detectTtml(input)) return "ttml";
|
|
2300
3221
|
};
|
|
@@ -2307,6 +3228,7 @@ var parse = (input, options = {}) => {
|
|
|
2307
3228
|
if (!format) return null;
|
|
2308
3229
|
switch (format) {
|
|
2309
3230
|
case "lrc": return parseLrc(input, options);
|
|
3231
|
+
case "qrc": return parseQrc(input, options);
|
|
2310
3232
|
case "ttml": return parseTtml(input, options);
|
|
2311
3233
|
}
|
|
2312
3234
|
};
|
|
@@ -2317,7 +3239,7 @@ var GENERATORS = {
|
|
|
2317
3239
|
/**
|
|
2318
3240
|
* Generates text in the requested format, which need not match `lyric.format`.
|
|
2319
3241
|
*
|
|
2320
|
-
* The writer map pairs
|
|
3242
|
+
* The writer map pairs options with results only for formats that have an implemented writer.
|
|
2321
3243
|
* An unknown target returns `null`.
|
|
2322
3244
|
*/
|
|
2323
3245
|
var generate = (lyric, target, options = {}) => {
|
|
@@ -2325,6 +3247,6 @@ var generate = (lyric, target, options = {}) => {
|
|
|
2325
3247
|
return GENERATORS[target](lyric, options);
|
|
2326
3248
|
};
|
|
2327
3249
|
//#endregion
|
|
2328
|
-
export { detectFormat, detectLrc, detectTtml, generate, generateLrc, generateTtml, parse, parseLrc, parseTtml };
|
|
3250
|
+
export { detectFormat, detectLrc, detectQrc, detectTtml, generate, generateLrc, generateTtml, parse, parseLrc, parseQrc, parseTtml, readLrc };
|
|
2329
3251
|
|
|
2330
3252
|
//# sourceMappingURL=formats.js.map
|