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 array_search; 30use function e; 31use function is_numeric; 32use function preg_match; 33use function strpos; 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 /** @var string A label to describe this element */ 52 private $label; 53 54 /** @var array<string,string> */ 55 private $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 (strpos($value, ' ') !== false) { 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('strip_tags', $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 'type' => 'text', 135 'id' => $id, 136 'name' => $name, 137 'value' => $value, 138 'maxlength' => static::MAXIMUM_LENGTH, 139 'pattern' => static::PATTERN, 140 ]; 141 142 return '<input ' . Html::attributes($attributes) . ' />'; 143 } 144 145 /** 146 * An edit control for this data. 147 * 148 * @param string $id 149 * @param string $name 150 * @param string $value 151 * 152 * @return string 153 */ 154 public function editHidden(string $id, string $name, string $value): string 155 { 156 return '<input class="form-control" type="hidden" id="' . e($id) . '" name="' . e($name) . '" value="' . e($value) . '" />'; 157 } 158 159 /** 160 * An edit control for this data. 161 * 162 * @param string $id 163 * @param string $name 164 * @param string $value 165 * 166 * @return string 167 */ 168 public function editTextArea(string $id, string $name, string $value): string 169 { 170 return '<textarea class="form-control" id="' . e($id) . '" name="' . e($name) . '" rows="5" dir="auto">' . e($value) . '</textarea>'; 171 } 172 173 /** 174 * Escape @ signs in a GEDCOM export. 175 * 176 * @param string $value 177 * 178 * @return string 179 */ 180 public function escape(string $value): string 181 { 182 return strtr($value, ['@' => '@@']); 183 } 184 185 /** 186 * Create a label for this element. 187 * 188 * @return string 189 */ 190 public function label(): string 191 { 192 return $this->label; 193 } 194 195 /** 196 * Create a label/value pair for this element. 197 * 198 * @param string $value 199 * @param Tree $tree 200 * 201 * @return string 202 */ 203 public function labelValue(string $value, Tree $tree): string 204 { 205 $label = '<span class="label">' . $this->label() . '</span>'; 206 $value = '<span class="value">' . $this->value($value, $tree) . '</span>'; 207 $html = I18N::translate(/* I18N: e.g. "Occupation: farmer" */ '%1$s: %2$s', $label, $value); 208 209 return '<div>' . $html . '</div>'; 210 } 211 212 /** 213 * Set, remove or replace a subtag. 214 * 215 * @param string $subtag 216 * @param string $repeat 217 * @param string $after 218 * 219 * @return void 220 */ 221 public function subtag(string $subtag, string $repeat = '', string $after = ''): void 222 { 223 if ($repeat === '') { 224 unset($this->subtags[$subtag]); 225 } elseif ($after === '' || ($this->subtags[$subtag] ?? null) === null) { 226 $this->subtags[$subtag] = $repeat; 227 } else { 228 $tmp = []; 229 230 foreach ($this->subtags as $key => $value) { 231 $tmp[$key] = $value; 232 233 if ($key === $after) { 234 $tmp[] = $repeat; 235 } 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