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