1#!/usr/bin/env python 2''' 3asciidoc - converts an AsciiDoc text file to DocBook, HTML or LinuxDoc 4 5SYNOPSIS 6asciidoc -b backend [ -d doctype ] [ -g glossary-entry ] 7 [ -e ] [-n] [ -s ] [ -f configfile ] [ -o outfile ] 8 [ --help | -h ] [ --version ] [ -v ] [ -c ] 9 infile 10 11DESCRIPTION 12 The asciidoc(1) command translates the AsciiDoc text file 'infile' 13 to the 'backend' formatted file 'outfile'. If 'infile' is '-' then 14 the standard input is used. 15 16OPTIONS 17 --help, -h 18 Print this documentation. 19 20 -b 21 Backend output file format: 'docbook', 'linuxdoc', 'html', 22 'css' or 'css-embedded'. 23 24 -c Dump configuration to stdout. 25 26 -d 27 Document type: 'article', 'manpage' or 'book'. The 'book' 28 document type is only supported by the 'docbook' backend and 29 the 'manpage' document type is not supported by the 'linuxdoc' 30 backend. 31 32 -e 33 Exclude implicitly loaded configuration files except for those 34 named like the input file ('infile.conf' and 35 'infile-backend.conf'). 36 37 -f 38 Use configuration file 'configfile'. 39 40 -g 41 Define glossary entry where 'glossary-entry' is formatted like 42 'name=value'. Alternate acceptable forms are 'name' (the 43 'value' defaults to an empty string) and '^name' (undefine the 44 'name' glossary entry). Use the '-g section-numbers' 45 command-line option to auto-number HTML article section 46 titles. 47 48 -n Synonym for '-g section-numbers'. 49 50 -o 51 Write output to file 'outfile'. Defaults to the base name of 52 input file with 'backend' extension. If the input is stdin 53 then the outfile defaults to stdout. If 'outfile' is '-' then 54 the standard output is used. 55 56 -s 57 Suppress document header and footer output. 58 59 -v 60 Verbosely print processing information to stderr. 61 62 --version 63 Print program version number. 64 65BUGS 66 - Keyboard EOF (Ctrl+D) ignored when reading source from console. 67 - Reported line numbers in diagnostic messages are sometimes 68 wrong. 69 - Diagnostic messages are often not that illuminating. 70 - Block filters only work in a UNIX environment. 71 - Embedding open brace characters { in argument values can cause 72 incorrect argument substitution. 73 - Section numbering is incorrect when outputting HTML against a book 74 type document with level 0 sections titles. This is not a biggy 75 since multi-part books are generally processed to DocBook. 76 77AUTHOR 78 Written by Stuart Rackham, <srackham@methods.co.nz> 79 80RESOURCES 81 SourceForge: http://sourceforge.net/projects/asciidoc/ 82 Main website: http://www.methods.co.nz/asciidoc/ 83 84COPYING 85 Copyright (C) 2002,2004 Stuart Rackham. Free use of this software is 86 granted under the terms of the GNU General Public License (GPL). 87''' 88 89import sys, os, re, string, time, traceback, tempfile, popen2 90from types import * 91 92VERSION = '5.0.5' # See CHANGLOG file for version history. 93 94#--------------------------------------------------------------------------- 95# Utility functions and classes. 96#--------------------------------------------------------------------------- 97# Allowed substitution options for subs List options and presubs and postsubs 98# Paragraph options. 99SUBS_OPTIONS = ('specialcharacters','quotes','specialwords','replacements', 100 'glossary','macros','none','default') 101# Default value for unspecified subs and presubs configuration file entries. 102SUBS_DEFAULT = ('specialcharacters','quotes','specialwords','replacements', 103 'glossary','macros') 104 105class EAsciiDoc(Exception): 106 pass 107 108class staticmethod: 109 '''Python 2.1 and earlier does not have the builtin staticmethod() 110 function.''' 111 def __init__(self,anycallable): 112 self.__call__ = anycallable 113 114from UserDict import UserDict 115 116class OrderedDict(UserDict): 117 '''Python Cookbook: Ordered Dictionary, Submitter: David Benjamin''' 118 def __init__(self, dict = None): 119 self._keys = [] 120 UserDict.__init__(self, dict) 121 def __delitem__(self, key): 122 UserDict.__delitem__(self, key) 123 self._keys.remove(key) 124 def __setitem__(self, key, item): 125 UserDict.__setitem__(self, key, item) 126 if key not in self._keys: self._keys.append(key) 127 def clear(self): 128 UserDict.clear(self) 129 self._keys = [] 130 def copy(self): 131 dict = UserDict.copy(self) 132 dict._keys = self._keys[:] 133 return dict 134 def items(self): 135 # zip() not available in Python 1.5.2 136 #return zip(self._keys, self.values()) 137 result = [] 138 for k in self._keys: 139 result.append((k,UserDict.__getitem__(self,k))) 140 return result 141 def keys(self): 142 return self._keys 143 def popitem(self): 144 try: 145 key = self._keys[-1] 146 except IndexError: 147 raise KeyError('dictionary is empty') 148 val = self[key] 149 del self[key] 150 return (key, val) 151 def setdefault(self, key, failobj = None): 152 UserDict.setdefault(self, key, failobj) 153 if key not in self._keys: self._keys.append(key) 154 def update(self, dict): 155 UserDict.update(self, dict) 156 for key in dict.keys(): 157 if key not in self._keys: self._keys.append(key) 158 def values(self): 159 return map(self.get, self._keys) 160 161def print_stderr(line): 162 sys.stderr.write(line+os.linesep) 163 164def verbose(msg,linenos=1): 165 '''-v option messages.''' 166 if config.verbose: 167 console(msg,linenos=linenos) 168def warning(msg,linenos=1): 169 console(msg,'WARNING: ',linenos) 170def console(msg, prefix='',linenos=1): 171 '''Print warning message to stdout. 'offset' is added to reported line 172 number for warnings emitted when reading ahead.''' 173 s = prefix 174 if linenos and reader.cursor: 175 s = s + "%s: line %d: " \ 176 % (os.path.basename(reader.cursor[0]),reader.cursor[1]) 177 s = s + msg 178 print_stderr(s) 179 180def realpath(fname): 181 '''Return the absolute pathname of the file fname. Follow symbolic links. 182 os.realpath() not available in Python prior to 2.2 and not portable.''' 183 # Follow symlinks to the actual executable. 184 wd = os.getcwd() 185 try: 186 while os.path.islink(fname): 187 linkdir = os.path.dirname(fname) 188 fname = os.readlink(fname) 189 if linkdir: os.chdir(linkdir) # Symlinks can be relative. 190 fname = os.path.abspath(fname) 191 finally: 192 os.chdir(wd) 193 return fname 194 195def assign(dst,src): 196 '''Assign all attributes from 'src' object to 'dst' object.''' 197 for a,v in src.__dict__.items(): 198 setattr(dst,a,v) 199 200def strip_quotes(s): 201 '''Trim white space and, if necessary, quote characters from s.''' 202 s = string.strip(s) 203 # Strip quotation mark characters from quoted strings. 204 if len(s) >= 3 and s[0] == '"' and s[-1] == '"': 205 s = s[1:-1] 206 return s 207 208def is_regexp(s): 209 '''Return 1 if s is a valid regular expression else return 0.''' 210 try: re.compile(s) 211 except: return 0 212 else: return 1 213 214def join_regexp(relist): 215 '''Join list of regular expressions re1,re2,... to single regular 216 expression (re1)|(re2)|...''' 217 if len(relist) == 0: 218 return '' 219 result = [] 220 # Delete named groups to avoid ambiguity. 221 for s in relist: 222 result.append(re.sub(r'\?P<\S+?>','',s)) 223 result = string.join(result,')|(') 224 result = '('+result+')' 225 return result 226 227def validate(value,rule,errmsg): 228 '''Validate value against rule expression. Throw EAsciiDoc exception with 229 errmsg if validation fails.''' 230 try: 231 if not eval(string.replace(rule,'$',str(value))): 232 raise EAsciiDoc,errmsg 233 except: 234 raise EAsciiDoc,errmsg 235 return value 236 237def join_lines(lines): 238 '''Return a list in which lines terminated with the backslash line 239 continuation character are joined.''' 240 result = [] 241 s = '' 242 continuation = 0 243 for line in lines: 244 if line and line[-1] == '\\': 245 s = s + line[:-1] 246 continuation = 1 247 continue 248 if continuation: 249 result.append(s+line) 250 s = '' 251 continuation = 0 252 else: 253 result.append(line) 254 if continuation: 255 result.append(s) 256 return result 257 258def parse_args(args,dict,default_arg=None): 259 '''Update a dictionary with name/value arguments from the args string. The 260 args string is a comma separated list of values and keyword name=value 261 pairs. Values must preceed keywords and are named '1','2'... The entire 262 args list is named '0'. If keywords are specified string values must be 263 quoted. Examples: 264 265 args: '' 266 dict: {} 267 268 args: 'hello,world' 269 dict: {'2': 'world', '0': 'hello,world', '1': 'hello'} 270 271 args: 'hello,,world' 272 default_arg: 'caption' 273 dict: {'3': 'world', 'caption': 'hello', '0': 'hello,,world', '1': 'hello'} 274 275 args: '"hello",planet="earth"' 276 dict: {'planet': 'earth', '0': '"hello",planet="earth"', '1': 'hello'} 277 ''' 278 def f(*args,**keywords): 279 # Name and add aguments '1','2'... to keywords. 280 for i in range(len(args)): 281 if not keywords.has_key(str(i+1)): 282 keywords[str(i+1)] = args[i] 283 return keywords 284 285 if not args: return 286 dict['0'] = args 287 s = args 288 try: 289 d = eval('f('+s+')') 290 dict.update(d) 291 except: 292 # Try quoting the args. 293 s = string.replace(s,'"',r'\"') # Escape double-quotes. 294 s = string.split(s,',') 295 s = map(lambda x: '"'+string.strip(x)+'"',s) 296 s = string.join(s,',') 297 try: 298 d = eval('f('+s+')') 299 except: 300 return # If there's a syntax error leave with {0}=args. 301 for k in d.keys(): # Drop any arguments that were missing. 302 if d[k] == '': del d[k] 303 dict.update(d) 304 assert len(d) > 0 305 if default_arg is not None and not d.has_key(default_arg) \ 306 and d.has_key('1'): 307 dict[default_arg] = dict['1'] 308 309def parse_list(s): 310 '''Parse comma separated string of Python literals. Return a tuple of of 311 parsed values.''' 312 try: 313 result = eval('tuple(['+s+'])') 314 except: 315 raise EAsciiDoc,'malformed list: '+s 316 return result 317 318def parse_options(options,allowed,errmsg): 319 '''Parse comma separated string of unquoted option names and return as a 320 tuple of valid options. 'allowed' is a list of allowed option values. 321 'errmsg' isan error message prefix if an illegal option error is thrown.''' 322 result = [] 323 if options: 324 for s in string.split(options,','): 325 s = string.strip(s) 326 if s not in allowed: 327 raise EAsciiDoc,'%s "%s"' % (errmsg,s) 328 result.append(s) 329 return tuple(result) 330 331def is_name(s): 332 '''Return 1 if s is valid glossary, macro or tag name.''' 333 mo = re.match(r'\w[-\w]*',s) 334 if mo is not None and s[-1] != '-': return 1 335 else: return 0 336 337def subs_quotes(text): 338 '''Quoted text is marked up and the resulting text is 339 returned.''' 340 quotes = config.quotes.keys() 341 # The quotes are iterated in reverse sort order to avoid ambiguity, 342 # for example, '' is processed before '. 343 quotes.sort() 344 quotes.reverse() 345 for quote in quotes: 346 i = string.find(quote,'|') 347 if i != -1 and quote != '|' and quote != '||': 348 lq = quote[:i] 349 rq = quote[i+1:] 350 else: 351 lq = rq = quote 352 reo = re.compile(r'(^|\W)(?:' + re.escape(lq) \ 353 + r')(.*?[^\\])(?:'+re.escape(rq)+r')(?=\W|$)') 354 pos = 0 355 while 1: 356 mo = reo.search(text,pos) 357 if not mo: break 358 if text[mo.start()] == '\\': 359 pos = mo.end() 360 else: 361 stag,etag = config.tag(config.quotes[quote]) 362 if stag == etag == None: 363 s = '' 364 else: 365 s = mo.group(1) + stag + mo.group(2) + etag 366 text = text[:mo.start()] + s + text[mo.end():] 367 pos = mo.start() + len(s) 368 text = string.replace(text,'\\'+lq, lq) # Unescape escaped quotes. 369 return text 370 371def subs_tag(tag,dict={}): 372 '''Perform glossary substitution and split tag string returning start, end 373 tag tuple (c.f. Config.tag()).''' 374 s = subs_glossary((tag,),dict)[0] 375 result = string.split(s,'|') 376 if len(result) == 1: 377 return result+[None] 378 elif len(result) == 2: 379 return result 380 else: 381 raise EAsciiDoc,'malformed tag "%s"' % (tag,) 382 383def parse_entry(entry,dict=None,unquote=0): 384 '''Parse name=value entry to dictionary 'dict'. Return tuple (name,value) 385 or None if illegal entry. If value is omitted (name=) then it is set to ''. 386 If only the name is present the value is set to None). Leading and trailing 387 white space is striped from 'name' and 'value'. If 'unquote' is True 388 leading and trailing double-quotes are stripped from 'name' and 'value'. 389 'name' can contain any printable characters. If 'name includes the equals 390 '=' character it must be escaped with a backslash.''' 391 mo=re.search(r'[^\\](=)',entry) 392 if mo: # name=value entry. 393 name = entry[:mo.start(1)] 394 value = entry[mo.end(1):] 395 else: # name entry. 396 name = entry 397 value = None 398 if unquote: 399 name = strip_quotes(name) 400 if value is not None: 401 value = strip_quotes(value) 402 else: 403 name = string.strip(name) 404 if value is not None: 405 value = string.strip(value) 406 if not name: 407 return None 408 if dict is not None: 409 dict[name] = value 410 return name,value 411 412def parse_entries(entries,dict,unquote=0): 413 '''Parse name=value entries from from lines of text in 'entries' into 414 dictionary 'dict'. Blank lines are skipped.''' 415 for entry in entries: 416 if entry and not parse_entry(entry,dict,unquote): 417 raise EAsciiDoc,'malformed section entry "%s"' % (entry,) 418 419def undefine_entries(entries): 420 '''All dictionary entries with None values are deleted.''' 421 for k,v in entries.items(): 422 if v is None: 423 del entries[k] 424 425def dump_section(name,dict,f=sys.stdout): 426 '''Write parameters in 'dict' as in configuration file section format with 427 section 'name'.''' 428 f.write('[%s]%s' % (name,writer.newline)) 429 for k,v in dict.items(): 430 k = str(k) 431 # Quote if necessary. 432 if len(k) != len(string.strip(k)): 433 k = '"'+k+'"' 434 if v and len(v) != len(string.strip(v)): 435 v = '"'+v+'"' 436 if v is None: 437 # Don't dump undefined entries. 438 continue 439 else: 440 s = k+'='+v 441 f.write('%s%s' % (s,writer.newline)) 442 f.write(writer.newline) 443 444def update_glossary(glossary,dict): 445 '''Update 'glossary' dictionary with entries from parsed glossary section 446 dictionary 'dict'.''' 447 for k,v in dict.items(): 448 if not is_name(k): 449 raise EAsciiDoc,'illegal "%s" glossary entry name' % (k,) 450 glossary[k] = v 451 452def readlines(fname): 453 '''Read lines from file named 'fname' and strip trailing white space.''' 454 # Read include file. 455 f = open(fname) 456 try: 457 lines = f.readlines() 458 finally: 459 f.close() 460 # Strip newlines. 461 for i in range(len(lines)): 462 lines[i] = string.rstrip(lines[i]) 463 return lines 464 465def filter_lines(filter,lines,dict={}): 466 '''Run 'lines' through the 'filter' shell command and return the result. The 467 'dict' dictionary contains additional filter glossary entry parameters.''' 468 if not filter: 469 return lines 470 if os.name != 'posix': 471 warning('filters do not work in a non-posix environment') 472 return lines 473 # Perform glossary substitution on the filter command. 474 f = subs_glossary((filter,),dict) 475 if not f: 476 raise EAsciiDoc,'filter "%s" has undefined parameter' % (filter,) 477 filter = f[0] 478 # Check in the 'filters' directory in both the asciidoc user and application 479 # directories for the filter command. 480 cmd = string.split(filter)[0] 481 found = 0 482 if not os.path.dirname(cmd): 483 if USER_DIR: 484 cmd2 = os.path.join(USER_DIR,'filters',cmd) 485 if os.path.isfile(cmd2): 486 found = 1 487 if not found: 488 cmd2 = os.path.join(APP_DIR,'filters',cmd) 489 if os.path.isfile(cmd2): 490 found = 1 491 if found: 492 filter = string.split(filter) 493 filter[0] = cmd2 494 filter = string.join(filter) 495 verbose('filtering: '+filter, linenos=0) 496 try: 497 import select 498 result = [] 499 r,w = popen2.popen2(filter) 500 # Polled I/O loop to alleviate full buffer deadlocks. 501 i = 0 502 while i < len(lines): 503 line = lines[i] 504 if select.select([],[w.fileno()],[],0)[1]: 505 w.write(line+os.linesep) # Use platform line terminator. 506 i = i+1 507 if select.select([r.fileno()],[],[],0)[0]: 508 s = r.readline() 509 if not s: break # Exit if filter output closes. 510 result.append(string.rstrip(s)) 511 w.close() 512 for s in r.readlines(): 513 result.append(string.rstrip(s)) 514 r.close() 515 except: 516 raise EAsciiDoc,'filter "%s" error' % (filter,) 517 # There's no easy way to guage whether popen2() found and executed the 518 # filter, so guess that if it produced no output there is probably a 519 # problem. 520 if lines and not result: 521 warning('no output from filter "%s"' % (filter,)) 522 return result 523 524def glossary_action(action, expr): 525 '''Return the result of a glossary {action:expr} reference.''' 526 verbose('evaluating: '+expr) 527 if action == 'eval': 528 result = None 529 try: 530 result = eval(expr) 531 if result is not None: 532 result = str(result) 533 except: 534 warning('{%s} "%s" expression evaluation error' % (action,expr)) 535 elif action in ('sys','sys2'): 536 result = '' 537 tmp = tempfile.mktemp() 538 try: 539 cmd = expr 540 cmd = cmd + (' > %s' % tmp) 541 if action == 'sys2': 542 cmd = cmd + ' 2>&1' 543 if os.system(cmd): 544 warning('{%s} "%s" command non-zero exit status' 545 % (action,expr)) 546 try: 547 if os.path.isfile(tmp): 548 lines = readlines(tmp) 549 else: 550 lines = [] 551 except: 552 raise EAsciiDoc,'{%s} temp file read error' % (action,) 553 result = string.join(lines, writer.newline) 554 finally: 555 if os.path.isfile(tmp): 556 os.remove(tmp) 557 else: 558 warning('Illegal {%s:} glossary action' % (action,)) 559 return result 560 561def subs_glossary(lines,dict={}): 562 '''Substitute 'lines' of text with glossary entries from the global 563 document.glossary dictionary and from the 'dict' dictionary ('dict' entries 564 take precedence). Return a tuple of the substituted lines. 'lines' 565 containing non-matching substitution parameters are deleted. None glossary 566 values are not substituted but '' glossary values are.''' 567 568 def end_brace(text,start): 569 '''Return index following end brace that matches brace at start in 570 text.''' 571 assert text[start] == '{' 572 n = 0 573 result = start 574 for c in text[start:]: 575 # Skip braces that are followed by a backslash. 576 if result == len(text)-1 or text[result+1] != '\\': 577 if c == '{': n = n + 1 578 elif c == '}': n = n - 1 579 result = result + 1 580 if n == 0: break 581 return result 582 583 lines = list(lines) 584 # Merge glossary and macro arguments (macro args take precedence). 585 gloss = document.glossary.copy() 586 gloss.update(dict) 587 # Substitute all occurences of all dictionary parameters in all lines. 588 for i in range(len(lines)-1,-1,-1): # Reverse iterate lines. 589 text = lines[i] 590 # Make it easier for regular expressions. 591 text = string.replace(text,'\\{','{\\') 592 text = string.replace(text,'\\}','}\\') 593 # Expand literal glossary references of form {name}. 594 # Nested glossary entries not allowed. 595 reo = re.compile(r'\{(?P<name>[^\\\W][-\w]*?)\}(?!\\)', re.DOTALL) 596 pos = 0 597 while 1: 598 mo = reo.search(text,pos) 599 if not mo: break 600 s = gloss.get(mo.group('name')) 601 if s is None: 602 pos = mo.end() 603 else: 604 s = str(s) 605 text = text[:mo.start()] + s + text[mo.end():] 606 pos = mo.start() + len(s) 607 # Expand conditional glossary references of form {name=value}, 608 # {name?value}, {name!value} and {name#value}. 609 reo = re.compile(r'\{(?P<name>[^\\\W][-\w]*?)(?P<op>\=|\?|!|#|%)' \ 610 r'(?P<value>.*?)\}(?!\\)',re.DOTALL) 611 pos = 0 612 while 1: 613 mo = reo.search(text,pos) 614 if not mo: break 615 name = mo.group('name') 616 lval = gloss.get(name) 617 op = mo.group('op') 618 # mo.end() is not good enough because '{x={y}}' matches '{x={y}'. 619 end = end_brace(text,mo.start()) 620 rval = text[mo.start('value'):end-1] 621 if lval is None: 622 # name glossary entry is undefined. 623 if op == '=': s = rval 624 elif op == '?': s = '' 625 elif op == '!': s = rval 626 elif op == '#': s = '{'+name+'}' # So the line is deleted. 627 elif op == '%': s = rval 628 else: assert 1,'illegal glossary operator' 629 else: 630 # name glossary entry is defined. 631 if op == '=': s = lval 632 elif op == '?': s = rval 633 elif op == '!': s = '' 634 elif op == '#': s = rval 635 elif op == '%': s = '{zzzzz}' # So the line is deleted. 636 else: assert 1,'illegal glossary operator' 637 s = str(s) 638 text = text[:mo.start()] + s + text[end:] 639 pos = mo.start() + len(s) 640 # Drop line if it contains unsubstituted {name} references. 641 skipped = re.search(r'\{[^\\\W][-\w]*?\}(?!\\)', text, re.DOTALL) 642 if skipped: 643 del lines[i] 644 continue; 645 # Expand calculated glossary references of form {name:expression}. 646 reo = re.compile(r'\{(?P<action>[^\\\W][-\w]*?):(?P<expr>.*?)\}(?!\\)', 647 re.DOTALL) 648 skipped = 0 649 pos = 0 650 while 1: 651 mo = reo.search(text,pos) 652 if not mo: break 653 expr = mo.group('expr') 654 expr = string.replace(expr,'{\\','{') 655 expr = string.replace(expr,'}\\','}') 656 s = glossary_action(mo.group('action'),expr) 657 if s is None: 658 skipped = 1 659 break 660 text = text[:mo.start()] + s + text[mo.end():] 661 pos = mo.start() + len(s) 662 # Drop line if the action returns None. 663 if skipped: 664 del lines[i] 665 continue; 666 # Remove backslash from escaped entries. 667 text = string.replace(text,'{\\','{') 668 text = string.replace(text,'}\\','}') 669 lines[i] = text 670 return tuple(lines) 671 672class Lex: 673 '''Lexical analysis routines. Static methods and attributes only.''' 674 prev_element = None 675 prev_cursor = None 676 def __init__(self): 677 raise AssertionError,'no class instances allowed' 678 def next(): 679 '''Returns class of next element on the input (None if EOF). The 680 reader is assumed to be at the first line following a previous element, 681 end of file or line one. Exits with the reader pointing to the first 682 line of the next element or EOF (leading blank lines are skipped).''' 683 reader.skip_blank_lines() 684 if reader.eof(): return None 685 # Optimization: If we've already checked for an element at this 686 # position return the element. 687 if Lex.prev_element and Lex.prev_cursor == reader.cursor: 688 return Lex.prev_element 689 result = None 690 # Check for BlockTitle. 691 if not result and BlockTitle.isnext(): 692 result = BlockTitle 693 # Check for Title. 694 if not result and Title.isnext(): 695 result = Title 696 # Check for SectionClose. 697 # Dont' process -- there is no good reason for explicit section closure. 698 #if not result and SectionClose.isnext(): 699 # result = SectionClose 700 # Check for Block Macro. 701 if not result and macros.isnext(): 702 result = macros.current 703 # Check for List. 704 if not result and lists.isnext(): 705 result = lists.current 706 # Check for DelimitedBlock. 707 if not result and blocks.isnext(): 708 # Skip comment blocks. 709 if 'skip' in blocks.current.options: 710 blocks.current.translate() 711 return Lex.next() 712 else: 713 result = blocks.current 714 # Check for Table. 715 if not result and tables.isnext(): 716 result = tables.current 717 # If it's none of the above then it must be an Paragraph. 718 if not result: 719 if not paragraphs.isnext(): 720 raise EAsciiDoc,'paragraph expected' 721 result = paragraphs.current 722 # Cache answer. 723 Lex.prev_cursor = reader.cursor 724 Lex.prev_element = result 725 return result 726 next = staticmethod(next) 727 728 def title_parse(lines): 729 '''Check for valid title at start of tuple lines. Return (title,level) 730 tuple or None if invalid title.''' 731 if len(lines) < 2: return None 732 title,ul = lines[:2] 733 # Title can't be blank. 734 if len(title) == 0: return None 735 if len(ul) < 2: return None 736 # Fast check. 737 if ul[:2] not in Title.underlines: return None 738 # Length of underline must be within +-3 of title. 739 if not (len(ul)-3 < len(title) < len(ul)+3): return None 740 # Underline must be have valid repetition of underline character pairs. 741 s = ul[:2]*((len(ul)+1)/2) 742 if ul != s[:len(ul)]: return None 743 return title,list(Title.underlines).index(ul[:2]) 744 title_parse = staticmethod(title_parse) 745 746 def subs_1(s,options): 747 '''Perform substitution specified in 'options' (in 'options' order) on 748 a single line 's' of text. Returns the substituted string.''' 749 if not s: 750 return s 751 result = s 752 for o in options: 753 if o == 'specialcharacters': 754 result = config.subs_specialchars(result) 755 # Quoted text. 756 elif o == 'quotes': 757 result = subs_quotes(result) 758 # Special words. 759 elif o == 'specialwords': 760 result = config.subs_specialwords(result) 761 # Replacements. 762 elif o == 'replacements': 763 result = config.subs_replacements(result) 764 # Inline macros. 765 elif o == 'macros': 766 result = macros.subs(result) 767 else: 768 raise EAsciiDoc,'illegal "%s" substitution option' % (o,) 769 return result 770 subs_1 = staticmethod(subs_1) 771 772 def subs(lines,options): 773 '''Perform inline processing specified by 'options' (in 'options' 774 order) on sequence of 'lines'.''' 775 if len(options) == 1: 776 if options[0] == 'none': 777 options = () 778 elif options[0] == 'default': 779 options = SUBS_DEFAULT 780 if not lines or not options: 781 return lines 782 for o in options: 783 if o == 'glossary': 784 lines = subs_glossary(lines) 785 else: 786 tmp = [] 787 for s in lines: 788 s = Lex.subs_1(s,(o,)) 789 tmp.append(s) 790 lines = tmp 791 return lines 792 subs = staticmethod(subs) 793 794 def set_margin(lines, margin=0): 795 '''Utility routine that sets the left margin to 'margin' space in a 796 block of non-blank lines.''' 797 # Calculate width of block margin. 798 lines = list(lines) 799 width = len(lines[0]) 800 for s in lines: 801 i = re.search(r'\S',s).start() 802 if i < width: width = i 803 # Strip margin width from all lines. 804 for i in range(len(lines)): 805 lines[i] = ' '*margin + lines[i][width:] 806 return lines 807 set_margin = staticmethod(set_margin) 808 809#--------------------------------------------------------------------------- 810# Document element classes parse AsciiDoc reader input and write DocBook writer 811# output. 812#--------------------------------------------------------------------------- 813class Document: 814 def __init__(self): 815 self.doctype = None # 'article','manpage' or 'book'. 816 self.backend = None # -b option argument. 817 self.glossary = {} # Combined glossary entries. 818 self.level = 0 # 0 => front matter. 1,2,3 => sect1,2,3. 819 def init_glossary(self): 820 # Set derived glossary enties. 821 d = time.localtime(time.time()) 822 self.glossary['localdate'] = time.strftime('%d-%b-%Y',d) 823 s = time.strftime('%H:%M:%S',d) 824 if time.daylight: 825 self.glossary['localtime'] = s + ' ' + time.tzname[1] 826 else: 827 self.glossary['localtime'] = s + ' ' + time.tzname[0] 828 self.glossary['asciidoc-version'] = VERSION 829 self.glossary['backend'] = document.backend 830 self.glossary['doctype'] = document.doctype 831 self.glossary['backend-'+document.backend] = '' 832 self.glossary['doctype-'+document.doctype] = '' 833 self.glossary[document.backend+'-'+document.doctype] = '' 834 self.glossary['asciidoc-dir'] = APP_DIR 835 self.glossary['user-dir'] = USER_DIR 836 if reader.fname: 837 self.glossary['infile'] = reader.fname 838 if writer.fname: 839 self.glossary['outfile'] = writer.fname 840 s = os.path.splitext(writer.fname)[1][1:] # Output file extension. 841 self.glossary['filetype'] = s 842 self.glossary['filetype-'+s] = '' 843 # Update with conf file entries. 844 self.glossary.update(config.conf_gloss) 845 # Update with command-line entries. 846 self.glossary.update(config.cmd_gloss) 847 # Set configuration glossary entries. 848 config.load_miscellaneous(config.conf_gloss) 849 config.load_miscellaneous(config.cmd_gloss) 850 self.glossary['newline'] = config.newline # Use raw (unescaped) value. 851 def translate(self): 852 assert self.doctype in ('article','manpage','book'), \ 853 'illegal document type' 854 assert self.level == 0 855 # Process document header. 856 has_header = Lex.next() is Title and Title.level == 0 857 if self.doctype == 'manpage' and not has_header: 858 raise EAsciiDoc,'manpage document title is mandatory' 859 if has_header: 860 Header.parse() 861 if not config.suppress_headers: 862 hdr = config.subs_section('header',{}) 863 writer.write(hdr) 864 if self.doctype in ('article','book'): 865 # Translate 'preamble' (untitled elements between header 866 # and first section title). 867 if Lex.next() is not Title: 868 if self.doctype == 'book': 869 warning('Preamble not allowed in docbook books') 870 stag,etag = config.section2tags('preamble') 871 writer.write(stag) 872 Section.translate_body() 873 writer.write(etag) 874 else: 875 # Translate manpage SYNOPSIS. 876 if Lex.next() is not Title: 877 raise EAsciiDoc,'second section must be SYNOPSIS' 878 Title.parse() 879 if string.upper(Title.title) <> 'SYNOPSIS': 880 raise EAsciiDoc,'second section must be SYNOPSIS' 881 if Title.level != 1: 882 raise EAsciiDoc,'SYNOPSIS section title must be level 1' 883 stag,etag = config.section2tags('sect-synopsis') 884 writer.write(stag) 885 Section.translate_body() 886 writer.write(etag) 887 else: 888 if not config.suppress_headers: 889 hdr = config.subs_section('header',{}) 890 writer.write(hdr) 891 if Lex.next() is not Title: 892 Section.translate_body() 893 # Process remaining sections. 894 while not reader.eof(): 895 if Lex.next() is not Title: 896 raise EAsciiDoc,'section title expected' 897 Section.translate() 898 Section.setlevel(0) # Write remaining unwritten section close tags. 899 # Substitute document parameters and write document footer. 900 if not config.suppress_headers: 901 ftr = config.subs_section('footer',{}) 902 writer.write(ftr) 903 904class Header: 905 '''Static methods and attributes only.''' 906 def __init__(self): 907 raise AssertionError,'no class instances allowed' 908 def parse(): 909 assert Lex.next() is Title 910 Title.parse() 911 if Title.level not in (0,1): 912 raise EAsciiDoc,'document title level must be 0 or 1' 913 gloss = document.glossary # Alias for readability. 914 gloss['doctitle'] = Title.title 915 if document.doctype == 'manpage': 916 # manpage title formatted like mantitle(manvolnum). 917 mo = re.match(r'^(?P<mantitle>.*)\((?P<manvolnum>.*)\)$', 918 Title.title) 919 if not mo: raise EAsciiDoc,'malformed manpage title' 920 gloss['mantitle'] = string.strip(string.lower(mo.group('mantitle'))) 921 gloss['manvolnum'] = string.strip(mo.group('manvolnum')) 922 s = reader.read_next() 923 if s: 924 # Parse author line. 925 s = reader.read() 926 s = subs_glossary([s])[0] 927 s = string.strip(s) 928 mo = re.match(r'^(?P<name1>[^<>\s]+)' 929 '(\s+(?P<name2>[^<>\s]+))?' 930 '(\s+(?P<name3>[^<>\s]+))?' 931 '(\s+<(?P<email>\S+)>)?$',s) 932 if not mo: 933 raise EAsciiDoc,'malformed author line' 934 firstname = mo.group('name1') 935 if mo.group('name3'): 936 middlename = mo.group('name2') 937 lastname = mo.group('name3') 938 else: 939 middlename = None 940 lastname = mo.group('name2') 941 email = mo.group('email') 942 gloss['firstname'] = firstname 943 gloss['middlename'] = middlename 944 gloss['lastname'] = lastname 945 gloss['email'] = email 946 author = firstname 947 initials = firstname[0] 948 if middlename: 949 author += ' '+middlename 950 initials += middlename[0] 951 if lastname: 952 author += ' '+lastname 953 initials += lastname[0] 954 gloss['author'] = author 955 initials = string.upper(initials) 956 gloss['authorinitials'] = initials 957 if reader.read_next(): 958 # Parse revision line. 959 s = reader.read() 960 s = subs_glossary([s])[0] 961 # Match RCS/CVS $Id$ marker format. 962 mo = re.match(r'^\$Id: \S+ (?P<revision>\S+)' 963 ' (?P<date>\S+) \S+ \S+ \S+ \$$',s) 964 if not mo: 965 # Match AsciiDoc revision,date format. 966 mo = re.match(r'^\D*(?P<revision>.*?),(?P<date>.+)$',s) 967 if mo: 968 revision = string.strip(mo.group('revision')) 969 date = string.strip(mo.group('date')) 970 else: 971 revision = None 972 date = string.strip(s) 973 if revision: 974 gloss['revision'] = revision 975 if date: 976 gloss['date'] = date 977 if document.backend == 'linuxdoc' and not gloss.has_key('author'): 978 warning('linuxdoc requires author name') 979 if document.doctype == 'manpage': 980 # Translate mandatory NAME section. 981 if Lex.next() is not Title: 982 raise EAsciiDoc,'manpage must start with a NAME section' 983 Title.parse() 984 if Title.level != 1: 985 raise EAsciiDoc,'manpage NAME section title must be level 1' 986 if string.upper(Title.title) <> 'NAME': 987 raise EAsciiDoc,'manpage must start with a NAME section' 988 if not isinstance(Lex.next(),Paragraph): 989 raise EAsciiDoc,'malformed manpage NAME section body' 990 lines = reader.read_until(r'^$') 991 s = string.join(lines) 992 mo = re.match(r'^(?P<manname>.*?)-(?P<manpurpose>.*)$',s) 993 if not mo: 994 raise EAsciiDoc,'malformed manpage NAME section body' 995 gloss['manname'] = string.strip(mo.group('manname')) 996 gloss['manpurpose'] = string.strip(mo.group('manpurpose')) 997 parse = staticmethod(parse) 998 999class BlockTitle: 1000 '''Static methods and attributes only.''' 1001 title = None 1002 pattern = None 1003 def __init__(self): 1004 raise AssertionError,'no class instances allowed' 1005 def isnext(): 1006 result = 0 # Assume failure. 1007 line = reader.read_next() 1008 if line: 1009 mo = re.match(BlockTitle.pattern,line) 1010 if mo: 1011 BlockTitle.title = mo.group('title') 1012 result = 1 1013 return result 1014 isnext = staticmethod(isnext) 1015 def translate(): 1016 assert Lex.next() is BlockTitle 1017 reader.read() # Discard title from reader. 1018 # Perform title substitutions. 1019 s = Lex.subs((BlockTitle.title,), Title.subs) 1020 s = string.join(s,writer.newline) 1021 if not s: 1022 warning('blank block title') 1023 BlockTitle.title = s 1024 translate = staticmethod(translate) 1025 def gettitle(dict): 1026 '''If there is a title add it to dict then reset title.''' 1027 if BlockTitle.title: 1028 dict['title'] = BlockTitle.title 1029 BlockTitle.title = None 1030 gettitle = staticmethod(gettitle) 1031 1032class Title: 1033 '''Processes Header and Section titles. Static methods and attributes 1034 only.''' 1035 # Configuration entries defaults. 1036 titles = ('==','--','~~','^^','++') # Levels 0,1,2,3,4. 1037 subs = ('specialcharacters','quotes','replacements','glossary','macros') 1038 # Class variables 1039 title = None 1040 level = 0 1041 sectname = None 1042 section_numbers = [0]*5 1043 dump_dict = {} 1044 def __init__(self): 1045 raise AssertionError,'no class instances allowed' 1046 def parse(): 1047 '''Parse the Title.title and Title.level from the reader. The 1048 real work has already been done by isnext().''' 1049 assert Lex.next() is Title 1050 reader.read(); reader.read() # Discard title from reader. 1051 Title.setsectname() 1052 # Perform title substitutions. 1053 s = Lex.subs((Title.title,), Title.subs) 1054 s = string.join(s,writer.newline) 1055 if not s: 1056 warning('blank section title') 1057 Title.title = s 1058 parse = staticmethod(parse) 1059 def isnext(): 1060 result = 0 # Assume failure. 1061 lines = reader.read_ahead(2) 1062 if len(lines) == 2: 1063 title = Lex.title_parse(lines) 1064 if title is not None: 1065 Title.title, Title.level = title 1066 result = 1 1067 return result 1068 isnext = staticmethod(isnext) 1069 def load(dict): 1070 '''Load and validate [titles] section entries from dict.''' 1071 if dict.has_key('underlines'): 1072 errmsg = 'malformed [titles] underlines entry' 1073 try: 1074 underlines = parse_list(dict['underlines']) 1075 except: 1076 raise EAsciiDoc,errmsg 1077 if len(underlines) != 5: 1078 raise EAsciiDoc,errmsg 1079 for s in underlines: 1080 if len(s) !=2: 1081 raise EAsciiDoc,errmsg 1082 Title.underlines = tuple(underlines) 1083 Title.dump_dict['underlines'] = dict['underlines'] 1084 if dict.has_key('subs'): 1085 Title.subs = parse_options(dict['subs'], SUBS_OPTIONS, 1086 'illegal [titles] subs entry') 1087 Title.dump_dict['subs'] = dict['subs'] 1088 if dict.has_key('blocktitle'): 1089 pat = dict['blocktitle'] 1090 if not pat or not is_regexp(pat): 1091 raise EAsciiDoc,'malformed [titles] blocktitle entry' 1092 BlockTitle.pattern = pat 1093 Title.dump_dict['blocktitle'] = pat 1094 load = staticmethod(load) 1095 def dump(): 1096 dump_section('titles',Title.dump_dict) 1097 dump = staticmethod(dump) 1098 def setsectname(): 1099 '''Set Title section name. First search for section title in 1100 [specialsections], if not found use default 'sect<level>' name.''' 1101 for pat,sect in config.specialsections.items(): 1102 mo = re.match(pat,Title.title) 1103 if mo: 1104 title = mo.groupdict().get('title') 1105 if title is not None: 1106 Title.title = string.strip(title) 1107 else: 1108 Title.title = string.strip(mo.group()) 1109 Title.sectname = sect 1110 break 1111 else: 1112 Title.sectname = 'sect%d' % (Title.level,) 1113 setsectname = staticmethod(setsectname) 1114 def getnumber(level): 1115 '''Return next section number at section 'level' formatted like 1116 1.2.3.4.''' 1117 number = '' 1118 for l in range(len(Title.section_numbers)): 1119 n = Title.section_numbers[l] 1120 if l == 0: 1121 continue 1122 elif l < level: 1123 number = '%s%d.' % (number, n) 1124 elif l == level: 1125 number = '%s%d.' % (number, n + 1) 1126 Title.section_numbers[l] = n + 1 1127 elif l > level: 1128 # Reset unprocessed section levels. 1129 Title.section_numbers[l] = 0 1130 return number 1131 getnumber = staticmethod(getnumber) 1132 1133 1134class Section: 1135 '''Static methods and attributes only.''' 1136 endtags = [] # Stack of currently open section (level,endtag) tuples. 1137 def __init__(self): 1138 raise AssertionError,'no class instances allowed' 1139 def savetag(level,etag): 1140 '''Save section end.''' 1141 if Section.endtags: 1142 # Previous open section is up one level. 1143 # Check deprecated to allow out of sequence titles. 1144 #assert level == Section.endtags[-1][0] + 1 1145 pass 1146 else: 1147 # Top open section is level 1. 1148 # Check deprecated to allow level 0 (specifically book part) titles. 1149 #assert level == 1 1150 pass 1151 Section.endtags.append((level,etag)) 1152 savetag = staticmethod(savetag) 1153 def setlevel(level): 1154 '''Set document level and write open section close tags up to level.''' 1155 while Section.endtags and Section.endtags[-1][0] >= level: 1156 writer.write(Section.endtags.pop()[1]) 1157 document.level = level 1158 setlevel = staticmethod(setlevel) 1159 def translate(): 1160 assert Lex.next() is Title 1161 Title.parse() 1162 if Title.level == 0 and document.doctype != 'book': 1163 raise EAsciiDoc,'only book doctypes can contain level 0 sections' 1164 if Title.level > document.level+1: 1165 warning('section title out of sequence: ' \ 1166 'expected level %d, got level %d' \ 1167 % (document.level+1, Title.level)) 1168 Section.setlevel(Title.level) 1169 dict = {} 1170 dict['title'] = Title.title 1171 dict['sectnum'] = Title.getnumber(document.level) 1172 stag,etag = config.section2tags(Title.sectname,dict) 1173 Section.savetag(Title.level,etag) 1174 writer.write(stag) 1175 Section.translate_body() 1176 translate = staticmethod(translate) 1177 def translate_body(terminator=Title): 1178 isempty = 1 1179 next = Lex.next() 1180 while next and next is not terminator: 1181 if next is Title and isinstance(terminator,DelimitedBlock): 1182 raise EAsciiDoc,'title not permitted in sidebar body' 1183 if next is SectionClose and isinstance(terminator,DelimitedBlock): 1184 raise EAsciiDoc,'section closure not permitted in sidebar body' 1185 if document.backend == 'linuxdoc' \ 1186 and document.level == 0 \ 1187 and not isinstance(next,Paragraph): 1188 warning('only paragraphs are permitted in linuxdoc synopsis') 1189 next.translate() 1190 next = Lex.next() 1191 isempty = 0 1192 # The section is not empty if contains a subsection. 1193 if next and isempty and Title.level > document.level: 1194 isempty = 0 1195 if isempty: 1196 warning('empty section') 1197 translate_body = staticmethod(translate_body) 1198 1199class SectionClose: 1200 def __init__(self): 1201 raise AssertionError,'no class instances allowed' 1202 def isnext(): 1203 line = reader.read_next() 1204 return line and line in Title.titles 1205 isnext = staticmethod(isnext) 1206 def translate(): 1207 assert Lex.next() is SectionClose 1208 line = reader.read() 1209 i = list(Title.titles).index(line) 1210 if i == 0: 1211 raise EAsciiDoc,'level 0 document closure not permitted' 1212 elif i > document.level: 1213 warning('unable to close section: level %d is not open' % (i,)) 1214 else: 1215 Section.setlevel(i) 1216 translate = staticmethod(translate) 1217 1218class Paragraphs: 1219 '''List of paragraph definitions.''' 1220 def __init__(self): 1221 self.current=None 1222 self.paragraphs = [] # List of Paragraph objects. 1223 self.default = None # The default [paradef-default] paragraph. 1224 def load(self,sections): 1225 '''Update paragraphs defined in 'sections' dictionary.''' 1226 for k in sections.keys(): 1227 if re.match(r'^paradef.+$',k): 1228 dict = {} 1229 parse_entries(sections.get(k,()),dict) 1230 for p in self.paragraphs: 1231 if p.name == k: break 1232 else: 1233 p = Paragraph() 1234 self.paragraphs.append(p) 1235 try: 1236 p.load(k,dict) 1237 except EAsciiDoc,e: 1238 raise EAsciiDoc,'[%s] %s' % (k,str(e)) 1239 def dump(self): 1240 for p in self.paragraphs: 1241 p.dump() 1242 def isnext(self): 1243 for p in self.paragraphs: 1244 if p.isnext(): 1245 self.current = p 1246 return 1; 1247 return 0 1248 def check(self): 1249 # Check all paragraphs have valid delimiter. 1250 for p in self.paragraphs: 1251 if not p.delimiter or not is_regexp(p.delimiter): 1252 raise EAsciiDoc,'[%s] missing or illegal delimiter' % (p.name,) 1253 # Check all paragraph sections exist. 1254 for p in self.paragraphs: 1255 if not p.section: 1256 warning('[%s] missing section entry' % (p.name,)) 1257 if not config.sections.has_key(p.section): 1258 warning('[%s] missing paragraph section' % (p.section,)) 1259 # Check we have a default paragraph definition, put it last in list. 1260 for i in range(len(self.paragraphs)): 1261 if self.paragraphs[i].name == 'paradef-default': 1262 p = self.paragraphs[i] 1263 del self.paragraphs[i] 1264 self.paragraphs.append(p) 1265 self.default = p 1266 break 1267 else: 1268 raise EAsciiDoc,'missing [paradef-default] section' 1269 1270class Paragraph: 1271 OPTIONS = ('listelement',) 1272 def __init__(self): 1273 self.name=None # Configuration file section name. 1274 self.delimiter=None # Regular expression matching paragraph delimiter. 1275 self.section=None # Name of section defining paragraph start/end tags. 1276 self.options=() # List of paragraph option names. 1277 self.presubs=SUBS_DEFAULT # List of pre-filter substitution option names. 1278 self.postsubs=() # List of post-filter substitution option names. 1279 self.filter=None # Executable paragraph filter command. 1280 def load(self,name,dict): 1281 '''Update paragraph definition from section entries in 'dict'.''' 1282 self.name = name 1283 for k,v in dict.items(): 1284 if k == 'delimiter': 1285 if v and is_regexp(v): 1286 self.delimiter = v 1287 else: 1288 raise EAsciiDoc,'malformed paragraph delimiter "%s"' % (v,) 1289 elif k == 'section': 1290 if is_name(v): 1291 self.section = v 1292 else: 1293 raise EAsciiDoc,'malformed paragraph section name "%s"' \ 1294 % (v,) 1295 elif k == 'options': 1296 self.options = parse_options(v,Paragraph.OPTIONS, 1297 'illegal Paragraph %s option' % (k,)) 1298 elif k == 'presubs': 1299 self.presubs = parse_options(v,SUBS_OPTIONS, 1300 'illegal Paragraph %s option' % (k,)) 1301 elif k == 'postsubs': 1302 self.postsubs = parse_options(v,SUBS_OPTIONS, 1303 'illegal Paragraph %s option' % (k,)) 1304 elif k == 'filter': 1305 self.filter = v 1306 else: 1307 raise EAsciiDoc,'illegal paragraph parameter name "%s"' % (k,) 1308 def dump(self): 1309 write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline)) 1310 write('['+self.name+']') 1311 write('delimiter='+self.delimiter) 1312 if self.section: 1313 write('section='+self.section) 1314 if self.options: 1315 write('options='+string.join(self.options,',')) 1316 if self.presubs: 1317 write('presubs='+string.join(self.presubs,',')) 1318 if self.postsubs: 1319 write('postsubs='+string.join(self.postsubs,',')) 1320 if self.filter: 1321 write('filter='+self.filter) 1322 write('') 1323 def isnext(self): 1324 reader.skip_blank_lines() 1325 if reader.read_next(): 1326 return re.match(self.delimiter,reader.read_next()) 1327 else: 1328 return 0 1329 def parse_delimiter_text(self): 1330 '''Return the text in the paragraph delimiter line.''' 1331 delimiter = reader.read() 1332 mo = re.match(self.delimiter,delimiter) 1333 assert mo 1334 result = mo.groupdict().get('text') 1335 if result is None: 1336 raise EAsciiDoc,'no text group in [%s] delimiter' % (self.name,) 1337 return result 1338 def write_body(self,body): 1339 dict = {} 1340 BlockTitle.gettitle(dict) 1341 stag,etag = config.section2tags(self.section,dict) 1342 # Writes blank line if the tag is empty (to separate LinuxDoc 1343 # paragraphs). 1344 if not stag: stag = [''] 1345 if not etag: etag = [''] 1346 writer.write(list(stag)+list(body)+list(etag)) 1347 def translate(self): 1348 line1 = self.parse_delimiter_text() 1349 # The next line introduces the requirement that a List cannot 1350 # immediately follow a preceding Paragraph (introduced in v3.2.2). 1351 body = reader.read_until(r'^$|'+blocks.delimiter+r'|'+tables.delimiter) 1352 body = [line1] + list(body) 1353 body = join_lines(body) 1354 body = Lex.set_margin(body) # Move body to left margin. 1355 body = Lex.subs(body,self.presubs) 1356 if self.filter: 1357 body = filter_lines(self.filter,body) 1358 body = Lex.subs(body,self.postsubs) 1359 self.write_body(body) 1360 1361class Lists: 1362 '''List of List objects.''' 1363 def __init__(self): 1364 self.current=None 1365 self.lists = [] # List objects. 1366 self.delimiter = '' # Combined blocks delimiter regular expression. 1367 self.open = [] # A stack of the current an parent lists. 1368 def load(self,sections): 1369 '''Update lists defined in 'sections' dictionary.''' 1370 for k in sections.keys(): 1371 if re.match(r'^listdef.+$',k): 1372 dict = {} 1373 parse_entries(sections.get(k,()),dict) 1374 for l in self.lists: 1375 if l.name == k: break 1376 else: 1377 l = List() # Create a new list if it doesn't exist. 1378 self.lists.append(l) 1379 try: 1380 l.load(k,dict) 1381 except EAsciiDoc,e: 1382 raise EAsciiDoc,'[%s] %s' % (k,str(e)) 1383 def dump(self): 1384 for l in self.lists: 1385 l.dump() 1386 def isnext(self): 1387 for l in self.lists: 1388 if l.isnext(): 1389 self.current = l 1390 return 1; 1391 return 0 1392 def check(self): 1393 for l in self.lists: 1394 # Check list has valid type . 1395 if not l.type in l.TYPES: 1396 raise EAsciiDoc,'[%s] illegal type' % (l.name,) 1397 # Check list has valid delimiter. 1398 if not l.delimiter or not is_regexp(l.delimiter): 1399 raise EAsciiDoc,'[%s] missing or illegal delimiter' % (l.name,) 1400 # Check all list tags. 1401 if not l.listtag or not config.tags.has_key(l.listtag): 1402 warning('[%s] missing listtag' % (l.name,)) 1403 if not l.itemtag or not config.tags.has_key(l.itemtag): 1404 warning('[%s] missing tag itemtag' % (l.name,)) 1405 if not l.texttag or not config.tags.has_key(l.texttag): 1406 warning('[%s] missing tag texttag' % (l.name,)) 1407 if l.type == 'variable': 1408 if not l.entrytag or not config.tags.has_key(l.entrytag): 1409 warning('[%s] missing entrytag' % (l.name,)) 1410 if not l.termtag or not config.tags.has_key(l.termtag): 1411 warning('[%s] missing termtag' % (l.name,)) 1412 # Build combined lists delimiter pattern. 1413 delimiters = [] 1414 for l in self.lists: 1415 delimiters.append(l.delimiter) 1416 self.delimiter = join_regexp(delimiters) 1417 1418class List: 1419 TAGS = ('listtag','itemtag','texttag','entrytag','termtag') 1420 TYPES = ('simple','variable') 1421 def __init__(self): 1422 self.name=None # List definition configuration file section name. 1423 self.type=None # 'simple' or 'variable' 1424 self.delimiter=None # Regular expression matching list item delimiter. 1425 self.subs=SUBS_DEFAULT # List of substitution option names. 1426 self.listtag=None 1427 self.itemtag=None 1428 self.texttag=None # Tag for list item text. 1429 self.termtag=None # Variable lists only. 1430 self.entrytag=None # Variable lists only. 1431 def load(self,name,dict): 1432 '''Update block definition from section entries in 'dict'.''' 1433 self.name = name 1434 for k,v in dict.items(): 1435 if k == 'type': 1436 if v in self.TYPES: 1437 self.type = v 1438 else: 1439 raise EAsciiDoc,'illegal list type "%s"' % (v,) 1440 elif k == 'delimiter': 1441 if v and is_regexp(v): 1442 self.delimiter = v 1443 else: 1444 raise EAsciiDoc,'malformed list delimiter "%s"' % (v,) 1445 elif k == 'subs': 1446 self.subs = parse_options(v,SUBS_OPTIONS, 1447 'illegal List %s option' % (k,)) 1448 elif k in self.TAGS: 1449 if is_name(v): 1450 setattr(self,k,v) 1451 else: 1452 raise EAsciiDoc,'illegal list %s name "%s"' % (k,v) 1453 else: 1454 raise EAsciiDoc,'illegal list parameter name "%s"' % (k,) 1455 def dump(self): 1456 write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline)) 1457 write('['+self.name+']') 1458 write('type='+self.type) 1459 write('delimiter='+self.delimiter) 1460 if self.subs: 1461 write('subs='+string.join(self.subs,',')) 1462 write('listtag='+self.listtag) 1463 write('itemtag='+self.itemtag) 1464 write('texttag='+self.texttag) 1465 if self.type == 'variable': 1466 write('entrytag='+self.entrytag) 1467 write('termtag='+self.termtag) 1468 write('') 1469 def isnext(self): 1470 reader.skip_blank_lines() 1471 if reader.read_next(): 1472 return re.match(self.delimiter,reader.read_next()) 1473 else: 1474 return 0 1475 def parse_delimiter_text(self): 1476 '''Return the text in the list delimiter line.''' 1477 delimiter = reader.read() 1478 mo = re.match(self.delimiter,delimiter) 1479 assert mo 1480 result = mo.groupdict().get('text') 1481 if result is None: 1482 raise EAsciiDoc,'no text group in [%s] delimiter' % (self.name,) 1483 return result 1484 def translate_entry(self): 1485 assert self.type == 'variable' 1486 stag,etag = config.tag(self.entrytag) 1487 if stag: writer.write(stag) 1488 # Write terms. 1489 while Lex.next() is self: 1490 term = self.parse_delimiter_text() 1491 writer.write_tag(self.termtag,[term],self.subs) 1492 # Write definition. 1493 self.translate_item() 1494 if etag: writer.write(etag) 1495 def translate_item(self,first_line=None): 1496 stag,etag = config.tag(self.itemtag) 1497 if stag: writer.write(stag) 1498 # Write ItemText. 1499 text = reader.read_until(lists.delimiter+'|^$|'+blocks.delimiter \ 1500 +r'|'+tables.delimiter) 1501 if first_line is not None: 1502 text = [first_line] + list(text) 1503 text = join_lines(text) 1504 writer.write_tag(self.texttag,text,self.subs) 1505 # Process nested ListParagraphs and Lists. 1506 while 1: 1507 next = Lex.next() 1508 if next in lists.open: 1509 break 1510 elif isinstance(next,List): 1511 next.translate() 1512 elif isinstance(next,Paragraph) and 'listelement' in next.options: 1513 next.translate() 1514 else: 1515 break 1516 if etag: writer.write(etag) 1517 def translate(self): 1518 lists.open.append(self) 1519 stag,etag = config.tag(self.listtag) 1520 if stag: 1521 dict = {} 1522 BlockTitle.gettitle(dict) 1523 stag = subs_glossary([stag],dict)[0] 1524 writer.write(stag) 1525 while Lex.next() is self: 1526 if self.type == 'simple': 1527 first_line = self.parse_delimiter_text() 1528 self.translate_item(first_line) 1529 elif self.type == 'variable': 1530 self.translate_entry() 1531 else: 1532 raise AssertionError,'illegal [%s] list type"' % (self.name,) 1533 if etag: 1534 writer.write(etag) 1535 lists.open.pop() 1536 1537 1538class DelimitedBlocks: 1539 '''List of delimited blocks.''' 1540 def __init__(self): 1541 self.current=None 1542 self.blocks = [] # List of DelimitedBlock objects. 1543 self.delimiter = '' # Combined blocks delimiter regular expression. 1544 def load(self,sections): 1545 '''Update blocks defined in 'sections' dictionary.''' 1546 for k in sections.keys(): 1547 if re.match(r'^blockdef.+$',k): 1548 dict = {} 1549 parse_entries(sections.get(k,()),dict) 1550 for b in self.blocks: 1551 if b.name == k: break 1552 else: 1553 b = DelimitedBlock() 1554 self.blocks.append(b) 1555 try: 1556 b.load(k,dict) 1557 except EAsciiDoc,e: 1558 raise EAsciiDoc,'[%s] %s' % (k,str(e)) 1559 def dump(self): 1560 for b in self.blocks: 1561 b.dump() 1562 def isnext(self): 1563 for b in self.blocks: 1564 if b.isnext(): 1565 self.current = b 1566 return 1; 1567 return 0 1568 def check(self): 1569 # Check all blocks have valid delimiter. 1570 for b in self.blocks: 1571 if not b.delimiter or not is_regexp(b.delimiter): 1572 raise EAsciiDoc,'[%s] missing or illegal delimiter' % (b.name,) 1573 # Check all block sections exist. 1574 for b in self.blocks: 1575 if 'skip' not in b.options: 1576 if not b.section: 1577 warning('[%s] missing section entry' % (b.name,)) 1578 if not config.sections.has_key(b.section): 1579 warning('[%s] missing block section' % (b.section,)) 1580 # Build combined block delimiter pattern. 1581 delimiters = [] 1582 for b in self.blocks: 1583 delimiters.append(b.delimiter) 1584 self.delimiter = join_regexp(delimiters) 1585 1586class DelimitedBlock: 1587 OPTIONS = ('section','skip','argsline') 1588 def __init__(self): 1589 self.name=None # Block definition configuration file section name. 1590 self.delimiter=None # Regular expression matching block delimiter. 1591 self.section=None # Name of section defining block header/footer. 1592 self.options=() # List of block option names. 1593 self.presubs=() # List of pre-filter substitution option names. 1594 self.postsubs=() # List of post-filter substitution option names. 1595 self.filter=None # Executable block filter command. 1596 def load(self,name,dict): 1597 '''Update block definition from section entries in 'dict'.''' 1598 self.name = name 1599 for k,v in dict.items(): 1600 if k == 'delimiter': 1601 if v and is_regexp(v): 1602 self.delimiter = v 1603 else: 1604 raise EAsciiDoc,'malformed block delimiter "%s"' % (v,) 1605 elif k == 'section': 1606 if is_name(v): 1607 self.section = v 1608 else: 1609 raise EAsciiDoc,'malformed block section name "%s"' % (v,) 1610 elif k == 'options': 1611 self.options = parse_options(v,DelimitedBlock.OPTIONS, 1612 'illegal DelimitedBlock %s option' % (k,)) 1613 elif k == 'presubs': 1614 self.presubs = parse_options(v,SUBS_OPTIONS, 1615 'illegal DelimitedBlock %s option' % (k,)) 1616 elif k == 'postsubs': 1617 self.postsubs = parse_options(v,SUBS_OPTIONS, 1618 'illegal DelimitedBlock %s option' % (k,)) 1619 elif k == 'filter': 1620 self.filter = v 1621 else: 1622 raise EAsciiDoc,'illegal block parameter name "%s"' % (k,) 1623 def dump(self): 1624 write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline)) 1625 write('['+self.name+']') 1626 write('delimiter='+self.delimiter) 1627 if self.section: 1628 write('section='+self.section) 1629 if self.options: 1630 write('options='+string.join(self.options,',')) 1631 if self.presubs: 1632 write('presubs='+string.join(self.presubs,',')) 1633 if self.postsubs: 1634 write('postsubs='+string.join(self.postsubs,',')) 1635 if self.filter: 1636 write('filter='+self.filter) 1637 write('') 1638 def isnext(self): 1639 reader.skip_blank_lines() 1640 if reader.read_next(): 1641 return re.match(self.delimiter,reader.read_next()) 1642 else: 1643 return 0 1644 def translate(self): 1645 dict = {} 1646 BlockTitle.gettitle(dict) 1647 delimiter = reader.read() 1648 mo = re.match(self.delimiter,delimiter) 1649 assert mo 1650 dict.update(mo.groupdict()) 1651 for k,v in dict.items(): 1652 if v is None: del dict[k] 1653 if dict.has_key('args'): 1654 # Extract embedded arguments from leading delimiter line. 1655 parse_args(dict['args'],dict) 1656 elif 'argsline' in self.options: 1657 # Parse block arguments line. 1658 reader.parse_arguments(dict) 1659 # Process block contents. 1660 if 'skip' in self.options: 1661 # Discard block body. 1662 reader.read_until(self.delimiter,same_file=1) 1663 elif 'section' in self.options: 1664 stag,etag = config.section2tags(self.section,dict) 1665 # The body is treated like a SimpleSection. 1666 writer.write(stag) 1667 Section.translate_body(self) 1668 writer.write(etag) 1669 else: 1670 stag,etag = config.section2tags(self.section,dict) 1671 body = reader.read_until(self.delimiter,same_file=1) 1672 body = Lex.subs(body,self.presubs) 1673 if self.filter: 1674 body = filter_lines(self.filter,body,dict) 1675 body = Lex.subs(body,self.postsubs) 1676 # Write start tag, content, end tag. 1677 writer.write(list(stag)+list(body)+list(etag)) 1678 if reader.eof(): 1679 raise EAsciiDoc,'closing [%s] delimiter expected' % (self.name,) 1680 delimiter = reader.read() # Discard delimiter line. 1681 assert re.match(self.delimiter,delimiter) 1682 1683 1684class Tables: 1685 '''List of tables.''' 1686 def __init__(self): 1687 self.current=None 1688 self.tables = [] # List of Table objects. 1689 self.delimiter = '' # Combined tables delimiter regular expression. 1690 def load(self,sections): 1691 '''Update tables defined in 'sections' dictionary.''' 1692 for k in sections.keys(): 1693 if re.match(r'^tabledef.+$',k): 1694 dict = {} 1695 parse_entries(sections.get(k,()),dict) 1696 for t in self.tables: 1697 if t.name == k: break 1698 else: 1699 t = Table() 1700 self.tables.append(t) 1701 try: 1702 t.load(k,dict) 1703 except EAsciiDoc,e: 1704 raise EAsciiDoc,'[%s] %s' % (k,str(e)) 1705 def dump(self): 1706 for t in self.tables: 1707 t.dump() 1708 def isnext(self): 1709 for t in self.tables: 1710 if t.isnext(): 1711 self.current = t 1712 return 1; 1713 return 0 1714 def check(self): 1715 # Check we have a default table definition, 1716 for i in range(len(self.tables)): 1717 if self.tables[i].name == 'tabledef-default': 1718 default = self.tables[i] 1719 break 1720 else: 1721 raise EAsciiDoc,'missing [table-default] section' 1722 # Set default table defaults. 1723 if default.subs is None: default.subs = SUBS_DEFAULT 1724 if default.format is None: default.subs = 'fixed' 1725 # Propagate defaults to unspecified table parameters. 1726 for t in self.tables: 1727 if t is not default: 1728 if t.fillchar is None: t.fillchar = default.fillchar 1729 if t.subs is None: t.subs = default.subs 1730 if t.format is None: t.format = default.format 1731 if t.section is None: t.section = default.section 1732 if t.colspec is None: t.colspec = default.colspec 1733 if t.headrow is None: t.headrow = default.headrow 1734 if t.footrow is None: t.footrow = default.footrow 1735 if t.bodyrow is None: t.bodyrow = default.bodyrow 1736 if t.headdata is None: t.headdata = default.headdata 1737 if t.footdata is None: t.footdata = default.footdata 1738 if t.bodydata is None: t.bodydata = default.bodydata 1739 # Check all tables have valid fill character. 1740 for t in self.tables: 1741 if not t.fillchar or len(t.fillchar) != 1: 1742 raise EAsciiDoc,'[%s] missing or illegal fillchar' % (t.name,) 1743 # Build combined tables delimiter patterns and assign defaults. 1744 delimiters = [] 1745 for t in self.tables: 1746 # Ruler is: 1747 # (ColStop,(ColWidth,FillChar+)?)+, FillChar+, TableWidth? 1748 t.delimiter = r'^(' + Table.COL_STOP \ 1749 + r'(\d*|' + re.escape(t.fillchar) + r'*)' \ 1750 + r')+' \ 1751 + re.escape(t.fillchar) + r'+' \ 1752 + '([\d\.]*)$' 1753 delimiters.append(t.delimiter) 1754 if not t.headrow: 1755 t.headrow = t.bodyrow 1756 if not t.footrow: 1757 t.footrow = t.bodyrow 1758 if not t.headdata: 1759 t.headdata = t.bodydata 1760 if not t.footdata: 1761 t.footdata = t.bodydata 1762 self.delimiter = join_regexp(delimiters) 1763 # Check table definitions are valid. 1764 for t in self.tables: 1765 t.check() 1766 if t.check_msg: 1767 warning('[%s] table definition: %s' % (t.name,t.check_msg)) 1768 1769class Column: 1770 '''Table column.''' 1771 def __init__(self): 1772 self.colalign = None # 'left','right','center' 1773 self.rulerwidth = None 1774 self.colwidth = None # Output width in page units. 1775 1776class Table: 1777 COL_STOP = r"(`|'|\.)" # RE. 1778 ALIGNMENTS = {'`':'left', "'":'right', '.':'center'} 1779 FORMATS = ('fixed','csv','dsv') 1780 def __init__(self): 1781 # Configuration parameters. 1782 self.name=None # Table definition configuration file section name. 1783 self.fillchar=None 1784 self.subs=None 1785 self.format=None # 'fixed','csv','dsv' 1786 self.section=None 1787 self.colspec=None 1788 self.headrow=None 1789 self.footrow=None 1790 self.bodyrow=None 1791 self.headdata=None 1792 self.footdata=None 1793 self.bodydata=None 1794 # Calculated parameters. 1795 self.delimiter=None # RE matching any table ruler. 1796 self.underline=None # RE matching current table underline. 1797 self.isnumeric=0 # True if numeric ruler, false if character ruler. 1798 self.tablewidth=None # Optional table width scale factor. 1799 self.columns=[] # List of Columns. 1800 self.dict={} # Substitutions dictionary. 1801 # Other. 1802 self.check_msg='' # Message set by previous self.check() call. 1803 def load(self,name,dict): 1804 '''Update table definition from section entries in 'dict'.''' 1805 self.name = name 1806 for k,v in dict.items(): 1807 if k == 'fillchar': 1808 if v and len(v) == 1: 1809 self.fillchar = v 1810 else: 1811 raise EAsciiDoc,'malformed table fillchar "%s"' % (v,) 1812 elif k == 'section': 1813 if is_name(v): 1814 self.section = v 1815 else: 1816 raise EAsciiDoc,'malformed table section name "%s"' % (v,) 1817 elif k == 'subs': 1818 self.subs = parse_options(v,SUBS_OPTIONS, 1819 'illegal Table %s option' % (k,)) 1820 elif k == 'format': 1821 if v in Table.FORMATS: 1822 self.format = v 1823 else: 1824 raise EAsciiDoc,'illegal table format "%s"' % (v,) 1825 elif k == 'colspec': 1826 self.colspec = v 1827 elif k == 'headrow': 1828 self.headrow = v 1829 elif k == 'footrow': 1830 self.footrow = v 1831 elif k == 'bodyrow': 1832 self.bodyrow = v 1833 elif k == 'headdata': 1834 self.headdata = v 1835 elif k == 'footdata': 1836 self.footdata = v 1837 elif k == 'bodydata': 1838 self.bodydata = v 1839 else: 1840 raise EAsciiDoc,'illegal table parameter name "%s"' % (k,) 1841 def dump(self): 1842 write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline)) 1843 write('['+self.name+']') 1844 write('fillchar='+self.fillchar) 1845 write('subs='+string.join(self.subs,',')) 1846 write('format='+self.format) 1847 write('section='+self.section) 1848 if self.colspec: 1849 write('colspec='+self.colspec) 1850 if self.headrow: 1851 write('headrow='+self.headrow) 1852 if self.footrow: 1853 write('footrow='+self.footrow) 1854 write('bodyrow='+self.bodyrow) 1855 if self.headdata: 1856 write('headdata='+self.headdata) 1857 if self.footdata: 1858 write('footdata='+self.footdata) 1859 write('bodydata='+self.bodydata) 1860 write('') 1861 def check(self): 1862 '''Check table definition and set self.check_msg if invalid else set 1863 self.check_msg to blank string.''' 1864 # Check global table parameters. 1865 if config.textwidth is None: 1866 self.check_msg = 'missing [miscellaneous] textwidth entry' 1867 elif config.pagewidth is None: 1868 self.check_msg = 'missing [miscellaneous] pagewidth entry' 1869 elif config.pageunits is None: 1870 self.check_msg = 'missing [miscellaneous] pageunits entry' 1871 elif not self.section: 1872 self.check_msg = 'missing section entry' 1873 elif not config.sections.has_key(self.section): 1874 self.check_msg = 'missing section %s' % (self.section,) 1875 elif self.headrow is None: 1876 self.check_msg = 'missing headrow entry' 1877 elif self.footrow is None: 1878 self.check_msg = 'missing footrow entry' 1879 elif self.bodyrow is None: 1880 self.check_msg = 'missing bodyrow entry' 1881 elif self.headdata is None: 1882 self.check_msg = 'missing headdata entry' 1883 elif self.footdata is None: 1884 self.check_msg = 'missing footdata entry' 1885 elif self.bodydata is None: 1886 self.check_msg = 'missing bodydata entry' 1887 else: 1888 # No errors. 1889 self.check_msg = '' 1890 1891 def isnext(self): 1892 reader.skip_blank_lines() 1893 if reader.read_next(): 1894 return re.match(self.delimiter,reader.read_next()) 1895 else: 1896 return 0 1897 def parse_ruler(self,ruler): 1898 '''Parse ruler calculating underline and ruler column widths.''' 1899 fc = re.escape(self.fillchar) 1900 # Strip and save optional tablewidth from end of ruler. 1901 mo = re.match(r'^(.*'+fc+r'+)([\d\.]+)$',ruler) 1902 if mo: 1903 ruler = mo.group(1) 1904 self.tablewidth = float(mo.group(2)) 1905 self.dict['tablewidth'] = str(float(self.tablewidth)) 1906 else: 1907 self.tablewidth = None 1908 self.dict['tablewidth'] = '100.0' 1909 # Guess whether column widths are specified numerically or not. 1910 if ruler[1] != self.fillchar: 1911 # If the first column does not start with a fillchar then numeric. 1912 self.isnumeric = 1 1913 elif ruler[1:] == self.fillchar*len(ruler[1:]): 1914 # The case of one column followed by fillchars is numeric. 1915 self.isnumeric = 1 1916 else: 1917 self.isnumeric = 0 1918 # Underlines must be 3 or more fillchars. 1919 self.underline = r'^' + fc + r'{3,}$' 1920 splits = re.split(self.COL_STOP,ruler)[1:] 1921 # Build self.columns. 1922 for i in range(0,len(splits),2): 1923 c = Column() 1924 c.colalign = self.ALIGNMENTS[splits[i]] 1925 s = splits[i+1] 1926 if self.isnumeric: 1927 # Strip trailing fillchars. 1928 s = re.sub(fc+r'+$','',s) 1929 if s == '': 1930 c.rulerwidth = None 1931 else: 1932 c.rulerwidth = int(validate(s,'int($)>0', 1933 'malformed ruler: bad width')) 1934 else: # Calculate column width from inter-fillchar intervals. 1935 if not re.match(r'^'+fc+r'+$',s): 1936 raise EAsciiDoc,'malformed ruler: illegal fillchars' 1937 c.rulerwidth = len(s)+1 1938 self.columns.append(c) 1939 # Fill in unspecified ruler widths. 1940 if self.isnumeric: 1941 if self.columns[0].rulerwidth is None: 1942 prevwidth = 1 1943 for c in self.columns: 1944 if c.rulerwidth is None: 1945 c.rulerwidth = prevwidth 1946 prevwidth = c.rulerwidth 1947 def build_colspecs(self): 1948 '''Generate colwidths and colspecs. This can only be done after the 1949 table arguments have been parsed since we use the table format.''' 1950 self.dict['cols'] = len(self.columns) 1951 # Calculate total ruler width. 1952 totalwidth = 0 1953 for c in self.columns: 1954 totalwidth = totalwidth + c.rulerwidth 1955 if totalwidth <= 0: 1956 raise EAsciiDoc,'zero width table' 1957 # Calculate marked up colwidths from rulerwidths. 1958 for c in self.columns: 1959 # Convert ruler width to output page width. 1960 width = float(c.rulerwidth) 1961 if self.format == 'fixed': 1962 if self.tablewidth is None: 1963 # Size proportional to ruler width. 1964 colfraction = width/config.textwidth 1965 else: 1966 # Size proportional to page width. 1967 colfraction = width/totalwidth 1968 else: 1969 # Size proportional to page width. 1970 colfraction = width/totalwidth 1971 c.colwidth = colfraction * config.pagewidth # To page units. 1972 if self.tablewidth is not None: 1973 c.colwidth = c.colwidth * self.tablewidth # Scale factor. 1974 if self.tablewidth > 1: 1975 c.colwidth = c.colwidth/100 # tablewidth is in percent. 1976 # Build colspecs. 1977 if self.colspec: 1978 s = [] 1979 for c in self.columns: 1980 self.dict['colalign'] = c.colalign 1981 self.dict['colwidth'] = str(int(c.colwidth)) 1982 s.append(subs_glossary((self.colspec,),self.dict)[0]) 1983 self.dict['colspecs'] = string.join(s,writer.newline) 1984 def parse_arguments(self): 1985 '''Parse table arguments string.''' 1986 d = {} 1987 reader.parse_arguments(d) 1988 # Update table with overridable parameters. 1989 if d.has_key('subs'): 1990 self.subs = parse_options(d['subs'],SUBS_OPTIONS, 1991 'illegal table subs %s option' % ('subs',)) 1992 if d.has_key('format'): 1993 self.format = d['format'] 1994 if d.has_key('tablewidth'): 1995 self.tablewidth = float(d['tablewidth']) 1996 # Add arguments to markup substitutions. 1997 self.dict.update(d) 1998 def split_rows(self,rows): 1999 '''Return a list of lines up to but not including the next underline. 2000 Continued lines are joined.''' 2001 reo = re.compile(self.underline) 2002 i = 0 2003 while not reo.match(rows[i]): 2004 i = i+1 2005 if i == 0: 2006 raise EAsciiDoc,'missing [%s] table rows' % (self.name,) 2007 if i >= len(rows): 2008 raise EAsciiDoc,'closing [%s] underline expected' % (self.name,) 2009 return (join_lines(rows[:i]), rows[i+1:]) 2010 def parse_rows(self, rows, rtag, dtag): 2011 '''Parse rows list using the row and data tags. Returns a substituted 2012 list of output lines.''' 2013 result = [] 2014 # Source rows are parsed as single block, rather than line by line, to 2015 # allow the CSV reader to handle multi-line rows. 2016 if self.format == 'fixed': 2017 rows = self.parse_fixed(rows) 2018 elif self.format == 'csv': 2019 rows = self.parse_csv(rows) 2020 elif self.format == 'dsv': 2021 rows = self.parse_dsv(rows) 2022 else: 2023 assert 1,'illegal table format' 2024 # Substitute and indent all data in all rows. 2025 stag,etag = subs_tag(rtag,self.dict) 2026 for row in rows: 2027 result.append(' '+stag) 2028 for data in self.subs_row(row,dtag): 2029 result.append(' '+data) 2030 result.append(' '+etag) 2031 return result 2032 def subs_row(self, data, dtag): 2033 '''Substitute the list of source row data elements using the data tag. 2034 Returns a substituted list of output table data items.''' 2035 result = [] 2036 if len(data) < len(self.columns): 2037 warning('fewer row data items then table columns') 2038 if len(data) > len(self.columns): 2039 warning('more row data items than table columns') 2040 for i in range(len(self.columns)): 2041 if i > len(data) - 1: 2042 d = '' # Fill missing column data with blanks. 2043 else: 2044 d = data[i] 2045 c = self.columns[i] 2046 self.dict['colalign'] = c.colalign 2047 self.dict['colwidth'] = str(int(c.colwidth)) + config.pageunits 2048 stag,etag = subs_tag(dtag,self.dict) 2049 # Insert AsciiDoc line break (' +') where row data has newlines 2050 # ('\n'). This is really only useful when the table format is csv 2051 # and the output markup is HTML. It's also a bit dubious in that it 2052 # assumes the user has not modified the shipped line break pattern. 2053 if 'replacements' in self.subs: 2054 # Insert line breaks in cell data. 2055 d = re.sub(r'(?m)\n',r' +\n',d) 2056 d = string.split(d,'\n') # So writer.newline is written. 2057 else: 2058 d = [d] 2059 result = result + [stag] + Lex.subs(d,self.subs) + [etag] 2060 return result 2061 def parse_fixed(self,rows): 2062 '''Parse the list of source table rows. Each row item in the returned 2063 list contains a list of cell data elements.''' 2064 result = [] 2065 for row in rows: 2066 data = [] 2067 start = 0 2068 for c in self.columns: 2069 end = start + c.rulerwidth 2070 if c is self.columns[-1]: 2071 # Text in last column can continue forever. 2072 data.append(string.strip(row[start:])) 2073 else: 2074 data.append(string.strip(row[start:end])) 2075 start = end 2076 result.append(data) 2077 return result 2078 def parse_csv(self,rows): 2079 '''Parse the list of source table rows. Each row item in the returned 2080 list contains a list of cell data elements.''' 2081 import StringIO 2082 try: 2083 import csv 2084 except: 2085 raise EAsciiDoc,'python 2.3 or better required to parse csv tables' 2086 result = [] 2087 rdr = csv.reader(StringIO.StringIO(string.join(rows,'\n'))) 2088 try: 2089 for row in rdr: 2090 result.append(row) 2091 except: 2092 raise EAsciiDoc,'csv parse error "%s"' % (row,) 2093 return result 2094 def parse_dsv(self,rows): 2095 '''Parse the list of source table rows. Each row item in the returned 2096 list contains a list of cell data elements.''' 2097 separator = self.dict.get('separator',':') 2098 separator = eval('"'+separator+'"') 2099 if len(separator) != 1: 2100 raise EAsciiDoc,'malformed dsv separator: %s' % (separator,) 2101 # TODO If separator is preceeded by and odd number of backslashes then 2102 # it is escaped and should not delimit. 2103 result = [] 2104 for row in rows: 2105 # Unescape escaped characters. 2106 row = eval('"'+string.replace(row,'"','\\"')+'"') 2107 data = string.split(row,separator) 2108 result.append(data) 2109 return result 2110 def translate(self): 2111 # Reset instance specific properties. 2112 self.underline = None 2113 self.columns = [] 2114 self.dict = {} 2115 BlockTitle.gettitle(self.dict) 2116 # Add relevant globals to table substitutions. 2117 self.dict['pagewidth'] = str(config.pagewidth) 2118 self.dict['pageunits'] = config.pageunits 2119 # Save overridable table parameters. 2120 save_subs = self.subs 2121 save_format = self.format 2122 # Parse table ruler. 2123 ruler = reader.read() 2124 assert re.match(self.delimiter,ruler) 2125 self.parse_ruler(ruler) 2126 # Parse table arguments. 2127 self.parse_arguments() 2128 # Read the entire table. 2129 table = reader.read_until(r'^$') # Tables terminated by blank line. 2130 if len(table) < 1 or not re.match(self.underline,table[-1]): 2131 raise EAsciiDoc,'closing [%s] underline expected' % (self.name,) 2132 if self.check_msg: 2133 warning('skipping %s table: %s' % (self.name,self.check_msg)) 2134 return 2135 # Generate colwidths and colspecs. 2136 self.build_colspecs() 2137 # Generate headrows, footrows, bodyrows. 2138 headrows = footrows = [] 2139 bodyrows,table = self.split_rows(table) 2140 if table: 2141 headrows = bodyrows 2142 bodyrows,table = self.split_rows(table) 2143 if table: 2144 footrows,table = self.split_rows(table) 2145 if headrows: 2146 headrows = self.parse_rows(headrows, self.headrow, self.headdata) 2147 self.dict['headrows'] = string.join(headrows,writer.newline) 2148 if footrows: 2149 footrows = self.parse_rows(footrows, self.footrow, self.footdata) 2150 self.dict['footrows'] = string.join(footrows,writer.newline) 2151 bodyrows = self.parse_rows(bodyrows, self.bodyrow, self.bodydata) 2152 self.dict['bodyrows'] = string.join(bodyrows,writer.newline) 2153 table = subs_glossary(config.sections[self.section],self.dict) 2154 writer.write(table) 2155 # Restore overridable table parameters. 2156 self.subs = save_subs 2157 self.format = save_format 2158 2159 2160class Macros: 2161 def __init__(self): 2162 self.macros = [] # List of Macros. 2163 self.current = None # The last matched block macro. 2164 def load(self,entries): 2165 for entry in entries: 2166 m = Macro() 2167 m.load(entry) 2168 if m.name is None: 2169 # Delete undefined macro. 2170 for i in range(len(self.macros)-1,-1,-1): 2171 if self.macros[i].pattern == m.pattern: 2172 del self.macros[i] 2173 else: 2174 # Check for duplicates. 2175 for m2 in self.macros: 2176 if m.equals(m2): 2177 warning('duplicate macro: '+entry) 2178 break 2179 else: 2180 self.macros.append(m) 2181 def dump(self): 2182 write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline)) 2183 write('[macros]') 2184 for m in self.macros: 2185 write('%s=%s%s' % (m.pattern,m.prefix,m.name)) 2186 write('') 2187 def check(self): 2188 # Check all named sections exist. 2189 for m in self.macros: 2190 if m.name and m.prefix != '+' \ 2191 and not config.sections.has_key(m.name): 2192 warning('missing macro section: [%s]' % (m.name,)) 2193 def subs(self,text,prefix=''): 2194 result = text 2195 for m in self.macros: 2196 if m.prefix == prefix: 2197 result = m.subs(result) 2198 return result 2199 def isnext(self): 2200 '''Return matching macro if block macro is next on reader.''' 2201 reader.skip_blank_lines() 2202 line = reader.read_next() 2203 if line: 2204 for m in self.macros: 2205 if m.prefix == '#': 2206 if m.reo.match(line): 2207 self.current = m 2208 return m 2209 return 0 2210 def match(self,prefix,name,text): 2211 '''Return re match object matching 'text' with macro type 'prefix', 2212 macro name 'name'.''' 2213 for m in self.macros: 2214 if m.prefix == prefix: 2215 mo = m.reo.match(text) 2216 if mo: 2217 if m.name == name: 2218 return mo 2219 if re.match(name,mo.group('name')): 2220 return mo 2221 return None 2222 2223# Macro set just prior to calling _subs_macro(). Ugly but there's no way 2224# to pass optional arguments with _subs_macro(). 2225_macro = None 2226 2227def _subs_macro(mo): 2228 '''Function called to perform inline macro substitution. Uses matched macro 2229 regular expression object and returns string containing the substituted 2230 macro body. Called by Macros().subs().''' 2231 # Check if macro reference is escaped. 2232 if mo.group()[0] == '\\': 2233 return mo.group()[1:] # Strip leading backslash. 2234 dict = mo.groupdict() 2235 # Delete groups that didn't participate in match. 2236 for k,v in dict.items(): 2237 if v is None: del dict[k] 2238 if _macro.name: 2239 name = _macro.name 2240 else: 2241 if not dict.has_key('name'): 2242 warning('missing macro name group: %s' % (mo.re.pattern,)) 2243 return '' 2244 name = dict['name'] 2245 # If we're dealing with a block macro get optional block title. 2246 if _macro.prefix == '#': 2247 BlockTitle.gettitle(dict) 2248 # Parse macro caption to macro arguments. 2249 assert dict.has_key('caption') and dict['caption'] is not None 2250 if dict['caption'] == '': 2251 del dict['caption'] 2252 else: 2253 parse_args(dict['caption'],dict) 2254 body = config.subs_section(name,dict) 2255 if len(body) == 0: 2256 result = '' 2257 elif len(body) == 1: 2258 result = body[0] 2259 else: 2260 result = string.join(body,writer.newline) 2261 return result 2262 2263class Macro: 2264 def __init__(self): 2265 self.pattern = None # Matching regular expression. 2266 self.name = '' # Conf file section name (None if implicit). 2267 self.prefix = '' # '' if inline, '+' if builtin, '#' if block. 2268 self.reo = None # Compiled pattern re object. 2269 def equals(self,m): 2270 if self.pattern != m.pattern: 2271 return 0 2272 if self.name != m.name: 2273 return 0 2274 if self.prefix != m.prefix: 2275 return 0 2276 return 1 2277 def load(self,entry): 2278 e = parse_entry(entry) 2279 if not e: 2280 raise EAsciiDoc,'malformed macro entry "%s"' % (entry,) 2281 self.pattern, self.name = e 2282 if not is_regexp(self.pattern): 2283 raise EAsciiDoc,'illegal regular expression in macro entry "%s"' \ 2284 % (entry,) 2285 self.reo = re.compile(self.pattern) 2286 if self.name: 2287 if self.name[0] in ('+','#'): 2288 self.prefix, self.name = self.name[0], self.name[1:] 2289 if self.name and not is_name(self.name): 2290 raise EAsciiDoc,'illegal section name in macro entry "%s"' % \ 2291 (entry,) 2292 def subs(self,text): 2293 global _macro 2294 _macro = self # Pass the macro to _subs_macro(). 2295 return self.reo.sub(_subs_macro,text) 2296 def translate(self): 2297 '''Translate block macro at reader.''' 2298 assert self.prefix == '#' 2299 line = reader.read() 2300 writer.write(self.subs(line)) 2301 2302#--------------------------------------------------------------------------- 2303# Input stream Reader and output stream writer classes. 2304#--------------------------------------------------------------------------- 2305 2306class Reader1: 2307 '''Line oriented AsciiDoc input file reader. Processes non lexical 2308 entities: transparently handles included files. Tabs are expanded and lines 2309 are right trimmed.''' 2310 # This class is not used directly, use Reader class instead. 2311 READ_BUFFER_MIN = 10 # Read buffer low level. 2312 def __init__(self): 2313 self.f = None # Input file object. 2314 self.fname = None # Input file name. 2315 self.next = [] # Read ahead buffer containing 2316 # (filename,linenumber,linetext) tuples. 2317 self.cursor = None # Last read() (filename,linenumber,linetext). 2318 self.tabsize = 8 # Tab expansion number of spaces. 2319 self.parent = None # Included reader's parent reader. 2320 self._lineno = 0 # The last line read from file object f. 2321 self.include_enabled = 1 # Enables/disables file inclusion. 2322 self.include_depth = 0 # Current include depth. 2323 self.include_max = 5 # Maxiumum allowed include depth. 2324 def open(self,fname): 2325 self.fname = fname 2326 verbose('reading: '+fname) 2327 if fname == '<stdin>': 2328 self.f = sys.stdin 2329 else: 2330 self.f = open(fname,"rb") 2331 self._lineno = 0 # The last line read from file object f. 2332 self.next = [] 2333 # Prefill buffer by reading the first line and then pushing it back. 2334 if Reader1.read(self): 2335 self.unread(self.cursor) 2336 self.cursor = None 2337 def closefile(self): 2338 '''Used by class methods to close nested include files.''' 2339 self.f.close() 2340 self.next = [] 2341 def close(self): 2342 self.closefile() 2343 self.__init__() 2344 def read(self): 2345 '''Read next line. Return None if EOF. Expand tabs. Strip trailing 2346 white space. Maintain self.next read ahead buffer.''' 2347 # Top up buffer. 2348 if len(self.next) <= self.READ_BUFFER_MIN: 2349 s = self.f.readline() 2350 if s: 2351 self._lineno = self._lineno + 1 2352 while s: 2353 if self.tabsize != 0: 2354 s = string.expandtabs(s,self.tabsize) 2355 s = string.rstrip(s) 2356 self.next.append((self.fname,self._lineno,s)) 2357 if len(self.next) > self.READ_BUFFER_MIN: 2358 break 2359 s = self.f.readline() 2360 if s: 2361 self._lineno = self._lineno + 1 2362 # Return first (oldest) buffer entry. 2363 if len(self.next) > 0: 2364 self.cursor = self.next[0] 2365 del self.next[0] 2366 result = self.cursor[2] 2367 # Check for include macro. 2368 mo = macros.match('+',r'include.?',result) 2369 if mo and self.include_enabled: 2370 # Perform glossary substitution on inlcude macro. 2371 a = subs_glossary([mo.group('target')]) 2372 # If undefined glossary entry then skip to next line of input. 2373 if not a: 2374 return Reader1.read(self) 2375 fname = a[0] 2376 if self.include_depth >= self.include_max: 2377 raise EAsciiDoc,'maxiumum inlude depth exceeded' 2378 if not os.path.isabs(fname) and self.fname != '<stdin>': 2379 # Include files are relative to parent document directory. 2380 fname = os.path.join(os.path.dirname(self.fname),fname) 2381 if self.fname != '<stdin>' and not os.path.isfile(fname): 2382 raise EAsciiDoc,'include file "%s" not found' % (fname,) 2383 # Parse include macro arguments. 2384 args = {} 2385 parse_args(mo.group('caption'),args) 2386 # Clone self and set as parent (self assumes the role of child). 2387 parent = Reader1() 2388 assign(parent,self) 2389 self.parent = parent 2390 if args.has_key('tabsize'): 2391 self.tabsize = int(validate(args['tabsize'],'int($)>=0', \ 2392 'illegal include macro tabsize argument')) 2393 # The include1 variant does not allow nested includes. 2394 if mo.group('name') == 'include1': 2395 self.include_enabled = 0 2396 self.open(fname) 2397 self.include_depth = self.include_depth + 1 2398 result = Reader1.read(self) 2399 else: 2400 if not Reader1.eof(self): 2401 result = Reader1.read(self) 2402 else: 2403 result = None 2404 return result 2405 def eof(self): 2406 '''Returns True if all lines have been read.''' 2407 if len(self.next) == 0: 2408 # End of current file. 2409 if self.parent: 2410 self.closefile() 2411 assign(self,self.parent) # Restore parent reader. 2412 return Reader1.eof(self) 2413 else: 2414 return 1 2415 else: 2416 return 0 2417 def read_next(self): 2418 '''Like read() but does not advance file pointer.''' 2419 if Reader1.eof(self): 2420 return None 2421 else: 2422 return self.next[0][2] 2423 def unread(self,cursor): 2424 '''Push the line (filename,linenumber,linetext) tuple back into the read 2425 buffer. Note that it's up to the caller to restore the previous 2426 cursor.''' 2427 assert cursor 2428 self.next.insert(0,cursor) 2429 2430class Reader(Reader1): 2431 ''' Wraps (well, sought of) Reader1 class and implements conditional text 2432 inclusion.''' 2433 def __init__(self): 2434 Reader1.__init__(self) 2435 self.depth = 0 # if nesting depth. 2436 self.skip = 0 # true if we're skipping ifdef...endif. 2437 self.skipname = '' # Name of current endif macro target. 2438 self.skipto = -1 # The depth at which skipping is reenabled. 2439 def read_super(self): 2440 result = Reader1.read(self) 2441 if result is None and self.skip: 2442 raise EAsciiDoc,'missing endif::%s[]' %(self.skipname,) 2443 return result 2444 def read(self): 2445 result = self.read_super() 2446 if result is None: 2447 return None 2448 while self.skip: 2449 mo = macros.match('+',r'ifdef|ifndef|endif',result) 2450 if mo: 2451 name = mo.group('name') 2452 target = mo.group('target') 2453 if name == 'endif': 2454 self.depth = self.depth-1 2455 if self.depth < 0: 2456 raise EAsciiDoc,'"%s" is mismatched' % (result,) 2457 if self.depth == self.skipto: 2458 self.skip = 0 2459 if target and self.skipname != target: 2460 raise EAsciiDoc,'"%s" is mismatched' % (result,) 2461 else: # ifdef or ifndef. 2462 if not target: 2463 raise EAsciiDoc,'"%s" missing macro target' % (result,) 2464 self.depth = self.depth+1 2465 result = self.read_super() 2466 if result is None: 2467 return None 2468 mo = macros.match('+',r'ifdef|ifndef|endif',result) 2469 if mo: 2470 name = mo.group('name') 2471 target = mo.group('target') 2472 if name == 'endif': 2473 self.depth = self.depth-1 2474 else: # ifdef or ifndef. 2475 if not target: 2476 raise EAsciiDoc,'"%s" missing macro target' % (result,) 2477 defined = document.glossary.get(target) is not None 2478 if name == 'ifdef': 2479 self.skip = not defined 2480 else: # ifndef. 2481 self.skip = defined 2482 if self.skip: 2483 self.skipto = self.depth 2484 self.skipname = target 2485 self.depth = self.depth+1 2486 result = self.read() 2487 return result 2488 def eof(self): 2489 return self.read_next() is None 2490 def read_next(self): 2491 save_cursor = self.cursor 2492 result = self.read() 2493 if result is not None: 2494 self.unread(self.cursor) 2495 self.cursor = save_cursor 2496 return result 2497 def read_all(self,fname): 2498 '''Read all lines from file fname and return as list. Use like class 2499 method: Reader().read_all(fname)''' 2500 result = [] 2501 self.open(fname) 2502 try: 2503 while not self.eof(): 2504 result.append(self.read()) 2505 finally: 2506 self.close() 2507 return result 2508 def read_lines(self,count=1): 2509 '''Return tuple containing count lines.''' 2510 result = [] 2511 i = 0 2512 while i < count and not self.eof(): 2513 result.append(self.read()) 2514 return tuple(result) 2515 def read_ahead(self,count=1): 2516 '''Same as read_lines() but does not advance the file pointer.''' 2517 result = [] 2518 putback = [] 2519 save_cursor = self.cursor 2520 try: 2521 i = 0 2522 while i < count and not self.eof(): 2523 result.append(self.read()) 2524 putback.append(self.cursor) 2525 i = i+1 2526 while putback: 2527 self.unread(putback.pop()) 2528 finally: 2529 self.cursor = save_cursor 2530 return tuple(result) 2531 def skip_blank_lines(self): 2532 reader.read_until(r'\s*\S+') 2533 def read_until(self,pattern,same_file=0): 2534 '''Like read() but reads lines up to (but not including) the first line 2535 that matches the pattern regular expression. If same_file is True 2536 then the terminating pattern must occur in the file the was being read 2537 when the routine was called.''' 2538 if same_file: 2539 fname = self.cursor[0] 2540 result = [] 2541 reo = re.compile(pattern) 2542 while not self.eof(): 2543 save_cursor = self.cursor 2544 s = self.read() 2545 if (not same_file or fname == self.cursor[0]) and reo.match(s): 2546 self.unread(self.cursor) 2547 self.cursor = save_cursor 2548 break 2549 result.append(s) 2550 return tuple(result) 2551 def read_continuation(self): 2552 '''Like read() but treats trailing backslash as line continuation 2553 character.''' 2554 s = self.read() 2555 if s is None: 2556 return None 2557 result = '' 2558 while s is not None and len(s) > 0 and s[-1] == '\\': 2559 result = result + s[:-1] 2560 s = self.read() 2561 if s is not None: 2562 result = result + s 2563 return result 2564 def parse_arguments(self,dict,default_arg=None): 2565 '''If an arguments line is in the reader parse it to dict.''' 2566 s = self.read_next() 2567 if s is not None: 2568 if s[:2] == '\\[': 2569 # Unescape next line. 2570 save_cursor = self.cursor 2571 self.read() 2572 self.cursor = self.cursor[0:2] + (s[1:],) 2573 self.unread(self.cursor) 2574 self.cursor = save_cursor 2575 elif re.match(r'^\[.*[\\\]]$',s): 2576 s = self.read_continuation() 2577 if not re.match(r'^\[.*\]$',s): 2578 warning('malformed arguments line') 2579 else: 2580 parse_args(s[1:-1],dict,default_arg) 2581 2582class Writer: 2583 '''Writes lines to output file.''' 2584 newline = '\r\n' # End of line terminator. 2585 f = None # Output file object. 2586 fname= None # Output file name. 2587 lines_out = 0 # Number of lines written. 2588 def open(self,fname): 2589 self.fname = fname 2590 verbose('writing: '+fname) 2591 if fname == '<stdout>': 2592 self.f = sys.stdout 2593 else: 2594 self.f = open(fname,"wb+") 2595 self.lines_out = 0 2596 def close(self,): 2597 if self.fname != '<stdout>': 2598 self.f.close() 2599 def write(self,*args): 2600 '''Iterates arguments, writes tuple and list arguments one line per 2601 element, else writes argument as single line. If no arguments writes 2602 blank line. self.newline is appended to each line.''' 2603 if len(args) == 0: 2604 self.f.write(self.newline) 2605 self.lines_out = self.lines_out + 1 2606 else: 2607 for arg in args: 2608 if type(arg) in (TupleType,ListType): 2609 for s in arg: 2610 self.f.write(s+self.newline) 2611 self.lines_out = self.lines_out + len(arg) 2612 else: 2613 self.f.write(arg+self.newline) 2614 self.lines_out = self.lines_out + 1 2615 def write_tag(self,tagname,content,subs=SUBS_DEFAULT): 2616 '''Write content enveloped by configuration file tag tagname. 2617 Substitutions specified in the 'subs' list are perform on the 2618 'content'.''' 2619 stag,etag = config.tag(tagname) 2620 self.write(stag,Lex.subs(content,subs),etag) 2621 2622#--------------------------------------------------------------------------- 2623# Configuration file processing. 2624#--------------------------------------------------------------------------- 2625def _subs_specialwords(mo): 2626 '''Special word substitution function called by 2627 Config.subs_specialwords().''' 2628 word = mo.re.pattern # The special word. 2629 macro = config.specialwords[word] # The corresponding inline macro. 2630 if not config.sections.has_key(macro): 2631 raise EAsciiDoc,'missing special word macro [%s]' % (macro,) 2632 args = {} 2633 args['words'] = mo.group() # The full match string is argument 'words'. 2634 args.update(mo.groupdict()) # Add named match groups to the arguments. 2635 # Delete groups that didn't participate in match. 2636 for k,v in args.items(): 2637 if v is None: del args[k] 2638 lines = subs_glossary(config.sections[macro],args) 2639 if len(lines) == 0: 2640 result = '' 2641 elif len(lines) == 1: 2642 result = lines[0] 2643 else: 2644 result = string.join(lines,writer.newline) 2645 return result 2646 2647class Config: 2648 '''Methods to process configuration files.''' 2649 # Predefined section name re's. 2650 SPECIAL_SECTIONS= ('tags','miscellaneous','glossary','specialcharacters', 2651 'specialwords','macros','replacements','quotes','titles', 2652 r'paradef.+',r'listdef.+',r'blockdef.+',r'tabledef.*') 2653 def __init__(self): 2654 self.sections = OrderedDict() # Keyed by section name containing 2655 # lists of section lines. 2656 # Command-line options. 2657 self.verbose = 0 2658 self.suppress_headers = 0 # -s option. 2659 # [miscellaneous] section. 2660 self.tabsize = 8 2661 self.textwidth = 70 2662 self.newline = '\r\n' 2663 self.pagewidth = None 2664 self.pageunits = None 2665 self.outfilesuffix = '' 2666 2667 self.tags = {} # Values contain (stag,etag) tuples. 2668 self.specialchars = {} # Values of special character substitutions. 2669 self.specialwords = {} # Name is special word pattern, value is macro. 2670 self.replacements = {} # Key is find pattern, value is replace pattern. 2671 self.specialsections = {} # Name is special section name pattern, value 2672 # is corresponding section name. 2673 self.quotes = {} # Values contain corresponding tag name. 2674 self.fname = '' # Most recently loaded configuration file name. 2675 self.conf_gloss = {} # Glossary entries from conf files. 2676 self.cmd_gloss = {} # From command-line -g option glossary entries. 2677 self.loaded = [] # Loaded conf files. 2678 2679 def load(self,fname,dir=None): 2680 '''Loads sections dictionary with section from file fname. 2681 Existing sections are overlaid. Silently skips missing configuration 2682 files.''' 2683 if dir: 2684 fname = os.path.join(dir, fname) 2685 # Sliently skip missing configuration file. 2686 if not os.path.isfile(fname): 2687 return 2688 # Don't load conf files twice (local and application conf files are the 2689 # same if the source file is in the application directory). 2690 if realpath(fname) in self.loaded: 2691 return 2692 rdr = Reader() # Use instead of file so we can use include:[] macro. 2693 rdr.open(fname) 2694 self.fname = fname 2695 reo = re.compile(r'^\s*\[\s*(?P<section>\S+)\s*\]\s*$') 2696 sections = OrderedDict() 2697 section,contents = '',[] 2698 while not rdr.eof(): 2699 s = rdr.read() 2700 if s and s[0] == '#': # Skip comment lines. 2701 continue 2702 s = string.rstrip(s) 2703 found = reo.findall(s) 2704 if found: 2705 if section: # Store previous section. 2706 if sections.has_key(section) \ 2707 and self.is_special_section(section): 2708 # Merge line oriented special sections. 2709 contents = sections[section] + contents 2710 sections[section] = contents 2711 section = string.lower(found[0]) 2712 contents = [] 2713 else: 2714 contents.append(s) 2715 if section and contents: # Store last section. 2716 if sections.has_key(section) \ 2717 and self.is_special_section(section): 2718 # Merge line oriented special sections. 2719 contents = sections[section] + contents 2720 sections[section] = contents 2721 rdr.close() 2722 # Delete blank lines from sections. 2723 for k in sections.keys(): 2724 for i in range(len(sections[k])-1,-1,-1): 2725 if not sections[k][i]: 2726 del sections[k][i] 2727 elif not self.is_special_section(k): 2728 break # Only trailing blanks from non-special sections. 2729 # Merge new sections. 2730 self.sections.update(sections) 2731 self.parse_tags() 2732 # Internally [miscellaneous] section entries are just glossary entries. 2733 dict = {} 2734 parse_entries(sections.get('miscellaneous',()),dict,unquote=1) 2735 update_glossary(self.conf_gloss,dict) 2736 dict = {} 2737 parse_entries(sections.get('glossary',()),dict,unquote=1) 2738 update_glossary(self.conf_gloss,dict) 2739 # Update document glossary so entries are available immediately. 2740 document.init_glossary() 2741 dict = {} 2742 parse_entries(sections.get('titles',()),dict) 2743 Title.load(dict) 2744 parse_entries(sections.get('specialcharacters',()),self.specialchars) 2745 undefine_entries(self.specialchars) 2746 parse_entries(sections.get('quotes',()),self.quotes) 2747 undefine_entries(self.quotes) 2748 self.parse_specialwords() 2749 self.parse_replacements() 2750 self.parse_specialsections() 2751 paragraphs.load(sections) 2752 lists.load(sections) 2753 blocks.load(sections) 2754 tables.load(sections) 2755 macros.load(sections.get('macros',())) 2756 self.loaded.append(realpath(fname)) 2757 2758 def load_all(self,dir): 2759 '''Load the standard configuration files from directory 'dir'.''' 2760 self.load('asciidoc.conf',dir) 2761 conf = document.backend + '.conf' 2762 self.load(conf,dir) 2763 conf = document.backend + '-' + document.doctype + '.conf' 2764 self.load(conf,dir) 2765 # Load ./filters/*.conf files if they exist. 2766 filters = os.path.join(dir,'filters') 2767 if os.path.isdir(filters): 2768 for file in os.listdir(filters): 2769 if re.match(r'^.+\.conf$',file): 2770 self.load(file,filters) 2771 2772 def load_miscellaneous(self,dict): 2773 '''Set miscellaneous configuration entries from dictionary values.''' 2774 def set_misc(name,rule='1',intval=0): 2775 if dict.has_key(name): 2776 errmsg = 'illegal [miscellaneous] %s entry' % name 2777 if intval: 2778 setattr(self, name, int(validate(dict[name],rule,errmsg))) 2779 else: 2780 setattr(self, name, validate(dict[name],rule,errmsg)) 2781 set_misc('tabsize','int($)>0',intval=1) 2782 set_misc('textwidth','int($)>0',intval=1) 2783 set_misc('pagewidth','int($)>0',intval=1) 2784 set_misc('pageunits') 2785 set_misc('outfilesuffix') 2786 if dict.has_key('newline'): 2787 # Convert escape sequences to their character values. 2788 self.newline = eval('"'+dict['newline']+'"') 2789 2790 def check(self): 2791 '''Check the configuration for internal consistancy. Called after all 2792 configuration files have been loaded.''' 2793 # Heuristic check that at least one configuration file was loaded. 2794 if not self.specialchars or not self.tags or not lists: 2795 raise EAsciiDoc,'incomplete or no configuration files' 2796 # Check special characters are only one character long. 2797 for k in self.specialchars.keys(): 2798 if len(k) != 1: 2799 raise EAsciiDoc,'[specialcharacters] "%s" ' \ 2800 'must be a single character' % (k,) 2801 # Check all special words have a corresponding inline macro body. 2802 for macro in self.specialwords.values(): 2803 if not is_name(macro): 2804 raise EAsciiDoc,'illegal "%s" special word name' % (macro,) 2805 if not self.sections.has_key(macro): 2806 warning('missing special word macro [%s]' % (macro,)) 2807 # Check all text quotes have a corresponding tag. 2808 for q in self.quotes.keys(): 2809 tag = self.quotes[q] 2810 if not self.tags.has_key(tag): 2811 warning('[quotes] %s missing "%s" tag definition' 2812 % (q,tag)) 2813 # Check all specialsections section names exist. 2814 for k,v in self.specialsections.items(): 2815 if not self.sections.has_key(v): 2816 warning('[%s] missing specialsections section' % (v,)) 2817 paragraphs.check() 2818 lists.check() 2819 blocks.check() 2820 tables.check() 2821 macros.check() 2822 2823 def is_special_section(self,section_name): 2824 for name in self.SPECIAL_SECTIONS: 2825 if re.match(name,section_name): 2826 return 1 2827 return 0 2828 2829 def dump(self): 2830 '''Dump configuration to stdout.''' 2831 # Header. 2832 hdr = '' 2833 hdr = hdr + '#' + writer.newline 2834 hdr = hdr + '# Generated by AsciiDoc %s for %s %s.%s' % \ 2835 (VERSION,document.backend,document.doctype,writer.newline) 2836 t = time.asctime(time.localtime(time.time())) 2837 hdr = hdr + '# %s%s' % (t,writer.newline) 2838 hdr = hdr + '#' + writer.newline 2839 sys.stdout.write(hdr) 2840 # Dump special sections. 2841 # Dump only the configuration file and command-line glossary entries. 2842 # [miscellanous] entries are dumped as part of the [glossary]. 2843 dict = {} 2844 dict.update(self.conf_gloss) 2845 dict.update(self.cmd_gloss) 2846 dump_section('glossary',dict) 2847 Title.dump() 2848 dump_section('quotes',self.quotes) 2849 dump_section('specialcharacters',self.specialchars) 2850 dict = {} 2851 for k,v in self.specialwords.items(): 2852 if dict.has_key(v): 2853 dict[v] = '%s "%s"' % (dict[v],k) # Append word list. 2854 else: 2855 dict[v] = '"%s"' % (k,) 2856 dump_section('specialwords',dict) 2857 dump_section('replacements',self.replacements) 2858 dump_section('specialsections',self.specialsections) 2859 dict = {} 2860 for k,v in self.tags.items(): 2861 dict[k] = '%s|%s' % v 2862 dump_section('tags',dict) 2863 paragraphs.dump() 2864 lists.dump() 2865 blocks.dump() 2866 tables.dump() 2867 macros.dump() 2868 # Dump remaining sections. 2869 for k in self.sections.keys(): 2870 if not self.is_special_section(k): 2871 sys.stdout.write('[%s]%s' % (k,writer.newline)) 2872 for line in self.sections[k]: 2873 sys.stdout.write('%s%s' % (line,writer.newline)) 2874 sys.stdout.write(writer.newline) 2875 2876 def subs_section(self,section,dict): 2877 '''Section glossary substitution from the document.glossary and 'dict'. 2878 the document.glossary. Lines containing undefinded glossary entries are 2879 deleted.''' 2880 if self.sections.has_key(section): 2881 return subs_glossary(self.sections[section],dict) 2882 else: 2883 warning('missing [%s] section' % (section,)) 2884 return () 2885 2886 def parse_tags(self): 2887 '''Parse [tags] section entries into self.tags dictionary.''' 2888 dict = {} 2889 parse_entries(self.sections.get('tags',()),dict) 2890 for k,v in dict.items(): 2891 if not is_name(k): 2892 raise EAsciiDoc,'[tag] %s malformed' % (k,) 2893 if v is None: 2894 if self.tags.has_key(k): 2895 del self.tags[k] 2896 elif v == 'none': 2897 self.tags[k] = (None,None) 2898 else: 2899 mo = re.match(r'(?P<stag>.*)\|(?P<etag>.*)',v) 2900 if mo: 2901 self.tags[k] = (mo.group('stag'), mo.group('etag')) 2902 else: 2903 raise EAsciiDoc,'[tag] %s value malformed' % (k,) 2904 2905 def tag(self,name): 2906 '''Returns (starttag,endtag) tuple named name from configuration file 2907 [tags] section. Raise error if not found''' 2908 if not self.tags.has_key(name): 2909 raise EAsciiDoc, 'missing tag "%s"' % (name,) 2910 return self.tags[name] 2911 2912 def parse_specialsections(self): 2913 '''Parse specialsections section to self.specialsections dictionary.''' 2914 # TODO: This is virtually the same as parse_replacements() and should 2915 # be factored to single routine. 2916 dict = {} 2917 parse_entries(self.sections.get('specialsections',()),dict,1) 2918 for pat,sectname in dict.items(): 2919 pat = strip_quotes(pat) 2920 if not is_regexp(pat): 2921 raise EAsciiDoc,'[specialsections] entry "%s" ' \ 2922 'is not a valid regular expression' % (pat,) 2923 if sectname is None: 2924 if self.specialsections.has_key(pat): 2925 del self.specialsections[pat] 2926 else: 2927 self.specialsections[pat] = sectname 2928 2929 def parse_replacements(self): 2930 '''Parse replacements section into self.replacements dictionary.''' 2931 dict = {} 2932 #TODO: Deprecated 2933 if self.sections.has_key('substitutions'): 2934 parse_entries(self.sections.get('substitutions',()),dict,1) 2935 warning('[substitutions] deprecated, rename [replacements]') 2936 else: 2937 parse_entries(self.sections.get('replacements',()),dict,1) 2938 for pat,rep in dict.items(): 2939 # The search pattern and the replacement are regular expressions so 2940 # check them both. 2941 pat = strip_quotes(pat) 2942 if not is_regexp(pat): 2943 raise EAsciiDoc,'"%s" ([replacements] entry in %s) ' \ 2944 'is not a valid regular expression' % (pat,self.fname) 2945 if rep is None: 2946 if self.replacements.has_key(pat): 2947 del self.replacements[pat] 2948 else: 2949 rep = strip_quotes(rep) 2950 if not is_regexp(pat): 2951 raise EAsciiDoc,'[replacements] entry "%s=%s" in %s ' \ 2952 'is an invalid find regular expression combination' \ 2953 % (pat,rep,self.fname) 2954 self.replacements[pat] = rep 2955 2956 def subs_replacements(self,s): 2957 '''Substitute patterns from self.replacements in 's'.''' 2958 result = s 2959 for pat,rep in self.replacements.items(): 2960 result = re.sub(pat, rep, result) 2961 return result 2962 2963 def parse_specialwords(self): 2964 '''Parse special words section into self.specialwords dictionary.''' 2965 reo = re.compile(r'(?:\s|^)(".+?"|[^"\s]+)(?=\s|$)') 2966 for line in self.sections.get('specialwords',()): 2967 e = parse_entry(line) 2968 if not e: 2969 raise EAsciiDoc,'[specialwords] entry "%s" in %s is malformed' \ 2970 % (line,self.fname) 2971 name,wordlist = e 2972 if not is_name(name): 2973 raise EAsciiDoc,'[specialwords] name "%s" in %s is illegal' \ 2974 % (name,self.fname) 2975 if wordlist == '': 2976 warning('[specialwords] entry "%s" in %s is blank' 2977 % (name,self.fname)) 2978 if wordlist is None: 2979 # Undefine all words associated with 'name'. 2980 for k,v in self.specialwords.items(): 2981 if v == name: 2982 del self.specialwords[k] 2983 else: 2984 words = reo.findall(wordlist) 2985 for word in words: 2986 word = strip_quotes(word) 2987 if not is_regexp(word): 2988 raise EAsciiDoc,'"%s" (%s [specialwords] entry in %s)' \ 2989 'is not a valid regular expression' \ 2990 % (word,name,self.fname) 2991 self.specialwords[word] = name 2992 2993 def subs_specialchars(self,s): 2994 '''Perform special character substitution on string 's'.''' 2995 '''It may seem like a good idea to escape special characters with a '\' 2996 character, the reason we don't is because the escape character itself 2997 then has to be escaped and this makes including code listings 2998 problematic. Use the predefined {amp},{lt},{gt} glossary entries 2999 instead.''' 3000 result = '' 3001 for ch in s: 3002 result = result + self.specialchars.get(ch,ch) 3003 return result 3004 3005 def subs_specialwords(self,s): 3006 '''Search for word patterns from self.specialwords in 's' and 3007 substitute using corresponding macro.''' 3008 result = s 3009 for word in self.specialwords.keys(): 3010 result = re.sub(word, _subs_specialwords, result) 3011 return result 3012 3013 def section2tags(self,section,dict={}): 3014 '''Perform glossary substitution on 'section' using document glossary 3015 plus 'dict' glossary. Return tuple (stag,etag) containing pre and post 3016 | placeholder tags.''' 3017 if self.sections.has_key(section): 3018 body = self.sections[section] 3019 else: 3020 warning('missing [%s] section' % (section,)) 3021 body = () 3022 # Split macro body into start and end tag lists. 3023 stag = [] 3024 etag = [] 3025 in_stag = 1 3026 for s in body: 3027 if in_stag: 3028 mo = re.match(r'(?P<stag>.*)\|(?P<etag>.*)',s) 3029 if mo: 3030 if mo.group('stag'): 3031 stag.append(mo.group('stag')) 3032 if mo.group('etag'): 3033 etag.append(mo.group('etag')) 3034 in_stag = 0 3035 else: 3036 stag.append(s) 3037 else: 3038 etag.append(s) 3039 # Do glossary substitution last so {brkbar} can be used to escape |. 3040 stag = subs_glossary(stag,dict) 3041 etag = subs_glossary(etag,dict) 3042 return (stag,etag) 3043 3044#--------------------------------------------------------------------------- 3045# Application code. 3046#--------------------------------------------------------------------------- 3047# Constants 3048# --------- 3049APP_DIR = None # This file's directory. 3050USER_DIR = None # ~/.asciidoc 3051 3052# Globals 3053# ------- 3054document = Document() # The document being processed. 3055config = Config() # Configuration file reader. 3056reader = Reader() # Input stream line reader. 3057writer = Writer() # Output stream line writer. 3058paragraphs = Paragraphs() # Paragraph definitions. 3059lists = Lists() # List definitions. 3060blocks = DelimitedBlocks() # DelimitedBlock definitions. 3061tables = Tables() # Table definitions. 3062macros = Macros() # Macro definitions. 3063 3064def asciidoc(backend, doctype, confiles, infile, outfile, options): 3065 '''Convert AsciiDoc document to DocBook document of type doctype 3066 The AsciiDoc document is read from file object src the translated 3067 DocBook file written to file object dst.''' 3068 try: 3069 if doctype not in ('article','manpage','book'): 3070 raise EAsciiDoc,'illegal document type' 3071 if backend == 'linuxdoc' and doctype != 'article': 3072 raise EAsciiDoc,'%s %s documents are not supported' \ 3073 % (backend,doctype) 3074 document.backend = backend 3075 document.doctype = doctype 3076 document.init_glossary() 3077 # Set processing options. 3078 for o in options: 3079 if o == '-s': config.suppress_headers = 1 3080 if o == '-v': config.verbose = 1 3081 # Check the infile exists. 3082 if infile != '<stdin>' and not os.path.isfile(infile): 3083 raise EAsciiDoc,'input file %s missing' % (infile,) 3084 if '-e' not in options: 3085 # Load global configuration files from asciidoc directory. 3086 config.load_all(APP_DIR) 3087 # Load configuration files from ~/.asciidoc if it exists. 3088 if USER_DIR is not None: 3089 config.load_all(USER_DIR) 3090 # Load configuration files from document directory. 3091 config.load_all(os.path.dirname(infile)) 3092 if infile != '<stdin>': 3093 # Load implicit document specific configuration files if they exist. 3094 config.load(os.path.splitext(infile)[0] + '.conf') 3095 config.load(os.path.splitext(infile)[0] + '-' + backend + '.conf') 3096 # If user specified configuration file(s) overlay the defaults. 3097 if confiles: 3098 for conf in confiles: 3099 # First look in current working directory. 3100 if os.path.isfile(conf): 3101 config.load(conf) 3102 else: 3103 raise EAsciiDoc,'configuration file %s missing' % (conf,) 3104 document.init_glossary() # Add conf file. 3105 # Check configuration for consistency. 3106 config.check() 3107 # Build outfile name now all conf files have been read. 3108 if outfile is None: 3109 outfile = os.path.splitext(infile)[0] + '.' + backend 3110 if config.outfilesuffix: 3111 # Change file extension. 3112 outfile = os.path.splitext(outfile)[0] + config.outfilesuffix 3113 if '-c' in options: 3114 config.dump() 3115 else: 3116 reader.tabsize = config.tabsize 3117 reader.open(infile) 3118 try: 3119 writer.newline = config.newline 3120 writer.open(outfile) 3121 try: 3122 document.init_glossary() # Add file name related entries. 3123 document.translate() 3124 finally: 3125 writer.close() 3126 finally: 3127 reader.closefile() # Keep reader state for postmortem. 3128 except (KeyboardInterrupt, SystemExit): 3129 print 3130 except Exception,e: 3131 # Cleanup. 3132 if outfile and outfile != '<stdout>' and os.path.isfile(outfile): 3133 os.unlink(outfile) 3134 # Build and print error description. 3135 msg = 'FAILED: ' 3136 if reader.cursor: 3137 msg = msg + "%s: line %d: " % (reader.cursor[0],reader.cursor[1]) 3138 if isinstance(e,EAsciiDoc): 3139 print_stderr(msg+str(e)) 3140 else: 3141 print_stderr(msg+'unexpected error:') 3142 print_stderr('-'*60) 3143 traceback.print_exc(file=sys.stderr) 3144 print_stderr('-'*60) 3145 sys.exit(1) 3146 3147def usage(msg=''): 3148 if msg: 3149 print_stderr(msg) 3150 print_stderr('Usage: asciidoc -b backend [-d doctype] [-g glossary-entry]') 3151 print_stderr(' [-e] [-n] [-s] [-f configfile] [-o outfile]') 3152 print_stderr(' [--help | -h] [--version] [-v] [ -c ]') 3153 print_stderr(' infile') 3154 3155def main(): 3156 # Locate the executable and configuration files directory. 3157 global APP_DIR,USER_DIR 3158 APP_DIR = os.path.dirname(realpath(sys.argv[0])) 3159 USER_DIR = os.environ.get('HOME') 3160 if USER_DIR is not None: 3161 USER_DIR = os.path.join(USER_DIR,'.asciidoc') 3162 if not os.path.isdir(USER_DIR): 3163 USER_DIR = None 3164 # Process command line options. 3165 import getopt 3166 opts,args = getopt.getopt(sys.argv[1:], 3167 'b:cd:ef:g:hno:svw:', 3168 ['help','profile','version']) 3169 if len(args) > 1: 3170 usage() 3171 sys.exit(1) 3172 backend = None 3173 doctype = 'article' 3174 confiles = [] 3175 outfile = None 3176 options = [] 3177 prof = 0 3178 for o,v in opts: 3179 if o in ('--help','-h'): 3180 print __doc__ 3181 sys.exit(0) 3182 if o == '--profile': 3183 prof = 1 3184 if o == '--version': 3185 print_stderr('asciidoc version %s' % (VERSION,)) 3186 sys.exit(0) 3187 if o == '-b': backend = v 3188 if o == '-c': options.append('-c') 3189 if o == '-d': doctype = v 3190 if o == '-e': options.append('-e') 3191 if o == '-f': confiles.append(v) 3192 if o == '-n': 3193 o = '-g' 3194 v = 'section-numbers' 3195 if o == '-g': 3196 e = parse_entry(v) 3197 if not e: 3198 usage('Illegal -g %s option' % (v,)) 3199 sys.exit(1) 3200 k,v = e 3201 if v is None: 3202 if k[0] == '^': 3203 k = k[1:] 3204 else: 3205 v = '' 3206 config.cmd_gloss[k] = v 3207 if o == '-o': 3208 if v == '-': 3209 outfile = '<stdout>' 3210 else: 3211 outfile = v 3212 if o == '-n': outfile = v 3213 if o == '-s': options.append('-s') 3214 if o == '-v': options.append('-v') 3215 if len(args) == 0 and len(opts) == 0: 3216 usage() 3217 sys.exit(1) 3218 if len(args) == 0: 3219 usage('No source file specified') 3220 sys.exit(1) 3221 if not backend: 3222 usage('No backend (-b) option specified') 3223 sys.exit(1) 3224 if args[0] == '-': 3225 infile = '<stdin>' 3226 else: 3227 infile = args[0] 3228 if infile == '<stdin>' and not outfile: 3229 outfile = '<stdout>' 3230 # Convert in and out files to absolute paths. 3231 if infile != '<stdin>': infile = os.path.abspath(infile) 3232 if outfile and outfile != '<stdout>': outfile = os.path.abspath(outfile) 3233 # Do the work. 3234 if prof: 3235 import profile 3236 profile.run("asciidoc('%s','%s',(),'%s',None,())" 3237 % (backend,doctype,infile)) 3238 else: 3239 asciidoc(backend, doctype, confiles, infile, outfile, options) 3240 3241if __name__ == "__main__": 3242 try: 3243 main() 3244 except (KeyboardInterrupt, SystemExit): 3245 pass 3246 except: 3247 print_stderr("%s: unexpected exit status: %s" % 3248 (os.path.basename(sys.argv[0]), sys.exc_info()[1])) 3249 # Exit with previous sys.exit() status or zero if no sys.exit(). 3250 sys.exit(sys.exc_info()[1]) 3251