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