1"""
2This module can parse a Delphi Form (dfm) file.
3
4The main is used in experimenting (to find which files fail
5to parse, and where), but isn't useful for anything else.
6"""
7__version__ = "1.0"
8__author__ = "Daniel 'Dang' Griffith <pythondev - dang at lazytwinacres . net>"
9
10
11from pyparsing import (
12    Literal,
13    CaselessLiteral,
14    Word,
15    delimitedList,
16    Optional,
17    Combine,
18    Group,
19    alphas,
20    nums,
21    alphanums,
22    Forward,
23    oneOf,
24    OneOrMore,
25    ZeroOrMore,
26    CharsNotIn,
27)
28
29
30# This converts DFM character constants into Python string (unicode) values.
31def to_chr(x):
32    """chr(x) if 0 < x < 128 ; unicode(x) if x > 127."""
33    return 0 < x < 128 and chr(x) or eval("u'\\u%d'" % x)
34
35
36#################
37# BEGIN GRAMMAR
38#################
39
40COLON = Literal(":").suppress()
41CONCAT = Literal("+").suppress()
42EQUALS = Literal("=").suppress()
43LANGLE = Literal("<").suppress()
44LBRACE = Literal("[").suppress()
45LPAREN = Literal("(").suppress()
46PERIOD = Literal(".").suppress()
47RANGLE = Literal(">").suppress()
48RBRACE = Literal("]").suppress()
49RPAREN = Literal(")").suppress()
50
51CATEGORIES = CaselessLiteral("categories").suppress()
52END = CaselessLiteral("end").suppress()
53FONT = CaselessLiteral("font").suppress()
54HINT = CaselessLiteral("hint").suppress()
55ITEM = CaselessLiteral("item").suppress()
56OBJECT = CaselessLiteral("object").suppress()
57
58attribute_value_pair = Forward()  # this is recursed in item_list_entry
59
60simple_identifier = Word(alphas, alphanums + "_")
61identifier = Combine(simple_identifier + ZeroOrMore(Literal(".") + simple_identifier))
62object_name = identifier
63object_type = identifier
64
65# Integer and floating point values are converted to Python longs and floats, respectively.
66int_value = Combine(Optional("-") + Word(nums)).setParseAction(
67    lambda s, l, t: [int(t[0])]
68)
69float_value = Combine(
70    Optional("-") + Optional(Word(nums)) + "." + Word(nums)
71).setParseAction(lambda s, l, t: [float(t[0])])
72number_value = float_value | int_value
73
74# Base16 constants are left in string form, including the surrounding braces.
75base16_value = Combine(
76    Literal("{") + OneOrMore(Word("0123456789ABCDEFabcdef")) + Literal("}"),
77    adjacent=False,
78)
79
80# This is the first part of a hack to convert the various delphi partial sglQuotedStrings
81#     into a single sglQuotedString equivalent.  The gist of it is to combine
82#     all sglQuotedStrings (with their surrounding quotes removed (suppressed))
83#     with sequences of #xyz character constants, with "strings" concatenated
84#     with a '+' sign.
85unquoted_sglQuotedString = Combine(
86    Literal("'").suppress() + ZeroOrMore(CharsNotIn("'\n\r")) + Literal("'").suppress()
87)
88
89# The parse action on this production converts repetitions of constants into a single string.
90pound_char = Combine(
91    OneOrMore(
92        (Literal("#").suppress() + Word(nums)).setParseAction(
93            lambda s, l, t: to_chr(int(t[0]))
94        )
95    )
96)
97
98# This is the second part of the hack.  It combines the various "unquoted"
99#     partial strings into a single one.  Then, the parse action puts
100#     a single matched pair of quotes around it.
101delphi_string = Combine(
102    OneOrMore(CONCAT | pound_char | unquoted_sglQuotedString), adjacent=False
103).setParseAction(lambda s, l, t: "'%s'" % t[0])
104
105string_value = delphi_string | base16_value
106
107list_value = (
108    LBRACE
109    + Optional(Group(delimitedList(identifier | number_value | string_value)))
110    + RBRACE
111)
112paren_list_value = (
113    LPAREN + ZeroOrMore(identifier | number_value | string_value) + RPAREN
114)
115
116item_list_entry = ITEM + ZeroOrMore(attribute_value_pair) + END
117item_list = LANGLE + ZeroOrMore(item_list_entry) + RANGLE
118
119generic_value = identifier
120value = (
121    item_list
122    | number_value
123    | string_value
124    | list_value
125    | paren_list_value
126    | generic_value
127)
128
129category_attribute = CATEGORIES + PERIOD + oneOf("strings itemsvisibles visibles", True)
130event_attribute = oneOf(
131    "onactivate onclosequery onclose oncreate ondeactivate onhide onshow", True
132)
133font_attribute = FONT + PERIOD + oneOf("charset color height name style", True)
134hint_attribute = HINT
135layout_attribute = oneOf("left top width height", True)
136generic_attribute = identifier
137attribute = (
138    category_attribute
139    | event_attribute
140    | font_attribute
141    | hint_attribute
142    | layout_attribute
143    | generic_attribute
144)
145
146category_attribute_value_pair = category_attribute + EQUALS + paren_list_value
147event_attribute_value_pair = event_attribute + EQUALS + value
148font_attribute_value_pair = font_attribute + EQUALS + value
149hint_attribute_value_pair = hint_attribute + EQUALS + value
150layout_attribute_value_pair = layout_attribute + EQUALS + value
151generic_attribute_value_pair = attribute + EQUALS + value
152attribute_value_pair << Group(
153    category_attribute_value_pair
154    | event_attribute_value_pair
155    | font_attribute_value_pair
156    | hint_attribute_value_pair
157    | layout_attribute_value_pair
158    | generic_attribute_value_pair
159)
160
161object_declaration = Group(OBJECT + object_name + COLON + object_type)
162object_attributes = Group(ZeroOrMore(attribute_value_pair))
163
164nested_object = Forward()
165object_definition = (
166    object_declaration + object_attributes + ZeroOrMore(nested_object) + END
167)
168nested_object << Group(object_definition)
169
170#################
171# END GRAMMAR
172#################
173
174
175def printer(s, loc, tok):
176    print(tok, end=" ")
177    return tok
178
179
180def get_filename_list(tf):
181    import sys, glob
182
183    if tf == None:
184        if len(sys.argv) > 1:
185            tf = sys.argv[1:]
186        else:
187            tf = glob.glob("*.dfm")
188    elif type(tf) == str:
189        tf = [tf]
190    testfiles = []
191    for arg in tf:
192        testfiles.extend(glob.glob(arg))
193    return testfiles
194
195
196def main(testfiles=None, action=printer):
197    """testfiles can be None, in which case the command line arguments are used as filenames.
198    testfiles can be a string, in which case that file is parsed.
199    testfiles can be a list.
200    In all cases, the filenames will be globbed.
201    If more than one file is parsed successfully, a dictionary of ParseResults is returned.
202    Otherwise, a simple ParseResults is returned.
203    """
204    testfiles = get_filename_list(testfiles)
205    print(testfiles)
206
207    if action:
208        for i in (simple_identifier, value, item_list):
209            i.setParseAction(action)
210
211    success = 0
212    failures = []
213
214    retval = {}
215    for f in testfiles:
216        try:
217            retval[f] = object_definition.parseFile(f)
218            success += 1
219        except Exception:
220            failures.append(f)
221
222    if failures:
223        print("\nfailed while processing %s" % ", ".join(failures))
224    print("\nsucceeded on %d of %d files" % (success, len(testfiles)))
225
226    if len(retval) == 1 and len(testfiles) == 1:
227        # if only one file is parsed, return the parseResults directly
228        return retval[list(retval.keys())[0]]
229
230    # else, return a dictionary of parseResults
231    return retval
232
233
234if __name__ == "__main__":
235    main()
236