// stringmap.js // MIT licensed, see LICENSE file // Copyright (c) 2013 Olov Lassus var StringMap = (function() { "use strict"; // to save us a few characters var hasOwnProperty = Object.prototype.hasOwnProperty; var create = (function() { function hasOwnEnumerableProps(obj) { for (var prop in obj) { if (hasOwnProperty.call(obj, prop)) { return true; } } return false; } // FF <= 3.6: // o = {}; o.hasOwnProperty("__proto__" or "__count__" or "__parent__") => true // o = {"__proto__": null}; Object.prototype.hasOwnProperty.call(o, "__proto__" or "__count__" or "__parent__") => false function hasOwnPollutedProps(obj) { return hasOwnProperty.call(obj, "__count__") || hasOwnProperty.call(obj, "__parent__"); } var useObjectCreate = false; if (typeof Object.create === "function") { if (!hasOwnEnumerableProps(Object.create(null))) { useObjectCreate = true; } } if (useObjectCreate === false) { if (hasOwnEnumerableProps({})) { throw new Error("StringMap environment error 0, please file a bug at https://github.com/olov/stringmap/issues"); } } // no throw yet means we can create objects without own enumerable props (safe-guard against VMs and shims) var o = (useObjectCreate ? Object.create(null) : {}); var useProtoClear = false; if (hasOwnPollutedProps(o)) { o.__proto__ = null; if (hasOwnEnumerableProps(o) || hasOwnPollutedProps(o)) { throw new Error("StringMap environment error 1, please file a bug at https://github.com/olov/stringmap/issues"); } useProtoClear = true; } // no throw yet means we can create objects without own polluted props (safe-guard against VMs and shims) return function() { var o = (useObjectCreate ? Object.create(null) : {}); if (useProtoClear) { o.__proto__ = null; } return o; }; })(); // stringmap ctor function stringmap(optional_object) { // use with or without new if (!(this instanceof stringmap)) { return new stringmap(optional_object); } this.obj = create(); this.hasProto = false; // false (no __proto__ key) or true (has __proto__ key) this.proto = undefined; // value for __proto__ key when hasProto is true, undefined otherwise if (optional_object) { this.setMany(optional_object); } }; // primitive methods that deals with data representation stringmap.prototype.has = function(key) { // The type-check of key in has, get, set and delete is important because otherwise an object // {toString: function() { return "__proto__"; }} can avoid the key === "__proto__" test. // The alternative to type-checking would be to force string conversion, i.e. key = String(key); if (typeof key !== "string") { throw new Error("StringMap expected string key"); } return (key === "__proto__" ? this.hasProto : hasOwnProperty.call(this.obj, key)); }; stringmap.prototype.get = function(key) { if (typeof key !== "string") { throw new Error("StringMap expected string key"); } return (key === "__proto__" ? this.proto : (hasOwnProperty.call(this.obj, key) ? this.obj[key] : undefined)); }; stringmap.prototype.set = function(key, value) { if (typeof key !== "string") { throw new Error("StringMap expected string key"); } if (key === "__proto__") { this.hasProto = true; this.proto = value; } else { this.obj[key] = value; } }; stringmap.prototype.remove = function(key) { if (typeof key !== "string") { throw new Error("StringMap expected string key"); } var didExist = this.has(key); if (key === "__proto__") { this.hasProto = false; this.proto = undefined; } else { delete this.obj[key]; } return didExist; }; // alias remove to delete but beware: // sm.delete("key"); // OK in ES5 and later // sm['delete']("key"); // OK in all ES versions // sm.remove("key"); // OK in all ES versions stringmap.prototype['delete'] = stringmap.prototype.remove; stringmap.prototype.isEmpty = function() { for (var key in this.obj) { if (hasOwnProperty.call(this.obj, key)) { return false; } } return !this.hasProto; }; stringmap.prototype.size = function() { var len = 0; for (var key in this.obj) { if (hasOwnProperty.call(this.obj, key)) { ++len; } } return (this.hasProto ? len + 1 : len); }; stringmap.prototype.keys = function() { var keys = []; for (var key in this.obj) { if (hasOwnProperty.call(this.obj, key)) { keys.push(key); } } if (this.hasProto) { keys.push("__proto__"); } return keys; }; stringmap.prototype.values = function() { var values = []; for (var key in this.obj) { if (hasOwnProperty.call(this.obj, key)) { values.push(this.obj[key]); } } if (this.hasProto) { values.push(this.proto); } return values; }; stringmap.prototype.items = function() { var items = []; for (var key in this.obj) { if (hasOwnProperty.call(this.obj, key)) { items.push([key, this.obj[key]]); } } if (this.hasProto) { items.push(["__proto__", this.proto]); } return items; }; // methods that rely on the above primitives stringmap.prototype.setMany = function(object) { if (object === null || (typeof object !== "object" && typeof object !== "function")) { throw new Error("StringMap expected Object"); } for (var key in object) { if (hasOwnProperty.call(object, key)) { this.set(key, object[key]); } } return this; }; stringmap.prototype.merge = function(other) { var keys = other.keys(); for (var i = 0; i < keys.length; i++) { var key = keys[i]; this.set(key, other.get(key)); } return this; }; stringmap.prototype.map = function(fn) { var keys = this.keys(); for (var i = 0; i < keys.length; i++) { var key = keys[i]; keys[i] = fn(this.get(key), key); // re-use keys array for results } return keys; }; stringmap.prototype.forEach = function(fn) { var keys = this.keys(); for (var i = 0; i < keys.length; i++) { var key = keys[i]; fn(this.get(key), key); } }; stringmap.prototype.clone = function() { var other = stringmap(); return other.merge(this); }; stringmap.prototype.toString = function() { var self = this; return "{" + this.keys().map(function(key) { return JSON.stringify(key) + ":" + JSON.stringify(self.get(key)); }).join(",") + "}"; }; return stringmap; })(); if (typeof module !== "undefined" && typeof module.exports !== "undefined") { module.exports = StringMap; }