mkhtml: apply own line numbering to highlighted sources
[gtk-doc.git] / gtkdoc / mkhtml2.py
blob1ec31c3f1385ccd5ed853a86d4b489305a668505
1 #!/usr/bin/env python3
2 # -*- python; coding: utf-8 -*-
4 # gtk-doc - GTK DocBook documentation generator.
5 # Copyright (C) 2018 Stefan Sauer
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22 """Prototype for builtin docbook processing
24 The tool loads the main xml document (<module>-docs.xml) and chunks it
25 like the xsl-stylesheets would do. For that it resolves all the xml-includes.
26 Each chunk is converted to htnml using python functions.
28 In contrast to our previous approach of running gtkdoc-mkhtml + gtkdoc-fixxref,
29 this tools will replace both without relying on external tools such as xsltproc
30 and source-highlight.
32 TODO:
33 - more chunk converters
34 - check each docbook tag if it can contain #PCDATA, if not don't check for
35 xml.text
37 OPTIONAL:
38 - minify html: https://pypi.python.org/pypi/htmlmin/
40 Requirements:
41 sudo pip3 install anytree lxml pygments
43 Example invocation:
44 cd tests/bugs/docs/
45 ../../../gtkdoc-mkhtml2 tester tester-docs.xml
46 xdg-open db2html/index.html
47 meld html db2html
49 Benchmarking:
50 cd tests/bugs/docs/;
51 rm html-build.stamp; time make html-build.stamp
52 """
54 import argparse
55 import errno
56 import logging
57 import os
58 import shutil
59 import sys
61 from anytree import Node, PreOrderIter
62 from copy import deepcopy
63 from glob import glob
64 from lxml import etree
65 from pygments import highlight
66 from pygments.lexers import CLexer
67 from pygments.formatters import HtmlFormatter
69 from . import config, fixxref
71 # pygments setup
72 # TODO: maybe use get_lexer_for_filename()
73 LEXER = CLexer()
74 HTML_FORMATTER = HtmlFormatter(nowrap=True)
76 # http://www.sagehill.net/docbookxsl/Chunking.html
77 CHUNK_TAGS = [
78 'appendix',
79 'article',
80 'bibliography', # in article or book
81 'book',
82 'chapter',
83 'colophon',
84 'glossary', # in article or book
85 'index', # in article or book
86 'part',
87 'preface',
88 'refentry',
89 'reference',
90 'sect1', # except first
91 'section', # if equivalent to sect1
92 'set',
93 'setindex',
97 class ChunkParams(object):
98 def __init__(self, prefix, parent=None):
99 self.prefix = prefix
100 self.parent = None
101 self.count = 0
104 # TODO: look up the abbrevs and hierarchy for other tags
105 # http://www.sagehill.net/docbookxsl/Chunking.html#GeneratedFilenames
106 # https://github.com/oreillymedia/HTMLBook/blob/master/htmlbook-xsl/chunk.xsl#L33
107 CHUNK_PARAMS = {
108 'appendix': ChunkParams('app', 'book'),
109 'book': ChunkParams('bk'),
110 'chapter': ChunkParams('ch', 'book'),
111 'index': ChunkParams('ix', 'book'),
112 'part': ChunkParams('pt', 'book'),
113 'sect1': ChunkParams('s', 'chapter'),
114 'section': ChunkParams('s', 'chapter'),
117 TITLE_XPATHS = {
118 '_': (etree.XPath('./title'), None),
119 'book': (etree.XPath('./bookinfo/title'), None),
120 'refentry': (
121 etree.XPath('./refmeta/refentrytitle'),
122 etree.XPath('./refnamediv/refpurpose')
126 ID_XPATH = etree.XPath('//@id')
129 def gen_chunk_name(node):
130 if 'id' in node.attrib:
131 return node.attrib['id']
133 tag = node.tag
134 if tag not in CHUNK_PARAMS:
135 CHUNK_PARAMS[tag] = ChunkParams(node.tag[:2])
136 logging.warning('Add CHUNK_PARAMS for "%s"', tag)
138 naming = CHUNK_PARAMS[tag]
139 naming.count += 1
140 name = ('%s%02d' % (naming.prefix, naming.count))
141 # handle parents to make names of nested tags unique
142 # TODO: we only need to prepend the parent if there are > 1 of them in the
143 # xml
144 # while naming.parent:
145 # parent = naming.parent
146 # if parent not in CHUNK_PARAMS:
147 # break;
148 # naming = CHUNK_PARAMS[parent]
149 # name = ('%s%02d' % (naming.prefix, naming.count)) + name
150 return name
153 def get_chunk_titles(node):
154 tag = node.tag
155 if tag not in TITLE_XPATHS:
156 # Use defaults
157 (title, subtitle) = TITLE_XPATHS['_']
158 else:
159 (title, subtitle) = TITLE_XPATHS[tag]
161 xml = title(node)[0]
162 result = {
163 'title': xml.text
165 if xml.tag != 'title':
166 result['title_tag'] = xml.tag
167 else:
168 result['title_tag'] = tag
170 if subtitle:
171 xml = subtitle(node)[0]
172 result['subtitle'] = xml.text
173 result['subtitle_tag'] = xml.tag
174 else:
175 result['subtitle'] = None
176 result['subtitle_tag'] = None
177 return result
180 def chunk(xml_node, parent=None):
181 """Chunk the tree.
183 The first time, we're called with parent=None and in that case we return
184 the new_node as the root of the tree
186 if xml_node.tag in CHUNK_TAGS:
187 if parent:
188 # remove the xml-node from the parent
189 sub_tree = etree.ElementTree(deepcopy(xml_node)).getroot()
190 xml_node.getparent().remove(xml_node)
191 xml_node = sub_tree
193 title_args = get_chunk_titles(xml_node)
194 chunk_name = gen_chunk_name(xml_node)
195 parent = Node(xml_node.tag, parent=parent, xml=xml_node,
196 filename=chunk_name + '.html', **title_args)
198 for child in xml_node:
199 chunk(child, parent)
201 return parent
204 def add_id_links(files, links):
205 for node in files:
206 chunk_name = node.filename[:-5]
207 chunk_base = node.filename + '#'
208 for attr in ID_XPATH(node.xml):
209 if attr == chunk_name:
210 links[attr] = node.filename
211 else:
212 links[attr] = chunk_base + attr
215 # conversion helpers
218 def convert_inner(ctx, xml, result):
219 for child in xml:
220 result.extend(convert_tags.get(child.tag, convert__unknown)(ctx, child))
223 def convert_ignore(ctx, xml):
224 result = []
225 convert_inner(ctx, xml, result)
226 return result
229 def convert_skip(ctx, xml):
230 return ['']
233 missing_tags = {}
236 def convert__unknown(ctx, xml):
237 # don't recurse on subchunks
238 if xml.tag in CHUNK_TAGS:
239 return []
240 # warn only once
241 if xml.tag not in missing_tags:
242 logging.warning('Add tag converter for "%s"', xml.tag)
243 missing_tags[xml.tag] = True
244 result = ['<!-- ' + xml.tag + '-->\n']
245 convert_inner(ctx, xml, result)
246 result.append('<!-- /' + xml.tag + '-->\n')
247 return result
250 def convert_refsect(ctx, xml, h_tag, inner_func=convert_inner):
251 result = ['<div class="%s">\n' % xml.tag]
252 title = xml.find('title')
253 if title is not None:
254 if 'id' in xml.attrib:
255 result.append('<a name="%s"></a>' % xml.attrib['id'])
256 result.append('<%s>%s</%s>' % (h_tag, title.text, h_tag))
257 xml.remove(title)
258 if xml.text:
259 result.append(xml.text)
260 inner_func(ctx, xml, result)
261 result.append('</div>')
262 if xml.tail:
263 result.append(xml.tail)
264 return result
267 def xml_get_title(xml):
268 title = xml.find('title')
269 if title is not None:
270 return title.text
271 else:
272 # TODO(ensonic): any way to get the file (inlcudes) too?
273 logging.warning('%s: Expected title tag under "%s %s"', xml.sourceline, xml.tag, str(xml.attrib))
274 return ''
277 # docbook tags
279 def convert_bookinfo(ctx, xml):
280 result = ['<div class="titlepage">']
281 for releaseinfo in xml.findall('releaseinfo'):
282 result.extend(convert_para(ctx, releaseinfo))
283 result.append("""<hr>
284 </div>""")
285 if xml.tail:
286 result.append(xml.tail)
287 return result
290 def convert_colspec(ctx, xml):
291 result = ['<col']
292 a = xml.attrib
293 if 'colname' in a:
294 result.append(' class="%s"' % a['colname'])
295 if 'colwidth' in a:
296 result.append(' width="%s"' % a['colwidth'])
297 result.append('>\n')
298 # is in tgroup and there can be no 'text'
299 return result
302 def convert_div(ctx, xml):
303 result = ['<div class="%s">\n' % xml.tag]
304 if xml.text:
305 result.append(xml.text)
306 convert_inner(ctx, xml, result)
307 result.append('</div>')
308 if xml.tail:
309 result.append(xml.tail)
310 return result
313 def convert_em_class(ctx, xml):
314 result = ['<em class="%s"><code>' % xml.tag]
315 if xml.text:
316 result.append(xml.text)
317 convert_inner(ctx, xml, result)
318 result.append('</code></em>')
319 if xml.tail:
320 result.append(xml.tail)
321 return result
324 def convert_entry(ctx, xml):
325 result = ['<td']
326 if 'role' in xml.attrib:
327 result.append(' class="%s">' % xml.attrib['role'])
328 else:
329 result.append('>')
330 if xml.text:
331 result.append(xml.text)
332 convert_inner(ctx, xml, result)
333 result.append('</td>')
334 if xml.tail:
335 result.append(xml.tail)
336 return result
339 def convert_indexdiv(ctx, xml):
340 title_tag = xml.find('title')
341 title = title_tag.text
342 xml.remove(title_tag)
343 result = [
344 '<a name="idx%s"></a><h3 class="title">%s</h3>' % (title, title)
346 convert_inner(ctx, xml, result)
347 return result
350 def convert_informaltable(ctx, xml):
351 result = ['<div class="informaltable"><table class="informaltable"']
352 a = xml.attrib
353 if 'pgwide' in a and a['pgwide'] == '1':
354 result.append(' width="100%"')
355 if 'frame' in a and a['frame'] == 'none':
356 result.append(' border="0"')
357 result.append('>\n')
358 convert_inner(ctx, xml, result)
359 result.append('</table></div>')
360 if xml.tail:
361 result.append(xml.tail)
362 return result
365 def convert_itemizedlist(ctx, xml):
366 result = ['<div class="itemizedlist"><ul class="itemizedlist" style="list-style-type: disc; ">']
367 convert_inner(ctx, xml, result)
368 result.append('</ul></div>')
369 if xml.tail:
370 result.append(xml.tail)
371 return result
374 def convert_link(ctx, xml):
375 linkend = xml.attrib['linkend']
376 if linkend in fixxref.NoLinks:
377 linkend = None
378 result = []
379 if linkend:
380 link_text = []
381 convert_inner(ctx, xml, link_text)
382 if xml.text:
383 link_text.append(xml.text)
384 # TODO: fixxref does some weird checks in xml.text
385 result = [fixxref.MakeXRef(ctx['module'], '', 0, linkend, ''.join(link_text))]
386 if xml.tail:
387 result.append(xml.tail)
388 return result
391 def convert_listitem(ctx, xml):
392 result = ['<li class="listitem">']
393 convert_inner(ctx, xml, result)
394 result.append('</li>')
395 # is in itemizedlist and there can be no 'text'
396 return result
399 def convert_literal(ctx, xml):
400 result = ['<code class="%s">' % xml.tag]
401 if xml.text:
402 result.append(xml.text)
403 convert_inner(ctx, xml, result)
404 result.append('</code>')
405 if xml.tail:
406 result.append(xml.tail)
407 return result
410 def convert_para(ctx, xml):
411 result = ['<p>']
412 if xml.tag != 'para':
413 result = ['<p class="%s">' % xml.tag]
414 if xml.text:
415 result.append(xml.text)
416 convert_inner(ctx, xml, result)
417 result.append('</p>')
418 if xml.tail:
419 result.append(xml.tail)
420 return result
423 def convert_phrase(ctx, xml):
424 result = ['<span']
425 if 'role' in xml.attrib:
426 result.append(' class="%s">' % xml.attrib['role'])
427 else:
428 result.append('>')
429 if xml.text:
430 result.append(xml.text)
431 convert_inner(ctx, xml, result)
432 result.append('</span>')
433 if xml.tail:
434 result.append(xml.tail)
435 return result
438 def convert_primaryie(ctx, xml):
439 result = ['<dt>\n']
440 convert_inner(ctx, xml, result)
441 result.append('\n</dt>\n<dd></dd>\n')
442 return result
445 def convert_programlisting(ctx, xml):
446 result = []
447 if xml.attrib.get('role', '') == 'example':
448 if xml.text:
449 # TODO: check 'language' attr and use respective lexer
450 highlighted = highlight(xml.text, LEXER, HTML_FORMATTER)
452 # we do own line-numbering
453 line_count = highlighted.count('\n')
454 source_lines = '\n'.join([str(i) for i in range(1, line_count + 1)])
455 result.append("""<table class="listing_frame" border="0" cellpadding="0" cellspacing="0">
456 <tbody>
457 <tr>
458 <td class="listing_lines" align="right"><pre>%s</pre></td>
459 <td class="listing_code"><pre class="programlisting">%s</pre></td>
460 </tr>
461 </tbody>
462 </table>
463 """ % (source_lines, highlighted))
464 else:
465 result.append('<pre class="programlisting">')
466 if xml.text:
467 result.append(xml.text)
468 convert_inner(ctx, xml, result)
469 result.append('</pre>')
470 if xml.tail:
471 result.append(xml.tail)
472 return result
475 def convert_refsect1(ctx, xml):
476 # Add a divider between two consequitive refsect2
477 def convert_inner(ctx, xml, result):
478 prev = None
479 for child in xml:
480 if child.tag == 'refsect2' and prev is not None and prev.tag == child.tag:
481 result.append('<hr>\n')
482 result.extend(convert_tags.get(child.tag, convert__unknown)(ctx, child))
483 prev = child
484 return convert_refsect(ctx, xml, 'h2', convert_inner)
487 def convert_refsect2(ctx, xml):
488 return convert_refsect(ctx, xml, 'h3')
491 def convert_refsect3(ctx, xml):
492 return convert_refsect(ctx, xml, 'h4')
495 def convert_row(ctx, xml):
496 result = ['<tr>\n']
497 convert_inner(ctx, xml, result)
498 result.append('</tr>\n')
499 return result
502 def convert_span(ctx, xml):
503 result = ['<span class="%s">' % xml.tag]
504 if xml.text:
505 result.append(xml.text)
506 convert_inner(ctx, xml, result)
507 result.append('</span>')
508 if xml.tail:
509 result.append(xml.tail)
510 return result
513 def convert_tbody(ctx, xml):
514 result = ['<tbody>']
515 convert_inner(ctx, xml, result)
516 result.append('</tbody>')
517 # is in tgroup and there can be no 'text'
518 return result
521 def convert_tgroup(ctx, xml):
522 # tgroup does not expand to anything, but the nested colspecs need to
523 # be put into a colgroup
524 cols = xml.findall('colspec')
525 result = []
526 if cols:
527 result.append('<colgroup>\n')
528 for col in cols:
529 result.extend(convert_colspec(ctx, col))
530 xml.remove(col)
531 result.append('</colgroup>\n')
532 convert_inner(ctx, xml, result)
533 # is in informaltable and there can be no 'text'
534 return result
537 def convert_ulink(ctx, xml):
538 result = ['<a class="%s" href="%s">%s</a>' % (xml.tag, xml.attrib['url'], xml.text)]
539 if xml.tail:
540 result.append(xml.tail)
541 return result
544 # TODO(ensonic): turn into class with converters as functions and ctx as self
545 convert_tags = {
546 'bookinfo': convert_bookinfo,
547 'colspec': convert_colspec,
548 'entry': convert_entry,
549 'function': convert_span,
550 'indexdiv': convert_indexdiv,
551 'indexentry': convert_ignore,
552 'indexterm': convert_skip,
553 'informalexample': convert_div,
554 'informaltable': convert_informaltable,
555 'itemizedlist': convert_itemizedlist,
556 'link': convert_link,
557 'listitem': convert_listitem,
558 'literal': convert_literal,
559 'para': convert_para,
560 'parameter': convert_em_class,
561 'phrase': convert_phrase,
562 'primaryie': convert_primaryie,
563 'programlisting': convert_programlisting,
564 'releaseinfo': convert_para,
565 'refsect1': convert_refsect1,
566 'refsect2': convert_refsect2,
567 'refsect3': convert_refsect3,
568 'returnvalue': convert_span,
569 'row': convert_row,
570 'structfield': convert_em_class,
571 'tbody': convert_tbody,
572 'tgroup': convert_tgroup,
573 'type': convert_span,
574 'ulink': convert_ulink,
575 'warning': convert_div,
578 # conversion helpers
580 HTML_HEADER = """<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
581 <html>
582 <head>
583 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
584 <title>%s</title>
585 %s<link rel="stylesheet" href="style.css" type="text/css">
586 </head>
587 <body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF">
591 def generate_head_links(ctx):
592 n = ctx['nav_home']
593 result = [
594 '<link rel="home" href="%s" title="%s">\n' % (n.filename, n.title)
596 if 'nav_up' in ctx:
597 n = ctx['nav_up']
598 result.append('<link rel="up" href="%s" title="%s">\n' % (n.filename, n.title))
599 if 'nav_prev' in ctx:
600 n = ctx['nav_prev']
601 result.append('<link rel="prev" href="%s" title="%s">\n' % (n.filename, n.title))
602 if 'nav_next' in ctx:
603 n = ctx['nav_next']
604 result.append('<link rel="next" href="%s" title="%s">\n' % (n.filename, n.title))
605 return ''.join(result)
608 def generate_nav_links(ctx):
609 n = ctx['nav_home']
610 result = [
611 '<td><a accesskey="h" href="%s"><img src="home.png" width="16" height="16" border="0" alt="Home"></a></td>' % n.filename
613 if 'nav_up' in ctx:
614 n = ctx['nav_up']
615 result.append(
616 '<td><a accesskey="u" href="%s"><img src="up.png" width="16" height="16" border="0" alt="Up"></a></td>' % n.filename)
617 else:
618 result.append('<td><img src="up-insensitive.png" width="16" height="16" border="0"></td>')
619 if 'nav_prev' in ctx:
620 n = ctx['nav_prev']
621 result.append(
622 '<td><a accesskey="p" href="%s"><img src="left.png" width="16" height="16" border="0" alt="Prev"></a></td>' % n.filename)
623 else:
624 result.append('<td><img src="left-insensitive.png" width="16" height="16" border="0"></td>')
625 if 'nav_next' in ctx:
626 n = ctx['nav_next']
627 result.append(
628 '<td><a accesskey="n" href="%s"><img src="right.png" width="16" height="16" border="0" alt="Next"></a></td>' % n.filename)
629 else:
630 result.append('<td><img src="right-insensitive.png" width="16" height="16" border="0"></td>')
632 return ''.join(result)
635 def generate_toc(ctx, node):
636 result = []
637 for c in node.children:
638 # TODO: urlencode the filename: urllib.parse.quote_plus()
639 result.append('<dt><span class="%s"><a href="%s">%s</a></span>\n' % (
640 c.title_tag, c.filename, c.title))
641 if c.subtitle:
642 result.append('<span class="%s"> — %s</span>' % (c.subtitle_tag, c.subtitle))
643 result.append('</dt>\n')
644 if c.children:
645 result.append('<dd><dl>')
646 result.extend(generate_toc(ctx, c))
647 result.append('</dl></dd>')
648 return result
651 def generate_basic_nav(ctx):
652 return """<table class="navigation" id="top" width="100%%" cellpadding="2" cellspacing="5">
653 <tr valign="middle">
654 <td width="100%%" align="left" class="shortcuts"></td>
656 </tr>
657 </table>
658 """ % generate_nav_links(ctx)
661 def generate_index_nav(ctx, indexdivs):
662 ix_nav = []
663 for s in indexdivs:
664 title = xml_get_title(s)
665 ix_nav.append('<a class="shortcut" href="#idx%s">%s</a>' % (title, title))
667 return """<table class="navigation" id="top" width="100%%" cellpadding="2" cellspacing="5">
668 <tr valign="middle">
669 <td width="100%%" align="left" class="shortcuts">
670 <span id="nav_index">
672 </span>
673 </td>
675 </tr>
676 </table>
677 """ % ('\n<span class="dim">|</span>\n'.join(ix_nav), generate_nav_links(ctx))
680 def generate_refentry_nav(ctx, refsect1s, result):
681 result.append("""<table class="navigation" id="top" width="100%%" cellpadding="2" cellspacing="5">
682 <tr valign="middle">
683 <td width="100%%" align="left" class="shortcuts">
684 <a href="#" class="shortcut">Top</a>""")
686 for s in refsect1s:
687 # don't list TOC sections (role="xxx_proto")
688 if s.attrib.get('role', '').endswith("_proto"):
689 continue
691 title = xml_get_title(s)
692 result.append("""
693 <span id="nav_description">
694   <span class="dim">|</span> 
695 <a href="#%s" class="shortcut">%s</a>
696 </span>""" % (s.attrib['id'], title))
697 result.append("""
698 </td>
700 </tr>
701 </table>
702 """ % generate_nav_links(ctx))
705 def get_id(node):
706 xml = node.xml
707 node_id = xml.attrib.get('id', None)
708 if node_id:
709 return node_id
711 logging.warning('%d: No "id" attribute on "%s"', xml.sourceline, xml.tag)
712 ix = []
713 # Generate the 'id'. We need to walk up the xml-tree and check the positions
714 # for each sibling.
715 parent = xml.getparent()
716 while parent is not None:
717 children = parent.getchildren()
718 ix.insert(0, str(children.index(xml) + 1))
719 xml = parent
720 parent = xml.getparent()
721 # logging.warning('%s: id indexes: %s', node.filename, str(ix))
722 return 'id-1.' + '.'.join(ix)
725 # docbook chunks
728 def convert_book(ctx):
729 node = ctx['node']
730 result = [
731 HTML_HEADER % (node.title, generate_head_links(ctx)),
732 """<table class="navigation" id="top" width="100%%" cellpadding="2" cellspacing="0">
733 <tr><th valign="middle"><p class="title">%s</p></th></tr>
734 </table>
735 <div class="book">
736 """ % node.title
738 bookinfo = node.xml.findall('bookinfo')[0]
739 result.extend(convert_bookinfo(ctx, bookinfo))
740 result.append("""<div class="toc">
741 <dl class="toc">
742 """)
743 result.extend(generate_toc(ctx, node.root))
744 result.append("""</dl>
745 </div>
746 </div>
747 </body>
748 </html>""")
749 return result
752 def convert_chapter(ctx):
753 node = ctx['node']
754 result = [
755 HTML_HEADER % (node.title + ": " + node.root.title, generate_head_links(ctx)),
756 generate_basic_nav(ctx),
757 '<div class="chapter">',
759 title = node.xml.find('title')
760 if title is not None:
761 result.append('<div class="titlepage"><h1 class="title"><a name="%s"></a>%s</h1></div>' % (
762 get_id(node), title.text))
763 node.xml.remove(title)
764 convert_inner(ctx, node.xml, result)
765 result.append("""<div class="toc">
766 <dl class="toc">
767 """)
768 result.extend(generate_toc(ctx, node))
769 result.append("""</dl>
770 </div>
771 </div>
772 </body>
773 </html>""")
774 return result
777 def convert_index(ctx):
778 node = ctx['node']
779 node_id = get_id(node)
780 # Get all indexdivs under indexdiv
781 indexdivs = node.xml.find('indexdiv').findall('indexdiv')
783 result = [
784 HTML_HEADER % (node.title + ": " + node.root.title, generate_head_links(ctx)),
785 generate_index_nav(ctx, indexdivs),
786 """<div class="index">
787 <div class="titlepage"><h1 class="title">
788 <a name="%s"></a>%s</h1>
789 </div>""" % (node_id, node.title)
791 for i in indexdivs:
792 result.extend(convert_indexdiv(ctx, i))
793 result.append("""</div>
794 </body>
795 </html>""")
796 return result
799 def convert_refentry(ctx):
800 node = ctx['node']
801 node_id = get_id(node)
802 refsect1s = node.xml.findall('refsect1')
804 result = [
805 HTML_HEADER % (node.title + ": " + node.root.title, generate_head_links(ctx))
807 generate_refentry_nav(ctx, refsect1s, result)
808 result.append("""
809 <div class="refentry">
810 <a name="%s"></a>
811 <div class="refnamediv">
812 <table width="100%%"><tr>
813 <td valign="top">
814 <h2><span class="refentrytitle"><a name="%s.top_of_page"></a>%s</span></h2>
815 <p>%s — module for gtk-doc unit test</p>
816 </td>
817 <td class="gallery_image" valign="top" align="right"></td>
818 </tr></table>
819 </div>
820 """ % (node_id, node_id, node.title, node.title))
822 for s in refsect1s:
823 result.extend(convert_refsect1(ctx, s))
824 result.append("""</div>
825 </body>
826 </html>""")
827 return result
830 # TODO(ensonic): turn into class with converters as functions and ctx as self
831 convert_chunks = {
832 'book': convert_book,
833 'chapter': convert_chapter,
834 'index': convert_index,
835 'refentry': convert_refentry,
839 def generate_nav_nodes(files, node):
840 nav = {
841 'nav_home': node.root,
843 # nav params: up, prev, next
844 if node.parent:
845 nav['nav_up'] = node.parent
846 ix = files.index(node)
847 if ix > 0:
848 nav['nav_prev'] = files[ix - 1]
849 if ix < len(files) - 1:
850 nav['nav_next'] = files[ix + 1]
851 return nav
854 def convert(out_dir, module, files, node):
855 """Convert the docbook chunks to a html file.
857 Args:
858 out_dir: already created output dir
859 files: list of nodes in the tree in pre-order
860 node: current tree node
863 logging.info('Writing: %s', node.filename)
864 with open(os.path.join(out_dir, node.filename), 'wt') as html:
865 ctx = {
866 'module': module,
867 'files': files,
868 'node': node,
870 ctx.update(generate_nav_nodes(files, node))
872 if node.name in convert_chunks:
873 for line in convert_chunks[node.name](ctx):
874 html.write(line)
875 else:
876 logging.warning('Add converter/template for "%s"', node.name)
879 def create_devhelp2_toc(node):
880 result = []
881 for c in node.children:
882 if c.children:
883 result.append('<sub name="%s" link="%s">\n' % (c.title, c.filename))
884 result.extend(create_devhelp2_toc(c))
885 result.append('</sub>\n')
886 else:
887 result.append('<sub name="%s" link="%s"/>\n' % (c.title, c.filename))
888 return result
891 def create_devhelp2_condition_attribs(node):
892 if 'condition' in node.attrib:
893 # condition -> since, deprecated, ... (separated with '|')
894 cond = node.attrib['condition'].replace('"', '&quot;').split('|')
895 return' ' + ' '.join(['%s="%s"' % tuple(c.split(':', 1)) for c in cond])
896 else:
897 return ''
900 def create_devhelp2_refsect2_keyword(node, base_link):
901 return' <keyword type="%s" name="%s" link="%s"%s/>\n' % (
902 node.attrib['role'], xml_get_title(node), base_link + node.attrib['id'],
903 create_devhelp2_condition_attribs(node))
906 def create_devhelp2_refsect3_keyword(node, base_link, title, name):
907 return' <keyword type="%s" name="%s" link="%s"%s/>\n' % (
908 node.attrib['role'], title, base_link + name,
909 create_devhelp2_condition_attribs(node))
912 def create_devhelp2(out_dir, module, xml, files):
913 with open(os.path.join(out_dir, module + '.devhelp2'), 'wt') as idx:
914 bookinfo_nodes = xml.xpath('/book/bookinfo')
915 title = ''
916 if bookinfo_nodes is not None:
917 bookinfo = bookinfo_nodes[0]
918 title = bookinfo.xpath('./title/text()')[0]
919 online_url = bookinfo.xpath('./releaseinfo/ulink[@role="online-location"]/@url')[0]
920 # TODO: support author too (see devhelp2.xsl)
921 # TODO: fixxref uses '--src-lang' to set the language
922 result = [
923 """<?xml version="1.0" encoding="utf-8" standalone="no"?>
924 <book xmlns="http://www.devhelp.net/book" title="%s" link="index.html" author="" name="%s" version="2" language="c" online="%s">
925 <chapters>
926 """ % (title, module, online_url)
928 # toc
929 result.extend(create_devhelp2_toc(files[0].root))
930 result.append(""" </chapters>
931 <functions>
932 """)
933 # keywords from all refsect2 and refsect3
934 refsect2 = etree.XPath('//refsect2[@role]')
935 refsect3_enum = etree.XPath('refsect3[@role="enum_members"]/informaltable/tgroup/tbody/row[@role="constant"]')
936 refsect3_enum_details = etree.XPath('entry[@role="enum_member_name"]/para')
937 refsect3_struct = etree.XPath('refsect3[@role="struct_members"]/informaltable/tgroup/tbody/row[@role="member"]')
938 refsect3_struct_details = etree.XPath('entry[@role="struct_member_name"]/para/structfield')
939 for node in files:
940 base_link = node.filename + '#'
941 refsect2_nodes = refsect2(node.xml)
942 for refsect2_node in refsect2_nodes:
943 result.append(create_devhelp2_refsect2_keyword(refsect2_node, base_link))
944 refsect3_nodes = refsect3_enum(refsect2_node)
945 for refsect3_node in refsect3_nodes:
946 details_node = refsect3_enum_details(refsect3_node)[0]
947 name = details_node.attrib['id']
948 result.append(create_devhelp2_refsect3_keyword(refsect3_node, base_link, details_node.text, name))
949 refsect3_nodes = refsect3_struct(refsect2_node)
950 for refsect3_node in refsect3_nodes:
951 details_node = refsect3_struct_details(refsect3_node)[0]
952 name = details_node.attrib['id']
953 result.append(create_devhelp2_refsect3_keyword(refsect3_node, base_link, name, name))
955 result.append(""" </functions>
956 </book>
957 """)
958 for line in result:
959 idx.write(line)
962 def get_dirs(uninstalled):
963 if uninstalled:
964 # this does not work from buiddir!=srcdir
965 gtkdocdir = os.path.split(sys.argv[0])[0]
966 if not os.path.exists(gtkdocdir + '/gtk-doc.xsl'):
967 # try 'srcdir' (set from makefiles) too
968 if os.path.exists(os.environ.get("ABS_TOP_SRCDIR", '') + '/gtk-doc.xsl'):
969 gtkdocdir = os.environ['ABS_TOP_SRCDIR']
970 styledir = gtkdocdir + '/style'
971 else:
972 gtkdocdir = os.path.join(config.datadir, 'gtk-doc/data')
973 styledir = gtkdocdir
974 return (gtkdocdir, styledir)
977 def main(module, index_file, out_dir, uninstalled):
978 tree = etree.parse(index_file)
979 tree.xinclude()
981 (gtkdocdir, styledir) = get_dirs(uninstalled)
982 # copy navigation images and stylesheets to html directory ...
983 css_file = os.path.join(styledir, 'style.css')
984 for f in glob(os.path.join(styledir, '*.png')) + [css_file]:
985 shutil.copy(f, out_dir)
986 css_file = os.path.join(out_dir, 'style.css')
987 with open(css_file, 'at') as css:
988 css.write(HTML_FORMATTER.get_style_defs())
990 # TODO: migrate options from fixxref
991 # TODO: do in parallel with loading the xml above.
992 fixxref.LoadIndicies(out_dir, '/usr/share/gtk-doc/html', [])
994 # We do multiple passes:
995 # 1) recursively walk the tree and chunk it into a python tree so that we
996 # can generate navigation and link tags.
997 files = chunk(tree.getroot())
998 files = list(PreOrderIter(files))
999 # 2) find all 'id' attribs and add them to the link map
1000 add_id_links(files, fixxref.Links)
1001 # 3) create a xxx.devhelp2 file, do this before 3), since we modify the tree
1002 create_devhelp2(out_dir, module, tree.getroot(), files)
1003 # 4) iterate the tree and output files
1004 # TODO: use multiprocessing
1005 for node in files:
1006 convert(out_dir, module, files, node)
1009 def run(options):
1010 logging.info('options: %s', str(options.__dict__))
1011 module = options.args[0]
1012 document = options.args[1]
1014 # TODO: rename to 'html' later on
1015 # - right now in mkhtml, the dir is created by the Makefile and mkhtml
1016 # outputs into the working directory
1017 out_dir = os.path.join(os.path.dirname(document), 'db2html')
1018 try:
1019 os.mkdir(out_dir)
1020 except OSError as e:
1021 if e.errno != errno.EEXIST:
1022 raise
1024 sys.exit(main(module, document, out_dir, options.uninstalled))