2020# software solely pursuant to the terms of the relevant commercial agreement.
2121
2222from furo .navigation import get_navigation_tree
23+ from datetime import datetime
24+ import os
25+ import re
26+
27+
28+ def _slugify_id (text ):
29+ """Normalize text to a safe HTML ID: alphanumerics and hyphens only."""
30+ s = re .sub (r'[^a-z0-9-]' , '-' , text .lower ())
31+ s = re .sub (r'-{2,}' , '-' , s ) # collapse multiple hyphens
32+ return s .strip ('-' )
33+
34+
35+ class _NavBuilder :
36+ """Helper to build navigation HTML."""
37+
38+ def __init__ (self , parts , project , master_path , toctree_fn ):
39+ self .parts = parts
40+ self .project = project
41+ self .master_path = master_path
42+ self .toctree = toctree_fn
43+
44+ def add_nav_link (self , entry_name , entry_url , li_base_class = 'toctree-l1' , border_top = False ):
45+ """Add a cross-project navigation link with expand icon.
46+
47+ Includes an empty <ul> so Furo's get_navigation_tree() adds the
48+ checkbox/icon structure. Since cross-project TOC content isn't
49+ available, clicking the icon navigates to that project instead
50+ of expanding (handled by JS in custom.js).
51+ """
52+ border = " border-top" if border_top else ""
53+ li_class = f'{ li_base_class } { border } '
54+ self .parts .append (f'<li class="{ li_class } ">' )
55+ self .parts .append (f'<a href="{ entry_url } ">{ entry_name } </a>' )
56+ # Empty <ul> triggers Furo to add has-children class and icon structure
57+ self .parts .append ('<ul></ul>' )
58+ self .parts .append ('</li>' )
59+
60+ def add_project_nav_item (
61+ self ,
62+ project_name ,
63+ display_name ,
64+ url_if_not_current ,
65+ border_top = False ,
66+ public_docs = True
67+ ):
68+ """Add a navigation item in left navbar for a project."""
69+ border = " border-top" if border_top else ""
70+ if self .project == project_name :
71+ self .parts .append (f'<li class="current{ border } ">' )
72+ self .parts .append (f'<a class="current-active" href="{ self .master_path } ">{ display_name } </a>' )
73+ self .parts .append (self .toctree ())
74+ self .parts .append ('</li>' )
75+ return
76+
77+ if public_docs :
78+ self .add_nav_link (display_name , url_if_not_current , 'navleft-item' , border_top )
2379
2480
2581def _generate_crate_navigation_html (context ):
@@ -32,7 +88,7 @@ def _generate_crate_navigation_html(context):
3288 return ""
3389
3490 theme_globaltoc_includehidden = context .get ("theme_globaltoc_includehidden" , True )
35- def get_toctree (maxdepth = - 1 , titles_only = True , collapse = False ):
91+ def _get_toctree (maxdepth = - 1 , titles_only = True , collapse = False ):
3692 return toctree (
3793 maxdepth = maxdepth ,
3894 titles_only = titles_only ,
@@ -46,35 +102,15 @@ def get_toctree(maxdepth=-1, titles_only=True, collapse=False):
46102 master_path = context ["pathto" ](master_doc )
47103
48104 parts = ['<ul class="toctree nav nav-list">' ]
49-
50- def _add_project_nav_item (
51- project_name ,
52- display_name ,
53- url_if_not_current ,
54- border_top = False ,
55- include_toctree = True ,
56- only_if_current_project = False
57- ):
58- """Add a navigation item in left navbar for a specific project."""
59- border = " border-top" if border_top else ""
60- if project == project_name :
61- parts .append (f'<li class="current{ border } ">' )
62- parts .append (f'<a class="current-active" href="{ master_path } ">{ display_name } </a>' )
63- if include_toctree :
64- parts .append (get_toctree ())
65- parts .append ('</li>' )
66- else :
67- if only_if_current_project :
68- return
69- parts .append (f'<li class="navleft-item{ border } "><a href="{ url_if_not_current } ">{ display_name } </a></li>' )
105+ builder = _NavBuilder (parts , project , master_path , _get_toctree )
70106
71107
72108 # Special project used standalone
73109 if project == 'SQL 99' :
74110 current_class = ' class="current"' if pagename == master_doc else ''
75111 parts .append (f'<li{ current_class } >' )
76112 parts .append (f'<a class="current-active" href="{ master_path } ">SQL-99 Complete, Really</a>' )
77- parts .append (get_toctree (maxdepth = 2 ))
113+ parts .append (_get_toctree (maxdepth = 2 ))
78114 parts .append ('</li>' )
79115 return '' .join (parts )
80116
@@ -86,7 +122,8 @@ def _add_project_nav_item(
86122 parts .append ('</div>' )
87123 parts .append ('</li>' )
88124
89- # Add Overview and top level entries defined in the Guide's toctree
125+ # Add Overview and top level entries defined in the Guide's toctree.
126+ # The Guide project is the only one that has multiple top-level entries.
90127 if project == 'CrateDB: Guide' :
91128 if pagename == 'index' :
92129 parts .append ('<li class="current">' )
@@ -96,24 +133,25 @@ def _add_project_nav_item(
96133 parts .append ('<li class="navleft-item">' )
97134 parts .append (f'<a href="{ master_path } ">Overview</a>' )
98135 parts .append ('</li>' )
99- parts .append (get_toctree ())
136+ parts .append (_get_toctree ())
100137 else :
101- parts .append ('<li class="current"><a class="current-active" href="#">Overview</a></li>' )
138+ # Show Overview link to Guide's index (no icon - it's just an index page)
139+ parts .append ('<li class="navleft-item"><a href="/docs/guide/">Overview</a></li>' )
140+ # Add Guide's level 1 entries with icons
141+ builder .add_nav_link ('Getting Started' ,'/docs/guide/start/' )
142+ builder .add_nav_link ('Handbook' , '/docs/guide/handbook/' )
102143
103144 # Add individual projects
104- _add_project_nav_item ('CrateDB Cloud' , 'CrateDB Cloud' , '/docs/cloud/' )
105- _add_project_nav_item ('CrateDB: Reference' , 'Reference Manual' , '/docs/crate/reference/' )
145+ builder . add_project_nav_item ('CrateDB Cloud' , 'CrateDB Cloud' , '/docs/cloud/' )
146+ builder . add_project_nav_item ('CrateDB: Reference' , 'Reference Manual' , '/docs/crate/reference/' )
106147
107148 # Start new section with a border
108- _add_project_nav_item ('CrateDB: Admin UI' , 'Admin UI' , '/docs/crate/admin-ui/' , border_top = True )
109- _add_project_nav_item ('CrateDB: Crash CLI' , 'CrateDB CLI' , '/docs/crate/crash/' )
110- _add_project_nav_item ('CrateDB Cloud: Croud CLI' , 'Cloud CLI' , '/docs/cloud/cli/' )
149+ builder . add_project_nav_item ('CrateDB: Admin UI' , 'Admin UI' , '/docs/crate/admin-ui/' , border_top = True )
150+ builder . add_project_nav_item ('CrateDB: Crash CLI' , 'CrateDB CLI' , '/docs/crate/crash/' )
151+ builder . add_project_nav_item ('CrateDB Cloud: Croud CLI' , 'Cloud CLI' , '/docs/cloud/cli/' )
111152
112153 # Add all Driver projects
113- parts .append ('<li class="navleft-item">' )
114- parts .append ('<a href="/docs/guide/connect/drivers.html">Database Drivers</a>' )
115- parts .append ('</li>' )
116-
154+ # The <ul> must be inside the same <li> for CSS sibling selectors to work
117155 _DRIVER_CONFIGS = [
118156 ('CrateDB JDBC' , 'JDBC' , '/docs/jdbc/' ),
119157 ('CrateDB DBAL' , 'PHP DBAL' , '/docs/dbal/' ),
@@ -122,22 +160,36 @@ def _add_project_nav_item(
122160 ('SQLAlchemy Dialect' , 'SQLAlchemy' , '/docs/sqlalchemy-cratedb/' ),
123161 ]
124162 driver_projects = [config [0 ] for config in _DRIVER_CONFIGS ]
125- if project in driver_projects or (project == 'CrateDB: Guide' and pagename .startswith ('connect' )):
126- parts .append ('<li><ul>' )
163+ show_drivers = project in driver_projects or (project == 'CrateDB: Guide' and pagename .startswith ('connect' ))
164+
165+ # Use data attribute to mark Database Drivers for auto-expansion
166+ driver_marker = ' data-auto-expand="true"' if show_drivers else ''
167+ parts .append (f'<li class="navleft-item"{ driver_marker } >' )
168+ parts .append ('<a href="/docs/guide/connect/drivers.html">Database Drivers</a>' )
169+ # Furo will add has-children class and icon structure when it detects the <ul>
170+ parts .append ('<ul>' )
171+ if show_drivers :
127172 for proj_name , display_name , url in _DRIVER_CONFIGS :
128- _add_project_nav_item (proj_name , display_name , url )
129- parts .append ('</ul></li>' )
173+ builder .add_project_nav_item (proj_name , display_name , url )
174+ parts .append ('</ul>' )
175+ parts .append ('</li>' )
176+
130177
131178 # Add Support and Community links section after a border
132179 parts .append ('<li class="navleft-item border-top"><a target="_blank" href="/support/">Support</a></li>' )
133180 parts .append ('<li class="navleft-item"><a target="_blank" href="https://community.cratedb.com/">Community</a></li>' )
134181
135182
136183 # Other internal docs projects only included in special builds
137- _add_project_nav_item ('CrateDB documentation theme' , 'Documentation theme' , '' ,
138- border_top = True , only_if_current_project = True )
139- _add_project_nav_item ('Doing Docs' , 'Doing Docs at CrateDB' , '' ,
140- only_if_current_project = True )
184+ builder .add_project_nav_item ('CrateDB documentation theme' , 'Documentation theme' , '' , border_top = True , public_docs = False )
185+ builder .add_project_nav_item ('Doing Docs' , 'Doing Docs at CrateDB' , '' , public_docs = False )
186+
187+ # Show build timestamp for local development (not on Read the Docs)
188+ if not os .environ .get ('READTHEDOCS' ):
189+ build_time = datetime .now ().strftime ('%Y-%m-%d %H:%M:%S' )
190+ parts .append (f'<li class="navleft-item border-top" style="font-size: 0.75em; color: #888; padding-top: 1em;">' )
191+ parts .append (f'Built: { build_time } ' )
192+ parts .append ('</li>' )
141193
142194 parts .append ('</ul>' )
143195 return '' .join (parts )
@@ -160,5 +212,16 @@ def add_crate_navigation(app, pagename, templatename, context, doctree):
160212 # Process through Furo's navigation enhancer
161213 enhanced_navigation = get_navigation_tree (navigation_html )
162214
215+ # Make checkbox IDs unique per project to prevent localStorage state collision.
216+ # Furo generates sequential IDs (toctree-checkbox-1, toctree-checkbox-2, etc.)
217+ # which collide across projects. Add project slug prefix to make them unique.
218+ project_slug = _slugify_id (context .get ("project" , "" ))
219+ if project_slug :
220+ enhanced_navigation = re .sub (
221+ r'toctree-checkbox-(\d+)' ,
222+ f'toctree-checkbox-{ project_slug } -\\ 1' ,
223+ enhanced_navigation
224+ )
225+
163226 # Add to context for use in templates
164227 context ["crate_navigation_tree" ] = enhanced_navigation
0 commit comments