xref: /webtrees/app/Elements/AbstractElement.php (revision e873f434551745f888937263ff89e80db3b0f785)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2023 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 nl2br;
33use function str_contains;
34use function str_starts_with;
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    // HTML attributes for an <input>
45    protected const int|false    MAXIMUM_LENGTH = false;
46    protected const string|false PATTERN        = false;
47
48    private const array WHITESPACE_LINE = [
49        "\t"       => ' ',
50        "\n"       => ' ',
51        "\r"       => ' ',
52        "\v"       => ' ', // Vertical tab
53        "\u{85}"   => ' ', // NEL - newline
54        "\u{2028}" => ' ', // LS - line separator
55        "\u{2029}" => ' ', // PS - paragraph separator
56    ];
57
58    private const array WHITESPACE_TEXT = [
59        "\t"       => ' ',
60        "\r\n"     => "\n",
61        "\r"       => "\n",
62        "\v"       => "\n",
63        "\u{85}"   => "\n",
64        "\u{2028}" => "\n",
65        "\u{2029}" => "\n\n",
66    ];
67
68    // Which child elements can appear under this element.
69    protected const array SUBTAGS = [];
70
71    // A label to describe this element
72    private string $label;
73
74    /** @var array<string,string> Subtags of this element */
75    private array $subtags;
76
77    /**
78     * @param string             $label
79     * @param array<string>|null $subtags
80     */
81    public function __construct(string $label, array|null $subtags = null)
82    {
83        $this->label   = $label;
84        $this->subtags = $subtags ?? static::SUBTAGS;
85    }
86
87    /**
88     * Convert a value to a canonical form.
89     *
90     * @param string $value
91     *
92     * @return string
93     */
94    public function canonical(string $value): string
95    {
96        $value = strtr($value, self::WHITESPACE_LINE);
97
98        while (str_contains($value, '  ')) {
99            $value = strtr($value, ['  ' => ' ']);
100        }
101
102        return trim($value);
103    }
104
105    /**
106     * Convert a multi-line value to a canonical form.
107     *
108     * @param string $value
109     *
110     * @return string
111     */
112    protected function canonicalText(string $value): string
113    {
114        $value = strtr($value, self::WHITESPACE_TEXT);
115
116        return trim($value, "\n");
117    }
118
119    /**
120     * Should we collapse the children of this element when editing?
121     *
122     * @return bool
123     */
124    public function collapseChildren(): bool
125    {
126        return false;
127    }
128
129    /**
130     * Create a default value for this element.
131     *
132     * @param Tree $tree
133     *
134     * @return string
135     */
136    public function default(Tree $tree): string
137    {
138        return '';
139    }
140
141    /**
142     * An edit control for this data.
143     *
144     * @param string $id
145     * @param string $name
146     * @param string $value
147     * @param Tree   $tree
148     *
149     * @return string
150     */
151    public function edit(string $id, string $name, string $value, Tree $tree): string
152    {
153        $values = $this->values();
154
155        if ($values !== []) {
156            $value = $this->canonical($value);
157
158            // Ensure the current data is in the list.
159            if (!array_key_exists($value, $values)) {
160                $values = [$value => $value] + $values;
161            }
162
163            // We may use markup to display values, but not when editing them.
164            $values = array_map(static fn (string $x): string => strip_tags($x), $values);
165
166            return view('components/select', [
167                'id'       => $id,
168                'name'     => $name,
169                'options'  => $values,
170                'selected' => $value,
171            ]);
172        }
173
174        $attributes = [
175            'class'     => 'form-control',
176            'dir'       => 'auto',
177            'type'      => 'text',
178            'id'        => $id,
179            'name'      => $name,
180            'value'     => $value,
181            'maxlength' => static::MAXIMUM_LENGTH,
182            'pattern'   => static::PATTERN,
183        ];
184
185        return '<input ' . Html::attributes($attributes) . ' />';
186    }
187
188    /**
189     * An edit control for this data.
190     *
191     * @param string $id
192     * @param string $name
193     * @param string $value
194     *
195     * @return string
196     */
197    public function editHidden(string $id, string $name, string $value): string
198    {
199        return '<input class="form-control" type="hidden" id="' . e($id) . '" name="' . e($name) . '" value="' . e($value) . '" />';
200    }
201
202    /**
203     * An edit control for this data.
204     *
205     * @param string $id
206     * @param string $name
207     * @param string $value
208     *
209     * @return string
210     */
211    public function editTextArea(string $id, string $name, string $value): string
212    {
213        return '<textarea class="form-control" id="' . e($id) . '" name="' . e($name) . '" rows="3" dir="auto">' . e($value) . '</textarea>';
214    }
215
216    /**
217     * Escape @ signs in a GEDCOM export.
218     *
219     * @param string $value
220     *
221     * @return string
222     */
223    public function escape(string $value): string
224    {
225        return strtr($value, ['@' => '@@']);
226    }
227
228    /**
229     * Create a label for this element.
230     *
231     * @return string
232     */
233    public function label(): string
234    {
235        return $this->label;
236    }
237
238    /**
239     * Create a label/value pair for this element.
240     *
241     * @param string $value
242     * @param Tree   $tree
243     *
244     * @return string
245     */
246    public function labelValue(string $value, Tree $tree): string
247    {
248        $label = '<span class="label">' . $this->label() . '</span>';
249        $value = '<span class="value align-top">' . $this->value($value, $tree) . '</span>';
250        $html  = I18N::translate(/* I18N: e.g. "Occupation: farmer" */ '%1$s: %2$s', $label, $value);
251
252        return '<div>' . $html . '</div>';
253    }
254
255    /**
256     * Set, remove or replace a subtag.
257     *
258     * @param string $subtag
259     * @param string $repeat
260     * @param string $before
261     *
262     * @return void
263     */
264    public function subtag(string $subtag, string $repeat, string $before = ''): void
265    {
266        if ($before === '' || ($this->subtags[$before] ?? null) === null) {
267            $this->subtags[$subtag] = $repeat;
268        } else {
269            $tmp = [];
270
271            foreach ($this->subtags as $key => $value) {
272                if ($key === $before) {
273                    $tmp[$subtag] = $repeat;
274                }
275                $tmp[$key] = $value;
276            }
277
278            $this->subtags = $tmp;
279        }
280    }
281
282    /**
283     * @return array<string,string>
284     */
285    public function subtags(): array
286    {
287        return $this->subtags;
288    }
289
290    /**
291     * Display the value of this type of element.
292     *
293     * @param string $value
294     * @param Tree   $tree
295     *
296     * @return string
297     */
298    public function value(string $value, Tree $tree): string
299    {
300        $values = $this->values();
301
302        if ($values === []) {
303            if (str_contains($value, "\n")) {
304                return '<span class="ut d-inline-block">' . nl2br(e($value, false)) . '</span>';
305            }
306
307            return '<span class="ut">' . e($value) . '</span>';
308        }
309
310        $canonical = $this->canonical($value);
311
312        return $values[$canonical] ?? '<bdi>' . e($value) . '</bdi>';
313    }
314
315    /**
316     * A list of controlled values for this element
317     *
318     * @return array<int|string,string>
319     */
320    public function values(): array
321    {
322        return [];
323    }
324
325    /**
326     * Display the value of this type of element - convert URLs to links.
327     *
328     * @param string $value
329     *
330     * @return string
331     */
332    protected function valueAutoLink(string $value): string
333    {
334        $canonical = $this->canonical($value);
335
336        if (str_contains($canonical, 'http://') || str_contains($canonical, 'https://')) {
337            $html = Registry::markdownFactory()->autolink($canonical);
338            $html = strip_tags($html, ['a', 'br']);
339        } else {
340            $html = nl2br(e($canonical), false);
341        }
342
343        if (str_contains($html, '<br>')) {
344            return '<span class="ut d-inline-block">' . $html . '</span>';
345        }
346
347        return '<span class="ut">' . $html . '</span>';
348    }
349
350    /**
351     * Display the value of this type of element - multi-line text with/without markdown.
352     *
353     * @param string $value
354     * @param Tree   $tree
355     *
356     * @return string
357     */
358    protected function valueFormatted(string $value, Tree $tree): string
359    {
360        $canonical = $this->canonical($value);
361
362        $format = $tree->getPreference('FORMAT_TEXT');
363
364        switch ($format) {
365            case 'markdown':
366                return Registry::markdownFactory()->markdown($canonical, $tree);
367
368            default:
369                return Registry::markdownFactory()->autolink($canonical, $tree);
370        }
371    }
372
373    /**
374     * Display the value of this type of element - convert to URL.
375     *
376     * @param string $value
377     *
378     * @return string
379     */
380    protected function valueLink(string $value): string
381    {
382        $canonical = $this->canonical($value);
383
384        if (str_starts_with($canonical, 'https://') || str_starts_with($canonical, 'http://')) {
385            return '<a dir="auto" href="' . e($canonical) . '">' . e($value) . '</a>';
386        }
387
388        return e($value);
389    }
390
391    /**
392     * Display the value of this type of element.
393     *
394     * @param string $value
395     *
396     * @return string
397     */
398    public function valueNumeric(string $value): string
399    {
400        $canonical = $this->canonical($value);
401
402        if (is_numeric($canonical)) {
403            return I18N::number((int) $canonical);
404        }
405
406        return e($value);
407    }
408}
409