370 lines
12 KiB
TypeScript
370 lines
12 KiB
TypeScript
import HorizontalLine from "components/HorizontalLine";
|
|
import Img, { getAssetURL, ImageQuality } from "components/Img";
|
|
import InsetBox from "components/InsetBox";
|
|
import LightBox from "components/LightBox";
|
|
import ToolTip from "components/ToolTip";
|
|
import { useAppLayout } from "contexts/AppLayoutContext";
|
|
import Markdown from "markdown-to-jsx";
|
|
import { slugify } from "queries/helpers";
|
|
import React, { useState } from "react";
|
|
|
|
type ScenBreakProps = {
|
|
className?: string;
|
|
text: string;
|
|
};
|
|
|
|
export default function Markdawn(props: ScenBreakProps): JSX.Element {
|
|
const appLayout = useAppLayout();
|
|
const text = preprocessMarkDawn(props.text);
|
|
|
|
const [lightboxOpen, setLightboxOpen] = useState(false);
|
|
const [lightboxImages, setLightboxImages] = useState([""]);
|
|
const [lightboxIndex, setLightboxIndex] = useState(0);
|
|
|
|
if (text) {
|
|
return (
|
|
<>
|
|
<LightBox
|
|
state={lightboxOpen}
|
|
setState={setLightboxOpen}
|
|
images={lightboxImages}
|
|
index={lightboxIndex}
|
|
setIndex={setLightboxIndex}
|
|
/>
|
|
<Markdown
|
|
className={`formatted ${props.className}`}
|
|
options={{
|
|
slugify: slugify,
|
|
overrides: {
|
|
h1: {
|
|
component: (props: {
|
|
id: string;
|
|
style: React.CSSProperties;
|
|
children: React.ReactNode;
|
|
}) => {
|
|
return (
|
|
<div className="flex flex-row place-items-center place-content-center gap-3">
|
|
<h1 id={props.id} style={props.style}>
|
|
{props.children}
|
|
</h1>
|
|
<HeaderToolTip id={props.id} />
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
h2: {
|
|
component: (props: {
|
|
id: string;
|
|
style: React.CSSProperties;
|
|
children: React.ReactNode;
|
|
}) => {
|
|
return (
|
|
<div className="flex flex-row place-items-center place-content-center gap-3">
|
|
<h2 id={props.id} style={props.style}>
|
|
{props.children}
|
|
</h2>
|
|
<HeaderToolTip id={props.id} />
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
h3: {
|
|
component: (props: {
|
|
id: string;
|
|
style: React.CSSProperties;
|
|
children: React.ReactNode;
|
|
}) => {
|
|
return (
|
|
<div className="flex flex-row place-items-center place-content-center gap-3">
|
|
<h3 id={props.id} style={props.style}>
|
|
{props.children}
|
|
</h3>
|
|
<HeaderToolTip id={props.id} />
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
h4: {
|
|
component: (props: {
|
|
id: string;
|
|
style: React.CSSProperties;
|
|
children: React.ReactNode;
|
|
}) => {
|
|
return (
|
|
<div className="flex flex-row place-items-center place-content-center gap-3">
|
|
<h4 id={props.id} style={props.style}>
|
|
{props.children}
|
|
</h4>
|
|
<HeaderToolTip id={props.id} />
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
h5: {
|
|
component: (props: {
|
|
id: string;
|
|
style: React.CSSProperties;
|
|
children: React.ReactNode;
|
|
}) => {
|
|
return (
|
|
<div className="flex flex-row place-items-center place-content-center gap-3">
|
|
<h5 id={props.id} style={props.style}>
|
|
{props.children}
|
|
</h5>
|
|
<HeaderToolTip id={props.id} />
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
h6: {
|
|
component: (props: {
|
|
id: string;
|
|
style: React.CSSProperties;
|
|
children: React.ReactNode;
|
|
}) => {
|
|
return (
|
|
<div className="flex flex-row place-items-center place-content-center gap-3">
|
|
<h6 id={props.id} style={props.style}>
|
|
{props.children}
|
|
</h6>
|
|
<HeaderToolTip id={props.id} />
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
Sep: {
|
|
component: () => {
|
|
return <div className="my-24"></div>;
|
|
},
|
|
},
|
|
SceneBreak: {
|
|
component: (props: { id: string }) => {
|
|
return (
|
|
<div
|
|
id={props.id}
|
|
className={
|
|
"h-0 text-center text-3xl text-dark mt-16 mb-20"
|
|
}
|
|
>
|
|
* * *
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
player: {
|
|
component: () => {
|
|
return (
|
|
<span className="text-dark opacity-70">
|
|
{appLayout.playerName ? appLayout.playerName : "<player>"}
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
Transcript: {
|
|
component: (props) => {
|
|
return (
|
|
<div className="grid grid-cols-[auto_1fr] mobile:grid-cols-1 gap-x-6 gap-y-2">
|
|
{props.children}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
Line: {
|
|
component: (props) => {
|
|
return (
|
|
<>
|
|
<strong className="text-dark opacity-60 mobile:!-mb-4">
|
|
{props.name}
|
|
</strong>
|
|
<p className="whitespace-pre-line">{props.children}</p>
|
|
</>
|
|
);
|
|
},
|
|
},
|
|
InsetBox: {
|
|
component: (props) => {
|
|
return (
|
|
<InsetBox className="my-12">{props.children}</InsetBox>
|
|
);
|
|
},
|
|
},
|
|
li: {
|
|
component: (props: { children: React.ReactNode }) => {
|
|
return (
|
|
<li
|
|
className={
|
|
props.children &&
|
|
props.children?.toString().length > 100
|
|
? "my-4"
|
|
: ""
|
|
}
|
|
>
|
|
{props.children}
|
|
</li>
|
|
);
|
|
},
|
|
},
|
|
Highlight: {
|
|
component: (props: { children: React.ReactNode }) => {
|
|
return <mark>{props.children}</mark>;
|
|
},
|
|
},
|
|
footer: {
|
|
component: (props: { children: React.ReactNode }) => {
|
|
return (
|
|
<>
|
|
<HorizontalLine />
|
|
<div>{props.children}</div>
|
|
</>
|
|
);
|
|
},
|
|
},
|
|
img: {
|
|
component: (props: {
|
|
alt: string;
|
|
src: string;
|
|
width?: number;
|
|
height?: number;
|
|
caption?: string;
|
|
name?: string;
|
|
}) => {
|
|
return (
|
|
<div
|
|
className="my-8 cursor-pointer"
|
|
onClick={() => {
|
|
setLightboxOpen(true);
|
|
setLightboxImages([
|
|
props.src.startsWith("/uploads/")
|
|
? getAssetURL(props.src, ImageQuality.Large)
|
|
: props.src,
|
|
]);
|
|
setLightboxIndex(0);
|
|
}}
|
|
>
|
|
{props.src.startsWith("/uploads/") ? (
|
|
<div className="relative w-full aspect-video">
|
|
<Img
|
|
image={{
|
|
__typename: "UploadFile",
|
|
alternativeText: props.alt,
|
|
url: props.src,
|
|
width: props.width || 1500,
|
|
height: props.height || 1000,
|
|
caption: props.caption || "",
|
|
name: props.name || "",
|
|
}}
|
|
layout="fill"
|
|
objectFit="contain"
|
|
quality={ImageQuality.Medium}
|
|
></Img>
|
|
</div>
|
|
) : (
|
|
<div className="grid place-content-center">
|
|
<img {...props} className="max-h-[50vh] " />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
},
|
|
}}
|
|
>
|
|
{text}
|
|
</Markdown>
|
|
</>
|
|
);
|
|
}
|
|
return <></>;
|
|
}
|
|
|
|
function HeaderToolTip(props: { id: string }) {
|
|
return (
|
|
<ToolTip content={"Copy anchor link"} trigger="mouseenter">
|
|
<ToolTip content={"Copied! 👍"} trigger="click">
|
|
<span
|
|
className="material-icons transition-color hover:text-dark cursor-pointer"
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(
|
|
process.env.NEXT_PUBLIC_URL_SELF +
|
|
window.location.pathname +
|
|
"#" +
|
|
props.id
|
|
);
|
|
}}
|
|
>
|
|
link
|
|
</span>
|
|
</ToolTip>
|
|
</ToolTip>
|
|
);
|
|
}
|
|
|
|
export function preprocessMarkDawn(text: string): string {
|
|
let scenebreakIndex = 0;
|
|
const visitedSlugs: string[] = [];
|
|
|
|
const result = text.split("\n").map((line) => {
|
|
if (line === "* * *" || line === "---") {
|
|
scenebreakIndex++;
|
|
return `<SceneBreak id="scene-break-${scenebreakIndex}">`;
|
|
}
|
|
|
|
if (line.startsWith("# ")) {
|
|
return markdawnHeadersParser(headerLevels.h1, line, visitedSlugs);
|
|
}
|
|
|
|
if (line.startsWith("## ")) {
|
|
return markdawnHeadersParser(headerLevels.h2, line, visitedSlugs);
|
|
}
|
|
|
|
if (line.startsWith("### ")) {
|
|
return markdawnHeadersParser(headerLevels.h3, line, visitedSlugs);
|
|
}
|
|
|
|
if (line.startsWith("#### ")) {
|
|
return markdawnHeadersParser(headerLevels.h4, line, visitedSlugs);
|
|
}
|
|
|
|
if (line.startsWith("##### ")) {
|
|
return markdawnHeadersParser(headerLevels.h5, line, visitedSlugs);
|
|
}
|
|
|
|
if (line.startsWith("###### ")) {
|
|
return markdawnHeadersParser(headerLevels.h6, line, visitedSlugs);
|
|
}
|
|
|
|
return line;
|
|
});
|
|
return result.join("\n");
|
|
}
|
|
|
|
enum headerLevels {
|
|
h1 = 1,
|
|
h2 = 2,
|
|
h3 = 3,
|
|
h4 = 4,
|
|
h5 = 5,
|
|
h6 = 6,
|
|
}
|
|
|
|
function markdawnHeadersParser(
|
|
headerLevel: headerLevels,
|
|
line: string,
|
|
visitedSlugs: string[]
|
|
): string {
|
|
const lineText = line.slice(headerLevel + 1);
|
|
let slug = slugify(lineText);
|
|
let newSlug = slug;
|
|
let index = 2;
|
|
while (visitedSlugs.includes(newSlug)) {
|
|
newSlug = `${slug}-${index}`;
|
|
index++;
|
|
}
|
|
visitedSlugs.push(newSlug);
|
|
return `<${headerLevels[headerLevel]} id="${newSlug}">${lineText}</${headerLevels[headerLevel]}>`;
|
|
}
|
|
function getAssetUrl(): React.SetStateAction<string[]> {
|
|
throw new Error("Function not implemented.");
|
|
}
|