diff options
Diffstat (limited to 'source-builder/sb/asciidoc/asciidoc.py')
-rwxr-xr-x | source-builder/sb/asciidoc/asciidoc.py | 6260 |
1 files changed, 0 insertions, 6260 deletions
diff --git a/source-builder/sb/asciidoc/asciidoc.py b/source-builder/sb/asciidoc/asciidoc.py deleted file mode 100755 index 8c68895..0000000 --- a/source-builder/sb/asciidoc/asciidoc.py +++ /dev/null @@ -1,6260 +0,0 @@ -#!/usr/bin/env python -""" -asciidoc - converts an AsciiDoc text file to HTML or DocBook - -Copyright (C) 2002-2010 Stuart Rackham. Free use of this software is granted -under the terms of the GNU General Public License (GPL). -""" - -import sys, os, re, time, traceback, tempfile, subprocess, codecs, locale, unicodedata, copy - -### Used by asciidocapi.py ### -VERSION = '8.6.8' # See CHANGLOG file for version history. - -MIN_PYTHON_VERSION = '2.4' # Require this version of Python or better. - -#--------------------------------------------------------------------------- -# Program constants. -#--------------------------------------------------------------------------- -DEFAULT_BACKEND = 'html' -DEFAULT_DOCTYPE = 'article' -# Allowed substitution options for List, Paragraph and DelimitedBlock -# definition subs entry. -SUBS_OPTIONS = ('specialcharacters','quotes','specialwords', - 'replacements', 'attributes','macros','callouts','normal','verbatim', - 'none','replacements2','replacements3') -# Default value for unspecified subs and presubs configuration file entries. -SUBS_NORMAL = ('specialcharacters','quotes','attributes', - 'specialwords','replacements','macros','replacements2') -SUBS_VERBATIM = ('specialcharacters','callouts') - -NAME_RE = r'(?u)[^\W\d][-\w]*' # Valid section or attribute name. -OR, AND = ',', '+' # Attribute list separators. - - -#--------------------------------------------------------------------------- -# Utility functions and classes. -#--------------------------------------------------------------------------- - -class EAsciiDoc(Exception): pass - -class OrderedDict(dict): - """ - Dictionary ordered by insertion order. - Python Cookbook: Ordered Dictionary, Submitter: David Benjamin. - http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/107747 - """ - def __init__(self, d=None, **kwargs): - self._keys = [] - if d is None: d = kwargs - dict.__init__(self, d) - def __delitem__(self, key): - dict.__delitem__(self, key) - self._keys.remove(key) - def __setitem__(self, key, item): - dict.__setitem__(self, key, item) - if key not in self._keys: self._keys.append(key) - def clear(self): - dict.clear(self) - self._keys = [] - def copy(self): - d = dict.copy(self) - d._keys = self._keys[:] - return d - def items(self): - return zip(self._keys, self.values()) - def keys(self): - return self._keys - def popitem(self): - try: - key = self._keys[-1] - except IndexError: - raise KeyError('dictionary is empty') - val = self[key] - del self[key] - return (key, val) - def setdefault(self, key, failobj = None): - dict.setdefault(self, key, failobj) - if key not in self._keys: self._keys.append(key) - def update(self, d=None, **kwargs): - if d is None: - d = kwargs - dict.update(self, d) - for key in d.keys(): - if key not in self._keys: self._keys.append(key) - def values(self): - return map(self.get, self._keys) - -class AttrDict(dict): - """ - Like a dictionary except values can be accessed as attributes i.e. obj.foo - can be used in addition to obj['foo']. - If an item is not present None is returned. - """ - def __getattr__(self, key): - try: return self[key] - except KeyError: return None - def __setattr__(self, key, value): - self[key] = value - def __delattr__(self, key): - try: del self[key] - except KeyError, k: raise AttributeError, k - def __repr__(self): - return '<AttrDict ' + dict.__repr__(self) + '>' - def __getstate__(self): - return dict(self) - def __setstate__(self,value): - for k,v in value.items(): self[k]=v - -class InsensitiveDict(dict): - """ - Like a dictionary except key access is case insensitive. - Keys are stored in lower case. - """ - def __getitem__(self, key): - return dict.__getitem__(self, key.lower()) - def __setitem__(self, key, value): - dict.__setitem__(self, key.lower(), value) - def has_key(self, key): - return dict.has_key(self,key.lower()) - def get(self, key, default=None): - return dict.get(self, key.lower(), default) - def update(self, dict): - for k,v in dict.items(): - self[k] = v - def setdefault(self, key, default = None): - return dict.setdefault(self, key.lower(), default) - - -class Trace(object): - """ - Used in conjunction with the 'trace' attribute to generate diagnostic - output. There is a single global instance of this class named trace. - """ - SUBS_NAMES = ('specialcharacters','quotes','specialwords', - 'replacements', 'attributes','macros','callouts', - 'replacements2','replacements3') - def __init__(self): - self.name_re = '' # Regexp pattern to match trace names. - self.linenos = True - self.offset = 0 - def __call__(self, name, before, after=None): - """ - Print trace message if tracing is on and the trace 'name' matches the - document 'trace' attribute (treated as a regexp). - 'before' is the source text before substitution; 'after' text is the - source text after substitutuion. - The 'before' and 'after' messages are only printed if they differ. - """ - name_re = document.attributes.get('trace') - if name_re == 'subs': # Alias for all the inline substitutions. - name_re = '|'.join(self.SUBS_NAMES) - self.name_re = name_re - if self.name_re is not None: - msg = message.format(name, 'TRACE: ', self.linenos, offset=self.offset) - if before != after and re.match(self.name_re,name): - if is_array(before): - before = '\n'.join(before) - if after is None: - msg += '\n%s\n' % before - else: - if is_array(after): - after = '\n'.join(after) - msg += '\n<<<\n%s\n>>>\n%s\n' % (before,after) - message.stderr(msg) - -class Message: - """ - Message functions. - """ - PROG = os.path.basename(os.path.splitext(__file__)[0]) - - def __init__(self): - # Set to True or False to globally override line numbers method - # argument. Has no effect when set to None. - self.linenos = None - self.messages = [] - self.prev_msg = '' - - def stdout(self,msg): - print msg - - def stderr(self,msg=''): - if msg == self.prev_msg: # Suppress repeated messages. - return - self.messages.append(msg) - if __name__ == '__main__': - sys.stderr.write('%s: %s%s' % (self.PROG, msg, os.linesep)) - self.prev_msg = msg - - def verbose(self, msg,linenos=True): - if config.verbose: - msg = self.format(msg,linenos=linenos) - self.stderr(msg) - - def warning(self, msg,linenos=True,offset=0): - msg = self.format(msg,'WARNING: ',linenos,offset=offset) - document.has_warnings = True - self.stderr(msg) - - def deprecated(self, msg, linenos=True): - msg = self.format(msg, 'DEPRECATED: ', linenos) - self.stderr(msg) - - def format(self, msg, prefix='', linenos=True, cursor=None, offset=0): - """Return formatted message string.""" - if self.linenos is not False and ((linenos or self.linenos) and reader.cursor): - if cursor is None: - cursor = reader.cursor - prefix += '%s: line %d: ' % (os.path.basename(cursor[0]),cursor[1]+offset) - return prefix + msg - - def error(self, msg, cursor=None, halt=False): - """ - Report fatal error. - If halt=True raise EAsciiDoc exception. - If halt=False don't exit application, continue in the hope of reporting - all fatal errors finishing with a non-zero exit code. - """ - if halt: - raise EAsciiDoc, self.format(msg,linenos=False,cursor=cursor) - else: - msg = self.format(msg,'ERROR: ',cursor=cursor) - self.stderr(msg) - document.has_errors = True - - def unsafe(self, msg): - self.error('unsafe: '+msg) - - -def userdir(): - """ - Return user's home directory or None if it is not defined. - """ - result = os.path.expanduser('~') - if result == '~': - result = None - return result - -def localapp(): - """ - Return True if we are not executing the system wide version - i.e. the configuration is in the executable's directory. - """ - return os.path.isfile(os.path.join(APP_DIR, 'asciidoc.conf')) - -def file_in(fname, directory): - """Return True if file fname resides inside directory.""" - assert os.path.isfile(fname) - # Empty directory (not to be confused with None) is the current directory. - if directory == '': - directory = os.getcwd() - else: - assert os.path.isdir(directory) - directory = os.path.realpath(directory) - fname = os.path.realpath(fname) - return os.path.commonprefix((directory, fname)) == directory - -def safe(): - return document.safe - -def is_safe_file(fname, directory=None): - # A safe file must reside in 'directory' (defaults to the source - # file directory). - if directory is None: - if document.infile == '<stdin>': - return not safe() - directory = os.path.dirname(document.infile) - elif directory == '': - directory = '.' - return ( - not safe() - or file_in(fname, directory) - or file_in(fname, APP_DIR) - or file_in(fname, CONF_DIR) - ) - -def safe_filename(fname, parentdir): - """ - Return file name which must reside in the parent file directory. - Return None if file is not safe. - """ - if not os.path.isabs(fname): - # Include files are relative to parent document - # directory. - fname = os.path.normpath(os.path.join(parentdir,fname)) - if not is_safe_file(fname, parentdir): - message.unsafe('include file: %s' % fname) - return None - return fname - -def assign(dst,src): - """Assign all attributes from 'src' object to 'dst' object.""" - for a,v in src.__dict__.items(): - setattr(dst,a,v) - -def strip_quotes(s): - """Trim white space and, if necessary, quote characters from s.""" - s = s.strip() - # Strip quotation mark characters from quoted strings. - if len(s) >= 3 and s[0] == '"' and s[-1] == '"': - s = s[1:-1] - return s - -def is_re(s): - """Return True if s is a valid regular expression else return False.""" - try: re.compile(s) - except: return False - else: return True - -def re_join(relist): - """Join list of regular expressions re1,re2,... to single regular - expression (re1)|(re2)|...""" - if len(relist) == 0: - return None - result = [] - # Delete named groups to avoid ambiguity. - for s in relist: - result.append(re.sub(r'\?P<\S+?>','',s)) - result = ')|('.join(result) - result = '('+result+')' - return result - -def lstrip_list(s): - """ - Return list with empty items from start of list removed. - """ - for i in range(len(s)): - if s[i]: break - else: - return [] - return s[i:] - -def rstrip_list(s): - """ - Return list with empty items from end of list removed. - """ - for i in range(len(s)-1,-1,-1): - if s[i]: break - else: - return [] - return s[:i+1] - -def strip_list(s): - """ - Return list with empty items from start and end of list removed. - """ - s = lstrip_list(s) - s = rstrip_list(s) - return s - -def is_array(obj): - """ - Return True if object is list or tuple type. - """ - return isinstance(obj,list) or isinstance(obj,tuple) - -def dovetail(lines1, lines2): - """ - Append list or tuple of strings 'lines2' to list 'lines1'. Join the last - non-blank item in 'lines1' with the first non-blank item in 'lines2' into a - single string. - """ - assert is_array(lines1) - assert is_array(lines2) - lines1 = strip_list(lines1) - lines2 = strip_list(lines2) - if not lines1 or not lines2: - return list(lines1) + list(lines2) - result = list(lines1[:-1]) - result.append(lines1[-1] + lines2[0]) - result += list(lines2[1:]) - return result - -def dovetail_tags(stag,content,etag): - """Merge the end tag with the first content line and the last - content line with the end tag. This ensures verbatim elements don't - include extraneous opening and closing line breaks.""" - return dovetail(dovetail(stag,content), etag) - -# The following functions are so we don't have to use the dangerous -# built-in eval() function. -if float(sys.version[:3]) >= 2.6 or sys.platform[:4] == 'java': - # Use AST module if CPython >= 2.6 or Jython. - import ast - from ast import literal_eval - - def get_args(val): - d = {} - args = ast.parse("d(" + val + ")", mode='eval').body.args - i = 1 - for arg in args: - if isinstance(arg, ast.Name): - d[str(i)] = literal_eval(arg.id) - else: - d[str(i)] = literal_eval(arg) - i += 1 - return d - - def get_kwargs(val): - d = {} - args = ast.parse("d(" + val + ")", mode='eval').body.keywords - for arg in args: - d[arg.arg] = literal_eval(arg.value) - return d - - def parse_to_list(val): - values = ast.parse("[" + val + "]", mode='eval').body.elts - return [literal_eval(v) for v in values] - -else: # Use deprecated CPython compiler module. - import compiler - from compiler.ast import Const, Dict, Expression, Name, Tuple, UnarySub, Keyword - - # Code from: - # http://mail.python.org/pipermail/python-list/2009-September/1219992.html - # Modified to use compiler.ast.List as this module has a List - def literal_eval(node_or_string): - """ - Safely evaluate an expression node or a string containing a Python - expression. The string or node provided may only consist of the - following Python literal structures: strings, numbers, tuples, - lists, dicts, booleans, and None. - """ - _safe_names = {'None': None, 'True': True, 'False': False} - if isinstance(node_or_string, basestring): - node_or_string = compiler.parse(node_or_string, mode='eval') - if isinstance(node_or_string, Expression): - node_or_string = node_or_string.node - def _convert(node): - if isinstance(node, Const) and isinstance(node.value, - (basestring, int, float, long, complex)): - return node.value - elif isinstance(node, Tuple): - return tuple(map(_convert, node.nodes)) - elif isinstance(node, compiler.ast.List): - return list(map(_convert, node.nodes)) - elif isinstance(node, Dict): - return dict((_convert(k), _convert(v)) for k, v - in node.items) - elif isinstance(node, Name): - if node.name in _safe_names: - return _safe_names[node.name] - elif isinstance(node, UnarySub): - return -_convert(node.expr) - raise ValueError('malformed string') - return _convert(node_or_string) - - def get_args(val): - d = {} - args = compiler.parse("d(" + val + ")", mode='eval').node.args - i = 1 - for arg in args: - if isinstance(arg, Keyword): - break - d[str(i)] = literal_eval(arg) - i = i + 1 - return d - - def get_kwargs(val): - d = {} - args = compiler.parse("d(" + val + ")", mode='eval').node.args - i = 0 - for arg in args: - if isinstance(arg, Keyword): - break - i += 1 - args = args[i:] - for arg in args: - d[str(arg.name)] = literal_eval(arg.expr) - return d - - def parse_to_list(val): - values = compiler.parse("[" + val + "]", mode='eval').node.asList() - return [literal_eval(v) for v in values] - -def parse_attributes(attrs,dict): - """Update a dictionary with name/value attributes from the attrs string. - The attrs string is a comma separated list of values and keyword name=value - pairs. Values must preceed keywords and are named '1','2'... The entire - attributes list is named '0'. If keywords are specified string values must - be quoted. Examples: - - attrs: '' - dict: {} - - attrs: 'hello,world' - dict: {'2': 'world', '0': 'hello,world', '1': 'hello'} - - attrs: '"hello", planet="earth"' - dict: {'planet': 'earth', '0': '"hello",planet="earth"', '1': 'hello'} - """ - def f(*args,**keywords): - # Name and add aguments '1','2'... to keywords. - for i in range(len(args)): - if not str(i+1) in keywords: - keywords[str(i+1)] = args[i] - return keywords - - if not attrs: - return - dict['0'] = attrs - # Replace line separators with spaces so line spanning works. - s = re.sub(r'\s', ' ', attrs) - d = {} - try: - d.update(get_args(s)) - d.update(get_kwargs(s)) - for v in d.values(): - if not (isinstance(v,str) or isinstance(v,int) or isinstance(v,float) or v is None): - raise Exception - except Exception: - s = s.replace('"','\\"') - s = s.split(',') - s = map(lambda x: '"' + x.strip() + '"', s) - s = ','.join(s) - try: - d = {} - d.update(get_args(s)) - d.update(get_kwargs(s)) - except Exception: - return # If there's a syntax error leave with {0}=attrs. - for k in d.keys(): # Drop any empty positional arguments. - if d[k] == '': del d[k] - dict.update(d) - assert len(d) > 0 - -def parse_named_attributes(s,attrs): - """Update a attrs dictionary with name="value" attributes from the s string. - Returns False if invalid syntax. - Example: - attrs: 'star="sun",planet="earth"' - dict: {'planet':'earth', 'star':'sun'} - """ - def f(**keywords): return keywords - - try: - d = {} - d = get_kwargs(s) - attrs.update(d) - return True - except Exception: - return False - -def parse_list(s): - """Parse comma separated string of Python literals. Return a tuple of of - parsed values.""" - try: - result = tuple(parse_to_list(s)) - except Exception: - raise EAsciiDoc,'malformed list: '+s - return result - -def parse_options(options,allowed,errmsg): - """Parse comma separated string of unquoted option names and return as a - tuple of valid options. 'allowed' is a list of allowed option values. - If allowed=() then all legitimate names are allowed. - 'errmsg' is an error message prefix if an illegal option error is thrown.""" - result = [] - if options: - for s in re.split(r'\s*,\s*',options): - if (allowed and s not in allowed) or not is_name(s): - raise EAsciiDoc,'%s: %s' % (errmsg,s) - result.append(s) - return tuple(result) - -def symbolize(s): - """Drop non-symbol characters and convert to lowercase.""" - return re.sub(r'(?u)[^\w\-_]', '', s).lower() - -def is_name(s): - """Return True if s is valid attribute, macro or tag name - (starts with alpha containing alphanumeric and dashes only).""" - return re.match(r'^'+NAME_RE+r'$',s) is not None - -def subs_quotes(text): - """Quoted text is marked up and the resulting text is - returned.""" - keys = config.quotes.keys() - for q in keys: - i = q.find('|') - if i != -1 and q != '|' and q != '||': - lq = q[:i] # Left quote. - rq = q[i+1:] # Right quote. - else: - lq = rq = q - tag = config.quotes[q] - if not tag: continue - # Unconstrained quotes prefix the tag name with a hash. - if tag[0] == '#': - tag = tag[1:] - # Unconstrained quotes can appear anywhere. - reo = re.compile(r'(?msu)(^|.)(\[(?P<attrlist>[^[\]]+?)\])?' \ - + r'(?:' + re.escape(lq) + r')' \ - + r'(?P<content>.+?)(?:'+re.escape(rq)+r')') - else: - # The text within constrained quotes must be bounded by white space. - # Non-word (\W) characters are allowed at boundaries to accomodate - # enveloping quotes and punctuation e.g. a='x', ('x'), 'x', ['x']. - reo = re.compile(r'(?msu)(^|[^\w;:}])(\[(?P<attrlist>[^[\]]+?)\])?' \ - + r'(?:' + re.escape(lq) + r')' \ - + r'(?P<content>\S|\S.*?\S)(?:'+re.escape(rq)+r')(?=\W|$)') - pos = 0 - while True: - mo = reo.search(text,pos) - if not mo: break - if text[mo.start()] == '\\': - # Delete leading backslash. - text = text[:mo.start()] + text[mo.start()+1:] - # Skip past start of match. - pos = mo.start() + 1 - else: - attrlist = {} - parse_attributes(mo.group('attrlist'), attrlist) - stag,etag = config.tag(tag, attrlist) - s = mo.group(1) + stag + mo.group('content') + etag - text = text[:mo.start()] + s + text[mo.end():] - pos = mo.start() + len(s) - return text - -def subs_tag(tag,dict={}): - """Perform attribute substitution and split tag string returning start, end - tag tuple (c.f. Config.tag()).""" - if not tag: - return [None,None] - s = subs_attrs(tag,dict) - if not s: - message.warning('tag \'%s\' dropped: contains undefined attribute' % tag) - return [None,None] - result = s.split('|') - if len(result) == 1: - return result+[None] - elif len(result) == 2: - return result - else: - raise EAsciiDoc,'malformed tag: %s' % tag - -def parse_entry(entry, dict=None, unquote=False, unique_values=False, - allow_name_only=False, escape_delimiter=True): - """Parse name=value entry to dictionary 'dict'. Return tuple (name,value) - or None if illegal entry. - If name= then value is set to ''. - If name and allow_name_only=True then value is set to ''. - If name! and allow_name_only=True then value is set to None. - Leading and trailing white space is striped from 'name' and 'value'. - 'name' can contain any printable characters. - If the '=' delimiter character is allowed in the 'name' then - it must be escaped with a backslash and escape_delimiter must be True. - If 'unquote' is True leading and trailing double-quotes are stripped from - 'name' and 'value'. - If unique_values' is True then dictionary entries with the same value are - removed before the parsed entry is added.""" - if escape_delimiter: - mo = re.search(r'(?:[^\\](=))',entry) - else: - mo = re.search(r'(=)',entry) - if mo: # name=value entry. - if mo.group(1): - name = entry[:mo.start(1)] - if escape_delimiter: - name = name.replace(r'\=','=') # Unescape \= in name. - value = entry[mo.end(1):] - elif allow_name_only and entry: # name or name! entry. - name = entry - if name[-1] == '!': - name = name[:-1] - value = None - else: - value = '' - else: - return None - if unquote: - name = strip_quotes(name) - if value is not None: - value = strip_quotes(value) - else: - name = name.strip() - if value is not None: - value = value.strip() - if not name: - return None - if dict is not None: - if unique_values: - for k,v in dict.items(): - if v == value: del dict[k] - dict[name] = value - return name,value - -def parse_entries(entries, dict, unquote=False, unique_values=False, - allow_name_only=False,escape_delimiter=True): - """Parse name=value entries from from lines of text in 'entries' into - dictionary 'dict'. Blank lines are skipped.""" - entries = config.expand_templates(entries) - for entry in entries: - if entry and not parse_entry(entry, dict, unquote, unique_values, - allow_name_only, escape_delimiter): - raise EAsciiDoc,'malformed section entry: %s' % entry - -def dump_section(name,dict,f=sys.stdout): - """Write parameters in 'dict' as in configuration file section format with - section 'name'.""" - f.write('[%s]%s' % (name,writer.newline)) - for k,v in dict.items(): - k = str(k) - k = k.replace('=',r'\=') # Escape = in name. - # Quote if necessary. - if len(k) != len(k.strip()): - k = '"'+k+'"' - if v and len(v) != len(v.strip()): - v = '"'+v+'"' - if v is None: - # Don't dump undefined attributes. - continue - else: - s = k+'='+v - if s[0] == '#': - s = '\\' + s # Escape so not treated as comment lines. - f.write('%s%s' % (s,writer.newline)) - f.write(writer.newline) - -def update_attrs(attrs,dict): - """Update 'attrs' dictionary with parsed attributes in dictionary 'dict'.""" - for k,v in dict.items(): - if not is_name(k): - raise EAsciiDoc,'illegal attribute name: %s' % k - attrs[k] = v - -def is_attr_defined(attrs,dic): - """ - Check if the sequence of attributes is defined in dictionary 'dic'. - Valid 'attrs' sequence syntax: - <attr> Return True if single attrbiute is defined. - <attr1>,<attr2>,... Return True if one or more attributes are defined. - <attr1>+<attr2>+... Return True if all the attributes are defined. - """ - if OR in attrs: - for a in attrs.split(OR): - if dic.get(a.strip()) is not None: - return True - else: return False - elif AND in attrs: - for a in attrs.split(AND): - if dic.get(a.strip()) is None: - return False - else: return True - else: - return dic.get(attrs.strip()) is not None - -def filter_lines(filter_cmd, lines, attrs={}): - """ - Run 'lines' through the 'filter_cmd' shell command and return the result. - The 'attrs' dictionary contains additional filter attributes. - """ - def findfilter(name,dir,filter): - """Find filter file 'fname' with style name 'name' in directory - 'dir'. Return found file path or None if not found.""" - if name: - result = os.path.join(dir,'filters',name,filter) - if os.path.isfile(result): - return result - result = os.path.join(dir,'filters',filter) - if os.path.isfile(result): - return result - return None - - # Return input lines if there's not filter. - if not filter_cmd or not filter_cmd.strip(): - return lines - # Perform attributes substitution on the filter command. - s = subs_attrs(filter_cmd, attrs) - if not s: - message.error('undefined filter attribute in command: %s' % filter_cmd) - return [] - filter_cmd = s.strip() - # Parse for quoted and unquoted command and command tail. - # Double quoted. - mo = re.match(r'^"(?P<cmd>[^"]+)"(?P<tail>.*)$', filter_cmd) - if not mo: - # Single quoted. - mo = re.match(r"^'(?P<cmd>[^']+)'(?P<tail>.*)$", filter_cmd) - if not mo: - # Unquoted catch all. - mo = re.match(r'^(?P<cmd>\S+)(?P<tail>.*)$', filter_cmd) - cmd = mo.group('cmd').strip() - found = None - if not os.path.dirname(cmd): - # Filter command has no directory path so search filter directories. - filtername = attrs.get('style') - d = document.attributes.get('docdir') - if d: - found = findfilter(filtername, d, cmd) - if not found: - if USER_DIR: - found = findfilter(filtername, USER_DIR, cmd) - if not found: - if localapp(): - found = findfilter(filtername, APP_DIR, cmd) - else: - found = findfilter(filtername, CONF_DIR, cmd) - else: - if os.path.isfile(cmd): - found = cmd - else: - message.warning('filter not found: %s' % cmd) - if found: - filter_cmd = '"' + found + '"' + mo.group('tail') - if found: - if cmd.endswith('.py'): - filter_cmd = '"%s" %s' % (document.attributes['python'], - filter_cmd) - elif cmd.endswith('.rb'): - filter_cmd = 'ruby ' + filter_cmd - - message.verbose('filtering: ' + filter_cmd) - if os.name == 'nt': - # Remove redundant quoting -- this is not just - # cosmetic, unnecessary quoting appears to cause - # command line truncation. - filter_cmd = re.sub(r'"([^ ]+?)"', r'\1', filter_cmd) - try: - p = subprocess.Popen(filter_cmd, shell=True, - stdin=subprocess.PIPE, stdout=subprocess.PIPE) - output = p.communicate(os.linesep.join(lines))[0] - except Exception: - raise EAsciiDoc,'filter error: %s: %s' % (filter_cmd, sys.exc_info()[1]) - if output: - result = [s.rstrip() for s in output.split(os.linesep)] - else: - result = [] - filter_status = p.wait() - if filter_status: - message.warning('filter non-zero exit code: %s: returned %d' % - (filter_cmd, filter_status)) - if lines and not result: - message.warning('no output from filter: %s' % filter_cmd) - return result - -def system(name, args, is_macro=False, attrs=None): - """ - Evaluate a system attribute ({name:args}) or system block macro - (name::[args]). - If is_macro is True then we are processing a system block macro otherwise - it's a system attribute. - The attrs dictionary is updated by the counter and set system attributes. - NOTE: The include1 attribute is used internally by the include1::[] macro - and is not for public use. - """ - if is_macro: - syntax = '%s::[%s]' % (name,args) - separator = '\n' - else: - syntax = '{%s:%s}' % (name,args) - separator = writer.newline - if name not in ('eval','eval3','sys','sys2','sys3','include','include1','counter','counter2','set','set2','template'): - if is_macro: - msg = 'illegal system macro name: %s' % name - else: - msg = 'illegal system attribute name: %s' % name - message.warning(msg) - return None - if is_macro: - s = subs_attrs(args) - if s is None: - message.warning('skipped %s: undefined attribute in: %s' % (name,args)) - return None - args = s - if name != 'include1': - message.verbose('evaluating: %s' % syntax) - if safe() and name not in ('include','include1'): - message.unsafe(syntax) - return None - result = None - if name in ('eval','eval3'): - try: - result = eval(args) - if result is True: - result = '' - elif result is False: - result = None - elif result is not None: - result = str(result) - except Exception: - message.warning('%s: evaluation error' % syntax) - elif name in ('sys','sys2','sys3'): - result = '' - fd,tmp = tempfile.mkstemp() - os.close(fd) - try: - cmd = args - cmd = cmd + (' > "%s"' % tmp) - if name == 'sys2': - cmd = cmd + ' 2>&1' - if os.name == 'nt': - # Remove redundant quoting -- this is not just - # cosmetic, unnecessary quoting appears to cause - # command line truncation. - cmd = re.sub(r'"([^ ]+?)"', r'\1', cmd) - message.verbose('shelling: %s' % cmd) - if os.system(cmd): - message.warning('%s: non-zero exit status' % syntax) - try: - if os.path.isfile(tmp): - f = open(tmp) - try: - lines = [s.rstrip() for s in f] - finally: - f.close() - else: - lines = [] - except Exception: - raise EAsciiDoc,'%s: temp file read error' % syntax - result = separator.join(lines) - finally: - if os.path.isfile(tmp): - os.remove(tmp) - elif name in ('counter','counter2'): - mo = re.match(r'^(?P<attr>[^:]*?)(:(?P<seed>.*))?$', args) - attr = mo.group('attr') - seed = mo.group('seed') - if seed and (not re.match(r'^\d+$', seed) and len(seed) > 1): - message.warning('%s: illegal counter seed: %s' % (syntax,seed)) - return None - if not is_name(attr): - message.warning('%s: illegal attribute name' % syntax) - return None - value = document.attributes.get(attr) - if value: - if not re.match(r'^\d+$', value) and len(value) > 1: - message.warning('%s: illegal counter value: %s' - % (syntax,value)) - return None - if re.match(r'^\d+$', value): - expr = value + '+1' - else: - expr = 'chr(ord("%s")+1)' % value - try: - result = str(eval(expr)) - except Exception: - message.warning('%s: evaluation error: %s' % (syntax, expr)) - else: - if seed: - result = seed - else: - result = '1' - document.attributes[attr] = result - if attrs is not None: - attrs[attr] = result - if name == 'counter2': - result = '' - elif name in ('set','set2'): - mo = re.match(r'^(?P<attr>[^:]*?)(:(?P<value>.*))?$', args) - attr = mo.group('attr') - value = mo.group('value') - if value is None: - value = '' - if attr.endswith('!'): - attr = attr[:-1] - value = None - if not is_name(attr): - message.warning('%s: illegal attribute name' % syntax) - else: - if attrs is not None: - attrs[attr] = value - if name != 'set2': # set2 only updates local attributes. - document.attributes[attr] = value - if value is None: - result = None - else: - result = '' - elif name == 'include': - if not os.path.exists(args): - message.warning('%s: file does not exist' % syntax) - elif not is_safe_file(args): - message.unsafe(syntax) - else: - f = open(args) - try: - result = [s.rstrip() for s in f] - finally: - f.close() - if result: - result = subs_attrs(result) - result = separator.join(result) - result = result.expandtabs(reader.tabsize) - else: - result = '' - elif name == 'include1': - result = separator.join(config.include1[args]) - elif name == 'template': - if not args in config.sections: - message.warning('%s: template does not exist' % syntax) - else: - result = [] - for line in config.sections[args]: - line = subs_attrs(line) - if line is not None: - result.append(line) - result = '\n'.join(result) - else: - assert False - if result and name in ('eval3','sys3'): - macros.passthroughs.append(result) - result = '\x07' + str(len(macros.passthroughs)-1) + '\x07' - return result - -def subs_attrs(lines, dictionary=None): - """Substitute 'lines' of text with attributes from the global - document.attributes dictionary and from 'dictionary' ('dictionary' - entries take precedence). Return a tuple of the substituted lines. 'lines' - containing undefined attributes are deleted. If 'lines' is a string then - return a string. - - - Attribute references are substituted in the following order: simple, - conditional, system. - - Attribute references inside 'dictionary' entry values are substituted. - """ - - def end_brace(text,start): - """Return index following end brace that matches brace at start in - text.""" - assert text[start] == '{' - n = 0 - result = start - for c in text[start:]: - # Skip braces that are followed by a backslash. - if result == len(text)-1 or text[result+1] != '\\': - if c == '{': n = n + 1 - elif c == '}': n = n - 1 - result = result + 1 - if n == 0: break - return result - - if type(lines) == str: - string_result = True - lines = [lines] - else: - string_result = False - if dictionary is None: - attrs = document.attributes - else: - # Remove numbered document attributes so they don't clash with - # attribute list positional attributes. - attrs = {} - for k,v in document.attributes.items(): - if not re.match(r'^\d+$', k): - attrs[k] = v - # Substitute attribute references inside dictionary values. - for k,v in dictionary.items(): - if v is None: - del dictionary[k] - else: - v = subs_attrs(str(v)) - if v is None: - del dictionary[k] - else: - dictionary[k] = v - attrs.update(dictionary) - # Substitute all attributes in all lines. - result = [] - for line in lines: - # Make it easier for regular expressions. - line = line.replace('\\{','{\\') - line = line.replace('\\}','}\\') - # Expand simple attributes ({name}). - # Nested attributes not allowed. - reo = re.compile(r'(?su)\{(?P<name>[^\\\W][-\w]*?)\}(?!\\)') - pos = 0 - while True: - mo = reo.search(line,pos) - if not mo: break - s = attrs.get(mo.group('name')) - if s is None: - pos = mo.end() - else: - s = str(s) - line = line[:mo.start()] + s + line[mo.end():] - pos = mo.start() + len(s) - # Expand conditional attributes. - # Single name -- higher precedence. - reo1 = re.compile(r'(?su)\{(?P<name>[^\\\W][-\w]*?)' \ - r'(?P<op>\=|\?|!|#|%|@|\$)' \ - r'(?P<value>.*?)\}(?!\\)') - # Multiple names (n1,n2,... or n1+n2+...) -- lower precedence. - reo2 = re.compile(r'(?su)\{(?P<name>[^\\\W][-\w'+OR+AND+r']*?)' \ - r'(?P<op>\=|\?|!|#|%|@|\$)' \ - r'(?P<value>.*?)\}(?!\\)') - for reo in [reo1,reo2]: - pos = 0 - while True: - mo = reo.search(line,pos) - if not mo: break - attr = mo.group() - name = mo.group('name') - if reo == reo2: - if OR in name: - sep = OR - else: - sep = AND - names = [s.strip() for s in name.split(sep) if s.strip() ] - for n in names: - if not re.match(r'^[^\\\W][-\w]*$',n): - message.error('illegal attribute syntax: %s' % attr) - if sep == OR: - # Process OR name expression: n1,n2,... - for n in names: - if attrs.get(n) is not None: - lval = '' - break - else: - lval = None - else: - # Process AND name expression: n1+n2+... - for n in names: - if attrs.get(n) is None: - lval = None - break - else: - lval = '' - else: - lval = attrs.get(name) - op = mo.group('op') - # mo.end() not good enough because '{x={y}}' matches '{x={y}'. - end = end_brace(line,mo.start()) - rval = line[mo.start('value'):end-1] - UNDEFINED = '{zzzzz}' - if lval is None: - if op == '=': s = rval - elif op == '?': s = '' - elif op == '!': s = rval - elif op == '#': s = UNDEFINED # So the line is dropped. - elif op == '%': s = rval - elif op in ('@','$'): - s = UNDEFINED # So the line is dropped. - else: - assert False, 'illegal attribute: %s' % attr - else: - if op == '=': s = lval - elif op == '?': s = rval - elif op == '!': s = '' - elif op == '#': s = rval - elif op == '%': s = UNDEFINED # So the line is dropped. - elif op in ('@','$'): - v = re.split(r'(?<!\\):',rval) - if len(v) not in (2,3): - message.error('illegal attribute syntax: %s' % attr) - s = '' - elif not is_re('^'+v[0]+'$'): - message.error('illegal attribute regexp: %s' % attr) - s = '' - else: - v = [s.replace('\\:',':') for s in v] - re_mo = re.match('^'+v[0]+'$',lval) - if op == '@': - if re_mo: - s = v[1] # {<name>@<re>:<v1>[:<v2>]} - else: - if len(v) == 3: # {<name>@<re>:<v1>:<v2>} - s = v[2] - else: # {<name>@<re>:<v1>} - s = '' - else: - if re_mo: - if len(v) == 2: # {<name>$<re>:<v1>} - s = v[1] - elif v[1] == '': # {<name>$<re>::<v2>} - s = UNDEFINED # So the line is dropped. - else: # {<name>$<re>:<v1>:<v2>} - s = v[1] - else: - if len(v) == 2: # {<name>$<re>:<v1>} - s = UNDEFINED # So the line is dropped. - else: # {<name>$<re>:<v1>:<v2>} - s = v[2] - else: - assert False, 'illegal attribute: %s' % attr - s = str(s) - line = line[:mo.start()] + s + line[end:] - pos = mo.start() + len(s) - # Drop line if it contains unsubstituted {name} references. - skipped = re.search(r'(?su)\{[^\\\W][-\w]*?\}(?!\\)', line) - if skipped: - trace('dropped line', line) - continue; - # Expand system attributes (eval has precedence). - reos = [ - re.compile(r'(?su)\{(?P<action>eval):(?P<expr>.*?)\}(?!\\)'), - re.compile(r'(?su)\{(?P<action>[^\\\W][-\w]*?):(?P<expr>.*?)\}(?!\\)'), - ] - skipped = False - for reo in reos: - pos = 0 - while True: - mo = reo.search(line,pos) - if not mo: break - expr = mo.group('expr') - action = mo.group('action') - expr = expr.replace('{\\','{') - expr = expr.replace('}\\','}') - s = system(action, expr, attrs=dictionary) - if dictionary is not None and action in ('counter','counter2','set','set2'): - # These actions create and update attributes. - attrs.update(dictionary) - if s is None: - # Drop line if the action returns None. - skipped = True - break - line = line[:mo.start()] + s + line[mo.end():] - pos = mo.start() + len(s) - if skipped: - break - if not skipped: - # Remove backslash from escaped entries. - line = line.replace('{\\','{') - line = line.replace('}\\','}') - result.append(line) - if string_result: - if result: - return '\n'.join(result) - else: - return None - else: - return tuple(result) - -def char_encoding(): - encoding = document.attributes.get('encoding') - if encoding: - try: - codecs.lookup(encoding) - except LookupError,e: - raise EAsciiDoc,str(e) - return encoding - -def char_len(s): - return len(char_decode(s)) - -east_asian_widths = {'W': 2, # Wide - 'F': 2, # Full-width (wide) - 'Na': 1, # Narrow - 'H': 1, # Half-width (narrow) - 'N': 1, # Neutral (not East Asian, treated as narrow) - 'A': 1} # Ambiguous (s/b wide in East Asian context, - # narrow otherwise, but that doesn't work) -"""Mapping of result codes from `unicodedata.east_asian_width()` to character -column widths.""" - -def column_width(s): - text = char_decode(s) - if isinstance(text, unicode): - width = 0 - for c in text: - width += east_asian_widths[unicodedata.east_asian_width(c)] - return width - else: - return len(text) - -def char_decode(s): - if char_encoding(): - try: - return s.decode(char_encoding()) - except Exception: - raise EAsciiDoc, \ - "'%s' codec can't decode \"%s\"" % (char_encoding(), s) - else: - return s - -def char_encode(s): - if char_encoding(): - return s.encode(char_encoding()) - else: - return s - -def time_str(t): - """Convert seconds since the Epoch to formatted local time string.""" - t = time.localtime(t) - s = time.strftime('%H:%M:%S',t) - if time.daylight and t.tm_isdst == 1: - result = s + ' ' + time.tzname[1] - else: - result = s + ' ' + time.tzname[0] - # Attempt to convert the localtime to the output encoding. - try: - result = char_encode(result.decode(locale.getdefaultlocale()[1])) - except Exception: - pass - return result - -def date_str(t): - """Convert seconds since the Epoch to formatted local date string.""" - t = time.localtime(t) - return time.strftime('%Y-%m-%d',t) - - -class Lex: - """Lexical analysis routines. Static methods and attributes only.""" - prev_element = None - prev_cursor = None - def __init__(self): - raise AssertionError,'no class instances allowed' - @staticmethod - def next(): - """Returns class of next element on the input (None if EOF). The - reader is assumed to be at the first line following a previous element, - end of file or line one. Exits with the reader pointing to the first - line of the next element or EOF (leading blank lines are skipped).""" - reader.skip_blank_lines() - if reader.eof(): return None - # Optimization: If we've already checked for an element at this - # position return the element. - if Lex.prev_element and Lex.prev_cursor == reader.cursor: - return Lex.prev_element - if AttributeEntry.isnext(): - result = AttributeEntry - elif AttributeList.isnext(): - result = AttributeList - elif BlockTitle.isnext() and not tables_OLD.isnext(): - result = BlockTitle - elif Title.isnext(): - if AttributeList.style() == 'float': - result = FloatingTitle - else: - result = Title - elif macros.isnext(): - result = macros.current - elif lists.isnext(): - result = lists.current - elif blocks.isnext(): - result = blocks.current - elif tables_OLD.isnext(): - result = tables_OLD.current - elif tables.isnext(): - result = tables.current - else: - if not paragraphs.isnext(): - raise EAsciiDoc,'paragraph expected' - result = paragraphs.current - # Optimization: Cache answer. - Lex.prev_cursor = reader.cursor - Lex.prev_element = result - return result - - @staticmethod - def canonical_subs(options): - """Translate composite subs values.""" - if len(options) == 1: - if options[0] == 'none': - options = () - elif options[0] == 'normal': - options = config.subsnormal - elif options[0] == 'verbatim': - options = config.subsverbatim - return options - - @staticmethod - def subs_1(s,options): - """Perform substitution specified in 'options' (in 'options' order).""" - if not s: - return s - if document.attributes.get('plaintext') is not None: - options = ('specialcharacters',) - result = s - options = Lex.canonical_subs(options) - for o in options: - if o == 'specialcharacters': - result = config.subs_specialchars(result) - elif o == 'attributes': - result = subs_attrs(result) - elif o == 'quotes': - result = subs_quotes(result) - elif o == 'specialwords': - result = config.subs_specialwords(result) - elif o in ('replacements','replacements2','replacements3'): - result = config.subs_replacements(result,o) - elif o == 'macros': - result = macros.subs(result) - elif o == 'callouts': - result = macros.subs(result,callouts=True) - else: - raise EAsciiDoc,'illegal substitution option: %s' % o - trace(o, s, result) - if not result: - break - return result - - @staticmethod - def subs(lines,options): - """Perform inline processing specified by 'options' (in 'options' - order) on sequence of 'lines'.""" - if not lines or not options: - return lines - options = Lex.canonical_subs(options) - # Join lines so quoting can span multiple lines. - para = '\n'.join(lines) - if 'macros' in options: - para = macros.extract_passthroughs(para) - for o in options: - if o == 'attributes': - # If we don't substitute attributes line-by-line then a single - # undefined attribute will drop the entire paragraph. - lines = subs_attrs(para.split('\n')) - para = '\n'.join(lines) - else: - para = Lex.subs_1(para,(o,)) - if 'macros' in options: - para = macros.restore_passthroughs(para) - return para.splitlines() - - @staticmethod - def set_margin(lines, margin=0): - """Utility routine that sets the left margin to 'margin' space in a - block of non-blank lines.""" - # Calculate width of block margin. - lines = list(lines) - width = len(lines[0]) - for s in lines: - i = re.search(r'\S',s).start() - if i < width: width = i - # Strip margin width from all lines. - for i in range(len(lines)): - lines[i] = ' '*margin + lines[i][width:] - return lines - -#--------------------------------------------------------------------------- -# Document element classes parse AsciiDoc reader input and write DocBook writer -# output. -#--------------------------------------------------------------------------- -class Document(object): - - # doctype property. - def getdoctype(self): - return self.attributes.get('doctype') - def setdoctype(self,doctype): - self.attributes['doctype'] = doctype - doctype = property(getdoctype,setdoctype) - - # backend property. - def getbackend(self): - return self.attributes.get('backend') - def setbackend(self,backend): - if backend: - backend = self.attributes.get('backend-alias-' + backend, backend) - self.attributes['backend'] = backend - backend = property(getbackend,setbackend) - - def __init__(self): - self.infile = None # Source file name. - self.outfile = None # Output file name. - self.attributes = InsensitiveDict() - self.level = 0 # 0 => front matter. 1,2,3 => sect1,2,3. - self.has_errors = False # Set true if processing errors were flagged. - self.has_warnings = False # Set true if warnings were flagged. - self.safe = False # Default safe mode. - def update_attributes(self,attrs=None): - """ - Set implicit attributes and attributes in 'attrs'. - """ - t = time.time() - self.attributes['localtime'] = time_str(t) - self.attributes['localdate'] = date_str(t) - self.attributes['asciidoc-version'] = VERSION - self.attributes['asciidoc-file'] = APP_FILE - self.attributes['asciidoc-dir'] = APP_DIR - if localapp(): - self.attributes['asciidoc-confdir'] = APP_DIR - else: - self.attributes['asciidoc-confdir'] = CONF_DIR - self.attributes['user-dir'] = USER_DIR - if config.verbose: - self.attributes['verbose'] = '' - # Update with configuration file attributes. - if attrs: - self.attributes.update(attrs) - # Update with command-line attributes. - self.attributes.update(config.cmd_attrs) - # Extract miscellaneous configuration section entries from attributes. - if attrs: - config.load_miscellaneous(attrs) - config.load_miscellaneous(config.cmd_attrs) - self.attributes['newline'] = config.newline - # File name related attributes can't be overridden. - if self.infile is not None: - if self.infile and os.path.exists(self.infile): - t = os.path.getmtime(self.infile) - elif self.infile == '<stdin>': - t = time.time() - else: - t = None - if t: - self.attributes['doctime'] = time_str(t) - self.attributes['docdate'] = date_str(t) - if self.infile != '<stdin>': - self.attributes['infile'] = self.infile - self.attributes['indir'] = os.path.dirname(self.infile) - self.attributes['docfile'] = self.infile - self.attributes['docdir'] = os.path.dirname(self.infile) - self.attributes['docname'] = os.path.splitext( - os.path.basename(self.infile))[0] - if self.outfile: - if self.outfile != '<stdout>': - self.attributes['outfile'] = self.outfile - self.attributes['outdir'] = os.path.dirname(self.outfile) - if self.infile == '<stdin>': - self.attributes['docname'] = os.path.splitext( - os.path.basename(self.outfile))[0] - ext = os.path.splitext(self.outfile)[1][1:] - elif config.outfilesuffix: - ext = config.outfilesuffix[1:] - else: - ext = '' - if ext: - self.attributes['filetype'] = ext - self.attributes['filetype-'+ext] = '' - def load_lang(self): - """ - Load language configuration file. - """ - lang = self.attributes.get('lang') - if lang is None: - filename = 'lang-en.conf' # Default language file. - else: - filename = 'lang-' + lang + '.conf' - if config.load_from_dirs(filename): - self.attributes['lang'] = lang # Reinstate new lang attribute. - else: - if lang is None: - # The default language file must exist. - message.error('missing conf file: %s' % filename, halt=True) - else: - message.warning('missing language conf file: %s' % filename) - def set_deprecated_attribute(self,old,new): - """ - Ensures the 'old' name of an attribute that was renamed to 'new' is - still honored. - """ - if self.attributes.get(new) is None: - if self.attributes.get(old) is not None: - self.attributes[new] = self.attributes[old] - else: - self.attributes[old] = self.attributes[new] - def consume_attributes_and_comments(self,comments_only=False,noblanks=False): - """ - Returns True if one or more attributes or comments were consumed. - If 'noblanks' is True then consumation halts if a blank line is - encountered. - """ - result = False - finished = False - while not finished: - finished = True - if noblanks and not reader.read_next(): return result - if blocks.isnext() and 'skip' in blocks.current.options: - result = True - finished = False - blocks.current.translate() - if noblanks and not reader.read_next(): return result - if macros.isnext() and macros.current.name == 'comment': - result = True - finished = False - macros.current.translate() - if not comments_only: - if AttributeEntry.isnext(): - result = True - finished = False - AttributeEntry.translate() - if AttributeList.isnext(): - result = True - finished = False - AttributeList.translate() - return result - def parse_header(self,doctype,backend): - """ - Parses header, sets corresponding document attributes and finalizes - document doctype and backend properties. - Returns False if the document does not have a header. - 'doctype' and 'backend' are the doctype and backend option values - passed on the command-line, None if no command-line option was not - specified. - """ - assert self.level == 0 - # Skip comments and attribute entries that preceed the header. - self.consume_attributes_and_comments() - if doctype is not None: - # Command-line overrides header. - self.doctype = doctype - elif self.doctype is None: - # Was not set on command-line or in document header. - self.doctype = DEFAULT_DOCTYPE - # Process document header. - has_header = (Title.isnext() and Title.level == 0 - and AttributeList.style() != 'float') - if self.doctype == 'manpage' and not has_header: - message.error('manpage document title is mandatory',halt=True) - if has_header: - Header.parse() - # Command-line entries override header derived entries. - self.attributes.update(config.cmd_attrs) - # DEPRECATED: revision renamed to revnumber. - self.set_deprecated_attribute('revision','revnumber') - # DEPRECATED: date renamed to revdate. - self.set_deprecated_attribute('date','revdate') - if doctype is not None: - # Command-line overrides header. - self.doctype = doctype - if backend is not None: - # Command-line overrides header. - self.backend = backend - elif self.backend is None: - # Was not set on command-line or in document header. - self.backend = DEFAULT_BACKEND - else: - # Has been set in document header. - self.backend = self.backend # Translate alias in header. - assert self.doctype in ('article','manpage','book'), 'illegal document type' - return has_header - def translate(self,has_header): - if self.doctype == 'manpage': - # Translate mandatory NAME section. - if Lex.next() is not Title: - message.error('name section expected') - else: - Title.translate() - if Title.level != 1: - message.error('name section title must be at level 1') - if not isinstance(Lex.next(),Paragraph): - message.error('malformed name section body') - lines = reader.read_until(r'^$') - s = ' '.join(lines) - mo = re.match(r'^(?P<manname>.*?)\s+-\s+(?P<manpurpose>.*)$',s) - if not mo: - message.error('malformed name section body') - self.attributes['manname'] = mo.group('manname').strip() - self.attributes['manpurpose'] = mo.group('manpurpose').strip() - names = [s.strip() for s in self.attributes['manname'].split(',')] - if len(names) > 9: - message.warning('too many manpage names') - for i,name in enumerate(names): - self.attributes['manname%d' % (i+1)] = name - if has_header: - # Do postponed substitutions (backend confs have been loaded). - self.attributes['doctitle'] = Title.dosubs(self.attributes['doctitle']) - if config.header_footer: - hdr = config.subs_section('header',{}) - writer.write(hdr,trace='header') - if 'title' in self.attributes: - del self.attributes['title'] - self.consume_attributes_and_comments() - if self.doctype in ('article','book'): - # Translate 'preamble' (untitled elements between header - # and first section title). - if Lex.next() is not Title: - stag,etag = config.section2tags('preamble') - writer.write(stag,trace='preamble open') - Section.translate_body() - writer.write(etag,trace='preamble close') - elif self.doctype == 'manpage' and 'name' in config.sections: - writer.write(config.subs_section('name',{}), trace='name') - else: - self.process_author_names() - if config.header_footer: - hdr = config.subs_section('header',{}) - writer.write(hdr,trace='header') - if Lex.next() is not Title: - Section.translate_body() - # Process remaining sections. - while not reader.eof(): - if Lex.next() is not Title: - raise EAsciiDoc,'section title expected' - Section.translate() - Section.setlevel(0) # Write remaining unwritten section close tags. - # Substitute document parameters and write document footer. - if config.header_footer: - ftr = config.subs_section('footer',{}) - writer.write(ftr,trace='footer') - def parse_author(self,s): - """ Return False if the author is malformed.""" - attrs = self.attributes # Alias for readability. - s = s.strip() - mo = re.match(r'^(?P<name1>[^<>\s]+)' - '(\s+(?P<name2>[^<>\s]+))?' - '(\s+(?P<name3>[^<>\s]+))?' - '(\s+<(?P<email>\S+)>)?$',s) - if not mo: - # Names that don't match the formal specification. - if s: - attrs['firstname'] = s - return - firstname = mo.group('name1') - if mo.group('name3'): - middlename = mo.group('name2') - lastname = mo.group('name3') - else: - middlename = None - lastname = mo.group('name2') - firstname = firstname.replace('_',' ') - if middlename: - middlename = middlename.replace('_',' ') - if lastname: - lastname = lastname.replace('_',' ') - email = mo.group('email') - if firstname: - attrs['firstname'] = firstname - if middlename: - attrs['middlename'] = middlename - if lastname: - attrs['lastname'] = lastname - if email: - attrs['email'] = email - return - def process_author_names(self): - """ Calculate any missing author related attributes.""" - attrs = self.attributes # Alias for readability. - firstname = attrs.get('firstname','') - middlename = attrs.get('middlename','') - lastname = attrs.get('lastname','') - author = attrs.get('author') - initials = attrs.get('authorinitials') - if author and not (firstname or middlename or lastname): - self.parse_author(author) - attrs['author'] = author.replace('_',' ') - self.process_author_names() - return - if not author: - author = '%s %s %s' % (firstname, middlename, lastname) - author = author.strip() - author = re.sub(r'\s+',' ', author) - if not initials: - initials = (char_decode(firstname)[:1] + - char_decode(middlename)[:1] + char_decode(lastname)[:1]) - initials = char_encode(initials).upper() - names = [firstname,middlename,lastname,author,initials] - for i,v in enumerate(names): - v = config.subs_specialchars(v) - v = subs_attrs(v) - names[i] = v - firstname,middlename,lastname,author,initials = names - if firstname: - attrs['firstname'] = firstname - if middlename: - attrs['middlename'] = middlename - if lastname: - attrs['lastname'] = lastname - if author: - attrs['author'] = author - if initials: - attrs['authorinitials'] = initials - if author: - attrs['authored'] = '' - - -class Header: - """Static methods and attributes only.""" - REV_LINE_RE = r'^(\D*(?P<revnumber>.*?),)?(?P<revdate>.*?)(:\s*(?P<revremark>.*))?$' - RCS_ID_RE = r'^\$Id: \S+ (?P<revnumber>\S+) (?P<revdate>\S+) \S+ (?P<author>\S+) (\S+ )?\$$' - def __init__(self): - raise AssertionError,'no class instances allowed' - @staticmethod - def parse(): - assert Lex.next() is Title and Title.level == 0 - attrs = document.attributes # Alias for readability. - # Postpone title subs until backend conf files have been loaded. - Title.translate(skipsubs=True) - attrs['doctitle'] = Title.attributes['title'] - document.consume_attributes_and_comments(noblanks=True) - s = reader.read_next() - mo = None - if s: - # Process first header line after the title that is not a comment - # or an attribute entry. - s = reader.read() - mo = re.match(Header.RCS_ID_RE,s) - if not mo: - document.parse_author(s) - document.consume_attributes_and_comments(noblanks=True) - if reader.read_next(): - # Process second header line after the title that is not a - # comment or an attribute entry. - s = reader.read() - s = subs_attrs(s) - if s: - mo = re.match(Header.RCS_ID_RE,s) - if not mo: - mo = re.match(Header.REV_LINE_RE,s) - document.consume_attributes_and_comments(noblanks=True) - s = attrs.get('revnumber') - if s: - mo = re.match(Header.RCS_ID_RE,s) - if mo: - revnumber = mo.group('revnumber') - if revnumber: - attrs['revnumber'] = revnumber.strip() - author = mo.groupdict().get('author') - if author and 'firstname' not in attrs: - document.parse_author(author) - revremark = mo.groupdict().get('revremark') - if revremark is not None: - revremark = [revremark] - # Revision remarks can continue on following lines. - while reader.read_next(): - if document.consume_attributes_and_comments(noblanks=True): - break - revremark.append(reader.read()) - revremark = Lex.subs(revremark,['normal']) - revremark = '\n'.join(revremark).strip() - attrs['revremark'] = revremark - revdate = mo.group('revdate') - if revdate: - attrs['revdate'] = revdate.strip() - elif revnumber or revremark: - # Set revision date to ensure valid DocBook revision. - attrs['revdate'] = attrs['docdate'] - document.process_author_names() - if document.doctype == 'manpage': - # manpage title formatted like mantitle(manvolnum). - mo = re.match(r'^(?P<mantitle>.*)\((?P<manvolnum>.*)\)$', - attrs['doctitle']) - if not mo: - message.error('malformed manpage title') - else: - mantitle = mo.group('mantitle').strip() - mantitle = subs_attrs(mantitle) - if mantitle is None: - message.error('undefined attribute in manpage title') - # mantitle is lowered only if in ALL CAPS - if mantitle == mantitle.upper(): - mantitle = mantitle.lower() - attrs['mantitle'] = mantitle; - attrs['manvolnum'] = mo.group('manvolnum').strip() - -class AttributeEntry: - """Static methods and attributes only.""" - pattern = None - subs = None - name = None - name2 = None - value = None - attributes = {} # Accumulates all the parsed attribute entries. - def __init__(self): - raise AssertionError,'no class instances allowed' - @staticmethod - def isnext(): - result = False # Assume not next. - if not AttributeEntry.pattern: - pat = document.attributes.get('attributeentry-pattern') - if not pat: - message.error("[attributes] missing 'attributeentry-pattern' entry") - AttributeEntry.pattern = pat - line = reader.read_next() - if line: - # Attribute entry formatted like :<name>[.<name2>]:[ <value>] - mo = re.match(AttributeEntry.pattern,line) - if mo: - AttributeEntry.name = mo.group('attrname') - AttributeEntry.name2 = mo.group('attrname2') - AttributeEntry.value = mo.group('attrvalue') or '' - AttributeEntry.value = AttributeEntry.value.strip() - result = True - return result - @staticmethod - def translate(): - assert Lex.next() is AttributeEntry - attr = AttributeEntry # Alias for brevity. - reader.read() # Discard attribute entry from reader. - while attr.value.endswith(' +'): - if not reader.read_next(): break - attr.value = attr.value[:-1] + reader.read().strip() - if attr.name2 is not None: - # Configuration file attribute. - if attr.name2 != '': - # Section entry attribute. - section = {} - # Some sections can have name! syntax. - if attr.name in ('attributes','miscellaneous') and attr.name2[-1] == '!': - section[attr.name] = [attr.name2] - else: - section[attr.name] = ['%s=%s' % (attr.name2,attr.value)] - config.load_sections(section) - config.load_miscellaneous(config.conf_attrs) - else: - # Markup template section attribute. - config.sections[attr.name] = [attr.value] - else: - # Normal attribute. - if attr.name[-1] == '!': - # Names like name! undefine the attribute. - attr.name = attr.name[:-1] - attr.value = None - # Strip white space and illegal name chars. - attr.name = re.sub(r'(?u)[^\w\-_]', '', attr.name).lower() - # Don't override most command-line attributes. - if attr.name in config.cmd_attrs \ - and attr.name not in ('trace','numbered'): - return - # Update document attributes with attribute value. - if attr.value is not None: - mo = re.match(r'^pass:(?P<attrs>.*)\[(?P<value>.*)\]$', attr.value) - if mo: - # Inline passthrough syntax. - attr.subs = mo.group('attrs') - attr.value = mo.group('value') # Passthrough. - else: - # Default substitution. - # DEPRECATED: attributeentry-subs - attr.subs = document.attributes.get('attributeentry-subs', - 'specialcharacters,attributes') - attr.subs = parse_options(attr.subs, SUBS_OPTIONS, - 'illegal substitution option') - attr.value = Lex.subs((attr.value,), attr.subs) - attr.value = writer.newline.join(attr.value) - document.attributes[attr.name] = attr.value - elif attr.name in document.attributes: - del document.attributes[attr.name] - attr.attributes[attr.name] = attr.value - -class AttributeList: - """Static methods and attributes only.""" - pattern = None - match = None - attrs = {} - def __init__(self): - raise AssertionError,'no class instances allowed' - @staticmethod - def initialize(): - if not 'attributelist-pattern' in document.attributes: - message.error("[attributes] missing 'attributelist-pattern' entry") - AttributeList.pattern = document.attributes['attributelist-pattern'] - @staticmethod - def isnext(): - result = False # Assume not next. - line = reader.read_next() - if line: - mo = re.match(AttributeList.pattern, line) - if mo: - AttributeList.match = mo - result = True - return result - @staticmethod - def translate(): - assert Lex.next() is AttributeList - reader.read() # Discard attribute list from reader. - attrs = {} - d = AttributeList.match.groupdict() - for k,v in d.items(): - if v is not None: - if k == 'attrlist': - v = subs_attrs(v) - if v: - parse_attributes(v, attrs) - else: - AttributeList.attrs[k] = v - AttributeList.subs(attrs) - AttributeList.attrs.update(attrs) - @staticmethod - def subs(attrs): - '''Substitute single quoted attribute values normally.''' - reo = re.compile(r"^'.*'$") - for k,v in attrs.items(): - if reo.match(str(v)): - attrs[k] = Lex.subs_1(v[1:-1], config.subsnormal) - @staticmethod - def style(): - return AttributeList.attrs.get('style') or AttributeList.attrs.get('1') - @staticmethod - def consume(d={}): - """Add attribute list to the dictionary 'd' and reset the list.""" - if AttributeList.attrs: - d.update(AttributeList.attrs) - AttributeList.attrs = {} - # Generate option attributes. - if 'options' in d: - options = parse_options(d['options'], (), 'illegal option name') - for option in options: - d[option+'-option'] = '' - -class BlockTitle: - """Static methods and attributes only.""" - title = None - pattern = None - def __init__(self): - raise AssertionError,'no class instances allowed' - @staticmethod - def isnext(): - result = False # Assume not next. - line = reader.read_next() - if line: - mo = re.match(BlockTitle.pattern,line) - if mo: - BlockTitle.title = mo.group('title') - result = True - return result - @staticmethod - def translate(): - assert Lex.next() is BlockTitle - reader.read() # Discard title from reader. - # Perform title substitutions. - if not Title.subs: - Title.subs = config.subsnormal - s = Lex.subs((BlockTitle.title,), Title.subs) - s = writer.newline.join(s) - if not s: - message.warning('blank block title') - BlockTitle.title = s - @staticmethod - def consume(d={}): - """If there is a title add it to dictionary 'd' then reset title.""" - if BlockTitle.title: - d['title'] = BlockTitle.title - BlockTitle.title = None - -class Title: - """Processes Header and Section titles. Static methods and attributes - only.""" - # Class variables - underlines = ('==','--','~~','^^','++') # Levels 0,1,2,3,4. - subs = () - pattern = None - level = 0 - attributes = {} - sectname = None - section_numbers = [0]*len(underlines) - dump_dict = {} - linecount = None # Number of lines in title (1 or 2). - def __init__(self): - raise AssertionError,'no class instances allowed' - @staticmethod - def translate(skipsubs=False): - """Parse the Title.attributes and Title.level from the reader. The - real work has already been done by parse().""" - assert Lex.next() in (Title,FloatingTitle) - # Discard title from reader. - for i in range(Title.linecount): - reader.read() - Title.setsectname() - if not skipsubs: - Title.attributes['title'] = Title.dosubs(Title.attributes['title']) - @staticmethod - def dosubs(title): - """ - Perform title substitutions. - """ - if not Title.subs: - Title.subs = config.subsnormal - title = Lex.subs((title,), Title.subs) - title = writer.newline.join(title) - if not title: - message.warning('blank section title') - return title - @staticmethod - def isnext(): - lines = reader.read_ahead(2) - return Title.parse(lines) - @staticmethod - def parse(lines): - """Parse title at start of lines tuple.""" - if len(lines) == 0: return False - if len(lines[0]) == 0: return False # Title can't be blank. - # Check for single-line titles. - result = False - for level in range(len(Title.underlines)): - k = 'sect%s' % level - if k in Title.dump_dict: - mo = re.match(Title.dump_dict[k], lines[0]) - if mo: - Title.attributes = mo.groupdict() - Title.level = level - Title.linecount = 1 - result = True - break - if not result: - # Check for double-line titles. - if not Title.pattern: return False # Single-line titles only. - if len(lines) < 2: return False - title,ul = lines[:2] - title_len = column_width(title) - ul_len = char_len(ul) - if ul_len < 2: return False - # Fast elimination check. - if ul[:2] not in Title.underlines: return False - # Length of underline must be within +-3 of title. - if not ((ul_len-3 < title_len < ul_len+3) - # Next test for backward compatibility. - or (ul_len-3 < char_len(title) < ul_len+3)): - return False - # Check for valid repetition of underline character pairs. - s = ul[:2]*((ul_len+1)/2) - if ul != s[:ul_len]: return False - # Don't be fooled by back-to-back delimited blocks, require at - # least one alphanumeric character in title. - if not re.search(r'(?u)\w',title): return False - mo = re.match(Title.pattern, title) - if mo: - Title.attributes = mo.groupdict() - Title.level = list(Title.underlines).index(ul[:2]) - Title.linecount = 2 - result = True - # Check for expected pattern match groups. - if result: - if not 'title' in Title.attributes: - message.warning('[titles] entry has no <title> group') - Title.attributes['title'] = lines[0] - for k,v in Title.attributes.items(): - if v is None: del Title.attributes[k] - try: - Title.level += int(document.attributes.get('leveloffset','0')) - except: - pass - Title.attributes['level'] = str(Title.level) - return result - @staticmethod - def load(entries): - """Load and validate [titles] section entries dictionary.""" - if 'underlines' in entries: - errmsg = 'malformed [titles] underlines entry' - try: - underlines = parse_list(entries['underlines']) - except Exception: - raise EAsciiDoc,errmsg - if len(underlines) != len(Title.underlines): - raise EAsciiDoc,errmsg - for s in underlines: - if len(s) !=2: - raise EAsciiDoc,errmsg - Title.underlines = tuple(underlines) - Title.dump_dict['underlines'] = entries['underlines'] - if 'subs' in entries: - Title.subs = parse_options(entries['subs'], SUBS_OPTIONS, - 'illegal [titles] subs entry') - Title.dump_dict['subs'] = entries['subs'] - if 'sectiontitle' in entries: - pat = entries['sectiontitle'] - if not pat or not is_re(pat): - raise EAsciiDoc,'malformed [titles] sectiontitle entry' - Title.pattern = pat - Title.dump_dict['sectiontitle'] = pat - if 'blocktitle' in entries: - pat = entries['blocktitle'] - if not pat or not is_re(pat): - raise EAsciiDoc,'malformed [titles] blocktitle entry' - BlockTitle.pattern = pat - Title.dump_dict['blocktitle'] = pat - # Load single-line title patterns. - for k in ('sect0','sect1','sect2','sect3','sect4'): - if k in entries: - pat = entries[k] - if not pat or not is_re(pat): - raise EAsciiDoc,'malformed [titles] %s entry' % k - Title.dump_dict[k] = pat - # TODO: Check we have either a Title.pattern or at least one - # single-line title pattern -- can this be done here or do we need - # check routine like the other block checkers? - @staticmethod - def dump(): - dump_section('titles',Title.dump_dict) - @staticmethod - def setsectname(): - """ - Set Title section name: - If the first positional or 'template' attribute is set use it, - next search for section title in [specialsections], - if not found use default 'sect<level>' name. - """ - sectname = AttributeList.attrs.get('1') - if sectname and sectname != 'float': - Title.sectname = sectname - elif 'template' in AttributeList.attrs: - Title.sectname = AttributeList.attrs['template'] - else: - for pat,sect in config.specialsections.items(): - mo = re.match(pat,Title.attributes['title']) - if mo: - title = mo.groupdict().get('title') - if title is not None: - Title.attributes['title'] = title.strip() - else: - Title.attributes['title'] = mo.group().strip() - Title.sectname = sect - break - else: - Title.sectname = 'sect%d' % Title.level - @staticmethod - def getnumber(level): - """Return next section number at section 'level' formatted like - 1.2.3.4.""" - number = '' - for l in range(len(Title.section_numbers)): - n = Title.section_numbers[l] - if l == 0: - continue - elif l < level: - number = '%s%d.' % (number, n) - elif l == level: - number = '%s%d.' % (number, n + 1) - Title.section_numbers[l] = n + 1 - elif l > level: - # Reset unprocessed section levels. - Title.section_numbers[l] = 0 - return number - - -class FloatingTitle(Title): - '''Floated titles are translated differently.''' - @staticmethod - def isnext(): - return Title.isnext() and AttributeList.style() == 'float' - @staticmethod - def translate(): - assert Lex.next() is FloatingTitle - Title.translate() - Section.set_id() - AttributeList.consume(Title.attributes) - template = 'floatingtitle' - if template in config.sections: - stag,etag = config.section2tags(template,Title.attributes) - writer.write(stag,trace='floating title') - else: - message.warning('missing template section: [%s]' % template) - - -class Section: - """Static methods and attributes only.""" - endtags = [] # Stack of currently open section (level,endtag) tuples. - ids = [] # List of already used ids. - def __init__(self): - raise AssertionError,'no class instances allowed' - @staticmethod - def savetag(level,etag): - """Save section end.""" - Section.endtags.append((level,etag)) - @staticmethod - def setlevel(level): - """Set document level and write open section close tags up to level.""" - while Section.endtags and Section.endtags[-1][0] >= level: - writer.write(Section.endtags.pop()[1],trace='section close') - document.level = level - @staticmethod - def gen_id(title): - """ - The normalized value of the id attribute is an NCName according to - the 'Namespaces in XML' Recommendation: - NCName ::= NCNameStartChar NCNameChar* - NCNameChar ::= NameChar - ':' - NCNameStartChar ::= Letter | '_' - NameChar ::= Letter | Digit | '.' | '-' | '_' | ':' - """ - # Replace non-alpha numeric characters in title with underscores and - # convert to lower case. - base_id = re.sub(r'(?u)\W+', '_', char_decode(title)).strip('_').lower() - if 'ascii-ids' in document.attributes: - # Replace non-ASCII characters with ASCII equivalents. - import unicodedata - base_id = unicodedata.normalize('NFKD', base_id).encode('ascii','ignore') - base_id = char_encode(base_id) - # Prefix the ID name with idprefix attribute or underscore if not - # defined. Prefix ensures the ID does not clash with existing IDs. - idprefix = document.attributes.get('idprefix','_') - base_id = idprefix + base_id - i = 1 - while True: - if i == 1: - id = base_id - else: - id = '%s_%d' % (base_id, i) - if id not in Section.ids: - Section.ids.append(id) - return id - else: - id = base_id - i += 1 - @staticmethod - def set_id(): - if not document.attributes.get('sectids') is None \ - and 'id' not in AttributeList.attrs: - # Generate ids for sections. - AttributeList.attrs['id'] = Section.gen_id(Title.attributes['title']) - @staticmethod - def translate(): - assert Lex.next() is Title - prev_sectname = Title.sectname - Title.translate() - if Title.level == 0 and document.doctype != 'book': - message.error('only book doctypes can contain level 0 sections') - if Title.level > document.level \ - and 'basebackend-docbook' in document.attributes \ - and prev_sectname in ('colophon','abstract', \ - 'dedication','glossary','bibliography'): - message.error('%s section cannot contain sub-sections' % prev_sectname) - if Title.level > document.level+1: - # Sub-sections of multi-part book level zero Preface and Appendices - # are meant to be out of sequence. - if document.doctype == 'book' \ - and document.level == 0 \ - and Title.level == 2 \ - and prev_sectname in ('preface','appendix'): - pass - else: - message.warning('section title out of sequence: ' - 'expected level %d, got level %d' - % (document.level+1, Title.level)) - Section.set_id() - Section.setlevel(Title.level) - if 'numbered' in document.attributes: - Title.attributes['sectnum'] = Title.getnumber(document.level) - else: - Title.attributes['sectnum'] = '' - AttributeList.consume(Title.attributes) - stag,etag = config.section2tags(Title.sectname,Title.attributes) - Section.savetag(Title.level,etag) - writer.write(stag,trace='section open: level %d: %s' % - (Title.level, Title.attributes['title'])) - Section.translate_body() - @staticmethod - def translate_body(terminator=Title): - isempty = True - next = Lex.next() - while next and next is not terminator: - if isinstance(terminator,DelimitedBlock) and next is Title: - message.error('section title not permitted in delimited block') - next.translate() - next = Lex.next() - isempty = False - # The section is not empty if contains a subsection. - if next and isempty and Title.level > document.level: - isempty = False - # Report empty sections if invalid markup will result. - if isempty: - if document.backend == 'docbook' and Title.sectname != 'index': - message.error('empty section is not valid') - -class AbstractBlock: - - blocknames = [] # Global stack of names for push_blockname() and pop_blockname(). - - def __init__(self): - # Configuration parameter names common to all blocks. - self.CONF_ENTRIES = ('delimiter','options','subs','presubs','postsubs', - 'posattrs','style','.*-style','template','filter') - self.start = None # File reader cursor at start delimiter. - self.defname=None # Configuration file block definition section name. - # Configuration parameters. - self.delimiter=None # Regular expression matching block delimiter. - self.delimiter_reo=None # Compiled delimiter. - self.template=None # template section entry. - self.options=() # options entry list. - self.presubs=None # presubs/subs entry list. - self.postsubs=() # postsubs entry list. - self.filter=None # filter entry. - self.posattrs=() # posattrs entry list. - self.style=None # Default style. - self.styles=OrderedDict() # Each entry is a styles dictionary. - # Before a block is processed it's attributes (from it's - # attributes list) are merged with the block configuration parameters - # (by self.merge_attributes()) resulting in the template substitution - # dictionary (self.attributes) and the block's processing parameters - # (self.parameters). - self.attributes={} - # The names of block parameters. - self.PARAM_NAMES=('template','options','presubs','postsubs','filter') - self.parameters=None - # Leading delimiter match object. - self.mo=None - def short_name(self): - """ Return the text following the first dash in the section name.""" - i = self.defname.find('-') - if i == -1: - return self.defname - else: - return self.defname[i+1:] - def error(self, msg, cursor=None, halt=False): - message.error('[%s] %s' % (self.defname,msg), cursor, halt) - def is_conf_entry(self,param): - """Return True if param matches an allowed configuration file entry - name.""" - for s in self.CONF_ENTRIES: - if re.match('^'+s+'$',param): - return True - return False - def load(self,defname,entries): - """Update block definition from section 'entries' dictionary.""" - self.defname = defname - self.update_parameters(entries, self, all=True) - def update_parameters(self, src, dst=None, all=False): - """ - Parse processing parameters from src dictionary to dst object. - dst defaults to self.parameters. - If all is True then copy src entries that aren't parameter names. - """ - dst = dst or self.parameters - msg = '[%s] malformed entry %%s: %%s' % self.defname - def copy(obj,k,v): - if isinstance(obj,dict): - obj[k] = v - else: - setattr(obj,k,v) - for k,v in src.items(): - if not re.match(r'\d+',k) and not is_name(k): - raise EAsciiDoc, msg % (k,v) - if k == 'template': - if not is_name(v): - raise EAsciiDoc, msg % (k,v) - copy(dst,k,v) - elif k == 'filter': - copy(dst,k,v) - elif k == 'options': - if isinstance(v,str): - v = parse_options(v, (), msg % (k,v)) - # Merge with existing options. - v = tuple(set(dst.options).union(set(v))) - copy(dst,k,v) - elif k in ('subs','presubs','postsubs'): - # Subs is an alias for presubs. - if k == 'subs': k = 'presubs' - if isinstance(v,str): - v = parse_options(v, SUBS_OPTIONS, msg % (k,v)) - copy(dst,k,v) - elif k == 'delimiter': - if v and is_re(v): - copy(dst,k,v) - else: - raise EAsciiDoc, msg % (k,v) - elif k == 'style': - if is_name(v): - copy(dst,k,v) - else: - raise EAsciiDoc, msg % (k,v) - elif k == 'posattrs': - v = parse_options(v, (), msg % (k,v)) - copy(dst,k,v) - else: - mo = re.match(r'^(?P<style>.*)-style$',k) - if mo: - if not v: - raise EAsciiDoc, msg % (k,v) - style = mo.group('style') - if not is_name(style): - raise EAsciiDoc, msg % (k,v) - d = {} - if not parse_named_attributes(v,d): - raise EAsciiDoc, msg % (k,v) - if 'subs' in d: - # Subs is an alias for presubs. - d['presubs'] = d['subs'] - del d['subs'] - self.styles[style] = d - elif all or k in self.PARAM_NAMES: - copy(dst,k,v) # Derived class specific entries. - def get_param(self,name,params=None): - """ - Return named processing parameter from params dictionary. - If the parameter is not in params look in self.parameters. - """ - if params and name in params: - return params[name] - elif name in self.parameters: - return self.parameters[name] - else: - return None - def get_subs(self,params=None): - """ - Return (presubs,postsubs) tuple. - """ - presubs = self.get_param('presubs',params) - postsubs = self.get_param('postsubs',params) - return (presubs,postsubs) - def dump(self): - """Write block definition to stdout.""" - write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline)) - write('['+self.defname+']') - if self.is_conf_entry('delimiter'): - write('delimiter='+self.delimiter) - if self.template: - write('template='+self.template) - if self.options: - write('options='+','.join(self.options)) - if self.presubs: - if self.postsubs: - write('presubs='+','.join(self.presubs)) - else: - write('subs='+','.join(self.presubs)) - if self.postsubs: - write('postsubs='+','.join(self.postsubs)) - if self.filter: - write('filter='+self.filter) - if self.posattrs: - write('posattrs='+','.join(self.posattrs)) - if self.style: - write('style='+self.style) - if self.styles: - for style,d in self.styles.items(): - s = '' - for k,v in d.items(): s += '%s=%r,' % (k,v) - write('%s-style=%s' % (style,s[:-1])) - def validate(self): - """Validate block after the complete configuration has been loaded.""" - if self.is_conf_entry('delimiter') and not self.delimiter: - raise EAsciiDoc,'[%s] missing delimiter' % self.defname - if self.style: - if not is_name(self.style): - raise EAsciiDoc, 'illegal style name: %s' % self.style - if not self.style in self.styles: - if not isinstance(self,List): # Lists don't have templates. - message.warning('[%s] \'%s\' style not in %s' % ( - self.defname,self.style,self.styles.keys())) - # Check all styles for missing templates. - all_styles_have_template = True - for k,v in self.styles.items(): - t = v.get('template') - if t and not t in config.sections: - # Defer check if template name contains attributes. - if not re.search(r'{.+}',t): - message.warning('missing template section: [%s]' % t) - if not t: - all_styles_have_template = False - # Check we have a valid template entry or alternatively that all the - # styles have templates. - if self.is_conf_entry('template') and not 'skip' in self.options: - if self.template: - if not self.template in config.sections: - # Defer check if template name contains attributes. - if not re.search(r'{.+}',self.template): - message.warning('missing template section: [%s]' - % self.template) - elif not all_styles_have_template: - if not isinstance(self,List): # Lists don't have templates. - message.warning('missing styles templates: [%s]' % self.defname) - def isnext(self): - """Check if this block is next in document reader.""" - result = False - reader.skip_blank_lines() - if reader.read_next(): - if not self.delimiter_reo: - # Cache compiled delimiter optimization. - self.delimiter_reo = re.compile(self.delimiter) - mo = self.delimiter_reo.match(reader.read_next()) - if mo: - self.mo = mo - result = True - return result - def translate(self): - """Translate block from document reader.""" - if not self.presubs: - self.presubs = config.subsnormal - if reader.cursor: - self.start = reader.cursor[:] - def push_blockname(self, blockname=None): - ''' - On block entry set the 'blockname' attribute. - Only applies to delimited blocks, lists and tables. - ''' - if blockname is None: - blockname = self.attributes.get('style', self.short_name()).lower() - trace('push blockname', blockname) - self.blocknames.append(blockname) - document.attributes['blockname'] = blockname - def pop_blockname(self): - ''' - On block exits restore previous (parent) 'blockname' attribute or - undefine it if we're no longer inside a block. - ''' - assert len(self.blocknames) > 0 - blockname = self.blocknames.pop() - trace('pop blockname', blockname) - if len(self.blocknames) == 0: - document.attributes['blockname'] = None - else: - document.attributes['blockname'] = self.blocknames[-1] - def merge_attributes(self,attrs,params=[]): - """ - Use the current block's attribute list (attrs dictionary) to build a - dictionary of block processing parameters (self.parameters) and tag - substitution attributes (self.attributes). - - 1. Copy the default parameters (self.*) to self.parameters. - self.parameters are used internally to render the current block. - Optional params array of additional parameters. - - 2. Copy attrs to self.attributes. self.attributes are used for template - and tag substitution in the current block. - - 3. If a style attribute was specified update self.parameters with the - corresponding style parameters; if there are any style parameters - remaining add them to self.attributes (existing attribute list entries - take precedence). - - 4. Set named positional attributes in self.attributes if self.posattrs - was specified. - - 5. Finally self.parameters is updated with any corresponding parameters - specified in attrs. - - """ - - def check_array_parameter(param): - # Check the parameter is a sequence type. - if not is_array(self.parameters[param]): - message.error('malformed %s parameter: %s' % - (param, self.parameters[param])) - # Revert to default value. - self.parameters[param] = getattr(self,param) - - params = list(self.PARAM_NAMES) + params - self.attributes = {} - if self.style: - # If a default style is defined make it available in the template. - self.attributes['style'] = self.style - self.attributes.update(attrs) - # Calculate dynamic block parameters. - # Start with configuration file defaults. - self.parameters = AttrDict() - for name in params: - self.parameters[name] = getattr(self,name) - # Load the selected style attributes. - posattrs = self.posattrs - if posattrs and posattrs[0] == 'style': - # Positional attribute style has highest precedence. - style = self.attributes.get('1') - else: - style = None - if not style: - # Use explicit style attribute, fall back to default style. - style = self.attributes.get('style',self.style) - if style: - if not is_name(style): - message.error('illegal style name: %s' % style) - style = self.style - # Lists have implicit styles and do their own style checks. - elif style not in self.styles and not isinstance(self,List): - message.warning('missing style: [%s]: %s' % (self.defname,style)) - style = self.style - if style in self.styles: - self.attributes['style'] = style - for k,v in self.styles[style].items(): - if k == 'posattrs': - posattrs = v - elif k in params: - self.parameters[k] = v - elif not k in self.attributes: - # Style attributes don't take precedence over explicit. - self.attributes[k] = v - # Set named positional attributes. - for i,v in enumerate(posattrs): - if str(i+1) in self.attributes: - self.attributes[v] = self.attributes[str(i+1)] - # Override config and style attributes with attribute list attributes. - self.update_parameters(attrs) - check_array_parameter('options') - check_array_parameter('presubs') - check_array_parameter('postsubs') - -class AbstractBlocks: - """List of block definitions.""" - PREFIX = '' # Conf file section name prefix set in derived classes. - BLOCK_TYPE = None # Block type set in derived classes. - def __init__(self): - self.current=None - self.blocks = [] # List of Block objects. - self.default = None # Default Block. - self.delimiters = None # Combined delimiters regular expression. - def load(self,sections): - """Load block definition from 'sections' dictionary.""" - for k in sections.keys(): - if re.match(r'^'+ self.PREFIX + r'.+$',k): - d = {} - parse_entries(sections.get(k,()),d) - for b in self.blocks: - if b.defname == k: - break - else: - b = self.BLOCK_TYPE() - self.blocks.append(b) - try: - b.load(k,d) - except EAsciiDoc,e: - raise EAsciiDoc,'[%s] %s' % (k,str(e)) - def dump(self): - for b in self.blocks: - b.dump() - def isnext(self): - for b in self.blocks: - if b.isnext(): - self.current = b - return True; - return False - def validate(self): - """Validate the block definitions.""" - # Validate delimiters and build combined lists delimiter pattern. - delimiters = [] - for b in self.blocks: - assert b.__class__ is self.BLOCK_TYPE - b.validate() - if b.delimiter: - delimiters.append(b.delimiter) - self.delimiters = re_join(delimiters) - -class Paragraph(AbstractBlock): - def __init__(self): - AbstractBlock.__init__(self) - self.text=None # Text in first line of paragraph. - def load(self,name,entries): - AbstractBlock.load(self,name,entries) - def dump(self): - AbstractBlock.dump(self) - write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline)) - write('') - def isnext(self): - result = AbstractBlock.isnext(self) - if result: - self.text = self.mo.groupdict().get('text') - return result - def translate(self): - AbstractBlock.translate(self) - attrs = self.mo.groupdict().copy() - if 'text' in attrs: del attrs['text'] - BlockTitle.consume(attrs) - AttributeList.consume(attrs) - self.merge_attributes(attrs) - reader.read() # Discard (already parsed item first line). - body = reader.read_until(paragraphs.terminators) - if 'skip' in self.parameters.options: - return - body = [self.text] + list(body) - presubs = self.parameters.presubs - postsubs = self.parameters.postsubs - if document.attributes.get('plaintext') is None: - body = Lex.set_margin(body) # Move body to left margin. - body = Lex.subs(body,presubs) - template = self.parameters.template - template = subs_attrs(template,attrs) - stag = config.section2tags(template, self.attributes,skipend=True)[0] - if self.parameters.filter: - body = filter_lines(self.parameters.filter,body,self.attributes) - body = Lex.subs(body,postsubs) - etag = config.section2tags(template, self.attributes,skipstart=True)[1] - # Write start tag, content, end tag. - writer.write(dovetail_tags(stag,body,etag),trace='paragraph') - -class Paragraphs(AbstractBlocks): - """List of paragraph definitions.""" - BLOCK_TYPE = Paragraph - PREFIX = 'paradef-' - def __init__(self): - AbstractBlocks.__init__(self) - self.terminators=None # List of compiled re's. - def initialize(self): - self.terminators = [ - re.compile(r'^\+$|^$'), - re.compile(AttributeList.pattern), - re.compile(blocks.delimiters), - re.compile(tables.delimiters), - re.compile(tables_OLD.delimiters), - ] - def load(self,sections): - AbstractBlocks.load(self,sections) - def validate(self): - AbstractBlocks.validate(self) - # Check we have a default paragraph definition, put it last in list. - for b in self.blocks: - if b.defname == 'paradef-default': - self.blocks.append(b) - self.default = b - self.blocks.remove(b) - break - else: - raise EAsciiDoc,'missing section: [paradef-default]' - -class List(AbstractBlock): - NUMBER_STYLES= ('arabic','loweralpha','upperalpha','lowerroman', - 'upperroman') - def __init__(self): - AbstractBlock.__init__(self) - self.CONF_ENTRIES += ('type','tags') - self.PARAM_NAMES += ('tags',) - # listdef conf file parameters. - self.type=None - self.tags=None # Name of listtags-<tags> conf section. - # Calculated parameters. - self.tag=None # Current tags AttrDict. - self.label=None # List item label (labeled lists). - self.text=None # Text in first line of list item. - self.index=None # Matched delimiter 'index' group (numbered lists). - self.type=None # List type ('numbered','bulleted','labeled'). - self.ordinal=None # Current list item ordinal number (1..) - self.number_style=None # Current numbered list style ('arabic'..) - def load(self,name,entries): - AbstractBlock.load(self,name,entries) - def dump(self): - AbstractBlock.dump(self) - write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline)) - write('type='+self.type) - write('tags='+self.tags) - write('') - def validate(self): - AbstractBlock.validate(self) - tags = [self.tags] - tags += [s['tags'] for s in self.styles.values() if 'tags' in s] - for t in tags: - if t not in lists.tags: - self.error('missing section: [listtags-%s]' % t,halt=True) - def isnext(self): - result = AbstractBlock.isnext(self) - if result: - self.label = self.mo.groupdict().get('label') - self.text = self.mo.groupdict().get('text') - self.index = self.mo.groupdict().get('index') - return result - def translate_entry(self): - assert self.type == 'labeled' - entrytag = subs_tag(self.tag.entry, self.attributes) - labeltag = subs_tag(self.tag.label, self.attributes) - writer.write(entrytag[0],trace='list entry open') - writer.write(labeltag[0],trace='list label open') - # Write labels. - while Lex.next() is self: - reader.read() # Discard (already parsed item first line). - writer.write_tag(self.tag.term, [self.label], - self.presubs, self.attributes,trace='list term') - if self.text: break - writer.write(labeltag[1],trace='list label close') - # Write item text. - self.translate_item() - writer.write(entrytag[1],trace='list entry close') - def translate_item(self): - if self.type == 'callout': - self.attributes['coids'] = calloutmap.calloutids(self.ordinal) - itemtag = subs_tag(self.tag.item, self.attributes) - writer.write(itemtag[0],trace='list item open') - # Write ItemText. - text = reader.read_until(lists.terminators) - if self.text: - text = [self.text] + list(text) - if text: - writer.write_tag(self.tag.text, text, self.presubs, self.attributes,trace='list text') - # Process explicit and implicit list item continuations. - while True: - continuation = reader.read_next() == '+' - if continuation: reader.read() # Discard continuation line. - while Lex.next() in (BlockTitle,AttributeList): - # Consume continued element title and attributes. - Lex.next().translate() - if not continuation and BlockTitle.title: - # Titled elements terminate the list. - break - next = Lex.next() - if next in lists.open: - break - elif isinstance(next,List): - next.translate() - elif isinstance(next,Paragraph) and 'listelement' in next.options: - next.translate() - elif continuation: - # This is where continued elements are processed. - if next is Title: - message.error('section title not allowed in list item',halt=True) - next.translate() - else: - break - writer.write(itemtag[1],trace='list item close') - - @staticmethod - def calc_style(index): - """Return the numbered list style ('arabic'...) of the list item index. - Return None if unrecognized style.""" - if re.match(r'^\d+[\.>]$', index): - style = 'arabic' - elif re.match(r'^[ivx]+\)$', index): - style = 'lowerroman' - elif re.match(r'^[IVX]+\)$', index): - style = 'upperroman' - elif re.match(r'^[a-z]\.$', index): - style = 'loweralpha' - elif re.match(r'^[A-Z]\.$', index): - style = 'upperalpha' - else: - assert False - return style - - @staticmethod - def calc_index(index,style): - """Return the ordinal number of (1...) of the list item index - for the given list style.""" - def roman_to_int(roman): - roman = roman.lower() - digits = {'i':1,'v':5,'x':10} - result = 0 - for i in range(len(roman)): - digit = digits[roman[i]] - # If next digit is larger this digit is negative. - if i+1 < len(roman) and digits[roman[i+1]] > digit: - result -= digit - else: - result += digit - return result - index = index[:-1] - if style == 'arabic': - ordinal = int(index) - elif style == 'lowerroman': - ordinal = roman_to_int(index) - elif style == 'upperroman': - ordinal = roman_to_int(index) - elif style == 'loweralpha': - ordinal = ord(index) - ord('a') + 1 - elif style == 'upperalpha': - ordinal = ord(index) - ord('A') + 1 - else: - assert False - return ordinal - - def check_index(self): - """Check calculated self.ordinal (1,2,...) against the item number - in the document (self.index) and check the number style is the same as - the first item (self.number_style).""" - assert self.type in ('numbered','callout') - if self.index: - style = self.calc_style(self.index) - if style != self.number_style: - message.warning('list item style: expected %s got %s' % - (self.number_style,style), offset=1) - ordinal = self.calc_index(self.index,style) - if ordinal != self.ordinal: - message.warning('list item index: expected %s got %s' % - (self.ordinal,ordinal), offset=1) - - def check_tags(self): - """ Check that all necessary tags are present. """ - tags = set(Lists.TAGS) - if self.type != 'labeled': - tags = tags.difference(['entry','label','term']) - missing = tags.difference(self.tag.keys()) - if missing: - self.error('missing tag(s): %s' % ','.join(missing), halt=True) - def translate(self): - AbstractBlock.translate(self) - if self.short_name() in ('bibliography','glossary','qanda'): - message.deprecated('old %s list syntax' % self.short_name()) - lists.open.append(self) - attrs = self.mo.groupdict().copy() - for k in ('label','text','index'): - if k in attrs: del attrs[k] - if self.index: - # Set the numbering style from first list item. - attrs['style'] = self.calc_style(self.index) - BlockTitle.consume(attrs) - AttributeList.consume(attrs) - self.merge_attributes(attrs,['tags']) - self.push_blockname() - if self.type in ('numbered','callout'): - self.number_style = self.attributes.get('style') - if self.number_style not in self.NUMBER_STYLES: - message.error('illegal numbered list style: %s' % self.number_style) - # Fall back to default style. - self.attributes['style'] = self.number_style = self.style - self.tag = lists.tags[self.parameters.tags] - self.check_tags() - if 'width' in self.attributes: - # Set horizontal list 'labelwidth' and 'itemwidth' attributes. - v = str(self.attributes['width']) - mo = re.match(r'^(\d{1,2})%?$',v) - if mo: - labelwidth = int(mo.group(1)) - self.attributes['labelwidth'] = str(labelwidth) - self.attributes['itemwidth'] = str(100-labelwidth) - else: - self.error('illegal attribute value: width="%s"' % v) - stag,etag = subs_tag(self.tag.list, self.attributes) - if stag: - writer.write(stag,trace='list open') - self.ordinal = 0 - # Process list till list syntax changes or there is a new title. - while Lex.next() is self and not BlockTitle.title: - self.ordinal += 1 - document.attributes['listindex'] = str(self.ordinal) - if self.type in ('numbered','callout'): - self.check_index() - if self.type in ('bulleted','numbered','callout'): - reader.read() # Discard (already parsed item first line). - self.translate_item() - elif self.type == 'labeled': - self.translate_entry() - else: - raise AssertionError,'illegal [%s] list type' % self.defname - if etag: - writer.write(etag,trace='list close') - if self.type == 'callout': - calloutmap.validate(self.ordinal) - calloutmap.listclose() - lists.open.pop() - if len(lists.open): - document.attributes['listindex'] = str(lists.open[-1].ordinal) - self.pop_blockname() - -class Lists(AbstractBlocks): - """List of List objects.""" - BLOCK_TYPE = List - PREFIX = 'listdef-' - TYPES = ('bulleted','numbered','labeled','callout') - TAGS = ('list', 'entry','item','text', 'label','term') - def __init__(self): - AbstractBlocks.__init__(self) - self.open = [] # A stack of the current and parent lists. - self.tags={} # List tags dictionary. Each entry is a tags AttrDict. - self.terminators=None # List of compiled re's. - def initialize(self): - self.terminators = [ - re.compile(r'^\+$|^$'), - re.compile(AttributeList.pattern), - re.compile(lists.delimiters), - re.compile(blocks.delimiters), - re.compile(tables.delimiters), - re.compile(tables_OLD.delimiters), - ] - def load(self,sections): - AbstractBlocks.load(self,sections) - self.load_tags(sections) - def load_tags(self,sections): - """ - Load listtags-* conf file sections to self.tags. - """ - for section in sections.keys(): - mo = re.match(r'^listtags-(?P<name>\w+)$',section) - if mo: - name = mo.group('name') - if name in self.tags: - d = self.tags[name] - else: - d = AttrDict() - parse_entries(sections.get(section,()),d) - for k in d.keys(): - if k not in self.TAGS: - message.warning('[%s] contains illegal list tag: %s' % - (section,k)) - self.tags[name] = d - def validate(self): - AbstractBlocks.validate(self) - for b in self.blocks: - # Check list has valid type. - if not b.type in Lists.TYPES: - raise EAsciiDoc,'[%s] illegal type' % b.defname - b.validate() - def dump(self): - AbstractBlocks.dump(self) - for k,v in self.tags.items(): - dump_section('listtags-'+k, v) - - -class DelimitedBlock(AbstractBlock): - def __init__(self): - AbstractBlock.__init__(self) - def load(self,name,entries): - AbstractBlock.load(self,name,entries) - def dump(self): - AbstractBlock.dump(self) - write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline)) - write('') - def isnext(self): - return AbstractBlock.isnext(self) - def translate(self): - AbstractBlock.translate(self) - reader.read() # Discard delimiter. - self.merge_attributes(AttributeList.attrs) - if not 'skip' in self.parameters.options: - BlockTitle.consume(self.attributes) - AttributeList.consume() - self.push_blockname() - options = self.parameters.options - if 'skip' in options: - reader.read_until(self.delimiter,same_file=True) - elif safe() and self.defname == 'blockdef-backend': - message.unsafe('Backend Block') - reader.read_until(self.delimiter,same_file=True) - else: - template = self.parameters.template - template = subs_attrs(template,self.attributes) - name = self.short_name()+' block' - if 'sectionbody' in options: - # The body is treated like a section body. - stag,etag = config.section2tags(template,self.attributes) - writer.write(stag,trace=name+' open') - Section.translate_body(self) - writer.write(etag,trace=name+' close') - else: - stag = config.section2tags(template,self.attributes,skipend=True)[0] - body = reader.read_until(self.delimiter,same_file=True) - presubs = self.parameters.presubs - postsubs = self.parameters.postsubs - body = Lex.subs(body,presubs) - if self.parameters.filter: - body = filter_lines(self.parameters.filter,body,self.attributes) - body = Lex.subs(body,postsubs) - # Write start tag, content, end tag. - etag = config.section2tags(template,self.attributes,skipstart=True)[1] - writer.write(dovetail_tags(stag,body,etag),trace=name) - trace(self.short_name()+' block close',etag) - if reader.eof(): - self.error('missing closing delimiter',self.start) - else: - delimiter = reader.read() # Discard delimiter line. - assert re.match(self.delimiter,delimiter) - self.pop_blockname() - -class DelimitedBlocks(AbstractBlocks): - """List of delimited blocks.""" - BLOCK_TYPE = DelimitedBlock - PREFIX = 'blockdef-' - def __init__(self): - AbstractBlocks.__init__(self) - def load(self,sections): - """Update blocks defined in 'sections' dictionary.""" - AbstractBlocks.load(self,sections) - def validate(self): - AbstractBlocks.validate(self) - -class Column: - """Table column.""" - def __init__(self, width=None, align_spec=None, style=None): - self.width = width or '1' - self.halign, self.valign = Table.parse_align_spec(align_spec) - self.style = style # Style name or None. - # Calculated attribute values. - self.abswidth = None # 1.. (page units). - self.pcwidth = None # 1..99 (percentage). - -class Cell: - def __init__(self, data, span_spec=None, align_spec=None, style=None): - self.data = data - self.span, self.vspan = Table.parse_span_spec(span_spec) - self.halign, self.valign = Table.parse_align_spec(align_spec) - self.style = style - self.reserved = False - def __repr__(self): - return '<Cell: %d.%d %s.%s %s "%s">' % ( - self.span, self.vspan, - self.halign, self.valign, - self.style or '', - self.data) - def clone_reserve(self): - """Return a clone of self to reserve vertically spanned cell.""" - result = copy.copy(self) - result.vspan = 1 - result.reserved = True - return result - -class Table(AbstractBlock): - ALIGN = {'<':'left', '>':'right', '^':'center'} - VALIGN = {'<':'top', '>':'bottom', '^':'middle'} - FORMATS = ('psv','csv','dsv') - SEPARATORS = dict( - csv=',', - dsv=r':|\n', - # The count and align group matches are not exact. - psv=r'((?<!\S)((?P<span>[\d.]+)(?P<op>[*+]))?(?P<align>[<\^>.]{,3})?(?P<style>[a-z])?)?\|' - ) - def __init__(self): - AbstractBlock.__init__(self) - self.CONF_ENTRIES += ('format','tags','separator') - # tabledef conf file parameters. - self.format='psv' - self.separator=None - self.tags=None # Name of tabletags-<tags> conf section. - # Calculated parameters. - self.abswidth=None # 1.. (page units). - self.pcwidth = None # 1..99 (percentage). - self.rows=[] # Parsed rows, each row is a list of Cells. - self.columns=[] # List of Columns. - @staticmethod - def parse_align_spec(align_spec): - """ - Parse AsciiDoc cell alignment specifier and return 2-tuple with - horizonatal and vertical alignment names. Unspecified alignments - set to None. - """ - result = (None, None) - if align_spec: - mo = re.match(r'^([<\^>])?(\.([<\^>]))?$', align_spec) - if mo: - result = (Table.ALIGN.get(mo.group(1)), - Table.VALIGN.get(mo.group(3))) - return result - @staticmethod - def parse_span_spec(span_spec): - """ - Parse AsciiDoc cell span specifier and return 2-tuple with horizonatal - and vertical span counts. Set default values (1,1) if not - specified. - """ - result = (None, None) - if span_spec: - mo = re.match(r'^(\d+)?(\.(\d+))?$', span_spec) - if mo: - result = (mo.group(1) and int(mo.group(1)), - mo.group(3) and int(mo.group(3))) - return (result[0] or 1, result[1] or 1) - def load(self,name,entries): - AbstractBlock.load(self,name,entries) - def dump(self): - AbstractBlock.dump(self) - write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline)) - write('format='+self.format) - write('') - def validate(self): - AbstractBlock.validate(self) - if self.format not in Table.FORMATS: - self.error('illegal format=%s' % self.format,halt=True) - self.tags = self.tags or 'default' - tags = [self.tags] - tags += [s['tags'] for s in self.styles.values() if 'tags' in s] - for t in tags: - if t not in tables.tags: - self.error('missing section: [tabletags-%s]' % t,halt=True) - if self.separator: - # Evaluate escape characters. - self.separator = literal_eval('"'+self.separator+'"') - #TODO: Move to class Tables - # Check global table parameters. - elif config.pagewidth is None: - self.error('missing [miscellaneous] entry: pagewidth') - elif config.pageunits is None: - self.error('missing [miscellaneous] entry: pageunits') - def validate_attributes(self): - """Validate and parse table attributes.""" - # Set defaults. - format = self.format - tags = self.tags - separator = self.separator - abswidth = float(config.pagewidth) - pcwidth = 100.0 - for k,v in self.attributes.items(): - if k == 'format': - if v not in self.FORMATS: - self.error('illegal %s=%s' % (k,v)) - else: - format = v - elif k == 'tags': - if v not in tables.tags: - self.error('illegal %s=%s' % (k,v)) - else: - tags = v - elif k == 'separator': - separator = v - elif k == 'width': - if not re.match(r'^\d{1,3}%$',v) or int(v[:-1]) > 100: - self.error('illegal %s=%s' % (k,v)) - else: - abswidth = float(v[:-1])/100 * config.pagewidth - pcwidth = float(v[:-1]) - # Calculate separator if it has not been specified. - if not separator: - separator = Table.SEPARATORS[format] - if format == 'csv': - if len(separator) > 1: - self.error('illegal csv separator=%s' % separator) - separator = ',' - else: - if not is_re(separator): - self.error('illegal regular expression: separator=%s' % - separator) - self.parameters.format = format - self.parameters.tags = tags - self.parameters.separator = separator - self.abswidth = abswidth - self.pcwidth = pcwidth - def get_tags(self,params): - tags = self.get_param('tags',params) - assert(tags and tags in tables.tags) - return tables.tags[tags] - def get_style(self,prefix): - """ - Return the style dictionary whose name starts with 'prefix'. - """ - if prefix is None: - return None - names = self.styles.keys() - names.sort() - for name in names: - if name.startswith(prefix): - return self.styles[name] - else: - self.error('missing style: %s*' % prefix) - return None - def parse_cols(self, cols, halign, valign): - """ - Build list of column objects from table 'cols', 'halign' and 'valign' - attributes. - """ - # [<multiplier>*][<align>][<width>][<style>] - COLS_RE1 = r'^((?P<count>\d+)\*)?(?P<align>[<\^>.]{,3})?(?P<width>\d+%?)?(?P<style>[a-z]\w*)?$' - # [<multiplier>*][<width>][<align>][<style>] - COLS_RE2 = r'^((?P<count>\d+)\*)?(?P<width>\d+%?)?(?P<align>[<\^>.]{,3})?(?P<style>[a-z]\w*)?$' - reo1 = re.compile(COLS_RE1) - reo2 = re.compile(COLS_RE2) - cols = str(cols) - if re.match(r'^\d+$',cols): - for i in range(int(cols)): - self.columns.append(Column()) - else: - for col in re.split(r'\s*,\s*',cols): - mo = reo1.match(col) - if not mo: - mo = reo2.match(col) - if mo: - count = int(mo.groupdict().get('count') or 1) - for i in range(count): - self.columns.append( - Column(mo.group('width'), mo.group('align'), - self.get_style(mo.group('style'))) - ) - else: - self.error('illegal column spec: %s' % col,self.start) - # Set column (and indirectly cell) default alignments. - for col in self.columns: - col.halign = col.halign or halign or document.attributes.get('halign') or 'left' - col.valign = col.valign or valign or document.attributes.get('valign') or 'top' - # Validate widths and calculate missing widths. - n = 0; percents = 0; props = 0 - for col in self.columns: - if col.width: - if col.width[-1] == '%': percents += int(col.width[:-1]) - else: props += int(col.width) - n += 1 - if percents > 0 and props > 0: - self.error('mixed percent and proportional widths: %s' - % cols,self.start) - pcunits = percents > 0 - # Fill in missing widths. - if n < len(self.columns) and percents < 100: - if pcunits: - width = float(100 - percents)/float(len(self.columns) - n) - else: - width = 1 - for col in self.columns: - if not col.width: - if pcunits: - col.width = str(int(width))+'%' - percents += width - else: - col.width = str(width) - props += width - # Calculate column alignment and absolute and percent width values. - percents = 0 - for col in self.columns: - if pcunits: - col.pcwidth = float(col.width[:-1]) - else: - col.pcwidth = (float(col.width)/props)*100 - col.abswidth = self.abswidth * (col.pcwidth/100) - if config.pageunits in ('cm','mm','in','em'): - col.abswidth = '%.2f' % round(col.abswidth,2) - else: - col.abswidth = '%d' % round(col.abswidth) - percents += col.pcwidth - col.pcwidth = int(col.pcwidth) - if round(percents) > 100: - self.error('total width exceeds 100%%: %s' % cols,self.start) - elif round(percents) < 100: - self.error('total width less than 100%%: %s' % cols,self.start) - def build_colspecs(self): - """ - Generate column related substitution attributes. - """ - cols = [] - i = 1 - for col in self.columns: - colspec = self.get_tags(col.style).colspec - if colspec: - self.attributes['halign'] = col.halign - self.attributes['valign'] = col.valign - self.attributes['colabswidth'] = col.abswidth - self.attributes['colpcwidth'] = col.pcwidth - self.attributes['colnumber'] = str(i) - s = subs_attrs(colspec, self.attributes) - if not s: - message.warning('colspec dropped: contains undefined attribute') - else: - cols.append(s) - i += 1 - if cols: - self.attributes['colspecs'] = writer.newline.join(cols) - def parse_rows(self, text): - """ - Parse the table source text into self.rows (a list of rows, each row - is a list of Cells. - """ - reserved = {} # Reserved cells generated by rowspans. - if self.parameters.format in ('psv','dsv'): - colcount = len(self.columns) - parsed_cells = self.parse_psv_dsv(text) - ri = 0 # Current row index 0.. - ci = 0 # Column counter 0..colcount - row = [] - i = 0 - while True: - resv = reserved.get(ri) and reserved[ri].get(ci) - if resv: - # We have a cell generated by a previous row span so - # process it before continuing with the current parsed - # cell. - cell = resv - else: - if i >= len(parsed_cells): - break # No more parsed or reserved cells. - cell = parsed_cells[i] - i += 1 - if cell.vspan > 1: - # Generate ensuing reserved cells spanned vertically by - # the current cell. - for j in range(1, cell.vspan): - if not ri+j in reserved: - reserved[ri+j] = {} - reserved[ri+j][ci] = cell.clone_reserve() - ci += cell.span - if ci <= colcount: - row.append(cell) - if ci >= colcount: - self.rows.append(row) - ri += 1 - row = [] - ci = 0 - elif self.parameters.format == 'csv': - self.rows = self.parse_csv(text) - else: - assert True,'illegal table format' - # Check for empty rows containing only reserved (spanned) cells. - for ri,row in enumerate(self.rows): - empty = True - for cell in row: - if not cell.reserved: - empty = False - break - if empty: - message.warning('table row %d: empty spanned row' % (ri+1)) - # Check that all row spans match. - for ri,row in enumerate(self.rows): - row_span = 0 - for cell in row: - row_span += cell.span - if ri == 0: - header_span = row_span - if row_span < header_span: - message.warning('table row %d: does not span all columns' % (ri+1)) - if row_span > header_span: - message.warning('table row %d: exceeds columns span' % (ri+1)) - def subs_rows(self, rows, rowtype='body'): - """ - Return a string of output markup from a list of rows, each row - is a list of raw data text. - """ - tags = tables.tags[self.parameters.tags] - if rowtype == 'header': - rtag = tags.headrow - elif rowtype == 'footer': - rtag = tags.footrow - else: - rtag = tags.bodyrow - result = [] - stag,etag = subs_tag(rtag,self.attributes) - for row in rows: - result.append(stag) - result += self.subs_row(row,rowtype) - result.append(etag) - return writer.newline.join(result) - def subs_row(self, row, rowtype): - """ - Substitute the list of Cells using the data tag. - Returns a list of marked up table cell elements. - """ - result = [] - i = 0 - for cell in row: - if cell.reserved: - # Skip vertically spanned placeholders. - i += cell.span - continue - if i >= len(self.columns): - break # Skip cells outside the header width. - col = self.columns[i] - self.attributes['halign'] = cell.halign or col.halign - self.attributes['valign'] = cell.valign or col.valign - self.attributes['colabswidth'] = col.abswidth - self.attributes['colpcwidth'] = col.pcwidth - self.attributes['colnumber'] = str(i+1) - self.attributes['colspan'] = str(cell.span) - self.attributes['colstart'] = self.attributes['colnumber'] - self.attributes['colend'] = str(i+cell.span) - self.attributes['rowspan'] = str(cell.vspan) - self.attributes['morerows'] = str(cell.vspan-1) - # Fill missing column data with blanks. - if i > len(self.columns) - 1: - data = '' - else: - data = cell.data - if rowtype == 'header': - # Use table style unless overriden by cell style. - colstyle = cell.style - else: - # If the cell style is not defined use the column style. - colstyle = cell.style or col.style - tags = self.get_tags(colstyle) - presubs,postsubs = self.get_subs(colstyle) - data = [data] - data = Lex.subs(data, presubs) - data = filter_lines(self.get_param('filter',colstyle), - data, self.attributes) - data = Lex.subs(data, postsubs) - if rowtype != 'header': - ptag = tags.paragraph - if ptag: - stag,etag = subs_tag(ptag,self.attributes) - text = '\n'.join(data).strip() - data = [] - for para in re.split(r'\n{2,}',text): - data += dovetail_tags([stag],para.split('\n'),[etag]) - if rowtype == 'header': - dtag = tags.headdata - elif rowtype == 'footer': - dtag = tags.footdata - else: - dtag = tags.bodydata - stag,etag = subs_tag(dtag,self.attributes) - result = result + dovetail_tags([stag],data,[etag]) - i += cell.span - return result - def parse_csv(self,text): - """ - Parse the table source text and return a list of rows, each row - is a list of Cells. - """ - import StringIO - import csv - rows = [] - rdr = csv.reader(StringIO.StringIO('\r\n'.join(text)), - delimiter=self.parameters.separator, skipinitialspace=True) - try: - for row in rdr: - rows.append([Cell(data) for data in row]) - except Exception: - self.error('csv parse error: %s' % row) - return rows - def parse_psv_dsv(self,text): - """ - Parse list of PSV or DSV table source text lines and return a list of - Cells. - """ - def append_cell(data, span_spec, op, align_spec, style): - op = op or '+' - if op == '*': # Cell multiplier. - span = Table.parse_span_spec(span_spec)[0] - for i in range(span): - cells.append(Cell(data, '1', align_spec, style)) - elif op == '+': # Column spanner. - cells.append(Cell(data, span_spec, align_spec, style)) - else: - self.error('illegal table cell operator') - text = '\n'.join(text) - separator = '(?msu)'+self.parameters.separator - format = self.parameters.format - start = 0 - span = None - op = None - align = None - style = None - cells = [] - data = '' - for mo in re.finditer(separator,text): - data += text[start:mo.start()] - if data.endswith('\\'): - data = data[:-1]+mo.group() # Reinstate escaped separators. - else: - append_cell(data, span, op, align, style) - span = mo.groupdict().get('span') - op = mo.groupdict().get('op') - align = mo.groupdict().get('align') - style = mo.groupdict().get('style') - if style: - style = self.get_style(style) - data = '' - start = mo.end() - # Last cell follows final separator. - data += text[start:] - append_cell(data, span, op, align, style) - # We expect a dummy blank item preceeding first PSV cell. - if format == 'psv': - if cells[0].data.strip() != '': - self.error('missing leading separator: %s' % separator, - self.start) - else: - cells.pop(0) - return cells - def translate(self): - AbstractBlock.translate(self) - reader.read() # Discard delimiter. - # Reset instance specific properties. - self.columns = [] - self.rows = [] - attrs = {} - BlockTitle.consume(attrs) - # Mix in document attribute list. - AttributeList.consume(attrs) - self.merge_attributes(attrs) - self.validate_attributes() - # Add global and calculated configuration parameters. - self.attributes['pagewidth'] = config.pagewidth - self.attributes['pageunits'] = config.pageunits - self.attributes['tableabswidth'] = int(self.abswidth) - self.attributes['tablepcwidth'] = int(self.pcwidth) - # Read the entire table. - text = reader.read_until(self.delimiter) - if reader.eof(): - self.error('missing closing delimiter',self.start) - else: - delimiter = reader.read() # Discard closing delimiter. - assert re.match(self.delimiter,delimiter) - if len(text) == 0: - message.warning('[%s] table is empty' % self.defname) - return - self.push_blockname('table') - cols = attrs.get('cols') - if not cols: - # Calculate column count from number of items in first line. - if self.parameters.format == 'csv': - cols = text[0].count(self.parameters.separator) + 1 - else: - cols = 0 - for cell in self.parse_psv_dsv(text[:1]): - cols += cell.span - self.parse_cols(cols, attrs.get('halign'), attrs.get('valign')) - # Set calculated attributes. - self.attributes['colcount'] = len(self.columns) - self.build_colspecs() - self.parse_rows(text) - # The 'rowcount' attribute is used by the experimental LaTeX backend. - self.attributes['rowcount'] = str(len(self.rows)) - # Generate headrows, footrows, bodyrows. - # Headrow, footrow and bodyrow data replaces same named attributes in - # the table markup template. In order to ensure this data does not get - # a second attribute substitution (which would interfere with any - # already substituted inline passthroughs) unique placeholders are used - # (the tab character does not appear elsewhere since it is expanded on - # input) which are replaced after template attribute substitution. - headrows = footrows = bodyrows = None - if self.rows and 'header' in self.parameters.options: - headrows = self.subs_rows(self.rows[0:1],'header') - self.attributes['headrows'] = '\x07headrows\x07' - self.rows = self.rows[1:] - if self.rows and 'footer' in self.parameters.options: - footrows = self.subs_rows( self.rows[-1:], 'footer') - self.attributes['footrows'] = '\x07footrows\x07' - self.rows = self.rows[:-1] - if self.rows: - bodyrows = self.subs_rows(self.rows) - self.attributes['bodyrows'] = '\x07bodyrows\x07' - table = subs_attrs(config.sections[self.parameters.template], - self.attributes) - table = writer.newline.join(table) - # Before we finish replace the table head, foot and body place holders - # with the real data. - if headrows: - table = table.replace('\x07headrows\x07', headrows, 1) - if footrows: - table = table.replace('\x07footrows\x07', footrows, 1) - if bodyrows: - table = table.replace('\x07bodyrows\x07', bodyrows, 1) - writer.write(table,trace='table') - self.pop_blockname() - -class Tables(AbstractBlocks): - """List of tables.""" - BLOCK_TYPE = Table - PREFIX = 'tabledef-' - TAGS = ('colspec', 'headrow','footrow','bodyrow', - 'headdata','footdata', 'bodydata','paragraph') - def __init__(self): - AbstractBlocks.__init__(self) - # Table tags dictionary. Each entry is a tags dictionary. - self.tags={} - def load(self,sections): - AbstractBlocks.load(self,sections) - self.load_tags(sections) - def load_tags(self,sections): - """ - Load tabletags-* conf file sections to self.tags. - """ - for section in sections.keys(): - mo = re.match(r'^tabletags-(?P<name>\w+)$',section) - if mo: - name = mo.group('name') - if name in self.tags: - d = self.tags[name] - else: - d = AttrDict() - parse_entries(sections.get(section,()),d) - for k in d.keys(): - if k not in self.TAGS: - message.warning('[%s] contains illegal table tag: %s' % - (section,k)) - self.tags[name] = d - def validate(self): - AbstractBlocks.validate(self) - # Check we have a default table definition, - for i in range(len(self.blocks)): - if self.blocks[i].defname == 'tabledef-default': - default = self.blocks[i] - break - else: - raise EAsciiDoc,'missing section: [tabledef-default]' - # Propagate defaults to unspecified table parameters. - for b in self.blocks: - if b is not default: - if b.format is None: b.format = default.format - if b.template is None: b.template = default.template - # Check tags and propagate default tags. - if not 'default' in self.tags: - raise EAsciiDoc,'missing section: [tabletags-default]' - default = self.tags['default'] - for tag in ('bodyrow','bodydata','paragraph'): # Mandatory default tags. - if tag not in default: - raise EAsciiDoc,'missing [tabletags-default] entry: %s' % tag - for t in self.tags.values(): - if t is not default: - if t.colspec is None: t.colspec = default.colspec - if t.headrow is None: t.headrow = default.headrow - if t.footrow is None: t.footrow = default.footrow - if t.bodyrow is None: t.bodyrow = default.bodyrow - if t.headdata is None: t.headdata = default.headdata - if t.footdata is None: t.footdata = default.footdata - if t.bodydata is None: t.bodydata = default.bodydata - if t.paragraph is None: t.paragraph = default.paragraph - # Use body tags if header and footer tags are not specified. - for t in self.tags.values(): - if not t.headrow: t.headrow = t.bodyrow - if not t.footrow: t.footrow = t.bodyrow - if not t.headdata: t.headdata = t.bodydata - if not t.footdata: t.footdata = t.bodydata - # Check table definitions are valid. - for b in self.blocks: - b.validate() - def dump(self): - AbstractBlocks.dump(self) - for k,v in self.tags.items(): - dump_section('tabletags-'+k, v) - -class Macros: - # Default system macro syntax. - SYS_RE = r'(?u)^(?P<name>[\\]?\w(\w|-)*?)::(?P<target>\S*?)' + \ - r'(\[(?P<attrlist>.*?)\])$' - def __init__(self): - self.macros = [] # List of Macros. - self.current = None # The last matched block macro. - self.passthroughs = [] - # Initialize default system macro. - m = Macro() - m.pattern = self.SYS_RE - m.prefix = '+' - m.reo = re.compile(m.pattern) - self.macros.append(m) - def load(self,entries): - for entry in entries: - m = Macro() - m.load(entry) - if m.name is None: - # Delete undefined macro. - for i,m2 in enumerate(self.macros): - if m2.pattern == m.pattern: - del self.macros[i] - break - else: - message.warning('unable to delete missing macro: %s' % m.pattern) - else: - # Check for duplicates. - for m2 in self.macros: - if m2.pattern == m.pattern: - message.verbose('macro redefinition: %s%s' % (m.prefix,m.name)) - break - else: - self.macros.append(m) - def dump(self): - write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline)) - write('[macros]') - # Dump all macros except the first (built-in system) macro. - for m in self.macros[1:]: - # Escape = in pattern. - macro = '%s=%s%s' % (m.pattern.replace('=',r'\='), m.prefix, m.name) - if m.subslist is not None: - macro += '[' + ','.join(m.subslist) + ']' - write(macro) - write('') - def validate(self): - # Check all named sections exist. - if config.verbose: - for m in self.macros: - if m.name and m.prefix != '+': - m.section_name() - def subs(self,text,prefix='',callouts=False): - # If callouts is True then only callout macros are processed, if False - # then all non-callout macros are processed. - result = text - for m in self.macros: - if m.prefix == prefix: - if callouts ^ (m.name != 'callout'): - result = m.subs(result) - return result - def isnext(self): - """Return matching macro if block macro is next on reader.""" - reader.skip_blank_lines() - line = reader.read_next() - if line: - for m in self.macros: - if m.prefix == '#': - if m.reo.match(line): - self.current = m - return m - return False - def match(self,prefix,name,text): - """Return re match object matching 'text' with macro type 'prefix', - macro name 'name'.""" - for m in self.macros: - if m.prefix == prefix: - mo = m.reo.match(text) - if mo: - if m.name == name: - return mo - if re.match(name, mo.group('name')): - return mo - return None - def extract_passthroughs(self,text,prefix=''): - """ Extract the passthrough text and replace with temporary - placeholders.""" - self.passthroughs = [] - for m in self.macros: - if m.has_passthrough() and m.prefix == prefix: - text = m.subs_passthroughs(text, self.passthroughs) - return text - def restore_passthroughs(self,text): - """ Replace passthough placeholders with the original passthrough - text.""" - for i,v in enumerate(self.passthroughs): - text = text.replace('\x07'+str(i)+'\x07', self.passthroughs[i]) - return text - -class Macro: - def __init__(self): - self.pattern = None # Matching regular expression. - self.name = '' # Conf file macro name (None if implicit). - self.prefix = '' # '' if inline, '+' if system, '#' if block. - self.reo = None # Compiled pattern re object. - self.subslist = [] # Default subs for macros passtext group. - def has_passthrough(self): - return self.pattern.find(r'(?P<passtext>') >= 0 - def section_name(self,name=None): - """Return macro markup template section name based on macro name and - prefix. Return None section not found.""" - assert self.prefix != '+' - if not name: - assert self.name - name = self.name - if self.prefix == '#': - suffix = '-blockmacro' - else: - suffix = '-inlinemacro' - if name+suffix in config.sections: - return name+suffix - else: - message.warning('missing macro section: [%s]' % (name+suffix)) - return None - def load(self,entry): - e = parse_entry(entry) - if e is None: - # Only the macro pattern was specified, mark for deletion. - self.name = None - self.pattern = entry - return - if not is_re(e[0]): - raise EAsciiDoc,'illegal macro regular expression: %s' % e[0] - pattern, name = e - if name and name[0] in ('+','#'): - prefix, name = name[0], name[1:] - else: - prefix = '' - # Parse passthrough subslist. - mo = re.match(r'^(?P<name>[^[]*)(\[(?P<subslist>.*)\])?$', name) - name = mo.group('name') - if name and not is_name(name): - raise EAsciiDoc,'illegal section name in macro entry: %s' % entry - subslist = mo.group('subslist') - if subslist is not None: - # Parse and validate passthrough subs. - subslist = parse_options(subslist, SUBS_OPTIONS, - 'illegal subs in macro entry: %s' % entry) - self.pattern = pattern - self.reo = re.compile(pattern) - self.prefix = prefix - self.name = name - self.subslist = subslist or [] - - def subs(self,text): - def subs_func(mo): - """Function called to perform macro substitution. - Uses matched macro regular expression object and returns string - containing the substituted macro body.""" - # Check if macro reference is escaped. - if mo.group()[0] == '\\': - return mo.group()[1:] # Strip leading backslash. - d = mo.groupdict() - # Delete groups that didn't participate in match. - for k,v in d.items(): - if v is None: del d[k] - if self.name: - name = self.name - else: - if not 'name' in d: - message.warning('missing macro name group: %s' % mo.re.pattern) - return '' - name = d['name'] - section_name = self.section_name(name) - if not section_name: - return '' - # If we're dealing with a block macro get optional block ID and - # block title. - if self.prefix == '#' and self.name != 'comment': - AttributeList.consume(d) - BlockTitle.consume(d) - # Parse macro attributes. - if 'attrlist' in d: - if d['attrlist'] in (None,''): - del d['attrlist'] - else: - if self.prefix == '': - # Unescape ] characters in inline macros. - d['attrlist'] = d['attrlist'].replace('\\]',']') - parse_attributes(d['attrlist'],d) - # Generate option attributes. - if 'options' in d: - options = parse_options(d['options'], (), - '%s: illegal option name' % name) - for option in options: - d[option+'-option'] = '' - # Substitute single quoted attribute values in block macros. - if self.prefix == '#': - AttributeList.subs(d) - if name == 'callout': - listindex =int(d['index']) - d['coid'] = calloutmap.add(listindex) - # The alt attribute is the first image macro positional attribute. - if name == 'image' and '1' in d: - d['alt'] = d['1'] - # Unescape special characters in LaTeX target file names. - if document.backend == 'latex' and 'target' in d and d['target']: - if not '0' in d: - d['0'] = d['target'] - d['target']= config.subs_specialchars_reverse(d['target']) - # BUG: We've already done attribute substitution on the macro which - # means that any escaped attribute references are now unescaped and - # will be substituted by config.subs_section() below. As a partial - # fix have withheld {0} from substitution but this kludge doesn't - # fix it for other attributes containing unescaped references. - # Passthrough macros don't have this problem. - a0 = d.get('0') - if a0: - d['0'] = chr(0) # Replace temporarily with unused character. - body = config.subs_section(section_name,d) - if len(body) == 0: - result = '' - elif len(body) == 1: - result = body[0] - else: - if self.prefix == '#': - result = writer.newline.join(body) - else: - # Internally processed inline macros use UNIX line - # separator. - result = '\n'.join(body) - if a0: - result = result.replace(chr(0), a0) - return result - - return self.reo.sub(subs_func, text) - - def translate(self): - """ Block macro translation.""" - assert self.prefix == '#' - s = reader.read() - before = s - if self.has_passthrough(): - s = macros.extract_passthroughs(s,'#') - s = subs_attrs(s) - if s: - s = self.subs(s) - if self.has_passthrough(): - s = macros.restore_passthroughs(s) - if s: - trace('macro block',before,s) - writer.write(s) - - def subs_passthroughs(self, text, passthroughs): - """ Replace macro attribute lists in text with placeholders. - Substitute and append the passthrough attribute lists to the - passthroughs list.""" - def subs_func(mo): - """Function called to perform inline macro substitution. - Uses matched macro regular expression object and returns string - containing the substituted macro body.""" - # Don't process escaped macro references. - if mo.group()[0] == '\\': - return mo.group() - d = mo.groupdict() - if not 'passtext' in d: - message.warning('passthrough macro %s: missing passtext group' % - d.get('name','')) - return mo.group() - passtext = d['passtext'] - if re.search('\x07\\d+\x07', passtext): - message.warning('nested inline passthrough') - return mo.group() - if d.get('subslist'): - if d['subslist'].startswith(':'): - message.error('block macro cannot occur here: %s' % mo.group(), - halt=True) - subslist = parse_options(d['subslist'], SUBS_OPTIONS, - 'illegal passthrough macro subs option') - else: - subslist = self.subslist - passtext = Lex.subs_1(passtext,subslist) - if passtext is None: passtext = '' - if self.prefix == '': - # Unescape ] characters in inline macros. - passtext = passtext.replace('\\]',']') - passthroughs.append(passtext) - # Tabs guarantee the placeholders are unambiguous. - result = ( - text[mo.start():mo.start('passtext')] + - '\x07' + str(len(passthroughs)-1) + '\x07' + - text[mo.end('passtext'):mo.end()] - ) - return result - - return self.reo.sub(subs_func, text) - - -class CalloutMap: - def __init__(self): - self.comap = {} # key = list index, value = callouts list. - self.calloutindex = 0 # Current callout index number. - self.listnumber = 1 # Current callout list number. - def listclose(self): - # Called when callout list is closed. - self.listnumber += 1 - self.calloutindex = 0 - self.comap = {} - def add(self,listindex): - # Add next callout index to listindex map entry. Return the callout id. - self.calloutindex += 1 - # Append the coindex to a list in the comap dictionary. - if not listindex in self.comap: - self.comap[listindex] = [self.calloutindex] - else: - self.comap[listindex].append(self.calloutindex) - return self.calloutid(self.listnumber, self.calloutindex) - @staticmethod - def calloutid(listnumber,calloutindex): - return 'CO%d-%d' % (listnumber,calloutindex) - def calloutids(self,listindex): - # Retieve list of callout indexes that refer to listindex. - if listindex in self.comap: - result = '' - for coindex in self.comap[listindex]: - result += ' ' + self.calloutid(self.listnumber,coindex) - return result.strip() - else: - message.warning('no callouts refer to list item '+str(listindex)) - return '' - def validate(self,maxlistindex): - # Check that all list indexes referenced by callouts exist. - for listindex in self.comap.keys(): - if listindex > maxlistindex: - message.warning('callout refers to non-existent list item ' - + str(listindex)) - -#--------------------------------------------------------------------------- -# Input stream Reader and output stream writer classes. -#--------------------------------------------------------------------------- - -UTF8_BOM = '\xef\xbb\xbf' - -class Reader1: - """Line oriented AsciiDoc input file reader. Processes include and - conditional inclusion system macros. Tabs are expanded and lines are right - trimmed.""" - # This class is not used directly, use Reader class instead. - READ_BUFFER_MIN = 10 # Read buffer low level. - def __init__(self): - self.f = None # Input file object. - self.fname = None # Input file name. - self.next = [] # Read ahead buffer containing - # [filename,linenumber,linetext] lists. - self.cursor = None # Last read() [filename,linenumber,linetext]. - self.tabsize = 8 # Tab expansion number of spaces. - self.parent = None # Included reader's parent reader. - self._lineno = 0 # The last line read from file object f. - self.current_depth = 0 # Current include depth. - self.max_depth = 10 # Initial maxiumum allowed include depth. - self.bom = None # Byte order mark (BOM). - self.infile = None # Saved document 'infile' attribute. - self.indir = None # Saved document 'indir' attribute. - def open(self,fname): - self.fname = fname - message.verbose('reading: '+fname) - if fname == '<stdin>': - self.f = sys.stdin - self.infile = None - self.indir = None - else: - self.f = open(fname,'rb') - self.infile = fname - self.indir = os.path.dirname(fname) - document.attributes['infile'] = self.infile - document.attributes['indir'] = self.indir - self._lineno = 0 # The last line read from file object f. - self.next = [] - # Prefill buffer by reading the first line and then pushing it back. - if Reader1.read(self): - if self.cursor[2].startswith(UTF8_BOM): - self.cursor[2] = self.cursor[2][len(UTF8_BOM):] - self.bom = UTF8_BOM - self.unread(self.cursor) - self.cursor = None - def closefile(self): - """Used by class methods to close nested include files.""" - self.f.close() - self.next = [] - def close(self): - self.closefile() - self.__init__() - def read(self, skip=False): - """Read next line. Return None if EOF. Expand tabs. Strip trailing - white space. Maintain self.next read ahead buffer. If skip=True then - conditional exclusion is active (ifdef and ifndef macros).""" - # Top up buffer. - if len(self.next) <= self.READ_BUFFER_MIN: - s = self.f.readline() - if s: - self._lineno = self._lineno + 1 - while s: - if self.tabsize != 0: - s = s.expandtabs(self.tabsize) - s = s.rstrip() - self.next.append([self.fname,self._lineno,s]) - if len(self.next) > self.READ_BUFFER_MIN: - break - s = self.f.readline() - if s: - self._lineno = self._lineno + 1 - # Return first (oldest) buffer entry. - if len(self.next) > 0: - self.cursor = self.next[0] - del self.next[0] - result = self.cursor[2] - # Check for include macro. - mo = macros.match('+',r'^include[1]?$',result) - if mo and not skip: - # Parse include macro attributes. - attrs = {} - parse_attributes(mo.group('attrlist'),attrs) - warnings = attrs.get('warnings', True) - # Don't process include macro once the maximum depth is reached. - if self.current_depth >= self.max_depth: - message.warning('maximum include depth exceeded') - return result - # Perform attribute substitution on include macro file name. - fname = subs_attrs(mo.group('target')) - if not fname: - return Reader1.read(self) # Return next input line. - if self.fname != '<stdin>': - fname = os.path.expandvars(os.path.expanduser(fname)) - fname = safe_filename(fname, os.path.dirname(self.fname)) - if not fname: - return Reader1.read(self) # Return next input line. - if not os.path.isfile(fname): - if warnings: - message.warning('include file not found: %s' % fname) - return Reader1.read(self) # Return next input line. - if mo.group('name') == 'include1': - if not config.dumping: - if fname not in config.include1: - message.verbose('include1: ' + fname, linenos=False) - # Store the include file in memory for later - # retrieval by the {include1:} system attribute. - f = open(fname) - try: - config.include1[fname] = [ - s.rstrip() for s in f] - finally: - f.close() - return '{include1:%s}' % fname - else: - # This is a configuration dump, just pass the macro - # call through. - return result - # Clone self and set as parent (self assumes the role of child). - parent = Reader1() - assign(parent,self) - self.parent = parent - # Set attributes in child. - if 'tabsize' in attrs: - try: - val = int(attrs['tabsize']) - if not val >= 0: - raise ValueError, 'not >= 0' - self.tabsize = val - except ValueError: - raise EAsciiDoc, 'illegal include macro tabsize argument' - else: - self.tabsize = config.tabsize - if 'depth' in attrs: - try: - val = int(attrs['depth']) - if not val >= 1: - raise ValueError, 'not >= 1' - self.max_depth = self.current_depth + val - except ValueError: - raise EAsciiDoc, "include macro: illegal 'depth' argument" - # Process included file. - message.verbose('include: ' + fname, linenos=False) - self.open(fname) - self.current_depth = self.current_depth + 1 - result = Reader1.read(self) - else: - if not Reader1.eof(self): - result = Reader1.read(self) - else: - result = None - return result - def eof(self): - """Returns True if all lines have been read.""" - if len(self.next) == 0: - # End of current file. - if self.parent: - self.closefile() - assign(self,self.parent) # Restore parent reader. - document.attributes['infile'] = self.infile - document.attributes['indir'] = self.indir - return Reader1.eof(self) - else: - return True - else: - return False - def read_next(self): - """Like read() but does not advance file pointer.""" - if Reader1.eof(self): - return None - else: - return self.next[0][2] - def unread(self,cursor): - """Push the line (filename,linenumber,linetext) tuple back into the read - buffer. Note that it's up to the caller to restore the previous - cursor.""" - assert cursor - self.next.insert(0,cursor) - -class Reader(Reader1): - """ Wraps (well, sought of) Reader1 class and implements conditional text - inclusion.""" - def __init__(self): - Reader1.__init__(self) - self.depth = 0 # if nesting depth. - self.skip = False # true if we're skipping ifdef...endif. - self.skipname = '' # Name of current endif macro target. - self.skipto = -1 # The depth at which skipping is reenabled. - def read_super(self): - result = Reader1.read(self,self.skip) - if result is None and self.skip: - raise EAsciiDoc,'missing endif::%s[]' % self.skipname - return result - def read(self): - result = self.read_super() - if result is None: - return None - while self.skip: - mo = macros.match('+',r'ifdef|ifndef|ifeval|endif',result) - if mo: - name = mo.group('name') - target = mo.group('target') - attrlist = mo.group('attrlist') - if name == 'endif': - self.depth -= 1 - if self.depth < 0: - raise EAsciiDoc,'mismatched macro: %s' % result - if self.depth == self.skipto: - self.skip = False - if target and self.skipname != target: - raise EAsciiDoc,'mismatched macro: %s' % result - else: - if name in ('ifdef','ifndef'): - if not target: - raise EAsciiDoc,'missing macro target: %s' % result - if not attrlist: - self.depth += 1 - elif name == 'ifeval': - if not attrlist: - raise EAsciiDoc,'missing ifeval condition: %s' % result - self.depth += 1 - result = self.read_super() - if result is None: - return None - mo = macros.match('+',r'ifdef|ifndef|ifeval|endif',result) - if mo: - name = mo.group('name') - target = mo.group('target') - attrlist = mo.group('attrlist') - if name == 'endif': - self.depth = self.depth-1 - else: - if not target and name in ('ifdef','ifndef'): - raise EAsciiDoc,'missing macro target: %s' % result - defined = is_attr_defined(target, document.attributes) - if name == 'ifdef': - if attrlist: - if defined: return attrlist - else: - self.skip = not defined - elif name == 'ifndef': - if attrlist: - if not defined: return attrlist - else: - self.skip = defined - elif name == 'ifeval': - if safe(): - message.unsafe('ifeval invalid') - raise EAsciiDoc,'ifeval invalid safe document' - if not attrlist: - raise EAsciiDoc,'missing ifeval condition: %s' % result - cond = False - attrlist = subs_attrs(attrlist) - if attrlist: - try: - cond = eval(attrlist) - except Exception,e: - raise EAsciiDoc,'error evaluating ifeval condition: %s: %s' % (result, str(e)) - message.verbose('ifeval: %s: %r' % (attrlist, cond)) - self.skip = not cond - if not attrlist or name == 'ifeval': - if self.skip: - self.skipto = self.depth - self.skipname = target - self.depth = self.depth+1 - result = self.read() - if result: - # Expand executable block macros. - mo = macros.match('+',r'eval|sys|sys2',result) - if mo: - action = mo.group('name') - cmd = mo.group('attrlist') - result = system(action, cmd, is_macro=True) - self.cursor[2] = result # So we don't re-evaluate. - if result: - # Unescape escaped system macros. - if macros.match('+',r'\\eval|\\sys|\\sys2|\\ifdef|\\ifndef|\\endif|\\include|\\include1',result): - result = result[1:] - return result - def eof(self): - return self.read_next() is None - def read_next(self): - save_cursor = self.cursor - result = self.read() - if result is not None: - self.unread(self.cursor) - self.cursor = save_cursor - return result - def read_lines(self,count=1): - """Return tuple containing count lines.""" - result = [] - i = 0 - while i < count and not self.eof(): - result.append(self.read()) - return tuple(result) - def read_ahead(self,count=1): - """Same as read_lines() but does not advance the file pointer.""" - result = [] - putback = [] - save_cursor = self.cursor - try: - i = 0 - while i < count and not self.eof(): - result.append(self.read()) - putback.append(self.cursor) - i = i+1 - while putback: - self.unread(putback.pop()) - finally: - self.cursor = save_cursor - return tuple(result) - def skip_blank_lines(self): - reader.read_until(r'\s*\S+') - def read_until(self,terminators,same_file=False): - """Like read() but reads lines up to (but not including) the first line - that matches the terminator regular expression, regular expression - object or list of regular expression objects. If same_file is True then - the terminating pattern must occur in the file the was being read when - the routine was called.""" - if same_file: - fname = self.cursor[0] - result = [] - if not isinstance(terminators,list): - if isinstance(terminators,basestring): - terminators = [re.compile(terminators)] - else: - terminators = [terminators] - while not self.eof(): - save_cursor = self.cursor - s = self.read() - if not same_file or fname == self.cursor[0]: - for reo in terminators: - if reo.match(s): - self.unread(self.cursor) - self.cursor = save_cursor - return tuple(result) - result.append(s) - return tuple(result) - -class Writer: - """Writes lines to output file.""" - def __init__(self): - self.newline = '\r\n' # End of line terminator. - self.f = None # Output file object. - self.fname = None # Output file name. - self.lines_out = 0 # Number of lines written. - self.skip_blank_lines = False # If True don't output blank lines. - def open(self,fname,bom=None): - ''' - bom is optional byte order mark. - http://en.wikipedia.org/wiki/Byte-order_mark - ''' - self.fname = fname - if fname == '<stdout>': - self.f = sys.stdout - else: - self.f = open(fname,'wb+') - message.verbose('writing: '+writer.fname,False) - if bom: - self.f.write(bom) - self.lines_out = 0 - def close(self): - if self.fname != '<stdout>': - self.f.close() - def write_line(self, line=None): - if not (self.skip_blank_lines and (not line or not line.strip())): - self.f.write((line or '') + self.newline) - self.lines_out = self.lines_out + 1 - def write(self,*args,**kwargs): - """Iterates arguments, writes tuple and list arguments one line per - element, else writes argument as single line. If no arguments writes - blank line. If argument is None nothing is written. self.newline is - appended to each line.""" - if 'trace' in kwargs and len(args) > 0: - trace(kwargs['trace'],args[0]) - if len(args) == 0: - self.write_line() - self.lines_out = self.lines_out + 1 - else: - for arg in args: - if is_array(arg): - for s in arg: - self.write_line(s) - elif arg is not None: - self.write_line(arg) - def write_tag(self,tag,content,subs=None,d=None,**kwargs): - """Write content enveloped by tag. - Substitutions specified in the 'subs' list are perform on the - 'content'.""" - if subs is None: - subs = config.subsnormal - stag,etag = subs_tag(tag,d) - content = Lex.subs(content,subs) - if 'trace' in kwargs: - trace(kwargs['trace'],[stag]+content+[etag]) - if stag: - self.write(stag) - if content: - self.write(content) - if etag: - self.write(etag) - -#--------------------------------------------------------------------------- -# Configuration file processing. -#--------------------------------------------------------------------------- -def _subs_specialwords(mo): - """Special word substitution function called by - Config.subs_specialwords().""" - word = mo.re.pattern # The special word. - template = config.specialwords[word] # The corresponding markup template. - if not template in config.sections: - raise EAsciiDoc,'missing special word template [%s]' % template - if mo.group()[0] == '\\': - return mo.group()[1:] # Return escaped word. - args = {} - args['words'] = mo.group() # The full match string is argument 'words'. - args.update(mo.groupdict()) # Add other named match groups to the arguments. - # Delete groups that didn't participate in match. - for k,v in args.items(): - if v is None: del args[k] - lines = subs_attrs(config.sections[template],args) - if len(lines) == 0: - result = '' - elif len(lines) == 1: - result = lines[0] - else: - result = writer.newline.join(lines) - return result - -class Config: - """Methods to process configuration files.""" - # Non-template section name regexp's. - ENTRIES_SECTIONS= ('tags','miscellaneous','attributes','specialcharacters', - 'specialwords','macros','replacements','quotes','titles', - r'paradef-.+',r'listdef-.+',r'blockdef-.+',r'tabledef-.+', - r'tabletags-.+',r'listtags-.+','replacements[23]', - r'old_tabledef-.+') - def __init__(self): - self.sections = OrderedDict() # Keyed by section name containing - # lists of section lines. - # Command-line options. - self.verbose = False - self.header_footer = True # -s, --no-header-footer option. - # [miscellaneous] section. - self.tabsize = 8 - self.textwidth = 70 # DEPRECATED: Old tables only. - self.newline = '\r\n' - self.pagewidth = None - self.pageunits = None - self.outfilesuffix = '' - self.subsnormal = SUBS_NORMAL - self.subsverbatim = SUBS_VERBATIM - - self.tags = {} # Values contain (stag,etag) tuples. - self.specialchars = {} # Values of special character substitutions. - self.specialwords = {} # Name is special word pattern, value is macro. - self.replacements = OrderedDict() # Key is find pattern, value is - #replace pattern. - self.replacements2 = OrderedDict() - self.replacements3 = OrderedDict() - self.specialsections = {} # Name is special section name pattern, value - # is corresponding section name. - self.quotes = OrderedDict() # Values contain corresponding tag name. - self.fname = '' # Most recently loaded configuration file name. - self.conf_attrs = {} # Attributes entries from conf files. - self.cmd_attrs = {} # Attributes from command-line -a options. - self.loaded = [] # Loaded conf files. - self.include1 = {} # Holds include1::[] files for {include1:}. - self.dumping = False # True if asciidoc -c option specified. - self.filters = [] # Filter names specified by --filter option. - - def init(self, cmd): - """ - Check Python version and locate the executable and configuration files - directory. - cmd is the asciidoc command or asciidoc.py path. - """ - if float(sys.version[:3]) < float(MIN_PYTHON_VERSION): - message.stderr('FAILED: Python %s or better required' % - MIN_PYTHON_VERSION) - sys.exit(1) - if not os.path.exists(cmd): - message.stderr('FAILED: Missing asciidoc command: %s' % cmd) - sys.exit(1) - global APP_FILE - APP_FILE = os.path.realpath(cmd) - global APP_DIR - APP_DIR = os.path.dirname(APP_FILE) - global USER_DIR - USER_DIR = userdir() - if USER_DIR is not None: - USER_DIR = os.path.join(USER_DIR,'.asciidoc') - if not os.path.isdir(USER_DIR): - USER_DIR = None - - def load_file(self, fname, dir=None, include=[], exclude=[]): - """ - Loads sections dictionary with sections from file fname. - Existing sections are overlaid. - The 'include' list contains the section names to be loaded. - The 'exclude' list contains section names not to be loaded. - Return False if no file was found in any of the locations. - """ - def update_section(section): - """ Update section in sections with contents. """ - if section and contents: - if section in sections and self.entries_section(section): - if ''.join(contents): - # Merge entries. - sections[section] += contents - else: - del sections[section] - else: - if section.startswith('+'): - # Append section. - if section in sections: - sections[section] += contents - else: - sections[section] = contents - else: - # Replace section. - sections[section] = contents - if dir: - fname = os.path.join(dir, fname) - # Sliently skip missing configuration file. - if not os.path.isfile(fname): - return False - # Don't load conf files twice (local and application conf files are the - # same if the source file is in the application directory). - if os.path.realpath(fname) in self.loaded: - return True - rdr = Reader() # Reader processes system macros. - message.linenos = False # Disable document line numbers. - rdr.open(fname) - message.linenos = None - self.fname = fname - reo = re.compile(r'(?u)^\[(?P<section>\+?[^\W\d][\w-]*)\]\s*$') - sections = OrderedDict() - section,contents = '',[] - while not rdr.eof(): - s = rdr.read() - if s and s[0] == '#': # Skip comment lines. - continue - if s[:2] == '\\#': # Unescape lines starting with '#'. - s = s[1:] - s = s.rstrip() - found = reo.findall(s) - if found: - update_section(section) # Store previous section. - section = found[0].lower() - contents = [] - else: - contents.append(s) - update_section(section) # Store last section. - rdr.close() - if include: - for s in set(sections) - set(include): - del sections[s] - if exclude: - for s in set(sections) & set(exclude): - del sections[s] - attrs = {} - self.load_sections(sections,attrs) - if not include: - # If all sections are loaded mark this file as loaded. - self.loaded.append(os.path.realpath(fname)) - document.update_attributes(attrs) # So they are available immediately. - return True - - def load_sections(self,sections,attrs=None): - """ - Loads sections dictionary. Each dictionary entry contains a - list of lines. - Updates 'attrs' with parsed [attributes] section entries. - """ - # Delete trailing blank lines from sections. - for k in sections.keys(): - for i in range(len(sections[k])-1,-1,-1): - if not sections[k][i]: - del sections[k][i] - elif not self.entries_section(k): - break - # Update new sections. - for k,v in sections.items(): - if k.startswith('+'): - # Append section. - k = k[1:] - if k in self.sections: - self.sections[k] += v - else: - self.sections[k] = v - else: - # Replace section. - self.sections[k] = v - self.parse_tags() - # Internally [miscellaneous] section entries are just attributes. - d = {} - parse_entries(sections.get('miscellaneous',()), d, unquote=True, - allow_name_only=True) - parse_entries(sections.get('attributes',()), d, unquote=True, - allow_name_only=True) - update_attrs(self.conf_attrs,d) - if attrs is not None: - attrs.update(d) - d = {} - parse_entries(sections.get('titles',()),d) - Title.load(d) - parse_entries(sections.get('specialcharacters',()),self.specialchars,escape_delimiter=False) - parse_entries(sections.get('quotes',()),self.quotes) - self.parse_specialwords() - self.parse_replacements() - self.parse_replacements('replacements2') - self.parse_replacements('replacements3') - self.parse_specialsections() - paragraphs.load(sections) - lists.load(sections) - blocks.load(sections) - tables_OLD.load(sections) - tables.load(sections) - macros.load(sections.get('macros',())) - - def get_load_dirs(self): - """ - Return list of well known paths with conf files. - """ - result = [] - if localapp(): - # Load from folders in asciidoc executable directory. - result.append(APP_DIR) - else: - # Load from global configuration directory. - result.append(CONF_DIR) - # Load configuration files from ~/.asciidoc if it exists. - if USER_DIR is not None: - result.append(USER_DIR) - return result - - def find_in_dirs(self, filename, dirs=None): - """ - Find conf files from dirs list. - Return list of found file paths. - Return empty list if not found in any of the locations. - """ - result = [] - if dirs is None: - dirs = self.get_load_dirs() - for d in dirs: - f = os.path.join(d,filename) - if os.path.isfile(f): - result.append(f) - return result - - def load_from_dirs(self, filename, dirs=None, include=[]): - """ - Load conf file from dirs list. - If dirs not specified try all the well known locations. - Return False if no file was sucessfully loaded. - """ - count = 0 - for f in self.find_in_dirs(filename,dirs): - if self.load_file(f, include=include): - count += 1 - return count != 0 - - def load_backend(self, dirs=None): - """ - Load the backend configuration files from dirs list. - If dirs not specified try all the well known locations. - If a <backend>.conf file was found return it's full path name, - if not found return None. - """ - result = None - if dirs is None: - dirs = self.get_load_dirs() - conf = document.backend + '.conf' - conf2 = document.backend + '-' + document.doctype + '.conf' - # First search for filter backends. - for d in [os.path.join(d, 'backends', document.backend) for d in dirs]: - if self.load_file(conf,d): - result = os.path.join(d, conf) - self.load_file(conf2,d) - if not result: - # Search in the normal locations. - for d in dirs: - if self.load_file(conf,d): - result = os.path.join(d, conf) - self.load_file(conf2,d) - return result - - def load_filters(self, dirs=None): - """ - Load filter configuration files from 'filters' directory in dirs list. - If dirs not specified try all the well known locations. Suppress - loading if a file named __noautoload__ is in same directory as the conf - file unless the filter has been specified with the --filter - command-line option (in which case it is loaded unconditionally). - """ - if dirs is None: - dirs = self.get_load_dirs() - for d in dirs: - # Load filter .conf files. - filtersdir = os.path.join(d,'filters') - for dirpath,dirnames,filenames in os.walk(filtersdir): - subdirs = dirpath[len(filtersdir):].split(os.path.sep) - # True if processing a filter specified by a --filter option. - filter_opt = len(subdirs) > 1 and subdirs[1] in self.filters - if '__noautoload__' not in filenames or filter_opt: - for f in filenames: - if re.match(r'^.+\.conf$',f): - self.load_file(f,dirpath) - - def find_config_dir(self, *dirnames): - """ - Return path of configuration directory. - Try all the well known locations. - Return None if directory not found. - """ - for d in [os.path.join(d, *dirnames) for d in self.get_load_dirs()]: - if os.path.isdir(d): - return d - return None - - def set_theme_attributes(self): - theme = document.attributes.get('theme') - if theme and 'themedir' not in document.attributes: - themedir = self.find_config_dir('themes', theme) - if themedir: - document.attributes['themedir'] = themedir - iconsdir = os.path.join(themedir, 'icons') - if 'data-uri' in document.attributes and os.path.isdir(iconsdir): - document.attributes['iconsdir'] = iconsdir - else: - message.warning('missing theme: %s' % theme, linenos=False) - - def load_miscellaneous(self,d): - """Set miscellaneous configuration entries from dictionary 'd'.""" - def set_if_int_gt_zero(name, d): - if name in d: - try: - val = int(d[name]) - if not val > 0: - raise ValueError, "not > 0" - if val > 0: - setattr(self, name, val) - except ValueError: - raise EAsciiDoc, 'illegal [miscellaneous] %s entry' % name - set_if_int_gt_zero('tabsize', d) - set_if_int_gt_zero('textwidth', d) # DEPRECATED: Old tables only. - - if 'pagewidth' in d: - try: - val = float(d['pagewidth']) - self.pagewidth = val - except ValueError: - raise EAsciiDoc, 'illegal [miscellaneous] pagewidth entry' - - if 'pageunits' in d: - self.pageunits = d['pageunits'] - if 'outfilesuffix' in d: - self.outfilesuffix = d['outfilesuffix'] - if 'newline' in d: - # Convert escape sequences to their character values. - self.newline = literal_eval('"'+d['newline']+'"') - if 'subsnormal' in d: - self.subsnormal = parse_options(d['subsnormal'],SUBS_OPTIONS, - 'illegal [%s] %s: %s' % - ('miscellaneous','subsnormal',d['subsnormal'])) - if 'subsverbatim' in d: - self.subsverbatim = parse_options(d['subsverbatim'],SUBS_OPTIONS, - 'illegal [%s] %s: %s' % - ('miscellaneous','subsverbatim',d['subsverbatim'])) - - def validate(self): - """Check the configuration for internal consistancy. Called after all - configuration files have been loaded.""" - message.linenos = False # Disable document line numbers. - # Heuristic to validate that at least one configuration file was loaded. - if not self.specialchars or not self.tags or not lists: - raise EAsciiDoc,'incomplete configuration files' - # Check special characters are only one character long. - for k in self.specialchars.keys(): - if len(k) != 1: - raise EAsciiDoc,'[specialcharacters] ' \ - 'must be a single character: %s' % k - # Check all special words have a corresponding inline macro body. - for macro in self.specialwords.values(): - if not is_name(macro): - raise EAsciiDoc,'illegal special word name: %s' % macro - if not macro in self.sections: - message.warning('missing special word macro: [%s]' % macro) - # Check all text quotes have a corresponding tag. - for q in self.quotes.keys()[:]: - tag = self.quotes[q] - if not tag: - del self.quotes[q] # Undefine quote. - else: - if tag[0] == '#': - tag = tag[1:] - if not tag in self.tags: - message.warning('[quotes] %s missing tag definition: %s' % (q,tag)) - # Check all specialsections section names exist. - for k,v in self.specialsections.items(): - if not v: - del self.specialsections[k] - elif not v in self.sections: - message.warning('missing specialsections section: [%s]' % v) - paragraphs.validate() - lists.validate() - blocks.validate() - tables_OLD.validate() - tables.validate() - macros.validate() - message.linenos = None - - def entries_section(self,section_name): - """ - Return True if conf file section contains entries, not a markup - template. - """ - for name in self.ENTRIES_SECTIONS: - if re.match(name,section_name): - return True - return False - - def dump(self): - """Dump configuration to stdout.""" - # Header. - hdr = '' - hdr = hdr + '#' + writer.newline - hdr = hdr + '# Generated by AsciiDoc %s for %s %s.%s' % \ - (VERSION,document.backend,document.doctype,writer.newline) - t = time.asctime(time.localtime(time.time())) - hdr = hdr + '# %s%s' % (t,writer.newline) - hdr = hdr + '#' + writer.newline - sys.stdout.write(hdr) - # Dump special sections. - # Dump only the configuration file and command-line attributes. - # [miscellanous] entries are dumped as part of the [attributes]. - d = {} - d.update(self.conf_attrs) - d.update(self.cmd_attrs) - dump_section('attributes',d) - Title.dump() - dump_section('quotes',self.quotes) - dump_section('specialcharacters',self.specialchars) - d = {} - for k,v in self.specialwords.items(): - if v in d: - d[v] = '%s "%s"' % (d[v],k) # Append word list. - else: - d[v] = '"%s"' % k - dump_section('specialwords',d) - dump_section('replacements',self.replacements) - dump_section('replacements2',self.replacements2) - dump_section('replacements3',self.replacements3) - dump_section('specialsections',self.specialsections) - d = {} - for k,v in self.tags.items(): - d[k] = '%s|%s' % v - dump_section('tags',d) - paragraphs.dump() - lists.dump() - blocks.dump() - tables_OLD.dump() - tables.dump() - macros.dump() - # Dump remaining sections. - for k in self.sections.keys(): - if not self.entries_section(k): - sys.stdout.write('[%s]%s' % (k,writer.newline)) - for line in self.sections[k]: - sys.stdout.write('%s%s' % (line,writer.newline)) - sys.stdout.write(writer.newline) - - def subs_section(self,section,d): - """Section attribute substitution using attributes from - document.attributes and 'd'. Lines containing undefinded - attributes are deleted.""" - if section in self.sections: - return subs_attrs(self.sections[section],d) - else: - message.warning('missing section: [%s]' % section) - return () - - def parse_tags(self): - """Parse [tags] section entries into self.tags dictionary.""" - d = {} - parse_entries(self.sections.get('tags',()),d) - for k,v in d.items(): - if v is None: - if k in self.tags: - del self.tags[k] - elif v == '': - self.tags[k] = (None,None) - else: - mo = re.match(r'(?P<stag>.*)\|(?P<etag>.*)',v) - if mo: - self.tags[k] = (mo.group('stag'), mo.group('etag')) - else: - raise EAsciiDoc,'[tag] %s value malformed' % k - - def tag(self, name, d=None): - """Returns (starttag,endtag) tuple named name from configuration file - [tags] section. Raise error if not found. If a dictionary 'd' is - passed then merge with document attributes and perform attribute - substitution on tags.""" - if not name in self.tags: - raise EAsciiDoc, 'missing tag: %s' % name - stag,etag = self.tags[name] - if d is not None: - # TODO: Should we warn if substitution drops a tag? - if stag: - stag = subs_attrs(stag,d) - if etag: - etag = subs_attrs(etag,d) - if stag is None: stag = '' - if etag is None: etag = '' - return (stag,etag) - - def parse_specialsections(self): - """Parse specialsections section to self.specialsections dictionary.""" - # TODO: This is virtually the same as parse_replacements() and should - # be factored to single routine. - d = {} - parse_entries(self.sections.get('specialsections',()),d,unquote=True) - for pat,sectname in d.items(): - pat = strip_quotes(pat) - if not is_re(pat): - raise EAsciiDoc,'[specialsections] entry ' \ - 'is not a valid regular expression: %s' % pat - if sectname is None: - if pat in self.specialsections: - del self.specialsections[pat] - else: - self.specialsections[pat] = sectname - - def parse_replacements(self,sect='replacements'): - """Parse replacements section into self.replacements dictionary.""" - d = OrderedDict() - parse_entries(self.sections.get(sect,()), d, unquote=True) - for pat,rep in d.items(): - if not self.set_replacement(pat, rep, getattr(self,sect)): - raise EAsciiDoc,'[%s] entry in %s is not a valid' \ - ' regular expression: %s' % (sect,self.fname,pat) - - @staticmethod - def set_replacement(pat, rep, replacements): - """Add pattern and replacement to replacements dictionary.""" - pat = strip_quotes(pat) - if not is_re(pat): - return False - if rep is None: - if pat in replacements: - del replacements[pat] - else: - replacements[pat] = strip_quotes(rep) - return True - - def subs_replacements(self,s,sect='replacements'): - """Substitute patterns from self.replacements in 's'.""" - result = s - for pat,rep in getattr(self,sect).items(): - result = re.sub(pat, rep, result) - return result - - def parse_specialwords(self): - """Parse special words section into self.specialwords dictionary.""" - reo = re.compile(r'(?:\s|^)(".+?"|[^"\s]+)(?=\s|$)') - for line in self.sections.get('specialwords',()): - e = parse_entry(line) - if not e: - raise EAsciiDoc,'[specialwords] entry in %s is malformed: %s' \ - % (self.fname,line) - name,wordlist = e - if not is_name(name): - raise EAsciiDoc,'[specialwords] name in %s is illegal: %s' \ - % (self.fname,name) - if wordlist is None: - # Undefine all words associated with 'name'. - for k,v in self.specialwords.items(): - if v == name: - del self.specialwords[k] - else: - words = reo.findall(wordlist) - for word in words: - word = strip_quotes(word) - if not is_re(word): - raise EAsciiDoc,'[specialwords] entry in %s ' \ - 'is not a valid regular expression: %s' \ - % (self.fname,word) - self.specialwords[word] = name - - def subs_specialchars(self,s): - """Perform special character substitution on string 's'.""" - """It may seem like a good idea to escape special characters with a '\' - character, the reason we don't is because the escape character itself - then has to be escaped and this makes including code listings - problematic. Use the predefined {amp},{lt},{gt} attributes instead.""" - result = '' - for ch in s: - result = result + self.specialchars.get(ch,ch) - return result - - def subs_specialchars_reverse(self,s): - """Perform reverse special character substitution on string 's'.""" - result = s - for k,v in self.specialchars.items(): - result = result.replace(v, k) - return result - - def subs_specialwords(self,s): - """Search for word patterns from self.specialwords in 's' and - substitute using corresponding macro.""" - result = s - for word in self.specialwords.keys(): - result = re.sub(word, _subs_specialwords, result) - return result - - def expand_templates(self,entries): - """Expand any template::[] macros in a list of section entries.""" - result = [] - for line in entries: - mo = macros.match('+',r'template',line) - if mo: - s = mo.group('attrlist') - if s in self.sections: - result += self.expand_templates(self.sections[s]) - else: - message.warning('missing section: [%s]' % s) - result.append(line) - else: - result.append(line) - return result - - def expand_all_templates(self): - for k,v in self.sections.items(): - self.sections[k] = self.expand_templates(v) - - def section2tags(self, section, d={}, skipstart=False, skipend=False): - """Perform attribute substitution on 'section' using document - attributes plus 'd' attributes. Return tuple (stag,etag) containing - pre and post | placeholder tags. 'skipstart' and 'skipend' are - used to suppress substitution.""" - assert section is not None - if section in self.sections: - body = self.sections[section] - else: - message.warning('missing section: [%s]' % section) - body = () - # Split macro body into start and end tag lists. - stag = [] - etag = [] - in_stag = True - for s in body: - if in_stag: - mo = re.match(r'(?P<stag>.*)\|(?P<etag>.*)',s) - if mo: - if mo.group('stag'): - stag.append(mo.group('stag')) - if mo.group('etag'): - etag.append(mo.group('etag')) - in_stag = False - else: - stag.append(s) - else: - etag.append(s) - # Do attribute substitution last so {brkbar} can be used to escape |. - # But don't do attribute substitution on title -- we've already done it. - title = d.get('title') - if title: - d['title'] = chr(0) # Replace with unused character. - if not skipstart: - stag = subs_attrs(stag, d) - if not skipend: - etag = subs_attrs(etag, d) - # Put the {title} back. - if title: - stag = map(lambda x: x.replace(chr(0), title), stag) - etag = map(lambda x: x.replace(chr(0), title), etag) - d['title'] = title - return (stag,etag) - - -#--------------------------------------------------------------------------- -# Deprecated old table classes follow. -# Naming convention is an _OLD name suffix. -# These will be removed from future versions of AsciiDoc - -def join_lines_OLD(lines): - """Return a list in which lines terminated with the backslash line - continuation character are joined.""" - result = [] - s = '' - continuation = False - for line in lines: - if line and line[-1] == '\\': - s = s + line[:-1] - continuation = True - continue - if continuation: - result.append(s+line) - s = '' - continuation = False - else: - result.append(line) - if continuation: - result.append(s) - return result - -class Column_OLD: - """Table column.""" - def __init__(self): - self.colalign = None # 'left','right','center' - self.rulerwidth = None - self.colwidth = None # Output width in page units. - -class Table_OLD(AbstractBlock): - COL_STOP = r"(`|'|\.)" # RE. - ALIGNMENTS = {'`':'left', "'":'right', '.':'center'} - FORMATS = ('fixed','csv','dsv') - def __init__(self): - AbstractBlock.__init__(self) - self.CONF_ENTRIES += ('template','fillchar','format','colspec', - 'headrow','footrow','bodyrow','headdata', - 'footdata', 'bodydata') - # Configuration parameters. - self.fillchar=None - self.format=None # 'fixed','csv','dsv' - self.colspec=None - self.headrow=None - self.footrow=None - self.bodyrow=None - self.headdata=None - self.footdata=None - self.bodydata=None - # Calculated parameters. - self.underline=None # RE matching current table underline. - self.isnumeric=False # True if numeric ruler. - self.tablewidth=None # Optional table width scale factor. - self.columns=[] # List of Columns. - # Other. - self.check_msg='' # Message set by previous self.validate() call. - def load(self,name,entries): - AbstractBlock.load(self,name,entries) - """Update table definition from section entries in 'entries'.""" - for k,v in entries.items(): - if k == 'fillchar': - if v and len(v) == 1: - self.fillchar = v - else: - raise EAsciiDoc,'malformed table fillchar: %s' % v - elif k == 'format': - if v in Table_OLD.FORMATS: - self.format = v - else: - raise EAsciiDoc,'illegal table format: %s' % v - elif k == 'colspec': - self.colspec = v - elif k == 'headrow': - self.headrow = v - elif k == 'footrow': - self.footrow = v - elif k == 'bodyrow': - self.bodyrow = v - elif k == 'headdata': - self.headdata = v - elif k == 'footdata': - self.footdata = v - elif k == 'bodydata': - self.bodydata = v - def dump(self): - AbstractBlock.dump(self) - write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline)) - write('fillchar='+self.fillchar) - write('format='+self.format) - if self.colspec: - write('colspec='+self.colspec) - if self.headrow: - write('headrow='+self.headrow) - if self.footrow: - write('footrow='+self.footrow) - write('bodyrow='+self.bodyrow) - if self.headdata: - write('headdata='+self.headdata) - if self.footdata: - write('footdata='+self.footdata) - write('bodydata='+self.bodydata) - write('') - def validate(self): - AbstractBlock.validate(self) - """Check table definition and set self.check_msg if invalid else set - self.check_msg to blank string.""" - # Check global table parameters. - if config.textwidth is None: - self.check_msg = 'missing [miscellaneous] textwidth entry' - elif config.pagewidth is None: - self.check_msg = 'missing [miscellaneous] pagewidth entry' - elif config.pageunits is None: - self.check_msg = 'missing [miscellaneous] pageunits entry' - elif self.headrow is None: - self.check_msg = 'missing headrow entry' - elif self.footrow is None: - self.check_msg = 'missing footrow entry' - elif self.bodyrow is None: - self.check_msg = 'missing bodyrow entry' - elif self.headdata is None: - self.check_msg = 'missing headdata entry' - elif self.footdata is None: - self.check_msg = 'missing footdata entry' - elif self.bodydata is None: - self.check_msg = 'missing bodydata entry' - else: - # No errors. - self.check_msg = '' - def isnext(self): - return AbstractBlock.isnext(self) - def parse_ruler(self,ruler): - """Parse ruler calculating underline and ruler column widths.""" - fc = re.escape(self.fillchar) - # Strip and save optional tablewidth from end of ruler. - mo = re.match(r'^(.*'+fc+r'+)([\d\.]+)$',ruler) - if mo: - ruler = mo.group(1) - self.tablewidth = float(mo.group(2)) - self.attributes['tablewidth'] = str(float(self.tablewidth)) - else: - self.tablewidth = None - self.attributes['tablewidth'] = '100.0' - # Guess whether column widths are specified numerically or not. - if ruler[1] != self.fillchar: - # If the first column does not start with a fillchar then numeric. - self.isnumeric = True - elif ruler[1:] == self.fillchar*len(ruler[1:]): - # The case of one column followed by fillchars is numeric. - self.isnumeric = True - else: - self.isnumeric = False - # Underlines must be 3 or more fillchars. - self.underline = r'^' + fc + r'{3,}$' - splits = re.split(self.COL_STOP,ruler)[1:] - # Build self.columns. - for i in range(0,len(splits),2): - c = Column_OLD() - c.colalign = self.ALIGNMENTS[splits[i]] - s = splits[i+1] - if self.isnumeric: - # Strip trailing fillchars. - s = re.sub(fc+r'+$','',s) - if s == '': - c.rulerwidth = None - else: - try: - val = int(s) - if not val > 0: - raise ValueError, 'not > 0' - c.rulerwidth = val - except ValueError: - raise EAsciiDoc, 'malformed ruler: bad width' - else: # Calculate column width from inter-fillchar intervals. - if not re.match(r'^'+fc+r'+$',s): - raise EAsciiDoc,'malformed ruler: illegal fillchars' - c.rulerwidth = len(s)+1 - self.columns.append(c) - # Fill in unspecified ruler widths. - if self.isnumeric: - if self.columns[0].rulerwidth is None: - prevwidth = 1 - for c in self.columns: - if c.rulerwidth is None: - c.rulerwidth = prevwidth - prevwidth = c.rulerwidth - def build_colspecs(self): - """Generate colwidths and colspecs. This can only be done after the - table arguments have been parsed since we use the table format.""" - self.attributes['cols'] = len(self.columns) - # Calculate total ruler width. - totalwidth = 0 - for c in self.columns: - totalwidth = totalwidth + c.rulerwidth - if totalwidth <= 0: - raise EAsciiDoc,'zero width table' - # Calculate marked up colwidths from rulerwidths. - for c in self.columns: - # Convert ruler width to output page width. - width = float(c.rulerwidth) - if self.format == 'fixed': - if self.tablewidth is None: - # Size proportional to ruler width. - colfraction = width/config.textwidth - else: - # Size proportional to page width. - colfraction = width/totalwidth - else: - # Size proportional to page width. - colfraction = width/totalwidth - c.colwidth = colfraction * config.pagewidth # To page units. - if self.tablewidth is not None: - c.colwidth = c.colwidth * self.tablewidth # Scale factor. - if self.tablewidth > 1: - c.colwidth = c.colwidth/100 # tablewidth is in percent. - # Build colspecs. - if self.colspec: - cols = [] - i = 0 - for c in self.columns: - i += 1 - self.attributes['colalign'] = c.colalign - self.attributes['colwidth'] = str(int(c.colwidth)) - self.attributes['colnumber'] = str(i + 1) - s = subs_attrs(self.colspec,self.attributes) - if not s: - message.warning('colspec dropped: contains undefined attribute') - else: - cols.append(s) - self.attributes['colspecs'] = writer.newline.join(cols) - def split_rows(self,rows): - """Return a two item tuple containing a list of lines up to but not - including the next underline (continued lines are joined ) and the - tuple of all lines after the underline.""" - reo = re.compile(self.underline) - i = 0 - while not reo.match(rows[i]): - i = i+1 - if i == 0: - raise EAsciiDoc,'missing table rows' - if i >= len(rows): - raise EAsciiDoc,'closing [%s] underline expected' % self.defname - return (join_lines_OLD(rows[:i]), rows[i+1:]) - def parse_rows(self, rows, rtag, dtag): - """Parse rows list using the row and data tags. Returns a substituted - list of output lines.""" - result = [] - # Source rows are parsed as single block, rather than line by line, to - # allow the CSV reader to handle multi-line rows. - if self.format == 'fixed': - rows = self.parse_fixed(rows) - elif self.format == 'csv': - rows = self.parse_csv(rows) - elif self.format == 'dsv': - rows = self.parse_dsv(rows) - else: - assert True,'illegal table format' - # Substitute and indent all data in all rows. - stag,etag = subs_tag(rtag,self.attributes) - for row in rows: - result.append(' '+stag) - for data in self.subs_row(row,dtag): - result.append(' '+data) - result.append(' '+etag) - return result - def subs_row(self, data, dtag): - """Substitute the list of source row data elements using the data tag. - Returns a substituted list of output table data items.""" - result = [] - if len(data) < len(self.columns): - message.warning('fewer row data items then table columns') - if len(data) > len(self.columns): - message.warning('more row data items than table columns') - for i in range(len(self.columns)): - if i > len(data) - 1: - d = '' # Fill missing column data with blanks. - else: - d = data[i] - c = self.columns[i] - self.attributes['colalign'] = c.colalign - self.attributes['colwidth'] = str(int(c.colwidth)) - self.attributes['colnumber'] = str(i + 1) - stag,etag = subs_tag(dtag,self.attributes) - # Insert AsciiDoc line break (' +') where row data has newlines - # ('\n'). This is really only useful when the table format is csv - # and the output markup is HTML. It's also a bit dubious in that it - # assumes the user has not modified the shipped line break pattern. - subs = self.get_subs()[0] - if 'replacements2' in subs: - # Insert line breaks in cell data. - d = re.sub(r'(?m)\n',r' +\n',d) - d = d.split('\n') # So writer.newline is written. - else: - d = [d] - result = result + [stag] + Lex.subs(d,subs) + [etag] - return result - def parse_fixed(self,rows): - """Parse the list of source table rows. Each row item in the returned - list contains a list of cell data elements.""" - result = [] - for row in rows: - data = [] - start = 0 - # build an encoded representation - row = char_decode(row) - for c in self.columns: - end = start + c.rulerwidth - if c is self.columns[-1]: - # Text in last column can continue forever. - # Use the encoded string to slice, but convert back - # to plain string before further processing - data.append(char_encode(row[start:]).strip()) - else: - data.append(char_encode(row[start:end]).strip()) - start = end - result.append(data) - return result - def parse_csv(self,rows): - """Parse the list of source table rows. Each row item in the returned - list contains a list of cell data elements.""" - import StringIO - import csv - result = [] - rdr = csv.reader(StringIO.StringIO('\r\n'.join(rows)), - skipinitialspace=True) - try: - for row in rdr: - result.append(row) - except Exception: - raise EAsciiDoc,'csv parse error: %s' % row - return result - def parse_dsv(self,rows): - """Parse the list of source table rows. Each row item in the returned - list contains a list of cell data elements.""" - separator = self.attributes.get('separator',':') - separator = literal_eval('"'+separator+'"') - if len(separator) != 1: - raise EAsciiDoc,'malformed dsv separator: %s' % separator - # TODO If separator is preceeded by an odd number of backslashes then - # it is escaped and should not delimit. - result = [] - for row in rows: - # Skip blank lines - if row == '': continue - # Unescape escaped characters. - row = literal_eval('"'+row.replace('"','\\"')+'"') - data = row.split(separator) - data = [s.strip() for s in data] - result.append(data) - return result - def translate(self): - message.deprecated('old tables syntax') - AbstractBlock.translate(self) - # Reset instance specific properties. - self.underline = None - self.columns = [] - attrs = {} - BlockTitle.consume(attrs) - # Add relevant globals to table substitutions. - attrs['pagewidth'] = str(config.pagewidth) - attrs['pageunits'] = config.pageunits - # Mix in document attribute list. - AttributeList.consume(attrs) - # Validate overridable attributes. - for k,v in attrs.items(): - if k == 'format': - if v not in self.FORMATS: - raise EAsciiDoc, 'illegal [%s] %s: %s' % (self.defname,k,v) - self.format = v - elif k == 'tablewidth': - try: - self.tablewidth = float(attrs['tablewidth']) - except Exception: - raise EAsciiDoc, 'illegal [%s] %s: %s' % (self.defname,k,v) - self.merge_attributes(attrs) - # Parse table ruler. - ruler = reader.read() - assert re.match(self.delimiter,ruler) - self.parse_ruler(ruler) - # Read the entire table. - table = [] - while True: - line = reader.read_next() - # Table terminated by underline followed by a blank line or EOF. - if len(table) > 0 and re.match(self.underline,table[-1]): - if line in ('',None): - break; - if line is None: - raise EAsciiDoc,'closing [%s] underline expected' % self.defname - table.append(reader.read()) - # EXPERIMENTAL: The number of lines in the table, requested by Benjamin Klum. - self.attributes['rows'] = str(len(table)) - if self.check_msg: # Skip if table definition was marked invalid. - message.warning('skipping [%s] table: %s' % (self.defname,self.check_msg)) - return - self.push_blockname('table') - # Generate colwidths and colspecs. - self.build_colspecs() - # Generate headrows, footrows, bodyrows. - # Headrow, footrow and bodyrow data replaces same named attributes in - # the table markup template. In order to ensure this data does not get - # a second attribute substitution (which would interfere with any - # already substituted inline passthroughs) unique placeholders are used - # (the tab character does not appear elsewhere since it is expanded on - # input) which are replaced after template attribute substitution. - headrows = footrows = [] - bodyrows,table = self.split_rows(table) - if table: - headrows = bodyrows - bodyrows,table = self.split_rows(table) - if table: - footrows,table = self.split_rows(table) - if headrows: - headrows = self.parse_rows(headrows, self.headrow, self.headdata) - headrows = writer.newline.join(headrows) - self.attributes['headrows'] = '\x07headrows\x07' - if footrows: - footrows = self.parse_rows(footrows, self.footrow, self.footdata) - footrows = writer.newline.join(footrows) - self.attributes['footrows'] = '\x07footrows\x07' - bodyrows = self.parse_rows(bodyrows, self.bodyrow, self.bodydata) - bodyrows = writer.newline.join(bodyrows) - self.attributes['bodyrows'] = '\x07bodyrows\x07' - table = subs_attrs(config.sections[self.template],self.attributes) - table = writer.newline.join(table) - # Before we finish replace the table head, foot and body place holders - # with the real data. - if headrows: - table = table.replace('\x07headrows\x07', headrows, 1) - if footrows: - table = table.replace('\x07footrows\x07', footrows, 1) - table = table.replace('\x07bodyrows\x07', bodyrows, 1) - writer.write(table,trace='table') - self.pop_blockname() - -class Tables_OLD(AbstractBlocks): - """List of tables.""" - BLOCK_TYPE = Table_OLD - PREFIX = 'old_tabledef-' - def __init__(self): - AbstractBlocks.__init__(self) - def load(self,sections): - AbstractBlocks.load(self,sections) - def validate(self): - # Does not call AbstractBlocks.validate(). - # Check we have a default table definition, - for i in range(len(self.blocks)): - if self.blocks[i].defname == 'old_tabledef-default': - default = self.blocks[i] - break - else: - raise EAsciiDoc,'missing section: [OLD_tabledef-default]' - # Set default table defaults. - if default.format is None: default.subs = 'fixed' - # Propagate defaults to unspecified table parameters. - for b in self.blocks: - if b is not default: - if b.fillchar is None: b.fillchar = default.fillchar - if b.format is None: b.format = default.format - if b.template is None: b.template = default.template - if b.colspec is None: b.colspec = default.colspec - if b.headrow is None: b.headrow = default.headrow - if b.footrow is None: b.footrow = default.footrow - if b.bodyrow is None: b.bodyrow = default.bodyrow - if b.headdata is None: b.headdata = default.headdata - if b.footdata is None: b.footdata = default.footdata - if b.bodydata is None: b.bodydata = default.bodydata - # Check all tables have valid fill character. - for b in self.blocks: - if not b.fillchar or len(b.fillchar) != 1: - raise EAsciiDoc,'[%s] missing or illegal fillchar' % b.defname - # Build combined tables delimiter patterns and assign defaults. - delimiters = [] - for b in self.blocks: - # Ruler is: - # (ColStop,(ColWidth,FillChar+)?)+, FillChar+, TableWidth? - b.delimiter = r'^(' + Table_OLD.COL_STOP \ - + r'(\d*|' + re.escape(b.fillchar) + r'*)' \ - + r')+' \ - + re.escape(b.fillchar) + r'+' \ - + '([\d\.]*)$' - delimiters.append(b.delimiter) - if not b.headrow: - b.headrow = b.bodyrow - if not b.footrow: - b.footrow = b.bodyrow - if not b.headdata: - b.headdata = b.bodydata - if not b.footdata: - b.footdata = b.bodydata - self.delimiters = re_join(delimiters) - # Check table definitions are valid. - for b in self.blocks: - b.validate() - if config.verbose: - if b.check_msg: - message.warning('[%s] table definition: %s' % (b.defname,b.check_msg)) - -# End of deprecated old table classes. -#--------------------------------------------------------------------------- - -#--------------------------------------------------------------------------- -# filter and theme plugin commands. -#--------------------------------------------------------------------------- -import shutil, zipfile - -def die(msg): - message.stderr(msg) - sys.exit(1) - -def extract_zip(zip_file, destdir): - """ - Unzip Zip file to destination directory. - Throws exception if error occurs. - """ - zipo = zipfile.ZipFile(zip_file, 'r') - try: - for zi in zipo.infolist(): - outfile = zi.filename - if not outfile.endswith('/'): - d, outfile = os.path.split(outfile) - directory = os.path.normpath(os.path.join(destdir, d)) - if not os.path.isdir(directory): - os.makedirs(directory) - outfile = os.path.join(directory, outfile) - perms = (zi.external_attr >> 16) & 0777 - message.verbose('extracting: %s' % outfile) - flags = os.O_CREAT | os.O_WRONLY - if sys.platform == 'win32': - flags |= os.O_BINARY - if perms == 0: - # Zip files created under Windows do not include permissions. - fh = os.open(outfile, flags) - else: - fh = os.open(outfile, flags, perms) - try: - os.write(fh, zipo.read(zi.filename)) - finally: - os.close(fh) - finally: - zipo.close() - -def create_zip(zip_file, src, skip_hidden=False): - """ - Create Zip file. If src is a directory archive all contained files and - subdirectories, if src is a file archive the src file. - Files and directories names starting with . are skipped - if skip_hidden is True. - Throws exception if error occurs. - """ - zipo = zipfile.ZipFile(zip_file, 'w') - try: - if os.path.isfile(src): - arcname = os.path.basename(src) - message.verbose('archiving: %s' % arcname) - zipo.write(src, arcname, zipfile.ZIP_DEFLATED) - elif os.path.isdir(src): - srcdir = os.path.abspath(src) - if srcdir[-1] != os.path.sep: - srcdir += os.path.sep - for root, dirs, files in os.walk(srcdir): - arcroot = os.path.abspath(root)[len(srcdir):] - if skip_hidden: - for d in dirs[:]: - if d.startswith('.'): - message.verbose('skipping: %s' % os.path.join(arcroot, d)) - del dirs[dirs.index(d)] - for f in files: - filename = os.path.join(root,f) - arcname = os.path.join(arcroot, f) - if skip_hidden and f.startswith('.'): - message.verbose('skipping: %s' % arcname) - continue - message.verbose('archiving: %s' % arcname) - zipo.write(filename, arcname, zipfile.ZIP_DEFLATED) - else: - raise ValueError,'src must specify directory or file: %s' % src - finally: - zipo.close() - -class Plugin: - """ - --filter and --theme option commands. - """ - CMDS = ('install','remove','list','build') - - type = None # 'backend', 'filter' or 'theme'. - - @staticmethod - def get_dir(): - """ - Return plugins path (.asciidoc/filters or .asciidoc/themes) in user's - home direcory or None if user home not defined. - """ - result = userdir() - if result: - result = os.path.join(result, '.asciidoc', Plugin.type+'s') - return result - - @staticmethod - def install(args): - """ - Install plugin Zip file. - args[0] is plugin zip file path. - args[1] is optional destination plugins directory. - """ - if len(args) not in (1,2): - die('invalid number of arguments: --%s install %s' - % (Plugin.type, ' '.join(args))) - zip_file = args[0] - if not os.path.isfile(zip_file): - die('file not found: %s' % zip_file) - reo = re.match(r'^\w+',os.path.split(zip_file)[1]) - if not reo: - die('file name does not start with legal %s name: %s' - % (Plugin.type, zip_file)) - plugin_name = reo.group() - if len(args) == 2: - plugins_dir = args[1] - if not os.path.isdir(plugins_dir): - die('directory not found: %s' % plugins_dir) - else: - plugins_dir = Plugin.get_dir() - if not plugins_dir: - die('user home directory is not defined') - plugin_dir = os.path.join(plugins_dir, plugin_name) - if os.path.exists(plugin_dir): - die('%s is already installed: %s' % (Plugin.type, plugin_dir)) - try: - os.makedirs(plugin_dir) - except Exception,e: - die('failed to create %s directory: %s' % (Plugin.type, str(e))) - try: - extract_zip(zip_file, plugin_dir) - except Exception,e: - if os.path.isdir(plugin_dir): - shutil.rmtree(plugin_dir) - die('failed to extract %s: %s' % (Plugin.type, str(e))) - - @staticmethod - def remove(args): - """ - Delete plugin directory. - args[0] is plugin name. - args[1] is optional plugin directory (defaults to ~/.asciidoc/<plugin_name>). - """ - if len(args) not in (1,2): - die('invalid number of arguments: --%s remove %s' - % (Plugin.type, ' '.join(args))) - plugin_name = args[0] - if not re.match(r'^\w+$',plugin_name): - die('illegal %s name: %s' % (Plugin.type, plugin_name)) - if len(args) == 2: - d = args[1] - if not os.path.isdir(d): - die('directory not found: %s' % d) - else: - d = Plugin.get_dir() - if not d: - die('user directory is not defined') - plugin_dir = os.path.join(d, plugin_name) - if not os.path.isdir(plugin_dir): - die('cannot find %s: %s' % (Plugin.type, plugin_dir)) - try: - message.verbose('removing: %s' % plugin_dir) - shutil.rmtree(plugin_dir) - except Exception,e: - die('failed to delete %s: %s' % (Plugin.type, str(e))) - - @staticmethod - def list(args): - """ - List all plugin directories (global and local). - """ - for d in [os.path.join(d, Plugin.type+'s') for d in config.get_load_dirs()]: - if os.path.isdir(d): - for f in os.walk(d).next()[1]: - message.stdout(os.path.join(d,f)) - - @staticmethod - def build(args): - """ - Create plugin Zip file. - args[0] is Zip file name. - args[1] is plugin directory. - """ - if len(args) != 2: - die('invalid number of arguments: --%s build %s' - % (Plugin.type, ' '.join(args))) - zip_file = args[0] - plugin_source = args[1] - if not (os.path.isdir(plugin_source) or os.path.isfile(plugin_source)): - die('plugin source not found: %s' % plugin_source) - try: - create_zip(zip_file, plugin_source, skip_hidden=True) - except Exception,e: - die('failed to create %s: %s' % (zip_file, str(e))) - - -#--------------------------------------------------------------------------- -# Application code. -#--------------------------------------------------------------------------- -# Constants -# --------- -APP_FILE = None # This file's full path. -APP_DIR = None # This file's directory. -USER_DIR = None # ~/.asciidoc -# Global configuration files directory (set by Makefile build target). -CONF_DIR = '/etc/asciidoc' -HELP_FILE = 'help.conf' # Default (English) help file. - -# Globals -# ------- -document = Document() # The document being processed. -config = Config() # Configuration file reader. -reader = Reader() # Input stream line reader. -writer = Writer() # Output stream line writer. -message = Message() # Message functions. -paragraphs = Paragraphs() # Paragraph definitions. -lists = Lists() # List definitions. -blocks = DelimitedBlocks() # DelimitedBlock definitions. -tables_OLD = Tables_OLD() # Table_OLD definitions. -tables = Tables() # Table definitions. -macros = Macros() # Macro definitions. -calloutmap = CalloutMap() # Coordinates callouts and callout list. -trace = Trace() # Implements trace attribute processing. - -### Used by asciidocapi.py ### -# List of message strings written to stderr. -messages = message.messages - - -def asciidoc(backend, doctype, confiles, infile, outfile, options): - """Convert AsciiDoc document to DocBook document of type doctype - The AsciiDoc document is read from file object src the translated - DocBook file written to file object dst.""" - def load_conffiles(include=[], exclude=[]): - # Load conf files specified on the command-line and by the conf-files attribute. - files = document.attributes.get('conf-files','') - files = [f.strip() for f in files.split('|') if f.strip()] - files += confiles - if files: - for f in files: - if os.path.isfile(f): - config.load_file(f, include=include, exclude=exclude) - else: - raise EAsciiDoc,'missing configuration file: %s' % f - try: - document.attributes['python'] = sys.executable - for f in config.filters: - if not config.find_config_dir('filters', f): - raise EAsciiDoc,'missing filter: %s' % f - if doctype not in (None,'article','manpage','book'): - raise EAsciiDoc,'illegal document type' - # Set processing options. - for o in options: - if o == '-c': config.dumping = True - if o == '-s': config.header_footer = False - if o == '-v': config.verbose = True - document.update_attributes() - if '-e' not in options: - # Load asciidoc.conf files in two passes: the first for attributes - # the second for everything. This is so that locally set attributes - # available are in the global asciidoc.conf - if not config.load_from_dirs('asciidoc.conf',include=['attributes']): - raise EAsciiDoc,'configuration file asciidoc.conf missing' - load_conffiles(include=['attributes']) - config.load_from_dirs('asciidoc.conf') - if infile != '<stdin>': - indir = os.path.dirname(infile) - config.load_file('asciidoc.conf', indir, - include=['attributes','titles','specialchars']) - else: - load_conffiles(include=['attributes','titles','specialchars']) - document.update_attributes() - # Check the infile exists. - if infile != '<stdin>': - if not os.path.isfile(infile): - raise EAsciiDoc,'input file %s missing' % infile - document.infile = infile - AttributeList.initialize() - # Open input file and parse document header. - reader.tabsize = config.tabsize - reader.open(infile) - has_header = document.parse_header(doctype,backend) - # doctype is now finalized. - document.attributes['doctype-'+document.doctype] = '' - config.set_theme_attributes() - # Load backend configuration files. - if '-e' not in options: - f = document.backend + '.conf' - conffile = config.load_backend() - if not conffile: - raise EAsciiDoc,'missing backend conf file: %s' % f - document.attributes['backend-confdir'] = os.path.dirname(conffile) - # backend is now known. - document.attributes['backend-'+document.backend] = '' - document.attributes[document.backend+'-'+document.doctype] = '' - doc_conffiles = [] - if '-e' not in options: - # Load filters and language file. - config.load_filters() - document.load_lang() - if infile != '<stdin>': - # Load local conf files (files in the source file directory). - config.load_file('asciidoc.conf', indir) - config.load_backend([indir]) - config.load_filters([indir]) - # Load document specific configuration files. - f = os.path.splitext(infile)[0] - doc_conffiles = [ - f for f in (f+'.conf', f+'-'+document.backend+'.conf') - if os.path.isfile(f) ] - for f in doc_conffiles: - config.load_file(f) - load_conffiles() - # Build asciidoc-args attribute. - args = '' - # Add custom conf file arguments. - for f in doc_conffiles + confiles: - args += ' --conf-file "%s"' % f - # Add command-line and header attributes. - attrs = {} - attrs.update(AttributeEntry.attributes) - attrs.update(config.cmd_attrs) - if 'title' in attrs: # Don't pass the header title. - del attrs['title'] - for k,v in attrs.items(): - if v: - args += ' --attribute "%s=%s"' % (k,v) - else: - args += ' --attribute "%s"' % k - document.attributes['asciidoc-args'] = args - # Build outfile name. - if outfile is None: - outfile = os.path.splitext(infile)[0] + '.' + document.backend - if config.outfilesuffix: - # Change file extension. - outfile = os.path.splitext(outfile)[0] + config.outfilesuffix - document.outfile = outfile - # Document header attributes override conf file attributes. - document.attributes.update(AttributeEntry.attributes) - document.update_attributes() - # Configuration is fully loaded. - config.expand_all_templates() - # Check configuration for consistency. - config.validate() - # Initialize top level block name. - if document.attributes.get('blockname'): - AbstractBlock.blocknames.append(document.attributes['blockname']) - paragraphs.initialize() - lists.initialize() - if config.dumping: - config.dump() - else: - writer.newline = config.newline - try: - writer.open(outfile, reader.bom) - try: - document.translate(has_header) # Generate the output. - finally: - writer.close() - finally: - reader.closefile() - except KeyboardInterrupt: - raise - except Exception,e: - # Cleanup. - if outfile and outfile != '<stdout>' and os.path.isfile(outfile): - os.unlink(outfile) - # Build and print error description. - msg = 'FAILED: ' - if reader.cursor: - msg = message.format('', msg) - if isinstance(e, EAsciiDoc): - message.stderr('%s%s' % (msg,str(e))) - else: - if __name__ == '__main__': - message.stderr(msg+'unexpected error:') - message.stderr('-'*60) - traceback.print_exc(file=sys.stderr) - message.stderr('-'*60) - else: - message.stderr('%sunexpected error: %s' % (msg,str(e))) - sys.exit(1) - -def usage(msg=''): - if msg: - message.stderr(msg) - show_help('default', sys.stderr) - -def show_help(topic, f=None): - """Print help topic to file object f.""" - if f is None: - f = sys.stdout - # Select help file. - lang = config.cmd_attrs.get('lang') - if lang and lang != 'en': - help_file = 'help-' + lang + '.conf' - else: - help_file = HELP_FILE - # Print [topic] section from help file. - config.load_from_dirs(help_file) - if len(config.sections) == 0: - # Default to English if specified language help files not found. - help_file = HELP_FILE - config.load_from_dirs(help_file) - if len(config.sections) == 0: - message.stderr('no help topics found') - sys.exit(1) - n = 0 - for k in config.sections: - if re.match(re.escape(topic), k): - n += 1 - lines = config.sections[k] - if n == 0: - if topic != 'topics': - message.stderr('help topic not found: [%s] in %s' % (topic, help_file)) - message.stderr('available help topics: %s' % ', '.join(config.sections.keys())) - sys.exit(1) - elif n > 1: - message.stderr('ambiguous help topic: %s' % topic) - else: - for line in lines: - print >>f, line - -### Used by asciidocapi.py ### -def execute(cmd,opts,args): - """ - Execute asciidoc with command-line options and arguments. - cmd is asciidoc command or asciidoc.py path. - opts and args conform to values returned by getopt.getopt(). - Raises SystemExit if an error occurs. - - Doctests: - - 1. Check execution: - - >>> import StringIO - >>> infile = StringIO.StringIO('Hello *{author}*') - >>> outfile = StringIO.StringIO() - >>> opts = [] - >>> opts.append(('--backend','html4')) - >>> opts.append(('--no-header-footer',None)) - >>> opts.append(('--attribute','author=Joe Bloggs')) - >>> opts.append(('--out-file',outfile)) - >>> execute(__file__, opts, [infile]) - >>> print outfile.getvalue() - <p>Hello <strong>Joe Bloggs</strong></p> - - >>> - - """ - config.init(cmd) - if len(args) > 1: - usage('Too many arguments') - sys.exit(1) - backend = None - doctype = None - confiles = [] - outfile = None - options = [] - help_option = False - for o,v in opts: - if o in ('--help','-h'): - help_option = True - #DEPRECATED: --unsafe option. - if o == '--unsafe': - document.safe = False - if o == '--safe': - document.safe = True - if o == '--version': - print('asciidoc %s' % VERSION) - sys.exit(0) - if o in ('-b','--backend'): - backend = v - if o in ('-c','--dump-conf'): - options.append('-c') - if o in ('-d','--doctype'): - doctype = v - if o in ('-e','--no-conf'): - options.append('-e') - if o in ('-f','--conf-file'): - confiles.append(v) - if o == '--filter': - config.filters.append(v) - if o in ('-n','--section-numbers'): - o = '-a' - v = 'numbered' - if o == '--theme': - o = '-a' - v = 'theme='+v - if o in ('-a','--attribute'): - e = parse_entry(v, allow_name_only=True) - if not e: - usage('Illegal -a option: %s' % v) - sys.exit(1) - k,v = e - # A @ suffix denotes don't override existing document attributes. - if v and v[-1] == '@': - document.attributes[k] = v[:-1] - else: - config.cmd_attrs[k] = v - if o in ('-o','--out-file'): - outfile = v - if o in ('-s','--no-header-footer'): - options.append('-s') - if o in ('-v','--verbose'): - options.append('-v') - if help_option: - if len(args) == 0: - show_help('default') - else: - show_help(args[-1]) - sys.exit(0) - if len(args) == 0 and len(opts) == 0: - usage() - sys.exit(0) - if len(args) == 0: - usage('No source file specified') - sys.exit(1) - stdin,stdout = sys.stdin,sys.stdout - try: - infile = args[0] - if infile == '-': - infile = '<stdin>' - elif isinstance(infile, str): - infile = os.path.abspath(infile) - else: # Input file is file object from API call. - sys.stdin = infile - infile = '<stdin>' - if outfile == '-': - outfile = '<stdout>' - elif isinstance(outfile, str): - outfile = os.path.abspath(outfile) - elif outfile is None: - if infile == '<stdin>': - outfile = '<stdout>' - else: # Output file is file object from API call. - sys.stdout = outfile - outfile = '<stdout>' - # Do the work. - asciidoc(backend, doctype, confiles, infile, outfile, options) - if document.has_errors: - sys.exit(1) - finally: - sys.stdin,sys.stdout = stdin,stdout - -if __name__ == '__main__': - # Process command line options. - import getopt - try: - #DEPRECATED: --unsafe option. - opts,args = getopt.getopt(sys.argv[1:], - 'a:b:cd:ef:hno:svw:', - ['attribute=','backend=','conf-file=','doctype=','dump-conf', - 'help','no-conf','no-header-footer','out-file=', - 'section-numbers','verbose','version','safe','unsafe', - 'doctest','filter=','theme=']) - except getopt.GetoptError: - message.stderr('illegal command options') - sys.exit(1) - opt_names = [opt[0] for opt in opts] - if '--doctest' in opt_names: - # Run module doctests. - import doctest - options = doctest.NORMALIZE_WHITESPACE + doctest.ELLIPSIS - failures,tries = doctest.testmod(optionflags=options) - if failures == 0: - message.stderr('All doctests passed') - sys.exit(0) - else: - sys.exit(1) - # Look for plugin management commands. - count = 0 - for o,v in opts: - if o in ('-b','--backend','--filter','--theme'): - if o == '-b': - o = '--backend' - plugin = o[2:] - cmd = v - if cmd not in Plugin.CMDS: - continue - count += 1 - if count > 1: - die('--backend, --filter and --theme options are mutually exclusive') - if count == 1: - # Execute plugin management commands. - if not cmd: - die('missing --%s command' % plugin) - if cmd not in Plugin.CMDS: - die('illegal --%s command: %s' % (plugin, cmd)) - Plugin.type = plugin - config.init(sys.argv[0]) - config.verbose = bool(set(['-v','--verbose']) & set(opt_names)) - getattr(Plugin,cmd)(args) - else: - # Execute asciidoc. - try: - execute(sys.argv[0],opts,args) - except KeyboardInterrupt: - sys.exit(1) |