#! @PYTHON@ from datetime import date import getopt import os import re import sys import xml.dom.minidom import ovs.json from ovs.db import error import ovs.db.schema argv0 = sys.argv[0] def textToNroff(s, font=r'\fR'): def escape(match): c = match.group(0) if c == '-': if font == r'\fB': return r'\-' else: return '-' if c == '\\': return r'\e' elif c == '"': return r'\(dq' elif c == "'": return r'\(cq' else: raise error.Error("bad escape") # Escape - \ " ' as needed by nroff. s = re.sub('([-"\'\\\\])', escape, s) if s.startswith('.'): s = '\\' + s return s def escapeNroffLiteral(s): return r'\fB%s\fR' % textToNroff(s, r'\fB') def inlineXmlToNroff(node, font): if node.nodeType == node.TEXT_NODE: return textToNroff(node.data, font) elif node.nodeType == node.ELEMENT_NODE: if node.tagName in ['code', 'em', 'option']: s = r'\fB' for child in node.childNodes: s += inlineXmlToNroff(child, r'\fB') return s + font elif node.tagName == 'ref': s = r'\fB' if node.hasAttribute('column'): s += node.attributes['column'].nodeValue elif node.hasAttribute('table'): s += node.attributes['table'].nodeValue elif node.hasAttribute('group'): s += node.attributes['group'].nodeValue else: raise error.Error("'ref' lacks column and table attributes") return s + font elif node.tagName == 'var': s = r'\fI' for child in node.childNodes: s += inlineXmlToNroff(child, r'\fI') return s + font else: raise error.Error("element <%s> unknown or invalid here" % node.tagName) else: raise error.Error("unknown node %s in inline xml" % node) def blockXmlToNroff(nodes, para='.PP'): s = '' for node in nodes: if node.nodeType == node.TEXT_NODE: s += textToNroff(node.data) s = s.lstrip() elif node.nodeType == node.ELEMENT_NODE: if node.tagName in ['ul', 'ol']: if s != "": s += "\n" s += ".RS\n" i = 0 for liNode in node.childNodes: if (liNode.nodeType == node.ELEMENT_NODE and liNode.tagName == 'li'): i += 1 if node.tagName == 'ul': s += ".IP \\bu\n" else: s += ".IP %d. .25in\n" % i s += blockXmlToNroff(liNode.childNodes, ".IP") elif (liNode.nodeType != node.TEXT_NODE or not liNode.data.isspace()): raise error.Error("<%s> element may only have
  • children" % node.tagName) s += ".RE\n" elif node.tagName == 'dl': if s != "": s += "\n" s += ".RS\n" prev = "dd" for liNode in node.childNodes: if (liNode.nodeType == node.ELEMENT_NODE and liNode.tagName == 'dt'): if prev == 'dd': s += '.TP\n' else: s += '.TQ\n' prev = 'dt' elif (liNode.nodeType == node.ELEMENT_NODE and liNode.tagName == 'dd'): if prev == 'dd': s += '.IP\n' prev = 'dd' elif (liNode.nodeType != node.TEXT_NODE or not liNode.data.isspace()): raise error.Error("
    element may only have
    and
    children") s += blockXmlToNroff(liNode.childNodes, ".IP") s += ".RE\n" elif node.tagName == 'p': if s != "": if not s.endswith("\n"): s += "\n" s += para + "\n" s += blockXmlToNroff(node.childNodes, para) else: s += inlineXmlToNroff(node, r'\fR') else: raise error.Error("unknown node %s in block xml" % node) if s != "" and not s.endswith('\n'): s += '\n' return s def typeAndConstraintsToNroff(column): type = column.type.toEnglish(escapeNroffLiteral) constraints = column.type.constraintsToEnglish(escapeNroffLiteral) if constraints: type += ", " + constraints return type def columnToNroff(columnName, column, node): type = typeAndConstraintsToNroff(column) s = '.IP "\\fB%s\\fR: %s"\n' % (columnName, type) s += blockXmlToNroff(node.childNodes, '.IP') + "\n" return s def columnGroupToNroff(table, groupXml): introNodes = [] columnNodes = [] for node in groupXml.childNodes: if (node.nodeType == node.ELEMENT_NODE and node.tagName in ('column', 'group')): columnNodes += [node] else: introNodes += [node] summary = [] intro = blockXmlToNroff(introNodes) body = '' for node in columnNodes: if node.tagName == 'column': columnName = node.attributes['name'].nodeValue column = table.columns[columnName] body += columnToNroff(columnName, column, node) summary += [('column', columnName, column)] elif node.tagName == 'group': title = node.attributes["title"].nodeValue subSummary, subIntro, subBody = columnGroupToNroff(table, node) summary += [('group', title, subSummary)] body += '.ST "%s:"\n' % textToNroff(title) body += subIntro + subBody else: raise error.Error("unknown element %s in " % node.tagName) return summary, intro, body def tableSummaryToNroff(summary, level=0): s = "" for type, name, arg in summary: if type == 'column': s += "%s\\fB%s\\fR\tT{\n%s\nT}\n" % ( r'\ \ ' * level, name, typeAndConstraintsToNroff(arg)) else: if s != "": s += "_\n" s += """.T& li | s l | l. %s%s _ """ % (r'\ \ ' * level, name) s += tableSummaryToNroff(arg, level + 1) return s def tableToNroff(schema, tableXml): tableName = tableXml.attributes['name'].nodeValue table = schema.tables[tableName] s = """.bp .SS "%s Table" """ % tableName summary, intro, body = columnGroupToNroff(table, tableXml) s += intro s += r""" .sp .ce 1 \fB%s\fR Table Columns: .TS center box; l | l. Column Type = """ % tableName s += tableSummaryToNroff(summary) s += ".TE\n" s += body return s def docsToNroff(schemaFile, xmlFile, erFile, title=None): schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile)) doc = xml.dom.minidom.parse(xmlFile).documentElement schemaDate = os.stat(schemaFile).st_mtime xmlDate = os.stat(xmlFile).st_mtime d = date.fromtimestamp(max(schemaDate, xmlDate)) if title == None: title = schema.name # Putting '\" pt as the first line tells "man" that the manpage # needs to be preprocessed by "pic" and "tbl". s = r''''\" pt .TH %s 5 "%s" "Open vSwitch" "Open vSwitch Manual" .\" -*- nroff -*- .de TQ . br . ns . TP "\\$1" .. .de ST . PP . RS -0.15in . I "\\$1" . RE .. ''' % (title, d.strftime("%B %Y")) s += '.SH "%s DATABASE"\n' % schema.name tables = "" introNodes = [] tableNodes = [] summary = [] for dbNode in doc.childNodes: if (dbNode.nodeType == dbNode.ELEMENT_NODE and dbNode.tagName == "table"): tableNodes += [dbNode] name = dbNode.attributes['name'].nodeValue if dbNode.hasAttribute("title"): title = dbNode.attributes['title'].nodeValue else: title = name + " configuration." summary += [(name, title)] else: introNodes += [dbNode] s += blockXmlToNroff(introNodes) + "\n" tableSummary = r""" .sp .ce 1 \fB%s\fR Database Tables: .TS center box; l | l lb | l. Table Purpose = """ % schema.name for name, title in summary: tableSummary += "%s\t%s\n" % (name, textToNroff(title)) tableSummary += '.TE\n' s += tableSummary if erFile: s += """ .if !'\*[.T]'ascii' \{ .sp 1 .SH "TABLE RELATIONSHIPS" .PP The following diagram shows the relationship among tables in the database. Each node represents a table. Tables that are part of the ``root set'' are shown with double borders. Each edge leads from the table that contains it and points to the table that its value represents. Edges are labeled with their column names, followed by a constraint on the number of allowed values: \\fB?\\fR for zero or one, \\fB*\\fR for zero or more, \\fB+\\fR for one or more. Thick lines represent strong references; thin lines represent weak references. .RS -1in """ erStream = open(erFile, "r") for line in erStream: s += line + '\n' erStream.close() s += ".RE\\}\n" for node in tableNodes: s += tableToNroff(schema, node) + "\n" return s def usage(): print """\ %(argv0)s: ovsdb schema documentation generator Prints documentation for an OVSDB schema as an nroff-formatted manpage. usage: %(argv0)s [OPTIONS] SCHEMA XML where SCHEMA is an OVSDB schema in JSON format and XML is OVSDB documentation in XML format. The following options are also available: --er-diagram=DIAGRAM.PIC include E-R diagram from DIAGRAM.PIC --title=TITLE use TITLE as title instead of schema name -h, --help display this help message -V, --version display version information\ """ % {'argv0': argv0} sys.exit(0) if __name__ == "__main__": try: try: options, args = getopt.gnu_getopt(sys.argv[1:], 'hV', ['er-diagram=', 'title=', 'help', 'version']) except getopt.GetoptError, geo: sys.stderr.write("%s: %s\n" % (argv0, geo.msg)) sys.exit(1) er_diagram = None title = None for key, value in options: if key == '--er-diagram': er_diagram = value elif key == '--title': title = value elif key in ['-h', '--help']: usage() elif key in ['-V', '--version']: print "ovsdb-doc (Open vSwitch) @VERSION@" else: sys.exit(0) if len(args) != 2: sys.stderr.write("%s: exactly 2 non-option arguments required " "(use --help for help)\n" % argv0) sys.exit(1) # XXX we should warn about undocumented tables or columns s = docsToNroff(args[0], args[1], er_diagram) for line in s.split("\n"): line = line.strip() if len(line): print line except error.Error, e: sys.stderr.write("%s: %s\n" % (argv0, e.msg)) sys.exit(1) # Local variables: # mode: python # End: