Skip to content

Commit 06ec44d

Browse files
authored
Feature: improve social cards integration (#294)
Related to #257
2 parents a51c596 + fd0b4d8 commit 06ec44d

File tree

2 files changed

+192
-41
lines changed

2 files changed

+192
-41
lines changed

mkdocs_rss_plugin/integrations/theme_material_social_plugin.py

Lines changed: 175 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,24 @@
55
# ##################################
66

77
# standard library
8+
import json
9+
from hashlib import md5
810
from pathlib import Path
911

1012
# 3rd party
11-
from mkdocs.config.config_options import Config
13+
from mkdocs.config.defaults import MkDocsConfig
1214
from mkdocs.plugins import get_plugin_logger
1315
from mkdocs.structure.pages import Page
1416

1517
# package
1618
from 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,

mkdocs_rss_plugin/util.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -534,40 +534,41 @@ def get_image(self, in_page: Page, base_url: str) -> tuple[str, str, int] | None
534534
f"{in_page.file.src_uri}"
535535
)
536536
elif (
537-
self.social_cards.IS_ENABLED
537+
isinstance(self.social_cards, IntegrationMaterialSocialCards)
538+
and self.social_cards.IS_ENABLED
538539
and self.social_cards.IS_SOCIAL_PLUGIN_CARDS_ENABLED
539540
and self.social_cards.is_social_plugin_enabled_page(
540541
mkdocs_page=in_page,
541-
fallback_value=self.social_cards,
542+
fallback_value=self.social_cards.IS_SOCIAL_PLUGIN_CARDS_ENABLED,
542543
)
543544
):
544-
img_local_path = self.social_cards.get_social_card_build_path_for_page(
545-
mkdocs_page=in_page
546-
)
545+
547546
img_url = self.social_cards.get_social_card_url_for_page(
548547
mkdocs_page=in_page
549548
)
550-
logger.debug(
551-
f"Image found ({img_url}) from social cards for page: "
552-
f"{in_page.file.src_uri}. Using local image to get mime and length: "
553-
f"{img_local_path}"
554-
)
555-
556-
if img_local_path.is_file():
557-
logger.debug("Local image already exists. Using it to get its length.")
558-
img_length = img_local_path.stat().st_size
549+
if img_local_cache_path := self.social_cards.get_social_card_cache_path_for_page(
550+
mkdocs_page=in_page
551+
):
552+
img_length = img_local_cache_path.stat().st_size
553+
img_type = guess_type(url=img_local_cache_path, strict=False)[0]
554+
elif img_local_build_path := self.social_cards.get_social_card_build_path_for_page(
555+
mkdocs_page=in_page
556+
):
557+
img_length = img_local_build_path.stat().st_size
558+
img_type = guess_type(url=img_local_build_path, strict=False)[0]
559559
else:
560560
logger.debug(
561-
f"Social card: {img_local_path} still not exists. Trying to "
561+
"Social card still not exists locally. Trying to "
562562
f"retrieve length from remote image: {img_url}. "
563563
"Note that would work only if the social card image has been "
564564
"already published before the build."
565565
)
566566
img_length = self.get_remote_image_length(image_url=img_url)
567+
img_type = guess_type(url=img_url, strict=False)[0]
567568

568569
return (
569570
img_url,
570-
guess_type(url=img_local_path, strict=False)[0],
571+
img_type,
571572
img_length,
572573
)
573574

0 commit comments

Comments
 (0)