0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously
That one line in your package.json — ^1.2.3, ~1.2.3, >=1.2 <2 — is really a filter, and almost nobody can say exactly what it lets through. Type a range here and the demo rewrites it as the plain comparators npm actually uses, then runs every candidate past it live — sorted the way an installer sorts, so 1.2.10 outranks 1.2.3 and beta.11 outranks beta.2 — naming the bound that accepted each version or kept it out. It opens on ^1.2.3 with 1.3.0-rc.1 pinned, which is the case that bites people: the release candidate clears both bounds and still gets filtered out, because a range skips pre-releases unless it names one itself. Try ^0.2.3 to watch the caret change its mind below 1.0.0, flip the pre-releases switch to see the floors pick up npm's hidden -0, then add a version of your own at the bottom and see where it lands.
Source — the code you see is the code that just ran 1263 lines 54.3 KB index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Version Range Tester — sloprun.dev</title>
<style>
/* sloprun design tokens — inline this block into every demo (self-contained rule).
Identity: instrument-panel. Machine facts in mono; human words in sans.
Green is EARNED: only for "it ran / it worked" states, never decoration. */
:root {
--bg: #F6F6F3; /* neutral paper, not cream */
--surface: #FFFFFF;
--ink: #1A1D21;
--muted: #5A6068;
--line: #E3E2DC;
--accent: #C05600; /* phosphor amber, darkened for light bg */
--accent-soft: #FFF3E6;
--run: #1A7F37; /* earned green */
--run-soft: #E7F4EA;
--danger: #C93C37;
--radius: 10px;
--font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
}
:root[data-theme="dark"] {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }
/* derived tints (kept out of the verbatim token block, themed the same three ways) */
:root { --danger-soft: rgba(201, 60, 55, .14); }
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) { --danger-soft: rgba(244, 112, 103, .18); }
}
:root[data-theme="dark"] { --danger-soft: rgba(244, 112, 103, .18); }
/* ---- shell ---- */
* { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { margin: 0; padding: 0; }
body { font-size: 16px; line-height: 1.55; -webkit-text-size-adjust: 100%; overflow-x: hidden; }
.wrap { max-width: 1060px; margin: 0 auto; padding: 26px 18px 36px; }
h1 { font-size: 1.5rem; line-height: 1.2; margin: 0 0 7px; letter-spacing: -.015em; }
.lede { margin: 0; color: var(--muted); font-size: .95rem; max-width: 68ch; }
.lede b { color: var(--ink); font-weight: 600; }
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
code, .mono { font-family: var(--font-mono); }
.k-lab {
font-family: var(--font-mono); font-size: .66rem; letter-spacing: .11em;
text-transform: uppercase; color: var(--muted);
}
.btn {
font-family: var(--font-mono); font-size: .73rem; letter-spacing: .02em;
background: var(--bg); color: var(--ink); border: 1px solid var(--line);
border-radius: 999px; padding: 7px 12px; min-height: 32px; cursor: pointer;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.btn.ghost { background: transparent; color: var(--muted); }
.btn.ghost:hover { color: var(--accent); }
.panel {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); padding: 14px; margin-top: 18px;
}
/* ---- range input ---- */
.rowlab { display: flex; align-items: baseline; gap: 10px; margin-bottom: 7px; flex-wrap: wrap; }
.lamp {
font-family: var(--font-mono); font-size: .66rem; letter-spacing: .11em;
text-transform: uppercase; color: var(--muted); display: inline-flex; align-items: center; gap: 6px;
}
.lamp::before {
content: ""; width: 7px; height: 7px; border-radius: 50%;
background: var(--muted); flex: none;
}
.lamp[data-state="ok"] { color: var(--muted); }
.lamp[data-state="ok"]::before { background: var(--accent); }
.lamp[data-state="bad"] { color: var(--danger); }
.lamp[data-state="bad"]::before { background: var(--danger); }
.rangebox {
display: flex; align-items: stretch; gap: 0;
border: 1px solid var(--line); border-radius: 9px; background: var(--bg); overflow: hidden;
}
.rangebox:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.rangebox .sig {
font-family: var(--font-mono); font-size: .9rem; color: var(--muted);
padding: 12px 0 12px 13px; user-select: none; flex: none;
}
#range {
flex: 1 1 auto; min-width: 0; border: 0; background: transparent; color: var(--ink);
font-family: var(--font-mono); font-size: 1rem; letter-spacing: 0;
padding: 12px 12px 12px 8px;
}
#range:focus { outline: none; }
#range::placeholder { color: var(--muted); opacity: 1; }
.rangebox.bad { border-color: var(--danger); }
.rangebox.bad:focus-within { box-shadow: 0 0 0 3px var(--danger-soft); }
.err { margin: 8px 0 0; color: var(--danger); font-size: .85rem; }
.err code { background: transparent; }
.presets { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.chip {
font-family: var(--font-mono); font-size: .72rem; letter-spacing: 0;
border: 1px solid var(--line); border-radius: 999px; padding: 6px 11px; min-height: 30px;
background: var(--bg); color: var(--ink); cursor: pointer;
display: inline-flex; align-items: center;
}
.chip:hover { border-color: var(--accent); color: var(--accent); }
.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.chip[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
/* ---- expansion strip ---- */
.expand { margin-top: 14px; display: flex; flex-direction: column; gap: 7px; }
.clause {
display: flex; flex-wrap: wrap; align-items: center; gap: 7px;
background: var(--bg); border: 1px solid var(--line); border-radius: 9px; padding: 9px 11px;
}
.clause .cno { font-family: var(--font-mono); font-size: .66rem; letter-spacing: .11em; color: var(--muted); flex: none; }
.cmp {
font-family: var(--font-mono); font-size: .78rem;
background: var(--surface); color: var(--ink);
border: 1px solid var(--line); border-bottom-width: 2px; border-bottom-color: var(--accent);
border-radius: 7px; padding: 5px 9px; min-height: 30px; cursor: help;
overflow-wrap: anywhere; text-align: left;
}
.cmp:hover, .cmp[data-on="1"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.cmp:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.orbar { display: flex; align-items: center; gap: 9px; }
.orbar .k-lab { flex: none; }
.orbar i { flex: 1 1 auto; height: 1px; background: var(--line); display: block; }
.cmp .z { opacity: .5; }
.nob {
font-family: var(--font-mono); font-size: .78rem; color: var(--muted);
border: 1px dashed var(--line); border-radius: 7px; padding: 5px 9px; min-height: 30px;
display: inline-flex; align-items: center;
}
.zeronote { margin: 9px 0 0; font-size: .84rem; color: var(--muted); }
.zeronote code { color: var(--ink); }
.plain { margin: 11px 0 0; font-size: .92rem; }
.plain b { color: var(--accent); font-weight: 600; }
/* ---- stats ---- */
.stats {
display: flex; flex-wrap: wrap; gap: 10px 22px; align-items: flex-end;
margin-top: 18px; padding: 12px 14px;
background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
}
.stat { display: flex; flex-direction: column; gap: 3px; }
.stat .val { font-family: var(--font-mono); font-size: 1.02rem; line-height: 1.2; }
.stat .val.run { color: var(--run); }
.stat .val.acc { color: var(--accent); }
.stat .val.off { color: var(--muted); }
.stats .spacer { flex: 1 1 auto; }
/* ---- main split ---- */
.main { display: flex; flex-direction: column; gap: 16px; margin-top: 16px; }
.why { order: -1; }
@media (min-width: 900px) {
.main { display: grid; grid-template-columns: minmax(0,1fr) 330px; align-items: start; }
.why { order: 0; position: sticky; top: 16px; }
}
/* ---- why panel ---- */
.why {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); padding: 14px;
}
.whyhead { display: flex; align-items: center; gap: 8px; justify-content: space-between; margin-bottom: 9px; }
.wv { font-family: var(--font-mono); font-size: 1.02rem; overflow-wrap: anywhere; margin: 0 0 2px; }
.verdict {
font-family: var(--font-mono); font-size: .66rem; letter-spacing: .11em; text-transform: uppercase;
border: 1px solid var(--line); border-radius: 999px; padding: 3px 9px; color: var(--muted); flex: none;
}
.verdict[data-v="yes"] { color: var(--run); border-color: var(--run); background: var(--run-soft); }
.checks { list-style: none; margin: 10px 0 0; padding: 0; display: flex; flex-direction: column; gap: 0; }
.checks li {
display: flex; align-items: baseline; gap: 9px; padding: 6px 0;
border-top: 1px dashed var(--line); font-family: var(--font-mono); font-size: .78rem;
}
.checks li:first-child { border-top: 0; }
.checks .cw { flex: 1 1 auto; overflow-wrap: anywhere; color: var(--ink); }
.checks .cs { flex: none; letter-spacing: .08em; font-size: .68rem; text-transform: uppercase; color: var(--muted); }
.checks li[data-p="1"] .cs { color: var(--run); }
.checks li[data-p="rule"] .cw { color: var(--muted); }
.say { margin: 11px 0 0; font-size: .89rem; color: var(--ink); }
.say .m { font-family: var(--font-mono); font-size: .82rem; }
.hint { color: var(--muted); font-size: .89rem; margin: 0; }
.neigh {
margin-top: 11px; padding-top: 10px; border-top: 1px solid var(--line);
font-family: var(--font-mono); font-size: .72rem; color: var(--muted); overflow-wrap: anywhere;
}
.neigh b { color: var(--ink); font-weight: 400; }
.pintag { font-family: var(--font-mono); font-size: .62rem; letter-spacing: .1em; text-transform: uppercase; color: var(--accent); }
/* ---- grid ---- */
.gridpanel {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); padding: 14px;
}
.gridhead { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px; margin-bottom: 4px; }
.gridhead h2 { font-size: .95rem; margin: 0; }
.gridhead .k-lab { margin-left: auto; }
.grp { margin-top: 13px; }
.grphead { display: flex; align-items: baseline; gap: 9px; margin-bottom: 6px; }
.grphead .gname { font-family: var(--font-mono); font-size: .72rem; color: var(--ink); letter-spacing: .04em; }
.grphead i { flex: 1 1 auto; height: 1px; background: var(--line); display: block; }
.grphead .gcount { font-family: var(--font-mono); font-size: .66rem; color: var(--muted); flex: none; }
.cells { display: grid; grid-template-columns: repeat(auto-fill, minmax(108px, 1fr)); gap: 7px; }
.cell {
position: relative; text-align: left; cursor: pointer;
background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
padding: 8px 9px 9px; min-height: 46px; color: var(--muted);
font-family: var(--font-mono); font-size: .74rem; line-height: 1.3;
overflow-wrap: anywhere; display: flex; align-items: flex-start; gap: 6px;
}
.cell .num { flex: 1 1 auto; }
.cell .mk { flex: none; font-size: .8rem; line-height: 1.2; color: var(--muted); opacity: .5; }
.cell[data-m="1"] { background: var(--run-soft); border-color: var(--run); color: var(--ink); }
.cell[data-m="1"] .mk { color: var(--run); opacity: 1; }
.cell[data-m="?"] { opacity: .45; }
.cell:hover { border-color: var(--accent); }
.cell:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.cell[data-pin="1"] { box-shadow: inset 0 0 0 2px var(--accent); }
.cell .pick {
display: block; margin-top: 3px; font-size: .6rem; letter-spacing: .1em;
text-transform: uppercase; color: var(--accent);
}
.legend { display: flex; flex-wrap: wrap; gap: 8px 16px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); }
.legend span { font-family: var(--font-mono); font-size: .66rem; color: var(--muted); display: inline-flex; align-items: center; gap: 6px; }
.legend i { width: 11px; height: 11px; border-radius: 3px; border: 1px solid var(--line); background: var(--bg); display: block; }
.legend i.on { background: var(--run-soft); border-color: var(--run); }
.note { margin: 10px 0 0; font-size: .85rem; color: var(--muted); }
/* ---- add row ---- */
.addrow { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 14px; align-items: center; }
#addv {
flex: 1 1 150px; min-width: 0; font-family: var(--font-mono); font-size: 16px;
background: var(--bg); color: var(--ink); border: 1px solid var(--line);
border-radius: 8px; padding: 8px 10px; min-height: 34px;
}
#addv:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
#addv::placeholder { color: var(--muted); opacity: 1; }
.addmsg { font-family: var(--font-mono); font-size: .7rem; color: var(--muted); flex-basis: 100%; }
.addmsg[data-s="bad"] { color: var(--danger); }
.addmsg[data-s="ok"] { color: var(--run); }
footer { margin-top: 24px; font-family: var(--font-mono); font-size: .7rem; color: var(--muted); }
@media (prefers-reduced-motion: no-preference) {
.btn, .chip, .cmp, .cell, .verdict, .checks li { transition: background-color .13s ease, border-color .13s ease, color .13s ease, box-shadow .13s ease, opacity .13s ease; }
@keyframes flash { 0% { box-shadow: 0 0 0 3px var(--accent-soft); border-color: var(--accent); } 100% { box-shadow: 0 0 0 0 transparent; } }
.cell.fresh { animation: flash .9s ease-out 1; }
}
@media (max-width: 400px) {
.wrap { padding: 20px 13px 30px; }
.cells { grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); }
}
</style>
</head>
<body>
<main class="wrap">
<h1>Version Range Tester</h1>
<p class="lede">The line you write in <b>package.json</b> is a filter. Type one below and every candidate version
says whether it gets through — and <b>which part of the range</b> let it in or kept it out.</p>
<section class="panel" aria-labelledby="rl">
<div class="rowlab">
<span class="k-lab" id="rl">version range</span>
<span class="lamp" id="lamp" data-state="ok">parsed</span>
</div>
<div class="rangebox" id="rangebox">
<span class="sig" aria-hidden="true">"pkg":</span>
<input id="range" type="text" value="^1.2.3" spellcheck="false" autocomplete="off"
autocapitalize="off" aria-describedby="rl expand-desc" placeholder="e.g. ^1.2.3">
</div>
<p class="err" id="err" role="status" hidden></p>
<div class="presets" id="presets" role="group" aria-label="Example ranges"></div>
<div class="expand" id="expand" aria-describedby="expand-desc"></div>
<p class="zeronote" id="zeronote" hidden></p>
<p class="plain" id="plain"></p>
<p class="sr-only" id="expand-desc">The range is rewritten as plain comparators. Point at any comparator for an explanation.</p>
</section>
<div class="stats">
<div class="stat">
<span class="k-lab">versions accepted</span>
<span class="val run" id="s-count" aria-live="polite">0 / 0</span>
</div>
<div class="stat">
<span class="k-lab">an installer picks</span>
<span class="val acc" id="s-pick">—</span>
</div>
<div class="stat">
<span class="k-lab">pre-releases</span>
<button type="button" class="btn" id="pretog" aria-pressed="false"><span class="sr-only">pre-releases </span><span id="pretogtxt">excluded</span></button>
</div>
<div class="spacer"></div>
<div class="stat">
<button type="button" class="btn ghost" id="reset">reset</button>
</div>
</div>
<div class="main">
<section class="gridpanel" aria-labelledby="gh">
<div class="gridhead">
<h2 id="gh">Candidate versions</h2>
<span class="k-lab">precedence order · oldest first</span>
</div>
<div id="groups"></div>
<div class="legend">
<span><i class="on" aria-hidden="true"></i> accepted by the range</span>
<span><i aria-hidden="true"></i> filtered out</span>
<span>arrow keys move · click to pin</span>
</div>
<p class="note">Sorted the way an installer sorts: numbers compare as numbers, so
<code>1.2.10</code> is newer than <code>1.2.3</code> and <code>beta.11</code> is newer than
<code>beta.2</code>. A pre-release always ranks below the release it leads to.</p>
<div class="addrow">
<label class="k-lab" for="addv">add a version</label>
<input id="addv" type="text" placeholder="1.4.0-rc.2" spellcheck="false" autocomplete="off" autocapitalize="off">
<button type="button" class="btn" id="addbtn">add</button>
<span class="addmsg" id="addmsg" role="status"></span>
</div>
</section>
<section class="why" id="why" aria-label="Explanation"></section>
</div>
<footer>demo · sloprun.dev</footer>
</main>
<script>
(function () {
"use strict";
var $ = function (id) { return document.getElementById(id); };
function esc(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
});
}
/* ===== semver core (start) ===== */
var FULLRE = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
var PARTRE = /^v?(\d+|[xX*])(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
/* a full, valid version — nothing less is accepted as a candidate */
function parseVersion(s) {
var m = FULLRE.exec(String(s).trim());
if (!m) return null;
return {
major: +m[1], minor: +m[2], patch: +m[3],
pre: m[4] ? m[4].split(".") : [],
build: m[5] || ""
};
}
function isNum(id) { return /^[0-9]+$/.test(id); }
/* one pre-release identifier against another: numbers numerically, text as text,
and a number always ranks below text */
function cmpIdent(a, b) {
var na = isNum(a), nb = isNum(b);
if (na && nb) { var x = +a, y = +b; return x < y ? -1 : x > y ? 1 : 0; }
if (na) return -1;
if (nb) return 1;
return a < b ? -1 : a > b ? 1 : 0;
}
function cmpPre(a, b) {
if (!a.length && !b.length) return 0;
if (!a.length) return 1; /* a release outranks any pre-release of itself */
if (!b.length) return -1;
var n = Math.min(a.length, b.length);
for (var i = 0; i < n; i++) {
var c = cmpIdent(a[i], b[i]);
if (c) return c;
}
return a.length < b.length ? -1 : a.length > b.length ? 1 : 0; /* fewer fields = lower */
}
/* build metadata is ignored for precedence, per spec */
function compare(a, b) {
if (a.major !== b.major) return a.major < b.major ? -1 : 1;
if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;
return cmpPre(a.pre, b.pre);
}
function fmtV(v) {
return v.major + "." + v.minor + "." + v.patch + (v.pre.length ? "-" + v.pre.join(".") : "") +
(v.build ? "+" + v.build : "");
}
function isXtok(t) { return t === undefined || t === "" || t === "x" || t === "X" || t === "*"; }
/* a possibly-partial version used inside a range: 1.2.x, 1.2, 1, * */
function parsePartial(s) {
if (s === "" || s === "*" || s === "x" || s === "X") return { major: null, minor: null, patch: null, pre: [] };
var m = PARTRE.exec(s);
if (!m) return null;
/* an x swallows everything to its right; a number after one is a typo, not a range */
if (isXtok(m[1]) && !isXtok(m[2])) return null;
if (isXtok(m[2]) && !isXtok(m[3])) return null;
function num(t) { return isXtok(t) ? null : +t; }
var maj = num(m[1]), min = num(m[2]), pat = num(m[3]);
if (maj === null) return { major: null, minor: null, patch: null, pre: [] };
var pre = m[4] ? m[4].split(".") : [];
for (var i = 0; i < pre.length; i++) if (!/^[0-9A-Za-z-]+$/.test(pre[i])) return null;
if (pre.length && (min === null || pat === null)) return null;
return { major: maj, minor: min, patch: pat, pre: pre };
}
function V(ma, mi, pa, pre) { return { major: ma, minor: mi, patch: pa, pre: pre || [], build: "" }; }
/* A comparator. `synth` marks a "-0" this tool added rather than one the author typed,
so it can be rendered quietly and explained. */
function C(op, v, synth) {
var full = op + fmtV(v);
var zero = !!synth && v.pre.length === 1 && v.pre[0] === "0";
return { op: op, ver: v, text: full, head: zero ? full.slice(0, -2) : full, tail: zero ? "-0" : "" };
}
/* the lowest possible pre-release tag: the floor of a version, below -alpha, below -0.0 */
function floorTag(inc) { return inc ? ["0"] : []; }
/* one whitespace-free piece of a range -> a list of plain comparators.
Ceilings always carry -0 so a pre-release of the ceiling stays outside; floors carry
one only when pre-releases are switched on. This is exactly what npm does. */
function expandSimple(tok, inc) {
var m = /^(\^|~>|~|>=|<=|>|<|=)?(.*)$/.exec(tok);
var op = m[1] || "", rest = m[2];
if (op === "~>") op = "~";
if (op && rest === "") return null; /* an operator with nothing after it is not a range */
var p = parsePartial(rest);
if (!p) return null;
var Z = floorTag(inc), z0 = ["0"];
var any = p.major === null;
var noMinor = !any && p.minor === null;
var noPatch = !any && !noMinor && p.patch === null;
var exact = V(p.major, p.minor, p.patch, p.pre);
/* the same version as a floor: a written pre-release wins, otherwise the tag rides along */
var zsyn = !p.pre.length, zex = zsyn ? V(p.major, p.minor, p.patch, Z) : exact;
var ANY = [C(">=", V(0, 0, 0, Z), true)];
var NONE = [C("<", V(0, 0, 0, z0), true)];
var kind, cmps;
if (op === "^") {
kind = "caret";
if (any) cmps = ANY;
else if (noMinor) cmps = [C(">=", V(p.major, 0, 0, Z), true), C("<", V(p.major + 1, 0, 0, z0), true)];
else if (noPatch) cmps = p.major > 0
? [C(">=", V(p.major, p.minor, 0, Z), true), C("<", V(p.major + 1, 0, 0, z0), true)]
: [C(">=", V(0, p.minor, 0, Z), true), C("<", V(0, p.minor + 1, 0, z0), true)];
else if (p.major > 0) cmps = [C(">=", exact), C("<", V(p.major + 1, 0, 0, z0), true)];
/* below 1.0.0 the caret floor picks up the tag as well, exactly where npm puts it */
else if (p.minor > 0) cmps = [C(">=", zex, zsyn), C("<", V(0, p.minor + 1, 0, z0), true)];
else cmps = [C(">=", zex, zsyn), C("<", V(0, 0, p.patch + 1, z0), true)];
} else if (op === "~") {
/* npm never lowers a tilde floor onto -0, even with pre-releases included */
kind = "tilde";
if (any) cmps = ANY;
else if (noMinor) cmps = [C(">=", V(p.major, 0, 0, []), true), C("<", V(p.major + 1, 0, 0, z0), true)];
else if (noPatch) cmps = [C(">=", V(p.major, p.minor, 0, []), true), C("<", V(p.major, p.minor + 1, 0, z0), true)];
else cmps = [C(">=", exact), C("<", V(p.major, p.minor + 1, 0, z0), true)];
} else if (op === "" || op === "=") {
kind = any ? "any" : (noMinor || noPatch) ? "xrange" : "exact";
if (any) cmps = ANY;
else if (noMinor) cmps = [C(">=", V(p.major, 0, 0, Z), true), C("<", V(p.major + 1, 0, 0, z0), true)];
else if (noPatch) cmps = [C(">=", V(p.major, p.minor, 0, Z), true), C("<", V(p.major, p.minor + 1, 0, z0), true)];
else cmps = [C("=", exact)];
} else if (op === ">") {
kind = "op";
if (any) cmps = NONE; /* nothing is greater than everything */
else if (noMinor) cmps = [C(">=", V(p.major + 1, 0, 0, Z), true)];
else if (noPatch) cmps = [C(">=", V(p.major, p.minor + 1, 0, Z), true)];
else cmps = [C(">", exact)];
} else if (op === ">=") {
kind = "op";
if (any) cmps = ANY;
else if (noMinor) cmps = [C(">=", V(p.major, 0, 0, Z), true)];
else if (noPatch) cmps = [C(">=", V(p.major, p.minor, 0, Z), true)];
else cmps = [C(">=", exact)];
} else if (op === "<") {
kind = "op";
if (any) cmps = NONE;
else if (noMinor) cmps = [C("<", V(p.major, 0, 0, z0), true)];
else if (noPatch) cmps = [C("<", V(p.major, p.minor, 0, z0), true)];
else cmps = [C("<", exact)];
} else { /* <= */
kind = "op";
if (any) cmps = ANY;
else if (noMinor) cmps = [C("<", V(p.major + 1, 0, 0, z0), true)];
else if (noPatch) cmps = [C("<", V(p.major, p.minor + 1, 0, z0), true)];
else cmps = [C("<=", exact)];
}
return { src: tok, kind: kind, op: op, part: p, cmps: cmps };
}
function expandHyphen(loTok, hiTok, inc) {
var lo = parsePartial(loTok), hi = parsePartial(hiTok);
if (!lo || !hi) return null;
var Z = floorTag(inc), z0 = ["0"], cmps = [];
if (lo.major === null) { /* open lower end */ }
else if (lo.minor === null) cmps.push(C(">=", V(lo.major, 0, 0, Z), true));
else if (lo.patch === null) cmps.push(C(">=", V(lo.major, lo.minor, 0, Z), true));
else if (lo.pre.length) cmps.push(C(">=", V(lo.major, lo.minor, lo.patch, lo.pre)));
else cmps.push(C(">=", V(lo.major, lo.minor, lo.patch, Z), true));
if (hi.major === null) { /* open upper end */ }
else if (hi.minor === null) cmps.push(C("<", V(hi.major + 1, 0, 0, z0), true));
else if (hi.patch === null) cmps.push(C("<", V(hi.major, hi.minor + 1, 0, z0), true));
else if (hi.pre.length) cmps.push(C("<=", V(hi.major, hi.minor, hi.patch, hi.pre)));
else if (inc) cmps.push(C("<", V(hi.major, hi.minor, hi.patch + 1, z0), true));
else cmps.push(C("<=", V(hi.major, hi.minor, hi.patch, [])));
if (!cmps.length) cmps.push(C(">=", V(0, 0, 0, Z), true));
return { src: loTok + " - " + hiTok, kind: "hyphen", op: "-", part: lo, cmps: cmps };
}
/* full range string -> { ok, clauses:[{tokens, cmps}] } or { ok:false, error, token } */
function parseRange(input, inc) {
var src = String(input).trim();
var parts = src.split("||");
var clauses = [];
for (var ci = 0; ci < parts.length; ci++) {
var s = parts[ci].trim().replace(/\s+/g, " ").replace(/([<>]=?|~>?|\^|=)\s+/g, "$1");
var tokens = [];
if (s === "") {
tokens.push(expandSimple("*", inc));
} else {
var raw = s.split(" ");
for (var i = 0; i < raw.length; i++) {
var t = raw[i];
if (t === "-") {
if (!tokens.length || i + 1 >= raw.length) {
return { ok: false, error: "A dash range needs a version on each side, like 1.2.3 - 2.0.0.", token: "-" };
}
var prev = tokens.pop();
var h = expandHyphen(prev.src, raw[i + 1], inc);
if (!h) return { ok: false, error: "Could not read the dash range.", token: prev.src + " - " + raw[i + 1] };
tokens.push(h);
i++;
continue;
}
var e = expandSimple(t, inc);
if (!e) return { ok: false, error: explainBadToken(t), token: t };
tokens.push(e);
}
}
var flat = [];
for (var k = 0; k < tokens.length; k++) {
tokens[k].cmps = tokens[k].cmps.filter(function (c) { return !isFloor(c, inc); });
flat = flat.concat(tokens[k].cmps);
}
clauses.push({ tokens: tokens, cmps: flat });
}
/* a clause with nothing left to check takes everything, and npm lets it swallow
the alternatives beside it */
for (var q = 0; q < clauses.length; q++) {
if (!clauses[q].cmps.length) { clauses = [clauses[q]]; break; }
}
return { ok: true, src: src, clauses: clauses };
}
/* ">=0.0.0" rules nothing out, so npm deletes it outright — which quietly matters,
because a bound that is gone cannot be the one that lets a pre-release through */
function isFloor(c, inc) {
if (c.op !== ">=" || c.ver.major || c.ver.minor || c.ver.patch) return false;
return inc ? (c.ver.pre.length === 1 && c.ver.pre[0] === "0") : !c.ver.pre.length;
}
function explainBadToken(t) {
var body = t.replace(/^(\^|~>|~|>=|<=|>|<|=)/, "");
if (/[xX*]\.\d/.test(body)) return "“" + t + "” puts a number after an x. An x already means “anything here”, so nothing can follow it.";
if (/^\d+(\.\d+){3,}/.test(body)) return "“" + t + "” has too many numbers — a version is exactly three: major.minor.patch.";
if (/,/.test(t)) return "Commas are not separators here. Use a space for “and”, or || for “or”.";
if (/^[<>=~^]+$/.test(t)) return "“" + t + "” is an operator with nothing after it.";
if (/^\d+\.\d+\.\d+-$/.test(body)) return "“" + t + "” ends in a dash with no pre-release name after it.";
return "“" + t + "” is not a version or a range operator.";
}
function cmpTest(v, c) {
var r = compare(v, c.ver);
switch (c.op) {
case "=": return r === 0;
case ">": return r > 0;
case ">=": return r >= 0;
case "<": return r < 0;
case "<=": return r <= 0;
}
return false;
}
/* the pre-release rule: a pre-release only slips through a comparator set when some
comparator in that same set names a pre-release on the identical x.y.z */
function preAllowed(cmps, v) {
for (var i = 0; i < cmps.length; i++) {
var cv = cmps[i].ver;
if (cv.pre.length && cv.major === v.major && cv.minor === v.minor && cv.patch === v.patch) return true;
}
return false;
}
function testClause(clause, v, includePre) {
var lines = [], passes = 0;
for (var i = 0; i < clause.cmps.length; i++) {
var ok = cmpTest(v, clause.cmps[i]);
if (ok) passes++;
lines.push({ c: clause.cmps[i], pass: ok });
}
var all = passes === clause.cmps.length;
var blocked = false;
if (all && v.pre.length && !includePre && !preAllowed(clause.cmps, v)) blocked = true;
return { match: all && !blocked, lines: lines, passes: passes, preBlocked: blocked };
}
function testVersion(v, range, includePre) {
var best = null;
for (var i = 0; i < range.clauses.length; i++) {
var r = testClause(range.clauses[i], v, includePre);
r.clause = i;
if (r.match) return r;
if (!best || r.passes > best.passes || (r.preBlocked && !best.preBlocked)) best = r;
}
return best;
}
/* ===== semver core (end) ===== */
/* ---- plain-language notes ---- */
/* the human-readable version behind a comparator: the quiet -0 is a mechanism, not a number */
function baseV(c) { return c.tail ? c.ver.major + "." + c.ver.minor + "." + c.ver.patch : fmtV(c.ver); }
function boundsPhrase(cmps) {
var lo = null, hi = null, eq = null;
for (var i = 0; i < cmps.length; i++) {
var c = cmps[i];
if (c.op === "=") eq = c;
else if (c.op === ">=" || c.op === ">") lo = c;
else hi = c;
}
if (!cmps.length) return "anything — there is no bound left to clear.";
if (eq) return "only " + fmtV(eq.ver) + " itself.";
/* >* and <* expand to <0.0.0-0, which nothing can satisfy */
if (!lo && hi && hi.op === "<" && hi.ver.major === 0 && hi.ver.minor === 0 && hi.ver.patch === 0 && hi.tail) {
return "nothing at all — nothing sits below 0.0.0.";
}
var a = lo ? (lo.op === ">" ? "anything after " + fmtV(lo.ver)
: "from " + baseV(lo) + (lo.tail ? " and its pre-releases" : "")) : "anything";
if (!hi) return a + " upward, with no ceiling.";
var b = hi.op === "<=" ? " through " + fmtV(hi.ver) + ", inclusive." : " up to but not including " + baseV(hi) + ".";
return a + b;
}
/* comparator as markup: the added -0 rides along dimmed, so the eye reads the number */
function cmpHTML(c) {
return esc(c.head) + (c.tail ? '<span class="z">' + c.tail + "</span>" : "");
}
function tokenNote(t) {
var p = t.part, b = boundsPhrase(t.cmps);
if (t.kind === "caret") {
if (p.major === null) return "A caret on * is just *: " + b;
if (p.major > 0) return "The caret pins the leading number and lets everything after it move: " + b;
if (p.minor > 0 || p.minor === null) return "Below 1.0.0 the caret gets cautious — the first non-zero number is treated as the breaking one, so it pins the minor instead: " + b;
return "At 0.0.x every patch is fair game to break things, so the caret pins all three: " + b;
}
if (t.kind === "tilde") {
if (p.minor === null) return "A tilde with only a major number behaves like a caret: " + b;
return "The tilde lets the last number move and freezes the rest: " + b;
}
if (t.kind === "hyphen") return "A dash range is inclusive on the left and, when the right side is complete, on the right too: " + b;
if (t.kind === "xrange") return "An x (or a missing number) means “any value here”: " + b;
if (t.kind === "any") return "A bare * has no bound to write down at all, so npm writes none. Anything gets through — except pre-releases, which need a bound naming one before they are let in.";
if (t.kind === "exact") return "A version with no operator is a pin: " + b;
return "Read literally: " + b;
}
function clauseSentence(clause) {
return boundsPhrase(clause.cmps).replace(/^./, function (c) { return c.toUpperCase(); });
}
/* ---- state ---- */
var DEFAULTS = [
"0.1.0", "0.2.3", "0.2.9", "0.3.0",
"1.0.0-alpha", "1.0.0-alpha.1", "1.0.0-alpha.beta", "1.0.0-beta",
"1.0.0-beta.2", "1.0.0-beta.11", "1.0.0-rc.1", "1.0.0",
"1.0.4", "1.1.0", "1.2.2", "1.2.3", "1.2.4", "1.2.10",
"1.3.0-rc.1", "1.3.0", "1.9.9",
"2.0.0-rc.1", "2.0.0", "2.5.1", "3.0.0"
];
var PRESETS = [
"^1.2.3", "~1.2.3", ">=1.2 <2", "1.2.3 - 2.0.0", "1.x",
"^0.2.3", "1.2.3 || >=2.5.0", "^2.0.0-rc.1", "*"
];
var cands = []; /* parsed, sorted ascending */
var results = []; /* per-candidate test result, aligned with cands */
var range = null;
var includePre = false;
var pin = null; /* {t:"v", i} | {t:"c", ci, ti} */
var hover = null;
var cells = [];
var activeIdx = 0;
function setCands(list) {
var seen = {}, out = [];
for (var i = 0; i < list.length; i++) {
var v = typeof list[i] === "string" ? parseVersion(list[i]) : list[i];
if (!v) continue;
var key = fmtV(v);
if (seen[key]) continue;
seen[key] = 1;
out.push(v);
}
out.sort(function (a, b) { var c = compare(a, b); return c !== 0 ? c : (fmtV(a) < fmtV(b) ? -1 : 1); });
cands = out;
}
/* ---- range panel ---- */
function renderRange() {
var box = $("rangebox"), lamp = $("lamp"), err = $("err");
var exp = $("expand"), plain = $("plain");
exp.innerHTML = "";
if (!range.ok) {
box.classList.add("bad");
lamp.dataset.state = "bad";
lamp.textContent = "cannot read";
err.hidden = false;
err.textContent = range.error;
plain.textContent = "";
return;
}
box.classList.remove("bad");
lamp.dataset.state = "ok";
var n = 0;
for (var q = 0; q < range.clauses.length; q++) n += range.clauses[q].cmps.length;
lamp.textContent = range.clauses.length > 1
? range.clauses.length + " alternatives · " + n + " bounds"
: n === 0 ? "no bounds" : n + (n === 1 ? " bound" : " bounds");
err.hidden = true;
for (var ci = 0; ci < range.clauses.length; ci++) {
if (ci > 0) {
var bar = document.createElement("div");
bar.className = "orbar";
bar.innerHTML = '<i></i><span class="k-lab">or</span><i></i>';
exp.appendChild(bar);
}
var cl = document.createElement("div");
cl.className = "clause";
var lab = document.createElement("span");
lab.className = "cno";
lab.textContent = "accepts";
cl.appendChild(lab);
var cs = range.clauses[ci].cmps;
if (!cs.length) {
var nob = document.createElement("span");
nob.className = "nob";
nob.textContent = "any version";
cl.appendChild(nob);
}
for (var i = 0; i < cs.length; i++) {
if (i > 0) {
var and = document.createElement("span");
and.className = "k-lab";
and.textContent = "and";
cl.appendChild(and);
}
var b = document.createElement("button");
b.type = "button";
b.className = "cmp";
b.innerHTML = cmpHTML(cs[i]);
b.dataset.ci = ci;
b.dataset.i = i;
b.setAttribute("aria-label", "Comparator " + cs[i].text + ". Show what it allows.");
cl.appendChild(b);
}
exp.appendChild(cl);
}
var ceil = null, floor = null;
for (var z = 0; z < range.clauses.length; z++) {
var zs = range.clauses[z].cmps;
for (var y = 0; y < zs.length; y++) {
if (!zs[y].tail) continue;
if (zs[y].op === ">=" || zs[y].op === ">") { if (!floor) floor = zs[y]; }
else if (!ceil) ceil = zs[y];
}
}
var zn = $("zeronote");
if (ceil || floor) {
zn.hidden = false;
zn.innerHTML = "That dim <code>-0</code> is npm's own handwriting, not a version anyone published: it is the lowest pre-release tag that can exist. " +
(ceil ? "A ceiling of <code>" + esc(ceil.text) + "</code> therefore means “not even " + esc(baseV(ceil)) + "'s own release candidates.” " : "") +
(floor ? "A floor of <code>" + esc(floor.text) + "</code> means “start underneath every pre-release of " + esc(baseV(floor)) + "” — floors pick the tag up only while pre-releases are switched on." : "");
} else {
zn.hidden = true;
zn.textContent = "";
}
var sent = [];
for (var s = 0; s < range.clauses.length; s++) sent.push(clauseSentence(range.clauses[s]));
plain.innerHTML = "<b>In words:</b> " + esc(sent.join(" — or — "));
}
/* ---- grid ---- */
function buildGrid() {
var host = $("groups");
host.innerHTML = "";
cells = [];
var i = 0;
while (i < cands.length) {
var maj = cands[i].major, start = i;
while (i < cands.length && cands[i].major === maj) i++;
var g = document.createElement("div");
g.className = "grp";
var head = document.createElement("div");
head.className = "grphead";
head.innerHTML = '<span class="gname">' + maj + '.x</span><i></i>' +
'<span class="gcount" data-from="' + start + '" data-to="' + i + '"></span>';
g.appendChild(head);
var wrap = document.createElement("div");
wrap.className = "cells";
for (var k = start; k < i; k++) {
var b = document.createElement("button");
b.type = "button";
b.className = "cell";
b.dataset.i = k;
b.tabIndex = -1;
b.innerHTML = '<span class="num">' + esc(fmtV(cands[k])) + '</span><span class="mk" aria-hidden="true">·</span>';
wrap.appendChild(b);
cells.push(b);
}
g.appendChild(wrap);
host.appendChild(g);
}
if (activeIdx >= cells.length) activeIdx = 0;
if (cells.length) cells[activeIdx].tabIndex = 0;
}
function paint() {
results = [];
var matches = 0, pick = -1;
for (var i = 0; i < cands.length; i++) {
var r = range.ok ? testVersion(cands[i], range, includePre) : null;
results.push(r);
if (r && r.match) { matches++; pick = i; }
}
for (var k = 0; k < cells.length; k++) {
var idx = +cells[k].dataset.i;
var m = results[idx];
var lbl = fmtV(cands[idx]);
if (!range.ok) {
cells[k].dataset.m = "?";
cells[k].setAttribute("aria-label", lbl + " — range not readable");
} else {
cells[k].dataset.m = m.match ? "1" : "0";
cells[k].setAttribute("aria-label", lbl + (m.match ? " — accepted" : " — filtered out"));
}
cells[k].querySelector(".mk").textContent = (range.ok && m.match) ? "✓" : "·";
var old = cells[k].querySelector(".pick");
if (old) old.remove();
if (idx === pick) {
var t = document.createElement("span");
t.className = "pick";
t.textContent = "installed";
cells[k].querySelector(".num").appendChild(t);
cells[k].setAttribute("aria-label", lbl + " — accepted, and the highest match, so this is the one installed");
}
cells[k].dataset.pin = (pin && pin.t === "v" && pin.i === idx) ? "1" : "0";
}
var counters = document.querySelectorAll(".gcount");
for (var c = 0; c < counters.length; c++) {
var from = +counters[c].dataset.from, to = +counters[c].dataset.to, hit = 0;
for (var j = from; j < to; j++) if (results[j] && results[j].match) hit++;
counters[c].textContent = range.ok ? hit + "/" + (to - from) : "—";
}
$("s-count").textContent = range.ok ? matches + " / " + cands.length : "— / " + cands.length;
$("s-count").className = "val " + (range.ok && matches ? "run" : "off");
$("s-pick").textContent = pick >= 0 ? fmtV(cands[pick]) : (range.ok ? "nothing" : "—");
$("s-pick").className = "val " + (pick >= 0 ? "acc" : "off");
renderWhy();
}
/* ---- why panel ---- */
function whyDefault() {
return '<p class="k-lab" style="margin:0 0 6px">reading</p>' +
'<p class="hint">Point at any version in the grid, or at a comparator above, and the reasoning shows up here. ' +
'Click to keep it pinned.</p>';
}
function whyVersion(idx, pinned) {
var v = cands[idx], r = results[idx];
var h = '<div class="whyhead"><p class="wv">' + esc(fmtV(v)) + '</p>';
if (!range.ok) {
h += '<span class="verdict">unknown</span></div><p class="hint">Fix the range above and this fills in.</p>';
return h;
}
h += '<span class="verdict" data-v="' + (r.match ? "yes" : "no") + '">' + (r.match ? "accepted" : "filtered out") + "</span></div>";
if (range.clauses.length > 1) {
h += '<p class="k-lab" style="margin:0 0 2px">' + (r.match ? "matched on" : "closest") + " alternative " + (r.clause + 1) + "</p>";
}
h += '<ul class="checks">';
if (!r.lines.length) {
h += '<li data-p="1"><span class="cw">no bounds to clear</span><span class="cs">✓ true</span></li>';
}
for (var i = 0; i < r.lines.length; i++) {
h += '<li data-p="' + (r.lines[i].pass ? "1" : "0") + '"><span class="cw">' + esc(fmtV(v)) + " " +
cmpHTML(r.lines[i].c) + '</span><span class="cs">' + (r.lines[i].pass ? "✓ true" : "✕ false") + "</span></li>";
}
if (v.pre.length) {
/* the rule only gets a say once every bound has already passed */
var allBounds = r.passes === r.lines.length;
var state, word;
if (!allBounds) { state = "rule"; word = "not reached"; }
else if (r.preBlocked) { state = "rule"; word = "✕ blocks"; }
else if (includePre) { state = "1"; word = "✓ turned off"; }
else { state = "1"; word = "✓ waived"; }
h += '<li data-p="' + state + '"><span class="cw">pre-release rule</span><span class="cs">' + word + "</span></li>";
}
h += "</ul>";
var say;
if (r.match) {
if (!r.lines.length) say = "This range writes down no bounds at all, so there is nothing for a version to fail.";
else if (!v.pre.length) say = "Every bound is satisfied.";
else if (includePre) say = "Every bound is satisfied, and pre-releases are switched on, so nothing holds it back.";
else say = "Every bound is satisfied, and the range names a pre-release on this exact <span class=\"m\">" +
v.major + "." + v.minor + "." + v.patch + "</span>, so this pre-release is allowed through.";
} else if (r.preBlocked) {
say = (r.lines.length ? "It clears every bound, but it" : "It") +
" is a pre-release. A range skips pre-releases unless the range itself " +
"mentions one on the same <span class=\"m\">" + v.major + "." + v.minor + "." + v.patch +
"</span>. Flip <span class=\"m\">pre-releases</span> to included above, or name it yourself with " +
"<span class=\"m\">>=" + esc(fmtV(v)) + "</span>.";
} else {
var fail = null;
for (var f = 0; f < r.lines.length; f++) if (!r.lines[f].pass) { fail = r.lines[f].c; break; }
var why = "";
if (fail) {
if (fail.op === "<" || fail.op === "<=") why = "it is at or past the upper bound";
else if (fail.op === ">" || fail.op === ">=") why = "it is older than the lower bound";
else why = "the range pins one exact version and this is not it";
}
say = "Fails <span class=\"m\">" + esc(fail ? fail.text : "") + "</span> — " + why + ".";
}
h += '<p class="say">' + say + "</p>";
var prev = idx > 0 ? fmtV(cands[idx - 1]) : null;
var next = idx < cands.length - 1 ? fmtV(cands[idx + 1]) : null;
h += '<div class="neigh">' + (prev ? esc(prev) + " < " : "") + "<b>" + esc(fmtV(v)) + "</b>" +
(next ? " < " + esc(next) : "") + "</div>";
if (v.pre.length) {
h += '<p class="note" style="font-size:.8rem">Pre-release fields are compared one at a time, left to right. ' +
'A number always ranks below a word, and a shorter list ranks below a longer one that starts the same.</p>';
}
if (pinned) h += '<p class="pintag" style="margin:8px 0 0">pinned · click again to release</p>';
return h;
}
function whyCmp(ci, i, pinned) {
var c = range.clauses[ci].cmps[i];
var tok = null, seen = 0;
var toks = range.clauses[ci].tokens;
for (var t = 0; t < toks.length; t++) {
if (i < seen + toks[t].cmps.length) { tok = toks[t]; break; }
seen += toks[t].cmps.length;
}
var pass = 0;
for (var k = 0; k < cands.length; k++) if (cmpTest(cands[k], c)) pass++;
var h = '<div class="whyhead"><p class="wv">' + cmpHTML(c) + '</p><span class="verdict">one bound</span></div>';
h += '<p class="k-lab" style="margin:0 0 4px">written as</p><p class="wv" style="font-size:.86rem">' + esc(tok ? tok.src : "") + "</p>";
h += '<p class="say">' + esc(tokenNote(tok || { kind: "op", part: {}, cmps: [c] })) + "</p>";
if (c.tail) {
h += '<p class="say" style="color:var(--muted)">Nobody wrote the <span class="m">-0</span> — npm adds it. ' +
(c.op === ">=" || c.op === ">"
? "It drops this floor underneath every pre-release of <span class=\"m\">" + esc(baseV(c)) + "</span>."
: "It pulls this ceiling underneath <span class=\"m\">" + esc(baseV(c)) + "</span>'s own pre-releases, so a release candidate for the next version cannot sneak in below the line.") + "</p>";
}
h += '<ul class="checks"><li data-p="1"><span class="cw">candidates clearing this bound alone</span>' +
'<span class="cs">' + pass + "/" + cands.length + "</span></li>";
if (range.clauses[ci].cmps.length > 1) {
h += '<li data-p="0"><span class="cw">bounds combined with it</span><span class="cs">' +
(range.clauses[ci].cmps.length - 1) + "</span></li>";
}
h += "</ul>";
if (pinned) h += '<p class="pintag" style="margin:8px 0 0">pinned · click again to release</p>';
return h;
}
function renderWhy() {
var target = hover || pin;
var pinned = !hover && !!pin;
var el = $("why");
/* put out any lit comparator first, so an early return can't leave one stuck on */
var on = document.querySelectorAll('.cmp[data-on="1"]');
for (var z = 0; z < on.length; z++) on[z].dataset.on = "0";
if (!target) { el.innerHTML = whyDefault(); return; }
if (target.t === "v") {
if (target.i >= cands.length) { el.innerHTML = whyDefault(); return; }
el.innerHTML = whyVersion(target.i, pinned);
} else {
if (!range.ok || !range.clauses[target.ci] || !range.clauses[target.ci].cmps[target.ti]) {
el.innerHTML = whyDefault();
return;
}
el.innerHTML = whyCmp(target.ci, target.ti, pinned);
}
if (target.t === "c") {
var b = document.querySelector('.cmp[data-ci="' + target.ci + '"][data-i="' + target.ti + '"]');
if (b) b.dataset.on = "1";
}
}
/* ---- apply ---- */
/* the pre-release switch changes how the range EXPANDS, not just how it is tested,
so every re-test goes back through the expander with the flag */
function applyRange(src) {
range = parseRange(src, includePre);
if (pin && pin.t === "c" &&
!(range.ok && range.clauses[pin.ci] && range.clauses[pin.ci].cmps[pin.ti])) pin = null;
renderRange();
paint();
var chips = $("presets").querySelectorAll(".chip");
for (var i = 0; i < chips.length; i++) {
chips[i].setAttribute("aria-pressed", chips[i].dataset.r === src.trim() ? "true" : "false");
}
}
function reapply() { applyRange($("range").value); }
/* ---- wiring ---- */
function buildPresets() {
var host = $("presets");
for (var i = 0; i < PRESETS.length; i++) {
var b = document.createElement("button");
b.type = "button";
b.className = "chip";
b.dataset.r = PRESETS[i];
b.textContent = PRESETS[i];
b.setAttribute("aria-pressed", "false");
host.appendChild(b);
}
host.addEventListener("click", function (e) {
var b = e.target.closest(".chip");
if (!b) return;
$("range").value = b.dataset.r;
applyRange(b.dataset.r, true);
});
}
$("range").addEventListener("input", function () { applyRange(this.value); });
function setPreTog() {
$("pretog").setAttribute("aria-pressed", includePre ? "true" : "false");
$("pretogtxt").textContent = includePre ? "included" : "excluded";
}
$("pretog").addEventListener("click", function () {
includePre = !includePre;
setPreTog();
reapply();
});
$("reset").addEventListener("click", function () {
setCands(DEFAULTS);
includePre = false;
setPreTog();
$("range").value = "^1.2.3";
$("addv").value = "";
setAddMsg("", "");
pin = { t: "v", i: indexOfVersion("1.3.0-rc.1") };
hover = null;
activeIdx = 0;
buildGrid();
applyRange("^1.2.3");
});
function indexOfVersion(s) {
var v = parseVersion(s);
if (!v) return -1;
for (var i = 0; i < cands.length; i++) if (compare(cands[i], v) === 0 && cands[i].build === v.build) return i;
return -1;
}
function setAddMsg(txt, state) {
var el = $("addmsg");
el.textContent = txt;
if (state) el.dataset.s = state; else el.removeAttribute("data-s");
}
function addVersion() {
var raw = $("addv").value.trim();
if (!raw) { setAddMsg("Type a version first.", "bad"); return; }
var v = parseVersion(raw);
if (!v) {
var m = /^v?(\d+)(\.\d+)?$/.exec(raw);
setAddMsg(m ? "Give all three numbers, like " + raw.replace(/^v/, "") + (m[2] ? ".0" : ".0.0") + "."
: "“" + raw + "” is not a valid version.", "bad");
return;
}
var existing = indexOfVersion(fmtV(v));
if (existing >= 0) {
setAddMsg(fmtV(v) + " is already in the grid.", "");
revealCandidate(existing);
return;
}
var list = cands.slice();
list.push(v);
setCands(list);
buildGrid();
paint();
var idx = indexOfVersion(fmtV(v));
setAddMsg("added " + fmtV(v) + " · " + (results[idx] && results[idx].match ? "accepted" : "filtered out"),
results[idx] && results[idx].match ? "ok" : "");
$("addv").value = "";
revealCandidate(idx);
}
/* show a version without stealing focus — the typist stays in the add field */
function revealCandidate(idx) {
for (var i = 0; i < cells.length; i++) {
if (+cells[i].dataset.i === idx) {
setActive(i);
pin = { t: "v", i: idx };
hover = null;
paint();
cells[i].classList.remove("fresh");
void cells[i].offsetWidth;
cells[i].classList.add("fresh");
if (cells[i].scrollIntoView) cells[i].scrollIntoView({ block: "nearest", inline: "nearest" });
return;
}
}
}
$("addbtn").addEventListener("click", addVersion);
$("addv").addEventListener("keydown", function (e) {
if (e.key === "Enter") { e.preventDefault(); addVersion(); }
});
function setActive(k) {
if (!cells.length) return;
if (cells[activeIdx]) cells[activeIdx].tabIndex = -1;
activeIdx = Math.max(0, Math.min(cells.length - 1, k));
cells[activeIdx].tabIndex = 0;
}
/* grid interaction: hover previews, click pins, focus pins */
var groups = $("groups");
groups.addEventListener("mouseover", function (e) {
var c = e.target.closest(".cell");
if (!c) return;
hover = { t: "v", i: +c.dataset.i };
renderWhy();
});
groups.addEventListener("mouseleave", function () { hover = null; renderWhy(); });
groups.addEventListener("click", function (e) {
var c = e.target.closest(".cell");
if (!c) return;
var i = +c.dataset.i;
pin = (pin && pin.t === "v" && pin.i === i) ? null : { t: "v", i: i };
hover = null;
setActive(cells.indexOf(c));
paint();
});
groups.addEventListener("focusin", function (e) {
var c = e.target.closest(".cell");
if (!c) return;
setActive(cells.indexOf(c));
pin = { t: "v", i: +c.dataset.i };
hover = null;
paint();
});
groups.addEventListener("keydown", function (e) {
var c = e.target.closest(".cell");
if (!c) return;
var k = cells.indexOf(c), next = -1;
if (e.key === "ArrowRight") next = k + 1;
else if (e.key === "ArrowLeft") next = k - 1;
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = cells.length - 1;
else if (e.key === "ArrowDown" || e.key === "ArrowUp") next = rowJump(k, e.key === "ArrowDown" ? 1 : -1);
else return;
if (next < 0 || next >= cells.length) { e.preventDefault(); return; }
e.preventDefault();
setActive(next);
cells[next].focus();
});
/* move to the visually nearest cell one row up or down */
function rowJump(k, dir) {
var cur = cells[k], top = cur.offsetTop, left = cur.offsetLeft;
var best = -1, bestTop = null, bestDx = Infinity;
for (var i = 0; i < cells.length; i++) {
var t = cells[i].offsetTop;
if (dir > 0 ? t <= top : t >= top) continue;
if (bestTop === null || (dir > 0 ? t < bestTop : t > bestTop)) { bestTop = t; best = -1; bestDx = Infinity; }
if (t !== bestTop) continue;
var dx = Math.abs(cells[i].offsetLeft - left);
if (dx < bestDx) { bestDx = dx; best = i; }
}
return best;
}
/* comparator chips */
$("expand").addEventListener("mouseover", function (e) {
var b = e.target.closest(".cmp");
if (!b) return;
hover = { t: "c", ci: +b.dataset.ci, ti: +b.dataset.i };
renderWhy();
});
$("expand").addEventListener("mouseleave", function () { hover = null; renderWhy(); });
$("expand").addEventListener("focusin", function (e) {
var b = e.target.closest(".cmp");
if (!b) return;
pin = { t: "c", ci: +b.dataset.ci, ti: +b.dataset.i };
hover = null;
paint();
});
$("expand").addEventListener("click", function (e) {
var b = e.target.closest(".cmp");
if (!b) return;
var same = pin && pin.t === "c" && pin.ci === +b.dataset.ci && pin.ti === +b.dataset.i;
pin = same ? null : { t: "c", ci: +b.dataset.ci, ti: +b.dataset.i };
hover = null;
paint();
});
/* platform theme */
window.addEventListener("message", function (e) {
var d = e.data;
if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
document.documentElement.setAttribute("data-theme", d.theme);
}
});
/* boot */
buildPresets();
setCands(DEFAULTS);
buildGrid();
pin = { t: "v", i: indexOfVersion("1.3.0-rc.1") };
applyRange("^1.2.3");
})();
</script>
</body>
</html>
source-visible by construction · nothing is published here without its code
Comments
0 totalNo comments yet. If you ran it, say what happened.