resetting manifest requested domain to floor
[platform/upstream/asciidoc.git] / asciidocapi.py
1 #!/usr/bin/env python
2 """
3 asciidocapi - AsciiDoc API wrapper class.
4
5 The AsciiDocAPI class provides an API for executing asciidoc. Minimal example
6 compiles `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
15 Doctests:
16
17 1. 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
36 2. 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
50 Copyright (C) 2009 Stuart Rackham. Free use of this software is granted
51 under the terms of the GNU General Public License (GPL).
52
53 """
54
55 import sys,os,re,imp
56
57 API_VERSION = '0.1.2'
58 MIN_ASCIIDOC_VERSION = '8.4.1'  # Minimum acceptable AsciiDoc version.
59
60
61 def 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
75 class AsciiDocError(Exception):
76     pass
77
78
79 class 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
94 class 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
144 class 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')
178         self.cmd = os.path.realpath(cmd)
179         self.__import_asciidoc()
180
181     def __import_asciidoc(self, reload=False):
182         '''
183         Import asciidoc module (script or compiled .pyc).
184         See
185         http://groups.google.com/group/asciidoc/browse_frm/thread/66e7b59d12cd2f91
186         for an explanation of why a seemingly straight-forward job turned out
187         quite complicated.
188         '''
189         if os.path.splitext(self.cmd)[1] in ['.py','.pyc']:
190             sys.path.insert(0, os.path.dirname(self.cmd))
191             try:
192                 try:
193                     if reload:
194                         import __builtin__  # Because reload() is shadowed.
195                         __builtin__.reload(self.asciidoc)
196                     else:
197                         import asciidoc
198                         self.asciidoc = asciidoc
199                 except ImportError:
200                     raise AsciiDocError('failed to import ' + self.cmd)
201             finally:
202                 del sys.path[0]
203         else:
204             # The import statement can only handle .py or .pyc files, have to
205             # use imp.load_source() for scripts with other names.
206             try:
207                 imp.load_source('asciidoc', self.cmd)
208                 import asciidoc
209                 self.asciidoc = asciidoc
210             except ImportError:
211                 raise AsciiDocError('failed to import ' + self.cmd)
212         if Version(self.asciidoc.VERSION) < Version(MIN_ASCIIDOC_VERSION):
213             raise AsciiDocError(
214                 'asciidocapi %s requires asciidoc %s or better'
215                 % (API_VERSION, MIN_ASCIIDOC_VERSION))
216
217     def execute(self, infile, outfile=None, backend=None):
218         """
219         Compile infile to outfile using backend format.
220         infile can outfile can be file path strings or file like objects.
221         """
222         self.messages = []
223         opts = Options(self.options.values)
224         if outfile is not None:
225             opts('--out-file', outfile)
226         if backend is not None:
227             opts('--backend', backend)
228         for k,v in self.attributes.items():
229             if v == '' or k[-1] in '!@':
230                 s = k
231             elif v is None: # A None value undefines the attribute.
232                 s = k + '!'
233             else:
234                 s = '%s=%s' % (k,v)
235             opts('--attribute', s)
236         args = [infile]
237         # The AsciiDoc command was designed to process source text then
238         # exit, there are globals and statics in asciidoc.py that have
239         # to be reinitialized before each run -- hence the reload.
240         self.__import_asciidoc(reload=True)
241         try:
242             try:
243                 self.asciidoc.execute(self.cmd, opts.values, args)
244             finally:
245                 self.messages = self.asciidoc.messages[:]
246         except SystemExit, e:
247             if e.code:
248                 raise AsciiDocError(self.messages[-1])
249
250
251 if __name__ == "__main__":
252     """
253     Run module doctests.
254     """
255     import doctest
256     options = doctest.NORMALIZE_WHITESPACE + doctest.ELLIPSIS
257     doctest.testmod(optionflags=options)