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