273 lines
6.2 KiB
TypeScript

/// <reference path="../../../lib/third-party-libs.d.ts" />
import type { Token, JSXToken } from "js-tokens";
import jsTokens from "js-tokens";
import {
isStrictReservedWord,
isKeyword,
} from "@babel/helper-validator-identifier";
import Chalk from "chalk";
type ChalkClass = ReturnType<typeof getChalk>;
/**
* Names that are always allowed as identifiers, but also appear as keywords
* within certain syntactic productions.
*
* https://tc39.es/ecma262/#sec-keywords-and-reserved-words
*
* `target` has been omitted since it is very likely going to be a false
* positive.
*/
const sometimesKeywords = new Set(["as", "async", "from", "get", "of", "set"]);
type InternalTokenType =
| "keyword"
| "capitalized"
| "jsxIdentifier"
| "punctuator"
| "number"
| "string"
| "regex"
| "comment"
| "invalid";
/**
* Chalk styles for token types.
*/
function getDefs(chalk: ChalkClass): Record<InternalTokenType, ChalkClass> {
return {
keyword: chalk.cyan,
capitalized: chalk.yellow,
jsxIdentifier: chalk.yellow,
punctuator: chalk.yellow,
number: chalk.magenta,
string: chalk.green,
regex: chalk.magenta,
comment: chalk.grey,
invalid: chalk.white.bgRed.bold,
};
}
/**
* RegExp to test for newlines in terminal.
*/
const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
/**
* RegExp to test for the three types of brackets.
*/
const BRACKET = /^[()[\]{}]$/;
let tokenize: (
text: string,
) => Generator<{ type: InternalTokenType | "uncolored"; value: string }>;
if (process.env.BABEL_8_BREAKING) {
/**
* Get the type of token, specifying punctuator type.
*/
const getTokenType = function (
token: Token | JSXToken,
): InternalTokenType | "uncolored" {
if (token.type === "IdentifierName") {
if (
isKeyword(token.value) ||
isStrictReservedWord(token.value, true) ||
sometimesKeywords.has(token.value)
) {
return "keyword";
}
if (token.value[0] !== token.value[0].toLowerCase()) {
return "capitalized";
}
}
if (token.type === "Punctuator" && BRACKET.test(token.value)) {
return "uncolored";
}
if (
token.type === "Invalid" &&
(token.value === "@" || token.value === "#")
) {
return "punctuator";
}
switch (token.type) {
case "NumericLiteral":
return "number";
case "StringLiteral":
case "JSXString":
case "NoSubstitutionTemplate":
return "string";
case "RegularExpressionLiteral":
return "regex";
case "Punctuator":
case "JSXPunctuator":
return "punctuator";
case "MultiLineComment":
case "SingleLineComment":
return "comment";
case "Invalid":
case "JSXInvalid":
return "invalid";
case "JSXIdentifier":
return "jsxIdentifier";
default:
return "uncolored";
}
};
/**
* Turn a string of JS into an array of objects.
*/
tokenize = function* (text: string) {
for (const token of jsTokens(text, { jsx: true })) {
switch (token.type) {
case "TemplateHead":
yield { type: "string", value: token.value.slice(0, -2) };
yield { type: "punctuator", value: "${" };
break;
case "TemplateMiddle":
yield { type: "punctuator", value: "}" };
yield { type: "string", value: token.value.slice(1, -2) };
yield { type: "punctuator", value: "${" };
break;
case "TemplateTail":
yield { type: "punctuator", value: "}" };
yield { type: "string", value: token.value.slice(1) };
break;
default:
yield {
type: getTokenType(token),
value: token.value,
};
}
}
};
} else {
/**
* RegExp to test for what seems to be a JSX tag name.
*/
const JSX_TAG = /^[a-z][\w-]*$/i;
const getTokenType = function (token, offset, text) {
if (token.type === "name") {
if (
isKeyword(token.value) ||
isStrictReservedWord(token.value, true) ||
sometimesKeywords.has(token.value)
) {
return "keyword";
}
if (
JSX_TAG.test(token.value) &&
(text[offset - 1] === "<" || text.substr(offset - 2, 2) == "</")
) {
return "jsxIdentifier";
}
if (token.value[0] !== token.value[0].toLowerCase()) {
return "capitalized";
}
}
if (token.type === "punctuator" && BRACKET.test(token.value)) {
return "bracket";
}
if (
token.type === "invalid" &&
(token.value === "@" || token.value === "#")
) {
return "punctuator";
}
return token.type;
};
tokenize = function* (text: string) {
let match;
while ((match = (jsTokens as any).default.exec(text))) {
const token = (jsTokens as any).matchToToken(match);
yield {
type: getTokenType(token, match.index, text),
value: token.value,
};
}
};
}
/**
* Highlight `text` using the token definitions in `defs`.
*/
function highlightTokens(defs: Record<string, ChalkClass>, text: string) {
let highlighted = "";
for (const { type, value } of tokenize(text)) {
const colorize = defs[type];
if (colorize) {
highlighted += value
.split(NEWLINE)
.map(str => colorize(str))
.join("\n");
} else {
highlighted += value;
}
}
return highlighted;
}
/**
* Highlight `text` using the token definitions in `defs`.
*/
type Options = {
forceColor?: boolean;
};
/**
* Whether the code should be highlighted given the passed options.
*/
export function shouldHighlight(options: Options): boolean {
return !!Chalk.supportsColor || options.forceColor;
}
/**
* The Chalk instance that should be used given the passed options.
*/
export function getChalk(options: Options) {
return options.forceColor
? new Chalk.constructor({ enabled: true, level: 1 })
: Chalk;
}
/**
* Highlight `code`.
*/
export default function highlight(code: string, options: Options = {}): string {
if (shouldHighlight(options)) {
const chalk = getChalk(options);
const defs = getDefs(chalk);
return highlightTokens(defs, code);
} else {
return code;
}
}