. */ 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 preg_replace; 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 MAXIMUM_LENGTH = false; protected const PATTERN = false; // Which child elements can appear under this element. protected const SUBTAGS = []; // A label to describe this element private string $label; /** @var array Subtags of this element */ private array $subtags; /** * AbstractGedcomElement constructor. * * @param string $label * @param array|null $subtags */ public function __construct(string $label, array $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, ["\t" => ' ', "\r" => ' ', "\n" => ' ']); 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 { // Browsers use MS-DOS line endings in multi-line data. $value = strtr($value, ["\t" => ' ', "\r\n" => "\n", "\r" => "\n"]); // Remove blank lines at start/end $value = preg_replace('/^( *\n)+/', '', $value); return preg_replace('/(\n *)+$/', '', $value); } /** * 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 ($repeat === '') { unset($this->subtags[$subtag]); } elseif ($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)) . ''; } 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()->convertToHtml($canonical); return strip_tags($html, ['a']); } return e($canonical); } /** * 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': $html = Registry::markdownFactory()->markdown($tree)->convertToHtml($canonical); return '
' . $html . '
'; case 'markdown-br': $html = Registry::markdownFactory()->markdown($tree)->convertToHtml($canonical); return '
' . $html . '
'; default: $html = Registry::markdownFactory()->autolink($tree)->convertToHtml($canonical); $html = strtr($html, ["

\n

" => "

"]); $html = strip_tags($html, ['a', 'br']); $html = trim($html); if (str_contains($html, "\n")) { $html = nl2br($html); return '

' . $html . '
'; } return '' . $html . ''; } } /** * 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); } }