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