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{ 5709482a55SGreg Roach /** @var array<string> */ 5809482a55SGreg Roach public array $glevels = []; 597c7d1e03SGreg Roach 6009482a55SGreg Roach /** @var array<string> */ 6109482a55SGreg Roach public array $tag = []; 627c7d1e03SGreg Roach 6309482a55SGreg Roach /** @var array<string> */ 6409482a55SGreg Roach public array $islink = []; 657c7d1e03SGreg Roach 6609482a55SGreg Roach /** @var array<string> */ 6709482a55SGreg Roach public array $text = []; 687c7d1e03SGreg Roach 6909482a55SGreg Roach /** @var array<string> */ 7009482a55SGreg Roach protected array $glevelsSOUR = []; 717c7d1e03SGreg Roach 7209482a55SGreg Roach /** @var array<string> */ 7309482a55SGreg Roach protected array $tagSOUR = []; 747c7d1e03SGreg Roach 7509482a55SGreg Roach /** @var array<string> */ 7609482a55SGreg Roach protected array $islinkSOUR = []; 777c7d1e03SGreg Roach 7809482a55SGreg Roach /** @var array<string> */ 7909482a55SGreg Roach protected array $textSOUR = []; 807c7d1e03SGreg Roach 8109482a55SGreg Roach /** @var array<string> */ 8209482a55SGreg Roach protected array $glevelsRest = []; 837c7d1e03SGreg Roach 8409482a55SGreg Roach /** @var array<string> */ 8509482a55SGreg Roach protected array $tagRest = []; 867c7d1e03SGreg Roach 8709482a55SGreg Roach /** @var array<string> */ 8809482a55SGreg Roach protected array $islinkRest = []; 897c7d1e03SGreg Roach 9009482a55SGreg Roach /** @var array<string> */ 9109482a55SGreg 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 441*05babb96SGreg Roach $subtags = array_filter($subtags, static 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 445686ef499SGreg Roach // Don't include facts/events that we have hidden in the control panel. 4467c3bf6b0SGreg Roach $subtags = array_filter($subtags, fn (string $subtag): bool => !$this->isHiddenTag($record->tag() . ':' . $subtag)); 4477c3bf6b0SGreg Roach 448abdaad0dSGreg Roach if (!$include_hidden) { 449abdaad0dSGreg Roach $fn_hidden = fn (string $t): bool => !$this->isHiddenTag($record->tag() . ':' . $t); 450abdaad0dSGreg Roach $subtags = array_filter($subtags, $fn_hidden); 451abdaad0dSGreg Roach } 452abdaad0dSGreg Roach 4539c7bc1e3SGreg Roach $subtags = array_diff($subtags, ['HUSB', 'WIFE', 'CHIL', 'FAMC', 'FAMS', 'CHAN']); 4549c7bc1e3SGreg Roach 455abdaad0dSGreg Roach return $subtags; 456abdaad0dSGreg Roach } 457abdaad0dSGreg Roach 458abdaad0dSGreg Roach /** 459f8505e4aSGreg Roach * @param Tree $tree 460f8505e4aSGreg Roach * @param string $tag 461f8505e4aSGreg Roach * @param string $gedcom 462f8505e4aSGreg Roach * @param bool $include_hidden 463f8505e4aSGreg Roach * 464f8505e4aSGreg Roach * @return string 465f8505e4aSGreg Roach */ 466f8505e4aSGreg Roach protected function insertMissingLevels(Tree $tree, string $tag, string $gedcom, bool $include_hidden): string 467f8505e4aSGreg Roach { 468f8505e4aSGreg Roach $next_level = substr_count($tag, ':') + 1; 469f8505e4aSGreg Roach $factory = Registry::elementFactory(); 470f8505e4aSGreg Roach $subtags = $factory->make($tag)->subtags(); 471f8505e4aSGreg Roach 472f8505e4aSGreg Roach // Merge CONT records onto their parent line. 473f8505e4aSGreg Roach $gedcom = strtr($gedcom, [ 474f8505e4aSGreg Roach "\n" . $next_level . ' CONT ' => "\r", 475f8505e4aSGreg Roach "\n" . $next_level . ' CONT' => "\r", 476f8505e4aSGreg Roach ]); 477f8505e4aSGreg Roach 478f8505e4aSGreg Roach // The first part is level N. The remainder are level N+1. 479f8505e4aSGreg Roach $parts = preg_split('/\n(?=' . $next_level . ')/', $gedcom); 4804502a888SGreg Roach $return = array_shift($parts) ?? ''; 481f8505e4aSGreg Roach 482f8505e4aSGreg Roach foreach ($subtags as $subtag => $occurrences) { 483f8505e4aSGreg Roach if (!$include_hidden && $this->isHiddenTag($tag . ':' . $subtag)) { 484f8505e4aSGreg Roach continue; 485f8505e4aSGreg Roach } 486f8505e4aSGreg Roach 487f8505e4aSGreg Roach [$min, $max] = explode(':', $occurrences); 488f8505e4aSGreg Roach 489f8505e4aSGreg Roach $min = (int) $min; 490f8505e4aSGreg Roach 491f8505e4aSGreg Roach if ($max === 'M') { 492f8505e4aSGreg Roach $max = PHP_INT_MAX; 493f8505e4aSGreg Roach } else { 494f8505e4aSGreg Roach $max = (int) $max; 495f8505e4aSGreg Roach } 496f8505e4aSGreg Roach 497f8505e4aSGreg Roach $count = 0; 498f8505e4aSGreg Roach 499f8505e4aSGreg Roach // Add expected subtags in our preferred order. 500f8505e4aSGreg Roach foreach ($parts as $n => $part) { 501f8505e4aSGreg Roach if (str_starts_with($part, $next_level . ' ' . $subtag)) { 502f8505e4aSGreg Roach $return .= "\n" . $this->insertMissingLevels($tree, $tag . ':' . $subtag, $part, $include_hidden); 503f8505e4aSGreg Roach $count++; 504f8505e4aSGreg Roach unset($parts[$n]); 505f8505e4aSGreg Roach } 506f8505e4aSGreg Roach } 507f8505e4aSGreg Roach 508f8505e4aSGreg Roach // Allowed to have more of this subtag? 509f8505e4aSGreg Roach if ($count < $max) { 510f8505e4aSGreg Roach // Create a new one. 511f8505e4aSGreg Roach $gedcom = $next_level . ' ' . $subtag; 512f8505e4aSGreg Roach $default = $factory->make($tag . ':' . $subtag)->default($tree); 513f8505e4aSGreg Roach if ($default !== '') { 514f8505e4aSGreg Roach $gedcom .= ' ' . $default; 515f8505e4aSGreg Roach } 516f8505e4aSGreg Roach 517f8505e4aSGreg Roach $number_to_add = max(1, $min - $count); 518f8505e4aSGreg Roach $gedcom_to_add = "\n" . $this->insertMissingLevels($tree, $tag . ':' . $subtag, $gedcom, $include_hidden); 519f8505e4aSGreg Roach 520f8505e4aSGreg Roach $return .= str_repeat($gedcom_to_add, $number_to_add); 521f8505e4aSGreg Roach } 522f8505e4aSGreg Roach } 523f8505e4aSGreg Roach 524f8505e4aSGreg Roach // Now add any unexpected/existing data. 525f8505e4aSGreg Roach if ($parts !== []) { 526f8505e4aSGreg Roach $return .= "\n" . implode("\n", $parts); 527f8505e4aSGreg Roach } 528f8505e4aSGreg Roach 529f8505e4aSGreg Roach return $return; 530f8505e4aSGreg Roach } 531f8505e4aSGreg Roach 532f8505e4aSGreg Roach /** 533f8505e4aSGreg Roach * List of tags to exclude when creating new data. 534f8505e4aSGreg Roach * 535f8505e4aSGreg Roach * @param string $tag 536f8505e4aSGreg Roach * 537f8505e4aSGreg Roach * @return bool 538f8505e4aSGreg Roach */ 539f8505e4aSGreg Roach private function isHiddenTag(string $tag): bool 540f8505e4aSGreg Roach { 541a9da2374SGreg Roach // Function to filter hidden tags. 542*05babb96SGreg Roach $fn_hide = static fn (string $x): bool => (bool) Site::getPreference('HIDE_' . $x); 543a9da2374SGreg Roach 544a9da2374SGreg Roach $preferences = array_filter(Gedcom::HIDDEN_TAGS, $fn_hide, ARRAY_FILTER_USE_KEY); 545f8505e4aSGreg Roach $preferences = array_values($preferences); 546f8505e4aSGreg Roach $hidden_tags = array_merge(...$preferences); 547f8505e4aSGreg Roach 548f8505e4aSGreg Roach foreach ($hidden_tags as $hidden_tag) { 549f8505e4aSGreg Roach if (str_contains($tag, $hidden_tag)) { 550f8505e4aSGreg Roach return true; 551f8505e4aSGreg Roach } 552f8505e4aSGreg Roach } 553f8505e4aSGreg Roach 554f8505e4aSGreg Roach return false; 555e22e42f7SGreg Roach } 5567c7d1e03SGreg Roach} 557