"""The configuration for the myst parser."""importdataclassesasdcfromcollections.abcimportCallable,Iterable,Iterator,Sequencefromimportlibimportimport_modulefromtypingimport(Any,TypedDict,)frommyst_parser.warnings_importMystWarningsfrom.dc_validatorsimport(any_,deep_iterable,deep_mapping,in_,instance_of,optional,validate_field,validate_fields,)
[文档]defcheck_extensions(inst:"MdParserConfig",field:dc.Field,value:Any)->None:"""Check that the extensions are a list of known strings"""ifnotisinstance(value,Iterable):raiseTypeError(f"'{field.name}' not iterable: {value}")diff=set(value).difference(["amsmath","attrs_image","attrs_inline","attrs_block","colon_fence","deflist","dollarmath","fieldlist","html_admonition","html_image","linkify","replacements","smartquotes","strikethrough","substitution","tasklist",])ifdiff:raiseValueError(f"'{field.name}' items not recognised: {diff}")setattr(inst,field.name,set(value))
[文档]classUrlSchemeType(TypedDict,total=False):"""Type of the external schemes dictionary."""url:strtitle:strclasses:list[str]
[文档]defcheck_url_schemes(inst:"MdParserConfig",field:dc.Field,value:Any)->None:"""Check that the external schemes are of the right format."""ifisinstance(value,list|tuple):ifnotall(isinstance(v,str)forvinvalue):raiseTypeError(f"'{field.name}' is not a list of strings: {value!r}")value={v:Noneforvinvalue}ifnotisinstance(value,dict):raiseTypeError(f"'{field.name}' is not a dictionary: {value!r}")new_dict:dict[str,UrlSchemeType|None]={}forkey,valinvalue.items():ifnotisinstance(key,str):raiseTypeError(f"'{field.name}' key is not a string: {key!r}")ifvalisNone:new_dict[key]=valelifisinstance(val,str):new_dict[key]={"url":val}elifisinstance(val,dict):ifnotall(isinstance(k,str)forkinval):raiseTypeError(f"'{field.name}[{key}]' keys are not strings: {val!r}")if"url"invalandnotisinstance(val["url"],str):raiseTypeError(f"'{field.name}[{key}][url]' is not a string: {val['url']!r}")if"title"invalandnotisinstance(val["title"],str):raiseTypeError(f"'{field.name}[{key}][title]' is not a string: {val['title']!r}")if("classes"invalandnotisinstance(val["classes"],list)andnotall(isinstance(c,str)forcinval["classes"])):raiseTypeError(f"'{field.name}[{key}][classes]' is not a list of str: {val['classes']!r}")new_dict[key]=val# type: ignore[assignment]else:raiseTypeError(f"'{field.name}[{key}]' value is not a string or dict: {val!r}")setattr(inst,field.name,new_dict)
[文档]defcheck_sub_delimiters(_:"MdParserConfig",field:dc.Field,value:Any)->None:"""Check that the sub_delimiters are a tuple of length 2 of strings of length 1"""if(notisinstance(value,tuple|list))orlen(value)!=2:raiseTypeError(f"'{field.name}' is not a tuple of length 2: {value}")fordeliminvalue:if(notisinstance(delim,str))orlen(delim)!=1:raiseTypeError(f"'{field.name}' does not contain strings of length 1: {value}")
[文档]defcheck_inventories(_:"MdParserConfig",field:dc.Field,value:Any)->None:"""Check that the inventories are a dict of {str: (str, Optional[str])}"""ifnotisinstance(value,dict):raiseTypeError(f"'{field.name}' is not a dictionary: {value!r}")forkey,valinvalue.items():ifnotisinstance(key,str):raiseTypeError(f"'{field.name}' key is not a string: {key!r}")ifnotisinstance(val,tuple|list)orlen(val)!=2:raiseTypeError(f"'{field.name}[{key}]' value is not a 2-item list: {val!r}")ifnotisinstance(val[0],str):raiseTypeError(f"'{field.name}[{key}][0]' is not a string: {val[0]}")ifnot(val[1]isNoneorisinstance(val[1],str)):raiseTypeError(f"'{field.name}[{key}][1]' is not a null/string: {val[1]}")
[文档]defcheck_heading_slug_func(inst:"MdParserConfig",field:dc.Field,value:Any)->None:"""Check that the heading_slug_func is a callable."""ifvalueisNone:returnifisinstance(value,str):# attempt to load the function as a python importtry:module_path,function_name=value.rsplit(".",1)mod=import_module(module_path)value=getattr(mod,function_name)exceptImportErrorasexc:raiseTypeError(f"'{field.name}' could not be loaded from string: {value!r}")fromexcsetattr(inst,field.name,value)ifnotcallable(value):raiseTypeError(f"'{field.name}' is not callable: {value!r}")
def_test_slug_func(text:str)->str:"""Dummy slug function, this is imported during testing."""# reverse the textreturntext[::-1]
[文档]defcheck_fence_as_directive(inst:"MdParserConfig",field:dc.Field,value:Any)->None:"""Check that the extensions are a sequence of known strings"""deep_iterable(instance_of(str),instance_of((list,tuple,set)))(inst,field,value)setattr(inst,field.name,set(value))
[文档]@dc.dataclass()classMdParserConfig:"""Configuration options for the Markdown Parser. Note in the sphinx configuration these option names are prepended with ``myst_`` """def__repr__(self)->str:"""Return a string representation of the config."""# this replicates the auto-generated __repr__,# but also allows for a repr function to be defined on the fieldattributes:list[str]=[]forname,val,finself.as_triple():ifnotf.repr:continueval_str=f.metadata.get("repr_func",repr)(val)attributes.append(f"{name}={val_str}")returnf"{self.__class__.__name__}({', '.join(attributes)})"# TODO replace commonmark_only, gfm_only with a single optioncommonmark_only:bool=dc.field(default=False,metadata={"validator":instance_of(bool),"help":"Use strict CommonMark parser",},)gfm_only:bool=dc.field(default=False,metadata={"validator":instance_of(bool),"help":"Use strict Github Flavoured Markdown parser",},)enable_extensions:set[str]=dc.field(default_factory=set,metadata={"validator":check_extensions,"help":"Enable syntax extensions"},)disable_syntax:Iterable[str]=dc.field(default_factory=list,metadata={"validator":deep_iterable(instance_of(str),instance_of((list,tuple))),"help":"Disable Commonmark syntax elements",},)all_links_external:bool=dc.field(default=False,metadata={"validator":instance_of(bool),"help":"Parse all links as simple hyperlinks",},)links_external_new_tab:bool=dc.field(default=False,metadata={"validator":instance_of(bool),"help":"Open all external links in a new tab",},)url_schemes:dict[str,UrlSchemeType|None]=dc.field(default_factory=lambda:{"http":None,"https":None,"mailto":None,"ftp":None,},metadata={"validator":check_url_schemes,"help":"URI schemes that are converted to external links","repr_func":lambdav:repr(tuple(v)),# Note, lists of strings will be coerced to dicts in the validator"doc_type":"list[str] | dict[str, None | str | dict]",},)ref_domains:Iterable[str]|None=dc.field(default=None,metadata={"validator":optional(deep_iterable(instance_of(str),instance_of((list,tuple)))),"help":"Sphinx domain names to search in for link references","omit":["docutils"],},)fence_as_directive:set[str]=dc.field(default_factory=set,metadata={"validator":check_fence_as_directive,"help":"Interpret a code fence as a directive, for certain language names. ""This can be useful for fences like dot and mermaid, ""and interoperability with other Markdown renderers.",},)number_code_blocks:Sequence[str]=dc.field(default_factory=list,metadata={"validator":deep_iterable(instance_of(str),instance_of((list,tuple))),"help":"Add line numbers to code blocks with these languages",},)title_to_header:bool=dc.field(default=False,metadata={"validator":instance_of(bool),"help":"Convert a `title` field in the front-matter to a H1 header",},)heading_anchors:int=dc.field(default=0,metadata={"validator":optional(in_([0,1,2,3,4,5,6,7])),"help":"Heading level depth to assign HTML anchors",},)heading_slug_func:Callable[[str],str]|None=dc.field(default=None,metadata={"validator":check_heading_slug_func,"help":("Function for creating heading anchors, ""or a python import path e.g. `my_package.my_module.my_function`"),"global_only":True,"doc_type":"None | Callable[[str], str] | str",},)html_meta:dict[str,str]=dc.field(default_factory=dict,metadata={"validator":deep_mapping(instance_of(str),instance_of(str),instance_of(dict)),"merge_topmatter":True,"help":"HTML meta tags","repr_func":lambdav:f"{{{', '.join(f'{k}: ...'forkinv)}}}",},)footnote_sort:bool=dc.field(default=True,metadata={"validator":instance_of(bool),"help":"Move all footnotes to the end of the document, and sort by reference order",},)footnote_transition:bool=dc.field(default=True,metadata={"validator":instance_of(bool),"help":"Place a transition before sorted footnotes",},)words_per_minute:int=dc.field(default=200,metadata={"validator":instance_of(int),"help":"For reading speed calculations",},)# Extension specificsubstitutions:dict[str,Any]=dc.field(default_factory=dict,metadata={"validator":deep_mapping(instance_of(str),any_,instance_of(dict)),"merge_topmatter":True,"help":"Substitutions mapping","extension":"substitutions","repr_func":lambdav:f"{{{', '.join(f'{k}: ...'forkinv)}}}",},)sub_delimiters:tuple[str,str]=dc.field(default=("{","}"),repr=False,metadata={"validator":check_sub_delimiters,"help":"Substitution delimiters","extension":"substitutions","omit":["docutils"],},)linkify_fuzzy_links:bool=dc.field(default=True,metadata={"validator":instance_of(bool),"help":"Recognise URLs without schema prefixes","extension":"linkify",},)dmath_allow_labels:bool=dc.field(default=True,metadata={"validator":instance_of(bool),"help":"Parse `$$...$$ (label)`","extension":"dollarmath",},)dmath_allow_space:bool=dc.field(default=True,metadata={"validator":instance_of(bool),"help":"Allow initial/final spaces in `$ ... $`","extension":"dollarmath",},)dmath_allow_digits:bool=dc.field(default=True,metadata={"validator":instance_of(bool),"help":"Allow initial/final digits `1$ ...$2`","extension":"dollarmath",},)dmath_double_inline:bool=dc.field(default=False,metadata={"validator":instance_of(bool),"help":"Parse inline `$$ ... $$`","extension":"dollarmath",},)update_mathjax:bool=dc.field(default=True,metadata={"validator":instance_of(bool),"help":"Update sphinx.ext.mathjax configuration to ignore `$` delimiters","extension":"dollarmath","global_only":True,"omit":["docutils"],},)mathjax_classes:str=dc.field(default="tex2jax_process|mathjax_process|math|output_area",metadata={"validator":instance_of(str),"help":"MathJax classes to add to math HTML","extension":"dollarmath","global_only":True,"omit":["docutils"],},)enable_checkboxes:bool=dc.field(default=False,metadata={"validator":instance_of(bool),"help":"Enable checkboxes","extension":"tasklist",},)# docutils only (replicating aspects of sphinx config)suppress_warnings:Sequence[str]=dc.field(default_factory=list,metadata={"validator":deep_iterable(instance_of(str),instance_of((list,tuple))),"help":"A list of warning types to suppress warning messages","omit":["sphinx"],"global_only":True,},)highlight_code_blocks:bool=dc.field(default=True,metadata={"validator":instance_of(bool),"help":"Syntax highlight code blocks with pygments","omit":["sphinx"],},)inventories:dict[str,tuple[str,str|None]]=dc.field(default_factory=dict,repr=False,metadata={"validator":check_inventories,"help":"Mapping of key to (url, inv file), for intra-project referencing","omit":["sphinx"],"global_only":True,},)def__post_init__(self):validate_fields(self)
[文档]defcopy(self,**kwargs:Any)->"MdParserConfig":"""Return a new object replacing specified fields with new values. Note: initiating the copy will also validate the new fields. """returndc.replace(self,**kwargs)
[文档]@classmethoddefget_fields(cls)->tuple[dc.Field,...]:"""Return all attribute fields in this class."""returndc.fields(cls)
[文档]defas_dict(self,dict_factory=dict)->dict:"""Return a dictionary of field name -> value."""returndc.asdict(self,dict_factory=dict_factory)
[文档]defas_triple(self)->Iterable[tuple[str,Any,dc.Field]]:"""Yield triples of (name, value, field)."""fields={f.name:fforfindc.fields(self.__class__)}forname,valueindc.asdict(self).items():yieldname,value,fields[name]
[文档]defmerge_file_level(config:MdParserConfig,topmatter:dict[str,Any],warning:Callable[[MystWarnings,str],None],)->MdParserConfig:"""Merge the file-level topmatter with the global config. :param config: Global config. :param topmatter: Topmatter from the file. :param warning: Function to call with a warning (type, message). :returns: A new config object """# get updatesupdates:dict[str,Any]={}myst=topmatter.get("myst",{})ifnotisinstance(myst,dict):warning(MystWarnings.MD_TOPMATTER,f"'myst' key not a dict: {type(myst)}")else:updates=myst# allow html_meta and substitutions at top-level for back-compatibilityif"html_meta"intopmatter:warning(MystWarnings.MD_TOPMATTER,"top-level 'html_meta' key is deprecated, ""place under 'myst' key instead",)updates["html_meta"]=topmatter["html_meta"]if"substitutions"intopmatter:warning(MystWarnings.MD_TOPMATTER,"top-level 'substitutions' key is deprecated, ""place under 'myst' key instead",)updates["substitutions"]=topmatter["substitutions"]new=config.copy()# validate each updatefields={name:(value,field)forname,value,fieldinconfig.as_triple()}forname,valueinupdates.items():ifnamenotinfields:warning(MystWarnings.MD_TOPMATTER,f"Unknown field: {name}")continueold_value,field=fields[name]try:validate_field(new,field,value)exceptExceptionasexc:warning(MystWarnings.MD_TOPMATTER,str(exc))continueiffield.metadata.get("merge_topmatter"):value={**old_value,**value}setattr(new,name,value)returnnew
[文档]defread_topmatter(text:str|Iterator[str])->dict[str,Any]|None:"""Read the (optional) YAML topmatter from a source string. This is identified by the first line starting with `---`, then read up to a terminating line of `---`, or `...`. :param source: The source string to read from :return: The topmatter """importyamlifisinstance(text,str):ifnottext.startswith("---"):# skip creating the line list in memoryreturnNonetext=(lineforlineintext.splitlines())try:ifnotnext(text).startswith("---"):returnNoneexceptStopIteration:returnNonetop_matter=[]forlineintext:ifline.startswith(("---","...")):breaktop_matter.append(line.rstrip()+"\n")try:metadata=yaml.safe_load("".join(top_matter))except(yaml.parser.ParserError,yaml.scanner.ScannerError)aserr:raiseTopmatterReadError("Malformed YAML")fromerrifnotisinstance(metadata,dict):raiseTopmatterReadError(f"YAML is not a dict: {type(metadata)}")returnmetadata