PageRenderTime 58ms CodeModel.GetById 31ms app.highlight 23ms RepoModel.GetById 1ms app.codeStats 0ms

/ftplugin/ATP_files/latex_log.py

https://github.com/vim-scripts/AutomaticLaTexPlugin
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