feat(ccli): add section-label constants and language mapping
This commit is contained in:
parent
02de6b03c0
commit
5c590eda9e
72
app/Support/CcliLabels.php
Normal file
72
app/Support/CcliLabels.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
final class CcliLabels
|
||||
{
|
||||
/**
|
||||
* Regex matching CCLI SongSelect section labels (English + German + variants).
|
||||
*/
|
||||
public const SECTION_LABEL_PATTERN = '/^(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';
|
||||
|
||||
/**
|
||||
* Regex matching CCLI footer/metadata lines (copyright, CCLI number).
|
||||
*/
|
||||
public const METADATA_PATTERN = '/©|CCLI[\s\-]|ccli\.com|SongSelect|All rights reserved|Alle Rechte vorbehalten/iu';
|
||||
|
||||
/**
|
||||
* Bidirectional English ↔ German label kind mapping.
|
||||
*/
|
||||
public const LABEL_NAME_MAP = [
|
||||
'Strophe' => '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('/^(?<kind>Verse|Chorus|Bridge|Pre-Chorus|Tag|Ending|Intro|Interlude|Outro|Misc|Strophe|Refrain|Brücke|Vorrefrain|Schluss|Zwischenspiel)(?<suffix>\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('/^(?<kind>Verse|Chorus|Bridge|Pre-Chorus|Tag|Ending|Intro|Interlude|Outro|Misc|Strophe|Refrain|Brücke|Vorrefrain|Schluss|Zwischenspiel)(?:\s+(?<number>\d+[a-z]?))?(?:\s*\((?<modifier>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,
|
||||
];
|
||||
}
|
||||
}
|
||||
117
tests/Unit/CcliLabelsTest.php
Normal file
117
tests/Unit/CcliLabelsTest.php
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
use App\Support\CcliLabels;
|
||||
|
||||
test('isSectionLabel detects english labels', function (string $label) {
|
||||
expect(CcliLabels::isSectionLabel($label))->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',
|
||||
]);
|
||||
Loading…
Reference in a new issue