function parse (input, parserData, callback, options = {}) {
  var arrayFilterCategories = ['regex', 'bothAnchored']
  var objectFilterCategories = ['hostAnchored']
  var trieFilterCategories = ['nonAnchoredString', 'leftAnchored', 'rightAnchored']

  parserData.exceptionFilters = parserData.exceptionFilters || {}

  for (var i = 0; i < arrayFilterCategories.length; i++) {
    parserData[arrayFilterCategories[i]] = parserData[arrayFilterCategories[i]] || []
    parserData.exceptionFilters[arrayFilterCategories[i]] = parserData.exceptionFilters[arrayFilterCategories[i]] || []
  }

  for (var i = 0; i < objectFilterCategories.length; i++) {
    parserData[objectFilterCategories[i]] = parserData[objectFilterCategories[i]] || {}
    parserData.exceptionFilters[objectFilterCategories[i]] = parserData.exceptionFilters[objectFilterCategories[i]] || {}
  }

  for (var i = 0; i < trieFilterCategories.length; i++) {
    parserData[trieFilterCategories[i]] = parserData[trieFilterCategories[i]] || new Trie()
    parserData.exceptionFilters[trieFilterCategories[i]] = parserData.exceptionFilters[trieFilterCategories[i]] || new Trie()
  }

  var filters = input.split('\n')

  function processChunk (start, end) {
    for (var i = start, len = end; i < len; i++) {
      var filter = filters[i]
      if (!filter) {
        continue
      }

      var parsedFilterData = {}

      var object

      if (parseFilter(filter, parsedFilterData)) {
        if (parsedFilterData.isException) {
          object = parserData.exceptionFilters
        } else {
          object = parserData
        }

        // add the filters to the appropriate category
        if (parsedFilterData.leftAnchored) {
          if (parsedFilterData.rightAnchored) {
            object.bothAnchored.push(parsedFilterData)
          } else {
            object.leftAnchored.add(parsedFilterData.data, parsedFilterData.options, maybeMergeDuplicateOptions)
          }
        } else if (parsedFilterData.rightAnchored) {
          object.rightAnchored.addReverse(parsedFilterData.data, parsedFilterData.options)
        } else if (parsedFilterData.hostAnchored) {
          /* add the filters to the object based on the last 6 characters of their domain.
            Domains can be just 5 characters long: the TLD is at least 2 characters,
            the . character adds one more character, and the domain name must be at least two
            characters long. However, slicing the last 6 characters of a 5-character string
            will give us the 5 available characters; we can then check for both a
            5-character and a 6-character match in matchesFilters. By storing the last
            characters in an object, we can skip checking whether every filter's domain
            is from the same origin as the URL we are checking. Instead, we can just get
            the last characters of the URL to check, get the filters stored in that
            property of the object, and then check if the complete domains match.
           */
          var ending = parsedFilterData.host.slice(-6)

          if (object.hostAnchored[ending]) {
            object.hostAnchored[ending].push(parsedFilterData)
          } else {
            object.hostAnchored[ending] = [parsedFilterData]
          }
        } else if (parsedFilterData.regex) {
          object.regex.push(parsedFilterData)
        } else if (parsedFilterData.wildcardMatchParts) {
          var wildcardToken = parsedFilterData.wildcardMatchParts[0].split('^')[0].substring(0, 10)
          if (wildcardToken.length < 4) {
            var wildcardToken2 = parsedFilterData.wildcardMatchParts[1].split('^')[0].substring(0, 10)
            if (wildcardToken2 && wildcardToken2.length > wildcardToken.length) {
              wildcardToken = wildcardToken2
            }
          }
          if (wildcardToken) {
            object.nonAnchoredString.add(wildcardToken, parsedFilterData)
          } else {
            object.nonAnchoredString.add('', parsedFilterData)
          }
        } else {
          object.nonAnchoredString.add(parsedFilterData.data.split('^')[0].substring(0, 10), parsedFilterData)
        }
      }
    }
  }

  if (options.async === false) {
    processChunk(0, filters.length)
    parserData.initialized = true
  } else {
    /* parse filters in chunks to prevent the main process from freezing */

    var filtersLength = filters.length
    var lastFilterIdx = 0
    var nextChunkSize = 1500
    var targetMsPerChunk = 12

    function nextChunk () {
      var t1 = Date.now()
      processChunk(lastFilterIdx, lastFilterIdx + nextChunkSize)
      var t2 = Date.now()

      lastFilterIdx += nextChunkSize

      if (t2 - t1 !== 0) {
        nextChunkSize = Math.round(nextChunkSize / ((t2 - t1) / targetMsPerChunk))
      }

      if (lastFilterIdx < filtersLength) {
        setTimeout(nextChunk, 16)
      } else {
        parserData.initialized = true

        if (callback) {
          callback()
        }
      }
    }

    nextChunk()
  }
}
Parses the set of filter rules and fills in parserData
@param input filter rules
@param parserData out parameter which will be filled
  with the filters, exceptionFilters and htmlRuleFilters.