From 5d3af8bd8f566b6b2aa5f0e8ce781d4ae2f1b93e Mon Sep 17 00:00:00 2001
From: Tristan Cavelier <tristan.cavelier@tiolive.com>
Date: Fri, 31 May 2013 13:44:10 +0200
Subject: [PATCH] WIP: Great changes on complex queries

Change to a module compatible with nodejs, requirejs, web workers and classic
browsers. Provides classes Query, SimpleQuery, ComplexQuery. Documentation
comming soon.
---
 src/queries/begin.js        |  30 ++-
 src/queries/complexquery.js |  17 ++
 src/queries/end.js          |  21 +-
 src/queries/parser-begin.js |  12 +-
 src/queries/parser-end.js   |   4 +-
 src/queries/query.js        | 433 +++++++++++++++++++++---------------
 src/queries/queryfactory.js |  22 ++
 src/queries/serializer.js   |  40 ++--
 src/queries/simplequery.js  | 138 ++++++++++++
 src/queries/tool.js         | 115 ++++++++++
 10 files changed, 617 insertions(+), 215 deletions(-)
 create mode 100644 src/queries/complexquery.js
 create mode 100644 src/queries/queryfactory.js
 create mode 100644 src/queries/simplequery.js
 create mode 100644 src/queries/tool.js

diff --git a/src/queries/begin.js b/src/queries/begin.js
index 7a60b32..c7bfd9a 100644
--- a/src/queries/begin.js
+++ b/src/queries/begin.js
@@ -4,11 +4,27 @@
 * http://www.gnu.org/licenses/lgpl.html
 */
 
-(function (scope) {
+/**
+ * Provides some function to use complex queries with item list
+ *
+ * @module complex_queries
+ */
+var complex_queries;
+(function () {
   "use strict";
-  Object.defineProperty(scope, "ComplexQueries", {
-    configurable: false,
-    enumerable: false,
-    writable: false,
-    value: {}
-  });
+  var to_export = {}, module_name = "complex_queries";
+  /**
+   * Add a secured (write permission denied) property to an object.
+   * @method defineProperty
+   * @param  {Object} object The object to fill
+   * @param  {String} key The object key where to store the property
+   * @param  {Any} value The value to store
+   */
+  function _export(key, value) {
+    Object.defineProperty(to_export, key, {
+      "configurable": false,
+      "enumerable": true,
+      "writable": false,
+      "value": value
+    });
+  }
diff --git a/src/queries/complexquery.js b/src/queries/complexquery.js
new file mode 100644
index 0000000..c55d2e7
--- /dev/null
+++ b/src/queries/complexquery.js
@@ -0,0 +1,17 @@
+// XXX
+var ComplexQuery = newClass(Query, function () {
+
+  /**
+   * Filter the item list only if all the sub queries match this item according
+   * to the logical operator.
+   * See {{#crossLink "Query/exec:method"}}{{/crossLink}}
+   */
+  this.exec = function () {
+    todo
+  };
+});
+todo
+
+query_class_dict.complex = ComplexQuery;
+
+_export("ComplexQuery", ComplexQuery);
diff --git a/src/queries/end.js b/src/queries/end.js
index 1a0684c..8ebe8eb 100644
--- a/src/queries/end.js
+++ b/src/queries/end.js
@@ -1,2 +1,21 @@
 
-}(jIO));
+  if (typeof define === "function" && define.amd) {
+    define(to_export);
+  } else if (typeof window === "object") {
+    Object.defineProperty(window, module_name, {
+      configurable: false,
+      enumerable: true,
+      writable: false,
+      value: to_export
+    });
+  } else if (typeof exports === "object") {
+    var i;
+    for (i in to_export) {
+      if (to_export.hasOwnProperty(i)) {
+        exports[i] = to_export[i];
+      }
+    }
+  } else {
+    complex_queries = to_export;
+  }
+}());
diff --git a/src/queries/parser-begin.js b/src/queries/parser-begin.js
index 4593660..633aa38 100644
--- a/src/queries/parser-begin.js
+++ b/src/queries/parser-begin.js
@@ -1,5 +1,7 @@
-Object.defineProperty(scope.ComplexQueries, "parse", {
-  configurable: false,
-  enumerable: false,
-  writable: false,
-  value: function (string) {
+/**
+ * Parse a text request to a json query tree
+ * @method parseStringToObject
+ * @param  {String} string The string to parse
+ * @return {Object} The json query tree
+ */
+function parseStringToObject(string) {
diff --git a/src/queries/parser-end.js b/src/queries/parser-end.js
index 2876c3a..4698002 100644
--- a/src/queries/parser-end.js
+++ b/src/queries/parser-end.js
@@ -1,4 +1,4 @@
   return result;
-  }
+}
 
-});
+}; // parseStringToQuery
diff --git a/src/queries/query.js b/src/queries/query.js
index 340242d..39e362c 100644
--- a/src/queries/query.js
+++ b/src/queries/query.js
@@ -1,186 +1,255 @@
-Object.defineProperty(scope.ComplexQueries,"query",{
-    configurable:false,enumerable:false,writable:false,
-    value: function (query, object_list) {
-        var wildcard_character = typeof query.wildcard_character === 'string' ?
-            query.wildcard_character : '%',
-        operator_actions = {
-            '=':   function (value1, value2) {
-                value1 = '' + value1;
-                return value1.match (convertToRegexp (
-                    value2, wildcard_character
-                )) || false && true;
-            },
-            '!=':  function (value1, value2) {
-                value1 = '' + value1;
-                return !(value1.match (convertToRegexp (
-                    value2, wildcard_character
-                )));
-            },
-            '<':   function (value1, value2) { return value1 < value2; },
-            '<=':  function (value1, value2) { return value1 <= value2; },
-            '>':   function (value1, value2) { return value1 > value2; },
-            '>=':  function (value1, value2) { return value1 >= value2; },
-            'AND': function (item, query_list) {
-                var i;
-                for (i=0; i<query_list.length; ++i) {
-                    if (! itemMatchesQuery (item, query_list[i])) {
-                        return false;
-                    }
-                }
-                return true;
-            },
-            'OR':  function (item, query_list) {
-                var i;
-                for (i=0; i<query_list.length; ++i) {
-                    if (itemMatchesQuery (item, query_list[i])) {
-                        return true;
-                    }
-                }
-                return false;
-            },
-            'NOT': function (item, query_list) {
-                return !itemMatchesQuery(item, query_list[0]);
-            }
-        },
-        convertToRegexp = function (string) {
-            return subString('^' + string.replace(
-                new RegExp(
-                    '([\\{\\}\\(\\)\\^\\$\\&\\.\\*\\?\\\/\\+\\|\\[\\]\\-\\\\])'.
-                        replace (wildcard_character?
-                                 '\\'+wildcard_character:undefined,''),
-                    'g'
-                ),
-                '\\$1'
-            ) + '$',(wildcard_character||undefined), '.*');
-        },
-        subString = function (string, substring, newsubstring) {
-            var res = '', i = 0;
-            if (substring === undefined) {
-                return string;
-            }
-            while (1) {
-                var tmp = string.indexOf(substring,i);
-                if (tmp === -1) {
-                    break;
-                }
-                for (; i < tmp; ++i) {
-                    res += string[i];
-                }
-                res += newsubstring;
-                i += substring.length;
-            }
-            for (; i<string.length; ++i) {
-                res += string[i];
-            }
-            return res;
-        },
-        itemMatchesQuery = function (item, query_object) {
-            var i;
-            if (query_object.type === 'complex') {
-                return operator_actions[query_object.operator](
-                    item, query_object.query_list
-                );
-            } else {
-                if (query_object.id) {
-                    if (typeof item[query_object.id] !== 'undefined') {
-                        return operator_actions[query_object.operator](
-                            item[query_object.id], query_object.value
-                        );
-                    } else {
-                        return false;
-                    }
-                } else {
-                    return true;
-                }
-            }
-        },
-        select = function (list, select_list) {
-            var i;
-            if (select_list.length === 0) {
-                return;
-            }
-            for (i=0; i<list.length; ++i) {
-                var list_value = {}, k;
-                for (k=0; k<select_list.length; ++k) {
-                    list_value[select_list[k]] =
-                        list[i][select_list[k]];
-                }
-                list[i] = list_value;
-            }
-        },
-        sortFunction = function (key, asc) {
-            if (asc === 'descending') {
-                return function (a,b) {
-                    return a[key] < b[key] ? 1 : a[key] > b[key] ? -1 : 0;
-                };
-            }
-            return function (a,b) {
-                return a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0;
-            };
-        },
-        mergeList = function (list, list_to_merge, index) {
-            var i,j;
-            for (i = index,j = 0; i < list_to_merge.length + index; ++i, ++j) {
-                list[i] = list_to_merge[j];
-            }
-        },
-        sort = function (list, sort_list) {
-            var i, tmp, key, asc, sortAndMerge = function() {
-                sort(tmp,sort_list.slice(1));
-                mergeList(list,tmp,i-tmp.length);
-                tmp = [list[i]];
-            };
-            if (list.length < 2) {
-                return;
-            }
-            if (sort_list.length === 0) {
-                return;
-            }
-            key = sort_list[0][0];
-            asc = sort_list[0][1];
-            list.sort (sortFunction (key,asc));
-            tmp = [list[0]];
-            for (i = 1; i < list.length; ++i) {
-                if (tmp[0][key] === list[i][key]) {
-                    tmp.push(list[i]);
-                } else {
-                    sortAndMerge();
-                }
-            }
-            sortAndMerge();
-        },
-        limit = function (list, limit_list) {
-            var i;
-            if (typeof limit_list[0] !== 'undefined') {
-                if (typeof limit_list[1] !== 'undefined') {
-                    if (list.length > limit_list[1] + limit_list[0]) {
-                        list.length = limit_list[1] + limit_list[0];
-                    }
-                    list.splice(0,limit_list[0]);
-                } else {
-                    list.length = limit_list[0];
-                }
-            }
-        },
-        ////////////////////////////////////////////////////////////
-        result_list = [], result_list_tmp = [], j;
-        object_list = object_list || [];
-        if (query.query === undefined) {
-            result_list = object_list;
-        } else {
-          for (j=0; j<object_list.length; ++j) {
-              if ( itemMatchesQuery (
-                  object_list[j], scope.ComplexQueries.parse (query.query)
-              )) {
-                  result_list.push(object_list[j]);
-              }
+/**
+ * The query to use to filter a list of objects.
+ * This is an abstract class.
+ *
+ * @class Query
+ * @constructor
+ */
+var Query = newClass(function() {
+  /**
+   * Creates a new item list with matching item only
+   *
+   * @method exec
+   * @param  {Array} item_list The list of object
+   * @param  {Object} [option={}] Some operation option
+   * @param  {String} [option.wildcard_character="%"] The wildcard character
+   * @return {Array} The new item list
+   */
+  this.exec = function (item_list, option) {};
+
+  /**
+   * Test if an item matches this query
+   * @method match
+   * @param  {Object} item The object to test
+   * @return {Boolean} true if match, false otherwise
+   */
+  this.match = function (item, wildcard_character) {};
+
+
+  /**
+   * Convert this query to a parsable string.
+   * @method toString
+   * @return {String} The string version of this query
+   */
+  this.toString = function () {};
+
+  /**
+   * Convert this query to an jsonable object in order to be remake thanks to
+   * QueryFactory class.
+   *
+   * @method serialized
+   * @return {Object} The jsonable object
+   */
+  this.serialized = function () {};
+}, {"static_methods": {
+  // XXX
+  "filterListSelect": function (select_option, list) {
+    list.forEach(function (item, index) {
+      var new_item = {};
+      select_option.forEach(function (key) {
+        new_item[key] = item[key];
+      });
+      list[index] = new_item;
+    });
+  },
+  // XXX
+  "sortOn": function (sort_on_option, list) {
+    var sort_index;
+    for (sort_index = sort_on_option.length - 1; sort_index >= 0;
+         sort_index += 1) {
+      list.sort(sortFunction(
+        sort_on_option[sort_index][0],
+        sort_on_option[sort_index][1]
+      ));
+    }
+  },
+  "parseStringToQuery": parseStringToQuery
+}});
+
+_export("Query", Query);
+
+
+function query(query, object_list) {
+  var wildcard_character = typeof query.wildcard_character === "string" ?
+    query.wildcard_character : "%",
+  // A list of methods according to operators
+    operator_actions = {
+      "=": function (value1, value2) {
+        value1 = value1.toString();
+        return value1.match(convertToRegexp(
+          value2, wildcard_character
+        )) || false && true;
+      },
+      '!=':  function (value1, value2) {
+        value1 = value1.toString();
+        return !(value1.match(convertToRegexp(
+          value2, wildcard_character
+        )));
+      },
+      '<':   function (value1, value2) { return value1 < value2; },
+      '<=':  function (value1, value2) { return value1 <= value2; },
+      '>':   function (value1, value2) { return value1 > value2; },
+      '>=':  function (value1, value2) { return value1 >= value2; },
+      'AND': function (item, query_list) {
+        var i;
+        for (i=0; i<query_list.length; ++i) {
+          if (! itemMatchesQuery (item, query_list[i])) {
+            return false;
           }
         }
-        if (query.filter) {
-            select(result_list,query.filter.select_list || []);
-            sort(result_list,query.filter.sort_on || []);
-            limit(result_list,query.filter.limit || []);
+        return true;
+      },
+      'OR':  function (item, query_list) {
+        var i;
+        for (i=0; i<query_list.length; ++i) {
+          if (itemMatchesQuery (item, query_list[i])) {
+            return true;
+          }
+        }
+        return false;
+      },
+      'NOT': function (item, query_list) {
+        return !itemMatchesQuery(item, query_list[0]);
+      }
+    },
+    convertToRegexp = function (string) {
+      return subString('^' + string.replace(
+        new RegExp(
+          '([\\{\\}\\(\\)\\^\\$\\&\\.\\*\\?\\\/\\+\\|\\[\\]\\-\\\\])'.
+            replace(wildcard_character ?
+                    "\\" + wildcard_character : undefined, ""),
+          "g"
+        ),
+        "\\$1"
+      ) + '$',(wildcard_character||undefined), '.*');
+    },
+    subString = function (string, substring, newsubstring) {
+      var res = '', i = 0;
+      if (substring === undefined) {
+        return string;
+      }
+      while (true) {
+        var tmp = string.indexOf(substring, i);
+        if (tmp === -1) {
+          break;
+        }
+        for (; i < tmp; i += 1) {
+          res += string[i];
         }
-        return result_list;
+        res += newsubstring;
+        i += substring.length;
+      }
+      for (; i < string.length; i += 1) {
+        res += string[i];
+      }
+      return res;
+    },
+  itemMatchesQuery = function (item, query_object) {
+    var i;
+    if (query_object.type === 'complex') {
+      return operator_actions[query_object.operator](
+        item, query_object.query_list
+      );
+    } else {
+      if (query_object.id) {
+        if (typeof item[query_object.id] !== 'undefined') {
+          return operator_actions[query_object.operator](
+            item[query_object.id], query_object.value
+          );
+        } else {
+          return false;
+        }
+      } else {
+        return true;
+      }
+    }
+  },
+  select = function (list, select_list) {
+    var i;
+    if (select_list.length === 0) {
+      return;
+    }
+    for (i=0; i<list.length; ++i) {
+      var list_value = {}, k;
+      for (k=0; k<select_list.length; ++k) {
+        list_value[select_list[k]] =
+          list[i][select_list[k]];
+      }
+      list[i] = list_value;
+    }
+  },
+  sortFunction = function (key, asc) {
+    if (asc === 'descending') {
+      return function (a,b) {
+        return a[key] < b[key] ? 1 : a[key] > b[key] ? -1 : 0;
+      };
+    }
+    return function (a,b) {
+      return a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0;
+    };
+  },
+  mergeList = function (list, list_to_merge, index) {
+    var i,j;
+    for (i = index,j = 0; i < list_to_merge.length + index; ++i, ++j) {
+      list[i] = list_to_merge[j];
+    }
+  },
+  sort = function (list, sort_list) {
+    var i, tmp, key, asc, sortAndMerge = function() {
+      sort(tmp,sort_list.slice(1));
+      mergeList(list,tmp,i-tmp.length);
+      tmp = [list[i]];
+    };
+    if (list.length < 2) {
+      return;
+    }
+    if (sort_list.length === 0) {
+      return;
+    }
+    key = sort_list[0][0];
+    asc = sort_list[0][1];
+    list.sort (sortFunction (key,asc));
+    tmp = [list[0]];
+    for (i = 1; i < list.length; ++i) {
+      if (tmp[0][key] === list[i][key]) {
+        tmp.push(list[i]);
+      } else {
+        sortAndMerge();
+      }
+    }
+    sortAndMerge();
+  },
+  limit = function (list, limit_list) {
+    var i;
+    if (typeof limit_list[0] !== 'undefined') {
+      if (typeof limit_list[1] !== 'undefined') {
+        if (list.length > limit_list[1] + limit_list[0]) {
+          list.length = limit_list[1] + limit_list[0];
+        }
+        list.splice(0,limit_list[0]);
+      } else {
+        list.length = limit_list[0];
+      }
+    }
+  },
+  ////////////////////////////////////////////////////////////
+  result_list = [], result_list_tmp = [], j;
+  object_list = object_list || [];
+  if (query.query === undefined) {
+    result_list = object_list;
+  } else {
+    for (j=0; j<object_list.length; ++j) {
+      if ( itemMatchesQuery (
+        object_list[j], scope.ComplexQueries.parse (query.query)
+      )) {
+        result_list.push(object_list[j]);
+      }
     }
-});
+  }
+  if (query.filter) {
+    select(result_list,query.filter.select_list || []);
+    sort(result_list,query.filter.sort_on || []);
+    limit(result_list,query.filter.limit || []);
+  }
+  return result_list;
+}
diff --git a/src/queries/queryfactory.js b/src/queries/queryfactory.js
new file mode 100644
index 0000000..18584f1
--- /dev/null
+++ b/src/queries/queryfactory.js
@@ -0,0 +1,22 @@
+/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
+/*global _export: true, ComplexQuery: true, SimpleQuery: true,
+         newClass: true,
+         sortFunction: true, convertSearchTextToRegExp: true */
+
+// XXX
+var query_class_dict = {}, query_factory = {};
+
+newClass.apply(query_factory, [{
+  "secure_methods": true
+}, function () {
+  // XXX
+  this.create = function (object) {
+    if (typeof object.type === "string" &&
+        query_class_dict[object.type]) {
+      return new query_class_dict[object.type](object);
+    }
+    return null;
+  };
+}]); // end QueryFactory
+
+_export("factory", query_factory);
diff --git a/src/queries/serializer.js b/src/queries/serializer.js
index 2b190d7..fceb3ff 100644
--- a/src/queries/serializer.js
+++ b/src/queries/serializer.js
@@ -1,18 +1,22 @@
-Object.defineProperty(scope.ComplexQueries,"serialize",{
-    configurable:false,enumerable:false,writable:false,value:function(query){
-        var str_list = [], i;
-        if (query.type === 'complex') {
-            str_list.push ( '(' );
-            for (i=0; i<query.query_list.length; ++i) {
-                str_list.push( scope.ComplexQueries.serialize(query.query_list[i]) );
-                str_list.push( query.operator );
-            }
-            str_list.length --;
-            str_list.push ( ')' );
-            return str_list.join(' ');
-        } else if (query.type === 'simple') {
-            return query.id + (query.id?': ':'') + query.operator + ' "' + query.value + '"';
-        }
-        return query;
-    }
-});
+/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
+/*global _export: true, to_export: true */
+
+function objectToSearchText(query) {
+  var str_list = [];
+  if (query.type === "complex") {
+    str_list.push("(");
+    (query.query_list || []).forEach(function (sub_query) {
+      str_list.push(objectToSearchText(sub_query));
+      str_list.push(query.operator);
+    });
+    str_list.length -= 1;
+    str_list.push(")");
+    return str_list.join(" ");
+  }
+  if (query.type === "simple") {
+    return query.id + (query.id ? ": " : "") + (query.operator || "=") + ' "' +
+      query.value + '"';
+  }
+  throw new TypeError("This object is not a query");
+}
+_export("objectToSearchText", objectToSearchText);
diff --git a/src/queries/simplequery.js b/src/queries/simplequery.js
new file mode 100644
index 0000000..bc9035a
--- /dev/null
+++ b/src/queries/simplequery.js
@@ -0,0 +1,138 @@
+/**
+ * The SimpleQuery inherits from Query, and compares one metadata value
+ *
+ * @class SimpleQuery
+ * @param  {Object} [spec={}] The specifications
+ * @param  {String} [spec.operator="="] The compare method to use
+ * @param  {String} spec.key The metadata key
+ * @param  {String} spec.value The value of the metadata to compare
+ * @param  {String} [spec.wildcard_character="%"] The wildcard character
+ */
+var SimpleQuery = newClass(Query, function (spec) {
+  /**
+   * Operator to use to compare object values
+   *
+   * @property operator
+   * @type String
+   * @default "="
+   */
+  this.operator = spec.operator || "=";
+
+  /**
+   * Key of the object which refers to the value to compare
+   *
+   * @property key
+   * @type String
+   */
+  this.key = spec.key;
+
+  /**
+   * Value is used to do the comparison with the object value
+   *
+   * @property value
+   * @type String
+   */
+  this.value = spec.value;
+
+  /**
+   * The wildcard character used to extend comparison action
+   *
+   * @property wildcard_character
+   * @type String
+   */
+  this.wildcard_character = spec.wildcard_character || "%";
+
+  /**
+   * #crossLink "Query/exec:method"
+   */
+  this.exec = function (item_list, option) {
+    var new_item_list = [];
+    item_list.forEach(function (item) {
+      if (!this.match(item, option.wildcard_character)) {
+        new_item_list.push(item);
+      }
+    });
+    if (option.sort_on) {
+      Query.sortOn(option.sort_on, new_item_list);
+    }
+    if (option.limit) {
+      new_item_list = new_item_list.slice(
+        option.limit[0],
+        option.limit[1] + option.limit[0] + 1
+      );
+    }
+    if (option.select_list) {
+      Query.filterListSelect(option.select_list, new_item_list);
+    }
+    return new_item_list;
+  };
+
+  /**
+   * #crossLink "Query/match:method"
+   */
+  this.match = function (item, wildcard_character) {
+    this[this.operator](item[this.key], this.value, wildcard_character);
+  };
+
+  /**
+   * #crossLink "Query/toString:method"
+   */
+  this.toString = function () {
+    return (this.key ? this.key + ": " : "") + (this.operator || "=") + ' "' +
+      this.value + '"';
+  };
+
+  /**
+   * #crossLink "Query/serialized:method"
+   */
+  this.serialized = function () {
+    return {
+      "type": "simple",
+      "operator": this.operator,
+      "key": this.key,
+      "value": this.value
+    };
+  };
+
+  // XXX
+  this["="] = function (object_value, comparison_value,
+                        wildcard_character) {
+    return convertSearchTextToRegExp(
+      comparison_value.toString(),
+      wildcard_character || this.wildcard_character
+    ).test(object_value.toString());
+  };
+
+  // XXX
+  this["!="] = function (object_value, comparison_value,
+                                          wildcard_character) {
+    return !convertSearchTextToRegExp(
+      comparison_value.toString(),
+      wildcard_character || this.wildcard_character
+    ).test(object_value.toString());
+  };
+
+  // XXX
+  this["<"] = function (object_value, comparison_value) {
+    return object_value < comparison_value;
+  };
+
+  // XXX
+  this["<="] = function (object_value, comparison_value) {
+    return object_value <= comparison_value;
+  };
+
+  // XXX
+  this[">"] = function (object_value, comparison_value) {
+    return object_value > comparison_value;
+  };
+
+  // XXX
+  this[">="] = function (object_value, comparison_value) {
+    return object_value >= comparison_value;
+  };
+});
+
+query_class_dict.simple = SimpleQuery;
+
+_export("SimpleQuery", SimpleQuery);
diff --git a/src/queries/tool.js b/src/queries/tool.js
new file mode 100644
index 0000000..aace7ca
--- /dev/null
+++ b/src/queries/tool.js
@@ -0,0 +1,115 @@
+/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
+/*global _export: true */
+
+#!/usr/bin/env node
+
+// keywords: js, javascript, new class creator, generator
+
+/**
+ * Create a class, manage inheritance, static methods,
+ * protected attributes and can hide methods or/and secure methods
+ *
+ * @method newClass
+ * @param  {Class} Class Classes to inherit from (0..n)
+ * @param  {Object} option Class option (0..n)
+ * @param  {Boolean} [option.secure_methods=false] Make methods not configurable
+ *                                                 and not writable
+ * @param  {Boolean} [option.hide_methods=false] Make methods not enumerable
+ * @param  {Object} [option.static_methods={}] Add static methods
+ * @param  {Function} constructor The new class constructor
+ * @return {Class} The new class
+ */
+function newClass() {
+  var j, k, constructors = [], option, new_class;
+
+  for (j = 0; j < arguments.length; j += 1) {
+    if (typeof arguments[j] === "function") {
+      constructors.push(arguments[j]);
+    } else if (typeof arguments[j] === "object") {
+      option = option || {};
+      for (k in arguments[j]) {
+        if (arguments[j].hasOwnProperty(k)) {
+          option[j] = arguments[j][k];
+        }
+      }
+    }
+  }
+
+  function postCreate(that) {
+    // modify the object according to 'option'
+    var key;
+    if (option) {
+      for (key in that) {
+        if (that.hasOwnProperty(key)) {
+          if (typeof that[key] === "function") {
+            Object.defineProperty(that, key, {
+              configurable: option.secure_methods ? false : true,
+              enumerable: option.hide_methods ? false : true,
+              writable: option.secure_methods ? false : true,
+              value: that[key]
+            });
+          }
+        }
+      }
+    }
+  }
+
+  new_class = function (spec, my) {
+    var i;
+    spec = spec || {};
+    my = my || {};
+    // don't use forEach !
+    for (i = 0; i < constructors.length; i += 1) {
+      constructors[i].apply(this, [spec, my]);
+    }
+    postCreate(this);
+    return this;
+  };
+  for (j in (option.static_methods || {})) {
+    if (option.static_methods.hasOwnProperty(j)) {
+      new_class[j] = option.static_methods[j];
+    }
+  }
+  postCreate(new_class);
+  return new_class;
+}
+
+/**
+ * Escapes regexp special chars from a string.
+ * @method regexpEscapeString
+ * @param  {String} string The string to escape
+ * @return {String} The escaped string
+ */
+function regexpEscapeString(string) {
+  if (typeof string === "string") {
+    return string.replace(/([\\\.\$\[\]\(\)\{\}\^\?\*\+\-])/g, "\\$1");
+  }
+}
+_export("regexpEscapeString", regexpEscapeString);
+
+/**
+ * Convert a search text to a regexp.
+ * @method convertSearchTextToRegExp
+ * @param  {String} string The string to convert
+ * @param  {String} [wildcard_character=undefined] The wildcard chararter
+ * @return {RegExp} The search text regexp
+ */
+function convertSearchTextToRegExp(string, wildcard_character) {
+  return new RegExp("^" + regexpEscapeString(string).replace(
+    regexpEscapeString(wildcard_character),
+    '.*'
+  ) + "$");
+}
+_export("convertSearchTextToRegExp", convertSearchTextToRegExp);
+
+// XXX
+function sortFunction(key, way) {
+  if (way === 'descending') {
+    return function (a,b) {
+      return a[key] < b[key] ? 1 : a[key] > b[key] ? -1 : 0;
+    };
+  }
+  return function (a,b) {
+    return a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0;
+  };
+}
-- 
2.30.9