diff --git a/plugins/smolnet.py b/plugins/smolnet.py index b757234..0608b7b 100644 --- a/plugins/smolnet.py +++ b/plugins/smolnet.py @@ -17,7 +17,9 @@ setting, and every setting has a default, so a site can switch it on with just SMOLNET_OUTPUT 'smolnet' directory under OUTPUT_PATH SMOLNET_SECTION_DIR 'section' subdirectory holding one index per category SMOLNET_LATEST 15 entries on the front page; 0 drops the section + SMOLNET_META [date, category] fields for the line under an article title SMOLNET_SECTION_ORDER {} category name -> sort key, else alphabetical + SMOLNET_MIN_SECTIONS 2 below this, the section index is dropped SMOLNET_LABELS {} override any heading, see LABELS SMOLNET_ASSETS None directories to copy; defaults to STATIC_PATHS SMOLNET_SIDECAR '.itsybitsy.toml' @@ -67,6 +69,13 @@ META_CONT = re.compile(r"^\s+\S") # Pelican's internal link syntax. Left alone these reach the capsule verbatim. INTERNAL = re.compile(r"\{(filename|static|attach)\}([^)\s]+)") +# Links written against the HTML build, e.g. ](/go-west.html). Pelican's +# {filename} form is the portable one, but plenty of content predates knowing +# that and hardcodes the output URL instead. A capsule has no .html, and the +# daemon resolves a name carrying an extension literally, so these 404 unless +# they are rewritten. +HARDCODED = re.compile(r"\]\(\s*(/[^)\s]*?\.html)([^)]*)\)") + SUMMARY_MARKER = "" # Markdown image syntax, for dropping the ones whose host is gone. Matched on @@ -137,6 +146,26 @@ def rewrite_internal(text, by_source, warn): return INTERNAL.sub(sub, text) +def rewrite_html_links(text, slugs, warn): + """Point links written against the HTML build at the capsule instead. + + `/pages/glossary.html` and `/go-west.html` both become `/`, since the + capsule is flat. A target whose slug is not among the documents built is + reported and left alone rather than silently rewritten into a second broken + link. + """ + + def sub(match): + target, rest = match.group(1), match.group(2) + slug = os.path.basename(target)[: -len(".html")] + if slug in slugs: + return "](/%s%s)" % (slug, rest) + warn("smolnet: link to %s matches no document", target) + return match.group(0) + + return HARDCODED.sub(sub, text) + + def reveal_rot13(text, attr): """Replace a block of HTML carrying a rot13 address with the address itself. @@ -237,8 +266,10 @@ class CapsuleGenerator(Generator): # Map source basename -> slug, so {filename} can be resolved. Built from # everything published, pages included, since an article may link one. by_source = {} + slugs = set() for doc in articles + pages: by_source[os.path.basename(doc.source_path)] = doc.slug + slugs.add(doc.slug) os.makedirs(out, exist_ok=True) root_titles = {} @@ -247,6 +278,7 @@ class CapsuleGenerator(Generator): body = strip_metadata(self._read(doc)) body = body.replace(SUMMARY_MARKER, "") body = rewrite_internal(body, by_source, self._warn) + body = rewrite_html_links(body, slugs, self._warn) body = reveal_rot13(body, rot_attr) body = drop_dead_images(body, dead_hosts) # Blank lines, not newlines: a heading and the line under it are @@ -261,12 +293,21 @@ class CapsuleGenerator(Generator): fh.write(text) root_titles[name] = doc.title + # Which metadata goes under the title. Default is date and category, + # but a site that files by tag has no useful category and may well have + # a field that means more: zonetoast carries `era`, which is the point + # of the site in a way that Pelican's default `misc` is not. + meta_fields = s.get("SMOLNET_META", ["date", "category"]) for article in articles: bits = [] - if getattr(article, "date", None): - bits.append(article.date.strftime("%d %b %Y")) - if getattr(article, "category", None): - bits.append(str(article.category)) + for field in meta_fields: + value = getattr(article, field, None) + if not value: + continue + if field == "date": + bits.append(value.strftime("%d %b %Y")) + else: + bits.append(str(value)) emit(article, " ยท ".join(bits)) for page in pages: emit(page, "") @@ -276,6 +317,12 @@ class CapsuleGenerator(Generator): self.context.get("categories", []), key=lambda pair: (order.get(str(pair[0]), 99), str(pair[0])), ) + # A site that files by tag rather than category still has one category, + # Pelican's DEFAULT_CATEGORY, holding everything. Offering "misc, 14" as + # the only way in is worse than offering nothing, so one bucket is not a + # taxonomy and the whole section index goes. + if len(sections) < s.get("SMOLNET_MIN_SECTIONS", 2): + sections = [] if sections: os.makedirs(os.path.join(out, section_dir), exist_ok=True) section_titles = {}