<commit_before>/* eslint-env node */
/* globals atom:false */

const { CompositeDisposable } = require('atom');
const { MessagePanelView, PlainMessageView } = require('atom-message-panel');
const path = require('path');
const child_process = require('child_process');

module.exports = {
  messages: null,
  subscriptions: null,

  // configuration options
  config: {
    // path to pawncc executable
    pawnExecutablePath: {
      title: 'Path to pawncc executable',
      type: 'string',
      default: 'C:\\pawn\\pawncc.exe',
    },

    // list of pawncc options (passed before filename)
    pawnOptions: {
      title: 'Pawn options',
      type: 'array',
      default: ['-d1', '-(', '-;'],
      items: {
        type: 'string',
      },
    },

    // should jump to error?
    shouldJumpToError: {
      title: 'Jump to first error?',
      type: 'boolean',
      default: false,
    },
  },

  activate(state) {
    // activate package
    this.subscriptions = new CompositeDisposable();
    this.subscriptions.add(atom.commands.add('atom-workspace', {
      'pawn-build:build': () => this.build(),
    }));
  },

  deactivate() {
    // deactivate package
    if(this.messages != null)
      this.messages.detatch();
    this.subscriptions.dispose();
    this.pawnBuildView.destroy();
  },

  serialize() { },

  createOutputPanel() {
    // create output panel (or return if already exists)
    if(this.message == null) {
      this.messages = new MessagePanelView({
        title: 'Pawn output',
      });
    }

    this.messages.attach();
    this.messages.clear();
    return this.messages;
  },

  build() {
    // get current editor
    const editor = atom.workspace.getActiveTextEditor();

    // get path of currently open file
    const filepath = editor != null ? editor.getPath() : null;

    // only continue if file is a .pwn file
    if(filepath == null || path.extname(filepath) !== '.pwn')
      return;

    // also save the current file, so the current buffer is included
    editor.save();

    // prepare command and arguments from package settigns
    const cmd = atom.config.get('pawn-build.pawnExecutablePath');
    const args = atom.config.get('pawn-build.pawnOptions');
    args.push(path.basename, filepath);

    // run pawncc command
    const process = child_process.spawn(cmd, args, {
      cwd: path.dirname(filepath), // run in the current file's directory
    });

    const procout = [];      // store output from pawncc
    let hasError = false;  // store error status

    // read output from pawncc
    process.stdout.on('data', (data) =>
      procout.push(data.toString())
    );
    process.stderr.on('data', (data) =>
      procout.push(data.toString())
    );

    process.on('error', (error) => {
      // show message if pawncc failed to run (for instance due to invalid path)
      hasError = true;
      const output = this.createOutputPanel();
      output.add(new PlainMessageView({
        message: 'Could not run pawncc: ' + error.message,
      }));
    });

    process.on('close', (exitCode, signal) => {
      if(hasError) return;

      const data = procout.join('');
      const output = this.createOutputPanel();

      if(data) {
        // jump to the first error line
        if(atom.config.get('build-pawn.shouldJumpToError')) {
          const match = data.match(/\(([1-9][0-9]*)\)/);
          if(match != null) {
            const row = parseInt(match[1]) - 1;
            if(row < editor.getLineCount())
              editor.setCursorBufferPosition([row, 0]);
          }
        }

        // show output of pawncc
        const lines = data.split('\n');
        for(let line of lines) {
          output.add(new PlainMessageView({
            message: line,
          }));
        }
      } else {
        // show error message if pawncc returned no output (unknown error)
        output.add(new PlainMessageView({
          message: 'Could not run pawncc: Unknown error (' + exitCode + ')',
        }));
      }
    });
  },
};
<commit_msg>stupid mistkae<commit_after>/* eslint-env node */
/* globals atom:false */

const { CompositeDisposable } = require('atom');
const { MessagePanelView, PlainMessageView } = require('atom-message-panel');
const path = require('path');
const child_process = require('child_process');

module.exports = {
  messages: null,
  subscriptions: null,

  // configuration options
  config: {
    // path to pawncc executable
    pawnExecutablePath: {
      title: 'Path to pawncc executable',
      type: 'string',
      default: 'C:\\pawn\\pawncc.exe',
    },

    // list of pawncc options (passed before filename)
    pawnOptions: {
      title: 'Pawn options',
      type: 'array',
      default: ['-d1', '-(', '-;'],
      items: {
        type: 'string',
      },
    },

    // should jump to error?
    shouldJumpToError: {
      title: 'Jump to first error?',
      type: 'boolean',
      default: false,
    },
  },

  activate(state) {
    // activate package
    this.subscriptions = new CompositeDisposable();
    this.subscriptions.add(atom.commands.add('atom-workspace', {
      'pawn-build:build': () => this.build(),
    }));
  },

  deactivate() {
    // deactivate package
    if(this.messages != null)
      this.messages.detatch();
    this.subscriptions.dispose();
    this.pawnBuildView.destroy();
  },

  serialize() { },

  createOutputPanel() {
    // create output panel (or return if already exists)
    if(this.messages == null) {
      this.messages = new MessagePanelView({
        title: 'Pawn output',
      });
    }

    this.messages.attach();
    this.messages.clear();
    return this.messages;
  },

  build() {
    // get current editor
    const editor = atom.workspace.getActiveTextEditor();

    // get path of currently open file
    const filepath = editor != null ? editor.getPath() : null;

    // only continue if file is a .pwn file
    if(filepath == null || path.extname(filepath) !== '.pwn')
      return;

    // also save the current file, so the current buffer is included
    editor.save();

    // prepare command and arguments from package settigns
    const cmd = atom.config.get('pawn-build.pawnExecutablePath');
    const args = atom.config.get('pawn-build.pawnOptions');
    args.push(path.basename, filepath);

    // run pawncc command
    const process = child_process.spawn(cmd, args, {
      cwd: path.dirname(filepath), // run in the current file's directory
    });

    const procout = [];      // store output from pawncc
    let hasError = false;  // store error status

    // read output from pawncc
    process.stdout.on('data', (data) =>
      procout.push(data.toString())
    );
    process.stderr.on('data', (data) =>
      procout.push(data.toString())
    );

    process.on('error', (error) => {
      // show message if pawncc failed to run (for instance due to invalid path)
      hasError = true;
      const output = this.createOutputPanel();
      output.add(new PlainMessageView({
        message: 'Could not run pawncc: ' + error.message,
      }));
    });

    process.on('close', (exitCode, signal) => {
      if(hasError) return;

      const data = procout.join('');
      const output = this.createOutputPanel();

      if(data) {
        // jump to the first error line
        if(atom.config.get('build-pawn.shouldJumpToError')) {
          const match = data.match(/\(([1-9][0-9]*)\)/);
          if(match != null) {
            const row = parseInt(match[1]) - 1;
            if(row < editor.getLineCount())
              editor.setCursorBufferPosition([row, 0]);
          }
        }

        // show output of pawncc
        const lines = data.split('\n');
        for(let line of lines) {
          output.add(new PlainMessageView({
            message: line,
          }));
        }
      } else {
        // show error message if pawncc returned no output (unknown error)
        output.add(new PlainMessageView({
          message: 'Could not run pawncc: Unknown error (' + exitCode + ')',
        }));
      }
    });
  },
};
<|endoftext|>


<commit_before>'use strict';

var request = require('request'),
  cheerio = require('cheerio');

var Projection = function() {
  this.GOOGLE_ENDPOINT = 'http://www.google.com/movies';
};

// Find showtimes of nearby theaters
Projection.prototype.findTheaters = function(near, callback) {
  var err,
  	result = {};

  var url = this.GOOGLE_ENDPOINT + '?near=' + near;

  request(url, function (error, response, body) {
	  if (error) {
	  	callback(error);
	  	return;
	  }

	  if (response.statusCode !== 200) {
	  	callback(response.statusCode);
	  	return;
	  }

		var $ = cheerio.load(body);
		var theaters = [];

		$('.theater').each(function(i, t) {
			t = $(t);

			var theater = {
				name: t.find('.desc .name').text(),
				address: t.find('.desc .info').text().split(' - ')[0].trim(),
				phone: t.find('.desc .info').text().split(' - ')[1].trim(),
				note: t.find('.desc .closure').text(), //status
				movies: []
			};

			// Google movie info format : Duration - Rating - Genre - Trailer - IMDB
			t.find('.showtimes .movie').each(function(j, m) {
				m = $(m);
				var infos = m.find('.info').text().split(' - ');

				var cursor = 0,
					duration = null,
					rating 	 = null,
					genre 	 = null,
					trailer  = null;

				if (infos[cursor].match(/(1|2|hr|min)/)){
					duration = infos[cursor].trim();
					cursor++;
				}

				if (infos[cursor].match(/(G|\+|13|16|18)/)){
					rating = infos[cursor].trim();
					cursor++;
				}

				if (infos[cursor].match(/(^\D*$)/)){
					genre = infos[cursor].trim();
					cursor++;
				}

				if (m.find('.info a').attr('href') && (m.find('.info a').attr('href').match(/(youtube|vimeo|daily)/))) {
					trailer = m.find('.info a').attr('href').replace('/url?q=', '').trim();
				}

				var showtimes = m.find('.times').text().split(' ');
				showtimes = showtimes.map(function(s) {
					return s.trim(); // TODO new Date (w/ am/pm)
					//return new Date(s);
				});

				//console.log(infos);
				var movie = {
					title: m.find('.name').text(),
					duration: duration,
					rating: rating,
					genre: genre,
					trailer: trailer,
					showtimes: showtimes
				};

				theater.movies.push(movie);
			});

			theaters.push(theater);
		});

		callback(err, theaters);
	})

};

// q=movie+name
// Find a movie showtimes in nearby theaters
Projection.prototype.findMovie = function(callback) {
  // TODO
};

// var p = new Projection();
// p.findTheaters('Montreal', function(err, theaters) {
// 	console.log(theaters[2].movies[0	]);
// });

module.exports = Projection;<commit_msg>First draft of movie scraping<commit_after>'use strict';

var request = require('request'),
  cheerio = require('cheerio');

var Projection = function() {
  this.GOOGLE_ENDPOINT = 'http://www.google.com/movies';
};

// Find showtimes of nearby theaters
Projection.prototype.findTheaters = function(near, callback) {
  var url = this.GOOGLE_ENDPOINT + '?near=' + near;

  request(url, function (error, response, body) {
	  if (error) {
	  	callback(error);
	  	return;
	  }

	  if (response.statusCode !== 200) {
	  	callback(response.statusCode);
	  	return;
	  }

		var $ = cheerio.load(body);

		// List of theaters returned
		var theaters = [];

		$('.theater').each(function(i, t) {
			t = $(t);

			var theater = {
				name: t.find('.desc .name').text(),
				address: t.find('.desc .info').text().split(' - ')[0].trim(),
				phone: t.find('.desc .info').text().split(' - ')[1].trim(),
				note: t.find('.desc .closure').text(), //status
				movies: []
			};

			// Google movie info format : Duration - Rating - Genre - Trailer - IMDB
			t.find('.showtimes .movie').each(function(j, m) {
				m = $(m);

				var infos = formatInfos(m.find('.info').text().split(' - '));
				var showtimes = formatShowtimes(m.find('.times').text().split(' '));

				var trailer = null;
				if (m.find('.info a').attr('href') && (m.find('.info a').attr('href').match(/(youtube|vimeo|daily)/))) {
					trailer = m.find('.info a').attr('href').replace('/url?q=', '').trim();
				}

				//console.log(infos);
				var movie = {
					title: m.find('.name').text(),
					duration: infos.duration,
					rating: infos.rating,
					genre: infos.genre,
					trailer: trailer,
					showtimes: showtimes
				};

				theater.movies.push(movie);
			});

			theaters.push(theater);
		});

		callback(null, theaters);
	})

};

// q=movie+name
// Find a movie showtimes in nearby theaters
Projection.prototype.findMovie = function(near, movieName, callback) {
  var url = this.GOOGLE_ENDPOINT + '?near=' + near + '&q=' + "Furious+7";

  request(url, function (error, response, body) {
	  if (error) {
	  	callback(error);
	  	return;
	  }

	  if (response.statusCode !== 200) {
	  	callback(response.statusCode);
	  	return;
	  }

		var $ = cheerio.load(body);

		var m = $('.movie');

		var content = m.find('.desc .info').not('.info.links').html().split('<br>');

		var persons = $(content[1]).text();
		console.log(persons);
		var categories = persons.split(' - ');
		var director = categories[0].replace('Director: ', '');
		var cast = categories[1].replace('Cast: ', '').split(', ');

		var infos = formatInfos(content[0].split(' - '));

		var trailer = null;
		if (m.find('.info a').attr('href') && (m.find('.info a').attr('href').match(/(youtube|vimeo|daily)/))) {
			trailer = m.find('.info a').attr('href').replace('/url?q=', '').trim();
		}

		var desc = m.find('span[itemprop="description"]').text() + m.find('#SynopsisSecond0').clone().children().remove().end().text().trim();

		var movie = {
			title: m.find('.desc h2').text(),
			desc: desc,
			director: director,
			cast: cast,
			duration: infos.duration,
			rating: infos.rating,
			genre: infos.genre,
			trailer: trailer,
			theaters: []
		};

		$('.showtimes .theater').each(function(i, t) {
			t = $(t);

			var theater = {
				name: t.find('.name').text(),
				address: t.find('.address').text(),
				showtimes: formatShowtimes(t.find('.times').text().split(' '))
			};

			movie.theaters.push(theater);
		});

		callback(null, movie);
	})

};

var formatInfos = function(infos) {
	var cursor = 0,
		duration = null,
		rating 	 = null,
		genre 	 = null;

	if (infos[cursor].match(/(1|2|hr|min)/)){
		duration = infos[cursor].trim();
		cursor++;
	}

	if (infos[cursor].match(/(G|\+|13|16|18)/)){
		rating = infos[cursor].trim();
		cursor++;
	}

	if (infos[cursor].match(/(^\D*$)/)){
		genre = infos[cursor].trim();
		cursor++;
	}

	return {
		duration: duration,
		rating: rating,
		genre: genre
	};
};

var formatShowtimes = function(showtimes) {
	// TODO what to do with dubbed?
	return showtimes.map(function(s) {
		return s.trim(); // TODO new Date (w/ am/pm)
		//return new Date(s);
	});
};

var p = new Projection();
// p.findTheaters('Montreal', function(err, theaters) {
// 	console.log(theaters[2].movies[0	]);
// });
p.findMovie('Montreal', 'Furious 7', function(err, movie) {
	console.log(movie);
});

module.exports = Projection;<|endoftext|>


<commit_before>'use strict';

var _ = require('underscore'),
  request = require('request'), // TODO use 'http' instead of request?
  cheerio = require('cheerio'),
  qs = require('querystring'),
  iconv = require('iconv-lite'),
  Cache = require('./cache');

function Projection() {
    if (!(this instanceof Projection))
        return new Projection()

    this.GOOGLE_ENDPOINT = 'http://www.google.com/movies';
    this._cache = new Cache(); // TODO problem with timeouts not dying at end of program
}

// Public
// Find showtimes of nearby theaters
Projection.prototype.findTheaters = function(near, options, callback) {
  var self = this;

  var url = this.GOOGLE_ENDPOINT + toQS(options, near);

  var cached = self._cache.get(url);
  if (cached) {
    callback(null, cached);
    return;
  }

  requestGET(url, function(err, $) {
    if (err) {
      callback(err);
      return;
    }

    // List of theaters returned
    var theaters = [];

    $('.theater').each(function(i, t) {
      t = $(t);

      var theater = {
        name: t.find('.desc .name').text(),
        address: t.find('.desc .info').text().split(' - ')[0].trim(),
        phone: t.find('.desc .info').text().split(' - ')[1].trim(),
        note: t.find('.desc .closure').text(), //status
        movies: []
      };

      // Google movie info format : Duration - Rating - Genre - Trailer - IMDB
      t.find('.showtimes .movie').each(function(j, m) {
        m = $(m);

        var infos = formatInfos(m.find('.info').text().split(' - '));
        var showtimes = [];
        m.find('.times > span').each(function() {
          showtimes.push($(this).text().trim());
        });
        showtimes = formatShowtimes(showtimes);

        var trailer = null;
        if (m.find('.info a').attr('href') && (m.find('.info a').attr('href').match(/(youtube|vimeo|daily)/))) {
          trailer = m.find('.info a').attr('href').replace('/url?q=', '').trim();
        }

        //console.log(infos);
        var movie = {
          title: m.find('.name').text(),
          duration: infos.duration,
          rating: infos.rating,
          genre: infos.genre,
          trailer: trailer,
          showtimes: showtimes
        };

        theater.movies.push(movie);
      });

      theaters.push(theater);
    });

    self._cache.set(url, theaters);
    callback(null, theaters);
  });

};

// Public
// Find a movie showtimes in nearby theaters
Projection.prototype.findMovie = function(near, movie, options, callback) {
  var self = this;

  var url = self.GOOGLE_ENDPOINT + toQS(options, near, movie);

  var cached = self._cache.get(url);
  if (cached) {
    callback(null, cached);
    return;
  }

  requestGET(url, function(err, $) {
    if (err) {
      callback(err);
      return;
    }

    var m = $('.movie');

    var content = m.find('.desc .info').not('.info.links').html().split('
');

    var infos = formatInfos(content[0].split(' - '));

    var persons = $('
' + content[1] + '
');
    var director = persons.find('span[itemprop="director"]').text().trim();
    var cast = [];
    persons.find('span[itemprop="actors"]').each(function(gg, a) {
      cast.push($(a).text().trim());
    });

    var trailer = null;
    if (m.find('.info a').attr('href') && (m.find('.info a').attr('href').match(/(youtube|vimeo|daily)/))) {
      trailer = m.find('.info a').attr('href').replace('/url?q=', '').trim();
    }

    var desc = m.find('span[itemprop="description"]').text() + m.find('#SynopsisSecond0').clone().children().remove().end().text().trim();

    var movie = {
      title: m.find('.desc h2').text(),
      desc: desc,
      director: director,
      cast: cast,
      duration: infos.duration,
      rating: infos.rating,
      genre: infos.genre,
      trailer: trailer,
      theaters: []
    };

    var showtimes = [];
    m.find('.times > span').each(function() {
      showtimes.push($(this).text().trim());
    });
    showtimes = formatShowtimes(showtimes);

    $('.showtimes .theater').each(function(i, t) {
      t = $(t);

      var showtimes = [];
      m.find('.times > span').each(function() {
        showtimes.push($(this).text().trim());
      });

      var theater = {
        name: t.find('.name').text(),
        address: t.find('.address').text(),
        showtimes: formatShowtimes(showtimes)
      };

      movie.theaters.push(theater);
    });

    self._cache.set(url, movie);
    callback(null, movie);
  })

};

// Private
function requestGET(url, callback) {
  request.get({url: url, encoding: null}, function (err, res, body) {
    if (err) {
      callback(err);
      return;
    }
    if (res.statusCode !== 200) {
      callback(res.statusCode);
      return;
    }

    var regex = /(?:charset|encoding)\s*=\s*['"]? *([\w\-]+)/i;
    var encoding = regex.exec(res.headers['content-type'])[1];

    var $ = cheerio.load(iconv.decode(new Buffer(body), encoding));

    callback(null, $);
  });
};

// Private
function toQS(options, near, movie){
  var args = _.pick(options, 'hl', 'date');
  if (near)  args.near = near;
  if (movie) args.movie = movie;
  return '?' + qs.stringify(args);
};

// Private
function formatInfos(infos) {
  var cursor = 0,
    duration = null,
    rating   = null,
    genre    = null;

  if (infos[cursor].match(/(1|2|hr|min)/)){
    duration = infos[cursor].trim();
    cursor++;
  }

  if (infos[cursor].match(/(G|\+|13|16|18)/)){
    rating = infos[cursor].trim();
    cursor++;
  }

  if (infos[cursor].match(/(^\D*$)/)){
    genre = infos[cursor].trim();
    cursor++;
  }

  return {
    duration: duration,
    rating: rating,
    genre: genre
  };
};

// Private
function formatShowtimes(showtimes) {
  var today = new Date();
  var d = today; // TODO Consider today, tomorrow, etc.

  var middle = -1;
  for (var i = 0; i < showtimes.length; i++) {
    if (showtimes[i + 1]) {
      var curr = parseInt(showtimes[i].split(':')[0]);
      var next = parseInt(showtimes[i + 1].split(':')[0]);
      if (curr > next) {
        middle = i;
        //console.log(showtimes[i] + ' ' + showtimes[i + 1] + ' ' + middle);
        break;
      }
    }
  }

  return showtimes.map(function(s, i) {
    var hm = s.split(':');
    var date = new Date(d.getTime());

    var pm = true;
    if (i <= middle)
      pm = false;

    if (pm)
      date.setHours(parseInt(hm[0]) + 12);
    else
      date.setHours(hm[0]);

    date.setMinutes(hm[1]);
    date.setSeconds(0);

    return date;
  });
};

// var p = Projection(10);
// p.findTheaters('Montreal', { hl:'ru'}, function(err, theaters) {
//  console.log(theaters[9].movies[3]);
// });
// p.findMovie('Sherbrooke', 'Mad Max', { hl:'ru'}, function(err, movie) {
//  console.log(movie.theaters[0]);
// });

module.exports = Projection;<commit_msg>Fix critical date scraping bug<commit_after>'use strict';

var _ = require('underscore'),
  request = require('request'), // TODO use 'http' instead of request?
  cheerio = require('cheerio'),
  qs = require('querystring'),
  iconv = require('iconv-lite'),
  Cache = require('./cache');

function Projection() {
    if (!(this instanceof Projection))
        return new Projection()

    this.GOOGLE_ENDPOINT = 'http://www.google.com/movies';
    this._cache = new Cache(); // TODO problem with timeouts not dying at end of program
}

// Public
// Find showtimes of nearby theaters
Projection.prototype.findTheaters = function(near, options, callback) {
  var self = this;

  var url = this.GOOGLE_ENDPOINT + toQS(options, near);

  var cached = self._cache.get(url);
  if (cached) {
    callback(null, cached);
    return;
  }

  requestGET(url, function(err, $) {
    if (err) {
      callback(err);
      return;
    }

    // List of theaters returned
    var theaters = [];

    $('.theater').each(function(i, t) {
      t = $(t);

      var theater = {
        name: t.find('.desc .name').text(),
        address: t.find('.desc .info').text().split(' - ')[0].trim(),
        phone: t.find('.desc .info').text().split(' - ')[1].trim(),
        note: t.find('.desc .closure').text(), //status
        movies: []
      };

      // Google movie info format : Duration - Rating - Genre - Trailer - IMDB
      t.find('.showtimes .movie').each(function(j, m) {
        m = $(m);

        var infos = formatInfos(m.find('.info').text().split(' - '));
        var showtimes = [];
        m.find('.times > span').each(function() {
          showtimes.push($(this).text().trim());
        });
        showtimes = formatShowtimes(showtimes);

        var trailer = null;
        if (m.find('.info a').attr('href') && (m.find('.info a').attr('href').match(/(youtube|vimeo|daily)/))) {
          trailer = m.find('.info a').attr('href').replace('/url?q=', '').trim();
        }

        //console.log(infos);
        var movie = {
          title: m.find('.name').text(),
          duration: infos.duration,
          rating: infos.rating,
          genre: infos.genre,
          trailer: trailer,
          showtimes: showtimes
        };

        theater.movies.push(movie);
      });

      theaters.push(theater);
    });

    self._cache.set(url, theaters);
    callback(null, theaters);
  });

};

// Public
// Find a movie showtimes in nearby theaters
Projection.prototype.findMovie = function(near, movie, options, callback) {
  var self = this;

  var url = self.GOOGLE_ENDPOINT + toQS(options, near, movie);

  var cached = self._cache.get(url);
  if (cached) {
    callback(null, cached);
    return;
  }

  requestGET(url, function(err, $) {
    if (err) {
      callback(err);
      return;
    }

    var m = $('.movie');

    var content = m.find('.desc .info').not('.info.links').html().split('
');

    var infos = formatInfos(content[0].split(' - '));

    var persons = $('
' + content[1] + '
');
    var director = persons.find('span[itemprop="director"]').text().trim();
    var cast = [];
    persons.find('span[itemprop="actors"]').each(function(gg, a) {
      cast.push($(a).text().trim());
    });

    var trailer = null;
    if (m.find('.info a').attr('href') && (m.find('.info a').attr('href').match(/(youtube|vimeo|daily)/))) {
      trailer = m.find('.info a').attr('href').replace('/url?q=', '').trim();
    }

    var desc = m.find('span[itemprop="description"]').text() + m.find('#SynopsisSecond0').clone().children().remove().end().text().trim();

    var movie = {
      title: m.find('.desc h2').text(),
      desc: desc,
      director: director,
      cast: cast,
      duration: infos.duration,
      rating: infos.rating,
      genre: infos.genre,
      trailer: trailer,
      theaters: []
    };

    m.find('.showtimes .theater').each(function(i, t) {
      t = $(t);

      var showtimes = [];
      t.find('.times > span').each(function() {
        showtimes.push($(this).text().trim());
      });

      var theater = {
        name: t.find('.name').text(),
        address: t.find('.address').text(),
        showtimes: formatShowtimes(showtimes)
      };

      movie.theaters.push(theater);
    });

    self._cache.set(url, movie);
    callback(null, movie);
  })

};

// Private
function requestGET(url, callback) {
  request.get({url: url, encoding: null}, function (err, res, body) {
    if (err) {
      callback(err);
      return;
    }
    if (res.statusCode !== 200) {
      callback(res.statusCode);
      return;
    }

    var regex = /(?:charset|encoding)\s*=\s*['"]? *([\w\-]+)/i;
    var encoding = regex.exec(res.headers['content-type'])[1];

    var $ = cheerio.load(iconv.decode(new Buffer(body), encoding));

    callback(null, $);
  });
};

// Private
function toQS(options, near, movie){
  var args = _.pick(options, 'hl', 'date');
  if (near)  args.near = near;
  if (movie) args.movie = movie;
  return '?' + qs.stringify(args);
};

// Private
function formatInfos(infos) {
  var cursor = 0,
    duration = null,
    rating   = null,
    genre    = null;

  if (infos[cursor].match(/(1|2|hr|min)/)){
    duration = infos[cursor].trim();
    cursor++;
  }

  if (infos[cursor].match(/(G|\+|13|16|18)/)){
    rating = infos[cursor].trim();
    cursor++;
  }

  if (infos[cursor].match(/(^\D*$)/)){
    genre = infos[cursor].trim();
    cursor++;
  }

  return {
    duration: duration,
    rating: rating,
    genre: genre
  };
};

// Private
function formatShowtimes(showtimes) {
  var today = new Date();
  var d = today; // TODO Consider today, tomorrow, etc.

  var middle = -1;
  for (var i = 0; i < showtimes.length; i++) {
    if (showtimes[i + 1]) {
      var curr = parseInt(showtimes[i].split(':')[0]);
      var next = parseInt(showtimes[i + 1].split(':')[0]);
      if (curr > next) {
        middle = i;
        //console.log(showtimes[i] + ' ' + showtimes[i + 1] + ' ' + middle);
        break;
      }
    }
  }

  return showtimes.map(function(s, i) {
    var hm = s.split(':');
    var date = new Date(d.getTime());

    var pm = true;
    if (i <= middle)
      pm = false;

    if (pm)
      date.setHours(parseInt(hm[0]) + 12);
    else
      date.setHours(hm[0]);

    date.setMinutes(hm[1]);
    date.setSeconds(0);

    return date;
  });
};

// var p = Projection(10);
// p.findTheaters('Montreal', { hl:'ru'}, function(err, theaters) {
//  console.log(theaters[9].movies[3]);
// });
// p.findMovie('Montreal', 'Mad Max', { hl:'en'}, function(err, movie) {
//  console.log(movie.theaters[0]);
// });

module.exports = Projection;<|endoftext|>


<commit_before>/**
 * JS file for adding Leaflet map and layers.
 * Copyright (c) 2014 University of Oxford
 */

var LeafletMap = (function () {
    'use strict';

    // Initialise map at "map" div
    var map = L.map('map', {
        attributionControl: false,
        zoomControl: false,
        zoomsliderControl: true,
        maxBounds: [ [-89, -179], [89, 179] ],
        maxZoom: 10,
        minZoom: 3,
        animate: true
    }).fitWorld();

    // Add the simplified shapefile base layer with WMS GET request
    L.tileLayer.wms(wmsUrl, {
        layers: ['abraid:simplified_base_layer'],
        format: 'image/png',
        reuseTiles: true
    }).addTo(map);

    // Global colour variables
    var defaultColour = '#bb619b';      // Lighter pink/red
    var highlightColour = '#9e1e71';    // The chosen pink/red compatible with colourblindness
    var strokeColour = '#c478a9';       // Darker pink/red

    // Define default style for unselected points
    var diseaseOccurrenceLayerStyle = {
        stroke: false,
        fill: true,
        color: strokeColour,
        fillColor: defaultColour,
        fillOpacity: 0.8,
        radius: 8
    };

    // Change a point's colour on roll-over
    function highlightFeature() {
        this.setStyle({
            stroke: true,
            color: highlightColour
        });
    }

    // Return to default colour
    function resetHighlight() {
        this.setStyle({
            stroke: false,
            color: defaultColour
        });
    }

    // Change the point's colour and size when clicked
    function selectFeature(marker) {
        // First, reset the style of all other points on layer, so only one point is animated as selected at a time
        diseaseOccurrenceLayer.setStyle(diseaseOccurrenceLayerStyle);
        marker.setStyle({
            stroke: false,
            fillColor: highlightColour,
            radius: 11
        });
    }

    // Define a circle, instead of the default leaflet marker, with listeners for mouse events
    function diseaseOccurrenceLayerPoint(feature, latlng) {
        return L.circleMarker(latlng).on({
            mouseover: highlightFeature,
            mouseout: resetHighlight,
            click: function () {
                selectedPointViewModel.selectedPoint(feature);
                selectFeature(this);
            }
        });
    }

    // Define a cluster icon, so that nearby occurrences are grouped into one marker.
    // Text displays the number of points in the cluster. Styling in separate CSS file.
    function clusterLayerPoint(cluster) {
        return new L.DivIcon({
            html: '
' + cluster.getChildCount() + '
',
            className: 'marker-cluster',
            iconSize: new L.Point(40, 40)
        });
    }

    // Add disease occurrence points to map
    // First define styling options of the geoJson layer to which data (the feature collection) will be added later via AJAX request
    var diseaseOccurrenceLayer = L.geoJson([], {
        pointToLayer: diseaseOccurrenceLayerPoint,
        style: diseaseOccurrenceLayerStyle
    });

    // Add this geoJson layer to the styled markerClusterGroup layer, so that nearby occurrences are grouped
    var clusterLayer = L.markerClusterGroup({
        maxClusterRadius: 10,
        polygonOptions: {
            color: highlightColour,
            weight: 2,
            fillColor: defaultColour,
            dashArray: 3
        },
        spiderfyDistanceMultiplier: 2,
        iconCreateFunction: clusterLayerPoint
    }).addLayer(diseaseOccurrenceLayer).addTo(map);

    // Reset to default style when a point is unselected (by clicking anywhere else on the map)
    map.on('click', function () {
        selectedPointViewModel.clearSelectedPoint();
        diseaseOccurrenceLayer.setStyle(diseaseOccurrenceLayerStyle);
    });

    return {
        diseaseOccurrenceLayer : diseaseOccurrenceLayer,
        clusterLayer : clusterLayer,
        map : map
    };
}());
<commit_msg>Reset selected point fix<commit_after>/**
 * JS file for adding Leaflet map and layers.
 * Copyright (c) 2014 University of Oxford
 */

var LeafletMap = (function () {
    'use strict';

    // Initialise map at "map" div
    var map = L.map('map', {
        attributionControl: false,
        zoomControl: false,
        zoomsliderControl: true,
        maxBounds: [ [-89, -179], [89, 179] ],
        maxZoom: 10,
        minZoom: 3,
        animate: true
    }).fitWorld();

    // Add the simplified shapefile base layer with WMS GET request
    L.tileLayer.wms(wmsUrl, {
        layers: ['abraid:simplified_base_layer'],
        format: 'image/png',
        reuseTiles: true
    }).addTo(map);

    // Global colour variables
    var defaultColour = '#bb619b';      // Lighter pink/red
    var highlightColour = '#9e1e71';    // The chosen pink/red compatible with colourblindness
    var strokeColour = '#c478a9';       // Darker pink/red

    // Define default style for unselected points
    var diseaseOccurrenceLayerStyle = {
        stroke: false,
        fill: true,
        color: strokeColour,
        fillColor: defaultColour,
        fillOpacity: 0.8,
        radius: 8
    };

    // Change a point's colour on roll-over
    function highlightFeature() {
        this.setStyle({
            stroke: true,
            color: highlightColour
        });
    }

    // Return to default colour
    function resetHighlight() {
        this.setStyle({
            stroke: false,
            color: defaultColour
        });
    }

    // Change the point's colour and size when clicked
    function selectFeature(marker) {
        // First, reset the style of all other points on layer, so only one point is animated as selected at a time
        diseaseOccurrenceLayer.setStyle(diseaseOccurrenceLayerStyle);
        marker.setStyle({
            stroke: false,
            fillColor: highlightColour,
            radius: 11
        });
    }

    // Define a circle, instead of the default leaflet marker, with listeners for mouse events
    function diseaseOccurrenceLayerPoint(feature, latlng) {
        return L.circleMarker(latlng).on({
            mouseover: highlightFeature,
            mouseout: resetHighlight,
            click: function () {
                selectedPointViewModel.selectedPoint(feature);
                selectFeature(this);
            }
        });
    }

    // Define a cluster icon, so that nearby occurrences are grouped into one marker.
    // Text displays the number of points in the cluster. Styling in separate CSS file.
    function clusterLayerPoint(cluster) {
        return new L.DivIcon({
            html: '
' + cluster.getChildCount() + '
',
            className: 'marker-cluster',
            iconSize: new L.Point(40, 40)
        });
    }

    // Add disease occurrence points to map
    // First define styling options of the geoJson layer to which data (the feature collection) will be added later via AJAX request
    var diseaseOccurrenceLayer = L.geoJson([], {
        pointToLayer: diseaseOccurrenceLayerPoint,
        style: diseaseOccurrenceLayerStyle
    });

    // Add this geoJson layer to the styled markerClusterGroup layer, so that nearby occurrences are grouped
    var clusterLayer = L.markerClusterGroup({
        maxClusterRadius: 10,
        polygonOptions: {
            color: highlightColour,
            weight: 2,
            fillColor: defaultColour,
            dashArray: 3
        },
        spiderfyDistanceMultiplier: 2,
        iconCreateFunction: clusterLayerPoint
    }).addLayer(diseaseOccurrenceLayer).addTo(map);

    // Reset to default style when a point is unselected (by clicking anywhere else on the map)
    map.on('click', function () { resetSelectedPoint() });
    clusterLayer.on('clusterclick', function () { resetSelectedPoint() });

    function resetSelectedPoint() {
        selectedPointViewModel.clearSelectedPoint();
        diseaseOccurrenceLayer.setStyle(diseaseOccurrenceLayerStyle);
    }

    return {
        diseaseOccurrenceLayer : diseaseOccurrenceLayer,
        clusterLayer : clusterLayer,
        map : map
    };
}());
<|endoftext|>


<commit_before>"use strict";

describe("navigator", function() {
  var navigator;
  var state;

  var s1 = {
    id: "1",
    tokens: {
      '01-01': {
        string: 'x',
      },
      '01-02': {
        string: 'y'
      }
    }
  };

  var s3 = {
    id: "3",
    tokens: {
      '03-01': {
        string: 'g',
      },
      '03-02': {
        string: 'h'
      }
    }
  };

  var s5 = {
    id: "5",
    tokens: {
      '05-01': {
        string: 'a',
      },
      '05-02': {
        string: 'b'
      }
    }
  };

  var sentences = [s1, s3, s5];

  beforeEach(module("arethusa.core", function($provide) {
    $provide.value('configurator', arethusaMocks.configurator());
  }));

  beforeEach(inject(function(_navigator_, _state_) {
    navigator = _navigator_;
    state = _state_;
    state.initServices(); // executes navigator init
    navigator.reset();
  }));

  describe("this.addSentences()", function() {
    it('adds an array of sentencs to its internal containers', function() {
      navigator.addSentences(sentences);

      // an array of sentences
      expect(navigator.sentences.length).toEqual(3);
      expect(navigator.sentences[0]).toBe(s1);
      expect(navigator.sentences[1]).toBe(s3);
      expect(navigator.sentences[2]).toBe(s5);

      // an object of sentences
      expect(navigator.sentencesById['1']).toBe(s1);
      expect(navigator.sentencesById['5']).toBe(s5);
    });
  });

  describe('this.currentChunk()', function() {
    it('returns the tokens of the current sentence', function() {
      navigator.addSentences(sentences);
      expect(navigator.currentChunk()).toEqual(s1.tokens);
    });

    describe('with a larger chunk size', function() {
      it('returns more than one sentence', function() {
        var res = angular.extend({}, s1.tokens, s3.tokens);
        navigator.addSentences(sentences);

        navigator.changeChunkSize(2);

        expect(navigator.currentChunk()).toEqual(res);
      });
    });
  });

  describe('this.state()', function() {
    it('returns the state object', function() {
      expect(navigator.state()).toBe(state);
    });
  });

  describe('this.updateId()', function() {
    it('updates the internal status obj with the id of the current sentence', function() {
      navigator.addSentences(sentences);
      expect(navigator.status.currentIds).toEqual([]);
      navigator.updateId();
      expect(navigator.status.currentIds).toEqual(['1']);
    });

    it('determines if a next and/or previous sentence is available', function() {
      navigator.addSentences(sentences);
      navigator.updateId();
      expect(navigator.status.hasNext).toBeTruthy();
      expect(navigator.status.hasPrev).toBeFalsy();

      navigator.nextChunk(); // calls updateId on its own
      expect(navigator.status.hasNext).toBeTruthy();
      expect(navigator.status.hasPrev).toBeTruthy();

      navigator.nextChunk();
      expect(navigator.status.hasNext).toBeFalsy();
      expect(navigator.status.hasPrev).toBeTruthy();
    });

    it('updates the status object with the current chunk position', function() {
      navigator.addSentences(sentences);
      navigator.updateId();
      expect(navigator.status.currentPos).toEqual(0);
      navigator.nextChunk();
      expect(navigator.status.currentPos).toEqual(1);
    });
  });

  describe('this.sentenceToString()', function() {
    it('takes a sentence object and returns it as a string', function() {
      var str1 = navigator.sentenceToString(s1);
      var str5 = navigator.sentenceToString(s5);

      expect(str1).toEqual('x y');
      expect(str5).toEqual('a b');
    });
  });

  describe('this.reset()', function() {
    it('flashes all internal state of the navigator', function() {
      navigator.addSentences(sentences);
      navigator.updateId();
      expect(navigator.status.currentIds).toEqual(['1']);
      expect(navigator.sentences.length).toEqual(3);
      expect(navigator.sentencesById).not.toEqual({});

      navigator.reset();
      expect(navigator.status.currentId).toBeUndefined();
      expect(navigator.sentences.length).toEqual(0);
      expect(navigator.sentencesById).toEqual({});
    });
  });

  describe('this.nextChunk()', function() {
    it('moves to the next sentence - mind how ids can be non-sequential!', function() {
      navigator.addSentences(sentences);
      expect(navigator.currentChunk()).toEqual(s1.tokens);

      navigator.nextChunk();
      expect(navigator.currentChunk()).toEqual(s3.tokens);
    });

    it('updates the state object', function() {
      navigator.addSentences(sentences);
      navigator.nextChunk();
      expect(state.tokens).toBe(navigator.currentChunk());
    });

    it('does nothing when there is no next sentence', function() {
      navigator.addSentences(sentences);
      navigator.goToLast();
      expect(navigator.currentChunk()).toEqual(s5.tokens);
      navigator.nextChunk();
      expect(navigator.currentChunk()).toEqual(s5.tokens);
    });

    describe('with larger chunk sizes', function() {
      it('moves in chunk sizes', function() {
        navigator.addSentences(sentences.concat(sentences));
        navigator.changeChunkSize(2);
        expect(navigator.status.currentPos).toEqual(0);
        navigator.nextChunk();
        expect(navigator.status.currentPos).toEqual(2);
      });
    });
  });

  describe('this.prevChunk()', function() {
    it('moves to the previous sentence - ids can be non-sequential', function() {
      navigator.addSentences(sentences);
      navigator.nextChunk();
      navigator.nextChunk();
      expect(navigator.currentChunk()).toEqual(s5.tokens);

      navigator.prevChunk();
      expect(navigator.currentChunk()).toEqual(s3.tokens);
    });

    it('updates the state object', function() {
      navigator.addSentences(sentences);
      navigator.nextChunk();
      navigator.prevChunk();
      expect(state.tokens).toEqual(navigator.currentChunk());
    });

    it('does nothing when there is no previous sentence', function() {
      navigator.addSentences(sentences);
      expect(navigator.currentChunk()).toEqual(s1.tokens);
      navigator.prevChunk();
      expect(navigator.currentChunk()).toEqual(s1.tokens);
    });

    describe('with larger chunk sizes', function() {
      it('moves in chunk sizes', function() {
        navigator.addSentences(sentences.concat(sentences));
        navigator.changeChunkSize(2);
        expect(navigator.status.currentPos).toEqual(0);
        navigator.nextChunk();
        navigator.nextChunk();
        expect(navigator.status.currentPos).toEqual(4);
        navigator.prevChunk();
        expect(navigator.status.currentPos).toEqual(2);
      });
    });
  });

  describe('this.goToLast()', function() {
    it('goes to the last element in the sentences array', function() {
      navigator.addSentences(sentences);

      navigator.goToLast();
      expect(navigator.currentChunk()).toEqual(s5.tokens);
    });

    it('updates the state object', function() {
      navigator.addSentences(sentences);
      navigator.goToLast();
      expect(state.tokens).toEqual(navigator.currentChunk());
    });
  });

  describe('this.goToFirst', function() {
    it('goes. to the first element in the sentence array', function() {
      navigator.addSentences(sentences);
      navigator.goToLast();
      expect(navigator.currentChunk()).toEqual(s5.tokens);

      navigator.goToFirst();
      expect(navigator.currentChunk()).toEqual(s1.tokens);
    });

    it('updates the state object', function() {
      navigator.addSentences(sentences);
      navigator.goToLast();
      navigator.goToFirst();
      expect(state.tokens).toEqual(navigator.currentChunk());
    });
  });

  describe('this.goTo()', function() {
    it('goes to a sentence identified by its id', function() {
      navigator.addSentences(sentences);

      navigator.goTo('3');
      expect(navigator.currentChunk()).toEqual(s3.tokens);

      navigator.goTo('5');
      expect(navigator.currentChunk()).toEqual(s5.tokens);
    });

    it('updates the state object', function() {
      navigator.addSentences(sentences);
      navigator.goTo('5');
      expect(state.tokens).toEqual(navigator.currentChunk());
    });

    it('returns true when the call succeeds', function() {
      navigator.addSentences(sentences);
      expect(navigator.goTo('5')).toBeTruthy();
    });
  });

  describe('this.goToByPosition', function() {
    it('goes to a sentence identified by its container position', function() {
      navigator.addSentences(sentences);
      navigator.goToByPosition(1);
      expect(navigator.currentChunk()).toEqual(s3.tokens);
    });
  });

  describe('this.hasNext()', function() {
    it('determines if a next sentence is available', function() {
      navigator.addSentences(sentences);

      expect(navigator.hasNext()).toBeTruthy();

      navigator.goTo('5');
      expect(navigator.hasNext()).toBeFalsy();
    });

    describe('with a larger chunk size', function() {
      it('determines if something next is available', function() {
        navigator.addSentences(sentences);
        navigator.changeChunkSize(3);

        expect(navigator.hasNext()).toBeFalsy();
      });
    });
  });

  describe('navigator.hasPrev()', function() {
    it('determines if a previous sentence is available', function() {
      navigator.addSentences(sentences);

      expect(navigator.hasPrev()).toBeFalsy();

      navigator.goTo('3');
      expect(navigator.hasPrev()).toBeTruthy();
    });
  });

  describe('this.switchView()', function() {
    it('switches between editor and list mode', function() {
      expect(navigator.listMode).toBeFalsy();
      navigator.switchView();
      expect(navigator.listMode).toBeTruthy();
      navigator.switchView();
      expect(navigator.listMode).toBeFalsy();
    });

    xit('toggles hiding of the editor and list DOM elements', function() {
    });
  });
});
<commit_msg>Add spec for navigator.addToken<commit_after>"use strict";

describe("navigator", function() {
  var navigator;
  var state;

  var s1 = {
    id: "1",
    tokens: {
      '01-01': {
        string: 'x',
        sentenceId: '1',
        id: '01-01'
      },
      '01-02': {
        string: 'y',
        sentenceId: '1',
        id: '01-02'
      }
    }
  };

  var s3 = {
    id: "3",
    tokens: {
      '03-01': {
        string: 'g',
        sentenceId: '3',
        id: '03-01'
      },
      '03-02': {
        string: 'h',
        sentenceId: '3',
        id: '03-02'
      }
    }
  };

  var s5 = {
    id: "5",
    tokens: {
      '05-01': {
        string: 'a',
        sentenceId: '5',
        id: '05-01'
      },
      '05-02': {
        string: 'b',
        sentenceId: '5',
        id: '05-02'
      }
    }
  };

  var sentences = [s1, s3, s5];

  beforeEach(module("arethusa.core", function($provide) {
    $provide.value('configurator', arethusaMocks.configurator());
  }));

  beforeEach(inject(function(_navigator_, _state_) {
    navigator = _navigator_;
    state = _state_;
    state.initServices(); // executes navigator init
    navigator.reset();
  }));

  describe("this.addSentences()", function() {
    it('adds an array of sentencs to its internal containers', function() {
      navigator.addSentences(sentences);

      // an array of sentences
      expect(navigator.sentences.length).toEqual(3);
      expect(navigator.sentences[0]).toBe(s1);
      expect(navigator.sentences[1]).toBe(s3);
      expect(navigator.sentences[2]).toBe(s5);

      // an object of sentences
      expect(navigator.sentencesById['1']).toBe(s1);
      expect(navigator.sentencesById['5']).toBe(s5);
    });
  });

  describe('this.currentChunk()', function() {
    it('returns the tokens of the current sentence', function() {
      navigator.addSentences(sentences);
      expect(navigator.currentChunk()).toEqual(s1.tokens);
    });

    describe('with a larger chunk size', function() {
      it('returns more than one sentence', function() {
        var res = angular.extend({}, s1.tokens, s3.tokens);
        navigator.addSentences(sentences);

        navigator.changeChunkSize(2);

        expect(navigator.currentChunk()).toEqual(res);
      });
    });
  });

  describe('this.state()', function() {
    it('returns the state object', function() {
      expect(navigator.state()).toBe(state);
    });
  });

  describe('this.updateId()', function() {
    it('updates the internal status obj with the id of the current sentence', function() {
      navigator.addSentences(sentences);
      expect(navigator.status.currentIds).toEqual([]);
      navigator.updateId();
      expect(navigator.status.currentIds).toEqual(['1']);
    });

    it('determines if a next and/or previous sentence is available', function() {
      navigator.addSentences(sentences);
      navigator.updateId();
      expect(navigator.status.hasNext).toBeTruthy();
      expect(navigator.status.hasPrev).toBeFalsy();

      navigator.nextChunk(); // calls updateId on its own
      expect(navigator.status.hasNext).toBeTruthy();
      expect(navigator.status.hasPrev).toBeTruthy();

      navigator.nextChunk();
      expect(navigator.status.hasNext).toBeFalsy();
      expect(navigator.status.hasPrev).toBeTruthy();
    });

    it('updates the status object with the current chunk position', function() {
      navigator.addSentences(sentences);
      navigator.updateId();
      expect(navigator.status.currentPos).toEqual(0);
      navigator.nextChunk();
      expect(navigator.status.currentPos).toEqual(1);
    });
  });

  describe('this.sentenceToString()', function() {
    it('takes a sentence object and returns it as a string', function() {
      var str1 = navigator.sentenceToString(s1);
      var str5 = navigator.sentenceToString(s5);

      expect(str1).toEqual('x y');
      expect(str5).toEqual('a b');
    });
  });

  describe('this.reset()', function() {
    it('flashes all internal state of the navigator', function() {
      navigator.addSentences(sentences);
      navigator.updateId();
      expect(navigator.status.currentIds).toEqual(['1']);
      expect(navigator.sentences.length).toEqual(3);
      expect(navigator.sentencesById).not.toEqual({});

      navigator.reset();
      expect(navigator.status.currentId).toBeUndefined();
      expect(navigator.sentences.length).toEqual(0);
      expect(navigator.sentencesById).toEqual({});
    });
  });

  describe('this.nextChunk()', function() {
    it('moves to the next sentence - mind how ids can be non-sequential!', function() {
      navigator.addSentences(sentences);
      expect(navigator.currentChunk()).toEqual(s1.tokens);

      navigator.nextChunk();
      expect(navigator.currentChunk()).toEqual(s3.tokens);
    });

    it('updates the state object', function() {
      navigator.addSentences(sentences);
      navigator.nextChunk();
      expect(state.tokens).toBe(navigator.currentChunk());
    });

    it('does nothing when there is no next sentence', function() {
      navigator.addSentences(sentences);
      navigator.goToLast();
      expect(navigator.currentChunk()).toEqual(s5.tokens);
      navigator.nextChunk();
      expect(navigator.currentChunk()).toEqual(s5.tokens);
    });

    describe('with larger chunk sizes', function() {
      it('moves in chunk sizes', function() {
        navigator.addSentences(sentences.concat(sentences));
        navigator.changeChunkSize(2);
        expect(navigator.status.currentPos).toEqual(0);
        navigator.nextChunk();
        expect(navigator.status.currentPos).toEqual(2);
      });
    });
  });

  describe('this.prevChunk()', function() {
    it('moves to the previous sentence - ids can be non-sequential', function() {
      navigator.addSentences(sentences);
      navigator.nextChunk();
      navigator.nextChunk();
      expect(navigator.currentChunk()).toEqual(s5.tokens);

      navigator.prevChunk();
      expect(navigator.currentChunk()).toEqual(s3.tokens);
    });

    it('updates the state object', function() {
      navigator.addSentences(sentences);
      navigator.nextChunk();
      navigator.prevChunk();
      expect(state.tokens).toEqual(navigator.currentChunk());
    });

    it('does nothing when there is no previous sentence', function() {
      navigator.addSentences(sentences);
      expect(navigator.currentChunk()).toEqual(s1.tokens);
      navigator.prevChunk();
      expect(navigator.currentChunk()).toEqual(s1.tokens);
    });

    describe('with larger chunk sizes', function() {
      it('moves in chunk sizes', function() {
        navigator.addSentences(sentences.concat(sentences));
        navigator.changeChunkSize(2);
        expect(navigator.status.currentPos).toEqual(0);
        navigator.nextChunk();
        navigator.nextChunk();
        expect(navigator.status.currentPos).toEqual(4);
        navigator.prevChunk();
        expect(navigator.status.currentPos).toEqual(2);
      });
    });
  });

  describe('this.goToLast()', function() {
    it('goes to the last element in the sentences array', function() {
      navigator.addSentences(sentences);

      navigator.goToLast();
      expect(navigator.currentChunk()).toEqual(s5.tokens);
    });

    it('updates the state object', function() {
      navigator.addSentences(sentences);
      navigator.goToLast();
      expect(state.tokens).toEqual(navigator.currentChunk());
    });
  });

  describe('this.goToFirst', function() {
    it('goes. to the first element in the sentence array', function() {
      navigator.addSentences(sentences);
      navigator.goToLast();
      expect(navigator.currentChunk()).toEqual(s5.tokens);

      navigator.goToFirst();
      expect(navigator.currentChunk()).toEqual(s1.tokens);
    });

    it('updates the state object', function() {
      navigator.addSentences(sentences);
      navigator.goToLast();
      navigator.goToFirst();
      expect(state.tokens).toEqual(navigator.currentChunk());
    });
  });

  describe('this.goTo()', function() {
    it('goes to a sentence identified by its id', function() {
      navigator.addSentences(sentences);

      navigator.goTo('3');
      expect(navigator.currentChunk()).toEqual(s3.tokens);

      navigator.goTo('5');
      expect(navigator.currentChunk()).toEqual(s5.tokens);
    });

    it('updates the state object', function() {
      navigator.addSentences(sentences);
      navigator.goTo('5');
      expect(state.tokens).toEqual(navigator.currentChunk());
    });

    it('returns true when the call succeeds', function() {
      navigator.addSentences(sentences);
      expect(navigator.goTo('5')).toBeTruthy();
    });
  });

  describe('this.goToByPosition', function() {
    it('goes to a sentence identified by its container position', function() {
      navigator.addSentences(sentences);
      navigator.goToByPosition(1);
      expect(navigator.currentChunk()).toEqual(s3.tokens);
    });
  });

  describe('this.hasNext()', function() {
    it('determines if a next sentence is available', function() {
      navigator.addSentences(sentences);

      expect(navigator.hasNext()).toBeTruthy();

      navigator.goTo('5');
      expect(navigator.hasNext()).toBeFalsy();
    });

    describe('with a larger chunk size', function() {
      it('determines if something next is available', function() {
        navigator.addSentences(sentences);
        navigator.changeChunkSize(3);

        expect(navigator.hasNext()).toBeFalsy();
      });
    });
  });

  describe('navigator.hasPrev()', function() {
    it('determines if a previous sentence is available', function() {
      navigator.addSentences(sentences);

      expect(navigator.hasPrev()).toBeFalsy();

      navigator.goTo('3');
      expect(navigator.hasPrev()).toBeTruthy();
    });
  });

  describe('navigator.addToken()', function() {
    it('adds tokens to the sentence stores', function() {
      navigator.addSentences(sentences);
      var token = { id: '01-03', sentenceId: '1'};
      var firstSentence = navigator.sentencesById['1'].tokens;

      expect(firstSentence['01-03']).toBeUndefined();

      navigator.addToken(token);
      expect(firstSentence['01-03']).toBeDefined();
    });
  });

  describe('navigator.removeToken()', function() {

  });

  describe('this.switchView()', function() {
    it('switches between editor and list mode', function() {
      expect(navigator.listMode).toBeFalsy();
      navigator.switchView();
      expect(navigator.listMode).toBeTruthy();
      navigator.switchView();
      expect(navigator.listMode).toBeFalsy();
    });

    xit('toggles hiding of the editor and list DOM elements', function() {
    });
  });
});
<|endoftext|>


<commit_before>'use strict';

const path = require('path');
const childProcess = require('child_process');
const fsp = require('./fs-promise');
const promiseify = require('./promiseify');
const exec = promiseify(childProcess.exec, childProcess);

const GET_REVISION = Symbol('GET_REVISION');
const GET_STORAGE = Symbol('GET_STORAGE');
const READ_FILE = Symbol('READ_FILE');

class Repository {

    /**
     * 仓库的变更观察者
     * @param   {string}    storagePath 版本记录文件地址
     * @param   {string}    type        仓库类型，支持 git 与 svn
     * @param   {string}    tableName   记录版本数据的字段前缀
     */
    constructor(storagePath, type = 'git', tableName = 'revision') {
        this.storagePath = storagePath;
        this.type = type;
        this.tableName = tableName;
        this.dirname = path.dirname(storagePath);
        this.cache = {};
        this.storage = this[GET_STORAGE]();
    }

    getOldCommitId(target) {
        return this.storage.then(storage => storage[this.tableName][target]);
    }

    getNewCommitId(target) {
        let commitId = this.cache[target];
        if (commitId) {
            return Promise.resolve(commitId);
        } else {
            return this[GET_REVISION](target).then(commitId => {
                this.cache[target] = commitId;
                return commitId;
            });
        }
    }

    /**
     * 对比版本更改
     * @param   {string} target   目标
     * @return {Promise}
     */
    watch(target) {
        return Promise.all([this.getNewCommitId(target), this.getOldCommitId(target)]);
    }

    /**
     * 保存新的 git commit 到文件
     */
    save() {
        // 读取最新本地存储再写入，避免产生意外覆盖数据
        return this[READ_FILE]().then(data => {

            // 合并本地数据写入
            Object.assign(data[this.tableName], this.cache);

            // 将绝对路径转换为相对路径存储
            Object.keys(data[this.tableName]).forEach(target => {
                if (path.isAbsolute(target)) {
                    let commitId = data[this.tableName][target];
                    let relativePath = path.relative(this.dirname, target);
                    data[this.tableName][relativePath] = commitId;
                    delete data[this.tableName][target];
                }
            });
            return data;
        }).then(data => {
            let content = JSON.stringify(data, null, 2);
            return fsp.writeFile(this.storagePath, content, 'utf8');
        });
    }

    // 获取处理后的本地缓存
    [GET_STORAGE]() {
        return this[READ_FILE]().then(data => {

            // 将相对路径转换为绝对路径进行内部运算
            Object.keys(data[this.tableName]).forEach(target => {
                if (!path.isAbsolute(target)) {
                    let commitId = data[this.tableName][target];
                    let resolvePath = path.resolve(this.dirname, target);
                    data[this.tableName][resolvePath] = commitId;
                    delete data[this.tableName][target];
                }
            });

            return data;
        });
    }

    // 读取原始本地存储文件
    [READ_FILE]() {
        return fsp.readFile(this.storagePath, 'utf8')
            .then(json => JSON.parse(json))
            .catch(() => Promise.resolve({}))
            .then(data => {
                if (typeof data[this.tableName] !== 'object') {
                    data[this.tableName] = {};
                }
                return data;
            });
    }

    // 获取目标的修订 ID
    [GET_REVISION](target) {
        let type = this.type;
        let cwd = path.dirname(target);
        let basename = path.basename(target);

        if (type === 'git') {
            let cmd = `git log --pretty=format:"%h" -1 ${basename}`;
            return exec(cmd, {
                cwd: cwd
            }).then(stdout => stdout.toString().trim());
        } else if (type === 'svn') {
            let cmd = `svn log --limit 1 --xml ${basename}`;
            return exec(cmd, {
                cwd: cwd
            }).then(stdout => {
                stdout = stdout.toString().trim();
                stdout = stdout.match(/revision="([^"]*?)"/)[1];
                return stdout;
            });
        } else {
            return Promise.reject(`does not support "${type}"`);
        }

    }

};

module.exports = Repository;<commit_msg>repository字段忽略大小写<commit_after>'use strict';

const path = require('path');
const childProcess = require('child_process');
const fsp = require('./fs-promise');
const promiseify = require('./promiseify');
const exec = promiseify(childProcess.exec, childProcess);

const GET_REVISION = Symbol('GET_REVISION');
const GET_STORAGE = Symbol('GET_STORAGE');
const READ_FILE = Symbol('READ_FILE');

class Repository {

    /**
     * 仓库的变更观察者
     * @param   {string}    storagePath 版本记录文件地址
     * @param   {string}    type        仓库类型，支持 git 与 svn
     * @param   {string}    tableName   记录版本数据的字段前缀
     */
    constructor(storagePath, type = 'git', tableName = 'revision') {
        this.storagePath = storagePath;
        this.type = type.toLowerCase();
        this.tableName = tableName;
        this.dirname = path.dirname(storagePath);
        this.cache = {};
        this.storage = this[GET_STORAGE]();
    }

    getOldCommitId(target) {
        return this.storage.then(storage => storage[this.tableName][target]);
    }

    getNewCommitId(target) {
        let commitId = this.cache[target];
        if (commitId) {
            return Promise.resolve(commitId);
        } else {
            return this[GET_REVISION](target).then(commitId => {
                this.cache[target] = commitId;
                return commitId;
            });
        }
    }

    /**
     * 对比版本更改
     * @param   {string} target   目标
     * @return {Promise}
     */
    watch(target) {
        return Promise.all([this.getNewCommitId(target), this.getOldCommitId(target)]);
    }

    /**
     * 保存新的 git commit 到文件
     */
    save() {
        // 读取最新本地存储再写入，避免产生意外覆盖数据
        return this[READ_FILE]().then(data => {

            // 合并本地数据写入
            Object.assign(data[this.tableName], this.cache);

            // 将绝对路径转换为相对路径存储
            Object.keys(data[this.tableName]).forEach(target => {
                if (path.isAbsolute(target)) {
                    let commitId = data[this.tableName][target];
                    let relativePath = path.relative(this.dirname, target);
                    data[this.tableName][relativePath] = commitId;
                    delete data[this.tableName][target];
                }
            });
            return data;
        }).then(data => {
            let content = JSON.stringify(data, null, 2);
            return fsp.writeFile(this.storagePath, content, 'utf8');
        });
    }

    // 获取处理后的本地缓存
    [GET_STORAGE]() {
        return this[READ_FILE]().then(data => {

            // 将相对路径转换为绝对路径进行内部运算
            Object.keys(data[this.tableName]).forEach(target => {
                if (!path.isAbsolute(target)) {
                    let commitId = data[this.tableName][target];
                    let resolvePath = path.resolve(this.dirname, target);
                    data[this.tableName][resolvePath] = commitId;
                    delete data[this.tableName][target];
                }
            });

            return data;
        });
    }

    // 读取原始本地存储文件
    [READ_FILE]() {
        return fsp.readFile(this.storagePath, 'utf8')
            .then(json => JSON.parse(json))
            .catch(() => Promise.resolve({}))
            .then(data => {
                if (typeof data[this.tableName] !== 'object') {
                    data[this.tableName] = {};
                }
                return data;
            });
    }

    // 获取目标的修订 ID
    [GET_REVISION](target) {
        let type = this.type;
        let cwd = path.dirname(target);
        let basename = path.basename(target);

        if (type === 'git') {
            let cmd = `git log --pretty=format:"%h" -1 ${basename}`;
            return exec(cmd, {
                cwd: cwd
            }).then(stdout => stdout.toString().trim());
        } else if (type === 'svn') {
            let cmd = `svn log --limit 1 --xml ${basename}`;
            return exec(cmd, {
                cwd: cwd
            }).then(stdout => {
                stdout = stdout.toString().trim();
                stdout = stdout.match(/revision="([^"]*?)"/)[1];
                return stdout;
            });
        } else {
            return Promise.reject(`does not support "${type}"`);
        }

    }

};

module.exports = Repository;<|endoftext|>


<commit_before>var couchdb = require('./couchdb'),
    settings = require('./settings'),
    cache = require('./cache'),
    utils = require('./utils'),
    async = require('../deps/async'),
    http = require('http'),
    https = require('https'),
    path = require('path'),
    url = require('url'),
    fs = require('fs');

exports.TMP_DIR = process.env.HOME + '/.kanso/tmp';

exports.readSettings = function (path, callback) {
    settings.load(path, function (err, cfg) {
        if (err) {
            return callback(err);
        }
        if (cfg.name === undefined) {
            return callback(
                new Error('kanso.json missing name property')
            );
        }
        if (cfg.version === undefined) {
            return callback(
                new Error('kanso.json missing version property')
            );
        }
        callback(null, cfg);
    });
};

exports.attachTar = function (doc, cfg, tfile, callback) {
    fs.readFile(tfile, function (err, content) {
        if (err) {
            return callback(err);
        }
        doc._attachments = doc._attachments || {};
        doc._attachments[cfg.name + '-' + cfg.version + '.tar.gz'] = {
            content_type: 'application/x-compressed-tar',
            data: content.toString('base64')
        };
        callback(null, doc);
    });
};

exports.updateDoc = function (doc, cfg, tfile, callback) {
    var now = new Date();

    doc.time.modified = utils.ISODateString(now)
    doc.time[cfg.version] = utils.ISODateString(now);

    doc.versions[cfg.version] = cfg;

    // only do this if latest?
    doc.name = cfg.name;
    doc.author = cfg.author;
    doc.website = cfg.website;
    doc.maintainers = cfg.maintainers;
    doc.description = cfg.description;

    exports.attachTar(doc, cfg, tfile, callback);
};

exports.createDoc = function (cfg, tfile, callback) {
    var now = new Date();
    var doc = {
        _id: cfg.name,
        name: cfg.name,
        type: 'package',
        versions: {},
        time: {
            created: utils.ISODateString(now)
        }
    };
    exports.updateDoc(doc, cfg, tfile, callback);
};

// just returns null if the document doesn't exist
exports.get = function (repository, name, callback) {
    var db = couchdb(repository);
    var id = couchdb.encode(name || '');
    db.client('GET', id, null, function (err, data, res) {
        res = res || {};
        if (res.statusCode !== 404 && err) {
            return callback(err);
        }
        callback(null, (res.statusCode === 200) ? data: null);
    });
};

exports.updateCache = function (cfg, path, callback) {
    cache.remove(cfg.name, cfg.version, function (err) {
        if (err) {
            return callback(err);
        }
        cache.add(cfg.name, cfg.version, path, callback);
    });
};

exports.publish = function (path, repository, /*optional*/options, callback) {
    if (!callback) {
        callback = options;
        options = {};
    }
    exports.readSettings(path, function (err, cfg) {
        if (err) {
            return callback(err);
        }
        async.parallel({
            get: async.apply(exports.get, repository, cfg.name),
            cache: async.apply(exports.updateCache, cfg, path)
        },
        function (err, results) {
            if (err) {
                return callback(err);
            }
            var curr = results.get;
            var tfile = results.cache[0];
            var dir = results.cache[1];

            var db = couchdb(repository);

            if (!curr) {
                return exports.createDoc(cfg, tfile, function (err, doc) {
                    db.save(cfg.name, doc, callback);
                });
            }
            else if (curr.versions && curr.versions[cfg.version]) {
                if (!options.force) {
                    return callback(
                        'Entry already exists for ' + cfg.name + ' ' +
                        cfg.version
                    );
                }
            }
            return exports.updateDoc(curr, cfg, tfile, function (err, doc) {
                db.save(cfg.name, doc, callback);
            });
        });
    });
};

exports.fetch = function (name, version, repository, callback) {
    // TODO: check cache
    var db = couchdb(repository);
    db.get(name, function (err, data) {
        if (err) {
            return callback(err);
        }
        if (version in data.versions) {
            var filename = name + '-' + version + '.tar.gz';
            var url = repository + '/' + name + '/' + filename;
            exports.download(url, function (err, tarfile) {
                if (err) {
                    return callback(err);
                }
                cache.moveTar(name, version, tarfile, callback);
            });
        }
        // TODO check tags
        else {
            callback(new Error(
                'No matching package for ' + name + '@' + version
            ));
        }
    });
};

exports.download = function (file, callback) {
    var target = exports.TMP_DIR + '/' + path.basename(file);
    var urlinfo = url.parse(file);
    var proto = (urlinfo.protocol === 'https:') ? https: http;

    var _cb = callback;
    callback = function (err) {
        if (err) {
            utils.rm('-rf', target, function (err) {
                if (err) {
                    // let the original error through, but still output this one
                    logger.error(err);
                }
                _cb.apply(this, arguments);
            });
            return;
        }
        _cb.apply(this, arguments);
    };

    utils.ensureDir(exports.TMP_DIR, function (err) {
        if (err) {
            return callback(err);
        }
        var request = proto.request({
            host: urlinfo.hostname,
            port: urlinfo.port,
            method: 'GET',
            path: urlinfo.pathname
        });
        request.on('response', function (response) {
            if (response.statusCode >= 300) {
                return callback(couchdb.statusCodeError(response.statusCode));
            }
            var outfile = fs.createWriteStream(target);
            response.on('data', function (chunk) {
                outfile.write(chunk);
            });
            response.on('end', function () {
                outfile.end();
                callback(null, target);
            });
        });
        request.end();
    });
};
<commit_msg>add 'latest' tag support to repository module<commit_after>var couchdb = require('./couchdb'),
    settings = require('./settings'),
    cache = require('./cache'),
    utils = require('./utils'),
    semver = require('../deps/node-semver/semver'),
    async = require('../deps/async'),
    http = require('http'),
    https = require('https'),
    path = require('path'),
    url = require('url'),
    fs = require('fs');

exports.TMP_DIR = process.env.HOME + '/.kanso/tmp';

exports.readSettings = function (path, callback) {
    settings.load(path, function (err, cfg) {
        if (err) {
            return callback(err);
        }
        if (cfg.name === undefined) {
            return callback(
                new Error('kanso.json missing name property')
            );
        }
        if (cfg.version === undefined) {
            return callback(
                new Error('kanso.json missing version property')
            );
        }
        callback(null, cfg);
    });
};

exports.attachTar = function (doc, cfg, tfile, callback) {
    fs.readFile(tfile, function (err, content) {
        if (err) {
            return callback(err);
        }
        doc._attachments = doc._attachments || {};
        doc._attachments[cfg.name + '-' + cfg.version + '.tar.gz'] = {
            content_type: 'application/x-compressed-tar',
            data: content.toString('base64')
        };
        callback(null, doc);
    });
};

exports.updateDoc = function (doc, cfg, tfile, callback) {
    var now = new Date();

    doc.time.modified = utils.ISODateString(now)
    doc.time[cfg.version] = utils.ISODateString(now);

    doc.versions[cfg.version] = cfg;

    var versions = Object.keys(doc.versions);
    // semver.rcompare not working here for some reason
    var highest = versions.sort(semver.compare).reverse()[0];

    if (highest === cfg.version) {
        doc.tags = doc.tags || {};
        doc.tags.latest = cfg.version;

        doc.name = cfg.name;
        doc.author = cfg.author;
        doc.website = cfg.website;
        doc.maintainers = cfg.maintainers;
        doc.description = cfg.description;
    }

    exports.attachTar(doc, cfg, tfile, callback);
};

exports.createDoc = function (cfg, tfile, callback) {
    var now = new Date();
    var doc = {
        _id: cfg.name,
        name: cfg.name,
        type: 'package',
        versions: {},
        time: {
            created: utils.ISODateString(now)
        }
    };
    exports.updateDoc(doc, cfg, tfile, callback);
};

// just returns null if the document doesn't exist
exports.get = function (repository, name, callback) {
    var db = couchdb(repository);
    var id = couchdb.encode(name || '');
    db.client('GET', id, null, function (err, data, res) {
        res = res || {};
        if (res.statusCode !== 404 && err) {
            return callback(err);
        }
        callback(null, (res.statusCode === 200) ? data: null);
    });
};

exports.updateCache = function (cfg, path, callback) {
    cache.remove(cfg.name, cfg.version, function (err) {
        if (err) {
            return callback(err);
        }
        cache.add(cfg.name, cfg.version, path, callback);
    });
};

exports.publish = function (path, repository, /*optional*/options, callback) {
    if (!callback) {
        callback = options;
        options = {};
    }
    exports.readSettings(path, function (err, cfg) {
        if (err) {
            return callback(err);
        }
        async.parallel({
            get: async.apply(exports.get, repository, cfg.name),
            cache: async.apply(exports.updateCache, cfg, path)
        },
        function (err, results) {
            if (err) {
                return callback(err);
            }
            var curr = results.get;
            var tfile = results.cache[0];
            var dir = results.cache[1];

            var db = couchdb(repository);

            if (!curr) {
                return exports.createDoc(cfg, tfile, function (err, doc) {
                    db.save(cfg.name, doc, callback);
                });
            }
            else if (curr.versions && curr.versions[cfg.version]) {
                if (!options.force) {
                    return callback(
                        'Entry already exists for ' + cfg.name + ' ' +
                        cfg.version
                    );
                }
            }
            return exports.updateDoc(curr, cfg, tfile, function (err, doc) {
                db.save(cfg.name, doc, callback);
            });
        });
    });
};

exports.fetch = function (name, version, repository, callback) {
    // TODO: check cache
    var db = couchdb(repository);
    db.get(name, function (err, data) {
        if (err) {
            return callback(err);
        }

        function getVersion (v) {
            var filename = name + '-' + v + '.tar.gz';
            var url = repository + '/' + name + '/' + filename;
            exports.download(url, function (err, tarfile) {
                if (err) {
                    return callback(err);
                }
                cache.moveTar(name, v, tarfile, callback);
            });
        }

        if (data.versions && version in data.versions) {
            getVersion(v);
        }
        else if (data.tags && version in data.tags) {
            getVersion(data.tags[version]);
        }
        else {
            callback(new Error(
                'No matching package for ' + name + '@' + version
            ));
        }
    });
};

exports.download = function (file, callback) {
    var target = exports.TMP_DIR + '/' + path.basename(file);
    var urlinfo = url.parse(file);
    var proto = (urlinfo.protocol === 'https:') ? https: http;

    var _cb = callback;
    callback = function (err) {
        if (err) {
            utils.rm('-rf', target, function (err) {
                if (err) {
                    // let the original error through, but still output this one
                    logger.error(err);
                }
                _cb.apply(this, arguments);
            });
            return;
        }
        _cb.apply(this, arguments);
    };

    utils.ensureDir(exports.TMP_DIR, function (err) {
        if (err) {
            return callback(err);
        }
        var request = proto.request({
            host: urlinfo.hostname,
            port: urlinfo.port,
            method: 'GET',
            path: urlinfo.pathname
        });
        request.on('response', function (response) {
            if (response.statusCode >= 300) {
                return callback(couchdb.statusCodeError(response.statusCode));
            }
            var outfile = fs.createWriteStream(target);
            response.on('data', function (chunk) {
                outfile.write(chunk);
            });
            response.on('end', function () {
                outfile.end();
                callback(null, target);
            });
        });
        request.end();
    });
};
<|endoftext|>


<commit_before>// General configuration data
//
// Read-only.

/* global define: false */
define({

	// Defines the default spelling of the twelve pitches based on D-minor.
	noteNames: ['C','C#','D','Eb','E','F','F#','G','G#','A','Bb','B'],

	// Maps the twleve pitch classes [0-11] onto [0-9,y,z]. We are substituting
	// y and z for 10 and 11 so that we can represent each pitch class with a
	// single character.
	pitchClasses: ["0","1","2","3","4","5","6","7","8","9","y","z"],

	// Defines the order of sharps.
	orderOfSharps: ["F","C","G","D","A","E","B"],

	// Maps each major/minor key to a default spelling of the twelve pitches
	// along with related key information. The keys are identified by unique
	// three character strings with the following format:
	//
	//		key identifier = [i|j] + [A-G] + [_|#|b]
	//
	//		[i|j] = minor key, major key
	//		[A-G] = note from A to G
	//		[_|#|b] = natural, sharp, flat
	//
	keyMap: {
		// SPECIAL CASE: this is a special case for "no key" (i.e. NULL value for the key)
		"h": {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 0,
			name: "No key",
			shortName: "",
			signature: ""
		},
		"iAb": {
			spelling: ["C","Db","D","Eb","Fb","F","Gb","G","Ab","Bbb","Bb","Cb"],
			pitchClass: 8,
			name: "Ab minor",
			shortName: "a",
			signature: "bbbbbbb",
		},
		"jCb": {
			spelling: ["C","Db","D","Eb","Fb","F","Gb","G","Ab","Bbb","Bb","Cb"],
			pitchClass: 11,
			name: "Cb major",
			shortName: "Cb",
			signature: "bbbbbbb",
		},
		"iEb": {
			spelling: ["C","Db","D","Eb","Fb","F","Gb","G","Ab","A","Bb","Cb"],
			pitchClass: 3,
			name: "Eb minor",
			shortName: "Eb",
			signature: "bbbbbb",
		},
		"jGb": {
			spelling: ["C","Db","D","Eb","Fb","F","Gb","G","Ab","A","Bb","Cb"],
			pitchClass: 6,
			name: "Gb major",
			shortName: "Gb",
			signature: "bbbbbb",
		},
		"iBb": {
			spelling: ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","Cb"],
			pitchClass: 10,
			name: "Bb minor",
			shortName: "bb",
			signature: "bbbbb",
		},
		"jDb": {
			spelling: ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","Cb"],
			pitchClass: 1,
			name: "Db major",
			shortName: "Db",
			signature: "bbbbb",
		},
		"iF_":  {
			spelling: ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","B"],
			pitchClass: 5,
			name: "F minor",
			shortName: "f",
			signature: "bbbb",
		},
		"jAb": {
			spelling: ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","B"],
			pitchClass: 8,
			name: "Ab major",
			shortName: "Ab",
			signature: "bbbb",
		},
		"iC_": {
			spelling: ["C","Db","D","Eb","E","F","F#","G","Ab","A","Bb","B"],
			pitchClass: 0,
			name: "C minor",
			shortName: "c",
			signature: "bbb",
		},
		"jEb": {
			spelling: ["C","Db","D","Eb","E","F","F#","G","Ab","A","Bb","B"],
			pitchClass: 3,
			name: "Eb major",
			shortName: "Eb",
			signature: "bbb",
		},
		"iG_":  {
			spelling: ["C","C#","D","Eb","E","F","F#","G","Ab","A","Bb","B"],
			pitchClass: 7,
			name: "G minor",
			shortName: "g",
			signature: "bb",
		},
		"jBb": {
			spelling: ["C","C#","D","Eb","E","F","F#","G","Ab","A","Bb","B"],
			pitchClass: 10,
			name: "Bb major",
			shortName: "Bb",
			signature: "bb",
		},
		"iD_":  {
			spelling: ["C","C#","D","Eb","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 2,
			name: "D minor",
			shortName: "d",
			signature: "b",
		},
		"jF_":  {
			spelling: ["C","C#","D","Eb","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 5,
			name: "F major",
			shortName: "F",
			signature: "b",
		},
		"iA_":  {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 9,
			name: "A minor",
			shortName: "a",
			signature: "",
		},
		"jC_":  {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 0,
			name: "C major",
			shortName: "C",
			signature: "",
		},
		"iE_":  {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"],
			pitchClass: 4,
			name: "E minor",
			shortName: "e",
			signature: "#",
		},
		"jG_": {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"],
			pitchClass: 7,
			name: "G major",
			shortName: "G",
			signature: "#",
		},
		"iB_": {
			spelling: ["C","C#","D","D#","E","E#","F#","G","G#","A","A#","B"],
			pitchClass: 11,
			name: "B minor",
			shortName: "b",
			signature: "##",
		},
		"jD_": {
			spelling: ["C","C#","D","D#","E","E#","F#","G","G#","A","A#","B"],
			pitchClass: 2,
			name: "D major",
			shortName: "D",
			signature: "##",
		},
		"iF#": {
			spelling: ["B#","C#","D","D#","E","E#","F#","G","G#","A","A#","B"],
			pitchClass: 6,
			name: "F# minor",
			shortName: "f#",
			signature: "###",
		},
		"jA_": {
			spelling: ["B#","C#","D","D#","E","E#","F#","G","G#","A","A#","B"],
			pitchClass: 9,
			name: "A major",
			shortName: "A",
			signature: "###",
		},
		"iC#": {
			spelling: ["B#","C#","D","D#","E","E#","F#","F##","G#","A","A#","B"],
			pitchClass: 1,
			name: "C# minor",
			shortName: "c#",
			signature: "####",
		},
		"jE_": {
			spelling: ["B#","C#","D","D#","E","E#","F#","F##","G#","A","A#","B"],
			pitchClass: 4,
			name: "E major",
			shortName: "E",
			signature: "####",
		},
		"iG#": {
			spelling: ["B#","C#","C##","D#","E","E#","F#","F##","G#","A","A#","B"],
			pitchClass: 8,
			name: "G# minor",
			shortName: "g#",
			signature: "#####",
		},
		"jB_":  {
			spelling: ["B#","C#","C##","D#","E","E#","F#","F##","G#","A","A#","B"],
			pitchClass: 11,
			name: "B major",
			shortName: "B",
			signature: "#####",
		},
		"iD#": {
			spelling: ["B#","C#","C##","D#","E","E#","F#","F##","G#","G##","A#","B"],
			pitchClass: 3,
			name: "D# minor",
			shortName: "d#",
			signature: "######",
		},
		"jF#": {
			spelling: ["B#","C#","C##","D#","E","E#","F#","F##","G#","G##","A#","B"],
			pitchClass: 6,
			name: "F# major",
			shortName: "F#",
			signature: "######",
		},
		"iA#": {
			spelling: ["B#","C#","C##","D#","D##","E#","F#","F##","G#","G##","A#","B"],
			pitchClass: 10,
			name: "A# minor",
			shortName: "a#",
			signature: "#######",
		},
		"jC#": {
			spelling: ["B#","C#","C##","D#","D##","E#","F#","F##","G#","G##","A#","A##"],
			pitchClass: 1,
			name: "C# major",
			shortName: "C#",
			signature: "#######",
		}
	},

	// Maps each of the 15 key signatures to an associated key.
	//
	// This mapping is arbitrary because a key signature could be major or
	// minor. Normally, we expect the user to select a specific key on the UI
	// and the key signature will then be updated to match due to the
	// key-to-key-signature lock.
	//
	// However, if the user chooses a key signature instead of (as we expect) a
	// key, and the key-to-key-signature lock is enabled, the app needs to know
	// what key should be selected in response, and this mapping provides that.
	//
	keySignatureMap: {
		"bbbbbbb": "jCb",
		"bbbbbb": "jGb",
		"bbbbb": "jDb",
		"bbbb": "jAb",
		"bbb": "jEb",
		"bb": "jBb",
		"b": "jF_",
		"": "jC_",
		"#": "jG_",
		"##": "jD_",
		"###": "jA_",
		"####": "jE_",
		"#####": "jB_",
		"######": "jF#",
		"#######": "jC#"
	},

	// Grouping of keys as they are to be displayed on the UI.
	keyDisplayGroups: [
		// [label, key1, key2, key3, ..., keyN]
		["basic", "h","jC_","iA_","jG_","jF_","iD_"],
		["sharps", "iE_","jD_","iB_","jA_","iF#","jE_","iC#","jB_","iG#","jF#","iD#","jC#","iA#"],
		["flats", "jBb","iG_","jEb","iC_","jAb","iF_","jDb","iBb","jGb","iEb","jCb","iAb"]
	],

	// Wheel for rotating keys sharpward or flatward
	// Default order is sharpward.
	keyWheel: [
		"jC_", "iE_","jG_","iB_","jD_","iF#","jA_","iC#",
		"jE_", "iG#","jB_","iEb","jGb","iBb","jDb","iF_",
		"jAb", "iC_","jEb","iG_","jBb","iD_","jF_","iA_"
	],

	// Default key and signature to use for notation. This should be
	// automatically selected on the UI.
	defaultKeyAndSignature: "h", // no key

	// Defines settings for the chord bank.
	chordBank: {
		// This defines the number of chords that can be displayed
		// on the sheet music at one time. It should be a positive
		// integer in the range of 1-10. Chords are displayed
		// in fixed-width areas spread across the sheet music.
		displaySize: 10
	},

	// Defines the default analysis settings
	analysisSettings: {
		// Enables or disables analysis
		enabled: true,
		// Enables or disables specific analysis modes
		mode: {
			note_names: false, // mutually exclusive with scientific_pitch
			scientific_pitch: false,
			scale_degrees: true, // mutually exclusive with solfege
			solfege: false,
			roman_numerals: true,
			intervals: true
		}
	},

	// Defines the default highlight settings
	highlightSettings: {
		// Enables or disables highlighting
		enabled: true,
		// Enables or disables specific highlight modes
		mode: {
			roothighlight: true,
			tritonehighlight: false
			//doublinghighlight: false,
			//octaveshighlight: false
		}
	},

	// Defines default metronome settings.
	metronomeSettings: {
		defaultTempo: 40,
		maxTempo: 360
	},

	// This defines whether keyboard shortcuts are enabled/disabled by default.
	// When true, keyboard shortcuts are enabled by default, otherwise when set
	// to false, they are disabled by default.
	keyboardShortcutsEnabled: true,

	// Defines when a bank happens in relation to a metronome "tick."
	// Expressed as a fraction of the current metronome tempo.
	bankAfterMetronomeTick: 0.25,

	// Defines the default keyboard size to use on the interface.
	defaultKeyboardSize: 49
});

<commit_msg>Update general.js<commit_after>// General configuration data
//
// Read-only.

/* global define: false */
define({

	// Defines the default spelling of the twelve pitches based on D-minor.
	noteNames: ['C','C#','D','Eb','E','F','F#','G','G#','A','Bb','B'],

	// Maps the twleve pitch classes [0-11] onto [0-9,y,z]. We are substituting
	// y and z for 10 and 11 so that we can represent each pitch class with a
	// single character.
	pitchClasses: ["0","1","2","3","4","5","6","7","8","9","y","z"],

	// Defines the order of sharps.
	orderOfSharps: ["F","C","G","D","A","E","B"],

	// Maps each major/minor key to a default spelling of the twelve pitches
	// along with related key information. The keys are identified by unique
	// three character strings with the following format:
	//
	//		key identifier = [i|j] + [A-G] + [_|#|b]
	//
	//		[i|j] = minor key, major key
	//		[A-G] = note from A to G
	//		[_|#|b] = natural, sharp, flat
	//
	keyMap: {
		// SPECIAL CASE: this is a special case for "no key" (i.e. NULL value for the key)
		"h": {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 0,
			name: "No key",
			shortName: "",
			signature: ""
		},
		"iAb": {
			spelling: ["C","Db","D","Eb","Fb","F","Gb","G","Ab","Bbb","Bb","Cb"],
			pitchClass: 8,
			name: "A♭ minor",
			shortName: "a♭",
			signature: "bbbbbbb",
		},
		"jCb": {
			spelling: ["C","Db","D","Eb","Fb","F","Gb","G","Ab","Bbb","Bb","Cb"],
			pitchClass: 11,
			name: "C♭ major",
			shortName: "C♭",
			signature: "bbbbbbb",
		},
		"iEb": {
			spelling: ["C","Db","D","Eb","Fb","F","Gb","G","Ab","A","Bb","Cb"],
			pitchClass: 3,
			name: "E♭ minor",
			shortName: "e♭",
			signature: "bbbbbb",
		},
		"jGb": {
			spelling: ["C","Db","D","Eb","Fb","F","Gb","G","Ab","A","Bb","Cb"],
			pitchClass: 6,
			name: "G♭ major",
			shortName: "G♭",
			signature: "bbbbbb",
		},
		"iBb": {
			spelling: ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","Cb"],
			pitchClass: 10,
			name: "B♭ minor",
			shortName: "b♭",
			signature: "bbbbb",
		},
		"jDb": {
			spelling: ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","Cb"],
			pitchClass: 1,
			name: "D♭ major",
			shortName: "D♭",
			signature: "bbbbb",
		},
		"iF_":  {
			spelling: ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","B"],
			pitchClass: 5,
			name: "F minor",
			shortName: "f",
			signature: "bbbb",
		},
		"jAb": {
			spelling: ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","B"],
			pitchClass: 8,
			name: "A♭ major",
			shortName: "A♭",
			signature: "bbbb",
		},
		"iC_": {
			spelling: ["C","Db","D","Eb","E","F","F#","G","Ab","A","Bb","B"],
			pitchClass: 0,
			name: "C minor",
			shortName: "c",
			signature: "bbb",
		},
		"jEb": {
			spelling: ["C","Db","D","Eb","E","F","F#","G","Ab","A","Bb","B"],
			pitchClass: 3,
			name: "E♭ major",
			shortName: "E♭",
			signature: "bbb",
		},
		"iG_":  {
			spelling: ["C","C#","D","Eb","E","F","F#","G","Ab","A","Bb","B"],
			pitchClass: 7,
			name: "G minor",
			shortName: "g",
			signature: "bb",
		},
		"jBb": {
			spelling: ["C","C#","D","Eb","E","F","F#","G","Ab","A","Bb","B"],
			pitchClass: 10,
			name: "B♭ major",
			shortName: "B♭",
			signature: "bb",
		},
		"iD_":  {
			spelling: ["C","C#","D","Eb","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 2,
			name: "D minor",
			shortName: "d",
			signature: "b",
		},
		"jF_":  {
			spelling: ["C","C#","D","Eb","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 5,
			name: "F major",
			shortName: "F",
			signature: "b",
		},
		"iA_":  {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 9,
			name: "A minor",
			shortName: "a",
			signature: "",
		},
		"jC_":  {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","Bb","B"],
			pitchClass: 0,
			name: "C major",
			shortName: "C",
			signature: "",
		},
		"iE_":  {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"],
			pitchClass: 4,
			name: "E minor",
			shortName: "e",
			signature: "#",
		},
		"jG_": {
			spelling: ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"],
			pitchClass: 7,
			name: "G major",
			shortName: "G",
			signature: "#",
		},
		"iB_": {
			spelling: ["C","C#","D","D#","E","E#","F#","G","G#","A","A#","B"],
			pitchClass: 11,
			name: "B minor",
			shortName: "b",
			signature: "##",
		},
		"jD_": {
			spelling: ["C","C#","D","D#","E","E#","F#","G","G#","A","A#","B"],
			pitchClass: 2,
			name: "D major",
			shortName: "D",
			signature: "##",
		},
		"iF#": {
			spelling: ["B#","C#","D","D#","E","E#","F#","G","G#","A","A#","B"],
			pitchClass: 6,
			name: "F♯ minor",
			shortName: "f♯",
			signature: "###",
		},
		"jA_": {
			spelling: ["B#","C#","D","D#","E","E#","F#","G","G#","A","A#","B"],
			pitchClass: 9,
			name: "A major",
			shortName: "A",
			signature: "###",
		},
		"iC#": {
			spelling: ["B#","C#","D","D#","E","E#","F#","F##","G#","A","A#","B"],
			pitchClass: 1,
			name: "C♯ minor",
			shortName: "c♯",
			signature: "####",
		},
		"jE_": {
			spelling: ["B#","C#","D","D#","E","E#","F#","F##","G#","A","A#","B"],
			pitchClass: 4,
			name: "E major",
			shortName: "E",
			signature: "####",
		},
		"iG#": {
			spelling: ["B#","C#","C##","D#","E","E#","F#","F##","G#","A","A#","B"],
			pitchClass: 8,
			name: "G♯ minor",
			shortName: "g♯",
			signature: "#####",
		},
		"jB_":  {
			spelling: ["B#","C#","C##","D#","E","E#","F#","F##","G#","A","A#","B"],
			pitchClass: 11,
			name: "B major",
			shortName: "B",
			signature: "#####",
		},
		"iD#": {
			spelling: ["B#","C#","C##","D#","E","E#","F#","F##","G#","G##","A#","B"],
			pitchClass: 3,
			name: "D♯ minor",
			shortName: "d♯",
			signature: "######",
		},
		"jF#": {
			spelling: ["B#","C#","C##","D#","E","E#","F#","F##","G#","G##","A#","B"],
			pitchClass: 6,
			name: "F♯ major",
			shortName: "F♯",
			signature: "######",
		},
		"iA#": {
			spelling: ["B#","C#","C##","D#","D##","E#","F#","F##","G#","G##","A#","B"],
			pitchClass: 10,
			name: "A♯ minor",
			shortName: "a♯",
			signature: "#######",
		},
		"jC#": {
			spelling: ["B#","C#","C##","D#","D##","E#","F#","F##","G#","G##","A#","A##"],
			pitchClass: 1,
			name: "C♯ major",
			shortName: "C♯",
			signature: "#######",
		}
	},

	// Maps each of the 15 key signatures to an associated key.
	//
	// This mapping is arbitrary because a key signature could be major or
	// minor. Normally, we expect the user to select a specific key on the UI
	// and the key signature will then be updated to match due to the
	// key-to-key-signature lock.
	//
	// However, if the user chooses a key signature instead of (as we expect) a
	// key, and the key-to-key-signature lock is enabled, the app needs to know
	// what key should be selected in response, and this mapping provides that.
	//
	keySignatureMap: {
		"bbbbbbb": "jCb",
		"bbbbbb": "jGb",
		"bbbbb": "jDb",
		"bbbb": "jAb",
		"bbb": "jEb",
		"bb": "jBb",
		"b": "jF_",
		"": "jC_",
		"#": "jG_",
		"##": "jD_",
		"###": "jA_",
		"####": "jE_",
		"#####": "jB_",
		"######": "jF#",
		"#######": "jC#"
	},

	// Grouping of keys as they are to be displayed on the UI.
	keyDisplayGroups: [
		// [label, key1, key2, key3, ..., keyN]
		/*["basic", "h","jC_","iA_","jG_","jF_","iD_"],*/
		/*["sharps", "iE_","jD_","iB_","jA_","iF#","jE_","iC#","jB_","iG#","jF#","iD#","jC#","iA#"],*/
		/*["flats", "jBb","iG_","jEb","iC_","jAb","iF_","jDb","iBb","jGb","iEb","jCb","iAb"]*/
		["basic", "h", "jC_", "iA_"],
		["major", "jCb", "jGb", "jDb", "jAb", "jEb", "jBb", "jF_", "jC_", "jG_", "jD_", "jA_", "jE_", "jB_", "jF#", "jC#"],
		["minor", "iAb", "iEb", "iBb", "iF_", "iC_", "iG_", "iD_", "iA_", "iE_", "iB_", "iF#", "iC#", "iG#", "iD#", "iA#"]
	],

	// Wheel for rotating keys sharpward or flatward
	// Default order is sharpward.
	keyWheel: [
		"jC_", "iE_","jG_","iB_","jD_","iF#","jA_","iC#",
		"jE_", "iG#","jB_","iEb","jGb","iBb","jDb","iF_",
		"jAb", "iC_","jEb","iG_","jBb","iD_","jF_","iA_"
	],

	// Default key and signature to use for notation. This should be
	// automatically selected on the UI.
	defaultKeyAndSignature: "h", // no key

	// Defines settings for the chord bank.
	chordBank: {
		// This defines the number of chords that can be displayed
		// on the sheet music at one time. It should be a positive
		// integer in the range of 1-10. Chords are displayed
		// in fixed-width areas spread across the sheet music.
		displaySize: 9
	},

	// Defines the default analysis settings
	analysisSettings: {
		// Enables or disables analysis
		enabled: true,
		// Enables or disables specific analysis modes
		mode: {
			note_names: false, // mutually exclusive with scientific_pitch
			scientific_pitch: false,
			scale_degrees: true, // mutually exclusive with solfege
			solfege: false,
			roman_numerals: true,
			intervals: true
		}
	},

	// Defines the default highlight settings
	highlightSettings: {
		// Enables or disables highlighting
		enabled: true,
		// Enables or disables specific highlight modes
		mode: {
			roothighlight: true,
			tritonehighlight: false
			//doublinghighlight: false,
			//octaveshighlight: false
		}
	},

	// Defines default metronome settings.
	metronomeSettings: {
		defaultTempo: 40,
		maxTempo: 360
	},

	// This defines whether keyboard shortcuts are enabled/disabled by default.
	// When true, keyboard shortcuts are enabled by default, otherwise when set
	// to false, they are disabled by default.
	keyboardShortcutsEnabled: true,

	// Defines when a bank happens in relation to a metronome "tick."
	// Expressed as a fraction of the current metronome tempo.
	bankAfterMetronomeTick: 0.25,

	// Defines the default keyboard size to use on the interface.
	defaultKeyboardSize: 49
});

<|endoftext|>