"""MyST Markdown parser for docutils."""fromcollections.abcimportCallable,Iterable,SequencefromdataclassesimportFieldfromtypingimport(Any,Literal,get_args,get_origin,)importyamlfromdocutilsimportfrontend,nodesfromdocutils.coreimportdefault_description,publish_cmdline,publish_stringfromdocutils.frontendimportfilter_settings_specfromdocutils.parsers.rstimportParserasRstParserfromdocutils.writers.html5_polyglotimportHTMLTranslator,Writerfrommyst_parser.config.mainimport(MdParserConfig,TopmatterReadError,merge_file_level,read_topmatter,)frommyst_parser.mdit_to_docutils.baseimportDocutilsRendererfrommyst_parser.mdit_to_docutils.transformsimport(CollectFootnotes,ResolveAnchorIds,SortFootnotes,UnreferencedFootnotesDetector,)frommyst_parser.parsers.mditimportcreate_md_parserfrommyst_parser.warnings_importMystWarnings,create_warningdef_validate_int(setting,value,option_parser,config_parser=None,config_section=None)->int:"""Validate an integer setting."""returnint(value)def_validate_comma_separated_set(setting,value,option_parser,config_parser=None,config_section=None)->set[str]:"""Validate an integer setting."""value=frontend.validate_comma_separated_list(setting,value,option_parser,config_parser,config_section)returnset(value)def_create_validate_tuple(length:int)->Callable[...,tuple[str,...]]:"""Create a validator for a tuple of length `length`."""def_validate(setting,value,option_parser,config_parser=None,config_section=None):string_list=frontend.validate_comma_separated_list(setting,value,option_parser,config_parser,config_section)iflen(string_list)!=length:raiseValueError(f"Expecting {length} items in {setting}, got {len(string_list)}.")returntuple(string_list)return_validate
[文档]classUnset:"""A sentinel class for unset settings."""def__repr__(self):return"UNSET"def__bool__(self):# this allows to check if the setting is unset/falsyreturnFalse
DOCUTILS_UNSET=Unset()"""Sentinel for arguments not set through docutils.conf."""def_create_validate_yaml(field:Field):"""Create a deserializer/validator for a json setting."""def_validate_yaml(setting,value,option_parser,config_parser=None,config_section=None):"""Check/normalize a key-value pair setting. Items delimited by `,`, and key-value pairs delimited by `=`. """try:output=yaml.safe_load(value)exceptExceptionaserr:raiseValueError("Invalid YAML string")fromerrifnotisinstance(output,dict):raiseValueError("Expecting a YAML dictionary")returnoutputreturn_validate_yamldef_validate_url_schemes(setting,value,option_parser,config_parser=None,config_section=None):"""Validate a url_schemes setting. This is a tricky one, because it can be either a comma-separated list or a YAML dictionary. """try:output=yaml.safe_load(value)exceptExceptionaserr:raiseValueError("Invalid YAML string")fromerrifisinstance(output,str):output={k:Noneforkinoutput.split(",")}ifnotisinstance(output,dict):raiseValueError("Expecting a comma-delimited str or YAML dictionary")returnoutputdef_attr_to_optparse_option(at:Field,default:Any)->tuple[dict[str,Any],str]:"""Convert a field into a Docutils optparse options dict. :returns: (option_dict, default) """ifat.name=="url_schemes":return{"metavar":"<comma-delimited>|<yaml-dict>","validator":_validate_url_schemes,},",".join(default)ifat.typeisint:return{"metavar":"<int>","validator":_validate_int},str(default)ifat.typeisbool:return{"metavar":"<boolean>","validator":frontend.validate_boolean,},str(default)ifat.typeisstrorat.name=="heading_slug_func":return{"metavar":"<str>",},f"(default: '{default}')"ifget_origin(at.type)isLiteralandall(isinstance(a,str)forainget_args(at.type)):args=get_args(at.type)return{"metavar":f"<{'|'.join(repr(a)forainargs)}>","type":"choice","choices":args,},repr(default)ifat.typein(Iterable[str],Sequence[str]):return{"metavar":"<comma-delimited>","validator":frontend.validate_comma_separated_list,},",".join(default)ifat.type==set[str]:return{"metavar":"<comma-delimited>","validator":_validate_comma_separated_set,},",".join(default)ifat.type==tuple[str,str]:return{"metavar":"<str,str>","validator":_create_validate_tuple(2),},",".join(default)ifat.type==int|type(None):return{"metavar":"<null|int>","validator":_validate_int,},str(default)ifat.type==Iterable[str]|type(None):return{"metavar":"<null|comma-delimited>","validator":frontend.validate_comma_separated_list,},",".join(default)ifdefaultelse""ifget_origin(at.type)isdict:return{"metavar":"<yaml-dict>","validator":_create_validate_yaml(at),},str(default)ifdefaultelse""raiseAssertionError(f"Configuration option {at.name} not set up for use in docutils.conf.")
[文档]defattr_to_optparse_option(attribute:Field,default:Any,prefix:str="myst_")->tuple[str,list[str],dict[str,Any]]:"""Convert an ``MdParserConfig`` attribute into a Docutils setting tuple. :returns: A tuple of ``(help string, option flags, optparse kwargs)``. """name=f"{prefix}{attribute.name}"flag="--"+name.replace("_","-")options={"dest":name,"default":DOCUTILS_UNSET}at_options,default_str=_attr_to_optparse_option(attribute,default)options.update(at_options)help_str=attribute.metadata.get("help","")ifattribute.metadataelse""ifdefault_str:help_str+=f" (default: {default_str})"return(help_str,[flag],options)
[文档]defcreate_myst_settings_spec(config_cls=MdParserConfig,prefix:str="myst_"):"""Return a list of Docutils setting for the docutils MyST section."""defaults=config_cls()returntuple(attr_to_optparse_option(at,getattr(defaults,at.name),prefix)foratinconfig_cls.get_fields()if("docutils"notinat.metadata.get("omit",[])))
[文档]defcreate_myst_config(settings:frontend.Values,config_cls=MdParserConfig,prefix:str="myst_",):"""Create a configuration instance from the given settings."""values={}forattributeinconfig_cls.get_fields():if"docutils"inattribute.metadata.get("omit",[]):continuesetting=f"{prefix}{attribute.name}"val=getattr(settings,setting,DOCUTILS_UNSET)ifvalisnotDOCUTILS_UNSET:values[attribute.name]=valreturnconfig_cls(**values)
[文档]classParser(RstParser):"""Docutils parser for Markedly Structured Text (MyST)."""supported:tuple[str,...]=("md","markdown","myst")"""Aliases this parser supports."""settings_spec=("MyST options",None,create_myst_settings_spec(),*RstParser.settings_spec,)"""Runtime settings specification."""config_section="myst parser"config_section_dependencies=("parsers",)translate_section_name=None
[文档]defparse(self,inputstring:str,document:nodes.document)->None:"""Parse source text. :param inputstring: The source string to parse :param document: The root docutils node to add AST elements to """fromdocutils.writers._html_baseimportHTMLTranslatorHTMLTranslator.visit_rubric=visit_rubric_htmlHTMLTranslator.depart_rubric=depart_rubric_htmlHTMLTranslator.visit_container=visit_container_htmlHTMLTranslator.depart_container=depart_container_htmlself.setup_parse(inputstring,document)# check for exorbitantly long linesifhasattr(document.settings,"line_length_limit"):fori,lineinenumerate(inputstring.split("\n")):iflen(line)>document.settings.line_length_limit:error=document.reporter.error(f"Line {i+1} exceeds the line-length-limit:"f" {document.settings.line_length_limit}.")document.append(error)return# create parsing configuration from the global configtry:config=create_myst_config(document.settings)exceptExceptionasexc:error=document.reporter.error(f"Global myst configuration invalid: {exc}")document.append(error)config=MdParserConfig()if"attrs_image"inconfig.enable_extensions:create_warning(document,"The `attrs_image` extension is deprecated, ""please use `attrs_inline` instead.",MystWarnings.DEPRECATED,)# update the global config with the file-level configtry:topmatter=read_topmatter(inputstring)exceptTopmatterReadError:pass# this will be reported during the renderelse:iftopmatter:warning=lambdawtype,msg:create_warning(# noqa: E731document,msg,wtype,line=1,append_to=document)config=merge_file_level(config,topmatter,warning)# parse contentparser=create_md_parser(config,DocutilsRenderer)parser.options["document"]=documentparser.render(inputstring)# post-processing# replace raw nodes if raw is not allowedifnotgetattr(document.settings,"raw_enabled",True):fornodeindocument.traverse(nodes.raw):warning=document.reporter.warning("Raw content disabled.")node.parent.replace(node,warning)self.finish_parse()
def_run_cli(writer_name:str,writer_description:str,argv:list[str]|None):"""Run the command line interface for a particular writer."""publish_cmdline(parser=Parser(),writer_name=writer_name,description=(f"Generates {writer_description} from standalone MyST sources.\n{default_description}"),argv=argv,)
[文档]defcli_html(argv:list[str]|None=None)->None:"""Cmdline entrypoint for converting MyST to HTML."""_run_cli("html","(X)HTML documents",argv)
[文档]defcli_html5(argv:list[str]|None=None):"""Cmdline entrypoint for converting MyST to HTML5."""_run_cli("html5","HTML5 documents",argv)
[文档]defcli_html5_demo(argv:list[str]|None=None):"""Cmdline entrypoint for converting MyST to simple HTML5 demonstrations. This is a special case of the HTML5 writer, that only outputs the body of the document. """publish_cmdline(parser=Parser(),writer=SimpleWriter(),description=(f"Generates body HTML5 from standalone MyST sources.\n{default_description}"),settings_overrides={"doctitle_xform":False,"sectsubtitle_xform":False,"initial_header_level":1,},argv=argv,)
[文档]defto_html5_demo(inputstring:str,**kwargs)->str:"""Convert a MyST string to HTML5."""overrides={"doctitle_xform":False,"sectsubtitle_xform":False,"initial_header_level":1,"output_encoding":"unicode",}overrides.update(kwargs)returnpublish_string(inputstring,parser=Parser(),writer=SimpleWriter(),settings_overrides=overrides,)
[文档]defcli_latex(argv:list[str]|None=None):"""Cmdline entrypoint for converting MyST to LaTeX."""_run_cli("latex","LaTeX documents",argv)
[文档]defcli_xml(argv:list[str]|None=None):"""Cmdline entrypoint for converting MyST to XML."""_run_cli("xml","Docutils-native XML",argv)
[文档]defcli_pseudoxml(argv:list[str]|None=None):"""Cmdline entrypoint for converting MyST to pseudo-XML."""_run_cli("pseudoxml","pseudo-XML",argv)
[文档]defvisit_rubric_html(self,node):"""Override the default HTML visit method for rubric nodes. docutils structures a document, based on the headings, into nested sections:: # h1 ## h2 ### h3 <section> <title> h1 <section> <title> h2 <section> <title> h3 This means that it is not possible to have "standard" headings nested inside other components, such as blockquotes, because it would break the structure:: # h1 > ## h2 ### h3 <section> <title> h1 <blockquote> <section> <title> h2 <section> <title> h3 we work around this shortcoming, in `DocutilsRenderer.render_heading`, by identifying if a heading is inside another component and instead outputting it as a "non-structural" rubric node, and capture the level:: <section> <title> h1 <blockquote> <rubric level=2> h2 <section> <title> h3 However, docutils natively just outputs rubrics as <p> tags, and does not "honor" the heading level. So here we override the visit/depart methods to output the correct <h> element """if"level"innode:self.body.append(self.starttag(node,f'h{node["level"]}',"",CLASS="rubric"))else:self.body.append(self.starttag(node,"p","",CLASS="rubric"))
[文档]defdepart_rubric_html(self,node):"""Override the default HTML visit method for rubric nodes. See explanation in `visit_rubric_html` """if"level"innode:self.body.append(f'</h{node["level"]}>\n')else:self.body.append("</p>\n")
[文档]defvisit_container_html(self,node:nodes.Node):"""Override the default HTML visit method for container nodes. to remove the "container" class for divs this avoids CSS clashes with the bootstrap theme """classes="docutils container"attrs={}ifnode.get("is_div",False):# we don't want the CSS for container for these nodesclasses="docutils"if"style"innode:attrs["style"]=node["style"]self.body.append(self.starttag(node,"div",CLASS=classes,**attrs))
[文档]defdepart_container_html(self,node:nodes.Node):"""Override the default HTML depart method for container nodes. See explanation in `visit_container_html` """self.body.append("</div>\n")