Skip to content

Commit 755d773

Browse files
committed
add(output): implement atom output format in addition to RSS and JSON Feed
1 parent 8b37500 commit 755d773

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
@@ -110,7 +110,13 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig:
110110
return config
111111

112112
# Fail if any export option is enabled
113-
if not any([self.config.json_feed_enabled, self.config.rss_feed_enabled]):
113+
if not any(
114+
[
115+
self.config.atom_feed_enabled,
116+
self.config.json_feed_enabled,
117+
self.config.rss_feed_enabled,
118+
]
119+
):
114120
logger.error(
115121
"At least one export option has to be enabled. Plugin is disabled."
116122
)
@@ -237,18 +243,24 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig:
237243
# final feed url
238244
if base_feed.html_url:
239245
# concatenate both URLs
240-
self.feed_created.rss_url = (
241-
base_feed.html_url + self.config.feeds_filenames.rss_created
246+
self.feed_created.atom_url = (
247+
base_feed.html_url + self.config.feeds_filenames.atom_created
242248
)
243-
self.feed_updated.rss_url = (
244-
base_feed.html_url + self.config.feeds_filenames.rss_updated
249+
self.feed_updated.atom_url = (
250+
base_feed.html_url + self.config.feeds_filenames.atom_updated
245251
)
246252
self.feed_created.json_url = (
247253
base_feed.html_url + self.config.feeds_filenames.json_created
248254
)
249255
self.feed_updated.json_url = (
250256
base_feed.html_url + self.config.feeds_filenames.json_updated
251257
)
258+
self.feed_created.rss_url = (
259+
base_feed.html_url + self.config.feeds_filenames.rss_created
260+
)
261+
self.feed_updated.rss_url = (
262+
base_feed.html_url + self.config.feeds_filenames.rss_updated
263+
)
252264
else:
253265
logger.error(
254266
"The variable `site_url` is not set in the MkDocs "
@@ -257,7 +269,9 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig:
257269
)
258270
self.feed_created.rss_url = self.feed_updated.json_url = (
259271
self.feed_updated.rss_url
260-
) = self.feed_updated.json_url = None
272+
) = self.feed_updated.json_url = self.feed_created.atom_url = (
273+
self.feed_updated.atom_url
274+
) = None
261275

262276
# ending event
263277
return config
@@ -323,6 +337,11 @@ def on_page_content(
323337
else:
324338
page_url_comments = None
325339

340+
# Store full HTML content if needed for Atom feed
341+
page_content: str | None = None
342+
if self.config.abstract_chars_count == -1:
343+
page_content = html
344+
326345
# append to list to be filtered later
327346
self.pages_to_filter.append(
328347
PageInformation(
@@ -332,6 +351,7 @@ def on_page_content(
332351
in_page=page, categories_labels=self.config.categories
333352
),
334353
comments_url=page_url_comments,
354+
html_content=page_content,
335355
created=page_dates[0],
336356
description=self.util.get_description_or_abstract(
337357
in_page=page,
@@ -362,13 +382,13 @@ def on_post_build(self, config: config_options.Config) -> None:
362382
return
363383

364384
# pretty print or not
365-
pretty_print = self.config.pretty_print
385+
pretty_print: bool = self.config.pretty_print
366386

367387
# output filepaths
368-
out_feed_created = Path(config.site_dir).joinpath(
388+
out_rss_created = Path(config.site_dir).joinpath(
369389
self.config.feeds_filenames.rss_created
370390
)
371-
out_feed_updated = Path(config.site_dir).joinpath(
391+
out_rss_updated = Path(config.site_dir).joinpath(
372392
self.config.feeds_filenames.rss_updated
373393
)
374394
out_json_created = Path(config.site_dir).joinpath(
@@ -377,6 +397,12 @@ def on_post_build(self, config: config_options.Config) -> None:
377397
out_json_updated = Path(config.site_dir).joinpath(
378398
self.config.feeds_filenames.json_updated
379399
)
400+
out_atom_created: Path = Path(config.site_dir).joinpath(
401+
self.config.feeds_filenames.atom_created
402+
)
403+
out_atom_updated: Path = Path(config.site_dir).joinpath(
404+
self.config.feeds_filenames.atom_updated
405+
)
380406

381407
# created items
382408
self.feed_created.entries.extend(
@@ -435,7 +461,7 @@ def on_post_build(self, config: config_options.Config) -> None:
435461
page.pub_date = format_datetime(dt=page.created)
436462

437463
# write file
438-
with out_feed_created.open(mode="w", encoding="UTF8") as fifeed_created:
464+
with out_rss_created.open(mode="w", encoding="UTF8") as fifeed_created:
439465
if pretty_print:
440466
fifeed_created.write(template.render(feed=self.feed_created))
441467
else:
@@ -457,7 +483,7 @@ def on_post_build(self, config: config_options.Config) -> None:
457483
page.pub_date = format_datetime(dt=page.updated)
458484

459485
# write file
460-
with out_feed_updated.open(mode="w", encoding="UTF8") as fifeed_updated:
486+
with out_rss_updated.open(mode="w", encoding="UTF8") as fifeed_updated:
461487
if pretty_print:
462488
fifeed_updated.write(template.render(feed=self.feed_updated))
463489
else:
@@ -487,3 +513,88 @@ def on_post_build(self, config: config_options.Config) -> None:
487513
fp,
488514
indent=4 if self.config.pretty_print else None,
489515
)
516+
517+
# ATOM FEED
518+
if self.config.atom_feed_enabled:
519+
# Jinja environment depending on the pretty print option
520+
if pretty_print:
521+
env = Environment(
522+
autoescape=select_autoescape(["html", "xml"]),
523+
loader=FileSystemLoader(self.tpl_folder),
524+
)
525+
else:
526+
env = Environment(
527+
autoescape=select_autoescape(["html", "xml"]),
528+
loader=FileSystemLoader(self.tpl_folder),
529+
lstrip_blocks=True,
530+
trim_blocks=True,
531+
)
532+
533+
template = env.get_template("atom.xml.jinja2")
534+
535+
# -- Feed sorted by creation date
536+
logger.debug(
537+
"Fill creation dates and dump created feed into Atom template."
538+
)
539+
540+
# Format dates for Atom (ISO 8601)
541+
for page in self.feed_created.entries:
542+
page.atom_published = page.created.isoformat()
543+
page.atom_updated = page.updated.isoformat()
544+
545+
# Format feed buildDate for Atom (ISO 8601)
546+
build_date_atom: str = datetime.fromtimestamp(
547+
get_build_timestamp()
548+
).isoformat()
549+
550+
# Temporarily store the ISO format
551+
original_build_date: str = self.feed_created.buildDate
552+
self.feed_created.buildDate = build_date_atom
553+
554+
# write file
555+
with out_atom_created.open(mode="w", encoding="UTF8") as fiatom_created:
556+
if pretty_print:
557+
fiatom_created.write(template.render(feed=self.feed_created))
558+
else:
559+
prev_char = ""
560+
for char in template.render(feed=asdict(self.feed_created)):
561+
if char == "\n":
562+
# convert new lines to spaces to preserve sentence structure
563+
char = " "
564+
if char == " " and prev_char == " ":
565+
prev_char = char
566+
continue
567+
prev_char = char
568+
fiatom_created.write(char)
569+
570+
# Restore original buildDate
571+
self.feed_created.buildDate = original_build_date
572+
573+
# -- Feed sorted by update date
574+
logger.debug("Fill update dates and dump updated feed into Atom template.")
575+
576+
for page in self.feed_updated.entries:
577+
page.atom_published = page.created.isoformat()
578+
page.atom_updated = page.updated.isoformat()
579+
580+
original_build_date = self.feed_updated.buildDate
581+
self.feed_updated.buildDate = build_date_atom
582+
583+
# write file
584+
with out_atom_updated.open(mode="w", encoding="UTF8") as fiatom_updated:
585+
if pretty_print:
586+
fiatom_updated.write(template.render(feed=self.feed_updated))
587+
else:
588+
prev_char = ""
589+
for char in template.render(feed=asdict(self.feed_updated)):
590+
if char == "\n":
591+
# convert new lines to spaces to preserve sentence structure
592+
char = " "
593+
if char == " " and prev_char == " ":
594+
prev_char = char
595+
continue
596+
prev_char = char
597+
fiatom_updated.write(char)
598+
599+
# Restore original buildDate
600+
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)