source: rtems-source-builder/source-builder/sb/asciidoc/a2x.py @ 0464153

4.104.114.95
Last change on this file since 0464153 was 0464153, checked in by Chris Johns <chrisj@…>, on 03/03/13 at 04:58:11

Change asciidoc to the 8.6.4 release package because Windows was broken.

  • Property mode set to 100755
File size: 36.0 KB
Line 
1#!/usr/bin/env python
2'''
3a2x - A toolchain manager for AsciiDoc (converts Asciidoc text files to other
4      file formats)
5
6Copyright: Stuart Rackham (c) 2009
7License:   MIT
8Email:     srackham@gmail.com
9
10'''
11
12import os
13import fnmatch
14import HTMLParser
15import re
16import shutil
17import subprocess
18import sys
19import traceback
20import urlparse
21import zipfile
22import xml.dom.minidom
23import mimetypes
24
25PROG = os.path.basename(os.path.splitext(__file__)[0])
26VERSION = '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.
30CONF_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.
40ENV = None
41
42# External executables.
43ASCIIDOC = 'asciidoc'
44XSLTPROC = 'xsltproc'
45DBLATEX = 'dblatex'         # pdf generation.
46FOP = 'fop'                 # pdf generation (--fop option).
47W3M = 'w3m'                 # text generation.
48LYNX = 'lynx'               # text generation (if no w3m).
49XMLLINT = 'xmllint'         # Set to '' to disable.
50EPUBCHECK = 'epubcheck'     # Set to '' to disable.
51# External executable default options.
52ASCIIDOC_OPTS = ''
53DBLATEX_OPTS = ''
54FOP_OPTS = ''
55XSLTPROC_OPTS = ''
56BACKEND_OPTS = ''
57
58######################################################################
59# End of configuration file parameters.
60######################################################################
61
62
63#####################################################################
64# Utility functions
65#####################################################################
66
67OPTIONS = None  # These functions read verbose and dry_run command options.
68
69def errmsg(msg):
70    sys.stderr.write('%s: %s\n' % (PROG,msg))
71
72def warning(msg):
73    errmsg('WARNING: %s' % msg)
74
75def infomsg(msg):
76    print '%s: %s' % (PROG,msg)
77
78def die(msg, exit_code=1):
79    errmsg('ERROR: %s' % msg)
80    sys.exit(exit_code)
81
82def trace():
83    """Print traceback to stderr."""
84    errmsg('-'*60)
85    traceback.print_exc(file=sys.stderr)
86    errmsg('-'*60)
87
88def verbose(msg):
89    if OPTIONS.verbose or OPTIONS.dry_run:
90        infomsg(msg)
91
92class 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
119def isexecutable(file_name):
120    return os.path.isfile(file_name) and os.access(file_name, os.X_OK)
121
122def 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
147def write_file(filename, data, mode='w'):
148    f = open(filename, mode)
149    try:
150        f.write(data)
151    finally:
152        f.close()
153
154def read_file(filename, mode='r'):
155    f = open(filename, mode)
156    try:
157        return f.read()
158    finally:
159        f.close()
160
161def shell_cd(path):
162    verbose('chdir %s' % path)
163    if not OPTIONS.dry_run:
164        os.chdir(path)
165
166def 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
173def shell_copy(src, dst):
174    verbose('copying "%s" to "%s"' % (src,dst))
175    if not OPTIONS.dry_run:
176        shutil.copy(src, dst)
177
178def 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
185def 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
192def 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
230def 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.
274def 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
292def 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
303def 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
311def 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
358class 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
838if __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)
Note: See TracBrowser for help on using the repository browser.