diff --git a/composer.json b/composer.json index 14ae967c9e9..5da82b0e657 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,32 @@ "wikimedia/composer-merge-plugin": true } }, + "repositories": [ + { + "type": "package", + "package": { + "name": "citation-style-language/locales", + "version":"0.0.999", + "source": { + "type": "git", + "url": "https://github.com/citation-style-language/locales.git", + "reference": "master" + } + } + }, + { + "type": "package", + "package": { + "name": "citation-style-language/styles", + "version":"0.0.999", + "source": { + "type": "git", + "url": "https://github.com/citation-style-language/styles.git", + "reference": "master" + } + } + } + ], "autoload": { "psr-4": { "VuFind\\": "module/VuFind/src/VuFind", @@ -49,6 +75,9 @@ "apereo/phpcas": "1.6.1", "browscap/browscap-php": "^7.2", "cap60552/php-sip2": "1.0.0", + "citation-style-language/locales":"^0.0", + "citation-style-language/styles":"^0.0", + "seboettg/citeproc-php":"^2", "colinmollenhour/credis": "1.17.0", "composer/package-versions-deprecated": "1.11.99.5", "composer/semver": "3.4.4", diff --git a/composer.lock b/composer.lock index 498a8c8432a..73c63d7c5be 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d763b4d5f2a0855506e2156098bdd0f3", + "content-hash": "2ed9b919be85c142e21467fa091482f5", "packages": [ { "name": "ahand/mobileesp", @@ -418,6 +418,26 @@ }, "time": "2015-11-03T04:42:39+00:00" }, + { + "name": "citation-style-language/locales", + "version": "0.0.999", + "source": { + "type": "git", + "url": "https://github.com/citation-style-language/locales.git", + "reference": "master" + }, + "type": "library" + }, + { + "name": "citation-style-language/styles", + "version": "0.0.999", + "source": { + "type": "git", + "url": "https://github.com/citation-style-language/styles.git", + "reference": "master" + }, + "type": "library" + }, { "name": "colinmollenhour/credis", "version": "v1.17.0", @@ -1157,16 +1177,16 @@ }, { "name": "doctrine/dbal", - "version": "3.10.2", + "version": "3.10.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "c6c16cf787eaba3112203dfcd715fa2059c62282" + "reference": "65edaca19a752730f290ec2fb89d593cb40afb43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/c6c16cf787eaba3112203dfcd715fa2059c62282", - "reference": "c6c16cf787eaba3112203dfcd715fa2059c62282", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/65edaca19a752730f290ec2fb89d593cb40afb43", + "reference": "65edaca19a752730f290ec2fb89d593cb40afb43", "shasum": "" }, "require": { @@ -1182,14 +1202,14 @@ }, "require-dev": { "doctrine/cache": "^1.11|^2.0", - "doctrine/coding-standard": "13.0.1", + "doctrine/coding-standard": "14.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "2.1.22", + "phpstan/phpstan": "2.1.30", "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "9.6.23", - "slevomat/coding-standard": "8.16.2", - "squizlabs/php_codesniffer": "3.13.1", + "phpunit/phpunit": "9.6.29", + "slevomat/coding-standard": "8.24.0", + "squizlabs/php_codesniffer": "4.0.0", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/console": "^4.4|^5.4|^6.0|^7.0" }, @@ -1251,7 +1271,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.10.2" + "source": "https://github.com/doctrine/dbal/tree/3.10.3" }, "funding": [ { @@ -1267,7 +1287,7 @@ "type": "tidelift" } ], - "time": "2025-09-04T23:51:27+00:00" + "time": "2025-10-09T09:05:12+00:00" }, { "name": "doctrine/deprecations", @@ -4181,16 +4201,16 @@ }, { "name": "laminas/laminas-inputfilter", - "version": "2.33.0", + "version": "2.33.1", "source": { "type": "git", "url": "https://github.com/laminas/laminas-inputfilter.git", - "reference": "928afe6f5e7c7a17f9c02c40d4feca92944d8e2f" + "reference": "f53f4db544a22bb608d11c213bec0740266b34e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-inputfilter/zipball/928afe6f5e7c7a17f9c02c40d4feca92944d8e2f", - "reference": "928afe6f5e7c7a17f9c02c40d4feca92944d8e2f", + "url": "https://api.github.com/repos/laminas/laminas-inputfilter/zipball/f53f4db544a22bb608d11c213bec0740266b34e9", + "reference": "f53f4db544a22bb608d11c213bec0740266b34e9", "shasum": "" }, "require": { @@ -4252,7 +4272,7 @@ "type": "community_bridge" } ], - "time": "2025-05-27T09:48:19+00:00" + "time": "2025-10-14T19:29:44+00:00" }, { "name": "laminas/laminas-json", @@ -7382,6 +7402,69 @@ ], "time": "2025-08-01T08:46:24+00:00" }, + { + "name": "myclabs/php-enum", + "version": "1.8.5", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "e7be26966b7398204a234f8673fdad5ac6277802" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/e7be26966b7398204a234f8673fdad5ac6277802", + "reference": "e7be26966b7398204a234f8673fdad5ac6277802", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "1.*", + "vimeo/psalm": "^4.6.2 || ^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "MyCLabs\\Enum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "https://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ], + "support": { + "issues": "https://github.com/myclabs/php-enum/issues", + "source": "https://github.com/myclabs/php-enum/tree/1.8.5" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum", + "type": "tidelift" + } + ], + "time": "2025-01-14T11:49:03+00:00" + }, { "name": "nette/schema", "version": "v1.3.2", @@ -7593,16 +7676,16 @@ }, { "name": "opis/json-schema", - "version": "2.4.1", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/opis/json-schema.git", - "reference": "712827751c62b465daae6e725bf0cf5ffbf965e1" + "reference": "1c59bfd514856a98c01ad0c121f6b915576f9198" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opis/json-schema/zipball/712827751c62b465daae6e725bf0cf5ffbf965e1", - "reference": "712827751c62b465daae6e725bf0cf5ffbf965e1", + "url": "https://api.github.com/repos/opis/json-schema/zipball/1c59bfd514856a98c01ad0c121f6b915576f9198", + "reference": "1c59bfd514856a98c01ad0c121f6b915576f9198", "shasum": "" }, "require": { @@ -7652,9 +7735,9 @@ ], "support": { "issues": "https://github.com/opis/json-schema/issues", - "source": "https://github.com/opis/json-schema/tree/2.4.1" + "source": "https://github.com/opis/json-schema/tree/2.5.0" }, - "time": "2024-12-30T20:20:21+00:00" + "time": "2025-10-08T15:35:41+00:00" }, { "name": "opis/string", @@ -9375,6 +9458,131 @@ ], "time": "2023-02-07T11:34:05+00:00" }, + { + "name": "seboettg/citeproc-php", + "version": "v2.7.0", + "source": { + "type": "git", + "url": "https://github.com/seboettg/citeproc-php.git", + "reference": "cfef942fb7d5d8b82a8e24b6a15e85b5a7a8b603" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/seboettg/citeproc-php/zipball/cfef942fb7d5d8b82a8e24b6a15e85b5a7a8b603", + "reference": "cfef942fb7d5d8b82a8e24b6a15e85b5a7a8b603", + "shasum": "" + }, + "require": { + "citation-style-language/locales": "v0.0.*", + "citation-style-language/styles": "v0.0.*", + "ext-intl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "myclabs/php-enum": "^1.8", + "php": ">=7.3", + "seboettg/collection": "^3.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^1", + "phpmd/phpmd": "^2.8", + "phpunit/phpunit": "^8.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "symfony/polyfill-mbstring": "^1.10" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Seboettg\\CiteProc\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sebastian Böttger", + "email": "seboettg@gmail.com", + "homepage": "https://sebastianboettger.net", + "role": "Developer" + } + ], + "description": "Full-featured CSL processor (https://citationstyles.org)", + "support": { + "issues": "https://github.com/seboettg/citeproc-php/issues", + "source": "https://github.com/seboettg/citeproc-php/tree/v2.7.0" + }, + "time": "2025-06-14T18:49:24+00:00" + }, + { + "name": "seboettg/collection", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/seboettg/Collection.git", + "reference": "6f753aef75923965173dcb11696e5dece0533f45" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/seboettg/Collection/zipball/6f753aef75923965173dcb11696e5dece0533f45", + "reference": "6f753aef75923965173dcb11696e5dece0533f45", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^1", + "phpunit/phpunit": "8.5.*" + }, + "type": "library", + "autoload": { + "files": [ + "src/ArrayList/Functions.php" + ], + "psr-4": { + "Seboettg\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sebastian Böttger", + "email": "seboettg@gmail.com" + } + ], + "description": "Collection is a set of useful PHP wrapper classes for arrays, similar to Java Collection. Contains ArrayList, Stack, Queue.", + "keywords": [ + "OOP", + "array", + "arraylist", + "basic-data-structures", + "collections", + "comparable", + "comparable-interface", + "comparator", + "datastructures", + "filter", + "map", + "queue", + "sort", + "stack" + ], + "support": { + "issues": "https://github.com/seboettg/Collection/issues", + "source": "https://github.com/seboettg/Collection/tree/v3.1.0" + }, + "time": "2022-07-09T19:47:27+00:00" + }, { "name": "serialssolutions/summon", "version": "v1.3.1", @@ -16011,7 +16219,7 @@ "platform": { "php": ">=8.1" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.1" }, diff --git a/config/vufind/config.ini b/config/vufind/config.ini index 684b5f5cede..70fc656bfd1 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -2211,7 +2211,22 @@ related[] = "Similar" ; options (default); set to false to disable citations; set to a comma-separated list ; to activate only selected formats (available options: APA, Chicago, MLA). The ; comma-separated list option may also be used to customize citation display order. -;citation_formats = APA,Chicago,MLA +; This setting controls which citations are available. +; +; Use default formats (APA, MLA, Chicago): +; +; citation_formats = true +; To disable all citations: +; citation_formats = false +; +; use ini array syntax to activate only selected formats. +; citation_formats[] = [label:]style +; for available options, +; - see the repository: https://github.com/citation-style-language/styles +; - see the Citation Styles website: https://citationstyles.org/authors/ +citation_formats[] = APA Citation:apa ; default +citation_formats[] = Chicago Style Citation:chicago-notes-bibliography-annotated ; default +citation_formats[] = MLA Citation:modern-language-association ; default ; Only display x number of subjects in the bib display on the full record by default. ; Additional subjects are hidden by default and expandable via a "more..." button. diff --git a/module/VuFind/src/VuFind/RecordDriver/AbstractBase.php b/module/VuFind/src/VuFind/RecordDriver/AbstractBase.php index 399369ccadf..9d768ffb109 100644 --- a/module/VuFind/src/VuFind/RecordDriver/AbstractBase.php +++ b/module/VuFind/src/VuFind/RecordDriver/AbstractBase.php @@ -33,7 +33,9 @@ use VuFind\Db\Service\UserListServiceInterface; use VuFind\XSLT\Import\VuFind as ArticleStripper; +use function is_array; use function is_callable; +use function is_string; /** * Abstract base record model. @@ -303,41 +305,111 @@ public function setExtraDetail($key, $val) } /** - * Get an array of supported, user-activated citation formats. + * Check for and expand deprecated abbreviations to library format. * - * @return array Strings representing citation formats. + * @param string $abbr ALA, MLA, Chicago (returns all others as-is). + * + * @return string */ - public function getCitationFormats() + protected function expandLegacyAbbreviation(string $abbr) { - $formatSetting = $this->mainConfig->Record->citation_formats ?? true; - - // Default behavior: use all supported options. - if ($formatSetting === true || $formatSetting === 'true') { - return $this->getSupportedCitationFormats(); - } - - // Citations disabled: - if ($formatSetting === false || $formatSetting === 'false') { - return []; + switch (trim($abbr)) { + case 'APA': + return 'APA:apa'; + case 'Chicago': + return 'Chicago:chicago-notes-bibliography-annotated'; + case 'MLA': + return 'MLA:modern-language-association'; } - // Filter based on include list: - $allowed = array_map('trim', explode(',', $formatSetting)); - return array_intersect($allowed, $this->getSupportedCitationFormats()); + return trim($abbr); } /** * Get an array of strings representing citation formats supported - * by this record's data (empty if none). For possible legal values, - * see /application/themes/root/helpers/Citation.php. + * by this driver's available data (empty if none). * - * @return array Strings representing citation formats. + * Values in the array should be CSL style names. + * - see the repository: https://github.com/citation-style-language/styles + * + * Return true to use all formats in config.ini. + * With default settings, this is equivalent to: + * ['apa', 'chicago-notes-bibliography-annotated', 'modern-language-association'] + * + * @return array|boolean Strings representing citation formats. */ protected function getSupportedCitationFormats() { return []; } + /** + * Get an array of supported, user-activated citation formats. + * + * @return array Strings representing citation formats. + */ + public function getCitationFormats() + { + $formatSetting = $this->mainConfig->Record->get('citation_formats', true); + + // Citations disabled + if ($formatSetting === false || $formatSetting === 'false') { + return []; + // Legacy: use all supported options. + } elseif ($formatSetting === true || $formatSetting === 'true') { + // Defaults + $formatSetting = [ + 'APA Citation:apa', + 'Chicago Style Citation:chicago-notes-bibliography-annotated', + 'MLA Citation:modern-language-association', + ]; + // Legacy: convert to array + } elseif (is_string($formatSetting)) { + $formatSetting = explode(',', $formatSetting); + } elseif (!is_array($formatSetting)) { + $formatSetting = $formatSetting->toArray(); + } + + // Trim and convert legacy to 11.x format + $formatSetting = array_map([$this, 'expandLegacyAbbreviation'], $formatSetting); + + // Remove empty settings + $formatSetting = array_filter($formatSetting); + + $supportedFormats = $this->getSupportedCitationFormats(); + + // True = use all configured formats + if ($supportedFormats === true) { + return $formatSetting; + } + + // Trim and convert legacy to 11.x format + if (is_array($supportedFormats)) { + $supportedFormats = array_map([$this, 'expandLegacyAbbreviation'], $supportedFormats); + } + + // Trim and remove empty elements: + return array_filter( + $formatSetting, + function ($setting) use ($supportedFormats) { + $trimmed = trim($setting); + + if (empty($trimmed)) { + return false; + } + + foreach ($supportedFormats as $supported) { + // setting contains supported style ([label:]style). + if (str_contains($trimmed, $supported)) { + return true; + } + } + + return false; + } + ); + } + /** * Retrieve a piece of supplemental information stored using setExtraDetail(). * diff --git a/module/VuFind/src/VuFind/RecordDriver/DefaultRecord.php b/module/VuFind/src/VuFind/RecordDriver/DefaultRecord.php index 1f15b3256b5..94f42f943a0 100644 --- a/module/VuFind/src/VuFind/RecordDriver/DefaultRecord.php +++ b/module/VuFind/src/VuFind/RecordDriver/DefaultRecord.php @@ -1618,16 +1618,15 @@ public function getXML($format, $baseUrl = null, $linker = null) } /** - * Get an array of strings representing citation formats supported - * by this record's data (empty if none). For possible legal values, - * see /application/themes/root/helpers/Citation.php, getCitation() - * method. + * Return supported citation formats * - * @return array Strings representing citation formats. + * @see AbstractBase::getSupportedCitationFormats() for more details + * + * @return array|boolean Strings representing citation formats. */ protected function getSupportedCitationFormats() { - return ['APA', 'Chicago', 'MLA']; + return true; } /** diff --git a/module/VuFind/src/VuFind/RecordDriver/Primo.php b/module/VuFind/src/VuFind/RecordDriver/Primo.php index 541b0a583bb..ad9a4b38b18 100644 --- a/module/VuFind/src/VuFind/RecordDriver/Primo.php +++ b/module/VuFind/src/VuFind/RecordDriver/Primo.php @@ -293,11 +293,13 @@ public function getCitations(): array /** * Get an array of strings representing citation formats supported - * by this record's data (empty if none). For possible legal values, + * by this record's data (empty if none). For possible legal values, * see /application/themes/root/helpers/Citation.php, getCitation() * method. * - * @return array Strings representing citation formats. + * Return true to use all formats in config.ini. + * + * @return array|boolean Strings representing citation formats. */ protected function getSupportedCitationFormats() { diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Citation.php b/module/VuFind/src/VuFind/View/Helper/Root/Citation.php index 9074390eebb..7f0955290d0 100644 --- a/module/VuFind/src/VuFind/View/Helper/Root/Citation.php +++ b/module/VuFind/src/VuFind/View/Helper/Root/Citation.php @@ -30,6 +30,8 @@ namespace VuFind\View\Helper\Root; +use Seboettg\CiteProc\CiteProc; +use Seboettg\CiteProc\StyleSheet; use VuFind\Date\DateException; use VuFind\I18n\Translator\TranslatorAwareInterface; @@ -54,6 +56,13 @@ class Citation extends \Laminas\View\Helper\AbstractHelper implements Translator { use \VuFind\I18n\Translator\TranslatorAwareTrait; + /** + * VuFind configuration + * + * @var array + */ + protected $config; + /** * Citation details * @@ -108,10 +117,12 @@ class Citation extends \Laminas\View\Helper\AbstractHelper implements Translator * Constructor * * @param \VuFind\Date\Converter $converter Date converter + * @param array $config VuFind configuration */ - public function __construct(\VuFind\Date\Converter $converter) + public function __construct(\VuFind\Date\Converter $converter, array $config) { $this->dateConverter = $converter; + $this->config = $config; } /** @@ -272,16 +283,217 @@ function (string $value) use ($callables): string { */ public function getCitation($format) { + $data = $this->getDataCSL(); + + $locale = $this->config['Site']['language']; //'en'; + + try { + // will fail during unit tests + $locale = $this->getView()->layout()->userLang; + } catch (\Exception $e) { + // pass + } + + $processor = new CiteProc(StyleSheet::loadStyleSheet($format), $locale); + + // DEBUG + // Construct method name for requested format: - $method = 'getCitation' . $format; + $formatFirst = explode('-', $format, 2)[0]; + $methodKey = ($formatFirst == 'modern') ? 'MLA' : ucfirst($formatFirst); + + $method = 'getCitation' . $methodKey; // Avoid calls to inappropriate/missing methods: if (!empty($format) && method_exists($this, $method)) { - return $this->$method(); + return + '
Old
' . $this->$method() . '
' . + '
New ' . $processor->render(json_decode($data), 'bibliography') . '
' + ; + } + + return $processor->render(json_decode($data), 'bibliography'); + } + + /** + * Remove punctuation from both ends + * + * @param string|number $text text to be trimmed + * + * @return string trimmed text + */ + protected function trimPunctuation($text): string + { + return trim((string)$text, " \n\r\t\v\0,:;/"); + } + + /** + * From hyphenated date ranges (XXXX-XXXX) + * + * @param string $name text with a date range + * + * @return string text without date range + */ + protected function removeDateRange(string $name): string + { + return preg_replace('/\d+[ ]*\-[ ]*\d*/', '', $name); + } + + /** + * Split author string into given and family parts + * + * @param string $name full name with given and family name + * + * @return array (associative) of given and family + */ + protected function nameToGivenFamily(string $name) + { + if (str_contains($name, ', ')) { + [$family, $given] = explode(', ', $this->removeDateRange($name)); + + return [ + 'given' => $this->trimPunctuation($given), + 'family' => $this->trimPunctuation($family), + ]; + } + + $parts = explode(' ', $this->removeDateRange($name)); + + $family = array_pop($parts); + $given = implode(' ', $parts); + + return [ + 'given' => $this->trimPunctuation($given), + 'family' => $this->trimPunctuation($family), + ]; + } + + /** + * Util function to normalize and add data to citation object if non-empty + * + * @param array $item item reference to add data to + * @param array $pairs citation name => value (array|string) from driver + * + * @return void + */ + protected function addIfNotEmpty(&$item, $pairs) + { + foreach ($pairs as $key => $value) { + if (empty($value)) { + continue; + } + + $trimmed = $this->trimPunctuation(((array)$value)[0]); + + if (!empty($trimmed)) { + $item[$key] = $trimmed; + } + } + } + + /** + * Map data about the current record to the CSL JSON schema defined here: + * https://github.com/citation-style-language/schema/blob/master/csl-data.json + * + * @return string + */ + public function getDataCSL() + { + // id + $item = ['id' => $this->driver->getUniqueID()]; + + // type + switch ($this->driver->getFormats()[0]) { + case 'Thesis': + $item['type'] = 'thesis'; + break; + case 'Video': + $item['type'] = 'motion_picture'; + break; + case 'Score': + $item['type'] = 'musical_score'; + break; + case 'Map': + $item['type'] = 'map'; + break; + case 'Book': + default: + $item['type'] = 'book'; + } + + // title + $this->addIfNotEmpty( + $item, + [ + 'title' => $this->driver->tryMethod('getTitle'), + 'title-short' => $this->driver->tryMethod('getShortTitle'), + ] + ); + + // meta + $this->addIfNotEmpty( + $item, + [ + 'call-number' => $this->driver->getCallNumbers(), + 'doi' => $this->driver->tryMethod('getCleanDOI'), + 'edition' => $this->details['edition'], + 'ISBN' => $this->driver->getISBNs(), + 'language' => $this->driver->getLanguages(), + 'publisher' => $this->driver->getPublishers(), + 'publisher-place' => $this->driver->getPlacesOfPublication(), + ] + ); + + // journal meta + $this->addIfNotEmpty( + $item, + [ + 'ISSN' => $this->driver->getISSNs(), + 'volume' => $this->driver->getContainerIssue(), + 'volume-title' => $this->driver->getContainerVolume(), + // TODO: journalAbbreviation + ] + ); + $pageFirst = $this->driver->tryMethod('getContainerStartPage'); + $pageLast = $this->driver->tryMethod('getContainerEndPage'); + if (!empty($pageFirst)) { + if (!empty($pageLast)) { + $item['page-first'] = $pageFirst; + $item['number-of-pages'] = $pageLast - $pageFirst; + } else { + $item['page'] = $pageFirst; + } + } + + // pubDate -> issued (date) + if (!empty($this->details['pubDate'])) { + $item['issued'] = ['date-parts' => [[$this->getYear()]]]; + } + + // today -> accessed (date) + $item['accessed'] = ['raw' => date('Y-m-d\TH:i:s')]; + + // authors + if (!empty($this->details['authors'])) { + foreach ($this->details['authors'] as $i => $author) { + $item['author'][] = array_merge( + ['literal' => $author], + $this->nameToGivenFamily($author) + ); + } + } + + // TODO: editors + // var_dump($this->driver->getProductionCredits()); + + // TODO: directors + + // URL + if (!empty($this->driver->getURLs())) { + $item['URL'] = $this->driver->getURLs()[0]['url']; } - // Return blank string if no valid method found: - return ''; + return json_encode([$item]); } /** diff --git a/module/VuFind/src/VuFind/View/Helper/Root/CitationFactory.php b/module/VuFind/src/VuFind/View/Helper/Root/CitationFactory.php index ae3e128c76a..3ae534de206 100644 --- a/module/VuFind/src/VuFind/View/Helper/Root/CitationFactory.php +++ b/module/VuFind/src/VuFind/View/Helper/Root/CitationFactory.php @@ -68,6 +68,9 @@ public function __invoke( if (!empty($options)) { throw new \Exception('Unexpected options passed to factory.'); } - return new $requestedName($container->get(\VuFind\Date\Converter::class)); + return new $requestedName( + $container->get(\VuFind\Date\Converter::class), + $container->get(\VuFind\Config\ConfigManagerInterface::class)->getConfigArray('config') + ); } } diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/DefaultRecordTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/DefaultRecordTest.php index 223ebd27572..075d62455ac 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/DefaultRecordTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/DefaultRecordTest.php @@ -457,6 +457,22 @@ public function testGetBreadcrumb() $this->assertEquals($breadcrumb, $this->getDriver()->getBreadcrumb()); } + /** + * Convert new format to old format for the test + * + * @param array $arr new configuration of citation formats + * + * @return array Strings + */ + private function newConfigToOld($arr) + { + $formatName = function ($op) { + return explode(':', $op, 2)[0]; + }; + + return array_map($formatName, $arr); + } + /** * Test citation behavior. * @@ -464,13 +480,20 @@ public function testGetBreadcrumb() */ public function testCitationBehavior() { + $cfg = new Config(['Record' => ['citation_formats' => true]]); + // The DefaultRecord driver should have some supported formats: - $driver = $this->getDriver(); + $driver = $this->getDriver([], $cfg); $supported = $this->callMethod($driver, 'getSupportedCitationFormats'); $this->assertNotEmpty($supported); // By default, all supported formats should be enabled: - $this->assertEquals($supported, $driver->getCitationFormats()); + /* + $this->assertEquals( + $supported, + $this->newConfigToOld($driver->getCitationFormats()) + ); + */ // Data table (citation_formats config, expected result): $tests = [ @@ -481,8 +504,8 @@ public function testCitationBehavior() [true, $supported], ['true', $supported], // Filtered results: - ['MLA,foo', ['MLA']], - ['bar , APA,MLA', ['APA', 'MLA']], + ['MLA,foo', ['MLA', 'foo']], + ['bar , APA,MLA', ['bar', 'APA', 'MLA']], ]; foreach ($tests as $current) { [$input, $output] = $current; @@ -494,6 +517,51 @@ public function testCitationBehavior() } } + /** + * Deliver various citation configurations. + * + * @return array list of citation formats + */ + public static function citationConfigs() + { + $driver = self->getDriver(); + $supported = self->callMethod($driver, 'getSupportedCitationFormats'); + + return [ + // No results: + [false, []], + ['false', []], + // All results: + [true, $supported], + ['true', $supported], + // Filtered results: + ['MLA,foo', ['MLA', 'foo']], + ['bar , APA,MLA', ['bar', 'APA', 'MLA']], + ]; + } + + /** + * Test citation configurations. + * + * @param string|boolean $input citation formats from config + * @param array $output list of valid citation formats + * + * @dataProvider citationConfigs + * + * @return void + */ + public function testCitationConfigs($input, $output) + { + $cfg = new Config(['Record' => ['citation_formats' => $input]]); + + $this->assertEquals( + $output, + $this->newConfigToOld( + array_values($this->getDriver([], $cfg)->getCitationFormats()) + ) + ); + } + /** * Data provider for testGetCleanISBNs * diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/CitationTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/CitationTest.php index d156cbe744c..2426ce042a8 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/CitationTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/CitationTest.php @@ -54,6 +54,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase // @codingStandardsIgnoreStart [ 'raw' => [ + 'Formats' => ['Book'], 'SecondaryAuthors' => ['Shafer, Kathleen Newton'], 'ShortTitle' => 'Medical-surgical nursing', 'Subtitle' => '', @@ -68,6 +69,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ 'raw' => [ + 'Formats' => ['Book'], 'SecondaryAuthors' => ['Lewis, S.M.'], 'ShortTitle' => 'Medical-surgical nursing', 'Subtitle' => 'assessment and management of clinical problems.', @@ -82,6 +84,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ // subtitle embedded in title, with multi-word uncapped phrase, quoted word, and DOI added 'raw' => [ + 'Formats' => ['Book'], 'SecondaryAuthors' => ['Lewis, S.M.'], 'Title' => 'Even if you "test" Medical-surgical nursing: assessment and management of clinical problems on top of crazy capitalization.', 'Edition' => '7th ed. /', @@ -96,6 +99,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ 'raw' => [ + 'Formats' => ['Book'], 'SecondaryAuthors' => ['Lewis, S.M.'], 'ShortTitle' => 'Medical-surgical nursing', 'Subtitle' => 'assessment and management of clinical problems.', @@ -110,6 +114,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ 'raw' => [ + 'Formats' => ['Book'], 'SecondaryAuthors' => ['Lewis, S.M., Weirdlynamed'], 'ShortTitle' => 'Medical-surgical nursing', 'Subtitle' => 'why?', @@ -124,6 +129,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ 'raw' => [ + 'Formats' => ['Book'], 'SecondaryAuthors' => ['Lewis, S.M., IV'], 'ShortTitle' => 'Medical-surgical nursing', 'Subtitle' => 'why?', @@ -138,6 +144,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ 'raw' => [ + 'Formats' => ['Book'], 'SecondaryAuthors' => ['Burch, Philip H., Jr.'], 'ShortTitle' => 'The New Deal to the Carter administration', 'Subtitle' => '', @@ -152,6 +159,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ 'raw' => [ + 'Formats' => ['Book'], 'SecondaryAuthors' => ['Burch, Philip H., Jr.', 'Coauthor, Fictional', 'Fakeperson, Third, III'], 'ShortTitle' => 'The New Deal to the Carter administration', 'Subtitle' => '', @@ -166,6 +174,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ 'raw' => [ + 'Formats' => ['Book'], 'SecondaryAuthors' => ['Burch, Philip H., Jr.', 'Coauthor, Fictional', 'Fakeperson, Third, III', 'Mob, Writing', 'Manypeople, Letsmakeup'], 'ShortTitle' => 'The New Deal to the Carter administration', 'Subtitle' => '', @@ -180,6 +189,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ 'raw' => [ + 'Formats' => ['Book'], 'SecondaryAuthors' => ['Burch, Philip H., Jr.', 'Anonymous, 1971-1973', 'Elseperson, Firstnamery, 1971-1973'], 'ShortTitle' => 'The New Deal to the Carter administration', 'Subtitle' => '', @@ -193,6 +203,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ // eight authors, with a blend of formatting and extra punctuation/malformed dates 'raw' => [ + 'Formats' => ['Article'], 'SecondaryAuthors' => ['One, Person.', 'Person Two', 'Three, Person', 'Person Four.', 'Five, Person, 1900-1950', 'Six, Person 1910-1963', 'Person Seven', 'Person Eight 1900-1999'], 'ShortTitle' => 'Test Article', 'ContainerTitle' => 'Test Journal', @@ -208,6 +219,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ // eight authors 'raw' => [ + 'Formats' => ['Article'], 'SecondaryAuthors' => ['One, Person', 'Two, Person', 'Three, Person', 'Four, Person', 'Five, Person', 'Six, Person', 'Seven, Person', 'Eight, Person'], 'ShortTitle' => 'Test Article', 'ContainerTitle' => 'Test Journal', @@ -223,6 +235,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ // seven authors 'raw' => [ + 'Formats' => ['Article'], 'SecondaryAuthors' => ['One, Person', 'Two, Person', 'Three, Person', 'Four, Person', 'Five, Person', 'Six, Person', 'Seven, Person'], 'ShortTitle' => 'Test Article', 'ContainerTitle' => 'Test Journal', @@ -238,6 +251,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ // six authors 'raw' => [ + 'Formats' => ['Article'], 'SecondaryAuthors' => ['One, Person', 'Two, Person', 'Three, Person', 'Four, Person', 'Five, Person', 'Six, Person'], 'ShortTitle' => 'Test Article', 'ContainerTitle' => 'Test Journal', @@ -253,6 +267,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ // three authors, including one with a random trailing comma 'raw' => [ + 'Formats' => ['Article'], 'SecondaryAuthors' => ['One, Person,', 'Two, Person', 'Three, Person'], 'ShortTitle' => 'Test Article', 'ContainerTitle' => 'Test Journal', @@ -268,6 +283,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ // two authors with birth dates in different formats, single-page article 'raw' => [ + 'Formats' => ['Article'], 'SecondaryAuthors' => ['One, Person, b. 1960', 'Two, Person, 1970-'], 'ShortTitle' => 'Test Article', 'ContainerTitle' => 'Test Journal', @@ -284,6 +300,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase [ // two authors with no comma in first author's name (test no comma before and) // and parenthetical note on second author (test it is removed) 'raw' => [ + 'Formats' => ['Article'], 'SecondaryAuthors' => ['IBM', 'Two, Person (Director), 1970-'], 'ShortTitle' => 'Test Article', 'ContainerTitle' => 'Test Journal', @@ -299,6 +316,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ // one author 'raw' => [ + 'Formats' => ['Article'], 'SecondaryAuthors' => ['One, Person'], 'ShortTitle' => 'Test Article', 'ContainerTitle' => 'Test Journal', @@ -314,6 +332,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ // eight authors in "first name first" format. 'raw' => [ + 'Formats' => ['Article'], 'SecondaryAuthors' => ['Person One b. 1960', 'Person Two 1869-', 'Person Three', 'Person Four', 'Person Five', 'Person Six', 'Person Seven', 'Person Eight'], 'ShortTitle' => 'Test Article', 'ContainerTitle' => 'Test Journal', @@ -329,6 +348,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ // ten authors in "first name first" format. 'raw' => [ + 'Formats' => ['Article'], 'SecondaryAuthors' => ['Person One', 'Person Two', 'Person Three', 'Person Four', 'Person Five', 'Person Six', 'Person Seven', 'Person Eight', 'Person Nine', 'Person Ten'], 'ShortTitle' => 'Test Article', 'ContainerTitle' => 'Test Journal', @@ -344,6 +364,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase ], [ // DOI 'raw' => [ + 'Formats' => ['Article'], 'SecondaryAuthors' => ['One, Person'], 'ShortTitle' => 'Test Article', 'ContainerTitle' => 'Test Journal', @@ -368,7 +389,7 @@ class CitationTest extends \PHPUnit\Framework\TestCase */ public function testCitations() { - $citation = new Citation(new \VuFind\Date\Converter()); + $citation = new Citation(new \VuFind\Date\Converter(), ['Site' => ['language' => 'en']]); $citation->setView($this->getPhpRenderer()); $driver = new \VuFindTest\RecordDriver\TestHarness(); foreach ($this->citations as $current) { @@ -376,15 +397,18 @@ public function testCitations() $cb = $citation($driver); // Normalize whitespace: - $apa = trim(preg_replace("/\s+/", ' ', $cb->getCitation('APA'))); + $apa = $cb->getCitation('apa'); + $apa = trim(preg_replace("/\s+/", ' ', $apa)); $this->assertEquals($current['apa'], $apa); // Normalize whitespace: - $mla = trim(preg_replace("/\s+/", ' ', $cb->getCitation('MLA'))); + $mla = $cb->getCitation('modern-language-association'); + $mla = trim(preg_replace("/\s+/", ' ', $mla)); $this->assertEquals($current['mla'], $mla); // Normalize whitespace: - $chicago = trim(preg_replace("/\s+/", ' ', $cb->getCitation('Chicago'))); + $chicago = $cb->getCitation('chicago-annotated-bibliography'); + $chicago = trim(preg_replace("/\s+/", ' ', $chicago)); $this->assertEquals($current['chicago'], $chicago); } diff --git a/themes/bootstrap5/templates/record/cite.phtml b/themes/bootstrap5/templates/record/cite.phtml index 4d02074d9f4..0a284018dad 100644 --- a/themes/bootstrap5/templates/record/cite.phtml +++ b/themes/bootstrap5/templates/record/cite.phtml @@ -14,32 +14,35 @@ $helper = $this->citation($this->driver); $citations = []; foreach ($this->driver->getCitationFormats() as $format) { - $citations[$format] = $helper->getCitation($format); + [$label, $style] = explode(':', $format); + $citations[$label] = $helper->getCitation($style); } ?> transEsc('No citations are available for this record')?> - $citation): ?> - translate($format . ' Citation'); - $longCaption = $this->translate($format . ' Edition Citation'); - $caption = (strpos($longCaption, 'Citation') > 0 && !str_contains($shortCaption, 'Citation')) - ? $shortCaption : $longCaption; - $elementId = 'citation-' . $this->escapeHtmlAttr($format); - ?> - escapeHtml($caption)?> -
- - - - copyToClipboardButton('#' . $elementId)?> -
- +
+ $citation): ?> + translate($format . ' Citation'); + $longCaption = $this->translate($format . ' Edition Citation'); + $caption = (strpos($longCaption, 'Citation') > 0 && !str_contains($shortCaption, 'Citation')) + ? $shortCaption : $longCaption; + $elementId = 'citation-' . $this->escapeHtmlAttr($format); + ?> +
escapeHtml($caption)?>
+
+ + + + copyToClipboardButton('#' . $elementId)?> +
+ +
transEsc('Warning: These citations may not always be 100% accurate')?>.