<commit_before>/*
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
var firmwarewizard = function() {
  var app = {};

  // helper functions
  function $(s) {
    return document.querySelector(s);
  }

  function toggleClass(e, cssClass) {
    setClass(e, cssClass, !hasClass(e, cssClass));
  }

  function hasClass(e, cssClass) {
    var searchstring = (' ' + e.className + ' ').replace(/[\n\t]/g, " ");
    return (searchstring.indexOf(' ' + cssClass+ ' ') !== -1);
  }

  function setClass(e, cssClass, active) {
    if (active && !hasClass(e, cssClass)) {
      e.className = (e.className+' '+cssClass).trim();
    } else if (!active && hasClass(e, cssClass)) {
      e.className = e.className.replace(cssClass, '');
    }
  }

  function show_inline(s) {
    $(s).style.display = 'inline-block';
  }

  function show_block(s) {
    $(s).style.display = 'block';
  }

  function hide(s) {
    $(s).style.display = 'none';
  }

  function sortCaseInsensitive(a, b) {
	return a.localeCompare(b, 'en', {'sensitivity': 'base'});
  }

  // Object.values() replacement
  function ObjectValues(obj) {
    return Object.keys(obj).map(function(key) { return obj[key]; });
  }

  function isEmptyObject(obj) {
      for (var name in obj) {
          return false;
      }
      return true;
  }

  function scrollDown() {
    try {
      window.scrollBy({
        top: 512,
        left: 0,
        behavior: 'smooth'
      });
    } catch (e) {
      var start = document.body.scrollTop;
      var lastTop = start - 1;
      var interval = window.setInterval(function() {
        if (document.body.scrollTop > start + 512 || lastTop - document.body.scrollTop === 0) {
          window.clearInterval(interval);
          console.log('stop');
        }
        lastTop = document.body.scrollTop;
        document.body.scrollTop += 4;
      }, 1);
    }
  }

  // constants
  var IGNORED_ELEMENTS = [
    './', '../', '.manifest',
    '-tftp', '-fat', '-loader', '-NA', '-x2-', '-hsv2', '-p1020'
  ];
  var PANE = {'MODEL': 0, 'IMAGETYPE': 1, 'BRANCH': 2};

  // the invisible separator is used to delimit searchstring parts, e.g.
  // "NanoStation[SEPARATOR]" vs. "NanoStation Loco[SEPARATOR]"
  var INVISIBLE_SEPARATOR = '\u2063';
  var NON_BREAKING_SPACE = '\u00A0';
  //var INVISIBLE_SEPARATOR = '#'; // debug
  //var NON_BREAKING_SPACE = '?'; // debug

  var MODEL_SEARCHSTRING = 0;
  var MODEL_STRIPPED_SEARCHSTRING = 1;
  var MODEL_VENDOR = 2;
  var MODEL_MODEL = 3;
  var MODEL_MATCHED_REVISION = 4;

  var wizard = parseWizardObject();
  app.currentVersions = {};
  var availableImages = {};
  var images = availableImages;
  var vendormodels_reverse;

  var typeNames = {
    'factory': 'Erstinstallation',
    'sysupgrade': 'Upgrade',
    'rootfs': "Root-Image",
    'kernel': "Kernel-Image",
    'bootloader': 'Bootloader-Image'
  };

  var branches = ObjectValues(config.directories).filter(function(e, index, self) {
    return index === self.indexOf(e);
  });

  var reFileExtension = new RegExp(/\.(bin|chk|img\.gz|img|tar|ubi)$/);
  var reRemoveDashes = new RegExp(/-/g);
  var reSearchable = new RegExp('[-/ '+NON_BREAKING_SPACE+']', 'g');
  var reRemoveSpaces = new RegExp(/ /g);
  var reStripDashes = new RegExp(/^\-+|\-+$/g);

  if (config.version_regex === undefined) {
    console.log("config.version_regex missing in config.js");
    return;
  }
  var reVersionRegex = new RegExp(config.version_regex);

  var rePrettyPrintVersionRegex;
  if(config.prettyPrintVersionRegex !== undefined) {
    rePrettyPrintVersionRegex = new RegExp(config.prettyPrintVersionRegex);
  }

  var PREVIEW_PICTURES_DIR = 'pictures/';
  if(config.preview_pictures !== undefined) {
    PREVIEW_PICTURES_DIR = config.preview_pictures;
  }

  var enabled_device_categories = ['recommended'];
  if ("enabled_device_categories" in config) {
    enabled_device_categories = config.enabled_device_categories;
  }
  if ("recommended_toggle" in config && config.recommended_toggle) {
    enabled_device_categories = ['recommended'];
    show_inline('.notRecommendedLink');

    if (config.recommended_info_link) {
      $('#notrecommendedinfo').innerHTML = '<p><a href="' + config.recommended_info_link + '" target="_new">Mehr Informationen</a>';
    }
  }

  function buildVendorModelsReverse() {
    var vendormodels_reverse = {};

    for (var device_category in config.vendormodels) {
      for (var vendor in config.vendormodels[device_category]) {
        var models = config.vendormodels[device_category][vendor];
        for (var model in models) {
          var match = models[model];
          if (typeof match == 'string') {
            addArray(vendormodels_reverse, match, {'vendor': vendor, 'model': model, 'revision': '', category: device_category});
          } else for (var m in match) {
            addArray(vendormodels_reverse, m, {'vendor': vendor, 'model': model, 'revision': match[m], category: device_category});
          }
        }
      }
    }

    return vendormodels_reverse;
  }

  function createHistoryState(wizard) {
    if (!window.history || !history.pushState) return;

    var parameters = '';
    for (var key in wizard) {
      if (wizard[key] != -1 && wizard[key] !== false) {
        parameters += '&' + key + '=' + encodeURIComponent(wizard[key]);
      }
    }

    // replace first occurence of "&" by "?"
    parameters = parameters.replace('&', '?');
    history.pushState(wizard, '', parameters || '?');
  }

  function parseWizardObject(wizard) {
    if (wizard === undefined || wizard === null) wizard = {};
    wizard.q                 = wizard.q || '';
    wizard.showFirmwareTable = (wizard.showFirmwareTable == 'true');
    return wizard;
  }

  function setFilteredImages() {
    images = {};
    for (var vendor in availableImages) {
      images[vendor] = {};
      for (var model in availableImages[vendor]) {
        for (var device in availableImages[vendor][model]) {
          if (enabled_device_categories.indexOf(availableImages[vendor][model][device].category) > -1) {
            addArray(images[vendor], model, availableImages[vendor][model][device]);
          }
        }
      }
    }
  }

  window.onpopstate = function(event) {
    if (event.state === null) return;
    wizard = parseWizardObject(event.state);
    $('.modelSearch').value = wizard.q;
    updateHTML(wizard);
  };

  window.onload = function() {
    if (config.title !== undefined) {
      document.title = config.title;
    }

    $('#notrecommendedselect').checked = false;

    function parseURLasJSON() {
      var search = location.search.substring(1);
      return search ? JSON.parse(
        '{"' + search.replace(/&/g, '","').replace(/=/g,'":"') + '"}',
        function(key, value) {
          return (key=== '') ? value:decodeURIComponent(value);
        }):{};
    }
    var parsedURL = parseURLasJSON();
    wizard = parseWizardObject(parsedURL);
    $('.modelSearch').value = wizard.q;

    $('.modelSearch').addEventListener('keyup', function(e) {
      firmwarewizard.updateSearchQuery($('.modelSearch').value);
    });

    $('#vendorselect').addEventListener('change', function(e) {
      firmwarewizard.setSearchQuery($('#vendorselect').value);
      scrollDown();
    });

    $('#modelselect').addEventListener('change', function(e) {
      firmwarewizard.setSearchQuery($('#modelselect').value);
      scrollDown();
    });

    $('#revisionselect').addEventListener('change', function(e) {
      firmwarewizard.setSearchQuery($('#revisionselect').value);
      scrollDown();
    });

    $('#wizard .notRecommendedLink').addEventListener('click', function(e) {
      toggleClass($('#model-pane'), 'show-notrecommended-warning');
    });

    $('#wizard .firmwareTableLink').addEventListener('click', function(e) {
      firmwarewizard.showFirmwareTable();
    });

    $('#firmwareTable .firmwareTableLink').addEventListener('click', function(e) {
      firmwarewizard.hideFirmwareTable();
    });

    $('#notrecommendedselect').addEventListener('change', function(e) {
      if (this.checked) {
        enabled_device_categories = config.enabled_device_categories;
      } else if ("enabled_device_categories" in config) {
        enabled_device_categories = ['recommended'];
      }
      setFilteredImages();
      updateHTML(wizard);
      updateFirmwareTable();
    });

    vendormodels_reverse = buildVendorModelsReverse();

    loadDirectories(function() {
      // initialize pictures for router models
      var previews = $('.imagePreview');
      var fullModelList = createSearchableModellist();
      for (var m in fullModelList) {
        var searchstring = fullModelList[m][MODEL_SEARCHSTRING];
        var vendor = fullModelList[m][MODEL_VENDOR];
        var model = fullModelList[m][MODEL_MODEL];
        previews.appendChild(createPicturePreview(vendor, model, searchstring));
      }
      setFilteredImages();
      updateHTML(wizard);
      show_block('.manualSelection');
      updateFirmwareTable();
    });

    // set link to firmware source directory
    for(var path in config.directories) {
      $('#firmware-source-dir').href = path.replace(/\/[^\/]*$/, '');
      break;
    }
  };

  function updateSearchQuery(query) {
    wizard.q = query;
    createHistoryState(wizard);
    updateHTML(wizard);
  }
  app.updateSearchQuery = updateSearchQuery;

  function setSearchQuery(query) {
    $('.modelSearch').value = query;
    updateSearchQuery(query);
  }
  app.setSearchQuery = setSearchQuery;

  app.showFirmwareTable = function() {
    wizard.showFirmwareTable = true;
    createHistoryState(wizard);
    updateHTML(wizard);
  };

  app.hideFirmwareTable = function() {
    wizard.showFirmwareTable = false;
    createHistoryState(wizard);
    updateHTML(wizard);
  };

  // exclude file names containing a string
  function ignoreFileName(name) {
    for (var i in IGNORED_ELEMENTS) {
      if (name.indexOf(IGNORED_ELEMENTS[i]) != -1) {
        return true;
      }
    }
    return false;
  }

  // simplified version string sort
  function sortByRevision(revisions) {
    revisions.sort(function(a, b) {
        a = a.revision; b = b.revision;
        if (a.length > b.length) return 1;
        if (a.length < b.length) return -1;
        if (a > b) return 1;
        if (a < b) return -1;
        return 0;
      });
    return revisions;
  }

  function findType(name) {
    var m = /-(sysupgrade|factory|rootfs|kernel|bootloader)[-.]/.exec(name);
    return m ? m[1] : 'factory';
  }

  function findVersion(name) {
    var m = reVersionRegex.exec(name);
    return m ? m[1] : '';
  }

  // regex that is applied to the version to print a prettier version
  function prettyPrintVersion(version) {
    if (rePrettyPrintVersionRegex !== undefined) {
      var v = rePrettyPrintVersionRegex.exec(version);
      return v ? v[1] : version;
    }
    return version;
  }

  function findRevision(name) {
    // reversion identifier like a1, v2
    var m = /-([a-z][0-9]+(.[0-9]+)?)[.-]/.exec(name);
    return m ? m[1] : 'alle';
  }

  function findRegion(name) {
    var m = /-(cn|de|en|eu|il|jp|us)[.-]/.exec(name);
    return m ? m[1] : '';
  }

  function findSize(name) {
    var m = /-(4M|8M|16M|32M|64M)[.-]/.exec(name);
    return m ? m[1] : '';
  }

  function addArray(obj, key, value) {
    if (key in obj) {
      obj[key].push(value);
    } else {
      obj[key] = [value];
    }
  }

  function parseFilePath(match, basePath, filename, branch) {
    var location = basePath + encodeURIComponent(filename);

    var devices = vendormodels_reverse[match];
    if (!(devices instanceof Array) || devices.length != 1) {
      console.log("Error: vendormodels_reverse did not contain", match);
      return;
    }
    var device = devices[0];

    if (device.model == '--ignore--' || device.revision == '--ignore--') {
      return;
    }

    var strippedFilename = filename;
    strippedFilename = strippedFilename.replace(config.community_prefix, '-');

    var version = findVersion(strippedFilename);
    strippedFilename = strippedFilename.replace(version, '');

    strippedFilename = strippedFilename.replace(match, '');

    var revision = device.revision || findRevision(strippedFilename);
    strippedFilename = strippedFilename.replace(revision, '');

    var region = findRegion(strippedFilename);
    strippedFilename = strippedFilename.replace(region, '');

    if (region.length) {
      revision += '-' + region;
    }

    var size = findSize(strippedFilename);
    strippedFilename = strippedFilename.replace(size, '');

    var type = findType(strippedFilename);
    strippedFilename = strippedFilename.replace(type, '');

    strippedFilename = strippedFilename.replace(reFileExtension, '');
    strippedFilename = strippedFilename.replace(reRemoveDashes, '');

    if (strippedFilename !== '') {
      console.log("Match for file", filename, "was not exhaustive. Missing filename parts:", strippedFilename);
      return;
    }

    // derive preview file name
    var preview = filename;
    preview = preview.replace(config.community_prefix, '');
    preview = preview.replace(version, '');
    preview = preview.replace(revision, '');
    preview = preview.replace(region, '');
    preview = preview.replace(size, '');
    preview = preview.replace(type, '');
    preview = preview.replace(reFileExtension, '');
    preview = preview.replace(reStripDashes, '');

    // vendor and model specific fine tuning
    preview = preview.replace('alfa-network', 'alfa');
    preview = preview.replace('buffalo-wzr-hp-ag300h', 'buffalo-wzr-hp-ag300h-wzr-600dhp');
    preview = preview.replace('buffalo-wzr-600dhp', 'buffalo-wzr-hp-ag300h-wzr-600dhp');
    preview = preview.replace('buffalo-wzr-hp-g300nh2', 'buffalo-wzr-hp-g300nh');
    preview = preview.replace('d-link-dir-505-rev', 'd-link-dir-505-rev-a1');
    preview = preview.replace('d-link-dir-825-rev', 'd-link-dir-825-rev-b1');
    preview = preview.replace('gl-inet-6408a', 'gl-inet-6408a-v1');
    preview = preview.replace('gl-inet-6416a', 'gl-inet-6416a-v1');
    preview = preview.replace('netgear-wndrmac', 'netgear-wndrmacv2');
    preview = preview.replace('openmesh-mr600', 'openmesh-mr600-v1');
    preview = preview.replace('openmesh-mr900', 'openmesh-mr900-v1');
    if (preview.indexOf('tp-link') != -1) preview += '-' + revision;
    preview = preview.replace('tp-link-archer-c5-v1', 'tp-link-archer-c7-v2'); // Archer C5 v1 and Archer C7 v2 are identical
    preview = preview.replace('tp-link-tl-wa801n-nd-v3', 'tp-link-tl-wa801n-nd-v2'); // no preview picture for v3 yet
    preview = preview.replace('tp-link-tl-wr940n-v3', 'tp-link-tl-wr940n-v2'); // no preview picture for v3 yet
    preview = preview.replace('ubiquiti-unifi-ap', 'ubiquiti-unifi');
    preview = preview.replace('ubiquiti-unifi-lr', 'ubiquiti-unifi');
    preview = preview.replace('ubiquiti-unifi-pro', 'ubiquiti-unifi-ap-pro');
    preview = preview.replace('x86-virtualbox', 'x86-virtualbox.vdi');
    preview = preview.replace('x86-64-virtualbox', 'x86-virtualbox.vdi');
    preview = preview.replace('x86-vmware', 'x86-vmware.vmdk');
    preview = preview.replace('x86-64-vmware', 'x86-vmware.vmdk');
    preview = preview.replace('x86-generic', 'x86-generic.img');
    preview = preview.replace('x86-64', 'x86-generic.img');
    preview = preview.replace('x86-kvm', 'x86-kvm.img');
    preview = preview.replace('x86-generic.img.vdi', 'x86-virtualbox.vdi');
    preview = preview.replace('x86-generic.img.vmdk', 'x86-vmware.vmdk');

    // collect branch versions
    app.currentVersions[branch] = version;

    if (!(device.vendor in availableImages)) {
      availableImages[device.vendor] = {};
    }

    addArray(availableImages[device.vendor], device.model, {
      'revision': revision,
      'branch': branch,
      'type': type,
      'version': version,
      'location': location,
      'size': size,
      'preview': preview+".jpg",
      'category': device.category
    });
  }

  function createOption(value, title, selectedOption) {
    var o = document.createElement('option');
    o.value = value;
    o.innerText = title;
    o.selected = (value === selectedOption);
    return o;
  }

  function getRevisions(currentVendor, currentModel) {
    var models = images[currentVendor];
    if (models === undefined) {
      return [];
    }

    var revisions = models[currentModel];
    if (revisions === undefined) {
      return [];
    }

    return sortByRevision(revisions)
      .map(function(e) { return e.revision; })
      .filter(function(value, index, self) { return self.indexOf(value) === index; });
  }

  function getImageTypes(modelList) {
    if (modelList.length == 1) {
      var vendor = modelList[0][MODEL_VENDOR];
      var model = modelList[0][MODEL_MODEL];
      var revision = modelList[0][MODEL_MATCHED_REVISION];
      return images[vendor][model]
        .filter(function(value, index, self) { return value['revision'] == revision; })
        .map(function(e) { return e.type; })
        .filter(function(value, index, self) { return self.indexOf(value) === index; })
        .sort();
    } else {
      return [];
    }
  }

  // format string for searching
  function searchable(q) {
    return q.toLowerCase().replace(reSearchable, '');
  }

  // make string atomic (atomic strings won't get split when searching for models
  // and have a well-defined ending)
  function atomic(q) {
    return q.replace(/ /g, NON_BREAKING_SPACE) + INVISIBLE_SEPARATOR;
  }

  // create a list with all models optimized for searching
  function createSearchableModellist() {
    var modelList = [];
    var vendors = Object.keys(images);
    for(var i in vendors) {
      var models = Object.keys(images[vendors[i]]);
      for (var j in models) {
        var revisions = getRevisions(vendors[i], models[j]);
        var searchingstring = searchable(
          vendors[i]+INVISIBLE_SEPARATOR+models[j]+INVISIBLE_SEPARATOR+
          revisions.map(atomic).join(''));

        // The 2nd searchingstring entry is used to strip already matched query
        // parts. This is needed when vendor and model are the same (e.g.
        // "Vocore Vocore") and there are more models from the same vendor.
        // The last entry is used to store the revision matched by the searchstring.
        modelList.push([searchingstring, searchingstring, vendors[i], models[j], '']);
      }
    }

    // Sort the reulting list alphabetically
    modelList = modelList.sort(function(a, b) {
      return a[0] > b[1] ? 1 : -1;
    });

    return modelList;
  }

  // search for models (and vendors)
  function searchModel(query) {
    var foundImageType = '';
    var modelList = createSearchableModellist(); // TODO: maybe generate once + deepcopy

    // search for vendors and models
    var queryparts = query.split(' ');
    for(var i in queryparts) {
      var q = searchable(queryparts[i]);
      var atomicQ = q;
      if (q.length === 0 || q[q.length-1] !== INVISIBLE_SEPARATOR) {
        atomicQ += INVISIBLE_SEPARATOR;
      }
      var filteredModelList = [];
      for (var m in modelList) {
        if (modelList[m][MODEL_STRIPPED_SEARCHSTRING].indexOf(q) != -1) {
          modelList[m][MODEL_STRIPPED_SEARCHSTRING] = modelList[m][MODEL_STRIPPED_SEARCHSTRING].replace(q, '');
          // add revision to modelList entry (if unique)
          var revisions = getRevisions(modelList[m][MODEL_VENDOR], modelList[m][MODEL_MODEL]);
          if (revisions.length == 1 && revisions[0] == 'alle') {
            modelList[m][MODEL_MATCHED_REVISION] = revisions[0];
          } else {
            var r = revisions.map(atomic).map(searchable).indexOf(atomicQ);
            if (r != -1) {
              modelList[m][MODEL_MATCHED_REVISION] = revisions[r]; // add revision to modelList entry
            }
          }
          filteredModelList.push(modelList[m]);
        } else {
          var typeKeys = Object.keys(typeNames);
          var typeValues = ObjectValues(typeNames);
          for (var k in typeKeys) {
            if (searchable(typeKeys[k]).substr(0, q.length) == q ||
                searchable(typeValues[k]).substr(0, q.length) == q) {
              foundImageType = Object.keys(typeNames)[k];
              filteredModelList.push(modelList[m]);
            }
          }
        }
      }
      modelList = filteredModelList;
    }

    var searchResult = {};
    searchResult.vendor = getVendorFromModelList(modelList);
    searchResult.model = '';
    searchResult.revision = '';
    searchResult.imageType = foundImageType;
    searchResult.imageTypes = [];
    searchResult.modelList = modelList;
    if (modelList.length == 1) {
      searchResult.model = modelList[0][MODEL_MODEL];
      searchResult.revision = modelList[0][MODEL_MATCHED_REVISION];
      searchResult.imageTypes = getImageTypes(modelList);
    }
    return searchResult;
  }

  // check if vendor is unique and return the vendor name
  function getVendorFromModelList(modelList) {
    var vendor = '';
    for (var i in modelList) {
      if (vendor === '') vendor = modelList[i][MODEL_VENDOR];
      if (vendor != modelList[i][MODEL_VENDOR]) return '';
    }
    return vendor;
  }

  function createPicturePreview(vendor, model, searchstring) {
    if (!(vendor in images)) return '';
    if (!(model in images[vendor])) return '';

    // determine revision index (use image for latest revision)
    var latestRevisionIndex = 0;
    var highestRevision = 1;
    for (var r in images[vendor][model]) {
      var rev = images[vendor][model][r].revision.substr(1);
      if (parseInt(rev) > highestRevision){
        latestRevisionIndex = r;
        highestRevision = parseInt(rev);
      }
    }

    var image = document.createElement('img');

    image.src = PREVIEW_PICTURES_DIR+images[vendor][model][latestRevisionIndex].preview;
    image.alt = name;
    image.addEventListener('error', firmwarewizard.setDefaultImg);

    var caption = document.createElement('span');
    caption.innerText = vendor+' '+model;

    var wrapper = document.createElement('div');
    wrapper.className = 'preview';
    wrapper.style.display = 'none';
    wrapper.setAttribute('data-searchstring', searchstring);
    wrapper.setAttribute('data-vendor', vendor);
    wrapper.setAttribute('data-model', model);
    wrapper.addEventListener('click', previewClickEventHandler);
    wrapper.appendChild(image);
    wrapper.appendChild(caption);
    return wrapper;
  }

  function previewClickEventHandler(e) {
    var vendor = e.currentTarget.getAttribute('data-vendor');
    var model = e.currentTarget.getAttribute('data-model');
    app.setSearchQuery(atomic(vendor)+' '+atomic(model));
    setClass(e.currentTarget, 'selected', true);
    scrollDown();
  }

  function setDefaultImg(e) {
    fallbackImg = PREVIEW_PICTURES_DIR+'no_picture_available.jpg';
    if (e.target.src.indexOf(fallbackImg) == -1) {
      e.target.src = fallbackImg;
    }
  }
  app.setDefaultImg = setDefaultImg;

  function updatePreviewList(modelList) {
    var previews = document.querySelectorAll('.imagePreview .preview');
    for(var p = 0; p < previews.length; p++) {
      previews[p].style.display = 'none';
      setClass(previews[p], 'selected', false);
    }

    for (var f in modelList) {
      var searchstring = modelList[f][MODEL_SEARCHSTRING];
      var vendor = modelList[f][MODEL_VENDOR];
      var model = modelList[f][MODEL_MODEL];

      for(p = 0; p < previews.length; p++) {
        if (previews[p].getAttribute('data-model') == model) {
          previews[p].style.display = 'inline-block';
          if (modelList.length == 1) {
            setClass(previews[p], 'selected', true);
          }
        }
      }
    }
  }

  function getVendors() {
    var vendorlist = [];
    for (var device_category_idx in enabled_device_categories) {
      var device_category = enabled_device_categories[device_category_idx];
      var category_vendors = Object.keys(config.vendormodels[device_category]);
      category_vendors.forEach(function (val, idx) {
        if (!vendorlist.includes(val)) {
          vendorlist.push(val);
        }
      });
    }
    return vendorlist;
  }

  // update all elements of the page according to the wizard object.
  function updateHTML(wizard) {
    // parse searchstring to retrieve current vendor and model
    var s = searchModel(wizard.q);

    if (wizard.showFirmwareTable) {
      show_block('#firmwareTable');
      hide('#wizard');
    } else {
      show_block('#wizard');
      hide('#firmwareTable');
    }

    // show vendor dropdown menu.
    function showVendors(currentVendor) {
      var select = $('#vendorselect');

      select.innerHTML = '';
      select.appendChild(
        createOption('', '-- Bitte Hersteller wählen --')
      );<commit_msg>app: filter vendors without images for enabled device categories<commit_after>/*
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
var firmwarewizard = function() {
  var app = {};

  // helper functions
  function $(s) {
    return document.querySelector(s);
  }

  function toggleClass(e, cssClass) {
    setClass(e, cssClass, !hasClass(e, cssClass));
  }

  function hasClass(e, cssClass) {
    var searchstring = (' ' + e.className + ' ').replace(/[\n\t]/g, " ");
    return (searchstring.indexOf(' ' + cssClass+ ' ') !== -1);
  }

  function setClass(e, cssClass, active) {
    if (active && !hasClass(e, cssClass)) {
      e.className = (e.className+' '+cssClass).trim();
    } else if (!active && hasClass(e, cssClass)) {
      e.className = e.className.replace(cssClass, '');
    }
  }

  function show_inline(s) {
    $(s).style.display = 'inline-block';
  }

  function show_block(s) {
    $(s).style.display = 'block';
  }

  function hide(s) {
    $(s).style.display = 'none';
  }

  function sortCaseInsensitive(a, b) {
	return a.localeCompare(b, 'en', {'sensitivity': 'base'});
  }

  // Object.values() replacement
  function ObjectValues(obj) {
    return Object.keys(obj).map(function(key) { return obj[key]; });
  }

  function isEmptyObject(obj) {
      for (var name in obj) {
          return false;
      }
      return true;
  }

  function scrollDown() {
    try {
      window.scrollBy({
        top: 512,
        left: 0,
        behavior: 'smooth'
      });
    } catch (e) {
      var start = document.body.scrollTop;
      var lastTop = start - 1;
      var interval = window.setInterval(function() {
        if (document.body.scrollTop > start + 512 || lastTop - document.body.scrollTop === 0) {
          window.clearInterval(interval);
          console.log('stop');
        }
        lastTop = document.body.scrollTop;
        document.body.scrollTop += 4;
      }, 1);
    }
  }

  // constants
  var IGNORED_ELEMENTS = [
    './', '../', '.manifest',
    '-tftp', '-fat', '-loader', '-NA', '-x2-', '-hsv2', '-p1020'
  ];
  var PANE = {'MODEL': 0, 'IMAGETYPE': 1, 'BRANCH': 2};

  // the invisible separator is used to delimit searchstring parts, e.g.
  // "NanoStation[SEPARATOR]" vs. "NanoStation Loco[SEPARATOR]"
  var INVISIBLE_SEPARATOR = '\u2063';
  var NON_BREAKING_SPACE = '\u00A0';
  //var INVISIBLE_SEPARATOR = '#'; // debug
  //var NON_BREAKING_SPACE = '?'; // debug

  var MODEL_SEARCHSTRING = 0;
  var MODEL_STRIPPED_SEARCHSTRING = 1;
  var MODEL_VENDOR = 2;
  var MODEL_MODEL = 3;
  var MODEL_MATCHED_REVISION = 4;

  var wizard = parseWizardObject();
  app.currentVersions = {};
  var availableImages = {};
  var images = availableImages;
  var vendormodels_reverse;

  var typeNames = {
    'factory': 'Erstinstallation',
    'sysupgrade': 'Upgrade',
    'rootfs': "Root-Image",
    'kernel': "Kernel-Image",
    'bootloader': 'Bootloader-Image'
  };

  var branches = ObjectValues(config.directories).filter(function(e, index, self) {
    return index === self.indexOf(e);
  });

  var reFileExtension = new RegExp(/\.(bin|chk|img\.gz|img|tar|ubi)$/);
  var reRemoveDashes = new RegExp(/-/g);
  var reSearchable = new RegExp('[-/ '+NON_BREAKING_SPACE+']', 'g');
  var reRemoveSpaces = new RegExp(/ /g);
  var reStripDashes = new RegExp(/^\-+|\-+$/g);

  if (config.version_regex === undefined) {
    console.log("config.version_regex missing in config.js");
    return;
  }
  var reVersionRegex = new RegExp(config.version_regex);

  var rePrettyPrintVersionRegex;
  if(config.prettyPrintVersionRegex !== undefined) {
    rePrettyPrintVersionRegex = new RegExp(config.prettyPrintVersionRegex);
  }

  var PREVIEW_PICTURES_DIR = 'pictures/';
  if(config.preview_pictures !== undefined) {
    PREVIEW_PICTURES_DIR = config.preview_pictures;
  }

  var enabled_device_categories = ['recommended'];
  if ("enabled_device_categories" in config) {
    enabled_device_categories = config.enabled_device_categories;
  }
  if ("recommended_toggle" in config && config.recommended_toggle) {
    enabled_device_categories = ['recommended'];
    show_inline('.notRecommendedLink');

    if (config.recommended_info_link) {
      $('#notrecommendedinfo').innerHTML = '<p><a href="' + config.recommended_info_link + '" target="_new">Mehr Informationen</a>';
    }
  }

  function buildVendorModelsReverse() {
    var vendormodels_reverse = {};

    for (var device_category in config.vendormodels) {
      for (var vendor in config.vendormodels[device_category]) {
        var models = config.vendormodels[device_category][vendor];
        for (var model in models) {
          var match = models[model];
          if (typeof match == 'string') {
            addArray(vendormodels_reverse, match, {'vendor': vendor, 'model': model, 'revision': '', category: device_category});
          } else for (var m in match) {
            addArray(vendormodels_reverse, m, {'vendor': vendor, 'model': model, 'revision': match[m], category: device_category});
          }
        }
      }
    }

    return vendormodels_reverse;
  }

  function createHistoryState(wizard) {
    if (!window.history || !history.pushState) return;

    var parameters = '';
    for (var key in wizard) {
      if (wizard[key] != -1 && wizard[key] !== false) {
        parameters += '&' + key + '=' + encodeURIComponent(wizard[key]);
      }
    }

    // replace first occurence of "&" by "?"
    parameters = parameters.replace('&', '?');
    history.pushState(wizard, '', parameters || '?');
  }

  function parseWizardObject(wizard) {
    if (wizard === undefined || wizard === null) wizard = {};
    wizard.q                 = wizard.q || '';
    wizard.showFirmwareTable = (wizard.showFirmwareTable == 'true');
    return wizard;
  }

  function setFilteredImages() {
    images = {};
    for (var vendor in availableImages) {
      images[vendor] = {};
      for (var model in availableImages[vendor]) {
        for (var device in availableImages[vendor][model]) {
          if (enabled_device_categories.indexOf(availableImages[vendor][model][device].category) > -1) {
            addArray(images[vendor], model, availableImages[vendor][model][device]);
          }
        }
      }
    }
  }

  window.onpopstate = function(event) {
    if (event.state === null) return;
    wizard = parseWizardObject(event.state);
    $('.modelSearch').value = wizard.q;
    updateHTML(wizard);
  };

  window.onload = function() {
    if (config.title !== undefined) {
      document.title = config.title;
    }

    $('#notrecommendedselect').checked = false;

    function parseURLasJSON() {
      var search = location.search.substring(1);
      return search ? JSON.parse(
        '{"' + search.replace(/&/g, '","').replace(/=/g,'":"') + '"}',
        function(key, value) {
          return (key=== '') ? value:decodeURIComponent(value);
        }):{};
    }
    var parsedURL = parseURLasJSON();
    wizard = parseWizardObject(parsedURL);
    $('.modelSearch').value = wizard.q;

    $('.modelSearch').addEventListener('keyup', function(e) {
      firmwarewizard.updateSearchQuery($('.modelSearch').value);
    });

    $('#vendorselect').addEventListener('change', function(e) {
      firmwarewizard.setSearchQuery($('#vendorselect').value);
      scrollDown();
    });

    $('#modelselect').addEventListener('change', function(e) {
      firmwarewizard.setSearchQuery($('#modelselect').value);
      scrollDown();
    });

    $('#revisionselect').addEventListener('change', function(e) {
      firmwarewizard.setSearchQuery($('#revisionselect').value);
      scrollDown();
    });

    $('#wizard .notRecommendedLink').addEventListener('click', function(e) {
      toggleClass($('#model-pane'), 'show-notrecommended-warning');
    });

    $('#wizard .firmwareTableLink').addEventListener('click', function(e) {
      firmwarewizard.showFirmwareTable();
    });

    $('#firmwareTable .firmwareTableLink').addEventListener('click', function(e) {
      firmwarewizard.hideFirmwareTable();
    });

    $('#notrecommendedselect').addEventListener('change', function(e) {
      if (this.checked) {
        enabled_device_categories = config.enabled_device_categories;
      } else if ("enabled_device_categories" in config) {
        enabled_device_categories = ['recommended'];
      }
      setFilteredImages();
      updateHTML(wizard);
      updateFirmwareTable();
    });

    vendormodels_reverse = buildVendorModelsReverse();

    loadDirectories(function() {
      // initialize pictures for router models
      var previews = $('.imagePreview');
      var fullModelList = createSearchableModellist();
      for (var m in fullModelList) {
        var searchstring = fullModelList[m][MODEL_SEARCHSTRING];
        var vendor = fullModelList[m][MODEL_VENDOR];
        var model = fullModelList[m][MODEL_MODEL];
        previews.appendChild(createPicturePreview(vendor, model, searchstring));
      }
      setFilteredImages();
      updateHTML(wizard);
      show_block('.manualSelection');
      updateFirmwareTable();
    });

    // set link to firmware source directory
    for(var path in config.directories) {
      $('#firmware-source-dir').href = path.replace(/\/[^\/]*$/, '');
      break;
    }
  };

  function updateSearchQuery(query) {
    wizard.q = query;
    createHistoryState(wizard);
    updateHTML(wizard);
  }
  app.updateSearchQuery = updateSearchQuery;

  function setSearchQuery(query) {
    $('.modelSearch').value = query;
    updateSearchQuery(query);
  }
  app.setSearchQuery = setSearchQuery;

  app.showFirmwareTable = function() {
    wizard.showFirmwareTable = true;
    createHistoryState(wizard);
    updateHTML(wizard);
  };

  app.hideFirmwareTable = function() {
    wizard.showFirmwareTable = false;
    createHistoryState(wizard);
    updateHTML(wizard);
  };

  // exclude file names containing a string
  function ignoreFileName(name) {
    for (var i in IGNORED_ELEMENTS) {
      if (name.indexOf(IGNORED_ELEMENTS[i]) != -1) {
        return true;
      }
    }
    return false;
  }

  // simplified version string sort
  function sortByRevision(revisions) {
    revisions.sort(function(a, b) {
        a = a.revision; b = b.revision;
        if (a.length > b.length) return 1;
        if (a.length < b.length) return -1;
        if (a > b) return 1;
        if (a < b) return -1;
        return 0;
      });
    return revisions;
  }

  function findType(name) {
    var m = /-(sysupgrade|factory|rootfs|kernel|bootloader)[-.]/.exec(name);
    return m ? m[1] : 'factory';
  }

  function findVersion(name) {
    var m = reVersionRegex.exec(name);
    return m ? m[1] : '';
  }

  // regex that is applied to the version to print a prettier version
  function prettyPrintVersion(version) {
    if (rePrettyPrintVersionRegex !== undefined) {
      var v = rePrettyPrintVersionRegex.exec(version);
      return v ? v[1] : version;
    }
    return version;
  }

  function findRevision(name) {
    // reversion identifier like a1, v2
    var m = /-([a-z][0-9]+(.[0-9]+)?)[.-]/.exec(name);
    return m ? m[1] : 'alle';
  }

  function findRegion(name) {
    var m = /-(cn|de|en|eu|il|jp|us)[.-]/.exec(name);
    return m ? m[1] : '';
  }

  function findSize(name) {
    var m = /-(4M|8M|16M|32M|64M)[.-]/.exec(name);
    return m ? m[1] : '';
  }

  function addArray(obj, key, value) {
    if (key in obj) {
      obj[key].push(value);
    } else {
      obj[key] = [value];
    }
  }

  function parseFilePath(match, basePath, filename, branch) {
    var location = basePath + encodeURIComponent(filename);

    var devices = vendormodels_reverse[match];
    if (!(devices instanceof Array) || devices.length != 1) {
      console.log("Error: vendormodels_reverse did not contain", match);
      return;
    }
    var device = devices[0];

    if (device.model == '--ignore--' || device.revision == '--ignore--') {
      return;
    }

    var strippedFilename = filename;
    strippedFilename = strippedFilename.replace(config.community_prefix, '-');

    var version = findVersion(strippedFilename);
    strippedFilename = strippedFilename.replace(version, '');

    strippedFilename = strippedFilename.replace(match, '');

    var revision = device.revision || findRevision(strippedFilename);
    strippedFilename = strippedFilename.replace(revision, '');

    var region = findRegion(strippedFilename);
    strippedFilename = strippedFilename.replace(region, '');

    if (region.length) {
      revision += '-' + region;
    }

    var size = findSize(strippedFilename);
    strippedFilename = strippedFilename.replace(size, '');

    var type = findType(strippedFilename);
    strippedFilename = strippedFilename.replace(type, '');

    strippedFilename = strippedFilename.replace(reFileExtension, '');
    strippedFilename = strippedFilename.replace(reRemoveDashes, '');

    if (strippedFilename !== '') {
      console.log("Match for file", filename, "was not exhaustive. Missing filename parts:", strippedFilename);
      return;
    }

    // derive preview file name
    var preview = filename;
    preview = preview.replace(config.community_prefix, '');
    preview = preview.replace(version, '');
    preview = preview.replace(revision, '');
    preview = preview.replace(region, '');
    preview = preview.replace(size, '');
    preview = preview.replace(type, '');
    preview = preview.replace(reFileExtension, '');
    preview = preview.replace(reStripDashes, '');

    // vendor and model specific fine tuning
    preview = preview.replace('alfa-network', 'alfa');
    preview = preview.replace('buffalo-wzr-hp-ag300h', 'buffalo-wzr-hp-ag300h-wzr-600dhp');
    preview = preview.replace('buffalo-wzr-600dhp', 'buffalo-wzr-hp-ag300h-wzr-600dhp');
    preview = preview.replace('buffalo-wzr-hp-g300nh2', 'buffalo-wzr-hp-g300nh');
    preview = preview.replace('d-link-dir-505-rev', 'd-link-dir-505-rev-a1');
    preview = preview.replace('d-link-dir-825-rev', 'd-link-dir-825-rev-b1');
    preview = preview.replace('gl-inet-6408a', 'gl-inet-6408a-v1');
    preview = preview.replace('gl-inet-6416a', 'gl-inet-6416a-v1');
    preview = preview.replace('netgear-wndrmac', 'netgear-wndrmacv2');
    preview = preview.replace('openmesh-mr600', 'openmesh-mr600-v1');
    preview = preview.replace('openmesh-mr900', 'openmesh-mr900-v1');
    if (preview.indexOf('tp-link') != -1) preview += '-' + revision;
    preview = preview.replace('tp-link-archer-c5-v1', 'tp-link-archer-c7-v2'); // Archer C5 v1 and Archer C7 v2 are identical
    preview = preview.replace('tp-link-tl-wa801n-nd-v3', 'tp-link-tl-wa801n-nd-v2'); // no preview picture for v3 yet
    preview = preview.replace('tp-link-tl-wr940n-v3', 'tp-link-tl-wr940n-v2'); // no preview picture for v3 yet
    preview = preview.replace('ubiquiti-unifi-ap', 'ubiquiti-unifi');
    preview = preview.replace('ubiquiti-unifi-lr', 'ubiquiti-unifi');
    preview = preview.replace('ubiquiti-unifi-pro', 'ubiquiti-unifi-ap-pro');
    preview = preview.replace('x86-virtualbox', 'x86-virtualbox.vdi');
    preview = preview.replace('x86-64-virtualbox', 'x86-virtualbox.vdi');
    preview = preview.replace('x86-vmware', 'x86-vmware.vmdk');
    preview = preview.replace('x86-64-vmware', 'x86-vmware.vmdk');
    preview = preview.replace('x86-generic', 'x86-generic.img');
    preview = preview.replace('x86-64', 'x86-generic.img');
    preview = preview.replace('x86-kvm', 'x86-kvm.img');
    preview = preview.replace('x86-generic.img.vdi', 'x86-virtualbox.vdi');
    preview = preview.replace('x86-generic.img.vmdk', 'x86-vmware.vmdk');

    // collect branch versions
    app.currentVersions[branch] = version;

    if (!(device.vendor in availableImages)) {
      availableImages[device.vendor] = {};
    }

    addArray(availableImages[device.vendor], device.model, {
      'revision': revision,
      'branch': branch,
      'type': type,
      'version': version,
      'location': location,
      'size': size,
      'preview': preview+".jpg",
      'category': device.category
    });
  }

  function createOption(value, title, selectedOption) {
    var o = document.createElement('option');
    o.value = value;
    o.innerText = title;
    o.selected = (value === selectedOption);
    return o;
  }

  function getRevisions(currentVendor, currentModel) {
    var models = images[currentVendor];
    if (models === undefined) {
      return [];
    }

    var revisions = models[currentModel];
    if (revisions === undefined) {
      return [];
    }

    return sortByRevision(revisions)
      .map(function(e) { return e.revision; })
      .filter(function(value, index, self) { return self.indexOf(value) === index; });
  }

  function getImageTypes(modelList) {
    if (modelList.length == 1) {
      var vendor = modelList[0][MODEL_VENDOR];
      var model = modelList[0][MODEL_MODEL];
      var revision = modelList[0][MODEL_MATCHED_REVISION];
      return images[vendor][model]
        .filter(function(value, index, self) { return value['revision'] == revision; })
        .map(function(e) { return e.type; })
        .filter(function(value, index, self) { return self.indexOf(value) === index; })
        .sort();
    } else {
      return [];
    }
  }

  // format string for searching
  function searchable(q) {
    return q.toLowerCase().replace(reSearchable, '');
  }

  // make string atomic (atomic strings won't get split when searching for models
  // and have a well-defined ending)
  function atomic(q) {
    return q.replace(/ /g, NON_BREAKING_SPACE) + INVISIBLE_SEPARATOR;
  }

  // create a list with all models optimized for searching
  function createSearchableModellist() {
    var modelList = [];
    var vendors = Object.keys(images);
    for(var i in vendors) {
      var models = Object.keys(images[vendors[i]]);
      for (var j in models) {
        var revisions = getRevisions(vendors[i], models[j]);
        var searchingstring = searchable(
          vendors[i]+INVISIBLE_SEPARATOR+models[j]+INVISIBLE_SEPARATOR+
          revisions.map(atomic).join(''));

        // The 2nd searchingstring entry is used to strip already matched query
        // parts. This is needed when vendor and model are the same (e.g.
        // "Vocore Vocore") and there are more models from the same vendor.
        // The last entry is used to store the revision matched by the searchstring.
        modelList.push([searchingstring, searchingstring, vendors[i], models[j], '']);
      }
    }

    // Sort the reulting list alphabetically
    modelList = modelList.sort(function(a, b) {
      return a[0] > b[1] ? 1 : -1;
    });

    return modelList;
  }

  // search for models (and vendors)
  function searchModel(query) {
    var foundImageType = '';
    var modelList = createSearchableModellist(); // TODO: maybe generate once + deepcopy

    // search for vendors and models
    var queryparts = query.split(' ');
    for(var i in queryparts) {
      var q = searchable(queryparts[i]);
      var atomicQ = q;
      if (q.length === 0 || q[q.length-1] !== INVISIBLE_SEPARATOR) {
        atomicQ += INVISIBLE_SEPARATOR;
      }
      var filteredModelList = [];
      for (var m in modelList) {
        if (modelList[m][MODEL_STRIPPED_SEARCHSTRING].indexOf(q) != -1) {
          modelList[m][MODEL_STRIPPED_SEARCHSTRING] = modelList[m][MODEL_STRIPPED_SEARCHSTRING].replace(q, '');
          // add revision to modelList entry (if unique)
          var revisions = getRevisions(modelList[m][MODEL_VENDOR], modelList[m][MODEL_MODEL]);
          if (revisions.length == 1 && revisions[0] == 'alle') {
            modelList[m][MODEL_MATCHED_REVISION] = revisions[0];
          } else {
            var r = revisions.map(atomic).map(searchable).indexOf(atomicQ);
            if (r != -1) {
              modelList[m][MODEL_MATCHED_REVISION] = revisions[r]; // add revision to modelList entry
            }
          }
          filteredModelList.push(modelList[m]);
        } else {
          var typeKeys = Object.keys(typeNames);
          var typeValues = ObjectValues(typeNames);
          for (var k in typeKeys) {
            if (searchable(typeKeys[k]).substr(0, q.length) == q ||
                searchable(typeValues[k]).substr(0, q.length) == q) {
              foundImageType = Object.keys(typeNames)[k];
              filteredModelList.push(modelList[m]);
            }
          }
        }
      }
      modelList = filteredModelList;
    }

    var searchResult = {};
    searchResult.vendor = getVendorFromModelList(modelList);
    searchResult.model = '';
    searchResult.revision = '';
    searchResult.imageType = foundImageType;
    searchResult.imageTypes = [];
    searchResult.modelList = modelList;
    if (modelList.length == 1) {
      searchResult.model = modelList[0][MODEL_MODEL];
      searchResult.revision = modelList[0][MODEL_MATCHED_REVISION];
      searchResult.imageTypes = getImageTypes(modelList);
    }
    return searchResult;
  }

  // check if vendor is unique and return the vendor name
  function getVendorFromModelList(modelList) {
    var vendor = '';
    for (var i in modelList) {
      if (vendor === '') vendor = modelList[i][MODEL_VENDOR];
      if (vendor != modelList[i][MODEL_VENDOR]) return '';
    }
    return vendor;
  }

  function createPicturePreview(vendor, model, searchstring) {
    if (!(vendor in images)) return '';
    if (!(model in images[vendor])) return '';

    // determine revision index (use image for latest revision)
    var latestRevisionIndex = 0;
    var highestRevision = 1;
    for (var r in images[vendor][model]) {
      var rev = images[vendor][model][r].revision.substr(1);
      if (parseInt(rev) > highestRevision){
        latestRevisionIndex = r;
        highestRevision = parseInt(rev);
      }
    }

    var image = document.createElement('img');

    image.src = PREVIEW_PICTURES_DIR+images[vendor][model][latestRevisionIndex].preview;
    image.alt = name;
    image.addEventListener('error', firmwarewizard.setDefaultImg);

    var caption = document.createElement('span');
    caption.innerText = vendor+' '+model;

    var wrapper = document.createElement('div');
    wrapper.className = 'preview';
    wrapper.style.display = 'none';
    wrapper.setAttribute('data-searchstring', searchstring);
    wrapper.setAttribute('data-vendor', vendor);
    wrapper.setAttribute('data-model', model);
    wrapper.addEventListener('click', previewClickEventHandler);
    wrapper.appendChild(image);
    wrapper.appendChild(caption);
    return wrapper;
  }

  function previewClickEventHandler(e) {
    var vendor = e.currentTarget.getAttribute('data-vendor');
    var model = e.currentTarget.getAttribute('data-model');
    app.setSearchQuery(atomic(vendor)+' '+atomic(model));
    setClass(e.currentTarget, 'selected', true);
    scrollDown();
  }

  function setDefaultImg(e) {
    fallbackImg = PREVIEW_PICTURES_DIR+'no_picture_available.jpg';
    if (e.target.src.indexOf(fallbackImg) == -1) {
      e.target.src = fallbackImg;
    }
  }
  app.setDefaultImg = setDefaultImg;

  function updatePreviewList(modelList) {
    var previews = document.querySelectorAll('.imagePreview .preview');
    for(var p = 0; p < previews.length; p++) {
      previews[p].style.display = 'none';
      setClass(previews[p], 'selected', false);
    }

    for (var f in modelList) {
      var searchstring = modelList[f][MODEL_SEARCHSTRING];
      var vendor = modelList[f][MODEL_VENDOR];
      var model = modelList[f][MODEL_MODEL];

      for(p = 0; p < previews.length; p++) {
        if (previews[p].getAttribute('data-model') == model) {
          previews[p].style.display = 'inline-block';
          if (modelList.length == 1) {
            setClass(previews[p], 'selected', true);
          }
        }
      }
    }
  }

  function hasVendorDevicesForEnabledDeviceCategories(vendor) {
    var image_vendors = Object.keys(images);
    for (let [key, value] of Object.entries(images[vendor])) {
      if (enabled_device_categories.includes(value[0].category)) {
        return true;
      }
    }
    return false;
  }

  function getVendors() {
    var vendorlist = [];
    for (var device_category_idx in enabled_device_categories) {
      var device_category = enabled_device_categories[device_category_idx];
      var category_vendors = Object.keys(config.vendormodels[device_category]);
      category_vendors.forEach(function (val, idx) {
        if (!vendorlist.includes(val) && hasVendorDevicesForEnabledDeviceCategories(val)) {
          vendorlist.push(val);
        }
      });
    }
    return vendorlist;
  }

  // update all elements of the page according to the wizard object.
  function updateHTML(wizard) {
    // parse searchstring to retrieve current vendor and model
    var s = searchModel(wizard.q);

    if (wizard.showFirmwareTable) {
      show_block('#firmwareTable');
      hide('#wizard');
    } else {
      show_block('#wizard');
      hide('#firmwareTable');
    }

    // show vendor dropdown menu.
    function showVendors(currentVendor) {
      var select = $('#vendorselect');

      select.innerHTML = '';
      select.appendChild(
        createOption('', '-- Bitte Hersteller wählen --')
      );
{'fraction_non_alphanumeric': 0.14533872638099146, 'fraction_numerical': 0.00916427301326768, 'mean_word_length': 3.112664738026358, 'pattern_counts': {'":': 2, '<': 19, '<?xml version=': 0, '>': 23, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, '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': '1701729', 'n_tokens_mistral': 16393, 'n_tokens_neox': 15753, 'n_words': 3348}