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