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 """Generate html from docbook
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 html 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
32 Please note, that we're not aiming for complete docbook-xml support. All tags
33 used in the generated xml are of course handled. More tags used in handwritten
34 xml can be easilly supported, but for some combinations of tags we prefer
39 - inside 'footnote' one can have many tags, we only handle 'para'/'simpara'
40 - inside 'inlinemediaobject'/'mediaobject' a 'textobject' becomes the 'alt'
41 attr on the <img> tag of the 'imageobject'
42 - glossary/index: depending on the parents, the headings as h1/h2
43 - maybe track depth when chunking
44 - the part titles have a generated prefix, such as 'Part I:'
45 - replace get_title with a result.extend(convert_title(ctx, title_tag))
47 - check each docbook tag if it can contain #PCDATA, if not don't check for
49 - consider some perf-warnings flag
50 - see 'No "id" attribute on'
53 - minify html: https://pypi.python.org/pypi/htmlmin/
56 sudo pip3 install anytree lxml pygments
60 ../../../gtkdoc-mkhtml2 tester tester-docs.xml
61 xdg-open db2html/index.html
66 rm html-build.stamp; time make html-build.stamp
76 from anytree
import Node
, PreOrderIter
77 from copy
import deepcopy
79 from lxml
import etree
80 from pygments
import highlight
81 from pygments
.lexers
import CLexer
82 from pygments
.formatters
import HtmlFormatter
84 from . import config
, fixxref
87 # lazily constructed lexer cache
91 HTML_FORMATTER
= HtmlFormatter(nowrap
=True)
93 # http://www.sagehill.net/docbookxsl/Chunking.html
97 'bibliography', # in article or book
101 'glossary', # in article or book
102 'index', # in article or book
107 'sect1', # except first
108 'section', # if equivalent to sect1
114 class ChunkParams(object):
115 def __init__(self
, prefix
, parent
=None, min_idx
=0):
118 self
.min_idx
= min_idx
121 # TODO: look up the abbrevs and hierarchy for other tags
122 # http://www.sagehill.net/docbookxsl/Chunking.html#GeneratedFilenames
123 # https://github.com/oreillymedia/HTMLBook/blob/master/htmlbook-xsl/chunk.xsl#L33
125 # If not defined, we can just create an example without an 'id' attr and see
128 'appendix': ChunkParams('app', 'book'),
129 'book': ChunkParams('bk'),
130 'chapter': ChunkParams('ch', 'book'),
131 'index': ChunkParams('ix', 'book'),
132 'part': ChunkParams('pt', 'book'),
133 'preface': ChunkParams('pr', 'book'),
134 'reference': ChunkParams('rn', 'book'),
135 'sect1': ChunkParams('s', 'chapter', 1),
136 'section': ChunkParams('s', 'chapter', 1),
140 '_': (etree
.XPath('./title'), None),
141 'book': (etree
.XPath('./bookinfo/title'), None),
143 etree
.XPath('./refmeta/refentrytitle'),
144 etree
.XPath('./refnamediv/refpurpose')
148 ID_XPATH
= etree
.XPath('//@id')
150 GLOSSENTRY_XPATH
= etree
.XPath('//glossentry')
156 def get_chunk_min_idx(tag
):
157 if tag
not in CHUNK_PARAMS
:
160 return CHUNK_PARAMS
[tag
].min_idx
163 def gen_chunk_name(node
, idx
):
164 if 'id' in node
.attrib
:
165 return node
.attrib
['id']
168 if tag
not in CHUNK_PARAMS
:
169 CHUNK_PARAMS
[tag
] = ChunkParams(node
.tag
[:2])
170 logging
.warning('Add CHUNK_PARAMS for "%s"', tag
)
172 naming
= CHUNK_PARAMS
[tag
]
173 name
= ('%s%02d' % (naming
.prefix
, idx
))
174 # handle parents to make names of nested tags unique
175 # TODO: we only need to prepend the parent if there are > 1 of them in the
177 # while naming.parent:
178 # parent = naming.parent
179 # if parent not in CHUNK_PARAMS:
181 # naming = CHUNK_PARAMS[parent]
182 # name = ('%s%02d' % (naming.prefix, idx)) + name
183 logging
.info('Gen chunk name: "%s"', name
)
187 def get_chunk_titles(node
):
189 if tag
not in TITLE_XPATHS
:
191 (title
, subtitle
) = TITLE_XPATHS
['_']
193 (title
, subtitle
) = TITLE_XPATHS
[tag
]
199 if xml
.tag
!= 'title':
200 result
['title_tag'] = xml
.tag
202 result
['title_tag'] = tag
205 xml
= subtitle(node
)[0]
206 result
['subtitle'] = xml
.text
207 result
['subtitle_tag'] = xml
.tag
209 result
['subtitle'] = None
210 result
['subtitle_tag'] = None
214 def chunk(xml_node
, idx
=0, parent
=None):
217 The first time, we're called with parent=None and in that case we return
218 the new_node as the root of the tree
221 # also check idx to handle 'sect1'/'section' special casing
222 if tag
in CHUNK_TAGS
and idx
>= get_chunk_min_idx(tag
):
223 logging
.info('chunk tag: "%s"[%d]', tag
, idx
)
225 # remove the xml-node from the parent
226 sub_tree
= etree
.ElementTree(deepcopy(xml_node
)).getroot()
227 xml_node
.getparent().remove(xml_node
)
230 title_args
= get_chunk_titles(xml_node
)
231 chunk_name
= gen_chunk_name(xml_node
, (idx
+ 1))
232 parent
= Node(tag
, parent
=parent
, xml
=xml_node
,
233 filename
=chunk_name
+ '.html', **title_args
)
236 for child
in xml_node
:
237 new_parent
= chunk(child
, idx
, parent
)
238 if child
.tag
in CHUNK_TAGS
:
244 def add_id_links(files
, links
):
246 chunk_name
= node
.filename
[:-5]
247 chunk_base
= node
.filename
+ '#'
248 for attr
in ID_XPATH(node
.xml
):
249 if attr
== chunk_name
:
250 links
[attr
] = node
.filename
252 links
[attr
] = chunk_base
+ attr
255 def build_glossary(files
):
257 if node
.xml
.tag
!= 'glossary':
259 for term
in GLOSSENTRY_XPATH(node
.xml
):
260 # TODO: there can be all kind of things in a glossary. This only supports
261 # what we commonly use
262 key
= etree
.tostring(term
.find('glossterm'), method
="text", encoding
=str).strip()
263 value
= etree
.tostring(term
.find('glossdef'), method
="text", encoding
=str).strip()
264 glossary
[key
] = value
265 # logging.debug('glosentry: %s:%s', key, value)
271 def convert_inner(ctx
, xml
, result
):
273 result
.extend(convert_tags
.get(child
.tag
, convert__unknown
)(ctx
, child
))
276 def convert_ignore(ctx
, xml
):
278 convert_inner(ctx
, xml
, result
)
282 def convert_skip(ctx
, xml
):
286 def append_text(text
, result
):
287 if text
and text
.strip():
288 result
.append(text
.replace('<', '<').replace('>', '>'))
294 def convert__unknown(ctx
, xml
):
295 # don't recurse on subchunks
296 if xml
.tag
in CHUNK_TAGS
:
298 if isinstance(xml
, etree
._Comment
):
299 return ['<!-- ' + xml
.text
+ '-->\n']
302 if xml
.tag
not in missing_tags
:
303 logging
.warning('Add tag converter for "%s"', xml
.tag
)
304 missing_tags
[xml
.tag
] = True
305 result
= ['<!-- ' + xml
.tag
+ '-->\n']
306 convert_inner(ctx
, xml
, result
)
307 result
.append('<!-- /' + xml
.tag
+ '-->\n')
311 def convert_sect(ctx
, xml
, h_tag
, inner_func
=convert_inner
):
312 result
= ['<div class="%s">\n' % xml
.tag
]
313 title
= xml
.find('title')
314 if title
is not None:
315 if 'id' in xml
.attrib
:
316 result
.append('<a name="%s"></a>' % xml
.attrib
['id'])
317 result
.append('<%s>%s</%s>' % (h_tag
, title
.text
, h_tag
))
319 append_text(xml
.text
, result
)
320 inner_func(ctx
, xml
, result
)
321 result
.append('</div>')
322 append_text(xml
.tail
, result
)
326 def xml_get_title(xml
):
327 title
= xml
.find('title')
328 if title
is not None:
331 # TODO(ensonic): any way to get the file (inlcudes) too?
332 logging
.warning('%s: Expected title tag under "%s %s"', xml
.sourceline
, xml
.tag
, str(xml
.attrib
))
339 def convert_abstract(ctx
, xml
):
340 result
= ["""<div class="abstract">
341 <p class="title"><b>Abstract</b></p>"""]
342 append_text(xml
.text
, result
)
343 convert_inner(ctx
, xml
, result
)
344 result
.append('</div>')
345 append_text(xml
.tail
, result
)
349 def convert_acronym(ctx
, xml
):
351 title
= glossary
.get(key
, '')
352 # TODO: print a sensible warning if missing
353 result
= ['<acronym title="%s"><span class="acronym">%s</span></acronym>' % (title
, key
)]
355 result
.append(xml
.tail
)
359 def convert_anchor(ctx
, xml
):
360 return ['<a name="%s"></a>' % xml
.attrib
['id']]
363 def convert_bookinfo(ctx
, xml
):
364 result
= ['<div class="titlepage">']
365 convert_inner(ctx
, xml
, result
)
366 result
.append("""<hr>
369 result
.append(xml
.tail
)
373 def convert_blockquote(ctx
, xml
):
374 result
= ['<div class="blockquote">\n<blockquote class="blockquote">']
375 append_text(xml
.text
, result
)
376 convert_inner(ctx
, xml
, result
)
377 result
.append('</blockquote>\n</div>')
378 append_text(xml
.tail
, result
)
382 def convert_code(ctx
, xml
):
383 result
= ['<code class="%s">' % xml
.tag
]
384 append_text(xml
.text
, result
)
385 convert_inner(ctx
, xml
, result
)
386 result
.append('</code>')
387 append_text(xml
.tail
, result
)
391 def convert_colspec(ctx
, xml
):
395 result
.append(' class="%s"' % a
['colname'])
397 result
.append(' width="%s"' % a
['colwidth'])
399 # is in tgroup and there can be no 'text'
403 def convert_command(ctx
, xml
):
404 result
= ['<strong class="userinput"><code>']
405 append_text(xml
.text
, result
)
406 convert_inner(ctx
, xml
, result
)
407 result
.append('</code></strong>')
408 append_text(xml
.tail
, result
)
412 def convert_corpauthor(ctx
, xml
):
413 result
= ['<div><h3 class="corpauthor">\n']
414 append_text(xml
.text
, result
)
415 convert_inner(ctx
, xml
, result
)
416 result
.append('</h3></div>\n')
417 append_text(xml
.tail
, result
)
421 def convert_div(ctx
, xml
):
422 result
= ['<div class="%s">\n' % xml
.tag
]
423 append_text(xml
.text
, result
)
424 convert_inner(ctx
, xml
, result
)
425 result
.append('</div>')
426 append_text(xml
.tail
, result
)
430 def convert_em_class(ctx
, xml
):
431 result
= ['<em class="%s"><code>' % xml
.tag
]
432 append_text(xml
.text
, result
)
433 convert_inner(ctx
, xml
, result
)
434 result
.append('</code></em>')
435 append_text(xml
.tail
, result
)
439 def convert_entry(ctx
, xml
):
440 entry_type
= ctx
['table.entry']
441 result
= ['<' + entry_type
]
442 if 'role' in xml
.attrib
:
443 result
.append(' class="%s"' % xml
.attrib
['role'])
444 if 'morerows' in xml
.attrib
:
445 result
.append(' rowspan="%s"' % (1 + int(xml
.attrib
['morerows'])))
447 append_text(xml
.text
, result
)
448 convert_inner(ctx
, xml
, result
)
449 result
.append('</' + entry_type
+ '>')
450 append_text(xml
.tail
, result
)
454 def convert_footnote(ctx
, xml
):
455 footnotes
= ctx
.get('footnotes', [])
456 # footnotes idx is not per page, but per doc
461 # need a pair of ids for each footnote (docbook generates different ids)
462 this_id
= 'footnote-%d' % idx
463 that_id
= 'ftn.' + this_id
465 inner
= ['<div id="%s" class="footnote">' % that_id
]
466 inner
.append('<p><a href="#%s" class="para"><sup class="para">[%d] </sup></a>' % (
468 # TODO(ensonic): this can contain all kind of tags, if we convert them we'll
469 # get double nested paras :/.
470 # convert_inner(ctx, xml, inner)
471 para
= xml
.find('para')
473 para
= xml
.find('simpara')
475 inner
.append(para
.text
)
477 logging
.warning('%s: Unhandled footnote content: %s', xml
.sourceline
,
478 etree
.tostring(xml
, method
="text", encoding
=str).strip())
479 inner
.append('</p></div>')
480 footnotes
.append(inner
)
481 ctx
['footnotes'] = footnotes
482 return ['<a href="#%s" class="footnote" name="%s"><sup class="footnote">[%s]</sup></a>' % (
483 that_id
, this_id
, idx
)]
486 def convert_formalpara(ctx
, xml
):
488 title_tag
= xml
.find('title')
489 result
= ['<p><b>%s</b>' % title_tag
.text
]
490 para_tag
= xml
.find('para')
491 append_text(para_tag
.text
, result
)
492 convert_inner(ctx
, para_tag
, result
)
493 append_text(para_tag
.tail
, result
)
494 result
.append('</p>')
495 append_text(xml
.tail
, result
)
499 def convert_glossdef(ctx
, xml
):
500 result
= ['<dd class="glossdef">']
501 convert_inner(ctx
, xml
, result
)
502 result
.append('</dd>\n')
506 def convert_glossdiv(ctx
, xml
):
507 title_tag
= xml
.find('title')
508 title
= title_tag
.text
509 xml
.remove(title_tag
)
511 '<a name="gls%s"></a><h3 class="title">%s</h3>' % (title
, title
)
513 convert_inner(ctx
, xml
, result
)
517 def convert_glossentry(ctx
, xml
):
519 convert_inner(ctx
, xml
, result
)
523 def convert_glossterm(ctx
, xml
):
526 anchor
= xml
.find('anchor')
527 if anchor
is not None:
528 glossid
= anchor
.attrib
.get('id', '')
529 text
+= anchor
.tail
or ''
530 text
+= xml
.text
or ''
532 glossid
= 'glossterm-' + text
534 '<dt><span class="glossterm"><a name="%s"></a>%s</span></dt>' % (
539 def convert_imageobject(ctx
, xml
):
540 imagedata
= xml
.find('imagedata')
541 if imagedata
is not None:
542 # TODO(ensonic): warn on missing fileref attr?
543 return ['<img src="%s">' % imagedata
.attrib
.get('fileref', '')]
548 def convert_indexdiv(ctx
, xml
):
549 title_tag
= xml
.find('title')
550 title
= title_tag
.text
551 xml
.remove(title_tag
)
553 '<a name="idx%s"></a><h3 class="title">%s</h3>' % (title
, title
)
555 convert_inner(ctx
, xml
, result
)
559 def convert_informaltable(ctx
, xml
):
560 result
= ['<div class="informaltable"><table class="informaltable"']
562 if 'pgwide' in a
and a
['pgwide'] == '1':
563 result
.append(' width="100%"')
564 if 'frame' in a
and a
['frame'] == 'none':
565 result
.append(' border="0"')
567 convert_inner(ctx
, xml
, result
)
568 result
.append('</table></div>')
570 result
.append(xml
.tail
)
574 def convert_itemizedlist(ctx
, xml
):
575 result
= ['<div class="itemizedlist"><ul class="itemizedlist" style="list-style-type: disc; ">']
576 convert_inner(ctx
, xml
, result
)
577 result
.append('</ul></div>')
579 result
.append(xml
.tail
)
583 def convert_link(ctx
, xml
):
584 linkend
= xml
.attrib
['linkend']
585 if linkend
in fixxref
.NoLinks
:
590 convert_inner(ctx
, xml
, link_text
)
591 append_text(xml
.text
, link_text
)
592 # TODO: fixxref does some weird checks in xml.text
593 result
= [fixxref
.MakeXRef(ctx
['module'], '', 0, linkend
, ''.join(link_text
))]
594 append_text(xml
.tail
, result
)
598 def convert_listitem(ctx
, xml
):
599 result
= ['<li class="listitem">']
600 convert_inner(ctx
, xml
, result
)
601 result
.append('</li>')
602 # is in itemizedlist and there can be no 'text'
606 def convert_literallayout(ctx
, xml
):
607 result
= ['<div class="literallayout"><p><br>\n']
608 append_text(xml
.text
, result
)
609 convert_inner(ctx
, xml
, result
)
610 result
.append('</p></div>')
611 append_text(xml
.tail
, result
)
615 def convert_orderedlist(ctx
, xml
):
616 result
= ['<div class="orderedlistlist"><ol class="orderedlistlist" type="1">']
617 convert_inner(ctx
, xml
, result
)
618 result
.append('</ol></div>')
619 append_text(xml
.tail
, result
)
623 def convert_para(ctx
, xml
):
625 if 'id' in xml
.attrib
:
626 result
.append('<a name="%s"></a>' % xml
.attrib
['id'])
628 append_text(xml
.text
, result
)
629 convert_inner(ctx
, xml
, result
)
630 result
.append('</p>')
631 append_text(xml
.tail
, result
)
635 def convert_para_like(ctx
, xml
):
637 if 'id' in xml
.attrib
:
638 result
.append('<a name="%s"></a>' % xml
.attrib
['id'])
639 result
.append('<p class="%s">' % xml
.tag
)
640 append_text(xml
.text
, result
)
641 convert_inner(ctx
, xml
, result
)
642 result
.append('</p>')
643 append_text(xml
.tail
, result
)
647 def convert_phrase(ctx
, xml
):
649 if 'role' in xml
.attrib
:
650 result
.append(' class="%s">' % xml
.attrib
['role'])
653 append_text(xml
.text
, result
)
654 convert_inner(ctx
, xml
, result
)
655 result
.append('</span>')
656 append_text(xml
.tail
, result
)
660 def convert_primaryie(ctx
, xml
):
662 convert_inner(ctx
, xml
, result
)
663 result
.append('\n</dt>\n<dd></dd>\n')
667 def convert_pre(ctx
, xml
):
668 result
= ['<pre class="%s">\n' % xml
.tag
]
669 append_text(xml
.text
, result
)
670 convert_inner(ctx
, xml
, result
)
671 result
.append('</pre>')
672 append_text(xml
.tail
, result
)
676 def convert_programlisting(ctx
, xml
):
678 if xml
.attrib
.get('role', '') == 'example':
680 lang
= xml
.attrib
.get('language', 'c').lower()
681 if lang
not in LEXERS
:
682 LEXERS
[lang
] = get_lexer_by_name(lang
)
683 lexer
= LEXERS
.get(lang
, None)
685 highlighted
= highlight(xml
.text
, lexer
, HTML_FORMATTER
)
687 # we do own line-numbering
688 line_count
= highlighted
.count('\n')
689 source_lines
= '\n'.join([str(i
) for i
in range(1, line_count
+ 1)])
690 result
.append("""<table class="listing_frame" border="0" cellpadding="0" cellspacing="0">
693 <td class="listing_lines" align="right"><pre>%s</pre></td>
694 <td class="listing_code"><pre class="programlisting">%s</pre></td>
698 """ % (source_lines
, highlighted
))
700 logging
.warn('No pygments lexer for language="%s"', lang
)
701 result
.append('<pre class="programlisting">')
702 result
.append(xml
.text
)
703 result
.append('</pre>')
705 result
.append('<pre class="programlisting">')
706 append_text(xml
.text
, result
)
707 convert_inner(ctx
, xml
, result
)
708 result
.append('</pre>')
709 append_text(xml
.tail
, result
)
713 def convert_quote(ctx
, xml
):
714 result
= ['<span class="quote">"<span class="quote">']
715 append_text(xml
.text
, result
)
716 convert_inner(ctx
, xml
, result
)
717 result
.append('</span>"</span>')
718 append_text(xml
.tail
, result
)
722 def convert_refsect1(ctx
, xml
):
723 # Add a divider between two consequitive refsect2
724 def convert_inner(ctx
, xml
, result
):
727 if child
.tag
== 'refsect2' and prev
is not None and prev
.tag
== child
.tag
:
728 result
.append('<hr>\n')
729 result
.extend(convert_tags
.get(child
.tag
, convert__unknown
)(ctx
, child
))
731 return convert_sect(ctx
, xml
, 'h2', convert_inner
)
734 def convert_refsect2(ctx
, xml
):
735 return convert_sect(ctx
, xml
, 'h3')
738 def convert_refsect3(ctx
, xml
):
739 return convert_sect(ctx
, xml
, 'h4')
742 def convert_row(ctx
, xml
):
744 convert_inner(ctx
, xml
, result
)
745 result
.append('</tr>\n')
749 def convert_sect1_tag(ctx
, xml
):
750 return convert_sect(ctx
, xml
, 'h2')
753 def convert_sect2(ctx
, xml
):
754 return convert_sect(ctx
, xml
, 'h3')
757 def convert_sect3(ctx
, xml
):
758 return convert_sect(ctx
, xml
, 'h4')
761 def convert_simpara(ctx
, xml
):
763 append_text(xml
.text
, result
)
764 result
.append('</p>')
765 append_text(xml
.tail
, result
)
769 def convert_span(ctx
, xml
):
770 result
= ['<span class="%s">' % xml
.tag
]
771 append_text(xml
.text
, result
)
772 convert_inner(ctx
, xml
, result
)
773 result
.append('</span>')
774 append_text(xml
.tail
, result
)
778 def convert_table(ctx
, xml
):
779 result
= ['<div class="table">']
780 if 'id' in xml
.attrib
:
781 result
.append('<a name="%s"></a>' % xml
.attrib
['id'])
782 title_tag
= xml
.find('title')
783 if title_tag
is not None:
784 result
.append('<p class="title"><b>')
785 # TODO(ensonic): Add a 'Table X. ' prefix, needs a table counter
786 result
.extend(convert_title(ctx
, title_tag
))
787 result
.append('</b></p>')
788 xml
.remove(title_tag
)
789 result
.append('<div class="table-contents"><table class="table" summary="g_object_new" border="1">')
791 convert_inner(ctx
, xml
, result
)
793 result
.append('</table></div></div>')
794 append_text(xml
.tail
, result
)
798 def convert_tbody(ctx
, xml
):
800 ctx
['table.entry'] = 'td'
801 convert_inner(ctx
, xml
, result
)
802 result
.append('</tbody>')
803 # is in tgroup and there can be no 'text'
807 def convert_tgroup(ctx
, xml
):
808 # tgroup does not expand to anything, but the nested colspecs need to
809 # be put into a colgroup
810 cols
= xml
.findall('colspec')
813 result
.append('<colgroup>\n')
815 result
.extend(convert_colspec(ctx
, col
))
817 result
.append('</colgroup>\n')
818 convert_inner(ctx
, xml
, result
)
819 # is in informaltable and there can be no 'text'
823 def convert_thead(ctx
, xml
):
825 ctx
['table.entry'] = 'th'
826 convert_inner(ctx
, xml
, result
)
827 result
.append('</thead>')
828 # is in tgroup and there can be no 'text'
832 def convert_title(ctx
, xml
):
833 # This is always called from some context
835 append_text(xml
.text
, result
)
836 convert_inner(ctx
, xml
, result
)
837 append_text(xml
.tail
, result
)
841 def convert_ulink(ctx
, xml
):
842 result
= ['<a class="%s" href="%s">%s</a>' % (xml
.tag
, xml
.attrib
['url'], xml
.text
)]
844 result
.append(xml
.tail
)
848 def convert_userinput(ctx
, xml
):
849 result
= ['<span class="command"><strong>']
850 append_text(xml
.text
, result
)
851 convert_inner(ctx
, xml
, result
)
852 result
.append('</strong></span>')
853 append_text(xml
.tail
, result
)
857 def convert_variablelist(ctx
, xml
):
858 result
= ["""<div class="variablelist"><table border="0" class="variablelist">
860 <col align="left" valign="top">
864 convert_inner(ctx
, xml
, result
)
865 result
.append("""</tbody>
870 def convert_varlistentry(ctx
, xml
):
873 result
.append('<td><p>')
874 term
= xml
.find('term')
875 result
.extend(convert_span(ctx
, term
))
876 result
.append('</p></td>')
878 result
.append('<td>')
879 listitem
= xml
.find('listitem')
880 convert_inner(ctx
, listitem
, result
)
881 result
.append('</td>')
883 result
.append('<tr>')
887 # TODO(ensonic): turn into class with converters as functions and ctx as self
889 'abstract': convert_abstract
,
890 'acronym': convert_acronym
,
891 'anchor': convert_anchor
,
892 'application': convert_span
,
893 'bookinfo': convert_bookinfo
,
894 'blockquote': convert_blockquote
,
895 'caption': convert_div
,
896 'code': convert_code
,
897 'colspec': convert_colspec
,
898 'constant': convert_code
,
899 'command': convert_command
,
900 'corpauthor': convert_corpauthor
,
901 'emphasis': convert_span
,
902 'entry': convert_entry
,
903 'envar': convert_code
,
904 'footnote': convert_footnote
,
905 'filename': convert_code
,
906 'formalpara': convert_formalpara
,
907 'function': convert_code
,
908 'glossdef': convert_glossdef
,
909 'glossdiv': convert_glossdiv
,
910 'glossentry': convert_glossentry
,
911 'glossterm': convert_glossterm
,
912 'imageobject': convert_imageobject
,
913 'indexdiv': convert_indexdiv
,
914 'indexentry': convert_ignore
,
915 'indexterm': convert_skip
,
916 'informalexample': convert_div
,
917 'informaltable': convert_informaltable
,
918 'inlinemediaobject': convert_span
,
919 'itemizedlist': convert_itemizedlist
,
920 'legalnotice': convert_div
,
921 'link': convert_link
,
922 'listitem': convert_listitem
,
923 'literal': convert_code
,
924 'literallayout': convert_literallayout
,
925 'mediaobject': convert_div
,
927 'option': convert_code
,
928 'orderedlist': convert_orderedlist
,
929 'para': convert_para
,
930 'partintro': convert_div
,
931 'parameter': convert_em_class
,
932 'phrase': convert_phrase
,
933 'primaryie': convert_primaryie
,
934 'programlisting': convert_programlisting
,
935 'quote': convert_quote
,
936 'releaseinfo': convert_para_like
,
937 'refsect1': convert_refsect1
,
938 'refsect2': convert_refsect2
,
939 'refsect3': convert_refsect3
,
940 'replaceable': convert_em_class
,
941 'returnvalue': convert_span
,
943 'screen': convert_pre
,
944 'sect1': convert_sect1_tag
,
945 'sect2': convert_sect2
,
946 'sect3': convert_sect3
,
947 'simpara': convert_simpara
,
948 'structfield': convert_em_class
,
949 'structname': convert_span
,
950 'synopsis': convert_pre
,
951 'symbol': convert_span
,
952 'table': convert_table
,
953 'tbody': convert_tbody
,
954 'term': convert_span
,
955 'tgroup': convert_tgroup
,
956 'thead': convert_thead
,
957 'type': convert_span
,
958 'ulink': convert_ulink
,
959 'userinput': convert_userinput
,
960 'varname': convert_code
,
961 'variablelist': convert_variablelist
,
962 'varlistentry': convert_varlistentry
,
963 'warning': convert_div
,
968 HTML_HEADER
= """<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
971 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
973 %s<link rel="stylesheet" href="style.css" type="text/css">
975 <body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF">
979 def generate_head_links(ctx
):
982 '<link rel="home" href="%s" title="%s">\n' % (n
.filename
, n
.title
)
986 result
.append('<link rel="up" href="%s" title="%s">\n' % (n
.filename
, n
.title
))
987 if 'nav_prev' in ctx
:
989 result
.append('<link rel="prev" href="%s" title="%s">\n' % (n
.filename
, n
.title
))
990 if 'nav_next' in ctx
:
992 result
.append('<link rel="next" href="%s" title="%s">\n' % (n
.filename
, n
.title
))
993 return ''.join(result
)
996 def generate_nav_links(ctx
):
999 '<td><a accesskey="h" href="%s"><img src="home.png" width="16" height="16" border="0" alt="Home"></a></td>' % n
.filename
1004 '<td><a accesskey="u" href="%s"><img src="up.png" width="16" height="16" border="0" alt="Up"></a></td>' % n
.filename
)
1006 result
.append('<td><img src="up-insensitive.png" width="16" height="16" border="0"></td>')
1007 if 'nav_prev' in ctx
:
1010 '<td><a accesskey="p" href="%s"><img src="left.png" width="16" height="16" border="0" alt="Prev"></a></td>' % n
.filename
)
1012 result
.append('<td><img src="left-insensitive.png" width="16" height="16" border="0"></td>')
1013 if 'nav_next' in ctx
:
1016 '<td><a accesskey="n" href="%s"><img src="right.png" width="16" height="16" border="0" alt="Next"></a></td>' % n
.filename
)
1018 result
.append('<td><img src="right-insensitive.png" width="16" height="16" border="0"></td>')
1020 return ''.join(result
)
1023 def generate_toc(ctx
, node
):
1025 for c
in node
.children
:
1026 # TODO: urlencode the filename: urllib.parse.quote_plus()
1027 result
.append('<dt><span class="%s"><a href="%s">%s</a></span>\n' % (
1028 c
.title_tag
, c
.filename
, c
.title
))
1030 result
.append('<span class="%s"> — %s</span>' % (c
.subtitle_tag
, c
.subtitle
))
1031 result
.append('</dt>\n')
1033 result
.append('<dd><dl>')
1034 result
.extend(generate_toc(ctx
, c
))
1035 result
.append('</dl></dd>')
1039 def generate_basic_nav(ctx
):
1040 return """<table class="navigation" id="top" width="100%%" cellpadding="2" cellspacing="5">
1041 <tr valign="middle">
1042 <td width="100%%" align="left" class="shortcuts"></td>
1046 """ % generate_nav_links(ctx
)
1049 def generate_alpha_nav(ctx
, divs
, prefix
):
1052 title
= xml_get_title(s
)
1053 ix_nav
.append('<a class="shortcut" href="#%s%s">%s</a>' % (prefix
, title
, title
))
1055 return """<table class="navigation" id="top" width="100%%" cellpadding="2" cellspacing="5">
1056 <tr valign="middle">
1057 <td width="100%%" align="left" class="shortcuts">
1058 <span id="nav_index">
1065 """ % ('\n<span class="dim">|</span>\n'.join(ix_nav
), generate_nav_links(ctx
))
1068 def generate_refentry_nav(ctx
, refsect1s
, result
):
1069 result
.append("""<table class="navigation" id="top" width="100%" cellpadding="2" cellspacing="5">
1070 <tr valign="middle">
1071 <td width="100%" align="left" class="shortcuts">
1072 <a href="#" class="shortcut">Top</a>""")
1075 # don't list TOC sections (role="xxx_proto")
1076 if s
.attrib
.get('role', '').endswith("_proto"):
1078 # skip section without 'id' attrs
1079 if 'id' not in s
.attrib
:
1082 title
= xml_get_title(s
)
1084 Â Â <span class="dim">|</span>Â
1085 <a href="#%s" class="shortcut">%s</a>
1086 """ % (s
.attrib
['id'], title
))
1092 """ % generate_nav_links(ctx
))
1095 def generate_footer(ctx
):
1097 if 'footnotes' in ctx
:
1098 result
.append("""<div class="footnotes">\n
1099 <br><hr style="width:100; text-align:left;margin-left: 0">
1101 for f
in ctx
['footnotes']:
1103 result
.append('</div>\n')
1109 node_id
= xml
.attrib
.get('id', None)
1113 logging
.info('%d: No "id" attribute on "%s", generating one',
1114 xml
.sourceline
, xml
.tag
)
1116 # Generate the 'id'. We need to walk up the xml-tree and check the positions
1118 parent
= xml
.getparent()
1119 while parent
is not None:
1120 children
= parent
.getchildren()
1121 ix
.insert(0, str(children
.index(xml
) + 1))
1123 parent
= xml
.getparent()
1124 # logging.warning('%s: id indexes: %s', node.filename, str(ix))
1125 return 'id-1.' + '.'.join(ix
)
1128 def convert_chunk_with_toc(ctx
, div_class
, title_tag
):
1131 HTML_HEADER
% (node
.title
+ ": " + node
.root
.title
, generate_head_links(ctx
)),
1132 generate_basic_nav(ctx
),
1133 '<div class="%s">' % div_class
,
1135 title
= node
.xml
.find('title')
1136 if title
is not None:
1138 <div class="titlepage">
1139 <%s class="title"><a name="%s"></a>%s</%s>
1141 title_tag
, get_id(node
), title
.text
, title_tag
))
1142 node
.xml
.remove(title
)
1143 convert_inner(ctx
, node
.xml
, result
)
1144 result
.append("""<p>
1145 <b>Table of Contents</b>
1150 result
.extend(generate_toc(ctx
, node
))
1151 result
.append("""</dl>
1154 result
.extend(generate_footer(ctx
))
1155 result
.append("""</div>
1164 def convert_book(ctx
):
1167 HTML_HEADER
% (node
.title
, generate_head_links(ctx
)),
1168 """<table class="navigation" id="top" width="100%%" cellpadding="2" cellspacing="0">
1169 <tr><th valign="middle"><p class="title">%s</p></th></tr>
1174 bookinfo
= node
.xml
.findall('bookinfo')[0]
1175 # we already used the title
1176 title
= bookinfo
.find('title')
1177 if title
is not None:
1178 bookinfo
.remove(title
)
1179 result
.extend(convert_bookinfo(ctx
, bookinfo
))
1180 result
.append("""<div class="toc">
1183 result
.extend(generate_toc(ctx
, node
.root
))
1184 result
.append("""</dl>
1187 result
.extend(generate_footer(ctx
))
1188 result
.append("""</div>
1194 def convert_chapter(ctx
):
1195 return convert_chunk_with_toc(ctx
, 'chapter', 'h2')
1198 def convert_glossary(ctx
):
1200 glossdivs
= node
.xml
.findall('glossdiv')
1203 HTML_HEADER
% (node
.title
+ ": " + node
.root
.title
, generate_head_links(ctx
)),
1204 generate_alpha_nav(ctx
, glossdivs
, 'gls'),
1205 """<div class="glossary">
1206 <div class="titlepage"><h1 class="title">
1207 <a name="%s"></a>%s</h1>
1208 </div>""" % (get_id(node
), node
.title
)
1211 result
.extend(convert_glossdiv(ctx
, i
))
1212 result
.extend(generate_footer(ctx
))
1213 result
.append("""</div>
1219 def convert_index(ctx
):
1221 # Get all indexdivs under indexdiv
1222 indexdivs
= node
.xml
.find('indexdiv').findall('indexdiv')
1225 HTML_HEADER
% (node
.title
+ ": " + node
.root
.title
, generate_head_links(ctx
)),
1226 generate_alpha_nav(ctx
, indexdivs
, 'idx'),
1227 """<div class="index">
1228 <div class="titlepage"><h2 class="title">
1229 <a name="%s"></a>%s</h2>
1230 </div>""" % (get_id(node
), node
.title
)
1233 result
.extend(convert_indexdiv(ctx
, i
))
1234 result
.extend(generate_footer(ctx
))
1235 result
.append("""</div>
1241 def convert_part(ctx
):
1242 return convert_chunk_with_toc(ctx
, 'part', 'h1')
1245 def convert_preface(ctx
):
1248 HTML_HEADER
% (node
.title
+ ": " + node
.root
.title
, generate_head_links(ctx
)),
1249 generate_basic_nav(ctx
),
1250 '<div class="preface">'
1252 title
= node
.xml
.find('title')
1253 if title
is not None:
1255 <div class="titlepage">
1256 <h2 class="title"><a name="%s"></a>%s</h2>
1257 </div>""" % (get_id(node
), title
.text
))
1258 node
.xml
.remove(title
)
1259 convert_inner(ctx
, node
.xml
, result
)
1260 result
.extend(generate_footer(ctx
))
1261 result
.append("""</div>
1267 def convert_reference(ctx
):
1268 return convert_chunk_with_toc(ctx
, 'reference', 'h1')
1271 def convert_refentry(ctx
):
1273 node_id
= get_id(node
)
1274 refsect1s
= node
.xml
.findall('refsect1')
1277 HTML_HEADER
% (node
.title
+ ": " + node
.root
.title
, generate_head_links(ctx
))
1279 generate_refentry_nav(ctx
, refsect1s
, result
)
1281 <div class="refentry">
1283 <div class="refnamediv">
1284 <table width="100%%"><tr>
1286 <h2><span class="refentrytitle"><a name="%s.top_of_page"></a>%s</span></h2>
1287 <p>%s — module for gtk-doc unit test</p>
1289 <td class="gallery_image" valign="top" align="right"></td>
1292 """ % (node_id
, node_id
, node
.title
, node
.title
))
1295 result
.extend(convert_refsect1(ctx
, s
))
1296 result
.extend(generate_footer(ctx
))
1297 result
.append("""</div>
1303 def convert_sect1(ctx
):
1304 return convert_chunk_with_toc(ctx
, 'sect1', 'h2')
1307 # TODO(ensonic): turn into class with converters as functions and ctx as self
1309 'book': convert_book
,
1310 'chapter': convert_chapter
,
1311 'glossary': convert_glossary
,
1312 'index': convert_index
,
1313 'part': convert_part
,
1314 'preface': convert_preface
,
1315 'reference': convert_reference
,
1316 'refentry': convert_refentry
,
1317 'sect1': convert_sect1
,
1321 def generate_nav_nodes(files
, node
):
1323 'nav_home': node
.root
,
1325 # nav params: up, prev, next
1327 nav
['nav_up'] = node
.parent
1328 ix
= files
.index(node
)
1330 nav
['nav_prev'] = files
[ix
- 1]
1331 if ix
< len(files
) - 1:
1332 nav
['nav_next'] = files
[ix
+ 1]
1336 def convert(out_dir
, module
, files
, node
):
1337 """Convert the docbook chunks to a html file.
1340 out_dir: already created output dir
1341 files: list of nodes in the tree in pre-order
1342 node: current tree node
1345 logging
.info('Writing: %s', node
.filename
)
1346 with
open(os
.path
.join(out_dir
, node
.filename
), 'wt',
1347 newline
='\n', encoding
='utf-8') as html
:
1353 ctx
.update(generate_nav_nodes(files
, node
))
1355 if node
.name
in convert_chunks
:
1356 for line
in convert_chunks
[node
.name
](ctx
):
1359 logging
.warning('Add converter/template for "%s"', node
.name
)
1362 def create_devhelp2_toc(node
):
1364 for c
in node
.children
:
1366 result
.append('<sub name="%s" link="%s">\n' % (c
.title
, c
.filename
))
1367 result
.extend(create_devhelp2_toc(c
))
1368 result
.append('</sub>\n')
1370 result
.append('<sub name="%s" link="%s"/>\n' % (c
.title
, c
.filename
))
1374 def create_devhelp2_condition_attribs(node
):
1375 if 'condition' in node
.attrib
:
1376 # condition -> since, deprecated, ... (separated with '|')
1377 cond
= node
.attrib
['condition'].replace('"', '"').split('|')
1381 keywords
.append('{}="{}"'.format(*c
.split(':', 1)))
1383 # deprecated can have no description
1384 keywords
.append('{}="{}"'.format(c
, ''))
1385 return ' ' + ' '.join(keywords
)
1390 def create_devhelp2_refsect2_keyword(node
, base_link
):
1391 return' <keyword type="%s" name="%s" link="%s"%s/>\n' % (
1392 node
.attrib
['role'], xml_get_title(node
), base_link
+ node
.attrib
['id'],
1393 create_devhelp2_condition_attribs(node
))
1396 def create_devhelp2_refsect3_keyword(node
, base_link
, title
, name
):
1397 return' <keyword type="%s" name="%s" link="%s"%s/>\n' % (
1398 node
.attrib
['role'], title
, base_link
+ name
,
1399 create_devhelp2_condition_attribs(node
))
1402 def create_devhelp2(out_dir
, module
, xml
, files
):
1403 with
open(os
.path
.join(out_dir
, module
+ '.devhelp2'), 'wt',
1404 newline
='\n', encoding
='utf-8') as idx
:
1405 bookinfo_nodes
= xml
.xpath('/book/bookinfo')
1407 if bookinfo_nodes
is not None:
1408 bookinfo
= bookinfo_nodes
[0]
1409 title
= bookinfo
.xpath('./title/text()')[0]
1410 online_url
= bookinfo
.xpath('./releaseinfo/ulink[@role="online-location"]/@url')[0]
1411 # TODO: support author too (see devhelp2.xsl)
1412 # TODO: fixxref uses '--src-lang' to set the language
1414 """<?xml version="1.0" encoding="utf-8" standalone="no"?>
1415 <book xmlns="http://www.devhelp.net/book" title="%s" link="index.html" author="" name="%s" version="2" language="c" online="%s">
1417 """ % (title
, module
, online_url
)
1420 result
.extend(create_devhelp2_toc(files
[0].root
))
1421 result
.append(""" </chapters>
1424 # keywords from all refsect2 and refsect3
1425 refsect2
= etree
.XPath('//refsect2[@role]')
1426 refsect3_enum
= etree
.XPath('refsect3[@role="enum_members"]/informaltable/tgroup/tbody/row[@role="constant"]')
1427 refsect3_enum_details
= etree
.XPath('entry[@role="enum_member_name"]/para')
1428 refsect3_struct
= etree
.XPath('refsect3[@role="struct_members"]/informaltable/tgroup/tbody/row[@role="member"]')
1429 refsect3_struct_details
= etree
.XPath('entry[@role="struct_member_name"]/para/structfield')
1431 base_link
= node
.filename
+ '#'
1432 refsect2_nodes
= refsect2(node
.xml
)
1433 for refsect2_node
in refsect2_nodes
:
1434 result
.append(create_devhelp2_refsect2_keyword(refsect2_node
, base_link
))
1435 refsect3_nodes
= refsect3_enum(refsect2_node
)
1436 for refsect3_node
in refsect3_nodes
:
1437 details_node
= refsect3_enum_details(refsect3_node
)[0]
1438 name
= details_node
.attrib
['id']
1439 result
.append(create_devhelp2_refsect3_keyword(refsect3_node
, base_link
, details_node
.text
, name
))
1440 refsect3_nodes
= refsect3_struct(refsect2_node
)
1441 for refsect3_node
in refsect3_nodes
:
1442 details_node
= refsect3_struct_details(refsect3_node
)[0]
1443 name
= details_node
.attrib
['id']
1444 result
.append(create_devhelp2_refsect3_keyword(refsect3_node
, base_link
, name
, name
))
1446 result
.append(""" </functions>
1453 def get_dirs(uninstalled
):
1455 # this does not work from buiddir!=srcdir
1456 gtkdocdir
= os
.path
.split(sys
.argv
[0])[0]
1457 if not os
.path
.exists(gtkdocdir
+ '/gtk-doc.xsl'):
1458 # try 'srcdir' (set from makefiles) too
1459 if os
.path
.exists(os
.environ
.get("ABS_TOP_SRCDIR", '') + '/gtk-doc.xsl'):
1460 gtkdocdir
= os
.environ
['ABS_TOP_SRCDIR']
1461 styledir
= gtkdocdir
+ '/style'
1463 gtkdocdir
= os
.path
.join(config
.datadir
, 'gtk-doc/data')
1464 styledir
= gtkdocdir
1465 return (gtkdocdir
, styledir
)
1468 def main(module
, index_file
, out_dir
, uninstalled
):
1469 tree
= etree
.parse(index_file
)
1472 (gtkdocdir
, styledir
) = get_dirs(uninstalled
)
1473 # copy navigation images and stylesheets to html directory ...
1474 css_file
= os
.path
.join(styledir
, 'style.css')
1475 for f
in glob(os
.path
.join(styledir
, '*.png')) + [css_file
]:
1476 shutil
.copy(f
, out_dir
)
1477 css_file
= os
.path
.join(out_dir
, 'style.css')
1478 with
open(css_file
, 'at', newline
='\n', encoding
='utf-8') as css
:
1479 css
.write(HTML_FORMATTER
.get_style_defs())
1481 # TODO: migrate options from fixxref
1482 # TODO: do in parallel with loading the xml above.
1483 fixxref
.LoadIndicies(out_dir
, '/usr/share/gtk-doc/html', [])
1485 # We do multiple passes:
1486 # 1) recursively walk the tree and chunk it into a python tree so that we
1487 # can generate navigation and link tags.
1488 files
= chunk(tree
.getroot())
1489 files
= list(PreOrderIter(files
))
1490 # 2) extract tables:
1491 # TODO: use multiprocessing
1492 # - find all 'id' attribs and add them to the link map
1493 add_id_links(files
, fixxref
.Links
)
1494 # - build glossary dict
1495 build_glossary(files
)
1497 # 3) create a xxx.devhelp2 file, do this before 3), since we modify the tree
1498 create_devhelp2(out_dir
, module
, tree
.getroot(), files
)
1499 # 4) iterate the tree and output files
1500 # TODO: use multiprocessing
1502 convert(out_dir
, module
, files
, node
)
1506 logging
.info('options: %s', str(options
.__dict
__))
1507 module
= options
.args
[0]
1508 document
= options
.args
[1]
1510 # TODO: rename to 'html' later on
1511 # - right now in mkhtml, the dir is created by the Makefile and mkhtml
1512 # outputs into the working directory
1513 out_dir
= os
.path
.join(os
.path
.dirname(document
), 'db2html')
1516 except OSError as e
:
1517 if e
.errno
!= errno
.EEXIST
:
1520 sys
.exit(main(module
, document
, out_dir
, options
.uninstalled
))