API reference¶
mkdocs_changelog_feed_plugin ¶
changelog_feed ¶
MkDocs changelog feed plugin and config schema.
ChangelogFeedPlugin ¶
Bases: BasePlugin[ChangelogFeedPluginConfig]
MkDocs plugin that adds RSS and Atom feeds for a changelog in the Keep a Changelog format.
Source code in mkdocs_changelog_feed_plugin/changelog_feed.py
class ChangelogFeedPlugin(BasePlugin[ChangelogFeedPluginConfig]):
"""MkDocs plugin that adds RSS and Atom feeds for a changelog in the Keep a
Changelog format.
"""
def on_config(self, config: MkDocsConfig):
"""Make sure that the `site_url` setting, which is required to build absolute
URLs, is present.
See also <https://www.mkdocs.org/dev-guide/plugins/#on_config>.
"""
if not config.site_url:
raise PluginError(
'The site_url setting is required by the changelog feed plugin.'
)
if not config.site_author:
log.warning(
'The site_author setting is required to create a valid Atom feed.'
)
def on_page_content(
self, html: str, page: Page, config: MkDocsConfig, files: Files
):
"""If `page` is the changelog as specified by the `changelog_file`, this method
splits the HTML it into sections for each changelog entry and generates RSS and
Atom feeds with items for each of those entries. Then, links to the feeds are
added at the top of the page.
See also <https://www.mkdocs.org/dev-guide/plugins/#on_page_content>.
"""
if page.file.src_uri != self.config.changelog_file:
return
feed_title = self.config.feed_title or f'{page.title} - {config.site_name}'
feeds = [
feed_class(
feed_title,
page.canonical_url,
self.config.feed_description,
author_name=config.site_author,
feed_copyright=config.copyright,
)
for feed_class in feed_classes
]
soup = BeautifulSoup(html, features='html.parser')
if self.config.remove_permalinks:
try:
permalink_class = config['mdx_configs']['toc']['permalink_class']
except KeyError:
permalink_class = 'headerlink'
for permalink in soup.select(f'a.{permalink_class}'):
permalink.decompose()
for h2 in soup.find_all('h2'):
try:
fragment = h2['id']
except KeyError:
fragment = None
try:
heading_version, heading_date = (
s.strip() for s in re.split(r'\s[-‒––]\s', h2.text, maxsplit=2)
)
except ValueError:
heading_version, heading_date = h2.text, None
if heading_date:
try:
day = date.fromisoformat(heading_date[:10])
except ValueError as e:
raise PluginError(
f'Unable to parse date "{heading_date}" in section heading.'
) from e
pubdate = datetime.combine(day, time(12, 00), tzinfo=timezone.utc)
else:
pubdate = datetime.fromtimestamp(
Path(page.file.abs_src_path).stat().st_mtime, tz=timezone.utc
)
section_content = ''.join(
str(s) for s in takewhile(lambda s: s.name != 'h2', h2.next_siblings)
).strip()
for feed in feeds:
link = (
f'{page.canonical_url}#{fragment}'
if fragment
else page.canonical_url
)
feed.add_item(
heading_version,
link,
section_content,
pubdate=pubdate,
unique_id=link,
unique_id_is_permalink=True,
)
link_tags = []
a_tags = []
for feed in feeds:
feed_file = File.generated(
config,
Path(page.file.src_uri).with_suffix(feed.file_suffix),
content=feed.writeString('utf-8'),
)
files.append(feed_file)
feed_url = feed_file.url_relative_to(page.file)
link_tags.append(
soup.new_tag(
'link',
attrs={
'rel': 'alternate',
'type': feed.content_type.split(';')[0],
'href': feed_url,
'title': f'{page.title} {feed.verbose_name}',
},
)
)
a_tags.append(
soup.new_tag('a', attrs={'href': feed_url}, string=feed.verbose_name)
)
page.meta['changelog_feed'] = {'link_tags': link_tags}
div = soup.new_tag('div', attrs={'style': 'float: inline-end'})
div.append(BeautifulSoup(self.config.links_icon, 'html.parser'))
for a_tag in a_tags:
div.append(' ')
div.append(a_tag)
html = '\n'.join([div.decode(formatter=formatter), html])
return html
def on_post_page(self, output: str, page: Page, config: MkDocsConfig):
"""Add `<link rel="alternate" …>` tags to the head of the HTML document for
each generated feed, if stored in the [`Page`][mkdocs.structure.pages.Page]'s
metadata by
[`on_page_content`][mkdocs_changelog_feed_plugin.changelog_feed.ChangelogFeedPlugin.on_page_content].
See also <https://www.mkdocs.org/dev-guide/plugins/#on_post_page>.
"""
try:
link_tags = page.meta['changelog_feed']['link_tags']
except KeyError:
return
# Split at the beginning of the line with with the closing head tag.
parts = re.split(r'^(\s*</head>)', output, maxsplit=1, flags=re.M)
output = ''.join(
[
parts[0],
*[tag.decode(indent_level=2, formatter=formatter) for tag in link_tags],
*parts[1:],
]
)
return output
on_config ¶
on_config(config: MkDocsConfig)
Make sure that the site_url setting, which is required to build absolute
URLs, is present.
See also https://www.mkdocs.org/dev-guide/plugins/#on_config.
Source code in mkdocs_changelog_feed_plugin/changelog_feed.py
def on_config(self, config: MkDocsConfig):
"""Make sure that the `site_url` setting, which is required to build absolute
URLs, is present.
See also <https://www.mkdocs.org/dev-guide/plugins/#on_config>.
"""
if not config.site_url:
raise PluginError(
'The site_url setting is required by the changelog feed plugin.'
)
if not config.site_author:
log.warning(
'The site_author setting is required to create a valid Atom feed.'
)
on_page_content ¶
on_page_content(html: str, page: Page, config: MkDocsConfig, files: Files)
If page is the changelog as specified by the changelog_file, this method
splits the HTML it into sections for each changelog entry and generates RSS and
Atom feeds with items for each of those entries. Then, links to the feeds are
added at the top of the page.
See also https://www.mkdocs.org/dev-guide/plugins/#on_page_content.
Source code in mkdocs_changelog_feed_plugin/changelog_feed.py
def on_page_content(
self, html: str, page: Page, config: MkDocsConfig, files: Files
):
"""If `page` is the changelog as specified by the `changelog_file`, this method
splits the HTML it into sections for each changelog entry and generates RSS and
Atom feeds with items for each of those entries. Then, links to the feeds are
added at the top of the page.
See also <https://www.mkdocs.org/dev-guide/plugins/#on_page_content>.
"""
if page.file.src_uri != self.config.changelog_file:
return
feed_title = self.config.feed_title or f'{page.title} - {config.site_name}'
feeds = [
feed_class(
feed_title,
page.canonical_url,
self.config.feed_description,
author_name=config.site_author,
feed_copyright=config.copyright,
)
for feed_class in feed_classes
]
soup = BeautifulSoup(html, features='html.parser')
if self.config.remove_permalinks:
try:
permalink_class = config['mdx_configs']['toc']['permalink_class']
except KeyError:
permalink_class = 'headerlink'
for permalink in soup.select(f'a.{permalink_class}'):
permalink.decompose()
for h2 in soup.find_all('h2'):
try:
fragment = h2['id']
except KeyError:
fragment = None
try:
heading_version, heading_date = (
s.strip() for s in re.split(r'\s[-‒––]\s', h2.text, maxsplit=2)
)
except ValueError:
heading_version, heading_date = h2.text, None
if heading_date:
try:
day = date.fromisoformat(heading_date[:10])
except ValueError as e:
raise PluginError(
f'Unable to parse date "{heading_date}" in section heading.'
) from e
pubdate = datetime.combine(day, time(12, 00), tzinfo=timezone.utc)
else:
pubdate = datetime.fromtimestamp(
Path(page.file.abs_src_path).stat().st_mtime, tz=timezone.utc
)
section_content = ''.join(
str(s) for s in takewhile(lambda s: s.name != 'h2', h2.next_siblings)
).strip()
for feed in feeds:
link = (
f'{page.canonical_url}#{fragment}'
if fragment
else page.canonical_url
)
feed.add_item(
heading_version,
link,
section_content,
pubdate=pubdate,
unique_id=link,
unique_id_is_permalink=True,
)
link_tags = []
a_tags = []
for feed in feeds:
feed_file = File.generated(
config,
Path(page.file.src_uri).with_suffix(feed.file_suffix),
content=feed.writeString('utf-8'),
)
files.append(feed_file)
feed_url = feed_file.url_relative_to(page.file)
link_tags.append(
soup.new_tag(
'link',
attrs={
'rel': 'alternate',
'type': feed.content_type.split(';')[0],
'href': feed_url,
'title': f'{page.title} {feed.verbose_name}',
},
)
)
a_tags.append(
soup.new_tag('a', attrs={'href': feed_url}, string=feed.verbose_name)
)
page.meta['changelog_feed'] = {'link_tags': link_tags}
div = soup.new_tag('div', attrs={'style': 'float: inline-end'})
div.append(BeautifulSoup(self.config.links_icon, 'html.parser'))
for a_tag in a_tags:
div.append(' ')
div.append(a_tag)
html = '\n'.join([div.decode(formatter=formatter), html])
return html
on_post_page ¶
on_post_page(output: str, page: Page, config: MkDocsConfig)
Add <link rel="alternate" …> tags to the head of the HTML document for
each generated feed, if stored in the Page’s
metadata by
on_page_content.
See also https://www.mkdocs.org/dev-guide/plugins/#on_post_page.
Source code in mkdocs_changelog_feed_plugin/changelog_feed.py
def on_post_page(self, output: str, page: Page, config: MkDocsConfig):
"""Add `<link rel="alternate" …>` tags to the head of the HTML document for
each generated feed, if stored in the [`Page`][mkdocs.structure.pages.Page]'s
metadata by
[`on_page_content`][mkdocs_changelog_feed_plugin.changelog_feed.ChangelogFeedPlugin.on_page_content].
See also <https://www.mkdocs.org/dev-guide/plugins/#on_post_page>.
"""
try:
link_tags = page.meta['changelog_feed']['link_tags']
except KeyError:
return
# Split at the beginning of the line with with the closing head tag.
parts = re.split(r'^(\s*</head>)', output, maxsplit=1, flags=re.M)
output = ''.join(
[
parts[0],
*[tag.decode(indent_level=2, formatter=formatter) for tag in link_tags],
*parts[1:],
]
)
return output
ChangelogFeedPluginConfig ¶
Bases: Config
Plugin config schema.
See also https://www.mkdocs.org/dev-guide/plugins/#config_scheme.
Source code in mkdocs_changelog_feed_plugin/changelog_feed.py
class ChangelogFeedPluginConfig(Config):
"""Plugin config schema.
See also <https://www.mkdocs.org/dev-guide/plugins/#config_scheme>.
"""
changelog_file = config_options.Type(str, default='CHANGELOG.md')
"""The file within your `docs` directory containing the changelog."""
feed_title = config_options.Optional(config_options.Type(str))
"""The feed's title.
Defaults to "*page name* - *site name*", if not set.
"""
feed_description = config_options.Optional(config_options.Type(str))
"""The feed's description (RSS)/subtitle (Atom)."""
remove_permalinks = config_options.Type(bool, default=True)
"""Remove permalinks added by Python-Markdown's Table of Contents extension from
the feeds.
"""
links_icon = config_options.Type(str, default='<i class="fa fa-square-rss"></i>')
"""Icon to be displayed next to the links to the feeds at the top of the page."""
changelog_file
class-attribute
instance-attribute
¶
changelog_file = Type(str, default='CHANGELOG.md')
The file within your docs directory containing the changelog.
feed_description
class-attribute
instance-attribute
¶
feed_description = Optional(Type(str))
The feed’s description (RSS)/subtitle (Atom).
feed_title
class-attribute
instance-attribute
¶
feed_title = Optional(Type(str))
The feed’s title.
Defaults to “page name - site name”, if not set.
feeds ¶
Subclasses of FeedGenerator’s Atom1Feed and Rss201rev2Feed classes with some adjustments.
AtomFeed ¶
Bases: Atom1Feed
An Atom feed.
Source code in mkdocs_changelog_feed_plugin/feeds.py
class AtomFeed(feedgenerator.Atom1Feed):
"""An Atom feed."""
verbose_name = 'Atom feed'
file_suffix = '.atom.xml'
def add_item(
self,
title,
link,
description,
author_email=None,
author_name=None,
author_link=None,
pubdate=None,
comments=None,
unique_id=None,
unique_id_is_permalink=None,
categories=(),
item_copyright=None,
ttl=None,
content=None,
updateddate=None,
enclosures=None,
**kwargs,
):
"""Add an item to the feed.
Ensure that items with full text `description` are correctly added to the feed.
"""
# Workaround for feedgenerator's “backwards-compatibility not in Django”
description, content = None, description
super().add_item(
title,
link,
description,
author_email=author_email,
author_name=author_name,
author_link=author_link,
pubdate=pubdate,
comments=comments,
unique_id=unique_id,
unique_id_is_permalink=unique_id_is_permalink,
categories=categories,
item_copyright=item_copyright,
ttl=ttl,
content=content,
updateddate=updateddate,
enclosures=enclosures,
**kwargs,
)
add_item ¶
add_item(title, link, description, author_email=None, author_name=None, author_link=None, pubdate=None, comments=None, unique_id=None, unique_id_is_permalink=None, categories=(), item_copyright=None, ttl=None, content=None, updateddate=None, enclosures=None, **kwargs)
Add an item to the feed.
Ensure that items with full text description are correctly added to the feed.
Source code in mkdocs_changelog_feed_plugin/feeds.py
def add_item(
self,
title,
link,
description,
author_email=None,
author_name=None,
author_link=None,
pubdate=None,
comments=None,
unique_id=None,
unique_id_is_permalink=None,
categories=(),
item_copyright=None,
ttl=None,
content=None,
updateddate=None,
enclosures=None,
**kwargs,
):
"""Add an item to the feed.
Ensure that items with full text `description` are correctly added to the feed.
"""
# Workaround for feedgenerator's “backwards-compatibility not in Django”
description, content = None, description
super().add_item(
title,
link,
description,
author_email=author_email,
author_name=author_name,
author_link=author_link,
pubdate=pubdate,
comments=comments,
unique_id=unique_id,
unique_id_is_permalink=unique_id_is_permalink,
categories=categories,
item_copyright=item_copyright,
ttl=ttl,
content=content,
updateddate=updateddate,
enclosures=enclosures,
**kwargs,
)
RSSFeed ¶
Bases: Rss201rev2Feed
An RSS feed.
Source code in mkdocs_changelog_feed_plugin/feeds.py
class RSSFeed(feedgenerator.Rss201rev2Feed):
"""An RSS feed."""
verbose_name = 'RSS feed'
file_suffix = '.rss.xml'
def add_item(
self,
title,
link,
description,
author_email=None,
author_name=None,
author_link=None,
pubdate=None,
comments=None,
unique_id=None,
unique_id_is_permalink=None,
categories=(),
item_copyright=None,
ttl=None,
content=None,
updateddate=None,
enclosures=None,
**kwargs,
):
"""Add an item to the feed.
If not provided as arguments, the author information is copied from the feed
object to the item, as (unlike Atom) RSS doesn't support author information for
the whole feed.
"""
if not author_email and not author_name and not author_link:
author_email = self.feed['author_email']
author_name = self.feed['author_name']
author_link = self.feed['author_link']
super().add_item(
title,
link,
description,
author_email=author_email,
author_name=author_name,
author_link=author_link,
pubdate=pubdate,
comments=comments,
unique_id=unique_id,
unique_id_is_permalink=unique_id_is_permalink,
categories=categories,
item_copyright=item_copyright,
ttl=ttl,
content=content,
updateddate=updateddate,
enclosures=enclosures,
**kwargs,
)
add_item ¶
add_item(title, link, description, author_email=None, author_name=None, author_link=None, pubdate=None, comments=None, unique_id=None, unique_id_is_permalink=None, categories=(), item_copyright=None, ttl=None, content=None, updateddate=None, enclosures=None, **kwargs)
Add an item to the feed.
If not provided as arguments, the author information is copied from the feed object to the item, as (unlike Atom) RSS doesn’t support author information for the whole feed.
Source code in mkdocs_changelog_feed_plugin/feeds.py
def add_item(
self,
title,
link,
description,
author_email=None,
author_name=None,
author_link=None,
pubdate=None,
comments=None,
unique_id=None,
unique_id_is_permalink=None,
categories=(),
item_copyright=None,
ttl=None,
content=None,
updateddate=None,
enclosures=None,
**kwargs,
):
"""Add an item to the feed.
If not provided as arguments, the author information is copied from the feed
object to the item, as (unlike Atom) RSS doesn't support author information for
the whole feed.
"""
if not author_email and not author_name and not author_link:
author_email = self.feed['author_email']
author_name = self.feed['author_name']
author_link = self.feed['author_link']
super().add_item(
title,
link,
description,
author_email=author_email,
author_name=author_name,
author_link=author_link,
pubdate=pubdate,
comments=comments,
unique_id=unique_id,
unique_id_is_permalink=unique_id_is_permalink,
categories=categories,
item_copyright=item_copyright,
ttl=ttl,
content=content,
updateddate=updateddate,
enclosures=enclosures,
**kwargs,
)