Completed
Push — master ( e09500...86ebd4 )
by Sebastian
02:59
created

Name::handleSubsequentAuthorSubstitution()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 17
nc 4
nop 2
dl 0
loc 25
rs 8.439
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
use Seboettg\CiteProc\CiteProc;
12
use Seboettg\CiteProc\Rendering\HasParent;
13
use Seboettg\CiteProc\Style\InheritableNameAttributesTrait;
14
use Seboettg\CiteProc\Style\Options\SubsequentAuthorSubstituteRule;
15
use Seboettg\CiteProc\Styles\AffixesTrait;
16
use Seboettg\CiteProc\Styles\DelimiterTrait;
17
use Seboettg\CiteProc\Styles\FormattingTrait;
18
use Seboettg\CiteProc\Util\Factory;
19
use Seboettg\CiteProc\Util\StringHelper;
20
21
22
/**
23
 * Class Name
24
 *
25
 * The cs:name element, an optional child element of cs:names, can be used to describe the formatting of individual
26
 * names, and the separation of names within a name variable.
27
 *
28
 * @package Seboettg\CiteProc\Rendering\Name
29
 *
30
 * @author Sebastian Böttger <[email protected]>
31
 */
32
class Name implements HasParent
33
{
34
    use InheritableNameAttributesTrait,
35
        FormattingTrait,
36
        AffixesTrait,
37
        DelimiterTrait;
38
39
    /**
40
     * @var array
41
     */
42
    protected $nameParts;
43
44
    /**
45
     * Specifies the text string used to separate names in a name variable. Default is ”, ” (e.g. “Doe, Smith”).
46
     * @var
47
     */
48
    private $delimiter = ", ";
49
50
    /**
51
     * @var Names
52
     */
53
    private $parent;
54
55
    /**
56
     * @var \SimpleXMLElement
57
     */
58
    private $node;
59
60
    /**
61
     * @var string
62
     */
63
    private $etAl;
64
65
    /**
66
     * Name constructor.
67
     * @param \SimpleXMLElement $node
68
     * @param Names $parent
69
     */
70
    public function __construct(\SimpleXMLElement $node, Names $parent)
71
    {
72
        $this->node = $node;
73
        $this->parent = $parent;
74
75
        $this->nameParts = [];
76
77
        /** @var \SimpleXMLElement $child */
78
        foreach ($node->children() as $child) {
79
80
            switch ($child->getName()) {
81
                case "name-part":
82
                    /** @var NamePart $namePart */
83
                    $namePart = Factory::create($child, $this);
84
                    $this->nameParts[$namePart->getName()] = $namePart;
85
            }
86
        }
87
88
        foreach ($node->attributes() as $attribute) {
89
            switch ($attribute->getName()) {
90
                case 'form':
91
                    $this->form = (string) $attribute;
92
                    break;
93
            }
94
95
        }
96
97
        $this->initFormattingAttributes($node);
98
        $this->initAffixesAttributes($node);
99
        $this->initDelimiterAttributes($node);
100
    }
101
102
    /**
103
     * @param array $data
104
     * @param integer|null $citationNumber
105
     * @return string
106
     */
107
    public function render($data, $citationNumber = null)
108
    {
109
        $text = "";
110
        if (!$this->attributesInitialized) {
111
            $this->initInheritableNameAttributes($this->node);
112
        }
113
        if ("text" === $this->and) {
114
            $this->and = CiteProc::getContext()->getLocale()->filter('terms', 'and')->single;
115
        } elseif ('symbol' === $this->and) {
116
            $this->and = '&#38;';
117
        }
118
119
        $resultNames = $this->handleSubsequentAuthorSubstitution($data, $citationNumber);
120
121
        if (empty($resultNames)) {
122
            return CiteProc::getContext()->getCitationItems()->getSubsequentAuthorSubstitute();
123
        }
124
125
        $resultNames = $this->prepareAbbreviation($resultNames);
126
127
        if ($this->etAlUseLast) {
128
            /* When set to “true” (the default is “false”), name lists truncated by et-al abbreviation are followed by
129
            the name delimiter, the ellipsis character, and the last name of the original name list. This is only
130
            possible when the original name list has at least two more names than the truncated name list (for this
131
            the value of et-al-use-first/et-al-subsequent-min must be at least 2 less than the value of
132
            et-al-min/et-al-subsequent-use-first). */
133
            $this->and = "…"; // set "and"
134
            $this->etAl = null; //reset $etAl;
135
        }
136
137
        /* add "and" */
138
        $count = count($resultNames);
139
        if (!empty($this->and) && $count > 1 && empty($this->etAl)) {
140
            $new = $this->and . ' ' . end($resultNames); // add and-prefix of the last name if "and" is defined
141
            $resultNames[count($resultNames) - 1] = $new; //set prefixed last name at the last position of $resultNames array
142
        }
143
144
        if (!empty($this->and) && empty($this->etAl)) {
145
            switch ($this->delimiterPrecedesLast) {
146
                case 'after-inverted-name':
147
                    //TODO: implement
148
                    break;
149
                case 'always':
150
                    $text = implode($this->delimiter, $resultNames);
151
                    break;
152
                case 'never':
153
                    if (!$this->etAlUseLast) {
154
                        if (count($resultNames) === 1) {
155
                            $text = $resultNames[0];
156 View Code Duplication
                        } else if (count($resultNames) === 2) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
157
                            $text = implode(" ", $resultNames);
158
                        } else { // >2
159
                            $lastName = array_pop($resultNames);
160
                            $text = implode($this->delimiter, $resultNames) . " " . $lastName;
161
                        }
162
                    }
163
164
                    break;
165
                case 'contextual':
166
                default:
167 View Code Duplication
                    if (count($resultNames) === 1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
168
                        $text = $resultNames[0];
169
                    } else if (count($resultNames) === 2) {
170
                        $text = implode(" ", $resultNames);
171
                    } else {
172
                        $text = implode($this->delimiter, $resultNames);
173
                    }
174
            }
175
        }
176
        if (empty($text)) {
177
            $text = implode($this->delimiter, $resultNames);
178
        }
179
180
        //append et al abbreviation
181
        if (count($data) > 1 &&
182
            !empty($resultNames) &&
183
            !empty($this->etAl) &&
184
            !empty($this->etAlMin) &&
185
            !empty($this->etAlUseFirst)) {
186
187
            $text = $this->appendEtAl($text, $resultNames);
188
        }
189
190
        /* A third value, “count”, returns the total number of names that would otherwise be rendered by the use of the
191
        cs:names element (taking into account the effects of et-al abbreviation and editor/translator collapsing),
192
        which allows for advanced sorting. */
193
194
        if ($this->form == 'count') {
195
            return (int) count($resultNames);
196
        }
197
198
        return $text;
199
    }
200
201
    /**
202
     * @param \stdClass $name
203
     * @param int $rank
204
     * @return string
205
     */
206
    private function formatName($name, $rank)
207
    {
208
209
        $nameObj = $this->cloneNamePOSC($name);
210
211
        $useInitials = $this->initialize && !is_null($this->initializeWith) && $this->initializeWith !== false;
212
        if ($useInitials && isset($name->given)) {
213
            $nameObj->given = StringHelper::initializeBySpaceOrHyphen($name->given, $this->initializeWith);
214
        }
215
216
        // format name-parts
217
        if (count($this->nameParts) > 0) {
218
            /** @var NamePart $namePart */
219
            foreach ($this->nameParts as $namePart) {
220
                $nameObj = $namePart->render($nameObj);
221
            }
222
        }
223
        $ret = $this->getNamesString($nameObj, $rank);
224
225
        return trim($ret);
226
    }
227
228
    /**
229
     * @param \stdClass $name
230
     * @param int $rank
231
     * @return string
232
     */
233
    private function getNamesString($name, $rank)
234
    {
235
        $text = "";
236
237
        if (!isset($name->family)) {
238
            return $text;
239
        }
240
241
        $given = !empty($name->given) ? trim($name->given) : "";
242
        $nonDroppingParticle = isset($name->{'non-dropping-particle'}) ? $name->{'non-dropping-particle'} : "";
243
        $droppingParticle = isset($name->{'dropping-particle'}) ? $name->{'dropping-particle'} : "";
244
        $suffix = (isset($name->{'suffix'})) ? $name->{'suffix'} : "";
245
246
        if (isset($name->family)) {
247
            $family = $name->family;
248
            if ($this->form == 'short') {
249
                $text = (!empty($nonDroppingParticle) ? $nonDroppingParticle . " " : "") . $family;
250
            } else {
251
                switch ($this->nameAsSortOrder) {
252
253
                    case 'all':
254
                    case 'first':
255
                        if ($this->nameAsSortOrder === "first" && $rank !== 0) {
256
                            break;
257
                        }
258
                        /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
259
                        use form "[non-dropping particel] family name,
260
                        given name [dropping particle], [suffix]"
261
                        */
262
                        $text  = !empty($nonDroppingParticle) ? "$nonDroppingParticle " : "";
263
                        $text .= $family;
264
                        $text .= !empty($given) ? $this->sortSeparator . $given : "";
265
                        $text .= !empty($droppingParticle) ? " $droppingParticle" : "";
266
                        $text .= !empty($suffix) ? $this->sortSeparator . $suffix : "";
267
268
                        //remove last comma when no suffix exist.
269
                        $text = trim($text);
270
                        $text = substr($text, -1) === $this->sortSeparator ? substr($text, 0, strlen($text) - 1) : $text;
271
                        break;
272
                    default:
273
                        /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
274
                        use form "given name [dropping particles] [non-dropping particles] family name [suffix]"
275
                        e.g. [Jean] [de] [La] [Fontaine] [III]
276
                        */
277
                        $text = sprintf(
278
                            "%s %s %s %s %s",
279
                            $given,
280
                            $droppingParticle,
281
                            $nonDroppingParticle,
282
                            $family,
283
                            $suffix);
284
285
                }
286
            }
287
        }
288
289
        //contains nbsp prefixed by normal space or followed by normal space?
290
        $text = htmlentities($text);
291
        if (strpos($text, " &nbsp;") !== false || strpos($text, "&nbsp; ") !== false) {
292
293
            $text = preg_replace("/[\s]+/", "", $text); //remove normal spaces
294
            return preg_replace("/&nbsp;+/", " ", $text);
295
        }
296
        $text = html_entity_decode(preg_replace("/[\s]+/", " ", $text));
297
        return $this->format(trim($text));
298
    }
299
300
    /**
301
     * @param $name
302
     * @return \stdClass
303
     */
304
    private function cloneNamePOSC($name)
305
    {
306
        $nameObj = new \stdClass();
307
        if (isset($name->family)) {
308
            $nameObj->family = $name->family;
309
        }
310
        if (isset($name->given)) {
311
            $nameObj->given = $name->given;
312
        }
313
        if (isset($name->{'non-dropping-particle'})) {
314
            $nameObj->{'non-dropping-particle'} = $name->{'non-dropping-particle'};
315
        }
316
        if (isset($name->{'dropping-particle'})) {
317
            $nameObj->{'dropping-particle'} = $name->{'dropping-particle'};
318
        }
319
        if (isset($name->{'suffix'})) {
320
            $nameObj->{'suffix'} = $name->{'suffix'};
321
        }
322
        return $nameObj;
323
    }
324
325
    /**
326
     * @param $text
327
     * @param $resultNames
328
     * @return string
329
     */
330
    protected function appendEtAl($text, $resultNames)
331
    {
332
        /* By default, when a name list is truncated to a single name, the name and the “et-al” (or “and others”)
333
        term are separated by a space (e.g. “Doe et al.”). When a name list is truncated to two or more names, the
334
        name delimiter is used (e.g. “Doe, Smith, et al.”). This behavior can be changed with the
335
        delimiter-precedes-et-al attribute. */
336
337
        switch ($this->delimiterPrecedesEtAl) {
338
            case 'never':
339
                $text = $text . " " . $this->etAl;
340
                break;
341
            case 'always':
342
                $text = $text . $this->delimiter . $this->etAl;
343
                break;
344
            case 'contextual':
345
            default:
346
                if (count($resultNames) === 1) {
347
                    $text .= " " . $this->etAl;
348
                } else {
349
                    $text .= $this->delimiter . $this->etAl;
350
                }
351
        }
352
353
        return $text;
354
    }
355
356
    /**
357
     * @param $resultNames
358
     * @return array
359
     */
360
    protected function prepareAbbreviation($resultNames)
361
    {
362
        $cnt = count($resultNames);
363
        /* Use of et-al-min and et-al-user-first enables et-al abbreviation. If the number of names in a name variable
364
        matches or exceeds the number set on et-al-min, the rendered name list is truncated after reaching the number of
365
        names set on et-al-use-first.  */
366
367
        if (isset($this->etAlMin) && isset($this->etAlUseFirst)) {
368
369
            if ($this->etAlMin <= $cnt) {
370
                if ($this->etAlUseLast && $this->etAlMin - $this->etAlUseFirst >= 2) {
371
                    /* et-al-use-last: When set to “true” (the default is “false”), name lists truncated by et-al
372
                    abbreviation are followed by the name delimiter, the ellipsis character, and the last name of the
373
                    original name list. This is only possible when the original name list has at least two more names
374
                    than the truncated name list (for this the value of et-al-use-first/et-al-subsequent-min must be at
375
                    least 2 less than the value of et-al-min/et-al-subsequent-use-first).*/
376
377
                    $lastName = array_pop($resultNames); //remove last Element and remember in $lastName
378
379
                }
380
                for ($i = $this->etAlUseFirst; $i < $cnt; ++$i) {
381
                    unset($resultNames[$i]);
382
                }
383
384
                $resultNames = array_values($resultNames);
385
386
                if (!empty($lastName)) { // append $lastName if exist
387
                    $resultNames[] = $lastName;
388
                }
389
390
                if ($this->parent->hasEtAl()) {
391
                    $this->etAl = $this->parent->getEtAl()->render(null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a array|object<Seboettg\CiteProc\Data\DataList>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
392
                    return $resultNames;
393
                } else {
394
                    $this->etAl = CiteProc::getContext()->getLocale()->filter('terms', 'et-al')->single;
395
                    return $resultNames;
396
                }
397
            }
398
            return $resultNames;
399
        }
400
        return $resultNames;
401
    }
402
403
    /**
404
     * @return Names
405
     */
406
    public function getParent()
407
    {
408
        return $this->parent;
409
    }
410
411
    /**
412
     * @param $data
413
     * @param \stdClass $preceding
414
     * @return array
415
     */
416
    protected function renderSubsequentSubstitution($data, $preceding)
417
    {
418
        $resultNames = [];
419
        $subsequentSubstitution = CiteProc::getContext()->getCitationItems()->getSubsequentAuthorSubstitute();
420
        $subsequentSubstitutionRule = CiteProc::getContext()->getCitationItems()->getSubsequentAuthorSubstituteRule();
421
422
        /**
423
         * @var string $type
424
         * @var \stdClass $name
425
         */
426
        foreach ($data as $rank => $name) {
427
428
            switch ($subsequentSubstitutionRule) {
429
430
                /* “partial-each” - when one or more rendered names in the name variable match those in the preceding
431
                bibliographic entry, the value of subsequent-author-substitute substitutes for each matching name.
432
                Matching starts with the first name, and continues up to the first mismatch. */
433 View Code Duplication
                case SubsequentAuthorSubstituteRule::PARTIAL_EACH:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
434
435
                    if ($this->precedingHasAuthor($preceding, $name)) {
436
                        $resultNames[] = $subsequentSubstitution;
437
                    } else {
438
                        $resultNames[] = $this->formatName($name, $rank);
439
                    }
440
                    break;
441
442
443
                /* “partial-first” - as “partial-each”, but substitution is limited to the first name of the name
444
                variable. */
445
                case SubsequentAuthorSubstituteRule::PARTIAL_FIRST:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
446
447
                    if ($rank === 0) {
448
                        if ($preceding->author[0]->family === $name->family) {
449
                            $resultNames[] = $subsequentSubstitution;
450
                        } else {
451
                            $resultNames[] = $this->formatName($name, $rank);
452
                        }
453
                    } else {
454
                        $resultNames[] = $this->formatName($name, $rank);
455
                    }
456
                    break;
457
458
                /* “complete-each” - requires a complete match like “complete-all”, but now the value of
459
                subsequent-author-substitute substitutes for each rendered name. */
460 View Code Duplication
                case SubsequentAuthorSubstituteRule::COMPLETE_EACH:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
461
                    if ($this->identicalAuthors($preceding, $data)) {
462
                        $resultNames[] = $subsequentSubstitution;
463
                    } else {
464
                        $resultNames[] = $this->formatName($name, $rank);
465
                    }
466
                    break;
467
            }
468
        }
469
        return $resultNames;
470
    }
471
472
    /**
473
     * @param \stdClass $preceding
474
     * @param \stdClass $name
475
     * @return bool
476
     */
477
    public function precedingHasAuthor($preceding, $name)
478
    {
479
        foreach ($preceding->author as $author) {
480
            if ($author->family === $name->family && $author->given === $name->given) {
481
                return true;
482
            }
483
        }
484
        return false;
485
    }
486
487
    /**
488
     * @param \stdClass $precedingItem
489
     * @param array $currentAuthor
490
     * @return bool
491
     */
492
    private function identicalAuthors($precedingItem, $currentAuthor)
493
    {
494
        if (count($precedingItem->author) !== count($currentAuthor)) {
495
            return false;
496
        }
497
        foreach ($currentAuthor as $current) {
498
            if ($this->precedingHasAuthor($precedingItem, $current)) {
499
                continue;
500
            }
501
            return false;
502
        }
503
        return true;
504
    }
505
506
    /**
507
     * @return string
508
     */
509
    public function getForm()
510
    {
511
        return $this->form;
512
    }
513
514
    /**
515
     * @return string
516
     */
517
    public function isNameAsSortOrder()
518
    {
519
        return $this->nameAsSortOrder;
520
    }
521
522
    /**
523
     * @return mixed
524
     */
525
    public function getDelimiter()
526
    {
527
        return $this->delimiter;
528
    }
529
530
    /**
531
     * @param mixed $delimiter
532
     */
533
    public function setDelimiter($delimiter)
534
    {
535
        $this->delimiter = $delimiter;
536
    }
537
538
    /**
539
     * @param array $data
540
     * @param int $citationNumber
541
     * @return array
542
     */
543
    private function handleSubsequentAuthorSubstitution($data, $citationNumber)
544
    {
545
        $hasPreceding = CiteProc::getContext()->getCitationItems()->hasKey($citationNumber - 1);
546
        $subsequentSubstitution = CiteProc::getContext()->getCitationItems()->getSubsequentAuthorSubstitute();
547
        $subsequentSubstitutionRule = CiteProc::getContext()->getCitationItems()->getSubsequentAuthorSubstituteRule();
548
        $preceding = CiteProc::getContext()->getCitationItems()->get($citationNumber - 1);
549
550
551
        if ($hasPreceding && !is_null($subsequentSubstitution) && !empty($subsequentSubstitutionRule)) {
552
            /** @var \stdClass $preceding */
553
            $identicalAuthors = $this->identicalAuthors($preceding, $data);
554
            if ($subsequentSubstitutionRule == SubsequentAuthorSubstituteRule::COMPLETE_ALL) {
555
                if ($identicalAuthors) {
556
                    return [];
557
                } else {
558
                    $resultNames = $this->getFormattedNames($data);
559
                }
560
            } else {
561
                $resultNames = $this->renderSubsequentSubstitution($data, $preceding);
562
            }
563
        } else {
564
            $resultNames = $this->getFormattedNames($data);
565
        }
566
        return $resultNames;
567
    }
568
569
570
    /**
571
     * @param array $data
572
     * @return array
573
     */
574
    protected function getFormattedNames($data)
575
    {
576
        $resultNames = [];
577
        foreach ($data as $rank => $name) {
578
            $resultNames[] = $this->formatName($name, $rank);
579
        }
580
        return $resultNames;
581
    }
582
583
}
0 ignored issues
show
Coding Style introduced by
As per coding style, files should not end with a newline character.

This check marks files that end in a newline character, i.e. an empy line.

Loading history...
584