<commit_before>var Item = require('./Item.js');
var Status = require('./Status.js');
var List = require('./List.js');
var Utils = require('./Utils.js');
var App = require('./App.js');
var Status = require('./Status.js');

var app = new App();

document.addEventListener( "DOMContentLoaded", drawDom, false )

function buttonClicked(event) {
	var action = event.target.dataset.action;
	switch (action) {
		case "add-list":
						{
							let list = new List("New Empty List");
							app.add(list);
							drawDom();
							break;
						}
		case "add-item":
						{
							let listId = Utils.getParents(event.target,".list")[0].dataset.id;
							let list = app.getListById(listId);
							let item = new Item("New Item");
							list.add(item);
							drawDom();
							break;
						}
		case "delete-item" :
						{
							let itemId = Utils.getParents(event.target,".item")[0].dataset.id
							let listId = Utils.getParents(event.target,".list")[0].dataset.id;
							let list = app.getListById(listId);
							let item = list.getItemById(itemId);
							list.remove(item);
							drawDom();
							break;
						}
		case "add-date":
						{
							let itemId = Utils.getParents(event.target,".item")[0].dataset.id
							let listId = Utils.getParents(event.target,".list")[0].dataset.id;
							let list = app.getListById(listId);
							let item = list.getItemById(itemId);
							item.date = Utils.formatDate(new Date());
							drawDom();
							break;
						}

	}
}

function statusChanged(event) {
	let itemId = Utils.getParents(event.target,".item")[0].dataset.id
	let item = list.getItemById(itemId);
	item.status = (event.target.checked)? Status.COMPLETE : Status.PENDING;
}

function listTitleInput(event) {
	let listId = Utils.getParents(event.target,".list")[0].dataset.id
	let list = app.getListById(listId);
	list.title = (event.target.textContent)? event.target.textContent : 'Enter titile';
}

function itemTitleInput(event) {
	let itemId = Utils.getParents(event.target,".item")[0].dataset.id
	let listId = Utils.getParents(event.target,".list")[0].dataset.id
	let list = app.getListById(listId);
	let item = list.getItemById(itemId);
	item.title = (event.target.textContent)? event.target.textContent : 'Enter titile';
}

function itemDateChanged(event) {
	let itemId = Utils.getParents(event.target,".item")[0].dataset.id
	let listId = Utils.getParents(event.target,".list")[0].dataset.id
	let list = app.getListById(listId);
	let item = list.getItemById(itemId);
	if (event.target.value) {
		item.date = event.target.value
	} else {
		item.date = "";
		drawDom();
	}
}

function drawDom() {
	document.body.innerHTML = app.render();
	var buttons = document.getElementsByClassName("btn");
	for ( let i = 0; i < buttons.length; i++) {
		buttons[i].addEventListener('click', buttonClicked);
	}
	var statusInputs = document.getElementsByClassName("status-input");
	for ( let i = 0; i < statusInputs.length; i++) {
		statusInputs[i].addEventListener('change', statusChanged);
	}
	var listTitles = document.getElementsByClassName("list-title");
	for ( let i = 0; i < listTitles.length; i++) {
		listTitles[i].setAttribute("contenteditable", true);
		listTitles[i].addEventListener('input', listTitleInput);
	}
	var itemTitles = document.getElementsByClassName("item-title");
	for ( let i = 0; i < itemTitles.length; i++) {
		itemTitles[i].setAttribute("contenteditable", true);
		itemTitles[i].addEventListener('input', itemTitleInput);
	}
	var itemDates = document.getElementsByClassName("item-date");
	for ( let i = 0; i < itemDates.length; i++) {
		itemDates[i].addEventListener('change', itemDateChanged);
	}
}<commit_msg>Add localstorage persistence<commit_after>var Item = require('./Item.js');
var Status = require('./Status.js');
var List = require('./List.js');
var Utils = require('./Utils.js');
var App = require('./App.js');
var Status = require('./Status.js');

var app = new App();

document.addEventListener( "DOMContentLoaded", ready, false )

function ready() {
	if (localStorage.getItem("app")) {
		var appModel = JSON.parse(localStorage.getItem("app"));
		appModel.lists.forEach(function(list){
			let items = [];
			list.items.forEach(function(item) {
				items.push(new Item(item.title, item.date, item.status));
			});
			app.add(new List(list.title, items, list.isDeletable));
		});
	}
	drawDom();
}

function buttonClicked(event) {
	var action = event.target.dataset.action;
	switch (action) {
		case "add-list":
						{
							let list = new List("New Empty List");
							app.add(list);
							drawDom();
							break;
						}
		case "add-item":
						{
							let listId = Utils.getParents(event.target,".list")[0].dataset.id;
							let list = app.getListById(listId);
							let item = new Item("New Item");
							list.add(item);
							drawDom();
							break;
						}
		case "delete-item" :
						{
							let itemId = Utils.getParents(event.target,".item")[0].dataset.id
							let listId = Utils.getParents(event.target,".list")[0].dataset.id;
							let list = app.getListById(listId);
							let item = list.getItemById(itemId);
							list.remove(item);
							drawDom();
							break;
						}
		case "add-date":
						{
							let itemId = Utils.getParents(event.target,".item")[0].dataset.id
							let listId = Utils.getParents(event.target,".list")[0].dataset.id;
							let list = app.getListById(listId);
							let item = list.getItemById(itemId);
							item.date = Utils.formatDate(new Date());
							drawDom();
							break;
						}

	}
}

function statusChanged(event) {
	let itemId = Utils.getParents(event.target,".item")[0].dataset.id
	let item = list.getItemById(itemId);
	item.status = (event.target.checked)? Status.COMPLETE : Status.PENDING;
	updateLocalStorage();
}

function listTitleInput(event) {
	let listId = Utils.getParents(event.target,".list")[0].dataset.id
	let list = app.getListById(listId);
	list.title = (event.target.textContent)? event.target.textContent : 'Enter titile';
	updateLocalStorage();
}

function itemTitleInput(event) {
	let itemId = Utils.getParents(event.target,".item")[0].dataset.id
	let listId = Utils.getParents(event.target,".list")[0].dataset.id
	let list = app.getListById(listId);
	let item = list.getItemById(itemId);
	item.title = (event.target.textContent)? event.target.textContent : 'Enter titile';
	updateLocalStorage();
}

function itemDateChanged(event) {
	let itemId = Utils.getParents(event.target,".item")[0].dataset.id
	let listId = Utils.getParents(event.target,".list")[0].dataset.id
	let list = app.getListById(listId);
	let item = list.getItemById(itemId);
	if (event.target.value) {
		item.date = event.target.value
	} else {
		item.date = "";
		drawDom();
	}
	updateLocalStorage();
}

function drawDom() { //TO-DO: REFRACTOR
	updateLocalStorage();
	document.body.innerHTML = app.render();
	var buttons = document.getElementsByClassName("btn");
	for ( let i = 0; i < buttons.length; i++) {
		buttons[i].addEventListener('click', buttonClicked);
	}
	var statusInputs = document.getElementsByClassName("status-input");
	for ( let i = 0; i < statusInputs.length; i++) {
		statusInputs[i].addEventListener('change', statusChanged);
	}
	var listTitles = document.getElementsByClassName("list-title");
	for ( let i = 0; i < listTitles.length; i++) {
		listTitles[i].setAttribute("contenteditable", true);
		listTitles[i].addEventListener('input', listTitleInput);
	}
	var itemTitles = document.getElementsByClassName("item-title");
	for ( let i = 0; i < itemTitles.length; i++) {
		itemTitles[i].setAttribute("contenteditable", true);
		itemTitles[i].addEventListener('input', itemTitleInput);
	}
	var itemDates = document.getElementsByClassName("item-date");
	for ( let i = 0; i < itemDates.length; i++) {
		itemDates[i].addEventListener('change', itemDateChanged);
	}
}

function updateLocalStorage() {
	localStorage.setItem("app", JSON.stringify(app));
}<|endoftext|>


<commit_before>$(document).ready(function ready() {

  var ref = new Firebase('https://turco-groceries.firebaseio.com/groceries/')

  var $grocerySubmit = $('.grocery-submit')
  var $groceryInput = $('#add-grocery-input')
  var $singleGrocery = $('.single-grocery-item')

  // HTML variables
  var deleteIcon = '<button id="delete-btn" class="btn btn-default"></button>'

  function init() {
    loadItems()

  // Saves the grocery item to Firebase
  // Calls the function to display it on the page
  $grocerySubmit.on('click', function() {
    var input = $groceryInput.val()
    var newInput = input.charAt(0).toUpperCase() + input.slice(1)

    if ($groceryInput.val().length == 0) {
      toastr["error"]("Error: Please enter a grocery item")
    } else {
      ref.push({
        item: newInput,
      })
      $groceryInput.val('')
      $singleGrocery.append("
" + deleteIcon + ' ' + newInput + "
")
    }
  })

  // Adds the grocery items to the page on page load
  function loadItems() {
    ref.once("value", function(allGroceriesSnapshot) {
      allGroceriesSnapshot.forEach(function(grocerySnapshot) {
        var key = grocerySnapshot.key()
        var item = grocerySnapshot.child("item").val()
        $singleGrocery.append("
" + deleteIcon + ' ' + item + "
")
      })
    })
  }

  // Removes an item once it's been acquired
  $(document).on('click', '#delete-btn', function() {
    ref.once("value", function(allGroceriesSnapshot) {
      allGroceriesSnapshot.forEach(function(grocerySnapshot) {
        var key = grocerySnapshot.key()
        console.log(key)
        // ref.child(key).remove()
      })
    })
  })

  // Toastr settings
  toastr.options = {
    "closeButton": true,
    "debug": false,
    "newestOnTop": false,
    "progressBar": false,
    "positionClass": "toast-top-center",
    "preventDuplicates": false,
    "onclick": null,
    "showDuration": "300",
    "hideDuration": "1000",
    "timeOut": "5000",
    "extendedTimeOut": "1000",
    "showEasing": "swing",
    "hideEasing": "linear",
    "showMethod": "fadeIn",
    "hideMethod": "fadeOut"
  }

    //     vm.deleteIncome = function (item) {
    //       var profileRef = new Firebase('https://bankroll.firebaseio.com/profiles/' + $rootScope.auth.uid + '/income')
    //       profileRef.child(item.$id).remove(function(error){
    //         if (error) {
    //           console.log("Error", error)
    //         } else {
    //           console.log("Removed successfully")
    //           vm.loadIncome()
    //         }
    //       })
    //     }

    // var onComplete = function(error) {
    //   if (error) {
    //     console.log('Synchronization failed')
    //   } else {
    //     console.log('Synchronization succeeded')
    //   }
    // }
    // fredRef.remove(onComplete)

  }
  init()
})
<commit_msg>reorganize function order<commit_after>$(document).ready(function ready() {

  var ref = new Firebase('https://turco-groceries.firebaseio.com/groceries/')

  var $grocerySubmit = $('.grocery-submit')
  var $groceryInput = $('#add-grocery-input')
  var $singleGrocery = $('.single-grocery-item')

  // HTML variables
  var deleteIcon = '<button id="delete-btn" class="btn btn-default"></button>'

  function init() {
    loadItems()

  // Adds the grocery items to the page on page load
  function loadItems() {
    ref.once("value", function(allGroceriesSnapshot) {
      allGroceriesSnapshot.forEach(function(grocerySnapshot) {
        var key = grocerySnapshot.key()
        var item = grocerySnapshot.child("item").val()
        $singleGrocery.append("
" + deleteIcon + ' ' + item + "
")
      })
    })
  }

  // Saves the grocery item to Firebase
  // Calls the function to display it on the page
  $grocerySubmit.on('click', function() {
    var input = $groceryInput.val()
    var newInput = input.charAt(0).toUpperCase() + input.slice(1)

    if ($groceryInput.val().length == 0) {
      toastr["error"]("Error: Please enter a grocery item")
    } else {
      ref.push({
        item: newInput,
      })
      $groceryInput.val('')
      $singleGrocery.append("
" + deleteIcon + ' ' + newInput + "
")
    }
  })

  // Removes an item once it's been acquired
  $(document).on('click', '#delete-btn', function() {
    ref.once("value", function(allGroceriesSnapshot) {
      allGroceriesSnapshot.forEach(function(grocerySnapshot) {
        var key = grocerySnapshot.key()
        console.log(key)
        // ref.child(key).remove()
      })
    })
  })

  // Toastr settings
  toastr.options = {
    "closeButton": true,
    "debug": false,
    "newestOnTop": false,
    "progressBar": false,
    "positionClass": "toast-top-center",
    "preventDuplicates": false,
    "onclick": null,
    "showDuration": "300",
    "hideDuration": "1000",
    "timeOut": "5000",
    "extendedTimeOut": "1000",
    "showEasing": "swing",
    "hideEasing": "linear",
    "showMethod": "fadeIn",
    "hideMethod": "fadeOut"
  }

    //     vm.deleteIncome = function (item) {
    //       var profileRef = new Firebase('https://bankroll.firebaseio.com/profiles/' + $rootScope.auth.uid + '/income')
    //       profileRef.child(item.$id).remove(function(error){
    //         if (error) {
    //           console.log("Error", error)
    //         } else {
    //           console.log("Removed successfully")
    //           vm.loadIncome()
    //         }
    //       })
    //     }

    // var onComplete = function(error) {
    //   if (error) {
    //     console.log('Synchronization failed')
    //   } else {
    //     console.log('Synchronization succeeded')
    //   }
    // }
    // fredRef.remove(onComplete)

  }
  init()
})
<|endoftext|>


<commit_before>function parseLines(s) {
  var rawLines = s.split('\n');
  var result = [];

  for (var i = 0, len = rawLines.length; i < len; ++i) {
    var number = i+1;
    var text = trimWhitespace(rawLines[i]);

    // If the line is a comment or is blank, skip it.
    if (text === '' || text[0] === '#') {
      continue;
    }

    // If the line is a normal line, append it.
    if (text[text.length-1] !== '\\') {
      result.push({text: text, number: number});
      continue;
    }

    // Keep reading lines until there's no line continuation.
    text = text.substring(0, text.length-1);
    for (++i; i < len; ++i) {
      var nextLine = rawLines[i];
      if (nextLine[nextLine.length-1] === '\\') {
        nextLine = nextLine.substring(0, nextLine.length-1);
        text = text + nextLine;
      } else {
        text = text + nextLine;
        break;
      }
    }

    // If the loop terminated because i === rawLines.length, then the last line
    // ended with a \.
    if (i === rawLines.length) {
      throw new Error('unexpected \\ at end of input.');
    }

    result.push({text: text, number: number});
  }

  return result;
}

function trimWhitespace(s) {
  while (s[0] === ' ' || s[0] === '\t') {
    s = s.substring(1);
  }
  return s;
}

exports.parseLines = parseLines;<commit_msg>drafted tokenizer<commit_after>// A LineError represents an error that occurred while tokenizing a line.
function LineError(msg) {
  this.message = msg;
}

// toString returns a string representation of the error.
LineError.prototype.toString = function() {
  return 'LineError: ' + this.message;
};

// A ParseError includes a line number and error message.
function ParseError(line, message) {
  this.line = line;
  this.message = message;
}

// toString returns a string which indicates the line number and message.
ParseError.prototype.toString = function() {
  return 'line ' + this.line + ': ' + this.message;
};

// A Scanner treats a string as an input stream.
function Scanner(s) {
  this.index = 0;
  this.text = s;
}

// done returns true if the scanner has no more characters to read.
Scanner.prototype.done = function() {
  return this.index >= this.text.length;
};

// next reads the next character from the scanner.
Scanner.prototype.next = function() {
  if (this.done()) {
    return '';
  }
  return this.text[this.index++];
};

// skipRequiredSpace skips until the next non-whitespace character.
// This throws an exception if the next character is not a whitespace character.
Scanner.prototype.skipRequiredSpace = function() {
  if (this.done()) {
    return;
  }

  // Make sure at least one character is consumed.
  var cur = this.index;
  this.skipSpace();
  if (cur === this.index) {
    throw new LineError('expecting whitespace but got: ' + this.text[cur]);
  }
}

// skipSpace skips until the first non-whitespace character.
Scanner.prototype.skipSpace = function() {
  while (!this.done()) {
    var next = this.next();
    if (!isSpace(next)) {
      this.unread();
      return;
    }
  }
};

// unread makes it as if the last character was not read.
Scanner.prototype.unread = function() {
  this.index--;
};

// isSpace returns true if a string is a whitespace character.
function isSpace(s) {
  return s === ' ' || s === '\t';
}

// parseLines returns an array of objects of the form
// {text: "...", number: ...}.
function parseLines(s) {
  var rawLines = s.split('\n');
  var result = [];

  for (var i = 0, len = rawLines.length; i < len; ++i) {
    var number = i+1;
    var text = trimWhitespace(rawLines[i]);

    // If the line is a comment or is blank, skip it.
    if (text === '' || text[0] === '#') {
      continue;
    }

    // If the line is a normal line, append it.
    if (text[text.length-1] !== '\\') {
      result.push({text: text, number: number});
      continue;
    }

    // Keep reading lines until there's no line continuation.
    text = text.substring(0, text.length-1);
    for (++i; i < len; ++i) {
      var nextLine = rawLines[i];
      if (nextLine[nextLine.length-1] === '\\') {
        nextLine = nextLine.substring(0, nextLine.length-1);
        text = text + nextLine;
      } else {
        text = text + nextLine;
        break;
      }
    }

    // If the loop terminated because i === rawLines.length, then the last line
    // ended with a \.
    if (i === rawLines.length) {
      throw new ParseError(rawLines.length, 'unexpected \\ at end of input');
    }

    result.push({text: text, number: number});
  }

  return result;
}

// readCommand reads the command after a "(".
function readCommand(scanner) {
  var tokens = [];
  scanner.skipSpace();
  while (!scanner.done()) {
    var next = scanner.next();
    if (next === '"') {
      tokens.push(readQuoted(scanner));
    } else if (next === '(') {
      tokens.push(readCommand(scanner));
    } else if (next === '$') {
      var variableName = readRaw(scanner);
      tokens.push(['get', variableName]);
    } else if (next === ')') {
      return tokens;
    } else {
      scanner.unread();
      tokens.push(readRaw(scanner));
    }

    // If this is the end of the line then we should trigger a syntax error
    // since there was no ')'.
    if (scanner.done()) {
      break;
    }

    // Skip the next bunch of whitespace or detect a ')' character.
    var next = scanner.next();
    if (next === ')') {
      return tokens;
    } else if (!isSpace(next)) {
      throw new LineError('expected whitespace but got: ' + next);
    }
    scanner.skipSpace();
  }
  throw new LineError('missing expected close parenthesis');
}

// readEscape reads the character(s) after a "\" and returns the unescaped.
function readEscape(scanner) {
  if (scanner.done()) {
    throw new LineError('no escape code');
  }

  // TODO: support hex escapes.

  var char = scanner.next();
  var res = {'n': '\n', 'r': '\r', 'a': '\a', 't': '\t'}[char];
  if ('undefined' === typeof res) {
    return char;
  } else {
    return res;
  }
}

// readQuoted reads the text after a quote and returns the parsed string.
function readQuoted(scanner) {
  var str = '';
  while (!scanner.done()) {
    var next = scanner.next();
    if (next === '\\') {
      next = readEscape(scanner);
    } else if (next === '"') {
      return str;
    }
    str += next;
  }
  throw new LineError("missing end quote");
}

// readRaw reads a raw string and returns it.
function readRaw(scanner) {
  var str = '';
  while (!scanner.done()) {
    var next = scanner.next();
    if (next === '\\') {
      next = readEscape(scanner);
    } else if (isSpace(next) || next === ')') {
      // NOTE: we unread the whitespace so that skipRequiredSpace doesn't throw
      // an exception.
      scanner.unread();
      return str;
    }
    str += next;
  }
  return str;
}

// tokenizeLine generates tokens for a line of code or throws a LineError.
// The resulting line is an array of strings or arrays. The nested arrays are
// likewise arrays of strings or arrays. Array tokens represent nested commands.
function tokenizeLine(s) {
  var scanner = new Scanner(s);
  var tokens = [];
  scanner.skipSpace();
  while (!scanner.done()) {
    var next = scanner.next();
    if (next === '"') {
      tokens.push(readQuoted(scanner));
    } else if (next === '(') {
      tokens.push(readCommand(scanner));
    } else if (next === '$') {
      var variableName = readRaw(scanner);
      tokens.push(['get', variableName]);
    } else {
      scanner.unread();
      tokens.push(readRaw(scanner));
    }
    scanner.skipRequiredSpace();
  }
  return tokens;
}

// tokenizeScript parses a script and returns an array of tokenized lines.
function tokenizeScript(s) {
  var lines = parseLines(s);
  var tokenizedLines = [];
  for (var i = 0, len = lines.length; i < len; ++i) {
    var line = lines[i];
    try {
      var tokens = tokenizeLine(line.text);
      tokenizedLines.push(tokens);
    } catch (lineError) {
      throw lineError;
      throw new ParseError(line.number, lineError.message);
    }
  }
  return tokenizedLines;
}

// trimWhitespace removes the whitespace from the beginning of a string.
function trimWhitespace(s) {
  while (isSpace(s[0])) {
    s = s.substring(1);
  }
  return s;
}

exports.parseLines = parseLines;
exports.tokenizeLine = tokenizeLine;
exports.tokenizeScript = tokenizeScript;<|endoftext|>


<commit_before>var	rules = {},
	rulesInternals = {};

// Declare or get a rule
dominoes.rule = function( id ) {

	var length = arguments.length;

	if ( length > 1 ) {

		var list = parseList( slice.call( arguments , 1 ) ),
			ruleInternal = rulesInternals[ id ];

		// Create entry no matter what
		if ( ! ruleInternal ) {
			ruleInternal = rulesInternals[ id ] = [];
			rules[ id ] = function ( callback ) {
				return execute( ruleInternal , callback );
			};
		}

		// Filter out empty lists
		if ( list.length ) {

			// Note as non optional
			list.push( FALSE );

			// Add in
			ruleInternal.push( list );
		}

	} else if ( length ) {

		return rules[ id ];

	} else {

		rules = {};
		rulesInternals = {};
	}

	return this;
};

<commit_msg>Fixed a nasty bug that made rules consumed ahead of time.<commit_after>var	rules = {},
	rulesInternals = {};

// Declare or get a rule
dominoes.rule = function( id ) {

	var length = arguments.length;

	if ( length > 1 ) {

		var list = parseList( slice.call( arguments , 1 ) ),
			ruleInternal = rulesInternals[ id ];

		// Create entry no matter what
		if ( ! ruleInternal ) {

			var go = function() {
					execute( ruleInternal , function() {
						if ( running = list.length ) {
							running--;
							list.shift()();
							if ( running ) {
								go();
							}
						}
					} );
				},
				running;

			ruleInternal = rulesInternals[ id ] = [];
			rules[ id ] = function ( callback ) {
				if ( isFunction(callback) ) {
					list.push( callback );
				}
				if ( ! running ) {
					running = TRUE;
					go();
				}
				return true;
			};
		}

		// Filter out empty lists
		if ( list.length ) {

			// Note as non optional
			list.push( FALSE );

			// Add in
			ruleInternal.push( list );
		}

		// Free list for re-use
		list = [];

	} else if ( length ) {

		return rules[ id ];

	} else {

		rules = {};
		rulesInternals = {};
	}

	return this;
};

<|endoftext|>


<commit_before>import cx from 'classnames'

const sizeClassNames = (props = {}, opts = { offsets: true }) => {
  const classes = {}
  const sizes = ['xs', 'sm', 'md', 'lg']
  // Adding column classes
  for (const k of sizes) {
    if (props[k] != null) classes[`col-${k}-${props[k]}`] = true
  }
  // Adding offset classes
  if (opts.offsets) {
    for (const size of sizes) {
      const k = `${size}Offset`
      if (props[k] == null) continue
      classes[`col-${size}-offset-${props[k]}`] = true
    }
  }
  return cx(classes)
}

const inputContainerCx = (props) => {
  const labelWidth = props.labelWidth || {}
  const inputWidth = {}
  // The width of the input is the number of columns left after the label
  // is placed on the row. If the label takes a full row (12 coluns) then
  // the input is given another row (12 columns wide).
  Object.keys(labelWidth).forEach((k) => {
    inputWidth[k] = (12 - labelWidth[k]) || 12
  })

  const horizontalClasses = sizeClassNames(inputWidth, { offsets: false })
  return cx({
    ['col-xs-12']: props.layout === 'horizontal' && props.block,
    [horizontalClasses]: props.layout === 'horizontal' && !props.block,
  })
}

const formGroupCx = (props) => {
  const { inputHtml } = props
  const isCheckbox = inputHtml ? inputHtml.type === 'checkbox' : false

  return cx(Object.assign({
    'form-group': !isCheckbox,
    checkbox: isCheckbox,
    'has-error': props.errors != null,
    'has-success': props.modified && props.errors == null,
  }))
}

export {
  sizeClassNames,
  inputContainerCx,
  formGroupCx,
}
<commit_msg>no-useless-computed-key: Fix util.js<commit_after>import cx from 'classnames'

const sizeClassNames = (props = {}, opts = { offsets: true }) => {
  const classes = {}
  const sizes = ['xs', 'sm', 'md', 'lg']
  // Adding column classes
  for (const k of sizes) {
    if (props[k] != null) classes[`col-${k}-${props[k]}`] = true
  }
  // Adding offset classes
  if (opts.offsets) {
    for (const size of sizes) {
      const k = `${size}Offset`
      if (props[k] == null) continue
      classes[`col-${size}-offset-${props[k]}`] = true
    }
  }
  return cx(classes)
}

const inputContainerCx = (props) => {
  const labelWidth = props.labelWidth || {}
  const inputWidth = {}
  // The width of the input is the number of columns left after the label
  // is placed on the row. If the label takes a full row (12 coluns) then
  // the input is given another row (12 columns wide).
  Object.keys(labelWidth).forEach((k) => {
    inputWidth[k] = (12 - labelWidth[k]) || 12
  })

  const horizontalClasses = sizeClassNames(inputWidth, { offsets: false })
  return cx({
    'col-xs-12': props.layout === 'horizontal' && props.block,
    [horizontalClasses]: props.layout === 'horizontal' && !props.block,
  })
}

const formGroupCx = (props) => {
  const { inputHtml } = props
  const isCheckbox = inputHtml ? inputHtml.type === 'checkbox' : false

  return cx(Object.assign({
    'form-group': !isCheckbox,
    checkbox: isCheckbox,
    'has-error': props.errors != null,
    'has-success': props.modified && props.errors == null,
  }))
}

export {
  sizeClassNames,
  inputContainerCx,
  formGroupCx,
}
<|endoftext|>


<commit_before>'use strict';

/**
 * Provides and configures the jsonrpc service.
 */
angular.module('jsonrpc', ['uuid']).provider('jsonrpc', function() {
  var defaults = this.defaults = {};

  // defaults
  defaults.basePath = '/rpc';

  // provider.$get
  this.$get = ['$http', 'uuid4', function($http, uuid4) {
    /**
     * Makes a JSON-RPC request to `method` with `data`.
     *
     * @param {{path:string=, method:string, data:*)}} options Call options.
     * @param {angular.$http.Config} config HTTP config.
     * @return {angular.$http.HttpPromise}
     */
    function jsonrpc(options, config) {
      var id = uuid4.generate();
      var payload = {
        jsonrpc: '2.0',
        method: options.method,
        id: id
      };
      if (angular.isDefined(options.data)) {
        payload.params = options.data;
      }

      // Transformers to extract the response data.
      // TODO(arunjit): Use response interceptors when the API is stable.
      var transforms = [];
      angular.forEach($http.defaults.transformResponse, function(t) {
        transforms.push(t);
      });
      transforms.push(function(data) {
        return data.id === id ? data.result || data.error : null;
      });

      config = config || {};
      var configTransforms = config.transformResponse;
      if (angular.isArray(configTransforms)) {
        [].push.apply(transforms, configTransforms);
      } else if (angular.isFunction(configTransforms)) {
        transforms.push(configTransforms);
      }
      config.transformResponse = transforms;

      // TODO(arunjit): Use $q to resolve the result.
      return $http.post(options.path || defaults.basePath, payload, config);
    }

    /**
     * Shorthand for making a request.
     *
     * @param {string} path The call path.
     * @param {string} method The method to call.
     * @param {?*} data The data for the call.
     * @param {angular.$http.Config} config HTTP config.
     * @return {angular.$http.HttpPromise}
     */
    jsonrpc.request = function(path, method, data, config) {
      if (arguments.length < 4) {
        config = data;
        data = method;
        method = path;
        path = null;
      }
      return jsonrpc({path: path, method: method, data: data}, config);
    };

    /**
     * Helper to create services.
     *
     * Usage:
     *     module.service('locationService', function(jsonrpc) {
     *       var service = jsonrpc.newService('locationsvc');
     *       this.get = service.createMethod('Get');
     *     });
     *     ...
     *     module.controller(..., function(locationService) {
     *       locationService.get({max: 10}).success(function(d) {...});
     *       // GET /rpc
     *       // {"method": "locationsvc.Get", "params": {"max": 10}, ...}
     *     });
     *
     * @param {string} name The name of the service. This is the prefix used for
     *     all methods created through this service.
     * @param {string} path Optional path for this service.
     * @constructor
     */
    function Service(name, path) {
      this.serviceName = name;
      this.path = path;
    }

    /**
     * Creates a new service method.
     *
     * @param {string} name Method name.
     * @param {angular.$http.Config=} config HTTP config.
     * @return {function(*):angular.$http.HttpPromise} An implementation for the
     *     service method.
     */
    Service.prototype.createMethod = function(name, config) {
      var path = this.path;
      var method = this.serviceName + '.' + name;
      return function(data) {
        return jsonrpc.request(path, method, data, config);
      };
    };

    /** Creates a new Service with the given `name` and optional `path`. */
    jsonrpc.newService = function(name, path) {
      return new Service(name, path);
    };

    return jsonrpc;
  }];

  /** Set the base path for all JSON-RPC calls to |path|. */
  this.setBasePath = function(path) {
    this.defaults.basePath = path;
    return this;
  };
});
<commit_msg>Call methods without serviceName<commit_after>'use strict';

/**
 * Provides and configures the jsonrpc service.
 */
angular.module('jsonrpc', ['uuid']).provider('jsonrpc', function() {
  var defaults = this.defaults = {};

  // defaults
  defaults.basePath = '/rpc';

  // provider.$get
  this.$get = ['$http', 'uuid4', function($http, uuid4) {
    /**
     * Makes a JSON-RPC request to `method` with `data`.
     *
     * @param {{path:string=, method:string, data:*)}} options Call options.
     * @param {angular.$http.Config} config HTTP config.
     * @return {angular.$http.HttpPromise}
     */
    function jsonrpc(options, config) {
      var id = uuid4.generate();
      var payload = {
        jsonrpc: '2.0',
        method: options.method,
        id: id
      };
      if (angular.isDefined(options.data)) {
        payload.params = options.data;
      }

      // Transformers to extract the response data.
      // TODO(arunjit): Use response interceptors when the API is stable.
      var transforms = [];
      angular.forEach($http.defaults.transformResponse, function(t) {
        transforms.push(t);
      });
      transforms.push(function(data) {
        return data.id === id ? data.result || data.error : null;
      });

      config = config || {};
      var configTransforms = config.transformResponse;
      if (angular.isArray(configTransforms)) {
        [].push.apply(transforms, configTransforms);
      } else if (angular.isFunction(configTransforms)) {
        transforms.push(configTransforms);
      }
      config.transformResponse = transforms;

      // TODO(arunjit): Use $q to resolve the result.
      return $http.post(options.path || defaults.basePath, payload, config);
    }

    /**
     * Shorthand for making a request.
     *
     * @param {string} path The call path.
     * @param {string} method The method to call.
     * @param {?*} data The data for the call.
     * @param {angular.$http.Config} config HTTP config.
     * @return {angular.$http.HttpPromise}
     */
    jsonrpc.request = function(path, method, data, config) {
      if (arguments.length < 4) {
        config = data;
        data = method;
        method = path;
        path = null;
      }
      return jsonrpc({path: path, method: method, data: data}, config);
    };

    /**
     * Helper to create services.
     *
     * Usage:
     *     module.service('locationService', function(jsonrpc) {
     *       var service = jsonrpc.newService('locationsvc');
     *       this.get = service.createMethod('Get');
     *     });
     *     ...
     *     module.controller(..., function(locationService) {
     *       locationService.get({max: 10}).success(function(d) {...});
     *       // GET /rpc
     *       // {"method": "locationsvc.Get", "params": {"max": 10}, ...}
     *     });
     *
     * @param {string} name The name of the service. This is the prefix used for
     *     all methods created through this service.
     * @param {string} path Optional path for this service.
     * @constructor
     */
    function Service(name, path) {
      this.serviceName = name;
      this.path = path;
    }

    /**
     * Creates a new service method.
     *
     * @param {string} name Method name.
     * @param {angular.$http.Config=} config HTTP config.
     * @return {function(*):angular.$http.HttpPromise} An implementation for the
     *     service method.
     */
    Service.prototype.createMethod = function(name, config) {
      var path = this.path;
      var method = name;
      if (this.serviceName) {
         method = this.serviceName + '.' + method;
      }
      return function(data) {
        return jsonrpc.request(path, method, data, config);
      };
    };

    /** Creates a new Service with the given `name` and optional `path`. */
    jsonrpc.newService = function(name, path) {
      return new Service(name, path);
    };

    return jsonrpc;
  }];

  /** Set the base path for all JSON-RPC calls to |path|. */
  this.setBasePath = function(path) {
    this.defaults.basePath = path;
    return this;
  };
});
<|endoftext|>


<commit_before>
// d - Day of the month, 2 digits with leading zeros: 01 to 31
// j - Day of the month without leading zeros: 1 to 31

// m - Numeric representation of a month, with leading zeros: 01 through 12
// n - Numeric representation of a month, without leading zeros: 1 through 12
// F - full month string, given as array

// Y - A full numeric representation of a year, 4 digits

// G - 24-hour format of an hour without leading zeros: 0 through 23
// H - 24-hour format of an hour with leading zeros	00 through 23
// i - Minutes with leading zeros	00 to 59
// s - Seconds, with leading zeros	00 through 59

Util.date = function(format, timestamp, months) {

	var date = timestamp ? new Date(timestamp) : new Date();

	// not a valid date
	if(isNaN(date.getTime())) {

		// look for correct timezone
		if(!timestamp.match(/[A-Z]{3}\+[0-9]{4}/)) {

			// offset declared without timezone (twitter - IE doesn't understand)
			if(timestamp.match(/ \+[0-9]{4}/)) {

				// add timezone and try to create date again
				date = new Date(timestamp.replace(/ (\+[0-9]{4})/, " GMT$1"));
			}
		}

		// final test - if it still fails, return current time
		if(isNaN(date.getTime())) {
			date = new Date();
		}
	}

	var tokens = /d|j|m|n|F|Y|G|H|i|s/g;

	var chars = new Object();

	chars.j = date.getDate();
	chars.d = (chars.j > 9 ? "" : "0") + chars.j;

	chars.n = date.getMonth()+1;
	chars.m = (chars.n > 9 ? "" : "0") + chars.n;
	chars.F = months ? months[date.getMonth()] : "";

	chars.Y = date.getFullYear();

	chars.G = date.getHours();
	chars.H = (chars.G > 9 ? "" : "0") + chars.G;

	var i = date.getMinutes();
	chars.i = (i > 9 ? "" : "0") + i;

	var s = date.getSeconds();
	chars.s = (s > 9 ? "" : "0") + s;

	return format.replace(tokens, function (_) {
		return _ in chars ? chars[_] : _.slice(1, _.length - 1);
	});
};
<commit_msg>Extended timestamp support<commit_after>
// d - Day of the month, 2 digits with leading zeros: 01 to 31
// j - Day of the month without leading zeros: 1 to 31

// m - Numeric representation of a month, with leading zeros: 01 through 12
// n - Numeric representation of a month, without leading zeros: 1 through 12
// F - full month string, given as array

// Y - A full numeric representation of a year, 4 digits

// G - 24-hour format of an hour without leading zeros: 0 through 23
// H - 24-hour format of an hour with leading zeros	00 through 23
// i - Minutes with leading zeros	00 to 59
// s - Seconds, with leading zeros	00 through 59

Util.date = function(format, timestamp, months) {

	// default to current time if timestamp is not set
	var date = timestamp ? new Date(timestamp) : new Date();

	// not a valid date
	if(isNaN(date.getTime())) {

		if(new Date(timestamp.replace(/ /, "T"))) {

			date = new Date(timestamp.replace(/ /, "T"));

		}
		else {

			// look for correct timezone
			if(!timestamp.match(/[A-Z]{3}\+[0-9]{4}/)) {

				// offset declared without timezone (twitter - IE doesn't understand)
				if(timestamp.match(/ \+[0-9]{4}/)) {

					// add timezone and try to create date again
					date = new Date(timestamp.replace(/ (\+[0-9]{4})/, " GMT$1"));
				}
			}

		}

		// final test - if it still fails, return current time
		if(isNaN(date.getTime())) {
			date = new Date();
		}
	}
//	u.bug("date:" + date.getDate());

	var tokens = /d|j|m|n|F|Y|G|H|i|s/g;

	var chars = new Object();

	chars.j = date.getDate();
	chars.d = (chars.j > 9 ? "" : "0") + chars.j;

	chars.n = date.getMonth()+1;
	chars.m = (chars.n > 9 ? "" : "0") + chars.n;
	chars.F = months ? months[date.getMonth()] : "";

	chars.Y = date.getFullYear();

	chars.G = date.getHours();
	chars.H = (chars.G > 9 ? "" : "0") + chars.G;

	var i = date.getMinutes();
	chars.i = (i > 9 ? "" : "0") + i;

	var s = date.getSeconds();
	chars.s = (s > 9 ? "" : "0") + s;

	return format.replace(tokens, function (_) {
		return _ in chars ? chars[_] : _.slice(1, _.length - 1);
	});
};
<|endoftext|>


<commit_before>// convenience methods ES6+
const objUtil = function objUtil(obj, f) {
	var keys = Object.keys(obj);
	var entries = [];
	for (var i = 0; i < keys.length; i++) {
		var key = keys[i];
		entries.push(f(key));
	}
	return entries;
};

if (!Object.values) {
	Object.values = function (o) {
		return objUtil(o, function (key) {
			return o[key];
		});
	};
}
if (!Object.entries) {
	Object.entries = function (o) {
		return objUtil(o, function (key) {
			return [key, o[key]];
		});
	};
}
if(!Array.prototype.last) {
	Array.prototype.last = function() {
		return this[this.length - 1];
	};
}
<commit_msg>moved to polyfill<commit_after><|endoftext|>


<commit_before>'use strict';

/**
 * Binds a ACE Ediitor widget
 */
angular.module('ui.ace', [])
  .constant('uiAceConfig', {})
  .directive('uiAce', ['uiAceConfig', function (uiAceConfig) {
    if (angular.isUndefined(window.ace)) {
      throw new Error('ui-ace need ace to work... (o rly?)');
    }
    return {
      restrict: 'EA',
      require: '?ngModel',
      link: function (scope, elm, attrs, ngModel) {
        var options, opts, acee, session, onChange;

        options = uiAceConfig.ace || {};
        opts = angular.extend({}, options, scope.$eval(attrs.uiAce));

        acee = window.ace.edit(elm[0]);
        session = acee.getSession();

        onChange = function (callback) {
          return function (e) {
            var newValue = session.getValue();
            if (newValue !== scope.$eval(attrs.value) && !scope.$$phase && !scope.$root.$$phase) {
              if (angular.isDefined(ngModel)) {
                scope.$apply(function () {
                  ngModel.$setViewValue(newValue);
                });
              }

              /**
               * Call the user onChange function.
               */
              if (angular.isDefined(callback)) {
                scope.$apply(function () {
                  if (angular.isFunction(callback)) {
                    callback(e, acee);
                  }
                  else {
                    throw new Error('ui-ace use a function as callback.');
                  }
                });
              }
            }
          };
        };

        // Boolean options
        if (angular.isDefined(opts.showGutter)) {
          acee.renderer.setShowGutter(opts.showGutter);
        }
        if (angular.isDefined(opts.useWrapMode)) {
          session.setUseWrapMode(opts.useWrapMode);
        }

        // onLoad callback
        if (angular.isFunction(opts.onLoad)) {
          opts.onLoad(acee);
        }

        // Basic options
        if (angular.isString(opts.theme)) {
          acee.setTheme('ace/theme/' + opts.theme);
        }
        if (angular.isString(opts.mode)) {
          session.setMode('ace/mode/' + opts.mode);
        }

        attrs.$observe('readonly', function (value) {
          acee.setReadOnly(value === 'true');
        });

        // Value Blind
        if (angular.isDefined(ngModel)) {
          ngModel.$formatters.push(function (value) {
            if (angular.isUndefined(value) || value === null) {
              return '';
            }
            else if (angular.isObject(value) || angular.isArray(value)) {
              throw new Error('ui-ace cannot use an object or an array as a model');
            }
            return value;
          });

          ngModel.$render = function () {
            session.setValue(ngModel.$viewValue);
          };
        }

        // EVENTS
        session.on('change', onChange(opts.onChange));

        elm.on('$destroy', function() {
          acee.session.$stopWorker();
          acee.destroy();
        });

        scope.$watch(function() {
          return [elm[0].offsetWidth, elm[0].offsetHeight];
        }, function() {
          acee.resize();
          acee.renderer.updateFull();
        }, true);
      }
    };
  }]);
<commit_msg>Data binding support added for ace options and JSDoc blocks added.<commit_after>'use strict';

/**
 * Binds a ACE Editor widget
 */
angular.module('ui.ace', [])
  .constant('uiAceConfig', {})
  .directive('uiAce', ['uiAceConfig', function (uiAceConfig) {

    if (angular.isUndefined(window.ace)) {
      throw new Error('ui-ace need ace to work... (o rly?)');
    }

    /**
     * Sets editor options such as the wrapping mode or the syntax checker.
     *
     * The supported options are:
     *
     *

     *
showGutter

     *
useWrapMode

     *
onLoad

     *
theme

     *
mode

     *

     *
     * @param acee
     * @param session ACE editor session
     * @param {object} opts Options to be set
     */
    var setOptions = function(acee, session, opts) {

      // Boolean options
      if (angular.isDefined(opts.showGutter)) {
        acee.renderer.setShowGutter(opts.showGutter);
      }
      if (angular.isDefined(opts.useWrapMode)) {
        session.setUseWrapMode(opts.useWrapMode);
      }

      // onLoad callback
      if (angular.isFunction(opts.onLoad)) {
        opts.onLoad(acee);
      }

      // Basic options
      if (angular.isString(opts.theme)) {
        acee.setTheme('ace/theme/' + opts.theme);
      }
      if (angular.isString(opts.mode)) {
        session.setMode('ace/mode/' + opts.mode);
      }
    }

    return {
      restrict: 'EA',
      require: '?ngModel',
      link: function (scope, elm, attrs, ngModel) {

        /**
         * Corresponds the uiAceConfig ACE configuration.
         * @type object
         */
        var options = uiAceConfig.ace || {};

        /**
         * uiAceConfig merged with user options via json in attribute or data binding
         * @type object
         */
        var opts = angular.extend({}, options, scope.$eval(attrs.uiAce));

        /**
         * ACE editor
         * @type object
         */
        var acee = window.ace.edit(elm[0]);

        /**
         * ACE editor session.
         * @type object
         * @see [EditSession]{@link http://ace.c9.io/#nav=api&api=edit_session}
         */
        var session = acee.getSession();

        /**
         * Reference to a change listener created by the listener factory.
         * @function
         * @see listenerFactory.onChange
         */
        var onChangeListener;

        /**
         * Listener factory. Until now only change listeners can be created.
         * @type object
         */
        var listenerFactory = {
          /**
           * Creates a change listener which propagates the change event
           * to the callback from the user option onChange. It might be
           * exchanged during runtime, if this happens the old listener
           * will be unbound.
           *
           * @param callback callback function taken from the
           * @see onChangeListener
           */
          onChange: function (callback) {
            return function (e) {
              var newValue = session.getValue();
              if (newValue !== scope.$eval(attrs.value) && !scope.$$phase && !scope.$root.$$phase) {
                if (angular.isDefined(ngModel)) {
                  scope.$apply(function () {
                    ngModel.$setViewValue(newValue);
                  });
                }

                /**
                 * Call the user onChange function.
                 */
                if (angular.isDefined(callback)) {
                  scope.$apply(function () {
                    if (angular.isFunction(callback)) {
                      callback(e, acee);
                    } else {
                      throw new Error('ui-ace use a function as callback.');
                    }
                  });
                }
              }
            };
          }
        };

        attrs.$observe('readonly', function (value) {
          acee.setReadOnly(value === 'true');
        });

        // Value Blind
        if (angular.isDefined(ngModel)) {
          ngModel.$formatters.push(function (value) {
            if (angular.isUndefined(value) || value === null) {
              return '';
            }
            else if (angular.isObject(value) || angular.isArray(value)) {
              throw new Error('ui-ace cannot use an object or an array as a model');
            }
            return value;
          });

          ngModel.$render = function () {
            session.setValue(ngModel.$viewValue);
          };
        }

        // Listen for option updates
        scope.$watch( attrs.uiAce, function() {
          opts = angular.extend({}, options, scope.$eval(attrs.uiAce));

          // unbind old listener
          session.removeListener('change', onChangeListener);

          // bind new listener
          onChangeListener = listenerFactory.onChange(opts.onChange);
          session.on('change', onChangeListener);

          setOptions(acee, session, opts);
        }, /* deep watch */ true );

        // EVENTS
        onChangeListener = listenerFactory.onChange(opts.onChange);
        session.on('change', onChangeListener);

        elm.on('$destroy', function () {
          acee.session.$stopWorker();
          acee.destroy();
        });

        scope.$watch(function() {
          return [elm[0].offsetWidth, elm[0].offsetHeight];
        }, function() {
          acee.resize();
          acee.renderer.updateFull();
        }, true);

      }
    };
  }]);
<|endoftext|>


<commit_before>// Laconic simplifies the generation of DOM content.
!function(context) {

  // Generic function for generating a dom element.
  //
  // The first parameter MUST be a string specifying the element's
  // tag name.
  //
  // An optional object of element attributs may follow directly
  // after the tag name.
  //
  // Additional arguments will be considered children of the new
  // element and may consist of elements, strings, or numbers.
  //
  // for example:
  // gen('div', {'class' : 'foo'}, 'bar');
  function gen() {
    // create a new element of the requested type
    var tagName = arguments.length === 0 ? 'div' : arguments[0];
    var el = document.createElement(tagName);

    // walk through the rest of the arguments
    var args = Array.prototype.slice.call(arguments, 1);
    for(var i=0; i<args.length; i++) {
      var arg = args[i];

      // if the argument is a dom node, we simply append it
      if(arg.nodeType === 1) {
        el.appendChild(arg);
      }

      // if the argument is a string or a number, we create and append
      // a new text node
      else if(
          (!!(arg === '' || (arg && arg.charCodeAt && arg.substr))) ||
          (!!(arg === 0 || (arg && arg.toExponential && arg.toFixed)))) {

        el.appendChild(document.createTextNode(arg));
      }

      // if the argument is a plain-old object, and we're processing the first
      // argument, then we apply the object's values as element attributes
      else if(i === 0 && typeof(arg) === 'object') {
        for(var key in arg) {
          el.setAttribute(key, arg[key]);
        }
      }
    }

    // Add an appendTo method to the newly created element, which will allow
    // the DOM insertion to be method chained to the creation.  For example:
    // $el.div('foo').appendTo(document.body);
    el.appendTo = function(parentNode) {
      if(parentNode.nodeType === 1 && this.nodeType === 1) {
        parentNode.appendChild(this);
      }
      return this;
    };

    return el;
  }

  // Create the laconic object which describes the external interface
  var laconic = {

    // registers a new tag
    registerTag : function(name, renderer) {
      if(!laconic[name]) {
        laconic[name] = function() {
          var el = gen('div', {'class' : name});
          renderer.apply(el, Array.prototype.slice.call(arguments));
          return el;
        };
      }
    }
  };

  // list of html 4 tags not valid in html 5 that should be added as methods
  // to the laconic interface
  var deprecatedTags = ['acronym', 'applet', 'basefont', 'big', 'center', 'dir',
    'font', 'frame', 'frameset', 'noframes', 'strike', 'tt', 'u', 'xmp'];

  // list all valid html 5 tags that should be added as methods to the
  // laconic namespace, as well as the above deprecated tags
  var tags = ['a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b',
    'base', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption',
    'cite', 'code', 'col', 'colgroup', 'command', 'datalist', 'dd', 'del',
    'details', 'dfn', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset',
    'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5',
    'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'img',
    'input', 'ins', 'keygen', 'kbd', 'label', 'legend', 'li', 'link', 'map',
    'mark', 'menu', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol',
    'optgroup', 'option', 'output', 'p', 'param', 'pre', 'progress', 'q', 'rp',
    'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', 'small',
    'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'table',
    'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr',
    'ul', 'var', 'video', 'wbr'].concat(deprecatedTags);

  // add our tag methods to the laconic object
  for(var i=0; i<tags.length; i++) {
    laconic[tags[i]] = (function(tagName) {
      return function() {
        return gen.apply(this,
          [tagName].concat(Array.prototype.slice.call(arguments)));
      };
    })(tags[i]);
  }

  // If we're in a CommonJS environment, we export our laconic methods
  if(typeof module !== 'undefined' && module.exports) {
    module.exports = laconic;
  }

  // otherwise, we attach them to the top level $.el namespace
  else {
    var dollar = context['$'] || {};
    dollar['el'] = laconic;
    context['$'] = dollar;
  }

}(this);
<commit_msg>allowing arrays, allowing gen to be invoked externally<commit_after>// Laconic simplifies the generation of DOM content.
!function(context) {

  // The laconic function serves as a generic method for generating
  // DOM content, and also as a placeholder for helper functions.
  //
  // The first parameter MUST be a string specifying the element's
  // tag name.
  //
  // An optional object of element attributs may follow directly
  // after the tag name.
  //
  // Additional arguments will be considered children of the new
  // element and may consist of elements, strings, or numbers.
  //
  // for example:
  // gen('div', {'class' : 'foo'}, 'bar');
  function laconic() {
    // create a new element of the requested type
    var tagName = arguments.length === 0 ? 'div' : arguments[0];
    var el = document.createElement(tagName);

    // walk through the rest of the arguments
    var args = Array.prototype.slice.call(arguments, 1);
    for(var i=0; i<args.length; i++) {
      var arg = args[i];

      // if the argument is a dom node, we simply append it
      if(arg.nodeType === 1) {
        el.appendChild(arg);
      }

      // if the argument is an array, we append each element
      else if(Object.prototype.toString.call(arg) === '[object Array]') {
        for(var j=0; j<arg.length; j++) {
          var child = arg[j];
          if(child.nodeType === 1) {
            el.appendChild(child);
          }
        }
      }

      // if the argument is a string or a number, we append it as
      // a new text node
      else if(
          (!!(arg === '' || (arg && arg.charCodeAt && arg.substr))) ||
          (!!(arg === 0 || (arg && arg.toExponential && arg.toFixed)))) {

        el.appendChild(document.createTextNode(arg));
      }

      // if the argument is a plain-old object, and we're processing the first
      // argument, then we apply the object's values as element attributes
      else if(i === 0 && typeof(arg) === 'object') {
        for(var key in arg) {
          var value = arg[key];
          if(value !== null && value !== undefined) {
            // IE classname nonsense
            if(key.toLowerCase() === 'classname') {
              el[key] = value;
            }
            else {
              el.setAttribute(key, value);
            }
          }
        }
      }
    }

    // Add an appendTo method to the newly created element, which will allow
    // the DOM insertion to be method chained to the creation.  For example:
    // $el.div('foo').appendTo(document.body);
    el.appendTo = function(parentNode) {
      if(parentNode.nodeType === 1 && this.nodeType === 1) {
        parentNode.appendChild(this);
      }
      return el;
    };

    return el;
  }

  laconic.registerTag = function(name, renderer) {
    if(!laconic[name]) {
      laconic[name] = function() {
        var el = laconic('div', {'class' : name});
        renderer.apply(el, Array.prototype.slice.call(arguments));
        return el;
      };
    }
  };

  // list of html 4 tags
  var deprecatedTags = ['acronym', 'applet', 'basefont', 'big', 'center', 'dir',
    'font', 'frame', 'frameset', 'noframes', 'strike', 'tt', 'u', 'xmp'];

  // list html 5 tags
  var tags = ['a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b',
    'base', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption',
    'cite', 'code', 'col', 'colgroup', 'command', 'datalist', 'dd', 'del',
    'details', 'dfn', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset',
    'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5',
    'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'img',
    'input', 'ins', 'keygen', 'kbd', 'label', 'legend', 'li', 'link', 'map',
    'mark', 'menu', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol',
    'optgroup', 'option', 'output', 'p', 'param', 'pre', 'progress', 'q', 'rp',
    'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', 'small',
    'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'table',
    'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr',
    'ul', 'var', 'video', 'wbr'].concat(deprecatedTags);

  // add our tag methods to the laconic object
  for(var i=0; i<tags.length; i++) {
    laconic[tags[i]] = (function(tagName) {
      return function() {
        return laconic.apply(this,
          [tagName].concat(Array.prototype.slice.call(arguments)));
      };
    })(tags[i]);
  }

  // If we're in a CommonJS environment, we export our laconic methods
  if(typeof module !== 'undefined' && module.exports) {
    module.exports = laconic;
  }

  // otherwise, we attach them to the top level $.el namespace
  else {
    var dollar = context['$'] || {};
    dollar['el'] = laconic;
    context['$'] = dollar;
  }

}(this);

<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

(function() {
  var pkg = 'foam.net.' + (typeof process === 'undefined' ? 'web' : 'node');
  console.log('PKG:', pkg);
  var clss = [
    'HTTPRequest',
    'HTTPResponse'
  ];

  // For each class with a "web" (browser) and "node" (Node JS)
  // implementation, register "foam.net.[environment].[class]" as
  // "foam.net.[class]".
  for ( var i = 0; i < clss.length; i++ ) {
    console.log('Lookup', pkg + '.' + clss[i]);
    foam.register(foam.lookup(pkg + '.' + clss[i]), 'foam.net.' + clss[i]);
  }
})();
<commit_msg>Add ContextFactory TODO to src/lib/net impl unifier script<commit_after>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

(function() {
  var pkg = 'foam.net.' + (typeof process === 'undefined' ? 'web' : 'node');
  console.log('PKG:', pkg);
  var clss = [
    'HTTPRequest',
    'HTTPResponse'
  ];

  // For each class with a "web" (browser) and "node" (Node JS)
  // implementation, register "foam.net.[environment].[class]" as
  // "foam.net.[class]".
  //
  // TODO: This should be provided via a sort of "ContextFactory" or similar.
  for ( var i = 0; i < clss.length; i++ ) {
    console.log('Lookup', pkg + '.' + clss[i]);
    foam.register(foam.lookup(pkg + '.' + clss[i]), 'foam.net.' + clss[i]);
  }
})();
<|endoftext|>


<commit_before>/* eslint no-use-before-define: 0 */

// >>> SORTERS <<<

const sorter = function(direction, a, b) {
  if (a === b) return 0;
  if (a < b) return -direction;
  if (a == null) return 1;
  if (b == null) return -1;

  return direction;
};

/**
 * stringSorter does not support nested property.
 * For nested properties or value transformation (e.g toLowerCase) we should use functionSorter
 * Based on benchmark testing using stringSorter is bit faster then using equivalent function sorter
 * @example sort(users).asc('firstName')
 */
const stringSorter = function(direction, sortBy, a, b) {
  return sorter(direction, a[sortBy], b[sortBy]);
};

/**
 * @example sort(users).asc(p => p.address.city)
 */
const functionSorter = function(direction, sortBy, a, b) {
  return sorter(direction, sortBy(a), sortBy(b));
};

/**
 * Used when we have sorting by multyple properties and when current sorter is function
 * @example sort(users).asc([p => p.address.city, p => p.firstName])
 */
const multiPropFunctionSorter = function(sortBy, thenBy, depth, direction, a, b) {
  return multiPropEqualityHandler(sortBy(a), sortBy(b), thenBy, depth, direction, a, b);
};

/**
 * Used when we have sorting by multyple properties and when current sorter is string
 * @example sort(users).asc(['firstName', 'lastName'])
 */
const multiPropStringSorter = function(sortBy, thenBy, depth, direction, a, b) {
  return multiPropEqualityHandler(a[sortBy], b[sortBy], thenBy, depth, direction, a, b);
};

/**
 * Used with 'by' sorter when we have sorting in multiple direction
 * @example sort(users).asc(['firstName', 'lastName'])
 */
const multiPropObjectSorter = function(sortByObj, thenBy, depth, _direction, a, b) {
  const sortBy = sortByObj.asc || sortByObj.desc;
  const direction = sortByObj.asc ? 1 : -1;

  if (!sortBy) {
    throw Error(`sort: Invalid 'by' sorting configuration.
      Expecting object with 'asc' or 'desc' key`);
  }

  const multiSorter = getMultiPropertySorter(sortBy);
  return multiSorter(sortBy, thenBy, depth, direction, a, b);
};

// >>> HELPERS <<<

/**
 * Return multiProperty sort handler based on sortBy value
 */
const getMultiPropertySorter = function(sortBy) {
  const type = typeof sortBy;
  if (type === 'string') {
    return multiPropStringSorter;
  } else if (type === 'function') {
    return multiPropFunctionSorter;
  }

  return multiPropObjectSorter;
};

const multiPropEqualityHandler = function(valA, valB, thenBy, depth, direction, a, b) {
  if (valA === valB || (valA == null && valB == null)) {
    if (thenBy.length > depth) {
      const multiSorter = getMultiPropertySorter(thenBy[depth]);
      return multiSorter(thenBy[depth], thenBy, depth + 1, direction, a, b);
    }
    return 0;
  }

  return sorter(direction, valA, valB);
};

/**
 * Pick sorter based on provided sortBy value
 */
const sort = function(direction, ctx, sortBy) {
  if (!Array.isArray(ctx)) return ctx;

  // Unwrap sortBy if array with only 1 value
  if (Array.isArray(sortBy) && sortBy.length < 2) {
    [sortBy] = sortBy;
  }

  let _sorter;

  if (!sortBy) {
    _sorter = sorter.bind(undefined, direction);
  } else if (typeof sortBy === 'string') {
    _sorter = stringSorter.bind(undefined, direction, sortBy);
  } else if (typeof sortBy === 'function') {
    _sorter = functionSorter.bind(undefined, direction, sortBy);
  } else {
    _sorter = getMultiPropertySorter(sortBy[0])
      .bind(undefined, sortBy.shift(), sortBy, 0, direction);
  }

  return ctx.sort(_sorter);
};

// >>> PUBLIC <<<

module.exports = function(ctx) {
  return {
    asc: (sortBy) => sort(1, ctx, sortBy),
    desc: (sortBy) => sort(-1, ctx, sortBy),
    by: (sortBy) => {
      if (!Array.isArray(ctx)) return ctx;

      if (!Array.isArray(sortBy)) {
        throw Error(`sort: Invalid usage of 'by' sorter. Array syntax is required.
          Did you mean to use 'asc' or 'desc' sorter instead?`);
      }

      // Unwrap sort by to faster path
      if (sortBy.length === 1) {
        const direction = sortBy[0].asc ? 1 : -1;
        const sortOnProp = sortBy[0].asc || sortBy[0].desc;
        if (!sortOnProp) {
          throw Error(`sort: Invalid 'by' sorting configuration.
            Expecting object with 'asc' or 'desc' key`);
        }
        return sort(direction, ctx, sortOnProp);
      }

      const _sorter = multiPropObjectSorter.bind(undefined, sortBy.shift(), sortBy, 0, undefined);
      return ctx.sort(_sorter);
    }
  };
};
<commit_msg>Fix typo<commit_after>/* eslint no-use-before-define: 0 */

// >>> SORTERS <<<

const sorter = function(direction, a, b) {
  if (a === b) return 0;
  if (a < b) return -direction;
  if (a == null) return 1;
  if (b == null) return -1;

  return direction;
};

/**
 * stringSorter does not support nested property.
 * For nested properties or value transformation (e.g toLowerCase) we should use functionSorter
 * Based on benchmark testing using stringSorter is bit faster then using equivalent function sorter
 * @example sort(users).asc('firstName')
 */
const stringSorter = function(direction, sortBy, a, b) {
  return sorter(direction, a[sortBy], b[sortBy]);
};

/**
 * @example sort(users).asc(p => p.address.city)
 */
const functionSorter = function(direction, sortBy, a, b) {
  return sorter(direction, sortBy(a), sortBy(b));
};

/**
 * Used when we have sorting by multyple properties and when current sorter is function
 * @example sort(users).asc([p => p.address.city, p => p.firstName])
 */
const multiPropFunctionSorter = function(sortBy, thenBy, depth, direction, a, b) {
  return multiPropEqualityHandler(sortBy(a), sortBy(b), thenBy, depth, direction, a, b);
};

/**
 * Used when we have sorting by multiple properties and when current sorter is string
 * @example sort(users).asc(['firstName', 'lastName'])
 */
const multiPropStringSorter = function(sortBy, thenBy, depth, direction, a, b) {
  return multiPropEqualityHandler(a[sortBy], b[sortBy], thenBy, depth, direction, a, b);
};

/**
 * Used with 'by' sorter when we have sorting in multiple direction
 * @example sort(users).asc(['firstName', 'lastName'])
 */
const multiPropObjectSorter = function(sortByObj, thenBy, depth, _direction, a, b) {
  const sortBy = sortByObj.asc || sortByObj.desc;
  const direction = sortByObj.asc ? 1 : -1;

  if (!sortBy) {
    throw Error(`sort: Invalid 'by' sorting configuration.
      Expecting object with 'asc' or 'desc' key`);
  }

  const multiSorter = getMultiPropertySorter(sortBy);
  return multiSorter(sortBy, thenBy, depth, direction, a, b);
};

// >>> HELPERS <<<

/**
 * Return multiProperty sort handler based on sortBy value
 */
const getMultiPropertySorter = function(sortBy) {
  const type = typeof sortBy;
  if (type === 'string') {
    return multiPropStringSorter;
  } else if (type === 'function') {
    return multiPropFunctionSorter;
  }

  return multiPropObjectSorter;
};

const multiPropEqualityHandler = function(valA, valB, thenBy, depth, direction, a, b) {
  if (valA === valB || (valA == null && valB == null)) {
    if (thenBy.length > depth) {
      const multiSorter = getMultiPropertySorter(thenBy[depth]);
      return multiSorter(thenBy[depth], thenBy, depth + 1, direction, a, b);
    }
    return 0;
  }

  return sorter(direction, valA, valB);
};

/**
 * Pick sorter based on provided sortBy value
 */
const sort = function(direction, ctx, sortBy) {
  if (!Array.isArray(ctx)) return ctx;

  // Unwrap sortBy if array with only 1 value
  if (Array.isArray(sortBy) && sortBy.length < 2) {
    [sortBy] = sortBy;
  }

  let _sorter;

  if (!sortBy) {
    _sorter = sorter.bind(undefined, direction);
  } else if (typeof sortBy === 'string') {
    _sorter = stringSorter.bind(undefined, direction, sortBy);
  } else if (typeof sortBy === 'function') {
    _sorter = functionSorter.bind(undefined, direction, sortBy);
  } else {
    _sorter = getMultiPropertySorter(sortBy[0])
      .bind(undefined, sortBy.shift(), sortBy, 0, direction);
  }

  return ctx.sort(_sorter);
};

// >>> PUBLIC <<<

module.exports = function(ctx) {
  return {
    asc: (sortBy) => sort(1, ctx, sortBy),
    desc: (sortBy) => sort(-1, ctx, sortBy),
    by: (sortBy) => {
      if (!Array.isArray(ctx)) return ctx;

      if (!Array.isArray(sortBy)) {
        throw Error(`sort: Invalid usage of 'by' sorter. Array syntax is required.
          Did you mean to use 'asc' or 'desc' sorter instead?`);
      }

      // Unwrap sort by to faster path
      if (sortBy.length === 1) {
        const direction = sortBy[0].asc ? 1 : -1;
        const sortOnProp = sortBy[0].asc || sortBy[0].desc;
        if (!sortOnProp) {
          throw Error(`sort: Invalid 'by' sorting configuration.
            Expecting object with 'asc' or 'desc' key`);
        }
        return sort(direction, ctx, sortOnProp);
      }

      const _sorter = multiPropObjectSorter.bind(undefined, sortBy.shift(), sortBy, 0, undefined);
      return ctx.sort(_sorter);
    }
  };
};
<|endoftext|>


<commit_before>SirTrevor.Locales = {
  en: {
    general: {
      'delete':           'Delete?',
      'drop':             'Drag __block__ here',
      'paste':            'Or paste URL here',
      'upload':           '...or choose a file',
      'close':            'close',
      'position':         'Position',
      'wait':             'Please wait...',
      'link':             'Enter a link'
    },
    errors: {
      'title': "You have the following errors:",
      'validation_fail': "__type__ block is invalid",
      'block_empty': "__name__ must not be empty",
      'type_missing': "You must have a block of type __type__",
      'required_type_empty': "A required block type __type__ is empty",
      'load_fail': "There was a problem loading the contents of the document"
    },
    blocks: {
      text: {
        'title': "Text"
      },
      list: {
        'title': "List"
      },
      quote: {
        'title': "Quote",
        'credit_field': "Credit"
      },
      image: {
        'title': "Image",
        'upload_error': "There was a problem with your upload"
      },
      video: {
        'title': "Video"
      },
      tweet: {
        'title': "Tweet",
        'fetch_error': "There was a problem fetching your tweet"
      },
      embedly: {
        'title': "Embedly",
        'fetch_error': "There was a problem fetching your embed",
        'key_missing': "An Embedly API key must be present"
      },
      heading: {
        'title': "Heading"
      }
    }
  }
};

if (window.i18n === undefined) {
  // Minimal i18n stub that only reads the English strings
  SirTrevor.log("Using i18n stub");
  window.i18n = {
    t: function(key, options) {
      var parts = key.split(':'), str, obj, part, i;

      obj = SirTrevor.Locales[SirTrevor.LANGUAGE];

      for(i = 0; i < parts.length; i++) {
        part = parts[i];

        if(!_.isUndefined(obj[part])) {
          obj = obj[part];
        }
      }

      str = obj;

      if (!_.isString(str)) { return ""; }

      if (str.indexOf('__') >= 0) {
        _.each(options, function(value, opt) {
          str = str.replace('__' + opt + '__', value);
        });
      }

      return str;
    }
  };
} else {
  SirTrevor.log("Using i18next");
  // Only use i18next when the library has been loaded by the user, keeps
  // dependencies slim
  i18n.init({ resStore: SirTrevor.Locales, fallbackLng: SirTrevor.LANGUAGE,
              ns: { namespaces: ['general', 'blocks'], defaultNs: 'general' }
  });
}<commit_msg>stop i18n stub from confusing ourself<commit_after>SirTrevor.Locales = {
  en: {
    general: {
      'delete':           'Delete?',
      'drop':             'Drag __block__ here',
      'paste':            'Or paste URL here',
      'upload':           '...or choose a file',
      'close':            'close',
      'position':         'Position',
      'wait':             'Please wait...',
      'link':             'Enter a link'
    },
    errors: {
      'title': "You have the following errors:",
      'validation_fail': "__type__ block is invalid",
      'block_empty': "__name__ must not be empty",
      'type_missing': "You must have a block of type __type__",
      'required_type_empty': "A required block type __type__ is empty",
      'load_fail': "There was a problem loading the contents of the document"
    },
    blocks: {
      text: {
        'title': "Text"
      },
      list: {
        'title': "List"
      },
      quote: {
        'title': "Quote",
        'credit_field': "Credit"
      },
      image: {
        'title': "Image",
        'upload_error': "There was a problem with your upload"
      },
      video: {
        'title': "Video"
      },
      tweet: {
        'title': "Tweet",
        'fetch_error': "There was a problem fetching your tweet"
      },
      embedly: {
        'title': "Embedly",
        'fetch_error': "There was a problem fetching your embed",
        'key_missing': "An Embedly API key must be present"
      },
      heading: {
        'title': "Heading"
      }
    }
  }
};

if (window.i18n === undefined || window.i18n.init === undefined) {
  // Minimal i18n stub that only reads the English strings
  SirTrevor.log("Using i18n stub");
  window.i18n = {
    t: function(key, options) {
      var parts = key.split(':'), str, obj, part, i;

      obj = SirTrevor.Locales[SirTrevor.LANGUAGE];

      for(i = 0; i < parts.length; i++) {
        part = parts[i];

        if(!_.isUndefined(obj[part])) {
          obj = obj[part];
        }
      }

      str = obj;

      if (!_.isString(str)) { return ""; }

      if (str.indexOf('__') >= 0) {
        _.each(options, function(value, opt) {
          str = str.replace('__' + opt + '__', value);
        });
      }

      return str;
    }
  };
} else {
  SirTrevor.log("Using i18next");
  // Only use i18next when the library has been loaded by the user, keeps
  // dependencies slim
  i18n.init({ resStore: SirTrevor.Locales, fallbackLng: SirTrevor.LANGUAGE,
              ns: { namespaces: ['general', 'blocks'], defaultNs: 'general' }
  });
}
<|endoftext|>


<commit_before>/* exported loadFromKinto */
/* exported saveToKinto */
/* exported BrowserStorageCredentials */

let syncDebounce = null;

const cryptographer = new Jose.WebCryptographer();
cryptographer.setKeyEncryptionAlgorithm('A256KW');
cryptographer.setContentEncryptionAlgorithm('A256GCM');

function shared_key(key) {
  return crypto.subtle.importKey(
    'jwk',
    { kty: key.kty, k: key.k.replace(/=/, '') },
    'AES-KW',
    true,
    ['wrapKey', 'unwrapKey']
  );
}

function encrypt(key, content) {
  const encrypter = new JoseJWE.Encrypter(cryptographer, shared_key(key));
  return encrypter.encrypt(JSON.stringify(content));
}

function decrypt(key, encrypted) {
  const decrypter = new JoseJWE.Decrypter(cryptographer, shared_key(key));
  return decrypter.decrypt(encrypted).then(result => {
    return JSON.parse(result);
  });
}

// An "id schema" used to validate Kinto IDs and generate new ones.
const notesIdSchema = {
  // FIXME: Maybe this should generate IDs?
  generate() {
    throw new Error('cannot generate IDs');
  },

  validate() {
    // FIXME: verify that at least this matches Kinto server ID format
    return true;
  },
};

class ServerKeyNewerError extends Error {
  constructor() {
    super('key used to encrypt the record appears to be newer than our key');
  }
}

class ServerKeyOlderError extends Error {
  constructor() {
    super('key used to encrypt the record appears to be older than our key');
  }
}

class JWETransformer {
  constructor(key) {
    this.key = key;
  }

  async encode(record) {
    // FIXME: should we try to obfuscate the record ID?
    const ciphertext = await encrypt(this.key, record);
    // Copy over the _status field, so that we handle concurrency
    // headers (If-Match, If-None-Match) correctly.
    // DON'T copy over "deleted" status, because then we'd leak
    // plaintext deletes.
    const status = record._status && (record._status === 'deleted' ? 'updated' : record._status);
    const encryptedResult = {
      content: ciphertext,
      id: record.id,
      _status: status,
      kid: this.key.kid,
    };
    if (record.hasOwnProperty('last_modified')) {
      encryptedResult.last_modified = record.last_modified;
    }

    return encryptedResult;
  }

  async decode(record) {
    if (!record.content) {
      // This can happen for tombstones if a record is deleted.
      if (record.deleted) {
        return record;
      }
      throw new Error('No ciphertext: nothing to decrypt?');
    }

    // FIXME: this is hack to work around a bug with FxA keys, which
    // seem to have timestamps embedded in them that are affected by
    // TZ offsets. Take out the substr calls once vladikoff says we can.
    if (record.kid.substr(15) !== this.key.kid.substr(15)) {
      if (this.key.kid < record.kid) {
        throw new ServerKeyNewerError();
      } else {
        throw new ServerKeyOlderError();
      }
    }

    // FIXME: Make sure this is backwards compatible??
    let decoded = await decrypt(this.key, record.content);
    if (!decoded.hasOwnProperty('id')) {
      // Old-style encrypted notes aren't true Kinto records --
      // they're just the content field.
      decoded = {
        content: decoded,
        id: 'singleNote',
      };
    }
    if (record.hasOwnProperty('last_modified')) {
      decoded.last_modified = record.last_modified;
    }

    // _status: deleted records were deleted on a client, but
    // uploaded as an encrypted blob so we don't leak deletions.
    // If we get such a record, flag it as deleted.
    if (decoded._status === 'deleted') {
      decoded.deleted = true;
    }
    return decoded;
  }
}

/**
 * Interface describing a mechanism to fetch credentials.
 */
class Credentials {
  async get() {
    return Promise.reject('Implement me');
  }

  /**
   * Call this if, for example, credentials were invalid.
   */
  async clear() {
    return Promise.reject('Implement me');
  }
}

class BrowserStorageCredentials extends Credentials {
  constructor(storage) {
    super();
    this.storage = storage;
  }

  async get() {
    const data = await this.storage.get(['credentials']);
    return data.credentials;
  }

  async clear() {
    return this.storage.remove('credentials');
  }
}

/**
 * Try to sync our data against the Kinto server.
 *
 * Returns a promise. The promise can reject in case of sync failure
 * or any other reason. This is so that programming errors can be
 * caught more easily in testing. Since this application is
 * offline-first, sync failure should not be a failure for callers.
 */
function syncKinto(client, credentials) {
  // Get credentials and lastmodified
  let collection, credential;
  return credentials.get()
    .then(received => {
      credential = received;
      // XXX: Ask for an refresh token
      // Query Kinto with the Bearer Token
      if (!received) return;
      collection = client
        .collection('notes', {
          idSchema: notesIdSchema,
          remoteTransformers: [new JWETransformer(credential.key)],
        });
      return collection
        .sync({
          headers: { Authorization: `Bearer ${credential.access_token}` },
          strategy: 'manual',
        });
    })
    .then(syncResult => {
      // FIXME: Do we need to do anything with errors, published,
      // updated, etc.?
      return Promise.all(syncResult.conflicts.map(conflict => {
        console.log(conflict);
        let totalOps = conflict.remote.content.ops.slice();
        totalOps.push({ insert: '\n====== On this computer: ======\n\n' });
        totalOps = totalOps.concat(conflict.local.content.ops);
        const resolution = {
          id: conflict.remote.id,
          content: {ops: totalOps},
        };
        return collection.resolve(conflict, resolution);
      }));
      // FIXME: Maybe sync again to "push" the resolution?
    })
    .catch(error => {
      if (error.response && error.response.status === 401) {
        // In case of 401 log the user out.
        // FIXME: Fetch a new token and retry?
        return credentials.clear();
      } else if (error instanceof ServerKeyNewerError) {
        // If the key date is greater than current one, log the user out.
        console.error(error);
        return credentials.clear();
      } else if (error instanceof ServerKeyOlderError) {
        // If the key date is older than the current one, we can't help
        // because there is no way we get the previous key.
        // Flush the server because whatever was there is wrong.
        // FIXME: need to reset sync status.
        console.error(error);
        const kintoHttp = client.api.remote;
        return kintoHttp.bucket('default').deleteCollection('notes', {
          headers: { Authorization: `Bearer ${credential.access_token}` }
        });
      } else {
        console.error(error);
        return Promise.reject(error);
      }
    });
}

function retrieveNote(client) {
  return client.collection('notes', {
    idSchema: notesIdSchema,
  }).getAny('singleNote');
}

/**
 * Try to sync against the Kinto server, and retrieve the current note
 * contents.
 *
 * On completion, a 'kinto-loaded' event will be fired with the
 * following structure:
 *
 * {
 *   action: 'kinto-loaded',
 *   data: the "content" that was previously saved to Kinto, or null
 *     if nothing was previously saved to Kinto (for example, a new
 *     FxA account, or if syncing failed on a fresh profile)
 *   last_modified: the timestamp of the sync, or null
 * }
 */
function loadFromKinto(client, credentials) {
  return syncKinto(client, credentials)
  // Ignore failure of syncKinto by retrieving note even when promise rejected
    .then(() => retrieveNote(client), () => retrieveNote(client))
    .then(result => {
      console.log('Collection had record', result);
      browser.runtime.sendMessage({
        action: 'kinto-loaded',
        data: result ? result.data.content : null,
        last_modified: result ? result.data.last_modified : null,
      });
    });
}

function saveToKinto(client, credentials, content) {
  let resolve;
  const promise = new Promise(thisResolve => {
    resolve = thisResolve;
  });

  // XXX: Debounce the call and set the status to Editing
  browser.runtime.sendMessage('notes@mozilla.com', {
    action: 'text-editing'
  });

  const later = function() {
    syncDebounce = null;
    const notes = client.collection('notes', {
      idSchema: notesIdSchema,
    });
    return notes.upsert({ id: 'singleNote', content })
      .then(() => {
        browser.runtime.sendMessage('notes@mozilla.com', {
          action: 'text-saved'
        });
        return syncKinto(client, credentials);
      })
      .then(() => retrieveNote(client), () => retrieveNote(client))
      .then(result => {
        // Set the status to synced
        return browser.runtime.sendMessage('notes@mozilla.com', {
          action: 'text-synced',
          last_modified: result.data.last_modified,
        });
      })
      .then(() => {
        resolve();
      });
  };

  clearTimeout(syncDebounce);
  syncDebounce = setTimeout(later, 1000);
  return promise;
}
<commit_msg>No longer need to hack around tz offset bug<commit_after>/* exported loadFromKinto */
/* exported saveToKinto */
/* exported BrowserStorageCredentials */

let syncDebounce = null;

const cryptographer = new Jose.WebCryptographer();
cryptographer.setKeyEncryptionAlgorithm('A256KW');
cryptographer.setContentEncryptionAlgorithm('A256GCM');

function shared_key(key) {
  return crypto.subtle.importKey(
    'jwk',
    { kty: key.kty, k: key.k.replace(/=/, '') },
    'AES-KW',
    true,
    ['wrapKey', 'unwrapKey']
  );
}

function encrypt(key, content) {
  const encrypter = new JoseJWE.Encrypter(cryptographer, shared_key(key));
  return encrypter.encrypt(JSON.stringify(content));
}

function decrypt(key, encrypted) {
  const decrypter = new JoseJWE.Decrypter(cryptographer, shared_key(key));
  return decrypter.decrypt(encrypted).then(result => {
    return JSON.parse(result);
  });
}

// An "id schema" used to validate Kinto IDs and generate new ones.
const notesIdSchema = {
  // FIXME: Maybe this should generate IDs?
  generate() {
    throw new Error('cannot generate IDs');
  },

  validate() {
    // FIXME: verify that at least this matches Kinto server ID format
    return true;
  },
};

class ServerKeyNewerError extends Error {
  constructor() {
    super('key used to encrypt the record appears to be newer than our key');
  }
}

class ServerKeyOlderError extends Error {
  constructor() {
    super('key used to encrypt the record appears to be older than our key');
  }
}

class JWETransformer {
  constructor(key) {
    this.key = key;
  }

  async encode(record) {
    // FIXME: should we try to obfuscate the record ID?
    const ciphertext = await encrypt(this.key, record);
    // Copy over the _status field, so that we handle concurrency
    // headers (If-Match, If-None-Match) correctly.
    // DON'T copy over "deleted" status, because then we'd leak
    // plaintext deletes.
    const status = record._status && (record._status === 'deleted' ? 'updated' : record._status);
    const encryptedResult = {
      content: ciphertext,
      id: record.id,
      _status: status,
      kid: this.key.kid,
    };
    if (record.hasOwnProperty('last_modified')) {
      encryptedResult.last_modified = record.last_modified;
    }

    return encryptedResult;
  }

  async decode(record) {
    if (!record.content) {
      // This can happen for tombstones if a record is deleted.
      if (record.deleted) {
        return record;
      }
      throw new Error('No ciphertext: nothing to decrypt?');
    }

    if (record.kid !== this.key.kid) {
      if (this.key.kid < record.kid) {
        throw new ServerKeyNewerError();
      } else {
        throw new ServerKeyOlderError();
      }
    }

    // FIXME: Make sure this is backwards compatible??
    let decoded = await decrypt(this.key, record.content);
    if (!decoded.hasOwnProperty('id')) {
      // Old-style encrypted notes aren't true Kinto records --
      // they're just the content field.
      decoded = {
        content: decoded,
        id: 'singleNote',
      };
    }
    if (record.hasOwnProperty('last_modified')) {
      decoded.last_modified = record.last_modified;
    }

    // _status: deleted records were deleted on a client, but
    // uploaded as an encrypted blob so we don't leak deletions.
    // If we get such a record, flag it as deleted.
    if (decoded._status === 'deleted') {
      decoded.deleted = true;
    }
    return decoded;
  }
}

/**
 * Interface describing a mechanism to fetch credentials.
 */
class Credentials {
  async get() {
    return Promise.reject('Implement me');
  }

  /**
   * Call this if, for example, credentials were invalid.
   */
  async clear() {
    return Promise.reject('Implement me');
  }
}

class BrowserStorageCredentials extends Credentials {
  constructor(storage) {
    super();
    this.storage = storage;
  }

  async get() {
    const data = await this.storage.get(['credentials']);
    return data.credentials;
  }

  async clear() {
    return this.storage.remove('credentials');
  }
}

/**
 * Try to sync our data against the Kinto server.
 *
 * Returns a promise. The promise can reject in case of sync failure
 * or any other reason. This is so that programming errors can be
 * caught more easily in testing. Since this application is
 * offline-first, sync failure should not be a failure for callers.
 */
function syncKinto(client, credentials) {
  // Get credentials and lastmodified
  let collection, credential;
  return credentials.get()
    .then(received => {
      credential = received;
      // XXX: Ask for an refresh token
      // Query Kinto with the Bearer Token
      if (!received) return;
      collection = client
        .collection('notes', {
          idSchema: notesIdSchema,
          remoteTransformers: [new JWETransformer(credential.key)],
        });
      return collection
        .sync({
          headers: { Authorization: `Bearer ${credential.access_token}` },
          strategy: 'manual',
        });
    })
    .then(syncResult => {
      // FIXME: Do we need to do anything with errors, published,
      // updated, etc.?
      return Promise.all(syncResult.conflicts.map(conflict => {
        console.log(conflict);
        let totalOps = conflict.remote.content.ops.slice();
        totalOps.push({ insert: '\n====== On this computer: ======\n\n' });
        totalOps = totalOps.concat(conflict.local.content.ops);
        const resolution = {
          id: conflict.remote.id,
          content: {ops: totalOps},
        };
        return collection.resolve(conflict, resolution);
      }));
      // FIXME: Maybe sync again to "push" the resolution?
    })
    .catch(error => {
      if (error.response && error.response.status === 401) {
        // In case of 401 log the user out.
        // FIXME: Fetch a new token and retry?
        return credentials.clear();
      } else if (error instanceof ServerKeyNewerError) {
        // If the key date is greater than current one, log the user out.
        console.error(error);
        return credentials.clear();
      } else if (error instanceof ServerKeyOlderError) {
        // If the key date is older than the current one, we can't help
        // because there is no way we get the previous key.
        // Flush the server because whatever was there is wrong.
        // FIXME: need to reset sync status.
        console.error(error);
        const kintoHttp = client.api.remote;
        return kintoHttp.bucket('default').deleteCollection('notes', {
          headers: { Authorization: `Bearer ${credential.access_token}` }
        });
      } else {
        console.error(error);
        return Promise.reject(error);
      }
    });
}

function retrieveNote(client) {
  return client.collection('notes', {
    idSchema: notesIdSchema,
  }).getAny('singleNote');
}

/**
 * Try to sync against the Kinto server, and retrieve the current note
 * contents.
 *
 * On completion, a 'kinto-loaded' event will be fired with the
 * following structure:
 *
 * {
 *   action: 'kinto-loaded',
 *   data: the "content" that was previously saved to Kinto, or null
 *     if nothing was previously saved to Kinto (for example, a new
 *     FxA account, or if syncing failed on a fresh profile)
 *   last_modified: the timestamp of the sync, or null
 * }
 */
function loadFromKinto(client, credentials) {
  return syncKinto(client, credentials)
  // Ignore failure of syncKinto by retrieving note even when promise rejected
    .then(() => retrieveNote(client), () => retrieveNote(client))
    .then(result => {
      console.log('Collection had record', result);
      browser.runtime.sendMessage({
        action: 'kinto-loaded',
        data: result ? result.data.content : null,
        last_modified: result ? result.data.last_modified : null,
      });
    });
}

function saveToKinto(client, credentials, content) {
  let resolve;
  const promise = new Promise(thisResolve => {
    resolve = thisResolve;
  });

  // XXX: Debounce the call and set the status to Editing
  browser.runtime.sendMessage('notes@mozilla.com', {
    action: 'text-editing'
  });

  const later = function() {
    syncDebounce = null;
    const notes = client.collection('notes', {
      idSchema: notesIdSchema,
    });
    return notes.upsert({ id: 'singleNote', content })
      .then(() => {
        browser.runtime.sendMessage('notes@mozilla.com', {
          action: 'text-saved'
        });
        return syncKinto(client, credentials);
      })
      .then(() => retrieveNote(client), () => retrieveNote(client))
      .then(result => {
        // Set the status to synced
        return browser.runtime.sendMessage('notes@mozilla.com', {
          action: 'text-synced',
          last_modified: result.data.last_modified,
        });
      })
      .then(() => {
        resolve();
      });
  };

  clearTimeout(syncDebounce);
  syncDebounce = setTimeout(later, 1000);
  return promise;
}
<|endoftext|>