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