all repos — markup @ v0.1.3

The code we use to render README.your_favorite_markup

lib/github/commands/asciidocapi.py (view raw)

  1#!/usr/bin/env python
  2"""
  3asciidocapi - AsciiDoc API wrapper class.
  4
  5The AsciiDocAPI class provides an API for executing asciidoc. Minimal example
  6compiles `mydoc.txt` to `mydoc.html`:
  7
  8  import asciidocapi
  9  asciidoc = asciidocapi.AsciiDocAPI()
 10  asciidoc.execute('mydoc.txt')
 11
 12- Full documentation in asciidocapi.txt.
 13- See the doctests below for more examples.
 14
 15Doctests:
 16
 171. Check execution:
 18
 19   >>> import StringIO
 20   >>> infile = StringIO.StringIO('Hello *{author}*')
 21   >>> outfile = StringIO.StringIO()
 22   >>> asciidoc = AsciiDocAPI()
 23   >>> asciidoc.options('--no-header-footer')
 24   >>> asciidoc.attributes['author'] = 'Joe Bloggs'
 25   >>> asciidoc.execute(infile, outfile, backend='html4')
 26   >>> print outfile.getvalue()
 27   <p>Hello <strong>Joe Bloggs</strong></p>
 28
 29   >>> asciidoc.attributes['author'] = 'Bill Smith'
 30   >>> infile = StringIO.StringIO('Hello _{author}_')
 31   >>> outfile = StringIO.StringIO()
 32   >>> asciidoc.execute(infile, outfile, backend='docbook')
 33   >>> print outfile.getvalue()
 34   <simpara>Hello <emphasis>Bill Smith</emphasis></simpara>
 35
 362. Check error handling:
 37
 38   >>> import StringIO
 39   >>> asciidoc = AsciiDocAPI()
 40   >>> infile = StringIO.StringIO('---------')
 41   >>> outfile = StringIO.StringIO()
 42   >>> asciidoc.execute(infile, outfile)
 43   Traceback (most recent call last):
 44     File "<stdin>", line 1, in <module>
 45     File "asciidocapi.py", line 189, in execute
 46       raise AsciiDocError(self.messages[-1])
 47   AsciiDocError: ERROR: <stdin>: line 1: [blockdef-listing] missing closing delimiter
 48
 49
 50Copyright (C) 2009 Stuart Rackham. Free use of this software is granted
 51under the terms of the GNU General Public License (GPL).
 52
 53"""
 54
 55import sys,os,re
 56
 57API_VERSION = '0.1.1'
 58MIN_ASCIIDOC_VERSION = '8.4.1'  # Minimum acceptable AsciiDoc version.
 59
 60
 61def find_in_path(fname, path=None):
 62    """
 63    Find file fname in paths. Return None if not found.
 64    """
 65    if path is None:
 66        path = os.environ.get('PATH', '')
 67    for dir in path.split(os.pathsep):
 68        fpath = os.path.join(dir, fname)
 69        if os.path.isfile(fpath):
 70            return fpath
 71    else:
 72        return None
 73
 74
 75class AsciiDocError(Exception):
 76    pass
 77
 78
 79class Options(object):
 80    """
 81    Stores asciidoc(1) command options.
 82    """
 83    def __init__(self, values=[]):
 84        self.values = values[:]
 85    def __call__(self, name, value=None):
 86        """Shortcut for append method."""
 87        self.append(name, value)
 88    def append(self, name, value=None):
 89        if type(value) in (int,float):
 90            value = str(value)
 91        self.values.append((name,value))
 92
 93
 94class Version(object):
 95    """
 96    Parse and compare AsciiDoc version numbers. Instance attributes:
 97
 98    string: String version number '<major>.<minor>[.<micro>][suffix]'.
 99    major:  Integer major version number.
100    minor:  Integer minor version number.
101    micro:  Integer micro version number.
102    suffix: Suffix (begins with non-numeric character) is ignored when
103            comparing.
104
105    Doctest examples:
106
107    >>> Version('8.2.5') < Version('8.3 beta 1')
108    True
109    >>> Version('8.3.0') == Version('8.3. beta 1')
110    True
111    >>> Version('8.2.0') < Version('8.20')
112    True
113    >>> Version('8.20').major
114    8
115    >>> Version('8.20').minor
116    20
117    >>> Version('8.20').micro
118    0
119    >>> Version('8.20').suffix
120    ''
121    >>> Version('8.20 beta 1').suffix
122    'beta 1'
123
124    """
125    def __init__(self, version):
126        self.string = version
127        reo = re.match(r'^(\d+)\.(\d+)(\.(\d+))?\s*(.*?)\s*$', self.string)
128        if not reo:
129            raise ValueError('invalid version number: %s' % self.string)
130        groups = reo.groups()
131        self.major = int(groups[0])
132        self.minor = int(groups[1])
133        self.micro = int(groups[3] or '0')
134        self.suffix = groups[4] or ''
135    def __cmp__(self, other):
136        result = cmp(self.major, other.major)
137        if result == 0:
138            result = cmp(self.minor, other.minor)
139            if result == 0:
140                result = cmp(self.micro, other.micro)
141        return result
142
143
144class AsciiDocAPI(object):
145    """
146    AsciiDoc API class.
147    """
148    def __init__(self, asciidoc_py=None):
149        """
150        Locate and import asciidoc.py.
151        Initialize instance attributes.
152        """
153        self.options = Options()
154        self.attributes = {}
155        self.messages = []
156        # Search for the asciidoc command file.
157        # Try ASCIIDOC_PY environment variable first.
158        cmd = os.environ.get('ASCIIDOC_PY')
159        if cmd:
160            if not os.path.isfile(cmd):
161                raise AsciiDocError('missing ASCIIDOC_PY file: %s' % cmd)
162        elif asciidoc_py:
163            # Next try path specified by caller.
164            cmd = asciidoc_py
165            if not os.path.isfile(cmd):
166                raise AsciiDocError('missing file: %s' % cmd)
167        else:
168            # Try shell search paths.
169            for fname in ['asciidoc.py','asciidoc.pyc','asciidoc']:
170                cmd = find_in_path(fname)
171                if cmd: break
172            else:
173                # Finally try current working directory.
174                for cmd in ['asciidoc.py','asciidoc.pyc','asciidoc']:
175                    if os.path.isfile(cmd): break
176                else:
177                    raise AsciiDocError('failed to locate asciidoc.py[c]')
178        cmd = os.path.realpath(cmd)
179        if os.path.splitext(cmd)[1] not in ['.py','.pyc']:
180            raise AsciiDocError('invalid Python module name: %s' % cmd)
181        sys.path.insert(0, os.path.dirname(cmd))
182        try:
183            try:
184                import asciidoc
185            except ImportError:
186                raise AsciiDocError('failed to import asciidoc')
187        finally:
188            del sys.path[0]
189        if Version(asciidoc.VERSION) < Version(MIN_ASCIIDOC_VERSION):
190            raise AsciiDocError(
191                'asciidocapi %s requires asciidoc %s or better'
192                % (API_VERSION, MIN_ASCIIDOC_VERSION))
193        self.asciidoc = asciidoc
194        self.cmd = cmd
195
196    def execute(self, infile, outfile=None, backend=None):
197        """
198        Compile infile to outfile using backend format.
199        infile can outfile can be file path strings or file like objects.
200        """
201        self.messages = []
202        opts = Options(self.options.values)
203        if outfile is not None:
204            opts('--out-file', outfile)
205        if backend is not None:
206            opts('--backend', backend)
207        for k,v in self.attributes.items():
208            if v == '' or k[-1] in '!@':
209                s = k
210            elif v is None: # A None value undefines the attribute.
211                s = k + '!'
212            else:
213                s = '%s=%s' % (k,v)
214            opts('--attribute', s)
215        args = [infile]
216        sys.path.insert(0, os.path.dirname(self.cmd))
217        try:
218            # The AsciiDoc command was designed to process source text then
219            # exit, there are globals and statics in asciidoc.py that have
220            # to be reinitialized before each run -- hence the reload.
221            reload(self.asciidoc)
222        finally:
223            del sys.path[0]
224        try:
225            try:
226                self.asciidoc.execute(self.cmd, opts.values, args)
227            finally:
228                self.messages = self.asciidoc.messages[:]
229        except SystemExit, e:
230            if e.code:
231                raise AsciiDocError(self.messages[-1])
232
233
234if __name__ == "__main__":
235    """
236    Run module doctests.
237    """
238    import doctest
239    options = doctest.NORMALIZE_WHITESPACE + doctest.ELLIPSIS
240    doctest.testmod(optionflags=options)