For files which do not qualify to be included as part of the HTML index it is needed to translate the sectioning when assembling the TeX code for PDF file generation, because in this case the files are included inline and it is needed for them to create subsections instead of top level chapters. Also, the sorting is improved to take only into account the numnbering that precedes the descriptive text in the filenames of .rst sources. This makes sorting the files for inlining in the TeX output work as expected.
351 lines
9.8 KiB
Python
Executable file
351 lines
9.8 KiB
Python
Executable file
#! /usr/bin/env python
|
|
|
|
import sys, shlex, subprocess
|
|
from optparse import OptionParser, OptionGroup
|
|
from docutils.writers import latex2e
|
|
import re
|
|
|
|
try:
|
|
import locale
|
|
locale.setlocale(locale.LC_ALL, '')
|
|
except:
|
|
pass
|
|
|
|
|
|
class AttributedDict(object):
|
|
def __init__(self):
|
|
object.__setattr__(self, "_items", dict())
|
|
|
|
def __getattr__(self, key):
|
|
return self._items.get(key)
|
|
|
|
def __delattr__(self, key):
|
|
if key in self._items:
|
|
del self._items[key]
|
|
else:
|
|
object.__delattr__(self, key)
|
|
|
|
def __setattr__(self, key, val):
|
|
if hasattr(self, key):
|
|
object.__setattr__(self, key, val)
|
|
else:
|
|
self._items[key] = val
|
|
|
|
|
|
parser = OptionParser(usage="Usage: %prog <command> [options]")
|
|
OPT = AttributedDict()
|
|
|
|
OPT.common = OptionGroup(parser, "Common options")
|
|
|
|
OPT.docinfo = OptionGroup(parser, "Documentation information")
|
|
OPT.docinfo.add_option("-i", "--info", dest="docinfo",
|
|
default=None, metavar="FILE",
|
|
help="Read project information from FILE")
|
|
|
|
|
|
is_index_filename = re.compile(r'^[0-9]{2}-[a-zA-Z].*').match
|
|
|
|
|
|
def str_to_numeric_tuple(s):
|
|
r = []
|
|
for item in s.split("-"):
|
|
try:
|
|
r.append(int(item, 10))
|
|
except ValueError:
|
|
# Tried to convert a non-integer, bail out
|
|
break
|
|
return tuple(r)
|
|
|
|
def str_tuple_sort(a, b):
|
|
return cmp(str_to_numeric_tuple(a), str_to_numeric_tuple(b))
|
|
|
|
|
|
def optparser(*groups):
|
|
parser.add_option_group(OPT.common)
|
|
[parser.add_option_group(g) for g in groups]
|
|
return parser
|
|
|
|
|
|
class DupRefsRemover(object):
|
|
def __init__(self, out):
|
|
if hasattr(out, "write"):
|
|
self.out = out.write
|
|
elif hasattr(out, "handle_line"):
|
|
self.out = out.handle_line
|
|
else:
|
|
raise ValueError("%r has no write/handle_line" % out)
|
|
self.ref = dict()
|
|
|
|
def handle_line(self, line):
|
|
if line.startswith(".. _") and ":" in line:
|
|
r = map(lambda x: x.strip(), line[4:].split(":", 1))
|
|
if r[0] in self.ref:
|
|
# Do some sanity check
|
|
if self.ref[r[0]] != r[1]:
|
|
raise ValueError("Reference '%s' mismatch (%s - %s)"
|
|
% (r[0], self.ref[r[0]], r[1]))
|
|
else:
|
|
# Add reference and print it out
|
|
self.ref[r[0]] = r[1]
|
|
self.out(line)
|
|
else:
|
|
self.out(line)
|
|
|
|
|
|
class UnderlineXlate(object):
|
|
_XLATE = {
|
|
"#": "=",
|
|
"=": "-",
|
|
"-": "~",
|
|
"~": "^",
|
|
}
|
|
|
|
def __init__(self, out):
|
|
if hasattr(out, "write"):
|
|
self.out = out.write
|
|
elif hasattr(out, "handle_line"):
|
|
self.out = out.handle_line
|
|
else:
|
|
raise ValueError("%r has no write/handle_line" % out)
|
|
|
|
def handle_line(self, line):
|
|
sline = line.strip()
|
|
if len(sline):
|
|
if sline[0] in self._XLATE:
|
|
sline = sline.replace(sline[0], self._XLATE[sline[0]])
|
|
self.out(sline)
|
|
return
|
|
self.out(line)
|
|
|
|
|
|
|
|
class ShellEscapeRunner(object):
|
|
def __init__(self, out):
|
|
self.out = out
|
|
|
|
def _expand(self, cmdline):
|
|
cmd = shlex.split(cmdline, False)
|
|
pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
|
|
return "\n ".join(pipe.communicate()[0].rstrip().splitlines())
|
|
|
|
|
|
def handle_line(self, line):
|
|
index = line.find("!!")
|
|
if index < 0:
|
|
self.out.write(line)
|
|
return
|
|
|
|
self.out.write(line[:index].replace("\\!", "!"))
|
|
cmd = line[index+2:]
|
|
end = cmd.find("!!")
|
|
if end > index:
|
|
self.out.write(self._expand(cmd[:end].replace("\\!", "!")))
|
|
self.out.write(cmd[end+2:].replace("\\!", "!"))
|
|
else:
|
|
self.out.write(self._expand(cmd.replace("\\!", "!")))
|
|
self.out.write("\n")
|
|
|
|
|
|
|
|
LATEX_SETTINGS = {
|
|
"documentclass" : "scrbook",
|
|
"documentoptions" : "11pt,oneside,a4paper",
|
|
"table_style" : "booktabs",
|
|
"use_latex_footnotes": 1,
|
|
"use_latex_citations": 1,
|
|
"use_latex_toc" : 1,
|
|
"use_latex_docinfo" : 0,
|
|
"use_latex_abstract" : 1,
|
|
}
|
|
|
|
EBOOK_SETTINGS = dict(LATEX_SETTINGS)
|
|
EBOOK_SETTINGS["documentoptions"] = "14pt,oneside,b5paper"
|
|
|
|
HTML_SETTINGS = {
|
|
"field_name_limit" : 20,
|
|
"cloak_email_addresses": 1,
|
|
}
|
|
|
|
|
|
# Those are two mocks needed to properly reuse the LaTeX code generator, but
|
|
# avoiding generation of all the cruft from the preamble and the postamble,
|
|
# and only the middle part.
|
|
#
|
|
class LaTeXStandaloneTranslator(latex2e.LaTeXTranslator):
|
|
def __init__(self, document):
|
|
self._class = document.settings.documentclass
|
|
self._typearea = ""
|
|
|
|
latex2e.LaTeXTranslator.__init__(self, document)
|
|
|
|
# XXX We rely on DocumentClass having a document_class attribute and
|
|
# on it not changing the attribute once initialization was done
|
|
#
|
|
if self.d_class.document_class == "igaliabk":
|
|
self.d_class.document_class = "book"
|
|
|
|
try:
|
|
del self.head_prefix[self.head_prefix.index(self.typearea)]
|
|
except AttributeError:
|
|
pass
|
|
|
|
def __get_typearea(self):
|
|
if self._class == "igaliabk" or self._class.startswith("scr"):
|
|
value = ""
|
|
else:
|
|
value = self._typearea
|
|
return value
|
|
|
|
def __set_typearea(self, value):
|
|
self._typearea = value
|
|
|
|
typarea = property(__get_typearea, __set_typearea)
|
|
|
|
|
|
|
|
class LaTeXInsertTranslator(LaTeXStandaloneTranslator):
|
|
def astext(self):
|
|
return "".join(self.body)
|
|
|
|
class LaTeXCustomWriter(latex2e.Writer):
|
|
def __init__(self, translator_class):
|
|
latex2e.Writer.__init__(self)
|
|
self.translator_class = translator_class
|
|
|
|
|
|
|
|
class DocTool(object):
|
|
|
|
def main(self, argv=None):
|
|
if argv is None:
|
|
argv = sys.argv
|
|
|
|
if len(argv) < 2:
|
|
raise SystemExit("Insufficient number of arguments")
|
|
|
|
method = getattr(self, "cmd_" + argv[1], self.no_command)
|
|
method(*argv[1:])
|
|
|
|
def cmd_rstinsert(self, cmd, *args):
|
|
from docutils.core import publish_cmdline, default_description
|
|
description = (
|
|
"Generates LaTeX portions to be included in documents " +
|
|
"from standalone reStructuredText sourtces " +
|
|
default_description)
|
|
|
|
publish_cmdline(writer=LaTeXCustomWriter(LaTeXInsertTranslator),
|
|
description=description,
|
|
argv=list(args)
|
|
)
|
|
|
|
def cmd_rst2latex(self, cmd, *args):
|
|
from docutils.core import publish_cmdline, default_description
|
|
description = (
|
|
'Generates LaTeX documents from RST sources. ' +
|
|
default_description)
|
|
|
|
publish_cmdline(writer=LaTeXCustomWriter(LaTeXStandaloneTranslator),
|
|
settings_overrides=LATEX_SETTINGS,
|
|
description=description,
|
|
argv=list(args),
|
|
)
|
|
|
|
def cmd_rst2ebook(self, cmd, *args):
|
|
from docutils.core import publish_cmdline, default_description
|
|
description = (
|
|
'Generates LaTeX documents from RST sources. ' +
|
|
default_description)
|
|
|
|
publish_cmdline(writer=LaTeXCustomWriter(LaTeXStandaloneTranslator),
|
|
settings_overrides=EBOOK_SETTINGS,
|
|
description=description,
|
|
argv=list(args),
|
|
)
|
|
|
|
def cmd_rst2html(self, cmd, *args):
|
|
from docutils.core import publish_cmdline, default_description
|
|
description = (
|
|
'Generates HTML documents from RST sources. ' +
|
|
default_description)
|
|
|
|
publish_cmdline(writer_name="html",
|
|
settings_overrides=HTML_SETTINGS,
|
|
description=description,
|
|
argv=list(args),
|
|
)
|
|
|
|
def cmd_toplevel(self, cmd, *args):
|
|
opts, args = optparser(OPT.docinfo).parse_args(list(args))
|
|
|
|
if opts.docinfo:
|
|
f = file(opts.docinfo, "rU")
|
|
e = ShellEscapeRunner(sys.stdout)
|
|
map(e.handle_line, f.readlines())
|
|
f.close()
|
|
print
|
|
|
|
print ".. contents::"
|
|
print
|
|
|
|
out = DupRefsRemover(sys.stdout)
|
|
out_xlate = UnderlineXlate(out)
|
|
|
|
args = list(args)
|
|
args.sort(str_tuple_sort)
|
|
|
|
for name in args:
|
|
if not name.endswith(".rst"):
|
|
continue
|
|
|
|
f = file(name, "rU")
|
|
map(is_index_filename(name) and out.handle_line or out_xlate.handle_line,
|
|
filter(lambda l: ".. contents::" not in l,
|
|
f.readlines())
|
|
)
|
|
f.close()
|
|
print
|
|
|
|
|
|
def cmd_htmlindex(self, cmd, *args):
|
|
opts, args = optparser(OPT.docinfo).parse_args(list(args))
|
|
|
|
if opts.docinfo:
|
|
f = file(opts.docinfo, "rU")
|
|
e = ShellEscapeRunner(sys.stdout)
|
|
map(e.handle_line, f.readlines())
|
|
f.close()
|
|
print
|
|
|
|
args = list(args)
|
|
args.sort(str_tuple_sort)
|
|
|
|
for name in args:
|
|
# Skip non-RST inputs
|
|
if not name.endswith(".rst") or not is_index_filename(name):
|
|
continue
|
|
|
|
f = file(name, "rU")
|
|
line = f.readline().strip()
|
|
f.close()
|
|
print "#. `%s <%s.html>`__" % (line, name[:-4])
|
|
|
|
|
|
def cmd_help(self, cmd, *args):
|
|
if args:
|
|
self.main([args[0], args[0], "--help"])
|
|
else:
|
|
print "Available commands:"
|
|
for k in dir(self):
|
|
if k.startswith("cmd_"):
|
|
print " ", k[4:].replace("_", "-")
|
|
|
|
cmd_commands = cmd_help
|
|
|
|
def no_command(self, cmd, *args):
|
|
raise SystemExit("No such command '%s'" % cmd)
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
DocTool().main()
|
|
|