mirror of
https://github.com/LukeHagar/unicorn-utterances.git
synced 2025-12-09 21:07:49 +00:00
enable tsconfig strict mode & fix type errors
This commit is contained in:
@@ -36,19 +36,24 @@ export default defineConfig({
|
|||||||
lastmod: new Date(),
|
lastmod: new Date(),
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: "en",
|
defaultLocale: "en",
|
||||||
locales: Object.keys(languages).reduce((prev, key) => {
|
locales: Object.keys(languages).reduce(
|
||||||
prev[key] = fileToOpenGraphConverter(key as keyof typeof languages);
|
(prev, key) => {
|
||||||
return prev;
|
prev[key] = fileToOpenGraphConverter(key as keyof typeof languages);
|
||||||
}, {}),
|
return prev;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
filter(page) {
|
filter(page) {
|
||||||
// return true, unless lart part of the URL ends with "_noindex"
|
// return true, unless lart part of the URL ends with "_noindex"
|
||||||
// in which case it should not be in the sitemap
|
// in which case it should not be in the sitemap
|
||||||
return !page
|
return !(
|
||||||
.split("/")
|
page
|
||||||
.filter((part) => !!part.length)
|
.split("/")
|
||||||
.at(-1)
|
.filter((part) => !!part.length)
|
||||||
.endsWith("_noindex");
|
.at(-1)
|
||||||
|
?.endsWith("_noindex") ?? false
|
||||||
|
);
|
||||||
},
|
},
|
||||||
serialize({ url, ...rest }) {
|
serialize({ url, ...rest }) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -58,9 +58,9 @@ async function generateCollectionEPub(
|
|||||||
collectionPosts: PostInfo[],
|
collectionPosts: PostInfo[],
|
||||||
fileLocation: string,
|
fileLocation: string,
|
||||||
) {
|
) {
|
||||||
const authors = collection.authors.map((id) => {
|
const authors = collection.authors
|
||||||
return getUnicornById(id, collection.locale).name;
|
.map((id) => getUnicornById(id, collection.locale)?.name)
|
||||||
});
|
.filter((name): name is string => !!name);
|
||||||
|
|
||||||
const epub = new EPub(
|
const epub = new EPub(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const createPostIndex = () => {
|
|||||||
getFn: (post) => {
|
getFn: (post) => {
|
||||||
return post.authors
|
return post.authors
|
||||||
.map((id) => api.getUnicornById(id, post.locale))
|
.map((id) => api.getUnicornById(id, post.locale))
|
||||||
.flatMap((author) => Object.values(author.socials))
|
.flatMap((author) => Object.values(author!.socials))
|
||||||
.join(", ");
|
.join(", ");
|
||||||
},
|
},
|
||||||
weight: 1.2,
|
weight: 1.2,
|
||||||
@@ -60,7 +60,7 @@ const createCollectionIndex = () => {
|
|||||||
name: "authorName",
|
name: "authorName",
|
||||||
getFn: (post) => {
|
getFn: (post) => {
|
||||||
return post.authors
|
return post.authors
|
||||||
.map((id) => api.getUnicornById(id, post.locale).name)
|
.map((id) => api.getUnicornById(id, post.locale)!.name)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
},
|
},
|
||||||
weight: 1.8,
|
weight: 1.8,
|
||||||
@@ -70,7 +70,7 @@ const createCollectionIndex = () => {
|
|||||||
getFn: (post) => {
|
getFn: (post) => {
|
||||||
return post.authors
|
return post.authors
|
||||||
.map((id) => api.getUnicornById(id, post.locale))
|
.map((id) => api.getUnicornById(id, post.locale))
|
||||||
.flatMap((author) => Object.values(author.socials))
|
.flatMap((author) => Object.values(author!.socials))
|
||||||
.join(", ");
|
.join(", ");
|
||||||
},
|
},
|
||||||
weight: 1.2,
|
weight: 1.2,
|
||||||
@@ -87,12 +87,13 @@ const createCollectionIndex = () => {
|
|||||||
const postIndex = createPostIndex();
|
const postIndex = createPostIndex();
|
||||||
const collectionIndex = createCollectionIndex();
|
const collectionIndex = createCollectionIndex();
|
||||||
|
|
||||||
const unicorns: Record<string, UnicornInfo> = api
|
const unicorns = api.getUnicornsByLang("en").reduce(
|
||||||
.getUnicornsByLang("en")
|
(obj, unicorn) => {
|
||||||
.reduce((obj, unicorn) => {
|
|
||||||
obj[unicorn.id] = unicorn;
|
obj[unicorn.id] = unicorn;
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
},
|
||||||
|
{} as Record<string, UnicornInfo>,
|
||||||
|
);
|
||||||
|
|
||||||
const json = JSON.stringify({
|
const json = JSON.stringify({
|
||||||
postIndex,
|
postIndex,
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import style from "./banner-css";
|
|||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
import tags from "../../../content/data/tags.json";
|
import tags from "../../../content/data/tags.json";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
import { TagInfo } from "types/TagInfo";
|
||||||
|
|
||||||
const TAG_SVG_DEFAULT = fs.readFileSync("public/stickers/role_devops.svg", "utf-8");
|
const TAG_SVG_DEFAULT = fs.readFileSync("public/stickers/role_devops.svg", "utf-8");
|
||||||
|
const tagsMap = new Map(Object.entries(tags));
|
||||||
|
|
||||||
function BannerCodeScreen({
|
function BannerCodeScreen({
|
||||||
post,
|
post,
|
||||||
@@ -19,8 +21,9 @@ function BannerCodeScreen({
|
|||||||
const rotX = (post.description.length % 20) - 10;
|
const rotX = (post.description.length % 20) - 10;
|
||||||
const rotY = (post.title.length * 3) % 20;
|
const rotY = (post.title.length * 3) % 20;
|
||||||
|
|
||||||
const tagInfo = post.tags.map(tag => tags[tag])
|
const tagInfo = post.tags.map(tag => tagsMap.get(tag))
|
||||||
.filter(t => t?.emoji || (t?.image && t?.shownWithBranding))[0];
|
.filter((t): t is TagInfo => !!t)
|
||||||
|
.filter(t => t.emoji || (t.image && t.shownWithBranding))[0];
|
||||||
|
|
||||||
const tagSvg = tagInfo?.image
|
const tagSvg = tagInfo?.image
|
||||||
? fs.readFileSync("public" + tagInfo.image, "utf-8")
|
? fs.readFileSync("public" + tagInfo.image, "utf-8")
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const TwitterLargeCard = ({
|
|||||||
</div>
|
</div>
|
||||||
<div class="postInfo">
|
<div class="postInfo">
|
||||||
<span class="authors">
|
<span class="authors">
|
||||||
{post.authors.map((id) => getUnicornById(id, post.locale).name).join(", ")}
|
{post.authors.map((id) => getUnicornById(id, post.locale)!.name).join(", ")}
|
||||||
</span>
|
</span>
|
||||||
<span class="date">
|
<span class="date">
|
||||||
{post.publishedMeta} · {post.wordCount.toLocaleString("en")} words
|
{post.publishedMeta} · {post.wordCount.toLocaleString("en")} words
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const layouts: Layout[] = [banner, twitterPreview];
|
|||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
const post = getPostBySlug("async-pipe-is-not-pure", "en");
|
const post = getPostBySlug("async-pipe-is-not-pure", "en")!;
|
||||||
|
|
||||||
const rebuild = async () => {
|
const rebuild = async () => {
|
||||||
console.log("rebuilding...");
|
console.log("rebuilding...");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PostInfo } from "types/index";
|
import { PostInfo } from "types/index";
|
||||||
import { render } from "preact-render-to-string";
|
import { render } from "preact-render-to-string";
|
||||||
import { createElement } from "preact";
|
import { VNode, createElement } from "preact";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import { unified } from "unified";
|
import { unified } from "unified";
|
||||||
import remarkParse from "remark-parse";
|
import remarkParse from "remark-parse";
|
||||||
@@ -68,7 +68,7 @@ export const renderPostPreviewToString = async (
|
|||||||
const authorImageMap = Object.fromEntries(
|
const authorImageMap = Object.fromEntries(
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
post.authors.map(async (authorId) => {
|
post.authors.map(async (authorId) => {
|
||||||
const author = getUnicornById(authorId, post.locale);
|
const author = getUnicornById(authorId, post.locale)!;
|
||||||
|
|
||||||
if (authorImageCache.has(author.id))
|
if (authorImageCache.has(author.id))
|
||||||
return [author.id, authorImageCache.get(author.id)];
|
return [author.id, authorImageCache.get(author.id)];
|
||||||
@@ -113,7 +113,7 @@ export const renderPostPreviewToString = async (
|
|||||||
width: PAGE_WIDTH,
|
width: PAGE_WIDTH,
|
||||||
height: PAGE_HEIGHT,
|
height: PAGE_HEIGHT,
|
||||||
authorImageMap,
|
authorImageMap,
|
||||||
}),
|
}) as VNode<{}>,
|
||||||
)}
|
)}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -34,6 +34,7 @@
|
|||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
"@types/json5": "^2.2.0",
|
"@types/json5": "^2.2.0",
|
||||||
"@types/node": "^20.5.0",
|
"@types/node": "^20.5.0",
|
||||||
|
"@types/probe-image-size": "^7.2.2",
|
||||||
"@types/uuid": "^9.0.2",
|
"@types/uuid": "^9.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
||||||
"@typescript-eslint/parser": "^6.3.0",
|
"@typescript-eslint/parser": "^6.3.0",
|
||||||
@@ -7273,6 +7274,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||||
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
|
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/needle": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/needle/-/needle-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-xUKAjFjDcucpgfyTvnbaqN+WBKyM9UehBuVRI/1AoPbaXrCScADqeTgTM1ZBYnS3Ovs9IEQt813IcJNyac7dNQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/nlcst": {
|
"node_modules/@types/nlcst": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-1.0.1.tgz",
|
||||||
@@ -7309,6 +7319,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/probe-image-size": {
|
||||||
|
"version": "7.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/probe-image-size/-/probe-image-size-7.2.2.tgz",
|
||||||
|
"integrity": "sha512-rNWbJLj3XdCfiHsaDVTkPgvTtxlVMgNEpGpJYU/d2pVwzIpjumZguQzlnOQPWF7ajPOpX00rmPQGerRlB3tL+g==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/needle": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
"@types/json5": "^2.2.0",
|
"@types/json5": "^2.2.0",
|
||||||
"@types/node": "^20.5.0",
|
"@types/node": "^20.5.0",
|
||||||
|
"@types/probe-image-size": "^7.2.2",
|
||||||
"@types/uuid": "^9.0.2",
|
"@types/uuid": "^9.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
||||||
"@typescript-eslint/parser": "^6.3.0",
|
"@typescript-eslint/parser": "^6.3.0",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
useRadioGroupState,
|
useRadioGroupState,
|
||||||
} from "react-stately";
|
} from "react-stately";
|
||||||
|
|
||||||
const RadioContext = createContext<RadioGroupState>(null);
|
const RadioContext = createContext<RadioGroupState | null>(null);
|
||||||
|
|
||||||
interface RadioButtonGroupProps extends PropsWithChildren<RadioGroupProps> {
|
interface RadioButtonGroupProps extends PropsWithChildren<RadioGroupProps> {
|
||||||
class?: string;
|
class?: string;
|
||||||
@@ -51,6 +51,10 @@ export function RadioButtonGroup(props: RadioButtonGroupProps) {
|
|||||||
export function RadioButton(props: AriaRadioProps) {
|
export function RadioButton(props: AriaRadioProps) {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
const state = useContext(RadioContext);
|
const state = useContext(RadioContext);
|
||||||
|
if (!state) {
|
||||||
|
throw new Error("<RadioButton> must only be used within a <RadioButtonGroup>!");
|
||||||
|
}
|
||||||
|
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const { inputProps, isSelected } = useRadio(props, state, ref);
|
const { inputProps, isSelected } = useRadio(props, state, ref);
|
||||||
const { isFocusVisible, focusProps } = useFocusRing();
|
const { isFocusVisible, focusProps } = useFocusRing();
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import { JSXNode, PropsWithChildren } from "../types";
|
import { JSXNode, PropsWithChildren } from "../types";
|
||||||
import { createElement, Ref, VNode } from "preact";
|
|
||||||
import { JSX } from "preact";
|
import { JSX } from "preact";
|
||||||
import { forwardRef } from "preact/compat";
|
import { ForwardedRef, forwardRef } from "preact/compat";
|
||||||
import { useMemo } from "preact/hooks";
|
|
||||||
|
|
||||||
type AllowedTags = "a" | "button" | "span" | "div";
|
type AllowedTags = "a" | "button" | "span" | "div";
|
||||||
|
|
||||||
|
type AllowedElements<Tag extends AllowedTags> = (
|
||||||
|
Tag extends "a"
|
||||||
|
? HTMLAnchorElement
|
||||||
|
: Tag extends "div"
|
||||||
|
? HTMLDivElement
|
||||||
|
: Tag extends "span"
|
||||||
|
? HTMLSpanElement
|
||||||
|
: HTMLButtonElement
|
||||||
|
);
|
||||||
|
|
||||||
type ButtonProps<Tag extends AllowedTags> = PropsWithChildren<
|
type ButtonProps<Tag extends AllowedTags> = PropsWithChildren<
|
||||||
{
|
{
|
||||||
tag?: Tag;
|
tag?: Tag;
|
||||||
@@ -19,19 +27,11 @@ type ButtonProps<Tag extends AllowedTags> = PropsWithChildren<
|
|||||||
| "secondary-emphasized"
|
| "secondary-emphasized"
|
||||||
| "primary"
|
| "primary"
|
||||||
| "secondary";
|
| "secondary";
|
||||||
} & JSX.HTMLAttributes<
|
} & JSX.HTMLAttributes<AllowedElements<Tag>>
|
||||||
Tag extends "a"
|
|
||||||
? HTMLAnchorElement
|
|
||||||
: Tag extends "div"
|
|
||||||
? HTMLDivElement
|
|
||||||
: Tag extends "span"
|
|
||||||
? HTMLSpanElement
|
|
||||||
: HTMLButtonElement
|
|
||||||
>
|
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const ButtonWrapper = forwardRef(
|
const ButtonWrapper = forwardRef<AllowedElements<AllowedTags> | null, ButtonProps<AllowedTags>>(
|
||||||
<T extends AllowedTags = "a">(
|
(
|
||||||
{
|
{
|
||||||
tag = "a" as never,
|
tag = "a" as never,
|
||||||
class: className,
|
class: className,
|
||||||
@@ -41,16 +41,8 @@ const ButtonWrapper = forwardRef(
|
|||||||
rightIcon,
|
rightIcon,
|
||||||
isFocusVisible,
|
isFocusVisible,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps<T>,
|
},
|
||||||
ref: Ref<
|
ref,
|
||||||
T extends "a"
|
|
||||||
? HTMLAnchorElement
|
|
||||||
: T extends "div"
|
|
||||||
? HTMLDivElement
|
|
||||||
: T extends "span"
|
|
||||||
? HTMLSpanElement
|
|
||||||
: HTMLButtonElement
|
|
||||||
>,
|
|
||||||
) => {
|
) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const Wrapper: any = tag;
|
const Wrapper: any = tag;
|
||||||
@@ -85,10 +77,10 @@ const ButtonWrapper = forwardRef(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Button = forwardRef(
|
export const Button = forwardRef<AllowedElements<AllowedTags> | null, ButtonProps<AllowedTags>>(
|
||||||
<T extends AllowedTags = "a">(
|
(
|
||||||
{ class: className = "", ...props }: ButtonProps<T>,
|
{ class: className = "", ...props },
|
||||||
ref: Ref<T extends "a" ? HTMLAnchorElement : HTMLButtonElement>,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<ButtonWrapper
|
<ButtonWrapper
|
||||||
@@ -100,10 +92,10 @@ export const Button = forwardRef(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const LargeButton = forwardRef(
|
export const LargeButton = forwardRef<AllowedElements<AllowedTags> | null, ButtonProps<AllowedTags>>(
|
||||||
<T extends AllowedTags = "a">(
|
(
|
||||||
{ class: className = "", ...props }: ButtonProps<T>,
|
{ class: className = "", ...props },
|
||||||
ref: Ref<T extends "a" ? HTMLAnchorElement : HTMLButtonElement>,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<ButtonWrapper
|
<ButtonWrapper
|
||||||
@@ -120,10 +112,10 @@ type IconOnlyButtonProps<T extends AllowedTags = "a"> = Omit<
|
|||||||
"leftIcon" | "rightIcon"
|
"leftIcon" | "rightIcon"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const IconOnlyButton = forwardRef(
|
export const IconOnlyButton = forwardRef<AllowedElements<AllowedTags> | null, IconOnlyButtonProps<AllowedTags>>(
|
||||||
<T extends AllowedTags = "a">(
|
(
|
||||||
{ class: className = "", children, ...props }: IconOnlyButtonProps<T>,
|
{ class: className = "", children, ...props },
|
||||||
ref: Ref<T extends "a" ? HTMLAnchorElement : HTMLButtonElement>,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<ButtonWrapper
|
<ButtonWrapper
|
||||||
@@ -139,10 +131,10 @@ export const IconOnlyButton = forwardRef(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const LargeIconOnlyButton = forwardRef(
|
export const LargeIconOnlyButton = forwardRef<AllowedElements<AllowedTags> | null, IconOnlyButtonProps<AllowedTags>>(
|
||||||
<T extends AllowedTags = "a">(
|
(
|
||||||
{ class: className = "", children, ...props }: IconOnlyButtonProps<T>,
|
{ class: className = "", children, ...props },
|
||||||
ref: Ref<T extends "a" ? HTMLAnchorElement : HTMLButtonElement>,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<ButtonWrapper {...props} class={`iconOnly large ${className}`} ref={ref}>
|
<ButtonWrapper {...props} class={`iconOnly large ${className}`} ref={ref}>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const CollectionCard = ({
|
|||||||
className={`text-style-button-regular ${style.authorListItem}`}
|
className={`text-style-button-regular ${style.authorListItem}`}
|
||||||
>
|
>
|
||||||
<UUPicture
|
<UUPicture
|
||||||
picture={unicornProfilePicMap.find((u) => u.id === author.id)}
|
picture={unicornProfilePicMap.find((u) => u.id === author.id)!}
|
||||||
alt=""
|
alt=""
|
||||||
class={style.authorImage}
|
class={style.authorImage}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function Dialog(props: DialogProps) {
|
|||||||
if (dialogRef.current) {
|
if (dialogRef.current) {
|
||||||
if (props.open && !dialogRef.current.open) {
|
if (props.open && !dialogRef.current.open) {
|
||||||
// reset the return value when re-opening the dialog
|
// reset the return value when re-opening the dialog
|
||||||
dialogRef.current.returnValue = undefined;
|
dialogRef.current.returnValue = "";
|
||||||
dialogRef.current.showModal();
|
dialogRef.current.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export const onSoftNavClick =
|
export const onSoftNavClick =
|
||||||
(softNavigate: (href: string) => void) => (e: MouseEvent) => {
|
(softNavigate: (href: string) => void) => (e: MouseEvent) => {
|
||||||
let link = e.target as HTMLElement;
|
let link = e.target as HTMLElement | null;
|
||||||
// Could click on a child element of an anchor
|
// Could click on a child element of an anchor
|
||||||
while (link && !(link instanceof HTMLAnchorElement)) {
|
while (link && !(link instanceof HTMLAnchorElement)) {
|
||||||
link = link.parentElement;
|
link = link.parentElement;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { DOMProps } from "@react-types/shared";
|
|||||||
|
|
||||||
function PopupContents(
|
function PopupContents(
|
||||||
props: Pick<PaginationProps, "page" | "getPageHref" | "softNavigate"> & {
|
props: Pick<PaginationProps, "page" | "getPageHref" | "softNavigate"> & {
|
||||||
titleId: string;
|
titleId?: string;
|
||||||
close: () => void;
|
close: () => void;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@@ -32,6 +32,8 @@ function PopupContents(
|
|||||||
class={style.popupInner}
|
class={style.popupInner}
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!props.getPageHref) return;
|
||||||
|
|
||||||
if (props.softNavigate) {
|
if (props.softNavigate) {
|
||||||
props.softNavigate(props.getPageHref(count));
|
props.softNavigate(props.getPageHref(count));
|
||||||
props.close();
|
props.close();
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function PostCardGrid({
|
|||||||
return (
|
return (
|
||||||
<ul {...props} class={style.list} role="list" id="post-list-container">
|
<ul {...props} class={style.list} role="list" id="post-list-container">
|
||||||
{postsToDisplay.map((post, i) => {
|
{postsToDisplay.map((post, i) => {
|
||||||
const authors = post.authors.map(id => postAuthors.get(id))
|
const authors = post.authors.map(id => postAuthors.get(id)!)
|
||||||
.filter(u => !!u);
|
.filter(u => !!u);
|
||||||
|
|
||||||
return expanded && post.bannerImg ? (
|
return expanded && post.bannerImg ? (
|
||||||
|
|||||||
@@ -213,8 +213,10 @@ function ListBox(props: ListBoxProps) {
|
|||||||
|
|
||||||
// As this is inside a portal (within <Popover>), nothing from Preact's useId can be trusted
|
// As this is inside a portal (within <Popover>), nothing from Preact's useId can be trusted
|
||||||
// ...but nothing should be using these IDs anyway.
|
// ...but nothing should be using these IDs anyway.
|
||||||
listBoxProps["id"] = undefined;
|
Object.assign(listBoxProps, {
|
||||||
listBoxProps["aria-labelledby"] = undefined;
|
["id"]: undefined,
|
||||||
|
["aria-labelledby"]: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul {...listBoxProps} ref={listBoxRef} class={styles.optionsList}>
|
<ul {...listBoxProps} ref={listBoxRef} class={styles.optionsList}>
|
||||||
@@ -242,8 +244,10 @@ export function Option({ item, state }: OptionProps) {
|
|||||||
|
|
||||||
// As this is inside a portal (within <Popover>), nothing from Preact's useId can be trusted
|
// As this is inside a portal (within <Popover>), nothing from Preact's useId can be trusted
|
||||||
// ...but nothing should be using these IDs anyway.
|
// ...but nothing should be using these IDs anyway.
|
||||||
optionProps["aria-labelledby"] = undefined;
|
Object.assign(optionProps, {
|
||||||
optionProps["aria-describedby"] = undefined;
|
["aria-labelledby"]: undefined,
|
||||||
|
["aria-describedby"]: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ interface UseElementSizeProps {
|
|||||||
export const useElementSize = ({
|
export const useElementSize = ({
|
||||||
includeMargin = true,
|
includeMargin = true,
|
||||||
}: UseElementSizeProps = {}) => {
|
}: UseElementSizeProps = {}) => {
|
||||||
const [el, setEl] = useState<HTMLElement>(null);
|
const [el, setEl] = useState<HTMLElement|null>(null);
|
||||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
function getElementSize() {
|
function getElementSize() {
|
||||||
|
if (!el) return;
|
||||||
const style = window.getComputedStyle(el);
|
const style = window.getComputedStyle(el);
|
||||||
let calculatedHeight = el.offsetHeight;
|
let calculatedHeight = el.offsetHeight;
|
||||||
let calculatedWidth = el.offsetWidth;
|
let calculatedWidth = el.offsetWidth;
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ export const get = () => {
|
|||||||
.map((id) => getUnicornById(id, post.locale))
|
.map((id) => getUnicornById(id, post.locale))
|
||||||
.map((author) => {
|
.map((author) => {
|
||||||
return {
|
return {
|
||||||
name: author.name,
|
name: author!.name,
|
||||||
link: `${siteUrl}/unicorns/${author.id}`,
|
link: `${siteUrl}/unicorns/${author!.id}`,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
date: new Date(post.published),
|
date: new Date(post.published),
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const userLogins = getUnicornsByLang("en")
|
|||||||
.filter((unicorn) => !!unicorn.socials.github)
|
.filter((unicorn) => !!unicorn.socials.github)
|
||||||
.map((unicorn) => unicorn.socials.github);
|
.map((unicorn) => unicorn.socials.github);
|
||||||
|
|
||||||
const userResult: Record<string, { id: string }> = await octokit?.graphql(`
|
const userResult = (await octokit?.graphql(`
|
||||||
query {
|
query {
|
||||||
${userLogins.map(
|
${userLogins.map(
|
||||||
(login, i) => `
|
(login, i) => `
|
||||||
@@ -35,12 +35,12 @@ query {
|
|||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
}
|
}
|
||||||
`);
|
`)) as Record<string, { id: string }>;
|
||||||
|
|
||||||
const userIds: Record<string, string> = {};
|
const userIds: Record<string, string> = {};
|
||||||
if (userResult) {
|
if (userResult) {
|
||||||
userLogins.forEach((login, i) => {
|
userLogins.forEach((login, i) => {
|
||||||
userIds[login] = userResult[`user${i}`].id;
|
if (login !== undefined) userIds[login] = userResult[`user${i}`].id;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,44 +80,44 @@ export async function* getAchievements(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data?.issueCount >= 25) {
|
if (data && data.issueCount >= 25) {
|
||||||
yield {
|
yield {
|
||||||
name: "Insect infestation!",
|
name: "Insect infestation!",
|
||||||
body: `Open 25 issues in our GitHub repo`,
|
body: `Open 25 issues in our GitHub repo`,
|
||||||
};
|
};
|
||||||
} else if (data?.issueCount >= 10) {
|
} else if (data && data.issueCount >= 10) {
|
||||||
yield {
|
yield {
|
||||||
name: "Creepy crawlies!",
|
name: "Creepy crawlies!",
|
||||||
body: "Open 10 issues in our GitHub repo",
|
body: "Open 10 issues in our GitHub repo",
|
||||||
};
|
};
|
||||||
} else if (data?.issueCount > 0) {
|
} else if (data && data.issueCount > 0) {
|
||||||
yield {
|
yield {
|
||||||
name: "Bug!",
|
name: "Bug!",
|
||||||
body: "Open an issue in our GitHub repo",
|
body: "Open an issue in our GitHub repo",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data?.pullRequestCount >= 30) {
|
if (data && data.pullRequestCount >= 30) {
|
||||||
yield {
|
yield {
|
||||||
name: "Rabid Requester",
|
name: "Rabid Requester",
|
||||||
body: `Open 30 pull requests in our GitHub repo`,
|
body: `Open 30 pull requests in our GitHub repo`,
|
||||||
};
|
};
|
||||||
} else if (data?.pullRequestCount >= 10) {
|
} else if (data && data.pullRequestCount >= 10) {
|
||||||
yield {
|
yield {
|
||||||
name: "Request Rampage",
|
name: "Request Rampage",
|
||||||
body: "Open 10 pull requests in our GitHub repo",
|
body: "Open 10 pull requests in our GitHub repo",
|
||||||
};
|
};
|
||||||
} else if (data?.pullRequestCount >= 5) {
|
} else if (data && data.pullRequestCount >= 5) {
|
||||||
yield {
|
yield {
|
||||||
name: "Request Robot",
|
name: "Request Robot",
|
||||||
body: "Open 5 pull requests in our GitHub repo",
|
body: "Open 5 pull requests in our GitHub repo",
|
||||||
};
|
};
|
||||||
} else if (data?.pullRequestCount >= 3) {
|
} else if (data && data.pullRequestCount >= 3) {
|
||||||
yield {
|
yield {
|
||||||
name: "Request Racer",
|
name: "Request Racer",
|
||||||
body: "Open 3 pull requests in our GitHub repo",
|
body: "Open 3 pull requests in our GitHub repo",
|
||||||
};
|
};
|
||||||
} else if (data?.pullRequestCount > 0) {
|
} else if (data && data.pullRequestCount > 0) {
|
||||||
yield {
|
yield {
|
||||||
name: "Request Ranger",
|
name: "Request Ranger",
|
||||||
body: "Open a pull request in our GitHub repo",
|
body: "Open a pull request in our GitHub repo",
|
||||||
@@ -142,7 +142,7 @@ export async function* getAchievements(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const year of contributorYears) {
|
for (const year of contributorYears) {
|
||||||
if (data?.commitsInYear?.includes(year)) {
|
if (data && data.commitsInYear?.includes(year)) {
|
||||||
yield {
|
yield {
|
||||||
name: `${year} Contributor`,
|
name: `${year} Contributor`,
|
||||||
body: `Make a commit to the site in ${year}!`,
|
body: `Make a commit to the site in ${year}!`,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export function getUnicornById(
|
|||||||
language: Languages,
|
language: Languages,
|
||||||
): UnicornInfo | undefined {
|
): UnicornInfo | undefined {
|
||||||
const locales = unicorns.get(id);
|
const locales = unicorns.get(id);
|
||||||
|
if (!locales) return undefined;
|
||||||
return locales.find((u) => u.locale === language) || locales[0];
|
return locales.find((u) => u.locale === language) || locales[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +52,9 @@ export function getPostsByCollection(
|
|||||||
.map((locales) => locales.find((p) => p.locale === language) || locales[0])
|
.map((locales) => locales.find((p) => p.locale === language) || locales[0])
|
||||||
.filter((p) => p?.collection === collectionSlug)
|
.filter((p) => p?.collection === collectionSlug)
|
||||||
.filter((p) => !p.noindex)
|
.filter((p) => !p.noindex)
|
||||||
.sort((postA, postB) => (postA.order > postB.order ? 1 : -1));
|
.sort((postA, postB) =>
|
||||||
|
Number(postA.order) > Number(postB.order) ? 1 : -1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPostsByUnicorn(
|
export function getPostsByUnicorn(
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const tagExplainerParser = unified()
|
|||||||
|
|
||||||
for (const [key, tag] of Object.entries(tagsRaw)) {
|
for (const [key, tag] of Object.entries(tagsRaw)) {
|
||||||
let explainer = undefined;
|
let explainer = undefined;
|
||||||
let explainerType = undefined;
|
let explainerType: TagInfo["explainerType"] | undefined = undefined;
|
||||||
|
|
||||||
if ("image" in tag && tag.image.endsWith(".svg")) {
|
if ("image" in tag && tag.image.endsWith(".svg")) {
|
||||||
const license = await fs
|
const license = await fs
|
||||||
@@ -100,6 +100,9 @@ async function readUnicorn(unicornPath: string): Promise<UnicornInfo[]> {
|
|||||||
const frontmatter = matter(fileContents).data as RawUnicornInfo;
|
const frontmatter = matter(fileContents).data as RawUnicornInfo;
|
||||||
|
|
||||||
const profileImgSize = getImageSize(frontmatter.profileImg, unicornPath);
|
const profileImgSize = getImageSize(frontmatter.profileImg, unicornPath);
|
||||||
|
if (!profileImgSize) {
|
||||||
|
throw new Error(`${unicornPath}: Unable to parse profile image size`);
|
||||||
|
}
|
||||||
|
|
||||||
const unicorn: UnicornInfo = {
|
const unicorn: UnicornInfo = {
|
||||||
pronouns: "",
|
pronouns: "",
|
||||||
@@ -115,7 +118,7 @@ async function readUnicorn(unicornPath: string): Promise<UnicornInfo[]> {
|
|||||||
profileImgMeta: {
|
profileImgMeta: {
|
||||||
height: profileImgSize.height as number,
|
height: profileImgSize.height as number,
|
||||||
width: profileImgSize.width as number,
|
width: profileImgSize.width as number,
|
||||||
...resolvePath(frontmatter.profileImg, unicornPath),
|
...resolvePath(frontmatter.profileImg, unicornPath)!,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -178,14 +181,17 @@ async function readCollection(
|
|||||||
const frontmatter = matter(fileContents).data as RawCollectionInfo;
|
const frontmatter = matter(fileContents).data as RawCollectionInfo;
|
||||||
|
|
||||||
const coverImgSize = getImageSize(frontmatter.coverImg, collectionPath);
|
const coverImgSize = getImageSize(frontmatter.coverImg, collectionPath);
|
||||||
|
if (!coverImgSize) {
|
||||||
|
throw new Error(`${collectionPath}: Unable to parse cover image size`);
|
||||||
|
}
|
||||||
|
|
||||||
const coverImgMeta = {
|
const coverImgMeta = {
|
||||||
height: coverImgSize.height as number,
|
height: coverImgSize.height as number,
|
||||||
width: coverImgSize.width as number,
|
width: coverImgSize.width as number,
|
||||||
...resolvePath(frontmatter.coverImg, collectionPath),
|
...resolvePath(frontmatter.coverImg, collectionPath)!,
|
||||||
};
|
};
|
||||||
|
|
||||||
const frontmatterTags = [...frontmatter.tags].filter((tag) => {
|
const frontmatterTags = (frontmatter.tags || []).filter((tag) => {
|
||||||
if (tags.has(tag)) {
|
if (tags.has(tag)) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -267,7 +273,7 @@ async function readPost(
|
|||||||
// get an excerpt of the post markdown no longer than 150 chars
|
// get an excerpt of the post markdown no longer than 150 chars
|
||||||
const excerpt = getExcerpt(fileMatter.content, 150);
|
const excerpt = getExcerpt(fileMatter.content, 150);
|
||||||
|
|
||||||
const frontmatterTags = [...frontmatter.tags].filter((tag) => {
|
const frontmatterTags = (frontmatter.tags || []).filter((tag) => {
|
||||||
if (tags.has(tag)) {
|
if (tags.has(tag)) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
const sizeStringRegex = / ([0-9.]+)([xw])$/;
|
const sizeStringRegex = / ([0-9.]+)([xw])$/;
|
||||||
|
|
||||||
const parseSourceString = (source: string) => {
|
const parseSourceString = (source: string) => {
|
||||||
const [fullMatch, sizeStr, sizeType] = sizeStringRegex.exec(source);
|
const [fullMatch, sizeStr, sizeType] = sizeStringRegex.exec(
|
||||||
|
source,
|
||||||
|
) as string[];
|
||||||
const srcStr = source.replace(fullMatch, "");
|
const srcStr = source.replace(fullMatch, "");
|
||||||
return {
|
return {
|
||||||
src: srcStr,
|
src: srcStr,
|
||||||
|
|||||||
@@ -46,17 +46,17 @@ const getOrderRange = (arr: PostInfo[]) => {
|
|||||||
smallest: curr,
|
smallest: curr,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (curr.order! < prev.smallest.order!) {
|
if (curr.order! < prev.smallest!.order!) {
|
||||||
prev.smallest = curr;
|
prev.smallest = curr;
|
||||||
}
|
}
|
||||||
if (curr.order! > prev.largest.order!) {
|
if (curr.order! > prev.largest!.order!) {
|
||||||
prev.largest = curr;
|
prev.largest = curr;
|
||||||
}
|
}
|
||||||
return prev;
|
return prev;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
largest: null as PostInfo,
|
largest: undefined as PostInfo | undefined,
|
||||||
smallest: null as PostInfo,
|
smallest: undefined as PostInfo | undefined,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -104,8 +104,8 @@ export const getSuggestedArticles = (postNode: PostInfo) => {
|
|||||||
const { largest, smallest } = getOrderRange(suggestedArticles) || {};
|
const { largest, smallest } = getOrderRange(suggestedArticles) || {};
|
||||||
for (const suggestedPost of extraSuggestedArticles) {
|
for (const suggestedPost of extraSuggestedArticles) {
|
||||||
if (
|
if (
|
||||||
suggestedPost.order === smallest.order! - 1 ||
|
suggestedPost.order === smallest!.order! - 1 ||
|
||||||
suggestedPost.order === largest.order! + 1
|
suggestedPost.order === largest!.order! + 1
|
||||||
) {
|
) {
|
||||||
suggestedArticles.push(suggestedPost);
|
suggestedArticles.push(suggestedPost);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,11 +30,19 @@ function isNestedElement(e: MouseEvent) {
|
|||||||
if (target.getAttribute("data-dont-bind-navigate-click") !== null)
|
if (target.getAttribute("data-dont-bind-navigate-click") !== null)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
target = target.parentElement;
|
if (target.parentElement) target = target.parentElement;
|
||||||
|
else break;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
function handleHrefContainerMouseDown(e: MouseEvent): void;
|
||||||
|
function handleHrefContainerMouseUp(e: MouseEvent): void;
|
||||||
|
function handleHrefContainerAuxClick(e: MouseEvent): void;
|
||||||
|
function handleHrefContainerClick(e: MouseEvent): void;
|
||||||
|
}
|
||||||
|
|
||||||
globalThis.handleHrefContainerMouseDown = (e: MouseEvent) => {
|
globalThis.handleHrefContainerMouseDown = (e: MouseEvent) => {
|
||||||
const isMiddleClick = e.button === 1;
|
const isMiddleClick = e.button === 1;
|
||||||
if (!isMiddleClick) return;
|
if (!isMiddleClick) return;
|
||||||
@@ -46,7 +54,7 @@ globalThis.handleHrefContainerMouseDown = (e: MouseEvent) => {
|
|||||||
// implement the AuxClick event using MouseUp (only on browsers that don't support auxclick; i.e. safari)
|
// implement the AuxClick event using MouseUp (only on browsers that don't support auxclick; i.e. safari)
|
||||||
globalThis.handleHrefContainerMouseUp = (e: MouseEvent) => {
|
globalThis.handleHrefContainerMouseUp = (e: MouseEvent) => {
|
||||||
// if auxclick is supported, do nothing
|
// if auxclick is supported, do nothing
|
||||||
if ("onauxclick" in e.currentTarget) return;
|
if (e.currentTarget && "onauxclick" in e.currentTarget) return;
|
||||||
// otherwise, pass mouseup events to auxclick
|
// otherwise, pass mouseup events to auxclick
|
||||||
globalThis.handleHrefContainerAuxClick(e);
|
globalThis.handleHrefContainerAuxClick(e);
|
||||||
};
|
};
|
||||||
@@ -95,7 +103,7 @@ globalThis.handleHrefContainerClick = (e: MouseEvent) => {
|
|||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
window.location.href = href;
|
window.location.href = String(href);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getHrefContainerProps(href: string) {
|
export function getHrefContainerProps(href: string) {
|
||||||
|
|||||||
@@ -20,12 +20,12 @@
|
|||||||
*/
|
*/
|
||||||
import { toString } from "hast-util-to-string";
|
import { toString } from "hast-util-to-string";
|
||||||
import type { Child as HChild } from "hastscript";
|
import type { Child as HChild } from "hastscript";
|
||||||
import { Element } from "hast";
|
import { Data, Element, Parent, Node } from "hast";
|
||||||
import { visit } from "unist-util-visit";
|
import { visit } from "unist-util-visit";
|
||||||
import replaceAllBetween from "unist-util-replace-all-between";
|
import replaceAllBetween from "unist-util-replace-all-between";
|
||||||
import { Node } from "unist";
|
|
||||||
import JSON5 from "json5";
|
import JSON5 from "json5";
|
||||||
import { FileList, Directory, File } from "./file-list";
|
import { FileList, Directory, File } from "./file-list";
|
||||||
|
import { Root } from "postcss";
|
||||||
|
|
||||||
interface DirectoryMetadata {
|
interface DirectoryMetadata {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
@@ -34,19 +34,24 @@ interface DirectoryMetadata {
|
|||||||
interface FileMetadata {}
|
interface FileMetadata {}
|
||||||
|
|
||||||
export const rehypeFileTree = () => {
|
export const rehypeFileTree = () => {
|
||||||
return (tree) => {
|
return (tree: Root) => {
|
||||||
function replaceFiletreeNodes(nodes: Node[]) {
|
function replaceFiletreeNodes(nodes: Node[]) {
|
||||||
const items: Array<Directory | File> = [];
|
const items: Array<Directory | File> = [];
|
||||||
|
|
||||||
const isNodeElement = (node: unknown): node is Element =>
|
const isNodeElement = (node: unknown): node is Element =>
|
||||||
typeof node === "object" && node["type"] === "element";
|
(typeof node === "object" &&
|
||||||
|
node &&
|
||||||
|
"type" in node &&
|
||||||
|
node["type"] === "element") ??
|
||||||
|
false;
|
||||||
|
|
||||||
function traverseUl(listNode: Element, listItems: typeof items) {
|
function traverseUl(listNode: Element, listItems: typeof items) {
|
||||||
if (listNode.children.length === 0) return;
|
if (listNode.children.length === 0) return;
|
||||||
|
|
||||||
for (const listItem of listNode.children) {
|
for (const listItem of listNode.children) {
|
||||||
// Filter out `\n` text nodes
|
// Filter out `\n` text nodes
|
||||||
if (!(isNodeElement(listItem) && listItem.tagName === "li")) continue;
|
if (!(listItem.type === "element" && listItem.tagName === "li"))
|
||||||
|
continue;
|
||||||
|
|
||||||
// Strip nodes that only contain newlines
|
// Strip nodes that only contain newlines
|
||||||
listItem.children = listItem.children.filter(
|
listItem.children = listItem.children.filter(
|
||||||
@@ -165,13 +170,13 @@ export const rehypeFileTree = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
replaceAllBetween(
|
replaceAllBetween(
|
||||||
tree,
|
tree as never as Parent,
|
||||||
{ type: "raw", value: "<!-- filetree:start -->" } as never,
|
{ type: "raw", value: "<!-- filetree:start -->" } as never,
|
||||||
{ type: "raw", value: "<!-- filetree:end -->" } as never,
|
{ type: "raw", value: "<!-- filetree:end -->" } as never,
|
||||||
replaceFiletreeNodes,
|
replaceFiletreeNodes,
|
||||||
);
|
);
|
||||||
replaceAllBetween(
|
replaceAllBetween(
|
||||||
tree,
|
tree as never as Parent,
|
||||||
{ type: "comment", value: " filetree:start " } as never,
|
{ type: "comment", value: " filetree:start " } as never,
|
||||||
{ type: "comment", value: " filetree:end " } as never,
|
{ type: "comment", value: " filetree:end " } as never,
|
||||||
replaceFiletreeNodes,
|
replaceFiletreeNodes,
|
||||||
|
|||||||
@@ -19,15 +19,17 @@ function isNodeSummary(e: Node) {
|
|||||||
*/
|
*/
|
||||||
export const rehypeHints: Plugin<[], Root> = () => {
|
export const rehypeHints: Plugin<[], Root> = () => {
|
||||||
return (tree) => {
|
return (tree) => {
|
||||||
visit(tree, (node: Element, index, parent: Element) => {
|
visit(tree, "element", (node: Element, index, parent) => {
|
||||||
if (node.tagName !== "details") return;
|
if (node.tagName !== "details") return;
|
||||||
|
|
||||||
const summaryNode = node.children.find(isNodeSummary);
|
const summaryNode = node.children.find(isNodeSummary);
|
||||||
|
|
||||||
parent.children[index] = Hint({
|
if (index !== undefined && parent?.children) {
|
||||||
title: toString(summaryNode as never),
|
parent.children[index] = Hint({
|
||||||
children: node.children.filter((e) => !isNodeSummary(e)),
|
title: toString(summaryNode as never),
|
||||||
});
|
children: node.children.filter((e) => !isNodeSummary(e)),
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,12 +9,10 @@ export const iFrameClickToRun = () => {
|
|||||||
[...iframeButtons].forEach((el) => {
|
[...iframeButtons].forEach((el) => {
|
||||||
el.addEventListener("click", () => {
|
el.addEventListener("click", () => {
|
||||||
const iframe = document.createElement("iframe");
|
const iframe = document.createElement("iframe");
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const parent = el.parentElement!;
|
||||||
(iframe as any).loading = "lazy";
|
iframe.loading = "lazy";
|
||||||
iframe.src = el.parentElement.dataset.iframeurl;
|
iframe.src = String(parent.dataset.iframeurl);
|
||||||
const propsToPreserve = JSON.parse(
|
const propsToPreserve = JSON.parse(parent.dataset.iframeprops || "{}");
|
||||||
el.parentElement.dataset.iframeprops || "{}",
|
|
||||||
);
|
|
||||||
for (const prop in propsToPreserve) {
|
for (const prop in propsToPreserve) {
|
||||||
const val = propsToPreserve[prop];
|
const val = propsToPreserve[prop];
|
||||||
// Handle array props per hast spec:
|
// Handle array props per hast spec:
|
||||||
@@ -25,9 +23,9 @@ export const iFrameClickToRun = () => {
|
|||||||
}
|
}
|
||||||
iframe.setAttribute(prop, propsToPreserve[prop]);
|
iframe.setAttribute(prop, propsToPreserve[prop]);
|
||||||
}
|
}
|
||||||
iframe.style.width = el.parentElement.style.width;
|
iframe.style.width = parent.style.width;
|
||||||
iframe.style.height = el.parentElement.style.height;
|
iframe.style.height = parent.style.height;
|
||||||
el.parentElement.replaceWith(iframe);
|
parent.replaceWith(iframe);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Root, Element } from "hast";
|
import { Root, Element } from "hast";
|
||||||
import { Plugin } from "unified";
|
|
||||||
|
|
||||||
import { visit } from "unist-util-visit";
|
import { visit } from "unist-util-visit";
|
||||||
|
|
||||||
@@ -13,8 +12,6 @@ import type { GetPictureResult } from "@astrojs/image/dist/lib/get-picture";
|
|||||||
import probe from "probe-image-size";
|
import probe from "probe-image-size";
|
||||||
import { IFramePlaceholder } from "./iframe-placeholder";
|
import { IFramePlaceholder } from "./iframe-placeholder";
|
||||||
|
|
||||||
interface RehypeUnicornIFrameClickToRunProps {}
|
|
||||||
|
|
||||||
// default icon, used if a frame's favicon cannot be resolved
|
// default icon, used if a frame's favicon cannot be resolved
|
||||||
let defaultPageIcon: Promise<GetPictureResult>;
|
let defaultPageIcon: Promise<GetPictureResult>;
|
||||||
function fetchDefaultPageIcon(): Promise<GetPictureResult> {
|
function fetchDefaultPageIcon(): Promise<GetPictureResult> {
|
||||||
@@ -34,20 +31,21 @@ function fetchDefaultPageIcon(): Promise<GetPictureResult> {
|
|||||||
// and multiple fetchPageInfo() calls can await the same icon
|
// and multiple fetchPageInfo() calls can await the same icon
|
||||||
const pageIconMap = new Map<string, Promise<GetPictureResult>>();
|
const pageIconMap = new Map<string, Promise<GetPictureResult>>();
|
||||||
function fetchPageIcon(src: URL, srcHast: Root): Promise<GetPictureResult> {
|
function fetchPageIcon(src: URL, srcHast: Root): Promise<GetPictureResult> {
|
||||||
if (pageIconMap.has(src.origin)) return pageIconMap.get(src.origin);
|
if (pageIconMap.has(src.origin)) return pageIconMap.get(src.origin)!;
|
||||||
|
|
||||||
const promise = (async () => {
|
const promise = (async () => {
|
||||||
// <link rel="manifest" href="/manifest.json">
|
// <link rel="manifest" href="/manifest.json">
|
||||||
const manifestPath: Element = find(
|
const manifestPath: Element | undefined = find(
|
||||||
srcHast,
|
srcHast,
|
||||||
(node: unknown) => (node as Element)?.properties?.rel?.[0] === "manifest",
|
(node: unknown) =>
|
||||||
|
(node as Element)?.properties?.rel?.toString() === "manifest",
|
||||||
);
|
);
|
||||||
|
|
||||||
let iconLink: string;
|
let iconLink: string | undefined;
|
||||||
|
|
||||||
if (manifestPath) {
|
if (manifestPath?.properties?.href) {
|
||||||
// `/manifest.json`
|
// `/manifest.json`
|
||||||
const manifestRelativeURL = manifestPath.properties.href.toString();
|
const manifestRelativeURL = String(manifestPath.properties.href);
|
||||||
const fullManifestURL = new URL(manifestRelativeURL, src).href;
|
const fullManifestURL = new URL(manifestRelativeURL, src).href;
|
||||||
|
|
||||||
const manifest = await fetch(fullManifestURL)
|
const manifest = await fetch(fullManifestURL)
|
||||||
@@ -56,20 +54,22 @@ function fetchPageIcon(src: URL, srcHast: Root): Promise<GetPictureResult> {
|
|||||||
|
|
||||||
if (manifest) {
|
if (manifest) {
|
||||||
const largestIcon = getLargestManifestIcon(manifest);
|
const largestIcon = getLargestManifestIcon(manifest);
|
||||||
iconLink = new URL(largestIcon.icon.src, src.origin).href;
|
if (largestIcon?.icon)
|
||||||
|
iconLink = new URL(largestIcon.icon.src, src.origin).href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!iconLink) {
|
if (!iconLink) {
|
||||||
// fetch `favicon.ico`
|
// fetch `favicon.ico`
|
||||||
// <link rel="shortcut icon" type="image/png" href="https://example.com/img.png">
|
// <link rel="shortcut icon" type="image/png" href="https://example.com/img.png">
|
||||||
const favicon: Element = find(
|
const favicon: Element | undefined = find(
|
||||||
srcHast,
|
srcHast,
|
||||||
(node: unknown) =>
|
(node: unknown) =>
|
||||||
(node as Element)?.properties?.rel?.toString()?.includes("icon"),
|
(node as Element)?.properties?.rel?.toString()?.includes("icon") ??
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (favicon) {
|
if (favicon?.properties?.href) {
|
||||||
iconLink = new URL(favicon.properties.href.toString(), src).href;
|
iconLink = new URL(favicon.properties.href.toString(), src).href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,11 +96,11 @@ function fetchPageIcon(src: URL, srcHast: Root): Promise<GetPictureResult> {
|
|||||||
|
|
||||||
const pageHtmlMap = new Map<string, Promise<Root | null>>();
|
const pageHtmlMap = new Map<string, Promise<Root | null>>();
|
||||||
function fetchPageHtml(src: string): Promise<Root | null> {
|
function fetchPageHtml(src: string): Promise<Root | null> {
|
||||||
if (pageHtmlMap.has(src)) return pageHtmlMap.get(src);
|
if (pageHtmlMap.has(src)) return pageHtmlMap.get(src)!;
|
||||||
|
|
||||||
const promise = (async () => {
|
const promise = (async () => {
|
||||||
const srcHTML = await fetch(src)
|
const srcHTML = await fetch(src)
|
||||||
.then((r) => r.status === 200 && r.text())
|
.then((r) => (r.status === 200 ? r.text() : undefined))
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
|
|
||||||
// if fetch fails...
|
// if fetch fails...
|
||||||
@@ -129,7 +129,7 @@ async function fetchPageInfo(src: string): Promise<PageInfo | null> {
|
|||||||
if (!srcHast) return null;
|
if (!srcHast) return null;
|
||||||
|
|
||||||
// find <title> element in response HTML
|
// find <title> element in response HTML
|
||||||
const titleEl: Element = find(srcHast, { tagName: "title" });
|
const titleEl = find<Element>(srcHast, { tagName: "title" });
|
||||||
const titleContentEl = titleEl && titleEl.children[0];
|
const titleContentEl = titleEl && titleEl.children[0];
|
||||||
const title =
|
const title =
|
||||||
titleContentEl?.type === "text" ? titleContentEl.value : undefined;
|
titleContentEl?.type === "text" ? titleContentEl.value : undefined;
|
||||||
@@ -140,13 +140,10 @@ async function fetchPageInfo(src: string): Promise<PageInfo | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add switch/case and dedicated files ala "Components"
|
// TODO: Add switch/case and dedicated files ala "Components"
|
||||||
export const rehypeUnicornIFrameClickToRun: Plugin<
|
export const rehypeUnicornIFrameClickToRun = () => {
|
||||||
[RehypeUnicornIFrameClickToRunProps | never],
|
return async (tree: Root) => {
|
||||||
Root
|
|
||||||
> = () => {
|
|
||||||
return async (tree) => {
|
|
||||||
const iframeNodes: Element[] = [];
|
const iframeNodes: Element[] = [];
|
||||||
visit(tree, (node: Element) => {
|
visit(tree, "element", (node: Element) => {
|
||||||
if (node.tagName === "iframe") {
|
if (node.tagName === "iframe") {
|
||||||
iframeNodes.push(node);
|
iframeNodes.push(node);
|
||||||
}
|
}
|
||||||
@@ -168,7 +165,7 @@ export const rehypeUnicornIFrameClickToRun: Plugin<
|
|||||||
width = width ?? EMBED_SIZE.w;
|
width = width ?? EMBED_SIZE.w;
|
||||||
height = height ?? EMBED_SIZE.h;
|
height = height ?? EMBED_SIZE.h;
|
||||||
const info: PageInfo = (await fetchPageInfo(
|
const info: PageInfo = (await fetchPageInfo(
|
||||||
iframeNode.properties.src.toString(),
|
String(iframeNode.properties.src),
|
||||||
).catch(() => null)) || { icon: await fetchDefaultPageIcon() };
|
).catch(() => null)) || { icon: await fetchDefaultPageIcon() };
|
||||||
|
|
||||||
const [, heightPx] = /^([0-9]+)(px)?$/.exec(height + "") || [];
|
const [, heightPx] = /^([0-9]+)(px)?$/.exec(height + "") || [];
|
||||||
@@ -177,7 +174,7 @@ export const rehypeUnicornIFrameClickToRun: Plugin<
|
|||||||
const iframeReplacement = IFramePlaceholder({
|
const iframeReplacement = IFramePlaceholder({
|
||||||
width: width.toString(),
|
width: width.toString(),
|
||||||
height: height.toString(),
|
height: height.toString(),
|
||||||
src: src.toString(),
|
src: String(src),
|
||||||
pageTitle: String(dataFrameTitle ?? "") || info.title || "",
|
pageTitle: String(dataFrameTitle ?? "") || info.title || "",
|
||||||
pageIcon: info.icon,
|
pageIcon: info.icon,
|
||||||
propsToPreserve: JSON.stringify(propsToPreserve),
|
propsToPreserve: JSON.stringify(propsToPreserve),
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Plugin } from "unified";
|
|
||||||
import rehypeSlug from "rehype-slug-custom-id";
|
import rehypeSlug from "rehype-slug-custom-id";
|
||||||
import rehypeRaw from "rehype-raw";
|
import rehypeRaw from "rehype-raw";
|
||||||
import { rehypeTabs } from "./tabs/rehype-transform";
|
import { rehypeTabs } from "./tabs/rehype-transform";
|
||||||
@@ -17,11 +16,17 @@ import { rehypeHeaderText } from "./rehype-header-text";
|
|||||||
import { rehypeHeaderClass } from "./rehype-header-class";
|
import { rehypeHeaderClass } from "./rehype-header-class";
|
||||||
import { rehypeFileTree } from "./file-tree/rehype-file-tree";
|
import { rehypeFileTree } from "./file-tree/rehype-file-tree";
|
||||||
import { rehypeTwoslashTabindex } from "./twoslash-tabindex/rehype-transform";
|
import { rehypeTwoslashTabindex } from "./twoslash-tabindex/rehype-transform";
|
||||||
|
import { Plugin } from "unified";
|
||||||
|
|
||||||
|
// TODO: this does not actually validate that the adjacent config matches the type of the plugin in an array structure
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type RehypePlugin = Plugin<any[]> | [Plugin<any[]>, any];
|
type RehypePlugin<T = any> =
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
| (T | ((config: T) => any))[]
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
| (() => any);
|
||||||
|
|
||||||
export function createRehypePlugins(config: MarkdownConfig): RehypePlugin[] {
|
export function createRehypePlugins(config: MarkdownConfig): Plugin[] {
|
||||||
return [
|
return [
|
||||||
// This is required to handle unsafe HTML embedded into Markdown
|
// This is required to handle unsafe HTML embedded into Markdown
|
||||||
[rehypeRaw, { passThrough: [`mdxjsEsm`] }],
|
[rehypeRaw, { passThrough: [`mdxjsEsm`] }],
|
||||||
@@ -29,7 +34,7 @@ export function createRehypePlugins(config: MarkdownConfig): RehypePlugin[] {
|
|||||||
...(config.format === "epub"
|
...(config.format === "epub"
|
||||||
? [
|
? [
|
||||||
rehypeFixTwoSlashXHTML,
|
rehypeFixTwoSlashXHTML,
|
||||||
[rehypeMakeImagePathsAbsolute, { path: config.path }] as RehypePlugin,
|
[rehypeMakeImagePathsAbsolute, { path: config.path }],
|
||||||
rehypeMakeHrefPathsAbsolute,
|
rehypeMakeHrefPathsAbsolute,
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
@@ -70,8 +75,8 @@ export function createRehypePlugins(config: MarkdownConfig): RehypePlugin[] {
|
|||||||
className: (depth: number) =>
|
className: (depth: number) =>
|
||||||
`text-style-headline-${Math.min(depth + 1, 6)}`,
|
`text-style-headline-${Math.min(depth + 1, 6)}`,
|
||||||
},
|
},
|
||||||
] as RehypePlugin,
|
],
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
];
|
] as RehypePlugin[] as Plugin[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,9 @@ import path from "path";
|
|||||||
*/
|
*/
|
||||||
import { getPicture } from "./get-picture-hack";
|
import { getPicture } from "./get-picture-hack";
|
||||||
import { getImageSize } from "../get-image-size";
|
import { getImageSize } from "../get-image-size";
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
import { resolvePath } from "../url-paths";
|
import { resolvePath } from "../url-paths";
|
||||||
import { getLargestSourceSetSrc } from "../get-largest-source-set-src";
|
import { getLargestSourceSetSrc } from "../get-largest-source-set-src";
|
||||||
|
import { ISizeCalculationResult } from "image-size/dist/types/interface";
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
||||||
|
|
||||||
const MAX_WIDTH = 768;
|
const MAX_WIDTH = 768;
|
||||||
const MAX_HEIGHT = 768;
|
const MAX_HEIGHT = 768;
|
||||||
@@ -33,7 +31,7 @@ function getPixelValue(attr: unknown): number | undefined {
|
|||||||
export const rehypeAstroImageMd: Plugin<[], Root> = () => {
|
export const rehypeAstroImageMd: Plugin<[], Root> = () => {
|
||||||
return async (tree, file) => {
|
return async (tree, file) => {
|
||||||
const imgNodes: Element[] = [];
|
const imgNodes: Element[] = [];
|
||||||
visit(tree, (node: Element) => {
|
visit(tree, "element", (node: Element) => {
|
||||||
if (node.tagName === "img") {
|
if (node.tagName === "img") {
|
||||||
imgNodes.push(node);
|
imgNodes.push(node);
|
||||||
}
|
}
|
||||||
@@ -72,7 +70,7 @@ export const rehypeAstroImageMd: Plugin<[], Root> = () => {
|
|||||||
const nodeWidth = getPixelValue(node.properties.width);
|
const nodeWidth = getPixelValue(node.properties.width);
|
||||||
const nodeHeight = getPixelValue(node.properties.height);
|
const nodeHeight = getPixelValue(node.properties.height);
|
||||||
|
|
||||||
const dimensions = { ...srcSize };
|
const dimensions = { ...srcSize } as { width: number; height: number };
|
||||||
if (nodeHeight) {
|
if (nodeHeight) {
|
||||||
dimensions.height = nodeHeight;
|
dimensions.height = nodeHeight;
|
||||||
dimensions.width = Math.floor(nodeHeight * imageRatio);
|
dimensions.width = Math.floor(nodeHeight * imageRatio);
|
||||||
@@ -119,7 +117,7 @@ export const rehypeAstroImageMd: Plugin<[], Root> = () => {
|
|||||||
alt: nodeAlt || "",
|
alt: nodeAlt || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
pngSource = originalPictureResult.sources.reduce(
|
const newPngSource = originalPictureResult.sources.reduce(
|
||||||
(prev, source) => {
|
(prev, source) => {
|
||||||
const largestSrc = getLargestSourceSetSrc(source.srcset);
|
const largestSrc = getLargestSourceSetSrc(source.srcset);
|
||||||
// select first option
|
// select first option
|
||||||
@@ -146,8 +144,10 @@ export const rehypeAstroImageMd: Plugin<[], Root> = () => {
|
|||||||
}
|
}
|
||||||
return prev;
|
return prev;
|
||||||
},
|
},
|
||||||
null as ReturnType<typeof getLargestSourceSetSrc>,
|
undefined as ReturnType<typeof getLargestSourceSetSrc> | undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (newPngSource) pngSource = newPngSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sources = pictureResult.sources.map((attrs) => {
|
const sources = pictureResult.sources.map((attrs) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { hasProperty } from "hast-util-has-property";
|
|||||||
import { toString } from "hast-util-to-string";
|
import { toString } from "hast-util-to-string";
|
||||||
import { Root, Parent } from "hast";
|
import { Root, Parent } from "hast";
|
||||||
import { visit } from "unist-util-visit";
|
import { visit } from "unist-util-visit";
|
||||||
|
import { AstroVFile } from "./types";
|
||||||
|
|
||||||
interface RehypeHeaderClassOpts {
|
interface RehypeHeaderClassOpts {
|
||||||
depth: number;
|
depth: number;
|
||||||
@@ -14,19 +15,22 @@ interface RehypeHeaderClassOpts {
|
|||||||
* at the intended visual level.
|
* at the intended visual level.
|
||||||
*/
|
*/
|
||||||
export const rehypeHeaderClass = (opts: RehypeHeaderClassOpts) => {
|
export const rehypeHeaderClass = (opts: RehypeHeaderClassOpts) => {
|
||||||
return (tree: Root, file) => {
|
return (tree: Root, file: AstroVFile) => {
|
||||||
// hacky (temporary) fix to exclude the site/about-us*.mdx files, since
|
// hacky (temporary) fix to exclude the site/about-us*.mdx files, since
|
||||||
// those start at a different heading level
|
// those start at a different heading level
|
||||||
if (file.data.astro.frontmatter.slug === "site") return;
|
if (file.data.astro.frontmatter.slug === "site") return;
|
||||||
|
|
||||||
// Find the minimum heading rank in the file
|
// Find the minimum heading rank in the file
|
||||||
// (e.g. if it starts at h2, minDepth = 2)
|
// (e.g. if it starts at h2, minDepth = 2)
|
||||||
let minDepth: number;
|
let minDepth: number | undefined;
|
||||||
visit(tree, "element", (node: Parent["children"][number]) => {
|
visit(tree, "element", (node: Parent["children"][number]) => {
|
||||||
const nodeHeadingRank = headingRank(node);
|
const nodeHeadingRank = headingRank(node);
|
||||||
if (!minDepth || nodeHeadingRank < minDepth) minDepth = nodeHeadingRank;
|
if (
|
||||||
|
!minDepth ||
|
||||||
|
(nodeHeadingRank !== undefined && nodeHeadingRank < minDepth)
|
||||||
|
)
|
||||||
|
minDepth = nodeHeadingRank;
|
||||||
});
|
});
|
||||||
minDepth ||= 1;
|
|
||||||
|
|
||||||
visit(tree, "element", (node: Parent["children"][number]) => {
|
visit(tree, "element", (node: Parent["children"][number]) => {
|
||||||
const nodeHeadingRank = headingRank(node);
|
const nodeHeadingRank = headingRank(node);
|
||||||
@@ -42,7 +46,7 @@ export const rehypeHeaderClass = (opts: RehypeHeaderClassOpts) => {
|
|||||||
// - when (minDepth = 5, depth = 2) h5 + 2 - 4 -> h3
|
// - when (minDepth = 5, depth = 2) h5 + 2 - 4 -> h3
|
||||||
// - when (minDepth = 1, depth = 2) h1 + 2 + 0 -> h3
|
// - when (minDepth = 1, depth = 2) h1 + 2 + 0 -> h3
|
||||||
const tagHeadingRank = Math.min(
|
const tagHeadingRank = Math.min(
|
||||||
nodeHeadingRank + opts.depth + (1 - minDepth),
|
nodeHeadingRank + opts.depth + (1 - (minDepth ?? 1)),
|
||||||
6,
|
6,
|
||||||
);
|
);
|
||||||
const className = opts.className(nodeHeadingRank);
|
const className = opts.className(nodeHeadingRank);
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import { toString } from "hast-util-to-string";
|
|||||||
import { Root, Parent } from "hast";
|
import { Root, Parent } from "hast";
|
||||||
import { visit } from "unist-util-visit";
|
import { visit } from "unist-util-visit";
|
||||||
import { PostHeadingInfo } from "src/types/index";
|
import { PostHeadingInfo } from "src/types/index";
|
||||||
|
import { AstroVFile } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin to add `data-header-text`s to headings.
|
* Plugin to add `data-header-text`s to headings.
|
||||||
*/
|
*/
|
||||||
export const rehypeHeaderText = () => {
|
export const rehypeHeaderText = () => {
|
||||||
return (tree: Root, file) => {
|
return (tree: Root, file: AstroVFile) => {
|
||||||
const headingsWithId: PostHeadingInfo[] =
|
const headingsWithId: PostHeadingInfo[] =
|
||||||
(file.data.astro.frontmatter.headingsWithId = []);
|
(file.data.astro.frontmatter.headingsWithId = []);
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Root, Element } from "hast";
|
import { Root, Element } from "hast";
|
||||||
import { Plugin } from "unified";
|
|
||||||
import { visit } from "unist-util-visit";
|
import { visit } from "unist-util-visit";
|
||||||
import { urlPathRegex, resolvePath } from "../url-paths";
|
import { urlPathRegex, resolvePath } from "../url-paths";
|
||||||
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { AstroVFile } from "./types";
|
||||||
|
|
||||||
// TODO: Add switch/case and dedicated files ala "Components"
|
// TODO: Add switch/case and dedicated files ala "Components"
|
||||||
export const rehypeUnicornElementMap: Plugin<[], Root> = () => {
|
export const rehypeUnicornElementMap = () => {
|
||||||
return async (tree, file) => {
|
return async (tree: Root, file: AstroVFile) => {
|
||||||
visit(tree, (node: Element) => {
|
visit(tree, "element", (node: Element) => {
|
||||||
if (node.tagName === "video") {
|
if (node.tagName === "video") {
|
||||||
node.properties.muted ??= true;
|
node.properties.muted ??= true;
|
||||||
node.properties.autoPlay ??= true;
|
node.properties.autoPlay ??= true;
|
||||||
@@ -19,7 +19,7 @@ export const rehypeUnicornElementMap: Plugin<[], Root> = () => {
|
|||||||
|
|
||||||
if (file.path) {
|
if (file.path) {
|
||||||
const resolvedPath = resolvePath(
|
const resolvedPath = resolvePath(
|
||||||
node.properties.src.toString(),
|
String(node.properties.src),
|
||||||
path.dirname(file.path),
|
path.dirname(file.path),
|
||||||
);
|
);
|
||||||
if (resolvedPath)
|
if (resolvedPath)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const TwitchTransformer = {
|
|||||||
},
|
},
|
||||||
getHTML(url: string) {
|
getHTML(url: string) {
|
||||||
const srcUrl = new URL(url);
|
const srcUrl = new URL(url);
|
||||||
let embedUrl: URL;
|
let embedUrl: URL | undefined = undefined;
|
||||||
|
|
||||||
if (srcUrl.host === "clips.twitch.tv") {
|
if (srcUrl.host === "clips.twitch.tv") {
|
||||||
const clipId =
|
const clipId =
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Root } from "hast";
|
import { Root } from "hast";
|
||||||
import replaceAllBetween from "unist-util-replace-all-between";
|
import replaceAllBetween from "unist-util-replace-all-between";
|
||||||
import { Plugin } from "unified";
|
|
||||||
import { getHeaderNodeId, slugs } from "rehype-slug-custom-id";
|
import { getHeaderNodeId, slugs } from "rehype-slug-custom-id";
|
||||||
import { Element, Node, Parent, Text } from "hast";
|
import { Element, Node, Parent, Text } from "hast";
|
||||||
import { TabInfo, Tabs } from "./tabs";
|
import { TabInfo, Tabs } from "./tabs";
|
||||||
@@ -77,8 +76,8 @@ const getApproxLineCount = (nodes: Node[], inParagraph?: boolean): number => {
|
|||||||
* To align with React Tabs package:
|
* To align with React Tabs package:
|
||||||
* @see https://github.com/reactjs/react-tabs
|
* @see https://github.com/reactjs/react-tabs
|
||||||
*/
|
*/
|
||||||
export const rehypeTabs: Plugin<[], Root> = () => {
|
export const rehypeTabs = () => {
|
||||||
return (tree) => {
|
return (tree: Root) => {
|
||||||
const replaceTabNodes = (nodes: Node[]) => {
|
const replaceTabNodes = (nodes: Node[]) => {
|
||||||
let sectionStarted = false;
|
let sectionStarted = false;
|
||||||
const largestSize = findLargestHeading(nodes as Element[]);
|
const largestSize = findLargestHeading(nodes as Element[]);
|
||||||
@@ -109,18 +108,18 @@ export const rehypeTabs: Plugin<[], Root> = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For any other heading found in the tab contents, append to the nested headers array
|
// For any other heading found in the tab contents, append to the nested headers array
|
||||||
if (isNodeHeading(localNode)) {
|
if (isNodeHeading(localNode) && tabs.length) {
|
||||||
const lastTab = tabs.at(-1);
|
const lastTab = tabs.at(-1);
|
||||||
|
|
||||||
// Store the related tab ID in the attributes of the header
|
// Store the related tab ID in the attributes of the header
|
||||||
localNode.properties["data-tabname"] = lastTab.slug;
|
localNode.properties["data-tabname"] = lastTab?.slug;
|
||||||
|
|
||||||
// Add header ID to array
|
// Add header ID to array
|
||||||
tabs.at(-1).headers.push(localNode.properties.id.toString());
|
tabs.at(-1)?.headers?.push(String(localNode.properties.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, append the node as tab content
|
// Otherwise, append the node as tab content
|
||||||
tabs.at(-1).contents.push(localNode);
|
tabs.at(-1)?.contents?.push(localNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if the set of tabs should use a constant height (via the "tabs-small" class)
|
// Determine if the set of tabs should use a constant height (via the "tabs-small" class)
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ export const enableTabs = () => {
|
|||||||
function handleClick(e: Event) {
|
function handleClick(e: Event) {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
const tabName = target.dataset.tabname;
|
const tabName = target.dataset.tabname;
|
||||||
|
if (!tabName) return;
|
||||||
|
|
||||||
changeTabs(tabName);
|
changeTabs(tabName);
|
||||||
|
|
||||||
if (shouldScrollToTab) {
|
if (shouldScrollToTab) {
|
||||||
@@ -76,32 +78,34 @@ export const enableTabs = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Iterate through all tabs to populate tabEntries & set listeners
|
// Iterate through all tabs to populate tabEntries & set listeners
|
||||||
document.querySelectorAll('[role="tablist"]').forEach((tabList) => {
|
document
|
||||||
const entry: TabEntry = new Map();
|
.querySelectorAll<HTMLElement>('[role="tablist"]')
|
||||||
const parent = tabList.parentElement;
|
.forEach((tabList) => {
|
||||||
|
const entry: TabEntry = new Map();
|
||||||
|
const parent = tabList.parentElement!;
|
||||||
|
|
||||||
const tabs: NodeListOf<HTMLElement> =
|
const tabs: NodeListOf<HTMLElement> =
|
||||||
tabList.querySelectorAll('[role="tab"]');
|
tabList.querySelectorAll('[role="tab"]');
|
||||||
|
|
||||||
tabs.forEach((tab) => {
|
tabs.forEach((tab) => {
|
||||||
const panel = parent.querySelector<HTMLElement>(
|
const panel = parent.querySelector<HTMLElement>(
|
||||||
`#${tab.getAttribute("aria-controls")}`,
|
`#${tab.getAttribute("aria-controls")}`,
|
||||||
);
|
)!;
|
||||||
entry.set(tab.dataset.tabname, {
|
entry.set(tab.dataset.tabname!, {
|
||||||
tab,
|
tab,
|
||||||
panel,
|
panel,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a click event handler to each tab
|
||||||
|
tab.addEventListener("click", handleClick);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add a click event handler to each tab
|
// Enable arrow navigation between tabs in the tab list
|
||||||
tab.addEventListener("click", handleClick);
|
tabList.addEventListener("keydown", handleKeydown);
|
||||||
|
|
||||||
|
tabEntries.push(entry);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enable arrow navigation between tabs in the tab list
|
|
||||||
tabList.addEventListener("keydown", handleKeydown);
|
|
||||||
|
|
||||||
tabEntries.push(entry);
|
|
||||||
});
|
|
||||||
|
|
||||||
function changeTabs(tabName: string) {
|
function changeTabs(tabName: string) {
|
||||||
// find all tabs on the page that match the selected tabname
|
// find all tabs on the page that match the selected tabname
|
||||||
for (const tabEntry of tabEntries) {
|
for (const tabEntry of tabEntries) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { toString } from "hast-util-to-string";
|
|||||||
*/
|
*/
|
||||||
export const rehypeTooltips: Plugin<[], Root> = () => {
|
export const rehypeTooltips: Plugin<[], Root> = () => {
|
||||||
return (tree) => {
|
return (tree) => {
|
||||||
visit(tree, (node: Element, index, parent: Element) => {
|
visit(tree, "element", (node: Element, index, parent) => {
|
||||||
if (node.tagName !== "blockquote") return;
|
if (node.tagName !== "blockquote") return;
|
||||||
|
|
||||||
const firstParagraph = node.children.find((e) => e.type === "element");
|
const firstParagraph = node.children.find((e) => e.type === "element");
|
||||||
@@ -42,11 +42,13 @@ export const rehypeTooltips: Plugin<[], Root> = () => {
|
|||||||
// remove `firstText` from children nodes
|
// remove `firstText` from children nodes
|
||||||
firstParagraph.children.splice(0, 1);
|
firstParagraph.children.splice(0, 1);
|
||||||
|
|
||||||
parent.children[index] = Tooltip({
|
if (parent?.children && index !== undefined) {
|
||||||
icon: firstText.tagName === "em" ? "warning" : "info",
|
parent.children[index] = Tooltip({
|
||||||
title: toString(firstText as never).replace(/:$/, ""),
|
icon: firstText.tagName === "em" ? "warning" : "info",
|
||||||
children: node.children,
|
title: toString(firstText as never).replace(/:$/, ""),
|
||||||
});
|
children: node.children,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { visit } from "unist-util-visit";
|
|||||||
|
|
||||||
export const rehypeTwoslashTabindex: Plugin<[], Root> = () => {
|
export const rehypeTwoslashTabindex: Plugin<[], Root> = () => {
|
||||||
return async (tree, _) => {
|
return async (tree, _) => {
|
||||||
visit(tree, (node: Element) => {
|
visit(tree, "element", (node: Element) => {
|
||||||
if (
|
if (
|
||||||
node.tagName === "div" &&
|
node.tagName === "div" &&
|
||||||
node.properties.className instanceof Array &&
|
node.properties.className instanceof Array &&
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { languages } from "../constants/index";
|
|||||||
import { basename } from "path";
|
import { basename } from "path";
|
||||||
import { MDXInstance, MarkdownInstance } from "astro";
|
import { MDXInstance, MarkdownInstance } from "astro";
|
||||||
|
|
||||||
function isLanguageKey(str: string): str is Languages {
|
function isLanguageKey(str: string | undefined): str is Languages {
|
||||||
return Object.keys(languages).includes(str);
|
return str !== undefined && Object.keys(languages).includes(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -116,7 +116,7 @@ export function getTranslatedPage<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetch translation files from /data/i18n
|
// fetch translation files from /data/i18n
|
||||||
let i18nFiles: Record<string, unknown>;
|
let i18nFiles: Record<string, { default: Record<string, string> }>;
|
||||||
try {
|
try {
|
||||||
i18nFiles = import.meta.glob("../../content/data/i18n/*.json", {
|
i18nFiles = import.meta.glob("../../content/data/i18n/*.json", {
|
||||||
eager: true,
|
eager: true,
|
||||||
@@ -127,12 +127,10 @@ try {
|
|||||||
|
|
||||||
const i18n: Partial<Record<Languages, Map<string, string>>> =
|
const i18n: Partial<Record<Languages, Map<string, string>>> =
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
Object.entries(i18nFiles).map(
|
Object.entries(i18nFiles).map(([file, content]) => [
|
||||||
([file, content]: [string, { default: Record<string, string> }]) => [
|
basename(file).split(".")[0],
|
||||||
basename(file).split(".")[0],
|
new Map(Object.entries(content.default)),
|
||||||
new Map(Object.entries(content.default)),
|
]),
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// warn about any values that do not have full translations
|
// warn about any values that do not have full translations
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import mastodon from "src/icons/mastodon.svg?raw";
|
|||||||
import facebook from "src/icons/facebook.svg?raw";
|
import facebook from "src/icons/facebook.svg?raw";
|
||||||
import rss from "src/icons/rss.svg?raw";
|
import rss from "src/icons/rss.svg?raw";
|
||||||
|
|
||||||
const icons = { discord, linkedin, twitter, mastodon, facebook, rss };
|
const icons: Record<string, string> = { discord, linkedin, twitter, mastodon, facebook, rss };
|
||||||
|
|
||||||
// Components used in the .MDX about files
|
// Components used in the .MDX about files
|
||||||
// - see /content/site/about-us*.mdx for usages
|
// - see /content/site/about-us*.mdx for usages
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ export const themeToggle = () => {
|
|||||||
const lightIconEls = document.querySelectorAll<HTMLElement>(
|
const lightIconEls = document.querySelectorAll<HTMLElement>(
|
||||||
"[data-theme-toggle-icon='light']",
|
"[data-theme-toggle-icon='light']",
|
||||||
);
|
);
|
||||||
function toggleButton(theme) {
|
function toggleButton(theme: string) {
|
||||||
themeToggleBtns.forEach((el) => (el.ariaPressed = `${theme === "dark"}`));
|
themeToggleBtns.forEach((el) => (el.ariaPressed = `${theme === "dark"}`));
|
||||||
lightIconEls.forEach((el) => {
|
lightIconEls.forEach((el) => {
|
||||||
el.style.display = theme === "light" ? null : "none";
|
el.style.display = theme === "light" ? "" : "none";
|
||||||
});
|
});
|
||||||
darkIconEls.forEach((el) => {
|
darkIconEls.forEach((el) => {
|
||||||
el.style.display = theme === "light" ? "none" : null;
|
el.style.display = theme === "light" ? "none" : "";
|
||||||
});
|
});
|
||||||
|
|
||||||
// update the meta theme-color attribute(s) based on the user preference
|
// update the meta theme-color attribute(s) based on the user preference
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export function getShortTitle(
|
|||||||
post: PostInfo,
|
post: PostInfo,
|
||||||
collection?: CollectionInfo,
|
collection?: CollectionInfo,
|
||||||
): string {
|
): string {
|
||||||
const collectionTitle = collection?.title || post.collection;
|
const collectionTitle = collection?.title || post.collection || "";
|
||||||
// if the post title starts with its collection title, remove it
|
// if the post title starts with its collection title, remove it
|
||||||
if (post.title.startsWith(`${collectionTitle}: `))
|
if (post.title.startsWith(`${collectionTitle}: `))
|
||||||
return post.title.substring(collectionTitle.length + 2);
|
return post.title.substring(collectionTitle.length + 2);
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-nocheck TODO
|
||||||
|
|
||||||
import { JSX } from "preact";
|
import { JSX } from "preact";
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-nocheck TODO
|
||||||
|
|
||||||
function throttle(callback, limit) {
|
function throttle(callback, limit) {
|
||||||
let waiting = false;
|
let waiting = false;
|
||||||
return function (...props) {
|
return function (...props) {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-nocheck TODO
|
||||||
|
|
||||||
// https://github.com/wuct/raf-throttle/blob/master/rafThrottle.js
|
// https://github.com/wuct/raf-throttle/blob/master/rafThrottle.js
|
||||||
const rafThrottle = (callback) => {
|
const rafThrottle = (callback) => {
|
||||||
let requestId = null;
|
let requestId = null;
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const FilterDialogMobile = ({
|
|||||||
count={author.numPosts}
|
count={author.numPosts}
|
||||||
icon={
|
icon={
|
||||||
<UUPicture
|
<UUPicture
|
||||||
picture={unicornProfilePicMap.find((u) => u.id === author.id)}
|
picture={unicornProfilePicMap.find((u) => u.id === author.id)!}
|
||||||
alt={""}
|
alt={""}
|
||||||
class={styles.authorIcon}
|
class={styles.authorIcon}
|
||||||
/>
|
/>
|
||||||
@@ -198,7 +198,7 @@ const FilterDialogSmallTablet = ({
|
|||||||
<UUPicture
|
<UUPicture
|
||||||
picture={unicornProfilePicMap.find(
|
picture={unicornProfilePicMap.find(
|
||||||
(u) => u.id === author.id,
|
(u) => u.id === author.id,
|
||||||
)}
|
)!}
|
||||||
alt={""}
|
alt={""}
|
||||||
class={styles.authorIcon}
|
class={styles.authorIcon}
|
||||||
/>
|
/>
|
||||||
@@ -261,7 +261,7 @@ export const FilterDialog = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onFormConfirm = useCallback(
|
const onFormConfirm = useCallback(
|
||||||
(returnValue: string) => {
|
(returnValue?: string) => {
|
||||||
// if the "confirm" button is pressed, the dialog should
|
// if the "confirm" button is pressed, the dialog should
|
||||||
// close with the selected values
|
// close with the selected values
|
||||||
if (returnValue === "confirm") {
|
if (returnValue === "confirm") {
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import { useWindowSize } from "../../../hooks/use-window-size";
|
|||||||
import { tabletLarge } from "../../../tokens/breakpoints";
|
import { tabletLarge } from "../../../tokens/breakpoints";
|
||||||
import { FilterDialog } from "./filter-dialog";
|
import { FilterDialog } from "./filter-dialog";
|
||||||
import { FilterSidebar } from "./filter-sidebar";
|
import { FilterSidebar } from "./filter-sidebar";
|
||||||
import tagMap from "../../../../content/data/tags.json";
|
import tagsObj from "../../../../content/data/tags.json";
|
||||||
import { SortType } from "./types";
|
import { SortType } from "./types";
|
||||||
|
|
||||||
|
const tagsMap = new Map(Object.entries(tagsObj));
|
||||||
|
|
||||||
interface FilterDisplayProps {
|
interface FilterDisplayProps {
|
||||||
unicornProfilePicMap: ProfilePictureMap;
|
unicornProfilePicMap: ProfilePictureMap;
|
||||||
posts: PostInfo[];
|
posts: PostInfo[];
|
||||||
@@ -73,9 +75,7 @@ export const FilterDisplay = ({
|
|||||||
.map((tag) => ({
|
.map((tag) => ({
|
||||||
tag,
|
tag,
|
||||||
numPosts: tagToPostNumMap.get(tag) || 0,
|
numPosts: tagToPostNumMap.get(tag) || 0,
|
||||||
emoji: tagMap[tag]?.emoji,
|
...tagsMap.get(tag),
|
||||||
image: tagMap[tag]?.image,
|
|
||||||
displayName: tagMap[tag]?.displayName,
|
|
||||||
}));
|
}));
|
||||||
}, [posts]);
|
}, [posts]);
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const FilterSection = ({
|
|||||||
// When cleared, the focus needs to be passed to the heading button
|
// When cleared, the focus needs to be passed to the heading button
|
||||||
// to avoid resetting to <body> when the clear button is removed from the DOM.
|
// to avoid resetting to <body> when the clear button is removed from the DOM.
|
||||||
// https://github.com/unicorn-utterances/unicorn-utterances/issues/742
|
// https://github.com/unicorn-utterances/unicorn-utterances/issues/742
|
||||||
const buttonRef = useRef<HTMLButtonElement>();
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
onClear();
|
onClear();
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export const FilterSidebar = ({
|
|||||||
count={author.numPosts}
|
count={author.numPosts}
|
||||||
icon={
|
icon={
|
||||||
<UUPicture
|
<UUPicture
|
||||||
picture={unicornProfilePicMap.find((u) => u.id === author.id)}
|
picture={unicornProfilePicMap.find((u) => u.id === author.id)!}
|
||||||
alt={""}
|
alt={""}
|
||||||
class={styles.authorIcon}
|
class={styles.authorIcon}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { JSXNode } from "components/types";
|
import { JSXNode } from "components/types";
|
||||||
import styles from "./search-hero.module.scss";
|
import styles from "./search-hero.module.scss";
|
||||||
import tags from "../../../../content/data/tags.json";
|
import tags from "../../../../content/data/tags.json";
|
||||||
import { MutableRef } from "preact/hooks";
|
|
||||||
import { Ref } from "preact";
|
|
||||||
|
|
||||||
const stickers = Object.values(tags)
|
const stickers = Object.values(tags)
|
||||||
.filter((tag) => !!tag["shownWithBranding"] && !!tag["image"])
|
.filter((tag) => "shownWithBranding" in tag && "image" in tag && tag.shownWithBranding)
|
||||||
.sort(() => 0.5 - Math.random()) as { image: string }[];
|
.sort(() => 0.5 - Math.random()) as { image: string }[];
|
||||||
|
|
||||||
const stickerTransforms = [
|
const stickerTransforms = [
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface SearchResultCountProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SearchResultCount = forwardRef<
|
export const SearchResultCount = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement | null,
|
||||||
SearchResultCountProps
|
SearchResultCountProps
|
||||||
>(({ numberOfPosts, numberOfCollections }, ref) => {
|
>(({ numberOfPosts, numberOfCollections }, ref) => {
|
||||||
const language = useMemo(() => {
|
const language = useMemo(() => {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const SearchTopbar = ({
|
|||||||
type="submit"
|
type="submit"
|
||||||
aria-label="Search"
|
aria-label="Search"
|
||||||
dangerouslySetInnerHTML={{ __html: forward }}
|
dangerouslySetInnerHTML={{ __html: forward }}
|
||||||
children={null}
|
children={[]}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
<div className={style.bigScreenContainer} />
|
<div className={style.bigScreenContainer} />
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ afterAll(() => server.close());
|
|||||||
function mockFetch(fn: (searchStr: string) => ServerReturnType) {
|
function mockFetch(fn: (searchStr: string) => ServerReturnType) {
|
||||||
server.use(
|
server.use(
|
||||||
rest.get<ServerReturnType>(`/api/search`, async (req, res, ctx) => {
|
rest.get<ServerReturnType>(`/api/search`, async (req, res, ctx) => {
|
||||||
const searchString = req.url.searchParams.get("query");
|
const searchString = req.url.searchParams.get("query")!;
|
||||||
return res(ctx.json(fn(searchString)));
|
return res(ctx.json(fn(searchString)));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -53,7 +53,9 @@ function mockFetchWithStatus(
|
|||||||
) {
|
) {
|
||||||
server.use(
|
server.use(
|
||||||
rest.get<never>(`/api/search`, async (req, res, ctx) => {
|
rest.get<never>(`/api/search`, async (req, res, ctx) => {
|
||||||
const searchString = req.url.searchParams.get("query");
|
const searchString = req.url.searchParams.get("query")!;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore it's fine
|
||||||
return res(ctx.status(status), ctx.json(fn(searchString)));
|
return res(ctx.status(status), ctx.json(fn(searchString)));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -338,7 +340,7 @@ describe("Search page", () => {
|
|||||||
const select =
|
const select =
|
||||||
container instanceof HTMLSelectElement
|
container instanceof HTMLSelectElement
|
||||||
? container
|
? container
|
||||||
: container.querySelector("select");
|
: container.querySelector("select")!;
|
||||||
|
|
||||||
await user.selectOptions(select, "newest");
|
await user.selectOptions(select, "newest");
|
||||||
|
|
||||||
@@ -398,7 +400,7 @@ describe("Search page", () => {
|
|||||||
const select =
|
const select =
|
||||||
container instanceof HTMLSelectElement
|
container instanceof HTMLSelectElement
|
||||||
? container
|
? container
|
||||||
: container.querySelector("select");
|
: container.querySelector("select")!;
|
||||||
|
|
||||||
user.selectOptions(select, "newest");
|
user.selectOptions(select, "newest");
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ function SearchPageBase({ unicornProfilePicMap }: SearchPageProps) {
|
|||||||
500,
|
500,
|
||||||
);
|
);
|
||||||
|
|
||||||
const resultsHeading = useRef<HTMLDivElement>();
|
const resultsHeading = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const onManualSubmit = useCallback(
|
const onManualSubmit = useCallback(
|
||||||
(str: string) => {
|
(str: string) => {
|
||||||
@@ -139,7 +139,7 @@ function SearchPageBase({ unicornProfilePicMap }: SearchPageProps) {
|
|||||||
totalPosts: 0,
|
totalPosts: 0,
|
||||||
collections: [],
|
collections: [],
|
||||||
totalCollections: 0,
|
totalCollections: 0,
|
||||||
},
|
} as ServerReturnType,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
retry: false,
|
retry: false,
|
||||||
enabled,
|
enabled,
|
||||||
@@ -194,7 +194,7 @@ function SearchPageBase({ unicornProfilePicMap }: SearchPageProps) {
|
|||||||
// Setup content to display
|
// Setup content to display
|
||||||
const contentToDisplay = useMemo(() => {
|
const contentToDisplay = useMemo(() => {
|
||||||
const urlVal = urlParams.get(CONTENT_TO_DISPLAY_KEY);
|
const urlVal = urlParams.get(CONTENT_TO_DISPLAY_KEY);
|
||||||
const isValid = ["all", "articles", "collections"].includes(urlVal);
|
const isValid = ["all", "articles", "collections"].includes(String(urlVal));
|
||||||
if (isValid) return urlVal as "all" | "articles" | "collections";
|
if (isValid) return urlVal as "all" | "articles" | "collections";
|
||||||
return DEFAULT_CONTENT_TO_DISPLAY;
|
return DEFAULT_CONTENT_TO_DISPLAY;
|
||||||
}, [urlParams]);
|
}, [urlParams]);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
// Enable stricter transpilation for better output.
|
// Enable stricter transpilation for better output.
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
"strict": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"jsxImportSource": "preact",
|
"jsxImportSource": "preact",
|
||||||
|
|||||||
Reference in New Issue
Block a user