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}
>
)}
>
);
};
/*
* ╭──────────────────────╮
* ───────────────────────────────────╯ 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) => (
-
{`${parentNumbering}${childIndex + 1}.`}{" "}
{{child.title}}
))}
);
};
/*
* ╭───────────────────╮
* ─────────────────────────────────────╯ 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 ``;
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
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(""));
const getSlug = (line: string): string =>
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;
};