48 private static $localizedDateFormats = [
66 private $variable =
"";
71 private $datePartsAttribute =
"";
83 foreach ($node->attributes() as $attribute) {
84 switch ($attribute->getName()) {
86 $this->form = (string) $attribute;
89 $this->variable = (string) $attribute;
92 $this->datePartsAttribute = (string) $attribute;
96 foreach ($node->children() as $child) {
97 if ($child->getName() ===
"date-part") {
98 $datePartName = (string) $child->attributes()[
"name"];
99 $this->dateParts->set($this->form.
"-".$datePartName, Util\
Factory::create($child));
103 $this->initAffixesAttributes($node);
104 $this->initDisplayAttributes($node);
105 $this->initFormattingAttributes($node);
106 $this->initTextCaseAttributes($node);
115 public function render($data)
119 if (isset($data->{$this->variable})) {
120 $var = $data->{$this->variable};
126 $this->prepareDatePartsInVariable($data, $var);
128 if (isset($data->{$this->variable}->{
'raw'}) &&
129 !preg_match(
"/(\p{L}+)\s?([\-\ &,])\s?(\p{L}+)/u", $data->{$this->variable}->{'raw'})) {
return $this->addAffixes($this->format($this->applyTextCase($data->{$this->variable}->{'raw'})));
} else {
if (isset($data->{$this->variable}->{'string-literal'})) {
return $this->addAffixes(
$this->format($this->applyTextCase($data->{$this->variable}->{'string-literal'}))
);
}
}
}
$form = $this->form;
$dateParts = !empty($this->datePartsAttribute) ? explode("-", $this->datePartsAttribute) : [];
$this->prepareDatePartsChildren($dateParts, $form);
// No date-parts in date-part attribute defined, take into account that the defined date-part children will
// be used.
if (empty($this->datePartsAttribute) && $this->dateParts->count() > 0) {
/** @var DatePart $part */
foreach ($this->dateParts as $part) {
$dateParts[] = $part->getName();
}
}
/* cs:date may have one or more cs:date-part child elements (see Date-part). The attributes set on
these elements override those specified for the localized date formats (e.g. to get abbreviated months for all
locales, the form attribute on the month-cs:date-part element can be set to “short”). These cs:date-part
elements do not affect which, or in what order, date parts are rendered. Affixes, which are very
locale-specific, are not allowed on these cs:date-part elements. */
if ($this->dateParts->count() > 0) {
if (!isset($var->{'date-parts'})) { // ignore empty date-parts
return "";
}
if (count($data->{$this->variable}->{'date-parts'}) === 1) {
$data_ = $this->createDateTime($data->{$this->variable}->{'date-parts'});
$ret .= $this->iterateAndRenderDateParts($dateParts, $data_);
} elseif (count($var->{'date-parts'}) === 2) { //date range
$data_ = $this->createDateTime($var->{'date-parts'});
$from = $data_[0];
$to = $data_[1];
$interval = $to->diff($from);
$delimiter = "";
$toRender = 0;
if ($interval->y > 0 && in_array('year', $dateParts)) {
$toRender |= self::DATE_RANGE_STATE_YEAR;
$delimiter = $this->dateParts->get($this->form."-year")->getRangeDelimiter();
}
if ($interval->m > 0 && $from->getMonth() - $to->getMonth() !== 0 && in_array('month', $dateParts)) {
$toRender |= self::DATE_RANGE_STATE_MONTH;
$delimiter = $this->dateParts->get($this->form."-month")->getRangeDelimiter();
}
if ($interval->d > 0 && $from->getDay() - $to->getDay() !== 0 && in_array('day', $dateParts)) {
$toRender |= self::DATE_RANGE_STATE_DAY;
$delimiter = $this->dateParts->get($this->form."-day")->getRangeDelimiter();
}
if ($toRender === self::DATE_RANGE_STATE_NONE) {
$ret .= $this->iterateAndRenderDateParts($dateParts, $data_);
} else {
$ret .= $this->renderDateRange($toRender, $from, $to, $delimiter);
}
}
if (isset($var->raw) && preg_match("/(\p{L}+)\s?([\-\–&,])\s?(\p{L}+)/u", $var->raw, $matches)) {
return $matches[1].$matches[2].$matches[3];
}
} elseif (!empty($this->datePartsAttribute)) {
// fallback:
// When there are no dateParts children, but date-parts attribute in date
// render numeric
$data = $this->createDateTime($var->{'date-parts'});
$ret = $this->renderNumeric($data[0]);
}
return !empty($ret) ? $this->addAffixes($this->format($this->applyTextCase($ret))) : "";
}
/**
* @param array $dates
* @return array
* @throws Exception
*/
private function createDateTime($dates)
{
$data = [];
foreach ($dates as $date) {
$date = $this->cleanDate($date);
if ($date[0] < 1000) {
$dateTime = new DateTime(0, 0, 0);
$dateTime->setDay(0)->setMonth(0)->setYear(0);
$data[] = $dateTime;
}
$dateTime = new DateTime(
$date[0],
array_key_exists(1, $date) ? $date[1] : 1,
array_key_exists(2, $date) ? $date[2] : 1
);
if (!array_key_exists(1, $date)) {
$dateTime->setMonth(0);
}
if (!array_key_exists(2, $date)) {
$dateTime->setDay(0);
}
$data[] = $dateTime;
}
return $data;
}
/**
* @param int $toRender
* @param DateTime $from
* @param DateTime $to
* @param $delimiter
* @return string
*/
private function renderDateRange($toRender, DateTime $from, DateTime $to, $delimiter)
{
$datePartRenderer = DateRangeRenderer::factory($this, $toRender);
return $datePartRenderer->parseDateRange($this->dateParts, $from, $to, $delimiter);
}
/**
* @param string $format
* @return bool
*/
private function hasDatePartsFromLocales($format)
{
$dateXml = CiteProc::getContext()->getLocale()->getDateXml();
return !empty($dateXml[$format]);
}
/**
* @param string $format
* @return array
*/
private function getDatePartsFromLocales($format)
{
$ret = [];
// date parts from locales
$dateFromLocale_ = CiteProc::getContext()->getLocale()->getDateXml();
$dateFromLocale = $dateFromLocale_[$format];
// no custom date parts within the date element (this)?
if (!empty($dateFromLocale)) {
$dateForm = array_filter(
is_array($dateFromLocale) ? $dateFromLocale : [$dateFromLocale],
function ($element) use ($format) {
/** @var SimpleXMLElement $element */
$dateForm = (string) $element->attributes()["form"];
return $dateForm === $format;
}
);
//has dateForm from locale children (date-part elements)?
$localeDate = array_pop($dateForm);
if ($localeDate instanceof SimpleXMLElement && $localeDate->count() > 0) {
foreach ($localeDate as $child) {
$ret[] = $child;
}
}
}
return $ret;
}
/**
* @return string
*/
public function getVariable()
{
return $this->variable;
}
/**
* @param $data
* @param $var
* @throws CiteProcException
*/
private function prepareDatePartsInVariable($data, $var)
{
if (!isset($data->{$this->variable}->{'date-parts'}) || empty($data->{$this->variable}->{'date-parts'})) {
if (isset($data->{$this->variable}->raw) && !empty($data->{$this->variable}->raw)) {
// try to parse date parts from "raw" attribute
$var->{'date-parts'} = Util\DateHelper::parseDateParts($data->{$this->variable});
} else {
throw new CiteProcException("No valid date format");
}
}
}
/**
* @param $dateParts
* @param string $form
* @throws InvalidStylesheetException
*/
private function prepareDatePartsChildren($dateParts, $form)
{
/* Localized date formats are selected with the optional form attribute, which must set to either “numeric”
(for fully numeric formats, e.g. “12-15-2005”), or “text” (for formats with a non-numeric month, e.g.
“December 15, 2005”). Localized date formats can be customized in two ways. First, the date-parts attribute may
be used to show fewer date parts. The possible values are:
- “year-month-day” - (default), renders the year, month and day
- “year-month” - renders the year and month
- “year” - renders the year */
if ($this->dateParts->count() < 1 && in_array($form, self::$localizedDateFormats)) {
if ($this->hasDatePartsFromLocales($form)) {
$datePartsFromLocales = $this->getDatePartsFromLocales($form);
array_filter($datePartsFromLocales, function (SimpleXMLElement $item) use ($dateParts) {
return in_array($item["name"], $dateParts);
});
foreach ($datePartsFromLocales as $datePartNode) {
$datePart = $datePartNode["name"];
$this->dateParts->set("$form-$datePart", Util\Factory::create($datePartNode));
}
} else { //otherwise create default date parts
foreach ($dateParts as $datePart) {
$this->dateParts->add(
"$form-$datePart",
new DatePart(
new SimpleXMLElement('<date-part name="'.$datePart.'" form="'.$form.'" />')
)
);
}
}
}
}
private function renderNumeric(DateTime $date)
{
return $date->renderNumeric();
}
public function getForm()
{
return $this->form;
}
private function cleanDate($date)
{
$ret = [];
foreach ($date as $key => $datePart) {
$ret[$key] = Util\NumberHelper::extractNumber(Util\StringHelper::removeBrackets($datePart));
}
return $ret;
}
/**
* @param array $dateParts
* @param array $data_
* @return string
*/
private function iterateAndRenderDateParts(array $dateParts, array $data_)
{
$ret = "";
/** @var DatePart $datePart */
foreach ($this->dateParts as $key => $datePart) {
/** @noinspection PhpUnusedLocalVariableInspection */
list($f, $p) = explode("-", $key);
if (in_array($p, $dateParts)) {
$ret .= $datePart->render($data_[0], $this);
}
}
return $ret;
}
}
&,])\s?(\p{L}+)/u", $data->{$this->variable}->{
'raw'})) {
130 return $this->addAffixes($this->format($this->applyTextCase($data->{$this->variable}->{
'raw'})));
132 if (isset($data->{$this->variable}->{
'string-literal'})) {
141 $dateParts = !empty($this->datePartsAttribute) ? explode(
"-", $this->datePartsAttribute) : [];
142 $this->prepareDatePartsChildren($dateParts, $form);
146 if (empty($this->datePartsAttribute) && $this->dateParts->count() > 0) {
148 foreach ($this->dateParts as $part) {
149 $dateParts[] = $part->getName();
159 if ($this->dateParts->count() > 0) {
160 if (!isset($var->{
'date-parts'})) {
164 if (
count($data->{$this->variable}->{
'date-parts'}) === 1) {
165 $data_ = $this->createDateTime($data->{$this->variable}->{
'date-parts'});
166 $ret .= $this->iterateAndRenderDateParts($dateParts, $data_);
167 } elseif (
count($var->{
'date-parts'}) === 2) {
168 $data_ = $this->createDateTime($var->{
'date-parts'});
171 $interval = $to->diff($from);
174 if ($interval->y > 0 && in_array(
'year', $dateParts)) {
175 $toRender |= self::DATE_RANGE_STATE_YEAR;
176 $delimiter = $this->dateParts->get($this->form.
"-year")->getRangeDelimiter();
178 if ($interval->m > 0 && $from->getMonth() - $to->getMonth() !== 0 && in_array(
'month', $dateParts)) {
179 $toRender |= self::DATE_RANGE_STATE_MONTH;
180 $delimiter = $this->dateParts->get($this->form.
"-month")->getRangeDelimiter();
182 if ($interval->d > 0 && $from->getDay() - $to->getDay() !== 0 && in_array(
'day', $dateParts)) {
183 $toRender |= self::DATE_RANGE_STATE_DAY;
184 $delimiter = $this->dateParts->get($this->form.
"-day")->getRangeDelimiter();
186 if ($toRender === self::DATE_RANGE_STATE_NONE) {
187 $ret .= $this->iterateAndRenderDateParts($dateParts, $data_);
189 $ret .= $this->renderDateRange($toRender, $from, $to, $delimiter);
193 if (isset($var->raw) && preg_match(
"/(\p{L}+)\s?([\-\ &,])\s?(\p{L}+)/u", $var->raw, $matches)) {
return $matches[1].$matches[2].$matches[3];
}
} elseif (!empty($this->datePartsAttribute)) {
// fallback:
// When there are no dateParts children, but date-parts attribute in date
// render numeric
$data = $this->createDateTime($var->{'date-parts'});
$ret = $this->renderNumeric($data[0]);
}
return !empty($ret) ? $this->addAffixes($this->format($this->applyTextCase($ret))) : "";
}
/**
* @param array $dates
* @return array
* @throws Exception
*/
private function createDateTime($dates)
{
$data = [];
foreach ($dates as $date) {
$date = $this->cleanDate($date);
if ($date[0] < 1000) {
$dateTime = new DateTime(0, 0, 0);
$dateTime->setDay(0)->setMonth(0)->setYear(0);
$data[] = $dateTime;
}
$dateTime = new DateTime(
$date[0],
array_key_exists(1, $date) ? $date[1] : 1,
array_key_exists(2, $date) ? $date[2] : 1
);
if (!array_key_exists(1, $date)) {
$dateTime->setMonth(0);
}
if (!array_key_exists(2, $date)) {
$dateTime->setDay(0);
}
$data[] = $dateTime;
}
return $data;
}
/**
* @param int $toRender
* @param DateTime $from
* @param DateTime $to
* @param $delimiter
* @return string
*/
private function renderDateRange($toRender, DateTime $from, DateTime $to, $delimiter)
{
$datePartRenderer = DateRangeRenderer::factory($this, $toRender);
return $datePartRenderer->parseDateRange($this->dateParts, $from, $to, $delimiter);
}
/**
* @param string $format
* @return bool
*/
private function hasDatePartsFromLocales($format)
{
$dateXml = CiteProc::getContext()->getLocale()->getDateXml();
return !empty($dateXml[$format]);
}
/**
* @param string $format
* @return array
*/
private function getDatePartsFromLocales($format)
{
$ret = [];
// date parts from locales
$dateFromLocale_ = CiteProc::getContext()->getLocale()->getDateXml();
$dateFromLocale = $dateFromLocale_[$format];
// no custom date parts within the date element (this)?
if (!empty($dateFromLocale)) {
$dateForm = array_filter(
is_array($dateFromLocale) ? $dateFromLocale : [$dateFromLocale],
function ($element) use ($format) {
/** @var SimpleXMLElement $element */
$dateForm = (string) $element->attributes()["form"];
return $dateForm === $format;
}
);
//has dateForm from locale children (date-part elements)?
$localeDate = array_pop($dateForm);
if ($localeDate instanceof SimpleXMLElement && $localeDate->count() > 0) {
foreach ($localeDate as $child) {
$ret[] = $child;
}
}
}
return $ret;
}
/**
* @return string
*/
public function getVariable()
{
return $this->variable;
}
/**
* @param $data
* @param $var
* @throws CiteProcException
*/
private function prepareDatePartsInVariable($data, $var)
{
if (!isset($data->{$this->variable}->{'date-parts'}) || empty($data->{$this->variable}->{'date-parts'})) {
if (isset($data->{$this->variable}->raw) && !empty($data->{$this->variable}->raw)) {
// try to parse date parts from "raw" attribute
$var->{'date-parts'} = Util\DateHelper::parseDateParts($data->{$this->variable});
} else {
throw new CiteProcException("No valid date format");
}
}
}
/**
* @param $dateParts
* @param string $form
* @throws InvalidStylesheetException
*/
private function prepareDatePartsChildren($dateParts, $form)
{
/* Localized date formats are selected with the optional form attribute, which must set to either “numeric”
(for fully numeric formats, e.g. “12-15-2005”), or “text” (for formats with a non-numeric month, e.g.
“December 15, 2005”). Localized date formats can be customized in two ways. First, the date-parts attribute may
be used to show fewer date parts. The possible values are:
- “year-month-day” - (default), renders the year, month and day
- “year-month” - renders the year and month
- “year” - renders the year */
if ($this->dateParts->count() < 1 && in_array($form, self::$localizedDateFormats)) {
if ($this->hasDatePartsFromLocales($form)) {
$datePartsFromLocales = $this->getDatePartsFromLocales($form);
array_filter($datePartsFromLocales, function (SimpleXMLElement $item) use ($dateParts) {
return in_array($item["name"], $dateParts);
});
foreach ($datePartsFromLocales as $datePartNode) {
$datePart = $datePartNode["name"];
$this->dateParts->set("$form-$datePart", Util\Factory::create($datePartNode));
}
} else { //otherwise create default date parts
foreach ($dateParts as $datePart) {
$this->dateParts->add(
"$form-$datePart",
new DatePart(
new SimpleXMLElement('<date-part name="'.$datePart.'" form="'.$form.'" />')
)
);
}
}
}
}
private function renderNumeric(DateTime $date)
{
return $date->renderNumeric();
}
public function getForm()
{
return $this->form;
}
private function cleanDate($date)
{
$ret = [];
foreach ($date as $key => $datePart) {
$ret[$key] = Util\NumberHelper::extractNumber(Util\StringHelper::removeBrackets($datePart));
}
return $ret;
}
/**
* @param array $dateParts
* @param array $data_
* @return string
*/
private function iterateAndRenderDateParts(array $dateParts, array $data_)
{
$ret = "";
/** @var DatePart $datePart */
foreach ($this->dateParts as $key => $datePart) {
/** @noinspection PhpUnusedLocalVariableInspection */
list($f, $p) = explode("-", $key);
if (in_array($p, $dateParts)) {
$ret .= $datePart->render($data_[0], $this);
}
}
return $ret;
}
}
&,])\s?(\p{L}+)/u", $var->raw, $matches)) {
194 return $matches[1].$matches[2].$matches[3];
196 } elseif (!empty($this->datePartsAttribute)) {
200 $data = $this->createDateTime($var->{
'date-parts'});
201 $ret = $this->renderNumeric($data[0]);
212 private function createDateTime($dates)
215 foreach ($dates as $date) {
216 $date = $this->cleanDate($date);
217 if ($date[0] < 1000) {
218 $dateTime =
new DateTime(0, 0, 0);
219 $dateTime->setDay(0)->setMonth(0)->setYear(0);
222 $dateTime =
new DateTime(
224 array_key_exists(1, $date) ? $date[1] : 1,
225 array_key_exists(2, $date) ? $date[2] : 1
227 if (!array_key_exists(1, $date)) {
228 $dateTime->setMonth(0);
230 if (!array_key_exists(2, $date)) {
231 $dateTime->setDay(0);
246 private function renderDateRange($toRender, DateTime $from, DateTime $to, $delimiter)
248 $datePartRenderer = DateRangeRenderer::factory($this, $toRender);
249 return $datePartRenderer->parseDateRange($this->dateParts, $from, $to, $delimiter);
256 private function hasDatePartsFromLocales($format)
258 $dateXml = CiteProc::getContext()->getLocale()->getDateXml();
259 return !empty($dateXml[$format]);
266 private function getDatePartsFromLocales($format)
270 $dateFromLocale_ = CiteProc::getContext()->getLocale()->getDateXml();
271 $dateFromLocale = $dateFromLocale_[$format];
274 if (!empty($dateFromLocale)) {
275 $dateForm = array_filter(
276 is_array($dateFromLocale) ? $dateFromLocale : [$dateFromLocale],
277 function ($element) use ($format) {
279 $dateForm = (string) $element->attributes()[
"form"];
280 return $dateForm === $format;
285 $localeDate = array_pop($dateForm);
287 if ($localeDate instanceof SimpleXMLElement && $localeDate->count() > 0) {
288 foreach ($localeDate as $child) {
299 public function getVariable()
301 return $this->variable;
309 private function prepareDatePartsInVariable($data, $var)
311 if (!isset($data->{$this->variable}->{
'date-parts'}) || empty($data->{$this->variable}->{
'date-parts'})) {
312 if (isset($data->{$this->variable}->raw) && !empty($data->{$this->variable}->raw)) {
314 $var->{
'date-parts'} = Util\DateHelper::parseDateParts($data->{$this->variable});
316 throw new CiteProcException(
"No valid date format");
326 private function prepareDatePartsChildren($dateParts, $form)
336 if ($this->dateParts->count() < 1 && in_array($form, self::$localizedDateFormats)) {
337 if ($this->hasDatePartsFromLocales($form)) {
338 $datePartsFromLocales = $this->getDatePartsFromLocales($form);
339 array_filter($datePartsFromLocales,
function (SimpleXMLElement $item) use ($dateParts) {
340 return in_array($item[
"name"], $dateParts);
343 foreach ($datePartsFromLocales as $datePartNode) {
344 $datePart = $datePartNode[
"name"];
345 $this->dateParts->set(
"$form-$datePart", Util\Factory::create($datePartNode));
348 foreach ($dateParts as $datePart) {
349 $this->dateParts->add(
352 new SimpleXMLElement(
'<date-part name="'.$datePart.
'" form="'.$form.
'" />')
361 private function renderNumeric(DateTime $date)
363 return $date->renderNumeric();
371 private function cleanDate($date)
374 foreach ($date as $key => $datePart) {
375 $ret[$key] = Util\NumberHelper::extractNumber(Util\StringHelper::removeBrackets($datePart));
385 private function iterateAndRenderDateParts(array $dateParts, array $data_)
389 foreach ($this->dateParts as $key => $datePart) {
391 list($f, $p) = explode(
"-", $key);
392 if (in_array($p, $dateParts)) {
393 $ret .= $datePart->render($data_[0], $this);