Completed
Push — master ( 86ebd4...251ea7 )
by Sebastian
02:41
created

Name::renderDelimiterPrecesdesLastContextual()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 8

Duplication

Lines 7
Ratio 63.64 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 1
dl 7
loc 11
rs 9.4285
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\NameHelper;
20
use Seboettg\CiteProc\Util\StringHelper;
21
22
23
/**
24
 * Class Name
25
 *
26
 * The cs:name element, an optional child element of cs:names, can be used to describe the formatting of individual
27
 * names, and the separation of names within a name variable.
28
 *
29
 * @package Seboettg\CiteProc\Rendering\Name
30
 *
31
 * @author Sebastian Böttger <[email protected]>
32
 */
33
class Name implements HasParent
34
{
35
    use InheritableNameAttributesTrait,
36
        FormattingTrait,
37
        AffixesTrait,
38
        DelimiterTrait;
39
40
    /**
41
     * @var array
42
     */
43
    protected $nameParts;
44
45
    /**
46
     * Specifies the text string used to separate names in a name variable. Default is ”, ” (e.g. “Doe, Smith”).
47
     * @var
48
     */
49
    private $delimiter = ", ";
50
51
    /**
52
     * @var Names
53
     */
54
    private $parent;
55
56
    /**
57
     * @var \SimpleXMLElement
58
     */
59
    private $node;
60
61
    /**
62
     * @var string
63
     */
64
    private $etAl;
65
66
    /**
67
     * Name constructor.
68
     * @param \SimpleXMLElement $node
69
     * @param Names $parent
70
     */
71
    public function __construct(\SimpleXMLElement $node, Names $parent)
72
    {
73
        $this->node = $node;
74
        $this->parent = $parent;
75
76
        $this->nameParts = [];
77
78
        /** @var \SimpleXMLElement $child */
79
        foreach ($node->children() as $child) {
80
81
            switch ($child->getName()) {
82
                case "name-part":
83
                    /** @var NamePart $namePart */
84
                    $namePart = Factory::create($child, $this);
85
                    $this->nameParts[$namePart->getName()] = $namePart;
86
            }
87
        }
88
89
        foreach ($node->attributes() as $attribute) {
90
            switch ($attribute->getName()) {
91
                case 'form':
92
                    $this->form = (string) $attribute;
93
                    break;
94
            }
95
96
        }
97
98
        $this->initFormattingAttributes($node);
99
        $this->initAffixesAttributes($node);
100
        $this->initDelimiterAttributes($node);
101
    }
102
103
    /**
104
     * @param array $data
105
     * @param integer|null $citationNumber
106
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|integer?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
107
     */
108
    public function render($data, $citationNumber = null)
109
    {
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
        /* When set to “true” (the default is “false”), name lists truncated by et-al abbreviation are followed by
128
        the name delimiter, the ellipsis character, and the last name of the original name list. This is only
129
        possible when the original name list has at least two more names than the truncated name list (for this
130
        the value of et-al-use-first/et-al-subsequent-min must be at least 2 less than the value of
131
        et-al-min/et-al-subsequent-use-first). */
132
        if ($this->etAlUseLast) {
133
            $this->and = "…"; // set "and"
134
            $this->etAl = null; //reset $etAl;
135
        }
136
137
        /* add "and" */
138
        $this->addAnd($resultNames);
139
140
        $text = $this->renderDelimiterPrecedesLast($resultNames);
141
142
        if (empty($text)) {
143
            $text = implode($this->delimiter, $resultNames);
144
        }
145
146
        $text = $this->appendEtAl($data, $text, $resultNames);
147
148
        /* A third value, “count”, returns the total number of names that would otherwise be rendered by the use of the
149
        cs:names element (taking into account the effects of et-al abbreviation and editor/translator collapsing),
150
        which allows for advanced sorting. */
151
        if ($this->form == 'count') {
152
            return (int) count($resultNames);
153
        }
154
155
        return $text;
156
    }
157
158
    /**
159
     * @param \stdClass $name
160
     * @param int $rank
161
     * @return string
162
     */
163
    private function formatName($name, $rank)
164
    {
165
166
        $nameObj = $this->cloneNamePOSC($name);
167
168
        $useInitials = $this->initialize && !is_null($this->initializeWith) && $this->initializeWith !== false;
169
        if ($useInitials && isset($name->given)) {
170
            $nameObj->given = StringHelper::initializeBySpaceOrHyphen($name->given, $this->initializeWith);
171
        }
172
173
        // format name-parts
174
        if (count($this->nameParts) > 0) {
175
            /** @var NamePart $namePart */
176
            foreach ($this->nameParts as $namePart) {
177
                $nameObj = $namePart->render($nameObj);
178
            }
179
        }
180
        $ret = $this->getNamesString($nameObj, $rank);
181
182
        return trim($ret);
183
    }
184
185
    /**
186
     * @param \stdClass $name
187
     * @param int $rank
188
     * @return string
189
     */
190
    private function getNamesString($name, $rank)
191
    {
192
        $text = "";
193
194
        if (!isset($name->family)) {
195
            return $text;
196
        }
197
198
        $given = !empty($name->given) ? trim($name->given) : "";
199
        $nonDroppingParticle = isset($name->{'non-dropping-particle'}) ? $name->{'non-dropping-particle'} : "";
200
        $droppingParticle = isset($name->{'dropping-particle'}) ? $name->{'dropping-particle'} : "";
201
        $suffix = (isset($name->{'suffix'})) ? $name->{'suffix'} : "";
202
203
        if (isset($name->family)) {
204
            $family = $name->family;
205
            if ($this->form == 'short') {
206
                $text = (!empty($nonDroppingParticle) ? $nonDroppingParticle . " " : "") . $family;
207
            } else {
208
                switch ($this->nameAsSortOrder) {
209
210
                    case 'all':
211
                    case 'first':
212
                        if ($this->nameAsSortOrder === "first" && $rank !== 0) {
213
                            break;
214
                        }
215
                        /*
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...
216
                        use form "[non-dropping particel] family name,
217
                        given name [dropping particle], [suffix]"
218
                        */
219
                        $text  = !empty($nonDroppingParticle) ? "$nonDroppingParticle " : "";
220
                        $text .= $family;
221
                        $text .= !empty($given) ? $this->sortSeparator . $given : "";
222
                        $text .= !empty($droppingParticle) ? " $droppingParticle" : "";
223
                        $text .= !empty($suffix) ? $this->sortSeparator . $suffix : "";
224
225
                        //remove last comma when no suffix exist.
226
                        $text = trim($text);
227
                        $text = substr($text, -1) === $this->sortSeparator ? substr($text, 0, strlen($text) - 1) : $text;
228
                        break;
229
                    default:
230
                        /*
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...
231
                        use form "given name [dropping particles] [non-dropping particles] family name [suffix]"
232
                        e.g. [Jean] [de] [La] [Fontaine] [III]
233
                        */
234
                        $text = sprintf(
235
                            "%s %s %s %s %s",
236
                            $given,
237
                            $droppingParticle,
238
                            $nonDroppingParticle,
239
                            $family,
240
                            $suffix);
241
242
                }
243
            }
244
        }
245
246
        //contains nbsp prefixed by normal space or followed by normal space?
247
        $text = htmlentities($text);
248
        if (strpos($text, " &nbsp;") !== false || strpos($text, "&nbsp; ") !== false) {
249
250
            $text = preg_replace("/[\s]+/", "", $text); //remove normal spaces
251
            return preg_replace("/&nbsp;+/", " ", $text);
252
        }
253
        $text = html_entity_decode(preg_replace("/[\s]+/", " ", $text));
254
        return $this->format(trim($text));
255
    }
256
257
    /**
258
     * @param $name
259
     * @return \stdClass
260
     */
261
    private function cloneNamePOSC($name)
262
    {
263
        $nameObj = new \stdClass();
264
        if (isset($name->family)) {
265
            $nameObj->family = $name->family;
266
        }
267
        if (isset($name->given)) {
268
            $nameObj->given = $name->given;
269
        }
270
        if (isset($name->{'non-dropping-particle'})) {
271
            $nameObj->{'non-dropping-particle'} = $name->{'non-dropping-particle'};
272
        }
273
        if (isset($name->{'dropping-particle'})) {
274
            $nameObj->{'dropping-particle'} = $name->{'dropping-particle'};
275
        }
276
        if (isset($name->{'suffix'})) {
277
            $nameObj->{'suffix'} = $name->{'suffix'};
278
        }
279
        return $nameObj;
280
    }
281
282
    /**
283
     * @param $data
284
     * @param $text
285
     * @param $resultNames
286
     * @return string
287
     */
288
    protected function appendEtAl($data, $text, $resultNames)
289
    {
290
        //append et al abbreviation
291
        if (count($data) > 1 &&
292
            !empty($resultNames) &&
293
            !empty($this->etAl) &&
294
            !empty($this->etAlMin) &&
295
            !empty($this->etAlUseFirst)) {
296
297
298
            /* By default, when a name list is truncated to a single name, the name and the “et-al” (or “and others”)
299
            term are separated by a space (e.g. “Doe et al.”). When a name list is truncated to two or more names, the
300
            name delimiter is used (e.g. “Doe, Smith, et al.”). This behavior can be changed with the
301
            delimiter-precedes-et-al attribute. */
302
303
            switch ($this->delimiterPrecedesEtAl) {
304
                case 'never':
305
                    $text = $text . " " . $this->etAl;
306
                    break;
307
                case 'always':
308
                    $text = $text . $this->delimiter . $this->etAl;
309
                    break;
310
                case 'contextual':
311
                default:
312
                    if (count($resultNames) === 1) {
313
                        $text .= " " . $this->etAl;
314
                    } else {
315
                        $text .= $this->delimiter . $this->etAl;
316
                    }
317
            }
318
        }
319
        return $text;
320
    }
321
    /**
322
     * @param $resultNames
323
     * @return array
324
     */
325
    protected function prepareAbbreviation($resultNames)
326
    {
327
        $cnt = count($resultNames);
328
        /* Use of et-al-min and et-al-user-first enables et-al abbreviation. If the number of names in a name variable
329
        matches or exceeds the number set on et-al-min, the rendered name list is truncated after reaching the number of
330
        names set on et-al-use-first.  */
331
332
        if (isset($this->etAlMin) && isset($this->etAlUseFirst)) {
333
334
            if ($this->etAlMin <= $cnt) {
335
                if ($this->etAlUseLast && $this->etAlMin - $this->etAlUseFirst >= 2) {
336
                    /* et-al-use-last: When set to “true” (the default is “false”), name lists truncated by et-al
337
                    abbreviation are followed by the name delimiter, the ellipsis character, and the last name of the
338
                    original name list. This is only possible when the original name list has at least two more names
339
                    than the truncated name list (for this the value of et-al-use-first/et-al-subsequent-min must be at
340
                    least 2 less than the value of et-al-min/et-al-subsequent-use-first).*/
341
342
                    $lastName = array_pop($resultNames); //remove last Element and remember in $lastName
343
344
                }
345
                for ($i = $this->etAlUseFirst; $i < $cnt; ++$i) {
346
                    unset($resultNames[$i]);
347
                }
348
349
                $resultNames = array_values($resultNames);
350
351
                if (!empty($lastName)) { // append $lastName if exist
352
                    $resultNames[] = $lastName;
353
                }
354
355
                if ($this->parent->hasEtAl()) {
356
                    $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...
357
                    return $resultNames;
358
                } else {
359
                    $this->etAl = CiteProc::getContext()->getLocale()->filter('terms', 'et-al')->single;
360
                    return $resultNames;
361
                }
362
            }
363
            return $resultNames;
364
        }
365
        return $resultNames;
366
    }
367
368
    /**
369
     * @param $data
370
     * @param \stdClass $preceding
371
     * @return array
372
     */
373
    protected function renderSubsequentSubstitution($data, $preceding)
374
    {
375
        $resultNames = [];
376
        $subsequentSubstitution = CiteProc::getContext()->getCitationItems()->getSubsequentAuthorSubstitute();
377
        $subsequentSubstitutionRule = CiteProc::getContext()->getCitationItems()->getSubsequentAuthorSubstituteRule();
378
379
        /**
380
         * @var string $type
381
         * @var \stdClass $name
382
         */
383
        foreach ($data as $rank => $name) {
384
385
            switch ($subsequentSubstitutionRule) {
386
387
                /* “partial-each” - when one or more rendered names in the name variable match those in the preceding
388
                bibliographic entry, the value of subsequent-author-substitute substitutes for each matching name.
389
                Matching starts with the first name, and continues up to the first mismatch. */
390 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...
391
392
                    if (NameHelper::precedingHasAuthor($preceding, $name)) {
393
                        $resultNames[] = $subsequentSubstitution;
394
                    } else {
395
                        $resultNames[] = $this->formatName($name, $rank);
396
                    }
397
                    break;
398
399
400
                /* “partial-first” - as “partial-each”, but substitution is limited to the first name of the name
401
                variable. */
402
                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...
403
404
                    if ($rank === 0) {
405
                        if ($preceding->author[0]->family === $name->family) {
406
                            $resultNames[] = $subsequentSubstitution;
407
                        } else {
408
                            $resultNames[] = $this->formatName($name, $rank);
409
                        }
410
                    } else {
411
                        $resultNames[] = $this->formatName($name, $rank);
412
                    }
413
                    break;
414
415
                /* “complete-each” - requires a complete match like “complete-all”, but now the value of
416
                subsequent-author-substitute substitutes for each rendered name. */
417 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...
418
                    if (NameHelper::identicalAuthors($preceding, $data)) {
419
                        $resultNames[] = $subsequentSubstitution;
420
                    } else {
421
                        $resultNames[] = $this->formatName($name, $rank);
422
                    }
423
                    break;
424
            }
425
        }
426
        return $resultNames;
427
    }
428
429
    /**
430
     * @param array $data
431
     * @param int $citationNumber
432
     * @return array
433
     */
434
    private function handleSubsequentAuthorSubstitution($data, $citationNumber)
435
    {
436
        $hasPreceding = CiteProc::getContext()->getCitationItems()->hasKey($citationNumber - 1);
437
        $subsequentSubstitution = CiteProc::getContext()->getCitationItems()->getSubsequentAuthorSubstitute();
438
        $subsequentSubstitutionRule = CiteProc::getContext()->getCitationItems()->getSubsequentAuthorSubstituteRule();
439
        $preceding = CiteProc::getContext()->getCitationItems()->get($citationNumber - 1);
440
441
442
        if ($hasPreceding && !is_null($subsequentSubstitution) && !empty($subsequentSubstitutionRule)) {
443
            /** @var \stdClass $preceding */
444
            $identicalAuthors = $this->identicalAuthors($preceding, $data);
0 ignored issues
show
Bug introduced by
The method identicalAuthors() does not seem to exist on object<Seboettg\CiteProc\Rendering\Name\Name>.

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...
445
            if ($subsequentSubstitutionRule == SubsequentAuthorSubstituteRule::COMPLETE_ALL) {
446
                if ($identicalAuthors) {
447
                    return [];
448
                } else {
449
                    $resultNames = $this->getFormattedNames($data);
450
                }
451
            } else {
452
                $resultNames = $this->renderSubsequentSubstitution($data, $preceding);
453
            }
454
        } else {
455
            $resultNames = $this->getFormattedNames($data);
456
        }
457
        return $resultNames;
458
    }
459
460
461
    /**
462
     * @param array $data
463
     * @return array
464
     */
465
    protected function getFormattedNames($data)
466
    {
467
        $resultNames = [];
468
        foreach ($data as $rank => $name) {
469
            $resultNames[] = $this->formatName($name, $rank);
470
        }
471
        return $resultNames;
472
    }
473
474
    /**
475
     * @param $resultNames
476
     * @return array
477
     */
478
    protected function renderDelimiterPrecesdesLastNever($resultNames)
479
    {
480
        $text = "";
481
        if (!$this->etAlUseLast) {
482
            if (count($resultNames) === 1) {
483
                $text = $resultNames[0];
484 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...
485
                $text = implode(" ", $resultNames);
486
            } else { // >2
487
                $lastName = array_pop($resultNames);
488
                $text = implode($this->delimiter, $resultNames) . " " . $lastName;
489
            }
490
        }
491
        return $text;
492
    }
493
494
    /**
495
     * @param $resultNames
496
     * @return string
497
     */
498
    protected function renderDelimiterPrecesdesLastContextual($resultNames)
499
    {
500 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...
501
            $text = $resultNames[0];
502
        } else if (count($resultNames) === 2) {
503
            $text = implode(" ", $resultNames);
504
        } else {
505
            $text = implode($this->delimiter, $resultNames);
506
        }
507
        return $text;
508
    }
509
510
    /**
511
     * @param $resultNames
512
     */
513
    protected function addAnd(&$resultNames)
514
    {
515
        $count = count($resultNames);
516
        if (!empty($this->and) && $count > 1 && empty($this->etAl)) {
517
            $new = $this->and . ' ' . end($resultNames); // add and-prefix of the last name if "and" is defined
518
            $resultNames[count($resultNames) - 1] = $new; //set prefixed last name at the last position of $resultNames array
519
        }
520
    }
521
522
    /**
523
     * @param $resultNames
524
     * @return array|string
525
     */
526
    protected function renderDelimiterPrecedesLast($resultNames)
527
    {
528
        $text = "";
529
        if (!empty($this->and) && empty($this->etAl)) {
530
            switch ($this->delimiterPrecedesLast) {
531
                case 'after-inverted-name':
532
                    //TODO: implement
533
                    break;
534
                case 'always':
535
                    $text = implode($this->delimiter, $resultNames);
536
                    break;
537
                case 'never':
538
                    $text = $this->renderDelimiterPrecesdesLastNever($resultNames);
539
                    break;
540
                case 'contextual':
541
                default:
542
                    $text = $this->renderDelimiterPrecesdesLastContextual($resultNames);
543
            }
544
        }
545
        return $text;
546
    }
547
548
    /**
549
     * @return string
550
     */
551
    public function getForm()
552
    {
553
        return $this->form;
554
    }
555
556
    /**
557
     * @return string
558
     */
559
    public function isNameAsSortOrder()
560
    {
561
        return $this->nameAsSortOrder;
562
    }
563
564
    /**
565
     * @return mixed
566
     */
567
    public function getDelimiter()
568
    {
569
        return $this->delimiter;
570
    }
571
572
    /**
573
     * @param mixed $delimiter
574
     */
575
    public function setDelimiter($delimiter)
576
    {
577
        $this->delimiter = $delimiter;
578
    }
579
580
    /**
581
     * @return Names
582
     */
583
    public function getParent()
584
    {
585
        return $this->parent;
586
    }
587
588
}
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...
589