1c2ed51d1SGreg Roach<?php 2c2ed51d1SGreg Roach 3c2ed51d1SGreg Roach/** 4c2ed51d1SGreg Roach * webtrees: online genealogy 5c2ed51d1SGreg Roach * Copyright (C) 2021 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; 339d477377SGreg Roachuse function preg_replace; 344da96842SGreg Roachuse function str_contains; 35cef4fcc0SGreg Roachuse function str_starts_with; 364da96842SGreg Roachuse function strip_tags; 37c2ed51d1SGreg Roachuse function trim; 38c2ed51d1SGreg Roachuse function view; 39c2ed51d1SGreg Roach 40c2ed51d1SGreg Roach/** 41c2ed51d1SGreg Roach * A GEDCOM element is a tag/primitive in a GEDCOM file. 42c2ed51d1SGreg Roach */ 43c2ed51d1SGreg Roachabstract class AbstractElement implements ElementInterface 44c2ed51d1SGreg Roach{ 45c2ed51d1SGreg Roach // HTML attributes for an <input> 46ae0043b7SGreg Roach protected const MAXIMUM_LENGTH = false; 47ae0043b7SGreg Roach protected const PATTERN = false; 48c2ed51d1SGreg Roach 49c2ed51d1SGreg Roach // Which child elements can appear under this element. 50c2ed51d1SGreg Roach protected const SUBTAGS = []; 51c2ed51d1SGreg Roach 524da96842SGreg Roach // A label to describe this element 534da96842SGreg Roach private string $label; 54c2ed51d1SGreg Roach 554da96842SGreg Roach /** @var array<string,string> Subtags of this element */ 564da96842SGreg Roach private array $subtags; 57c2ed51d1SGreg Roach 58c2ed51d1SGreg Roach /** 59c2ed51d1SGreg Roach * AbstractGedcomElement constructor. 60c2ed51d1SGreg Roach * 61c2ed51d1SGreg Roach * @param string $label 62c2ed51d1SGreg Roach * @param array<string>|null $subtags 63c2ed51d1SGreg Roach */ 64c2ed51d1SGreg Roach public function __construct(string $label, array $subtags = null) 65c2ed51d1SGreg Roach { 66c2ed51d1SGreg Roach $this->label = $label; 67c2ed51d1SGreg Roach $this->subtags = $subtags ?? static::SUBTAGS; 68c2ed51d1SGreg Roach } 69c2ed51d1SGreg Roach 70c2ed51d1SGreg Roach /** 71c2ed51d1SGreg Roach * Convert a value to a canonical form. 72c2ed51d1SGreg Roach * 73c2ed51d1SGreg Roach * @param string $value 74c2ed51d1SGreg Roach * 75c2ed51d1SGreg Roach * @return string 76c2ed51d1SGreg Roach */ 77c2ed51d1SGreg Roach public function canonical(string $value): string 78c2ed51d1SGreg Roach { 79c2ed51d1SGreg Roach $value = strtr($value, ["\t" => ' ', "\r" => ' ', "\n" => ' ']); 80c2ed51d1SGreg Roach 814da96842SGreg Roach while (str_contains($value, ' ')) { 82c2ed51d1SGreg Roach $value = strtr($value, [' ' => ' ']); 83c2ed51d1SGreg Roach } 84c2ed51d1SGreg Roach 85c2ed51d1SGreg Roach return trim($value); 86c2ed51d1SGreg Roach } 87c2ed51d1SGreg Roach 88c2ed51d1SGreg Roach /** 894e09581bSGreg Roach * Convert a multi-line value to a canonical form. 904e09581bSGreg Roach * 914e09581bSGreg Roach * @param string $value 924e09581bSGreg Roach * 934e09581bSGreg Roach * @return string 944e09581bSGreg Roach */ 954e09581bSGreg Roach protected function canonicalText(string $value): string 964e09581bSGreg Roach { 974e09581bSGreg Roach // Browsers use MS-DOS line endings in multi-line data. 984e09581bSGreg Roach $value = strtr($value, ["\t" => ' ', "\r\n" => "\n", "\r" => "\n"]); 994e09581bSGreg Roach 1004e09581bSGreg Roach // Remove blank lines at start/end 1014e09581bSGreg Roach $value = preg_replace('/^( *\n)+/', '', $value); 1024e09581bSGreg Roach 1034e09581bSGreg Roach return preg_replace('/(\n *)+$/', '', $value); 1044e09581bSGreg Roach } 1054e09581bSGreg Roach 1064e09581bSGreg Roach /** 1074e09581bSGreg Roach * Should we collapse the children of this element when editing? 1084e09581bSGreg Roach * 1094e09581bSGreg Roach * @return bool 1104e09581bSGreg Roach */ 1114e09581bSGreg Roach public function collapseChildren(): bool 1124e09581bSGreg Roach { 1134e09581bSGreg Roach return false; 1144e09581bSGreg Roach } 1154e09581bSGreg Roach 1164e09581bSGreg Roach /** 117c2ed51d1SGreg Roach * Create a default value for this element. 118c2ed51d1SGreg Roach * 119c2ed51d1SGreg Roach * @param Tree $tree 120c2ed51d1SGreg Roach * 121c2ed51d1SGreg Roach * @return string 122c2ed51d1SGreg Roach */ 123c2ed51d1SGreg Roach public function default(Tree $tree): string 124c2ed51d1SGreg Roach { 125c2ed51d1SGreg Roach return ''; 126c2ed51d1SGreg Roach } 127c2ed51d1SGreg Roach 128c2ed51d1SGreg Roach /** 129c2ed51d1SGreg Roach * An edit control for this data. 130c2ed51d1SGreg Roach * 131c2ed51d1SGreg Roach * @param string $id 132c2ed51d1SGreg Roach * @param string $name 133c2ed51d1SGreg Roach * @param string $value 134c2ed51d1SGreg Roach * @param Tree $tree 135c2ed51d1SGreg Roach * 136c2ed51d1SGreg Roach * @return string 137c2ed51d1SGreg Roach */ 138c2ed51d1SGreg Roach public function edit(string $id, string $name, string $value, Tree $tree): string 139c2ed51d1SGreg Roach { 140c2ed51d1SGreg Roach $values = $this->values(); 141c2ed51d1SGreg Roach 142c2ed51d1SGreg Roach if ($values !== []) { 143c2ed51d1SGreg Roach $value = $this->canonical($value); 144c2ed51d1SGreg Roach 145c2ed51d1SGreg Roach // Ensure the current data is in the list. 146c2ed51d1SGreg Roach if (!array_key_exists($value, $values)) { 147c2ed51d1SGreg Roach $values = [$value => $value] + $values; 148c2ed51d1SGreg Roach } 149c2ed51d1SGreg Roach 150c2ed51d1SGreg Roach // We may use markup to display values, but not when editing them. 1514da96842SGreg Roach $values = array_map(fn (string $x): string => strip_tags($x), $values); 152c2ed51d1SGreg Roach 153c2ed51d1SGreg Roach return view('components/select', [ 154c2ed51d1SGreg Roach 'id' => $id, 155c2ed51d1SGreg Roach 'name' => $name, 156c2ed51d1SGreg Roach 'options' => $values, 157c2ed51d1SGreg Roach 'selected' => $value, 158c2ed51d1SGreg Roach ]); 159c2ed51d1SGreg Roach } 160c2ed51d1SGreg Roach 161c2ed51d1SGreg Roach $attributes = [ 162c2ed51d1SGreg Roach 'class' => 'form-control', 1639deadf1cSGreg Roach 'dir' => 'auto', 164c2ed51d1SGreg Roach 'type' => 'text', 165c2ed51d1SGreg Roach 'id' => $id, 166c2ed51d1SGreg Roach 'name' => $name, 167c2ed51d1SGreg Roach 'value' => $value, 168ca561ce7SGreg Roach 'maxlength' => static::MAXIMUM_LENGTH, 169ae0043b7SGreg Roach 'pattern' => static::PATTERN, 170c2ed51d1SGreg Roach ]; 171c2ed51d1SGreg Roach 1724a213054SGreg Roach return '<input ' . Html::attributes($attributes) . ' />'; 173c2ed51d1SGreg Roach } 174c2ed51d1SGreg Roach 175c2ed51d1SGreg Roach /** 176c2ed51d1SGreg Roach * An edit control for this data. 177c2ed51d1SGreg Roach * 178c2ed51d1SGreg Roach * @param string $id 179c2ed51d1SGreg Roach * @param string $name 180c2ed51d1SGreg Roach * @param string $value 181c2ed51d1SGreg Roach * 182c2ed51d1SGreg Roach * @return string 183c2ed51d1SGreg Roach */ 184c2ed51d1SGreg Roach public function editHidden(string $id, string $name, string $value): string 185c2ed51d1SGreg Roach { 1864a213054SGreg Roach return '<input class="form-control" type="hidden" id="' . e($id) . '" name="' . e($name) . '" value="' . e($value) . '" />'; 187c2ed51d1SGreg Roach } 188c2ed51d1SGreg Roach 189c2ed51d1SGreg Roach /** 190c2ed51d1SGreg Roach * An edit control for this data. 191c2ed51d1SGreg Roach * 192c2ed51d1SGreg Roach * @param string $id 193c2ed51d1SGreg Roach * @param string $name 194c2ed51d1SGreg Roach * @param string $value 195c2ed51d1SGreg Roach * 196c2ed51d1SGreg Roach * @return string 197c2ed51d1SGreg Roach */ 198c2ed51d1SGreg Roach public function editTextArea(string $id, string $name, string $value): string 199c2ed51d1SGreg Roach { 2004e09581bSGreg Roach return '<textarea class="form-control" id="' . e($id) . '" name="' . e($name) . '" rows="3" dir="auto">' . e($value) . '</textarea>'; 201c2ed51d1SGreg Roach } 202c2ed51d1SGreg Roach 203c2ed51d1SGreg Roach /** 204c2ed51d1SGreg Roach * Escape @ signs in a GEDCOM export. 205c2ed51d1SGreg Roach * 206c2ed51d1SGreg Roach * @param string $value 207c2ed51d1SGreg Roach * 208c2ed51d1SGreg Roach * @return string 209c2ed51d1SGreg Roach */ 210c2ed51d1SGreg Roach public function escape(string $value): string 211c2ed51d1SGreg Roach { 212c2ed51d1SGreg Roach return strtr($value, ['@' => '@@']); 213c2ed51d1SGreg Roach } 214c2ed51d1SGreg Roach 215c2ed51d1SGreg Roach /** 216c2ed51d1SGreg Roach * Create a label for this element. 217c2ed51d1SGreg Roach * 218c2ed51d1SGreg Roach * @return string 219c2ed51d1SGreg Roach */ 220c2ed51d1SGreg Roach public function label(): string 221c2ed51d1SGreg Roach { 222c2ed51d1SGreg Roach return $this->label; 223c2ed51d1SGreg Roach } 224c2ed51d1SGreg Roach 225c2ed51d1SGreg Roach /** 226c2ed51d1SGreg Roach * Create a label/value pair for this element. 227c2ed51d1SGreg Roach * 228c2ed51d1SGreg Roach * @param string $value 229c2ed51d1SGreg Roach * @param Tree $tree 230c2ed51d1SGreg Roach * 231c2ed51d1SGreg Roach * @return string 232c2ed51d1SGreg Roach */ 233c2ed51d1SGreg Roach public function labelValue(string $value, Tree $tree): string 234c2ed51d1SGreg Roach { 235c2ed51d1SGreg Roach $label = '<span class="label">' . $this->label() . '</span>'; 2368da28f8eSGreg Roach $value = '<span class="value align-top">' . $this->value($value, $tree) . '</span>'; 237c2ed51d1SGreg Roach $html = I18N::translate(/* I18N: e.g. "Occupation: farmer" */ '%1$s: %2$s', $label, $value); 238c2ed51d1SGreg Roach 239c2ed51d1SGreg Roach return '<div>' . $html . '</div>'; 240c2ed51d1SGreg Roach } 241c2ed51d1SGreg Roach 242c2ed51d1SGreg Roach /** 2434dbb2a39SGreg Roach * Set, remove or replace a subtag. 2444dbb2a39SGreg Roach * 2454dbb2a39SGreg Roach * @param string $subtag 2464dbb2a39SGreg Roach * @param string $repeat 2474da96842SGreg Roach * @param string $before 2484dbb2a39SGreg Roach * 2494dbb2a39SGreg Roach * @return void 2504dbb2a39SGreg Roach */ 251efd4768bSGreg Roach public function subtag(string $subtag, string $repeat = '0:1', string $before = ''): void 2524dbb2a39SGreg Roach { 2534dbb2a39SGreg Roach if ($repeat === '') { 2544dbb2a39SGreg Roach unset($this->subtags[$subtag]); 2554da96842SGreg Roach } elseif ($before === '' || ($this->subtags[$before] ?? null) === null) { 2564dbb2a39SGreg Roach $this->subtags[$subtag] = $repeat; 2574dbb2a39SGreg Roach } else { 2584dbb2a39SGreg Roach $tmp = []; 2594dbb2a39SGreg Roach 2604dbb2a39SGreg Roach foreach ($this->subtags as $key => $value) { 2614da96842SGreg Roach if ($key === $before) { 262efd4768bSGreg Roach $tmp[$subtag] = $repeat; 2634dbb2a39SGreg Roach } 2644da96842SGreg Roach $tmp[$key] = $value; 2654dbb2a39SGreg Roach } 2664dbb2a39SGreg Roach 2674dbb2a39SGreg Roach $this->subtags = $tmp; 2684dbb2a39SGreg Roach } 2694dbb2a39SGreg Roach } 2704dbb2a39SGreg Roach 2714dbb2a39SGreg Roach /** 272c2ed51d1SGreg Roach * @return array<string,string> 273c2ed51d1SGreg Roach */ 2743d2c98d1SGreg Roach public function subtags(): array 275c2ed51d1SGreg Roach { 276c2ed51d1SGreg Roach return $this->subtags; 277c2ed51d1SGreg Roach } 278c2ed51d1SGreg Roach 279c2ed51d1SGreg Roach /** 280c2ed51d1SGreg Roach * Display the value of this type of element. 281c2ed51d1SGreg Roach * 282c2ed51d1SGreg Roach * @param string $value 283c2ed51d1SGreg Roach * @param Tree $tree 284c2ed51d1SGreg Roach * 285c2ed51d1SGreg Roach * @return string 286c2ed51d1SGreg Roach */ 287c2ed51d1SGreg Roach public function value(string $value, Tree $tree): string 288c2ed51d1SGreg Roach { 289c2ed51d1SGreg Roach $values = $this->values(); 290c2ed51d1SGreg Roach 291c2ed51d1SGreg Roach if ($values === []) { 292c2ed51d1SGreg Roach if (str_contains($value, "\n")) { 293486b00e0SGreg Roach return '<bdi class="d-inline-block">' . nl2br(e($value)) . '</bdi>'; 294c2ed51d1SGreg Roach } 295c2ed51d1SGreg Roach 296315eb316SGreg Roach return '<bdi>' . e($value) . '</bdi>'; 297c2ed51d1SGreg Roach } 298c2ed51d1SGreg Roach 299c2ed51d1SGreg Roach $canonical = $this->canonical($value); 300c2ed51d1SGreg Roach 301315eb316SGreg Roach return $values[$canonical] ?? '<bdi>' . e($value) . '</bdi>'; 302c2ed51d1SGreg Roach } 303c2ed51d1SGreg Roach 304c2ed51d1SGreg Roach /** 305c2ed51d1SGreg Roach * A list of controlled values for this element 306c2ed51d1SGreg Roach * 307c2ed51d1SGreg Roach * @return array<int|string,string> 308c2ed51d1SGreg Roach */ 309c2ed51d1SGreg Roach public function values(): array 310c2ed51d1SGreg Roach { 311c2ed51d1SGreg Roach return []; 312c2ed51d1SGreg Roach } 313c2ed51d1SGreg Roach 314c2ed51d1SGreg Roach /** 315cef4fcc0SGreg Roach * Display the value of this type of element - convert URLs to links. 316c2ed51d1SGreg Roach * 317c2ed51d1SGreg Roach * @param string $value 318c2ed51d1SGreg Roach * 319c2ed51d1SGreg Roach * @return string 320c2ed51d1SGreg Roach */ 321c2ed51d1SGreg Roach protected function valueAutoLink(string $value): string 322c2ed51d1SGreg Roach { 323a3287c67SGreg Roach $canonical = $this->canonical($value); 324c2ed51d1SGreg Roach 3258392edd9SGreg Roach if (str_contains($canonical, 'http://') || str_contains($canonical, 'https://')) { 3268392edd9SGreg Roach $html = Registry::markdownFactory()->autolink()->convertToHtml($canonical); 3278392edd9SGreg Roach 3288392edd9SGreg Roach return strip_tags($html, ['a']); 329a3287c67SGreg Roach } 330c2ed51d1SGreg Roach 331a3287c67SGreg Roach return e($canonical); 332c2ed51d1SGreg Roach } 333c2ed51d1SGreg Roach 334c2ed51d1SGreg Roach /** 335486b00e0SGreg Roach * Display the value of this type of element - multi-line text with/without markdown. 336486b00e0SGreg Roach * 337486b00e0SGreg Roach * @param string $value 338*7f50305dSGreg Roach * @param Tree $tree 339486b00e0SGreg Roach * 340486b00e0SGreg Roach * @return string 341486b00e0SGreg Roach */ 342486b00e0SGreg Roach protected function valueFormatted(string $value, Tree $tree): string 343486b00e0SGreg Roach { 344486b00e0SGreg Roach $canonical = $this->canonical($value); 345486b00e0SGreg Roach 346486b00e0SGreg Roach $format = $tree->getPreference('FORMAT_TEXT'); 347486b00e0SGreg Roach 348486b00e0SGreg Roach if ($format === 'markdown') { 349486b00e0SGreg Roach $html = Registry::markdownFactory()->markdown($tree)->convertToHtml($canonical); 350486b00e0SGreg Roach } else { 351486b00e0SGreg Roach $html = Registry::markdownFactory()->autolink($tree)->convertToHtml($canonical); 352486b00e0SGreg Roach $html = nl2br($html); 353486b00e0SGreg Roach } 354486b00e0SGreg Roach 355486b00e0SGreg Roach return '<div class="markdown" dir="auto">' . $html . '</div>'; 356486b00e0SGreg Roach } 357486b00e0SGreg Roach 358486b00e0SGreg Roach /** 359cef4fcc0SGreg Roach * Display the value of this type of element - convert to URL. 360cef4fcc0SGreg Roach * 361cef4fcc0SGreg Roach * @param string $value 362cef4fcc0SGreg Roach * 363cef4fcc0SGreg Roach * @return string 364cef4fcc0SGreg Roach */ 365cef4fcc0SGreg Roach protected function valueLink(string $value): string 366cef4fcc0SGreg Roach { 367cef4fcc0SGreg Roach $canonical = $this->canonical($value); 368cef4fcc0SGreg Roach 369cef4fcc0SGreg Roach if (str_starts_with($canonical, 'https://') || str_starts_with($canonical, 'http://')) { 370cef4fcc0SGreg Roach return '<a dir="auto" href="' . e($canonical) . '">' . e($value) . '</a>'; 371cef4fcc0SGreg Roach } 372cef4fcc0SGreg Roach 373cef4fcc0SGreg Roach return e($value); 374cef4fcc0SGreg Roach } 3754e09581bSGreg Roach 3764e09581bSGreg Roach /** 3774e09581bSGreg Roach * Display the value of this type of element. 3784e09581bSGreg Roach * 3794e09581bSGreg Roach * @param string $value 3804e09581bSGreg Roach * 3814e09581bSGreg Roach * @return string 3824e09581bSGreg Roach */ 3834e09581bSGreg Roach public function valueNumeric(string $value): string 3844e09581bSGreg Roach { 3854e09581bSGreg Roach $canonical = $this->canonical($value); 3864e09581bSGreg Roach 3874e09581bSGreg Roach if (is_numeric($canonical)) { 3884e09581bSGreg Roach return I18N::number((int) $canonical); 3894e09581bSGreg Roach } 3904e09581bSGreg Roach 3914e09581bSGreg Roach return e($value); 3924e09581bSGreg Roach } 393c2ed51d1SGreg Roach} 394