vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

355 lines
12 KiB

9 years ago
  1. var util = require('../util');
  2. let errorFound = false;
  3. let allOptions;
  4. let printStyle = 'background: #FFeeee; color: #dd0000';
  5. /**
  6. * Used to validate options.
  7. */
  8. class Validator {
  9. /**
  10. * @ignore
  11. */
  12. constructor() {
  13. }
  14. /**
  15. * Main function to be called
  16. * @param {Object} options
  17. * @param {Object} referenceOptions
  18. * @param {Object} subObject
  19. * @returns {boolean}
  20. * @static
  21. */
  22. static validate(options, referenceOptions, subObject) {
  23. errorFound = false;
  24. allOptions = referenceOptions;
  25. let usedOptions = referenceOptions;
  26. if (subObject !== undefined) {
  27. usedOptions = referenceOptions[subObject];
  28. }
  29. Validator.parse(options, usedOptions, []);
  30. return errorFound;
  31. }
  32. /**
  33. * Will traverse an object recursively and check every value
  34. * @param {Object} options
  35. * @param {Object} referenceOptions
  36. * @param {array} path | where to look for the actual option
  37. * @static
  38. */
  39. static parse(options, referenceOptions, path) {
  40. for (let option in options) {
  41. if (options.hasOwnProperty(option)) {
  42. Validator.check(option, options, referenceOptions, path);
  43. }
  44. }
  45. }
  46. /**
  47. * Check every value. If the value is an object, call the parse function on that object.
  48. * @param {string} option
  49. * @param {Object} options
  50. * @param {Object} referenceOptions
  51. * @param {array} path | where to look for the actual option
  52. * @static
  53. */
  54. static check(option, options, referenceOptions, path) {
  55. if (referenceOptions[option] === undefined && referenceOptions.__any__ === undefined) {
  56. Validator.getSuggestion(option, referenceOptions, path);
  57. return;
  58. }
  59. let referenceOption = option;
  60. let is_object = true;
  61. if (referenceOptions[option] === undefined && referenceOptions.__any__ !== undefined) {
  62. // NOTE: This only triggers if the __any__ is in the top level of the options object.
  63. // THAT'S A REALLY BAD PLACE TO ALLOW IT!!!!
  64. // TODO: Examine if needed, remove if possible
  65. // __any__ is a wildcard. Any value is accepted and will be further analysed by reference.
  66. referenceOption = '__any__';
  67. // if the any-subgroup is not a predefined object in the configurator,
  68. // we do not look deeper into the object.
  69. is_object = (Validator.getType(options[option]) === 'object');
  70. }
  71. else {
  72. // Since all options in the reference are objects, we can check whether
  73. // they are supposed to be the object to look for the __type__ field.
  74. // if this is an object, we check if the correct type has been supplied to account for shorthand options.
  75. }
  76. let refOptionObj = referenceOptions[referenceOption];
  77. if (is_object && refOptionObj.__type__ !== undefined) {
  78. refOptionObj = refOptionObj.__type__;
  79. }
  80. Validator.checkFields(option, options, referenceOptions, referenceOption, refOptionObj, path);
  81. }
  82. /**
  83. *
  84. * @param {string} option | the option property
  85. * @param {Object} options | The supplied options object
  86. * @param {Object} referenceOptions | The reference options containing all options and their allowed formats
  87. * @param {string} referenceOption | Usually this is the same as option, except when handling an __any__ tag.
  88. * @param {string} refOptionObj | This is the type object from the reference options
  89. * @param {Array} path | where in the object is the option
  90. * @static
  91. */
  92. static checkFields(option, options, referenceOptions, referenceOption, refOptionObj, path) {
  93. let log = function(message) {
  94. console.log('%c' + message + Validator.printLocation(path, option), printStyle);
  95. };
  96. let optionType = Validator.getType(options[option]);
  97. let refOptionType = refOptionObj[optionType];
  98. if (refOptionType !== undefined) {
  99. // if the type is correct, we check if it is supposed to be one of a few select values
  100. if (Validator.getType(refOptionType) === 'array' && refOptionType.indexOf(options[option]) === -1) {
  101. log('Invalid option detected in "' + option + '".' +
  102. ' Allowed values are:' + Validator.print(refOptionType) +
  103. ' not "' + options[option] + '". ');
  104. errorFound = true;
  105. }
  106. else if (optionType === 'object' && referenceOption !== "__any__") {
  107. path = util.copyAndExtendArray(path, option);
  108. Validator.parse(options[option], referenceOptions[referenceOption], path);
  109. }
  110. }
  111. else if (refOptionObj['any'] === undefined) {
  112. // type of the field is incorrect and the field cannot be any
  113. log('Invalid type received for "' + option +
  114. '". Expected: ' + Validator.print(Object.keys(refOptionObj)) +
  115. '. Received [' + optionType + '] "' + options[option] + '"');
  116. errorFound = true;
  117. }
  118. }
  119. /**
  120. *
  121. * @param {Object|boolean|number|string|Array.<number>|Date|Node|Moment|undefined|null} object
  122. * @returns {string}
  123. * @static
  124. */
  125. static getType(object) {
  126. var type = typeof object;
  127. if (type === 'object') {
  128. if (object === null) {
  129. return 'null';
  130. }
  131. if (object instanceof Boolean) {
  132. return 'boolean';
  133. }
  134. if (object instanceof Number) {
  135. return 'number';
  136. }
  137. if (object instanceof String) {
  138. return 'string';
  139. }
  140. if (Array.isArray(object)) {
  141. return 'array';
  142. }
  143. if (object instanceof Date) {
  144. return 'date';
  145. }
  146. if (object.nodeType !== undefined) {
  147. return 'dom';
  148. }
  149. if (object._isAMomentObject === true) {
  150. return 'moment';
  151. }
  152. return 'object';
  153. }
  154. else if (type === 'number') {
  155. return 'number';
  156. }
  157. else if (type === 'boolean') {
  158. return 'boolean';
  159. }
  160. else if (type === 'string') {
  161. return 'string';
  162. }
  163. else if (type === undefined) {
  164. return 'undefined';
  165. }
  166. return type;
  167. }
  168. /**
  169. * @param {string} option
  170. * @param {Object} options
  171. * @param {Array.<string>} path
  172. * @static
  173. */
  174. static getSuggestion(option, options, path) {
  175. let localSearch = Validator.findInOptions(option,options,path,false);
  176. let globalSearch = Validator.findInOptions(option,allOptions,[],true);
  177. let localSearchThreshold = 8;
  178. let globalSearchThreshold = 4;
  179. let msg;
  180. if (localSearch.indexMatch !== undefined) {
  181. msg = ' in ' + Validator.printLocation(localSearch.path, option,'') +
  182. 'Perhaps it was incomplete? Did you mean: "' + localSearch.indexMatch + '"?\n\n';
  183. }
  184. else if (globalSearch.distance <= globalSearchThreshold && localSearch.distance > globalSearch.distance) {
  185. msg = ' in ' + Validator.printLocation(localSearch.path, option,'') +
  186. 'Perhaps it was misplaced? Matching option found at: ' +
  187. Validator.printLocation(globalSearch.path, globalSearch.closestMatch,'');
  188. }
  189. else if (localSearch.distance <= localSearchThreshold) {
  190. msg = '. Did you mean "' + localSearch.closestMatch + '"?' +
  191. Validator.printLocation(localSearch.path, option);
  192. }
  193. else {
  194. msg = '. Did you mean one of these: ' + Validator.print(Object.keys(options)) +
  195. Validator.printLocation(path, option);
  196. }
  197. console.log('%cUnknown option detected: "' + option + '"' + msg, printStyle);
  198. errorFound = true;
  199. }
  200. /**
  201. * traverse the options in search for a match.
  202. * @param {string} option
  203. * @param {Object} options
  204. * @param {Array} path | where to look for the actual option
  205. * @param {boolean} [recursive=false]
  206. * @returns {{closestMatch: string, path: Array, distance: number}}
  207. * @static
  208. */
  209. static findInOptions(option, options, path, recursive = false) {
  210. let min = 1e9;
  211. let closestMatch = '';
  212. let closestMatchPath = [];
  213. let lowerCaseOption = option.toLowerCase();
  214. let indexMatch = undefined;
  215. for (let op in options) { // eslint-disable-line guard-for-in
  216. let distance;
  217. if (options[op].__type__ !== undefined && recursive === true) {
  218. let result = Validator.findInOptions(option, options[op], util.copyAndExtendArray(path,op));
  219. if (min > result.distance) {
  220. closestMatch = result.closestMatch;
  221. closestMatchPath = result.path;
  222. min = result.distance;
  223. indexMatch = result.indexMatch;
  224. }
  225. }
  226. else {
  227. if (op.toLowerCase().indexOf(lowerCaseOption) !== -1) {
  228. indexMatch = op;
  229. }
  230. distance = Validator.levenshteinDistance(option, op);
  231. if (min > distance) {
  232. closestMatch = op;
  233. closestMatchPath = util.copyArray(path);
  234. min = distance;
  235. }
  236. }
  237. }
  238. return {closestMatch:closestMatch, path:closestMatchPath, distance:min, indexMatch: indexMatch};
  239. }
  240. /**
  241. * @param {Array.<string>} path
  242. * @param {Object} option
  243. * @param {string} prefix
  244. * @returns {String}
  245. * @static
  246. */
  247. static printLocation(path, option, prefix = 'Problem value found at: \n') {
  248. let str = '\n\n' + prefix + 'options = {\n';
  249. for (let i = 0; i < path.length; i++) {
  250. for (let j = 0; j < i + 1; j++) {
  251. str += ' ';
  252. }
  253. str += path[i] + ': {\n'
  254. }
  255. for (let j = 0; j < path.length + 1; j++) {
  256. str += ' ';
  257. }
  258. str += option + '\n';
  259. for (let i = 0; i < path.length + 1; i++) {
  260. for (let j = 0; j < path.length - i; j++) {
  261. str += ' ';
  262. }
  263. str += '}\n'
  264. }
  265. return str + '\n\n';
  266. }
  267. /**
  268. * @param {Object} options
  269. * @returns {String}
  270. * @static
  271. */
  272. static print(options) {
  273. return JSON.stringify(options).replace(/(\")|(\[)|(\])|(,"__type__")/g, "").replace(/(\,)/g, ', ')
  274. }
  275. /**
  276. * Compute the edit distance between the two given strings
  277. * http://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#JavaScript
  278. *
  279. * Copyright (c) 2011 Andrei Mackenzie
  280. *
  281. * 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:
  282. *
  283. * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  284. *
  285. * 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.
  286. *
  287. * @param {string} a
  288. * @param {string} b
  289. * @returns {Array.<Array.<number>>}}
  290. * @static
  291. */
  292. static levenshteinDistance(a, b) {
  293. if (a.length === 0) return b.length;
  294. if (b.length === 0) return a.length;
  295. var matrix = [];
  296. // increment along the first column of each row
  297. var i;
  298. for (i = 0; i <= b.length; i++) {
  299. matrix[i] = [i];
  300. }
  301. // increment each column in the first row
  302. var j;
  303. for (j = 0; j <= a.length; j++) {
  304. matrix[0][j] = j;
  305. }
  306. // Fill in the rest of the matrix
  307. for (i = 1; i <= b.length; i++) {
  308. for (j = 1; j <= a.length; j++) {
  309. if (b.charAt(i - 1) == a.charAt(j - 1)) {
  310. matrix[i][j] = matrix[i - 1][j - 1];
  311. } else {
  312. matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
  313. Math.min(matrix[i][j - 1] + 1, // insertion
  314. matrix[i - 1][j] + 1)); // deletion
  315. }
  316. }
  317. }
  318. return matrix[b.length][a.length];
  319. }
  320. }
  321. export default Validator;
  322. export {printStyle}