309 lines
7.2 KiB
JavaScript
309 lines
7.2 KiB
JavaScript
import {
|
|
eat,
|
|
finishToken,
|
|
getTokenFromCode,
|
|
IdentifierRole,
|
|
match,
|
|
next,
|
|
skipSpace,
|
|
Token,
|
|
} from "../../tokenizer/index";
|
|
import {TokenType as tt} from "../../tokenizer/types";
|
|
import {input, isTypeScriptEnabled, state} from "../../traverser/base";
|
|
import {parseExpression, parseMaybeAssign} from "../../traverser/expression";
|
|
import {expect, unexpected} from "../../traverser/util";
|
|
import {charCodes} from "../../util/charcodes";
|
|
import {IS_IDENTIFIER_CHAR, IS_IDENTIFIER_START} from "../../util/identifier";
|
|
import {tsTryParseJSXTypeArgument} from "../typescript";
|
|
|
|
// Reads inline JSX contents token.
|
|
function jsxReadToken() {
|
|
for (;;) {
|
|
if (state.pos >= input.length) {
|
|
unexpected("Unterminated JSX contents");
|
|
return;
|
|
}
|
|
|
|
const ch = input.charCodeAt(state.pos);
|
|
|
|
switch (ch) {
|
|
case charCodes.lessThan:
|
|
case charCodes.leftCurlyBrace:
|
|
if (state.pos === state.start) {
|
|
if (ch === charCodes.lessThan) {
|
|
state.pos++;
|
|
finishToken(tt.jsxTagStart);
|
|
return;
|
|
}
|
|
getTokenFromCode(ch);
|
|
return;
|
|
}
|
|
finishToken(tt.jsxText);
|
|
return;
|
|
|
|
default:
|
|
state.pos++;
|
|
}
|
|
}
|
|
}
|
|
|
|
function jsxReadString(quote) {
|
|
state.pos++;
|
|
for (;;) {
|
|
if (state.pos >= input.length) {
|
|
unexpected("Unterminated string constant");
|
|
return;
|
|
}
|
|
|
|
const ch = input.charCodeAt(state.pos);
|
|
if (ch === quote) {
|
|
state.pos++;
|
|
break;
|
|
}
|
|
state.pos++;
|
|
}
|
|
finishToken(tt.string);
|
|
}
|
|
|
|
// Read a JSX identifier (valid tag or attribute name).
|
|
//
|
|
// Optimized version since JSX identifiers can't contain
|
|
// escape characters and so can be read as single slice.
|
|
// Also assumes that first character was already checked
|
|
// by isIdentifierStart in readToken.
|
|
|
|
function jsxReadWord() {
|
|
let ch;
|
|
do {
|
|
if (state.pos > input.length) {
|
|
unexpected("Unexpectedly reached the end of input.");
|
|
return;
|
|
}
|
|
ch = input.charCodeAt(++state.pos);
|
|
} while (IS_IDENTIFIER_CHAR[ch] || ch === charCodes.dash);
|
|
finishToken(tt.jsxName);
|
|
}
|
|
|
|
// Parse next token as JSX identifier
|
|
function jsxParseIdentifier() {
|
|
nextJSXTagToken();
|
|
}
|
|
|
|
// Parse namespaced identifier.
|
|
function jsxParseNamespacedName(identifierRole) {
|
|
jsxParseIdentifier();
|
|
if (!eat(tt.colon)) {
|
|
// Plain identifier, so this is an access.
|
|
state.tokens[state.tokens.length - 1].identifierRole = identifierRole;
|
|
return;
|
|
}
|
|
// Process the second half of the namespaced name.
|
|
jsxParseIdentifier();
|
|
}
|
|
|
|
// Parses element name in any form - namespaced, member
|
|
// or single identifier.
|
|
function jsxParseElementName() {
|
|
jsxParseNamespacedName(IdentifierRole.Access);
|
|
while (match(tt.dot)) {
|
|
nextJSXTagToken();
|
|
jsxParseIdentifier();
|
|
}
|
|
}
|
|
|
|
// Parses any type of JSX attribute value.
|
|
function jsxParseAttributeValue() {
|
|
switch (state.type) {
|
|
case tt.braceL:
|
|
next();
|
|
jsxParseExpressionContainer();
|
|
nextJSXTagToken();
|
|
return;
|
|
|
|
case tt.jsxTagStart:
|
|
jsxParseElement();
|
|
nextJSXTagToken();
|
|
return;
|
|
|
|
case tt.string:
|
|
nextJSXTagToken();
|
|
return;
|
|
|
|
default:
|
|
unexpected("JSX value should be either an expression or a quoted JSX text");
|
|
}
|
|
}
|
|
|
|
function jsxParseEmptyExpression() {
|
|
// Do nothing.
|
|
}
|
|
|
|
// Parse JSX spread child, after already processing the {
|
|
// Does not parse the closing }
|
|
function jsxParseSpreadChild() {
|
|
expect(tt.ellipsis);
|
|
parseExpression();
|
|
}
|
|
|
|
// Parses JSX expression enclosed into curly brackets, after already processing the {
|
|
// Does not parse the closing }
|
|
function jsxParseExpressionContainer() {
|
|
if (match(tt.braceR)) {
|
|
jsxParseEmptyExpression();
|
|
} else {
|
|
parseExpression();
|
|
}
|
|
}
|
|
|
|
// Parses following JSX attribute name-value pair.
|
|
function jsxParseAttribute() {
|
|
if (eat(tt.braceL)) {
|
|
expect(tt.ellipsis);
|
|
parseMaybeAssign();
|
|
// }
|
|
nextJSXTagToken();
|
|
return;
|
|
}
|
|
jsxParseNamespacedName(IdentifierRole.ObjectKey);
|
|
if (match(tt.eq)) {
|
|
nextJSXTagToken();
|
|
jsxParseAttributeValue();
|
|
}
|
|
}
|
|
|
|
// Parses JSX opening tag starting after "<".
|
|
// Returns true if the tag was self-closing.
|
|
// Does not parse the last token.
|
|
function jsxParseOpeningElement() {
|
|
if (match(tt.jsxTagEnd)) {
|
|
// This is an open-fragment.
|
|
return false;
|
|
}
|
|
jsxParseElementName();
|
|
if (isTypeScriptEnabled) {
|
|
tsTryParseJSXTypeArgument();
|
|
}
|
|
while (!match(tt.slash) && !match(tt.jsxTagEnd) && !state.error) {
|
|
jsxParseAttribute();
|
|
}
|
|
const isSelfClosing = match(tt.slash);
|
|
if (isSelfClosing) {
|
|
// /
|
|
nextJSXTagToken();
|
|
}
|
|
return isSelfClosing;
|
|
}
|
|
|
|
// Parses JSX closing tag starting after "</".
|
|
// Does not parse the last token.
|
|
function jsxParseClosingElement() {
|
|
if (match(tt.jsxTagEnd)) {
|
|
// Fragment syntax, so we immediately have a tag end.
|
|
return;
|
|
}
|
|
jsxParseElementName();
|
|
}
|
|
|
|
// Parses entire JSX element, including its opening tag
|
|
// (starting after "<"), attributes, contents and closing tag.
|
|
// Does not parse the last token.
|
|
function jsxParseElementAt() {
|
|
const isSelfClosing = jsxParseOpeningElement();
|
|
if (!isSelfClosing) {
|
|
nextJSXExprToken();
|
|
while (true) {
|
|
switch (state.type) {
|
|
case tt.jsxTagStart:
|
|
nextJSXTagToken();
|
|
if (match(tt.slash)) {
|
|
nextJSXTagToken();
|
|
jsxParseClosingElement();
|
|
return;
|
|
}
|
|
jsxParseElementAt();
|
|
nextJSXExprToken();
|
|
break;
|
|
|
|
case tt.jsxText:
|
|
nextJSXExprToken();
|
|
break;
|
|
|
|
case tt.braceL:
|
|
next();
|
|
if (match(tt.ellipsis)) {
|
|
jsxParseSpreadChild();
|
|
nextJSXExprToken();
|
|
} else {
|
|
jsxParseExpressionContainer();
|
|
nextJSXExprToken();
|
|
}
|
|
|
|
break;
|
|
|
|
// istanbul ignore next - should never happen
|
|
default:
|
|
unexpected();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parses entire JSX element from current position.
|
|
// Does not parse the last token.
|
|
export function jsxParseElement() {
|
|
nextJSXTagToken();
|
|
jsxParseElementAt();
|
|
}
|
|
|
|
// ==================================
|
|
// Overrides
|
|
// ==================================
|
|
|
|
export function nextJSXTagToken() {
|
|
state.tokens.push(new Token());
|
|
skipSpace();
|
|
state.start = state.pos;
|
|
const code = input.charCodeAt(state.pos);
|
|
|
|
if (IS_IDENTIFIER_START[code]) {
|
|
jsxReadWord();
|
|
} else if (code === charCodes.quotationMark || code === charCodes.apostrophe) {
|
|
jsxReadString(code);
|
|
} else {
|
|
// The following tokens are just one character each.
|
|
++state.pos;
|
|
switch (code) {
|
|
case charCodes.greaterThan:
|
|
finishToken(tt.jsxTagEnd);
|
|
break;
|
|
case charCodes.lessThan:
|
|
finishToken(tt.jsxTagStart);
|
|
break;
|
|
case charCodes.slash:
|
|
finishToken(tt.slash);
|
|
break;
|
|
case charCodes.equalsTo:
|
|
finishToken(tt.eq);
|
|
break;
|
|
case charCodes.leftCurlyBrace:
|
|
finishToken(tt.braceL);
|
|
break;
|
|
case charCodes.dot:
|
|
finishToken(tt.dot);
|
|
break;
|
|
case charCodes.colon:
|
|
finishToken(tt.colon);
|
|
break;
|
|
default:
|
|
unexpected();
|
|
}
|
|
}
|
|
}
|
|
|
|
function nextJSXExprToken() {
|
|
state.tokens.push(new Token());
|
|
state.start = state.pos;
|
|
jsxReadToken();
|
|
}
|