xqlint
Version:
XQuery & JSONiq Quality Checker
1,575 lines (1,345 loc) • 60.3 kB
JavaScript
'use strict';
if (String.prototype.rtrimSpaces != 'function') {
String.prototype.rtrimSpaces = function (s) {
return this.replace(/[ \t]+$/, '');
}
}
if (String.prototype.ltrimSpaces != 'function') {
String.prototype.ltrimSpaces = function (s) {
return this.replace(/^[ \t]+/, '');
}
}
if (Array.prototype.contains != 'function') {
Array.prototype.contains = function (value) {
for (var i = 0; i < this.length; i++) {
if (this[i] === value) {
return true;
}
}
return false;
}
}
if (Array.prototype.containsAny != 'function') {
Array.prototype.containsAny = function (values) {
for (var i = 0; i < this.length; i++) {
for (var j = 0; j < values.length; j++) {
if (this[i] === values[j]) {
return true;
}
}
}
return false;
}
}
var FormatWriter = require('./FormatWriter.js').FormatWriter;
var CommentParser = require('./CommentParser.js').CommentParser;
var CommentHandler = require('./CommentHandler.js').CommentHandler;
var CodeFormatter = exports.CodeFormatter = function (ast, newLinesEnabled, DEBUG) {
// DEBUG = true;
if (newLinesEnabled === undefined) {
newLinesEnabled = true;
}
function debugMsg(msg) {
if (DEBUG && writer.newLinesEnabled) {
console.log(msg);
}
}
/* These nodes will ignore WS and be formatted.
* Note that if a child node of one of these nodes is in 'ignoreNodes',
* that child node will not be formatted. I.e., the most specific
* setting is used.
*/
var formatNodes = ['DefaultNamespaceDecl', 'Setter', 'NamespaceDecl',
'Import', 'FTOptionDecl', 'ContextItemDecl', 'AnnotatedDecl', 'OptionDecl',
'FunctionDecl', 'FunctionCall', 'Expr', 'IfExpr', 'TypeswitchExpr', 'ParenthesizedExpr',
'AdditiveExpr', 'VersionDecl', 'FLWORExpr', 'Statement', 'ComparisonExpr',
'BlockExpr', 'BlockStatement', 'ApplyStatement', 'AssignStatement',
'BreakStatement', 'ContinueStatement', 'ExitStatement', 'FLWORStatement',
'IfStatement', 'SwitchStatement', 'TryCatchStatement', 'TypeswitchStatement',
'VarDeclStatement', 'WhileStatement', 'ReturnClause', 'ReturnStatement', 'Separator',
'Comment', 'ParamList', 'SwitchExpr', 'SwitchStatement', 'LetClause', 'LetBinding',
'ComparisonExpr'
];
/* These nodes (and their children, unless they are in 'formatNodes')
* will print WS and not be formatted. */
var ignoreNodes = ['XQuery', 'StatementsAndOptionalExpr', 'Statements', 'DirElemConstructor'];
var noSpaceRight = ['$', '(', '[', '/', '//', '@', '%', '::'];
var noSpaceLeft = [',', ')', '[', ']', '*', '/', '//', ';', '::', '?'];
/* If the previously written node was a TOKEN, this variable contains the
* value of that node. It has a nonempty initial value for the special case
* that the query starts with a '+' or '-' in order to detect that it is a
* prefix operator.
*/
var lastToken = 'INIT';
var options = {
MAX_LINE_LENGTH: 80, // Max length of one line of code without wrap
MAX_IF_LENGTH: 60, // Max length of an IfExpr without wrap
MAX_RETURN_LENGTH: 80, // Max length of a Return without wrap
// #blank lines...
BLANKS_VERSIONDECL: 2, // after a VersionDecl
BLANKS_MODULEDECL: 2, // after a ModuleDecl
BLANKS_FUNCTIONDECL: 1, // after a FunctionDecl
BLANKS_PROLOGGROUP: 2, // between groups of alike items in the Prolog
BLANKS_PROLOGITEM: 1, // after all other items in a Prolog
BLANKS_PROLOG: 2, // after the Prolog
PREBLANKS_COMMENT: 1, // before a comment on a single line that
// is followed by a non-blank
// Post a new line after...
newLineAfter: {
multiLineArg: true // An Arg/Param that spans multiple lines
},
indent: ' '
}
var writer = new FormatWriter(options.indent, options.MAX_LINE_LENGTH);
writer.DEBUG = DEBUG;
writer.newLinesEnabled = newLinesEnabled;
// True if wsEnabled was toggled after the last written content
var wsToggled = false;
// Default setting for whitespace handling. Specialized by formatNodes/ignoreNodes.
var wsEnabled = true;
// Keeps track of what the next visited separator separates.
var separatorStack = [];
// When visiting a separator via the default handler, the separatorStack is
// consumed up to one of these nodes.
// Note: These nodes are actually the possible children of a Prolog node and
// always followed by a Separator.
var separatorBase = ['DefaultNamespaceDecl', 'Setter', 'NamespaceDecl', 'Import', 'FTOptionDecl', 'ContextItemDecl', 'AnnotatedDecl', 'OptionDecl'];
// The name of the previously formatted separatorBase node, if any.
// Used to determine whether we switched between groups of alike
// items in the Prolog and have to add two blank lines instead of one.
var lastSeparatorBase = '';
function isWS(str) {
return !(/\S/.test(str));
}
function pushIndent() {
//debugMsg('pushIndent() -> indent = ' + (writer.indentAmount + 1));
writer.pushIndent();
}
function popIndent() {
//debugMsg('popIndent() -> indent = ' + (writer.indentAmount - 1));
writer.popIndent();
}
function postSpace(forceSpace) {
if (!wsEnabled || forceSpace) {
//debugMsg('postSpace()');
writer.postSpace();
}
}
function postNewLine(count) {
if (count === undefined) {
count = 1;
}
//debugMsg('postNewLine()');
writer.postNewLine(count);
}
function appendStr(str) {
writer.appendStr(str);
}
var noFormat = false;
// Global variable to determine wether the previously
// written TOKEN is a prefix operator
var prefixOp = true;
function appendValue(node, forceSkipSpace) {
//////debugMsg('appendvalue(node="' + node.name + '' : '' + node.value + '', forceSkipSpace=' + forceSkipSpace + ')');
var nodeValue = getNodeValue(node);
if (!noFormat && !forceSkipSpace) {
// Check if we have to prepend a space
// 0. Previous written char mustn't be whitespace
// 1. Previous token (if any) must allow appending spaces and not be a
// prefix operator (+, - could be)
// 2. Current node mustn't be Whitespace
// 3. If current node is a TOKEN, its value must allow prepending spaces
if (!(isWS(writer.lastChar())) && !prefixOp && noSpaceRight.indexOf(lastToken) === -1 && !(node.name === 'WS' && isWS(getNodeValue(node)[0])) &&
(!(node.name === 'TOKEN') || noSpaceLeft.indexOf(nodeValue) === -1)) {
if (wsToggled) {
postSpace(true);
} else {
postSpace();
}
}
}
// If ws is enabled nodeValue is non-empty and we just have indentation
// on the current line, remove the indentation and write the value to its
// original position
if (wsEnabled && nodeValue.length > 0 && writer.isEmpty()) {
writer.resetLine();
for (var i = 0; i < node.pos.sc; i++) {
writer.appendStr(' ');
}
}
// Make sure separators are immediately following the previous content
// TODO export this to explicit formatting rules for nodes that contain
// Separator
if (nodeValue === ';') {
noFormat || writer.trimRight();
}
// Append the node value
appendStr(nodeValue);
// Update prefixOp
var prefixOpExceptions = ['!', '.', ')'];
if (node.name === 'TOKEN' &&
(nodeValue === '+' || nodeValue === '-') &&
lastToken.length > 0 &&
prefixOpExceptions.indexOf(lastToken) === -1) {
prefixOp = true;
} else {
prefixOp = false;
}
// Update lastToken
if (node.name === 'TOKEN') {
lastToken = nodeValue;
} else {
lastToken = '';
}
wsToggled = false;
//debugMsg('wsToggled=' + wsToggled);
}
function getNodeValue(node) {
var value = '';
if (node.value === undefined) {
for (var i = 0; i < node.children.length; i++) {
var child = node.children[i];
value += getNodeValue(child);
}
} else {
value += node.value;
}
return value;
}
// Return true if traversing the syntax tree starting from node yields no
// value at least until one of targetValues is seen
function noValueUntil(node, targetValues) {
if (targetValues.contains(node.value)) {
return true;
} else if (node.value !== undefined) {
return false;
} else {
if (node.children.length == 0) {
return false;
} else {
return noValueUntil(node.children[0], targetValues);
}
}
}
// Return true iff formatting node and adding it to the content would
// increase the number of lines
// Side-effect free
this.wouldIncreaseLines = function (node) {
var newWriter = new FormatWriter(options.indent, options.MAX_LINE_LENGTH,
writer);
var tmp = writer;
writer = newWriter;
var prev = {
lastToken: lastToken,
wsEnabled: wsEnabled,
wsToggled: wsToggled,
prefixOp: prefixOp
}
this.visit(node);
var ret = tmp.lineCount() !== writer.lineCount();
writer = tmp;
lastToken = prev.lastToken;
wsEnabled = prev.wsEnabled;
wsToggled = prev.wsToggled;
prefixOp = prev.prefixOp;
return ret;
}
// Format the node in a one-line string and return it
// Side-effect free
this.formatNode = function (node) {
var newWriter = new FormatWriter('', 10000000);
newWriter.newLinesEnabled = false;
var tmp = writer;
writer = newWriter;
var prev = {
lastToken: lastToken,
wsEnabled: wsEnabled,
wsToggled: wsToggled,
prefixOp: prefixOp
}
this.visit(node);
var ret = writer.getResult();
writer = tmp;
lastToken = prev.lastToken;
wsEnabled = prev.wsEnabled;
wsToggled = prev.wsToggled;
prefixOp = prev.prefixOp;
return ret;
}
//==========================================================================
// NODE HANDLERS
//==========================================================================
this.everythingElse = function (node) {
if (node.value !== undefined) {
appendValue(node);
} else {
this.visitChildren(node);
}
return true;
};
this.Prolog = function (node) {
this.everythingElse(node);
if (writer.getResult() !== '' && !noFormat) {
postNewLine(options.BLANKS_PROLOG + 1);
}
return true;
};
this.VersionDecl = function (node) {
this.everythingElse(node);
noFormat || postNewLine(options.BLANKS_VERSIONDECL + 1);
return true;
};
this.ModuleDecl = function (node) {
this.everythingElse(node);
noFormat || postNewLine(options.BLANKS_MODULEDECL + 1);
return true;
};
this.Separator = function (node) {
if (separatorStack.length > 0) {
//debugMsg('Visiting Separator, separatorStack is: [' + separatorStack.join(', ') + ']');
// Retrieve the info which node this is a separator for
var lastNode = separatorStack.pop();
if (separatorBase.indexOf(lastNode) === -1) {
// Consume from stack until a separatorBase is reached
while (separatorStack.length > 0 &&
separatorBase.indexOf(separatorStack.pop()) === -1) {}
}
//debugMsg('separatorStack is now: [' + separatorStack.join(', ') + ']');
// Remove WS before Separator
writer.trimRight();
// Append separator value, skipping space
appendValue(node, true);
// Apply special behavior according to lastNode
if (lastNode === 'FunctionDecl') {
// Blanks after FunctionDecl
noFormat || postNewLine(options.BLANKS_FUNCTIONDECL + 1);
} else if (separatorBase.indexOf(lastNode) !== -1) {
noFormat || postNewLine(options.BLANKS_PROLOGITEM + 1);
}
} else {
//debugMsg('Warning: visit separator, empty separatorStack');
appendValue(node, true); // Skip space
}
return true;
};
this.ReturnClause = this.ReturnStatement = function (node) {
var _self = this;
if (this.FormatOneLine(node, options.MAX_RETURN_LENGTH)) {
return true;
} else {
var Handler = function () {
this.TOKEN = function (node) {
if (node.value === 'return') {
_self[node.name](node);
}
};
this.ExprSingle = function (node) {
// If this ExprSingle is not a block Expr enclosed in '{' '}',
// post a new line
return _self.newLineIfNotBlockNode(node);
};
this.Statement = function (node) {
return this.ExprSingle(node);
}
};
this.visitChildren(node, new Handler());
}
return true;
};
this.WS = function (node) {
if (!noFormat) {
var wsCode = getNodeValue(node);
//console.log('WS:\n' + wsCode);
var h = new CommentHandler(wsCode);
var parser = new CommentParser(wsCode, h);
parser.parse_Comments();
var commentAst = h.getParseTree();
//console.log(JSON.stringify(commentAst));
this.visit(commentAst);
} else {
appendValue(node);
}
return true;
};
this.EOF = function (node) {
// Comments are parsed with an ending EOF - avoid
// writing a space before this node
return true;
};
this.Comments = function (node) {
return this.everythingElse(node);
};
// Whitespace in a comment AST
this.S = function (node) {
if (noFormat || wsEnabled) {
appendValue(node);
}
return true;
};
// TODO comments are not completely taken care of yet, see comments/ testfolder.
this.Comment = function (node) {
var Handler = function () {
// Position after the opening '(:' on the line
var postStartTag = 0;
var startLine = 0;
this.TOKEN = function (node) {
if (node.value === '(:') {
postStartTag = node.pos.sc + 2;
startLine = node.pos.sl;
postSpace();
appendStr('(');
writer.pushIndentBase();
writer.pushLineStartStr(': ');
appendStr(':');
} else if (node.value === ':)') {
writer.popLineStartStr();
var prevWord = writer.backOneWord();
if (node.pos.sl > startLine && prevWord === '') {
// multiline comment and writer.isEmpty(), starting with ': '
var curLine = writer.curLine;
writer.curLine = curLine.substring(0, curLine.length - 1);
writer.appendStr(')');
} else {
// oneline comment and/or last line is not empty
if (prevWord !== '') {
writer.appendStr(prevWord);
}
if (!isWS(writer.lastChar())) {
postSpace(true);
}
appendValue(node);
}
writer.popIndentBase();
}
return true;
};
this.CommentContents = function (node) {
var content = getNodeValue(node);
var parts = content.split('\n');
// Find minimum indentation of the lines
// This indentation will be subtracted from all lines before
// being written, in order to align on the left
var firstLine = parts[0].rtrimSpaces();
if (isWS(firstLine) || firstLine === '~') {
var minIndent = 10000;
} else {
var curIdx = 0;
if (firstLine.length > 0 && firstLine[0] == '~') {
// xqdoc comment
curIdx++;
}
while (curIdx < firstLine.length) {
if (!isWS(firstLine[curIdx])) {
break;
}
curIdx++;
}
var minIndent = postStartTag + curIdx;
}
for (var i = 1; i < parts.length; i++) {
if (isWS(parts[i])) {
continue;
}
var curIdx = 0;
while (curIdx < minIndent && curIdx < parts[i].length) {
if (parts[i][curIdx] !== ':' && !isWS(parts[i][curIdx])) {
break;
}
curIdx++;
}
if (curIdx < parts[i].length && curIdx < minIndent) {
minIndent = curIdx;
}
}
if (minIndent == 10000) {
minIndent = 0;
}
var firstLineStart = minIndent - postStartTag;
if (firstLineStart <= 0) {
// Special case where the lines after the first line start further left
firstLineStart = 0;
while (firstLineStart < firstLine.length &&
isWS(firstLine[firstLineStart])) {
firstLineStart++;
}
}
//console.log('postStartTag: ' + postStartTag);
//console.log('minIndent: ' + minIndent);
//console.log('firstLineStart: ' + minIndent);
// Check if comment is a xqdoc comment ( (:~ )
if (firstLine.length > 0 && firstLine[0] == '~') {
appendStr(firstLine);
} else {
appendStr(' ' + firstLine.substring(firstLineStart));
}
if (parts.length > 1) {
appendStr('\n');
}
for (var i = 1; i < parts.length; i++) {
var curPart = parts[i].substring(minIndent).rtrimSpaces();
if (i < parts.length - 1) {
curPart += '\n';
}
appendStr(curPart);
}
return true;
};
};
// TODO not implemented properly yet. Need to check whether comment is on
// its own line and whether content follows on the next line. if not,
// no blanks are needed.
/*
if (options.PREBLANKS_COMMENT > 0 &&
!(writer.isEmpty() && writer.result === '')){
if (writer.isEmpty()){
postNewLine(options.PREBLANKS_COMMENT + 1);
}
} else {
postSpace();
}
*/
var prev_clearBlankLines = writer.clearBlankLines;
var prev_ignoreWSIndent = writer.ignoreWSIndent;
var prev_wrapIndented = writer.wrapIndented;
var prev_separateWords = writer.separateWords;
writer.clearBlankLines = false;
writer.ignoreWSIndent = false;
writer.wrapIndented = false;
writer.separateWords = true;
this.visitChildren(node, new Handler());
writer.clearBlankLines = prev_clearBlankLines;
writer.ignoreWSIndent = prev_ignoreWSIndent;
writer.wrapIndented = prev_wrapIndented;
writer.separateWords = prev_separateWords;
return true;
};
this.TOKEN = function (node) {
appendValue(node);
return true;
};
var inIfExprIfStatement = false;
this.IfExpr = this.IfStatement = function (node) {
var _self = this;
var switched = false;
if (!inIfExprIfStatement) {
inIfExprIfStatement = true;
switched = true;
if (this.FormatOneLine(node, options.MAX_IF_LENGTH)) {
return true;
}
}
var Handler = function () {
this.ExprSingle = this.Statement = function (node) {
if (lastToken === 'else' &&
(node.children[0].name === 'IfExpr' || node.children[0].name === 'IfStatement')) {
// 'if' of an 'else if', don't post a newline
return _self.ExprSingle(node);
} else {
pushIndent();
postNewLine();
_self[node.name](node);
popIndent();
postNewLine();
return true;
}
};
};
this.visitChildren(node, new Handler());
if (switched) {
inIfExprIfStatement = false;
}
return true;
};
/*
* TryCatchExpr ::= TryClause CatchClause+
*
* TryCatchStatement
* ::= 'try' BlockStatement ( 'catch' CatchErrorList BlockStatement )+
*/
this.TryCatchExpr = this.TryCatchStatement = function (node) {
if (this.FormatOneLine(node, options.MAX_TRY_CATCH_LENGTH)) {
return true;
}
this.visitChildren(node);
return true;
};
/*
* CatchClause ::= 'catch' CatchErrorList '{' Expr '}'
*
* TryClause ::= 'try' '{' TryTargetExpr '}
*/
this.CatchClause = this.TryClause = function (node) {
var _self = this;
var Handler = function () {
this.TOKEN = function (node) {
if (node.value === '{') {
_self.TOKEN(node);
pushIndent();
postNewLine();
return true;
} else if (node.value === '}') {
popIndent();
postNewLine();
return _self.TOKEN(node);
} else {
return _self.TOKEN(node);
}
};
};
this.visitChildren(node, new Handler());
return true;
};
this.MultiplicativeExpr = function (node) {
var _self = this;
var Handler = function () {
this.TOKEN = function (node) {
if (noSpaceLeft.indexOf(node.value) !== -1) {
// Ensure space e.g. for '*' in MultiplicativeExpr
postSpace();
}
return _self.TOKEN(node);
};
};
this.visitChildren(node, new Handler());
return true;
};
/*
* TypeswitchExpr
* ::= 'typeswitch' '(' Expr ')' CaseClause+ 'default'
* ( '$' VarName )? 'return' ExprSingle
*
* SwitchExpr
* ::= 'switch' '(' Expr ')' SwitchCaseClause+ 'default'
* 'return' ExprSingle
*/
this.TypeswitchExpr = this.SwitchExpr = function (node) {
var _self = this;
var Handler = function () {
this.CaseClause = this.SwitchCaseClause = function (node) {
pushIndent();
postNewLine();
_self[node.name](node);
popIndent();
};
this.TOKEN = function (node) {
if (node.value === 'default') {
pushIndent();
postNewLine();
_self[node.name](node);
popIndent();
} else if (node.value === 'return') {
pushIndent();
pushIndent();
postNewLine();
_self[node.name](node);
popIndent();
popIndent();
} else {
_self[node.name](node);
}
};
};
this.visitChildren(node, new Handler());
return true;
};
/*
* CaseClause
* ::= 'case' ( '$' VarName 'as' )? SequenceTypeUnion 'return'
* ExprSingle
*/
this.CaseClause = function (node) {
var _self = this;
var Handler = function () {
this.SequenceTypeUnion = function (node) {
_self[node.name](node);
pushIndent();
postNewLine();
};
this.ExprSingle = function (node) {
_self[node.name](node);
popIndent();
};
};
this.visitChildren(node, new Handler());
return true;
};
/*
* SwitchCaseClause
* ::= ( 'case' SwitchCaseOperand )+ 'return' ExprSingle
*/
this.SwitchCaseClause = function (node) {
var _self = this;
var Handler = function () {
var hadCase = false;
this.TOKEN = function (node) {
if (node.value === 'case') {
if (hadCase) {
postNewLine();
} else {
hadCase = true;
}
} else if (node.value === 'return') {
pushIndent();
postNewLine();
}
return _self.TOKEN(node);
}
this.ExprSingle = function (node) {
_self[node.name](node);
popIndent();
};
};
this.visitChildren(node, new Handler());
return true;
};
this.DocumentTest = function (node) {
return this.AllKindTests(node);
};
this.ElementTest = function (node) {
return this.AllKindTests(node);
};
this.AttributeTest = function (node) {
return this.AllKindTests(node);
};
this.SchemaElementTest = function (node) {
return this.AllKindTests(node);
};
this.SchemaAttributeTest = function (node) {
return this.AllKindTests(node);
};
this.PITest = function (node) {
return this.AllKindTests(node);
};
this.CommentTest = function (node) {
return this.AllKindTests(node);
};
this.TextTest = function (node) {
return this.AllKindTests(node);
};
this.NamespaceNodeTest = function (node) {
return this.AllKindTests(node);
};
this.JSONTest = function (node) {
return this.AllKindTests(node);
};
this.AnyKindTest = function (node) {
return this.AllKindTests(node);
};
this.AllKindTests = function (node) {
var _self = this;
var Handler = function () {
this.TOKEN = function (node) {
if (node.value === '(') {
// Opening bracket of a kindtest - no space to the left
appendValue(node, true);
} else {
_self.TOKEN(node);
}
return true;
};
};
this.visitChildren(node, new Handler());
return true;
};
/*
* ArgumentList
* ::= '(' ( Argument ( ',' Argument )* )? ')'
*
* ParamList
* ::= Param ( ',' Param )*
*/
this.ArgumentList = this.ParamList = function (node) {
var _self = this;
var Handler = function () {
var wasMultiLine = false; // was last written arg/param multiline?
var lastLine = -1; // line on which last written arg/param ended
this.TOKEN = function (node) {
if (node.value === '(') {
// TODO: only use indentbase if none of the arguments wrap
// otherwise use default indentation
// Opening bracket of Argumentlist - no space to the left
appendValue(node, true);
writer.pushIndentBase();
} else if (node.value === ')') {
// If ')' goes to its own line, it must be aligned with the opening
// '('
writer.changeIndentBase(-1);
var prev_wrapIndented = writer.wrapIndented;
writer.wrapIndented = false;
appendValue(node, true);
writer.popIndentBase();
writer.wrapIndented = prev_wrapIndented;
} else if (node.value === ',') {
lastLine = writer.lineCount();
if (wasMultiLine && options.newLineAfter.multiLineArg) {
// Post newline after multiline arg/param
appendValue(node, true);
postNewLine();
wasMultiLine = false;
} else {
_self.TOKEN(node);
}
} else {
_self.TOKEN(node);
}
return true;
};
this.Argument = this.Param = function (node) {
// Test if Arg doesn't fit on current line
if (_self.wouldIncreaseLines(node)) {
if (lastLine === writer.lineCount()) {
postNewLine();
}
}
_self.Argument(node);
var nodeVal = _self.formatNode(node);
wasMultiLine = writer.curLine.indexOf(nodeVal) === -1;
return true;
};
};
this.visitChildren(node, new Handler());
return true;
};
this.Param = function (node) {
pushIndent();
this.everythingElse(node);
popIndent();
return true;
};
this.Annotation = function (node) {
var _self = this;
var Handler = function () {
this.TOKEN = function (node) {
if (node.value === '(') {
appendValue(node, true); // Skip space of opening bracket
return true;
} else {
return _self.TOKEN(node);
}
};
};
this.visitChildren(node, new Handler());
return true;
};
this.ObjectConstructor = function (node) {
var _self = this;
if (this.FormatOneLine(node)) {
return true;
}
var Handler = function () {
this.TOKEN = function (node) {
if (node.value === '{') {
appendValue(node);
pushIndent();
postNewLine();
return true;
} else if (node.value === '}') {
popIndent();
postNewLine();
appendValue(node);
return true;
} else if (node.value === ',') {
appendValue(node, true);
postNewLine();
return true;
} else {
return _self.TOKEN(node);
}
};
};
this.visitChildren(node, new Handler());
return true;
};
this.PairConstructor = function (node) {
var _self = this;
var Handler = function () {
this.TOKEN = function (node) {
if (node.value === ':') {
appendValue(node, true);
return true;
} else {
return _self.TOKEN(node);
}
};
};
pushIndent();
this.visitChildren(node, new Handler());
popIndent();
return true;
};
this.ArrayConstructor = function (node) {
var _self = this;
var Handler = function () {
this.TOKEN = function (node) {
if (node.value === '[') {
var idx = noSpaceLeft.indexOf('[');
if (idx > -1) {
noSpaceLeft[idx] = undefined;
}
appendValue(node);
if (idx > -1) {
noSpaceLeft[idx] = '[';
}
return true;
} else {
return _self.TOKEN(node);
}
};
};
this.visitChildren(node, new Handler());
return true;
};
this.FunctionDecl = function (node) {
var _self = this;
var Handler = function () {
this.TOKEN = function (node) {
var val = node.value;
if (val === 'as') {
// Return type on new line, align with function decl
postNewLine();
_self.TOKEN(node);
} else if (val === '{') {
// Opening function bracket
postNewLine();
appendValue(node);
pushIndent();
postNewLine();
} else if (val === '}') {
// Closing function bracket
popIndent();
postNewLine();
appendValue(node);
separatorFor = 'FunctionDecl';
} else if (val === '(') {
// Bracket opening paramlist
appendValue(node, true);
writer.pushIndentBase();
} else if (val === ')') {
// If ')' goes to its own line, it must be aligned with the opening
// '('
//writer.indentBase[writer.indentBase.length - 1].amount--;
writer.changeIndentBase(-1);
var prev_wrapIndented = writer.wrapIndented;
writer.wrapIndented = false;
appendValue(node, true);
writer.popIndentBase();
writer.wrapIndented = prev_wrapIndented;
} else {
_self.TOKEN(node);
}
return true;
};
};
this.visitChildren(node, new Handler());
separatorStack.push(node.name); // Need special handling of Separator after FunctionDecl
return true;
};
this.ContextItemDecl = this.LetBinding = this.AssignStatement =
function (node) {
var _self = this;
var Handler = function () {
this.ExprSingle = this.VarValue = this.VarDefaultValue =
function (node) {
return _self.newLineIfMultiLine(node, true);
};
};
this.visitChildren(node, new Handler());
return true;
};
this.Expr = function (node) {
var _self = this;
var Handler = function () {
var wasMultiLine = false;
var hadExprSingle = false;
this.ExprSingle = function (node) {
if (hadExprSingle &&
_self.wouldIncreaseLines(node)) {
postNewLine();
}
var ret = _self.ExprSingle(node);
var val = _self.formatNode(node).trim();
wasMultiLine = writer.curLine.indexOf(val) == -1;
hadExprSingle = true;
return ret;
};
this.TOKEN = function (node) {
var ret = _self.TOKEN(node);
if (wasMultiLine) {
postNewLine();
}
return ret;
};
};
this.visitChildren(node, new Handler());
return true;
};
this.FLWORExpr = this.FLWORStatement = function (node) {
var _self = this;
var Handler = function () {
this.clauseHandler = function (node) {
_self[node.name](node);
postNewLine();
return true;
};
this.InitialClause = function (node) {
return this.clauseHandler(node);
};
this.IntermediateClause = function (node) {
return this.clauseHandler(node);
};
};
this.visitChildren(node, new Handler());
return true;
};
this.LetClause = function (node) {
var _self = this;
if (this.FormatOneLine(node)) {
return true;
}
var multipleLetBindings = false;
for (var i = 0; i < node.children.length; i++) {
if (node.children[i].name === 'TOKEN' &&
node.children[i].value === ',') {
multipleLetBindings = true;
}
}
var Handler = function () {
var afterComma = false;
this.TOKEN = function (node) {
if (node.value === ',') {
afterComma = true;
}
return _self.TOKEN(node);
}
this.LetBinding = function (node) {
var ret;
if (afterComma) {
pushIndent();
postNewLine();
ret = _self.LetBinding(node);
popIndent();
} else {
if (multipleLetBindings) {
ret = _self.newLineIfMultiLine(node);
} else {
ret = _self.LetBinding(node);
}
}
return ret;
}
};
this.visitChildren(node, new Handler());
return true;
};
/*
* ForBinding
* ::= '$' VarName TypeDeclaration? AllowingEmpty? PositionalVar? FTScoreVar? 'in'
* ExprSingle
*/
this.ForBinding = function (node) {
var _self = this;
var Handler = function () {
this.ExprSingle = function (node) {
return _self.newLineIfMultiLine(node);
};
};
this.visitChildren(node, new Handler());
return true;
};
/*
* SlidingWindowClause
* ::= 'sliding' 'window' '$' VarName TypeDeclaration? 'in' ExprSingle
* WindowStartCondition WindowEndCondition
*
* TumblingWindowClause
* ::= 'tumbling' 'window' '$' VarName TypeDeclaration? 'in' ExprSingle
* WindowStartCondition WindowEndCondition
*/
this.SlidingWindowClause = this.TumblingWindowClause = function (node) {
var _self = this;
var Handler = function () {
this.ExprSingle = function (node) {
return _self.newLineIfMultiLine(node);
};
this.WindowStartCondition = this.WindowEndCondition = function (node) {
postNewLine();
return _self[node.name](node);
};
};
this.visitChildren(node, new Handler());
return true;
};
this.WindowStartCondition = this.WindowEndCondition = function (node) {
var _self = this;
var Handler = function () {
this.ExprSingle = function (node) {
return _self.newLineIfMultiLine(node);
};
}
this.visitChildren(node, new Handler());
return true;
};
this.BlockExpr = this.BlockStatement = function (node) {
var _self = this;
if (this.FormatOneLine(node, options.MAX_LINE_LENGTH)) {
return true;
}
var Handler = function () {
this.TOKEN = function (node) {
if (node.value === '{') {
appendValue(node, wsToggled);
pushIndent();
postNewLine();
return true;
} else if (node.value === '}') {
popIndent();
postNewLine();
appendValue(node);
return true;
} else {
return _self.TOKEN(node);
}
};
};
this.visitChildren(node, new Handler());
return true;
};
/*
* CompElemConstructor
* ::= 'element' ( EQName | '{' Expr '}' ) '{' ContentExpr? '}'
*/
this.CompElemConstructor = function (node) {
var _self = this;
if (this.FormatOneLine(node, options.MAX_LINE_LENGTH)) {
return true;
}
var Handler = function () {
var forContentExpr = false;
var haveEQName = false;
var bracketLine;
this.EQName = function (node) {
forContentExpr = true;
haveEQName = true;
return _self.EQName(node);
};
this.TOKEN = function (node) {
if (node.value === '{') {
if (forContentExpr) {
// This is the opening bracket of ContentExpr?
appendValue(node);
} else {
// This is the opening bracket of Expr
forContentExpr = true;
appendValue(node);
}
pushIndent();
bracketLine = writer.lineCount();
return true;
} else if (node.value === '}') {
popIndent();
if (haveEQName) {
postNewLine();
appendValue(node);
} else {
if (bracketLine < writer.lineCount()) {
postNewLine();
}
appendValue(node);
}
return true;
} else {
return _self.TOKEN(node);
}
};
};
this.visitChildren(node, new Handler());
return true;
};
this.ContentExpr = function (node) {
var _self = this;
var Handler = function () {
this.StatementsAndExpr = function (node) {
var Handler2 = function () {
this.Expr = function (node) {
var Handler3 = function () {
this.TOKEN = function (node) {
if (node.value === ',') {
appendValue(node);
postNewLine();
return true;
} else {
return _self.TOKEN(node);
}
}
} // Handler3
_self.visitChildren(node, new Handler3());
return true;
} // StatementsAndExpr.Expr
} // Handler2
_self.visitChildren(node, new Handler2());
return true;
} // this.StatementsAndExpr
} // Handler
postNewLine();
this.visitChildren(node, new Handler());
return true;
};
/*
* Format the node with its default formatter, but first check if it would
* increase lines and post a newline before it if so, unless the first value
* in the node occurs at a node in blockNodes.
*/
this.newLineIfMultiLine = function (node, blockNodes) {
var atBlockNode = false;
if (blockNodes) {
atBlockNode = noValueUntil(node, ['{']);
}
if (atBlockNode) {
var newLine = false;
} else {
var newLine = this.wouldIncreaseLines(node);
}
if (newLine) {
pushIndent();
postNewLine();
}
var ret = this[node.name](node);
if (newLine) {
popIndent();
}
return ret;
};
this.newLineIfNotBlockNode = function (node) {
var atBlockNode = noValueUntil(node, ['{']);
if (!atBlockNode) {
pushIndent();
postNewLine();
}
var ret = this[node.name](node);
if (!atBlockNode) {
popIndent();
}
return ret;
};
/**
* Try to format node on one line. Return undefined if already in one-line-mode or
* if formatting the node on one line would result in a string which is longer than maxLength or contains newlines.
*/
this.FormatOneLine = function (node, maxLength) {
if (writer.newLinesEnabled == false) {
return false;
}
if (node.pos.sl < node.pos.el) {
return false;
}
if (maxLength === undefined) {
maxLength = options.MAX_LINE_LENGTH;
}
var formatter = new CodeFormatter(node, false, false);
var resCode = formatter.format();
if (resCode.length <= maxLength && resCode.indexOf('\n') == -1 && !(writer.wouldIncreaseLines(resCode))) {
if (!wsToggled && !wsEnabled) {
postSpace();
}
appendStr(resCode);
lastToken = formatter.lastToken;
wsToggled = formatter.wsToggled;
wsEnabled = formatter.wsEnabled;
prefixOp = formatter.prefixOp;
return true;
} else {
return false;
}
};
this.visit = function (node) {
//debugMsg('ENTER ' + node.name);
var name = node.name;
var skip = false;
if (typeof this[name] === 'function')
skip = this[name](node) === true ? true : false;
else
skip = this.everythingElse(node) === true ? true : false;
if (!skip) {
this.visitChildren(node);
}
//debugMsg('LEAVE ' + node.name);
};
this.visitChildren = function (node, handler) {
for (var i = 0; i < node.children.length; i++) {
var child = node.children[i];
if (handler !== undefined && typeof handler[child.name] === 'function') {
handler[child.name](child);
} else {
this.visit(child);
}
}
};
this.astToText = function (node) {
if (node !== undefined) {
var resText = '';
if (node.value !== undefined) {
resText += node.value;
}
for (var i = 0; i < node.children.length; i++) {
resText += this.astToText(node.children[i]);
}
return resText;
} else {
return '';
}
};
this.removeWS = function (node) {
for (var i = 0; i < node.children.length; i++) {
if (node.children[i].name === 'WS') {
delete node.children[i];
}
}
node.children = node.children.filter(function (a) {
return typeof a !== 'undefined';
});
};
this.format = function (pos) {
// console.log('Formatting the following xquery code: ' + this.astToText(ast));
// format from pos.sl, pos.sc to pos.el, pos.ec, write rest as-is
if (pos) {
this.formatWithPos(ast, pos);
} else {
this.visit(ast);
}
return writer.getResult();
};
function containsPos(a, b) {
var slDiff = b.sl - a.sl;
var elDiff = a.el - b.el;
var scDiff = b.sc - a.sc;
var ecDiff = a.ec - b.ec;
return (slDiff > 0 || !slDiff && scDiff >= 0) && (elDiff > 0 || !elDiff && ecDiff >= 0);
}
function isSamePos(a, b) {
return a.sl === b.sl && a.el === b.el && a.sc === b.sc && a.ec === b.ec;
}
this.formatWithPos = function (node, pos) {
var prev = {
newLinesEnabled: writer.newLinesEnabled,
doIndent: writer.doIndent,
doWrap: writer.doWrap
};
// Turn off formatting
writer.newLinesEnabled = true;
writer.doIndent = false;
writer.doWrap = false;
// Check if a child contains the pos
var childContainsPos = false;
for (var i = 0; node.children && i < node.children.length; i++) {
if (containsPos(node.children[i].pos, pos)) {
childContainsPos = true;
break;
}
}
if (!containsPos(node.pos, pos)) {
// Don't format this node
this.prevNoFormat = noFormat;
noFormat = true;
this.visit(node);
noFormat = false;
} else if