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; 26use League\CommonMark\Block\Element\Document; 27use League\CommonMark\Block\Element\Paragraph; 28use League\CommonMark\Block\Renderer\DocumentRenderer; 29use League\CommonMark\Block\Renderer\ParagraphRenderer; 30use League\CommonMark\CommonMarkConverter; 31use League\CommonMark\Environment; 32use League\CommonMark\Inline\Element\Link; 33use League\CommonMark\Inline\Element\Text; 34use League\CommonMark\Inline\Parser\AutolinkParser; 35use League\CommonMark\Inline\Renderer\LinkRenderer; 36use League\CommonMark\Inline\Renderer\TextRenderer; 37 38use function array_key_exists; 39use function array_map; 40use function e; 41use function is_numeric; 42use function preg_replace; 43use function strpos; 44use function trim; 45use function view; 46 47/** 48 * A GEDCOM element is a tag/primitive in a GEDCOM file. 49 */ 50abstract class AbstractElement implements ElementInterface 51{ 52 protected const REGEX_URL = '~((https?|ftp]):)(//([^\s/?#<>]*))?([^\s?#<>]*)(\?([^\s#<>]*))?(#[^\s?#<>]+)?~'; 53 54 // HTML attributes for an <input> 55 protected const MAX_LENGTH = false; 56 57 // Which child elements can appear under this element. 58 protected const SUBTAGS = []; 59 60 /** @var string A label to describe this element */ 61 private $label; 62 63 /** @var array<string,string> */ 64 private $subtags; 65 66 /** 67 * AbstractGedcomElement constructor. 68 * 69 * @param string $label 70 * @param array<string>|null $subtags 71 */ 72 public function __construct(string $label, array $subtags = null) 73 { 74 $this->label = $label; 75 $this->subtags = $subtags ?? static::SUBTAGS; 76 } 77 78 /** 79 * Convert a value to a canonical form. 80 * 81 * @param string $value 82 * 83 * @return string 84 */ 85 public function canonical(string $value): string 86 { 87 $value = strtr($value, ["\t" => ' ', "\r" => ' ', "\n" => ' ']); 88 89 while (strpos($value, ' ') !== false) { 90 $value = strtr($value, [' ' => ' ']); 91 } 92 93 return trim($value); 94 } 95 96 /** 97 * Create a default value for this element. 98 * 99 * @param Tree $tree 100 * 101 * @return string 102 */ 103 public function default(Tree $tree): string 104 { 105 return ''; 106 } 107 108 /** 109 * An edit control for this data. 110 * 111 * @param string $id 112 * @param string $name 113 * @param string $value 114 * @param Tree $tree 115 * 116 * @return string 117 */ 118 public function edit(string $id, string $name, string $value, Tree $tree): string 119 { 120 $values = $this->values(); 121 122 if ($values !== []) { 123 $value = $this->canonical($value); 124 125 // Ensure the current data is in the list. 126 if (!array_key_exists($value, $values)) { 127 $values = [$value => $value] + $values; 128 } 129 130 // We may use markup to display values, but not when editing them. 131 $values = array_map('strip_tags', $values); 132 133 return view('components/select', [ 134 'id' => $id, 135 'name' => $name, 136 'options' => $values, 137 'selected' => $value, 138 ]); 139 } 140 141 $attributes = [ 142 'class' => 'form-control', 143 'type' => 'text', 144 'id' => $id, 145 'name' => $name, 146 'value' => $value, 147 'maxvalue' => static::MAX_LENGTH, 148 ]; 149 150 return '<input ' . Html::attributes($attributes) . '">'; 151 } 152 153 /** 154 * An edit control for this data. 155 * 156 * @param string $id 157 * @param string $name 158 * @param string $value 159 * 160 * @return string 161 */ 162 public function editHidden(string $id, string $name, string $value): string 163 { 164 return '<input class="form-control" type="hidden" id="' . e($id) . '" name="' . e($name) . '" value="' . e($value) . '">'; 165 } 166 167 /** 168 * An edit control for this data. 169 * 170 * @param string $id 171 * @param string $name 172 * @param string $value 173 * 174 * @return string 175 */ 176 public function editTextArea(string $id, string $name, string $value): string 177 { 178 return '<textarea class="form-control" id="' . e($id) . '" name="' . e($name) . '" rows="5" dir="auto">' . e($value) . '</textarea>'; 179 } 180 181 /** 182 * Escape @ signs in a GEDCOM export. 183 * 184 * @param string $value 185 * 186 * @return string 187 */ 188 public function escape(string $value): string 189 { 190 return strtr($value, ['@' => '@@']); 191 } 192 193 /** 194 * Create a label for this element. 195 * 196 * @return string 197 */ 198 public function label(): string 199 { 200 return $this->label; 201 } 202 203 /** 204 * Create a label/value pair for this element. 205 * 206 * @param string $value 207 * @param Tree $tree 208 * 209 * @return string 210 */ 211 public function labelValue(string $value, Tree $tree): string 212 { 213 $label = '<span class="label">' . $this->label() . '</span>'; 214 $value = '<span class="value">' . $this->value($value, $tree) . '</span>'; 215 $html = I18N::translate(/* I18N: e.g. "Occupation: farmer" */ '%1$s: %2$s', $label, $value); 216 217 return '<div>' . $html . '</div>'; 218 } 219 220 /** 221 * @param Tree $tree 222 * 223 * @return array<string,string> 224 */ 225 public function subtags(Tree $tree): array 226 { 227 return $this->subtags; 228 } 229 230 /** 231 * Display the value of this type of element. 232 * 233 * @param string $value 234 * @param Tree $tree 235 * 236 * @return string 237 */ 238 public function value(string $value, Tree $tree): string 239 { 240 $values = $this->values(); 241 242 if ($values === []) { 243 if (str_contains($value, "\n")) { 244 return '<span dir="auto" class="d-inline-block" style="white-space: pre-wrap;">' . e($value) . '</span>'; 245 } 246 247 return '<span dir="auto">' . e($value) . '</span>'; 248 } 249 250 $canonical = $this->canonical($value); 251 252 return $values[$canonical] ?? '<span dir="auto">' . e($value) . '</span>'; 253 } 254 255 /** 256 * A list of controlled values for this element 257 * 258 * @return array<int|string,string> 259 */ 260 public function values(): array 261 { 262 return []; 263 } 264 265 /** 266 * Display the value of this type of element - convert URLs to links 267 * 268 * @param string $value 269 * 270 * @return string 271 */ 272 protected function valueAutoLink(string $value): string 273 { 274 // Convert URLs into markdown auto-links. 275 $value = preg_replace(self::REGEX_URL, '<$0>', $value); 276 277 // Create a minimal commonmark processor - just add support for autolinks. 278 $environment = new Environment(); 279 $environment 280 ->addBlockRenderer(Document::class, new DocumentRenderer()) 281 ->addBlockRenderer(Paragraph::class, new ParagraphRenderer()) 282 ->addInlineRenderer(Text::class, new TextRenderer()) 283 ->addInlineRenderer(Link::class, new LinkRenderer()) 284 ->addInlineParser(new AutolinkParser()); 285 286 $converter = new CommonMarkConverter(['html_input' => Environment::HTML_INPUT_ESCAPE], $environment); 287 288 return $converter->convertToHtml($value); 289 } 290 291 /** 292 * Display the value of this type of element. 293 * 294 * @param string $value 295 * 296 * @return string 297 */ 298 public function valueNumeric(string $value): string 299 { 300 $canonical = $this->canonical($value); 301 302 if (is_numeric($canonical)) { 303 return I18N::number((int) $canonical); 304 } 305 306 return e($value); 307 } 308} 309