1#!/usr/bin/env python
2
3# Copyright (c) 2012 Google Inc. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Pretty-prints the contents of a GYP file."""
8
9from __future__ import print_function
10
11import sys
12import re
13
14
15# Regex to remove comments when we're counting braces.
16COMMENT_RE = re.compile(r"\s*#.*")
17
18# Regex to remove quoted strings when we're counting braces.
19# It takes into account quoted quotes, and makes sure that the quotes match.
20# NOTE: It does not handle quotes that span more than one line, or
21# cases where an escaped quote is preceded by an escaped backslash.
22QUOTE_RE_STR = r'(?P<q>[\'"])(.*?)(?<![^\\][\\])(?P=q)'
23QUOTE_RE = re.compile(QUOTE_RE_STR)
24
25
26def comment_replace(matchobj):
27    return matchobj.group(1) + matchobj.group(2) + "#" * len(matchobj.group(3))
28
29
30def mask_comments(input):
31    """Mask the quoted strings so we skip braces inside quoted strings."""
32    search_re = re.compile(r"(.*?)(#)(.*)")
33    return [search_re.sub(comment_replace, line) for line in input]
34
35
36def quote_replace(matchobj):
37    return "%s%s%s%s" % (
38        matchobj.group(1),
39        matchobj.group(2),
40        "x" * len(matchobj.group(3)),
41        matchobj.group(2),
42    )
43
44
45def mask_quotes(input):
46    """Mask the quoted strings so we skip braces inside quoted strings."""
47    search_re = re.compile(r"(.*?)" + QUOTE_RE_STR)
48    return [search_re.sub(quote_replace, line) for line in input]
49
50
51def do_split(input, masked_input, search_re):
52    output = []
53    mask_output = []
54    for (line, masked_line) in zip(input, masked_input):
55        m = search_re.match(masked_line)
56        while m:
57            split = len(m.group(1))
58            line = line[:split] + r"\n" + line[split:]
59            masked_line = masked_line[:split] + r"\n" + masked_line[split:]
60            m = search_re.match(masked_line)
61        output.extend(line.split(r"\n"))
62        mask_output.extend(masked_line.split(r"\n"))
63    return (output, mask_output)
64
65
66def split_double_braces(input):
67    """Masks out the quotes and comments, and then splits appropriate
68  lines (lines that matche the double_*_brace re's above) before
69  indenting them below.
70
71  These are used to split lines which have multiple braces on them, so
72  that the indentation looks prettier when all laid out (e.g. closing
73  braces make a nice diagonal line).
74  """
75    double_open_brace_re = re.compile(r"(.*?[\[\{\(,])(\s*)([\[\{\(])")
76    double_close_brace_re = re.compile(r"(.*?[\]\}\)],?)(\s*)([\]\}\)])")
77
78    masked_input = mask_quotes(input)
79    masked_input = mask_comments(masked_input)
80
81    (output, mask_output) = do_split(input, masked_input, double_open_brace_re)
82    (output, mask_output) = do_split(output, mask_output, double_close_brace_re)
83
84    return output
85
86
87def count_braces(line):
88    """keeps track of the number of braces on a given line and returns the result.
89
90  It starts at zero and subtracts for closed braces, and adds for open braces.
91  """
92    open_braces = ["[", "(", "{"]
93    close_braces = ["]", ")", "}"]
94    closing_prefix_re = re.compile(r"(.*?[^\s\]\}\)]+.*?)([\]\}\)],?)\s*$")
95    cnt = 0
96    stripline = COMMENT_RE.sub(r"", line)
97    stripline = QUOTE_RE.sub(r"''", stripline)
98    for char in stripline:
99        for brace in open_braces:
100            if char == brace:
101                cnt += 1
102        for brace in close_braces:
103            if char == brace:
104                cnt -= 1
105
106    after = False
107    if cnt > 0:
108        after = True
109
110    # This catches the special case of a closing brace having something
111    # other than just whitespace ahead of it -- we don't want to
112    # unindent that until after this line is printed so it stays with
113    # the previous indentation level.
114    if cnt < 0 and closing_prefix_re.match(stripline):
115        after = True
116    return (cnt, after)
117
118
119def prettyprint_input(lines):
120    """Does the main work of indenting the input based on the brace counts."""
121    indent = 0
122    basic_offset = 2
123    for line in lines:
124        if COMMENT_RE.match(line):
125            print(line)
126        else:
127            line = line.strip("\r\n\t ")  # Otherwise doesn't strip \r on Unix.
128            if len(line) > 0:
129                (brace_diff, after) = count_braces(line)
130                if brace_diff != 0:
131                    if after:
132                        print(" " * (basic_offset * indent) + line)
133                        indent += brace_diff
134                    else:
135                        indent += brace_diff
136                        print(" " * (basic_offset * indent) + line)
137                else:
138                    print(" " * (basic_offset * indent) + line)
139            else:
140                print("")
141
142
143def main():
144    if len(sys.argv) > 1:
145        data = open(sys.argv[1]).read().splitlines()
146    else:
147        data = sys.stdin.read().splitlines()
148    # Split up the double braces.
149    lines = split_double_braces(data)
150
151    # Indent and print the output.
152    prettyprint_input(lines)
153    return 0
154
155
156if __name__ == "__main__":
157    sys.exit(main())
158