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