diff --git a/app/Support/CcliLabels.php b/app/Support/CcliLabels.php new file mode 100644 index 0000000..12e320d --- /dev/null +++ b/app/Support/CcliLabels.php @@ -0,0 +1,72 @@ + 'Verse', + 'Refrain' => 'Chorus', + 'Brücke' => 'Bridge', + 'Vorrefrain' => 'Pre-Chorus', + 'Schluss' => 'Ending', + 'Zwischenspiel' => 'Interlude', + ]; + + public static function isSectionLabel(string $line): bool + { + return (bool) preg_match(self::SECTION_LABEL_PATTERN, trim($line)); + } + + public static function isMetadataLine(string $line): bool + { + return (bool) preg_match(self::METADATA_PATTERN, $line); + } + + public static function normalizeLabelName(string $label): string + { + $trimmed = trim($label); + + if (! preg_match('/^(?Verse|Chorus|Bridge|Pre-Chorus|Tag|Ending|Intro|Interlude|Outro|Misc|Strophe|Refrain|Brücke|Vorrefrain|Schluss|Zwischenspiel)(?\s+\d+[a-z]?)?$/iu', $trimmed, $matches)) { + return $trimmed; + } + + $kind = $matches['kind']; + $suffix = $matches['suffix'] ?? ''; + + return (self::LABEL_NAME_MAP[$kind] ?? $kind).$suffix; + } + + /** + * @return array{kind: string, number: string|null, modifier: string|null}|null + */ + public static function parseLabel(string $line): ?array + { + $trimmed = trim($line); + + if (! preg_match('/^(?Verse|Chorus|Bridge|Pre-Chorus|Tag|Ending|Intro|Interlude|Outro|Misc|Strophe|Refrain|Brücke|Vorrefrain|Schluss|Zwischenspiel)(?:\s+(?\d+[a-z]?))?(?:\s*\((?Repeat|Wdh\.?)\))?(?:\s*[xX]\s*\d+)?$/iu', $trimmed, $matches)) { + return null; + } + + $modifier = $matches['modifier'] ?? null; + + return [ + 'kind' => $matches['kind'], + 'number' => $matches['number'] ?? null, + 'modifier' => $modifier !== null ? rtrim($modifier, '.') : null, + ]; + } +} diff --git a/tests/Unit/CcliLabelsTest.php b/tests/Unit/CcliLabelsTest.php new file mode 100644 index 0000000..4fc7b57 --- /dev/null +++ b/tests/Unit/CcliLabelsTest.php @@ -0,0 +1,117 @@ +toBeTrue(); +})->with([ + 'Verse 1', + 'Chorus', + 'Bridge', + 'Pre-Chorus', + 'Tag', + 'Ending', + 'Intro', + 'Interlude', + 'Outro', + 'Misc', +]); + +test('isSectionLabel detects german labels', function (string $label) { + expect(CcliLabels::isSectionLabel($label))->toBeTrue(); +})->with([ + 'Strophe 1', + 'Refrain', + 'Brücke', + 'Vorrefrain', + 'Schluss', + 'Zwischenspiel', +]); + +test('isSectionLabel detects label variants', function (string $label) { + expect(CcliLabels::isSectionLabel($label))->toBeTrue(); +})->with([ + 'Verse 2a', + 'Chorus 1 (Repeat)', + 'Bridge x2', + 'Verse 2b', +]); + +test('isSectionLabel rejects non labels', function (string $text) { + expect(CcliLabels::isSectionLabel($text))->toBeFalse(); +})->with([ + 'Random text', + 'We are singing', + 'CCLI # 123456', + '© 2024 Publisher', + '', + 'Amazing grace how sweet', +]); + +test('isMetadataLine detects metadata lines', function (string $line) { + expect(CcliLabels::isMetadataLine($line))->toBeTrue(); +})->with([ + '© 2020 Hillsong Music', + 'CCLI # 4760', + 'CCLI-Nr. 1234567', + 'ccli.com/license', + 'SongSelect Terms', + 'All rights reserved', + 'Alle Rechte vorbehalten', +]); + +test('isMetadataLine rejects normal lines', function (string $line) { + expect(CcliLabels::isMetadataLine($line))->toBeFalse(); +})->with([ + 'Verse 1', + 'Amazing grace how sweet the sound', + 'Test Song 1', +]); + +test('normalizeLabelName converts german labels to english', function (string $input, string $expected) { + expect(CcliLabels::normalizeLabelName($input))->toBe($expected); +})->with([ + ['Strophe 1', 'Verse 1'], + ['Refrain', 'Chorus'], + ['Brücke', 'Bridge'], + ['Vorrefrain', 'Pre-Chorus'], + ['Schluss', 'Ending'], + ['Zwischenspiel', 'Interlude'], +]); + +test('normalizeLabelName keeps english labels unchanged', function (string $input) { + expect(CcliLabels::normalizeLabelName($input))->toBe($input); +})->with([ + 'Verse 1', + 'Chorus', + 'Bridge', +]); + +test('normalizeLabelName keeps unknown labels unchanged', function () { + expect(CcliLabels::normalizeLabelName('Foobar'))->toBe('Foobar'); + expect(CcliLabels::normalizeLabelName(''))->toBe(''); +}); + +test('parseLabel returns structured data for labels', function (string $label, string $kind, ?string $number, ?string $modifier) { + $result = CcliLabels::parseLabel($label); + + expect($result)->not->toBeNull(); + expect($result['kind'])->toBe($kind); + expect($result['number'])->toBe($number); + expect($result['modifier'])->toBe($modifier); +})->with([ + ['Verse 1', 'Verse', '1', null], + ['Chorus', 'Chorus', null, null], + ['Verse 2a', 'Verse', '2a', null], + ['Chorus 1 (Repeat)', 'Chorus', '1', 'Repeat'], + ['Strophe 2', 'Strophe', '2', null], +]); + +test('parseLabel rejects non labels', function (string $text) { + expect(CcliLabels::parseLabel($text))->toBeNull(); +})->with([ + 'Random text', + 'CCLI # 123', + '', + 'Amazing grace', +]);