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