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