Completed
Push — develop ( f9771f...17d5e2 )
by Greg
43:26 queued 34:02
created

FunctionsPrintFacts   F

Complexity

Total Complexity 177

Size/Duplication

Total Lines 868
Duplicated Lines 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 177
eloc 510
c 6
b 0
f 0
dl 0
loc 868
rs 2

9 Methods

Rating   Name   Duplication   Size   Complexity  
F printFact() 0 229 67
D formatAssociateRelationship() 0 91 19
B printMainMedia() 0 50 11
B printMediaLinks() 0 32 7
B printSourceStructure() 0 29 7
F printMainSources() 0 121 26
F printMainNotes() 0 119 27
B printFactSources() 0 59 10
A getSourceStructure() 0 27 3

How to fix   Complexity   

Complex Class

Complex classes like FunctionsPrintFacts often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FunctionsPrintFacts, and based on these observations, apply Extract Interface, too.

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\Functions;
21
22
use Fisharebest\Webtrees\Age;
23
use Fisharebest\Webtrees\Auth;
24
use Fisharebest\Webtrees\Fact;
25
use Fisharebest\Webtrees\Family;
26
use Fisharebest\Webtrees\Filter;
27
use Fisharebest\Webtrees\Gedcom;
28
use Fisharebest\Webtrees\Elements\UnknownElement;
29
use Fisharebest\Webtrees\GedcomRecord;
30
use Fisharebest\Webtrees\GedcomTag;
31
use Fisharebest\Webtrees\Http\RequestHandlers\EditFactPage;
32
use Fisharebest\Webtrees\I18N;
33
use Fisharebest\Webtrees\Individual;
34
use Fisharebest\Webtrees\Media;
35
use Fisharebest\Webtrees\Module\ModuleChartInterface;
36
use Fisharebest\Webtrees\Module\ModuleInterface;
37
use Fisharebest\Webtrees\Module\RelationshipsChartModule;
38
use Fisharebest\Webtrees\Note;
39
use Fisharebest\Webtrees\Registry;
40
use Fisharebest\Webtrees\Services\ModuleService;
41
use Fisharebest\Webtrees\Tree;
42
use Ramsey\Uuid\Uuid;
43
44
use function app;
45
use function array_merge;
46
use function count;
47
use function e;
48
use function explode;
49
use function implode;
50
use function ob_get_clean;
51
use function ob_start;
52
use function preg_match;
53
use function preg_match_all;
54
use function preg_replace;
55
use function route;
56
use function str_contains;
57
use function strip_tags;
58
use function strlen;
59
use function strpos;
60
use function substr;
61
use function trim;
62
use function view;
63
64
use const PREG_SET_ORDER;
65
66
/**
67
 * Class FunctionsPrintFacts - common functions
68
 *
69
 * @deprecated since 2.0.6.  Will be removed in 2.1.0
70
 */
71
class FunctionsPrintFacts
72
{
73
    /**
74
     * Print a fact record, for the individual/family/source/repository/etc. pages.
75
     * Although a Fact has a parent object, we also need to know
76
     * the GedcomRecord for which we are printing it. For example,
77
     * we can show the death of X on the page of Y, or the marriage
78
     * of X+Y on the page of Z. We need to know both records to
79
     * calculate ages, relationships, etc.
80
     *
81
     * @param Fact         $fact
82
     * @param GedcomRecord $record
83
     *
84
     * @return void
85
     */
86
    public static function printFact(Fact $fact, GedcomRecord $record): void
87
    {
88
        $parent = $fact->record();
89
        $tree   = $parent->tree();
90
        $tag    = $fact->getTag();
91
        $label  = $fact->label();
92
        $value  = $fact->value();
93
        $type   = $fact->attribute('TYPE');
94
        $id     = $fact->id();
95
96
        $element = Registry::elementFactory()->make($fact->tag());
97
98
        // This preference is named HIDE instead of SHOW
99
        $hide_errors = $tree->getPreference('HIDE_GEDCOM_ERRORS') === '0';
100
101
        // Some facts don't get printed here ...
102
        switch ($tag) {
103
            case 'NOTE':
104
                self::printMainNotes($fact, 1);
105
106
                return;
107
            case 'SOUR':
108
                self::printMainSources($fact, 1);
109
110
                return;
111
            case 'OBJE':
112
                self::printMainMedia($fact, 1);
113
114
                return;
115
            case 'FAMC':
116
            case 'FAMS':
117
            case 'CHIL':
118
            case 'HUSB':
119
            case 'WIFE':
120
                // These are internal links, not facts
121
                return;
122
            case '_WT_OBJE_SORT':
123
                // These links were once used internally to record the sort order.
124
                return;
125
            default:
126
                if ($element instanceof UnknownElement && $hide_errors) {
127
                    return;
128
                }
129
                break;
130
        }
131
132
        // New or deleted facts need different styling
133
        $styles = [];
134
        if ($fact->isPendingAddition()) {
135
            $styles[] = 'wt-new';
136
        }
137
        if ($fact->isPendingDeletion()) {
138
            $styles[] = 'wt-old';
139
        }
140
141
        // Event of close relative
142
        if ($tag === 'EVEN' && $value === 'CLOSE_RELATIVE') {
143
            $styles[] = 'wt-relation-fact collapse';
144
        }
145
146
        // Event of close associates
147
        if ($id === 'asso') {
148
            $styles[] = 'wt-relation-fact collapse';
149
        }
150
151
        // historical facts
152
        if ($id === 'histo') {
153
            $styles[] = 'wt-historic-fact collapse';
154
        }
155
156
        // Use marriage type as the label.  e.g. "Civil partnership"
157
        if ($tag === 'MARR') {
158
            $label = $fact->label();
159
            $type  = '';
160
        }
161
162
        echo '<tr class="', implode(' ', $styles), '">';
163
        echo '<th scope="row">';
164
        echo $label;
165
166
        if ($id !== 'histo' && $id !== 'asso' && $fact->canEdit()) {
167
            echo '<div class="editfacts nowrap">';
168
            echo view('edit/icon-fact-edit', ['fact' => $fact, 'url' => $record->url()]);
169
            echo view('edit/icon-fact-copy', ['fact' => $fact]);
170
            echo view('edit/icon-fact-delete', ['fact' => $fact]);
171
            echo '</div>';
172
        }
173
174
        if ($tree->getPreference('SHOW_FACT_ICONS')) {
175
            echo '<span class="wt-fact-icon wt-fact-icon-' . $tag . '" title="' . strip_tags($label) . '"></span>';
176
        }
177
178
        echo '</th>';
179
        echo '<td>';
180
181
        // Event from another record?
182
        if ($parent !== $record) {
183
            if ($parent instanceof Family) {
184
                foreach ($parent->spouses() as $spouse) {
185
                    if ($record !== $spouse) {
186
                        echo '<a href="', e($spouse->url()), '">', $spouse->fullName(), '</a> — ';
187
                    }
188
                }
189
                echo '<a href="', e($parent->url()), '">', I18N::translate('View this family'), '</a><br>';
190
            } elseif ($parent instanceof Individual) {
191
                echo '<a href="', e($parent->url()), '">', $parent->fullName(), '</a><br>';
192
            }
193
        }
194
195
        // Print the value of this fact/event
196
        // we handle ASSO later, in formatAssociateRelationship()
197
        if ($tag !== 'ASSO') {
198
            echo '<div class="field">', $element->value($value, $tree), '</div>';
199
        }
200
201
        // Print the type of this fact/event
202
        if ($type !== '' && $tag !== 'EVEN' && $tag !== 'FACT') {
203
            // Allow (custom) translations for other types
204
            $type = I18N::translate($type);
205
            echo GedcomTag::getLabelValue('TYPE', e($type));
206
        }
207
208
        // Print the date of this fact/event
209
        echo FunctionsPrint::formatFactDate($fact, $record, true, true);
210
211
        // Print the place of this fact/event
212
        echo '<div class="place">', FunctionsPrint::formatFactPlace($fact, true, true, true), '</div>';
213
        // A blank line between the primary attributes (value, date, place) and the secondary ones
214
        echo '<br>';
215
216
        $addr = $fact->attribute('ADDR');
217
        if ($addr !== '') {
218
            $addr = e($addr);
219
            if (str_contains($addr, "\n")) {
220
                $addr = '<span class="d-block" style="white-space: pre-wrap">' . $addr . '</span';
221
            }
222
223
            echo GedcomTag::getLabelValue($fact->tag() . ':ADDR', $addr);
224
        }
225
226
        // Print the associates of this fact/event
227
        if ($id !== 'asso') {
228
            echo self::formatAssociateRelationship($fact);
229
        }
230
231
        // Print any other "2 XXXX" attributes, in the order in which they appear.
232
        preg_match_all('/\n2 (' . Gedcom::REGEX_TAG . ') ?(.*)((\n[3-9].*)*)/', $fact->gedcom(), $l2_matches, PREG_SET_ORDER);
233
234
        foreach ($l2_matches as $l2_match) {
235
            switch ($l2_match[1]) {
236
                case 'DATE':
237
                case 'TIME':
238
                case 'AGE':
239
                case 'HUSB':
240
                case 'WIFE':
241
                case 'PLAC':
242
                case 'ADDR':
243
                case 'ALIA':
244
                case 'ASSO':
245
                case '_ASSO':
246
                case 'DESC':
247
                case 'RELA':
248
                case 'STAT':
249
                case 'TEMP':
250
                case 'TYPE':
251
                case 'FAMS':
252
                case 'CONT':
253
                    // These were already shown at the beginning
254
                    break;
255
                case 'NOTE':
256
                case 'OBJE':
257
                case 'SOUR':
258
                    // These will be shown at the end
259
                    break;
260
                default:
261
                    $subtag = $record::RECORD_TYPE . ':' . $tag . ':' . $l2_match[1];
262
                    $element = Registry::elementFactory()->make($subtag);
263
264
                    if ($element instanceof UnknownElement && $hide_errors) {
265
                        break;
266
                    }
267
268
                    echo $element->labelValue($l2_match[2], $tree);
269
270
                    preg_match_all('/\n3 (' . Gedcom::REGEX_TAG . ') ?(.*)((\n[4-9].*)*)/', $l2_match[3], $l3_matches, PREG_SET_ORDER);
271
272
                    foreach ($l3_matches as $l3_match) {
273
                        $subsubtag = $subtag . ':' . $l3_match[1];
274
                        $element   = Registry::elementFactory()->make($subsubtag);
275
276
                        if ($element instanceof UnknownElement && $hide_errors) {
277
                            continue;
278
                        }
279
280
                        echo $element->labelValue($l3_match[2], $tree);
281
282
                        preg_match_all('/\n4 (' . Gedcom::REGEX_TAG . ') ?(.*)((\n[5-9].*)*)/', $l3_match[3], $l4_matches, PREG_SET_ORDER);
283
284
                        foreach ($l4_matches as $l4_match) {
285
                            $subsubsubtag = $subsubtag . ':' . $l4_match[1];
286
                            $element = Registry::elementFactory()->make($subsubsubtag);
287
288
                            if ($element instanceof UnknownElement && $hide_errors) {
289
                                continue;
290
                            }
291
292
                            echo $element->labelValue($l4_match[2], $tree);
293
294
                            preg_match_all('/\n5 (' . Gedcom::REGEX_TAG . ') ?(.*)((\n[6-9].*)*)/', $l4_match[3], $l5_matches, PREG_SET_ORDER);
295
296
                            foreach ($l5_matches as $l5_match) {
297
                                $subsubsubsubtag = $subsubsubtag . ':' . $l5_match[1];
298
                                $element = Registry::elementFactory()->make($subsubsubsubtag);
299
300
                                if ($element instanceof UnknownElement && $hide_errors) {
301
                                    continue;
302
                                }
303
304
                                echo $element->labelValue($l5_match[2], $tree);
305
                            }
306
                        }
307
                    }
308
                    break;
309
            }
310
        }
311
        echo self::printFactSources($tree, $fact->gedcom(), 2);
312
        echo FunctionsPrint::printFactNotes($tree, $fact->gedcom(), 2);
313
        self::printMediaLinks($tree, $fact->gedcom(), 2);
314
        echo '</td></tr>';
315
    }
316
317
    /**
318
     * Print the associations from the associated individuals in $event to the individuals in $record
319
     *
320
     * @param Fact $event
321
     *
322
     * @return string
323
     */
324
    private static function formatAssociateRelationship(Fact $event): string
325
    {
326
        $parent = $event->record();
327
        // To whom is this record an assocate?
328
        if ($parent instanceof Individual) {
329
            // On an individual page, we just show links to the person
330
            $associates = [$parent];
331
        } elseif ($parent instanceof Family) {
332
            // On a family page, we show links to both spouses
333
            $associates = $parent->spouses();
334
        } else {
335
            // On other pages, it does not make sense to show associates
336
            return '';
337
        }
338
339
        preg_match_all('/^1 ASSO @(' . Gedcom::REGEX_XREF . ')@((\n[2-9].*)*)/', $event->gedcom(), $amatches1, PREG_SET_ORDER);
340
        preg_match_all('/\n2 _?ASSO @(' . Gedcom::REGEX_XREF . ')@((\n[3-9].*)*)/', $event->gedcom(), $amatches2, PREG_SET_ORDER);
341
342
        $html = '';
343
        // For each ASSO record
344
        foreach (array_merge($amatches1, $amatches2) as $amatch) {
345
            $person = Registry::individualFactory()->make($amatch[1], $event->record()->tree());
346
            if ($person && $person->canShowName()) {
347
                // Is there a "RELA" tag
348
                if (preg_match('/\n([23]) RELA (.+)/', $amatch[2], $rmatch)) {
349
                    if ($rmatch[1] === '2') {
350
                        $base_tag = $event->record()->tag();
351
                    } else {
352
                        $base_tag = $event->tag();
353
                    }
354
                    // Use the supplied relationship as a label
355
                    $label = Registry::elementFactory()->make($base_tag . ':_ASSO:RELA')->value($rmatch[2], $parent->tree());
356
                } elseif (preg_match('/^1 _?ASSO/', $event->gedcom())) {
357
                    // Use a default label
358
                    $label = Registry::elementFactory()->make($event->tag())->label();
359
                } else {
360
                    // Use a default label
361
                    $label = Registry::elementFactory()->make($event->tag() . ':_ASSO')->label();
362
                }
363
364
                if ($person->getBirthDate()->isOK() && $event->date()->isOK()) {
365
                    $age = new Age($person->getBirthDate(), $event->date());
366
                    switch ($person->sex()) {
367
                        case 'M':
368
                            $age_text = ' ' . I18N::translateContext('Male', '(aged %s)', (string) $age);
369
                            break;
370
                        case 'F':
371
                            $age_text = ' ' . I18N::translateContext('Female', '(aged %s)', (string) $age);
372
                            break;
373
                        default:
374
                            $age_text = ' ' . I18N::translate('(aged %s)', (string) $age);
375
                            break;
376
                    }
377
                } else {
378
                    $age_text = '';
379
                }
380
381
                $values = ['<a href="' . e($person->url()) . '">' . $person->fullName() . '</a>' . $age_text];
382
383
                $module = app(ModuleService::class)->findByComponent(ModuleChartInterface::class, $person->tree(), Auth::user())->first(static function (ModuleInterface $module) {
384
                    return $module instanceof RelationshipsChartModule;
385
                });
386
387
                if ($module instanceof RelationshipsChartModule) {
388
                    foreach ($associates as $associate) {
389
                        $relationship_name = Functions::getCloseRelationshipName($associate, $person);
390
                        if ($relationship_name === '') {
391
                            $relationship_name = GedcomTag::getLabel('RELA');
392
                        }
393
394
                        if ($parent instanceof Family) {
395
                            // For family ASSO records (e.g. MARR), identify the spouse with a sex icon
396
                            $relationship_name .= '<small>' . view('icons/sex', ['sex' => $associate->sex()]) . '</small>';
397
                        }
398
399
                        $values[] = '<a href="' . $module->chartUrl($associate, ['xref2' => $person->xref()]) . '" rel="nofollow">' . $relationship_name . '</a>';
400
                    }
401
                }
402
                $value = implode(' — ', $values);
403
404
                // Use same markup as GedcomTag::getLabelValue()
405
                $asso = I18N::translate('<span class="label">%1$s:</span> <span class="field" dir="auto">%2$s</span>', $label, $value);
406
            } elseif (!$person && Auth::isEditor($event->record()->tree())) {
407
                $asso = GedcomTag::getLabelValue('ASSO', '<span class="error">' . $amatch[1] . '</span>');
408
            } else {
409
                $asso = '';
410
            }
411
            $html .= '<div class="fact_ASSO">' . $asso . '</div>';
412
        }
413
414
        return $html;
415
    }
416
417
    /**
418
     * print a source linked to a fact (2 SOUR)
419
     * this function is called by the FunctionsPrintFacts::print_fact function and other functions to
420
     * print any source information attached to the fact
421
     *
422
     * @param Tree   $tree
423
     * @param string $factrec The fact record to look for sources in
424
     * @param int    $level   The level to look for sources at
425
     *
426
     * @return string HTML text
427
     */
428
    public static function printFactSources(Tree $tree, string $factrec, int $level): string
429
    {
430
        $data   = '';
431
        $nlevel = $level + 1;
432
433
        // Systems not using source records
434
        // The old style is not supported when entering or editing sources, but may be found in imported trees.
435
        // Also, the old style sources allow histo.* files to use tree independent source citations, which
436
        // will display nicely when markdown is used.
437
        $ct = preg_match_all('/' . $level . ' SOUR (.*)((?:\n\d CONT.*)*)/', $factrec, $match, PREG_SET_ORDER);
438
        for ($j = 0; $j < $ct; $j++) {
439
            if (!str_contains($match[$j][1], '@')) {
440
                $source = e($match[$j][1] . preg_replace('/\n\d CONT ?/', "\n", $match[$j][2]));
441
                $data   .= '<div class="fact_SOUR"><span class="label">' . I18N::translate('Source') . ':</span> <span class="field" dir="auto">' . Filter::formatText($source, $tree) . '</span></div>';
442
            }
443
        }
444
        // Find source for each fact
445
        $ct    = preg_match_all("/$level SOUR @(.*)@/", $factrec, $match, PREG_SET_ORDER);
446
        $spos2 = 0;
447
        for ($j = 0; $j < $ct; $j++) {
448
            $sid    = $match[$j][1];
449
            $source = Registry::sourceFactory()->make($sid, $tree);
450
            if ($source) {
451
                if ($source->canShow()) {
452
                    $spos1 = strpos($factrec, "$level SOUR @" . $sid . '@', $spos2);
453
                    $spos2 = strpos($factrec, "\n$level", $spos1);
454
                    if (!$spos2) {
455
                        $spos2 = strlen($factrec);
456
                    }
457
                    $srec     = substr($factrec, $spos1, $spos2 - $spos1);
458
                    $lt       = preg_match_all("/$nlevel \w+/", $srec, $matches);
459
                    $data     .= '<div class="fact_SOUR">';
460
                    $id       = 'collapse-' . Uuid::uuid4()->toString();
461
                    $expanded = (bool) $tree->getPreference('EXPAND_SOURCES');
462
                    if ($lt > 0) {
463
                        $data .= '<a href="#' . e($id) . '" role="button" data-toggle="collapse" aria-controls="' . e($id) . '" aria-expanded="' . ($expanded ? 'true' : 'false') . '">';
464
                        $data .= view('icons/expand');
465
                        $data .= view('icons/collapse');
466
                        $data .= '</a> ';
467
                    }
468
                    $data .= GedcomTag::getLabelValue('SOUR', '<a href="' . e($source->url()) . '">' . $source->fullName() . '</a>', null, 'span');
469
                    $data .= '</div>';
470
471
                    $data .= '<div id="' . e($id) . '" class="collapse ' . ($expanded ? 'show' : '') . '">';
472
                    $data .= self::printSourceStructure($tree, self::getSourceStructure($srec));
473
                    $data .= '<div class="indent">';
474
                    ob_start();
475
                    self::printMediaLinks($tree, $srec, $nlevel);
476
                    $data .= ob_get_clean();
477
                    $data .= FunctionsPrint::printFactNotes($tree, $srec, $nlevel);
478
                    $data .= '</div>';
479
                    $data .= '</div>';
480
                }
481
            } else {
482
                $data .= GedcomTag::getLabelValue('SOUR', '<span class="error">' . $sid . '</span>');
483
            }
484
        }
485
486
        return $data;
487
    }
488
489
    /**
490
     * Print the links to media objects
491
     *
492
     * @param Tree   $tree
493
     * @param string $factrec
494
     * @param int    $level
495
     *
496
     * @return void
497
     */
498
    public static function printMediaLinks(Tree $tree, string $factrec, int $level): void
499
    {
500
        $nlevel = $level + 1;
501
        if (preg_match_all("/$level OBJE @(.*)@/", $factrec, $omatch, PREG_SET_ORDER) === 0) {
502
            return;
503
        }
504
        $objectNum = 0;
505
        while ($objectNum < count($omatch)) {
506
            $media_id = $omatch[$objectNum][1];
507
            $media    = Registry::mediaFactory()->make($media_id, $tree);
508
            if ($media) {
509
                if ($media->canShow()) {
510
                    echo '<div class="d-flex align-items-center"><div class="p-1">';
511
                    foreach ($media->mediaFiles() as $media_file) {
512
                        echo $media_file->displayImage(100, 100, 'contain', []);
513
                    }
514
                    echo '</div>';
515
                    echo '<div>';
516
                    echo '<a href="', e($media->url()), '">', $media->fullName(), '</a>';
517
                    // NOTE: echo the notes of the media
518
                    echo '<p>';
519
                    echo FunctionsPrint::printFactNotes($tree, $media->gedcom(), 1);
520
                    //-- print spouse name for marriage events
521
                    echo FunctionsPrint::printFactNotes($tree, $media->gedcom(), $nlevel);
522
                    echo self::printFactSources($tree, $media->gedcom(), $nlevel);
523
                    echo '</div>'; //close div "media-display-title"
524
                    echo '</div>'; //close div "media-display"
525
                }
526
            } elseif ($tree->getPreference('HIDE_GEDCOM_ERRORS') === '1') {
527
                echo '<p class="alert alert-danger">', $media_id, '</p>';
528
            }
529
            $objectNum++;
530
        }
531
    }
532
533
    /**
534
     * Print a row for the sources tab on the individual page.
535
     *
536
     * @param Fact $fact
537
     * @param int  $level
538
     *
539
     * @return void
540
     */
541
    public static function printMainSources(Fact $fact, int $level): void
542
    {
543
        $factrec = $fact->gedcom();
544
        $parent  = $fact->record();
545
        $tree    = $fact->record()->tree();
546
547
        $nlevel = $level + 1;
548
        if ($fact->isPendingAddition()) {
549
            $styleadd = 'wt-new';
550
            $can_edit = $level === 1 && $fact->canEdit();
551
        } elseif ($fact->isPendingDeletion()) {
552
            $styleadd = 'wt-old';
553
            $can_edit = false;
554
        } else {
555
            $styleadd = '';
556
            $can_edit = $level === 1 && $fact->canEdit();
557
        }
558
559
        // -- find source for each fact
560
        preg_match_all('/(?:^|\n)(' . $level . ' SOUR (.*)(?:\n[' . $nlevel . '-9] .*)*)/', $fact->gedcom(), $matches, PREG_SET_ORDER);
561
562
        foreach ($matches as $match) {
563
            $srec   = $match[1];
564
            $sid    = $match[2];
565
            $source = Registry::sourceFactory()->make(trim($sid, '@'), $tree);
566
            // Allow access to "1 SOUR @non_existent_source@", so it can be corrected/deleted
567
            if (!$source || $source->canShow()) {
568
                if ($level > 1) {
569
                    echo '<tr class="wt-level-two-source collapse">';
570
                } else {
571
                    echo '<tr>';
572
                }
573
                echo '<th class="';
574
                if ($level > 1) {
575
                    echo ' rela';
576
                }
577
                echo ' ', $styleadd, '">';
578
                $factlines = explode("\n", $factrec); // 1 BIRT Y\n2 SOUR ...
579
                $factwords = explode(' ', $factlines[0]); // 1 BIRT Y
580
                $factname  = $factwords[1]; // BIRT
581
                if ($factname === 'EVEN' || $factname === 'FACT') {
582
                    // Add ' EVEN' to provide sensible output for an event with an empty TYPE record
583
                    $ct = preg_match('/2 TYPE (.*)/', $factrec, $ematch);
584
                    if ($ct > 0) {
585
                        $factname = trim($ematch[1]);
586
                        echo $factname;
587
                    } else {
588
                        echo GedcomTag::getLabel($factname);
589
                    }
590
                } elseif ($can_edit) {
591
                    echo '<a href="' . e(route(EditFactPage::class, [
592
                            'xref'    => $parent->xref(),
593
                            'fact_id' => $fact->id(),
594
                            'tree'    => $tree->name(),
595
                        ])) . '" title="', I18N::translate('Edit'), '">';
596
                    echo GedcomTag::getLabel($factname), '</a>';
597
                    echo '<div class="editfacts nowrap">';
598
                    if (preg_match('/^@.+@$/', $sid)) {
599
                        // Inline sources can't be edited. Attempting to save one will convert it
600
                        // into a link, and delete it.
601
                        // e.g. "1 SOUR my source" becomes "1 SOUR @my source@" which does not exist.
602
                        echo view('edit/icon-fact-edit', ['fact' => $fact]);
603
                        echo view('edit/icon-fact-copy', ['fact' => $fact]);
604
                    }
605
                    echo view('edit/icon-fact-delete', ['fact' => $fact]);
606
                } else {
607
                    echo GedcomTag::getLabel($factname);
608
                }
609
                echo '</th>';
610
                echo '<td class="', $styleadd, '">';
611
                if ($source) {
612
                    echo '<a href="', e($source->url()), '">', $source->fullName(), '</a>';
613
                    // 2 RESN tags. Note, there can be more than one, such as "privacy" and "locked"
614
                    if (preg_match_all("/\n2 RESN (.+)/", $factrec, $rmatches)) {
615
                        foreach ($rmatches[1] as $rmatch) {
616
                            echo '<br><span class="label">', GedcomTag::getLabel('RESN'), ':</span> <span class="field">';
617
                            switch ($rmatch) {
618
                                case 'none':
619
                                    // Note: "2 RESN none" is not valid gedcom, and the GUI will not let you add it.
620
                                    // However, webtrees privacy rules will interpret it as "show an otherwise private fact to public".
621
                                    echo '<i class="icon-resn-none"></i> ', I18N::translate('Show to visitors');
622
                                    break;
623
                                case 'privacy':
624
                                    echo '<i class="icon-resn-privacy"></i> ', I18N::translate('Show to members');
625
                                    break;
626
                                case 'confidential':
627
                                    echo '<i class="icon-resn-confidential"></i> ', I18N::translate('Show to managers');
628
                                    break;
629
                                case 'locked':
630
                                    echo '<i class="icon-resn-locked"></i> ', I18N::translate('Only managers can edit');
631
                                    break;
632
                                default:
633
                                    echo $rmatch;
634
                                    break;
635
                            }
636
                            echo '</span>';
637
                        }
638
                    }
639
                    $cs = preg_match("/$nlevel EVEN (.*)/", $srec, $cmatch);
640
                    if ($cs > 0) {
641
                        echo '<br><span class="label">', GedcomTag::getLabel('EVEN'), ' </span><span class="field">', $cmatch[1], '</span>';
642
                        $cs = preg_match('/' . ($nlevel + 1) . ' ROLE (.*)/', $srec, $cmatch);
643
                        if ($cs > 0) {
644
                            echo '<br>&nbsp;&nbsp;&nbsp;&nbsp;<span class="label">', GedcomTag::getLabel('ROLE'), ' </span><span class="field">', $cmatch[1], '</span>';
645
                        }
646
                    }
647
                    echo self::printSourceStructure($tree, self::getSourceStructure($srec));
648
                    echo '<div class="indent">';
649
                    self::printMediaLinks($tree, $srec, $nlevel);
650
                    if ($nlevel === 2) {
651
                        self::printMediaLinks($tree, $source->gedcom(), 1);
652
                    }
653
                    echo FunctionsPrint::printFactNotes($tree, $srec, $nlevel);
654
                    if ($nlevel === 2) {
655
                        echo FunctionsPrint::printFactNotes($tree, $source->gedcom(), 1);
656
                    }
657
                    echo '</div>';
658
                } else {
659
                    echo $sid;
660
                }
661
                echo '</td></tr>';
662
            }
663
        }
664
    }
665
666
    /**
667
     * Print SOUR structure
668
     *  This function prints the input array of SOUR sub-records built by the
669
     *  getSourceStructure() function.
670
     *
671
     * @param Tree                $tree
672
     * @param string[]|string[][] $textSOUR
673
     *
674
     * @return string
675
     */
676
    public static function printSourceStructure(Tree $tree, array $textSOUR): string
677
    {
678
        $html = '';
679
680
        if ($textSOUR['PAGE'] !== '') {
681
            $html .= Registry::elementFactory()->make('INDI:SOUR:PAGE')->labelValue($textSOUR['PAGE'], $tree);
682
        }
683
684
        if ($textSOUR['EVEN'] !== '') {
685
            $html .= Registry::elementFactory()->make('INDI:SOUR:EVEN')->labelValue($textSOUR['EVEN'], $tree);
686
687
            if ($textSOUR['ROLE']) {
688
                $html .= Registry::elementFactory()->make('INDI:SOUR:EVEN:ROLE')->labelValue($textSOUR['ROLE'], $tree);
689
            }
690
        }
691
692
        if ($textSOUR['DATE'] !== '') {
693
            $html .= Registry::elementFactory()->make('INDI:SOUR:DATA:DATE')->labelValue($textSOUR['DATE'], $tree);
694
        }
695
696
        foreach ($textSOUR['TEXT'] as $text) {
697
            $html .= Registry::elementFactory()->make('INDI:SOUR:DATA:TEXT')->labelValue($text, $tree);
698
        }
699
700
        if ($textSOUR['QUAY'] !== '') {
701
            $html .= Registry::elementFactory()->make('INDI:SOUR:QUAY')->labelValue($textSOUR['QUAY'], $tree);
702
        }
703
704
        return '<div class="indent">' . $html . '</div>';
705
    }
706
707
    /**
708
     * Extract SOUR structure from the incoming Source sub-record
709
     * The output array is defined as follows:
710
     *  $textSOUR['PAGE'] = Source citation
711
     *  $textSOUR['EVEN'] = Event type
712
     *  $textSOUR['ROLE'] = Role in event
713
     *  $textSOUR['DATA'] = place holder (no text in this sub-record)
714
     *  $textSOUR['DATE'] = Entry recording date
715
     *  $textSOUR['TEXT'] = (array) Text from source
716
     *  $textSOUR['QUAY'] = Certainty assessment
717
     *
718
     * @param string $srec
719
     *
720
     * @return array<array<string>>
721
     */
722
    public static function getSourceStructure(string $srec): array
723
    {
724
        // Set up the output array
725
        $textSOUR = [
726
            'PAGE' => '',
727
            'EVEN' => '',
728
            'ROLE' => '',
729
            'DATE' => '',
730
            'TEXT' => [],
731
            'QUAY' => '',
732
        ];
733
734
        preg_match_all('/^\d (PAGE|EVEN|ROLE|DATE|TEXT|QUAY) ?(.*(\n\d CONT.*)*)$/m', $srec, $matches, PREG_SET_ORDER);
735
736
        foreach ($matches as $match) {
737
            $tag   = $match[1];
738
            $value = $match[2];
739
            $value = preg_replace('/\n\d CONT ?/', "\n", $value);
740
741
            if ($tag === 'TEXT') {
742
                $textSOUR[$tag][] = $value;
743
            } else {
744
                $textSOUR[$tag] = $value;
745
            }
746
        }
747
748
        return $textSOUR;
749
    }
750
751
    /**
752
     * Print a row for the notes tab on the individual page.
753
     *
754
     * @param Fact $fact
755
     * @param int  $level
756
     *
757
     * @return void
758
     */
759
    public static function printMainNotes(Fact $fact, int $level): void
760
    {
761
        $factrec = $fact->gedcom();
762
        $parent  = $fact->record();
763
        $tree    = $parent->tree();
764
765
        if ($fact->isPendingAddition()) {
766
            $styleadd = 'wt-new ';
767
            $can_edit = $level === 1 && $fact->canEdit();
768
        } elseif ($fact->isPendingDeletion()) {
769
            $styleadd = 'wt-old ';
770
            $can_edit = false;
771
        } else {
772
            $styleadd = '';
773
            $can_edit = $level === 1 && $fact->canEdit();
774
        }
775
776
        $ct = preg_match_all("/$level NOTE (.*)/", $factrec, $match, PREG_SET_ORDER);
777
        for ($j = 0; $j < $ct; $j++) {
778
            // Note object, or inline note?
779
            if (preg_match("/$level NOTE @(.*)@/", $match[$j][0], $nmatch)) {
780
                $note = Registry::noteFactory()->make($nmatch[1], $tree);
781
                if ($note && !$note->canShow()) {
782
                    continue;
783
                }
784
            } else {
785
                $note = null;
786
            }
787
788
            if ($level >= 2) {
789
                echo '<tr class="wt-level-two-note collapse"><th scope="row" class="rela ', $styleadd, '">';
790
            } else {
791
                echo '<tr><th scope="row" class="', $styleadd, '">';
792
            }
793
            if ($can_edit) {
794
                if ($level < 2) {
795
                    if ($note instanceof Note) {
796
                        echo '<a href="' . e($note->url()) . '">';
797
                        echo GedcomTag::getLabel('SHARED_NOTE');
798
                        echo view('icons/note');
799
                        echo '</a>';
800
                    } else {
801
                        echo GedcomTag::getLabel('NOTE');
802
                    }
803
                    echo '<div class="editfacts nowrap">';
804
                    echo view('edit/icon-fact-edit', ['fact' => $fact]);
805
                    echo view('edit/icon-fact-copy', ['fact' => $fact]);
806
                    echo view('edit/icon-fact-delete', ['fact' => $fact]);
807
                    echo '</div>';
808
                }
809
            } else {
810
                if ($level < 2) {
811
                    if ($note) {
812
                        echo GedcomTag::getLabel('SHARED_NOTE');
813
                    } else {
814
                        echo GedcomTag::getLabel('NOTE');
815
                    }
816
                }
817
                $factlines = explode("\n", $factrec); // 1 BIRT Y\n2 NOTE ...
818
                $factwords = explode(' ', $factlines[0]); // 1 BIRT Y
819
                $factname  = $factwords[1]; // BIRT
820
                if ($factname === 'EVEN' || $factname === 'FACT') {
821
                    // Add ' EVEN' to provide sensible output for an event with an empty TYPE record
822
                    $ct = preg_match('/2 TYPE (.*)/', $factrec, $ematch);
823
                    if ($ct > 0) {
824
                        $factname = trim($ematch[1]);
825
                        echo $factname;
826
                    } else {
827
                        echo GedcomTag::getLabel($factname);
828
                    }
829
                } elseif ($factname !== 'NOTE') {
830
                    // Note is already printed
831
                    echo GedcomTag::getLabel($factname);
832
                    if ($note) {
833
                        echo '<a class="btn btn-link" href="' . e($note->url()) . '" title="' . I18N::translate('View') . '"><span class="sr-only">' . I18N::translate('View') . '</span></a>';
834
                    }
835
                }
836
            }
837
            echo '</th>';
838
            if ($note) {
839
                // Note objects
840
                $text = Filter::formatText($note->getNote(), $tree);
841
            } else {
842
                // Inline notes
843
                $nrec = Functions::getSubRecord($level, "$level NOTE", $factrec, $j + 1);
844
                $text = $match[$j][1] . Functions::getCont($level + 1, $nrec);
845
                $text = Filter::formatText($text, $tree);
846
            }
847
848
            echo '<td class="', $styleadd, ' wrap">';
849
            echo $text;
850
851
            // 2 RESN tags. Note, there can be more than one, such as "privacy" and "locked"
852
            if (preg_match_all("/\n2 RESN (.+)/", $factrec, $rmatches)) {
853
                foreach ($rmatches[1] as $rmatch) {
854
                    echo '<br><span class="label">', GedcomTag::getLabel('RESN'), ':</span> <span class="field">';
855
                    switch ($rmatch) {
856
                        case 'none':
857
                            // Note: "2 RESN none" is not valid gedcom, and the GUI will not let you add it.
858
                            // However, webtrees privacy rules will interpret it as "show an otherwise private fact to public".
859
                            echo '<i class="icon-resn-none"></i> ', I18N::translate('Show to visitors');
860
                            break;
861
                        case 'privacy':
862
                            echo '<i class="icon-resn-privacy"></i> ', I18N::translate('Show to members');
863
                            break;
864
                        case 'confidential':
865
                            echo '<i class="icon-resn-confidential"></i> ', I18N::translate('Show to managers');
866
                            break;
867
                        case 'locked':
868
                            echo '<i class="icon-resn-locked"></i> ', I18N::translate('Only managers can edit');
869
                            break;
870
                        default:
871
                            echo $rmatch;
872
                            break;
873
                    }
874
                    echo '</span>';
875
                }
876
            }
877
            echo '</td></tr>';
878
        }
879
    }
880
881
    /**
882
     * Print a row for the media tab on the individual page.
883
     *
884
     * @param Fact $fact
885
     * @param int  $level
886
     *
887
     * @return void
888
     */
889
    public static function printMainMedia(Fact $fact, int $level): void
890
    {
891
        $tree = $fact->record()->tree();
892
893
        if ($fact->isPendingAddition()) {
894
            $styleadd = 'wt-new';
895
        } elseif ($fact->isPendingDeletion()) {
896
            $styleadd = 'wt-old';
897
        } else {
898
            $styleadd = '';
899
        }
900
901
        // -- find source for each fact
902
        preg_match_all('/(?:^|\n)' . $level . ' OBJE @(.*)@/', $fact->gedcom(), $matches);
903
        foreach ($matches[1] as $xref) {
904
            $media = Registry::mediaFactory()->make($xref, $tree);
905
            // Allow access to "1 OBJE @non_existent_source@", so it can be corrected/deleted
906
            if (!$media instanceof Media || $media->canShow()) {
907
                echo '<tr class="', $styleadd, '">';
908
                echo '<th scope="row">';
909
                echo $fact->label();
910
911
                if ($level === 1 && $fact->canEdit()) {
912
                    echo '<div class="editfacts nowrap">';
913
                    echo view('edit/icon-fact-copy', ['fact' => $fact]);
914
                    echo view('edit/icon-fact-delete', ['fact' => $fact]);
915
                    echo '</div>';
916
                }
917
918
                echo '</th>';
919
                echo '<td>';
920
                if ($media instanceof Media) {
921
                    foreach ($media->mediaFiles() as $media_file) {
922
                        echo '<div>';
923
                        echo $media_file->displayImage(100, 100, 'contain', []);
924
                        echo '<br>';
925
                        echo '<a href="' . e($media->url()) . '"> ';
926
                        echo '<em>';
927
                        echo e($media_file->title() ?: $media_file->filename());
928
                        echo '</em>';
929
                        echo '</a>';
930
                        echo '</div>';
931
                    }
932
933
                    echo FunctionsPrint::printFactNotes($tree, $media->gedcom(), 1);
934
                    echo self::printFactSources($tree, $media->gedcom(), 1);
935
                } else {
936
                    echo $xref;
937
                }
938
                echo '</td></tr>';
939
            }
940
        }
941
    }
942
}
943