TASKPM/doc/tools/doctool
2025-03-19 16:02:47 +01:00

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()