Skip to content

Commit 722fcc2

Browse files
committed
add(output): implement atom output format in addition to RSS and JSON Feed
1 parent 18bc7a9 commit 722fcc2

10 files changed

Lines changed: 552 additions & 11 deletions

File tree

mkdocs_rss_plugin/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ class _DateFromMeta(Config):
3030
class _FeedsFilenamesConfig(Config):
3131
"""Sub configuration for feeds filenames."""
3232

33+
atom_created = config_options.Type(str, default="feed_atom_created.xml")
34+
atom_updated = config_options.Type(str, default="feed_atom_updated.xml")
3335
json_created = config_options.Type(str, default="feed_json_created.json")
3436
json_updated = config_options.Type(str, default="feed_json_updated.json")
3537
rss_created = config_options.Type(str, default="feed_rss_created.xml")
@@ -41,6 +43,7 @@ class RssPluginConfig(Config):
4143

4244
abstract_chars_count = config_options.Type(int, default=160)
4345
abstract_delimiter = config_options.Type(str, default="<!-- more -->")
46+
atom_feed_enabled = config_options.Type(bool, default=True)
4447
categories = config_options.Optional(
4548
config_options.ListOfItems(config_options.Type(str))
4649
)

mkdocs_rss_plugin/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class PageInformation:
6262
created: datetime | None = None
6363
description: str | None = None
6464
guid: str | None = None
65+
html_content: str | None = None
6566
image: tuple[str, str, int] | None = None
6667
link: str | None = None
6768
pub_date: str | None = None
@@ -77,6 +78,7 @@ class PageInformation:
7778
class RssFeedBase:
7879
"""Object describing a feed."""
7980

81+
atom_url: str | None = None
8082
author: str | None = None
8183
buildDate: str | None = None
8284
copyright: str | None = None

mkdocs_rss_plugin/plugin.py

Lines changed: 122 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,13 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig:
111111
return config
112112

113113
# Fail if any export option is enabled
114-
if not any([self.config.json_feed_enabled, self.config.rss_feed_enabled]):
114+
if not any(
115+
[
116+
self.config.atom_feed_enabled,
117+
self.config.json_feed_enabled,
118+
self.config.rss_feed_enabled,
119+
]
120+
):
115121
logger.error(
116122
"At least one export option has to be enabled. Plugin is disabled."
117123
)
@@ -255,18 +261,24 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig:
255261
# final feed url
256262
if base_feed.html_url:
257263
# concatenate both URLs
258-
self.feed_created.rss_url = (
259-
base_feed.html_url + self.config.feeds_filenames.rss_created
264+
self.feed_created.atom_url = (
265+
base_feed.html_url + self.config.feeds_filenames.atom_created
260266
)
261-
self.feed_updated.rss_url = (
262-
base_feed.html_url + self.config.feeds_filenames.rss_updated
267+
self.feed_updated.atom_url = (
268+
base_feed.html_url + self.config.feeds_filenames.atom_updated
263269
)
264270
self.feed_created.json_url = (
265271
base_feed.html_url + self.config.feeds_filenames.json_created
266272
)
267273
self.feed_updated.json_url = (
268274
base_feed.html_url + self.config.feeds_filenames.json_updated
269275
)
276+
self.feed_created.rss_url = (
277+
base_feed.html_url + self.config.feeds_filenames.rss_created
278+
)
279+
self.feed_updated.rss_url = (
280+
base_feed.html_url + self.config.feeds_filenames.rss_updated
281+
)
270282
else:
271283
logger.error(
272284
"The variable `site_url` is not set in the MkDocs "
@@ -275,7 +287,9 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig:
275287
)
276288
self.feed_created.rss_url = self.feed_updated.json_url = (
277289
self.feed_updated.rss_url
278-
) = self.feed_updated.json_url = None
290+
) = self.feed_updated.json_url = self.feed_created.atom_url = (
291+
self.feed_updated.atom_url
292+
) = None
279293

280294
# ending event
281295
return config
@@ -341,6 +355,11 @@ def on_page_content(
341355
else:
342356
page_url_comments = None
343357

358+
# Store full HTML content if needed for Atom feed
359+
page_content: str | None = None
360+
if self.config.abstract_chars_count == -1:
361+
page_content = html
362+
344363
# append to list to be filtered later
345364
self.pages_to_filter.append(
346365
PageInformation(
@@ -350,6 +369,7 @@ def on_page_content(
350369
in_page=page, categories_labels=self.config.categories
351370
),
352371
comments_url=page_url_comments,
372+
html_content=page_content,
353373
created=page_dates[0],
354374
description=self.util.get_description_or_abstract(
355375
in_page=page,
@@ -380,13 +400,13 @@ def on_post_build(self, config: config_options.Config) -> None:
380400
return
381401

382402
# pretty print or not
383-
pretty_print = self.config.pretty_print
403+
pretty_print: bool = self.config.pretty_print
384404

385405
# output filepaths
386-
out_feed_created = Path(config.site_dir).joinpath(
406+
out_rss_created = Path(config.site_dir).joinpath(
387407
self.config.feeds_filenames.rss_created
388408
)
389-
out_feed_updated = Path(config.site_dir).joinpath(
409+
out_rss_updated = Path(config.site_dir).joinpath(
390410
self.config.feeds_filenames.rss_updated
391411
)
392412
out_json_created = Path(config.site_dir).joinpath(
@@ -395,6 +415,12 @@ def on_post_build(self, config: config_options.Config) -> None:
395415
out_json_updated = Path(config.site_dir).joinpath(
396416
self.config.feeds_filenames.json_updated
397417
)
418+
out_atom_created: Path = Path(config.site_dir).joinpath(
419+
self.config.feeds_filenames.atom_created
420+
)
421+
out_atom_updated: Path = Path(config.site_dir).joinpath(
422+
self.config.feeds_filenames.atom_updated
423+
)
398424

399425
# stylesheet for RSS feed
400426
if self.config.stylesheet == "auto":
@@ -459,7 +485,7 @@ def on_post_build(self, config: config_options.Config) -> None:
459485
page.pub_date = format_datetime(dt=page.created)
460486

461487
# write file
462-
with out_feed_created.open(mode="w", encoding="UTF8") as fifeed_created:
488+
with out_rss_created.open(mode="w", encoding="UTF8") as fifeed_created:
463489
if pretty_print:
464490
fifeed_created.write(template.render(feed=self.feed_created))
465491
else:
@@ -481,7 +507,7 @@ def on_post_build(self, config: config_options.Config) -> None:
481507
page.pub_date = format_datetime(dt=page.updated)
482508

483509
# write file
484-
with out_feed_updated.open(mode="w", encoding="UTF8") as fifeed_updated:
510+
with out_rss_updated.open(mode="w", encoding="UTF8") as fifeed_updated:
485511
if pretty_print:
486512
fifeed_updated.write(template.render(feed=self.feed_updated))
487513
else:
@@ -511,3 +537,88 @@ def on_post_build(self, config: config_options.Config) -> None:
511537
fp,
512538
indent=4 if self.config.pretty_print else None,
513539
)
540+
541+
# ATOM FEED
542+
if self.config.atom_feed_enabled:
543+
# Jinja environment depending on the pretty print option
544+
if pretty_print:
545+
env = Environment(
546+
autoescape=select_autoescape(["html", "xml"]),
547+
loader=FileSystemLoader(self.tpl_folder),
548+
)
549+
else:
550+
env = Environment(
551+
autoescape=select_autoescape(["html", "xml"]),
552+
loader=FileSystemLoader(self.tpl_folder),
553+
lstrip_blocks=True,
554+
trim_blocks=True,
555+
)
556+
557+
template = env.get_template("atom.xml.jinja2")
558+
559+
# -- Feed sorted by creation date
560+
logger.debug(
561+
"Fill creation dates and dump created feed into Atom template."
562+
)
563+
564+
# Format dates for Atom (ISO 8601)
565+
for page in self.feed_created.entries:
566+
page.atom_published = page.created.isoformat()
567+
page.atom_updated = page.updated.isoformat()
568+
569+
# Format feed buildDate for Atom (ISO 8601)
570+
build_date_atom: str = datetime.fromtimestamp(
571+
get_build_timestamp()
572+
).isoformat()
573+
574+
# Temporarily store the ISO format
575+
original_build_date: str = self.feed_created.buildDate
576+
self.feed_created.buildDate = build_date_atom
577+
578+
# write file
579+
with out_atom_created.open(mode="w", encoding="UTF8") as fiatom_created:
580+
if pretty_print:
581+
fiatom_created.write(template.render(feed=self.feed_created))
582+
else:
583+
prev_char = ""
584+
for char in template.render(feed=asdict(self.feed_created)):
585+
if char == "\n":
586+
# convert new lines to spaces to preserve sentence structure
587+
char = " "
588+
if char == " " and prev_char == " ":
589+
prev_char = char
590+
continue
591+
prev_char = char
592+
fiatom_created.write(char)
593+
594+
# Restore original buildDate
595+
self.feed_created.buildDate = original_build_date
596+
597+
# -- Feed sorted by update date
598+
logger.debug("Fill update dates and dump updated feed into Atom template.")
599+
600+
for page in self.feed_updated.entries:
601+
page.atom_published = page.created.isoformat()
602+
page.atom_updated = page.updated.isoformat()
603+
604+
original_build_date = self.feed_updated.buildDate
605+
self.feed_updated.buildDate = build_date_atom
606+
607+
# write file
608+
with out_atom_updated.open(mode="w", encoding="UTF8") as fiatom_updated:
609+
if pretty_print:
610+
fiatom_updated.write(template.render(feed=self.feed_updated))
611+
else:
612+
prev_char = ""
613+
for char in template.render(feed=asdict(self.feed_updated)):
614+
if char == "\n":
615+
# convert new lines to spaces to preserve sentence structure
616+
char = " "
617+
if char == " " and prev_char == " ":
618+
prev_char = char
619+
continue
620+
prev_char = char
621+
fiatom_updated.write(char)
622+
623+
# Restore original buildDate
624+
self.feed_updated.buildDate = original_build_date
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<feed xmlns="http://www.w3.org/2005/Atom" {% if feed.language is not none %} xml:lang="{{ feed.language }}"{% endif %}>
3+
{# Mandatory elements #}
4+
{% if feed.title is not none %}<title>{{ feed.title|e }}</title>{% endif %}
5+
{% if feed.html_url is not none %}<link href="{{ feed.html_url }}" rel="alternate" type="text/html"/>{% endif %}
6+
{% if feed.atom_url is not none %}<link href="{{ feed.atom_url }}" rel="self" type="application/atom+xml"/>{% endif %}
7+
{% if feed.atom_url is not none %}<id>{{ feed.atom_url }}</id>{% endif %}
8+
9+
{# Optional elements #}
10+
{% if feed.description is not none %}<subtitle>{{ feed.description|e }}</subtitle>{% endif %}
11+
{% if feed.author is not none %}
12+
<author>
13+
<name>{{ feed.author|e }}</name>
14+
</author>
15+
{% endif %}
16+
17+
{# Timestamps #}
18+
<updated>{{ feed.buildDate }}</updated>
19+
20+
{# Credits #}
21+
<generator>{{ feed.generator }}</generator>
22+
23+
{# Feed illustration #}
24+
{% if feed.logo_url is defined %}
25+
<logo>{{ feed.logo_url }}</logo>
26+
{% endif %}
27+
28+
{# Entries #}
29+
{% for item in feed.entries %}
30+
<entry>
31+
<title>{{ item.title|e }}</title>
32+
{% if item.link is not none %}<link href="{{ item.link|e }}" rel="alternate" type="text/html"/>{% endif %}
33+
{% if item.guid is not none %}<id>{{ item.guid }}</id>{% endif %}
34+
<updated>{{ item.atom_updated }}</updated>
35+
<published>{{ item.atom_published }}</published>
36+
37+
{# Authors loop #}
38+
{% if item.authors is not none %}
39+
{% for author in item.authors %}
40+
<author>
41+
<name>{{ author }}</name>
42+
</author>
43+
{% endfor %}
44+
{% endif %}
45+
46+
{# Categories loop #}
47+
{% if item.categories is not none %}
48+
{% for category in item.categories %}
49+
<category term="{{ category }}"/>
50+
{% endfor %}
51+
{% endif %}
52+
53+
<summary type="html">{{ item.description|e }}</summary>
54+
55+
{# Full content if available - only include if not empty #}
56+
{% if item.content is not none and item.content|trim != "" %}
57+
<content type="html">{{ item.content|e }}</content>
58+
{% endif %}
59+
60+
61+
{# Image enclosure #}
62+
{% if item.image is not none %}
63+
<link rel="enclosure" href="{{ item.image[0] }}" type="{{ item.image[1] }}" length="{{ item.image[2] }}"/>
64+
{% endif %}
65+
</entry>
66+
{% endfor %}
67+
</feed>

mkdocs_rss_plugin/util.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,18 @@ def get_file_dates(
219219
tuple[datetime, datetime]: tuple of timestamps (creation date, last commit date)
220220
"""
221221
logger.debug(f"Extracting dates for {in_page.file.src_uri}")
222+
223+
# handle temporary pages (e.g. archive pages of blog plugin of Material for Mkdocs)
224+
if in_page.file.abs_src_path.startswith("/tmp"):
225+
logger.debug(
226+
f"Temporary page detected ({in_page.file.abs_src_path}). "
227+
"Falling back to build date for both creation and update dates."
228+
)
229+
return (
230+
get_build_datetime(),
231+
get_build_datetime(),
232+
)
233+
222234
# empty vars
223235
dt_created = dt_updated = None
224236
if meta_default_time is None:
@@ -619,6 +631,7 @@ def get_image(
619631
if img_local_cache_path := self.social_cards.get_social_card_cache_path_for_page(
620632
mkdocs_page=in_page
621633
):
634+
print("HA", img_local_cache_path, img_url)
622635
img_length = img_local_cache_path.stat().st_size
623636
img_type = guess_type(url=img_local_cache_path, strict=False)[0]
624637
elif img_local_build_path := self.social_cards.get_social_card_build_path_for_page(
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
site_name: Test RSS Plugin
2+
site_description: Test with Atom disabled
3+
site_url: https://guts.github.io/mkdocs-rss-plugin
4+
5+
plugins:
6+
- rss:
7+
atom_feed_enabled: false
8+
9+
theme:
10+
name: mkdocs
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
site_name: Test RSS Plugin with custom Atom filenames
2+
site_description: Test Atom with custom filenames
3+
site_url: https://guts.github.io/mkdocs-rss-plugin
4+
5+
plugins:
6+
- rss:
7+
feeds_filenames:
8+
atom_created: atom.xml
9+
atom_updated: atom-updated.xml
10+
11+
theme:
12+
name: mkdocs

0 commit comments

Comments
 (0)