22
33An MkDocs plugin allowing links to your pages using a custom alias.
44"""
5+
56from __future__ import annotations
67
78import logging
1920# group 1: escape character
2021# group 2: alias name
2122# group 3: alias text
22- ALIAS_TAG_REGEX = r"([\\])?\[\[([^|\]]+)\|?([^\]] +)?\]\]"
23+ ALIAS_TAG_REGEX = r"([\\])?\[\[([^|\]]+)\|?((?: [^\]\\]|\\.) +)?\]\]"
2324# RE for finding reference-style alias links
2425REFERENCE_REGEX = re .compile (
25- r'^' # Start of the line
26- r' \[(?P<ref_id>[^\]]+)\]:\s*' # 1. [ref_id]:
27- r' \[\['
28- r' (?P<alias_name>[^|\]]+)' # 2. [[page-alias#anchor]]
29- r' \]\]'
30- r' [ \t]*$' , # Optional trailing spaces and end of line
31- re .MULTILINE
26+ r"^" # Start of the line
27+ r" \[(?P<ref_id>[^\]]+)\]:\s*" # 1. [ref_id]:
28+ r" \[\["
29+ r" (?P<alias_name>[^|\]]+)" # 2. [[page-alias#anchor]]
30+ r" \]\]"
31+ r" [ \t]*$" , # Optional trailing spaces and end of line
32+ re .MULTILINE ,
3233)
3334
3435
3536class MarkdownAnchor (TypedDict ):
3637 """A single entry in the table of contents. See the following link for more info:
3738 https://python-markdown.github.io/extensions/toc/#syntax"""
39+
3840 level : int
3941 id : str
4042 name : str
41- children : list [' MarkdownAnchor' ]
43+ children : list [" MarkdownAnchor" ]
4244
4345
4446def get_markdown_toc (markdown_source ) -> list [MarkdownAnchor ]:
4547 """Parse the markdown source and return the table of contents tokens."""
46- md = Markdown (extensions = [' toc' ])
48+ md = Markdown (extensions = [" toc" ])
4749 md .convert (markdown_source )
48- return getattr (md , ' toc_tokens' , [])
50+ return getattr (md , " toc_tokens" , [])
4951
5052
51- def find_anchor_by_id (anchors : list [MarkdownAnchor ], anchor_id : str ) -> MarkdownAnchor | None :
53+ def find_anchor_by_id (
54+ anchors : list [MarkdownAnchor ], anchor_id : str
55+ ) -> MarkdownAnchor | None :
5256 """Find an anchor by its ID in a list of anchors returned by get_markdown_toc."""
5357 for anchor in anchors :
54- if anchor ['id' ] == anchor_id :
58+ if anchor ["id" ] == anchor_id :
5559 return anchor
56- if ' children' in anchor :
57- child = find_anchor_by_id (anchor [' children' ], anchor_id )
60+ if " children" in anchor :
61+ child = find_anchor_by_id (anchor [" children" ], anchor_id )
5862 if child is not None :
5963 return child
6064 return None
@@ -63,26 +67,26 @@ def find_anchor_by_id(anchors: list[MarkdownAnchor], anchor_id: str) -> Markdown
6367def get_page_title (page_src : str , meta_data : dict , include_icon : bool = False ):
6468 """Returns the title of the page. The title in the meta data section
6569 will take precedence over the H1 markdown title if both are provided."""
66- if ' title' in meta_data and isinstance (meta_data [' title' ], str ):
67- title = meta_data [' title' ]
68- if include_icon and ' icon' in meta_data and isinstance (meta_data [' icon' ], str ):
69- icon = ':' + meta_data [' icon' ].replace ('/' , '-' ) + ':'
70- title = f' { icon } { title } '
70+ if " title" in meta_data and isinstance (meta_data [" title" ], str ):
71+ title = meta_data [" title" ]
72+ if include_icon and " icon" in meta_data and isinstance (meta_data [" icon" ], str ):
73+ icon = ":" + meta_data [" icon" ].replace ("/" , "-" ) + ":"
74+ title = f" { icon } { title } "
7175 else :
7276 title = get_markdown_title (page_src )
7377 return title
7478
7579
76- def get_alias_names (meta_data : dict , meta_key : str = ' alias' ) -> list [str ] | None :
80+ def get_alias_names (meta_data : dict , meta_key : str = " alias" ) -> list [str ] | None :
7781 """Returns the list of configured alias names."""
7882 if len (meta_data ) <= 0 or meta_key not in meta_data :
7983 return None
8084 aliases = meta_data [meta_key ]
8185 if isinstance (aliases , list ):
8286 # If the alias meta data is a list, ensure that they're strings
8387 return list (filter (lambda value : isinstance (value , str ), aliases ))
84- if isinstance (aliases , dict ) and ' name' in aliases :
85- return [aliases [' name' ]]
88+ if isinstance (aliases , dict ) and " name" in aliases :
89+ return [aliases [" name" ]]
8690 if isinstance (aliases , str ):
8791 return [aliases ]
8892 return None
@@ -93,30 +97,28 @@ def replace_tag(
9397 aliases : dict ,
9498 log : logging .Logger ,
9599 page_file : File ,
96- use_anchor_titles : bool = False
100+ use_anchor_titles : bool = False ,
97101):
98102 """Callback used in the sub function within on_page_markdown."""
99103 if match .group (1 ) is not None :
100104 # if the alias match was escaped, return the unescaped version
101105 return match .group (0 )[1 :]
102106 # split the tag up in case there's an anchor in the link
103- tag_bits = ['' ]
107+ tag_bits = ["" ]
104108 if match .group (2 ) is not None :
105- tag_bits = str (match .group (2 )).split ('#' )
109+ tag_bits = str (match .group (2 )).split ("#" )
106110 alias = aliases .get (tag_bits [0 ])
107111
108112 # if the alias is not found, log a warning and return the input string
109113 # unless the alias is an anchor tag, then try to find the anchor tag
110114 # and replace it with the anchor's title
111115 if alias is None :
112116 matched = str (match .group (2 ))
113- if len (tag_bits ) < 2 or not matched .startswith ('#' ):
117+ if len (tag_bits ) < 2 or not matched .startswith ("#" ):
114118 log .warning (
115- "Alias '%s' not found in '%s'" ,
116- match .group (2 ),
117- page_file .src_path
119+ "Alias '%s' not found in '%s'" , match .group (2 ), page_file .src_path
118120 )
119- return match .group (0 ) # return the input string
121+ return match .group (0 ) # return the input string
120122 # using the [[#anchor]] syntax to link within the current page:
121123 anchor = tag_bits [1 ]
122124 anchors = get_markdown_toc (page_file .content_string )
@@ -132,46 +134,41 @@ def replace_tag(
132134 # if the use_anchor_titles config option is set, replace the text with the
133135 # anchor title, but only if the alias tag doesn't have a custom text
134136 if use_anchor_titles and anchor is not None and match .group (3 ) is None :
135- anchor_tag = find_anchor_by_id (alias [' anchors' ], anchor )
137+ anchor_tag = find_anchor_by_id (alias [" anchors" ], anchor )
136138 if anchor_tag is not None :
137- text = anchor_tag [' name' ]
139+ text = anchor_tag [" name" ]
138140 if text is None :
139141 # if the alias tag has a custom text, use that instead
140- text = alias [' text' ] if match .group (3 ) is None else match .group (3 )
142+ text = alias [" text" ] if match .group (3 ) is None else match .group (3 )
141143 # if the alias tag has no text, use the alias URL
142144 if text is None :
143- text = alias [' url' ]
145+ text = alias [" url" ]
144146
145- url = get_relative_url (alias [' url' ], page_file .src_uri )
147+ url = get_relative_url (alias [" url" ], page_file .src_uri )
146148 if anchor is not None :
147149 url = f"{ url } #{ tag_bits [1 ]} "
148150
149- log .info (
150- "replaced alias '%s' with '%s' to '%s'" ,
151- alias ['alias' ],
152- text ,
153- url
154- )
151+ log .info ("replaced alias '%s' with '%s' to '%s'" , alias ["alias" ], text , url )
155152 return f"[{ text } ]({ url } )"
156153
157154
158155def replace_reference (match , aliases , log , page_file ):
159156 """Callback used in the sub function within on_page_markdown for
160157 reference-style links."""
161- ref_id = match .group (' ref_id' )
162- alias_name = match .group (' alias_name' )
158+ ref_id = match .group (" ref_id" )
159+ alias_name = match .group (" alias_name" )
163160
164- tag_bits = alias_name .split ('#' , 1 )
161+ tag_bits = alias_name .split ("#" , 1 )
165162 base_alias = tag_bits [0 ]
166163 anchor = tag_bits [1 ] if len (tag_bits ) > 1 else None
167164
168165 alias = aliases .get (base_alias )
169166 if alias is None :
170167 log .warning (f"Alias '{ base_alias } ' not found for reference link..." )
171- match .group (0 ) # return original string
168+ match .group (0 ) # return original string
172169
173170 # Resolve the final URL and anchor
174- url = get_relative_url (alias [' url' ], page_file .src_uri )
171+ url = get_relative_url (alias [" url" ], page_file .src_uri )
175172 if anchor :
176173 url = f"{ url } #{ anchor } "
177174
@@ -190,20 +187,19 @@ class AliasPlugin(BasePlugin):
190187
191188 For overridden BasePlugin methods, see the MkDocs source code.
192189 """
190+
193191 config_scheme = (
194- (' verbose' , config_options .Type (bool , default = False )),
195- (' use_anchor_titles' , config_options .Type (bool , default = False )),
196- (' use_page_icon' , config_options .Type (bool , default = False )),
192+ (" verbose" , config_options .Type (bool , default = False )),
193+ (" use_anchor_titles" , config_options .Type (bool , default = False )),
194+ (" use_page_icon" , config_options .Type (bool , default = False )),
197195 )
198196 aliases = {}
199- log = logging .getLogger (f' mkdocs.plugins.{ __name__ } ' )
197+ log = logging .getLogger (f" mkdocs.plugins.{ __name__ } " )
200198 current_page = None
201199
202200 def on_config (self , _ ):
203201 """Set the log level if the verbose config option is set"""
204- self .log .setLevel (
205- logging .INFO if self .config ['verbose' ] else logging .WARNING
206- )
202+ self .log .setLevel (logging .INFO if self .config ["verbose" ] else logging .WARNING )
207203
208204 def on_post_build (self , ** _ ):
209205 """Executed after the build has completed. Clears the aliases from
@@ -214,18 +210,11 @@ def on_post_build(self, **_):
214210
215211 def on_page_markdown (self , markdown : str , / , * , page : Page , ** _ ):
216212 """Replaces any alias tags on the page with markdown links."""
217- self .current_page = page
218-
219213 # Replace any reference-style links first
220214 markdown = re .sub (
221215 REFERENCE_REGEX ,
222- lambda match : replace_reference (
223- match ,
224- self .aliases ,
225- self .log ,
226- self .current_page .file
227- ),
228- markdown
216+ lambda match : replace_reference (match , self .aliases , self .log , page .file ),
217+ markdown ,
229218 )
230219
231220 # Replace any inline alias tags
@@ -235,23 +224,28 @@ def on_page_markdown(self, markdown: str, /, *, page: Page, **_):
235224 match ,
236225 self .aliases ,
237226 self .log ,
238- self . current_page .file ,
239- self .config [' use_anchor_titles' ]
227+ page .file ,
228+ self .config [" use_anchor_titles" ],
240229 ),
241- markdown
230+ markdown ,
242231 )
232+ self .current_page = page
243233 return markdown
244234
245235 def on_files (self , files : Files , / , ** _ ):
246236 """When MkDocs loads its files, extract aliases from any Markdown files
247237 that were found.
248238 """
249239 for file in filter (lambda f : f .is_documentation_page (), files ):
250- with open (file .abs_src_path , encoding = 'utf-8-sig' , errors = 'strict' ) as handle :
240+ if file .abs_src_path is None :
241+ continue
242+ with open (
243+ file .abs_src_path , encoding = "utf-8-sig" , errors = "strict"
244+ ) as handle :
251245 self .process_file (file , handle )
252246 # write the aliases to the aliases log file if the verbose option is set
253- if self .config [' verbose' ]:
254- with open (' aliases.log' , 'w' , encoding = ' utf-8' ) as log_file :
247+ if self .config [" verbose" ]:
248+ with open (" aliases.log" , "w" , encoding = " utf-8" ) as log_file :
255249 log_file .write ("alias\t title\t url\n " )
256250 for alias in self .aliases .values ():
257251 log_file .write (
@@ -261,49 +255,45 @@ def on_files(self, files: Files, /, **_):
261255 def process_file (self , file , handle ):
262256 """Extract aliases from the given file and add them to the aliases"""
263257 source , meta_data = meta .get_data (handle .read ())
264- for section in [' alias' , ' aliases' ]:
258+ for section in [" alias" , " aliases" ]:
265259 alias_names = get_alias_names (meta_data , section )
266260 if alias_names is None or len (alias_names ) < 1 :
267261 continue
268262
269263 # If the use_anchor_titles config option is set, parse the markdown
270264 # and get the table of contents for the page
271265 anchors : list [MarkdownAnchor ] = []
272- if self .config [' use_anchor_titles' ]:
266+ if self .config [" use_anchor_titles" ]:
273267 anchors = get_markdown_toc (source )
274268
275269 if len (alias_names ) > 1 :
276- self .log .info (
277- '%s defines %d aliases:' , file .url , len (alias_names )
278- )
270+ self .log .info ("%s defines %d aliases:" , file .url , len (alias_names ))
279271 for alias in alias_names :
280272 existing = self .aliases .get (alias )
281273 if existing is not None :
282274 self .log .warning (
283275 "%s: alias %s already defined in %s, skipping." ,
284276 file .src_uri ,
285277 alias ,
286- existing [' url' ]
278+ existing [" url" ],
287279 )
288280 continue
289281
290282 new_alias = {
291- ' alias' : alias ,
292- ' text' : (
293- meta_data [section ][' text' ]
283+ " alias" : alias ,
284+ " text" : (
285+ meta_data [section ][" text" ]
294286 # if meta_data['alias'] is a dictionary and 'text' is a key
295287 if (
296- isinstance (meta_data [section ], dict ) and
297- 'text' in meta_data [section ]
288+ isinstance (meta_data [section ], dict )
289+ and "text" in meta_data [section ]
290+ )
291+ else get_page_title (
292+ source , meta_data , self .config ["use_page_icon" ]
298293 )
299- else get_page_title (source , meta_data , self .config ['use_page_icon' ])
300294 ),
301- ' url' : file .src_uri ,
302- ' anchors' : anchors ,
295+ " url" : file .src_uri ,
296+ " anchors" : anchors ,
303297 }
304- self .log .info (
305- "Alias %s to %s" ,
306- alias ,
307- new_alias ['url' ]
308- )
298+ self .log .info ("Alias %s to %s" , alias , new_alias ["url" ])
309299 self .aliases [alias ] = new_alias
0 commit comments