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 // 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 ($before === '' || ($this->subtags[$before] ?? null) === null) { 254 $this->subtags[$subtag] = $repeat; 255 } else { 256 $tmp = []; 257 258 foreach ($this->subtags as $key => $value) { 259 if ($key === $before) { 260 $tmp[$subtag] = $repeat; 261 } 262 $tmp[$key] = $value; 263 } 264 265 $this->subtags = $tmp; 266 } 267 } 268 269 /** 270 * @return array<string,string> 271 */ 272 public function subtags(): array 273 { 274 return $this->subtags; 275 } 276 277 /** 278 * Display the value of this type of element. 279 * 280 * @param string $value 281 * @param Tree $tree 282 * 283 * @return string 284 */ 285 public function value(string $value, Tree $tree): string 286 { 287 $values = $this->values(); 288 289 if ($values === []) { 290 if (str_contains($value, "\n")) { 291 return '<bdi class="d-inline-block">' . nl2br(e($value, false)) . '</bdi>'; 292 } 293 294 return '<bdi>' . e($value) . '</bdi>'; 295 } 296 297 $canonical = $this->canonical($value); 298 299 return $values[$canonical] ?? '<bdi>' . e($value) . '</bdi>'; 300 } 301 302 /** 303 * A list of controlled values for this element 304 * 305 * @return array<int|string,string> 306 */ 307 public function values(): array 308 { 309 return []; 310 } 311 312 /** 313 * Display the value of this type of element - convert URLs to links. 314 * 315 * @param string $value 316 * 317 * @return string 318 */ 319 protected function valueAutoLink(string $value): string 320 { 321 $canonical = $this->canonical($value); 322 323 if (str_contains($canonical, 'http://') || str_contains($canonical, 'https://')) { 324 $html = Registry::markdownFactory()->autolink($canonical); 325 326 return strip_tags($html, ['a']); 327 } 328 329 return e($canonical); 330 } 331 332 /** 333 * Display the value of this type of element - multi-line text with/without markdown. 334 * 335 * @param string $value 336 * @param Tree $tree 337 * 338 * @return string 339 */ 340 protected function valueFormatted(string $value, Tree $tree): string 341 { 342 $canonical = $this->canonical($value); 343 344 $format = $tree->getPreference('FORMAT_TEXT'); 345 346 switch ($format) { 347 case 'markdown': 348 $html = Registry::markdownFactory()->markdown($canonical, $tree); 349 350 return '<div class="markdown" dir="auto">' . $html . '</div>'; 351 352 default: 353 $html = Registry::markdownFactory()->autolink($canonical, $tree); 354 355 if (str_contains($html, "\n")) { 356 return '<div class="markdown" dir="auto">' . $html . '</div>'; 357 } 358 359 return '<span class="markdown" dir="auto">' . $html . '</span>'; 360 } 361 } 362 363 /** 364 * Display the value of this type of element - convert to URL. 365 * 366 * @param string $value 367 * 368 * @return string 369 */ 370 protected function valueLink(string $value): string 371 { 372 $canonical = $this->canonical($value); 373 374 if (str_starts_with($canonical, 'https://') || str_starts_with($canonical, 'http://')) { 375 return '<a dir="auto" href="' . e($canonical) . '">' . e($value) . '</a>'; 376 } 377 378 return e($value); 379 } 380 381 /** 382 * Display the value of this type of element. 383 * 384 * @param string $value 385 * 386 * @return string 387 */ 388 public function valueNumeric(string $value): string 389 { 390 $canonical = $this->canonical($value); 391 392 if (is_numeric($canonical)) { 393 return I18N::number((int) $canonical); 394 } 395 396 return e($value); 397 } 398} 399