2323import logging
2424import os
2525import platform
26+ import regex
2627import requests
2728import site
2829import sys
29-
3030import yaml
31+
3132from colorama import Fore , Style
3233from importlib .metadata import distributions , version
3334from io import BytesIO
3435from markdown .extensions .toc import slugify
3536from mkdocs .config .defaults import MkDocsConfig
3637from mkdocs .plugins import BasePlugin , event_priority
3738from mkdocs .utils import get_yaml_loader
38- import regex
3939from zipfile import ZipFile , ZIP_DEFLATED
4040
4141from .config import InfoConfig
@@ -110,21 +110,24 @@ def on_config(self, config):
110110 log .error ("Please remove 'hooks' setting." )
111111 self ._help_on_customizations_and_exit ()
112112
113- # Assure that possible relative paths, which will be validated
114- # or used to generate other paths are absolute.
113+ # Assure all paths that will be validated are absolute. Convert possible
114+ # relative config_file_path to absolute. Its absolute directory path is
115+ # being later used to resolve other paths.
115116 config .config_file_path = _convert_to_abs (config .config_file_path )
116117 config_file_parent = os .path .dirname (config .config_file_path )
117118
118- # The theme.custom_dir property cannot be set, therefore a helper
119- # variable is used.
120- custom_dir = config .theme .custom_dir
121- if custom_dir :
122- custom_dir = _convert_to_abs (
123- custom_dir ,
119+ # Convert relative custom_dir path to absolute. The Theme.custom_dir
120+ # property cannot be set, therefore a helper variable is used.
121+ if config .theme .custom_dir :
122+ abs_custom_dir = _convert_to_abs (
123+ config .theme .custom_dir ,
124124 abs_prefix = config_file_parent
125125 )
126+ else :
127+ abs_custom_dir = ""
126128
127- # Support projects plugin
129+ # Extract the absolute path to projects plugin's directory to explicitly
130+ # support path validation and dynamic exclusion for the plugin
128131 projects_plugin = config .plugins .get ("material/projects" )
129132 if projects_plugin :
130133 abs_projects_dir = _convert_to_abs (
@@ -134,29 +137,39 @@ def on_config(self, config):
134137 else :
135138 abs_projects_dir = ""
136139
137- # Load the current MkDocs config(s) to get access to INHERIT
140+ # MkDocs removes the INHERIT configuration key during load, and doesn't
141+ # expose the information in any way, as the parent configuration is
142+ # merged into one. To validate that the INHERIT config file will be
143+ # included in the ZIP file the current config file must be loaded again
144+ # without parsing. Each file can have their own INHERIT key, so a list
145+ # of configurations is supported. The INHERIT path is converted during
146+ # load to absolute.
138147 loaded_configs = _load_yaml (config .config_file_path )
139148 if not isinstance (loaded_configs , list ):
140149 loaded_configs = [loaded_configs ]
141150
142- # Validate different MkDocs paths to assure that
143- # they're children of the current working directory.
151+ # We need to make sure the user put every file in the current working
152+ # directory. To assure the reproduction inside the ZIP file can be run,
153+ # validate that the MkDocs paths are children of the current root.
144154 paths_to_validate = [
145155 config .config_file_path ,
146156 config .docs_dir ,
147- custom_dir or "" ,
157+ abs_custom_dir ,
148158 abs_projects_dir ,
149159 * [cfg .get ("INHERIT" , "" ) for cfg in loaded_configs ]
150160 ]
151161
162+ # Convert relative hook paths to absolute path
152163 for hook in config .hooks :
153164 path = _convert_to_abs (hook , abs_prefix = config_file_parent )
154165 paths_to_validate .append (path )
155166
167+ # Remove valid paths from the list
156168 for path in list (paths_to_validate ):
157169 if not path or path .startswith (os .getcwd ()):
158170 paths_to_validate .remove (path )
159171
172+ # Report the invalid paths to the user
160173 if paths_to_validate :
161174 log .error (f"One or more paths aren't children of root" )
162175 self ._help_on_not_in_cwd (paths_to_validate )
@@ -198,26 +211,36 @@ def on_config(self, config):
198211 files : list [str ] = []
199212 with ZipFile (archive , "a" , ZIP_DEFLATED , False ) as f :
200213 for abs_root , dirnames , filenames in os .walk (os .getcwd ()):
201- # Prune the folders in-place to prevent
202- # scanning excluded folders
214+ # Prune the folders in-place to prevent their processing
203215 for name in list (dirnames ):
216+ # Resolve the absolute directory path
204217 path = os .path .join (abs_root , name )
218+
219+ # Exclude the directory and all subdirectories
205220 if self ._is_excluded (_resolve_pattern (path )):
206221 dirnames .remove (name )
207222 continue
208- # Multi-language setup from #2346 separates the
209- # language config, so each mkdocs.yml file is
210- # unaware of other site_dir directories. Therefore,
211- # we add this with the assumption a site_dir contains
212- # the sitemap file.
223+
224+ # Projects, which don't use the projects plugin for
225+ # multi-language support could have separate build folders
226+ # for each config file or language. Therefore, we exclude
227+ # them with the assumption a site_dir contains the sitemap
228+ # file. Example of such a setup: https://t.ly/DLQcy
213229 sitemap_gz = os .path .join (path , "sitemap.xml.gz" )
214230 if os .path .exists (sitemap_gz ):
215231 log .debug (f"Excluded site_dir: { path } " )
216232 dirnames .remove (name )
233+
234+ # Write files to the in-memory archive
217235 for name in filenames :
236+ # Resolve the absolute file path
218237 path = os .path .join (abs_root , name )
238+
239+ # Exclude the file
219240 if self ._is_excluded (_resolve_pattern (path )):
220241 continue
242+
243+ # Resolve the relative path to create a matching structure
221244 path = os .path .relpath (path , os .path .curdir )
222245 f .write (path , os .path .join (example , path ))
223246
@@ -320,27 +343,31 @@ def _help_on_customizations_and_exit(self):
320343 print (Style .NORMAL )
321344 print (" - extra_css" )
322345 print (" - extra_javascript" )
346+ print (Fore .YELLOW )
347+ print (" If you're using customizations from the theme's documentation" )
348+ print (" and you want to report a bug specific to those customizations" )
349+ print (" then set the 'archive_stop_on_violation: false' option in the" )
350+ print (" info plugin config." )
323351 print (Style .RESET_ALL )
324352
325353 # Exit, unless explicitly told not to
326354 if self .config .archive_stop_on_violation :
327355 sys .exit (1 )
328356
329357 # Print help on not in current working directory and exit
330- def _help_on_not_in_cwd (self , bad_paths ):
358+ def _help_on_not_in_cwd (self , outside_root ):
331359 print (Fore .RED )
332360 print (" The current working (root) directory:\n " )
333361 print (f" { os .getcwd ()} \n " )
334362 print (" is not a parent of the following paths:" )
335363 print (Style .NORMAL )
336- for path in bad_paths :
364+ for path in outside_root :
337365 print (f" { path } " )
338- print ()
339- print (" To assure that all project files are found" )
340- print (" please adjust your config or file structure and" )
341- print (" put everything within the root directory of the project.\n " )
342- print (" Please also make sure `mkdocs build` is run in" )
343- print (" the actual root directory of the project." )
366+ print (" \n To assure that all project files are found please adjust" )
367+ print (" your config or file structure and put everything within the" )
368+ print (" root directory of the project.\n " )
369+ print (" Please also make sure `mkdocs build` is run in the actual" )
370+ print (" root directory of the project." )
344371 print (Style .RESET_ALL )
345372
346373 # Exit, unless explicitly told not to
@@ -370,17 +397,19 @@ def _size(value, factor = 1):
370397 return f"{ color } { value :3.1f} { unit } "
371398 value /= 1000.0
372399
373- # To validate if a file is within the file tree,
374- # it needs to be absolute, so that it is possible to
400+ # Get the absolute path with set prefix. To validate if a file is inside the
401+ # current working directory it needs to be absolute, so that it is possible to
375402# check the prefix.
376403def _convert_to_abs (path : str , abs_prefix : str = None ) -> str :
377404 if os .path .isabs (path ): return path
378405 if abs_prefix is None : abs_prefix = os .getcwd ()
379406 return os .path .normpath (os .path .join (abs_prefix , path ))
380407
381- # Custom YAML loader - required to handle the parent INHERIT config.
382- # It converts the INHERIT path to absolute as a side effect.
383- # Returns the loaded config, or a list of all loaded configs.
408+ # Get the loaded config, or a list with all loaded configs. MkDocs removes the
409+ # INHERIT configuration key during load, and doesn't expose the information in
410+ # any way, as the parent configuration is merged into one. The INHERIT path is
411+ # needed for validation. This custom YAML loader replicates MkDocs' loading
412+ # logic. Side effect: It converts the INHERIT path to absolute.
384413def _load_yaml (abs_src_path : str ):
385414
386415 with open (abs_src_path , "r" , encoding = "utf-8-sig" ) as file :
@@ -417,11 +446,9 @@ def _load_exclusion_patterns(path: str = None):
417446
418447 return [line for line in lines if line and not line .startswith ("#" )]
419448
420- # For the pattern matching it is best to remove the CWD
421- # prefix and keep only the relative root of the reproduction.
422- # Additionally, as the patterns are in POSIX format,
423- # assure that the path is also in POSIX format.
424- # Side-effect: It appends "/" for directory patterns.
449+ # Get a normalized POSIX path for the pattern matching with removed current
450+ # working directory prefix. Directory paths end with a '/' to allow more control
451+ # in the pattern creation for files and directories.
425452def _resolve_pattern (abspath : str ):
426453 path = abspath .replace (os .getcwd (), "" , 1 ).replace (os .sep , "/" )
427454
@@ -434,7 +461,7 @@ def _resolve_pattern(abspath: str):
434461
435462 return path
436463
437- # Get project configuration
464+ # Get project configuration with resolved absolute paths for validation
438465def _get_project_config (project_config_file : str ):
439466 with open (project_config_file , encoding = "utf-8" ) as file :
440467 config = MkDocsConfig (config_file_path = project_config_file )
0 commit comments