From 1cee13088445116244e9b5b661625bd742b8685a Mon Sep 17 00:00:00 2001 From: s-heppner Date: Thu, 8 Oct 2026 21:35:41 +0200 Subject: [PATCH] Allow DOI form of `note` in IEEEtran rules Previously, the IEEEtran ruleset only accepted the `note` field in the form `[ONLINE]. Available: \url{...}, Accessed: YYYY-MM-DD`. For entries with a DOI this is wrong: The IEEE Reference Guide gives a DOI as `doi: 10.xxxx/yyy`, without the `[Online]` prefix and without an access date, since a DOI is persistent. Additionally, a `doi` field was silently dropped by `IEEEtran.bst` without any warning. This change extends `check_url_field` to also accept `doi: \href{https://doi.org/10.xxxx/yyy}{10.xxxx/yyy}` in `note`, where the shown DOI must be the same as the DOI in the URL. A `doi` field is now flagged in the same way as `url`. The error message and `docs/IEEEtran_best_practices.md` now list both forms and use the date format `YYYY-MM-DD` that the check actually requires, instead of `YYYY-mmm-dd`. Tests for `check_url_field` are added. Since importing the IEEEtran rules registers them globally, the existing `verify` tests now patch the list of rules to only contain their own example rule. --- bibtex_linter/ieeetran_rules.py | 31 +++++++++------- docs/IEEEtran_best_practices.md | 64 ++++++++++++++++++++++++--------- test/test_verification.py | 61 +++++++++++++++++++++++++++++-- 3 files changed, 125 insertions(+), 31 deletions(-) diff --git a/bibtex_linter/ieeetran_rules.py b/bibtex_linter/ieeetran_rules.py index c9512cd..9dccff5 100644 --- a/bibtex_linter/ieeetran_rules.py +++ b/bibtex_linter/ieeetran_rules.py @@ -14,32 +14,37 @@ @linter_rule(entry_type=None) def check_url_field(entry: BibTeXEntry) -> List[str]: """ - Check that the `url` field is not set. - Additionally, if the `note` field is set, check that it conforms to the following schema: + Check that the `url` and `doi` fields are not set, since `IEEEtran.bst` does not render them properly. + Additionally, if the `note` field is set, check that it conforms to one of the following schemas: ``` - [ONLINE]. Available: \\url{...}, Accessed: YYYY-mmm-dd + [ONLINE]. Available: \\url{...}, Accessed: YYYY-MM-DD + doi: \\href{https://doi.org/10.xxxx/yyy}{10.xxxx/yyy} ``` Note, that the backslash had to be escaped here and is only meant to be a single one. + In the DOI form, the shown DOI must be the same as the DOI in the URL. Since a DOI is persistent, it has no + access date. :param entry: The BibTeXEntry :return: A list of string descriptions of rule violations for this entry. """ invariant_violations: List[str] = [] - if "url" in entry.fields.keys(): - invariant_violations.append( - "Contains the non-allowed field: [url]. " - "Move the content of the field into the [note] field." - ) + for field in ("url", "doi"): + if field in entry.fields.keys(): + invariant_violations.append( + f"Contains the non-allowed field: [{field}]. " + "Move the content of the field into the [note] field." + ) if "note" in entry.fields.keys(): note_content: str = entry.fields["note"] - pattern = r"^\[ONLINE\]\. Available: \\url\{(.+?)\}, Accessed: (\d{4}-\d{2}-\d{2})$" - match = re.match(pattern, note_content) - if not match: + online_pattern = r"^\[ONLINE\]\. Available: \\url\{(.+?)\}, Accessed: (\d{4}-\d{2}-\d{2})$" + doi_pattern = r"^doi: \\href\{https://doi\.org/(10\.\d{4,9}/\S+)\}\{\1\}$" + if not (re.match(online_pattern, note_content) or re.match(doi_pattern, note_content)): invariant_violations.append( "Contains a malformed field [note]. " - "Make sure the [note] field follows the following pattern: '[ONLINE]. Available: \\url{...}, " - "Accessed: YYYY-mmm-dd'" + "Make sure the [note] field follows one of the following patterns: " + "'[ONLINE]. Available: \\url{...}, Accessed: YYYY-MM-DD' or " + "'doi: \\href{https://doi.org/10.xxxx/yyy}{10.xxxx/yyy}'" ) return invariant_violations diff --git a/docs/IEEEtran_best_practices.md b/docs/IEEEtran_best_practices.md index 92191eb..658eb91 100644 --- a/docs/IEEEtran_best_practices.md +++ b/docs/IEEEtran_best_practices.md @@ -31,9 +31,13 @@ This document is meant as a cheatsheet for how to set up your `refs.bib` file wh - `note`: See special rule below - Disallowed fields: - `url`: See special rule below + - `doi`: See special rule below - Special rules: - - `note` and `url` convention: Do not use `url`, rather format the `note` in the following pattern: - `[ONLINE]. Available: \url{...}, Accessed: YYYY-mmm-dd` + - `note`, `url` and `doi` convention: Do not use `url` or `doi`, rather format the `note` in one of the following + patterns: + - For links: `[ONLINE]. Available: \url{...}, Accessed: YYYY-MM-DD` + - For DOIs: `doi: \href{https://doi.org/10.xxxx/yyy}{10.xxxx/yyy}` (no access date, the shown DOI must be the + same as the DOI in the URL) ## Conference - Entry names: `@conference`, `@inproceedings` @@ -58,9 +62,13 @@ This document is meant as a cheatsheet for how to set up your `refs.bib` file wh - Disallowed fields: - `paper`: Seems to be not standard, so avoid using it - `url`: See special rule below + - `doi`: See special rule below - Special rules: - - `note` and `url` convention: Do not use `url`, rather format the `note` in the following pattern: - `[ONLINE]. Available: \url{...}, Accessed: YYYY-mmm-dd` + - `note`, `url` and `doi` convention: Do not use `url` or `doi`, rather format the `note` in one of the following + patterns: + - For links: `[ONLINE]. Available: \url{...}, Accessed: YYYY-MM-DD` + - For DOIs: `doi: \href{https://doi.org/10.xxxx/yyy}{10.xxxx/yyy}` (no access date, the shown DOI must be the + same as the DOI in the URL) - If both `organization` and `publisher` have the exact same value, only use `publisher`. ## Online @@ -78,9 +86,13 @@ This document is meant as a cheatsheet for how to set up your `refs.bib` file wh - `note`: See special rule below - Disallowed fields: - `url`: See special rule below + - `doi`: See special rule below - Special rules: - - `note` and `url` convention: Do not use `url`, rather format the `note` in the following pattern: - `[ONLINE]. Available: \url{...}, Accessed: YYYY-mmm-dd` + - `note`, `url` and `doi` convention: Do not use `url` or `doi`, rather format the `note` in one of the following + patterns: + - For links: `[ONLINE]. Available: \url{...}, Accessed: YYYY-MM-DD` + - For DOIs: `doi: \href{https://doi.org/10.xxxx/yyy}{10.xxxx/yyy}` (no access date, the shown DOI must be the + same as the DOI in the URL) - If `organization` and `author` are the same, only use `author` ## Book @@ -102,9 +114,13 @@ This document is meant as a cheatsheet for how to set up your `refs.bib` file wh - `note`: See special rule below - Disallowed fields: - `url`: See special rule below + - `doi`: See special rule below - Special rules: - - `note` and `url` convention: Do not use `url`, rather format the `note` in the following pattern: - `[ONLINE]. Available: \url{...}, Accessed: YYYY-mmm-dd` + - `note`, `url` and `doi` convention: Do not use `url` or `doi`, rather format the `note` in one of the following + patterns: + - For links: `[ONLINE]. Available: \url{...}, Accessed: YYYY-MM-DD` + - For DOIs: `doi: \href{https://doi.org/10.xxxx/yyy}{10.xxxx/yyy}` (no access date, the shown DOI must be the + same as the DOI in the URL) - If `editor` and `publisher` are the same, only use `publisher`. ## InBook @@ -128,10 +144,14 @@ This document is meant as a cheatsheet for how to set up your `refs.bib` file wh - `note`: See special rule below - Disallowed fields: - `url`: See special rule below + - `doi`: See special rule below - `editor`: Field is not rendered - Special rules: - - `note` and `url` convention: Do not use `url`, rather format the `note` in the following pattern: - `[ONLINE]. Available: \url{...}, Accessed: YYYY-mmm-dd` + - `note`, `url` and `doi` convention: Do not use `url` or `doi`, rather format the `note` in one of the following + patterns: + - For links: `[ONLINE]. Available: \url{...}, Accessed: YYYY-MM-DD` + - For DOIs: `doi: \href{https://doi.org/10.xxxx/yyy}{10.xxxx/yyy}` (no access date, the shown DOI must be the + same as the DOI in the URL) ## InCollection - Entry name: `@incollection` @@ -153,10 +173,14 @@ This document is meant as a cheatsheet for how to set up your `refs.bib` file wh - `note`: See special rule below - Disallowed fields: - `url`: See special rule below + - `doi`: See special rule below - `type`: Disallowed, since if it was set to (Article, Paper, etc.), we should use the proper entry type instead.) - Special rules: - - `note` and `url` convention: Do not use `url`, rather format the `note` in the following pattern: - `[ONLINE]. Available: \url{...}, Accessed: YYYY-mmm-dd` + - `note`, `url` and `doi` convention: Do not use `url` or `doi`, rather format the `note` in one of the following + patterns: + - For links: `[ONLINE]. Available: \url{...}, Accessed: YYYY-MM-DD` + - For DOIs: `doi: \href{https://doi.org/10.xxxx/yyy}{10.xxxx/yyy}` (no access date, the shown DOI must be the + same as the DOI in the URL) - If `editor` and `publisher` are the same, use `publisher`. ## Standard @@ -176,9 +200,13 @@ This document is meant as a cheatsheet for how to set up your `refs.bib` file wh - `note`: See special rule bewlo - Disallowed fields: - `url`: See special rule below + - `doi`: See special rule below - Special rules: - - `note` and `url` convention: Do not use `url`, rather format the `note` in the following pattern: - `[ONLINE]. Available: \url{...}, Accessed: YYYY-mmm-dd` + - `note`, `url` and `doi` convention: Do not use `url` or `doi`, rather format the `note` in one of the following + patterns: + - For links: `[ONLINE]. Available: \url{...}, Accessed: YYYY-MM-DD` + - For DOIs: `doi: \href{https://doi.org/10.xxxx/yyy}{10.xxxx/yyy}` (no access date, the shown DOI must be the + same as the DOI in the URL) - If `author` and `organization` are the same, use `organization` > [!note] @@ -200,9 +228,13 @@ This document is meant as a cheatsheet for how to set up your `refs.bib` file wh - `note`: See special rule below - Disallowed fields: - `url`: See special rule below + - `doi`: See special rule below - Special rules: - - `note` and `url` convention: Do not use `url`, rather format the `note` in the following pattern: - `[ONLINE]. Available: \url{...}, Accessed: YYYY-mmm-dd` + - `note`, `url` and `doi` convention: Do not use `url` or `doi`, rather format the `note` in one of the following + patterns: + - For links: `[ONLINE]. Available: \url{...}, Accessed: YYYY-MM-DD` + - For DOIs: `doi: \href{https://doi.org/10.xxxx/yyy}{10.xxxx/yyy}` (no access date, the shown DOI must be the + same as the DOI in the URL) > [!note] > Try using the other entry types instead of this one, if possible. diff --git a/test/test_verification.py b/test/test_verification.py index 9b3e733..10ca4e6 100644 --- a/test/test_verification.py +++ b/test/test_verification.py @@ -1,8 +1,11 @@ import unittest +from unittest import mock from typing import List, Set +from bibtex_linter import verification from bibtex_linter.verification import check_required_fields, check_omitted_fields, verify, linter_rule from bibtex_linter.parser import BibTeXEntry +from bibtex_linter.ieeetran_rules import check_url_field @linter_rule(entry_type="test_entry_type") @@ -66,7 +69,9 @@ def test_verify_combined_rule(self) -> None: "Has fields present that would be omitted in the compiled document: [url]. " "This could lead to a loss of information." ] - actual = verify(entry) + # Only check the rule defined here, not the ones registered by importing a ruleset + with mock.patch.object(verification, "_rules", [example_linter_rule]): + actual = verify(entry) self.assertEqual(expected, actual) def test_verify_skips_different_entry_type(self) -> None: @@ -75,10 +80,62 @@ def test_verify_skips_different_entry_type(self) -> None: name="skipped_entry", fields={"author": "Someone", "url": "http://example.org"} ) - actual = verify(entry) + with mock.patch.object(verification, "_rules", [example_linter_rule]): + actual = verify(entry) expected: List[str] = [] # No rules should apply self.assertEqual(expected, actual) +class TestIEEEtranUrlField(unittest.TestCase): + MALFORMED_NOTE = ( + "Contains a malformed field [note]. " + "Make sure the [note] field follows one of the following patterns: " + "'[ONLINE]. Available: \\url{...}, Accessed: YYYY-MM-DD' or " + "'doi: \\href{https://doi.org/10.xxxx/yyy}{10.xxxx/yyy}'" + ) + + def test_online_note_valid(self) -> None: + entry = BibTeXEntry( + entry_type="misc", + name="online_note", + fields={"note": "[ONLINE]. Available: \\url{https://example.com}, Accessed: 2025-01-01"} + ) + self.assertEqual([], check_url_field(entry)) + + def test_doi_note_valid(self) -> None: + entry = BibTeXEntry( + entry_type="article", + name="doi_note", + fields={"note": "doi: \\href{https://doi.org/10.1109/TPAMI.2008.12}{10.1109/TPAMI.2008.12}"} + ) + self.assertEqual([], check_url_field(entry)) + + def test_doi_note_mismatching_doi(self) -> None: + entry = BibTeXEntry( + entry_type="article", + name="doi_mismatch", + fields={"note": "doi: \\href{https://doi.org/10.1109/TPAMI.2008.12}{10.1109/TPAMI.2008.13}"} + ) + self.assertEqual([self.MALFORMED_NOTE], check_url_field(entry)) + + def test_doi_note_with_access_date(self) -> None: + entry = BibTeXEntry( + entry_type="article", + name="doi_accessed", + fields={"note": "doi: \\href{https://doi.org/10.1109/TPAMI.2008.12}{10.1109/TPAMI.2008.12}, " + "Accessed: 2025-01-01"} + ) + self.assertEqual([self.MALFORMED_NOTE], check_url_field(entry)) + + def test_doi_field_disallowed(self) -> None: + entry = BibTeXEntry( + entry_type="article", + name="doi_field", + fields={"doi": "10.1109/TPAMI.2008.12"} + ) + expected = ["Contains the non-allowed field: [doi]. Move the content of the field into the [note] field."] + self.assertEqual(expected, check_url_field(entry)) + + if __name__ == "__main__": unittest.main()