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