mirror of
https://github.com/anotherhadi/blog.git
synced 2026-05-20 13:32:33 +02:00
73b668b204
Signed-off-by: Hadi <hadi@example.com>
341 lines
13 KiB
Plaintext
341 lines
13 KiB
Plaintext
---
|
||
import { getCollection, render } from "astro:content";
|
||
import Layout from "../../layouts/Layout.astro";
|
||
import { List, PanelRight } from "@lucide/astro";
|
||
import NoteTOC from "../../components/NoteTOC.astro";
|
||
import NoteNavSidebar from "../../components/NoteNavSidebar.svelte";
|
||
import NoteGraphSidebar from "../../components/NoteGraphSidebar.astro";
|
||
import NoteVars from "../../components/NoteVars.svelte";
|
||
import { getCategory, extractLinks, extractHeadings } from "../../utils/notes";
|
||
|
||
export async function getStaticPaths() {
|
||
const notes = await getCollection("notes");
|
||
return notes.map((entry) => ({
|
||
params: { slug: entry.id },
|
||
props: { entry },
|
||
}));
|
||
}
|
||
|
||
const { entry } = Astro.props;
|
||
const { Content } = await render(entry);
|
||
|
||
const allNotes = await getCollection("notes");
|
||
const sortedNotes = allNotes.sort((a, b) =>
|
||
a.data.title.localeCompare(b.data.title),
|
||
);
|
||
const categories = [...new Set(allNotes.map(getCategory))].sort();
|
||
|
||
const allLinks = Object.fromEntries(
|
||
allNotes.map((n) => [n.id, extractLinks(n.body ?? "")]),
|
||
);
|
||
const forwardLinks = (allLinks[entry.id] ?? [])
|
||
.map((id) => allNotes.find((n) => n.id === id))
|
||
.filter(Boolean) as typeof allNotes;
|
||
const backlinks = allNotes.filter(
|
||
(n) => n.id !== entry.id && (allLinks[n.id] ?? []).includes(entry.id),
|
||
);
|
||
|
||
const graphNodes = [
|
||
{ id: entry.id, title: entry.data.title, current: true },
|
||
...forwardLinks.map((n) => ({
|
||
id: n.id,
|
||
title: n.data.title,
|
||
current: false,
|
||
})),
|
||
...backlinks
|
||
.filter((n) => !forwardLinks.some((f) => f.id === n.id))
|
||
.map((n) => ({ id: n.id, title: n.data.title, current: false })),
|
||
];
|
||
const graphEdges = [
|
||
...forwardLinks.map((n) => ({ from: entry.id, to: n.id })),
|
||
...backlinks.map((n) => ({ from: n.id, to: entry.id })),
|
||
];
|
||
|
||
const noteVars = [
|
||
...new Set(
|
||
Array.from(
|
||
(entry.body ?? "").matchAll(/\$([a-zA-Z_][a-zA-Z0-9_]*)/g),
|
||
(m) => m[1],
|
||
),
|
||
),
|
||
];
|
||
|
||
const headings = extractHeadings(entry.body ?? "");
|
||
---
|
||
|
||
<style>
|
||
.drawer.lg\:drawer-open > .drawer-side,
|
||
.drawer.xl\:drawer-open > .drawer-side {
|
||
top: 3rem;
|
||
height: calc(100vh - 3rem);
|
||
}
|
||
</style>
|
||
|
||
<Layout
|
||
title={`${entry.data.title} - Infosec Notes`}
|
||
description={entry.data.description}
|
||
>
|
||
<main class="max-w-screen-2xl mx-auto">
|
||
<div class="drawer drawer-end xl:drawer-open min-h-[calc(100vh-3rem)]">
|
||
<input id="graph-drawer" type="checkbox" class="drawer-toggle" />
|
||
|
||
<div class="drawer-content flex min-h-[calc(100vh-3rem)]">
|
||
<div class="drawer lg:drawer-open w-full">
|
||
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
|
||
|
||
<div class="drawer-content flex flex-col min-w-0">
|
||
<main class="flex-1 px-4 sm:px-6 lg:px-10 py-6 lg:py-10 min-w-0">
|
||
<div class="max-w-3xl mx-auto lg:mx-0">
|
||
<div class="flex items-center justify-between mb-6">
|
||
<div
|
||
class="breadcrumbs text-xs font-mono text-base-content/35 p-0"
|
||
>
|
||
<ul>
|
||
<li>
|
||
<a href="/notes" class="hover:text-base-content/70"
|
||
>notes</a
|
||
>
|
||
</li>
|
||
<li>
|
||
{
|
||
entry.id.includes("/") ? (
|
||
<a
|
||
href={`/notes/${getCategory(entry)}`}
|
||
class="hover:text-base-content/70"
|
||
>
|
||
{getCategory(entry)}
|
||
</a>
|
||
) : (
|
||
getCategory(entry)
|
||
)
|
||
}
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<label
|
||
for="nav-drawer"
|
||
class="btn btn-ghost btn-xs lg:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
||
>
|
||
<List size={11} />
|
||
nav
|
||
</label>
|
||
<NoteVars client:load vars={noteVars} />
|
||
<label
|
||
for="graph-drawer"
|
||
id="graph-toggle"
|
||
class="btn btn-ghost btn-xs font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
||
title="Toggle graph"
|
||
>
|
||
<PanelRight size={11} />
|
||
graph
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<header class="mb-8">
|
||
<h1
|
||
class="text-4xl sm:text-5xl font-bold tracking-tight mb-3"
|
||
>
|
||
{entry.data.title}
|
||
</h1>
|
||
<p class="text-base-content/50 mb-4">
|
||
{entry.data.description}
|
||
</p>
|
||
{
|
||
entry.data.tags.length > 0 && (
|
||
<div class="flex flex-wrap gap-1 mb-4">
|
||
{entry.data.tags.map((tag) => (
|
||
<a
|
||
href={`/notes?tag=${tag}`}
|
||
class="badge badge-ghost badge-xs font-mono text-base-content/30 hover:text-primary/70 transition-colors"
|
||
>
|
||
{tag}
|
||
</a>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
</header>
|
||
|
||
<NoteTOC headings={headings} />
|
||
|
||
<div
|
||
class="note-content text-sm leading-relaxed text-base-content/80
|
||
[&_h2]:text-lg [&_h2]:font-bold [&_h2]:mt-8 [&_h2]:mb-3 [&_h2]:text-base-content [&_h2]:tracking-tight [&_h2]:pb-1.5 [&_h2]:border-b [&_h2]:border-base-300/30
|
||
[&_h3]:text-base [&_h3]:font-semibold [&_h3]:mt-6 [&_h3]:mb-2 [&_h3]:text-base-content/90
|
||
[&_h4]:text-sm [&_h4]:font-semibold [&_h4]:mt-4 [&_h4]:mb-2 [&_h4]:text-base-content/80
|
||
[&_p]:mb-4 [&_p]:leading-relaxed
|
||
[&_a]:text-primary/80 [&_a]:underline [&_a]:underline-offset-2 [&_a]:hover:text-primary [&_a]:transition-colors
|
||
[&_ul]:mb-4 [&_ul]:ml-5 [&_ul]:list-none [&_ul]:space-y-1
|
||
[&_ul_li]:before:content-['–'] [&_ul_li]:before:text-base-content/25 [&_ul_li]:before:mr-2 [&_ul_li]:before:font-mono
|
||
[&_ol]:mb-4 [&_ol]:ml-5 [&_ol]:list-decimal [&_ol]:space-y-1
|
||
[&_li]:text-base-content/75
|
||
[&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded-field [&_code]:font-mono [&_code]:text-xs [&_code]:bg-base-200 [&_code]:text-primary/80 [&_code]:border [&_code]:border-base-300/50
|
||
[&_pre]:p-4 [&_pre]:overflow-x-auto [&_pre]:mb-4 [&_pre]:rounded-box [&_pre]:bg-base-200/60 [&_pre]:border [&_pre]:border-base-300/50 [&_pre]:text-xs
|
||
[&_pre_code]:bg-transparent [&_pre_code]:border-0 [&_pre_code]:p-0 [&_pre_code]:text-base-content/80
|
||
[&_blockquote]:border-l-2 [&_blockquote]:border-primary/25 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:my-4 [&_blockquote]:text-base-content/50
|
||
[&_table]:w-full [&_table]:mb-6 [&_table]:text-xs [&_table]:border-collapse
|
||
[&_th]:text-left [&_th]:px-3 [&_th]:py-2 [&_th]:border [&_th]:border-base-300/50 [&_th]:bg-base-200/60 [&_th]:font-mono [&_th]:text-[10px] [&_th]:uppercase [&_th]:tracking-widest [&_th]:text-base-content/50
|
||
[&_td]:px-3 [&_td]:py-2 [&_td]:border [&_td]:border-base-300/40 [&_td]:font-mono [&_td]:text-xs [&_td]:text-base-content/70
|
||
[&_tr:nth-child(even)_td]:bg-base-200/20
|
||
[&_hr]:border-t [&_hr]:border-base-300/30 [&_hr]:my-8"
|
||
>
|
||
<Content />
|
||
</div>
|
||
|
||
<div
|
||
class="border-t border-base-300/30 mt-12 pt-6 flex items-center justify-between font-mono text-[10px] text-base-content/25"
|
||
>
|
||
<a
|
||
href="/notes"
|
||
class="hover:text-base-content/50 transition-colors"
|
||
>
|
||
← all notes
|
||
</a>
|
||
<a
|
||
href="/"
|
||
class="hover:text-base-content/50 transition-colors"
|
||
>
|
||
~/hadi
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<div class="drawer-side z-50">
|
||
<label
|
||
for="nav-drawer"
|
||
aria-label="close sidebar"
|
||
class="drawer-overlay"></label>
|
||
<NoteNavSidebar
|
||
client:load
|
||
notes={sortedNotes}
|
||
currentEntry={entry}
|
||
categories={categories}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="drawer-side z-40">
|
||
<label
|
||
for="graph-drawer"
|
||
aria-label="close sidebar"
|
||
class="drawer-overlay xl:hidden"></label>
|
||
<NoteGraphSidebar
|
||
entry={entry}
|
||
graphNodes={graphNodes}
|
||
graphEdges={graphEdges}
|
||
forwardLinks={forwardLinks}
|
||
backlinks={backlinks}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</Layout>
|
||
|
||
<script>
|
||
function injectHeadingAnchors() {
|
||
if (!document.getElementById("heading-anchor-styles")) {
|
||
const s = document.createElement("style");
|
||
s.id = "heading-anchor-styles";
|
||
s.textContent = `
|
||
.note-content h2:not(.link-card h2),
|
||
.note-content h3:not(.link-card h3),
|
||
.note-content h4:not(.link-card h4) {
|
||
display: flex !important;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 0;
|
||
}
|
||
.heading-anchor {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
margin-left: 0.4em;
|
||
color: oklch(38% 0 0);
|
||
opacity: 0;
|
||
transition: opacity 120ms, color 120ms;
|
||
text-decoration: none;
|
||
}
|
||
.note-content h2:hover .heading-anchor,
|
||
.note-content h3:hover .heading-anchor,
|
||
.note-content h4:hover .heading-anchor { opacity: 1; }
|
||
.heading-anchor:hover, .heading-anchor.copied { color: oklch(71% 0.0863 296.59); opacity: 1; }
|
||
`;
|
||
document.head.appendChild(s);
|
||
}
|
||
|
||
document
|
||
.querySelectorAll(".note-content h2, .note-content h3, .note-content h4")
|
||
.forEach((heading) => {
|
||
if (!heading.id || heading.querySelector(".heading-anchor")) return;
|
||
const anchor = document.createElement("a");
|
||
anchor.href = `#${heading.id}`;
|
||
anchor.className = "heading-anchor";
|
||
anchor.setAttribute("aria-label", "Copy link to section");
|
||
anchor.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`;
|
||
anchor.addEventListener("click", (e) => {
|
||
e.preventDefault();
|
||
const url = `${location.origin}${location.pathname}#${heading.id}`;
|
||
navigator.clipboard.writeText(url).then(() => {
|
||
anchor.classList.add("copied");
|
||
setTimeout(() => anchor.classList.remove("copied"), 1800);
|
||
});
|
||
history.pushState(null, "", `#${heading.id}`);
|
||
});
|
||
heading.appendChild(anchor);
|
||
});
|
||
}
|
||
|
||
function initXlGraphToggle() {
|
||
const graphDrawer = document.getElementById(
|
||
"graph-drawer",
|
||
) as HTMLInputElement | null;
|
||
if (!graphDrawer) return;
|
||
|
||
const outerDrawer = graphDrawer.closest<HTMLElement>(".drawer.drawer-end");
|
||
const xlQuery = window.matchMedia("(min-width: 1280px)");
|
||
const STORAGE_KEY = "notes-graph-sidebar";
|
||
|
||
function setXlSidebar(open: boolean) {
|
||
if (!outerDrawer) return;
|
||
if (open) {
|
||
outerDrawer.classList.add("xl:drawer-open");
|
||
} else {
|
||
outerDrawer.classList.remove("xl:drawer-open");
|
||
}
|
||
localStorage.setItem(STORAGE_KEY, open ? "1" : "0");
|
||
}
|
||
|
||
const graphToggle = document.getElementById("graph-toggle");
|
||
graphToggle?.addEventListener("click", (e) => {
|
||
if (!xlQuery.matches) return;
|
||
e.preventDefault();
|
||
setXlSidebar(!outerDrawer?.classList.contains("xl:drawer-open"));
|
||
});
|
||
|
||
if (xlQuery.matches) {
|
||
const saved = localStorage.getItem(STORAGE_KEY);
|
||
// Open by default unless user explicitly closed it
|
||
setXlSidebar(saved !== "0");
|
||
}
|
||
|
||
xlQuery.addEventListener("change", (e) => {
|
||
if (!e.matches) {
|
||
outerDrawer?.classList.remove("xl:drawer-open");
|
||
} else {
|
||
const saved = localStorage.getItem(STORAGE_KEY);
|
||
setXlSidebar(saved !== "0");
|
||
}
|
||
});
|
||
}
|
||
|
||
document.addEventListener("astro:page-load", () => {
|
||
injectHeadingAnchors();
|
||
initXlGraphToggle();
|
||
});
|
||
</script>
|