1#!/usr/bin/python
2
3# Audio Tools, a module and set of tools for manipulating audio data
4# Copyright (C) 2007-2014  Brian Langenberger
5
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
19
20import re
21import time
22from sys import version_info
23
24PY3 = version_info[0] >= 3
25
26
27WHITESPACE = re.compile(r'\s+')
28
29
30def subtag(node, name):
31    return [child for child in node.childNodes
32            if (hasattr(child, "nodeName") and
33                (child.nodeName == name))][0]
34
35
36def subtags(node, name):
37    return [child for child in node.childNodes
38            if (hasattr(child, "nodeName") and
39                (child.nodeName == name))]
40
41
42def text(node):
43    try:
44        return WHITESPACE.sub(u" ", node.childNodes[0].wholeText.strip())
45    except IndexError:
46        return u""
47
48
49def man_escape(s):
50    return s.replace('-', '\\-')
51
52
53if PY3:
54    def write_u(stream, unicode_string):
55        assert(isinstance(unicode_string, str))
56        stream.write(unicode_string)
57else:
58    def write_u(stream, unicode_string):
59        assert(isinstance(unicode_string, unicode))
60        stream.write(unicode_string.encode("utf-8"))
61
62
63class Manpage:
64    FIELDS = [
65        ("%(track_number)2.2d", "the track's number on the CD"),
66        ("%(track_total)d", "the total number of tracks on the CD"),
67        ("%(album_number)d", "the CD's album number"),
68        ("%(album_total)d", "the total number of CDs in the set"),
69        ("%(album_track_number)s", "combination of album and track number"),
70        ("%(track_name)s", "the track's name"),
71        ("%(album_name)s", "the album's name"),
72        ("%(artist_name)s", "the track's artist name"),
73        ("%(performer_name)s", "the track's performer name"),
74        ("%(composer_name)s", "the track's composer name"),
75        ("%(conductor_name)s", "the track's conductor name"),
76        ("%(media)s", "the track's source media"),
77        ("%(ISRC)s", "the track's ISRC"),
78        ("%(catalog)s", "the track's catalog number"),
79        ("%(copyright)s", "the track's copyright information"),
80        ("%(publisher)s", "the track's publisher"),
81        ("%(year)s", "the track's publication year"),
82        ("%(date)s", "the track's original recording date"),
83        ("%(suffix)s", "the track's suffix"),
84        ("%(basename)s", "the track's original name, without suffix")]
85
86    def __init__(self,
87                 utility=u"",
88                 section=1,
89                 name=u"",
90                 title=u"",
91                 synopsis=None,
92                 description=u"",
93                 author=u"",
94                 options=None,
95                 elements=None,
96                 examples=None,
97                 see_also=None):
98        self.utility = utility
99        self.section = int(section)
100        self.name = name
101        self.title = title
102        self.synopsis = synopsis
103        self.description = description
104        self.author = author
105
106        if options is not None:
107            self.options = options
108        else:
109            self.options = []
110
111        if examples is not None:
112            self.examples = examples
113        else:
114            self.examples = []
115
116        if elements is not None:
117            self.elements = elements
118        else:
119            self.elements = []
120
121        if see_also is not None:
122            self.see_also = see_also
123        else:
124            self.see_also = []
125
126    def __repr__(self):
127        return "Manpage(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)" % \
128            (repr(self.utility),
129             repr(self.section),
130             repr(self.name),
131             repr(self.title),
132             repr(self.synopsis),
133             repr(self.description),
134             repr(self.author),
135             repr(self.options),
136             repr(self.elements),
137             repr(self.examples),
138             repr(self.see_also))
139
140    def flatten_options(self):
141        for option_category in self.options:
142            for option in option_category.options:
143                yield option
144
145    @classmethod
146    def parse_file(cls, filename):
147        return cls.parse(xml.dom.minidom.parse(filename))
148
149    @classmethod
150    def parse(cls, xml_dom):
151        manpage = xml_dom.getElementsByTagName(u"manpage")[0]
152
153        try:
154            synopsis = text(subtag(manpage, u"synopsis"))
155        except IndexError:
156            synopsis = None
157
158        options = [Options.parse(options)
159                   for options in subtags(manpage, u"options")]
160
161        elements = [Element.parse(element)
162                    for element in subtags(manpage, u"element")]
163
164        try:
165            examples = [Example.parse(example)
166                        for example in subtags(subtag(manpage,
167                                                      u"examples"),
168                                               u"example")]
169        except IndexError:
170            examples = None
171
172        return cls(utility=text(subtag(manpage, u"utility")),
173                   section=text(subtag(manpage, u"section")),
174                   name=text(subtag(manpage, u"name")),
175                   title=text(subtag(manpage, u"title")),
176                   synopsis=synopsis,
177                   description=text(subtag(manpage, u"description")),
178                   author=text(subtag(manpage, u"author")),
179                   options=options,
180                   elements=elements,
181                   examples=examples)
182
183    def to_man(self, stream):
184        write_u(stream,
185                (u".TH \"%(utility)s\" %(section)d " +
186                 u"\"%(date)s\" \"\" \"%(title)s\"\n") %
187                {"utility": self.utility.upper(),
188                 "section": self.section,
189                 "date": time.strftime("%B %Y", time.localtime()),
190                 "title": self.title})
191        write_u(stream, u".SH NAME\n")
192        write_u(stream, u"%(utility)s \\- %(name)s\n" %
193                {"utility": self.utility,
194                 "name": self.name})
195        if self.synopsis is not None:
196            write_u(stream, u".SH SYNOPSIS\n")
197            write_u(stream, u"%(utility)s %(synopsis)s\n" %
198                    {"utility": self.utility,
199                     "synopsis": self.synopsis})
200        write_u(stream, u".SH DESCRIPTION\n")
201        write_u(stream, u".PP\n")
202        write_u(stream, self.description)
203        write_u(stream, u"\n")
204        for option in self.options:
205            option.to_man(stream)
206        for element in self.elements:
207            element.to_man(stream)
208        if len(self.examples) > 0:
209            if len(self.examples) > 1:
210                write_u(stream, u".SH EXAMPLES\n")
211            else:
212                write_u(stream, u".SH EXAMPLE\n")
213            for example in self.examples:
214                example.to_man(stream)
215
216        for option in self.flatten_options():
217            if option.long_arg == 'format':
218                self.format_fields_man(stream)
219                break
220
221        self.see_also.sort(key=lambda x: x.utility)
222
223        if len(self.see_also) > 0:
224            write_u(stream, u".SH SEE ALSO\n")
225            # handle the trailing comma correctly
226            for page in self.see_also[0:-1]:
227                write_u(stream, u".BR %(utility)s (%(section)d),\n" %
228                        {"utility": page.utility,
229                         "section": page.section})
230
231            write_u(stream, u".BR %(utility)s (%(section)d)\n" %
232                    {"utility": self.see_also[-1].utility,
233                     "section": self.see_also[-1].section})
234
235        write_u(stream, u".SH AUTHOR\n")
236        write_u(stream, u"%(author)s\n" % {"author": self.author})
237
238    def format_fields_man(self, stream):
239        write_u(stream, u".SH FORMAT STRING FIELDS\n")
240        write_u(stream, u".TS\n")
241        write_u(stream, u"tab(:);\n")
242        write_u(stream, u"| c   s |\n")
243        write_u(stream, u"| c | c |\n")
244        write_u(stream, u"| r | l |.\n")
245        write_u(stream, u"_\n")
246        write_u(stream, u"Template Fields\n")
247        write_u(stream, u"Key:Value\n")
248        write_u(stream, u"=\n")
249        for (field, description) in self.FIELDS:
250            write_u(stream, u"\\fC%(field)s\\fR:%(description)s\n" %
251                    {"field": field,
252                     "description": description})
253        write_u(stream, u"_\n")
254        write_u(stream, u".TE\n")
255
256    def to_html(self, stream):
257        write_u(stream, u'<div class="utility" id="%s">\n' % (self.utility))
258
259        # display utility name
260        write_u(stream, u"<h2>%s</h2>\n" % (self.utility))
261
262        # display utility description
263        write_u(stream, u"<p>%s</p>\n" % (self.description))
264
265        # display options
266        for option_section in self.options:
267            option_section.to_html(stream)
268
269        # display additional sections
270        for element in self.elements:
271            element.to_html(stream)
272
273        # display examples
274        if len(self.examples) > 0:
275            write_u(stream, u'<dl class="examples">\n')
276            if len(self.examples) > 1:
277                write_u(stream, u"<dt>Examples</dt>\n")
278            else:
279                write_u(stream, u"<dt>Example</dt>\n")
280            write_u(stream, u"<dd>\n")
281            for example in self.examples:
282                example.to_html(stream)
283            write_u(stream, u"</dd>\n")
284            write_u(stream, u"</dl>\n")
285
286        write_u(stream, u'</div>\n')
287
288
289class Options:
290    def __init__(self, options, category=None):
291        self.options = options
292        self.category = category
293
294    def __repr__(self):
295        return "Options(%s, %s)" % \
296            (self.options,
297             self.category)
298
299    @classmethod
300    def parse(cls, xml_dom):
301        if xml_dom.hasAttribute(u"category"):
302            category = xml_dom.getAttribute(u"category")
303        else:
304            category = None
305
306        return cls(options=[Option.parse(child) for child in
307                            subtags(xml_dom, u"option")],
308                   category=category)
309
310    def to_man(self, stream):
311        if self.category is None:
312            write_u(stream, u".SH OPTIONS\n")
313        else:
314            write_u(stream, u".SH %(category)s OPTIONS\n" %
315                    {"category": self.category.upper()})
316        for option in self.options:
317            option.to_man(stream)
318
319    def to_html(self, stream):
320        write_u(stream, u"<dl>\n")
321        if self.category is None:
322            write_u(stream, u"<dt>Options</dt>\n")
323        else:
324            write_u(stream, u"<dt>%s Options</dt>\n" %
325                    self.category.capitalize())
326        write_u(stream, u"<dd>\n")
327        write_u(stream, u"<dl>\n")
328        for option in self.options:
329            option.to_html(stream)
330        write_u(stream, u"</dl>\n")
331        write_u(stream, u"</dd>\n")
332        write_u(stream, u"</dl>\n")
333
334
335class Option:
336    def __init__(self,
337                 short_arg=None,
338                 long_arg=None,
339                 arg_name=None,
340                 description=None):
341        self.short_arg = short_arg
342        self.long_arg = long_arg
343        self.arg_name = arg_name
344        self.description = description
345
346    def __repr__(self):
347        return "Option(%s, %s, %s, %s)" % \
348            (repr(self.short_arg),
349             repr(self.long_arg),
350             repr(self.arg_name),
351             repr(self.description))
352
353    @classmethod
354    def parse(cls, xml_dom):
355        if xml_dom.hasAttribute("short"):
356            short_arg = xml_dom.getAttribute("short")
357            if len(short_arg) > 1:
358                raise ValueError("short arguments should be 1 character")
359        else:
360            short_arg = None
361
362        if xml_dom.hasAttribute("long"):
363            long_arg = xml_dom.getAttribute("long")
364        else:
365            long_arg = None
366
367        if xml_dom.hasAttribute("arg"):
368            arg_name = xml_dom.getAttribute("arg")
369        else:
370            arg_name = None
371
372        if len(xml_dom.childNodes) > 0:
373            description = WHITESPACE.sub(
374                u" ", xml_dom.childNodes[0].wholeText.strip())
375        else:
376            description = None
377
378        return cls(short_arg=short_arg,
379                   long_arg=long_arg,
380                   arg_name=arg_name,
381                   description=description)
382
383    def to_man(self, stream):
384        write_u(stream, u".TP\n")
385        if (self.short_arg is not None) and (self.long_arg is not None):
386            if self.arg_name is not None:
387                write_u(stream,
388                        (u"\\fB\\-%(short_arg)s\\fR, " +
389                         u"\\fB\\-\\-%(long_arg)s\\fR=" +
390                         u"\\fI%(arg_name)s\\fR\n") %
391                        {"short_arg": man_escape(self.short_arg),
392                         "long_arg": man_escape(self.long_arg),
393                         "arg_name": man_escape(self.arg_name.upper())})
394            else:
395                write_u(stream,
396                        (u"\\fB\\-%(short_arg)s\\fR, " +
397                         u"\\fB\\-\\-%(long_arg)s\\fR\n") %
398                        {"short_arg": man_escape(self.short_arg),
399                         "long_arg": man_escape(self.long_arg)})
400        elif self.short_arg is not None:
401            if self.arg_name is not None:
402                write_u(stream,
403                        (u"\\fB\\-%(short_arg)s\\fR " +
404                         u"\\fI%(arg_name)s\\fR\n") %
405                        {"short_arg": man_escape(self.short_arg),
406                         "arg_name": man_escape(self.arg_name.upper())})
407            else:
408                write_u(stream,
409                        u"\\fB\\-%(short_arg)s\\fR\n" %
410                        {"short_arg": man_escape(self.short_arg)})
411        elif self.long_arg is not None:
412            if self.arg_name is not None:
413                write_u(stream,
414                        (u"\\fB\\-\\-%(long_arg)s\\fR" +
415                         u"=\\fI%(arg_name)s\\fR\n") %
416                        {"long_arg": man_escape(self.long_arg),
417                         "arg_name": man_escape(self.arg_name.upper())})
418            else:
419                write_u(stream,
420                        u"\\fB\\-\\-%(long_arg)s\\fR\n" %
421                        {"long_arg": man_escape(self.long_arg)})
422        else:
423            raise ValueError("short arg or long arg must be present in option")
424
425        if self.description is not None:
426            write_u(stream, self.description)
427            write_u(stream, u"\n")
428
429    def to_html(self, stream):
430        write_u(stream, u"<dt>\n")
431        if (self.short_arg is not None) and (self.long_arg is not None):
432            if self.arg_name is not None:
433                write_u(stream,
434                        (u"<b>-%(short_arg)s</b>, " +
435                         u"<b>--%(long_arg)s</b>=" +
436                         u"<i>%(arg_name)s</i>\n") %
437                        {"short_arg": self.short_arg,
438                         "long_arg": self.long_arg,
439                         "arg_name": man_escape(self.arg_name.upper())})
440            else:
441                write_u(stream,
442                        (u"<b>-%(short_arg)s</b>, " +
443                         u"<b>--%(long_arg)s</b>\n") %
444                        {"short_arg": self.short_arg,
445                         "long_arg": self.long_arg})
446        elif self.short_arg is not None:
447            if self.arg_name is not None:
448                write_u(stream,
449                        (u"<b>-%(short_arg)s</b> " +
450                         u"<i>%(arg_name)s</i>\n") %
451                        {"short_arg": self.short_arg,
452                         "arg_name": self.arg_name.upper()})
453            else:
454                write_u(stream,
455                        u"<b>-%(short_arg)s\n" % {"short_arg": self.short_arg})
456        elif self.long_arg is not None:
457            if self.arg_name is not None:
458                write_u(stream,
459                        (u"<b>--%(long_arg)s</b>" +
460                         u"=<i>%(arg_name)s</i>\n") %
461                        {"long_arg": self.long_arg,
462                         "arg_name": self.arg_name.upper()})
463            else:
464                write_u(stream,
465                        u"<b>--%(long_arg)s</b>\n" %
466                        {"long_arg": self.long_arg})
467        else:
468            raise ValueError("short arg or long arg must be present in option")
469
470        write_u(stream, u"</dt>\n")
471
472        if self.description is not None:
473            write_u(stream, u"<dd>%s</dd>\n" % (self.description))
474        else:
475            write_u(stream, u"<dd></dd>\n")
476
477
478class Example:
479    def __init__(self,
480                 description=u"",
481                 commands=[]):
482        self.description = description
483        self.commands = commands
484
485    def __repr__(self):
486        return "Example(%s, %s)" % \
487            (repr(self.description),
488             repr(self.commands))
489
490    @classmethod
491    def parse(cls, xml_dom):
492        return cls(description=text(subtag(xml_dom, u"description")),
493                   commands=map(Command.parse, subtags(xml_dom, u"command")))
494
495    def to_man(self, stream):
496        write_u(stream, u".LP\n")
497        write_u(stream, self.description)  # FIXME
498        write_u(stream, u"\n")
499        for command in self.commands:
500            command.to_man(stream)
501
502    def to_html(self, stream):
503        write_u(stream, u'<dl>\n')
504        write_u(stream, u"<dt>%s</dt>\n" % (self.description))
505        write_u(stream, u"<dd>\n")
506        for command in self.commands:
507            command.to_html(stream)
508        write_u(stream, u"</dd>\n")
509        write_u(stream, u"</dl>\n")
510
511
512class Command:
513    def __init__(self, commandline, note=None):
514        self.commandline = commandline
515        self.note = note
516
517    def __repr__(self):
518        return "Command(%s, %s)" % (repr(self.commandline),
519                                    repr(self.note))
520
521    @classmethod
522    def parse(cls, xml_dom):
523        if xml_dom.hasAttribute(u"note"):
524            note = xml_dom.getAttribute(u"note")
525        else:
526            note = None
527
528        return cls(commandline=text(xml_dom),
529                   note=note)
530
531    def to_man(self, stream):
532        if self.note is not None:
533            write_u(stream, u".LP\n")
534            write_u(stream, self.note + u" :\n\n")
535
536        write_u(stream, u".IP\n")
537        write_u(stream, self.commandline)
538        write_u(stream, u"\n\n")
539
540    def to_html(self, stream):
541        if self.note is not None:
542            write_u(stream,
543                    u'<span class="note">%s :</span><br>\n' % (self.note))
544
545        write_u(stream, self.commandline)
546        write_u(stream, u"<br>\n")
547
548
549class Element_P:
550    def __init__(self, contents):
551        self.contents = contents
552
553    def __repr__(self):
554        return "Element_P(%s)" % (repr(self.contents))
555
556    @classmethod
557    def parse(cls, xml_dom):
558        return cls(contents=text(xml_dom))
559
560    def to_man(self, stream):
561        write_u(stream, self.contents)
562        write_u(stream, u"\n.PP\n")
563
564    def to_html(self, stream):
565        write_u(stream, u"<p>%s</p>" % (self.contents))
566
567
568class Element_UL:
569    def __init__(self, list_items):
570        self.list_items = list_items
571
572    def __repr__(self):
573        return "Element_UL(%s)" % (repr(self.list_items))
574
575    @classmethod
576    def parse(cls, xml_dom):
577        return cls(list_items=map(text, subtags(xml_dom, u"li")))
578
579    def to_man(self, stream):
580        for item in self.list_items:
581            write_u(stream, u"\\[bu] ")
582            write_u(stream, item)
583            write_u(stream, u"\n")
584            write_u(stream, u".PP\n")
585
586    def to_html(self, stream):
587        write_u(stream, u"<ul>\n")
588        for item in self.list_items:
589            write_u(stream, u"<li>%s</li>\n" % (item))
590        write_u(stream, u"</ul>\n")
591
592
593class Element_TABLE:
594    def __init__(self, rows):
595        self.rows = rows
596
597    def __repr__(self):
598        return "Element_TABLE(%s)" % (repr(self.rows))
599
600    @classmethod
601    def parse(cls, xml_dom):
602        return cls(rows=[Element_TR.parse(tr) for tr in subtags(xml_dom,
603                                                                u"tr")])
604
605    def to_man(self, stream):
606        if len(self.rows) == 0:
607            return
608
609        if (len(set([len(row.columns) for row in self.rows
610                     if row.tr_class in (TR_NORMAL, TR_HEADER)])) != 1):
611            raise ValueError("all rows must have the same number of columns")
612        else:
613            columns = len(self.rows[0].columns)
614
615        write_u(stream, u".TS\n")
616        write_u(stream, u"tab(:);\n")
617        write_u(stream,
618                u" ".join([u"l" for l in self.rows[0].columns]) + u".\n")
619        for row in self.rows:
620            row.to_man(stream)
621        write_u(stream, u".TE\n")
622
623    def to_html(self, stream):
624        if len(self.rows) == 0:
625            return
626
627        if (len({len(row.columns) for row in self.rows
628                 if row.tr_class in (TR_NORMAL, TR_HEADER)}) != 1):
629            raise ValueError("all rows must have the same number of columns")
630
631        write_u(stream, u"<table>\n")
632        for (row, spans) in zip(self.rows, self.calculate_row_spans()):
633            row.to_html(stream, spans)
634        write_u(stream, u"</table>\n")
635
636    def calculate_row_spans(self):
637        # turn rows into arrays of "span" boolean values
638        row_spans = []
639        for row in self.rows:
640            if row.tr_class in (TR_NORMAL, TR_HEADER):
641                row_spans.append([col.empty() for col in row.columns])
642            elif row.tr_class == TR_DIVIDER:
643                row_spans.append([False] * len(row_spans[-1]))
644
645        # turn columns into arrays of integers containing the row span
646        columns = [list(self.calculate_span_column([row[i] for
647                                                    row in row_spans]))
648                   for i in xrange(len(row_spans[0]))]
649
650        # turn columns back into rows and return them
651        return zip(*columns)
652
653    def calculate_span_column(self, row_spans):
654        rows = None
655        for span in row_spans:
656            if span:
657                rows += 1
658            else:
659                if rows is not None:
660                    yield rows
661                    for i in xrange(rows - 1):
662                        yield 0
663                rows = 1
664
665        if rows is not None:
666            yield rows
667            for i in xrange(rows - 1):
668                yield 0
669
670
671(TR_NORMAL, TR_HEADER, TR_DIVIDER) = range(3)
672
673
674class Element_TR:
675    def __init__(self, columns, tr_class):
676        self.columns = columns
677        self.tr_class = tr_class
678
679    def __repr__(self):
680        if self.tr_class in (TR_NORMAL, TR_HEADER):
681            return "Element_TR(%s, %s)" % (repr(self.columns), self.tr_class)
682        else:
683            return "Element_TR_DIVIDER()"
684
685    @classmethod
686    def parse(cls, xml_dom):
687        if xml_dom.hasAttribute("class"):
688            if xml_dom.getAttribute("class") == "header":
689                return cls(columns=[Element_TD.parse(tag)
690                                    for tag in subtags(xml_dom, u"td")],
691                           tr_class=TR_HEADER)
692            elif xml_dom.getAttribute("class") == "divider":
693                return cls(columns=None, tr_class=TR_DIVIDER)
694            else:
695                raise ValueError("unsupported class \"%s\"" %
696                                 (xmldom_getAttribute("class")))
697        else:
698            return cls(columns=[Element_TD.parse(tag)
699                                for tag in subtags(xml_dom, u"td")],
700                       tr_class=TR_NORMAL)
701
702    def to_man(self, stream):
703        if self.tr_class == TR_NORMAL:
704            write_u(stream,
705                    u":".join(column.string() for column in self.columns) +
706                    u"\n")
707        elif self.tr_class == TR_HEADER:
708            write_u(stream,
709                    u":".join(u"\\fB%s\\fR" % (column.string())
710                              for column in self.columns) +
711                    u"\n")
712            write_u(stream, u"_\n")
713        elif self.tr_class == TR_DIVIDER:
714            write_u(stream, u"_\n")
715
716    def column_widths(self):
717        if self.tr_class in (TR_NORMAL, TR_HEADER):
718            return [column.width() for column in self.columns]
719        else:
720            return None
721
722    def to_html(self, stream, rowspans):
723        if self.tr_class in (TR_NORMAL, TR_HEADER):
724            write_u(stream, u"<tr>\n")
725            for (column, span) in zip(self.columns, rowspans):
726                column.to_html(stream, self.tr_class == TR_HEADER, span)
727            write_u(stream, u"</tr>\n")
728
729
730class Element_TD:
731    def __init__(self, value):
732        self.value = value
733
734    def __repr__(self):
735        return "Element_TD(%s)" % (repr(self.value))
736
737    @classmethod
738    def parse(cls, xml_dom):
739        try:
740            return cls(value=WHITESPACE.sub(
741                       u" ",
742                       xml_dom.childNodes[0].wholeText.strip()))
743        except IndexError:
744            return cls(value=None)
745
746    def empty(self):
747        return self.value is None
748
749    def string(self):
750        if self.value is not None:
751            return self.value.encode('ascii')
752        else:
753            return "\\^"
754
755    def to_man(self, stream):
756        stream.write(self.value)
757
758    def width(self):
759        if self.value is not None:
760            return len(self.value)
761        else:
762            return 0
763
764    def to_html(self, stream, header, rowspan):
765        if self.value is not None:
766            if rowspan > 1:
767                rowspan = u" rowspan=\"%d\"" % (rowspan)
768            else:
769                rowspan = u""
770
771            if header:
772                write_u(stream,
773                        u"<th%s>%s</th>" % (rowspan, self.value))
774            else:
775                write_u(stream,
776                        u"<td%s>%s</td>" % (rowspan, self.value))
777
778
779class Element:
780    SUB_ELEMENTS = {u"p": Element_P,
781                    u"ul": Element_UL,
782                    u"table": Element_TABLE}
783
784    def __init__(self, name, elements):
785        self.name = name
786        self.elements = elements
787
788    def __repr__(self):
789        return "Element(%s, %s)" % (repr(self.name), repr(self.elements))
790
791    @classmethod
792    def parse(cls, xml_dom):
793        if xml_dom.hasAttribute(u"name"):
794            name = xml_dom.getAttribute(u"name")
795        else:
796            raise ValueError("elements must have names")
797
798        elements = []
799        for child in xml_dom.childNodes:
800            if hasattr(child, "tagName"):
801                if child.tagName in cls.SUB_ELEMENTS.keys():
802                    elements.append(
803                        cls.SUB_ELEMENTS[child.tagName].parse(child))
804                else:
805                    raise ValueError("unsupported tag %s" %
806                                     (child.tagName.encode('ascii')))
807
808        return cls(name=name,
809                   elements=elements)
810
811    def to_man(self, stream):
812        write_u(stream, u".SH %s\n" % (self.name.upper()))
813        for element in self.elements:
814            element.to_man(stream)
815
816    def to_html(self, stream):
817        write_u(stream, u"<dl>\n")
818        write_u(stream,
819                u"<dt>%s</dt>\n" %
820                (u" ".join(part.capitalize() for part in self.name.split())))
821        write_u(stream, u"<dd>\n")
822        for element in self.elements:
823            element.to_html(stream)
824        write_u(stream, u"</dd>\n")
825        write_u(stream, u"</dl>\n")
826
827
828if (__name__ == '__main__'):
829    import sys
830    import xml.dom.minidom
831    import argparse
832
833    parser = argparse.ArgumentParser(description="manual page generator")
834
835    parser.add_argument("-i", "--input",
836                        dest="input",
837                        help="the primary input XML file")
838
839    parser.add_argument("-t", "--type",
840                        dest="type",
841                        choices=("man", "html"),
842                        default="man",
843                        help="the output type")
844
845    parser.add_argument("see_also",
846                        metavar="FILENAME",
847                        nargs="*",
848                        help="\"see also\" man pages")
849
850    options = parser.parse_args()
851
852    if options.input is not None:
853        main_page = Manpage.parse_file(options.input)
854        all_pages = [Manpage.parse_file(filename)
855                     for filename in options.see_also]
856        main_page.see_also = [page for page in all_pages
857                              if (page.utility != main_page.utility)]
858
859        if options.type == "man":
860            main_page.to_man(sys.stdout)
861        elif options.type == "html":
862            main_page.to_html(sys.stdout)
863