mklatex.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. # The script builds the LaTeX documentation.
  2. # Testing:
  3. # python mklatex.py latex .. 1.0
  4. from docstructure import SITE_STRUCTURE, BASENAME_MAP
  5. import os, shutil, re, sys, datetime
  6. TARGET_FILE = "lxmldoc.tex"
  7. RST2LATEX_OPTIONS = " ".join([
  8. # "--no-toc-backlinks",
  9. "--strip-comments",
  10. "--language en",
  11. # "--date",
  12. # "--use-latex-footnotes",
  13. "--use-latex-citations",
  14. "--use-latex-toc",
  15. "--font-encoding=T1",
  16. "--output-encoding=utf-8",
  17. "--input-encoding=utf-8",
  18. "--graphicx-option=pdftex",
  19. ])
  20. htmlnsmap = {"h" : "http://www.w3.org/1999/xhtml"}
  21. replace_invalid = re.compile(r'[-_/.\s\\]').sub
  22. replace_content = re.compile("\{[^\}]*\}").sub
  23. replace_epydoc_macros = re.compile(r'(,\s*amssymb|dvips\s*,\s*)').sub
  24. replace_rst_macros = re.compile(r'(\\usepackage\{color}|\\usepackage\[[^]]*]\{hyperref})').sub
  25. BASENAME_MAP = BASENAME_MAP.copy()
  26. BASENAME_MAP.update({'api' : 'lxmlapi'})
  27. # LaTeX snippets
  28. DOCUMENT_CLASS = r"""
  29. \documentclass[10pt,english]{report}
  30. \usepackage[a4paper]{geometry}
  31. \usepackage{tabularx}
  32. \usepackage{ifthen}
  33. \usepackage[pdftex]{graphicx}
  34. \parindent0pt
  35. \parskip1ex
  36. %%% Fallback definitions for Docutils-specific commands
  37. % providelength (provide a length variable and set default, if it is new)
  38. \providecommand*{\DUprovidelength}[2]{
  39. \ifthenelse{\isundefined{#1}}{\newlength{#1}\setlength{#1}{#2}}{}
  40. }
  41. % docinfo (width of docinfo table)
  42. \DUprovidelength{\DUdocinfowidth}{0.9\textwidth}
  43. % titlereference role
  44. \providecommand*{\DUroletitlereference}[1]{\textsl{#1}}
  45. """
  46. PYGMENTS_IMPORT = r"""
  47. \usepackage{fancyvrb}
  48. \input{_part_pygments.tex}
  49. """
  50. EPYDOC_IMPORT = r"""
  51. \input{_part_epydoc.tex}
  52. """
  53. def write_chapter(master, title, filename):
  54. filename = os.path.join(os.path.dirname(filename),
  55. "_part_%s" % os.path.basename(filename))
  56. master.write(r"""
  57. \chapter{%s}
  58. \label{%s}
  59. \input{%s}
  60. """ % (title, filename, filename))
  61. # the program ----
  62. def rest2latex(script, source_path, dest_path):
  63. command = ('%s %s %s %s > %s' %
  64. (sys.executable, script, RST2LATEX_OPTIONS,
  65. source_path, dest_path))
  66. os.system(command)
  67. def build_pygments_macros(filename):
  68. from pygments.formatters import LatexFormatter
  69. text = LatexFormatter().get_style_defs()
  70. with open(filename, "w") as f:
  71. f.write(text)
  72. f.write('\n')
  73. def copy_epydoc_macros(src, dest, existing_header_lines):
  74. doc = open(src, 'r')
  75. out = open(dest, "w")
  76. for line in doc:
  77. if line.startswith('%% generator') \
  78. or line.startswith('% generated by ') \
  79. or '\\begin{document}' in line \
  80. or '\\makeindex' in line:
  81. break
  82. if line.startswith('%') or \
  83. r'\documentclass' in line or \
  84. r'\makeindex' in line or \
  85. r'{inputenc}' in line:
  86. continue
  87. if line.startswith(r'\usepackage'):
  88. if line in existing_header_lines:
  89. continue
  90. if '{hyperref}' in line:
  91. line = line.replace('black', 'blue')
  92. out.write( replace_epydoc_macros('', line) )
  93. out.close()
  94. doc.close()
  95. def noop(input):
  96. return input
  97. counter_no = 0
  98. def tex_postprocess(src_path, dest_path, want_header=False, process_line=noop):
  99. """
  100. Postprocessing of the LaTeX file generated from ReST.
  101. Reads file src_path and saves to dest_path only the true content
  102. (without the document header and final) - so it is suitable
  103. to be used as part of the longer document.
  104. Returns the title of document
  105. If want_header is set, returns also the document header (as
  106. the list of lines).
  107. """
  108. header = []
  109. add_header_line = header.append
  110. global counter_no
  111. counter_no = counter_no + 1
  112. counter_text = "listcnt%d" % counter_no
  113. search_title = re.compile(r'\\title{([^{}]*(?:{[^}]*})*)}').search
  114. skipping = re.compile(r'(\\end{document}|\\tableofcontents|^%)').search
  115. with open(src_path) as src:
  116. src_text = src.read()
  117. dest = open(dest_path, "w")
  118. title = search_title(src_text)
  119. if title:
  120. # remove any commands from the title
  121. title = re.sub(r'\\\w+({[^}]*})?', '', title.group(1))
  122. iter_lines = iter(src_text.splitlines())
  123. for l in iter_lines:
  124. l = process_line(l)
  125. if not l:
  126. continue
  127. if want_header:
  128. add_header_line(replace_rst_macros('', l))
  129. if l.startswith("\\maketitle"):
  130. break
  131. for l in iter_lines:
  132. l = process_line(l)
  133. if skipping(l):
  134. # To-Do minitoc instead of tableofcontents
  135. continue
  136. elif "\hypertarget{old-versions}" in l:
  137. break
  138. elif "listcnt0" in l:
  139. l = l.replace("listcnt0", counter_text)
  140. dest.write(l + '\n')
  141. dest.close()
  142. if not title:
  143. raise Exception("Bueee, no title in %s" % src_path)
  144. return title, header
  145. def publish(dirname, lxml_path, release):
  146. if not os.path.exists(dirname):
  147. os.mkdir(dirname)
  148. book_title = "lxml %s" % release
  149. doc_dir = os.path.join(lxml_path, 'doc')
  150. script = os.path.join(doc_dir, 'rest2latex.py')
  151. pubkey = os.path.join(doc_dir, 'pubkey.asc')
  152. shutil.copy(pubkey, dirname)
  153. # build pygments macros
  154. build_pygments_macros(os.path.join(dirname, '_part_pygments.tex'))
  155. # Used in postprocessing of generated LaTeX files
  156. header = []
  157. titles = {}
  158. replace_interdoc_hyperrefs = re.compile(
  159. r'\\href\{([^/}]+)[.]([^./}]+)\}').sub
  160. replace_docinternal_hyperrefs = re.compile(
  161. r'\\href\{\\#([^}]+)\}').sub
  162. replace_image_paths = re.compile(
  163. r'^(\\includegraphics{)').sub
  164. def build_hyperref(match):
  165. basename, extension = match.groups()
  166. outname = BASENAME_MAP.get(basename, basename)
  167. if '#' in extension:
  168. anchor = extension.split('#')[-1]
  169. return r"\hyperref[%s]" % anchor
  170. elif extension != 'html':
  171. return r'\href{https://lxml.de/%s.%s}' % (
  172. outname, extension)
  173. else:
  174. return r"\hyperref[_part_%s.tex]" % outname
  175. def fix_relative_hyperrefs(line):
  176. line = replace_image_paths(r'\1../html/', line)
  177. if r'\href' not in line:
  178. return line
  179. line = replace_interdoc_hyperrefs(build_hyperref, line)
  180. return replace_docinternal_hyperrefs(r'\\hyperref[\1]', line)
  181. # Building pages
  182. for section, text_files in SITE_STRUCTURE:
  183. for filename in text_files:
  184. if filename.startswith('@'):
  185. continue
  186. #page_title = filename[1:]
  187. #url = href_map[page_title]
  188. #build_menu_entry(page_title, url, section_head)
  189. basename = os.path.splitext(os.path.basename(filename))[0]
  190. basename = BASENAME_MAP.get(basename, basename)
  191. outname = basename + '.tex'
  192. outpath = os.path.join(dirname, outname)
  193. path = os.path.join(doc_dir, filename)
  194. print("Creating %s" % outname)
  195. rest2latex(script, path, outpath)
  196. final_name = os.path.join(dirname, os.path.dirname(outname),
  197. "_part_%s" % os.path.basename(outname))
  198. title, hd = tex_postprocess(outpath, final_name,
  199. want_header = not header,
  200. process_line=fix_relative_hyperrefs)
  201. if not header:
  202. header = hd
  203. titles[outname] = title
  204. # integrate generated API docs
  205. print("Integrating API docs")
  206. apidocsname = 'api.tex'
  207. apipath = os.path.join(dirname, apidocsname)
  208. tex_postprocess(apipath, os.path.join(dirname, "_part_%s" % apidocsname),
  209. process_line=fix_relative_hyperrefs)
  210. copy_epydoc_macros(apipath, os.path.join(dirname, '_part_epydoc.tex'),
  211. set(header))
  212. # convert CHANGES.txt
  213. print("Integrating ChangeLog")
  214. find_version_title = re.compile(
  215. r'(.*\\section\{)([0-9][^\} ]*)\s+\(([^)]+)\)(\}.*)').search
  216. def fix_changelog(line):
  217. m = find_version_title(line)
  218. if m:
  219. line = "%sChanges in version %s, released %s%s" % m.groups()
  220. else:
  221. line = line.replace(r'\subsection{', r'\subsection*{')
  222. return line
  223. chgname = 'changes-%s.tex' % release
  224. chgpath = os.path.join(dirname, chgname)
  225. rest2latex(script,
  226. os.path.join(lxml_path, 'CHANGES.txt'),
  227. chgpath)
  228. tex_postprocess(chgpath, os.path.join(dirname, "_part_%s" % chgname),
  229. process_line=fix_changelog)
  230. # Writing a master file
  231. print("Building %s\n" % TARGET_FILE)
  232. master = open( os.path.join(dirname, TARGET_FILE), "w")
  233. for hln in header:
  234. if hln.startswith(r"\documentclass"):
  235. #hln = hln.replace('article', 'book')
  236. hln = DOCUMENT_CLASS + EPYDOC_IMPORT
  237. elif hln.startswith(r"\begin{document}"):
  238. # pygments and epydoc support
  239. master.write(PYGMENTS_IMPORT)
  240. elif hln.startswith(r"\title{"):
  241. hln = replace_content(
  242. r'{%s\\\\\\vspace{1cm}\\includegraphics[width=2.5cm]{../html/tagpython-big.png}}' % book_title, hln)
  243. elif hln.startswith(r"\date{"):
  244. hln = replace_content(
  245. r'{%s}' % datetime.date.today().isoformat(), hln)
  246. elif hln.startswith("pdftitle"):
  247. hln = replace_content(
  248. r'{%s}' % book_title, hln)
  249. master.write(hln + '\n')
  250. master.write("\\setcounter{page}{2}\n")
  251. master.write("\\tableofcontents\n")
  252. for section, text_files in SITE_STRUCTURE:
  253. master.write("\n\n\\part{%s}\n" % section)
  254. for filename in text_files:
  255. if filename.startswith('@'):
  256. continue
  257. #print "Not yet implemented: %s" % filename[1:]
  258. #page_title = filename[1:]
  259. #url = href_map[page_title]
  260. #build_menu_entry(page_title, url, section_head)
  261. else:
  262. basename = os.path.splitext(os.path.basename(filename))[0]
  263. basename = BASENAME_MAP.get(basename, basename)
  264. outname = basename + '.tex'
  265. write_chapter(master, titles[outname], outname)
  266. master.write("\\appendix\n")
  267. master.write("\\begin{appendix}\n")
  268. write_chapter(master, "Changes", chgname)
  269. write_chapter(master, "Generated API documentation", apidocsname)
  270. master.write("\\end{appendix}\n")
  271. master.write("\\end{document}\n")
  272. if __name__ == '__main__':
  273. publish(sys.argv[1], sys.argv[2], sys.argv[3])