Skip to content

Commit 517556b

Browse files
committed
add(output): implement atom output format in addition to RSS and JSON Feed
1 parent 6d3fc84 commit 517556b

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
@@ -63,6 +63,7 @@ class PageInformation:
6363
created: datetime | None = None
6464
description: str | None = None
6565
guid: str | None = None
66+
html_content: str | None = None
6667
image: tuple[str, str, int] | None = None
6768
link: str | None = None
6869
pub_date: str | None = None
@@ -78,6 +79,7 @@ class PageInformation:
7879
class RssFeedBase:
7980
"""Object describing a feed."""
8081

82+
atom_url: str | None = None
8183
author: str | None = None
8284
buildDate: str | None = None
8385
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
)
@@ -254,18 +260,24 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig:
254260
# final feed url
255261
if base_feed.html_url:
256262
# concatenate both URLs
257-
self.feed_created.rss_url = (
258-
base_feed.html_url + self.config.feeds_filenames.rss_created
263+
self.feed_created.atom_url = (
264+
base_feed.html_url + self.config.feeds_filenames.atom_created
259265
)
260-
self.feed_updated.rss_url = (
261-
base_feed.html_url + self.config.feeds_filenames.rss_updated
266+
self.feed_updated.atom_url = (
267+
base_feed.html_url + self.config.feeds_filenames.atom_updated
262268
)
263269
self.feed_created.json_url = (
264270
base_feed.html_url + self.config.feeds_filenames.json_created
265271
)
266272
self.feed_updated.json_url = (
267273
base_feed.html_url + self.config.feeds_filenames.json_updated
268274
)
275+
self.feed_created.rss_url = (
276+
base_feed.html_url + self.config.feeds_filenames.rss_created
277+
)
278+
self.feed_updated.rss_url = (
279+
base_feed.html_url + self.config.feeds_filenames.rss_updated
280+
)
269281
else:
270282
logger.error(
271283
"The variable `site_url` is not set in the MkDocs "
@@ -274,7 +286,9 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig:
274286
)
275287
self.feed_created.rss_url = self.feed_updated.json_url = (
276288
self.feed_updated.rss_url
277-
) = self.feed_updated.json_url = None
289+
) = self.feed_updated.json_url = self.feed_created.atom_url = (
290+
self.feed_updated.atom_url
291+
) = None
278292

279293
# ending event
280294
return config
@@ -340,6 +354,11 @@ def on_page_content(
340354
else:
341355
page_url_comments = None
342356

357+
# Store full HTML content if needed for Atom feed
358+
page_content: str | None = None
359+
if self.config.abstract_chars_count == -1:
360+
page_content = html
361+
343362
# append to list to be filtered later
344363
self.pages_to_filter.append(
345364
PageInformation(
@@ -349,6 +368,7 @@ def on_page_content(
349368
in_page=page, categories_labels=self.config.categories
350369
),
351370
comments_url=page_url_comments,
371+
html_content=page_content,
352372
created=page_dates[0],
353373
description=self.util.get_description_or_abstract(
354374
in_page=page,
@@ -379,13 +399,13 @@ def on_post_build(self, config: config_options.Config) -> None:
379399
return
380400

381401
# pretty print or not
382-
pretty_print = self.config.pretty_print
402+
pretty_print: bool = self.config.pretty_print
383403

384404
# output filepaths
385-
out_feed_created = Path(config.site_dir).joinpath(
405+
out_rss_created = Path(config.site_dir).joinpath(
386406
self.config.feeds_filenames.rss_created
387407
)
388-
out_feed_updated = Path(config.site_dir).joinpath(
408+
out_rss_updated = Path(config.site_dir).joinpath(
389409
self.config.feeds_filenames.rss_updated
390410
)
391411
out_json_created = Path(config.site_dir).joinpath(
@@ -394,6 +414,12 @@ def on_post_build(self, config: config_options.Config) -> None:
394414
out_json_updated = Path(config.site_dir).joinpath(
395415
self.config.feeds_filenames.json_updated
396416
)
417+
out_atom_created: Path = Path(config.site_dir).joinpath(
418+
self.config.feeds_filenames.atom_created
419+
)
420+
out_atom_updated: Path = Path(config.site_dir).joinpath(
421+
self.config.feeds_filenames.atom_updated
422+
)
397423

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

460486
# write file
461-
with out_feed_created.open(mode="w", encoding="UTF8") as fifeed_created:
487+
with out_rss_created.open(mode="w", encoding="UTF8") as fifeed_created:
462488
if pretty_print:
463489
fifeed_created.write(template.render(feed=self.feed_created))
464490
else:
@@ -480,7 +506,7 @@ def on_post_build(self, config: config_options.Config) -> None:
480506
page.pub_date = format_datetime(dt=page.updated)
481507

482508
# write file
483-
with out_feed_updated.open(mode="w", encoding="UTF8") as fifeed_updated:
509+
with out_rss_updated.open(mode="w", encoding="UTF8") as fifeed_updated:
484510
if pretty_print:
485511
fifeed_updated.write(template.render(feed=self.feed_updated))
486512
else:
@@ -510,3 +536,88 @@ def on_post_build(self, config: config_options.Config) -> None:
510536
fp,
511537
indent=4 if self.config.pretty_print else None,
512538
)
539+
540+
# ATOM FEED
541+
if self.config.atom_feed_enabled:
542+
# Jinja environment depending on the pretty print option
543+
if pretty_print:
544+
env = Environment(
545+
autoescape=select_autoescape(["html", "xml"]),
546+
loader=FileSystemLoader(self.tpl_folder),
547+
)
548+
else:
549+
env = Environment(
550+
autoescape=select_autoescape(["html", "xml"]),
551+
loader=FileSystemLoader(self.tpl_folder),
552+
lstrip_blocks=True,
553+
trim_blocks=True,
554+
)
555+
556+
template = env.get_template("atom.xml.jinja2")
557+
558+
# -- Feed sorted by creation date
559+
logger.debug(
560+
"Fill creation dates and dump created feed into Atom template."
561+
)
562+
563+
# Format dates for Atom (ISO 8601)
564+
for page in self.feed_created.entries:
565+
page.atom_published = page.created.isoformat()
566+
page.atom_updated = page.updated.isoformat()
567+
568+
# Format feed buildDate for Atom (ISO 8601)
569+
build_date_atom: str = datetime.fromtimestamp(
570+
get_build_timestamp()
571+
).isoformat()
572+
573+
# Temporarily store the ISO format
574+
original_build_date: str = self.feed_created.buildDate
575+
self.feed_created.buildDate = build_date_atom
576+
577+
# write file
578+
with out_atom_created.open(mode="w", encoding="UTF8") as fiatom_created:
579+
if pretty_print:
580+
fiatom_created.write(template.render(feed=self.feed_created))
581+
else:
582+
prev_char = ""
583+
for char in template.render(feed=asdict(self.feed_created)):
584+
if char == "\n":
585+
# convert new lines to spaces to preserve sentence structure
586+
char = " "
587+
if char == " " and prev_char == " ":
588+
prev_char = char
589+
continue
590+
prev_char = char
591+
fiatom_created.write(char)
592+
593+
# Restore original buildDate
594+
self.feed_created.buildDate = original_build_date
595+
596+
# -- Feed sorted by update date
597+
logger.debug("Fill update dates and dump updated feed into Atom template.")
598+
599+
for page in self.feed_updated.entries:
600+
page.atom_published = page.created.isoformat()
601+
page.atom_updated = page.updated.isoformat()
602+
603+
original_build_date = self.feed_updated.buildDate
604+
self.feed_updated.buildDate = build_date_atom
605+
606+
# write file
607+
with out_atom_updated.open(mode="w", encoding="UTF8") as fiatom_updated:
608+
if pretty_print:
609+
fiatom_updated.write(template.render(feed=self.feed_updated))
610+
else:
611+
prev_char = ""
612+
for char in template.render(feed=asdict(self.feed_updated)):
613+
if char == "\n":
614+
# convert new lines to spaces to preserve sentence structure
615+
char = " "
616+
if char == " " and prev_char == " ":
617+
prev_char = char
618+
continue
619+
prev_char = char
620+
fiatom_updated.write(char)
621+
622+
# Restore original buildDate
623+
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
@@ -223,6 +223,18 @@ def get_file_dates(
223223
tuple[datetime, datetime]: tuple of timestamps (creation date, last commit date)
224224
"""
225225
logger.debug(f"Extracting dates for {in_page.file.src_uri}")
226+
227+
# handle temporary pages (e.g. archive pages of blog plugin of Material for Mkdocs)
228+
if in_page.file.abs_src_path.startswith("/tmp"):
229+
logger.debug(
230+
f"Temporary page detected ({in_page.file.abs_src_path}). "
231+
"Falling back to build date for both creation and update dates."
232+
)
233+
return (
234+
get_build_datetime(),
235+
get_build_datetime(),
236+
)
237+
226238
# empty vars
227239
dt_created = dt_updated = None
228240
if meta_default_time is None:
@@ -623,6 +635,7 @@ def get_image(
623635
if img_local_cache_path := self.social_cards.get_social_card_cache_path_for_page(
624636
mkdocs_page=in_page
625637
):
638+
print("HA", img_local_cache_path, img_url)
626639
img_length = img_local_cache_path.stat().st_size
627640
img_type = guess_type(url=img_local_cache_path, strict=False)[0]
628641
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)