| OLD | NEW |
| (Empty) |
| 1 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
| 2 # See LICENSE for details. | |
| 3 | |
| 4 | |
| 5 import re, os, cStringIO, time, cgi, string, urlparse | |
| 6 from twisted import copyright | |
| 7 from twisted.python import htmlizer, text | |
| 8 from twisted.web import microdom, domhelpers | |
| 9 import process, latex, indexer, numberer, htmlbook | |
| 10 from twisted.python.util import InsensitiveDict | |
| 11 | |
| 12 # relative links to html files | |
| 13 def fixLinks(document, ext): | |
| 14 """ | |
| 15 Rewrite links to XHTML lore input documents so they point to lore XHTML | |
| 16 output documents. | |
| 17 | |
| 18 Any node with an C{href} attribute which does not contain a value starting | |
| 19 with C{http}, C{https}, C{ftp}, or C{mailto} and which does not have a | |
| 20 C{class} attribute of C{absolute} or which contains C{listing} and which | |
| 21 does point to an URL ending with C{html} will have that attribute value | |
| 22 rewritten so that the filename extension is C{ext} instead of C{html}. | |
| 23 | |
| 24 @type document: A DOM Node or Document | |
| 25 @param document: The input document which contains all of the content to be | |
| 26 presented. | |
| 27 | |
| 28 @type ext: C{str} | |
| 29 @param ext: The extension to use when selecting an output file name. This | |
| 30 replaces the extension of the input file name. | |
| 31 | |
| 32 @return: C{None} | |
| 33 """ | |
| 34 supported_schemes=['http', 'https', 'ftp', 'mailto'] | |
| 35 for node in domhelpers.findElementsWithAttribute(document, 'href'): | |
| 36 href = node.getAttribute("href") | |
| 37 if urlparse.urlparse(href)[0] in supported_schemes: | |
| 38 continue | |
| 39 if node.getAttribute("class", "") == "absolute": | |
| 40 continue | |
| 41 if node.getAttribute("class", "").find('listing') != -1: | |
| 42 continue | |
| 43 | |
| 44 # This is a relative link, so it should be munged. | |
| 45 if href.endswith('html') or href[:href.rfind('#')].endswith('html'): | |
| 46 fname, fext = os.path.splitext(href) | |
| 47 if '#' in fext: | |
| 48 fext = ext+'#'+fext.split('#', 1)[1] | |
| 49 else: | |
| 50 fext = ext | |
| 51 node.setAttribute("href", fname + fext) | |
| 52 | |
| 53 | |
| 54 | |
| 55 def addMtime(document, fullpath): | |
| 56 """ | |
| 57 Set the last modified time of the given document. | |
| 58 | |
| 59 @type document: A DOM Node or Document | |
| 60 @param document: The output template which defines the presentation of the | |
| 61 last modified time. | |
| 62 | |
| 63 @type fullpath: C{str} | |
| 64 @param fullpath: The file name from which to take the last modified time. | |
| 65 | |
| 66 @return: C{None} | |
| 67 """ | |
| 68 for node in domhelpers.findElementsWithAttribute(document, "class","mtime"): | |
| 69 node.appendChild(microdom.Text(time.ctime(os.path.getmtime(fullpath)))) | |
| 70 | |
| 71 | |
| 72 | |
| 73 def _getAPI(node): | |
| 74 """ | |
| 75 Retrieve the fully qualified Python name represented by the given node. | |
| 76 | |
| 77 The name is represented by one or two aspects of the node: the value of the | |
| 78 node's first child forms the end of the name. If the node has a C{base} | |
| 79 attribute, that attribute's value is prepended to the node's value, with | |
| 80 C{.} separating the two parts. | |
| 81 | |
| 82 @rtype: C{str} | |
| 83 @return: The fully qualified Python name. | |
| 84 """ | |
| 85 base = "" | |
| 86 if node.hasAttribute("base"): | |
| 87 base = node.getAttribute("base") + "." | |
| 88 return base+node.childNodes[0].nodeValue | |
| 89 | |
| 90 | |
| 91 | |
| 92 def fixAPI(document, url): | |
| 93 """ | |
| 94 Replace API references with links to API documentation. | |
| 95 | |
| 96 @type document: A DOM Node or Document | |
| 97 @param document: The input document which contains all of the content to be | |
| 98 presented. | |
| 99 | |
| 100 @type url: C{str} | |
| 101 @param url: A string which will be interpolated with the fully qualified | |
| 102 Python name of any API reference encountered in the input document, the | |
| 103 result of which will be used as a link to API documentation for that name | |
| 104 in the output document. | |
| 105 | |
| 106 @return: C{None} | |
| 107 """ | |
| 108 # API references | |
| 109 for node in domhelpers.findElementsWithAttribute(document, "class", "API"): | |
| 110 fullname = _getAPI(node) | |
| 111 node2 = microdom.Element('a', {'href': url%fullname, 'title': fullname}) | |
| 112 node2.childNodes = node.childNodes | |
| 113 node.childNodes = [node2] | |
| 114 node.removeAttribute('base') | |
| 115 | |
| 116 | |
| 117 | |
| 118 def fontifyPython(document): | |
| 119 """ | |
| 120 Syntax color any node in the given document which contains a Python source | |
| 121 listing. | |
| 122 | |
| 123 @type document: A DOM Node or Document | |
| 124 @param document: The input document which contains all of the content to be | |
| 125 presented. | |
| 126 | |
| 127 @return: C{None} | |
| 128 """ | |
| 129 def matcher(node): | |
| 130 return (node.nodeName == 'pre' and node.hasAttribute('class') and | |
| 131 node.getAttribute('class') == 'python') | |
| 132 for node in domhelpers.findElements(document, matcher): | |
| 133 fontifyPythonNode(node) | |
| 134 | |
| 135 | |
| 136 | |
| 137 def fontifyPythonNode(node): | |
| 138 """ | |
| 139 Syntax color the given node containing Python source code. | |
| 140 | |
| 141 @return: C{None} | |
| 142 """ | |
| 143 oldio = cStringIO.StringIO() | |
| 144 latex.getLatexText(node, oldio.write, | |
| 145 entities={'lt': '<', 'gt': '>', 'amp': '&'}) | |
| 146 oldio = cStringIO.StringIO(oldio.getvalue().strip()+'\n') | |
| 147 newio = cStringIO.StringIO() | |
| 148 htmlizer.filter(oldio, newio, writer=htmlizer.SmallerHTMLWriter) | |
| 149 newio.seek(0) | |
| 150 newel = microdom.parse(newio).documentElement | |
| 151 newel.setAttribute("class", "python") | |
| 152 node.parentNode.replaceChild(newel, node) | |
| 153 | |
| 154 | |
| 155 | |
| 156 def addPyListings(document, dir): | |
| 157 """ | |
| 158 Insert Python source listings into the given document from files in the | |
| 159 given directory based on C{py-listing} nodes. | |
| 160 | |
| 161 Any node in C{document} with a C{class} attribute set to C{py-listing} will | |
| 162 have source lines taken from the file named in that node's C{href} | |
| 163 attribute (searched for in C{dir}) inserted in place of that node. | |
| 164 | |
| 165 If a node has a C{skipLines} attribute, its value will be parsed as an | |
| 166 integer and that many lines will be skipped at the beginning of the source | |
| 167 file. | |
| 168 | |
| 169 @type document: A DOM Node or Document | |
| 170 @param document: The document within which to make listing replacements. | |
| 171 | |
| 172 @type dir: C{str} | |
| 173 @param dir: The directory in which to find source files containing the | |
| 174 referenced Python listings. | |
| 175 | |
| 176 @return: C{None} | |
| 177 """ | |
| 178 for node in domhelpers.findElementsWithAttribute(document, "class", | |
| 179 "py-listing"): | |
| 180 filename = node.getAttribute("href") | |
| 181 outfile = cStringIO.StringIO() | |
| 182 lines = map(string.rstrip, open(os.path.join(dir, filename)).readlines()
) | |
| 183 data = '\n'.join(lines[int(node.getAttribute('skipLines', 0)):]) | |
| 184 data = cStringIO.StringIO(text.removeLeadingTrailingBlanks(data)) | |
| 185 htmlizer.filter(data, outfile, writer=htmlizer.SmallerHTMLWriter) | |
| 186 val = outfile.getvalue() | |
| 187 _replaceWithListing(node, val, filename, "py-listing") | |
| 188 | |
| 189 | |
| 190 | |
| 191 def _replaceWithListing(node, val, filename, class_): | |
| 192 captionTitle = domhelpers.getNodeText(node) | |
| 193 if captionTitle == os.path.basename(filename): | |
| 194 captionTitle = 'Source listing' | |
| 195 text = ('<div class="%s">%s<div class="caption">%s - ' | |
| 196 '<a href="%s"><span class="filename">%s</span></a></div></div>' % | |
| 197 (class_, val, captionTitle, filename, filename)) | |
| 198 newnode = microdom.parseString(text).documentElement | |
| 199 node.parentNode.replaceChild(newnode, node) | |
| 200 | |
| 201 | |
| 202 | |
| 203 def addHTMLListings(document, dir): | |
| 204 """ | |
| 205 Insert HTML source listings into the given document from files in the given | |
| 206 directory based on C{html-listing} nodes. | |
| 207 | |
| 208 Any node in C{document} with a C{class} attribute set to C{html-listing} | |
| 209 will have source lines taken from the file named in that node's C{href} | |
| 210 attribute (searched for in C{dir}) inserted in place of that node. | |
| 211 | |
| 212 @type document: A DOM Node or Document | |
| 213 @param document: The document within which to make listing replacements. | |
| 214 | |
| 215 @type dir: C{str} | |
| 216 @param dir: The directory in which to find source files containing the | |
| 217 referenced HTML listings. | |
| 218 | |
| 219 @return: C{None} | |
| 220 """ | |
| 221 for node in domhelpers.findElementsWithAttribute(document, "class", | |
| 222 "html-listing"): | |
| 223 filename = node.getAttribute("href") | |
| 224 val = ('<pre class="htmlsource">\n%s</pre>' % | |
| 225 cgi.escape(open(os.path.join(dir, filename)).read())) | |
| 226 _replaceWithListing(node, val, filename, "html-listing") | |
| 227 | |
| 228 | |
| 229 | |
| 230 def addPlainListings(document, dir): | |
| 231 """ | |
| 232 Insert text listings into the given document from files in the given | |
| 233 directory based on C{listing} nodes. | |
| 234 | |
| 235 Any node in C{document} with a C{class} attribute set to C{listing} will | |
| 236 have source lines taken from the file named in that node's C{href} | |
| 237 attribute (searched for in C{dir}) inserted in place of that node. | |
| 238 | |
| 239 @type document: A DOM Node or Document | |
| 240 @param document: The document within which to make listing replacements. | |
| 241 | |
| 242 @type dir: C{str} | |
| 243 @param dir: The directory in which to find source files containing the | |
| 244 referenced text listings. | |
| 245 | |
| 246 @return: C{None} | |
| 247 """ | |
| 248 for node in domhelpers.findElementsWithAttribute(document, "class", | |
| 249 "listing"): | |
| 250 filename = node.getAttribute("href") | |
| 251 val = ('<pre>\n%s</pre>' % | |
| 252 cgi.escape(open(os.path.join(dir, filename)).read())) | |
| 253 _replaceWithListing(node, val, filename, "listing") | |
| 254 | |
| 255 | |
| 256 | |
| 257 def getHeaders(document): | |
| 258 """ | |
| 259 Return all H2 and H3 nodes in the given document. | |
| 260 | |
| 261 @type document: A DOM Node or Document | |
| 262 | |
| 263 @rtype: C{list} | |
| 264 """ | |
| 265 return domhelpers.findElements( | |
| 266 document, | |
| 267 lambda n, m=re.compile('h[23]$').match: m(n.nodeName)) | |
| 268 | |
| 269 | |
| 270 | |
| 271 def generateToC(document): | |
| 272 """ | |
| 273 Create a table of contents for the given document. | |
| 274 | |
| 275 @type document: A DOM Node or Document | |
| 276 | |
| 277 @rtype: A DOM Node | |
| 278 @return: a Node containing a table of contents based on the headers of the | |
| 279 given document. | |
| 280 """ | |
| 281 toc, level, id = '\n<ol>\n', 0, 0 | |
| 282 for element in getHeaders(document): | |
| 283 elementLevel = int(element.tagName[1])-2 | |
| 284 toc += (level-elementLevel)*'</ul>\n' | |
| 285 toc += (elementLevel-level)*'<ul>' | |
| 286 toc += '<li><a href="#auto%d">' % id | |
| 287 toc += domhelpers.getNodeText(element) | |
| 288 toc += '</a></li>\n' | |
| 289 level = elementLevel | |
| 290 anchor = microdom.parseString('<a name="auto%d" />' % id).documentElemen
t | |
| 291 element.childNodes.append(anchor) | |
| 292 id += 1 | |
| 293 toc += '</ul>\n' * level | |
| 294 toc += '</ol>\n' | |
| 295 return microdom.parseString(toc).documentElement | |
| 296 | |
| 297 | |
| 298 | |
| 299 def putInToC(document, toc): | |
| 300 """ | |
| 301 Insert the given table of contents into the given document. | |
| 302 | |
| 303 The node with C{class} attribute set to C{toc} has its children replaced | |
| 304 with C{toc}. | |
| 305 | |
| 306 @type document: A DOM Node or Document | |
| 307 @type toc: A DOM Node | |
| 308 """ | |
| 309 tocOrig = domhelpers.findElementsWithAttribute(document, 'class', 'toc') | |
| 310 if tocOrig: | |
| 311 tocOrig= tocOrig[0] | |
| 312 tocOrig.childNodes = [toc] | |
| 313 | |
| 314 | |
| 315 | |
| 316 def removeH1(document): | |
| 317 """ | |
| 318 Replace all C{h1} nodes in the given document with empty C{span} nodes. | |
| 319 | |
| 320 C{h1} nodes mark up document sections and the output template is given an | |
| 321 opportunity to present this information in a different way. | |
| 322 | |
| 323 @type document: A DOM Node or Document | |
| 324 @param document: The input document which contains all of the content to be | |
| 325 presented. | |
| 326 | |
| 327 @return: C{None} | |
| 328 """ | |
| 329 h1 = domhelpers.findNodesNamed(document, 'h1') | |
| 330 empty = microdom.Element('span') | |
| 331 for node in h1: | |
| 332 node.parentNode.replaceChild(empty, node) | |
| 333 | |
| 334 | |
| 335 | |
| 336 def footnotes(document): | |
| 337 """ | |
| 338 Find footnotes in the given document, move them to the end of the body, and | |
| 339 generate links to them. | |
| 340 | |
| 341 A footnote is any node with a C{class} attribute set to C{footnote}. | |
| 342 Footnote links are generated as superscript. Footnotes are collected in a | |
| 343 C{ol} node at the end of the document. | |
| 344 | |
| 345 @type document: A DOM Node or Document | |
| 346 @param document: The input document which contains all of the content to be | |
| 347 presented. | |
| 348 | |
| 349 @return: C{None} | |
| 350 """ | |
| 351 footnotes = domhelpers.findElementsWithAttribute(document, "class", | |
| 352 "footnote") | |
| 353 if not footnotes: | |
| 354 return | |
| 355 footnoteElement = microdom.Element('ol') | |
| 356 id = 1 | |
| 357 for footnote in footnotes: | |
| 358 href = microdom.parseString('<a href="#footnote-%(id)d">' | |
| 359 '<super>%(id)d</super></a>' | |
| 360 % vars()).documentElement | |
| 361 text = ' '.join(domhelpers.getNodeText(footnote).split()) | |
| 362 href.setAttribute('title', text) | |
| 363 target = microdom.Element('a', attributes={'name': 'footnote-%d' % id}) | |
| 364 target.childNodes = [footnote] | |
| 365 footnoteContent = microdom.Element('li') | |
| 366 footnoteContent.childNodes = [target] | |
| 367 footnoteElement.childNodes.append(footnoteContent) | |
| 368 footnote.parentNode.replaceChild(href, footnote) | |
| 369 id += 1 | |
| 370 body = domhelpers.findNodesNamed(document, "body")[0] | |
| 371 header = microdom.parseString('<h2>Footnotes</h2>').documentElement | |
| 372 body.childNodes.append(header) | |
| 373 body.childNodes.append(footnoteElement) | |
| 374 | |
| 375 | |
| 376 | |
| 377 def notes(document): | |
| 378 """ | |
| 379 Find notes in the given document and mark them up as such. | |
| 380 | |
| 381 A note is any node with a C{class} attribute set to C{note}. | |
| 382 | |
| 383 (I think this is a very stupid feature. When I found it I actually | |
| 384 exclaimed out loud. -exarkun) | |
| 385 | |
| 386 @type document: A DOM Node or Document | |
| 387 @param document: The input document which contains all of the content to be | |
| 388 presented. | |
| 389 | |
| 390 @return: C{None} | |
| 391 """ | |
| 392 notes = domhelpers.findElementsWithAttribute(document, "class", "note") | |
| 393 notePrefix = microdom.parseString('<strong>Note: </strong>').documentElement | |
| 394 for note in notes: | |
| 395 note.childNodes.insert(0, notePrefix) | |
| 396 | |
| 397 | |
| 398 | |
| 399 def compareMarkPos(a, b): | |
| 400 """ | |
| 401 Perform in every way identically to L{cmp} for valid inputs. | |
| 402 | |
| 403 XXX - replace this with L{cmp} | |
| 404 """ | |
| 405 linecmp = cmp(a[0], b[0]) | |
| 406 if linecmp: | |
| 407 return linecmp | |
| 408 return cmp(a[1], b[1]) | |
| 409 | |
| 410 | |
| 411 | |
| 412 def comparePosition(firstElement, secondElement): | |
| 413 """ | |
| 414 Compare the two elements given by their position in the document or | |
| 415 documents they were parsed from. | |
| 416 | |
| 417 @type firstElement: C{twisted.web.microdom.Element} | |
| 418 @type secondElement: C{twisted.web.microdom.Element} | |
| 419 | |
| 420 @return: C{-1}, C{0}, or C{1}, with the same meanings as the return value | |
| 421 of L{cmp}. | |
| 422 """ | |
| 423 return compareMarkPos(firstElement._markpos, secondElement._markpos) | |
| 424 | |
| 425 | |
| 426 | |
| 427 def findNodeJustBefore(target, nodes): | |
| 428 """ | |
| 429 Find the node in C{nodes} which appeared immediately before C{target} in | |
| 430 the input document. | |
| 431 | |
| 432 @type target: L{twisted.web.microdom.Element} | |
| 433 @type nodes: C{list} of L{twisted.web.microdom.Element} | |
| 434 @return: An element from C{nodes} | |
| 435 """ | |
| 436 result = None | |
| 437 for node in nodes: | |
| 438 if comparePosition(target, node) < 0: | |
| 439 return result | |
| 440 result = node | |
| 441 return result | |
| 442 | |
| 443 | |
| 444 | |
| 445 def getFirstAncestorWithSectionHeader(entry): | |
| 446 """ | |
| 447 Visit the ancestors of C{entry} until one with at least one C{h2} child | |
| 448 node is found, then return all of that node's C{h2} child nodes. | |
| 449 | |
| 450 @type entry: A DOM Node | |
| 451 @param entry: The node from which to begin traversal. This node itself is | |
| 452 excluded from consideration. | |
| 453 | |
| 454 @rtype: C{list} of DOM Nodes | |
| 455 @return: All C{h2} nodes of the ultimately selected parent node. | |
| 456 """ | |
| 457 for a in domhelpers.getParents(entry)[1:]: | |
| 458 headers = domhelpers.findNodesNamed(a, "h2") | |
| 459 if len(headers) > 0: | |
| 460 return headers | |
| 461 return [] | |
| 462 | |
| 463 | |
| 464 | |
| 465 def getSectionNumber(header): | |
| 466 """ | |
| 467 Retrieve the section number of the given node. | |
| 468 | |
| 469 @type header: A DOM Node or L{None} | |
| 470 @param header: The section from which to extract a number. The section | |
| 471 number is the value of this node's first child. | |
| 472 | |
| 473 @return: C{None} or a C{str} giving the section number. | |
| 474 """ | |
| 475 if not header: | |
| 476 return None | |
| 477 return header.childNodes[0].value.strip() | |
| 478 | |
| 479 | |
| 480 | |
| 481 def getSectionReference(entry): | |
| 482 """ | |
| 483 Find the section number which contains the given node. | |
| 484 | |
| 485 This function looks at the given node's ancestry until it finds a node | |
| 486 which defines a section, then returns that section's number. | |
| 487 | |
| 488 @type entry: A DOM Node | |
| 489 @param entry: The node for which to determine the section. | |
| 490 | |
| 491 @rtype: C{str} | |
| 492 @return: The section number, as returned by C{getSectionNumber} of the | |
| 493 first ancestor of C{entry} which defines a section, as determined by | |
| 494 L{getFirstAncestorWithSectionHeader}. | |
| 495 """ | |
| 496 headers = getFirstAncestorWithSectionHeader(entry) | |
| 497 myHeader = findNodeJustBefore(entry, headers) | |
| 498 return getSectionNumber(myHeader) | |
| 499 | |
| 500 | |
| 501 | |
| 502 def index(document, filename, chapterReference): | |
| 503 """ | |
| 504 Extract index entries from the given document and store them for later use | |
| 505 and insert named anchors so that the index can link back to those entries. | |
| 506 | |
| 507 Any node with a C{class} attribute set to C{index} is considered an index | |
| 508 entry. | |
| 509 | |
| 510 @type document: A DOM Node or Document | |
| 511 @param document: The input document which contains all of the content to be | |
| 512 presented. | |
| 513 | |
| 514 @type filename: C{str} | |
| 515 @param filename: A link to the output for the given document which will be | |
| 516 included in the index to link to any index entry found here. | |
| 517 | |
| 518 @type chapterReference: ??? | |
| 519 @param chapterReference: ??? | |
| 520 | |
| 521 @return: C{None} | |
| 522 """ | |
| 523 entries = domhelpers.findElementsWithAttribute(document, "class", "index") | |
| 524 if not entries: | |
| 525 return | |
| 526 i = 0; | |
| 527 for entry in entries: | |
| 528 i += 1 | |
| 529 anchor = 'index%02d' % i | |
| 530 if chapterReference: | |
| 531 ref = getSectionReference(entry) or chapterReference | |
| 532 else: | |
| 533 ref = 'link' | |
| 534 indexer.addEntry(filename, anchor, entry.attributes['value'], ref) | |
| 535 # does nodeName even affect anything? | |
| 536 entry.nodeName = entry.tagName = entry.endTagName = 'a' | |
| 537 entry.attributes = InsensitiveDict({'name': anchor}) | |
| 538 | |
| 539 | |
| 540 | |
| 541 def setIndexLink(template, indexFilename): | |
| 542 """ | |
| 543 Insert a link to an index document. | |
| 544 | |
| 545 Any node with a C{class} attribute set to C{index-link} will have its tag | |
| 546 name changed to C{a} and its C{href} attribute set to C{indexFilename}. | |
| 547 | |
| 548 @type template: A DOM Node or Document | |
| 549 @param template: The output template which defines the presentation of the | |
| 550 version information. | |
| 551 | |
| 552 @type indexFilename: C{str} | |
| 553 @param indexFilename: The address of the index document to which to link. | |
| 554 If any C{False} value, this function will remove all index-link nodes. | |
| 555 | |
| 556 @return: C{None} | |
| 557 """ | |
| 558 indexLinks = domhelpers.findElementsWithAttribute(template, | |
| 559 "class", | |
| 560 "index-link") | |
| 561 for link in indexLinks: | |
| 562 if indexFilename is None: | |
| 563 link.parentNode.removeChild(link) | |
| 564 else: | |
| 565 link.nodeName = link.tagName = link.endTagName = 'a' | |
| 566 link.attributes = InsensitiveDict({'href': indexFilename}) | |
| 567 | |
| 568 | |
| 569 | |
| 570 def numberDocument(document, chapterNumber): | |
| 571 """ | |
| 572 Number the sections of the given document. | |
| 573 | |
| 574 A dot-separated chapter, section number is added to the beginning of each | |
| 575 section, as defined by C{h2} nodes. | |
| 576 | |
| 577 @type document: A DOM Node or Document | |
| 578 @param document: The input document which contains all of the content to be | |
| 579 presented. | |
| 580 | |
| 581 @type chapterNumber: C{int} | |
| 582 @param chapterNumber: The chapter number of this content in an overall | |
| 583 document. | |
| 584 | |
| 585 @return: C{None} | |
| 586 """ | |
| 587 i = 1 | |
| 588 for node in domhelpers.findNodesNamed(document, "h2"): | |
| 589 node.childNodes = [microdom.Text("%s.%d " % (chapterNumber, i))] + node.
childNodes | |
| 590 i += 1 | |
| 591 | |
| 592 | |
| 593 | |
| 594 def fixRelativeLinks(document, linkrel): | |
| 595 """ | |
| 596 Replace relative links in C{str} and C{href} attributes with links relative | |
| 597 to C{linkrel}. | |
| 598 | |
| 599 @type document: A DOM Node or Document | |
| 600 @param document: The output template. | |
| 601 | |
| 602 @type linkrel: C{str} | |
| 603 @param linkrel: An prefix to apply to all relative links in C{src} or | |
| 604 C{href} attributes in the input document when generating the output | |
| 605 document. | |
| 606 """ | |
| 607 for attr in 'src', 'href': | |
| 608 for node in domhelpers.findElementsWithAttribute(document, attr): | |
| 609 href = node.getAttribute(attr) | |
| 610 if not href.startswith('http') and not href.startswith('/'): | |
| 611 node.setAttribute(attr, linkrel+node.getAttribute(attr)) | |
| 612 | |
| 613 | |
| 614 | |
| 615 def setTitle(template, title, chapterNumber): | |
| 616 """ | |
| 617 Add title and chapter number information to the template document. | |
| 618 | |
| 619 The title is added to the end of the first C{title} tag and the end of the | |
| 620 first tag with a C{class} attribute set to C{title}. If specified, the | |
| 621 chapter is inserted before the title. | |
| 622 | |
| 623 @type template: A DOM Node or Document | |
| 624 @param template: The output template which defines the presentation of the | |
| 625 version information. | |
| 626 | |
| 627 @type title: C{list} of DOM Nodes | |
| 628 @param title: Nodes from the input document defining its title. | |
| 629 | |
| 630 @type chapterNumber: C{int} | |
| 631 @param chapterNumber: The chapter number of this content in an overall | |
| 632 document. If not applicable, any C{False} value will result in this | |
| 633 information being omitted. | |
| 634 | |
| 635 @return: C{None} | |
| 636 """ | |
| 637 for nodeList in (domhelpers.findNodesNamed(template, "title"), | |
| 638 domhelpers.findElementsWithAttribute(template, "class", | |
| 639 'title')): | |
| 640 if nodeList: | |
| 641 if numberer.getNumberSections() and chapterNumber: | |
| 642 nodeList[0].childNodes.append(microdom.Text('%s. ' % chapterNumb
er)) | |
| 643 nodeList[0].childNodes.extend(title) | |
| 644 | |
| 645 | |
| 646 | |
| 647 def setAuthors(template, authors): | |
| 648 """ | |
| 649 Add author information to the template document. | |
| 650 | |
| 651 Names and contact information for authors are added to each node with a | |
| 652 C{class} attribute set to C{authors} and to the template head as C{link} | |
| 653 nodes. | |
| 654 | |
| 655 @type template: A DOM Node or Document | |
| 656 @param template: The output template which defines the presentation of the | |
| 657 version information. | |
| 658 | |
| 659 @type authors: C{list} of two-tuples of C{str} | |
| 660 @param authors: List of names and contact information for the authors of | |
| 661 the input document. | |
| 662 | |
| 663 @return: C{None} | |
| 664 """ | |
| 665 # First, similarly to setTitle, insert text into an <div class="authors"> | |
| 666 text = '' | |
| 667 for name, href in authors: | |
| 668 # FIXME: Do proper quoting/escaping (is it ok to use | |
| 669 # xml.sax.saxutils.{escape,quoteattr}?) | |
| 670 anchor = '<a href="%s">%s</a>' % (href, name) | |
| 671 if (name, href) == authors[-1]: | |
| 672 if len(authors) == 1: | |
| 673 text = anchor | |
| 674 else: | |
| 675 text += 'and ' + anchor | |
| 676 else: | |
| 677 text += anchor + ',' | |
| 678 | |
| 679 childNodes = microdom.parseString('<span>' + text +'</span>').childNodes | |
| 680 | |
| 681 for node in domhelpers.findElementsWithAttribute(template, | |
| 682 "class", 'authors'): | |
| 683 node.childNodes.extend(childNodes) | |
| 684 | |
| 685 # Second, add appropriate <link rel="author" ...> tags to the <head>. | |
| 686 head = domhelpers.findNodesNamed(template, 'head')[0] | |
| 687 authors = [microdom.parseString('<link rel="author" href="%s" title="%s"/>' | |
| 688 % (href, name)).childNodes[0] | |
| 689 for name, href in authors] | |
| 690 head.childNodes.extend(authors) | |
| 691 | |
| 692 | |
| 693 | |
| 694 def setVersion(template, version): | |
| 695 """ | |
| 696 Add a version indicator to the given template. | |
| 697 | |
| 698 @type template: A DOM Node or Document | |
| 699 @param template: The output template which defines the presentation of the | |
| 700 version information. | |
| 701 | |
| 702 @type version: C{str} | |
| 703 @param version: The version string to add to the template. | |
| 704 | |
| 705 @return: C{None} | |
| 706 """ | |
| 707 for node in domhelpers.findElementsWithAttribute(template, "class", | |
| 708 "version"): | |
| 709 node.appendChild(microdom.Text(version)) | |
| 710 | |
| 711 | |
| 712 | |
| 713 def getOutputFileName(originalFileName, outputExtension, index=None): | |
| 714 """ | |
| 715 Return a filename which is the same as C{originalFileName} except for the | |
| 716 extension, which is replaced with C{outputExtension}. | |
| 717 | |
| 718 For example, if C{originalFileName} is C{'/foo/bar.baz'} and | |
| 719 C{outputExtension} is C{'quux'}, the return value will be | |
| 720 C{'/foo/bar.quux'}. | |
| 721 | |
| 722 @type originalFileName: C{str} | |
| 723 @type outputExtension: C{stR} | |
| 724 @param index: ignored, never passed. | |
| 725 @rtype: C{str} | |
| 726 """ | |
| 727 return os.path.splitext(originalFileName)[0]+outputExtension | |
| 728 | |
| 729 | |
| 730 | |
| 731 def munge(document, template, linkrel, dir, fullpath, ext, url, config, outfileG
enerator=getOutputFileName): | |
| 732 """ | |
| 733 Mutate C{template} until it resembles C{document}. | |
| 734 | |
| 735 @type document: A DOM Node or Document | |
| 736 @param document: The input document which contains all of the content to be | |
| 737 presented. | |
| 738 | |
| 739 @type template: A DOM Node or Document | |
| 740 @param template: The template document which defines the desired | |
| 741 presentation format of the content. | |
| 742 | |
| 743 @type linkrel: C{str} | |
| 744 @param linkrel: An prefix to apply to all relative links in C{src} or | |
| 745 C{href} attributes in the input document when generating the output | |
| 746 document. | |
| 747 | |
| 748 @type dir: C{str} | |
| 749 @param dir: The directory in which to search for source listing files. | |
| 750 | |
| 751 @type fullpath: C{str} | |
| 752 @param fullpath: The file name which contained the input document. | |
| 753 | |
| 754 @type ext: C{str} | |
| 755 @param ext: The extension to use when selecting an output file name. This | |
| 756 replaces the extension of the input file name. | |
| 757 | |
| 758 @type url: C{str} | |
| 759 @param url: A string which will be interpolated with the fully qualified | |
| 760 Python name of any API reference encountered in the input document, the | |
| 761 result of which will be used as a link to API documentation for that name | |
| 762 in the output document. | |
| 763 | |
| 764 @type config: C{dict} | |
| 765 @param config: Further specification of the desired form of the output. | |
| 766 Valid keys in this dictionary:: | |
| 767 | |
| 768 noapi: If present and set to a True value, links to API documentation | |
| 769 will not be generated. | |
| 770 | |
| 771 version: A string which will be included in the output to indicate the | |
| 772 version of this documentation. | |
| 773 | |
| 774 @type outfileGenerator: Callable of C{str}, C{str} returning C{str} | |
| 775 @param outfileGenerator: Output filename factory. This is invoked with the | |
| 776 intput filename and C{ext} and the output document is serialized to the | |
| 777 file with the name returned. | |
| 778 | |
| 779 @return: C{None} | |
| 780 """ | |
| 781 fixRelativeLinks(template, linkrel) | |
| 782 addMtime(template, fullpath) | |
| 783 removeH1(document) | |
| 784 if not config.get('noapi', False): | |
| 785 fixAPI(document, url) | |
| 786 fontifyPython(document) | |
| 787 fixLinks(document, ext) | |
| 788 addPyListings(document, dir) | |
| 789 addHTMLListings(document, dir) | |
| 790 addPlainListings(document, dir) | |
| 791 putInToC(template, generateToC(document)) | |
| 792 footnotes(document) | |
| 793 notes(document) | |
| 794 | |
| 795 setIndexLink(template, indexer.getIndexFilename()) | |
| 796 setVersion(template, config.get('version', '')) | |
| 797 | |
| 798 # Insert the document into the template | |
| 799 chapterNumber = htmlbook.getNumber(fullpath) | |
| 800 title = domhelpers.findNodesNamed(document, 'title')[0].childNodes | |
| 801 setTitle(template, title, chapterNumber) | |
| 802 if numberer.getNumberSections() and chapterNumber: | |
| 803 numberDocument(document, chapterNumber) | |
| 804 index(document, outfileGenerator(os.path.split(fullpath)[1], ext), | |
| 805 htmlbook.getReference(fullpath)) | |
| 806 | |
| 807 authors = domhelpers.findNodesNamed(document, 'link') | |
| 808 authors = [(node.getAttribute('title',''), node.getAttribute('href', '')) | |
| 809 for node in authors if node.getAttribute('rel', '') == 'author'] | |
| 810 setAuthors(template, authors) | |
| 811 | |
| 812 body = domhelpers.findNodesNamed(document, "body")[0] | |
| 813 tmplbody = domhelpers.findElementsWithAttribute(template, "class", | |
| 814 "body")[0] | |
| 815 tmplbody.childNodes = body.childNodes | |
| 816 tmplbody.setAttribute("class", "content") | |
| 817 | |
| 818 | |
| 819 def parseFileAndReport(filename): | |
| 820 """ | |
| 821 Parse and return the contents of the given lore XHTML document. | |
| 822 | |
| 823 @type filename: C{str} | |
| 824 @param filename: The name of a file containing a lore XHTML document to | |
| 825 load. | |
| 826 | |
| 827 @raise process.ProcessingFailure: When the contents of the specified file | |
| 828 cannot be parsed. | |
| 829 | |
| 830 @rtype: A DOM Document | |
| 831 @return: The document contained in C{filename}. | |
| 832 """ | |
| 833 try: | |
| 834 return microdom.parse(open(filename)) | |
| 835 except microdom.MismatchedTags, e: | |
| 836 raise process.ProcessingFailure( | |
| 837 "%s:%s: begin mismatched tags <%s>/</%s>" % | |
| 838 (e.begLine, e.begCol, e.got, e.expect), | |
| 839 "%s:%s: end mismatched tags <%s>/</%s>" % | |
| 840 (e.endLine, e.endCol, e.got, e.expect)) | |
| 841 except microdom.ParseError, e: | |
| 842 raise process.ProcessingFailure("%s:%s:%s" % (e.line, e.col, e.message)) | |
| 843 except IOError, e: | |
| 844 raise process.ProcessingFailure(e.strerror + ", filename was '" + filena
me + "'") | |
| 845 | |
| 846 def makeSureDirectoryExists(filename): | |
| 847 filename = os.path.abspath(filename) | |
| 848 dirname = os.path.dirname(filename) | |
| 849 if (not os.path.exists(dirname)): | |
| 850 os.makedirs(dirname) | |
| 851 | |
| 852 def doFile(filename, linkrel, ext, url, templ, options={}, outfileGenerator=getO
utputFileName): | |
| 853 """ | |
| 854 Process the input document at C{filename} and write an output document. | |
| 855 | |
| 856 @type filename: C{str} | |
| 857 @param filename: The path to the input file which will be processed. | |
| 858 | |
| 859 @type linkrel: C{str} | |
| 860 @param linkrel: An prefix to apply to all relative links in C{src} or | |
| 861 C{href} attributes in the input document when generating the output | |
| 862 document. | |
| 863 | |
| 864 @type ext: C{str} | |
| 865 @param ext: The extension to use when selecting an output file name. This | |
| 866 replaces the extension of the input file name. | |
| 867 | |
| 868 @type url: C{str} | |
| 869 @param url: A string which will be interpolated with the fully qualified | |
| 870 Python name of any API reference encountered in the input document, the | |
| 871 result of which will be used as a link to API documentation for that name | |
| 872 in the output document. | |
| 873 | |
| 874 @type templ: A DOM Node or Document | |
| 875 @param templ: The template on which the output document will be based. | |
| 876 This is mutated and then serialized to the output file. | |
| 877 | |
| 878 @type options: C{dict} | |
| 879 @param options: Further specification of the desired form of the output. | |
| 880 Valid keys in this dictionary:: | |
| 881 | |
| 882 noapi: If present and set to a True value, links to API documentation | |
| 883 will not be generated. | |
| 884 | |
| 885 version: A string which will be included in the output to indicate the | |
| 886 version of this documentation. | |
| 887 | |
| 888 @type outfileGenerator: Callable of C{str}, C{str} returning C{str} | |
| 889 @param outfileGenerator: Output filename factory. This is invoked with the | |
| 890 intput filename and C{ext} and the output document is serialized to the | |
| 891 file with the name returned. | |
| 892 | |
| 893 @return: C{None} | |
| 894 """ | |
| 895 doc = parseFileAndReport(filename) | |
| 896 clonedNode = templ.cloneNode(1) | |
| 897 munge(doc, clonedNode, linkrel, os.path.dirname(filename), filename, ext, | |
| 898 url, options, outfileGenerator) | |
| 899 newFilename = outfileGenerator(filename, ext) | |
| 900 makeSureDirectoryExists(newFilename) | |
| 901 clonedNode.writexml(open(newFilename, 'wb')) | |
| OLD | NEW |