classGraph:"""Represents the connection graph between files."""LINK_PATTERN=r"\[[^\]]+\]\((?P<url>.*?)\)|\[\[(?P<wikilink>[^\]]+)\]\]"def__init__(self,config):"""Initializes the graph data structure."""ifconfig.get("debug",False):logger.setLevel("DEBUG")logger.debug("Graph initialized")self.nodes=[]self.edges=[]self.config=configdef_create_nodes(self,files:Files):"""Create nodes from the file collection."""logger.debug("Creating nodes...")documentation_pages=list(files.documentation_pages())logger.debug(f"Found {len(documentation_pages)} documentation pages")forfileindocumentation_pages:iffile.page:name=self._get_name_from_config(file.page)self.nodes.append({"id":file.src_path,"path":file.abs_src_path,"name":name,"url":file.url,},)logger.info(f"Created {len(self.nodes)} nodes")def_get_name_from_config(self,page:Page)->str:"""Return the name of the node based on the plugin configuration."""ifself.config["name"]=="title":logger.debug(f"Using 'title' for node name for page '{page.title}'")if"title"inpage.meta:returnstr(page.meta["title"])ifpage.titleisnotNone:returnstr(page.title)logger.debug(f"Using 'file_name' for node name for page '{page.title}'")returnpage.file.namedef_unescape_url(self,url:str)->str:"""Unescape a URL."""# Strip angle brackets if present (for links like [text](<url>))ifurl.startswith("<")andurl.endswith(">"):url=url[1:-1]returnunquote(url)def_normalize_link(self,match:re.Match)->Optional[str]:"""Normalize the URL from a regex match."""url=match.group("url")ormatch.group("wikilink")ifnoturl:returnNone# For wikilinks, add the .md extensionifmatch.group("wikilink")andnoturl.endswith(".md"):url+=".md"url=self._unescape_url(url)# Remove query and fragment from the URLurl=urlsplit(url).pathreturnurldef_find_links(self,markdown:str,node_id:str,files:Files)->Iterator[dict]:"""Find all links in a markdown string and yield resolved edges."""formatchinre.finditer(self.LINK_PATTERN,markdown):url=self._normalize_link(match)ifnoturl:continuetarget_path=os.path.normpath(os.path.join(os.path.dirname(node_id),url))# Check if the target is a node in the graphifany(node["id"]==target_pathfornodeinself.nodes):yield{"source":node_id,"target":target_path}def_create_edges(self,files:Files):"""Create edges by parsing links from markdown files."""logger.debug("Creating edges...")fornodeinself.nodes:logger.debug(f"Parsing file {node['path']} for links")try:withopen(node["path"],"r",encoding="utf-8")asf:markdown=f.read()self.edges.extend(self._find_links(markdown,node["id"],files))exceptFileNotFoundError:logger.warning(f"File not found: {node['path']}")# This should not happen if the file is in the `files` collectionpasslogger.info(f"Created {len(self.edges)} edges")def__call__(self,files:Files):"""Build the graph from the file collection."""logger.info("Building graph...")self._create_nodes(files)self._create_edges(files)returnselfdefto_dict(self):"""Return the graph as a dictionary."""return{"nodes":self.nodes,"edges":self.edges}
def__call__(self,files:Files):"""Build the graph from the file collection."""logger.info("Building graph...")self._create_nodes(files)self._create_edges(files)returnself
def__init__(self,config):"""Initializes the graph data structure."""ifconfig.get("debug",False):logger.setLevel("DEBUG")logger.debug("Graph initialized")self.nodes=[]self.edges=[]self.config=config
defcopy_static_assets(static_dir:str,config:MkDocsConfig)->None:"""Copy static assets into the site directory. Args: config (MkDocsConfig): The MkDocs configuration. """# Copy JSjs_output_dir=os.path.join(config["site_dir"],"js")os.makedirs(js_output_dir,exist_ok=True)shutil.copy(os.path.join(static_dir,"graph.js"),js_output_dir)# Copy CSScss_output_dir=os.path.join(config["site_dir"],"css")os.makedirs(css_output_dir,exist_ok=True)shutil.copy(os.path.join(static_dir,"graph.css"),css_output_dir)
definject_graph_script(output:str,config:MkDocsConfig,debug:bool=False)->str:"""Inject the graph script into the HTML page. Args: output (str): The HTML output. config (MkDocsConfig): The MkDocs configuration. debug (bool): Whether to enable debug mode. Returns: str: The HTML with the graph script injected. """site_url=config.get("site_url")ifsite_url:base_path=urlparse(site_url).path# Ensure base_path ends with a slashifnotbase_path.endswith("/"):base_path+="/"else:base_path="/"options_script=("<script>"f"window.graph_options = {{"f" debug: {str(debug).lower()},"f" base_path: '{base_path}'"f"}};""</script>")if"</body>"inoutput:returnoutput.replace("</body>",f"{options_script}</body>")returnoutput