.ts
TypeScript
(application/typescript)
// 1st-party
import { ReqHandler } from "@ethicdevs/react-monolith";
// 3rd-party
import Prism from "prismjs";
import { parse as parseHtmlToJson, TextNode, RealNode } from "himalaya";
// app
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>`);
    }

    // json
    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);
  }

  // unknown format
  return reply.status(200).send({
    success: false,
    error: {
      message: "Unknown output format. Must be one of: `html` | `json`.",
    },
  });
};

export default highlightCodeAction;