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