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