splux-site · log · files · refs · tags · releases · people · languages · GitHub · commit
0cefdbf
Paginate news and show author, local time, and verified commits.
Parents
Stat
README.md | 8 +- site/assets/live.js | 245 ++++++++++++++++++++++++++++++++++++++++++----- site/assets/style.css | 68 ++++++++++++- site/download/index.html | 2 +- site/index.html | 2 +- site/news/index.html | 7 +- tools/build-site.sh | 34 ++++++- tools/collect-news.sh | 54 ++++++++--- tools/news.awk | 165 +++++++++++++++++++++++++++---- tools/subst.awk | 6 ++ 10 files changed, 527 insertions(+), 64 deletions(-)
Diff
diff --git a/README.md b/README.md
index d326e8a..e62a4b9 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,13 @@ and refreshed from GitHub when you open the page.
- every published SPS live ISO release (`RobertFlexx/SPS` GitHub Releases)
- every commit on SPS, sps-core, sps-extra, and this repository that
- Git or the GitHub API can see
+ Git or the GitHub API can see, with author and whether GitHub verified
+ the signature
+
+The news feed splits into pages of twenty items. Each page is a real
+URL (`/news/`, `/news/2/`, ...). Times in the page HTML are UTC; the
+browser rewrites them to the viewer's local timezone. There is a page
+number field for jumping when the archive grows.
The Pages workflow rebuilds every five minutes, and also on push,
`workflow_dispatch`, and a `rebuild` repository dispatch. The news and
diff --git a/site/assets/live.js b/site/assets/live.js
index 82cfe4b..df85ab0 100644
--- a/site/assets/live.js
+++ b/site/assets/live.js
@@ -1,8 +1,10 @@
// 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" },
@@ -15,6 +17,9 @@
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")) || "./";
if (!feed && !brief && !isoEl && !tagEl)
return;
@@ -26,10 +31,31 @@
.replace(/"/g, """);
}
- function dayOf(iso) {
- var d = String(iso || "");
- var t = d.indexOf("T");
- return t === -1 ? d : d.slice(0, t);
+ 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) {
@@ -46,6 +72,32 @@
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)
@@ -54,23 +106,17 @@
for (var i = 0; i < articles.length; i++) {
var a = articles[i];
var time = a.querySelector("time");
- var kind = a.querySelector(".kind");
+ var kindEl = a.querySelector(".kind");
var link = a.querySelector("h3 a");
var summary = a.querySelector(".summary");
- var meta = a.querySelector(".meta");
if (!time || !link)
continue;
- var repo = "";
- if (meta) {
- repo = meta.textContent.replace(time.textContent, "");
- if (kind)
- repo = repo.replace(kind.textContent, "");
- repo = repo.replace(/\s+/g, " ").trim();
- }
items.push({
iso: time.getAttribute("datetime") || "",
- kind: kind ? kind.textContent : "commit",
- repo: repo,
+ 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",
url: link.getAttribute("href") || "",
title: link.textContent || "",
summary: summary ? summary.textContent : ""
@@ -79,11 +125,41 @@
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,
+ url: it.url,
+ title: it.title || "",
+ summary: it.summary || ""
+ });
+ }
+ return items;
+ }
+
function renderItem(it, hideSummary) {
- var html = "<article class=\"item\">";
+ 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") + "\">";
html += "<p class=\"meta\"><time datetime=\"" + esc(it.iso) + "\">" +
- esc(dayOf(it.iso)) + "</time> <span class=\"kind\">" +
- esc(it.kind) + "</span> " + esc(it.repo) + "</p>";
+ esc(formatLocal(it.iso)) + "</time> <span class=\"kind\">" +
+ esc(it.kind) + "</span> " + esc(it.repo);
+ if (it.author)
+ html += " <span class=\"who\">" + esc(it.author) + "</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>";
@@ -128,6 +204,81 @@
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;
@@ -146,15 +297,18 @@
if (!rel)
return;
var tag = rel.tag_name || rel.name || "";
- var when = dayOf(rel.published_at || "");
+ var when = rel.published_at || "";
if (isoEl && tag) {
isoEl.innerHTML = "<a href=\"https://github.com/RobertFlexx/SPS/releases/latest\">" +
- esc(tag) + "</a> (" + esc(when) + ")";
+ esc(tag) + "</a> (<time datetime=\"" + esc(when) + "\">" +
+ esc(formatLocal(when)) + "</time>)";
}
if (tagEl && tag)
tagEl.textContent = tag;
- if (dateEl && when)
- dateEl.textContent = when;
+ if (dateEl && when) {
+ dateEl.setAttribute("data-iso", when);
+ dateEl.textContent = formatLocal(when);
+ }
}
function fromReleases(list) {
@@ -168,6 +322,8 @@
iso: r.published_at || "",
kind: "release",
repo: "SPS",
+ author: (r.author && r.author.login) || "",
+ verified: false,
url: r.html_url || "",
title: r.name || r.tag_name || "",
summary: body
@@ -185,10 +341,15 @@
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,
url: c.html_url || "",
title: firstLine(commit.message),
summary: ""
@@ -197,9 +358,30 @@
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());
+ });
+
var req = [json(API + "SPS/releases?per_page=100")];
for (var r = 0; r < REPOS.length; r++)
req.push(json(API + REPOS[r].path + "/commits?per_page=100"));
+ if (feed)
+ req.push(json(dataRoot + "data/news.json").catch(function () { return []; }));
Promise.all(req).then(function (rows) {
var releases = rows[0] || [];
@@ -207,9 +389,24 @@
for (var i = 0; i < REPOS.length; i++)
extra = extra.concat(fromCommits(rows[i + 1] || [], REPOS[i].label));
- var baked = readItems(feed).concat(readItems(brief));
+ var baked = [];
+ if (feed)
+ baked = fromJson(rows[REPOS.length + 1] || []);
+ if (!baked.length)
+ baked = readItems(feed).concat(readItems(brief));
var items = merge(baked, extra);
- paint(feed, items, 0, false);
+
+ 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);
var latest = null;
diff --git a/site/assets/style.css b/site/assets/style.css
index ed5878f..6421253 100644
--- a/site/assets/style.css
+++ b/site/assets/style.css
@@ -278,8 +278,74 @@ footer p {
font-size: 0.9rem;
}
-.feed .kind {
+.feed .who {
+ color: var(--fg);
+}
+
+.feed .verified {
+ display: inline-block;
+ color: var(--accent);
+ border: 1px solid var(--accent);
+ padding: 0 0.35rem;
+ font-size: 0.8rem;
+}
+
+.pager {
+ margin: 1.25rem 0 0;
+ padding-top: 1rem;
+ border-top: 1px solid var(--rule);
+ color: var(--muted);
+}
+
+.pager-status {
+ margin: 0 0 0.5rem;
+}
+
+.pager-links {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem 0.85rem;
+ margin: 0 0 0.85rem;
+}
+
+.pager-links a[aria-current="page"] {
+ color: var(--fg);
+ text-decoration: underline;
+}
+
+.pager-gap {
+ color: var(--muted);
+}
+
+.pager-jump {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.4rem 0.6rem;
+ margin: 0;
+}
+
+.pager-jump input {
+ width: 4.5rem;
+ background: #111;
+ color: var(--fg);
+ border: 1px solid var(--rule);
+ padding: 0.35rem 0.5rem;
+ font: inherit;
+}
+
+.pager-jump button {
+ font: inherit;
+ background: #111;
color: var(--accent);
+ border: 1px solid var(--rule);
+ padding: 0.35rem 0.6rem;
+ cursor: pointer;
+}
+
+.pager-jump button:hover {
+ text-decoration: underline;
}
.feed .summary {
diff --git a/site/download/index.html b/site/download/index.html
index 6efee0f..326e186 100644
--- a/site/download/index.html
+++ b/site/download/index.html
@@ -37,6 +37,6 @@ Plasma/Wayland live session plus the same installer path. Needs a KMS device (vi
</main>
@@FOOTER@@
</div>
-<script src="../../../../assets/live.js"></script>
+<script src="../../../../assets/live.js" data-root="../../../../"></script>
</body>
</html>
diff --git a/site/index.html b/site/index.html
index 979c9d4..59b2d20 100644
--- a/site/index.html
+++ b/site/index.html
@@ -111,6 +111,6 @@ this website
</main>
@@FOOTER@@
</div>
-<script src="../../../../assets/live.js"></script>
+<script src="../../../../assets/live.js" data-root="../../../../"></script>
</body>
</html>
diff --git a/site/news/index.html b/site/news/index.html
index ce1f8cc..db885cb 100644
--- a/site/news/index.html
+++ b/site/news/index.html
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>News - Splux Linux</title>
+<title>News - Splux Linux</title>
<meta name="description" content="Splux Linux news from ISO releases and git history.">
<link rel="icon" href="../../../../assets/favicon.png" type="image/png">
<link rel="stylesheet" href="../../../../assets/style.css">
@@ -17,16 +17,17 @@
<h2>News</h2>
<hr class="rule">
<p>This page is filled from GitHub: every published SPS live ISO release, plus commits on SPS, sps-core, sps-extra, and this website. Nobody has to edit it for a new snapshot or commit to show up.</p>
-<p class="note">The Pages workflow rebuilds every five minutes, on push, and when SPS asks it to. This page also reads GitHub when you open it, so a delayed rebuild cannot hide a new ISO. <a href="../../../../news/atom.xml">Atom feed</a>.</p>
+<p class="note">Times are shown in your local timezone. Commits name the author and whether GitHub verified the signature. Long feeds split into pages of twenty. <a href="../../../../news/atom.xml">Atom feed</a>.</p>
@@NEWS_INFO@@
<h2>Feed</h2>
<hr class="rule">
<div id="live-feed">
@@NEWS@@
</div>
+@@PAGER@@
</main>
@@FOOTER@@
</div>
-<script src="../../../../assets/live.js"></script>
+<script src="../../../../assets/live.js" data-root="../../../../"></script>
</body>
</html>
diff --git a/tools/build-site.sh b/tools/build-site.sh
index caae463..4399740 100755
--- a/tools/build-site.sh
+++ b/tools/build-site.sh
@@ -191,14 +191,25 @@ then
printf '%s\n' "warning: news collection failed" >&2
: >"$tmp/news.tsv"
fi
-awk -f tools/news.awk -v mode=html "$tmp/news.tsv" >"$tmp/news.html"
awk -f tools/news.awk -v mode=brief "$tmp/news.tsv" >"$tmp/news-brief.html"
generated_iso=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date -u '+%Y-%m-%dT00:00:00Z')
awk -f tools/news.awk -v mode=atom -v siteurl="$SITE_URL" \
-v feedupdated="$generated_iso" \
"$tmp/news.tsv" >"$OUT/news/atom.xml"
+awk -f tools/news.awk -v mode=json "$tmp/news.tsv" >"$OUT/data/news.json"
cp "$tmp/news.tsv" "$OUT/data/news.tsv"
+news_per=20
+nnews=$(awk 'END { print NR + 0 }' "$tmp/news.tsv")
+if [ "$nnews" -lt 1 ]; then
+ news_pages=1
+else
+ news_pages=$(( (nnews + news_per - 1) / news_per ))
+fi
+: >"$tmp/pager.html"
+: >"$tmp/news.html"
+pagetitle=
+
sps_sha=$(short_sha "$SPS")
core_sha=$(short_sha "$CORE")
extra_sha=$(short_sha "$EXTRA")
@@ -229,6 +240,8 @@ subst() {
-v newsfile="$tmp/news.html" \
-v brieffile="$tmp/news-brief.html" \
-v infofile="$tmp/news-info.html" \
+ -v pagerfile="$tmp/pager.html" \
+ -v pagetitle="$pagetitle" \
-v root="$root" \
-v tag="$tag" \
-v date="$date" \
@@ -247,7 +260,26 @@ subst site/packages/index.html "$OUT/packages/index.html" "../"
subst site/install/index.html "$OUT/install/index.html" "../"
subst site/docs/index.html "$OUT/docs/index.html" "../"
subst site/source/index.html "$OUT/source/index.html" "../"
+
+awk -f tools/news.awk -v mode=html -v page=1 -v per="$news_per" \
+ "$tmp/news.tsv" >"$tmp/news.html"
+awk -f tools/news.awk -v mode=pager -v page=1 -v pages="$news_pages" \
+ -v per="$news_per" -v nest=0 "$tmp/news.tsv" >"$tmp/pager.html"
+pagetitle=
subst site/news/index.html "$OUT/news/index.html" "../"
+news_p=2
+while [ "$news_p" -le "$news_pages" ]
+do
+ awk -f tools/news.awk -v mode=html -v page="$news_p" -v per="$news_per" \
+ "$tmp/news.tsv" >"$tmp/news.html"
+ awk -f tools/news.awk -v mode=pager -v page="$news_p" -v pages="$news_pages" \
+ -v per="$news_per" -v nest=1 "$tmp/news.tsv" >"$tmp/pager.html"
+ pagetitle=" page $news_p"
+ subst site/news/index.html "$OUT/news/$news_p/index.html" "../../"
+ news_p=$((news_p + 1))
+done
+pagetitle=
+: >"$tmp/pager.html"
# 404 must use an absolute prefix so nested missing URLs still load CSS.
if [ -n "$SITE_PREFIX" ]; then
diff --git a/tools/collect-news.sh b/tools/collect-news.sh
index 6038e1c..11ffa3f 100755
--- a/tools/collect-news.sh
+++ b/tools/collect-news.sh
@@ -1,9 +1,10 @@
#!/bin/sh
# Collect Splux releases and git history into TSV for the news page.
-# Fields: epoch iso kind repo url title summary
+# Fields: epoch iso kind repo url title summary author verified
# Env: CORE EXTRA SPS SITE (git work trees). GH_TOKEN optional via gh.
# Pulls every non-draft SPS release and every commit available from the
-# local clones and from the GitHub API, then drops duplicate URLs.
+# local clones and from the GitHub API. Duplicate URLs keep the last row
+# so GitHub author and verified data win over local git.
set -eu
@@ -26,10 +27,18 @@ emit() {
url=$5
title=$6
summary=${7-}
+ author=${8-}
+ verified=${9-}
title=$(printf '%s' "$title" | tr '\t\r\n' ' ')
summary=$(printf '%s' "$summary" | tr '\t\r\n' ' ')
- printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
- "$epoch" "$iso" "$kind" "$repo" "$url" "$title" "$summary" >>"$out"
+ author=$(printf '%s' "$author" | tr '\t\r\n' ' ')
+ case $verified in
+ yes|no) ;;
+ *) verified= ;;
+ esac
+ printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
+ "$epoch" "$iso" "$kind" "$repo" "$url" "$title" "$summary" \
+ "$author" "$verified" >>"$out"
}
epoch_of() {
@@ -51,27 +60,28 @@ git_commits() {
repo=$2
baseurl=$3
git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
- git -C "$dir" log --format='%ct%x09%cI%x09%H%x09%s' 2>/dev/null |
- while IFS=$tab read -r epoch iso hash subject || [ -n "${epoch-}" ]
+ git -C "$dir" log --format='%ct%x09%cI%x09%H%x09%an%x09%s' 2>/dev/null |
+ while IFS=$tab read -r epoch iso hash author subject || [ -n "${epoch-}" ]
do
[ -n "${epoch-}" ] || continue
[ -n "${iso-}" ] || continue
[ -n "${hash-}" ] || continue
emit "$epoch" "$iso" commit "$repo" \
- "$baseurl/commit/$hash" "$subject" ""
+ "$baseurl/commit/$hash" "$subject" "" "$author" ""
done
}
read_commit_tsv() {
repo=$1
file=$2
- while IFS=$tab read -r published url title || [ -n "${published-}" ]
+ while IFS=$tab read -r published url title author verified || [ -n "${published-}" ]
do
[ -n "${published-}" ] || continue
[ -n "${url-}" ] || continue
[ -n "${title-}" ] || title=$url
epoch=$(epoch_of "$published")
- emit "$epoch" "$published" commit "$repo" "$url" "$title" ""
+ emit "$epoch" "$published" commit "$repo" "$url" "$title" "" \
+ "$author" "$verified"
done <"$file"
}
@@ -86,15 +96,17 @@ if command -v gh >/dev/null 2>&1; then
.published_at,
.html_url,
((.name // .tag_name) | gsub("[\r\t]"; " ")),
- ((.body // "") | gsub("[\r\t]"; " ") | gsub("\n+"; " ") | .[0:900])
+ ((.body // "") | gsub("[\r\t]"; " ") | gsub("\n+"; " ") | .[0:900]),
+ ((.author.login // "") | gsub("[\r\t]"; " "))
] | @tsv' >"$tmp/rel"
then
- while IFS=$tab read -r published url name body || [ -n "${published-}" ]
+ while IFS=$tab read -r published url name body author || [ -n "${published-}" ]
do
[ -n "${published-}" ] || continue
[ -n "${url-}" ] || continue
epoch=$(epoch_of "$published")
- emit "$epoch" "$published" release SPS "$url" "$name" "$body"
+ emit "$epoch" "$published" release SPS "$url" "$name" "$body" \
+ "$author" ""
done <"$tmp/rel"
else
printf '%s\n' "collect-news: GitHub releases request failed" >&2
@@ -107,7 +119,9 @@ if command -v gh >/dev/null 2>&1; then
'.[] | [
.commit.committer.date,
.html_url,
- ((.commit.message // "") | split("\n")[0] | gsub("[\r\t]"; " "))
+ ((.commit.message // "") | split("\n")[0] | gsub("[\r\t]"; " ")),
+ ((.author.login // .commit.author.name // "") | gsub("[\r\t]"; " ")),
+ (if .commit.verification.verified == true then "yes" else "no" end)
] | @tsv' >"$tmp/c"
then
read_commit_tsv "$label" "$tmp/c"
@@ -122,5 +136,15 @@ if command -v gh >/dev/null 2>&1; then
api_commits RobertFlexx/splux-site splux-site
fi
-LC_ALL=C sort -t "$tab" -k1,1nr "$out" |
-awk -F '\t' 'NF >= 5 && $5 != "" && !seen[$5]++'
+awk -F '\t' '
+NF >= 5 && $5 != "" {
+ row[$5] = $0
+ if (!seen[$5]++) {
+ n++
+ urls[n] = $5
+ }
+}
+END {
+ for (i = 1; i <= n; i++)
+ print row[urls[i]]
+}' "$out" | LC_ALL=C sort -t "$tab" -k1,1nr
diff --git a/tools/news.awk b/tools/news.awk
index cae80f8..af2d0c1 100644
--- a/tools/news.awk
+++ b/tools/news.awk
@@ -1,6 +1,7 @@
-# Turn collect-news.sh TSV into HTML or Atom.
-# Fields: epoch iso kind repo url title summary
-# -v mode=html|brief|atom -v siteurl= -v feedupdated=
+# Turn collect-news.sh TSV into HTML, a pager, JSON, or Atom.
+# Fields: epoch iso kind repo url title summary author verified
+# -v mode=html|brief|atom|json|pager
+# -v page= -v per= -v pages= -v nest=0|1 -v siteurl= -v feedupdated=
BEGIN {
FS = "\t"
@@ -8,6 +9,13 @@ BEGIN {
mode = "html"
if (siteurl == "")
siteurl = "https://splux.robertflexx.dev"
+ per = per + 0
+ if (per < 1)
+ per = 20
+ page = page + 0
+ if (page < 1)
+ page = 1
+ nest = nest + 0
html_limit = (mode == "brief") ? 8 : 10000
atom_limit = 200
}
@@ -20,10 +28,21 @@ function esc(s) {
return s
}
-function day_of(iso, d) {
- d = iso
- sub(/T.*/, "", d)
- return d
+function jesc(s) {
+ gsub(/\\/, "\\\\", s)
+ gsub(/"/, "\\\"", s)
+ gsub(/\t/, " ", s)
+ return s
+}
+
+function utc_label(iso, s) {
+ s = iso
+ sub(/T/, " ", s)
+ sub(/:[0-9][0-9](\.[0-9]+)?Z$/, " UTC", s)
+ sub(/Z$/, " UTC", s)
+ if (s == "")
+ return iso
+ return s
}
function kind_label(k) {
@@ -32,6 +51,17 @@ function kind_label(k) {
return "commit"
}
+function page_href(p) {
+ if (nest) {
+ if (p <= 1)
+ return "../"
+ return "../" p "/"
+ }
+ if (p <= 1)
+ return "./"
+ return p "/"
+}
+
{
n++
epoch[n] = $1
@@ -41,6 +71,8 @@ function kind_label(k) {
url[n] = $5
title[n] = $6
summary[n] = $7
+ author[n] = $8
+ verified[n] = $9
}
END {
@@ -48,26 +80,52 @@ END {
print_atom()
exit
}
+ if (mode == "json") {
+ print_json()
+ exit
+ }
+ if (mode == "pager") {
+ print_pager()
+ exit
+ }
if (n == 0) {
print "<p class=\"note\">No GitHub events were available when this page was built.</p>"
exit
}
print "<div class=\"feed\">"
- limit = html_limit
- if (n < limit)
- limit = n
- for (i = 1; i <= limit; i++) {
- print_item(i, mode == "brief")
+ if (mode == "brief") {
+ limit = html_limit
+ if (n < limit)
+ limit = n
+ for (i = 1; i <= limit; i++)
+ print_item(i, 1)
+ } else {
+ start = (page - 1) * per + 1
+ stop = start + per - 1
+ if (start < 1)
+ start = 1
+ if (stop > n)
+ stop = n
+ for (i = start; i <= stop; i++)
+ print_item(i, 0)
}
print "</div>"
}
-function print_item(i, brief, k, when, s) {
- when = esc(day_of(iso[i]))
+function print_item(i, brief, k, when, s, v, a) {
+ when = esc(utc_label(iso[i]))
k = esc(kind_label(kind[i]))
- print "<article class=\"item\">"
- printf "<p class=\"meta\"><time datetime=\"%s\">%s</time> <span class=\"kind\">%s</span> %s</p>\n", \
+ a = author[i]
+ v = verified[i]
+ printf "<article class=\"item\" data-kind=\"%s\" data-repo=\"%s\" data-author=\"%s\" data-verified=\"%s\">\n", \
+ k, esc(repo[i]), esc(a), esc(v)
+ printf "<p class=\"meta\"><time datetime=\"%s\">%s</time> <span class=\"kind\">%s</span> %s", \
esc(iso[i]), when, k, esc(repo[i])
+ if (a != "")
+ printf " <span class=\"who\">%s</span>", esc(a)
+ if (v == "yes")
+ printf " <span class=\"verified\">verified</span>"
+ print "</p>"
printf "<h3><a href=\"%s\">%s</a></h3>\n", esc(url[i]), esc(title[i])
if (!brief) {
s = summary[i]
@@ -77,7 +135,80 @@ function print_item(i, brief, k, when, s) {
print "</article>"
}
-function print_atom( i, limit, updated, id) {
+function print_pager( i, lo, hi, prev, shown, need_dots) {
+ pages = pages + 0
+ if (pages < 1)
+ pages = 1
+ if (page < 1)
+ page = 1
+ if (page > pages)
+ page = pages
+ if (pages <= 1) {
+ printf "<nav class=\"pager\" id=\"news-pager\" hidden data-page=\"1\" data-pages=\"1\" data-per=\"%d\" aria-label=\"News pages\"></nav>\n", per
+ return
+ }
+ lo = page - 2
+ hi = page + 2
+ if (lo < 1) {
+ hi += (1 - lo)
+ lo = 1
+ }
+ if (hi > pages) {
+ lo -= (hi - pages)
+ hi = pages
+ }
+ if (lo < 1)
+ lo = 1
+ printf "<nav class=\"pager\" id=\"news-pager\" data-page=\"%d\" data-pages=\"%d\" data-per=\"%d\" aria-label=\"News pages\">\n", \
+ page, pages, per
+ printf "<p class=\"pager-status\">Page %d of %d</p>\n", page, pages
+ print "<p class=\"pager-links\">"
+ if (page > 1)
+ printf "<a rel=\"prev\" href=\"%s\">prev</a>\n", esc(page_href(page - 1))
+ need_dots = 0
+ for (i = 1; i <= pages; i++) {
+ shown = (i == 1 || i == pages || (i >= lo && i <= hi))
+ if (!shown) {
+ need_dots = 1
+ continue
+ }
+ if (need_dots) {
+ print "<span class=\"pager-gap\">...</span>"
+ need_dots = 0
+ }
+ if (i == page)
+ printf "<a href=\"%s\" aria-current=\"page\">%d</a>\n", \
+ esc(page_href(i)), i
+ else
+ printf "<a href=\"%s\">%d</a>\n", esc(page_href(i)), i
+ }
+ if (page < pages)
+ printf "<a rel=\"next\" href=\"%s\">next</a>\n", esc(page_href(page + 1))
+ print "</p>"
+ print "<form class=\"pager-jump\" id=\"news-jump\" action=\"#\">"
+ printf "<label for=\"news-page-n\">Go to page</label>\n"
+ printf "<input id=\"news-page-n\" name=\"n\" type=\"number\" min=\"1\" max=\"%d\" value=\"%d\" inputmode=\"numeric\">\n", \
+ pages, page
+ print "<button type=\"submit\">Go</button>"
+ print "</form>"
+ print "</nav>"
+}
+
+function print_json( i, v) {
+ print "["
+ for (i = 1; i <= n; i++) {
+ if (i > 1)
+ print ","
+ v = (verified[i] == "yes") ? "true" : "false"
+ printf "{\"iso\":\"%s\",\"kind\":\"%s\",\"repo\":\"%s\",\"url\":\"%s\",\"title\":\"%s\",\"summary\":\"%s\",\"author\":\"%s\",\"verified\":%s}", \
+ jesc(iso[i]), jesc(kind_label(kind[i])), jesc(repo[i]), \
+ jesc(url[i]), jesc(title[i]), jesc(summary[i]), \
+ jesc(author[i]), v
+ }
+ print "\n]"
+}
+
+function print_atom( i, limit, updated) {
updated = feedupdated
if (updated == "" && n > 0)
updated = iso[1]
diff --git a/tools/subst.awk b/tools/subst.awk
index 0b49d9c..e1890a0 100644
--- a/tools/subst.awk
+++ b/tools/subst.awk
@@ -58,6 +58,11 @@ $0 == "@@NEWS_INFO@@" {
next
}
+$0 == "@@PAGER@@" {
+ dump_file(pagerfile)
+ next
+}
+
{
gsub(/../../../..//, root)
gsub(/live-2026-09-06-1/, tag)
@@ -67,5 +72,6 @@ $0 == "@@NEWS_INFO@@" {
gsub(/1355/, nextra)
gsub(/2026-10-19 13:01 UTC/, generated)
gsub(/8.21.0-2/, curlver)
+ gsub(//, pagetitle)
print
}