diff options
Diffstat (limited to 'doc/asciidoc/a2x.py')
-rwxr-xr-x | doc/asciidoc/a2x.py | 960 |
1 files changed, 960 insertions, 0 deletions
diff --git a/doc/asciidoc/a2x.py b/doc/asciidoc/a2x.py new file mode 100755 index 0000000..1b192a2 --- /dev/null +++ b/doc/asciidoc/a2x.py @@ -0,0 +1,960 @@ +#!/usr/bin/env python +''' +a2x - A toolchain manager for AsciiDoc (converts Asciidoc text files to other + file formats) + +Copyright: Stuart Rackham (c) 2009 +License: MIT +Email: srackham@gmail.com + +''' + +import os +import fnmatch +import HTMLParser +import re +import shutil +import subprocess +import sys +import traceback +import urlparse +import zipfile +import xml.dom.minidom +import mimetypes + +PROG = os.path.basename(os.path.splitext(__file__)[0]) +VERSION = '8.6.8' + +# AsciiDoc global configuration file directory. +# NOTE: CONF_DIR is "fixed up" by Makefile -- don't rename or change syntax. +CONF_DIR = '/etc/asciidoc' + + +###################################################################### +# Default configuration file parameters. +###################################################################### + +# Optional environment variable dictionary passed to +# executing programs. If set to None the existing +# environment is used. +ENV = None + +# External executables. +ASCIIDOC = 'asciidoc' +XSLTPROC = 'xsltproc' +DBLATEX = 'dblatex' # pdf generation. +FOP = 'fop' # pdf generation (--fop option). +W3M = 'w3m' # text generation. +LYNX = 'lynx' # text generation (if no w3m). +XMLLINT = 'xmllint' # Set to '' to disable. +EPUBCHECK = 'epubcheck' # Set to '' to disable. +# External executable default options. +ASCIIDOC_OPTS = '' +DBLATEX_OPTS = '' +FOP_OPTS = '' +XSLTPROC_OPTS = '' +BACKEND_OPTS = '' + +###################################################################### +# End of configuration file parameters. +###################################################################### + + +##################################################################### +# Utility functions +##################################################################### + +OPTIONS = None # These functions read verbose and dry_run command options. + +def errmsg(msg): + sys.stderr.write('%s: %s\n' % (PROG,msg)) + +def warning(msg): + errmsg('WARNING: %s' % msg) + +def infomsg(msg): + print '%s: %s' % (PROG,msg) + +def die(msg, exit_code=1): + errmsg('ERROR: %s' % msg) + sys.exit(exit_code) + +def trace(): + """Print traceback to stderr.""" + errmsg('-'*60) + traceback.print_exc(file=sys.stderr) + errmsg('-'*60) + +def verbose(msg): + if OPTIONS.verbose or OPTIONS.dry_run: + infomsg(msg) + +class AttrDict(dict): + """ + Like a dictionary except values can be accessed as attributes i.e. obj.foo + can be used in addition to obj['foo']. + If self._default has been set then it will be returned if a non-existant + attribute is accessed (instead of raising an AttributeError). + """ + def __getattr__(self, key): + try: + return self[key] + except KeyError, k: + if self.has_key('_default'): + return self['_default'] + else: + raise AttributeError, k + def __setattr__(self, key, value): + self[key] = value + def __delattr__(self, key): + try: del self[key] + except KeyError, k: raise AttributeError, k + def __repr__(self): + return '<AttrDict ' + dict.__repr__(self) + '>' + def __getstate__(self): + return dict(self) + def __setstate__(self,value): + for k,v in value.items(): self[k]=v + +def isexecutable(file_name): + return os.path.isfile(file_name) and os.access(file_name, os.X_OK) + +def find_executable(file_name): + ''' + Search for executable file_name in the system PATH. + Return full path name or None if not found. + ''' + def _find_executable(file_name): + if os.path.split(file_name)[0] != '': + # file_name includes directory so don't search path. + if not isexecutable(file_name): + return None + else: + return file_name + for p in os.environ.get('PATH', os.defpath).split(os.pathsep): + f = os.path.join(p, file_name) + if isexecutable(f): + return os.path.realpath(f) + return None + if os.name == 'nt' and os.path.splitext(file_name)[1] == '': + for ext in ('.cmd','.bat','.exe'): + result = _find_executable(file_name + ext) + if result: break + else: + result = _find_executable(file_name) + return result + +def write_file(filename, data, mode='w'): + f = open(filename, mode) + try: + f.write(data) + finally: + f.close() + +def read_file(filename, mode='r'): + f = open(filename, mode) + try: + return f.read() + finally: + f.close() + +def shell_cd(path): + verbose('chdir %s' % path) + if not OPTIONS.dry_run: + os.chdir(path) + +def shell_makedirs(path): + if os.path.isdir(path): + return + verbose('creating %s' % path) + if not OPTIONS.dry_run: + os.makedirs(path) + +def shell_copy(src, dst): + verbose('copying "%s" to "%s"' % (src,dst)) + if not OPTIONS.dry_run: + shutil.copy(src, dst) + +def shell_rm(path): + if not os.path.exists(path): + return + verbose('deleting %s' % path) + if not OPTIONS.dry_run: + os.unlink(path) + +def shell_rmtree(path): + if not os.path.isdir(path): + return + verbose('deleting %s' % path) + if not OPTIONS.dry_run: + shutil.rmtree(path) + +def shell(cmd, raise_error=True): + ''' + Execute command cmd in shell and return tuple + (stdoutdata, stderrdata, returncode). + If raise_error is True then a non-zero return terminates the application. + ''' + if os.name == 'nt': + # TODO: this is probably unnecessary, see: + # http://groups.google.com/group/asciidoc/browse_frm/thread/9442ee0c419f1242 + # Windows doesn't like running scripts directly so explicitly + # specify python interpreter. + # Extract first (quoted or unquoted) argument. + mo = re.match(r'^\s*"\s*(?P<arg0>[^"]+)\s*"', cmd) + if not mo: + mo = re.match(r'^\s*(?P<arg0>[^ ]+)', cmd) + if mo.group('arg0').endswith('.py'): + cmd = 'python ' + cmd + # Remove redundant quoting -- this is not just cosmetic, + # quoting seems to dramatically decrease the allowed command + # length in Windows XP. + cmd = re.sub(r'"([^ ]+?)"', r'\1', cmd) + verbose('executing: %s' % cmd) + if OPTIONS.dry_run: + return + stdout = stderr = subprocess.PIPE + try: + popen = subprocess.Popen(cmd, stdout=stdout, stderr=stderr, + shell=True, env=ENV) + except OSError, e: + die('failed: %s: %s' % (cmd, e)) + stdoutdata, stderrdata = popen.communicate() + if OPTIONS.verbose: + print stdoutdata + print stderrdata + if popen.returncode != 0 and raise_error: + die('%s returned non-zero exit status %d' % (cmd, popen.returncode)) + return (stdoutdata, stderrdata, popen.returncode) + +def find_resources(files, tagname, attrname, filter=None): + ''' + Search all files and return a list of local URIs from attrname attribute + values in tagname tags. + Handles HTML open and XHTML closed tags. + Non-local URIs are skipped. + files can be a file name or a list of file names. + The filter function takes a dictionary of tag attributes and returns True if + the URI is to be included. + ''' + class FindResources(HTMLParser.HTMLParser): + # Nested parser class shares locals with enclosing function. + def handle_startendtag(self, tag, attrs): + self.handle_starttag(tag, attrs) + def handle_starttag(self, tag, attrs): + attrs = dict(attrs) + if tag == tagname and (filter is None or filter(attrs)): + # Accept only local URIs. + uri = urlparse.urlparse(attrs[attrname]) + if uri[0] in ('','file') and not uri[1] and uri[2]: + result.append(uri[2]) + if isinstance(files, str): + files = [files] + result = [] + for filename in files: + verbose('finding resources in: %s' % filename) + if OPTIONS.dry_run: + continue + parser = FindResources() + # HTMLParser has problems with non-ASCII strings. + # See http://bugs.python.org/issue3932 + contents = read_file(filename) + mo = re.search(r'\A<\?xml.* encoding="(.*?)"', contents) + if mo: + encoding = mo.group(1) + parser.feed(contents.decode(encoding)) + else: + parser.feed(contents) + parser.close() + result = list(set(result)) # Drop duplicate values. + result.sort() + return result + +# NOT USED. +def copy_files(files, src_dir, dst_dir): + ''' + Copy list of relative file names from src_dir to dst_dir. + ''' + for filename in files: + filename = os.path.normpath(filename) + if os.path.isabs(filename): + continue + src = os.path.join(src_dir, filename) + dst = os.path.join(dst_dir, filename) + if not os.path.exists(dst): + if not os.path.isfile(src): + warning('missing file: %s' % src) + continue + dstdir = os.path.dirname(dst) + shell_makedirs(dstdir) + shell_copy(src, dst) + +def find_files(path, pattern): + ''' + Return list of file names matching pattern in directory path. + ''' + result = [] + for (p,dirs,files) in os.walk(path): + for f in files: + if fnmatch.fnmatch(f, pattern): + result.append(os.path.normpath(os.path.join(p,f))) + return result + +def exec_xsltproc(xsl_file, xml_file, dst_dir, opts = ''): + cwd = os.getcwd() + shell_cd(dst_dir) + try: + shell('"%s" %s "%s" "%s"' % (XSLTPROC, opts, xsl_file, xml_file)) + finally: + shell_cd(cwd) + +def get_source_options(asciidoc_file): + ''' + Look for a2x command options in AsciiDoc source file. + Limitation: options cannot contain double-quote characters. + ''' + def parse_options(): + # Parse options to result sequence. + inquotes = False + opt = '' + for c in options: + if c == '"': + if inquotes: + result.append(opt) + opt = '' + inquotes = False + else: + inquotes = True + elif c == ' ': + if inquotes: + opt += c + elif opt: + result.append(opt) + opt = '' + else: + opt += c + if opt: + result.append(opt) + + result = [] + if os.path.isfile(asciidoc_file): + options = '' + f = open(asciidoc_file) + try: + for line in f: + mo = re.search(r'^//\s*a2x:', line) + if mo: + options += ' ' + line[mo.end():].strip() + finally: + f.close() + parse_options() + return result + + +##################################################################### +# Application class +##################################################################### + +class A2X(AttrDict): + ''' + a2x options and conversion functions. + ''' + + def execute(self): + ''' + Process a2x command. + ''' + self.process_options() + # Append configuration file options. + self.asciidoc_opts += ' ' + ASCIIDOC_OPTS + self.dblatex_opts += ' ' + DBLATEX_OPTS + self.fop_opts += ' ' + FOP_OPTS + self.xsltproc_opts += ' ' + XSLTPROC_OPTS + self.backend_opts += ' ' + BACKEND_OPTS + # Execute to_* functions. + if self.backend: + self.to_backend() + else: + self.__getattribute__('to_'+self.format)() + if not (self.keep_artifacts or self.format == 'docbook' or self.skip_asciidoc): + shell_rm(self.dst_path('.xml')) + + def load_conf(self): + ''' + Load a2x configuration file from default locations and --conf-file + option. + ''' + global ASCIIDOC + CONF_FILE = 'a2x.conf' + a2xdir = os.path.dirname(os.path.realpath(__file__)) + conf_files = [] + # From a2x.py directory. + conf_files.append(os.path.join(a2xdir, CONF_FILE)) + # If the asciidoc executable and conf files are in the a2x directory + # then use the local copy of asciidoc and skip the global a2x conf. + asciidoc = os.path.join(a2xdir, 'asciidoc.py') + asciidoc_conf = os.path.join(a2xdir, 'asciidoc.conf') + if os.path.isfile(asciidoc) and os.path.isfile(asciidoc_conf): + self.asciidoc = asciidoc + else: + self.asciidoc = None + # From global conf directory. + conf_files.append(os.path.join(CONF_DIR, CONF_FILE)) + # From $HOME directory. + home_dir = os.environ.get('HOME') + if home_dir is not None: + conf_files.append(os.path.join(home_dir, '.asciidoc', CONF_FILE)) + # If asciidoc is not local to a2x then search the PATH. + if not self.asciidoc: + self.asciidoc = find_executable(ASCIIDOC) + if not self.asciidoc: + die('unable to find asciidoc: %s' % ASCIIDOC) + # From backend plugin directory. + if self.backend is not None: + stdout = shell(self.asciidoc + ' --backend list')[0] + backends = [(i, os.path.split(i)[1]) for i in stdout.splitlines()] + backend_dir = [i[0] for i in backends if i[1] == self.backend] + if len(backend_dir) == 0: + die('missing %s backend' % self.backend) + if len(backend_dir) > 1: + die('more than one %s backend' % self.backend) + verbose('found %s backend directory: %s' % + (self.backend, backend_dir[0])) + conf_files.append(os.path.join(backend_dir[0], 'a2x-backend.py')) + # From --conf-file option. + if self.conf_file is not None: + if not os.path.isfile(self.conf_file): + die('missing configuration file: %s' % self.conf_file) + conf_files.append(self.conf_file) + # From --xsl-file option. + if self.xsl_file is not None: + if not os.path.isfile(self.xsl_file): + die('missing XSL file: %s' % self.xsl_file) + self.xsl_file = os.path.abspath(self.xsl_file) + # Load ordered files. + for f in conf_files: + if os.path.isfile(f): + verbose('loading configuration file: %s' % f) + execfile(f, globals()) + + def process_options(self): + ''' + Validate and command options and set defaults. + ''' + if not os.path.isfile(self.asciidoc_file): + die('missing SOURCE_FILE: %s' % self.asciidoc_file) + self.asciidoc_file = os.path.abspath(self.asciidoc_file) + if not self.destination_dir: + self.destination_dir = os.path.dirname(self.asciidoc_file) + else: + if not os.path.isdir(self.destination_dir): + die('missing --destination-dir: %s' % self.destination_dir) + self.destination_dir = os.path.abspath(self.destination_dir) + self.resource_dirs = [] + self.resource_files = [] + if self.resource_manifest: + if not os.path.isfile(self.resource_manifest): + die('missing --resource-manifest: %s' % self.resource_manifest) + f = open(self.resource_manifest) + try: + for r in f: + self.resources.append(r.strip()) + finally: + f.close() + for r in self.resources: + r = os.path.expanduser(r) + r = os.path.expandvars(r) + if r.endswith('/') or r.endswith('\\'): + if os.path.isdir(r): + self.resource_dirs.append(r) + else: + die('missing resource directory: %s' % r) + elif os.path.isdir(r): + self.resource_dirs.append(r) + elif r.startswith('.') and '=' in r: + ext, mimetype = r.split('=') + mimetypes.add_type(mimetype, ext) + else: + self.resource_files.append(r) + for p in (os.path.dirname(self.asciidoc), CONF_DIR): + for d in ('images','stylesheets'): + d = os.path.join(p,d) + if os.path.isdir(d): + self.resource_dirs.append(d) + verbose('resource files: %s' % self.resource_files) + verbose('resource directories: %s' % self.resource_dirs) + if not self.doctype and self.format == 'manpage': + self.doctype = 'manpage' + if self.doctype: + self.asciidoc_opts += ' --doctype %s' % self.doctype + for attr in self.attributes: + self.asciidoc_opts += ' --attribute "%s"' % attr +# self.xsltproc_opts += ' --nonet' + if self.verbose: + self.asciidoc_opts += ' --verbose' + self.dblatex_opts += ' -V' + if self.icons or self.icons_dir: + params = [ + 'callout.graphics 1', + 'navig.graphics 1', + 'admon.textlabel 0', + 'admon.graphics 1', + ] + if self.icons_dir: + params += [ + 'admon.graphics.path "%s/"' % self.icons_dir, + 'callout.graphics.path "%s/callouts/"' % self.icons_dir, + 'navig.graphics.path "%s/"' % self.icons_dir, + ] + else: + params = [ + 'callout.graphics 0', + 'navig.graphics 0', + 'admon.textlabel 1', + 'admon.graphics 0', + ] + if self.stylesheet: + params += ['html.stylesheet "%s"' % self.stylesheet] + if self.format == 'htmlhelp': + params += ['htmlhelp.chm "%s"' % self.basename('.chm'), + 'htmlhelp.hhp "%s"' % self.basename('.hhp'), + 'htmlhelp.hhk "%s"' % self.basename('.hhk'), + 'htmlhelp.hhc "%s"' % self.basename('.hhc')] + if self.doctype == 'book': + params += ['toc.section.depth 1'] + # Books are chunked at chapter level. + params += ['chunk.section.depth 0'] + for o in params: + if o.split()[0]+' ' not in self.xsltproc_opts: + self.xsltproc_opts += ' --stringparam ' + o + if self.fop_opts: + self.fop = True + if os.path.splitext(self.asciidoc_file)[1].lower() == '.xml': + self.skip_asciidoc = True + else: + self.skip_asciidoc = False + + def dst_path(self, ext): + ''' + Return name of file or directory in the destination directory with + the same name as the asciidoc source file but with extension ext. + ''' + return os.path.join(self.destination_dir, self.basename(ext)) + + def basename(self, ext): + ''' + Return the base name of the asciidoc source file but with extension + ext. + ''' + return os.path.basename(os.path.splitext(self.asciidoc_file)[0]) + ext + + def asciidoc_conf_file(self, path): + ''' + Return full path name of file in asciidoc configuration files directory. + Search first the directory containing the asciidoc executable then + the global configuration file directory. + ''' + f = os.path.join(os.path.dirname(self.asciidoc), path) + if not os.path.isfile(f): + f = os.path.join(CONF_DIR, path) + if not os.path.isfile(f): + die('missing configuration file: %s' % f) + return os.path.normpath(f) + + def xsl_stylesheet(self, file_name=None): + ''' + Return full path name of file in asciidoc docbook-xsl configuration + directory. + If an XSL file was specified with the --xsl-file option then it is + returned. + ''' + if self.xsl_file is not None: + return self.xsl_file + if not file_name: + file_name = self.format + '.xsl' + return self.asciidoc_conf_file(os.path.join('docbook-xsl', file_name)) + + def copy_resources(self, html_files, src_dir, dst_dir, resources=[]): + ''' + Search html_files for images and CSS resource URIs (html_files can be a + list of file names or a single file name). + Copy them from the src_dir to the dst_dir. + If not found in src_dir then recursively search all specified + resource directories. + Optional additional resources files can be passed in the resources list. + ''' + resources = resources[:] + resources += find_resources(html_files, 'link', 'href', + lambda attrs: attrs.get('type') == 'text/css') + resources += find_resources(html_files, 'img', 'src') + resources += self.resource_files + resources = list(set(resources)) # Drop duplicates. + resources.sort() + for f in resources: + if '=' in f: + src, dst = f.split('=') + if not dst: + dst = src + else: + src = dst = f + src = os.path.normpath(src) + dst = os.path.normpath(dst) + if os.path.isabs(dst): + die('absolute resource file name: %s' % dst) + if dst.startswith(os.pardir): + die('resource file outside destination directory: %s' % dst) + src = os.path.join(src_dir, src) + dst = os.path.join(dst_dir, dst) + if not os.path.isfile(src): + for d in self.resource_dirs: + d = os.path.join(src_dir, d) + found = find_files(d, os.path.basename(src)) + if found: + src = found[0] + break + else: + if not os.path.isfile(dst): + die('missing resource: %s' % src) + continue + # Arrive here if resource file has been found. + if os.path.normpath(src) != os.path.normpath(dst): + dstdir = os.path.dirname(dst) + shell_makedirs(dstdir) + shell_copy(src, dst) + + def to_backend(self): + ''' + Convert AsciiDoc source file to a backend output file using the global + 'to_<backend name>' function (loaded from backend plugin a2x-backend.py + file). + Executes the global function in an A2X class instance context. + ''' + eval('to_%s(self)' % self.backend) + + def to_docbook(self): + ''' + Use asciidoc to convert asciidoc_file to DocBook. + args is a string containing additional asciidoc arguments. + ''' + docbook_file = self.dst_path('.xml') + if self.skip_asciidoc: + if not os.path.isfile(docbook_file): + die('missing docbook file: %s' % docbook_file) + return + shell('"%s" --backend docbook -a "a2x-format=%s" %s --out-file "%s" "%s"' % + (self.asciidoc, self.format, self.asciidoc_opts, docbook_file, self.asciidoc_file)) + if not self.no_xmllint and XMLLINT: + shell('"%s" --nonet --noout --valid "%s"' % (XMLLINT, docbook_file)) + + def to_xhtml(self): + self.to_docbook() + docbook_file = self.dst_path('.xml') + xhtml_file = self.dst_path('.html') + opts = '%s --output "%s"' % (self.xsltproc_opts, xhtml_file) + exec_xsltproc(self.xsl_stylesheet(), docbook_file, self.destination_dir, opts) + src_dir = os.path.dirname(self.asciidoc_file) + self.copy_resources(xhtml_file, src_dir, self.destination_dir) + + def to_manpage(self): + self.to_docbook() + docbook_file = self.dst_path('.xml') + opts = self.xsltproc_opts + exec_xsltproc(self.xsl_stylesheet(), docbook_file, self.destination_dir, opts) + + def to_pdf(self): + if self.fop: + self.exec_fop() + else: + self.exec_dblatex() + + def exec_fop(self): + self.to_docbook() + docbook_file = self.dst_path('.xml') + xsl = self.xsl_stylesheet('fo.xsl') + fo = self.dst_path('.fo') + pdf = self.dst_path('.pdf') + opts = '%s --output "%s"' % (self.xsltproc_opts, fo) + exec_xsltproc(xsl, docbook_file, self.destination_dir, opts) + shell('"%s" %s -fo "%s" -pdf "%s"' % (FOP, self.fop_opts, fo, pdf)) + if not self.keep_artifacts: + shell_rm(fo) + + def exec_dblatex(self): + self.to_docbook() + docbook_file = self.dst_path('.xml') + xsl = self.asciidoc_conf_file(os.path.join('dblatex','asciidoc-dblatex.xsl')) + sty = self.asciidoc_conf_file(os.path.join('dblatex','asciidoc-dblatex.sty')) + shell('"%s" -t %s -p "%s" -s "%s" %s "%s"' % + (DBLATEX, self.format, xsl, sty, self.dblatex_opts, docbook_file)) + + def to_dvi(self): + self.exec_dblatex() + + def to_ps(self): + self.exec_dblatex() + + def to_tex(self): + self.exec_dblatex() + + def to_htmlhelp(self): + self.to_chunked() + + def to_chunked(self): + self.to_docbook() + docbook_file = self.dst_path('.xml') + opts = self.xsltproc_opts + xsl_file = self.xsl_stylesheet() + if self.format == 'chunked': + dst_dir = self.dst_path('.chunked') + elif self.format == 'htmlhelp': + dst_dir = self.dst_path('.htmlhelp') + if not 'base.dir ' in opts: + opts += ' --stringparam base.dir "%s/"' % os.path.basename(dst_dir) + # Create content. + shell_rmtree(dst_dir) + shell_makedirs(dst_dir) + exec_xsltproc(xsl_file, docbook_file, self.destination_dir, opts) + html_files = find_files(dst_dir, '*.html') + src_dir = os.path.dirname(self.asciidoc_file) + self.copy_resources(html_files, src_dir, dst_dir) + + def update_epub_manifest(self, opf_file): + ''' + Scan the OEBPS directory for any files that have not been registered in + the OPF manifest then add them to the manifest. + ''' + opf_dir = os.path.dirname(opf_file) + resource_files = [] + for (p,dirs,files) in os.walk(os.path.dirname(opf_file)): + for f in files: + f = os.path.join(p,f) + if os.path.isfile(f): + assert f.startswith(opf_dir) + f = '.' + f[len(opf_dir):] + f = os.path.normpath(f) + if f not in ['content.opf']: + resource_files.append(f) + opf = xml.dom.minidom.parseString(read_file(opf_file)) + manifest_files = [] + manifest = opf.getElementsByTagName('manifest')[0] + for el in manifest.getElementsByTagName('item'): + f = el.getAttribute('href') + f = os.path.normpath(f) + manifest_files.append(f) + count = 0 + for f in resource_files: + if f not in manifest_files: + count += 1 + verbose('adding to manifest: %s' % f) + item = opf.createElement('item') + item.setAttribute('href', f.replace(os.path.sep, '/')) + item.setAttribute('id', 'a2x-%d' % count) + mimetype = mimetypes.guess_type(f)[0] + if mimetype is None: + die('unknown mimetype: %s' % f) + item.setAttribute('media-type', mimetype) + manifest.appendChild(item) + if count > 0: + write_file(opf_file, opf.toxml()) + + def to_epub(self): + self.to_docbook() + xsl_file = self.xsl_stylesheet() + docbook_file = self.dst_path('.xml') + epub_file = self.dst_path('.epub') + build_dir = epub_file + '.d' + shell_rmtree(build_dir) + shell_makedirs(build_dir) + # Create content. + exec_xsltproc(xsl_file, docbook_file, build_dir, self.xsltproc_opts) + # Copy resources referenced in the OPF and resources referenced by the + # generated HTML (in theory DocBook XSL should ensure they are + # identical but this is not always the case). + src_dir = os.path.dirname(self.asciidoc_file) + dst_dir = os.path.join(build_dir, 'OEBPS') + opf_file = os.path.join(dst_dir, 'content.opf') + opf_resources = find_resources(opf_file, 'item', 'href') + html_files = find_files(dst_dir, '*.html') + self.copy_resources(html_files, src_dir, dst_dir, opf_resources) + # Register any unregistered resources. + self.update_epub_manifest(opf_file) + # Build epub archive. + cwd = os.getcwd() + shell_cd(build_dir) + try: + if not self.dry_run: + zip = zipfile.ZipFile(epub_file, 'w') + try: + # Create and add uncompressed mimetype file. + verbose('archiving: mimetype') + write_file('mimetype', 'application/epub+zip') + zip.write('mimetype', compress_type=zipfile.ZIP_STORED) + # Compress all remaining files. + for (p,dirs,files) in os.walk('.'): + for f in files: + f = os.path.normpath(os.path.join(p,f)) + if f != 'mimetype': + verbose('archiving: %s' % f) + zip.write(f, compress_type=zipfile.ZIP_DEFLATED) + finally: + zip.close() + verbose('created archive: %s' % epub_file) + finally: + shell_cd(cwd) + if not self.keep_artifacts: + shell_rmtree(build_dir) + if self.epubcheck and EPUBCHECK: + if not find_executable(EPUBCHECK): + warning('epubcheck skipped: unable to find executable: %s' % EPUBCHECK) + else: + shell('"%s" "%s"' % (EPUBCHECK, epub_file)) + + def to_text(self): + text_file = self.dst_path('.text') + html_file = self.dst_path('.text.html') + if self.lynx: + shell('"%s" %s --conf-file "%s" -b html4 -a "a2x-format=%s" -o "%s" "%s"' % + (self.asciidoc, self.asciidoc_opts, self.asciidoc_conf_file('text.conf'), + self.format, html_file, self.asciidoc_file)) + shell('"%s" -dump "%s" > "%s"' % + (LYNX, html_file, text_file)) + else: + # Use w3m(1). + self.to_docbook() + docbook_file = self.dst_path('.xml') + opts = '%s --output "%s"' % (self.xsltproc_opts, html_file) + exec_xsltproc(self.xsl_stylesheet(), docbook_file, + self.destination_dir, opts) + shell('"%s" -cols 70 -dump -T text/html -no-graph "%s" > "%s"' % + (W3M, html_file, text_file)) + if not self.keep_artifacts: + shell_rm(html_file) + + +##################################################################### +# Script main line. +##################################################################### + +if __name__ == '__main__': + description = '''A toolchain manager for AsciiDoc (converts Asciidoc text files to other file formats)''' + from optparse import OptionParser + parser = OptionParser(usage='usage: %prog [OPTIONS] SOURCE_FILE', + version='%s %s' % (PROG,VERSION), + description=description) + parser.add_option('-a', '--attribute', + action='append', dest='attributes', default=[], metavar='ATTRIBUTE', + help='set asciidoc attribute value') + parser.add_option('--asciidoc-opts', + action='append', dest='asciidoc_opts', default=[], + metavar='ASCIIDOC_OPTS', help='asciidoc options') + #DEPRECATED + parser.add_option('--copy', + action='store_true', dest='copy', default=False, + help='DEPRECATED: does nothing') + parser.add_option('--conf-file', + dest='conf_file', default=None, metavar='CONF_FILE', + help='configuration file') + parser.add_option('-D', '--destination-dir', + action='store', dest='destination_dir', default=None, metavar='PATH', + help='output directory (defaults to SOURCE_FILE directory)') + parser.add_option('-d','--doctype', + action='store', dest='doctype', metavar='DOCTYPE', + choices=('article','manpage','book'), + help='article, manpage, book') + parser.add_option('-b','--backend', + action='store', dest='backend', metavar='BACKEND', + help='name of backend plugin') + parser.add_option('--epubcheck', + action='store_true', dest='epubcheck', default=False, + help='check EPUB output with epubcheck') + parser.add_option('-f','--format', + action='store', dest='format', metavar='FORMAT', default = 'pdf', + choices=('chunked','epub','htmlhelp','manpage','pdf', 'text', + 'xhtml','dvi','ps','tex','docbook'), + help='chunked, epub, htmlhelp, manpage, pdf, text, xhtml, dvi, ps, tex, docbook') + parser.add_option('--icons', + action='store_true', dest='icons', default=False, + help='use admonition, callout and navigation icons') + parser.add_option('--icons-dir', + action='store', dest='icons_dir', + default=None, metavar='PATH', + help='admonition and navigation icon directory') + parser.add_option('-k', '--keep-artifacts', + action='store_true', dest='keep_artifacts', default=False, + help='do not delete temporary build files') + parser.add_option('--lynx', + action='store_true', dest='lynx', default=False, + help='use lynx to generate text files') + parser.add_option('-L', '--no-xmllint', + action='store_true', dest='no_xmllint', default=False, + help='do not check asciidoc output with xmllint') + parser.add_option('-n','--dry-run', + action='store_true', dest='dry_run', default=False, + help='just print the commands that would have been executed') + parser.add_option('-r','--resource', + action='append', dest='resources', default=[], + metavar='PATH', + help='resource file or directory containing resource files') + parser.add_option('-m', '--resource-manifest', + action='store', dest='resource_manifest', default=None, metavar='FILE', + help='read resources from FILE') + #DEPRECATED + parser.add_option('--resource-dir', + action='append', dest='resources', default=[], + metavar='PATH', + help='DEPRECATED: use --resource') + #DEPRECATED + parser.add_option('-s','--skip-asciidoc', + action='store_true', dest='skip_asciidoc', default=False, + help='DEPRECATED: redundant') + parser.add_option('--stylesheet', + action='store', dest='stylesheet', default=None, + metavar='STYLESHEET', + help='HTML CSS stylesheet file name') + #DEPRECATED + parser.add_option('--safe', + action='store_true', dest='safe', default=False, + help='DEPRECATED: does nothing') + parser.add_option('--dblatex-opts', + action='append', dest='dblatex_opts', default=[], + metavar='DBLATEX_OPTS', help='dblatex options') + parser.add_option('--backend-opts', + action='append', dest='backend_opts', default=[], + metavar='BACKEND_OPTS', help='backend plugin options') + parser.add_option('--fop', + action='store_true', dest='fop', default=False, + help='use FOP to generate PDF files') + parser.add_option('--fop-opts', + action='append', dest='fop_opts', default=[], + metavar='FOP_OPTS', help='options for FOP pdf generation') + parser.add_option('--xsltproc-opts', + action='append', dest='xsltproc_opts', default=[], + metavar='XSLTPROC_OPTS', help='xsltproc options for XSL stylesheets') + parser.add_option('--xsl-file', + action='store', dest='xsl_file', metavar='XSL_FILE', + help='custom XSL stylesheet') + parser.add_option('-v', '--verbose', + action='count', dest='verbose', default=0, + help='increase verbosity') + if len(sys.argv) == 1: + parser.parse_args(['--help']) + source_options = get_source_options(sys.argv[-1]) + argv = source_options + sys.argv[1:] + opts, args = parser.parse_args(argv) + if len(args) != 1: + parser.error('incorrect number of arguments') + opts.asciidoc_opts = ' '.join(opts.asciidoc_opts) + opts.dblatex_opts = ' '.join(opts.dblatex_opts) + opts.fop_opts = ' '.join(opts.fop_opts) + opts.xsltproc_opts = ' '.join(opts.xsltproc_opts) + opts.backend_opts = ' '.join(opts.backend_opts) + opts = eval(str(opts)) # Convert optparse.Values to dict. + a2x = A2X(opts) + OPTIONS = a2x # verbose and dry_run used by utility functions. + verbose('args: %r' % argv) + a2x.asciidoc_file = args[0] + try: + a2x.load_conf() + a2x.execute() + except KeyboardInterrupt: + exit(1) |