"""A post-transform for overriding the behaviour of sphinx reference resolution.This is applied to MyST type references only, such as ``[text](target)``,and allows for nested syntax"""from__future__importannotationsimportrefromtypingimportAny,castfromdocutilsimportnodesfromdocutils.nodesimportElement,documentfrommarkdown_it.common.normalize_urlimportnormalizeLinkfromsphinximportaddnodesfromsphinx.addnodesimportpending_xreffromsphinx.domains.stdimportStandardDomainfromsphinx.errorsimportNoUrifromsphinx.ext.intersphinximportInventoryAdapterfromsphinx.transforms.post_transformsimportReferencesResolverfromsphinx.utilimportdocname_join,loggingfromsphinx.util.nodesimportclean_astext,make_refnodefrommyst_parserimportinventoryfrommyst_parser._compatimportfindallfrommyst_parser.warnings_importMystWarningsLOGGER=logging.getLogger(__name__)
[文档]classMystReferenceResolver(ReferencesResolver):"""Resolves cross-references on doctrees. Overrides default sphinx implementation, to allow for nested syntax """default_priority=9# higher priority than ReferencesResolver (10)
[文档]deflog_warning(self,target:None|str,msg:str,subtype:MystWarnings,**kwargs:Any):"""Log a warning, with a myst type and specific subtype."""# MyST references are warned about by default (the same as the `any` role)# However, warnings can also be ignored by adding ("myst", target)# nitpick_ignore/nitpick_ignore_regex lists# https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-nitpickyif(targetandself.config.nitpick_ignoreand("myst",target)inself.config.nitpick_ignore):returnif(targetandself.config.nitpick_ignore_regexandany((re.fullmatch(ignore_type,"myst")andre.fullmatch(ignore_target,target))forignore_type,ignore_targetinself.config.nitpick_ignore_regex)):returnLOGGER.warning(msg,type="myst",subtype=subtype.value,**kwargs)
[文档]defrun(self,**kwargs:Any)->None:self.document:documentfornodeinfindall(self.document)(addnodes.pending_xref):ifnode["reftype"]!="myst":continueifnode["refdomain"]=="doc":self.resolve_myst_ref_doc(node)continuenewnode=Nonecontnode=cast(nodes.TextElement,node[0].deepcopy())target=node["reftarget"]refdoc=node.get("refdoc",self.env.docname)search_domains:None|list[str]=self.env.config.myst_ref_domains# try to resolve the reference within the local project,# this asks all domains to resolve the reference,# return None if no domain could resolve the reference# or returns the first result, and logs a warning if# multiple domains resolved the referencetry:newnode=self.resolve_myst_ref_any(refdoc,node,contnode,search_domains)exceptNoUri:newnode=contnodeifnewnodeisNone:# If no local domain could resolve the reference, try to# resolve it as an inter-sphinx referencenewnode=self._resolve_myst_ref_intersphinx(node,contnode,target,search_domains)ifnewnodeisNone:# if still not resolved, log a warning,self.log_warning(target,f"'myst' cross-reference target not found: {target!r}",MystWarnings.XREF_MISSING,location=node,)# if the target could not be found, then default to using an external linkifnotnewnode:newnode=nodes.reference()newnode["refid"]=normalizeLink(target)newnode.append(node[0].deepcopy())# ensure the output node has some contentif(len(newnode.children)==1andisinstance(newnode[0],nodes.inline)andnot(newnode[0].children)):newnode[0].replace_self(nodes.literal(target,target))elifnotnewnode.children:newnode.append(nodes.literal(target,target))node.replace_self(newnode)
[文档]defresolve_myst_ref_doc(self,node:pending_xref):"""Resolve a reference, from a markdown link, to another document, optionally with a target id within that document. """from_docname=node.get("refdoc",self.env.docname)ref_docname:str=node["reftarget"]ref_id:str|None=node["reftargetid"]ifref_docnamenotinself.env.all_docs:self.log_warning(ref_docname,f"Unknown source document {ref_docname!r}",MystWarnings.XREF_MISSING,location=node,)node.replace_self(node[0].deepcopy())returntargetid=""implicit_text=""inner_classes=["std","std-doc"]ifref_id:slug_to_section=self.env.metadata[ref_docname].get("myst_slugs",{})ifref_idnotinslug_to_section:self.log_warning(ref_id,f"local id not found in doc {ref_docname!r}: {ref_id!r}",MystWarnings.XREF_MISSING,location=node,)targetid=ref_idelse:_,targetid,implicit_text=slug_to_section[ref_id]inner_classes=["std","std-ref"]else:implicit_text=clean_astext(self.env.titles[ref_docname])ifnode["refexplicit"]:caption=node.astext()innernode=nodes.inline(caption,"",classes=inner_classes)innernode.extend(node[0].children)else:innernode=nodes.inline(implicit_text,implicit_text,classes=inner_classes)assertself.app.buildertry:ref_node=make_refnode(self.app.builder,from_docname,ref_docname,targetid,innernode)exceptNoUri:ref_node=innernodenode.replace_self(ref_node)
[文档]defresolve_myst_ref_any(self,refdoc:str,node:pending_xref,contnode:Element,only_domains:None|list[str],)->Element|None:"""Resolve reference generated by the "myst" role; ``[text](#reference)``. This builds on the sphinx ``any`` role to also resolve: - Document references with extensions; ``[text](./doc.md)`` - Document references with anchors with anchors; ``[text](./doc.md#target)`` - Nested syntax for explicit text with std:doc and std:ref; ``[**nested**](reference)`` """target:str=node["reftarget"]results:list[tuple[str,Element]]=[]# resolve standard referencesres=self._resolve_ref_nested(node,refdoc)ifres:results.append(("std:ref",res))# resolve doc namesres=self._resolve_doc_nested(node,refdoc)ifres:results.append(("std:doc",res))assertself.app.builder# next resolve for any other standard reference objectsifonly_domainsisNoneor"std"inonly_domains:stddomain=cast(StandardDomain,self.env.get_domain("std"))forobjtypeinstddomain.object_types:key=(objtype,target)ifobjtype=="term":key=(objtype,target.lower())ifkeyinstddomain.objects:docname,labelid=stddomain.objects[key]domain_role="std:"+(stddomain.role_for_objtype(objtype)or"")ref_node=make_refnode(self.app.builder,refdoc,docname,labelid,contnode)results.append((domain_role,ref_node))# finally resolve for any other type of allowed reference domainfordomaininself.env.domains.values():ifdomain.name=="std":continue# we did this one alreadyifonly_domainsisnotNoneanddomain.namenotinonly_domains:continuetry:results.extend(domain.resolve_any_xref(self.env,refdoc,self.app.builder,target,node,contnode))exceptNotImplementedError:# the domain doesn't yet support the new interface# we have to manually collect possible references (SLOW)ifnot(getattr(domain,"__module__","").startswith("sphinx.")):self.log_warning(None,f"Domain '{domain.__module__}::{domain.name}' has not ""implemented a `resolve_any_xref` method",MystWarnings.LEGACY_DOMAIN,once=True,)forroleindomain.roles:res=domain.resolve_xref(self.env,refdoc,self.app.builder,role,target,node,contnode)ifresandlen(res)andisinstance(res[0],nodes.Element):results.append((f"{domain.name}:{role}",res))# now, see how many matches we got...ifnotresults:returnNoneiflen(results)>1:defstringify(name,node):reftitle=node.get("reftitle",node.astext())returnf":{name}:`{reftitle}`"candidates=" or ".join(stringify(name,role)forname,roleinresults)self.log_warning(target,f"more than one target found for 'myst' cross-reference {target}: "f"could be {candidates}",MystWarnings.XREF_AMBIGUOUS,location=node,)res_role,newnode=results[0]# Override "myst" class with the actual role type to get the styling# approximately correct.res_domain=res_role.split(":")[0]iflen(newnode)>0andisinstance(newnode[0],nodes.Element):newnode[0]["classes"]=newnode[0].get("classes",[])+[res_domain,res_role.replace(":","-"),]returnnewnode
def_resolve_ref_nested(self,node:pending_xref,fromdocname:str,target=None)->Element|None:"""This is the same as ``sphinx.domains.std._resolve_ref_xref``, but allows for nested syntax, rather than converting the inner node to raw text. """stddomain=cast(StandardDomain,self.env.get_domain("std"))target=targetornode["reftarget"].lower()ifnode["refexplicit"]:# reference to anonymous label; the reference uses# the supplied link captiondocname,labelid=stddomain.anonlabels.get(target,("",""))sectname=node.astext()innernode=nodes.inline(sectname,"")innernode.extend(node[0].children)else:# reference to named label; the final node will# contain the section name after the labeldocname,labelid,sectname=stddomain.labels.get(target,("","",""))innernode=nodes.inline(sectname,sectname)ifnotdocname:returnNoneassertself.app.builderreturnmake_refnode(self.app.builder,fromdocname,docname,labelid,innernode)def_resolve_doc_nested(self,node:pending_xref,fromdocname:str)->Element|None:"""This is the same as ``sphinx.domains.std._resolve_doc_xref``, but allows for nested syntax, rather than converting the inner node to raw text. It also allows for extensions on document names. """docname=docname_join(node.get("refdoc",fromdocname),node["reftarget"])ifdocnamenotinself.env.all_docs:returnNoneifnode["refexplicit"]:# reference with explicit titlecaption=node.astext()innernode=nodes.inline(caption,"",classes=["doc"])innernode.extend(node[0].children)else:caption=clean_astext(self.env.titles[docname])innernode=nodes.inline(caption,caption,classes=["doc"])assertself.app.builderreturnmake_refnode(self.app.builder,fromdocname,docname,"",innernode)def_resolve_myst_ref_intersphinx(self,node:nodes.Element,contnode:nodes.Element,target:str,only_domains:list[str]|None,)->None|nodes.reference:"""Resolve a myst reference to an intersphinx inventory."""matches=[mformininventory.filter_sphinx_inventories(InventoryAdapter(self.env).named_inventory,targets=target,)ifonly_domainsisNoneorm.domaininonly_domains]ifnotmatches:returnNoneiflen(matches)>1:# log a warning if there are multiple matchesshow_num=3matches_str=", ".join([inventory.filter_string(m.inv,m.domain,m.otype,m.name)forminmatches[:show_num]])iflen(matches)>show_num:matches_str+=", ..."self.log_warning(target,f"Multiple matches found for {target!r}: {matches_str}",MystWarnings.IREF_AMBIGUOUS,location=node,)# get the first match and create a reference nodematch=matches[0]newnode=nodes.reference("","",internal=False,refuri=match.loc)if"reftitle"innode:newnode["reftitle"]=node["reftitle"]else:newnode["reftitle"]=f"{match.project}{match.version}".strip()ifnode.get("refexplicit"):newnode.append(contnode)elifmatch.text:newnode.append(contnode.__class__(match.text,match.text,classes=["iref","myst"]))else:newnode.append(nodes.literal(match.name,match.name,classes=["iref","myst"]))returnnewnode