| 1 | #! /usr/bin/env python |
| 2 | |
| 3 | import re |
| 4 | import tempfile |
| 5 | import os, getopt, sys, shutil, random |
| 6 | from os.path import splitext, basename |
| 7 | |
| 8 | class TxtFile: |
| 9 | def __init__(self, title, author = None, style = None, includeToc = None): |
| 10 | self.title = title |
| 11 | self.author = author |
| 12 | self.buffer = "" |
| 13 | |
| 14 | def reset(self): |
| 15 | self.buffer = "" |
| 16 | |
| 17 | def __add__(self, aStr): |
| 18 | self.buffer += aStr |
| 19 | return self |
| 20 | |
| 21 | def tempFile(self): |
| 22 | return tempfile.NamedTemporaryFile('w+', -1, '.txt', 'pbook') |
| 23 | |
| 24 | def writeFile(self, fileName = None): |
| 25 | if not fileName: |
| 26 | file = self.tempFile() |
| 27 | elif fileName == "-": |
| 28 | file = sys.stdout |
| 29 | else: |
| 30 | name,ext = splitext(fileName) |
| 31 | if ext == "": |
| 32 | ext = ".txt" |
| 33 | elif ext != ".txt": |
| 34 | raise "Can not write format " + ext |
| 35 | file = open(name + ext, "w+") |
| 36 | file.write(self.buffer) |
| 37 | file.flush() |
| 38 | return file |
| 39 | |
| 40 | def addEscape(self, escape, fileName, line): |
| 41 | return |
| 42 | def addHeading(self, level, heading, fileName, line): |
| 43 | heading = re.sub("\s+", " ", heading) |
| 44 | self += "++ " + heading + "\n" |
| 45 | def addComment(self, comment, fileName, startLine, endLine): |
| 46 | self += comment |
| 47 | def addFigure(self, figureFile, figureScale, figureName, fileName, endLine): |
| 48 | self += " " + figureName + " (" + figureFile + ")" + "\n" |
| 49 | def addCode(self, code, fileName, startLine, endLine): |
| 50 | code = code.rstrip() |
| 51 | code = re.sub("\t", " ", code) |
| 52 | self += "\n== %s (%s:%s) ================\n" % (basename(fileName), startLine, endLine) |
| 53 | self += code |
| 54 | self += "\n=========================================================\n\n" |
| 55 | |
| 56 | class TexFile(TxtFile): |
| 57 | def __init__(self, title, author = None, style = "article", includeToc = True): |
| 58 | TxtFile.__init__(self, title, author, style, includeToc) |
| 59 | self.style = style |
| 60 | self.includeToc = includeToc |
| 61 | self.bookSectioningCommands = ("chapter", "section", \ |
| 62 | "subsection", "subsubsection") |
| 63 | self.articleSectioningCommands = ("section", "subsection", \ |
| 64 | "subsubsection") |
| 65 | |
| 66 | def beginning(self): |
| 67 | return '\n\\documentclass[notitlepage,a4paper,makeidx]{' + self.style + '}\n' + \ |
| 68 | '\\usepackage{fancyvrb,color,palatino,makeidx}\n' + \ |
| 69 | "\\usepackage{graphicx}\n" + \ |
| 70 | '\\definecolor{gray}{gray}{0.6}\n' + \ |
| 71 | '\\title{' + TexFile.escapeString(self.title) + '}\n' + \ |
| 72 | (self.author and ('\\author{' + self.author + '}\n') or '') + \ |
| 73 | '\\makeindex' + \ |
| 74 | '\\begin{document}\n\\maketitle\n' + \ |
| 75 | (self.includeToc and '\\tableofcontents\n' or '') |
| 76 | def ending(self): |
| 77 | return '\\printindex\n\\end{document}\n' |
| 78 | def sectioningCommand(self, level): |
| 79 | if self.style == "article": |
| 80 | return self.articleSectioningCommands[min(level, len(self.articleSectioningCommands))] |
| 81 | elif self.style == "book": |
| 82 | return self.bookSectioningCommands[min(level, len(self.bookSectioningCommands))] |
| 83 | |
| 84 | def escapeString(aStr): |
| 85 | aStr = re.sub("\\\\", "$\\\\backslash$", aStr) |
| 86 | def escapeRepl(match): |
| 87 | if match.group(1) != '$' or \ |
| 88 | not (aStr[match.start():].startswith("$\\backslash$") or \ |
| 89 | aStr[:match.start()].endswith("$\\backslash")): |
| 90 | return '\\' + match.group(1) |
| 91 | else: |
| 92 | return match.group(0) |
| 93 | return re.sub("([#%&~$_^{}])", escapeRepl, aStr) |
| 94 | escapeString = staticmethod(escapeString) |
| 95 | |
| 96 | def tempFile(self): |
| 97 | return tempfile.NamedTemporaryFile('w+', -1, '.tex', 'pbook') |
| 98 | def writeFile(self, fileName = None): |
| 99 | if not fileName: |
| 100 | file = self.tempFile() |
| 101 | elif fileName == "-": |
| 102 | return self.writeTex(sys.stdout) |
| 103 | else: |
| 104 | name,ext = splitext(fileName) |
| 105 | if ext == "": |
| 106 | ext = ".pdf" |
| 107 | file = open(name + ext, "w+") |
| 108 | name,ext = splitext(file.name) |
| 109 | if ext == ".tex": |
| 110 | return self.writeTex(file) |
| 111 | elif ext == ".pdf": |
| 112 | return self.writePdf(file) |
| 113 | else: |
| 114 | raise "Can not write format " + ext |
| 115 | |
| 116 | def writeTex(self, output): |
| 117 | output.write(self.beginning()) |
| 118 | output.write(self.buffer) |
| 119 | output.write(self.ending()) |
| 120 | output.flush() |
| 121 | return output |
| 122 | |
| 123 | def writePdf(self, output): |
| 124 | tmpfile = self.tempFile() |
| 125 | self.writeTex(tmpfile) |
| 126 | (dir, name) = os.path.split(tmpfile.name) |
| 127 | print "cd " + dir + "; latex " + name + " && pdflatex " + name |
| 128 | os.system("cd " + dir + "; latex " + name + " && pdflatex " + name) |
| 129 | tmpfile.close() |
| 130 | pdfname = splitext(tmpfile.name)[0] + ".pdf" |
| 131 | shutil.copyfile(pdfname, output.name) |
| 132 | os.remove(pdfname) |
| 133 | return output |
| 134 | |
| 135 | def addEscape(self, escape, fileName, line): |
| 136 | self += escape + "\n" |
| 137 | def addFigure(self, figureFile, figureScale, figureName, fileName, endLine): |
| 138 | self += "\\begin{figure}[htbp]\n \\centering\n" |
| 139 | self += " \\fbox{\\includegraphics[scale=" + figureScale + "]{" + figureFile + "}}\n" |
| 140 | self += " \\caption{" + figureName + "}\n" |
| 141 | self += "\\end{figure}\n" |
| 142 | def addHeading(self, level, heading, fileName, line): |
| 143 | heading = re.sub("\s+", " ", heading) |
| 144 | self += "\n\n\\" + self.sectioningCommand(level) + "{" + \ |
| 145 | TexFile.escapeString(heading) + "}\n" |
| 146 | def addComment(self, comment, fileName, startLine, endLine): |
| 147 | comment = TexFile.escapeString(comment) |
| 148 | comment = re.sub("`([^`']*)'", "{\\\\tt \\1}", comment) |
| 149 | self += re.sub("\"([^\"]*)\"", "``\\1''", comment) |
| 150 | def addCode(self, code, fileName, startLine, endLine): |
| 151 | code = code.rstrip() |
| 152 | code = re.sub("\\\\end{Verbatim}", "\\\\_end{Verbatim}", code) |
| 153 | code = re.sub("\t", " ", code) |
| 154 | self += "\n\\begin{Verbatim}[fontsize=\\small,frame=leftline,framerule=0.9mm," + \ |
| 155 | "rulecolor=\\color{gray},framesep=5.1mm,xleftmargin=5mm,fontfamily=cmtt]\n" |
| 156 | self += code |
| 157 | self += "\n\\end{Verbatim}\n" |
| 158 | |
| 159 | class IdqTexFile(TexFile): |
| 160 | def __init__(self, title, author = "id Quantique", style = "article", includeToc = True): |
| 161 | TexFile.__init__(self, title, author, style, includeToc) |
| 162 | |
| 163 | class BknrTexFile(TexFile): |
| 164 | def __init__(self, title, author, style, includeToc): |
| 165 | TexFile.__init__(self, title, author, style, includeToc) |
| 166 | self.firstSection = True |
| 167 | def beginning(self): |
| 168 | return '\\chapter{' + TexFile.escapeString(self.title) + '}\n' |
| 169 | def ending(self): |
| 170 | return '' |
| 171 | def addComment(self, comment, fileName, startLine, endLine): |
| 172 | comment = TexFile.escapeString(comment) |
| 173 | self += re.sub("\"([^\"]*)\"", "``\\1''", comment) |
| 174 | def sectioningCommand(self, level): |
| 175 | string = "" |
| 176 | if level == 0: |
| 177 | if self.firstSection == False: |
| 178 | string = "" |
| 179 | # string = "vbox{\n\\vspace{1cm}\n\\centering\n" + \ |
| 180 | # "\\includegraphics[scale=0.6]{peecol" + \ |
| 181 | # str(random.randint(1, 10) ) + "}}\n\n\\" |
| 182 | else: |
| 183 | self.firstSection = False |
| 184 | if self.style == "article": |
| 185 | return string + self.articleSectioningCommands[min(level, len(self.articleSectioningCommands))] |
| 186 | elif self.style == "book": |
| 187 | return string + self.bookSectioningCommands[min(level, len(self.bookSectioningCommands))] |
| 188 | |
| 189 | class Pbook: |
| 190 | def __init__(self, files, outFile): |
| 191 | self.files = files |
| 192 | self.commentRe = None |
| 193 | self.headingRe = None |
| 194 | self.removeRe = None |
| 195 | self.outFile = outFile |
| 196 | self.lineCounter = 0 |
| 197 | if not self.outFile.title: self.outFile.title = basename(file) |
| 198 | |
| 199 | def formatBuffer(self): |
| 200 | self.outFile.reset() |
| 201 | self.lineCounter = 0 |
| 202 | for file in self.files: |
| 203 | data = open(file, "r").read() |
| 204 | if self.removeRe: |
| 205 | data = self.removeRe.sub("", data) |
| 206 | # search the first heading |
| 207 | startMatch = self.headingRe.search(data) |
| 208 | if not startMatch: |
| 209 | raise "File must have at least one heading" |
| 210 | self.lineCounter += len(data[:startMatch.start()].split('\n')) |
| 211 | data = data[startMatch.start():] |
| 212 | self.fileName = file |
| 213 | |
| 214 | lines = data.split('\n') |
| 215 | while len(lines) > 0: |
| 216 | line = lines[0] |
| 217 | if re.match("^\s*$", line): |
| 218 | lines.pop(0) |
| 219 | self.lineCounter += 1 |
| 220 | continue |
| 221 | elif self.figureRe.match(line): |
| 222 | line = lines.pop(0) |
| 223 | self.doFigure(line) |
| 224 | self.lineCounter += 1 |
| 225 | elif self.escapeRe.match(line): |
| 226 | line = lines.pop(0) |
| 227 | self.doEscape(line) |
| 228 | self.lineCounter += 1 |
| 229 | elif self.headingRe.match(line): |
| 230 | line = lines.pop(0) |
| 231 | self.doHeading(line) |
| 232 | self.lineCounter += 1 |
| 233 | elif self.commentRe.match(line): |
| 234 | self.doComment(lines) |
| 235 | else: |
| 236 | self.doCode(lines) |
| 237 | |
| 238 | def doHeading(self, line): |
| 239 | match = self.headingRe.match(line) |
| 240 | assert(match != None) |
| 241 | level = len(match.group(1)) - 1 |
| 242 | headingName = line[match.end():] |
| 243 | self.outFile.addHeading(level, headingName, self.fileName, self.lineCounter) |
| 244 | |
| 245 | def doFigure(self, line): |
| 246 | match = self.figureRe.match(line) |
| 247 | assert(match != None) |
| 248 | figureFile = match.group(1) |
| 249 | figureName = match.group(3) |
| 250 | figureScale = match.group(2) |
| 251 | self.outFile.addFigure(figureFile, figureScale, figureName, self.fileName, self.lineCounter) |
| 252 | |
| 253 | def doEscape(self, line): |
| 254 | match = self.escapeRe.match(line) |
| 255 | assert(match != None) |
| 256 | escape = match.group(1) |
| 257 | self.outFile.addEscape(escape, self.fileName, self.lineCounter) |
| 258 | |
| 259 | def doComment(self, lines): |
| 260 | comment = "" |
| 261 | lineCount = 0 |
| 262 | while len(lines) > 0: |
| 263 | line = lines[0] |
| 264 | match = self.commentRe.match(line) |
| 265 | if not match: break |
| 266 | line = lines.pop(0) |
| 267 | lineCount += 1 |
| 268 | comment += line[:match.start()] + line[match.end():] + "\n" |
| 269 | self.outFile.addComment(comment, self.fileName, self.lineCounter, self.lineCounter + lineCount) |
| 270 | self.lineCounter += lineCount |
| 271 | |
| 272 | def doCode(self, lines): |
| 273 | lineCount = 0 |
| 274 | code = "" |
| 275 | while len(lines) > 0: |
| 276 | line = lines[0] |
| 277 | if (self.headingRe.match(line) or self.escapeRe.match(line) \ |
| 278 | or self.figureRe.match(line) \ |
| 279 | or self.commentRe.match(line)): |
| 280 | break |
| 281 | line = lines.pop(0) |
| 282 | lineCount += 1 |
| 283 | code += line + "\n" |
| 284 | self.outFile.addCode(code, self.fileName, self.lineCounter, self.lineCounter + lineCount) |
| 285 | self.lineCounter += lineCount |
| 286 | |
| 287 | def makeFile(self, fileName): |
| 288 | self.outFile.reset() |
| 289 | self.formatBuffer() |
| 290 | return self.outFile.writeFile(fileName) |
| 291 | |
| 292 | class LispPbook(Pbook): |
| 293 | def __init__(self, files, outFile): |
| 294 | Pbook.__init__(self, files, outFile) |
| 295 | self.commentRe = re.compile('^;;;($|[^#f])', re.M) |
| 296 | self.headingRe = re.compile('^;;;(#+)', re.M) |
| 297 | self.figureRe = re.compile('^;;;f\s+\"(.+)\"\s+([^\s]+)\s+(.*)', re.M) |
| 298 | self.escapeRe = re.compile('^;;;t\s+(.+)', re.M) |
| 299 | |
| 300 | class CPbook(Pbook): |
| 301 | def __init__(self, files, outFile): |
| 302 | Pbook.__init__(self, files, outFile) |
| 303 | self.commentRe = re.compile('^(\s|/)*\*\*($|[^f#/])', re.M); |
| 304 | self.headingRe = re.compile("^/\*\*(#+)", re.M) |
| 305 | self.removeRe = re.compile('\*\*+/', re.M) |
| 306 | self.figureRe = re.compile('^/\*\*f \"(.+)\"\s+([^\s]+)\s(.*)', re.M) |
| 307 | |
| 308 | |
| 309 | def usage(): |
| 310 | print "Usage: ", sys.argv[0], " [-h] [-c TexFile|BknrTexFile|IdqTexFile|TxtFile] ", \ |
| 311 | "[-T C|Lisp] [-t title] [-a author] [-O] [-o output] [-s style] file ..." |
| 312 | |
| 313 | def extToType(ext): |
| 314 | fileExtToType = ( ((".c", ".cpp", ".C", ".h"), CPbook), |
| 315 | ((".lisp", ".el", ".l", ".cl"), LispPbook) ) |
| 316 | for types, typeClass in fileExtToType: |
| 317 | if (ext in types): |
| 318 | return typeClass |
| 319 | return None |
| 320 | |
| 321 | def main(): |
| 322 | texClass = TexFile |
| 323 | type = None |
| 324 | output = None |
| 325 | (author, title, toc, style) = (None, None, True, "article") |
| 326 | try: |
| 327 | opts, args = getopt.getopt(sys.argv[1:], "hc:T:t:a:Oo:s:") |
| 328 | if not args: |
| 329 | raise getopt.error, "At least one file argument required" |
| 330 | for optname, optvalue in opts: |
| 331 | if optname == "-h": |
| 332 | usage() |
| 333 | return |
| 334 | elif optname == "-c": |
| 335 | if optvalue == "TexFile": |
| 336 | texClass = TexFile |
| 337 | elif optvalue == "IdqTexFile": |
| 338 | texClass = IdqTexFile |
| 339 | elif optvalue == "BknrTexFile": |
| 340 | texClass = BknrTexFile |
| 341 | elif optvalue == "TxtFile": |
| 342 | texClass = TxtFile |
| 343 | else: |
| 344 | raise getopt.error, "Unknown TexFile class ", optvalue |
| 345 | elif optname == "-t": |
| 346 | title = optvalue |
| 347 | elif optname == "-a": |
| 348 | author = optvalue |
| 349 | elif optname == "-O": |
| 350 | toc = False |
| 351 | elif optname == "-s": |
| 352 | style = optvalue |
| 353 | elif optname == "-T": |
| 354 | if optvalue == "C": |
| 355 | type = CPbook |
| 356 | elif optvalue == "Lisp": |
| 357 | type = LispPbook |
| 358 | else: |
| 359 | raise getopt.error, "Unknown pbook file type ", optvalue |
| 360 | elif optname == "-o": |
| 361 | output = optvalue |
| 362 | except getopt.error, msg: |
| 363 | print msg |
| 364 | usage() |
| 365 | return 1 |
| 366 | file = args[0] |
| 367 | name,ext = splitext(file) |
| 368 | if not title: |
| 369 | title = basename(name) |
| 370 | if not type: |
| 371 | type = extToType(ext) |
| 372 | if not type: |
| 373 | print "Could not get type for ", ext |
| 374 | return 2 |
| 375 | pbook = type(args, texClass(title, author, style, toc)) |
| 376 | if not output: |
| 377 | output = basename(name) |
| 378 | try: |
| 379 | file = pbook.makeFile(output) |
| 380 | if output != "-": |
| 381 | print "Wrote output to ", file.name |
| 382 | except IOError, (errno, strerror): |
| 383 | print "Caught an error while generating \"%s\": %s" % (output, strerror) |
| 384 | except: |
| 385 | print "Caught an error while generating \"%s\"" % (output) |
| 386 | return 1 |
| 387 | |
| 388 | if __name__ == "__main__": |
| 389 | sys.exit(main()) |