highlight

tcmts.cli.pages.highlight · #sgynn3rh · term · runs on the server

uses tcmts.cli.pages.jsxPieces, which uses tcmts.core.ts.parse, which uses tcmts.core.ts.compiler, which uses process

Source as HTML: tokens classed for colouring (k keyword, x control keyword, s string, n number, c comment, f called function, t type name), each reference to a definition a link (<a data-hash>), and an import's module a link to its namespace's or package's page. Uses TypeScript's scanner, re-scanning where it needs context: a } that resumes a template literal, a / that starts a regex.

import { CONTROL, esc, jsxPieces } from "#codebase/tcmts/cli/pages";
import type { HighlightRef } from "#codebase/tcmts/cli/pages";
import {
  createScanner as ts_createScanner, LanguageVariant as ts_LanguageVariant, SyntaxKind as ts_SyntaxKind,
} from "typescript/ast";

export function highlight(
  text: string,
  refs: HighlightRef[],
  typeHashes: Set<string>,
  jsx = false,
): string {
  const scanner = ts_createScanner(false, ts_LanguageVariant.Standard, text);
  const at = new Map(refs.map((r) => [r.start, r]));
  // (the scanner alone reads JSX's text as code: those parts come from the syntax tree)
  const pieces = jsx ? jsxPieces(text, at) : undefined;
  // open braces since each template substitution started (`${`): the `}` closing one resumes the template
  const templates: number[] = [];
  let previous = ts_SyntaxKind.Unknown;
  const endsExpression = (k: ts_SyntaxKind) =>
    k === ts_SyntaxKind.Identifier ||
    k === ts_SyntaxKind.CloseParenToken ||
    k === ts_SyntaxKind.CloseBracketToken ||
    k === ts_SyntaxKind.CloseBraceToken ||
    k === ts_SyntaxKind.NumericLiteral ||
    k === ts_SyntaxKind.StringLiteral ||
    k === ts_SyntaxKind.ThisKeyword ||
    k === ts_SyntaxKind.NoSubstitutionTemplateLiteral ||
    k === ts_SyntaxKind.TemplateTail;
  let out = "";
  for (let kind = scanner.scan(); kind !== ts_SyntaxKind.EndOfFile; kind = scanner.scan()) {
    if (kind === ts_SyntaxKind.OpenBraceToken && templates.length)
      templates[templates.length - 1]!++;
    if (kind === ts_SyntaxKind.CloseBraceToken && templates.length) {
      if (templates.at(-1) === 0) {
        kind = scanner.reScanTemplateToken(false);
        if (kind === ts_SyntaxKind.TemplateTail) templates.pop();
      } else templates[templates.length - 1]!--;
    }
    if (kind === ts_SyntaxKind.TemplateHead) templates.push(0);
    if (
      (kind === ts_SyntaxKind.SlashToken || kind === ts_SyntaxKind.SlashEqualsToken) &&
      !endsExpression(previous)
    )
      kind = scanner.reScanSlashToken();
    const start = scanner.getTokenStart();
    const piece = pieces?.get(start);
    if (piece) {
      out += piece.html;
      scanner.resetTokenState(piece.end);
      // (a JSX element ends an expression)
      previous = ts_SyntaxKind.Identifier;
      continue;
    }
    // a token that takes nothing (TypeScript 7's scanner gives a `#` with no name after it so, as in JSX text read as
    // code): that character as it is, and on from it, or the scan would stay there forever
    if (scanner.getTokenEnd() <= start) {
      const n = text.codePointAt(start)! > 0xffff ? 2 : 1;
      out += esc(text.slice(start, start + n));
      scanner.resetTokenState(start + n);
      continue;
    }
    const raw = text.slice(start, scanner.getTokenEnd());
    const trivia = kind === ts_SyntaxKind.WhitespaceTrivia || kind === ts_SyntaxKind.NewLineTrivia;
    const ref = at.get(start);
    // a name followed by a call (or type arguments, then a call): coloured as a function, linked or not
    const called =
      kind === ts_SyntaxKind.Identifier &&
      /^\s*(<[^>()]*>)?\s*\(/.test(text.slice(scanner.getTokenEnd(), scanner.getTokenEnd() + 80));
    if (ref?.hash && kind === ts_SyntaxKind.Identifier) {
      // (a link keeps the colour the name would have)
      out += `<a class="ref${typeHashes.has(ref.hash) ? " t" : called ? " f" : ""}" data-hash="${ref.hash}">${esc(raw)}</a>`;
    } else if (
      ref &&
      kind === ts_SyntaxKind.StringLiteral &&
      (ref.namespace !== undefined || ref.package)
    ) {
      const to = ref.package
        ? `data-package-name="${esc(ref.package.name)}" data-package-range="${esc(ref.package.range)}"`
        : `data-namespace="${esc(ref.namespace!)}"`;
      out += `<a class="ref s" ${to}>${esc(raw)}</a>`;
    } else {
      const cls =
        kind === ts_SyntaxKind.SingleLineCommentTrivia ||
        kind === ts_SyntaxKind.MultiLineCommentTrivia
          ? "c"
          : kind >= ts_SyntaxKind.FirstKeyword && kind <= ts_SyntaxKind.LastKeyword
            ? CONTROL.has(raw)
              ? "x"
              : "k"
            : kind === ts_SyntaxKind.StringLiteral ||
                kind === ts_SyntaxKind.RegularExpressionLiteral ||
                (kind >= ts_SyntaxKind.FirstTemplateToken &&
                  kind <= ts_SyntaxKind.LastTemplateToken)
              ? "s"
              : kind === ts_SyntaxKind.NumericLiteral || kind === ts_SyntaxKind.BigIntLiteral
                ? "n"
                : called
                  ? "f"
                  : "";
      out += cls ? `<span class="${cls}">${esc(raw)}</span>` : esc(raw);
    }
    if (
      !trivia &&
      kind !== ts_SyntaxKind.SingleLineCommentTrivia &&
      kind !== ts_SyntaxKind.MultiLineCommentTrivia
    )
      previous = kind;
  }
  return out;
}

uses

used by