3 asciidocapi - AsciiDoc API wrapper class.
5 The AsciiDocAPI class provides an API for executing asciidoc. Minimal example
6 compiles `mydoc.txt` to `mydoc.html`:
9 asciidoc = asciidocapi.AsciiDocAPI()
10 asciidoc.execute('mydoc.txt')
12 - Full documentation in asciidocapi.txt.
13 - See the doctests below for more examples.
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>
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>
36 2. Check error handling:
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
50 Copyright (C) 2009 Stuart Rackham. Free use of this software is granted
51 under the terms of the GNU General Public License (GPL).
58 MIN_ASCIIDOC_VERSION = '8.4.1' # Minimum acceptable AsciiDoc version.
61 def find_in_path(fname, path=None):
63 Find file fname in paths. Return None if not found.
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):
75 class AsciiDocError(Exception):
79 class Options(object):
81 Stores asciidoc(1) command options.
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):
91 self.values.append((name,value))
94 class Version(object):
96 Parse and compare AsciiDoc version numbers. Instance attributes:
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
107 >>> Version('8.2.5') < Version('8.3 beta 1')
109 >>> Version('8.3.0') == Version('8.3. beta 1')
111 >>> Version('8.2.0') < Version('8.20')
113 >>> Version('8.20').major
115 >>> Version('8.20').minor
117 >>> Version('8.20').micro
119 >>> Version('8.20').suffix
121 >>> Version('8.20 beta 1').suffix
125 def __init__(self, version):
126 self.string = version
127 reo = re.match(r'^(\d+)\.(\d+)(\.(\d+))?\s*(.*?)\s*$', self.string)
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)
138 result = cmp(self.minor, other.minor)
140 result = cmp(self.micro, other.micro)
144 class AsciiDocAPI(object):
148 def __init__(self, asciidoc_py=None):
150 Locate and import asciidoc.py.
151 Initialize instance attributes.
153 self.options = Options()
156 # Search for the asciidoc command file.
157 # Try ASCIIDOC_PY environment variable first.
158 cmd = os.environ.get('ASCIIDOC_PY')
160 if not os.path.isfile(cmd):
161 raise AsciiDocError('missing ASCIIDOC_PY file: %s' % cmd)
163 # Next try path specified by caller.
165 if not os.path.isfile(cmd):
166 raise AsciiDocError('missing file: %s' % cmd)
168 # Try shell search paths.
169 for fname in ['asciidoc.py','asciidoc.pyc','asciidoc']:
170 cmd = find_in_path(fname)
173 # Finally try current working directory.
174 for cmd in ['asciidoc.py','asciidoc.pyc','asciidoc']:
175 if os.path.isfile(cmd): break
177 raise AsciiDocError('failed to locate asciidoc')
178 self.cmd = os.path.realpath(cmd)
179 self.__import_asciidoc()
181 def __import_asciidoc(self, reload=False):
183 Import asciidoc module (script or compiled .pyc).
185 http://groups.google.com/group/asciidoc/browse_frm/thread/66e7b59d12cd2f91
186 for an explanation of why a seemingly straight-forward job turned out
189 if os.path.splitext(self.cmd)[1] in ['.py','.pyc']:
190 sys.path.insert(0, os.path.dirname(self.cmd))
194 import __builtin__ # Because reload() is shadowed.
195 __builtin__.reload(self.asciidoc)
198 self.asciidoc = asciidoc
200 raise AsciiDocError('failed to import ' + self.cmd)
204 # The import statement can only handle .py or .pyc files, have to
205 # use imp.load_source() for scripts with other names.
207 imp.load_source('asciidoc', self.cmd)
209 self.asciidoc = asciidoc
211 raise AsciiDocError('failed to import ' + self.cmd)
212 if Version(self.asciidoc.VERSION) < Version(MIN_ASCIIDOC_VERSION):
214 'asciidocapi %s requires asciidoc %s or better'
215 % (API_VERSION, MIN_ASCIIDOC_VERSION))
217 def execute(self, infile, outfile=None, backend=None):
219 Compile infile to outfile using backend format.
220 infile can outfile can be file path strings or file like objects.
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 '!@':
231 elif v is None: # A None value undefines the attribute.
235 opts('--attribute', s)
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)
243 self.asciidoc.execute(self.cmd, opts.values, args)
245 self.messages = self.asciidoc.messages[:]
246 except SystemExit, e:
248 raise AsciiDocError(self.messages[-1])
251 if __name__ == "__main__":
256 options = doctest.NORMALIZE_WHITESPACE + doctest.ELLIPSIS
257 doctest.testmod(optionflags=options)