xref: /webtrees/app/Services/GedcomEditService.php (revision a9da23742230e8aaebca3a72f1379485d683af31)
17c7d1e03SGreg Roach<?php
27c7d1e03SGreg Roach
37c7d1e03SGreg Roach/**
47c7d1e03SGreg Roach * webtrees: online genealogy
589f7189bSGreg Roach * Copyright (C) 2021 webtrees development team
67c7d1e03SGreg Roach * This program is free software: you can redistribute it and/or modify
77c7d1e03SGreg Roach * it under the terms of the GNU General Public License as published by
87c7d1e03SGreg Roach * the Free Software Foundation, either version 3 of the License, or
97c7d1e03SGreg Roach * (at your option) any later version.
107c7d1e03SGreg Roach * This program is distributed in the hope that it will be useful,
117c7d1e03SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
127c7d1e03SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
137c7d1e03SGreg Roach * GNU General Public License for more details.
147c7d1e03SGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
167c7d1e03SGreg Roach */
177c7d1e03SGreg Roach
187c7d1e03SGreg Roachdeclare(strict_types=1);
197c7d1e03SGreg Roach
207c7d1e03SGreg Roachnamespace Fisharebest\Webtrees\Services;
217c7d1e03SGreg Roach
22e22e42f7SGreg Roachuse Fisharebest\Webtrees\Fact;
237c7d1e03SGreg Roachuse Fisharebest\Webtrees\Gedcom;
24f8505e4aSGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
25c2ed51d1SGreg Roachuse Fisharebest\Webtrees\Registry;
26f8505e4aSGreg Roachuse Fisharebest\Webtrees\Site;
277c7d1e03SGreg Roachuse Fisharebest\Webtrees\Tree;
287c7d1e03SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
297c7d1e03SGreg Roach
30f8505e4aSGreg Roachuse function array_filter;
317c7d1e03SGreg Roachuse function array_merge;
32f8505e4aSGreg Roachuse function array_shift;
337c7d1e03SGreg Roachuse function array_unique;
34f8505e4aSGreg Roachuse function array_values;
357c7d1e03SGreg Roachuse function assert;
367c7d1e03SGreg Roachuse function count;
37f8505e4aSGreg Roachuse function explode;
38f8505e4aSGreg Roachuse function implode;
39f8505e4aSGreg Roachuse function max;
407c7d1e03SGreg Roachuse function preg_match_all;
41f8505e4aSGreg Roachuse function preg_replace;
42f8505e4aSGreg Roachuse function preg_split;
43f8505e4aSGreg Roachuse function str_repeat;
447c7d1e03SGreg Roachuse function str_replace;
45f8505e4aSGreg Roachuse function substr_count;
467c7d1e03SGreg Roachuse function trim;
477c7d1e03SGreg Roach
48f8505e4aSGreg Roachuse const ARRAY_FILTER_USE_KEY;
49f8505e4aSGreg Roachuse const PHP_INT_MAX;
50f8505e4aSGreg Roach
517c7d1e03SGreg Roach/**
527c7d1e03SGreg Roach * Utilities to edit/save GEDCOM data.
537c7d1e03SGreg Roach */
547c7d1e03SGreg Roachclass GedcomEditService
557c7d1e03SGreg Roach{
567c7d1e03SGreg Roach    /** @var string[] */
577c7d1e03SGreg Roach    public $glevels = [];
587c7d1e03SGreg Roach
597c7d1e03SGreg Roach    /** @var string[] */
607c7d1e03SGreg Roach    public $tag = [];
617c7d1e03SGreg Roach
627c7d1e03SGreg Roach    /** @var string[] */
637c7d1e03SGreg Roach    public $islink = [];
647c7d1e03SGreg Roach
657c7d1e03SGreg Roach    /** @var string[] */
667c7d1e03SGreg Roach    public $text = [];
677c7d1e03SGreg Roach
687c7d1e03SGreg Roach    /** @var string[] */
697c7d1e03SGreg Roach    protected $glevelsSOUR = [];
707c7d1e03SGreg Roach
717c7d1e03SGreg Roach    /** @var string[] */
727c7d1e03SGreg Roach    protected $tagSOUR = [];
737c7d1e03SGreg Roach
747c7d1e03SGreg Roach    /** @var string[] */
757c7d1e03SGreg Roach    protected $islinkSOUR = [];
767c7d1e03SGreg Roach
777c7d1e03SGreg Roach    /** @var string[] */
787c7d1e03SGreg Roach    protected $textSOUR = [];
797c7d1e03SGreg Roach
807c7d1e03SGreg Roach    /** @var string[] */
817c7d1e03SGreg Roach    protected $glevelsRest = [];
827c7d1e03SGreg Roach
837c7d1e03SGreg Roach    /** @var string[] */
847c7d1e03SGreg Roach    protected $tagRest = [];
857c7d1e03SGreg Roach
867c7d1e03SGreg Roach    /** @var string[] */
877c7d1e03SGreg Roach    protected $islinkRest = [];
887c7d1e03SGreg Roach
897c7d1e03SGreg Roach    /** @var string[] */
907c7d1e03SGreg Roach    protected $textRest = [];
917c7d1e03SGreg Roach
927c7d1e03SGreg Roach    /**
937c7d1e03SGreg Roach     * This function splits the $glevels, $tag, $islink, and $text arrays so that the
947c7d1e03SGreg Roach     * entries associated with a SOUR record are separate from everything else.
957c7d1e03SGreg Roach     *
967c7d1e03SGreg Roach     * Input arrays:
977c7d1e03SGreg Roach     * - $glevels[] - an array of the gedcom level for each line that was edited
987c7d1e03SGreg Roach     * - $tag[] - an array of the tags for each gedcom line that was edited
997c7d1e03SGreg Roach     * - $islink[] - an array of 1 or 0 values to indicate when the text is a link element
1007c7d1e03SGreg Roach     * - $text[] - an array of the text data for each line
1017c7d1e03SGreg Roach     *
1027c7d1e03SGreg Roach     * Output arrays:
1037c7d1e03SGreg Roach     * ** For the SOUR record:
1047c7d1e03SGreg Roach     * - $glevelsSOUR[] - an array of the gedcom level for each line that was edited
1057c7d1e03SGreg Roach     * - $tagSOUR[] - an array of the tags for each gedcom line that was edited
1067c7d1e03SGreg Roach     * - $islinkSOUR[] - an array of 1 or 0 values to indicate when the text is a link element
1077c7d1e03SGreg Roach     * - $textSOUR[] - an array of the text data for each line
1087c7d1e03SGreg Roach     * ** For the remaining records:
1097c7d1e03SGreg Roach     * - $glevelsRest[] - an array of the gedcom level for each line that was edited
1107c7d1e03SGreg Roach     * - $tagRest[] - an array of the tags for each gedcom line that was edited
1117c7d1e03SGreg Roach     * - $islinkRest[] - an array of 1 or 0 values to indicate when the text is a link element
1127c7d1e03SGreg Roach     * - $textRest[] - an array of the text data for each line
1137c7d1e03SGreg Roach     *
1147c7d1e03SGreg Roach     * @return void
1157c7d1e03SGreg Roach     */
1167c7d1e03SGreg Roach    public function splitSource(): void
1177c7d1e03SGreg Roach    {
1187c7d1e03SGreg Roach        $this->glevelsSOUR = [];
1197c7d1e03SGreg Roach        $this->tagSOUR     = [];
1207c7d1e03SGreg Roach        $this->islinkSOUR  = [];
1217c7d1e03SGreg Roach        $this->textSOUR    = [];
1227c7d1e03SGreg Roach
1237c7d1e03SGreg Roach        $this->glevelsRest = [];
1247c7d1e03SGreg Roach        $this->tagRest     = [];
1257c7d1e03SGreg Roach        $this->islinkRest  = [];
1267c7d1e03SGreg Roach        $this->textRest    = [];
1277c7d1e03SGreg Roach
1287c7d1e03SGreg Roach        $inSOUR    = false;
1297c7d1e03SGreg Roach        $levelSOUR = 0;
1307c7d1e03SGreg Roach
1317c7d1e03SGreg Roach        // Assume all arrays are the same size.
1327c7d1e03SGreg Roach        $count = count($this->glevels);
1337c7d1e03SGreg Roach
1347c7d1e03SGreg Roach        for ($i = 0; $i < $count; $i++) {
1357c7d1e03SGreg Roach            if ($inSOUR) {
1367c7d1e03SGreg Roach                if ($levelSOUR < $this->glevels[$i]) {
1377c7d1e03SGreg Roach                    $dest = 'S';
1387c7d1e03SGreg Roach                } else {
1397c7d1e03SGreg Roach                    $inSOUR = false;
1407c7d1e03SGreg Roach                    $dest   = 'R';
1417c7d1e03SGreg Roach                }
1427c7d1e03SGreg Roach            } elseif ($this->tag[$i] === 'SOUR') {
1437c7d1e03SGreg Roach                $inSOUR    = true;
1447c7d1e03SGreg Roach                $levelSOUR = $this->glevels[$i];
1457c7d1e03SGreg Roach                $dest      = 'S';
1467c7d1e03SGreg Roach            } else {
1477c7d1e03SGreg Roach                $dest = 'R';
1487c7d1e03SGreg Roach            }
1497c7d1e03SGreg Roach
1507c7d1e03SGreg Roach            if ($dest === 'S') {
1517c7d1e03SGreg Roach                $this->glevelsSOUR[] = $this->glevels[$i];
1527c7d1e03SGreg Roach                $this->tagSOUR[]     = $this->tag[$i];
1537c7d1e03SGreg Roach                $this->islinkSOUR[]  = $this->islink[$i];
1547c7d1e03SGreg Roach                $this->textSOUR[]    = $this->text[$i];
1557c7d1e03SGreg Roach            } else {
1567c7d1e03SGreg Roach                $this->glevelsRest[] = $this->glevels[$i];
1577c7d1e03SGreg Roach                $this->tagRest[]     = $this->tag[$i];
1587c7d1e03SGreg Roach                $this->islinkRest[]  = $this->islink[$i];
1597c7d1e03SGreg Roach                $this->textRest[]    = $this->text[$i];
1607c7d1e03SGreg Roach            }
1617c7d1e03SGreg Roach        }
1627c7d1e03SGreg Roach    }
1637c7d1e03SGreg Roach
1647c7d1e03SGreg Roach    /**
1657c7d1e03SGreg Roach     * Add new GEDCOM lines from the $xxxRest interface update arrays, which
1667c7d1e03SGreg Roach     * were produced by the splitSOUR() function.
1677c7d1e03SGreg Roach     * See the FunctionsEdit::handle_updatesges() function for details.
1687c7d1e03SGreg Roach     *
1697c7d1e03SGreg Roach     * @param string $inputRec
1707c7d1e03SGreg Roach     *
1717c7d1e03SGreg Roach     * @return string
1727c7d1e03SGreg Roach     */
1737c7d1e03SGreg Roach    public function updateRest(string $inputRec): string
1747c7d1e03SGreg Roach    {
1757c7d1e03SGreg Roach        if (count($this->tagRest) === 0) {
1767c7d1e03SGreg Roach            return $inputRec; // No update required
1777c7d1e03SGreg Roach        }
1787c7d1e03SGreg Roach
1797c7d1e03SGreg Roach        // Save original interface update arrays before replacing them with the xxxRest ones
1807c7d1e03SGreg Roach        $glevelsSave = $this->glevels;
1817c7d1e03SGreg Roach        $tagSave     = $this->tag;
1827c7d1e03SGreg Roach        $islinkSave  = $this->islink;
1837c7d1e03SGreg Roach        $textSave    = $this->text;
1847c7d1e03SGreg Roach
1857c7d1e03SGreg Roach        $this->glevels = $this->glevelsRest;
1867c7d1e03SGreg Roach        $this->tag     = $this->tagRest;
1877c7d1e03SGreg Roach        $this->islink  = $this->islinkRest;
1887c7d1e03SGreg Roach        $this->text    = $this->textRest;
1897c7d1e03SGreg Roach
1907c7d1e03SGreg Roach        $myRecord = $this->handleUpdates($inputRec, 'no'); // Now do the update
1917c7d1e03SGreg Roach
1927c7d1e03SGreg Roach        // Restore the original interface update arrays (just in case ...)
1937c7d1e03SGreg Roach        $this->glevels = $glevelsSave;
1947c7d1e03SGreg Roach        $this->tag     = $tagSave;
1957c7d1e03SGreg Roach        $this->islink  = $islinkSave;
1967c7d1e03SGreg Roach        $this->text    = $textSave;
1977c7d1e03SGreg Roach
1987c7d1e03SGreg Roach        return $myRecord;
1997c7d1e03SGreg Roach    }
2007c7d1e03SGreg Roach
2017c7d1e03SGreg Roach    /**
2027c7d1e03SGreg Roach     * Add new gedcom lines from interface update arrays
2037c7d1e03SGreg Roach     * The edit_interface and FunctionsEdit::add_simple_tag function produce the following
2047c7d1e03SGreg Roach     * arrays incoming from the $_POST form
2057c7d1e03SGreg Roach     * - $glevels[] - an array of the gedcom level for each line that was edited
2067c7d1e03SGreg Roach     * - $tag[] - an array of the tags for each gedcom line that was edited
2077c7d1e03SGreg Roach     * - $islink[] - an array of 1 or 0 values to tell whether the text is a link element and should be surrounded by @@
2087c7d1e03SGreg Roach     * - $text[] - an array of the text data for each line
2097c7d1e03SGreg Roach     * With these arrays you can recreate the gedcom lines like this
2107c7d1e03SGreg Roach     * <code>$glevel[0].' '.$tag[0].' '.$text[0]</code>
2117c7d1e03SGreg Roach     * There will be an index in each of these arrays for each line of the gedcom
2127c7d1e03SGreg Roach     * fact that is being edited.
2137c7d1e03SGreg Roach     * If the $text[] array is empty for the given line, then it means that the
2147c7d1e03SGreg Roach     * user removed that line during editing or that the line is supposed to be
2157c7d1e03SGreg Roach     * empty (1 DEAT, 1 BIRT) for example. To know if the line should be removed
2167c7d1e03SGreg Roach     * there is a section of code that looks ahead to the next lines to see if there
2177c7d1e03SGreg Roach     * are sub lines. For example we don't want to remove the 1 DEAT line if it has
2187c7d1e03SGreg Roach     * a 2 PLAC or 2 DATE line following it. If there are no sub lines, then the line
2197c7d1e03SGreg Roach     * can be safely removed.
2207c7d1e03SGreg Roach     *
2217c7d1e03SGreg Roach     * @param string $newged        the new gedcom record to add the lines to
2227c7d1e03SGreg Roach     * @param string $levelOverride Override GEDCOM level specified in $glevels[0]
2237c7d1e03SGreg Roach     *
2247c7d1e03SGreg Roach     * @return string The updated gedcom record
2257c7d1e03SGreg Roach     */
226c8183f29SGreg Roach    public function handleUpdates(string $newged, string $levelOverride = 'no'): string
2277c7d1e03SGreg Roach    {
2287c7d1e03SGreg Roach        if ($levelOverride === 'no') {
2297c7d1e03SGreg Roach            $levelAdjust = 0;
2307c7d1e03SGreg Roach        } else {
2317c7d1e03SGreg Roach            $levelAdjust = 1;
2327c7d1e03SGreg Roach        }
2337c7d1e03SGreg Roach
2347c7d1e03SGreg Roach        // Assert all arrays are the same size.
2357c7d1e03SGreg Roach        assert(count($this->glevels) === count($this->tag));
2367c7d1e03SGreg Roach        assert(count($this->glevels) === count($this->text));
2377c7d1e03SGreg Roach        assert(count($this->glevels) === count($this->islink));
2387c7d1e03SGreg Roach
2397c7d1e03SGreg Roach        $count = count($this->glevels);
2407c7d1e03SGreg Roach
2417c7d1e03SGreg Roach        for ($j = 0; $j < $count; $j++) {
2427c7d1e03SGreg Roach            // Look for empty SOUR reference with non-empty sub-records.
2437c7d1e03SGreg Roach            // This can happen when the SOUR entry is deleted but its sub-records
2447c7d1e03SGreg Roach            // were incorrectly left intact.
2457c7d1e03SGreg Roach            // The sub-records should be deleted.
2467c7d1e03SGreg Roach            if ($this->tag[$j] === 'SOUR' && ($this->text[$j] === '@@' || $this->text[$j] === '')) {
2477c7d1e03SGreg Roach                $this->text[$j] = '';
2487c7d1e03SGreg Roach                $k              = $j + 1;
2497c7d1e03SGreg Roach                while ($k < $count && $this->glevels[$k] > $this->glevels[$j]) {
2507c7d1e03SGreg Roach                    $this->text[$k] = '';
2517c7d1e03SGreg Roach                    $k++;
2527c7d1e03SGreg Roach                }
2537c7d1e03SGreg Roach            }
2547c7d1e03SGreg Roach
2557c7d1e03SGreg Roach            if (trim($this->text[$j]) !== '') {
2567c7d1e03SGreg Roach                $pass = true;
2577c7d1e03SGreg Roach            } else {
2587c7d1e03SGreg Roach                //-- for facts with empty values they must have sub records
2597c7d1e03SGreg Roach                //-- this section checks if they have subrecords
2607c7d1e03SGreg Roach                $k    = $j + 1;
2617c7d1e03SGreg Roach                $pass = false;
2627c7d1e03SGreg Roach                while ($k < $count && $this->glevels[$k] > $this->glevels[$j]) {
2637c7d1e03SGreg Roach                    if ($this->text[$k] !== '') {
264c2ed51d1SGreg Roach                        if ($this->tag[$j] !== 'OBJE' || $this->tag[$k] === 'FILE') {
2657c7d1e03SGreg Roach                            $pass = true;
2667c7d1e03SGreg Roach                            break;
2677c7d1e03SGreg Roach                        }
2687c7d1e03SGreg Roach                    }
2697c7d1e03SGreg Roach                    $k++;
2707c7d1e03SGreg Roach                }
2717c7d1e03SGreg Roach            }
2727c7d1e03SGreg Roach
2737c7d1e03SGreg Roach            //-- if the value is not empty or it has sub lines
2747c7d1e03SGreg Roach            //--- then write the line to the gedcom record
2757c7d1e03SGreg Roach            //-- we have to let some emtpy text lines pass through... (DEAT, BIRT, etc)
2767c7d1e03SGreg Roach            if ($pass) {
2777c7d1e03SGreg Roach                $newline = (int) $this->glevels[$j] + $levelAdjust . ' ' . $this->tag[$j];
2787c7d1e03SGreg Roach                if ($this->text[$j] !== '') {
2797c7d1e03SGreg Roach                    if ($this->islink[$j]) {
28065de9aa7SGreg Roach                        $newline .= ' @' . trim($this->text[$j], '@') . '@';
2817c7d1e03SGreg Roach                    } else {
2827c7d1e03SGreg Roach                        $newline .= ' ' . $this->text[$j];
2837c7d1e03SGreg Roach                    }
2847c7d1e03SGreg Roach                }
2857c7d1e03SGreg Roach                $next_level = 1 + (int) $this->glevels[$j] + $levelAdjust;
2867c7d1e03SGreg Roach
2877c7d1e03SGreg Roach                $newged .= "\n" . str_replace("\n", "\n" . $next_level . ' CONT ', $newline);
2887c7d1e03SGreg Roach            }
2897c7d1e03SGreg Roach        }
2907c7d1e03SGreg Roach
2917c7d1e03SGreg Roach        return $newged;
2927c7d1e03SGreg Roach    }
2937c7d1e03SGreg Roach
2947c7d1e03SGreg Roach    /**
2957c7d1e03SGreg Roach     * Create a form to add a new fact.
2967c7d1e03SGreg Roach     *
2977c7d1e03SGreg Roach     * @param ServerRequestInterface $request
2987c7d1e03SGreg Roach     * @param Tree                   $tree
2997c7d1e03SGreg Roach     * @param string                 $fact
3007c7d1e03SGreg Roach     *
3017c7d1e03SGreg Roach     * @return string
3027c7d1e03SGreg Roach     */
30324f2a3afSGreg Roach    public function addNewFact(ServerRequestInterface $request, Tree $tree, string $fact): string
3047c7d1e03SGreg Roach    {
3057c7d1e03SGreg Roach        $params = (array) $request->getParsedBody();
3067c7d1e03SGreg Roach
3077c7d1e03SGreg Roach        $FACT = $params[$fact];
3087c7d1e03SGreg Roach        $DATE = $params[$fact . '_DATE'] ?? '';
3097c7d1e03SGreg Roach        $PLAC = $params[$fact . '_PLAC'] ?? '';
3107c7d1e03SGreg Roach
3117c7d1e03SGreg Roach        if ($DATE !== '' || $PLAC !== '' || $FACT !== '' && $FACT !== 'Y') {
3127c7d1e03SGreg Roach            if ($FACT !== '' && $FACT !== 'Y') {
3137c7d1e03SGreg Roach                $gedrec = "\n1 " . $fact . ' ' . $FACT;
3147c7d1e03SGreg Roach            } else {
3157c7d1e03SGreg Roach                $gedrec = "\n1 " . $fact;
3167c7d1e03SGreg Roach            }
3177c7d1e03SGreg Roach            if ($DATE !== '') {
3187c7d1e03SGreg Roach                $gedrec .= "\n2 DATE " . $DATE;
3197c7d1e03SGreg Roach            }
3207c7d1e03SGreg Roach            if ($PLAC !== '') {
3217c7d1e03SGreg Roach                $gedrec .= "\n2 PLAC " . $PLAC;
3227c7d1e03SGreg Roach
3237c7d1e03SGreg Roach                if (preg_match_all('/(' . Gedcom::REGEX_TAG . ')/', $tree->getPreference('ADVANCED_PLAC_FACTS'), $match)) {
3247c7d1e03SGreg Roach                    foreach ($match[1] as $tag) {
3257c7d1e03SGreg Roach                        $TAG = $params[$fact . '_' . $tag];
3267c7d1e03SGreg Roach                        if ($TAG !== '') {
3277c7d1e03SGreg Roach                            $gedrec .= "\n3 " . $tag . ' ' . $TAG;
3287c7d1e03SGreg Roach                        }
3297c7d1e03SGreg Roach                    }
3307c7d1e03SGreg Roach                }
33150f35038SGreg Roach                $LATI = $params[$fact . '_LATI'] ?? '';
33250f35038SGreg Roach                $LONG = $params[$fact . '_LONG'] ?? '';
3337c7d1e03SGreg Roach                if ($LATI !== '' || $LONG !== '') {
3347c7d1e03SGreg Roach                    $gedrec .= "\n3 MAP\n4 LATI " . $LATI . "\n4 LONG " . $LONG;
3357c7d1e03SGreg Roach                }
3367c7d1e03SGreg Roach            }
3377c7d1e03SGreg Roach            if ((bool) ($params['SOUR_' . $fact] ?? false)) {
3387c7d1e03SGreg Roach                return $this->updateSource($gedrec, 'yes');
3397c7d1e03SGreg Roach            }
3407c7d1e03SGreg Roach
3417c7d1e03SGreg Roach            return $gedrec;
3427c7d1e03SGreg Roach        }
3437c7d1e03SGreg Roach
3447c7d1e03SGreg Roach        if ($FACT === 'Y') {
3457c7d1e03SGreg Roach            if ((bool) ($params['SOUR_' . $fact] ?? false)) {
3467c7d1e03SGreg Roach                return $this->updateSource("\n1 " . $fact . ' Y', 'yes');
3477c7d1e03SGreg Roach            }
3487c7d1e03SGreg Roach
3497c7d1e03SGreg Roach            return "\n1 " . $fact . ' Y';
3507c7d1e03SGreg Roach        }
3517c7d1e03SGreg Roach
3527c7d1e03SGreg Roach        return '';
3537c7d1e03SGreg Roach    }
3547c7d1e03SGreg Roach
3557c7d1e03SGreg Roach    /**
3567c7d1e03SGreg Roach     * Add new GEDCOM lines from the $xxxSOUR interface update arrays, which
3577c7d1e03SGreg Roach     * were produced by the splitSOUR() function.
3587c7d1e03SGreg Roach     * See the FunctionsEdit::handle_updatesges() function for details.
3597c7d1e03SGreg Roach     *
3607c7d1e03SGreg Roach     * @param string $inputRec
3617c7d1e03SGreg Roach     * @param string $levelOverride
3627c7d1e03SGreg Roach     *
3637c7d1e03SGreg Roach     * @return string
3647c7d1e03SGreg Roach     */
3657c7d1e03SGreg Roach    public function updateSource(string $inputRec, string $levelOverride = 'no'): string
3667c7d1e03SGreg Roach    {
3677c7d1e03SGreg Roach        if (count($this->tagSOUR) === 0) {
3687c7d1e03SGreg Roach            return $inputRec; // No update required
3697c7d1e03SGreg Roach        }
3707c7d1e03SGreg Roach
3717c7d1e03SGreg Roach        // Save original interface update arrays before replacing them with the xxxSOUR ones
3727c7d1e03SGreg Roach        $glevelsSave = $this->glevels;
3737c7d1e03SGreg Roach        $tagSave     = $this->tag;
3747c7d1e03SGreg Roach        $islinkSave  = $this->islink;
3757c7d1e03SGreg Roach        $textSave    = $this->text;
3767c7d1e03SGreg Roach
3777c7d1e03SGreg Roach        $this->glevels = $this->glevelsSOUR;
3787c7d1e03SGreg Roach        $this->tag     = $this->tagSOUR;
3797c7d1e03SGreg Roach        $this->islink  = $this->islinkSOUR;
3807c7d1e03SGreg Roach        $this->text    = $this->textSOUR;
3817c7d1e03SGreg Roach
3827c7d1e03SGreg Roach        $myRecord = $this->handleUpdates($inputRec, $levelOverride); // Now do the update
3837c7d1e03SGreg Roach
3847c7d1e03SGreg Roach        // Restore the original interface update arrays (just in case ...)
3857c7d1e03SGreg Roach        $this->glevels = $glevelsSave;
3867c7d1e03SGreg Roach        $this->tag     = $tagSave;
3877c7d1e03SGreg Roach        $this->islink  = $islinkSave;
3887c7d1e03SGreg Roach        $this->text    = $textSave;
3897c7d1e03SGreg Roach
3907c7d1e03SGreg Roach        return $myRecord;
3917c7d1e03SGreg Roach    }
3927c7d1e03SGreg Roach
3937c7d1e03SGreg Roach    /**
3947c7d1e03SGreg Roach     * Create a form to add a sex record.
3957c7d1e03SGreg Roach     *
3967c7d1e03SGreg Roach     * @param ServerRequestInterface $request
3977c7d1e03SGreg Roach     *
3987c7d1e03SGreg Roach     * @return string
3997c7d1e03SGreg Roach     */
4007c7d1e03SGreg Roach    public function addNewSex(ServerRequestInterface $request): string
4017c7d1e03SGreg Roach    {
4027c7d1e03SGreg Roach        $params = (array) $request->getParsedBody();
4037c7d1e03SGreg Roach
4047c7d1e03SGreg Roach        switch ($params['SEX']) {
4057c7d1e03SGreg Roach            case 'M':
4067c7d1e03SGreg Roach                return "\n1 SEX M";
4077c7d1e03SGreg Roach            case 'F':
4087c7d1e03SGreg Roach                return "\n1 SEX F";
4097c7d1e03SGreg Roach            default:
4107c7d1e03SGreg Roach                return "\n1 SEX U";
4117c7d1e03SGreg Roach        }
4127c7d1e03SGreg Roach    }
4137c7d1e03SGreg Roach
4147c7d1e03SGreg Roach    /**
4157c7d1e03SGreg Roach     * Assemble the pieces of a newly created record into gedcom
4167c7d1e03SGreg Roach     *
4177c7d1e03SGreg Roach     * @param ServerRequestInterface $request
4187c7d1e03SGreg Roach     * @param Tree                   $tree
4197c7d1e03SGreg Roach     *
4207c7d1e03SGreg Roach     * @return string
4217c7d1e03SGreg Roach     */
4227c7d1e03SGreg Roach    public function addNewName(ServerRequestInterface $request, Tree $tree): string
4237c7d1e03SGreg Roach    {
4247c7d1e03SGreg Roach        $params = (array) $request->getParsedBody();
4257c7d1e03SGreg Roach        $gedrec = "\n1 NAME " . $params['NAME'];
4267c7d1e03SGreg Roach
4277c7d1e03SGreg Roach        $tags = [
4287c7d1e03SGreg Roach            'NPFX',
4297c7d1e03SGreg Roach            'GIVN',
4307c7d1e03SGreg Roach            'SPFX',
4317c7d1e03SGreg Roach            'SURN',
4327c7d1e03SGreg Roach            'NSFX',
4337c7d1e03SGreg Roach            'NICK',
4347c7d1e03SGreg Roach        ];
4357c7d1e03SGreg Roach
4367c7d1e03SGreg Roach        if (preg_match_all('/(' . Gedcom::REGEX_TAG . ')/', $tree->getPreference('ADVANCED_NAME_FACTS'), $match)) {
4377c7d1e03SGreg Roach            $tags = array_merge($tags, $match[1]);
4387c7d1e03SGreg Roach        }
4397c7d1e03SGreg Roach
4407c7d1e03SGreg Roach        // Paternal and Polish and Lithuanian surname traditions can also create a _MARNM
4417c7d1e03SGreg Roach        $SURNAME_TRADITION = $tree->getPreference('SURNAME_TRADITION');
4427c7d1e03SGreg Roach        if ($SURNAME_TRADITION === 'paternal' || $SURNAME_TRADITION === 'polish' || $SURNAME_TRADITION === 'lithuanian') {
4437c7d1e03SGreg Roach            $tags[] = '_MARNM';
4447c7d1e03SGreg Roach        }
4457c7d1e03SGreg Roach
4467c7d1e03SGreg Roach        foreach (array_unique($tags) as $tag) {
4477c7d1e03SGreg Roach            $TAG = $params[$tag];
4487c7d1e03SGreg Roach
4497c7d1e03SGreg Roach            if ($TAG !== '') {
4507c7d1e03SGreg Roach                $gedrec .= "\n2 " . $tag . ' ' . $TAG;
4517c7d1e03SGreg Roach            }
4527c7d1e03SGreg Roach        }
4537c7d1e03SGreg Roach
4547c7d1e03SGreg Roach        return $gedrec;
4557c7d1e03SGreg Roach    }
456c2ed51d1SGreg Roach
457c2ed51d1SGreg Roach    /**
458c2ed51d1SGreg Roach     * Reassemble edited GEDCOM fields into a GEDCOM fact/event string.
459c2ed51d1SGreg Roach     *
460c2ed51d1SGreg Roach     * @param string        $record_type
461c2ed51d1SGreg Roach     * @param array<string> $levels
462c2ed51d1SGreg Roach     * @param array<string> $tags
463c2ed51d1SGreg Roach     * @param array<string> $values
464c2ed51d1SGreg Roach     *
465c2ed51d1SGreg Roach     * @return string
466c2ed51d1SGreg Roach     */
467c2ed51d1SGreg Roach    public function editLinesToGedcom(string $record_type, array $levels, array $tags, array $values): string
468c2ed51d1SGreg Roach    {
469c2ed51d1SGreg Roach        // Assert all arrays are the same size.
470c2ed51d1SGreg Roach        $count = count($levels);
471c2ed51d1SGreg Roach        assert($count > 0);
472c2ed51d1SGreg Roach        assert(count($tags) === $count);
473c2ed51d1SGreg Roach        assert(count($values) === $count);
474c2ed51d1SGreg Roach
475c2ed51d1SGreg Roach        $gedcom_lines = [];
476c2ed51d1SGreg Roach        $hierarchy    = [$record_type];
477c2ed51d1SGreg Roach
478c2ed51d1SGreg Roach        for ($i = 0; $i < $count; $i++) {
479c2ed51d1SGreg Roach            $hierarchy[$levels[$i]] = $tags[$i];
480c2ed51d1SGreg Roach
481c2ed51d1SGreg Roach            $full_tag   = implode(':', array_slice($hierarchy, 0, 1 + (int) $levels[$i]));
482c2ed51d1SGreg Roach            $element    = Registry::elementFactory()->make($full_tag);
483c2ed51d1SGreg Roach            $values[$i] = $element->canonical($values[$i]);
484c2ed51d1SGreg Roach
485c2ed51d1SGreg Roach            // If "1 FACT Y" has a DATE or PLAC, then delete the value of Y
486c2ed51d1SGreg Roach            if ($levels[$i] === '1' && $values[$i] === 'Y') {
487c2ed51d1SGreg Roach                for ($j = $i + 1; $j < $count && $levels[$j] > $levels[$i]; ++$j) {
488c2ed51d1SGreg Roach                    if ($levels[$j] === '2' && ($tags[$j] === 'DATE' || $tags[$j] === 'PLAC') && $values[$j] !== '') {
489c2ed51d1SGreg Roach                        $values[$i] = '';
490c2ed51d1SGreg Roach                        break;
491c2ed51d1SGreg Roach                    }
492c2ed51d1SGreg Roach                }
493c2ed51d1SGreg Roach            }
494c2ed51d1SGreg Roach
495c2ed51d1SGreg Roach            // Include this line if there is a value - or if there is a child record with a value.
496c2ed51d1SGreg Roach            $include = $values[$i] !== '';
497c2ed51d1SGreg Roach
498c2ed51d1SGreg Roach            for ($j = $i + 1; !$include && $j < $count && $levels[$j] > $levels[$i]; $j++) {
499c2ed51d1SGreg Roach                $include = $values[$j] !== '';
500c2ed51d1SGreg Roach            }
501c2ed51d1SGreg Roach
502c2ed51d1SGreg Roach            if ($include) {
503c2ed51d1SGreg Roach                if ($values[$i] === '') {
504c2ed51d1SGreg Roach                    $gedcom_lines[] = $levels[$i] . ' ' . $tags[$i];
505c2ed51d1SGreg Roach                } else {
506f7c88e25SGreg Roach                    if ($tags[$i] === 'CONC') {
507f7c88e25SGreg Roach                        $next_level = (int) $levels[$i];
508f7c88e25SGreg Roach                    } else {
509c2ed51d1SGreg Roach                        $next_level = 1 + (int) $levels[$i];
510f7c88e25SGreg Roach                    }
511c2ed51d1SGreg Roach
512c2ed51d1SGreg Roach                    $gedcom_lines[] = $levels[$i] . ' ' . $tags[$i] . ' ' . str_replace("\n", "\n" . $next_level . ' CONT ', $values[$i]);
513c2ed51d1SGreg Roach                }
514c2ed51d1SGreg Roach            }
515c2ed51d1SGreg Roach        }
516c2ed51d1SGreg Roach
517c2ed51d1SGreg Roach        return implode("\n", $gedcom_lines);
518c2ed51d1SGreg Roach    }
519e22e42f7SGreg Roach
520e22e42f7SGreg Roach    /**
521e22e42f7SGreg Roach     * Add blank lines, to allow a user to add/edit new values.
522e22e42f7SGreg Roach     *
523e22e42f7SGreg Roach     * @param Fact $fact
524e22e42f7SGreg Roach     * @param bool $include_hidden
525e22e42f7SGreg Roach     *
526e22e42f7SGreg Roach     * @return string
527e22e42f7SGreg Roach     */
528e22e42f7SGreg Roach    public function insertMissingSubtags(Fact $fact, bool $include_hidden): string
529e22e42f7SGreg Roach    {
530f8505e4aSGreg Roach        return $this->insertMissingLevels($fact->record()->tree(), $fact->tag(), $fact->gedcom(), $include_hidden);
531f8505e4aSGreg Roach    }
532f8505e4aSGreg Roach
533f8505e4aSGreg Roach    /**
534f8505e4aSGreg Roach     * Add blank lines, to allow a user to add/edit new values.
535f8505e4aSGreg Roach     *
536f8505e4aSGreg Roach     * @param GedcomRecord $record
537f8505e4aSGreg Roach     * @param bool         $include_hidden
538f8505e4aSGreg Roach     *
539f8505e4aSGreg Roach     * @return string
540f8505e4aSGreg Roach     */
541f8505e4aSGreg Roach    public function insertMissingRecordSubtags(GedcomRecord $record, bool $include_hidden): string
542f8505e4aSGreg Roach    {
543f8505e4aSGreg Roach        $gedcom = $this->insertMissingLevels($record->tree(), $record->tag(), $record->gedcom(), $include_hidden);
544f8505e4aSGreg Roach
545f8505e4aSGreg Roach        // NOTE records have data at level 0.  Move it to 1 CONC.
546f8505e4aSGreg Roach        if ($record->tag() === 'NOTE') {
547f8505e4aSGreg Roach            return preg_replace('/^0 @[^@]+@ NOTE/', '1 CONC', $gedcom);
548f8505e4aSGreg Roach        }
549f8505e4aSGreg Roach
550f8505e4aSGreg Roach        return preg_replace('/^0.*\n/', '', $gedcom);
551f8505e4aSGreg Roach    }
552f8505e4aSGreg Roach
553f8505e4aSGreg Roach    /**
554f8505e4aSGreg Roach     * @param Tree   $tree
555f8505e4aSGreg Roach     * @param string $tag
556f8505e4aSGreg Roach     * @param string $gedcom
557f8505e4aSGreg Roach     * @param bool   $include_hidden
558f8505e4aSGreg Roach     *
559f8505e4aSGreg Roach     * @return string
560f8505e4aSGreg Roach     */
561f8505e4aSGreg Roach    protected function insertMissingLevels(Tree $tree, string $tag, string $gedcom, bool $include_hidden): string
562f8505e4aSGreg Roach    {
563f8505e4aSGreg Roach        $next_level = substr_count($tag, ':') + 1;
564f8505e4aSGreg Roach        $factory    = Registry::elementFactory();
565f8505e4aSGreg Roach        $subtags    = $factory->make($tag)->subtags();
566f8505e4aSGreg Roach
567f8505e4aSGreg Roach        // Merge CONT records onto their parent line.
568f8505e4aSGreg Roach        $gedcom = strtr($gedcom, [
569f8505e4aSGreg Roach            "\n" . $next_level . ' CONT ' => "\r",
570f8505e4aSGreg Roach            "\n" . $next_level . ' CONT' => "\r",
571f8505e4aSGreg Roach        ]);
572f8505e4aSGreg Roach
573f8505e4aSGreg Roach        // The first part is level N.  The remainder are level N+1.
574f8505e4aSGreg Roach        $parts  = preg_split('/\n(?=' . $next_level . ')/', $gedcom);
575f8505e4aSGreg Roach        $return = array_shift($parts);
576f8505e4aSGreg Roach
577f8505e4aSGreg Roach        foreach ($subtags as $subtag => $occurrences) {
578f8505e4aSGreg Roach            if (!$include_hidden && $this->isHiddenTag($tag . ':' . $subtag)) {
579f8505e4aSGreg Roach                continue;
580f8505e4aSGreg Roach            }
581f8505e4aSGreg Roach
582f8505e4aSGreg Roach            [$min, $max] = explode(':', $occurrences);
583f8505e4aSGreg Roach
584f8505e4aSGreg Roach            $min = (int) $min;
585f8505e4aSGreg Roach
586f8505e4aSGreg Roach            if ($max === 'M') {
587f8505e4aSGreg Roach                $max = PHP_INT_MAX;
588f8505e4aSGreg Roach            } else {
589f8505e4aSGreg Roach                $max = (int) $max;
590f8505e4aSGreg Roach            }
591f8505e4aSGreg Roach
592f8505e4aSGreg Roach            $count = 0;
593f8505e4aSGreg Roach
594f8505e4aSGreg Roach            // Add expected subtags in our preferred order.
595f8505e4aSGreg Roach            foreach ($parts as $n => $part) {
596f8505e4aSGreg Roach                if (str_starts_with($part, $next_level . ' ' . $subtag)) {
597f8505e4aSGreg Roach                    $return .= "\n" . $this->insertMissingLevels($tree, $tag . ':' . $subtag, $part, $include_hidden);
598f8505e4aSGreg Roach                    $count++;
599f8505e4aSGreg Roach                    unset($parts[$n]);
600f8505e4aSGreg Roach                }
601f8505e4aSGreg Roach            }
602f8505e4aSGreg Roach
603f8505e4aSGreg Roach            // Allowed to have more of this subtag?
604f8505e4aSGreg Roach            if ($count < $max) {
605f8505e4aSGreg Roach                // Create a new one.
606f8505e4aSGreg Roach                $gedcom  = $next_level . ' ' . $subtag;
607f8505e4aSGreg Roach                $default = $factory->make($tag . ':' . $subtag)->default($tree);
608f8505e4aSGreg Roach                if ($default !== '') {
609f8505e4aSGreg Roach                    $gedcom .= ' ' . $default;
610f8505e4aSGreg Roach                }
611f8505e4aSGreg Roach
612f8505e4aSGreg Roach                $number_to_add = max(1, $min - $count);
613f8505e4aSGreg Roach                $gedcom_to_add = "\n" . $this->insertMissingLevels($tree, $tag . ':' . $subtag, $gedcom, $include_hidden);
614f8505e4aSGreg Roach
615f8505e4aSGreg Roach                $return .= str_repeat($gedcom_to_add, $number_to_add);
616f8505e4aSGreg Roach            }
617f8505e4aSGreg Roach        }
618f8505e4aSGreg Roach
619f8505e4aSGreg Roach        // Now add any unexpected/existing data.
620f8505e4aSGreg Roach        if ($parts !== []) {
621f8505e4aSGreg Roach            $return .= "\n" . implode("\n", $parts);
622f8505e4aSGreg Roach        }
623f8505e4aSGreg Roach
624f8505e4aSGreg Roach        return $return;
625f8505e4aSGreg Roach    }
626f8505e4aSGreg Roach
627f8505e4aSGreg Roach    /**
628f8505e4aSGreg Roach     * List of tags to exclude when creating new data.
629f8505e4aSGreg Roach     *
630f8505e4aSGreg Roach     * @param string $tag
631f8505e4aSGreg Roach     *
632f8505e4aSGreg Roach     * @return bool
633f8505e4aSGreg Roach     */
634f8505e4aSGreg Roach    private function isHiddenTag(string $tag): bool
635f8505e4aSGreg Roach    {
636*a9da2374SGreg Roach        // Function to filter hidden tags.
637*a9da2374SGreg Roach        $fn_hide = fn (string $x): bool => (bool) Site::getPreference('HIDE_' . $x);
638*a9da2374SGreg Roach
639*a9da2374SGreg Roach        $preferences = array_filter(Gedcom::HIDDEN_TAGS, $fn_hide, ARRAY_FILTER_USE_KEY);
640f8505e4aSGreg Roach        $preferences = array_values($preferences);
641f8505e4aSGreg Roach        $hidden_tags = array_merge(...$preferences);
642f8505e4aSGreg Roach
643f8505e4aSGreg Roach        foreach ($hidden_tags as $hidden_tag) {
644f8505e4aSGreg Roach            if (str_contains($tag, $hidden_tag)) {
645f8505e4aSGreg Roach                return true;
646f8505e4aSGreg Roach            }
647f8505e4aSGreg Roach        }
648f8505e4aSGreg Roach
649f8505e4aSGreg Roach        return false;
650e22e42f7SGreg Roach    }
6517c7d1e03SGreg Roach}
652