source: rtems-tools/doc/asciidoc/asciidoc.py @ f91e023

4.104.115
Last change on this file since f91e023 was f91e023, checked in by Chris Johns <chrisj@…>, on 02/17/14 at 07:04:46

Add the documentation.

  • Property mode set to 100755
File size: 247.9 KB
Line 
1#!/usr/bin/env python
2"""
3asciidoc - converts an AsciiDoc text file to HTML or DocBook
4
5Copyright (C) 2002-2010 Stuart Rackham. Free use of this software is granted
6under the terms of the GNU General Public License (GPL).
7"""
8
9import sys, os, re, time, traceback, tempfile, subprocess, codecs, locale, unicodedata, copy
10
11### Used by asciidocapi.py ###
12VERSION = '8.6.8'           # See CHANGLOG file for version history.
13
14MIN_PYTHON_VERSION = '2.4'  # Require this version of Python or better.
15
16#---------------------------------------------------------------------------
17# Program constants.
18#---------------------------------------------------------------------------
19DEFAULT_BACKEND = 'html'
20DEFAULT_DOCTYPE = 'article'
21# Allowed substitution options for List, Paragraph and DelimitedBlock
22# definition subs entry.
23SUBS_OPTIONS = ('specialcharacters','quotes','specialwords',
24    'replacements', 'attributes','macros','callouts','normal','verbatim',
25    'none','replacements2','replacements3')
26# Default value for unspecified subs and presubs configuration file entries.
27SUBS_NORMAL = ('specialcharacters','quotes','attributes',
28    'specialwords','replacements','macros','replacements2')
29SUBS_VERBATIM = ('specialcharacters','callouts')
30
31NAME_RE = r'(?u)[^\W\d][-\w]*'  # Valid section or attribute name.
32OR, AND = ',', '+'              # Attribute list separators.
33
34
35#---------------------------------------------------------------------------
36# Utility functions and classes.
37#---------------------------------------------------------------------------
38
39class EAsciiDoc(Exception): pass
40
41class OrderedDict(dict):
42    """
43    Dictionary ordered by insertion order.
44    Python Cookbook: Ordered Dictionary, Submitter: David Benjamin.
45    http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/107747
46    """
47    def __init__(self, d=None, **kwargs):
48        self._keys = []
49        if d is None: d = kwargs
50        dict.__init__(self, d)
51    def __delitem__(self, key):
52        dict.__delitem__(self, key)
53        self._keys.remove(key)
54    def __setitem__(self, key, item):
55        dict.__setitem__(self, key, item)
56        if key not in self._keys: self._keys.append(key)
57    def clear(self):
58        dict.clear(self)
59        self._keys = []
60    def copy(self):
61        d = dict.copy(self)
62        d._keys = self._keys[:]
63        return d
64    def items(self):
65        return zip(self._keys, self.values())
66    def keys(self):
67        return self._keys
68    def popitem(self):
69        try:
70            key = self._keys[-1]
71        except IndexError:
72            raise KeyError('dictionary is empty')
73        val = self[key]
74        del self[key]
75        return (key, val)
76    def setdefault(self, key, failobj = None):
77        dict.setdefault(self, key, failobj)
78        if key not in self._keys: self._keys.append(key)
79    def update(self, d=None, **kwargs):
80        if d is None:
81            d = kwargs
82        dict.update(self, d)
83        for key in d.keys():
84            if key not in self._keys: self._keys.append(key)
85    def values(self):
86        return map(self.get, self._keys)
87
88class AttrDict(dict):
89    """
90    Like a dictionary except values can be accessed as attributes i.e. obj.foo
91    can be used in addition to obj['foo'].
92    If an item is not present None is returned.
93    """
94    def __getattr__(self, key):
95        try: return self[key]
96        except KeyError: return None
97    def __setattr__(self, key, value):
98        self[key] = value
99    def __delattr__(self, key):
100        try: del self[key]
101        except KeyError, k: raise AttributeError, k
102    def __repr__(self):
103        return '<AttrDict ' + dict.__repr__(self) + '>'
104    def __getstate__(self):
105        return dict(self)
106    def __setstate__(self,value):
107        for k,v in value.items(): self[k]=v
108
109class InsensitiveDict(dict):
110    """
111    Like a dictionary except key access is case insensitive.
112    Keys are stored in lower case.
113    """
114    def __getitem__(self, key):
115        return dict.__getitem__(self, key.lower())
116    def __setitem__(self, key, value):
117        dict.__setitem__(self, key.lower(), value)
118    def has_key(self, key):
119        return dict.has_key(self,key.lower())
120    def get(self, key, default=None):
121        return dict.get(self, key.lower(), default)
122    def update(self, dict):
123        for k,v in dict.items():
124            self[k] = v
125    def setdefault(self, key, default = None):
126        return dict.setdefault(self, key.lower(), default)
127
128
129class Trace(object):
130    """
131    Used in conjunction with the 'trace' attribute to generate diagnostic
132    output. There is a single global instance of this class named trace.
133    """
134    SUBS_NAMES = ('specialcharacters','quotes','specialwords',
135                  'replacements', 'attributes','macros','callouts',
136                  'replacements2','replacements3')
137    def __init__(self):
138        self.name_re = ''        # Regexp pattern to match trace names.
139        self.linenos = True
140        self.offset = 0
141    def __call__(self, name, before, after=None):
142        """
143        Print trace message if tracing is on and the trace 'name' matches the
144        document 'trace' attribute (treated as a regexp).
145        'before' is the source text before substitution; 'after' text is the
146        source text after substitutuion.
147        The 'before' and 'after' messages are only printed if they differ.
148        """
149        name_re = document.attributes.get('trace')
150        if name_re == 'subs':    # Alias for all the inline substitutions.
151            name_re = '|'.join(self.SUBS_NAMES)
152        self.name_re = name_re
153        if self.name_re is not None:
154            msg = message.format(name, 'TRACE: ', self.linenos, offset=self.offset)
155            if before != after and re.match(self.name_re,name):
156                if is_array(before):
157                    before = '\n'.join(before)
158                if after is None:
159                    msg += '\n%s\n' % before
160                else:
161                    if is_array(after):
162                        after = '\n'.join(after)
163                    msg += '\n<<<\n%s\n>>>\n%s\n' % (before,after)
164                message.stderr(msg)
165
166class Message:
167    """
168    Message functions.
169    """
170    PROG = os.path.basename(os.path.splitext(__file__)[0])
171
172    def __init__(self):
173        # Set to True or False to globally override line numbers method
174        # argument. Has no effect when set to None.
175        self.linenos = None
176        self.messages = []
177        self.prev_msg = ''
178
179    def stdout(self,msg):
180        print msg
181
182    def stderr(self,msg=''):
183        if msg == self.prev_msg:  # Suppress repeated messages.
184            return
185        self.messages.append(msg)
186        if __name__ == '__main__':
187            sys.stderr.write('%s: %s%s' % (self.PROG, msg, os.linesep))
188        self.prev_msg = msg
189
190    def verbose(self, msg,linenos=True):
191        if config.verbose:
192            msg = self.format(msg,linenos=linenos)
193            self.stderr(msg)
194
195    def warning(self, msg,linenos=True,offset=0):
196        msg = self.format(msg,'WARNING: ',linenos,offset=offset)
197        document.has_warnings = True
198        self.stderr(msg)
199
200    def deprecated(self, msg, linenos=True):
201        msg = self.format(msg, 'DEPRECATED: ', linenos)
202        self.stderr(msg)
203
204    def format(self, msg, prefix='', linenos=True, cursor=None, offset=0):
205        """Return formatted message string."""
206        if self.linenos is not False and ((linenos or self.linenos) and reader.cursor):
207            if cursor is None:
208                cursor = reader.cursor
209            prefix += '%s: line %d: ' % (os.path.basename(cursor[0]),cursor[1]+offset)
210        return prefix + msg
211
212    def error(self, msg, cursor=None, halt=False):
213        """
214        Report fatal error.
215        If halt=True raise EAsciiDoc exception.
216        If halt=False don't exit application, continue in the hope of reporting
217        all fatal errors finishing with a non-zero exit code.
218        """
219        if halt:
220            raise EAsciiDoc, self.format(msg,linenos=False,cursor=cursor)
221        else:
222            msg = self.format(msg,'ERROR: ',cursor=cursor)
223            self.stderr(msg)
224            document.has_errors = True
225
226    def unsafe(self, msg):
227        self.error('unsafe: '+msg)
228
229
230def userdir():
231    """
232    Return user's home directory or None if it is not defined.
233    """
234    result = os.path.expanduser('~')
235    if result == '~':
236        result = None
237    return result
238
239def localapp():
240    """
241    Return True if we are not executing the system wide version
242    i.e. the configuration is in the executable's directory.
243    """
244    return os.path.isfile(os.path.join(APP_DIR, 'asciidoc.conf'))
245
246def file_in(fname, directory):
247    """Return True if file fname resides inside directory."""
248    assert os.path.isfile(fname)
249    # Empty directory (not to be confused with None) is the current directory.
250    if directory == '':
251        directory = os.getcwd()
252    else:
253        assert os.path.isdir(directory)
254        directory = os.path.realpath(directory)
255    fname = os.path.realpath(fname)
256    return os.path.commonprefix((directory, fname)) == directory
257
258def safe():
259    return document.safe
260
261def is_safe_file(fname, directory=None):
262    # A safe file must reside in 'directory' (defaults to the source
263    # file directory).
264    if directory is None:
265        if document.infile == '<stdin>':
266           return not safe()
267        directory = os.path.dirname(document.infile)
268    elif directory == '':
269        directory = '.'
270    return (
271        not safe()
272        or file_in(fname, directory)
273        or file_in(fname, APP_DIR)
274        or file_in(fname, CONF_DIR)
275    )
276
277def safe_filename(fname, parentdir):
278    """
279    Return file name which must reside in the parent file directory.
280    Return None if file is not safe.
281    """
282    if not os.path.isabs(fname):
283        # Include files are relative to parent document
284        # directory.
285        fname = os.path.normpath(os.path.join(parentdir,fname))
286    if not is_safe_file(fname, parentdir):
287        message.unsafe('include file: %s' % fname)
288        return None
289    return fname
290
291def assign(dst,src):
292    """Assign all attributes from 'src' object to 'dst' object."""
293    for a,v in src.__dict__.items():
294        setattr(dst,a,v)
295
296def strip_quotes(s):
297    """Trim white space and, if necessary, quote characters from s."""
298    s = s.strip()
299    # Strip quotation mark characters from quoted strings.
300    if len(s) >= 3 and s[0] == '"' and s[-1] == '"':
301        s = s[1:-1]
302    return s
303
304def is_re(s):
305    """Return True if s is a valid regular expression else return False."""
306    try: re.compile(s)
307    except: return False
308    else: return True
309
310def re_join(relist):
311    """Join list of regular expressions re1,re2,... to single regular
312    expression (re1)|(re2)|..."""
313    if len(relist) == 0:
314        return None
315    result = []
316    # Delete named groups to avoid ambiguity.
317    for s in relist:
318        result.append(re.sub(r'\?P<\S+?>','',s))
319    result = ')|('.join(result)
320    result = '('+result+')'
321    return result
322
323def lstrip_list(s):
324    """
325    Return list with empty items from start of list removed.
326    """
327    for i in range(len(s)):
328        if s[i]: break
329    else:
330        return []
331    return s[i:]
332
333def rstrip_list(s):
334    """
335    Return list with empty items from end of list removed.
336    """
337    for i in range(len(s)-1,-1,-1):
338        if s[i]: break
339    else:
340        return []
341    return s[:i+1]
342
343def strip_list(s):
344    """
345    Return list with empty items from start and end of list removed.
346    """
347    s = lstrip_list(s)
348    s = rstrip_list(s)
349    return s
350
351def is_array(obj):
352    """
353    Return True if object is list or tuple type.
354    """
355    return isinstance(obj,list) or isinstance(obj,tuple)
356
357def dovetail(lines1, lines2):
358    """
359    Append list or tuple of strings 'lines2' to list 'lines1'.  Join the last
360    non-blank item in 'lines1' with the first non-blank item in 'lines2' into a
361    single string.
362    """
363    assert is_array(lines1)
364    assert is_array(lines2)
365    lines1 = strip_list(lines1)
366    lines2 = strip_list(lines2)
367    if not lines1 or not lines2:
368        return list(lines1) + list(lines2)
369    result = list(lines1[:-1])
370    result.append(lines1[-1] + lines2[0])
371    result += list(lines2[1:])
372    return result
373
374def dovetail_tags(stag,content,etag):
375    """Merge the end tag with the first content line and the last
376    content line with the end tag. This ensures verbatim elements don't
377    include extraneous opening and closing line breaks."""
378    return dovetail(dovetail(stag,content), etag)
379
380# The following functions are so we don't have to use the dangerous
381# built-in eval() function.
382if float(sys.version[:3]) >= 2.6 or sys.platform[:4] == 'java':
383    # Use AST module if CPython >= 2.6 or Jython.
384    import ast
385    from ast import literal_eval
386
387    def get_args(val):
388        d = {}
389        args = ast.parse("d(" + val + ")", mode='eval').body.args
390        i = 1
391        for arg in args:
392            if isinstance(arg, ast.Name):
393                d[str(i)] = literal_eval(arg.id)
394            else:
395                d[str(i)] = literal_eval(arg)
396            i += 1
397        return d
398
399    def get_kwargs(val):
400        d = {}
401        args = ast.parse("d(" + val + ")", mode='eval').body.keywords
402        for arg in args:
403            d[arg.arg] = literal_eval(arg.value)
404        return d
405
406    def parse_to_list(val):
407        values = ast.parse("[" + val + "]", mode='eval').body.elts
408        return [literal_eval(v) for v in values]
409
410else:   # Use deprecated CPython compiler module.
411    import compiler
412    from compiler.ast import Const, Dict, Expression, Name, Tuple, UnarySub, Keyword
413
414    # Code from:
415    # http://mail.python.org/pipermail/python-list/2009-September/1219992.html
416    # Modified to use compiler.ast.List as this module has a List
417    def literal_eval(node_or_string):
418        """
419        Safely evaluate an expression node or a string containing a Python
420        expression.  The string or node provided may only consist of the
421        following Python literal structures: strings, numbers, tuples,
422        lists, dicts, booleans, and None.
423        """
424        _safe_names = {'None': None, 'True': True, 'False': False}
425        if isinstance(node_or_string, basestring):
426            node_or_string = compiler.parse(node_or_string, mode='eval')
427        if isinstance(node_or_string, Expression):
428            node_or_string = node_or_string.node
429        def _convert(node):
430            if isinstance(node, Const) and isinstance(node.value,
431                    (basestring, int, float, long, complex)):
432                 return node.value
433            elif isinstance(node, Tuple):
434                return tuple(map(_convert, node.nodes))
435            elif isinstance(node, compiler.ast.List):
436                return list(map(_convert, node.nodes))
437            elif isinstance(node, Dict):
438                return dict((_convert(k), _convert(v)) for k, v
439                            in node.items)
440            elif isinstance(node, Name):
441                if node.name in _safe_names:
442                    return _safe_names[node.name]
443            elif isinstance(node, UnarySub):
444                return -_convert(node.expr)
445            raise ValueError('malformed string')
446        return _convert(node_or_string)
447
448    def get_args(val):
449        d = {}
450        args = compiler.parse("d(" + val + ")", mode='eval').node.args
451        i = 1
452        for arg in args:
453            if isinstance(arg, Keyword):
454                break
455            d[str(i)] = literal_eval(arg)
456            i = i + 1
457        return d
458
459    def get_kwargs(val):
460        d = {}
461        args = compiler.parse("d(" + val + ")", mode='eval').node.args
462        i = 0
463        for arg in args:
464            if isinstance(arg, Keyword):
465                break
466            i += 1
467        args = args[i:]
468        for arg in args:
469            d[str(arg.name)] = literal_eval(arg.expr)
470        return d
471
472    def parse_to_list(val):
473        values = compiler.parse("[" + val + "]", mode='eval').node.asList()
474        return [literal_eval(v) for v in values]
475
476def parse_attributes(attrs,dict):
477    """Update a dictionary with name/value attributes from the attrs string.
478    The attrs string is a comma separated list of values and keyword name=value
479    pairs. Values must preceed keywords and are named '1','2'... The entire
480    attributes list is named '0'. If keywords are specified string values must
481    be quoted. Examples:
482
483    attrs: ''
484    dict: {}
485
486    attrs: 'hello,world'
487    dict: {'2': 'world', '0': 'hello,world', '1': 'hello'}
488
489    attrs: '"hello", planet="earth"'
490    dict: {'planet': 'earth', '0': '"hello",planet="earth"', '1': 'hello'}
491    """
492    def f(*args,**keywords):
493        # Name and add aguments '1','2'... to keywords.
494        for i in range(len(args)):
495            if not str(i+1) in keywords:
496                keywords[str(i+1)] = args[i]
497        return keywords
498
499    if not attrs:
500        return
501    dict['0'] = attrs
502    # Replace line separators with spaces so line spanning works.
503    s = re.sub(r'\s', ' ', attrs)
504    d = {}
505    try:
506        d.update(get_args(s))
507        d.update(get_kwargs(s))
508        for v in d.values():
509            if not (isinstance(v,str) or isinstance(v,int) or isinstance(v,float) or v is None):
510                raise Exception
511    except Exception:
512        s = s.replace('"','\\"')
513        s = s.split(',')
514        s = map(lambda x: '"' + x.strip() + '"', s)
515        s = ','.join(s)
516        try:
517            d = {}
518            d.update(get_args(s))
519            d.update(get_kwargs(s))
520        except Exception:
521            return  # If there's a syntax error leave with {0}=attrs.
522        for k in d.keys():  # Drop any empty positional arguments.
523            if d[k] == '': del d[k]
524    dict.update(d)
525    assert len(d) > 0
526
527def parse_named_attributes(s,attrs):
528    """Update a attrs dictionary with name="value" attributes from the s string.
529    Returns False if invalid syntax.
530    Example:
531    attrs: 'star="sun",planet="earth"'
532    dict: {'planet':'earth', 'star':'sun'}
533    """
534    def f(**keywords): return keywords
535
536    try:
537        d = {}
538        d = get_kwargs(s)
539        attrs.update(d)
540        return True
541    except Exception:
542        return False
543
544def parse_list(s):
545    """Parse comma separated string of Python literals. Return a tuple of of
546    parsed values."""
547    try:
548        result = tuple(parse_to_list(s))
549    except Exception:
550        raise EAsciiDoc,'malformed list: '+s
551    return result
552
553def parse_options(options,allowed,errmsg):
554    """Parse comma separated string of unquoted option names and return as a
555    tuple of valid options. 'allowed' is a list of allowed option values.
556    If allowed=() then all legitimate names are allowed.
557    'errmsg' is an error message prefix if an illegal option error is thrown."""
558    result = []
559    if options:
560        for s in re.split(r'\s*,\s*',options):
561            if (allowed and s not in allowed) or not is_name(s):
562                raise EAsciiDoc,'%s: %s' % (errmsg,s)
563            result.append(s)
564    return tuple(result)
565
566def symbolize(s):
567    """Drop non-symbol characters and convert to lowercase."""
568    return re.sub(r'(?u)[^\w\-_]', '', s).lower()
569
570def is_name(s):
571    """Return True if s is valid attribute, macro or tag name
572    (starts with alpha containing alphanumeric and dashes only)."""
573    return re.match(r'^'+NAME_RE+r'$',s) is not None
574
575def subs_quotes(text):
576    """Quoted text is marked up and the resulting text is
577    returned."""
578    keys = config.quotes.keys()
579    for q in keys:
580        i = q.find('|')
581        if i != -1 and q != '|' and q != '||':
582            lq = q[:i]      # Left quote.
583            rq = q[i+1:]    # Right quote.
584        else:
585            lq = rq = q
586        tag = config.quotes[q]
587        if not tag: continue
588        # Unconstrained quotes prefix the tag name with a hash.
589        if tag[0] == '#':
590            tag = tag[1:]
591            # Unconstrained quotes can appear anywhere.
592            reo = re.compile(r'(?msu)(^|.)(\[(?P<attrlist>[^[\]]+?)\])?' \
593                    + r'(?:' + re.escape(lq) + r')' \
594                    + r'(?P<content>.+?)(?:'+re.escape(rq)+r')')
595        else:
596            # The text within constrained quotes must be bounded by white space.
597            # Non-word (\W) characters are allowed at boundaries to accomodate
598            # enveloping quotes and punctuation e.g. a='x', ('x'), 'x', ['x'].
599            reo = re.compile(r'(?msu)(^|[^\w;:}])(\[(?P<attrlist>[^[\]]+?)\])?' \
600                + r'(?:' + re.escape(lq) + r')' \
601                + r'(?P<content>\S|\S.*?\S)(?:'+re.escape(rq)+r')(?=\W|$)')
602        pos = 0
603        while True:
604            mo = reo.search(text,pos)
605            if not mo: break
606            if text[mo.start()] == '\\':
607                # Delete leading backslash.
608                text = text[:mo.start()] + text[mo.start()+1:]
609                # Skip past start of match.
610                pos = mo.start() + 1
611            else:
612                attrlist = {}
613                parse_attributes(mo.group('attrlist'), attrlist)
614                stag,etag = config.tag(tag, attrlist)
615                s = mo.group(1) + stag + mo.group('content') + etag
616                text = text[:mo.start()] + s + text[mo.end():]
617                pos = mo.start() + len(s)
618    return text
619
620def subs_tag(tag,dict={}):
621    """Perform attribute substitution and split tag string returning start, end
622    tag tuple (c.f. Config.tag())."""
623    if not tag:
624        return [None,None]
625    s = subs_attrs(tag,dict)
626    if not s:
627        message.warning('tag \'%s\' dropped: contains undefined attribute' % tag)
628        return [None,None]
629    result = s.split('|')
630    if len(result) == 1:
631        return result+[None]
632    elif len(result) == 2:
633        return result
634    else:
635        raise EAsciiDoc,'malformed tag: %s' % tag
636
637def parse_entry(entry, dict=None, unquote=False, unique_values=False,
638        allow_name_only=False, escape_delimiter=True):
639    """Parse name=value entry to dictionary 'dict'. Return tuple (name,value)
640    or None if illegal entry.
641    If name= then value is set to ''.
642    If name and allow_name_only=True then value is set to ''.
643    If name! and allow_name_only=True then value is set to None.
644    Leading and trailing white space is striped from 'name' and 'value'.
645    'name' can contain any printable characters.
646    If the '=' delimiter character is allowed in  the 'name' then
647    it must be escaped with a backslash and escape_delimiter must be True.
648    If 'unquote' is True leading and trailing double-quotes are stripped from
649    'name' and 'value'.
650    If unique_values' is True then dictionary entries with the same value are
651    removed before the parsed entry is added."""
652    if escape_delimiter:
653        mo = re.search(r'(?:[^\\](=))',entry)
654    else:
655        mo = re.search(r'(=)',entry)
656    if mo:  # name=value entry.
657        if mo.group(1):
658            name = entry[:mo.start(1)]
659            if escape_delimiter:
660                name = name.replace(r'\=','=')  # Unescape \= in name.
661            value = entry[mo.end(1):]
662    elif allow_name_only and entry:         # name or name! entry.
663        name = entry
664        if name[-1] == '!':
665            name = name[:-1]
666            value = None
667        else:
668            value = ''
669    else:
670        return None
671    if unquote:
672        name = strip_quotes(name)
673        if value is not None:
674            value = strip_quotes(value)
675    else:
676        name = name.strip()
677        if value is not None:
678            value = value.strip()
679    if not name:
680        return None
681    if dict is not None:
682        if unique_values:
683            for k,v in dict.items():
684                if v == value: del dict[k]
685        dict[name] = value
686    return name,value
687
688def parse_entries(entries, dict, unquote=False, unique_values=False,
689        allow_name_only=False,escape_delimiter=True):
690    """Parse name=value entries from  from lines of text in 'entries' into
691    dictionary 'dict'. Blank lines are skipped."""
692    entries = config.expand_templates(entries)
693    for entry in entries:
694        if entry and not parse_entry(entry, dict, unquote, unique_values,
695                allow_name_only, escape_delimiter):
696            raise EAsciiDoc,'malformed section entry: %s' % entry
697
698def dump_section(name,dict,f=sys.stdout):
699    """Write parameters in 'dict' as in configuration file section format with
700    section 'name'."""
701    f.write('[%s]%s' % (name,writer.newline))
702    for k,v in dict.items():
703        k = str(k)
704        k = k.replace('=',r'\=')    # Escape = in name.
705        # Quote if necessary.
706        if len(k) != len(k.strip()):
707            k = '"'+k+'"'
708        if v and len(v) != len(v.strip()):
709            v = '"'+v+'"'
710        if v is None:
711            # Don't dump undefined attributes.
712            continue
713        else:
714            s = k+'='+v
715        if s[0] == '#':
716            s = '\\' + s    # Escape so not treated as comment lines.
717        f.write('%s%s' % (s,writer.newline))
718    f.write(writer.newline)
719
720def update_attrs(attrs,dict):
721    """Update 'attrs' dictionary with parsed attributes in dictionary 'dict'."""
722    for k,v in dict.items():
723        if not is_name(k):
724            raise EAsciiDoc,'illegal attribute name: %s' % k
725        attrs[k] = v
726
727def is_attr_defined(attrs,dic):
728    """
729    Check if the sequence of attributes is defined in dictionary 'dic'.
730    Valid 'attrs' sequence syntax:
731    <attr> Return True if single attrbiute is defined.
732    <attr1>,<attr2>,... Return True if one or more attributes are defined.
733    <attr1>+<attr2>+... Return True if all the attributes are defined.
734    """
735    if OR in attrs:
736        for a in attrs.split(OR):
737            if dic.get(a.strip()) is not None:
738                return True
739        else: return False
740    elif AND in attrs:
741        for a in attrs.split(AND):
742            if dic.get(a.strip()) is None:
743                return False
744        else: return True
745    else:
746        return dic.get(attrs.strip()) is not None
747
748def filter_lines(filter_cmd, lines, attrs={}):
749    """
750    Run 'lines' through the 'filter_cmd' shell command and return the result.
751    The 'attrs' dictionary contains additional filter attributes.
752    """
753    def findfilter(name,dir,filter):
754        """Find filter file 'fname' with style name 'name' in directory
755        'dir'. Return found file path or None if not found."""
756        if name:
757            result = os.path.join(dir,'filters',name,filter)
758            if os.path.isfile(result):
759                return result
760        result = os.path.join(dir,'filters',filter)
761        if os.path.isfile(result):
762            return result
763        return None
764
765    # Return input lines if there's not filter.
766    if not filter_cmd or not filter_cmd.strip():
767        return lines
768    # Perform attributes substitution on the filter command.
769    s = subs_attrs(filter_cmd, attrs)
770    if not s:
771        message.error('undefined filter attribute in command: %s' % filter_cmd)
772        return []
773    filter_cmd = s.strip()
774    # Parse for quoted and unquoted command and command tail.
775    # Double quoted.
776    mo = re.match(r'^"(?P<cmd>[^"]+)"(?P<tail>.*)$', filter_cmd)
777    if not mo:
778        # Single quoted.
779        mo = re.match(r"^'(?P<cmd>[^']+)'(?P<tail>.*)$", filter_cmd)
780        if not mo:
781            # Unquoted catch all.
782            mo = re.match(r'^(?P<cmd>\S+)(?P<tail>.*)$', filter_cmd)
783    cmd = mo.group('cmd').strip()
784    found = None
785    if not os.path.dirname(cmd):
786        # Filter command has no directory path so search filter directories.
787        filtername = attrs.get('style')
788        d = document.attributes.get('docdir')
789        if d:
790            found = findfilter(filtername, d, cmd)
791        if not found:
792            if USER_DIR:
793                found = findfilter(filtername, USER_DIR, cmd)
794            if not found:
795                if localapp():
796                    found = findfilter(filtername, APP_DIR, cmd)
797                else:
798                    found = findfilter(filtername, CONF_DIR, cmd)
799    else:
800        if os.path.isfile(cmd):
801            found = cmd
802        else:
803            message.warning('filter not found: %s' % cmd)
804    if found:
805        filter_cmd = '"' + found + '"' + mo.group('tail')
806    if found:
807        if cmd.endswith('.py'):
808            filter_cmd = '"%s" %s' % (document.attributes['python'],
809                filter_cmd)
810        elif cmd.endswith('.rb'):
811            filter_cmd = 'ruby ' + filter_cmd
812
813    message.verbose('filtering: ' + filter_cmd)
814    if os.name == 'nt':
815        # Remove redundant quoting -- this is not just
816        # cosmetic, unnecessary quoting appears to cause
817        # command line truncation.
818        filter_cmd = re.sub(r'"([^ ]+?)"', r'\1', filter_cmd)
819    try:
820        p = subprocess.Popen(filter_cmd, shell=True,
821                stdin=subprocess.PIPE, stdout=subprocess.PIPE)
822        output = p.communicate(os.linesep.join(lines))[0]
823    except Exception:
824        raise EAsciiDoc,'filter error: %s: %s' % (filter_cmd, sys.exc_info()[1])
825    if output:
826        result = [s.rstrip() for s in output.split(os.linesep)]
827    else:
828        result = []
829    filter_status = p.wait()
830    if filter_status:
831        message.warning('filter non-zero exit code: %s: returned %d' %
832               (filter_cmd, filter_status))
833    if lines and not result:
834        message.warning('no output from filter: %s' % filter_cmd)
835    return result
836
837def system(name, args, is_macro=False, attrs=None):
838    """
839    Evaluate a system attribute ({name:args}) or system block macro
840    (name::[args]).
841    If is_macro is True then we are processing a system block macro otherwise
842    it's a system attribute.
843    The attrs dictionary is updated by the counter and set system attributes.
844    NOTE: The include1 attribute is used internally by the include1::[] macro
845    and is not for public use.
846    """
847    if is_macro:
848        syntax = '%s::[%s]' % (name,args)
849        separator = '\n'
850    else:
851        syntax = '{%s:%s}' % (name,args)
852        separator = writer.newline
853    if name not in ('eval','eval3','sys','sys2','sys3','include','include1','counter','counter2','set','set2','template'):
854        if is_macro:
855            msg = 'illegal system macro name: %s' % name
856        else:
857            msg = 'illegal system attribute name: %s' % name
858        message.warning(msg)
859        return None
860    if is_macro:
861        s = subs_attrs(args)
862        if s is None:
863            message.warning('skipped %s: undefined attribute in: %s' % (name,args))
864            return None
865        args = s
866    if name != 'include1':
867        message.verbose('evaluating: %s' % syntax)
868    if safe() and name not in ('include','include1'):
869        message.unsafe(syntax)
870        return None
871    result = None
872    if name in ('eval','eval3'):
873        try:
874            result = eval(args)
875            if result is True:
876                result = ''
877            elif result is False:
878                result = None
879            elif result is not None:
880                result = str(result)
881        except Exception:
882            message.warning('%s: evaluation error' % syntax)
883    elif name in ('sys','sys2','sys3'):
884        result = ''
885        fd,tmp = tempfile.mkstemp()
886        os.close(fd)
887        try:
888            cmd = args
889            cmd = cmd + (' > "%s"' % tmp)
890            if name == 'sys2':
891                cmd = cmd + ' 2>&1'
892            if os.name == 'nt':
893                # Remove redundant quoting -- this is not just
894                # cosmetic, unnecessary quoting appears to cause
895                # command line truncation.
896                cmd = re.sub(r'"([^ ]+?)"', r'\1', cmd)
897            message.verbose('shelling: %s' % cmd)
898            if os.system(cmd):
899                message.warning('%s: non-zero exit status' % syntax)
900            try:
901                if os.path.isfile(tmp):
902                    f = open(tmp)
903                    try:
904                        lines = [s.rstrip() for s in f]
905                    finally:
906                        f.close()
907                else:
908                    lines = []
909            except Exception:
910                raise EAsciiDoc,'%s: temp file read error' % syntax
911            result = separator.join(lines)
912        finally:
913            if os.path.isfile(tmp):
914                os.remove(tmp)
915    elif name in ('counter','counter2'):
916        mo = re.match(r'^(?P<attr>[^:]*?)(:(?P<seed>.*))?$', args)
917        attr = mo.group('attr')
918        seed = mo.group('seed')
919        if seed and (not re.match(r'^\d+$', seed) and len(seed) > 1):
920            message.warning('%s: illegal counter seed: %s' % (syntax,seed))
921            return None
922        if not is_name(attr):
923            message.warning('%s: illegal attribute name' % syntax)
924            return None
925        value = document.attributes.get(attr)
926        if value:
927            if not re.match(r'^\d+$', value) and len(value) > 1:
928                message.warning('%s: illegal counter value: %s'
929                                % (syntax,value))
930                return None
931            if re.match(r'^\d+$', value):
932                expr = value + '+1'
933            else:
934                expr = 'chr(ord("%s")+1)' % value
935            try:
936                result = str(eval(expr))
937            except Exception:
938                message.warning('%s: evaluation error: %s' % (syntax, expr))
939        else:
940            if seed:
941                result = seed
942            else:
943                result = '1'
944        document.attributes[attr] = result
945        if attrs is not None:
946            attrs[attr] = result
947        if name == 'counter2':
948            result = ''
949    elif name in ('set','set2'):
950        mo = re.match(r'^(?P<attr>[^:]*?)(:(?P<value>.*))?$', args)
951        attr = mo.group('attr')
952        value = mo.group('value')
953        if value is None:
954            value = ''
955        if attr.endswith('!'):
956            attr = attr[:-1]
957            value = None
958        if not is_name(attr):
959            message.warning('%s: illegal attribute name' % syntax)
960        else:
961            if attrs is not None:
962                attrs[attr] = value
963            if name != 'set2':  # set2 only updates local attributes.
964                document.attributes[attr] = value
965        if value is None:
966            result = None
967        else:
968            result = ''
969    elif name == 'include':
970        if not os.path.exists(args):
971            message.warning('%s: file does not exist' % syntax)
972        elif not is_safe_file(args):
973            message.unsafe(syntax)
974        else:
975            f = open(args)
976            try:
977                result = [s.rstrip() for s in f]
978            finally:
979                f.close()
980            if result:
981                result = subs_attrs(result)
982                result = separator.join(result)
983                result = result.expandtabs(reader.tabsize)
984            else:
985                result = ''
986    elif name == 'include1':
987        result = separator.join(config.include1[args])
988    elif name == 'template':
989        if not args in config.sections:
990            message.warning('%s: template does not exist' % syntax)
991        else:
992            result = []
993            for line in  config.sections[args]:
994                line = subs_attrs(line)
995                if line is not None:
996                    result.append(line)
997            result = '\n'.join(result)
998    else:
999        assert False
1000    if result and name in ('eval3','sys3'):
1001        macros.passthroughs.append(result)
1002        result = '\x07' + str(len(macros.passthroughs)-1) + '\x07'
1003    return result
1004
1005def subs_attrs(lines, dictionary=None):
1006    """Substitute 'lines' of text with attributes from the global
1007    document.attributes dictionary and from 'dictionary' ('dictionary'
1008    entries take precedence). Return a tuple of the substituted lines.  'lines'
1009    containing undefined attributes are deleted. If 'lines' is a string then
1010    return a string.
1011
1012    - Attribute references are substituted in the following order: simple,
1013      conditional, system.
1014    - Attribute references inside 'dictionary' entry values are substituted.
1015    """
1016
1017    def end_brace(text,start):
1018        """Return index following end brace that matches brace at start in
1019        text."""
1020        assert text[start] == '{'
1021        n = 0
1022        result = start
1023        for c in text[start:]:
1024            # Skip braces that are followed by a backslash.
1025            if result == len(text)-1 or text[result+1] != '\\':
1026                if c == '{': n = n + 1
1027                elif c == '}': n = n - 1
1028            result = result + 1
1029            if n == 0: break
1030        return result
1031
1032    if type(lines) == str:
1033        string_result = True
1034        lines = [lines]
1035    else:
1036        string_result = False
1037    if dictionary is None:
1038        attrs = document.attributes
1039    else:
1040        # Remove numbered document attributes so they don't clash with
1041        # attribute list positional attributes.
1042        attrs = {}
1043        for k,v in document.attributes.items():
1044            if not re.match(r'^\d+$', k):
1045                attrs[k] = v
1046        # Substitute attribute references inside dictionary values.
1047        for k,v in dictionary.items():
1048            if v is None:
1049                del dictionary[k]
1050            else:
1051                v = subs_attrs(str(v))
1052                if v is None:
1053                    del dictionary[k]
1054                else:
1055                    dictionary[k] = v
1056        attrs.update(dictionary)
1057    # Substitute all attributes in all lines.
1058    result = []
1059    for line in lines:
1060        # Make it easier for regular expressions.
1061        line = line.replace('\\{','{\\')
1062        line = line.replace('\\}','}\\')
1063        # Expand simple attributes ({name}).
1064        # Nested attributes not allowed.
1065        reo = re.compile(r'(?su)\{(?P<name>[^\\\W][-\w]*?)\}(?!\\)')
1066        pos = 0
1067        while True:
1068            mo = reo.search(line,pos)
1069            if not mo: break
1070            s =  attrs.get(mo.group('name'))
1071            if s is None:
1072                pos = mo.end()
1073            else:
1074                s = str(s)
1075                line = line[:mo.start()] + s + line[mo.end():]
1076                pos = mo.start() + len(s)
1077        # Expand conditional attributes.
1078        # Single name -- higher precedence.
1079        reo1 = re.compile(r'(?su)\{(?P<name>[^\\\W][-\w]*?)' \
1080                          r'(?P<op>\=|\?|!|#|%|@|\$)' \
1081                          r'(?P<value>.*?)\}(?!\\)')
1082        # Multiple names (n1,n2,... or n1+n2+...) -- lower precedence.
1083        reo2 = re.compile(r'(?su)\{(?P<name>[^\\\W][-\w'+OR+AND+r']*?)' \
1084                          r'(?P<op>\=|\?|!|#|%|@|\$)' \
1085                          r'(?P<value>.*?)\}(?!\\)')
1086        for reo in [reo1,reo2]:
1087            pos = 0
1088            while True:
1089                mo = reo.search(line,pos)
1090                if not mo: break
1091                attr = mo.group()
1092                name =  mo.group('name')
1093                if reo == reo2:
1094                    if OR in name:
1095                        sep = OR
1096                    else:
1097                        sep = AND
1098                    names = [s.strip() for s in name.split(sep) if s.strip() ]
1099                    for n in names:
1100                        if not re.match(r'^[^\\\W][-\w]*$',n):
1101                            message.error('illegal attribute syntax: %s' % attr)
1102                    if sep == OR:
1103                        # Process OR name expression: n1,n2,...
1104                        for n in names:
1105                            if attrs.get(n) is not None:
1106                                lval = ''
1107                                break
1108                        else:
1109                            lval = None
1110                    else:
1111                        # Process AND name expression: n1+n2+...
1112                        for n in names:
1113                            if attrs.get(n) is None:
1114                                lval = None
1115                                break
1116                        else:
1117                            lval = ''
1118                else:
1119                    lval =  attrs.get(name)
1120                op = mo.group('op')
1121                # mo.end() not good enough because '{x={y}}' matches '{x={y}'.
1122                end = end_brace(line,mo.start())
1123                rval = line[mo.start('value'):end-1]
1124                UNDEFINED = '{zzzzz}'
1125                if lval is None:
1126                    if op == '=': s = rval
1127                    elif op == '?': s = ''
1128                    elif op == '!': s = rval
1129                    elif op == '#': s = UNDEFINED   # So the line is dropped.
1130                    elif op == '%': s = rval
1131                    elif op in ('@','$'):
1132                        s = UNDEFINED               # So the line is dropped.
1133                    else:
1134                        assert False, 'illegal attribute: %s' % attr
1135                else:
1136                    if op == '=': s = lval
1137                    elif op == '?': s = rval
1138                    elif op == '!': s = ''
1139                    elif op == '#': s = rval
1140                    elif op == '%': s = UNDEFINED   # So the line is dropped.
1141                    elif op in ('@','$'):
1142                        v = re.split(r'(?<!\\):',rval)
1143                        if len(v) not in (2,3):
1144                            message.error('illegal attribute syntax: %s' % attr)
1145                            s = ''
1146                        elif not is_re('^'+v[0]+'$'):
1147                            message.error('illegal attribute regexp: %s' % attr)
1148                            s = ''
1149                        else:
1150                            v = [s.replace('\\:',':') for s in v]
1151                            re_mo = re.match('^'+v[0]+'$',lval)
1152                            if op == '@':
1153                                if re_mo:
1154                                    s = v[1]         # {<name>@<re>:<v1>[:<v2>]}
1155                                else:
1156                                    if len(v) == 3:   # {<name>@<re>:<v1>:<v2>}
1157                                        s = v[2]
1158                                    else:             # {<name>@<re>:<v1>}
1159                                        s = ''
1160                            else:
1161                                if re_mo:
1162                                    if len(v) == 2:   # {<name>$<re>:<v1>}
1163                                        s = v[1]
1164                                    elif v[1] == '':  # {<name>$<re>::<v2>}
1165                                        s = UNDEFINED # So the line is dropped.
1166                                    else:             # {<name>$<re>:<v1>:<v2>}
1167                                        s = v[1]
1168                                else:
1169                                    if len(v) == 2:   # {<name>$<re>:<v1>}
1170                                        s = UNDEFINED # So the line is dropped.
1171                                    else:             # {<name>$<re>:<v1>:<v2>}
1172                                        s = v[2]
1173                    else:
1174                        assert False, 'illegal attribute: %s' % attr
1175                s = str(s)
1176                line = line[:mo.start()] + s + line[end:]
1177                pos = mo.start() + len(s)
1178        # Drop line if it contains  unsubstituted {name} references.
1179        skipped = re.search(r'(?su)\{[^\\\W][-\w]*?\}(?!\\)', line)
1180        if skipped:
1181            trace('dropped line', line)
1182            continue;
1183        # Expand system attributes (eval has precedence).
1184        reos = [
1185            re.compile(r'(?su)\{(?P<action>eval):(?P<expr>.*?)\}(?!\\)'),
1186            re.compile(r'(?su)\{(?P<action>[^\\\W][-\w]*?):(?P<expr>.*?)\}(?!\\)'),
1187        ]
1188        skipped = False
1189        for reo in reos:
1190            pos = 0
1191            while True:
1192                mo = reo.search(line,pos)
1193                if not mo: break
1194                expr = mo.group('expr')
1195                action = mo.group('action')
1196                expr = expr.replace('{\\','{')
1197                expr = expr.replace('}\\','}')
1198                s = system(action, expr, attrs=dictionary)
1199                if dictionary is not None and action in ('counter','counter2','set','set2'):
1200                    # These actions create and update attributes.
1201                    attrs.update(dictionary)
1202                if s is None:
1203                    # Drop line if the action returns None.
1204                    skipped = True
1205                    break
1206                line = line[:mo.start()] + s + line[mo.end():]
1207                pos = mo.start() + len(s)
1208            if skipped:
1209                break
1210        if not skipped:
1211            # Remove backslash from escaped entries.
1212            line = line.replace('{\\','{')
1213            line = line.replace('}\\','}')
1214            result.append(line)
1215    if string_result:
1216        if result:
1217            return '\n'.join(result)
1218        else:
1219            return None
1220    else:
1221        return tuple(result)
1222
1223def char_encoding():
1224    encoding = document.attributes.get('encoding')
1225    if encoding:
1226        try:
1227            codecs.lookup(encoding)
1228        except LookupError,e:
1229            raise EAsciiDoc,str(e)
1230    return encoding
1231
1232def char_len(s):
1233    return len(char_decode(s))
1234
1235east_asian_widths = {'W': 2,   # Wide
1236                     'F': 2,   # Full-width (wide)
1237                     'Na': 1,  # Narrow
1238                     'H': 1,   # Half-width (narrow)
1239                     'N': 1,   # Neutral (not East Asian, treated as narrow)
1240                     'A': 1}   # Ambiguous (s/b wide in East Asian context,
1241                               # narrow otherwise, but that doesn't work)
1242"""Mapping of result codes from `unicodedata.east_asian_width()` to character
1243column widths."""
1244
1245def column_width(s):
1246    text = char_decode(s)
1247    if isinstance(text, unicode):
1248        width = 0
1249        for c in text:
1250            width += east_asian_widths[unicodedata.east_asian_width(c)]
1251        return width
1252    else:
1253        return len(text)
1254
1255def char_decode(s):
1256    if char_encoding():
1257        try:
1258            return s.decode(char_encoding())
1259        except Exception:
1260            raise EAsciiDoc, \
1261                "'%s' codec can't decode \"%s\"" % (char_encoding(), s)
1262    else:
1263        return s
1264
1265def char_encode(s):
1266    if char_encoding():
1267        return s.encode(char_encoding())
1268    else:
1269        return s
1270
1271def time_str(t):
1272    """Convert seconds since the Epoch to formatted local time string."""
1273    t = time.localtime(t)
1274    s = time.strftime('%H:%M:%S',t)
1275    if time.daylight and t.tm_isdst == 1:
1276        result = s + ' ' + time.tzname[1]
1277    else:
1278        result = s + ' ' + time.tzname[0]
1279    # Attempt to convert the localtime to the output encoding.
1280    try:
1281        result = char_encode(result.decode(locale.getdefaultlocale()[1]))
1282    except Exception:
1283        pass
1284    return result
1285
1286def date_str(t):
1287    """Convert seconds since the Epoch to formatted local date string."""
1288    t = time.localtime(t)
1289    return time.strftime('%Y-%m-%d',t)
1290
1291
1292class Lex:
1293    """Lexical analysis routines. Static methods and attributes only."""
1294    prev_element = None
1295    prev_cursor = None
1296    def __init__(self):
1297        raise AssertionError,'no class instances allowed'
1298    @staticmethod
1299    def next():
1300        """Returns class of next element on the input (None if EOF).  The
1301        reader is assumed to be at the first line following a previous element,
1302        end of file or line one.  Exits with the reader pointing to the first
1303        line of the next element or EOF (leading blank lines are skipped)."""
1304        reader.skip_blank_lines()
1305        if reader.eof(): return None
1306        # Optimization: If we've already checked for an element at this
1307        # position return the element.
1308        if Lex.prev_element and Lex.prev_cursor == reader.cursor:
1309            return Lex.prev_element
1310        if AttributeEntry.isnext():
1311            result = AttributeEntry
1312        elif AttributeList.isnext():
1313            result = AttributeList
1314        elif BlockTitle.isnext() and not tables_OLD.isnext():
1315            result = BlockTitle
1316        elif Title.isnext():
1317            if AttributeList.style() == 'float':
1318                result = FloatingTitle
1319            else:
1320                result = Title
1321        elif macros.isnext():
1322            result = macros.current
1323        elif lists.isnext():
1324            result = lists.current
1325        elif blocks.isnext():
1326            result = blocks.current
1327        elif tables_OLD.isnext():
1328            result = tables_OLD.current
1329        elif tables.isnext():
1330            result = tables.current
1331        else:
1332            if not paragraphs.isnext():
1333                raise EAsciiDoc,'paragraph expected'
1334            result = paragraphs.current
1335        # Optimization: Cache answer.
1336        Lex.prev_cursor = reader.cursor
1337        Lex.prev_element = result
1338        return result
1339
1340    @staticmethod
1341    def canonical_subs(options):
1342        """Translate composite subs values."""
1343        if len(options) == 1:
1344            if options[0] == 'none':
1345                options = ()
1346            elif options[0] == 'normal':
1347                options = config.subsnormal
1348            elif options[0] == 'verbatim':
1349                options = config.subsverbatim
1350        return options
1351
1352    @staticmethod
1353    def subs_1(s,options):
1354        """Perform substitution specified in 'options' (in 'options' order)."""
1355        if not s:
1356            return s
1357        if document.attributes.get('plaintext') is not None:
1358            options = ('specialcharacters',)
1359        result = s
1360        options = Lex.canonical_subs(options)
1361        for o in options:
1362            if o == 'specialcharacters':
1363                result = config.subs_specialchars(result)
1364            elif o == 'attributes':
1365                result = subs_attrs(result)
1366            elif o == 'quotes':
1367                result = subs_quotes(result)
1368            elif o == 'specialwords':
1369                result = config.subs_specialwords(result)
1370            elif o in ('replacements','replacements2','replacements3'):
1371                result = config.subs_replacements(result,o)
1372            elif o == 'macros':
1373                result = macros.subs(result)
1374            elif o == 'callouts':
1375                result = macros.subs(result,callouts=True)
1376            else:
1377                raise EAsciiDoc,'illegal substitution option: %s' % o
1378            trace(o, s, result)
1379            if not result:
1380                break
1381        return result
1382
1383    @staticmethod
1384    def subs(lines,options):
1385        """Perform inline processing specified by 'options' (in 'options'
1386        order) on sequence of 'lines'."""
1387        if not lines or not options:
1388            return lines
1389        options = Lex.canonical_subs(options)
1390        # Join lines so quoting can span multiple lines.
1391        para = '\n'.join(lines)
1392        if 'macros' in options:
1393            para = macros.extract_passthroughs(para)
1394        for o in options:
1395            if o == 'attributes':
1396                # If we don't substitute attributes line-by-line then a single
1397                # undefined attribute will drop the entire paragraph.
1398                lines = subs_attrs(para.split('\n'))
1399                para = '\n'.join(lines)
1400            else:
1401                para = Lex.subs_1(para,(o,))
1402        if 'macros' in options:
1403            para = macros.restore_passthroughs(para)
1404        return para.splitlines()
1405
1406    @staticmethod
1407    def set_margin(lines, margin=0):
1408        """Utility routine that sets the left margin to 'margin' space in a
1409        block of non-blank lines."""
1410        # Calculate width of block margin.
1411        lines = list(lines)
1412        width = len(lines[0])
1413        for s in lines:
1414            i = re.search(r'\S',s).start()
1415            if i < width: width = i
1416        # Strip margin width from all lines.
1417        for i in range(len(lines)):
1418            lines[i] = ' '*margin + lines[i][width:]
1419        return lines
1420
1421#---------------------------------------------------------------------------
1422# Document element classes parse AsciiDoc reader input and write DocBook writer
1423# output.
1424#---------------------------------------------------------------------------
1425class Document(object):
1426
1427    # doctype property.
1428    def getdoctype(self):
1429        return self.attributes.get('doctype')
1430    def setdoctype(self,doctype):
1431        self.attributes['doctype'] = doctype
1432    doctype = property(getdoctype,setdoctype)
1433
1434    # backend property.
1435    def getbackend(self):
1436        return self.attributes.get('backend')
1437    def setbackend(self,backend):
1438        if backend:
1439            backend = self.attributes.get('backend-alias-' + backend, backend)
1440        self.attributes['backend'] = backend
1441    backend = property(getbackend,setbackend)
1442
1443    def __init__(self):
1444        self.infile = None      # Source file name.
1445        self.outfile = None     # Output file name.
1446        self.attributes = InsensitiveDict()
1447        self.level = 0          # 0 => front matter. 1,2,3 => sect1,2,3.
1448        self.has_errors = False # Set true if processing errors were flagged.
1449        self.has_warnings = False # Set true if warnings were flagged.
1450        self.safe = False       # Default safe mode.
1451    def update_attributes(self,attrs=None):
1452        """
1453        Set implicit attributes and attributes in 'attrs'.
1454        """
1455        t = time.time()
1456        self.attributes['localtime'] = time_str(t)
1457        self.attributes['localdate'] = date_str(t)
1458        self.attributes['asciidoc-version'] = VERSION
1459        self.attributes['asciidoc-file'] = APP_FILE
1460        self.attributes['asciidoc-dir'] = APP_DIR
1461        if localapp():
1462            self.attributes['asciidoc-confdir'] = APP_DIR
1463        else:
1464            self.attributes['asciidoc-confdir'] = CONF_DIR
1465        self.attributes['user-dir'] = USER_DIR
1466        if config.verbose:
1467            self.attributes['verbose'] = ''
1468        # Update with configuration file attributes.
1469        if attrs:
1470            self.attributes.update(attrs)
1471        # Update with command-line attributes.
1472        self.attributes.update(config.cmd_attrs)
1473        # Extract miscellaneous configuration section entries from attributes.
1474        if attrs:
1475            config.load_miscellaneous(attrs)
1476        config.load_miscellaneous(config.cmd_attrs)
1477        self.attributes['newline'] = config.newline
1478        # File name related attributes can't be overridden.
1479        if self.infile is not None:
1480            if self.infile and os.path.exists(self.infile):
1481                t = os.path.getmtime(self.infile)
1482            elif self.infile == '<stdin>':
1483                t = time.time()
1484            else:
1485                t = None
1486            if t:
1487                self.attributes['doctime'] = time_str(t)
1488                self.attributes['docdate'] = date_str(t)
1489            if self.infile != '<stdin>':
1490                self.attributes['infile'] = self.infile
1491                self.attributes['indir'] = os.path.dirname(self.infile)
1492                self.attributes['docfile'] = self.infile
1493                self.attributes['docdir'] = os.path.dirname(self.infile)
1494                self.attributes['docname'] = os.path.splitext(
1495                        os.path.basename(self.infile))[0]
1496        if self.outfile:
1497            if self.outfile != '<stdout>':
1498                self.attributes['outfile'] = self.outfile
1499                self.attributes['outdir'] = os.path.dirname(self.outfile)
1500                if self.infile == '<stdin>':
1501                    self.attributes['docname'] = os.path.splitext(
1502                            os.path.basename(self.outfile))[0]
1503                ext = os.path.splitext(self.outfile)[1][1:]
1504            elif config.outfilesuffix:
1505                ext = config.outfilesuffix[1:]
1506            else:
1507                ext = ''
1508            if ext:
1509                self.attributes['filetype'] = ext
1510                self.attributes['filetype-'+ext] = ''
1511    def load_lang(self):
1512        """
1513        Load language configuration file.
1514        """
1515        lang = self.attributes.get('lang')
1516        if lang is None:
1517            filename = 'lang-en.conf'   # Default language file.
1518        else:
1519            filename = 'lang-' + lang + '.conf'
1520        if config.load_from_dirs(filename):
1521            self.attributes['lang'] = lang  # Reinstate new lang attribute.
1522        else:
1523            if lang is None:
1524                # The default language file must exist.
1525                message.error('missing conf file: %s' % filename, halt=True)
1526            else:
1527                message.warning('missing language conf file: %s' % filename)
1528    def set_deprecated_attribute(self,old,new):
1529        """
1530        Ensures the 'old' name of an attribute that was renamed to 'new' is
1531        still honored.
1532        """
1533        if self.attributes.get(new) is None:
1534            if self.attributes.get(old) is not None:
1535                self.attributes[new] = self.attributes[old]
1536        else:
1537            self.attributes[old] = self.attributes[new]
1538    def consume_attributes_and_comments(self,comments_only=False,noblanks=False):
1539        """
1540        Returns True if one or more attributes or comments were consumed.
1541        If 'noblanks' is True then consumation halts if a blank line is
1542        encountered.
1543        """
1544        result = False
1545        finished = False
1546        while not finished:
1547            finished = True
1548            if noblanks and not reader.read_next(): return result
1549            if blocks.isnext() and 'skip' in blocks.current.options:
1550                result = True
1551                finished = False
1552                blocks.current.translate()
1553            if noblanks and not reader.read_next(): return result
1554            if macros.isnext() and macros.current.name == 'comment':
1555                result = True
1556                finished = False
1557                macros.current.translate()
1558            if not comments_only:
1559                if AttributeEntry.isnext():
1560                    result = True
1561                    finished = False
1562                    AttributeEntry.translate()
1563                if AttributeList.isnext():
1564                    result = True
1565                    finished = False
1566                    AttributeList.translate()
1567        return result
1568    def parse_header(self,doctype,backend):
1569        """
1570        Parses header, sets corresponding document attributes and finalizes
1571        document doctype and backend properties.
1572        Returns False if the document does not have a header.
1573        'doctype' and 'backend' are the doctype and backend option values
1574        passed on the command-line, None if no command-line option was not
1575        specified.
1576        """
1577        assert self.level == 0
1578        # Skip comments and attribute entries that preceed the header.
1579        self.consume_attributes_and_comments()
1580        if doctype is not None:
1581            # Command-line overrides header.
1582            self.doctype = doctype
1583        elif self.doctype is None:
1584            # Was not set on command-line or in document header.
1585            self.doctype = DEFAULT_DOCTYPE
1586        # Process document header.
1587        has_header = (Title.isnext() and Title.level == 0
1588                      and AttributeList.style() != 'float')
1589        if self.doctype == 'manpage' and not has_header:
1590            message.error('manpage document title is mandatory',halt=True)
1591        if has_header:
1592            Header.parse()
1593        # Command-line entries override header derived entries.
1594        self.attributes.update(config.cmd_attrs)
1595        # DEPRECATED: revision renamed to revnumber.
1596        self.set_deprecated_attribute('revision','revnumber')
1597        # DEPRECATED: date renamed to revdate.
1598        self.set_deprecated_attribute('date','revdate')
1599        if doctype is not None:
1600            # Command-line overrides header.
1601            self.doctype = doctype
1602        if backend is not None:
1603            # Command-line overrides header.
1604            self.backend = backend
1605        elif self.backend is None:
1606            # Was not set on command-line or in document header.
1607            self.backend = DEFAULT_BACKEND
1608        else:
1609            # Has been set in document header.
1610            self.backend = self.backend # Translate alias in header.
1611        assert self.doctype in ('article','manpage','book'), 'illegal document type'
1612        return has_header
1613    def translate(self,has_header):
1614        if self.doctype == 'manpage':
1615            # Translate mandatory NAME section.
1616            if Lex.next() is not Title:
1617                message.error('name section expected')
1618            else:
1619                Title.translate()
1620                if Title.level != 1:
1621                    message.error('name section title must be at level 1')
1622                if not isinstance(Lex.next(),Paragraph):
1623                    message.error('malformed name section body')
1624                lines = reader.read_until(r'^$')
1625                s = ' '.join(lines)
1626                mo = re.match(r'^(?P<manname>.*?)\s+-\s+(?P<manpurpose>.*)$',s)
1627                if not mo:
1628                    message.error('malformed name section body')
1629                self.attributes['manname'] = mo.group('manname').strip()
1630                self.attributes['manpurpose'] = mo.group('manpurpose').strip()
1631                names = [s.strip() for s in self.attributes['manname'].split(',')]
1632                if len(names) > 9:
1633                    message.warning('too many manpage names')
1634                for i,name in enumerate(names):
1635                    self.attributes['manname%d' % (i+1)] = name
1636        if has_header:
1637            # Do postponed substitutions (backend confs have been loaded).
1638            self.attributes['doctitle'] = Title.dosubs(self.attributes['doctitle'])
1639            if config.header_footer:
1640                hdr = config.subs_section('header',{})
1641                writer.write(hdr,trace='header')
1642            if 'title' in self.attributes:
1643                del self.attributes['title']
1644            self.consume_attributes_and_comments()
1645            if self.doctype in ('article','book'):
1646                # Translate 'preamble' (untitled elements between header
1647                # and first section title).
1648                if Lex.next() is not Title:
1649                    stag,etag = config.section2tags('preamble')
1650                    writer.write(stag,trace='preamble open')
1651                    Section.translate_body()
1652                    writer.write(etag,trace='preamble close')
1653            elif self.doctype == 'manpage' and 'name' in config.sections:
1654                writer.write(config.subs_section('name',{}), trace='name')
1655        else:
1656            self.process_author_names()
1657            if config.header_footer:
1658                hdr = config.subs_section('header',{})
1659                writer.write(hdr,trace='header')
1660            if Lex.next() is not Title:
1661                Section.translate_body()
1662        # Process remaining sections.
1663        while not reader.eof():
1664            if Lex.next() is not Title:
1665                raise EAsciiDoc,'section title expected'
1666            Section.translate()
1667        Section.setlevel(0) # Write remaining unwritten section close tags.
1668        # Substitute document parameters and write document footer.
1669        if config.header_footer:
1670            ftr = config.subs_section('footer',{})
1671            writer.write(ftr,trace='footer')
1672    def parse_author(self,s):
1673        """ Return False if the author is malformed."""
1674        attrs = self.attributes # Alias for readability.
1675        s = s.strip()
1676        mo = re.match(r'^(?P<name1>[^<>\s]+)'
1677                '(\s+(?P<name2>[^<>\s]+))?'
1678                '(\s+(?P<name3>[^<>\s]+))?'
1679                '(\s+<(?P<email>\S+)>)?$',s)
1680        if not mo:
1681            # Names that don't match the formal specification.
1682            if s:
1683                attrs['firstname'] = s
1684            return
1685        firstname = mo.group('name1')
1686        if mo.group('name3'):
1687            middlename = mo.group('name2')
1688            lastname = mo.group('name3')
1689        else:
1690            middlename = None
1691            lastname = mo.group('name2')
1692        firstname = firstname.replace('_',' ')
1693        if middlename:
1694            middlename = middlename.replace('_',' ')
1695        if lastname:
1696            lastname = lastname.replace('_',' ')
1697        email = mo.group('email')
1698        if firstname:
1699            attrs['firstname'] = firstname
1700        if middlename:
1701            attrs['middlename'] = middlename
1702        if lastname:
1703            attrs['lastname'] = lastname
1704        if email:
1705            attrs['email'] = email
1706        return
1707    def process_author_names(self):
1708        """ Calculate any missing author related attributes."""
1709        attrs = self.attributes # Alias for readability.
1710        firstname = attrs.get('firstname','')
1711        middlename = attrs.get('middlename','')
1712        lastname = attrs.get('lastname','')
1713        author = attrs.get('author')
1714        initials = attrs.get('authorinitials')
1715        if author and not (firstname or middlename or lastname):
1716            self.parse_author(author)
1717            attrs['author'] = author.replace('_',' ')
1718            self.process_author_names()
1719            return
1720        if not author:
1721            author = '%s %s %s' % (firstname, middlename, lastname)
1722            author = author.strip()
1723            author = re.sub(r'\s+',' ', author)
1724        if not initials:
1725            initials = (char_decode(firstname)[:1] +
1726                       char_decode(middlename)[:1] + char_decode(lastname)[:1])
1727            initials = char_encode(initials).upper()
1728        names = [firstname,middlename,lastname,author,initials]
1729        for i,v in enumerate(names):
1730            v = config.subs_specialchars(v)
1731            v = subs_attrs(v)
1732            names[i] = v
1733        firstname,middlename,lastname,author,initials = names
1734        if firstname:
1735            attrs['firstname'] = firstname
1736        if middlename:
1737            attrs['middlename'] = middlename
1738        if lastname:
1739            attrs['lastname'] = lastname
1740        if author:
1741            attrs['author'] = author
1742        if initials:
1743            attrs['authorinitials'] = initials
1744        if author:
1745            attrs['authored'] = ''
1746
1747
1748class Header:
1749    """Static methods and attributes only."""
1750    REV_LINE_RE = r'^(\D*(?P<revnumber>.*?),)?(?P<revdate>.*?)(:\s*(?P<revremark>.*))?$'
1751    RCS_ID_RE = r'^\$Id: \S+ (?P<revnumber>\S+) (?P<revdate>\S+) \S+ (?P<author>\S+) (\S+ )?\$$'
1752    def __init__(self):
1753        raise AssertionError,'no class instances allowed'
1754    @staticmethod
1755    def parse():
1756        assert Lex.next() is Title and Title.level == 0
1757        attrs = document.attributes # Alias for readability.
1758        # Postpone title subs until backend conf files have been loaded.
1759        Title.translate(skipsubs=True)
1760        attrs['doctitle'] = Title.attributes['title']
1761        document.consume_attributes_and_comments(noblanks=True)
1762        s = reader.read_next()
1763        mo = None
1764        if s:
1765            # Process first header line after the title that is not a comment
1766            # or an attribute entry.
1767            s = reader.read()
1768            mo = re.match(Header.RCS_ID_RE,s)
1769            if not mo:
1770                document.parse_author(s)
1771                document.consume_attributes_and_comments(noblanks=True)
1772                if reader.read_next():
1773                    # Process second header line after the title that is not a
1774                    # comment or an attribute entry.
1775                    s = reader.read()
1776                    s = subs_attrs(s)
1777                    if s:
1778                        mo = re.match(Header.RCS_ID_RE,s)
1779                        if not mo:
1780                            mo = re.match(Header.REV_LINE_RE,s)
1781            document.consume_attributes_and_comments(noblanks=True)
1782        s = attrs.get('revnumber')
1783        if s:
1784            mo = re.match(Header.RCS_ID_RE,s)
1785        if mo:
1786            revnumber = mo.group('revnumber')
1787            if revnumber:
1788                attrs['revnumber'] = revnumber.strip()
1789            author = mo.groupdict().get('author')
1790            if author and 'firstname' not in attrs:
1791                document.parse_author(author)
1792            revremark = mo.groupdict().get('revremark')
1793            if revremark is not None:
1794                revremark = [revremark]
1795                # Revision remarks can continue on following lines.
1796                while reader.read_next():
1797                    if document.consume_attributes_and_comments(noblanks=True):
1798                        break
1799                    revremark.append(reader.read())
1800                revremark = Lex.subs(revremark,['normal'])
1801                revremark = '\n'.join(revremark).strip()
1802                attrs['revremark'] = revremark
1803            revdate = mo.group('revdate')
1804            if revdate:
1805                attrs['revdate'] = revdate.strip()
1806            elif revnumber or revremark:
1807                # Set revision date to ensure valid DocBook revision.
1808                attrs['revdate'] = attrs['docdate']
1809        document.process_author_names()
1810        if document.doctype == 'manpage':
1811            # manpage title formatted like mantitle(manvolnum).
1812            mo = re.match(r'^(?P<mantitle>.*)\((?P<manvolnum>.*)\)$',
1813                          attrs['doctitle'])
1814            if not mo:
1815                message.error('malformed manpage title')
1816            else:
1817                mantitle = mo.group('mantitle').strip()
1818                mantitle = subs_attrs(mantitle)
1819                if mantitle is None:
1820                    message.error('undefined attribute in manpage title')
1821                # mantitle is lowered only if in ALL CAPS
1822                if mantitle == mantitle.upper():
1823                    mantitle = mantitle.lower()
1824                attrs['mantitle'] = mantitle;
1825                attrs['manvolnum'] = mo.group('manvolnum').strip()
1826
1827class AttributeEntry:
1828    """Static methods and attributes only."""
1829    pattern = None
1830    subs = None
1831    name = None
1832    name2 = None
1833    value = None
1834    attributes = {}     # Accumulates all the parsed attribute entries.
1835    def __init__(self):
1836        raise AssertionError,'no class instances allowed'
1837    @staticmethod
1838    def isnext():
1839        result = False  # Assume not next.
1840        if not AttributeEntry.pattern:
1841            pat = document.attributes.get('attributeentry-pattern')
1842            if not pat:
1843                message.error("[attributes] missing 'attributeentry-pattern' entry")
1844            AttributeEntry.pattern = pat
1845        line = reader.read_next()
1846        if line:
1847            # Attribute entry formatted like :<name>[.<name2>]:[ <value>]
1848            mo = re.match(AttributeEntry.pattern,line)
1849            if mo:
1850                AttributeEntry.name = mo.group('attrname')
1851                AttributeEntry.name2 = mo.group('attrname2')
1852                AttributeEntry.value = mo.group('attrvalue') or ''
1853                AttributeEntry.value = AttributeEntry.value.strip()
1854                result = True
1855        return result
1856    @staticmethod
1857    def translate():
1858        assert Lex.next() is AttributeEntry
1859        attr = AttributeEntry    # Alias for brevity.
1860        reader.read()            # Discard attribute entry from reader.
1861        while attr.value.endswith(' +'):
1862            if not reader.read_next(): break
1863            attr.value = attr.value[:-1] + reader.read().strip()
1864        if attr.name2 is not None:
1865            # Configuration file attribute.
1866            if attr.name2 != '':
1867                # Section entry attribute.
1868                section = {}
1869                # Some sections can have name! syntax.
1870                if attr.name in ('attributes','miscellaneous') and attr.name2[-1] == '!':
1871                    section[attr.name] = [attr.name2]
1872                else:
1873                   section[attr.name] = ['%s=%s' % (attr.name2,attr.value)]
1874                config.load_sections(section)
1875                config.load_miscellaneous(config.conf_attrs)
1876            else:
1877                # Markup template section attribute.
1878                config.sections[attr.name] = [attr.value]
1879        else:
1880            # Normal attribute.
1881            if attr.name[-1] == '!':
1882                # Names like name! undefine the attribute.
1883                attr.name = attr.name[:-1]
1884                attr.value = None
1885            # Strip white space and illegal name chars.
1886            attr.name = re.sub(r'(?u)[^\w\-_]', '', attr.name).lower()
1887            # Don't override most command-line attributes.
1888            if attr.name in config.cmd_attrs \
1889                    and attr.name not in ('trace','numbered'):
1890                return
1891            # Update document attributes with attribute value.
1892            if attr.value is not None:
1893                mo = re.match(r'^pass:(?P<attrs>.*)\[(?P<value>.*)\]$', attr.value)
1894                if mo:
1895                    # Inline passthrough syntax.
1896                    attr.subs = mo.group('attrs')
1897                    attr.value = mo.group('value')  # Passthrough.
1898                else:
1899                    # Default substitution.
1900                    # DEPRECATED: attributeentry-subs
1901                    attr.subs = document.attributes.get('attributeentry-subs',
1902                                'specialcharacters,attributes')
1903                attr.subs = parse_options(attr.subs, SUBS_OPTIONS,
1904                            'illegal substitution option')
1905                attr.value = Lex.subs((attr.value,), attr.subs)
1906                attr.value = writer.newline.join(attr.value)
1907                document.attributes[attr.name] = attr.value
1908            elif attr.name in document.attributes:
1909                del document.attributes[attr.name]
1910            attr.attributes[attr.name] = attr.value
1911
1912class AttributeList:
1913    """Static methods and attributes only."""
1914    pattern = None
1915    match = None
1916    attrs = {}
1917    def __init__(self):
1918        raise AssertionError,'no class instances allowed'
1919    @staticmethod
1920    def initialize():
1921        if not 'attributelist-pattern' in document.attributes:
1922            message.error("[attributes] missing 'attributelist-pattern' entry")
1923        AttributeList.pattern = document.attributes['attributelist-pattern']
1924    @staticmethod
1925    def isnext():
1926        result = False  # Assume not next.
1927        line = reader.read_next()
1928        if line:
1929            mo = re.match(AttributeList.pattern, line)
1930            if mo:
1931                AttributeList.match = mo
1932                result = True
1933        return result
1934    @staticmethod
1935    def translate():
1936        assert Lex.next() is AttributeList
1937        reader.read()   # Discard attribute list from reader.
1938        attrs = {}
1939        d = AttributeList.match.groupdict()
1940        for k,v in d.items():
1941            if v is not None:
1942                if k == 'attrlist':
1943                    v = subs_attrs(v)
1944                    if v:
1945                        parse_attributes(v, attrs)
1946                else:
1947                    AttributeList.attrs[k] = v
1948        AttributeList.subs(attrs)
1949        AttributeList.attrs.update(attrs)
1950    @staticmethod
1951    def subs(attrs):
1952        '''Substitute single quoted attribute values normally.'''
1953        reo = re.compile(r"^'.*'$")
1954        for k,v in attrs.items():
1955            if reo.match(str(v)):
1956                attrs[k] = Lex.subs_1(v[1:-1], config.subsnormal)
1957    @staticmethod
1958    def style():
1959        return AttributeList.attrs.get('style') or AttributeList.attrs.get('1')
1960    @staticmethod
1961    def consume(d={}):
1962        """Add attribute list to the dictionary 'd' and reset the list."""
1963        if AttributeList.attrs:
1964            d.update(AttributeList.attrs)
1965            AttributeList.attrs = {}
1966            # Generate option attributes.
1967            if 'options' in d:
1968                options = parse_options(d['options'], (), 'illegal option name')
1969                for option in options:
1970                    d[option+'-option'] = ''
1971
1972class BlockTitle:
1973    """Static methods and attributes only."""
1974    title = None
1975    pattern = None
1976    def __init__(self):
1977        raise AssertionError,'no class instances allowed'
1978    @staticmethod
1979    def isnext():
1980        result = False  # Assume not next.
1981        line = reader.read_next()
1982        if line:
1983            mo = re.match(BlockTitle.pattern,line)
1984            if mo:
1985                BlockTitle.title = mo.group('title')
1986                result = True
1987        return result
1988    @staticmethod
1989    def translate():
1990        assert Lex.next() is BlockTitle
1991        reader.read()   # Discard title from reader.
1992        # Perform title substitutions.
1993        if not Title.subs:
1994            Title.subs = config.subsnormal
1995        s = Lex.subs((BlockTitle.title,), Title.subs)
1996        s = writer.newline.join(s)
1997        if not s:
1998            message.warning('blank block title')
1999        BlockTitle.title = s
2000    @staticmethod
2001    def consume(d={}):
2002        """If there is a title add it to dictionary 'd' then reset title."""
2003        if BlockTitle.title:
2004            d['title'] = BlockTitle.title
2005            BlockTitle.title = None
2006
2007class Title:
2008    """Processes Header and Section titles. Static methods and attributes
2009    only."""
2010    # Class variables
2011    underlines = ('==','--','~~','^^','++') # Levels 0,1,2,3,4.
2012    subs = ()
2013    pattern = None
2014    level = 0
2015    attributes = {}
2016    sectname = None
2017    section_numbers = [0]*len(underlines)
2018    dump_dict = {}
2019    linecount = None    # Number of lines in title (1 or 2).
2020    def __init__(self):
2021        raise AssertionError,'no class instances allowed'
2022    @staticmethod
2023    def translate(skipsubs=False):
2024        """Parse the Title.attributes and Title.level from the reader. The
2025        real work has already been done by parse()."""
2026        assert Lex.next() in (Title,FloatingTitle)
2027        # Discard title from reader.
2028        for i in range(Title.linecount):
2029            reader.read()
2030        Title.setsectname()
2031        if not skipsubs:
2032            Title.attributes['title'] = Title.dosubs(Title.attributes['title'])
2033    @staticmethod
2034    def dosubs(title):
2035        """
2036        Perform title substitutions.
2037        """
2038        if not Title.subs:
2039            Title.subs = config.subsnormal
2040        title = Lex.subs((title,), Title.subs)
2041        title = writer.newline.join(title)
2042        if not title:
2043            message.warning('blank section title')
2044        return title
2045    @staticmethod
2046    def isnext():
2047        lines = reader.read_ahead(2)
2048        return Title.parse(lines)
2049    @staticmethod
2050    def parse(lines):
2051        """Parse title at start of lines tuple."""
2052        if len(lines) == 0: return False
2053        if len(lines[0]) == 0: return False # Title can't be blank.
2054        # Check for single-line titles.
2055        result = False
2056        for level in range(len(Title.underlines)):
2057            k = 'sect%s' % level
2058            if k in Title.dump_dict:
2059                mo = re.match(Title.dump_dict[k], lines[0])
2060                if mo:
2061                    Title.attributes = mo.groupdict()
2062                    Title.level = level
2063                    Title.linecount = 1
2064                    result = True
2065                    break
2066        if not result:
2067            # Check for double-line titles.
2068            if not Title.pattern: return False  # Single-line titles only.
2069            if len(lines) < 2: return False
2070            title,ul = lines[:2]
2071            title_len = column_width(title)
2072            ul_len = char_len(ul)
2073            if ul_len < 2: return False
2074            # Fast elimination check.
2075            if ul[:2] not in Title.underlines: return False
2076            # Length of underline must be within +-3 of title.
2077            if not ((ul_len-3 < title_len < ul_len+3)
2078                    # Next test for backward compatibility.
2079                    or (ul_len-3 < char_len(title) < ul_len+3)):
2080                return False
2081            # Check for valid repetition of underline character pairs.
2082            s = ul[:2]*((ul_len+1)/2)
2083            if ul != s[:ul_len]: return False
2084            # Don't be fooled by back-to-back delimited blocks, require at
2085            # least one alphanumeric character in title.
2086            if not re.search(r'(?u)\w',title): return False
2087            mo = re.match(Title.pattern, title)
2088            if mo:
2089                Title.attributes = mo.groupdict()
2090                Title.level = list(Title.underlines).index(ul[:2])
2091                Title.linecount = 2
2092                result = True
2093        # Check for expected pattern match groups.
2094        if result:
2095            if not 'title' in Title.attributes:
2096                message.warning('[titles] entry has no <title> group')
2097                Title.attributes['title'] = lines[0]
2098            for k,v in Title.attributes.items():
2099                if v is None: del Title.attributes[k]
2100        try:
2101            Title.level += int(document.attributes.get('leveloffset','0'))
2102        except:
2103            pass
2104        Title.attributes['level'] = str(Title.level)
2105        return result
2106    @staticmethod
2107    def load(entries):
2108        """Load and validate [titles] section entries dictionary."""
2109        if 'underlines' in entries:
2110            errmsg = 'malformed [titles] underlines entry'
2111            try:
2112                underlines = parse_list(entries['underlines'])
2113            except Exception:
2114                raise EAsciiDoc,errmsg
2115            if len(underlines) != len(Title.underlines):
2116                raise EAsciiDoc,errmsg
2117            for s in underlines:
2118                if len(s) !=2:
2119                    raise EAsciiDoc,errmsg
2120            Title.underlines = tuple(underlines)
2121            Title.dump_dict['underlines'] = entries['underlines']
2122        if 'subs' in entries:
2123            Title.subs = parse_options(entries['subs'], SUBS_OPTIONS,
2124                'illegal [titles] subs entry')
2125            Title.dump_dict['subs'] = entries['subs']
2126        if 'sectiontitle' in entries:
2127            pat = entries['sectiontitle']
2128            if not pat or not is_re(pat):
2129                raise EAsciiDoc,'malformed [titles] sectiontitle entry'
2130            Title.pattern = pat
2131            Title.dump_dict['sectiontitle'] = pat
2132        if 'blocktitle' in entries:
2133            pat = entries['blocktitle']
2134            if not pat or not is_re(pat):
2135                raise EAsciiDoc,'malformed [titles] blocktitle entry'
2136            BlockTitle.pattern = pat
2137            Title.dump_dict['blocktitle'] = pat
2138        # Load single-line title patterns.
2139        for k in ('sect0','sect1','sect2','sect3','sect4'):
2140            if k in entries:
2141                pat = entries[k]
2142                if not pat or not is_re(pat):
2143                    raise EAsciiDoc,'malformed [titles] %s entry' % k
2144                Title.dump_dict[k] = pat
2145        # TODO: Check we have either a Title.pattern or at least one
2146        # single-line title pattern -- can this be done here or do we need
2147        # check routine like the other block checkers?
2148    @staticmethod
2149    def dump():
2150        dump_section('titles',Title.dump_dict)
2151    @staticmethod
2152    def setsectname():
2153        """
2154        Set Title section name:
2155        If the first positional or 'template' attribute is set use it,
2156        next search for section title in [specialsections],
2157        if not found use default 'sect<level>' name.
2158        """
2159        sectname = AttributeList.attrs.get('1')
2160        if sectname and sectname != 'float':
2161            Title.sectname = sectname
2162        elif 'template' in AttributeList.attrs:
2163            Title.sectname = AttributeList.attrs['template']
2164        else:
2165            for pat,sect in config.specialsections.items():
2166                mo = re.match(pat,Title.attributes['title'])
2167                if mo:
2168                    title = mo.groupdict().get('title')
2169                    if title is not None:
2170                        Title.attributes['title'] = title.strip()
2171                    else:
2172                        Title.attributes['title'] = mo.group().strip()
2173                    Title.sectname = sect
2174                    break
2175            else:
2176                Title.sectname = 'sect%d' % Title.level
2177    @staticmethod
2178    def getnumber(level):
2179        """Return next section number at section 'level' formatted like
2180        1.2.3.4."""
2181        number = ''
2182        for l in range(len(Title.section_numbers)):
2183            n = Title.section_numbers[l]
2184            if l == 0:
2185                continue
2186            elif l < level:
2187                number = '%s%d.' % (number, n)
2188            elif l == level:
2189                number = '%s%d.' % (number, n + 1)
2190                Title.section_numbers[l] = n + 1
2191            elif l > level:
2192                # Reset unprocessed section levels.
2193                Title.section_numbers[l] = 0
2194        return number
2195
2196
2197class FloatingTitle(Title):
2198    '''Floated titles are translated differently.'''
2199    @staticmethod
2200    def isnext():
2201        return Title.isnext() and AttributeList.style() == 'float'
2202    @staticmethod
2203    def translate():
2204        assert Lex.next() is FloatingTitle
2205        Title.translate()
2206        Section.set_id()
2207        AttributeList.consume(Title.attributes)
2208        template = 'floatingtitle'
2209        if template in config.sections:
2210            stag,etag = config.section2tags(template,Title.attributes)
2211            writer.write(stag,trace='floating title')
2212        else:
2213            message.warning('missing template section: [%s]' % template)
2214
2215
2216class Section:
2217    """Static methods and attributes only."""
2218    endtags = []  # Stack of currently open section (level,endtag) tuples.
2219    ids = []      # List of already used ids.
2220    def __init__(self):
2221        raise AssertionError,'no class instances allowed'
2222    @staticmethod
2223    def savetag(level,etag):
2224        """Save section end."""
2225        Section.endtags.append((level,etag))
2226    @staticmethod
2227    def setlevel(level):
2228        """Set document level and write open section close tags up to level."""
2229        while Section.endtags and Section.endtags[-1][0] >= level:
2230            writer.write(Section.endtags.pop()[1],trace='section close')
2231        document.level = level
2232    @staticmethod
2233    def gen_id(title):
2234        """
2235        The normalized value of the id attribute is an NCName according to
2236        the 'Namespaces in XML' Recommendation:
2237        NCName          ::=     NCNameStartChar NCNameChar*
2238        NCNameChar      ::=     NameChar - ':'
2239        NCNameStartChar ::=     Letter | '_'
2240        NameChar        ::=     Letter | Digit | '.' | '-' | '_' | ':'
2241        """
2242        # Replace non-alpha numeric characters in title with underscores and
2243        # convert to lower case.
2244        base_id = re.sub(r'(?u)\W+', '_', char_decode(title)).strip('_').lower()
2245        if 'ascii-ids' in document.attributes:
2246            # Replace non-ASCII characters with ASCII equivalents.
2247            import unicodedata
2248            base_id = unicodedata.normalize('NFKD', base_id).encode('ascii','ignore')
2249        base_id = char_encode(base_id)
2250        # Prefix the ID name with idprefix attribute or underscore if not
2251        # defined. Prefix ensures the ID does not clash with existing IDs.
2252        idprefix = document.attributes.get('idprefix','_')
2253        base_id = idprefix + base_id
2254        i = 1
2255        while True:
2256            if i == 1:
2257                id = base_id
2258            else:
2259                id = '%s_%d' % (base_id, i)
2260            if id not in Section.ids:
2261                Section.ids.append(id)
2262                return id
2263            else:
2264                id = base_id
2265            i += 1
2266    @staticmethod
2267    def set_id():
2268        if not document.attributes.get('sectids') is None \
2269                and 'id' not in AttributeList.attrs:
2270            # Generate ids for sections.
2271            AttributeList.attrs['id'] = Section.gen_id(Title.attributes['title'])
2272    @staticmethod
2273    def translate():
2274        assert Lex.next() is Title
2275        prev_sectname = Title.sectname
2276        Title.translate()
2277        if Title.level == 0 and document.doctype != 'book':
2278            message.error('only book doctypes can contain level 0 sections')
2279        if Title.level > document.level \
2280                and 'basebackend-docbook' in document.attributes \
2281                and prev_sectname in ('colophon','abstract', \
2282                    'dedication','glossary','bibliography'):
2283            message.error('%s section cannot contain sub-sections' % prev_sectname)
2284        if Title.level > document.level+1:
2285            # Sub-sections of multi-part book level zero Preface and Appendices
2286            # are meant to be out of sequence.
2287            if document.doctype == 'book' \
2288                    and document.level == 0 \
2289                    and Title.level == 2 \
2290                    and prev_sectname in ('preface','appendix'):
2291                pass
2292            else:
2293                message.warning('section title out of sequence: '
2294                    'expected level %d, got level %d'
2295                    % (document.level+1, Title.level))
2296        Section.set_id()
2297        Section.setlevel(Title.level)
2298        if 'numbered' in document.attributes:
2299            Title.attributes['sectnum'] = Title.getnumber(document.level)
2300        else:
2301            Title.attributes['sectnum'] = ''
2302        AttributeList.consume(Title.attributes)
2303        stag,etag = config.section2tags(Title.sectname,Title.attributes)
2304        Section.savetag(Title.level,etag)
2305        writer.write(stag,trace='section open: level %d: %s' %
2306                (Title.level, Title.attributes['title']))
2307        Section.translate_body()
2308    @staticmethod
2309    def translate_body(terminator=Title):
2310        isempty = True
2311        next = Lex.next()
2312        while next and next is not terminator:
2313            if isinstance(terminator,DelimitedBlock) and next is Title:
2314                message.error('section title not permitted in delimited block')
2315            next.translate()
2316            next = Lex.next()
2317            isempty = False
2318        # The section is not empty if contains a subsection.
2319        if next and isempty and Title.level > document.level:
2320            isempty = False
2321        # Report empty sections if invalid markup will result.
2322        if isempty:
2323            if document.backend == 'docbook' and Title.sectname != 'index':
2324                message.error('empty section is not valid')
2325
2326class AbstractBlock:
2327
2328    blocknames = [] # Global stack of names for push_blockname() and pop_blockname().
2329
2330    def __init__(self):
2331        # Configuration parameter names common to all blocks.
2332        self.CONF_ENTRIES = ('delimiter','options','subs','presubs','postsubs',
2333                             'posattrs','style','.*-style','template','filter')
2334        self.start = None   # File reader cursor at start delimiter.
2335        self.defname=None   # Configuration file block definition section name.
2336        # Configuration parameters.
2337        self.delimiter=None # Regular expression matching block delimiter.
2338        self.delimiter_reo=None # Compiled delimiter.
2339        self.template=None  # template section entry.
2340        self.options=()     # options entry list.
2341        self.presubs=None   # presubs/subs entry list.
2342        self.postsubs=()    # postsubs entry list.
2343        self.filter=None    # filter entry.
2344        self.posattrs=()    # posattrs entry list.
2345        self.style=None     # Default style.
2346        self.styles=OrderedDict() # Each entry is a styles dictionary.
2347        # Before a block is processed it's attributes (from it's
2348        # attributes list) are merged with the block configuration parameters
2349        # (by self.merge_attributes()) resulting in the template substitution
2350        # dictionary (self.attributes) and the block's processing parameters
2351        # (self.parameters).
2352        self.attributes={}
2353        # The names of block parameters.
2354        self.PARAM_NAMES=('template','options','presubs','postsubs','filter')
2355        self.parameters=None
2356        # Leading delimiter match object.
2357        self.mo=None
2358    def short_name(self):
2359        """ Return the text following the first dash in the section name."""
2360        i = self.defname.find('-')
2361        if i == -1:
2362            return self.defname
2363        else:
2364            return self.defname[i+1:]
2365    def error(self, msg, cursor=None, halt=False):
2366        message.error('[%s] %s' % (self.defname,msg), cursor, halt)
2367    def is_conf_entry(self,param):
2368        """Return True if param matches an allowed configuration file entry
2369        name."""
2370        for s in self.CONF_ENTRIES:
2371            if re.match('^'+s+'$',param):
2372                return True
2373        return False
2374    def load(self,defname,entries):
2375        """Update block definition from section 'entries' dictionary."""
2376        self.defname = defname
2377        self.update_parameters(entries, self, all=True)
2378    def update_parameters(self, src, dst=None, all=False):
2379        """
2380        Parse processing parameters from src dictionary to dst object.
2381        dst defaults to self.parameters.
2382        If all is True then copy src entries that aren't parameter names.
2383        """
2384        dst = dst or self.parameters
2385        msg = '[%s] malformed entry %%s: %%s' % self.defname
2386        def copy(obj,k,v):
2387            if isinstance(obj,dict):
2388                obj[k] = v
2389            else:
2390                setattr(obj,k,v)
2391        for k,v in src.items():
2392            if not re.match(r'\d+',k) and not is_name(k):
2393                raise EAsciiDoc, msg % (k,v)
2394            if k == 'template':
2395                if not is_name(v):
2396                    raise EAsciiDoc, msg % (k,v)
2397                copy(dst,k,v)
2398            elif k == 'filter':
2399                copy(dst,k,v)
2400            elif k == 'options':
2401                if isinstance(v,str):
2402                    v = parse_options(v, (), msg % (k,v))
2403                    # Merge with existing options.
2404                    v = tuple(set(dst.options).union(set(v)))
2405                copy(dst,k,v)
2406            elif k in ('subs','presubs','postsubs'):
2407                # Subs is an alias for presubs.
2408                if k == 'subs': k = 'presubs'
2409                if isinstance(v,str):
2410                    v = parse_options(v, SUBS_OPTIONS, msg % (k,v))
2411                copy(dst,k,v)
2412            elif k == 'delimiter':
2413                if v and is_re(v):
2414                    copy(dst,k,v)
2415                else:
2416                    raise EAsciiDoc, msg % (k,v)
2417            elif k == 'style':
2418                if is_name(v):
2419                    copy(dst,k,v)
2420                else:
2421                    raise EAsciiDoc, msg % (k,v)
2422            elif k == 'posattrs':
2423                v = parse_options(v, (), msg % (k,v))
2424                copy(dst,k,v)
2425            else:
2426                mo = re.match(r'^(?P<style>.*)-style$',k)
2427                if mo:
2428                    if not v:
2429                        raise EAsciiDoc, msg % (k,v)
2430                    style = mo.group('style')
2431                    if not is_name(style):
2432                        raise EAsciiDoc, msg % (k,v)
2433                    d = {}
2434                    if not parse_named_attributes(v,d):
2435                        raise EAsciiDoc, msg % (k,v)
2436                    if 'subs' in d:
2437                        # Subs is an alias for presubs.
2438                        d['presubs'] = d['subs']
2439                        del d['subs']
2440                    self.styles[style] = d
2441                elif all or k in self.PARAM_NAMES:
2442                    copy(dst,k,v) # Derived class specific entries.
2443    def get_param(self,name,params=None):
2444        """
2445        Return named processing parameter from params dictionary.
2446        If the parameter is not in params look in self.parameters.
2447        """
2448        if params and name in params:
2449            return params[name]
2450        elif name in self.parameters:
2451            return self.parameters[name]
2452        else:
2453            return None
2454    def get_subs(self,params=None):
2455        """
2456        Return (presubs,postsubs) tuple.
2457        """
2458        presubs = self.get_param('presubs',params)
2459        postsubs = self.get_param('postsubs',params)
2460        return (presubs,postsubs)
2461    def dump(self):
2462        """Write block definition to stdout."""
2463        write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
2464        write('['+self.defname+']')
2465        if self.is_conf_entry('delimiter'):
2466            write('delimiter='+self.delimiter)
2467        if self.template:
2468            write('template='+self.template)
2469        if self.options:
2470            write('options='+','.join(self.options))
2471        if self.presubs:
2472            if self.postsubs:
2473                write('presubs='+','.join(self.presubs))
2474            else:
2475                write('subs='+','.join(self.presubs))
2476        if self.postsubs:
2477            write('postsubs='+','.join(self.postsubs))
2478        if self.filter:
2479            write('filter='+self.filter)
2480        if self.posattrs:
2481            write('posattrs='+','.join(self.posattrs))
2482        if self.style:
2483            write('style='+self.style)
2484        if self.styles:
2485            for style,d in self.styles.items():
2486                s = ''
2487                for k,v in d.items(): s += '%s=%r,' % (k,v)
2488                write('%s-style=%s' % (style,s[:-1]))
2489    def validate(self):
2490        """Validate block after the complete configuration has been loaded."""
2491        if self.is_conf_entry('delimiter') and not self.delimiter:
2492            raise EAsciiDoc,'[%s] missing delimiter' % self.defname
2493        if self.style:
2494            if not is_name(self.style):
2495                raise EAsciiDoc, 'illegal style name: %s' % self.style
2496            if not self.style in self.styles:
2497                if not isinstance(self,List):   # Lists don't have templates.
2498                    message.warning('[%s] \'%s\' style not in %s' % (
2499                        self.defname,self.style,self.styles.keys()))
2500        # Check all styles for missing templates.
2501        all_styles_have_template = True
2502        for k,v in self.styles.items():
2503            t = v.get('template')
2504            if t and not t in config.sections:
2505                # Defer check if template name contains attributes.
2506                if not re.search(r'{.+}',t):
2507                    message.warning('missing template section: [%s]' % t)
2508            if not t:
2509                all_styles_have_template = False
2510        # Check we have a valid template entry or alternatively that all the
2511        # styles have templates.
2512        if self.is_conf_entry('template') and not 'skip' in self.options:
2513            if self.template:
2514                if not self.template in config.sections:
2515                    # Defer check if template name contains attributes.
2516                    if not re.search(r'{.+}',self.template):
2517                        message.warning('missing template section: [%s]'
2518                                        % self.template)
2519            elif not all_styles_have_template:
2520                if not isinstance(self,List): # Lists don't have templates.
2521                    message.warning('missing styles templates: [%s]' % self.defname)
2522    def isnext(self):
2523        """Check if this block is next in document reader."""
2524        result = False
2525        reader.skip_blank_lines()
2526        if reader.read_next():
2527            if not self.delimiter_reo:
2528                # Cache compiled delimiter optimization.
2529                self.delimiter_reo = re.compile(self.delimiter)
2530            mo = self.delimiter_reo.match(reader.read_next())
2531            if mo:
2532                self.mo = mo
2533                result = True
2534        return result
2535    def translate(self):
2536        """Translate block from document reader."""
2537        if not self.presubs:
2538            self.presubs = config.subsnormal
2539        if reader.cursor:
2540            self.start = reader.cursor[:]
2541    def push_blockname(self, blockname=None):
2542        '''
2543        On block entry set the 'blockname' attribute.
2544        Only applies to delimited blocks, lists and tables.
2545        '''
2546        if blockname is None:
2547            blockname = self.attributes.get('style', self.short_name()).lower()
2548        trace('push blockname', blockname)
2549        self.blocknames.append(blockname)
2550        document.attributes['blockname'] = blockname
2551    def pop_blockname(self):
2552        '''
2553        On block exits restore previous (parent) 'blockname' attribute or
2554        undefine it if we're no longer inside a block.
2555        '''
2556        assert len(self.blocknames) > 0
2557        blockname = self.blocknames.pop()
2558        trace('pop blockname', blockname)
2559        if len(self.blocknames) == 0:
2560            document.attributes['blockname'] = None
2561        else:
2562            document.attributes['blockname'] = self.blocknames[-1]
2563    def merge_attributes(self,attrs,params=[]):
2564        """
2565        Use the current block's attribute list (attrs dictionary) to build a
2566        dictionary of block processing parameters (self.parameters) and tag
2567        substitution attributes (self.attributes).
2568
2569        1. Copy the default parameters (self.*) to self.parameters.
2570        self.parameters are used internally to render the current block.
2571        Optional params array of additional parameters.
2572
2573        2. Copy attrs to self.attributes. self.attributes are used for template
2574        and tag substitution in the current block.
2575
2576        3. If a style attribute was specified update self.parameters with the
2577        corresponding style parameters; if there are any style parameters
2578        remaining add them to self.attributes (existing attribute list entries
2579        take precedence).
2580
2581        4. Set named positional attributes in self.attributes if self.posattrs
2582        was specified.
2583
2584        5. Finally self.parameters is updated with any corresponding parameters
2585        specified in attrs.
2586
2587        """
2588
2589        def check_array_parameter(param):
2590            # Check the parameter is a sequence type.
2591            if not is_array(self.parameters[param]):
2592                message.error('malformed %s parameter: %s' %
2593                        (param, self.parameters[param]))
2594                # Revert to default value.
2595                self.parameters[param] = getattr(self,param)
2596
2597        params = list(self.PARAM_NAMES) + params
2598        self.attributes = {}
2599        if self.style:
2600            # If a default style is defined make it available in the template.
2601            self.attributes['style'] = self.style
2602        self.attributes.update(attrs)
2603        # Calculate dynamic block parameters.
2604        # Start with configuration file defaults.
2605        self.parameters = AttrDict()
2606        for name in params:
2607            self.parameters[name] = getattr(self,name)
2608        # Load the selected style attributes.
2609        posattrs = self.posattrs
2610        if posattrs and posattrs[0] == 'style':
2611            # Positional attribute style has highest precedence.
2612            style = self.attributes.get('1')
2613        else:
2614            style = None
2615        if not style:
2616            # Use explicit style attribute, fall back to default style.
2617            style = self.attributes.get('style',self.style)
2618        if style:
2619            if not is_name(style):
2620                message.error('illegal style name: %s' % style)
2621                style = self.style
2622            # Lists have implicit styles and do their own style checks.
2623            elif style not in self.styles and not isinstance(self,List):
2624                message.warning('missing style: [%s]: %s' % (self.defname,style))
2625                style = self.style
2626            if style in self.styles:
2627                self.attributes['style'] = style
2628                for k,v in self.styles[style].items():
2629                    if k == 'posattrs':
2630                        posattrs = v
2631                    elif k in params:
2632                        self.parameters[k] = v
2633                    elif not k in self.attributes:
2634                        # Style attributes don't take precedence over explicit.
2635                        self.attributes[k] = v
2636        # Set named positional attributes.
2637        for i,v in enumerate(posattrs):
2638            if str(i+1) in self.attributes:
2639                self.attributes[v] = self.attributes[str(i+1)]
2640        # Override config and style attributes with attribute list attributes.
2641        self.update_parameters(attrs)
2642        check_array_parameter('options')
2643        check_array_parameter('presubs')
2644        check_array_parameter('postsubs')
2645
2646class AbstractBlocks:
2647    """List of block definitions."""
2648    PREFIX = ''         # Conf file section name prefix set in derived classes.
2649    BLOCK_TYPE = None   # Block type set in derived classes.
2650    def __init__(self):
2651        self.current=None
2652        self.blocks = []        # List of Block objects.
2653        self.default = None     # Default Block.
2654        self.delimiters = None  # Combined delimiters regular expression.
2655    def load(self,sections):
2656        """Load block definition from 'sections' dictionary."""
2657        for k in sections.keys():
2658            if re.match(r'^'+ self.PREFIX + r'.+$',k):
2659                d = {}
2660                parse_entries(sections.get(k,()),d)
2661                for b in self.blocks:
2662                    if b.defname == k:
2663                        break
2664                else:
2665                    b = self.BLOCK_TYPE()
2666                    self.blocks.append(b)
2667                try:
2668                    b.load(k,d)
2669                except EAsciiDoc,e:
2670                    raise EAsciiDoc,'[%s] %s' % (k,str(e))
2671    def dump(self):
2672        for b in self.blocks:
2673            b.dump()
2674    def isnext(self):
2675        for b in self.blocks:
2676            if b.isnext():
2677                self.current = b
2678                return True;
2679        return False
2680    def validate(self):
2681        """Validate the block definitions."""
2682        # Validate delimiters and build combined lists delimiter pattern.
2683        delimiters = []
2684        for b in self.blocks:
2685            assert b.__class__ is self.BLOCK_TYPE
2686            b.validate()
2687            if b.delimiter:
2688                delimiters.append(b.delimiter)
2689        self.delimiters = re_join(delimiters)
2690
2691class Paragraph(AbstractBlock):
2692    def __init__(self):
2693        AbstractBlock.__init__(self)
2694        self.text=None          # Text in first line of paragraph.
2695    def load(self,name,entries):
2696        AbstractBlock.load(self,name,entries)
2697    def dump(self):
2698        AbstractBlock.dump(self)
2699        write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
2700        write('')
2701    def isnext(self):
2702        result = AbstractBlock.isnext(self)
2703        if result:
2704            self.text = self.mo.groupdict().get('text')
2705        return result
2706    def translate(self):
2707        AbstractBlock.translate(self)
2708        attrs = self.mo.groupdict().copy()
2709        if 'text' in attrs: del attrs['text']
2710        BlockTitle.consume(attrs)
2711        AttributeList.consume(attrs)
2712        self.merge_attributes(attrs)
2713        reader.read()   # Discard (already parsed item first line).
2714        body = reader.read_until(paragraphs.terminators)
2715        if 'skip' in self.parameters.options:
2716            return
2717        body = [self.text] + list(body)
2718        presubs = self.parameters.presubs
2719        postsubs = self.parameters.postsubs
2720        if document.attributes.get('plaintext') is None:
2721            body = Lex.set_margin(body) # Move body to left margin.
2722        body = Lex.subs(body,presubs)
2723        template = self.parameters.template
2724        template = subs_attrs(template,attrs)
2725        stag = config.section2tags(template, self.attributes,skipend=True)[0]
2726        if self.parameters.filter:
2727            body = filter_lines(self.parameters.filter,body,self.attributes)
2728        body = Lex.subs(body,postsubs)
2729        etag = config.section2tags(template, self.attributes,skipstart=True)[1]
2730        # Write start tag, content, end tag.
2731        writer.write(dovetail_tags(stag,body,etag),trace='paragraph')
2732
2733class Paragraphs(AbstractBlocks):
2734    """List of paragraph definitions."""
2735    BLOCK_TYPE = Paragraph
2736    PREFIX = 'paradef-'
2737    def __init__(self):
2738        AbstractBlocks.__init__(self)
2739        self.terminators=None    # List of compiled re's.
2740    def initialize(self):
2741        self.terminators = [
2742                re.compile(r'^\+$|^$'),
2743                re.compile(AttributeList.pattern),
2744                re.compile(blocks.delimiters),
2745                re.compile(tables.delimiters),
2746                re.compile(tables_OLD.delimiters),
2747            ]
2748    def load(self,sections):
2749        AbstractBlocks.load(self,sections)
2750    def validate(self):
2751        AbstractBlocks.validate(self)
2752        # Check we have a default paragraph definition, put it last in list.
2753        for b in self.blocks:
2754            if b.defname == 'paradef-default':
2755                self.blocks.append(b)
2756                self.default = b
2757                self.blocks.remove(b)
2758                break
2759        else:
2760            raise EAsciiDoc,'missing section: [paradef-default]'
2761
2762class List(AbstractBlock):
2763    NUMBER_STYLES= ('arabic','loweralpha','upperalpha','lowerroman',
2764                    'upperroman')
2765    def __init__(self):
2766        AbstractBlock.__init__(self)
2767        self.CONF_ENTRIES += ('type','tags')
2768        self.PARAM_NAMES += ('tags',)
2769        # listdef conf file parameters.
2770        self.type=None
2771        self.tags=None      # Name of listtags-<tags> conf section.
2772        # Calculated parameters.
2773        self.tag=None       # Current tags AttrDict.
2774        self.label=None     # List item label (labeled lists).
2775        self.text=None      # Text in first line of list item.
2776        self.index=None     # Matched delimiter 'index' group (numbered lists).
2777        self.type=None      # List type ('numbered','bulleted','labeled').
2778        self.ordinal=None   # Current list item ordinal number (1..)
2779        self.number_style=None # Current numbered list style ('arabic'..)
2780    def load(self,name,entries):
2781        AbstractBlock.load(self,name,entries)
2782    def dump(self):
2783        AbstractBlock.dump(self)
2784        write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
2785        write('type='+self.type)
2786        write('tags='+self.tags)
2787        write('')
2788    def validate(self):
2789        AbstractBlock.validate(self)
2790        tags = [self.tags]
2791        tags += [s['tags'] for s in self.styles.values() if 'tags' in s]
2792        for t in tags:
2793            if t not in lists.tags:
2794                self.error('missing section: [listtags-%s]' % t,halt=True)
2795    def isnext(self):
2796        result = AbstractBlock.isnext(self)
2797        if result:
2798            self.label = self.mo.groupdict().get('label')
2799            self.text = self.mo.groupdict().get('text')
2800            self.index = self.mo.groupdict().get('index')
2801        return result
2802    def translate_entry(self):
2803        assert self.type == 'labeled'
2804        entrytag = subs_tag(self.tag.entry, self.attributes)
2805        labeltag = subs_tag(self.tag.label, self.attributes)
2806        writer.write(entrytag[0],trace='list entry open')
2807        writer.write(labeltag[0],trace='list label open')
2808        # Write labels.
2809        while Lex.next() is self:
2810            reader.read()   # Discard (already parsed item first line).
2811            writer.write_tag(self.tag.term, [self.label],
2812                             self.presubs, self.attributes,trace='list term')
2813            if self.text: break
2814        writer.write(labeltag[1],trace='list label close')
2815        # Write item text.
2816        self.translate_item()
2817        writer.write(entrytag[1],trace='list entry close')
2818    def translate_item(self):
2819        if self.type == 'callout':
2820            self.attributes['coids'] = calloutmap.calloutids(self.ordinal)
2821        itemtag = subs_tag(self.tag.item, self.attributes)
2822        writer.write(itemtag[0],trace='list item open')
2823        # Write ItemText.
2824        text = reader.read_until(lists.terminators)
2825        if self.text:
2826            text = [self.text] + list(text)
2827        if text:
2828            writer.write_tag(self.tag.text, text, self.presubs, self.attributes,trace='list text')
2829        # Process explicit and implicit list item continuations.
2830        while True:
2831            continuation = reader.read_next() == '+'
2832            if continuation: reader.read()  # Discard continuation line.
2833            while Lex.next() in (BlockTitle,AttributeList):
2834                # Consume continued element title and attributes.
2835                Lex.next().translate()
2836            if not continuation and BlockTitle.title:
2837                # Titled elements terminate the list.
2838                break
2839            next = Lex.next()
2840            if next in lists.open:
2841                break
2842            elif isinstance(next,List):
2843                next.translate()
2844            elif isinstance(next,Paragraph) and 'listelement' in next.options:
2845                next.translate()
2846            elif continuation:
2847                # This is where continued elements are processed.
2848                if next is Title:
2849                    message.error('section title not allowed in list item',halt=True)
2850                next.translate()
2851            else:
2852                break
2853        writer.write(itemtag[1],trace='list item close')
2854
2855    @staticmethod
2856    def calc_style(index):
2857        """Return the numbered list style ('arabic'...) of the list item index.
2858        Return None if unrecognized style."""
2859        if re.match(r'^\d+[\.>]$', index):
2860            style = 'arabic'
2861        elif re.match(r'^[ivx]+\)$', index):
2862            style = 'lowerroman'
2863        elif re.match(r'^[IVX]+\)$', index):
2864            style = 'upperroman'
2865        elif re.match(r'^[a-z]\.$', index):
2866            style = 'loweralpha'
2867        elif re.match(r'^[A-Z]\.$', index):
2868            style = 'upperalpha'
2869        else:
2870            assert False
2871        return style
2872
2873    @staticmethod
2874    def calc_index(index,style):
2875        """Return the ordinal number of (1...) of the list item index
2876        for the given list style."""
2877        def roman_to_int(roman):
2878            roman = roman.lower()
2879            digits = {'i':1,'v':5,'x':10}
2880            result = 0
2881            for i in range(len(roman)):
2882                digit = digits[roman[i]]
2883                # If next digit is larger this digit is negative.
2884                if i+1 < len(roman) and digits[roman[i+1]] > digit:
2885                    result -= digit
2886                else:
2887                    result += digit
2888            return result
2889        index = index[:-1]
2890        if style == 'arabic':
2891            ordinal = int(index)
2892        elif style == 'lowerroman':
2893            ordinal = roman_to_int(index)
2894        elif style == 'upperroman':
2895            ordinal = roman_to_int(index)
2896        elif style == 'loweralpha':
2897            ordinal = ord(index) - ord('a') + 1
2898        elif style == 'upperalpha':
2899            ordinal = ord(index) - ord('A') + 1
2900        else:
2901            assert False
2902        return ordinal
2903
2904    def check_index(self):
2905        """Check calculated self.ordinal (1,2,...) against the item number
2906        in the document (self.index) and check the number style is the same as
2907        the first item (self.number_style)."""
2908        assert self.type in ('numbered','callout')
2909        if self.index:
2910            style = self.calc_style(self.index)
2911            if style != self.number_style:
2912                message.warning('list item style: expected %s got %s' %
2913                        (self.number_style,style), offset=1)
2914            ordinal = self.calc_index(self.index,style)
2915            if ordinal != self.ordinal:
2916                message.warning('list item index: expected %s got %s' %
2917                        (self.ordinal,ordinal), offset=1)
2918
2919    def check_tags(self):
2920        """ Check that all necessary tags are present. """
2921        tags = set(Lists.TAGS)
2922        if self.type != 'labeled':
2923            tags = tags.difference(['entry','label','term'])
2924        missing = tags.difference(self.tag.keys())
2925        if missing:
2926            self.error('missing tag(s): %s' % ','.join(missing), halt=True)
2927    def translate(self):
2928        AbstractBlock.translate(self)
2929        if self.short_name() in ('bibliography','glossary','qanda'):
2930            message.deprecated('old %s list syntax' % self.short_name())
2931        lists.open.append(self)
2932        attrs = self.mo.groupdict().copy()
2933        for k in ('label','text','index'):
2934            if k in attrs: del attrs[k]
2935        if self.index:
2936            # Set the numbering style from first list item.
2937            attrs['style'] = self.calc_style(self.index)
2938        BlockTitle.consume(attrs)
2939        AttributeList.consume(attrs)
2940        self.merge_attributes(attrs,['tags'])
2941        self.push_blockname()
2942        if self.type in ('numbered','callout'):
2943            self.number_style = self.attributes.get('style')
2944            if self.number_style not in self.NUMBER_STYLES:
2945                message.error('illegal numbered list style: %s' % self.number_style)
2946                # Fall back to default style.
2947                self.attributes['style'] = self.number_style = self.style
2948        self.tag = lists.tags[self.parameters.tags]
2949        self.check_tags()
2950        if 'width' in self.attributes:
2951            # Set horizontal list 'labelwidth' and 'itemwidth' attributes.
2952            v = str(self.attributes['width'])
2953            mo = re.match(r'^(\d{1,2})%?$',v)
2954            if mo:
2955                labelwidth = int(mo.group(1))
2956                self.attributes['labelwidth'] = str(labelwidth)
2957                self.attributes['itemwidth'] = str(100-labelwidth)
2958            else:
2959                self.error('illegal attribute value: width="%s"' % v)
2960        stag,etag = subs_tag(self.tag.list, self.attributes)
2961        if stag:
2962            writer.write(stag,trace='list open')
2963        self.ordinal = 0
2964        # Process list till list syntax changes or there is a new title.
2965        while Lex.next() is self and not BlockTitle.title:
2966            self.ordinal += 1
2967            document.attributes['listindex'] = str(self.ordinal)
2968            if self.type in ('numbered','callout'):
2969                self.check_index()
2970            if self.type in ('bulleted','numbered','callout'):
2971                reader.read()   # Discard (already parsed item first line).
2972                self.translate_item()
2973            elif self.type == 'labeled':
2974                self.translate_entry()
2975            else:
2976                raise AssertionError,'illegal [%s] list type' % self.defname
2977        if etag:
2978            writer.write(etag,trace='list close')
2979        if self.type == 'callout':
2980            calloutmap.validate(self.ordinal)
2981            calloutmap.listclose()
2982        lists.open.pop()
2983        if len(lists.open):
2984            document.attributes['listindex'] = str(lists.open[-1].ordinal)
2985        self.pop_blockname()
2986
2987class Lists(AbstractBlocks):
2988    """List of List objects."""
2989    BLOCK_TYPE = List
2990    PREFIX = 'listdef-'
2991    TYPES = ('bulleted','numbered','labeled','callout')
2992    TAGS = ('list', 'entry','item','text', 'label','term')
2993    def __init__(self):
2994        AbstractBlocks.__init__(self)
2995        self.open = []  # A stack of the current and parent lists.
2996        self.tags={}    # List tags dictionary. Each entry is a tags AttrDict.
2997        self.terminators=None    # List of compiled re's.
2998    def initialize(self):
2999        self.terminators = [
3000                re.compile(r'^\+$|^$'),
3001                re.compile(AttributeList.pattern),
3002                re.compile(lists.delimiters),
3003                re.compile(blocks.delimiters),
3004                re.compile(tables.delimiters),
3005                re.compile(tables_OLD.delimiters),
3006            ]
3007    def load(self,sections):
3008        AbstractBlocks.load(self,sections)
3009        self.load_tags(sections)
3010    def load_tags(self,sections):
3011        """
3012        Load listtags-* conf file sections to self.tags.
3013        """
3014        for section in sections.keys():
3015            mo = re.match(r'^listtags-(?P<name>\w+)$',section)
3016            if mo:
3017                name = mo.group('name')
3018                if name in self.tags:
3019                    d = self.tags[name]
3020                else:
3021                    d = AttrDict()
3022                parse_entries(sections.get(section,()),d)
3023                for k in d.keys():
3024                    if k not in self.TAGS:
3025                        message.warning('[%s] contains illegal list tag: %s' %
3026                                (section,k))
3027                self.tags[name] = d
3028    def validate(self):
3029        AbstractBlocks.validate(self)
3030        for b in self.blocks:
3031            # Check list has valid type.
3032            if not b.type in Lists.TYPES:
3033                raise EAsciiDoc,'[%s] illegal type' % b.defname
3034            b.validate()
3035    def dump(self):
3036        AbstractBlocks.dump(self)
3037        for k,v in self.tags.items():
3038            dump_section('listtags-'+k, v)
3039
3040
3041class DelimitedBlock(AbstractBlock):
3042    def __init__(self):
3043        AbstractBlock.__init__(self)
3044    def load(self,name,entries):
3045        AbstractBlock.load(self,name,entries)
3046    def dump(self):
3047        AbstractBlock.dump(self)
3048        write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
3049        write('')
3050    def isnext(self):
3051        return AbstractBlock.isnext(self)
3052    def translate(self):
3053        AbstractBlock.translate(self)
3054        reader.read()   # Discard delimiter.
3055        self.merge_attributes(AttributeList.attrs)
3056        if not 'skip' in self.parameters.options:
3057            BlockTitle.consume(self.attributes)
3058            AttributeList.consume()
3059        self.push_blockname()
3060        options = self.parameters.options
3061        if 'skip' in options:
3062            reader.read_until(self.delimiter,same_file=True)
3063        elif safe() and self.defname == 'blockdef-backend':
3064            message.unsafe('Backend Block')
3065            reader.read_until(self.delimiter,same_file=True)
3066        else:
3067            template = self.parameters.template
3068            template = subs_attrs(template,self.attributes)
3069            name = self.short_name()+' block'
3070            if 'sectionbody' in options:
3071                # The body is treated like a section body.
3072                stag,etag = config.section2tags(template,self.attributes)
3073                writer.write(stag,trace=name+' open')
3074                Section.translate_body(self)
3075                writer.write(etag,trace=name+' close')
3076            else:
3077                stag = config.section2tags(template,self.attributes,skipend=True)[0]
3078                body = reader.read_until(self.delimiter,same_file=True)
3079                presubs = self.parameters.presubs
3080                postsubs = self.parameters.postsubs
3081                body = Lex.subs(body,presubs)
3082                if self.parameters.filter:
3083                    body = filter_lines(self.parameters.filter,body,self.attributes)
3084                body = Lex.subs(body,postsubs)
3085                # Write start tag, content, end tag.
3086                etag = config.section2tags(template,self.attributes,skipstart=True)[1]
3087                writer.write(dovetail_tags(stag,body,etag),trace=name)
3088            trace(self.short_name()+' block close',etag)
3089        if reader.eof():
3090            self.error('missing closing delimiter',self.start)
3091        else:
3092            delimiter = reader.read()   # Discard delimiter line.
3093            assert re.match(self.delimiter,delimiter)
3094        self.pop_blockname()
3095
3096class DelimitedBlocks(AbstractBlocks):
3097    """List of delimited blocks."""
3098    BLOCK_TYPE = DelimitedBlock
3099    PREFIX = 'blockdef-'
3100    def __init__(self):
3101        AbstractBlocks.__init__(self)
3102    def load(self,sections):
3103        """Update blocks defined in 'sections' dictionary."""
3104        AbstractBlocks.load(self,sections)
3105    def validate(self):
3106        AbstractBlocks.validate(self)
3107
3108class Column:
3109    """Table column."""
3110    def __init__(self, width=None, align_spec=None, style=None):
3111        self.width = width or '1'
3112        self.halign, self.valign = Table.parse_align_spec(align_spec)
3113        self.style = style      # Style name or None.
3114        # Calculated attribute values.
3115        self.abswidth = None    # 1..   (page units).
3116        self.pcwidth = None     # 1..99 (percentage).
3117
3118class Cell:
3119    def __init__(self, data, span_spec=None, align_spec=None, style=None):
3120        self.data = data
3121        self.span, self.vspan = Table.parse_span_spec(span_spec)
3122        self.halign, self.valign = Table.parse_align_spec(align_spec)
3123        self.style = style
3124        self.reserved = False
3125    def __repr__(self):
3126        return '<Cell: %d.%d %s.%s %s "%s">' % (
3127                self.span, self.vspan,
3128                self.halign, self.valign,
3129                self.style or '',
3130                self.data)
3131    def clone_reserve(self):
3132        """Return a clone of self to reserve vertically spanned cell."""
3133        result = copy.copy(self)
3134        result.vspan = 1
3135        result.reserved = True
3136        return result
3137
3138class Table(AbstractBlock):
3139    ALIGN = {'<':'left', '>':'right', '^':'center'}
3140    VALIGN = {'<':'top', '>':'bottom', '^':'middle'}
3141    FORMATS = ('psv','csv','dsv')
3142    SEPARATORS = dict(
3143        csv=',',
3144        dsv=r':|\n',
3145        # The count and align group matches are not exact.
3146        psv=r'((?<!\S)((?P<span>[\d.]+)(?P<op>[*+]))?(?P<align>[<\^>.]{,3})?(?P<style>[a-z])?)?\|'
3147    )
3148    def __init__(self):
3149        AbstractBlock.__init__(self)
3150        self.CONF_ENTRIES += ('format','tags','separator')
3151        # tabledef conf file parameters.
3152        self.format='psv'
3153        self.separator=None
3154        self.tags=None          # Name of tabletags-<tags> conf section.
3155        # Calculated parameters.
3156        self.abswidth=None      # 1..   (page units).
3157        self.pcwidth = None     # 1..99 (percentage).
3158        self.rows=[]            # Parsed rows, each row is a list of Cells.
3159        self.columns=[]         # List of Columns.
3160    @staticmethod
3161    def parse_align_spec(align_spec):
3162        """
3163        Parse AsciiDoc cell alignment specifier and return 2-tuple with
3164        horizonatal and vertical alignment names. Unspecified alignments
3165        set to None.
3166        """
3167        result = (None, None)
3168        if align_spec:
3169            mo = re.match(r'^([<\^>])?(\.([<\^>]))?$', align_spec)
3170            if mo:
3171                result = (Table.ALIGN.get(mo.group(1)),
3172                          Table.VALIGN.get(mo.group(3)))
3173        return result
3174    @staticmethod
3175    def parse_span_spec(span_spec):
3176        """
3177        Parse AsciiDoc cell span specifier and return 2-tuple with horizonatal
3178        and vertical span counts. Set default values (1,1) if not
3179        specified.
3180        """
3181        result = (None, None)
3182        if span_spec:
3183            mo = re.match(r'^(\d+)?(\.(\d+))?$', span_spec)
3184            if mo:
3185                result = (mo.group(1) and int(mo.group(1)),
3186                          mo.group(3) and int(mo.group(3)))
3187        return (result[0] or 1, result[1] or 1)
3188    def load(self,name,entries):
3189        AbstractBlock.load(self,name,entries)
3190    def dump(self):
3191        AbstractBlock.dump(self)
3192        write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
3193        write('format='+self.format)
3194        write('')
3195    def validate(self):
3196        AbstractBlock.validate(self)
3197        if self.format not in Table.FORMATS:
3198            self.error('illegal format=%s' % self.format,halt=True)
3199        self.tags = self.tags or 'default'
3200        tags = [self.tags]
3201        tags += [s['tags'] for s in self.styles.values() if 'tags' in s]
3202        for t in tags:
3203            if t not in tables.tags:
3204                self.error('missing section: [tabletags-%s]' % t,halt=True)
3205        if self.separator:
3206            # Evaluate escape characters.
3207            self.separator = literal_eval('"'+self.separator+'"')
3208        #TODO: Move to class Tables
3209        # Check global table parameters.
3210        elif config.pagewidth is None:
3211            self.error('missing [miscellaneous] entry: pagewidth')
3212        elif config.pageunits is None:
3213            self.error('missing [miscellaneous] entry: pageunits')
3214    def validate_attributes(self):
3215        """Validate and parse table attributes."""
3216        # Set defaults.
3217        format = self.format
3218        tags = self.tags
3219        separator = self.separator
3220        abswidth = float(config.pagewidth)
3221        pcwidth = 100.0
3222        for k,v in self.attributes.items():
3223            if k == 'format':
3224                if v not in self.FORMATS:
3225                    self.error('illegal %s=%s' % (k,v))
3226                else:
3227                    format = v
3228            elif k == 'tags':
3229                if v not in tables.tags:
3230                    self.error('illegal %s=%s' % (k,v))
3231                else:
3232                    tags = v
3233            elif k == 'separator':
3234                separator = v
3235            elif k == 'width':
3236                if not re.match(r'^\d{1,3}%$',v) or int(v[:-1]) > 100:
3237                    self.error('illegal %s=%s' % (k,v))
3238                else:
3239                    abswidth = float(v[:-1])/100 * config.pagewidth
3240                    pcwidth = float(v[:-1])
3241        # Calculate separator if it has not been specified.
3242        if not separator:
3243            separator = Table.SEPARATORS[format]
3244        if format == 'csv':
3245            if len(separator) > 1:
3246                self.error('illegal csv separator=%s' % separator)
3247                separator = ','
3248        else:
3249            if not is_re(separator):
3250                self.error('illegal regular expression: separator=%s' %
3251                        separator)
3252        self.parameters.format = format
3253        self.parameters.tags = tags
3254        self.parameters.separator = separator
3255        self.abswidth = abswidth
3256        self.pcwidth = pcwidth
3257    def get_tags(self,params):
3258        tags = self.get_param('tags',params)
3259        assert(tags and tags in tables.tags)
3260        return tables.tags[tags]
3261    def get_style(self,prefix):
3262        """
3263        Return the style dictionary whose name starts with 'prefix'.
3264        """
3265        if prefix is None:
3266            return None
3267        names = self.styles.keys()
3268        names.sort()
3269        for name in names:
3270            if name.startswith(prefix):
3271                return self.styles[name]
3272        else:
3273            self.error('missing style: %s*' % prefix)
3274            return None
3275    def parse_cols(self, cols, halign, valign):
3276        """
3277        Build list of column objects from table 'cols', 'halign' and 'valign'
3278        attributes.
3279        """
3280        # [<multiplier>*][<align>][<width>][<style>]
3281        COLS_RE1 = r'^((?P<count>\d+)\*)?(?P<align>[<\^>.]{,3})?(?P<width>\d+%?)?(?P<style>[a-z]\w*)?$'
3282        # [<multiplier>*][<width>][<align>][<style>]
3283        COLS_RE2 = r'^((?P<count>\d+)\*)?(?P<width>\d+%?)?(?P<align>[<\^>.]{,3})?(?P<style>[a-z]\w*)?$'
3284        reo1 = re.compile(COLS_RE1)
3285        reo2 = re.compile(COLS_RE2)
3286        cols = str(cols)
3287        if re.match(r'^\d+$',cols):
3288            for i in range(int(cols)):
3289                self.columns.append(Column())
3290        else:
3291            for col in re.split(r'\s*,\s*',cols):
3292                mo = reo1.match(col)
3293                if not mo:
3294                    mo = reo2.match(col)
3295                if mo:
3296                    count = int(mo.groupdict().get('count') or 1)
3297                    for i in range(count):
3298                        self.columns.append(
3299                            Column(mo.group('width'), mo.group('align'),
3300                                   self.get_style(mo.group('style')))
3301                        )
3302                else:
3303                    self.error('illegal column spec: %s' % col,self.start)
3304        # Set column (and indirectly cell) default alignments.
3305        for col in self.columns:
3306            col.halign = col.halign or halign or document.attributes.get('halign') or 'left'
3307            col.valign = col.valign or valign or document.attributes.get('valign') or 'top'
3308        # Validate widths and calculate missing widths.
3309        n = 0; percents = 0; props = 0
3310        for col in self.columns:
3311            if col.width:
3312                if col.width[-1] == '%': percents += int(col.width[:-1])
3313                else: props += int(col.width)
3314                n += 1
3315        if percents > 0 and props > 0:
3316            self.error('mixed percent and proportional widths: %s'
3317                    % cols,self.start)
3318        pcunits = percents > 0
3319        # Fill in missing widths.
3320        if n < len(self.columns) and percents < 100:
3321            if pcunits:
3322                width = float(100 - percents)/float(len(self.columns) - n)
3323            else:
3324                width = 1
3325            for col in self.columns:
3326                if not col.width:
3327                    if pcunits:
3328                        col.width = str(int(width))+'%'
3329                        percents += width
3330                    else:
3331                        col.width = str(width)
3332                        props += width
3333        # Calculate column alignment and absolute and percent width values.
3334        percents = 0
3335        for col in self.columns:
3336            if pcunits:
3337                col.pcwidth = float(col.width[:-1])
3338            else:
3339                col.pcwidth = (float(col.width)/props)*100
3340            col.abswidth = self.abswidth * (col.pcwidth/100)
3341            if config.pageunits in ('cm','mm','in','em'):
3342                col.abswidth = '%.2f' % round(col.abswidth,2)
3343            else:
3344                col.abswidth = '%d' % round(col.abswidth)
3345            percents += col.pcwidth
3346            col.pcwidth = int(col.pcwidth)
3347        if round(percents) > 100:
3348            self.error('total width exceeds 100%%: %s' % cols,self.start)
3349        elif round(percents) < 100:
3350            self.error('total width less than 100%%: %s' % cols,self.start)
3351    def build_colspecs(self):
3352        """
3353        Generate column related substitution attributes.
3354        """
3355        cols = []
3356        i = 1
3357        for col in self.columns:
3358            colspec = self.get_tags(col.style).colspec
3359            if colspec:
3360                self.attributes['halign'] = col.halign
3361                self.attributes['valign'] = col.valign
3362                self.attributes['colabswidth'] = col.abswidth
3363                self.attributes['colpcwidth'] = col.pcwidth
3364                self.attributes['colnumber'] = str(i)
3365                s = subs_attrs(colspec, self.attributes)
3366                if not s:
3367                    message.warning('colspec dropped: contains undefined attribute')
3368                else:
3369                    cols.append(s)
3370            i += 1
3371        if cols:
3372            self.attributes['colspecs'] = writer.newline.join(cols)
3373    def parse_rows(self, text):
3374        """
3375        Parse the table source text into self.rows (a list of rows, each row
3376        is a list of Cells.
3377        """
3378        reserved = {}  # Reserved cells generated by rowspans.
3379        if self.parameters.format in ('psv','dsv'):
3380            colcount = len(self.columns)
3381            parsed_cells = self.parse_psv_dsv(text)
3382            ri = 0  # Current row index 0..
3383            ci = 0  # Column counter 0..colcount
3384            row = []
3385            i = 0
3386            while True:
3387                resv = reserved.get(ri) and reserved[ri].get(ci)
3388                if resv:
3389                    # We have a cell generated by a previous row span so
3390                    # process it before continuing with the current parsed
3391                    # cell.
3392                    cell = resv
3393                else:
3394                    if i >= len(parsed_cells):
3395                        break   # No more parsed or reserved cells.
3396                    cell = parsed_cells[i]
3397                    i += 1
3398                    if cell.vspan > 1:
3399                        # Generate ensuing reserved cells spanned vertically by
3400                        # the current cell.
3401                        for j in range(1, cell.vspan):
3402                            if not ri+j in reserved:
3403                                reserved[ri+j] = {}
3404                            reserved[ri+j][ci] = cell.clone_reserve()
3405                ci += cell.span
3406                if ci <= colcount:
3407                    row.append(cell)
3408                if ci >= colcount:
3409                    self.rows.append(row)
3410                    ri += 1
3411                    row = []
3412                    ci = 0
3413        elif self.parameters.format == 'csv':
3414            self.rows = self.parse_csv(text)
3415        else:
3416            assert True,'illegal table format'
3417        # Check for empty rows containing only reserved (spanned) cells.
3418        for ri,row in enumerate(self.rows):
3419            empty = True
3420            for cell in row:
3421                if not cell.reserved:
3422                    empty = False
3423                    break
3424            if empty:
3425                message.warning('table row %d: empty spanned row' % (ri+1))
3426        # Check that all row spans match.
3427        for ri,row in enumerate(self.rows):
3428            row_span = 0
3429            for cell in row:
3430                row_span += cell.span
3431            if ri == 0:
3432                header_span = row_span
3433            if row_span < header_span:
3434                message.warning('table row %d: does not span all columns' % (ri+1))
3435            if row_span > header_span:
3436                message.warning('table row %d: exceeds columns span' % (ri+1))
3437    def subs_rows(self, rows, rowtype='body'):
3438        """
3439        Return a string of output markup from a list of rows, each row
3440        is a list of raw data text.
3441        """
3442        tags = tables.tags[self.parameters.tags]
3443        if rowtype == 'header':
3444            rtag = tags.headrow
3445        elif rowtype == 'footer':
3446            rtag = tags.footrow
3447        else:
3448            rtag = tags.bodyrow
3449        result = []
3450        stag,etag = subs_tag(rtag,self.attributes)
3451        for row in rows:
3452            result.append(stag)
3453            result += self.subs_row(row,rowtype)
3454            result.append(etag)
3455        return writer.newline.join(result)
3456    def subs_row(self, row, rowtype):
3457        """
3458        Substitute the list of Cells using the data tag.
3459        Returns a list of marked up table cell elements.
3460        """
3461        result = []
3462        i = 0
3463        for cell in row:
3464            if cell.reserved:
3465                # Skip vertically spanned placeholders.
3466                i += cell.span
3467                continue
3468            if i >= len(self.columns):
3469                break   # Skip cells outside the header width.
3470            col = self.columns[i]
3471            self.attributes['halign'] = cell.halign or col.halign
3472            self.attributes['valign'] = cell.valign or  col.valign
3473            self.attributes['colabswidth'] = col.abswidth
3474            self.attributes['colpcwidth'] = col.pcwidth
3475            self.attributes['colnumber'] = str(i+1)
3476            self.attributes['colspan'] = str(cell.span)
3477            self.attributes['colstart'] = self.attributes['colnumber']
3478            self.attributes['colend'] = str(i+cell.span)
3479            self.attributes['rowspan'] = str(cell.vspan)
3480            self.attributes['morerows'] = str(cell.vspan-1)
3481            # Fill missing column data with blanks.
3482            if i > len(self.columns) - 1:
3483                data = ''
3484            else:
3485                data = cell.data
3486            if rowtype == 'header':
3487                # Use table style unless overriden by cell style.
3488                colstyle = cell.style
3489            else:
3490                # If the cell style is not defined use the column style.
3491                colstyle = cell.style or col.style
3492            tags = self.get_tags(colstyle)
3493            presubs,postsubs = self.get_subs(colstyle)
3494            data = [data]
3495            data = Lex.subs(data, presubs)
3496            data = filter_lines(self.get_param('filter',colstyle),
3497                                data, self.attributes)
3498            data = Lex.subs(data, postsubs)
3499            if rowtype != 'header':
3500                ptag = tags.paragraph
3501                if ptag:
3502                    stag,etag = subs_tag(ptag,self.attributes)
3503                    text = '\n'.join(data).strip()
3504                    data = []
3505                    for para in re.split(r'\n{2,}',text):
3506                        data += dovetail_tags([stag],para.split('\n'),[etag])
3507            if rowtype == 'header':
3508                dtag = tags.headdata
3509            elif rowtype == 'footer':
3510                dtag = tags.footdata
3511            else:
3512                dtag = tags.bodydata
3513            stag,etag = subs_tag(dtag,self.attributes)
3514            result = result + dovetail_tags([stag],data,[etag])
3515            i += cell.span
3516        return result
3517    def parse_csv(self,text):
3518        """
3519        Parse the table source text and return a list of rows, each row
3520        is a list of Cells.
3521        """
3522        import StringIO
3523        import csv
3524        rows = []
3525        rdr = csv.reader(StringIO.StringIO('\r\n'.join(text)),
3526                     delimiter=self.parameters.separator, skipinitialspace=True)
3527        try:
3528            for row in rdr:
3529                rows.append([Cell(data) for data in row])
3530        except Exception:
3531            self.error('csv parse error: %s' % row)
3532        return rows
3533    def parse_psv_dsv(self,text):
3534        """
3535        Parse list of PSV or DSV table source text lines and return a list of
3536        Cells.
3537        """
3538        def append_cell(data, span_spec, op, align_spec, style):
3539            op = op or '+'
3540            if op == '*':   # Cell multiplier.
3541                span = Table.parse_span_spec(span_spec)[0]
3542                for i in range(span):
3543                    cells.append(Cell(data, '1', align_spec, style))
3544            elif op == '+': # Column spanner.
3545                cells.append(Cell(data, span_spec, align_spec, style))
3546            else:
3547                self.error('illegal table cell operator')
3548        text = '\n'.join(text)
3549        separator = '(?msu)'+self.parameters.separator
3550        format = self.parameters.format
3551        start = 0
3552        span = None
3553        op = None
3554        align = None
3555        style = None
3556        cells = []
3557        data = ''
3558        for mo in re.finditer(separator,text):
3559            data += text[start:mo.start()]
3560            if data.endswith('\\'):
3561                data = data[:-1]+mo.group() # Reinstate escaped separators.
3562            else:
3563                append_cell(data, span, op, align, style)
3564                span = mo.groupdict().get('span')
3565                op = mo.groupdict().get('op')
3566                align = mo.groupdict().get('align')
3567                style = mo.groupdict().get('style')
3568                if style:
3569                    style = self.get_style(style)
3570                data = ''
3571            start = mo.end()
3572        # Last cell follows final separator.
3573        data += text[start:]
3574        append_cell(data, span, op, align, style)
3575        # We expect a dummy blank item preceeding first PSV cell.
3576        if format == 'psv':
3577            if cells[0].data.strip() != '':
3578                self.error('missing leading separator: %s' % separator,
3579                        self.start)
3580            else:
3581                cells.pop(0)
3582        return cells
3583    def translate(self):
3584        AbstractBlock.translate(self)
3585        reader.read()   # Discard delimiter.
3586        # Reset instance specific properties.
3587        self.columns = []
3588        self.rows = []
3589        attrs = {}
3590        BlockTitle.consume(attrs)
3591        # Mix in document attribute list.
3592        AttributeList.consume(attrs)
3593        self.merge_attributes(attrs)
3594        self.validate_attributes()
3595        # Add global and calculated configuration parameters.
3596        self.attributes['pagewidth'] = config.pagewidth
3597        self.attributes['pageunits'] = config.pageunits
3598        self.attributes['tableabswidth'] = int(self.abswidth)
3599        self.attributes['tablepcwidth'] = int(self.pcwidth)
3600        # Read the entire table.
3601        text = reader.read_until(self.delimiter)
3602        if reader.eof():
3603            self.error('missing closing delimiter',self.start)
3604        else:
3605            delimiter = reader.read()   # Discard closing delimiter.
3606            assert re.match(self.delimiter,delimiter)
3607        if len(text) == 0:
3608            message.warning('[%s] table is empty' % self.defname)
3609            return
3610        self.push_blockname('table')
3611        cols = attrs.get('cols')
3612        if not cols:
3613            # Calculate column count from number of items in first line.
3614            if self.parameters.format == 'csv':
3615                cols = text[0].count(self.parameters.separator) + 1
3616            else:
3617                cols = 0
3618                for cell in self.parse_psv_dsv(text[:1]):
3619                    cols += cell.span
3620        self.parse_cols(cols, attrs.get('halign'), attrs.get('valign'))
3621        # Set calculated attributes.
3622        self.attributes['colcount'] = len(self.columns)
3623        self.build_colspecs()
3624        self.parse_rows(text)
3625        # The 'rowcount' attribute is used by the experimental LaTeX backend.
3626        self.attributes['rowcount'] = str(len(self.rows))
3627        # Generate headrows, footrows, bodyrows.
3628        # Headrow, footrow and bodyrow data replaces same named attributes in
3629        # the table markup template. In order to ensure this data does not get
3630        # a second attribute substitution (which would interfere with any
3631        # already substituted inline passthroughs) unique placeholders are used
3632        # (the tab character does not appear elsewhere since it is expanded on
3633        # input) which are replaced after template attribute substitution.
3634        headrows = footrows = bodyrows = None
3635        if self.rows and 'header' in self.parameters.options:
3636            headrows = self.subs_rows(self.rows[0:1],'header')
3637            self.attributes['headrows'] = '\x07headrows\x07'
3638            self.rows = self.rows[1:]
3639        if self.rows and 'footer' in self.parameters.options:
3640            footrows = self.subs_rows( self.rows[-1:], 'footer')
3641            self.attributes['footrows'] = '\x07footrows\x07'
3642            self.rows = self.rows[:-1]
3643        if self.rows:
3644            bodyrows = self.subs_rows(self.rows)
3645            self.attributes['bodyrows'] = '\x07bodyrows\x07'
3646        table = subs_attrs(config.sections[self.parameters.template],
3647                           self.attributes)
3648        table = writer.newline.join(table)
3649        # Before we finish replace the table head, foot and body place holders
3650        # with the real data.
3651        if headrows:
3652            table = table.replace('\x07headrows\x07', headrows, 1)
3653        if footrows:
3654            table = table.replace('\x07footrows\x07', footrows, 1)
3655        if bodyrows:
3656            table = table.replace('\x07bodyrows\x07', bodyrows, 1)
3657        writer.write(table,trace='table')
3658        self.pop_blockname()
3659
3660class Tables(AbstractBlocks):
3661    """List of tables."""
3662    BLOCK_TYPE = Table
3663    PREFIX = 'tabledef-'
3664    TAGS = ('colspec', 'headrow','footrow','bodyrow',
3665            'headdata','footdata', 'bodydata','paragraph')
3666    def __init__(self):
3667        AbstractBlocks.__init__(self)
3668        # Table tags dictionary. Each entry is a tags dictionary.
3669        self.tags={}
3670    def load(self,sections):
3671        AbstractBlocks.load(self,sections)
3672        self.load_tags(sections)
3673    def load_tags(self,sections):
3674        """
3675        Load tabletags-* conf file sections to self.tags.
3676        """
3677        for section in sections.keys():
3678            mo = re.match(r'^tabletags-(?P<name>\w+)$',section)
3679            if mo:
3680                name = mo.group('name')
3681                if name in self.tags:
3682                    d = self.tags[name]
3683                else:
3684                    d = AttrDict()
3685                parse_entries(sections.get(section,()),d)
3686                for k in d.keys():
3687                    if k not in self.TAGS:
3688                        message.warning('[%s] contains illegal table tag: %s' %
3689                                (section,k))
3690                self.tags[name] = d
3691    def validate(self):
3692        AbstractBlocks.validate(self)
3693        # Check we have a default table definition,
3694        for i in range(len(self.blocks)):
3695            if self.blocks[i].defname == 'tabledef-default':
3696                default = self.blocks[i]
3697                break
3698        else:
3699            raise EAsciiDoc,'missing section: [tabledef-default]'
3700        # Propagate defaults to unspecified table parameters.
3701        for b in self.blocks:
3702            if b is not default:
3703                if b.format is None: b.format = default.format
3704                if b.template is None: b.template = default.template
3705        # Check tags and propagate default tags.
3706        if not 'default' in self.tags:
3707            raise EAsciiDoc,'missing section: [tabletags-default]'
3708        default = self.tags['default']
3709        for tag in ('bodyrow','bodydata','paragraph'): # Mandatory default tags.
3710            if tag not in default:
3711                raise EAsciiDoc,'missing [tabletags-default] entry: %s' % tag
3712        for t in self.tags.values():
3713            if t is not default:
3714                if t.colspec is None: t.colspec = default.colspec
3715                if t.headrow is None: t.headrow = default.headrow
3716                if t.footrow is None: t.footrow = default.footrow
3717                if t.bodyrow is None: t.bodyrow = default.bodyrow
3718                if t.headdata is None: t.headdata = default.headdata
3719                if t.footdata is None: t.footdata = default.footdata
3720                if t.bodydata is None: t.bodydata = default.bodydata
3721                if t.paragraph is None: t.paragraph = default.paragraph
3722        # Use body tags if header and footer tags are not specified.
3723        for t in self.tags.values():
3724            if not t.headrow: t.headrow = t.bodyrow
3725            if not t.footrow: t.footrow = t.bodyrow
3726            if not t.headdata: t.headdata = t.bodydata
3727            if not t.footdata: t.footdata = t.bodydata
3728        # Check table definitions are valid.
3729        for b in self.blocks:
3730            b.validate()
3731    def dump(self):
3732        AbstractBlocks.dump(self)
3733        for k,v in self.tags.items():
3734            dump_section('tabletags-'+k, v)
3735
3736class Macros:
3737    # Default system macro syntax.
3738    SYS_RE = r'(?u)^(?P<name>[\\]?\w(\w|-)*?)::(?P<target>\S*?)' + \
3739             r'(\[(?P<attrlist>.*?)\])$'
3740    def __init__(self):
3741        self.macros = []        # List of Macros.
3742        self.current = None     # The last matched block macro.
3743        self.passthroughs = []
3744        # Initialize default system macro.
3745        m = Macro()
3746        m.pattern = self.SYS_RE
3747        m.prefix = '+'
3748        m.reo = re.compile(m.pattern)
3749        self.macros.append(m)
3750    def load(self,entries):
3751        for entry in entries:
3752            m = Macro()
3753            m.load(entry)
3754            if m.name is None:
3755                # Delete undefined macro.
3756                for i,m2 in enumerate(self.macros):
3757                    if m2.pattern == m.pattern:
3758                        del self.macros[i]
3759                        break
3760                else:
3761                    message.warning('unable to delete missing macro: %s' % m.pattern)
3762            else:
3763                # Check for duplicates.
3764                for m2 in self.macros:
3765                    if m2.pattern == m.pattern:
3766                        message.verbose('macro redefinition: %s%s' % (m.prefix,m.name))
3767                        break
3768                else:
3769                    self.macros.append(m)
3770    def dump(self):
3771        write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
3772        write('[macros]')
3773        # Dump all macros except the first (built-in system) macro.
3774        for m in self.macros[1:]:
3775            # Escape = in pattern.
3776            macro = '%s=%s%s' % (m.pattern.replace('=',r'\='), m.prefix, m.name)
3777            if m.subslist is not None:
3778                macro += '[' + ','.join(m.subslist) + ']'
3779            write(macro)
3780        write('')
3781    def validate(self):
3782        # Check all named sections exist.
3783        if config.verbose:
3784            for m in self.macros:
3785                if m.name and m.prefix != '+':
3786                    m.section_name()
3787    def subs(self,text,prefix='',callouts=False):
3788        # If callouts is True then only callout macros are processed, if False
3789        # then all non-callout macros are processed.
3790        result = text
3791        for m in self.macros:
3792            if m.prefix == prefix:
3793                if callouts ^ (m.name != 'callout'):
3794                    result = m.subs(result)
3795        return result
3796    def isnext(self):
3797        """Return matching macro if block macro is next on reader."""
3798        reader.skip_blank_lines()
3799        line = reader.read_next()
3800        if line:
3801            for m in self.macros:
3802                if m.prefix == '#':
3803                    if m.reo.match(line):
3804                        self.current = m
3805                        return m
3806        return False
3807    def match(self,prefix,name,text):
3808        """Return re match object matching 'text' with macro type 'prefix',
3809        macro name 'name'."""
3810        for m in self.macros:
3811            if m.prefix == prefix:
3812                mo = m.reo.match(text)
3813                if mo:
3814                    if m.name == name:
3815                        return mo
3816                    if re.match(name, mo.group('name')):
3817                        return mo
3818        return None
3819    def extract_passthroughs(self,text,prefix=''):
3820        """ Extract the passthrough text and replace with temporary
3821        placeholders."""
3822        self.passthroughs = []
3823        for m in self.macros:
3824            if m.has_passthrough() and m.prefix == prefix:
3825                text = m.subs_passthroughs(text, self.passthroughs)
3826        return text
3827    def restore_passthroughs(self,text):
3828        """ Replace passthough placeholders with the original passthrough
3829        text."""
3830        for i,v in enumerate(self.passthroughs):
3831            text = text.replace('\x07'+str(i)+'\x07', self.passthroughs[i])
3832        return text
3833
3834class Macro:
3835    def __init__(self):
3836        self.pattern = None     # Matching regular expression.
3837        self.name = ''          # Conf file macro name (None if implicit).
3838        self.prefix = ''        # '' if inline, '+' if system, '#' if block.
3839        self.reo = None         # Compiled pattern re object.
3840        self.subslist = []      # Default subs for macros passtext group.
3841    def has_passthrough(self):
3842        return self.pattern.find(r'(?P<passtext>') >= 0
3843    def section_name(self,name=None):
3844        """Return macro markup template section name based on macro name and
3845        prefix.  Return None section not found."""
3846        assert self.prefix != '+'
3847        if not name:
3848            assert self.name
3849            name = self.name
3850        if self.prefix == '#':
3851            suffix = '-blockmacro'
3852        else:
3853            suffix = '-inlinemacro'
3854        if name+suffix in config.sections:
3855            return name+suffix
3856        else:
3857            message.warning('missing macro section: [%s]' % (name+suffix))
3858            return None
3859    def load(self,entry):
3860        e = parse_entry(entry)
3861        if e is None:
3862            # Only the macro pattern was specified, mark for deletion.
3863            self.name = None
3864            self.pattern = entry
3865            return
3866        if not is_re(e[0]):
3867            raise EAsciiDoc,'illegal macro regular expression: %s' % e[0]
3868        pattern, name = e
3869        if name and name[0] in ('+','#'):
3870            prefix, name = name[0], name[1:]
3871        else:
3872            prefix = ''
3873        # Parse passthrough subslist.
3874        mo = re.match(r'^(?P<name>[^[]*)(\[(?P<subslist>.*)\])?$', name)
3875        name = mo.group('name')
3876        if name and not is_name(name):
3877            raise EAsciiDoc,'illegal section name in macro entry: %s' % entry
3878        subslist = mo.group('subslist')
3879        if subslist is not None:
3880            # Parse and validate passthrough subs.
3881            subslist = parse_options(subslist, SUBS_OPTIONS,
3882                                 'illegal subs in macro entry: %s' % entry)
3883        self.pattern = pattern
3884        self.reo = re.compile(pattern)
3885        self.prefix = prefix
3886        self.name = name
3887        self.subslist = subslist or []
3888
3889    def subs(self,text):
3890        def subs_func(mo):
3891            """Function called to perform macro substitution.
3892            Uses matched macro regular expression object and returns string
3893            containing the substituted macro body."""
3894            # Check if macro reference is escaped.
3895            if mo.group()[0] == '\\':
3896                return mo.group()[1:]   # Strip leading backslash.
3897            d = mo.groupdict()
3898            # Delete groups that didn't participate in match.
3899            for k,v in d.items():
3900                if v is None: del d[k]
3901            if self.name:
3902                name = self.name
3903            else:
3904                if not 'name' in d:
3905                    message.warning('missing macro name group: %s' % mo.re.pattern)
3906                    return ''
3907                name = d['name']
3908            section_name = self.section_name(name)
3909            if not section_name:
3910                return ''
3911            # If we're dealing with a block macro get optional block ID and
3912            # block title.
3913            if self.prefix == '#' and self.name != 'comment':
3914                AttributeList.consume(d)
3915                BlockTitle.consume(d)
3916            # Parse macro attributes.
3917            if 'attrlist' in d:
3918                if d['attrlist'] in (None,''):
3919                    del d['attrlist']
3920                else:
3921                    if self.prefix == '':
3922                        # Unescape ] characters in inline macros.
3923                        d['attrlist'] = d['attrlist'].replace('\\]',']')
3924                    parse_attributes(d['attrlist'],d)
3925                    # Generate option attributes.
3926                    if 'options' in d:
3927                        options = parse_options(d['options'], (),
3928                                '%s: illegal option name' % name)
3929                        for option in options:
3930                            d[option+'-option'] = ''
3931                    # Substitute single quoted attribute values in block macros.
3932                    if self.prefix == '#':
3933                        AttributeList.subs(d)
3934            if name == 'callout':
3935                listindex =int(d['index'])
3936                d['coid'] = calloutmap.add(listindex)
3937            # The alt attribute is the first image macro positional attribute.
3938            if name == 'image' and '1' in d:
3939                d['alt'] = d['1']
3940            # Unescape special characters in LaTeX target file names.
3941            if document.backend == 'latex' and 'target' in d and d['target']:
3942                if not '0' in d:
3943                    d['0'] = d['target']
3944                d['target']= config.subs_specialchars_reverse(d['target'])
3945            # BUG: We've already done attribute substitution on the macro which
3946            # means that any escaped attribute references are now unescaped and
3947            # will be substituted by config.subs_section() below. As a partial
3948            # fix have withheld {0} from substitution but this kludge doesn't
3949            # fix it for other attributes containing unescaped references.
3950            # Passthrough macros don't have this problem.
3951            a0 = d.get('0')
3952            if a0:
3953                d['0'] = chr(0)  # Replace temporarily with unused character.
3954            body = config.subs_section(section_name,d)
3955            if len(body) == 0:
3956                result = ''
3957            elif len(body) == 1:
3958                result = body[0]
3959            else:
3960                if self.prefix == '#':
3961                    result = writer.newline.join(body)
3962                else:
3963                    # Internally processed inline macros use UNIX line
3964                    # separator.
3965                    result = '\n'.join(body)
3966            if a0:
3967                result = result.replace(chr(0), a0)
3968            return result
3969
3970        return self.reo.sub(subs_func, text)
3971
3972    def translate(self):
3973        """ Block macro translation."""
3974        assert self.prefix == '#'
3975        s = reader.read()
3976        before = s
3977        if self.has_passthrough():
3978            s = macros.extract_passthroughs(s,'#')
3979        s = subs_attrs(s)
3980        if s:
3981            s = self.subs(s)
3982            if self.has_passthrough():
3983                s = macros.restore_passthroughs(s)
3984            if s:
3985                trace('macro block',before,s)
3986                writer.write(s)
3987
3988    def subs_passthroughs(self, text, passthroughs):
3989        """ Replace macro attribute lists in text with placeholders.
3990        Substitute and append the passthrough attribute lists to the
3991        passthroughs list."""
3992        def subs_func(mo):
3993            """Function called to perform inline macro substitution.
3994            Uses matched macro regular expression object and returns string
3995            containing the substituted macro body."""
3996            # Don't process escaped macro references.
3997            if mo.group()[0] == '\\':
3998                return mo.group()
3999            d = mo.groupdict()
4000            if not 'passtext' in d:
4001                message.warning('passthrough macro %s: missing passtext group' %
4002                        d.get('name',''))
4003                return mo.group()
4004            passtext = d['passtext']
4005            if re.search('\x07\\d+\x07', passtext):
4006                message.warning('nested inline passthrough')
4007                return mo.group()
4008            if d.get('subslist'):
4009                if d['subslist'].startswith(':'):
4010                    message.error('block macro cannot occur here: %s' % mo.group(),
4011                          halt=True)
4012                subslist = parse_options(d['subslist'], SUBS_OPTIONS,
4013                          'illegal passthrough macro subs option')
4014            else:
4015                subslist = self.subslist
4016            passtext = Lex.subs_1(passtext,subslist)
4017            if passtext is None: passtext = ''
4018            if self.prefix == '':
4019                # Unescape ] characters in inline macros.
4020                passtext = passtext.replace('\\]',']')
4021            passthroughs.append(passtext)
4022            # Tabs guarantee the placeholders are unambiguous.
4023            result = (
4024                text[mo.start():mo.start('passtext')] +
4025                '\x07' + str(len(passthroughs)-1) + '\x07' +
4026                text[mo.end('passtext'):mo.end()]
4027            )
4028            return result
4029
4030        return self.reo.sub(subs_func, text)
4031
4032
4033class CalloutMap:
4034    def __init__(self):
4035        self.comap = {}         # key = list index, value = callouts list.
4036        self.calloutindex = 0   # Current callout index number.
4037        self.listnumber = 1     # Current callout list number.
4038    def listclose(self):
4039        # Called when callout list is closed.
4040        self.listnumber += 1
4041        self.calloutindex = 0
4042        self.comap = {}
4043    def add(self,listindex):
4044        # Add next callout index to listindex map entry. Return the callout id.
4045        self.calloutindex += 1
4046        # Append the coindex to a list in the comap dictionary.
4047        if not listindex in self.comap:
4048            self.comap[listindex] = [self.calloutindex]
4049        else:
4050            self.comap[listindex].append(self.calloutindex)
4051        return self.calloutid(self.listnumber, self.calloutindex)
4052    @staticmethod
4053    def calloutid(listnumber,calloutindex):
4054        return 'CO%d-%d' % (listnumber,calloutindex)
4055    def calloutids(self,listindex):
4056        # Retieve list of callout indexes that refer to listindex.
4057        if listindex in self.comap:
4058            result = ''
4059            for coindex in self.comap[listindex]:
4060                result += ' ' + self.calloutid(self.listnumber,coindex)
4061            return result.strip()
4062        else:
4063            message.warning('no callouts refer to list item '+str(listindex))
4064            return ''
4065    def validate(self,maxlistindex):
4066        # Check that all list indexes referenced by callouts exist.
4067        for listindex in self.comap.keys():
4068            if listindex > maxlistindex:
4069                message.warning('callout refers to non-existent list item '
4070                        + str(listindex))
4071
4072#---------------------------------------------------------------------------
4073# Input stream Reader and output stream writer classes.
4074#---------------------------------------------------------------------------
4075
4076UTF8_BOM = '\xef\xbb\xbf'
4077
4078class Reader1:
4079    """Line oriented AsciiDoc input file reader. Processes include and
4080    conditional inclusion system macros. Tabs are expanded and lines are right
4081    trimmed."""
4082    # This class is not used directly, use Reader class instead.
4083    READ_BUFFER_MIN = 10        # Read buffer low level.
4084    def __init__(self):
4085        self.f = None           # Input file object.
4086        self.fname = None       # Input file name.
4087        self.next = []          # Read ahead buffer containing
4088                                # [filename,linenumber,linetext] lists.
4089        self.cursor = None      # Last read() [filename,linenumber,linetext].
4090        self.tabsize = 8        # Tab expansion number of spaces.
4091        self.parent = None      # Included reader's parent reader.
4092        self._lineno = 0        # The last line read from file object f.
4093        self.current_depth = 0  # Current include depth.
4094        self.max_depth = 10     # Initial maxiumum allowed include depth.
4095        self.bom = None         # Byte order mark (BOM).
4096        self.infile = None      # Saved document 'infile' attribute.
4097        self.indir = None       # Saved document 'indir' attribute.
4098    def open(self,fname):
4099        self.fname = fname
4100        message.verbose('reading: '+fname)
4101        if fname == '<stdin>':
4102            self.f = sys.stdin
4103            self.infile = None
4104            self.indir = None
4105        else:
4106            self.f = open(fname,'rb')
4107            self.infile = fname
4108            self.indir = os.path.dirname(fname)
4109        document.attributes['infile'] = self.infile
4110        document.attributes['indir'] = self.indir
4111        self._lineno = 0            # The last line read from file object f.
4112        self.next = []
4113        # Prefill buffer by reading the first line and then pushing it back.
4114        if Reader1.read(self):
4115            if self.cursor[2].startswith(UTF8_BOM):
4116                self.cursor[2] = self.cursor[2][len(UTF8_BOM):]
4117                self.bom = UTF8_BOM
4118            self.unread(self.cursor)
4119            self.cursor = None
4120    def closefile(self):
4121        """Used by class methods to close nested include files."""
4122        self.f.close()
4123        self.next = []
4124    def close(self):
4125        self.closefile()
4126        self.__init__()
4127    def read(self, skip=False):
4128        """Read next line. Return None if EOF. Expand tabs. Strip trailing
4129        white space. Maintain self.next read ahead buffer. If skip=True then
4130        conditional exclusion is active (ifdef and ifndef macros)."""
4131        # Top up buffer.
4132        if len(self.next) <= self.READ_BUFFER_MIN:
4133            s = self.f.readline()
4134            if s:
4135                self._lineno = self._lineno + 1
4136            while s:
4137                if self.tabsize != 0:
4138                    s = s.expandtabs(self.tabsize)
4139                s = s.rstrip()
4140                self.next.append([self.fname,self._lineno,s])
4141                if len(self.next) > self.READ_BUFFER_MIN:
4142                    break
4143                s = self.f.readline()
4144                if s:
4145                    self._lineno = self._lineno + 1
4146        # Return first (oldest) buffer entry.
4147        if len(self.next) > 0:
4148            self.cursor = self.next[0]
4149            del self.next[0]
4150            result = self.cursor[2]
4151            # Check for include macro.
4152            mo = macros.match('+',r'^include[1]?$',result)
4153            if mo and not skip:
4154                # Parse include macro attributes.
4155                attrs = {}
4156                parse_attributes(mo.group('attrlist'),attrs)
4157                warnings = attrs.get('warnings', True)
4158                # Don't process include macro once the maximum depth is reached.
4159                if self.current_depth >= self.max_depth:
4160                    message.warning('maximum include depth exceeded')
4161                    return result
4162                # Perform attribute substitution on include macro file name.
4163                fname = subs_attrs(mo.group('target'))
4164                if not fname:
4165                    return Reader1.read(self)   # Return next input line.
4166                if self.fname != '<stdin>':
4167                    fname = os.path.expandvars(os.path.expanduser(fname))
4168                    fname = safe_filename(fname, os.path.dirname(self.fname))
4169                    if not fname:
4170                        return Reader1.read(self)   # Return next input line.
4171                    if not os.path.isfile(fname):
4172                        if warnings:
4173                            message.warning('include file not found: %s' % fname)
4174                        return Reader1.read(self)   # Return next input line.
4175                    if mo.group('name') == 'include1':
4176                        if not config.dumping:
4177                            if fname not in config.include1:
4178                                message.verbose('include1: ' + fname, linenos=False)
4179                                # Store the include file in memory for later
4180                                # retrieval by the {include1:} system attribute.
4181                                f = open(fname)
4182                                try:
4183                                    config.include1[fname] = [
4184                                        s.rstrip() for s in f]
4185                                finally:
4186                                    f.close()
4187                            return '{include1:%s}' % fname
4188                        else:
4189                            # This is a configuration dump, just pass the macro
4190                            # call through.
4191                            return result
4192                # Clone self and set as parent (self assumes the role of child).
4193                parent = Reader1()
4194                assign(parent,self)
4195                self.parent = parent
4196                # Set attributes in child.
4197                if 'tabsize' in attrs:
4198                    try:
4199                        val = int(attrs['tabsize'])
4200                        if not val >= 0:
4201                            raise ValueError, 'not >= 0'
4202                        self.tabsize = val
4203                    except ValueError:
4204                        raise EAsciiDoc, 'illegal include macro tabsize argument'
4205                else:
4206                    self.tabsize = config.tabsize
4207                if 'depth' in attrs:
4208                    try:
4209                        val = int(attrs['depth'])
4210                        if not val >= 1:
4211                            raise ValueError, 'not >= 1'
4212                        self.max_depth = self.current_depth + val
4213                    except ValueError:
4214                        raise EAsciiDoc, "include macro: illegal 'depth' argument"
4215                # Process included file.
4216                message.verbose('include: ' + fname, linenos=False)
4217                self.open(fname)
4218                self.current_depth = self.current_depth + 1
4219                result = Reader1.read(self)
4220        else:
4221            if not Reader1.eof(self):
4222                result = Reader1.read(self)
4223            else:
4224                result = None
4225        return result
4226    def eof(self):
4227        """Returns True if all lines have been read."""
4228        if len(self.next) == 0:
4229            # End of current file.
4230            if self.parent:
4231                self.closefile()
4232                assign(self,self.parent)    # Restore parent reader.
4233                document.attributes['infile'] = self.infile
4234                document.attributes['indir'] = self.indir
4235                return Reader1.eof(self)
4236            else:
4237                return True
4238        else:
4239            return False
4240    def read_next(self):
4241        """Like read() but does not advance file pointer."""
4242        if Reader1.eof(self):
4243            return None
4244        else:
4245            return self.next[0][2]
4246    def unread(self,cursor):
4247        """Push the line (filename,linenumber,linetext) tuple back into the read
4248        buffer. Note that it's up to the caller to restore the previous
4249        cursor."""
4250        assert cursor
4251        self.next.insert(0,cursor)
4252
4253class Reader(Reader1):
4254    """ Wraps (well, sought of) Reader1 class and implements conditional text
4255    inclusion."""
4256    def __init__(self):
4257        Reader1.__init__(self)
4258        self.depth = 0          # if nesting depth.
4259        self.skip = False       # true if we're skipping ifdef...endif.
4260        self.skipname = ''      # Name of current endif macro target.
4261        self.skipto = -1        # The depth at which skipping is reenabled.
4262    def read_super(self):
4263        result = Reader1.read(self,self.skip)
4264        if result is None and self.skip:
4265            raise EAsciiDoc,'missing endif::%s[]' % self.skipname
4266        return result
4267    def read(self):
4268        result = self.read_super()
4269        if result is None:
4270            return None
4271        while self.skip:
4272            mo = macros.match('+',r'ifdef|ifndef|ifeval|endif',result)
4273            if mo:
4274                name = mo.group('name')
4275                target = mo.group('target')
4276                attrlist = mo.group('attrlist')
4277                if name == 'endif':
4278                    self.depth -= 1
4279                    if self.depth < 0:
4280                        raise EAsciiDoc,'mismatched macro: %s' % result
4281                    if self.depth == self.skipto:
4282                        self.skip = False
4283                        if target and self.skipname != target:
4284                            raise EAsciiDoc,'mismatched macro: %s' % result
4285                else:
4286                    if name in ('ifdef','ifndef'):
4287                        if not target:
4288                            raise EAsciiDoc,'missing macro target: %s' % result
4289                        if not attrlist:
4290                            self.depth += 1
4291                    elif name == 'ifeval':
4292                        if not attrlist:
4293                            raise EAsciiDoc,'missing ifeval condition: %s' % result
4294                        self.depth += 1
4295            result = self.read_super()
4296            if result is None:
4297                return None
4298        mo = macros.match('+',r'ifdef|ifndef|ifeval|endif',result)
4299        if mo:
4300            name = mo.group('name')
4301            target = mo.group('target')
4302            attrlist = mo.group('attrlist')
4303            if name == 'endif':
4304                self.depth = self.depth-1
4305            else:
4306                if not target and name in ('ifdef','ifndef'):
4307                    raise EAsciiDoc,'missing macro target: %s' % result
4308                defined = is_attr_defined(target, document.attributes)
4309                if name == 'ifdef':
4310                    if attrlist:
4311                        if defined: return attrlist
4312                    else:
4313                        self.skip = not defined
4314                elif name == 'ifndef':
4315                    if attrlist:
4316                        if not defined: return attrlist
4317                    else:
4318                        self.skip = defined
4319                elif name == 'ifeval':
4320                    if safe():
4321                        message.unsafe('ifeval invalid')
4322                        raise EAsciiDoc,'ifeval invalid safe document'
4323                    if not attrlist:
4324                        raise EAsciiDoc,'missing ifeval condition: %s' % result
4325                    cond = False
4326                    attrlist = subs_attrs(attrlist)
4327                    if attrlist:
4328                        try:
4329                            cond = eval(attrlist)
4330                        except Exception,e:
4331                            raise EAsciiDoc,'error evaluating ifeval condition: %s: %s' % (result, str(e))
4332                        message.verbose('ifeval: %s: %r' % (attrlist, cond))
4333                    self.skip = not cond
4334                if not attrlist or name == 'ifeval':
4335                    if self.skip:
4336                        self.skipto = self.depth
4337                        self.skipname = target
4338                    self.depth = self.depth+1
4339            result = self.read()
4340        if result:
4341            # Expand executable block macros.
4342            mo = macros.match('+',r'eval|sys|sys2',result)
4343            if mo:
4344                action = mo.group('name')
4345                cmd = mo.group('attrlist')
4346                result = system(action, cmd, is_macro=True)
4347                self.cursor[2] = result  # So we don't re-evaluate.
4348        if result:
4349            # Unescape escaped system macros.
4350            if macros.match('+',r'\\eval|\\sys|\\sys2|\\ifdef|\\ifndef|\\endif|\\include|\\include1',result):
4351                result = result[1:]
4352        return result
4353    def eof(self):
4354        return self.read_next() is None
4355    def read_next(self):
4356        save_cursor = self.cursor
4357        result = self.read()
4358        if result is not None:
4359            self.unread(self.cursor)
4360            self.cursor = save_cursor
4361        return result
4362    def read_lines(self,count=1):
4363        """Return tuple containing count lines."""
4364        result = []
4365        i = 0
4366        while i < count and not self.eof():
4367            result.append(self.read())
4368        return tuple(result)
4369    def read_ahead(self,count=1):
4370        """Same as read_lines() but does not advance the file pointer."""
4371        result = []
4372        putback = []
4373        save_cursor = self.cursor
4374        try:
4375            i = 0
4376            while i < count and not self.eof():
4377                result.append(self.read())
4378                putback.append(self.cursor)
4379                i = i+1
4380            while putback:
4381                self.unread(putback.pop())
4382        finally:
4383            self.cursor = save_cursor
4384        return tuple(result)
4385    def skip_blank_lines(self):
4386        reader.read_until(r'\s*\S+')
4387    def read_until(self,terminators,same_file=False):
4388        """Like read() but reads lines up to (but not including) the first line
4389        that matches the terminator regular expression, regular expression
4390        object or list of regular expression objects. If same_file is True then
4391        the terminating pattern must occur in the file the was being read when
4392        the routine was called."""
4393        if same_file:
4394            fname = self.cursor[0]
4395        result = []
4396        if not isinstance(terminators,list):
4397            if isinstance(terminators,basestring):
4398                terminators = [re.compile(terminators)]
4399            else:
4400                terminators = [terminators]
4401        while not self.eof():
4402            save_cursor = self.cursor
4403            s = self.read()
4404            if not same_file or fname == self.cursor[0]:
4405                for reo in terminators:
4406                    if reo.match(s):
4407                        self.unread(self.cursor)
4408                        self.cursor = save_cursor
4409                        return tuple(result)
4410            result.append(s)
4411        return tuple(result)
4412
4413class Writer:
4414    """Writes lines to output file."""
4415    def __init__(self):
4416        self.newline = '\r\n'            # End of line terminator.
4417        self.f = None                    # Output file object.
4418        self.fname = None                # Output file name.
4419        self.lines_out = 0               # Number of lines written.
4420        self.skip_blank_lines = False    # If True don't output blank lines.
4421    def open(self,fname,bom=None):
4422        '''
4423        bom is optional byte order mark.
4424        http://en.wikipedia.org/wiki/Byte-order_mark
4425        '''
4426        self.fname = fname
4427        if fname == '<stdout>':
4428            self.f = sys.stdout
4429        else:
4430            self.f = open(fname,'wb+')
4431        message.verbose('writing: '+writer.fname,False)
4432        if bom:
4433            self.f.write(bom)
4434        self.lines_out = 0
4435    def close(self):
4436        if self.fname != '<stdout>':
4437            self.f.close()
4438    def write_line(self, line=None):
4439        if not (self.skip_blank_lines and (not line or not line.strip())):
4440            self.f.write((line or '') + self.newline)
4441            self.lines_out = self.lines_out + 1
4442    def write(self,*args,**kwargs):
4443        """Iterates arguments, writes tuple and list arguments one line per
4444        element, else writes argument as single line. If no arguments writes
4445        blank line. If argument is None nothing is written. self.newline is
4446        appended to each line."""
4447        if 'trace' in kwargs and len(args) > 0:
4448            trace(kwargs['trace'],args[0])
4449        if len(args) == 0:
4450            self.write_line()
4451            self.lines_out = self.lines_out + 1
4452        else:
4453            for arg in args:
4454                if is_array(arg):
4455                    for s in arg:
4456                        self.write_line(s)
4457                elif arg is not None:
4458                    self.write_line(arg)
4459    def write_tag(self,tag,content,subs=None,d=None,**kwargs):
4460        """Write content enveloped by tag.
4461        Substitutions specified in the 'subs' list are perform on the
4462        'content'."""
4463        if subs is None:
4464            subs = config.subsnormal
4465        stag,etag = subs_tag(tag,d)
4466        content = Lex.subs(content,subs)
4467        if 'trace' in kwargs:
4468            trace(kwargs['trace'],[stag]+content+[etag])
4469        if stag:
4470            self.write(stag)
4471        if content:
4472            self.write(content)
4473        if etag:
4474            self.write(etag)
4475
4476#---------------------------------------------------------------------------
4477# Configuration file processing.
4478#---------------------------------------------------------------------------
4479def _subs_specialwords(mo):
4480    """Special word substitution function called by
4481    Config.subs_specialwords()."""
4482    word = mo.re.pattern                    # The special word.
4483    template = config.specialwords[word]    # The corresponding markup template.
4484    if not template in config.sections:
4485        raise EAsciiDoc,'missing special word template [%s]' % template
4486    if mo.group()[0] == '\\':
4487        return mo.group()[1:]   # Return escaped word.
4488    args = {}
4489    args['words'] = mo.group()  # The full match string is argument 'words'.
4490    args.update(mo.groupdict()) # Add other named match groups to the arguments.
4491    # Delete groups that didn't participate in match.
4492    for k,v in args.items():
4493        if v is None: del args[k]
4494    lines = subs_attrs(config.sections[template],args)
4495    if len(lines) == 0:
4496        result = ''
4497    elif len(lines) == 1:
4498        result = lines[0]
4499    else:
4500        result = writer.newline.join(lines)
4501    return result
4502
4503class Config:
4504    """Methods to process configuration files."""
4505    # Non-template section name regexp's.
4506    ENTRIES_SECTIONS= ('tags','miscellaneous','attributes','specialcharacters',
4507            'specialwords','macros','replacements','quotes','titles',
4508            r'paradef-.+',r'listdef-.+',r'blockdef-.+',r'tabledef-.+',
4509            r'tabletags-.+',r'listtags-.+','replacements[23]',
4510            r'old_tabledef-.+')
4511    def __init__(self):
4512        self.sections = OrderedDict()   # Keyed by section name containing
4513                                        # lists of section lines.
4514        # Command-line options.
4515        self.verbose = False
4516        self.header_footer = True       # -s, --no-header-footer option.
4517        # [miscellaneous] section.
4518        self.tabsize = 8
4519        self.textwidth = 70             # DEPRECATED: Old tables only.
4520        self.newline = '\r\n'
4521        self.pagewidth = None
4522        self.pageunits = None
4523        self.outfilesuffix = ''
4524        self.subsnormal = SUBS_NORMAL
4525        self.subsverbatim = SUBS_VERBATIM
4526
4527        self.tags = {}          # Values contain (stag,etag) tuples.
4528        self.specialchars = {}  # Values of special character substitutions.
4529        self.specialwords = {}  # Name is special word pattern, value is macro.
4530        self.replacements = OrderedDict()   # Key is find pattern, value is
4531                                            #replace pattern.
4532        self.replacements2 = OrderedDict()
4533        self.replacements3 = OrderedDict()
4534        self.specialsections = {} # Name is special section name pattern, value
4535                                  # is corresponding section name.
4536        self.quotes = OrderedDict()    # Values contain corresponding tag name.
4537        self.fname = ''         # Most recently loaded configuration file name.
4538        self.conf_attrs = {}    # Attributes entries from conf files.
4539        self.cmd_attrs = {}     # Attributes from command-line -a options.
4540        self.loaded = []        # Loaded conf files.
4541        self.include1 = {}      # Holds include1::[] files for {include1:}.
4542        self.dumping = False    # True if asciidoc -c option specified.
4543        self.filters = []       # Filter names specified by --filter option.
4544
4545    def init(self, cmd):
4546        """
4547        Check Python version and locate the executable and configuration files
4548        directory.
4549        cmd is the asciidoc command or asciidoc.py path.
4550        """
4551        if float(sys.version[:3]) < float(MIN_PYTHON_VERSION):
4552            message.stderr('FAILED: Python %s or better required' %
4553                    MIN_PYTHON_VERSION)
4554            sys.exit(1)
4555        if not os.path.exists(cmd):
4556            message.stderr('FAILED: Missing asciidoc command: %s' % cmd)
4557            sys.exit(1)
4558        global APP_FILE
4559        APP_FILE = os.path.realpath(cmd)
4560        global APP_DIR
4561        APP_DIR = os.path.dirname(APP_FILE)
4562        global USER_DIR
4563        USER_DIR = userdir()
4564        if USER_DIR is not None:
4565            USER_DIR = os.path.join(USER_DIR,'.asciidoc')
4566            if not os.path.isdir(USER_DIR):
4567                USER_DIR = None
4568
4569    def load_file(self, fname, dir=None, include=[], exclude=[]):
4570        """
4571        Loads sections dictionary with sections from file fname.
4572        Existing sections are overlaid.
4573        The 'include' list contains the section names to be loaded.
4574        The 'exclude' list contains section names not to be loaded.
4575        Return False if no file was found in any of the locations.
4576        """
4577        def update_section(section):
4578            """ Update section in sections with contents. """
4579            if section and contents:
4580                if section in sections and self.entries_section(section):
4581                    if ''.join(contents):
4582                        # Merge entries.
4583                        sections[section] += contents
4584                    else:
4585                        del sections[section]
4586                else:
4587                    if section.startswith('+'):
4588                        # Append section.
4589                        if section in sections:
4590                            sections[section] += contents
4591                        else:
4592                            sections[section] = contents
4593                    else:
4594                        # Replace section.
4595                        sections[section] = contents
4596        if dir:
4597            fname = os.path.join(dir, fname)
4598        # Sliently skip missing configuration file.
4599        if not os.path.isfile(fname):
4600            return False
4601        # Don't load conf files twice (local and application conf files are the
4602        # same if the source file is in the application directory).
4603        if os.path.realpath(fname) in self.loaded:
4604            return True
4605        rdr = Reader()  # Reader processes system macros.
4606        message.linenos = False         # Disable document line numbers.
4607        rdr.open(fname)
4608        message.linenos = None
4609        self.fname = fname
4610        reo = re.compile(r'(?u)^\[(?P<section>\+?[^\W\d][\w-]*)\]\s*$')
4611        sections = OrderedDict()
4612        section,contents = '',[]
4613        while not rdr.eof():
4614            s = rdr.read()
4615            if s and s[0] == '#':       # Skip comment lines.
4616                continue
4617            if s[:2] == '\\#':          # Unescape lines starting with '#'.
4618                s = s[1:]
4619            s = s.rstrip()
4620            found = reo.findall(s)
4621            if found:
4622                update_section(section) # Store previous section.
4623                section = found[0].lower()
4624                contents = []
4625            else:
4626                contents.append(s)
4627        update_section(section)         # Store last section.
4628        rdr.close()
4629        if include:
4630            for s in set(sections) - set(include):
4631                del sections[s]
4632        if exclude:
4633            for s in set(sections) & set(exclude):
4634                del sections[s]
4635        attrs = {}
4636        self.load_sections(sections,attrs)
4637        if not include:
4638            # If all sections are loaded mark this file as loaded.
4639            self.loaded.append(os.path.realpath(fname))
4640        document.update_attributes(attrs) # So they are available immediately.
4641        return True
4642
4643    def load_sections(self,sections,attrs=None):
4644        """
4645        Loads sections dictionary. Each dictionary entry contains a
4646        list of lines.
4647        Updates 'attrs' with parsed [attributes] section entries.
4648        """
4649        # Delete trailing blank lines from sections.
4650        for k in sections.keys():
4651            for i in range(len(sections[k])-1,-1,-1):
4652                if not sections[k][i]:
4653                    del sections[k][i]
4654                elif not self.entries_section(k):
4655                    break
4656        # Update new sections.
4657        for k,v in sections.items():
4658            if k.startswith('+'):
4659                # Append section.
4660                k = k[1:]
4661                if k in self.sections:
4662                    self.sections[k] += v
4663                else:
4664                    self.sections[k] = v
4665            else:
4666                # Replace section.
4667                self.sections[k] = v
4668        self.parse_tags()
4669        # Internally [miscellaneous] section entries are just attributes.
4670        d = {}
4671        parse_entries(sections.get('miscellaneous',()), d, unquote=True,
4672                allow_name_only=True)
4673        parse_entries(sections.get('attributes',()), d, unquote=True,
4674                allow_name_only=True)
4675        update_attrs(self.conf_attrs,d)
4676        if attrs is not None:
4677            attrs.update(d)
4678        d = {}
4679        parse_entries(sections.get('titles',()),d)
4680        Title.load(d)
4681        parse_entries(sections.get('specialcharacters',()),self.specialchars,escape_delimiter=False)
4682        parse_entries(sections.get('quotes',()),self.quotes)
4683        self.parse_specialwords()
4684        self.parse_replacements()
4685        self.parse_replacements('replacements2')
4686        self.parse_replacements('replacements3')
4687        self.parse_specialsections()
4688        paragraphs.load(sections)
4689        lists.load(sections)
4690        blocks.load(sections)
4691        tables_OLD.load(sections)
4692        tables.load(sections)
4693        macros.load(sections.get('macros',()))
4694
4695    def get_load_dirs(self):
4696        """
4697        Return list of well known paths with conf files.
4698        """
4699        result = []
4700        if localapp():
4701            # Load from folders in asciidoc executable directory.
4702            result.append(APP_DIR)
4703        else:
4704            # Load from global configuration directory.
4705            result.append(CONF_DIR)
4706        # Load configuration files from ~/.asciidoc if it exists.
4707        if USER_DIR is not None:
4708            result.append(USER_DIR)
4709        return result
4710
4711    def find_in_dirs(self, filename, dirs=None):
4712        """
4713        Find conf files from dirs list.
4714        Return list of found file paths.
4715        Return empty list if not found in any of the locations.
4716        """
4717        result = []
4718        if dirs is None:
4719            dirs = self.get_load_dirs()
4720        for d in dirs:
4721            f = os.path.join(d,filename)
4722            if os.path.isfile(f):
4723                result.append(f)
4724        return result
4725
4726    def load_from_dirs(self, filename, dirs=None, include=[]):
4727        """
4728        Load conf file from dirs list.
4729        If dirs not specified try all the well known locations.
4730        Return False if no file was sucessfully loaded.
4731        """
4732        count = 0
4733        for f in self.find_in_dirs(filename,dirs):
4734            if self.load_file(f, include=include):
4735                count += 1
4736        return count != 0
4737
4738    def load_backend(self, dirs=None):
4739        """
4740        Load the backend configuration files from dirs list.
4741        If dirs not specified try all the well known locations.
4742        If a <backend>.conf file was found return it's full path name,
4743        if not found return None.
4744        """
4745        result = None
4746        if dirs is None:
4747            dirs = self.get_load_dirs()
4748        conf = document.backend + '.conf'
4749        conf2 = document.backend + '-' + document.doctype + '.conf'
4750        # First search for filter backends.
4751        for d in [os.path.join(d, 'backends', document.backend) for d in dirs]:
4752            if self.load_file(conf,d):
4753                result = os.path.join(d, conf)
4754            self.load_file(conf2,d)
4755        if not result:
4756            # Search in the normal locations.
4757            for d in dirs:
4758                if self.load_file(conf,d):
4759                    result = os.path.join(d, conf)
4760                self.load_file(conf2,d)
4761        return result
4762
4763    def load_filters(self, dirs=None):
4764        """
4765        Load filter configuration files from 'filters' directory in dirs list.
4766        If dirs not specified try all the well known locations.  Suppress
4767        loading if a file named __noautoload__ is in same directory as the conf
4768        file unless the filter has been specified with the --filter
4769        command-line option (in which case it is loaded unconditionally).
4770        """
4771        if dirs is None:
4772            dirs = self.get_load_dirs()
4773        for d in dirs:
4774            # Load filter .conf files.
4775            filtersdir = os.path.join(d,'filters')
4776            for dirpath,dirnames,filenames in os.walk(filtersdir):
4777                subdirs = dirpath[len(filtersdir):].split(os.path.sep)
4778                # True if processing a filter specified by a --filter option.
4779                filter_opt = len(subdirs) > 1 and subdirs[1] in self.filters
4780                if '__noautoload__' not in filenames or filter_opt:
4781                    for f in filenames:
4782                        if re.match(r'^.+\.conf$',f):
4783                            self.load_file(f,dirpath)
4784
4785    def find_config_dir(self, *dirnames):
4786        """
4787        Return path of configuration directory.
4788        Try all the well known locations.
4789        Return None if directory not found.
4790        """
4791        for d in [os.path.join(d, *dirnames) for d in self.get_load_dirs()]:
4792            if os.path.isdir(d):
4793                return d
4794        return None
4795
4796    def set_theme_attributes(self):
4797        theme = document.attributes.get('theme')
4798        if theme and 'themedir' not in document.attributes:
4799            themedir = self.find_config_dir('themes', theme)
4800            if themedir:
4801                document.attributes['themedir'] = themedir
4802                iconsdir = os.path.join(themedir, 'icons')
4803                if 'data-uri' in document.attributes and os.path.isdir(iconsdir):
4804                    document.attributes['iconsdir'] = iconsdir
4805            else:
4806                message.warning('missing theme: %s' % theme, linenos=False)
4807
4808    def load_miscellaneous(self,d):
4809        """Set miscellaneous configuration entries from dictionary 'd'."""
4810        def set_if_int_gt_zero(name, d):
4811            if name in d:
4812                try:
4813                    val = int(d[name])
4814                    if not val > 0:
4815                        raise ValueError, "not > 0"
4816                    if val > 0:
4817                        setattr(self, name, val)
4818                except ValueError:
4819                    raise EAsciiDoc, 'illegal [miscellaneous] %s entry' % name
4820        set_if_int_gt_zero('tabsize', d)
4821        set_if_int_gt_zero('textwidth', d) # DEPRECATED: Old tables only.
4822
4823        if 'pagewidth' in d:
4824            try:
4825                val = float(d['pagewidth'])
4826                self.pagewidth = val
4827            except ValueError:
4828                raise EAsciiDoc, 'illegal [miscellaneous] pagewidth entry'
4829
4830        if 'pageunits' in d:
4831            self.pageunits = d['pageunits']
4832        if 'outfilesuffix' in d:
4833            self.outfilesuffix = d['outfilesuffix']
4834        if 'newline' in d:
4835            # Convert escape sequences to their character values.
4836            self.newline = literal_eval('"'+d['newline']+'"')
4837        if 'subsnormal' in d:
4838            self.subsnormal = parse_options(d['subsnormal'],SUBS_OPTIONS,
4839                    'illegal [%s] %s: %s' %
4840                    ('miscellaneous','subsnormal',d['subsnormal']))
4841        if 'subsverbatim' in d:
4842            self.subsverbatim = parse_options(d['subsverbatim'],SUBS_OPTIONS,
4843                    'illegal [%s] %s: %s' %
4844                    ('miscellaneous','subsverbatim',d['subsverbatim']))
4845
4846    def validate(self):
4847        """Check the configuration for internal consistancy. Called after all
4848        configuration files have been loaded."""
4849        message.linenos = False     # Disable document line numbers.
4850        # Heuristic to validate that at least one configuration file was loaded.
4851        if not self.specialchars or not self.tags or not lists:
4852            raise EAsciiDoc,'incomplete configuration files'
4853        # Check special characters are only one character long.
4854        for k in self.specialchars.keys():
4855            if len(k) != 1:
4856                raise EAsciiDoc,'[specialcharacters] ' \
4857                                'must be a single character: %s' % k
4858        # Check all special words have a corresponding inline macro body.
4859        for macro in self.specialwords.values():
4860            if not is_name(macro):
4861                raise EAsciiDoc,'illegal special word name: %s' % macro
4862            if not macro in self.sections:
4863                message.warning('missing special word macro: [%s]' % macro)
4864        # Check all text quotes have a corresponding tag.
4865        for q in self.quotes.keys()[:]:
4866            tag = self.quotes[q]
4867            if not tag:
4868                del self.quotes[q]  # Undefine quote.
4869            else:
4870                if tag[0] == '#':
4871                    tag = tag[1:]
4872                if not tag in self.tags:
4873                    message.warning('[quotes] %s missing tag definition: %s' % (q,tag))
4874        # Check all specialsections section names exist.
4875        for k,v in self.specialsections.items():
4876            if not v:
4877                del self.specialsections[k]
4878            elif not v in self.sections:
4879                message.warning('missing specialsections section: [%s]' % v)
4880        paragraphs.validate()
4881        lists.validate()
4882        blocks.validate()
4883        tables_OLD.validate()
4884        tables.validate()
4885        macros.validate()
4886        message.linenos = None
4887
4888    def entries_section(self,section_name):
4889        """
4890        Return True if conf file section contains entries, not a markup
4891        template.
4892        """
4893        for name in self.ENTRIES_SECTIONS:
4894            if re.match(name,section_name):
4895                return True
4896        return False
4897
4898    def dump(self):
4899        """Dump configuration to stdout."""
4900        # Header.
4901        hdr = ''
4902        hdr = hdr + '#' + writer.newline
4903        hdr = hdr + '# Generated by AsciiDoc %s for %s %s.%s' % \
4904            (VERSION,document.backend,document.doctype,writer.newline)
4905        t = time.asctime(time.localtime(time.time()))
4906        hdr = hdr + '# %s%s' % (t,writer.newline)
4907        hdr = hdr + '#' + writer.newline
4908        sys.stdout.write(hdr)
4909        # Dump special sections.
4910        # Dump only the configuration file and command-line attributes.
4911        # [miscellanous] entries are dumped as part of the [attributes].
4912        d = {}
4913        d.update(self.conf_attrs)
4914        d.update(self.cmd_attrs)
4915        dump_section('attributes',d)
4916        Title.dump()
4917        dump_section('quotes',self.quotes)
4918        dump_section('specialcharacters',self.specialchars)
4919        d = {}
4920        for k,v in self.specialwords.items():
4921            if v in d:
4922                d[v] = '%s "%s"' % (d[v],k)   # Append word list.
4923            else:
4924                d[v] = '"%s"' % k
4925        dump_section('specialwords',d)
4926        dump_section('replacements',self.replacements)
4927        dump_section('replacements2',self.replacements2)
4928        dump_section('replacements3',self.replacements3)
4929        dump_section('specialsections',self.specialsections)
4930        d = {}
4931        for k,v in self.tags.items():
4932            d[k] = '%s|%s' % v
4933        dump_section('tags',d)
4934        paragraphs.dump()
4935        lists.dump()
4936        blocks.dump()
4937        tables_OLD.dump()
4938        tables.dump()
4939        macros.dump()
4940        # Dump remaining sections.
4941        for k in self.sections.keys():
4942            if not self.entries_section(k):
4943                sys.stdout.write('[%s]%s' % (k,writer.newline))
4944                for line in self.sections[k]:
4945                    sys.stdout.write('%s%s' % (line,writer.newline))
4946                sys.stdout.write(writer.newline)
4947
4948    def subs_section(self,section,d):
4949        """Section attribute substitution using attributes from
4950        document.attributes and 'd'.  Lines containing undefinded
4951        attributes are deleted."""
4952        if section in self.sections:
4953            return subs_attrs(self.sections[section],d)
4954        else:
4955            message.warning('missing section: [%s]' % section)
4956            return ()
4957
4958    def parse_tags(self):
4959        """Parse [tags] section entries into self.tags dictionary."""
4960        d = {}
4961        parse_entries(self.sections.get('tags',()),d)
4962        for k,v in d.items():
4963            if v is None:
4964                if k in self.tags:
4965                    del self.tags[k]
4966            elif v == '':
4967                self.tags[k] = (None,None)
4968            else:
4969                mo = re.match(r'(?P<stag>.*)\|(?P<etag>.*)',v)
4970                if mo:
4971                    self.tags[k] = (mo.group('stag'), mo.group('etag'))
4972                else:
4973                    raise EAsciiDoc,'[tag] %s value malformed' % k
4974
4975    def tag(self, name, d=None):
4976        """Returns (starttag,endtag) tuple named name from configuration file
4977        [tags] section. Raise error if not found. If a dictionary 'd' is
4978        passed then merge with document attributes and perform attribute
4979        substitution on tags."""
4980        if not name in self.tags:
4981            raise EAsciiDoc, 'missing tag: %s' % name
4982        stag,etag = self.tags[name]
4983        if d is not None:
4984            # TODO: Should we warn if substitution drops a tag?
4985            if stag:
4986                stag = subs_attrs(stag,d)
4987            if etag:
4988                etag = subs_attrs(etag,d)
4989        if stag is None: stag = ''
4990        if etag is None: etag = ''
4991        return (stag,etag)
4992
4993    def parse_specialsections(self):
4994        """Parse specialsections section to self.specialsections dictionary."""
4995        # TODO: This is virtually the same as parse_replacements() and should
4996        # be factored to single routine.
4997        d = {}
4998        parse_entries(self.sections.get('specialsections',()),d,unquote=True)
4999        for pat,sectname in d.items():
5000            pat = strip_quotes(pat)
5001            if not is_re(pat):
5002                raise EAsciiDoc,'[specialsections] entry ' \
5003                                'is not a valid regular expression: %s' % pat
5004            if sectname is None:
5005                if pat in self.specialsections:
5006                    del self.specialsections[pat]
5007            else:
5008                self.specialsections[pat] = sectname
5009
5010    def parse_replacements(self,sect='replacements'):
5011        """Parse replacements section into self.replacements dictionary."""
5012        d = OrderedDict()
5013        parse_entries(self.sections.get(sect,()), d, unquote=True)
5014        for pat,rep in d.items():
5015            if not self.set_replacement(pat, rep, getattr(self,sect)):
5016                raise EAsciiDoc,'[%s] entry in %s is not a valid' \
5017                    ' regular expression: %s' % (sect,self.fname,pat)
5018
5019    @staticmethod
5020    def set_replacement(pat, rep, replacements):
5021        """Add pattern and replacement to replacements dictionary."""
5022        pat = strip_quotes(pat)
5023        if not is_re(pat):
5024            return False
5025        if rep is None:
5026            if pat in replacements:
5027                del replacements[pat]
5028        else:
5029            replacements[pat] = strip_quotes(rep)
5030        return True
5031
5032    def subs_replacements(self,s,sect='replacements'):
5033        """Substitute patterns from self.replacements in 's'."""
5034        result = s
5035        for pat,rep in getattr(self,sect).items():
5036            result = re.sub(pat, rep, result)
5037        return result
5038
5039    def parse_specialwords(self):
5040        """Parse special words section into self.specialwords dictionary."""
5041        reo = re.compile(r'(?:\s|^)(".+?"|[^"\s]+)(?=\s|$)')
5042        for line in self.sections.get('specialwords',()):
5043            e = parse_entry(line)
5044            if not e:
5045                raise EAsciiDoc,'[specialwords] entry in %s is malformed: %s' \
5046                    % (self.fname,line)
5047            name,wordlist = e
5048            if not is_name(name):
5049                raise EAsciiDoc,'[specialwords] name in %s is illegal: %s' \
5050                    % (self.fname,name)
5051            if wordlist is None:
5052                # Undefine all words associated with 'name'.
5053                for k,v in self.specialwords.items():
5054                    if v == name:
5055                        del self.specialwords[k]
5056            else:
5057                words = reo.findall(wordlist)
5058                for word in words:
5059                    word = strip_quotes(word)
5060                    if not is_re(word):
5061                        raise EAsciiDoc,'[specialwords] entry in %s ' \
5062                            'is not a valid regular expression: %s' \
5063                            % (self.fname,word)
5064                    self.specialwords[word] = name
5065
5066    def subs_specialchars(self,s):
5067        """Perform special character substitution on string 's'."""
5068        """It may seem like a good idea to escape special characters with a '\'
5069        character, the reason we don't is because the escape character itself
5070        then has to be escaped and this makes including code listings
5071        problematic. Use the predefined {amp},{lt},{gt} attributes instead."""
5072        result = ''
5073        for ch in s:
5074            result = result + self.specialchars.get(ch,ch)
5075        return result
5076
5077    def subs_specialchars_reverse(self,s):
5078        """Perform reverse special character substitution on string 's'."""
5079        result = s
5080        for k,v in self.specialchars.items():
5081            result = result.replace(v, k)
5082        return result
5083
5084    def subs_specialwords(self,s):
5085        """Search for word patterns from self.specialwords in 's' and
5086        substitute using corresponding macro."""
5087        result = s
5088        for word in self.specialwords.keys():
5089            result = re.sub(word, _subs_specialwords, result)
5090        return result
5091
5092    def expand_templates(self,entries):
5093        """Expand any template::[] macros in a list of section entries."""
5094        result = []
5095        for line in entries:
5096            mo = macros.match('+',r'template',line)
5097            if mo:
5098                s = mo.group('attrlist')
5099                if s in self.sections:
5100                    result += self.expand_templates(self.sections[s])
5101                else:
5102                    message.warning('missing section: [%s]' % s)
5103                    result.append(line)
5104            else:
5105                result.append(line)
5106        return result
5107
5108    def expand_all_templates(self):
5109        for k,v in self.sections.items():
5110            self.sections[k] = self.expand_templates(v)
5111
5112    def section2tags(self, section, d={}, skipstart=False, skipend=False):
5113        """Perform attribute substitution on 'section' using document
5114        attributes plus 'd' attributes. Return tuple (stag,etag) containing
5115        pre and post | placeholder tags. 'skipstart' and 'skipend' are
5116        used to suppress substitution."""
5117        assert section is not None
5118        if section in self.sections:
5119            body = self.sections[section]
5120        else:
5121            message.warning('missing section: [%s]' % section)
5122            body = ()
5123        # Split macro body into start and end tag lists.
5124        stag = []
5125        etag = []
5126        in_stag = True
5127        for s in body:
5128            if in_stag:
5129                mo = re.match(r'(?P<stag>.*)\|(?P<etag>.*)',s)
5130                if mo:
5131                    if mo.group('stag'):
5132                        stag.append(mo.group('stag'))
5133                    if mo.group('etag'):
5134                        etag.append(mo.group('etag'))
5135                    in_stag = False
5136                else:
5137                    stag.append(s)
5138            else:
5139                etag.append(s)
5140        # Do attribute substitution last so {brkbar} can be used to escape |.
5141        # But don't do attribute substitution on title -- we've already done it.
5142        title = d.get('title')
5143        if title:
5144            d['title'] = chr(0)  # Replace with unused character.
5145        if not skipstart:
5146            stag = subs_attrs(stag, d)
5147        if not skipend:
5148            etag = subs_attrs(etag, d)
5149        # Put the {title} back.
5150        if title:
5151            stag = map(lambda x: x.replace(chr(0), title), stag)
5152            etag = map(lambda x: x.replace(chr(0), title), etag)
5153            d['title'] = title
5154        return (stag,etag)
5155
5156
5157#---------------------------------------------------------------------------
5158# Deprecated old table classes follow.
5159# Naming convention is an _OLD name suffix.
5160# These will be removed from future versions of AsciiDoc
5161
5162def join_lines_OLD(lines):
5163    """Return a list in which lines terminated with the backslash line
5164    continuation character are joined."""
5165    result = []
5166    s = ''
5167    continuation = False
5168    for line in lines:
5169        if line and line[-1] == '\\':
5170            s = s + line[:-1]
5171            continuation = True
5172            continue
5173        if continuation:
5174            result.append(s+line)
5175            s = ''
5176            continuation = False
5177        else:
5178            result.append(line)
5179    if continuation:
5180        result.append(s)
5181    return result
5182
5183class Column_OLD:
5184    """Table column."""
5185    def __init__(self):
5186        self.colalign = None    # 'left','right','center'
5187        self.rulerwidth = None
5188        self.colwidth = None    # Output width in page units.
5189
5190class Table_OLD(AbstractBlock):
5191    COL_STOP = r"(`|'|\.)"  # RE.
5192    ALIGNMENTS = {'`':'left', "'":'right', '.':'center'}
5193    FORMATS = ('fixed','csv','dsv')
5194    def __init__(self):
5195        AbstractBlock.__init__(self)
5196        self.CONF_ENTRIES += ('template','fillchar','format','colspec',
5197                              'headrow','footrow','bodyrow','headdata',
5198                              'footdata', 'bodydata')
5199        # Configuration parameters.
5200        self.fillchar=None
5201        self.format=None    # 'fixed','csv','dsv'
5202        self.colspec=None
5203        self.headrow=None
5204        self.footrow=None
5205        self.bodyrow=None
5206        self.headdata=None
5207        self.footdata=None
5208        self.bodydata=None
5209        # Calculated parameters.
5210        self.underline=None     # RE matching current table underline.
5211        self.isnumeric=False    # True if numeric ruler.
5212        self.tablewidth=None    # Optional table width scale factor.
5213        self.columns=[]         # List of Columns.
5214        # Other.
5215        self.check_msg=''       # Message set by previous self.validate() call.
5216    def load(self,name,entries):
5217        AbstractBlock.load(self,name,entries)
5218        """Update table definition from section entries in 'entries'."""
5219        for k,v in entries.items():
5220            if k == 'fillchar':
5221                if v and len(v) == 1:
5222                    self.fillchar = v
5223                else:
5224                    raise EAsciiDoc,'malformed table fillchar: %s' % v
5225            elif k == 'format':
5226                if v in Table_OLD.FORMATS:
5227                    self.format = v
5228                else:
5229                    raise EAsciiDoc,'illegal table format: %s' % v
5230            elif k == 'colspec':
5231                self.colspec = v
5232            elif k == 'headrow':
5233                self.headrow = v
5234            elif k == 'footrow':
5235                self.footrow = v
5236            elif k == 'bodyrow':
5237                self.bodyrow = v
5238            elif k == 'headdata':
5239                self.headdata = v
5240            elif k == 'footdata':
5241                self.footdata = v
5242            elif k == 'bodydata':
5243                self.bodydata = v
5244    def dump(self):
5245        AbstractBlock.dump(self)
5246        write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
5247        write('fillchar='+self.fillchar)
5248        write('format='+self.format)
5249        if self.colspec:
5250            write('colspec='+self.colspec)
5251        if self.headrow:
5252            write('headrow='+self.headrow)
5253        if self.footrow:
5254            write('footrow='+self.footrow)
5255        write('bodyrow='+self.bodyrow)
5256        if self.headdata:
5257            write('headdata='+self.headdata)
5258        if self.footdata:
5259            write('footdata='+self.footdata)
5260        write('bodydata='+self.bodydata)
5261        write('')
5262    def validate(self):
5263        AbstractBlock.validate(self)
5264        """Check table definition and set self.check_msg if invalid else set
5265        self.check_msg to blank string."""
5266        # Check global table parameters.
5267        if config.textwidth is None:
5268            self.check_msg = 'missing [miscellaneous] textwidth entry'
5269        elif config.pagewidth is None:
5270            self.check_msg = 'missing [miscellaneous] pagewidth entry'
5271        elif config.pageunits is None:
5272            self.check_msg = 'missing [miscellaneous] pageunits entry'
5273        elif self.headrow is None:
5274            self.check_msg = 'missing headrow entry'
5275        elif self.footrow is None:
5276            self.check_msg = 'missing footrow entry'
5277        elif self.bodyrow is None:
5278            self.check_msg = 'missing bodyrow entry'
5279        elif self.headdata is None:
5280            self.check_msg = 'missing headdata entry'
5281        elif self.footdata is None:
5282            self.check_msg = 'missing footdata entry'
5283        elif self.bodydata is None:
5284            self.check_msg = 'missing bodydata entry'
5285        else:
5286            # No errors.
5287            self.check_msg = ''
5288    def isnext(self):
5289        return AbstractBlock.isnext(self)
5290    def parse_ruler(self,ruler):
5291        """Parse ruler calculating underline and ruler column widths."""
5292        fc = re.escape(self.fillchar)
5293        # Strip and save optional tablewidth from end of ruler.
5294        mo = re.match(r'^(.*'+fc+r'+)([\d\.]+)$',ruler)
5295        if mo:
5296            ruler = mo.group(1)
5297            self.tablewidth = float(mo.group(2))
5298            self.attributes['tablewidth'] = str(float(self.tablewidth))
5299        else:
5300            self.tablewidth = None
5301            self.attributes['tablewidth'] = '100.0'
5302        # Guess whether column widths are specified numerically or not.
5303        if ruler[1] != self.fillchar:
5304            # If the first column does not start with a fillchar then numeric.
5305            self.isnumeric = True
5306        elif ruler[1:] == self.fillchar*len(ruler[1:]):
5307            # The case of one column followed by fillchars is numeric.
5308            self.isnumeric = True
5309        else:
5310            self.isnumeric = False
5311        # Underlines must be 3 or more fillchars.
5312        self.underline = r'^' + fc + r'{3,}$'
5313        splits = re.split(self.COL_STOP,ruler)[1:]
5314        # Build self.columns.
5315        for i in range(0,len(splits),2):
5316            c = Column_OLD()
5317            c.colalign = self.ALIGNMENTS[splits[i]]
5318            s = splits[i+1]
5319            if self.isnumeric:
5320                # Strip trailing fillchars.
5321                s = re.sub(fc+r'+$','',s)
5322                if s == '':
5323                    c.rulerwidth = None
5324                else:
5325                    try:
5326                        val = int(s)
5327                        if not val > 0:
5328                            raise ValueError, 'not > 0'
5329                        c.rulerwidth = val
5330                    except ValueError:
5331                        raise EAsciiDoc, 'malformed ruler: bad width'
5332            else:   # Calculate column width from inter-fillchar intervals.
5333                if not re.match(r'^'+fc+r'+$',s):
5334                    raise EAsciiDoc,'malformed ruler: illegal fillchars'
5335                c.rulerwidth = len(s)+1
5336            self.columns.append(c)
5337        # Fill in unspecified ruler widths.
5338        if self.isnumeric:
5339            if self.columns[0].rulerwidth is None:
5340                prevwidth = 1
5341            for c in self.columns:
5342                if c.rulerwidth is None:
5343                    c.rulerwidth = prevwidth
5344                prevwidth = c.rulerwidth
5345    def build_colspecs(self):
5346        """Generate colwidths and colspecs. This can only be done after the
5347        table arguments have been parsed since we use the table format."""
5348        self.attributes['cols'] = len(self.columns)
5349        # Calculate total ruler width.
5350        totalwidth = 0
5351        for c in self.columns:
5352            totalwidth = totalwidth + c.rulerwidth
5353        if totalwidth <= 0:
5354            raise EAsciiDoc,'zero width table'
5355        # Calculate marked up colwidths from rulerwidths.
5356        for c in self.columns:
5357            # Convert ruler width to output page width.
5358            width = float(c.rulerwidth)
5359            if self.format == 'fixed':
5360                if self.tablewidth is None:
5361                    # Size proportional to ruler width.
5362                    colfraction = width/config.textwidth
5363                else:
5364                    # Size proportional to page width.
5365                    colfraction = width/totalwidth
5366            else:
5367                    # Size proportional to page width.
5368                colfraction = width/totalwidth
5369            c.colwidth = colfraction * config.pagewidth # To page units.
5370            if self.tablewidth is not None:
5371                c.colwidth = c.colwidth * self.tablewidth   # Scale factor.
5372                if self.tablewidth > 1:
5373                    c.colwidth = c.colwidth/100 # tablewidth is in percent.
5374        # Build colspecs.
5375        if self.colspec:
5376            cols = []
5377            i = 0
5378            for c in self.columns:
5379                i += 1
5380                self.attributes['colalign'] = c.colalign
5381                self.attributes['colwidth'] = str(int(c.colwidth))
5382                self.attributes['colnumber'] = str(i + 1)
5383                s = subs_attrs(self.colspec,self.attributes)
5384                if not s:
5385                    message.warning('colspec dropped: contains undefined attribute')
5386                else:
5387                    cols.append(s)
5388            self.attributes['colspecs'] = writer.newline.join(cols)
5389    def split_rows(self,rows):
5390        """Return a two item tuple containing a list of lines up to but not
5391        including the next underline (continued lines are joined ) and the
5392        tuple of all lines after the underline."""
5393        reo = re.compile(self.underline)
5394        i = 0
5395        while not reo.match(rows[i]):
5396            i = i+1
5397        if i == 0:
5398            raise EAsciiDoc,'missing table rows'
5399        if i >= len(rows):
5400            raise EAsciiDoc,'closing [%s] underline expected' % self.defname
5401        return (join_lines_OLD(rows[:i]), rows[i+1:])
5402    def parse_rows(self, rows, rtag, dtag):
5403        """Parse rows list using the row and data tags. Returns a substituted
5404        list of output lines."""
5405        result = []
5406        # Source rows are parsed as single block, rather than line by line, to
5407        # allow the CSV reader to handle multi-line rows.
5408        if self.format == 'fixed':
5409            rows = self.parse_fixed(rows)
5410        elif self.format == 'csv':
5411            rows = self.parse_csv(rows)
5412        elif self.format == 'dsv':
5413            rows = self.parse_dsv(rows)
5414        else:
5415            assert True,'illegal table format'
5416        # Substitute and indent all data in all rows.
5417        stag,etag = subs_tag(rtag,self.attributes)
5418        for row in rows:
5419            result.append('  '+stag)
5420            for data in self.subs_row(row,dtag):
5421                result.append('    '+data)
5422            result.append('  '+etag)
5423        return result
5424    def subs_row(self, data, dtag):
5425        """Substitute the list of source row data elements using the data tag.
5426        Returns a substituted list of output table data items."""
5427        result = []
5428        if len(data) < len(self.columns):
5429            message.warning('fewer row data items then table columns')
5430        if len(data) > len(self.columns):
5431            message.warning('more row data items than table columns')
5432        for i in range(len(self.columns)):
5433            if i > len(data) - 1:
5434                d = ''  # Fill missing column data with blanks.
5435            else:
5436                d = data[i]
5437            c = self.columns[i]
5438            self.attributes['colalign'] = c.colalign
5439            self.attributes['colwidth'] = str(int(c.colwidth))
5440            self.attributes['colnumber'] = str(i + 1)
5441            stag,etag = subs_tag(dtag,self.attributes)
5442            # Insert AsciiDoc line break (' +') where row data has newlines
5443            # ('\n').  This is really only useful when the table format is csv
5444            # and the output markup is HTML. It's also a bit dubious in that it
5445            # assumes the user has not modified the shipped line break pattern.
5446            subs = self.get_subs()[0]
5447            if 'replacements2' in subs:
5448                # Insert line breaks in cell data.
5449                d = re.sub(r'(?m)\n',r' +\n',d)
5450                d = d.split('\n')    # So writer.newline is written.
5451            else:
5452                d = [d]
5453            result = result + [stag] + Lex.subs(d,subs) + [etag]
5454        return result
5455    def parse_fixed(self,rows):
5456        """Parse the list of source table rows. Each row item in the returned
5457        list contains a list of cell data elements."""
5458        result = []
5459        for row in rows:
5460            data = []
5461            start = 0
5462            # build an encoded representation
5463            row = char_decode(row)
5464            for c in self.columns:
5465                end = start + c.rulerwidth
5466                if c is self.columns[-1]:
5467                    # Text in last column can continue forever.
5468                    # Use the encoded string to slice, but convert back
5469                    # to plain string before further processing
5470                    data.append(char_encode(row[start:]).strip())
5471                else:
5472                    data.append(char_encode(row[start:end]).strip())
5473                start = end
5474            result.append(data)
5475        return result
5476    def parse_csv(self,rows):
5477        """Parse the list of source table rows. Each row item in the returned
5478        list contains a list of cell data elements."""
5479        import StringIO
5480        import csv
5481        result = []
5482        rdr = csv.reader(StringIO.StringIO('\r\n'.join(rows)),
5483            skipinitialspace=True)
5484        try:
5485            for row in rdr:
5486                result.append(row)
5487        except Exception:
5488            raise EAsciiDoc,'csv parse error: %s' % row
5489        return result
5490    def parse_dsv(self,rows):
5491        """Parse the list of source table rows. Each row item in the returned
5492        list contains a list of cell data elements."""
5493        separator = self.attributes.get('separator',':')
5494        separator = literal_eval('"'+separator+'"')
5495        if len(separator) != 1:
5496            raise EAsciiDoc,'malformed dsv separator: %s' % separator
5497        # TODO If separator is preceeded by an odd number of backslashes then
5498        # it is escaped and should not delimit.
5499        result = []
5500        for row in rows:
5501            # Skip blank lines
5502            if row == '': continue
5503            # Unescape escaped characters.
5504            row = literal_eval('"'+row.replace('"','\\"')+'"')
5505            data = row.split(separator)
5506            data = [s.strip() for s in data]
5507            result.append(data)
5508        return result
5509    def translate(self):
5510        message.deprecated('old tables syntax')
5511        AbstractBlock.translate(self)
5512        # Reset instance specific properties.
5513        self.underline = None
5514        self.columns = []
5515        attrs = {}
5516        BlockTitle.consume(attrs)
5517        # Add relevant globals to table substitutions.
5518        attrs['pagewidth'] = str(config.pagewidth)
5519        attrs['pageunits'] = config.pageunits
5520        # Mix in document attribute list.
5521        AttributeList.consume(attrs)
5522        # Validate overridable attributes.
5523        for k,v in attrs.items():
5524            if k == 'format':
5525                if v not in self.FORMATS:
5526                    raise EAsciiDoc, 'illegal [%s] %s: %s' % (self.defname,k,v)
5527                self.format = v
5528            elif k == 'tablewidth':
5529                try:
5530                    self.tablewidth = float(attrs['tablewidth'])
5531                except Exception:
5532                    raise EAsciiDoc, 'illegal [%s] %s: %s' % (self.defname,k,v)
5533        self.merge_attributes(attrs)
5534        # Parse table ruler.
5535        ruler = reader.read()
5536        assert re.match(self.delimiter,ruler)
5537        self.parse_ruler(ruler)
5538        # Read the entire table.
5539        table = []
5540        while True:
5541            line = reader.read_next()
5542            # Table terminated by underline followed by a blank line or EOF.
5543            if len(table) > 0 and re.match(self.underline,table[-1]):
5544                if line in ('',None):
5545                    break;
5546            if line is None:
5547                raise EAsciiDoc,'closing [%s] underline expected' % self.defname
5548            table.append(reader.read())
5549        # EXPERIMENTAL: The number of lines in the table, requested by Benjamin Klum.
5550        self.attributes['rows'] = str(len(table))
5551        if self.check_msg:  # Skip if table definition was marked invalid.
5552            message.warning('skipping [%s] table: %s' % (self.defname,self.check_msg))
5553            return
5554        self.push_blockname('table')
5555        # Generate colwidths and colspecs.
5556        self.build_colspecs()
5557        # Generate headrows, footrows, bodyrows.
5558        # Headrow, footrow and bodyrow data replaces same named attributes in
5559        # the table markup template. In order to ensure this data does not get
5560        # a second attribute substitution (which would interfere with any
5561        # already substituted inline passthroughs) unique placeholders are used
5562        # (the tab character does not appear elsewhere since it is expanded on
5563        # input) which are replaced after template attribute substitution.
5564        headrows = footrows = []
5565        bodyrows,table = self.split_rows(table)
5566        if table:
5567            headrows = bodyrows
5568            bodyrows,table = self.split_rows(table)
5569            if table:
5570                footrows,table = self.split_rows(table)
5571        if headrows:
5572            headrows = self.parse_rows(headrows, self.headrow, self.headdata)
5573            headrows = writer.newline.join(headrows)
5574            self.attributes['headrows'] = '\x07headrows\x07'
5575        if footrows:
5576            footrows = self.parse_rows(footrows, self.footrow, self.footdata)
5577            footrows = writer.newline.join(footrows)
5578            self.attributes['footrows'] = '\x07footrows\x07'
5579        bodyrows = self.parse_rows(bodyrows, self.bodyrow, self.bodydata)
5580        bodyrows = writer.newline.join(bodyrows)
5581        self.attributes['bodyrows'] = '\x07bodyrows\x07'
5582        table = subs_attrs(config.sections[self.template],self.attributes)
5583        table = writer.newline.join(table)
5584        # Before we finish replace the table head, foot and body place holders
5585        # with the real data.
5586        if headrows:
5587            table = table.replace('\x07headrows\x07', headrows, 1)
5588        if footrows:
5589            table = table.replace('\x07footrows\x07', footrows, 1)
5590        table = table.replace('\x07bodyrows\x07', bodyrows, 1)
5591        writer.write(table,trace='table')
5592        self.pop_blockname()
5593
5594class Tables_OLD(AbstractBlocks):
5595    """List of tables."""
5596    BLOCK_TYPE = Table_OLD
5597    PREFIX = 'old_tabledef-'
5598    def __init__(self):
5599        AbstractBlocks.__init__(self)
5600    def load(self,sections):
5601        AbstractBlocks.load(self,sections)
5602    def validate(self):
5603        # Does not call AbstractBlocks.validate().
5604        # Check we have a default table definition,
5605        for i in range(len(self.blocks)):
5606            if self.blocks[i].defname == 'old_tabledef-default':
5607                default = self.blocks[i]
5608                break
5609        else:
5610            raise EAsciiDoc,'missing section: [OLD_tabledef-default]'
5611        # Set default table defaults.
5612        if default.format is None: default.subs = 'fixed'
5613        # Propagate defaults to unspecified table parameters.
5614        for b in self.blocks:
5615            if b is not default:
5616                if b.fillchar is None: b.fillchar = default.fillchar
5617                if b.format is None: b.format = default.format
5618                if b.template is None: b.template = default.template
5619                if b.colspec is None: b.colspec = default.colspec
5620                if b.headrow is None: b.headrow = default.headrow
5621                if b.footrow is None: b.footrow = default.footrow
5622                if b.bodyrow is None: b.bodyrow = default.bodyrow
5623                if b.headdata is None: b.headdata = default.headdata
5624                if b.footdata is None: b.footdata = default.footdata
5625                if b.bodydata is None: b.bodydata = default.bodydata
5626        # Check all tables have valid fill character.
5627        for b in self.blocks:
5628            if not b.fillchar or len(b.fillchar) != 1:
5629                raise EAsciiDoc,'[%s] missing or illegal fillchar' % b.defname
5630        # Build combined tables delimiter patterns and assign defaults.
5631        delimiters = []
5632        for b in self.blocks:
5633            # Ruler is:
5634            #   (ColStop,(ColWidth,FillChar+)?)+, FillChar+, TableWidth?
5635            b.delimiter = r'^(' + Table_OLD.COL_STOP \
5636                + r'(\d*|' + re.escape(b.fillchar) + r'*)' \
5637                + r')+' \
5638                + re.escape(b.fillchar) + r'+' \
5639                + '([\d\.]*)$'
5640            delimiters.append(b.delimiter)
5641            if not b.headrow:
5642                b.headrow = b.bodyrow
5643            if not b.footrow:
5644                b.footrow = b.bodyrow
5645            if not b.headdata:
5646                b.headdata = b.bodydata
5647            if not b.footdata:
5648                b.footdata = b.bodydata
5649        self.delimiters = re_join(delimiters)
5650        # Check table definitions are valid.
5651        for b in self.blocks:
5652            b.validate()
5653            if config.verbose:
5654                if b.check_msg:
5655                    message.warning('[%s] table definition: %s' % (b.defname,b.check_msg))
5656
5657# End of deprecated old table classes.
5658#---------------------------------------------------------------------------
5659
5660#---------------------------------------------------------------------------
5661# filter and theme plugin commands.
5662#---------------------------------------------------------------------------
5663import shutil, zipfile
5664
5665def die(msg):
5666    message.stderr(msg)
5667    sys.exit(1)
5668
5669def extract_zip(zip_file, destdir):
5670    """
5671    Unzip Zip file to destination directory.
5672    Throws exception if error occurs.
5673    """
5674    zipo = zipfile.ZipFile(zip_file, 'r')
5675    try:
5676        for zi in zipo.infolist():
5677            outfile = zi.filename
5678            if not outfile.endswith('/'):
5679                d, outfile = os.path.split(outfile)
5680                directory = os.path.normpath(os.path.join(destdir, d))
5681                if not os.path.isdir(directory):
5682                    os.makedirs(directory)
5683                outfile = os.path.join(directory, outfile)
5684                perms = (zi.external_attr >> 16) & 0777
5685                message.verbose('extracting: %s' % outfile)
5686                flags = os.O_CREAT | os.O_WRONLY
5687                if sys.platform == 'win32':
5688                    flags |= os.O_BINARY
5689                if perms == 0:
5690                    # Zip files created under Windows do not include permissions.
5691                    fh = os.open(outfile, flags)
5692                else:
5693                    fh = os.open(outfile, flags, perms)
5694                try:
5695                    os.write(fh, zipo.read(zi.filename))
5696                finally:
5697                    os.close(fh)
5698    finally:
5699        zipo.close()
5700
5701def create_zip(zip_file, src, skip_hidden=False):
5702    """
5703    Create Zip file. If src is a directory archive all contained files and
5704    subdirectories, if src is a file archive the src file.
5705    Files and directories names starting with . are skipped
5706    if skip_hidden is True.
5707    Throws exception if error occurs.
5708    """
5709    zipo = zipfile.ZipFile(zip_file, 'w')
5710    try:
5711        if os.path.isfile(src):
5712            arcname = os.path.basename(src)
5713            message.verbose('archiving: %s' % arcname)
5714            zipo.write(src, arcname, zipfile.ZIP_DEFLATED)
5715        elif os.path.isdir(src):
5716            srcdir = os.path.abspath(src)
5717            if srcdir[-1] != os.path.sep:
5718                srcdir += os.path.sep
5719            for root, dirs, files in os.walk(srcdir):
5720                arcroot = os.path.abspath(root)[len(srcdir):]
5721                if skip_hidden:
5722                    for d in dirs[:]:
5723                        if d.startswith('.'):
5724                            message.verbose('skipping: %s' % os.path.join(arcroot, d))
5725                            del dirs[dirs.index(d)]
5726                for f in files:
5727                    filename = os.path.join(root,f)
5728                    arcname = os.path.join(arcroot, f)
5729                    if skip_hidden and f.startswith('.'):
5730                        message.verbose('skipping: %s' % arcname)
5731                        continue
5732                    message.verbose('archiving: %s' % arcname)
5733                    zipo.write(filename, arcname, zipfile.ZIP_DEFLATED)
5734        else:
5735            raise ValueError,'src must specify directory or file: %s' % src
5736    finally:
5737        zipo.close()
5738
5739class Plugin:
5740    """
5741    --filter and --theme option commands.
5742    """
5743    CMDS = ('install','remove','list','build')
5744
5745    type = None     # 'backend', 'filter' or 'theme'.
5746
5747    @staticmethod
5748    def get_dir():
5749        """
5750        Return plugins path (.asciidoc/filters or .asciidoc/themes) in user's
5751        home direcory or None if user home not defined.
5752        """
5753        result = userdir()
5754        if result:
5755            result = os.path.join(result, '.asciidoc', Plugin.type+'s')
5756        return result
5757
5758    @staticmethod
5759    def install(args):
5760        """
5761        Install plugin Zip file.
5762        args[0] is plugin zip file path.
5763        args[1] is optional destination plugins directory.
5764        """
5765        if len(args) not in (1,2):
5766            die('invalid number of arguments: --%s install %s'
5767                    % (Plugin.type, ' '.join(args)))
5768        zip_file = args[0]
5769        if not os.path.isfile(zip_file):
5770            die('file not found: %s' % zip_file)
5771        reo = re.match(r'^\w+',os.path.split(zip_file)[1])
5772        if not reo:
5773            die('file name does not start with legal %s name: %s'
5774                    % (Plugin.type, zip_file))
5775        plugin_name = reo.group()
5776        if len(args) == 2:
5777            plugins_dir = args[1]
5778            if not os.path.isdir(plugins_dir):
5779                die('directory not found: %s' % plugins_dir)
5780        else:
5781            plugins_dir = Plugin.get_dir()
5782            if not plugins_dir:
5783                die('user home directory is not defined')
5784        plugin_dir = os.path.join(plugins_dir, plugin_name)
5785        if os.path.exists(plugin_dir):
5786            die('%s is already installed: %s' % (Plugin.type, plugin_dir))
5787        try:
5788            os.makedirs(plugin_dir)
5789        except Exception,e:
5790            die('failed to create %s directory: %s' % (Plugin.type, str(e)))
5791        try:
5792            extract_zip(zip_file, plugin_dir)
5793        except Exception,e:
5794            if os.path.isdir(plugin_dir):
5795                shutil.rmtree(plugin_dir)
5796            die('failed to extract %s: %s' % (Plugin.type, str(e)))
5797
5798    @staticmethod
5799    def remove(args):
5800        """
5801        Delete plugin directory.
5802        args[0] is plugin name.
5803        args[1] is optional plugin directory (defaults to ~/.asciidoc/<plugin_name>).
5804        """
5805        if len(args) not in (1,2):
5806            die('invalid number of arguments: --%s remove %s'
5807                    % (Plugin.type, ' '.join(args)))
5808        plugin_name = args[0]
5809        if not re.match(r'^\w+$',plugin_name):
5810            die('illegal %s name: %s' % (Plugin.type, plugin_name))
5811        if len(args) == 2:
5812            d = args[1]
5813            if not os.path.isdir(d):
5814                die('directory not found: %s' % d)
5815        else:
5816            d = Plugin.get_dir()
5817            if not d:
5818                die('user directory is not defined')
5819        plugin_dir = os.path.join(d, plugin_name)
5820        if not os.path.isdir(plugin_dir):
5821            die('cannot find %s: %s' % (Plugin.type, plugin_dir))
5822        try:
5823            message.verbose('removing: %s' % plugin_dir)
5824            shutil.rmtree(plugin_dir)
5825        except Exception,e:
5826            die('failed to delete %s: %s' % (Plugin.type, str(e)))
5827
5828    @staticmethod
5829    def list(args):
5830        """
5831        List all plugin directories (global and local).
5832        """
5833        for d in [os.path.join(d, Plugin.type+'s') for d in config.get_load_dirs()]:
5834            if os.path.isdir(d):
5835                for f in os.walk(d).next()[1]:
5836                    message.stdout(os.path.join(d,f))
5837
5838    @staticmethod
5839    def build(args):
5840        """
5841        Create plugin Zip file.
5842        args[0] is Zip file name.
5843        args[1] is plugin directory.
5844        """
5845        if len(args) != 2:
5846            die('invalid number of arguments: --%s build %s'
5847                    % (Plugin.type, ' '.join(args)))
5848        zip_file = args[0]
5849        plugin_source = args[1]
5850        if not (os.path.isdir(plugin_source) or os.path.isfile(plugin_source)):
5851            die('plugin source not found: %s' % plugin_source)
5852        try:
5853            create_zip(zip_file, plugin_source, skip_hidden=True)
5854        except Exception,e:
5855            die('failed to create %s: %s' % (zip_file, str(e)))
5856
5857
5858#---------------------------------------------------------------------------
5859# Application code.
5860#---------------------------------------------------------------------------
5861# Constants
5862# ---------
5863APP_FILE = None             # This file's full path.
5864APP_DIR = None              # This file's directory.
5865USER_DIR = None             # ~/.asciidoc
5866# Global configuration files directory (set by Makefile build target).
5867CONF_DIR = '/etc/asciidoc'
5868HELP_FILE = 'help.conf'     # Default (English) help file.
5869
5870# Globals
5871# -------
5872document = Document()       # The document being processed.
5873config = Config()           # Configuration file reader.
5874reader = Reader()           # Input stream line reader.
5875writer = Writer()           # Output stream line writer.
5876message = Message()         # Message functions.
5877paragraphs = Paragraphs()   # Paragraph definitions.
5878lists = Lists()             # List definitions.
5879blocks = DelimitedBlocks()  # DelimitedBlock definitions.
5880tables_OLD = Tables_OLD()   # Table_OLD definitions.
5881tables = Tables()           # Table definitions.
5882macros = Macros()           # Macro definitions.
5883calloutmap = CalloutMap()   # Coordinates callouts and callout list.
5884trace = Trace()             # Implements trace attribute processing.
5885
5886### Used by asciidocapi.py ###
5887# List of message strings written to stderr.
5888messages = message.messages
5889
5890
5891def asciidoc(backend, doctype, confiles, infile, outfile, options):
5892    """Convert AsciiDoc document to DocBook document of type doctype
5893    The AsciiDoc document is read from file object src the translated
5894    DocBook file written to file object dst."""
5895    def load_conffiles(include=[], exclude=[]):
5896        # Load conf files specified on the command-line and by the conf-files attribute.
5897        files = document.attributes.get('conf-files','')
5898        files = [f.strip() for f in files.split('|') if f.strip()]
5899        files += confiles
5900        if files:
5901            for f in files:
5902                if os.path.isfile(f):
5903                    config.load_file(f, include=include, exclude=exclude)
5904                else:
5905                    raise EAsciiDoc,'missing configuration file: %s' % f
5906    try:
5907        document.attributes['python'] = sys.executable
5908        for f in config.filters:
5909            if not config.find_config_dir('filters', f):
5910                raise EAsciiDoc,'missing filter: %s' % f
5911        if doctype not in (None,'article','manpage','book'):
5912            raise EAsciiDoc,'illegal document type'
5913        # Set processing options.
5914        for o in options:
5915            if o == '-c': config.dumping = True
5916            if o == '-s': config.header_footer = False
5917            if o == '-v': config.verbose = True
5918        document.update_attributes()
5919        if '-e' not in options:
5920            # Load asciidoc.conf files in two passes: the first for attributes
5921            # the second for everything. This is so that locally set attributes
5922            # available are in the global asciidoc.conf
5923            if not config.load_from_dirs('asciidoc.conf',include=['attributes']):
5924                raise EAsciiDoc,'configuration file asciidoc.conf missing'
5925            load_conffiles(include=['attributes'])
5926            config.load_from_dirs('asciidoc.conf')
5927            if infile != '<stdin>':
5928                indir = os.path.dirname(infile)
5929                config.load_file('asciidoc.conf', indir,
5930                                include=['attributes','titles','specialchars'])
5931        else:
5932            load_conffiles(include=['attributes','titles','specialchars'])
5933        document.update_attributes()
5934        # Check the infile exists.
5935        if infile != '<stdin>':
5936            if not os.path.isfile(infile):
5937                raise EAsciiDoc,'input file %s missing' % infile
5938        document.infile = infile
5939        AttributeList.initialize()
5940        # Open input file and parse document header.
5941        reader.tabsize = config.tabsize
5942        reader.open(infile)
5943        has_header = document.parse_header(doctype,backend)
5944        # doctype is now finalized.
5945        document.attributes['doctype-'+document.doctype] = ''
5946        config.set_theme_attributes()
5947        # Load backend configuration files.
5948        if '-e' not in options:
5949            f = document.backend + '.conf'
5950            conffile = config.load_backend()
5951            if not conffile:
5952                raise EAsciiDoc,'missing backend conf file: %s' % f
5953            document.attributes['backend-confdir'] = os.path.dirname(conffile)
5954        # backend is now known.
5955        document.attributes['backend-'+document.backend] = ''
5956        document.attributes[document.backend+'-'+document.doctype] = ''
5957        doc_conffiles = []
5958        if '-e' not in options:
5959            # Load filters and language file.
5960            config.load_filters()
5961            document.load_lang()
5962            if infile != '<stdin>':
5963                # Load local conf files (files in the source file directory).
5964                config.load_file('asciidoc.conf', indir)
5965                config.load_backend([indir])
5966                config.load_filters([indir])
5967                # Load document specific configuration files.
5968                f = os.path.splitext(infile)[0]
5969                doc_conffiles = [
5970                        f for f in (f+'.conf', f+'-'+document.backend+'.conf')
5971                        if os.path.isfile(f) ]
5972                for f in doc_conffiles:
5973                    config.load_file(f)
5974        load_conffiles()
5975        # Build asciidoc-args attribute.
5976        args = ''
5977        # Add custom conf file arguments.
5978        for f in doc_conffiles + confiles:
5979            args += ' --conf-file "%s"' % f
5980        # Add command-line and header attributes.
5981        attrs = {}
5982        attrs.update(AttributeEntry.attributes)
5983        attrs.update(config.cmd_attrs)
5984        if 'title' in attrs:    # Don't pass the header title.
5985            del attrs['title']
5986        for k,v in attrs.items():
5987            if v:
5988                args += ' --attribute "%s=%s"' % (k,v)
5989            else:
5990                args += ' --attribute "%s"' % k
5991        document.attributes['asciidoc-args'] = args
5992        # Build outfile name.
5993        if outfile is None:
5994            outfile = os.path.splitext(infile)[0] + '.' + document.backend
5995            if config.outfilesuffix:
5996                # Change file extension.
5997                outfile = os.path.splitext(outfile)[0] + config.outfilesuffix
5998        document.outfile = outfile
5999        # Document header attributes override conf file attributes.
6000        document.attributes.update(AttributeEntry.attributes)
6001        document.update_attributes()
6002        # Configuration is fully loaded.
6003        config.expand_all_templates()
6004        # Check configuration for consistency.
6005        config.validate()
6006        # Initialize top level block name.
6007        if document.attributes.get('blockname'):
6008            AbstractBlock.blocknames.append(document.attributes['blockname'])
6009        paragraphs.initialize()
6010        lists.initialize()
6011        if config.dumping:
6012            config.dump()
6013        else:
6014            writer.newline = config.newline
6015            try:
6016                writer.open(outfile, reader.bom)
6017                try:
6018                    document.translate(has_header) # Generate the output.
6019                finally:
6020                    writer.close()
6021            finally:
6022                reader.closefile()
6023    except KeyboardInterrupt:
6024        raise
6025    except Exception,e:
6026        # Cleanup.
6027        if outfile and outfile != '<stdout>' and os.path.isfile(outfile):
6028            os.unlink(outfile)
6029        # Build and print error description.
6030        msg = 'FAILED: '
6031        if reader.cursor:
6032            msg = message.format('', msg)
6033        if isinstance(e, EAsciiDoc):
6034            message.stderr('%s%s' % (msg,str(e)))
6035        else:
6036            if __name__ == '__main__':
6037                message.stderr(msg+'unexpected error:')
6038                message.stderr('-'*60)
6039                traceback.print_exc(file=sys.stderr)
6040                message.stderr('-'*60)
6041            else:
6042                message.stderr('%sunexpected error: %s' % (msg,str(e)))
6043        sys.exit(1)
6044
6045def usage(msg=''):
6046    if msg:
6047        message.stderr(msg)
6048    show_help('default', sys.stderr)
6049
6050def show_help(topic, f=None):
6051    """Print help topic to file object f."""
6052    if f is None:
6053        f = sys.stdout
6054    # Select help file.
6055    lang = config.cmd_attrs.get('lang')
6056    if lang and lang != 'en':
6057        help_file = 'help-' + lang + '.conf'
6058    else:
6059        help_file = HELP_FILE
6060    # Print [topic] section from help file.
6061    config.load_from_dirs(help_file)
6062    if len(config.sections) == 0:
6063        # Default to English if specified language help files not found.
6064        help_file = HELP_FILE
6065        config.load_from_dirs(help_file)
6066    if len(config.sections) == 0:
6067        message.stderr('no help topics found')
6068        sys.exit(1)
6069    n = 0
6070    for k in config.sections:
6071        if re.match(re.escape(topic), k):
6072            n += 1
6073            lines = config.sections[k]
6074    if n == 0:
6075        if topic != 'topics':
6076            message.stderr('help topic not found: [%s] in %s' % (topic, help_file))
6077        message.stderr('available help topics: %s' % ', '.join(config.sections.keys()))
6078        sys.exit(1)
6079    elif n > 1:
6080        message.stderr('ambiguous help topic: %s' % topic)
6081    else:
6082        for line in lines:
6083            print >>f, line
6084
6085### Used by asciidocapi.py ###
6086def execute(cmd,opts,args):
6087    """
6088    Execute asciidoc with command-line options and arguments.
6089    cmd is asciidoc command or asciidoc.py path.
6090    opts and args conform to values returned by getopt.getopt().
6091    Raises SystemExit if an error occurs.
6092
6093    Doctests:
6094
6095    1. Check execution:
6096
6097       >>> import StringIO
6098       >>> infile = StringIO.StringIO('Hello *{author}*')
6099       >>> outfile = StringIO.StringIO()
6100       >>> opts = []
6101       >>> opts.append(('--backend','html4'))
6102       >>> opts.append(('--no-header-footer',None))
6103       >>> opts.append(('--attribute','author=Joe Bloggs'))
6104       >>> opts.append(('--out-file',outfile))
6105       >>> execute(__file__, opts, [infile])
6106       >>> print outfile.getvalue()
6107       <p>Hello <strong>Joe Bloggs</strong></p>
6108
6109       >>>
6110
6111    """
6112    config.init(cmd)
6113    if len(args) > 1:
6114        usage('Too many arguments')
6115        sys.exit(1)
6116    backend = None
6117    doctype = None
6118    confiles = []
6119    outfile = None
6120    options = []
6121    help_option = False
6122    for o,v in opts:
6123        if o in ('--help','-h'):
6124            help_option = True
6125        #DEPRECATED: --unsafe option.
6126        if o == '--unsafe':
6127            document.safe = False
6128        if o == '--safe':
6129            document.safe = True
6130        if o == '--version':
6131            print('asciidoc %s' % VERSION)
6132            sys.exit(0)
6133        if o in ('-b','--backend'):
6134            backend = v
6135        if o in ('-c','--dump-conf'):
6136            options.append('-c')
6137        if o in ('-d','--doctype'):
6138            doctype = v
6139        if o in ('-e','--no-conf'):
6140            options.append('-e')
6141        if o in ('-f','--conf-file'):
6142            confiles.append(v)
6143        if o == '--filter':
6144            config.filters.append(v)
6145        if o in ('-n','--section-numbers'):
6146            o = '-a'
6147            v = 'numbered'
6148        if o == '--theme':
6149            o = '-a'
6150            v = 'theme='+v
6151        if o in ('-a','--attribute'):
6152            e = parse_entry(v, allow_name_only=True)
6153            if not e:
6154                usage('Illegal -a option: %s' % v)
6155                sys.exit(1)
6156            k,v = e
6157            # A @ suffix denotes don't override existing document attributes.
6158            if v and v[-1] == '@':
6159                document.attributes[k] = v[:-1]
6160            else:
6161                config.cmd_attrs[k] = v
6162        if o in ('-o','--out-file'):
6163            outfile = v
6164        if o in ('-s','--no-header-footer'):
6165            options.append('-s')
6166        if o in ('-v','--verbose'):
6167            options.append('-v')
6168    if help_option:
6169        if len(args) == 0:
6170            show_help('default')
6171        else:
6172            show_help(args[-1])
6173        sys.exit(0)
6174    if len(args) == 0 and len(opts) == 0:
6175        usage()
6176        sys.exit(0)
6177    if len(args) == 0:
6178        usage('No source file specified')
6179        sys.exit(1)
6180    stdin,stdout = sys.stdin,sys.stdout
6181    try:
6182        infile = args[0]
6183        if infile == '-':
6184            infile = '<stdin>'
6185        elif isinstance(infile, str):
6186            infile = os.path.abspath(infile)
6187        else:   # Input file is file object from API call.
6188            sys.stdin = infile
6189            infile = '<stdin>'
6190        if outfile == '-':
6191            outfile = '<stdout>'
6192        elif isinstance(outfile, str):
6193            outfile = os.path.abspath(outfile)
6194        elif outfile is None:
6195            if infile == '<stdin>':
6196                outfile = '<stdout>'
6197        else:   # Output file is file object from API call.
6198            sys.stdout = outfile
6199            outfile = '<stdout>'
6200        # Do the work.
6201        asciidoc(backend, doctype, confiles, infile, outfile, options)
6202        if document.has_errors:
6203            sys.exit(1)
6204    finally:
6205        sys.stdin,sys.stdout = stdin,stdout
6206
6207if __name__ == '__main__':
6208    # Process command line options.
6209    import getopt
6210    try:
6211        #DEPRECATED: --unsafe option.
6212        opts,args = getopt.getopt(sys.argv[1:],
6213            'a:b:cd:ef:hno:svw:',
6214            ['attribute=','backend=','conf-file=','doctype=','dump-conf',
6215            'help','no-conf','no-header-footer','out-file=',
6216            'section-numbers','verbose','version','safe','unsafe',
6217            'doctest','filter=','theme='])
6218    except getopt.GetoptError:
6219        message.stderr('illegal command options')
6220        sys.exit(1)
6221    opt_names = [opt[0] for opt in opts]
6222    if '--doctest' in opt_names:
6223        # Run module doctests.
6224        import doctest
6225        options = doctest.NORMALIZE_WHITESPACE + doctest.ELLIPSIS
6226        failures,tries = doctest.testmod(optionflags=options)
6227        if failures == 0:
6228            message.stderr('All doctests passed')
6229            sys.exit(0)
6230        else:
6231            sys.exit(1)
6232    # Look for plugin management commands.
6233    count = 0
6234    for o,v in opts:
6235        if o in ('-b','--backend','--filter','--theme'):
6236            if o == '-b':
6237                o = '--backend'
6238            plugin = o[2:]
6239            cmd = v
6240            if cmd not in Plugin.CMDS:
6241                continue
6242            count += 1
6243    if count > 1:
6244        die('--backend, --filter and --theme options are mutually exclusive')
6245    if count == 1:
6246        # Execute plugin management commands.
6247        if not cmd:
6248            die('missing --%s command' % plugin)
6249        if cmd not in Plugin.CMDS:
6250            die('illegal --%s command: %s' % (plugin, cmd))
6251        Plugin.type = plugin
6252        config.init(sys.argv[0])
6253        config.verbose = bool(set(['-v','--verbose']) & set(opt_names))
6254        getattr(Plugin,cmd)(args)
6255    else:
6256        # Execute asciidoc.
6257        try:
6258            execute(sys.argv[0],opts,args)
6259        except KeyboardInterrupt:
6260            sys.exit(1)
Note: See TracBrowser for help on using the repository browser.