[ 'teaser' => null, 'teaserLong' => 'Beschreibung', 'feature' => 'Highlights', 'conceptDescription' => 'Reisekonzept', 'boardDescription' => 'Verpflegung', 'includedServices' => 'Leistungen', 'additionalServices' => 'Zusatzleistungen', 'programmeDescription' => 'Programm', 'skiPassDescription' => 'Skipass', 'nonBookableInfo' => 'Hinweise zur Buchbarkeit', ], 'hotel' => [ 'teaser' => null, 'description' => 'Beschreibung', 'features' => 'Ausstattung', 'roomTypes' => 'Zimmer', 'additionalInformation' => 'Weitere Informationen', ], 'region' => [ 'teaser' => null, 'description' => 'Beschreibung', 'feature' => 'Highlights', 'skiArea' => 'Skigebiet', 'skiAreaExtended' => 'Skigebiet im Detail', 'skiPass' => 'Skipass', 'news' => 'Neuigkeiten', ], 'country' => [ 'teaser' => null, 'description' => 'Beschreibung', 'descriptionExtended' => 'Mehr erfahren', ], 'city' => [ 'teaser' => null, 'description' => 'Beschreibung', 'descriptionExtended' => 'Mehr erfahren', 'party' => 'Party', 'leisure' => 'Freizeit', ], 'concept' => [ 'teaser' => null, 'description' => 'Beschreibung', 'descriptionExtended' => 'Mehr erfahren', 'popupText' => 'Kurz erklärt', ], ]; /** * Facts per type, with their German label. */ private const FACTS = [ 'hotel' => [ 'bedsCount' => 'Betten', 'parkingCount' => 'Parkplätze', 'altitude' => 'Höhenlage', 'distanceBus' => 'Entfernung zur Bushaltestelle', 'distanceLift' => 'Entfernung zum Lift', 'shower' => 'Dusche/WC', 'partyroom' => 'Partyraum', 'gamesroom' => 'Spieleraum', 'sauna' => 'Sauna', 'wifi' => 'WLAN', ], 'region' => [ 'length' => 'Pistenkilometer', 'height' => 'Höhe', 'lifts' => 'Lifte', ], ]; public function __construct(private readonly HtmlToMarkdownConverter $converter) { } /** * @param array $record */ public function title(string $type, array $record): string { $title = 'news' === $type ? ($record['title'] ?? null) : ($record['name'] ?? null); return is_string($title) ? trim($title) : ''; } /** * @param array $record */ public function build(string $type, array $record): string { $baseUrl = is_string($record['url'] ?? null) ? $record['url'] : null; $blocks = []; $title = $this->title($type, $record); if ('' !== $title) { $blocks[] = '# '.$title; } if ('news' === $type) { return $this->join(array_merge($blocks, $this->newsBlocks($record, $baseUrl))); } foreach (self::LEAD_FIELDS as $field) { $value = $this->plain($record[$field] ?? null); if ('' !== $value && $value !== $title) { $blocks[] = $value; } } if ('hotel' === $type) { $address = $this->plain($record['address'] ?? null); if ('' !== $address) { $blocks[] = 'Adresse: '.$address; } } $text = is_array($record['text'] ?? null) ? $record['text'] : []; foreach (self::TEXT_SECTIONS[$type] ?? [] as $field => $heading) { $markdown = $this->converter->convert(is_string($text[$field] ?? null) ? $text[$field] : null, $baseUrl); if ('' === $markdown) { continue; } $blocks[] = null === $heading ? $markdown : "## {$heading}\n\n{$markdown}"; } $facts = $this->facts($type, is_array($record['facts'] ?? null) ? $record['facts'] : []); if ('' !== $facts) { $blocks[] = "## Fakten\n\n".$facts; } if ('region' === $type && is_string($record['webcam'] ?? null) && '' !== trim($record['webcam'])) { $blocks[] = 'Webcam: '.trim($record['webcam']); } if ('product' === $type) { $faqs = $this->faqs(is_array($record['faqs'] ?? null) ? $record['faqs'] : [], $baseUrl); if ('' !== $faqs) { $blocks[] = "## Häufige Fragen\n\n".$faqs; } } return $this->join($blocks); } /** * @param array $record * * @return list */ private function newsBlocks(array $record, ?string $baseUrl): array { $blocks = []; if (is_string($record['datetime'] ?? null) && false !== ($date = date_create_immutable($record['datetime']))) { $blocks[] = 'Veröffentlicht am '.$date->format('d.m.Y'); } foreach (['teaser', 'bodytext'] as $field) { $markdown = $this->converter->convert(is_string($record[$field] ?? null) ? $record[$field] : null, $baseUrl); if ('' !== $markdown) { $blocks[] = $markdown; } } $tags = $this->labels($record['tags'] ?? null); if ([] !== $tags) { $blocks[] = 'Themen: '.implode(', ', $tags); } return $blocks; } /** * @param array $facts */ private function facts(string $type, array $facts): string { $lines = []; foreach (self::FACTS[$type] ?? [] as $field => $label) { $value = $facts[$field] ?? null; $rendered = match (true) { is_bool($value) => $value ? 'ja' : 'nein', is_int($value), is_float($value) => (string) $value, is_string($value) => $this->plain($value), default => '', }; if ('' !== $rendered) { $lines[] = "- {$label}: {$rendered}"; } } return implode("\n", $lines); } /** * @param array $faqs */ private function faqs(array $faqs, ?string $baseUrl): string { $blocks = []; foreach ($faqs as $faq) { if (!is_array($faq)) { continue; } $question = $this->plain($faq['question'] ?? null); $answer = $this->converter->convert(is_string($faq['answer'] ?? null) ? $faq['answer'] : null, $baseUrl); if ('' !== $question && '' !== $answer) { $blocks[] = "### {$question}\n\n{$answer}"; } } return $this->join($blocks); } /** * Tags and categories may arrive as strings or as objects with a title or name. * * @return list */ private function labels(mixed $values): array { if (!is_array($values)) { return []; } $labels = []; foreach ($values as $value) { $label = is_array($value) ? ($value['title'] ?? $value['name'] ?? null) : $value; if (is_string($label) && '' !== trim($label)) { $labels[] = trim($label); } } return $labels; } /** * A field that is plain text in the feed, defensively stripped of any markup. */ private function plain(mixed $value): string { if (!is_string($value)) { return ''; } return trim((string) preg_replace('/\s+/u', ' ', html_entity_decode(strip_tags($value), \ENT_QUOTES | \ENT_HTML5, 'UTF-8'))); } /** * @param list $blocks */ private function join(array $blocks): string { return implode("\n\n", $blocks); } }