/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Gareth Johnson
*/
/**
* Defines {@link baja.SlotPath}.
* @module baja/ord/SlotPath
*/
define([
"bajaScript/sys",
"bajaScript/baja/ord/OrdQuery" ], function (
baja,
OrdQuery) {
"use strict";
var subclass = baja.subclass,
callSuper = baja.callSuper,
strictArg = baja.strictArg;
function parseSlotPathBackup(slotPath) {
var body = slotPath.getBody(),
len = body.length,
c0, c1, c2, i;
for (i = 0; i < len; i += 3) {
c0 = body.charAt(i);
c1 = (i + 1 < len) ? body.charAt(i + 1) : -1;
c2 = (i + 2 < len) ? body.charAt(i + 2) : "/";
if (c0 !== ".") {
return i;
}
if (c1 !== "." || c2 !== "/") {
// Since we know c0 is a period ('.'), we can check to see
// if that is a valid path name. For SlotPath's, it
// should always return false, so the SyntaxException
// will be thrown. But for subclasses (such as VirtualPath),
// this may be a legal path name, so we don't want to throw
// the Syntax Exception.
if (slotPath.isValidPathName(c0)) {
return i;
}
throw new Error("Expecting ../ backup");
}
slotPath.$backupDepth++;
}
return len;
}
function parseSlotPathNames(slotPath, start) {
var body = slotPath.getBody(),
len = body.length,
c, // Character
nm, // Name
i;
if (start >= len) {
return;
}
if (body.charAt(len - 1) === "/") {
throw new Error("Invalid Slot Path - Trailing Slash");
}
for (i = start; i < len; ++i) {
c = body.charAt(i);
if (c === "/") {
if (i === start) {
throw new Error("Invalid Slot Path - Double Slashes");
}
nm = body.substring(start, i);
if (!slotPath.isValidPathName(nm)) {
throw new Error("Invalid name in path");
}
slotPath.$names.push(nm);
start = i + 1;
}
}
nm = body.substring(start, len);
if (!slotPath.isValidPathName(nm)) {
throw new Error("Invalid name in path");
}
slotPath.$names.push(nm);
}
function parseSlotPath(slotPath) {
slotPath.$names = [];
slotPath.$abs = false;
if (slotPath.getBody().length === 0) {
return;
}
var s = 0, // Start
c = slotPath.getBody().charAt(0);
if (c === "/") {
slotPath.$abs = true;
s = 1;
} else if (c === ".") {
s = parseSlotPathBackup(slotPath);
}
parseSlotPathNames(slotPath, s);
}
/**
* `SlotPath` is used for resolving `BValue`s using slot names.
*
* @class
* @alias baja.SlotPath
* @extends baja.OrdQuery
*
* @param {String} body the body of the ORD scheme
*/
var SlotPath = function SlotPath(body) {
callSuper(SlotPath, this, [ {
scheme: baja.SlotScheme.DEFAULT,
schemeName: "slot",
body: strictArg(body, String)
} ]);
this.$abs = false;
this.$names = [];
this.$backupDepth = 0;
parseSlotPath(this);
};
subclass(SlotPath, OrdQuery);
/**
* Make a Slot Path.
*
* @private
*
* @param {Object} body the body.
* @returns {baja.SlotPath} the new Slot Path.
*/
SlotPath.prototype.makeSlotPath = function (body) {
return new SlotPath(body);
};
/**
* Return the SlotPath depth.
*
* @returns {Number} the SlotPath depth.
*/
SlotPath.prototype.depth = function () {
return this.$names.length;
};
/**
* Return the SlotPath backup depth.
*
* @returns {Number} the SlotPath depth.
*/
SlotPath.prototype.getBackupDepth = function () {
return this.$backupDepth;
};
/**
* Return the name at the given depth.
*
* @param {Number} depth the specified depth for the name.
*
* @returns {String} the name at the specified depth.
*/
SlotPath.prototype.nameAt = function (depth) {
strictArg(depth, Number);
return this.$names[depth];
};
/**
* Return true if the SlotPath is absolute.
*
* @returns {Boolean} true if the SlotPath is absolute.
*/
SlotPath.prototype.isAbsolute = function () {
return this.$abs;
};
/**
* Return whether the specified path name is valid.
*
* @param {String} pathName the path name to validate.
*
* @returns {Boolean} true if the slot name is valid.
*/
SlotPath.prototype.isValidPathName = function (pathName) {
return SlotPath.isValidName(pathName);
};
/**
* Return whether the slot name is valid
*
* @param {String} nm the name to be validated.
*
* @returns {Boolean} true if the slot name is valid.
*/
SlotPath.isValidName = function (nm) {
return (/^([a-zA-Z$]([a-zA-Z0-9_]|(\$([0-9a-fA-F]{2}))|(\$u([0-9a-fA-F]{4})))*)$/).test(nm);
};
/**
* Verify whether the slot name is valid.
*
* @param {String} nm the name to be validated.
*
* @throws {Error} if the slot name isn't valid.
*/
SlotPath.verifyValidName = function (nm) {
if (!SlotPath.isValidName(nm)) {
throw new Error("Illegal name for Slot: " + nm);
}
};
// Converts a character
function convertSlotPathChar(c) {
var code = c.charCodeAt(0),
hex = code.toString(16),
buf = "$";
if (code < 0x10) {
buf += "0" + hex;
} else if (code < 0x100) {
buf += hex;
} else if (code < 0x1000) {
buf += "u0" + hex;
} else {
buf += "u" + hex;
}
return buf;
}
/**
* Escape the string so it becomes a valid name for a slot.
*
* @see baja.SlotPath.unescape
*
* @param {String} str the string to be escaped.
*
* @returns {String} the escaped String.
*/
SlotPath.escape = function (str) {
if (str.length === 0) {
return str;
}
// Convert first character
var res = str.charAt(0).replace(/[^a-zA-Z]/, function (c) {
return convertSlotPathChar(c);
});
if (str.length > 1) {
// Convert everything after first character
res += str.substring(1, str.length).replace(/[^a-zA-Z0-9_]/g, function (c) {
return convertSlotPathChar(c);
});
}
return res;
};
/**
* Unescape the string so all escaped characters become readable.
*
* @see baja.SlotPath.escape
*
* @param {String} str the string to be unescaped.
*
* @returns {String} the unescaped String.
*/
SlotPath.unescape = function (str) {
//Note: Any changes made here need to be made to lex.unescape, also.
//@see js.lex.unescape
if (str.length === 0) {
return str;
}
// Convert from $uxxxx
str = str.replace(/\$u[0-9a-fA-F]{4}/g, function (s) {
return String.fromCharCode(parseInt(s.substring(2, s.length), 16));
});
// Convert from $xx
str = str.replace(/\$[0-9a-fA-F]{2}/g, function (s) {
return String.fromCharCode(parseInt(s.substring(1, s.length), 16));
});
return str;
};
/**
* Sometimes a slot name will run through `escape` multiple times before
* getting to your code, so calling `baja.SlotPath.unescape` will still give
* you a string that's escaped and not very readable by humans.
*
* This function will essentially run `unescape` multiple times until it stops
* having any effect, removing all traces of `SlotPath.escape()`ing.
*
* Note that while `unescape(escape(str)) === str`, the same does not hold
* for `unescapeFully(escape(str))`, so do not use this function if you will
* need to translate back to a valid `Slot` name.
*
* @param str a slot name, potentially escaped multiple times
* @since Niagara 4.4
* @returns {String} a fully unescaped, human readable slot name
*/
SlotPath.unescapeFully = function (str) {
return SlotPath.unescape(str.replace(/\$(24)+/g, '$'));
};
/**
* Merge this path with the specified path.
*
* @param {baja.SlotPath} a
* @returns {String} the body of the SlotPath.
*/
SlotPath.prototype.merge = function (a) {
// if absolute then return a
if (a.isAbsolute()) {
return a.getBody();
}
// otherwise we have no backup or a backup
// contained within my path
var s = "",
backups,
i,
needSlash = false;
if (this.isAbsolute()) {
s += "/";
}
// if the backup is past me
if (a.getBackupDepth() > 0 && a.getBackupDepth() > this.depth()) {
// can't handle backup past absolute root
if (this.isAbsolute()) {
throw new Error("Invalid merge " + this + " + " + a);
}
backups = a.getBackupDepth() - this.depth() + this.getBackupDepth();
for (i = 0; i < backups; ++i) {
s += "../";
}
}
// Need to handle the case where this relative SlotPath contains a backup (that hasn't
// already been accounted for above). For example, when "slot:../../a/b/c" is followed by
// "slot:d/e/f", it needs to merge to "slot:../../a/b/c/d/e/f".
if (this.getBackupDepth() > 0 && s.length === 0) {
for (i = 0; i < this.getBackupDepth(); ++i) {
s += "../";
}
}
// add my path minus backup
for (i = 0; i < this.depth() - a.getBackupDepth(); ++i) {
if (needSlash) {
s += "/";
} else {
needSlash = true;
}
s += this.nameAt(i);
}
// now add relative path
for (i = 0; i < a.depth(); ++i) {
if (needSlash) {
s += '/';
} else {
needSlash = true;
}
s += a.nameAt(i);
}
return s;
};
/**
* Normalize the ORD Query list.
*
* @private
*
* @param {baja.OrdQueryList} list the ORD Query List.
* @param {Number} index the ORD Query List index.
* @returns {Boolean} true if the list was modified.
*/
SlotPath.prototype.normalize = function (list, index) {
var current = list.get(index),
next = list.get(index + 1),
modified = false,
currentSlotPath,
newSlotPath;
// Merge two Slot paths together
if (next && next.getSchemeName() === current.getSchemeName()) {
// Merge the slot paths together
currentSlotPath = this.makeSlotPath(current.getBody());
newSlotPath = this.makeSlotPath(currentSlotPath.merge(this.makeSlotPath(next.getBody())));
// Update the OrdQueryList
list.set(index, newSlotPath);
// Remove the next item from the list
list.remove(index + 1);
modified = true;
}
return modified;
};
/**
* Return all of the names for this SlotPath
*
* @returns {Array<String>} the names
*/ SlotPath.prototype.getNames = function () {
return this.$names.slice();
};
return SlotPath;
});