104 lines
4.9 KiB
JavaScript
104 lines
4.9 KiB
JavaScript
'use strict';
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert');
|
|
const LC = require('./lyrics-core');
|
|
|
|
test('fromText: timed, untimed, sections, cues, tags', () => {
|
|
const doc = LC.fromText([
|
|
'@ Key G, Capo 2 (Play F) | 70 BPM',
|
|
'[0:18] # Verse 1',
|
|
'[0:18.00] Worthy of every song',
|
|
'an untimed line',
|
|
'! Drums in',
|
|
'',
|
|
].join('\n'));
|
|
assert.deepStrictEqual(doc.tags, ['Key G', 'Capo 2 (Play F)', '70 BPM']);
|
|
assert.deepStrictEqual(doc.lines, [
|
|
{ t: 18, text: 'Verse 1', kind: 'section' },
|
|
{ t: 18, text: 'Worthy of every song', kind: 'line' },
|
|
{ t: null, text: 'an untimed line', kind: 'line' },
|
|
{ t: null, text: 'Drums in', kind: 'cue' },
|
|
]);
|
|
assert.strictEqual(doc.offset, null);
|
|
});
|
|
|
|
test('fromText: LRC multi-stamps expand and a fully timed sheet is time-sorted', () => {
|
|
const doc = LC.fromText('[ti:Song]\n[ar:Band]\n[offset:+500]\n[00:30.00]b\n[00:10.00][00:50.00]chorus');
|
|
assert.deepStrictEqual(doc.lines.map((l) => [l.t, l.text]), [[10, 'chorus'], [30, 'b'], [50, 'chorus']]);
|
|
assert.strictEqual(doc.offset, -0.5); // LRC +500 ms = sooner = our -0.5 s
|
|
});
|
|
|
|
test('toText round-trips through fromText', () => {
|
|
const doc = {
|
|
tags: ['Key G'],
|
|
lines: [
|
|
{ t: 83.456, text: 'line one', kind: 'line' },
|
|
{ t: null, text: 'Bridge', kind: 'section' },
|
|
{ t: 90, text: 'swell', kind: 'cue' },
|
|
],
|
|
};
|
|
const text = LC.toText(doc);
|
|
assert.strictEqual(text, '@ Key G\n[1:23.46] line one\n# Bridge\n[1:30.00] ! swell');
|
|
const back = LC.fromText(text);
|
|
assert.deepStrictEqual(back.lines.map((l) => l.kind), ['line', 'section', 'cue']);
|
|
assert.strictEqual(back.lines[0].t, 83.46);
|
|
});
|
|
|
|
test('activeIndex follows sung lines only and honours the offset', () => {
|
|
const lines = [
|
|
{ t: 10, text: 'Verse', kind: 'section' },
|
|
{ t: 10, text: 'a', kind: 'line' },
|
|
{ t: null, text: 'untimed', kind: 'line' },
|
|
{ t: 20, text: 'b', kind: 'line' },
|
|
{ t: 25, text: 'cue', kind: 'cue' },
|
|
];
|
|
assert.strictEqual(LC.activeIndex(lines, 5, 0), -1);
|
|
assert.strictEqual(LC.activeIndex(lines, 12, 0), 1);
|
|
assert.strictEqual(LC.activeIndex(lines, 26, 0), 3);
|
|
// +2 s offset: the "b" line shows at 22 s of playback, not 20.
|
|
assert.strictEqual(LC.activeIndex(lines, 21, 2), 1);
|
|
assert.strictEqual(LC.activeIndex(lines, 22, 2), 3);
|
|
assert.strictEqual(LC.activeSection(lines, 12, 0).text, 'Verse');
|
|
assert.strictEqual(LC.timedCount(lines), 2);
|
|
});
|
|
|
|
test('clock and stamp formatting', () => {
|
|
assert.strictEqual(LC.fmtClock(83.9), '1:23');
|
|
assert.strictEqual(LC.fmtClock(3723), '1:02:03');
|
|
assert.strictEqual(LC.fmtStamp(5.5), '0:05.50');
|
|
assert.strictEqual(LC.parseStamp('1:23.5'), 83.5);
|
|
assert.strictEqual(LC.parseStamp('1:02:03'), 3723);
|
|
assert.strictEqual(LC.parseStamp('nope'), null);
|
|
});
|
|
|
|
test('YouTube export adds a 0:00 entry only when needed', () => {
|
|
const items = [{ t: 130, title: 'Key change → D' }, { t: 42, title: 'Intro' }];
|
|
assert.strictEqual(LC.toYouTubeTimestamps(items), '0:00 Start\n0:42 Intro\n2:10 Key change → D');
|
|
assert.strictEqual(LC.toYouTubeTimestamps([{ t: 0, title: 'Top' }]), '0:00 Top');
|
|
assert.strictEqual(LC.activeMarker(items, 60).title, 'Intro');
|
|
assert.strictEqual(LC.activeMarker(items, 10), null);
|
|
});
|
|
|
|
|
|
test('multiline cues round-trip without adding timestamps or active rows', () => {
|
|
const doc = { lines: [{ t: 12, text: 'Because You are God\nYou can do anything', kind: 'line' }, { t: 18, text: 'Next lyric', kind: 'line' }], tags: [] };
|
|
const copy = LC.fromText(LC.toText(doc));
|
|
assert.deepStrictEqual(copy.lines, doc.lines);
|
|
assert.strictEqual(LC.activeIndex(copy.lines, 15, 0), 0);
|
|
assert.strictEqual(LC.activeIndex(copy.lines, 18, 0), 1);
|
|
assert.strictEqual(LC.timedCount(copy.lines), 2);
|
|
const repeated = LC.fromText('[0:12][0:30] Because You are God\n| You can do anything');
|
|
assert.strictEqual(repeated.lines.length, 2);
|
|
assert.strictEqual(repeated.lines[1].text, doc.lines[0].text);
|
|
});
|
|
test('bilingual lyrics and pinyin share one timed cue and round-trip', () => {
|
|
const doc = LC.fromText('[1:23.45] 主你的恩典 / Your grace is enough\n!pinyin: zhǔ nǐ de ēndiǎn');
|
|
assert.equal(doc.lines.length, 1); assert.equal(doc.lines[0].t,83.45); assert.equal(doc.lines[0].secondary,'Your grace is enough'); assert.equal(doc.lines[0].phonetic,'zhǔ nǐ de ēndiǎn');
|
|
assert.deepEqual(LC.fromText(LC.toText(doc)).lines, doc.lines);
|
|
});
|
|
test('translations preserve grouped breaks, multi-stamps and legacy slash text', () => {
|
|
const doc = LC.fromText('[0:01][0:10] 主 / Lord\n| 恩典 / Grace\n!pinyin: zhǔ\n!pinyin: ēndiǎn');
|
|
assert.equal(doc.lines.length,2); assert.equal(doc.lines[1].secondary,'Lord\nGrace'); assert.equal(doc.lines[1].phonetic,'zhǔ\nēndiǎn'); assert.deepEqual(LC.fromText(LC.toText(doc)).lines, doc.lines);
|
|
assert.equal(LC.fromText('and/or').lines[0].text,'and/or'); assert.equal(LC.fromText('! Band cue').lines[0].kind,'cue');
|
|
});
|