/**
* remark-python-refs
*
* A remark plugin that detects Python ecosystem URLs in Markdown links
* and transforms them into styled inline badges.
*
* Detected patterns:
* - PEP links (by URL pattern OR link text matching "PEP NNN")
* - CPython docs (docs.python.org/...)
* - PyPI links (pypi.org/project/NAME/)
* - GitHub issues/PRs (github.com/OWNER/REPO/issues|pull/NNN)
* - GitHub repos (github.com/OWNER/REPO exactly 2 path segments)
* - GitHub users/orgs (github.com/NAME exactly 1 segment, not reserved)
* - CVE references (nvd.nist.gov/vuln/detail/CVE-YYYY-NNNNN)
* - Python releases (python.org/downloads/release/python-XXXX/)
*/
import type { Root, Link, Paragraph, PhrasingContent } from "mdast";
import { visit } from "unist-util-visit";
import {
pythonIcon,
docsIcon,
githubIcon,
packageIcon,
issueIcon,
shieldIcon,
downloadIcon,
externalIcon,
} from "../components/references/_icons.js";
// URL matchers
const PEP_OLD = /^https?:\/\/(?:www\.)?python\.org\/dev\/peps\/pep-(\d+)\/?/i;
const PEP_NEW = /^https?:\/\/peps\.python\.org\/pep-(\d+)\/?/i;
const DOCS = /^https?:\/\/docs\.python\.org\//i;
const PYPI = /^https?:\/\/pypi\.org\/project\/([^/]+)\/?/i;
const GH_ISSUE = /^https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/(issues|pull)\/(\d+)\/?/i;
const CVE = /^https?:\/\/nvd\.nist\.gov\/vuln\/detail\/(CVE-[\d-]+)\/?/i;
const PY_RELEASE = /^https?:\/\/(?:www\.)?python\.org\/downloads\/release\/(python-[\w.]+)\/?/i;
const GITHUB =
/^https?:\/\/github\.com\/([\w.-]+)(?:\/([\w.-]+))?\/?$/i;
/** GitHub top-level paths that are NOT user/org profiles */
const GH_RESERVED = new Set([
"about",
"collections",
"contact",
"customer-stories",
"enterprise",
"events",
"explore",
"features",
"issues",
"login",
"marketplace",
"new",
"nonprofit",
"notifications",
"open-source",
"organizations",
"orgs",
"pricing",
"pulls",
"readme",
"search",
"security",
"settings",
"site",
"sponsors",
"team",
"topics",
"trending",
]);
// Helpers
type RefType = "pep" | "docs" | "pypi" | "gh-repo" | "gh-user" | "gh-issue" | "cve" | "py-release";
interface Match {
type: RefType;
icon: string;
label: string; // author's original link text preserved
url: string;
}
function extractText(children: PhrasingContent[]): string {
return children
.map((c) => {
if (c.type === "text" || c.type === "inlineCode") return c.value;
if ("children" in c) return extractText(c.children as PhrasingContent[]);
return "";
})
.join("");
}
/** Match by link text catches "PEP 649", "PEP 649," etc. */
const PEP_TEXT = /^PEP\s+\d+$/i;
function classify(url: string, linkText?: string): Omit | null {
let m: RegExpMatchArray | null;
// URL-based PEP detection
if ((m = url.match(PEP_OLD)) || (m = url.match(PEP_NEW))) {
return { type: "pep", icon: pythonIcon };
}
// Text-based PEP detection catches links like [PEP 799](https://docs.python.org/...)
if (linkText && PEP_TEXT.test(linkText.trim())) {
return { type: "pep", icon: pythonIcon };
}
// CPython docs checked after PEP text so [PEP NNN](docs.python.org/...) stays a PEP badge
if (DOCS.test(url)) {
return { type: "docs", icon: docsIcon };
}
if ((m = url.match(PYPI))) {
return { type: "pypi", icon: packageIcon };
}
if ((m = url.match(GH_ISSUE))) {
return { type: "gh-issue", icon: issueIcon };
}
if ((m = url.match(CVE))) {
return { type: "cve", icon: shieldIcon };
}
if ((m = url.match(PY_RELEASE))) {
return { type: "py-release", icon: downloadIcon };
}
if ((m = url.match(GITHUB))) {
const [, owner, repo] = m;
if (repo) {
return { type: "gh-repo", icon: githubIcon };
}
if (!GH_RESERVED.has(owner.toLowerCase())) {
return { type: "gh-user", icon: githubIcon };
}
}
return null;
}
function buildBadgeHtml(match: Match): string {
const cls = `ref-badge ref-${match.type}`;
// Escape HTML in label
const safeLabel = match.label
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
return (
`` +
`${match.icon}` +
`${safeLabel}` +
`${externalIcon}` +
``
);
}
// Markdoc tag handling
// Keystatic serializes inline components as Markdoc tags:
// {% Pep number=649 /%} or {% Pep number=649 %}PEP 649{% /Pep %}
const MARKDOC_TAG =
/\{%\s*(Pep|Docs|PyPi|GhRepo|GhUser)\s+([\s\S]*?)\s*(?:\/%\}|%\}[\s\S]*?\{%\s*\/\1\s*%\})/g;
const ATTR_RE = /(\w+)=(?:"([^"]*?)"|(\S+))/g;
function parseAttrs(str: string): Record {
const attrs: Record = {};
let m: RegExpExecArray | null;
while ((m = ATTR_RE.exec(str)) !== null) {
attrs[m[1]] = m[2] ?? m[3];
}
ATTR_RE.lastIndex = 0;
return attrs;
}
function markdocTagToBadge(tag: string, attrs: Record): string | null {
switch (tag) {
case "Pep": {
const num = attrs.number;
if (!num) return null;
const padded = String(num).padStart(4, "0");
return buildBadgeHtml({
type: "pep",
icon: pythonIcon,
label: `PEP ${num}`,
url: `https://peps.python.org/pep-${padded}/`,
});
}
case "Docs": {
const path = attrs.path;
if (!path) return null;
const label = attrs.label ?? path.replace(/^\//, "").replace(/\.html$/, "").split("/").pop()!;
return buildBadgeHtml({
type: "docs",
icon: docsIcon,
label,
url: `https://docs.python.org/${path.replace(/^\//, "")}`,
});
}
case "PyPi": {
const name = attrs.name;
if (!name) return null;
return buildBadgeHtml({
type: "pypi",
icon: packageIcon,
label: name,
url: `https://pypi.org/project/${name}/`,
});
}
case "GhRepo": {
const repo = attrs.repo;
if (!repo) return null;
return buildBadgeHtml({
type: "gh-repo",
icon: githubIcon,
label: repo.split("/").pop()!,
url: `https://github.com/${repo}`,
});
}
case "GhUser": {
const name = attrs.name;
if (!name) return null;
return buildBadgeHtml({
type: "gh-user",
icon: githubIcon,
label: `@${name}`,
url: `https://github.com/${name}`,
});
}
default:
return null;
}
}
/** Split a text value on Markdoc tags, returning text + html nodes. */
function splitMarkdocTags(value: string): Array {
const parts: Array = [];
let lastIndex = 0;
MARKDOC_TAG.lastIndex = 0;
let m: RegExpExecArray | null;
while ((m = MARKDOC_TAG.exec(value)) !== null) {
if (m.index > lastIndex) {
parts.push({ type: "text", value: value.slice(lastIndex, m.index) });
}
const html = markdocTagToBadge(m[1], parseAttrs(m[2]));
if (html) {
parts.push({ type: "html", value: html });
} else {
parts.push({ type: "text", value: m[0] });
}
lastIndex = m.index + m[0].length;
}
if (lastIndex < value.length) {
parts.push({ type: "text", value: value.slice(lastIndex) });
}
return parts;
}
// Plugin
export default function remarkPythonRefs() {
return (tree: Root, file: any) => {
// Collect unique references for the post footer
const seen = new Set();
const refs: Array = [];
function collectRef(type: RefType, label: string, url: string) {
const key = `${type}:${url}`;
if (!seen.has(key)) {
seen.add(key);
refs.push({ type, label, url });
}
}
// Pass 1: Transform matching links badges
visit(tree, "link", (node: Link, index, parent) => {
if (index == null || !parent) return;
const rawLabel = extractText(node.children);
const info = classify(node.url, rawLabel);
if (!info) return;
// For autolinked bare URLs, drop the protocol, leading "www.",
// and trailing slash so the badge reads "python.org/downloads/..."
// rather than the full "https://www.python.org/.../" form.
const label = rawLabel.startsWith("http")
? rawLabel.replace(/^https?:\/\/(?:www\.)?/i, "").replace(/\/$/, "")
: rawLabel;
collectRef(info.type, label, node.url);
const html = buildBadgeHtml({ ...info, label, url: node.url });
// Replace the link node with a raw HTML node
(parent.children as any)[index] = {
type: "html",
value: html,
};
});
// Pass 2: Transform Markdoc inline tags (from Keystatic) badges
visit(tree, "paragraph", (node: Paragraph) => {
let changed = false;
const newChildren: PhrasingContent[] = [];
for (const child of node.children) {
if (child.type !== "text" || !MARKDOC_TAG.test(child.value)) {
MARKDOC_TAG.lastIndex = 0;
newChildren.push(child);
continue;
}
MARKDOC_TAG.lastIndex = 0;
changed = true;
const parts = splitMarkdocTags(child.value);
for (const part of parts) {
newChildren.push(part as any);
}
}
if (changed) {
node.children = newChildren;
}
});
// Expose collected references via remarkPluginFrontmatter
if (!file.data.astro) file.data.astro = {};
if (!file.data.astro.frontmatter) file.data.astro.frontmatter = {};
file.data.astro.frontmatter.references = refs;
};
}