Test Failed
Push — new-api ( 275856...44902a )
by Sebastian
07:02
created

Date::renderStyles()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
/*
3
 * citeproc-php
4
 *
5
 * @link        http://github.com/seboettg/citeproc-php for the source repository
6
 * @copyright   Copyright (c) 2016 Sebastian Böttger.
7
 * @license     https://opensource.org/licenses/MIT
8
 */
9
10
namespace Seboettg\CiteProc\Rendering\Date;
11
12
use Exception;
13
use Seboettg\CiteProc\CiteProc;
14
use Seboettg\CiteProc\Exception\CiteProcException;
15
use Seboettg\CiteProc\Exception\InvalidStylesheetException;
16
use Seboettg\CiteProc\Rendering\Date\DateRange\DateRangeRenderer;
17
use Seboettg\CiteProc\Rendering\Observer\RenderingObserver;
18
use Seboettg\CiteProc\Rendering\Observer\RenderingObserverTrait;
19
use Seboettg\CiteProc\Styles\StylesRenderer;
20
use Seboettg\CiteProc\Util;
21
use Seboettg\Collection\ArrayList;
22
use SimpleXMLElement;
23
24
/**
25
 * Class Date
26
 * @package Seboettg\CiteProc\Rendering
27
 *
28
 * @author Sebastian Böttger <[email protected]>
29
 */
30
class Date implements RenderingObserver
31
{
32
    use RenderingObserverTrait;
33
34
    // bitmask: ymd
35
    const DATE_RANGE_STATE_NONE         = 0; // 000
36
    const DATE_RANGE_STATE_DAY          = 1; // 001
37
    const DATE_RANGE_STATE_MONTH        = 2; // 010
38
    const DATE_RANGE_STATE_MONTHDAY     = 3; // 011
39
    const DATE_RANGE_STATE_YEAR         = 4; // 100
40
    const DATE_RANGE_STATE_YEARDAY      = 5; // 101
41
    const DATE_RANGE_STATE_YEARMONTH    = 6; // 110
42
    const DATE_RANGE_STATE_YEARMONTHDAY = 7; // 111
43
44
    public static function factory(SimpleXMLElement $node)
45
    {
46
        $variable = $form = $datePartsAttribute = "";
47
        $dateParts = new ArrayList();
48
        foreach ($node->attributes() as $attribute) {
49
            switch ($attribute->getName()) {
50
                case 'form':
51
                    $form = (string) $attribute;
52
                    break;
53
                case 'variable':
54
                    $variable = (string) $attribute;
55
                    break;
56
                case 'date-parts':
57
                    $datePartsAttribute = (string) $attribute;
58
            }
59
        }
60
        foreach ($node->children() as $child) {
61
            if ($child->getName() === "date-part") {
62
                $datePartName = (string) $child->attributes()["name"];
63
                $dateParts->set($form . "-" . $datePartName, Util\Factory::create($child));
64
            }
65
        }
66
        $stylesRenderer = StylesRenderer::factory($node);
67
        return new self(
68
            $variable,
69
            $form,
70
            $datePartsAttribute,
71
            $dateParts,
72
            $stylesRenderer
73
        );
74
    }
75
76
    public function __construct(
77
        string $variable,
78 72
        string $form,
79
        string $datePartsAttribute,
80 72
        ArrayList $dateParts,
81
        StylesRenderer $stylesRenderer
82
    ) {
83 72
        $this->variable = $variable;
84 72
        $this->form = $form;
85 72
        $this->datePartsAttribute = $datePartsAttribute;
86 36
        $this->dateParts = $dateParts;
87 36
        $this->stylesRenderer = $stylesRenderer;
88 72
    }
89 72
90 72
    private static $localizedDateFormats = [
91 45
        'numeric',
92 72
        'text'
93
    ];
94
95
    /**
96 72
     * @var ArrayList
97 56
     */
98 56
    private $dateParts;
99 56
100
    /**
101
     * @var string
102
     */
103 72
    private $form = "";
104 72
105 72
    /**
106 72
     * @var string
107 72
     */
108
    private $variable = "";
109
110
    /**
111
     * @var string
112
     */
113
    private $datePartsAttribute = "";
114
115 65
    /**
116
     * @var StylesRenderer
117 65
     */
118 65
    private $stylesRenderer;
119 65
120 61
    /**
121
     * @param $data
122 10
     * @return string
123
     * @throws InvalidStylesheetException
124
     * @throws Exception
125
     */
126 61
    public function render($data)
127 2
    {
128 2
        $ret = "";
129 2
        $var = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $var is dead and can be removed.
Loading history...
130 1
        if (isset($data->{$this->variable})) {
131
            $var = $data->{$this->variable};
132 1
        } else {
133 1
            return "";
134 1
        }
135
136
        try {
137
            $this->prepareDatePartsInVariable($data, $var);
138
        } catch (CiteProcException $e) {
139
            if (isset($data->{$this->variable}->{'raw'}) &&
140 60
                !preg_match("/(\p{L}+)\s?([\-\–&,])\s?(\p{L}+)/u", $data->{$this->variable}->{'raw'})) {
141 60
                return $this->renderStyles($data->{$this->variable}->{'raw'});
142 60
            } else {
143
                if (isset($data->{$this->variable}->{'string-literal'})) {
144
                    return $this->renderStyles($data->{$this->variable}->{'string-literal'});
145
                }
146 60
            }
147
        }
148 48
149 48
        $form = $this->form;
150
        $dateParts = !empty($this->datePartsAttribute) ? explode("-", $this->datePartsAttribute) : [];
151
        $this->prepareDatePartsChildren($dateParts, $form);
152
153
        // No date-parts in date-part attribute defined, take into account that the defined date-part children will
154
        // be used.
155
        if (empty($this->datePartsAttribute) && $this->dateParts->count() > 0) {
156
            /** @var DatePart $part */
157
            foreach ($this->dateParts as $part) {
158
                $dateParts[] = $part->getName();
159 60
            }
160 59
        }
161
162
        /* cs:date may have one or more cs:date-part child elements (see Date-part). The attributes set on
163
        these elements override those specified for the localized date formats (e.g. to get abbreviated months for all
164 59
        locales, the form attribute on the month-cs:date-part element can be set to “short”). These cs:date-part
165 57
        elements do not affect which, or in what order, date parts are rendered. Affixes, which are very
166 57
        locale-specific, are not allowed on these cs:date-part elements. */
167 2
168 2
        if ($this->dateParts->count() > 0) {
169 2
            if (!isset($var->{'date-parts'})) { // ignore empty date-parts
170 2
                return "";
171 2
            }
172 2
173 2
            if (count($data->{$this->variable}->{'date-parts'}) === 1) {
174 2
                $data_ = $this->createDateTime($data->{$this->variable}->{'date-parts'});
175 1
                $ret .= $this->iterateAndRenderDateParts($dateParts, $data_);
176 1
            } elseif (count($var->{'date-parts'}) === 2) { //date range
177
                $data_ = $this->createDateTime($var->{'date-parts'});
178 2
                $from = $data_[0];
179 1
                $to = $data_[1];
180 1
                $interval = $to->diff($from);
181
                $delimiter = "";
182 2
                $toRender = 0;
183 1
                if ($interval->y > 0 && in_array('year', $dateParts)) {
184 1
                    $toRender |= self::DATE_RANGE_STATE_YEAR;
185
                    $delimiter = $this->dateParts->get($this->form."-year")->getRangeDelimiter();
186 2
                }
187 1
                if ($interval->m > 0 && $from->getMonth() - $to->getMonth() !== 0 && in_array('month', $dateParts)) {
188
                    $toRender |= self::DATE_RANGE_STATE_MONTH;
189 1
                    $delimiter = $this->dateParts->get($this->form."-month")->getRangeDelimiter();
190
                }
191
                if ($interval->d > 0 && $from->getDay() - $to->getDay() !== 0 && in_array('day', $dateParts)) {
192
                    $toRender |= self::DATE_RANGE_STATE_DAY;
193 59
                    $delimiter = $this->dateParts->get($this->form."-day")->getRangeDelimiter();
194 59
                }
195
                if ($toRender === self::DATE_RANGE_STATE_NONE) {
196 1
                    $ret .= $this->iterateAndRenderDateParts($dateParts, $data_);
197
                } else {
198
                    $ret .= $this->renderDateRange($toRender, $from, $to, $delimiter);
199
                }
200 1
            }
201 1
202
            if (isset($var->raw) && preg_match("/(\p{L}+)\s?([\-\–&,])\s?(\p{L}+)/u", $var->raw, $matches)) {
203
                return $matches[1].$matches[2].$matches[3];
204 60
            }
205
        } elseif (!empty($this->datePartsAttribute)) {
206
            // fallback:
207
            // When there are no dateParts children, but date-parts attribute in date
208
            // render numeric
209
            $data = $this->createDateTime($var->{'date-parts'});
210
            $ret = $this->renderNumeric($data[0]);
211
        }
212 60
213
        return !empty($ret) ? $this->renderStyles($ret) : "";
214 60
    }
215 60
216 60
    /**
217 60
     * @param array $dates
218 1
     * @return array
219 1
     * @throws Exception
220 1
     */
221
    private function createDateTime($dates)
222 60
    {
223 60
        $data = [];
224 60
        foreach ($dates as $date) {
225 60
            $date = $this->cleanDate($date);
226
            if ($date[0] < 1000) {
227 60
                $dateTime = new DateTime(0, 0, 0);
228 31
                $dateTime->setDay(0)->setMonth(0)->setYear(0);
229
                $data[] = $dateTime;
230 60
            }
231 36
            $dateTime = new DateTime(
232
                $date[0],
233 60
                array_key_exists(1, $date) ? $date[1] : 1,
234
                array_key_exists(2, $date) ? $date[2] : 1
235
            );
236 60
            if (!array_key_exists(1, $date)) {
237
                $dateTime->setMonth(0);
238
            }
239
            if (!array_key_exists(2, $date)) {
240
                $dateTime->setDay(0);
241
            }
242
            $data[] = $dateTime;
243
        }
244
245
        return $data;
246 1
    }
247
248 1
    /**
249 1
     * @param int $toRender
250
     * @param DateTime $from
251
     * @param DateTime $to
252
     * @param $delimiter
253
     * @return string
254
     */
255
    private function renderDateRange($toRender, DateTime $from, DateTime $to, $delimiter)
256 16
    {
257
        $datePartRenderer = DateRangeRenderer::factory($this, $toRender);
258 16
        return $datePartRenderer->parseDateRange($this->dateParts, $from, $to, $delimiter);
259 16
    }
260
261
    /**
262
     * @param string $format
263
     * @return bool
264
     */
265
    private function hasDatePartsFromLocales($format)
266 16
    {
267
        $dateXml = CiteProc::getContext()->getLocale()->getDateXml();
268 16
        return !empty($dateXml[$format]);
269
    }
270 16
271 16
    /**
272
     * @param string $format
273
     * @return array
274 16
     */
275 16
    private function getDatePartsFromLocales($format)
276 16
    {
277
        $ret = [];
278
        // date parts from locales
279 16
        $dateFromLocale_ = CiteProc::getContext()->getLocale()->getDateXml();
280 16
        $dateFromLocale = $dateFromLocale_[$format];
281 16
282
        // no custom date parts within the date element (this)?
283
        if (!empty($dateFromLocale)) {
284
            $dateForm = array_filter(
285 16
                is_array($dateFromLocale) ? $dateFromLocale : [$dateFromLocale],
286
                function ($element) use ($format) {
287 16
                    /** @var SimpleXMLElement $element */
288 16
                    $dateForm = (string) $element->attributes()["form"];
289 16
                    return $dateForm === $format;
290
                }
291
            );
292
293 16
            //has dateForm from locale children (date-part elements)?
294
            $localeDate = array_pop($dateForm);
295
296
            if ($localeDate instanceof SimpleXMLElement && $localeDate->count() > 0) {
297
                foreach ($localeDate as $child) {
298
                    $ret[] = $child;
299 26
                }
300
            }
301 26
        }
302
        return $ret;
303
    }
304
305
    /**
306
     * @return string
307
     */
308
    public function getVariable()
309 61
    {
310
        return $this->variable;
311 61
    }
312 8
313
    /**
314 7
     * @param $data
315
     * @param $var
316 1
     * @throws CiteProcException
317
     */
318
    private function prepareDatePartsInVariable($data, $var)
319 60
    {
320
        if (!isset($data->{$this->variable}->{'date-parts'}) || empty($data->{$this->variable}->{'date-parts'})) {
321
            if (isset($data->{$this->variable}->raw) && !empty($data->{$this->variable}->raw)) {
322
                // try to parse date parts from "raw" attribute
323
                $var->{'date-parts'} = Util\DateHelper::parseDateParts($data->{$this->variable});
324
            } else {
325
                throw new CiteProcException("No valid date format");
326 60
            }
327
        }
328
    }
329
330
    /**
331
     * @param $dateParts
332
     * @param string $form
333
     * @throws InvalidStylesheetException
334
     */
335
    private function prepareDatePartsChildren($dateParts, $form)
336 60
    {
337 16
        /* Localized date formats are selected with the optional form attribute, which must set to either “numeric”
338 16
        (for fully numeric formats, e.g. “12-15-2005”), or “text” (for formats with a non-numeric month, e.g.
339
        “December 15, 2005”). Localized date formats can be customized in two ways. First, the date-parts attribute may
340 16
        be used to show fewer date parts. The possible values are:
341 16
            - “year-month-day” - (default), renders the year, month and day
342
            - “year-month” - renders the year and month
343 16
            - “year” - renders the year */
344 16
345 16
        if ($this->dateParts->count() < 1 && in_array($form, self::$localizedDateFormats)) {
346
            if ($this->hasDatePartsFromLocales($form)) {
347
                $datePartsFromLocales = $this->getDatePartsFromLocales($form);
348
                array_filter($datePartsFromLocales, function (SimpleXMLElement $item) use ($dateParts) {
349
                    return in_array($item["name"], $dateParts);
350
                });
351
352
                foreach ($datePartsFromLocales as $datePartNode) {
353
                    $datePart = $datePartNode["name"];
354
                    $this->dateParts->set("$form-$datePart", Util\Factory::create($datePartNode));
355
                }
356
            } else { //otherwise create default date parts
357
                foreach ($dateParts as $datePart) {
358 60
                    $this->dateParts->add(
359
                        "$form-$datePart",
360
                        new DatePart(
361 1
                            new SimpleXMLElement('<date-part name="'.$datePart.'" form="'.$form.'" />')
362
                        )
363 1
                    );
364
                }
365
            }
366 12
        }
367
    }
368 12
369
370
    private function renderNumeric(DateTime $date)
371 60
    {
372
        return $date->renderNumeric();
373 60
    }
374 60
375 60
    public function getForm()
376
    {
377 60
        return $this->form;
378
    }
379
380
    private function cleanDate($date)
381
    {
382
        $ret = [];
383
        foreach ($date as $key => $datePart) {
384
            $ret[$key] = Util\NumberHelper::extractNumber(Util\StringHelper::removeBrackets($datePart));
385 58
        }
386
        return $ret;
387 58
    }
388
389 58
    /**
390
     * @param array $dateParts
391 58
     * @param array $data_
392 58
     * @return string
393 58
     */
394
    private function iterateAndRenderDateParts(array $dateParts, array $data_)
395
    {
396 58
        $result = [];
397
        /** @var DatePart $datePart */
398
        foreach ($this->dateParts as $key => $datePart) {
399
            /** @noinspection PhpUnusedLocalVariableInspection */
400
            list($f, $p) = explode("-", $key);
401
            if (in_array($p, $dateParts)) {
402
                $result[] = $datePart->render($data_[0], $this);
403
            }
404
        }
405
        $result = array_filter($result);
406
        $glue = $this->datePartsHaveAffixes() ? "" : " ";
407
        $return = implode($glue, $result);
408
        return trim($return);
409
    }
410
411
    /**
412
     * @return bool
413
     */
414
    private function datePartsHaveAffixes()
415
    {
416
        $result = $this->dateParts->filter(function (DatePart $datePart) {
417
            return $datePart->renderSuffix() !== "" || $datePart->renderPrefix() !== "";
418
        });
419
        return $result->count() > 0;
420
    }
421
422
    /**
423
     * @param string $var
424
     * @return string
425
     */
426
    private function renderStyles(string $var): string
427
    {
428
        return $this->stylesRenderer->renderAffixes(
429
            $this->stylesRenderer->renderFormatting(
430
                $this->stylesRenderer->renderTextCase($var)
431
            )
432
        );
433
    }
434
}
435