<commit_before>(function() {
  "use strict";

  class Store {

    static log() {
      if (!document.debug) {
        console.debug.apply(console, arguments);
      }
    }

    static defaultValueFactoryFactory(default_value) {
      return function() { return default_value; };
    }

    // Add methods for application level field in format of `{value: <factory()>}`
    static addAccessToAppField(field_name, factory) {
      var capitalized = $.camelize(field_name);
      var field = field_name;

      var getMethodName = `get${capitalized}`;
      var saveMethodName = `save${capitalized}`;
      var deleteMethodName = `delete${capitalized}`;
      var registerMethodName = `register${capitalized}`;


      // read
      this.prototype[getMethodName] = function() {
        var value = this.getJSON(field, factory);
        Store.log(`[store] ${getMethodName}:`, value);
        return value;
      };

      // write
      this.prototype[saveMethodName] = function(new_value) {
        Store.log(`[store] ${saveMethodName}:`, new_value);
        this.saveJSON(field, new_value);
        return new_value;
      };

      // delete
      this.prototype[deleteMethodName] = function() {
        var old_value = this[getMethodName]();
        Store.log(`[store] ${deleteMethodName}:`, old_value);
        this.saveJSON(field, factory);
        return old_value;
      };

      // register
      this.prototype[registerMethodName] = function(callback) {
        return this.register(field, callback);
      };

      return field_name;
    }

    // Add methods for account level field in format of `{<screen_name>: <factory()>}`
    static addAccessToAccountField(field_name, factory) {
      var capitalized = $.camelize(field_name);
      var field = field_name + 's';
      var account_default_factory = this.defaultValueFactoryFactory({});

      var getAllMethodName = `getAll${capitalized}s`;
      var getMethodName = `get${capitalized}`;
      var saveAllMethodName = `saveAll${capitalized}s`;
      var saveMethodName = `save${capitalized}`;
      var deleteAllMethodName = `deleteAll${capitalized}s`;
      var deleteMethodName = `delete${capitalized}`;
      var registerMethodName = `register${capitalized}s`;

      // read
      this.prototype[getAllMethodName] = function() {
        // the default value of whole field is always {}, because they are all saved in account space
        var values = this.getJSON(field, account_default_factory);
        Store.log(`[store] ${getAllMethodName}:`, values);
        return values;
      };
      this.prototype[getMethodName] = function(account) {
        var value = this[getAllMethodName]()[account.screen_name];
        Store.log(`[store] ${getMethodName}:`, account.screen_name, value);
        if ($.isDefined(value)) {
          return value;
        } else if ($.isDefined(factory)) {
          return this[saveMethodName](account, factory(account));
        } else {
          return null;
        }
      };

      // write
      this.prototype[saveAllMethodName] = function(new_value) {
        Store.log(`[store] ${saveAllMethodName}:`, new_value);
        this.saveJSON(field, new_value);
        return new_value;
      };
      this.prototype[saveMethodName] = function(account, new_value) {
        var values = this[getAllMethodName]();
        Store.log(`[store] ${saveMethodName}:`, account.screen_name, new_value);
        values[account.screen_name] = new_value;
        this.saveJSON(field, values);
        return new_value;
      };

      // delete
      this.prototype[deleteAllMethodName] = function() {
        Store.log(`[store] ${deleteAllMethodName}:`);
        this.saveJSON(field, account_default_factory());
      };
      this.prototype[deleteMethodName] = function(account) {
        var values = this[getAllMethodName]();
        var old_value = values[account.screen_name];
        Store.log(`[store] ${deleteMethodName}:`, account.screen_name, old_value);
        if ($.isDefined(old_value)) {
          delete values[account.screen_name];
        }
        this.saveJSON(field, values);
        return old_value;
      };

      // register
      this.prototype[registerMethodName] = function(callback) {
        return this.register(field, callback);
      };

      return field_name;
    }

    constructor() {
      this.storage = {};
      this.callbacks = {};
    }

    extends(fields_object) {
      return Object.assign(this, fields_object);
    }

    register(field, callback) {
      if (!this.callbacks[field]) {
        this.callbacks[field] = [];
      }
      this.callbacks[field].push(callback);
      Store.log('[store] register:', field, this.callbacks);
    }

    // Don't call saveJSON! Calls may infinitely loop.
    // We don't have to save the default value to local storage,
    // since we can create the same/similar one every time.
    getJSON(field, factory) {
      if (field in this.storage) {
        return this.storage[field];
      }
      var data = this.getLocalStorageItem(field);
      if (! $.isDefined(data)) {
        data = factory();
      }
      this.storage[field] = data;
      return data;
    }

    saveJSON(field, value) {
      var old_value = this.getJSON(field, Store.defaultValueFactoryFactory(value));
      this.storage[field] = value;
      this.saveLocalStorageItem(field, value);
      Store.log('[store] saveJSON:', field, value, old_value, this.callbacks);
      if (this.callbacks[field]) {
        Store.log('[store] trigger callbacks', field);
        this.callbacks[field].forEach((callback) => {
          callback(value, old_value);
        });
      }
      return value;
    }

    getLocalStorageItem(field) {
      try {
        return JSON.parse(localStorage.getItem(field));
      } catch (e) {
        console.error('[storage] getLocalStorageItem: broken data, drop it.', field);
        localStorage.removeItem(field);
        return undefined;
      }
    }

    saveLocalStorageItem(field, value) {
      return localStorage.setItem(field, JSON.stringify(value));
    }
  }


  var store = new Store();


  const DEFAULT_CONFIG =
  {
    "filters": [
      "isBetweenUser('ranmocy', 'CathellieAir')",
    ],
    "channels": [{
      "name": "Home",
      "sources": {
        "statuses_homeTimeline": {},
      },
      "filters": [
        "tweetContainsAny('SomeDirtyWord', 'SomeUninterestingKeyword', 'OrSomeBoringEventName')"
      ],
    }, {
      "name": "Mentions",
      "sources": {
        "statuses_mentionsTimeline": {},
      },
    }, {
      "name": "Direct Messages",
      "sources": {
        "directMessages": {},
        "directMessages_sent": {},
      },
    }, {
      "name": "Mix sources",
      "sources": {
        "statuses_homeTimeline": {},
        "statuses_userTimeline": { "screen_name": "ranmocy" },
      },
      "filters": [
        "sender.screen_name == 'CathellieAir' && receiver.screen_name == 'ranmocy'",
      ],
    }, {
      "name": "My tweets",
      "sources": {
        "statuses_userTimeline": { "user_id": "me" },
      }
    }]
  };
  const CONFIG_VERIFIER_TEMPLATE = {
    'filters_optional': [
      '',
    ],
    'channels': [
      {
        'name': '',
        'sources': {
          '': {},
        },
        'filters_optional': [
          '',
        ]
      }
    ]
  };
  /* Returns array of error messages. */
  function verifyConfigWithTemplate(template, config) {
    let error_messages = [];
    let template_str = JSON.stringify(template).replace('_optional', '');
    if ($.isDefined(template)) {
      if ($.isDefined(config)) {

        if ($.isString(template)) {
          if ($.isString(config)) {<commit_msg>add config spec<commit_after>(function() {
  "use strict";

  class Store {

    static log() {
      if (!document.debug) {
        console.debug.apply(console, arguments);
      }
    }

    static defaultValueFactoryFactory(default_value) {
      return function() { return default_value; };
    }

    // Add methods for application level field in format of `{value: <factory()>}`
    static addAccessToAppField(field_name, factory) {
      var capitalized = $.camelize(field_name);
      var field = field_name;

      var getMethodName = `get${capitalized}`;
      var saveMethodName = `save${capitalized}`;
      var deleteMethodName = `delete${capitalized}`;
      var registerMethodName = `register${capitalized}`;


      // read
      this.prototype[getMethodName] = function() {
        var value = this.getJSON(field, factory);
        Store.log(`[store] ${getMethodName}:`, value);
        return value;
      };

      // write
      this.prototype[saveMethodName] = function(new_value) {
        Store.log(`[store] ${saveMethodName}:`, new_value);
        this.saveJSON(field, new_value);
        return new_value;
      };

      // delete
      this.prototype[deleteMethodName] = function() {
        var old_value = this[getMethodName]();
        Store.log(`[store] ${deleteMethodName}:`, old_value);
        this.saveJSON(field, factory);
        return old_value;
      };

      // register
      this.prototype[registerMethodName] = function(callback) {
        return this.register(field, callback);
      };

      return field_name;
    }

    // Add methods for account level field in format of `{<screen_name>: <factory()>}`
    static addAccessToAccountField(field_name, factory) {
      var capitalized = $.camelize(field_name);
      var field = field_name + 's';
      var account_default_factory = this.defaultValueFactoryFactory({});

      var getAllMethodName = `getAll${capitalized}s`;
      var getMethodName = `get${capitalized}`;
      var saveAllMethodName = `saveAll${capitalized}s`;
      var saveMethodName = `save${capitalized}`;
      var deleteAllMethodName = `deleteAll${capitalized}s`;
      var deleteMethodName = `delete${capitalized}`;
      var registerMethodName = `register${capitalized}s`;

      // read
      this.prototype[getAllMethodName] = function() {
        // the default value of whole field is always {}, because they are all saved in account space
        var values = this.getJSON(field, account_default_factory);
        Store.log(`[store] ${getAllMethodName}:`, values);
        return values;
      };
      this.prototype[getMethodName] = function(account) {
        var value = this[getAllMethodName]()[account.screen_name];
        Store.log(`[store] ${getMethodName}:`, account.screen_name, value);
        if ($.isDefined(value)) {
          return value;
        } else if ($.isDefined(factory)) {
          return this[saveMethodName](account, factory(account));
        } else {
          return null;
        }
      };

      // write
      this.prototype[saveAllMethodName] = function(new_value) {
        Store.log(`[store] ${saveAllMethodName}:`, new_value);
        this.saveJSON(field, new_value);
        return new_value;
      };
      this.prototype[saveMethodName] = function(account, new_value) {
        var values = this[getAllMethodName]();
        Store.log(`[store] ${saveMethodName}:`, account.screen_name, new_value);
        values[account.screen_name] = new_value;
        this.saveJSON(field, values);
        return new_value;
      };

      // delete
      this.prototype[deleteAllMethodName] = function() {
        Store.log(`[store] ${deleteAllMethodName}:`);
        this.saveJSON(field, account_default_factory());
      };
      this.prototype[deleteMethodName] = function(account) {
        var values = this[getAllMethodName]();
        var old_value = values[account.screen_name];
        Store.log(`[store] ${deleteMethodName}:`, account.screen_name, old_value);
        if ($.isDefined(old_value)) {
          delete values[account.screen_name];
        }
        this.saveJSON(field, values);
        return old_value;
      };

      // register
      this.prototype[registerMethodName] = function(callback) {
        return this.register(field, callback);
      };

      return field_name;
    }

    constructor() {
      this.storage = {};
      this.callbacks = {};
    }

    extends(fields_object) {
      return Object.assign(this, fields_object);
    }

    register(field, callback) {
      if (!this.callbacks[field]) {
        this.callbacks[field] = [];
      }
      this.callbacks[field].push(callback);
      Store.log('[store] register:', field, this.callbacks);
    }

    // Don't call saveJSON! Calls may infinitely loop.
    // We don't have to save the default value to local storage,
    // since we can create the same/similar one every time.
    getJSON(field, factory) {
      if (field in this.storage) {
        return this.storage[field];
      }
      var data = this.getLocalStorageItem(field);
      if (! $.isDefined(data)) {
        data = factory();
      }
      this.storage[field] = data;
      return data;
    }

    saveJSON(field, value) {
      var old_value = this.getJSON(field, Store.defaultValueFactoryFactory(value));
      this.storage[field] = value;
      this.saveLocalStorageItem(field, value);
      Store.log('[store] saveJSON:', field, value, old_value, this.callbacks);
      if (this.callbacks[field]) {
        Store.log('[store] trigger callbacks', field);
        this.callbacks[field].forEach((callback) => {
          callback(value, old_value);
        });
      }
      return value;
    }

    getLocalStorageItem(field) {
      try {
        return JSON.parse(localStorage.getItem(field));
      } catch (e) {
        console.error('[storage] getLocalStorageItem: broken data, drop it.', field);
        localStorage.removeItem(field);
        return undefined;
      }
    }

    saveLocalStorageItem(field, value) {
      return localStorage.setItem(field, JSON.stringify(value));
    }
  }


  var store = new Store();


  const DEFAULT_CONFIG =
  {
    "filters": [
      "isBetweenUser('ranmocy', 'CathellieAir')",
    ],
    "channels": [{
      "name": "Home",
      "sources": {
        "statuses_homeTimeline": {},
      },
      "filters": [
        "tweetContainsAny('SomeDirtyWord', 'SomeUninterestingKeyword', 'OrSomeBoringEventName')"
      ],
    }, {
      "name": "Mentions",
      "sources": {
        "statuses_mentionsTimeline": {},
      },
    }, {
      "name": "Direct Messages",
      "sources": {
        "directMessages": {},
        "directMessages_sent": {},
      },
    }, {
      "name": "Mix sources",
      "sources": {
        "statuses_homeTimeline": {},
        "statuses_userTimeline": { "screen_name": "ranmocy" },
      },
      "filters": [
        "sender.screen_name == 'CathellieAir' && receiver.screen_name == 'ranmocy'",
      ],
    }, {
      "name": "<NAME>",
      "sources": {
        "statuses_userTimeline": { "user_id": "me" },
      }
    }]
  };
  const CONFIG_SPEC = {
    name: 'entire config',
    type: 'object',
    required: {
      channels: {
        name: 'channels',
        type: 'array',
        elements: {
          name: 'channel',
          type: 'object',
          required: {
            name: {
              name: 'channel name',
              type: 'string',
            },
            sources: {
              name: 'channel sources',
              type: 'object',
              values: {
                name: 'channel source parameters',
                type: 'object'
              }
            }
          },
          optional: {
            filters: {
              name: 'channel filters',
              type: 'array',
              elements: {
                name: 'channel filter rule',
                type: 'string',
              }
            }
          }
        }
      }
    },
    optional: {
      filters: {
        name: 'global filters',
        type: 'array',
        elements: {
          name: 'global filter rule',
          type: 'string',
        }
      }
    }
  };
  const CONFIG_VERIFIER_TEMPLATE = {
    'filters_optional': [''],
    'channels': [
      {
        'name': '',
        'sources': {
          '': {},
        },
        'filters_optional': ['']
      }
    ]
  };
  /* Returns array of error messages. */
  function verifyConfigWithTemplate(template, config) {
    let error_messages = [];
    let template_str = JSON.stringify(template).replace('_optional', '');
    if ($.isDefined(template)) {
      if ($.isDefined(config)) {

        if ($.isString(template)) {
          if ($.isString(config)) {
{'fraction_non_alphanumeric': 0.13976329917032698, 'fraction_numerical': 0.0, 'mean_word_length': 2.346193100632782, 'pattern_counts': {'":': 46, '<': 10, '<?xml version=': 0, '>': 12, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '300983', 'n_tokens_mistral': 4824, 'n_tokens_neox': 4755, 'n_words': 937}