Issues (2563)

app/Elements/AbstractElement.php (1 issue)

Labels
Severity
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 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
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Elements;
21
22
use Fisharebest\Webtrees\Contracts\ElementInterface;
23
use Fisharebest\Webtrees\Html;
24
use Fisharebest\Webtrees\I18N;
25
use Fisharebest\Webtrees\Registry;
26
use Fisharebest\Webtrees\Tree;
27
28
use function array_key_exists;
29
use function array_map;
30
use function e;
31
use function is_numeric;
32
use function nl2br;
33
use function str_contains;
34
use function str_starts_with;
35
use function strip_tags;
36
use function trim;
37
use function view;
38
39
/**
40
 * A GEDCOM element is a tag/primitive in a GEDCOM file.
41
 */
42
abstract class AbstractElement implements ElementInterface
43
{
44
    // HTML attributes for an <input>
45
    protected const int|false    MAXIMUM_LENGTH = false;
0 ignored issues
show
A parse error occurred: Syntax error, unexpected '|', expecting '=' on line 45 at column 23
Loading history...
46
    protected const string|false PATTERN        = false;
47
48
    private const array WHITESPACE_LINE = [
49
        "\t"       => ' ',
50
        "\n"       => ' ',
51
        "\r"       => ' ',
52
        "\v"       => ' ', // Vertical tab
53
        "\u{85}"   => ' ', // NEL - newline
54
        "\u{2028}" => ' ', // LS - line separator
55
        "\u{2029}" => ' ', // PS - paragraph separator
56
    ];
57
58
    private const array WHITESPACE_TEXT = [
59
        "\t"       => ' ',
60
        "\r\n"     => "\n",
61
        "\r"       => "\n",
62
        "\v"       => "\n",
63
        "\u{85}"   => "\n",
64
        "\u{2028}" => "\n",
65
        "\u{2029}" => "\n\n",
66
    ];
67
68
    // Which child elements can appear under this element.
69
    protected const array SUBTAGS = [];
70
71
    // A label to describe this element
72
    private string $label;
73
74
    /** @var array<string,string> Subtags of this element */
75
    private array $subtags;
76
77
    /**
78
     * @param string             $label
79
     * @param array<string>|null $subtags
80
     */
81
    public function __construct(string $label, array|null $subtags = null)
82
    {
83
        $this->label   = $label;
84
        $this->subtags = $subtags ?? static::SUBTAGS;
85
    }
86
87
    /**
88
     * Convert a value to a canonical form.
89
     *
90
     * @param string $value
91
     *
92
     * @return string
93
     */
94
    public function canonical(string $value): string
95
    {
96
        $value = strtr($value, self::WHITESPACE_LINE);
97
98
        while (str_contains($value, '  ')) {
99
            $value = strtr($value, ['  ' => ' ']);
100
        }
101
102
        return trim($value);
103
    }
104
105
    /**
106
     * Convert a multi-line value to a canonical form.
107
     *
108
     * @param string $value
109
     *
110
     * @return string
111
     */
112
    protected function canonicalText(string $value): string
113
    {
114
        $value = strtr($value, self::WHITESPACE_TEXT);
115
116
        return trim($value, "\n");
117
    }
118
119
    /**
120
     * Should we collapse the children of this element when editing?
121
     *
122
     * @return bool
123
     */
124
    public function collapseChildren(): bool
125
    {
126
        return false;
127
    }
128
129
    /**
130
     * Create a default value for this element.
131
     *
132
     * @param Tree $tree
133
     *
134
     * @return string
135
     */
136
    public function default(Tree $tree): string
137
    {
138
        return '';
139
    }
140
141
    /**
142
     * An edit control for this data.
143
     *
144
     * @param string $id
145
     * @param string $name
146
     * @param string $value
147
     * @param Tree   $tree
148
     *
149
     * @return string
150
     */
151
    public function edit(string $id, string $name, string $value, Tree $tree): string
152
    {
153
        $values = $this->values();
154
155
        if ($values !== []) {
156
            $value = $this->canonical($value);
157
158
            // Ensure the current data is in the list.
159
            if (!array_key_exists($value, $values)) {
160
                $values = [$value => $value] + $values;
161
            }
162
163
            // We may use markup to display values, but not when editing them.
164
            $values = array_map(static fn (string $x): string => strip_tags($x), $values);
165
166
            return view('components/select', [
167
                'id'       => $id,
168
                'name'     => $name,
169
                'options'  => $values,
170
                'selected' => $value,
171
            ]);
172
        }
173
174
        $attributes = [
175
            'class'     => 'form-control',
176
            'dir'       => 'auto',
177
            'type'      => 'text',
178
            'id'        => $id,
179
            'name'      => $name,
180
            'value'     => $value,
181
            'maxlength' => static::MAXIMUM_LENGTH,
182
            'pattern'   => static::PATTERN,
183
        ];
184
185
        return '<input ' . Html::attributes($attributes) . ' />';
186
    }
187
188
    /**
189
     * An edit control for this data.
190
     *
191
     * @param string $id
192
     * @param string $name
193
     * @param string $value
194
     *
195
     * @return string
196
     */
197
    public function editHidden(string $id, string $name, string $value): string
198
    {
199
        return '<input class="form-control" type="hidden" id="' . e($id) . '" name="' . e($name) . '" value="' . e($value) . '" />';
200
    }
201
202
    /**
203
     * An edit control for this data.
204
     *
205
     * @param string $id
206
     * @param string $name
207
     * @param string $value
208
     *
209
     * @return string
210
     */
211
    public function editTextArea(string $id, string $name, string $value): string
212
    {
213
        return '<textarea class="form-control" id="' . e($id) . '" name="' . e($name) . '" rows="3" dir="auto">' . e($value) . '</textarea>';
214
    }
215
216
    /**
217
     * Escape @ signs in a GEDCOM export.
218
     *
219
     * @param string $value
220
     *
221
     * @return string
222
     */
223
    public function escape(string $value): string
224
    {
225
        return strtr($value, ['@' => '@@']);
226
    }
227
228
    /**
229
     * Create a label for this element.
230
     *
231
     * @return string
232
     */
233
    public function label(): string
234
    {
235
        return $this->label;
236
    }
237
238
    /**
239
     * Create a label/value pair for this element.
240
     *
241
     * @param string $value
242
     * @param Tree   $tree
243
     *
244
     * @return string
245
     */
246
    public function labelValue(string $value, Tree $tree): string
247
    {
248
        $label = '<span class="label">' . $this->label() . '</span>';
249
        $value = '<span class="value align-top">' . $this->value($value, $tree) . '</span>';
250
        $html  = I18N::translate(/* I18N: e.g. "Occupation: farmer" */ '%1$s: %2$s', $label, $value);
251
252
        return '<div>' . $html . '</div>';
253
    }
254
255
    /**
256
     * Set, remove or replace a subtag.
257
     *
258
     * @param string $subtag
259
     * @param string $repeat
260
     * @param string $before
261
     *
262
     * @return void
263
     */
264
    public function subtag(string $subtag, string $repeat, string $before = ''): void
265
    {
266
        if ($before === '' || ($this->subtags[$before] ?? null) === null) {
267
            $this->subtags[$subtag] = $repeat;
268
        } else {
269
            $tmp = [];
270
271
            foreach ($this->subtags as $key => $value) {
272
                if ($key === $before) {
273
                    $tmp[$subtag] = $repeat;
274
                }
275
                $tmp[$key] = $value;
276
            }
277
278
            $this->subtags = $tmp;
279
        }
280
    }
281
282
    /**
283
     * @return array<string,string>
284
     */
285
    public function subtags(): array
286
    {
287
        return $this->subtags;
288
    }
289
290
    /**
291
     * Display the value of this type of element.
292
     *
293
     * @param string $value
294
     * @param Tree   $tree
295
     *
296
     * @return string
297
     */
298
    public function value(string $value, Tree $tree): string
299
    {
300
        $values = $this->values();
301
302
        if ($values === []) {
303
            if (str_contains($value, "\n")) {
304
                return '<span class="ut d-inline-block">' . nl2br(e($value, false)) . '</span>';
305
            }
306
307
            return '<span class="ut">' . e($value) . '</span>';
308
        }
309
310
        $canonical = $this->canonical($value);
311
312
        return $values[$canonical] ?? '<bdi>' . e($value) . '</bdi>';
313
    }
314
315
    /**
316
     * A list of controlled values for this element
317
     *
318
     * @return array<int|string,string>
319
     */
320
    public function values(): array
321
    {
322
        return [];
323
    }
324
325
    /**
326
     * Display the value of this type of element - convert URLs to links.
327
     *
328
     * @param string $value
329
     *
330
     * @return string
331
     */
332
    protected function valueAutoLink(string $value): string
333
    {
334
        $canonical = $this->canonical($value);
335
336
        if (str_contains($canonical, 'http://') || str_contains($canonical, 'https://')) {
337
            $html = Registry::markdownFactory()->autolink($canonical);
338
            $html = strip_tags($html, ['a', 'br']);
339
        } else {
340
            $html = nl2br(e($canonical), false);
341
        }
342
343
        if (str_contains($html, '<br>')) {
344
            return '<span class="ut d-inline-block">' . $html . '</span>';
345
        }
346
347
        return '<span class="ut">' . $html . '</span>';
348
    }
349
350
    /**
351
     * Display the value of this type of element - multi-line text with/without markdown.
352
     *
353
     * @param string $value
354
     * @param Tree   $tree
355
     *
356
     * @return string
357
     */
358
    protected function valueFormatted(string $value, Tree $tree): string
359
    {
360
        $canonical = $this->canonical($value);
361
362
        $format = $tree->getPreference('FORMAT_TEXT');
363
364
        switch ($format) {
365
            case 'markdown':
366
                return Registry::markdownFactory()->markdown($canonical, $tree);
367
368
            default:
369
                return Registry::markdownFactory()->autolink($canonical, $tree);
370
        }
371
    }
372
373
    /**
374
     * Display the value of this type of element - convert to URL.
375
     *
376
     * @param string $value
377
     *
378
     * @return string
379
     */
380
    protected function valueLink(string $value): string
381
    {
382
        $canonical = $this->canonical($value);
383
384
        if (str_starts_with($canonical, 'https://') || str_starts_with($canonical, 'http://')) {
385
            return '<a dir="auto" href="' . e($canonical) . '">' . e($value) . '</a>';
386
        }
387
388
        return e($value);
389
    }
390
391
    /**
392
     * Display the value of this type of element.
393
     *
394
     * @param string $value
395
     *
396
     * @return string
397
     */
398
    public function valueNumeric(string $value): string
399
    {
400
        $canonical = $this->canonical($value);
401
402
        if (is_numeric($canonical)) {
403
            return I18N::number((int) $canonical);
404
        }
405
406
        return e($value);
407
    }
408
}
409