From 29a4f48db8f6529a54c219f4dbaf3f7d300df049 Mon Sep 17 00:00:00 2001 From: Chris Johns Date: Sun, 3 Mar 2013 13:26:25 +1100 Subject: [PATCH] Import asciidoc into the tool. --- source-builder/sb/asciidoc.py | 6265 +++++++++++++++++++++ source-builder/sb/asciidocapi.py | 257 + source-builder/sb/images/rtemswhitebg.jpg | Bin 0 -> 117890 bytes 3 files changed, 6522 insertions(+) create mode 100755 source-builder/sb/asciidoc.py create mode 100644 source-builder/sb/asciidocapi.py create mode 100644 source-builder/sb/images/rtemswhitebg.jpg diff --git a/source-builder/sb/asciidoc.py b/source-builder/sb/asciidoc.py new file mode 100755 index 0000000..6128173 --- /dev/null +++ b/source-builder/sb/asciidoc.py @@ -0,0 +1,6265 @@ +#!/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 '' + 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 == '': + 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[^[\]]+?)\])?' \ + + r'(?:' + re.escape(lq) + r')' \ + + r'(?P.+?)(?:'+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[^[\]]+?)\])?' \ + + r'(?:' + re.escape(lq) + r')' \ + + r'(?P\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: + Return True if single attrbiute is defined. + ,,... Return True if one or more attributes are defined. + ++... 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[^"]+)"(?P.*)$', filter_cmd) + if not mo: + # Single quoted. + mo = re.match(r"^'(?P[^']+)'(?P.*)$", filter_cmd) + if not mo: + # Unquoted catch all. + mo = re.match(r'^(?P\S+)(?P.*)$', 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[^:]*?)(:(?P.*))?$', 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[^:]*?)(:(?P.*))?$', 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[^\\\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[^\\\W][-\w]*?)' \ + r'(?P\=|\?|!|#|%|@|\$)' \ + r'(?P.*?)\}(?!\\)') + # Multiple names (n1,n2,... or n1+n2+...) -- lower precedence. + reo2 = re.compile(r'(?su)\{(?P[^\\\W][-\w'+OR+AND+r']*?)' \ + r'(?P\=|\?|!|#|%|@|\$)' \ + r'(?P.*?)\}(?!\\)') + 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'(?@:[:]} + else: + if len(v) == 3: # {@::} + s = v[2] + else: # {@:} + s = '' + else: + if re_mo: + if len(v) == 2: # {$:} + s = v[1] + elif v[1] == '': # {$::} + s = UNDEFINED # So the line is dropped. + else: # {$::} + s = v[1] + else: + if len(v) == 2: # {$:} + s = UNDEFINED # So the line is dropped. + else: # {$::} + 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)\{(?Peval):(?P.*?)\}(?!\\)'), + re.compile(r'(?su)\{(?P[^\\\W][-\w]*?):(?P.*?)\}(?!\\)'), + ] + 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 == '': + t = time.time() + else: + t = None + if t: + self.attributes['doctime'] = time_str(t) + self.attributes['docdate'] = date_str(t) + if self.infile != '': + 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 != '': + self.attributes['outfile'] = self.outfile + self.attributes['outdir'] = os.path.dirname(self.outfile) + if self.infile == '': + 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.*?)\s+-\s+(?P.*)$',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[^<>\s]+)' + '(\s+(?P[^<>\s]+))?' + '(\s+(?P[^<>\s]+))?' + '(\s+<(?P\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.*?),)?(?P.*?)(:\s*(?P.*))?$' + RCS_ID_RE = r'^\$Id: \S+ (?P\S+) (?P\S+) \S+ (?P\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.*)\((?P.*)\)$', + 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 :[.]:[ ] + 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.*)\[(?P.*)\]$', 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 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 + for option in self.parameters.options: + self.attributes[option+'-option'] = '' + 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_ge(name, d, min_value): + if name in d: + try: + val = int(d[name]) + if not val >= min_value: + raise ValueError, "not >= " + str(min_value) + setattr(self, name, val) + except ValueError: + raise EAsciiDoc, 'illegal [miscellaneous] %s entry' % name + set_if_int_ge('tabsize', d, 0) + set_if_int_ge('textwidth', d, 1) # 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() + # Set the default embedded icons directory. + if 'data-uri' in document.attributes and not os.path.isdir(document.attributes['iconsdir']): + document.attributes['iconsdir'] = os.path.join( + document.attributes['asciidoc-confdir'], 'images/icons') + # 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) diff --git a/source-builder/sb/asciidocapi.py b/source-builder/sb/asciidocapi.py new file mode 100644 index 0000000..dcdf262 --- /dev/null +++ b/source-builder/sb/asciidocapi.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python +""" +asciidocapi - AsciiDoc API wrapper class. + +The AsciiDocAPI class provides an API for executing asciidoc. Minimal example +compiles `mydoc.txt` to `mydoc.html`: + + import asciidocapi + asciidoc = asciidocapi.AsciiDocAPI() + asciidoc.execute('mydoc.txt') + +- Full documentation in asciidocapi.txt. +- See the doctests below for more examples. + +Doctests: + +1. Check execution: + + >>> import StringIO + >>> infile = StringIO.StringIO('Hello *{author}*') + >>> outfile = StringIO.StringIO() + >>> asciidoc = AsciiDocAPI() + >>> asciidoc.options('--no-header-footer') + >>> asciidoc.attributes['author'] = 'Joe Bloggs' + >>> asciidoc.execute(infile, outfile, backend='html4') + >>> print outfile.getvalue() + <p>Hello <strong>Joe Bloggs</strong></p> + + >>> asciidoc.attributes['author'] = 'Bill Smith' + >>> infile = StringIO.StringIO('Hello _{author}_') + >>> outfile = StringIO.StringIO() + >>> asciidoc.execute(infile, outfile, backend='docbook') + >>> print outfile.getvalue() + <simpara>Hello <emphasis>Bill Smith</emphasis></simpara> + +2. Check error handling: + + >>> import StringIO + >>> asciidoc = AsciiDocAPI() + >>> infile = StringIO.StringIO('---------') + >>> outfile = StringIO.StringIO() + >>> asciidoc.execute(infile, outfile) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "asciidocapi.py", line 189, in execute + raise AsciiDocError(self.messages[-1]) + AsciiDocError: ERROR: <stdin>: line 1: [blockdef-listing] missing closing delimiter + + +Copyright (C) 2009 Stuart Rackham. Free use of this software is granted +under the terms of the GNU General Public License (GPL). + +""" + +import sys,os,re,imp + +API_VERSION = '0.1.2' +MIN_ASCIIDOC_VERSION = '8.4.1' # Minimum acceptable AsciiDoc version. + + +def find_in_path(fname, path=None): + """ + Find file fname in paths. Return None if not found. + """ + if path is None: + path = os.environ.get('PATH', '') + for dir in path.split(os.pathsep): + fpath = os.path.join(dir, fname) + if os.path.isfile(fpath): + return fpath + else: + return None + + +class AsciiDocError(Exception): + pass + + +class Options(object): + """ + Stores asciidoc(1) command options. + """ + def __init__(self, values=[]): + self.values = values[:] + def __call__(self, name, value=None): + """Shortcut for append method.""" + self.append(name, value) + def append(self, name, value=None): + if type(value) in (int,float): + value = str(value) + self.values.append((name,value)) + + +class Version(object): + """ + Parse and compare AsciiDoc version numbers. Instance attributes: + + string: String version number '<major>.<minor>[.<micro>][suffix]'. + major: Integer major version number. + minor: Integer minor version number. + micro: Integer micro version number. + suffix: Suffix (begins with non-numeric character) is ignored when + comparing. + + Doctest examples: + + >>> Version('8.2.5') < Version('8.3 beta 1') + True + >>> Version('8.3.0') == Version('8.3. beta 1') + True + >>> Version('8.2.0') < Version('8.20') + True + >>> Version('8.20').major + 8 + >>> Version('8.20').minor + 20 + >>> Version('8.20').micro + 0 + >>> Version('8.20').suffix + '' + >>> Version('8.20 beta 1').suffix + 'beta 1' + + """ + def __init__(self, version): + self.string = version + reo = re.match(r'^(\d+)\.(\d+)(\.(\d+))?\s*(.*?)\s*$', self.string) + if not reo: + raise ValueError('invalid version number: %s' % self.string) + groups = reo.groups() + self.major = int(groups[0]) + self.minor = int(groups[1]) + self.micro = int(groups[3] or '0') + self.suffix = groups[4] or '' + def __cmp__(self, other): + result = cmp(self.major, other.major) + if result == 0: + result = cmp(self.minor, other.minor) + if result == 0: + result = cmp(self.micro, other.micro) + return result + + +class AsciiDocAPI(object): + """ + AsciiDoc API class. + """ + def __init__(self, asciidoc_py=None): + """ + Locate and import asciidoc.py. + Initialize instance attributes. + """ + self.options = Options() + self.attributes = {} + self.messages = [] + # Search for the asciidoc command file. + # Try ASCIIDOC_PY environment variable first. + cmd = os.environ.get('ASCIIDOC_PY') + if cmd: + if not os.path.isfile(cmd): + raise AsciiDocError('missing ASCIIDOC_PY file: %s' % cmd) + elif asciidoc_py: + # Next try path specified by caller. + cmd = asciidoc_py + if not os.path.isfile(cmd): + raise AsciiDocError('missing file: %s' % cmd) + else: + # Try shell search paths. + for fname in ['asciidoc.py','asciidoc.pyc','asciidoc']: + cmd = find_in_path(fname) + if cmd: break + else: + # Finally try current working directory. + for cmd in ['asciidoc.py','asciidoc.pyc','asciidoc']: + if os.path.isfile(cmd): break + else: + raise AsciiDocError('failed to locate asciidoc') + self.cmd = os.path.realpath(cmd) + self.__import_asciidoc() + + def __import_asciidoc(self, reload=False): + ''' + Import asciidoc module (script or compiled .pyc). + See + http://groups.google.com/group/asciidoc/browse_frm/thread/66e7b59d12cd2f91 + for an explanation of why a seemingly straight-forward job turned out + quite complicated. + ''' + if os.path.splitext(self.cmd)[1] in ['.py','.pyc']: + sys.path.insert(0, os.path.dirname(self.cmd)) + try: + try: + if reload: + import __builtin__ # Because reload() is shadowed. + __builtin__.reload(self.asciidoc) + else: + import asciidoc + self.asciidoc = asciidoc + except ImportError: + raise AsciiDocError('failed to import ' + self.cmd) + finally: + del sys.path[0] + else: + # The import statement can only handle .py or .pyc files, have to + # use imp.load_source() for scripts with other names. + try: + imp.load_source('asciidoc', self.cmd) + import asciidoc + self.asciidoc = asciidoc + except ImportError: + raise AsciiDocError('failed to import ' + self.cmd) + if Version(self.asciidoc.VERSION) < Version(MIN_ASCIIDOC_VERSION): + raise AsciiDocError( + 'asciidocapi %s requires asciidoc %s or better' + % (API_VERSION, MIN_ASCIIDOC_VERSION)) + + def execute(self, infile, outfile=None, backend=None): + """ + Compile infile to outfile using backend format. + infile can outfile can be file path strings or file like objects. + """ + self.messages = [] + opts = Options(self.options.values) + if outfile is not None: + opts('--out-file', outfile) + if backend is not None: + opts('--backend', backend) + for k,v in self.attributes.items(): + if v == '' or k[-1] in '!@': + s = k + elif v is None: # A None value undefines the attribute. + s = k + '!' + else: + s = '%s=%s' % (k,v) + opts('--attribute', s) + args = [infile] + # The AsciiDoc command was designed to process source text then + # exit, there are globals and statics in asciidoc.py that have + # to be reinitialized before each run -- hence the reload. + self.__import_asciidoc(reload=True) + try: + try: + self.asciidoc.execute(self.cmd, opts.values, args) + finally: + self.messages = self.asciidoc.messages[:] + except SystemExit, e: + if e.code: + raise AsciiDocError(self.messages[-1]) + + +if __name__ == "__main__": + """ + Run module doctests. + """ + import doctest + options = doctest.NORMALIZE_WHITESPACE + doctest.ELLIPSIS + doctest.testmod(optionflags=options) diff --git a/source-builder/sb/images/rtemswhitebg.jpg b/source-builder/sb/images/rtemswhitebg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f883f2c495d4d91ef94d35e28995b9d2b7a863f7 GIT binary patch literal 117890 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≶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 literal 0 HcmV?d00001