#!/usr/bin/env python3 import markdown import sys import io import re from pygments.formatters import HtmlFormatter from markdown.extensions.toc import TocExtension from markdown.extensions import Extension from markdown.preprocessors import Preprocessor # Python-Markdown (unlike CommonMark/GitHub) refuses to let a list interrupt a # paragraph unless there's a blank line between them, so a very common README # shape renders as one run-on paragraph instead of a list: # # Passing: # - foo # - bar # # This preprocessor restores the GitHub behavior: when a bullet ( - * + ) or a # "1."/"1)" ordered-list marker immediately follows a paragraph line, insert a # blank line so the list starts. We deliberately only interrupt on "1" for # ordered lists (matching CommonMark) so prose like "... the year\n1985. was # ..." is not turned into a list. Fenced code, indented code, and lines that # already follow a list item are left untouched. _MD_FENCE = re.compile(r'^(\s{0,3})(`{3,}|~{3,})') _MD_BULLET = re.compile(r'^ {0,3}[-*+][ \t]+\S') _MD_ORDERED1 = re.compile(r'^ {0,3}1[.)][ \t]+\S') _MD_ANYITEM = re.compile(r'^ {0,3}(?:[-*+]|\d{1,9}[.)])[ \t]') class _ListInterruptPreprocessor(Preprocessor): def run(self, lines): out, in_fence, fence = [], False, None for line in lines: m = _MD_FENCE.match(line) if m: tok = m.group(2)[0] if not in_fence: in_fence, fence = True, tok elif fence == tok: in_fence, fence = False, None out.append(line) continue if not in_fence and out: prev = out[-1] if (prev.strip() and not _MD_ANYITEM.match(prev) and not prev.startswith((' ', '\t')) and (_MD_BULLET.match(line) or _MD_ORDERED1.match(line))): out.append('') out.append(line) return out class ListInterruptExtension(Extension): def extendMarkdown(self, md): # priority 21: just above the built-in 'normalize_whitespace' (20) so we # operate on clean lines before block parsing. md.preprocessors.register( _ListInterruptPreprocessor(md), 'list_interrupt', 21) sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8') sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') sys.stdout.write(''' ''') # Render to a string (not straight to stdout) so we can post-process it. # Note: you may want to run this through bleach for sanitization html = markdown.markdown( sys.stdin.read(), output_format="html5", extensions=[ "markdown.extensions.fenced_code", "markdown.extensions.codehilite", "markdown.extensions.tables", "markdown.extensions.sane_lists", ListInterruptExtension(), TocExtension(anchorlink=True)], extension_configs={ "markdown.extensions.codehilite":{"css_class":"highlight"}}) # A README's relative paths point at repo content, but a browser resolves them # against the page the readme is shown on (//about/ or the summary page # //), not against the repo root — so they 404. cgit serves blobs only via # its `plain`/`tree` commands, so rewrite relative references to absolute cgit # URLs (absolute so it works both on the about page and the summary page): # -> //plain/?h= (raw bytes for the browser) # -> //tree/?h= (browsable file/dir view) # Relative hrefs are resolved as paths against the repo root; a href that climbs # out of the repo with ".." (e.g. ../sibling) is resolved against the repo's URL # location instead (-> /sibling), matching how sibling repos are addressed. # cgit exports the repo context to about-filters via these environment vars; if # they're absent we leave the references untouched. import os, re from urllib.parse import urljoin repo = os.environ.get("CGIT_REPO_URL", "").strip("/") if repo: branch = os.environ.get("CGIT_REPO_DEFBRANCH", "").strip() repo_root = "/" + repo + "/" plain = repo_root + "plain/" query = ("?h=" + branch) if branch else "" # leave absolute, protocol-relative, root-relative, anchor, query-only and # scheme (mailto:, http:, data:, ...) URLs alone skip = re.compile(r"^(?:[a-z][a-z0-9+.\-]*:|//|/|[#?])", re.I) def rewrite_img(m): url = m.group(2) if not url or skip.match(url): return m.group(0) return m.group(1) + plain + re.sub(r"^\./", "", url) + query + m.group(3) def _split_frag(url): # split off a trailing #fragment (kept verbatim on the rewritten URL) if "#" in url: path, frag = url.split("#", 1) return path, "#" + frag return url, "" def _normalize(path): # resolve "." / ".." against the repo root; returns (climbs, cleaned) # where `climbs` counts ".." segments that escaped above the repo root. out, climbs = [], 0 for seg in path.split("/"): if seg in ("", "."): continue if seg == "..": if out: out.pop() else: climbs += 1 else: out.append(seg) return climbs, "/".join(out) def rewrite_href(m): url = m.group(2) if not url or skip.match(url): return m.group(0) path, frag = _split_frag(url) climbs, cleaned = _normalize(path) if climbs: # escapes the repo (sibling repo etc.): resolve against repo location dest = urljoin(repo_root, "../" * climbs + cleaned) elif cleaned: dest = repo_root + "tree/" + cleaned + query else: dest = repo_root + (query or "") return m.group(1) + dest + frag + m.group(3) html = re.sub(r'(]*?\bsrc=")([^"]*)(")', rewrite_img, html, flags=re.I) html = re.sub(r'(]*?\bhref=")([^"]*)(")', rewrite_href, html, flags=re.I) sys.stdout.write("") sys.stdout.write(html) sys.stdout.write("")