xref: /webtrees/app/Elements/AbstractElement.php (revision 2c6f1bd538f46b93645991518398bb087011cb42)
1c2ed51d1SGreg Roach<?php
2c2ed51d1SGreg Roach
3c2ed51d1SGreg Roach/**
4c2ed51d1SGreg Roach * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
6c2ed51d1SGreg Roach * This program is free software: you can redistribute it and/or modify
7c2ed51d1SGreg Roach * it under the terms of the GNU General Public License as published by
8c2ed51d1SGreg Roach * the Free Software Foundation, either version 3 of the License, or
9c2ed51d1SGreg Roach * (at your option) any later version.
10c2ed51d1SGreg Roach * This program is distributed in the hope that it will be useful,
11c2ed51d1SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
12c2ed51d1SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13c2ed51d1SGreg Roach * GNU General Public License for more details.
14c2ed51d1SGreg Roach * You should have received a copy of the GNU General Public License
15c2ed51d1SGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
16c2ed51d1SGreg Roach */
17c2ed51d1SGreg Roach
18c2ed51d1SGreg Roachdeclare(strict_types=1);
19c2ed51d1SGreg Roach
20c2ed51d1SGreg Roachnamespace Fisharebest\Webtrees\Elements;
21c2ed51d1SGreg Roach
22c2ed51d1SGreg Roachuse Fisharebest\Webtrees\Contracts\ElementInterface;
23c2ed51d1SGreg Roachuse Fisharebest\Webtrees\Html;
24c2ed51d1SGreg Roachuse Fisharebest\Webtrees\I18N;
258392edd9SGreg Roachuse Fisharebest\Webtrees\Registry;
26c2ed51d1SGreg Roachuse Fisharebest\Webtrees\Tree;
27c2ed51d1SGreg Roach
28c2ed51d1SGreg Roachuse function array_key_exists;
29c2ed51d1SGreg Roachuse function array_map;
30c2ed51d1SGreg Roachuse function e;
31c2ed51d1SGreg Roachuse function is_numeric;
32486b00e0SGreg Roachuse function nl2br;
334da96842SGreg Roachuse function str_contains;
34cef4fcc0SGreg Roachuse function str_starts_with;
354da96842SGreg Roachuse function strip_tags;
36c2ed51d1SGreg Roachuse function trim;
37c2ed51d1SGreg Roachuse function view;
38c2ed51d1SGreg Roach
39c2ed51d1SGreg Roach/**
40c2ed51d1SGreg Roach * A GEDCOM element is a tag/primitive in a GEDCOM file.
41c2ed51d1SGreg Roach */
42c2ed51d1SGreg Roachabstract class AbstractElement implements ElementInterface
43c2ed51d1SGreg Roach{
44c2ed51d1SGreg Roach    // HTML attributes for an <input>
45ae0043b7SGreg Roach    protected const MAXIMUM_LENGTH = false;
46ae0043b7SGreg Roach    protected const PATTERN        = false;
47c2ed51d1SGreg Roach
48b3d7ece6SGreg Roach    private const WHITESPACE_LINE = [
49b3d7ece6SGreg Roach        "\t"       => ' ',
50b3d7ece6SGreg Roach        "\n"       => ' ',
51b3d7ece6SGreg Roach        "\r"       => ' ',
52b3d7ece6SGreg Roach        "\v"       => ' ', // Vertical tab
53b3d7ece6SGreg Roach        "\u{85}"   => ' ', // NEL - newline
54b3d7ece6SGreg Roach        "\u{2028}" => ' ', // LS - line separator
55b3d7ece6SGreg Roach        "\u{2029}" => ' ', // PS - paragraph separator
56b3d7ece6SGreg Roach    ];
57b3d7ece6SGreg Roach
58b3d7ece6SGreg Roach    private const WHITESPACE_TEXT = [
59b3d7ece6SGreg Roach        "\t"       => ' ',
60b3d7ece6SGreg Roach        "\r\n"     => "\n",
61b3d7ece6SGreg Roach        "\r"       => "\n",
62b3d7ece6SGreg Roach        "\v"       => "\n",
63b3d7ece6SGreg Roach        "\u{85}"   => "\n",
64b3d7ece6SGreg Roach        "\u{2028}" => "\n",
65b3d7ece6SGreg Roach        "\u{2029}" => "\n\n",
66b3d7ece6SGreg Roach    ];
67b3d7ece6SGreg Roach
68c2ed51d1SGreg Roach    // Which child elements can appear under this element.
69c2ed51d1SGreg Roach    protected const SUBTAGS = [];
70c2ed51d1SGreg Roach
714da96842SGreg Roach    // A label to describe this element
724da96842SGreg Roach    private string $label;
73c2ed51d1SGreg Roach
744da96842SGreg Roach    /** @var array<string,string> Subtags of this element */
754da96842SGreg Roach    private array $subtags;
76c2ed51d1SGreg Roach
77c2ed51d1SGreg Roach    /**
78c2ed51d1SGreg Roach     * @param string             $label
79c2ed51d1SGreg Roach     * @param array<string>|null $subtags
80c2ed51d1SGreg Roach     */
81*2c6f1bd5SGreg Roach    public function __construct(string $label, array|null $subtags = null)
82c2ed51d1SGreg Roach    {
83c2ed51d1SGreg Roach        $this->label   = $label;
84c2ed51d1SGreg Roach        $this->subtags = $subtags ?? static::SUBTAGS;
85c2ed51d1SGreg Roach    }
86c2ed51d1SGreg Roach
87c2ed51d1SGreg Roach    /**
88c2ed51d1SGreg Roach     * Convert a value to a canonical form.
89c2ed51d1SGreg Roach     *
90c2ed51d1SGreg Roach     * @param string $value
91c2ed51d1SGreg Roach     *
92c2ed51d1SGreg Roach     * @return string
93c2ed51d1SGreg Roach     */
94c2ed51d1SGreg Roach    public function canonical(string $value): string
95c2ed51d1SGreg Roach    {
96b3d7ece6SGreg Roach        $value = strtr($value, self::WHITESPACE_LINE);
97c2ed51d1SGreg Roach
984da96842SGreg Roach        while (str_contains($value, '  ')) {
99c2ed51d1SGreg Roach            $value = strtr($value, ['  ' => ' ']);
100c2ed51d1SGreg Roach        }
101c2ed51d1SGreg Roach
102c2ed51d1SGreg Roach        return trim($value);
103c2ed51d1SGreg Roach    }
104c2ed51d1SGreg Roach
105c2ed51d1SGreg Roach    /**
1064e09581bSGreg Roach     * Convert a multi-line value to a canonical form.
1074e09581bSGreg Roach     *
1084e09581bSGreg Roach     * @param string $value
1094e09581bSGreg Roach     *
1104e09581bSGreg Roach     * @return string
1114e09581bSGreg Roach     */
1124e09581bSGreg Roach    protected function canonicalText(string $value): string
1134e09581bSGreg Roach    {
114b3d7ece6SGreg Roach        $value = strtr($value, self::WHITESPACE_TEXT);
1154e09581bSGreg Roach
116b3d7ece6SGreg Roach        return trim($value, "\n");
1174e09581bSGreg Roach    }
1184e09581bSGreg Roach
1194e09581bSGreg Roach    /**
1204e09581bSGreg Roach     * Should we collapse the children of this element when editing?
1214e09581bSGreg Roach     *
1224e09581bSGreg Roach     * @return bool
1234e09581bSGreg Roach     */
1244e09581bSGreg Roach    public function collapseChildren(): bool
1254e09581bSGreg Roach    {
1264e09581bSGreg Roach        return false;
1274e09581bSGreg Roach    }
1284e09581bSGreg Roach
1294e09581bSGreg Roach    /**
130c2ed51d1SGreg Roach     * Create a default value for this element.
131c2ed51d1SGreg Roach     *
132c2ed51d1SGreg Roach     * @param Tree $tree
133c2ed51d1SGreg Roach     *
134c2ed51d1SGreg Roach     * @return string
135c2ed51d1SGreg Roach     */
136c2ed51d1SGreg Roach    public function default(Tree $tree): string
137c2ed51d1SGreg Roach    {
138c2ed51d1SGreg Roach        return '';
139c2ed51d1SGreg Roach    }
140c2ed51d1SGreg Roach
141c2ed51d1SGreg Roach    /**
142c2ed51d1SGreg Roach     * An edit control for this data.
143c2ed51d1SGreg Roach     *
144c2ed51d1SGreg Roach     * @param string $id
145c2ed51d1SGreg Roach     * @param string $name
146c2ed51d1SGreg Roach     * @param string $value
147c2ed51d1SGreg Roach     * @param Tree   $tree
148c2ed51d1SGreg Roach     *
149c2ed51d1SGreg Roach     * @return string
150c2ed51d1SGreg Roach     */
151c2ed51d1SGreg Roach    public function edit(string $id, string $name, string $value, Tree $tree): string
152c2ed51d1SGreg Roach    {
153c2ed51d1SGreg Roach        $values = $this->values();
154c2ed51d1SGreg Roach
155c2ed51d1SGreg Roach        if ($values !== []) {
156c2ed51d1SGreg Roach            $value = $this->canonical($value);
157c2ed51d1SGreg Roach
158c2ed51d1SGreg Roach            // Ensure the current data is in the list.
159c2ed51d1SGreg Roach            if (!array_key_exists($value, $values)) {
160c2ed51d1SGreg Roach                $values = [$value => $value] + $values;
161c2ed51d1SGreg Roach            }
162c2ed51d1SGreg Roach
163c2ed51d1SGreg Roach            // We may use markup to display values, but not when editing them.
16405babb96SGreg Roach            $values = array_map(static fn (string $x): string => strip_tags($x), $values);
165c2ed51d1SGreg Roach
166c2ed51d1SGreg Roach            return view('components/select', [
167c2ed51d1SGreg Roach                'id'       => $id,
168c2ed51d1SGreg Roach                'name'     => $name,
169c2ed51d1SGreg Roach                'options'  => $values,
170c2ed51d1SGreg Roach                'selected' => $value,
171c2ed51d1SGreg Roach            ]);
172c2ed51d1SGreg Roach        }
173c2ed51d1SGreg Roach
174c2ed51d1SGreg Roach        $attributes = [
175c2ed51d1SGreg Roach            'class'     => 'form-control',
1769deadf1cSGreg Roach            'dir'       => 'auto',
177c2ed51d1SGreg Roach            'type'      => 'text',
178c2ed51d1SGreg Roach            'id'        => $id,
179c2ed51d1SGreg Roach            'name'      => $name,
180c2ed51d1SGreg Roach            'value'     => $value,
181ca561ce7SGreg Roach            'maxlength' => static::MAXIMUM_LENGTH,
182ae0043b7SGreg Roach            'pattern'   => static::PATTERN,
183c2ed51d1SGreg Roach        ];
184c2ed51d1SGreg Roach
1854a213054SGreg Roach        return '<input ' . Html::attributes($attributes) . ' />';
186c2ed51d1SGreg Roach    }
187c2ed51d1SGreg Roach
188c2ed51d1SGreg Roach    /**
189c2ed51d1SGreg Roach     * An edit control for this data.
190c2ed51d1SGreg Roach     *
191c2ed51d1SGreg Roach     * @param string $id
192c2ed51d1SGreg Roach     * @param string $name
193c2ed51d1SGreg Roach     * @param string $value
194c2ed51d1SGreg Roach     *
195c2ed51d1SGreg Roach     * @return string
196c2ed51d1SGreg Roach     */
197c2ed51d1SGreg Roach    public function editHidden(string $id, string $name, string $value): string
198c2ed51d1SGreg Roach    {
1994a213054SGreg Roach        return '<input class="form-control" type="hidden" id="' . e($id) . '" name="' . e($name) . '" value="' . e($value) . '" />';
200c2ed51d1SGreg Roach    }
201c2ed51d1SGreg Roach
202c2ed51d1SGreg Roach    /**
203c2ed51d1SGreg Roach     * An edit control for this data.
204c2ed51d1SGreg Roach     *
205c2ed51d1SGreg Roach     * @param string $id
206c2ed51d1SGreg Roach     * @param string $name
207c2ed51d1SGreg Roach     * @param string $value
208c2ed51d1SGreg Roach     *
209c2ed51d1SGreg Roach     * @return string
210c2ed51d1SGreg Roach     */
211c2ed51d1SGreg Roach    public function editTextArea(string $id, string $name, string $value): string
212c2ed51d1SGreg Roach    {
2134e09581bSGreg Roach        return '<textarea class="form-control" id="' . e($id) . '" name="' . e($name) . '" rows="3" dir="auto">' . e($value) . '</textarea>';
214c2ed51d1SGreg Roach    }
215c2ed51d1SGreg Roach
216c2ed51d1SGreg Roach    /**
217c2ed51d1SGreg Roach     * Escape @ signs in a GEDCOM export.
218c2ed51d1SGreg Roach     *
219c2ed51d1SGreg Roach     * @param string $value
220c2ed51d1SGreg Roach     *
221c2ed51d1SGreg Roach     * @return string
222c2ed51d1SGreg Roach     */
223c2ed51d1SGreg Roach    public function escape(string $value): string
224c2ed51d1SGreg Roach    {
225c2ed51d1SGreg Roach        return strtr($value, ['@' => '@@']);
226c2ed51d1SGreg Roach    }
227c2ed51d1SGreg Roach
228c2ed51d1SGreg Roach    /**
229c2ed51d1SGreg Roach     * Create a label for this element.
230c2ed51d1SGreg Roach     *
231c2ed51d1SGreg Roach     * @return string
232c2ed51d1SGreg Roach     */
233c2ed51d1SGreg Roach    public function label(): string
234c2ed51d1SGreg Roach    {
235c2ed51d1SGreg Roach        return $this->label;
236c2ed51d1SGreg Roach    }
237c2ed51d1SGreg Roach
238c2ed51d1SGreg Roach    /**
239c2ed51d1SGreg Roach     * Create a label/value pair for this element.
240c2ed51d1SGreg Roach     *
241c2ed51d1SGreg Roach     * @param string $value
242c2ed51d1SGreg Roach     * @param Tree   $tree
243c2ed51d1SGreg Roach     *
244c2ed51d1SGreg Roach     * @return string
245c2ed51d1SGreg Roach     */
246c2ed51d1SGreg Roach    public function labelValue(string $value, Tree $tree): string
247c2ed51d1SGreg Roach    {
248c2ed51d1SGreg Roach        $label = '<span class="label">' . $this->label() . '</span>';
2498da28f8eSGreg Roach        $value = '<span class="value align-top">' . $this->value($value, $tree) . '</span>';
250c2ed51d1SGreg Roach        $html  = I18N::translate(/* I18N: e.g. "Occupation: farmer" */ '%1$s: %2$s', $label, $value);
251c2ed51d1SGreg Roach
252c2ed51d1SGreg Roach        return '<div>' . $html . '</div>';
253c2ed51d1SGreg Roach    }
254c2ed51d1SGreg Roach
255c2ed51d1SGreg Roach    /**
2564dbb2a39SGreg Roach     * Set, remove or replace a subtag.
2574dbb2a39SGreg Roach     *
2584dbb2a39SGreg Roach     * @param string $subtag
2594dbb2a39SGreg Roach     * @param string $repeat
2604da96842SGreg Roach     * @param string $before
2614dbb2a39SGreg Roach     *
2624dbb2a39SGreg Roach     * @return void
2634dbb2a39SGreg Roach     */
2648c21658eSGreg Roach    public function subtag(string $subtag, string $repeat, string $before = ''): void
2654dbb2a39SGreg Roach    {
2666c747bc0SGreg Roach        if ($before === '' || ($this->subtags[$before] ?? null) === null) {
2674dbb2a39SGreg Roach            $this->subtags[$subtag] = $repeat;
2684dbb2a39SGreg Roach        } else {
2694dbb2a39SGreg Roach            $tmp = [];
2704dbb2a39SGreg Roach
2714dbb2a39SGreg Roach            foreach ($this->subtags as $key => $value) {
2724da96842SGreg Roach                if ($key === $before) {
273efd4768bSGreg Roach                    $tmp[$subtag] = $repeat;
2744dbb2a39SGreg Roach                }
2754da96842SGreg Roach                $tmp[$key] = $value;
2764dbb2a39SGreg Roach            }
2774dbb2a39SGreg Roach
2784dbb2a39SGreg Roach            $this->subtags = $tmp;
2794dbb2a39SGreg Roach        }
2804dbb2a39SGreg Roach    }
2814dbb2a39SGreg Roach
2824dbb2a39SGreg Roach    /**
283c2ed51d1SGreg Roach     * @return array<string,string>
284c2ed51d1SGreg Roach     */
2853d2c98d1SGreg Roach    public function subtags(): array
286c2ed51d1SGreg Roach    {
287c2ed51d1SGreg Roach        return $this->subtags;
288c2ed51d1SGreg Roach    }
289c2ed51d1SGreg Roach
290c2ed51d1SGreg Roach    /**
291c2ed51d1SGreg Roach     * Display the value of this type of element.
292c2ed51d1SGreg Roach     *
293c2ed51d1SGreg Roach     * @param string $value
294c2ed51d1SGreg Roach     * @param Tree   $tree
295c2ed51d1SGreg Roach     *
296c2ed51d1SGreg Roach     * @return string
297c2ed51d1SGreg Roach     */
298c2ed51d1SGreg Roach    public function value(string $value, Tree $tree): string
299c2ed51d1SGreg Roach    {
300c2ed51d1SGreg Roach        $values = $this->values();
301c2ed51d1SGreg Roach
302c2ed51d1SGreg Roach        if ($values === []) {
303c2ed51d1SGreg Roach            if (str_contains($value, "\n")) {
3045d2c6313SGreg Roach                return '<span class="ut d-inline-block">' . nl2br(e($value, false)) . '</span>';
305c2ed51d1SGreg Roach            }
306c2ed51d1SGreg Roach
3075d2c6313SGreg Roach            return '<span class="ut">' . e($value) . '</span>';
308c2ed51d1SGreg Roach        }
309c2ed51d1SGreg Roach
310c2ed51d1SGreg Roach        $canonical = $this->canonical($value);
311c2ed51d1SGreg Roach
312315eb316SGreg Roach        return $values[$canonical] ?? '<bdi>' . e($value) . '</bdi>';
313c2ed51d1SGreg Roach    }
314c2ed51d1SGreg Roach
315c2ed51d1SGreg Roach    /**
316c2ed51d1SGreg Roach     * A list of controlled values for this element
317c2ed51d1SGreg Roach     *
318c2ed51d1SGreg Roach     * @return array<int|string,string>
319c2ed51d1SGreg Roach     */
320c2ed51d1SGreg Roach    public function values(): array
321c2ed51d1SGreg Roach    {
322c2ed51d1SGreg Roach        return [];
323c2ed51d1SGreg Roach    }
324c2ed51d1SGreg Roach
325c2ed51d1SGreg Roach    /**
326cef4fcc0SGreg Roach     * Display the value of this type of element - convert URLs to links.
327c2ed51d1SGreg Roach     *
328c2ed51d1SGreg Roach     * @param string $value
329c2ed51d1SGreg Roach     *
330c2ed51d1SGreg Roach     * @return string
331c2ed51d1SGreg Roach     */
332c2ed51d1SGreg Roach    protected function valueAutoLink(string $value): string
333c2ed51d1SGreg Roach    {
334a3287c67SGreg Roach        $canonical = $this->canonical($value);
335c2ed51d1SGreg Roach
3368392edd9SGreg Roach        if (str_contains($canonical, 'http://') || str_contains($canonical, 'https://')) {
3376f595250SGreg Roach            $html = Registry::markdownFactory()->autolink($canonical);
3385aa15f0cSGreg Roach            $html = strip_tags($html, ['a', 'br']);
3395aa15f0cSGreg Roach        } else {
3405aa15f0cSGreg Roach            $html = nl2br(e($canonical), false);
341a3287c67SGreg Roach        }
342c2ed51d1SGreg Roach
3435aa15f0cSGreg Roach        if (str_contains($html, '<br>')) {
3445aa15f0cSGreg Roach            return '<span class="ut d-inline-block">' . $html . '</span>';
3455aa15f0cSGreg Roach        }
3465aa15f0cSGreg Roach
3475aa15f0cSGreg Roach        return '<span class="ut">' . $html . '</span>';
348c2ed51d1SGreg Roach    }
349c2ed51d1SGreg Roach
350c2ed51d1SGreg Roach    /**
351486b00e0SGreg Roach     * Display the value of this type of element - multi-line text with/without markdown.
352486b00e0SGreg Roach     *
353486b00e0SGreg Roach     * @param string $value
3547f50305dSGreg Roach     * @param Tree   $tree
355486b00e0SGreg Roach     *
356486b00e0SGreg Roach     * @return string
357486b00e0SGreg Roach     */
358486b00e0SGreg Roach    protected function valueFormatted(string $value, Tree $tree): string
359486b00e0SGreg Roach    {
360486b00e0SGreg Roach        $canonical = $this->canonical($value);
361486b00e0SGreg Roach
362486b00e0SGreg Roach        $format = $tree->getPreference('FORMAT_TEXT');
363486b00e0SGreg Roach
36467721f6fSGreg Roach        switch ($format) {
36567721f6fSGreg Roach            case 'markdown':
3662de327daSGreg Roach                return Registry::markdownFactory()->markdown($canonical, $tree);
367486b00e0SGreg Roach
36867721f6fSGreg Roach            default:
3692de327daSGreg Roach                return Registry::markdownFactory()->autolink($canonical, $tree);
3701adf58a6SGreg Roach        }
37167721f6fSGreg Roach    }
3721adf58a6SGreg Roach
373486b00e0SGreg Roach    /**
374cef4fcc0SGreg Roach     * Display the value of this type of element - convert to URL.
375cef4fcc0SGreg Roach     *
376cef4fcc0SGreg Roach     * @param string $value
377cef4fcc0SGreg Roach     *
378cef4fcc0SGreg Roach     * @return string
379cef4fcc0SGreg Roach     */
380cef4fcc0SGreg Roach    protected function valueLink(string $value): string
381cef4fcc0SGreg Roach    {
382cef4fcc0SGreg Roach        $canonical = $this->canonical($value);
383cef4fcc0SGreg Roach
384cef4fcc0SGreg Roach        if (str_starts_with($canonical, 'https://') || str_starts_with($canonical, 'http://')) {
385cef4fcc0SGreg Roach            return '<a dir="auto" href="' . e($canonical) . '">' . e($value) . '</a>';
386cef4fcc0SGreg Roach        }
387cef4fcc0SGreg Roach
388cef4fcc0SGreg Roach        return e($value);
389cef4fcc0SGreg Roach    }
3904e09581bSGreg Roach
3914e09581bSGreg Roach    /**
3924e09581bSGreg Roach     * Display the value of this type of element.
3934e09581bSGreg Roach     *
3944e09581bSGreg Roach     * @param string $value
3954e09581bSGreg Roach     *
3964e09581bSGreg Roach     * @return string
3974e09581bSGreg Roach     */
3984e09581bSGreg Roach    public function valueNumeric(string $value): string
3994e09581bSGreg Roach    {
4004e09581bSGreg Roach        $canonical = $this->canonical($value);
4014e09581bSGreg Roach
4024e09581bSGreg Roach        if (is_numeric($canonical)) {
4034e09581bSGreg Roach            return I18N::number((int) $canonical);
4044e09581bSGreg Roach        }
4054e09581bSGreg Roach
4064e09581bSGreg Roach        return e($value);
4074e09581bSGreg Roach    }
408c2ed51d1SGreg Roach}
409