Skip to content

Commit 41e2879

Browse files
authored
Add new ruleset for IEEEtran citation style (#15)
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 41e2879

9 files changed

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