xref: /webtrees/app/Elements/AbstractElement.php (revision 7f50305d94e8a7ebaeee4764a3016c1638d01197)
1c2ed51d1SGreg Roach<?php
2c2ed51d1SGreg Roach
3c2ed51d1SGreg Roach/**
4c2ed51d1SGreg Roach * webtrees: online genealogy
5c2ed51d1SGreg Roach * Copyright (C) 2021 webtrees development team
6c2ed51d1SGreg Roach * This program is free software: you can redistribute it and/or modify
7c2ed51d1SGreg Roach * it under the terms of the GNU General Public License as published by
8c2ed51d1SGreg Roach * the Free Software Foundation, either version 3 of the License, or
9c2ed51d1SGreg Roach * (at your option) any later version.
10c2ed51d1SGreg Roach * This program is distributed in the hope that it will be useful,
11c2ed51d1SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
12c2ed51d1SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13c2ed51d1SGreg Roach * GNU General Public License for more details.
14c2ed51d1SGreg Roach * You should have received a copy of the GNU General Public License
15c2ed51d1SGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
16c2ed51d1SGreg Roach */
17c2ed51d1SGreg Roach
18c2ed51d1SGreg Roachdeclare(strict_types=1);
19c2ed51d1SGreg Roach
20c2ed51d1SGreg Roachnamespace Fisharebest\Webtrees\Elements;
21c2ed51d1SGreg Roach
22c2ed51d1SGreg Roachuse Fisharebest\Webtrees\Contracts\ElementInterface;
23c2ed51d1SGreg Roachuse Fisharebest\Webtrees\Html;
24c2ed51d1SGreg Roachuse Fisharebest\Webtrees\I18N;
258392edd9SGreg Roachuse Fisharebest\Webtrees\Registry;
26c2ed51d1SGreg Roachuse Fisharebest\Webtrees\Tree;
27c2ed51d1SGreg Roach
28c2ed51d1SGreg Roachuse function array_key_exists;
29c2ed51d1SGreg Roachuse function array_map;
30c2ed51d1SGreg Roachuse function e;
31c2ed51d1SGreg Roachuse function is_numeric;
32486b00e0SGreg Roachuse function nl2br;
339d477377SGreg Roachuse function preg_replace;
344da96842SGreg Roachuse function str_contains;
35cef4fcc0SGreg Roachuse function str_starts_with;
364da96842SGreg Roachuse function strip_tags;
37c2ed51d1SGreg Roachuse function trim;
38c2ed51d1SGreg Roachuse function view;
39c2ed51d1SGreg Roach
40c2ed51d1SGreg Roach/**
41c2ed51d1SGreg Roach * A GEDCOM element is a tag/primitive in a GEDCOM file.
42c2ed51d1SGreg Roach */
43c2ed51d1SGreg Roachabstract class AbstractElement implements ElementInterface
44c2ed51d1SGreg Roach{
45c2ed51d1SGreg Roach    // HTML attributes for an <input>
46ae0043b7SGreg Roach    protected const MAXIMUM_LENGTH = false;
47ae0043b7SGreg Roach    protected const PATTERN        = false;
48c2ed51d1SGreg Roach
49c2ed51d1SGreg Roach    // Which child elements can appear under this element.
50c2ed51d1SGreg Roach    protected const SUBTAGS = [];
51c2ed51d1SGreg Roach
524da96842SGreg Roach    // A label to describe this element
534da96842SGreg Roach    private string $label;
54c2ed51d1SGreg Roach
554da96842SGreg Roach    /** @var array<string,string> Subtags of this element */
564da96842SGreg Roach    private array $subtags;
57c2ed51d1SGreg Roach
58c2ed51d1SGreg Roach    /**
59c2ed51d1SGreg Roach     * AbstractGedcomElement constructor.
60c2ed51d1SGreg Roach     *
61c2ed51d1SGreg Roach     * @param string             $label
62c2ed51d1SGreg Roach     * @param array<string>|null $subtags
63c2ed51d1SGreg Roach     */
64c2ed51d1SGreg Roach    public function __construct(string $label, array $subtags = null)
65c2ed51d1SGreg Roach    {
66c2ed51d1SGreg Roach        $this->label   = $label;
67c2ed51d1SGreg Roach        $this->subtags = $subtags ?? static::SUBTAGS;
68c2ed51d1SGreg Roach    }
69c2ed51d1SGreg Roach
70c2ed51d1SGreg Roach    /**
71c2ed51d1SGreg Roach     * Convert a value to a canonical form.
72c2ed51d1SGreg Roach     *
73c2ed51d1SGreg Roach     * @param string $value
74c2ed51d1SGreg Roach     *
75c2ed51d1SGreg Roach     * @return string
76c2ed51d1SGreg Roach     */
77c2ed51d1SGreg Roach    public function canonical(string $value): string
78c2ed51d1SGreg Roach    {
79c2ed51d1SGreg Roach        $value = strtr($value, ["\t" => ' ', "\r" => ' ', "\n" => ' ']);
80c2ed51d1SGreg Roach
814da96842SGreg Roach        while (str_contains($value, '  ')) {
82c2ed51d1SGreg Roach            $value = strtr($value, ['  ' => ' ']);
83c2ed51d1SGreg Roach        }
84c2ed51d1SGreg Roach
85c2ed51d1SGreg Roach        return trim($value);
86c2ed51d1SGreg Roach    }
87c2ed51d1SGreg Roach
88c2ed51d1SGreg Roach    /**
894e09581bSGreg Roach     * Convert a multi-line value to a canonical form.
904e09581bSGreg Roach     *
914e09581bSGreg Roach     * @param string $value
924e09581bSGreg Roach     *
934e09581bSGreg Roach     * @return string
944e09581bSGreg Roach     */
954e09581bSGreg Roach    protected function canonicalText(string $value): string
964e09581bSGreg Roach    {
974e09581bSGreg Roach        // Browsers use MS-DOS line endings in multi-line data.
984e09581bSGreg Roach        $value = strtr($value, ["\t" => ' ', "\r\n" => "\n", "\r" => "\n"]);
994e09581bSGreg Roach
1004e09581bSGreg Roach        // Remove blank lines at start/end
1014e09581bSGreg Roach        $value = preg_replace('/^( *\n)+/', '', $value);
1024e09581bSGreg Roach
1034e09581bSGreg Roach        return preg_replace('/(\n *)+$/', '', $value);
1044e09581bSGreg Roach    }
1054e09581bSGreg Roach
1064e09581bSGreg Roach    /**
1074e09581bSGreg Roach     * Should we collapse the children of this element when editing?
1084e09581bSGreg Roach     *
1094e09581bSGreg Roach     * @return bool
1104e09581bSGreg Roach     */
1114e09581bSGreg Roach    public function collapseChildren(): bool
1124e09581bSGreg Roach    {
1134e09581bSGreg Roach        return false;
1144e09581bSGreg Roach    }
1154e09581bSGreg Roach
1164e09581bSGreg Roach    /**
117c2ed51d1SGreg Roach     * Create a default value for this element.
118c2ed51d1SGreg Roach     *
119c2ed51d1SGreg Roach     * @param Tree $tree
120c2ed51d1SGreg Roach     *
121c2ed51d1SGreg Roach     * @return string
122c2ed51d1SGreg Roach     */
123c2ed51d1SGreg Roach    public function default(Tree $tree): string
124c2ed51d1SGreg Roach    {
125c2ed51d1SGreg Roach        return '';
126c2ed51d1SGreg Roach    }
127c2ed51d1SGreg Roach
128c2ed51d1SGreg Roach    /**
129c2ed51d1SGreg Roach     * An edit control for this data.
130c2ed51d1SGreg Roach     *
131c2ed51d1SGreg Roach     * @param string $id
132c2ed51d1SGreg Roach     * @param string $name
133c2ed51d1SGreg Roach     * @param string $value
134c2ed51d1SGreg Roach     * @param Tree   $tree
135c2ed51d1SGreg Roach     *
136c2ed51d1SGreg Roach     * @return string
137c2ed51d1SGreg Roach     */
138c2ed51d1SGreg Roach    public function edit(string $id, string $name, string $value, Tree $tree): string
139c2ed51d1SGreg Roach    {
140c2ed51d1SGreg Roach        $values = $this->values();
141c2ed51d1SGreg Roach
142c2ed51d1SGreg Roach        if ($values !== []) {
143c2ed51d1SGreg Roach            $value = $this->canonical($value);
144c2ed51d1SGreg Roach
145c2ed51d1SGreg Roach            // Ensure the current data is in the list.
146c2ed51d1SGreg Roach            if (!array_key_exists($value, $values)) {
147c2ed51d1SGreg Roach                $values = [$value => $value] + $values;
148c2ed51d1SGreg Roach            }
149c2ed51d1SGreg Roach
150c2ed51d1SGreg Roach            // We may use markup to display values, but not when editing them.
1514da96842SGreg Roach            $values = array_map(fn (string $x): string => strip_tags($x), $values);
152c2ed51d1SGreg Roach
153c2ed51d1SGreg Roach            return view('components/select', [
154c2ed51d1SGreg Roach                'id'       => $id,
155c2ed51d1SGreg Roach                'name'     => $name,
156c2ed51d1SGreg Roach                'options'  => $values,
157c2ed51d1SGreg Roach                'selected' => $value,
158c2ed51d1SGreg Roach            ]);
159c2ed51d1SGreg Roach        }
160c2ed51d1SGreg Roach
161c2ed51d1SGreg Roach        $attributes = [
162c2ed51d1SGreg Roach            'class'     => 'form-control',
1639deadf1cSGreg Roach            'dir'       => 'auto',
164c2ed51d1SGreg Roach            'type'      => 'text',
165c2ed51d1SGreg Roach            'id'        => $id,
166c2ed51d1SGreg Roach            'name'      => $name,
167c2ed51d1SGreg Roach            'value'     => $value,
168ca561ce7SGreg Roach            'maxlength' => static::MAXIMUM_LENGTH,
169ae0043b7SGreg Roach            'pattern'   => static::PATTERN,
170c2ed51d1SGreg Roach        ];
171c2ed51d1SGreg Roach
1724a213054SGreg Roach        return '<input ' . Html::attributes($attributes) . ' />';
173c2ed51d1SGreg Roach    }
174c2ed51d1SGreg Roach
175c2ed51d1SGreg Roach    /**
176c2ed51d1SGreg Roach     * An edit control for this data.
177c2ed51d1SGreg Roach     *
178c2ed51d1SGreg Roach     * @param string $id
179c2ed51d1SGreg Roach     * @param string $name
180c2ed51d1SGreg Roach     * @param string $value
181c2ed51d1SGreg Roach     *
182c2ed51d1SGreg Roach     * @return string
183c2ed51d1SGreg Roach     */
184c2ed51d1SGreg Roach    public function editHidden(string $id, string $name, string $value): string
185c2ed51d1SGreg Roach    {
1864a213054SGreg Roach        return '<input class="form-control" type="hidden" id="' . e($id) . '" name="' . e($name) . '" value="' . e($value) . '" />';
187c2ed51d1SGreg Roach    }
188c2ed51d1SGreg Roach
189c2ed51d1SGreg Roach    /**
190c2ed51d1SGreg Roach     * An edit control for this data.
191c2ed51d1SGreg Roach     *
192c2ed51d1SGreg Roach     * @param string $id
193c2ed51d1SGreg Roach     * @param string $name
194c2ed51d1SGreg Roach     * @param string $value
195c2ed51d1SGreg Roach     *
196c2ed51d1SGreg Roach     * @return string
197c2ed51d1SGreg Roach     */
198c2ed51d1SGreg Roach    public function editTextArea(string $id, string $name, string $value): string
199c2ed51d1SGreg Roach    {
2004e09581bSGreg Roach        return '<textarea class="form-control" id="' . e($id) . '" name="' . e($name) . '" rows="3" dir="auto">' . e($value) . '</textarea>';
201c2ed51d1SGreg Roach    }
202c2ed51d1SGreg Roach
203c2ed51d1SGreg Roach    /**
204c2ed51d1SGreg Roach     * Escape @ signs in a GEDCOM export.
205c2ed51d1SGreg Roach     *
206c2ed51d1SGreg Roach     * @param string $value
207c2ed51d1SGreg Roach     *
208c2ed51d1SGreg Roach     * @return string
209c2ed51d1SGreg Roach     */
210c2ed51d1SGreg Roach    public function escape(string $value): string
211c2ed51d1SGreg Roach    {
212c2ed51d1SGreg Roach        return strtr($value, ['@' => '@@']);
213c2ed51d1SGreg Roach    }
214c2ed51d1SGreg Roach
215c2ed51d1SGreg Roach    /**
216c2ed51d1SGreg Roach     * Create a label for this element.
217c2ed51d1SGreg Roach     *
218c2ed51d1SGreg Roach     * @return string
219c2ed51d1SGreg Roach     */
220c2ed51d1SGreg Roach    public function label(): string
221c2ed51d1SGreg Roach    {
222c2ed51d1SGreg Roach        return $this->label;
223c2ed51d1SGreg Roach    }
224c2ed51d1SGreg Roach
225c2ed51d1SGreg Roach    /**
226c2ed51d1SGreg Roach     * Create a label/value pair for this element.
227c2ed51d1SGreg Roach     *
228c2ed51d1SGreg Roach     * @param string $value
229c2ed51d1SGreg Roach     * @param Tree   $tree
230c2ed51d1SGreg Roach     *
231c2ed51d1SGreg Roach     * @return string
232c2ed51d1SGreg Roach     */
233c2ed51d1SGreg Roach    public function labelValue(string $value, Tree $tree): string
234c2ed51d1SGreg Roach    {
235c2ed51d1SGreg Roach        $label = '<span class="label">' . $this->label() . '</span>';
2368da28f8eSGreg Roach        $value = '<span class="value align-top">' . $this->value($value, $tree) . '</span>';
237c2ed51d1SGreg Roach        $html  = I18N::translate(/* I18N: e.g. "Occupation: farmer" */ '%1$s: %2$s', $label, $value);
238c2ed51d1SGreg Roach
239c2ed51d1SGreg Roach        return '<div>' . $html . '</div>';
240c2ed51d1SGreg Roach    }
241c2ed51d1SGreg Roach
242c2ed51d1SGreg Roach    /**
2434dbb2a39SGreg Roach     * Set, remove or replace a subtag.
2444dbb2a39SGreg Roach     *
2454dbb2a39SGreg Roach     * @param string $subtag
2464dbb2a39SGreg Roach     * @param string $repeat
2474da96842SGreg Roach     * @param string $before
2484dbb2a39SGreg Roach     *
2494dbb2a39SGreg Roach     * @return void
2504dbb2a39SGreg Roach     */
251efd4768bSGreg Roach    public function subtag(string $subtag, string $repeat = '0:1', string $before = ''): void
2524dbb2a39SGreg Roach    {
2534dbb2a39SGreg Roach        if ($repeat === '') {
2544dbb2a39SGreg Roach            unset($this->subtags[$subtag]);
2554da96842SGreg Roach        } elseif ($before === '' || ($this->subtags[$before] ?? null) === null) {
2564dbb2a39SGreg Roach            $this->subtags[$subtag] = $repeat;
2574dbb2a39SGreg Roach        } else {
2584dbb2a39SGreg Roach            $tmp = [];
2594dbb2a39SGreg Roach
2604dbb2a39SGreg Roach            foreach ($this->subtags as $key => $value) {
2614da96842SGreg Roach                if ($key === $before) {
262efd4768bSGreg Roach                    $tmp[$subtag] = $repeat;
2634dbb2a39SGreg Roach                }
2644da96842SGreg Roach                $tmp[$key] = $value;
2654dbb2a39SGreg Roach            }
2664dbb2a39SGreg Roach
2674dbb2a39SGreg Roach            $this->subtags = $tmp;
2684dbb2a39SGreg Roach        }
2694dbb2a39SGreg Roach    }
2704dbb2a39SGreg Roach
2714dbb2a39SGreg Roach    /**
272c2ed51d1SGreg Roach     * @return array<string,string>
273c2ed51d1SGreg Roach     */
2743d2c98d1SGreg Roach    public function subtags(): array
275c2ed51d1SGreg Roach    {
276c2ed51d1SGreg Roach        return $this->subtags;
277c2ed51d1SGreg Roach    }
278c2ed51d1SGreg Roach
279c2ed51d1SGreg Roach    /**
280c2ed51d1SGreg Roach     * Display the value of this type of element.
281c2ed51d1SGreg Roach     *
282c2ed51d1SGreg Roach     * @param string $value
283c2ed51d1SGreg Roach     * @param Tree   $tree
284c2ed51d1SGreg Roach     *
285c2ed51d1SGreg Roach     * @return string
286c2ed51d1SGreg Roach     */
287c2ed51d1SGreg Roach    public function value(string $value, Tree $tree): string
288c2ed51d1SGreg Roach    {
289c2ed51d1SGreg Roach        $values = $this->values();
290c2ed51d1SGreg Roach
291c2ed51d1SGreg Roach        if ($values === []) {
292c2ed51d1SGreg Roach            if (str_contains($value, "\n")) {
293486b00e0SGreg Roach                return '<bdi class="d-inline-block">' . nl2br(e($value)) . '</bdi>';
294c2ed51d1SGreg Roach            }
295c2ed51d1SGreg Roach
296315eb316SGreg Roach            return '<bdi>' . e($value) . '</bdi>';
297c2ed51d1SGreg Roach        }
298c2ed51d1SGreg Roach
299c2ed51d1SGreg Roach        $canonical = $this->canonical($value);
300c2ed51d1SGreg Roach
301315eb316SGreg Roach        return $values[$canonical] ?? '<bdi>' . e($value) . '</bdi>';
302c2ed51d1SGreg Roach    }
303c2ed51d1SGreg Roach
304c2ed51d1SGreg Roach    /**
305c2ed51d1SGreg Roach     * A list of controlled values for this element
306c2ed51d1SGreg Roach     *
307c2ed51d1SGreg Roach     * @return array<int|string,string>
308c2ed51d1SGreg Roach     */
309c2ed51d1SGreg Roach    public function values(): array
310c2ed51d1SGreg Roach    {
311c2ed51d1SGreg Roach        return [];
312c2ed51d1SGreg Roach    }
313c2ed51d1SGreg Roach
314c2ed51d1SGreg Roach    /**
315cef4fcc0SGreg Roach     * Display the value of this type of element - convert URLs to links.
316c2ed51d1SGreg Roach     *
317c2ed51d1SGreg Roach     * @param string $value
318c2ed51d1SGreg Roach     *
319c2ed51d1SGreg Roach     * @return string
320c2ed51d1SGreg Roach     */
321c2ed51d1SGreg Roach    protected function valueAutoLink(string $value): string
322c2ed51d1SGreg Roach    {
323a3287c67SGreg Roach        $canonical = $this->canonical($value);
324c2ed51d1SGreg Roach
3258392edd9SGreg Roach        if (str_contains($canonical, 'http://') || str_contains($canonical, 'https://')) {
3268392edd9SGreg Roach            $html = Registry::markdownFactory()->autolink()->convertToHtml($canonical);
3278392edd9SGreg Roach
3288392edd9SGreg Roach            return strip_tags($html, ['a']);
329a3287c67SGreg Roach        }
330c2ed51d1SGreg Roach
331a3287c67SGreg Roach        return e($canonical);
332c2ed51d1SGreg Roach    }
333c2ed51d1SGreg Roach
334c2ed51d1SGreg Roach    /**
335486b00e0SGreg Roach     * Display the value of this type of element - multi-line text with/without markdown.
336486b00e0SGreg Roach     *
337486b00e0SGreg Roach     * @param string $value
338*7f50305dSGreg Roach     * @param Tree   $tree
339486b00e0SGreg Roach     *
340486b00e0SGreg Roach     * @return string
341486b00e0SGreg Roach     */
342486b00e0SGreg Roach    protected function valueFormatted(string $value, Tree $tree): string
343486b00e0SGreg Roach    {
344486b00e0SGreg Roach        $canonical = $this->canonical($value);
345486b00e0SGreg Roach
346486b00e0SGreg Roach        $format = $tree->getPreference('FORMAT_TEXT');
347486b00e0SGreg Roach
348486b00e0SGreg Roach        if ($format === 'markdown') {
349486b00e0SGreg Roach            $html = Registry::markdownFactory()->markdown($tree)->convertToHtml($canonical);
350486b00e0SGreg Roach        } else {
351486b00e0SGreg Roach            $html = Registry::markdownFactory()->autolink($tree)->convertToHtml($canonical);
352486b00e0SGreg Roach            $html = nl2br($html);
353486b00e0SGreg Roach        }
354486b00e0SGreg Roach
355486b00e0SGreg Roach        return '<div class="markdown" dir="auto">' . $html . '</div>';
356486b00e0SGreg Roach    }
357486b00e0SGreg Roach
358486b00e0SGreg Roach    /**
359cef4fcc0SGreg Roach     * Display the value of this type of element - convert to URL.
360cef4fcc0SGreg Roach     *
361cef4fcc0SGreg Roach     * @param string $value
362cef4fcc0SGreg Roach     *
363cef4fcc0SGreg Roach     * @return string
364cef4fcc0SGreg Roach     */
365cef4fcc0SGreg Roach    protected function valueLink(string $value): string
366cef4fcc0SGreg Roach    {
367cef4fcc0SGreg Roach        $canonical = $this->canonical($value);
368cef4fcc0SGreg Roach
369cef4fcc0SGreg Roach        if (str_starts_with($canonical, 'https://') || str_starts_with($canonical, 'http://')) {
370cef4fcc0SGreg Roach            return '<a dir="auto" href="' . e($canonical) . '">' . e($value) . '</a>';
371cef4fcc0SGreg Roach        }
372cef4fcc0SGreg Roach
373cef4fcc0SGreg Roach        return e($value);
374cef4fcc0SGreg Roach    }
3754e09581bSGreg Roach
3764e09581bSGreg Roach    /**
3774e09581bSGreg Roach     * Display the value of this type of element.
3784e09581bSGreg Roach     *
3794e09581bSGreg Roach     * @param string $value
3804e09581bSGreg Roach     *
3814e09581bSGreg Roach     * @return string
3824e09581bSGreg Roach     */
3834e09581bSGreg Roach    public function valueNumeric(string $value): string
3844e09581bSGreg Roach    {
3854e09581bSGreg Roach        $canonical = $this->canonical($value);
3864e09581bSGreg Roach
3874e09581bSGreg Roach        if (is_numeric($canonical)) {
3884e09581bSGreg Roach            return I18N::number((int) $canonical);
3894e09581bSGreg Roach        }
3904e09581bSGreg Roach
3914e09581bSGreg Roach        return e($value);
3924e09581bSGreg Roach    }
393c2ed51d1SGreg Roach}
394