1
+ try :
2
+ from lark import Lark , Transformer
3
+ from lark .exceptions import UnexpectedCharacters , UnexpectedEOF , UnexpectedToken
4
+ LARK_AVAILABLE = True
5
+ except ImportError :
6
+ LARK_AVAILABLE = False
7
+
8
+ import re
9
+
10
+ if LARK_AVAILABLE :
11
+ mvd_grammar = r'''
12
+ start: entry+
13
+
14
+ entry: "ViewDefinition" "[" simple_value_list "]" -> view_definition
15
+ | "Comment" "[" simple_value_list "]" -> comment
16
+ | "ExchangeRequirement" "[" other_keyword "]" -> exchangerequirement
17
+ | "Option" "[" other_keyword "]" -> option
18
+ | GENERIC_KEYWORD "[" dynamic_option_word "]" -> dynamic_option
19
+
20
+ GENERIC_KEYWORD: /[A-Za-z0-9_]+/
21
+
22
+ simple_value_list: value ("," value)*
23
+
24
+ value_list_set: value_set (";" value_set)*
25
+
26
+ value_set: set_name ":" simple_value_list
27
+
28
+ set_name: /[A-Za-z0-9_]+/
29
+
30
+ value: /[A-Za-z0-9 _\.-]+/
31
+
32
+ other_keyword: /[^\[\]]+/
33
+
34
+ dynamic_option_word: /[^\[\]]+/
35
+
36
+ %import common.WS
37
+ %ignore WS
38
+ '''
39
+
40
+ parser = Lark (mvd_grammar , parser = 'lalr' )
41
+
42
+ class DescriptionTransform (Transformer ):
43
+ def __init__ (self ):
44
+ self .view_definitions = []
45
+ self .keywords = set ()
46
+ self .comments = ""
47
+ self .exchange_requirements = ""
48
+ self .options = ""
49
+ self ._dynamic = {}
50
+
51
+ def view_definition (self , args ):
52
+ self .keywords .add ('view_definitions' )
53
+ self .view_definitions .extend (args [0 ])
54
+
55
+ def store_text_attribute (self , args , keyword ):
56
+ self .keywords .add (keyword )
57
+ setattr (self , keyword , " " .join (" " .join (str (child ) for child in args [0 ].children ).split ()))
58
+
59
+ def comment (self , args ):
60
+ self .keywords .add ("comments" )
61
+ self .comments = args [0 ] if len (args [0 ]) > 1 else args [0 ][0 ]
62
+
63
+ def exchangerequirement (self , args ):
64
+ self .store_text_attribute (args , "exchange_requirements" )
65
+
66
+ def option (self , args ):
67
+ if v := parse_semicolon_separated_kv (" " .join (" " .join (str (child ) for child in args [0 ].children ).split ())):
68
+ setattr (self , 'options' , v )
69
+ else :
70
+ self .store_text_attribute (args , "options" )
71
+
72
+ def dynamic_option (self , args ):
73
+ try :
74
+ original_keyword = str (args [0 ])
75
+ key = original_keyword .lower ()
76
+ raw_text = args [1 ].children [0 ].value
77
+ parsed_value = parse_semicolon_separated_kv (raw_text )
78
+ self ._dynamic [key ] = (parsed_value , original_keyword )
79
+ self .keywords .add (key )
80
+ setattr (self , key , parsed_value )
81
+ except Exception :
82
+ setattr (self , key , None )
83
+
84
+ def simple_value_list (self , args ):
85
+ return [str (arg ) for arg in args ]
86
+
87
+ def value_list_set (self , args ):
88
+ return args
89
+
90
+ def value_set (self , args ):
91
+ return [str (args [0 ])] + args [1 ]
92
+
93
+ def value (self , args ):
94
+ return str (args [0 ])
95
+
96
+ def set_name (self , args ):
97
+ return str (args [0 ])
98
+
99
+ def parse_mvd (description ):
100
+ text = ' ' .join (description )
101
+ parsed_description = DescriptionTransform ()
102
+ try :
103
+ if not text :
104
+ parsed_description .view_definitions = None
105
+ return parsed_description
106
+ parse_tree = parser .parse (text )
107
+ parsed_description .transform (parse_tree )
108
+ except (UnexpectedCharacters , UnexpectedEOF , UnexpectedToken ):
109
+ parsed_description .view_definitions = None
110
+ return parsed_description
111
+
112
+ def parse_semicolon_separated_kv (text : str ) -> dict [str , str | list [str ]] | None :
113
+ if not re .search (r'\w+\s*:\s*[^:]+' , text ):
114
+ return None
115
+ result = {}
116
+ try :
117
+ pairs = text .split (';' )
118
+ for pair in pairs :
119
+ if ':' in pair :
120
+ key , value = pair .split (':' , 1 )
121
+ key = key .strip ()
122
+ values = [v .strip () for v in value .split (',' )]
123
+ result [key ] = values [0 ] if len (values ) == 1 else values
124
+ return result
125
+ except Exception :
126
+ return None
127
+ else :
128
+ def parse_mvd (description ):
129
+ return None
130
+
131
+
132
+ class MvdInfo :
133
+ def __init__ (self , header ):
134
+ self ._header = header
135
+ self ._parsed = None
136
+
137
+ def _ensure_parsed (self ):
138
+ if not LARK_AVAILABLE :
139
+ return
140
+ if self ._parsed is None :
141
+ description = self ._header .file_description .description
142
+ if not description :
143
+ self ._parsed = DescriptionTransform () # avoid AttributeError
144
+ else :
145
+ self ._parsed = parse_mvd (description )
146
+
147
+ @property
148
+ def description (self ) -> list [str ]:
149
+ return self ._header .file_description .description
150
+
151
+ @description .setter
152
+ def description (self , new_description : list [str ]):
153
+ self ._header .file_description .description = tuple (new_description )
154
+ self ._parsed = None
155
+
156
+ @property
157
+ def view_definitions (self ):
158
+ self ._ensure_parsed ()
159
+ if not self ._parsed or self ._parsed .view_definitions is None :
160
+ return None #
161
+
162
+ vd = self ._parsed .view_definitions
163
+ vd_list = vd if isinstance (vd , list ) else [vd ] if vd else []
164
+ return AutoCommitList (
165
+ vd_list ,
166
+ callback = lambda val : (self ._update_keyword ("ViewDefinition" , val ), setattr (self , "_parsed" , None )),
167
+ formatter = lambda lst : "," .join (str (i ) for i in lst )
168
+ )
169
+
170
+ @view_definitions .setter
171
+ def view_definitions (self , new_value : str | list [str ]):
172
+ if isinstance (new_value , list ):
173
+ value = ", " .join (new_value )
174
+ else :
175
+ value = str (new_value )
176
+ self ._update_keyword ("ViewDefinition" , value )
177
+
178
+ @property
179
+ def comments (self ):
180
+ self ._ensure_parsed ()
181
+ comments = self ._parsed .comments
182
+ comment_list = comments if isinstance (comments , list ) else [comments ] if comments else []
183
+ return AutoCommitList (
184
+ comment_list ,
185
+ callback = lambda val : self ._update_keyword ("Comment" , val ),
186
+ formatter = lambda lst : ", " .join (str (i ) for i in lst )
187
+ )
188
+
189
+ @comments .setter
190
+ def comments (self , new_value : str | list [str ]):
191
+ if isinstance (new_value , list ):
192
+ value = ", " .join (new_value )
193
+ else :
194
+ value = str (new_value )
195
+ self ._update_keyword ("Comment" , value )
196
+
197
+ @property
198
+ def exchange_requirements (self ):
199
+ self ._ensure_parsed ()
200
+ return self ._parsed .exchange_requirements if self ._parsed else None
201
+
202
+ @exchange_requirements .setter
203
+ def exchange_requirements (self , new_value : str ):
204
+ self ._update_keyword ("ExchangeRequirement" , new_value )
205
+
206
+ @property
207
+ def options (self ):
208
+ self ._ensure_parsed ()
209
+ if isinstance (self ._parsed .options , dict ):
210
+ return DictionaryHandler (self ._parsed .options , self , "Option" )
211
+ return self ._parsed .options if self ._parsed else None
212
+
213
+ @options .setter
214
+ def options (self , new_value : str ):
215
+ self ._update_keyword ("Option" , new_value )
216
+
217
+ @property
218
+ def keywords (self ):
219
+ self ._ensure_parsed ()
220
+ return self ._parsed .keywords if self ._parsed else set ()
221
+
222
+ def _update_keyword (self , keyword : str , new_value : str ):
223
+ updated = False
224
+ new_line = f"{ keyword } [{ new_value } ]"
225
+ lines = []
226
+ for line in self .description :
227
+ if line .strip ().startswith (f"{ keyword } [" ):
228
+ lines .append (new_line )
229
+ updated = True
230
+ else :
231
+ lines .append (line )
232
+ if not updated :
233
+ lines .append (new_line )
234
+ self .description = lines
235
+
236
+ def __getattr__ (self , name ):
237
+ self ._ensure_parsed ()
238
+ if hasattr (self ._parsed , '_dynamic' ):
239
+ name_lc = name .lower ()
240
+ if name_lc in self ._parsed ._dynamic :
241
+ value , original_keyword = self ._parsed ._dynamic [name_lc ]
242
+ return DictionaryHandler (value , self , original_keyword )
243
+ raise AttributeError (f"'MvdInfo' object has no attribute '{ name } '" )
244
+
245
+ def __dir__ (self ):
246
+ base = super ().__dir__ ()
247
+ if self ._parsed and hasattr (self ._parsed , '_dynamic' ):
248
+ return base + [kw for _ , kw in self ._parsed ._dynamic .values ()]
249
+ return base
250
+
251
+
252
+ class DictionaryHandler (dict ):
253
+ def __init__ (self , initial_data , mvdinfo , keyword ):
254
+ super ().__init__ ()
255
+ self ._mvdinfo = mvdinfo
256
+ self ._keyword = keyword
257
+ for k , v in initial_data .items ():
258
+ if isinstance (v , list ):
259
+ super ().__setitem__ (k , AutoCommitList (v , self ._commit ))
260
+ else :
261
+ super ().__setitem__ (k , v )
262
+
263
+ def _commit (self ):
264
+ new_value = "; " .join (
265
+ f"{ k } : { ', ' .join (v ) if isinstance (v , list ) else v } "
266
+ for k , v in self .items ()
267
+ )
268
+ self ._mvdinfo ._update_keyword (self ._keyword , new_value )
269
+
270
+ def __setitem__ (self , key , value ):
271
+ if isinstance (value , list ):
272
+ value = AutoCommitList (value , self ._commit )
273
+ super ().__setitem__ (key , value )
274
+ self ._commit ()
275
+
276
+ def __delitem__ (self , key ):
277
+ super ().__delitem__ (key )
278
+ self ._commit ()
279
+
280
+
281
+ class AutoCommitList (list ):
282
+ "ensures keyword attributes are written back to ifcopenshell.file.header"
283
+ def __init__ (self , iterable , callback , formatter = None ):
284
+ super ().__init__ (iterable )
285
+ self ._callback = callback
286
+ self ._formatter = formatter
287
+
288
+ def _commit (self ):
289
+ if self ._formatter :
290
+ self ._callback (self ._formatter (self ))
291
+ else :
292
+ self ._callback ()
293
+
294
+ def append (self , item ):
295
+ super ().append (item )
296
+ self ._commit ()
297
+
298
+ def extend (self , iterable ):
299
+ super ().extend (iterable )
300
+ self ._commit ()
301
+
302
+ def insert (self , index , item ):
303
+ super ().insert (index , item )
304
+ self ._commit ()
305
+
306
+ def remove (self , item ):
307
+ super ().remove (item )
308
+ self ._commit ()
309
+
310
+ def pop (self , index = - 1 ):
311
+ item = super ().pop (index )
312
+ self ._commit ()
313
+ return item
314
+
315
+ def clear (self ):
316
+ super ().clear ()
317
+ self ._commit ()
318
+
319
+ def __setitem__ (self , index , value ):
320
+ super ().__setitem__ (index , value )
321
+ self ._commit ()
322
+
323
+ def __delitem__ (self , index ):
324
+ super ().__delitem__ (index )
325
+ self ._commit ()
0 commit comments