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()