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; 25*8392edd9SGreg 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; 32a3287c67SGreg Roachuse function preg_match; 334da96842SGreg Roachuse function str_contains; 34cef4fcc0SGreg Roachuse function str_starts_with; 35cef4fcc0SGreg Roachuse function stream_copy_to_stream; 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{ 45455a30feSGreg Roach protected const REGEX_URL = '~((https?|ftp]):)(//([^\s/?#<>]*))?([^\s?#<>]*)(\?([^\s#<>]*))?(#[^\s?#<>]+)?~'; 46c2ed51d1SGreg Roach 47c2ed51d1SGreg Roach // HTML attributes for an <input> 48ae0043b7SGreg Roach protected const MAXIMUM_LENGTH = false; 49ae0043b7SGreg Roach protected const PATTERN = false; 50c2ed51d1SGreg Roach 51c2ed51d1SGreg Roach // Which child elements can appear under this element. 52c2ed51d1SGreg Roach protected const SUBTAGS = []; 53c2ed51d1SGreg Roach 544da96842SGreg Roach // A label to describe this element 554da96842SGreg Roach private string $label; 56c2ed51d1SGreg Roach 574da96842SGreg Roach /** @var array<string,string> Subtags of this element */ 584da96842SGreg Roach private array $subtags; 59c2ed51d1SGreg Roach 60c2ed51d1SGreg Roach /** 61c2ed51d1SGreg Roach * AbstractGedcomElement constructor. 62c2ed51d1SGreg Roach * 63c2ed51d1SGreg Roach * @param string $label 64c2ed51d1SGreg Roach * @param array<string>|null $subtags 65c2ed51d1SGreg Roach */ 66c2ed51d1SGreg Roach public function __construct(string $label, array $subtags = null) 67c2ed51d1SGreg Roach { 68c2ed51d1SGreg Roach $this->label = $label; 69c2ed51d1SGreg Roach $this->subtags = $subtags ?? static::SUBTAGS; 70c2ed51d1SGreg Roach } 71c2ed51d1SGreg Roach 72c2ed51d1SGreg Roach /** 73c2ed51d1SGreg Roach * Convert a value to a canonical form. 74c2ed51d1SGreg Roach * 75c2ed51d1SGreg Roach * @param string $value 76c2ed51d1SGreg Roach * 77c2ed51d1SGreg Roach * @return string 78c2ed51d1SGreg Roach */ 79c2ed51d1SGreg Roach public function canonical(string $value): string 80c2ed51d1SGreg Roach { 81c2ed51d1SGreg Roach $value = strtr($value, ["\t" => ' ', "\r" => ' ', "\n" => ' ']); 82c2ed51d1SGreg Roach 834da96842SGreg Roach while (str_contains($value, ' ')) { 84c2ed51d1SGreg Roach $value = strtr($value, [' ' => ' ']); 85c2ed51d1SGreg Roach } 86c2ed51d1SGreg Roach 87c2ed51d1SGreg Roach return trim($value); 88c2ed51d1SGreg Roach } 89c2ed51d1SGreg Roach 90c2ed51d1SGreg Roach /** 91c2ed51d1SGreg Roach * Create a default value for this element. 92c2ed51d1SGreg Roach * 93c2ed51d1SGreg Roach * @param Tree $tree 94c2ed51d1SGreg Roach * 95c2ed51d1SGreg Roach * @return string 96c2ed51d1SGreg Roach */ 97c2ed51d1SGreg Roach public function default(Tree $tree): string 98c2ed51d1SGreg Roach { 99c2ed51d1SGreg Roach return ''; 100c2ed51d1SGreg Roach } 101c2ed51d1SGreg Roach 102c2ed51d1SGreg Roach /** 103c2ed51d1SGreg Roach * An edit control for this data. 104c2ed51d1SGreg Roach * 105c2ed51d1SGreg Roach * @param string $id 106c2ed51d1SGreg Roach * @param string $name 107c2ed51d1SGreg Roach * @param string $value 108c2ed51d1SGreg Roach * @param Tree $tree 109c2ed51d1SGreg Roach * 110c2ed51d1SGreg Roach * @return string 111c2ed51d1SGreg Roach */ 112c2ed51d1SGreg Roach public function edit(string $id, string $name, string $value, Tree $tree): string 113c2ed51d1SGreg Roach { 114c2ed51d1SGreg Roach $values = $this->values(); 115c2ed51d1SGreg Roach 116c2ed51d1SGreg Roach if ($values !== []) { 117c2ed51d1SGreg Roach $value = $this->canonical($value); 118c2ed51d1SGreg Roach 119c2ed51d1SGreg Roach // Ensure the current data is in the list. 120c2ed51d1SGreg Roach if (!array_key_exists($value, $values)) { 121c2ed51d1SGreg Roach $values = [$value => $value] + $values; 122c2ed51d1SGreg Roach } 123c2ed51d1SGreg Roach 124c2ed51d1SGreg Roach // We may use markup to display values, but not when editing them. 1254da96842SGreg Roach $values = array_map(fn (string $x): string => strip_tags($x), $values); 126c2ed51d1SGreg Roach 127c2ed51d1SGreg Roach return view('components/select', [ 128c2ed51d1SGreg Roach 'id' => $id, 129c2ed51d1SGreg Roach 'name' => $name, 130c2ed51d1SGreg Roach 'options' => $values, 131c2ed51d1SGreg Roach 'selected' => $value, 132c2ed51d1SGreg Roach ]); 133c2ed51d1SGreg Roach } 134c2ed51d1SGreg Roach 135c2ed51d1SGreg Roach $attributes = [ 136c2ed51d1SGreg Roach 'class' => 'form-control', 1379deadf1cSGreg Roach 'dir' => 'auto', 138c2ed51d1SGreg Roach 'type' => 'text', 139c2ed51d1SGreg Roach 'id' => $id, 140c2ed51d1SGreg Roach 'name' => $name, 141c2ed51d1SGreg Roach 'value' => $value, 142ca561ce7SGreg Roach 'maxlength' => static::MAXIMUM_LENGTH, 143ae0043b7SGreg Roach 'pattern' => static::PATTERN, 144c2ed51d1SGreg Roach ]; 145c2ed51d1SGreg Roach 1464a213054SGreg Roach return '<input ' . Html::attributes($attributes) . ' />'; 147c2ed51d1SGreg Roach } 148c2ed51d1SGreg Roach 149c2ed51d1SGreg Roach /** 150c2ed51d1SGreg Roach * An edit control for this data. 151c2ed51d1SGreg Roach * 152c2ed51d1SGreg Roach * @param string $id 153c2ed51d1SGreg Roach * @param string $name 154c2ed51d1SGreg Roach * @param string $value 155c2ed51d1SGreg Roach * 156c2ed51d1SGreg Roach * @return string 157c2ed51d1SGreg Roach */ 158c2ed51d1SGreg Roach public function editHidden(string $id, string $name, string $value): string 159c2ed51d1SGreg Roach { 1604a213054SGreg Roach return '<input class="form-control" type="hidden" id="' . e($id) . '" name="' . e($name) . '" value="' . e($value) . '" />'; 161c2ed51d1SGreg Roach } 162c2ed51d1SGreg Roach 163c2ed51d1SGreg Roach /** 164c2ed51d1SGreg Roach * An edit control for this data. 165c2ed51d1SGreg Roach * 166c2ed51d1SGreg Roach * @param string $id 167c2ed51d1SGreg Roach * @param string $name 168c2ed51d1SGreg Roach * @param string $value 169c2ed51d1SGreg Roach * 170c2ed51d1SGreg Roach * @return string 171c2ed51d1SGreg Roach */ 172c2ed51d1SGreg Roach public function editTextArea(string $id, string $name, string $value): string 173c2ed51d1SGreg Roach { 174c2ed51d1SGreg Roach return '<textarea class="form-control" id="' . e($id) . '" name="' . e($name) . '" rows="5" dir="auto">' . e($value) . '</textarea>'; 175c2ed51d1SGreg Roach } 176c2ed51d1SGreg Roach 177c2ed51d1SGreg Roach /** 178c2ed51d1SGreg Roach * Escape @ signs in a GEDCOM export. 179c2ed51d1SGreg Roach * 180c2ed51d1SGreg Roach * @param string $value 181c2ed51d1SGreg Roach * 182c2ed51d1SGreg Roach * @return string 183c2ed51d1SGreg Roach */ 184c2ed51d1SGreg Roach public function escape(string $value): string 185c2ed51d1SGreg Roach { 186c2ed51d1SGreg Roach return strtr($value, ['@' => '@@']); 187c2ed51d1SGreg Roach } 188c2ed51d1SGreg Roach 189c2ed51d1SGreg Roach /** 190c2ed51d1SGreg Roach * Create a label for this element. 191c2ed51d1SGreg Roach * 192c2ed51d1SGreg Roach * @return string 193c2ed51d1SGreg Roach */ 194c2ed51d1SGreg Roach public function label(): string 195c2ed51d1SGreg Roach { 196c2ed51d1SGreg Roach return $this->label; 197c2ed51d1SGreg Roach } 198c2ed51d1SGreg Roach 199c2ed51d1SGreg Roach /** 200c2ed51d1SGreg Roach * Create a label/value pair for this element. 201c2ed51d1SGreg Roach * 202c2ed51d1SGreg Roach * @param string $value 203c2ed51d1SGreg Roach * @param Tree $tree 204c2ed51d1SGreg Roach * 205c2ed51d1SGreg Roach * @return string 206c2ed51d1SGreg Roach */ 207c2ed51d1SGreg Roach public function labelValue(string $value, Tree $tree): string 208c2ed51d1SGreg Roach { 209c2ed51d1SGreg Roach $label = '<span class="label">' . $this->label() . '</span>'; 2108da28f8eSGreg Roach $value = '<span class="value align-top">' . $this->value($value, $tree) . '</span>'; 211c2ed51d1SGreg Roach $html = I18N::translate(/* I18N: e.g. "Occupation: farmer" */ '%1$s: %2$s', $label, $value); 212c2ed51d1SGreg Roach 213c2ed51d1SGreg Roach return '<div>' . $html . '</div>'; 214c2ed51d1SGreg Roach } 215c2ed51d1SGreg Roach 216c2ed51d1SGreg Roach /** 2174dbb2a39SGreg Roach * Set, remove or replace a subtag. 2184dbb2a39SGreg Roach * 2194dbb2a39SGreg Roach * @param string $subtag 2204dbb2a39SGreg Roach * @param string $repeat 2214da96842SGreg Roach * @param string $before 2224dbb2a39SGreg Roach * 2234dbb2a39SGreg Roach * @return void 2244dbb2a39SGreg Roach */ 225efd4768bSGreg Roach public function subtag(string $subtag, string $repeat = '0:1', string $before = ''): void 2264dbb2a39SGreg Roach { 2274dbb2a39SGreg Roach if ($repeat === '') { 2284dbb2a39SGreg Roach unset($this->subtags[$subtag]); 2294da96842SGreg Roach } elseif ($before === '' || ($this->subtags[$before] ?? null) === null) { 2304dbb2a39SGreg Roach $this->subtags[$subtag] = $repeat; 2314dbb2a39SGreg Roach } else { 2324dbb2a39SGreg Roach $tmp = []; 2334dbb2a39SGreg Roach 2344dbb2a39SGreg Roach foreach ($this->subtags as $key => $value) { 2354da96842SGreg Roach if ($key === $before) { 236efd4768bSGreg Roach $tmp[$subtag] = $repeat; 2374dbb2a39SGreg Roach } 2384da96842SGreg Roach $tmp[$key] = $value; 2394dbb2a39SGreg Roach } 2404dbb2a39SGreg Roach 2414dbb2a39SGreg Roach $this->subtags = $tmp; 2424dbb2a39SGreg Roach } 2434dbb2a39SGreg Roach } 2444dbb2a39SGreg Roach 2454dbb2a39SGreg Roach /** 246c2ed51d1SGreg Roach * @return array<string,string> 247c2ed51d1SGreg Roach */ 2483d2c98d1SGreg Roach public function subtags(): array 249c2ed51d1SGreg Roach { 250c2ed51d1SGreg Roach return $this->subtags; 251c2ed51d1SGreg Roach } 252c2ed51d1SGreg Roach 253c2ed51d1SGreg Roach /** 254c2ed51d1SGreg Roach * Display the value of this type of element. 255c2ed51d1SGreg Roach * 256c2ed51d1SGreg Roach * @param string $value 257c2ed51d1SGreg Roach * @param Tree $tree 258c2ed51d1SGreg Roach * 259c2ed51d1SGreg Roach * @return string 260c2ed51d1SGreg Roach */ 261c2ed51d1SGreg Roach public function value(string $value, Tree $tree): string 262c2ed51d1SGreg Roach { 263c2ed51d1SGreg Roach $values = $this->values(); 264c2ed51d1SGreg Roach 265c2ed51d1SGreg Roach if ($values === []) { 266c2ed51d1SGreg Roach if (str_contains($value, "\n")) { 267c2ed51d1SGreg Roach return '<span dir="auto" class="d-inline-block" style="white-space: pre-wrap;">' . e($value) . '</span>'; 268c2ed51d1SGreg Roach } 269c2ed51d1SGreg Roach 270c2ed51d1SGreg Roach return '<span dir="auto">' . e($value) . '</span>'; 271c2ed51d1SGreg Roach } 272c2ed51d1SGreg Roach 273c2ed51d1SGreg Roach $canonical = $this->canonical($value); 274c2ed51d1SGreg Roach 275c2ed51d1SGreg Roach return $values[$canonical] ?? '<span dir="auto">' . e($value) . '</span>'; 276c2ed51d1SGreg Roach } 277c2ed51d1SGreg Roach 278c2ed51d1SGreg Roach /** 279c2ed51d1SGreg Roach * A list of controlled values for this element 280c2ed51d1SGreg Roach * 281c2ed51d1SGreg Roach * @return array<int|string,string> 282c2ed51d1SGreg Roach */ 283c2ed51d1SGreg Roach public function values(): array 284c2ed51d1SGreg Roach { 285c2ed51d1SGreg Roach return []; 286c2ed51d1SGreg Roach } 287c2ed51d1SGreg Roach 288c2ed51d1SGreg Roach /** 289cef4fcc0SGreg Roach * Display the value of this type of element - convert URLs to links. 290c2ed51d1SGreg Roach * 291c2ed51d1SGreg Roach * @param string $value 292c2ed51d1SGreg Roach * 293c2ed51d1SGreg Roach * @return string 294c2ed51d1SGreg Roach */ 295c2ed51d1SGreg Roach protected function valueAutoLink(string $value): string 296c2ed51d1SGreg Roach { 297a3287c67SGreg Roach $canonical = $this->canonical($value); 298c2ed51d1SGreg Roach 299*8392edd9SGreg Roach if (str_contains($canonical, 'http://') || str_contains($canonical, 'https://')) { 300*8392edd9SGreg Roach $html = Registry::markdownFactory()->autolink()->convertToHtml($canonical); 301*8392edd9SGreg Roach 302*8392edd9SGreg Roach return strip_tags($html, ['a']); 303a3287c67SGreg Roach } 304c2ed51d1SGreg Roach 305a3287c67SGreg Roach return e($canonical); 306c2ed51d1SGreg Roach } 307c2ed51d1SGreg Roach 308c2ed51d1SGreg Roach /** 309cef4fcc0SGreg Roach * Display the value of this type of element - convert to URL. 310cef4fcc0SGreg Roach * 311cef4fcc0SGreg Roach * @param string $value 312cef4fcc0SGreg Roach * 313cef4fcc0SGreg Roach * @return string 314cef4fcc0SGreg Roach */ 315cef4fcc0SGreg Roach protected function valueLink(string $value): string 316cef4fcc0SGreg Roach { 317cef4fcc0SGreg Roach $canonical = $this->canonical($value); 318cef4fcc0SGreg Roach 319cef4fcc0SGreg Roach if (str_starts_with($canonical, 'https://') || str_starts_with($canonical, 'http://')) { 320cef4fcc0SGreg Roach return '<a dir="auto" href="' . e($canonical) . '">' . e($value) . '</a>'; 321cef4fcc0SGreg Roach } 322cef4fcc0SGreg Roach 323cef4fcc0SGreg Roach return e($value); 324cef4fcc0SGreg Roach } 325cef4fcc0SGreg Roach 326cef4fcc0SGreg Roach /** 327c2ed51d1SGreg Roach * Display the value of this type of element. 328c2ed51d1SGreg Roach * 329c2ed51d1SGreg Roach * @param string $value 330c2ed51d1SGreg Roach * 331c2ed51d1SGreg Roach * @return string 332c2ed51d1SGreg Roach */ 333c2ed51d1SGreg Roach public function valueNumeric(string $value): string 334c2ed51d1SGreg Roach { 335c2ed51d1SGreg Roach $canonical = $this->canonical($value); 336c2ed51d1SGreg Roach 337c2ed51d1SGreg Roach if (is_numeric($canonical)) { 338c2ed51d1SGreg Roach return I18N::number((int) $canonical); 339c2ed51d1SGreg Roach } 340c2ed51d1SGreg Roach 341c2ed51d1SGreg Roach return e($value); 342c2ed51d1SGreg Roach } 343c2ed51d1SGreg Roach} 344