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