123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- #!/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)
|