import { ReqHandler } from "@ethicdevs/react-monolith";
import Prism from "prismjs";
import { parse as parseHtmlToJson, TextNode, RealNode } from "himalaya";
import type { AppThemeScheme } from "../../types";
import { AppRoute, AppRouteParams } from "../../routes.defs";
import { escapeHtmlCode } from "../../utils/shared/escapeHtmlCode";
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 syntaxHighlightThemes: Record<AppThemeScheme, string> = {
light: `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:0.5em 0;overflow-y:hidden;overflow-x:auto}:not(pre) > code[class*="language-"],pre[class*="language-"]{background:#f5f2f0}:not(pre) > code[class*="language-"]{padding:0.1em;border-radius:0.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:0.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%, 0.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}`,
dark: `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:0.5em 0;overflow-y:hidden;overflow-x:auto;border:0.3em solid #7a6651;border-radius:0.5em;box-shadow:1px 1px 0.5em #000 inset}:not(pre) > code[class*="language-"]{padding:0.15em 0.2em 0.05em;border-radius:0.3em;border:0.13em solid #7a6651;box-shadow:1px 1px 0.3em -0.1em #000 inset;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#997f66}.token.punctuation{opacity:0.7}.token.namespace{opacity:0.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}`,
};
function getHighlightedCode(
code: string,
language: string,
themeScheme: AppThemeScheme
): { html: string; cssRules: string; durationMs: number } {
const _startTimeMs = Date.now();
const isLanguageSupportedByPrism = !!(
Prism.languages != null &&
typeof Prism.languages === "object" &&
language in Prism.languages &&
Prism.languages[language] != null
);
const html = isLanguageSupportedByPrism
? Prism.highlight(code, Prism.languages[language], language)
: escapeHtmlCode(code);
return {
html,
cssRules: syntaxHighlightThemes[themeScheme],
durationMs: Date.now() - _startTimeMs,
};
}
const getNodeTextRecursive = (node: TextNode | RealNode, depth = 0): string => {
if (depth > 1000) throw new Error("Too much recursion.");
return node.type === "text"
? node.content.replace(/\r\n/i, "\n")
: getNodeTextRecursive(node.children[0], depth + 1);
};
const getNodesRecursive = (
node: TextNode | RealNode,
depth = 0
): { text: string; type: string }[] => {
if (depth > 1000) throw new Error("Too much recursion.");
return node.type === "text"
? [
{
text: node.content.replace(/\r\n/i, "\n"),
type: "text",
},
]
: node.children.map((childNode) => ({
text: getNodeTextRecursive(childNode, depth + 1),
type:
childNode.type === "text"
? "text"
: childNode.attributes[0].key === "class"
? childNode.attributes[0].value.replace(/^token /i, "")
: "attr",
}));
};
const highlightCodeAction: ReqHandler<
AppRouteParams,
AppRoute.SYNTAX_HIGHLIGHT_HIGHLIGHT_CODE_ACTION
> = async (request, reply) => {
const { outputFormat = "html" } = request.params;
const { code, language, theme_scheme: themeScheme } = request.body;
if (request.validationError != null) {
if (outputFormat === "html") {
return reply.status(500).send(`<div class="error">
<h1>Woops, Internal Server Error...</h1>
<p>${request.validationError.message}</p>
</div>`);
}
return reply.status(500).send({
success: false,
error: request.validationError,
});
}
const result = getHighlightedCode(code, language, themeScheme);
if (outputFormat === "html") {
return reply.status(200).send(result);
} else if (outputFormat === "json") {
const parsedJson = parseHtmlToJson(result.html);
const tokens = parsedJson.reduce((acc, node) => {
if (node.type === "text") {
acc = [
...acc,
{ text: node.content.replace(/\r\n/i, "\n"), type: "text" },
];
} else {
acc = [...acc, ...getNodesRecursive(node)];
}
return acc;
}, [] as { text: string; type: string }[]);
return reply.status(200).send(tokens);
}
return reply.status(200).send({
success: false,
error: {
message: "Unknown output format. Must be one of: `html` | `json`.",
},
});
};
export default highlightCodeAction;