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