[babel 8] Improve syntax highlighting (#12660)

Co-authored-by: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>
Co-authored-by: Simon Lydell <simon.lydell@gmail.com>
This commit is contained in:
Nicolò Ribaudo 2021-01-22 17:56:20 +01:00 committed by GitHub
parent 22eb99bea4
commit 6a961497aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 218 additions and 42 deletions

View File

@ -283,6 +283,36 @@ describe("@babel/code-frame", function () {
); );
}); });
test("jsx", function () {
const gutter = chalk.grey;
const yellow = chalk.yellow;
const rawLines = ["<div />"].join("\n");
expect(
JSON.stringify(
codeFrame(rawLines, 0, null, {
linesAbove: 1,
linesBelow: 1,
forceColor: true,
}),
),
).toEqual(
JSON.stringify(
chalk.reset(
" " +
gutter(" 1 |") +
" " +
yellow("<") +
yellow("div") +
" " +
yellow("/") +
yellow(">"),
),
),
);
});
test("basic usage, new API", function () { test("basic usage, new API", function () {
const rawLines = ["class Foo {", " constructor()", "};"].join("\n"); const rawLines = ["class Foo {", " constructor()", "};"].join("\n");
expect( expect(

View File

@ -17,7 +17,7 @@
"dependencies": { "dependencies": {
"@babel/helper-validator-identifier": "workspace:^7.10.4", "@babel/helper-validator-identifier": "workspace:^7.10.4",
"chalk": "^2.0.0", "chalk": "^2.0.0",
"js-tokens": "^4.0.0" "js-tokens": "condition:BABEL_8_BREAKING ? ^6.0.0 : ^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"strip-ansi": "^4.0.0" "strip-ansi": "^4.0.0"

View File

@ -1,7 +1,21 @@
import jsTokens, { matchToToken } from "js-tokens"; import jsTokens, * as jsTokensNs from "js-tokens";
import { isReservedWord, isKeyword } from "@babel/helper-validator-identifier"; import {
isStrictReservedWord,
isKeyword,
} from "@babel/helper-validator-identifier";
import Chalk from "chalk"; import Chalk from "chalk";
/**
* 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"]);
/** /**
* Chalk styles for token types. * Chalk styles for token types.
*/ */
@ -9,9 +23,8 @@ function getDefs(chalk) {
return { return {
keyword: chalk.cyan, keyword: chalk.cyan,
capitalized: chalk.yellow, capitalized: chalk.yellow,
jsx_tag: chalk.yellow, jsxIdentifier: chalk.yellow,
punctuator: chalk.yellow, punctuator: chalk.yellow,
// bracket: intentionally omitted.
number: chalk.magenta, number: chalk.magenta,
string: chalk.green, string: chalk.green,
regex: chalk.magenta, regex: chalk.magenta,
@ -25,25 +38,121 @@ function getDefs(chalk) {
*/ */
const NEWLINE = /\r\n|[\n\r\u2028\u2029]/; const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
/**
* RegExp to test for what seems to be a JSX tag name.
*/
const JSX_TAG = /^[a-z][\w-]*$/i;
/** /**
* RegExp to test for the three types of brackets. * RegExp to test for the three types of brackets.
*/ */
const BRACKET = /^[()[\]{}]$/; const BRACKET = /^[()[\]{}]$/;
/** let tokenize;
if (process.env.BABEL_8_BREAKING) {
/**
* Get the type of token, specifying punctuator type. * Get the type of token, specifying punctuator type.
*/ */
function getTokenType(match) { const getTokenType = function (token) {
const [offset, text] = match.slice(-2); if (token.type === "IdentifierName") {
const token = matchToToken(match); 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 {
// This is only available in js-tokens@4, and not in js-tokens@6
const { matchToToken } = jsTokensNs;
/**
* 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 (token.type === "name") {
if (isKeyword(token.value) || isReservedWord(token.value)) { if (
isKeyword(token.value) ||
isStrictReservedWord(token.value, true) ||
sometimesKeywords.has(token.value)
) {
return "keyword"; return "keyword";
} }
@ -51,7 +160,7 @@ function getTokenType(match) {
JSX_TAG.test(token.value) && JSX_TAG.test(token.value) &&
(text[offset - 1] === "<" || text.substr(offset - 2, 2) == "</") (text[offset - 1] === "<" || text.substr(offset - 2, 2) == "</")
) { ) {
return "jsx_tag"; return "jsxIdentifier";
} }
if (token.value[0] !== token.value[0].toLowerCase()) { if (token.value[0] !== token.value[0].toLowerCase()) {
@ -71,26 +180,46 @@ function getTokenType(match) {
} }
return token.type; return token.type;
};
tokenize = function* (text: string) {
let match;
while ((match = jsTokens.exec(text))) {
const token = matchToToken(match);
yield {
type: getTokenType(token, match.index, text),
value: token.value,
};
}
};
} }
/** /**
* Highlight `text` using the token definitions in `defs`. * Highlight `text` using the token definitions in `defs`.
*/ */
function highlightTokens(defs: Object, text: string) { function highlightTokens(defs: Object, text: string) {
return text.replace(jsTokens, function (...args) { let highlighted = "";
const type = getTokenType(args);
for (const { type, value } of tokenize(text)) {
const colorize = defs[type]; const colorize = defs[type];
if (colorize) { if (colorize) {
return args[0] highlighted += value
.split(NEWLINE) .split(NEWLINE)
.map(str => colorize(str)) .map(str => colorize(str))
.join("\n"); .join("\n");
} else { } else {
return args[0]; highlighted += value;
} }
}); }
return highlighted;
} }
/**
* Highlight `text` using the token definitions in `defs`.
*/
type Options = { type Options = {
forceColor?: boolean, forceColor?: boolean,
}; };

View File

@ -858,7 +858,7 @@ __metadata:
dependencies: dependencies:
"@babel/helper-validator-identifier": "workspace:^7.10.4" "@babel/helper-validator-identifier": "workspace:^7.10.4"
chalk: ^2.0.0 chalk: ^2.0.0
js-tokens: ^4.0.0 js-tokens: "condition:BABEL_8_BREAKING ? ^6.0.0 : ^4.0.0"
strip-ansi: ^4.0.0 strip-ansi: ^4.0.0
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@ -9060,13 +9060,30 @@ fsevents@^1.2.7:
languageName: node languageName: node
linkType: hard linkType: hard
"js-tokens@npm:^4.0.0": "js-tokens-BABEL_8_BREAKING-false@npm:js-tokens@^4.0.0, js-tokens@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "js-tokens@npm:4.0.0" resolution: "js-tokens@npm:4.0.0"
checksum: 1fc4e4667ac2d972aba65148b9cbf9c17566b2394d3504238d8492bbd3e68f496c657eab06b26b40b17db5cac0a34d153a12130e2d2d2bb6dc2cdc8a4764eb1b checksum: 1fc4e4667ac2d972aba65148b9cbf9c17566b2394d3504238d8492bbd3e68f496c657eab06b26b40b17db5cac0a34d153a12130e2d2d2bb6dc2cdc8a4764eb1b
languageName: node languageName: node
linkType: hard linkType: hard
"js-tokens-BABEL_8_BREAKING-true@npm:js-tokens@^6.0.0":
version: 6.0.0
resolution: "js-tokens@npm:6.0.0"
checksum: 975859a4fd68cbaaabf106639df316e662b87b296afa9c6b00cfd25bc7642137433d18bf78e1e5578fc63c2a3e7334aad4fbed47f87c6c29f9a4f6760e79e322
languageName: node
linkType: hard
"js-tokens@condition:BABEL_8_BREAKING ? ^6.0.0 : ^4.0.0":
version: 0.0.0-condition-bceac3
resolution: "js-tokens@condition:BABEL_8_BREAKING?^6.0.0:^4.0.0#bceac3"
dependencies:
js-tokens-BABEL_8_BREAKING-false: "npm:js-tokens@^4.0.0"
js-tokens-BABEL_8_BREAKING-true: "npm:js-tokens@^6.0.0"
checksum: 036166b3ba76e31549eeb404d986ff5b1af55f91137bbcc6d5147b1e4c8d4c74f01d9aae10cf5d5221e60f3bcef98e7460bbf2a54a9e7b47d3b63789b11297e3
languageName: node
linkType: hard
"js-yaml@npm:^3.13.1, js-yaml@npm:^3.2.1": "js-yaml@npm:^3.13.1, js-yaml@npm:^3.2.1":
version: 3.13.1 version: 3.13.1
resolution: "js-yaml@npm:3.13.1" resolution: "js-yaml@npm:3.13.1"