keyvaluecoding
Version:
A native javascript implementation of Apple's NSKeyValueCoding
294 lines (262 loc) • 9.18 kB
JavaScript
/* --------------------------------------------------------------------
* keyvaluecoding/utilities.js
* --------------------------------------------------------------------
* The MIT License
*
* Copyright (c) 2012 kd
*
* 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.
*/
// Goop in here is just to help with the perl -> js conversion
// and make my life a whole lot easier. The general idea is
// to use this during the port and then punt it when things
// are "working".
var util = require("util");
_p_length = function(thing) {
if (!thing) { return null }
return thing.length;
};
_p_keys = function(thing) {
if (!thing) { return [] }
return Object.keys(thing);
};
_p_values = function(thing) {
// if this is an objj object, send it an allKeys message
if (!thing) { return [] }
var values = [];
for (key in _p_keys(thing)) {
_p_push( values, thing[key] );
}
return values;
};
_p_push = function(a, thing) {
if (!a) { return }
a[a.length] = thing;
};
_p_2_split = function(re, st) {
var bits = st.split(re);
var first = bits.shift();
if (bits.length > 0) {
st = st.replace(first, "");
var second = st.substring(st.indexOf(bits.shift())) || "";
return [first, second];
}
if (first == "") { return [] }
return [first];
};
_p_lcfirst = function(str) {
return str.substr(0, 1).toLowerCase() + str.substr(1, str.length);
};
_p_ucfirst = function(str) {
return str.substr(0, 1).toUpperCase() + str.substr(1, str.length);
};
_p_niceName = function(name) {
if (name.match(/^[A-Z0-9_]+$/)) {
return _p_lcfirst(name.split("_").map(function (_u) { return _p_ucfirst(_u.toLowerCase()) }).join(""));
}
return name;
};
_p_quotemeta = function(str) {
return str.replace( /([^A-Za-z0-9])/g , "\\$1" );
}
_p_trim = function(str) {
return str.replace(/^\s+/, "").replace(/\s+$/, "");
}
_p_can = function(object, method) {
if ( typeof object[method] === 'function' ) {
return true;
}
return false;
}
_p_keyNameFromNiceName = function(niceName) {
var pieces = niceName.split(/([A-Z0-9])/);
var uppercasePieces = [];
for (var i=0; i<pieces.length; i++) {
if (pieces[i] == "") { continue }
if (pieces[i].match(/^[a-z0-9]$/)) {
uppercasePieces.push(pieces[i].toUpperCase());
} else if (pieces[i].match(/^[A-Z0-9]$/)) {
// either it's an acronym, a single char or
// a first char
if (pieces[i+1] != "") {
uppercasePieces.push((pieces[i] + pieces[i+1]).toUpperCase());
i++;
} else {
var j = i;
// acronyms
var acronym = "";
while (pieces[i+1] == "" && i < pieces.length) {
acronym = acronym + pieces[i];
i+=2;
}
uppercasePieces.push(acronym);
if (i >= pieces.length) { break }
i--;
}
} else {
uppercasePieces.push(pieces[i].toUpperCase());
}
}
var keyName = uppercasePieces.join("_");
return keyName;
};
_p_isArray = function( v ) {
if ( !v ) { return false }
if ( typeof v !== "object" ) { return false }
if ( v.constructor && v.constructor.name === "Array" ) { return true }
return false;
};
var KP_RE = new RegExp('^[A-Za-z_\(\)]+[A-Za-z0-9_#\@\.\(\)\"]*$');
var KP_RE_PLUS = new RegExp('^[A-Za-z_\(\)]+[A-Za-z0-9_#\@]*[\(\.]+');
var QUOTE_RE = new RegExp('["' + "']");
var OPEN_RE = new RegExp("[\[\{\(]");
var KEY_RE = new RegExp( "([a-zA-Z0-9_\@]+)\\(" );
expressionIsKeyPath = function( expression ) {
if ( !expression ) { return false }
if ( expression.match(KP_RE) ) { return true }
return expression.match(KP_RE_PLUS);
}
keyPathElementsForPath = function( path ) {
if ( !path.match(/\(/) ) {
var bits = path.split(".");
var elements = [];
for ( var i = 0; i < bits.length; i++ ) {
elements[elements.length] = { 'key': bits[i] };
}
return elements;
}
var keyPathElements = [];
while (1) {
var bits = _p_2_split(".", path);
var firstElement = bits[0],
rest = bits[1];
if (!firstElement) { break; }
var match = firstElement.match(KEY_RE);
if ( match ) {
var key = match[1];
var element = new RegExp(_p_quotemeta(key + "("));
path = path.replace(element, "");
var argumentString = extractDelimitedChunk(path, ')');
var quotedArguments = _p_quotemeta(argumentString + ")") + "\\.?";
// extract arguments:
var arguments = [];
while (1) {
var argument = extractDelimitedChunk(argumentString, ",");
if (!argument) { break }
arguments.push(_p_trim(argument));
var quotedArgument = _p_quotemeta(argument) + ",?\\s*";
argumentString = argumentString.replace(new RegExp(quotedArgument), "");
}
keyPathElements.push({ key: key, arguments: arguments });
path = path.replace(new RegExp(quotedArguments), "");
} else {
if (firstElement) {
keyPathElements.push({ key: firstElement });
}
path = rest;
}
if (!rest) { break }
}
return keyPathElements;
}
// It's easier to do it this way than to import Text::Balanced
extractDelimitedChunk = function( chunk, terminator ) {
var extracted = "";
var balanced = { '(': 0, '{': 0, '[': 0, '"': 0, "'": 0 };
var isQuoting = false;
var outerQuoteChar = '';
var chars = chunk.split("");
for (var i = 0; i < chars.length; i++) {
var chrAt = chars[i];
if (chrAt == '\\') {
extracted = extracted + chars[i] + chars[i+1];
i++;
continue;
}
if (chrAt == terminator) {
if (isBalanced(balanced)) {
return extracted;
}
}
if (!isQuoting) {
if (chrAt.match(QUOTE_RE)) {
isQuoting = true;
outerQuoteChar = chrAt;
balanced[chrAt]++;
} else if (chrAt.match(OPEN_RE)) {
balanced[chrAt]++;
} else if (chrAt == ']') {
balanced['[']--;
} else if (chrAt == '}') {
balanced['{']--;
} else if (chrAt == ')') {
balanced['(']--;
}
} else {
if (chrAt == outerQuoteChar) {
isQuoting = false;
outerQuoteChar = '';
balanced[chrAt] ++;
}
}
extracted = extracted + chrAt;
}
if (isBalanced(balanced)) {
return extracted;
} else {
// error - unbalanced
}
return "";
}
isBalanced = function( balanced ) {
for (var ch in balanced) {
if (ch.match(OPEN_RE) && balanced[ch] != 0) { return false }
if (ch.match(QUOTE_RE) && balanced[ch] % 2 != 0) { return false }
}
return true;
}
unBalanced = function( balanced ) {
for (var ch in balanced) {
if (ch.match(OPEN_RE) && balanced[ch] != 0) { return ch }
if (ch.match(QUOTE_RE) && balanced[ch] % 2 != 0) { return ch }
}
return null;
}
// These are all just plain helper functions
// that helped me port some stuff from Perl.
module.exports = {
_p_length: _p_length,
_p_keys: _p_keys,
_p_values: _p_values,
_p_push: _p_push,
_p_2_split: _p_2_split,
_p_lcfirst: _p_lcfirst,
_p_ucfirst: _p_ucfirst,
_p_niceName: _p_niceName,
_p_quotemeta: _p_quotemeta,
_p_trim: _p_trim,
_p_can: _p_can,
_p_keyNameFromNiceName: _p_keyNameFromNiceName,
_p_isArray: _p_isArray,
expressionIsKeyPath: expressionIsKeyPath,
keyPathElementsForPath: keyPathElementsForPath,
extractDelimitedChunk: extractDelimitedChunk,
isBalanced: isBalanced,
unBalanced: unBalanced
};