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