a big lump of work to get the command line able to execute commands, and to hack in a trivial cli

This commit is contained in:
Joe Walker 2010-12-08 13:13:33 +00:00
commit 38c9fc7a93
17 changed files with 1715 additions and 379 deletions

View file

@ -0,0 +1,459 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Skywriter.
*
* The Initial Developer of the Original Code is
* Mozilla.
* Portions created by the Initial Developer are Copyright (C) 2009
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Skywriter Team (skywriter@mozilla.com)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
define(function(request, exports, module) {
var console = require('pilot/console');
var Trace = require('pilot/stacktrace').Trace;
var keyutil = require('pilot/keyboard/keyutil');
var history = require('canon/history');
var Request = require('canon/request').Request;
var env = require('environment').env;
exports.keymappings = {};
exports.addKeymapping = function(mapping) {
exports.keymappings[mapping.name] = mapping;
};
exports.removeKeymapping = function(name) {
delete exports.keymapping[name];
};
exports.startup = function(data, reason) {
var settings = data.env.settings;
// TODO register this
// catalog.addExtensionSpec("keymapping", {
// "description": "A keymapping defines how keystrokes are interpreted.",
// "params": [
// {
// "name": "states",
// "required": true,
// "description":
// "Holds the states and all the informations about the keymapping. See docs: pluginguide/keymapping"
// }
// ]
// });
settings.settingChange.add({
match: "customKeymapping",
ref: exports.keyboardManager,
func: exports.keyboardManager._customKeymappingChanged
.bind(exports.keyboardManager)
});
};
exports.shutdown = function(data, reason) {
var settings = data.env.settings;
settings.settingChange.remove(exports.keyboardManager);
};
/*
* Things to do to sanitize this code:
* - 'no command' is a bizarre special value at the very least it should be a
* constant to make typos more obvious, but it would be better to refactor
* so that a natural value like null worked.
* - sender seems to be totally customized to the editor case, and the functions
* that we assume that it has make no sense for the commandLine case. We
* should either document and implement the same function set for both cases
* or admit that the cases are different enough to have separate
* implementations.
* - remove remaining sproutcore-isms
* - fold buildFlags into processKeyEvent or something better, preferably the
* latter. We don't want the environment to become a singleton
*/
/**
* Every time we call processKeyEvent, we pass in some flags that require the
* same processing to set them up. This function can be called to do that
* setup.
* @param env Probably environment.env
* @param flags Probably {} (but check other places where this is called)
*/
exports.buildFlags = function(flags) {
flags.context = env.contexts[0];
return flags;
};
/**
* The canon, or the repository of commands, contains functions to process
* events and dispatch command messages to targets.
* @class
*/
var KeyboardManager = function() { };
KeyboardManager.prototype = {
_customKeymappingCache: { states: {} },
/**
* Searches through the command canon for an event matching the given flags
* with a key equivalent matching the given SproutCore event, and, if the
* command is found, sends a message to the appropriate target.
*
* This will get a couple of upgrades in the not-too-distant future:
* 1. caching in the Canon for fast lookup based on key
* 2. there will be an extra layer in between to allow remapping via
* user preferences and keyboard mapping plugins
*
* @return True if a matching command was found, false otherwise.
*/
processKeyEvent: function(evt, sender, flags) {
// Use our modified commandCodes function to detect the meta key in
// more circumstances than SproutCore alone does.
var symbolicName = keyutil.commandCodes(evt, true)[0];
if (util.none(symbolicName)) {
return false;
}
// TODO: Maybe it should be the job of our caller to do this?
exports.buildFlags(flags);
flags.isCommandKey = true;
return this._matchCommand(symbolicName, sender, flags);
},
_matchCommand: function(symbolicName, sender, flags) {
var match = this._findCommandExtension(symbolicName, sender, flags);
if (match && match.commandExt !== 'no command') {
if (flags.isTextView) {
sender.resetKeyBuffers();
}
var commandExt = match.commandExt;
commandExt.load(function(command) {
var request = new Request({
command: command,
commandExt: commandExt
});
history.execute(match.args, request);
});
return true;
}
// 'no command' is returned if a keyevent is handled but there is no
// command executed (for example when switchting the keyboard state).
if (match && match.commandExt === 'no command') {
return true;
} else {
return false;
}
},
_buildBindingsRegex: function(bindings) {
// Escape a given Regex string.
bindings.forEach(function(binding) {
if (!util.none(binding.key)) {
binding.key = new RegExp('^' + binding.key + '$');
} else if (Array.isArray(binding.regex)) {
binding.key = new RegExp('^' + binding.regex[1] + '$');
binding.regex = new RegExp(binding.regex.join('') + '$');
} else {
binding.regex = new RegExp(binding.regex + '$');
}
});
},
/**
* Build the RegExp from the keymapping as RegExp can't stored directly
* in the metadata JSON and as the RegExp used to match the keys/buffer
* need to be adapted.
*/
_buildKeymappingRegex: function(keymapping) {
for (state in keymapping.states) {
this._buildBindingsRegex(keymapping.states[state]);
}
keymapping._convertedRegExp = true;
},
/**
* Loop through the commands in the canon, looking for something that
* matches according to #_commandMatches, and return that.
*/
_findCommandExtension: function(symbolicName, sender, flags) {
// If the flags indicate that we handle the textView's input then take
// a look at keymappings as well.
if (flags.isTextView) {
var currentState = sender._keyState;
// Don't add the symbolic name to the key buffer if the alt_ key is
// part of the symbolic name. If it starts with alt_, this means
// that the user hit an alt keycombo and there will be a single,
// new character detected after this event, which then will be
// added to the buffer (e.g. alt_j will result in ∆).
if (!flags.isCommandKey || symbolicName.indexOf('alt_') === -1) {
sender._keyBuffer +=
symbolicName.replace(/ctrl_meta|meta/,'ctrl');
sender._keyMetaBuffer += symbolicName;
}
// List of all the keymappings to look at.
var ak = [ this._customKeymappingCache ];
// Get keymapping extension points.
ak = ak.concat(catalog.getExtensions('keymapping'));
for (var i = 0; i < ak.length; i++) {
// Check if the keymapping has the current state.
if (util.none(ak[i].states[currentState])) {
continue;
}
if (util.none(ak[i]._convertedRegExp)) {
this._buildKeymappingRegex(ak[i]);
}
// Try to match the current mapping.
var result = this._bindingsMatch(
symbolicName,
flags,
sender,
ak[i]);
if (!util.none(result)) {
return result;
}
}
}
var commandExts = catalog.getExtensions('command');
var reply = null;
var args = {};
symbolicName = symbolicName.replace(/ctrl_meta|meta/,'ctrl');
commandExts.some(function(commandExt) {
if (this._commandMatches(commandExt, symbolicName, flags)) {
reply = commandExt;
return true;
}
return false;
}.bind(this));
return util.none(reply) ? null : { commandExt: reply, args: args };
},
/**
* Checks if the given parameters fit to one binding in the given bindings.
* Returns the command and arguments if a command was matched.
*/
_bindingsMatch: function(symbolicName, flags, sender, keymapping) {
var match;
var commandExt = null;
var args = {};
var bufferToUse;
if (!util.none(keymapping.hasMetaKey)) {
bufferToUse = sender._keyBuffer;
} else {
bufferToUse = sender._keyMetaBuffer;
}
// Add the alt_key to the buffer as we don't want it to be in the buffer
// that is saved but for matching, it needs to be there.
if (symbolicName.indexOf('alt_') === 0 && flags.isCommandKey) {
bufferToUse += symbolicName;
}
// Loop over all the bindings of the keymapp until a match is found.
keymapping.states[sender._keyState].some(function(binding) {
// Check if the key matches.
if (binding.key && !binding.key.test(symbolicName)) {
return false;
}
// Check if the regex matches.
if (binding.regex && !(match = binding.regex.exec(bufferToUse))) {
return false;
}
// Check for disallowed matches.
if (binding.disallowMatches) {
for (var i = 0; i < binding.disallowMatches.length; i++) {
if (!!match[binding.disallowMatches[i]]) {
return true;
}
}
}
// Check predicates.
if (!exports.flagsMatch(binding.predicates, flags)) {
return false;
}
// If there is a command to execute, then figure out the
// comand and the arguments.
if (binding.exec) {
// Get the command.
commandExt = catalog.getExtensionByKey('command', binding.exec);
if (util.none(commandExt)) {
throw new Error('Can\'t find command ' + binding.exec +
' in state=' + sender._keyState +
', symbolicName=' + symbolicName);
}
// Bulid the arguments.
if (binding.params) {
var value;
binding.params.forEach(function(param) {
if (!util.none(param.match) && !util.none(match)) {
value = match[param.match] || param.defaultValue;
} else {
value = param.defaultValue;
}
if (param.type === 'number') {
value = parseInt(value, 10);
}
args[param.name] = value;
});
}
sender.resetKeyBuffers();
}
// Handle the 'then' property.
if (binding.then) {
sender._keyState = binding.then;
sender.resetKeyBuffers();
}
// If there is no command matched now, then return a 'false'
// command to stop matching.
if (util.none(commandExt)) {
commandExt = 'no command';
}
return true;
});
if (util.none(commandExt)) {
return null;
}
return { commandExt: commandExt, args: args };
},
/**
* Check that the given command fits the given key name and flags.
*/
_commandMatches: function(commandExt, symbolicName, flags) {
var mappedKeys = commandExt.key;
if (!mappedKeys) {
return false;
}
// Check predicates
if (!exports.flagsMatch(commandExt.predicates, flags)) {
return false;
}
if (typeof(mappedKeys) === 'string') {
if (mappedKeys != symbolicName) {
return false;
}
return true;
}
if (!Array.isArray(mappedKeys)) {
mappedKeys = [mappedKeys];
commandExt.key = mappedKeys;
}
for (var i = 0; i < mappedKeys.length; i++) {
var keymap = mappedKeys[i];
if (typeof(keymap) === 'string') {
if (keymap == symbolicName) {
return true;
}
continue;
}
if (keymap.key != symbolicName) {
continue;
}
return exports.flagsMatch(keymap.predicates, flags);
}
return false;
},
/**
* Build a cache of custom keymappings whenever the associated setting
* changes.
*/
_customKeymappingChanged: function(settingName, value) {
var ckc = this._customKeymappingCache =
JSON.parse(value);
ckc.states = ckc.states || {};
for (state in ckc.states) {
this._buildBindingsRegex(ckc.states[state]);
}
ckc._convertedRegExp = true;
}
};
/**
*
*/
exports.flagsMatch = function(predicates, flags) {
if (util.none(predicates)) {
return true;
}
if (!flags) {
return false;
}
for (var flagName in predicates) {
if (flags[flagName] !== predicates[flagName]) {
return false;
}
}
return true;
};
/**
* The global exported KeyboardManager
*/
exports.keyboardManager = new KeyboardManager();
});

View file

@ -0,0 +1,273 @@
/*! @license
==========================================================================
SproutCore -- JavaScript Application Framework
copyright 2006-2009, Sprout Systems Inc., Apple Inc. and contributors.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
SproutCore and the SproutCore logo are trademarks of Sprout Systems, Inc.
For more information about SproutCore, visit http://www.sproutcore.com
==========================================================================
@license */
// Most of the following code is taken from SproutCore with a few changes.
define(function(require, exports, module) {
var util = require('pilot/util');
/**
* Helper functions and hashes for key handling.
*/
exports.KeyHelper = function() {
var ret = {
MODIFIER_KEYS: {
16: 'shift', 17: 'ctrl', 18: 'alt', 224: 'meta'
},
FUNCTION_KEYS : {
8: 'backspace', 9: 'tab', 13: 'return', 19: 'pause',
27: 'escape', 33: 'pageup', 34: 'pagedown', 35: 'end',
36: 'home', 37: 'left', 38: 'up', 39: 'right',
40: 'down', 44: 'printscreen', 45: 'insert', 46: 'delete',
112: 'f1', 113: 'f2', 114: 'f3', 115: 'f4',
116: 'f5', 117: 'f7', 119: 'f8', 120: 'f9',
121: 'f10', 122: 'f11', 123: 'f12', 144: 'numlock',
145: 'scrolllock'
},
PRINTABLE_KEYS: {
32: ' ', 48: '0', 49: '1', 50: '2', 51: '3', 52: '4', 53: '5',
54: '6', 55: '7', 56: '8', 57: '9', 59: ';', 61: '=', 65: 'a',
66: 'b', 67: 'c', 68: 'd', 69: 'e', 70: 'f', 71: 'g', 72: 'h',
73: 'i', 74: 'j', 75: 'k', 76: 'l', 77: 'm', 78: 'n', 79: 'o',
80: 'p', 81: 'q', 82: 'r', 83: 's', 84: 't', 85: 'u', 86: 'v',
87: 'w', 88: 'x', 89: 'y', 90: 'z', 107: '+', 109: '-', 110: '.',
188: ',', 190: '.', 191: '/', 192: '`', 219: '[', 220: '\\',
221: ']', 222: '\"'
},
/**
* Create the lookup table for Firefox to convert charCodes to keyCodes
* in the keyPress event.
*/
PRINTABLE_KEYS_CHARCODE: {},
/**
* Allow us to lookup keyCodes by symbolic name rather than number
*/
KEY: {}
};
// Create the PRINTABLE_KEYS_CHARCODE hash.
for (var i in ret.PRINTABLE_KEYS) {
var k = ret.PRINTABLE_KEYS[i];
ret.PRINTABLE_KEYS_CHARCODE[k.charCodeAt(0)] = i;
if (k.toUpperCase() != k) {
ret.PRINTABLE_KEYS_CHARCODE[k.toUpperCase().charCodeAt(0)] = i;
}
}
// A reverse map of FUNCTION_KEYS
for (i in ret.FUNCTION_KEYS) {
var name = ret.FUNCTION_KEYS[i].toUpperCase();
ret.KEY[name] = parseInt(i, 10);
}
return ret;
}();
/**
* Determines if the keyDown event is a non-printable or function key.
* These kinds of events are processed as keyboard shortcuts.
* If no shortcut handles the event, then it will be sent as a regular
* keyDown event.
* @private
*/
var isFunctionOrNonPrintableKey = function(evt) {
return !!(evt.altKey || evt.ctrlKey || evt.metaKey ||
((evt.charCode !== evt.which) &&
exports.KeyHelper.FUNCTION_KEYS[evt.which]));
};
/**
* Returns character codes for the event.
* The first value is the normalized code string, with any Shift or Ctrl
* characters added to the beginning.
* The second value is the char string by itself.
* @return {Array}
*/
exports.commandCodes = function(evt, dontIgnoreMeta) {
var code = evt._keyCode || evt.keyCode;
var charCode = (evt._charCode === undefined ? evt.charCode : evt._charCode);
var ret = null;
var key = null;
var modifiers = '';
var lowercase;
var allowShift = true;
// Absent a value for 'keyCode' or 'which', we can't compute the
// command codes. Bail out.
if (code === 0 && evt.which === 0) {
return false;
}
// If the charCode is not zero, then we do not handle a command key
// here. Bail out.
if (charCode !== 0) {
return false;
}
// Check for modifier keys.
if (exports.KeyHelper.MODIFIER_KEYS[charCode]) {
return [exports.KeyHelper.MODIFIER_KEYS[charCode], null];
}
// handle function keys.
if (code) {
ret = exports.KeyHelper.FUNCTION_KEYS[code];
if (!ret && (evt.altKey || evt.ctrlKey || evt.metaKey)) {
ret = exports.KeyHelper.PRINTABLE_KEYS[code];
// Don't handle the shift key if the combo is
// (meta_|ctrl_)<number>
// This is necessary for the French keyboard. On that keyboard,
// you have to hold down the shift key to access the number
// characters.
if (code > 47 && code < 58) {
allowShift = evt.altKey;
}
}
if (ret) {
if (evt.altKey) {
modifiers += 'alt_';
}
if (evt.ctrlKey) {
modifiers += 'ctrl_';
}
if (evt.metaKey) {
modifiers += 'meta_';
}
} else if (evt.ctrlKey || evt.metaKey) {
return false;
}
}
// otherwise just go get the right key.
if (!ret) {
code = evt.which;
key = ret = String.fromCharCode(code);
lowercase = ret.toLowerCase();
if (evt.metaKey) {
modifiers = 'meta_';
ret = lowercase;
} else ret = null;
}
if (evt.shiftKey && ret && allowShift) {
modifiers += 'shift_';
}
if (ret) {
ret = modifiers + ret;
}
if (!dontIgnoreMeta && ret) {
ret = ret.replace(/ctrl_meta|meta/,'ctrl');
}
return [ret, key];
};
// Note: Most of the following code is taken from SproutCore with a few changes.
/**
* Firefox sends a few key events twice: the first time to the keydown event
* and then later again to the keypress event. To handle them correct, they
* should be processed only once. Due to this, we will skip these events
* in keydown and handle them then in keypress.
*/
exports.addKeyDownListener = function(element, boundFunction) {
var handleBoundFunction = function(ev) {
var handled = boundFunction(ev);
// If the boundFunction returned true, then stop the event.
if (handled) {
util.stopEvent(ev);
}
return handled;
};
element.addEventListener('keydown', function(ev) {
if (util.isMozilla) {
// Check for function keys (like DELETE, TAB, LEFT, RIGHT...)
if (exports.KeyHelper.FUNCTION_KEYS[ev.keyCode]) {
return true;
// Check for command keys (like ctrl_c, ctrl_z...)
} else if ((ev.ctrlKey || ev.metaKey) &&
exports.KeyHelper.PRINTABLE_KEYS[ev.keyCode]) {
return true;
}
}
if (isFunctionOrNonPrintableKey(ev)) {
return handleBoundFunction(ev);
}
return true;
}, false);
element.addEventListener('keypress', function(ev) {
if (util.isMozilla) {
// If this is a function key, we have to use the keyCode.
if (exports.KeyHelper.FUNCTION_KEYS[ev.keyCode]) {
return handleBoundFunction(ev);
} else if ((ev.ctrlKey || ev.metaKey) &&
exports.KeyHelper.PRINTABLE_KEYS_CHARCODE[ev.charCode]){
// Check for command keys (like ctrl_c, ctrl_z...).
// For command keys have to convert the charCode to a keyCode
// as it has been sent from the keydown event to be in line
// with the other browsers implementations.
// FF does not allow let you change the keyCode or charCode
// property. Store to a custom keyCode/charCode variable.
// The getCommandCodes() function takes care of these
// special variables.
ev._keyCode = exports.KeyHelper.PRINTABLE_KEYS_CHARCODE[ev.charCode];
ev._charCode = 0;
return handleBoundFunction(ev);
}
}
// normal processing: send keyDown for printable keys.
if (ev.charCode !== undefined && ev.charCode === 0) {
return true;
}
return handleBoundFunction(ev);
}, false);
};
});

View file

@ -0,0 +1,99 @@
require.def(['require', 'exports', 'module',
'keyboard/keyboard',
'keyboard/tests/plugindev'
], function(require, exports, module,
keyboard,
t
) {
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Skywriter.
*
* The Initial Developer of the Original Code is
* Mozilla.
* Portions created by the Initial Developer are Copyright (C) 2009
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Skywriter Team (skywriter@mozilla.com)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
exports.testKeyMatching = function() {
var km = keyboard.keyboardManager;
var command = {};
t.equal(km._commandMatches(command, 'meta_z', {}), false,
'no keymapping means false');
command = {
key: 'meta_z'
};
t.equal(km._commandMatches(command, 'meta_z', {}), true,
'matching keys, simple string');
t.equal(km._commandMatches(command, 'meta_a', {}), false,
'not matching key, simple string');
command = {
key: {key: 'meta_z', predicates: {isGreen: true}}
};
t.equal(km._commandMatches(command, 'meta_z', {}), false,
'object with not matching predicate');
t.equal(km._commandMatches(command, 'meta_z', {isGreen: true}), true,
'object with matching key and predicate');
t.equal(km._commandMatches(command, 'meta_a', {isGreen: true}), false,
'object with not matching key');
t.equal(km._commandMatches(command, 'meta_a', {isGreen: false}), false,
'object with neither matching');
t.equal(km._commandMatches(command, 'meta_z', {isGreen: false}), false,
'object with matching key and but different predicate');
command = {
key: ['meta_b', {key: 'meta_z', predicates: {isGreen: true}},
{key: 'meta_c'}]
};
t.equal(km._commandMatches(command, 'meta_z', {}), false,
'list: object with not matching predicate');
t.equal(km._commandMatches(command, 'meta_z', {isGreen: true}), true,
'list: object with matching key and predicate');
t.equal(km._commandMatches(command, 'meta_a', {isGreen: true}), false,
'list: object with not matching key');
t.equal(km._commandMatches(command, 'meta_a', {isGreen: false}), false,
'list: object with neither matching');
t.equal(km._commandMatches(command, 'meta_z', {isGreen: false}), false,
'list: object with matching key and but different predicate');
t.equal(km._commandMatches(command, 'meta_b'), true,
'list: simple key match');
t.equal(km._commandMatches(command, 'meta_c'), true,
'list: object without predicate match');
t.equal(km._commandMatches(command, 'meta_c', {isGreen: false}), true,
'list: flags don\'t matter without predicates');
};
});