splux-site · log · files · refs · tags · releases · people · languages · GitHub · file
site/assets/live.js
16615 bytes at 4c4f12c · GitHub
// Refresh ISO tags and news from GitHub so a delayed Pages build
// cannot hide a new release or commit. Baked HTML stays if this fails.
// News times are rewritten to the viewer's local timezone.
(function () {
var API = "https://api.github.com/repos/RobertFlexx/";
var ACCEPT = { Accept: "application/vnd.github+json" };
var PER = 20;
var REPOS = [
{ path: "SPS", label: "SPS" },
{ path: "sps-core", label: "sps-core" },
{ path: "sps-extra", label: "sps-extra" },
{ path: "sps-community", label: "sps-community" },
{ path: "splux-site", label: "splux-site" }
];
var feed = document.getElementById("live-feed");
var brief = document.getElementById("live-brief");
var isoEl = document.getElementById("live-iso");
var tagEl = document.getElementById("live-tag");
var dateEl = document.getElementById("live-date");
var thisScript = document.currentScript ||
document.querySelector("script[src*=\"live.js\"]");
var dataRoot = (thisScript && thisScript.getAttribute("data-root")) || "./";
var gitHost = (thisScript && thisScript.getAttribute("data-git")) ||
"https://splux.robertflexx.dev/git";
gitHost = String(gitHost).replace(/\/+$/, "");
if (!feed && !brief && !isoEl && !tagEl)
return;
function esc(s) {
return String(s)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
}
function formatLocal(iso) {
var d = new Date(iso);
if (isNaN(d.getTime()))
return iso;
try {
return d.toLocaleString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
timeZoneName: "short"
});
} catch (e) {
return d.toString();
}
}
function localizeTimes(root) {
var times = (root || document).querySelectorAll("time[datetime]");
for (var i = 0; i < times.length; i++) {
var iso = times[i].getAttribute("datetime");
if (iso)
times[i].textContent = formatLocal(iso);
}
}
function json(url) {
return fetch(url, { headers: ACCEPT, cache: "no-store" }).then(function (r) {
if (!r.ok)
throw new Error(String(r.status));
return r.json();
});
}
function firstLine(msg) {
msg = String(msg || "");
var n = msg.indexOf("\n");
return n === -1 ? msg : msg.slice(0, n);
}
function newsNest() {
var p = location.pathname.replace(/\/index\.html$/, "").replace(/\/+$/, "");
return /\/news\/\d+$/.test(p);
}
function newsPage() {
var p = location.pathname.replace(/\/index\.html$/, "").replace(/\/+$/, "");
var m = p.match(/\/news\/(\d+)$/);
if (m)
return parseInt(m[1], 10) || 1;
return 1;
}
function newsHref(n, nest) {
if (n < 1)
n = 1;
if (nest) {
if (n <= 1)
return "../";
return "../" + n + "/";
}
if (n <= 1)
return "./";
return n + "/";
}
function readItems(root) {
var items = [];
if (!root)
return items;
var articles = root.querySelectorAll("article.item");
for (var i = 0; i < articles.length; i++) {
var a = articles[i];
var time = a.querySelector("time");
var kindEl = a.querySelector(".kind");
var link = a.querySelector("h3 a");
var summary = a.querySelector(".summary");
if (!time || !link)
continue;
items.push({
iso: time.getAttribute("datetime") || "",
kind: a.getAttribute("data-kind") || (kindEl ? kindEl.textContent : "commit"),
repo: a.getAttribute("data-repo") || "",
author: a.getAttribute("data-author") || "",
verified: a.getAttribute("data-verified") === "yes",
avatar: a.getAttribute("data-avatar") || "",
url: forgeCommitHref(link.getAttribute("href") || ""),
title: link.textContent || "",
summary: summary ? summary.textContent : ""
});
}
return items;
}
function fromJson(list) {
var items = [];
if (!list || !list.length)
return items;
for (var i = 0; i < list.length; i++) {
var it = list[i];
if (!it || !it.url)
continue;
items.push({
iso: it.iso || "",
kind: it.kind || "commit",
repo: it.repo || "",
author: it.author || "",
verified: !!it.verified,
avatar: it.avatar || "",
url: forgeCommitHref(it.url),
title: it.title || "",
summary: it.summary || ""
});
}
return items;
}
function avatarOf(it) {
var av = it && it.avatar ? String(it.avatar) : "";
if (/^https:\/\/(avatars\.githubusercontent\.com\/|github\.com\/)/.test(av))
return av;
var name = it && it.author ? String(it.author) : "";
if (/^[A-Za-z0-9-]+$/.test(name))
return "https://github.com/" + name + ".png?size=48";
return "";
}
function profileOf(it) {
var name = it && it.author ? String(it.author) : "";
if (/^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?$/.test(name) && name.length <= 39)
return gitHost + "/users/" + name + "/";
return "";
}
function renderItem(it, hideSummary) {
var av = avatarOf(it);
var profile = profileOf(it);
var html = "<article class=\"item\" data-kind=\"" + esc(it.kind || "") +
"\" data-repo=\"" + esc(it.repo || "") +
"\" data-author=\"" + esc(it.author || "") +
"\" data-verified=\"" + (it.verified ? "yes" : "no") +
"\" data-avatar=\"" + esc(av) + "\">";
html += "<p class=\"meta\"><time datetime=\"" + esc(it.iso) + "\">" +
esc(formatLocal(it.iso)) + "</time> <span class=\"kind\">" +
esc(it.kind) + "</span> " + esc(it.repo);
if (it.author || av) {
if (profile)
html += " <a class=\"who\" href=\"" + esc(profile) + "\">";
else
html += " <span class=\"who\">";
if (av)
html += "<img class=\"avatar\" src=\"" + esc(av) +
"\" width=\"24\" height=\"24\" alt=\"\" loading=\"lazy\">";
if (it.author)
html += esc(it.author);
html += profile ? "</a>" : "</span>";
}
if (it.verified)
html += " <span class=\"verified\">verified</span>";
html += "</p>";
html += "<h3><a href=\"" + esc(it.url) + "\">" + esc(it.title) + "</a></h3>";
if (!hideSummary && it.summary)
html += "<p class=\"summary\">" + esc(it.summary) + "</p>";
html += "</article>";
return html;
}
function merge(base, extra) {
var seen = {};
var out = [];
function add(it) {
if (!it || !it.url || seen[it.url])
return;
seen[it.url] = true;
out.push(it);
}
var i;
for (i = 0; i < extra.length; i++)
add(extra[i]);
for (i = 0; i < base.length; i++)
add(base[i]);
out.sort(function (a, b) {
if (a.iso < b.iso)
return 1;
if (a.iso > b.iso)
return -1;
return 0;
});
return out;
}
function paint(root, items, limit, hideSummary) {
if (!root)
return;
var n = items.length;
if (limit && n > limit)
n = limit;
var html = "<div class=\"feed\">";
for (var i = 0; i < n; i++)
html += renderItem(items[i], hideSummary);
html += "</div>";
root.innerHTML = html;
}
function paintPage(root, items, page, per) {
if (!root)
return;
var pages = Math.max(1, Math.ceil(items.length / per) || 1);
if (page > pages)
page = pages;
if (page < 1)
page = 1;
var start = (page - 1) * per;
var slice = items.slice(start, start + per);
paint(root, slice, 0, false);
return pages;
}
function inWindow(i, page, pages) {
var lo = page - 2;
var hi = page + 2;
if (lo < 1) {
hi += (1 - lo);
lo = 1;
}
if (hi > pages) {
lo -= (hi - pages);
hi = pages;
}
if (lo < 1)
lo = 1;
return i === 1 || i === pages || (i >= lo && i <= hi);
}
function paintPager(page, pages, nest, per) {
var nav = document.getElementById("news-pager");
if (!nav)
return;
nav.setAttribute("data-page", String(page));
nav.setAttribute("data-pages", String(pages));
nav.setAttribute("data-per", String(per));
if (pages <= 1) {
nav.hidden = true;
nav.innerHTML = "";
return;
}
nav.hidden = false;
var html = "<p class=\"pager-status\">Page " + page + " of " + pages + "</p>";
html += "<p class=\"pager-links\">";
if (page > 1)
html += "<a rel=\"prev\" href=\"" + esc(newsHref(page - 1, nest)) + "\">prev</a> ";
var needDots = false;
for (var i = 1; i <= pages; i++) {
if (!inWindow(i, page, pages)) {
needDots = true;
continue;
}
if (needDots) {
html += "<span class=\"pager-gap\">...</span> ";
needDots = false;
}
if (i === page)
html += "<a href=\"" + esc(newsHref(i, nest)) +
"\" aria-current=\"page\">" + i + "</a> ";
else
html += "<a href=\"" + esc(newsHref(i, nest)) + "\">" + i + "</a> ";
}
if (page < pages)
html += "<a rel=\"next\" href=\"" + esc(newsHref(page + 1, nest)) + "\">next</a>";
html += "</p>";
html += "<form class=\"pager-jump\" id=\"news-jump\" action=\"#\">";
html += "<label for=\"news-page-n\">Go to page</label> ";
html += "<input id=\"news-page-n\" name=\"n\" type=\"number\" min=\"1\" max=\"" +
pages + "\" value=\"" + page + "\" inputmode=\"numeric\"> ";
html += "<button type=\"submit\">Go</button>";
html += "</form>";
nav.innerHTML = html;
}
function shortSha(sha) {
sha = String(sha || "");
return sha.length > 7 ? sha.slice(0, 7) : sha;
}
function commitHref(repoPath, sha, htmlUrl) {
if (gitHost && repoPath && sha)
return gitHost + "/" + repoPath + "/commit/" + sha + "/";
return htmlUrl || "";
}
function forgeCommitHref(url) {
var u = String(url || "");
var m = /^https:\/\/github\.com\/RobertFlexx\/([^/]+)\/commit\/([0-9a-fA-F]+)/.exec(u);
if (m && gitHost)
return gitHost + "/" + m[1] + "/commit/" + m[2] + "/";
m = /^https:\/\/git\.splux\.robertflexx\.dev\/RobertFlexx\/([^/]+)\/commit\/([0-9a-fA-F]+)/.exec(u);
if (m && gitHost)
return gitHost + "/" + m[1] + "/commit/" + m[2] + "/";
return u;
}
function setSha(id, repoPath, sha) {
var el = document.getElementById(id);
if (!el || !sha)
return;
el.innerHTML = "<a href=\"" +
esc(commitHref(repoPath, sha, "")) + "\">" +
esc(shortSha(sha)) + "</a>";
}
// live-2026-08-31-9 sorts after live-2026-08-31-15 as a string.
function liveRank(tag) {
var m = /^live-(\d{4})-(\d{2})-(\d{2})(?:-(\d+))?$/.exec(String(tag || ""));
if (!m)
return null;
return [
parseInt(m[1], 10),
parseInt(m[2], 10),
parseInt(m[3], 10),
parseInt(m[4] || "0", 10)
];
}
function liveBetter(a, b) {
var ra = liveRank(a);
var rb = liveRank(b);
if (!ra)
return false;
if (!rb)
return true;
for (var i = 0; i < 4; i++) {
if (ra[i] > rb[i])
return true;
if (ra[i] < rb[i])
return false;
}
return false;
}
function pickLiveRelease(list) {
var best = null;
if (!list || !list.length)
return null;
for (var i = 0; i < list.length; i++) {
var r = list[i];
if (!r || r.draft)
continue;
var tag = r.tag_name || r.tag || "";
if (!liveRank(tag))
continue;
if (!best || liveBetter(tag, best.tag_name || best.tag || ""))
best = r;
}
return best;
}
var shownIsoTag = "";
function rewriteDownloads(tag) {
if (!tag)
return;
var encoded = encodeURIComponent(tag);
var prefix = "https://github.com/RobertFlexx/SPS/releases/";
var as = document.getElementsByTagName("a");
for (var i = 0; i < as.length; i++) {
var href = as[i].getAttribute("href") || "";
var m = href.match(/^https:\/\/github\.com\/RobertFlexx\/SPS\/releases\/(?:latest\/download|download\/[^/]+)\/([^/?#]+)$/);
if (m) {
as[i].setAttribute("href", prefix + "download/" + encoded + "/" + m[1]);
continue;
}
if (href === prefix + "latest" || href === prefix + "latest/" ||
/^https:\/\/github\.com\/RobertFlexx\/SPS\/releases\/tag\/[^/]+\/?$/.test(href))
as[i].setAttribute("href", prefix + "tag/" + encoded);
}
}
function setIso(rel) {
if (!rel)
return;
var tag = rel.tag_name || rel.tag || rel.name || "";
var when = rel.published_at || rel.published || rel.date || "";
var href = tag
? "https://github.com/RobertFlexx/SPS/releases/tag/" + encodeURIComponent(tag)
: "https://github.com/RobertFlexx/SPS/releases/latest";
if (isoEl && tag) {
isoEl.innerHTML = "<a href=\"" + esc(href) + "\">" +
esc(tag) + "</a> (<time datetime=\"" + esc(when) + "\">" +
esc(formatLocal(when)) + "</time>)";
}
if (tagEl && tag)
tagEl.textContent = tag;
if (dateEl && when) {
dateEl.setAttribute("data-iso", when);
dateEl.setAttribute("datetime", when);
dateEl.textContent = formatLocal(when);
}
}
function applyRelease(rel) {
if (!rel)
return;
var tag = rel.tag_name || rel.tag || rel.name || "";
if (!liveRank(tag))
return;
if (shownIsoTag && liveBetter(shownIsoTag, tag))
return;
shownIsoTag = tag;
setIso(rel);
rewriteDownloads(tag);
}
function fromReleases(list) {
var items = [];
for (var i = 0; i < list.length; i++) {
var r = list[i];
if (!r || r.draft)
continue;
var body = r.body ? String(r.body).replace(/\s+/g, " ").slice(0, 900) : "";
items.push({
iso: r.published_at || "",
kind: "release",
repo: "SPS",
author: (r.author && r.author.login) || "",
verified: false,
avatar: (r.author && r.author.avatar_url) || "",
url: r.html_url || "",
title: r.name || r.tag_name || "",
summary: body
});
}
return items;
}
function fromCommits(list, label) {
var items = [];
for (var i = 0; i < list.length; i++) {
var c = list[i];
if (!c)
continue;
var commit = c.commit || {};
var when = (commit.committer && commit.committer.date) ||
(commit.author && commit.author.date) || "";
var author = (c.author && c.author.login) ||
(commit.author && commit.author.name) || "";
var ver = commit.verification || {};
items.push({
iso: when,
kind: "commit",
repo: label,
author: author,
verified: !!ver.verified,
avatar: (c.author && c.author.avatar_url) || "",
url: forgeCommitHref(c.html_url || commitHref(label, c.sha, "")),
title: firstLine(commit.message),
summary: ""
});
}
return items;
}
localizeTimes(document);
document.addEventListener("submit", function (e) {
var form = e.target;
if (!form || form.id !== "news-jump")
return;
e.preventDefault();
var input = form.querySelector("input[name=\"n\"]") ||
document.getElementById("news-page-n");
var nav = document.getElementById("news-pager");
var pages = nav ? parseInt(nav.getAttribute("data-pages"), 10) || 1 : 1;
var n = input ? parseInt(input.value, 10) : 1;
if (isNaN(n) || n < 1)
n = 1;
if (n > pages)
n = pages;
location.href = newsHref(n, newsNest());
});
json(dataRoot + "data/release.json").then(function (info) {
if (!info || !info.tag)
return;
applyRelease({
tag_name: info.tag,
published_at: info.published_at || info.date || ""
});
}).catch(function () {});
function refreshCommitsAndNews(releases) {
var needCommits = !!(feed || brief || document.getElementById("live-sps"));
if (!needCommits)
return;
var extra = fromReleases(releases || []);
var commitReqs = [];
for (var r = 0; r < REPOS.length; r++)
commitReqs.push(json(API + REPOS[r].path + "/commits?per_page=100")
.catch(function () { return []; }));
var newsReq = feed
? json(dataRoot + "data/news.json").catch(function () { return []; })
: Promise.resolve([]);
Promise.all(commitReqs.concat([newsReq])).then(function (rows) {
var i;
for (i = 0; i < REPOS.length; i++)
extra = extra.concat(fromCommits(rows[i] || [], REPOS[i].label));
var baked = [];
if (feed)
baked = fromJson(rows[REPOS.length] || []);
if (!baked.length)
baked = readItems(feed).concat(readItems(brief));
var items = merge(baked, extra);
if (feed) {
var nest = newsNest();
var page = newsPage();
var pages = Math.max(1, Math.ceil(items.length / PER) || 1);
if (page > pages) {
location.replace(newsHref(pages, nest));
return;
}
paintPage(feed, items, page, PER);
paintPager(page, pages, nest, PER);
}
paint(brief, items, 8, true);
for (var k = 0; k < REPOS.length; k++) {
var commits = rows[k] || [];
var sha = commits[0] && commits[0].sha;
if (REPOS[k].path === "SPS")
setSha("live-sps", "SPS", sha);
else if (REPOS[k].path === "sps-core")
setSha("live-core", "sps-core", sha);
else if (REPOS[k].path === "sps-extra")
setSha("live-extra", "sps-extra", sha);
else if (REPOS[k].path === "sps-community")
setSha("live-community", "sps-community", sha);
}
}).catch(function () {
/* keep the HTML from the last site build */
});
}
json(API + "SPS/releases?per_page=100").then(function (releases) {
var rel = pickLiveRelease(releases);
if (rel)
applyRelease(rel);
refreshCommitsAndNews(releases);
}).catch(function () {
refreshCommitsAndNews([]);
});
})();