365 lines
10 KiB
Python
Executable file
365 lines
10 KiB
Python
Executable file
#! /usr/bin/env python3
|
|
|
|
import sys, shlex, subprocess
|
|
from optparse import OptionParser, OptionGroup
|
|
from docutils.writers import latex2e
|
|
import re
|
|
from functools import reduce
|
|
|
|
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 = [x.strip() for x in 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",
|
|
"docutils_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
|
|
|
|
|
|
|
|
def docutils_bug_workaround(acc, x):
|
|
"""
|
|
|
|
Workarounds this bug: http://sourceforge.net/p/docutils/bugs/215/
|
|
"""
|
|
problematic_parameter = '--stylesheet-path'
|
|
if str.find(x, problematic_parameter) == -1:
|
|
acc.append(x)
|
|
else:
|
|
acc.append(problematic_parameter)
|
|
acc.append([x[str.find(x,'=') + 1:]])
|
|
return acc
|
|
|
|
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=reduce(docutils_bug_workaround, list(args), []),
|
|
)
|
|
|
|
def cmd_toplevel(self, cmd, *args):
|
|
opts, args = optparser(OPT.docinfo).parse_args(list(args))
|
|
|
|
if opts.docinfo:
|
|
f = open(opts.docinfo, "r")
|
|
e = ShellEscapeRunner(sys.stdout)
|
|
list(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 = open(name, "r")
|
|
list(map(is_index_filename(name) and out.handle_line or out_xlate.handle_line,
|
|
[l for l in f.readlines() if ".. contents::" not in l]
|
|
))
|
|
f.close()
|
|
print()
|
|
|
|
|
|
def cmd_htmlindex(self, cmd, *args):
|
|
opts, args = optparser(OPT.docinfo).parse_args(list(args))
|
|
|
|
if opts.docinfo:
|
|
f = open(opts.docinfo, "r")
|
|
e = ShellEscapeRunner(sys.stdout)
|
|
list(map(e.handle_line, f.readlines()))
|
|
f.close()
|
|
print()
|
|
|
|
args = list(args)
|
|
#args.sort(key=str_tuple_sort)
|
|
args.sort()
|
|
|
|
for name in args:
|
|
# Skip non-RST inputs
|
|
if not name.endswith(".rst") or not is_index_filename(name):
|
|
continue
|
|
|
|
f = open(name, "r")
|
|
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()
|
|
|