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