55# ##################################
66
77# standard library
8+ import json
9+ from hashlib import md5
810from pathlib import Path
911
1012# 3rd party
11- from mkdocs .config .config_options import Config
13+ from mkdocs .config .defaults import MkDocsConfig
1214from mkdocs .plugins import get_plugin_logger
1315from mkdocs .structure .pages import Page
1416
1517# package
1618from mkdocs_rss_plugin .constants import MKDOCS_LOGGER_NAME
1719
20+ # conditional
21+ try :
22+ from material import __version__ as material_version
23+ except ImportError :
24+ material_version = None
25+
1826# ############################################################################
1927# ########## Globals #############
2028# ################################
@@ -32,12 +40,14 @@ class IntegrationMaterialSocialCards:
3240 IS_SOCIAL_PLUGIN_ENABLED : bool = True
3341 IS_SOCIAL_PLUGIN_CARDS_ENABLED : bool = True
3442 IS_THEME_MATERIAL : bool = False
43+ IS_INSIDERS : bool = False
44+ CARDS_MANIFEST : dict | None = None
3545
36- def __init__ (self , mkdocs_config : Config , switch_force : bool = True ) -> None :
46+ def __init__ (self , mkdocs_config : MkDocsConfig , switch_force : bool = True ) -> None :
3747 """Integration instanciation.
3848
3949 Args:
40- mkdocs_config (Config ): Mkdocs website configuration object.
50+ mkdocs_config (MkDocsConfig ): Mkdocs website configuration object.
4151 switch_force (bool, optional): option to force integration disabling. Set
4252 it to False to disable it even if Social Cards are enabled in Mkdocs
4353 configuration. Defaults to True.
@@ -68,55 +78,85 @@ def __init__(self, mkdocs_config: Config, switch_force: bool = True) -> None:
6878 if self .IS_ENABLED :
6979 self .mkdocs_site_url = mkdocs_config .site_url
7080 self .mkdocs_site_build_dir = mkdocs_config .site_dir
71- self .social_cards_assets_dir = self .get_social_cards_dir (
81+ self .social_cards_assets_dir = self .get_social_cards_build_dir (
82+ mkdocs_config = mkdocs_config
83+ )
84+ self .social_cards_cache_dir = self .get_social_cards_cache_dir (
7285 mkdocs_config = mkdocs_config
7386 )
87+ if self .is_mkdocs_theme_material_insiders ():
88+ self .load_cache_cards_manifest ()
89+
90+ # store some attributes used to compute social card hash
91+ self .site_name = mkdocs_config .site_name
92+ self .site_description = mkdocs_config .site_description or ""
7493
75- def is_theme_material (self , mkdocs_config : Config ) -> bool :
94+ def is_mkdocs_theme_material (self , mkdocs_config : MkDocsConfig ) -> bool :
7695 """Check if the theme set in mkdocs.yml is material or not.
7796
7897 Args:
79- mkdocs_config (Config ): Mkdocs website configuration object.
98+ mkdocs_config (MkDocsConfig ): Mkdocs website configuration object.
8099
81100 Returns:
82101 bool: True if the theme's name is 'material'. False if not.
83102 """
84103 self .IS_THEME_MATERIAL = mkdocs_config .theme .name == "material"
85104 return self .IS_THEME_MATERIAL
86105
87- def is_social_plugin_enabled_mkdocs (self , mkdocs_config : Config ) -> bool :
106+ def is_mkdocs_theme_material_insiders (self ) -> bool | None :
107+ """Check if the material theme is community or insiders edition.
108+
109+ Returns:
110+ bool: True if the theme is Insiders edition. False if community. None if
111+ the Material theme is not installed.
112+ """
113+ if not self .IS_THEME_MATERIAL :
114+ return None
115+
116+ if material_version is not None and "insiders" in material_version :
117+ logger .debug ("Material theme edition INSIDERS" )
118+ self .IS_INSIDERS = True
119+ return True
120+ else :
121+ logger .debug ("Material theme edition COMMUNITY" )
122+ self .IS_INSIDERS = False
123+ return False
124+
125+ def is_social_plugin_enabled_mkdocs (self , mkdocs_config : MkDocsConfig ) -> bool :
88126 """Check if social plugin is installed and enabled.
89127
90128 Args:
91- mkdocs_config (Config ): Mkdocs website configuration object.
129+ mkdocs_config (MkDocsConfig ): Mkdocs website configuration object.
92130
93131 Returns:
94132 bool: True if the theme material and the plugin social cards is enabled.
95133 """
96- if not self .is_theme_material (mkdocs_config = mkdocs_config ):
134+ if not self .is_mkdocs_theme_material (mkdocs_config = mkdocs_config ):
97135 logger .debug ("Installed theme is not 'material'. Integration disabled." )
98136 return False
99137
100138 if not mkdocs_config .plugins .get ("material/social" ):
101- logger .debug ("Social plugin not listed in configuration." )
139+ logger .debug ("Material Social plugin not listed in configuration." )
102140 return False
103141
104142 social_plugin_cfg = mkdocs_config .plugins .get ("material/social" )
105143
106144 if not social_plugin_cfg .config .enabled :
107- logger .debug ("Social plugin is installed but disabled." )
145+ logger .debug ("Material Social plugin is installed but disabled." )
108146 self .IS_SOCIAL_PLUGIN_ENABLED = False
109147 return False
110148
111- logger .debug ("Social plugin is enabled in Mkdocs configuration." )
149+ logger .debug ("Material Social plugin is enabled in Mkdocs configuration." )
112150 self .IS_SOCIAL_PLUGIN_CARDS_ENABLED = True
113151 return True
114152
115- def is_social_plugin_and_cards_enabled_mkdocs (self , mkdocs_config : Config ) -> bool :
153+ def is_social_plugin_and_cards_enabled_mkdocs (
154+ self , mkdocs_config : MkDocsConfig
155+ ) -> bool :
116156 """Check if social cards plugin is enabled.
117157
118158 Args:
119- mkdocs_config (Config ): Mkdocs website configuration object.
159+ mkdocs_config (MkDocsConfig ): Mkdocs website configuration object.
120160
121161 Returns:
122162 bool: True if the theme material and the plugin social cards is enabled.
@@ -127,19 +167,21 @@ def is_social_plugin_and_cards_enabled_mkdocs(self, mkdocs_config: Config) -> bo
127167 social_plugin_cfg = mkdocs_config .plugins .get ("material/social" )
128168
129169 if not social_plugin_cfg .config .cards :
130- logger .debug ("Social plugin is installed, present but cards are disabled." )
170+ logger .debug (
171+ "Material Social plugin is installed, present but cards are disabled."
172+ )
131173 self .IS_SOCIAL_PLUGIN_CARDS_ENABLED = False
132174 return False
133175
134- logger .debug ("Social cards are enabled in Mkdocs configuration." )
176+ logger .debug ("Material Social cards are enabled in Mkdocs configuration." )
135177 self .IS_SOCIAL_PLUGIN_CARDS_ENABLED = True
136178 return True
137179
138180 def is_social_plugin_enabled_page (
139181 self , mkdocs_page : Page , fallback_value : bool = True
140182 ) -> bool :
141183 """Check if the social plugin is enabled or disabled for a specific page. Plugin
142- has to enabled in Mkdocs configuration before.
184+ has to be enabled in Mkdocs configuration before.
143185
144186 Args:
145187 mkdocs_page (Page): Mkdocs page object.
@@ -153,46 +195,154 @@ def is_social_plugin_enabled_page(
153195 "cards" , fallback_value
154196 )
155197
156- def get_social_cards_dir (self , mkdocs_config : Config ) -> str :
198+ def load_cache_cards_manifest (self ) -> dict | None :
199+ """Load social cards manifest if the file exists.
200+
201+ Returns:
202+ dict | None: manifest as dict or None if the file does not exist
203+ """
204+ cache_cards_manifest = Path (self .social_cards_cache_dir ).joinpath (
205+ "manifest.json"
206+ )
207+ if not cache_cards_manifest .is_file ():
208+ logger .debug (
209+ "Material Social Cards cache manifest file not found: "
210+ f"{ cache_cards_manifest } "
211+ )
212+ return None
213+
214+ with cache_cards_manifest .open (mode = "r" , encoding = "UTF-8" ) as manifest :
215+ self .CARDS_MANIFEST = json .load (manifest )
216+ logger .debug (
217+ f"Material Social Cards cache manifest loaded from { cache_cards_manifest } "
218+ )
219+
220+ return self .CARDS_MANIFEST
221+
222+ def get_social_cards_build_dir (self , mkdocs_config : MkDocsConfig ) -> Path :
157223 """Get Social Cards folder within Mkdocs site_dir.
158224 See: https://squidfunk.github.io/mkdocs-material/plugins/social/#config.cards_dir
159225
160226 Args:
161- mkdocs_config (Config ): Mkdocs website configuration object.
227+ mkdocs_config (MkDocsConfig ): Mkdocs website configuration object.
162228
163229 Returns:
164230 str: True if the theme material and the plugin social cards is enabled.
165231 """
166232 social_plugin_cfg = mkdocs_config .plugins .get ("material/social" )
167233
168234 logger .debug (
169- "Social cards folder in Mkdocs build directory: "
235+ "Material Social cards folder in Mkdocs build directory: "
170236 f"{ social_plugin_cfg .config .cards_dir } ."
171237 )
172238
173- return social_plugin_cfg .config .cards_dir
239+ return Path (social_plugin_cfg .config .cards_dir ).resolve ()
240+
241+ def get_social_cards_cache_dir (self , mkdocs_config : MkDocsConfig ) -> Path :
242+ """Get Social Cards folder within Mkdocs site_dir.
243+ See: https://squidfunk.github.io/mkdocs-material/plugins/social/#config.cards_dir
244+
245+ Args:
246+ mkdocs_config (MkDocsConfig): Mkdocs website configuration object.
247+
248+ Returns:
249+ str: True if the theme material and the plugin social cards is enabled.
250+ """
251+ social_plugin_cfg = mkdocs_config .plugins .get ("material/social" )
252+ self .social_cards_cache_dir = Path (social_plugin_cfg .config .cache_dir ).resolve ()
253+
254+ logger .debug (
255+ "Material Social cards cache folder: " f"{ self .social_cards_cache_dir } ."
256+ )
257+
258+ return self .social_cards_cache_dir
174259
175260 def get_social_card_build_path_for_page (
176261 self , mkdocs_page : Page , mkdocs_site_dir : str | None = None
177- ) -> Path :
178- """Get social card URL for a specific page in documentation .
262+ ) -> Path | None :
263+ """Get social card path in Mkdocs build dir for a specific page.
179264
180265 Args:
181266 mkdocs_page (Page): Mkdocs page object.
182267 mkdocs_site_dir (Optional[str], optional): Mkdocs build site dir. If None, the
183268 'class.mkdocs_site_build_dir' is used. is Defaults to None.
184269
185270 Returns:
186- str: URL to the image once published
271+ Path: path to the image once published
187272 """
188273 if mkdocs_site_dir is None and self .mkdocs_site_build_dir :
189274 mkdocs_site_dir = self .mkdocs_site_build_dir
190275
191- return Path (
276+ expected_built_card_path = Path (
192277 f"{ mkdocs_site_dir } /{ self .social_cards_assets_dir } /"
193278 f"{ Path (mkdocs_page .file .src_uri ).with_suffix ('.png' )} "
194279 )
195280
281+ if expected_built_card_path .is_file ():
282+ logger .debug (
283+ f"Social card file found in cache folder: { expected_built_card_path } "
284+ )
285+ return expected_built_card_path
286+ else :
287+ logger .debug (f"Not found: { expected_built_card_path } " )
288+ return None
289+
290+ def get_social_card_cache_path_for_page (self , mkdocs_page : Page ) -> Path | None :
291+ """Get social card path in social plugin cache folder for a specific page.
292+
293+ Note:
294+ As we write this code (June 2024), the cache mechanism in Insiders edition
295+ has stores images directly with the corresponding Page's path and name and
296+ keep a correspondance matrix with hashes in a manifest.json;
297+ the cache mechanism in Community edition uses the hash as file names without
298+ any exposed matching criteria.
299+
300+ Args:
301+ mkdocs_page (Page): Mkdocs page object.
302+
303+ Returns:
304+ Path: path to the image in local cache folder if it exists
305+ """
306+ if self .IS_INSIDERS :
307+ expected_cached_card_path = self .social_cards_cache_dir .joinpath (
308+ f"assets/images/social/{ Path (mkdocs_page .file .src_uri ).with_suffix ('.png' )} "
309+ )
310+ if expected_cached_card_path .is_file ():
311+ logger .debug (
312+ f"Social card file found in cache folder: { expected_cached_card_path } "
313+ )
314+ return expected_cached_card_path
315+ else :
316+ logger .debug (f"Not found: { expected_cached_card_path } " )
317+
318+ else :
319+ if "description" in mkdocs_page .meta :
320+ description = mkdocs_page .meta ["description" ]
321+ else :
322+ description = self .site_description
323+
324+ page_hash = md5 (
325+ "" .join (
326+ [
327+ self .site_name ,
328+ str (mkdocs_page .meta .get ("title" , mkdocs_page .title )),
329+ description ,
330+ ]
331+ ).encode ("utf-8" )
332+ )
333+ expected_cached_card_path = self .social_cards_cache_dir .joinpath (
334+ f"{ page_hash .hexdigest ()} .png"
335+ )
336+
337+ if expected_cached_card_path .is_file ():
338+ logger .debug (
339+ f"Social card file found in cache folder: { expected_cached_card_path } "
340+ )
341+ return expected_cached_card_path
342+ else :
343+ logger .debug (f"Not found: { expected_cached_card_path } " )
344+ return None
345+
196346 def get_social_card_url_for_page (
197347 self ,
198348 mkdocs_page : Page ,
0 commit comments