/ftplugin/ATP_files/latextags.py
Python | 314 lines | 258 code | 11 blank | 45 comment | 12 complexity | 6b7e88c049019bb79f66c647e921baa5 MD5 | raw file
1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3# Author: Marcin Szamotulski 4# http://atp-vim.sourceforge.net 5# 6# Copyright Statement: 7# This file is a part of Automatic Tex Plugin for Vim. 8# 9# Automatic Tex Plugin for Vim is free software: you can redistribute it 10# and/or modify it under the terms of the GNU General Public License as 11# published by the Free Software Foundation, either version 3 of the 12# License, or (at your option) any later version. 13# 14# Automatic Tex Plugin for Vim is distributed in the hope that it will be 15# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17# General Public License for more details. 18# 19# You should have received a copy of the GNU General Public License along 20# with Automatic Tex Plugin for Vim. If not, see <http://www.gnu.org/licenses/>. 21 22import re, optparse, subprocess, os, traceback, sys 23from optparse import OptionParser 24from time import strftime, localtime 25import locale 26encoding = locale.getpreferredencoding() 27 28# Usage: 29# latextags.py --files main_file.tex;input_file1.tex;input_file2.tex --auxfile main_file.aux --bibfiles bibfile.bib --dir ./ 30# --files : specifies all the tex files in a ";"-separated list to parse (all input files) [this option is necessary] 31# --auxfile : the aux file [this option is necessary] 32# --bibfiles : specifies bibfiles 33# --hyperref : if added \hypertarget{}{} commands are scanned too. 34# --cite : has values: "natbib"/"biblatex"/"" (or not given). It sets apropriate pattern to find all \cite commands. 35# --dir : directory where to put the tag file 36# --silent : be silent 37# --servername : specifies the server name of vim, if given error messages are send to (g)vim 38# --progname : aparentely only two values are supported: "vim"/"gvim". 39 40 41# ToDoList: 42# (1) Use synstack function remotely to get tag_type. 43# (2) Scan bib files to get bibkeys (but this might be slow)! 44# this could be written to seprate file. 45 46class Dict(dict): 47 """ 2to3 Python transition. """ 48 def iterkeys(self): 49 if sys.version_info < (3,0): 50 return super(type(self), self).iterkeys() 51 else: 52 return self.keys() 53 54 def iteritems(self): 55 if sys.version_info < (3,0): 56 return super(type(self), self).iteritems() 57 else: 58 return self.items() 59 60 def itervalues(self): 61 if sys.version_info < (3,0): 62 return super(type(self), self).itervalues() 63 else: 64 return self.values() 65 66# OPTIONS: 67usage = "usage: %prog [options]" 68parser = OptionParser(usage=usage) 69parser.add_option("--files", dest="files" ) 70parser.add_option("--silent", dest="silent", default=False, action="store_true") 71parser.add_option("--auxfile", dest="auxfile" ) 72parser.add_option("--hyperref", dest="hyperref", default=False, action="store_true") 73parser.add_option("--servername", dest="servername", default="" ) 74parser.add_option("--progname", dest="progname", default="gvim" ) 75parser.add_option("--bibfiles", dest="bibfiles", default="" ) 76parser.add_option("--bibtags", dest="bibtags", default=False, action="store_true") 77parser.add_option("--bibtags_env", dest="bibtags_env", default=False, action="store_true") 78parser.add_option("--dir", dest="directory") 79parser.add_option("--cite", dest="cite", default="default" ) 80(options, args) = parser.parse_args() 81file_list=options.files.split(";") 82bib_list=options.bibfiles.split(";") 83 84# Cite Pattern: 85if options.cite == "natbib": 86 cite_pattern=re.compile('^(?:[^%]|\\\\%)*\\\\(?:c|C)ite(?:(?:al)?[tp]\*?|year(?:par)?|(?:full)?author\*?|num)?(?:\[.*\])?{([^}]*)}') 87elif options.cite == "biblatex": 88# there is no pattern for \[aA]utocites, \[tT]extcites, \[sS]martcites, 89# \[pP]arencites, \[cC]ites, \footcites, \footcitetexts commands 90 cite_pattern=re.compile('^(?:[^%]|\\\\%)*\\\\(?:[cC]ite\*?|[pP]arencite\*?|footcite(?:text)?|[tT]extcite|[sS]martcite|supercite|[aA]utocite\*?|[cC]iteauthor|citetitle\*?|cite(?:year|date|url)|nocite|(?:foot)?fullcite|(?:[vV]ol|fvol|ftvol|[sStTpP]vol)cite(?:\[.*\])?{(?:[^}]*)}|[nN]otecite|[pP]nocite|citename|citefield)(?:\[.*\])?{([^}]*)}') 91# (?:[aA]utoc|[tT]extc|[sS]martc|[pP]arenc|[cC]|footc)ites(?:(?:\[.*\]{([^}]*)]))* 92else: 93 cite_pattern=re.compile('^(?:[^%]|\\\\%)*\\\\(?:no)?cite(?:\[.*\])?{([^}]*)}') 94 95def vim_remote_expr(servername, expr): 96 """Send <expr> to vim server, 97 98 expr must be well quoted: 99 vim_remote_expr('GVIM', "atplib#callback#TexReturnCode()")""" 100 cmd=[options.progname, '--servername', servername, '--remote-expr', expr] 101 subprocess.Popen(cmd) 102 103def get_tag_type(line, match, label): 104 """Find tag type, 105 106 line is a string, match is an element of a MatchingObject.""" 107 tag_type="" 108 if label == 'label': 109 pat='(?:\\\\hypertarget{.*})?\s*\\\\label' 110 else: 111 pat='(?:\\\\label{.*})?\s*\\\\hypertarget' 112 if re.search('\\\\part{.*}\s*%s{%s}' % (pat, match), line): 113 tag_type="part" 114 elif re.search('\\\\chapter(?:\[.*\])?{.*}\s*%s{%s}' % (pat, match), line): 115 tag_type="chapter" 116 elif re.search('\\\\section(?:\[.*\])?{.*}\s*%s{%s}' % (pat, match), line): 117 tag_type="section" 118 elif re.search('\\\\subsection(?:\[.*\])?{.*}\s*%s{%s}' % (pat, match), line): 119 tag_type="subsection" 120 elif re.search('\\\\subsubsection(?:\[.*\])?{.*}\s*%s{%s}' % (pat, match), line): 121 tag_type="subsubsection" 122 elif re.search('\\\\paragraph(?:\[.*\])?{.*}\s*%s{%s}' % (pat, match), line): 123 tag_type="paragraph" 124 elif re.search('\\\\subparagraph(?:\[.*\])?{.*}\s*%s{%s}' % (pat, match), line): 125 tag_type="subparagraph" 126 elif re.search('\\\\begin{[^}]*}', line): 127 # \label command must be in the same line, 128 # To do: I should add searching in next line too. 129 # Find that it is inside \begin{equation}:\end{equation}. 130 type_match=re.search('\\\\begin\s*{\s*([^}]*)\s*}(?:\s*{.*})?\s*(?:\[.*\])?\s*%s{%s}' % (pat, match), line) 131 try: 132 # Use the last found match (though it should be just one). 133 tag_type=type_match.group(len(type_match.groups())) 134 except AttributeError: 135 tag_type="" 136 return tag_type 137 138def find_in_filelist(match, file_dict, get_type=False, type_pattern=None): 139 """find match in list of files, 140 141 file_dict is a dictionary with { 'file_name' : file }.""" 142 r_file = "" 143 r_type = "" 144 for file in file_dict.iterkeys(): 145 flinenr=1 146 for line in file_dict[file]: 147 pat_match=re.search(match, line) 148 if pat_match: 149 r_file=file 150 if get_type: 151 r_type=re.match(type_pattern, line).group(1) 152 break 153 flinenr+=1 154 if get_type: 155 return [ r_file, flinenr, r_type ] 156 else: 157 return [ r_file, flinenr ] 158 159def comma_split(arg_list): 160 ret_list = [] 161 for element in arg_list: 162 ret_list.extend(element.split(",")) 163 return ret_list 164 165try: 166 # Read tex files: 167 file_dict=Dict({}) 168 # { 'file_name' : list_of_lines } 169 for file in file_list: 170 try: 171 if sys.version_info.major < 3: 172 with open(file, "r") as file_obj: 173 file_data=file_obj.read().decode(encoding, errors="replace") 174 else: 175 with open(file, "r", encoding=enconding, errors="replace") as file_obj: 176 file_data=file_obj.read() 177 file_dict[file]=file_data.split("\n") 178 except IOError: 179 if options.servername != "": 180 vim_remote_expr(options.servername, "atplib#callback#Echo(\"[LatexTags:] file %s not found.\",'echomsg','WarningMsg')" % file) 181 file_dict[file]=[] 182 pass 183 184 # Read bib files: 185 if len(bib_list) > 1: 186 bib_dict=Dict({}) 187 # { 'bib_name' : list_of_lines } 188 for bibfile in bib_list: 189 if sys.version_info.major < 3 : 190 with open(bibfile, "r") as file_obj: 191 bib_data=file_obj.read().decode(encoding, errors="replace") 192 else: 193 with open(bibfile, "r", encoding=encoding, errors="replace") as file_obj: 194 bib_data=file_obj.read() 195 bib_dict[bibfile]=bib_data.split("\n") 196 197 # GENERATE TAGS: 198 # From \label{} and \hypertarget{}{} commands: 199 tags=[] 200 tag_dict=Dict({}) 201 for file_name in file_list: 202 file_ll=file_dict[file_name] 203 linenr=0 204 p_line="" 205 for line in file_ll: 206 linenr+=1 207 # Find LABELS in the current line: 208 matches=re.findall('^(?:[^%]|\\\\%)*\\\\label{([^}]*)}', line) 209 for match in matches: 210 tag="%s\t%s\t%d" % (match, file_name, linenr) 211 # Set the tag type: 212 tag_type=get_tag_type(line, match, "label") 213 if tag_type == "": 214 tag_type=get_tag_type(p_line+line, match, "label") 215 tag+=";\"\tinfo:%s\tkind:label" % tag_type 216 # Add tag: 217 tags.extend([tag]) 218 tag_dict[str(match)]=[str(linenr), file_name, tag_type, 'label'] 219 # Find HYPERTARGETS in the current line: /this could be switched on/off depending on useage of hyperref/ 220 if options.hyperref: 221 matches=re.findall('^(?:[^%]|\\\\%)*\\\\hypertarget{([^}]*)}', line) 222 for match in matches: 223 # Add only if not yet present in tag list: 224 if not str(match) in tag_dict: 225 tag_dict[str(match)]=[str(linenr), file_name, tag_type, 'hyper'] 226 tag_type=get_tag_type(line, match, 'hypertarget') 227 if tag_type == "": 228 tag_type=get_tag_type(p_line+line, match, "label") 229 tags.extend(["%s\t%s\t%d;\"\tinfo:%s\tkind:hyper" % (match,file_name,linenr,tag_type)]) 230 # Find CITATIONS in the current line: 231 if options.bibtags and not options.bibtags_env: 232 # There is no support for \citealias comman in natbib. 233 # complex matches are slower so I should pass an option if one uses natbib. 234 matches=re.findall(cite_pattern, line) 235 matches=comma_split(matches) 236 for match in matches: 237 if not str(match) in tag_dict: 238 if len(bib_list) == 1: 239 tag="%s\t%s\t/%s/;\"\tkind:cite" % (match, bib_list[0], match) 240 tag_dict[str(match)]=['', bib_list[0], '', 'cite'] 241 tags.extend([tag]) 242 elif len(bib_list) > 1: 243 bib_file="" 244 [ bib_file, bib_linenr, bib_type ] = find_in_filelist(re.compile(str(match)), bib_dict, True, re.compile('\s*@(.*){')) 245 if bib_file != "": 246 tag="%s\t%s\t%d;\"\tkind:cite\tinfo:%s" % (match, bib_file, bib_linenr, bib_type) 247 tag_dict[str(match)]=['', bib_file, bib_type, 'cite'] 248 tags.extend([tag]) 249 if options.bibtags and options.bibtags_env: 250 matches=re.findall(cite_pattern, line) 251 matches=comma_split(matches) 252 for match in matches: 253 if not str(match) in tag_dict: 254 [ r_file, r_linenr ] = find_in_filelist(re.compile("\\\\bibitem(?:\s*\[.*\])?\s*{"+str(match)+"}"), file_dict) 255 tag="%s\t%s\t%d;\"\tkind:cite" % (match, r_file, r_linenr) 256 tag_dict[str(match)]=[str(r_linenr), r_file, '', 'cite'] 257 tags.extend([tag]) 258 p_line=line 259 260 # From aux file: 261 ioerror=False 262 try: 263 if sys.version_info.major < 3: 264 with open(options.auxfile, "r") as file_obj: 265 aux_data=file_obj.read().decode(encoding, errors="replace").split("\n") 266 else: 267 with open(options.auxfile, "r", encoding=encoding, errors="replace") as file_obj: 268 aux_data=file_obj.readlines() 269 for line in aux_data: 270 if re.match('\\\\newlabel{[^}]*}{{[^}]*}', line): 271 [label, counter]=re.match('\\\\newlabel{([^}]*)}{{([^}]*)}', line).group(1,2) 272 counter=re.sub('{', '', counter) 273 if re.search('[0-9.]+', counter): 274 # Arabic Numbered counter: 275 counter=re.search('[0-9.]+', counter).group(0) 276 elif re.search('[ivx]+\)?$', counter): 277 # Roman numberd counter: 278 counter=re.search('\(?([ivx]+)\)?$', counter).group(1) 279 else: 280 counter="" 281 try: 282 [linenr, file, tag_type, kind]=tag_dict[label] 283 except KeyError: 284 [linenr, file, tag_type, kind]=["no_label", "no_label", "", ""] 285 except ValueError: 286 [linenr, file, tag_type, kind]=["no_label", "no_label", "", ""] 287 if linenr != "no_label" and counter != "": 288 tags.extend(["%s\t%s\t%s;\"\tinfo:%s\tkind:%s" % (counter, file, linenr, tag_type, kind)]) 289 except IOError: 290 ioerror=True 291 pass 292 293 # SORT (vim works faster when tag file is sorted) AND WRITE TAGS 294 time=strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()) 295 tags = map(lambda u: u.encode(encoding, errors="replace"), tags) 296 tags_sorted=sorted(tags, key=str.lower) 297 tags_sorted=['!_TAG_FILE_SORTED\t1\t/'+time]+tags_sorted 298 os.chdir(options.directory) 299 with open("tags", 'w') as file_obj: 300 file_obj.write("\n".join(tags_sorted)) 301 302 # Communicate to Vim: 303 if not options.silent: 304 if options.servername != "": 305 vim_remote_expr(options.servername, "atplib#callback#Echo(\"[LatexTags:] tags file written.\",'echo','')") 306 if ioerror and options.servername: 307 vim_remote_expr(options.servername, "atplib#callback#Echo(\"[LatexTags:] no aux file.\",'echomsg', 'WarningMsg')") 308except Exception: 309 # Send errors to vim is options.servername is non empty. 310 error_str=re.sub("'", "''",re.sub('"', '\\"', traceback.format_exc())) 311 if options.servername != "": 312 vim_remote_expr(options.servername, "atplib#callback#Echo(\"[ATP:] error in latextags.py, catched python exception:\n%s\",'echo','ErrorMsg')" % error_str) 313 else: 314 print(error_str)