├── .gitignore ├── README.md ├── bin └── draftcheck ├── draftcheck ├── __init__.py ├── rules.py ├── script.py └── validator.py ├── examples └── simple.tex ├── setup.py └── tests └── test_rules.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.pyc 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | draftcheck 2 | ========== 3 | 4 | `draftcheck` is an LaTeX linter that is specifically designed for academic writing. 5 | 6 | Installation 7 | ------------ 8 | 9 | `draftcheck` can be installed using pip: 10 | 11 | ```bash 12 | pip install draftcheck 13 | ``` 14 | 15 | You can also install it from source: 16 | 17 | ```bash 18 | mkdir draftcheck && cd draftcheck 19 | git clone https://github.com/ebnn/draftcheck.git 20 | python setup.py install 21 | ``` 22 | 23 | Usage 24 | ----- 25 | 26 | The supplied files contains several example LaTeX files that can be used to test it. 27 | 28 | ``` 29 | $ draftcheck examples/simple.tex 30 | examples/simple.tex:26:1: (http://www.comp.leeds.ac.uk/andyr/misc/latex/\-latextut... 31 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 32 | [030] Wrap URLs with the \url command. 33 | 34 | examples/simple.tex:31:37: ...thin \LaTeX\cite{lamport94}... 35 | ^^^^^^^ 36 | [004] Place a single, non-breaking space '~' before citations. 37 | 38 | examples/simple.tex:49:10: ...%Set up an 'itemize' environmen... 39 | ^^^^^^^^^^^ 40 | [014] Use left and right quotation marks ` and ' rather than '. 41 | 42 | examples/simple.tex:93:0: \begin{center} 43 | ^^^^^^^^^^^^^^ 44 | [015] Use \centering instead of \begin{center}. 45 | 46 | examples/simple.tex:123:29: ...the number '9'. This is b... 47 | ^^^^^ 48 | [014] Use left and right quotation marks ` and ' rather than '. 49 | 50 | 51 | Total of 5 mistakes found. 52 | ``` -------------------------------------------------------------------------------- /bin/draftcheck: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | 6 | from draftcheck import script 7 | 8 | if __name__ == "__main__": 9 | sys.exit(script.main()) -------------------------------------------------------------------------------- /draftcheck/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ebnn/draftcheck/ae9122af5781a16bc75b04447c3b725dc8ec3816/draftcheck/__init__.py -------------------------------------------------------------------------------- /draftcheck/rules.py: -------------------------------------------------------------------------------- 1 | """This module contains rule definitions.""" 2 | 3 | import re 4 | 5 | # Global rules list to store all the registered rules 6 | RULES_LIST = [] 7 | 8 | 9 | def rule(pattern, show_spaces=False, in_env='paragraph'): 10 | """Decorator used to create rules. 11 | 12 | The decorated function must have the following signature: 13 | def example_rule(text, matches): 14 | return ... 15 | 16 | where `text` is the string that needs to be checked, and matches is the 17 | result of calling `re.finditer(pattern, text)`, i.e. the `MatchObject`s 18 | representing substrings that match the specified regex pattern. 19 | 20 | The decorated function must return a list of tuple pairs, where each tuple 21 | pair (start, end) represents the start and end indices of substrings in 22 | `text` that violate the rule. 23 | 24 | Parameters 25 | ---------- 26 | pattern : string 27 | If specified, pattern is treated as a regular expression and the result 28 | of calling `finditer` on the text being checked is passed to the wrapped 29 | function. 30 | show_spaces : boolean, optional 31 | Whether the output should replace whitespace with underscores 32 | (in order to clearly indicate errors involving whitespace). Defaults to 33 | false. 34 | in_env : string, optional 35 | The LaTeX environment in which this rule can be applied. Only text in 36 | the specified environment are checked against this rule. This may be 37 | set to 'any' if this rule applies in any environment. Defaults to 38 | 'paragraph'. 39 | """ 40 | regexpr = re.compile(pattern) 41 | 42 | def inner_rule(func): 43 | def wrapper(text, env): 44 | if in_env == 'any' or env == in_env: 45 | return func(text, regexpr.finditer(text)) 46 | return [] 47 | 48 | # Store the parameters in the function as attributes 49 | wrapper.id = len(RULES_LIST) + 1 50 | wrapper.show_spaces = show_spaces 51 | wrapper.in_env = in_env 52 | 53 | # Inherit the docstring from the function 54 | wrapper.__doc__ = func.__doc__ 55 | 56 | # Add it to our global rules list 57 | RULES_LIST.append(wrapper) 58 | 59 | return wrapper 60 | return inner_rule 61 | 62 | 63 | def rule_generator(show_spaces=False, in_env='paragraph'): 64 | """Decorator that generates rules from a generator.""" 65 | def inner_rule(func): 66 | for r in func(): 67 | # Register this rule into our global rules list 68 | @rule(pattern=r[0], show_spaces=show_spaces, in_env=in_env) 69 | def generated_rule(_, matches): 70 | return [m.span() for m in matches] 71 | 72 | # Format the docstring with parameters specific to this instance 73 | # of the rule 74 | RULES_LIST[-1].__doc__ = func.__doc__.format(*r[1:]) 75 | return inner_rule 76 | 77 | 78 | @rule(r'\s+\\footnote{', show_spaces=True) 79 | def check_space_before_footnote(text, matches): 80 | """Do not precede footnotes with spaces. 81 | 82 | Remove the extraneous spaces before the \\footnote command. 83 | 84 | Examples 85 | -------- 86 | Bad: 87 | Napolean's armies were defeated in Waterloo \\footnote{In present day 88 | Belgium}. 89 | 90 | Good: 91 | Napolean's armies were defeated in Waterloo\\footnote{In present day 92 | Belgium}. 93 | """ 94 | return [m.span() for m in matches] 95 | 96 | 97 | @rule(r'\.\\cite{') 98 | def check_cite_after_period(text, matches): 99 | """Place citations before periods with a non-breaking space. 100 | 101 | Move the \\cite command inside the sentence, before the period. 102 | 103 | Examples 104 | -------- 105 | Bad: 106 | Johannes Brahms was born in Hamburg.\\cite{} 107 | 108 | Good: 109 | Johannes Brahms was born in Hamburg~\\cite{}. 110 | """ 111 | return [m.span() for m in matches] 112 | 113 | 114 | @rule(r'\b(:?in|as|on|by)[ ~]\\cite{') 115 | def check_cite_used_as_noun(text, matches): 116 | """Avoid using citations as nouns. 117 | 118 | Examples 119 | -------- 120 | Bad: 121 | The method proposed in~\\cite{} shows a decrease in methanol toxicity. 122 | 123 | Good: 124 | A proposed method shows a decrease in methanol toxicity~\\cite{}. 125 | """ 126 | return [m.span() for m in matches] 127 | 128 | 129 | @rule(r'[^~]\\cite{') 130 | def check_no_space_before_cite(text, matches): 131 | """Place a single, non-breaking space '~' before citations. 132 | 133 | Examples 134 | -------- 135 | Bad: 136 | Apollo 17's ``The Blue Marble'' \\cite{} photo of the Earth became an 137 | icon of the environmental movement. 138 | 139 | Good: 140 | Apollo 17's ``The Blue Marble''~\\cite{} photo of the Earth became an 141 | icon of the environmental movement. 142 | """ 143 | return [m.span() for m in matches] 144 | 145 | 146 | @rule(r'[^~]\\ref{') 147 | def check_no_space_before_ref(text, matches): 148 | """Place a single, non-breaking space '~' before references. 149 | 150 | Examples 151 | -------- 152 | Bad: 153 | The performance of the engine is shown in Figure \\ref{}. 154 | 155 | Good: 156 | The performance of the engine is shown in Figure~\\ref{}. 157 | """ 158 | return [m.span() for m in matches] 159 | 160 | 161 | @rule(r'\d+%') 162 | def check_unescaped_percentage(text, matches): 163 | """Escape percentages with backslash. 164 | 165 | Examples 166 | -------- 167 | Bad: 168 | The company's stocks rose by 15%. 169 | 170 | Good: 171 | The company's stocks rose by 15\\%. 172 | """ 173 | return [m.span() for m in matches] 174 | 175 | 176 | @rule(r'\s[,;.!?]', show_spaces=True) 177 | def check_space_before_punctuation(text, matches): 178 | """Do not precede punctuation characters with spaces. 179 | 180 | Example 181 | ------- 182 | Bad: 183 | Nether Stowey, where Coleridge wrote The Rime of the Ancient Mariner , 184 | is a few miles from Bridgewater. 185 | 186 | Good: 187 | Nether Stowey, where Coleridge wrote The Rime of the Ancient Mariner, 188 | is a few miles from Bridgewater. 189 | """ 190 | return [m.span() for m in matches] 191 | 192 | 193 | @rule(r'\w+\(|\)\w+', show_spaces=True) 194 | def check_no_space_next_to_parentheses(text, matches): 195 | """Separate parentheses from text with a space. 196 | 197 | Example 198 | ------- 199 | Bad: 200 | Pablo Picasso(1881--1973) is one of the pioneers of Cubism. 201 | 202 | Good: 203 | Pablo Picasso (1881--1973) is one of the pioneers of Cubism. 204 | """ 205 | return [m.span() for m in matches] 206 | 207 | 208 | @rule(r'\d+\s?x\d+') 209 | def check_incorrect_usage_of_x_as_times(text, matches): 210 | """In the context of 'times' or 'multiply', use $\\times$ instead of 'x'. 211 | 212 | Example 213 | ------- 214 | Bad: 215 | We used an 10x10 grid for the image filter. 216 | 217 | Good: 218 | We used an $10 \\times 10$ grid for the image filter. 219 | """ 220 | return [m.span() for m in matches] 221 | 222 | 223 | @rule('[a-z]+\s-\s[a-z]+') 224 | def check_space_surrounded_dash(text, matches): 225 | """Use an em-dash '---' to denote parenthetical breaks or statements. 226 | 227 | Example 228 | ------- 229 | Bad: 230 | He only desired one thing - success. 231 | 232 | Good: 233 | He only desired one thing --- success. 234 | """ 235 | return [m.span() for m in matches] 236 | 237 | 238 | @rule(r'\b([a-z]+)\s+\1\b(?![^{]*})') 239 | def check_duplicate_word(text, matches): 240 | """Remove duplicated word. 241 | 242 | Example 243 | ------- 244 | Bad: 245 | The famous two masks associated with drama are symbols of the 246 | the ancient Muses, Thalia (comedy) and Melpomene (tragedy). 247 | 248 | Good: 249 | The famous two masks associated with drama are symbols of the 250 | ancient Muses, Thalia (comedy) and Melpomene (tragedy). 251 | """ 252 | return [m.span() for m in matches] 253 | 254 | 255 | @rule(r'\.\.\.') 256 | def check_dot_dot_dot(text, matches): 257 | """Typeset ellipses by \\ldots, not '...'. 258 | 259 | Example 260 | ------- 261 | Bad: 262 | New York, Tokyo, Budapest, ... 263 | 264 | Good: 265 | New York, Tokyo, Budapest, \\ldots 266 | """ 267 | return [m.span() for m in matches] 268 | 269 | 270 | @rule(r'"') 271 | def check_double_quote(text, matches): 272 | """Use left and right quotation marks `` and '' rather than ". 273 | 274 | Example 275 | ------- 276 | Bad: 277 | "Very much indeed," Alice said politely. 278 | 279 | Good: 280 | ``Very much indeed,'' Alice said politely. 281 | """ 282 | return [m.span() for m in matches] 283 | 284 | 285 | @rule(r'\s\'.+?\'[\s\.,]') 286 | def check_single_quote(text, matches): 287 | """Use left and right quotation marks ` and ' rather than '. 288 | 289 | Example 290 | ------- 291 | Bad: 292 | It is 'too good to be true'. 293 | 294 | Good: 295 | It is `too good to be true'. 296 | """ 297 | return [m.span() for m in matches] 298 | 299 | 300 | @rule(r'\\begin{center}', in_env='any') 301 | def check_begin_center(text, matches): 302 | """Use \\centering instead of \\begin{center}. 303 | 304 | Example 305 | ------- 306 | Bad: 307 | \\begin{figure} 308 | \\begin{center} 309 | \\includegraphics 310 | \\end{center} 311 | \\end{figure} 312 | 313 | Good: 314 | \\begin{figure} 315 | \\centering 316 | \\includegraphics 317 | \\end{figure} 318 | """ 319 | return [m.span() for m in matches] 320 | 321 | 322 | @rule(r'^\$\$', in_env='math') 323 | def check_double_dollar_math(text, matches): 324 | """Use \[ or \\begin{equation} instead of $$. 325 | 326 | Example 327 | ------- 328 | Bad: 329 | $$ 1 + 1 = 2 $$ 330 | 331 | Good: 332 | \\[ 1 + 1 = 2 \\] 333 | 334 | Good: 335 | \\begin{equation} 336 | 1 + 1 = 2 337 | \\end{equation} 338 | """ 339 | return [m.span() for m in matches] 340 | 341 | 342 | @rule(r'\d\s?-\s?\d') 343 | def check_numeric_range_dash(text, matches): 344 | """Use endash '--' for numeric ranges instead of hyphens. 345 | 346 | Example 347 | ------- 348 | Bad: 349 | A description of medical practices at the time are on pages 17-20. 350 | 351 | Good: 352 | A description of medical practices at the time are on pages 17--20. 353 | """ 354 | return [m.span() for m in matches] 355 | 356 | 357 | @rule(r'\\footnote{.+?}[,;.?]') 358 | def check_footnote_before_punctuation(text, matches): 359 | """Place footnotes after punctuation marks. 360 | 361 | Example 362 | ------- 363 | Bad: 364 | \emph{Waiting for Godot}\\footnote{First performed on 5 January 1953 in 365 | Paris}, written by Samuel Beckett, is an example of Absurdist Theatre. 366 | 367 | Good: 368 | \emph{Waiting for Godot},\\footnote{First performed on 5 January 1953 in 369 | Paris} written by Samuel Beckett, is an example of Absurdist Theatre. 370 | """ 371 | return [m.span() for m in matches] 372 | 373 | 374 | @rule(r'<[^\s](.+?)[^\s]>', in_env='math') 375 | def check_relational_operators(text, matches): 376 | """Use \\langle and \\rangle instead of '<' and '>' for angle brackets. 377 | 378 | Example 379 | ------- 380 | Bad: 381 | Inner product of $a$ and $b$ is denoted by $$. 382 | 383 | Good: 384 | Inner product of $a$ and $b$ is denoted by $\\langle a, b \\rangle$. 385 | 386 | Good: 387 | It must satisfy this inequality: $a < b, c > d$. 388 | """ 389 | return [m.span() for m in matches] 390 | 391 | 392 | @rule(r'\\cite{.+?}\s?\\cite{') 393 | def check_multiple_cite(text, matches): 394 | """Use \\cite{..., ...} for multiple citations. 395 | 396 | Example 397 | ------- 398 | Bad: 399 | This problem has many real-world applications~\\cite{A}\\cite{B}. 400 | 401 | Good: 402 | This problem has many real-world applications~\\cite{A, B}. 403 | """ 404 | return [m.span() for m in matches] 405 | 406 | 407 | @rule(r'\d(m|A|kg|s|K|mol|cd)\b') 408 | def check_number_next_to_unit(text, matches): 409 | """Place a non-breaking space between a number and its unit. 410 | 411 | Example 412 | ------- 413 | Bad: 414 | We measured the distance travelled by the ball to be 14.5m. 415 | 416 | Good: 417 | We measured the distance travelled by the ball to be 14.5~m. 418 | """ 419 | return [m.span() for m in matches] 420 | 421 | 422 | @rule(r'[^\\](sin|cos|tan|log|max|min)', in_env='math') 423 | def check_unescaped_named_math_operators(text, matches): 424 | """Precede named mathematical operators with a backslash. 425 | 426 | Example 427 | ------- 428 | Bad: 429 | The famous trignometric identity: $sin^2(x) + cos^2(x) = 1$. 430 | 431 | Good: 432 | The famous trignometric identity: $\\sin^2(x) + \\cos^2(x) = 1$. 433 | """ 434 | return [m.span() for m in matches] 435 | 436 | 437 | @rule(r'\b(e\.g\.|i\.e\.)\s+') 438 | def check_abbreviation_innerword_spacing(text, matches): 439 | """Place a '\\ ' (backslash space) after the period of an abbreviation. 440 | 441 | Example 442 | ------- 443 | Bad: 444 | This shows that new technological gadgets, e.g. smart phones, decrease 445 | the attention span of young adults. 446 | 447 | Good: 448 | This shows that new technological gadgets, e.g.\\\\ smart phones, 449 | decrease the attention span of young adults. 450 | """ 451 | return [m.span() for m in matches] 452 | 453 | 454 | @rule(r'\\def\\[a-z]+{') 455 | def check_def_command(text, matches): 456 | """Do not use the \\def command. Use \\newcommand instead.""" 457 | return [m.span() for m in matches] 458 | 459 | 460 | @rule(r'\\sloppy') 461 | def check_sloppy_command(text, matches): 462 | """Avoid the \\sloppy command.""" 463 | return [m.span() for m in matches] 464 | 465 | 466 | @rule(r"'''|```") 467 | def check_triple_quote(text, matches): 468 | """Use a thin space \, to separate quotes.""" 469 | return [m.span() for m in matches] 470 | 471 | 472 | @rule(r'1st|2nd|3rd') 473 | def check_unspelt_ordinal_numbers(text, matches): 474 | """Spell out ordinal numbers (1st, 2nd, etc.) in words.""" 475 | return [m.span() for m in matches] 476 | 477 | 478 | @rule(r'[a-z]+ \d [a-z]+') 479 | def check_unspelt_single_digit_numbers(text, matches): 480 | """Spell out single digit numbers in words.""" 481 | return [m.span() for m in matches] 482 | 483 | 484 | @rule(r',\s*\.\.\.\s*,', in_env='math') 485 | def check_dot_dot_dot_maths(text, matches): 486 | """Use \\cdots to denote ellipsis in maths.""" 487 | return [m.span() for m in matches] 488 | 489 | 490 | @rule(r'(? 0: 61 | print '\nTotal of {0} mistakes found.'.format(num_errors) 62 | return 1 63 | else: 64 | print 'No mistakes found.' 65 | return 0 -------------------------------------------------------------------------------- /draftcheck/validator.py: -------------------------------------------------------------------------------- 1 | """This modules contains code to find rule violations in text.""" 2 | 3 | import rules 4 | import itertools 5 | import re 6 | 7 | # Different LaTeX environments 8 | LATEX_ENVS = { 9 | 'math': ['math', 'array', 'eqnarray', 'equation', 'align'], 10 | 'paragraph': ['abstract', 'document', 'titlepage'] 11 | } 12 | 13 | LATEX_ENVS = dict((k, env) for env in LATEX_ENVS for k in LATEX_ENVS[env]) 14 | 15 | 16 | class Validator(object): 17 | # Regular expressions to extract environments 18 | env_begin_regex = re.compile(r'\\begin{(\w+)}') 19 | env_end_regex = re.compile(r'\\end{(\w+)}') 20 | math_env_regex = re.compile(r'((?:\$\$|\$|\\\[).+?(?:\$\$|\$|\\\]))') 21 | 22 | def __init__(self): 23 | # Initialise the environment stack 24 | self._envs = ['paragraph'] 25 | 26 | def validate(self, line): 27 | """Validate a particular line of text. 28 | 29 | This function finds rules that are violated by the text. The validation 30 | is performed in a stateful manner, with past calls to validate possibly 31 | affecting the results of future calls. 32 | 33 | Parameters 34 | ---------- 35 | line : string 36 | The line of text to validate. 37 | 38 | Yields 39 | ------ 40 | rule, span : (rule, (start, end)) 41 | The first element is the rule that is violated. The second element 42 | is the tuple pair representing the start and end indices of the 43 | substring which violates that rule. 44 | """ 45 | # Check if the environment has changed 46 | match = Validator.env_begin_regex.match(line) 47 | if match: 48 | self._envs.append(LATEX_ENVS.get(match.group(1), 'unknown')) 49 | 50 | match = Validator.env_end_regex.match(line) 51 | if match: 52 | self._envs.pop() 53 | 54 | # See if we need to extract inline math expressions 55 | if self._envs[-1] == 'math': 56 | # Because we are already in maths mode, there is no need to detect 57 | # nested math environments. 58 | chunks = ['', line] 59 | else: 60 | # Split the text into chunks of text and inline maths 61 | chunks = Validator.math_env_regex.split(line) 62 | 63 | # The chunks will alternate from text and maths 64 | chunk_envs = itertools.cycle([self._envs[-1], 'math']) 65 | 66 | offset = 0 67 | for chunk, chunk_env in zip(chunks, chunk_envs): 68 | for rule in rules.RULES_LIST: 69 | for span in rule(chunk, chunk_env): 70 | offsetted_span = (span[0] + offset, span[1] + offset) 71 | yield rule, offsetted_span 72 | 73 | offset += len(chunk) 74 | -------------------------------------------------------------------------------- /examples/simple.tex: -------------------------------------------------------------------------------- 1 | % simple.tex - A simple article to illustrate document structure. 2 | 3 | % Andrew Roberts - June 2003 4 | 5 | \documentclass{article} 6 | \usepackage{times} 7 | 8 | \begin{document} 9 | 10 | % Article top matter 11 | \title{How to Structure a \LaTeX{} Document} %\LaTeX is a macro for printing the Latex logo 12 | \author{Andrew Roberts\\ 13 | School of Computing,\\ 14 | University of Leeds,\\ 15 | Leeds,\\ 16 | United Kingdom,\\ 17 | LS2 1HE\\ 18 | \texttt{andyr@comp.leeds.ac.uk}} %\texttt formats the text to a typewriter style font 19 | \date{\today} %\today is replaced with the current date 20 | \maketitle 21 | 22 | \begin{abstract} 23 | In this article, I shall discuss some of the fundamental topics in 24 | producing a structured document. This document itself does not go into 25 | much depth, but is instead the output of an example of how to implement 26 | structure. Its \LaTeX{} source, when in used with my tutorial 27 | (http://www.comp.leeds.ac.uk/andyr/misc/latex/\-latextutorial2.html) 28 | provides all the relevant information. \end{abstract} 29 | 30 | \section{Introduction} 31 | This small document is designed to illustrate how easy it is to create a 32 | well structured document within \LaTeX\cite{lamport94}. You should quickly be able to 33 | see how the article looks very professional, despite the content being 34 | far from academic. Titles, section headings, justified text, text 35 | formatting etc., is all there, and you would be surprised when you see 36 | just how little markup was required to get this output. 37 | 38 | \section{Structure} 39 | One of the great advantages of \LaTeX{} is that all it needs to know is 40 | the structure of a document, and then it will take care of the layout 41 | and presentation itself. So, here we shall begin looking at how exactly 42 | you tell \LaTeX{} what it needs to know about your document. 43 | 44 | \subsection{Top Matter} 45 | The first thing you normally have is a title of the document, as well as 46 | information about the author and date of publication. In \LaTeX{} terms, 47 | this is all generally referred to as \emph{top matter}. 48 | 49 | \subsubsection{Article Information} 50 | %Set up an 'itemize' environment to start a bulleted list. Each 51 | %individual item begins with the \item command. Also note in this list 52 | %that it has two levels, with a list embedded in one of the list items. 53 | \begin{itemize} 54 | \item \texttt{\textbackslash title\{\emph{title}\}} - The title of the article. 55 | \item \texttt{\textbackslash date} - The date. Use: 56 | \begin{itemize} 57 | \item \texttt{\textbackslash date\{\textbackslash today\}} - to get the 58 | date that the document is typeset. 59 | \item \texttt{\textbackslash date\{\emph{date}\}} - for a %\emph{} emphasises the specified text. Italics by default. 60 | specific date. 61 | \item \texttt{\textbackslash date\{\}} - for no date. 62 | \end{itemize} 63 | \end{itemize} 64 | 65 | \subsubsection{Author Information} 66 | The basic article class only provides the one command: 67 | \begin{itemize} 68 | \item \texttt{\textbackslash author} - The author of the document. 69 | \end{itemize} 70 | 71 | It is common to not only include the author name, but to insert new 72 | lines (\texttt{\textbackslash\textbackslash}) after and add things such 73 | as address and email details. For a slightly more logical approach, use 74 | the AMS article class (\texttt{amsart}) and you have the following extra 75 | commands: 76 | 77 | \begin{itemize} 78 | \item \texttt{\textbackslash address} - The author's address. Use 79 | the new line command (\texttt{\textbackslash\textbackslash}) for 80 | line breaks. 81 | \item \texttt{\textbackslash thanks} - Where you put any acknowledgments. 82 | \item \texttt{\textbackslash email} - The author's email address. 83 | \item \texttt{\textbackslash urladdr} - The URL for the author's web page. 84 | \end{itemize} 85 | 86 | \subsection{Sectioning Commands} 87 | The commands for inserting sections are fairly intuitive. Of course, 88 | certain commands are appropriate to different document classes. 89 | For example, a book has chapters but a article doesn't. 90 | 91 | %A simple table. The center environment is first set up, otherwise the 92 | %table is left aligned. The tabular environment is what tells Latex 93 | %that the data within is data for the table. 94 | \begin{center} 95 | \begin{tabular}{| l | l |} 96 | %The tabular environment is what tells Latex that the data within is 97 | %data for the table. The arguments say that there will be two 98 | %columns, both left justified (indicated by the 'l', you could also 99 | %have 'c' or 'r'. The bars '|' indicate vertical lines throughout 100 | %the table. 101 | 102 | \hline % Print horizontal line 103 | Command & Level \\ \hline % Columns are delimited by '&'. And 104 | %rows are delimited by '\\' 105 | \texttt{\textbackslash part\{\emph{part}\}} & -1 \\ 106 | \texttt{\textbackslash chapter\{\emph{chapter}\}} & 0 \\ 107 | \texttt{\textbackslash section\{\emph{section}\}} & 1 \\ 108 | \texttt{\textbackslash subsection\{\emph{subsection}\}} & 2 \\ 109 | \texttt{\textbackslash subsubsection\{\emph{subsubsection}\}} & 3 \\ 110 | \texttt{\textbackslash paragraph\{\emph{paragraph}\}} & 4 \\ 111 | \texttt{\textbackslash subparagraph\{\emph{subparagraph}\}} & 5 \\ 112 | \hline 113 | \end{tabular} 114 | \end{center} 115 | 116 | Numbering of the sections is performed automatically by \LaTeX{}, so don't 117 | bother adding them explicitly, just insert the heading you want between 118 | the curly braces. If you don't want sections number, then add an asterisk (*) after the 119 | section command, but before the first curly brace, e.g., \texttt{\textbackslash 120 | section*\{A Title Without Numbers\}}. 121 | 122 | %Create the environment for the bibliography. Since there is only one 123 | %reference, set the label width to be one character (I shall follow 124 | %convention as use the number '9'. This is because it helps to remind 125 | %that it is the maximum number of refs that is now permitted by that 126 | %width). 127 | \begin{thebibliography}{9} 128 | %The \bibitem is to start a new reference. Ensure that the cite_key is 129 | %unique. You don't need to put each element on a new line, but I did 130 | %simply for readability. 131 | \bibitem{lamport94} 132 | Leslie Lamport, 133 | \emph{\LaTeX: A Document Preparation System}. 134 | Addison Wesley, Massachusetts, 135 | 2nd Edition, 136 | 1994. 137 | 138 | \end{thebibliography} %Must end the environment 139 | 140 | \end{document} %End of document. 141 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | setup_args = { 4 | 'name': 'draftcheck', 5 | 'version': '0.1', 6 | 'description': 'LaTeX Lint for Academic Writing', 7 | 'packages': ['draftcheck'], 8 | 'zip_safe': True, 9 | } 10 | 11 | try: 12 | from setuptools import setup 13 | setup_args['entry_points'] = { 14 | 'console_scripts': ['draftcheck = draftcheck.script:main'] 15 | } 16 | 17 | except ImportError: 18 | from distutils.core import setup 19 | setup_args['scripts'] = ['bin/draftcheck'] 20 | 21 | setup(**setup_args) -------------------------------------------------------------------------------- /tests/test_rules.py: -------------------------------------------------------------------------------- 1 | from nose.tools import assert_equals 2 | from draftcheck.validator import Validator 3 | import draftcheck.rules as rules 4 | 5 | 6 | def found_error(rule, text): 7 | """Return whether a particular rule has been violated.""" 8 | for r, _ in Validator().validate(text): 9 | if r.id == rule.id: 10 | print r.__doc__ 11 | return True 12 | return False 13 | 14 | 15 | def normalise_text(text): 16 | """Replace newlines and surrounding whitespace with a single space.""" 17 | return ' '.join(map(lambda x: x.lstrip(), text.split('\n'))) 18 | 19 | 20 | def test_examples(): 21 | """A test generator that creates tests from examples in rule docstrings.""" 22 | import re 23 | 24 | example_regex = re.compile(r'(Good|Bad):\n(.+?)(?:\n\n|\s*$)', flags=re.S) 25 | for rule in rules.RULES_LIST: 26 | for match in example_regex.finditer(rule.__doc__): 27 | expected = False if match.group(1) == 'Good' else True 28 | text = normalise_text(match.group(2)) 29 | 30 | yield assert_equals, found_error(rule, text), expected 31 | --------------------------------------------------------------------------------