import os from sphinx.util import logging from .nav_utils import extract_headings_from_file, MAX_HEADING_LEVEL logger = logging.getLogger(__name__) CANDIDATES = ['index.rst', 'readme.md', 'main.rst'] def collect_folder_tree(dir_path, base_url): """ Recursively collects the folder tree starting from the given directory. For each folder: - Hidden folders (names starting with a dot) are skipped. - A folder is processed only if it contains one of the representative files: index.rst, index.md, readme.md, or readme.rst. - The first heading of the representative file is used as the folder title. - The representative file is not listed as a file in the folder. - All other Markdown and reStructuredText files are listed without sub-headings, using their first heading as the file title. """ # Skip hidden directories if os.path.basename(dir_path).startswith('.'): return None # List all files in the current directory with .md or .rst extension files = [f for f in os.listdir(dir_path) if os.path.isfile(os.path.join(dir_path, f)) and (f.endswith('.md') or f.endswith('.rst'))] # Find representative file for folder title using index or readme rep_file = None for candidate in CANDIDATES: for f in files: if f.lower() == candidate: rep_file = f break if rep_file: break # Skip this folder if no representative file exists if not rep_file: return None rep_path = os.path.join(dir_path, rep_file) headings = extract_headings_from_file(rep_path, max_level=MAX_HEADING_LEVEL) folder_title = headings[0]['text'] if headings else os.path.basename(dir_path) folder_link = os.path.join(base_url, os.path.splitext(rep_file)[0]) # Remove the representative file from the list to avoid duplication, # and filter out any additional "readme.md" or "index.rst" files. files.remove(rep_file) files = [f for f in files if f.lower() not in CANDIDATES] # Process the remaining files in the current directory file_items = [] for file in sorted(files, key=lambda s: s.lower()): file_path = os.path.join(dir_path, file) file_headings = extract_headings_from_file(file_path, max_level=MAX_HEADING_LEVEL) file_title = file_headings[0]['text'] if file_headings else file file_base = os.path.splitext(file)[0] file_link = os.path.join(base_url, file_base) file_items.append({ 'level': 1, 'text': file_title, 'link': file_link, 'anchor': '', 'priority': 1, 'filename': file }) # Process subdirectories (ignoring hidden ones) dir_items = [] for item in sorted(os.listdir(dir_path), key=lambda s: s.lower()): full_path = os.path.join(dir_path, item) if os.path.isdir(full_path) and not item.startswith('.'): subtree = collect_folder_tree(full_path, os.path.join(base_url, item)) if subtree: dir_items.append(subtree) # Combine files and subdirectories as children of the current folder children = file_items + dir_items return { 'text': folder_title, 'link': folder_link, 'children': children, 'filename': os.path.basename(dir_path) } def mark_current(node, active): """ Recursively mark nodes as current if the active page (pagename) matches the node's link or is a descendant of it. The function sets node['current'] = True if: - The node's link matches the active page exactly, or - The active page begins with the node's link plus a separator (indicating a child). Additionally, if any child node is current, the parent is marked as current. """ is_current = False node_link = node.get('link', '').rstrip('/') active = active.rstrip('/') if node_link and (active == node_link or active.startswith(node_link + '/')): is_current = True # Recurse into children if they exist children = node.get('children', []) for child in children: if mark_current(child, active): is_current = True node['current'] = is_current return is_current def add_local_subfolders(app, pagename, templatename, context, doctree): """ Sets the 'local_subfolders' context variable with the entire folder tree starting from app.srcdir, and marks the tree with the 'current' flag up to the active page. """ root_dir = app.srcdir folder_tree = collect_folder_tree(root_dir, '') if folder_tree: mark_current(folder_tree, pagename) context['local_subfolders'] = [folder_tree] if folder_tree else [] def setup(app): app.connect('html-page-context', add_local_subfolders) return {'version': '0.1', 'parallel_read_safe': True}