diff --git a/src/Rendering/Name/Name.php b/src/Rendering/Name/Name.php index da20c1f..3ecce50 100644 --- a/src/Rendering/Name/Name.php +++ b/src/Rendering/Name/Name.php @@ -147,7 +147,7 @@ public function render($data, $var, $citationNumber = null) possible when the original name list has at least two more names than the truncated name list (for this the value of et-al-use-first/et-al-subsequent-min must be at least 2 less than the value of et-al-min/et-al-subsequent-use-first). */ - if ($this->etAlUseLast) { + if ($this->etAlUseLast && $this->isEtAl($name, $resultNames)) { $this->and = "…"; // set "and" $this->etAl = null; //reset $etAl; } @@ -244,6 +244,21 @@ private function cloneNamePOSC($name) return $nameObj; } + /** + * @param $data + * @param $resultNames + * @return bool + */ + protected function isEtAl($data, $resultNames): bool + { + return count($data) > 1 + && !empty($resultNames) + && !empty($this->etAl) + && !empty($this->etAlMin) + && !empty($this->etAlUseFirst) + && count($data) != count($resultNames); + } + /** * @param $data * @param $text @@ -253,13 +268,7 @@ private function cloneNamePOSC($name) protected function appendEtAl($data, $text, $resultNames) { //append et al abbreviation - if (count($data) > 1 - && !empty($resultNames) - && !empty($this->etAl) - && !empty($this->etAlMin) - && !empty($this->etAlUseFirst) - && count($data) != count($resultNames) - ) { + if ($this->isEtAl($data, $resultNames)) { /* By default, when a name list is truncated to a single name, the name and the “et-al” (or “and others”) term are separated by a space (e.g. “Doe et al.”). When a name list is truncated to two or more names, the name delimiter is used (e.g. “Doe, Smith, et al.”). This behavior can be changed with the diff --git a/src/Style/InheritableNameAttributesTrait.php b/src/Style/InheritableNameAttributesTrait.php index d581d1a..4695030 100644 --- a/src/Style/InheritableNameAttributesTrait.php +++ b/src/Style/InheritableNameAttributesTrait.php @@ -232,18 +232,15 @@ public function initInheritableNameAttributes(SimpleXMLElement $node) { $context = CiteProc::getContext(); $parentStyleElement = null; + $root = $context->getRoot(); if ($this instanceof Name || $this instanceof Names) { if ($context->getMode() === "bibliography") { - if ($this->isDescendantOfMacro()) { - $parentStyleElement = $context->getRoot(); - } else { - $parentStyleElement = $context->getBibliography(); - } + $parentStyleElement = $context->getBibliography(); } else { $parentStyleElement = $context->getCitation(); } } elseif ($this instanceof StyleElement) { - $parentStyleElement = $context->getRoot(); + $parentStyleElement = $root; } foreach (self::$attributes as $nameAttribute) { @@ -251,86 +248,111 @@ public function initInheritableNameAttributes(SimpleXMLElement $node) switch ($nameAttribute) { case 'and': if (!empty($attribute)) { - $this->and = (string) $attribute; - } elseif (!empty($parentStyleElement)) { //inherit from parent style - $this->and = $parentStyleElement->getAnd(); + $this->setAnd((string) $attribute); + } elseif (!empty($parentStyleElement) && !empty($parentStyleElement->getAnd())) { + $this->setAnd($parentStyleElement->getAnd()); + } elseif (!empty($root) && !empty($root->getAnd())) { + $this->setAnd($root->getAnd()); } break; case 'delimiter-precedes-et-al': if (!empty($attribute)) { - $this->delimiterPrecedesEtAl = (string) $attribute; - } elseif (!empty($parentStyleElement)) { //inherit from parent style - $this->delimiterPrecedesEtAl = $parentStyleElement->getDelimiterPrecedesEtAl(); + $this->setDelimiterPrecedesEtAl((string) $attribute); + } elseif (!empty($parentStyleElement) && !empty($parentStyleElement->getDelimiterPrecedesEtAl())) { + $this->setDelimiterPrecedesEtAl($parentStyleElement->getDelimiterPrecedesEtAl()); + } elseif (!empty($root)) { + $this->setDelimiterPrecedesEtAl($root->getDelimiterPrecedesEtAl()); } break; case 'delimiter-precedes-last': if (!empty($attribute)) { - $this->delimiterPrecedesLast = (string) $attribute; - } elseif (!empty($parentStyleElement)) { //inherit from parent style - $this->delimiterPrecedesLast = $parentStyleElement->getDelimiterPrecedesLast(); + $this->setDelimiterPrecedesLast((string) $attribute); + } elseif (!empty($parentStyleElement) && !empty($parentStyleElement->getDelimiterPrecedesLast())) { + $this->setDelimiterPrecedesLast($parentStyleElement->getDelimiterPrecedesLast()); + } elseif (!empty($root)) { + $this->setDelimiterPrecedesLast($root->getDelimiterPrecedesLast()); } break; case 'et-al-min': if (!empty($attribute)) { - $this->etAlMin = intval((string) $attribute); - } elseif (!empty($parentStyleElement)) { - $this->etAlMin = $parentStyleElement->getEtAlMin(); + $this->setEtAlMin(intval((string) $attribute)); + } elseif (!empty($parentStyleElement) && !empty($parentStyleElement->getEtAlMin())) { + $this->setEtAlMin($parentStyleElement->getEtAlMin()); + } elseif (!empty($root)) { + $this->setEtAlMin($root->getEtAlMin()); } break; case 'et-al-use-first': if (!empty($attribute)) { - $this->etAlUseFirst = intval((string) $attribute); - } elseif (!empty($parentStyleElement)) { - $this->etAlUseFirst = $parentStyleElement->getEtAlUseFirst(); + $this->setEtAlUseFirst(intval((string) $attribute)); + } elseif (!empty($parentStyleElement) && !empty($parentStyleElement->getEtAlUseFirst())) { + $this->setEtAlUseFirst($parentStyleElement->getEtAlUseFirst()); + } elseif (!empty($root)) { + $this->setEtAlUseFirst($root->getEtAlUseFirst()); } break; case 'et-al-subsequent-min': if (!empty($attribute)) { - $this->etAlSubsequentMin = intval((string) $attribute); - } elseif (!empty($parentStyleElement)) { - $this->etAlSubsequentMin = $parentStyleElement->getEtAlSubsequentMin(); + $this->setEtAlSubsequentMin(intval((string) $attribute)); + } elseif (!empty($parentStyleElement) && !empty($parentStyleElement->getEtAlSubsequentMin())) { + $this->setEtAlSubsequentMin($parentStyleElement->getEtAlSubsequentMin()); + } elseif (!empty($root)) { + $this->setEtAlSubsequentMin($root->getEtAlSubsequentMin()); } break; case 'et-al-subsequent-use-first': if (!empty($attribute)) { $this->etAlSubsequentUseFirst = intval((string) $attribute); - } elseif (!empty($parentStyleElement)) { - $this->etAlSubsequentUseFirst = $parentStyleElement->getEtAlSubsequentUseFirst(); + $this->setEtAlSubsequentUseFirst(intval((string) $attribute)); + } elseif (!empty($parentStyleElement) && !empty($parentStyleElement->getEtAlSubsequentUseFirst())) { + $this->setEtAlSubsequentUseFirst($parentStyleElement->getEtAlSubsequentUseFirst()); + } elseif (!empty($root)) { + $this->setEtAlSubsequentUseFirst($root->getEtAlSubsequentUseFirst()); } break; case 'et-al-use-last': if (!empty($attribute)) { - $this->etAlUseLast = ((string) $attribute) === "true"; - } elseif (!empty($parentStyleElement)) { - $this->etAlUseLast = $parentStyleElement->getEtAlUseLast(); + $this->setEtAlUseLast(((string) $attribute) === "true"); + } elseif (!empty($parentStyleElement) && !empty($parentStyleElement->getEtAlUseLast())) { + $this->setEtAlUseLast($parentStyleElement->getEtAlUseLast()); + } elseif (!empty($root)) { + $this->setEtAlUseLast($root->getEtAlUseLast()); } break; case 'initialize': if (!empty($attribute)) { - $this->initialize = ((string) $attribute) === "true"; - } elseif (!empty($parentStyleElement)) { - $this->initialize = $parentStyleElement->getInitialize(); + $this->setInitialize(((string) $attribute) === "true"); + } elseif (!empty($parentStyleElement) && !empty($parentStyleElement->getInitialize())) { + $this->setInitialize($parentStyleElement->getInitialize()); + } elseif (!empty($root)) { + $this->setInitialize($root->getInitialize()); } break; case 'initialize-with': if (!empty($attribute)) { - $this->initializeWith = (string) $attribute; - } elseif (!empty($parentStyleElement)) { - $this->initializeWith = $parentStyleElement->getInitializeWith(); + $this->setInitializeWith((string) $attribute); + } elseif (!empty($parentStyleElement) && !empty($parentStyleElement->getInitializeWith())) { + $this->setInitializeWith($parentStyleElement->getInitializeWith()); + } elseif (!empty($root)) { + $this->setInitializeWith($root->getInitializeWith()); } break; case 'name-as-sort-order': if (!empty($attribute)) { - $this->nameAsSortOrder = (string) $attribute; - } elseif (!empty($parentStyleElement)) { - $this->nameAsSortOrder = $parentStyleElement->getNameAsSortOrder(); + $this->setNameAsSortOrder((string) $attribute); + } elseif (!empty($parentStyleElement) && !empty($parentStyleElement->getNameAsSortOrder())) { + $this->setNameAsSortOrder($parentStyleElement->getNameAsSortOrder()); + } elseif (!empty($root)) { + $this->setNameAsSortOrder($root->getNameAsSortOrder()); } break; case 'sort-separator': if (!empty($attribute)) { - $this->sortSeparator = (string) $attribute; - } elseif (!empty($parentStyleElement)) { - $this->sortSeparator = $parentStyleElement->getSortSeparator(); + $this->setSortSeparator((string) $attribute); + } elseif (!empty($parentStyleElement) && !empty($parentStyleElement->getSortSeparator())) { + $this->setSortSeparator($parentStyleElement->getSortSeparator()); + } elseif (!empty($root)) { + $this->setSortSeparator($root->getSortSeparator()); } break; case 'name-form': @@ -346,8 +368,10 @@ public function initInheritableNameAttributes(SimpleXMLElement $node) if ($this instanceof Name) { if (!empty($attribute)) { $this->setForm((string) $attribute); - } elseif (!empty($parentStyleElement)) { + } elseif (!empty($parentStyleElement) && !empty($parentStyleElement->getNameForm())) { $this->setForm($parentStyleElement->getNameForm()); + } elseif (!empty($root)) { + $this->setForm($root->getNameForm()); } } break; @@ -363,8 +387,10 @@ public function initInheritableNameAttributes(SimpleXMLElement $node) on cs:name. Similarly, names-delimiter corresponds to the delimiter attribute on cs:names. */ if (!empty($attribute)) { $this->nameDelimiter = $this->delimiter = (string) $attribute; - } elseif (!empty($parentStyleElement)) { + } elseif (!empty($parentStyleElement) && !empty($parentStyleElement->getNameDelimiter())) { $this->nameDelimiter = $this->delimiter = $parentStyleElement->getNameDelimiter(); + } elseif (!empty($root)) { + $this->nameDelimiter = $this->delimiter = $root->getNameDelimiter(); } } break; @@ -374,6 +400,8 @@ public function initInheritableNameAttributes(SimpleXMLElement $node) $this->setDelimiter((string) $attribute); } elseif (!empty($parentStyleElement)) { $this->setDelimiter($parentStyleElement->getNameDelimiter()); + } elseif (!empty($root)) { + $this->setDelimiter($root->getNameDelimiter()); } } } diff --git a/src/Util/StringHelper.php b/src/Util/StringHelper.php index 86685aa..843b233 100644 --- a/src/Util/StringHelper.php +++ b/src/Util/StringHelper.php @@ -142,12 +142,18 @@ public static function keepLowerCase($word) public static function mb_ucfirst($string, $encoding = 'UTF-8') {// phpcs:enable $strlen = mb_strlen($string, $encoding); + if ($strlen == 0) return ''; $firstChar = mb_substr($string, 0, 1, $encoding); $then = mb_substr($string, 1, $strlen - 1, $encoding); /** @noinspection PhpInternalEntityUsedInspection */ + // We can not rely on mb_detect_encoding. See https://www.php.net/manual/en/function.mb-detect-encoding.php. + // We need to double-check if the first char is not a multibyte char otherwise mb_strtoupper() process it + // incorrectly, and it causes issues later. For example 'こ' transforms to 'Á�'. + $original_ord = mb_ord($firstChar, $encoding); $encoding = mb_detect_encoding($firstChar, self::ISO_ENCODINGS, true); - return in_array($encoding, self::ISO_ENCODINGS) ? + $new_ord = mb_ord($firstChar, $encoding); + return $original_ord === $new_ord && in_array($encoding, self::ISO_ENCODINGS) ? mb_strtoupper($firstChar, $encoding).$then : $firstChar.$then; } // phpcs:disable diff --git a/tests/fixtures/basic-tests/processor-tests/humans/bugfix-github-143.txt b/tests/fixtures/basic-tests/processor-tests/humans/bugfix-github-143.txt new file mode 100644 index 0000000..4535eb8 --- /dev/null +++ b/tests/fixtures/basic-tests/processor-tests/humans/bugfix-github-143.txt @@ -0,0 +1,1257 @@ +>>===== MODE =====>> +bibliography +<<===== MODE =====<< + +>>===== RESULT =====>> +
+
Anderson, John. こんにちは世界.
+
+<<===== RESULT =====<< + +>>===== CSL =====>> + + +<<===== CSL =====<< + +>>===== INPUT =====>> +[ + { + "author": [ + { + "family": "Anderson", + "given": "John" + } + ], + "title": "こんにちは世界", + "id": "ITEM-2", + "type": "book" + } +] +<<===== INPUT =====<< diff --git a/tests/fixtures/basic-tests/processor-tests/humans/bugfix-github-145.txt b/tests/fixtures/basic-tests/processor-tests/humans/bugfix-github-145.txt new file mode 100644 index 0000000..5a9cc31 --- /dev/null +++ b/tests/fixtures/basic-tests/processor-tests/humans/bugfix-github-145.txt @@ -0,0 +1,1301 @@ +>>===== MODE =====>> +bibliography +<<===== MODE =====<< + +>>===== RESULT =====>> +
+
First, James, Jay Second, John Third, Jack Fourth, Jacob Fifth, Joseph Sixth, Julian Seventh, et al. Test.
+
+<<===== RESULT =====<< + +>>===== CSL =====>> + + +<<===== CSL =====<< + +>>===== INPUT =====>> +[ + { + "author": [ + { + "family": "First", + "given": "James" + }, + { + "family": "Second", + "given": "Jay" + }, + { + "family": "Third", + "given": "John" + }, + { + "family": "Fourth", + "given": "Jack" + }, + { + "family": "Fifth", + "given": "Jacob" + }, + { + "family": "Sixth", + "given": "Joseph" + }, + { + "family": "Seventh", + "given": "Julian" + }, + { + "family": "Eighth", + "given": "Jayden" + }, + { + "family": "Ninth", + "given": "Jackson" + }, + { + "family": "Tenth", + "given": "Jasper" + }, + { + "family": "Eleventh", + "given": "Jonathan" + }, + { + "family": "Twelfth", + "given": "Jeremiah" + } + ], + "id": "ITEM-1", + "type": "book", + "title": "Test" + } +] +<<===== INPUT =====<< diff --git a/tests/src/BugfixTest.php b/tests/src/BugfixTest.php index 2c52799..37935f4 100644 --- a/tests/src/BugfixTest.php +++ b/tests/src/BugfixTest.php @@ -163,4 +163,14 @@ public function testBugfixGithub114() { $this->runTestSuite('bugfix-github-114'); } + + public function testBugfixGithub143() + { + $this->runTestSuite('bugfix-github-143'); + } + + public function testBugfixGithub145() + { + $this->runTestSuite('bugfix-github-145'); + } }