11import os
22import re
3+ import copy
34
4- from pyvba .browser import Browser
5+ from win32com .universal import com_error
6+
7+ from pyvba .browser import Browser , visited
58from pyvba .viewer import FunctionViewer
69
710
811class ExportStr :
9- def __init__ (self , browser : Browser , skip_func : bool = False , skip_err : bool = False ):
12+ def __init__ (self , browser : Browser , skip_func : bool = False , skip_err : bool = False , vba_form : bool = False ):
1013 """The base class for exporting
1114
1215 Parameters
@@ -17,12 +20,15 @@ def __init__(self, browser: Browser, skip_func: bool = False, skip_err: bool = F
1720 Skips reporting any FunctionViewer instant.
1821 skip_err: bool
1922 Skips reporting any error.
23+ vba_form: bool
24+ A flag that determines if the output mimics the VBA tree structure or a more list-like view.
2025 """
2126 self ._browser = browser
2227 self ._data = None
2328
2429 self ._skip_func = skip_func
2530 self ._skip_err = skip_err
31+ self ._vba_form = vba_form
2632
2733 @property
2834 def data_str (self ) -> str :
@@ -32,7 +38,7 @@ def data_str(self) -> str:
3238
3339 @property
3440 def data_min (self ) -> str :
35- """Return the data in minimized string format.
41+ """Return the data in a minimized string format.
3642
3743 The minimized version removes all newlines and tabs.
3844 """
@@ -42,10 +48,14 @@ def data_min(self) -> str:
4248 def _check (self ):
4349 """Check if the string needs to be generated."""
4450 if self ._data is None :
45- self ._generate ()
51+ self ._generate_vba () if self ._vba_form else self ._generate_dict ()
52+
53+ def _generate_vba (self , * args , ** kwargs ):
54+ """Begin generating the string based on the VBA tree."""
55+ pass
4656
47- def _generate (self , * args , ** kwargs ):
48- """Begin generating the string."""
57+ def _generate_dict (self , * args , ** kwargs ):
58+ """Begin generating the string based on the browser.visited dictionary ."""
4959 pass
5060
5161 def save_as (self , name : str , ext : str , path : str = '.\\ ' , minimize : bool = False ):
@@ -60,15 +70,21 @@ def save_as(self, name: str, ext: str, path: str = '.\\', minimize: bool = False
6070 path: str
6171 The save location.
6272 minimize: bool
63- A flag that determines the format of the final output .
73+ A flag that determines if the data is returned in a minimized string format .
6474 """
6575 os .makedirs (path , exist_ok = True )
6676 with open (os .path .join (path , name + ext ), "w" ) as file :
6777 file .write (self .data_str if not minimize else self .data_min )
6878 file .close ()
6979
7080 def print (self , minimize : bool = False ):
71- """Print the string in the normal or minimized version."""
81+ """Print the string in the normal or minimized version.
82+
83+ Parameters
84+ ----------
85+ minimize: bool
86+ A flag that determines if the data is returned in a minimized string format.
87+ """
7288 self ._check ()
7389 print (self .data_str if not minimize else self .data_min )
7490
@@ -83,7 +99,7 @@ class XMLExport(ExportStr):
8399 }
84100
85101 def __init__ (self , browser : Browser , version = 1.0 , encoding : str = "UTF-8" , skip_func : bool = False ,
86- skip_err : bool = False ):
102+ skip_err : bool = False , vba_form : bool = False ):
87103 """Create a well-formed XML string for export.
88104
89105 Parameters
@@ -95,7 +111,7 @@ def __init__(self, browser: Browser, version=1.0, encoding: str = "UTF-8", skip_
95111 encoding: str
96112 The encoding type (default is UTF-8).
97113 """
98- super ().__init__ (browser , skip_func , skip_err )
114+ super ().__init__ (browser , skip_func , skip_err , vba_form )
99115
100116 self ._xml_head = f'<?xml version="{ str (version )} " encoding="{ encoding } "?>\n '
101117
@@ -104,8 +120,8 @@ def xml_encode(text: str) -> str:
104120 """Map special XML characters to their encoded form in a given string."""
105121 return "" .join (XMLExport .XML_ESCAPE_CHARS .get (c , c ) for c in str (text ))
106122
107- def _generate (self ):
108- """Begin generating the XML string."""
123+ def _generate_vba (self ):
124+ """Begin generating the XML string based on the VBA tree ."""
109125 self ._data = self ._xml_head + self ._generate_tag (self ._browser )
110126
111127 # convert empty elements to a single tag
@@ -128,12 +144,19 @@ def _generate_tag(self, elem, tabs: int = 0, **kwargs) -> str:
128144 """
129145
130146 xml = ''
147+ stack = kwargs .get ('stack' , [])
148+
131149 if isinstance (elem , Browser ):
132- # display the browser and its children
150+ tag = XMLExport . Tag ( elem . name )
133151
134- # setup the tag and attributes
152+ # check if in stack already
153+ if elem in stack :
154+ return tag .enclose ('BrowserObject: See ancestors' , tabs )
155+ else :
156+ stack .append (elem )
157+
158+ # setup the tag attributes
135159 attrs = ["Name" , "Count" ]
136- tag = XMLExport .Tag (elem .name )
137160 [
138161 tag .add_attr (attr , value )
139162 for attr , value in elem .all .items ()
@@ -143,45 +166,110 @@ def _generate_tag(self, elem, tabs: int = 0, **kwargs) -> str:
143166 # add the element and start adding the sub-elements
144167 xml += '\t ' * tabs + tag .open_tag + '\n '
145168 for item , value in elem .all .items ():
146- if type (value ) is list :
169+ if isinstance (value , list ) :
147170 item_tag = XMLExport .Tag ("Item" )
148171
149172 xml += '\t ' * (tabs + 1 ) + item_tag .open_tag + '\n '
150173 for i in value :
151- xml += self ._generate_tag (i , tabs + 2 )
174+ xml += self ._generate_tag (i , tabs + 2 , stack = stack )
152175 xml += '\t ' * (tabs + 1 ) + item_tag .close_tag + '\n '
153176
154177 elif item not in attrs :
155- # overlook objects that call itself
178+ # overlook objects that point to themselves
156179 if item == elem .name :
157180 continue
158181 else :
159- xml += self ._generate_tag (value , tabs + 1 , name = item )
182+ xml += self ._generate_tag (value , tabs + 1 , name = item , stack = stack )
160183
161184 xml += '\t ' * tabs + tag .close_tag + '\n '
185+
162186 elif isinstance (elem , FunctionViewer ):
163187 if not self ._skip_func :
164188 # display the function and its properties
165189 tag = XMLExport .Tag ("Function" , name = elem .name , args = len (elem .args ))
166190 xml += tag .enclose (str (elem )[26 :], tabs )
167- elif isinstance (elem , BaseException ):
191+
192+ elif isinstance (elem , com_error ):
168193 # display the error location and method
169194 if not self ._skip_err :
170- try :
171- tag = XMLExport .Tag ("Error" , on = str (elem .args [2 ][1 ]))
172- xml += tag .enclose (self .xml_encode (str (elem .args [2 ][2 ])), tabs )
173- except TypeError :
174- tag = XMLExport .Tag ("Error" )
175- xml += tag .enclose (self .xml_encode (str (elem .args [2 ])), tabs )
176- except IndexError :
177- tag = XMLExport .Tag ("Error" )
178- xml += tag .enclose (self .xml_encode (str (elem )), tabs )
195+ tag = XMLExport .Tag ("Error" )
196+ xml += tag .enclose (self .xml_encode (str (elem )), tabs )
197+
179198 else :
180199 # display the variable and value
181200 tag = XMLExport .Tag (kwargs .get ('name' , 'Unknown' ))
182201 xml += tag .enclose (self .xml_encode (str (elem )), tabs )
183202 return xml
184203
204+ def _generate_dict (self ):
205+ """Begin generating the XML string based on the visited dictionary."""
206+ # populate browser and copy visited
207+ self ._browser .browse_all ()
208+ visited2 = copy .copy (visited )
209+
210+ tag = XMLExport .Tag (self ._browser .name , count = len (visited2 ))
211+ xml = self ._xml_head + tag .open_tag + "\n "
212+
213+ # iterate through dictionary items
214+ for var , value in visited2 .items ():
215+ tag1 = XMLExport .Tag (var , count = len (value ))
216+ xml += "\t " + tag1 .open_tag + "\n "
217+
218+ # iterate through each list
219+ for item in value :
220+ tag2 = XMLExport .Tag (item .name )
221+ xml += "\t " * 2 + tag2 .open_tag + "\n "
222+
223+ # add name attribute
224+ if 'Name' in item .all :
225+ tag2 .add_attr ('Name' , item .Name )
226+
227+ # iterate through each browser in the list
228+ for var2 , value2 in item .all .items ():
229+ tag3 = XMLExport .Tag (var2 )
230+
231+ # add name attribute
232+ if isinstance (value2 , Browser ) and 'Name' in value2 .all :
233+ tag3 .add_attr ('Name' , value2 .Name )
234+
235+ # check for a collection object
236+ if isinstance (value2 , list ):
237+ tag3 .add_attr ('count' , len (value2 ))
238+ xml += "\t " * 3 + tag3 .open_tag + "\n "
239+
240+ # iterate through the browser's collection
241+ for item2 in value2 :
242+ tag4 = XMLExport .Tag (item2 .name if isinstance (item2 , Browser ) else item2 )
243+
244+ # add name attribute
245+ if isinstance (item2 , Browser ) and 'Name' in item2 .all :
246+ tag4 .add_attr ('Name' , item2 .Name )
247+
248+ xml += tag4 .enclose (item2 .name if isinstance (item2 , Browser ) else item2 , 4 )
249+
250+ xml += "\t " * 3 + tag3 .close_tag + "\n "
251+ else :
252+ if isinstance (value2 , Browser ):
253+ output = 'BrowserObject'
254+ elif isinstance (value2 , com_error ):
255+ if self ._skip_err :
256+ continue
257+ output = self .xml_encode (repr (value2 ))
258+ elif isinstance (value2 , FunctionViewer ):
259+ if self ._skip_func :
260+ continue
261+ tag3 = XMLExport .Tag ("Function" , name = value2 .name , args = len (value2 .args ))
262+ output = str (value2 )[26 :]
263+ else :
264+ output = self .xml_encode (value2 )
265+ xml += tag3 .enclose (output , 3 )
266+
267+ xml += "\t " * 2 + tag1 .close_tag + "\n "
268+
269+ xml += "\t " + tag1 .close_tag + "\n "
270+
271+ self ._data = xml + tag .close_tag
272+
185273 def save (self , name : str , path : str = '.\\ ' , minimize : bool = False ):
186274 """Save to a file."""
187275 super ().save_as (name , '.xml' , path , minimize )
@@ -224,6 +312,7 @@ def open_tag(self) -> str:
224312 tag += " " + " " .join (
225313 f'{ key } ="{ XMLExport .xml_encode (value )} "'
226314 for key , value in self ._attrs .items ()
315+ if not isinstance (value , com_error )
227316 )
228317 return tag + ">"
229318
@@ -256,8 +345,8 @@ def rm_attr(self, attr):
256345class JSONExport (ExportStr ):
257346 JSON_ESCAPE_CHARS = ["\b " , "\f " , "\n " , "\r " , "\t " , "\" " , "\\ " ]
258347
259- def __init__ (self , browser : Browser , skip_func : bool = False , skip_err : bool = False ):
260- super (JSONExport , self ).__init__ (browser , skip_func , skip_err )
348+ def __init__ (self , browser : Browser , skip_func : bool = False , skip_err : bool = False , vba_form : bool = False ):
349+ super (JSONExport , self ).__init__ (browser , skip_func , skip_err , vba_form )
261350
262351 @staticmethod
263352 def json_encode (text : str ) -> str :
@@ -271,10 +360,10 @@ def json_encode(text: str) -> str:
271360 def _check (self ):
272361 """Check if the JSON string needs to be generated."""
273362 if self ._data is None :
274- self ._data = self ._generate (self ._browser )
363+ self ._data = self ._generate_vba (self ._browser )
275364 self ._data = re .sub (r',(?!\s*?[{\[\"\'\w])' , '' , self ._data )
276365
277- def _generate (self , elem , tabs : int = 0 , ** kwargs ) -> str :
366+ def _generate_vba (self , elem , tabs : int = 0 , ** kwargs ) -> str :
278367 """Recursively generate each element into a string.
279368
280369 Parameters
@@ -299,10 +388,10 @@ def _generate(self, elem, tabs: int = 0, **kwargs) -> str:
299388 if type (value ) is list and len (value ) > 0 :
300389 json += "\t " * (tabs + 1 ) + "{ \" Item\" : [\n "
301390 for i in value :
302- json += self ._generate (i , tabs + 2 )
391+ json += self ._generate_vba (i , tabs + 2 )
303392 json += "\t " * (tabs + 1 ) + "]},\n "
304393 else :
305- json += self ._generate (value , tabs + 1 , name = item )
394+ json += self ._generate_vba (value , tabs + 1 , name = item )
306395
307396 json += "\t " * tabs + "]},\n "
308397 elif isinstance (elem , FunctionViewer ):
0 commit comments