import Markdown from "markdown-to-jsx"; import { useRouter } from "next/router"; import React, { Fragment, useMemo } from "react"; import ReactDOMServer from "react-dom/server"; import { HorizontalLine } from "components/HorizontalLine"; import { Img } from "components/Img"; import { InsetBox } from "components/InsetBox"; import { useAppLayout } from "contexts/AppLayoutContext"; import { AppStaticProps } from "graphql/getAppStaticProps"; import { cJoin } from "helpers/className"; import { slugify } from "helpers/formatters"; import { getAssetURL, ImageQuality } from "helpers/img"; import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/others"; import { useLightBox } from "hooks/useLightBox"; import { AnchorShare } from "components/AnchorShare"; /* * ╭─────────────╮ * ───────────────────────────────────────╯ COMPONENT ╰─────────────────────────────────────────── */ interface MarkdawnProps { className?: string; text: string; } // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ export const Markdawn = ({ className, text: rawText, }: MarkdawnProps): JSX.Element => { const { playerName } = useAppLayout(); const router = useRouter(); const [openLightBox, LightBox] = useLightBox(); /* eslint-disable no-irregular-whitespace */ const text = useMemo( () => `${preprocessMarkDawn(rawText, playerName)} ​`, [playerName, rawText] ); /* eslint-enable no-irregular-whitespace */ if (isUndefined(text) || text === "") { return <>; } return ( <> { if ( compProps.href.startsWith("/") || compProps.href.startsWith("#") ) { return ( router.push(compProps.href)}> {compProps.children} ); } return ( {compProps.children} ); }, }, h1: { component: (compProps: { id: string; style: React.CSSProperties; children: React.ReactNode; }) => (

{compProps.children}

), }, h2: { component: (compProps: { id: string; style: React.CSSProperties; children: React.ReactNode; }) => (

{compProps.children}

), }, h3: { component: (compProps: { id: string; style: React.CSSProperties; children: React.ReactNode; }) => (

{compProps.children}

), }, h4: { component: (compProps: { id: string; style: React.CSSProperties; children: React.ReactNode; }) => (

{compProps.children}

), }, h5: { component: (compProps: { id: string; style: React.CSSProperties; children: React.ReactNode; }) => (
{compProps.children}
), }, h6: { component: (compProps: { id: string; style: React.CSSProperties; children: React.ReactNode; }) => (
{compProps.children}
), }, SceneBreak: { component: (compProps: { id: string }) => (
* * *
), }, IntraLink: { component: (compProps: { children: React.ReactNode; target?: string; page?: string; }) => { const slug = isDefinedAndNotEmpty(compProps.target) ? slugify(compProps.target) : slugify(compProps.children?.toString()); return ( router.replace(`${compProps.page ?? ""}#${slug}`) } > {compProps.children} ); }, }, Transcript: { component: (compProps) => (
{compProps.children}
), }, Line: { component: (compProps) => ( <>

{compProps.children}

), }, InsetBox: { component: (compProps) => ( {compProps.children} ), }, li: { component: (compProps: { children: React.ReactNode }) => (
  • {compProps.children} ).length > 100 ? "my-4" : "" } > {compProps.children}
  • ), }, Highlight: { component: (compProps: { children: React.ReactNode }) => ( {compProps.children} ), }, footer: { component: (compProps: { children: React.ReactNode }) => ( <>
    {compProps.children}
    ), }, blockquote: { component: (compProps: { children: React.ReactNode; cite?: string; }) => (
    {isDefinedAndNotEmpty(compProps.cite) ? ( <> “{compProps.children}” — {compProps.cite} ) : ( compProps.children )}
    ), }, img: { component: (compProps: { alt: string; src: string; width?: number; height?: number; caption?: string; name?: string; }) => (
    { openLightBox([ compProps.src.startsWith("/uploads/") ? getAssetURL(compProps.src, ImageQuality.Large) : compProps.src, ]); }} >
    ), }, }, }} > {text}
    ); }; // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ interface TableOfContentsProps { text: string; title?: string; langui: AppStaticProps["langui"]; } // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ export const TableOfContents = ({ text, title, langui, }: TableOfContentsProps): JSX.Element => { const router = useRouter(); const toc = useMemo( () => getTocFromMarkdawn(preprocessMarkDawn(text), title), [text, title] ); return ( <>

    {langui.table_of_contents}

    router.replace(`#${toc.slug}`)}> {{toc.title}}

    ); }; /* * ╭──────────────────────╮ * ───────────────────────────────────╯ PRIVATE COMPONENTS ╰────────────────────────────────────── */ interface TocInterface { title: string; slug: string; children: TocInterface[]; } interface LevelProps { tocchildren: TocInterface[]; parentNumbering: string; } const TocLevel = ({ tocchildren, parentNumbering, }: LevelProps): JSX.Element => { const router = useRouter(); return (
      {tocchildren.map((child, childIndex) => (
    1. {`${parentNumbering}${ childIndex + 1 }.`}{" "} router.replace(`#${child.slug}`)}> {{child.title}}
    2. ))}
    ); }; /* * ╭──────────────────────╮ * ───────────────────────────────────╯ PRIVATE COMPONENTS ╰────────────────────────────────────── */ enum HeaderLevels { H1 = 1, H2 = 2, H3 = 3, H4 = 4, H5 = 5, H6 = 6, } const preprocessMarkDawn = (text: string, playerName = ""): string => { if (!text) return ""; const processedPlayerName = playerName .replaceAll("_", "\\_") .replaceAll("*", "\\*"); let preprocessed = text .replaceAll("--", "—") .replaceAll( "@player", isDefinedAndNotEmpty(processedPlayerName) ? processedPlayerName : "(player)" ); let scenebreakIndex = 0; const visitedSlugs: string[] = []; preprocessed = preprocessed .split("\n") .map((line) => { if (line === "* * *" || line === "---") { scenebreakIndex++; return ``; } 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; }) .join("\n"); return preprocessed; }; // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ const markdawnHeadersParser = ( headerLevel: HeaderLevels, line: string, visitedSlugs: string[] ): string => { const lineText = line.slice(headerLevel + 1); const slug = slugify(lineText); let newSlug = slug; let index = 2; while (visitedSlugs.includes(newSlug)) { newSlug = `${slug}-${index}`; index++; } visitedSlugs.push(newSlug); return `${lineText}`; }; // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => { const toc: TocInterface = { title: title ?? "Return to top", slug: slugify(title), children: [], }; let h2 = -1; let h3 = -1; let h4 = -1; let h5 = -1; let scenebreak = 0; let scenebreakIndex = 0; const getTitle = (line: string): string => line.slice(line.indexOf(`">`) + 2, line.indexOf(" line.slice(line.indexOf(`id="`) + 4, line.indexOf(`">`)); text.split("\n").map((line) => { if (line.startsWith("