blob: 5d4029c68139bd39495459af71245b04818798e7 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015 QNX Software Systems and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* QNX Software Systems - Initial API and implementation
*******************************************************************************/
(function (root, mod) {
if (typeof exports === "object" && typeof module === "object") // CommonJS
return mod(exports);
if (typeof define === "function" && define.amd) // AMD
return define(["exports"], mod);
mod(root.acornQMLInjector || (root.acornQMLInjector = {})); // Plain browser env
})(this, function (exports) {
'use strict';
exports.inject = function (acorn) {
// Add the 'mode' option to acorn
acorn.defaultOptions.mode = "qml";
// Acorn token types
var tt = acorn.tokTypes;
// QML token types
var qtt = acorn.qmlTokTypes = {};
var keywords = acorn.qmlKeywords = {};
/*
* Shorthand for defining keywords in the 'keywords' variable with the following
* format:
* keywords[name].isPrimitive : if this is a primitive type
* keywords[name].isQMLContextual : if this is a contextual keyword for QML
*
* Also stores the token's name in qtt._<keyword> for easy referencing later. None
* of these keywords will be tokenized and, as such, are allowed to be used in
* JavaScript expressions by acorn. The 'isQMLContextual' boolean in keywords refers
* to those contextual keywords that are also contextual in QML's parser rules such
* as 'color', 'list', 'alias', etc.
*/
function kw(name, options) {
if (options === undefined)
options = {};
qtt["_" + name] = name;
keywords[name] = {};
keywords[name].isPrimitive = options.isPrimitive ? true : false;
keywords[name].isQMLContextual = options.isQMLContextual ? true : false;
}
// QML keywords
kw("import");
kw("pragma");
kw("property", { isQMLContextual: true });
kw("readonly", { isQMLContextual: true });
kw("signal", { isQMLContextual: true });
kw("as");
kw("on", { isQMLContextual: true });
kw("boolean", { isPrimitive: true });
kw("double", { isPrimitive: true });
kw("int", { isPrimitive: true });
kw("alias", { isPrimitive: true, isQMLContextual: true });
kw("list", { isPrimitive: true, isQMLContextual: true });
kw("color", { isPrimitive: true, isQMLContextual: true });
kw("real", { isPrimitive: true, isQMLContextual: true });
kw("string", { isPrimitive: true, isQMLContextual: true });
kw("url", { isPrimitive: true, isQMLContextual: true });
// Future reserved words
kw("transient");
kw("synchronized");
kw("abstract");
kw("volatile");
kw("native");
kw("goto");
kw("byte");
kw("long");
kw("char");
kw("short");
kw("float");
// QML parser methods
var pp = acorn.Parser.prototype;
/*
* Parses a set of QML Header Items (QMLImport or QMLPragma)
*/
pp.qml_parseHeaderItemList = function () {
var node = this.startNode();
node.items = [];
var loop = true;
while (loop) {
if (this.isContextual(qtt._import)) {
node.items.push(this.qml_parseImport());
} else if (this.isContextual(qtt._pragma)) {
node.items.push(this.qml_parsePragma());
} else {
loop = false;
}
}
return this.finishNode(node, "QMLHeaderItemList");
};
/*
* Parses a QML Pragma statement of the form:
* 'pragma' <QMLQualifiedID>
*/
pp.qml_parsePragma = function () {
var node = this.startNode();
this.expectContextual(qtt._pragma);
node.id = this.qml_parseQualifiedId(true);
this.semicolon();
return this.finishNode(node, "QMLPragma");
};
/*
* Parses a QML Import of the form:
* 'import' <QMLModule> [as <QMLQualifier>]
* 'import' <StringLiteral> [as <QMLQualifier>]
*
* as specified by http://doc.qt.io/qt-5/qtqml-syntax-imports.html
*/
pp.qml_parseImport = function () {
var node = this.startNode();
if (!this.eatContextual(qtt._import)) {
this.unexpected();
}
switch (this.type) {
case tt.name:
node.module = this.qml_parseModule();
node.directory = null;
break;
case tt.string:
node.module = null;
node.directory = this.parseLiteral(this.value);
break;
default:
this.unexpected();
}
// Parse the qualifier, if any
if (this.isContextual(qtt._as)) {
node.qualifier = this.qml_parseQualifier();
}
this.semicolon();
return this.finishNode(node, "QMLImport");
};
/*
* Parses a QML Module of the form:
* <QMLQualifiedId> <QMLVersionLiteral>
*/
pp.qml_parseModule = function () {
var node = this.startNode();
node.id = this.qml_parseQualifiedId(false);
if (this.type === tt.num) {
node.version = this.qml_parseVersionLiteral();
} else {
this.unexpected();
}
return this.finishNode(node, "QMLModule");
};
/*
* Parses a QML Version Literal which consists of a major and minor
* version separated by a '.'
*/
pp.qml_parseVersionLiteral = function () {
var node = this.startNode();
node.value = this.value;
node.raw = this.input.slice(this.start, this.end);
if (!(/(\d+)\.(\d+)/.exec(node.raw))) {
this.raise(this.start, "QML module must specify major and minor version");
}
this.next();
return this.finishNode(node, "QMLVersionLiteral");
};
/*
* Parses a QML Qualifier of the form:
* 'as' <Identifier>
*/
pp.qml_parseQualifier = function () {
var node = this.startNode();
this.expectContextual(qtt._as);
node.id = this.qml_parseIdent(false);
return this.finishNode(node, "QMLQualifier");
};
/*
* Parses a QML Object Definition of the form:
* <QMLQualifiedId> { (<QMLObjectMember>)* }
*
* http://doc.qt.io/qt-5/qtqml-syntax-basics.html#object-declarations
*/
pp.qml_parseObjectDefinition = function (node, isBinding) {
if (!node) {
node = this.startNode();
}
if (!node.id) {
node.id = this.qml_parseQualifiedId(false);
}
node.body = this.qml_parseObjectInitializer();
return this.finishNode(node, isBinding ? "QMLObjectBinding" : "QMLObjectDefinition");
};
/*
* Parses a QML Object Initializer of the form:
* '{' <QMLObjectMember>* '}'
*/
pp.qml_parseObjectInitializer = function () {
var node = this.startNode();
this.expect(tt.braceL);
node.members = [];
while (this.type !== tt.braceR) {
node.members.push(this.qml_parseObjectMember());
}
this.expect(tt.braceR);
return this.finishNode(node, "QMLObjectInitializer");
};
/*
* Parses a QML Object Member which can be one of the following:
* - a QML Property Binding
* - a QML Property Declaration
* - a QML Property Modifier
* - a QML Object Literal
* - a JavaScript Function Declaration
* - a QML Signal Definition
*/
pp.qml_parseObjectMember = function () {
if (this.type === tt._default || this.isContextual(qtt._readonly) || this.isContextual(qtt._property)) {
return this.qml_parsePropertyDeclaration();
} else if (this.isContextual(qtt._signal)) {
return this.qml_parseSignalDefinition();
} else if (this.type === tt._function) {
return this.qml_parseFunctionMember();
}
return this.qml_parseObjectDefinitionOrPropertyBinding();
};
/*
* Parses a JavaScript function as a member of a QML Object Literal
*/
pp.qml_parseFunctionMember = function () {
var node = this.startNode();
this.expect(tt._function);
return this.parseFunction(node, true);
};
/*
* Parses a QML Object Definition or Property Binding depending on the tokens found.
*/
pp.qml_parseObjectDefinitionOrPropertyBinding = function (node) {
if (!node) {
node = this.startNode();
}
if (!node.id) {
node.id = this.qml_parseQualifiedId(false);
}
switch (this.type) {
case tt.braceL:
return this.qml_parseObjectDefinition(node);
case tt.colon:
return this.qml_parsePropertyBinding(node);
}
this.unexpected();
};
/*
* Parses a QML Property Modifier of the form:
* <QMLQualifiedID> 'on' <QMLQualifiedID> <QMLInitializer>
* TODO: Call this method in the normal parser once we can do lookahead
* Without lookahead, telling the difference between an Object Declaration,
* Property Binding, and Property Modifier would be too difficult. For now,
* we've implemented a workaround for Object Declarations and Property Bindings
* until Acorn gets lookahead.
*/
pp.qml_parsePropertyModifier = function () {
var node = this.startNode();
node.kind = this.qml_parseQualifiedID(false);
this.expectContextual(qtt._on);
node.id = this.qml_parseQualifiedID(false);
node.body = this.qml_parseObjectInitializer();
return this.finishNode(node, "QMLPropertyModifier");
};
/*
* Parses a QML Property Binding of the form:
* <QMLQualifiedID> <QMLBinding>
*/
pp.qml_parsePropertyBinding = function (node) {
if (!node) {
node = this.startNode();
}
if (!node.id) {
node.id = this.qml_parseQualifiedId(false);
}
this.expect(tt.colon);
node.binding = this.qml_parseBinding();
return this.finishNode(node, "QMLPropertyBinding");
};
/*
* Parses a QML Signal Definition of the form:
* 'signal' <Identifier> [(<QMLPropertyType> <Identifier> [',' <QMLPropertyType> <Identifier>]* )]?
*/
pp.qml_parseSignalDefinition = function () {
var node = this.startNode();
// Parse as a qualified id in case this is not a signal definition
var signal = this.qml_parseQualifiedId(true);
if (signal.parts.length === 1) {
if (signal.name !== qtt._signal) {
this.unexpected();
}
if (this.type === tt.colon || this.type === tt.braceL) {
// This is a property binding or object literal
node.id = signal;
return this.qml_parseObjectDefinitionOrPropertyBinding(node);
}
} else {
// Signal keyword is a qualified ID. This is not a signal definition
node.id = signal;
return this.qml_parseObjectDefinitionOrPropertyBinding(node);
}
node.id = this.qml_parseIdent(false);
this.qml_parseSignalParams(node);
this.semicolon();
return this.finishNode(node, "QMLSignalDefinition");
};
/*
* Parses QML Signal Parameters of the form:
* [(<QMLPropertyType> <Identifier> [',' <QMLPropertyType> <Identifier>]* )]?
*/
pp.qml_parseSignalParams = function (node) {
node.params = [];
if (this.eat(tt.parenL)) {
if (!this.eat(tt.parenR)) {
do {
var param = this.startNode();
param.kind = this.qml_parsePropertyType();
param.id = this.qml_parseIdent(false);
node.params.push(this.finishNode(param, "QMLParameter"));
} while (this.eat(tt.comma));
this.expect(tt.parenR);
}
}
};
/*
* Parses a QML Property Declaration of the form:
* ['default'|'readonly'] 'property' <QMLType> <Identifier> [<QMLBinding>]
*/
pp.qml_parsePropertyDeclaration = function () {
var node = this.startNode();
// Parse 'default' or 'readonly'
node.default = false;
node.readonly = false;
if (this.eat(tt._default)) {
node.default = true;
} else if (this.isContextual(qtt._readonly)) {
// Parse as a qualified id in case this is not a property declaration
var readonly = this.qml_parseQualifiedId(true);
if (readonly.parts.length === 1) {
if (this.type === tt.colon || this.type === tt.braceL) {
// This is a property binding or object literal.
node.id = readonly;
return this.qml_parseObjectDefinitionOrPropertyBinding(node);
}
node.readonly = true;
} else {
// Readonly keyword is a qualified ID. This is not a property declaration.
node.id = readonly;
return this.qml_parseObjectDefinitionOrPropertyBinding(node);
}
}
// Parse as a qualified id in case this is not a property declaration
var property = this.qml_parseQualifiedId(true);
if (property.parts.length === 1 || node.default || node.readonly) {
if (property.name !== qtt._property) {
this.unexpected();
}
if (this.type === tt.colon || this.type === tt.braceL) {
// This is a property binding or object literal.
node.default = undefined;
node.readonly = undefined;
node.id = property;
return this.qml_parseObjectDefinitionOrPropertyBinding(node);
}
} else {
// Property keyword is a qualified ID. This is not a property declaration.
node.default = undefined;
node.readonly = undefined;
node.id = property;
return this.qml_parseObjectDefinitionOrPropertyBinding(node);
}
node.kind = this.qml_parsePropertyType();
if (this.value === "<") {
this.expect(tt.relational); // '<'
node.modifier = this.qml_parsePropertyType();
if (this.value !== ">") {
this.unexpected();
}
this.expect(tt.relational); // '>'
}
node.id = this.qml_parseIdent(false);
if (!this.eat(tt.colon)) {
node.binding = null;
this.semicolon();
} else {
node.binding = this.qml_parseBinding();
}
return this.finishNode(node, "QMLPropertyDeclaration");
};
/*
* Parses a QML Property Type of the form:
* <Identifier>
*/
pp.qml_parsePropertyType = function () {
var node = this.startNode();
node.primitive = false;
if (this.qml_isPrimitiveType(this.type, this.value)) {
node.primitive = true;
}
node.id = this.qml_parseIdent(true);
return this.finishNode(node, "QMLPropertyType");
};
/*
* Parses one of the following possibilities for a QML Property assignment:
* - QML Object Binding
* - QML Array Binding
* - QML Script Binding
*/
pp.qml_parseBinding = function () {
if (this.options.mode === "qmltypes") {
return this.qml_parseScriptBinding(false);
}
// TODO: solve ambiguity where a QML Object Literal starts with a
// Qualified Id that looks very similar to a MemberExpression in
// JavaScript. For now, we just won't parse statements like:
// test: QMLObject { }
// test: QMLObject.QualifiedId { }
return this.qml_parseScriptBinding(true);
};
/*
* Parses a QML Array Binding of the form:
* '[' [<QMLObjectDefinition> (',' <QMLObjectDefinition>)*] ']'
*
* TODO: call this in the parser once we can use lookahead to distinguish between
* a QML Array Binding and a JavaScript array.
*/
pp.qml_parseArrayBinding = function () {
var node = this.startNode();
this.expect(tt.bracketL);
node.members = [];
while (!this.eat(tt.bracketR)) {
node.members.push(this.qml_parseObjectDefinition());
}
return this.finishNode(node, "QMLArrayBinding");
};
/*
* Parses one of the following Script Bindings:
* - Single JavaScript Expression
* - QML Statement Block (A block of JavaScript statements)
*/
pp.qml_parseScriptBinding = function (allowStatementBlock) {
var node = this.startNode();
node.block = false;
if (allowStatementBlock && this.type === tt.braceL) {
node.block = true;
node.script = this.qml_parseStatementBlock();
} else {
node.script = this.parseExpression(false);
this.semicolon();
}
return this.finishNode(node, "QMLScriptBinding");
};
/*
* Parses a QML Statement Block of the form:
* { <Statement>* }
*/
pp.qml_parseStatementBlock = function () {
var node = this.startNode();
this.expect(tt.braceL);
node.body = [];
while (!this.eat(tt.braceR)) {
node.body.push(this.parseStatement(true, false));
}
return this.finishNode(node, "QMLStatementBlock");
};
/*
* Parses a Qualified ID of the form:
* <Identifier> ('.' <Identifier>)*
*
* If 'liberal' is true then this method will allow non-contextual QML keywords as
* identifiers.
*/
pp.qml_parseQualifiedId = function (liberal) {
var node = this.startNode();
node.parts = [];
node.parts.push(this.qml_parseIdent(liberal));
while (this.eat(tt.dot)) {
node.parts.push(this.qml_parseIdent(liberal));
}
node.name = "";
for (var i = 0; i < node.parts.length; i++) {
node.name += node.parts[i].name;
if (i < node.parts.length - 1) {
node.name += ".";
}
}
return this.finishNode(node, "QMLQualifiedID");
};
/*
* Parses an Identifier in a QML Context. That is, this method uses 'isQMLContextual'
* to throw an error if a non-contextual QML keyword is found.
*
* If 'liberal' is true then this method will allow non-contextual QML keywords as
* identifiers.
*/
pp.qml_parseIdent = function (liberal) {
// Check for non-contextual QML keywords
if (!liberal) {
if (!this.qml_isIdent(this.type, this.value)) {
this.unexpected();
}
}
return this.parseIdent(liberal);
};
/*
* Returns whether or not a given token type and name can be a QML Identifier.
* Uses the 'isQMLContextual' boolean of 'keywords' to determine this.
*/
pp.qml_isIdent = function (type, name) {
if (type === tt.name) {
var key;
if ((key = keywords[name])) {
return key.isQMLContextual;
}
return true;
}
return false;
};
/*
* Returns whether or not the current token is a QML primitive type and consumes
* it as a side effect if it is.
*/
pp.qml_eatPrimitiveType = function (type, name) {
if (this.qml_isPrimitiveType(type, name)) {
this.next();
return true;
}
return false;
};
/*
* Returns whether or not the current token is a QML primitive type.
*/
pp.qml_isPrimitiveType = function (type, name) {
if (name === "var") {
return true;
}
if (type === tt.name) {
var key;
if ((key = keywords[name])) {
return key.isPrimitive;
}
}
return false;
};
acorn.plugins.qml = function (instance) {
// Extend acorn's 'parseTopLevel' method
instance.extend("parseTopLevel", function (nextMethod) {
return function (node) {
// Make parsing simpler by only allowing ECMA Version 5 or older ('import' is
// not a keyword in this version of ECMA Script). Qt 5.5 runs with ECMA Script
// 5 anyway, so this makes sense.
if (!this.options.ecmaVersion || this.options.ecmaVersion > 5) {
throw new Error("QML only supports ECMA Script Language Specification 5 or older");
}
// Disabled 'qmltypes' mode for now since the normal parser can't parse it anyway
if (this.options.mode === "qml") {
// Force strict mode
this.strict = true;
// Most of QML's constructs sit at the top-level of the parse tree,
// replacing JavaScripts top-level. Here we are parsing such things
// as the root object literal and header statements of QML. Eventually,
// these rules will delegate down to JavaScript expressions.
node.mode = this.options.mode;
node.headerItemList = this.qml_parseHeaderItemList();
node.rootObject = null;
if (this.type !== tt.eof) {
node.rootObject = this.qml_parseObjectDefinition();
}
if (!this.eat(tt.eof)) {
this.raise(this.pos, "Expected EOF after QML Root Object");
}
return this.finishNode(node, "QMLProgram");
} else if (this.options.mode === "js") {
return nextMethod.call(this, node);
} else {
throw new Error("Unknown mode '" + this.options.mode + "'");
}
};
});
};
return acorn;
};
});