Merge "citrix" branch into "master".
[sliver-openvswitch.git] / ovsdb / ovsdb-doc.in
1 #! @PYTHON@
2
3 from datetime import date
4 import getopt
5 import os
6 import re
7 import sys
8 import xml.dom.minidom
9
10 sys.path.insert(0, "@abs_top_srcdir@/ovsdb")
11 import simplejson as json
12
13 from OVSDB import *
14
15 argv0 = sys.argv[0]
16
17 def textToNroff(s):
18     def escape(match):
19         c = match.group(0)
20         if c == '\\':
21             return r'\e'
22         elif c == '"':
23             return r'\(dq'
24         elif c == "'":
25             return r'\(cq'
26         else:
27             raise Error("bad escape")
28
29     s = re.sub('([\\\\"\'])', escape, s)
30     if s.startswith('.'):
31         s = '\\' + s
32     return s
33
34 def escapeNroffLiteral(s):
35     return r'\fB%s\fR' % textToNroff(s)
36
37 def inlineXmlToNroff(node, font):
38     if node.nodeType == node.TEXT_NODE:
39         return textToNroff(node.data)
40     elif node.nodeType == node.ELEMENT_NODE:
41         if node.tagName == 'code' or node.tagName == 'em':
42             s = r'\fB'
43             for child in node.childNodes:
44                 s += inlineXmlToNroff(child, r'\fB')
45             return s + font
46         elif node.tagName == 'ref':
47             s = r'\fB'
48             if node.hasAttribute('column'):
49                 s += node.attributes['column'].nodeValue
50             elif node.hasAttribute('table'):
51                 s += node.attributes['table'].nodeValue
52             elif node.hasAttribute('group'):
53                 s += node.attributes['group'].nodeValue
54             else:
55                 raise Error("'ref' lacks column and table attributes")
56             return s + font
57         elif node.tagName == 'var':
58             s = r'\fI'
59             for child in node.childNodes:
60                 s += inlineXmlToNroff(child, r'\fI')
61             return s + font
62         else:
63             raise Error("element <%s> unknown or invalid here" % node.tagName)
64     else:
65         raise Error("unknown node %s in inline xml" % node)
66
67 def blockXmlToNroff(nodes, para='.PP'):
68     s = ''
69     for node in nodes:
70         if node.nodeType == node.TEXT_NODE:
71             s += textToNroff(node.data)
72             s = s.lstrip()
73         elif node.nodeType == node.ELEMENT_NODE:
74             if node.tagName == 'ul':
75                 if s != "":
76                     s += "\n"
77                 s += ".RS\n"
78                 for liNode in node.childNodes:
79                     if (liNode.nodeType == node.ELEMENT_NODE
80                         and liNode.tagName == 'li'):
81                         s += ".IP \\(bu\n" + blockXmlToNroff(liNode.childNodes, ".IP")
82                     elif (liNode.nodeType != node.TEXT_NODE
83                           or not liNode.data.isspace()):
84                         raise Error("<ul> element may only have <li> children")
85                 s += ".RE\n"
86             elif node.tagName == 'dl':
87                 if s != "":
88                     s += "\n"
89                 s += ".RS\n"
90                 prev = "dd"
91                 for liNode in node.childNodes:
92                     if (liNode.nodeType == node.ELEMENT_NODE
93                         and liNode.tagName == 'dt'):
94                         if prev == 'dd':
95                             s += '.TP\n'
96                         else:
97                             s += '.TQ\n'
98                         prev = 'dt'
99                     elif (liNode.nodeType == node.ELEMENT_NODE
100                           and liNode.tagName == 'dd'):
101                         if prev == 'dd':
102                             s += '.IP\n'
103                         prev = 'dd'
104                     elif (liNode.nodeType != node.TEXT_NODE
105                           or not liNode.data.isspace()):
106                         raise Error("<dl> element may only have <dt> and <dd> children")
107                     s += blockXmlToNroff(liNode.childNodes, ".IP")
108                 s += ".RE\n"
109             elif node.tagName == 'p':
110                 if s != "":
111                     if not s.endswith("\n"):
112                         s += "\n"
113                     s += para + "\n"
114                 s += blockXmlToNroff(node.childNodes, para)
115             else:
116                 s += inlineXmlToNroff(node, r'\fR')
117         else:
118             raise Error("unknown node %s in block xml" % node)
119     if s != "" and not s.endswith('\n'):
120         s += '\n'
121     return s
122
123 def typeAndConstraintsToNroff(column):
124     type = column.type.toEnglish(escapeNroffLiteral)
125     constraints = column.type.constraintsToEnglish(escapeNroffLiteral)
126     if constraints:
127         type += ", " + constraints
128     return type
129
130 def columnToNroff(columnName, column, node):
131     type = typeAndConstraintsToNroff(column)
132     s = '.IP "\\fB%s\\fR: %s"\n' % (columnName, type)
133     s += blockXmlToNroff(node.childNodes, '.IP') + "\n"
134     return s
135
136 def columnGroupToNroff(table, groupXml):
137     introNodes = []
138     columnNodes = []
139     for node in groupXml.childNodes:
140         if (node.nodeType == node.ELEMENT_NODE
141             and node.tagName in ('column', 'group')):
142             columnNodes += [node]
143         else:
144             introNodes += [node]
145
146     summary = []
147     intro = blockXmlToNroff(introNodes)
148     body = ''
149     for node in columnNodes:
150         if node.tagName == 'column':
151             columnName = node.attributes['name'].nodeValue
152             column = table.columns[columnName]
153             body += columnToNroff(columnName, column, node)
154             summary += [('column', columnName, column)]
155         elif node.tagName == 'group':
156             title = node.attributes["title"].nodeValue
157             subSummary, subIntro, subBody = columnGroupToNroff(table, node)
158             summary += [('group', title, subSummary)]
159             body += '.ST "%s:"\n' % textToNroff(title)
160             body += subIntro + subBody
161         else:
162             raise Error("unknown element %s in <table>" % node.tagName)
163     return summary, intro, body
164
165 def tableSummaryToNroff(summary, level=0):
166     s = ""
167     for type, name, arg in summary:
168         if type == 'column':
169             
170             s += "%s\\fB%s\\fR\tT{\n%s\nT}\n" % (
171                 r'\ \ ' * level, name, typeAndConstraintsToNroff(arg))
172         else:
173             if s != "":
174                 s += "_\n"
175             s += """.T&
176 li | s
177 l | l.
178 %s%s
179 _
180 """ % (r'\ \ ' * level, name)
181             s += tableSummaryToNroff(arg, level + 1)
182     return s
183
184 def tableToNroff(schema, tableXml):
185     tableName = tableXml.attributes['name'].nodeValue
186     table = schema.tables[tableName]
187
188     s = """.bp
189 .SS "%s Table"
190 """ % tableName
191     summary, intro, body = columnGroupToNroff(table, tableXml)
192     s += intro
193
194     s += r"""
195 .sp
196 .ce 1
197 \fB%s\fR Table Columns:
198 .TS
199 center box;
200 l | l.
201 Column  Type
202 =
203 """ % tableName
204     s += tableSummaryToNroff(summary)
205     s += ".TE\n"
206
207     s += body
208     return s
209
210 def docsToNroff(schemaFile, xmlFile, title=None):
211     schema = DbSchema.fromJson(json.load(open(schemaFile, "r")))
212     doc = xml.dom.minidom.parse(xmlFile).documentElement
213
214     schemaDate = os.stat(schemaFile).st_mtime
215     xmlDate = os.stat(xmlFile).st_mtime
216     d = date.fromtimestamp(max(schemaDate, xmlDate))
217     
218     if title == None:
219         title = schema.name
220
221     s = r'''.TH %s 5 "%s" "Open vSwitch" "Open vSwitch Manual"
222 .\" -*- nroff -*-
223 .de TQ
224 .  br
225 .  ns
226 .  TP "\\$1"
227 ..
228 .de ST
229 .  PP
230 .  RS -0.15in
231 .  I "\\$1"
232 .  RE
233 ..
234 ''' % (title, d.strftime("%B %Y"))
235
236     s += '.SH "%s DATABASE"\n' % schema.name
237
238     tables = ""
239     introNodes = []
240     tableNodes = []
241     summary = []
242     for dbNode in doc.childNodes:
243         if (dbNode.nodeType == dbNode.ELEMENT_NODE
244             and dbNode.tagName == "table"):
245             tableNodes += [dbNode]
246
247             name = dbNode.attributes['name'].nodeValue
248             if dbNode.hasAttribute("title"):
249                 title = dbNode.attributes['title'].nodeValue
250             else:
251                 title = name + " configuration."
252             summary += [(name, title)]
253         else:
254             introNodes += [dbNode]
255
256     s += blockXmlToNroff(introNodes) + "\n"
257     tableSummary = r"""
258 .sp
259 .ce 1
260 \fB%s\fR Database Tables:
261 .TS
262 center box;
263 l | l
264 lb | l.
265 Table   Purpose
266 =
267 """ % schema.name
268     for name, title in summary:
269         tableSummary += "%s\t%s\n" % (name, textToNroff(title))
270     tableSummary += '.TE\n'
271     s += tableSummary
272     for node in tableNodes:
273         s += tableToNroff(schema, node) + "\n"
274     return s
275
276 def usage():
277     print """\
278 %(argv0)s: ovsdb schema documentation generator
279 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
280 usage: %(argv0)s [OPTIONS] SCHEMA XML
281 where SCHEMA is an OVSDB schema in JSON format
282   and XML is OVSDB documentation in XML format.
283
284 The following options are also available:
285   --title=TITLE               use TITLE as title instead of schema name
286   -h, --help                  display this help message
287   -V, --version               display version information\
288 """ % {'argv0': argv0}
289     sys.exit(0)
290
291 if __name__ == "__main__":
292     try:
293         try:
294             options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
295                                               ['title=', 'help', 'version'])
296         except getopt.GetoptError, geo:
297             sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
298             sys.exit(1)
299
300         title = None
301         for key, value in options:
302             if key == '--title':
303                 title = value
304             elif key in ['-h', '--help']:
305                 usage()
306             elif key in ['-V', '--version']:
307                 print "ovsdb-doc (Open vSwitch) @VERSION@"
308             else:
309                 sys.exit(0)
310             
311         if len(args) != 2:
312             sys.stderr.write("%s: exactly 2 non-option arguments required "
313                              "(use --help for help)\n" % argv0)
314             sys.exit(1)
315         
316         # XXX we should warn about undocumented tables or columns
317         s = docsToNroff(args[0], args[1])
318         for line in s.split("\n"):
319             line = line.strip()
320             if len(line):
321                 print line
322             
323     except Error, e:
324         sys.stderr.write("%s: %s\n" % (argv0, e.msg))
325         sys.exit(1)
326
327 # Local variables:
328 # mode: python
329 # End: