. */ declare(strict_types=1); namespace Fisharebest\Webtrees\Elements; use Fisharebest\Webtrees\Contracts\ElementInterface; use Fisharebest\Webtrees\Html; use Fisharebest\Webtrees\I18N; use Fisharebest\Webtrees\Registry; use Fisharebest\Webtrees\Tree; use function array_key_exists; use function array_map; use function e; use function is_numeric; use function nl2br; use function str_contains; use function str_starts_with; use function strip_tags; use function trim; use function view; /** * A GEDCOM element is a tag/primitive in a GEDCOM file. */ abstract class AbstractElement implements ElementInterface { // HTML attributes for an protected const int|false MAXIMUM_LENGTH = false; protected const string|false PATTERN = false; private const array WHITESPACE_LINE = [ "\t" => ' ', "\n" => ' ', "\r" => ' ', "\v" => ' ', // Vertical tab "\u{85}" => ' ', // NEL - newline "\u{2028}" => ' ', // LS - line separator "\u{2029}" => ' ', // PS - paragraph separator ]; private const array WHITESPACE_TEXT = [ "\t" => ' ', "\r\n" => "\n", "\r" => "\n", "\v" => "\n", "\u{85}" => "\n", "\u{2028}" => "\n", "\u{2029}" => "\n\n", ]; // Which child elements can appear under this element. protected const array SUBTAGS = []; // A label to describe this element private string $label; /** @var array Subtags of this element */ private array $subtags; /** * @param string $label * @param array|null $subtags */ public function __construct(string $label, array|null $subtags = null) { $this->label = $label; $this->subtags = $subtags ?? static::SUBTAGS; } /** * Convert a value to a canonical form. * * @param string $value * * @return string */ public function canonical(string $value): string { $value = strtr($value, self::WHITESPACE_LINE); while (str_contains($value, ' ')) { $value = strtr($value, [' ' => ' ']); } return trim($value); } /** * Convert a multi-line value to a canonical form. * * @param string $value * * @return string */ protected function canonicalText(string $value): string { $value = strtr($value, self::WHITESPACE_TEXT); return trim($value, "\n"); } /** * Should we collapse the children of this element when editing? * * @return bool */ public function collapseChildren(): bool { return false; } /** * Create a default value for this element. * * @param Tree $tree * * @return string */ public function default(Tree $tree): string { return ''; } /** * An edit control for this data. * * @param string $id * @param string $name * @param string $value * @param Tree $tree * * @return string */ public function edit(string $id, string $name, string $value, Tree $tree): string { $values = $this->values(); if ($values !== []) { $value = $this->canonical($value); // Ensure the current data is in the list. if (!array_key_exists($value, $values)) { $values = [$value => $value] + $values; } // We may use markup to display values, but not when editing them. $values = array_map(static fn (string $x): string => strip_tags($x), $values); return view('components/select', [ 'id' => $id, 'name' => $name, 'options' => $values, 'selected' => $value, ]); } $attributes = [ 'class' => 'form-control', 'dir' => 'auto', 'type' => 'text', 'id' => $id, 'name' => $name, 'value' => $value, 'maxlength' => static::MAXIMUM_LENGTH, 'pattern' => static::PATTERN, ]; return ''; } /** * An edit control for this data. * * @param string $id * @param string $name * @param string $value * * @return string */ public function editHidden(string $id, string $name, string $value): string { return ''; } /** * An edit control for this data. * * @param string $id * @param string $name * @param string $value * * @return string */ public function editTextArea(string $id, string $name, string $value): string { return ''; } /** * Escape @ signs in a GEDCOM export. * * @param string $value * * @return string */ public function escape(string $value): string { return strtr($value, ['@' => '@@']); } /** * Create a label for this element. * * @return string */ public function label(): string { return $this->label; } /** * Create a label/value pair for this element. * * @param string $value * @param Tree $tree * * @return string */ public function labelValue(string $value, Tree $tree): string { $label = '' . $this->label() . ''; $value = '' . $this->value($value, $tree) . ''; $html = I18N::translate(/* I18N: e.g. "Occupation: farmer" */ '%1$s: %2$s', $label, $value); return '
' . $html . '
'; } /** * Set, remove or replace a subtag. * * @param string $subtag * @param string $repeat * @param string $before * * @return void */ public function subtag(string $subtag, string $repeat, string $before = ''): void { if ($before === '' || ($this->subtags[$before] ?? null) === null) { $this->subtags[$subtag] = $repeat; } else { $tmp = []; foreach ($this->subtags as $key => $value) { if ($key === $before) { $tmp[$subtag] = $repeat; } $tmp[$key] = $value; } $this->subtags = $tmp; } } /** * @return array */ public function subtags(): array { return $this->subtags; } /** * Display the value of this type of element. * * @param string $value * @param Tree $tree * * @return string */ public function value(string $value, Tree $tree): string { $values = $this->values(); if ($values === []) { if (str_contains($value, "\n")) { return '' . nl2br(e($value, false)) . ''; } return '' . e($value) . ''; } $canonical = $this->canonical($value); return $values[$canonical] ?? '' . e($value) . ''; } /** * A list of controlled values for this element * * @return array */ public function values(): array { return []; } /** * Display the value of this type of element - convert URLs to links. * * @param string $value * * @return string */ protected function valueAutoLink(string $value): string { $canonical = $this->canonical($value); if (str_contains($canonical, 'http://') || str_contains($canonical, 'https://')) { $html = Registry::markdownFactory()->autolink($canonical); $html = strip_tags($html, ['a', 'br']); } else { $html = nl2br(e($canonical), false); } if (str_contains($html, '
')) { return '' . $html . ''; } return '' . $html . ''; } /** * Display the value of this type of element - multi-line text with/without markdown. * * @param string $value * @param Tree $tree * * @return string */ protected function valueFormatted(string $value, Tree $tree): string { $canonical = $this->canonical($value); $format = $tree->getPreference('FORMAT_TEXT'); switch ($format) { case 'markdown': return Registry::markdownFactory()->markdown($canonical, $tree); default: return Registry::markdownFactory()->autolink($canonical, $tree); } } /** * Display the value of this type of element - convert to URL. * * @param string $value * * @return string */ protected function valueLink(string $value): string { $canonical = $this->canonical($value); if (str_starts_with($canonical, 'https://') || str_starts_with($canonical, 'http://')) { return '' . e($value) . ''; } return e($value); } /** * Display the value of this type of element. * * @param string $value * * @return string */ public function valueNumeric(string $value): string { $canonical = $this->canonical($value); if (is_numeric($canonical)) { return I18N::number((int) $canonical); } return e($value); } }