computer-playbook/sphinx/extensions/local_file_headings.py

120 lines
5.0 KiB
Python

import os
import sys
import logging as std_logging # Use the standard logging module
from sphinx.util import logging # Sphinx logging is used elsewhere if needed
from docutils.parsers.rst import Directive
from .nav_utils import natural_sort_key, extract_headings_from_file, group_headings, sort_tree, MAX_HEADING_LEVEL, DEFAULT_MAX_NAV_DEPTH
# Use standard logging to set the level based on command-line args.
logger = std_logging.getLogger(__name__)
if any(arg in sys.argv for arg in ["-v", "--verbose"]):
logger.setLevel(std_logging.DEBUG)
else:
logger.setLevel(std_logging.INFO)
DEFAULT_MAX_NAV_DEPTH = 4
def postprocess_current(nodes, current_file, current_anchor):
"""
Recursively process the tree nodes in post-order.
If a node or any child node matches the current file and anchor,
mark that node as current.
Returns True if the current subtree contains a current node.
"""
found_in_subtree = False
for node in nodes:
# Process children first (post-order)
child_found = False
if 'children' in node and node['children']:
child_found = postprocess_current(node['children'], current_file, current_anchor)
# Get the file and anchor for the current node, trimming trailing slashes and whitespace.
node_file = node.get('link', '').rstrip('/')
node_anchor = node.get('anchor', '').strip()
# Debug: output the current node's values and the comparison values.
logger.debug("Checking node: text=%s, link=%s, anchor=%s",
node.get('text', ''),
node_file,
node_anchor)
logger.debug("Comparing with current_file=%s, current_anchor=%s",
current_file.rstrip('/'),
current_anchor.strip())
# Mark node as current if it exactly matches the current file and anchor.
if node_file == current_file.rstrip('/') and node_anchor == current_anchor.strip():
node['current'] = True
logger.debug("Node '%s' marked as current (exact match).", node.get('text', ''))
found = True
else:
node['current'] = child_found
if child_found:
logger.debug("Node '%s' marked as current (child match).", node.get('text', ''))
if node['current']:
found_in_subtree = True
return found_in_subtree
def add_local_file_headings(app, pagename, templatename, context, doctree):
logger.debug("add_local_file_headings called with pagename: %s", pagename)
srcdir = app.srcdir
directory = os.path.dirname(pagename)
abs_dir = os.path.join(srcdir, directory)
if not os.path.isdir(abs_dir):
logger.warning("Directory %s not found for page %s.", abs_dir, pagename)
context['local_md_headings'] = []
return
# Get only files with .md or .rst extensions.
files = [f for f in os.listdir(abs_dir) if f.endswith('.md') or f.endswith('.rst')]
# If an index file is present, remove any readme files (case-insensitive).
files_lower = [f.lower() for f in files]
if 'index.md' in files_lower or 'index.rst' in files_lower:
files = [f for f in files if f.lower() not in ['readme.md', 'readme.rst']]
file_items = []
for file in files:
filepath = os.path.join(abs_dir, file)
headings = extract_headings_from_file(filepath, max_level=MAX_HEADING_LEVEL)
basename, _ = os.path.splitext(file)
# Set priority: index gets priority 0, otherwise 1.
priority = 0 if basename.lower() == 'index' else 1
for heading in headings:
file_link = os.path.join(directory, basename)
file_items.append({
'level': heading['level'],
'text': heading['text'],
'link': file_link,
'anchor': heading['anchor'],
'priority': priority,
'filename': basename
})
tree = group_headings(file_items)
sort_tree(tree)
logger.debug("Generated tree: %s", tree)
# Determine current file and anchor.
# This implementation assumes that if an anchor is present, it is appended to pagename as "#anchor".
if '#' in pagename:
current_file, current_anchor = pagename.split('#', 1)
else:
current_file, current_anchor = pagename, ''
logger.debug("Current file: %s, Current anchor: %s", current_file, current_anchor)
# Postprocess the tree: bubble up the 'current' flag from children to parents.
if current_anchor:
postprocess_current(tree, current_file, current_anchor)
else:
logger.debug("No anchor provided; skipping current marking.")
logger.debug("Final tree after postprocessing: %s", tree)
context['local_md_headings'] = tree
def setup(app):
app.add_config_value('local_nav_max_depth', DEFAULT_MAX_NAV_DEPTH, 'env')
app.connect('html-page-context', add_local_file_headings)
return {'version': '0.1', 'parallel_read_safe': True}