mirror of
https://github.com/KazooTTT/kazoottt-blog-v2.git
synced 2025-06-22 18:21:31 +08:00
Initial commit
This commit is contained in:
23
src/utils/date.ts
Normal file
23
src/utils/date.ts
Normal file
@ -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();
|
||||
}
|
11
src/utils/domElement.ts
Normal file
11
src/utils/domElement.ts
Normal file
@ -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";
|
||||
}
|
37
src/utils/generateToc.ts
Normal file
37
src/utils/generateToc.ts
Normal file
@ -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<MarkdownHeading>,
|
||||
{ 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<TocItem> = [];
|
||||
|
||||
for (const heading of bodyHeadings) injectChild(toc, { ...heading, children: [] });
|
||||
|
||||
return toc;
|
||||
}
|
115
src/utils/webmentions.ts
Normal file
115
src/utils/webmentions.ts
Normal file
@ -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);
|
||||
}
|
Reference in New Issue
Block a user