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 |