/ftplugin/ATP_files/latex_log.py
Python | 528 lines | 461 code | 14 blank | 53 comment | 9 complexity | 27a7d8d41586205bed520132eb1959e1 MD5 | raw file
1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3 4# Usage: latex_log.py {tex_log_file} 5# Produces a "._log" file. 6 7# Author: Marcin Szamotulski 8# http://atp-vim.sourceforge.net 9# 10# Copyright Statement: 11# This file is a part of Automatic Tex Plugin for Vim. 12# 13# Automatic Tex Plugin for Vim is free software: you can redistribute it 14# and/or modify it under the terms of the GNU General Public License as 15# published by the Free Software Foundation, either version 3 of the 16# License, or (at your option) any later version. 17# 18# Automatic Tex Plugin for Vim is distributed in the hope that it will be 19# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 20# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 21# General Public License for more details. 22# 23# You should have received a copy of the GNU General Public License along 24# with Automatic Tex Plugin for Vim. If not, see <http://www.gnu.org/licenses/>. 25 26# INFO: 27# This is a python script which reads latex log file (which path is gven as 28# the only argument) and it writes back a log messages which are in the 29# following format: 30# WARNING_TYPE::FILE::INPUT_LINE::INPUT_COL::MESSAGE (ADDITIONAL_INFO) 31# this was intendent to be used for vim quick fix: 32# set errorformat=LaTeX\ %tarning::%f::%l::%c::%m,Citation\ %tarning::%f::%l::%c::%m,Reference\ %tarning::%f::%l::%c::%m,Package\ %tarning::%f::%l::%c::%m,hbox\ %tarning::%f::%l::%c::%m,LaTeX\ %tnfo::%f::%l::%c::%m,LaTeX\ %trror::%f::%l::%c::%m 33# 34# The fowllowing WARNING_TYPEs are available: 35# LaTeX Warning 36# Citation Warning 37# Reference Warning 38# Package Warning 39# hbox Warning : Overfull and Underfull hbox warnings 40# LaTeX Font Warning 41# LaTeX Font Info 42# LaTeX Info 43# LaTeX Error 44# Input File 45# Input Package : includes packges and document class 46 47# Note: when FILE,INPUT_LINE,INPUT_COL doesn't exists 0 is put. 48 49# It will work well when the tex file was compiled with a big value of 50# max_print_line (for example with `max_print_line=2000 latex file.tex') 51# so that latex messages are not broken into lines. 52 53# The scripts assumes the default encoding to be utf-8. Though you will not see 54# errors since decode(errors='replace') is used, that is bytes not recognized 55# will be substituted with '?'. 56 57import sys, re, os, os.path, fnmatch 58from optparse import OptionParser 59 60__all__ = [ 'rewrite_log' ] 61 62class Dict(dict): 63 """ 2to3 Python transition. """ 64 def iterkeys(self): 65 if sys.version_info < (3,0): 66 return super(type(self), self).iterkeys() 67 else: 68 return self.keys() 69 70 def iteritems(self): 71 if sys.version_info < (3,0): 72 return super(type(self), self).iteritems() 73 else: 74 return self.items() 75 76 def itervalues(self): 77 if sys.version_info < (3,0): 78 return super(type(self), self).itervalues() 79 else: 80 return self.values() 81 82def shift_dict( dictionary, nr ): 83 ''' 84 Add nr to every value of dictionary. 85 ''' 86 for key in dictionary.iterkeys(): 87 dictionary[key]+=nr 88 return dictionary 89 90if sys.platform.startswith('linux'): 91 log_to_path = "/tmp/latex_log.log" 92else: 93 log_to_path = None 94 95def rewrite_log(input_fname, output_fname=None, check_path=False, project_dir="", project_tmpdir="", encoding="utf8"): 96 # this function rewrites LaTeX log file (input_fname) to output_fname, 97 # changeing its format to something readable by Vim. 98 # check_path -- ATP process files in a temporary directory, with this 99 # option the files under project_tmpdir will be written using project_dir 100 # (this is for the aux file). 101 102 if output_fname is None: 103 output_fname = os.path.splitext(input_fname)[0]+"._log" 104 105 try: 106 if sys.version_info < (3, 0): 107 log_file = open(input_fname, 'r') 108 else: 109 # We are assuming the default encoding (utf-8) 110 log_file = open(input_fname, 'r', errors='replace') 111 except IOError: 112 print("IOError: cannot open %s file for reading" % input_fname) 113 sys.exit(1) 114 else: 115 log_stream = log_file.read().decode(encoding, 'ignore') 116 log_file.close() 117 # Todo: In python3 there is UnicodeDecodeError. I should remove all the 118 # bytes where python cannot decode the character. 119 120 dir = os.path.dirname(os.path.abspath(input_fname)) 121 os.chdir(dir) 122 123 # Filter the log_stream: remove all unbalanced brackets (:) 124 # some times the log file contains unbalanced brackets! 125 # This removes all the lines after 'Overfull \hbox' message until first non 126 # empty line and all lines just after 'Runaway argument?'. 127 log_lines = log_stream.split("\n") 128 output_lines = [] 129 idx = 0 130 remove = False 131 prev_line = "" 132 overfull = False 133 runawayarg = False 134 for line in log_lines: 135 idx+=1 136 match_overfull = re.match(r'(Over|Under)full \\hbox ',line) 137 match_runawayarg = re.match('Runaway argument\?',prev_line) 138 if match_overfull or match_runawayarg: 139 if match_overfull: 140 overfull = True 141 if match_runawayarg: 142 runawayarg = True 143 remove = True 144 elif re.match('^\s*$', line) and overfull: 145 remove = False 146 overfull = False 147 elif runawayarg: 148 remove = False 149 runawayarg = False 150 if not remove or match_overfull: 151 output_lines.append(line) 152 prev_line = line 153 log_stream='\n'.join(output_lines) 154 del output_lines 155 output_data = [] 156 log_lines = log_stream.split("\n") 157 global log_to_path 158 if log_to_path: 159 try: 160 log_fo=open(log_to_path, 'w') 161 except IOError: 162 print("IOError: cannot open %s file for writting" % log_to_path) 163 else: 164 log_fo.write(log_stream.encode(encoding, 'ignore')) 165 log_fo.close() 166 167 # File stack 168 file_stack = [] 169 170 line_nr = 1 171 col_nr = 1 172 173 # Message Patterns: 174 latex_warning_pat = re.compile('(LaTeX Warning: )') 175 latex_warning= "LaTeX Warning" 176 177 font_warning_pat = re.compile('LaTeX Font Warning: ') 178 font_warning = "LaTeX Font Warning" 179 180 font_info_pat = re.compile('LaTeX Font Info: ') 181 font_info = "LaTeX Font Info" 182 183 package_warning_pat = re.compile('Package (\w+) Warning: ') 184 package_warning = "Package Warning" 185 186 package_info_pat = re.compile('Package (\w+) Info: ') 187 package_info = "Package Info" 188 189 hbox_info_pat = re.compile('(Over|Under)full \\\\hbox ') 190 hbox_info = "hbox Warning" 191 192 latex_info_pat = re.compile('LaTeX Info: ') 193 latex_info = "LaTeX Info" 194 195 latex_emergency_stop_pat = re.compile('\! Emergency stop\.') 196 latex_emergency_stop = "LaTeX Error" 197 198 latex_error_pat = re.compile('\! (?:LaTeX Error: |Package (\w+) Error: )?') 199 latex_error = "LaTeX Error" 200 201 input_package_pat = re.compile('(?:Package: |Document Class: )') 202 input_package = 'Input Package' 203 204 open_dict = Dict({}) 205 # This dictionary is of the form: 206 # { file_name : number_of_brackets_opened_after_the_file_name_was_found ... } 207 208 idx=-1 209 line_up_to_col = "" 210 # This variable stores the current line up to the current column. 211 for char in log_stream: 212 idx+=1 213 if char == "\n": 214 line_nr+=1 215 col_nr=0 216 line_up_to_col = "" 217 else: 218 col_nr+=1 219 line_up_to_col += char 220 if char == "(" and not re.match('l\.\d+', line_up_to_col): 221 # If we are at the '(' bracket, check for the file name just after it. 222 line = log_lines[line_nr-1][col_nr:] 223 fname_re = re.match('([^\(\)]*\.(?:tex|sty|cls|cfg|def|aux|fd|out|bbl|blg|bcf|lof|toc|lot|ind|idx|thm|synctex\.gz|pdfsync|clo|lbx|mkii|run\.xml|spl|snm|nav|brf|mpx|ilg|maf|glo|mtc[0-9]+))', line) 224 if fname_re: 225 fname = os.path.abspath(fname_re.group(1)) 226 if check_path and fnmatch.fnmatch(fname, project_tmpdir+"*"): 227 # ATP specific path rewritting: 228 fname = os.path.normpath(os.path.join(project_dir, os.path.relpath(fname, project_tmpdir))) 229 output_data.append(["Input File", fname, "0", "0", "Input File"]) 230 file_stack.append(fname) 231 open_dict[fname]=0 232 open_dict = shift_dict(open_dict, 1) 233 elif char == ")" and not re.match('l\.\d+', line_up_to_col): 234 if len(file_stack) and not( re.match('\!', log_lines[line_nr-1]) or re.match('\s{5,}',log_lines[line_nr-1]) or re.match('l\.\d+', log_lines[line_nr-1])): 235 # If the ')' is in line that we check, then substrackt 1 from values of 236 # open_dict and pop both the open_dict and the file_stack. 237 open_dict = shift_dict(open_dict, -1) 238 if open_dict[file_stack[-1]] == 0: 239 open_dict.pop(file_stack[-1]) 240 file_stack.pop() 241 242 line = log_lines[line_nr-1][col_nr:] 243 if col_nr == 0: 244 # Check for the error message in the current line 245 # (that's why we only check it when col_nr == 0) 246 try: 247 last_file = file_stack[-1] 248 except IndexError: 249 last_file = "0" 250 if check_path and fnmatch.fnmatch(last_file, project_tmpdir+"*"): 251 # ATP specific path rewritting: 252 last_file = os.path.normpath(os.path.join(project_dir, os.path.relpath(last_file, project_tmpdir))) 253 if re.match(latex_warning_pat, line): 254 # Log Message: 'LaTeX Warning: ' 255 input_line = re.search('on input line (\d+)', line) 256 warning_type = re.match('LaTeX Warning: (Citation|Reference)', line) 257 if warning_type: 258 wtype = warning_type.group(1) 259 else: 260 wtype = "" 261 msg = re.sub('\s+on input line (\d+)', '', re.sub(latex_warning_pat,'', line)) 262 if msg == "": 263 msg = " " 264 if input_line: 265 output_data.append([wtype+" "+latex_warning, last_file, input_line.group(1), "0", msg]) 266 else: 267 output_data.append([latex_warning, last_file, "0", "0", msg]) 268 elif re.match(font_warning_pat, line): 269 # Log Message: 'LaTeX Font Warning: ' 270 input_line = re.search('on input line (\d+)', line) 271 if not input_line and line_nr < len(log_lines) and re.match('\(Font\)', log_lines[line_nr]): 272 input_line = re.search('on input line (\d+)', line) 273 if not input_line and line_nr+1 < len(log_lines) and re.match('\(Font\)', log_lines[line_nr+1]): 274 input_line = re.search('on input line (\d+)', log_lines[line_nr+1]) 275 msg = re.sub(' on input line \d+', '', re.sub(font_warning_pat,'', line)) 276 if msg == "": 277 msg = " " 278 i=0 279 while line_nr+i < len(log_lines) and re.match('\(Font\)', log_lines[line_nr+i]): 280 msg += re.sub(' on input line \d+', '', re.sub('\(Font\)\s*', ' ', log_lines[line_nr])) 281 i+=1 282 if not re.search("\.\s*$", msg): 283 msg = re.sub("\s*$", ".", msg) 284 if input_line: 285 output_data.append([font_warning, last_file, input_line.group(1), "0", msg]) 286 else: 287 output_data.append([font_warning, last_file, "0", "0", msg]) 288 elif re.match(font_info_pat, line): 289 # Log Message: 'LaTeX Font Info: ' 290 input_line = re.search('on input line (\d+)', line) 291 if not input_line and line_nr < len(log_lines) and re.match('\(Font\)', log_lines[line_nr]): 292 input_line = re.search('on input line (\d+)', log_lines[line_nr]) 293 if not input_line and line_nr+1 < len(log_lines) and re.match('\(Font\)', log_lines[line_nr+1]): 294 input_line = re.search('on input line (\d+)', log_lines[line_nr+1]) 295 msg = re.sub(' on input line \d+', '', re.sub(font_info_pat,'', line)) 296 if msg == "": 297 msg = " " 298 i=0 299 while line_nr+i < len(log_lines) and re.match('\(Font\)', log_lines[line_nr+i]): 300 msg += re.sub(' on input line \d+', '', re.sub('\(Font\)\s*', ' ', log_lines[line_nr])) 301 i+=1 302 if not re.search("\.\s*$", msg): 303 msg = re.sub("\s*$", ".", msg) 304 if input_line: 305 output_data.append([font_info, last_file, input_line.group(1), "0", msg]) 306 else: 307 output_data.append([font_info, last_file, "0", "0", msg]) 308 elif re.match(package_warning_pat, line): 309 # Log Message: 'Package (\w+) Warning: ' 310 package = re.match(package_warning_pat, line).group(1) 311 input_line = re.search('on input line (\d+)', line) 312 msg = re.sub(package_warning_pat,'', line) 313 if line_nr < len(log_lines): 314 nline = log_lines[line_nr] 315 i=0 316 while re.match('\('+package+'\)',nline): 317 msg+=re.sub('\('+package+'\)\s*', ' ', nline) 318 if not input_line: 319 input_line = re.search('on input line (\d+)', nline) 320 i+=1 321 if line_nr+i < len(log_lines): 322 nline = log_lines[line_nr+i] 323 else: 324 break 325 if msg == "": 326 msg = " " 327 msg = re.sub(' on input line \d+', '', msg) 328 if input_line: 329 output_data.append([package_warning, last_file, input_line.group(1), "0", "%s (%s package)" % (msg, package)]) 330 else: 331 output_data.append([package_warning, last_file, "0", "0", "%s (%s package)" % (msg, package)]) 332 elif re.match(package_info_pat, line): 333 # Log Message: 'Package (\w+) Info: ' 334 package = re.match(package_info_pat, line).group(1) 335 input_line = re.search('on input line (\d+)', line) 336 msg = re.sub(package_info_pat,'', line) 337 if line_nr < len(log_lines): 338 nline = log_lines[line_nr] 339 i=0 340 while re.match(('\(%s\)' % package), nline): 341 msg+=re.sub(('\(%s\)\s*' % package), ' ', nline) 342 if not input_line: 343 input_line = re.search('on input line (\d+)', nline) 344 i+=1 345 if line_nr+i < len(log_lines): 346 nline = log_lines[line_nr+i] 347 else: 348 break 349 if msg == "": 350 msg = " " 351 msg = re.sub(' on input line \d+', '', msg) 352 if input_line: 353 output_data.append([package_info, last_file, input_line.group(1), "0", msg+" ("+package+")"]) 354 else: 355 output_data.append([package_info, last_file, "0", "0", msg+" ("+package+")"]) 356 elif re.match(hbox_info_pat, line): 357 # Log Message: '(Over|Under)full \\\\hbox' 358 input_line = re.search('at lines? (\d+)(?:--(?:\d+))?', line) 359 if re.match('Underfull', line): 360 h_type = 'Underfull ' 361 else: 362 h_type = 'Overfull ' 363 msg = h_type+'\\hbox '+str(re.sub(hbox_info_pat, '', line)) 364 if msg == "": 365 msg = " " 366 if input_line: 367 output_data.append([hbox_info, last_file, input_line.group(1), "0", msg]) 368 else: 369 output_data.append([hbox_info, last_file, "0", "0", msg]) 370 elif re.match(latex_info_pat, line): 371 # Log Message: 'LaTeX Info: ' 372 input_line = re.search('on input line (\d+)', line) 373 msg = re.sub(' on input line \d+', '', re.sub(latex_info_pat,'', line)) 374 if msg == "": 375 msg = " " 376 if input_line: 377 output_data.append([latex_info, last_file, input_line.group(1), "0", msg]) 378 else: 379 output_data.append([latex_info, last_file, "0", "0", msg]) 380 elif re.match(input_package_pat, line): 381 # Log Message: 'Package: ', 'Document Class: ' 382 msg = re.sub(input_package_pat, '', line) 383 if msg == "": 384 msg = " " 385 output_data.append([input_package, last_file, "0", "0", msg]) 386 elif re.match(latex_emergency_stop_pat, line): 387 # Log Message: '! Emergency stop.' 388 msg = "Emergency stop." 389 nline = log_lines[line_nr] 390 match = re.match('<\*>\s+(.*)', nline) 391 if match: 392 e_file = match.group(1) 393 else: 394 e_file = "0" 395 i=-1 396 while True: 397 i+=1 398 try: 399 nline = log_lines[line_nr-1+i] 400 line_m = re.match('\*\*\*\s+(.*)', nline) 401 if line_m: 402 rest = line_m.group(1) 403 break 404 elif i>50: 405 rest = "" 406 break 407 except IndexError: 408 rest = "" 409 break 410 msg += " "+rest 411 output_data.append([latex_emergency_stop, e_file, "0", "0",msg]) 412 elif re.match(latex_error_pat, line): 413 # Log Message: '\! (?:LaTeX Error: |Package (\w+) Error: )?' 414 # get the line unmber of the error 415 match = re.search('on input line (\d+)', line) 416 input_line = (match and [match.group(1)] or [None])[0] 417 i=-1 418 while True: 419 i+=1 420 try: 421 nline = log_lines[line_nr-1+i] 422 line_m = re.match('l\.(\d+) (.*)', nline) 423 if line_m: 424 if input_line is None: 425 input_line = line_m.group(1) 426 rest = line_m.group(2)+re.sub('^\s*', ' ', log_lines[line_nr+i]) 427 break 428 elif i>50: 429 if input_line is None: 430 input_line="0" 431 rest = "" 432 break 433 except IndexError: 434 if input_line is None: 435 input_line="0" 436 rest = "" 437 break 438 msg = re.sub(latex_error_pat, '', line) 439 if msg == "": 440 msg = " " 441 p_match = re.match('! Package (\w+) Error', line) 442 if p_match: 443 info = p_match.group(1) 444 elif rest: 445 info = rest 446 else: 447 info = "" 448 if re.match('\s*\\\\]\s*', info) or re.match('\s*$', info): 449 info = "" 450 if info != "": 451 info = " |"+info 452 if re.match('!\s+A <box> was supposed to be here\.', line) or \ 453 re.match('!\s+Infinite glue shrinkage found in a paragraph', line) or \ 454 re.match('!\s+Missing \$ inserted\.', line): 455 info = "" 456 verbose_msg = "" 457 for j in range(1,i): 458 if not re.match("See\s+the\s+\w+\s+manual\s+or\s+\w+\s+Companion\s+for\s+explanation\.|Type\s+[HI]", log_lines[line_nr-1+j]): 459 verbose_msg+=re.sub("^\s*", " ", log_lines[line_nr-1+j]) 460 else: 461 break 462 if re.match('\s*<(?:inserted text|to be read again|recently read)>', verbose_msg) or \ 463 re.match('\s*See the LaTeX manual', verbose_msg) or \ 464 re.match('!\s+Infinite glue shrinkage found in a paragraph', line): 465 verbose_msg = "" 466 if not re.match('\s*$',verbose_msg): 467 verbose_msg = " |"+verbose_msg 468 469 if last_file == "0": 470 i=-1 471 while True: 472 i+=1 473 try: 474 nline = log_lines[line_nr-1+i] 475 line_m = re.match('<\*>\s+(.*)', nline) 476 if line_m: 477 e_file = line_m.group(1) 478 break 479 elif i>50: 480 e_file="0" 481 break 482 except IndexError: 483 e_file="0" 484 break 485 else: 486 e_file = last_file 487 if not match: 488 index = len(output_data) 489 else: 490 # Find the correct place to put the error message: 491 try: 492 try: 493 prev_element=filter(lambda d: d[1] == e_file and int(d[2]) <= int(input_line), output_data)[-1] 494 index = output_data.index(prev_element)+1 495 except IndexError: 496 prev_element=filter(lambda d: d[1] == e_file and int(d[2]) > int(input_line), output_data)[0] 497 index = output_data.index(prev_element) 498 except IndexError: 499 index = len(output_data) 500 501 output_data.insert(index, [latex_error, e_file, input_line, "0", msg+info+verbose_msg]) 502 503 output_data=map(lambda x: "::".join(x), output_data) 504 try: 505 output_fo=open(output_fname, 'w') 506 except IOError: 507 print("IOError: cannot open %s file for writting" % output_fname) 508 sys.exit(1) 509 else: 510 output_fo.write(('\n'.join(output_data)+'\n').encode(encoding, 'ignore')) 511 output_fo.close() 512 513# Main call 514if __name__ == '__main__': 515 516 usage = "%prog [options] {log_file}" 517 parser = OptionParser(usage=usage) 518 519 import locale 520 encoding = locale.getpreferredencoding() 521 522 parser.add_option("-e", "--encoding", dest="encoding", default=encoding, help="encoding to use (default=%s)" % encoding) 523 (options, args) = parser.parse_args() 524 525 try: 526 rewrite_log(args[0], encoding=options.encoding) 527 except IOError: 528 pass