rsrc/index.html
2026-09-04 11:17:38 +00:00

859 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="dark">
<meta name="theme-color" content="#050506">
<title>rsrc</title>
<link rel="preconnect" href="https://raw.githubusercontent.com">
<link rel="preconnect" href="https://api.fmhy.net">
<link rel="dns-prefetch" href="//raw.githubusercontent.com">
<link rel="dns-prefetch" href="//api.fmhy.net">
<style>
:root {
--bg: #050506;
--panel: #09090b;
--panel-hover: #101013;
--fg: #e4e4e7;
--title: #fff;
--head: #d4d4d8;
--link: #f4f4f5;
--muted: #a1a1aa;
--soft: #71717a;
--line: #202024;
--line-strong: #3a3a42;
--mark: #ffffff24;
--max: 112rem;
--sidebar: 17rem;
--content-max: 92rem
}
* {
box-sizing: border-box
}
html {
scroll-behavior: smooth
}
body {
margin: 0;
min-width: 320px;
background: radial-gradient(75rem 38rem at 55% -22rem, #1a1a22 0, #08080a 48%, var(--bg) 100%), var(--bg);
color: var(--fg);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: .94rem;
line-height: 1.62;
letter-spacing: -.01em;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility
}
a {
color: var(--link);
text-decoration: none;
text-decoration-thickness: 1px;
text-underline-offset: .18em;
overflow-wrap: anywhere
}
a:hover,
a:focus-visible {
text-decoration: underline
}
button,
input {
font: inherit
}
button:focus-visible,
input:focus-visible,
a:focus-visible {
outline: 2px solid #fff;
outline-offset: 3px
}
.wrap {
width: min(96vw, var(--max));
margin: 0 auto;
padding: clamp(1.5rem, 3vw, 3rem) 0 4rem;
display: grid;
grid-template-columns: var(--sidebar) minmax(0, 1fr);
gap: clamp(2rem, 4vw, 4.5rem);
align-items: start
}
.sidebar {
min-width: 0;
align-self: start
}
.sidebar-head {
position: sticky;
top: 0;
z-index: 10;
padding: 0 0 1rem;
background: linear-gradient(to bottom, var(--bg) 82%, #05050600)
}
.sidebar-title {
margin: 0 0 .6rem;
color: var(--muted);
font-size: .72rem;
font-weight: 700;
letter-spacing: .1em;
text-transform: uppercase
}
.search-wrap {
position: relative
}
#search {
width: 100%;
height: 2.45rem;
padding: .5rem 2.3rem .5rem .75rem;
border: 1px solid var(--line-strong);
border-radius: .6rem;
outline: 0;
background: #0c0c0f;
color: var(--title);
font-size: .84rem;
transition: border-color .18s ease, background .18s ease
}
#search::placeholder {
color: var(--soft)
}
#search:focus {
border-color: #85858e;
background: #111114
}
#search-clear {
position: absolute;
top: 50%;
right: .4rem;
display: none;
width: 1.7rem;
height: 1.7rem;
padding: 0;
border: 0;
border-radius: .35rem;
transform: translateY(-50%);
background: transparent;
color: var(--muted);
line-height: 1;
cursor: pointer
}
#search-clear:hover {
background: var(--panel-hover);
color: var(--title)
}
.search-help {
margin: .55rem 0 0;
color: var(--soft);
font-size: .72rem;
line-height: 1.4
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: .18rem;
padding-bottom: 1rem
}
.sidebar-link {
display: flex;
align-items: center;
gap: .5rem;
overflow: hidden;
min-height: 2rem;
padding: .4rem .52rem;
border: 1px solid transparent;
border-radius: .45rem;
color: var(--muted);
font-size: .8rem;
line-height: 1.25;
text-decoration: none;
cursor: pointer
}
.sidebar-link:hover,
.sidebar-link.active {
background: var(--panel-hover);
color: var(--head);
text-decoration: none
}
.sidebar-link.active {
border-color: var(--line-strong);
box-shadow: inset 2px 0 0 #fff
}
.sidebar-link input {
flex: 0 0 auto;
width: .85rem;
height: .85rem;
margin: 0;
accent-color: #fff;
cursor: pointer
}
.sidebar-link span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
.content {
width: min(100%, var(--content-max));
min-width: 0
}
.meta {
margin: 0 0 .3rem;
color: var(--title);
font-size: 1.15rem;
font-weight: 650;
letter-spacing: -.025em
}
.status,
.empty {
margin: 0 0 1.2rem;
color: var(--soft);
font-size: .78rem
}
.empty {
padding: 1.25rem 0;
border-top: 1px solid var(--line)
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 2.2rem 3rem;
align-items: start
}
.cat {
min-width: 0;
padding: 1rem 0 0;
border-top: 1px solid var(--line);
scroll-margin-top: 1.5rem;
content-visibility: auto;
contain-intrinsic-size: auto 32rem
}
.cat-title {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: .35rem;
margin: 0 0 .7rem;
color: var(--head);
font-size: .92rem;
line-height: 1.35;
font-weight: 700;
letter-spacing: -.01em
}
.cat-title>a:first-child {
color: var(--head)
}
.cat-title>a:first-child:hover {
color: var(--title)
}
.source-tag {
display: inline-flex;
align-items: center;
min-height: 1.35rem;
margin-left: .15rem;
padding: .08rem .38rem;
border: 1px solid var(--line-strong);
border-radius: .3rem;
color: var(--soft);
font-size: .62rem;
font-weight: 650;
letter-spacing: .055em;
line-height: 1;
text-decoration: none;
text-transform: uppercase;
vertical-align: middle
}
.source-tag:hover,
.source-tag:focus-visible {
border-color: #74747d;
color: var(--head);
text-decoration: none
}
ul {
margin: 0;
padding-left: 1.15rem
}
li {
margin: .45rem 0;
padding-left: .08rem
}
li::marker {
color: #707078
}
.children {
margin-top: .35rem;
padding: .15rem 0 .1rem .5rem;
border-left: 1px solid #25252a
}
.children li {
margin: .34rem 0
}
.inline-links {
display: inline
}
.inline-links .desc {
margin: 0 .22rem
}
.result-link,
.item-name {
color: var(--link);
font-weight: 620;
letter-spacing: -.012em
}
.result-link:hover {
color: var(--title)
}
.github-link::after {
content: " ↗";
color: var(--soft);
font-size: .75em;
font-weight: 400
}
.desc {
color: var(--muted)
}
.result-desc {
display: inline;
margin: 0 0 0 .28rem;
color: var(--muted);
font-size: .92em;
line-height: inherit
}
.result-desc::before {
content: "— ";
color: var(--soft)
}
mark {
padding: 0 .1em;
border-radius: .15rem;
background: var(--mark);
color: #fff
}
.search-selected {
border-radius: .2rem;
background: #ffffff24;
box-shadow: 0 0 0 .15rem #ffffff24
}
footer {
margin-top: 3rem;
padding-top: 1rem;
border-top: 1px solid var(--line);
color: var(--soft);
font-size: .78rem
}
footer a {
margin-right: .32rem;
color: var(--muted)
}
footer a:hover {
color: var(--head)
}
[hidden] {
display: none !important
}
@media(min-width:122rem) {
.grid {
grid-template-columns: repeat(3, minmax(20rem, 1fr));
gap: 2.3rem 2.75rem
}
}
@media(max-width:72rem) {
:root {
--sidebar: 14.5rem
}
.wrap {
width: min(95vw, var(--max));
gap: 2rem
}
.grid {
gap: 1.6rem 1.8rem
}
}
@media(max-width:56rem) {
.wrap {
grid-template-columns: 13rem minmax(0, 1fr);
gap: 1.4rem
}
.grid {
grid-template-columns: 1fr;
gap: 1.4rem
}
}
@media(max-width:40rem) {
body {
font-size: .88rem
}
.wrap {
display: block;
width: 92%;
padding-top: 1.1rem
}
.sidebar {
margin-bottom: 1.35rem
}
.sidebar-head {
position: relative;
top: auto;
padding-bottom: .65rem
}
.sidebar-nav {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
gap: .32rem;
max-height: 14rem;
overflow-y: auto
}
.sidebar-link {
border-color: var(--line)
}
.cat {
padding-top: .85rem
}
}
@media(prefers-reduced-motion:reduce) {
html {
scroll-behavior: auto
}
#search {
transition: none
}
}
</style>
</head>
<body>
<main id="app" class="wrap"></main>
<script>
(() => {
"use strict";
const V = "v24",
TTL = 216e5,
STTL = 31536e6,
TO = 15e3,
S = [{
id: "awesome",
label: "Awesome Lists",
url: "https://raw.githubusercontent.com/sindresorhus/awesome/refs/heads/main/readme.md",
type: "markdown"
}, {
id: "billing",
label: "Awesome Billing",
url: "https://raw.githubusercontent.com/kdeldycke/awesome-billing/refs/heads/main/readme.md",
type: "markdown"
}, {
id: "startup-posting",
label: "Places to Post Your Startup",
url: "https://raw.githubusercontent.com/mmccaff/PlacesToPostYourStartup/refs/heads/master/README.md",
type: "markdown"
}, {
id: "developer-first",
label: "Awesome Developer-First",
url: "https://raw.githubusercontent.com/agamm/awesome-developer-first/refs/heads/main/README.md",
type: "markdown"
}, {
id: "selfhosted",
label: "Awesome Self-Hosted",
url: "https://raw.githubusercontent.com/awesome-selfhosted/awesome-selfhosted/refs/heads/master/README.md",
type: "markdown"
}, {
id: "productivity",
label: "Awesome Productivity Tools",
url: "https://raw.githubusercontent.com/ProductivityDirectory/awesome-productivity-tools/main/README.md",
type: "markdown"
}, {
id: "privacy",
label: "Awesome Privacy",
url: "https://raw.githubusercontent.com/pluja/awesome-privacy/main/README.md",
type: "markdown"
}, {
id: "fmhy",
label: "FMHY",
url: "https://api.fmhy.net/single-page",
type: "fmhy"
}],
SK = `awsm-rsrc:selected:${V}`,
MK = id => `awsm-rsrc:markdown:${id}:${V}`,
app = document.getElementById("app"),
mem = new Map,
req = new Map;
let results = [],
selected = -1,
frame = 0;
const esc = v => String(v ?? "").replace(/[&<>"']/g, c => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;"
} [c])),
plain = v => String(v ?? "").replace(/!\[[^\]]*]\([^)]*\)/g, "").replace(/\[([^\]]+)]\([^)]+\)/g, "$1").replace(/<[^>]*>/g, "").replace(/[`*_~\]]/g, "").replace(/&amp;/g, "&").trim(),
norm = v => String(v ?? "").toLocaleLowerCase(),
url = v => {
try {
let x = String(v ?? "").trim().replace(/[),.;]+$/, "");
if (!x) return "";
if (x.startsWith("#")) return x;
let u = new URL(x, location.href);
return /^https?:$/.test(u.protocol) ? u.href : ""
} catch {
return ""
}
},
host = v => {
try {
return new URL(v).hostname.replace(/^www\./, "")
} catch {
return v
}
},
gh = v => {
try {
return /(^|\.)github\.com$/i.test(new URL(v).hostname)
} catch {
return !1
}
},
cache = (k, f = "", age = 0) => {
let n = Date.now(),
m = mem.get(k);
if (m && (!age || n - m.time < age)) return m.value;
try {
let x = localStorage.getItem(k);
if (!x) return f;
let p = JSON.parse(x);
if (!p || typeof p != "object" || (p.expires && p.expires <= n)) return localStorage.removeItem(k), mem.delete(k), f;
return mem.set(k, {
value: p.value,
time: p.time || n
}), p.value
} catch {
return f
}
},
set = (k, v, t) => {
let time = Date.now();
mem.set(k, {
value: v,
time
});
try {
localStorage.setItem(k, JSON.stringify({
value: v,
time,
expires: time + t
}))
} catch {}
return v
},
extractFMHY = t => {
try {
let p = JSON.parse(t),
f = v => {
if (typeof v == "string") return v;
if (Array.isArray(v)) return v.map(f).filter(Boolean).join("\n");
if (v && typeof v == "object")
for (let k of ["markdown", "content", "data", "body", "text", "readme", "singlePage", "single_page"]) {
let r = f(v[k]);
if (r) return r
}
return ""
};
return f(p) || t
} catch {
return t
}
},
fetcher = (k, u, transform = v => v) => {
if (req.has(k)) return req.get(k);
let p = (async () => {
let c = cache(k, "", TTL);
if (c) return c;
if (navigator.onLine === !1) return "";
let a = new AbortController,
t = setTimeout(() => a.abort(), TO);
try {
let r = await fetch(u, {
cache: "force-cache",
signal: a.signal
});
if (!r.ok) throw 0;
return set(k, transform(await r.text()), TTL)
} catch {
return ""
} finally {
clearTimeout(t)
}
})();
return req.set(k, p), p.finally(() => req.delete(k)), p
},
ids = () => {
let s = new Set;
return v => {
let b = plain(v).toLocaleLowerCase().replace(/\s+/g, "-").replace(/[^\p{L}\p{N}-]/gu, "") || "section",
i = b,
n = 2;
for (; s.has(i);) i = `${b}-${n++}`;
return s.add(i), i
}
},
item = v => {
let s = String(v ?? "").replace(/^\[[ xX]\]\s*/i, "").trim(),
ml = [...s.matchAll(/(? < !!)\[([ ^ \]] + )]\(([ ^ )\ s] + ( ? : \s + "[^"] * ")?)\)/g)],rl=[...s.matchAll(/https?:\/\/[^\s<>)"] + /g)],links=[],seen=new Set,add=(t,h)=>{let x=url(String(h??"").trim().replace(/\
s + "[^"] * "$/,"
"));if(!x||seen.has(x))return;seen.add(x),links.push({title:plain(t)||host(x),href:x,github:gh(x)})};ml.forEach(m=>add(m[1],m[2]));rl.forEach(m=>add(host(m[0]),m[0]));let d=s;ml.forEach(m=>d=d.replace(m[0],"
"));rl.forEach(m=>d=d.replace(m[0],"
"));d=plain(d.replace(/^\s*[-–—::]\s*/,"
").replace(/\s{2,}/g,"
"));let f=links[0]||{title:plain(s),href:"
",github:!1};return{title:f.title||host(f.href),href:f.href,desc:d,links,children:[]}},dedupe=a=>{let s=new Set;return(a||[]).map(x=>({...x,children:dedupe(x.children||[])})).filter(x=>{let k=[x.title,x.href,...(x.links||[]).map(l=>l.href)].join(" | ");return!!k&&!s.has(k)&&(s.add(k),!0)}).sort((a,b)=>String(a.title||a.href).localeCompare(String(b.title||b.href),void 0,{sensitivity:"
base "}))},parse=md=>{let l=String(md??"
").replace(/<div\b[^>]*>[\s\S]*?<\/div>\s*/gi,"
").replace(/<details\b[^>]*>[\s\S]*?<\/details>\s*/gi,"
").replace(/\r/g,"
").split("\
n "),id=ids(),title="
rsrc ",sec=[],cur,sub,stack=[];for(let x of l){let m=x.trim().match(/^#\s+(.+?)(?:\s+#*)?$/);if(m){title=plain(m[1])||title;break}}let add=t=>(cur={title:t,id:id(t),items:[]},sec.push(cur),sub=null,stack=[],cur);for(let x of l){x=x.replace(/\t/g,"
");let t=x.trim();if(!t||/^ < !-/.test(t)||/ ^ \[!\[/.test(t)||/ ^ [- * _] {
3,
}
$ / .test(t) || /^<[^>]+>/.test(t)) continue;
let h = t.match(/^(#{2,6})\s+(.+?)(?:\s+#*)?$/);
if (h) {
let d = h[1].length,
n = plain(h[2]);
if (!n) continue;
if (stack = [], d === 2) add(n);
else {
cur || add("Contents");
sub = {
title: n,
id: id(n),
href: "",
desc: "",
links: [],
children: []
};
cur.items.push(sub)
}
continue
}
let q = x.match(/^(\s*)(?:[-*+]|\d+\.)\s+(.*)$/);
if (!q) continue;cur || add("Contents");
let ind = q[1].length,
it = item(q[2]);
if (!it.title && !it.href) continue;
for (; stack.length && ind <= stack.at(-1).ind;) stack.pop();stack.length ? stack.at(-1).item.children.push(it) : sub ? sub.children.push(it) : cur.items.push(it);stack.push({
ind,
item: it
})
}
return {
title,
sections: sec.map(x => ({
...x,
items: dedupe(x.items)
})).filter(x => x.items.length)
}
}, merge = docs => {
let id = ids(),
m = new Map;
docs.forEach(d => d.sections.forEach(s => {
let k = norm(s.title);
m.has(k) || m.set(k, {
title: s.title,
id: id(s.title),
items: [],
sources: []
});
let t = m.get(k);
t.items.push(...s.items);
t.sources.some(x => x.id === d.source.id) || t.sources.push(d.source)
}));
return {
title: "rsrc",
sections: [...m.values()].map(x => ({
...x,
items: dedupe(x.items)
})).filter(x => x.items.length).sort((a, b) => a.title.localeCompare(b.title)),
sources: docs.map(x => x.source)
}
}, count = a => (a || []).reduce((n, x) => n + 1 + count(x.children), 0), links = x => {
let a = x.links?.length ? x.links : x.href ? [{
title: x.title,
href: x.href,
github: gh(x.href)
}] : [];
return a.length ? `<span class="inline-links">${a.map((l,i)=>`${i?'<span class="desc">·</span>':""}<a class="result-link${l.github?" github-link":""}" href="${esc(l.href)}" target="_blank" rel="noopener noreferrer">${esc(l.title||l.href)}</a>`).join("")}</span>` : `<span class="item-name">${esc(x.title)}</span>`
}, data = x => [x.title, x.desc, x.href, ...(x.links || []).map(l => `${l.title} ${l.href}`), ...(x.children || []).map(data)].join(" "), items = a => `<ul>${(a||[]).map(x=>`<li ${x.id?`id="${esc(x.id)}" data-subsection="${esc(x.id)}"`:""} data-search="${esc(norm(data(x)))}">${links(x)}${x.desc?`<span class="desc result-desc">${esc(x.desc)}</span>`:""}${x.children?.length?`<div class="children">${items(x.children)}</div>`:""}</li>`).join("")}</ul>`, side = a => `<aside class="sidebar" aria-label="Category navigation"><div class="sidebar-head"><p class="sidebar-title">categories</p><div class="search-wrap"><input id="search" type="search" autocomplete="off" spellcheck="false" aria-label="Search resources" placeholder="Search resources…"><button id="search-clear" type="button" aria-label="Clear search">×</button></div><p class="search-help">/ focus · ↑↓ results · Enter open · Esc clear</p></div><nav id="sidebar-list" class="sidebar-nav">${a.map(s=>`<label class="sidebar-link"><input type="checkbox" value="${esc(s.id)}"><span>${esc(s.title)}</span></label>`).join("")}</nav></aside>`, page = d => {
let n = count(d.sections.flatMap(s => s.items));
return `${side(d.sections)}<section class="content"><p class="meta">${esc(d.title)}</p><p class="status" id="filter-status">all ${d.sections.length} categories · ${n} entries</p><p class="empty" id="empty" hidden>no matching categories; choose a category or clear search</p><div class="grid" id="grid">${d.sections.map(s=>`<section class="cat" id="${esc(s.id)}" data-section="${esc(s.id)}" data-search="${esc(norm(s.title+" "+s.sources.map(x=>x.label).join(" ")))}"><h2 class="cat-title"><a href="#${encodeURIComponent(s.id)}">${esc(s.title)}</a>${s.sources.map(x=>`<a class="source-tag" href="${esc(x.url)}" target="_blank" rel="noopener noreferrer">${esc(x.label)}</a>`).join("")}</h2>${items(s.items)}</section>`).join("")}</div><footer>sources: ${(d.sources||[]).map((s,i)=>`${i?'<span class="desc">·</span>':""}<a href="${esc(s.url)}" target="_blank" rel="noopener noreferrer">${esc(s.label)}</a>`).join("")}</footer></section>`
}, input = () => document.getElementById("search"), apply = () => {
let q = norm(input()?.value || ""),
checks = [...document.querySelectorAll("#sidebar-list input")],
sel = checks.filter(x => x.checked).map(x => x.value),
secs = [...document.querySelectorAll("[data-section]")],
vc = 0,
ve = 0;
secs.forEach(s => {
let li = [...s.querySelectorAll("li")].filter(x => {
let ok = !q || x.dataset.search.includes(q);
x.hidden = !ok;
return ok
}),
ok = (!sel.length || sel.includes(s.dataset.section)) && (!q || s.dataset.search.includes(q) || li.length);
s.hidden = !ok;
if (ok) vc++, ve += li.length
});
document.getElementById("grid").hidden = !vc;
document.getElementById("empty").hidden = !!vc;
document.getElementById("search-clear").style.display = q ? "block" : "none";
document.getElementById("filter-status").textContent = q ? `${vc} matching categories · ${ve} entries` : sel.length ? `${vc} selected categories · ${ve} entries` : `all ${secs.length} categories · ${ve} entries`;
set(SK, sel, STTL);
results = [...document.querySelectorAll(".cat:not([hidden]) .result-link,.cat:not([hidden]) .item-name,.cat:not([hidden]) [data-subsection]")].filter(x => !x.closest("[hidden]"));
selected = -1
}, init = () => {
let saved = new Set(cache(SK, [], STTL)),
i = input();
document.querySelectorAll("#sidebar-list input").forEach(x => {
x.checked = saved.has(x.value);
x.addEventListener("change", apply)
});
i.addEventListener("input", () => {
cancelAnimationFrame(frame);
frame = requestAnimationFrame(apply)
});
document.getElementById("search-clear").addEventListener("click", () => {
i.value = "";
apply();
i.focus()
});
document.addEventListener("keydown", e => {
let text = e.target.matches?.("input,textarea") || e.target.isContentEditable;
if (e.key === "/" && !text) e.preventDefault(), i.focus();
if (e.key === "Escape" && document.activeElement === i && i.value) i.value = "", apply();
if (document.activeElement !== i || !results.length) return;
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
selected = (selected + (e.key === "ArrowDown" ? 1 : -1) + results.length) % results.length;
document.querySelectorAll(".search-selected").forEach(x => x.classList.remove("search-selected"));
results[selected].classList.add("search-selected");
results[selected].scrollIntoView({
block: "center",
behavior: "smooth"
})
}
if (e.key === "Enter") {
e.preventDefault();
let x = results[selected < 0 ? 0 : selected];
x.matches("a") ? x.click() : x.scrollIntoView({
block: "center",
behavior: "smooth"
})
}
});
apply()
}, mount = h => {
app.innerHTML = h;
init()
}, load = async s => {
let markdown = await fetcher(MK(s.id), s.url, s.type === "fmhy" ? extractFMHY : v => v);
return markdown ? {
...s,
markdown
} : null
}, start = async () => {
let cached = S.map(s => ({
...s,
markdown: cache(MK(s.id), "", TTL)
})).filter(s => s.markdown);
cached.length && mount(page(merge(cached.map(s => ({
...parse(s.markdown),
source: s
})))));
let loaded = (await Promise.all(S.map(load))).filter(Boolean);
loaded.length ? mount(page(merge(loaded.map(s => ({
...parse(s.markdown),
source: s
}))))) : cached.length || mount(page({
title: "rsrc",
sources: [],
sections: [{
title: "Error",
id: "error",
sources: [],
items: [{
title: "Unable to load sources",
href: "",
desc: "",
links: [],
children: []
}]
}]
}))
}; start()
})()
</script>
</body>
</html>