Add index.html
This commit is contained in:
commit
c1fdbce36c
859
index.html
Normal file
859
index.html
Normal file
@ -0,0 +1,859 @@
|
|||||||
|
<!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 => ({
|
||||||
|
"&": "&",
|
||||||
|
"<": "<",
|
||||||
|
">": ">",
|
||||||
|
'"': """,
|
||||||
|
"'": "'"
|
||||||
|
} [c])),
|
||||||
|
plain = v => String(v ?? "").replace(/!\[[^\]]*]\([^)]*\)/g, "").replace(/\[([^\]]+)]\([^)]+\)/g, "$1").replace(/<[^>]*>/g, "").replace(/[`*_~\]]/g, "").replace(/&/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>
|
||||||
Loading…
x
Reference in New Issue
Block a user