blob: 21b13e0c5e2bccbc9634f68b33de67bbeb57daee [file] [log] [blame] [edit]
import { syntaxError } from "./error.js";
import { unescape } from "./productions/helpers.js";
// These regular expressions use the sticky flag so they will only match at
// the current location (ie. the offset of lastIndex).
const tokenRe = {
// This expression uses a lookahead assertion to catch false matches
// against integers early.
decimal:
/-?(?=[0-9]*\.|[0-9]+[eE])(([0-9]+\.[0-9]*|[0-9]*\.[0-9]+)([Ee][-+]?[0-9]+)?|[0-9]+[Ee][-+]?[0-9]+)/y,
integer: /-?(0([Xx][0-9A-Fa-f]+|[0-7]*)|[1-9][0-9]*)/y,
identifier: /[_-]?[A-Za-z][0-9A-Z_a-z-]*/y,
string: /"[^"]*"/y,
whitespace: /[\t\n\r ]+/y,
comment: /\/\/.*|\/\*[\s\S]*?\*\//y,
other: /[^\t\n\r 0-9A-Za-z]/y,
};
export const typeNameKeywords = [
"ArrayBuffer",
"DataView",
"Int8Array",
"Int16Array",
"Int32Array",
"Uint8Array",
"Uint16Array",
"Uint32Array",
"Uint8ClampedArray",
"BigInt64Array",
"BigUint64Array",
"Float32Array",
"Float64Array",
"any",
"object",
"symbol",
];
export const stringTypes = ["ByteString", "DOMString", "USVString"];
export const argumentNameKeywords = [
"async",
"attribute",
"callback",
"const",
"constructor",
"deleter",
"dictionary",
"enum",
"getter",
"includes",
"inherit",
"interface",
"iterable",
"maplike",
"namespace",
"partial",
"required",
"setlike",
"setter",
"static",
"stringifier",
"typedef",
"unrestricted",
];
const nonRegexTerminals = [
"-Infinity",
"FrozenArray",
"Infinity",
"NaN",
"ObservableArray",
"Promise",
"bigint",
"boolean",
"byte",
"double",
"false",
"float",
"long",
"mixin",
"null",
"octet",
"optional",
"or",
"readonly",
"record",
"sequence",
"short",
"true",
"undefined",
"unsigned",
"void",
].concat(argumentNameKeywords, stringTypes, typeNameKeywords);
const punctuations = [
"(",
")",
",",
"...",
":",
";",
"<",
"=",
">",
"?",
"*",
"[",
"]",
"{",
"}",
];
const reserved = [
// "constructor" is now a keyword
"_constructor",
"toString",
"_toString",
];
/**
* @typedef {ArrayItemType<ReturnType<typeof tokenise>>} Token
* @param {string} str
*/
function tokenise(str) {
const tokens = [];
let lastCharIndex = 0;
let trivia = "";
let line = 1;
let index = 0;
while (lastCharIndex < str.length) {
const nextChar = str.charAt(lastCharIndex);
let result = -1;
if (/[\t\n\r ]/.test(nextChar)) {
result = attemptTokenMatch("whitespace", { noFlushTrivia: true });
} else if (nextChar === "/") {
result = attemptTokenMatch("comment", { noFlushTrivia: true });
}
if (result !== -1) {
const currentTrivia = tokens.pop().value;
line += (currentTrivia.match(/\n/g) || []).length;
trivia += currentTrivia;
index -= 1;
} else if (/[-0-9.A-Z_a-z]/.test(nextChar)) {
result = attemptTokenMatch("decimal");
if (result === -1) {
result = attemptTokenMatch("integer");
}
if (result === -1) {
result = attemptTokenMatch("identifier");
const lastIndex = tokens.length - 1;
const token = tokens[lastIndex];
if (result !== -1) {
if (reserved.includes(token.value)) {
const message = `${unescape(
token.value
)} is a reserved identifier and must not be used.`;
throw new WebIDLParseError(
syntaxError(tokens, lastIndex, null, message)
);
} else if (nonRegexTerminals.includes(token.value)) {
token.type = "inline";
}
}
}
} else if (nextChar === '"') {
result = attemptTokenMatch("string");
}
for (const punctuation of punctuations) {
if (str.startsWith(punctuation, lastCharIndex)) {
tokens.push({
type: "inline",
value: punctuation,
trivia,
line,
index,
});
trivia = "";
lastCharIndex += punctuation.length;
result = lastCharIndex;
break;
}
}
// other as the last try
if (result === -1) {
result = attemptTokenMatch("other");
}
if (result === -1) {
throw new Error("Token stream not progressing");
}
lastCharIndex = result;
index += 1;
}
// remaining trivia as eof
tokens.push({
type: "eof",
value: "",
trivia,
line,
index,
});
return tokens;
/**
* @param {keyof typeof tokenRe} type
* @param {object} options
* @param {boolean} [options.noFlushTrivia]
*/
function attemptTokenMatch(type, { noFlushTrivia } = {}) {
const re = tokenRe[type];
re.lastIndex = lastCharIndex;
const result = re.exec(str);
if (result) {
tokens.push({ type, value: result[0], trivia, line, index });
if (!noFlushTrivia) {
trivia = "";
}
return re.lastIndex;
}
return -1;
}
}
export class Tokeniser {
/**
* @param {string} idl
*/
constructor(idl) {
this.source = tokenise(idl);
this.position = 0;
}
/**
* @param {string} message
* @return {never}
*/
error(message) {
throw new WebIDLParseError(
syntaxError(this.source, this.position, this.current, message)
);
}
/**
* @param {string} type
*/
probeKind(type) {
return (
this.source.length > this.position &&
this.source[this.position].type === type
);
}
/**
* @param {string} value
*/
probe(value) {
return (
this.probeKind("inline") && this.source[this.position].value === value
);
}
/**
* @param {...string} candidates
*/
consumeKind(...candidates) {
for (const type of candidates) {
if (!this.probeKind(type)) continue;
const token = this.source[this.position];
this.position++;
return token;
}
}
/**
* @param {...string} candidates
*/
consume(...candidates) {
if (!this.probeKind("inline")) return;
const token = this.source[this.position];
for (const value of candidates) {
if (token.value !== value) continue;
this.position++;
return token;
}
}
/**
* @param {string} value
*/
consumeIdentifier(value) {
if (!this.probeKind("identifier")) {
return;
}
if (this.source[this.position].value !== value) {
return;
}
return this.consumeKind("identifier");
}
/**
* @param {number} position
*/
unconsume(position) {
this.position = position;
}
}
export class WebIDLParseError extends Error {
/**
* @param {object} options
* @param {string} options.message
* @param {string} options.bareMessage
* @param {string} options.context
* @param {number} options.line
* @param {*} options.sourceName
* @param {string} options.input
* @param {*[]} options.tokens
*/
constructor({
message,
bareMessage,
context,
line,
sourceName,
input,
tokens,
}) {
super(message);
this.name = "WebIDLParseError"; // not to be mangled
this.bareMessage = bareMessage;
this.context = context;
this.line = line;
this.sourceName = sourceName;
this.input = input;
this.tokens = tokens;
}
}