xref: /webtrees/app/Services/GedcomEditService.php (revision 09482a558a7989d76059e7f9911605cf836b77ba)
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;
23abdaad0dSGreg Roachuse Fisharebest\Webtrees\Family;
247c7d1e03SGreg Roachuse Fisharebest\Webtrees\Gedcom;
25f8505e4aSGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
26abdaad0dSGreg Roachuse Fisharebest\Webtrees\Individual;
27abdaad0dSGreg Roachuse Fisharebest\Webtrees\Note;
28c2ed51d1SGreg Roachuse Fisharebest\Webtrees\Registry;
29f8505e4aSGreg Roachuse Fisharebest\Webtrees\Site;
307c7d1e03SGreg Roachuse Fisharebest\Webtrees\Tree;
317c7d1e03SGreg Roach
32f8505e4aSGreg Roachuse function array_filter;
337c7d1e03SGreg Roachuse function array_merge;
34f8505e4aSGreg Roachuse function array_shift;
35f8505e4aSGreg Roachuse function array_values;
367c7d1e03SGreg Roachuse function assert;
377c7d1e03SGreg Roachuse function count;
38f8505e4aSGreg Roachuse function explode;
39f8505e4aSGreg Roachuse function implode;
40f8505e4aSGreg Roachuse function max;
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
489c7bc1e3SGreg Roachuse const ARRAY_FILTER_USE_BOTH;
49f8505e4aSGreg Roachuse const ARRAY_FILTER_USE_KEY;
50f8505e4aSGreg Roachuse const PHP_INT_MAX;
51f8505e4aSGreg Roach
527c7d1e03SGreg Roach/**
537c7d1e03SGreg Roach * Utilities to edit/save GEDCOM data.
547c7d1e03SGreg Roach */
557c7d1e03SGreg Roachclass GedcomEditService
567c7d1e03SGreg Roach{
57*09482a55SGreg Roach    /** @var array<string> */
58*09482a55SGreg Roach    public array $glevels = [];
597c7d1e03SGreg Roach
60*09482a55SGreg Roach    /** @var array<string> */
61*09482a55SGreg Roach    public array $tag = [];
627c7d1e03SGreg Roach
63*09482a55SGreg Roach    /** @var array<string> */
64*09482a55SGreg Roach    public array $islink = [];
657c7d1e03SGreg Roach
66*09482a55SGreg Roach    /** @var array<string> */
67*09482a55SGreg Roach    public array $text = [];
687c7d1e03SGreg Roach
69*09482a55SGreg Roach    /** @var array<string> */
70*09482a55SGreg Roach    protected array $glevelsSOUR = [];
717c7d1e03SGreg Roach
72*09482a55SGreg Roach    /** @var array<string> */
73*09482a55SGreg Roach    protected array $tagSOUR = [];
747c7d1e03SGreg Roach
75*09482a55SGreg Roach    /** @var array<string> */
76*09482a55SGreg Roach    protected array $islinkSOUR = [];
777c7d1e03SGreg Roach
78*09482a55SGreg Roach    /** @var array<string> */
79*09482a55SGreg Roach    protected array $textSOUR = [];
807c7d1e03SGreg Roach
81*09482a55SGreg Roach    /** @var array<string> */
82*09482a55SGreg Roach    protected array $glevelsRest = [];
837c7d1e03SGreg Roach
84*09482a55SGreg Roach    /** @var array<string> */
85*09482a55SGreg Roach    protected array $tagRest = [];
867c7d1e03SGreg Roach
87*09482a55SGreg Roach    /** @var array<string> */
88*09482a55SGreg Roach    protected array $islinkRest = [];
897c7d1e03SGreg Roach
90*09482a55SGreg Roach    /** @var array<string> */
91*09482a55SGreg Roach    protected array $textRest = [];
927c7d1e03SGreg Roach
937c7d1e03SGreg Roach    /**
947c7d1e03SGreg Roach     * This function splits the $glevels, $tag, $islink, and $text arrays so that the
957c7d1e03SGreg Roach     * entries associated with a SOUR record are separate from everything else.
967c7d1e03SGreg Roach     *
977c7d1e03SGreg Roach     * Input arrays:
987c7d1e03SGreg Roach     * - $glevels[] - an array of the gedcom level for each line that was edited
997c7d1e03SGreg Roach     * - $tag[] - an array of the tags for each gedcom line that was edited
1007c7d1e03SGreg Roach     * - $islink[] - an array of 1 or 0 values to indicate when the text is a link element
1017c7d1e03SGreg Roach     * - $text[] - an array of the text data for each line
1027c7d1e03SGreg Roach     *
1037c7d1e03SGreg Roach     * Output arrays:
1047c7d1e03SGreg Roach     * ** For the SOUR record:
1057c7d1e03SGreg Roach     * - $glevelsSOUR[] - an array of the gedcom level for each line that was edited
1067c7d1e03SGreg Roach     * - $tagSOUR[] - an array of the tags for each gedcom line that was edited
1077c7d1e03SGreg Roach     * - $islinkSOUR[] - an array of 1 or 0 values to indicate when the text is a link element
1087c7d1e03SGreg Roach     * - $textSOUR[] - an array of the text data for each line
1097c7d1e03SGreg Roach     * ** For the remaining records:
1107c7d1e03SGreg Roach     * - $glevelsRest[] - an array of the gedcom level for each line that was edited
1117c7d1e03SGreg Roach     * - $tagRest[] - an array of the tags for each gedcom line that was edited
1127c7d1e03SGreg Roach     * - $islinkRest[] - an array of 1 or 0 values to indicate when the text is a link element
1137c7d1e03SGreg Roach     * - $textRest[] - an array of the text data for each line
1147c7d1e03SGreg Roach     *
1157c7d1e03SGreg Roach     * @return void
1167c7d1e03SGreg Roach     */
1177c7d1e03SGreg Roach    public function splitSource(): void
1187c7d1e03SGreg Roach    {
1197c7d1e03SGreg Roach        $this->glevelsSOUR = [];
1207c7d1e03SGreg Roach        $this->tagSOUR     = [];
1217c7d1e03SGreg Roach        $this->islinkSOUR  = [];
1227c7d1e03SGreg Roach        $this->textSOUR    = [];
1237c7d1e03SGreg Roach
1247c7d1e03SGreg Roach        $this->glevelsRest = [];
1257c7d1e03SGreg Roach        $this->tagRest     = [];
1267c7d1e03SGreg Roach        $this->islinkRest  = [];
1277c7d1e03SGreg Roach        $this->textRest    = [];
1287c7d1e03SGreg Roach
1297c7d1e03SGreg Roach        $inSOUR    = false;
1307c7d1e03SGreg Roach        $levelSOUR = 0;
1317c7d1e03SGreg Roach
1327c7d1e03SGreg Roach        // Assume all arrays are the same size.
1337c7d1e03SGreg Roach        $count = count($this->glevels);
1347c7d1e03SGreg Roach
1357c7d1e03SGreg Roach        for ($i = 0; $i < $count; $i++) {
1367c7d1e03SGreg Roach            if ($inSOUR) {
1377c7d1e03SGreg Roach                if ($levelSOUR < $this->glevels[$i]) {
1387c7d1e03SGreg Roach                    $dest = 'S';
1397c7d1e03SGreg Roach                } else {
1407c7d1e03SGreg Roach                    $inSOUR = false;
1417c7d1e03SGreg Roach                    $dest   = 'R';
1427c7d1e03SGreg Roach                }
1437c7d1e03SGreg Roach            } elseif ($this->tag[$i] === 'SOUR') {
1447c7d1e03SGreg Roach                $inSOUR    = true;
1457c7d1e03SGreg Roach                $levelSOUR = $this->glevels[$i];
1467c7d1e03SGreg Roach                $dest      = 'S';
1477c7d1e03SGreg Roach            } else {
1487c7d1e03SGreg Roach                $dest = 'R';
1497c7d1e03SGreg Roach            }
1507c7d1e03SGreg Roach
1517c7d1e03SGreg Roach            if ($dest === 'S') {
1527c7d1e03SGreg Roach                $this->glevelsSOUR[] = $this->glevels[$i];
1537c7d1e03SGreg Roach                $this->tagSOUR[]     = $this->tag[$i];
1547c7d1e03SGreg Roach                $this->islinkSOUR[]  = $this->islink[$i];
1557c7d1e03SGreg Roach                $this->textSOUR[]    = $this->text[$i];
1567c7d1e03SGreg Roach            } else {
1577c7d1e03SGreg Roach                $this->glevelsRest[] = $this->glevels[$i];
1587c7d1e03SGreg Roach                $this->tagRest[]     = $this->tag[$i];
1597c7d1e03SGreg Roach                $this->islinkRest[]  = $this->islink[$i];
1607c7d1e03SGreg Roach                $this->textRest[]    = $this->text[$i];
1617c7d1e03SGreg Roach            }
1627c7d1e03SGreg Roach        }
1637c7d1e03SGreg Roach    }
1647c7d1e03SGreg Roach
1657c7d1e03SGreg Roach    /**
1667c7d1e03SGreg Roach     * Add new GEDCOM lines from the $xxxRest interface update arrays, which
1677c7d1e03SGreg Roach     * were produced by the splitSOUR() function.
1687c7d1e03SGreg Roach     * See the FunctionsEdit::handle_updatesges() function for details.
1697c7d1e03SGreg Roach     *
1707c7d1e03SGreg Roach     * @param string $inputRec
1717c7d1e03SGreg Roach     *
1727c7d1e03SGreg Roach     * @return string
1737c7d1e03SGreg Roach     */
1747c7d1e03SGreg Roach    public function updateRest(string $inputRec): string
1757c7d1e03SGreg Roach    {
1767c7d1e03SGreg Roach        if (count($this->tagRest) === 0) {
1777c7d1e03SGreg Roach            return $inputRec; // No update required
1787c7d1e03SGreg Roach        }
1797c7d1e03SGreg Roach
1807c7d1e03SGreg Roach        // Save original interface update arrays before replacing them with the xxxRest ones
1817c7d1e03SGreg Roach        $glevelsSave = $this->glevels;
1827c7d1e03SGreg Roach        $tagSave     = $this->tag;
1837c7d1e03SGreg Roach        $islinkSave  = $this->islink;
1847c7d1e03SGreg Roach        $textSave    = $this->text;
1857c7d1e03SGreg Roach
1867c7d1e03SGreg Roach        $this->glevels = $this->glevelsRest;
1877c7d1e03SGreg Roach        $this->tag     = $this->tagRest;
1887c7d1e03SGreg Roach        $this->islink  = $this->islinkRest;
1897c7d1e03SGreg Roach        $this->text    = $this->textRest;
1907c7d1e03SGreg Roach
1917c7d1e03SGreg Roach        $myRecord = $this->handleUpdates($inputRec, 'no'); // Now do the update
1927c7d1e03SGreg Roach
1937c7d1e03SGreg Roach        // Restore the original interface update arrays (just in case ...)
1947c7d1e03SGreg Roach        $this->glevels = $glevelsSave;
1957c7d1e03SGreg Roach        $this->tag     = $tagSave;
1967c7d1e03SGreg Roach        $this->islink  = $islinkSave;
1977c7d1e03SGreg Roach        $this->text    = $textSave;
1987c7d1e03SGreg Roach
1997c7d1e03SGreg Roach        return $myRecord;
2007c7d1e03SGreg Roach    }
2017c7d1e03SGreg Roach
2027c7d1e03SGreg Roach    /**
2037c7d1e03SGreg Roach     * Add new gedcom lines from interface update arrays
2047c7d1e03SGreg Roach     * The edit_interface and FunctionsEdit::add_simple_tag function produce the following
2057c7d1e03SGreg Roach     * arrays incoming from the $_POST form
2067c7d1e03SGreg Roach     * - $glevels[] - an array of the gedcom level for each line that was edited
2077c7d1e03SGreg Roach     * - $tag[] - an array of the tags for each gedcom line that was edited
2087c7d1e03SGreg Roach     * - $islink[] - an array of 1 or 0 values to tell whether the text is a link element and should be surrounded by @@
2097c7d1e03SGreg Roach     * - $text[] - an array of the text data for each line
2107c7d1e03SGreg Roach     * With these arrays you can recreate the gedcom lines like this
2117c7d1e03SGreg Roach     * <code>$glevel[0].' '.$tag[0].' '.$text[0]</code>
2127c7d1e03SGreg Roach     * There will be an index in each of these arrays for each line of the gedcom
2137c7d1e03SGreg Roach     * fact that is being edited.
2147c7d1e03SGreg Roach     * If the $text[] array is empty for the given line, then it means that the
2157c7d1e03SGreg Roach     * user removed that line during editing or that the line is supposed to be
2167c7d1e03SGreg Roach     * empty (1 DEAT, 1 BIRT) for example. To know if the line should be removed
2177c7d1e03SGreg Roach     * there is a section of code that looks ahead to the next lines to see if there
2187c7d1e03SGreg Roach     * are sub lines. For example we don't want to remove the 1 DEAT line if it has
2197c7d1e03SGreg Roach     * a 2 PLAC or 2 DATE line following it. If there are no sub lines, then the line
2207c7d1e03SGreg Roach     * can be safely removed.
2217c7d1e03SGreg Roach     *
2227c7d1e03SGreg Roach     * @param string $newged        the new gedcom record to add the lines to
2237c7d1e03SGreg Roach     * @param string $levelOverride Override GEDCOM level specified in $glevels[0]
2247c7d1e03SGreg Roach     *
2257c7d1e03SGreg Roach     * @return string The updated gedcom record
2267c7d1e03SGreg Roach     */
227c8183f29SGreg Roach    public function handleUpdates(string $newged, string $levelOverride = 'no'): string
2287c7d1e03SGreg Roach    {
2297c7d1e03SGreg Roach        if ($levelOverride === 'no') {
2307c7d1e03SGreg Roach            $levelAdjust = 0;
2317c7d1e03SGreg Roach        } else {
2327c7d1e03SGreg Roach            $levelAdjust = 1;
2337c7d1e03SGreg Roach        }
2347c7d1e03SGreg Roach
2357c7d1e03SGreg Roach        // Assert all arrays are the same size.
2367c7d1e03SGreg Roach        assert(count($this->glevels) === count($this->tag));
2377c7d1e03SGreg Roach        assert(count($this->glevels) === count($this->text));
2387c7d1e03SGreg Roach        assert(count($this->glevels) === count($this->islink));
2397c7d1e03SGreg Roach
2407c7d1e03SGreg Roach        $count = count($this->glevels);
2417c7d1e03SGreg Roach
2427c7d1e03SGreg Roach        for ($j = 0; $j < $count; $j++) {
2437c7d1e03SGreg Roach            // Look for empty SOUR reference with non-empty sub-records.
2447c7d1e03SGreg Roach            // This can happen when the SOUR entry is deleted but its sub-records
2457c7d1e03SGreg Roach            // were incorrectly left intact.
2467c7d1e03SGreg Roach            // The sub-records should be deleted.
2477c7d1e03SGreg Roach            if ($this->tag[$j] === 'SOUR' && ($this->text[$j] === '@@' || $this->text[$j] === '')) {
2487c7d1e03SGreg Roach                $this->text[$j] = '';
2497c7d1e03SGreg Roach                $k              = $j + 1;
2507c7d1e03SGreg Roach                while ($k < $count && $this->glevels[$k] > $this->glevels[$j]) {
2517c7d1e03SGreg Roach                    $this->text[$k] = '';
2527c7d1e03SGreg Roach                    $k++;
2537c7d1e03SGreg Roach                }
2547c7d1e03SGreg Roach            }
2557c7d1e03SGreg Roach
2567c7d1e03SGreg Roach            if (trim($this->text[$j]) !== '') {
2577c7d1e03SGreg Roach                $pass = true;
2587c7d1e03SGreg Roach            } else {
2597c7d1e03SGreg Roach                //-- for facts with empty values they must have sub records
2607c7d1e03SGreg Roach                //-- this section checks if they have subrecords
2617c7d1e03SGreg Roach                $k    = $j + 1;
2627c7d1e03SGreg Roach                $pass = false;
2637c7d1e03SGreg Roach                while ($k < $count && $this->glevels[$k] > $this->glevels[$j]) {
2647c7d1e03SGreg Roach                    if ($this->text[$k] !== '') {
265c2ed51d1SGreg Roach                        if ($this->tag[$j] !== 'OBJE' || $this->tag[$k] === 'FILE') {
2667c7d1e03SGreg Roach                            $pass = true;
2677c7d1e03SGreg Roach                            break;
2687c7d1e03SGreg Roach                        }
2697c7d1e03SGreg Roach                    }
2707c7d1e03SGreg Roach                    $k++;
2717c7d1e03SGreg Roach                }
2727c7d1e03SGreg Roach            }
2737c7d1e03SGreg Roach
2747c7d1e03SGreg Roach            //-- if the value is not empty or it has sub lines
2757c7d1e03SGreg Roach            //--- then write the line to the gedcom record
2767c7d1e03SGreg Roach            //-- we have to let some emtpy text lines pass through... (DEAT, BIRT, etc)
2777c7d1e03SGreg Roach            if ($pass) {
2787c7d1e03SGreg Roach                $newline = (int) $this->glevels[$j] + $levelAdjust . ' ' . $this->tag[$j];
2797c7d1e03SGreg Roach                if ($this->text[$j] !== '') {
2807c7d1e03SGreg Roach                    if ($this->islink[$j]) {
28165de9aa7SGreg Roach                        $newline .= ' @' . trim($this->text[$j], '@') . '@';
2827c7d1e03SGreg Roach                    } else {
2837c7d1e03SGreg Roach                        $newline .= ' ' . $this->text[$j];
2847c7d1e03SGreg Roach                    }
2857c7d1e03SGreg Roach                }
2867c7d1e03SGreg Roach                $next_level = 1 + (int) $this->glevels[$j] + $levelAdjust;
2877c7d1e03SGreg Roach
2887c7d1e03SGreg Roach                $newged .= "\n" . str_replace("\n", "\n" . $next_level . ' CONT ', $newline);
2897c7d1e03SGreg Roach            }
2907c7d1e03SGreg Roach        }
2917c7d1e03SGreg Roach
2927c7d1e03SGreg Roach        return $newged;
2937c7d1e03SGreg Roach    }
2947c7d1e03SGreg Roach
2957c7d1e03SGreg Roach    /**
2967c7d1e03SGreg Roach     * Add new GEDCOM lines from the $xxxSOUR interface update arrays, which
2977c7d1e03SGreg Roach     * were produced by the splitSOUR() function.
2987c7d1e03SGreg Roach     * See the FunctionsEdit::handle_updatesges() function for details.
2997c7d1e03SGreg Roach     *
3007c7d1e03SGreg Roach     * @param string $inputRec
3017c7d1e03SGreg Roach     * @param string $levelOverride
3027c7d1e03SGreg Roach     *
3037c7d1e03SGreg Roach     * @return string
3047c7d1e03SGreg Roach     */
3057c7d1e03SGreg Roach    public function updateSource(string $inputRec, string $levelOverride = 'no'): string
3067c7d1e03SGreg Roach    {
3077c7d1e03SGreg Roach        if (count($this->tagSOUR) === 0) {
3087c7d1e03SGreg Roach            return $inputRec; // No update required
3097c7d1e03SGreg Roach        }
3107c7d1e03SGreg Roach
3117c7d1e03SGreg Roach        // Save original interface update arrays before replacing them with the xxxSOUR ones
3127c7d1e03SGreg Roach        $glevelsSave = $this->glevels;
3137c7d1e03SGreg Roach        $tagSave     = $this->tag;
3147c7d1e03SGreg Roach        $islinkSave  = $this->islink;
3157c7d1e03SGreg Roach        $textSave    = $this->text;
3167c7d1e03SGreg Roach
3177c7d1e03SGreg Roach        $this->glevels = $this->glevelsSOUR;
3187c7d1e03SGreg Roach        $this->tag     = $this->tagSOUR;
3197c7d1e03SGreg Roach        $this->islink  = $this->islinkSOUR;
3207c7d1e03SGreg Roach        $this->text    = $this->textSOUR;
3217c7d1e03SGreg Roach
3227c7d1e03SGreg Roach        $myRecord = $this->handleUpdates($inputRec, $levelOverride); // Now do the update
3237c7d1e03SGreg Roach
3247c7d1e03SGreg Roach        // Restore the original interface update arrays (just in case ...)
3257c7d1e03SGreg Roach        $this->glevels = $glevelsSave;
3267c7d1e03SGreg Roach        $this->tag     = $tagSave;
3277c7d1e03SGreg Roach        $this->islink  = $islinkSave;
3287c7d1e03SGreg Roach        $this->text    = $textSave;
3297c7d1e03SGreg Roach
3307c7d1e03SGreg Roach        return $myRecord;
3317c7d1e03SGreg Roach    }
3327c7d1e03SGreg Roach
3337c7d1e03SGreg Roach    /**
334c2ed51d1SGreg Roach     * Reassemble edited GEDCOM fields into a GEDCOM fact/event string.
335c2ed51d1SGreg Roach     *
336c2ed51d1SGreg Roach     * @param string        $record_type
337c2ed51d1SGreg Roach     * @param array<string> $levels
338c2ed51d1SGreg Roach     * @param array<string> $tags
339c2ed51d1SGreg Roach     * @param array<string> $values
340c2ed51d1SGreg Roach     *
341c2ed51d1SGreg Roach     * @return string
342c2ed51d1SGreg Roach     */
343c2ed51d1SGreg Roach    public function editLinesToGedcom(string $record_type, array $levels, array $tags, array $values): string
344c2ed51d1SGreg Roach    {
345c2ed51d1SGreg Roach        // Assert all arrays are the same size.
346c2ed51d1SGreg Roach        $count = count($levels);
347c2ed51d1SGreg Roach        assert($count > 0);
348c2ed51d1SGreg Roach        assert(count($tags) === $count);
349c2ed51d1SGreg Roach        assert(count($values) === $count);
350c2ed51d1SGreg Roach
351c2ed51d1SGreg Roach        $gedcom_lines = [];
352c2ed51d1SGreg Roach        $hierarchy    = [$record_type];
353c2ed51d1SGreg Roach
354c2ed51d1SGreg Roach        for ($i = 0; $i < $count; $i++) {
355c2ed51d1SGreg Roach            $hierarchy[$levels[$i]] = $tags[$i];
356c2ed51d1SGreg Roach
357c2ed51d1SGreg Roach            $full_tag   = implode(':', array_slice($hierarchy, 0, 1 + (int) $levels[$i]));
358c2ed51d1SGreg Roach            $element    = Registry::elementFactory()->make($full_tag);
359c2ed51d1SGreg Roach            $values[$i] = $element->canonical($values[$i]);
360c2ed51d1SGreg Roach
361c2ed51d1SGreg Roach            // If "1 FACT Y" has a DATE or PLAC, then delete the value of Y
362c2ed51d1SGreg Roach            if ($levels[$i] === '1' && $values[$i] === 'Y') {
363c2ed51d1SGreg Roach                for ($j = $i + 1; $j < $count && $levels[$j] > $levels[$i]; ++$j) {
364c2ed51d1SGreg Roach                    if ($levels[$j] === '2' && ($tags[$j] === 'DATE' || $tags[$j] === 'PLAC') && $values[$j] !== '') {
365c2ed51d1SGreg Roach                        $values[$i] = '';
366c2ed51d1SGreg Roach                        break;
367c2ed51d1SGreg Roach                    }
368c2ed51d1SGreg Roach                }
369c2ed51d1SGreg Roach            }
370c2ed51d1SGreg Roach
371c2ed51d1SGreg Roach            // Include this line if there is a value - or if there is a child record with a value.
372c2ed51d1SGreg Roach            $include = $values[$i] !== '';
373c2ed51d1SGreg Roach
374c2ed51d1SGreg Roach            for ($j = $i + 1; !$include && $j < $count && $levels[$j] > $levels[$i]; $j++) {
375c2ed51d1SGreg Roach                $include = $values[$j] !== '';
376c2ed51d1SGreg Roach            }
377c2ed51d1SGreg Roach
378c2ed51d1SGreg Roach            if ($include) {
379c2ed51d1SGreg Roach                if ($values[$i] === '') {
380c2ed51d1SGreg Roach                    $gedcom_lines[] = $levels[$i] . ' ' . $tags[$i];
381c2ed51d1SGreg Roach                } else {
382f7c88e25SGreg Roach                    if ($tags[$i] === 'CONC') {
383f7c88e25SGreg Roach                        $next_level = (int) $levels[$i];
384f7c88e25SGreg Roach                    } else {
385c2ed51d1SGreg Roach                        $next_level = 1 + (int) $levels[$i];
386f7c88e25SGreg Roach                    }
387c2ed51d1SGreg Roach
388c2ed51d1SGreg Roach                    $gedcom_lines[] = $levels[$i] . ' ' . $tags[$i] . ' ' . str_replace("\n", "\n" . $next_level . ' CONT ', $values[$i]);
389c2ed51d1SGreg Roach                }
390c2ed51d1SGreg Roach            }
391c2ed51d1SGreg Roach        }
392c2ed51d1SGreg Roach
393c2ed51d1SGreg Roach        return implode("\n", $gedcom_lines);
394c2ed51d1SGreg Roach    }
395e22e42f7SGreg Roach
396e22e42f7SGreg Roach    /**
397e22e42f7SGreg Roach     * Add blank lines, to allow a user to add/edit new values.
398e22e42f7SGreg Roach     *
399e22e42f7SGreg Roach     * @param Fact $fact
400e22e42f7SGreg Roach     * @param bool $include_hidden
401e22e42f7SGreg Roach     *
402e22e42f7SGreg Roach     * @return string
403e22e42f7SGreg Roach     */
404abdaad0dSGreg Roach    public function insertMissingFactSubtags(Fact $fact, bool $include_hidden): string
405e22e42f7SGreg Roach    {
406f8505e4aSGreg Roach        return $this->insertMissingLevels($fact->record()->tree(), $fact->tag(), $fact->gedcom(), $include_hidden);
407f8505e4aSGreg Roach    }
408f8505e4aSGreg Roach
409f8505e4aSGreg Roach    /**
410f8505e4aSGreg Roach     * Add blank lines, to allow a user to add/edit new values.
411f8505e4aSGreg Roach     *
412f8505e4aSGreg Roach     * @param GedcomRecord $record
413f8505e4aSGreg Roach     * @param bool         $include_hidden
414f8505e4aSGreg Roach     *
415f8505e4aSGreg Roach     * @return string
416f8505e4aSGreg Roach     */
417f8505e4aSGreg Roach    public function insertMissingRecordSubtags(GedcomRecord $record, bool $include_hidden): string
418f8505e4aSGreg Roach    {
419f8505e4aSGreg Roach        $gedcom = $this->insertMissingLevels($record->tree(), $record->tag(), $record->gedcom(), $include_hidden);
420f8505e4aSGreg Roach
421f8505e4aSGreg Roach        // NOTE records have data at level 0.  Move it to 1 CONC.
422abdaad0dSGreg Roach        if ($record instanceof Note) {
423f8505e4aSGreg Roach            return preg_replace('/^0 @[^@]+@ NOTE/', '1 CONC', $gedcom);
424f8505e4aSGreg Roach        }
425f8505e4aSGreg Roach
426f8505e4aSGreg Roach        return preg_replace('/^0.*\n/', '', $gedcom);
427f8505e4aSGreg Roach    }
428f8505e4aSGreg Roach
429f8505e4aSGreg Roach    /**
430abdaad0dSGreg Roach     * List of facts/events to add to families and individuals.
431abdaad0dSGreg Roach     *
432abdaad0dSGreg Roach     * @param Family|Individual $record
433abdaad0dSGreg Roach     * @param bool              $include_hidden
434abdaad0dSGreg Roach     *
435abdaad0dSGreg Roach     * @return array<string>
436abdaad0dSGreg Roach     */
437abdaad0dSGreg Roach    public function factsToAdd(GedcomRecord $record, bool $include_hidden): array
438abdaad0dSGreg Roach    {
439abdaad0dSGreg Roach        $subtags = Registry::elementFactory()->make($record->tag())->subtags();
440abdaad0dSGreg Roach
4419c7bc1e3SGreg Roach        $subtags = array_filter($subtags, fn (string $v, string $k) => !str_ends_with($v, ':1') || $record->facts([$k])->isEmpty(), ARRAY_FILTER_USE_BOTH);
4429c7bc1e3SGreg Roach
4439c7bc1e3SGreg Roach        $subtags = array_keys($subtags);
4449c7bc1e3SGreg Roach
445abdaad0dSGreg Roach        if (!$include_hidden) {
446abdaad0dSGreg Roach            $fn_hidden = fn (string $t): bool => !$this->isHiddenTag($record->tag() . ':' . $t);
447abdaad0dSGreg Roach            $subtags   = array_filter($subtags, $fn_hidden);
448abdaad0dSGreg Roach        }
449abdaad0dSGreg Roach
4509c7bc1e3SGreg Roach        $subtags = array_diff($subtags, ['HUSB', 'WIFE', 'CHIL', 'FAMC', 'FAMS', 'CHAN']);
4519c7bc1e3SGreg Roach
452abdaad0dSGreg Roach        return $subtags;
453abdaad0dSGreg Roach    }
454abdaad0dSGreg Roach
455abdaad0dSGreg Roach    /**
456f8505e4aSGreg Roach     * @param Tree   $tree
457f8505e4aSGreg Roach     * @param string $tag
458f8505e4aSGreg Roach     * @param string $gedcom
459f8505e4aSGreg Roach     * @param bool   $include_hidden
460f8505e4aSGreg Roach     *
461f8505e4aSGreg Roach     * @return string
462f8505e4aSGreg Roach     */
463f8505e4aSGreg Roach    protected function insertMissingLevels(Tree $tree, string $tag, string $gedcom, bool $include_hidden): string
464f8505e4aSGreg Roach    {
465f8505e4aSGreg Roach        $next_level = substr_count($tag, ':') + 1;
466f8505e4aSGreg Roach        $factory    = Registry::elementFactory();
467f8505e4aSGreg Roach        $subtags    = $factory->make($tag)->subtags();
468f8505e4aSGreg Roach
469f8505e4aSGreg Roach        // Merge CONT records onto their parent line.
470f8505e4aSGreg Roach        $gedcom = strtr($gedcom, [
471f8505e4aSGreg Roach            "\n" . $next_level . ' CONT ' => "\r",
472f8505e4aSGreg Roach            "\n" . $next_level . ' CONT' => "\r",
473f8505e4aSGreg Roach        ]);
474f8505e4aSGreg Roach
475f8505e4aSGreg Roach        // The first part is level N.  The remainder are level N+1.
476f8505e4aSGreg Roach        $parts  = preg_split('/\n(?=' . $next_level . ')/', $gedcom);
477f8505e4aSGreg Roach        $return = array_shift($parts);
478f8505e4aSGreg Roach
479f8505e4aSGreg Roach        foreach ($subtags as $subtag => $occurrences) {
480f8505e4aSGreg Roach            if (!$include_hidden && $this->isHiddenTag($tag . ':' . $subtag)) {
481f8505e4aSGreg Roach                continue;
482f8505e4aSGreg Roach            }
483f8505e4aSGreg Roach
484f8505e4aSGreg Roach            [$min, $max] = explode(':', $occurrences);
485f8505e4aSGreg Roach
486f8505e4aSGreg Roach            $min = (int) $min;
487f8505e4aSGreg Roach
488f8505e4aSGreg Roach            if ($max === 'M') {
489f8505e4aSGreg Roach                $max = PHP_INT_MAX;
490f8505e4aSGreg Roach            } else {
491f8505e4aSGreg Roach                $max = (int) $max;
492f8505e4aSGreg Roach            }
493f8505e4aSGreg Roach
494f8505e4aSGreg Roach            $count = 0;
495f8505e4aSGreg Roach
496f8505e4aSGreg Roach            // Add expected subtags in our preferred order.
497f8505e4aSGreg Roach            foreach ($parts as $n => $part) {
498f8505e4aSGreg Roach                if (str_starts_with($part, $next_level . ' ' . $subtag)) {
499f8505e4aSGreg Roach                    $return .= "\n" . $this->insertMissingLevels($tree, $tag . ':' . $subtag, $part, $include_hidden);
500f8505e4aSGreg Roach                    $count++;
501f8505e4aSGreg Roach                    unset($parts[$n]);
502f8505e4aSGreg Roach                }
503f8505e4aSGreg Roach            }
504f8505e4aSGreg Roach
505f8505e4aSGreg Roach            // Allowed to have more of this subtag?
506f8505e4aSGreg Roach            if ($count < $max) {
507f8505e4aSGreg Roach                // Create a new one.
508f8505e4aSGreg Roach                $gedcom  = $next_level . ' ' . $subtag;
509f8505e4aSGreg Roach                $default = $factory->make($tag . ':' . $subtag)->default($tree);
510f8505e4aSGreg Roach                if ($default !== '') {
511f8505e4aSGreg Roach                    $gedcom .= ' ' . $default;
512f8505e4aSGreg Roach                }
513f8505e4aSGreg Roach
514f8505e4aSGreg Roach                $number_to_add = max(1, $min - $count);
515f8505e4aSGreg Roach                $gedcom_to_add = "\n" . $this->insertMissingLevels($tree, $tag . ':' . $subtag, $gedcom, $include_hidden);
516f8505e4aSGreg Roach
517f8505e4aSGreg Roach                $return .= str_repeat($gedcom_to_add, $number_to_add);
518f8505e4aSGreg Roach            }
519f8505e4aSGreg Roach        }
520f8505e4aSGreg Roach
521f8505e4aSGreg Roach        // Now add any unexpected/existing data.
522f8505e4aSGreg Roach        if ($parts !== []) {
523f8505e4aSGreg Roach            $return .= "\n" . implode("\n", $parts);
524f8505e4aSGreg Roach        }
525f8505e4aSGreg Roach
526f8505e4aSGreg Roach        return $return;
527f8505e4aSGreg Roach    }
528f8505e4aSGreg Roach
529f8505e4aSGreg Roach    /**
530f8505e4aSGreg Roach     * List of tags to exclude when creating new data.
531f8505e4aSGreg Roach     *
532f8505e4aSGreg Roach     * @param string $tag
533f8505e4aSGreg Roach     *
534f8505e4aSGreg Roach     * @return bool
535f8505e4aSGreg Roach     */
536f8505e4aSGreg Roach    private function isHiddenTag(string $tag): bool
537f8505e4aSGreg Roach    {
538a9da2374SGreg Roach        // Function to filter hidden tags.
539a9da2374SGreg Roach        $fn_hide = fn (string $x): bool => (bool) Site::getPreference('HIDE_' . $x);
540a9da2374SGreg Roach
541a9da2374SGreg Roach        $preferences = array_filter(Gedcom::HIDDEN_TAGS, $fn_hide, ARRAY_FILTER_USE_KEY);
542f8505e4aSGreg Roach        $preferences = array_values($preferences);
543f8505e4aSGreg Roach        $hidden_tags = array_merge(...$preferences);
544f8505e4aSGreg Roach
545f8505e4aSGreg Roach        foreach ($hidden_tags as $hidden_tag) {
546f8505e4aSGreg Roach            if (str_contains($tag, $hidden_tag)) {
547f8505e4aSGreg Roach                return true;
548f8505e4aSGreg Roach            }
549f8505e4aSGreg Roach        }
550f8505e4aSGreg Roach
551f8505e4aSGreg Roach        return false;
552e22e42f7SGreg Roach    }
5537c7d1e03SGreg Roach}
554