<commit_before>var http = require('http')
  , https = require('https')
  , jsonpath = require('JSONPath').eval
  , shared = require('./shared')
  , urlLib = require('url')

var testType = function(parentOpts, type) {
  return function(opts, callback) {
    if (typeof(opts) !== 'object') opts = { url: opts }
    opts.url = (parentOpts.url || '') + opts.url

    return function(test) {
      test = shared.setupTest(test)
      test._ntf.parentOpts = parentOpts
      if (typeof(test._ntf.meta) !== 'object') test._ntf.meta = this.meta || {}
      TEST[type].call(this, test, opts, callback)
    }
  }
}

var TEST = module.exports = function(opts) {
  if (typeof(opts) === 'string') opts = { url: opts }
  return {
    del: testType(opts, 'del'),
    get: testType(opts, 'get'),
    post: testType(opts, 'post'),
    put: testType(opts, 'put')
  }
}

var asserts = TEST.asserts = {
  statusCode: function(statusCode) {
    var res = this.ntf.res
    this.equal(typeof(res) === 'object' ? res.statusCode : undefined,
      statusCode, 'Status code is ' + statusCode)
  },
  header: function(name, match) {
    var res = this.ntf.res
      , values
      , result = typeof(res) === 'object' && typeof(res.headers) === 'object'
    if (name === undefined)
      return result ? res.headers : undefined
    result = result && typeof(name) === 'string'
    if (result) {
      name = name.toLowerCase()
      result = result && res.headers.hasOwnProperty(name)
    }
    if (match === undefined) {
      this.ok(result, 'Header[' + name + '] exists')
      return result ? res.headers[name] : undefined
    }
    if (result) {
      values = res.headers[name]
      if (!Array.isArray(values)) values = [values]
      for (var i in values) {
        v = values[i]
        result = (match instanceof RegExp) ? v.match(match) : v == match
        if (result) break
      }
    }
    this.ok(result, 'Header[' + name + '] matches "' + match + '"')
    if (Array.isArray(result)) return result
  },
  body: function(match) {
    if (match === undefined) return this.ntf.data
    var data = this.ntf.data
      , result = typeof(data) === 'string' &&
          (match instanceof RegExp ? data.match(match) : data.indexOf(match) >= 0)
    this.ok(result, 'Content matches "' + match + '"')
    if (Array.isArray(result)) return result
  },
  json: function(match) {
    var result
      , data = this.ntf.data
    try {
      result = JSON.parse(data)
    } catch (err) {}
    this.ok(result !== undefined, 'Content is JSON')
    if (match !== undefined) this.deepEqual(result, match)
    return result
  },
  jsonPath: function(path) {
    var result = jsonpath(this.json(), path)
    if (!Array.isArray(result)) result = []
    for (var i = 1; i < arguments.length; i++) {
      this.deepEqual(result[i-1], arguments[i],
        'JSONPath[' + (i-1) + ']' +
        (result[i-1] === undefined ? '' : result[i-1]) +
        ' !== ' + JSON.stringify(arguments[i]))
    }
    return result
  }
}

var subTestType = function(test, type) {
  return function() {
    var args = Array.prototype.slice.call(arguments)
    if (typeof(args[0]) !== 'object') args[0] = { url: args[0] }
    args[0].url = (test._ntf.parentOpts.url || '') + args[0].url
    args.unshift(test)
    TEST[type].apply(this, args)
  }
}

TEST.request = function(test, opts, callback) {
  // ensure valid timeout
  if (typeof(opts.timeout) !== 'number') opts.timeout = 30 * 1000

  var reqTimeout
    , url = urlLib.parse(opts.url)

  if (typeof(test._ntf.meta) === 'object' && test._ntf.meta.currentTest) {
    if (typeof(test._ntf.meta.test) !== 'object') { test._ntf.meta.test = {} }
    test._ntf.meta.test[test._ntf.meta.currentTest] = {
      type: 'http',
      url: opts.url,
      http_method: opts.method
    }
  }

  for (var i in asserts) {
    if (!test.hasOwnProperty(i)) test[i] = asserts[i]
  }

  // add subtests
  if (!test.del) test.del = subTestType(test, 'del')
  if (!test.get) test.get = subTestType(test, 'get')
  if (!test.post) test.post = subTestType(test, 'post')
  if (!test.put) test.put = subTestType(test, 'put')

  // normalize header keys to lowercase
  var headers = {}
  if (typeof(opts.headers) === 'object') {
    Object.keys(opts.headers).forEach(function(key) {
      headers[key.toLowerCase()] = opts.headers[key]
    })
  }

  // build http(s).request parameters using url and options
  var httpOpts = test.ntf.httpOpts = {
    host: url.hostname,
    port: url.port,
    headers: headers,
    path: (url.pathname || '') + (url.search || ''),
    method: opts.method.toUpperCase()
  }
  if (!httpOpts.port) {
    if (url.protocol === 'http:') {
      httpOpts.port = 80
    } else if (url.protocol === 'https:') {
      httpOpts.port = 443
    }
  }
  if ((opts.auth || url.auth) && !httpOpts.headers.authorization) {
    httpOpts.headers.authorization = 'Basic ' +
      new Buffer(opts.auth || url.auth).toString('base64')
  }
  if (!opts.handle && !httpOpts.headers.hasOwnProperty('content-length')) {
    httpOpts.headers['content-length'] = opts.data ?
      Buffer.byteLength(opts.data, opts.reqEncoding || 'utf8') : 0
  }

  var req = test.ntf.req = url.protocol === 'https:' ?
            https.request(httpOpts) : http.request(httpOpts)

  test.ntf.reqDone = false
  test.ntf.reqClosed = false

  test._ntf.teardown.push(function() {
    if (!test.ntf.reqClosed) {
      test.ntf.reqClosed = true
      req.end()
    }
  })

  // fail test on http errors
  req.on('error', function(err) {
    if (!test.ntf.reqDone) {
      test.ntf.reqDone = true
      test.ok(false, err)
      test.done()
    }
  })

  // add timeout if not explicitly disabled, if timeout is raised the test
  // will stop and fail immediately
  if (opts.timeout > 0) {
    var start = new Date().getTime()

    reqTimeout = setTimeout(function() {
      req.emit('reqTimeout')
    }, opts.timeout)

    req.on('reqTimeout', function() {
      if (!test.ntf.reqDone) {
        test.ntf.reqDone = true
        test.fail(new Date().getTime() - start, opts.timeout, '', '>=',
          'Request timed out')
        test.done()
      }
    })
  }

  req.on('response', function(res) {
    test.ntf.res = res

    if (!opts.handle || opts.handleData) {
      test.ntf.data = ''
      res.setEncoding(opts.resEncoding || 'utf8')
      res.on('data', function(chunk) {
        test.ntf.data += chunk
      })
    }

    res.on('end', function() {
      if (reqTimeout) clearTimeout(reqTimeout)
      test.ntf.reqClosed = true
      if (!opts.handle && !test.ntf.reqDone) {
        test.ntf.reqDone = true
        callback(test, req, res)
      }
    })

    if (opts.handle) callback(test, req, res)
  })

  if (!opts.handle) req.end(opts.data)
}

TEST.del = function(test, opts, callback) {
  opts.method = 'delete'
  return TEST.request(test, opts, callback)
}

TEST.get = function(test, opts, callback) {
  opts.method = 'get'
  return TEST.request(test, opts, callback)
}

TEST.post = function(test, opts, callback) {
  opts.data = opts.data || ''
  opts.method = 'post'
  return TEST.request(test, opts, callback)
}

TEST.put = function(test, opts, callback) {
  opts.data = opts.data || ''
  opts.method = 'put'
  return TEST.request(test, opts, callback)
}
<commit_msg>Add type to http test<commit_after>var http = require('http')
  , https = require('https')
  , jsonpath = require('JSONPath').eval
  , shared = require('./shared')
  , qs = require('querystring')
  , urlLib = require('url')

var testType = function(parentOpts, type) {
  return function(opts, callback) {
    if (typeof(opts) !== 'object') opts = { url: opts }
    opts.url = (parentOpts.url || '') + opts.url

    return function(test) {
      test = shared.setupTest(test)
      test._ntf.parentOpts = parentOpts
      if (typeof(test._ntf.meta) !== 'object') test._ntf.meta = this.meta || {}
      TEST[type].call(this, test, opts, callback)
    }
  }
}

var TEST = module.exports = function(opts) {
  if (typeof(opts) === 'string') opts = { url: opts }
  return {
    del: testType(opts, 'del'),
    get: testType(opts, 'get'),
    post: testType(opts, 'post'),
    put: testType(opts, 'put')
  }
}

var asserts = TEST.asserts = {
  statusCode: function(statusCode) {
    var res = this.ntf.res
    this.equal(typeof(res) === 'object' ? res.statusCode : undefined,
      statusCode, 'Status code is ' + statusCode)
  },
  header: function(name, match) {
    var res = this.ntf.res
      , values
      , result = typeof(res) === 'object' && typeof(res.headers) === 'object'
    if (name === undefined)
      return result ? res.headers : undefined
    result = result && typeof(name) === 'string'
    if (result) {
      name = name.toLowerCase()
      result = result && res.headers.hasOwnProperty(name)
    }
    if (match === undefined) {
      this.ok(result, 'Header[' + name + '] exists')
      return result ? res.headers[name] : undefined
    }
    if (result) {
      values = res.headers[name]
      if (!Array.isArray(values)) values = [values]
      for (var i in values) {
        v = values[i]
        result = (match instanceof RegExp) ? v.match(match) : v == match
        if (result) break
      }
    }
    this.ok(result, 'Header[' + name + '] matches "' + match + '"')
    if (Array.isArray(result)) return result
  },
  body: function(match) {
    if (match === undefined) return this.ntf.data
    var data = this.ntf.data
      , result = typeof(data) === 'string' &&
          (match instanceof RegExp ? data.match(match) : data.indexOf(match) >= 0)
    this.ok(result, 'Content matches "' + match + '"')
    if (Array.isArray(result)) return result
  },
  json: function(match) {
    var result
      , data = this.ntf.data
    try {
      result = JSON.parse(data)
    } catch (err) {}
    this.ok(result !== undefined, 'Content is JSON')
    if (match !== undefined) this.deepEqual(result, match)
    return result
  },
  jsonPath: function(path) {
    var result = jsonpath(this.json(), path)
    if (!Array.isArray(result)) result = []
    for (var i = 1; i < arguments.length; i++) {
      this.deepEqual(result[i-1], arguments[i],
        'JSONPath[' + (i-1) + ']' +
        (result[i-1] === undefined ? '' : result[i-1]) +
        ' !== ' + JSON.stringify(arguments[i]))
    }
    return result
  }
}

var subTestType = function(test, type) {
  return function() {
    var args = Array.prototype.slice.call(arguments)
    if (typeof(args[0]) !== 'object') args[0] = { url: args[0] }
    args[0].url = (test._ntf.parentOpts.url || '') + args[0].url
    args.unshift(test)
    TEST[type].apply(this, args)
  }
}

TEST.request = function(test, opts, callback) {
  // ensure valid timeout
  if (typeof(opts.timeout) !== 'number') opts.timeout = 30 * 1000

  var reqTimeout
    , url = urlLib.parse(opts.url)

  if (typeof(test._ntf.meta) === 'object' && test._ntf.meta.currentTest) {
    if (typeof(test._ntf.meta.test) !== 'object') { test._ntf.meta.test = {} }
    test._ntf.meta.test[test._ntf.meta.currentTest] = {
      type: 'http',
      url: opts.url,
      http_method: opts.method
    }
  }

  for (var i in asserts) {
    if (!test.hasOwnProperty(i)) test[i] = asserts[i]
  }

  // add subtests
  if (!test.del) test.del = subTestType(test, 'del')
  if (!test.get) test.get = subTestType(test, 'get')
  if (!test.post) test.post = subTestType(test, 'post')
  if (!test.put) test.put = subTestType(test, 'put')

  // normalize header keys to lowercase
  var headers = {}
  if (typeof(opts.headers) === 'object') {
    Object.keys(opts.headers).forEach(function(key) {
      headers[key.toLowerCase()] = opts.headers[key]
    })
  }

  // build http(s).request parameters using url and options
  var httpOpts = test.ntf.httpOpts = {
    host: url.hostname,
    port: url.port,
    headers: headers,
    path: (url.pathname || '') + (url.search || ''),
    method: opts.method.toUpperCase()
  }
  if (!httpOpts.port) {
    if (url.protocol === 'http:') {
      httpOpts.port = 80
    } else if (url.protocol === 'https:') {
      httpOpts.port = 443
    }
  }
  if ((opts.auth || url.auth) && !httpOpts.headers.authorization) {
    httpOpts.headers.authorization = 'Basic ' +
      new Buffer(opts.auth || url.auth).toString('base64')
  }
  if (!opts.handle) {
    var setContentType = function(type) {
      if (!httpOpts.headers.hasOwnProperty('content-type')) {
        httpOpts.headers['content-type'] = type
      }
    }
    switch (opts.type) {
      case 'form':
        opts.data = qs.stringify(opts.data).toString('utf8')
        setContentType('application/x-www-form-urlencoded')
        break
      case 'json':
        opts.data = JSON.stringify(opts.data)
        setContentType('application/json')
        break
    }
    if (!httpOpts.headers.hasOwnProperty('content-length')) {
      httpOpts.headers['content-length'] = opts.data ?
        Buffer.byteLength(opts.data, opts.reqEncoding || 'utf8') : 0
    }
  }

  var req = test.ntf.req = url.protocol === 'https:' ?
            https.request(httpOpts) : http.request(httpOpts)

  test.ntf.reqDone = false
  test.ntf.reqClosed = false

  test._ntf.teardown.push(function() {
    if (!test.ntf.reqClosed) {
      test.ntf.reqClosed = true
      req.end()
    }
  })

  // fail test on http errors
  req.on('error', function(err) {
    if (!test.ntf.reqDone) {
      test.ntf.reqDone = true
      test.ok(false, err)
      test.done()
    }
  })

  // add timeout if not explicitly disabled, if timeout is raised the test
  // will stop and fail immediately
  if (opts.timeout > 0) {
    var start = new Date().getTime()

    reqTimeout = setTimeout(function() {
      req.emit('reqTimeout')
    }, opts.timeout)

    req.on('reqTimeout', function() {
      if (!test.ntf.reqDone) {
        test.ntf.reqDone = true
        test.fail(new Date().getTime() - start, opts.timeout, '', '>=',
          'Request timed out')
        test.done()
      }
    })
  }

  req.on('response', function(res) {
    test.ntf.res = res

    if (!opts.handle || opts.handleData) {
      test.ntf.data = ''
      res.setEncoding(opts.resEncoding || 'utf8')
      res.on('data', function(chunk) {
        test.ntf.data += chunk
      })
    }

    res.on('end', function() {
      if (reqTimeout) clearTimeout(reqTimeout)
      test.ntf.reqClosed = true
      if (!opts.handle && !test.ntf.reqDone) {
        test.ntf.reqDone = true
        callback(test, req, res)
      }
    })

    if (opts.handle) callback(test, req, res)
  })

  if (!opts.handle) req.end(opts.data)
}

TEST.del = function(test, opts, callback) {
  opts.method = 'delete'
  return TEST.request(test, opts, callback)
}

TEST.get = function(test, opts, callback) {
  opts.method = 'get'
  return TEST.request(test, opts, callback)
}

TEST.post = function(test, opts, callback) {
  opts.data = opts.data || ''
  opts.method = 'post'
  return TEST.request(test, opts, callback)
}

TEST.put = function(test, opts, callback) {
  opts.data = opts.data || ''
  opts.method = 'put'
  return TEST.request(test, opts, callback)
}
<|endoftext|>


<commit_before>const TEST_ENV = 'TEST';
const PROD_ENV = 'PROD';

class GoogleCalendarMoveToDate {
    // Maximum number of events to keep in a cell.
    #DEFAULT_MAX_EVENTS_PER_CELL = 14;

    // Oldest available cell date that has been found and should be used.
    topCellDate;

    constructor(options) {
        this.options = options;
        this.options.maxEventsPerCell = this.options.maxEventsPerCell || this.#DEFAULT_MAX_EVENTS_PER_CELL;
        this.env = options.env;
        this.debug = options.debug ? true : false;
    }

    findCellsInRow(row) {
        this.debug && console.info('findCellsInRow');
        this.debug && console.log('row:', row);
        var rowCells;
        if (this.env === PROD_ENV) {
            rowCells = row.querySelectorAll('[role="gridcell"]');
        } else if (this.env === TEST_ENV) {
            rowCells = row;
        }
        this.debug && console.log('rowCells:', rowCells);
        return rowCells;
    }

    clearTopCellDate() {
        this.debug && console.info('clearTopCellDate');
        this.topCellDate = undefined;
    }

    countEventsInCell(cell) {
        this.debug && console.info('countEventsInCell');
        this.debug && console.log('cell:', cell);
        var cellEventsFound;
        if (this.env === PROD_ENV) {
            var cellEvents = cell.querySelectorAll('[data-eventid]');
            cellEventsFound = cellEvents.length;
            var moreCellEvents = cell.querySelector('[data-opens-day-overview]');
            if (moreCellEvents) {
                cellEventsFound += parseInt(moreCellEvents.innerText.match(/(\d+) more/)[1]);
            }
        } else if (this.env === TEST_ENV) {
            var cellEvents = cell;
            cellEventsFound = cellEvents.length;
        }
        this.debug && console.log('cellEventsFound:', cellEventsFound);
        return cellEventsFound;
    }

    findTopCell(cells) {
        // Find first top-right most cell in current view containing a non-full number of events.
        this.debug && console.group('findTopCell');
        this.debug && console.log('cells:', cells);
        var topCell;

        // Check top to bottom.
        outer_loop:
        for (var rowIndex = 0; rowIndex < cells.length; rowIndex++) {
            this.debug && console.group('rowIndex:', rowIndex);

            var row = cells[rowIndex];
            this.debug && console.log('row:', row);

            var rowCells = this.findCellsInRow(row);
            // Check right to left.
            for (var cellIndex = rowCells.length - 1; cellIndex >= 0; cellIndex--) {
                this.debug && console.log('cellIndex:', cellIndex);
                var cell = rowCells[cellIndex];
                this.debug && console.log('cell:', cell);

                var cellEventsFound = this.countEventsInCell(cell);

                if (this.env === PROD_ENV) {
                    cell = cell;
                } else if (this.env === TEST_ENV) {
                    cell = [rowIndex, cellIndex];
                }

                // Determine if current top cell should be cleared now that it has the maximum number of events per
                // cell.
                if (this.topCellDate !== undefined &&
                    cellEventsFound === this.options.maxEventsPerCell) {
                    var cellDate = this.getCellDate(cell);
                    if (cellDate === this.topCellDate) {
                        this.topCellDate = undefined;
                    }
                }

                if (cellEventsFound < this.options.maxEventsPerCell) {
                    topCell = cell;
                    this.debug && console.groupEnd();
                    break outer_loop;
                } else {
                    this.debug && console.log('ELSE');
                }
            }

            this.debug && console.groupEnd();
        }

        this.debug && console.log('topCell:', topCell);
        this.debug && console.groupEnd();
        return topCell;
    }

    getCalendarYear() {
        this.debug && console.info('getCalendarYear');

        // Fetch calendar year from url (e.g. 2019 in "https://calendar.google.com/calendar/r/customweek/2019/12/9" and
        // not 2020 using "(new Date()).getFullYear()").
        var calendarYear = window.location.toString().match(/calendar\/r\/[a-z]+\/(\d\d\d\d)\//)[1];

        this.debug && console.log('calendarYear:', calendarYear);
        return calendarYear;
    }

    getCellDate(cell) {
        this.debug && console.info('getCellDate');
        this.debug && console.log('cell:', cell);
        var cellDate;
        if (this.env === PROD_ENV) {
            // "7 events, Thursday, October 17".
            var cellDescription = cell.querySelector('h2').innerText;

            // "October 17".
            var monthDate = cellDescription.match(/ events?, .*day, (.*?)$/)[1];
            this.debug && console.log('monthDate:', monthDate);

            // "Oct 17".
            monthDate = monthDate.substr(0, 3) + ' ' + monthDate.split(' ')[1];
            this.debug && console.log('monthDate:', monthDate);

            // "Oct 17, 2019".
            var monthDateYear = monthDate + ', ' + this.getCalendarYear();
            this.debug && console.log('monthDateYear:', monthDateYear);

            cellDate = monthDateYear;
        } else if (this.env === TEST_ENV) {
            var rowIndex = cell[0];
            var cellIndex = cell[1];
            cellDate = this.options.cellDates[rowIndex][cellIndex];
        }
        this.debug && console.log('cellDate:', cellDate);
        return cellDate;
    }

    getTopCellDate(calendarGridRows) {
        this.debug && console.info('getTopCellDate');

        // Calculate if cell date is older than the current top cell date only when a top cell is found.
        var topCell = this.findTopCell(calendarGridRows);
        if (topCell !== undefined) {
            var cellDate = this.getCellDate(topCell);
            if (this.topCellDate === undefined) {
                this.debug && console.log('using newly calculated cellDate:', cellDate);
                this.topCellDate = cellDate;
            } else {
                this.debug && console.log('comparing cellDate %s to current topCellDate %s', cellDate, this.topCellDate);
                if (this._getDateComparisonObject(cellDate) < this._getDateComparisonObject(this.topCellDate)) {
                    this.debug && console.log('updating current topCellDate');
                    this.topCellDate = cellDate;
                } else {
                    this.debug && console.log('keeping current topCellDate');
                }
            }
        }

        this.debug && console.log('topCellDate:', this.topCellDate);
        return this.topCellDate;
    }

    setOption(option, value) {
        this.options[option] = value;
    }

    _getDateComparisonObject(date) {
        return new Date(date).getTime();
    }
}

if (typeof(window) === 'undefined') {
    module.exports = GoogleCalendarMoveToDate
}
<commit_msg>Log url used for retrieving calendar year<commit_after>const TEST_ENV = 'TEST';
const PROD_ENV = 'PROD';

class GoogleCalendarMoveToDate {
    // Maximum number of events to keep in a cell.
    #DEFAULT_MAX_EVENTS_PER_CELL = 14;

    // Oldest available cell date that has been found and should be used.
    topCellDate;

    constructor(options) {
        this.options = options;
        this.options.maxEventsPerCell = this.options.maxEventsPerCell || this.#DEFAULT_MAX_EVENTS_PER_CELL;
        this.env = options.env;
        this.debug = options.debug ? true : false;
    }

    findCellsInRow(row) {
        this.debug && console.info('findCellsInRow');
        this.debug && console.log('row:', row);
        var rowCells;
        if (this.env === PROD_ENV) {
            rowCells = row.querySelectorAll('[role="gridcell"]');
        } else if (this.env === TEST_ENV) {
            rowCells = row;
        }
        this.debug && console.log('rowCells:', rowCells);
        return rowCells;
    }

    clearTopCellDate() {
        this.debug && console.info('clearTopCellDate');
        this.topCellDate = undefined;
    }

    countEventsInCell(cell) {
        this.debug && console.info('countEventsInCell');
        this.debug && console.log('cell:', cell);
        var cellEventsFound;
        if (this.env === PROD_ENV) {
            var cellEvents = cell.querySelectorAll('[data-eventid]');
            cellEventsFound = cellEvents.length;
            var moreCellEvents = cell.querySelector('[data-opens-day-overview]');
            if (moreCellEvents) {
                cellEventsFound += parseInt(moreCellEvents.innerText.match(/(\d+) more/)[1]);
            }
        } else if (this.env === TEST_ENV) {
            var cellEvents = cell;
            cellEventsFound = cellEvents.length;
        }
        this.debug && console.log('cellEventsFound:', cellEventsFound);
        return cellEventsFound;
    }

    findTopCell(cells) {
        // Find first top-right most cell in current view containing a non-full number of events.
        this.debug && console.group('findTopCell');
        this.debug && console.log('cells:', cells);
        var topCell;

        // Check top to bottom.
        outer_loop:
        for (var rowIndex = 0; rowIndex < cells.length; rowIndex++) {
            this.debug && console.group('rowIndex:', rowIndex);

            var row = cells[rowIndex];
            this.debug && console.log('row:', row);

            var rowCells = this.findCellsInRow(row);
            // Check right to left.
            for (var cellIndex = rowCells.length - 1; cellIndex >= 0; cellIndex--) {
                this.debug && console.log('cellIndex:', cellIndex);
                var cell = rowCells[cellIndex];
                this.debug && console.log('cell:', cell);

                var cellEventsFound = this.countEventsInCell(cell);

                if (this.env === PROD_ENV) {
                    cell = cell;
                } else if (this.env === TEST_ENV) {
                    cell = [rowIndex, cellIndex];
                }

                // Determine if current top cell should be cleared now that it has the maximum number of events per
                // cell.
                if (this.topCellDate !== undefined &&
                    cellEventsFound === this.options.maxEventsPerCell) {
                    var cellDate = this.getCellDate(cell);
                    if (cellDate === this.topCellDate) {
                        this.topCellDate = undefined;
                    }
                }

                if (cellEventsFound < this.options.maxEventsPerCell) {
                    topCell = cell;
                    this.debug && console.groupEnd();
                    break outer_loop;
                } else {
                    this.debug && console.log('ELSE');
                }
            }

            this.debug && console.groupEnd();
        }

        this.debug && console.log('topCell:', topCell);
        this.debug && console.groupEnd();
        return topCell;
    }

    getCalendarYear() {
        this.debug && console.info('getCalendarYear');

        var url = window.location.toString();
        this.debug && console.log('url:', url);

        // Fetch calendar year from url (e.g. 2019 in "https://calendar.google.com/calendar/r/customweek/2019/12/9" and
        // not 2020 using "(new Date()).getFullYear()").
        var calendarYear = url.match(/calendar\/r\/[a-z]+\/(\d\d\d\d)\//)[1];

        this.debug && console.log('calendarYear:', calendarYear);
        return calendarYear;
    }

    getCellDate(cell) {
        this.debug && console.info('getCellDate');
        this.debug && console.log('cell:', cell);
        var cellDate;
        if (this.env === PROD_ENV) {
            // "7 events, Thursday, October 17".
            var cellDescription = cell.querySelector('h2').innerText;

            // "October 17".
            var monthDate = cellDescription.match(/ events?, .*day, (.*?)$/)[1];
            this.debug && console.log('monthDate:', monthDate);

            // "Oct 17".
            monthDate = monthDate.substr(0, 3) + ' ' + monthDate.split(' ')[1];
            this.debug && console.log('monthDate:', monthDate);

            // "Oct 17, 2019".
            var monthDateYear = monthDate + ', ' + this.getCalendarYear();
            this.debug && console.log('monthDateYear:', monthDateYear);

            cellDate = monthDateYear;
        } else if (this.env === TEST_ENV) {
            var rowIndex = cell[0];
            var cellIndex = cell[1];
            cellDate = this.options.cellDates[rowIndex][cellIndex];
        }
        this.debug && console.log('cellDate:', cellDate);
        return cellDate;
    }

    getTopCellDate(calendarGridRows) {
        this.debug && console.info('getTopCellDate');

        // Calculate if cell date is older than the current top cell date only when a top cell is found.
        var topCell = this.findTopCell(calendarGridRows);
        if (topCell !== undefined) {
            var cellDate = this.getCellDate(topCell);
            if (this.topCellDate === undefined) {
                this.debug && console.log('using newly calculated cellDate:', cellDate);
                this.topCellDate = cellDate;
            } else {
                this.debug && console.log('comparing cellDate %s to current topCellDate %s', cellDate, this.topCellDate);
                if (this._getDateComparisonObject(cellDate) < this._getDateComparisonObject(this.topCellDate)) {
                    this.debug && console.log('updating current topCellDate');
                    this.topCellDate = cellDate;
                } else {
                    this.debug && console.log('keeping current topCellDate');
                }
            }
        }

        this.debug && console.log('topCellDate:', this.topCellDate);
        return this.topCellDate;
    }

    setOption(option, value) {
        this.options[option] = value;
    }

    _getDateComparisonObject(date) {
        return new Date(date).getTime();
    }
}

if (typeof(window) === 'undefined') {
    module.exports = GoogleCalendarMoveToDate
}
<|endoftext|>


<commit_before>var format      = require('url').format;
var querystring = require('querystring');
var JStream     = require('jstream');
var sax         = require('sax');
var request     = require('./request');
var util        = require('./util');
var sig         = require('./sig');
var FORMATS  = require('./formats');

var VIDEO_URL = 'https://www.youtube.com/watch?v=';
var EMBED_URL = 'https://www.youtube.com/embed/';
var VIDEO_EURL = 'https://youtube.googleapis.com/v/';
var INFO_HOST = 'www.youtube.com';
var INFO_PATH = '/get_video_info';
var KEYS_TO_SPLIT = [
  'keywords',
  'fmt_list',
  'fexp',
  'watermark'
];

/**
 * Gets info from a video.
 *
 * @param {String} link
 * @param {Object} opts
 * @param {Function(Error, Object)} callback
 */
module.exports = function getInfo(link, opts, callback) {
  if (typeof opts === 'function') {
    callback = opts;
    opts = {};
  } else if (!opts) {
    opts = {};
  }

  var id = util.getVideoID(link);
  var url = VIDEO_URL + id;
  request(url, function(err, body) {
    if (err) return callback(err);

    // Get description from #eow-description
    var description = util.getVideoDescription(body);

    var jsonStr = util.between(body, 'ytplayer.config = ', '</script>');
    if (jsonStr) {
      var config = parseJSON(jsonStr);
      if (!config) {
        return callback(new Error('could not parse video page config'));
      }
      gotConfig(opts, description, config, callback);
    } else {
      url = EMBED_URL + id;
      request(url, function(err, body) {
        if (err) return callback(err);

        jsonStr = util.between(body,
          'yt.setConfig(\'PLAYER_CONFIG\', ', '</script>');
        if (!jsonStr) {
          return callback(new Error('could not find `player config`'));
        }

        var config = parseJSON(jsonStr);
        if (!config) {
          return callback(new Error('could not parse embed page config'));
        }

        url = format({
          protocol: 'https',
          host: INFO_HOST,
          pathname: INFO_PATH,
          query: {
            video_id: id,
            eurl: VIDEO_EURL + id,
            sts: config.sts || '',
          },
        });
        request(url, function(err, body) {
          if (err) return callback(err);
          config.args = querystring.parse(body);
          gotConfig(opts, description, config, callback);
        });
      });
    }
  });
};

/**
 * JStream is used because we know when the JSON string begins,
 * but not when it ends. So a string that contains it, and that may
 * contain a bunch of other things, is read until the first object
 * is completely parsed.
 *
 * @param {String} body
 * @return {Object}
 */
function parseJSON(body) {
  var jstream = new JStream();
  var config = null;

  jstream.on('data', function(data) {
    config = data;
    jstream.pause();
  });

  // Suppress errors. If there is one, `config` won't be defined,
  // which is already checked.
  jstream.on('error', function() {});

  jstream.end(body);
  return config;
}

/**
 * @param {Object} opts
 * @param {String} description
 * @param {Object} config
 * @param {Function(Error, Object)} callback
 */
function gotConfig(opts, description, config, callback) {
  var info = config.args;
  if (info.status === 'fail') {
    var msg = info.errorcode && info.reason ?
      'Code ' + info.errorcode + ': ' + info.reason : 'Video not found';
    callback(new Error(msg));
    return;
  }

  // Split some keys by commas.
  KEYS_TO_SPLIT.forEach(function(key) {
    if (!info[key]) return;
    info[key] = info[key]
    .split(',')
    .filter(function(v) { return v !== ''; });
  });

  info.fmt_list = info.fmt_list ?
    info.fmt_list.map(function(format) {
      return format.split('/');
    }) : [];

  if (info.video_verticals) {
    info.video_verticals = info.video_verticals
    .slice(1, -1)
    .split(', ')
    .filter(function(val) { return val !== ''; })
    .map(function(val) { return parseInt(val, 10); })
    ;
  }

  info.formats = util.parseFormats(info);
  info.description = description;

  if (info.dashmpd) {
    sig.getTokens(config.assets.js, opts.debug, function(err, tokens) {
      if (err) return callback(err);

      info.dashmpd = info.dashmpd
        .replace(/\/s\/([a-fA-F0-9\.]+)/, function(_, s) {
          return '/signature/' + sig.decipher(tokens, s);
        });
      getDashManifest(info.dashmpd, opts.debug, function(err, dformats) {
        if (err) return callback(err);

        sig.decipherFormats(info.formats, tokens, opts.debug);
        var formatsMap = {};
        info.formats.forEach(function(f) { formatsMap[f.itag] = f; });

        // Merge formats from dash manifest with formats from video info page.
        dformats.forEach(function(f) {
          var cf = formatsMap[f.itag];
          if (cf) {
            for (var key in f) { if (!cf[key]) { cf[key] = f[key]; } }
          } else {
            formatsMap[f.itag] = f;
          }
        });
        info.formats = [];
        for (var itag in formatsMap) { info.formats.push(formatsMap[itag]); }
        if (!info.formats.length) {
          callback(new Error('No formats found'));
          return;
        }
        info.formats.sort(util.sortFormats);
        callback(null, info);
      });
    });
  } else {
    if (!info.formats.length) {
      callback(new Error('Video not found'));
      return;
    }
    sig.get(info.formats, config.assets.js, opts.debug, function(err) {
      if (err) return callback(err);
      info.formats.sort(util.sortFormats);
      callback(null, info);
    });
  }
}

/**
 * Gets additional DASH formats.
 *
 * @param {String} url
 * @param {Boolean} debug
 * @param {Function(!Error, Array.<Object>)} callback
 */
function getDashManifest(url, debug, callback) {
  var formats = [];
  var currentFormat = null;
  var expectUrl = false;

  var parser = sax.parser(true);
  parser.onerror = callback;
  parser.onopentag = function(node) {
    if (node.name === 'Representation') {
      var itag = node.attributes.id;
      var meta = FORMATS[itag];
      if (!meta && debug) {
        console.warn('No format metadata for itag ' + itag + ' found');
      }
      currentFormat = { itag: itag };
      for (var key in meta) {
        currentFormat[key] = meta[key];
      }
      formats.push(currentFormat);
    }
    expectUrl = node.name === 'BaseURL';
  };
  parser.ontext = function(text) {
    if (expectUrl) {
      currentFormat.url = text;
    }
  };
  parser.onend = function() { callback(null, formats); };

  var req = request(url);
  req.on('error', callback);
  req.on('response', function(res) {
    res.setEncoding('utf8');
    res.on('error', callback);
    res.on('data', function(chunk) { parser.write(chunk); });
    res.on('end', parser.close.bind(parser));
  });
}
<commit_msg>get and merge from hlsvp formats<commit_after>var format      = require('url').format;
var querystring = require('querystring');
var JStream     = require('jstream');
var sax         = require('sax');
var request     = require('./request');
var util        = require('./util');
var sig         = require('./sig');
var FORMATS  = require('./formats');

var VIDEO_URL = 'https://www.youtube.com/watch?v=';
var EMBED_URL = 'https://www.youtube.com/embed/';
var VIDEO_EURL = 'https://youtube.googleapis.com/v/';
var INFO_HOST = 'www.youtube.com';
var INFO_PATH = '/get_video_info';
var KEYS_TO_SPLIT = [
  'keywords',
  'fmt_list',
  'fexp',
  'watermark'
];

/**
 * Gets info from a video.
 *
 * @param {String} link
 * @param {Object} opts
 * @param {Function(Error, Object)} callback
 */
module.exports = function getInfo(link, opts, callback) {
  if (typeof opts === 'function') {
    callback = opts;
    opts = {};
  } else if (!opts) {
    opts = {};
  }

  var id = util.getVideoID(link);
  var url = VIDEO_URL + id;
  request(url, function(err, body) {
    if (err) return callback(err);

    // Get description from #eow-description
    var description = util.getVideoDescription(body);

    var jsonStr = util.between(body, 'ytplayer.config = ', '</script>');
    if (jsonStr) {
      var config = parseJSON(jsonStr);
      if (!config) {
        return callback(new Error('could not parse video page config'));
      }
      gotConfig(opts, description, config, callback);
    } else {
      url = EMBED_URL + id;
      request(url, function(err, body) {
        if (err) return callback(err);

        jsonStr = util.between(body,
          'yt.setConfig(\'PLAYER_CONFIG\', ', '</script>');
        if (!jsonStr) {
          return callback(new Error('could not find `player config`'));
        }

        var config = parseJSON(jsonStr);
        if (!config) {
          return callback(new Error('could not parse embed page config'));
        }

        url = format({
          protocol: 'https',
          host: INFO_HOST,
          pathname: INFO_PATH,
          query: {
            video_id: id,
            eurl: VIDEO_EURL + id,
            sts: config.sts || '',
          },
        });
        request(url, function(err, body) {
          if (err) return callback(err);
          config.args = querystring.parse(body);
          gotConfig(opts, description, config, callback);
        });
      });
    }
  });
};

/**
 * JStream is used because we know when the JSON string begins,
 * but not when it ends. So a string that contains it, and that may
 * contain a bunch of other things, is read until the first object
 * is completely parsed.
 *
 * @param {String} body
 * @return {Object}
 */
function parseJSON(body) {
  var jstream = new JStream();
  var config = null;

  jstream.on('data', function(data) {
    config = data;
    jstream.pause();
  });

  // Suppress errors. If there is one, `config` won't be defined,
  // which is already checked.
  jstream.on('error', function() {});

  jstream.end(body);
  return config;
}

/**
 * @param {Object} opts
 * @param {String} description
 * @param {Object} config
 * @param {Function(Error, Object)} callback
 */
function gotConfig(opts, description, config, callback) {
  var info = config.args;
  if (info.status === 'fail') {
    var msg = info.errorcode && info.reason ?
      'Code ' + info.errorcode + ': ' + info.reason : 'Video not found';
    callback(new Error(msg));
    return;
  }

  // Split some keys by commas.
  KEYS_TO_SPLIT.forEach(function(key) {
    if (!info[key]) return;
    info[key] = info[key]
    .split(',')
    .filter(function(v) { return v !== ''; });
  });

  info.fmt_list = info.fmt_list ?
    info.fmt_list.map(function(format) {
      return format.split('/');
    }) : [];

  if (info.video_verticals) {
    info.video_verticals = info.video_verticals
    .slice(1, -1)
    .split(', ')
    .filter(function(val) { return val !== ''; })
    .map(function(val) { return parseInt(val, 10); })
    ;
  }

  info.formats = util.parseFormats(info);
  info.description = description;

  if (info.dashmpd || info.hlsvp) {
    sig.getTokens(config.assets.js, opts.debug, function(err, tokens) {
      if (err) return callback(err);

      var concurrent = +!!info.dashmpd + +!!info.hlsvp;
      function checkDone() {
        if (--concurrent > 0) { return; }
        if (!info.formats.length) {
          callback(new Error('No formats found'));
          return;
        }
        info.formats.sort(util.sortFormats);
        callback(null, info);
      }

      if (info.dashmpd) {
        info.dashmpd = decipherURL(info.dashmpd, tokens);
        getDashManifest(info.dashmpd, opts.debug, function(err, formats) {
          if (err) return callback(err);
          sig.decipherFormats(info.formats, tokens, opts.debug);
          mergeFormats(info, formats);
          checkDone();
        });
      }

      if (info.hlsvp) {
        info.hlsvp = decipherURL(info.hlsvp, tokens);
        getM3U8(info.hlsvp, opts.debug, function(err, formats) {
          if (err) return callback(err);
          mergeFormats(info, formats);
          checkDone();
        });
      }
    });
  } else {
    if (!info.formats.length) {
      callback(new Error('Video not found'));
      return;
    }
    sig.get(info.formats, config.assets.js, opts.debug, function(err) {
      if (err) return callback(err);
      info.formats.sort(util.sortFormats);
      callback(null, info);
    });
  }
}

/**
 * @param {String} url
 * @param {Array.<String>} tokens
 */
function decipherURL(url, tokens) {
  return url.replace(/\/s\/([a-fA-F0-9\.]+)/, function(_, s) {
    return '/signature/' + sig.decipher(tokens, s);
  });
}

/**
 * Merges formats from DASH or M3U8 with formats from video info page.
 *
 * @param {Object} info
 * @param {Object} formatsMap
 */
function mergeFormats(info, formatsMap) {
  info.formats.forEach(function(f) {
    var cf = formatsMap[f.itag];
    if (cf) {
      for (var key in f) { cf[key] = f[key]; }
    } else {
      formatsMap[f.itag] = f;
    }
  });
  info.formats = [];
  for (var itag in formatsMap) { info.formats.push(formatsMap[itag]); }
}

/**
 * Gets additional DASH formats.
 *
 * @param {String} url
 * @param {Boolean} debug
 * @param {Function(!Error, Array.<Object>)} callback
 */
function getDashManifest(url, debug, callback) {
  var formats = {};
  var currentFormat = null;
  var expectUrl = false;

  var parser = sax.parser(true);
  parser.onerror = callback;
  parser.onopentag = function(node) {
    if (node.name === 'Representation') {
      var itag = node.attributes.id;
      var meta = FORMATS[itag];
      if (!meta && debug) {
        console.warn('No format metadata for itag ' + itag + ' found');
      }
      currentFormat = { itag: itag };
      for (var key in meta) {
        currentFormat[key] = meta[key];
      }
      formats[itag] = currentFormat;
    }
    expectUrl = node.name === 'BaseURL';
  };
  parser.ontext = function(text) {
    if (expectUrl) {
      currentFormat.url = text;
    }
  };
  parser.onend = function() { callback(null, formats); };

  var req = request(url);
  req.on('error', callback);
  req.on('response', function(res) {
    res.setEncoding('utf8');
    res.on('error', callback);
    res.on('data', function(chunk) { parser.write(chunk); });
    res.on('end', parser.close.bind(parser));
  });
}

/**
 * Gets additional formats.
 *
 * @param {String} url
 * @param {Boolean} debug
 * @param {Function(!Error, Array.<Object>)} callback
 */
function getM3U8(url, debug, callback) {
  request(url, function(err, body) {
    if (err) return callback(err);

    var formats = {};
    body
      .split('\n')
      .filter(function(line) {
        return line.trim().length && line[0] !== '#';
      })
      .forEach(function(line) {
        var itag = line.match(/\/itag\/(\d+)\//)[1];
        if (!itag) {
          if (debug) {
            console.warn('No itag found in url ' + line);
          }
          return;
        }
        var meta = FORMATS[itag];
        if (!meta && debug) {
          console.warn('No format metadata for itag ' + itag + ' found');
        }
        var format = { itag: itag, url: line };
        for (var key in meta) {
          format[key] = meta[key];
        }
        formats[itag] = format;
      });
    callback(null, formats);
  });
}
<|endoftext|>


<commit_before>
/*!
* Jade - Language Independent Templates.
* Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
* MIT Licensed
*/

/**
* Library version.
*/

exports.version = '0.0.1';

/**
* Module dependencies.
*/

var sys = require('sys'),
    fs = require('fs');

/**
* Self closing tags.
*/

var selfClosing = exports.selfClosing = [
    'meta',
    'img',
    'link',
    'br',
    'hr',
    'input',
    'area',
    'base'
];

/**
* Default supported doctypes.
*/

var doctypes = exports.doctypes = {
    '5': '<!DOCTYPE html>',
    'xml': '<?xml version="1.0" encoding="utf-8" ?>',
    'default': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
    'transitional': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
    'strict': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
    'frameset': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
    '1.1': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
    'basic': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
    'mobile': '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
};

/**
* Initialize jade parser with the given input string.
*
* @param {String} str
* @api public
*/

function Parser(str){
    this.lineno = 1;
    this.input = str;
    this.lastIndents = 0;
    this.deferredTokens = [];
}

// TODO: html 5 mode... use sections vs divs? ... and selected vs selected="selected"
// TODO: handle line endings
// TODO: attrs with code...
// TODO: attrs with props...
// TODO: alternate output formats
// TODO: use process.compile() or whatever
// TODO: bundled markdown support?
// TODO: interpolation

Parser.prototype = {

    /**
     * Output token stack for debugging.
     *
     * @api private
     */

    debug: function(){
        var tok, width = 8;
        while ((tok = parser.advance).type !== 'eos') {
            var type = tok.type,
            pad = width - type.length;
            while (pad--) type += ' ';
            sys.puts(tok.line
                + ' : \x1B[1m' + type + '\x1B[0m'
                + ' ' + sys.inspect(tok.val)
                + (tok.attrs ? ' ' + sys.inspect(tok.attrs) : ''));
        }
    },

    /**
     * Return the next token object.
     *
     * @return {Object}
     * @api private
     */

    get advance(){
        var self = this,
        captures;

        if (this.stash) {
            var tok = this.stash;
            delete this.stash;
            return tok;
        }

        if (this.deferredTokens.length) {
            return this.deferredTokens.shift();
        }

        /**
        * Generate token object.
        */

        function token(type){
            self.input = self.input.substr(captures[0].length);
            return {
                type: type,
                line: self.lineno,
                val: captures[1]
            };
        }

        // EOS
        if (!this.input.length) {
            if (this.lastIndents-- > 0) {
                return { type: 'outdent', line: this.lineno };
            } else {
                return { type: 'eos', line: this.lineno };
            }
        }

        // Tag
        if (captures = /^(\w[:\w]*)/.exec(this.input)) {
            return token('tag');
        }

        // Code
        if (captures = /^(!?=|-)([^\n]+)/.exec(this.input)) {
            var flags = captures[1];
            captures[1] = captures[2];
            var tok = token('code');
            tok.escape = flags[0] === '=';
            tok.buffer = flags[0] === '=' || flags[1] === '=';
            return tok;
        }

        // Doctype
        if (captures = /^!!! *(\w+)?/.exec(this.input)) {
            return token('doctype');
        }

        // Id
        if (captures = /^#([\w-]+)/.exec(this.input)) {
            return token('id');
        }

        // Class
        if (captures = /^\.([\w-]+)/.exec(this.input)) {
            return token('class');
        }

        // Attributes
        if (captures = /^\(([^\)]+)\)/.exec(this.input)) {
            var tok = token('attrs'),
            attrs = tok.val.split(/ *, */);
            tok.attrs = {};
            for (var i = 0, len = attrs.length; i < len; ++i) {
                var pair = attrs[i].split(/ *[:=] */);
                tok.attrs[pair[0]] = pair[1];
            }
            return tok;
        }

        // Indent
        if (captures = /^\n( *)/.exec(this.input)) {
            ++this.lineno;
            var tok = token('indent'),
                indents = tok.val.length / 2;
            if (indents % 1 !== 0) {
                throw new Error('Jade(' + this.lineno + '): Invalid indents, got '
                    + tok.val.length + ' space'
                    + (tok.val.length > 1 ? 's' : '')
                    + ', must be a multiple of two.');
            } else if (indents === this.lastIndents) {
                tok.type = 'newline';
            } else if (indents > this.lastIndents + 1) {
                throw new Error('Jade(' + this.lineno + '): Invalid indentation, got '
                    + indents + ' expected '
                    + (this.lastIndents + 1));
            } else if (indents < this.lastIndents) {
                var n = this.lastIndents - indents;
                tok.type = 'outdent';
                while (--n) {
                    this.deferredTokens.push({
                        type: 'outdent',
                        line: this.lineno
                    });
                }
            }
            this.lastIndents = indents;
            return tok;
        }

        // Text
        if (captures = /^(?:\| ?)?([^\n]+)/.exec(this.input)) {
            return token('text');
        }
    },

    /**
     * Single token lookahead.
     *
     * @return {Object}
     * @api private
     */

    get peek() {
        return this.stash = this.advance;
    },

    /**
     * Parse input returning a string of js for evaluation.
     *
     * @return {String}
     * @api public
     */

    parse: function(){
        var buf = ['var buf = [];\n'];
        while (this.peek.type !== 'eos') {
            buf.push(this.parseExpr());
        }
        buf.push("\nreturn buf.join('');");
        return buf.join('\n');
    },

    /**
     * Expect the given type, or throw an exception.
     *
     * @param {String} type
     * @api private
     */

    expect: function(type){
        if (this.peek.type === type) {
            return this.advance;
        } else {
            throw new Error('Jade(' + this.lineno
                + '): expected "' + type
                + '", but got "' + this.peek.type + '"');
        }
    },

    /**
     *   tag
     * | id
     * | class
     * | text
     * | doctype
     * | code block?
     * | expr newline
     */

    parseExpr: function(){
        switch (this.peek.type) {
            case 'tag':
                return this.parseTag();
            case 'doctype':
                return this.parseDoctype();
            case 'text':
                return "buf.push('" + this.advance.val + " ');";
            case 'id':
            case 'class':
                var tok = this.advance;
                this.deferredTokens.push({
                    val: 'div',
                    type: 'tag',
                    line: this.lineno
                });
                this.deferredTokens.push(tok);
                return this.parseExpr();
            case 'code':
                // TODO: support both
                // TODO: escape '
                var tok = this.advance;
                var buf = tok.buffer
                    ? 'buf.push(' + tok.val + ')'
                    : tok.val;
                return this.peek.type === 'indent'
                    ? buf + '\n(function(){' + this.parseBlock() + '})();'
                    : buf + ';';
            case 'newline':
                this.advance;
                return this.parseExpr();
        }
    },

    /**
     * doctype.
     */

    parseDoctype: function(){
        var tok = this.expect('doctype');
        return "buf.push('" + doctypes[tok.val || 'default'] + "');";
    },

    /**
     * indent expr outdent
     */

    parseBlock: function(){
        var buf = [];
        this.expect('indent');
        while (this.peek.type !== 'outdent') {
            buf.push(this.parseExpr());
        }
        this.expect('outdent');
        return buf.join('\n');
    },

    /**
     * tag (attrs | class | id)* (text | code | block)
     */

    parseTag: function(){
        var name = this.advance.val,
            hasAttrs = false,
            attrBuf = '',
            attrs = {},
            buf = [];

        // (attrs | class | id)*
        out:
            while (1) {
                switch (this.peek.type) {
                    case 'id':
                    case 'class':
                        hasAttrs = true;
                        var tok = this.advance;
                        attrs[tok.type] = attrs[tok.type]
                            ? attrs[tok.type] += ' ' + tok.val
                            : '"' + tok.val + '"';
                        continue;
                    case 'attrs':
                        hasAttrs = true;
                        var obj = this.advance.attrs,
                            keys = Object.keys(obj);
                        for (var i = 0, len = keys.length; i < len; ++i) {
                            var key = keys[i],
                                val = obj[key];

                            // Append class only
                            val = key === 'class'
                                ? attrs['class']
                                    ? attrs['class'] + ' ' + val
                                    : val
                                : val;

                            attrs[key] = val;
                        }
                        continue;
                    default:
                        break out;
                }
            }

        // (text | code | block)
        switch (this.peek.type) {
            case 'text':
                // TODO: escape '
                buf.push("buf.push('" + this.advance.val.trim() + "');");
                break;
            case 'code':
                // TODO: throw on non buffered code
                buf.push('buf.push(' + this.advance.val + ');');
                break;
            case 'indent':
                buf.push(this.parseBlock());
                break;
        }

        // Build attrs
        if (hasAttrs) {
            // TODO: escape '
            attrBuf += "' + attrs({ ";
            var keys = Object.keys(attrs);
            for (var i = 0, len = keys.length; i < len; ++i) {
                var key = keys[i],
                    val = attrs[key];
                attrBuf += key + ': ' + val + (i === len - 1 ? '' : ', ');
            }
            attrBuf += " }) + '";
        } else {
            attrBuf = "' + '";
        }

        // Build the tag
        if (selfClosing.indexOf(name) >= 0) {
            return [
                "buf.push('<" + name + attrBuf + " />');",
                buf.join('\n')
            ].join('\n');
        } else {
            return [
                "buf.push('<" + name + attrBuf + ">');",
                buf.join('\n'),
                "buf.push('</" + name + ">');"
            ].join('\n');
        }
    }
};

/**
 * Render the given attributes object.
 *
 * @param {Object} obj
 * @return {String}
 * @api private
 */

function attrs(obj){
    var buf = [],
        keys = Object.keys(obj),
        len = keys.length;
    if (len) {
        buf.push('');
        for (var i = 0; i < len; ++i) {
            var key = keys[i],
                val = obj[key];
            // Boolean
            if (val === undefined) {
                val = key;
            }
            buf.push(key + '="' + escape(val) + '"');
        }
    }
    return buf.join(' ');
}

/**
 * Escape the given string of `html`.
 *
 * @param {String} html
 * @return {String}
 * @api private
 */

function escape(html){
    return String(html)
        .replace(/&/g, '&')
        .replace(/</g, '<')
        .replace(/>/g, '>')
        .replace(/"/g, '"');
}

/**
 * Render the given `str` of jade.
 *
 * Options:
 *
 *   - `scope`   Evaluation scope (`this`)
 *   - `locals`  Local variable object
 *
 * Examples:
 *
 *     jade.render('h1= title', {
 *         title: 'Jade rules!'
 *     });
 *
 * @param {String} str
 * @param {Object} options
 * @return {String}
 * @api public
 */

exports.render = function(str, options){
    options = options || {};
    var parser = new Parser(str),
        js = parser.parse(),
        fn = Function('locals, attrs, escape', 'with (locals) {' + js + '}');
    return fn.call(options.scope,
        options.locals || {},
        attrs,
        escape);
};

var str = fs.readFileSync('examples/layout.jade', 'ascii');

var parser = new Parser(str);
parser.debug();

var parser = new Parser(str);
sys.puts(parser.parse());

sys.puts(exports.render(str));<commit_msg>Added renderFile()<commit_after>
/*!
* Jade - Language Independent Templates.
* Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
* MIT Licensed
*/

/**
* Library version.
*/

exports.version = '0.0.1';

/**
* Module dependencies.
*/

var sys = require('sys'),
    fs = require('fs');

/**
* Self closing tags.
*/

var selfClosing = exports.selfClosing = [
    'meta',
    'img',
    'link',
    'br',
    'hr',
    'input',
    'area',
    'base'
];

/**
* Default supported doctypes.
*/

var doctypes = exports.doctypes = {
    '5': '<!DOCTYPE html>',
    'xml': '<?xml version="1.0" encoding="utf-8" ?>',
    'default': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
    'transitional': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
    'strict': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
    'frameset': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
    '1.1': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
    'basic': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
    'mobile': '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
};

/**
* Initialize jade parser with the given input string.
*
* @param {String} str
* @api public
*/

function Parser(str){
    this.lineno = 1;
    this.input = str;
    this.lastIndents = 0;
    this.deferredTokens = [];
}

// TODO: html 5 mode... use sections vs divs? ... and selected vs selected="selected"
// TODO: handle line endings
// TODO: attrs with code...
// TODO: attrs with props...
// TODO: alternate output formats
// TODO: use process.compile() or whatever
// TODO: bundled markdown support?
// TODO: interpolation

Parser.prototype = {

    /**
     * Output token stack for debugging.
     *
     * @api private
     */

    debug: function(){
        var tok, width = 8;
        while ((tok = parser.advance).type !== 'eos') {
            var type = tok.type,
            pad = width - type.length;
            while (pad--) type += ' ';
            sys.puts(tok.line
                + ' : \x1B[1m' + type + '\x1B[0m'
                + ' ' + sys.inspect(tok.val)
                + (tok.attrs ? ' ' + sys.inspect(tok.attrs) : ''));
        }
    },

    /**
     * Return the next token object.
     *
     * @return {Object}
     * @api private
     */

    get advance(){
        var self = this,
        captures;

        if (this.stash) {
            var tok = this.stash;
            delete this.stash;
            return tok;
        }

        if (this.deferredTokens.length) {
            return this.deferredTokens.shift();
        }

        /**
        * Generate token object.
        */

        function token(type){
            self.input = self.input.substr(captures[0].length);
            return {
                type: type,
                line: self.lineno,
                val: captures[1]
            };
        }

        // EOS
        if (!this.input.length) {
            if (this.lastIndents-- > 0) {
                return { type: 'outdent', line: this.lineno };
            } else {
                return { type: 'eos', line: this.lineno };
            }
        }

        // Tag
        if (captures = /^(\w[:\w]*)/.exec(this.input)) {
            return token('tag');
        }

        // Code
        if (captures = /^(!?=|-)([^\n]+)/.exec(this.input)) {
            var flags = captures[1];
            captures[1] = captures[2];
            var tok = token('code');
            tok.escape = flags[0] === '=';
            tok.buffer = flags[0] === '=' || flags[1] === '=';
            return tok;
        }

        // Doctype
        if (captures = /^!!! *(\w+)?/.exec(this.input)) {
            return token('doctype');
        }

        // Id
        if (captures = /^#([\w-]+)/.exec(this.input)) {
            return token('id');
        }

        // Class
        if (captures = /^\.([\w-]+)/.exec(this.input)) {
            return token('class');
        }

        // Attributes
        if (captures = /^\(([^\)]+)\)/.exec(this.input)) {
            var tok = token('attrs'),
            attrs = tok.val.split(/ *, */);
            tok.attrs = {};
            for (var i = 0, len = attrs.length; i < len; ++i) {
                var pair = attrs[i].split(/ *[:=] */);
                tok.attrs[pair[0]] = pair[1];
            }
            return tok;
        }

        // Indent
        if (captures = /^\n( *)/.exec(this.input)) {
            ++this.lineno;
            var tok = token('indent'),
                indents = tok.val.length / 2;
            if (indents % 1 !== 0) {
                throw new Error('Jade(' + this.lineno + '): Invalid indents, got '
                    + tok.val.length + ' space'
                    + (tok.val.length > 1 ? 's' : '')
                    + ', must be a multiple of two.');
            } else if (indents === this.lastIndents) {
                tok.type = 'newline';
            } else if (indents > this.lastIndents + 1) {
                throw new Error('Jade(' + this.lineno + '): Invalid indentation, got '
                    + indents + ' expected '
                    + (this.lastIndents + 1));
            } else if (indents < this.lastIndents) {
                var n = this.lastIndents - indents;
                tok.type = 'outdent';
                while (--n) {
                    this.deferredTokens.push({
                        type: 'outdent',
                        line: this.lineno
                    });
                }
            }
            this.lastIndents = indents;
            return tok;
        }

        // Text
        if (captures = /^(?:\| ?)?([^\n]+)/.exec(this.input)) {
            return token('text');
        }
    },

    /**
     * Single token lookahead.
     *
     * @return {Object}
     * @api private
     */

    get peek() {
        return this.stash = this.advance;
    },

    /**
     * Parse input returning a string of js for evaluation.
     *
     * @return {String}
     * @api public
     */

    parse: function(){
        var buf = ['var buf = [];\n'];
        while (this.peek.type !== 'eos') {
            buf.push(this.parseExpr());
        }
        buf.push("\nreturn buf.join('');");
        return buf.join('\n');
    },

    /**
     * Expect the given type, or throw an exception.
     *
     * @param {String} type
     * @api private
     */

    expect: function(type){
        if (this.peek.type === type) {
            return this.advance;
        } else {
            throw new Error('Jade(' + this.lineno
                + '): expected "' + type
                + '", but got "' + this.peek.type + '"');
        }
    },

    /**
     *   tag
     * | id
     * | class
     * | text
     * | doctype
     * | code block?
     * | expr newline
     */

    parseExpr: function(){
        switch (this.peek.type) {
            case 'tag':
                return this.parseTag();
            case 'doctype':
                return this.parseDoctype();
            case 'text':
                return "buf.push('" + this.advance.val + " ');";
            case 'id':
            case 'class':
                var tok = this.advance;
                this.deferredTokens.push({
                    val: 'div',
                    type: 'tag',
                    line: this.lineno
                });
                this.deferredTokens.push(tok);
                return this.parseExpr();
            case 'code':
                // TODO: support both
                // TODO: escape '
                var tok = this.advance;
                var buf = tok.buffer
                    ? 'buf.push(' + tok.val + ')'
                    : tok.val;
                return this.peek.type === 'indent'
                    ? buf + '\n(function(){' + this.parseBlock() + '})();'
                    : buf + ';';
            case 'newline':
                this.advance;
                return this.parseExpr();
        }
    },

    /**
     * doctype.
     */

    parseDoctype: function(){
        var tok = this.expect('doctype');
        return "buf.push('" + doctypes[tok.val || 'default'] + "');";
    },

    /**
     * indent expr outdent
     */

    parseBlock: function(){
        var buf = [];
        this.expect('indent');
        while (this.peek.type !== 'outdent') {
            buf.push(this.parseExpr());
        }
        this.expect('outdent');
        return buf.join('\n');
    },

    /**
     * tag (attrs | class | id)* (text | code | block)
     */

    parseTag: function(){
        var name = this.advance.val,
            hasAttrs = false,
            attrBuf = '',
            attrs = {},
            buf = [];

        // (attrs | class | id)*
        out:
            while (1) {
                switch (this.peek.type) {
                    case 'id':
                    case 'class':
                        hasAttrs = true;
                        var tok = this.advance;
                        attrs[tok.type] = attrs[tok.type]
                            ? attrs[tok.type] += ' ' + tok.val
                            : '"' + tok.val + '"';
                        continue;
                    case 'attrs':
                        hasAttrs = true;
                        var obj = this.advance.attrs,
                            keys = Object.keys(obj);
                        for (var i = 0, len = keys.length; i < len; ++i) {
                            var key = keys[i],
                                val = obj[key];

                            // Append class only
                            val = key === 'class'
                                ? attrs['class']
                                    ? attrs['class'] + ' ' + val
                                    : val
                                : val;

                            attrs[key] = val;
                        }
                        continue;
                    default:
                        break out;
                }
            }

        // (text | code | block)
        switch (this.peek.type) {
            case 'text':
                // TODO: escape '
                buf.push("buf.push('" + this.advance.val.trim() + "');");
                break;
            case 'code':
                // TODO: throw on non buffered code
                buf.push('buf.push(' + this.advance.val + ');');
                break;
            case 'indent':
                buf.push(this.parseBlock());
                break;
        }

        // Build attrs
        if (hasAttrs) {
            // TODO: escape '
            attrBuf += "' + attrs({ ";
            var keys = Object.keys(attrs);
            for (var i = 0, len = keys.length; i < len; ++i) {
                var key = keys[i],
                    val = attrs[key];
                attrBuf += key + ': ' + val + (i === len - 1 ? '' : ', ');
            }
            attrBuf += " }) + '";
        } else {
            attrBuf = "' + '";
        }

        // Build the tag
        if (selfClosing.indexOf(name) >= 0) {
            return [
                "buf.push('<" + name + attrBuf + " />');",
                buf.join('\n')
            ].join('\n');
        } else {
            return [
                "buf.push('<" + name + attrBuf + ">');",
                buf.join('\n'),
                "buf.push('</" + name + ">');"
            ].join('\n');
        }
    }
};

/**
 * Render the given attributes object.
 *
 * @param {Object} obj
 * @return {String}
 * @api private
 */

function attrs(obj){
    var buf = [],
        keys = Object.keys(obj),
        len = keys.length;
    if (len) {
        buf.push('');
        for (var i = 0; i < len; ++i) {
            var key = keys[i],
                val = obj[key];
            // Boolean
            if (val === undefined) {
                val = key;
            }
            buf.push(key + '="' + escape(val) + '"');
        }
    }
    return buf.join(' ');
}

/**
 * Escape the given string of `html`.
 *
 * @param {String} html
 * @return {String}
 * @api private
 */

function escape(html){
    return String(html)
        .replace(/&/g, '&')
        .replace(/</g, '<')
        .replace(/>/g, '>')
        .replace(/"/g, '"');
}

/**
 * Render the given `str` of jade.
 *
 * Options:
 *
 *   - `scope`   Evaluation scope (`this`)
 *   - `locals`  Local variable object
 *
 * Examples:
 *
 *     jade.render('h1= title', {
 *         title: 'Jade rules!'
 *     });
 *
 * @param {String} str
 * @param {Object} options
 * @return {String}
 * @api public
 */

exports.render = function(str, options){
    options = options || {};
    var parser = new Parser(str),
        js = parser.parse(),
        fn = Function('locals, attrs, escape', 'with (locals) {' + js + '}');
    return fn.call(options.scope,
        options.locals || {},
        attrs,
        escape);
};

/**
 * Render jade template at the given `path` and optional callback `fn`.
 *
 * @param {String} path
 * @param {Object} options
 * @param {Function} fn
 * @api public
 */

exports.renderFile = function(path, options, fn){
    fn = typeof options === 'function'
        ? options
        : fn;
    fs.readFile(path, 'utf8', function(err, str){
        if (err) {
            fn && fn(err);
        } else {
            fn && fn(null, exports.render(str, options));
        }
    });
};

var str = fs.readFileSync('examples/layout.jade', 'ascii');

var parser = new Parser(str);
parser.debug();

var parser = new Parser(str);
sys.puts(parser.parse());

sys.puts(exports.render(str));<|endoftext|>


<commit_before>/*
 * Copyright 2015-2017 Hewlett Packard Enterprise Development Company, L.P.
 * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
 */

define([
    'underscore',
    'jquery',
    'backbone',
    'find/app/util/topic-map-view',
    'find/app/model/entity-collection',
    'i18n!find/nls/bundle',
    'find/app/util/generate-error-support-message',
    'text!find/templates/app/page/search/results/entity-topic-map-view.html',
    'text!find/templates/app/page/loading-spinner.html',
    'iCheck'
], function(_, $, Backbone, TopicMapView, EntityCollection, i18n, generateErrorHtml,
            template, loadingTemplate) {
    'use strict';

    const loadingHtml = _.template(loadingTemplate)({i18n: i18n, large: true});

    /**
     * @readonly
     * @enum {String}
     */
    const ViewState = {
        LOADING: 'LOADING',
        ERROR: 'ERROR',
        EMPTY: 'EMPTY',
        MAP: 'MAP'
    };

    const SPEED_SLIDER_MIN = 50;
    const DEFAULT_MAX_RESULTS = 300;

    return Backbone.View.extend({
        template: _.template(template),

        events: {
            'change .speed-slider': function(e) {
                const $target = $(e.target);
                const value = $target.val();
                $target.attr('data-original-title', value);
                $target.tooltip('show');
                $target.blur();
                this.model.set('maxResults', value);
            },
            'input .speed-slider': function(e) {
                const $target = $(e.target);
                const value = $target.val();
                this.$('.tooltip-inner').text(value);
            }
        },

        initialize: function(options) {
            this.queryState = options.queryState;

            this.entityCollection = new EntityCollection([], {
                getSelectedRelatedConcepts: function() {
                    // Comparison topic view does not have queryState
                    return this.queryState
                        ? _.flatten(this.queryState.conceptGroups.pluck('concepts'))
                        : [];
                }.bind(this)
            });

            this.debouncedFetchRelatedConcepts = _.debounce(this.entityCollection.fetchRelatedConcepts.bind(this.entityCollection), 500);

            this.queryModel = options.queryModel;
            this.type = options.type;
            this.showSlider = _.isUndefined(options.showSlider) || options.showSlider;
            this.fixedHeight = _.isUndefined(options.fixedHeight) || options.fixedHeight;

            this.topicMap = new TopicMapView({
                clickHandler: options.clickHandler
            });

            this.errorTemplate = generateErrorHtml({messageToUser: i18n['search.topicMap.error']});

            this.viewModel = new Backbone.Model({
                state: ViewState.EMPTY
            });

            const configuredMaxResults = options.configuration && _.isNumber(options.configuration.topicMapMaxResults)
                ? Math.max(options.configuration.topicMapMaxResults, SPEED_SLIDER_MIN + 1)
                : 0;

            const constructorMaxResults = _.isNumber(options.maxResults)
                ? Math.max(options.maxResults, SPEED_SLIDER_MIN + 1)
                : 0;

            this.maximumMaxResults = constructorMaxResults || configuredMaxResults || DEFAULT_MAX_RESULTS;

            this.model = new Backbone.Model({
                maxCount: 10,
                maxResults: constructorMaxResults || Math.min(this.maximumMaxResults, DEFAULT_MAX_RESULTS)
            });

            this.listenTo(this.model, 'change:maxResults', function() {
                this.debouncedFetchRelatedConcepts(this.queryModel, this.type, this.model.get('maxResults'));
            });

            this.listenTo(this.queryModel, 'change', function() {
                this.entityCollection.fetchRelatedConcepts(this.queryModel, this.type, this.model.get('maxResults'));
            });

            this.listenTo(this.entityCollection, 'sync', function() {
                this.viewModel.set('state', this.entityCollection.isEmpty()
                    ? ViewState.EMPTY
                    : ViewState.MAP);
                this.updateTopicMapData();
                this.update();
            });

            this.listenTo(this.entityCollection, 'request', function() {
                this.viewModel.set('state', ViewState.LOADING);
            });

            this.listenTo(this.entityCollection, 'error', function(collection, xhr) {
                this.generateErrorMessage(xhr);
                // Status of zero means the request has been aborted
                this.viewModel.set('state', xhr.status === 0
                    ? ViewState.LOADING
                    : ViewState.ERROR);
            });

            this.listenTo(this.viewModel, 'change', this.updateViewState);
            this.entityCollection.fetchRelatedConcepts(this.queryModel, this.type, this.model.get('maxResults'));
        },

        render: function() {
            if(this.showSlider) {
                this.$('.speed-slider').tooltip('destroy');
            }

            this.$el.html(this.template({
                cid: this.cid,
                errorTemplate: this.errorTemplate,
                i18n: i18n,
                loadingHtml: loadingHtml,
                showSlider: this.showSlider,
                fixedHeight: this.fixedHeight
            }));

            this.$error = this.$('.entity-topic-map-error');
            this.$empty = this.$('.entity-topic-map-empty');
            this.$loading = this.$('.entity-topic-map-loading');

            if(this.showSlider) {
                const maxResults = this.model.get('maxResults');
                this.$('.speed-slider')
                    .attr({
                        min: SPEED_SLIDER_MIN,
                        max: this.maximumMaxResults,
                        step: 1
                    })
                    .val(maxResults)
                    .tooltip({
                        title: maxResults,
                        placement: 'top'
                    });
            }

            this.topicMap.setElement(this.$('.entity-topic-map')).render();
            this.update();
            this.updateViewState();
        },

        remove: function() {
            if(this.showSlider) {
                this.$('.speed-slider').tooltip('destroy');
            }

            Backbone.View.prototype.remove.call(this);
        },

        update: function() {
            this.topicMap.draw();
        },

        updateTopicMapData: function() {
            this.topicMap.setData(this.entityCollection.processDataForTopicMap());
        },

        updateViewState: function() {
            const state = this.viewModel.get('state');
            this.topicMap.$el.toggleClass('hide', state !== ViewState.MAP);

            if(this.$error) {
                if(state === ViewState.ERROR) {
                    this.$error.html(this.errorTemplate);
                }
                this.$error.toggleClass('hide', state !== ViewState.ERROR);
            }

            if(this.$empty) {
                this.$empty.toggleClass('hide', state !== ViewState.EMPTY);
            }

            if(this.$loading) {
                this.$loading.toggleClass('hide', state !== ViewState.LOADING);
            }
        },

        generateErrorMessage: function(xhr) {
            this.errorTemplate = generateErrorHtml(
                _.extend(
                    {messageToUser: i18n['search.topicMap.error']},
                    xhr.responseJSON
                        ? {
                            errorDetails: xhr.responseJSON.message,
                            errorDetailsFallback: xhr.responseJSON.uuid,
                            errorUUID: xhr.responseJSON.uuid,
                            errorLookup: xhr.responseJSON.backendErrorCode
                        }
                        : {}
                )
            );
        }
    });
});
<commit_msg>[FIND-1158] Update tooltip text on change<commit_after>/*
 * Copyright 2015-2017 Hewlett Packard Enterprise Development Company, L.P.
 * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
 */

define([
    'underscore',
    'jquery',
    'backbone',
    'find/app/util/topic-map-view',
    'find/app/model/entity-collection',
    'i18n!find/nls/bundle',
    'find/app/util/generate-error-support-message',
    'text!find/templates/app/page/search/results/entity-topic-map-view.html',
    'text!find/templates/app/page/loading-spinner.html',
    'iCheck'
], function(_, $, Backbone, TopicMapView, EntityCollection, i18n, generateErrorHtml,
            template, loadingTemplate) {
    'use strict';

    const loadingHtml = _.template(loadingTemplate)({i18n: i18n, large: true});

    /**
     * @readonly
     * @enum {String}
     */
    const ViewState = {
        LOADING: 'LOADING',
        ERROR: 'ERROR',
        EMPTY: 'EMPTY',
        MAP: 'MAP'
    };

    const SPEED_SLIDER_MIN = 50;
    const DEFAULT_MAX_RESULTS = 300;

    return Backbone.View.extend({
        template: _.template(template),

        events: {
            'change .speed-slider': function(e) {
                const $target = $(e.target);
                const value = $target.val();
                $target.attr('data-original-title', value);
                this.$('.tooltip-inner').text(value);
                $target.blur();
                this.model.set('maxResults', value);
            },
            'input .speed-slider': function(e) {
                const $target = $(e.target);
                const value = $target.val();
                this.$('.tooltip-inner').text(value);
            }
        },

        initialize: function(options) {
            this.queryState = options.queryState;

            this.entityCollection = new EntityCollection([], {
                getSelectedRelatedConcepts: function() {
                    // Comparison topic view does not have queryState
                    return this.queryState
                        ? _.flatten(this.queryState.conceptGroups.pluck('concepts'))
                        : [];
                }.bind(this)
            });

            this.debouncedFetchRelatedConcepts = _.debounce(this.entityCollection.fetchRelatedConcepts.bind(this.entityCollection), 500);

            this.queryModel = options.queryModel;
            this.type = options.type;
            this.showSlider = _.isUndefined(options.showSlider) || options.showSlider;
            this.fixedHeight = _.isUndefined(options.fixedHeight) || options.fixedHeight;

            this.topicMap = new TopicMapView({
                clickHandler: options.clickHandler
            });

            this.errorTemplate = generateErrorHtml({messageToUser: i18n['search.topicMap.error']});

            this.viewModel = new Backbone.Model({
                state: ViewState.EMPTY
            });

            const configuredMaxResults = options.configuration && _.isNumber(options.configuration.topicMapMaxResults)
                ? Math.max(options.configuration.topicMapMaxResults, SPEED_SLIDER_MIN + 1)
                : 0;

            const constructorMaxResults = _.isNumber(options.maxResults)
                ? Math.max(options.maxResults, SPEED_SLIDER_MIN + 1)
                : 0;

            this.maximumMaxResults = constructorMaxResults || configuredMaxResults || DEFAULT_MAX_RESULTS;

            this.model = new Backbone.Model({
                maxCount: 10,
                maxResults: constructorMaxResults || Math.min(this.maximumMaxResults, DEFAULT_MAX_RESULTS)
            });

            this.listenTo(this.model, 'change:maxResults', function() {
                this.debouncedFetchRelatedConcepts(this.queryModel, this.type, this.model.get('maxResults'));
            });

            this.listenTo(this.queryModel, 'change', function() {
                this.entityCollection.fetchRelatedConcepts(this.queryModel, this.type, this.model.get('maxResults'));
            });

            this.listenTo(this.entityCollection, 'sync', function() {
                this.viewModel.set('state', this.entityCollection.isEmpty()
                    ? ViewState.EMPTY
                    : ViewState.MAP);
                this.updateTopicMapData();
                this.update();
            });

            this.listenTo(this.entityCollection, 'request', function() {
                this.viewModel.set('state', ViewState.LOADING);
            });

            this.listenTo(this.entityCollection, 'error', function(collection, xhr) {
                this.generateErrorMessage(xhr);
                // Status of zero means the request has been aborted
                this.viewModel.set('state', xhr.status === 0
                    ? ViewState.LOADING
                    : ViewState.ERROR);
            });

            this.listenTo(this.viewModel, 'change', this.updateViewState);
            this.entityCollection.fetchRelatedConcepts(this.queryModel, this.type, this.model.get('maxResults'));
        },

        render: function() {
            if(this.showSlider) {
                this.$('.speed-slider').tooltip('destroy');
            }

            this.$el.html(this.template({
                cid: this.cid,
                errorTemplate: this.errorTemplate,
                i18n: i18n,
                loadingHtml: loadingHtml,
                showSlider: this.showSlider,
                fixedHeight: this.fixedHeight
            }));

            this.$error = this.$('.entity-topic-map-error');
            this.$empty = this.$('.entity-topic-map-empty');
            this.$loading = this.$('.entity-topic-map-loading');

            if(this.showSlider) {
                const maxResults = this.model.get('maxResults');
                this.$('.speed-slider')
                    .attr({
                        min: SPEED_SLIDER_MIN,
                        max: this.maximumMaxResults,
                        step: 1
                    })
                    .val(maxResults)
                    .tooltip({
                        title: maxResults,
                        placement: 'top'
                    });
            }

            this.topicMap.setElement(this.$('.entity-topic-map')).render();
            this.update();
            this.updateViewState();
        },

        remove: function() {
            if(this.showSlider) {
                this.$('.speed-slider').tooltip('destroy');
            }

            Backbone.View.prototype.remove.call(this);
        },

        update: function() {
            this.topicMap.draw();
        },

        updateTopicMapData: function() {
            this.topicMap.setData(this.entityCollection.processDataForTopicMap());
        },

        updateViewState: function() {
            const state = this.viewModel.get('state');
            this.topicMap.$el.toggleClass('hide', state !== ViewState.MAP);

            if(this.$error) {
                if(state === ViewState.ERROR) {
                    this.$error.html(this.errorTemplate);
                }
                this.$error.toggleClass('hide', state !== ViewState.ERROR);
            }

            if(this.$empty) {
                this.$empty.toggleClass('hide', state !== ViewState.EMPTY);
            }

            if(this.$loading) {
                this.$loading.toggleClass('hide', state !== ViewState.LOADING);
            }
        },

        generateErrorMessage: function(xhr) {
            this.errorTemplate = generateErrorHtml(
                _.extend(
                    {messageToUser: i18n['search.topicMap.error']},
                    xhr.responseJSON
                        ? {
                            errorDetails: xhr.responseJSON.message,
                            errorDetailsFallback: xhr.responseJSON.uuid,
                            errorUUID: xhr.responseJSON.uuid,
                            errorLookup: xhr.responseJSON.backendErrorCode
                        }
                        : {}
                )
            );
        }
    });
});
<|endoftext|>