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