213 lines
6.9 KiB
TypeScript
213 lines
6.9 KiB
TypeScript
import rangeParser from "parse-numeric-range";
|
|
import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
import tsx from "react-syntax-highlighter/dist/cjs/languages/prism/tsx";
|
|
import typescript from "react-syntax-highlighter/dist/cjs/languages/prism/typescript";
|
|
import scss from "react-syntax-highlighter/dist/cjs/languages/prism/scss";
|
|
import bash from "react-syntax-highlighter/dist/cjs/languages/prism/bash";
|
|
import markdown from "react-syntax-highlighter/dist/cjs/languages/prism/markdown";
|
|
import json from "react-syntax-highlighter/dist/cjs/languages/prism/json";
|
|
import { coldarkDark as defaulttheme } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
|
import { BiCopy } from "react-icons/bi";
|
|
import { Components } from "react-markdown";
|
|
import { generateSlug } from "../utils/general";
|
|
import ReactMarkdown from "react-markdown";
|
|
import remarkGfm from "remark-gfm";
|
|
import remarkTypograf from "@mavrin/remark-typograf";
|
|
import smartypants from "remark-smartypants";
|
|
import rehypeRaw from "rehype-raw";
|
|
import emoji from "remark-emoji";
|
|
import oembed from "@agentofuser/remark-oembed";
|
|
import remarkDirective from 'remark-directive'
|
|
import remarkDirectiveRehype from 'remark-directive-rehype'
|
|
import { YouTubeVideo } from "./Youtube";
|
|
import { ImageSlide } from "./ImageSlide";
|
|
//import rehypeSanitize from "rehype-sanitize";
|
|
|
|
SyntaxHighlighter.registerLanguage("tsx", tsx);
|
|
SyntaxHighlighter.registerLanguage("typescript", typescript);
|
|
SyntaxHighlighter.registerLanguage("scss", scss);
|
|
SyntaxHighlighter.registerLanguage("bash", bash);
|
|
SyntaxHighlighter.registerLanguage("markdown", markdown);
|
|
SyntaxHighlighter.registerLanguage("json", json);
|
|
|
|
|
|
export const StyledMarkdown = ({ html }: { html: string }) => {
|
|
const MarkdownComponents: Components = {
|
|
h1: (props: any) => {
|
|
const arr = props.children;
|
|
let heading = "";
|
|
|
|
for (let i = 0; i < arr.length; i++) {
|
|
if (arr[i]?.type !== undefined) {
|
|
for (let j = 0; j < arr[i].props.children.length; j++) {
|
|
heading += arr[i]?.props?.children[0];
|
|
}
|
|
} else heading += arr[i];
|
|
}
|
|
|
|
const slug = generateSlug(heading);
|
|
return (
|
|
<h1 id={slug}>
|
|
<a href={`#${slug}`} {...props}></a>
|
|
</h1>
|
|
);
|
|
},
|
|
h2: (props: any) => {
|
|
const arr = props.children;
|
|
let heading = "";
|
|
|
|
for (let i = 0; i < arr.length; i++) {
|
|
if (arr[i]?.type !== undefined) {
|
|
for (let j = 0; j < arr[i].props.children.length; j++) {
|
|
heading += arr[i]?.props?.children[0];
|
|
}
|
|
} else heading += arr[i];
|
|
}
|
|
|
|
const slug = generateSlug(heading);
|
|
return (
|
|
<h2 id={slug}>
|
|
<a href={`#${slug}`} {...props}></a>
|
|
</h2>
|
|
);
|
|
},
|
|
h3: (props: any) => {
|
|
const arr = props.children;
|
|
let heading = "";
|
|
|
|
for (let i = 0; i < arr.length; i++) {
|
|
if (arr[i]?.type !== undefined) {
|
|
for (let j = 0; j < arr[i].props.children.length; j++) {
|
|
heading += arr[i]?.props?.children[0];
|
|
}
|
|
} else heading += arr[i];
|
|
}
|
|
|
|
const slug = generateSlug(heading);
|
|
return (
|
|
<h3 id={slug} className="text-red-500">
|
|
<a href={`#${slug}`} {...props}></a>
|
|
</h3>
|
|
);
|
|
},
|
|
h4: (props: any) => {
|
|
const arr = props.children;
|
|
let heading = "";
|
|
|
|
for (let i = 0; i < arr.length; i++) {
|
|
if (arr[i]?.type !== undefined) {
|
|
for (let j = 0; j < arr[i].props.children.length; j++) {
|
|
heading += arr[i]?.props?.children[0];
|
|
}
|
|
} else heading += arr[i];
|
|
}
|
|
|
|
const slug = generateSlug(heading);
|
|
return (
|
|
<h4 id={slug}>
|
|
<a href={`#${slug}`} {...props}></a>
|
|
</h4>
|
|
);
|
|
},
|
|
code({ node, inline, className, ...props }: any) {
|
|
|
|
const match = /language-(\w+)/.exec(className || "");
|
|
const hasMeta = node?.data?.meta;
|
|
|
|
|
|
const applyHighlights: object = (applyHighlights: number) => {
|
|
if (hasMeta) {
|
|
const RE = /{([\d,-]+)}/;
|
|
const metadata = node.data.meta?.replace(/\s/g, "");
|
|
const strlineNumbers = RE?.test(metadata)
|
|
? // @ts-ignore
|
|
RE?.exec(metadata)[1]
|
|
: "0";
|
|
|
|
const highlightLines = rangeParser(strlineNumbers);
|
|
const highlight = highlightLines;
|
|
const data = highlight.includes(applyHighlights) ? "highlight" : null;
|
|
return { data };
|
|
} else {
|
|
return {};
|
|
}
|
|
};
|
|
|
|
const applyStartingNumber = () => {
|
|
if (hasMeta) {
|
|
const metadata = node.data.meta?.replace(/\s/g, "");
|
|
const RE = /line=(\d+)/;
|
|
const start = RE?.test(metadata) && RE.exec(metadata);
|
|
return start ? +start[1] : 1;
|
|
} else {
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
const getTitle = () => {
|
|
if (hasMeta) {
|
|
const metadata = node.data.meta;
|
|
const RE = /title=\"(.*)\"/;
|
|
const title = RE?.test(metadata) && RE.exec(metadata);
|
|
return title ? title[1] : "Code";
|
|
}
|
|
return "Code";
|
|
};
|
|
|
|
|
|
return match ? (
|
|
<div>
|
|
<div className="w-full flex flex-row items-center gap-4">
|
|
<button
|
|
className="w-fit ml-2 important"
|
|
onClick={() => navigator.clipboard.writeText(props.children)}
|
|
>
|
|
<BiCopy />
|
|
</button>
|
|
{<div>{getTitle()}</div>}
|
|
</div>
|
|
<SyntaxHighlighter
|
|
style={defaulttheme}
|
|
language={match[1]}
|
|
PreTag="div"
|
|
className="codeStyle"
|
|
showLineNumbers={true}
|
|
wrapLines={hasMeta ? true : false}
|
|
useInlineStyles={true}
|
|
lineProps={applyHighlights}
|
|
startingLineNumber={applyStartingNumber()}
|
|
{...props}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<code className={className} {...props} />
|
|
);
|
|
},
|
|
//custom directives
|
|
//@ts-ignore
|
|
'yt': YouTubeVideo,
|
|
'img-slide': ImageSlide,
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="
|
|
prose
|
|
prose-code:font-mono prose-code:text-gray-100 prose-code:bg-gray-700 prose-code:p-1 prose-code:m-1 prose-code:rounded-md
|
|
prose-headings:text-gray-800 dark:prose-headings:text-gray-100 prose-p:text-gray-600 dark:prose-p:text-gray-300
|
|
prose-img:w-full prose-img:h-auto prose-img:object-cover
|
|
prose-li:text-gray-600 dark:prose-li:text-gray-300 prose-td:text-gray-600 dark:prose-td:text-gray-400
|
|
prose-a:text-primary prose-strong:text-gray-900 dark:prose-strong:text-gray-50
|
|
dark:prose-hr:bg-gray-200 prose-hr:bg-gray-400
|
|
"
|
|
>
|
|
<ReactMarkdown
|
|
components={MarkdownComponents}
|
|
rehypePlugins={[rehypeRaw]}
|
|
remarkPlugins={[remarkDirective, remarkDirectiveRehype, oembed, remarkGfm, smartypants, emoji, remarkTypograf]}
|
|
children={html}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|