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