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