summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShaun McCance <shaunm@redhat.com>2021-01-30 22:43:05 -0500
committerShaun McCance <shaunm@redhat.com>2021-01-30 22:43:05 -0500
commitab19610426aeb43e000294e790ab3cd1eab9b146 (patch)
tree77eb7acdf1d371172d82d79bc046146ea591b524
parent7e610eb8d5d2fd8dfc28778ce1516a7b1bff6dc3 (diff)
downloadyelp-tools-ab19610426aeb43e000294e790ab3cd1eab9b146.tar.gz
yelp-build.py: Convert yelp-build to python
-rw-r--r--tools/yelp-build.py834
1 files changed, 834 insertions, 0 deletions
diff --git a/tools/yelp-build.py b/tools/yelp-build.py
new file mode 100644
index 0000000..1c52335
--- /dev/null
+++ b/tools/yelp-build.py
@@ -0,0 +1,834 @@
+#!/bin/python3
+#
+# yelp-build
+# Copyright (C) 2010-2020 Shaun McCance <shaunm@gnome.org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+import configparser
+import os
+import sys
+import shutil
+import subprocess
+import tempfile
+import urllib.parse
+import uuid
+
+import lxml.etree
+import lxml.ElementInclude
+
+
+# FIXME: don't hardcode these
+XSL_DB2HTML = '/usr/share/yelp-xsl/xslt/docbook/html/db2html.xsl'
+XSL_DB2XHTML = '/usr/share/yelp-xsl/xslt/docbook/html/db2xhtml.xsl'
+XSL_MALCACHE = '/usr/share/yelp-xsl/xslt/mallard/cache/mal-cache.xsl'
+XSL_MAL2HTML = '/usr/share/yelp-xsl/xslt/mallard/html/mal2html.xsl'
+XSL_MAL2XHTML = '/usr/share/yelp-xsl/xslt/mallard/html/mal2xhtml.xsl'
+XSL_MAL_OPF='/usr/share/yelp-tools/xslt/mal-opf.xsl'
+XSL_MAL_NCX='/usr/share/yelp-tools/xslt/mal-ncx.xsl'
+DATADIR = '/usr/share/yelp-tools'
+YELP_JSDIR = '/usr/share/yelp-xsl/js'
+
+XSLCOMMON = ('''
+<xsl:variable name="yelp.internal.datadir" select="'{intdatadir}'"/>
+<xsl:param name="html.css.root" select="$yelp.internal.datadir"/>
+<xsl:param name="html.js.root" select="$yelp.internal.datadir"/>
+{includes}
+<xsl:template name="html.css">
+ <xsl:param name="node" select="."/>
+ <xsl:variable name="yelp.locale">
+ <xsl:choose>
+ <xsl:when test="$node/@xml:lang != ''">
+ <xsl:value-of select="$node/@xml:lang"/>
+ </xsl:when>
+ <xsl:when test="$node/@lang != ''">
+ <xsl:value-of select="$node/@lang"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:text>C</xsl:text>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:variable>
+ <exsl:document href="{{$yelp.internal.datadir}}{{$yelp.locale}}.css" method="text">
+ <xsl:call-template name="html.css.content">
+ <xsl:with-param name="node" select="$node"/>
+ <xsl:with-param name="direction">
+ <xsl:call-template name="l10n.direction">
+ <xsl:with-param name="lang" select="$yelp.locale"/>
+ </xsl:call-template>
+ </xsl:with-param>
+ </xsl:call-template>
+ </exsl:document>
+ <link rel="stylesheet" type="text/css" href="{{$html.css.root}}{{$yelp.locale}}.css"/>
+</xsl:template>
+<xsl:template name="html.js.script">
+ <xsl:param name="node" select="."/>
+ <exsl:document href="{{$yelp.internal.datadir}}yelp.js" method="text">
+ <xsl:call-template name="html.js.content">
+ <xsl:with-param name="node" select="$node"/>
+ </xsl:call-template>
+ </exsl:document>
+ <script type="text/javascript" src="{{$html.js.root}}yelp.js"/>
+</xsl:template>
+''')
+
+DB2HTML = ('''
+<xsl:stylesheet
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:exsl="http://exslt.org/common"
+ xmlns="http://www.w3.org/1999/xhtml"
+ extension-element-prefixes="exsl"
+ version="1.0">
+<xsl:import href="file://{xslfile}"/>
+'''
++ XSLCOMMON +
+'''
+</xsl:stylesheet>
+''')
+
+
+MAL2HTML = ('''
+<xsl:stylesheet
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:mal="http://projectmallard.org/1.0/"
+ xmlns:cache="http://projectmallard.org/cache/1.0/"
+ xmlns:exsl="http://exslt.org/common"
+ xmlns="http://www.w3.org/1999/xhtml"
+ exclude-result-prefixes="mal cache"
+ extension-element-prefixes="exsl"
+ version="1.0">
+<xsl:import href="file://{xslfile}"/>
+<xsl:param name="mal.cache.file" select="'file://{cachefile}'"/>
+'''
++ XSLCOMMON +
+'''
+<xsl:template match="/">
+ <xsl:for-each select="cache:cache/mal:page | cache:cache/mal:stack">
+ <xsl:variable name="href" select="@cache:href"/>
+ <xsl:for-each select="document(@cache:href)">
+ <xsl:for-each select="mal:page | mal:stack/mal:page">
+ <xsl:call-template name="html.output"/>
+ </xsl:for-each>
+ </xsl:for-each>
+ </xsl:for-each>
+</xsl:template>
+</xsl:stylesheet>
+''')
+
+
+
+class InputFile:
+ def __init__(self, filepath, filename, sitedir=None):
+ self.filepath = filepath
+ self.filename = filename
+ self.absfile = os.path.join(filepath, filename)
+ self.absdir = os.path.dirname(self.absfile)
+ self.sitedir = sitedir or ''
+ self.sitefilename = self.sitedir + self.filename
+
+
+class PathResolver(lxml.etree.Resolver):
+ def __init__(self, srcdir, path):
+ if srcdir.endswith('/'):
+ self.srcdir = srcdir
+ else:
+ self.srcdir = srcdir + '/'
+ self.path = path
+
+ def resolve(self, uri, id, context):
+ if os.path.exists(uri):
+ return self.resolve_filename(uri, context)
+ if uri.startswith(self.srcdir):
+ ref = uri[len(self.srcdir):]
+ else:
+ ref = uri
+ for p in self.path:
+ tryfile = os.path.join(p, ref)
+ if os.path.exists(tryfile):
+ return self.resolve_filename(tryfile, context)
+ return None
+
+
+class Builder:
+ name = None
+ desc = None
+ blurb = None
+ formats = []
+ arguments = []
+ postblurb = None
+ config = None
+
+ def __init__(self, yelpbuild):
+ self.yelpbuild = yelpbuild
+ self.options = {}
+ self.fileargs = []
+ self.tmpdir = None
+
+ def __del__(self):
+ if self.tmpdir is not None:
+ shutil.rmtree(self.tmpdir)
+ self.tmpdir = None
+
+ def parse_args(self, args):
+ while len(args) > 0:
+ argdef = None
+ if args[0].startswith('--'):
+ for arg_ in self.arguments:
+ if args[0] == '--' + arg_[0]:
+ argdef = arg_
+ break
+ if argdef is None:
+ self.print_help()
+ return 1
+ elif args[0].startswith('-'):
+ for arg_ in self.arguments:
+ if args[0] == arg_[1]:
+ argdef = arg_
+ break
+ if argdef is None:
+ self.print_help()
+ return 1
+ if argdef is not None:
+ takesarg = (argdef[2] is not None)
+ if takesarg:
+ if len(args) < 2:
+ self.print_help()
+ return 1
+ self.options.setdefault(argdef[0], [])
+ self.options[argdef[0]].append(args[1])
+ args = args[2:]
+ else:
+ self.options[argdef[0]] = True
+ args = args[1:]
+ else:
+ self.fileargs.append(args[0])
+ args = args[1:]
+ cfgfile = None
+ if len(self.fileargs) > 0:
+ cfgfile = os.path.join(os.path.dirname(self.fileargs[0]), '.yelp-tools.cfg')
+ if not os.path.exists(cfgfile):
+ cfgfile = None
+ if cfgfile is None:
+ cfgfile = os.path.join(os.getcwd(), '.yelp-tools.cfg')
+ if os.path.exists(cfgfile):
+ self.config = configparser.ConfigParser()
+ try:
+ self.config.read(cfgfile)
+ except Exception as e:
+ print(e, file=sys.stderr)
+ sys.exit(1)
+ return 0
+
+ def get_option_bool(self, arg):
+ if arg in self.options:
+ return self.options[arg] == True
+ if self.config is not None:
+ val = self.config.get('build:' + self.name, arg, fallback=None)
+ if val is not None:
+ return (val == 'true')
+ val = self.config.get('build', arg, fallback=None)
+ if val is not None:
+ return (val == 'true')
+ val = self.config.get('default', arg, fallback=None)
+ if val is not None:
+ return (val == 'true')
+ return False
+
+ def get_option_str(self, arg):
+ if arg in self.options:
+ if isinstance(self.options[arg], list):
+ return self.options[arg][-1]
+ if self.config is not None:
+ val = self.config.get('build:' + self.name, arg, fallback=None)
+ if val is not None:
+ return val
+ val = self.config.get('build', arg, fallback=None)
+ if val is not None:
+ return val
+ val = self.config.get('default', arg, fallback=None)
+ if val is not None:
+ return val
+ return None
+
+ def get_option_list(self, arg):
+ if arg in self.options:
+ if isinstance(self.options[arg], list):
+ ret = []
+ for opt in self.options[arg]:
+ ret.extend(opt.replace(',', ' ').split())
+ return ret
+ if self.config is not None:
+ val = self.config.get('build:' + self.name, arg, fallback=None)
+ if val is not None:
+ return val.replace(',', ' ').split()
+ val = self.config.get('build', arg, fallback=None)
+ if val is not None:
+ return val.replace(',', ' ').split()
+ val = self.config.get('default', arg, fallback=None)
+ if val is not None:
+ return val.replace(',', ' ').split()
+ return None
+
+ def get_xml(self, infile, path):
+ parser = lxml.etree.XMLParser()
+ parser.resolvers.add(PathResolver(os.path.realpath(infile.absdir), path))
+ tree = lxml.etree.parse(infile.absfile, parser=parser)
+ def pathloader(href, parse, encoding=None):
+ usefile = os.path.join(infile.absdir, href)
+ if not os.path.exists(href):
+ usefile = None
+ if usefile is None:
+ absdir = infile.absdir
+ if not absdir.endswith('/'):
+ absdir = absdir + '/'
+ ref = href
+ if ref.startswith(absdir):
+ ref = ref[len(absdir):]
+ for p in path:
+ tryfile = os.path.join(p, ref)
+ if os.path.exists(tryfile):
+ usefile = tryfile
+ break
+ if usefile is not None:
+ if parse == 'xml':
+ return lxml.etree.parse(usefile, parser=parser).getroot()
+ elif parse == 'text':
+ return open(usefile).read()
+ return None
+ lxml.ElementInclude.include(tree, loader=pathloader)
+ return tree
+
+ def iter_files(self, sitedir=None):
+ issite = self.get_option_bool('site')
+ if len(self.fileargs) == 0:
+ self.fileargs.append('.')
+ for filearg in self.fileargs:
+ if os.path.isdir(filearg):
+ if issite:
+ for infile in self.iter_site(filearg, '/'):
+ yield infile
+ else:
+ for fname in os.listdir(filearg):
+ if fname.endswith('.page'):
+ yield InputFile(filearg, fname)
+ else:
+ if issite:
+ # FIXME: should do some normalization here, I guess.
+ # It's hard to get this perfect without a defined start dir
+ yield InputFile(os.getcwd(), filearg, '/' + os.path.dirname(filearg))
+ else:
+ yield InputFile(os.getcwd(), filearg)
+
+ def iter_site(self, filepath, sitedir):
+ for fname in os.listdir(filepath):
+ newpath = os.path.join(filepath, fname)
+ if os.path.isdir(newpath):
+ # FIXME https://github.com/projectmallard/pintail/issues/36
+ if fname == '__pintail__':
+ continue
+ for infile in self.iter_site(newpath, sitedir + fname + '/'):
+ yield infile
+ elif fname.endswith('.page'):
+ yield InputFile(filepath, fname, sitedir)
+
+ def create_tmpdir(self):
+ if self.tmpdir is None:
+ self.tmpdir = tempfile.mkdtemp()
+
+ def print_help(self):
+ print('Usage: yelp-build ' + self.name + ' [OPTIONS] [FILES]')
+ print('Formats: ' + ' '.join(self.formats) + '\n')
+ #FIXME: prettify names of formats
+ if self.blurb is not None:
+ print(self.blurb + '\n')
+ print('Options:')
+ maxarglen = 2
+ args = []
+ for arg in self.arguments:
+ argkey = '--' + arg[0]
+ if arg[1] is not None:
+ argkey = arg[1] + ', ' + argkey
+ if arg[2] is not None:
+ argkey = argkey + ' ' + arg[2]
+ args.append((argkey, arg[3]))
+ for arg in args:
+ maxarglen = max(maxarglen, len(arg[0]) + 1)
+ for arg in args:
+ print(' ' + (arg[0]).ljust(maxarglen) + ' ' + arg[1])
+ if self.postblurb is not None:
+ print(self.postblurb)
+
+ def main(self, args):
+ pass
+
+
+class CacheBuilder (Builder):
+ name = 'cache'
+ desc = 'Convert a Mallard cache file'
+ blurb = ('Create a Mallard cache file from the page files FILES.\n' +
+ 'If FILES contains directories, all .page files in those\n' +
+ 'directories will be used.')
+ formats = ['mallard']
+ arguments = [
+ ('help', '-h', None, 'Show this help and exit'),
+ ('output', '-o', 'OUT', 'Output files in the directory OUT'),
+ ('path', '-p', 'PATH', 'Extra directories to search for files'),
+ ('site', '-s', None, 'Treat pages as belonging to a Mallard site')
+ ]
+
+ def build_cache_in(self, filename):
+ with open(filename, 'w') as cachein:
+ print('<cache:cache xmlns:cache="http://projectmallard.org/cache/1.0/"' +
+ ' xmlns:site="http://projectmallard.org/site/1.0/"'
+ ' xmlns="http://projectmallard.org/1.0/">',
+ file=cachein)
+ for infile in self.iter_files():
+ if infile.filename.endswith('.page'):
+ page = '<page'
+ elif infile.filename.endswith('.stack'):
+ page = '<stack'
+ else:
+ continue
+ page += ' cache:href="file://' + urllib.parse.quote(os.path.realpath(infile.absfile)) + '"'
+ if self.get_option_bool('site'):
+ page += ' site:dir="' + infile.sitedir + '"'
+ page += '/>'
+ print(page, file=cachein)
+ print('</cache:cache>', file=cachein)
+
+ def main(self, args, output=None, path=None):
+ if self.parse_args(args) != 0:
+ return 1
+ if 'help' in self.options:
+ self.print_help()
+ return 0
+
+ retcode = 0
+ self.create_tmpdir()
+ cacheinfile = os.path.join(self.tmpdir, 'index.cache.in')
+ self.build_cache_in(cacheinfile)
+ if output is None:
+ output = self.get_option_str('output')
+ if output is None:
+ output = 'index.cache'
+ if path is None:
+ path = self.get_option_list('path')
+ if path is None:
+ path = ':'
+ else:
+ path = ':'.join(path)
+ retcode = subprocess.call(['xsltproc', '--xinclude', '-o', output,
+ '--path', path,
+ XSL_MALCACHE, cacheinfile])
+ return retcode
+
+
+class XhtmlBuilder (Builder):
+ name = 'xhtml'
+ desc = 'Convert input files to XHTML'
+ blurb = ('Create XHTML output from the input files FILES.\n' +
+ 'FILES can be DocBook files, Mallard page files,\n' +
+ 'or directories containing Mallard page files.')
+ formats = ['docbook4', 'docbook5', 'mallard']
+ arguments = [
+ ('help', '-h', None, 'Show this help and exit'),
+ ('cache', '-c', 'CACHE', 'Use the existing Mallard cache CACHE'),
+ ('output', '-o', 'OUT', 'Output files in the directory OUT'),
+ ('xsl', '-x', 'CUSTOM', 'Import the custom XSLT file CUSTOM'),
+ ('path', '-p', 'PATH', 'Extra directories to search for files'),
+ ('ignore', '-i', None, 'Ignore missing media files')
+ ]
+
+ def __init__(self, yelpbuild, xhtml=True, epub=False):
+ super().__init__(yelpbuild)
+ self.mal2html = None
+ self.db2html = None
+ self.xhtml = xhtml
+ self.epub = epub
+ if self.epub:
+ self.intdatadir = 'yelp'
+ else:
+ self.intdatadir = ''
+ self.cacheinfile = None
+
+
+ def build_mallard_all(self, cache=None, output=None, xsl=None, path=None):
+ if self.mal2html is not None:
+ # We build all the pages on the first call, because it's faster
+ return 0
+ if path is None:
+ path = self.get_option_list('path')
+ self.create_tmpdir()
+ if cache is None:
+ cachefile = self.get_option_str('cache')
+ else:
+ cachefile = cache
+ cachebuilder = CacheBuilder(self.yelpbuild)
+ if cachefile is None:
+ cachefile = os.path.join(self.tmpdir, 'index.cache')
+ retcode = cachebuilder.main(self.fileargs, output=cachefile, path=path)
+ if retcode != 0:
+ return retcode
+ self.cacheinfile = cachefile
+ else:
+ cachefile = os.path.realpath(cachefile)
+ self.cacheinfile = os.path.join(self.tmpdir, 'index.cache.in')
+ cachebuilder.parse_args(self.fileargs)
+ cachebuilder.build_cache_in(self.cacheinfile)
+ self.mal2html = os.path.join(self.tmpdir, 'mal2html.xsl')
+ with open(self.mal2html, 'w') as xslout:
+ if self.xhtml:
+ xslfile = XSL_MAL2XHTML
+ else:
+ xslfile = XSL_MAL2HTML
+ includes = ''
+ if xsl is None:
+ customxsl = self.get_option_str('xsl')
+ else:
+ customxsl = xsl
+ if customxsl is not None:
+ customxsl = urllib.parse.quote(os.path.realpath(customxsl))
+ includes += '<xsl:include href="file://' + customxsl + '"/>'
+ if self.epub:
+ includes += '''<xsl:param name="mal.if.target" select="'target:epub target:html target:xhtml'"/>'''
+ includes += '''<xsl:template mode="html.header.mode" match="mal:page"/>'''
+ includes += '''<xsl:template mode="html.footer.mode" match="mal:page"/>'''
+ xslout.write(MAL2HTML.format(xslfile=xslfile,
+ cachefile=cachefile,
+ includes=includes,
+ intdatadir=self.intdatadir))
+ if output is None:
+ output = self.get_option_str('output')
+ if output is None:
+ output = os.getcwd()
+ else:
+ if not os.path.isdir(output):
+ print('Output must be a directory', file=sys.stderr)
+ return 1
+ if not output.endswith('/'):
+ # xsltproc is picky about this
+ output = output + '/'
+ if path is None:
+ pathstr = ':'
+ else:
+ pathstr = ':'.join(path)
+ retcode = subprocess.call(['xsltproc', '--xinclude', '-o', output,
+ '--path', pathstr,
+ '--stringparam', 'mal.cache.file', cachefile,
+ self.mal2html, self.cacheinfile])
+ return retcode
+
+
+ def build_docbook(self, infile, output=None, xsl=None, path=None):
+ if self.db2html is None:
+ self.create_tmpdir()
+ self.db2html = os.path.join(self.tmpdir, 'db2html.xsl')
+ with open(self.db2html, 'w') as xslout:
+ if self.xhtml:
+ xslfile = XSL_DB2XHTML
+ else:
+ xslfile = XSL_DB2HTML
+ includes = ''
+ if xsl is not None:
+ customxsl = xsl
+ else:
+ customxsl = self.get_option_str('xsl')
+ if customxsl is not None:
+ customxsl = urllib.parse.quote(os.path.realpath(customxsl))
+ includes += '<xsl:include href="file://' + customxsl + '"/>'
+ xslout.write(DB2HTML.format(xslfile=xslfile,
+ includes=includes,
+ intdatadir=self.intdatadir))
+ if output is None:
+ output = self.get_option_str('output')
+ if output is None:
+ output = os.getcwd()
+ else:
+ if not os.path.isdir(output):
+ print('Output must be a directory', file=sys.stderr)
+ return 1
+ if path is None:
+ path = self.get_option_list('path')
+ if path is None:
+ pathstr = ':'
+ else:
+ pathstr = ':'.join(path)
+ retcode = subprocess.call(['xsltproc', '--xinclude', '-o', output,
+ '--path', pathstr,
+ self.db2html, infile.absfile])
+ return retcode
+
+
+ def main(self, args, cache=None, output=None, xsl=None, path=None, ignore=None):
+ if self.parse_args(args) != 0:
+ return 1
+ if 'help' in self.options:
+ self.print_help()
+ return 0
+
+ if path is None:
+ pathopt = self.get_option_list('path')
+ else:
+ pathopt = path
+ path = []
+ if pathopt is not None:
+ for p in pathopt:
+ path.extend(p.split(':'))
+ if output is None:
+ output = self.get_option_str('output')
+ srcs = {}
+ for infile in self.iter_files():
+ if infile.filename.endswith('.page') or infile.filename.endswith('.stack'):
+ retcode = self.build_mallard_all(cache=cache, output=output, xsl=xsl, path=path)
+ if retcode != 0:
+ return retcode
+ if output is not None:
+ tree = self.get_xml(infile, path)
+ if tree is None:
+ return 1
+ for el in tree.xpath('//*[@src]'):
+ src = el.get('src')
+ srcs.setdefault(src, [])
+ orig = os.path.join(os.path.realpath(infile.absdir), src)
+ if orig not in srcs[src]:
+ srcs[src].append(orig)
+ elif infile.filename.endswith('.docbook') or infile.filename.endswith('.xml'):
+ retcode = self.build_docbook(infile, output=output, xsl=xsl, path=path)
+ if retcode != 0:
+ return retcode
+ if output is not None:
+ tree = self.get_xml(infile, path)
+ if tree is None:
+ return 1
+ for el in tree.xpath('//*[@fileref]'):
+ src = el.get('fileref')
+ srcs.setdefault(src, [])
+ orig = os.path.join(os.path.realpath(infile.absdir), src)
+ if orig not in srcs[src]:
+ srcs[src].append(orig)
+ else:
+ print('Error: No builder for ' + infile.filename)
+ return 1
+
+ if ignore is None:
+ ignore = self.get_option_bool('ignore')
+ tocopy = {}
+ for src in srcs:
+ useorig = None
+ for orig in srcs[src]:
+ if os.path.exists(orig):
+ if useorig is None:
+ useorig = orig
+ else:
+ print('Warning: Multiple sources for ' + src + '. Using first.',
+ file=sys.stderr)
+ if useorig is None:
+ for p in path:
+ tryorig = os.path.join(p, src)
+ if os.path.exists(tryorig):
+ useorig = tryorig
+ break
+ if useorig is None:
+ if ignore:
+ print('Warning: No source found for ' + src, file=sys.stderr)
+ else:
+ print('Error: No source found for ' + src, file=sys.stderr)
+ return 1
+ if useorig is not None:
+ destfile = os.path.join(output, src)
+ destdir = os.path.dirname(destfile)
+ os.makedirs(destdir, exist_ok=True)
+ shutil.copyfile(useorig, destfile)
+
+ if output is None:
+ shutil.copyfile(os.path.join(YELP_JSDIR, 'highlight.pack.js'),
+ os.path.join(self.intdatadir, 'highlight.pack.js'))
+ else:
+ shutil.copyfile(os.path.join(YELP_JSDIR, 'highlight.pack.js'),
+ os.path.join(output, self.intdatadir, 'highlight.pack.js'))
+
+ return 0
+
+
+class HtmlBuilder (Builder):
+ name = 'html'
+ desc = 'Convert input files to HTML'
+ blurb = ('Create HTML output from the input files FILES.\n' +
+ 'FILES can be DocBook files, Mallard page files,\n' +
+ 'or directories containing Mallard page files.')
+ formats = ['docbook4', 'docbook5', 'mallard']
+ arguments = [
+ ('help', '-h', None, 'Show this help and exit'),
+ ('cache', '-c', 'CACHE', 'Use the existing Mallard cache CACHE'),
+ ('output', '-o', 'OUT', 'Output files in the directory OUT'),
+ ('xsl', '-x', 'CUSTOM', 'Import the custom XSLT file CUSTOM'),
+ ('path', '-p', 'PATH', 'Extra directories to search for files'),
+ ('ignore', '-i', None, 'Ignore missing media files')
+ ]
+
+ def __init__(self, yelpbuild):
+ super().__init__(yelpbuild)
+ self.xhtmlbuilder = XhtmlBuilder(yelpbuild, xhtml=False)
+
+ def main(self, args):
+ if self.parse_args(args) != 0:
+ return 1
+ if 'help' in self.options:
+ self.print_help()
+ return 0
+
+ return self.xhtmlbuilder.main(args)
+
+
+class EpubBuilder (Builder):
+ name = 'epub'
+ desc = 'Create an EPUB file for Mallard'
+ blurb = ('Create an EPUB file from the Mallard page files FILES')
+ formats = ['mallard']
+ arguments = [
+ ('help', '-h', None, 'Show this help and exit'),
+ ('cache', '-c', 'CACHE', 'Use the existing Mallard cache CACHE'),
+ ('output', '-o', 'OUT', 'Output files in the directory OUT'),
+ ('xsl', '-x', 'CUSTOM', 'Import the custom XSLT file CUSTOM'),
+ ('path', '-p', 'PATH', 'Extra directories to search for files'),
+ ('ignore', '-i', None, 'Ignore missing media files'),
+ ('nozip', None, None, 'Do not zip the output directory')
+ ]
+
+ def __init__(self, yelpbuild):
+ super().__init__(yelpbuild)
+
+ def main(self, args):
+ if self.parse_args(args) != 0:
+ return 1
+ if 'help' in self.options:
+ self.print_help()
+ return 0
+
+ output = self.get_option_str('output')
+ nozip = self.get_option_bool('nozip')
+ if nozip:
+ if output is None:
+ output = 'EPUB'
+ if os.path.isfile(output):
+ print('Error: Output must be a directory', file=sys.stderr)
+ sys.exit(1)
+ epubdir = output
+ else:
+ self.create_tmpdir()
+ if output is None:
+ output = 'index.epub'
+ if os.path.isdir(output):
+ print('Error: Output must be a file', file=sys.stderr)
+ sys.exit(1)
+ epubdir = os.path.join(self.tmpdir, 'EPUB')
+ os.makedirs(epubdir, exist_ok=True)
+ os.makedirs(os.path.join(epubdir, 'OPS', 'yelp'), exist_ok=True)
+
+ xhtmlbuilder = XhtmlBuilder(self.yelpbuild, epub=True)
+ retcode = xhtmlbuilder.main(self.fileargs,
+ cache=self.get_option_str('cache'),
+ output=os.path.join(epubdir, 'OPS'),
+ xsl=self.get_option_str('xsl'),
+ path=self.get_option_list('path'),
+ ignore=self.get_option_bool('ignore'))
+ if retcode != 0:
+ return retcode
+
+ with open(os.path.join(epubdir, 'mimetype'), 'w') as fd:
+ fd.write('application/epub+zip\n')
+
+ os.makedirs(os.path.join(epubdir, 'META-INF'), exist_ok=True)
+
+ with open(os.path.join(epubdir, 'META-INF', 'container.xml'), 'w') as fd:
+ fd.write('<?xml version="1.0" encoding="UTF-8"?>')
+ fd.write('<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">')
+ fd.write('<rootfiles>')
+ fd.write('<rootfile full-path="OPS/opf.opf" media-type="application/oebps-package+xml"/>')
+ fd.write('</rootfiles>')
+ fd.write('</container>\n')
+
+ path = self.get_option_list('path')
+ if path is None:
+ pathstr = ':'
+ else:
+ pathstr = ':'.join(path)
+ epubid = str(uuid.uuid4())
+ opfdata = ''
+ for fname in os.listdir(os.path.join(epubdir, 'OPS', 'yelp')):
+ opfdata += ' OPS/yelp/' + urllib.parse.quote(fname)
+ retcode = subprocess.call(['xsltproc', '--xinclude',
+ '-o', os.path.join(epubdir, 'OPS', 'opf.opf'),
+ '--path', pathstr,
+ '--stringparam', 'opf.id', epubid,
+ '--stringparam', 'opf.data', opfdata,
+ XSL_MAL_OPF, xhtmlbuilder.cacheinfile])
+ if retcode != 0:
+ return retcode
+ retcode = subprocess.call(['xsltproc', '--xinclude',
+ '-o', os.path.join(epubdir, 'OPS', 'ncx.ncx'),
+ '--path', pathstr,
+ '--stringparam', 'ncx.id', epubid,
+ XSL_MAL_NCX, xhtmlbuilder.cacheinfile])
+ if retcode != 0:
+ return retcode
+
+ if not nozip:
+ retcode = subprocess.call(['zip', '-q', '-r', os.path.realpath(output),
+ 'mimetype', 'META-INF', 'OPS'],
+ cwd=os.path.realpath(epubdir))
+ if retcode != 0:
+ return retcode
+ return 0
+
+
+class YelpBuild:
+ def __init__(self):
+ pass
+
+ def main(self):
+ if len(sys.argv) < 2:
+ self.print_usage()
+ return 1
+
+ builder = None
+ for cls in Builder.__subclasses__():
+ if sys.argv[1] == cls.name:
+ builder = cls(self)
+
+ if builder is None:
+ print('Unrecognized command: ' + sys.argv[1], file=sys.stderr)
+ return 1
+
+ return builder.main(sys.argv[2:])
+
+ def print_usage(self):
+ print('Usage: yelp-builder <COMMAND> [OPTIONS] [FILES]')
+ namelen = 2
+ builders = []
+ for cls in sorted(Builder.__subclasses__(), key=(lambda cls: cls.name or '')):
+ namelen = max(namelen, len(cls.name) + 2)
+ builders.append(cls)
+
+ print('\nCommands:')
+ for cls in builders:
+ print(' ' + cls.name.ljust(namelen) + cls.desc)
+
+
+if __name__ == '__main__':
+ try:
+ sys.exit(YelpBuild().main())
+ except KeyboardInterrupt:
+ sys.exit(1)