27
27
import site
28
28
import sys
29
29
30
+ import yaml
30
31
from colorama import Fore , Style
31
32
from importlib .metadata import distributions , version
32
33
from io import BytesIO
33
34
from markdown .extensions .toc import slugify
34
35
from mkdocs .config .defaults import MkDocsConfig
35
36
from mkdocs .plugins import BasePlugin , event_priority
36
- from mkdocs .utils import get_theme_dir
37
+ from mkdocs .utils import get_yaml_loader
37
38
import regex
38
39
from zipfile import ZipFile , ZIP_DEFLATED
39
40
@@ -97,7 +98,7 @@ def on_config(self, config):
97
98
# hack to detect whether the custom_dir setting was used without parsing
98
99
# mkdocs.yml again - we check at which position the directory provided
99
100
# 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 :
101
102
log .error ("Please remove 'custom_dir' setting." )
102
103
self ._help_on_customizations_and_exit ()
103
104
@@ -109,27 +110,57 @@ def on_config(self, config):
109
110
log .error ("Please remove 'hooks' setting." )
110
111
self ._help_on_customizations_and_exit ()
111
112
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
+ )
120
126
121
127
# Support projects plugin
122
128
projects_plugin = config .plugins .get ("material/projects" )
123
129
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
129
133
)
130
134
else :
131
135
abs_projects_dir = ""
132
136
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
+
133
164
# Create in-memory archive and prompt author for a short descriptive
134
165
# name for the archive, which is also used as the directory name. Note
135
166
# that the name is slugified for better readability and stripped of any
@@ -295,7 +326,28 @@ def _help_on_customizations_and_exit(self):
295
326
if self .config .archive_stop_on_violation :
296
327
sys .exit (1 )
297
328
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
299
351
def _is_excluded (self , posix_path : str ) -> bool :
300
352
for pattern in self .exclusion_patterns :
301
353
if regex .match (pattern , posix_path ):
@@ -318,6 +370,42 @@ def _size(value, factor = 1):
318
370
return f"{ color } { value :3.1f} { unit } "
319
371
value /= 1000.0
320
372
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
+
321
409
# Load info.gitignore, ignore any empty lines or # comments
322
410
def _load_exclusion_patterns (path : str = None ):
323
411
if path is None :
0 commit comments