"""This module provides classes to Mock the core components of the docutils.RSTParser,the key difference being that nested parsing treats the text as Markdown not rST."""from__future__importannotationsimportosimportreimportsysfrompathlibimportPathfromtypingimportTYPE_CHECKING,Anyfromdocutilsimportnodesfromdocutils.parsers.rstimportDirective,DirectiveErrorfromdocutils.parsers.rstimportParserasRSTParserfromdocutils.parsers.rst.directives.miscimportIncludefromdocutils.parsers.rst.statesimportBody,Inliner,RSTStateMachinefromdocutils.statemachineimportStringListfromdocutils.utilsimportunescapefrom.parsers.directivesimportMarkupError,parse_directive_textifTYPE_CHECKING:from.mdit_to_docutils.baseimportDocutilsRenderer
[文档]classMockingError(Exception):"""An exception to signal an error during mocking of docutils components."""
[文档]classMockInliner:"""A mock version of `docutils.parsers.rst.states.Inliner`. This is parsed to role functions. """def__init__(self,renderer:DocutilsRenderer):"""Initialize the mock inliner."""self._renderer=renderer# here we mock that the `parse` method has already been called# which is where these attributes are set (via the RST state Memo)self.document=renderer.documentself.reporter=renderer.document.reporterself.language=renderer.language_module_rstself.parent=renderer.current_nodeifnothasattr(self.reporter,"get_source_and_line"):# In docutils this is set by `RSTState.runtime_init`self.reporter.get_source_and_line=lambdali:(self.document["source"],li)self.rfc_url="rfc%d.html"
[文档]defproblematic(self,text:str,rawsource:str,message:nodes.system_message)->nodes.problematic:"""Record a system message from parsing."""msgid=self.document.set_id(message,self.parent)problematic=nodes.problematic(rawsource,text,refid=msgid)prbid=self.document.set_id(problematic)message.add_backref(prbid)returnproblematic
[文档]defparse(self,text:str,lineno:int,memo:Any,parent:nodes.Node)->tuple[list[nodes.Node],list[nodes.system_message]]:"""Parse the text and return a list of nodes."""# note the only place this is normally called,# is by `RSTState.inline_text`, or in directives: `self.state.inline_text`,# and there the state parses its own parent# self.reporter = memo.reporter# self.document = memo.document# self.language = memo.languagewithself._renderer.current_node_context(parent):# the parent is never actually appended to though,# so we make a temporary parent to parse intocontainer=nodes.Element()withself._renderer.current_node_context(container):self._renderer.nested_render_text(text,lineno,inline=True)returncontainer.children,[]
def__getattr__(self,name:str):"""This method is only be called if the attribute requested has not been defined. Defined attributes will not be overridden. """# TODO use document.reporter mechanism?ifhasattr(Inliner,name):msg=f"{type(self).__name__} has not yet implemented attribute '{name}'"raiseMockingError(msg).with_traceback(sys.exc_info()[2])msg=f"{type(self).__name__} has no attribute {name}"raiseMockingError(msg).with_traceback(sys.exc_info()[2])
[文档]classMockState:"""A mock version of `docutils.parsers.rst.states.RSTState`. This is parsed to the `Directives.run()` method, so that they may run nested parses on their content that will be parsed as markdown, rather than RST. """def__init__(self,renderer:DocutilsRenderer,state_machine:MockStateMachine,lineno:int,):self._renderer=rendererself._lineno=linenoself.document=renderer.documentself.reporter=renderer.document.reporterself.state_machine=state_machineself.inliner=MockInliner(renderer)classStruct:document=self.documentreporter=self.document.reporterlanguage=renderer.language_module_rsttitle_styles:list[str]=[]section_level=max(renderer._level_to_section)section_bubble_up_kludge=Falseinliner=self.inlinerself.memo=Struct
[文档]defparse_directive_block(self,content:StringList,line_offset:int,directive:type[Directive],option_presets:dict[str,Any],)->tuple[list[str],dict[str,Any],StringList,int]:"""Parse the full directive text :raises MarkupError: for errors in parsing the directive :returns: (arguments, options, content, content_offset) """# note this is essentially only used by the docutils `role` directiveifoption_presets:raiseMockingError("parse_directive_block: option_presets not implemented")# TODO should argument_str always be ""?parsed=parse_directive_text(directive,"","\n".join(content))ifparsed.warnings:raiseMarkupError(",".join(w.msgforwinparsed.warnings))return(parsed.arguments,parsed.options,StringList(parsed.body,source=content.source),line_offset+parsed.body_offset,)
[文档]defnested_parse(self,block:StringList,input_offset:int,node:nodes.Element,match_titles:bool=False,state_machine_class=None,state_machine_kwargs=None,)->None:"""Perform a nested parse of the input block, with ``node`` as the parent. :param block: The block of lines to parse. :param input_offset: The offset of the first line of block, to the starting line of the state (i.e. directive). :param node: The parent node to attach the parsed content to. :param match_titles: Whether to to allow the parsing of headings (normally this is false, since nested heading would break the document structure) """sm_match_titles=self.state_machine.match_titleswithself._renderer.current_node_context(node):self._renderer.nested_render_text("\n".join(block),self._lineno+input_offset,temp_root_node=nodeifmatch_titleselseNone,)self.state_machine.match_titles=sm_match_titles
[文档]defparse_target(self,block,block_text,lineno:int):""" Taken from https://github.com/docutils-mirror/docutils/blob/e88c5fb08d5cdfa8b4ac1020dd6f7177778d5990/docutils/parsers/rst/states.py#L1927 """# Commenting out this code because it only applies to rST# if block and block[-1].strip()[-1:] == "_": # possible indirect target# reference = " ".join([line.strip() for line in block])# refname = self.is_reference(reference)# if refname:# return "refname", refnamereference="".join(["".join(line.split())forlineinblock])return"refuri",unescape(reference)
[文档]definline_text(self,text:str,lineno:int)->tuple[list[nodes.Element],list[nodes.Element]]:"""Parse text with only inline rules. :returns: (list of nodes, list of messages) """returnself.inliner.parse(text,lineno,self.memo,self._renderer.current_node)
# U+2014 is an em-dash:attribution_pattern=re.compile("^((?:---?(?!-)|\u2014) *)(.+)")
[文档]defblock_quote(self,lines:list[str],line_offset:int)->list[nodes.Element]:"""Parse a block quote, which is a block of text, followed by an (optional) attribution. :: No matter where you go, there you are. -- Buckaroo Banzai """elements=[]# split attributionlast_line_blank=Falseblockquote_lines=linesattribution_lines=[]attribution_line_offset=None# First line after a blank line must begin with a dashfori,lineinenumerate(lines):ifnotline.strip():last_line_blank=Truecontinueifnotlast_line_blank:last_line_blank=Falsecontinuelast_line_blank=Falsematch=self.attribution_pattern.match(line)ifnotmatch:continueattribution_line_offset=iattribution_lines=[match.group(2)]forat_lineinlines[i+1:]:indented_line=at_line[len(match.group(1)):]iflen(indented_line)!=len(at_line.lstrip()):breakattribution_lines.append(indented_line)blockquote_lines=lines[:i]break# parse blockblockquote=nodes.block_quote()self.nested_parse(blockquote_lines,line_offset,blockquote)elements.append(blockquote)# parse attributionifattribution_lines:attribution_text="\n".join(attribution_lines)lineno=self._lineno+line_offset+(attribution_line_offsetor0)textnodes,messages=self.inline_text(attribution_text,lineno)attribution=nodes.attribution(attribution_text,"",*textnodes)(attribution.source,attribution.line,)=self.state_machine.get_source_and_line(lineno)blockquote+=attributionelements+=messagesreturnelements
[文档]defnest_line_block_lines(self,block:nodes.line_block):"""Modify the line block element in-place, to nest line block segments. Line nodes are placed into child line block containers, based on their indentation. """forindexinrange(1,len(block)):ifgetattr(block[index],"indent",None)isNone:block[index].indent=block[index-1].indentself._nest_line_block_segment(block)
def_nest_line_block_segment(self,block:nodes.line_block):indents=[item.indentforiteminblock]least=min(indents)new_items=[]new_block=nodes.line_block()foriteminblock:ifitem.indent>least:new_block.append(item)else:iflen(new_block):self._nest_line_block_segment(new_block)new_items.append(new_block)new_block=nodes.line_block()new_items.append(item)iflen(new_block):self._nest_line_block_segment(new_block)new_items.append(new_block)block[:]=new_itemsdef__getattr__(self,name:str):"""This method is only be called if the attribute requested has not been defined. Defined attributes will not be overridden. """cls=type(self).__name__msg=(f"{cls} has not yet implemented attribute '{name}'. ""You can parse RST directly via the `{{eval-rst}}` directive: ""https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html#how-directives-parse-content"ifhasattr(Body,name)elsef"{cls} has no attribute '{name}'")raiseMockingError(msg).with_traceback(sys.exc_info()[2])
[文档]classMockStateMachine:"""A mock version of `docutils.parsers.rst.states.RSTStateMachine`. This is parsed to the `Directives.run()` method. """def__init__(self,renderer:DocutilsRenderer,lineno:int):self._renderer=rendererself._lineno=linenoself.document=renderer.documentself.language=renderer.language_module_rstself.reporter=self.document.reporterself.node:nodes.Element=renderer.current_nodeself.match_titles:bool=True
[文档]defget_source_and_line(self,lineno:int|None=None):"""Return (source path, line) tuple for current or given line number."""returnself.document["source"],linenoorself._lineno
def__getattr__(self,name:str):"""This method is only be called if the attribute requested has not been defined. Defined attributes will not be overridden. """ifhasattr(RSTStateMachine,name):msg=f"{type(self).__name__} has not yet implemented attribute '{name}'"raiseMockingError(msg).with_traceback(sys.exc_info()[2])msg=f"{type(self).__name__} has no attribute {name}"raiseMockingError(msg).with_traceback(sys.exc_info()[2])
[文档]classMockIncludeDirective:"""This directive uses a lot of statemachine logic that is not yet mocked. Therefore, we treat it as a special case (at least for now). See: https://docutils.sourceforge.io/docs/ref/rst/directives.html#including-an-external-document-fragment """def__init__(self,renderer:DocutilsRenderer,name:str,klass:type[Include],arguments:list[str],options:dict[str,Any],body:list[str],lineno:int,):self.renderer=rendererself.document=renderer.documentself.name=nameself.klass=klassself.arguments=argumentsself.options=optionsself.body=bodyself.lineno=lineno
[文档]defrun(self)->list[nodes.Element]:fromdocutils.parsers.rst.directives.bodyimportCodeBlock,NumberLinesifnotself.document.settings.file_insertion_enabled:raiseDirectiveError(2,f'Directive "{self.name}" disabled.')source_dir=Path(self.document["source"]).absolute().parentinclude_arg="".join([s.strip()forsinself.arguments[0].splitlines()])ifinclude_arg.startswith("<")andinclude_arg.endswith(">"):# # docutils "standard" includespath=Path(self.klass.standard_include_path).joinpath(include_arg[1:-1])else:# if using sphinx interpret absolute paths "correctly",# i.e. relative to source directorytry:sphinx_env=self.document.settings.envexceptAttributeError:passelse:_,include_arg=sphinx_env.relfn2path(self.arguments[0])sphinx_env.note_included(include_arg)path=Path(include_arg)path=source_dir.joinpath(path)# this ensures that the parent file is rebuilt if the included file changesself.document.settings.record_dependencies.add(str(path))# read fileencoding=self.options.get("encoding",self.document.settings.input_encoding)error_handler=self.document.settings.input_encoding_error_handler# tab_width = self.options.get("tab-width", self.document.settings.tab_width)try:file_content=path.read_text(encoding=encoding,errors=error_handler)exceptFileNotFoundErroraserror:raiseDirectiveError(4,f'Directive "{self.name}": file not found: {str(path)!r}')fromerrorexceptExceptionaserror:raiseDirectiveError(4,f'Directive "{self.name}": error reading file: {path}\n{error}.')fromerrorifself.renderer.sphinx_envisnotNone:# Emit the "include-read" event# see: https://github.com/sphinx-doc/sphinx/commit/ff18318613db56d0000db47e5c8f0140556cef0carg=[file_content]relative_path=Path(os.path.relpath(path,start=self.renderer.sphinx_env.srcdir))parent_docname=Path(self.renderer.document["source"]).stemself.renderer.sphinx_env.app.events.emit("include-read",relative_path,parent_docname,arg,)file_content=arg[0]# get required section of textstartline=self.options.get("start-line",None)endline=self.options.get("end-line",None)file_content="\n".join(file_content.splitlines()[startline:endline])startline=startlineor0forsplit_on_typein["start-after","end-before"]:split_on=self.options.get(split_on_type,None)ifnotsplit_on:continuesplit_index=file_content.find(split_on)ifsplit_index<0:raiseDirectiveError(4,f'Directive "{self.name}"; option "{split_on_type}": text not found "{split_on}".',)ifsplit_on_type=="start-after":startline+=split_index+len(split_on)file_content=file_content[split_index+len(split_on):]else:file_content=file_content[:split_index]if"literal"inself.options:literal_block=nodes.literal_block(file_content,source=str(path),classes=self.options.get("class",[]))literal_block.line=1# TODO don;t think this should be 1?self.add_name(literal_block)if"number-lines"inself.options:try:startline=int(self.options["number-lines"]or1)exceptValueErroraserr:raiseDirectiveError(3,":number-lines: with non-integer start value")fromerrendline=startline+len(file_content.splitlines())iffile_content.endswith("\n"):file_content=file_content[:-1]tokens=NumberLines([([],file_content)],startline,endline)forclasses,valueintokens:ifclasses:literal_block+=nodes.inline(value,value,classes=classes)else:literal_block+=nodes.Text(value)else:literal_block+=nodes.Text(file_content)return[literal_block]if"code"inself.options:self.options["source"]=str(path)state_machine=MockStateMachine(self.renderer,self.lineno)state=MockState(self.renderer,state_machine,self.lineno)codeblock=CodeBlock(name=self.name,arguments=[self.options.pop("code")],options=self.options,content=file_content.splitlines(),lineno=self.lineno,content_offset=0,block_text=file_content,state=state,state_machine=state_machine,)returncodeblock.run()# Here we perform a nested render, but temporarily setup the document/reporter# with the correct document path and lineno for the included file.source=self.renderer.document["source"]rsource=self.renderer.reporter.sourceline_func=getattr(self.renderer.reporter,"get_source_and_line",None)try:self.renderer.document["source"]=str(path)self.renderer.reporter.source=str(path)self.renderer.reporter.get_source_and_line=lambdali:(str(path),li)if"relative-images"inself.options:self.renderer.md_env["relative-images"]=os.path.relpath(path.parent,source_dir)if"relative-docs"inself.options:self.renderer.md_env["relative-docs"]=(self.options["relative-docs"],source_dir,path.parent,)self.renderer.nested_render_text(file_content,startline+1,heading_offset=self.options.get("heading-offset",0),)finally:self.renderer.document["source"]=sourceself.renderer.reporter.source=rsourceself.renderer.md_env.pop("relative-images",None)self.renderer.md_env.pop("relative-docs",None)ifline_funcisnotNone:self.renderer.reporter.get_source_and_line=line_funcelse:delself.renderer.reporter.get_source_and_linereturn[]
[文档]defadd_name(self,node:nodes.Element):"""Append self.options['name'] to node['names'] if it exists. Also normalize the name string and register it as explicit target. """if"name"inself.options:name=nodes.fully_normalize_name(self.options.pop("name"))if"name"innode:delnode["name"]node["names"].append(name)self.renderer.document.note_explicit_target(node,node)
[文档]classMockRSTParser(RSTParser):"""RSTParser which avoids a negative side effect."""
[文档]defparse(self,inputstring:str,document:nodes.document):"""Parse the input to populate the document AST."""fromdocutils.parsers.rstimportrolesshould_restore=Falseif""inroles._roles:should_restore=Trueblankrole=roles._roles[""]super().parse(inputstring,document)ifshould_restore:roles._roles[""]=blankrole