splux-site · log · files · refs · tags · releases · people · languages · GitHub · commit
fb98907
Add tag and release pages, language highlighting, and Markdown rendering.
Parents
Stat
README.md | 22 +- site/404.html | 3 +- site/assets/git.js | 595 ++++++++++++++++++++++- site/assets/render.js | 1017 ++++++++++++++++++++++++++++++++++++++ site/assets/style.css | 127 +++++ tools/build-git.sh | 615 ++++++++++++++++++++++- tools/git-lang.awk | 8 + tools/git.awk | 280 +++++++++++ tools/render.awk | 1294 +++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 3909 insertions(+), 52 deletions(-)
Diff
diff --git a/README.md b/README.md
index ac4a92e..1281751 100644
--- a/README.md
+++ b/README.md
@@ -79,12 +79,22 @@ repository there. `SITE_PREFIX` stays empty so links are rooted at `/`.
`/git/` is a live HTML mirror of SPS, sps-core, sps-extra, and this
repository. Languages match GitHub Linguist. Open a language to see
-how many files use it (`/git/<repo>/lang/<Lang>/`). Commits show the
-GitHub account that uploaded them, with avatars. In a repository,
-People lists that tree's commit authors (`/git/<repo>/people/`).
-`/git/users/` holds profiles across all official trees. The Pages
-workflow rebuilds those pages; the browser also asks GitHub for
-anything newer. There is no git server and no pull requests. Clone
+how many files use it (`/git/<repo>/lang/<Lang>/`). Source files are
+highlighted per language. Markdown files, README notes, and GitHub
+release bodies render as HTML. Commits show the GitHub account that
+uploaded them, with avatars. In a repository, People lists that tree's
+commit authors (`/git/<repo>/people/`). `/git/users/` holds profiles
+across all official trees.
+
+Tags are `/git/<repo>/tags/` and `/git/<repo>/tags/<name>/`. Releases
+are `/git/<repo>/releases/`, `/git/<repo>/releases/tag/<name>/`, and
+`/git/<repo>/releases/latest/`. Those pages follow GitHub's lists and
+notes, with this site's URLs and layout. Source zip and tar.gz links
+point at GitHub. ISO files and other release assets stay on GitHub
+Releases.
+
+The Pages workflow rebuilds those pages; the browser also asks GitHub
+for anything newer. There is no git server and no pull requests. Clone
from GitHub:
```sh
diff --git a/site/404.html b/site/404.html
index dfaaef5..6aea9e2 100644
--- a/site/404.html
+++ b/site/404.html
@@ -13,11 +13,12 @@
<main id="main" class="git-page">
<h2>404</h2>
<hr class="rule">
-<p>The requested page does not exist. If this is a new git commit, file, or account, the page may fill in from GitHub.</p>
+<p>The requested page does not exist. If this is a new git commit, file, tag, release, or account, the page may fill in from GitHub.</p>
<p><a href="../../../../">home</a> · <a href="../../../../docs/">handbook</a> · <a href="../../../../git/">git</a> · <a href="../../../../install/">install</a></p>
</main>
@@FOOTER@@
</div>
+<script src="../../../../assets/render.js?v=4c4f12c-20261019130111"></script>
<script src="../../../../assets/git.js?v=4c4f12c-20261019130111" data-root="../../../../" data-git="https://splux.robertflexx.dev/git"></script>
</body>
</html>
diff --git a/site/assets/git.js b/site/assets/git.js
index d8fff6d..2e2b9e7 100644
--- a/site/assets/git.js
+++ b/site/assets/git.js
@@ -93,13 +93,18 @@
function colorsReady() {
if (Object.keys(COLORS).length)
return Promise.resolve(COLORS);
- return fetch(dataRoot + "data/linguist-colors.json", { cache: "force-cache" })
- .then(function (r) { return r.ok ? r.json() : {}; })
- .then(function (c) {
- COLORS = c || {};
- return COLORS;
- })
- .catch(function () { return COLORS; });
+ return Promise.all([
+ fetch(dataRoot + "data/linguist-colors.json", { cache: "force-cache" })
+ .then(function (r) { return r.ok ? r.json() : {}; }),
+ fetch(dataRoot + "data/linguist-fams.json", { cache: "force-cache" })
+ .then(function (r) { return r.ok ? r.json() : {}; })
+ .catch(function () { return {}; })
+ ]).then(function (rows) {
+ COLORS = rows[0] || {};
+ if (window.SpluxRender && SpluxRender.setFams)
+ SpluxRender.setFams(rows[1] || {});
+ return COLORS;
+ }).catch(function () { return COLORS; });
}
function colorOf(name) {
@@ -136,6 +141,18 @@
out.kind = "langs";
else
out.path = parts.slice(3).join("/");
+ } else if (out.kind === "tags") {
+ if (parts.length > 3) {
+ out.kind = "tag";
+ out.path = decodeSeg(parts.slice(3).join("/"));
+ }
+ } else if (out.kind === "releases") {
+ if (parts[3] === "latest")
+ out.kind = "latest";
+ else if (parts[3] === "tag" && parts[4]) {
+ out.kind = "release";
+ out.path = decodeSeg(parts.slice(4).join("/"));
+ }
}
return out;
}
@@ -178,6 +195,104 @@
encodeURIComponent(sha) + "/";
}
+ function decodeSeg(s) {
+ s = String(s || "");
+ try {
+ return decodeURIComponent(s);
+ } catch (e) {
+ return s;
+ }
+ }
+
+ function navHtml(repo, here) {
+ var r = repoHref(repo);
+ var bits = [];
+ if (here === "home")
+ bits.push(esc(repo));
+ else
+ bits.push("<a href=\"" + esc(r) + "\">" + esc(repo) + "</a>");
+ bits.push(here === "log"
+ ? "log"
+ : "<a href=\"" + esc(r + "log/") + "\">log</a>");
+ bits.push(here === "files"
+ ? "files"
+ : "<a href=\"" + esc(r + "tree/") + "\">files</a>");
+ bits.push(here === "refs"
+ ? "refs"
+ : "<a href=\"" + esc(r + "refs/") + "\">refs</a>");
+ bits.push(here === "tags" || here === "tag"
+ ? "tags"
+ : "<a href=\"" + esc(r + "tags/") + "\">tags</a>");
+ bits.push(here === "releases" || here === "release" || here === "latest"
+ ? "releases"
+ : "<a href=\"" + esc(r + "releases/") + "\">releases</a>");
+ bits.push(here === "people"
+ ? "people"
+ : "<a href=\"" + esc(r + "people/") + "\">people</a>");
+ bits.push(here === "langs" || here === "lang"
+ ? "languages"
+ : "<a href=\"" + esc(r + "lang/") + "\">languages</a>");
+ bits.push("<a href=\"https://github.com/" + OWNER + "/" +
+ encodeURIComponent(repo) + "\">GitHub</a>");
+ if (here === "commit")
+ bits.push("commit");
+ if (here === "blob")
+ bits.push("file");
+ if (here === "tag")
+ bits.push("tag");
+ if (here === "release")
+ bits.push("release");
+ if (here === "latest")
+ bits.push("latest");
+ return "<p class=\"git-nav\">" + bits.join(" · ") + "</p>";
+ }
+
+ function tagHref(repo, name) {
+ return repoHref(repo) + "tags/" + encodeURIComponent(name) + "/";
+ }
+
+ function releaseHref(repo, name) {
+ return repoHref(repo) + "releases/tag/" + encodeURIComponent(name) + "/";
+ }
+
+ function archiveHref(repo, tag, kind) {
+ return "https://github.com/" + OWNER + "/" + encodeURIComponent(repo) +
+ "/archive/refs/tags/" + encodeURIComponent(tag) + "." + kind;
+ }
+
+ function humansize(n) {
+ n = Number(n) || 0;
+ if (n >= 1073741824)
+ return (n / 1073741824).toFixed(1) + " GB";
+ if (n >= 1048576) {
+ var mb = n / 1048576;
+ return (mb >= 10 ? mb.toFixed(0) : mb.toFixed(1)) + " MB";
+ }
+ if (n >= 1024)
+ return (n / 1024).toFixed(0) + " KB";
+ if (n > 0)
+ return String(n) + " B";
+ return "0 B";
+ }
+
+ function renderText(text, lang, path) {
+ if (window.SpluxRender) {
+ if (SpluxRender.isMarkdown(lang, path))
+ return SpluxRender.markdown(text);
+ return SpluxRender.highlight(text,
+ lang || SpluxRender.langFromPath(path) || "");
+ }
+ return "<pre class=\"block git-blob\">" + esc(text) + "</pre>";
+ }
+
+ function sourceArchivesHtml(repo, tag) {
+ return "<h3>Source</h3><ul class=\"plain\">" +
+ "<li><a href=\"" + esc(archiveHref(repo, tag, "zip")) + "\">" +
+ esc(tag) + ".zip</a></li>" +
+ "<li><a href=\"" + esc(archiveHref(repo, tag, "tar.gz")) + "\">" +
+ esc(tag) + ".tar.gz</a></li></ul>";
+ }
+
function repoHref(repo) {
return gitHost + "/" + encodeURIComponent(repo) + "/";
}
@@ -344,19 +459,34 @@
var table = document.getElementById("git-repos") ||
document.getElementById("git-people-table") ||
document.getElementById("git-lang-table") ||
+ document.getElementById("git-tag-table") ||
+ document.getElementById("git-asset-table") ||
document.querySelector("table.git-tree") ||
document.querySelector("table.git-log") ||
document.querySelector("table.pkgs");
- if (!table)
+ var list = document.getElementById("git-releases");
+ if (!table && !list)
return;
function apply() {
var q = (input.value || "").toLowerCase();
- var rows = table.querySelectorAll("tbody tr");
- for (var i = 0; i < rows.length; i++) {
- var hay = rows[i].getAttribute("data-search") ||
- rows[i].textContent || "";
- rows[i].hidden = q !== "" &&
- hay.toLowerCase().indexOf(q) === -1;
+ var i;
+ if (table) {
+ var rows = table.querySelectorAll("tbody tr");
+ for (i = 0; i < rows.length; i++) {
+ var hay = rows[i].getAttribute("data-search") ||
+ rows[i].textContent || "";
+ rows[i].hidden = q !== "" &&
+ hay.toLowerCase().indexOf(q) === -1;
+ }
+ }
+ if (list) {
+ var arts = list.querySelectorAll("article.release");
+ for (i = 0; i < arts.length; i++) {
+ var hay2 = arts[i].getAttribute("data-search") ||
+ arts[i].textContent || "";
+ arts[i].hidden = q !== "" &&
+ hay2.toLowerCase().indexOf(q) === -1;
+ }
}
}
input.addEventListener("input", apply);
@@ -502,9 +632,7 @@
var it = fromCommit(c, info.repo);
var parents = c.parents || [];
var files = c.files || [];
- var html = "<p class=\"git-nav\"><a href=\"" + esc(repoHref(info.repo)) +
- "\">" + esc(info.repo) + "</a> · <a href=\"" +
- esc(repoHref(info.repo) + "log/") + "\">log</a> · commit</p>";
+ var html = navHtml(info.repo, "commit");
html += "<h2><code>" + esc(it.short) + "</code></h2><hr class=\"rule\">";
html += "<p>" + esc(it.subject) + "</p>";
html += "<div class=\"info\">";
@@ -534,23 +662,35 @@
if (c.stats)
html += "<p class=\"muted\">+" + esc(String(c.stats.additions || 0)) +
" / -" + esc(String(c.stats.deletions || 0)) + "</p>";
+ var patch = "";
+ for (var k = 0; k < files.length; k++) {
+ if (files[k].patch)
+ patch += "diff --git a/" + (files[k].filename || "") +
+ " b/" + (files[k].filename || "") + "\n" +
+ files[k].patch + "\n";
+ }
+ if (patch)
+ html += "<h3>Diff</h3>" + renderText(patch, "Diff", "commit.diff");
main.innerHTML = html;
document.title = it.short + " - " + info.repo + " - Splux Git";
+ localizeTimes(main);
}
function paintBlob(info, item) {
if (!main || !item)
return;
- var html = "<p class=\"git-nav\"><a href=\"" + esc(repoHref(info.repo)) +
- "\">" + esc(info.repo) + "</a> · file</p>";
+ var html = navHtml(info.repo, "blob");
html += "<h2>" + esc(info.path) + "</h2><hr class=\"rule\">";
if (item.html_url)
html += "<p class=\"muted\"><a href=\"" + esc(item.html_url) +
"\">GitHub</a></p>";
if (item.type === "file") {
var text = decodeContent(item);
+ var lang = (window.SpluxRender && SpluxRender.langFromPath(info.path)) || "";
+ if (lang)
+ html += "<p class=\"lang-badge\">" + esc(lang) + "</p>";
if (text)
- html += "<pre class=\"block git-blob\">" + esc(text) + "</pre>";
+ html += renderText(text, lang, info.path);
else
html += "<p>Open the GitHub mirror for this file.</p>";
} else {
@@ -567,8 +707,7 @@
paintBlob(info, list);
return;
}
- var html = "<p class=\"git-nav\"><a href=\"" + esc(repoHref(info.repo)) +
- "\">" + esc(info.repo) + "</a> · files</p>";
+ var html = navHtml(info.repo, "files");
html += "<h2>" + esc(info.path || "Files") + "</h2><hr class=\"rule\">";
html += "<table class=\"pkgs git-tree\"><thead><tr><th></th><th>Name</th><th>Size</th></tr></thead><tbody>";
for (var i = 0; i < list.length; i++) {
@@ -630,6 +769,284 @@
document.title = login + " - Splux Git";
}
+ function fetchTags(repo) {
+ return cached("git-tags-" + repo, API + "/repos/" + OWNER + "/" +
+ repo + "/tags?per_page=100");
+ }
+
+ function fetchReleases(repo) {
+ return cached("git-releases-" + repo, API + "/repos/" + OWNER + "/" +
+ repo + "/releases?per_page=100").then(function (list) {
+ var out = [];
+ for (var i = 0; i < (list || []).length; i++) {
+ if (list[i] && !list[i].draft)
+ out.push(list[i]);
+ }
+ return out;
+ });
+ }
+
+ function fetchLatestRelease(repo) {
+ return cached("git-latest-" + repo, API + "/repos/" + OWNER + "/" +
+ repo + "/releases/latest");
+ }
+
+ function fetchReleaseByTag(repo, tag) {
+ return cached("git-rel-" + repo + "-" + tag, API + "/repos/" + OWNER +
+ "/" + repo + "/releases/tags/" + encodeURIComponent(tag));
+ }
+
+ function tagRow(it, repo, relset) {
+ var name = it.name || "";
+ var sha = (it.commit && it.commit.sha) || it.sha || "";
+ var short = sha.slice(0, 7);
+ var hasRel = relset && relset[name];
+ var search = name.toLowerCase();
+ var html = "<tr data-tag=\"" + esc(name) + "\" data-sha=\"" +
+ esc(sha) + "\" data-search=\"" + esc(search) + "\">";
+ html += "<td><a href=\"" + esc(tagHref(repo, name)) +
+ "\"><code>" + esc(name) + "</code></a></td>";
+ html += "<td>";
+ if (sha)
+ html += "<a href=\"" + esc(commitHref(repo, sha)) +
+ "\"><code>" + esc(short) + "</code></a>";
+ html += "</td><td></td><td></td><td>";
+ if (hasRel)
+ html += "<a href=\"" + esc(releaseHref(repo, name)) +
+ "\">release</a>";
+ html += "</td></tr>";
+ return html;
+ }
+
+ function mergeTagRows(table, tags, repo, relset) {
+ if (!table || !tags || !tags.length)
+ return 0;
+ var body = table.tBodies[0] || table;
+ var seen = {};
+ var existing = body.querySelectorAll("tr[data-tag]");
+ var i;
+ for (i = 0; i < existing.length; i++)
+ seen[existing[i].getAttribute("data-tag")] = true;
+ var added = 0;
+ var html = "";
+ for (i = 0; i < tags.length; i++) {
+ var name = tags[i] && tags[i].name;
+ if (!name || seen[name])
+ continue;
+ seen[name] = true;
+ html += tagRow(tags[i], repo, relset);
+ added++;
+ }
+ if (html) {
+ body.insertAdjacentHTML("afterbegin", html);
+ localizeTimes(body);
+ }
+ return added;
+ }
+
+ function releaseArticle(rel, repo, latestTag) {
+ var tag = rel.tag_name || "";
+ var title = rel.name || tag;
+ var login = (rel.author && rel.author.login) || "";
+ var avatar = (rel.author && rel.author.avatar_url) || "";
+ var iso = rel.published_at || rel.created_at || "";
+ var search = (tag + " " + title + " " + login).toLowerCase();
+ var nassets = (rel.assets && rel.assets.length) || 0;
+ var react = (rel.reactions && rel.reactions.total_count) || 0;
+ var html = "<article class=\"release\" data-tag=\"" + esc(tag) +
+ "\" data-search=\"" + esc(search) + "\">";
+ html += "<p class=\"meta\">";
+ if (latestTag && tag === latestTag)
+ html += "<span class=\"badge latest\">Latest</span> ";
+ if (rel.prerelease)
+ html += "<span class=\"badge\">Pre-release</span> ";
+ if (iso)
+ html += "<time datetime=\"" + esc(iso) + "\">" +
+ esc(dayOf(iso)) + "</time> ";
+ html += whoHtml(login, login, avatar) + "</p>";
+ html += "<h3><a href=\"" + esc(releaseHref(repo, tag)) + "\">" +
+ esc(title) + "</a></h3>";
+ html += "<p class=\"muted\"><a href=\"" + esc(tagHref(repo, tag)) +
+ "\"><code>" + esc(tag) + "</code></a>";
+ if (nassets)
+ html += " · " + nassets + " files";
+ if (react)
+ html += " · " + react + " reactions";
+ html += "</p>";
+ if (rel.body) {
+ var excerpt = String(rel.body).replace(/\s+/g, " ");
+ if (excerpt.length > 400)
+ excerpt = excerpt.slice(0, 400) + "...";
+ html += "<p class=\"summary\">" + esc(excerpt) + "</p>";
+ }
+ html += "</article>";
+ return html;
+ }
+
+ function mergeReleaseList(root, releases, repo, latestTag) {
+ if (!root || !releases || !releases.length)
+ return 0;
+ var seen = {};
+ var existing = root.querySelectorAll("article.release[data-tag]");
+ var i;
+ for (i = 0; i < existing.length; i++)
+ seen[existing[i].getAttribute("data-tag")] = true;
+ var added = 0;
+ var html = "";
+ for (i = 0; i < releases.length; i++) {
+ var tag = releases[i] && releases[i].tag_name;
+ if (!tag || seen[tag])
+ continue;
+ seen[tag] = true;
+ html += releaseArticle(releases[i], repo, latestTag);
+ added++;
+ }
+ if (html) {
+ root.insertAdjacentHTML("afterbegin", html);
+ localizeTimes(root);
+ }
+ return added;
+ }
+
+ function paintTags(info, tags, releases) {
+ if (!main)
+ return;
+ var relset = {};
+ var i;
+ for (i = 0; releases && i < releases.length; i++) {
+ if (releases[i] && releases[i].tag_name)
+ relset[releases[i].tag_name] = true;
+ }
+ var html = navHtml(info.repo, "tags");
+ html += "<h2>Tags</h2><hr class=\"rule\">";
+ html += "<p>Every tag in this tree. A tag with a GitHub release links through to that page.</p>";
+ html += "<p><label for=\"git-filter\">Filter</label> <input id=\"git-filter\" type=\"search\" placeholder=\"tag\"></p>";
+ if (!tags || !tags.length)
+ html += "<p class=\"muted\">No tags in this tree.</p>";
+ else {
+ html += "<table class=\"pkgs git-tags\" id=\"git-tag-table\"><thead><tr><th>Tag</th><th>Commit</th><th>When</th><th>Message</th><th></th></tr></thead><tbody>";
+ for (i = 0; i < tags.length; i++)
+ html += tagRow(tags[i], info.repo, relset);
+ html += "</tbody></table>";
+ }
+ main.innerHTML = html;
+ document.title = "Tags - " + info.repo + " - Splux Git";
+ bindFilter();
+ }
+
+ function paintTag(info, tagObj, releases) {
+ if (!main)
+ return;
+ var name = info.path || (tagObj && tagObj.name) || "";
+ var sha = (tagObj && tagObj.commit && tagObj.commit.sha) ||
+ (tagObj && tagObj.sha) || "";
+ var rel = null;
+ var i;
+ for (i = 0; releases && i < releases.length; i++) {
+ if (releases[i] && releases[i].tag_name === name) {
+ rel = releases[i];
+ break;
+ }
+ }
+ var html = navHtml(info.repo, "tag");
+ html += "<h2><code>" + esc(name) + "</code></h2><hr class=\"rule\">";
+ html += "<div class=\"info\">";
+ if (sha)
+ html += "<div class=\"dl-row\"><span class=\"muted\">Commit</span><span><a href=\"" +
+ esc(commitHref(info.repo, sha)) + "\"><code>" +
+ esc(sha.slice(0, 7)) + "</code></a></span></div>";
+ if (rel)
+ html += "<div class=\"dl-row\"><span class=\"muted\">Release</span><span><a href=\"" +
+ esc(releaseHref(info.repo, name)) + "\">" +
+ esc(name) + "</a></span></div>";
+ html += "<div class=\"dl-row\"><span class=\"muted\">GitHub</span><span><a href=\"https://github.com/" +
+ OWNER + "/" + encodeURIComponent(info.repo) + "/releases/tag/" +
+ encodeURIComponent(name) + "\">mirror</a></span></div></div>";
+ html += sourceArchivesHtml(info.repo, name);
+ main.innerHTML = html;
+ document.title = name + " - " + info.repo + " - Splux Git";
+ }
+
+ function assetsTable(assets) {
+ if (!assets || !assets.length)
+ return "";
+ var html = "<h3>Assets</h3><table class=\"pkgs git-assets\" id=\"git-asset-table\"><thead><tr><th>File</th><th>Size</th><th>Downloads</th></tr></thead><tbody>";
+ for (var i = 0; i < assets.length; i++) {
+ var a = assets[i];
+ html += "<tr data-search=\"" + esc((a.name || "").toLowerCase()) + "\">";
+ html += "<td><a href=\"" + esc(a.browser_download_url || "") + "\">" +
+ esc(a.name || "") + "</a></td>";
+ html += "<td class=\"muted\">" + esc(humansize(a.size)) + "</td>";
+ html += "<td class=\"muted\">" + esc(String(a.download_count || 0)) +
+ "</td></tr>";
+ }
+ html += "</tbody></table>";
+ return html;
+ }
+
+ function paintRelease(info, rel, latestTag, here) {
+ if (!main || !rel)
+ return;
+ var tag = rel.tag_name || info.path || "";
+ var title = rel.name || tag;
+ var login = (rel.author && rel.author.login) || "";
+ var avatar = (rel.author && rel.author.avatar_url) || "";
+ var iso = rel.published_at || rel.created_at || "";
+ var html = navHtml(info.repo, here || "release");
+ html += "<h2>" + esc(title) + "</h2><hr class=\"rule\">";
+ html += "<p class=\"meta\">";
+ if (latestTag && tag === latestTag)
+ html += "<span class=\"badge latest\">Latest</span> ";
+ if (rel.prerelease)
+ html += "<span class=\"badge\">Pre-release</span> ";
+ html += "</p><div class=\"info\">";
+ html += "<div class=\"dl-row\"><span class=\"muted\">Tag</span><span><a href=\"" +
+ esc(tagHref(info.repo, tag)) + "\"><code>" + esc(tag) +
+ "</code></a></span></div>";
+ if (iso)
+ html += "<div class=\"dl-row\"><span class=\"muted\">Date</span><span><time datetime=\"" +
+ esc(iso) + "\">" + esc(formatLocal(iso)) + "</time></span></div>";
+ if (login)
+ html += "<div class=\"dl-row\"><span class=\"muted\">Author</span><span>" +
+ whoHtml(login, login, avatar) + "</span></div>";
+ if (rel.assets && rel.assets.length)
+ html += "<div class=\"dl-row\"><span class=\"muted\">Files</span><span>" +
+ esc(String(rel.assets.length)) + "</span></div>";
+ html += "<div class=\"dl-row\"><span class=\"muted\">GitHub</span><span><a href=\"" +
+ esc(rel.html_url || ("https://github.com/" + OWNER + "/" +
+ info.repo + "/releases/tag/" + encodeURIComponent(tag))) +
+ "\">mirror</a></span></div></div>";
+ if (rel.body)
+ html += "<h3>Notes</h3>" + renderText(rel.body, "Markdown", tag + ".md");
+ html += assetsTable(rel.assets);
+ html += sourceArchivesHtml(info.repo, tag);
+ main.innerHTML = html;
+ document.title = title + " - " + info.repo + " - Splux Git";
+ localizeTimes(main);
+ }
+
+ function paintReleases(info, releases, latestTag) {
+ if (!main)
+ return;
+ var html = navHtml(info.repo, "releases");
+ html += "<h2>Releases</h2><hr class=\"rule\">";
+ html += "<p>GitHub releases for this tree. ISO files and other assets stay on GitHub. Notes are Markdown. <a href=\"" +
+ esc(repoHref(info.repo) + "releases/latest/") + "\">Latest</a></p>";
+ html += "<p><label for=\"git-filter\">Filter</label> <input id=\"git-filter\" type=\"search\" placeholder=\"release\"></p>";
+ if (!releases || !releases.length)
+ html += "<p class=\"muted\">No GitHub releases yet.</p>";
+ else {
+ html += "<div class=\"release-list\" id=\"git-releases\">";
+ for (var i = 0; i < releases.length; i++)
+ html += releaseArticle(releases[i], info.repo, latestTag);
+ html += "</div>";
+ }
+ main.innerHTML = html;
+ document.title = "Releases - " + info.repo + " - Splux Git";
+ bindFilter();
+ localizeTimes(main);
+ }
+
function knownRepo(name) {
for (var i = 0; i < REPOS.length; i++) {
if (REPOS[i] === name)
@@ -646,12 +1063,18 @@
return Promise.all([
fetchLangs(repo).catch(function () { return null; }),
fetchCommits(repo, info.kind === "log" ? 100 : 20).catch(function () { return null; }),
- fetchRepo(repo).catch(function () { return null; })
+ fetchRepo(repo).catch(function () { return null; }),
+ fetchTags(repo).catch(function () { return null; }),
+ fetchReleases(repo).catch(function () { return null; }),
+ fetchLatestRelease(repo).catch(function () { return null; })
]);
}).then(function (rows) {
var langs = rows[0];
var commits = rows[1];
var meta = rows[2];
+ var tags = rows[3];
+ var releases = rows[4];
+ var latest = rows[5];
var changed = 0;
if (langs && langs.length) {
var box = document.getElementById("git-langs");
@@ -677,11 +1100,77 @@
typeof meta.size === "number") {
/* keep baked commit count unless GitHub reports more via commits list */
}
+ if (tags && document.getElementById("git-ntags")) {
+ var ntagsEl = document.getElementById("git-ntags");
+ var ntags = String(tags.length);
+ if (ntagsEl.textContent.replace(/\s/g, "") !== ntags) {
+ ntagsEl.innerHTML = "<a href=\"" +
+ esc(repoHref(repo) + "tags/") + "\">" +
+ esc(ntags) + "</a>";
+ changed++;
+ }
+ }
+ if (releases && document.getElementById("git-nreleases")) {
+ var nrelEl = document.getElementById("git-nreleases");
+ var nrel = String(releases.length);
+ if (nrelEl.textContent.replace(/\s/g, "") !== nrel) {
+ nrelEl.innerHTML = "<a href=\"" +
+ esc(repoHref(repo) + "releases/") + "\">" +
+ esc(nrel) + "</a>";
+ changed++;
+ }
+ }
+ if (latest && latest.tag_name && document.getElementById("git-latest")) {
+ var latestEl = document.getElementById("git-latest");
+ if (latestEl.textContent.replace(/\s/g, "") !== latest.tag_name) {
+ latestEl.innerHTML = "<a href=\"" +
+ esc(repoHref(repo) + "releases/latest/") +
+ "\"><code>" + esc(latest.tag_name) + "</code></a>";
+ changed++;
+ }
+ }
if (changed)
showLive();
});
}
+ function refreshTags(info) {
+ if (!knownRepo(info.repo))
+ return;
+ return Promise.all([
+ fetchTags(info.repo),
+ fetchReleases(info.repo).catch(function () { return []; })
+ ]).then(function (rows) {
+ var tags = rows[0] || [];
+ var releases = rows[1] || [];
+ var relset = {};
+ for (var i = 0; i < releases.length; i++) {
+ if (releases[i] && releases[i].tag_name)
+ relset[releases[i].tag_name] = true;
+ }
+ var table = document.getElementById("git-tag-table");
+ if (table && mergeTagRows(table, tags, info.repo, relset))
+ showLive();
+ });
+ }
+
+ function refreshReleases(info) {
+ if (!knownRepo(info.repo))
+ return;
+ return Promise.all([
+ fetchReleases(info.repo),
+ fetchLatestRelease(info.repo).catch(function () { return null; })
+ ]).then(function (rows) {
+ var releases = rows[0] || [];
+ var latest = rows[1];
+ var latestTag = (latest && latest.tag_name) ||
+ (releases[0] && releases[0].tag_name) || "";
+ var list = document.getElementById("git-releases");
+ if (list && mergeReleaseList(list, releases, info.repo, latestTag))
+ showLive();
+ });
+ }
+
function refreshIndex() {
return Promise.all(REPOS.map(function (repo) {
return fetchRepo(repo).then(function (meta) {
@@ -767,9 +1256,7 @@
repo: info.repo
});
}
- var html = "<p class=\"git-nav\"><a href=\"" +
- esc(repoHref(info.repo)) + "\">" + esc(info.repo) +
- "</a> · people</p>";
+ var html = navHtml(info.repo, "people");
html += "<h2>People</h2><hr class=\"rule\">";
html += "<p>Everyone who authored a commit in " + esc(info.repo) +
". <a href=\"" + esc(gitHost + "/users/") +
@@ -783,6 +1270,53 @@
bindFilter();
});
}
+ if (info.kind === "tags" && knownRepo(info.repo)) {
+ return Promise.all([
+ fetchTags(info.repo),
+ fetchReleases(info.repo).catch(function () { return []; })
+ ]).then(function (rows) {
+ paintTags(info, rows[0], rows[1]);
+ });
+ }
+ if (info.kind === "tag" && knownRepo(info.repo) && info.path) {
+ return Promise.all([
+ fetchTags(info.repo),
+ fetchReleases(info.repo).catch(function () { return []; })
+ ]).then(function (rows) {
+ var tags = rows[0] || [];
+ var found = null;
+ for (var i = 0; i < tags.length; i++) {
+ if (tags[i] && tags[i].name === info.path) {
+ found = tags[i];
+ break;
+ }
+ }
+ paintTag(info, found || { name: info.path }, rows[1]);
+ });
+ }
+ if (info.kind === "releases" && knownRepo(info.repo)) {
+ return Promise.all([
+ fetchReleases(info.repo),
+ fetchLatestRelease(info.repo).catch(function () { return null; })
+ ]).then(function (rows) {
+ var latest = rows[1];
+ paintReleases(info, rows[0], latest && latest.tag_name);
+ });
+ }
+ if ((info.kind === "release" || info.kind === "latest") &&
+ knownRepo(info.repo)) {
+ var work = info.kind === "latest"
+ ? fetchLatestRelease(info.repo)
+ : fetchReleaseByTag(info.repo, info.path);
+ return Promise.all([
+ work,
+ fetchLatestRelease(info.repo).catch(function () { return null; })
+ ]).then(function (rows) {
+ var latest = rows[1];
+ paintRelease(info, rows[0], latest && latest.tag_name,
+ info.kind === "latest" ? "latest" : "release");
+ });
+ }
if (info.kind === "user" && validLogin(info.user)) {
return fetchUser(info.user).then(function (u) {
return Promise.all(REPOS.map(function (repo) {
@@ -819,6 +1353,8 @@
function ready() {
bindFilter();
localizeTimes(document);
+ if (window.SpluxRender && main)
+ SpluxRender.apply(main);
var info = pageInfo();
var work = colorsReady();
if (info.hydrate) {
@@ -834,6 +1370,13 @@
if (info.kind === "people" || info.kind === "repo")
work = work.then(function () { return refreshPeople(info); })
.catch(function () {});
+ if (info.kind === "tags" || info.kind === "tag" || info.kind === "refs")
+ work = work.then(function () { return refreshTags(info); })
+ .catch(function () {});
+ if (info.kind === "releases" || info.kind === "release" ||
+ info.kind === "latest")
+ work = work.then(function () { return refreshReleases(info); })
+ .catch(function () {});
if (info.kind === "user")
work = work.then(function () { return refreshUser(info); })
.catch(function () {});
diff --git a/site/assets/render.js b/site/assets/render.js
new file mode 100644
index 0000000..cde306b
--- /dev/null
+++ b/site/assets/render.js
@@ -0,0 +1,1017 @@
+// Highlight and Markdown for Splux Git. Matches tools/render.awk so
+// live/404 pages get the same boxes as the baked HTML.
+(function (root) {
+ var FAM = {
+ Shell: "shell", Bash: "shell", Zsh: "shell", Fish: "shell",
+ Nushell: "shell", PowerShell: "shell", Batchfile: "shell",
+ "Alpine Abuild": "shell", "Gentoo Ebuild": "shell",
+ C: "clike", "C++": "clike", "C#": "clike",
+ "Objective-C": "clike", "Objective-C++": "clike",
+ Java: "clike", JavaScript: "clike", TypeScript: "clike",
+ Go: "clike", Rust: "clike", PHP: "clike", Kotlin: "clike",
+ Scala: "clike", Swift: "clike", Dart: "clike", D: "clike",
+ Groovy: "clike", Apex: "clike", Vala: "clike", Zig: "clike",
+ Carbon: "clike", V: "clike", Nim: "clike", Crystal: "clike",
+ Python: "python", Starlark: "python", Cython: "python",
+ Snakemake: "python", Ruby: "ruby", Perl: "perl", Raku: "perl",
+ Awk: "awk", Lua: "lua", R: "r", Tcl: "tcl",
+ SQL: "sql", PLpgSQL: "sql", TSQL: "sql", PLSQL: "sql",
+ HTML: "xml", XML: "xml", SVG: "xml", XSLT: "xml", Vue: "xml",
+ Svelte: "xml", Astro: "xml", JSX: "xml", TSX: "xml",
+ CSS: "css", SCSS: "css", Sass: "css", Less: "css", PostCSS: "css",
+ JSON: "json", JSON5: "json", JSONC: "json",
+ "JSON with Comments": "json", YAML: "yaml", TOML: "toml", INI: "ini",
+ Desktop: "ini", "Git Config": "ini", Makefile: "make", CMake: "cmake",
+ Diff: "diff", "Ignore List": "ini", Lisp: "lisp", Scheme: "lisp",
+ "Common Lisp": "lisp", "Emacs Lisp": "lisp", Clojure: "lisp",
+ Racket: "lisp", Haskell: "haskell", Idris: "haskell",
+ Elixir: "elixir", Erlang: "erlang", Fortran: "fortran",
+ "Fortran Free Form": "fortran", "Vim Script": "vim",
+ "Vim Help File": "vim", Dockerfile: "docker", Nix: "nix",
+ "Protocol Buffer": "proto", GraphQL: "graphql",
+ TeX: "tex", LaTeX: "tex", BibTeX: "tex",
+ Assembly: "asm", "Unix Assembly": "asm",
+ Markdown: "markdown", MDX: "markdown", Text: "plain"
+ };
+ var EXTRA_FAM = {};
+ var FENCE = {
+ sh: "Shell", bash: "Shell", zsh: "Shell", shell: "Shell",
+ js: "JavaScript", javascript: "JavaScript", mjs: "JavaScript",
+ cjs: "JavaScript", ts: "TypeScript", typescript: "TypeScript",
+ jsx: "JavaScript", tsx: "TypeScript",
+ py: "Python", python: "Python", pyi: "Python",
+ rb: "Ruby", ruby: "Ruby", pl: "Perl", perl: "Perl",
+ lua: "Lua", sql: "SQL", c: "C", h: "C", cpp: "C++", "c++": "C++",
+ cc: "C++", cxx: "C++", hpp: "C++", hh: "C++",
+ rs: "Rust", rust: "Rust", go: "Go", golang: "Go", java: "Java",
+ kt: "Kotlin", kts: "Kotlin", cs: "C#", php: "PHP",
+ swift: "Swift", dart: "Dart", zig: "Zig",
+ html: "HTML", htm: "HTML", xml: "XML", css: "CSS",
+ scss: "SCSS", sass: "Sass", less: "Less", json: "JSON",
+ jsonc: "JSONC", yml: "YAML", yaml: "YAML", toml: "TOML",
+ md: "Markdown", markdown: "Markdown", mdx: "Markdown",
+ awk: "Awk", make: "Makefile", makefile: "Makefile",
+ diff: "Diff", patch: "Diff",
+ dockerfile: "Dockerfile", docker: "Dockerfile", nix: "Nix",
+ cmake: "CMake", r: "R", hs: "Haskell", lisp: "Lisp",
+ el: "Emacs Lisp", vim: "Vim Script", ex: "Elixir", exs: "Elixir",
+ erl: "Erlang", proto: "Protocol Buffer", graphql: "GraphQL",
+ gql: "GraphQL", tex: "TeX", latex: "TeX", ini: "INI",
+ cfg: "INI", conf: "INI", s: "Assembly", asm: "Assembly",
+ f90: "Fortran", f95: "Fortran", vue: "Vue", svg: "SVG"
+ };
+ var KW = {};
+
+ function addkw(key, words) {
+ var a = words.split(/\s+/);
+ KW[key] = KW[key] || {};
+ for (var i = 0; i < a.length; i++)
+ if (a[i])
+ KW[key][a[i]] = 1;
+ }
+
+ addkw("clike", "if else for while do switch case default break continue return goto sizeof typedef struct union enum const static extern volatile void int char short long float double signed unsigned bool true false NULL nullptr this new delete try catch throw class public private protected namespace using template typename virtual override final inline explicit operator");
+ addkw("JavaScript", "function var let const async await yield import export from as of in instanceof typeof new this class extends super constructor null true false undefined");
+ addkw("TypeScript", "function var let const async await yield import export from as of in instanceof typeof new this class extends super constructor interface type enum implements declare abstract readonly never unknown any void string number boolean");
+ addkw("Java", "package import class interface enum extends implements abstract synchronized throws throw finally true false null instanceof var record");
+ addkw("Go", "func package import var const type map chan interface struct defer go select range iota true false nil make new append len");
+ addkw("Rust", "fn let mut pub crate mod use struct enum impl trait where async await move ref self Self super type const static unsafe match loop true false as Some None Ok Err");
+ addkw("PHP", "function class interface trait namespace use as public private protected static abstract final echo print new clone true false null");
+ addkw("C#", "namespace using class interface struct record enum public private protected internal static abstract override virtual async await var string object bool true false null");
+ addkw("shell", "if then else elif fi for while until do done case esac in function select return break continue true false shift export local readonly declare alias eval exec source trap wait pwd test echo printf read cd mkdir rmdir rm cp mv cat");
+ addkw("python", "and as assert async await break class continue def del elif else except False finally for from global if import in is lambda None nonlocal not or pass raise return True try while with yield match case");
+ addkw("ruby", "alias and begin break case class def do else elsif end ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield");
+ addkw("perl", "if unless elsif else while until for foreach sub my our local package use require next last redo return die warn eval shift push pop split join map grep");
+ addkw("awk", "BEGIN END if else while do for in break continue next nextfile delete exit return print printf getline function");
+ addkw("lua", "and break do else elseif end false for function goto if in local nil not or repeat return then true until while");
+ addkw("sql", "select insert update delete from where join inner left right full outer on group by order having limit offset as and or not null is in like between exists case when then else end create alter drop table index view into values set distinct union all");
+ addkw("make", "ifeq ifneq ifdef ifndef else endif include define endef export override");
+ addkw("cmake", "if else elseif endif foreach endforeach while function endfunction macro set list string file include option project add_executable add_library message");
+ addkw("docker", "FROM RUN CMD LABEL EXPOSE ENV ADD COPY ENTRYPOINT VOLUME USER WORKDIR ARG ONBUILD HEALTHCHECK SHELL AS");
+ addkw("nix", "let in rec with inherit import if then else assert true false null");
+ addkw("haskell", "module where import qualified as hiding data type class instance deriving do of let in if then else case");
+ addkw("lisp", "defun defmacro defvar lambda if when unless cond case let loop progn quote setq setf and or not nil t");
+ addkw("elixir", "def defp defmodule defmacro alias import require use if unless cond case with for try rescue catch after else end fn do true false nil");
+ addkw("erlang", "after and andalso begin case catch end fun if not of or orelse receive try when");
+ addkw("vim", "if else elseif endif for endfor while endwhile function endfunction return let set noremap syntax highlight command autocmd");
+ addkw("r", "if else repeat while function for in next break TRUE FALSE NULL NA Inf NaN");
+ addkw("tcl", "if else elseif then for foreach while switch proc return break continue set puts source");
+ addkw("proto", "syntax package import option message enum service rpc returns repeated optional required true false");
+ addkw("graphql", "type interface union enum input extend schema scalar query mutation subscription fragment on true false null");
+ addkw("asm", "section global extern jmp call ret push pop mov add sub mul div cmp and or xor lea nop");
+ addkw("fortran", "program end subroutine function module use implicit none integer real character logical if then else endif do enddo call return");
+
+ function esc(s) {
+ return String(s == null ? "" : s)
+ .replace(/&/g, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/"/g, """);
+ }
+
+ function sp(cls, s) {
+ return s ? "<span class=\"" + cls + "\">" + esc(s) + "</span>" : "";
+ }
+
+ function famOf(lang) {
+ if (!lang)
+ return "generic";
+ if (FAM[lang])
+ return FAM[lang];
+ if (EXTRA_FAM[lang])
+ return EXTRA_FAM[lang];
+ if (FENCE[String(lang).toLowerCase()])
+ return famOf(FENCE[String(lang).toLowerCase()]);
+ return "generic";
+ }
+
+ function setFams(obj) {
+ EXTRA_FAM = obj || {};
+ }
+
+ function iskw(lang, fam, w) {
+ return (KW[lang] && KW[lang][w]) || (KW[fam] && KW[fam][w]);
+ }
+
+ function takeWord(text, i) {
+ var j = i;
+ while (j < text.length && /[A-Za-z0-9_]/.test(text.charAt(j)))
+ j++;
+ return j - i;
+ }
+
+ function takeNum(text, i) {
+ var j = i;
+ if (text.slice(i, i + 2).toLowerCase() === "0x") {
+ j = i + 2;
+ while (j < text.length && /[0-9A-Fa-f_]/.test(text.charAt(j)))
+ j++;
+ return j - i;
+ }
+ var dot = false;
+ while (j < text.length) {
+ var c = text.charAt(j);
+ if (/[0-9_]/.test(c))
+ j++;
+ else if (c === "." && !dot) {
+ dot = true;
+ j++;
+ } else
+ break;
+ }
+ return j - i || 1;
+ }
+
+ function takeString(text, i, q) {
+ var j = i + 1;
+ var escst = false;
+ while (j < text.length) {
+ var c = text.charAt(j);
+ if (escst) {
+ escst = false;
+ j++;
+ continue;
+ }
+ if (c === "\\") {
+ escst = true;
+ j++;
+ continue;
+ }
+ j++;
+ if (c === q)
+ break;
+ }
+ return j - i;
+ }
+
+ function lineComment(text, i) {
+ var n = text.indexOf("\n", i);
+ return n === -1 ? text.length - i : n - i;
+ }
+
+ function hlHash(text, lang, fam) {
+ var i = 0;
+ var n = text.length;
+ var out = "";
+ while (i < n) {
+ var c = text.charAt(i);
+ if (c === "\n" || /[ \t]/.test(c)) {
+ out += c;
+ i++;
+ continue;
+ }
+ if (c === "#" && !(fam === "elixir" && text.slice(i, i + 2) === "#{")) {
+ var w = lineComment(text, i);
+ out += sp("c", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (c === "\"" || c === "'" || (c === "`" && fam === "shell")) {
+ w = takeString(text, i, c);
+ out += sp("s", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (/[0-9]/.test(c)) {
+ w = takeNum(text, i);
+ out += sp("n", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (c === "$" && (fam === "shell" || fam === "perl")) {
+ w = 1;
+ if (text.charAt(i + 1) === "{") {
+ var brace = text.indexOf("}", i);
+ w = brace === -1 ? 2 : brace - i + 1;
+ } else if (/[A-Za-z_0-9?!@*]/.test(text.charAt(i + 1)))
+ w = 1 + takeWord(text, i + 1);
+ out += sp("b", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (/[A-Za-z_]/.test(c)) {
+ w = takeWord(text, i);
+ var word = text.slice(i, i + w);
+ if (iskw(lang, fam, word))
+ out += sp("k", word);
+ else if ((fam === "yaml" || fam === "toml" || fam === "ini") &&
+ text.charAt(i + w) === ":")
+ out += sp("a", word);
+ else if (text.charAt(i + w) === "(")
+ out += sp("f", word);
+ else
+ out += esc(word);
+ i += w;
+ continue;
+ }
+ out += esc(c);
+ i++;
+ }
+ return out;
+ }
+
+ function hlClike(text, lang, fam) {
+ var i = 0;
+ var n = text.length;
+ var out = "";
+ while (i < n) {
+ var c = text.charAt(i);
+ var two = text.slice(i, i + 2);
+ if (c === "\n" || /[ \t]/.test(c)) {
+ out += c;
+ i++;
+ continue;
+ }
+ if (two === "//") {
+ var w = lineComment(text, i);
+ out += sp("c", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (two === "/*") {
+ var end = text.indexOf("*/", i + 2);
+ w = end === -1 ? n - i : end - i + 2;
+ out += sp("c", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (c === "#" && (lang === "C" || lang === "C++" ||
+ lang === "Objective-C" || lang === "Objective-C++")) {
+ w = lineComment(text, i);
+ out += sp("p", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (c === "'" && lang === "Rust") {
+ if (/[A-Za-z_]/.test(text.charAt(i + 1))) {
+ w = 1 + takeWord(text, i + 1);
+ out += sp("t", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ out += esc(c);
+ i++;
+ continue;
+ }
+ if (c === "\"" || c === "'" || (c === "`" && (lang === "JavaScript" || lang === "TypeScript"))) {
+ w = takeString(text, i, c);
+ out += sp("s", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (/[0-9]/.test(c)) {
+ w = takeNum(text, i);
+ out += sp("n", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (/[A-Za-z_]/.test(c)) {
+ w = takeWord(text, i);
+ var word = text.slice(i, i + w);
+ if (iskw(lang, fam, word) || iskw(lang, "clike", word))
+ out += sp("k", word);
+ else if (/^[A-Z]/.test(word) && lang !== "JavaScript" && lang !== "TypeScript")
+ out += sp("t", word);
+ else if (text.charAt(i + w) === "(")
+ out += sp("f", word);
+ else
+ out += esc(word);
+ i += w;
+ continue;
+ }
+ out += esc(c);
+ i++;
+ }
+ return out;
+ }
+
+ function hlPython(text, lang) {
+ var i = 0;
+ var n = text.length;
+ var out = "";
+ while (i < n) {
+ var c = text.charAt(i);
+ var three = text.slice(i, i + 3);
+ if (c === "\n" || /[ \t]/.test(c)) {
+ out += c;
+ i++;
+ continue;
+ }
+ if (c === "#") {
+ var w = lineComment(text, i);
+ out += sp("c", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (three === "\"\"\"" || three === "'''") {
+ var end = text.indexOf(three, i + 3);
+ w = end === -1 ? n - i : end - i + 3;
+ out += sp("s", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (c === "\"" || c === "'") {
+ w = takeString(text, i, c);
+ out += sp("s", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (/[0-9]/.test(c)) {
+ w = takeNum(text, i);
+ out += sp("n", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (/[A-Za-z_]/.test(c)) {
+ w = takeWord(text, i);
+ var word = text.slice(i, i + w);
+ if (iskw(lang, "python", word))
+ out += sp("k", word);
+ else if (text.charAt(i + w) === "(")
+ out += sp("f", word);
+ else
+ out += esc(word);
+ i += w;
+ continue;
+ }
+ out += esc(c);
+ i++;
+ }
+ return out;
+ }
+
+ function hlLinec(text, lang, fam, mark) {
+ var i = 0;
+ var n = text.length;
+ var out = "";
+ while (i < n) {
+ var c = text.charAt(i);
+ if (c === "\n" || /[ \t]/.test(c)) {
+ out += c;
+ i++;
+ continue;
+ }
+ if (text.slice(i, i + mark.length) === mark) {
+ var w = lineComment(text, i);
+ out += sp("c", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (c === "\"" || c === "'") {
+ w = takeString(text, i, c);
+ out += sp("s", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (/[0-9]/.test(c)) {
+ w = takeNum(text, i);
+ out += sp("n", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (/[A-Za-z_]/.test(c)) {
+ w = takeWord(text, i);
+ var word = text.slice(i, i + w);
+ if (iskw(lang, fam, word))
+ out += sp("k", word);
+ else if (text.charAt(i + w) === "(")
+ out += sp("f", word);
+ else
+ out += esc(word);
+ i += w;
+ continue;
+ }
+ out += esc(c);
+ i++;
+ }
+ return out;
+ }
+
+ function hlXml(text) {
+ var i = 0;
+ var n = text.length;
+ var out = "";
+ while (i < n) {
+ if (text.slice(i, i + 4) === "<!--") {
+ var end = text.indexOf("-->", i + 4);
+ var w = end === -1 ? n - i : end - i + 3;
+ out += sp("c", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (text.charAt(i) === "<") {
+ end = text.indexOf(">", i);
+ w = end === -1 ? n - i : end - i + 1;
+ out += hlTag(text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ out += esc(text.charAt(i));
+ i++;
+ }
+ return out;
+ }
+
+ function hlTag(tag) {
+ var i = 0;
+ var n = tag.length;
+ var out = "";
+ while (i < n) {
+ var c = tag.charAt(i);
+ if (/[ \t\n]/.test(c)) {
+ out += c;
+ i++;
+ continue;
+ }
+ if (c === "\"" || c === "'") {
+ var w = takeString(tag, i, c);
+ out += sp("s", tag.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (/[A-Za-z_:]/.test(c)) {
+ w = 0;
+ while (i + w < n && /[A-Za-z0-9_:-]/.test(tag.charAt(i + w)))
+ w++;
+ var word = tag.slice(i, i + w);
+ if (i === 0 || tag.charAt(i - 1) === "<" || tag.charAt(i - 1) === "/")
+ out += sp("g", word);
+ else
+ out += sp("a", word);
+ i += w;
+ continue;
+ }
+ out += esc(c);
+ i++;
+ }
+ return out;
+ }
+
+ function hlCss(text) {
+ var i = 0;
+ var n = text.length;
+ var out = "";
+ while (i < n) {
+ if (text.slice(i, i + 2) === "/*") {
+ var end = text.indexOf("*/", i + 2);
+ var w = end === -1 ? n - i : end - i + 2;
+ out += sp("c", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ var c = text.charAt(i);
+ if (c === "\"" || c === "'") {
+ w = takeString(text, i, c);
+ out += sp("s", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (/[A-Za-z_-]/.test(c)) {
+ w = 0;
+ while (i + w < n && /[A-Za-z0-9_-]/.test(text.charAt(i + w)))
+ w++;
+ var word = text.slice(i, i + w);
+ out += sp(text.charAt(i + w) === ":" ? "a" : "g", word);
+ i += w;
+ continue;
+ }
+ if (/[0-9]/.test(c)) {
+ w = takeNum(text, i);
+ out += sp("n", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ out += esc(c);
+ i++;
+ }
+ return out;
+ }
+
+ function hlJson(text) {
+ var i = 0;
+ var n = text.length;
+ var out = "";
+ while (i < n) {
+ var c = text.charAt(i);
+ if (c === "\n" || /[ \t]/.test(c)) {
+ out += c;
+ i++;
+ continue;
+ }
+ if (c === "\"") {
+ var w = takeString(text, i, "\"");
+ var j = i + w;
+ while (j < n && /[ \t\n]/.test(text.charAt(j)))
+ j++;
+ out += sp(text.charAt(j) === ":" ? "a" : "s", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (/[0-9-]/.test(c)) {
+ w = c === "-" ? 1 + takeNum(text, i + 1) : takeNum(text, i);
+ out += sp("n", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (/[A-Za-z]/.test(c)) {
+ w = takeWord(text, i);
+ var word = text.slice(i, i + w);
+ out += (word === "true" || word === "false" || word === "null")
+ ? sp("k", word) : esc(word);
+ i += w;
+ continue;
+ }
+ out += esc(c);
+ i++;
+ }
+ return out;
+ }
+
+ function hlDiff(text) {
+ var lines = text.split("\n");
+ var out = [];
+ for (var i = 0; i < lines.length; i++) {
+ var line = lines[i] + (i < lines.length - 1 ? "\n" : "");
+ if (/^(diff |index |--- |\+\+\+ )/.test(line))
+ out.push(sp("p", line));
+ else if (line.charAt(0) === "@")
+ out.push(sp("t", line));
+ else if (line.charAt(0) === "+")
+ out.push(sp("s", line));
+ else if (line.charAt(0) === "-")
+ out.push(sp("g", line));
+ else
+ out.push(esc(line));
+ }
+ return out.join("");
+ }
+
+ function hlLua(text) {
+ var i = 0;
+ var n = text.length;
+ var out = "";
+ while (i < n) {
+ var c = text.charAt(i);
+ var two = text.slice(i, i + 2);
+ if (c === "\n" || /[ \t]/.test(c)) {
+ out += c;
+ i++;
+ continue;
+ }
+ if (text.slice(i, i + 4) === "--[[") {
+ var end = text.indexOf("]]", i + 4);
+ var w = end === -1 ? n - i : end - i + 2;
+ out += sp("c", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (two === "--") {
+ w = lineComment(text, i);
+ out += sp("c", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (c === "\"" || c === "'") {
+ w = takeString(text, i, c);
+ out += sp("s", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (/[0-9]/.test(c)) {
+ w = takeNum(text, i);
+ out += sp("n", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (/[A-Za-z_]/.test(c)) {
+ w = takeWord(text, i);
+ var word = text.slice(i, i + w);
+ if (iskw("Lua", "lua", word))
+ out += sp("k", word);
+ else if (text.charAt(i + w) === "(")
+ out += sp("f", word);
+ else
+ out += esc(word);
+ i += w;
+ continue;
+ }
+ out += esc(c);
+ i++;
+ }
+ return out;
+ }
+
+ function hlSql(text, lang) {
+ var i = 0;
+ var n = text.length;
+ var out = "";
+ while (i < n) {
+ var c = text.charAt(i);
+ var two = text.slice(i, i + 2);
+ if (c === "\n" || /[ \t]/.test(c)) {
+ out += c;
+ i++;
+ continue;
+ }
+ if (two === "--") {
+ var w = lineComment(text, i);
+ out += sp("c", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (two === "/*") {
+ var end = text.indexOf("*/", i + 2);
+ w = end === -1 ? n - i : end - i + 2;
+ out += sp("c", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (c === "'" || c === "\"") {
+ w = takeString(text, i, c);
+ out += sp("s", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (/[0-9]/.test(c)) {
+ w = takeNum(text, i);
+ out += sp("n", text.slice(i, i + w));
+ i += w;
+ continue;
+ }
+ if (/[A-Za-z_]/.test(c)) {
+ w = takeWord(text, i);
+ var word = text.slice(i, i + w);
+ if (iskw(lang, "sql", word.toLowerCase()) || iskw(lang, "sql", word))
+ out += sp("k", word);
+ else
+ out += esc(word);
+ i += w;
+ continue;
+ }
+ out += esc(c);
+ i++;
+ }
+ return out;
+ }
+
+ function hlSrc(text, lang) {
+ var fam = famOf(lang);
+ if (fam === "plain" || fam === "markdown")
+ return esc(text);
+ if (fam === "xml")
+ return hlXml(text);
+ if (fam === "css")
+ return hlCss(text);
+ if (fam === "json")
+ return hlJson(text);
+ if (fam === "diff")
+ return hlDiff(text);
+ if (fam === "lisp")
+ return hlLinec(text, lang, fam, ";");
+ if (fam === "sql")
+ return hlSql(text, lang);
+ if (fam === "lua")
+ return hlLua(text);
+ if (fam === "haskell")
+ return hlLinec(text, lang, fam, "--");
+ if (fam === "erlang" || fam === "tex")
+ return hlLinec(text, lang, fam, "%");
+ if (fam === "vim")
+ return hlLinec(text, lang, fam, "\"");
+ if (fam === "fortran")
+ return hlLinec(text, lang, fam, "!");
+ if (fam === "asm")
+ return hlLinec(text, lang, fam, ";");
+ if (fam === "clike")
+ return hlClike(text, lang, fam);
+ if (fam === "python")
+ return hlPython(text, lang);
+ return hlHash(text, lang, fam);
+ }
+
+ function wrapHl(inner, lang) {
+ var label = lang || "text";
+ return "<pre class=\"block git-blob is-hl\" data-lang=\"" +
+ esc(lang || "") + "\" aria-label=\"" + esc(label) +
+ "\"><code>" + inner + "</code></pre>";
+ }
+
+ function fenceLang(info) {
+ info = String(info || "").replace(/^[ \t{]+/, "").replace(/[ \t}].*/, "");
+ if (!info)
+ return "";
+ var low = info.toLowerCase();
+ if (low === "text" || low === "plain")
+ return "Text";
+ return FENCE[low] || info;
+ }
+
+ function mdInline(s) {
+ var out = "";
+ var i = 0;
+ var n = s.length;
+ while (i < n) {
+ var c = s.charAt(i);
+ var two = s.slice(i, i + 2);
+ if (c === "`") {
+ var ticks = 1;
+ while (s.charAt(i + ticks) === "`")
+ ticks++;
+ var close = s.indexOf(s.slice(i, i + ticks), i + ticks);
+ if (close !== -1) {
+ out += "<code>" + esc(s.slice(i + ticks, close)) + "</code>";
+ i = close + ticks;
+ continue;
+ }
+ }
+ if (two === "![") {
+ var m = s.slice(i).match(/^!\[[^\]]*\]\([^)]+\)/);
+ if (m) {
+ var label = m[0].replace(/^!\[/, "").replace(/\]\([^)]+\)$/, "");
+ var url = m[0].replace(/^!\[[^\]]*\]\(/, "").replace(/\)$/, "");
+ if (/^https?:\/\//.test(url))
+ out += "<img src=\"" + esc(url) + "\" alt=\"" + esc(label) + "\">";
+ else
+ out += esc(m[0]);
+ i += m[0].length;
+ continue;
+ }
+ }
+ if (c === "[") {
+ m = s.slice(i).match(/^\[[^\]]+\]\([^)]+\)/);
+ if (m) {
+ label = m[0].replace(/^\[/, "").replace(/\]\([^)]+\)$/, "");
+ url = m[0].replace(/^\[[^\]]+\]\(/, "").replace(/\)$/, "");
+ if (/^(https?:\/\/|\/|\.\.?\/)/.test(url))
+ out += "<a href=\"" + esc(url) + "\">" + mdInline(label) + "</a>";
+ else
+ out += esc(m[0]);
+ i += m[0].length;
+ continue;
+ }
+ }
+ if (two === "**" || two === "__") {
+ close = s.indexOf(two, i + 2);
+ if (close !== -1) {
+ out += "<strong>" + mdInline(s.slice(i + 2, close)) + "</strong>";
+ i = close + 2;
+ continue;
+ }
+ }
+ if (two === "~~") {
+ close = s.indexOf("~~", i + 2);
+ if (close !== -1) {
+ out += "<del>" + mdInline(s.slice(i + 2, close)) + "</del>";
+ i = close + 2;
+ continue;
+ }
+ }
+ if ((c === "*" || c === "_") && i + 1 < n && !/[ \t]/.test(s.charAt(i + 1))) {
+ close = s.indexOf(c, i + 1);
+ if (close !== -1 && s.charAt(close + 1) !== c) {
+ out += "<em>" + mdInline(s.slice(i + 1, close)) + "</em>";
+ i = close + 1;
+ continue;
+ }
+ }
+ if (s.slice(i, i + 8) === "https://" || s.slice(i, i + 7) === "http://") {
+ w = 0;
+ while (i + w < n && !/[ \t<>"']/.test(s.charAt(i + w)))
+ w++;
+ while (w > 0 && /[.,;:!?)]/.test(s.charAt(i + w - 1)))
+ w--;
+ url = s.slice(i, i + w);
+ out += "<a href=\"" + esc(url) + "\">" + esc(url) + "</a>";
+ i += w;
+ continue;
+ }
+ out += esc(c);
+ i++;
+ }
+ return out;
+ }
+
+ function mdTask(text) {
+ if (/^\[[xX]\]\s/.test(text))
+ return "<span class=\"task done\">[x]</span> " + mdInline(text.slice(4).replace(/^\s+/, ""));
+ if (/^\[ \]\s/.test(text))
+ return "<span class=\"task\">[ ]</span> " + mdInline(text.slice(4).replace(/^\s+/, ""));
+ return mdInline(text);
+ }
+
+ function markdown(src) {
+ var lines = String(src || "").replace(/\r/g, "").split("\n");
+ var html = "";
+ var i = 0;
+ while (i < lines.length) {
+ var line = lines[i];
+ var fm = line.match(/^[ \t]*(```|~~~)(.*)$/);
+ if (fm) {
+ var ticks = fm[1];
+ var buf = [];
+ i++;
+ while (i < lines.length && !new RegExp("^[ \\t]*" + ticks.replace(/`/g, "\\`") + "\\s*$").test(lines[i])) {
+ buf.push(lines[i]);
+ i++;
+ }
+ if (i < lines.length)
+ i++;
+ var lang = fenceLang(fm[2]);
+ html += wrapHl(hlSrc(buf.join("\n"), lang), lang);
+ continue;
+ }
+ if (/^[ \t]*(-{3,}|\*{3,}|_{3,})[ \t]*$/.test(line)) {
+ html += "<hr class=\"rule\">";
+ i++;
+ continue;
+ }
+ var hm = line.match(/^[ \t]*(#{1,6})[ \t]+(.*?)[ \t]*#*[ \t]*$/);
+ if (hm) {
+ var lv = hm[1].length + 1;
+ html += "<h" + lv + ">" + mdInline(hm[2]) + "</h" + lv + ">";
+ i++;
+ continue;
+ }
+ if (/^[ \t]*>/.test(line)) {
+ html += "<blockquote>";
+ while (i < lines.length && /^[ \t]*>/.test(lines[i])) {
+ html += "<p>" + mdInline(lines[i].replace(/^[ \t]*>[ \t]?/, "")) + "</p>";
+ i++;
+ }
+ html += "</blockquote>";
+ continue;
+ }
+ if (/^[ \t]*\|.*\|[ \t]*$/.test(line) && i + 1 < lines.length &&
+ /\|[ \t]*:?-+:?[ \t]*\|/.test(lines[i + 1])) {
+ var splitCells = function (row) {
+ row = row.replace(/^[ \t]*\|/, "").replace(/\|[ \t]*$/, "");
+ return row.split("|").map(function (c) {
+ return c.replace(/^[ \t]+|[ \t]+$/g, "");
+ });
+ };
+ var head = splitCells(line);
+ html += "<table class=\"pkgs md-table\"><thead><tr>";
+ for (var h = 0; h < head.length; h++)
+ html += "<th>" + mdInline(head[h]) + "</th>";
+ html += "</tr></thead><tbody>";
+ i += 2;
+ while (i < lines.length && /^[ \t]*\|.*\|[ \t]*$/.test(lines[i])) {
+ var cells = splitCells(lines[i]);
+ html += "<tr>";
+ for (var c = 0; c < head.length; c++)
+ html += "<td>" + mdInline(cells[c] || "") + "</td>";
+ html += "</tr>";
+ i++;
+ }
+ html += "</tbody></table>";
+ continue;
+ }
+ if (/^[ \t]*[-*+][ \t]+/.test(line)) {
+ html += "<ul class=\"plain\">";
+ while (i < lines.length && /^[ \t]*[-*+][ \t]+/.test(lines[i])) {
+ html += "<li>" + mdTask(lines[i].replace(/^[ \t]*[-*+][ \t]+/, "")) + "</li>";
+ i++;
+ }
+ html += "</ul>";
+ continue;
+ }
+ if (/^[ \t]*[0-9]+\.[ \t]+/.test(line)) {
+ html += "<ol>";
+ while (i < lines.length && /^[ \t]*[0-9]+\.[ \t]+/.test(lines[i])) {
+ html += "<li>" + mdTask(lines[i].replace(/^[ \t]*[0-9]+\.[ \t]+/, "")) + "</li>";
+ i++;
+ }
+ html += "</ol>";
+ continue;
+ }
+ if (/^[ \t]*$/.test(line)) {
+ i++;
+ continue;
+ }
+ if (/^( |\t)/.test(line)) {
+ var code = [];
+ while (i < lines.length && /^( |\t)/.test(lines[i])) {
+ code.push(lines[i].replace(/^ /, "").replace(/^\t/, ""));
+ i++;
+ }
+ html += wrapHl(esc(code.join("\n")), "");
+ continue;
+ }
+ var para = line;
+ i++;
+ if (i < lines.length && /^[ \t]*=+[ \t]*$/.test(lines[i])) {
+ html += "<h2>" + mdInline(para) + "</h2>";
+ i++;
+ continue;
+ }
+ if (i < lines.length && /^[ \t]*-+[ \t]*$/.test(lines[i])) {
+ html += "<h3>" + mdInline(para) + "</h3>";
+ i++;
+ continue;
+ }
+ while (i < lines.length && !/^[ \t]*$/.test(lines[i]) &&
+ !/^[ \t]*(```|~~~|#{1,6}[ \t]|[-*+][ \t]|[0-9]+\.[ \t]|>)/.test(lines[i])) {
+ para += " " + lines[i];
+ i++;
+ }
+ html += "<p>" + mdInline(para) + "</p>";
+ }
+ return "<div class=\"md\">" + html + "</div>";
+ }
+
+ function highlight(text, lang) {
+ return wrapHl(hlSrc(String(text || ""), lang || ""), lang || "");
+ }
+
+ function isMarkdown(lang, path) {
+ if (lang === "Markdown" || lang === "MDX")
+ return true;
+ path = String(path || "");
+ return /\.(md|markdown|mdown|mdx)$/i.test(path);
+ }
+
+ function langFromPath(path) {
+ var base = String(path || "").replace(/^.*\//, "");
+ if (!base)
+ return "";
+ if (/^recipe$/i.test(base))
+ return "Shell";
+ if (/^(GNU)?Makefile$/i.test(base) || base === "makefile")
+ return "Makefile";
+ if (/^Dockerfile$/i.test(base))
+ return "Dockerfile";
+ if (/^CMakeLists\.txt$/i.test(base))
+ return "CMake";
+ var n = base.lastIndexOf(".");
+ if (n <= 0)
+ return "";
+ var ext = base.slice(n + 1).toLowerCase();
+ return FENCE[ext] || "";
+ }
+
+ function apply(root) {
+ root = root || document;
+ var nodes = root.querySelectorAll("pre.git-blob:not(.is-hl)[data-lang]");
+ for (var i = 0; i < nodes.length; i++) {
+ var pre = nodes[i];
+ var lang = pre.getAttribute("data-lang") || "";
+ var text = pre.textContent || "";
+ pre.outerHTML = highlight(text, lang);
+ }
+ var md = root.querySelectorAll("[data-md]:not(.md)");
+ for (var j = 0; j < md.length; j++) {
+ var el = md[j];
+ el.outerHTML = markdown(el.textContent || "");
+ }
+ }
+
+ root.SpluxRender = {
+ highlight: highlight,
+ markdown: markdown,
+ isMarkdown: isMarkdown,
+ langOf: famOf,
+ langFromPath: langFromPath,
+ setFams: setFams,
+ apply: apply
+ };
+})(window);
diff --git a/site/assets/style.css b/site/assets/style.css
index d451817..8e391a3 100644
--- a/site/assets/style.css
+++ b/site/assets/style.css
@@ -757,3 +757,130 @@ table.git-log td {
padding: 0.25rem 0.5rem;
min-width: 12rem;
}
+
+.badge {
+ display: inline-block;
+ border: 1px solid var(--rule);
+ color: var(--muted);
+ padding: 0 0.35rem;
+ font-size: 0.8rem;
+}
+
+.badge.latest {
+ color: var(--accent);
+ border-color: var(--accent);
+}
+
+.release-list {
+ display: flex;
+ flex-direction: column;
+ gap: 1.25rem;
+ margin: 0.75rem 0 1.25rem;
+}
+
+article.release {
+ border-bottom: 1px solid var(--rule);
+ padding-bottom: 1rem;
+}
+
+article.release h3 {
+ margin: 0.25rem 0 0.35rem;
+}
+
+article.release .meta {
+ color: var(--muted);
+ margin: 0 0 0.25rem;
+}
+
+article.release .summary {
+ margin: 0.5rem 0 0;
+}
+
+.git-blob code,
+.git-blob span {
+ font: inherit;
+}
+
+.git-blob .c { color: #6a6a6a; }
+.git-blob .k { color: var(--accent); }
+.git-blob .s { color: #c8c890; }
+.git-blob .n { color: #d6d6d6; }
+.git-blob .t { color: #e8e8e8; }
+.git-blob .b { color: #ffff88; }
+.git-blob .f { color: #d6d6d6; }
+.git-blob .p { color: #aaaa66; }
+.git-blob .g { color: var(--accent); }
+.git-blob .a { color: #c8c890; }
+.git-blob .o { color: var(--muted); }
+
+.md {
+ margin: 0.75rem 0 1.25rem;
+}
+
+.md h2, .md h3, .md h4, .md h5, .md h6 {
+ margin: 1.1rem 0 0.45rem;
+}
+
+.md p {
+ margin: 0.55rem 0;
+}
+
+.md hr {
+ border: 0;
+ border-top: 1px solid var(--rule);
+ margin: 1rem 0;
+}
+
+.md blockquote {
+ margin: 0.75rem 0;
+ padding: 0 0 0 0.85rem;
+ border-left: 2px solid var(--rule);
+ color: var(--muted);
+}
+
+.md img {
+ max-width: 100%;
+ height: auto;
+ border: 1px solid var(--rule);
+}
+
+.md code {
+ background: #111;
+ border: 1px solid var(--rule);
+ padding: 0 0.25rem;
+ font: inherit;
+}
+
+.md pre.block {
+ margin: 0.75rem 0;
+}
+
+.md pre code {
+ background: none;
+ border: 0;
+ padding: 0;
+}
+
+.md ul, .md ol {
+ margin: 0.5rem 0 0.75rem 1.25rem;
+}
+
+.md .task {
+ color: var(--muted);
+}
+
+.md .task.done {
+ color: var(--accent);
+}
+
+.md-table {
+ margin: 0.75rem 0;
+}
+
+.md del {
+ color: var(--muted);
+}
+
+article.release:last-child {
+ border-bottom: 0;
+}
diff --git a/tools/build-git.sh b/tools/build-git.sh
index 28af5af..dfbef10 100755
--- a/tools/build-git.sh
+++ b/tools/build-git.sh
@@ -16,6 +16,7 @@ SITE_URL=${SITE_URL:-https://splux.robertflexx.dev}
GIT_HOST=${GIT_HOST:-$SITE_URL/git}
GIT_HOST=${GIT_HOST%/}
AWK=tools/git.awk
+RENDER=tools/render.awk
LANGAWK=tools/git-lang.awk
MAPFILE=${MAPFILE:-tools/linguist.map}
MAXBLOB=262144
@@ -117,6 +118,7 @@ page_end() {
printf '%s\n' '</main>'
printf '%s\n' '@@FOOTER@@'
printf '%s\n' '</div>'
+ printf '%s\n' '<script src="../../../../assets/render.js?v=4c4f12c-20261019130111"></script>'
printf '%s\n' '<script src="../../../../assets/git.js?v=4c4f12c-20261019130111" data-root="../../../../" data-git="https://splux.robertflexx.dev/git"></script>'
printf '%s\n' '</body></html>'
} >>"$_dest"
@@ -145,6 +147,218 @@ lookup_lang() {
awk -F '\t' -v p="$_path" '$1 == p { print $2 "\t" $4; exit }' "$_file"
}
+# Highlight a blob, or render Markdown. Baked HTML matches site/assets/render.js.
+render_blob() {
+ _lang=${1-}
+ _path=${2-}
+ case $_path in
+ *.md|*.mdx|*.markdown|*.mdown|*.MD)
+ awk -f "$RENDER" -v mode=markdown -v mapfile="$MAPFILE"
+ return
+ ;;
+ esac
+ case $_lang in
+ Markdown|MDX)
+ awk -f "$RENDER" -v mode=markdown -v mapfile="$MAPFILE"
+ ;;
+ *)
+ awk -f "$RENDER" -v mode=highlight -v lang="$_lang" -v mapfile="$MAPFILE"
+ ;;
+ esac
+}
+
+source_archives() {
+ _repo=$1
+ _tag=$2
+ _gh=$(github_of "$_repo")
+ _enc=$(pct_seg "$_tag")
+ printf '%s\n' '<h3>Source</h3>'
+ printf '%s\n' '<ul class="plain">'
+ printf '<li><a href="%s/archive/refs/tags/%s.zip">%s.zip</a></li>\n' \
+ "$(esc "$_gh")" "$(esc "$_enc")" "$(esc "$_tag")"
+ printf '<li><a href="%s/archive/refs/tags/%s.tar.gz">%s.tar.gz</a></li>\n' \
+ "$(esc "$_gh")" "$(esc "$_enc")" "$(esc "$_tag")"
+ printf '%s\n' '</ul>'
+}
+
+# Local tags plus GitHub tags/releases. Writes .tags.tsv .releases.tsv .assets.tsv
+# and decoded note bodies under .bodies/.
+collect_repo_meta() {
+ _name=$1
+ _dir=$2
+ _base=$3
+ _tagstsv=$_base/.tags.tsv
+ _reltsv=$_base/.releases.tsv
+ _assetstsv=$_base/.assets.tsv
+ _bodies=$_base/.bodies
+ _ghtags=$_base/.tags-gh.tsv
+ _local=$_base/.tags-local.tsv
+ _rawrel=$_base/.releases-raw.tsv
+ _names=$_base/.tag-names
+ mkdir -p "$_bodies"
+ : >"$_tagstsv"
+ : >"$_reltsv"
+ : >"$_assetstsv"
+ : >"$_names"
+
+ if ! fetch_github_tags "$_name" "$_ghtags"; then
+ : >"$_ghtags"
+ fi
+
+ git -C "$_dir" for-each-ref --sort=-creatordate \
+ --format='%(objectname)%09%(objecttype)%09%(refname:short)%09%(creatordate:iso-strict)%09%(taggername)%09%(*objectname)%09%(contents:subject)' \
+ refs/tags 2>/dev/null |
+ awk -F '\t' -v OFS='\t' '{
+ subj = $7
+ for (i = 8; i <= NF; i++)
+ subj = subj " " $i
+ gsub(/\t/, " ", subj)
+ print $1, $2, $3, $4, $5, $6, subj
+ }' >"$_local" || : >"$_local"
+
+ if [ -s "$_local" ]; then
+ while IFS= read -r _line || [ -n "${_line-}" ]
+ do
+ [ -n "${_line-}" ] || continue
+ _obj=$(tsv_cut "$_line" 1)
+ _otype=$(tsv_cut "$_line" 2)
+ _tname=$(tsv_cut "$_line" 3)
+ _iso=$(tsv_cut "$_line" 4)
+ _peeled=$(tsv_cut "$_line" 6)
+ _subj=$(tsv_cut "$_line" 7)
+ [ -n "$_tname" ] || continue
+ if [ "$_otype" = tag ]; then
+ _kind=annotated
+ _sha=$_peeled
+ [ -n "$_sha" ] || _sha=$_obj
+ else
+ _kind=lightweight
+ _sha=$_obj
+ fi
+ if [ -z "$_sha" ] && [ -s "$_ghtags" ]; then
+ _sha=$(awk -F '\t' -v n="$_tname" '$1 == n { print $2; exit }' "$_ghtags")
+ fi
+ _short=$(printf '%s' "$_sha" | cut -c1-7)
+ _seg=$(pct_seg "$_tname")
+ printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
+ "$_tname" "$_sha" "$_short" "$_iso" "$_kind" "$_subj" "" "$_seg" \
+ >>"$_tagstsv"
+ printf '%s\n' "$_tname" >>"$_names"
+ done <"$_local"
+ fi
+
+ if [ -s "$_ghtags" ]; then
+ while IFS= read -r _line || [ -n "${_line-}" ]
+ do
+ [ -n "${_line-}" ] || continue
+ _tname=$(tsv_cut "$_line" 1)
+ _sha=$(tsv_cut "$_line" 2)
+ [ -n "$_tname" ] || continue
+ if [ -s "$_names" ] && grep -Fxq "$_tname" "$_names"; then
+ continue
+ fi
+ _short=$(printf '%s' "$_sha" | cut -c1-7)
+ _seg=$(pct_seg "$_tname")
+ printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
+ "$_tname" "$_sha" "$_short" "" "lightweight" "" "" "$_seg" \
+ >>"$_tagstsv"
+ printf '%s\n' "$_tname" >>"$_names"
+ done <"$_ghtags"
+ fi
+
+ _latest=
+ if fetch_github_releases "$_name" "$_rawrel"; then
+ fetch_github_release_assets "$_name" "$_assetstsv" || : >"$_assetstsv"
+ _latest=$(fetch_github_latest_tag "$_name" 2>/dev/null || true)
+ while IFS= read -r _line || [ -n "${_line-}" ]
+ do
+ [ -n "${_line-}" ] || continue
+ _rtag=$(tsv_cut "$_line" 1)
+ _title=$(tsv_cut "$_line" 2)
+ _riso=$(tsv_cut "$_line" 3)
+ _rlogin=$(tsv_cut "$_line" 4)
+ _ravatar=$(tsv_cut "$_line" 5)
+ _pre=$(tsv_cut "$_line" 6)
+ _react=$(tsv_cut "$_line" 9)
+ _b64=$(tsv_cut "$_line" 10)
+ [ -n "$_rtag" ] || continue
+ [ -n "$_title" ] || _title=$_rtag
+ _seg=$(pct_seg "$_rtag")
+ _body=$_bodies/$_seg
+ if [ -n "$_b64" ]; then
+ printf '%s' "$_b64" | base64 -d >"$_body" 2>/dev/null || : >"$_body"
+ else
+ : >"$_body"
+ fi
+ _excerpt=$(excerpt_text "$_body")
+ _islatest=no
+ [ "$_rtag" = "$_latest" ] && _islatest=yes
+ _nassets=0
+ if [ -s "$_assetstsv" ]; then
+ _nassets=$(awk -F '\t' -v t="$_rtag" '$1 == t { c++ } END { print c + 0 }' \
+ "$_assetstsv")
+ fi
+ printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
+ "$_rtag" "$_title" "$_riso" "$_rlogin" "$_ravatar" "$_pre" \
+ "$_islatest" "$_nassets" "$_excerpt" "$_react" "$_seg" \
+ >>"$_reltsv"
+ if [ -s "$_names" ] && grep -Fxq "$_rtag" "$_names"; then
+ :
+ else
+ _rsha=$(awk -F '\t' -v n="$_rtag" '$1 == n { print $2; exit }' "$_ghtags")
+ _rshort=$(printf '%s' "$_rsha" | cut -c1-7)
+ printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
+ "$_rtag" "$_rsha" "$_rshort" "$_riso" "lightweight" "" \
+ "$_rtag" "$_seg" >>"$_tagstsv"
+ printf '%s\n' "$_rtag" >>"$_names"
+ fi
+ done <"$_rawrel"
+ fi
+
+ if [ -s "$_reltsv" ]; then
+ if ! awk -F '\t' '$7 == "yes" { found = 1; exit } END { exit found ? 0 : 1 }' "$_reltsv"
+ then
+ awk -F '\t' -v OFS='\t' '
+ {
+ n++
+ line[n] = $0
+ pre[n] = $6
+ }
+ END {
+ pick = 0
+ for (i = 1; i <= n; i++) {
+ if (pre[i] != "yes") {
+ pick = i
+ break
+ }
+ }
+ if (!pick)
+ pick = 1
+ for (i = 1; i <= n; i++) {
+ split(line[i], f, "\t")
+ f[7] = (i == pick) ? "yes" : "no"
+ out = f[1]
+ for (j = 2; j <= 11; j++)
+ out = out "\t" f[j]
+ print out
+ }
+ }
+ ' "$_reltsv" >"$_base/.releases-mark.tsv"
+ mv "$_base/.releases-mark.tsv" "$_reltsv"
+ fi
+ awk -F '\t' -v OFS='\t' '
+ FNR == NR { has[$1] = 1; next }
+ {
+ $7 = ($1 in has) ? $1 : ""
+ print
+ }
+ ' "$_reltsv" "$_tagstsv" >"$_base/.tags-joined.tsv"
+ mv "$_base/.tags-joined.tsv" "$_tagstsv"
+ fi
+
+ rm -f "$_ghtags" "$_local" "$_rawrel" "$_names"
+}
+
colorize_langs() {
awk -F '\t' -v OFS='\t' '
FNR == NR {
@@ -231,6 +445,97 @@ fetch_github_user() {
] | @tsv' >"$_dest" 2>/dev/null && [ -s "$_dest" ]
}
+# One URL path segment. Keep unreserved characters, percent-encode the rest.
+pct_seg() {
+ printf '%s' "$1" | LC_ALL=C awk '
+ BEGIN {
+ hex = "0123456789ABCDEF"
+ for (i = 0; i < 256; i++)
+ ord[sprintf("%c", i)] = i
+ }
+ {
+ for (i = 1; i <= length($0); i++) {
+ c = substr($0, i, 1)
+ if (c ~ /[A-Za-z0-9._~-]/)
+ printf "%s", c
+ else {
+ o = ord[c] + 0
+ printf "%%%s%s", substr(hex, int(o / 16) + 1, 1), \
+ substr(hex, (o % 16) + 1, 1)
+ }
+ }
+ }'
+}
+
+fetch_github_tags() {
+ _repo=$1
+ _dest=$2
+ have_gh || return 1
+ gh api --paginate "repos/$OWNER/$_repo/tags?per_page=100" --jq \
+ '.[] | [.name, .commit.sha] | @tsv' \
+ >"$_dest" 2>/dev/null && [ -s "$_dest" ]
+}
+
+fetch_github_releases() {
+ _repo=$1
+ _dest=$2
+ have_gh || return 1
+ gh api --paginate "repos/$OWNER/$_repo/releases?per_page=100" --jq \
+ '.[] | select(.draft == false) | [
+ .tag_name,
+ ((.name // .tag_name) | gsub("[\r\t\n]"; " ")),
+ (.published_at // .created_at // ""),
+ (.author.login // ""),
+ ((.author.avatar_url // "") | gsub("[\r\t]"; " ")),
+ (if .prerelease then "yes" else "no" end),
+ (.html_url // ""),
+ ((.target_commitish // "") | gsub("[\r\t]"; " ")),
+ ((.reactions.total_count // 0) | tostring),
+ ((.body // "") | @base64)
+ ] | @tsv' >"$_dest" 2>/dev/null && [ -s "$_dest" ]
+}
+
+fetch_github_release_assets() {
+ _repo=$1
+ _dest=$2
+ have_gh || return 1
+ gh api --paginate "repos/$OWNER/$_repo/releases?per_page=100" --jq \
+ '.[] | select(.draft == false) | .tag_name as $t |
+ (.assets[]? | [
+ $t,
+ ((.name // "") | gsub("[\r\t\n]"; " ")),
+ (.size | tostring),
+ (.download_count | tostring),
+ (.browser_download_url // ""),
+ ((.content_type // "") | gsub("[\r\t]"; " "))
+ ] | @tsv)' >"$_dest" 2>/dev/null
+ return 0
+}
+
+fetch_github_latest_tag() {
+ _repo=$1
+ have_gh || return 1
+ gh api "repos/$OWNER/$_repo/releases/latest" --jq .tag_name 2>/dev/null
+}
+
+excerpt_text() {
+ awk 'BEGIN { n = 0 }
+ {
+ line = $0
+ gsub(/[ \t]+/, " ", line)
+ if (n > 0)
+ buf = buf " "
+ buf = buf line
+ n++
+ }
+ END {
+ if (length(buf) > 400)
+ buf = substr(buf, 1, 400) "..."
+ gsub(/\t/, " ", buf)
+ print buf
+ }' "$1"
+}
+
who_cell() {
awk -f "$AWK" -v mode=who \
-v userpfx="$1" -v login="$2" -v name="$3" \
@@ -273,6 +578,18 @@ git_nav() {
printf '<a href="%srefs/">refs</a>' "$(esc "$_r")"
fi
printf ' · '
+ if [ "$_here" = tags ] || [ "$_here" = tag ]; then
+ printf 'tags'
+ else
+ printf '<a href="%stags/">tags</a>' "$(esc "$_r")"
+ fi
+ printf ' · '
+ if [ "$_here" = releases ] || [ "$_here" = release ] || [ "$_here" = latest ]; then
+ printf 'releases'
+ else
+ printf '<a href="%sreleases/">releases</a>' "$(esc "$_r")"
+ fi
+ printf ' · '
if [ "$_here" = people ]; then
printf 'people'
else
@@ -291,6 +608,15 @@ git_nav() {
if [ "$_here" = blob ]; then
printf ' · file'
fi
+ if [ "$_here" = tag ]; then
+ printf ' · tag'
+ fi
+ if [ "$_here" = release ]; then
+ printf ' · release'
+ fi
+ if [ "$_here" = latest ]; then
+ printf ' · latest'
+ fi
printf '</p>\n'
}
@@ -331,6 +657,9 @@ $1 == "lang" && $3 != "" && $5 != "" {
END { print "}" }
' "$MAPFILE" >"$OUT/data/linguist-colors.json"
+awk -f "$RENDER" -v mode=famjson -v mapfile="$MAPFILE" \
+ </dev/null >"$OUT/data/linguist-fams.json"
+
index_rows=$OUT/data/git-index.tsv
all_commits=$OUT/data/git-commits.tsv
all_people=$OUT/data/git-people.tsv
@@ -355,7 +684,8 @@ build_repo() {
base=$OUT/git/$name
mkdir -p "$base/log" "$base/refs" "$base/tree" "$base/commit" "$base/blob" \
- "$base/people" "$base/lang"
+ "$base/people" "$base/lang" "$base/tags" "$base/releases/tag" \
+ "$base/releases/latest" "$base/.bodies"
userpfx="../../../../git/users/"
reporoot="../../../../git/$name/"
@@ -499,6 +829,21 @@ build_repo() {
rm -f "$base/.langs-bar.tsv"
fi
+ collect_repo_meta "$name" "$dir" "$base"
+ tagstsv=$base/.tags.tsv
+ reltsv=$base/.releases.tsv
+ assetstsv=$base/.assets.tsv
+ n_tags=0
+ n_releases=0
+ latest_tag=
+ if [ -s "$tagstsv" ]; then
+ n_tags=$(wc -l <"$tagstsv" | tr -d ' ')
+ fi
+ if [ -s "$reltsv" ]; then
+ n_releases=$(wc -l <"$reltsv" | tr -d ' ')
+ latest_tag=$(awk -F '\t' '$7 == "yes" { print $1; exit }' "$reltsv")
+ fi
+
GIT_KIND=repo
GIT_REPO=$name
GIT_USER=
@@ -519,6 +864,16 @@ build_repo() {
"$(esc "$ncommits")"
printf '<div class="dl-row"><span class="muted">Files</span><span>%s</span></div>\n' \
"$(esc "$nfiles")"
+ printf '<div class="dl-row"><span class="muted">Tags</span><span id="git-ntags"><a href="%stags/">%s</a></span></div>\n' \
+ "$(esc "$reporoot")" "$(esc "$n_tags")"
+ printf '<div class="dl-row"><span class="muted">Releases</span><span id="git-nreleases"><a href="%sreleases/">%s</a></span></div>\n' \
+ "$(esc "$reporoot")" "$(esc "$n_releases")"
+ if [ -n "$latest_tag" ]; then
+ printf '<div class="dl-row"><span class="muted">Latest</span><span id="git-latest"><a href="%sreleases/latest/"><code>%s</code></a></span></div>\n' \
+ "$(esc "$reporoot")" "$(esc "$latest_tag")"
+ else
+ printf '%s\n' '<div class="dl-row"><span class="muted">Latest</span><span id="git-latest" class="muted">none</span></div>'
+ fi
printf '<div class="dl-row"><span class="muted">Clone</span><span><code>git clone %s.git</code></span></div>\n' \
"$(esc "$gh")"
printf '%s\n' '</div>'
@@ -543,10 +898,10 @@ build_repo() {
printf '%s\n' '<p><a href="log/">Full log</a></p>'
if git -C "$dir" cat-file -e HEAD:README.md 2>/dev/null; then
printf '%s\n' '<h3>README.md</h3>'
- git -C "$dir" show HEAD:README.md | awk -f "$AWK" -v mode=pre
+ git -C "$dir" show HEAD:README.md | render_blob Markdown README.md
elif git -C "$dir" cat-file -e HEAD:README 2>/dev/null; then
printf '%s\n' '<h3>README</h3>'
- git -C "$dir" show HEAD:README | awk -f "$AWK" -v mode=pre
+ git -C "$dir" show HEAD:README | render_blob Markdown README
fi
} >>"$base/index.html"
page_end "$base/index.html"
@@ -584,18 +939,24 @@ build_repo() {
done
printf '%s\n' '</ul>'
printf '%s\n' '<h3>Tags</h3>'
- printf '%s\n' '<ul class="plain">'
- git -C "$dir" for-each-ref --format='%(objectname) %(refname:short)' refs/tags |
- while IFS= read -r line
- do
- [ -n "$line" ] || continue
- h=${line%% *}
- r=${line#* }
- c=$(git -C "$dir" rev-parse "$r^{commit}" 2>/dev/null || printf '%s\n' "$h")
- printf '<li><a href="../commit/%s/">%s</a></li>\n' \
- "$(esc "$c")" "$(esc "$r")"
- done
- printf '%s\n' '</ul>'
+ printf '<p><a href="%stags/">All tags</a>' "$(esc "$reporoot")"
+ if [ "$n_tags" -gt 0 ]; then
+ printf ' · %s' "$(esc "$n_tags")"
+ fi
+ printf '</p>\n'
+ if [ -s "$tagstsv" ]; then
+ head -n 15 "$tagstsv" |
+ awk -f "$AWK" -v mode=tags \
+ -v tagpfx="${reporoot}tags/" \
+ -v commitpfx="${reporoot}commit/" \
+ -v relpfx="${reporoot}releases/tag/"
+ if [ "$n_tags" -gt 15 ]; then
+ printf '<p class="muted">%s more on the <a href="%stags/">tags</a> page.</p>\n' \
+ "$(esc $((n_tags - 15)))" "$(esc "$reporoot")"
+ fi
+ else
+ printf '%s\n' '<p class="muted">No tags in this tree.</p>'
+ fi
} >>"$base/refs/index.html"
page_end "$base/refs/index.html"
@@ -649,7 +1010,7 @@ build_repo() {
printf '%s\n' '<h3>Diff</h3>'
git -C "$dir" show --format= --color=never "$hash" |
dd bs=1024 count=300 2>/dev/null |
- awk -f "$AWK" -v mode=pre
+ render_blob Diff "$hash.diff"
} >>"$cdir/index.html"
page_end "$cdir/index.html"
if [ -n "$shortc" ] && [ "$shortc" != "$hash" ]; then
@@ -789,7 +1150,7 @@ build_repo() {
elif [ "$szn" -gt "$MAXBLOB" ]; then
printf '%s\n' '<p>File is too large to embed here. Clone the repository or open the GitHub mirror.</p>'
else
- git -C "$dir" cat-file blob "$obj" | awk -f "$AWK" -v mode=pre
+ git -C "$dir" cat-file blob "$obj" | render_blob "$flang" "$path"
fi
} >>"$bdest"
page_end "$bdest"
@@ -879,14 +1240,230 @@ build_repo() {
done <"$base/.lang-index.tsv"
fi
+ GIT_KIND=tags
+ GIT_SHA=
+ GIT_PATH=
+ page_begin "$base/tags/index.html" "Tags - $name - Splux Git" "Tags in $name"
+ {
+ git_nav "$name" tags
+ printf '%s\n' '<h2>Tags</h2>'
+ printf '%s\n' '<hr class="rule">'
+ printf '%s\n' '<p class="live-note" id="git-live" hidden>Updated from GitHub.</p>'
+ printf '%s\n' '<p>Every tag in this tree. Annotated tags keep their message. A tag with a GitHub release links through to that page. Source archives are zip and tar.gz from GitHub.</p>'
+ printf '%s\n' '<p><label for="git-filter">Filter</label> <input id="git-filter" type="search" placeholder="tag"></p>'
+ if [ -s "$tagstsv" ]; then
+ awk -f "$AWK" -v mode=tags \
+ -v tagpfx="${reporoot}tags/" \
+ -v commitpfx="${reporoot}commit/" \
+ -v relpfx="${reporoot}releases/tag/" \
+ "$tagstsv"
+ else
+ printf '%s\n' '<p class="muted">No tags in this tree.</p>'
+ fi
+ } >>"$base/tags/index.html"
+ page_end "$base/tags/index.html"
+
+ if [ -s "$tagstsv" ]; then
+ while IFS= read -r _tline || [ -n "${_tline-}" ]
+ do
+ [ -n "${_tline-}" ] || continue
+ tname=$(tsv_cut "$_tline" 1)
+ tsha=$(tsv_cut "$_tline" 2)
+ tshort=$(tsv_cut "$_tline" 3)
+ tiso=$(tsv_cut "$_tline" 4)
+ tkind=$(tsv_cut "$_tline" 5)
+ tsubj=$(tsv_cut "$_tline" 6)
+ trel=$(tsv_cut "$_tline" 7)
+ tseg=$(tsv_cut "$_tline" 8)
+ [ -n "$tname" ] || continue
+ [ -n "$tseg" ] || tseg=$(pct_seg "$tname")
+ tdest=$base/tags/$tseg/index.html
+ GIT_KIND=tag
+ GIT_PATH=$tname
+ GIT_SHA=$tsha
+ page_begin "$tdest" "$tname - $name - Splux Git" "Tag $tname in $name"
+ {
+ git_nav "$name" tag
+ printf '<h2><code>%s</code></h2>\n' "$(esc "$tname")"
+ printf '%s\n' '<hr class="rule">'
+ printf '%s\n' '<p class="live-note" id="git-live" hidden>Updated from GitHub.</p>'
+ printf '%s\n' '<div class="info">'
+ if [ "$tkind" = annotated ]; then
+ printf '%s\n' '<div class="dl-row"><span class="muted">Kind</span><span><span class="badge">annotated</span></span></div>'
+ else
+ printf '%s\n' '<div class="dl-row"><span class="muted">Kind</span><span><span class="badge">lightweight</span></span></div>'
+ fi
+ if [ -n "$tsha" ]; then
+ printf '<div class="dl-row"><span class="muted">Commit</span><span><a href="%scommit/%s/"><code>%s</code></a></span></div>\n' \
+ "$(esc "$reporoot")" "$(esc "$tsha")" "$(esc "${tshort:-$tsha}")"
+ fi
+ if [ -n "$tiso" ]; then
+ printf '<div class="dl-row"><span class="muted">Date</span><span><time datetime="%s">%s</time></span></div>\n' \
+ "$(esc "$tiso")" "$(esc "$tiso")"
+ fi
+ if [ -n "$trel" ]; then
+ printf '<div class="dl-row"><span class="muted">Release</span><span><a href="%sreleases/tag/%s/">%s</a></span></div>\n' \
+ "$(esc "$reporoot")" "$(esc "$tseg")" "$(esc "$tname")"
+ fi
+ printf '<div class="dl-row"><span class="muted">GitHub</span><span><a href="%s/releases/tag/%s">mirror</a></span></div>\n' \
+ "$(esc "$gh")" "$(esc "$tseg")"
+ printf '%s\n' '</div>'
+ if [ -n "$tsubj" ]; then
+ printf '<p>%s</p>\n' "$(esc "$tsubj")"
+ fi
+ source_archives "$name" "$tname"
+ } >>"$tdest"
+ page_end "$tdest"
+ done <"$tagstsv"
+ fi
+
+ GIT_KIND=releases
+ GIT_SHA=
+ GIT_PATH=
+ page_begin "$base/releases/index.html" "Releases - $name - Splux Git" \
+ "GitHub releases for $name"
+ {
+ git_nav "$name" releases
+ printf '%s\n' '<h2>Releases</h2>'
+ printf '%s\n' '<hr class="rule">'
+ printf '%s\n' '<p class="live-note" id="git-live" hidden>Updated from GitHub.</p>'
+ printf '%s\n' '<p>GitHub releases for this tree. ISO files and other assets stay on GitHub. Notes are Markdown. <a href="latest/">Latest</a></p>'
+ printf '%s\n' '<p><label for="git-filter">Filter</label> <input id="git-filter" type="search" placeholder="release"></p>'
+ if [ -s "$reltsv" ]; then
+ awk -f "$AWK" -v mode=releases \
+ -v relpfx="${reporoot}releases/tag/" \
+ -v tagpfx="${reporoot}tags/" \
+ -v userpfx="$userpfx" \
+ "$reltsv"
+ else
+ printf '%s\n' '<p class="muted">No GitHub releases yet.</p>'
+ fi
+ } >>"$base/releases/index.html"
+ page_end "$base/releases/index.html"
+
+ write_release_page() {
+ _rdest=$1
+ _rhere=$2
+ _rline=$3
+ _rtag=$(tsv_cut "$_rline" 1)
+ _rtitle=$(tsv_cut "$_rline" 2)
+ _riso=$(tsv_cut "$_rline" 3)
+ _rlogin=$(tsv_cut "$_rline" 4)
+ _ravatar=$(tsv_cut "$_rline" 5)
+ _rpre=$(tsv_cut "$_rline" 6)
+ _rlatest=$(tsv_cut "$_rline" 7)
+ _rnassets=$(tsv_cut "$_rline" 8)
+ _rreact=$(tsv_cut "$_rline" 10)
+ _rseg=$(tsv_cut "$_rline" 11)
+ [ -n "$_rtag" ] || return 0
+ [ -n "$_rtitle" ] || _rtitle=$_rtag
+ [ -n "$_rseg" ] || _rseg=$(pct_seg "$_rtag")
+ GIT_KIND=$_rhere
+ GIT_PATH=$_rtag
+ GIT_SHA=
+ page_begin "$_rdest" "$_rtitle - $name - Splux Git" "Release $_rtag of $name"
+ {
+ git_nav "$name" "$_rhere"
+ printf '<h2>%s</h2>\n' "$(esc "$_rtitle")"
+ printf '%s\n' '<hr class="rule">'
+ printf '%s\n' '<p class="live-note" id="git-live" hidden>Updated from GitHub.</p>'
+ printf '%s\n' '<p class="meta">'
+ if [ "$_rlatest" = yes ]; then
+ printf '%s' '<span class="badge latest">Latest</span> '
+ fi
+ if [ "$_rpre" = yes ]; then
+ printf '%s' '<span class="badge">Pre-release</span> '
+ fi
+ printf '</p>\n'
+ printf '%s\n' '<div class="info">'
+ printf '<div class="dl-row"><span class="muted">Tag</span><span><a href="%stags/%s/"><code>%s</code></a></span></div>\n' \
+ "$(esc "$reporoot")" "$(esc "$_rseg")" "$(esc "$_rtag")"
+ if [ -n "$_riso" ]; then
+ printf '<div class="dl-row"><span class="muted">Date</span><span><time datetime="%s">%s</time></span></div>\n' \
+ "$(esc "$_riso")" "$(esc "$_riso")"
+ fi
+ if [ -n "$_rlogin" ]; then
+ printf '<div class="dl-row"><span class="muted">Author</span><span>%s</span></div>\n' \
+ "$(who_cell "$userpfx" "$_rlogin" "$_rlogin" "$_ravatar" "")"
+ fi
+ if [ -n "$_rnassets" ] && [ "$_rnassets" != 0 ]; then
+ printf '<div class="dl-row"><span class="muted">Files</span><span>%s</span></div>\n' \
+ "$(esc "$_rnassets")"
+ fi
+ if [ -n "$_rreact" ] && [ "$_rreact" != 0 ]; then
+ printf '<div class="dl-row"><span class="muted">Reactions</span><span>%s</span></div>\n' \
+ "$(esc "$_rreact")"
+ fi
+ printf '<div class="dl-row"><span class="muted">GitHub</span><span><a href="%s/releases/tag/%s">mirror</a></span></div>\n' \
+ "$(esc "$gh")" "$(esc "$_rseg")"
+ printf '%s\n' '</div>'
+ _body=$base/.bodies/$_rseg
+ if [ -s "$_body" ]; then
+ printf '%s\n' '<h3>Notes</h3>'
+ awk -f "$RENDER" -v mode=markdown -v mapfile="$MAPFILE" "$_body"
+ fi
+ _atmp=$base/.assets-one.tsv
+ : >"$_atmp"
+ if [ -s "$assetstsv" ]; then
+ awk -F '\t' -v OFS='\t' -v t="$_rtag" \
+ '$1 == t { print $2, $3, $4, $5, $6 }' \
+ "$assetstsv" >"$_atmp"
+ fi
+ if [ -s "$_atmp" ]; then
+ printf '%s\n' '<h3>Assets</h3>'
+ awk -f "$AWK" -v mode=assets "$_atmp"
+ fi
+ rm -f "$_atmp"
+ source_archives "$name" "$_rtag"
+ } >>"$_rdest"
+ page_end "$_rdest"
+ }
+
+ if [ -s "$reltsv" ]; then
+ while IFS= read -r _rline || [ -n "${_rline-}" ]
+ do
+ [ -n "${_rline-}" ] || continue
+ _rseg=$(tsv_cut "$_rline" 11)
+ _rtag=$(tsv_cut "$_rline" 1)
+ [ -n "$_rtag" ] || continue
+ [ -n "$_rseg" ] || _rseg=$(pct_seg "$_rtag")
+ write_release_page "$base/releases/tag/$_rseg/index.html" release "$_rline"
+ done <"$reltsv"
+ fi
+
+ GIT_KIND=latest
+ GIT_PATH=$latest_tag
+ latest_line=
+ if [ -s "$reltsv" ]; then
+ latest_line=$(awk -F '\t' '$7 == "yes" { print; exit }' "$reltsv")
+ if [ -z "$latest_line" ]; then
+ latest_line=$(head -n 1 "$reltsv")
+ fi
+ fi
+ if [ -n "$latest_line" ]; then
+ write_release_page "$base/releases/latest/index.html" latest "$latest_line"
+ else
+ page_begin "$base/releases/latest/index.html" "Latest - $name - Splux Git" \
+ "Latest GitHub release of $name"
+ {
+ git_nav "$name" latest
+ printf '%s\n' '<h2>Latest</h2>'
+ printf '%s\n' '<hr class="rule">'
+ printf '%s\n' '<p class="muted">No GitHub releases yet.</p>'
+ } >>"$base/releases/latest/index.html"
+ page_end "$base/releases/latest/index.html"
+ fi
+
day=${lastiso%%T*}
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
"$name" "$repo_desc" "$short" "$head" "$ncommits" "$nfiles" "$day" "$gh" \
>>"$index_rows"
rm -f "$langtmp" "$clog" "$base/.entries.tsv" "$base/.people.tsv" \
- "$filelang" "$base/.lang-index.tsv"
- printf '%s\n' "git: $name ($ncommits commits, $nfiles files)" >&2
+ "$filelang" "$base/.lang-index.tsv" "$base/.tags.tsv" \
+ "$base/.releases.tsv" "$base/.assets.tsv"
+ rm -rf "$base/.bodies"
+ printf '%s\n' "git: $name ($ncommits commits, $nfiles files, $n_tags tags, $n_releases releases)" >&2
}
build_repo SPS "$SPS"
diff --git a/tools/git-lang.awk b/tools/git-lang.awk
index e111cd6..0d9dbe4 100644
--- a/tools/git-lang.awk
+++ b/tools/git-lang.awk
@@ -40,6 +40,14 @@ function loadmap( line, f, k) {
prefer(".md", "Markdown")
prefer(".markdown", "Markdown")
prefer(".scm", "Scheme")
+ fnname["recipe"] = "Shell"
+ fntype["recipe"] = "programming"
+ fncolor["recipe"] = "#89e051"
+ fngroup["recipe"] = "Shell"
+ fnname["Recipe"] = "Shell"
+ fntype["Recipe"] = "programming"
+ fncolor["Recipe"] = "#89e051"
+ fngroup["Recipe"] = "Shell"
}
function prefer(ext, lang) {
diff --git a/tools/git.awk b/tools/git.awk
index 7933240..84646e5 100644
--- a/tools/git.awk
+++ b/tools/git.awk
@@ -14,6 +14,12 @@
# -v userpfx= -v commitpfx=
# mode=entries TSV kind size name href [lang]
# -v langpfx= -v showlang=no
+# mode=tags TSV name sha short iso kind subject rel [seg]
+# -v tagpfx= -v commitpfx= -v relpfx=
+# mode=releases TSV tag title iso login avatar pre latest nassets excerpt reactions [seg]
+# -v relpfx= -v tagpfx= -v userpfx=
+# mode=assets TSV name size downloads url [type]
+# mode=notes release body as text; light markdown (lists, headings, code, links)
function esc(s) {
gsub(/&/, "\\&", s)
@@ -40,6 +46,60 @@ function langhref(name) {
return langpfx slug(name) "/"
}
+function humansize(n, mb) {
+ n = n + 0
+ if (n >= 1073741824)
+ return sprintf("%.1f GB", n / 1073741824)
+ if (n >= 1048576) {
+ mb = n / 1048576
+ if (mb >= 10)
+ return sprintf("%.0f MB", mb)
+ return sprintf("%.1f MB", mb)
+ }
+ if (n >= 1024)
+ return sprintf("%.0f KB", n / 1024)
+ if (n > 0)
+ return n " B"
+ return "0 B"
+}
+
+function autolink(s, out, url) {
+ out = ""
+ while (match(s, /https:\/\/[^[:space:]<>"']+/)) {
+ url = substr(s, RSTART, RLENGTH)
+ out = out substr(s, 1, RSTART - 1) "<a href=\"" url "\">" url "</a>"
+ s = substr(s, RSTART + RLENGTH)
+ }
+ return out s
+}
+
+function inline(s, out, code) {
+ s = esc(s)
+ out = ""
+ while (match(s, /`[^`]+`/)) {
+ code = substr(s, RSTART + 1, RLENGTH - 2)
+ out = out substr(s, 1, RSTART - 1) "<code>" code "</code>"
+ s = substr(s, RSTART + RLENGTH)
+ }
+ return autolink(out s)
+}
+
+function notes_flush_p() {
+ if (para != "") {
+ print "<p>" inline(para) "</p>"
+ para = ""
+ any = 1
+ }
+}
+
+function notes_flush_ul() {
+ if (inul) {
+ print "</ul>"
+ inul = 0
+ any = 1
+ }
+}
+
function who(login, name, avatar, html, av, label) {
label = login != "" ? login : name
if (login != "") {
@@ -147,7 +207,116 @@ mode == "entries" {
elang[n] = $5
}
+mode == "tags" {
+ n++
+ name[n] = $1
+ hash[n] = $2
+ short[n] = $3
+ iso[n] = $4
+ kind[n] = $5
+ subject[n] = $6
+ rel[n] = $7
+ tseg[n] = ($8 != "") ? $8 : $1
+}
+
+mode == "releases" {
+ n++
+ rtag[n] = $1
+ title[n] = $2
+ iso[n] = $3
+ login[n] = $4
+ avatar[n] = $5
+ pre[n] = $6
+ latest[n] = $7
+ nassets[n] = $8
+ excerpt[n] = $9
+ react[n] = $10
+ rseg[n] = ($11 != "") ? $11 : $1
+}
+
+mode == "assets" {
+ n++
+ name[n] = $1
+ size[n] = $2
+ count[n] = $3
+ href[n] = $4
+ kind[n] = $5
+}
+
+mode == "notes" {
+ line = $0
+ sub(/\r$/, "", line)
+ if (line ~ /^```/) {
+ notes_flush_p()
+ notes_flush_ul()
+ if (inpre) {
+ print "</pre>"
+ inpre = 0
+ } else {
+ print "<pre class=\"block\">"
+ inpre = 1
+ }
+ any = 1
+ next
+ }
+ if (inpre) {
+ print esc(line)
+ any = 1
+ next
+ }
+ if (line ~ /^### /) {
+ notes_flush_p()
+ notes_flush_ul()
+ print "<h4>" inline(substr(line, 5)) "</h4>"
+ any = 1
+ next
+ }
+ if (line ~ /^## /) {
+ notes_flush_p()
+ notes_flush_ul()
+ print "<h3>" inline(substr(line, 4)) "</h3>"
+ any = 1
+ next
+ }
+ if (line ~ /^# /) {
+ notes_flush_p()
+ notes_flush_ul()
+ print "<h3>" inline(substr(line, 3)) "</h3>"
+ any = 1
+ next
+ }
+ if (line ~ /^- / || line ~ /^\* /) {
+ notes_flush_p()
+ if (!inul) {
+ print "<ul class=\"plain\">"
+ inul = 1
+ }
+ print "<li>" inline(substr(line, 3)) "</li>"
+ any = 1
+ next
+ }
+ if (line ~ /^[[:space:]]*$/) {
+ notes_flush_p()
+ notes_flush_ul()
+ next
+ }
+ notes_flush_ul()
+ if (para != "")
+ para = para " " line
+ else
+ para = line
+}
+
END {
+ if (mode == "notes") {
+ if (inpre)
+ print "</pre>"
+ notes_flush_p()
+ notes_flush_ul()
+ if (!any)
+ print "<p class=\"muted\">No notes.</p>"
+ exit
+ }
if (mode == "pre") {
if (!opened)
print "<pre class=\"block git-blob\"></pre>"
@@ -316,4 +485,115 @@ END {
print "</tbody></table>"
exit
}
+ if (mode == "tags") {
+ if (n < 1) {
+ print "<p class=\"muted\">No tags in this tree.</p>"
+ exit
+ }
+ print "<table class=\"pkgs git-tags\" id=\"git-tag-table\">"
+ print "<thead><tr><th>Tag</th><th>Commit</th><th>When</th><th>Message</th><th></th></tr></thead>"
+ print "<tbody>"
+ for (i = 1; i <= n; i++) {
+ thref = (tagpfx != "") ? tagpfx esc(tseg[i]) "/" : ""
+ chref = (commitpfx != "" && hash[i] != "") ? commitpfx esc(hash[i]) "/" : ""
+ rhref = (relpfx != "" && rel[i] != "") ? relpfx esc(tseg[i]) "/" : ""
+ search = tolower(name[i] " " subject[i] " " kind[i])
+ printf "<tr data-tag=\"%s\" data-sha=\"%s\" data-search=\"%s\">", \
+ esc(name[i]), esc(hash[i]), esc(search)
+ printf "<td>"
+ if (thref != "")
+ printf "<a href=\"%s\"><code>%s</code></a>", thref, esc(name[i])
+ else
+ printf "<code>%s</code>", esc(name[i])
+ printf "</td>"
+ printf "<td>"
+ if (chref != "")
+ printf "<a href=\"%s\"><code>%s</code></a>", chref, esc(short[i])
+ else
+ printf "<code>%s</code>", esc(short[i])
+ printf "</td>"
+ day = iso[i]
+ sub(/T.*/, "", day)
+ if (iso[i] != "")
+ printf "<td><time datetime=\"%s\">%s</time></td>", \
+ esc(iso[i]), esc(day)
+ else
+ printf "<td></td>"
+ printf "<td>%s</td>", esc(subject[i])
+ printf "<td>"
+ if (kind[i] == "annotated")
+ printf "<span class=\"badge\">annotated</span> "
+ if (rhref != "")
+ printf "<a href=\"%s\">release</a>", rhref
+ print "</td></tr>"
+ }
+ print "</tbody></table>"
+ exit
+ }
+ if (mode == "releases") {
+ if (n < 1) {
+ print "<p class=\"muted\">No GitHub releases yet.</p>"
+ exit
+ }
+ print "<div class=\"release-list\" id=\"git-releases\">"
+ for (i = 1; i <= n; i++) {
+ thref = (tagpfx != "") ? tagpfx esc(rseg[i]) "/" : ""
+ rhref = (relpfx != "") ? relpfx esc(rseg[i]) "/" : ""
+ search = tolower(rtag[i] " " title[i] " " login[i])
+ printf "<article class=\"release\" data-tag=\"%s\" data-search=\"%s\">\n", \
+ esc(rtag[i]), esc(search)
+ printf "<p class=\"meta\">"
+ if (latest[i] == "yes")
+ printf "<span class=\"badge latest\">Latest</span> "
+ if (pre[i] == "yes")
+ printf "<span class=\"badge\">Pre-release</span> "
+ if (iso[i] != "") {
+ day = iso[i]
+ sub(/T.*/, "", day)
+ printf "<time datetime=\"%s\">%s</time> ", esc(iso[i]), esc(day)
+ }
+ printf "%s", who(login[i], login[i], avatar[i])
+ print "</p>"
+ printf "<h3>"
+ if (rhref != "")
+ printf "<a href=\"%s\">%s</a>", rhref, esc(title[i])
+ else
+ printf "%s", esc(title[i])
+ print "</h3>"
+ printf "<p class=\"muted\">"
+ if (thref != "")
+ printf "<a href=\"%s\"><code>%s</code></a>", thref, esc(rtag[i])
+ else
+ printf "<code>%s</code>", esc(rtag[i])
+ if (nassets[i] + 0 > 0)
+ printf " · %s files", esc(nassets[i])
+ if (react[i] + 0 > 0)
+ printf " · %s reactions", esc(react[i])
+ print "</p>"
+ if (excerpt[i] != "")
+ printf "<p class=\"summary\">%s</p>\n", esc(excerpt[i])
+ print "</article>"
+ }
+ print "</div>"
+ exit
+ }
+ if (mode == "assets") {
+ if (n < 1) {
+ print "<p class=\"muted\">No files attached.</p>"
+ exit
+ }
+ print "<table class=\"pkgs git-assets\" id=\"git-asset-table\">"
+ print "<thead><tr><th>File</th><th>Size</th><th>Downloads</th></tr></thead>"
+ print "<tbody>"
+ for (i = 1; i <= n; i++) {
+ search = tolower(name[i] " " kind[i])
+ printf "<tr data-search=\"%s\">", esc(search)
+ printf "<td><a href=\"%s\">%s</a></td>", esc(href[i]), esc(name[i])
+ printf "<td class=\"muted\">%s</td>", esc(humansize(size[i]))
+ printf "<td class=\"muted\">%s</td>", esc(count[i])
+ print "</tr>"
+ }
+ print "</tbody></table>"
+ exit
+ }
}
diff --git a/tools/render.awk b/tools/render.awk
new file mode 100644
index 0000000..d9f5bb9
--- /dev/null
+++ b/tools/render.awk
@@ -0,0 +1,1294 @@
+# Highlight source and render Markdown for Splux Git.
+# mode=highlight stdin -> <pre class="block git-blob is-hl"> -v lang=
+# mode=markdown stdin -> HTML (fenced code uses the highlighter)
+# -v mapfile=tools/linguist.map maps Linguist names onto highlighter families
+
+function esc(s) {
+ gsub(/&/, "\\&", s)
+ gsub(/</, "\\<", s)
+ gsub(/>/, "\\>", s)
+ gsub(/"/, "\\"", s)
+ return s
+}
+
+function kadd(key, words, n, a, i) {
+ n = split(words, a, /[ \t]+/)
+ for (i = 1; i <= n; i++)
+ if (a[i] != "")
+ KW[key SUBSEP a[i]] = 1
+}
+
+function iskw(lang, fam, w) {
+ return (lang SUBSEP w) in KW || (fam SUBSEP w) in KW
+}
+
+function sp(cls, s) {
+ if (s == "")
+ return ""
+ return "<span class=\"" cls "\">" esc(s) "</span>"
+}
+
+function load_langmap( line, f) {
+ if (mapfile == "")
+ mapfile = "tools/linguist.map"
+ while ((getline line < mapfile) > 0) {
+ split(line, f, "\t")
+ if (f[1] == "lang" && f[3] != "") {
+ LGROUP[f[3]] = (f[6] != "") ? f[6] : f[3]
+ LTYPE[f[3]] = f[4]
+ }
+ }
+ close(mapfile)
+}
+
+function setfam(name, fam) {
+ FAM[name] = fam
+}
+
+function load_fams() {
+ setfam("Shell", "shell"); setfam("Bash", "shell"); setfam("Zsh", "shell")
+ setfam("Fish", "shell"); setfam("Nushell", "shell")
+ setfam("PowerShell", "shell"); setfam("Batchfile", "shell")
+ setfam("Alpine Abuild", "shell"); setfam("Gentoo Ebuild", "shell")
+ setfam("C", "clike"); setfam("C++", "clike"); setfam("C#", "clike")
+ setfam("Objective-C", "clike"); setfam("Objective-C++", "clike")
+ setfam("Java", "clike"); setfam("JavaScript", "clike")
+ setfam("TypeScript", "clike"); setfam("Go", "clike"); setfam("Rust", "clike")
+ setfam("PHP", "clike"); setfam("Kotlin", "clike"); setfam("Scala", "clike")
+ setfam("Swift", "clike"); setfam("Dart", "clike"); setfam("D", "clike")
+ setfam("Groovy", "clike"); setfam("Apex", "clike"); setfam("Vala", "clike")
+ setfam("Zig", "clike"); setfam("Carbon", "clike"); setfam("V", "clike")
+ setfam("Nim", "clike"); setfam("Crystal", "clike")
+ setfam("Python", "python"); setfam("Starlark", "python")
+ setfam("Cython", "python"); setfam("Snakemake", "python")
+ setfam("Ruby", "ruby"); setfam("Perl", "perl"); setfam("Raku", "perl")
+ setfam("Awk", "awk"); setfam("Lua", "lua"); setfam("R", "r")
+ setfam("Tcl", "tcl"); setfam("SQL", "sql"); setfam("PLpgSQL", "sql")
+ setfam("TSQL", "sql"); setfam("PLSQL", "sql")
+ setfam("HTML", "xml"); setfam("XML", "xml"); setfam("SVG", "xml")
+ setfam("XSLT", "xml"); setfam("Vue", "xml"); setfam("Svelte", "xml")
+ setfam("Astro", "xml"); setfam("JSX", "xml"); setfam("TSX", "xml")
+ setfam("CSS", "css"); setfam("SCSS", "css"); setfam("Sass", "css")
+ setfam("Less", "css"); setfam("PostCSS", "css")
+ setfam("JSON", "json"); setfam("JSON5", "json"); setfam("JSONC", "json")
+ setfam("JSON with Comments", "json")
+ setfam("YAML", "yaml"); setfam("TOML", "toml"); setfam("INI", "ini")
+ setfam("Desktop", "ini"); setfam("Git Config", "ini")
+ setfam("Makefile", "make"); setfam("CMake", "cmake")
+ setfam("Diff", "diff"); setfam("Ignore List", "ini")
+ setfam("Lisp", "lisp"); setfam("Scheme", "lisp")
+ setfam("Common Lisp", "lisp"); setfam("Emacs Lisp", "lisp")
+ setfam("Clojure", "lisp"); setfam("Racket", "lisp")
+ setfam("Haskell", "haskell"); setfam("Idris", "haskell")
+ setfam("Elixir", "elixir"); setfam("Erlang", "erlang")
+ setfam("Fortran", "fortran"); setfam("Fortran Free Form", "fortran")
+ setfam("Vim Script", "vim"); setfam("Vim Help File", "vim")
+ setfam("Dockerfile", "docker"); setfam("Nix", "nix")
+ setfam("Protocol Buffer", "proto"); setfam("GraphQL", "graphql")
+ setfam("TeX", "tex"); setfam("LaTeX", "tex"); setfam("BibTeX", "tex")
+ setfam("Assembly", "asm"); setfam("Unix Assembly", "asm")
+ setfam("Markdown", "markdown"); setfam("MDX", "markdown")
+ setfam("AsciiDoc", "markdown"); setfam("reStructuredText", "markdown")
+ setfam("Pod", "markdown"); setfam("RDoc", "markdown")
+ setfam("Text", "plain"); setfam("Gettext Catalog", "plain")
+ FENCE["sh"] = "Shell"; FENCE["bash"] = "Shell"; FENCE["zsh"] = "Shell"
+ FENCE["shell"] = "Shell"; FENCE["fish"] = "Shell"
+ FENCE["js"] = "JavaScript"; FENCE["javascript"] = "JavaScript"
+ FENCE["ts"] = "TypeScript"; FENCE["typescript"] = "TypeScript"
+ FENCE["jsx"] = "JavaScript"; FENCE["tsx"] = "TypeScript"
+ FENCE["py"] = "Python"; FENCE["python"] = "Python"
+ FENCE["rb"] = "Ruby"; FENCE["ruby"] = "Ruby"
+ FENCE["pl"] = "Perl"; FENCE["perl"] = "Perl"
+ FENCE["lua"] = "Lua"; FENCE["sql"] = "SQL"
+ FENCE["c"] = "C"; FENCE["h"] = "C"; FENCE["cpp"] = "C++"; FENCE["c++"] = "C++"
+ FENCE["cc"] = "C++"; FENCE["hpp"] = "C++"; FENCE["rs"] = "Rust"
+ FENCE["rust"] = "Rust"; FENCE["go"] = "Go"; FENCE["golang"] = "Go"
+ FENCE["java"] = "Java"; FENCE["kt"] = "Kotlin"; FENCE["cs"] = "C#"
+ FENCE["php"] = "PHP"; FENCE["swift"] = "Swift"; FENCE["dart"] = "Dart"
+ FENCE["html"] = "HTML"; FENCE["xml"] = "XML"; FENCE["svg"] = "SVG"
+ FENCE["css"] = "CSS"; FENCE["scss"] = "SCSS"; FENCE["json"] = "JSON"
+ FENCE["yml"] = "YAML"; FENCE["yaml"] = "YAML"; FENCE["toml"] = "TOML"
+ FENCE["md"] = "Markdown"; FENCE["markdown"] = "Markdown"
+ FENCE["mdx"] = "Markdown"; FENCE["mdown"] = "Markdown"
+ FENCE["awk"] = "Awk"; FENCE["make"] = "Makefile"; FENCE["makefile"] = "Makefile"
+ FENCE["mjs"] = "JavaScript"; FENCE["cjs"] = "JavaScript"
+ FENCE["pyi"] = "Python"; FENCE["hh"] = "C++"; FENCE["cxx"] = "C++"
+ FENCE["kts"] = "Kotlin"; FENCE["exs"] = "Elixir"; FENCE["gql"] = "GraphQL"
+ FENCE["s"] = "Assembly"; FENCE["vue"] = "Vue"
+ FENCE["diff"] = "Diff"; FENCE["patch"] = "Diff"
+ FENCE["dockerfile"] = "Dockerfile"; FENCE["docker"] = "Dockerfile"
+ FENCE["nix"] = "Nix"; FENCE["cmake"] = "CMake"; FENCE["r"] = "R"
+ FENCE["hs"] = "Haskell"; FENCE["lisp"] = "Lisp"; FENCE["el"] = "Emacs Lisp"
+ FENCE["vim"] = "Vim Script"; FENCE["elixir"] = "Elixir"
+ FENCE["ex"] = "Elixir"; FENCE["erl"] = "Erlang"
+ FENCE["proto"] = "Protocol Buffer"; FENCE["graphql"] = "GraphQL"
+ FENCE["tex"] = "TeX"; FENCE["latex"] = "TeX"; FENCE["asm"] = "Assembly"
+ FENCE["ini"] = "INI"; FENCE["conf"] = "INI"; FENCE["cfg"] = "INI"
+}
+
+function load_kw() {
+ kadd("clike", "if else for while do switch case default break continue return goto sizeof typedef struct union enum const static extern volatile register auto void int char short long float double signed unsigned bool true false NULL nullptr this new delete try catch throw class public private protected namespace using template typename virtual override final inline explicit friend operator constexpr static_assert alignas alignof noexcept decltype thread_local")
+ kadd("C", "restrict _Bool _Complex _Imaginary _Atomic _Static_assert _Generic _Alignas _Alignof _Noreturn _Thread_local")
+ kadd("C++", "and and_eq bitand bitor compl not not_eq or or_eq xor xor_eq concept requires co_await co_yield co_return import module export consteval constinit")
+ kadd("JavaScript", "function var let const async await yield import export from as of in instanceof typeof new this class extends super constructor get set of with debugger NaN Infinity undefined null true false")
+ kadd("TypeScript", "function var let const async await yield import export from as of in instanceof typeof new this class extends super constructor interface type enum implements declare abstract readonly namespace module never unknown any void string number boolean")
+ kadd("Java", "package import class interface enum extends implements abstract synchronized throws throw native strictfp transient volatile finally true false null instanceof var record sealed permits non-sealed")
+ kadd("Go", "func package import var const type map chan interface struct defer go select fallthrough range iota true false nil make new append cap copy delete len panic recover")
+ kadd("Rust", "fn let mut pub crate mod use struct enum impl trait where async await move ref self Self super type const static unsafe dyn match loop for in if else while break continue return true false as Box Option Result Some None Ok Err")
+ kadd("PHP", "function class interface trait namespace use as public private protected static abstract final echo print isset empty unset new clone instanceof insteadof yield from match true false null")
+ kadd("C#", "namespace using class interface struct record enum delegate event public private protected internal static abstract sealed override virtual async await var dynamic string object bool true false null get set init")
+ kadd("Kotlin", "fun val var class object interface data sealed inner companion when in is as typealias suspend override lateinit by constructor init true false null")
+ kadd("Scala", "def val var object trait implicit lazy override sealed abstract case match forSome yield true false null")
+ kadd("Swift", "func let var class struct enum protocol extension guard defer inout associatedtype typealias actor isolated throwing async await true false nil self Self")
+ kadd("Dart", "void var final const late required factory mixin on covariant true false null")
+ kadd("Zig", "fn pub const var struct enum union error test comptime export extern packed align asm unreachable true false undefined null")
+ kadd("D", "alias mixin invariant scope lazy pure nothrow ref inout immutable shared true false null")
+ kadd("shell", "if then else elif fi for while until do done case esac in function select time coproc return break continue true false shift export local readonly declare typeset alias eval exec source trap wait jobs fg bg pwd test echo printf read cd mkdir rmdir rm cp mv cat")
+ kadd("python", "and as assert async await break class continue def del elif else except False finally for from global if import in is lambda None nonlocal not or pass raise return True try while with yield match case type")
+ kadd("ruby", "alias and begin break case class def defined? do else elsif end ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield")
+ kadd("perl", "if unless elsif else while until for foreach do done given when default sub my our local state package use require no next last redo goto return die warn eval bless shift unshift push pop split join map grep sort keys values each defined undef exists delete ref true false")
+ kadd("awk", "BEGIN END if else while do for in break continue next nextfile delete exit return print printf getline function")
+ kadd("lua", "and break do else elseif end false for function goto if in local nil not or repeat return then true until while")
+ kadd("r", "if else repeat while function for in next break TRUE FALSE NULL NA Inf NaN")
+ kadd("tcl", "if else elseif then for foreach while switch proc return break continue set unset lappend lindex lrange concat expr puts gets open close source package namespace")
+ kadd("sql", "select insert update delete from where join inner left right full outer on group by order having limit offset as and or not null is in like between exists case when then else end create alter drop table index view into values set distinct union all primary key foreign references default constraint")
+ kadd("make", "ifeq ifneq ifdef ifndef else endif include define endef export unexport override private vpath")
+ kadd("cmake", "if else elseif endif foreach endforeach while endwhile function endfunction macro endmacro set unset list string math file include option project add_executable add_library target_link_libraries message return break continue")
+ kadd("docker", "FROM RUN CMD LABEL MAINTAINER EXPOSE ENV ADD COPY ENTRYPOINT VOLUME USER WORKDIR ARG ONBUILD STOPSIGNAL HEALTHCHECK SHELL AS")
+ kadd("nix", "let in rec with inherit import if then else assert abort throw builtins true false null")
+ kadd("haskell", "module where import qualified as hiding data type class instance deriving newtype do of let in if then else case infix infixl infixr foreign default")
+ kadd("lisp", "defun defmacro defvar defparameter defun lambda if when unless cond case let let* flet labels loop progn quote setq setf funcall apply and or not nil t")
+ kadd("erlang", "after and andalso band begin bnot bor bsl bsr bxor case catch cond div end fun if let not of or orelse receive rem try when xor")
+ kadd("elixir", "def defp defmodule defmacro defmacrop defstruct defprotocol defimpl defdelegate defguard alias import require use if unless cond case with for try rescue catch after else end fn do true false nil")
+ kadd("fortran", "program end subroutine function module use implicit none integer real character logical complex parameter allocatable dimension intent in out inout if then else elseif endif do enddo while select case call return stop continue")
+ kadd("vim", "if else elseif endif for endfor while endwhile function endfunction return let unlet set setlocal noremap nnoremap inoremap vnoremap syntax highlight command autocmd augroup")
+ kadd("proto", "syntax package import option message enum service rpc returns repeated optional required oneof map reserved true false")
+ kadd("graphql", "type interface union enum input extend schema scalar query mutation subscription fragment on true false null implements")
+ kadd("asm", "section global extern db dw dd dq resb resw resd resq equ times jmp je jne jz jnz call ret push pop mov add sub mul div cmp and or xor not lea nop")
+}
+
+function jesc(s) {
+ gsub(/\\/, "\\\\", s)
+ gsub(/"/, "\\\"", s)
+ return s
+}
+
+function fam_of(lang, g) {
+ if (lang == "")
+ return "generic"
+ if (lang in FAM)
+ return FAM[lang]
+ g = LGROUP[lang]
+ if (g != "" && g in FAM)
+ return FAM[g]
+ if (g != "" && g != lang && (g in FAM))
+ return FAM[g]
+ return "generic"
+}
+
+function fence_lang(info, s) {
+ s = info
+ sub(/^[ \t{]+/, "", s)
+ sub(/[ \t}].*/, "", s)
+ if (s == "")
+ return ""
+ if (tolower(s) in FENCE)
+ return FENCE[tolower(s)]
+ if (s in FAM || s in LGROUP)
+ return s
+ if (tolower(s) == "text" || tolower(s) == "plain")
+ return "Text"
+ return s
+}
+
+function hl_src(text, lang, fam) {
+ fam = fam_of(lang)
+ if (fam == "plain" || fam == "markdown")
+ return esc(text)
+ if (fam == "xml")
+ return hl_xml(text)
+ if (fam == "css")
+ return hl_css(text)
+ if (fam == "json")
+ return hl_json(text)
+ if (fam == "diff")
+ return hl_diff(text)
+ if (fam == "lisp")
+ return hl_linec(text, lang, fam, ";")
+ if (fam == "sql")
+ return hl_sql(text, lang)
+ if (fam == "lua")
+ return hl_lua(text)
+ if (fam == "haskell")
+ return hl_linec(text, lang, fam, "--")
+ if (fam == "erlang")
+ return hl_linec(text, lang, fam, "%")
+ if (fam == "tex")
+ return hl_linec(text, lang, fam, "%")
+ if (fam == "vim")
+ return hl_vim(text)
+ if (fam == "fortran")
+ return hl_linec(text, lang, fam, "!")
+ if (fam == "asm")
+ return hl_linec(text, lang, fam, ";")
+ if (fam == "clike")
+ return hl_clike(text, lang, fam)
+ if (fam == "python")
+ return hl_python(text, lang)
+ if (fam == "elixir")
+ return hl_hash(text, lang, fam)
+ return hl_hash(text, lang, fam)
+}
+
+function take_word(text, i, j, n, c) {
+ n = length(text)
+ j = i
+ while (j <= n) {
+ c = substr(text, j, 1)
+ if (c !~ /[A-Za-z0-9_]/)
+ break
+ j++
+ }
+ return j - i
+}
+
+function take_num(text, i, j, n, c, dot) {
+ n = length(text)
+ j = i
+ dot = 0
+ if (substr(text, i, 2) == "0x" || substr(text, i, 2) == "0X") {
+ j = i + 2
+ while (j <= n && substr(text, j, 1) ~ /[0-9A-Fa-f_]/)
+ j++
+ return j - i
+ }
+ while (j <= n) {
+ c = substr(text, j, 1)
+ if (c ~ /[0-9_]/)
+ j++
+ else if (c == "." && dot == 0) {
+ dot = 1
+ j++
+ } else if ((c == "e" || c == "E") && j < n) {
+ j++
+ c = substr(text, j, 1)
+ if (c == "+" || c == "-")
+ j++
+ while (j <= n && substr(text, j, 1) ~ /[0-9]/)
+ j++
+ break
+ } else
+ break
+ }
+ return j - i
+}
+
+function hl_string(text, i, q, j, n, c, escst) {
+ n = length(text)
+ j = i + 1
+ escst = 0
+ while (j <= n) {
+ c = substr(text, j, 1)
+ if (escst) {
+ escst = 0
+ j++
+ continue
+ }
+ if (c == "\\") {
+ escst = 1
+ j++
+ continue
+ }
+ if (c == q) {
+ j++
+ break
+ }
+ j++
+ }
+ return j - i
+}
+
+function hl_hash(text, lang, fam, i, n, c, out, wlen, word, q, nxt, lnstart) {
+ n = length(text)
+ i = 1
+ out = ""
+ lnstart = 1
+ while (i <= n) {
+ c = substr(text, i, 1)
+ if (c == "\n") {
+ out = out c
+ i++
+ lnstart = 1
+ continue
+ }
+ if (c ~ /[ \t]/) {
+ out = out c
+ i++
+ continue
+ }
+ if (c == "#" && (fam != "elixir" || substr(text, i, 2) != "#{")) {
+ wlen = n - i + 1
+ nxt = index(substr(text, i), "\n")
+ if (nxt > 0)
+ wlen = nxt - 1
+ out = out sp("c", substr(text, i, wlen))
+ i += wlen
+ lnstart = 0
+ continue
+ }
+ if (c == "\"" || c == "'") {
+ wlen = hl_string(text, i, c)
+ out = out sp("s", substr(text, i, wlen))
+ i += wlen
+ lnstart = 0
+ continue
+ }
+ if (c == "`" && fam == "shell") {
+ wlen = hl_string(text, i, "`")
+ out = out sp("s", substr(text, i, wlen))
+ i += wlen
+ lnstart = 0
+ continue
+ }
+ if (c ~ /[0-9]/) {
+ wlen = take_num(text, i)
+ out = out sp("n", substr(text, i, wlen))
+ i += wlen
+ lnstart = 0
+ continue
+ }
+ if (c == "$" && (fam == "shell" || fam == "perl")) {
+ wlen = 1
+ if (substr(text, i + 1, 1) ~ /[A-Za-z_0-9{?!@*#$-]/) {
+ if (substr(text, i + 1, 1) == "{") {
+ nxt = index(substr(text, i), "}")
+ wlen = (nxt > 0) ? nxt : 2
+ } else
+ wlen = 1 + take_word(text, i + 1)
+ }
+ out = out sp("b", substr(text, i, wlen))
+ i += wlen
+ lnstart = 0
+ continue
+ }
+ if (c ~ /[A-Za-z_]/) {
+ wlen = take_word(text, i)
+ word = substr(text, i, wlen)
+ if (iskw(lang, fam, word))
+ out = out sp("k", word)
+ else if ((fam == "yaml" || fam == "toml" || fam == "ini") && \
+ substr(text, i + wlen, 1) == ":")
+ out = out sp("a", word)
+ else if (substr(text, i + wlen, 1) == "(")
+ out = out sp("f", word)
+ else
+ out = out esc(word)
+ i += wlen
+ lnstart = 0
+ continue
+ }
+ out = out esc(c)
+ i++
+ lnstart = 0
+ }
+ return out
+}
+
+function hl_clike(text, lang, fam, i, n, c, out, wlen, word, nxt, two) {
+ n = length(text)
+ i = 1
+ out = ""
+ while (i <= n) {
+ c = substr(text, i, 1)
+ two = substr(text, i, 2)
+ if (c == "\n" || c ~ /[ \t]/) {
+ out = out c
+ i++
+ continue
+ }
+ if (two == "//") {
+ nxt = index(substr(text, i), "\n")
+ wlen = (nxt > 0) ? nxt - 1 : n - i + 1
+ out = out sp("c", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (two == "/*") {
+ nxt = index(substr(text, i + 2), "*/")
+ wlen = (nxt > 0) ? nxt + 3 : n - i + 1
+ out = out sp("c", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c == "#" && (lang == "C" || lang == "C++" || lang == "Objective-C")) {
+ nxt = index(substr(text, i), "\n")
+ wlen = (nxt > 0) ? nxt - 1 : n - i + 1
+ out = out sp("p", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c == "'" && lang == "Rust") {
+ if (substr(text, i + 1, 1) ~ /[A-Za-z_]/) {
+ wlen = 1 + take_word(text, i + 1)
+ out = out sp("t", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ out = out esc(c)
+ i++
+ continue
+ }
+ if (c == "\"" || c == "'") {
+ wlen = hl_string(text, i, c)
+ out = out sp("s", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c == "`" && (lang == "JavaScript" || lang == "TypeScript")) {
+ wlen = hl_string(text, i, "`")
+ out = out sp("s", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c ~ /[0-9]/) {
+ wlen = take_num(text, i)
+ out = out sp("n", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c ~ /[A-Za-z_]/) {
+ wlen = take_word(text, i)
+ word = substr(text, i, wlen)
+ if (iskw(lang, fam, word) || iskw(lang, "clike", word))
+ out = out sp("k", word)
+ else if (word ~ /^[A-Z]/ && lang != "JavaScript" && lang != "TypeScript")
+ out = out sp("t", word)
+ else if (substr(text, i + wlen, 1) == "(")
+ out = out sp("f", word)
+ else
+ out = out esc(word)
+ i += wlen
+ continue
+ }
+ out = out esc(c)
+ i++
+ }
+ return out
+}
+
+function hl_python(text, lang, i, n, c, out, wlen, word, nxt, three) {
+ n = length(text)
+ i = 1
+ out = ""
+ while (i <= n) {
+ c = substr(text, i, 1)
+ three = substr(text, i, 3)
+ if (c == "\n" || c ~ /[ \t]/) {
+ out = out c
+ i++
+ continue
+ }
+ if (c == "#") {
+ nxt = index(substr(text, i), "\n")
+ wlen = (nxt > 0) ? nxt - 1 : n - i + 1
+ out = out sp("c", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (three == "\"\"\"" || three == "'''") {
+ nxt = index(substr(text, i + 3), three)
+ wlen = (nxt > 0) ? nxt + 5 : n - i + 1
+ out = out sp("s", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c == "\"" || c == "'") {
+ wlen = hl_string(text, i, c)
+ out = out sp("s", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c ~ /[0-9]/) {
+ wlen = take_num(text, i)
+ out = out sp("n", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c ~ /[A-Za-z_]/) {
+ wlen = take_word(text, i)
+ word = substr(text, i, wlen)
+ if (iskw(lang, "python", word))
+ out = out sp("k", word)
+ else if (substr(text, i + wlen, 1) == "(")
+ out = out sp("f", word)
+ else
+ out = out esc(word)
+ i += wlen
+ continue
+ }
+ out = out esc(c)
+ i++
+ }
+ return out
+}
+
+function hl_linec(text, lang, fam, mark, i, n, c, out, wlen, word, nxt, mlen) {
+ n = length(text)
+ i = 1
+ out = ""
+ mlen = length(mark)
+ while (i <= n) {
+ c = substr(text, i, 1)
+ if (c == "\n" || c ~ /[ \t]/) {
+ out = out c
+ i++
+ continue
+ }
+ if (substr(text, i, mlen) == mark) {
+ nxt = index(substr(text, i), "\n")
+ wlen = (nxt > 0) ? nxt - 1 : n - i + 1
+ out = out sp("c", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (fam == "haskell" && substr(text, i, 2) == "{-") {
+ nxt = index(substr(text, i + 2), "-}")
+ wlen = (nxt > 0) ? nxt + 3 : n - i + 1
+ out = out sp("c", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c == "\"" || c == "'") {
+ wlen = hl_string(text, i, c)
+ out = out sp("s", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c ~ /[0-9]/) {
+ wlen = take_num(text, i)
+ out = out sp("n", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c ~ /[A-Za-z_]/) {
+ wlen = take_word(text, i)
+ word = substr(text, i, wlen)
+ if (iskw(lang, fam, word))
+ out = out sp("k", word)
+ else if (substr(text, i + wlen, 1) == "(")
+ out = out sp("f", word)
+ else
+ out = out esc(word)
+ i += wlen
+ continue
+ }
+ out = out esc(c)
+ i++
+ }
+ return out
+}
+
+function hl_lua(text, i, n, c, out, wlen, word, nxt, two) {
+ n = length(text)
+ i = 1
+ out = ""
+ while (i <= n) {
+ c = substr(text, i, 1)
+ two = substr(text, i, 2)
+ if (c == "\n" || c ~ /[ \t]/) {
+ out = out c
+ i++
+ continue
+ }
+ if (substr(text, i, 4) == "--[[") {
+ nxt = index(substr(text, i + 4), "]]")
+ wlen = (nxt > 0) ? nxt + 5 : n - i + 1
+ out = out sp("c", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (two == "--") {
+ nxt = index(substr(text, i), "\n")
+ wlen = (nxt > 0) ? nxt - 1 : n - i + 1
+ out = out sp("c", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c == "\"" || c == "'") {
+ wlen = hl_string(text, i, c)
+ out = out sp("s", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c ~ /[0-9]/) {
+ wlen = take_num(text, i)
+ out = out sp("n", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c ~ /[A-Za-z_]/) {
+ wlen = take_word(text, i)
+ word = substr(text, i, wlen)
+ if (iskw("Lua", "lua", word))
+ out = out sp("k", word)
+ else if (substr(text, i + wlen, 1) == "(")
+ out = out sp("f", word)
+ else
+ out = out esc(word)
+ i += wlen
+ continue
+ }
+ out = out esc(c)
+ i++
+ }
+ return out
+}
+
+function hl_vim(text) {
+ return hl_linec(text, "Vim Script", "vim", "\"")
+}
+
+function hl_sql(text, lang, i, n, c, out, wlen, word, nxt, two, up) {
+ n = length(text)
+ i = 1
+ out = ""
+ while (i <= n) {
+ c = substr(text, i, 1)
+ two = substr(text, i, 2)
+ if (c == "\n" || c ~ /[ \t]/) {
+ out = out c
+ i++
+ continue
+ }
+ if (two == "--") {
+ nxt = index(substr(text, i), "\n")
+ wlen = (nxt > 0) ? nxt - 1 : n - i + 1
+ out = out sp("c", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (two == "/*") {
+ nxt = index(substr(text, i + 2), "*/")
+ wlen = (nxt > 0) ? nxt + 3 : n - i + 1
+ out = out sp("c", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c == "'" || c == "\"") {
+ wlen = hl_string(text, i, c)
+ out = out sp("s", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c ~ /[0-9]/) {
+ wlen = take_num(text, i)
+ out = out sp("n", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c ~ /[A-Za-z_]/) {
+ wlen = take_word(text, i)
+ word = substr(text, i, wlen)
+ up = toupper(word)
+ if (iskw(lang, "sql", tolower(word)) || iskw(lang, "sql", up))
+ out = out sp("k", word)
+ else
+ out = out esc(word)
+ i += wlen
+ continue
+ }
+ out = out esc(c)
+ i++
+ }
+ return out
+}
+
+function hl_xml(text, i, n, c, out, wlen, nxt, two, inattr) {
+ n = length(text)
+ i = 1
+ out = ""
+ while (i <= n) {
+ c = substr(text, i, 1)
+ two = substr(text, i, 2)
+ if (substr(text, i, 4) == "<!--") {
+ nxt = index(substr(text, i + 4), "-->")
+ wlen = (nxt > 0) ? nxt + 6 : n - i + 1
+ out = out sp("c", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c == "<") {
+ nxt = index(substr(text, i), ">")
+ wlen = (nxt > 0) ? nxt : n - i + 1
+ out = out hl_tag(substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ out = out esc(c)
+ i++
+ }
+ return out
+}
+
+function hl_tag(tag, i, n, c, out, wlen, word) {
+ n = length(tag)
+ i = 1
+ out = ""
+ while (i <= n) {
+ c = substr(tag, i, 1)
+ if (c ~ /[ \t\n]/) {
+ out = out c
+ i++
+ continue
+ }
+ if (c == "\"" || c == "'") {
+ wlen = hl_string(tag, i, c)
+ out = out sp("s", substr(tag, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c ~ /[A-Za-z_:]/) {
+ wlen = 0
+ while (i + wlen <= n && substr(tag, i + wlen, 1) ~ /[A-Za-z0-9_:-]/)
+ wlen++
+ word = substr(tag, i, wlen)
+ if (i == 1 || substr(tag, i - 1, 1) == "<" || substr(tag, i - 1, 1) == "/")
+ out = out sp("g", word)
+ else
+ out = out sp("a", word)
+ i += wlen
+ continue
+ }
+ out = out esc(c)
+ i++
+ }
+ return out
+}
+
+function hl_css(text, i, n, c, out, wlen, nxt, two, word) {
+ n = length(text)
+ i = 1
+ out = ""
+ while (i <= n) {
+ c = substr(text, i, 1)
+ two = substr(text, i, 2)
+ if (two == "/*") {
+ nxt = index(substr(text, i + 2), "*/")
+ wlen = (nxt > 0) ? nxt + 3 : n - i + 1
+ out = out sp("c", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c == "\"" || c == "'") {
+ wlen = hl_string(text, i, c)
+ out = out sp("s", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c == "#") {
+ wlen = 1
+ while (i + wlen <= n && substr(text, i + wlen, 1) ~ /[0-9A-Fa-f]/)
+ wlen++
+ if (wlen > 1) {
+ out = out sp("n", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ }
+ if (c ~ /[A-Za-z_-]/) {
+ wlen = 0
+ while (i + wlen <= n && substr(text, i + wlen, 1) ~ /[A-Za-z0-9_-]/)
+ wlen++
+ word = substr(text, i, wlen)
+ if (substr(text, i + wlen, 1) == ":")
+ out = out sp("a", word)
+ else
+ out = out sp("g", word)
+ i += wlen
+ continue
+ }
+ if (c ~ /[0-9]/) {
+ wlen = take_num(text, i)
+ out = out sp("n", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ out = out esc(c)
+ i++
+ }
+ return out
+}
+
+function hl_json(text, i, n, c, out, wlen, word) {
+ n = length(text)
+ i = 1
+ out = ""
+ while (i <= n) {
+ c = substr(text, i, 1)
+ if (c == "\n" || c ~ /[ \t]/) {
+ out = out c
+ i++
+ continue
+ }
+ if (c == "\"") {
+ wlen = hl_string(text, i, "\"")
+ word = substr(text, i, wlen)
+ if (looking_colon(text, i + wlen))
+ out = out sp("a", word)
+ else
+ out = out sp("s", word)
+ i += wlen
+ continue
+ }
+ if (c ~ /[0-9-]/ && (c != "-" || substr(text, i + 1, 1) ~ /[0-9]/)) {
+ if (c == "-") {
+ wlen = 1 + take_num(text, i + 1)
+ } else
+ wlen = take_num(text, i)
+ out = out sp("n", substr(text, i, wlen))
+ i += wlen
+ continue
+ }
+ if (c ~ /[A-Za-z]/) {
+ wlen = take_word(text, i)
+ word = substr(text, i, wlen)
+ if (word == "true" || word == "false" || word == "null")
+ out = out sp("k", word)
+ else
+ out = out esc(word)
+ i += wlen
+ continue
+ }
+ out = out esc(c)
+ i++
+ }
+ return out
+}
+
+function looking_colon(text, i, n, c) {
+ n = length(text)
+ while (i <= n) {
+ c = substr(text, i, 1)
+ if (c ~ /[ \t\n]/) {
+ i++
+ continue
+ }
+ return c == ":"
+ }
+ return 0
+}
+
+function hl_diff(text, i, n, line, out, nxt, first) {
+ n = length(text)
+ i = 1
+ out = ""
+ while (i <= n) {
+ nxt = index(substr(text, i), "\n")
+ if (nxt > 0) {
+ line = substr(text, i, nxt)
+ i += nxt
+ } else {
+ line = substr(text, i)
+ i = n + 1
+ }
+ first = substr(line, 1, 1)
+ if (line ~ /^diff / || line ~ /^index / || line ~ /^--- / || line ~ /^\+\+\+ /)
+ out = out sp("p", line)
+ else if (line ~ /^@@/)
+ out = out sp("t", line)
+ else if (first == "+")
+ out = out sp("s", line)
+ else if (first == "-")
+ out = out sp("g", line)
+ else
+ out = out esc(line)
+ }
+ return out
+}
+
+function wrap_hl(html, lang, label) {
+ label = (lang != "") ? lang : "text"
+ return "<pre class=\"block git-blob is-hl\" data-lang=\"" esc(lang) \
+ "\" aria-label=\"" esc(label) "\"><code>" html "</code></pre>"
+}
+
+function md_split(text, n, i, c, line) {
+ n = 0
+ line = ""
+ for (i = 1; i <= length(text); i++) {
+ c = substr(text, i, 1)
+ if (c == "\n") {
+ n++
+ MD[n] = line
+ line = ""
+ } else if (c != "\r")
+ line = line c
+ }
+ n++
+ MD[n] = line
+ NMD = n
+}
+
+function md_fence_open(line, s, ticks) {
+ s = line
+ sub(/^[ \t]*/, "", s)
+ if (s !~ /^(```|~~~)/)
+ return 0
+ ticks = substr(s, 1, 3)
+ FENCE_TICKS = ticks
+ s = substr(s, 4)
+ sub(/^[ \t]+/, "", s)
+ FENCE_INFO = s
+ return 1
+}
+
+function md_fence_close(line, s) {
+ s = line
+ sub(/^[ \t]*/, "", s)
+ sub(/[ \t]*$/, "", s)
+ return s == FENCE_TICKS
+}
+
+function md_hr(line, s) {
+ s = line
+ sub(/^[ \t]+/, "", s)
+ sub(/[ \t]+$/, "", s)
+ return s ~ /^(-{3,}|\*{3,}|_{3,})$/
+}
+
+function md_heading(line, s, i) {
+ s = line
+ sub(/^[ \t]+/, "", s)
+ if (s !~ /^#{1,6}[ \t]/)
+ return 0
+ i = 1
+ while (substr(s, i, 1) == "#" && i <= 6)
+ i++
+ HD_LEVEL = i - 1
+ HD_TEXT = substr(s, i)
+ sub(/^[ \t]+/, "", HD_TEXT)
+ sub(/[ \t]+#*[ \t]*$/, "", HD_TEXT)
+ return 1
+}
+
+function md_ul(line, s) {
+ s = line
+ if (match(s, /^[ \t]*[-*+][ \t]+/)) {
+ UL_INDENT = RLENGTH
+ UL_TEXT = substr(s, RLENGTH + 1)
+ return 1
+ }
+ return 0
+}
+
+function md_ol(line, s) {
+ s = line
+ if (match(s, /^[ \t]*[0-9]+\.[ \t]+/)) {
+ UL_INDENT = RLENGTH
+ UL_TEXT = substr(s, RLENGTH + 1)
+ return 1
+ }
+ return 0
+}
+
+function md_quote(line, s) {
+ s = line
+ if (match(s, /^[ \t]*>[ \t]?/)) {
+ UL_TEXT = substr(s, RLENGTH + 1)
+ return 1
+ }
+ return 0
+}
+
+function md_table_row(line, s) {
+ s = line
+ sub(/^[ \t]+/, "", s)
+ sub(/[ \t]+$/, "", s)
+ return s ~ /^\|.*\|$/
+}
+
+function md_inline(s, out, n, i, c, two, nxt, tmp, url, label, clpos, ticks, wlen) {
+ out = ""
+ n = length(s)
+ i = 1
+ while (i <= n) {
+ c = substr(s, i, 1)
+ two = substr(s, i, 2)
+ if (c == "`") {
+ ticks = 1
+ while (i + ticks <= n && substr(s, i + ticks, 1) == "`")
+ ticks++
+ clpos = index(substr(s, i + ticks), substr("``````", 1, ticks))
+ if (clpos > 0) {
+ tmp = substr(s, i + ticks, clpos - 1)
+ out = out "<code>" esc(tmp) "</code>"
+ i = i + ticks + clpos - 1 + ticks
+ continue
+ }
+ }
+ if (two == "![") {
+ nxt = match(substr(s, i), /^\[[^\]]*\]\([^)]+\)/)
+ if (match(substr(s, i), /^!\[[^\]]*\]\([^)]+\)/)) {
+ tmp = substr(s, i, RLENGTH)
+ label = tmp
+ sub(/^!\[/, "", label)
+ sub(/\]\([^)]+\)$/, "", label)
+ url = tmp
+ sub(/^!\[[^\]]*\]\(/, "", url)
+ sub(/\)$/, "", url)
+ if (url ~ /^https?:\/\//)
+ out = out "<img src=\"" esc(url) "\" alt=\"" esc(label) "\">"
+ else
+ out = out esc(tmp)
+ i += RLENGTH
+ continue
+ }
+ }
+ if (c == "[" && match(substr(s, i), /^\[[^\]]+\]\([^)]+\)/)) {
+ tmp = substr(s, i, RLENGTH)
+ label = tmp
+ sub(/^\[/, "", label)
+ sub(/\]\([^)]+\)$/, "", label)
+ url = tmp
+ sub(/^\[[^\]]+\]\(/, "", url)
+ sub(/\)$/, "", url)
+ if (url ~ /^https?:\/\// || url ~ /^\// || url ~ /^\.\.?\//)
+ out = out "<a href=\"" esc(url) "\">" md_inline(label) "</a>"
+ else
+ out = out esc(tmp)
+ i += RLENGTH
+ continue
+ }
+ if (two == "**" || two == "__") {
+ clpos = index(substr(s, i + 2), two)
+ if (clpos > 0) {
+ tmp = substr(s, i + 2, clpos - 1)
+ out = out "<strong>" md_inline(tmp) "</strong>"
+ i = i + 2 + clpos - 1 + 2
+ continue
+ }
+ }
+ if (two == "~~") {
+ clpos = index(substr(s, i + 2), "~~")
+ if (clpos > 0) {
+ tmp = substr(s, i + 2, clpos - 1)
+ out = out "<del>" md_inline(tmp) "</del>"
+ i = i + 2 + clpos - 1 + 2
+ continue
+ }
+ }
+ if ((c == "*" || c == "_") && i < n && substr(s, i + 1, 1) !~ /[ \t]/) {
+ clpos = index(substr(s, i + 1), c)
+ if (clpos > 0 && substr(s, i + clpos + 1, 1) != c) {
+ tmp = substr(s, i + 1, clpos - 1)
+ if (tmp !~ /\n/) {
+ out = out "<em>" md_inline(tmp) "</em>"
+ i = i + 1 + clpos
+ continue
+ }
+ }
+ }
+ if (substr(s, i, 8) == "https://" || substr(s, i, 7) == "http://") {
+ wlen = 0
+ while (i + wlen <= n && substr(s, i + wlen, 1) !~ /[ \t<>"']/)
+ wlen++
+ while (wlen > 0 && substr(s, i + wlen - 1, 1) ~ /[.,;:!?)]/)
+ wlen--
+ url = substr(s, i, wlen)
+ out = out "<a href=\"" esc(url) "\">" esc(url) "</a>"
+ i += wlen
+ continue
+ }
+ out = out esc(c)
+ i++
+ }
+ return out
+}
+
+function md_task(text, box, rest) {
+ if (text ~ /^\[[xX]\][ \t]/) {
+ rest = substr(text, 5)
+ sub(/^[ \t]+/, "", rest)
+ return "<span class=\"task done\">[x]</span> " md_inline(rest)
+ }
+ if (text ~ /^\[[ ]\][ \t]/) {
+ rest = substr(text, 5)
+ sub(/^[ \t]+/, "", rest)
+ return "<span class=\"task\">[ ]</span> " md_inline(rest)
+ }
+ return md_inline(text)
+}
+
+function md_render( i, line, buf, lang, html, cells, ncol, j, row, align, a) {
+ html = ""
+ i = 1
+ while (i <= NMD) {
+ line = MD[i]
+ if (md_fence_open(line)) {
+ buf = ""
+ i++
+ while (i <= NMD && !md_fence_close(MD[i])) {
+ if (buf != "")
+ buf = buf "\n"
+ buf = buf MD[i]
+ i++
+ }
+ if (i <= NMD)
+ i++
+ lang = fence_lang(FENCE_INFO)
+ html = html wrap_hl(hl_src(buf, lang), lang)
+ continue
+ }
+ if (md_hr(line)) {
+ html = html "<hr class=\"rule\">\n"
+ i++
+ continue
+ }
+ if (md_heading(line)) {
+ html = html "<h" (HD_LEVEL + 1) ">" md_inline(HD_TEXT) "</h" (HD_LEVEL + 1) ">\n"
+ i++
+ continue
+ }
+ if (md_quote(line)) {
+ html = html "<blockquote>"
+ while (i <= NMD && md_quote(MD[i])) {
+ html = html "<p>" md_inline(UL_TEXT) "</p>"
+ i++
+ }
+ html = html "</blockquote>\n"
+ continue
+ }
+ if (md_table_row(line) && i < NMD && MD[i + 1] ~ /\|[ \t]*:?-+:?[ \t]*\|/) {
+ html = html "<table class=\"pkgs md-table\"><thead><tr>"
+ row = line
+ sub(/^[ \t]*\|/, "", row)
+ sub(/\|[ \t]*$/, "", row)
+ ncol = split(row, cells, /\|/)
+ for (j = 1; j <= ncol; j++) {
+ sub(/^[ \t]+/, "", cells[j])
+ sub(/[ \t]+$/, "", cells[j])
+ html = html "<th>" md_inline(cells[j]) "</th>"
+ }
+ html = html "</tr></thead><tbody>"
+ i += 2
+ while (i <= NMD && md_table_row(MD[i])) {
+ row = MD[i]
+ sub(/^[ \t]*\|/, "", row)
+ sub(/\|[ \t]*$/, "", row)
+ split(row, cells, /\|/)
+ html = html "<tr>"
+ for (j = 1; j <= ncol; j++) {
+ sub(/^[ \t]+/, "", cells[j])
+ sub(/[ \t]+$/, "", cells[j])
+ html = html "<td>" md_inline(cells[j]) "</td>"
+ }
+ html = html "</tr>"
+ i++
+ }
+ html = html "</tbody></table>\n"
+ continue
+ }
+ if (md_ul(line)) {
+ html = html "<ul class=\"plain\">"
+ while (i <= NMD && md_ul(MD[i])) {
+ html = html "<li>" md_task(UL_TEXT) "</li>"
+ i++
+ }
+ html = html "</ul>\n"
+ continue
+ }
+ if (md_ol(line)) {
+ html = html "<ol>"
+ while (i <= NMD && md_ol(MD[i])) {
+ html = html "<li>" md_task(UL_TEXT) "</li>"
+ i++
+ }
+ html = html "</ol>\n"
+ continue
+ }
+ if (line ~ /^( |\t)/) {
+ buf = ""
+ while (i <= NMD && MD[i] ~ /^( |\t)/) {
+ row = MD[i]
+ if (substr(row, 1, 4) == " ")
+ row = substr(row, 5)
+ else if (substr(row, 1, 1) == "\t")
+ row = substr(row, 2)
+ if (buf != "")
+ buf = buf "\n"
+ buf = buf row
+ i++
+ }
+ html = html wrap_hl(esc(buf), "")
+ continue
+ }
+ if (line ~ /^[ \t]*$/) {
+ i++
+ continue
+ }
+ buf = line
+ i++
+ if (i <= NMD && MD[i] ~ /^[ \t]*=+[ \t]*$/) {
+ html = html "<h2>" md_inline(buf) "</h2>\n"
+ i++
+ continue
+ }
+ if (i <= NMD && MD[i] ~ /^[ \t]*-+[ \t]*$/) {
+ html = html "<h3>" md_inline(buf) "</h3>\n"
+ i++
+ continue
+ }
+ while (i <= NMD && MD[i] !~ /^[ \t]*$/ && !md_fence_open(MD[i]) && !md_heading(MD[i]) && !md_hr(MD[i]) && !md_ul(MD[i]) && !md_ol(MD[i]) && !md_quote(MD[i])) {
+ buf = buf " " MD[i]
+ i++
+ }
+ html = html "<p>" md_inline(buf) "</p>\n"
+ }
+ return html
+}
+
+BEGIN {
+ load_fams()
+ load_kw()
+ load_langmap()
+}
+
+{
+ if (body != "")
+ body = body "\n"
+ body = body $0
+}
+
+END {
+ if (mode == "famjson") {
+ printf "{"
+ n = 0
+ for (name in FAM) {
+ seen[name] = 1
+ if (n++)
+ printf ","
+ printf "\"%s\":\"%s\"", jesc(name), jesc(FAM[name])
+ }
+ for (name in LGROUP) {
+ if (name in seen)
+ continue
+ if (n++)
+ printf ","
+ printf "\"%s\":\"%s\"", jesc(name), jesc(fam_of(name))
+ seen[name] = 1
+ }
+ print "}"
+ exit
+ }
+ if (mode == "highlight") {
+ print wrap_hl(hl_src(body, lang), lang)
+ exit
+ }
+ if (mode == "markdown") {
+ md_split(body)
+ print "<div class=\"md\">"
+ printf "%s", md_render()
+ print "</div>"
+ exit
+ }
+}