'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'); });