Passed
Push — new-api ( 4bfe18...7ec1cc )
by Sebastian
05:06
created

Name::isNameAsSortOrder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
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\Config\RenderingMode;
14
use Seboettg\CiteProc\Exception\CiteProcException;
15
use Seboettg\CiteProc\Exception\InvalidStylesheetException;
16
use Seboettg\CiteProc\Rendering\HasParent;
17
use Seboettg\CiteProc\Style\Options\NameOptions;
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 FormattingTrait,
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
        AffixesTrait,
43
        DelimiterTrait;
44
45
    /**
46
     * @var array
47
     */
48
    protected $nameParts;
49
50
    /**
51
     * Specifies the text string used to separate names in a name variable. Default is ”, ” (e.g. “Doe, Smith”).
52
     *
53
     * @var
54
     */
55
    private $delimiter = ", ";
56
57
    /**
58
     * @var Names
59
     */
60
    private $parent;
61
62
    /**
63
     * @var SimpleXMLElement
64
     */
65
    private $node;
66
67
    /**
68
     * @var string
69
     */
70
    private $etAl;
71
72
    /**
73
     * @var string
74
     */
75
    private $variable;
76
77
    /** @var NameOptions[] */
78
    private $nameOptionsArray;
79
80
    /** @var NameOptions|null */
81
    private $nameOptions;
82
83
    /** @var string */
84
    private $and;
85
86
    /** @var NameOrderRenderer */
87
    private $nameOrderRenderer;
88
89
    /**
90
     * Name constructor.
91
     *
92
     * @param  SimpleXMLElement $node
93
     * @param  Names            $parent
94
     * @throws InvalidStylesheetException
95
     */
96 118
    public function __construct(SimpleXMLElement $node, Names $parent)
97
    {
98 118
        $this->node = $node;
99 118
        $this->parent = $parent;
100
101 118
        $this->nameParts = [];
102
103 118
        foreach ($node->children() as $child) {
104 3
            switch ($child->getName()) {
105 3
                case "name-part":
106
                    /** @var NamePart $namePart */
107 3
                    $namePart = Factory::create($child, $this);
108 3
                    $this->nameParts[$namePart->getName()] = $namePart;
109
            }
110
        }
111
112 118
        $this->nameOptionsArray[RenderingMode::CITATION] =
113 118
            NameOptions::updateNameOptions($node, null, $parent->getNameOptions(RenderingMode::CITATION()));
114 118
        $this->nameOptionsArray[RenderingMode::BIBLIOGRAPHY] =
115 118
            NameOptions::updateNameOptions($node, null, $parent->getNameOptions(RenderingMode::BIBLIOGRAPHY()));
116
117 118
        $this->initFormattingAttributes($node);
118 118
        $this->initAffixesAttributes($node);
119 118
        $this->initDelimiterAttributes($node);
120 118
        $this->nameOrderRenderer = new NameOrderRenderer(
121 118
            CiteProc::getContext()->getGlobalOptions(),
122 118
            $this->nameParts,
123 118
            $this->delimiter
124
        );
125 118
    }
126
127
    /**
128
     * @param  stdClass     $data
129
     * @param  string       $var
130
     * @param  integer|null $citationNumber
131
     * @return string
132
     * @throws CiteProcException
133
     */
134 105
    public function render($data, $var, $citationNumber = null)
135
    {
136 105
        $this->nameOptions = $this->nameOptionsArray[(string)CiteProc::getContext()->getMode()];
137 105
        $this->nameOrderRenderer->setNameOptions($this->nameOptions);
138 105
        $this->delimiter = $this->nameOptions->getNameDelimiter() ?? $this->delimiter;
139 105
        $this->variable = $var;
140 105
        $name = $data->{$var};
141 105
        if ("text" === $this->nameOptions->getAnd()) {
142 23
            $this->and = CiteProc::getContext()->getLocale()->filter('terms', 'and')->single;
143 83
        } elseif ('symbol' === $this->nameOptions->getAnd()) {
144 30
            $this->and = '&#38;';
145
        }
146
147 105
        $resultNames = $this->handleSubsequentAuthorSubstitution($name, $citationNumber);
148
149 105
        if (empty($resultNames)) {
150 2
            return CiteProc::getContext()->getCitationData()->getSubsequentAuthorSubstitute();
151
        }
152
153 105
        $resultNames = $this->prepareAbbreviation($resultNames);
154
155
        /* When set to “true” (the default is “false”), name lists truncated by et-al abbreviation are followed by
156
        the name delimiter, the ellipsis character, and the last name of the original name list. This is only
157
        possible when the original name list has at least two more names than the truncated name list (for this
158
        the value of et-al-use-first/et-al-subsequent-min must be at least 2 less than the value of
159
        et-al-min/et-al-subsequent-use-first). */
160 105
        if ("symbol" !== $this->nameOptions->getAnd() && $this->nameOptions->isEtAlUseLast()) {
161 3
            $this->and = "…"; // set "and"
162 3
            $this->etAl = null; //reset $etAl;
163
        }
164
165
        /* add "and" */
166 105
        $this->addAnd($resultNames);
167
168 105
        $text = $this->renderDelimiterPrecedesLast($resultNames);
169
170 105
        if (empty($text)) {
171 65
            $text = implode($this->delimiter, $resultNames);
172
        }
173
174 105
        $text = $this->appendEtAl($name, $text, $resultNames);
175
176
        /* A third value, “count”, returns the total number of names that would otherwise be rendered by the use of the
177
        cs:names element (taking into account the effects of et-al abbreviation and editor/translator collapsing),
178
        which allows for advanced sorting. */
179 105
        if ($this->nameOptions->getForm() == 'count') {
180 5
            return (int) count($resultNames);
181
        }
182
183 100
        return $text;
184
    }
185
186
    /**
187
     * @param  stdClass $nameItem
188
     * @param  int      $rank
189
     * @return string
190
     * @throws CiteProcException
191
     */
192 105
    private function formatName($nameItem, $rank)
193
    {
194 105
        $nameObj = $this->cloneNamePOSC($nameItem);
195
196 105
        $useInitials = $this->nameOptions->isInitialize() &&
0 ignored issues
show
Bug introduced by
The method isInitialize() does not exist on null. ( Ignorable by Annotation )

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

196
        $useInitials = $this->nameOptions->/** @scrutinizer ignore-call */ isInitialize() &&

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
197 105
            !is_null($this->nameOptions->getInitializeWith()) && $this->nameOptions->getInitializeWith() !== false;
198 105
        if ($useInitials && isset($nameItem->given)) {
199 34
            $nameObj->given = StringHelper::initializeBySpaceOrHyphen(
200 34
                $nameItem->given,
201 34
                $this->nameOptions->getInitializeWith()
202
            );
203
        }
204
205 105
        $renderedResult = $this->getNamesString($nameObj, $rank);
206 105
        CiteProcHelper::applyAdditionMarkupFunction($nameItem, $this->parent->getVariables()[0], $renderedResult);
207 105
        return trim($renderedResult);
208
    }
209
210
    /**
211
     * @param  stdClass $name
212
     * @param  int      $rank
213
     * @return string
214
     * @throws CiteProcException
215
     */
216 105
    private function getNamesString($name, $rank)
217
    {
218 105
        $text = "";
219
220 105
        if (!isset($name->family)) {
221
            return $text;
222
        }
223
224 105
        $text = $this->nameOrderRenderer->render($name, $rank);
225
226
        //contains nbsp prefixed by normal space or followed by normal space?
227 105
        $text = htmlentities($text);
228 105
        if (strpos($text, " &nbsp;") !== false || strpos($text, "&nbsp; ") !== false) {
229
            $text = preg_replace("/[\s]+/", "", $text); //remove normal spaces
230
            return preg_replace("/&nbsp;+/", " ", $text);
231
        }
232 105
        $text = html_entity_decode(preg_replace("/[\s]+/", " ", $text));
233 105
        return $this->format(trim($text));
234
    }
235
236
    /**
237
     * @param  stdClass $name
238
     * @return stdClass
239
     */
240 105
    private function cloneNamePOSC($name)
241
    {
242 105
        $nameObj = new stdClass();
243 105
        if (isset($name->family)) {
244 105
            $nameObj->family = $name->family;
245
        }
246 105
        if (isset($name->given)) {
247 103
            $nameObj->given = $name->given;
248
        }
249 105
        if (isset($name->{'non-dropping-particle'})) {
250 8
            $nameObj->{'non-dropping-particle'} = $name->{'non-dropping-particle'};
251
        }
252 105
        if (isset($name->{'dropping-particle'})) {
253 8
            $nameObj->{'dropping-particle'} = $name->{'dropping-particle'};
254
        }
255 105
        if (isset($name->{'suffix'})) {
256 17
            $nameObj->{'suffix'} = $name->{'suffix'};
257
        }
258 105
        return $nameObj;
259
    }
260
261
    /**
262
     * @param  $data
263
     * @param  $text
264
     * @param  $resultNames
265
     * @return string
266
     */
267 105
    protected function appendEtAl($data, $text, $resultNames)
268
    {
269
        //append et al abbreviation
270 105
        if (count($data) > 1
271 105
            && !empty($resultNames)
272 105
            && !empty($this->etAl)
273 105
            && !empty($this->nameOptions->getEtAlMin())
274 105
            && !empty($this->nameOptions->getEtAlUseFirst())
275 105
            && count($data) != count($resultNames)
276
        ) {
277
            /* By default, when a name list is truncated to a single name, the name and the “et-al” (or “and others”)
278
            term are separated by a space (e.g. “Doe et al.”). When a name list is truncated to two or more names, the
279
            name delimiter is used (e.g. “Doe, Smith, et al.”). This behavior can be changed with the
280
            delimiter-precedes-et-al attribute. */
281
282 13
            switch ($this->nameOptions->getDelimiterPrecedesEtAl()) {
283 13
                case 'never':
284 5
                    $text = $text . " " . $this->etAl;
285 5
                    break;
286 8
                case 'always':
287 1
                    $text = $text . $this->delimiter . $this->etAl;
288 1
                    break;
289 8
                case 'contextual':
290
                default:
291 8
                    if (count($resultNames) === 1) {
292 5
                        $text .= " " . $this->etAl;
293
                    } else {
294 3
                        $text .= $this->delimiter . $this->etAl;
295
                    }
296
            }
297
        }
298 105
        return $text;
299
    }
300
301
    /**
302
     * @param  $resultNames
303
     * @return array
304
     */
305 105
    protected function prepareAbbreviation($resultNames)
306
    {
307 105
        $cnt = count($resultNames);
308
        /* Use of et-al-min and et-al-user-first enables et-al abbreviation. If the number of names in a name variable
309
        matches or exceeds the number set on et-al-min, the rendered name list is truncated after reaching the number of
310
        names set on et-al-use-first.  */
311
312 105
        if (null !== $this->nameOptions->getEtAlMin() && null !== $this->nameOptions->getEtAlUseFirst()) {
0 ignored issues
show
introduced by
The condition null !== $this->nameOptions->getEtAlUseFirst() is always true.
Loading history...
313 52
            if ($this->nameOptions->getEtAlMin() <= $cnt) {
314 16
                if ($this->nameOptions->isEtAlUseLast() &&
315 16
                    $this->nameOptions->getEtAlMin() - $this->nameOptions->getEtAlUseFirst() >= 2) {
316
                    /* et-al-use-last: When set to “true” (the default is “false”), name lists truncated by et-al
317
                    abbreviation are followed by the name delimiter, the ellipsis character, and the last name of the
318
                    original name list. This is only possible when the original name list has at least two more names
319
                    than the truncated name list (for this the value of et-al-use-first/et-al-subsequent-min must be at
320
                    least 2 less than the value of et-al-min/et-al-subsequent-use-first).*/
321
322 3
                    $lastName = array_pop($resultNames); //remove last Element and remember in $lastName
323
                }
324 16
                for ($i = $this->nameOptions->getEtAlUseFirst(); $i < $cnt; ++$i) {
325 16
                    unset($resultNames[$i]);
326
                }
327
328 16
                $resultNames = array_values($resultNames);
329
330 16
                if (!empty($lastName)) { // append $lastName if exist
331 3
                    $resultNames[] = $lastName;
332
                }
333
334 16
                if ($this->parent->hasEtAl()) {
335 1
                    $this->etAl = $this->parent->getEtAl()->render(null);
336 1
                    return $resultNames;
337
                } else {
338 15
                    $this->etAl = CiteProc::getContext()->getLocale()->filter('terms', 'et-al')->single;
339 15
                    return $resultNames;
340
                }
341
            }
342 44
            return $resultNames;
343
        }
344 54
        return $resultNames;
345
    }
346
347
    /**
348
     * @param  $data
349
     * @param  stdClass $preceding
350
     * @return array
351
     * @throws CiteProcException
352
     */
353 3
    protected function renderSubsequentSubstitution($data, $preceding)
354
    {
355 3
        $resultNames = [];
356 3
        $subsequentSubstitution = CiteProc::getContext()->getCitationData()->getSubsequentAuthorSubstitute();
357 3
        $subsequentSubstitutionRule = CiteProc::getContext()->getCitationData()->getSubsequentAuthorSubstituteRule();
358
359
        /**
360
         * @var string $type
361
         * @var stdClass $name
362
         */
363 3
        foreach ($data as $rank => $name) {
364
            switch ($subsequentSubstitutionRule) {
365
                /*
366
                 * “partial-each” - when one or more rendered names in the name variable match those in the
367
                 * preceding bibliographic entry, the value of subsequent-author-substitute substitutes for each
368
                 * matching name. Matching starts with the first name, and continues up to the first mismatch.
369
                 */
370 3
                case SubsequentAuthorSubstituteRule::PARTIAL_EACH:
371 1
                    if (NameHelper::precedingHasAuthor($preceding, $name)) {
372 1
                        $resultNames[] = $subsequentSubstitution;
373
                    } else {
374 1
                        $resultNames[] = $this->formatName($name, $rank);
375
                    }
376 1
                    break;
377
378
                /*
379
                 * “partial-first” - as “partial-each”, but substitution is limited to the first name of the name
380
                 * variable.
381
                 */
382 2
                case SubsequentAuthorSubstituteRule::PARTIAL_FIRST:
383 1
                    if ($rank === 0) {
384 1
                        if ($preceding->author[0]->family === $name->family) {
385 1
                            $resultNames[] = $subsequentSubstitution;
386
                        } else {
387 1
                            $resultNames[] = $this->formatName($name, $rank);
388
                        }
389
                    } else {
390 1
                        $resultNames[] = $this->formatName($name, $rank);
391
                    }
392 1
                    break;
393
394
                 /*
395
                  * “complete-each” - requires a complete match like “complete-all”, but now the value of
396
                  * subsequent-author-substitute substitutes for each rendered name.
397
                  */
398 1
                case SubsequentAuthorSubstituteRule::COMPLETE_EACH:
399
                    try {
400 1
                        if (NameHelper::identicalAuthors($preceding, $data)) {
401 1
                            $resultNames[] = $subsequentSubstitution;
402
                        } else {
403 1
                            $resultNames[] = $this->formatName($name, $rank);
404
                        }
405
                    } catch (CiteProcException $e) {
406
                        $resultNames[] = $this->formatName($name, $rank);
407
                    }
408 3
                    break;
409
            }
410
        }
411 3
        return $resultNames;
412
    }
413
414
    /**
415
     * @param  array $data
416
     * @param  int   $citationNumber
417
     * @return array
418
     * @throws CiteProcException
419
     */
420 105
    private function handleSubsequentAuthorSubstitution($data, $citationNumber)
421
    {
422 105
        $hasPreceding = CiteProc::getContext()->getCitationData()->hasKey($citationNumber - 1);
423 105
        $subsequentSubstitution = CiteProc::getContext()->getCitationData()->getSubsequentAuthorSubstitute();
424 105
        $subsequentSubstitutionRule = CiteProc::getContext()->getCitationData()->getSubsequentAuthorSubstituteRule();
425 105
        $preceding = CiteProc::getContext()->getCitationData()->get($citationNumber - 1);
426
427 105
        if ($hasPreceding && !is_null($subsequentSubstitution) && !empty($subsequentSubstitutionRule)) {
428
            /**
429
             * @var stdClass $preceding
430
             */
431 8
            if ($subsequentSubstitutionRule == SubsequentAuthorSubstituteRule::COMPLETE_ALL) {
0 ignored issues
show
introduced by
The condition $subsequentSubstitutionR...ituteRule::COMPLETE_ALL is always false.
Loading history...
432
                try {
433 5
                    if (NameHelper::identicalAuthors($preceding, $data)) {
434 2
                        return [];
435
                    } else {
436 4
                        $resultNames = $this->getFormattedNames($data);
437
                    }
438 1
                } catch (CiteProcException $e) {
439 5
                    $resultNames = $this->getFormattedNames($data);
440
                }
441
            } else {
442 8
                $resultNames = $this->renderSubsequentSubstitution($data, $preceding);
443
            }
444
        } else {
445 105
            $resultNames = $this->getFormattedNames($data);
446
        }
447 105
        return $resultNames;
448
    }
449
450
451
    /**
452
     * @param  array $data
453
     * @return array
454
     * @throws CiteProcException
455
     */
456 105
    protected function getFormattedNames($data)
457
    {
458 105
        $resultNames = [];
459 105
        foreach ($data as $rank => $name) {
460 105
            $formatted = $this->formatName($name, $rank);
461 105
            $resultNames[] = NameHelper::addExtendedMarkup($this->variable, $name, $formatted);
462
        }
463 105
        return $resultNames;
464
    }
465
466
    /**
467
     * @param  $resultNames
468
     * @return string
469
     */
470 15
    protected function renderDelimiterPrecedesLastNever($resultNames)
471
    {
472 15
        $text = "";
473 15
        if (!$this->nameOptions->isEtAlUseLast()) {
474 13
            if (count($resultNames) === 1) {
475 10
                $text = $resultNames[0];
476 10
            } elseif (count($resultNames) === 2) {
477 8
                $text = implode(" ", $resultNames);
478
            } else { // >2
479 7
                $lastName = array_pop($resultNames);
480 7
                $text = implode($this->delimiter, $resultNames) . " " . $lastName;
481
            }
482
        }
483 15
        return $text;
484
    }
485
486
    /**
487
     * @param  $resultNames
488
     * @return string
489
     */
490 14
    protected function renderDelimiterPrecedesLastContextual($resultNames)
491
    {
492 14
        if (count($resultNames) === 1) {
493 9
            $text = $resultNames[0];
494 6
        } elseif (count($resultNames) === 2) {
495 5
            $text = implode(" ", $resultNames);
496
        } else {
497 1
            $text = implode($this->delimiter, $resultNames);
498
        }
499 14
        return $text;
500
    }
501
502
    /**
503
     * @param $resultNames
504
     */
505 105
    protected function addAnd(&$resultNames)
506
    {
507 105
        $count = count($resultNames);
508 105
        if (!empty($this->and) && $count > 1 && empty($this->etAl)) {
509 30
            $new = $this->and . ' ' . end($resultNames); // add and-prefix of the last name if "and" is defined
510
            // set prefixed last name at the last position of $resultNames array
511 30
            $resultNames[count($resultNames) - 1] = $new;
512
        }
513 105
    }
514
515
    /**
516
     * @param  $resultNames
517
     * @return array|string
518
     */
519 105
    protected function renderDelimiterPrecedesLast($resultNames)
520
    {
521 105
        $text = "";
522 105
        if (!empty($this->and) && empty($this->etAl)) {
523 51
            switch ($this->nameOptions->getDelimiterPrecedesLast()) {
524 51
                case 'after-inverted-name':
525
                    //TODO: implement
526
                    break;
527 51
                case 'always':
528 25
                    $text = implode($this->delimiter, $resultNames);
529 25
                    break;
530 29
                case 'never':
531 15
                    $text = $this->renderDelimiterPrecedesLastNever($resultNames);
532 15
                    break;
533 14
                case 'contextual':
534
                default:
535 14
                    $text = $this->renderDelimiterPrecedesLastContextual($resultNames);
536
            }
537
        }
538 105
        return $text;
539
    }
540
541
    /**
542
     * @return string
543
     */
544 5
    public function getForm()
545
    {
546 5
        return $this->nameOptions->getForm();
547
    }
548
549
    /**
550
     * @param mixed $delimiter
551
     */
552
    public function setDelimiter($delimiter)
553
    {
554
        $this->delimiter = $delimiter;
555
    }
556
557
    /**
558
     * @return Names
559
     */
560
    public function getParent()
561
    {
562
        return $this->parent;
563
    }
564
565
    public function setParent($parent)
566
    {
567
        $this->parent = $parent;
568
    }
569
}
570