1 | #!/usr/bin/env python |
---|
2 | ''' |
---|
3 | a2x - A toolchain manager for AsciiDoc (converts Asciidoc text files to other |
---|
4 | file formats) |
---|
5 | |
---|
6 | Copyright: Stuart Rackham (c) 2009 |
---|
7 | License: MIT |
---|
8 | Email: srackham@gmail.com |
---|
9 | |
---|
10 | ''' |
---|
11 | |
---|
12 | import os |
---|
13 | import fnmatch |
---|
14 | import HTMLParser |
---|
15 | import re |
---|
16 | import shutil |
---|
17 | import subprocess |
---|
18 | import sys |
---|
19 | import traceback |
---|
20 | import urlparse |
---|
21 | import zipfile |
---|
22 | import xml.dom.minidom |
---|
23 | import mimetypes |
---|
24 | |
---|
25 | PROG = os.path.basename(os.path.splitext(__file__)[0]) |
---|
26 | VERSION = '8.6.8' |
---|
27 | |
---|
28 | # AsciiDoc global configuration file directory. |
---|
29 | # NOTE: CONF_DIR is "fixed up" by Makefile -- don't rename or change syntax. |
---|
30 | CONF_DIR = '/etc/asciidoc' |
---|
31 | |
---|
32 | |
---|
33 | ###################################################################### |
---|
34 | # Default configuration file parameters. |
---|
35 | ###################################################################### |
---|
36 | |
---|
37 | # Optional environment variable dictionary passed to |
---|
38 | # executing programs. If set to None the existing |
---|
39 | # environment is used. |
---|
40 | ENV = None |
---|
41 | |
---|
42 | # External executables. |
---|
43 | ASCIIDOC = 'asciidoc' |
---|
44 | XSLTPROC = 'xsltproc' |
---|
45 | DBLATEX = 'dblatex' # pdf generation. |
---|
46 | FOP = 'fop' # pdf generation (--fop option). |
---|
47 | W3M = 'w3m' # text generation. |
---|
48 | LYNX = 'lynx' # text generation (if no w3m). |
---|
49 | XMLLINT = 'xmllint' # Set to '' to disable. |
---|
50 | EPUBCHECK = 'epubcheck' # Set to '' to disable. |
---|
51 | # External executable default options. |
---|
52 | ASCIIDOC_OPTS = '' |
---|
53 | DBLATEX_OPTS = '' |
---|
54 | FOP_OPTS = '' |
---|
55 | XSLTPROC_OPTS = '' |
---|
56 | BACKEND_OPTS = '' |
---|
57 | |
---|
58 | ###################################################################### |
---|
59 | # End of configuration file parameters. |
---|
60 | ###################################################################### |
---|
61 | |
---|
62 | |
---|
63 | ##################################################################### |
---|
64 | # Utility functions |
---|
65 | ##################################################################### |
---|
66 | |
---|
67 | OPTIONS = None # These functions read verbose and dry_run command options. |
---|
68 | |
---|
69 | def errmsg(msg): |
---|
70 | sys.stderr.write('%s: %s\n' % (PROG,msg)) |
---|
71 | |
---|
72 | def warning(msg): |
---|
73 | errmsg('WARNING: %s' % msg) |
---|
74 | |
---|
75 | def infomsg(msg): |
---|
76 | print '%s: %s' % (PROG,msg) |
---|
77 | |
---|
78 | def die(msg, exit_code=1): |
---|
79 | errmsg('ERROR: %s' % msg) |
---|
80 | sys.exit(exit_code) |
---|
81 | |
---|
82 | def trace(): |
---|
83 | """Print traceback to stderr.""" |
---|
84 | errmsg('-'*60) |
---|
85 | traceback.print_exc(file=sys.stderr) |
---|
86 | errmsg('-'*60) |
---|
87 | |
---|
88 | def verbose(msg): |
---|
89 | if OPTIONS.verbose or OPTIONS.dry_run: |
---|
90 | infomsg(msg) |
---|
91 | |
---|
92 | class AttrDict(dict): |
---|
93 | """ |
---|
94 | Like a dictionary except values can be accessed as attributes i.e. obj.foo |
---|
95 | can be used in addition to obj['foo']. |
---|
96 | If self._default has been set then it will be returned if a non-existant |
---|
97 | attribute is accessed (instead of raising an AttributeError). |
---|
98 | """ |
---|
99 | def __getattr__(self, key): |
---|
100 | try: |
---|
101 | return self[key] |
---|
102 | except KeyError, k: |
---|
103 | if self.has_key('_default'): |
---|
104 | return self['_default'] |
---|
105 | else: |
---|
106 | raise AttributeError, k |
---|
107 | def __setattr__(self, key, value): |
---|
108 | self[key] = value |
---|
109 | def __delattr__(self, key): |
---|
110 | try: del self[key] |
---|
111 | except KeyError, k: raise AttributeError, k |
---|
112 | def __repr__(self): |
---|
113 | return '<AttrDict ' + dict.__repr__(self) + '>' |
---|
114 | def __getstate__(self): |
---|
115 | return dict(self) |
---|
116 | def __setstate__(self,value): |
---|
117 | for k,v in value.items(): self[k]=v |
---|
118 | |
---|
119 | def isexecutable(file_name): |
---|
120 | return os.path.isfile(file_name) and os.access(file_name, os.X_OK) |
---|
121 | |
---|
122 | def find_executable(file_name): |
---|
123 | ''' |
---|
124 | Search for executable file_name in the system PATH. |
---|
125 | Return full path name or None if not found. |
---|
126 | ''' |
---|
127 | def _find_executable(file_name): |
---|
128 | if os.path.split(file_name)[0] != '': |
---|
129 | # file_name includes directory so don't search path. |
---|
130 | if not isexecutable(file_name): |
---|
131 | return None |
---|
132 | else: |
---|
133 | return file_name |
---|
134 | for p in os.environ.get('PATH', os.defpath).split(os.pathsep): |
---|
135 | f = os.path.join(p, file_name) |
---|
136 | if isexecutable(f): |
---|
137 | return os.path.realpath(f) |
---|
138 | return None |
---|
139 | if os.name == 'nt' and os.path.splitext(file_name)[1] == '': |
---|
140 | for ext in ('.cmd','.bat','.exe'): |
---|
141 | result = _find_executable(file_name + ext) |
---|
142 | if result: break |
---|
143 | else: |
---|
144 | result = _find_executable(file_name) |
---|
145 | return result |
---|
146 | |
---|
147 | def write_file(filename, data, mode='w'): |
---|
148 | f = open(filename, mode) |
---|
149 | try: |
---|
150 | f.write(data) |
---|
151 | finally: |
---|
152 | f.close() |
---|
153 | |
---|
154 | def read_file(filename, mode='r'): |
---|
155 | f = open(filename, mode) |
---|
156 | try: |
---|
157 | return f.read() |
---|
158 | finally: |
---|
159 | f.close() |
---|
160 | |
---|
161 | def shell_cd(path): |
---|
162 | verbose('chdir %s' % path) |
---|
163 | if not OPTIONS.dry_run: |
---|
164 | os.chdir(path) |
---|
165 | |
---|
166 | def shell_makedirs(path): |
---|
167 | if os.path.isdir(path): |
---|
168 | return |
---|
169 | verbose('creating %s' % path) |
---|
170 | if not OPTIONS.dry_run: |
---|
171 | os.makedirs(path) |
---|
172 | |
---|
173 | def shell_copy(src, dst): |
---|
174 | verbose('copying "%s" to "%s"' % (src,dst)) |
---|
175 | if not OPTIONS.dry_run: |
---|
176 | shutil.copy(src, dst) |
---|
177 | |
---|
178 | def shell_rm(path): |
---|
179 | if not os.path.exists(path): |
---|
180 | return |
---|
181 | verbose('deleting %s' % path) |
---|
182 | if not OPTIONS.dry_run: |
---|
183 | os.unlink(path) |
---|
184 | |
---|
185 | def shell_rmtree(path): |
---|
186 | if not os.path.isdir(path): |
---|
187 | return |
---|
188 | verbose('deleting %s' % path) |
---|
189 | if not OPTIONS.dry_run: |
---|
190 | shutil.rmtree(path) |
---|
191 | |
---|
192 | def shell(cmd, raise_error=True): |
---|
193 | ''' |
---|
194 | Execute command cmd in shell and return tuple |
---|
195 | (stdoutdata, stderrdata, returncode). |
---|
196 | If raise_error is True then a non-zero return terminates the application. |
---|
197 | ''' |
---|
198 | if os.name == 'nt': |
---|
199 | # TODO: this is probably unnecessary, see: |
---|
200 | # http://groups.google.com/group/asciidoc/browse_frm/thread/9442ee0c419f1242 |
---|
201 | # Windows doesn't like running scripts directly so explicitly |
---|
202 | # specify python interpreter. |
---|
203 | # Extract first (quoted or unquoted) argument. |
---|
204 | mo = re.match(r'^\s*"\s*(?P<arg0>[^"]+)\s*"', cmd) |
---|
205 | if not mo: |
---|
206 | mo = re.match(r'^\s*(?P<arg0>[^ ]+)', cmd) |
---|
207 | if mo.group('arg0').endswith('.py'): |
---|
208 | cmd = 'python ' + cmd |
---|
209 | # Remove redundant quoting -- this is not just cosmetic, |
---|
210 | # quoting seems to dramatically decrease the allowed command |
---|
211 | # length in Windows XP. |
---|
212 | cmd = re.sub(r'"([^ ]+?)"', r'\1', cmd) |
---|
213 | verbose('executing: %s' % cmd) |
---|
214 | if OPTIONS.dry_run: |
---|
215 | return |
---|
216 | stdout = stderr = subprocess.PIPE |
---|
217 | try: |
---|
218 | popen = subprocess.Popen(cmd, stdout=stdout, stderr=stderr, |
---|
219 | shell=True, env=ENV) |
---|
220 | except OSError, e: |
---|
221 | die('failed: %s: %s' % (cmd, e)) |
---|
222 | stdoutdata, stderrdata = popen.communicate() |
---|
223 | if OPTIONS.verbose: |
---|
224 | print stdoutdata |
---|
225 | print stderrdata |
---|
226 | if popen.returncode != 0 and raise_error: |
---|
227 | die('%s returned non-zero exit status %d' % (cmd, popen.returncode)) |
---|
228 | return (stdoutdata, stderrdata, popen.returncode) |
---|
229 | |
---|
230 | def find_resources(files, tagname, attrname, filter=None): |
---|
231 | ''' |
---|
232 | Search all files and return a list of local URIs from attrname attribute |
---|
233 | values in tagname tags. |
---|
234 | Handles HTML open and XHTML closed tags. |
---|
235 | Non-local URIs are skipped. |
---|
236 | files can be a file name or a list of file names. |
---|
237 | The filter function takes a dictionary of tag attributes and returns True if |
---|
238 | the URI is to be included. |
---|
239 | ''' |
---|
240 | class FindResources(HTMLParser.HTMLParser): |
---|
241 | # Nested parser class shares locals with enclosing function. |
---|
242 | def handle_startendtag(self, tag, attrs): |
---|
243 | self.handle_starttag(tag, attrs) |
---|
244 | def handle_starttag(self, tag, attrs): |
---|
245 | attrs = dict(attrs) |
---|
246 | if tag == tagname and (filter is None or filter(attrs)): |
---|
247 | # Accept only local URIs. |
---|
248 | uri = urlparse.urlparse(attrs[attrname]) |
---|
249 | if uri[0] in ('','file') and not uri[1] and uri[2]: |
---|
250 | result.append(uri[2]) |
---|
251 | if isinstance(files, str): |
---|
252 | files = [files] |
---|
253 | result = [] |
---|
254 | for filename in files: |
---|
255 | verbose('finding resources in: %s' % filename) |
---|
256 | if OPTIONS.dry_run: |
---|
257 | continue |
---|
258 | parser = FindResources() |
---|
259 | # HTMLParser has problems with non-ASCII strings. |
---|
260 | # See http://bugs.python.org/issue3932 |
---|
261 | contents = read_file(filename) |
---|
262 | mo = re.search(r'\A<\?xml.* encoding="(.*?)"', contents) |
---|
263 | if mo: |
---|
264 | encoding = mo.group(1) |
---|
265 | parser.feed(contents.decode(encoding)) |
---|
266 | else: |
---|
267 | parser.feed(contents) |
---|
268 | parser.close() |
---|
269 | result = list(set(result)) # Drop duplicate values. |
---|
270 | result.sort() |
---|
271 | return result |
---|
272 | |
---|
273 | # NOT USED. |
---|
274 | def copy_files(files, src_dir, dst_dir): |
---|
275 | ''' |
---|
276 | Copy list of relative file names from src_dir to dst_dir. |
---|
277 | ''' |
---|
278 | for filename in files: |
---|
279 | filename = os.path.normpath(filename) |
---|
280 | if os.path.isabs(filename): |
---|
281 | continue |
---|
282 | src = os.path.join(src_dir, filename) |
---|
283 | dst = os.path.join(dst_dir, filename) |
---|
284 | if not os.path.exists(dst): |
---|
285 | if not os.path.isfile(src): |
---|
286 | warning('missing file: %s' % src) |
---|
287 | continue |
---|
288 | dstdir = os.path.dirname(dst) |
---|
289 | shell_makedirs(dstdir) |
---|
290 | shell_copy(src, dst) |
---|
291 | |
---|
292 | def find_files(path, pattern): |
---|
293 | ''' |
---|
294 | Return list of file names matching pattern in directory path. |
---|
295 | ''' |
---|
296 | result = [] |
---|
297 | for (p,dirs,files) in os.walk(path): |
---|
298 | for f in files: |
---|
299 | if fnmatch.fnmatch(f, pattern): |
---|
300 | result.append(os.path.normpath(os.path.join(p,f))) |
---|
301 | return result |
---|
302 | |
---|
303 | def exec_xsltproc(xsl_file, xml_file, dst_dir, opts = ''): |
---|
304 | cwd = os.getcwd() |
---|
305 | shell_cd(dst_dir) |
---|
306 | try: |
---|
307 | shell('"%s" %s "%s" "%s"' % (XSLTPROC, opts, xsl_file, xml_file)) |
---|
308 | finally: |
---|
309 | shell_cd(cwd) |
---|
310 | |
---|
311 | def get_source_options(asciidoc_file): |
---|
312 | ''' |
---|
313 | Look for a2x command options in AsciiDoc source file. |
---|
314 | Limitation: options cannot contain double-quote characters. |
---|
315 | ''' |
---|
316 | def parse_options(): |
---|
317 | # Parse options to result sequence. |
---|
318 | inquotes = False |
---|
319 | opt = '' |
---|
320 | for c in options: |
---|
321 | if c == '"': |
---|
322 | if inquotes: |
---|
323 | result.append(opt) |
---|
324 | opt = '' |
---|
325 | inquotes = False |
---|
326 | else: |
---|
327 | inquotes = True |
---|
328 | elif c == ' ': |
---|
329 | if inquotes: |
---|
330 | opt += c |
---|
331 | elif opt: |
---|
332 | result.append(opt) |
---|
333 | opt = '' |
---|
334 | else: |
---|
335 | opt += c |
---|
336 | if opt: |
---|
337 | result.append(opt) |
---|
338 | |
---|
339 | result = [] |
---|
340 | if os.path.isfile(asciidoc_file): |
---|
341 | options = '' |
---|
342 | f = open(asciidoc_file) |
---|
343 | try: |
---|
344 | for line in f: |
---|
345 | mo = re.search(r'^//\s*a2x:', line) |
---|
346 | if mo: |
---|
347 | options += ' ' + line[mo.end():].strip() |
---|
348 | finally: |
---|
349 | f.close() |
---|
350 | parse_options() |
---|
351 | return result |
---|
352 | |
---|
353 | |
---|
354 | ##################################################################### |
---|
355 | # Application class |
---|
356 | ##################################################################### |
---|
357 | |
---|
358 | class A2X(AttrDict): |
---|
359 | ''' |
---|
360 | a2x options and conversion functions. |
---|
361 | ''' |
---|
362 | |
---|
363 | def execute(self): |
---|
364 | ''' |
---|
365 | Process a2x command. |
---|
366 | ''' |
---|
367 | self.process_options() |
---|
368 | # Append configuration file options. |
---|
369 | self.asciidoc_opts += ' ' + ASCIIDOC_OPTS |
---|
370 | self.dblatex_opts += ' ' + DBLATEX_OPTS |
---|
371 | self.fop_opts += ' ' + FOP_OPTS |
---|
372 | self.xsltproc_opts += ' ' + XSLTPROC_OPTS |
---|
373 | self.backend_opts += ' ' + BACKEND_OPTS |
---|
374 | # Execute to_* functions. |
---|
375 | if self.backend: |
---|
376 | self.to_backend() |
---|
377 | else: |
---|
378 | self.__getattribute__('to_'+self.format)() |
---|
379 | if not (self.keep_artifacts or self.format == 'docbook' or self.skip_asciidoc): |
---|
380 | shell_rm(self.dst_path('.xml')) |
---|
381 | |
---|
382 | def load_conf(self): |
---|
383 | ''' |
---|
384 | Load a2x configuration file from default locations and --conf-file |
---|
385 | option. |
---|
386 | ''' |
---|
387 | global ASCIIDOC |
---|
388 | CONF_FILE = 'a2x.conf' |
---|
389 | a2xdir = os.path.dirname(os.path.realpath(__file__)) |
---|
390 | conf_files = [] |
---|
391 | # From a2x.py directory. |
---|
392 | conf_files.append(os.path.join(a2xdir, CONF_FILE)) |
---|
393 | # If the asciidoc executable and conf files are in the a2x directory |
---|
394 | # then use the local copy of asciidoc and skip the global a2x conf. |
---|
395 | asciidoc = os.path.join(a2xdir, 'asciidoc.py') |
---|
396 | asciidoc_conf = os.path.join(a2xdir, 'asciidoc.conf') |
---|
397 | if os.path.isfile(asciidoc) and os.path.isfile(asciidoc_conf): |
---|
398 | self.asciidoc = asciidoc |
---|
399 | else: |
---|
400 | self.asciidoc = None |
---|
401 | # From global conf directory. |
---|
402 | conf_files.append(os.path.join(CONF_DIR, CONF_FILE)) |
---|
403 | # From $HOME directory. |
---|
404 | home_dir = os.environ.get('HOME') |
---|
405 | if home_dir is not None: |
---|
406 | conf_files.append(os.path.join(home_dir, '.asciidoc', CONF_FILE)) |
---|
407 | # If asciidoc is not local to a2x then search the PATH. |
---|
408 | if not self.asciidoc: |
---|
409 | self.asciidoc = find_executable(ASCIIDOC) |
---|
410 | if not self.asciidoc: |
---|
411 | die('unable to find asciidoc: %s' % ASCIIDOC) |
---|
412 | # From backend plugin directory. |
---|
413 | if self.backend is not None: |
---|
414 | stdout = shell(self.asciidoc + ' --backend list')[0] |
---|
415 | backends = [(i, os.path.split(i)[1]) for i in stdout.splitlines()] |
---|
416 | backend_dir = [i[0] for i in backends if i[1] == self.backend] |
---|
417 | if len(backend_dir) == 0: |
---|
418 | die('missing %s backend' % self.backend) |
---|
419 | if len(backend_dir) > 1: |
---|
420 | die('more than one %s backend' % self.backend) |
---|
421 | verbose('found %s backend directory: %s' % |
---|
422 | (self.backend, backend_dir[0])) |
---|
423 | conf_files.append(os.path.join(backend_dir[0], 'a2x-backend.py')) |
---|
424 | # From --conf-file option. |
---|
425 | if self.conf_file is not None: |
---|
426 | if not os.path.isfile(self.conf_file): |
---|
427 | die('missing configuration file: %s' % self.conf_file) |
---|
428 | conf_files.append(self.conf_file) |
---|
429 | # From --xsl-file option. |
---|
430 | if self.xsl_file is not None: |
---|
431 | if not os.path.isfile(self.xsl_file): |
---|
432 | die('missing XSL file: %s' % self.xsl_file) |
---|
433 | self.xsl_file = os.path.abspath(self.xsl_file) |
---|
434 | # Load ordered files. |
---|
435 | for f in conf_files: |
---|
436 | if os.path.isfile(f): |
---|
437 | verbose('loading configuration file: %s' % f) |
---|
438 | execfile(f, globals()) |
---|
439 | |
---|
440 | def process_options(self): |
---|
441 | ''' |
---|
442 | Validate and command options and set defaults. |
---|
443 | ''' |
---|
444 | if not os.path.isfile(self.asciidoc_file): |
---|
445 | die('missing SOURCE_FILE: %s' % self.asciidoc_file) |
---|
446 | self.asciidoc_file = os.path.abspath(self.asciidoc_file) |
---|
447 | if not self.destination_dir: |
---|
448 | self.destination_dir = os.path.dirname(self.asciidoc_file) |
---|
449 | else: |
---|
450 | if not os.path.isdir(self.destination_dir): |
---|
451 | die('missing --destination-dir: %s' % self.destination_dir) |
---|
452 | self.destination_dir = os.path.abspath(self.destination_dir) |
---|
453 | self.resource_dirs = [] |
---|
454 | self.resource_files = [] |
---|
455 | if self.resource_manifest: |
---|
456 | if not os.path.isfile(self.resource_manifest): |
---|
457 | die('missing --resource-manifest: %s' % self.resource_manifest) |
---|
458 | f = open(self.resource_manifest) |
---|
459 | try: |
---|
460 | for r in f: |
---|
461 | self.resources.append(r.strip()) |
---|
462 | finally: |
---|
463 | f.close() |
---|
464 | for r in self.resources: |
---|
465 | r = os.path.expanduser(r) |
---|
466 | r = os.path.expandvars(r) |
---|
467 | if r.endswith('/') or r.endswith('\\'): |
---|
468 | if os.path.isdir(r): |
---|
469 | self.resource_dirs.append(r) |
---|
470 | else: |
---|
471 | die('missing resource directory: %s' % r) |
---|
472 | elif os.path.isdir(r): |
---|
473 | self.resource_dirs.append(r) |
---|
474 | elif r.startswith('.') and '=' in r: |
---|
475 | ext, mimetype = r.split('=') |
---|
476 | mimetypes.add_type(mimetype, ext) |
---|
477 | else: |
---|
478 | self.resource_files.append(r) |
---|
479 | for p in (os.path.dirname(self.asciidoc), CONF_DIR): |
---|
480 | for d in ('images','stylesheets'): |
---|
481 | d = os.path.join(p,d) |
---|
482 | if os.path.isdir(d): |
---|
483 | self.resource_dirs.append(d) |
---|
484 | verbose('resource files: %s' % self.resource_files) |
---|
485 | verbose('resource directories: %s' % self.resource_dirs) |
---|
486 | if not self.doctype and self.format == 'manpage': |
---|
487 | self.doctype = 'manpage' |
---|
488 | if self.doctype: |
---|
489 | self.asciidoc_opts += ' --doctype %s' % self.doctype |
---|
490 | for attr in self.attributes: |
---|
491 | self.asciidoc_opts += ' --attribute "%s"' % attr |
---|
492 | # self.xsltproc_opts += ' --nonet' |
---|
493 | if self.verbose: |
---|
494 | self.asciidoc_opts += ' --verbose' |
---|
495 | self.dblatex_opts += ' -V' |
---|
496 | if self.icons or self.icons_dir: |
---|
497 | params = [ |
---|
498 | 'callout.graphics 1', |
---|
499 | 'navig.graphics 1', |
---|
500 | 'admon.textlabel 0', |
---|
501 | 'admon.graphics 1', |
---|
502 | ] |
---|
503 | if self.icons_dir: |
---|
504 | params += [ |
---|
505 | 'admon.graphics.path "%s/"' % self.icons_dir, |
---|
506 | 'callout.graphics.path "%s/callouts/"' % self.icons_dir, |
---|
507 | 'navig.graphics.path "%s/"' % self.icons_dir, |
---|
508 | ] |
---|
509 | else: |
---|
510 | params = [ |
---|
511 | 'callout.graphics 0', |
---|
512 | 'navig.graphics 0', |
---|
513 | 'admon.textlabel 1', |
---|
514 | 'admon.graphics 0', |
---|
515 | ] |
---|
516 | if self.stylesheet: |
---|
517 | params += ['html.stylesheet "%s"' % self.stylesheet] |
---|
518 | if self.format == 'htmlhelp': |
---|
519 | params += ['htmlhelp.chm "%s"' % self.basename('.chm'), |
---|
520 | 'htmlhelp.hhp "%s"' % self.basename('.hhp'), |
---|
521 | 'htmlhelp.hhk "%s"' % self.basename('.hhk'), |
---|
522 | 'htmlhelp.hhc "%s"' % self.basename('.hhc')] |
---|
523 | if self.doctype == 'book': |
---|
524 | params += ['toc.section.depth 1'] |
---|
525 | # Books are chunked at chapter level. |
---|
526 | params += ['chunk.section.depth 0'] |
---|
527 | for o in params: |
---|
528 | if o.split()[0]+' ' not in self.xsltproc_opts: |
---|
529 | self.xsltproc_opts += ' --stringparam ' + o |
---|
530 | if self.fop_opts: |
---|
531 | self.fop = True |
---|
532 | if os.path.splitext(self.asciidoc_file)[1].lower() == '.xml': |
---|
533 | self.skip_asciidoc = True |
---|
534 | else: |
---|
535 | self.skip_asciidoc = False |
---|
536 | |
---|
537 | def dst_path(self, ext): |
---|
538 | ''' |
---|
539 | Return name of file or directory in the destination directory with |
---|
540 | the same name as the asciidoc source file but with extension ext. |
---|
541 | ''' |
---|
542 | return os.path.join(self.destination_dir, self.basename(ext)) |
---|
543 | |
---|
544 | def basename(self, ext): |
---|
545 | ''' |
---|
546 | Return the base name of the asciidoc source file but with extension |
---|
547 | ext. |
---|
548 | ''' |
---|
549 | return os.path.basename(os.path.splitext(self.asciidoc_file)[0]) + ext |
---|
550 | |
---|
551 | def asciidoc_conf_file(self, path): |
---|
552 | ''' |
---|
553 | Return full path name of file in asciidoc configuration files directory. |
---|
554 | Search first the directory containing the asciidoc executable then |
---|
555 | the global configuration file directory. |
---|
556 | ''' |
---|
557 | f = os.path.join(os.path.dirname(self.asciidoc), path) |
---|
558 | if not os.path.isfile(f): |
---|
559 | f = os.path.join(CONF_DIR, path) |
---|
560 | if not os.path.isfile(f): |
---|
561 | die('missing configuration file: %s' % f) |
---|
562 | return os.path.normpath(f) |
---|
563 | |
---|
564 | def xsl_stylesheet(self, file_name=None): |
---|
565 | ''' |
---|
566 | Return full path name of file in asciidoc docbook-xsl configuration |
---|
567 | directory. |
---|
568 | If an XSL file was specified with the --xsl-file option then it is |
---|
569 | returned. |
---|
570 | ''' |
---|
571 | if self.xsl_file is not None: |
---|
572 | return self.xsl_file |
---|
573 | if not file_name: |
---|
574 | file_name = self.format + '.xsl' |
---|
575 | return self.asciidoc_conf_file(os.path.join('docbook-xsl', file_name)) |
---|
576 | |
---|
577 | def copy_resources(self, html_files, src_dir, dst_dir, resources=[]): |
---|
578 | ''' |
---|
579 | Search html_files for images and CSS resource URIs (html_files can be a |
---|
580 | list of file names or a single file name). |
---|
581 | Copy them from the src_dir to the dst_dir. |
---|
582 | If not found in src_dir then recursively search all specified |
---|
583 | resource directories. |
---|
584 | Optional additional resources files can be passed in the resources list. |
---|
585 | ''' |
---|
586 | resources = resources[:] |
---|
587 | resources += find_resources(html_files, 'link', 'href', |
---|
588 | lambda attrs: attrs.get('type') == 'text/css') |
---|
589 | resources += find_resources(html_files, 'img', 'src') |
---|
590 | resources += self.resource_files |
---|
591 | resources = list(set(resources)) # Drop duplicates. |
---|
592 | resources.sort() |
---|
593 | for f in resources: |
---|
594 | if '=' in f: |
---|
595 | src, dst = f.split('=') |
---|
596 | if not dst: |
---|
597 | dst = src |
---|
598 | else: |
---|
599 | src = dst = f |
---|
600 | src = os.path.normpath(src) |
---|
601 | dst = os.path.normpath(dst) |
---|
602 | if os.path.isabs(dst): |
---|
603 | die('absolute resource file name: %s' % dst) |
---|
604 | if dst.startswith(os.pardir): |
---|
605 | die('resource file outside destination directory: %s' % dst) |
---|
606 | src = os.path.join(src_dir, src) |
---|
607 | dst = os.path.join(dst_dir, dst) |
---|
608 | if not os.path.isfile(src): |
---|
609 | for d in self.resource_dirs: |
---|
610 | d = os.path.join(src_dir, d) |
---|
611 | found = find_files(d, os.path.basename(src)) |
---|
612 | if found: |
---|
613 | src = found[0] |
---|
614 | break |
---|
615 | else: |
---|
616 | if not os.path.isfile(dst): |
---|
617 | die('missing resource: %s' % src) |
---|
618 | continue |
---|
619 | # Arrive here if resource file has been found. |
---|
620 | if os.path.normpath(src) != os.path.normpath(dst): |
---|
621 | dstdir = os.path.dirname(dst) |
---|
622 | shell_makedirs(dstdir) |
---|
623 | shell_copy(src, dst) |
---|
624 | |
---|
625 | def to_backend(self): |
---|
626 | ''' |
---|
627 | Convert AsciiDoc source file to a backend output file using the global |
---|
628 | 'to_<backend name>' function (loaded from backend plugin a2x-backend.py |
---|
629 | file). |
---|
630 | Executes the global function in an A2X class instance context. |
---|
631 | ''' |
---|
632 | eval('to_%s(self)' % self.backend) |
---|
633 | |
---|
634 | def to_docbook(self): |
---|
635 | ''' |
---|
636 | Use asciidoc to convert asciidoc_file to DocBook. |
---|
637 | args is a string containing additional asciidoc arguments. |
---|
638 | ''' |
---|
639 | docbook_file = self.dst_path('.xml') |
---|
640 | if self.skip_asciidoc: |
---|
641 | if not os.path.isfile(docbook_file): |
---|
642 | die('missing docbook file: %s' % docbook_file) |
---|
643 | return |
---|
644 | shell('"%s" --backend docbook -a "a2x-format=%s" %s --out-file "%s" "%s"' % |
---|
645 | (self.asciidoc, self.format, self.asciidoc_opts, docbook_file, self.asciidoc_file)) |
---|
646 | if not self.no_xmllint and XMLLINT: |
---|
647 | shell('"%s" --nonet --noout --valid "%s"' % (XMLLINT, docbook_file)) |
---|
648 | |
---|
649 | def to_xhtml(self): |
---|
650 | self.to_docbook() |
---|
651 | docbook_file = self.dst_path('.xml') |
---|
652 | xhtml_file = self.dst_path('.html') |
---|
653 | opts = '%s --output "%s"' % (self.xsltproc_opts, xhtml_file) |
---|
654 | exec_xsltproc(self.xsl_stylesheet(), docbook_file, self.destination_dir, opts) |
---|
655 | src_dir = os.path.dirname(self.asciidoc_file) |
---|
656 | self.copy_resources(xhtml_file, src_dir, self.destination_dir) |
---|
657 | |
---|
658 | def to_manpage(self): |
---|
659 | self.to_docbook() |
---|
660 | docbook_file = self.dst_path('.xml') |
---|
661 | opts = self.xsltproc_opts |
---|
662 | exec_xsltproc(self.xsl_stylesheet(), docbook_file, self.destination_dir, opts) |
---|
663 | |
---|
664 | def to_pdf(self): |
---|
665 | if self.fop: |
---|
666 | self.exec_fop() |
---|
667 | else: |
---|
668 | self.exec_dblatex() |
---|
669 | |
---|
670 | def exec_fop(self): |
---|
671 | self.to_docbook() |
---|
672 | docbook_file = self.dst_path('.xml') |
---|
673 | xsl = self.xsl_stylesheet('fo.xsl') |
---|
674 | fo = self.dst_path('.fo') |
---|
675 | pdf = self.dst_path('.pdf') |
---|
676 | opts = '%s --output "%s"' % (self.xsltproc_opts, fo) |
---|
677 | exec_xsltproc(xsl, docbook_file, self.destination_dir, opts) |
---|
678 | shell('"%s" %s -fo "%s" -pdf "%s"' % (FOP, self.fop_opts, fo, pdf)) |
---|
679 | if not self.keep_artifacts: |
---|
680 | shell_rm(fo) |
---|
681 | |
---|
682 | def exec_dblatex(self): |
---|
683 | self.to_docbook() |
---|
684 | docbook_file = self.dst_path('.xml') |
---|
685 | xsl = self.asciidoc_conf_file(os.path.join('dblatex','asciidoc-dblatex.xsl')) |
---|
686 | sty = self.asciidoc_conf_file(os.path.join('dblatex','asciidoc-dblatex.sty')) |
---|
687 | shell('"%s" -t %s -p "%s" -s "%s" %s "%s"' % |
---|
688 | (DBLATEX, self.format, xsl, sty, self.dblatex_opts, docbook_file)) |
---|
689 | |
---|
690 | def to_dvi(self): |
---|
691 | self.exec_dblatex() |
---|
692 | |
---|
693 | def to_ps(self): |
---|
694 | self.exec_dblatex() |
---|
695 | |
---|
696 | def to_tex(self): |
---|
697 | self.exec_dblatex() |
---|
698 | |
---|
699 | def to_htmlhelp(self): |
---|
700 | self.to_chunked() |
---|
701 | |
---|
702 | def to_chunked(self): |
---|
703 | self.to_docbook() |
---|
704 | docbook_file = self.dst_path('.xml') |
---|
705 | opts = self.xsltproc_opts |
---|
706 | xsl_file = self.xsl_stylesheet() |
---|
707 | if self.format == 'chunked': |
---|
708 | dst_dir = self.dst_path('.chunked') |
---|
709 | elif self.format == 'htmlhelp': |
---|
710 | dst_dir = self.dst_path('.htmlhelp') |
---|
711 | if not 'base.dir ' in opts: |
---|
712 | opts += ' --stringparam base.dir "%s/"' % os.path.basename(dst_dir) |
---|
713 | # Create content. |
---|
714 | shell_rmtree(dst_dir) |
---|
715 | shell_makedirs(dst_dir) |
---|
716 | exec_xsltproc(xsl_file, docbook_file, self.destination_dir, opts) |
---|
717 | html_files = find_files(dst_dir, '*.html') |
---|
718 | src_dir = os.path.dirname(self.asciidoc_file) |
---|
719 | self.copy_resources(html_files, src_dir, dst_dir) |
---|
720 | |
---|
721 | def update_epub_manifest(self, opf_file): |
---|
722 | ''' |
---|
723 | Scan the OEBPS directory for any files that have not been registered in |
---|
724 | the OPF manifest then add them to the manifest. |
---|
725 | ''' |
---|
726 | opf_dir = os.path.dirname(opf_file) |
---|
727 | resource_files = [] |
---|
728 | for (p,dirs,files) in os.walk(os.path.dirname(opf_file)): |
---|
729 | for f in files: |
---|
730 | f = os.path.join(p,f) |
---|
731 | if os.path.isfile(f): |
---|
732 | assert f.startswith(opf_dir) |
---|
733 | f = '.' + f[len(opf_dir):] |
---|
734 | f = os.path.normpath(f) |
---|
735 | if f not in ['content.opf']: |
---|
736 | resource_files.append(f) |
---|
737 | opf = xml.dom.minidom.parseString(read_file(opf_file)) |
---|
738 | manifest_files = [] |
---|
739 | manifest = opf.getElementsByTagName('manifest')[0] |
---|
740 | for el in manifest.getElementsByTagName('item'): |
---|
741 | f = el.getAttribute('href') |
---|
742 | f = os.path.normpath(f) |
---|
743 | manifest_files.append(f) |
---|
744 | count = 0 |
---|
745 | for f in resource_files: |
---|
746 | if f not in manifest_files: |
---|
747 | count += 1 |
---|
748 | verbose('adding to manifest: %s' % f) |
---|
749 | item = opf.createElement('item') |
---|
750 | item.setAttribute('href', f.replace(os.path.sep, '/')) |
---|
751 | item.setAttribute('id', 'a2x-%d' % count) |
---|
752 | mimetype = mimetypes.guess_type(f)[0] |
---|
753 | if mimetype is None: |
---|
754 | die('unknown mimetype: %s' % f) |
---|
755 | item.setAttribute('media-type', mimetype) |
---|
756 | manifest.appendChild(item) |
---|
757 | if count > 0: |
---|
758 | write_file(opf_file, opf.toxml()) |
---|
759 | |
---|
760 | def to_epub(self): |
---|
761 | self.to_docbook() |
---|
762 | xsl_file = self.xsl_stylesheet() |
---|
763 | docbook_file = self.dst_path('.xml') |
---|
764 | epub_file = self.dst_path('.epub') |
---|
765 | build_dir = epub_file + '.d' |
---|
766 | shell_rmtree(build_dir) |
---|
767 | shell_makedirs(build_dir) |
---|
768 | # Create content. |
---|
769 | exec_xsltproc(xsl_file, docbook_file, build_dir, self.xsltproc_opts) |
---|
770 | # Copy resources referenced in the OPF and resources referenced by the |
---|
771 | # generated HTML (in theory DocBook XSL should ensure they are |
---|
772 | # identical but this is not always the case). |
---|
773 | src_dir = os.path.dirname(self.asciidoc_file) |
---|
774 | dst_dir = os.path.join(build_dir, 'OEBPS') |
---|
775 | opf_file = os.path.join(dst_dir, 'content.opf') |
---|
776 | opf_resources = find_resources(opf_file, 'item', 'href') |
---|
777 | html_files = find_files(dst_dir, '*.html') |
---|
778 | self.copy_resources(html_files, src_dir, dst_dir, opf_resources) |
---|
779 | # Register any unregistered resources. |
---|
780 | self.update_epub_manifest(opf_file) |
---|
781 | # Build epub archive. |
---|
782 | cwd = os.getcwd() |
---|
783 | shell_cd(build_dir) |
---|
784 | try: |
---|
785 | if not self.dry_run: |
---|
786 | zip = zipfile.ZipFile(epub_file, 'w') |
---|
787 | try: |
---|
788 | # Create and add uncompressed mimetype file. |
---|
789 | verbose('archiving: mimetype') |
---|
790 | write_file('mimetype', 'application/epub+zip') |
---|
791 | zip.write('mimetype', compress_type=zipfile.ZIP_STORED) |
---|
792 | # Compress all remaining files. |
---|
793 | for (p,dirs,files) in os.walk('.'): |
---|
794 | for f in files: |
---|
795 | f = os.path.normpath(os.path.join(p,f)) |
---|
796 | if f != 'mimetype': |
---|
797 | verbose('archiving: %s' % f) |
---|
798 | zip.write(f, compress_type=zipfile.ZIP_DEFLATED) |
---|
799 | finally: |
---|
800 | zip.close() |
---|
801 | verbose('created archive: %s' % epub_file) |
---|
802 | finally: |
---|
803 | shell_cd(cwd) |
---|
804 | if not self.keep_artifacts: |
---|
805 | shell_rmtree(build_dir) |
---|
806 | if self.epubcheck and EPUBCHECK: |
---|
807 | if not find_executable(EPUBCHECK): |
---|
808 | warning('epubcheck skipped: unable to find executable: %s' % EPUBCHECK) |
---|
809 | else: |
---|
810 | shell('"%s" "%s"' % (EPUBCHECK, epub_file)) |
---|
811 | |
---|
812 | def to_text(self): |
---|
813 | text_file = self.dst_path('.text') |
---|
814 | html_file = self.dst_path('.text.html') |
---|
815 | if self.lynx: |
---|
816 | shell('"%s" %s --conf-file "%s" -b html4 -a "a2x-format=%s" -o "%s" "%s"' % |
---|
817 | (self.asciidoc, self.asciidoc_opts, self.asciidoc_conf_file('text.conf'), |
---|
818 | self.format, html_file, self.asciidoc_file)) |
---|
819 | shell('"%s" -dump "%s" > "%s"' % |
---|
820 | (LYNX, html_file, text_file)) |
---|
821 | else: |
---|
822 | # Use w3m(1). |
---|
823 | self.to_docbook() |
---|
824 | docbook_file = self.dst_path('.xml') |
---|
825 | opts = '%s --output "%s"' % (self.xsltproc_opts, html_file) |
---|
826 | exec_xsltproc(self.xsl_stylesheet(), docbook_file, |
---|
827 | self.destination_dir, opts) |
---|
828 | shell('"%s" -cols 70 -dump -T text/html -no-graph "%s" > "%s"' % |
---|
829 | (W3M, html_file, text_file)) |
---|
830 | if not self.keep_artifacts: |
---|
831 | shell_rm(html_file) |
---|
832 | |
---|
833 | |
---|
834 | ##################################################################### |
---|
835 | # Script main line. |
---|
836 | ##################################################################### |
---|
837 | |
---|
838 | if __name__ == '__main__': |
---|
839 | description = '''A toolchain manager for AsciiDoc (converts Asciidoc text files to other file formats)''' |
---|
840 | from optparse import OptionParser |
---|
841 | parser = OptionParser(usage='usage: %prog [OPTIONS] SOURCE_FILE', |
---|
842 | version='%s %s' % (PROG,VERSION), |
---|
843 | description=description) |
---|
844 | parser.add_option('-a', '--attribute', |
---|
845 | action='append', dest='attributes', default=[], metavar='ATTRIBUTE', |
---|
846 | help='set asciidoc attribute value') |
---|
847 | parser.add_option('--asciidoc-opts', |
---|
848 | action='append', dest='asciidoc_opts', default=[], |
---|
849 | metavar='ASCIIDOC_OPTS', help='asciidoc options') |
---|
850 | #DEPRECATED |
---|
851 | parser.add_option('--copy', |
---|
852 | action='store_true', dest='copy', default=False, |
---|
853 | help='DEPRECATED: does nothing') |
---|
854 | parser.add_option('--conf-file', |
---|
855 | dest='conf_file', default=None, metavar='CONF_FILE', |
---|
856 | help='configuration file') |
---|
857 | parser.add_option('-D', '--destination-dir', |
---|
858 | action='store', dest='destination_dir', default=None, metavar='PATH', |
---|
859 | help='output directory (defaults to SOURCE_FILE directory)') |
---|
860 | parser.add_option('-d','--doctype', |
---|
861 | action='store', dest='doctype', metavar='DOCTYPE', |
---|
862 | choices=('article','manpage','book'), |
---|
863 | help='article, manpage, book') |
---|
864 | parser.add_option('-b','--backend', |
---|
865 | action='store', dest='backend', metavar='BACKEND', |
---|
866 | help='name of backend plugin') |
---|
867 | parser.add_option('--epubcheck', |
---|
868 | action='store_true', dest='epubcheck', default=False, |
---|
869 | help='check EPUB output with epubcheck') |
---|
870 | parser.add_option('-f','--format', |
---|
871 | action='store', dest='format', metavar='FORMAT', default = 'pdf', |
---|
872 | choices=('chunked','epub','htmlhelp','manpage','pdf', 'text', |
---|
873 | 'xhtml','dvi','ps','tex','docbook'), |
---|
874 | help='chunked, epub, htmlhelp, manpage, pdf, text, xhtml, dvi, ps, tex, docbook') |
---|
875 | parser.add_option('--icons', |
---|
876 | action='store_true', dest='icons', default=False, |
---|
877 | help='use admonition, callout and navigation icons') |
---|
878 | parser.add_option('--icons-dir', |
---|
879 | action='store', dest='icons_dir', |
---|
880 | default=None, metavar='PATH', |
---|
881 | help='admonition and navigation icon directory') |
---|
882 | parser.add_option('-k', '--keep-artifacts', |
---|
883 | action='store_true', dest='keep_artifacts', default=False, |
---|
884 | help='do not delete temporary build files') |
---|
885 | parser.add_option('--lynx', |
---|
886 | action='store_true', dest='lynx', default=False, |
---|
887 | help='use lynx to generate text files') |
---|
888 | parser.add_option('-L', '--no-xmllint', |
---|
889 | action='store_true', dest='no_xmllint', default=False, |
---|
890 | help='do not check asciidoc output with xmllint') |
---|
891 | parser.add_option('-n','--dry-run', |
---|
892 | action='store_true', dest='dry_run', default=False, |
---|
893 | help='just print the commands that would have been executed') |
---|
894 | parser.add_option('-r','--resource', |
---|
895 | action='append', dest='resources', default=[], |
---|
896 | metavar='PATH', |
---|
897 | help='resource file or directory containing resource files') |
---|
898 | parser.add_option('-m', '--resource-manifest', |
---|
899 | action='store', dest='resource_manifest', default=None, metavar='FILE', |
---|
900 | help='read resources from FILE') |
---|
901 | #DEPRECATED |
---|
902 | parser.add_option('--resource-dir', |
---|
903 | action='append', dest='resources', default=[], |
---|
904 | metavar='PATH', |
---|
905 | help='DEPRECATED: use --resource') |
---|
906 | #DEPRECATED |
---|
907 | parser.add_option('-s','--skip-asciidoc', |
---|
908 | action='store_true', dest='skip_asciidoc', default=False, |
---|
909 | help='DEPRECATED: redundant') |
---|
910 | parser.add_option('--stylesheet', |
---|
911 | action='store', dest='stylesheet', default=None, |
---|
912 | metavar='STYLESHEET', |
---|
913 | help='HTML CSS stylesheet file name') |
---|
914 | #DEPRECATED |
---|
915 | parser.add_option('--safe', |
---|
916 | action='store_true', dest='safe', default=False, |
---|
917 | help='DEPRECATED: does nothing') |
---|
918 | parser.add_option('--dblatex-opts', |
---|
919 | action='append', dest='dblatex_opts', default=[], |
---|
920 | metavar='DBLATEX_OPTS', help='dblatex options') |
---|
921 | parser.add_option('--backend-opts', |
---|
922 | action='append', dest='backend_opts', default=[], |
---|
923 | metavar='BACKEND_OPTS', help='backend plugin options') |
---|
924 | parser.add_option('--fop', |
---|
925 | action='store_true', dest='fop', default=False, |
---|
926 | help='use FOP to generate PDF files') |
---|
927 | parser.add_option('--fop-opts', |
---|
928 | action='append', dest='fop_opts', default=[], |
---|
929 | metavar='FOP_OPTS', help='options for FOP pdf generation') |
---|
930 | parser.add_option('--xsltproc-opts', |
---|
931 | action='append', dest='xsltproc_opts', default=[], |
---|
932 | metavar='XSLTPROC_OPTS', help='xsltproc options for XSL stylesheets') |
---|
933 | parser.add_option('--xsl-file', |
---|
934 | action='store', dest='xsl_file', metavar='XSL_FILE', |
---|
935 | help='custom XSL stylesheet') |
---|
936 | parser.add_option('-v', '--verbose', |
---|
937 | action='count', dest='verbose', default=0, |
---|
938 | help='increase verbosity') |
---|
939 | if len(sys.argv) == 1: |
---|
940 | parser.parse_args(['--help']) |
---|
941 | source_options = get_source_options(sys.argv[-1]) |
---|
942 | argv = source_options + sys.argv[1:] |
---|
943 | opts, args = parser.parse_args(argv) |
---|
944 | if len(args) != 1: |
---|
945 | parser.error('incorrect number of arguments') |
---|
946 | opts.asciidoc_opts = ' '.join(opts.asciidoc_opts) |
---|
947 | opts.dblatex_opts = ' '.join(opts.dblatex_opts) |
---|
948 | opts.fop_opts = ' '.join(opts.fop_opts) |
---|
949 | opts.xsltproc_opts = ' '.join(opts.xsltproc_opts) |
---|
950 | opts.backend_opts = ' '.join(opts.backend_opts) |
---|
951 | opts = eval(str(opts)) # Convert optparse.Values to dict. |
---|
952 | a2x = A2X(opts) |
---|
953 | OPTIONS = a2x # verbose and dry_run used by utility functions. |
---|
954 | verbose('args: %r' % argv) |
---|
955 | a2x.asciidoc_file = args[0] |
---|
956 | try: |
---|
957 | a2x.load_conf() |
---|
958 | a2x.execute() |
---|
959 | except KeyboardInterrupt: |
---|
960 | exit(1) |
---|