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
38 changes: 38 additions & 0 deletions docs/_static/validator-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Runs the packaging source from this docs build in Pyodide, off the main
// thread. Each request is {id, args}; each reply is {id, result} or
// {id, error}. The args go to ``run`` in validator.py.

const PYODIDE_URL = "https://cdn.jsdelivr.net/pyodide/v314.0.7/full/";

async function fetchOk(name) {
const response = await fetch(new URL(name, import.meta.url));
if (!response.ok) {
throw new Error(`Cannot fetch ${name}: ${response.status}`);
}
return response;
}

async function load() {
const [pyodide, zip, source] = await Promise.all([
import(PYODIDE_URL + "pyodide.mjs").then((m) =>
m.loadPyodide({ indexURL: PYODIDE_URL }),
),
fetchOk("packaging.zip").then((r) => r.arrayBuffer()),
fetchOk("validator.py").then((r) => r.text()),
]);
pyodide.unpackArchive(zip, "zip", { extractDir: "/packaging-src" });
pyodide.runPython("import sys; sys.path.insert(0, '/packaging-src')");
const namespace = pyodide.toPy({});
pyodide.runPython(source, { globals: namespace });
return namespace.get("run");
}

const run = load();

self.addEventListener("message", async ({ data: { id, args } }) => {
try {
self.postMessage({ id, result: JSON.parse((await run)(...args)) });
} catch (err) {
self.postMessage({ id, error: String(err.message ?? err) });
}
});
183 changes: 183 additions & 0 deletions docs/_static/validator.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
.validator-status {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.75rem;
font-size: var(--font-size--small);
color: var(--color-foreground-secondary);
background: var(--color-background-secondary);
border: 1px solid var(--color-background-border);
border-radius: 1rem;
}

.validator-status::before {
content: "";
width: 0.55rem;
height: 0.55rem;
background: var(--color-foreground-border);
border-radius: 50%;
}

.validator-status[data-state="loading"]::before {
background: var(--color-admonition-title--warning);
animation: validator-pulse 1s ease-in-out infinite alternate;
}

.validator-status[data-state="ready"]::before {
background: var(--color-admonition-title--tip);
}

.validator-status[data-state="error"]::before {
background: var(--color-admonition-title--danger);
}

@keyframes validator-pulse {
to {
opacity: 0.3;
}
}

form.validator {
display: flex;
flex-wrap: wrap;
gap: 0.6rem 0.5rem;
margin: 1rem 0 2rem;
padding: 1rem;
background: var(--color-background-secondary);
border: 1px solid var(--color-background-border);
border-radius: 0.5rem;
}

form.validator input {
flex: 1 1 12rem;
min-width: 0;
padding: 0.5rem 0.75rem;
font-family: var(--font-stack--monospace);
font-size: var(--font-size--normal);
color: var(--color-foreground-primary);
background: var(--color-background-primary);
border: 1px solid var(--color-background-border);
border-radius: 0.3rem;
}

form.validator input:focus {
outline: none;
border-color: var(--color-brand-primary);
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--color-brand-primary) 25%, transparent);
}

form.validator button[type="submit"] {
padding: 0.5rem 1.25rem;
font-weight: 600;
color: var(--color-background-primary);
background: var(--color-brand-primary);
border: none;
border-radius: 0.3rem;
cursor: pointer;
}

.validator-examples {
display: flex;
flex-basis: 100%;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem;
font-size: var(--font-size--small);
color: var(--color-foreground-muted);
}

.validator-examples button {
padding: 0.1rem 0.5rem;
font-family: var(--font-stack--monospace);
font-size: var(--font-size--small--2);
color: var(--color-foreground-secondary);
background: var(--color-background-primary);
border: 1px solid var(--color-background-border);
border-radius: 1rem;
cursor: pointer;
}

.validator-examples button:hover {
color: var(--color-brand-content);
border-color: var(--color-brand-content);
}

.validator-examples button.invalid {
background: var(--color-admonition-title-background--danger);
}

.validator-examples button.invalid:hover {
border-color: var(--color-admonition-title--danger);
}

form.validator output:empty {
display: none;
}

form.validator output {
flex-basis: 100%;
margin-top: 0.4rem;
overflow: hidden;
background: var(--color-background-primary);
border: 1px solid var(--color-background-border);
border-left: 4px solid var(--result-color, var(--color-foreground-border));
border-radius: 0.3rem;
}

form.validator output.valid {
--result-color: var(--color-admonition-title--tip);
--result-bg: var(--color-admonition-title-background--tip);
}

form.validator output.invalid {
--result-color: var(--color-admonition-title--danger);
--result-bg: var(--color-admonition-title-background--danger);
}

.validator-result-title {
padding: 0.4rem 1rem;
font-weight: 600;
background: var(--result-bg);
}

.validator-normalized {
padding: 0.75rem 1rem 0.25rem;
font-family: var(--font-stack--monospace);
font-size: 1.15em;
overflow-wrap: anywhere;
}

form.validator output dl {
display: grid;
grid-template-columns: max-content 1fr;
align-items: baseline;
gap: 0.3rem 1.5rem;
margin: 0;
padding: 0.5rem 1rem 0.75rem;
}

form.validator output dt {
font-size: var(--font-size--small);
color: var(--color-foreground-muted);
}

form.validator output dd {
margin: 0;
font-family: var(--font-stack--monospace);
font-size: var(--font-size--small);
overflow-wrap: anywhere;
}

form.validator output dd:is([data-value="None"], [data-value="False"]) {
color: var(--color-foreground-muted);
}

form.validator output pre {
margin: 0;
padding: 0.75rem 1rem;
overflow-x: auto;
font-size: var(--font-size--small);
background: none;
border: none;
}
148 changes: 148 additions & 0 deletions docs/_static/validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Sends checks to validator-worker.js, which runs Python in Pyodide. The
// worker starts on first use, so the page itself stays light.

const status = document.getElementById("validator-status");
let ready = null;

function setStatus(state, text) {
status.dataset.state = state;
status.textContent = text;
}

// Starts a worker and resolves to a function that sends it one check.
async function start() {
const worker = new Worker(new URL("validator-worker.js", import.meta.url), {
type: "module",
});
const pending = new Map();
let nextId = 0;
worker.addEventListener("message", ({ data: { id, result, error } }) => {
const { resolve, reject } = pending.get(id);
pending.delete(id);
if (error === undefined) {
resolve(result);
} else {
reject(new Error(error));
}
});
worker.addEventListener("error", (event) => {
event.preventDefault();
worker.terminate();
for (const { reject } of pending.values()) {
reject(new Error(event.message || "Cannot start the worker"));
}
pending.clear();
});
const call = (...args) =>
new Promise((resolve, reject) => {
const id = nextId++;
pending.set(id, { resolve, reject });
worker.postMessage({ id, args });
});
const { rows } = await call("info");
setStatus("ready", `Using ${rows.Info}`);
return call;
}

function getReady() {
if (ready === null) {
setStatus("loading", "Loading Python (Pyodide)…");
ready = start().catch((err) => {
// Discard the failed worker so the next attempt starts fresh.
ready = null;
setStatus("error", `Error: ${err.message}`);
throw err;
});
}
return ready;
}

function el(tag, className, text) {
const node = document.createElement(tag);
if (className) {
node.className = className;
}
if (text !== undefined) {
node.textContent = text;
}
return node;
}

function render(output, result) {
const { Normalized, Error: error, ...rows } = result.rows;
output.className = result.ok ? "valid" : "invalid";
const title = result.ok ? "✓ Valid" : "✗ Invalid";
const parts = [el("div", "validator-result-title", title)];
if (Normalized !== undefined) {
parts.push(el("div", "validator-normalized", Normalized));
}
if (Object.keys(rows).length) {
const dl = el("dl");
for (const [key, value] of Object.entries(rows)) {
const dd = el("dd", "", value);
dd.dataset.value = value;
dl.append(el("dt", "", key), dd);
}
parts.push(dl);
}
if (error !== undefined) {
parts.push(el("pre", "", error));
}
output.replaceChildren(...parts);
}

async function check(event) {
event.preventDefault();
const form = event.currentTarget;
const output = form.querySelector("output");
const args = [...form.querySelectorAll("input")].map((i) => i.value.trim());
if (!args[0]) {
output.replaceChildren();
return;
}
output.className = "";
output.replaceChildren(el("div", "validator-result-title", "Working…"));
try {
const call = await getReady();
render(output, await call(form.dataset.check, ...args));
} catch (err) {
output.className = "invalid";
output.replaceChildren(
el("div", "validator-result-title", "✗ Error"),
el("pre", "", err.message),
);
}
}

// Adds buttons that fill in and check example inputs.
function addExamples(form) {
const examples = JSON.parse(form.dataset.examples ?? "[]");
const invalid = JSON.parse(form.dataset.invalidExamples ?? "[]");
if (!examples.length && !invalid.length) {
return;
}
const inputs = form.querySelectorAll("input");
const row = el("div", "validator-examples", "Try:");
for (const example of [...examples, ...invalid]) {
const values = [example].flat();
const label = values[1] ? `${values[1]} in ${values[0]}` : values[0];
const className = invalid.includes(example) ? "invalid" : "";
const button = el("button", className, label);
button.type = "button";
button.addEventListener("click", () => {
inputs.forEach((input, i) => (input.value = values[i] ?? ""));
form.requestSubmit();
});
row.append(button);
}
form.querySelector("output").before(row);
}

for (const form of document.querySelectorAll("form.validator")) {
addExamples(form);
form.addEventListener("submit", check);
// Start the download as soon as the reader shows interest.
form.addEventListener("focusin", () => getReady().catch(() => {}), {
once: true,
});
}
Loading
Loading