Completed
Push — master ( 28f4e3...e09500 )
by Sebastian
02:32
created

Name::getDelimiter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
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
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 = [];
120
121
        $hasPreceding = CiteProc::getContext()->getCitationItems()->hasKey($citationNumber - 1);
122
        $subsequentSubstitution = CiteProc::getContext()->getCitationItems()->getSubsequentAuthorSubstitute();
123
        $subsequentSubstitutionRule = CiteProc::getContext()->getCitationItems()->getSubsequentAuthorSubstituteRule();
124
        $useSubseqSubstitution = !is_null($subsequentSubstitution) && !empty($subsequentSubstitutionRule);
125
        $preceding = CiteProc::getContext()->getCitationItems()->get($citationNumber - 1);
126
127
128
        if ($hasPreceding && $useSubseqSubstitution) {
129
            /** @var \stdClass $preceding */
130
            $identicalAuthors = $this->identicalAuthors($preceding, $data);
131
            if ($subsequentSubstitutionRule == SubsequentAuthorSubstituteRule::COMPLETE_ALL) {
132
                if ($identicalAuthors) {
133
                    return $subsequentSubstitution;
134
                } else {
135
                    $resultNames = $this->getFormattedNames($data, $resultNames);
136
                }
137
            } else {
138
                $resultNames = $this->renderSubsequentSubstitution($data, $preceding);
139
            }
140
        } else {
141
            $resultNames = $this->getFormattedNames($data, $resultNames);
142
        }
143
144
        $resultNames = $this->prepareAbbreviation($resultNames);
145
146
        if ($this->etAlUseLast) {
147
            /* When set to “true” (the default is “false”), name lists truncated by et-al abbreviation are followed by
148
            the name delimiter, the ellipsis character, and the last name of the original name list. This is only
149
            possible when the original name list has at least two more names than the truncated name list (for this
150
            the value of et-al-use-first/et-al-subsequent-min must be at least 2 less than the value of
151
            et-al-min/et-al-subsequent-use-first). */
152
            $this->and = "…"; // set "and"
153
            $this->etAl = null; //reset $etAl;
154
        }
155
156
        /* add "and" */
157
        $count = count($resultNames);
158
        if (!empty($this->and) && $count > 1 && empty($this->etAl)) {
159
            $new = $this->and . ' ' . end($resultNames); // add and-prefix of the last name if "and" is defined
160
            $resultNames[count($resultNames) - 1] = $new; //set prefixed last name at the last position of $resultNames array
161
        }
162
163
        if (!empty($this->and) && empty($this->etAl)) {
164
            switch ($this->delimiterPrecedesLast) {
165
                case 'after-inverted-name':
166
                    //TODO: implement
167
                    break;
168
                case 'always':
169
                    $text = implode($this->delimiter, $resultNames);
170
                    break;
171
                case 'never':
172
                    if (!$this->etAlUseLast) {
173
                        if (count($resultNames) === 1) {
174
                            $text = $resultNames[0];
175 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...
176
                            $text = implode(" ", $resultNames);
177
                        } else { // >2
178
                            $lastName = array_pop($resultNames);
179
                            $text = implode($this->delimiter, $resultNames) . " " . $lastName;
180
                        }
181
                    } /*else {
0 ignored issues
show
Unused Code Comprehensibility introduced by
51% 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...
182
                        if (count($resultNames) === 1) {
183
                            $text = $resultNames[0];
184
                        } else if (count($resultNames) === 2) {
185
                            $text = implode(" ", $resultNames);
186
                        } else { // >2
187
                            $lastName = array_pop($resultNames);
188
                            $text = implode($this->delimiter, $resultNames) . ", " . $lastName;
189
                        }
190
                    }*/
191
192
                    break;
193
                case 'contextual':
194
                default:
195 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...
196
                        $text = $resultNames[0];
197
                    } else if (count($resultNames) === 2) {
198
                        $text = implode(" ", $resultNames);
199
                    } else {
200
                        $text = implode($this->delimiter, $resultNames);
201
                    }
202
            }
203
        }
204
        if (empty($text)) {
205
            $text = implode($this->delimiter, $resultNames);
206
        }
207
208
        //append et al abbreviation
209
        if (count($data) > 1 &&
210
            !empty($resultNames) &&
211
            !empty($this->etAl) &&
212
            !empty($this->etAlMin) &&
213
            !empty($this->etAlUseFirst)) {
214
215
            $text = $this->appendEtAl($text, $resultNames);
216
        }
217
218
        /* A third value, “count”, returns the total number of names that would otherwise be rendered by the use of the
219
        cs:names element (taking into account the effects of et-al abbreviation and editor/translator collapsing),
220
        which allows for advanced sorting. */
221
222
        if ($this->form == 'count') {
223
            return (int) count($resultNames);
224
        }
225
226
        return $text;
227
    }
228
229
    /**
230
     * @param \stdClass $name
231
     * @param int $rank
232
     * @return string
233
     */
234
    private function formatName($name, $rank)
235
    {
236
237
        $nameObj = $this->cloneNamePOSC($name);
238
239
        $useInitials = $this->initialize && !is_null($this->initializeWith) && $this->initializeWith !== false;
240
        if ($useInitials && isset($name->given)) {
241
            $nameObj->given = StringHelper::initializeBySpaceOrHyphen($name->given, $this->initializeWith);
242
        }
243
244
        // format name-parts
245
        if (count($this->nameParts) > 0) {
246
            /** @var NamePart $namePart */
247
            foreach ($this->nameParts as $namePart) {
248
                $nameObj = $namePart->render($nameObj);
249
            }
250
        }
251
        $ret = $this->getNamesString($nameObj, $rank);
252
253
        return trim($ret);
254
    }
255
256
    /**
257
     * @param \stdClass $name
258
     * @param int $rank
259
     * @return string
260
     */
261
    private function getNamesString($name, $rank)
262
    {
263
        $text = "";
264
265
        if (!isset($name->family)) {
266
            return $text;
267
        }
268
269
        $given = !empty($name->given) ? trim($name->given) : "";
270
        $nonDroppingParticle = isset($name->{'non-dropping-particle'}) ? $name->{'non-dropping-particle'} : "";
271
        $droppingParticle = isset($name->{'dropping-particle'}) ? $name->{'dropping-particle'} : "";
272
        $suffix = (isset($name->{'suffix'})) ? $name->{'suffix'} : "";
273
274
        if (isset($name->family)) {
275
            $family = $name->family;
276
            if ($this->form == 'short') {
277
                $text = (!empty($nonDroppingParticle) ? $nonDroppingParticle . " " : "") . $family;
278
            } else {
279
                switch ($this->nameAsSortOrder) {
280
281
                    case 'all':
282
                    case 'first':
283
                        if ($this->nameAsSortOrder === "first" && $rank !== 0) {
284
                            break;
285
                        }
286
                        /*
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...
287
                        use form "[non-dropping particel] family name,
288
                        given name [dropping particle], [suffix]"
289
                        */
290
                        $text  = !empty($nonDroppingParticle) ? "$nonDroppingParticle " : "";
291
                        $text .= $family;
292
                        $text .= !empty($given) ? $this->sortSeparator . $given : "";
293
                        $text .= !empty($droppingParticle) ? " $droppingParticle" : "";
294
                        $text .= !empty($suffix) ? $this->sortSeparator . $suffix : "";
295
296
                        //remove last comma when no suffix exist.
297
                        $text = trim($text);
298
                        $text = substr($text, -1) === $this->sortSeparator ? substr($text, 0, strlen($text) - 1) : $text;
299
                        break;
300
                    default:
301
                        /*
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...
302
                        use form "given name [dropping particles] [non-dropping particles] family name [suffix]"
303
                        e.g. [Jean] [de] [La] [Fontaine] [III]
304
                        */
305
                        $text = sprintf(
306
                            "%s %s %s %s %s",
307
                            $given,
308
                            $droppingParticle,
309
                            $nonDroppingParticle,
310
                            $family,
311
                            $suffix);
312
313
                }
314
            }
315
        }
316
317
        //contains nbsp prefixed by normal space or followed by normal space?
318
        $text = htmlentities($text);
319
        if (strpos($text, " &nbsp;") !== false || strpos($text, "&nbsp; ") !== false) {
320
321
            $text = preg_replace("/[\s]+/", "", $text); //remove normal spaces
322
            return preg_replace("/&nbsp;+/", " ", $text);
323
        }
324
        $text = html_entity_decode(preg_replace("/[\s]+/", " ", $text));
325
        return $this->format(trim($text));
326
    }
327
328
    /**
329
     * @param $name
330
     * @return \stdClass
331
     */
332
    private function cloneNamePOSC($name)
333
    {
334
        $nameObj = new \stdClass();
335
        if (isset($name->family)) {
336
            $nameObj->family = $name->family;
337
        }
338
        if (isset($name->given)) {
339
            $nameObj->given = $name->given;
340
        }
341
        if (isset($name->{'non-dropping-particle'})) {
342
            $nameObj->{'non-dropping-particle'} = $name->{'non-dropping-particle'};
343
        }
344
        if (isset($name->{'dropping-particle'})) {
345
            $nameObj->{'dropping-particle'} = $name->{'dropping-particle'};
346
        }
347
        if (isset($name->{'suffix'})) {
348
            $nameObj->{'suffix'} = $name->{'suffix'};
349
        }
350
        return $nameObj;
351
    }
352
353
    /**
354
     * @param $text
355
     * @param $resultNames
356
     * @return string
357
     */
358
    protected function appendEtAl($text, $resultNames)
359
    {
360
        /* By default, when a name list is truncated to a single name, the name and the “et-al” (or “and others”)
361
        term are separated by a space (e.g. “Doe et al.”). When a name list is truncated to two or more names, the
362
        name delimiter is used (e.g. “Doe, Smith, et al.”). This behavior can be changed with the
363
        delimiter-precedes-et-al attribute. */
364
365
        switch ($this->delimiterPrecedesEtAl) {
366
            case 'never':
367
                $text = $text . " " . $this->etAl;
368
                break;
369
            case 'always':
370
                $text = $text . $this->delimiter . $this->etAl;
371
                break;
372
            case 'contextual':
373
            default:
374
                if (count($resultNames) === 1) {
375
                    $text .= " " . $this->etAl;
376
                } else {
377
                    $text .= $this->delimiter . $this->etAl;
378
                }
379
        }
380
381
        return $text;
382
    }
383
384
    /**
385
     * @param $resultNames
386
     * @return array
387
     */
388
    protected function prepareAbbreviation($resultNames)
389
    {
390
        $cnt = count($resultNames);
391
        /* Use of et-al-min and et-al-user-first enables et-al abbreviation. If the number of names in a name variable
392
        matches or exceeds the number set on et-al-min, the rendered name list is truncated after reaching the number of
393
        names set on et-al-use-first.  */
394
395
        if (isset($this->etAlMin) && isset($this->etAlUseFirst)) {
396
397
            if ($this->etAlMin <= $cnt) {
398
                if ($this->etAlUseLast && $this->etAlMin - $this->etAlUseFirst >= 2) {
399
                    /* et-al-use-last: When set to “true” (the default is “false”), name lists truncated by et-al
400
                    abbreviation are followed by the name delimiter, the ellipsis character, and the last name of the
401
                    original name list. This is only possible when the original name list has at least two more names
402
                    than the truncated name list (for this the value of et-al-use-first/et-al-subsequent-min must be at
403
                    least 2 less than the value of et-al-min/et-al-subsequent-use-first).*/
404
405
                    $lastName = array_pop($resultNames); //remove last Element and remember in $lastName
406
407
                }
408
                for ($i = $this->etAlUseFirst; $i < $cnt; ++$i) {
409
                    unset($resultNames[$i]);
410
                }
411
412
                $resultNames = array_values($resultNames);
413
414
                if (!empty($lastName)) { // append $lastName if exist
415
                    $resultNames[] = $lastName;
416
                }
417
418
                if ($this->parent->hasEtAl()) {
419
                    $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...
420
                    return $resultNames;
421
                } else {
422
                    $this->etAl = CiteProc::getContext()->getLocale()->filter('terms', 'et-al')->single;
423
                    return $resultNames;
424
                }
425
            }
426
            return $resultNames;
427
        }
428
        return $resultNames;
429
    }
430
431
    /**
432
     * @return Names
433
     */
434
    public function getParent()
435
    {
436
        return $this->parent;
437
    }
438
439
    /**
440
     * @param $data
441
     * @param \stdClass $preceding
442
     * @return array
443
     */
444
    protected function renderSubsequentSubstitution($data, $preceding)
445
    {
446
        $resultNames = [];
447
        $subsequentSubstitution = CiteProc::getContext()->getCitationItems()->getSubsequentAuthorSubstitute();
448
        $subsequentSubstitutionRule = CiteProc::getContext()->getCitationItems()->getSubsequentAuthorSubstituteRule();
449
450
        /**
451
         * @var string $type
452
         * @var array $name
453
         */
454
        foreach ($data as $rank => $name) {
455
456
            switch ($subsequentSubstitutionRule) {
457
458
                /* “partial-each” - when one or more rendered names in the name variable match those in the preceding
459
                bibliographic entry, the value of subsequent-author-substitute substitutes for each matching name.
460
                Matching starts with the first name, and continues up to the first mismatch. */
461 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...
462
463
                    if ($this->precedingHasAuthor($preceding, $name)) {
464
                        $resultNames[] = $subsequentSubstitution;
465
                    } else {
466
                        $resultNames[] = $this->formatName($name, $rank);
467
                    }
468
                    break;
469
470
471
                /* “partial-first” - as “partial-each”, but substitution is limited to the first name of the name
472
                variable. */
473
                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...
474
475
                    if ($rank === 0) {
476
                        if ($preceding->author[0]->family === $name->family) {
477
                            $resultNames[] = $subsequentSubstitution;
478
                        } else {
479
                            $resultNames[] = $this->formatName($name, $rank);
480
                        }
481
                    } else {
482
                        $resultNames[] = $this->formatName($name, $rank);
483
                    }
484
                    break;
485
486
                /* “complete-each” - requires a complete match like “complete-all”, but now the value of
487
                subsequent-author-substitute substitutes for each rendered name. */
488 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...
489
                    if ($this->identicalAuthors($preceding, $data)) {
490
                        $resultNames[] = $subsequentSubstitution;
491
                    } else {
492
                        $resultNames[] = $this->formatName($name, $rank);
493
                    }
494
                    break;
495
            }
496
        }
497
        return $resultNames;
498
    }
499
500
    /**
501
     * @param \stdClass $preceding
502
     * @param \stdClass $name
503
     * @return bool
504
     */
505
    public function precedingHasAuthor($preceding, $name)
506
    {
507
        foreach ($preceding->author as $author) {
508
            if ($author->family === $name->family && $author->given === $name->given) {
509
                return true;
510
            }
511
        }
512
        return false;
513
    }
514
515
    /**
516
     * @param \stdClass $precedingItem
517
     * @param array $currentAuthor
518
     * @return bool
519
     */
520
    private function identicalAuthors($precedingItem, $currentAuthor)
521
    {
522
        if (count($precedingItem->author) !== count($currentAuthor)) {
523
            return false;
524
        }
525
        foreach ($currentAuthor as $current) {
526
            if ($this->precedingHasAuthor($precedingItem, $current)) {
527
                continue;
528
            }
529
            return false;
530
        }
531
        return true;
532
    }
533
534
    /**
535
     * @param $data
536
     * @param $resultNames
537
     * @return array
538
     */
539
    protected function getFormattedNames($data, $resultNames)
540
    {
541
        foreach ($data as $rank => $name) {
542
            $resultNames[] = $this->formatName($name, $rank);
543
        }
544
        return $resultNames;
545
    }
546
547
    /**
548
     * @return string
549
     */
550
    public function getForm()
551
    {
552
        return $this->form;
553
    }
554
555
    /**
556
     * @return string
557
     */
558
    public function isNameAsSortOrder()
559
    {
560
        return $this->nameAsSortOrder;
561
    }
562
563
    /**
564
     * @return mixed
565
     */
566
    public function getDelimiter()
567
    {
568
        return $this->delimiter;
569
    }
570
571
    /**
572
     * @param mixed $delimiter
573
     */
574
    public function setDelimiter($delimiter)
575
    {
576
        $this->delimiter = $delimiter;
577
    }
578
579
580
}
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...
581