1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2021 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Elements; 21 22use Fisharebest\Webtrees\Contracts\ElementInterface; 23use Fisharebest\Webtrees\Html; 24use Fisharebest\Webtrees\I18N; 25use Fisharebest\Webtrees\Tree; 26 27use function array_key_exists; 28use function array_map; 29use function e; 30use function is_numeric; 31use function preg_match; 32use function str_contains; 33use function strip_tags; 34use function trim; 35use function view; 36 37/** 38 * A GEDCOM element is a tag/primitive in a GEDCOM file. 39 */ 40abstract class AbstractElement implements ElementInterface 41{ 42 protected const REGEX_URL = '~((https?|ftp]):)(//([^\s/?#<>]*))?([^\s?#<>]*)(\?([^\s#<>]*))?(#[^\s?#<>]+)?~'; 43 44 // HTML attributes for an <input> 45 protected const MAXIMUM_LENGTH = false; 46 protected const PATTERN = false; 47 48 // Which child elements can appear under this element. 49 protected const SUBTAGS = []; 50 51 // A label to describe this element 52 private string $label; 53 54 /** @var array<string,string> Subtags of this element */ 55 private array $subtags; 56 57 /** 58 * AbstractGedcomElement constructor. 59 * 60 * @param string $label 61 * @param array<string>|null $subtags 62 */ 63 public function __construct(string $label, array $subtags = null) 64 { 65 $this->label = $label; 66 $this->subtags = $subtags ?? static::SUBTAGS; 67 } 68 69 /** 70 * Convert a value to a canonical form. 71 * 72 * @param string $value 73 * 74 * @return string 75 */ 76 public function canonical(string $value): string 77 { 78 $value = strtr($value, ["\t" => ' ', "\r" => ' ', "\n" => ' ']); 79 80 while (str_contains($value, ' ')) { 81 $value = strtr($value, [' ' => ' ']); 82 } 83 84 return trim($value); 85 } 86 87 /** 88 * Create a default value for this element. 89 * 90 * @param Tree $tree 91 * 92 * @return string 93 */ 94 public function default(Tree $tree): string 95 { 96 return ''; 97 } 98 99 /** 100 * An edit control for this data. 101 * 102 * @param string $id 103 * @param string $name 104 * @param string $value 105 * @param Tree $tree 106 * 107 * @return string 108 */ 109 public function edit(string $id, string $name, string $value, Tree $tree): string 110 { 111 $values = $this->values(); 112 113 if ($values !== []) { 114 $value = $this->canonical($value); 115 116 // Ensure the current data is in the list. 117 if (!array_key_exists($value, $values)) { 118 $values = [$value => $value] + $values; 119 } 120 121 // We may use markup to display values, but not when editing them. 122 $values = array_map(fn (string $x): string => strip_tags($x), $values); 123 124 return view('components/select', [ 125 'id' => $id, 126 'name' => $name, 127 'options' => $values, 128 'selected' => $value, 129 ]); 130 } 131 132 $attributes = [ 133 'class' => 'form-control', 134 'type' => 'text', 135 'id' => $id, 136 'name' => $name, 137 'value' => $value, 138 'maxlength' => static::MAXIMUM_LENGTH, 139 'pattern' => static::PATTERN, 140 ]; 141 142 return '<input ' . Html::attributes($attributes) . ' />'; 143 } 144 145 /** 146 * An edit control for this data. 147 * 148 * @param string $id 149 * @param string $name 150 * @param string $value 151 * 152 * @return string 153 */ 154 public function editHidden(string $id, string $name, string $value): string 155 { 156 return '<input class="form-control" type="hidden" id="' . e($id) . '" name="' . e($name) . '" value="' . e($value) . '" />'; 157 } 158 159 /** 160 * An edit control for this data. 161 * 162 * @param string $id 163 * @param string $name 164 * @param string $value 165 * 166 * @return string 167 */ 168 public function editTextArea(string $id, string $name, string $value): string 169 { 170 return '<textarea class="form-control" id="' . e($id) . '" name="' . e($name) . '" rows="5" dir="auto">' . e($value) . '</textarea>'; 171 } 172 173 /** 174 * Escape @ signs in a GEDCOM export. 175 * 176 * @param string $value 177 * 178 * @return string 179 */ 180 public function escape(string $value): string 181 { 182 return strtr($value, ['@' => '@@']); 183 } 184 185 /** 186 * Create a label for this element. 187 * 188 * @return string 189 */ 190 public function label(): string 191 { 192 return $this->label; 193 } 194 195 /** 196 * Create a label/value pair for this element. 197 * 198 * @param string $value 199 * @param Tree $tree 200 * 201 * @return string 202 */ 203 public function labelValue(string $value, Tree $tree): string 204 { 205 $label = '<span class="label">' . $this->label() . '</span>'; 206 $value = '<span class="value align-top">' . $this->value($value, $tree) . '</span>'; 207 $html = I18N::translate(/* I18N: e.g. "Occupation: farmer" */ '%1$s: %2$s', $label, $value); 208 209 return '<div>' . $html . '</div>'; 210 } 211 212 /** 213 * Set, remove or replace a subtag. 214 * 215 * @param string $subtag 216 * @param string $repeat 217 * @param string $before 218 * 219 * @return void 220 */ 221 public function subtag(string $subtag, string $repeat = '0:1', string $before = ''): void 222 { 223 if ($repeat === '') { 224 unset($this->subtags[$subtag]); 225 } elseif ($before === '' || ($this->subtags[$before] ?? null) === null) { 226 $this->subtags[$subtag] = $repeat; 227 } else { 228 $tmp = []; 229 230 foreach ($this->subtags as $key => $value) { 231 if ($key === $before) { 232 $tmp[$subtag] = $repeat; 233 } 234 $tmp[$key] = $value; 235 } 236 237 $this->subtags = $tmp; 238 } 239 } 240 241 /** 242 * @return array<string,string> 243 */ 244 public function subtags(): array 245 { 246 return $this->subtags; 247 } 248 249 /** 250 * Display the value of this type of element. 251 * 252 * @param string $value 253 * @param Tree $tree 254 * 255 * @return string 256 */ 257 public function value(string $value, Tree $tree): string 258 { 259 $values = $this->values(); 260 261 if ($values === []) { 262 if (str_contains($value, "\n")) { 263 return '<span dir="auto" class="d-inline-block" style="white-space: pre-wrap;">' . e($value) . '</span>'; 264 } 265 266 return '<span dir="auto">' . e($value) . '</span>'; 267 } 268 269 $canonical = $this->canonical($value); 270 271 return $values[$canonical] ?? '<span dir="auto">' . e($value) . '</span>'; 272 } 273 274 /** 275 * A list of controlled values for this element 276 * 277 * @return array<int|string,string> 278 */ 279 public function values(): array 280 { 281 return []; 282 } 283 284 /** 285 * Display the value of this type of element - convert URLs to links 286 * 287 * @param string $value 288 * 289 * @return string 290 */ 291 protected function valueAutoLink(string $value): string 292 { 293 $canonical = $this->canonical($value); 294 295 if (preg_match(static::REGEX_URL, $canonical)) { 296 return '<a href="' . e($canonical) . '" rel="no-follow">' . e($canonical) . '</a>'; 297 } 298 299 return e($canonical); 300 } 301 302 /** 303 * Display the value of this type of element. 304 * 305 * @param string $value 306 * 307 * @return string 308 */ 309 public function valueNumeric(string $value): string 310 { 311 $canonical = $this->canonical($value); 312 313 if (is_numeric($canonical)) { 314 return I18N::number((int) $canonical); 315 } 316 317 return e($value); 318 } 319} 320