xref: /webtrees/app/Elements/AbstractElement.php (revision c2ed51d13a57743094c11c8fe84befd9d4f158cd)
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\Tree;
26use League\CommonMark\Block\Element\Document;
27use League\CommonMark\Block\Element\Paragraph;
28use League\CommonMark\Block\Renderer\DocumentRenderer;
29use League\CommonMark\Block\Renderer\ParagraphRenderer;
30use League\CommonMark\CommonMarkConverter;
31use League\CommonMark\Environment;
32use League\CommonMark\Inline\Element\Link;
33use League\CommonMark\Inline\Element\Text;
34use League\CommonMark\Inline\Parser\AutolinkParser;
35use League\CommonMark\Inline\Renderer\LinkRenderer;
36use League\CommonMark\Inline\Renderer\TextRenderer;
37
38use function array_key_exists;
39use function array_map;
40use function e;
41use function is_numeric;
42use function preg_replace;
43use function strpos;
44use function trim;
45use function view;
46
47/**
48 * A GEDCOM element is a tag/primitive in a GEDCOM file.
49 */
50abstract class AbstractElement implements ElementInterface
51{
52    private const REGEX_URL = '~((https?|ftp]):)(//([^\s/?#<>]*))?([^\s?#<>]*)(\?([^\s#<>]*))?(#[^\s?#<>]+)?~';
53
54    // HTML attributes for an <input>
55    protected const MAX_LENGTH = false;
56
57    // Which child elements can appear under this element.
58    protected const SUBTAGS = [];
59
60    /** @var string A label to describe this element */
61    private $label;
62
63    /** @var array<string,string> */
64    private $subtags;
65
66    /**
67     * AbstractGedcomElement constructor.
68     *
69     * @param string             $label
70     * @param array<string>|null $subtags
71     */
72    public function __construct(string $label, array $subtags = null)
73    {
74        $this->label   = $label;
75        $this->subtags = $subtags ?? static::SUBTAGS;
76    }
77
78    /**
79     * Convert a value to a canonical form.
80     *
81     * @param string $value
82     *
83     * @return string
84     */
85    public function canonical(string $value): string
86    {
87        $value = strtr($value, ["\t" => ' ', "\r" => ' ', "\n" => ' ']);
88
89        while (strpos($value, '  ') !== false) {
90            $value = strtr($value, ['  ' => ' ']);
91        }
92
93        return trim($value);
94    }
95
96    /**
97     * Create a default value for this element.
98     *
99     * @param Tree $tree
100     *
101     * @return string
102     */
103    public function default(Tree $tree): string
104    {
105        return '';
106    }
107
108    /**
109     * An edit control for this data.
110     *
111     * @param string $id
112     * @param string $name
113     * @param string $value
114     * @param Tree   $tree
115     *
116     * @return string
117     */
118    public function edit(string $id, string $name, string $value, Tree $tree): string
119    {
120        $values = $this->values();
121
122        if ($values !== []) {
123            $value = $this->canonical($value);
124
125            // Ensure the current data is in the list.
126            if (!array_key_exists($value, $values)) {
127                $values = [$value => $value] + $values;
128            }
129
130            // We may use markup to display values, but not when editing them.
131            $values = array_map('strip_tags', $values);
132
133            return view('components/select', [
134                'id'       => $id,
135                'name'     => $name,
136                'options'  => $values,
137                'selected' => $value,
138            ]);
139        }
140
141        $attributes = [
142            'class'    => 'form-control',
143            'type'     => 'text',
144            'id'       => $id,
145            'name'     => $name,
146            'value'    => $value,
147            'maxvalue' => static::MAX_LENGTH,
148        ];
149
150        return '<input ' . Html::attributes($attributes) . '">';
151    }
152
153    /**
154     * An edit control for this data.
155     *
156     * @param string $id
157     * @param string $name
158     * @param string $value
159     *
160     * @return string
161     */
162    public function editHidden(string $id, string $name, string $value): string
163    {
164        return '<input class="form-control" type="hidden" id="' . e($id) . '" name="' . e($name) . '" value="' . e($value) . '">';
165    }
166
167    /**
168     * An edit control for this data.
169     *
170     * @param string $id
171     * @param string $name
172     * @param string $value
173     *
174     * @return string
175     */
176    public function editTextArea(string $id, string $name, string $value): string
177    {
178        return '<textarea class="form-control" id="' . e($id) . '" name="' . e($name) . '" rows="5" dir="auto">' . e($value) . '</textarea>';
179    }
180
181    /**
182     * Escape @ signs in a GEDCOM export.
183     *
184     * @param string $value
185     *
186     * @return string
187     */
188    public function escape(string $value): string
189    {
190        return strtr($value, ['@' => '@@']);
191    }
192
193    /**
194     * Create a label for this element.
195     *
196     * @return string
197     */
198    public function label(): string
199    {
200        return $this->label;
201    }
202
203    /**
204     * Create a label/value pair for this element.
205     *
206     * @param string $value
207     * @param Tree   $tree
208     *
209     * @return string
210     */
211    public function labelValue(string $value, Tree $tree): string
212    {
213        $label = '<span class="label">' . $this->label() . '</span>';
214        $value = '<span class="value">' . $this->value($value, $tree) . '</span>';
215        $html  = I18N::translate(/* I18N: e.g. "Occupation: farmer" */ '%1$s: %2$s', $label, $value);
216
217        return '<div>' . $html . '</div>';
218    }
219
220    /**
221     * @param Tree $tree
222     *
223     * @return array<string,string>
224     */
225    public function subtags(Tree $tree): array
226    {
227        return $this->subtags;
228    }
229
230    /**
231     * Display the value of this type of element.
232     *
233     * @param string $value
234     * @param Tree   $tree
235     *
236     * @return string
237     */
238    public function value(string $value, Tree $tree): string
239    {
240        $values = $this->values();
241
242        if ($values === []) {
243            if (str_contains($value, "\n")) {
244                return '<span dir="auto" class="d-inline-block" style="white-space: pre-wrap;">' . e($value) . '</span>';
245            }
246
247            return '<span dir="auto">' . e($value) . '</span>';
248        }
249
250        $canonical = $this->canonical($value);
251
252        return $values[$canonical] ?? '<span dir="auto">' . e($value) . '</span>';
253    }
254
255    /**
256     * A list of controlled values for this element
257     *
258     * @return array<int|string,string>
259     */
260    public function values(): array
261    {
262        return [];
263    }
264
265    /**
266     * Display the value of this type of element - convert URLs to links
267     *
268     * @param string $value
269     *
270     * @return string
271     */
272    protected function valueAutoLink(string $value): string
273    {
274        // Convert URLs into markdown auto-links.
275        $value = preg_replace(self::REGEX_URL, '<$0>', $value);
276
277        // Create a minimal commonmark processor - just add support for autolinks.
278        $environment = new Environment();
279        $environment
280            ->addBlockRenderer(Document::class, new DocumentRenderer())
281            ->addBlockRenderer(Paragraph::class, new ParagraphRenderer())
282            ->addInlineRenderer(Text::class, new TextRenderer())
283            ->addInlineRenderer(Link::class, new LinkRenderer())
284            ->addInlineParser(new AutolinkParser());
285
286        $converter = new CommonMarkConverter(['html_input' => Environment::HTML_INPUT_ESCAPE], $environment);
287
288        return $converter->convertToHtml($value);
289    }
290
291    /**
292     * Display the value of this type of element.
293     *
294     * @param string $value
295     *
296     * @return string
297     */
298    public function valueNumeric(string $value): string
299    {
300        $canonical = $this->canonical($value);
301
302        if (is_numeric($canonical)) {
303            return I18N::number((int) $canonical);
304        }
305
306        return e($value);
307    }
308}
309