1"""YANG output plugin"""
2
3import optparse
4
5from .. import plugin
6from .. import util
7from .. import grammar
8
9def pyang_plugin_init():
10    plugin.register_plugin(YANGPlugin())
11
12class YANGPlugin(plugin.PyangPlugin):
13    def add_output_format(self, fmts):
14        fmts['yang'] = self
15        self.handle_comments = True
16
17    def add_opts(self, optparser):
18        optlist = [
19            optparse.make_option("--yang-canonical",
20                                 dest="yang_canonical",
21                                 action="store_true",
22                                 help="Print in canonical order"),
23            optparse.make_option("--yang-remove-unused-imports",
24                                 dest="yang_remove_unused_imports",
25                                 action="store_true"),
26            optparse.make_option("--yang-line-length",
27                                 type="int",
28                                 dest="yang_line_length",
29                                 help="Maximum line length"),
30            ]
31        g = optparser.add_option_group("YANG output specific options")
32        g.add_options(optlist)
33
34    def setup_fmt(self, ctx):
35        ctx.implicit_errors = False
36        ctx.keep_arg_substrings = True
37
38    def emit(self, ctx, modules, fd):
39        module = modules[0]
40        emit_yang(ctx, module, fd)
41
42def emit_yang(ctx, module, fd):
43    emit_stmt(ctx, module, fd, 0, None, None, False, '', '  ')
44
45# always add newline between keyword and argument
46_force_newline_arg = ('description', 'reference', 'contact', 'organization')
47
48# do not quote these arguments
49_non_quote_arg_type = ('identifier', 'identifier-ref', 'boolean', 'integer',
50                       'non-negative-integer', 'max-value',
51                       'date', 'ordered-by-arg',
52                       'fraction-digits-arg', 'deviate-arg', 'version',
53                       'status-arg')
54
55_maybe_quote_arg_type = ('enum-arg', )
56
57# add extra blank line after these, when they occur on the top level
58_keyword_with_trailing_blank_line_toplevel = (
59    'identity',
60    'feature',
61    'extension',
62    'rpc',
63    'notification',
64    'augment',
65    'deviation',
66    )
67
68# always add extra blank line after these
69_keyword_with_trailing_blank_line = (
70    'typedef',
71    'grouping',
72    )
73
74# use single quote for the arguments to these keywords (if possible)
75_keyword_prefer_single_quote_arg = (
76    'must',
77    'when',
78    'pattern',
79)
80
81_keyword_with_path_arg = (
82## FIXME: change these when emit_path_arg does a good job
83#    'augment',
84#    'refine',
85#    'deviation',
86## FIXME: uncomment this and run tests/test_yang/g--yang-line-length_50
87##        it doesn't look good
88#    'path',
89)
90
91_kwd_class = {
92    'yang-version': 'header',
93    'namespace': 'header',
94    'prefix': 'header',
95    'belongs-to': 'header',
96    'organization': 'meta',
97    'contact': 'meta',
98    'description': 'meta',
99    'reference': 'meta',
100    'import': 'linkage',
101    'include': 'linkage',
102    'revision': 'revision',
103    'typedef': 'defs',
104    'grouping': 'defs',
105    'identity': 'defs',
106    'feature': 'defs',
107    'extension': 'defs',
108    '_comment': 'comment',
109    'augment': 'augment',
110    'rpc': 'rpc',
111    'notification': 'notification',
112    'deviation': 'deviation',
113    'module': None,
114    'submodule': None,
115}
116def get_kwd_class(keyword):
117    if util.is_prefixed(keyword):
118        return 'extension'
119    else:
120        try:
121            return _kwd_class[keyword]
122        except KeyError:
123            return 'body'
124
125_need_quote = (
126    " ", "}", "{", ";", '"', "'",
127    "\n", "\t", "\r", "//", "/*", "*/",
128    )
129
130def emit_stmt(ctx, stmt, fd, level, prev_kwd, prev_kwd_class, islast,
131              indent, indentstep):
132    if ctx.opts.yang_remove_unused_imports and stmt.keyword == 'import':
133        for p in stmt.parent.i_unused_prefixes:
134            if stmt.parent.i_unused_prefixes[p] == stmt:
135                return
136
137    max_line_len = ctx.opts.yang_line_length
138    if util.is_prefixed(stmt.raw_keyword):
139        (prefix, identifier) = stmt.raw_keyword
140        keywordstr = prefix + ':' + identifier
141    else:
142        keywordstr = stmt.keyword
143
144    kwd_class = get_kwd_class(stmt.keyword)
145    if ((level == 1 and
146         kwd_class != prev_kwd_class and kwd_class != 'extension') and
147        not ((level == 1 and prev_kwd in
148              _keyword_with_trailing_blank_line_toplevel) or
149             prev_kwd in _keyword_with_trailing_blank_line)):
150        fd.write('\n')
151
152    if stmt.keyword == '_comment':
153        emit_comment(stmt.arg, fd, indent)
154        return
155
156    fd.write(indent + keywordstr)
157    arg_on_new_line = False
158    if len(stmt.substmts) == 0:
159        eol = ';'
160    else:
161        eol = ' {'
162    if stmt.arg is not None:
163        # line_len is length of line w/o arg but with quotes and space before
164        # the arg
165        line_len = len(indent) + len(keywordstr) + 1 + 2 + len(eol)
166        if (stmt.keyword in _keyword_prefer_single_quote_arg and
167            stmt.arg.find("'") == -1):
168            # print with single quotes
169            if hasattr(stmt, 'arg_substrings') and len(stmt.arg_substrings) > 1:
170                # the arg was already split into multiple lines, keep them
171                emit_multi_str_arg(keywordstr, stmt.arg_substrings, fd, "'",
172                                   indent, indentstep, max_line_len, line_len)
173            elif not(need_new_line(max_line_len, line_len, stmt.arg)):
174                # fits into a single line
175                fd.write(" '" + stmt.arg + "'")
176            else:
177                # otherwise, print on new line, don't check line length
178                # since we can't break the string into multiple lines
179                fd.write('\n' + indent + indentstep)
180                fd.write("'" + stmt.arg + "'")
181                arg_on_new_line = True
182        elif hasattr(stmt, 'arg_substrings') and len(stmt.arg_substrings) > 1:
183            # the arg was already split into multiple lines, keep them
184            emit_multi_str_arg(keywordstr, stmt.arg_substrings, fd, '"',
185                               indent, indentstep, max_line_len, line_len)
186        elif '\n' in stmt.arg:
187            # the arg string contains newlines; print it as double quoted
188            arg_on_new_line = emit_arg(keywordstr, stmt, fd, indent, indentstep,
189                                       max_line_len, line_len)
190        elif stmt.keyword in _keyword_with_path_arg:
191            # special code for path argument; pretty-prints a long path with
192            # line breaks
193            arg_on_new_line = emit_path_arg(keywordstr, stmt.arg, fd,
194                                            indent, max_line_len, line_len, eol)
195        elif stmt.keyword in grammar.stmt_map:
196            (arg_type, _subspec) = grammar.stmt_map[stmt.keyword]
197            if (arg_type in _non_quote_arg_type or
198                (arg_type in _maybe_quote_arg_type and
199                 not need_quote(stmt.arg))):
200                # minus 2 since we don't quote
201                if not(need_new_line(max_line_len, line_len-2, stmt.arg)):
202                    fd.write(' ' + stmt.arg)
203                else:
204                    fd.write('\n' + indent + indentstep + stmt.arg)
205                    arg_on_new_line = True
206            else:
207                arg_on_new_line = emit_arg(keywordstr, stmt, fd,
208                                           indent, indentstep,
209                                           max_line_len, line_len)
210        else:
211            arg_on_new_line = emit_arg(keywordstr, stmt, fd, indent, indentstep,
212                                       max_line_len, line_len)
213    fd.write(eol + '\n')
214
215    if len(stmt.substmts) > 0:
216        if ctx.opts.yang_canonical:
217            substmts = grammar.sort_canonical(stmt.keyword, stmt.substmts)
218        else:
219            substmts = stmt.substmts
220        if level == 0:
221            kwd_class = 'header'
222        prev_kwd = None
223        for i, s in enumerate(substmts, start=1):
224            n = 1
225            if arg_on_new_line:
226                # arg was printed on a new line, increase indentation
227                n = 2
228            emit_stmt(ctx, s, fd, level + 1, prev_kwd, kwd_class,
229                      i == len(substmts),
230                      indent + (indentstep * n), indentstep)
231            kwd_class = get_kwd_class(s.keyword)
232            prev_kwd = s.keyword
233        fd.write(indent + '}\n')
234
235    if (not(islast) and
236        ((level == 1 and stmt.keyword in
237          _keyword_with_trailing_blank_line_toplevel) or
238         stmt.keyword in _keyword_with_trailing_blank_line)):
239        fd.write('\n')
240
241def need_new_line(max_line_len, line_len, arg):
242    eol = arg.find('\n')
243    if eol == -1:
244        eol = len(arg)
245    if (max_line_len is not None and line_len + eol > max_line_len):
246        return True
247    else:
248        return False
249
250def emit_multi_str_arg(keywordstr, strs, fd, pref_q,
251                       indent, indentstep, max_line_len,
252                       line_len):
253    # we want to align all strings on the same column; check if
254    # we can print w/o a newline
255    need_new_line = False
256    if max_line_len is not None:
257        for (s, q) in strs:
258            q = select_quote(s, q, pref_q)
259            if q == '"':
260                s = escape_str(s)
261            if line_len + len(s) > max_line_len:
262                need_new_line = True
263                break
264    if need_new_line:
265        fd.write('\n' + indent + indentstep)
266        prefix = (len(indent) - 2) * ' ' + indentstep + '+ '
267    else:
268        fd.write(' ')
269        prefix = indent + ((len(keywordstr) - 1) * ' ') + '+ '
270    # print first substring
271    (s, q) = strs[0]
272    q = select_quote(s, q, pref_q)
273    if q == '"':
274        s = escape_str(s)
275    fd.write("%s%s%s\n" % (q, s, q))
276    # then print the rest with the prefix and a newline at the end
277    for (s, q) in strs[1:-1]:
278        q = select_quote(s, q, pref_q)
279        if q == '"':
280            s = escape_str(s)
281        fd.write("%s%s%s%s\n" % (prefix, q, s, q))
282    # then print last substring with prefix but no newline
283    (s, q) = strs[-1]
284    q = select_quote(s, q, pref_q)
285    if q == '"':
286        s = escape_str(s)
287    fd.write("%s%s%s%s" % (prefix, q, s, q))
288
289    return need_new_line
290
291def select_quote(s, q, pref_q):
292    if pref_q == q:
293        return q
294    elif pref_q == "'":
295        if s.find("'") == -1:
296            # the string was double quoted, but it wasn't necessary,
297            # use preferred single quote
298            return "'"
299        else:
300            # the string was double quoted for a reason, keep it
301            return '"'
302    elif q == "'":
303        if need_quote(s):
304            # the string was single quoted for a reason, keep it
305            return "'"
306        else:
307            # the string was single quoted but it wasn't necessary,
308            # use preferred double quote
309            return '"'
310
311def escape_str(s):
312    s = s.replace('\\', r'\\')
313    s = s.replace('"', r'\"')
314    s = s.replace('\t', r'\t')
315    return s
316
317def emit_path_arg(keywordstr, arg, fd, indent, max_line_len, line_len, eol):
318    """Heuristically pretty print a path argument"""
319
320    quote = '"'
321
322    arg = escape_str(arg)
323
324    if not(need_new_line(max_line_len, line_len, arg)):
325        fd.write(" " + quote + arg + quote)
326        return False
327
328    ## FIXME: we should split the path on '/' and '[]' into multiple lines
329    ## and then print each line
330
331    num_chars = max_line_len - line_len
332    if num_chars <= 0:
333        # really small max_line_len; we give up
334        fd.write(" " + quote + arg + quote)
335        return False
336
337    while num_chars > 2 and arg[num_chars - 1:num_chars].isalnum():
338        num_chars -= 1
339    fd.write(" " + quote + arg[:num_chars] + quote)
340    arg = arg[num_chars:]
341    keyword_cont = ((len(keywordstr) - 1) * ' ') + '+'
342    while arg != '':
343        line_len = len(
344            "%s%s %s%s%s%s" % (indent, keyword_cont, quote, arg, quote, eol))
345        num_chars = len(arg) - (line_len - max_line_len)
346        while num_chars > 2 and arg[num_chars - 1:num_chars].isalnum():
347            num_chars -= 1
348        fd.write('\n' + indent + keyword_cont + " " +
349                 quote + arg[:num_chars] + quote)
350        arg = arg[num_chars:]
351
352def emit_arg(keywordstr, stmt, fd, indent, indentstep, max_line_len, line_len):
353    """Heuristically pretty print the argument string with double quotes"""
354    arg = escape_str(stmt.arg)
355    lines = arg.splitlines(True)
356    if len(lines) <= 1:
357        if len(arg) > 0 and arg[-1] == '\n':
358            arg = arg[:-1] + r'\n'
359        if (stmt.keyword in _force_newline_arg or
360            need_new_line(max_line_len, line_len, arg)):
361            fd.write('\n' + indent + indentstep + '"' + arg + '"')
362            return True
363        else:
364            fd.write(' "' + arg + '"')
365            return False
366    else:
367        need_nl = False
368        if stmt.keyword in _force_newline_arg:
369            need_nl = True
370        elif len(keywordstr) > 8:
371            # Heuristics: multi-line after a "long" keyword looks better
372            # than after a "short" keyword (compare 'when' and 'error-message')
373            need_nl = True
374        else:
375            for line in lines:
376                if need_new_line(max_line_len, line_len + 1, line):
377                    need_nl = True
378                    break
379        if need_nl:
380            fd.write('\n' + indent + indentstep)
381            prefix = indent + indentstep
382        else:
383            fd.write(' ')
384            prefix = indent + len(keywordstr) * ' ' + ' '
385        fd.write('"' + lines[0])
386        for line in lines[1:-1]:
387            if line[0] == '\n':
388                fd.write('\n')
389            else:
390                fd.write(prefix + ' ' + line)
391        # write last line
392        fd.write(prefix + ' ' + lines[-1])
393        if lines[-1][-1] == '\n':
394            # last line ends with a newline, indent the ending quote
395            fd.write(prefix + '"')
396        else:
397            fd.write('"')
398        return True
399
400def emit_comment(comment, fd, indent):
401    lines = comment.splitlines(True)
402    for x in lines:
403        if x[0] == '*':
404            fd.write(indent + ' ' + x)
405        else:
406            fd.write(indent + x)
407    fd.write('\n')
408
409def need_quote(arg):
410    for ch in _need_quote:
411        if arg.find(ch) != -1:
412            return True
413    return False
414