2727import site
2828import sys
2929
30+ import yaml
3031from colorama import Fore , Style
3132from importlib .metadata import distributions , version
3233from io import BytesIO
3334from markdown .extensions .toc import slugify
3435from mkdocs .config .defaults import MkDocsConfig
3536from mkdocs .plugins import BasePlugin , event_priority
36- from mkdocs .utils import get_theme_dir
37+ from mkdocs .utils import get_yaml_loader
3738import regex
3839from zipfile import ZipFile , ZIP_DEFLATED
3940
@@ -97,7 +98,7 @@ def on_config(self, config):
9798 # hack to detect whether the custom_dir setting was used without parsing
9899 # mkdocs.yml again - we check at which position the directory provided
99100 # by the theme resides, and if it's not the first one, abort.
100- if config .theme .dirs . index ( get_theme_dir ( config . theme . name )) :
101+ if config .theme .custom_dir :
101102 log .error ("Please remove 'custom_dir' setting." )
102103 self ._help_on_customizations_and_exit ()
103104
@@ -109,27 +110,57 @@ def on_config(self, config):
109110 log .error ("Please remove 'hooks' setting." )
110111 self ._help_on_customizations_and_exit ()
111112
112- # Assure that config_file_path is absolute.
113- # If the --config-file option is used then the path is
114- # used as provided, so it is likely relative.
115- if not os .path .isabs (config .config_file_path ):
116- config .config_file_path = os .path .normpath (os .path .join (
117- os .getcwd (),
118- config .config_file_path
119- ))
113+ # Assure that possible relative paths, which will be validated
114+ # or used to generate other paths are absolute.
115+ config .config_file_path = _convert_to_abs (config .config_file_path )
116+ config_file_parent = os .path .dirname (config .config_file_path )
117+
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 ,
124+ abs_prefix = config_file_parent
125+ )
120126
121127 # Support projects plugin
122128 projects_plugin = config .plugins .get ("material/projects" )
123129 if projects_plugin :
124- abs_projects_dir = os .path .normpath (
125- os .path .join (
126- os .path .dirname (config .config_file_path ),
127- projects_plugin .config .projects_dir
128- )
130+ abs_projects_dir = _convert_to_abs (
131+ projects_plugin .config .projects_dir ,
132+ abs_prefix = config_file_parent
129133 )
130134 else :
131135 abs_projects_dir = ""
132136
137+ # Load the current MkDocs config(s) to get access to INHERIT
138+ loaded_configs = _load_yaml (config .config_file_path )
139+ if not isinstance (loaded_configs , list ):
140+ loaded_configs = [loaded_configs ]
141+
142+ # Validate different MkDocs paths to assure that
143+ # they're children of the current working directory.
144+ paths_to_validate = [
145+ config .config_file_path ,
146+ config .docs_dir ,
147+ custom_dir or "" ,
148+ abs_projects_dir ,
149+ * [cfg .get ("INHERIT" , "" ) for cfg in loaded_configs ]
150+ ]
151+
152+ for hook in config .hooks :
153+ path = _convert_to_abs (hook , abs_prefix = config_file_parent )
154+ paths_to_validate .append (path )
155+
156+ for path in list (paths_to_validate ):
157+ if not path or path .startswith (os .getcwd ()):
158+ paths_to_validate .remove (path )
159+
160+ if paths_to_validate :
161+ log .error (f"One or more paths aren't children of root" )
162+ self ._help_on_not_in_cwd (paths_to_validate )
163+
133164 # Create in-memory archive and prompt author for a short descriptive
134165 # name for the archive, which is also used as the directory name. Note
135166 # that the name is slugified for better readability and stripped of any
@@ -295,7 +326,28 @@ def _help_on_customizations_and_exit(self):
295326 if self .config .archive_stop_on_violation :
296327 sys .exit (1 )
297328
298- # Exclude files, which we don't want in our zip file
329+ # Print help on not in current working directory and exit
330+ def _help_on_not_in_cwd (self , bad_paths ):
331+ print (Fore .RED )
332+ print (" The current working (root) directory:\n " )
333+ print (f" { os .getcwd ()} \n " )
334+ print (" is not a parent of the following paths:" )
335+ print (Style .NORMAL )
336+ for path in bad_paths :
337+ 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." )
344+ print (Style .RESET_ALL )
345+
346+ # Exit, unless explicitly told not to
347+ if self .config .archive_stop_on_violation :
348+ sys .exit (1 )
349+
350+ # Exclude files which we don't want in our zip file
299351 def _is_excluded (self , posix_path : str ) -> bool :
300352 for pattern in self .exclusion_patterns :
301353 if regex .match (pattern , posix_path ):
@@ -318,6 +370,42 @@ def _size(value, factor = 1):
318370 return f"{ color } { value :3.1f} { unit } "
319371 value /= 1000.0
320372
373+ # To validate if a file is within the file tree,
374+ # it needs to be absolute, so that it is possible to
375+ # check the prefix.
376+ def _convert_to_abs (path : str , abs_prefix : str = None ) -> str :
377+ if os .path .isabs (path ): return path
378+ if abs_prefix is None : abs_prefix = os .getcwd ()
379+ return os .path .normpath (os .path .join (abs_prefix , path ))
380+
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.
384+ def _load_yaml (abs_src_path : str ):
385+
386+ with open (abs_src_path , "r" , encoding = "utf-8-sig" ) as file :
387+ source = file .read ()
388+
389+ try :
390+ result = yaml .load (source , Loader = get_yaml_loader ()) or {}
391+ except yaml .YAMLError :
392+ result = {}
393+
394+ if "INHERIT" in result :
395+ relpath = result .get ('INHERIT' )
396+ parent_path = os .path .dirname (abs_src_path )
397+ abspath = _convert_to_abs (relpath , abs_prefix = parent_path )
398+ if os .path .exists (abspath ):
399+ result ["INHERIT" ] = abspath
400+ log .debug (f"Loading inherited configuration file: { abspath } " )
401+ parent = _load_yaml (abspath )
402+ if isinstance (parent , list ):
403+ result = [result , * parent ]
404+ elif isinstance (parent , dict ):
405+ result = [result , parent ]
406+
407+ return result
408+
321409# Load info.gitignore, ignore any empty lines or # comments
322410def _load_exclusion_patterns (path : str = None ):
323411 if path is None :
0 commit comments