nroff: Add support for <b>...</b> and <i>...</i> inline markup.
[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 text_to_nroff(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 r'\[char46]'
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 escape_nroff_literal(s, font=r'\fB'):
59     return font + r'%s\fR' % text_to_nroff(s, font)
60
61 def inline_xml_to_nroff(node, font, to_upper=False, newline='\n'):
62     if node.nodeType == node.TEXT_NODE:
63         if to_upper:
64             s = text_to_nroff(node.data.upper(), font)
65         else:
66             s = text_to_nroff(node.data, font)
67         return s.replace('\n', newline)
68     elif node.nodeType == node.ELEMENT_NODE:
69         if node.tagName in ['code', 'em', 'option', 'env', 'b']:
70             s = r'\fB'
71             for child in node.childNodes:
72                 s += inline_xml_to_nroff(child, r'\fB', to_upper, newline)
73             return s + font
74         elif node.tagName == 'ref':
75             s = r'\fB'
76             if node.hasAttribute('column'):
77                 s += node.attributes['column'].nodeValue
78                 if node.hasAttribute('key'):
79                     s += ':' + node.attributes['key'].nodeValue
80             elif node.hasAttribute('table'):
81                 s += node.attributes['table'].nodeValue
82             elif node.hasAttribute('group'):
83                 s += node.attributes['group'].nodeValue
84             elif node.hasAttribute('db'):
85                 s += node.attributes['db'].nodeValue
86             else:
87                 raise error.Error("'ref' lacks required attributes: %s" % node.attributes.keys())
88             return s + font
89         elif node.tagName in ['var', 'dfn', 'i']:
90             s = r'\fI'
91             for child in node.childNodes:
92                 s += inline_xml_to_nroff(child, r'\fI', to_upper, newline)
93             return s + font
94         else:
95             raise error.Error("element <%s> unknown or invalid here" % node.tagName)
96     elif node.nodeType == node.COMMENT_NODE:
97         return ''
98     else:
99         raise error.Error("unknown node %s in inline xml" % node)
100
101 def pre_to_nroff(nodes, para, font):
102     # This puts 'font' at the beginning of each line so that leading and
103     # trailing whitespace stripping later doesn't removed leading spaces
104     # from preformatted text.
105     s = para + '\n.nf\n' + font
106     for node in nodes:
107         s += inline_xml_to_nroff(node, font, False, '\n.br\n' + font)
108     s += '\n.fi\n'
109     return s
110
111 def diagram_header_to_nroff(header_node):
112     header_fields = []
113     i = 0
114     for node in header_node.childNodes:
115         if node.nodeType == node.ELEMENT_NODE and node.tagName == 'bits':
116             name = node.attributes['name'].nodeValue
117             width = node.attributes['width'].nodeValue
118             above = node.getAttribute('above')
119             below = node.getAttribute('below')
120             fill = node.getAttribute('fill')
121             header_fields += [{"name": name,
122                               "tag": "B%d" % i,
123                               "width": width,
124                               "above": above,
125                               "below": below,
126                               "fill": fill}]
127             i += 1
128         elif node.nodeType == node.COMMENT_NODE:
129             pass
130         elif node.nodeType == node.TEXT_NODE and node.data.isspace():
131             pass
132         else:
133             fatal("unknown node %s in diagram <header> element" % node)
134
135     pic_s = ""
136     for f in header_fields:
137         pic_s += "  %s: box \"%s\" width %s" % (f['tag'], f['name'], f['width'])
138         if f['fill'] == 'yes':
139             pic_s += " fill"
140         pic_s += '\n'
141     for f in header_fields:
142         pic_s += "  \"%s\" at %s.n above\n" % (f['above'], f['tag'])
143         pic_s += "  \"%s\" at %s.s below\n" % (f['below'], f['tag'])
144     name = header_node.getAttribute('name')
145     if name == "":
146         visible = " invis"
147     else:
148         visible = ""
149     pic_s += "line <->%s \"%s\" above " % (visible, name)
150     pic_s += "from %s.nw + (0,textht) " % header_fields[0]['tag']
151     pic_s += "to %s.ne + (0,textht)\n" % header_fields[-1]['tag']
152
153     text_s = ""
154     for f in header_fields:
155         text_s += """.IP \\(bu
156 %s bits""" % (f['above'])
157         if f['name']:
158             text_s += ": %s" % f['name']
159         if f['below']:
160             text_s += " (%s)" % f['below']
161         text_s += "\n"
162     return pic_s, text_s
163
164 def diagram_to_nroff(nodes, para):
165     pic_s = ''
166     text_s = ''
167     move = False
168     for node in nodes:
169         if node.nodeType == node.ELEMENT_NODE and node.tagName == 'header':
170             if move:
171                 pic_s += "move .1\n"
172                 text_s += ".sp\n"
173             pic_header, text_header = diagram_header_to_nroff(node)
174             pic_s += "[\n" + pic_header + "]\n"
175             text_s += text_header
176             move = True
177         elif node.nodeType == node.ELEMENT_NODE and node.tagName == 'nospace':
178             move = False
179         elif node.nodeType == node.ELEMENT_NODE and node.tagName == 'dots':
180             pic_s += "move .1\n"
181             pic_s += '". . ." ljust\n'
182             text_s += ".sp\n"
183         elif node.nodeType == node.COMMENT_NODE:
184             pass
185         elif node.nodeType == node.TEXT_NODE and node.data.isspace():
186             pass
187         else:
188             fatal("unknown node %s in diagram <header> element" % node)
189     return para + """
190 .\\" check if in troff mode (TTY)
191 .if t \{
192 .PS
193 boxht = .2
194 textht = 1/6
195 fillval = .2
196 """ + pic_s + """\
197 .PE
198 \\}
199 .\\" check if in nroff mode:
200 .if n \{
201 .RS
202 """ + text_s + """\
203 .RE
204 \\}"""
205
206 def block_xml_to_nroff(nodes, para='.PP'):
207     s = ''
208     for node in nodes:
209         if node.nodeType == node.TEXT_NODE:
210             s += text_to_nroff(node.data)
211             s = s.lstrip()
212         elif node.nodeType == node.ELEMENT_NODE:
213             if node.tagName in ['ul', 'ol']:
214                 if s != "":
215                     s += "\n"
216                 s += ".RS\n"
217                 i = 0
218                 for li_node in node.childNodes:
219                     if (li_node.nodeType == node.ELEMENT_NODE
220                         and li_node.tagName == 'li'):
221                         i += 1
222                         if node.tagName == 'ul':
223                             s += ".IP \\(bu\n"
224                         else:
225                             s += ".IP %d. .25in\n" % i
226                         s += block_xml_to_nroff(li_node.childNodes, ".IP")
227                     elif li_node.nodeType == node.COMMENT_NODE:
228                         pass
229                     elif (li_node.nodeType != node.TEXT_NODE
230                           or not li_node.data.isspace()):
231                         raise error.Error("<%s> element may only have <li> children" % node.tagName)
232                 s += ".RE\n"
233             elif node.tagName == 'dl':
234                 if s != "":
235                     s += "\n"
236                 s += ".RS\n"
237                 prev = "dd"
238                 for li_node in node.childNodes:
239                     if (li_node.nodeType == node.ELEMENT_NODE
240                         and li_node.tagName == 'dt'):
241                         if prev == 'dd':
242                             s += '.TP\n'
243                         else:
244                             s += '.TQ .5in\n'
245                         prev = 'dt'
246                     elif (li_node.nodeType == node.ELEMENT_NODE
247                           and li_node.tagName == 'dd'):
248                         if prev == 'dd':
249                             s += '.IP\n'
250                         prev = 'dd'
251                     elif li_node.nodeType == node.COMMENT_NODE:
252                         continue
253                     elif (li_node.nodeType != node.TEXT_NODE
254                           or not li_node.data.isspace()):
255                         raise error.Error("<dl> element may only have <dt> and <dd> children")
256                     s += block_xml_to_nroff(li_node.childNodes, ".IP")
257                 s += ".RE\n"
258             elif node.tagName == 'p':
259                 if s != "":
260                     if not s.endswith("\n"):
261                         s += "\n"
262                     s += para + "\n"
263                 s += block_xml_to_nroff(node.childNodes, para)
264             elif node.tagName in ('h1', 'h2', 'h3'):
265                 if s != "":
266                     if not s.endswith("\n"):
267                         s += "\n"
268                 nroffTag = {'h1': 'SH', 'h2': 'SS', 'h3': 'ST'}[node.tagName]
269                 s += '.%s "' % nroffTag
270                 for child_node in node.childNodes:
271                     s += inline_xml_to_nroff(child_node, r'\fR',
272                                           to_upper=(nroffTag == 'SH'))
273                 s += '"\n'
274             elif node.tagName == 'pre':
275                 fixed = node.getAttribute('fixed')
276                 if fixed == 'yes':
277                     font = r'\fL'
278                 else:
279                     font = r'\fB'
280                 s += pre_to_nroff(node.childNodes, para, font)
281             elif node.tagName == 'diagram':
282                 s += diagram_to_nroff(node.childNodes, para)
283             else:
284                 s += inline_xml_to_nroff(node, r'\fR')
285         elif node.nodeType == node.COMMENT_NODE:
286             pass
287         else:
288             raise error.Error("unknown node %s in block xml" % node)
289     if s != "" and not s.endswith('\n'):
290         s += '\n'
291     return s