From 456cf011b36de91c9936994b1fa45703adcd309b Mon Sep 17 00:00:00 2001 From: Dawid Rycerz Date: Thu, 3 Jul 2025 10:56:21 +0300 Subject: Initial fork of chrismwilliams/astro-theme-cactus theme --- src/utils/date.ts | 23 ++++++++++ src/utils/domElement.ts | 11 +++++ src/utils/generateToc.ts | 37 +++++++++++++++ src/utils/webmentions.ts | 115 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 186 insertions(+) create mode 100644 src/utils/date.ts create mode 100644 src/utils/domElement.ts create mode 100644 src/utils/generateToc.ts create mode 100644 src/utils/webmentions.ts (limited to 'src/utils') diff --git a/src/utils/date.ts b/src/utils/date.ts new file mode 100644 index 0000000..fb943a5 --- /dev/null +++ b/src/utils/date.ts @@ -0,0 +1,23 @@ +import type { CollectionEntry } from "astro:content"; +import { siteConfig } from "@/site.config"; + +export function getFormattedDate( + date: Date | undefined, + options?: Intl.DateTimeFormatOptions, +): string { + if (date === undefined) { + return "Invalid Date"; + } + + return new Intl.DateTimeFormat(siteConfig.date.locale, { + ...(siteConfig.date.options as Intl.DateTimeFormatOptions), + ...options, + }).format(date); +} + +export function collectionDateSort( + a: CollectionEntry<"post" | "note">, + b: CollectionEntry<"post" | "note">, +) { + return b.data.publishDate.getTime() - a.data.publishDate.getTime(); +} diff --git a/src/utils/domElement.ts b/src/utils/domElement.ts new file mode 100644 index 0000000..09361fc --- /dev/null +++ b/src/utils/domElement.ts @@ -0,0 +1,11 @@ +export function toggleClass(element: HTMLElement, className: string) { + element.classList.toggle(className); +} + +export function elementHasClass(element: HTMLElement, className: string) { + return element.classList.contains(className); +} + +export function rootInDarkMode() { + return document.documentElement.getAttribute("data-theme") === "dark"; +} diff --git a/src/utils/generateToc.ts b/src/utils/generateToc.ts new file mode 100644 index 0000000..f63f0bf --- /dev/null +++ b/src/utils/generateToc.ts @@ -0,0 +1,37 @@ +// Heavy inspiration from starlight: https://github.com/withastro/starlight/blob/main/packages/starlight/utils/generateToC.ts +import type { MarkdownHeading } from "astro"; + +export interface TocItem extends MarkdownHeading { + children: TocItem[]; +} + +interface TocOpts { + maxHeadingLevel?: number | undefined; + minHeadingLevel?: number | undefined; +} + +/** Inject a ToC entry as deep in the tree as its `depth` property requires. */ +function injectChild(items: TocItem[], item: TocItem): void { + const lastItem = items.at(-1); + if (!lastItem || lastItem.depth >= item.depth) { + items.push(item); + } else { + injectChild(lastItem.children, item); + return; + } +} + +export function generateToc( + headings: ReadonlyArray, + { maxHeadingLevel = 4, minHeadingLevel = 2 }: TocOpts = {}, +) { + // by default this ignores/filters out h1 and h5 heading(s) + const bodyHeadings = headings.filter( + ({ depth }) => depth >= minHeadingLevel && depth <= maxHeadingLevel, + ); + const toc: Array = []; + + for (const heading of bodyHeadings) injectChild(toc, { ...heading, children: [] }); + + return toc; +} diff --git a/src/utils/webmentions.ts b/src/utils/webmentions.ts new file mode 100644 index 0000000..c9f62b7 --- /dev/null +++ b/src/utils/webmentions.ts @@ -0,0 +1,115 @@ +import * as fs from "node:fs"; +import { WEBMENTION_API_KEY } from "astro:env/server"; +import type { WebmentionsCache, WebmentionsChildren, WebmentionsFeed } from "@/types"; + +const DOMAIN = import.meta.env.SITE; +const CACHE_DIR = ".data"; +const filePath = `${CACHE_DIR}/webmentions.json`; +const validWebmentionTypes = ["like-of", "mention-of", "in-reply-to"]; + +const hostName = new URL(DOMAIN).hostname; + +// Calls webmention.io api. +async function fetchWebmentions(timeFrom: string | null, perPage = 1000) { + if (!DOMAIN) { + console.warn("No domain specified. Please set in astro.config.ts"); + return null; + } + + if (!WEBMENTION_API_KEY) { + console.warn("No webmention api token specified in .env"); + return null; + } + + let url = `https://webmention.io/api/mentions.jf2?domain=${hostName}&token=${WEBMENTION_API_KEY}&sort-dir=up&per-page=${perPage}`; + + if (timeFrom) url += `&since${timeFrom}`; + + const res = await fetch(url); + + if (res.ok) { + const data = (await res.json()) as WebmentionsFeed; + return data; + } + + return null; +} + +// Merge cached entries [a] with fresh webmentions [b], merge by wm-id +function mergeWebmentions(a: WebmentionsCache, b: WebmentionsFeed): WebmentionsChildren[] { + return Array.from( + [...a.children, ...b.children] + .reduce((map, obj) => map.set(obj["wm-id"], obj), new Map()) + .values(), + ); +} + +// filter out WebmentionChildren +export function filterWebmentions(webmentions: WebmentionsChildren[]) { + return webmentions.filter((webmention) => { + // make sure the mention has a property so we can sort them later + if (!validWebmentionTypes.includes(webmention["wm-property"])) return false; + + // make sure 'mention-of' or 'in-reply-to' has text content. + if (webmention["wm-property"] === "mention-of" || webmention["wm-property"] === "in-reply-to") { + return webmention.content && webmention.content.text !== ""; + } + + return true; + }); +} + +// save combined webmentions in cache file +function writeToCache(data: WebmentionsCache) { + const fileContent = JSON.stringify(data, null, 2); + + // create cache folder if it doesn't exist already + if (!fs.existsSync(CACHE_DIR)) { + fs.mkdirSync(CACHE_DIR); + } + + // write data to cache json file + fs.writeFile(filePath, fileContent, (err) => { + if (err) throw err; + console.log(`Webmentions saved to ${filePath}`); + }); +} + +function getFromCache(): WebmentionsCache { + if (fs.existsSync(filePath)) { + const data = fs.readFileSync(filePath, "utf-8"); + return JSON.parse(data); + } + // no cache found + return { + lastFetched: null, + children: [], + }; +} + +async function getAndCacheWebmentions() { + const cache = getFromCache(); + const mentions = await fetchWebmentions(cache.lastFetched); + + if (mentions) { + mentions.children = filterWebmentions(mentions.children); + const webmentions: WebmentionsCache = { + lastFetched: new Date().toISOString(), + // Make sure the first arg is the cache + children: mergeWebmentions(cache, mentions), + }; + + writeToCache(webmentions); + return webmentions; + } + + return cache; +} + +let webMentions: WebmentionsCache; + +export async function getWebmentionsForUrl(url: string) { + if (!webMentions) webMentions = await getAndCacheWebmentions(); + + return webMentions.children.filter((entry) => entry["wm-target"] === url); +} -- cgit v1.2.3