, * unmatched_labels: string[], * distributed_text: string * } */ public function pair(Song $localSong, string $ccliRawText, string $arrangementName = 'normal'): array { $parsed = $this->parser->parse($ccliRawText); $localSong->loadMissing(['arrangements.arrangementLabels.label.songSlides']); $arrangement = $this->findArrangement($localSong, $arrangementName); if ($arrangement === null) { return [ 'song' => $localSong, 'mapping' => [], 'unmatched_labels' => [], 'distributed_text' => '', ]; } $ccliByCanonical = $this->sectionsByCanonicalLabel($parsed->sections); $mapping = []; $unmatchedLabels = []; $allDistributedLines = []; foreach ($arrangement->arrangementLabels->sortBy('order') as $arrangementLabel) { $label = $arrangementLabel->label; if ($label === null) { continue; } $localCanonical = $this->canonicalLabel($label->name, null); $matchedSection = $ccliByCanonical[$localCanonical] ?? null; $slides = $label->songSlides->sortBy('order')->values(); if ($matchedSection === null) { $unmatchedLabels[] = $label->name; $distributedLines = array_fill(0, max($slides->count(), 1), ''); } else { $distributedLines = $this->distributeLines( $matchedSection->linesTranslated ?? $matchedSection->lines, $slides, ); } $allDistributedLines = array_merge($allDistributedLines, $distributedLines); $mapping[] = [ 'local_label' => $label->name, 'ccli_label' => $matchedSection?->label, 'distributed_lines' => $distributedLines, ]; } return [ 'song' => $localSong, 'mapping' => $mapping, 'unmatched_labels' => $unmatchedLabels, 'distributed_text' => implode("\n", $allDistributedLines), ]; } private function findArrangement(Song $localSong, string $arrangementName): ?SongArrangement { return $localSong->arrangements->where('name', $arrangementName)->first() ?? $localSong->arrangements->where('is_default', true)->first() ?? $localSong->arrangements->first(); } /** * @param ParsedCcliSection[] $sections * @return array */ private function sectionsByCanonicalLabel(array $sections): array { $byCanonical = []; foreach ($sections as $section) { $canonical = $this->canonicalLabel($section->kind, $section->number); $byCanonical[$canonical] ??= $section; } return $byCanonical; } private function canonicalLabel(string $kind, ?string $number): string { $label = trim($kind.' '.($number ?? '')); return mb_strtolower(CcliLabels::normalizeLabelName($label)); } /** * Distribute CCLI lines into local slide slots, preserving each local slide line count. * * @param string[] $lines * @param Collection $slides * @return string[] */ private function distributeLines(array $lines, Collection $slides): array { if ($slides->isEmpty()) { return $lines; } $distributed = []; $offset = 0; $lastSlideIndex = $slides->count() - 1; foreach ($slides as $index => $slide) { $lineCount = max(count(explode("\n", $slide->text_content ?? '')), 1); $chunk = array_slice($lines, $offset, $lineCount); $offset += $lineCount; if ($index === $lastSlideIndex && $offset < count($lines)) { $chunk = array_merge($chunk, array_slice($lines, $offset)); } $distributed[] = implode("\n", $chunk); } return $distributed; } }