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