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)