1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2022 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\Registry; 26use Fisharebest\Webtrees\Tree; 27 28use function array_key_exists; 29use function array_map; 30use function e; 31use function is_numeric; 32use function nl2br; 33use function preg_replace; 34use function str_contains; 35use function str_starts_with; 36use function strip_tags; 37use function trim; 38use function view; 39 40/** 41 * A GEDCOM element is a tag/primitive in a GEDCOM file. 42 */ 43abstract class AbstractElement implements ElementInterface 44{ 45 // HTML attributes for an <input> 46 protected const MAXIMUM_LENGTH = false; 47 protected const PATTERN = false; 48 49 private const WHITESPACE_LINE = [ 50 "\t" => ' ', 51 "\n" => ' ', 52 "\r" => ' ', 53 "\v" => ' ', // Vertical tab 54 "\u{85}" => ' ', // NEL - newline 55 "\u{2028}" => ' ', // LS - line separator 56 "\u{2029}" => ' ', // PS - paragraph separator 57 ]; 58 59 private const WHITESPACE_TEXT = [ 60 "\t" => ' ', 61 "\r\n" => "\n", 62 "\r" => "\n", 63 "\v" => "\n", 64 "\u{85}" => "\n", 65 "\u{2028}" => "\n", 66 "\u{2029}" => "\n\n", 67 ]; 68 69 // Which child elements can appear under this element. 70 protected const SUBTAGS = []; 71 72 // A label to describe this element 73 private string $label; 74 75 /** @var array<string,string> Subtags of this element */ 76 private array $subtags; 77 78 /** 79 * AbstractGedcomElement constructor. 80 * 81 * @param string $label 82 * @param array<string>|null $subtags 83 */ 84 public function __construct(string $label, array $subtags = null) 85 { 86 $this->label = $label; 87 $this->subtags = $subtags ?? static::SUBTAGS; 88 } 89 90 /** 91 * Convert a value to a canonical form. 92 * 93 * @param string $value 94 * 95 * @return string 96 */ 97 public function canonical(string $value): string 98 { 99 $value = strtr($value, self::WHITESPACE_LINE); 100 101 while (str_contains($value, ' ')) { 102 $value = strtr($value, [' ' => ' ']); 103 } 104 105 return trim($value); 106 } 107 108 /** 109 * Convert a multi-line value to a canonical form. 110 * 111 * @param string $value 112 * 113 * @return string 114 */ 115 protected function canonicalText(string $value): string 116 { 117 $value = strtr($value, self::WHITESPACE_TEXT); 118 119 return trim($value, "\n"); 120 } 121 122 /** 123 * Should we collapse the children of this element when editing? 124 * 125 * @return bool 126 */ 127 public function collapseChildren(): bool 128 { 129 return false; 130 } 131 132 /** 133 * Create a default value for this element. 134 * 135 * @param Tree $tree 136 * 137 * @return string 138 */ 139 public function default(Tree $tree): string 140 { 141 return ''; 142 } 143 144 /** 145 * An edit control for this data. 146 * 147 * @param string $id 148 * @param string $name 149 * @param string $value 150 * @param Tree $tree 151 * 152 * @return string 153 */ 154 public function edit(string $id, string $name, string $value, Tree $tree): string 155 { 156 $values = $this->values(); 157 158 if ($values !== []) { 159 $value = $this->canonical($value); 160 161 // Ensure the current data is in the list. 162 if (!array_key_exists($value, $values)) { 163 $values = [$value => $value] + $values; 164 } 165 166 // We may use markup to display values, but not when editing them. 167 $values = array_map(static fn (string $x): string => strip_tags($x), $values); 168 169 return view('components/select', [ 170 'id' => $id, 171 'name' => $name, 172 'options' => $values, 173 'selected' => $value, 174 ]); 175 } 176 177 $attributes = [ 178 'class' => 'form-control', 179 'dir' => 'auto', 180 'type' => 'text', 181 'id' => $id, 182 'name' => $name, 183 'value' => $value, 184 'maxlength' => static::MAXIMUM_LENGTH, 185 'pattern' => static::PATTERN, 186 ]; 187 188 return '<input ' . Html::attributes($attributes) . ' />'; 189 } 190 191 /** 192 * An edit control for this data. 193 * 194 * @param string $id 195 * @param string $name 196 * @param string $value 197 * 198 * @return string 199 */ 200 public function editHidden(string $id, string $name, string $value): string 201 { 202 return '<input class="form-control" type="hidden" id="' . e($id) . '" name="' . e($name) . '" value="' . e($value) . '" />'; 203 } 204 205 /** 206 * An edit control for this data. 207 * 208 * @param string $id 209 * @param string $name 210 * @param string $value 211 * 212 * @return string 213 */ 214 public function editTextArea(string $id, string $name, string $value): string 215 { 216 return '<textarea class="form-control" id="' . e($id) . '" name="' . e($name) . '" rows="3" dir="auto">' . e($value) . '</textarea>'; 217 } 218 219 /** 220 * Escape @ signs in a GEDCOM export. 221 * 222 * @param string $value 223 * 224 * @return string 225 */ 226 public function escape(string $value): string 227 { 228 return strtr($value, ['@' => '@@']); 229 } 230 231 /** 232 * Create a label for this element. 233 * 234 * @return string 235 */ 236 public function label(): string 237 { 238 return $this->label; 239 } 240 241 /** 242 * Create a label/value pair for this element. 243 * 244 * @param string $value 245 * @param Tree $tree 246 * 247 * @return string 248 */ 249 public function labelValue(string $value, Tree $tree): string 250 { 251 $label = '<span class="label">' . $this->label() . '</span>'; 252 $value = '<span class="value align-top">' . $this->value($value, $tree) . '</span>'; 253 $html = I18N::translate(/* I18N: e.g. "Occupation: farmer" */ '%1$s: %2$s', $label, $value); 254 255 return '<div>' . $html . '</div>'; 256 } 257 258 /** 259 * Set, remove or replace a subtag. 260 * 261 * @param string $subtag 262 * @param string $repeat 263 * @param string $before 264 * 265 * @return void 266 */ 267 public function subtag(string $subtag, string $repeat, string $before = ''): void 268 { 269 if ($before === '' || ($this->subtags[$before] ?? null) === null) { 270 $this->subtags[$subtag] = $repeat; 271 } else { 272 $tmp = []; 273 274 foreach ($this->subtags as $key => $value) { 275 if ($key === $before) { 276 $tmp[$subtag] = $repeat; 277 } 278 $tmp[$key] = $value; 279 } 280 281 $this->subtags = $tmp; 282 } 283 } 284 285 /** 286 * @return array<string,string> 287 */ 288 public function subtags(): array 289 { 290 return $this->subtags; 291 } 292 293 /** 294 * Display the value of this type of element. 295 * 296 * @param string $value 297 * @param Tree $tree 298 * 299 * @return string 300 */ 301 public function value(string $value, Tree $tree): string 302 { 303 $values = $this->values(); 304 305 if ($values === []) { 306 if (str_contains($value, "\n")) { 307 return '<span class="ut d-inline-block">' . nl2br(e($value, false)) . '</span>'; 308 } 309 310 return '<span class="ut">' . e($value) . '</span>'; 311 } 312 313 $canonical = $this->canonical($value); 314 315 return $values[$canonical] ?? '<bdi>' . e($value) . '</bdi>'; 316 } 317 318 /** 319 * A list of controlled values for this element 320 * 321 * @return array<int|string,string> 322 */ 323 public function values(): array 324 { 325 return []; 326 } 327 328 /** 329 * Display the value of this type of element - convert URLs to links. 330 * 331 * @param string $value 332 * 333 * @return string 334 */ 335 protected function valueAutoLink(string $value): string 336 { 337 $canonical = $this->canonical($value); 338 339 if (str_contains($canonical, 'http://') || str_contains($canonical, 'https://')) { 340 $html = Registry::markdownFactory()->autolink($canonical); 341 $html = strip_tags($html, ['a', 'br']); 342 } else { 343 $html = nl2br(e($canonical), false); 344 } 345 346 if (str_contains($html, '<br>')) { 347 return '<span class="ut d-inline-block">' . $html . '</span>'; 348 } 349 350 return '<span class="ut">' . $html . '</span>'; 351 } 352 353 /** 354 * Display the value of this type of element - multi-line text with/without markdown. 355 * 356 * @param string $value 357 * @param Tree $tree 358 * 359 * @return string 360 */ 361 protected function valueFormatted(string $value, Tree $tree): string 362 { 363 $canonical = $this->canonical($value); 364 365 $format = $tree->getPreference('FORMAT_TEXT'); 366 367 switch ($format) { 368 case 'markdown': 369 return Registry::markdownFactory()->markdown($canonical, $tree); 370 371 default: 372 return Registry::markdownFactory()->autolink($canonical, $tree); 373 } 374 } 375 376 /** 377 * Display the value of this type of element - convert to URL. 378 * 379 * @param string $value 380 * 381 * @return string 382 */ 383 protected function valueLink(string $value): string 384 { 385 $canonical = $this->canonical($value); 386 387 if (str_starts_with($canonical, 'https://') || str_starts_with($canonical, 'http://')) { 388 return '<a dir="auto" href="' . e($canonical) . '">' . e($value) . '</a>'; 389 } 390 391 return e($value); 392 } 393 394 /** 395 * Display the value of this type of element. 396 * 397 * @param string $value 398 * 399 * @return string 400 */ 401 public function valueNumeric(string $value): string 402 { 403 $canonical = $this->canonical($value); 404 405 if (is_numeric($canonical)) { 406 return I18N::number((int) $canonical); 407 } 408 409 return e($value); 410 } 411} 412