Name::__construct()   A
last analyzed

Complexity

Conditions 5
Paths 9

Size

Total Lines 30
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 16
nc 9
nop 2
dl 0
loc 30
rs 9.4222
c 0
b 0
f 0
1
<?php
2
/*
3
 * citeproc-php
4
 *
5
 * @link        http://github.com/seboettg/citeproc-php for the source repository
6
 * @copyright   Copyright (c) 2016 Sebastian Böttger.
7
 * @license     https://opensource.org/licenses/MIT
8
 */
9
10
namespace Seboettg\CiteProc\Rendering\Name;
11
12
use Seboettg\CiteProc\CiteProc;
13
use Seboettg\CiteProc\Exception\CiteProcException;
14
use Seboettg\CiteProc\Exception\InvalidStylesheetException;
15
use Seboettg\CiteProc\Rendering\HasParent;
16
use Seboettg\CiteProc\Style\InheritableNameAttributesTrait;
17
use Seboettg\CiteProc\Style\Options\DemoteNonDroppingParticle;
18
use Seboettg\CiteProc\Style\Options\SubsequentAuthorSubstituteRule;
19
use Seboettg\CiteProc\Styles\AffixesTrait;
20
use Seboettg\CiteProc\Styles\DelimiterTrait;
21
use Seboettg\CiteProc\Styles\FormattingTrait;
22
use Seboettg\CiteProc\Util\CiteProcHelper;
23
use Seboettg\CiteProc\Util\Factory;
24
use Seboettg\CiteProc\Util\NameHelper;
25
use Seboettg\CiteProc\Util\StringHelper;
26
use SimpleXMLElement;
27
use stdClass;
28
29
/**
30
 * Class Name
31
 *
32
 * The cs:name element, an optional child element of cs:names, can be used to describe the formatting of individual
33
 * names, and the separation of names within a name variable.
34
 *
35
 * @package Seboettg\CiteProc\Rendering\Name
36
 *
37
 * @author Sebastian Böttger <[email protected]>
38
 */
39
class Name implements HasParent
40
{
41
    use InheritableNameAttributesTrait,
0 ignored issues
show
Bug introduced by
The trait Seboettg\CiteProc\Styles\AffixesTrait requires the property $single which is not provided by Seboettg\CiteProc\Rendering\Name\Name.
Loading history...
42
        FormattingTrait,
43
        AffixesTrait,
44
        DelimiterTrait;
45
46
    /**
47
     * @var array
48
     */
49
    protected $nameParts;
50
51
    /**
52
     * Specifies the text string used to separate names in a name variable. Default is ”, ” (e.g. “Doe, Smith”).
53
     *
54
     * @var
55
     */
56
    private $delimiter = ", ";
57
58
    /**
59
     * @var Names
60
     */
61
    private $parent;
62
63
    /**
64
     * @var SimpleXMLElement
65
     */
66
    private $node;
67
68
    /**
69
     * @var string
70
     */
71
    private $etAl;
72
73
    /**
74
     * @var string
75
     */
76
    private $variable;
77
78
    /**
79
     * Name constructor.
80
     *
81
     * @param  SimpleXMLElement $node
82
     * @param  Names            $parent
83
     * @throws InvalidStylesheetException
84
     */
85
    public function __construct(SimpleXMLElement $node, Names $parent)
86
    {
87
        $this->node = $node;
88
        $this->parent = $parent;
89
90
        $this->nameParts = [];
91
92
        /**
93
         * @var SimpleXMLElement $child
94
*/
95
        foreach ($node->children() as $child) {
96
            switch ($child->getName()) {
97
                case "name-part":
98
                    /** @var NamePart $namePart */
99
                    $namePart = Factory::create($child, $this);
100
                    $this->nameParts[$namePart->getName()] = $namePart;
101
            }
102
        }
103
104
        foreach ($node->attributes() as $attribute) {
105
            switch ($attribute->getName()) {
106
                case 'form':
107
                    $this->form = (string) $attribute;
108
                    break;
109
            }
110
        }
111
112
        $this->initFormattingAttributes($node);
113
        $this->initAffixesAttributes($node);
114
        $this->initDelimiterAttributes($node);
115
    }
116
117
    /**
118
     * @param  stdClass     $data
119
     * @param  string       $var
120
     * @param  integer|null $citationNumber
121
     * @return string
122
     * @throws CiteProcException
123
     */
124
    public function render($data, $var, $citationNumber = null)
125
    {
126
        $this->variable = $var;
127
        $name = $data->{$var};
128
        if (!$this->attributesInitialized) {
129
            $this->initInheritableNameAttributes($this->node);
130
        }
131
        if ("text" === $this->and) {
132
            $this->and = CiteProc::getContext()->getLocale()->filter('terms', 'and')->single;
133
        } elseif ('symbol' === $this->and) {
134
            $this->and = '&#38;';
135
        }
136
137
        $resultNames = $this->handleSubsequentAuthorSubstitution($name, $citationNumber);
138
139
        if (empty($resultNames)) {
140
            return CiteProc::getContext()->getCitationData()->getSubsequentAuthorSubstitute();
141
        }
142
143
        $resultNames = $this->prepareAbbreviation($resultNames);
144
145
        /* When set to “true” (the default is “false”), name lists truncated by et-al abbreviation are followed by
146
        the name delimiter, the ellipsis character, and the last name of the original name list. This is only
147
        possible when the original name list has at least two more names than the truncated name list (for this
148
        the value of et-al-use-first/et-al-subsequent-min must be at least 2 less than the value of
149
        et-al-min/et-al-subsequent-use-first). */
150
        if ($this->etAlUseLast) {
151
            $this->and = "…"; // set "and"
152
            $this->etAl = null; //reset $etAl;
153
        }
154
155
        /* add "and" */
156
        $this->addAnd($resultNames);
157
158
        $text = $this->renderDelimiterPrecedesLast($resultNames);
159
160
        if (empty($text)) {
161
            $text = implode($this->delimiter, $resultNames);
162
        }
163
164
        $text = $this->appendEtAl($name, $text, $resultNames);
165
166
        /* A third value, “count”, returns the total number of names that would otherwise be rendered by the use of the
167
        cs:names element (taking into account the effects of et-al abbreviation and editor/translator collapsing),
168
        which allows for advanced sorting. */
169
        if ($this->form == 'count') {
170
            return (int) count($resultNames);
171
        }
172
173
        return $text;
174
    }
175
176
    /**
177
     * @param  stdClass $nameItem
178
     * @param  int      $rank
179
     * @return string
180
     * @throws CiteProcException
181
     */
182
    private function formatName($nameItem, $rank)
183
    {
184
        $nameObj = $this->cloneNamePOSC($nameItem);
185
186
        $useInitials = $this->initialize && !is_null($this->initializeWith) && $this->initializeWith !== false;
187
        if ($useInitials && isset($nameItem->given)) {
188
            $nameObj->given = StringHelper::initializeBySpaceOrHyphen($nameItem->given, $this->initializeWith);
189
        }
190
191
        $renderedResult = $this->getNamesString($nameObj, $rank);
192
        CiteProcHelper::applyAdditionMarkupFunction($nameItem, $this->parent->getVariables()->first(), $renderedResult);
193
        return trim($renderedResult);
194
    }
195
196
    /**
197
     * @param  stdClass $name
198
     * @param  int      $rank
199
     * @return string
200
     * @throws CiteProcException
201
     */
202
    private function getNamesString($name, $rank)
203
    {
204
        $text = "";
205
206
        if (!isset($name->family)) {
207
            return $text;
208
        }
209
210
        $text = $this->nameOrder($name, $rank);
211
212
        //contains nbsp prefixed by normal space or followed by normal space?
213
        $text = htmlentities($text);
214
        if (strpos($text, " &nbsp;") !== false || strpos($text, "&nbsp; ") !== false) {
215
            $text = preg_replace("/[\s]+/", "", $text); //remove normal spaces
216
            return preg_replace("/&nbsp;+/", " ", $text);
217
        }
218
        $text = html_entity_decode(preg_replace("/[\s]+/", " ", $text));
219
        return $this->format(trim($text));
220
    }
221
222
    /**
223
     * @param  stdClass $name
224
     * @return stdClass
225
     */
226
    private function cloneNamePOSC($name)
227
    {
228
        $nameObj = new stdClass();
229
        if (isset($name->family)) {
230
            $nameObj->family = $name->family;
231
        }
232
        if (isset($name->given)) {
233
            $nameObj->given = $name->given;
234
        }
235
        if (isset($name->{'non-dropping-particle'})) {
236
            $nameObj->{'non-dropping-particle'} = $name->{'non-dropping-particle'};
237
        }
238
        if (isset($name->{'dropping-particle'})) {
239
            $nameObj->{'dropping-particle'} = $name->{'dropping-particle'};
240
        }
241
        if (isset($name->{'suffix'})) {
242
            $nameObj->{'suffix'} = $name->{'suffix'};
243
        }
244
        return $nameObj;
245
    }
246
247
    /**
248
     * @param  $data
249
     * @param  $text
250
     * @param  $resultNames
251
     * @return string
252
     */
253
    protected function appendEtAl($data, $text, $resultNames)
254
    {
255
        //append et al abbreviation
256
        if (count($data) > 1
257
            && !empty($resultNames)
258
            && !empty($this->etAl)
259
            && !empty($this->etAlMin)
260
            && !empty($this->etAlUseFirst)
261
            && count($data) != count($resultNames)
262
        ) {
263
            /* By default, when a name list is truncated to a single name, the name and the “et-al” (or “and others”)
264
            term are separated by a space (e.g. “Doe et al.”). When a name list is truncated to two or more names, the
265
            name delimiter is used (e.g. “Doe, Smith, et al.”). This behavior can be changed with the
266
            delimiter-precedes-et-al attribute. */
267
268
            switch ($this->delimiterPrecedesEtAl) {
269
                case 'never':
270
                    $text = $text . " " . $this->etAl;
271
                    break;
272
                case 'always':
273
                    $text = $text . $this->delimiter . $this->etAl;
274
                    break;
275
                case 'contextual':
276
                default:
277
                    if (count($resultNames) === 1) {
278
                        $text .= " " . $this->etAl;
279
                    } else {
280
                        $text .= $this->delimiter . $this->etAl;
281
                    }
282
            }
283
        }
284
        return $text;
285
    }
286
287
    /**
288
     * @param  $resultNames
289
     * @return array
290
     */
291
    protected function prepareAbbreviation($resultNames)
292
    {
293
        $cnt = count($resultNames);
294
        /* Use of et-al-min and et-al-user-first enables et-al abbreviation. If the number of names in a name variable
295
        matches or exceeds the number set on et-al-min, the rendered name list is truncated after reaching the number of
296
        names set on et-al-use-first.  */
297
298
        if (isset($this->etAlMin) && isset($this->etAlUseFirst)) {
299
            if ($this->etAlMin <= $cnt) {
300
                if ($this->etAlUseLast && $this->etAlMin - $this->etAlUseFirst >= 2) {
301
                    /* et-al-use-last: When set to “true” (the default is “false”), name lists truncated by et-al
302
                    abbreviation are followed by the name delimiter, the ellipsis character, and the last name of the
303
                    original name list. This is only possible when the original name list has at least two more names
304
                    than the truncated name list (for this the value of et-al-use-first/et-al-subsequent-min must be at
305
                    least 2 less than the value of et-al-min/et-al-subsequent-use-first).*/
306
307
                    $lastName = array_pop($resultNames); //remove last Element and remember in $lastName
308
                }
309
                for ($i = $this->etAlUseFirst; $i < $cnt; ++$i) {
310
                    unset($resultNames[$i]);
311
                }
312
313
                $resultNames = array_values($resultNames);
314
315
                if (!empty($lastName)) { // append $lastName if exist
316
                    $resultNames[] = $lastName;
317
                }
318
319
                if ($this->parent->hasEtAl()) {
320
                    $this->etAl = $this->parent->getEtAl()->render(null);
321
                    return $resultNames;
322
                } else {
323
                    $this->etAl = CiteProc::getContext()->getLocale()->filter('terms', 'et-al')->single;
324
                    return $resultNames;
325
                }
326
            }
327
            return $resultNames;
328
        }
329
        return $resultNames;
330
    }
331
332
    /**
333
     * @param  $data
334
     * @param  stdClass $preceding
335
     * @return array
336
     * @throws CiteProcException
337
     */
338
    protected function renderSubsequentSubstitution($data, $preceding)
339
    {
340
        $resultNames = [];
341
        $subsequentSubstitution = CiteProc::getContext()->getCitationData()->getSubsequentAuthorSubstitute();
342
        $subsequentSubstitutionRule = CiteProc::getContext()->getCitationData()->getSubsequentAuthorSubstituteRule();
343
344
        /**
345
         * @var string $type
346
         * @var stdClass $name
347
         */
348
        foreach ($data as $rank => $name) {
349
            switch ($subsequentSubstitutionRule) {
350
                /* “partial-each” - when one or more rendered names in the name variable match those in the preceding
351
                bibliographic entry, the value of subsequent-author-substitute substitutes for each matching name.
352
                Matching starts with the first name, and continues up to the first mismatch. */
353
                case SubsequentAuthorSubstituteRule::PARTIAL_EACH:
354
                    if (NameHelper::precedingHasAuthor($preceding, $name)) {
355
                        $resultNames[] = $subsequentSubstitution;
356
                    } else {
357
                        $resultNames[] = $this->formatName($name, $rank);
358
                    }
359
                    break;
360
                 /* “partial-first” - as “partial-each”, but substitution is limited to the first name of the name
361
                variable. */
362
                case SubsequentAuthorSubstituteRule::PARTIAL_FIRST:
363
                    if ($rank === 0) {
364
                        if ($preceding->author[0]->family === $name->family) {
365
                            $resultNames[] = $subsequentSubstitution;
366
                        } else {
367
                            $resultNames[] = $this->formatName($name, $rank);
368
                        }
369
                    } else {
370
                        $resultNames[] = $this->formatName($name, $rank);
371
                    }
372
                    break;
373
374
                 /* “complete-each” - requires a complete match like “complete-all”, but now the value of
375
                subsequent-author-substitute substitutes for each rendered name. */
376
                case SubsequentAuthorSubstituteRule::COMPLETE_EACH:
377
                    try {
378
                        if (NameHelper::identicalAuthors($preceding, $data)) {
379
                            $resultNames[] = $subsequentSubstitution;
380
                        } else {
381
                            $resultNames[] = $this->formatName($name, $rank);
382
                        }
383
                    } catch (CiteProcException $e) {
384
                        $resultNames[] = $this->formatName($name, $rank);
385
                    }
386
                    break;
387
            }
388
        }
389
        return $resultNames;
390
    }
391
392
    /**
393
     * @throws CiteProcException
394
     */
395
    private function handleSubsequentAuthorSubstitution($data, $citationNumber): array
396
    {
397
        $hasPreceding = CiteProc::getContext()->getCitationData()->has($citationNumber - 1);
398
        $subsequentSubstitution = CiteProc::getContext()->getCitationData()->getSubsequentAuthorSubstitute();
399
        $subsequentSubstitutionRule = CiteProc::getContext()->getCitationData()->getSubsequentAuthorSubstituteRule();
400
        $preceding = CiteProc::getContext()->getCitationData()->get($citationNumber - 1);
401
402
403
        if ($hasPreceding && !is_null($subsequentSubstitution) && !empty($subsequentSubstitutionRule)) {
404
            /**
405
             * @var stdClass $preceding
406
             */
407
            if ($subsequentSubstitutionRule == SubsequentAuthorSubstituteRule::COMPLETE_ALL) {
0 ignored issues
show
introduced by
The condition $subsequentSubstitutionR...ituteRule::COMPLETE_ALL is always false.
Loading history...
408
                try {
409
                    if (NameHelper::identicalAuthors($preceding, $data)) {
410
                        return [];
411
                    } else {
412
                        $resultNames = $this->getFormattedNames($data);
413
                    }
414
                } catch (CiteProcException $e) {
415
                    $resultNames = $this->getFormattedNames($data);
416
                }
417
            } else {
418
                $resultNames = $this->renderSubsequentSubstitution($data, $preceding);
419
            }
420
        } else {
421
            $resultNames = $this->getFormattedNames($data);
422
        }
423
        return $resultNames;
424
    }
425
426
427
    /**
428
     * @param  array $data
429
     * @return array
430
     * @throws CiteProcException
431
     */
432
    protected function getFormattedNames($data)
433
    {
434
        $resultNames = [];
435
        foreach ($data as $rank => $name) {
436
            $formatted = $this->formatName($name, $rank);
437
            $resultNames[] = NameHelper::addExtendedMarkup($this->variable, $name, $formatted);
438
        }
439
        return $resultNames;
440
    }
441
442
    /**
443
     * @param  $resultNames
444
     * @return string
445
     */
446
    protected function renderDelimiterPrecedesLastNever($resultNames)
447
    {
448
        $text = "";
449
        if (!$this->etAlUseLast) {
450
            if (count($resultNames) === 1) {
451
                $text = $resultNames[0];
452
            } elseif (count($resultNames) === 2) {
453
                $text = implode(" ", $resultNames);
454
            } else { // >2
455
                $lastName = array_pop($resultNames);
456
                $text = implode($this->delimiter, $resultNames)." ".$lastName;
457
            }
458
        }
459
        return $text;
460
    }
461
462
    /**
463
     * @param  $resultNames
464
     * @return string
465
     */
466
    protected function renderDelimiterPrecedesLastContextual($resultNames)
467
    {
468
        if (count($resultNames) === 1) {
469
            $text = $resultNames[0];
470
        } elseif (count($resultNames) === 2) {
471
            $text = implode(" ", $resultNames);
472
        } else {
473
            $text = implode($this->delimiter, $resultNames);
474
        }
475
        return $text;
476
    }
477
478
    /**
479
     * @param $resultNames
480
     */
481
    protected function addAnd(&$resultNames)
482
    {
483
        $count = count($resultNames);
484
        if (!empty($this->and) && $count > 1 && empty($this->etAl)) {
485
            $new = $this->and.' '.end($resultNames); // add and-prefix of the last name if "and" is defined
486
            // set prefixed last name at the last position of $resultNames array
487
            $resultNames[count($resultNames) - 1] = $new;
488
        }
489
    }
490
491
    /**
492
     * @param  $resultNames
493
     * @return array|string
494
     */
495
    protected function renderDelimiterPrecedesLast($resultNames)
496
    {
497
        $text = "";
498
        if (!empty($this->and) && empty($this->etAl)) {
499
            switch ($this->delimiterPrecedesLast) {
500
                case 'after-inverted-name':
501
                    //TODO: implement
502
                    break;
503
                case 'always':
504
                    $text = implode($this->delimiter, $resultNames);
505
                    break;
506
                case 'never':
507
                    $text = $this->renderDelimiterPrecedesLastNever($resultNames);
508
                    break;
509
                case 'contextual':
510
                default:
511
                    $text = $this->renderDelimiterPrecedesLastContextual($resultNames);
512
            }
513
        }
514
        return $text;
515
    }
516
517
518
    /**
519
     * @param stdClass $data
520
     * @param integer  $rank
521
     *
522
     * @return string
523
     * @throws CiteProcException
524
     */
525
    private function nameOrder($data, $rank)
526
    {
527
        $nameAsSortOrder = (($this->nameAsSortOrder === "first" && $rank === 0) || $this->nameAsSortOrder === "all");
528
        $demoteNonDroppingParticle = CiteProc::getContext()->getGlobalOptions()->getDemoteNonDroppingParticles();
529
        $normalizedName = NameHelper::normalizeName($data);
530
        if (StringHelper::isLatinString($normalizedName) || StringHelper::isCyrillicString($normalizedName)) {
531
            if ($this->form === "long"
532
                && $nameAsSortOrder
533
                && ((string) $demoteNonDroppingParticle === DemoteNonDroppingParticle::NEVER
534
                || (string) $demoteNonDroppingParticle === DemoteNonDroppingParticle::SORT_ONLY)
535
            ) {
536
                // [La] [Fontaine], [Jean] [de], [III]
537
                NameHelper::prependParticleTo($data, "family", "non-dropping-particle");
538
                NameHelper::appendParticleTo($data, "given", "dropping-particle");
539
540
                list($family, $given) = $this->renderNameParts($data);
541
542
                $text = $family.(!empty($given) ? $this->sortSeparator.$given : "");
543
                $text .= !empty($data->suffix) ? $this->sortSeparator.$data->suffix : "";
544
            } elseif ($this->form === "long"
545
                && $nameAsSortOrder
546
                && (is_null($demoteNonDroppingParticle)
547
                || (string) $demoteNonDroppingParticle === DemoteNonDroppingParticle::DISPLAY_AND_SORT)
548
            ) {
549
                // [Fontaine], [Jean] [de] [La], [III]
550
                NameHelper::appendParticleTo($data, "given", "dropping-particle");
551
                NameHelper::appendParticleTo($data, "given", "non-dropping-particle");
552
                list($family, $given) = $this->renderNameParts($data);
553
                $text = $family;
554
                $text .= !empty($given) ? $this->sortSeparator.$given : "";
555
                $text .= !empty($data->suffix) ? $this->sortSeparator.$data->suffix : "";
556
            } elseif ($this->form === "long" && $nameAsSortOrder && empty($demoteNonDroppingParticle)) {
557
                list($family, $given) = $this->renderNameParts($data);
558
                $text = $family;
559
                $text .= !empty($given) ? $this->delimiter.$given : "";
560
                $text .= !empty($data->suffix) ? $this->sortSeparator.$data->suffix : "";
561
            } elseif ($this->form === "short") {
562
                // [La] [Fontaine]
563
                NameHelper::prependParticleTo($data, "family", "non-dropping-particle");
564
                $text = $data->family;
565
            } else {// form "long" (default)
566
                // [Jean] [de] [La] [Fontaine] [III]
567
                NameHelper::prependParticleTo($data, "family", "non-dropping-particle");
568
                NameHelper::prependParticleTo($data, "family", "dropping-particle");
569
                NameHelper::appendParticleTo($data, "family", "suffix");
570
                list($family, $given) = $this->renderNameParts($data);
571
                $text = !empty($given) ? $given." ".$family : $family;
572
            }
573
        } elseif (StringHelper::isAsianString(NameHelper::normalizeName($data))) {
574
            $text = $this->form === "long" ? $data->family . $data->given : $data->family;
575
        } else {
576
            $text = $this->form === "long" ? $data->family . " " . $data->given : $data->family;
577
        }
578
        return $text;
579
    }
580
581
    /**
582
     * @param  $data
583
     * @return array
584
     */
585
    private function renderNameParts($data)
586
    {
587
        $given = "";
588
        if (array_key_exists("family", $this->nameParts)) {
589
            $family = $this->nameParts["family"]->render($data);
590
        } else {
591
            $family = $data->family;
592
        }
593
        if (isset($data->given)) {
594
            if (array_key_exists("given", $this->nameParts)) {
595
                $given = $this->nameParts["given"]->render($data);
596
            } else {
597
                $given = $data->given;
598
            }
599
        }
600
        return [$family, $given];
601
    }
602
603
604
    /**
605
     * @return string
606
     */
607
    public function getForm()
608
    {
609
        return $this->form;
610
    }
611
612
    /**
613
     * @return string
614
     */
615
    public function isNameAsSortOrder()
616
    {
617
        return $this->nameAsSortOrder;
618
    }
619
620
    /**
621
     * @return string
622
     */
623
    public function getDelimiter()
624
    {
625
        return $this->delimiter;
626
    }
627
628
    /**
629
     * @param mixed $delimiter
630
     */
631
    public function setDelimiter($delimiter)
632
    {
633
        $this->delimiter = $delimiter;
634
    }
635
636
    /**
637
     * @return Names
638
     */
639
    public function getParent()
640
    {
641
        return $this->parent;
642
    }
643
}
644