This commit is contained in:
parent
729704984c
commit
4bd975796e
148 changed files with 4865 additions and 27561 deletions
12
src/utils/dom.ts
Normal file
12
src/utils/dom.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export function wrapElementsInClass(
|
||||
elements: Iterable<Element>,
|
||||
classes: string[],
|
||||
) {
|
||||
for (const e of elements) {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.dataset.buckBaked = "false";
|
||||
wrapper.classList.add(...classes);
|
||||
e.replaceWith(wrapper);
|
||||
wrapper.appendChild(e);
|
||||
}
|
||||
}
|
79
src/utils/posts.ts
Normal file
79
src/utils/posts.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
function toPadLeft(n: number) {
|
||||
if (n < 10) {
|
||||
return `0${n}`;
|
||||
} else {
|
||||
return n.toString();
|
||||
}
|
||||
}
|
||||
|
||||
export function getPostParam(post: CollectionEntry<"posts">) {
|
||||
const meta = post.data;
|
||||
return {
|
||||
year: meta.date.getUTCFullYear(),
|
||||
month: toPadLeft(meta.date.getUTCMonth() + 1),
|
||||
date: toPadLeft(meta.date.getUTCDate() + 1),
|
||||
postid: post.id,
|
||||
};
|
||||
}
|
||||
|
||||
export function postParamlink(post: CollectionEntry<"posts">) {
|
||||
const meta = post.data;
|
||||
return `/${meta.date.getUTCFullYear()}/${toPadLeft(meta.date.getUTCMonth() + 1)}/${toPadLeft(meta.date.getUTCDate() + 1)}/${post.id}/`;
|
||||
}
|
||||
|
||||
export function getAllTags(posts: CollectionEntry<"posts">[]) {
|
||||
const tags = new Set(posts.filter(shouldBeVisible).flatMap((post) => post.data.tags || []));
|
||||
|
||||
return tags.values().toArray().sort();
|
||||
}
|
||||
|
||||
function p50Of(histogram: number[]) {
|
||||
if (histogram.length === 0) {
|
||||
return undefined;
|
||||
} else if (histogram.length === 1) {
|
||||
return histogram[0];
|
||||
}
|
||||
const idx = histogram.length / 2;
|
||||
if (Math.trunc(idx) !== idx) {
|
||||
const leftIdx = Math.trunc(idx);
|
||||
const rightIdx = Math.ceil(idx);
|
||||
|
||||
return (histogram[leftIdx] + histogram[rightIdx]) / 2;
|
||||
} else {
|
||||
return histogram[idx];
|
||||
}
|
||||
}
|
||||
|
||||
export function getHotTags(posts: CollectionEntry<"posts">[], tags: string[]) {
|
||||
const freqs = new Map(tags.map((x) => [x, 0]));
|
||||
|
||||
for (const post of posts) {
|
||||
for (const tag of post.data.tags || []) {
|
||||
const count = freqs.get(tag)!;
|
||||
freqs.set(tag, count + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const histogram = freqs.values().toArray().sort();
|
||||
const p50 = p50Of(histogram);
|
||||
|
||||
if (p50 === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results = [] as string[];
|
||||
|
||||
for (const [tag, freq] of freqs.entries()) {
|
||||
if (freq > p50) {
|
||||
results.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export function shouldBeVisible(post: CollectionEntry<"posts">) {
|
||||
return !post.data.visibility || post.data.visibility === "visible"
|
||||
}
|
51
src/utils/table.ts
Normal file
51
src/utils/table.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { createRenderEffect, onCleanup } from "solid-js";
|
||||
|
||||
function findCorrespondingTableHeadOfCell(
|
||||
target: HTMLTableCellElement,
|
||||
): HTMLTableCellElement | null {
|
||||
const parentElement = target.parentElement as HTMLTableRowElement;
|
||||
const collection = parentElement.cells;
|
||||
for (let i = 0; i < collection.length; i++) {
|
||||
if (collection.item(i) === target) {
|
||||
const tbody = parentElement.parentElement!;
|
||||
const table = tbody.parentElement!;
|
||||
const allHeads = table.querySelectorAll("thead > tr > th");
|
||||
return allHeads.item(i) as HTMLTableCellElement;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const TABLE_COLUMN_HOVER_CLASSNAME = "table-column-hover";
|
||||
|
||||
export function renderAdvancedTablesOn(elements: Iterable<Element>) {
|
||||
for (const e of elements) {
|
||||
if (!(e instanceof HTMLTableElement)) {
|
||||
console.warn(e, "is unable to be rendered as advanced table");
|
||||
continue;
|
||||
}
|
||||
|
||||
const onMouseEnter = (ev: MouseEvent) => {
|
||||
const head = findCorrespondingTableHeadOfCell(
|
||||
ev.target as HTMLTableCellElement,
|
||||
)!;
|
||||
if (!head.classList.contains(TABLE_COLUMN_HOVER_CLASSNAME)) {
|
||||
head.classList.add(TABLE_COLUMN_HOVER_CLASSNAME);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseLeave = (ev: MouseEvent) => {
|
||||
const head = findCorrespondingTableHeadOfCell(
|
||||
ev.target as HTMLTableCellElement,
|
||||
)!;
|
||||
if (head.classList.contains(TABLE_COLUMN_HOVER_CLASSNAME)) {
|
||||
head.classList.remove(TABLE_COLUMN_HOVER_CLASSNAME);
|
||||
}
|
||||
};
|
||||
|
||||
for (const td of e.querySelectorAll("td")) {
|
||||
td.addEventListener("mouseenter", onMouseEnter);
|
||||
td.addEventListener("mouseleave", onMouseLeave);
|
||||
}
|
||||
}
|
||||
}
|
67
src/utils/useCurrentDate.ts
Normal file
67
src/utils/useCurrentDate.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { differenceInMilliseconds } from "date-fns";
|
||||
import { createRenderEffect, createSignal, onCleanup, untrack } from "solid-js";
|
||||
import { isServer } from "solid-js/web";
|
||||
|
||||
export enum DateRefreshPercision {
|
||||
seconds = 0,
|
||||
minutes = 1,
|
||||
hours,
|
||||
days,
|
||||
}
|
||||
|
||||
function useCurrentDate(
|
||||
percision:
|
||||
| (() => DateRefreshPercision)
|
||||
| DateRefreshPercision = DateRefreshPercision.minutes,
|
||||
) {
|
||||
const [current, setCurrent] = createSignal(new Date());
|
||||
|
||||
const updateInterval = () => {
|
||||
switch (typeof percision === "function" ? percision() : percision) {
|
||||
case DateRefreshPercision.seconds:
|
||||
return 1000;
|
||||
case DateRefreshPercision.minutes:
|
||||
return 15 * 1000;
|
||||
case DateRefreshPercision.hours:
|
||||
return 60 * 1000;
|
||||
case DateRefreshPercision.days:
|
||||
return 60 * 60 * 1000;
|
||||
}
|
||||
};
|
||||
|
||||
let id: undefined | number;
|
||||
|
||||
const update = () => setCurrent(new Date());
|
||||
|
||||
createRenderEffect(() => {
|
||||
if (typeof id !== "undefined") {
|
||||
window.clearInterval(id);
|
||||
id = undefined;
|
||||
}
|
||||
|
||||
const interval = updateInterval();
|
||||
|
||||
const now = new Date();
|
||||
if (differenceInMilliseconds(now, untrack(current)) <= interval) {
|
||||
update();
|
||||
}
|
||||
|
||||
id = window.setInterval(update, interval);
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
if (typeof id !== "undefined") {
|
||||
window.clearInterval(id);
|
||||
id = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
function useCurrentDateServer() {
|
||||
const now = new Date();
|
||||
return () => now;
|
||||
}
|
||||
|
||||
export default isServer ? useCurrentDateServer : useCurrentDate;
|
82
src/utils/wikipedia.ts
Normal file
82
src/utils/wikipedia.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { match } from "@formatjs/intl-localematcher";
|
||||
|
||||
async function selectWikipediaInstance(
|
||||
instance: string,
|
||||
escapedTitle: string,
|
||||
langs: readonly string[],
|
||||
userAgent: string,
|
||||
) {
|
||||
const url = `https://${instance}.wikipedia.org/w/rest.php/v1/page/${escapedTitle}/links/language`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"api-user-agent": userAgent,
|
||||
},
|
||||
});
|
||||
|
||||
if (!(response.ok && response.status === 200)) {
|
||||
throw new TypeError("resource not found");
|
||||
}
|
||||
|
||||
const languages = (await response.json()) as {
|
||||
code: string;
|
||||
key: string;
|
||||
}[];
|
||||
|
||||
const code = match(
|
||||
langs,
|
||||
languages.map((x) => x.code),
|
||||
instance,
|
||||
);
|
||||
|
||||
return languages.find((x) => x.code === code)!;
|
||||
}
|
||||
|
||||
export async function fetchWikipediaSummary(
|
||||
instance: string,
|
||||
title: string,
|
||||
opts: {
|
||||
langs?: readonly string[];
|
||||
userAgent: string;
|
||||
},
|
||||
) {
|
||||
const escapedTitle = encodeURIComponent(title);
|
||||
|
||||
const { key, code } = opts.langs
|
||||
? await selectWikipediaInstance(
|
||||
instance,
|
||||
escapedTitle,
|
||||
opts.langs,
|
||||
opts.userAgent,
|
||||
)
|
||||
: {
|
||||
key: title,
|
||||
code: instance,
|
||||
};
|
||||
|
||||
const baseUrl = `https://${code}.wikipedia.org`;
|
||||
|
||||
const summaryUrl = new URL(
|
||||
`${baseUrl}/api/rest_v1/page/summary/${key}?redirect=false`,
|
||||
);
|
||||
|
||||
const response = await fetch(summaryUrl, {
|
||||
headers: {
|
||||
accept:
|
||||
'application/json; charset=utf-8; profile="https://www.mediawiki.org/wiki/Specs/Summary/1.4.2"',
|
||||
"api-user-agent": `Rubicon's Rubicon (on Server, https://rubicon.lightstands.xyz/external-resource-usage/)`,
|
||||
},
|
||||
});
|
||||
const data = (await response.json()) as {
|
||||
extract: string;
|
||||
titles: { normalized: string };
|
||||
lang: string;
|
||||
content_urls: {
|
||||
desktop: {
|
||||
page: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue