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