Skip to content

Commit 363e3d8

Browse files
committed
Add new ruleset for IEEEtran citation style
Previously, we did not draw a precise distinction between the two citation styles `IEEEtran` and `ieeetr` (from `IEEEtran.cls`). This was mainly due to the reason that I did not know myself, that those two were separate styles. This adds a new ruleset for the LaTeX built-in `IEEEtran` citation style. For this, we modify the way the script argument `ruleset` is parsed by allowing to specify the rulesets shipped with the program directly (e.g. by calling it by its name: `bibtex_linter path/to/refs.bib IEEEtran`). However, the default behavior stayed the same, so this change should be backward compatible. Additionally, we adapt the `README.md` accordingly, drawing a more precise disctinction between the `ieeetr` and `IEEEtran` styles. Furthermore, this adds the observations for the styles: `plain`, `apalike` and `IEEEtran` to the `test/test_template` directory. Fixes #12
1 parent b8d308c commit 363e3d8

9 files changed

Lines changed: 1119 additions & 8 deletions

File tree

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ As it turns out, a lot of different citation styles omit various fields, and it'
2020
Therefore, I created this tool (in Python, since that's what I know best), that can parse the entries and then performs
2121
arbitrary (self-defined) invariant checks on them.
2222

23-
In my field the most used citation style is `IEEEtran` so this is how I've defined the default rules of the script.
24-
I've written down the observations on which the rules are based [here](test/test_template/IEEEtran_observations.md).
23+
In my field the most used citation style is `ieeetr` so this is how I've defined the default rules of the script.
24+
I've written down the observations on which the rules are based [here](test/test_template/IEEEtr_observations.md).
25+
These should not be confused with the LaTeX built-in `IEEEtran` citation style, for which I also developed rules.
2526

2627
It is however relatively easy to define your own [custom ruleset](#advanced-custom-rulesets), should the need arise.
2728

@@ -45,6 +46,14 @@ The script will parse the file, perform the checks and print out the results.
4546
> As the `bibtex_linter` returns exit code `0`, if all checks have passed and `1`, if violations were found,
4647
> you could also use it in the CI of your LaTeX projects.
4748
49+
### Defined Rulesets
50+
Currently, the following rulesets are shipped with the `bibtex_linter`:
51+
52+
- `bibtex_linter path/to/refs.bib ieeetr` (default): Citation style of some IEEE conferences (needs `IEEEtran.cls`)
53+
- `bibtex_linter path/to/refs.bib IEEEtran`: LaTeX built-in IEEE citation style (via `\bibliographystyle{IEEEtran}`)
54+
55+
If you want to define your own rules, see the next section on how to do this:
56+
4857
### Advanced: Custom Rulesets
4958

5059
It is also possible to define your own rules inside a Python file.

bibtex_linter/ieeetran_rules.py

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
from typing import List, Set
2+
import re
3+
4+
from bibtex_linter.parser import BibTeXEntry
5+
from bibtex_linter.verification import (
6+
linter_rule,
7+
check_required_fields,
8+
check_required_field,
9+
check_omitted_fields,
10+
check_disallowed_field,
11+
)
12+
13+
@linter_rule(entry_type=None)
14+
def check_url_field(entry: BibTeXEntry) -> List[str]:
15+
"""
16+
Check that the `url` field is not set.
17+
Additionally, if the `note` field is set, check that it conforms to the following schema:
18+
19+
```
20+
[ONLINE]. Available: \\url{...}, Accessed: YYYY-mmm-dd
21+
```
22+
Note, that the backslash had to be escaped here and is only meant to be a single one.
23+
24+
:param entry: The BibTeXEntry
25+
:return: A list of string descriptions of rule violations for this entry.
26+
"""
27+
invariant_violations: List[str] = []
28+
if "url" in entry.fields.keys():
29+
invariant_violations.append(
30+
f"Entry '{entry.name}' contains the non-allowed field: [url]. "
31+
f"Move the content of the field into the [note] field."
32+
)
33+
if "note" in entry.fields.keys():
34+
note_content: str = entry.fields["note"]
35+
pattern = r"^\[ONLINE\]\. Available: \\url\{(.+?)\}, Accessed: (\d{4}-\d{2}-\d{2})$"
36+
match = re.match(pattern, note_content)
37+
if not match:
38+
invariant_violations.append(
39+
f"Entry '{entry.name}' contains a malformed field [note]. "
40+
"Make sure the [note] field follows the following pattern: '[ONLINE]. Available: \\url{...}, "
41+
"Accessed: YYYY-mmm-dd'"
42+
)
43+
return invariant_violations
44+
45+
46+
@linter_rule(entry_type="article")
47+
def check_article(entry: BibTeXEntry) -> List[str]:
48+
"""
49+
Check that the article entry type contains the required fields.
50+
51+
:param entry: The BibTeXEntry
52+
:return: A list of string descriptions of rule violations for this entry.
53+
"""
54+
invariant_violations: List[str] = []
55+
invariant_violations.extend(check_required_fields(
56+
entry,
57+
fields={
58+
"author",
59+
"title",
60+
"journal",
61+
"year"
62+
}
63+
))
64+
return invariant_violations
65+
66+
@linter_rule(entry_type="conference")
67+
def check_conference(entry: BibTeXEntry) -> List[str]:
68+
"""
69+
Check that conference entry type contains all required fields.
70+
Additionally, check that 'publisher' and 'organization' are not duplicates of each other.
71+
72+
:param entry: The BibTeXEntry
73+
:return: A list of string descriptions of rule violations for this entry.
74+
"""
75+
invariant_violations: List[str] = []
76+
invariant_violations.extend(check_required_fields(
77+
entry,
78+
fields={
79+
"author",
80+
"title",
81+
"booktitle",
82+
"publisher",
83+
"year",
84+
"type",
85+
}
86+
))
87+
invariant_violations.extend(check_required_field(
88+
entry,
89+
field="booktitle",
90+
explanation="This should be the name of the conference.",
91+
))
92+
invariant_violations.extend(check_required_field(
93+
entry,
94+
field="publisher",
95+
explanation="This should be the company that published the proceedings.",
96+
))
97+
invariant_violations.extend(check_required_field(
98+
entry,
99+
field="type",
100+
explanation="This should describe the type of report/publication (e.g., “Conference Paper”).",
101+
))
102+
if entry.fields.get("organization") == entry.fields.get("publisher"):
103+
invariant_violations.append(
104+
f"Entry '{entry.name}' fields [organization] and [publisher] are the same. Remove field [organization]."
105+
)
106+
return invariant_violations
107+
108+
109+
@linter_rule(entry_type="online")
110+
def check_online(entry: BibTeXEntry) -> List[str]:
111+
"""
112+
Check that online entry type contains all required fields.
113+
Additionally, check that 'author' and 'organization' are not duplicates of each other.
114+
115+
:param entry: The BibTeXEntry
116+
:return: A list of string descriptions of rule violations for this entry.
117+
"""
118+
invariant_violations: List[str] = []
119+
invariant_violations.extend(check_required_fields(
120+
entry,
121+
fields={
122+
"author",
123+
"title",
124+
"year",
125+
"howpublished",
126+
}
127+
))
128+
invariant_violations.extend(check_required_field(
129+
entry,
130+
field="howpublished",
131+
explanation="This should be something like: 'White paper', 'Blog post', 'GitHub repository', etc.",
132+
))
133+
if entry.fields.get("organization") == entry.fields.get("author"):
134+
invariant_violations.append(
135+
f"Entry '{entry.name}' fields [organization] and [author] are the same. Remove field [organization]."
136+
)
137+
return invariant_violations
138+
139+
140+
@linter_rule(entry_type="book")
141+
def check_book(entry: BibTeXEntry) -> List[str]:
142+
"""
143+
Check that book entry type contains all required fields.
144+
Additionally, check that 'publisher' and 'editor' are not duplicates of each other.
145+
146+
:param entry: The BibTeXEntry
147+
:return: A list of string descriptions of rule violations for this entry.
148+
"""
149+
invariant_violations: List[str] = []
150+
invariant_violations.extend(check_required_fields(
151+
entry,
152+
fields={
153+
"author",
154+
"title",
155+
"year",
156+
"publisher",
157+
}
158+
))
159+
if entry.fields.get("publisher") == entry.fields.get("editor"):
160+
invariant_violations.append(
161+
f"Entry '{entry.name}' fields [publisher] and [editor] are the same. Remove field [editor]."
162+
)
163+
return invariant_violations
164+
165+
166+
@linter_rule(entry_type="inbook")
167+
def check_in_book(entry: BibTeXEntry) -> List[str]:
168+
"""
169+
Check that inbook entry type contains all required fields.
170+
Additionally check that the field `editor` is not present.
171+
172+
:param entry: The BibTeXEntry
173+
:return: A list of string descriptions of rule violations for this entry.
174+
"""
175+
invariant_violations: List[str] = []
176+
invariant_violations.extend(check_required_fields(
177+
entry,
178+
fields={
179+
"author",
180+
"title",
181+
"year",
182+
"publisher",
183+
}
184+
))
185+
invariant_violations.extend(check_required_field(
186+
entry,
187+
field="title",
188+
explanation="This should be the title of the book.",
189+
))
190+
invariant_violations.extend(check_disallowed_field(
191+
entry,
192+
field="editor",
193+
explanation="This field is not rendered in IEEEtran-style.",
194+
))
195+
return invariant_violations
196+
197+
198+
@linter_rule(entry_type="incollection")
199+
def check_in_collection(entry: BibTeXEntry) -> List[str]:
200+
"""
201+
Check that incollection entry type contains all required fields.
202+
Additionally, check that the field `type` is not set.
203+
Furthermore, check that 'editor' and 'publisher' are not duplicates of each other.
204+
205+
:param entry: The BibTeXEntry
206+
:return: A list of string descriptions of rule violations for this entry.
207+
"""
208+
invariant_violations: List[str] = []
209+
invariant_violations.extend(check_required_fields(
210+
entry,
211+
fields={
212+
"author",
213+
"title",
214+
"year",
215+
"booktitle",
216+
"publisher",
217+
}
218+
))
219+
invariant_violations.extend(check_disallowed_field(
220+
entry,
221+
field="type",
222+
explanation="If this field is set to (Article, Paper, Essay etc.), you should use a different entry type."
223+
))
224+
if entry.fields.get("editor") == entry.fields.get("publisher"):
225+
invariant_violations.append(
226+
f"Entry '{entry.name}' fields [editor] and [publisher] are the same. Remove field [editor]."
227+
)
228+
return invariant_violations
229+
230+
231+
@linter_rule(entry_type="standard")
232+
def check_standard(entry: BibTeXEntry) -> List[str]:
233+
"""
234+
Check that standard entry type contains all required fields.
235+
Furthermore, check that 'author' and 'organization' are not duplicates of each other.
236+
237+
:param entry: The BibTeXEntry
238+
:return: A list of string descriptions of rule violations for this entry.
239+
"""
240+
invariant_violations: List[str] = []
241+
invariant_violations.extend(check_required_fields(
242+
entry,
243+
fields={
244+
"title",
245+
"organization",
246+
"type",
247+
"number",
248+
"year",
249+
}
250+
))
251+
invariant_violations.extend(check_required_field(
252+
entry,
253+
field="organization",
254+
explanation="This should be the issuing body or standards organization.",
255+
))
256+
invariant_violations.extend(check_required_field(
257+
entry,
258+
field="type",
259+
explanation="This should be something like "
260+
"(Standard, Technical Report, Recommendation, Specification, Guideline, Draft Standard).",
261+
))
262+
if entry.fields.get("author") == entry.fields.get("organization"):
263+
invariant_violations.append(
264+
f"Entry '{entry.name}' fields [author] and [organization] are the same. Remove field [author]."
265+
)
266+
return invariant_violations
267+
268+
269+
@linter_rule(entry_type="techreport")
270+
def check_tech_report(entry: BibTeXEntry) -> List[str]:
271+
"""
272+
Disallow the use of the techreport entry type.
273+
274+
:param entry: The BibTeXEntry
275+
:return: A list of string descriptions of rule violations for this entry.
276+
"""
277+
return [f"Entry '{entry.name}' is of type 'TECHREPORT'. Please use a different entry type, such as 'STANDARD'."]
278+
279+
280+
@linter_rule(entry_type="misc")
281+
def check_misc(entry: BibTeXEntry) -> List[str]:
282+
"""
283+
Check that misc entry type contains all required fields.
284+
285+
:param entry: The BibTeXEntry
286+
:return: A list of string descriptions of rule violations for this entry.
287+
"""
288+
invariant_violations: List[str] = []
289+
invariant_violations.extend(check_required_fields(
290+
entry,
291+
fields={
292+
"author",
293+
"title",
294+
"howpublished",
295+
"year",
296+
}
297+
))
298+
return invariant_violations

bibtex_linter/main.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,25 @@ def main() -> None:
3131
type=str,
3232
nargs="?",
3333
default=None,
34-
help="Path to the rules.py that define the rules. If left empty, the default ruleset is used. "
35-
"WARNING: Executes the Python code inside rules.py, so be sure that it's safe!")
34+
help="Name (ieeetr, IEEEtran) of or path to the rules.py that define the rules. "
35+
"If left empty, the default ruleset (ieeetr) is used. "
36+
"WARNING: Executes the Python code inside rules.py, so be sure that it's safe! "
37+
"See https://github.com/s-heppner/python-bibtex-linter for more information.")
3638

3739
args = parser.parse_args()
3840

3941
# Try to import the ruleset
4042
if args.ruleset is None:
41-
import bibtex_linter.default_rules
43+
import bibtex_linter.ieeetr_rules
4244
print("Using the default ruleset.")
4345
else:
4446
print(f"Importing rules from {args.ruleset}.")
45-
import_from_path(args.ruleset)
47+
if args.ruleset in ["default", "ieeetr"]:
48+
import bibtex_linter.ieeetr_rules
49+
elif args.ruleset == "IEEEtran":
50+
import bibtex_linter.ieeetran_rules
51+
else:
52+
import_from_path(args.ruleset)
4653

4754
entries: List[BibTeXEntry] = parse_bibtex_file(args.filepath)
4855
had_violations = False

0 commit comments

Comments
 (0)