xref: /webtrees/app/Elements/AbstractElement.php (revision 9dcd14745b56f6fe4313ffc54cb41b25d2dcc9fd)
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 nl2br;
33use function preg_replace;
34use function str_contains;
35use function str_starts_with;
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    // 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     * Convert a multi-line value to a canonical form.
90     *
91     * @param string $value
92     *
93     * @return string
94     */
95    protected function canonicalText(string $value): string
96    {
97        // Browsers use MS-DOS line endings in multi-line data.
98        $value = strtr($value, ["\t" => ' ', "\r\n" => "\n", "\r" => "\n"]);
99
100        // Remove blank lines at start/end
101        $value = preg_replace('/^( *\n)+/', '', $value);
102
103        return preg_replace('/(\n *)+$/', '', $value);
104    }
105
106    /**
107     * Should we collapse the children of this element when editing?
108     *
109     * @return bool
110     */
111    public function collapseChildren(): bool
112    {
113        return false;
114    }
115
116    /**
117     * Create a default value for this element.
118     *
119     * @param Tree $tree
120     *
121     * @return string
122     */
123    public function default(Tree $tree): string
124    {
125        return '';
126    }
127
128    /**
129     * An edit control for this data.
130     *
131     * @param string $id
132     * @param string $name
133     * @param string $value
134     * @param Tree   $tree
135     *
136     * @return string
137     */
138    public function edit(string $id, string $name, string $value, Tree $tree): string
139    {
140        $values = $this->values();
141
142        if ($values !== []) {
143            $value = $this->canonical($value);
144
145            // Ensure the current data is in the list.
146            if (!array_key_exists($value, $values)) {
147                $values = [$value => $value] + $values;
148            }
149
150            // We may use markup to display values, but not when editing them.
151            $values = array_map(static fn (string $x): string => strip_tags($x), $values);
152
153            return view('components/select', [
154                'id'       => $id,
155                'name'     => $name,
156                'options'  => $values,
157                'selected' => $value,
158            ]);
159        }
160
161        $attributes = [
162            'class'     => 'form-control',
163            'dir'       => 'auto',
164            'type'      => 'text',
165            'id'        => $id,
166            'name'      => $name,
167            'value'     => $value,
168            'maxlength' => static::MAXIMUM_LENGTH,
169            'pattern'   => static::PATTERN,
170        ];
171
172        return '<input ' . Html::attributes($attributes) . ' />';
173    }
174
175    /**
176     * An edit control for this data.
177     *
178     * @param string $id
179     * @param string $name
180     * @param string $value
181     *
182     * @return string
183     */
184    public function editHidden(string $id, string $name, string $value): string
185    {
186        return '<input class="form-control" type="hidden" id="' . e($id) . '" name="' . e($name) . '" value="' . e($value) . '" />';
187    }
188
189    /**
190     * An edit control for this data.
191     *
192     * @param string $id
193     * @param string $name
194     * @param string $value
195     *
196     * @return string
197     */
198    public function editTextArea(string $id, string $name, string $value): string
199    {
200        return '<textarea class="form-control" id="' . e($id) . '" name="' . e($name) . '" rows="3" dir="auto">' . e($value) . '</textarea>';
201    }
202
203    /**
204     * Escape @ signs in a GEDCOM export.
205     *
206     * @param string $value
207     *
208     * @return string
209     */
210    public function escape(string $value): string
211    {
212        return strtr($value, ['@' => '@@']);
213    }
214
215    /**
216     * Create a label for this element.
217     *
218     * @return string
219     */
220    public function label(): string
221    {
222        return $this->label;
223    }
224
225    /**
226     * Create a label/value pair for this element.
227     *
228     * @param string $value
229     * @param Tree   $tree
230     *
231     * @return string
232     */
233    public function labelValue(string $value, Tree $tree): string
234    {
235        $label = '<span class="label">' . $this->label() . '</span>';
236        $value = '<span class="value align-top">' . $this->value($value, $tree) . '</span>';
237        $html  = I18N::translate(/* I18N: e.g. "Occupation: farmer" */ '%1$s: %2$s', $label, $value);
238
239        return '<div>' . $html . '</div>';
240    }
241
242    /**
243     * Set, remove or replace a subtag.
244     *
245     * @param string $subtag
246     * @param string $repeat
247     * @param string $before
248     *
249     * @return void
250     */
251    public function subtag(string $subtag, string $repeat, string $before = ''): void
252    {
253        if ($repeat === '') {
254            unset($this->subtags[$subtag]);
255        } elseif ($before === '' || ($this->subtags[$before] ?? null) === null) {
256            $this->subtags[$subtag] = $repeat;
257        } else {
258            $tmp = [];
259
260            foreach ($this->subtags as $key => $value) {
261                if ($key === $before) {
262                    $tmp[$subtag] = $repeat;
263                }
264                $tmp[$key] = $value;
265            }
266
267            $this->subtags = $tmp;
268        }
269    }
270
271    /**
272     * @return array<string,string>
273     */
274    public function subtags(): array
275    {
276        return $this->subtags;
277    }
278
279    /**
280     * Display the value of this type of element.
281     *
282     * @param string $value
283     * @param Tree   $tree
284     *
285     * @return string
286     */
287    public function value(string $value, Tree $tree): string
288    {
289        $values = $this->values();
290
291        if ($values === []) {
292            if (str_contains($value, "\n")) {
293                return '<bdi class="d-inline-block">' . nl2br(e($value, false)) . '</bdi>';
294            }
295
296            return '<bdi>' . e($value) . '</bdi>';
297        }
298
299        $canonical = $this->canonical($value);
300
301        return $values[$canonical] ?? '<bdi>' . e($value) . '</bdi>';
302    }
303
304    /**
305     * A list of controlled values for this element
306     *
307     * @return array<int|string,string>
308     */
309    public function values(): array
310    {
311        return [];
312    }
313
314    /**
315     * Display the value of this type of element - convert URLs to links.
316     *
317     * @param string $value
318     *
319     * @return string
320     */
321    protected function valueAutoLink(string $value): string
322    {
323        $canonical = $this->canonical($value);
324
325        if (str_contains($canonical, 'http://') || str_contains($canonical, 'https://')) {
326            $html = Registry::markdownFactory()->autolink()->convertToHtml($canonical);
327
328            return strip_tags($html, ['a']);
329        }
330
331        return e($canonical);
332    }
333
334    /**
335     * Display the value of this type of element - multi-line text with/without markdown.
336     *
337     * @param string $value
338     * @param Tree   $tree
339     *
340     * @return string
341     */
342    protected function valueFormatted(string $value, Tree $tree): string
343    {
344        $canonical = $this->canonical($value);
345
346        $format = $tree->getPreference('FORMAT_TEXT');
347
348        switch ($format) {
349            case 'markdown':
350                $html = Registry::markdownFactory()->markdown($tree)->convertToHtml($canonical);
351
352                return '<div class="markdown" dir="auto">' . $html . '</div>';
353
354            default:
355                $html = Registry::markdownFactory()->autolink($tree)->convertToHtml($canonical);
356
357                if (str_contains($html, "\n")) {
358                    return '<div class="markdown" dir="auto">' . $html . '</div>';
359                }
360
361                return '<span class="markdown" dir="auto">' . $html . '</span>';
362        }
363    }
364
365    /**
366     * Display the value of this type of element - convert to URL.
367     *
368     * @param string $value
369     *
370     * @return string
371     */
372    protected function valueLink(string $value): string
373    {
374        $canonical = $this->canonical($value);
375
376        if (str_starts_with($canonical, 'https://') || str_starts_with($canonical, 'http://')) {
377            return '<a dir="auto" href="' . e($canonical) . '">' . e($value) . '</a>';
378        }
379
380        return e($value);
381    }
382
383    /**
384     * Display the value of this type of element.
385     *
386     * @param string $value
387     *
388     * @return string
389     */
390    public function valueNumeric(string $value): string
391    {
392        $canonical = $this->canonical($value);
393
394        if (is_numeric($canonical)) {
395            return I18N::number((int) $canonical);
396        }
397
398        return e($value);
399    }
400}
401