import Markdown from "markdown-to-jsx"; 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/Containers/InsetBox"; import { cIf, cJoin } from "helpers/className"; import { slugify } from "helpers/formatters"; import { getAssetURL, ImageQuality } from "helpers/img"; import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/asserts"; import { AnchorShare } from "components/AnchorShare"; import { useIntersectionList } from "hooks/useIntersectionList"; import { Ico, Icon } from "components/Ico"; import { useDeviceSupportsHover } from "hooks/useMediaQuery"; import { atoms } from "contexts/atoms"; import { useAtomGetter } from "helpers/atoms"; import { Link } from "components/Inputs/Link"; /* * ╭─────────────╮ * ───────────────────────────────────────╯ COMPONENT ╰─────────────────────────────────────────── */ interface MarkdawnProps { className?: string; text: string; } // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Element => { const playerName = useAtomGetter(atoms.settings.playerName); const isContentPanelAtLeastLg = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastLg); const { showLightBox } = useAtomGetter(atoms.lightBox); /* eslint-disable no-irregular-whitespace */ const text = `${preprocessMarkDawn(rawText, playerName)} ​`; /* eslint-enable no-irregular-whitespace */ if (isUndefined(text) || text === "") { return <>; } return ( { if (compProps.href.startsWith("/") || compProps.href.startsWith("#")) { return ( {compProps.children} ); } return ( {compProps.children} ); }, }, Header: { component: (compProps: { id: string; style: React.CSSProperties; children: string; level: string; }) => (
), }, 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 ( {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; }) => (
    { showLightBox([ compProps.src.startsWith("/uploads/") ? getAssetURL(compProps.src, ImageQuality.Large) : compProps.src, ]); }}>
    ), }, }, }}> {text} ); }; // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ interface TableOfContentsProps { text: string; title?: string; horizontalLine?: boolean; } export const TableOfContents = ({ text, title, horizontalLine = false, }: TableOfContentsProps): JSX.Element => { const langui = useAtomGetter(atoms.localData.langui); const toc = getTocFromMarkdawn(preprocessMarkDawn(text), title); return ( <> {toc.children.length > 0 && ( <> {horizontalLine && }

    {langui.table_of_contents}

    {{toc.title}}

    )} ); }; /* * ╭──────────────────────╮ * ───────────────────────────────────╯ PRIVATE COMPONENTS ╰────────────────────────────────────── */ interface HeaderProps { level: number; title: string; slug: string; } const Header = ({ level, title, slug }: HeaderProps): JSX.Element => { const isHoverable = useDeviceSupportsHover(); const innerComponent = ( <>
    {title === "* * *" ? (
    ) : (
    {title}
    )}
    ); switch (level) { case 1: return (

    {innerComponent}

    ); case 2: return (

    {innerComponent}

    ); case 3: return (

    {innerComponent}

    ); case 4: return (

    {innerComponent}

    ); case 5: return (
    {innerComponent}
    ); default: return (
    {innerComponent}
    ); } }; interface TocInterface { title: string; slug: string; children: TocInterface[]; } interface LevelProps { tocchildren: TocInterface[]; parentNumbering: string; allowIntersection?: boolean; } const TocLevel = ({ tocchildren, parentNumbering, allowIntersection = true, }: LevelProps): JSX.Element => { const ids = useMemo(() => tocchildren.map((child) => child.slug), [tocchildren]); const currentIntersection = useIntersectionList(ids); return (
      {tocchildren.map((child, childIndex) => (
    1. {`${parentNumbering}${childIndex + 1}.`}{" "} {{child.title}}
    2. ))}
    ); }; /* * ╭───────────────────╮ * ─────────────────────────────────────╯ PRIVATE METHODS ╰─────────────────────────────────────── */ 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 (/^[#]+ /u.test(line)) { return markdawnHeadersParser(line.indexOf(" "), line, visitedSlugs); } return line; }) .join("\n"); return preprocessed; }; // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ const markdawnHeadersParser = ( headerLevel: number, 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('
    = 0 && line.startsWith('
    = 0 && line.startsWith('
    = 0 && line.startsWith('
    = 0 && line.startsWith('
    = 0) { toc.children[h2]?.children[h3]?.children[h4]?.children[h5]?.children.push(child); } else if (h4 >= 0) { toc.children[h2]?.children[h3]?.children[h4]?.children.push(child); } else if (h3 >= 0) { toc.children[h2]?.children[h3]?.children.push(child); } else if (h2 >= 0) { toc.children[h2]?.children.push(child); } else { toc.children.push(child); } } }); return toc; };