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