99import importlib
1010import os
1111from copy import copy
12+ import pathspec
13+ import json
14+ from datetime import datetime
1215
1316import yaml
1417from jinja2 import (
1518 Environment , FileSystemLoader , Undefined , DebugUndefined , StrictUndefined ,
1619)
17- import pathspec
18-
1920from mkdocs .config import config_options
2021from mkdocs .config .config_options import Type as PluginType
2122from mkdocs .plugins import BasePlugin
2526from mkdocs_macros .context import define_env
2627from mkdocs_macros .util import (
2728 install_package , parse_package , trace , debug ,
28- update , SuperDict , import_local_module , format_chatter , LOG ,
29+ update , SuperDict , import_local_module , format_chatter , LOG , get_log_level ,
30+ setup_directory , CustomEncoder
2931)
3032
3133# ------------------------------------------
3840# The default name of the Python module:
3941DEFAULT_MODULE_NAME = 'main' # main.py
4042
41- # Possible behavior in case of ignored variables or macros (first is default)
43+ # the directory where the rendered macros must go
44+ RENDERED_MACROS_DIRNAME = '__docs_macros_rendered'
45+
46+
47+
48+ # ------------------------------------------
49+ # Debug
50+ # ------------------------------------------
4251
52+ # message for the front matter of markdown pages saved after rendering:
53+ YAML_HEADER_WARNING = (
54+ "# IMPORTANT NOTE:"
55+ "\n # This page was automatically generated by MkDocs-Macros "
56+ "for debug purposes,"
57+ "\n # after rendering the macros as plain text."
58+ f"\n # ({ datetime .now ():%Y-%m-%d %H:%M:%S} )"
59+ )
4360
61+
62+ # Possible behavior in case of ignored variables or macros (first is default)
4463class LaxUndefined (Undefined ):
4564 "Pass anything wrong as blank"
4665
@@ -271,6 +290,27 @@ def reverse(x):
271290 self .filters [name ] = v
272291 return v
273292
293+
294+
295+ @property
296+ def rendered_macros_dir (self ):
297+ """
298+ The directory, beside the docs_dir, that contains
299+ the rendered pages from the macros.
300+ """
301+ try :
302+ r = self ._rendered_macros_dir
303+ except AttributeError :
304+ raise AttributeError ("Rendered macros directory is undefined" )
305+ if not os .path .isdir (self ._rendered_macros_dir ):
306+ raise FileNotFoundError ("Rendered macros directory is defined "
307+ "but does not exists" )
308+ return r
309+
310+
311+ # ------------------------------------------------
312+ # Property of the current page for on_page_markdown()
313+ # ------------------------------------------------
274314 @property
275315 def page (self ) -> Page :
276316 """
@@ -296,7 +336,10 @@ def markdown(self) -> str:
296336 @markdown .setter
297337 def markdown (self , value ):
298338 """
299- Used to set the raw markdown of the current page
339+ Used to set the raw markdown of the current page.
340+
341+ [Especially used in the `on_pre_page_macros()` and
342+ `on_ost_page_macros()` hooks.]
300343 """
301344 if not isinstance (value , str ):
302345 raise ValueError ("Value provided to attribute markdown "
@@ -561,7 +604,12 @@ def _load_modules(self):
561604 "module in '%s'." %
562605 (local_module_name , self .project_dir ))
563606
564- def render (self , markdown : str , force_rendering :bool = False ):
607+
608+ # ----------------------------------
609+ # output elements
610+ # ----------------------------------
611+
612+ def render (self , markdown : str , force_rendering :bool = False ) -> str :
565613 """
566614 Render a page through jinja2: it reads the code and
567615 executes the macros.
@@ -605,11 +653,14 @@ def render(self, markdown: str, force_rendering:bool=False):
605653 # this is a premature rendering, no meta variables in the page
606654 meta_variables = {}
607655
608-
609656 # Warning this is ternary logic(True, False, None: nothing said)
610657 render_macros = None
611658
612659 if meta_variables :
660+ # file_path = self.variables.page.file.src_path
661+ file_path = self .page .file .src_path
662+ debug (f"Metadata in page '{ file_path } '" ,
663+ payload = meta_variables )
613664 # determine whether the page will be rendered or not
614665 # the two formulations are accepted
615666 render_macros = meta_variables .get ('render_macros' )
@@ -658,6 +709,44 @@ def render(self, markdown: str, force_rendering:bool=False):
658709 else :
659710 return error_message
660711
712+ def _save_debug_file (self , page :Page ,
713+ rendered_markdown :str ) -> str :
714+ """
715+ Saves a page to disk for debug/testing purposes,
716+ with a reconstituted YAML front matter.
717+
718+ Argument:
719+ - page: the Page (page.markdown contains the old markdown)
720+ - rendered_mardkown (the new markdown)
721+
722+ Returns the saved document.
723+ """
724+ dest_file = os .path .join (self .rendered_macros_dir ,
725+ page .file .src_path )
726+ debug (f"Saving page '{ page .title } ' in destination file:" ,
727+ dest_file )
728+ # Create the subdirectory hierarchy if necessary
729+ os .makedirs (os .path .dirname (dest_file ), exist_ok = True )
730+ if page .meta :
731+ # recreate the YAML header:
732+ yaml_values = yaml .dump (dict (page .meta ),
733+ default_flow_style = False , sort_keys = False )
734+ document = '\n ' .join ([ '---' ,
735+ YAML_HEADER_WARNING ,
736+ yaml_values .strip (),
737+ '---' ,
738+ rendered_markdown
739+ ])
740+ else :
741+ # re-generate the document with YAML header
742+ document = rendered_markdown
743+ # write on file:
744+ debug ("Saved " )
745+ with open (dest_file , 'w' ) as f :
746+ f .write (document )
747+ return document
748+
749+
661750 # ----------------------------------
662751 # Standard Hooks for a mkdocs plugin
663752 # ----------------------------------
@@ -669,7 +758,7 @@ def on_config(self, config):
669758 with variables, functions and filters.
670759 """
671760 # WARNING: this is not the config argument:
672- trace ("Macros arguments: " , self .config )
761+ trace ("Macros arguments\n " , self .config )
673762 # define the variables and macros as dictionaries
674763 # (for update function to work):
675764 self ._variables = SuperDict ()
@@ -716,12 +805,20 @@ def on_config(self, config):
716805 register_items ('filter' , self .filters , self ._add_filters )
717806
718807 # Provide information:
719- debug ("Variables:" , list (self .variables .keys ()))
720- if len (extra ):
721- trace ("Extra variables (config file):" , list (extra .keys ()))
722- debug ("Content of extra variables (config file):" , extra )
808+ trace ("Config variables:" , list (self .variables .keys ()))
809+ debug ("Config variables:\n " , payload = json .dumps (self .variables ,
810+ cls = CustomEncoder ))
811+ if self .macros :
812+ trace ("Config macros:" , list (self .macros .keys ()))
813+ debug ("Config macros:" , payload = json .dumps (self .macros ,
814+ cls = CustomEncoder ))
723815 if self .filters :
724- trace ("Extra filters (module):" , list (self .filters .keys ()))
816+ trace ("Config filters:" , list (self .filters .keys ()))
817+ debug ("Config filters:" , payload = json .dumps (self .filters ,
818+ cls = CustomEncoder ))
819+ # if len(extra):
820+ # trace("Extra variables (config file):", list(extra.keys()))
821+ # debug("Content of extra variables (config file):\n", dict(extra))
725822
726823
727824 # Define the spec for the file paths whose rendering must be forced.
@@ -793,6 +890,17 @@ def on_config(self, config):
793890 # update environment with the custom filters:
794891 self .env .filters .update (self .filters )
795892
893+ # -------------------
894+ # Setup the markdown (rendered) directory
895+ # -------------------
896+ docs_dir = config ['docs_dir' ]
897+ abs_docs_dir = os .path .abspath (docs_dir )
898+ # recreate only if debug (otherewise delete):
899+ recreate = get_log_level ('DEBUG' )
900+ self ._rendered_macros_dir = setup_directory (abs_docs_dir ,
901+ RENDERED_MACROS_DIRNAME ,
902+ recreate = recreate )
903+
796904 def on_nav (self , nav , config , files ):
797905 """
798906 Called after the site navigation is created.
@@ -840,14 +948,11 @@ def on_page_markdown(self, markdown, page:Page,
840948 It uses the jinja2 directives, together with
841949 variables, macros and filters, to create pure markdown code.
842950 """
843- # the site_navigation argument has been made optional
844- # (deleted in post-1.0 mkdocs, but maintained here
845- # for backward compatibility)
846- # We REALLY want the same object
847951 self ._page = page
848952 if not self .variables :
849953 return markdown
850954 else :
955+ trace ("Rendering source page:" , page .file .src_path )
851956 # Update the page info in the document
852957 # page is an object with a number of properties (title, url, ...)
853958 # see: https://github.com/mkdocs/mkdocs/blob/master/mkdocs/structure/pages.py
@@ -880,6 +985,12 @@ def on_page_markdown(self, markdown, page:Page,
880985 # execute the post-macro functions in the various modules
881986 for func in self .post_macro_functions :
882987 func (self )
988+
989+ # save the rendered page, with its YAML header
990+ if get_log_level ('DEBUG' ):
991+ self ._save_debug_file (page ,
992+ rendered_markdown = self .markdown )
993+
883994 return self .markdown
884995
885996 def on_post_build (self , config : config_options .Config ):
0 commit comments