Passed
Push — develop ( 7fbded...e50e9c )
by Greg
17:21 queued 05:10
created

GedcomEditService::factsToAdd()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 2
nop 2
dl 0
loc 17
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2022 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_ends_with;
49
use function str_repeat;
50
use function str_replace;
51
use function str_starts_with;
52
use function substr_count;
53
use function trim;
54
55
use const ARRAY_FILTER_USE_BOTH;
56
use const ARRAY_FILTER_USE_KEY;
57
use const PHP_INT_MAX;
58
59
/**
60
 * Utilities to edit/save GEDCOM data.
61
 */
62
class GedcomEditService
63
{
64
    /**
65
     * @param Tree $tree
66
     *
67
     * @return Collection<int,Fact>
68
     */
69
    public function newFamilyFacts(Tree $tree): Collection
70
    {
71
        $dummy = Registry::familyFactory()->new('', '0 @@ FAM', null, $tree);
72
        $tags  = new Collection(explode(',', $tree->getPreference('QUICK_REQUIRED_FAMFACTS')));
0 ignored issues
show
Bug introduced by
explode(',', $tree->getP...CK_REQUIRED_FAMFACTS')) of type string[] is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $items of Illuminate\Support\Collection::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

72
        $tags  = new Collection(/** @scrutinizer ignore-type */ explode(',', $tree->getPreference('QUICK_REQUIRED_FAMFACTS')));
Loading history...
73
        $facts = $tags->map(fn (string $tag): Fact => $this->createNewFact($dummy, $tag));
74
75
        return Fact::sortFacts($facts);
76
    }
77
78
    /**
79
     * @param Tree          $tree
80
     * @param string        $sex
81
     * @param array<string> $names
82
     *
83
     * @return Collection<int,Fact>
84
     */
85
    public function newIndividualFacts(Tree $tree, string $sex, array $names): Collection
86
    {
87
        $dummy      = Registry::individualFactory()->new('', '0 @@ INDI', null, $tree);
88
        $tags       = new Collection(explode(',', $tree->getPreference('QUICK_REQUIRED_FACTS')));
0 ignored issues
show
Bug introduced by
explode(',', $tree->getP...QUICK_REQUIRED_FACTS')) of type string[] is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $items of Illuminate\Support\Collection::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
        $tags       = new Collection(/** @scrutinizer ignore-type */ explode(',', $tree->getPreference('QUICK_REQUIRED_FACTS')));
Loading history...
89
        $facts      = $tags->map(fn (string $tag): Fact => $this->createNewFact($dummy, $tag));
90
        $sex_fact   = new Collection([new Fact('1 SEX ' . $sex, $dummy, '')]);
0 ignored issues
show
Bug introduced by
array(new Fisharebest\We... ' . $sex, $dummy, '')) of type array<integer,Fisharebest\Webtrees\Fact> is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $items of Illuminate\Support\Collection::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

90
        $sex_fact   = new Collection(/** @scrutinizer ignore-type */ [new Fact('1 SEX ' . $sex, $dummy, '')]);
Loading history...
91
        $name_facts = Collection::make($names)->map(static fn (string $gedcom): Fact => new Fact($gedcom, $dummy, ''));
0 ignored issues
show
Bug introduced by
$names of type string[] is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $items of Illuminate\Support\Collection::make(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

91
        $name_facts = Collection::make(/** @scrutinizer ignore-type */ $names)->map(static fn (string $gedcom): Fact => new Fact($gedcom, $dummy, ''));
Loading history...
92
93
        return $sex_fact->concat($name_facts)->concat(Fact::sortFacts($facts));
94
    }
95
96
    /**
97
     * @param GedcomRecord $record
98
     * @param string       $tag
99
     *
100
     * @return Fact
101
     */
102
    private function createNewFact(GedcomRecord $record, string $tag): Fact
103
    {
104
        $element = Registry::elementFactory()->make($record->tag() . ':' . $tag);
105
        $default = $element->default($record->tree());
106
        $gedcom  = trim('1 ' . $tag . ' ' . $default);
107
108
        return new Fact($gedcom, $record, '');
109
    }
110
111
    /**
112
     * Reassemble edited GEDCOM fields into a GEDCOM fact/event string.
113
     *
114
     * @param string        $record_type
115
     * @param array<string> $levels
116
     * @param array<string> $tags
117
     * @param array<string> $values
118
     * @param bool          $append - Are we appending to a level 0 record, or replacing a level 1 record?
119
     *
120
     * @return string
121
     */
122
    public function editLinesToGedcom(string $record_type, array $levels, array $tags, array $values, bool $append = true): string
123
    {
124
        // Assert all arrays are the same size.
125
        $count = count($levels);
126
        assert($count > 0);
127
        assert(count($tags) === $count);
128
        assert(count($values) === $count);
129
130
        $gedcom_lines = [];
131
        $hierarchy    = [$record_type];
132
133
        for ($i = 0; $i < $count; $i++) {
134
            $hierarchy[$levels[$i]] = $tags[$i];
135
136
            $full_tag   = implode(':', array_slice($hierarchy, 0, 1 + (int) $levels[$i]));
137
            $element    = Registry::elementFactory()->make($full_tag);
138
            $values[$i] = $element->canonical($values[$i]);
139
140
            // If "1 FACT Y" has a DATE or PLAC, then delete the value of Y
141
            if ($levels[$i] === '1' && $values[$i] === 'Y') {
142
                for ($j = $i + 1; $j < $count && $levels[$j] > $levels[$i]; ++$j) {
143
                    if ($levels[$j] === '2' && ($tags[$j] === 'DATE' || $tags[$j] === 'PLAC') && $values[$j] !== '') {
144
                        $values[$i] = '';
145
                        break;
146
                    }
147
                }
148
            }
149
150
            // Find the next tag at the same level.  Check if any child tags have values.
151
            $children_with_values = false;
152
            for ($j = $i + 1; $j < $count && $levels[$j] > $levels[$i]; $j++) {
153
                if ($values[$j] !== '') {
154
                    $children_with_values = true;
155
                }
156
            }
157
158
            if ($values[$i] !== '' || $children_with_values  && !$element instanceof AbstractXrefElement) {
159
                if ($values[$i] === '') {
160
                    $gedcom_lines[] = $levels[$i] . ' ' . $tags[$i];
161
                } else {
162
                    // We use CONC for editing NOTE records.
163
                    if ($tags[$i] === 'CONC') {
164
                        $next_level = (int) $levels[$i];
165
                    } else {
166
                        $next_level = 1 + (int) $levels[$i];
167
                    }
168
169
                    $gedcom_lines[] = $levels[$i] . ' ' . $tags[$i] . ' ' . str_replace("\n", "\n" . $next_level . ' CONT ', $values[$i]);
170
                }
171
            } else {
172
                $i = $j - 1;
173
            }
174
        }
175
176
        $gedcom = implode("\n", $gedcom_lines);
177
178
        if ($append && $gedcom !== '') {
179
            $gedcom = "\n" . $gedcom;
180
        }
181
182
        return $gedcom;
183
    }
184
185
    /**
186
     * Add blank lines, to allow a user to add/edit new values.
187
     *
188
     * @param Fact $fact
189
     * @param bool $include_hidden
190
     *
191
     * @return string
192
     */
193
    public function insertMissingFactSubtags(Fact $fact, bool $include_hidden): string
194
    {
195
        // Merge CONT records onto their parent line.
196
        $gedcom = preg_replace('/\n\d CONT ?/', "\r", $fact->gedcom());
197
198
        return $this->insertMissingLevels($fact->record()->tree(), $fact->tag(), $gedcom, $include_hidden);
199
    }
200
201
    /**
202
     * Add blank lines, to allow a user to add/edit new values.
203
     *
204
     * @param GedcomRecord $record
205
     * @param bool         $include_hidden
206
     *
207
     * @return string
208
     */
209
    public function insertMissingRecordSubtags(GedcomRecord $record, bool $include_hidden): string
210
    {
211
        // Merge CONT records onto their parent line.
212
        $gedcom = preg_replace('/\n\d CONT ?/', "\r", $record->gedcom());
213
214
        $gedcom = $this->insertMissingLevels($record->tree(), $record->tag(), $gedcom, $include_hidden);
215
216
        // NOTE records have data at level 0.  Move it to 1 CONC.
217
        if ($record instanceof Note) {
218
            return preg_replace('/^0 @[^@]+@ NOTE/', '1 CONC', $gedcom);
219
        }
220
221
        return preg_replace('/^0.*\n/', '', $gedcom);
222
    }
223
224
    /**
225
     * List of facts/events to add to families and individuals.
226
     *
227
     * @param Family|Individual $record
228
     * @param bool              $include_hidden
229
     *
230
     * @return array<string>
231
     */
232
    public function factsToAdd(GedcomRecord $record, bool $include_hidden): array
233
    {
234
        $subtags = Registry::elementFactory()->make($record->tag())->subtags();
235
236
        $subtags = array_filter($subtags, static fn (string $v, string $k) => !str_ends_with($v, ':1') || $record->facts([$k])->isEmpty(), ARRAY_FILTER_USE_BOTH);
237
238
        $subtags = array_keys($subtags);
239
240
        // Don't include facts/events that we have hidden in the control panel.
241
        $subtags = array_filter($subtags, fn (string $subtag): bool => !$this->isHiddenTag($record->tag() . ':' . $subtag));
242
243
        if (!$include_hidden) {
244
            $fn_hidden = fn (string $t): bool => !$this->isHiddenTag($record->tag() . ':' . $t);
245
            $subtags   = array_filter($subtags, $fn_hidden);
246
        }
247
248
        return array_diff($subtags, ['HUSB', 'WIFE', 'CHIL', 'FAMC', 'FAMS', 'CHAN']);
249
    }
250
251
    /**
252
     * @param Tree   $tree
253
     * @param string $tag
254
     * @param string $gedcom
255
     * @param bool   $include_hidden
256
     *
257
     * @return string
258
     */
259
    protected function insertMissingLevels(Tree $tree, string $tag, string $gedcom, bool $include_hidden): string
260
    {
261
        $next_level = substr_count($tag, ':') + 1;
262
        $factory    = Registry::elementFactory();
263
        $subtags    = $factory->make($tag)->subtags();
264
265
        // The first part is level N.  The remainder are level N+1.
266
        $parts  = preg_split('/\n(?=' . $next_level . ')/', $gedcom);
267
        $return = array_shift($parts) ?? '';
268
269
        foreach ($subtags as $subtag => $occurrences) {
270
            $hidden = str_ends_with($occurrences, ':?') || $this->isHiddenTag($tag . ':' . $subtag);
271
272
            if (!$include_hidden && $hidden) {
273
                continue;
274
            }
275
276
            [$min, $max] = explode(':', $occurrences);
277
278
            $min = (int) $min;
279
280
            if ($max === 'M') {
281
                $max = PHP_INT_MAX;
282
            } else {
283
                $max = (int) $max;
284
            }
285
286
            $count = 0;
287
288
            // Add expected subtags in our preferred order.
289
            foreach ($parts as $n => $part) {
290
                if (str_starts_with($part, $next_level . ' ' . $subtag)) {
291
                    $return .= "\n" . $this->insertMissingLevels($tree, $tag . ':' . $subtag, $part, $include_hidden);
292
                    $count++;
293
                    unset($parts[$n]);
294
                }
295
            }
296
297
            // Allowed to have more of this subtag?
298
            if ($count < $max) {
299
                // Create a new one.
300
                $gedcom  = $next_level . ' ' . $subtag;
301
                $default = $factory->make($tag . ':' . $subtag)->default($tree);
302
                if ($default !== '') {
303
                    $gedcom .= ' ' . $default;
304
                }
305
306
                $number_to_add = max(1, $min - $count);
307
                $gedcom_to_add = "\n" . $this->insertMissingLevels($tree, $tag . ':' . $subtag, $gedcom, $include_hidden);
308
309
                $return .= str_repeat($gedcom_to_add, $number_to_add);
310
            }
311
        }
312
313
        // Now add any unexpected/existing data.
314
        if ($parts !== []) {
315
            $return .= "\n" . implode("\n", $parts);
316
        }
317
318
        return $return;
319
    }
320
321
    /**
322
     * List of tags to exclude when creating new data.
323
     *
324
     * @param string $tag
325
     *
326
     * @return bool
327
     */
328
    private function isHiddenTag(string $tag): bool
329
    {
330
        // Function to filter hidden tags.
331
        $fn_hide = static fn (string $x): bool => (bool) Site::getPreference('HIDE_' . $x);
332
333
        $preferences = array_filter(Gedcom::HIDDEN_TAGS, $fn_hide, ARRAY_FILTER_USE_KEY);
334
        $preferences = array_values($preferences);
335
        $hidden_tags = array_merge(...$preferences);
336
337
        foreach ($hidden_tags as $hidden_tag) {
338
            if (str_contains($tag, $hidden_tag)) {
339
                return true;
340
            }
341
        }
342
343
        return false;
344
    }
345
}
346