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 + '