diff --git a/Makefile b/Makefile index 7ef7a2cd..eae81018 100644 --- a/Makefile +++ b/Makefile @@ -227,6 +227,11 @@ rebuild-nodes: $(addprefix levels/,$(addsuffix .wad,$(NODE_BUILDER_LEVELS))) do \ $(NODE_BUILDER) $$level -o $$level; \ done + +# Update feed.mxl (RSS feed) on the website based on NEWS.adoc. This assumes +# that the website has the same parent directory as this build. +news-to-feed: NEWS.adoc + scripts/news-to-feed NEWS.adoc ../freedoom.github.io/feed.xml %.6: $(MAKE) ASCIIDOC_MAN="$(ASCIIDOC_MAN)" -C dist $@ diff --git a/scripts/news-to-feed b/scripts/news-to-feed new file mode 100755 index 00000000..50ba35fb --- /dev/null +++ b/scripts/news-to-feed @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# +# new-to-feed - Convert NEWS.adoc to feed.xml +# +# This script converts from "NEWS.adoc" in the main Freedoom repository to +# "feed.xml" on the Freedoom website, which is an RSS feed. +# +# It's possible that there is no need for this script given that GitHub +# generates an Atom feed based on the releases done on GitHub. It can be +# found at: +# https://github.com/freedoom/freedoom/releases.atom +# Perhaps the website should link to the above in place of the current +# "atom.xml", and "feed.xml" should be abandon. + +import datetime +import os +import re +import sys +from xml.dom.minidom import Document + +# Globals + +channel = None +leading_whitespace = " " +date = None +desc_lines = [] +doc = None +pubdate = None +ver = None + +# Regular expressions + +# A regular expression that matches version / date lines. +ver_date_patt = re.compile(r'\s*==\s+((?:\d+\.)+\d+)\s+\((\d{4}-\d{2}-\d{2})\)') + +# Initial document processing and header prior to the first version. +def add_header(): + global channel + global doc + + doc = Document() + + rss = doc.createElement('rss') + rss.setAttribute('version', '2.0') + doc.appendChild(rss) + + channel = doc.createElement('channel') + rss.appendChild(channel) + + title = doc.createElement('title') + title.appendChild(doc.createTextNode('Freedoom Feed')) + channel.appendChild(title) + + link = doc.createElement('link') + link.appendChild(doc.createTextNode('https://freedoom.github.io')) + channel.appendChild(link) + + description = doc.createElement('description') + description.appendChild(doc.createTextNode('The latest news from Freedoom')) + channel.appendChild(description) + + language = doc.createElement('language') + language.appendChild(doc.createTextNode('en-us')) + channel.appendChild(language) + + image = doc.createElement('image') + channel.appendChild(image) + + url = doc.createElement('url') + url.appendChild(doc.createTextNode('https://freedoom.github.io/favicon.ico')) + image.appendChild(url) + + +# Called each time we have a complete version section +def add_version(): + item = doc.createElement('item') + channel.appendChild(item) + + title_elem = doc.createElement('title') + title_elem.appendChild(doc.createTextNode(date + ": Freedoom " + ver + " released")) + item.appendChild(title_elem) + + pubdate_elem = doc.createElement('pubDate') + pubdate_elem.appendChild(doc.createTextNode(pubdate)) + item.appendChild(pubdate_elem) + + link_elem = doc.createElement('link') + link_elem.appendChild(doc.createTextNode("https://freedoom.github.io/#freedoom-" + ver)) + item.appendChild(link_elem) + + # Avoid join() in order to treat newlines specially. + nl_ws = "\n" + leading_whitespace + desc_text = "\n" + for desc_line in desc_lines: + if desc_line: + desc_text += leading_whitespace + desc_line + "\n" + leading_whitespace + "

\n" + else: + desc_text += "\n" + desc_elem = doc.createElement('description') + desc_elem.appendChild(doc.createTextNode(desc_text)) + item.appendChild(desc_elem) + + +# Iterates through news_adoc in order to process each version found. +def add_versions(): + global date + global desc_lines + global pubdate + global ver + + first_section = True + desc_line = "" + with open(news_adoc) as news_hand: + line_num = 0 + for line in news_hand: + line_num += 1 + line = line.rstrip() + ver_date_match = ver_date_patt.match(line) + if ver_date_match: + ver_date_match_groups = ver_date_match.groups() + if len(ver_date_match_groups) != 2: + # Should not happen. + print("Line number", line_num, "has", len(ver_date_match_groups), + "groups.", file=sys.stderr) + sys.exit(1) + # With the exception of the "HEAD" each new version line indicates + # that the previous one is complete, so output it before setting + # "pubdate" to the new value. + if pubdate: + add_version() + desc_lines = [] + ver, date = ver_date_match.groups() + pubdate = datetime.datetime.strptime(date, '%Y-%m-%d').strftime('%d %b %Y') + first_section = True + else: + # Possible description text, but only after the first version date line. + if not pubdate: + continue + line = line.strip() + if line.startswith("=== "): + # Add the existing line, if any. + if desc_line: + desc_lines.append(desc_line) + desc_line = "" + if first_section: + first_section = False + else: + desc_lines.append("") + desc_lines.append("> " + line[4:]) + first_section = False + elif line.startswith("*"): + if desc_line: + desc_lines.append(desc_line) + desc_line = "" + try: + space_index = line.index(" ") + except ValueError: + # Should not happen. + print("Line number", line_num, "begins with \"*\", but has no space.") + sys.exit(1) + desc_line = ("- " * (space_index - 1) if (space_index > 1) \ + else "") + line[space_index + 1:] + elif line: + # Assume it's a continuation of the previous "*" line. + desc_line += (" " + line) + else: + # Blank line marking the end of a bullet. + if desc_line: + desc_lines.append(desc_line) + desc_line = "" + if not pubdate: + print("No pubdate at the end.", file=sys.stderr) + sys.exit(1) + if desc_line: + desc_lines.append(desc_line) + desc_line = "" + add_version() + + +# Main enty point. +def main(): + parse_args() + add_header() + add_versions() + output_feed() + + +# Write the feed to feed_xml. +def output_feed(): + with open(feed_xml, "w") as feed_hand: + print(doc.toprettyxml(indent=" "), file=feed_hand) + + +# Parse the command line arguments and store the result in 'args'. +def parse_args(): + global news_adoc + global feed_xml + + if len(sys.argv) != 3: + print("Usage:", os.path.basename(sys.argv[0]), \ + "news-file feed-xml", file=sys.stderr) + sys.exit(1) + news_adoc = sys.argv[1] + feed_xml = sys.argv[2] + + +# So that this script may be accessed as a module. +if __name__ == "__main__": + main()