1c2ed51d1SGreg Roach<?php 2c2ed51d1SGreg Roach 3c2ed51d1SGreg Roach/** 4c2ed51d1SGreg Roach * webtrees: online genealogy 5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team 6c2ed51d1SGreg Roach * This program is free software: you can redistribute it and/or modify 7c2ed51d1SGreg Roach * it under the terms of the GNU General Public License as published by 8c2ed51d1SGreg Roach * the Free Software Foundation, either version 3 of the License, or 9c2ed51d1SGreg Roach * (at your option) any later version. 10c2ed51d1SGreg Roach * This program is distributed in the hope that it will be useful, 11c2ed51d1SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 12c2ed51d1SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13c2ed51d1SGreg Roach * GNU General Public License for more details. 14c2ed51d1SGreg Roach * You should have received a copy of the GNU General Public License 15c2ed51d1SGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 16c2ed51d1SGreg Roach */ 17c2ed51d1SGreg Roach 18c2ed51d1SGreg Roachdeclare(strict_types=1); 19c2ed51d1SGreg Roach 20c2ed51d1SGreg Roachnamespace Fisharebest\Webtrees\Elements; 21c2ed51d1SGreg Roach 22c2ed51d1SGreg Roachuse Fisharebest\Webtrees\Contracts\ElementInterface; 23c2ed51d1SGreg Roachuse Fisharebest\Webtrees\Html; 24c2ed51d1SGreg Roachuse Fisharebest\Webtrees\I18N; 258392edd9SGreg Roachuse Fisharebest\Webtrees\Registry; 26c2ed51d1SGreg Roachuse Fisharebest\Webtrees\Tree; 27c2ed51d1SGreg Roach 28c2ed51d1SGreg Roachuse function array_key_exists; 29c2ed51d1SGreg Roachuse function array_map; 30c2ed51d1SGreg Roachuse function e; 31c2ed51d1SGreg Roachuse function is_numeric; 32486b00e0SGreg Roachuse function nl2br; 334da96842SGreg Roachuse function str_contains; 34cef4fcc0SGreg Roachuse function str_starts_with; 354da96842SGreg Roachuse function strip_tags; 36c2ed51d1SGreg Roachuse function trim; 37c2ed51d1SGreg Roachuse function view; 38c2ed51d1SGreg Roach 39c2ed51d1SGreg Roach/** 40c2ed51d1SGreg Roach * A GEDCOM element is a tag/primitive in a GEDCOM file. 41c2ed51d1SGreg Roach */ 42c2ed51d1SGreg Roachabstract class AbstractElement implements ElementInterface 43c2ed51d1SGreg Roach{ 44c2ed51d1SGreg Roach // HTML attributes for an <input> 45ae0043b7SGreg Roach protected const MAXIMUM_LENGTH = false; 46ae0043b7SGreg Roach protected const PATTERN = false; 47c2ed51d1SGreg Roach 48b3d7ece6SGreg Roach private const WHITESPACE_LINE = [ 49b3d7ece6SGreg Roach "\t" => ' ', 50b3d7ece6SGreg Roach "\n" => ' ', 51b3d7ece6SGreg Roach "\r" => ' ', 52b3d7ece6SGreg Roach "\v" => ' ', // Vertical tab 53b3d7ece6SGreg Roach "\u{85}" => ' ', // NEL - newline 54b3d7ece6SGreg Roach "\u{2028}" => ' ', // LS - line separator 55b3d7ece6SGreg Roach "\u{2029}" => ' ', // PS - paragraph separator 56b3d7ece6SGreg Roach ]; 57b3d7ece6SGreg Roach 58b3d7ece6SGreg Roach private const WHITESPACE_TEXT = [ 59b3d7ece6SGreg Roach "\t" => ' ', 60b3d7ece6SGreg Roach "\r\n" => "\n", 61b3d7ece6SGreg Roach "\r" => "\n", 62b3d7ece6SGreg Roach "\v" => "\n", 63b3d7ece6SGreg Roach "\u{85}" => "\n", 64b3d7ece6SGreg Roach "\u{2028}" => "\n", 65b3d7ece6SGreg Roach "\u{2029}" => "\n\n", 66b3d7ece6SGreg Roach ]; 67b3d7ece6SGreg Roach 68c2ed51d1SGreg Roach // Which child elements can appear under this element. 69c2ed51d1SGreg Roach protected const SUBTAGS = []; 70c2ed51d1SGreg Roach 714da96842SGreg Roach // A label to describe this element 724da96842SGreg Roach private string $label; 73c2ed51d1SGreg Roach 744da96842SGreg Roach /** @var array<string,string> Subtags of this element */ 754da96842SGreg Roach private array $subtags; 76c2ed51d1SGreg Roach 77c2ed51d1SGreg Roach /** 78c2ed51d1SGreg Roach * @param string $label 79c2ed51d1SGreg Roach * @param array<string>|null $subtags 80c2ed51d1SGreg Roach */ 81*2c6f1bd5SGreg Roach public function __construct(string $label, array|null $subtags = null) 82c2ed51d1SGreg Roach { 83c2ed51d1SGreg Roach $this->label = $label; 84c2ed51d1SGreg Roach $this->subtags = $subtags ?? static::SUBTAGS; 85c2ed51d1SGreg Roach } 86c2ed51d1SGreg Roach 87c2ed51d1SGreg Roach /** 88c2ed51d1SGreg Roach * Convert a value to a canonical form. 89c2ed51d1SGreg Roach * 90c2ed51d1SGreg Roach * @param string $value 91c2ed51d1SGreg Roach * 92c2ed51d1SGreg Roach * @return string 93c2ed51d1SGreg Roach */ 94c2ed51d1SGreg Roach public function canonical(string $value): string 95c2ed51d1SGreg Roach { 96b3d7ece6SGreg Roach $value = strtr($value, self::WHITESPACE_LINE); 97c2ed51d1SGreg Roach 984da96842SGreg Roach while (str_contains($value, ' ')) { 99c2ed51d1SGreg Roach $value = strtr($value, [' ' => ' ']); 100c2ed51d1SGreg Roach } 101c2ed51d1SGreg Roach 102c2ed51d1SGreg Roach return trim($value); 103c2ed51d1SGreg Roach } 104c2ed51d1SGreg Roach 105c2ed51d1SGreg Roach /** 1064e09581bSGreg Roach * Convert a multi-line value to a canonical form. 1074e09581bSGreg Roach * 1084e09581bSGreg Roach * @param string $value 1094e09581bSGreg Roach * 1104e09581bSGreg Roach * @return string 1114e09581bSGreg Roach */ 1124e09581bSGreg Roach protected function canonicalText(string $value): string 1134e09581bSGreg Roach { 114b3d7ece6SGreg Roach $value = strtr($value, self::WHITESPACE_TEXT); 1154e09581bSGreg Roach 116b3d7ece6SGreg Roach return trim($value, "\n"); 1174e09581bSGreg Roach } 1184e09581bSGreg Roach 1194e09581bSGreg Roach /** 1204e09581bSGreg Roach * Should we collapse the children of this element when editing? 1214e09581bSGreg Roach * 1224e09581bSGreg Roach * @return bool 1234e09581bSGreg Roach */ 1244e09581bSGreg Roach public function collapseChildren(): bool 1254e09581bSGreg Roach { 1264e09581bSGreg Roach return false; 1274e09581bSGreg Roach } 1284e09581bSGreg Roach 1294e09581bSGreg Roach /** 130c2ed51d1SGreg Roach * Create a default value for this element. 131c2ed51d1SGreg Roach * 132c2ed51d1SGreg Roach * @param Tree $tree 133c2ed51d1SGreg Roach * 134c2ed51d1SGreg Roach * @return string 135c2ed51d1SGreg Roach */ 136c2ed51d1SGreg Roach public function default(Tree $tree): string 137c2ed51d1SGreg Roach { 138c2ed51d1SGreg Roach return ''; 139c2ed51d1SGreg Roach } 140c2ed51d1SGreg Roach 141c2ed51d1SGreg Roach /** 142c2ed51d1SGreg Roach * An edit control for this data. 143c2ed51d1SGreg Roach * 144c2ed51d1SGreg Roach * @param string $id 145c2ed51d1SGreg Roach * @param string $name 146c2ed51d1SGreg Roach * @param string $value 147c2ed51d1SGreg Roach * @param Tree $tree 148c2ed51d1SGreg Roach * 149c2ed51d1SGreg Roach * @return string 150c2ed51d1SGreg Roach */ 151c2ed51d1SGreg Roach public function edit(string $id, string $name, string $value, Tree $tree): string 152c2ed51d1SGreg Roach { 153c2ed51d1SGreg Roach $values = $this->values(); 154c2ed51d1SGreg Roach 155c2ed51d1SGreg Roach if ($values !== []) { 156c2ed51d1SGreg Roach $value = $this->canonical($value); 157c2ed51d1SGreg Roach 158c2ed51d1SGreg Roach // Ensure the current data is in the list. 159c2ed51d1SGreg Roach if (!array_key_exists($value, $values)) { 160c2ed51d1SGreg Roach $values = [$value => $value] + $values; 161c2ed51d1SGreg Roach } 162c2ed51d1SGreg Roach 163c2ed51d1SGreg Roach // We may use markup to display values, but not when editing them. 16405babb96SGreg Roach $values = array_map(static fn (string $x): string => strip_tags($x), $values); 165c2ed51d1SGreg Roach 166c2ed51d1SGreg Roach return view('components/select', [ 167c2ed51d1SGreg Roach 'id' => $id, 168c2ed51d1SGreg Roach 'name' => $name, 169c2ed51d1SGreg Roach 'options' => $values, 170c2ed51d1SGreg Roach 'selected' => $value, 171c2ed51d1SGreg Roach ]); 172c2ed51d1SGreg Roach } 173c2ed51d1SGreg Roach 174c2ed51d1SGreg Roach $attributes = [ 175c2ed51d1SGreg Roach 'class' => 'form-control', 1769deadf1cSGreg Roach 'dir' => 'auto', 177c2ed51d1SGreg Roach 'type' => 'text', 178c2ed51d1SGreg Roach 'id' => $id, 179c2ed51d1SGreg Roach 'name' => $name, 180c2ed51d1SGreg Roach 'value' => $value, 181ca561ce7SGreg Roach 'maxlength' => static::MAXIMUM_LENGTH, 182ae0043b7SGreg Roach 'pattern' => static::PATTERN, 183c2ed51d1SGreg Roach ]; 184c2ed51d1SGreg Roach 1854a213054SGreg Roach return '<input ' . Html::attributes($attributes) . ' />'; 186c2ed51d1SGreg Roach } 187c2ed51d1SGreg Roach 188c2ed51d1SGreg Roach /** 189c2ed51d1SGreg Roach * An edit control for this data. 190c2ed51d1SGreg Roach * 191c2ed51d1SGreg Roach * @param string $id 192c2ed51d1SGreg Roach * @param string $name 193c2ed51d1SGreg Roach * @param string $value 194c2ed51d1SGreg Roach * 195c2ed51d1SGreg Roach * @return string 196c2ed51d1SGreg Roach */ 197c2ed51d1SGreg Roach public function editHidden(string $id, string $name, string $value): string 198c2ed51d1SGreg Roach { 1994a213054SGreg Roach return '<input class="form-control" type="hidden" id="' . e($id) . '" name="' . e($name) . '" value="' . e($value) . '" />'; 200c2ed51d1SGreg Roach } 201c2ed51d1SGreg Roach 202c2ed51d1SGreg Roach /** 203c2ed51d1SGreg Roach * An edit control for this data. 204c2ed51d1SGreg Roach * 205c2ed51d1SGreg Roach * @param string $id 206c2ed51d1SGreg Roach * @param string $name 207c2ed51d1SGreg Roach * @param string $value 208c2ed51d1SGreg Roach * 209c2ed51d1SGreg Roach * @return string 210c2ed51d1SGreg Roach */ 211c2ed51d1SGreg Roach public function editTextArea(string $id, string $name, string $value): string 212c2ed51d1SGreg Roach { 2134e09581bSGreg Roach return '<textarea class="form-control" id="' . e($id) . '" name="' . e($name) . '" rows="3" dir="auto">' . e($value) . '</textarea>'; 214c2ed51d1SGreg Roach } 215c2ed51d1SGreg Roach 216c2ed51d1SGreg Roach /** 217c2ed51d1SGreg Roach * Escape @ signs in a GEDCOM export. 218c2ed51d1SGreg Roach * 219c2ed51d1SGreg Roach * @param string $value 220c2ed51d1SGreg Roach * 221c2ed51d1SGreg Roach * @return string 222c2ed51d1SGreg Roach */ 223c2ed51d1SGreg Roach public function escape(string $value): string 224c2ed51d1SGreg Roach { 225c2ed51d1SGreg Roach return strtr($value, ['@' => '@@']); 226c2ed51d1SGreg Roach } 227c2ed51d1SGreg Roach 228c2ed51d1SGreg Roach /** 229c2ed51d1SGreg Roach * Create a label for this element. 230c2ed51d1SGreg Roach * 231c2ed51d1SGreg Roach * @return string 232c2ed51d1SGreg Roach */ 233c2ed51d1SGreg Roach public function label(): string 234c2ed51d1SGreg Roach { 235c2ed51d1SGreg Roach return $this->label; 236c2ed51d1SGreg Roach } 237c2ed51d1SGreg Roach 238c2ed51d1SGreg Roach /** 239c2ed51d1SGreg Roach * Create a label/value pair for this element. 240c2ed51d1SGreg Roach * 241c2ed51d1SGreg Roach * @param string $value 242c2ed51d1SGreg Roach * @param Tree $tree 243c2ed51d1SGreg Roach * 244c2ed51d1SGreg Roach * @return string 245c2ed51d1SGreg Roach */ 246c2ed51d1SGreg Roach public function labelValue(string $value, Tree $tree): string 247c2ed51d1SGreg Roach { 248c2ed51d1SGreg Roach $label = '<span class="label">' . $this->label() . '</span>'; 2498da28f8eSGreg Roach $value = '<span class="value align-top">' . $this->value($value, $tree) . '</span>'; 250c2ed51d1SGreg Roach $html = I18N::translate(/* I18N: e.g. "Occupation: farmer" */ '%1$s: %2$s', $label, $value); 251c2ed51d1SGreg Roach 252c2ed51d1SGreg Roach return '<div>' . $html . '</div>'; 253c2ed51d1SGreg Roach } 254c2ed51d1SGreg Roach 255c2ed51d1SGreg Roach /** 2564dbb2a39SGreg Roach * Set, remove or replace a subtag. 2574dbb2a39SGreg Roach * 2584dbb2a39SGreg Roach * @param string $subtag 2594dbb2a39SGreg Roach * @param string $repeat 2604da96842SGreg Roach * @param string $before 2614dbb2a39SGreg Roach * 2624dbb2a39SGreg Roach * @return void 2634dbb2a39SGreg Roach */ 2648c21658eSGreg Roach public function subtag(string $subtag, string $repeat, string $before = ''): void 2654dbb2a39SGreg Roach { 2666c747bc0SGreg Roach if ($before === '' || ($this->subtags[$before] ?? null) === null) { 2674dbb2a39SGreg Roach $this->subtags[$subtag] = $repeat; 2684dbb2a39SGreg Roach } else { 2694dbb2a39SGreg Roach $tmp = []; 2704dbb2a39SGreg Roach 2714dbb2a39SGreg Roach foreach ($this->subtags as $key => $value) { 2724da96842SGreg Roach if ($key === $before) { 273efd4768bSGreg Roach $tmp[$subtag] = $repeat; 2744dbb2a39SGreg Roach } 2754da96842SGreg Roach $tmp[$key] = $value; 2764dbb2a39SGreg Roach } 2774dbb2a39SGreg Roach 2784dbb2a39SGreg Roach $this->subtags = $tmp; 2794dbb2a39SGreg Roach } 2804dbb2a39SGreg Roach } 2814dbb2a39SGreg Roach 2824dbb2a39SGreg Roach /** 283c2ed51d1SGreg Roach * @return array<string,string> 284c2ed51d1SGreg Roach */ 2853d2c98d1SGreg Roach public function subtags(): array 286c2ed51d1SGreg Roach { 287c2ed51d1SGreg Roach return $this->subtags; 288c2ed51d1SGreg Roach } 289c2ed51d1SGreg Roach 290c2ed51d1SGreg Roach /** 291c2ed51d1SGreg Roach * Display the value of this type of element. 292c2ed51d1SGreg Roach * 293c2ed51d1SGreg Roach * @param string $value 294c2ed51d1SGreg Roach * @param Tree $tree 295c2ed51d1SGreg Roach * 296c2ed51d1SGreg Roach * @return string 297c2ed51d1SGreg Roach */ 298c2ed51d1SGreg Roach public function value(string $value, Tree $tree): string 299c2ed51d1SGreg Roach { 300c2ed51d1SGreg Roach $values = $this->values(); 301c2ed51d1SGreg Roach 302c2ed51d1SGreg Roach if ($values === []) { 303c2ed51d1SGreg Roach if (str_contains($value, "\n")) { 3045d2c6313SGreg Roach return '<span class="ut d-inline-block">' . nl2br(e($value, false)) . '</span>'; 305c2ed51d1SGreg Roach } 306c2ed51d1SGreg Roach 3075d2c6313SGreg Roach return '<span class="ut">' . e($value) . '</span>'; 308c2ed51d1SGreg Roach } 309c2ed51d1SGreg Roach 310c2ed51d1SGreg Roach $canonical = $this->canonical($value); 311c2ed51d1SGreg Roach 312315eb316SGreg Roach return $values[$canonical] ?? '<bdi>' . e($value) . '</bdi>'; 313c2ed51d1SGreg Roach } 314c2ed51d1SGreg Roach 315c2ed51d1SGreg Roach /** 316c2ed51d1SGreg Roach * A list of controlled values for this element 317c2ed51d1SGreg Roach * 318c2ed51d1SGreg Roach * @return array<int|string,string> 319c2ed51d1SGreg Roach */ 320c2ed51d1SGreg Roach public function values(): array 321c2ed51d1SGreg Roach { 322c2ed51d1SGreg Roach return []; 323c2ed51d1SGreg Roach } 324c2ed51d1SGreg Roach 325c2ed51d1SGreg Roach /** 326cef4fcc0SGreg Roach * Display the value of this type of element - convert URLs to links. 327c2ed51d1SGreg Roach * 328c2ed51d1SGreg Roach * @param string $value 329c2ed51d1SGreg Roach * 330c2ed51d1SGreg Roach * @return string 331c2ed51d1SGreg Roach */ 332c2ed51d1SGreg Roach protected function valueAutoLink(string $value): string 333c2ed51d1SGreg Roach { 334a3287c67SGreg Roach $canonical = $this->canonical($value); 335c2ed51d1SGreg Roach 3368392edd9SGreg Roach if (str_contains($canonical, 'http://') || str_contains($canonical, 'https://')) { 3376f595250SGreg Roach $html = Registry::markdownFactory()->autolink($canonical); 3385aa15f0cSGreg Roach $html = strip_tags($html, ['a', 'br']); 3395aa15f0cSGreg Roach } else { 3405aa15f0cSGreg Roach $html = nl2br(e($canonical), false); 341a3287c67SGreg Roach } 342c2ed51d1SGreg Roach 3435aa15f0cSGreg Roach if (str_contains($html, '<br>')) { 3445aa15f0cSGreg Roach return '<span class="ut d-inline-block">' . $html . '</span>'; 3455aa15f0cSGreg Roach } 3465aa15f0cSGreg Roach 3475aa15f0cSGreg Roach return '<span class="ut">' . $html . '</span>'; 348c2ed51d1SGreg Roach } 349c2ed51d1SGreg Roach 350c2ed51d1SGreg Roach /** 351486b00e0SGreg Roach * Display the value of this type of element - multi-line text with/without markdown. 352486b00e0SGreg Roach * 353486b00e0SGreg Roach * @param string $value 3547f50305dSGreg Roach * @param Tree $tree 355486b00e0SGreg Roach * 356486b00e0SGreg Roach * @return string 357486b00e0SGreg Roach */ 358486b00e0SGreg Roach protected function valueFormatted(string $value, Tree $tree): string 359486b00e0SGreg Roach { 360486b00e0SGreg Roach $canonical = $this->canonical($value); 361486b00e0SGreg Roach 362486b00e0SGreg Roach $format = $tree->getPreference('FORMAT_TEXT'); 363486b00e0SGreg Roach 36467721f6fSGreg Roach switch ($format) { 36567721f6fSGreg Roach case 'markdown': 3662de327daSGreg Roach return Registry::markdownFactory()->markdown($canonical, $tree); 367486b00e0SGreg Roach 36867721f6fSGreg Roach default: 3692de327daSGreg Roach return Registry::markdownFactory()->autolink($canonical, $tree); 3701adf58a6SGreg Roach } 37167721f6fSGreg Roach } 3721adf58a6SGreg Roach 373486b00e0SGreg Roach /** 374cef4fcc0SGreg Roach * Display the value of this type of element - convert to URL. 375cef4fcc0SGreg Roach * 376cef4fcc0SGreg Roach * @param string $value 377cef4fcc0SGreg Roach * 378cef4fcc0SGreg Roach * @return string 379cef4fcc0SGreg Roach */ 380cef4fcc0SGreg Roach protected function valueLink(string $value): string 381cef4fcc0SGreg Roach { 382cef4fcc0SGreg Roach $canonical = $this->canonical($value); 383cef4fcc0SGreg Roach 384cef4fcc0SGreg Roach if (str_starts_with($canonical, 'https://') || str_starts_with($canonical, 'http://')) { 385cef4fcc0SGreg Roach return '<a dir="auto" href="' . e($canonical) . '">' . e($value) . '</a>'; 386cef4fcc0SGreg Roach } 387cef4fcc0SGreg Roach 388cef4fcc0SGreg Roach return e($value); 389cef4fcc0SGreg Roach } 3904e09581bSGreg Roach 3914e09581bSGreg Roach /** 3924e09581bSGreg Roach * Display the value of this type of element. 3934e09581bSGreg Roach * 3944e09581bSGreg Roach * @param string $value 3954e09581bSGreg Roach * 3964e09581bSGreg Roach * @return string 3974e09581bSGreg Roach */ 3984e09581bSGreg Roach public function valueNumeric(string $value): string 3994e09581bSGreg Roach { 4004e09581bSGreg Roach $canonical = $this->canonical($value); 4014e09581bSGreg Roach 4024e09581bSGreg Roach if (is_numeric($canonical)) { 4034e09581bSGreg Roach return I18N::number((int) $canonical); 4044e09581bSGreg Roach } 4054e09581bSGreg Roach 4064e09581bSGreg Roach return e($value); 4074e09581bSGreg Roach } 408c2ed51d1SGreg Roach} 409