Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 155 additions & 67 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ jobs:
description: "${DESC}"
permalink: pretty
baseurl: "${BASE_URL}"
github_repo: "https://github.com/${REPO_FULL}"

defaults:
- scope:
Expand All @@ -97,45 +98,68 @@ jobs:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root{
--bg-page:#f6f8fa;--bg-content:#fff;--bg-header:#fff;--bg-code:#f6f8fa;
--text-primary:#24292f;--text-secondary:#57606a;--text-muted:#8b949e;
--border:#d0d7de;--link:#0969da;--accent:#fd8c73;
--badge-bg:#dafbe1;--badge-text:#1a7f37;
}
@media(prefers-color-scheme:dark){
:root{
--bg-page:#0d1117;--bg-content:#161b22;--bg-header:#161b22;--bg-code:#1c2129;
--text-primary:#e6edf3;--text-secondary:#8b949e;--text-muted:#6e7681;
--border:#30363d;--link:#58a6ff;--accent:#f78166;
--badge-bg:#1b3826;--badge-text:#3fb950;
}
}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'IBM Plex Sans',-apple-system,sans-serif;color:#24292f;background:#f6f8fa;line-height:1.7}
a{color:#0969da;text-decoration:none}
body{font-family:'IBM Plex Sans',-apple-system,sans-serif;color:var(--text-primary);background:var(--bg-page);line-height:1.7}
a{color:var(--link);text-decoration:none}
a:hover{text-decoration:underline}
.site-header{background:#fff;border-bottom:1px solid #d0d7de;padding:16px 24px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px}
.site-title{font-family:'IBM Plex Mono',monospace;font-size:16px;font-weight:500;color:#24292f;text-decoration:none}
.site-header{background:var(--bg-header);border-bottom:1px solid var(--border);padding:16px 24px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px}
.site-title{font-size:16px;font-weight:500;color:var(--text-primary);text-decoration:none}
.site-nav{display:flex;gap:20px;flex-wrap:wrap}
.site-nav a{font-size:14px;color:#57606a;text-decoration:none;padding:4px 0}
.site-nav a:hover{color:#24292f}
.site-nav a.active{color:#24292f;font-weight:500;border-bottom:2px solid #fd8c73}
.site-nav a{font-size:14px;color:var(--text-secondary);text-decoration:none;padding:4px 0}
.site-nav a:hover{color:var(--text-primary)}
.site-nav a.active{color:var(--text-primary);font-weight:500;border-bottom:2px solid var(--accent)}
.container{max-width:820px;margin:32px auto;padding:0 24px}
.content{background:#fff;border:1px solid #d0d7de;border-radius:6px;padding:32px}
.content h1{font-size:24px;font-weight:500;margin:0 0 16px;border-bottom:1px solid #d0d7de;padding-bottom:8px}
.content h2{font-size:20px;font-weight:500;margin:24px 0 12px;border-bottom:1px solid #d0d7de;padding-bottom:6px}
.content{background:var(--bg-content);border:1px solid var(--border);border-radius:6px;padding:32px}
.content h1{font-size:24px;font-weight:500;margin:0 0 16px;border-bottom:1px solid var(--border);padding-bottom:8px}
.content h2{font-size:20px;font-weight:500;margin:24px 0 12px;border-bottom:1px solid var(--border);padding-bottom:6px}
.content h3{font-size:16px;font-weight:500;margin:20px 0 8px}
.content h4{font-size:14px;font-weight:500;margin:16px 0 6px}
.content p{font-size:14px;color:#57606a;margin:0 0 16px}
.content ul,.content ol{font-size:14px;color:#57606a;margin:0 0 16px;padding-left:24px}
.content p{font-size:14px;color:var(--text-secondary);margin:0 0 16px}
.content ul,.content ol{font-size:14px;color:var(--text-secondary);margin:0 0 16px;padding-left:24px}
.content li{margin-bottom:4px}
.content code{font-family:'IBM Plex Mono',monospace;font-size:13px;background:#f6f8fa;padding:2px 6px;border-radius:4px}
.content pre{background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:16px;overflow-x:auto;margin:8px 0 16px;line-height:1.5}
.content code{font-family:'IBM Plex Mono',monospace;font-size:13px;background:var(--bg-code);padding:2px 6px;border-radius:4px}
.content pre{background:var(--bg-code);border:1px solid var(--border);border-radius:6px;padding:16px;overflow-x:auto;margin:8px 0 16px;line-height:1.5}
.content pre code{background:transparent;padding:0}
.content table{width:100%;border-collapse:collapse;margin:8px 0 16px;font-size:14px}
.content th,.content td{border:1px solid #d0d7de;padding:8px 12px;text-align:left}
.content th{background:#f6f8fa;font-weight:500}
.content th,.content td{border:1px solid var(--border);padding:8px 12px;text-align:left}
.content th{background:var(--bg-code);font-weight:500}
.content img{max-width:100%}
#releases-container .release{border-bottom:1px solid #d0d7de;padding:20px 0}
#releases-container .release:last-child{border-bottom:none}
.release-tag{font-family:'IBM Plex Mono',monospace;font-size:16px;font-weight:500;color:#0969da}
.release-date{font-size:13px;color:#8b949e;margin-left:8px}
.release-latest{font-size:11px;padding:2px 8px;border-radius:16px;background:#dafbe1;color:#1a7f37;font-weight:500;margin-left:8px}
.release-body{font-size:14px;color:#57606a;margin-top:8px;line-height:1.7}
.release-body h2,.release-body h3{font-size:14px;font-weight:500;color:#24292f;margin:12px 0 4px;border:none;padding:0}
.content strong{color:var(--text-primary)}
.release{border:1px solid var(--border);border-radius:6px;margin-bottom:8px}
.release summary{padding:12px 16px;cursor:pointer;display:flex;align-items:center;gap:8px;font-size:14px;list-style:none}
.release summary::-webkit-details-marker{display:none}
.release summary::before{content:'';display:inline-block;width:0;height:0;border-left:5px solid var(--text-muted);border-top:4px solid transparent;border-bottom:4px solid transparent;transition:transform 0.15s}
.release[open] summary::before{transform:rotate(90deg)}
.release-tag{font-family:'IBM Plex Mono',monospace;font-weight:500;color:var(--link)}
.release-date{font-size:13px;color:var(--text-muted)}
.release-latest{font-size:11px;padding:2px 8px;border-radius:16px;background:var(--badge-bg);color:var(--badge-text);font-weight:500}
.release-body{padding:0 16px 16px;font-size:14px;color:var(--text-secondary);line-height:1.7}
.release-body h2,.release-body h3{font-size:14px;font-weight:500;color:var(--text-primary);margin:12px 0 4px;border:none;padding:0}
.release-body ul{padding-left:20px}
.release-body li{margin-bottom:2px;font-size:13px}
.release-assets{margin-top:12px}
.release-assets summary{font-size:13px;font-weight:500;cursor:pointer;color:#24292f}
.release-asset{font-family:'IBM Plex Mono',monospace;font-size:12px;color:#57606a;padding:3px 0}
.loading{text-align:center;padding:32px;color:#8b949e;font-size:14px}
.release-body pre{background:var(--bg-code);border:1px solid var(--border);border-radius:6px;padding:12px;overflow-x:auto;font-size:13px;line-height:1.5}
.release-body code{font-family:'IBM Plex Mono',monospace;font-size:13px;background:var(--bg-code);padding:2px 4px;border-radius:3px}
.release-body pre code{background:transparent;padding:0}
.release-assets{margin-top:12px;border-top:1px solid var(--border);padding-top:12px}
.release-assets summary{padding:0;font-size:13px;font-weight:500;cursor:pointer;color:var(--text-primary);list-style:revert}
.release-assets summary::-webkit-details-marker{display:revert}
.release-assets summary::before{display:none}
.release-asset{font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--text-secondary);padding:3px 0}
.loading{text-align:center;padding:32px;color:var(--text-muted);font-size:14px}
@media(max-width:640px){.site-header{padding:12px 16px}.container{padding:0 12px;margin:16px auto}.content{padding:20px}}
</style>
</head>
Expand All @@ -150,6 +174,7 @@ jobs:
{% for p in site.pages %}{% if p.path == 'LICENSE.md' %}<a href="{{ '/license/' | relative_url }}" {% if page.url contains '/license' %}class="active"{% endif %}>License</a>{% break %}{% endif %}{% endfor %}
{% for p in site.pages %}{% if p.path == 'CONTRIBUTING.md' %}<a href="{{ '/contributing/' | relative_url }}" {% if page.url contains '/contributing' %}class="active"{% endif %}>Contributing</a>{% break %}{% endif %}{% endfor %}
{% for p in site.pages %}{% if p.path == 'CODE_OF_CONDUCT.md' %}<a href="{{ '/code-of-conduct/' | relative_url }}" {% if page.url contains '/code-of-conduct' %}class="active"{% endif %}>Code of conduct</a>{% break %}{% endif %}{% endfor %}
<a href="{{ site.github_repo }}" target="_blank" rel="noopener">View source</a>
</nav>
</header>
<main class="container">
Expand All @@ -171,6 +196,18 @@ jobs:
echo "Processed README.md → homepage"
fi

# ── Fix relative links for Jekyll ─────────────────────────────
# Jekyll with permalink:pretty changes paths, so relative links
# like [X](CONTRIBUTING.md) or [X](LICENSE) would 404.
# Point them at their Jekyll permalinks instead.
# Only targets known community files in root-level markdown.
for f in *.md; do
[ -f "$f" ] || continue
[ -f "CONTRIBUTING.md" ] && sed -i 's|\](CONTRIBUTING\.md)|\](contributing/)|g' "$f"
[ -f "CODE_OF_CONDUCT.md" ] && sed -i 's|\](CODE_OF_CONDUCT\.md)|\](code-of-conduct/)|g' "$f"
([ -f "LICENSE" ] || [ -f "LICENSE.txt" ] || [ -f "LICENSE.md" ]) && sed -i 's|\](LICENSE)|\](license/)|g' "$f"
done

# ── Docs: add front matter + create index ─────────────────────
if [ -d "docs" ]; then
for f in docs/*.md; do
Expand Down Expand Up @@ -213,65 +250,116 @@ jobs:
fi

# ── Releases page ─────────────────────────────────────────────
# Write the JS to a separate file to avoid heredoc escaping issues
mkdir -p _includes
cat > _includes/releases.js << 'JSEOF'
(async function() {
var c = document.getElementById('releases-container');
var repo = c.getAttribute('data-repo');
try {
var res = await fetch('https://api.github.com/repos/' + repo + '/releases');
if (!res.ok) throw new Error(res.status);
var data = await res.json();
if (!data.length) { c.innerHTML = '<p>No releases found.</p>'; return; }

function md(s) {
if (!s) return '';
var lines = s.split('\n');
var html = '';
var inList = false;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
var h3 = line.match(/^### (.+)$/);
var h2 = line.match(/^## (.+)$/);
var li = line.match(/^\* (.+)$/);
var cb = line.match(/^```/);
if (cb) {
if (inList) { html += '</ul>'; inList = false; }
var code = '';
i++;
while (i < lines.length && !lines[i].match(/^```/)) { code += lines[i] + '\n'; i++; }
html += '<pre><code>' + code + '</code></pre>';
} else if (h2) {
if (inList) { html += '</ul>'; inList = false; }
html += '<h2>' + h2[1] + '</h2>';
} else if (h3) {
if (inList) { html += '</ul>'; inList = false; }
html += '<h3>' + h3[1] + '</h3>';
} else if (li) {
if (!inList) { html += '<ul>'; inList = true; }
var text = li[1].replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
html += '<li>' + text + '</li>';
} else {
if (inList) { html += '</ul>'; inList = false; }
if (line.trim() === '---') { html += '<hr>'; }
else if (line.trim()) {
var text = line.replace(/`([^`]+)`/g, '<code>$1</code>').replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>').replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
html += '<p>' + text + '</p>';
}
}
}
if (inList) html += '</ul>';
return html;
}

c.innerHTML = data.map(function(r, i) {
var d = new Date(r.published_at).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
var latest = i === 0 ? '<span class="release-latest">latest</span>' : '';
var openAttr = i === 0 ? ' open' : '';
var assets = '';
if (r.assets.length) {
assets = '<details class="release-assets"><summary>Assets (' + r.assets.length + ')</summary>';
for (var j = 0; j < r.assets.length; j++) {
var a = r.assets[j];
assets += '<div class="release-asset">' + a.name + ' <span style="color:var(--text-muted)">' + (a.size / 1024).toFixed(1) + ' KB</span></div>';
}
assets += '</details>';
}
return '<details class="release"' + openAttr + '><summary><span class="release-tag">' + r.tag_name + '</span>' + latest + '<span class="release-date">' + d + '</span></summary><div class="release-body">' + md(r.body) + assets + '</div></details>';
}).join('');
} catch(e) {
c.innerHTML = '<p>Unable to load releases. Visit <a href="https://github.com/' + repo + '/releases">GitHub</a> directly.</p>';
}
})();
JSEOF

cat > releases.md << RELEOF
---
layout: default
title: Releases
permalink: /releases/
---
# Releases
<div id="releases-container"><div class="loading">Loading releases...</div></div>
<div id="releases-container" data-repo="${REPO_FULL}"><div class="loading">Loading releases...</div></div>
<script>
(async function(){
const c=document.getElementById('releases-container');
try{
const r=await fetch('https://api.github.com/repos/${REPO_FULL}/releases');
if(!r.ok)throw new Error(r.status);
const data=await r.json();
if(!data.length){c.innerHTML='<p>No releases found.</p>';return}
c.innerHTML=data.map((r,i)=>{
const d=new Date(r.published_at).toLocaleDateString('en-GB',{day:'numeric',month:'short',year:'numeric'});
const l=i===0?'<span class="release-latest">latest</span>':'';
const a=r.assets.length?'<details class="release-assets"><summary>Assets ('+r.assets.length+')</summary>'+r.assets.map(a=>'<div class="release-asset">'+a.name+' <span style="color:#8b949e">'+(a.size/1024).toFixed(1)+' KB</span></div>').join('')+'</details>':'';
let b=(r.body||'').replace(/^### (.+)$/gm,'<h3>$1</h3>').replace(/^## (.+)$/gm,'<h2>$1</h2>').replace(/^\* (.+)$/gm,'<li>$1</li>').replace(/(<li>.*<\/li>\n?)+/gs,'<ul>$&</ul>').replace(/\x60\x60\x60(\w*)\n([\s\S]*?)\x60\x60\x60/g,'<pre><code>$2</code></pre>').replace(/\x60([^\x60]+)\x60/g,'<code>$1</code>').replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2">$1</a>').replace(/\n\n/g,'<br><br>').replace(/\n/g,'<br>');
return '<div class="release"><div><span class="release-tag">'+r.tag_name+'</span>'+l+'<span class="release-date">'+d+'</span></div><div class="release-body">'+b+'</div>'+a+'</div>';
}).join('');
}catch(e){
c.innerHTML='<p>Unable to load releases. Visit <a href="https://github.com/${REPO_FULL}/releases">GitHub</a> directly.</p>';
}
})();
{% include releases.js %}
</script>
RELEOF
echo "Created releases.md"

# ── License page (from LICENSE/LICENSE.txt) ────────────────────
# ── License page (full content from LICENSE/LICENSE.txt) ──────
LICENSE_SRC=""
for lf in LICENSE LICENSE.txt; do
[ -f "$lf" ] && LICENSE_SRC="$lf" && break
done

if [ -n "$LICENSE_SRC" ] || [ -f "LICENSE.md" ]; then
SRC="${LICENSE_SRC:-LICENSE.md}"
LICENSE_TYPE="License"
grep -qi "apache" "$SRC" 2>/dev/null && LICENSE_TYPE="Apache License 2.0"
grep -qi "mit license" "$SRC" 2>/dev/null && LICENSE_TYPE="MIT License"
grep -qi "gnu general public" "$SRC" 2>/dev/null && LICENSE_TYPE="GPL"
grep -qi "bsd" "$SRC" 2>/dev/null && LICENSE_TYPE="BSD License"

if [ -n "$LICENSE_SRC" ]; then
# Create LICENSE.md with front matter + full license text
if ! ([ -f "LICENSE.md" ] && head -1 LICENSE.md | grep -q '^\-\-\-'); then
cat > LICENSE.md << LICEOF
---
layout: default
title: License
permalink: /license/
---
# License
This project is licensed under the **${LICENSE_TYPE}**.

See the [LICENSE](https://github.com/${REPO_FULL}/blob/main/${SRC}) file for the full license text.
LICEOF
echo "Created LICENSE.md (${LICENSE_TYPE})"
TEMP=$(mktemp)
printf -- '---\nlayout: default\ntitle: License\npermalink: /license/\n---\n\n# License\n\n```\n' > "$TEMP"
cat "$LICENSE_SRC" >> "$TEMP"
printf '\n```\n' >> "$TEMP"
mv "$TEMP" LICENSE.md
echo "Created LICENSE.md from ${LICENSE_SRC}"
fi
elif [ -f "LICENSE.md" ] && ! head -1 LICENSE.md | grep -q '^\-\-\-'; then
# LICENSE.md exists but has no front matter
TEMP=$(mktemp)
printf -- '---\nlayout: default\ntitle: License\npermalink: /license/\n---\n\n' > "$TEMP"
cat LICENSE.md >> "$TEMP"
mv "$TEMP" LICENSE.md
echo "Processed LICENSE.md"
fi

# ── CONTRIBUTING.md ───────────────────────────────────────────
Expand Down
Loading