Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 242 additions & 0 deletions pep_sphinx_extensions/pep_theme/static/pep_version_filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
/**
* Python version filter for PEP Index page
* Filters PEP tables by Python version using the JSON API
*/
(function () {
"use strict";

const STORAGE_KEY = "pep_version_filter";
const API_URL =
document.currentScript?.dataset.apiUrl || "/api/peps.json";
let pepVersionMap = {};
let allVersions = [];

/**
* Parse python_version string and extract individual versions
* Handles formats like "3.10", "2.4, 2.5, 2.6", "2.7, 3.0"
*/
function parseVersions(versionStr) {
if (!versionStr) return [];
return versionStr
.split(",")
.map((v) => v.trim())
.filter((v) => v);
}

/**
* Extract major.minor version from a version string
* "3.10.1" -> "3.10", "3.10" -> "3.10"
*/
function getMajorMinor(version) {
const match = version.match(/^(\d+\.\d+)/);
return match ? match[1] : version;
}

/**
* Sort versions in descending order (newest first)
* Handles both 2.x and 3.x versions
*/
function sortVersionsDescending(versions) {
return versions.sort((a, b) => {
const [aMajor, aMinor] = a.split(".").map(Number);
const [bMajor, bMinor] = b.split(".").map(Number);
if (bMajor !== aMajor) return bMajor - aMajor;
return bMinor - aMinor;
});
}

/**
* Fetch PEP data and build version map
*/
async function loadPepData() {
try {
const response = await fetch(API_URL);
if (!response.ok) throw new Error("Failed to fetch PEP data");
const data = await response.json();

const versionSet = new Set();

for (const [pepNum, pep] of Object.entries(data)) {
const versions = parseVersions(pep.python_version);
pepVersionMap[pepNum] = versions;

// Collect unique major.minor versions
for (const v of versions) {
const majorMinor = getMajorMinor(v);
if (/^\d+\.\d+$/.test(majorMinor)) {
versionSet.add(majorMinor);
}
}
}

allVersions = sortVersionsDescending([...versionSet]);
return true;
} catch (error) {
console.error("Error loading PEP data:", error);
return false;
}
}

/**
* Extract PEP number from a table row
*/
function getPepNumberFromRow(row) {
const link = row.querySelector('a[href*="pep-"]');
if (!link) return null;
const match = link.getAttribute("href").match(/pep-0*(\d+)/);
return match ? match[1] : null;
}

/**
* Check if a PEP matches the selected version filter
*/
function pepMatchesVersion(pepNum, selectedVersion) {
if (!selectedVersion || selectedVersion === "all") return true;
const pepVersions = pepVersionMap[pepNum] || [];
return pepVersions.some((v) => getMajorMinor(v) === selectedVersion);
}

/**
* Apply the filter to all PEP tables
*/
function applyFilter(selectedVersion) {
const tables = document.querySelectorAll("table.pep-zero-table");
let totalVisible = 0;
let totalPeps = 0;

tables.forEach((table) => {
const rows = table.querySelectorAll("tbody tr");
let visibleInTable = 0;

rows.forEach((row) => {
const pepNum = getPepNumberFromRow(row);
if (pepNum === null) return;

totalPeps++;
const matches = pepMatchesVersion(pepNum, selectedVersion);

if (matches) {
row.style.display = "";
visibleInTable++;
totalVisible++;
} else {
row.style.display = "none";
}
});

// Find the section container (h2 + table) and hide if empty
const section = table.closest("section") || table.parentElement;
const h2 = section?.querySelector("h2");
if (h2 && visibleInTable === 0 && selectedVersion !== "all") {
section.style.display = "none";
} else if (section) {
section.style.display = "";
}
});

updateCount(totalVisible, totalPeps, selectedVersion);
}

/**
* Update the count display
*/
function updateCount(visible, total, selectedVersion) {
const countEl = document.getElementById("pep-filter-count");
if (!countEl) return;

if (!selectedVersion || selectedVersion === "all") {
countEl.textContent = "";
} else {
countEl.textContent = `Showing ${visible} of ${total} PEPs`;
}
}

/**
* Create the filter UI
*/
function createFilterUI() {
const container = document.createElement("div");
container.id = "pep-version-filter";
container.innerHTML = `
<label for="pep-version-select">Filter by Python version:</label>
<select id="pep-version-select">
<option value="all">All versions</option>
</select>
<span id="pep-filter-count" aria-live="polite"></span>
`;

const select = container.querySelector("#pep-version-select");

// Add version options
for (const version of allVersions) {
const option = document.createElement("option");
option.value = version;
option.textContent = version;
select.appendChild(option);
}

// Restore saved selection
const saved = localStorage.getItem(STORAGE_KEY);
if (saved && (saved === "all" || allVersions.includes(saved))) {
select.value = saved;
}

// Handle filter changes
select.addEventListener("change", () => {
const value = select.value;
localStorage.setItem(STORAGE_KEY, value);
applyFilter(value);
});

return container;
}

/**
* Insert the filter UI into the page
*/
function insertFilterUI(filterUI) {
// Find the "Index by Category" section and insert after its heading
const indexByCategory = document.getElementById("index-by-category");
if (indexByCategory) {
const heading = indexByCategory.querySelector("h2");
if (heading) {
heading.after(filterUI);
return true;
}
}

return false;
}

/**
* Initialize the filter
*/
async function init() {
// Only run on PEP 0 (index page)
const isPepIndex =
document.querySelector("section#introduction") &&
document.querySelector("table.pep-zero-table");
if (!isPepIndex) return;

const loaded = await loadPepData();
if (!loaded || allVersions.length === 0) return;

const filterUI = createFilterUI();
const inserted = insertFilterUI(filterUI);

if (inserted) {
// Apply initial filter
const saved = localStorage.getItem(STORAGE_KEY);
if (saved && saved !== "all") {
applyFilter(saved);
}
}
}

// Run when DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();
36 changes: 36 additions & 0 deletions pep_sphinx_extensions/pep_theme/static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -649,3 +649,39 @@ mark.pagefind-ui__highlight {
color: inherit;
margin-top: 0;
}

/* Python version filter */
#pep-version-filter {
background-color: var(--colour-background-accent-light);
border: 1px solid var(--colour-background-accent-strong);
border-radius: 4px;
padding: 0.75rem 1rem;
margin: 1rem 0 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
#pep-version-filter label {
font-weight: 500;
color: var(--colour-text-strong);
}
#pep-version-select {
padding: 0.4rem 0.6rem;
border: 1px solid var(--colour-background-accent-strong);
border-radius: 4px;
background-color: var(--colour-background);
color: var(--colour-text);
font-size: 0.95rem;
cursor: pointer;
min-width: 150px;
}
#pep-version-select:focus {
outline: none;
border-color: var(--colour-links);
}
#pep-filter-count {
color: var(--colour-text);
font-size: 0.9rem;
margin-left: auto;
}
4 changes: 4 additions & 0 deletions pep_sphinx_extensions/pep_theme/templates/page.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ <h2>Contents</h2>
<script src="{{ pathto('_static/colour_scheme.js', resource=True) }}"></script>
<script src="{{ pathto('_static/wrap_tables.js', resource=True) }}"></script>
<script src="{{ pathto('_static/sticky_banner.js', resource=True) }}"></script>
{%- if pagename == "pep-0000" %}
<script src="{{ pathto('_static/pep_version_filter.js', resource=True) }}"
data-api-url="{{ pathto('api/peps.json', resource=True) }}"></script>
{%- endif %}
<script src="https://analytics.python.org/js/script.outbound-links.js"
data-domain="peps.python.org" defer></script>
<script src="/pagefind/pagefind-ui.js"></script>
Expand Down
Loading