tscircuit/ti

ti tscircuit library

Version
1.0.195
License
unset
Stars
0

system-block-ui/src/components/TsxCode.tsx

type TsxTokenKind =
  | "attribute"
  | "comment"
  | "keyword"
  | "number"
  | "operator"
  | "punctuation"
  | "string"
  | "tag"
  | "type";

export interface TsxToken {
  content: string;
  kind?: TsxTokenKind;
}

const KEYWORDS = new Set([
  "as",
  "async",
  "await",
  "break",
  "case",
  "catch",
  "class",
  "const",
  "continue",
  "default",
  "delete",
  "do",
  "else",
  "enum",
  "export",
  "extends",
  "false",
  "finally",
  "for",
  "from",
  "function",
  "if",
  "implements",
  "import",
  "in",
  "instanceof",
  "interface",
  "keyof",
  "let",
  "new",
  "null",
  "of",
  "return",
  "satisfies",
  "static",
  "super",
  "switch",
  "this",
  "throw",
  "true",
  "try",
  "type",
  "typeof",
  "undefined",
  "var",
  "void",
  "while",
  "with",
  "yield",
]);

const OPERATORS = [
  "===",
  "!==",
  ">>>",
  "**=",
  "=>",
  "==",
  "!=",
  "<=",
  ">=",
  "&&",
  "||",
  "??",
  "?.",
  "++",
  "--",
  "+=",
  "-=",
  "*=",
  "/=",
  "%=",
  "**",
  "<<",
  ">>",
  "=",
  "+",
  "-",
  "*",
  "/",
  "%",
  "!",
  "?",
  ":",
  "&",
  "|",
  "^",
  "~",
] as const;

const isIdentifierStart = (character: string): boolean =>
  /[A-Za-z_$]/.test(character);

const isIdentifierPart = (character: string): boolean =>
  /[A-Za-z0-9_$]/.test(character);

const readQuotedValue = (
  source: string,
  start: number,
  quote: string,
): number => {
  let cursor = start + 1;
  while (cursor < source.length) {
    if (source[cursor] === "\\") {
      cursor += 2;
      continue;
    }
    if (source[cursor] === quote) return cursor + 1;
    cursor += 1;
  }
  return source.length;
};

const appendToken = (
  tokens: TsxToken[],
  content: string,
  kind?: TsxTokenKind,
): void => {
  if (content.length === 0) return;
  const previous = tokens.at(-1);
  if (previous && previous.kind === kind) {
    previous.content += content;
    return;
  }
  tokens.push({ content, kind });
};

/**
 * A small deterministic TSX lexer for the generated source panel. Keeping the
 * source as text nodes means React performs all escaping for us.
 */
export const tokenizeTsx = (source: string): readonly TsxToken[] => {
  const tokens: TsxToken[] = [];
  let cursor = 0;
  let insideJsxTag = false;

  while (cursor < source.length) {
    const rest = source.slice(cursor);

    if (rest.startsWith("//")) {
      const newlineIndex = source.indexOf("\n", cursor + 2);
      const end = newlineIndex === -1 ? source.length : newlineIndex;
      appendToken(tokens, source.slice(cursor, end), "comment");
      cursor = end;
      continue;
    }

    if (rest.startsWith("/*")) {
      const closingIndex = source.indexOf("*/", cursor + 2);
      const end = closingIndex === -1 ? source.length : closingIndex + 2;
      appendToken(tokens, source.slice(cursor, end), "comment");
      cursor = end;
      continue;
    }

    const character = source[cursor] ?? "";
    if (character === '"' || character === "'" || character === "`") {
      const end = readQuotedValue(source, cursor, character);
      appendToken(tokens, source.slice(cursor, end), "string");
      cursor = end;
      continue;
    }

    const jsxTagMatch = rest.match(/^<(\/)?([A-Za-z][A-Za-z0-9:._$-]*)/);
    if (jsxTagMatch) {
      const prefix = jsxTagMatch[1] ? "</" : "<";
      const tagName = jsxTagMatch[2] ?? "";
      appendToken(tokens, prefix, "punctuation");
      appendToken(tokens, tagName, "tag");
      cursor += prefix.length + tagName.length;
      insideJsxTag = true;
      continue;
    }

    if (insideJsxTag && rest.startsWith("/>")) {
      appendToken(tokens, "/>", "punctuation");
      cursor += 2;
      insideJsxTag = false;
      continue;
    }

    if (insideJsxTag && character === ">") {
      appendToken(tokens, character, "punctuation");
      cursor += 1;
      insideJsxTag = false;
      continue;
    }

    const numberMatch = rest.match(
      /^(?:0[xob][\da-f]+|(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)/i,
    );
    if (numberMatch) {
      const value = numberMatch[0];
      appendToken(tokens, value, "number");
      cursor += value.length;
      continue;
    }

    if (isIdentifierStart(character)) {
      let end = cursor + 1;
      while (end < source.length && isIdentifierPart(source[end] ?? "")) {
        end += 1;
      }
      const identifier = source.slice(cursor, end);
      const kind = insideJsxTag
        ? "attribute"
        : KEYWORDS.has(identifier)
          ? "keyword"
          : /^[A-Z]/.test(identifier)
            ? "type"
            : undefined;
      appendToken(tokens, identifier, kind);
      cursor = end;
      continue;
    }

    const operator = OPERATORS.find((candidate) => rest.startsWith(candidate));
    if (operator) {
      appendToken(tokens, operator, "operator");
      cursor += operator.length;
      continue;
    }

    if ("{}[](),;.".includes(character)) {
      appendToken(tokens, character, "punctuation");
      cursor += 1;
      continue;
    }

    appendToken(tokens, character);
    cursor += 1;
  }

  return tokens;
};

interface TsxCodeProps {
  source: string;
}

export function TsxCode({ source }: TsxCodeProps) {
  const tokens = tokenizeTsx(source);
  let tokenOffset = 0;
  const renderedTokens = tokens.map((token) => {
    const key = `${tokenOffset}-${token.kind ?? "plain"}`;
    tokenOffset += token.content.length;
    return token.kind ? (
      <span className={`syntax-token syntax-${token.kind}`} key={key}>
        {token.content}
      </span>
    ) : (
      token.content
    );
  });

  return (
    // biome-ignore lint/a11y/useSemanticElements: A textarea cannot preserve token-level syntax markup.
    <div
      aria-label="Generated TSX source code"
      aria-multiline="true"
      aria-readonly="true"
      className="code-view"
      role="textbox"
      tabIndex={0}
    >
      <pre>
        <code>{renderedTokens}</code>
      </pre>
    </div>
  );
}