Passed
Push — dev ( 9fabb1...8976f1 )
by Greg
06:13
created

GedcomEditService::createNewFact()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 7
rs 10
c 1
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2021 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Services;
21
22
use Fisharebest\Webtrees\Elements\AbstractXrefElement;
23
use Fisharebest\Webtrees\Fact;
24
use Fisharebest\Webtrees\Family;
25
use Fisharebest\Webtrees\Gedcom;
26
use Fisharebest\Webtrees\GedcomRecord;
27
use Fisharebest\Webtrees\Individual;
28
use Fisharebest\Webtrees\Note;
29
use Fisharebest\Webtrees\Registry;
30
use Fisharebest\Webtrees\Site;
31
use Fisharebest\Webtrees\Tree;
32
use Illuminate\Support\Collection;
33
34
use function array_diff;
35
use function array_filter;
36
use function array_keys;
37
use function array_merge;
38
use function array_shift;
39
use function array_slice;
40
use function array_values;
41
use function assert;
42
use function count;
43
use function explode;
44
use function implode;
45
use function max;
46
use function preg_replace;
47
use function preg_split;
48
use function str_repeat;
49
use function str_replace;
50
use function substr_count;
51
use function trim;
52
53
use const ARRAY_FILTER_USE_BOTH;
54
use const ARRAY_FILTER_USE_KEY;
55
use const PHP_INT_MAX;
56
57
/**
58
 * Utilities to edit/save GEDCOM data.
59
 */
60
class GedcomEditService
61
{
62
    /**
63
     * @param Tree $tree
64
     *
65
     * @return Collection<Fact>
66
     */
67
    public function newFamilyFacts(Tree $tree): Collection
68
    {
69
        $dummy = Registry::familyFactory()->new('', '0 @@ FAM', null, $tree);
70
        $tags  = new Collection(explode(',', $tree->getPreference('QUICK_REQUIRED_FAMFACTS')));
71
        $facts = $tags->map(fn (string $tag): Fact => $this->createNewFact($dummy, $tag));
72
73
        return Fact::sortFacts($facts);
74
    }
75
76
    /**
77
     * @param Tree          $tree
78
     * @param string        $sex
79
     * @param array<string> $names
80
     *
81
     * @return Collection<Fact>
82
     */
83
    public function newIndividualFacts(Tree $tree, string $sex, array $names): Collection
84
    {
85
        $dummy      = Registry::individualFactory()->new('', '0 @@ INDI', null, $tree);
86
        $tags       = new Collection(explode(',', $tree->getPreference('QUICK_REQUIRED_FACTS')));
87
        $facts      = $tags->map(fn (string $tag): Fact => $this->createNewFact($dummy, $tag));
88
        $sex_fact   = new Collection([new Fact('1 SEX ' . $sex, $dummy, '')]);
89
        $name_facts = Collection::make($names)->map(static fn (string $gedcom): Fact => new Fact($gedcom, $dummy, ''));
90
91
        return $sex_fact->concat($name_facts)->concat(Fact::sortFacts($facts));
92
    }
93
94
    /**
95
     * @param GedcomRecord $record
96
     * @param string       $tag
97
     *
98
     * @return Fact
99
     */
100
    private function createNewFact(GedcomRecord $record, string $tag): Fact
101
    {
102
        $element = Registry::elementFactory()->make($record->tag() . ':' . $tag);
103
        $default = $element->default($record->tree());
104
        $gedcom  = trim('1 ' . $tag . ' ' . $default);
105
106
        return new Fact($gedcom, $record, '');
107
    }
108
109
    /**
110
     * Reassemble edited GEDCOM fields into a GEDCOM fact/event string.
111
     *
112
     * @param string        $record_type
113
     * @param array<string> $levels
114
     * @param array<string> $tags
115
     * @param array<string> $values
116
     *
117
     * @return string
118
     */
119
    public function editLinesToGedcom(string $record_type, array $levels, array $tags, array $values): string
120
    {
121
        // Assert all arrays are the same size.
122
        $count = count($levels);
123
        assert($count > 0);
124
        assert(count($tags) === $count);
125
        assert(count($values) === $count);
126
127
        $gedcom_lines = [];
128
        $hierarchy    = [$record_type];
129
130
        for ($i = 0; $i < $count; $i++) {
131
            $hierarchy[$levels[$i]] = $tags[$i];
132
133
            $full_tag   = implode(':', array_slice($hierarchy, 0, 1 + (int) $levels[$i]));
134
            $element    = Registry::elementFactory()->make($full_tag);
135
            $values[$i] = $element->canonical($values[$i]);
136
137
            // If "1 FACT Y" has a DATE or PLAC, then delete the value of Y
138
            if ($levels[$i] === '1' && $values[$i] === 'Y') {
139
                for ($j = $i + 1; $j < $count && $levels[$j] > $levels[$i]; ++$j) {
140
                    if ($levels[$j] === '2' && ($tags[$j] === 'DATE' || $tags[$j] === 'PLAC') && $values[$j] !== '') {
141
                        $values[$i] = '';
142
                        break;
143
                    }
144
                }
145
            }
146
147
            // Find the next tag at the same level.  Check if any child tags have values.
148
            $children_with_values = false;
149
            for ($j = $i + 1; $j < $count && $levels[$j] > $levels[$i]; $j++) {
150
                if ($values[$j] !== '') {
151
                    $children_with_values = true;
152
                }
153
            }
154
155
            if ($values[$i] !== '' || $children_with_values  && !$element instanceof AbstractXrefElement) {
156
                if ($values[$i] === '') {
157
                    $gedcom_lines[] = $levels[$i] . ' ' . $tags[$i];
158
                } else {
159
                    // We use CONC for editing NOTE records.
160
                    if ($tags[$i] === 'CONC') {
161
                        $next_level = (int) $levels[$i];
162
                    } else {
163
                        $next_level = 1 + (int) $levels[$i];
164
                    }
165
166
                    $gedcom_lines[] = $levels[$i] . ' ' . $tags[$i] . ' ' . str_replace("\n", "\n" . $next_level . ' CONT ', $values[$i]);
167
                }
168
            } else {
169
                $i = $j - 1;
170
            }
171
        }
172
173
        return implode("\n", $gedcom_lines);
174
    }
175
176
    /**
177
     * Add blank lines, to allow a user to add/edit new values.
178
     *
179
     * @param Fact $fact
180
     * @param bool $include_hidden
181
     *
182
     * @return string
183
     */
184
    public function insertMissingFactSubtags(Fact $fact, bool $include_hidden): string
185
    {
186
        return $this->insertMissingLevels($fact->record()->tree(), $fact->tag(), $fact->gedcom(), $include_hidden);
187
    }
188
189
    /**
190
     * Add blank lines, to allow a user to add/edit new values.
191
     *
192
     * @param GedcomRecord $record
193
     * @param bool         $include_hidden
194
     *
195
     * @return string
196
     */
197
    public function insertMissingRecordSubtags(GedcomRecord $record, bool $include_hidden): string
198
    {
199
        $gedcom = $this->insertMissingLevels($record->tree(), $record->tag(), $record->gedcom(), $include_hidden);
200
201
        // NOTE records have data at level 0.  Move it to 1 CONC.
202
        if ($record instanceof Note) {
203
            return preg_replace('/^0 @[^@]+@ NOTE/', '1 CONC', $gedcom);
204
        }
205
206
        return preg_replace('/^0.*\n/', '', $gedcom);
207
    }
208
209
    /**
210
     * List of facts/events to add to families and individuals.
211
     *
212
     * @param Family|Individual $record
213
     * @param bool              $include_hidden
214
     *
215
     * @return array<string>
216
     */
217
    public function factsToAdd(GedcomRecord $record, bool $include_hidden): array
218
    {
219
        $subtags = Registry::elementFactory()->make($record->tag())->subtags();
220
221
        $subtags = array_filter($subtags, static fn (string $v, string $k) => !str_ends_with($v, ':1') || $record->facts([$k])->isEmpty(), ARRAY_FILTER_USE_BOTH);
222
223
        $subtags = array_keys($subtags);
224
225
        // Don't include facts/events that we have hidden in the control panel.
226
        $subtags = array_filter($subtags, fn (string $subtag): bool => !$this->isHiddenTag($record->tag() . ':' . $subtag));
227
228
        if (!$include_hidden) {
229
            $fn_hidden = fn (string $t): bool => !$this->isHiddenTag($record->tag() . ':' . $t);
230
            $subtags   = array_filter($subtags, $fn_hidden);
231
        }
232
233
        return array_diff($subtags, ['HUSB', 'WIFE', 'CHIL', 'FAMC', 'FAMS', 'CHAN']);
234
    }
235
236
    /**
237
     * @param Tree   $tree
238
     * @param string $tag
239
     * @param string $gedcom
240
     * @param bool   $include_hidden
241
     *
242
     * @return string
243
     */
244
    protected function insertMissingLevels(Tree $tree, string $tag, string $gedcom, bool $include_hidden): string
245
    {
246
        $next_level = substr_count($tag, ':') + 1;
247
        $factory    = Registry::elementFactory();
248
        $subtags    = $factory->make($tag)->subtags();
249
250
        // Merge CONT records onto their parent line.
251
        $gedcom = strtr($gedcom, [
252
            "\n" . $next_level . ' CONT ' => "\r",
253
            "\n" . $next_level . ' CONT' => "\r",
254
        ]);
255
256
        // The first part is level N.  The remainder are level N+1.
257
        $parts  = preg_split('/\n(?=' . $next_level . ')/', $gedcom);
258
        $return = array_shift($parts) ?? '';
259
260
        foreach ($subtags as $subtag => $occurrences) {
261
            if (!$include_hidden && $this->isHiddenTag($tag . ':' . $subtag)) {
262
                continue;
263
            }
264
265
            [$min, $max] = explode(':', $occurrences);
266
267
            $min = (int) $min;
268
269
            if ($max === 'M') {
270
                $max = PHP_INT_MAX;
271
            } else {
272
                $max = (int) $max;
273
            }
274
275
            $count = 0;
276
277
            // Add expected subtags in our preferred order.
278
            foreach ($parts as $n => $part) {
279
                if (str_starts_with($part, $next_level . ' ' . $subtag)) {
280
                    $return .= "\n" . $this->insertMissingLevels($tree, $tag . ':' . $subtag, $part, $include_hidden);
281
                    $count++;
282
                    unset($parts[$n]);
283
                }
284
            }
285
286
            // Allowed to have more of this subtag?
287
            if ($count < $max) {
288
                // Create a new one.
289
                $gedcom  = $next_level . ' ' . $subtag;
290
                $default = $factory->make($tag . ':' . $subtag)->default($tree);
291
                if ($default !== '') {
292
                    $gedcom .= ' ' . $default;
293
                }
294
295
                $number_to_add = max(1, $min - $count);
296
                $gedcom_to_add = "\n" . $this->insertMissingLevels($tree, $tag . ':' . $subtag, $gedcom, $include_hidden);
297
298
                $return .= str_repeat($gedcom_to_add, $number_to_add);
299
            }
300
        }
301
302
        // Now add any unexpected/existing data.
303
        if ($parts !== []) {
304
            $return .= "\n" . implode("\n", $parts);
305
        }
306
307
        return $return;
308
    }
309
310
    /**
311
     * List of tags to exclude when creating new data.
312
     *
313
     * @param string $tag
314
     *
315
     * @return bool
316
     */
317
    private function isHiddenTag(string $tag): bool
318
    {
319
        // Function to filter hidden tags.
320
        $fn_hide = static fn (string $x): bool => (bool) Site::getPreference('HIDE_' . $x);
321
322
        $preferences = array_filter(Gedcom::HIDDEN_TAGS, $fn_hide, ARRAY_FILTER_USE_KEY);
323
        $preferences = array_values($preferences);
324
        $hidden_tags = array_merge(...$preferences);
325
326
        foreach ($hidden_tags as $hidden_tag) {
327
            if (str_contains($tag, $hidden_tag)) {
328
                return true;
329
            }
330
        }
331
332
        return false;
333
    }
334
}
335