futurescript
Version:
A functional style, but highly readable language that compiles to JavaScript.
1,385 lines (1,328 loc) • 63 kB
JavaScript
// `isJoint=true` means the token can't be the start or end of something.
// That is, it must act as a "joint".
import * as $tools from "./tools.mjs";
import * as $lockedApi from "../locked-api.mjs";
export class Lex {
// Both `raw` and `path` are optional, but at least one should exist.
// If `raw` is absent, it will return a promise rather than an instance.
constructor(raw, path) {
let internalConstructor = () => {
this.path = path;
this.raw = raw;
// In fact this is "round 0". Source map and error message need line and column.
// Note: "\n" may be in strings. We must add all "\n".
let rawLinePositions = [0];
for (let i = 0; i < raw.length; i++) {
if (raw[i] === "\n") {
rawLinePositions.push(i + 1);
}
}
this._rawLinePositions = rawLinePositions;
this._round1();
this._round2();
this._round3();
this._round4();
this._round5();
this._round6();
this._round7();
this._round8();
this._round9();
this._round10();
this._round11();
this._round12();
this._round13();
this._round14();
this._round15();
this._round16();
this._round17();
this._round18();
this._round19();
this._round20();
this._round21();
this._round22();
this._round23();
};
if (raw === undefined) {
return new Promise((resolve, reject) => {(async () => {try {
raw = await $lockedApi.readTextFile(path);
internalConstructor();
resolve(this);
} catch (ex) {reject(ex);}})();});
}
else {
internalConstructor();
}
}
_round1() {
let raw = this.raw;
// Note: we can only append things to the raw end. If we modify raw's middle,
// raw positions are lost.
if (!raw.endsWith("\n")) {
raw += "\n";
}
let result = [];
let pendingToken = null;
let versionDirectiveInBraces = false;
let numberForcesInt = false;
let numberIsHex = false;
let stringType = null;
let stringInterpolationParenthesisLevel = null;
let indentValue = null;
let stringIndentValue = null;
let stringIndentValueRemaining = null;
let stringIndentFinished = false;
let stringLine = {content: ""};
let postQuote = null;
let i = 0;
while (i < raw.length) {
let char = raw[i];
// process length-unfixed tokens
if (pendingToken instanceof VersionDirective) {
if (char === "{") {
versionDirectiveInBraces = true;
pendingToken.value += char;
i++;
}
else if (char === "}") {
versionDirectiveInBraces = false;
pendingToken.value += char;
i++;
}
else if (!versionDirectiveInBraces && (char === "\r" || char === "\n")) {
result.push(pendingToken.end(i - 1));
pendingToken = null;
}
else {
pendingToken.value += char;
i++;
}
}
else if (pendingToken instanceof Indent) {
if (char === " " || char === "\t") {
pendingToken.value++;
i++;
}
else {
indentValue = pendingToken.value;
result.push(pendingToken.end(i - 1));
pendingToken = null;
}
}
else if (pendingToken instanceof Num) {
if (!numberIsHex && char === "e" && (raw[i + 1] === "+" || raw[i + 1] === "-")) {
pendingToken.value += "e" + raw[i + 1];
i += 2;
}
else if ((
numberForcesInt ?
char.search(/[0-9A-Fa-fx]/) :
char.search(/[0-9A-Fa-fx.]/)
) !== -1) {
pendingToken.value += char;
i++;
}
else {
result.push(pendingToken.end(i - 1));
pendingToken = null;
numberForcesInt = false;
numberIsHex = false;
}
}
else if (pendingToken instanceof Str) {
let indentOffset = stringIndentValue - indentValue;
if (postQuote !== null) {
if (char.search(/[a-z]/) !== -1) {
postQuote.value += char;
i++;
}
else {
result.push(postQuote.end(i - 1));
result.push(new PseudoCallRightParenthesis());
postQuote = null;
pendingToken = null;
}
}
else if (!(char === "\"" && (
stringType.isInline || (!stringType.isInline && indentOffset === 0)
))) {
let interpolationChar = null;
if (stringType === InlineNormalString || stringType === FormattedNormalString) {
interpolationChar = "\\";
}
else if (stringType === InlineRegex || stringType === FormattedRegex) {
interpolationChar = "#";
}
let quoteEscapeEnabled =
stringType === InlineNormalString ||
stringType === FormattedNormalString ||
stringType === InlineRegex;
let joinEscapeEnabled =
stringType === InlineNormalString ||
stringType === FormattedNormalString ||
stringType === InlineRegex ||
stringType === FormattedRegex;
if (char === interpolationChar && raw[i + 1] === "(") {
stringLine.indentOffset = indentOffset;
stringLine.isJoin = false;
pendingToken.value.push(stringLine);
result.push(pendingToken.end(i - 1));
result.push(new Plus());
result.push(new NormalLeftParenthesis().start(i).end(i + 1));
pendingToken = null;
stringLine = {content: ""};
stringInterpolationParenthesisLevel = 1;
stringIndentFinished = true;
i += 2;
}
else if (
quoteEscapeEnabled &&
char === "\\" && raw[i + 1] === "\""
) {
stringLine.content += "\\\"";
stringIndentFinished = true;
i += 2;
}
else if (
joinEscapeEnabled &&
char === "\\" && (raw[i + 1] === "\r" || raw[i + 1] === "\n")
) {
stringLine.indentOffset = indentOffset;
stringLine.isJoin = true;
pendingToken.value.push(stringLine);
stringLine = {content: ""};
stringIndentValue = 0;
stringIndentFinished = false;
i += 2;
}
else if (
stringIndentValue !== null &&
(char === " " || char === "\t") &&
!stringIndentFinished
) {
stringIndentValue++;
i++;
}
else if (char === "\r" || char === "\n") {
stringLine.indentOffset = indentOffset;
stringLine.isJoin = false;
pendingToken.value.push(stringLine);
stringLine = {content: ""};
stringIndentValue = 0;
stringIndentFinished = false;
i++;
}
else {
stringLine.content += char;
stringIndentFinished = true;
i++;
}
}
else {
stringLine.indentOffset = indentOffset;
stringLine.isJoin = false;
pendingToken.value.push(stringLine);
pendingToken.isLast = true;
result.push(pendingToken.end(i - 1));
if (raw[i + 1].search(/[a-z]/) !== -1) {
result.push(new Comma());
postQuote = new PostQuote("").start(i + 1);
}
else {
result.push(new PseudoCallRightParenthesis());
pendingToken = null;
}
stringType = null;
stringIndentValue = null;
stringLine = {content: ""};
stringIndentFinished = false;
i++;
}
}
else if (pendingToken instanceof InlineComment) {
if (char !== "\r" && char !== "\n") {
pendingToken.value += char;
i++;
}
else {
result.push(pendingToken.end(i - 1));
pendingToken = null;
}
}
else if (pendingToken instanceof FormattedComment) {
if (!(char === "#" && raw[i + 1] === "#" && raw[i + 2] === "#")) {
pendingToken.value += char;
i++;
}
else {
result.push(pendingToken.end(i + 2));
pendingToken = null;
i += 3;
}
}
else if (pendingToken instanceof NormalToken) {
if (char.search(/[A-Za-z0-9_$]/) !== -1) {
pendingToken.value += char;
i++;
}
else {
result.push(pendingToken.end(i - 1));
pendingToken = null;
}
}
// characters that won't be processed
else if (char === " ") {
i++;
}
else if (char === "\r" && raw[i + 1] === "\n") {
i++;
}
// recognize length-unfixed tokens: version directive
else if (i === 0) {
pendingToken = new VersionDirective(char).start(i);
i++;
}
// recognize length-unfixed tokens: three characters
else if (char === "j" && raw[i + 1] === "s" && raw[i + 2] === "\"") {
if (raw[i + 3] === "\r" || raw[i + 3] === "\n") {
stringType = FormattedJs;
result.push(new FormattedJs().start(i).end(i + 1));
}
else {
stringType = InlineJs;
result.push(new InlineJs().start(i).end(i + 1));
}
result.push(new PseudoCallLeftParenthesis());
pendingToken = new Str([]).start(i + 3);
pendingToken.type = stringType;
pendingToken.isFirst = true;
stringIndentValue = indentValue;
i += 3;
}
else if (char === "#" && raw[i + 1] === "#" && raw[i + 2] === "#") {
pendingToken = new FormattedComment("").start(i);
i += 3;
}
// recognize length-unfixed tokens: double character
else if (char === "v" && raw[i + 1] === "\"") {
if (raw[i + 2] === "\r" || raw[i + 2] === "\n") {
stringType = FormattedVerbatimString;
result.push(new FormattedVerbatimString().start(i).end(i));
}
else {
stringType = InlineVerbatimString;
result.push(new InlineVerbatimString().start(i).end(i));
}
result.push(new PseudoCallLeftParenthesis());
pendingToken = new Str([]).start(i + 2);
pendingToken.type = stringType;
pendingToken.isFirst = true;
stringIndentValue = indentValue;
i += 2;
}
else if (char === "r" && raw[i + 1] === "\"") {
if (raw[i + 2] === "\r" || raw[i + 2] === "\n") {
stringType = FormattedRegex;
result.push(new FormattedRegex().start(i).end(i));
}
else {
stringType = InlineRegex;
result.push(new InlineRegex().start(i).end(i));
}
result.push(new PseudoCallLeftParenthesis());
pendingToken = new Str([]).start(i + 2);
pendingToken.type = stringType;
pendingToken.isFirst = true;
stringIndentValue = indentValue;
i += 2;
}
// recognize length-fixed tokens: double character
else if (char === "<" && raw[i + 1] === "<") {
result.push(new LeftChevron().start(i).end(i + 1));
i += 2;
}
else if (char === ">" && raw[i + 1] === ">") {
result.push(new RightChevron().start(i).end(i + 1));
i += 2;
}
else if (char === "-" && raw[i + 1] === "-") {
result.push(new DashFunction().start(i).end(i + 1));
i += 2;
}
else if (char === "-" && raw[i + 1] === ">") {
result.push(new ArrowFunction().start(i).end(i + 1));
i += 2;
}
else if (char === "<" && raw[i + 1] === ">") {
result.push(new DiamondFunction().start(i).end(i + 1));
i += 2;
}
else if (char === "|" && raw[i + 1] === ">") {
result.push(new Pipe().start(i).end(i + 1));
i += 2;
}
else if (char === ":" && raw[i + 1] === ":") {
result.push(new FatDot().start(i).end(i + 1));
i += 2;
}
else if (char === "/" && raw[i + 1] === "=") {
result.push(new NotEqual().start(i).end(i + 1));
i += 2;
}
else if (char === "<" && raw[i + 1] === "=") {
result.push(new LessThanOrEqual().start(i).end(i + 1));
i += 2;
}
else if (char === ">" && raw[i + 1] === "=") {
result.push(new GreaterThanOrEqual().start(i).end(i + 1));
i += 2;
}
else if (char === "*" && raw[i + 1] === "*") {
result.push(new Power().start(i).end(i + 1));
i += 2;
}
// recognize length-unfixed tokens: single character
else if (char === "\n") {
pendingToken = new Indent(0).start(i + 1);
i++;
}
else if (char.search(/[0-9]/) !== -1) {
pendingToken = new Num(char).start(i);
if (raw[i - 1] === "." || raw[i - 1] === "@") {
numberForcesInt = true;
}
if (char === "0" && raw[i + 1] === "x") {
numberIsHex = true;
}
i++;
}
else if (char === "\"") {
if (raw[i + 1] === "\r" || raw[i + 1] === "\n") {
stringType = FormattedNormalString;
result.push(new FormattedNormalString());
}
else {
stringType = InlineNormalString;
result.push(new InlineNormalString());
}
result.push(new PseudoCallLeftParenthesis());
pendingToken = new Str([]).start(i + 1);
pendingToken.type = stringType;
pendingToken.isFirst = true;
stringIndentValue = indentValue;
i++;
}
else if (char === "#") {
pendingToken = new InlineComment("").start(i);
i++;
}
else if (char.search(/[A-Za-z_$]/) !== -1) {
pendingToken = new NormalToken(char).start(i);
i++;
}
// recognize length-fixed tokens: single character
else if (char === ";") {
result.push(new Semicolon().start(i).end(i));
i++;
}
else if (char === ":") {
result.push(new Colon().start(i).end(i));
i++;
}
else if (char === ",") {
result.push(new Comma().start(i).end(i));
i++;
}
else if (char === ".") {
result.push(new Dot().start(i).end(i));
i++;
}
else if (char === "(") {
if (stringType !== null) {
stringInterpolationParenthesisLevel++;
}
if (raw[i - 1].search(/["')@A-Za-z0-9_$]/) !== -1) {
result.push(new CallLeftParenthesis().start(i).end(i));
}
else {
result.push(new NormalLeftParenthesis().start(i).end(i));
}
i++;
}
else if (char === ")") {
if (stringType !== null) {
stringInterpolationParenthesisLevel--;
}
if (stringType !== null && stringInterpolationParenthesisLevel === 0) {
result.push(new RightParenthesis().start(i).end(i));
result.push(new Plus());
stringInterpolationParenthesisLevel = null;
pendingToken = new Str([]).start(i + 1);
pendingToken.type = stringType;
}
else {
result.push(new RightParenthesis().start(i).end(i));
}
i++;
}
else if (char === "[") {
if (raw[i - 1].search(/["')@A-Za-z0-9_$]/) !== -1) {
result.push(new CallLeftBracket().start(i).end(i));
}
else {
result.push(new NormalLeftBracket().start(i).end(i));
}
i++;
}
else if (char === "]") {
result.push(new RightBracket().start(i).end(i));
i++;
}
else if (char === "{") {
if (raw[i - 1].search(/["')@A-Za-z0-9_$]/) !== -1) {
result.push(new CallLeftBrace().start(i).end(i));
}
else {
result.push(new NormalLeftBrace().start(i).end(i));
}
i++;
}
else if (char === "}") {
result.push(new RightBrace().start(i).end(i));
i++;
}
else if (char === "=") {
result.push(new Equal().start(i).end(i));
i++;
}
else if (char === "≠") {
result.push(new NotEqual().start(i).end(i));
i++;
}
else if (char === "<") {
result.push(new LessThan().start(i).end(i));
i++;
}
else if (char === ">") {
result.push(new GreaterThan().start(i).end(i));
i++;
}
else if (char === "≤") {
result.push(new LessThanOrEqual().start(i).end(i));
i++;
}
else if (char === "≥") {
result.push(new GreaterThanOrEqual().start(i).end(i));
i++;
}
else if (char === "+") {
if (
raw[i - 1].search(/[")\]}@A-Za-z0-9_$]/) === -1 &&
raw[i + 1] !== " " && raw[i + 1] !== "\t" &&
raw[i + 1] !== "\r" && raw[i + 1] !== "\n"
) {
result.push(new Positive().start(i).end(i));
}
else {
result.push(new Plus().start(i).end(i));
}
i++;
}
else if (char === "-") {
if (
raw[i - 1].search(/[")\]}@A-Za-z0-9_$]/) === -1 &&
raw[i + 1] !== " " && raw[i + 1] !== "\t" &&
raw[i + 1] !== "\r" && raw[i + 1] !== "\n"
) {
result.push(new Negative().start(i).end(i));
}
else {
result.push(new Minus().start(i).end(i));
}
i++;
}
else if (char === "*") {
result.push(new Times().start(i).end(i));
i++;
}
else if (char === "/") {
result.push(new Over().start(i).end(i));
i++;
}
else if (char === "?") {
result.push(new Then().start(i).end(i));
i++;
}
else if (char === "|") {
result.push(new Else().start(i).end(i));
i++;
}
else if (char === "@") {
result.push(new Arg().start(i).end(i));
if (raw[i + 1].search(/[A-Za-z0-9_$]/) !== -1) {
result.push(new Dot());
}
i++;
}
else if (char === "'") {
if (raw[i + 1].search(/[A-Za-z0-9]/) !== -1) {
result.push(new NormalVariant().start(i).end(i));
}
else {
result.push(new FunctionVariant().start(i).end(i));
}
i++;
}
else if (char === "\\" && (raw[i + 1] === "\r" || raw[i + 1] === "\n")) {
result.push(new Join().start(i).end(i));
i++;
}
else {
throw new CharacterError([i, i], this.raw);
}
}
this.value = result;
}
// Check syntax error and let right-hand pair parts be more accurate.
// For example "CallLeftParenthesis, ... , RightParenthesis" will be normalized to
// "CallLeftParenthesis, ... , CallRightParenthesis".
_round2() {
let newValue = [];
let stack = [];
let opposite = left => {
if (left === LeftParenthesis) {
return RightParenthesis;
}
else if (left === NormalLeftParenthesis) {
return NormalRightParenthesis;
}
else if (left === CallLeftParenthesis) {
return CallRightParenthesis;
}
else if (left === PseudoCallLeftParenthesis) {
return PseudoCallRightParenthesis;
}
else if (left === LeftBracket) {
return RightBracket;
}
else if (left === NormalLeftBracket) {
return NormalRightBracket;
}
else if (left === CallLeftBracket) {
return CallRightBracket;
}
else if (left === LeftBrace) {
return RightBrace;
}
else if (left === NormalLeftBrace) {
return NormalRightBrace;
}
else if (left === CallLeftBrace) {
return CallRightBrace;
}
else if (left === LeftChevron) {
return RightChevron;
}
else {
throw new Error("No opposite.");
}
};
this.value.forEach((token, index) => {
if (
token instanceof LeftParenthesis ||
token instanceof LeftBracket ||
token instanceof LeftBrace ||
token instanceof LeftChevron
) {
stack.push(opposite(token.constructor));
newValue.push(token);
}
else if (
token instanceof RightParenthesis ||
token instanceof RightBracket ||
token instanceof RightBrace ||
token instanceof RightChevron
) {
if (token.constructor === stack[stack.length - 1]) {
newValue.push(token);
stack.pop();
}
else if ($tools.classIsClass(stack[stack.length - 1], token.constructor)) {
newValue.push(new stack[stack.length - 1]().copyPositionFrom(token));
stack.pop();
}
else {
throw new PunctuationPairError(this.part(index, index));
}
}
else {
newValue.push(token);
}
});
if (stack.length > 0) {
throw new PunctuationPairError(this.part(this.count() - 1, this.count() - 1));
}
this.value = newValue;
}
_round3() {
let min = null;
let arr = null;
this.value.forEach((token, index) => {
if (token instanceof Str && !token.type.isInline) {
if (token.isFirst) {
min = 9999;
arr = [];
}
let start = token.isFirst ? 1 : 0;
let end = token.isLast ? token.value.length - 1 : token.value.length;
for (let i = start; i < end; i++) {
if (token.value[i].content.trim() !== "" && token.value[i].indentOffset < min) {
min = token.value[i].indentOffset;
}
}
arr.push(token);
if (token.isLast) {
for (let i = 0; i < arr.length; i++) {
arr[i].minIndentOffset = min;
}
}
}
});
}
_round4() {
let newValue = [];
this.value.forEach((token, index) => {
if (token instanceof Str) {
if (token.type.isInline) {
let s = "";
for (let i = 0; i < token.value.length; i++) {
s += " ".repeat(token.value[i].indentOffset) + token.value[i].content;
}
newValue.push(token.replaceWith(s));
}
else {
let arr = [];
let start = token.isFirst ? 1 : 0;
let end = token.isLast ? token.value.length - 1 : token.value.length;
for (let i = start; i < end; i++) {
let spaceCount = token.value[i].indentOffset - token.minIndentOffset;
if (spaceCount < 0) { // empty line may make it negative
spaceCount = 0;
}
let s = " ".repeat(spaceCount) + token.value[i].content;
if (i > 0 && token.value[i - 1].isJoin && arr.length > 0) {
arr[arr.length - 1] += s;
}
else {
arr.push(s);
}
}
newValue.push(token.replaceWith(arr.join("\n")));
}
}
else {
newValue.push(token);
}
});
this.value = newValue;
}
// This will add a parenthesis wrapper to make it easier to parse later.
_round5() {
let newValue = [];
let level = null;
this.value.forEach((token, index) => {
if (this.value[index - 1] instanceof Dot && token instanceof InlineNormalString) {
newValue.push(new NormalLeftParenthesis());
newValue.push(token);
level = 0;
}
else if (level !== null && token instanceof LeftParenthesis) {
newValue.push(token);
level++;
}
else if (level !== null && token instanceof RightParenthesis) {
newValue.push(token);
level--;
if (level === 0) {
newValue.push(new NormalRightParenthesis());
level = null;
}
}
else {
newValue.push(token);
}
});
this.value = newValue;
}
// This will add a parenthesis wrapper to make things like `a.0` easier to parse later.
_round6() {
let newValue = [];
this.value.forEach((token, index) => {
if (this.value[index - 1] instanceof Dot && token instanceof Num) {
newValue.push(new NormalLeftParenthesis());
newValue.push(token);
newValue.push(new NormalRightParenthesis());
}
else {
newValue.push(token);
}
});
this.value = newValue;
}
_round7() {
let newValue = [];
this.value.forEach((token, index) => {
if (token instanceof NormalToken) {
let t = null;
switch (token.value) {
case "above":
t = Above; break;
case "and":
t = And; break;
case "as":
t = As; break;
case "catch":
t = Catch; break;
case "class":
t = Class; break;
case "delete":
t = Delete; break;
case "do":
t = Do; break;
case "else":
t = Else; break;
case "export":
t = Export; break;
case "false":
t = False; break;
case "finally":
t = Finally; break;
case "fun":
t = Fun; break;
case "if":
t = If; break;
case "ifnull":
t = Ifnull; break;
case "ifvoid":
t = Ifvoid; break;
case "import":
t = Import; break;
case "in":
t = In; break;
case "is":
t = Is; break;
case "isnt":
t = Isnt; break;
case "match":
t = Match; break;
case "me":
t = Me; break;
case "Me":
t = ClassMe; break;
case "mod":
t = Mod; break;
case "new":
t = New; break;
case "nonew":
t = Nonew; break;
case "not":
t = Not; break;
case "null":
t = Null; break;
case "or":
t = Or; break;
case "pause":
t = Pause; break;
case "rem":
t = Rem; break;
case "self":
t = Self; break;
case "super":
t = Super; break;
case "then":
t = Then; break;
case "throw":
t = Throw; break;
case "true":
t = True; break;
case "try":
t = Try; break;
case "void":
t = Void; break;
}
if (
t !== null &&
!(this.value[index - 1] instanceof Dot) &&
!(this.value[index - 1] instanceof NormalVariant) &&
!(
this.value[index + 1] instanceof Colon || (
this.value[index + 1] instanceof NormalVariant &&
this.value[index + 3] instanceof Colon
) || (
this.value[index + 1] instanceof NormalVariant &&
this.value[index + 3] instanceof NormalVariant &&
this.value[index + 5] instanceof Colon
)
) &&
!(newValue[newValue.length - 1] instanceof As)
) {
newValue.push(new t().copyPositionFrom(token));
}
else {
newValue.push(token);
}
}
else {
newValue.push(token);
}
});
this.value = newValue;
}
_round8() {
let newValue = [];
this.value.forEach((token, index) => {
if (
(
this.value[index - 1] instanceof NormalToken ||
this.value[index - 1] instanceof RightBracket
) &&
token instanceof NormalToken &&
this.value[index + 1] instanceof Colon
) {
if (token.value === "ifvoid") {
newValue.push(new Ifvoid().copyPositionFrom(token));
}
else if (token.value === "ifnull") {
newValue.push(new Ifnull().copyPositionFrom(token));
}
else {
newValue.push(token);
}
}
else {
newValue.push(token);
}
});
this.value = newValue;
}
_round9() {
let newValue = [];
let i = 0;
while (i < this.value.length) {
let token = this.value[i];
if (token instanceof Export && this.value[i + 1] instanceof As) {
newValue.push(new ExportAs().start(i).end(i + 1));
i += 2;
}
else if (token instanceof Not && this.value[i + 1] instanceof Equal) {
newValue.push(new NotEqual().start(i).end(i + 1));
i += 2;
}
else if (token instanceof Not && this.value[i + 1] instanceof In) {
newValue.push(new NotIn().start(i).end(i + 1));
i += 2;
}
else if (token instanceof Not && this.value[i + 1] instanceof Is) {
newValue.push(new Isnt().start(i).end(i + 1));
i += 2;
}
else if (token instanceof Is && this.value[i + 1] instanceof Not) {
newValue.push(new Isnt().start(i).end(i + 1));
i += 2;
}
else {
newValue.push(token);
i++;
}
}
this.value = newValue;
}
_removeEmptyLines() {
let newValue = [];
this.value.forEach((token, index) => {
if (token instanceof Indent && (
index === this.value.length - 1 || this.value[index + 1] instanceof Indent
)) {
}
else {
newValue.push(token);
}
});
this.value = newValue;
}
_round10() {
this._removeEmptyLines();
}
_round11() {
let newValue = [];
let ignoredTokens = [];
this.value.forEach((token, index) => {
if (token instanceof InlineComment) {
ignoredTokens.push(token);
}
// Removing version directive is temporary, and just for simplification.
// In the final round we will recover version directive.
else if (token instanceof VersionDirective) {
this._versionDirective = token;
}
else if (
token instanceof FormattedComment &&
!(
this.value[index - 2] instanceof VersionDirective &&
this.value[index - 1] instanceof Indent &&
(index === this.value.length - 1 || this.value[index + 1] instanceof Indent)
)
) {
ignoredTokens.push(token);
}
else if (token instanceof Above) {
ignoredTokens.push(token);
}
else {
newValue.push(token);
}
});
this.value = newValue;
this.ignoredTokens = ignoredTokens;
}
_round12() {
this._removeEmptyLines();
}
// Each indent uses only 1 whitespace.
_round13() {
let newValue = [];
let stack = [];
this.value.forEach((token, index) => {
if (token instanceof Indent) {
if (stack.length === 0 || token.value > stack[stack.length - 1]) {
let newIndentValue = stack.length;
stack.push(token.value);
newValue.push(token.replaceWith(newIndentValue));
}
else if (token.value === stack[stack.length - 1]) {
newValue.push(token.replaceWith(stack.length - 1));
}
else {
let found = stack.indexOf(token.value);
if (found !== -1) {
newValue.push(token.replaceWith(found));
stack.splice(found + 1, stack.length - found - 1);
}
else {
throw new IndentError(this.part(index, index));
}
}
}
else {
newValue.push(token);
}
});
this.value = newValue;
}
// Normalize indents to all use inline "<<" and ">>" and ";".
_round14() {
let newValue = [];
let oldIndentValue = 0;
this.value.forEach((token, index) => {
if (token instanceof Indent) {
if (token.value > oldIndentValue) {
newValue.push(new LeftChevron().copyPositionFrom(token));
oldIndentValue = token.value;
}
else if (token.value < oldIndentValue) {
for (let i = 0; i < oldIndentValue - token.value; i++) {
newValue.push(new RightChevron().copyPositionFrom(token));
}
if (!(
this.value[index + 1] instanceof RightParenthesis ||
this.value[index + 1] instanceof RightBracket ||
this.value[index + 1] instanceof RightBrace
)) {
newValue.push(new Semicolon());
}
oldIndentValue = token.value;
}
else if (newValue.length !== 0) {
newValue.push(new Semicolon());
}
}
else {
newValue.push(token);
}
});
for (let i = 0; i < oldIndentValue; i++) {
newValue.push(new RightChevron());
}
this.value = newValue;
}
// Add necessary ";".
_round15() {
let newValue = [];
this.value.forEach((token, index) => {
newValue.push(token);
if (
token instanceof RightChevron &&
!(this.value[index + 1] instanceof RightChevron) &&
!(this.value[index + 1] instanceof Semicolon) &&
!(this.value[index + 1] instanceof RightParenthesis) &&
!(this.value[index + 1] instanceof RightBracket) &&
!(this.value[index + 1] instanceof RightBrace) &&
index !== this.value.length - 1
) {
newValue.push(new Semicolon());
}
});
this.value = newValue;
}
// Remove redundant ";".
_round16() {
let newValue = [];
this.value.forEach((token, index) => {
if (token instanceof Semicolon) {
if (this.value[index - 1] instanceof Semicolon) {
}
else {
newValue.push(token);
}
}
else {
newValue.push(token);
}
});
this.value = newValue;
}
// Remove redundant ";".
_round17() {
let newValue = [];
this.value.forEach((token, index) => {
if (token instanceof Semicolon) {
if (index === 0) {
}
else if (index === this.value.length - 1) {
}
else if (this.value[index - 1] instanceof LeftChevron) {
}
else if (this.value[index + 1] instanceof RightChevron) {
}
else if (this.value[index - 1] instanceof LeftBrace) {
}
else if (this.value[index + 1] instanceof RightBrace) {
}
else if (this.value[index - 1] instanceof LeftBracket) {
}
else if (this.value[index + 1] instanceof RightBracket) {
}
else if (this.value[index - 1] instanceof LeftParenthesis) {
}
else if (this.value[index + 1] instanceof RightParenthesis) {
}
else {
newValue.push(token);
}
}
else {
newValue.push(token);
}
});
this.value = newValue;
}
/*
Remove useless or incorrect indents and semicolons to combine multiple lines. Example:
a:
if true
1
else
2
Or:
a:
if true
1
else
2
Both normalized to:
a: if true
1
else
2
3 +
5
Normalized to:
3 + 5
Objects and arrays like:
{
a: 2
b: 3
}
Normalized to:
{a: 2, b: 3}
Note that we don't remove empty chevron pair, because empty chevron pair can be nested,
like "<< << >> << >> >>", removing it will be too complicated. Also, "(())"
isn't stripped, so there's no reason to strip empty chevrons.
*/
_round18() {
let newValue = [];
let stack = [];
this.value.forEach((token, index) => {
let prev = this.value[index - 1];
let next = this.value[index + 1];
if (token instanceof LeftChevron) {
if (newValue[newValue.length - 1] instanceof Join) {
newValue.pop();
stack.push(true);
}
else if (
prev instanceof LeftParenthesis ||
prev instanceof LeftBracket ||
prev instanceof LeftBrace ||
(prev !== undefined && prev.constructor.isJoint) ||
(next !== undefined && next.constructor.isJoint)
) {
stack.push(true);
}
else {
stack.push(false);
newValue.push(token);
}
}
else if (token instanceof RightChevron) {
if (stack.pop()) {
}
else {
newValue.push(token);
}
}
else if (token instanceof Semicolon) {
if (newValue[newValue.length - 1] instanceof Join) {
newValue.pop();
}
else if (
(prev !== undefined && prev.constructor.isJoint) ||
(next !== undefined && next.constructor.isJoint)
) {
}
else {
newValue.push(token);
}
}
else {
newValue.push(token);
}
});
this.value = newValue;
}
// Remove ";" if "," is before ";".
_round19() {
let newValue = [];
this.value.forEach((token, index) => {
if (
this.value[index - 1] instanceof Comma &&
token instanceof Semicolon
) {
}
else {
newValue.push(token);
}
});
this.value = newValue;
}
// Remove incorrect ";" before token that can't be statement start. But this doesn't
// include "else" because "else" may appear in "match", where "else" can be after ";".
_round20() {
let newValue = [];
this.value.forEach((token, index) => {
if (
token instanceof Semicolon &&
this.value[index + 1].constructor.expressionStartForbidden &&
!this.value[index + 1].constructor.isCommand &&
!(this.value[index + 1] instanceof Else)
) {
}
else {
newValue.push(token);
}
});
this.value = newValue;
}
// Remove incorrect ";" before "else" that's not in "match".
_round21() {
let newValue = [];
let stack = [];
this.value.forEach((token, index) => {
if (token instanceof Match) {
stack.push({position: index, kind: Match});
newValue.push(token);
}
else if (token instanceof LeftChevron) {
stack.push({position: index, kind: LeftChevron});
newValue.push(token);
}
else if (token instanceof LeftParenthesis) {
stack.push({position: index, kind: LeftParenthesis});
newValue.push(token);
}
else if (token instanceof LeftBracket) {
stack.push({position: index, kind: LeftBracket});
newValue.push(token);
}
else if (token instanceof LeftBrace) {
stack.push({position: index, kind: LeftBrace});
newValue.push(token);
}
else if (
token instanceof RightChevron ||
token instanceof RightParenthesis ||
token instanceof RightBracket ||
token instanceof RightBrace
) {
if (stack[stack.length - 1].kind === Match) {
stack.pop();
}
stack.pop();
newValue.push(token);
}
else if (token instanceof Semicolon && this.value[index + 1] instanceof Else) {
if (
stack.length >= 2 &&
stack[stack.length - 2].kind === Match &&
stack[stack.length - 1].kind === LeftChevron
) {
newValue.push(token);
}
}
else {
newValue.push(token);
}
});
this.value = newValue;
}
// Two features: Mark levels, and correct the default export token.
_round22() {
let newValue = [];
let level = 0;
this.value.forEach((token, index) => {
if (
token instanceof LeftChevron ||
token instanceof LeftParenthesis ||
token instanceof LeftBracket ||
token instanceof LeftBrace
) {
token.level = level;
level++;
newValue.push(token);
}
else if (
token instanceof RightChevron ||
token instanceof RightParenthesis ||
token instanceof RightBracket ||
token instanceof RightBrace
) {
level--;
token.level = level;
newValue.push(token);
}
else if (
level === 0 &&
(index === 0 || this.value[index - 1] instanceof Semicolon) &&
token instanceof NormalToken && token.value === "export" &&
this.value[index + 1] instanceof Colon
) {
let newToken = new Export().copyPositionFrom(token);