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) |
---|