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