Completed
Push — version2.0 ( af5a57...2f739c )
by Sebastian
06:52
created

Name::getOptions()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 15
nc 5
nop 0
dl 0
loc 21
rs 7.551
c 0
b 0
f 0
1
<?php
2
3
namespace Seboettg\CiteProc\Rendering\Name;
4
use Seboettg\CiteProc\CiteProc;
5
use Seboettg\CiteProc\Styles\AffixesTrait;
6
use Seboettg\CiteProc\Styles\DelimiterTrait;
7
use Seboettg\CiteProc\Styles\FormattingTrait;
8
use Seboettg\CiteProc\Util\Factory;
9
use Seboettg\CiteProc\Util\StringHelper;
10
11
12
/**
13
 * Class Name
14
 *
15
 * The cs:name element, an optional child element of cs:names, can be used to describe the formatting of individual
16
 * names, and the separation of names within a name variable.
17
 *
18
 * @package Seboettg\CiteProc\Rendering\Name
19
 *
20
 * @author Sebastian Böttger <[email protected]>
21
 */
22
class Name
23
{
24
    use FormattingTrait,
25
        AffixesTrait,
26
        DelimiterTrait;
27
28
    /**
29
     * @var array
30
     */
31
    protected $nameParts;
32
33
    /**
34
     * Specifies the delimiter between the second to last and last name of the names in a name variable. Allowed values
35
     * are “text” (selects the “and” term, e.g. “Doe, Johnson and Smith”) and “symbol” (selects the ampersand,
36
     * e.g. “Doe, Johnson & Smith”).
37
     *
38
     * @var string
39
     */
40
    private $and;
41
42
    /**
43
     * Determines when the name delimiter or a space is used between a truncated name list and the “et-al”
44
     * (or “and others”) term in case of et-al abbreviation. Allowed values:
45
     * - “contextual” - (default), name delimiter is only used for name lists truncated to two or more names
46
     *   - 1 name: “J. Doe et al.”
47
     *   - 2 names: “J. Doe, S. Smith, et al.”
48
     * - “after-inverted-name” - name delimiter is only used if the preceding name is inverted as a result of the
49
     *   - name-as-sort-order attribute. E.g. with name-as-sort-order set to “first”:
50
     *   - “Doe, J., et al.”
51
     *   - “Doe, J., S. Smith et al.”
52
     * - “always” - name delimiter is always used
53
     *   - 1 name: “J. Doe, et al.”
54
     *   - 2 names: “J. Doe, S. Smith, et al.”
55
     * - “never” - name delimiter is never used
56
     *   - 1 name: “J. Doe et al.”
57
     *   - 2 names: “J. Doe, S. Smith et al.”
58
     *
59
     * @var string
60
     */
61
    private $delimiterPrecedesEtAl;
62
63
    /**
64
     * Determines when the name delimiter is used to separate the second to last and the last name in name lists (if
65
     * and is not set, the name delimiter is always used, regardless of the value of delimiter-precedes-last). Allowed
66
     * values:
67
     *
68
     * - “contextual” - (default), name delimiter is only used for name lists with three or more names
69
     *   - 2 names: “J. Doe and T. Williams”
70
     *   - 3 names: “J. Doe, S. Smith, and T. Williams”
71
     * - “after-inverted-name” - name delimiter is only used if the preceding name is inverted as a result of the
72
     *   name-as-sort-order attribute. E.g. with name-as-sort-order set to “first”:
73
     *   - “Doe, J., and T. Williams”
74
     *   - “Doe, J., S. Smith and T. Williams”
75
     * - “always” - name delimiter is always used
76
     *   - 2 names: “J. Doe, and T. Williams”
77
     *   - 3 names: “J. Doe, S. Smith, and T. Williams”
78
     * - “never” - name delimiter is never used
79
     *   - 2 names: “J. Doe and T. Williams”
80
     *   - 3 names: “J. Doe, S. Smith and T. Williams”
81
     *
82
     * @var string
83
     */
84
    private $delimiterPrecedesLast;
85
86
    /**
87
     * Use of etAlMin (et-al-min attribute) and etAlUseFirst (et-al-use-first attribute) enables et-al abbreviation. If
88
     * the number of names in a name variable matches or exceeds the number set on etAlMin, the rendered name list is
89
     * truncated after reaching the number of names set on etAlUseFirst.
90
     *
91
     * @var int
92
     */
93
    private $etAlMin;
94
95
    /**
96
     * Use of etAlMin (et-al-min attribute) and etAlUseFirst (et-al-use-first attribute) enables et-al abbreviation. If
97
     * the number of names in a name variable matches or exceeds the number set on etAlMin, the rendered name list is
98
     * truncated after reaching the number of names set on etAlUseFirst.
99
     *
100
     * @var int
101
     */
102
    private $etAlUseFirst;
103
104
    /**
105
     * If used, the values of these attributes (et-al-subsequent-min and et-al-subsequent-use-first) replace those of
106
     * respectively et-al-min and et-al-use-first for subsequent cites (cites referencing earlier cited items).
107
     *
108
     * @var int
109
     */
110
    private $etAlSubsequentMin;
111
112
    /**
113
     * If used, the values of these attributes (et-al-subsequent-min and et-al-subsequent-use-first) replace those of
114
     * respectively et-al-min and et-al-use-first for subsequent cites (cites referencing earlier cited items).
115
     *
116
     * @var int
117
     */
118
    private $etAlSubsequentUseFirst;
119
120
    /**
121
     * When set to “true” (the default is “false”), name lists truncated by et-al abbreviation are followed by the name
122
     * delimiter, the ellipsis character, and the last name of the original name list. This is only possible when the
123
     * original name list has at least two more names than the truncated name list (for this the value of
124
     * et-al-use-first/et-al-subsequent-min must be at least 2 less than the value of
125
     * et-al-min/et-al-subsequent-use-first).
126
     * A. Goffeau, B. G. Barrell, H. Bussey, R. W. Davis, B. Dujon, H. Feldmann, … S. G. Oliver
127
     *
128
     * @var bool
129
     */
130
    private $etAlUseLast = false;
131
132
    /**
133
     * Specifies whether all the name-parts of personal names should be displayed (value “long”, the default), or only
134
     * the family name and the non-dropping-particle (value “short”). A third value, “count”, returns the total number
135
     * of names that would otherwise be rendered by the use of the cs:names element (taking into account the effects of
136
     * et-al abbreviation and editor/translator collapsing), which allows for advanced sorting.
137
     *
138
     * @var string
139
     */
140
    private $form = "long";
141
142
    /**
143
     * When set to “false” (the default is “true”), given names are no longer initialized when “initialize-with” is set.
144
     * However, the value of “initialize-with” is still added after initials present in the full name (e.g. with
145
     * initialize set to “false”, and initialize-with set to ”.”, “James T Kirk” becomes “James T. Kirk”).
146
     *
147
     * @var bool
148
     */
149
    private $initialize = true;
150
151
    /**
152
     * When set, given names are converted to initials. The attribute value is added after each initial (”.” results
153
     * in “J.J. Doe”). For compound given names (e.g. “Jean-Luc”), hyphenation of the initials can be controlled with
154
     * the global initialize-with-hyphen option
155
     *
156
     * @var string
157
     */
158
    private $initializeWith = "";
159
160
    /**
161
     * Specifies that names should be displayed with the given name following the family name (e.g. “John Doe” becomes
162
     * “Doe, John”). The attribute has two possible values:
163
     *   - “first” - attribute only has an effect on the first name of each name variable
164
     *   - “all” - attribute has an effect on all names
165
     * Note that even when name-as-sort-order changes the name-part order, the display order is not necessarily the same
166
     * as the sorting order for names containing particles and suffixes (see Name-part order). Also, name-as-sort-order
167
     * only affects names written in the latin or Cyrillic alphabets. Names written in other alphabets (e.g. Asian
168
     * scripts) are always displayed with the family name preceding the given name.
169
     *
170
     * @var string
171
     */
172
    private $nameAsSortOrder = "";
173
174
    /**
175
     * Sets the delimiter for name-parts that have switched positions as a result of name-as-sort-order. The default
176
     * value is ”, ” (“Doe, John”). As is the case for name-as-sort-order, this attribute only affects names written in
177
     * the latin or Cyrillic alphabets.
178
     *
179
     * @var string
180
     */
181
    private $sortSeparator = ", ";
182
183
    /**
184
     * Specifies the text string used to separate names in a name variable. Default is ”, ” (e.g. “Doe, Smith”).
185
     * @var
186
     */
187
    private $delimiter = ", ";
188
189
190
    /**
191
     * @var Names
192
     */
193
    private $parent;
194
195
    /**
196
     * Name constructor.
197
     * @param \SimpleXMLElement $node
198
     * @param Names $parent
199
     */
200
    public function __construct(\SimpleXMLElement $node, Names $parent)
201
    {
202
        $this->nameParts = [];
203
        $this->parent = $parent;
204
205
        /** @var \SimpleXMLElement $child */
206
        foreach ($node->children() as $child) {
207
208
            switch ($child->getName()) {
209
                case "name-part":
210
                    /** @var NamePart $namePart */
211
                    $namePart = Factory::create($child, $this);
212
                    $this->nameParts[$namePart->getName()] = $namePart;
213
            }
214
        }
215
216
217
        /** @var \SimpleXMLElement $attribute */
218
        foreach ($node->attributes() as $attribute) {
219
            switch ($attribute->getName()) {
220
                case 'and':
221
                    $and = (string)$attribute;
222
                    if ("text" === $and) {
223
                        $this->and = CiteProc::getContext()->getLocale()->filter('terms', 'and')->single;
224
                    } elseif ('symbol' === $and) {
225
                        $this->and = '&';
226
                    }
227
                    break;
228
                case 'delimiter-precedes-et-al':
229
                    $this->delimiterPrecedesEtAl = (string) $attribute;
230
                    break;
231
                case 'delimiter-precedes-last':
232
                    $this->delimiterPrecedesLast = (string) $attribute;
233
                    break;
234
                case 'et-al-min':
235
                    $this->etAlMin = intval((string) $attribute);
236
                    break;
237
                case 'et-al-use-first':
238
                    $this->etAlUseFirst = intval((string) $attribute);
239
                    break;
240
                case 'et-al-subsequent-min':
241
                    $this->etAlSubsequentMin = intval((string) $attribute);
242
                    break;
243
                case 'et-al-subsequent-use-first':
244
                    $this->etAlSubsequentUseFirst = intval((string) $attribute);
245
                    break;
246
                case 'et-al-use-last':
247
                    $this->etAlUseLast = boolval((string) $attribute);
248
                    break;
249
                case 'form':
250
                    $this->form = (string) $attribute;
251
                    break;
252
                case 'initialize':
253
                    $this->initialize = boolval((string) $attribute);
254
                    break;
255
                case 'initialize-with':
256
                    $this->initializeWith = (string) $attribute;
257
                    break;
258
                case 'name-as-sort-order':
259
                    $this->nameAsSortOrder = (string) $attribute;
260
                    break;
261
                case 'sort-separator':
262
                    $this->sortSeparator = (string) $attribute;
263
264
            }
265
        }
266
267
        $this->initFormattingAttributes($node);
268
        $this->initAffixesAttributes($node);
269
        $this->initDelimiterAttributes($node);
270
    }
271
272
    public function render($data)
273
    {
274
        $resultNames = [];
275
        $etAl = false;
276
        $count = 0;
277
        /**
278
         * @var string $type
279
         * @var array $names
280
         */
281
        foreach ($data as $rank => $names) {
282
            ++$count;
283
            $resultNames[] = $this->formatName($names, $rank);
284
        }
285
286
        /* Use of et-al-min and et-al-user-first enables et-al abbreviation. If the number of names in a name variable
287
        matches or exceeds the number set on et-al-min, the rendered name list is truncated after reaching the number of
288
        names set on et-al-use-first.  */
289
        if (isset($this->etAlMin) && isset($this->etAlUseFirst)) {
290
            $cnt = count($resultNames);
291
            if ($this->etAlMin >= count($cnt)) {
292
                for ($i = $this->etAlUseFirst; $i < $cnt; ++$i) {
293
                    unset($resultNames[$i]);
294
                }
295
            }
296
            if ($this->parent->hasEtAl()) {
297
                $etAl = $this->parent->getEtAl()->render($names);
0 ignored issues
show
Bug introduced by
The variable $names seems to be defined by a foreach iteration on line 281. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
298
            } else {
299
                $etAl = CiteProc::getContext()->getLocale()->filter('terms', 'et-al')->single;
300
            }
301
        }
302
303
304
        /* add "and" */
305
        $count = count($resultNames);
306
        if (!empty($this->and) && $count > 1 && !$etAl) {
307
            $new = $this->and . ' ' . end($resultNames); //stick an "and" in front of the last author if "and" is defined
308
            $resultNames[key($resultNames)] = $new;
309
        }
310
311
        $text = implode($this->delimiter, $resultNames);
312
313
        if (!empty($resultNames) && $etAl) {
314
315
            /* By default, when a name list is truncated to a single name, the name and the “et-al” (or “and others”)
316
            term are separated by a space (e.g. “Doe et al.”). When a name list is truncated to two or more names, the
317
            name delimiter is used (e.g. “Doe, Smith, et al.”). This behavior can be changed with the
318
            delimiter-precedes-et-al attribute. */
319
            switch ($this->delimiterPrecedesEtAl) {
320
                case 'never':
321
                    $text = $text . " $etAl";
322
                    break;
323
                case 'always':
324
                    $text = $text . "$this->delimiter$etAl";
325
                    break;
326
                default:
327
                    if (count($resultNames) === 1) {
328
                        $text .= " $etAl";
329
                    } else {
330
                        $text .=  $this->delimiter . $etAl;
331
                    }
332
333
            }
334
        }
335
        if ($this->form == 'count') {
336
            if ($etAl === false) {
337
                return (int)count($resultNames);
338
            } else {
339
                return (int)(count($resultNames) - 1);
340
            }
341
        }
342
        // strip out the last delimiter if not required
343
        if (isset($this->and) && count($resultNames) > 1) {
344
            $lastDelimiter = strrpos($text, $this->delimiter . $this->and);
345
            switch ($this->delimiterPrecedesLast) {
346
                case 'always':
347
                    return $text;
348
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
349
                case 'never':
350
                    return substr_replace($text, ' ', $lastDelimiter, strlen($this->delimiter));
351
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
352
                case 'contextual':
353
                default:
354
                    if (count($resultNames) < 3) {
355
                        return substr_replace($text, ' ', $lastDelimiter, strlen($this->delimiter));
356
                    }
357
            }
358
        }
359
        return $text;
360
    }
361
362
    private function formatName($name, $rank)
363
    {
364
        $useInitials = $this->initialize && !empty($this->initializeWith);
365
        if ($useInitials) {
366
            //TODO: initialize with hyphen
367
            $given = $name->given;
368
            $name->given = "";
369
            $givenParts = StringHelper::explodeBySpaceOrHyphen($given);
370
            foreach ($givenParts as $givenPart) {
371
                $name->given .= substr($givenPart, 0, 1) . $this->initializeWith;
372
            }
373
        }
374
375
        // format name-parts
376
        if (count($this->nameParts) > 0) {
377
            /** @var NamePart $namePart */
378
            foreach ($this->nameParts as $namePart) {
379
                $name->{$namePart->getName()} =   $namePart->render($name);
380
            }
381
            $name->suffix = '';
382
            $name->{'non-dropping-particle'} = '';
383
            $name->{'dropping-particle'} = '';
384
        }
385
386
        $return = $this->getNamesString($name, $rank);
387
388
        return trim($return);
389
    }
390
391
    /**
392
     * @param $name
393
     * @return string
394
     */
395
    private function getNamesString($name, $rank)
396
    {
397
        $text = "";
398
        $nonDroppingParticle = isset($name->{'non-dropping-particle'}) ? $name->{'non-dropping-particle'} : "";
399
        $droppingParticle = isset($name->{'dropping-particle'}) ? $name->{'dropping-particle'} : "";
400
        $suffix = (isset($name->{'suffix'})) ? ' ' . $name->{'suffix'} : '';
401
        if (!empty($name->given)) {
402
            $name->given = $this->format(trim($name->given));
403
        }
404
        if (isset($name->family)) {
405
            $name->family = $this->format($name->family);
406
            if ($this->form == 'short') {
407
                $text = (!empty($nonDroppingParticle) ? $nonDroppingParticle . " " : "") . $name->family;
408
            } else {
409
                switch ($this->nameAsSortOrder) {
410
                    /*
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...
411
                        use form "[non-dropping particel] family name,
412
                        given name [dropping particle], [suffix]"
413
                     */
414
                    case 'all':
415
                    case 'first':
416
                        if ($this->nameAsSortOrder === "first" && $rank !== 0) {
417
                            break;
418
                        }
419
                        $text =
420
                            (!empty($nonDroppingParticle) ? $nonDroppingParticle . " " : "") .
421
                            (trim($name->family) . $this->sortSeparator . trim($name->given)) .
422
                            (!empty($droppingParticle) ? " " . $droppingParticle : "") .
423
                            (!empty($suffix) ? $this->sortSeparator . trim($suffix) : "");
424
                        break;
425
                    /*
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...
426
                       use form "given name [dropping particles] [non-dropping particles] family name [suffix]"
427
                       e.g. [Jean] [de] [La] [Fontaine] [III]
428
                    */
429
                    default:
430
                        $text = trim($name->given) .
431
                            (!empty($droppingParticle) ? " " . trim($droppingParticle) : "") .
432
                            (!empty($nonDroppingParticle) ? " " . trim($nonDroppingParticle) : "") .
433
                            (" " . $name->family) .
434
                            (!empty($suffix) ? " " . trim($suffix) : "");
435
                }
436
            }
437
        }
438
439
        return $text;
440
    }
441
442
    public function getOptions()
443
    {
444
        $ignore = ["namePart", "parent", "substitute"];
445
        $options = [];
446
        $reflectedName = new \ReflectionClass($this);
447
448
        foreach ($reflectedName->getProperties() as $property) {
449
            $property->setAccessible(true);
450
            if (in_array($property->getName(), $ignore)) {
451
                continue;
452
            } else if ($property->getName() == "and" && $property->getValue($this) === "&") {
453
                $options["and"] = "symbol";
454
            } else {
455
                $propValue = $property->getValue($this);
456
                if (isset($propValue) && !empty($propValue)) {
457
                    $options[StringHelper::camelCase2Hyphen($property->getName())] = $propValue;
458
                }
459
            }
460
        }
461
        return $options;
462
    }
463
464
}