xref: /webtrees/app/Elements/AbstractElement.php (revision a020b8bd22327c55a48db9cc18431b6152b701c4)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2022 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 ($before === '' || ($this->subtags[$before] ?? null) === null) {
254            $this->subtags[$subtag] = $repeat;
255        } else {
256            $tmp = [];
257
258            foreach ($this->subtags as $key => $value) {
259                if ($key === $before) {
260                    $tmp[$subtag] = $repeat;
261                }
262                $tmp[$key] = $value;
263            }
264
265            $this->subtags = $tmp;
266        }
267    }
268
269    /**
270     * @return array<string,string>
271     */
272    public function subtags(): array
273    {
274        return $this->subtags;
275    }
276
277    /**
278     * Display the value of this type of element.
279     *
280     * @param string $value
281     * @param Tree   $tree
282     *
283     * @return string
284     */
285    public function value(string $value, Tree $tree): string
286    {
287        $values = $this->values();
288
289        if ($values === []) {
290            if (str_contains($value, "\n")) {
291                return '<bdi class="d-inline-block">' . nl2br(e($value, false)) . '</bdi>';
292            }
293
294            return '<bdi>' . e($value) . '</bdi>';
295        }
296
297        $canonical = $this->canonical($value);
298
299        return $values[$canonical] ?? '<bdi>' . e($value) . '</bdi>';
300    }
301
302    /**
303     * A list of controlled values for this element
304     *
305     * @return array<int|string,string>
306     */
307    public function values(): array
308    {
309        return [];
310    }
311
312    /**
313     * Display the value of this type of element - convert URLs to links.
314     *
315     * @param string $value
316     *
317     * @return string
318     */
319    protected function valueAutoLink(string $value): string
320    {
321        $canonical = $this->canonical($value);
322
323        if (str_contains($canonical, 'http://') || str_contains($canonical, 'https://')) {
324            $html = Registry::markdownFactory()->autolink($canonical);
325
326            return strip_tags($html, ['a']);
327        }
328
329        return e($canonical);
330    }
331
332    /**
333     * Display the value of this type of element - multi-line text with/without markdown.
334     *
335     * @param string $value
336     * @param Tree   $tree
337     *
338     * @return string
339     */
340    protected function valueFormatted(string $value, Tree $tree): string
341    {
342        $canonical = $this->canonical($value);
343
344        $format = $tree->getPreference('FORMAT_TEXT');
345
346        switch ($format) {
347            case 'markdown':
348                $html = Registry::markdownFactory()->markdown($canonical, $tree);
349
350                return '<div class="markdown" dir="auto">' . $html . '</div>';
351
352            default:
353                $html = Registry::markdownFactory()->autolink($canonical, $tree);
354
355                if (str_contains($html, "\n")) {
356                    return '<div class="markdown" dir="auto">' . $html . '</div>';
357                }
358
359                return '<span class="markdown" dir="auto">' . $html . '</span>';
360        }
361    }
362
363    /**
364     * Display the value of this type of element - convert to URL.
365     *
366     * @param string $value
367     *
368     * @return string
369     */
370    protected function valueLink(string $value): string
371    {
372        $canonical = $this->canonical($value);
373
374        if (str_starts_with($canonical, 'https://') || str_starts_with($canonical, 'http://')) {
375            return '<a dir="auto" href="' . e($canonical) . '">' . e($value) . '</a>';
376        }
377
378        return e($value);
379    }
380
381    /**
382     * Display the value of this type of element.
383     *
384     * @param string $value
385     *
386     * @return string
387     */
388    public function valueNumeric(string $value): string
389    {
390        $canonical = $this->canonical($value);
391
392        if (is_numeric($canonical)) {
393            return I18N::number((int) $canonical);
394        }
395
396        return e($value);
397    }
398}
399