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