From 14c4ab52b25f4384f6db7938c2666fdd0f75a723 Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Fri, 16 Dec 2022 03:01:21 +0000 Subject: CTAN sync 202212160301 --- macros/latex/contrib/bibcop/DEPENDS.txt | 3 +- macros/latex/contrib/bibcop/README.md | 8 + macros/latex/contrib/bibcop/bibcop.1 | 15 +- macros/latex/contrib/bibcop/bibcop.dtx | 140 ++++++++--- macros/latex/contrib/bibcop/bibcop.pdf | Bin 331588 -> 341825 bytes macros/latex/contrib/bibcop/bibcop.pl | 404 ++++++++++++++++++++------------ 6 files changed, 372 insertions(+), 198 deletions(-) (limited to 'macros/latex/contrib/bibcop') diff --git a/macros/latex/contrib/bibcop/DEPENDS.txt b/macros/latex/contrib/bibcop/DEPENDS.txt index 424bb3f506..6767eafad4 100644 --- a/macros/latex/contrib/bibcop/DEPENDS.txt +++ b/macros/latex/contrib/bibcop/DEPENDS.txt @@ -1,2 +1,3 @@ hard iexec -hard verbatimcopy \ No newline at end of file +hard verbatimcopy +hard pgfopts diff --git a/macros/latex/contrib/bibcop/README.md b/macros/latex/contrib/bibcop/README.md index 08672deb1a..8a58d415f8 100644 --- a/macros/latex/contrib/bibcop/README.md +++ b/macros/latex/contrib/bibcop/README.md @@ -36,6 +36,14 @@ $ perl bibcop.pl --fix main.bib > fixed.bib This command will read the `main.bib` file and create `fixed.bib`, which will have the fixed and properly formatted content (well, to some extent). +If you install the package using [`tlmgr`](https://www.tug.org/texlive/tlmgr.html), +you should be able to use `bibcop` directly, without the necessity to mention Perl: + +``` +$ tlgmr install bibcop +$ bibcop --help +``` + ## How to Contribute If you want to contribute yourself, make a fork, then create a branch, diff --git a/macros/latex/contrib/bibcop/bibcop.1 b/macros/latex/contrib/bibcop/bibcop.1 index 8f92e42629..5cc7f77c15 100644 --- a/macros/latex/contrib/bibcop/bibcop.1 +++ b/macros/latex/contrib/bibcop/bibcop.1 @@ -1,16 +1,16 @@ -.TH bibcop 1 "2022-12-13" +.TH bibcop 1 "2022-12-15" .SH NAME -bibcop \- Style Checker and Fixer of .bib Files +bibcop \- Style Checker and Fixer of BibTeX Files (.bib) .SH SYNOPSIS bibcop [] <.bib file path> .SH DESCRIPTION -The bibcop tool is a Perl script for checking the quality of .bib -files and automatically fixing found inconsistencies. It is not only +The bibcop tool is a Perl script for checking the quality of BibTeX bibliograph +files (.bib) and automatically fixing found inconsistencies. It is not only a command line tool, but a LaTeX package, which can check the quality of a .bib file during the rendering of a LaTeX document. All found issues are reported as LaTeX warnings. .SH OPTIONS -Various options apply +Various options apply: .IP --version Print the version of the tool .IP --help @@ -19,10 +19,11 @@ Print the introduction screen Autho-fix all issues and print a new content to the console .IP --latex Print everything in LaTeX format -.SH BUGS +.IP --verbose +Print debugging information too .SH AUTHOR Yegor Bugayenko (yegor256@gmail.com) -.PP +.SH BUGS Please log issues on the GitHub homepage: https://github.com/yegor256/bibcop/issues. .SH SEE ALSO diff --git a/macros/latex/contrib/bibcop/bibcop.dtx b/macros/latex/contrib/bibcop/bibcop.dtx index 36c8ac747b..de48e128ef 100644 --- a/macros/latex/contrib/bibcop/bibcop.dtx +++ b/macros/latex/contrib/bibcop/bibcop.dtx @@ -50,7 +50,7 @@ %\NeedsTeXFormat{LaTeX2e} %\ProvidesPackage{bibcop} %<*package> -[2022-12-13 0.0.3 Style Checker of Bibliography Files] +[2022-12-15 0.0.4 Style Checker of Bibliography Files] % %<*driver> \documentclass{ltxdoc} @@ -68,9 +68,9 @@ \CodelineIndex \RecordChanges \begin{document} - \DocInput{bibcop.dtx} - \PrintChanges - \PrintIndex + \DocInput{bibcop.dtx} + \PrintChanges + \PrintIndex \end{document} % % \fi @@ -88,8 +88,8 @@ % \section{Introduction} % -% This package scans your |.bib| files for style errors and emits -% warning messages if any issues are found (make sure you use it before +% This package scans a |.bib| file for style errors and emits +% warning messages if any issues are found (the package must be included before % all other bibliography related packages): %\iffalse %<*verb> @@ -105,40 +105,57 @@ %\iffalse % %\fi -% You may see warnings in the \TeX{} log --- fix the issues in the |main.bib| file -% and the warnings will disappear. +% Some warnings may be printed in the \TeX{} log. +% Once the issues in the |main.bib| file are fixed, the warnings disappear. -% If you use the |.sty| file (without installing it into the \TeX{} tree), don't forget -% to also copy the |bibcop.pl| file --- it is the Perl script that does all the work +% If the |.sty| file is used directly (without installing it into the \TeX{} tree), +% the |bibcop.pl| file must also be placed next to it --- it is the Perl script that does all the work % of checking your |.bib| files. The |.sty| is just a simple wrapper around it. -% Make sure |\usepackage{bibcop}| stays right after |\usepackage{biblatex}| -% (if you use it), otherwise -% you won't see any warnings from |bibcop|. +% The |\usepackage{bibcop}| must stay right after |\usepackage{biblatex}| +% (if Bib\LaTeX{} is used), otherwise +% there won't be any warnings from |bibcop|. + +% \section{Package Options} + +% It's possible to configure the behavior of the package with the help of a few package options: + +% \DescribeMacro{verbose} +% The |verbose| package option prints all debugging messages to the \TeX{} log: +%\iffalse +%<*verb> +%\fi +\begin{verbatim} +\usepackage[verbose]{bibcop} +\end{verbatim} +%\iffalse +% +%\fi % \section{The Rules} -% This is a more or less complete list of rules we enforce on a |.bib| file: +% This is a more or less complete list of rules enforced on a |.bib| file: % \DescribeMacro{types} -% Only |@article|, |@book|, |@inproceedings|, and |@misc| types of bib items are allowed. +% Only |@article|, |@book|, |@inproceedings|, and |@misc| types of \BibTeX{} entries are allowed. % Everything else, like |@manual|, |@phdthesis|, and many others are simply prohibited. % The mentioned four should be enough for everything. -% \DescribeMacro{keys} -% There are pretty limited lists of allowed keys for each type of bib item. The keys +% \DescribeMacro{tags} +% There are pretty limited lists of allowed tags for each type of \BibTeX{} entry. The tags % that are not in the list are prohibited to use. % \DescribeMacro{doi} -% Every bib item must have the |doi| key, which is a unique +% Every \BibTeX{} entry must have the |doi| tag, which is a unique % \href{https://www.doi.org}{Digital Object Identifier} % of the material that you reference. It seems to be a good practice, in order to avoid ambiguity, -% to always mention the DOI. I would also recommend to use \href{https://ctan.org/pkg/iexec}{doi} package, +% to always mention the DOI. +% It also recommended to use the \href{https://ctan.org/pkg/iexec}{doi} package, % in order to make all ``|doi|'' fields turned into hyper links. % \DescribeMacro{caps} -% In |title|, |booktitle|, and |journal| keys, all major words must be capitalized, -% as it is \href{https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case}{recommended by APA}: +% In the |title|, |booktitle|, and |journal| tags, all major words must be capitalized, +% as it is \href{https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case}{recommended} by APA: %\iffalse %<*verb> %\fi @@ -151,9 +168,9 @@ title = {A Preliminary Architecture for a Basic Data-Flow Processor} % Here, the leading ``|A|'' is capital because it opens the title. % The word ``|for|'' and the article ``|a|'' are minor words, that's why they are in lower case. % Both parts of the composite word ``|Data-Flow|'' are capitalized. -% Sometimes you need to violate this rule and use custom capitalization, as it is done -% by the author of the paper. In order to do this, wrap the words with custom capitalization -% in curled brackets, for example: +% Sometimes this rule may need to be violated, when there is custom capitalization, as it is done +% by the author of the paper. In order to do this, the words with custom capitalization +% must be wrapped in curled brackets, for example: %\iffalse %<*verb> %\fi @@ -190,7 +207,7 @@ author = {Knuth, Donald E. and others} % When first names are shortened to a single letter, it has to have a tailing dot. % \DescribeMacro{shorts} -% It is not allowed to shorten any words, for example this is illegal: +% It is not allowed to shorten any words aside from the |author| tag, for example this is illegal: %\iffalse %<*verb> %\fi @@ -212,7 +229,7 @@ journal = {Journal of Logic and Computation} %\fi % \DescribeMacro{brackets} -% The |title|, |booktitle|, and |journal| must be wrapped with double brackets, for example: +% The |title|, |booktitle|, and |journal| must be wrapped in double brackets, for example: %\iffalse %<*verb> %\fi @@ -237,8 +254,8 @@ booktitle = {{1994 IEEE International Conference on Computer Languages}}, %\iffalse % %\fi -% The year should only be mentioned in the |year| key, nowhere else. -% In the |year| key only numbers are allowed: +% The year should only be mentioned in the |year| tag, nowhere else. +% In the |year| tag only numbers are allowed: %\iffalse %<*verb> %\fi @@ -298,7 +315,7 @@ pages = {145--163}, %\fi % \DescribeMacro{proceedings} -% The |booktitle| in the |@inproceedings| bib item must always start with ``|Proceedings| |of| |the|'', +% The |booktitle| in the |@inproceedings| entry must always start with ``|Proceedings| |of| |the|'', % as in this example: %\iffalse %<*verb> @@ -311,11 +328,38 @@ booktitle = {{Proceedings of the International % %\fi +% \DescribeMacro{arXiv} +% If the |archivePrefix| is present, the |eprint| and the |primaryClass| must also be present and must adhere to the formatting principles of \href{https://arxiv.org/help/arxiv_identifier}{arXiv identifiers}: +%\iffalse +%<*verb> +%\fi +\begin{verbatim} +@misc{bugayenko2021, + archivePrefix = {arXiv}, + eprint = {2111.13384}, + primaryClass = {cs.PL}, +} +\end{verbatim} +%\iffalse +% +%\fi + +% \DescribeMacro{typography} +% All tags in each \BibTeX{} entry are checked for obeying the basic typography rules: +% \begin{itemize} +% \item No spaces are allowed in front of a comma, a semi-colon, a colon, a dot, a question mark, and an exclamation mark; +% \item A text may not end with a dot, a comma, a semi-colon, a colon, or a dash; +% \item A triple dash must be surrounded by spaces. +% \end{itemize} + % \StopEventually{} % \section{Implementation} % \changes{0.0.1}{2022/12/11}{First draft.} % \changes{0.0.2}{2022/12/12}{Documentation extended, more rules added.} +% \changes{0.0.4}{2022/12/14}{Extra checks for the typography, together with more extensive Perl testing.} +% \changes{0.0.4}{2022/12/14}{The \texttt{--verbose} option introduced, to enable debugging information only on demand.} +% \changes{0.0.4}{2022/12/15}{Package options introduced, the \texttt{verbose} option enables detailed logging inside the \TeX{} log.} % First, we include a few packages. % We need \href{https://ctan.org/pkg/iexec}{iexec} for executing Perl scripts: @@ -323,6 +367,16 @@ booktitle = {{Proceedings of the International \RequirePackage{iexec} % \end{macrocode} +% Then, we process package options: +% \begin{macrocode} +\RequirePackage{pgfopts} +\pgfkeys{ + /bibcop/.cd, + verbose/.store in=\bibcop@verbose, +} +\ProcessPgfPackageOptions{/bibcop} +% \end{macrocode} + % \begin{macro}{bibcop.pl} % Then, we copy the Perl script using |\VerbatimCopy| from % \href{https://ctan.org/pkg/verbatimcopy}{verbatimcopy}: @@ -338,12 +392,14 @@ booktitle = {{Proceedings of the International % \begin{macrocode} \makeatletter \ifdefined\bibliography - \let\bibcop@oldbibliography\bibliography - \renewcommand\bibliography[1]{% - \iexec{perl "./bibcop.tmp.pl" --latex '#1.bib'}% + \let\bibcop@oldbibliography\bibliography + \renewcommand\bibliography[1]{% + \iexec{perl "./bibcop.tmp.pl"\space + \ifdefined\bibcop@verbose--verbose\fi\space + --latex '#1.bib'}% \message{bibcop: style checking finished^^J}% - \bibcop@oldbibliography{#1}% - } + \bibcop@oldbibliography{#1}% + } \fi \makeatother % \end{macrocode} @@ -354,17 +410,23 @@ booktitle = {{Proceedings of the International % \begin{macrocode} \makeatletter \ifdefined\addbibresource - \let\bibcop@oldaddbibresource\addbibresource - \renewcommand\addbibresource[1]{% - \iexec{perl "./bibcop.tmp.pl" --latex '#1'}% + \let\bibcop@oldaddbibresource\addbibresource + \renewcommand\addbibresource[1]{% + \iexec{perl "./bibcop.tmp.pl"\space + \ifdefined\bibcop@verbose--verbose\fi\space + --latex '#1'}% \message{bibcop: style checking finished^^J}% - \bibcop@oldaddbibresource{#1}% - } + \bibcop@oldaddbibresource{#1}% + } \fi \makeatother % \end{macrocode} % \end{macro} +% \begin{macrocode} +\endinput +% \end{macrocode} + % \Finale % \clearpage diff --git a/macros/latex/contrib/bibcop/bibcop.pdf b/macros/latex/contrib/bibcop/bibcop.pdf index 09901670e8..e6683a73bb 100644 Binary files a/macros/latex/contrib/bibcop/bibcop.pdf and b/macros/latex/contrib/bibcop/bibcop.pdf differ diff --git a/macros/latex/contrib/bibcop/bibcop.pl b/macros/latex/contrib/bibcop/bibcop.pl index 8b900bbc35..ef5ee1f457 100755 --- a/macros/latex/contrib/bibcop/bibcop.pl +++ b/macros/latex/contrib/bibcop/bibcop.pl @@ -32,36 +32,36 @@ my %args = map { $_ => 1 } @ARGV; # If you want to add an extra check, just create a new procedure # named as "check_*". -# Only these keys are allowed and only these types of items. +# Only these tags are allowed and only these types of entries. my %blessed = ( - 'article' => ['doi', 'year', 'title', 'author', 'journal', 'volume', 'number', 'publisher?'], + 'article' => ['doi', 'year', 'title', 'author', 'journal', 'volume', 'number', 'publisher?', 'pages?'], 'inproceedings' => ['doi', 'booktitle', 'title', 'author', 'year', 'pages?', 'organization?', 'volume?'], 'book' => ['doi', 'title', 'author', 'year', 'publisher'], 'misc' => ['title', 'author', 'year', 'eprint?', 'archiveprefix?', 'primaryclass?', 'publisher?', 'organization?', 'doi?'], ); -# Check the presence of mandatory keys. -sub check_mandatory_keys { - my (%item) = @_; - my $type = $item{':type'}; +# Check the presence of mandatory tags. +sub check_mandatory_tags { + my (%entry) = @_; + my $type = $entry{':type'}; my $mandatory = $blessed{$type}; - foreach my $key (@$mandatory) { - if ($key =~ /^.*\?$/) { + foreach my $tag (@$mandatory) { + if ($tag =~ /^.*\?$/) { next; } - if (not(exists $item{$key})) { - my $listed = listed_keys(%item); - return "A mandatory '$key' key for '\@$type' is missing among $listed" + if (not(exists $entry{$tag})) { + my $listed = listed_tags(%entry); + return "A mandatory '$tag' tag for '\@$type' is missing among $listed" } } if (exists $blessed{$type}) { my %required = map { $_ => 1 } @$mandatory; - foreach my $key (keys %item) { - if ($key =~ /^:/) { + foreach my $tag (keys %entry) { + if ($tag =~ /^:/) { next; } - if (not(exists $required{$key}) && not(exists $required{$key . '?'})) { - return "The '$key' key is not suitable for '$type', use only these: (@$mandatory)" + if (not(exists $required{$tag}) && not(exists $required{$tag . '?'})) { + return "The '$tag' tag is not suitable for '$type', use only these: (@$mandatory)" } } } @@ -69,14 +69,14 @@ sub check_mandatory_keys { # Check that all major words are capitalized. sub check_capitalization { - my (%item) = @_; - my %keys = map { $_ => 1 } qw/title booktitle journal/; + my (%entry) = @_; + my %tags = map { $_ => 1 } qw/title booktitle journal publisher organization/; my %minors = map { $_ => 1 } qw/in of at to by the a an and or as if up via yet nor but off on for into/; - foreach my $key (keys %item) { - if (not exists $keys{$key}) { + foreach my $tag (keys %entry) { + if (not exists $tags{$tag}) { next; } - my $value = $item{$key}; + my $value = $entry{$tag}; my @words = only_words($value); my $pos = 0; foreach my $word (@words) { @@ -88,10 +88,10 @@ sub check_capitalization { next; } if (exists $minors{lc($word)} and $pos gt 1) { - return "All minor words in the '$key' must be lower-cased, while '$word' (no.$pos) is not" + return "All minor words in the '$tag' must be lower-cased, while '$word' (no.$pos) is not" } if ($word =~ /^[a-z].*/) { - return "All major words in the '$key' must be capitalized, while '$word' (no.$pos) is not" + return "All major words in the '$tag' must be capitalized, while '$word' (no.$pos) is not" } } } @@ -99,9 +99,9 @@ sub check_capitalization { # Check that the 'author' is formatted correctly. sub check_author { - my (%item) = @_; - if (exists $item{'author'} and not $item{'author'} =~ /^\{.+\}$/) { - my $author = clean_tex($item{'author'}); + my (%entry) = @_; + if (exists $entry{'author'} and not $entry{'author'} =~ /^\{.+\}$/) { + my $author = clean_tex($entry{'author'}); if (not $author =~ /^[A-Z][^ ]+(,( [A-Z][^ ]+)+)?( and [A-Z][^ ]+(,( [A-Z][^ ]+)+)?)*( and others)?$/) { return "The format of the 'author' is wrong, use something like 'Knuth, Donald E. and Duane, Bibby'" } @@ -113,20 +113,20 @@ sub check_author { # Check that titles don't have shortened words with a tailing dot. sub check_shortenings { - my (%item) = @_; - my %keys = map { $_ => 1 } qw/title booktitle journal/; - foreach my $key (keys %item) { - if (not exists $keys{$key}) { + my (%entry) = @_; + my %tags = map { $_ => 1 } qw/title booktitle journal/; + foreach my $tag (keys %entry) { + if (not exists $tags{$tag}) { next; } - my $value = $item{$key}; + my $value = $entry{$tag}; my @words = only_words($value); foreach my $word (@words) { if (not $word =~ /^[A-Za-z]/) { next; } if ($word =~ /^.*\.$/) { - return "Do not shorten the words in the '$key', such as '$word'" + return "Do not shorten the words in the '$tag', such as '$word'" } } } @@ -134,15 +134,59 @@ sub check_shortenings { # Check the right format of the 'title' and 'booktitle.' sub check_titles { - my (%item) = @_; - my @keys = qw/title booktitle/; - foreach my $key (@keys) { - if (not exists($item{$key})) { + my (%entry) = @_; + my @tags = qw/title booktitle/; + foreach my $tag (@tags) { + if (not exists($entry{$tag})) { next; } - my $title = $item{$key}; + my $title = $entry{$tag}; if (not $title =~ /^\{.+\}$/) { - return "The '$key' must be wrapped in double curled brackets" + return "The '$tag' must be wrapped in double curled brackets" + } + } +} + +# Check the right format of the tags for arXiv. +# See https://arxiv.org/help/arxiv_identifier +sub check_arXiv { + my (%entry) = @_; + if (exists($entry{'archiveprefix'})) { + if (not exists $entry{'eprint'}) { + return "The 'eprint' is mandatory when 'archiveprefix' is there" + } + if (not $entry{'eprint'} =~ /^[0-9]{4}\.[0-9]{4,5}(v[0-9]+)?$/) { + return "The 'eprint' must have two integers separated by a dot" + } + my $eprint = $entry{'eprint'}; + my ($head, $tail) = split(/\./, $eprint); + my $year = substr($head, 0, 2); + my $month = substr($head, 2); + if ($month > 12) { + return "The month '$month' of the 'eprint' is wrong, it can't be bigger than 12" + } + if (not exists $entry{'primaryclass'}) { + return "The 'primaryclass' is mandatory when 'archiveprefix' is there" + } + if (not $entry{'primaryclass'} =~ /^[a-z]{2,}\.[A-Z]{2}$/) { + return "The 'primaryclass' must have two parts, like 'cs.PL'" + } + } +} + +# Check that organization is not mentioned in the booktitle. +sub check_org_in_booktitle { + my (%entry) = @_; + my @orgs = ( 'ACM', 'IEEE' ); + if (exists($entry{'booktitle'})) { + my $title = $entry{'booktitle'}; + foreach my $o (@orgs) { + if ($title =~ /^.*\Q$o\E.*$/) { + return "The '$o' organization must not be mentioned in the booktitle, use 'publisher' tag instead" + } + } + if ($title =~ /^.*(ACM|IEEE).*$/) { + return "Don't mention the" } } } @@ -150,47 +194,79 @@ sub check_titles { # Check that no values have tailing dots. # Check that there are no spaces before commans. sub check_typography { - my (%item) = @_; - foreach my $key (keys %item) { - if ($key =~ /^:.*/) { + my (%entry) = @_; + my %symbols = ( + '.' => 'dot', + ',' => 'comma', + ';' => 'semi-colon', + ':' => 'colon', + '!' => 'exclamation mark', + '?' => 'question mark', + '-' => 'dash', + '--' => 'double dash', + '---' => 'triple dash', + ); + my @spaces_around = ( '---' ); + my @no_spaces_around = ( '--', '-' ); + my @no_space_before = ( '.', ',', ';', ':', '?', '!' ); + my @bad_tails = ( '.', ',', ';', ':', '-' ); + foreach my $tag (keys %entry) { + if ($tag =~ /^:.*/) { next; } - my $value = $item{$key}; - if ($value =~ /.*\.$/ and $key ne 'author') { - return "The '$key' must not end with a dot" + my $value = $entry{$tag}; + foreach my $s (@bad_tails) { + if ($s eq '.' and $tag eq 'author') { + next; + } + if ($value =~ /^.*\Q$s\E$/) { + return "The '$tag' must not end with a $symbols{$s}" + } + } + foreach my $s (@no_space_before) { + if ($value =~ /^.*\s\Q$s\E.*$/) { + return "In the '$tag', do not put a space before a $symbols{$s}" + } } - if ($value =~ /.* ,.*/) { - return "In the '$key', do not put a space before the comma" + foreach my $s (@spaces_around) { + if ($value =~ /^.*[^\s]\Q$s\E.*$/ or $value =~ /^.*\Q$s\E[^\s].*$/) { + return "In the '$tag', put spaces around a $symbols{$s}" + } + } + foreach my $s (@no_spaces_around) { + if ($value =~ /^.*\s\Q$s\E\s.*$/) { + return "In the '$tag', don't put spaces around a $symbols{$s}" + } } } } # Check the year is not mentioned in titles. sub check_year_in_titles { - my (%item) = @_; - my @keys = qw/title booktitle journal/; - foreach my $key (@keys) { - if (not exists($item{$key})) { + my (%entry) = @_; + my @tags = qw/title booktitle journal/; + foreach my $tag (@tags) { + if (not exists($entry{$tag})) { next; } - my @words = only_words($item{$key}); + my @words = only_words($entry{$tag}); foreach my $word (@words) { if ($word =~ /^[1-9][0-9]{3}$/) { - return "The '$key' must not contain the year $word, it is enough to have the 'year' key" + return "The '$tag' must not contain the year $word, it is enough to have the 'year' tag" } } } } -# Check the right format of the 'booktitle' in the 'inproceedings' item. +# Check the right format of the 'booktitle' in the 'inproceedings' entry. sub check_booktile_of_inproceedings { - my (%item) = @_; - my $key = 'inproceedings'; - if ($item{':type'} eq $key) { - if (exists $item{'booktitle'}) { - my @words = only_words($item{'booktitle'}); + my (%entry) = @_; + my $tag = 'inproceedings'; + if ($entry{':type'} eq $tag) { + if (exists $entry{'booktitle'}) { + my @words = only_words($entry{'booktitle'}); if (lc($words[0]) ne 'proceedings' or lc($words[1]) ne 'of' or lc($words[2]) ne 'the') { - return "The '$key' must start with 'Proceedings of the ...'" + return "The '$tag' must start with 'Proceedings of the ...'" } } } @@ -198,10 +274,10 @@ sub check_booktile_of_inproceedings { # Check the right format of the 'doi.' sub check_doi { - my (%item) = @_; - if (exists $item{'doi'}) { - my $doi = $item{'doi'}; - if (not $item{'doi'} =~ /^[0-9a-zA-Z.]+\/[0-9a-zA-Z._\-]+$/) { + my (%entry) = @_; + if (exists $entry{'doi'}) { + my $doi = $entry{'doi'}; + if (not $doi =~ /^[0-9a-zA-Z.]+\/[0-9a-zA-Z._\-)(]+$/) { return "The format of the 'doi' is wrong" } } @@ -209,10 +285,10 @@ sub check_doi { # Check the right format of the 'year.' sub check_year { - my (%item) = @_; - if (exists $item{'year'}) { - my $year = $item{'year'}; - if (not $item{'year'} =~ /^[0-9]{3,4}$/) { + my (%entry) = @_; + if (exists $entry{'year'}) { + my $year = $entry{'year'}; + if (not $year =~ /^[0-9]{3,4}$/) { return "The format of the 'year' is wrong" } } @@ -220,10 +296,10 @@ sub check_year { # Check the right format of the 'month.' sub check_month { - my (%item) = @_; - if (exists $item{'month'}) { - my $month = $item{'month'}; - if (not $item{'month'} =~ /^[1-9]|10|11|12$/) { + my (%entry) = @_; + if (exists $entry{'month'}) { + my $month = $entry{'month'}; + if (not $month =~ /^[1-9]|10|11|12$/) { return "The format of the 'month' is wrong" } } @@ -231,10 +307,10 @@ sub check_month { # Check the right format of the 'volume.' sub check_volume { - my (%item) = @_; - if (exists $item{'volume'}) { - my $volume = $item{'volume'}; - if (not $item{'volume'} =~ /^[1-9][0-9]*$/) { + my (%entry) = @_; + if (exists $entry{'volume'}) { + my $volume = $entry{'volume'}; + if (not $volume =~ /^[1-9][0-9]*$/) { return "The format of the 'volume' is wrong" } } @@ -242,10 +318,10 @@ sub check_volume { # Check the right format of the 'number.' sub check_number { - my (%item) = @_; - if (exists $item{'number'}) { - my $number = $item{'number'}; - if (not $item{'number'} =~ /^[1-9][0-9]*$/) { + my (%entry) = @_; + if (exists $entry{'number'}) { + my $number = $entry{'number'}; + if (not $number =~ /^[1-9][0-9]*$/) { return "The format of the 'number' is wrong" } } @@ -253,14 +329,17 @@ sub check_number { # Check the right format of the 'pages.' sub check_pages { - my (%item) = @_; - if (exists $item{'pages'}) { - my $pages = $item{'pages'}; - if (not $item{'pages'} =~ /^[1-9][0-9]*--[1-9][0-9]*|[1-9][0-9]*$/) { + my (%entry) = @_; + if (exists $entry{'pages'}) { + my $pages = $entry{'pages'}; + if (not $pages =~ /^([1-9][0-9]*--[1-9][0-9]*|[1-9][0-9]*)$/) { return "The format of the 'pages' is wrong" } my @parts = split(/--/, $pages); if (@parts+0 eq 2) { + if ($parts[0] eq $parts[1]) { + return "The 'pages' mentions the same page twice, just use it once" + } if ($parts[0] gt $parts[1]) { return "The 'pages' are in the wrong order" } @@ -268,9 +347,9 @@ sub check_pages { } } -# Check one item. -sub process_item { - my (%item) = @_; +# Check one entry. +sub process_entry { + my (%entry) = @_; my @checks; foreach my $entry (keys %bibcop::) { if ($entry =~ /^check_/) { @@ -281,7 +360,7 @@ sub process_item { my @errors; foreach my $check (@sorted) { no strict 'refs'; - my $err = $check->(%item); + my $err = $check->(%entry); if ($err ne '') { push(@errors, $err); } @@ -290,14 +369,14 @@ sub process_item { } # Parse the incoming .bib file and return an array -# of hash-maps, where each one is a bibitem. -sub bibitems { +# of hash-maps, where each one is a bibentry. +sub entries { my ($bib) = @_; - my @items; + my @entries; my $s = 'top'; - my %item; + my %entry; my $acc = ''; - my $key = ''; + my $tag = ''; my $lineno = 0; my $nest = 0; my $escape = 0; @@ -309,56 +388,56 @@ sub bibitems { # ignore the EOL $lineno = $lineno + 1; } elsif ($char eq '@' and $s eq 'top') { - %item = (); + %entry = (); $s = 'start'; $acc = ''; } elsif ($char =~ /[a-z]/ and $s eq 'start') { # @article } elsif ($char eq '{' and $s eq 'start') { - $item{':type'} = substr($acc, 1); + $entry{':type'} = substr($acc, 1); $acc = ''; $s = 'body'; } elsif ($char =~ /[a-zA-Z]/ and $s eq 'body') { $acc = ''; - $s = 'key'; - } elsif ($char =~ /[a-zA-Z0-9_]/ and $s eq 'key') { - # reading the key + $s = 'tag'; + } elsif ($char =~ /[a-zA-Z0-9_]/ and $s eq 'tag') { + # reading the tag } elsif ($char =~ /[a-zA-Z0-9]/ and $s eq 'value') { # reading the value without quotes or brackets - } elsif ($char eq ',' and $s eq 'key') { - $item{':name'} = $acc; + } elsif ($char eq ',' and $s eq 'tag') { + $entry{':name'} = $acc; $s = 'body'; - } elsif ($char eq '=' and $s eq 'key') { - $key = $acc; + } elsif ($char eq '=' and $s eq 'tag') { + $tag = $acc; $s = 'value'; $acc = ''; } elsif ($char eq ',' and $s eq 'value') { - if (not exists $item{lc($key)}) { + if (not exists $entry{lc($tag)}) { my $tex = substr($acc, 1); $tex =~ s/\s//g; - $item{lc($key)} = $tex; + $entry{lc($tag)} = $tex; } $s = 'body'; } elsif ($char eq '}' and $s eq 'body') { - push(@items, { %item }); + push(@entries, { %entry }); $s = 'top'; } elsif ($char eq '}' and $s eq 'value') { - if (not exists $item{lc($key)}) { + if (not exists $entry{lc($tag)}) { my $tex = substr($acc, 1); $tex =~ s/\s//g; - $item{lc($key)} = $tex; + $entry{lc($tag)} = $tex; } - push(@items, { %item }); + push(@entries, { %entry }); $s = 'top'; - } elsif ($char eq '}' and $s eq 'key') { - $item{':name'} = $acc; - push(@items, { %item }); + } elsif ($char eq '}' and $s eq 'tag') { + $entry{':name'} = $acc; + push(@entries, { %entry }); $s = 'top'; } elsif ($char eq '"' and $s eq 'value') { $s = 'quote'; $acc = ''; } elsif ($char eq '"' and $s eq 'quote') { - $item{lc($key)} = substr($acc, 1); + $entry{lc($tag)} = substr($acc, 1); $s = 'value'; } elsif ($s eq 'quote') { # nothing @@ -374,13 +453,13 @@ sub bibitems { } elsif ($char eq '}' and $escape ne 1) { $nest = $nest - 1; if ($nest eq 0) { - $item{lc($key)} = substr($acc, 1); + $entry{lc($tag)} = substr($acc, 1); $s = 'value'; } } $escape = 0; } else { - print "\\PackageWarningNoLine{bibcop}{It is impossible to parse the .bib file, because I do not know what to do with '$char' at line #$lineno (s=$s)}\n"; + warning("It is impossible to parse the .bib file, because I do not know what to do with '$char' at line #$lineno (s=$s)"); last; } if ($char eq ' ' and not($s =~ /quote|brackets/)) { @@ -388,7 +467,7 @@ sub bibitems { } $acc = $acc . $char; } - return @items; + return @entries; } # Takes the text and returns only list of words seen there. @@ -407,15 +486,15 @@ sub clean_tex { return $tex; } -# Take a bibitem and print all its keys as a comma-separated string. -sub listed_keys { - my (%item) = @_; +# Take a bibentry and print all its tags as a comma-separated string. +sub listed_tags { + my (%entry) = @_; my @list; - foreach my $key (keys %item) { - if ($key =~ /^:.*/) { + foreach my $tag (keys %entry) { + if ($tag =~ /^:.*/) { next; } - push(@list, $key); + push(@list, $tag); } my @sorted = sort @list; return '(' . join(', ', @sorted) . ')'; @@ -424,12 +503,28 @@ sub listed_keys { # Print ERROR message to the console and die. sub error { my ($txt) = @_; - print $txt . "\n"; + if (exists $args{'--latex'}) { + print "\\PackageError{bibcop}{$txt}\n"; + } else { + print $txt . "\n"; + } exit 1; } # Print DEBUG message to the console. sub debug { + my ($txt) = @_; + if (exists $args{'--verbose'}) { + if (exists $args{'--latex'}) { + print "\\message{bibcop: $txt^^J}\n"; + } else { + print $txt . "\n"; + } + } +} + +# Print INFO message to the console. +sub info { my ($txt) = @_; if (exists $args{'--latex'}) { print '% '; @@ -447,57 +542,64 @@ sub warning { } } -if (@ARGV+0 eq 0 or exists $args{'--help'}) { - debug("Bibcop is a Style Checker of .bib Files\n" . - "Usage: bibcop [] <.bib file path>\n" . - " --version Print the current version of the tool and exit\n" . - " --help Print this help screen\n" . - " --fix Fix the errors and print a new version of the .bib file to the console\n" . - " --latex Report errors in LaTeX format using \\PackageWarningNoLine command"); -} elsif (exists $args{'--version'}) { - debug('0.0.3'); +if (@ARGV+0 eq 0 or exists $args{'--help'} or exists $args{'-?'}) { + info("Bibcop is a Style Checker of BibTeX Files\n\n" . + "Usage:\n" . + " bibcop [] <.bib file path>\n\n" . + "Options:\n" . + " -v, --version Print the current version of the tool and exit\n" . + " -?, --help Print this help screen\n" . + " --fix Fix the errors and print a new version of the .bib file to the console\n" . + " --verbose Print supplementary debugging information\n" . + " --latex Report errors in LaTeX format using \\PackageWarningNoLine command\n\n" . + "If any issues, report to GitHub: https://github.com/yegor256/bibcop"); +} elsif (exists $args{'--version'} or exists $args{'-v'}) { + info('0.0.4'); } else { my ($file) = grep { not($_ =~ /^--.*$/) } @ARGV; + if (not $file) { + error('File name must be specified'); + } open(my $fh, '<', $file); my $bib; { local $/; $bib = <$fh>; } - my @items = bibitems($bib); + my @entries = entries($bib); if (exists $args{'--fix'}) { - for my $i (0..(@items+0 - 1)) { - my %item = %{ $items[$i] }; - my $type = $item{':type'}; + for my $i (0..(@entries+0 - 1)) { + my %entry = %{ $entries[$i] }; + my $type = $entry{':type'}; if (not exists $blessed{$type}) { - error("I don't know what to do with \@$type type of bibitem"); + error("I don't know what to do with \@$type type of bibentry"); } - my $keys = $blessed{$item{':type'}}; - my %allowed = map { $_ => 1 } @$keys; + my $tags = $blessed{$entry{':type'}}; + my %allowed = map { $_ => 1 } @$tags; my @lines; - foreach my $key (keys %item) { - if ($key =~ /^:/) { + foreach my $tag (keys %entry) { + if ($tag =~ /^:/) { next; } - if (not exists $allowed{$key} and not exists $allowed{$key . '?'}) { + if (not exists $allowed{$tag} and not exists $allowed{$tag . '?'}) { next; } - my $value = clean_tex($item{$key}); - if ($key =~ /title|booktitle|journal/) { + my $value = clean_tex($entry{$tag}); + if ($tag =~ /title|booktitle|journal/) { $value = '{' . $value . '}'; } - push(@lines, " $key = {$value},"); + push(@lines, " $tag = {$value},"); } - debug("\@$type\{$item{':name'},"); + info("\@$type\{$entry{':name'},"); my @sorted = sort @lines; foreach my $line (@sorted) { - debug($line); + info($line); } - debug("}\n"); + info("}\n"); } } else { - debug((@items+0) . ' bibitems found in ' . $file); - for my $i (0..(@items+0 - 1)) { - my %item = %{ $items[$i] }; - debug("Checking $item{':name'} (#$i)..."); - foreach my $err (process_item(%item)) { - warning("$err, in the '$item{':name'}' bibitem"); + debug((@entries+0) . ' entries found in ' . $file); + for my $i (0..(@entries+0 - 1)) { + my %entry = %{ $entries[$i] }; + debug("Checking $entry{':name'} (no.$i)..."); + foreach my $err (process_entry(%entry)) { + warning("$err, in the '$entry{':name'}' bib entry"); } } } -- cgit v1.2.3