(function () { 'use strict'; angular .module('ds.objectDiff', []) .factory('ObjectDiff', objectDiff) .filter('toJsonView', toJsonViewFilter) .filter('toJsonDiffView', toJsonDiffViewFilter) .filter('objToJsonView', objToJsonViewFilter); objectDiff.$inject = ['$sce']; toJsonViewFilter.$inject = ['ObjectDiff']; toJsonDiffViewFilter.$inject = ['ObjectDiff']; objToJsonViewFilter.$inject = ['ObjectDiff']; /* service implementation */ function objectDiff($sce) { var openChar = '{', closeChar = '}', service = { setOpenChar: setOpenChar, setCloseChar: setCloseChar, diff: diff, diffOwnProperties: diffOwnProperties, toJsonView: formatToJsonXMLString, objToJsonView: formatObjToJsonXMLString, toJsonDiffView: formatChangesToXMLString }; return service; /* service methods */ /** * @param char */ function setOpenChar(char) { openChar = char; } /** * @param char */ function setCloseChar(char) { closeChar = char; } /** * diff between object a and b * @param {Object} a * @param {Object} b * @param shallow * @param isOwn * @return {Object} */ function diff(a, b, shallow, isOwn) { if (a === b) { return equalObj(a); } var diffValue = {}; var equal = true; for (var key in a) { if ((!isOwn && key in b) || (isOwn && typeof b != 'undefined' && b.hasOwnProperty(key))) { if (a[key] === b[key]) { diffValue[key] = equalObj(a[key]); } else { if (!shallow && isValidAttr(a[key], b[key])) { var valueDiff = diff(a[key], b[key], shallow, isOwn); if (valueDiff.changed == 'equal') { diffValue[key] = equalObj(a[key]); } else { equal = false; diffValue[key] = valueDiff; } } else { equal = false; diffValue[key] = { changed: 'primitive change', removed: a[key], added: b[key] } } } } else { equal = false; diffValue[key] = { changed: 'removed', value: a[key] } } } for (key in b) { if ((!isOwn && !(key in a)) || (isOwn && typeof a != 'undefined' && !a.hasOwnProperty(key))) { equal = false; diffValue[key] = { changed: 'added', value: b[key] } } } if (equal) { return equalObj(a); } else { return { changed: 'object change', value: diffValue } } } /** * diff between object a and b own properties only * @param {Object} a * @param {Object} b * @return {Object} * @param deep */ function diffOwnProperties(a, b, shallow) { return diff(a, b, shallow, true); } /** * Convert to a readable xml/html Json structure * @param {Object} changes * @return {string} * @param shallow */ function formatToJsonXMLString(changes, shallow) { var properties = []; var diff = changes.value; if (changes.changed == 'equal') { return $sce.trustAsHtml(inspect(diff, shallow)); } for (var key in diff) { properties.push(formatChange(key, diff[key], shallow)); } return $sce.trustAsHtml('' + openChar + '\n
' + properties.join(',\n') + '\n
' + closeChar + ''); } /** * Convert to a readable xml/html Json structure * @return {string} * @param obj * @param shallow */ function formatObjToJsonXMLString(obj, shallow) { return $sce.trustAsHtml(inspect(obj, shallow)); } /** * Convert to a readable xml/html Json structure * @param {Object} changes * @return {string} * @param shallow */ function formatChangesToXMLString(changes, shallow) { var properties = []; if (changes.changed == 'equal') { return ''; } var diff = changes.value; for (var key in diff) { var changed = diff[key].changed; if (changed !== 'equal') properties.push(formatChange(key, diff[key], shallow, true)); } return $sce.trustAsHtml('' + openChar + '\n
' + properties.join(',\n') + '\n
' + closeChar + ''); } /** * @param obj * @returns {{changed: string, value: *}} */ function equalObj(obj) { return { changed: 'equal', value: obj } } /** * @param a * @param b * @returns {*|boolean} */ function isValidAttr(a, b) { var typeA = typeof a; var typeB = typeof b; return (a && b && (typeA == 'object' || typeA == 'function') && (typeB == 'object' || typeB == 'function')); } /** * @param key * @param diffItem * @returns {*} * @param shallow * @param diffOnly */ function formatChange(key, diffItem, shallow, diffOnly) { var changed = diffItem.changed; var property; switch (changed) { case 'equal': property = (stringifyObjectKey(escapeHTML(key)) + ': ' + inspect(diffItem.value)); break; case 'removed': property = ('' + stringifyObjectKey(escapeHTML(key)) + ': ' + inspect(diffItem.value) + ''); break; case 'added': property = ('' + stringifyObjectKey(escapeHTML(key)) + ': ' + inspect(diffItem.value) + ''); break; case 'primitive change': var prefix = stringifyObjectKey(escapeHTML(key)) + ': '; property = ( '' + prefix + inspect(diffItem.removed) + ',\n' + '' + prefix + inspect(diffItem.added) + ''); break; case 'object change': property = shallow ? '' : (stringifyObjectKey(key) + ': ' + ( diffOnly ? formatChangesToXMLString(diffItem) : formatToJsonXMLString(diffItem))); break; } return property; } /** * @param {string} key * @return {string} */ function stringifyObjectKey(key) { return /^[a-z0-9_$]*$/i.test(key) ? key : JSON.stringify(key); } /** * @param {string} string * @return {string} */ function escapeHTML(string) { return string.replace(/&/g, '&').replace(//g, '>'); } /** * @param {Object} obj * @return {string} * @param shallow */ function inspect(obj, shallow) { return _inspect('', obj, shallow); /** * @param {string} accumulator * @param {object} obj * @see http://jsperf.com/continuation-passing-style/3 * @return {string} * @param shallow */ function _inspect(accumulator, obj, shallow) { switch (typeof obj) { case 'object': if (!obj) { accumulator += 'null'; break; } if (shallow) { accumulator += '[object]'; break; } var keys = Object.keys(obj); var length = keys.length; if (length === 0) { accumulator += '' + openChar + closeChar + ''; } else { accumulator += '' + openChar + '\n
'; for (var i = 0; i < length; i++) { var key = keys[i]; accumulator = _inspect(accumulator + stringifyObjectKey(escapeHTML(key)) + ': ', obj[key]); if (i < length - 1) { accumulator += ',\n'; } } accumulator += '\n
' + closeChar + '' } break; case 'string': accumulator += JSON.stringify(escapeHTML(obj)); break; case 'undefined': accumulator += 'undefined'; break; default: accumulator += escapeHTML(String(obj)); break; } return accumulator; } } } /* filter implementation */ function toJsonViewFilter(ObjectDiff) { return function (value) { return ObjectDiff.toJsonView(value); }; } function toJsonDiffViewFilter(ObjectDiff) { return function (value) { return ObjectDiff.toJsonDiffView(value); }; } function objToJsonViewFilter(ObjectDiff) { return function (value) { return ObjectDiff.objToJsonView(value); }; } })();