ovsdb-doc: Factor out nroff formatting into a separate Python module.
[cascardo/ovs.git] / python / build / nroff.py
1 # Copyright (c) 2010, 2011, 2012, 2015 Nicira, Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at:
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 import re
16
17 from ovs.db import error
18
19 def textToNroff(s, font=r'\fR'):
20     def escape(match):
21         c = match.group(0)
22
23         # In Roman type, let -- in XML be \- in nroff.  That gives us a way to
24         # write minus signs, which is important in some places in manpages.
25         #
26         # Bold in nroff usually represents literal text, where there's no
27         # distinction between hyphens and minus sign.  The convention in nroff
28         # appears to be to use a minus sign in such cases, so we follow that
29         # convention.
30         #
31         # Finally, we always output - as a minus sign when it is followed by a
32         # digit.
33         if c.startswith('-'):
34             if c == '--' and font == r'\fR':
35                 return r'\-'
36             if c != '-' or font in (r'\fB', r'\fL'):
37                 return c.replace('-', r'\-')
38             else:
39                 return '-'
40
41         if c == '\\':
42             return r'\e'
43         elif c == '"':
44             return r'\(dq'
45         elif c == "'":
46             return r'\(cq'
47         elif c == ".":
48             # groff(7) says that . can be escaped by \. but in practice groff
49             # still gives an error with \. at the beginning of a line.
50             return font + "."
51         else:
52             raise error.Error("bad escape")
53
54     # Escape - \ " ' . as needed by nroff.
55     s = re.sub('(-[0-9]|--|[-"\'\\\\.])', escape, s)
56     return s
57
58 def escapeNroffLiteral(s, font=r'\fB'):
59     return font + r'%s\fR' % textToNroff(s, font)
60
61 def inlineXmlToNroff(node, font, to_upper=False):
62     if node.nodeType == node.TEXT_NODE:
63         if to_upper:
64             return textToNroff(node.data.upper(), font)
65         else:
66             return textToNroff(node.data, font)
67     elif node.nodeType == node.ELEMENT_NODE:
68         if node.tagName in ['code', 'em', 'option']:
69             s = r'\fB'
70             for child in node.childNodes:
71                 s += inlineXmlToNroff(child, r'\fB')
72             return s + font
73         elif node.tagName == 'ref':
74             s = r'\fB'
75             if node.hasAttribute('column'):
76                 s += node.attributes['column'].nodeValue
77                 if node.hasAttribute('key'):
78                     s += ':' + node.attributes['key'].nodeValue
79             elif node.hasAttribute('table'):
80                 s += node.attributes['table'].nodeValue
81             elif node.hasAttribute('group'):
82                 s += node.attributes['group'].nodeValue
83             elif node.hasAttribute('db'):
84                 s += node.attributes['db'].nodeValue
85             else:
86                 raise error.Error("'ref' lacks required attributes: %s" % node.attributes.keys())
87             return s + font
88         elif node.tagName == 'var' or node.tagName == 'dfn':
89             s = r'\fI'
90             for child in node.childNodes:
91                 s += inlineXmlToNroff(child, r'\fI')
92             return s + font
93         else:
94             raise error.Error("element <%s> unknown or invalid here" % node.tagName)
95     else:
96         raise error.Error("unknown node %s in inline xml" % node)
97
98 def pre_to_nroff(nodes, para, font):
99     s = para + '\n.nf\n'
100     for node in nodes:
101         if node.nodeType != node.TEXT_NODE:
102             fatal("<pre> element may only have text children")
103         for line in node.data.split('\n'):
104             s += escapeNroffLiteral(line, font) + '\n.br\n'
105     s += '.fi\n'
106     return s
107
108 def blockXmlToNroff(nodes, para='.PP'):
109     s = ''
110     for node in nodes:
111         if node.nodeType == node.TEXT_NODE:
112             s += textToNroff(node.data)
113             s = s.lstrip()
114         elif node.nodeType == node.ELEMENT_NODE:
115             if node.tagName in ['ul', 'ol']:
116                 if s != "":
117                     s += "\n"
118                 s += ".RS\n"
119                 i = 0
120                 for liNode in node.childNodes:
121                     if (liNode.nodeType == node.ELEMENT_NODE
122                         and liNode.tagName == 'li'):
123                         i += 1
124                         if node.tagName == 'ul':
125                             s += ".IP \\(bu\n"
126                         else:
127                             s += ".IP %d. .25in\n" % i
128                         s += blockXmlToNroff(liNode.childNodes, ".IP")
129                     elif (liNode.nodeType != node.TEXT_NODE
130                           or not liNode.data.isspace()):
131                         raise error.Error("<%s> element may only have <li> children" % node.tagName)
132                 s += ".RE\n"
133             elif node.tagName == 'dl':
134                 if s != "":
135                     s += "\n"
136                 s += ".RS\n"
137                 prev = "dd"
138                 for liNode in node.childNodes:
139                     if (liNode.nodeType == node.ELEMENT_NODE
140                         and liNode.tagName == 'dt'):
141                         if prev == 'dd':
142                             s += '.TP\n'
143                         else:
144                             s += '.TQ .5in\n'
145                         prev = 'dt'
146                     elif (liNode.nodeType == node.ELEMENT_NODE
147                           and liNode.tagName == 'dd'):
148                         if prev == 'dd':
149                             s += '.IP\n'
150                         prev = 'dd'
151                     elif (liNode.nodeType != node.TEXT_NODE
152                           or not liNode.data.isspace()):
153                         raise error.Error("<dl> element may only have <dt> and <dd> children")
154                     s += blockXmlToNroff(liNode.childNodes, ".IP")
155                 s += ".RE\n"
156             elif node.tagName == 'p':
157                 if s != "":
158                     if not s.endswith("\n"):
159                         s += "\n"
160                     s += para + "\n"
161                 s += blockXmlToNroff(node.childNodes, para)
162             elif node.tagName in ('h1', 'h2', 'h3'):
163                 if s != "":
164                     if not s.endswith("\n"):
165                         s += "\n"
166                 nroffTag = {'h1': 'SH', 'h2': 'SS', 'h3': 'ST'}[node.tagName]
167                 s += '.%s "' % nroffTag
168                 for child_node in node.childNodes:
169                     s += inlineXmlToNroff(child_node, r'\fR',
170                                           to_upper=(nroffTag == 'SH'))
171                 s += '"\n'
172             elif node.tagName == 'pre':
173                 fixed = node.getAttribute('fixed')
174                 if fixed == 'yes':
175                     font = r'\fL'
176                 else:
177                     font = r'\fB'
178                 s += pre_to_nroff(node.childNodes, para, font)
179             else:
180                 s += inlineXmlToNroff(node, r'\fR')
181         else:
182             raise error.Error("unknown node %s in block xml" % node)
183     if s != "" and not s.endswith('\n'):
184         s += '\n'
185     return s