@@ -80,6 +80,176 @@ def load_items_in_file(filename):
8080 return items
8181
8282
83+ def convert_navtree_format (js_content ):
84+ """
85+ Fix Doxygen 1.15+ NAVTREE format.
86+ Doxygen 1.15 incorrectly wraps all top-level items as children of the first item.
87+ Instead of unwrapping (which breaks breadcrumbs), we'll just reorder items and
88+ use CSS to hide the wrapper node.
89+
90+ Incorrect (Doxygen 1.15):
91+ var NAVTREE = [ ["Root", "index.html", [["Item1",...], ["Deprecated",...], ["Item2",...]] ] ];
92+
93+ We'll reorder to:
94+ var NAVTREE = [ ["Root", "index.html", [["Item1",...], ["Item2",...], ["Deprecated",...]] ] ];
95+ """
96+ # Find NAVTREE definition
97+ navtree_pattern = r'var NAVTREE\s*=\s*\[(.*?)\];'
98+ match = re .search (navtree_pattern , js_content , re .DOTALL )
99+
100+ if not match :
101+ print ("WARNING: NAVTREE not found, skipping format conversion" )
102+ return js_content
103+
104+ navtree_content = match .group (1 ).strip ()
105+
106+ print ("Checking NAVTREE format..." )
107+
108+ # Parse the NAVTREE array
109+ try :
110+ # Wrap in array brackets to make it valid JSON
111+ navtree_array = json .loads ('[' + navtree_content + ']' )
112+ except json .JSONDecodeError as e :
113+ print (f"WARNING: Could not parse NAVTREE as JSON: { e } " )
114+ return js_content
115+
116+ # Check if this is the Doxygen 1.15 format:
117+ # - Single top-level item
118+ # - That item has an array of children as its 3rd element
119+ if len (navtree_array ) == 1 and isinstance (navtree_array [0 ], list ) and len (navtree_array [0 ]) == 3 :
120+ root_item = navtree_array [0 ]
121+ children = root_item [2 ]
122+
123+ if isinstance (children , list ) and len (children ) > 0 :
124+ print (f"Detected Doxygen 1.15 format with wrapper containing { len (children )} items" )
125+
126+ # Doxygen 1.15 reorders items incorrectly
127+ # Correct order should be: Binary Ninja C++ API, Topics, Namespaces, Classes, Deprecated List
128+ # Doxygen 1.15 puts: Binary Ninja C++ API, Deprecated List, Topics, Namespaces, Classes
129+ # Let's reorder to put Deprecated List at the end
130+ deprecated_idx = None
131+ for i , item in enumerate (children ):
132+ if item [0 ] == "Deprecated List" :
133+ deprecated_idx = i
134+ break
135+
136+ if deprecated_idx is not None and deprecated_idx > 0 :
137+ # Move Deprecated List to the end
138+ deprecated_item = children .pop (deprecated_idx )
139+ children .append (deprecated_item )
140+ print (f"Reordered: moved 'Deprecated List' from position { deprecated_idx } to end" )
141+
142+ # Keep the wrapper structure, just with reordered children
143+ root_item [2 ] = children
144+ fixed_array = [root_item ]
145+
146+ # Convert back to JavaScript
147+ fixed_json = json .dumps (fixed_array , indent = None , separators = (',' , ':' ))
148+
149+ # Replace in original content
150+ new_navtree_def = f'var NAVTREE = { fixed_json } ;'
151+ result = js_content [:match .start ()] + new_navtree_def + js_content [match .end ():]
152+
153+ # Adjust NAVTREEINDEX breadcrumbs to account for the reordering
154+ result = adjust_breadcrumbs_for_reorder (result , deprecated_idx )
155+
156+ print (f"Fixed NAVTREE: kept wrapper structure, reordered children" )
157+ return result
158+
159+ print ("NAVTREE format appears correct, no conversion needed" )
160+ return js_content
161+
162+
163+ def adjust_breadcrumbs_for_reorder (js_content , deprecated_moved_from_idx ):
164+ """
165+ Adjust breadcrumbs in NAVTREEINDEX to account for moving Deprecated List.
166+
167+ Original order (Doxygen 1.15): [0: Binary Ninja, 1: Deprecated, 2: Topics, 3: Namespaces, 4: Classes]
168+ New order after reorder: [0: Binary Ninja, 1: Topics, 2: Namespaces, 3: Classes, 4: Deprecated]
169+
170+ So breadcrumbs need adjustment:
171+ - Items at old index 1 (Deprecated) -> new index 4
172+ - Items at old index 2+ -> subtract 1
173+ """
174+ if deprecated_moved_from_idx is None :
175+ return js_content
176+
177+ print (f"Adjusting NAVTREEINDEX breadcrumbs for reordering (Deprecated moved from { deprecated_moved_from_idx } to end)..." )
178+
179+ result = []
180+ pos = 0
181+ adjusted_count = 0
182+
183+ while True :
184+ # Find next NAVTREEINDEX variable
185+ match = re .search (r'var (NAVTREEINDEX\d*)\s*=\s*\{' , js_content [pos :])
186+ if not match :
187+ result .append (js_content [pos :])
188+ break
189+
190+ # Add everything before this match
191+ result .append (js_content [pos :pos + match .start ()])
192+
193+ var_name = match .group (1 )
194+ obj_start = pos + match .end () - 1 # Position of the opening {
195+
196+ # Find the matching closing } by counting braces
197+ brace_count = 1
198+ i = obj_start + 1
199+ while i < len (js_content ) and brace_count > 0 :
200+ if js_content [i ] == '{' :
201+ brace_count += 1
202+ elif js_content [i ] == '}' :
203+ brace_count -= 1
204+ i += 1
205+
206+ if brace_count != 0 :
207+ print (f"WARNING: Could not find closing brace for { var_name } " )
208+ result .append (js_content [pos :])
209+ break
210+
211+ obj_end = i # Position after the closing }
212+ obj_content = js_content [obj_start :obj_end ]
213+
214+ try :
215+ # Parse the index object
216+ index_obj = json .loads (obj_content )
217+ adjusted_obj = {}
218+
219+ for key , breadcrumbs in index_obj .items ():
220+ if isinstance (breadcrumbs , list ) and len (breadcrumbs ) > 0 :
221+ # Adjust the first breadcrumb index for the reordering
222+ first_idx = breadcrumbs [0 ]
223+ new_breadcrumbs = breadcrumbs .copy ()
224+
225+ if first_idx == deprecated_moved_from_idx :
226+ # This points to Deprecated List, now at the end
227+ new_breadcrumbs [0 ] = 4
228+ elif first_idx > deprecated_moved_from_idx :
229+ # This was after Deprecated, shift down by 1
230+ new_breadcrumbs [0 ] = first_idx - 1
231+
232+ adjusted_obj [key ] = new_breadcrumbs
233+ else :
234+ adjusted_obj [key ] = breadcrumbs
235+
236+ # Convert back to JavaScript
237+ adjusted_json = json .dumps (adjusted_obj , indent = None , separators = (',' , ':' ))
238+ result .append (f'var { var_name } = { adjusted_json } ;' )
239+ adjusted_count += 1
240+
241+ except json .JSONDecodeError as e :
242+ print (f"WARNING: Could not parse { var_name } : { e } " )
243+ result .append (js_content [pos :obj_end ])
244+
245+ pos = obj_end
246+
247+ print (f"Adjusted { adjusted_count } NAVTREEINDEX variables for reordering" )
248+ return '' .join (result )
249+
250+
251+
252+
83253def replace_getScript_function (js_content , replacement_func ):
84254 """
85255 Robustly find and replace the getScript function definition.
@@ -134,6 +304,12 @@ def minifier():
134304 for mod in load_items_in_file ("html/annotated.js" ):
135305 navtree_built_data += mod + "\n "
136306
307+ # Load navtreedata.js which contains NAVTREE and NAVTREEINDEX definitions (Doxygen 1.15+)
308+ if os .path .exists ("html/navtreedata.js" ):
309+ with open ("html/navtreedata.js" , "r" ) as fp :
310+ navtree_built_data += fp .read () + "\n "
311+ deletion_queue .append ("html/navtreedata.js" )
312+
137313 # The navtree indices also need to be loaded in since we're modifying how navbar.js::getScript works.
138314 # This also saves another ~60 files.
139315 for nav_tree_index_file in os .listdir ("html" ):
@@ -145,18 +321,23 @@ def minifier():
145321 while "\n \n " in navtree_built_data :
146322 navtree_built_data = navtree_built_data .replace ("\n \n " , "\n " )
147323
324+ # Fix Doxygen 1.15 NAVTREE format BEFORE removing newlines
325+ navtree_built_data = convert_navtree_format (navtree_built_data )
326+
148327 navtree_built_data = navtree_built_data .replace ("\n " , "" )
149328
150329 fp = open ("html/navtree.js" , "r" )
151330 navtree_orig = fp .read ()
152331 fp .close ()
153332
154333 # getScript(scriptName,func,show) here originally loads the js file and calls func once that is complete
155- # Here, we just want to skip the whole process and immediately call the callback.
334+ # Here, we just want to skip the whole process and call the callback.
335+ # We use setTimeout(0) to make it async, which may be important for the tree sync logic
156336 # This replacement works across different Doxygen versions (tested with 1.12.0, 1.14.0, and 1.15.0)
157- nav_tree_fixed_get_script = "function getScript(scriptName,func,show) { func( ); }"
337+ nav_tree_fixed_get_script = "function getScript(scriptName,func,show) { setTimeout(func, 0 ); }"
158338
159339 nav_tree_fixed = replace_getScript_function (navtree_orig , nav_tree_fixed_get_script )
340+
160341 navtree = navtree_built_data + "\n " + nav_tree_fixed
161342
162343 fp = open ("html/navtree.js" , "w" )
@@ -200,6 +381,31 @@ def build_doxygen(args):
200381 print (f"Created docset with status code { stat } " )
201382
202383
384+ def remove_navtreedata_references ():
385+ """
386+ Remove references to navtreedata.js from HTML files since we've inlined it into navtree.js
387+ """
388+ import glob
389+ html_files = glob .glob ("html/**/*.html" , recursive = True )
390+ count = 0
391+ for html_file in html_files :
392+ with open (html_file , 'r' ) as f :
393+ content = f .read ()
394+
395+ # Remove the navtreedata.js script tag
396+ if 'navtreedata.js' in content :
397+ new_content = re .sub (
398+ r'<script type="text/javascript" src="navtreedata\.js"></script>\s*\n' ,
399+ '' ,
400+ content
401+ )
402+ with open (html_file , 'w' ) as f :
403+ f .write (new_content )
404+ count += 1
405+
406+ print (f'Removed navtreedata.js references from { count } HTML files' )
407+
408+
203409def main ():
204410 parser = argparse .ArgumentParser (prog = sys .argv [0 ])
205411 parser .add_argument ("--docset" , action = "store_true" , default = False , help = "Generate Dash docset" )
@@ -209,6 +415,7 @@ def main():
209415 print ("Minifying Output" )
210416 if os .path .exists ("html/navtree.js" ):
211417 minifier ()
418+ remove_navtreedata_references ()
212419 for file in deletion_queue :
213420 file = "./" + file
214421 os .remove (file )
0 commit comments