import { ReactIsland } from "@ethicdevs/react-monolith";
import React, {
CSSProperties,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import Prism from "prismjs";
import styled, { css } from "styled-components";
import type { AppThemeScheme, WithThemeSchemeProp } from "../types";
import { NamedColors } from "../utils/style";
import { escapeHtmlCode, getGitdiffLineStart } from "../utils/shared";
import { ClientSideRouterEvents } from "./InstantRouterIndicator";
interface CodeProps {
code: string;
language: string;
style?: CSSProperties;
[x: string]: unknown;
}
if (typeof window === "undefined") {
Prism.languages.prisma = Prism.languages.extend("javascript", {
keyword: /\b(?:datasource|enum|generator|model|type)\b/,
});
Prism.languages.insertBefore("prisma", "function", {
annotation: {
pattern: /(^|[^.])@+\w+/,
lookbehind: true,
alias: "punctuation",
},
});
Prism.languages.insertBefore("prisma", "punctuation", {
"type-args": /\b(?:references|fields):/,
});
}
const Code: ReactIsland<CodeProps & WithThemeSchemeProp> = ({
code,
language,
themeScheme,
style,
...props
}) => {
const { before: lineStartAt } = useMemo(
() => getGitdiffLineStart(code),
[code]
);
const computeSyntaxHighlightingSSR = useCallback(() => {
const safeLanguage =
"languages" in Prism &&
Prism.languages != null &&
typeof Prism.languages === "object" &&
language in Prism.languages &&
Prism.languages[language] != null
? Prism.languages[language]
: null;
return {
__html:
safeLanguage != null
? Prism.highlight(code, safeLanguage, language)
: escapeHtmlCode(code),
};
}, [code, language]);
const appendLineNumbers = useCallback(
(inHtml: {
__html: string;
}): {
__html: string;
} => {
const copyInnerHtml = { __html: inHtml.__html };
const linesCount = copyInnerHtml.__html.split("\n").length;
copyInnerHtml.__html += `\n<span aria-hidden="true" class="line-numbers-rows">`;
for (let i = linesCount; i > 0; i--) {
const lineNumber = lineStartAt + (linesCount - i + 1);
copyInnerHtml.__html += `<a id="l-${lineNumber}" href="#l-${lineNumber}" data-line-number="${lineNumber}"></a>`;
}
copyInnerHtml.__html += `</span>`;
return copyInnerHtml;
},
[lineStartAt]
);
const [innerHtml, setInnerHtml] = useState<{ __html: string }>(
appendLineNumbers(computeSyntaxHighlightingSSR())
);
const getHighlightedCodeInnerHtml = useCallback(async (): Promise<{
__html: string;
}> => {
let inHtml;
if (typeof window === "undefined") {
inHtml = computeSyntaxHighlightingSSR();
} else {
const { protocol, hostname, port } = new URL(window.location.href);
const res = await fetch(
`${protocol}//${hostname}:${port}/api/syntax/highlight/html`,
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
code,
language,
theme_scheme: themeScheme,
}),
}
);
const jsonRes = await res.json();
inHtml = {
__html: jsonRes.html,
};
}
inHtml = appendLineNumbers(inHtml);
return inHtml;
}, [
code,
language,
themeScheme,
appendLineNumbers,
computeSyntaxHighlightingSSR,
]);
const getHighlightedCodeAsync = useCallback(async () => {
try {
if (code == null || code.trim() === "") {
return;
}
const innerHtmlGenerated = await getHighlightedCodeInnerHtml();
setInnerHtml(innerHtmlGenerated);
} catch (_) {}
}, [code, getHighlightedCodeInnerHtml, setInnerHtml]);
const onClientSideRouterLoadComplete = useCallback(() => {
getHighlightedCodeAsync();
}, [getHighlightedCodeAsync]);
useEffect(() => {
onClientSideRouterLoadComplete();
document.addEventListener(
ClientSideRouterEvents.NAVIGATED,
onClientSideRouterLoadComplete
);
return () => {
document.removeEventListener(
ClientSideRouterEvents.NAVIGATED,
onClientSideRouterLoadComplete
);
};
}, []);
return (
<div className="line-numbers" style={{ width: "100%" }}>
<StylePreTag
data-language={language}
data-start={lineStartAt}
className={` language-${language} line-numbers`}
themeScheme={themeScheme}
style={{ counterReset: "linenumber 0", ...(style || {}) }}
>
<StyledCodeTag {...props} dangerouslySetInnerHTML={innerHtml} />
</StylePreTag>
</div>
);
};
export const getThemedCodeCss = (themeScheme: AppThemeScheme): JSX.Element => (
<>
{themeScheme === "light" ? (
<style
dangerouslySetInnerHTML={{
__html: `code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;padding-left:3.5em;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow-y:hidden;overflow-x:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}`,
}}
/>
) : (
<style
dangerouslySetInnerHTML={{
__html: `code[class*=language-],pre[class*=language-]{color:#fff;background:0 0;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;padding-left:3.5em;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}:not(pre)>code[class*=language-],pre[class*=language-]{background:#4c3f33}pre[class*=language-]{padding:1em;margin:.5em 0;overflow-y:hidden;overflow-x:auto;border:.3em solid #7a6651;border-radius:.5em;box-shadow:1px 1px .5em #000 inset}:not(pre)>code[class*=language-]{padding:.15em .2em .05em;border-radius:.3em;border:.13em solid #7a6651;box-shadow:1px 1px .3em -.1em #000 inset;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#997f66}.token.punctuation{opacity:.7}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.number,.token.property,.token.symbol,.token.tag{color:#d1939e}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#bce051}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f4b73d}.token.atrule,.token.attr-value,.token.keyword{color:#d1939e}.token.important,.token.regex{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.deleted{color:red}`,
}}
/>
)}
<style
dangerouslySetInnerHTML={{
__html: `pre.line-numbers {
display: flex;
flex-flow: row nowrap;
position: relative;
padding-left: 3.8em;
counter-reset: linenumber;
}
pre.line-numbers > code {
position: relative;
display: block;
padding-left: 3.5em;
padding-bottom: 0;
white-space: inherit;
}
.line-numbers .line-numbers-rows {
position: absolute;
top: 0;
bottom: 0;
left: 0;
font-size: 100%;
width: 3em; /* works for line-numbers below 1000 lines */
letter-spacing: -1px;
border-right: 1px solid ${NamedColors.BORDER_DEFAULT[themeScheme]};
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.line-numbers-rows > a {
display: block;
counter-increment: linenumber;
font-weight: normal;
}
.line-numbers-rows > a:before {
content: attr(data-line-number);
color: ${NamedColors.TEXT_MUTED[themeScheme]};
display: block;
padding-right: 0.4em;
text-align: right;
}
`,
}}
/>
</>
);
const StylePreTag = styled.pre<WithThemeSchemeProp>`
width: 100%;
margin: 0 !important;
padding: 0 !important;
box-shadow: none !important;
text-shadow: none !important;
border-radius: 8px;
${({ themeScheme }) => css`
background-color: ${NamedColors.CARD[themeScheme]} !important;
border: 1px solid ${NamedColors.BORDER_DEFAULT[themeScheme]} !important;
`};
`;
export const StyledCodeTag = styled.code`
display: block;
min-height: 20px;
width: 100%;
padding: 4px 8px;
font-size: 16px;
border-radius: 4px !important;
`;
Code.displayName = "Code";
export default Code;