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